From 6ae2d5aadbf6a626cf27ca4594a3945e2c249122 Mon Sep 17 00:00:00 2001 From: mhecko Date: Tue, 1 Aug 2023 12:44:47 +0200 Subject: [PATCH 14/38] makefile: add dev_test_no_lint target Add a target for testing individual actors with almost-instant execution time. Testing individual actors currently involves a process in which every actor is instantiated in a separate process, the created instance reports actor information such as actor's name and then exits. As many processes are created, this process is time consuming (cca 7s) which disrupts developer's workflow and causes attention shift. A newly added target `dev_test_no_lint` uses an introduced script `find_actors`. To achieve the similar level of framework protection as spawning actors in a separate process, the `find_actors` script does not execute actors at all, and instead works on their ASTs. Specifically, the script looks for all files named `actor.py`, finds all classes that (explicitely) subclass Actor, and reads its `name` attribute. Usage example: ACTOR=check_target_iso make dev_test_no_lint --- Makefile | 15 +++++--- utils/find_actors.py | 81 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 utils/find_actors.py diff --git a/Makefile b/Makefile index b63192e3..e3c40e01 100644 --- a/Makefile +++ b/Makefile @@ -16,9 +16,12 @@ REPOSITORIES ?= $(shell ls $(_SYSUPG_REPOS) | xargs echo | tr " " ",") SYSUPG_TEST_PATHS=$(shell echo $(REPOSITORIES) | sed -r "s|(,\\|^)| $(_SYSUPG_REPOS)/|g") TEST_PATHS:=commands repos/common $(SYSUPG_TEST_PATHS) +# python version to run test with +_PYTHON_VENV=$${PYTHON_VENV:-python2.7} ifdef ACTOR - TEST_PATHS=`python utils/actor_path.py $(ACTOR)` + TEST_PATHS=`$(_PYTHON_VENV) utils/actor_path.py $(ACTOR)` + APPROX_TEST_PATHS=$(shell $(_PYTHON_VENV) utils/find_actors.py -C repos $(ACTOR)) # Dev only endif ifeq ($(TEST_LIBS),y) @@ -32,9 +35,6 @@ endif # needed only in case the Python2 should be used _USE_PYTHON_INTERPRETER=$${_PYTHON_INTERPRETER} -# python version to run test with -_PYTHON_VENV=$${PYTHON_VENV:-python2.7} - # by default use values you can see below, but in case the COPR_* var is defined # use it instead of the default _COPR_REPO=$${COPR_REPO:-leapp} @@ -127,6 +127,7 @@ help: @echo " - can be changed by setting TEST_CONTAINER env" @echo " test_container_all run lint and tests in all available containers" @echo " test_container_no_lint run tests without linting in container, see test_container" + @echo " dev_test_no_lint (advanced users) run only tests of a single actor specified by the ACTOR variable" @echo " test_container_all_no_lint run tests without linting in all available containers" @echo " clean_containers clean all testing and building container images (to force a rebuild for example)" @echo "" @@ -486,6 +487,10 @@ fast_lint: echo "No files to lint."; \ fi +dev_test_no_lint: + . $(VENVNAME)/bin/activate; \ + $(_PYTHON_VENV) -m pytest $(REPORT_ARG) $(APPROX_TEST_PATHS) $(LIBRARY_PATH) + dashboard_data: . $(VENVNAME)/bin/activate; \ snactor repo find --path repos/; \ @@ -494,4 +499,4 @@ dashboard_data: popd .PHONY: help build clean prepare source srpm copr_build _build_local build_container print_release register install-deps install-deps-fedora lint test_no_lint test dashboard_data fast_lint -.PHONY: test_container test_container_no_lint test_container_all test_container_all_no_lint clean_containers _build_container_image _test_container_ipu +.PHONY: test_container test_container_no_lint test_container_all test_container_all_no_lint clean_containers _build_container_image _test_container_ipu dev_test_no_lint diff --git a/utils/find_actors.py b/utils/find_actors.py new file mode 100644 index 00000000..25cc2217 --- /dev/null +++ b/utils/find_actors.py @@ -0,0 +1,81 @@ +import argparse +import ast +import os +import sys + + +def is_direct_actor_def(ast_node): + if not isinstance(ast_node, ast.ClassDef): + return False + + direcly_named_bases = (base for base in ast_node.bases if isinstance(base, ast.Name)) + for class_base in direcly_named_bases: + # We are looking for direct name 'Actor' + if class_base.id == 'Actor': + return True + + return False + + +def extract_actor_name_from_def(actor_class_def): + assignment_value_class = ast.Str if sys.version_info < (3,8) else ast.Constant + assignment_value_attrib = 's' if sys.version_info < (3,8) else 'value' + + actor_name = None + class_level_assignments = (child for child in actor_class_def.body if isinstance(child, ast.Assign)) + # Search for class-level assignment specifying actor's name: `name = 'name'` + for child in class_level_assignments: + assignment = child + for target in assignment.targets: + assignment_adds_name_attrib = isinstance(target, ast.Name) and target.id == 'name' + assignment_uses_a_constant_string = isinstance(assignment.value, assignment_value_class) + if assignment_adds_name_attrib and assignment_uses_a_constant_string: + rhs = assignment.value # = + actor_name = getattr(rhs, assignment_value_attrib) + break + if actor_name is not None: + break + return actor_name + + +def get_actor_names(actor_path): + with open(actor_path) as actor_file: + try: + actor_def = ast.parse(actor_file.read()) + except SyntaxError: + error = ('Failed to parse {0}. The actor might contain syntax errors, or perhaps it ' + 'is written with Python3-specific syntax?\n') + sys.stderr.write(error.format(actor_path)) + return [] + actor_defs = [ast_node for ast_node in actor_def.body if is_direct_actor_def(ast_node)] + actors = [extract_actor_name_from_def(actor_def) for actor_def in actor_defs] + return actors + + +def make_parser(): + parser = argparse.ArgumentParser() + parser.add_argument('actor_names', nargs='+', + help='Actor names (the name attribute of the actor class) to look for.') + parser.add_argument('-C', '--change-dir', dest='cwd', + help='Path in which the actors will be looked for.', default='.') + return parser + + +if __name__ == '__main__': + parser = make_parser() + args = parser.parse_args() + cwd = os.path.abspath(args.cwd) + actor_names_to_search_for = set(args.actor_names) + + actor_paths = [] + for directory, dummy_subdirs, dir_files in os.walk(cwd): + for actor_path in dir_files: + actor_path = os.path.join(directory, actor_path) + if os.path.basename(actor_path) != 'actor.py': + continue + + defined_actor_names = set(get_actor_names(actor_path)) + if defined_actor_names.intersection(actor_names_to_search_for): + actor_module_path = directory + actor_paths.append(actor_module_path) + print('\n'.join(actor_paths)) -- 2.41.0