Skip to content

Commit

Permalink
Use cnfig inside .fmf dir to load plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
jscotka committed May 25, 2021
1 parent 5ae40a7 commit 42b4965
Show file tree
Hide file tree
Showing 18 changed files with 172 additions and 94 deletions.
36 changes: 24 additions & 12 deletions fmf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@

import fmf.context
import fmf.utils as utils

from io import open
from fmf.utils import log, dict_to_yaml, FileSorting
from fmf.constants import SUFFIX, IGNORED_DIRECTORIES, MAIN
from fmf.plugin_loader import get_suffixes, get_plugin_for_file
from pprint import pformat as pretty
from fmf.constants import (CONFIG_FILE_NAME, CONFIG_PLUGIN,
IGNORED_DIRECTORIES, MAIN, SUFFIX)
from fmf.plugin_loader import get_plugin_for_file, get_suffixes
from fmf.utils import FileSorting, dict_to_yaml, log

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# YAML
Expand Down Expand Up @@ -94,6 +92,7 @@ def __init__(self, data, name=None, parent=None):
self._commit = None
self._raw_data = dict()
self._plugin = None
self._config = dict()
# Track whether the data dictionary has been updated
# (needed to prevent removing nodes with an empty dict).
self._updated = False
Expand All @@ -106,6 +105,7 @@ def __init__(self, data, name=None, parent=None):
# Handle child node creation
else:
self.root = self.parent.root
self._config = self.parent._config
self.name = os.path.join(self.parent.name, name)

# Update data from a dictionary (handle empty nodes)
Expand Down Expand Up @@ -177,6 +177,11 @@ def _initialize(self, path):
"Unable to detect format version: {0}".format(error))
except ValueError:
raise utils.FormatError("Invalid version format")
# try to read fmf config
config_file = os.path.join(self.root, ".fmf", CONFIG_FILE_NAME)
if os.path.exists(config_file):
with open(config_file) as fd:
self._config = yaml.safe_load(fd)

def _merge_plus(self, data, key, value):
""" Handle extending attributes using the '+' suffix """
Expand Down Expand Up @@ -453,8 +458,8 @@ def grow(self, path):
log.debug("Skipping '{0}' (not accessible).".format(path))
return

filenames_sorted = sorted(
[FileSorting(filename) for filename in filenames if any(filter(filename.endswith, get_suffixes()))])
filenames_sorted = sorted([FileSorting(filename) for filename in filenames if any(
filter(filename.endswith, get_suffixes(*self._config.get(CONFIG_PLUGIN, []))))])
# Check every metadata file and load data (ignore hidden)
for filename in [filename.value for filename in filenames_sorted]:
if filename.startswith("."):
Expand All @@ -467,11 +472,14 @@ def grow(self, path):
with open(fullpath, encoding='utf-8') as datafile:
data = yaml.load(datafile, Loader=YamlLoader)
except yaml.error.YAMLError as error:
raise (utils.FileError("Failed to parse '{0}'.\n{1}".format(
fullpath, error)))
raise (
utils.FileError(
"Failed to parse '{0}'.\n{1}".format(
fullpath, error)))
else:
data = None
plugin = get_plugin_for_file(fullpath)
plugin = get_plugin_for_file(
fullpath, *self._config.get(CONFIG_PLUGIN, []))
log.debug("Used plugin {}".format(plugin))
if plugin:
data = plugin().read(fullpath)
Expand All @@ -486,7 +494,11 @@ def grow(self, path):
self.update(data)
# Handle other *.fmf files as children
else:
self.child(os.path.splitext(filename)[0], data, fullpath, plugin=plugin)
self.child(
os.path.splitext(filename)[0],
data,
fullpath,
plugin=plugin)
# Explore every child directory (ignore hidden dirs and subtrees)
for dirname in sorted(dirnames):
if dirname.startswith("."):
Expand Down
2 changes: 2 additions & 0 deletions fmf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
IGNORED_DIRECTORIES = ['/dev', '/proc', '/sys']
# comma separated list for plugin env var
PLUGIN_ENV = "PLUGINS"
CONFIG_FILE_NAME = "config"
CONFIG_PLUGIN = "plugins"
50 changes: 32 additions & 18 deletions fmf/plugin_loader.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from functools import lru_cache
import importlib
import inspect
import copy
import os
import re
from functools import lru_cache

