-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
plugin metadata loader with pytest and bash example #136
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
# Constants | ||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
SUFFIX = ".fmf" | ||
MAIN = "main" + SUFFIX | ||
IGNORED_DIRECTORIES = ['/dev', '/proc', '/sys'] | ||
# comma separated list for plugin env var | ||
PLUGIN_ENV = "PLUGINS" | ||
CONFIG_FILE_NAME = "config" | ||
CONFIG_PLUGIN = "plugins" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import importlib | ||
import inspect | ||
import os | ||
import re | ||
from functools import lru_cache | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fmf should support also python2.7 where this is not available. |
||
|
||
import yaml | ||
|
||
from fmf.constants import PLUGIN_ENV, SUFFIX | ||
from fmf.utils import log | ||
|
||
|
||
class Plugin: | ||
""" | ||
Main abstact class for FMF plugins | ||
""" | ||
# you have to define extension list as class attribute e.g. [".py"] | ||
extensions = list() | ||
file_patters = list() | ||
|
||
def read(self, filename): | ||
""" | ||
return python dictionary representation of metadata inside file (FMF structure) | ||
""" | ||
raise NotImplementedError("Define own impementation") | ||
|
||
@staticmethod | ||
def __define_undefined(hierarchy, modified, append): | ||
output = dict() | ||
current = output | ||
for key in hierarchy: | ||
if key not in current or current[key] is None: | ||
current[key] = dict() | ||
current = current[key] | ||
for k, v in modified.items(): | ||
current[k] = v | ||
for k, v in append.items(): | ||
current[k] = v | ||
return output | ||
|
||
def write( | ||
self, filename, hierarchy, data, append_dict, modified_dict, | ||
deleted_items): | ||
""" | ||
Write data in dictionary representation back to file, if not defined, create new fmf file with same name. | ||
When created, nodes will not use plugin method anyway | ||
""" | ||
path = os.path.dirname(filename) | ||
basename = os.path.basename(filename) | ||
current_extension = list( | ||
filter( | ||
lambda x: basename.endswith(x), | ||
self.extensions))[0] | ||
without_extension = basename[0:-len(list(current_extension))] | ||
fmf_file = os.path.join(path, without_extension + ".fmf") | ||
with open(fmf_file, "w") as fd: | ||
yaml.safe_dump( | ||
self.__define_undefined( | ||
hierarchy, | ||
modified_dict, | ||
append_dict), | ||
stream=fd) | ||
|
||
|
||
@lru_cache(maxsize=None) | ||
def enabled_plugins(*plugins): | ||
plugins = os.getenv(PLUGIN_ENV).split( | ||
",") if os.getenv(PLUGIN_ENV) else plugins | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe TMT splits on space, let's be consistent. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I can change it to whatever more consistent solution, I also thought to use |
||
plugin_list = list() | ||
for item in plugins: | ||
if os.path.exists(item): | ||
loader = importlib.machinery.SourceFileLoader( | ||
os.path.basename(item), item) | ||
module = importlib.util.module_from_spec( | ||
importlib.util.spec_from_loader(loader.name, loader) | ||
) | ||
loader.exec_module(module) | ||
else: | ||
module = importlib.import_module(item) | ||
for name, plugin in inspect.getmembers(module): | ||
if inspect.isclass(plugin) and plugin != Plugin and issubclass( | ||
plugin, Plugin): | ||
plugin_list.append(plugin) | ||
log.info("Loaded plugin {}".format(plugin)) | ||
return plugin_list | ||
|
||
|
||
def get_suffixes(*plugins): | ||
output = [SUFFIX] | ||
for item in enabled_plugins(*plugins): | ||
output += item.extensions | ||
return output | ||
|
||
|
||
def get_plugin_for_file(filename, *plugins): | ||
extension = "." + filename.rsplit(".", 1)[1] | ||
for item in enabled_plugins(*plugins): | ||
if extension in item.extensions and any( | ||
filter( | ||
lambda x: re.search( | ||
x, | ||
filename), | ||
item.file_patters)): | ||
log.debug("File {} parsed by by plugin {}".format(filename, item)) | ||
return item |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import os | ||
import re | ||
|
||
from fmf.plugin_loader import Plugin | ||
from fmf.utils import log | ||
|
||
|
||
class Bash(Plugin): | ||
extensions = [".sh"] | ||
file_patters = ["test.*"] | ||
|
||
@staticmethod | ||
def update_data(filename, pattern="^#.*:FMF:"): | ||
out = dict(test="./" + os.path.basename(filename)) | ||
with open(filename) as fd: | ||
for line in fd.readlines(): | ||
if re.match(pattern, line): | ||
item = re.match( | ||
r"{}\s*(.*)".format(pattern), | ||
line).groups()[0] | ||
identifier, value = item.split(":", 1) | ||
out[identifier] = value.lstrip(" ") | ||
return out | ||
|
||
def read(self, file_name): | ||
log.info("Processing Item: {}".format(file_name)) | ||
return self.update_data(file_name) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from fmf.plugins.pytest.plugin import Pytest | ||
from fmf.plugins.pytest.tmt_semantic import TMT | ||
|
||
__all__ = [Pytest.__name__, TMT.__name__] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
CONFIG_POSTPROCESSING_TEST = "test_postprocessing" | ||
PYTEST_DEFAULT_CONF = { | ||
CONFIG_POSTPROCESSING_TEST: { | ||
"test": """ | ||
cls_str = ("::" + str(cls.name)) if cls.name else "" | ||
escaped = shlex.quote(filename + cls_str + "::" + test.name) | ||
f"python3 -m pytest -m '' -v {escaped}" """ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure about pytest as a require :/ As it is now it is a 'hidden' require - you need to remember. But on the other hand it leaves up to user whether rpm or pip install is better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, it is now hidden, you are rigt, and theoretically it is also why I've let it configurable, not hardcoded. in my project |
||
} | ||
} | ||
CONFIG_MERGE_PLUS = "merge_plus" | ||
CONFIG_MERGE_MINUS = "merge_minus" | ||
CONFIG_ADDITIONAL_KEY = "additional_keys" | ||
CONFIG_POSTPROCESSING_TEST = "test_postprocessing" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FMF_PLUGINS
to mitigate name conflictThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sounds reasonable.