Skip to content

Commit a92848d

Browse files
committed
add fmf nosetests scheduler and rulesets
1 parent 9ea5f56 commit a92848d

File tree

3 files changed

+205
-0
lines changed

3 files changed

+205
-0
lines changed

fmf_scheduler.py

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/python3
2+
"""
3+
FMF scheduler for nosetests
4+
Found test via fmf files and creates dynamic testclassed based on that.
5+
"""
6+
7+
8+
import os
9+
import sys
10+
import imp
11+
import logging
12+
import unittest
13+
14+
from colin.core.target import Target, is_compatible
15+
from colin.core.checks.fmf_check import ExtendedTree, FMFAbstractCheck
16+
from colin.core.constant import PASSED
17+
from colin.core.ruleset.ruleset import get_checks_path
18+
19+
from colin.core.checks.dockerfile import DockerfileAbstractCheck, DockerfileLabelAbstractCheck,\
20+
InstructionCountAbstractCheck, InstructionAbstractCheck
21+
22+
CLASS_MAPPING = {"DockerfileAbstractCheck": DockerfileAbstractCheck,
23+
"DockerfileLabelAbstractCheck": DockerfileLabelAbstractCheck,
24+
"InstructionCountAbstractCheck": InstructionCountAbstractCheck,
25+
"InstructionAbstractCheck": InstructionAbstractCheck
26+
}
27+
logger = logging.getLogger(__name__)
28+
29+
30+
def get_log_level():
31+
return int(os.environ.get(("DEBUG")) or logging.INFO)
32+
33+
34+
# COPYied from:
35+
# https://eli.thegreenplace.net/2014/04/02/dynamically-generating-python-test-cases
36+
class DynamicClassBase(unittest.TestCase):
37+
"""
38+
Basic Derived test Class
39+
"""
40+
backendclass = None
41+
longMessage = True
42+
43+
44+
def make_check_function(target):
45+
"""
46+
rename check function to test, to be able to find this function via testing frameworks
47+
:return: function
48+
"""
49+
50+
def test(self):
51+
out = self.backendclass().check(target)
52+
if out.status is not PASSED:
53+
raise AssertionError("test:{} -> {}".format(
54+
self.backendclass.name,
55+
str(out))
56+
)
57+
return test
58+
59+
60+
def make_base_fmf_class_abstract(node, target, base_class=DynamicClassBase, fmf_class=FMFAbstractCheck):
61+
class_name = node.data["class"]
62+
out_class_name = node.name.rsplit("/", 1)[-1]
63+
outclass = type(out_class_name, (base_class,), {
64+
'test': make_check_function(target=target),
65+
'backendclass': type(class_name, (fmf_class, CLASS_MAPPING[class_name],), {
66+
'__doc__': node.data.get("description"),
67+
'name': out_class_name,
68+
'metadata': node,
69+
})
70+
})
71+
return outclass
72+
73+
74+
def make_wrapped_fmf_class(node, target):
75+
first_class_name = node.name.rsplit("/", 1)[-1]
76+
second_class_name = node.data.get("class")
77+
modulepath = os.path.join(os.path.dirname(
78+
node.sources[-1]), node.data["test"])
79+
modulename = os.path.basename(
80+
node.sources[-1]).split(".", 1)[0]
81+
# in case of referencing use original data tree for info
82+
if "@" in node.name and not os.path.exists(modulepath):
83+
modulepath = os.path.join(os.path.dirname(
84+
node.sources[-2]), node.data["test"])
85+
modulename = os.path.basename(
86+
node.sources[-2]).split(".", 1)[0]
87+
test_func = make_check_function(target=target)
88+
logger.debug("Try to import: %s from path %s", modulename, modulepath)
89+
moduleimp = imp.load_source(modulename, modulepath)
90+
inernalclass = getattr(moduleimp, second_class_name)
91+
# more verbose output
92+
# full_class_name = '{0}_{1}'.format(first_class_name, second_class_name)
93+
full_class_name = '{0}'.format(first_class_name)
94+
return type(full_class_name, (DynamicClassBase,), {'test': test_func,
95+
'backendclass': inernalclass
96+
})
97+
98+
def nosetests_class_fmf_generator(fmfpath, target_name, log_level, ruleset_tree_path=None, filter_names=None, filters=None):
99+
"""
100+
generates dynamic test classes for nosetest or unittest scheduler based on FMF metadata.
101+
102+
:param fmfpath: path to checks
103+
:param target_name: what is the target object
104+
:param log_level:
105+
:param ruleset_tree_path:
106+
:return:
107+
"""
108+
target = Target(target_name, log_level)
109+
test_classes = {}
110+
if not ruleset_tree_path:
111+
ruleset_tree_path = fmfpath
112+
ruleset_metadatatree = ExtendedTree(ruleset_tree_path)
113+
metadatatree = ExtendedTree(fmfpath)
114+
ruleset_metadatatree.references(metadatatree)
115+
for node in ruleset_metadatatree.prune(names=filter_names, filters=filters):
116+
if node.data.get("class") or node.data.get("test"):
117+
logger.debug("node (%s) contains test and class item", node.name)
118+
119+
if node.data.get("class") in CLASS_MAPPING:
120+
logger.debug("Using pure FMF metadata for %s (class %s)", node.name, node.data.get("class"))
121+
test_class = make_base_fmf_class_abstract(node=node, target=target)
122+
else:
123+
logger.debug("searching for %s", node.name)
124+
test_class = make_wrapped_fmf_class(node=node, target=target)
125+
if is_compatible(target_type=target.target_type, check_instance=test_class.backendclass()):
126+
test_classes[test_class.__name__] = test_class
127+
logger.debug("Test added: %s", node.name)
128+
else:
129+
logger.debug("Test (not target): %s", node.name)
130+
else:
131+
if "__pycache__" not in node.name:
132+
logger.warning("error in fmf config for node (missing test and class items): %s (data: %s) ", node.name, node.data)
133+
return test_classes
134+
135+
136+
def scheduler_opts(target_name=None, checks=None, ruleset_path=None,
137+
filter_names=None, filters=None, log_level=None):
138+
"""
139+
gather all options what have to be set for function class_fmf_generator
140+
now it is able set via ENVVARS
141+
142+
:param target_name: override envvar TARGET
143+
:param checks: override envvar CHECKS
144+
:param ruleset_path: path to directory, where are fmf rulesets
145+
:param filters: dict of filters, filter out just selected cases, use FMF filter format,
146+
via FILTER envvar use ";" as filter separator
147+
:param filter_names: dict of item names for filtering user ";" as separator for NAMES envvar
148+
:return: dict of test classes
149+
"""
150+
if not target_name:
151+
target_name = os.environ.get("TARGET")
152+
if not target_name:
153+
raise EnvironmentError("TARGET envvar is not set.")
154+
if not checks:
155+
checks = get_checks_path()
156+
if not ruleset_path:
157+
ruleset_path = os.environ.get("RULESETPATH")
158+
if not filters:
159+
filters = os.environ.get("FILTERS", "").split(";")
160+
if not filter_names:
161+
filter_names = os.environ.get("NAMES", "").split(";")
162+
if not log_level:
163+
log_level = get_log_level()
164+
output = nosetests_class_fmf_generator(checks, target_name,
165+
ruleset_tree_path=ruleset_path,
166+
filter_names=filter_names,
167+
filters=filters,
168+
log_level=log_level)
169+
return output
170+
171+
172+
if __name__ == "__main__":
173+
logging.basicConfig(stream=sys.stdout, level=get_log_level())
174+
classes = scheduler_opts()
175+
for item in classes:
176+
globals()[item] = classes[item]
177+
178+
# try to schedule it via nosetests in case of direct schedule
179+
import nose
180+
logger.info("number of test classes: %s", len(classes))
181+
module_name = sys.modules[__name__].__file__
182+
logging.debug("running nose for package: %s", module_name)
183+
result = nose.run(argv=[sys.argv[0], module_name, '-v'])
184+
logging.info("all tests ok: %s", result)
185+
else:
186+
classes = scheduler_opts()
187+
for item in classes:
188+
globals()[item] = classes[item]

rulesets/.fmf/version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1

rulesets/default.fmf

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/default:
2+
version: 1
3+
name: "Default ruleset for checking containers/images/dockerfiles."
4+
description: "This set contains general checks applicable to any target."
5+
contact_email: "[email protected]"
6+
names:
7+
8+
/checks:
9+
/check@maintainer_label:
10+
tags+: ["required"]
11+
/check@from_tag_not_latest:
12+
tags+: ["required"]
13+
usable_targets: ["dockerfile"]
14+
/check@maintainer_deprecated:
15+
tags+: ["required"]
16+
usable_targets: ["dockerfile"]

0 commit comments

Comments
 (0)