import yaml

from fmf.constants import PLUGIN_ENV, SUFFIX
from fmf.utils import log
import importlib
import os
import re


class Plugin:
Expand All @@ -32,9 +32,9 @@ def __define_undefined(hierarchy, modified, append):
if key not in current or current[key] is None:
current[key] = dict()
current = current[key]
for k,v in modified.items():
for k, v in modified.items():
current[k] = v
for k,v in append.items():
for k, v in append.items():
current[k] = v
return output

Expand All @@ -47,24 +47,33 @@ def write(
"""
path = os.path.dirname(filename)
basename = os.path.basename(filename)
current_extension = list(filter(lambda x: basename.endswith(x), self.extensions))[0]
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)
yaml.safe_dump(
self.__define_undefined(
hierarchy,
modified_dict,
append_dict),
stream=fd)


@lru_cache(maxsize=1)
def enabled_plugins():
plugins = os.getenv(PLUGIN_ENV).split(",") if os.getenv(PLUGIN_ENV) else []
@lru_cache(maxsize=None)
def enabled_plugins(*plugins):
plugins = os.getenv(PLUGIN_ENV).split(
",") if os.getenv(PLUGIN_ENV) else plugins
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)
Expand All @@ -76,16 +85,21 @@ def enabled_plugins():
return plugin_list


def get_suffixes():
def get_suffixes(*plugins):
output = [SUFFIX]
for item in enabled_plugins():
for item in enabled_plugins(*plugins):
output += item.extensions
return output


def get_plugin_for_file(filename):
def get_plugin_for_file(filename, *plugins):
extension = "." + filename.rsplit(".", 1)[1]
for item in enabled_plugins():
if extension in item.extensions and any(filter(lambda x: re.search(x, filename), item.file_patters)):
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
5 changes: 3 additions & 2 deletions fmf/plugins/bash.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os
import re

from fmf.plugin_loader import Plugin
from fmf.utils import log
import re
import os


class Bash(Plugin):
Expand Down
2 changes: 1 addition & 1 deletion fmf/plugins/pytest/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from fmf.plugins.pytest.tmt_semantic import TMT
from fmf.plugins.pytest.plugin import Pytest
from fmf.plugins.pytest.tmt_semantic import TMT
2 changes: 1 addition & 1 deletion fmf/plugins/pytest/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
cls_str = ("::" + str(cls.name)) if cls.name else ""
escaped = shlex.quote(f"{filename}{cls_str}::{test.name}")
f"python3 -m pytest -m '' -v {escaped}" """
}
}
}
CONFIG_MERGE_PLUS = "merge_plus"
CONFIG_MERGE_MINUS = "merge_minus"
Expand Down
60 changes: 37 additions & 23 deletions fmf/plugins/pytest/plugin.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
from fmf.plugin_loader import Plugin
from fmf.utils import log
from fmf.plugins.pytest.constants import PYTEST_DEFAULT_CONF, CONFIG_MERGE_PLUS, CONFIG_MERGE_MINUS, CONFIG_ADDITIONAL_KEY, CONFIG_POSTPROCESSING_TEST
from fmf.plugins.pytest.tmt_semantic import SUMMARY_KEY, DESCRIPTION_KEY, TMT_ATTRIBUTES, fmf_prefixed_name, FMF_POSTFIX_MARKS
import ast
import importlib
import inspect
import re
import os
import re
import shlex
from multiprocessing import Process, Queue
import yaml

import pytest
from fmf_metadata.base import FMF
import ast
from fmf.utils import GeneralError
import shlex
import yaml

