From d7fda60d52b2e4262fed1efb1d52c456b839a5e3 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Wed, 31 Oct 2018 16:50:33 +0000 Subject: [PATCH 01/17] exekall: minor log improvements --- tools/exekall/exekall/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/exekall/exekall/main.py b/tools/exekall/exekall/main.py index d4b647841..a2fc1fa86 100755 --- a/tools/exekall/exekall/main.py +++ b/tools/exekall/exekall/main.py @@ -566,7 +566,7 @@ the name of the parameter, the start value, stop value and step size.""") db_loader = adaptor.get_db_loader() - out('\nArtifacts dir: {}'.format(artifact_dir)) + out('\nArtifacts dir: {}\n'.format(artifact_dir)) for testcase in testcase_list: testcase_short_id = take_first(testcase.get_id( @@ -615,7 +615,6 @@ the name of the parameter, the start value, stop value and step size.""") if only_template_scripts: return 0 - out('\n') result_map = collections.defaultdict(list) for testcase, executor in engine.Expression.get_executor_map(testcase_list).items(): exec_start_msg = 'Executing: {short_id}\n\nID: {full_id}\nArtifacts: {folder}'.format( @@ -746,7 +745,8 @@ the name of the parameter, the start value, stop value and step size.""") db.to_path(db_path) out('#'*80) - info('Result summary:\n') + info('Artifacts dir: {}'.format(artifact_dir)) + info('Result summary:') # Display the results adaptor.process_results(result_map) -- GitLab From d4d520fc14dba71837cbaf77279519ff74136c5c Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Wed, 31 Oct 2018 17:06:17 +0000 Subject: [PATCH 02/17] tests: sanity: Add annotated from_testenv() --- lisa/tests/kernel/scheduler/sanity.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lisa/tests/kernel/scheduler/sanity.py b/lisa/tests/kernel/scheduler/sanity.py index fe11bb427..b3a878e30 100644 --- a/lisa/tests/kernel/scheduler/sanity.py +++ b/lisa/tests/kernel/scheduler/sanity.py @@ -17,6 +17,7 @@ import sys +from lisa.env import TestEnv, ArtifactPath from lisa.tests.kernel.test_bundle import TestMetric, ResultBundle, TestBundle from lisa.wlgen.sysbench import Sysbench @@ -51,6 +52,13 @@ class CapacitySanity(TestBundle): return cls(res_dir, te.plat_info, capa_work) + @classmethod + def from_testenv(cls, te:TestEnv, res_dir:ArtifactPath=None) -> 'CapacitySanity': + """ + Factory method to create a bundle using a live target + """ + return super().from_testenv(te, res_dir) + def test_capacity_sanity(self) -> ResultBundle: """ Assert that higher CPU capacity means more work done -- GitLab From c378aa9556b5c6347ccf5da357fe0d2c49677f54 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Wed, 31 Oct 2018 17:53:02 +0000 Subject: [PATCH 03/17] utils: Move ArtifactPath to utils.py Make TestEnv.get_res_dir() return an ArtifactPath, so it can be relocated by exekall. Users won't see any difference since it is a subclass of str. --- lisa/env.py | 63 ++++++++++++++++++++++----- lisa/exekall_customize.py | 54 ++++------------------- lisa/tests/kernel/cpufreq/sanity.py | 3 +- lisa/tests/kernel/hotplug/torture.py | 3 +- lisa/tests/kernel/scheduler/sanity.py | 3 +- lisa/tests/kernel/test_bundle.py | 8 ++-- lisa/utils.py | 41 +++++++++++++++++ 7 files changed, 114 insertions(+), 61 deletions(-) diff --git a/lisa/env.py b/lisa/env.py index 611c4b836..a329fa94c 100644 --- a/lisa/env.py +++ b/lisa/env.py @@ -21,7 +21,6 @@ import os import os.path import contextlib import logging -from pathlib import Path import shlex from collections.abc import Mapping import copy @@ -36,7 +35,7 @@ from devlib.platform.gem5 import Gem5SimulationPlatform from lisa.wlgen.rta import RTA from lisa.energy_meter import EnergyMeter -from lisa.utils import Loggable, MultiSrcConf, HideExekallID, resolve_dotted_name, get_all_subclasses, import_all_submodules, LISA_HOME, StrList +from lisa.utils import Loggable, MultiSrcConf, HideExekallID, resolve_dotted_name, get_all_subclasses, import_all_submodules, LISA_HOME, StrList, ArtifactPath from lisa.platforms.platinfo import PlatformInfo @@ -49,11 +48,46 @@ RESULT_DIR = 'results' LATEST_LINK = 'results_latest' DEFAULT_DEVLIB_MODULES = ['sched', 'cpufreq', 'cpuidle'] -class ArtifactPath(str): +class ArtifactPath(str, Loggable, HideExekallID): """Path to a folder that can be used to store artifacts of a function. This must be a clean folder, already created on disk. """ - pass + def __new__(cls, root, relative, *args, **kwargs): + root = os.path.realpath(str(root)) + relative = str(relative) + # we only support paths relative to the root parameter + assert not os.path.isabs(relative) + absolute = os.path.join(root, relative) + + # Use a resolved absolute path so it is more convenient for users to + # manipulate + path = os.path.realpath(absolute) + + path_str = super().__new__(cls, path, *args, **kwargs) + # Record the actual root, so we can relocate the path later with an + # updated root + path_str.root = root + path_str.relative = relative + return path_str + + def __fspath__(self): + return str(self) + + def __reduce__(self): + # Serialize the path relatively to the root, so it can be relocated + # easily + relative = self.relative_to(self.root) + return (type(self), (self.root, relative)) + + def relative_to(self, path): + return os.path.relpath(str(self), start=str(path)) + + def with_root(self, root): + # Get the path relative to the old root + relative = self.relative_to(self.root) + + # Swap-in the new root and return a new instance + return type(self)(root, relative) class TargetConf(MultiSrcConf, HideExekallID): YAML_MAP_TOP_LEVEL_KEY = 'target-conf' @@ -283,7 +317,7 @@ class TestEnv(Loggable, HideExekallID): args = parser.parse_args(argv) platform_info = PlatformInfo.from_yaml_map(args.platform_info) if args.platform_info else None - target_conf = TargetConf( + target_conf = TargetConf( {k : v for k, v in vars(args).items() if k != "platform_info"}) return TestEnv(target_conf, platform_info) @@ -437,6 +471,13 @@ class TestEnv(Loggable, HideExekallID): """ logger = self.get_logger() + if isinstance(self._res_dir, ArtifactPath): + root = self._res_dir.root + relative = self._res_dir.relative + else: + root = self._res_dir + relative = '' + while True: time_str = datetime.now().strftime('%Y%m%d_%H%M%S.%f') if not name: @@ -444,10 +485,12 @@ class TestEnv(Loggable, HideExekallID): elif append_time: name = "{}-{}".format(name, time_str) - res_dir = os.path.join(self._res_dir, name) + # If we were given an ArtifactPath with an existing root, we + # preserve that root so it can be relocated as the caller wants it + res_dir = ArtifactPath(root, os.path.join(relative,name)) # Compute base installation path - logger.info('Creating result directory: %s', res_dir) + logger.info('Creating result directory: %s', str(res_dir)) # It will fail if the folder already exists. In that case, # append_time should be used to ensure we get a unique name. @@ -464,14 +507,14 @@ class TestEnv(Loggable, HideExekallID): raise if symlink: - res_lnk = Path(LISA_HOME, LATEST_LINK) + res_lnk = os.path.join(LISA_HOME, LATEST_LINK) with contextlib.suppress(FileNotFoundError): - res_lnk.unlink() + os.remove(res_lnk) # There may be a race condition with another tool trying to create # the link with contextlib.suppress(FileExistsError): - res_lnk.symlink_to(res_dir) + os.symlink(res_dir, res_lnk) return res_dir diff --git a/lisa/exekall_customize.py b/lisa/exekall_customize.py index 236255819..97236cf72 100644 --- a/lisa/exekall_customize.py +++ b/lisa/exekall_customize.py @@ -27,9 +27,9 @@ from pathlib import Path import xml.etree.ElementTree as ET import traceback -from lisa.env import TestEnv, TargetConf, ArtifactPath +from lisa.env import TestEnv, TargetConf from lisa.platforms.platinfo import PlatformInfo -from lisa.utils import HideExekallID, Loggable +from lisa.utils import HideExekallID, Loggable, ArtifactPath from lisa.tests.kernel.test_bundle import TestBundle, Result, ResultBundle, CannotCreateError from exekall import utils, engine @@ -37,45 +37,9 @@ from exekall.engine import reusable, ExprData, Consumer, PrebuiltOperator, NoVal from exekall.customization import AdaptorBase @reusable(False) -class ArtifactStorage(ArtifactPath, Loggable, HideExekallID): - def __new__(cls, root, relative, *args, **kwargs): - root = Path(root).resolve() - relative = Path(relative) - # we only support paths relative to the root parameter - assert not relative.is_absolute() - absolute = root/relative - - # Use a resolved absolute path so it is more convenient for users to - # manipulate - path = absolute.resolve() - - path_str = super().__new__(cls, str(path), *args, **kwargs) - # Record the actual root, so we can relocate the path later with an - # updated root - path_str.root = root - return path_str - - def __fspath__(self): - return str(self) - - def __reduce__(self): - # Serialize the path relatively to the root, so it can be relocated - # easily - relative = self.relative_to(self.root) - return (type(self), (self.root, relative)) - - def relative_to(self, path): - return os.path.relpath(str(self), start=str(path)) - - def with_root(self, root): - # Get the path relative to the old root - relative = self.relative_to(self.root) - - # Swap-in the new root and return a new instance - return type(self)(root, relative) - +class ExekallArtifactPath(ArtifactPath): @classmethod - def from_expr_data(cls, data:ExprData, consumer:Consumer) -> 'ArtifactStorage': + def from_expr_data(cls, data:ExprData, consumer:Consumer) -> 'ExekallArtifactPath': """ Factory used when running under `exekall` """ @@ -143,12 +107,12 @@ class LISAAdaptor(AdaptorBase): @classmethod def load_db(cls, db_path, *args, **kwargs): - # This will relocate ArtifactStorage instances to the new absolute path - # of the results folder, in case it has been moved to another place + # This will relocate ArtifactPath instances to the new absolute path of + # the results folder, in case it has been moved to another place artifact_dir = Path(db_path).parent.resolve() db = engine.StorageDB.from_path(db_path, *args, **kwargs) - # Relocate ArtifactStorage embeded in objects so they will always + # Relocate ArtifactPath embeded in objects so they will always # contain an absolute path that adapts to the local filesystem for serial in db.obj_store.get_all(): val = serial.value @@ -157,7 +121,7 @@ class LISAAdaptor(AdaptorBase): except AttributeError: continue for attr, attr_val in dct.items(): - if isinstance(attr_val, ArtifactStorage): + if isinstance(attr_val, ArtifactPath): setattr(val, attr, attr_val.with_root(artifact_dir) ) @@ -175,7 +139,7 @@ class LISAAdaptor(AdaptorBase): # Add symlinks to artifact folders for ExprValue that were used in the # ExprValue graph, but were initially computed for another Expression - if isinstance(val, ArtifactStorage): + if isinstance(val, ArtifactPath): val = Path(val) is_subfolder = (testcase_artifact_dir.resolve() in val.resolve().parents) # The folder is reachable from our ExprValue, but is not a diff --git a/lisa/tests/kernel/cpufreq/sanity.py b/lisa/tests/kernel/cpufreq/sanity.py index 830c509f0..c2b1d7a58 100644 --- a/lisa/tests/kernel/cpufreq/sanity.py +++ b/lisa/tests/kernel/cpufreq/sanity.py @@ -17,7 +17,8 @@ from lisa.tests.kernel.test_bundle import Result, ResultBundle, TestBundle from lisa.wlgen.sysbench import Sysbench -from lisa.env import TestEnv, ArtifactPath +from lisa.env import TestEnv +from lisa.utils import ArtifactPath class UserspaceSanity(TestBundle): """ diff --git a/lisa/tests/kernel/hotplug/torture.py b/lisa/tests/kernel/hotplug/torture.py index eea607c5b..096a90f1f 100644 --- a/lisa/tests/kernel/hotplug/torture.py +++ b/lisa/tests/kernel/hotplug/torture.py @@ -24,7 +24,8 @@ from devlib.exception import TimeoutError from lisa.tests.kernel.test_bundle import TestMetric, ResultBundle, TestBundle from lisa.target_script import TargetScript -from lisa.env import TestEnv, ArtifactPath +from lisa.env import TestEnv +from lisa.utils import ArtifactPath class HotplugTorture(TestBundle): diff --git a/lisa/tests/kernel/scheduler/sanity.py b/lisa/tests/kernel/scheduler/sanity.py index b3a878e30..978a15772 100644 --- a/lisa/tests/kernel/scheduler/sanity.py +++ b/lisa/tests/kernel/scheduler/sanity.py @@ -17,7 +17,8 @@ import sys -from lisa.env import TestEnv, ArtifactPath +from lisa.env import TestEnv +from lisa.utils import ArtifactPath from lisa.tests.kernel.test_bundle import TestMetric, ResultBundle, TestBundle from lisa.wlgen.sysbench import Sysbench diff --git a/lisa/tests/kernel/test_bundle.py b/lisa/tests/kernel/test_bundle.py index 18d732b09..2b435a362 100644 --- a/lisa/tests/kernel/test_bundle.py +++ b/lisa/tests/kernel/test_bundle.py @@ -28,8 +28,8 @@ from lisa.trace import Trace from lisa.wlgen.rta import RTA from lisa.perf_analysis import PerfAnalysis -from lisa.utils import Serializable, memoized -from lisa.env import TestEnv, ArtifactPath +from lisa.utils import Serializable, memoized, ArtifactPath +from lisa.env import TestEnv from lisa.platforms.platinfo import PlatformInfo class TestMetric: @@ -300,9 +300,11 @@ class TestBundle(Serializable, abc.ABC): @classmethod def from_dir(cls, res_dir): - """ + """ See :meth:`lisa.utils.Serializable.from_path` """ + res_dir = ArtifactPath(root=res_dir, relative='') + bundle = super().from_path(cls._filepath(res_dir)) # We need to update the res_dir to the one we were given bundle.res_dir = res_dir diff --git a/lisa/utils.py b/lisa/utils.py index ec53ef6e9..87e42b199 100644 --- a/lisa/utils.py +++ b/lisa/utils.py @@ -923,4 +923,45 @@ def setup_logging(filepath='logging.conf', level=logging.INFO): logging.info('Using LISA logging configuration:') logging.info(' %s', filepath) +class ArtifactPath(str, Loggable, HideExekallID): + """Path to a folder that can be used to store artifacts of a function. + This must be a clean folder, already created on disk. + """ + def __new__(cls, root, relative, *args, **kwargs): + root = os.path.realpath(str(root)) + relative = str(relative) + # we only support paths relative to the root parameter + assert not os.path.isabs(relative) + absolute = os.path.join(root, relative) + + # Use a resolved absolute path so it is more convenient for users to + # manipulate + path = os.path.realpath(absolute) + + path_str = super().__new__(cls, path, *args, **kwargs) + # Record the actual root, so we can relocate the path later with an + # updated root + path_str.root = root + path_str.relative = relative + return path_str + + def __fspath__(self): + return str(self) + + def __reduce__(self): + # Serialize the path relatively to the root, so it can be relocated + # easily + relative = self.relative_to(self.root) + return (type(self), (self.root, relative)) + + def relative_to(self, path): + return os.path.relpath(str(self), start=str(path)) + + def with_root(self, root): + # Get the path relative to the old root + relative = self.relative_to(self.root) + + # Swap-in the new root and return a new instance + return type(self)(root, relative) + # vim :set tabstop=4 shiftwidth=4 textwidth=80 expandtab -- GitLab From 93bceefef704f554723b9e5721d14b37fb57fc19 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Wed, 31 Oct 2018 17:54:13 +0000 Subject: [PATCH 04/17] misc: add/remove some TODO --- lisa/exekall_customize.py | 1 - lisa/utils.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lisa/exekall_customize.py b/lisa/exekall_customize.py index 97236cf72..b1e4e2af0 100644 --- a/lisa/exekall_customize.py +++ b/lisa/exekall_customize.py @@ -251,7 +251,6 @@ class LISAAdaptor(AdaptorBase): if isinstance(value, ResultBundle): result = RESULT_TAG_MAP[value.result] short_msg = value.result.lower_name - #TODO: add API to ResultBundle to print the message without the Result msg = str(value) type_ = type(value) diff --git a/lisa/utils.py b/lisa/utils.py index 87e42b199..cacff7727 100644 --- a/lisa/utils.py +++ b/lisa/utils.py @@ -398,6 +398,7 @@ class DeferredValue: return ''.format(self.callback.__qualname__) class MultiSrcConf(SerializableConfABC, Loggable, Mapping): + #TODO: also add a help string in the structure and derive a help paragraph @abc.abstractmethod def STRUCTURE(): """ -- GitLab From 5987a4a03cdc04ef07766b7eb11c240627670054 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Wed, 31 Oct 2018 17:54:30 +0000 Subject: [PATCH 05/17] exekall: improve ID handling --- tools/exekall/exekall/engine.py | 13 +++++++------ tools/exekall/exekall/main.py | 7 ++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tools/exekall/exekall/engine.py b/tools/exekall/exekall/engine.py index 2443e9a27..3c7a446cf 100644 --- a/tools/exekall/exekall/engine.py +++ b/tools/exekall/exekall/engine.py @@ -1811,10 +1811,11 @@ class SerializableExprValue: # Pre-compute all the IDs so they are readily available once the value # is deserialized self.recorded_id_map = dict() - for full_qual, with_tags in itertools.product((True, False), repeat=2): - self.recorded_id_map[(full_qual, with_tags)] = expr_val.get_id( - full_qual = full_qual, - with_tags = with_tags, + for full_qual, qual, with_tags in itertools.product((True, False), repeat=3): + self.recorded_id_map[(full_qual, qual, with_tags)] = expr_val.get_id( + full_qual=full_qual, + qual=qual, + with_tags=with_tags, hidden_callable_set=hidden_callable_set, ) @@ -1832,8 +1833,8 @@ class SerializableExprValue: ) self.param_value_map[param] = param_serialzable - def get_id(self, full_qual=True, with_tags=True): - args = (full_qual, with_tags) + def get_id(self, full_qual=True, qual=True, with_tags=True): + args = (full_qual, qual, with_tags) return self.recorded_id_map[args] def get_parent_set(self, predicate, _parent_set=None): diff --git a/tools/exekall/exekall/main.py b/tools/exekall/exekall/main.py index a2fc1fa86..bcde6daf5 100755 --- a/tools/exekall/exekall/main.py +++ b/tools/exekall/exekall/main.py @@ -372,7 +372,7 @@ the name of the parameter, the start value, stop value and step size.""") serial_list = list(group) type_ = type(serial_list[0].value) - id_ = serial_list[0].get_id(full_qual=False, with_tags=True) + id_ = serial_list[0].get_id(qual=False, with_tags=True) prebuilt_op_pool_list.append( engine.PrebuiltOperator( type_, serial_list, id_=id_, @@ -537,8 +537,9 @@ the name of the parameter, the start value, stop value and step size.""") testcase_list = [ testcase for testcase in testcase_list if fnmatch.fnmatch(take_first(testcase.get_id( - # These options need to match what --dry-run gives - full_qual=verbose, + # These options need to match what --dry-run gives (unless + # verbose is used) + full_qual=False, qual=False, hidden_callable_set=hidden_callable_set)), user_filter) ] -- GitLab From e1824e9ab148af2fa6e79faa0347d5223e729e0e Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Wed, 31 Oct 2018 18:08:44 +0000 Subject: [PATCH 06/17] exekall: fix PrebuiltOperator ID --- tools/exekall/exekall/engine.py | 4 ++-- tools/exekall/exekall/main.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/exekall/exekall/engine.py b/tools/exekall/exekall/engine.py index 3c7a446cf..02a813eec 100644 --- a/tools/exekall/exekall/engine.py +++ b/tools/exekall/exekall/engine.py @@ -1849,10 +1849,10 @@ class SerializableExprValue: def get_name(obj, full_qual=True, qual=True): # full_qual enabled implies qual enabled - qual = qual or full_qual - + _qual = qual or full_qual # qual disabled implies full_qual disabled full_qual = full_qual and qual + qual = _qual # Add the module's name in front of the name to get a fully # qualified name diff --git a/tools/exekall/exekall/main.py b/tools/exekall/exekall/main.py index bcde6daf5..f4300f7fb 100755 --- a/tools/exekall/exekall/main.py +++ b/tools/exekall/exekall/main.py @@ -372,7 +372,14 @@ the name of the parameter, the start value, stop value and step size.""") serial_list = list(group) type_ = type(serial_list[0].value) - id_ = serial_list[0].get_id(qual=False, with_tags=True) + id_ = serial_list[0].get_id( + full_qual=False, + qual=False, + # Do not include the tags to avoid having them displayed + # twice, and to avoid wrongfully using the tag of the first + # item in the list for all items. + with_tags=False, + ) prebuilt_op_pool_list.append( engine.PrebuiltOperator( type_, serial_list, id_=id_, -- GitLab From 947b1ac1f9ef787cd63a003dca632bebd67fba0f Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Fri, 2 Nov 2018 11:27:01 +0000 Subject: [PATCH 07/17] env: Refactor and augment TestEnv.from_cli * add --target-conf * add --res-dir * add --log-level: If the option is specified, lisa.utils.setup_logging will be called. * Renamed --hostname into --host to match the config key, and also because it is not necessarily a hostname but can also be an IP address * Made --host available for both Linux and Android targets since it is relevant in both cases * Make the parser simpler to maintain --- lisa/env.py | 116 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/lisa/env.py b/lisa/env.py index a329fa94c..488274fdc 100644 --- a/lisa/env.py +++ b/lisa/env.py @@ -35,7 +35,7 @@ from devlib.platform.gem5 import Gem5SimulationPlatform from lisa.wlgen.rta import RTA from lisa.energy_meter import EnergyMeter -from lisa.utils import Loggable, MultiSrcConf, HideExekallID, resolve_dotted_name, get_all_subclasses, import_all_submodules, LISA_HOME, StrList, ArtifactPath +from lisa.utils import Loggable, MultiSrcConf, HideExekallID, resolve_dotted_name, get_all_subclasses, import_all_submodules, LISA_HOME, StrList, setup_logging from lisa.platforms.platinfo import PlatformInfo @@ -254,7 +254,7 @@ class TestEnv(Loggable, HideExekallID): return cls(target_conf=target_conf, plat_info=plat_info) @classmethod - def from_cli(cls, argv=None): + def from_cli(cls, argv=None) -> 'TestEnv': """ Create a TestEnv from command line arguments. @@ -266,61 +266,79 @@ class TestEnv(Loggable, HideExekallID): to be confusing (help message woes, argument clashes...), so for now this should only be used in scripts that only expect TestEnv args. """ - # Subparsers cannot let us specify --kind=android, at best we could - # have --android which is lousy. Instead, use a first parser to figure - # out the target kind, then create a new parser for that specific kind. - kind_parser = argparse.ArgumentParser( - # Disable the automatic help to not catch e.g. ./script.py -k linux -h - add_help=False, + parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent( """ - Extra arguments differ depending on the value passed to 'kind'. - Try e.g. "--kind android -h" to see the arguments for android targets. - """)) - - kind_parser.add_argument( - "--kind", "-k", choices=["android", "linux", "host"], - help="The kind of target to create") - - # Add a self-managed help argument, see why below - kind_parser.add_argument("--help", "-h", action="store_true") - - args = kind_parser.parse_known_args(argv)[0] - - # Print the generic help only if we can't print the proper --kind help - if not args.kind or (args.help and not args.kind): - kind_parser.print_help() - sys.exit(2) - - kind = args.kind - - parser = argparse.ArgumentParser() - parser.add_argument("--kind", "-k", - choices=[kind], - required=True, - help="The kind of target to create") - - if kind == "android": - parser.add_argument("--device", "-d", type=str, required=True, - help="The ADB ID of the target") - elif kind == "linux": - parser.add_argument("--hostname", "-n", type=str, required=True, dest="host", - help="The hostname/IP of the target") - parser.add_argument("--username", "-u", type=str, required=True, - help="Login username") - parser.add_argument("--password", "-p", type=str, required=True, - help="Login password") - - parser.add_argument("--platform-info", "-pi", type=str, - help="Path to a PlatformInfo yaml file") + Connect to a target using the provided configuration in order + to run a test. + + EXAMPLES + + --target-conf can point to a YAML target configuration file + with all the necessary connection information: + $ {script} --target-conf my_target.yml + + Alternatively, --kind must be set along the relevant credentials: + $ {script} --kind linux --host 192.0.2.1 --username root --password root + + In both cases, --platform-info can point to a PlatformInfo YAML + file. + + """.format( + script=os.path.basename(sys.argv[0]) + ))) + + + kind_group = parser.add_mutually_exclusive_group(required=True) + kind_group.add_argument("--kind", "-k", + choices=["android", "linux", "host"], + help="The kind of target to connect to.") + + kind_group.add_argument("--target-conf", "-t", + help="Path to a TargetConf yaml file. Superseeds other target connection related options.") + + device_group = parser.add_mutually_exclusive_group() + device_group.add_argument("--device", "-d", + help="The ADB ID of the target. Superseeds --host. Only applies to Android kind.") + device_group.add_argument("--host", "-n", + help="The hostname/IP of the target.") + + parser.add_argument("--username", "-u", + help="Login username. Only applies to Linux kind.") + parser.add_argument("--password", "-p", + help="Login password. Only applies to Linux kind.") + + parser.add_argument("--platform-info", "-pi", + help="Path to a PlatformInfo yaml file.") + + parser.add_argument("--log-level", + choices=('warning', 'info', 'debug'), + help="Verbosity level of the logs.") + + parser.add_argument("--res-dir", "-o", + help="Result directory of the created TestEnv. If no directory is specified, a default location under $LISA_HOME will be used.") + + # Options that are not a key in TargetConf must be listed here + not_target_conf_opt = ( + 'platform_info', 'log_level', 'res_dir', 'target_conf', + ) args = parser.parse_args(argv) + if args.log_level: + setup_logging(level=args.log_level.upper()) + + if args.kind and not (args.host or args.device): + parser.error('--host or --device must be specified') + platform_info = PlatformInfo.from_yaml_map(args.platform_info) if args.platform_info else None + if args.target_conf: + target_conf = TargetConf.from_yaml_map(args.target_conf) + else: target_conf = TargetConf( - {k : v for k, v in vars(args).items() if k != "platform_info"}) + {k : v for k, v in vars(args).items() if k not in not_target_conf_opt}) - return TestEnv(target_conf, platform_info) + return cls(target_conf, platform_info, res_dir=args.res_dir) def _init_target(self, target_conf, res_dir): """ -- GitLab From b82cbbeca8f54e5947075d593385b3a93902f9c0 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Fri, 2 Nov 2018 11:29:47 +0000 Subject: [PATCH 08/17] env: TestEnv enforce empty or non-existing res_dir TestEnv requires either a path not already used by a file or folder, or an empty folder. That ensures the created folder is empty, to avoid misuse of using the same res_dir for multiple TestEnv which is not supported and will lead to overwritten files. --- lisa/env.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lisa/env.py b/lisa/env.py index 488274fdc..cb2f76056 100644 --- a/lisa/env.py +++ b/lisa/env.py @@ -198,6 +198,8 @@ class TestEnv(Loggable, HideExekallID): self._res_dir = res_dir if self._res_dir: os.makedirs(self._res_dir, exist_ok=True) + if os.listdir(self._res_dir): + raise ValueError('res_dir must be empty: {}'.format(self._res_dir)) self.target_conf = target_conf logger.debug('Target configuration %s', self.target_conf) -- GitLab From 3e500769d9e33502a577707a8cfbd811c7953ed5 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Fri, 2 Nov 2018 11:33:44 +0000 Subject: [PATCH 09/17] test_bundle: Add ResultBundle.display_and_exit Add a display_and_exit() and exit method to be called in simple scripts only executing one simple test. Also add exekall option --callable-goal that is a pattern matching callable names. This allows exporting our set of tests as a collection of independent scripts, each executing one test and exiting through the same helper. That is obviously much slower than running all tests under exekall, but can be useful to check the structure of a test or to execute them in another environment. --- lisa/exekall_customize.py | 8 ++- lisa/tests/kernel/test_bundle.py | 8 +++ tools/exekall/exekall/customization.py | 4 ++ tools/exekall/exekall/engine.py | 9 ++- tools/exekall/exekall/main.py | 76 ++++++++++++++++---------- tools/exekall/exekall/utils.py | 2 +- 6 files changed, 73 insertions(+), 34 deletions(-) diff --git a/lisa/exekall_customize.py b/lisa/exekall_customize.py index b1e4e2af0..d087b219a 100644 --- a/lisa/exekall_customize.py +++ b/lisa/exekall_customize.py @@ -33,7 +33,7 @@ from lisa.utils import HideExekallID, Loggable, ArtifactPath from lisa.tests.kernel.test_bundle import TestBundle, Result, ResultBundle, CannotCreateError from exekall import utils, engine -from exekall.engine import reusable, ExprData, Consumer, PrebuiltOperator, NoValue, get_name +from exekall.engine import reusable, ExprData, Consumer, PrebuiltOperator, NoValue, get_name, get_mro from exekall.customization import AdaptorBase @reusable(False) @@ -102,6 +102,10 @@ class LISAAdaptor(AdaptorBase): parser.add_argument('--platform-info', help="Platform info file") + @staticmethod + def get_default_type_goal_pattern_set(): + return {'*.ResultBundle'} + def get_db_loader(self): return self.load_db @@ -270,7 +274,7 @@ def append_result_tag(et_testcase, result, type_, short_msg, msg): type=get_name(type_, full_qual=True), type_bases=','.join( get_name(type_, full_qual=True) - for type_ in inspect.getmro(type_) + for type_ in get_mro(type_) ), message=str(short_msg), )) diff --git a/lisa/tests/kernel/test_bundle.py b/lisa/tests/kernel/test_bundle.py index 2b435a362..b56b222a5 100644 --- a/lisa/tests/kernel/test_bundle.py +++ b/lisa/tests/kernel/test_bundle.py @@ -19,6 +19,7 @@ import enum import os import os.path import abc +import sys from collections.abc import Mapping from devlib.target import KernelVersion @@ -141,6 +142,13 @@ class ResultBundle: """ self.metrics[name] = TestMetric(data, units) + def display_and_exit(self) -> type(None): + print("Test result: {}".format(self)) + if self: + sys.exit(0) + else: + sys.exit(1) + class CannotCreateError(RuntimeError): """ Something prevented the creation of a :class:`TestBundle` instance diff --git a/tools/exekall/exekall/customization.py b/tools/exekall/exekall/customization.py index e988275cc..bf91b331b 100644 --- a/tools/exekall/exekall/customization.py +++ b/tools/exekall/exekall/customization.py @@ -60,6 +60,10 @@ class AdaptorBase: def register_cli_param(parser): pass + @staticmethod + def get_default_type_goal_pattern_set(): + return {'*Result'} + def resolve_cls_name(self, goal): return engine.get_class_from_name(goal, sys.modules) diff --git a/tools/exekall/exekall/engine.py b/tools/exekall/exekall/engine.py index 02a813eec..7c3a1b3de 100644 --- a/tools/exekall/exekall/engine.py +++ b/tools/exekall/exekall/engine.py @@ -63,6 +63,13 @@ def get_type_hints(f, module_vars=None): return resolve_annotations(f.__annotations__, module_vars) +def get_mro(cls): + assert isinstance(cls, type) + if issubclass(cls, type(None)): + return (type(None), object) + else: + return inspect.getmro(cls) + def resolve_annotations(annotations, module_vars): return { # If we get a string, evaluate it in the global namespace of the @@ -1821,7 +1828,7 @@ class SerializableExprValue: self.type_names = [ get_name(type_, full_qual=True) - for type_ in inspect.getmro(expr_val.expr.op.value_type) + for type_ in get_mro(expr_val.expr.op.value_type) if type_ is not object ] diff --git a/tools/exekall/exekall/main.py b/tools/exekall/exekall/main.py index f4300f7fb..bb8af3b07 100755 --- a/tools/exekall/exekall/main.py +++ b/tools/exekall/exekall/main.py @@ -102,10 +102,15 @@ refined with --load-type.""") help="""Load the parameters of the values that were used to compute the given UUID from the database.""") - run_parser.add_argument('--goal', default='*ResultBundle', + goal_group = run_parser.add_mutually_exclusive_group() + goal_group.add_argument('--goal', action='append', help="""Compute expressions leading to an instance of the specified class or a subclass of it.""") + goal_group.add_argument('--callable-goal', action='append', + help="""Compute expressions ending with a callable which name is +matching this pattern.""") + run_parser.add_argument('--sweep', nargs=5, action='append', default=[], metavar=('CALLABLE', 'PARAM', 'START', 'STOP', 'STEP'), help="""Parametric sweep on a function parameter. @@ -217,7 +222,11 @@ the name of the parameter, the start value, stop value and step size.""") dry_run = args.dry_run only_template_scripts = args.template_scripts - goal_pattern = args.goal + type_goal_pattern = args.goal + callable_goal_pattern = args.callable_goal + + if not (type_goal_pattern or callable_goal_pattern): + type_goal_pattern = set(adaptor_cls.get_default_type_goal_pattern_set()) load_db_path = args.load_db load_db_pattern_list = args.load_type @@ -425,8 +434,9 @@ the name of the parameter, the start value, stop value and step size.""") # dependended upon as well. cls_set = set() for produced in produced_pool: - cls_set.update(inspect.getmro(produced)) + cls_set.update(engine.get_mro(produced)) cls_set.discard(object) + cls_set.discard(type(None)) # Map all types to the subclasses that can be used when the type is # requested. @@ -441,23 +451,23 @@ the name of the parameter, the start value, stop value and step size.""") cls_map = adaptor.filter_cls_map(cls_map) - # Augment the list of classes that can only be provided by a prebuilt - # Operator with all the compatible classes + # Augment the list of classes that can only be provided by a prebuilt + # Operator with all the compatible classes only_prebuilt_cls_ = set() for cls in only_prebuilt_cls: only_prebuilt_cls_.update(cls_map[cls]) only_prebuilt_cls = only_prebuilt_cls_ # Map of all produced types to a set of what operator can create them - op_map = dict() - for op in op_pool: - param_map, produced = op.get_prototype() + op_map = dict() + for op in op_pool: + param_map, produced = op.get_prototype() if not ( # Some types may only be produced by prebuilt operators produced in only_prebuilt_cls and not isinstance(op, engine.PrebuiltOperator) - ): - op_map.setdefault(produced, set()).add(op) + ): + op_map.setdefault(produced, set()).add(op) op_map = adaptor.filter_op_map(op_map) # Restrict the production of some types to a set of operators. @@ -489,10 +499,16 @@ the name of the parameter, the start value, stop value and step size.""") # Get the list of root operators root_op_set = set() for produced, op_set in op_map.items(): - # All producers of Result can be a root operator in the expressions - # we are going to build, i.e. the outermost function call - if utils.match_base_cls(produced, goal_pattern): - root_op_set.update(op_set) + # All producers of the goal types can be a root operator in the + # expressions we are going to build, i.e. the outermost function call + if type_goal_pattern: + if utils.match_base_cls(produced, type_goal_pattern): + root_op_set.update(op_set) + + if callable_goal_pattern: + for op in op_set: + if utils.match_name(op.get_name(full_qual=True), callable_goal_pattern): + root_op_set.add(op) # Sort for stable output root_op_list = sorted(root_op_set, key=lambda op: str(op.name)) @@ -502,27 +518,27 @@ the name of the parameter, the start value, stop value and step size.""") hidden_callable_set = adaptor.get_hidden_callable_set(op_map) # Only print once per parameters' tuple - @utils.once - def handle_non_produced(cls_name, consumer_name, param_name, callable_path): + @utils.once + def handle_non_produced(cls_name, consumer_name, param_name, callable_path): # When reloading from the DB, we don't want to be annoyed with lots of # output related to missing PrebuiltOperator if load_db_path and not verbose: return - info('Nothing can produce instances of {cls} needed for {consumer} (parameter "{param}", along path {path})'.format( - cls = cls_name, - consumer = consumer_name, - param = param_name, - path = ' -> '.join(engine.get_name(callable_) for callable_ in callable_path) - )) + info('Nothing can produce instances of {cls} needed for {consumer} (parameter "{param}", along path {path})'.format( + cls = cls_name, + consumer = consumer_name, + param = param_name, + path = ' -> '.join(engine.get_name(callable_) for callable_ in callable_path) + )) - @utils.once - def handle_cycle(path): - error('Cyclic dependency detected: {path}'.format( - path = ' -> '.join( - engine.get_name(callable_) - for callable_ in path - ) - )) + @utils.once + def handle_cycle(path): + error('Cyclic dependency detected: {path}'.format( + path = ' -> '.join( + engine.get_name(callable_) + for callable_ in path + ) + )) # Build the list of Expression that can be constructed from the set of # callables diff --git a/tools/exekall/exekall/utils.py b/tools/exekall/exekall/utils.py index 871b496c2..68d9935a3 100644 --- a/tools/exekall/exekall/utils.py +++ b/tools/exekall/exekall/utils.py @@ -68,7 +68,7 @@ def load_serial_from_db(db, uuid_seq=None, type_pattern_seq=None): def match_base_cls(cls, pattern): # Match on the name of the class of the object and all its base classes - for base_cls in inspect.getmro(cls): + for base_cls in engine.get_mro(cls): base_cls_name = engine.get_name(base_cls, full_qual=True) if fnmatch.fnmatch(base_cls_name, pattern): return True -- GitLab From ca198823e731c22161117c0a757dc76e2ea7f644 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Fri, 2 Nov 2018 11:38:08 +0000 Subject: [PATCH 10/17] exekall: various fixes * Apply CSE before generating the template scripts * generated scripts now honor reusable operators * Improve cyclic dependency handling * simplify the exekall customization API to filter sets of callables * fix handling of slightly inconsistent op_map and cls_map --- tools/exekall/exekall/customization.py | 16 +-- tools/exekall/exekall/engine.py | 77 ++++++----- tools/exekall/exekall/main.py | 176 +++++++++++++------------ tools/exekall/exekall/utils.py | 21 ++- 4 files changed, 163 insertions(+), 127 deletions(-) diff --git a/tools/exekall/exekall/customization.py b/tools/exekall/exekall/customization.py index bf91b331b..277ca809e 100644 --- a/tools/exekall/exekall/customization.py +++ b/tools/exekall/exekall/customization.py @@ -40,14 +40,14 @@ class AdaptorBase: def get_db_loader(self): return None - def filter_callable_pool(self, callable_pool): - return callable_pool - - def filter_cls_map(self, cls_map): - return cls_map - - def filter_op_map(self, op_map): - return op_map + def filter_op_pool(self, op_pool): + return { + op for op in op_pool + # Only select operators with non-empty parameter list. This + # rules out all classes __init__ that do not take parameter, as + # they are typically not interesting to us. + if op.get_prototype()[0] + } def get_prebuilt_list(self): return [] diff --git a/tools/exekall/exekall/engine.py b/tools/exekall/exekall/engine.py index 7c3a1b3de..a81ce4df1 100644 --- a/tools/exekall/exekall/engine.py +++ b/tools/exekall/exekall/engine.py @@ -235,9 +235,6 @@ class ObjectStore: class CycleError(Exception): pass -class IgnoredCycleError(CycleError): - pass - class ExpressionWrapper: def __init__(self, expr): self.expr = expr @@ -249,23 +246,28 @@ class ExpressionWrapper: def build_expr_list(cls, result_op_seq, op_map, cls_map, non_produced_handler='raise', cycle_handler='raise'): op_map = copy.copy(op_map) - cls_map = copy.copy(cls_map) + cls_map = { + cls: compat_cls_set + for cls, compat_cls_set in cls_map.items() + # If there is at least one compatible subclass that is produced, we + # keep it, otherwise it will mislead _build_expr into thinking the + # class can be built where in fact it cannot + if compat_cls_set & op_map.keys() + } for internal_cls in (Consumer, ExprData): op_map[internal_cls] = {Operator(internal_cls)} cls_map[internal_cls] = [internal_cls] expr_list = list() for result_op in result_op_seq: - # We just skip over Expression where a CycleError happened - with contextlib.suppress(IgnoredCycleError): - expr_gen = cls._build_expr(result_op, op_map, cls_map, - op_stack = [], - non_produced_handler=non_produced_handler, - cycle_handler=cycle_handler, - ) - for expr in expr_gen: - if expr.validate_expr(op_map): - expr_list.append(expr) + expr_gen = cls._build_expr(result_op, op_map, cls_map, + op_stack = [], + non_produced_handler=non_produced_handler, + cycle_handler=cycle_handler, + ) + for expr in expr_gen: + if expr.validate_expr(op_map): + expr_list.append(expr) return expr_list @@ -276,6 +278,9 @@ class ExpressionWrapper: if op in op_stack: if cycle_handler == 'ignore': return + elif callable(cycle_handler): + cycle_handler(tuple(op.callable_ for op in new_op_stack)) + return elif cycle_handler == 'raise': raise CycleError('Cyclic dependency found: {path}'.format( path = ' -> '.join( @@ -283,9 +288,7 @@ class ExpressionWrapper: ) )) else: - cycle_handler(tuple(op.callable_ for op in new_op_stack)) - raise IgnoredCycleError - + raise ValueError('Invalid cycle_handler') op_stack = new_op_stack @@ -321,6 +324,11 @@ class ExpressionWrapper: else: if non_produced_handler == 'ignore': return + elif callable(non_produced_handler): + non_produced_handler(wanted_cls.__qualname__, op.name, param, + tuple(op.resolved_callable for op in op_stack) + ) + return elif non_produced_handler == 'raise': raise NoOperatorError('No operator can produce instances of {cls} needed for {op} (parameter "{param}" along path {path})'.format( cls = wanted_cls.__qualname__, @@ -331,10 +339,7 @@ class ExpressionWrapper: ) )) else: - non_produced_handler(wanted_cls.__qualname__, op.name, param, - tuple(op.resolved_callable for op in op_stack) - ) - return + raise ValueError('Invalid non_produced_handler') param_list = remove_indices(param_list, ignored_indices) cls_combis = remove_indices(cls_combis, ignored_indices) @@ -359,11 +364,10 @@ class ExpressionWrapper: op_combi = list(op_combi) # Get all the possible ways of calling these operators - param_combis = itertools.product(*( - cls._build_expr(param_op, op_map, cls_map, + param_combis = itertools.product(*(cls._build_expr( + param_op, op_map, cls_map, op_stack, non_produced_handler, cycle_handler, - ) - for param_op in op_combi + ) for param_op in op_combi )) for param_combi in param_combis: @@ -697,6 +701,7 @@ class Expression: plain_name_cls_set = set() script = '' result_name_map = dict() + reusable_outvar_map = dict() for i, expr in enumerate(expr_list): script += ( '#'*80 + '\n# Computed expressions:' + @@ -710,6 +715,7 @@ class Expression: expr_val_set = set(expr.get_all_values()) result_name, snippet = expr._get_script( + reusable_outvar_map = reusable_outvar_map, prefix = prefix + str(i), obj_store = obj_store, module_name_set = module_name_set, @@ -801,7 +807,18 @@ class Expression: EXPR_DATA_VAR_NAME = 'EXPR_DATA' - def _get_script(self, prefix, obj_store, module_name_set, idt, expr_val_set, consumer_expr_stack): + def _get_script(self, reusable_outvar_map, *args, **kwargs): + with contextlib.suppress(KeyError): + outvar = reusable_outvar_map[self] + return (outvar, '') + outvar, script = self._get_script_internal( + reusable_outvar_map, *args, **kwargs + ) + if self.op.reusable: + reusable_outvar_map[self] = outvar + return (outvar, script) + + def _get_script_internal(self, reusable_outvar_map, prefix, obj_store, module_name_set, idt, expr_val_set, consumer_expr_stack): def make_method_self_name(expr): return expr.op.value_type.__name__.replace('.', '') @@ -934,7 +951,7 @@ class Expression: # Do a deep first search traversal of the expression. param_outvar, param_out = param_expr._get_script( - param_prefix, obj_store, module_name_set, idt, + reusable_outvar_map, param_prefix, obj_store, module_name_set, idt, param_expr_val_set, consumer_expr_stack = consumer_expr_stack + [self], ) @@ -1134,6 +1151,7 @@ class Expression: for expr_val in executor(*args, **kwargs): yield (expr_wrapper, expr_val) + #TODO: make that stateless by returning copies of Expression's def _prepare_exec(self, expr_set): self.discard_result() @@ -1151,9 +1169,8 @@ class Expression: return replacement_expr # Otherwise register this Expression so no other duplicate will be used - else: - expr_set.add(self) - return self + expr_set.add(self) + return self def execute(self, post_compute_cb=None): return self._execute([], post_compute_cb) diff --git a/tools/exekall/exekall/main.py b/tools/exekall/exekall/main.py index bb8af3b07..240c1f929 100755 --- a/tools/exekall/exekall/main.py +++ b/tools/exekall/exekall/main.py @@ -30,7 +30,6 @@ import traceback import uuid import traceback import gzip -import fnmatch import functools import itertools import importlib @@ -72,6 +71,14 @@ than one to choose from.""") help="""Callable names patterns. Types produced by these callables will only be produced by these (other callables will be excluded).""") + run_parser.add_argument('--forbid', action='append', + default=[], + help="""Type names patterns. Callable returning these types or any subclass will not be called.""") + + run_parser.add_argument('--allow', action='append', + default=[], + help="""Allow using callable with a fully qualified name matching these patterns, even if they have been not selected for various reasons..""") + run_parser.add_argument('--modules-root', action='append', default=[], help="Equivalent to setting PYTHONPATH") @@ -234,7 +241,10 @@ the name of the parameter, the start value, stop value and step size.""") load_db_uuid_args = args.load_uuid_args user_filter = args.filter - restrict_list = args.restrict + restricted_pattern_set = set(args.restrict) + forbidden_pattern_set = set(args.forbid) + allowed_pattern_set = set(args.allow) + allowed_pattern_set.update(restricted_pattern_set) sys.path.extend(args.modules_root) @@ -264,40 +274,6 @@ the name of the parameter, the start value, stop value and step size.""") module_set.update(utils.import_file(path) for path in args.python_files) - # Pool of all callable considered - callable_pool = utils.get_callable_set(module_set) - callable_pool = adaptor.filter_callable_pool(callable_pool) - - op_pool = { - engine.Operator(callable_, tag_list_getter=adaptor.get_tag_list) - for callable_ in callable_pool - } - op_pool = { - op for op in op_pool - # Only select operators with non-empty parameter list. This rules out - # all classes __init__ that do not take parameter, as they are - # typically not interesting to us. - if op.get_prototype()[0] - } - - # Force some parameter values to be provided with a specific callable - patch_map = dict() - for sweep_spec in args.sweep: - number_type = float - callable_pattern, param, start, stop, step = sweep_spec - for callable_ in callable_pool: - callable_name = engine.get_name(callable_) - if not fnmatch.fnmatch(callable_name, callable_pattern): - continue - patch_map.setdefault(callable_name, dict())[param] = [ - i for i in utils.sweep_number( - callable_, param, - number_type(start), number_type(stop), number_type(step) - ) - ] - - only_prebuilt_cls = set() - # Get the prebuilt operators from the adaptor if not load_db_path: prebuilt_op_pool_list = adaptor.get_prebuilt_list() @@ -395,14 +371,35 @@ the name of the parameter, the start value, stop value and step size.""") tag_list_getter=adaptor.get_tag_list, )) - # Make sure that the provided PrebuiltOperator will be the only ones used - # to provide their types - only_prebuilt_cls.update( - op.obj_type - for op in prebuilt_op_pool_list + # Pool of all callable considered + callable_pool = utils.get_callable_set(module_set) + op_pool = { + engine.Operator(callable_, tag_list_getter=adaptor.get_tag_list) + for callable_ in callable_pool + } + filtered_op_pool = adaptor.filter_op_pool(op_pool) + # Make sure we have all the explicitely allowed operators + filtered_op_pool.update( + op for op in op_pool + if utils.match_name(op.get_name(full_qual=True), allowed_pattern_set) ) + op_pool = filtered_op_pool - only_prebuilt_cls.discard(type(NoValue)) + # Force some parameter values to be provided with a specific callable + patch_map = dict() + for sweep_spec in args.sweep: + number_type = float + callable_pattern, param, start, stop, step = sweep_spec + for callable_ in callable_pool: + callable_name = engine.get_name(callable_, full_qual=True) + if not utils.match_name(callable_name, [callable_pattern]): + continue + patch_map.setdefault(callable_name, dict())[param] = [ + i for i in utils.sweep_number( + callable_, param, + number_type(start), number_type(stop), number_type(step) + ) + ] for op_name, param_patch_map in patch_map.items(): for op in op_pool: @@ -449,34 +446,38 @@ the name of the parameter, the start value, stop value and step size.""") for cls in cls_set } - cls_map = adaptor.filter_cls_map(cls_map) + # Make sure that the provided PrebuiltOperator will be the only ones used + # to provide their types + only_prebuilt_cls = set(itertools.chain.from_iterable( + # Augment the list of classes that can only be provided by a prebuilt + # Operator with all the compatible classes + cls_map[op.obj_type] + for op in prebuilt_op_pool_list + )) - # Augment the list of classes that can only be provided by a prebuilt - # Operator with all the compatible classes - only_prebuilt_cls_ = set() - for cls in only_prebuilt_cls: - only_prebuilt_cls_.update(cls_map[cls]) - only_prebuilt_cls = only_prebuilt_cls_ + only_prebuilt_cls.discard(type(NoValue)) # Map of all produced types to a set of what operator can create them - op_map = dict() - for op in op_pool: - param_map, produced = op.get_prototype() - if not ( - # Some types may only be produced by prebuilt operators - produced in only_prebuilt_cls and - not isinstance(op, engine.PrebuiltOperator) - ): - op_map.setdefault(produced, set()).add(op) - op_map = adaptor.filter_op_map(op_map) + def build_op_map(op_pool, only_prebuilt_cls, forbidden_pattern_set): + op_map = dict() + for op in op_pool: + param_map, produced = op.get_prototype() + is_prebuilt_op = isinstance(op, engine.PrebuiltOperator) + if ( + (is_prebuilt_op or produced not in only_prebuilt_cls) + and not utils.match_base_cls(produced, forbidden_pattern_set) + ): + op_map.setdefault(produced, set()).add(op) + return op_map + + op_map = build_op_map(op_pool, only_prebuilt_cls, forbidden_pattern_set) + # Make sure that we only use what is available from now on + op_pool = set(itertools.chain.from_iterable(op_map.values())) # Restrict the production of some types to a set of operators. restricted_op_set = { op for op in op_pool - if any( - fnmatch.fnmatch(op.get_name(full_qual=True), pattern) - for pattern in restrict_list - ) + if utils.match_name(op.get_name(full_qual=True), restricted_pattern_set) } def apply_restrict(produced, op_set, restricted_op_set, cls_map): restricted_op_set = { @@ -518,27 +519,31 @@ the name of the parameter, the start value, stop value and step size.""") hidden_callable_set = adaptor.get_hidden_callable_set(op_map) # Only print once per parameters' tuple - @utils.once - def handle_non_produced(cls_name, consumer_name, param_name, callable_path): + if verbose: + @utils.once + def handle_non_produced(cls_name, consumer_name, param_name, callable_path): # When reloading from the DB, we don't want to be annoyed with lots of # output related to missing PrebuiltOperator if load_db_path and not verbose: return - info('Nothing can produce instances of {cls} needed for {consumer} (parameter "{param}", along path {path})'.format( - cls = cls_name, - consumer = consumer_name, - param = param_name, - path = ' -> '.join(engine.get_name(callable_) for callable_ in callable_path) - )) + info('Nothing can produce instances of {cls} needed for {consumer} (parameter "{param}", along path {path})'.format( + cls = cls_name, + consumer = consumer_name, + param = param_name, + path = ' -> '.join(engine.get_name(callable_) for callable_ in callable_path) + )) - @utils.once - def handle_cycle(path): - error('Cyclic dependency detected: {path}'.format( - path = ' -> '.join( - engine.get_name(callable_) - for callable_ in path - ) - )) + @utils.once + def handle_cycle(path): + error('Cyclic dependency detected: {path}'.format( + path = ' -> '.join( + engine.get_name(callable_) + for callable_ in path + ) + )) + else: + handle_non_produced = 'ignore' + handle_cycle = 'ignore' # Build the list of Expression that can be constructed from the set of # callables @@ -547,6 +552,7 @@ the name of the parameter, the start value, stop value and step size.""") non_produced_handler = handle_non_produced, cycle_handler = handle_cycle, )) + testcase_list.sort(key=lambda expr: take_first(expr.get_id(full_qual=True, with_tags=True))) # Only keep the Expression where the outermost (root) operator is defined # in one of the files that were explicitely specified on the command line. @@ -559,12 +565,12 @@ the name of the parameter, the start value, stop value and step size.""") if user_filter: testcase_list = [ testcase for testcase in testcase_list - if fnmatch.fnmatch(take_first(testcase.get_id( + if utils.match_name(take_first(testcase.get_id( # These options need to match what --dry-run gives (unless # verbose is used) full_qual=False, qual=False, - hidden_callable_set=hidden_callable_set)), user_filter) + hidden_callable_set=hidden_callable_set)), [user_filter]) ] if not testcase_list: @@ -592,7 +598,11 @@ the name of the parameter, the start value, stop value and step size.""") out('\nArtifacts dir: {}\n'.format(artifact_dir)) - for testcase in testcase_list: + # Apply the common subexpression elimination before trying to create the + # template scripts + executor_map = engine.Expression.get_executor_map(testcase_list) + + for testcase in executor_map.keys(): testcase_short_id = take_first(testcase.get_id( hidden_callable_set=hidden_callable_set, with_tags=False, @@ -640,7 +650,7 @@ the name of the parameter, the start value, stop value and step size.""") return 0 result_map = collections.defaultdict(list) - for testcase, executor in engine.Expression.get_executor_map(testcase_list).items(): + for testcase, executor in executor_map.items(): exec_start_msg = 'Executing: {short_id}\n\nID: {full_id}\nArtifacts: {folder}'.format( short_id=take_first(testcase.get_id( hidden_callable_set=hidden_callable_set, diff --git a/tools/exekall/exekall/utils.py b/tools/exekall/exekall/utils.py index 68d9935a3..24e2d6541 100644 --- a/tools/exekall/exekall/utils.py +++ b/tools/exekall/exekall/utils.py @@ -46,10 +46,7 @@ def load_serial_from_db(db, uuid_seq=None, type_pattern_seq=None): ) def type_pattern_predicate(serial): - return any( - match_base_cls(type(serial.value), pattern) - for pattern in type_pattern_seq - ) + return match_base_cls(type(serial.value), type_pattern_seq) if type_pattern_seq and not uuid_seq: predicate = type_pattern_predicate @@ -66,15 +63,27 @@ def load_serial_from_db(db, uuid_seq=None, type_pattern_seq=None): return db.obj_store.get_by_predicate(predicate) -def match_base_cls(cls, pattern): +def match_base_cls(cls, pattern_list): # Match on the name of the class of the object and all its base classes for base_cls in engine.get_mro(cls): base_cls_name = engine.get_name(base_cls, full_qual=True) - if fnmatch.fnmatch(base_cls_name, pattern): + if any( + fnmatch.fnmatch(base_cls_name, pattern) + for pattern in pattern_list + ): return True return False +def match_name(name, pattern_list): + if name is None: + return False + return any( + fnmatch.fnmatch(name, pattern) + for pattern in pattern_list + ) + + def get_recursive_module_set(module_set, package_set): """Retrieve the set of all modules recurisvely imported from the modules in `module_set`, if they are (indirectly) part of one of the packages named in -- GitLab From c55094ea19929a14279a75474a4055c30fc872ad Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Fri, 2 Nov 2018 12:25:37 +0000 Subject: [PATCH 11/17] exekall: misc improvements * engine.Expression._prepare_exec now returns copy of the Expression, leaving the original ones untouched * reusability of operators is stored in a '_exekall_reusable' attribute to avoid clashes. * Better sorting of the testcase list * Add --verbose levels (-v, -vv) * Show hidden callables in the ID before executing test if --verbose is used --- tools/exekall/exekall/engine.py | 34 ++++++++------- tools/exekall/exekall/main.py | 74 ++++++++++++++++----------------- tools/exekall/exekall/utils.py | 4 +- 3 files changed, 59 insertions(+), 53 deletions(-) diff --git a/tools/exekall/exekall/engine.py b/tools/exekall/exekall/engine.py index a81ce4df1..9e3720c7e 100644 --- a/tools/exekall/exekall/engine.py +++ b/tools/exekall/exekall/engine.py @@ -1151,26 +1151,32 @@ class Expression: for expr_val in executor(*args, **kwargs): yield (expr_wrapper, expr_val) - #TODO: make that stateless by returning copies of Expression's def _prepare_exec(self, expr_set): - self.discard_result() + """Apply a flavor of common subexpressions elimination to the Expression + graph and cleanup results of previous runs. + + :return: return an updated copy of the Expression + """ + # Make a copy so we don't modify the original Expression + new_expr = copy.copy(self) + new_expr.discard_result() - for param, param_expr in list(self.param_map.items()): + for param, param_expr in list(new_expr.param_map.items()): # Update the param map in case param_expr was deduplicated - self.param_map[param] = param_expr._prepare_exec(expr_set) + new_expr.param_map[param] = param_expr._prepare_exec(expr_set) # Look for an existing Expression that has the same parameters so we # don't add duplicates. - for replacement_expr in expr_set - {self}: + for replacement_expr in expr_set - {new_expr}: if ( - self.op.callable_ is replacement_expr.op.callable_ and - self.param_map == replacement_expr.param_map + new_expr.op.callable_ is replacement_expr.op.callable_ and + new_expr.param_map == replacement_expr.param_map ): return replacement_expr # Otherwise register this Expression so no other duplicate will be used - expr_set.add(self) - return self + expr_set.add(new_expr) + return new_expr def execute(self, post_compute_cb=None): return self._execute([], post_compute_cb) @@ -1457,10 +1463,10 @@ class Operator: ) } - if hasattr(self.resolved_callable, 'reusable'): - self.reusable = self.resolved_callable.reusable - elif hasattr(self.value_type, 'reusable'): - self.reusable = self.value_type.reusable + if hasattr(self.resolved_callable, '_exekall_reusable'): + self.reusable = self.resolved_callable._exekall_reusable + elif hasattr(self.value_type, '_exekall_reusable'): + self.reusable = self.value_type._exekall_reusable else: self.reusable = self.REUSABLE_DEFAULT @@ -1763,7 +1769,7 @@ class PrebuiltOperator(Operator): def reusable(reusable=Operator.REUSABLE_DEFAULT): def decorator(wrapped): - wrapped.reusable = reusable + wrapped._exekall_reusable = reusable return wrapped return decorator diff --git a/tools/exekall/exekall/main.py b/tools/exekall/exekall/main.py index 240c1f929..3f4b801b5 100755 --- a/tools/exekall/exekall/main.py +++ b/tools/exekall/exekall/main.py @@ -124,7 +124,7 @@ matching this pattern.""") It needs five fields: the qualified name of the callable (pattern can be used), the name of the parameter, the start value, stop value and step size.""") - run_parser.add_argument('--verbose', action='store_true', + run_parser.add_argument('--verbose', '-v', action='count', default=0, help="""More verbose output.""") run_parser.add_argument('--dry-run', action='store_true', @@ -140,8 +140,6 @@ the name of the parameter, the start value, stop value and step size.""") run_parser.add_argument('--debug', action='store_true', help="""Show complete Python backtrace when exekall crashes.""") - - args = argparse.Namespace() # Avoid showing help message on the incomplete parser. Instead, we carry on # and the help will be displayed after the parser customization has a # chance to take place. @@ -154,12 +152,12 @@ the name of the parameter, the start value, stop value and step size.""") # Silence argparse until we know what is going on stream = io.StringIO() with contextlib.redirect_stderr(stream): - args, _ = parser.parse_known_args(no_help_argv, args) + args, _ = parser.parse_known_args(no_help_argv) # If it fails, that may be because of an incomplete command line with just # --help for example. If it was for another reason, it will fail again and # show the message. except SystemExit: - args, _ = parser.parse_known_args(argv, args) + args, _ = parser.parse_known_args(argv) if not args.subcommand: parser.print_help() @@ -449,8 +447,8 @@ the name of the parameter, the start value, stop value and step size.""") # Make sure that the provided PrebuiltOperator will be the only ones used # to provide their types only_prebuilt_cls = set(itertools.chain.from_iterable( - # Augment the list of classes that can only be provided by a prebuilt - # Operator with all the compatible classes + # Augment the list of classes that can only be provided by a prebuilt + # Operator with all the compatible classes cls_map[op.obj_type] for op in prebuilt_op_pool_list )) @@ -459,15 +457,15 @@ the name of the parameter, the start value, stop value and step size.""") # Map of all produced types to a set of what operator can create them def build_op_map(op_pool, only_prebuilt_cls, forbidden_pattern_set): - op_map = dict() - for op in op_pool: - param_map, produced = op.get_prototype() + op_map = dict() + for op in op_pool: + param_map, produced = op.get_prototype() is_prebuilt_op = isinstance(op, engine.PrebuiltOperator) if ( (is_prebuilt_op or produced not in only_prebuilt_cls) and not utils.match_base_cls(produced, forbidden_pattern_set) - ): - op_map.setdefault(produced, set()).add(op) + ): + op_map.setdefault(produced, set()).add(op) return op_map op_map = build_op_map(op_pool, only_prebuilt_cls, forbidden_pattern_set) @@ -520,27 +518,23 @@ the name of the parameter, the start value, stop value and step size.""") # Only print once per parameters' tuple if verbose: - @utils.once - def handle_non_produced(cls_name, consumer_name, param_name, callable_path): - # When reloading from the DB, we don't want to be annoyed with lots of - # output related to missing PrebuiltOperator - if load_db_path and not verbose: - return - info('Nothing can produce instances of {cls} needed for {consumer} (parameter "{param}", along path {path})'.format( - cls = cls_name, - consumer = consumer_name, - param = param_name, - path = ' -> '.join(engine.get_name(callable_) for callable_ in callable_path) - )) + @utils.once + def handle_non_produced(cls_name, consumer_name, param_name, callable_path): + info('Nothing can produce instances of {cls} needed for {consumer} (parameter "{param}", along path {path})'.format( + cls = cls_name, + consumer = consumer_name, + param = param_name, + path = ' -> '.join(engine.get_name(callable_) for callable_ in callable_path) + )) - @utils.once - def handle_cycle(path): - error('Cyclic dependency detected: {path}'.format( - path = ' -> '.join( - engine.get_name(callable_) - for callable_ in path - ) - )) + @utils.once + def handle_cycle(path): + error('Cyclic dependency detected: {path}'.format( + path = ' -> '.join( + engine.get_name(callable_) + for callable_ in path + ) + )) else: handle_non_produced = 'ignore' handle_cycle = 'ignore' @@ -552,7 +546,13 @@ the name of the parameter, the start value, stop value and step size.""") non_produced_handler = handle_non_produced, cycle_handler = handle_cycle, )) + # First, sort with the fully qualified ID so we have the strongest stability + # possible from one run to another testcase_list.sort(key=lambda expr: take_first(expr.get_id(full_qual=True, with_tags=True))) + # Then sort again according to what will be displayed. Since it is a stable + # sort, it will keep a stable order for IDs that look the same but actually + # differ in their hidden part + testcase_list.sort(key=lambda expr: take_first(expr.get_id(qual=False, with_tags=True))) # Only keep the Expression where the outermost (root) operator is defined # in one of the files that were explicitely specified on the command line. @@ -580,11 +580,11 @@ the name of the parameter, the start value, stop value and step size.""") out('The following expressions will be executed:\n') for testcase in testcase_list: out(take_first(testcase.get_id( - full_qual=verbose, - qual=False, + full_qual=bool(verbose), + qual=bool(verbose), hidden_callable_set=hidden_callable_set ))) - if verbose: + if verbose >= 2: out(testcase.pretty_structure() + '\n') if dry_run: @@ -659,7 +659,7 @@ the name of the parameter, the start value, stop value and step size.""") )), full_id=take_first(testcase.get_id( - hidden_callable_set=hidden_callable_set, + hidden_callable_set=hidden_callable_set if not verbose else None, full_qual=True, )), folder=testcase.data['testcase_artifact_dir'] @@ -732,8 +732,6 @@ the name of the parameter, the start value, stop value and step size.""") prefix=prefix, uuid=get_uuid_str(result), )) - if verbose: - out('Full ID:{}'.format(result.get_id(full_qual=True))) out(adaptor.result_str(result)) result_list.append(result) diff --git a/tools/exekall/exekall/utils.py b/tools/exekall/exekall/utils.py index 24e2d6541..8bdbd90d0 100644 --- a/tools/exekall/exekall/utils.py +++ b/tools/exekall/exekall/utils.py @@ -67,6 +67,8 @@ def match_base_cls(cls, pattern_list): # Match on the name of the class of the object and all its base classes for base_cls in engine.get_mro(cls): base_cls_name = engine.get_name(base_cls, full_qual=True) + if not base_cls_name: + continue if any( fnmatch.fnmatch(base_cls_name, pattern) for pattern in pattern_list @@ -330,7 +332,7 @@ class ExekallFormatter(logging.Formatter): else: return self.default_fmt.format(record) -def setup_logging(log_level, debug_log_file=None, verbose=False): +def setup_logging(log_level, debug_log_file=None, verbose=0): logging.addLevelName(LOGGING_OUT_LEVEL, 'OUT') level=getattr(logging, log_level.upper()) -- GitLab From e021e2af48d4cab385866e2511a897c0523e69d9 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Fri, 2 Nov 2018 15:54:09 +0000 Subject: [PATCH 12/17] utils: Improve deserialization Add a handler for unknown YAML tags, so deserialization does not fail and the raw data is still available. --- lisa/utils.py | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/lisa/utils.py b/lisa/utils.py index cacff7727..041fa4557 100644 --- a/lisa/utils.py +++ b/lisa/utils.py @@ -107,7 +107,16 @@ def import_all_submodules(pkg): for loader, module_name, is_pkg in pkgutil.walk_packages(pkg.__path__) ] -class Serializable: +class UnknownTagPlaceholder: + def __init__(self, yaml_tag, data, location=None): + self.yaml_tag = yaml_tag + self.data = data + self.location = location + + def __str__(self): + return ''.format(self.yaml_tag) + +class Serializable(Loggable): """ A helper class for YAML serialization/deserialization @@ -135,17 +144,47 @@ class Serializable: yaml = cls._yaml # If allow_unicode=True, true unicode characters will be written to the # file instead of being replaced by escape sequence. - yaml.allow_unicode = (cls.YAML_ENCODING == 'utf-8') + yaml.allow_unicode = ('utf' in cls.YAML_ENCODING) yaml.default_flow_style = False yaml.indent = 4 yaml.Constructor.add_constructor('!include', cls._yaml_include_constructor) yaml.Constructor.add_constructor('!var', cls._yaml_var_constructor) yaml.Constructor.add_multi_constructor('!call:', cls._yaml_call_constructor) + + # Replace unknown tags by a placeholder object containing the data. + # This happens when the class was not imported at the time the object + # was deserialized + yaml.Constructor.add_constructor(None, cls._yaml_unknown_tag_constructor) + #TODO: remove that once the issue is fixed # Workaround for ruamel.yaml bug #244: # https://bitbucket.org/ruamel/yaml/issues/244 yaml.Representer.add_multi_representer(type, yaml.Representer.represent_name) + @classmethod + def _yaml_unknown_tag_constructor(cls, loader, node): + # Get the basic data types that can be expressed using the YAML syntax, + # without using any tag-specific constructor + data = None + for constructor in ( + loader.construct_scalar, + loader.construct_sequence, + loader.construct_mapping + ): + try: + data = constructor(node) + except ruamel.yaml.constructor.ConstructorError: + continue + else: + break + + tag = node.tag + cls.get_logger().debug('Could not find constructor for YAML tag "{tag}" ({mark}), using a placeholder'.format( + tag=tag, + mark=str(node.start_mark).strip() + )) + + return UnknownTagPlaceholder(tag, data, location=node.start_mark) @classmethod def _yaml_call_constructor(cls, loader, suffix, node): -- GitLab From 86cb609ca1f4eb09b97fb06a18706eb960790157 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Fri, 2 Nov 2018 17:48:19 +0000 Subject: [PATCH 13/17] bisector: fix typo --- tools/bisector | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/bisector b/tools/bisector index e253aad31..8273d3cce 100755 --- a/tools/bisector +++ b/tools/bisector @@ -1904,12 +1904,12 @@ class ExekallLISATestStep(ShellStep): show_dist = False, show_details = False, show_pass_rate = False, - show_artifact_dir = False, + show_artifact_dirs = False, testcase = [], iterations = [], ignore_non_issue = False, ignore_excep = [], - dump_artifact_dir = False, + dump_artifact_dirs = False, xunit2json = False, export_logs = False, download = True, @@ -1923,7 +1923,7 @@ class ExekallLISATestStep(ShellStep): show_basic = True show_rates = True show_dist = True - show_artifact_dir = True + show_artifact_dirs = True show_details = True ignore_non_issue = False @@ -2038,7 +2038,7 @@ class ExekallLISATestStep(ShellStep): step_res_seq = filtered_step_res_seq - if show_artifact_dir: + if show_artifact_dirs: out('Results directories:') # Apply processing on selected results @@ -2056,12 +2056,12 @@ class ExekallLISATestStep(ShellStep): else: error('No upload service available, could not upload LISA results.') - if show_artifact_dir: + if show_artifact_dirs: out(' #{i_stack: <2}: {step_res.results_path}'.format(**locals())) # Accumulate the results path to a file, that can be used to garbage # collect all results path that are not referenced by any report. - if dump_artifact_dir: + if dump_artifact_dirs: with open(dump_results_dir, 'a') as f: f.write(step_res.results_path + '\n') @@ -2184,7 +2184,7 @@ class ExekallLISATestStep(ShellStep): if show_details and not (ignore_non_issue and issue == 'passed'): for entry in filtered_entry_list: i_stack = entry['i_stack'] - results_path = '\n' + entry['results_path'] if show_artifact_dir else '' + results_path = '\n' + entry['results_path'] if show_artifact_dirs else '' exception_name, short_msg, msg = entry['details'] if show_details == 'msg': -- GitLab From 25c41d1d9eab668deb4178dc4e09e26b7812d9de Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Fri, 2 Nov 2018 17:50:14 +0000 Subject: [PATCH 14/17] exekall: misc improvements --- tools/exekall/exekall/customization.py | 2 +- tools/exekall/exekall/engine.py | 30 ++++++++++++++++------- tools/exekall/exekall/main.py | 33 +++++++++++++++----------- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/tools/exekall/exekall/customization.py b/tools/exekall/exekall/customization.py index 277ca809e..1fb437ad3 100644 --- a/tools/exekall/exekall/customization.py +++ b/tools/exekall/exekall/customization.py @@ -79,7 +79,7 @@ class AdaptorBase: failed_parents = result.get_failed_values() for failed_parent in failed_parents: excep = failed_parent.excep - return '{type}: {msg}'.format( + return 'EXCEPTION ({type}): {msg}'.format( type = get_name(type(excep)), msg = excep ) diff --git a/tools/exekall/exekall/engine.py b/tools/exekall/exekall/engine.py index 9e3720c7e..b71ebed50 100644 --- a/tools/exekall/exekall/engine.py +++ b/tools/exekall/exekall/engine.py @@ -823,9 +823,26 @@ class Expression: return expr.op.value_type.__name__.replace('.', '') def make_var(name): - # Make sure we don't have clashes between the variable names - name = name.replace('_', '__') - name = '_' + name if name else '' + # If the variable name already contains a double underscore, we use + # 3 of them for the separator between the prefix and the name, so + # it will avoid ambiguity between these cases: + # prefix="prefix", name="my__name": + # prefix___my__name + # prefix="prefix__my", name="name": + # prefix__my__name + + # Find the longest run of underscores + nr_underscore = 0 + current_counter = 0 + for letter in name: + if letter == '_': + current_counter += 1 + else: + nr_underscore = max(current_counter, nr_underscore) + current_counter = 0 + + sep = (nr_underscore + 1) * '_' + name = sep + name if name else '' return prefix + name def make_comment(code, idt): @@ -1257,10 +1274,8 @@ class Expression: expr_val = ExprValue(self, param_expr_val_map) expr_val_seq = ExprValueSeq( self, None, param_expr_val_map, - post_compute_cb ) expr_val_seq.value_list.append(expr_val) - expr_val_seq.completed = True self.result_list.append(expr_val_seq) yield expr_val continue @@ -1779,7 +1794,6 @@ class ExprValueSeq: self.iterator = iterator self.value_list = list() self.param_expr_val_map = param_expr_val_map - self.completed = False self.post_compute_cb = post_compute_cb def get_expr_value_iter(self): @@ -1796,7 +1810,7 @@ class ExprValueSeq: yield from yielder(self.value_list, True) # Then compute the remaining ones - if not self.completed: + if self.iterator: for (value, value_uuid), (excep, excep_uuid) in self.iterator: expr_val = ExprValue(self.expr, self.param_expr_val_map, value, value_uuid, @@ -1819,7 +1833,7 @@ class ExprValueSeq: True ) - self.completed = True + self.iterator = None def any_value_is_NoValue(value_list): return any( diff --git a/tools/exekall/exekall/main.py b/tools/exekall/exekall/main.py index 3f4b801b5..61f56c23a 100755 --- a/tools/exekall/exekall/main.py +++ b/tools/exekall/exekall/main.py @@ -609,14 +609,9 @@ the name of the parameter, the start value, stop value and step size.""") full_qual=False, qual=False, )) - testcase_id = take_first(testcase.get_id( - hidden_callable_set=hidden_callable_set, - with_tags=False, - full_qual=True, - )) data = testcase.data - data['id'] = testcase_id + data['id'] = testcase_short_id data['uuid'] = testcase.uuid testcase_artifact_dir = pathlib.Path( @@ -630,8 +625,19 @@ the name of the parameter, the start value, stop value and step size.""") data['artifact_dir'] = artifact_dir data['testcase_artifact_dir'] = testcase_artifact_dir + with open(str(testcase_artifact_dir.joinpath('UUID')), 'wt') as f: + f.write(testcase.uuid + '\n') + with open(str(testcase_artifact_dir.joinpath('ID')), 'wt') as f: - f.write(testcase_id+'\n') + f.write(testcase_short_id+'\n') + + with open(str(testcase_artifact_dir.joinpath('STRUCTURE')), 'wt') as f: + f.write(take_first(testcase.get_id( + hidden_callable_set=hidden_callable_set, + with_tags=False, + full_qual=True, + )) + '\n\n') + f.write(testcase.pretty_structure()) with open( str(testcase_artifact_dir.joinpath('testcase_template.py')), @@ -651,7 +657,7 @@ the name of the parameter, the start value, stop value and step size.""") result_map = collections.defaultdict(list) for testcase, executor in executor_map.items(): - exec_start_msg = 'Executing: {short_id}\n\nID: {full_id}\nArtifacts: {folder}'.format( + exec_start_msg = 'Executing: {short_id}\n\nID: {full_id}\nArtifacts: {folder}\nUUID: {uuid_}'.format( short_id=take_first(testcase.get_id( hidden_callable_set=hidden_callable_set, full_qual=False, @@ -662,7 +668,8 @@ the name of the parameter, the start value, stop value and step size.""") hidden_callable_set=hidden_callable_set if not verbose else None, full_qual=True, )), - folder=testcase.data['testcase_artifact_dir'] + folder=testcase.data['testcase_artifact_dir'], + uuid_=testcase.uuid ).replace('\n', '\n# ') delim = '#' * (len(exec_start_msg.splitlines()[0]) + 2) @@ -720,8 +727,8 @@ the name of the parameter, the start value, stop value and step size.""") ), ) - prefix = 'Finished ' - out('{prefix}{id}{uuid}'.format( + prefix = 'Finished {uuid} '.format(uuid=get_uuid_str(result)) + out('{prefix}{id}'.format( id=result.get_id( full_qual=False, qual=False, @@ -730,7 +737,6 @@ the name of the parameter, the start value, stop value and step size.""") hidden_callable_set=hidden_callable_set, ).strip().replace('\n', '\n'+len(prefix)*' '), prefix=prefix, - uuid=get_uuid_str(result), )) out(adaptor.result_str(result)) @@ -757,8 +763,7 @@ the name of the parameter, the start value, stop value and step size.""") )[1]+'\n', ) - - with open(str(testcase_artifact_dir.joinpath('UUID')), 'wt') as f: + with open(str(testcase_artifact_dir.joinpath('VALUES_UUID')), 'wt') as f: for expr_val in result_list: if expr_val.value is not NoValue: f.write(expr_val.value_uuid + '\n') -- GitLab From 701c132eefe8e670e99f5b26c1dee2dcf831b0e6 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Tue, 6 Nov 2018 11:25:33 +0000 Subject: [PATCH 15/17] analysis: remove explicit inheritance of object Not needed in Python3 --- lisa/analysis/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisa/analysis/base.py b/lisa/analysis/base.py index d79dd6169..e8b491e02 100644 --- a/lisa/analysis/base.py +++ b/lisa/analysis/base.py @@ -30,7 +30,7 @@ from trappy.utils import listify ResidencyTime = namedtuple('ResidencyTime', ['total', 'active']) ResidencyData = namedtuple('ResidencyData', ['label', 'residency']) -class AnalysisBase(object): +class AnalysisBase: """ Base class for Analysis modules. -- GitLab From ff9efb6f8b835aebe364c518464a0845e32ee514 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Tue, 6 Nov 2018 11:26:10 +0000 Subject: [PATCH 16/17] exekall: minor fixes --- tools/exekall/exekall/engine.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/exekall/exekall/engine.py b/tools/exekall/exekall/engine.py index b71ebed50..8ca9b83ee 100644 --- a/tools/exekall/exekall/engine.py +++ b/tools/exekall/exekall/engine.py @@ -64,10 +64,10 @@ def get_type_hints(f, module_vars=None): return resolve_annotations(f.__annotations__, module_vars) def get_mro(cls): - assert isinstance(cls, type) - if issubclass(cls, type(None)): + if cls is type(None) or cls is None: return (type(None), object) else: + assert isinstance(cls, type) return inspect.getmro(cls) def resolve_annotations(annotations, module_vars): @@ -1799,7 +1799,7 @@ class ExprValueSeq: def get_expr_value_iter(self): callback = self.post_compute_cb if not callback: - callback = lambda x,y: None + callback = lambda x, reused: None def yielder(iteratable, reused): for x in iteratable: -- GitLab From f89096634deaa43bc87af39354b00c314e834248 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Tue, 6 Nov 2018 11:40:08 +0000 Subject: [PATCH 17/17] test_bundle: fix TestBundle.from_dir res_dir handling Since TestBundle.from_dir function is mainly for interactive use, it cannot make assumption on the location of the folder. That means that it cannot take the decision to keep the root of the ArtifactPath or not. Add an explicit parameter to allow not updating the res_dir for serialization checks. --- lisa/tests/kernel/test_bundle.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lisa/tests/kernel/test_bundle.py b/lisa/tests/kernel/test_bundle.py index b56b222a5..2903a1842 100644 --- a/lisa/tests/kernel/test_bundle.py +++ b/lisa/tests/kernel/test_bundle.py @@ -298,7 +298,8 @@ class TestBundle(Serializable, abc.ABC): # it does not get broken. if cls.verify_serialization: bundle.to_dir(res_dir) - bundle = cls.from_dir(res_dir) + # Updating the res_dir breaks deserialization for some use cases + bundle = cls.from_dir(res_dir, update_res_dir=False) return bundle @@ -307,15 +308,16 @@ class TestBundle(Serializable, abc.ABC): return os.path.join(res_dir, "{}.yaml".format(cls.__qualname__)) @classmethod - def from_dir(cls, res_dir): - """ + def from_dir(cls, res_dir, update_res_dir=True): + """ See :meth:`lisa.utils.Serializable.from_path` """ res_dir = ArtifactPath(root=res_dir, relative='') bundle = super().from_path(cls._filepath(res_dir)) # We need to update the res_dir to the one we were given - bundle.res_dir = res_dir + if update_res_dir: + bundle.res_dir = res_dir return bundle -- GitLab