from fmf.plugin_loader import Plugin
from fmf.plugins.pytest.constants import (CONFIG_ADDITIONAL_KEY,
CONFIG_MERGE_MINUS,
CONFIG_MERGE_PLUS,
CONFIG_POSTPROCESSING_TEST,
PYTEST_DEFAULT_CONF)
from fmf.plugins.pytest.tmt_semantic import (DESCRIPTION_KEY,
FMF_POSTFIX_MARKS, SUMMARY_KEY,
TMT, TMT_ATTRIBUTES,
fmf_prefixed_name)
from fmf.utils import GeneralError, log

_ = shlex


class _Test:
Expand All @@ -35,7 +44,8 @@ def __init__(self, test_class, filename):


class ItemsCollector:
# current solution based on https://github.com/pytest-dev/pytest/discussions/8554
# current solution based on
# https://github.com/pytest-dev/pytest/discussions/8554
def pytest_collection_modifyitems(self, items):
self.items = items[:]

Expand Down Expand Up @@ -129,7 +139,7 @@ def test_data_dict(test_dict, config, filename, cls, test,
(f"{os.path.basename(filename)} " if filename else "")
+ (f"{cls.name} " if cls.name else "")
+ test.name
)
)
setattr(test.method, current_name, summary)

# set description attribute by docstring if not given by decorator
Expand All @@ -153,7 +163,7 @@ def test_data_dict(test_dict, config, filename, cls, test,
key,
test_dict,
override_postfix,
)
)

# special config items
if CONFIG_ADDITIONAL_KEY in config:
Expand All @@ -162,7 +172,7 @@ def test_data_dict(test_dict, config, filename, cls, test,
if CONFIG_POSTPROCESSING_TEST in config:
__post_processing(
test_dict, config[CONFIG_POSTPROCESSING_TEST], cls, test, filename
)
)
return test_dict


Expand All @@ -177,7 +187,7 @@ def define_undefined(input_dict, keys, config, relative_test_path, cls, test):
filename=relative_test_path,
cls=cls,
test=test,
)
)


def collect(opts):
Expand All @@ -193,21 +203,22 @@ def collect(opts):
kwargs = marker.kwargs

if key == "skip":
FMF.enabled(False)(func)
TMT.enabled(False)(func)
elif key == "skipif":
# add skipif as tag as well (possible to use adjust, but conditions are python code)
# add skipif as tag as well (possible to use adjust, but
# conditions are python code)
arg_string = "SKIP "
if args:
arg_string += " ".join(map(str, args))
if "reason" in kwargs:
arg_string += " " + kwargs["reason"]
FMF.tag(arg_string)(func)
TMT.tag(arg_string)(func)
elif key == "parametrize":
# do nothing, parameters are already part of test name
pass
else:
# generic mark store as tag
FMF.tag(key)(func)
TMT.tag(key)(func)
return plugin_col.items


Expand All @@ -228,7 +239,7 @@ def update_data(store_dict, func, config):
# normalise test name to pytest identifier
test.name = re.search(
f".*({os.path.basename(func.function.__name__)}.*)", func.name
).group(1)
).group(1)
# TODO: removed str_normalise(...) will see what happen
keys.append(test.name)
define_undefined(store_dict, keys, config, filename, cls, test)
Expand Down Expand Up @@ -264,7 +275,7 @@ def import_test_module(filename):
os.path.basename(filename), filename)
module = importlib.util.module_from_spec(
importlib.util.spec_from_loader(loader.name, loader)
)
)
loader.exec_module(module)
return module

Expand All @@ -290,7 +301,10 @@ def write(
contents.pop(start_line + num - 1)
append_dict[k] = v
for k, v in append_dict.items():
contents.insert(start_line, """{}@TMT.{}({})\n""".format(spaces,
k, repr(v)[1:-1] if isinstance(v, list) else repr(v)))
contents.insert(start_line,
"""{}@TMT.{}({})\n""".format(spaces,
k,
repr(v)[1:-1] if isinstance(v,
list) else repr(v)))
with open(filename, "w") as f:
f.writelines(contents)
Loading

0 comments on commit 42b4965

Please sign in to comment.