From 0bc16daf7b22bdf119395721e6111fd3fef2afd1 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 3 Dec 2025 11:48:12 +0100 Subject: [PATCH 01/18] added cicd --- .github/workflows/autopep8.yml | 34 +++++++++++++++++++++++++++++++++ .github/workflows/run-tests.yml | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/workflows/autopep8.yml create mode 100644 .github/workflows/run-tests.yml diff --git a/.github/workflows/autopep8.yml b/.github/workflows/autopep8.yml new file mode 100644 index 00000000..9940bbac --- /dev/null +++ b/.github/workflows/autopep8.yml @@ -0,0 +1,34 @@ +name: autopep8 Check + +on: [pull_request] + +jobs: + autopep8-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install autopep8 + run: pip install autopep8 + + - name: Check formatting with autopep8 + id: check + run: | + # Check if autopep8 would make changes + formatting_issues=$(autopep8 --diff --recursive .) + if [[ formatting_issues ]] then + echo "Formatting issues found:" + printf "%s\n" "$formatting_issues" + echo "------------------------------" + echo "-- Formatting issues found! --" + echo "------------------------------" + exit 1 + else + echo "No formatting issues found." + fi + diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 00000000..f989c58b --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,34 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Run Acceptance and Unit tests + +on: + # push: + # branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip # upgrade pip to latest version + pip install ".[visualization]" # install PyProject.toml dependencies (including optional dependencies) + - name: Test with pytest + run: | + python run_tests.py + #pytest # test unit tests only + From a1db9d5f04db71aea648b0daefc5fa603a709eeb Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Mon, 8 Dec 2025 10:13:01 +0100 Subject: [PATCH 02/18] added error propagation to run_tests.py --- run_tests.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/run_tests.py b/run_tests.py index c0c66bea..5387e5c8 100644 --- a/run_tests.py +++ b/run_tests.py @@ -27,8 +27,8 @@ if utest: utestrun = unittest.main(module=None, - argv=[__file__, 'discover', os.path.join(THIS_DIR, 'utest')], - exit=False) + argv=[__file__, 'discover', os.path.join(THIS_DIR, 'utest')], + exit=False) if not utestrun.result.wasSuccessful(): sys.exit(1) @@ -40,8 +40,10 @@ # Adding the robotframeworkMBT folder to the python path forces the development # version to be used instead of the one installed on your system. You will also # need to add this path to your IDE options when running from there. - robot.run_cli(['--outputdir', OUTPUT_ROOT, - '--pythonpath', THIS_DIR] - + sys.argv[1:], exit=False) + exit_code: int = robot.run_cli(['--outputdir', OUTPUT_ROOT, + '--pythonpath', THIS_DIR] + + sys.argv[1:], exit=False) if utest: print(f"Also ran {utestrun.result.testsRun} unit tests") + + sys.exit(exit_code) From 5dde33c236b41cc2cacc292efb3a803fac6ec5f3 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Mon, 8 Dec 2025 10:14:47 +0100 Subject: [PATCH 03/18] removed commented out comment in run-tests.yml CI/CD, fixed 'pip install' --- .github/workflows/run-tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f989c58b..c712f563 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -4,8 +4,6 @@ name: Run Acceptance and Unit tests on: - # push: - # branches: [ "main" ] pull_request: branches: [ "main" ] @@ -26,9 +24,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip # upgrade pip to latest version - pip install ".[visualization]" # install PyProject.toml dependencies (including optional dependencies) + pip install . # install PyProject.toml dependencies - excludes optional dependencies (such as visualisation)! - name: Test with pytest run: | python run_tests.py - #pytest # test unit tests only From 926a83499305ea125aa58e221b9f4f4720710057 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:42:41 +0100 Subject: [PATCH 04/18] apply pep8 Python formatting at max 120 char/line --- .github/workflows/autopep8.yml | 2 +- robotmbt/__init__.py | 2 + robotmbt/modelspace.py | 14 +++-- robotmbt/steparguments.py | 9 ++-- robotmbt/substitutionmap.py | 10 ++-- robotmbt/suitedata.py | 99 ++++++++++++++++++---------------- robotmbt/suiteprocessors.py | 40 ++++++++------ robotmbt/suitereplacer.py | 30 ++++++----- robotmbt/tracestate.py | 9 ++-- 9 files changed, 121 insertions(+), 94 deletions(-) diff --git a/.github/workflows/autopep8.yml b/.github/workflows/autopep8.yml index 9940bbac..a866977f 100644 --- a/.github/workflows/autopep8.yml +++ b/.github/workflows/autopep8.yml @@ -20,7 +20,7 @@ jobs: id: check run: | # Check if autopep8 would make changes - formatting_issues=$(autopep8 --diff --recursive .) + formatting_issues=$(autopep8 --diff --recursive --max-line-length 120 .) if [[ formatting_issues ]] then echo "Formatting issues found:" printf "%s\n" "$formatting_issues" diff --git a/robotmbt/__init__.py b/robotmbt/__init__.py index 6ea102f8..15f9b3e0 100644 --- a/robotmbt/__init__.py +++ b/robotmbt/__init__.py @@ -33,9 +33,11 @@ from .version import VERSION from .suitereplacer import SuiteReplacer + class robotmbt(SuiteReplacer): """ Process test suites on-the-fly to optimise test suite execution """ + __version__ = VERSION diff --git a/robotmbt/modelspace.py b/robotmbt/modelspace.py index b01d2a66..51f9edec 100644 --- a/robotmbt/modelspace.py +++ b/robotmbt/modelspace.py @@ -34,15 +34,17 @@ from .steparguments import StepArguments + class ModellingError(Exception): pass + class ModelSpace: def __init__(self, reference_id=None): self.ref_id = str(reference_id) self.std_attrs = [] self.props = dict() - self.values = dict() # For using literals without having to use quotes (abc='abc') + self.values = dict() # For using literals without having to use quotes (abc='abc') self.scenario_vars = [] self.std_attrs = dir(self) @@ -118,7 +120,7 @@ def process_expression(self, expression, step_args=StepArguments()): self.__handle_attribute_error(err) except NameError as missing: if missing.name == expr: - raise # Putting only a name in an expression can be used as exists check + raise # Putting only a name in an expression can be used as exists check self.__add_alias(missing.name, step_args) result = self.process_expression(expression, step_args) except AttributeError as err: @@ -144,9 +146,9 @@ def __add_alias(self, missing_name, step_args): matching_args = [arg.value for arg in step_args if arg.codestring == missing_name] value = matching_args[0] if matching_args else missing_name if isinstance(value, str): - for esc_char in "$@&=": # Prevent "Syntaxwarning: invalid escape sequence" on Robot escapes like '\$' and '\=' + for esc_char in "$@&=": # Prevent "Syntaxwarning: invalid escape sequence" on Robot escapes like '\$' and '\=' value = value.replace(f'\\{esc_char}', f'\\\\{esc_char}') - value = value.replace("'", r"\'") # Needed because we use single quotes in low level processing later on + value = value.replace("'", r"\'") # Needed because we use single quotes in low level processing later on self.values[missing_name] = value @staticmethod @@ -177,6 +179,7 @@ def get_status_text(self): status += f" {attr}={value}\n" return status + class RecursiveScope: """ Generic scoping object with the properties needed for handling scenario variables with refinement. @@ -190,6 +193,7 @@ class RecursiveScope: executed on the highest available level. Creating new attributes, will make the current level the highest available level for that atrribute. """ + def __init__(self, outer): super().__setattr__('_outer_scope', outer) @@ -206,7 +210,7 @@ def __setattr__(self, attr, value): def __iter__(self): return iter([(attr, getattr(self, attr)) for attr in dir(self._outer_scope) + dir(self) - if not attr.startswith('__') and attr != '_outer_scope']) + if not attr.startswith('__') and attr != '_outer_scope']) def __bool__(self): return any(True for _ in self) diff --git a/robotmbt/steparguments.py b/robotmbt/steparguments.py index aa62d604..3f9d918c 100644 --- a/robotmbt/steparguments.py +++ b/robotmbt/steparguments.py @@ -55,6 +55,7 @@ def __getitem__(self, key): def modified(self): return any([arg.modified for arg in self]) + class StepArgument: # kind list EMBEDDED = 'EMBEDDED' @@ -66,13 +67,13 @@ class StepArgument: def __init__(self, arg_name, value, kind=None, is_default=False): self.name = arg_name self.org_value = value - self.kind = kind # one of the values from the kind list + self.kind = kind # one of the values from the kind list self._value = None self._codestr = None self.value = value - self.is_default = is_default # indicates that the argument was not - # filled in from the scenario. This argment's value is taken - # from the keyword's default as provided by Robot. + # is_default indicates that the argument was not filled in from the scenario. This + # argment's value is taken from the keyword's default as provided by Robot. + self.is_default = is_default @property def arg(self): diff --git a/robotmbt/substitutionmap.py b/robotmbt/substitutionmap.py index 29ba0a25..55419702 100644 --- a/robotmbt/substitutionmap.py +++ b/robotmbt/substitutionmap.py @@ -41,9 +41,10 @@ class SubstitutionMap: constraints. solve() takes the current set of example values and assigns a unique concrete value to each. """ + def __init__(self): - self.substitutions = {} # {example_value:Constraint} - self.solution = {} # {example_value:solution_value} + self.substitutions = {} # {example_value:Constraint} + self.solution = {} # {example_value:solution_value} def __str__(self): src = self.solution or self.substitutions @@ -51,7 +52,7 @@ def __str__(self): def copy(self): new = SubstitutionMap() - new.substitutions = {k: v.copy() for k,v in self.substitutions.items()} + new.substitutions = {k: v.copy() for k, v in self.substitutions.items()} new.solution = self.solution.copy() return new @@ -132,7 +133,8 @@ def copy(self): return Constraint(self.optionset) def add_constraint(self, constraint): - if constraint is None: return + if constraint is None: + return self.optionset = [opt for opt in self.optionset if opt in constraint] if not len(self.optionset): raise ValueError('No options left after adding constraint') diff --git a/robotmbt/suitedata.py b/robotmbt/suitedata.py index b9ca651a..113c6c49 100644 --- a/robotmbt/suitedata.py +++ b/robotmbt/suitedata.py @@ -36,6 +36,7 @@ from .steparguments import StepArgument, StepArguments + class Suite: def __init__(self, name, parent=None): self.name = name @@ -43,32 +44,34 @@ def __init__(self, name, parent=None): self.parent = parent self.suites = [] self.scenarios = [] - self.setup = None # Can be a single step or None - self.teardown = None # Can be a single step or None + self.setup = None # Can be a single step or None + self.teardown = None # Can be a single step or None @property def longname(self): return f"{self.parent.longname}.{self.name}" if self.parent else self.name def has_error(self): - return ( (self.setup.has_error() if self.setup else False) - or any([s.has_error() for s in self.suites]) - or any([s.has_error() for s in self.scenarios]) - or (self.teardown.has_error() if self.teardown else False)) + return ((self.setup.has_error() if self.setup else False) + or any([s.has_error() for s in self.suites]) + or any([s.has_error() for s in self.scenarios]) + or (self.teardown.has_error() if self.teardown else False)) def steps_with_errors(self): - return ( ([self.setup] if self.setup and self.setup.has_error() else []) - + [e for s in map(Suite.steps_with_errors, self.suites) for e in s] - + [e for s in map(Scenario.steps_with_errors, self.scenarios) for e in s] - + ([self.teardown] if self.teardown and self.teardown.has_error() else [])) + return (([self.setup] if self.setup and self.setup.has_error() else []) + + [e for s in map(Suite.steps_with_errors, self.suites) for e in s] + + [e for s in map(Scenario.steps_with_errors, self.scenarios) for e in s] + + ([self.teardown] if self.teardown and self.teardown.has_error() else [])) + class Scenario: def __init__(self, name, parent=None): self.name = name - self.parent = parent # Parent scenario for easy searching, processing and referencing - # after steps and scenarios have been potentially moved around - self.setup = None # Can be a single step or None - self.teardown = None # Can be a single step or None + # Parent scenario is kept for easy searching, processing and referencing + # after steps and scenarios have been potentially moved around + self.parent = parent + self.setup = None # Can be a single step or None + self.teardown = None # Can be a single step or None self.steps = [] self.src_id = None self.data_choices = {} @@ -79,13 +82,13 @@ def longname(self): def has_error(self): return ((self.setup.has_error() if self.setup else False) - or any([s.has_error() for s in self.steps]) - or (self.teardown.has_error() if self.teardown else False)) + or any([s.has_error() for s in self.steps]) + or (self.teardown.has_error() if self.teardown else False)) def steps_with_errors(self): - return ( ([self.setup] if self.setup and self.setup.has_error() else []) - + [s for s in self.steps if s.has_error()] - + ([self.teardown] if self.teardown and self.teardown.has_error() else [])) + return (([self.setup] if self.setup and self.setup.has_error() else []) + + [s for s in self.steps if s.has_error()] + + ([self.teardown] if self.teardown and self.teardown.has_error() else [])) def copy(self): duplicate = copy.copy(self) @@ -108,27 +111,31 @@ def split_at_step(self, stepindex): back.setup = None return front, back + class Step: def __init__(self, steptext, *args, parent, assign=(), prev_gherkin_kw=None): - self.org_step = steptext # first keyword cell of the Robot line, including step_kw, - # excluding positional args, excluding variable assignment. - self.org_pn_args = args # positional and named arguments as parsed from Robot text ('posA' , 'posB', 'named1=namedA') - self.parent = parent # Parent scenario for easy searching and processing. - self.assign = assign # For when a keyword's return value is assigned to a variable. - # Taken directly from Robot. - self.gherkin_kw = self.step_kw if str(self.step_kw).lower() in ['given', 'when', 'then', 'none'] else prev_gherkin_kw - # 'given', 'when', 'then' or None for non-bdd keywords. - self.signature = None # Robot keyword with its embedded arguments in ${...} notation. - self.args = StepArguments() # embedded arguments list of StepArgument objects. - self.detached = False # Decouples StepArguments from the step text (refinement use case) - self.model_info = dict() # Modelling information is available as a dictionary. - # The standard format is dict(IN=[], OUT=[]) and can - # optionally contain an error field. - # IN and OUT are lists of Python evaluatable expressions. - # The `new vocab` form can be used to create new domain objects. - # The `vocab.attribute` form can then be used to express relations - # between properties from the domain vocabulaire. - # Custom processors can define their own attributes. + # org_step is the first keyword cell of the Robot line, including step_kw, + # excluding positional args, excluding variable assignment. + self.org_step = steptext + # org_pn_args are the positional and named arguments as parsed + # from the Robot text ('posA' , 'posB', 'named1=namedA') + self.org_pn_args = args + self.parent = parent # Parent scenario for easy searching and processing. + self.assign = assign # For when a keyword's return value is assigned to a variable. Taken directly from Robot. + # gherkin_kw is one of 'given', 'when', 'then', or None for non-bdd keywords. + self.gherkin_kw = self.step_kw if \ + str(self.step_kw).lower() in ['given', 'when', 'then', 'none'] else prev_gherkin_kw + self.signature = None # Robot keyword with its embedded arguments in ${...} notation. + self.args = StepArguments() # embedded arguments list of StepArgument objects. + self.detached = False # Decouples StepArguments from the step text (refinement use case) + # model_info contains modelling information as a dictionary. The standard format is + # dict(IN=[], OUT=[]) and can optionally contain an error field. + # IN and OUT are lists of Python evaluatable expressions. + # The `new vocab` form can be used to create new domain objects. + # The `vocab.attribute` form can then be used to express relations + # between properties from the domain vocabulaire. + # Custom processors can define their own attributes. + self.model_info = dict() def __str__(self): return self.keyword @@ -197,7 +204,7 @@ def gherkin_kw(self, value): @property def step_kw(self): first_word = self.org_step.split()[0] - return first_word if first_word.lower() in ['given','when','then','and','but'] else None + return first_word if first_word.lower() in ['given', 'when', 'then', 'and', 'but'] else None @property def kw_wo_gherkin(self): @@ -216,7 +223,7 @@ def add_robot_dependent_data(self, robot_kw): self.signature = robot_kw.name self.model_info = self.__parse_model_info(robot_kw._doc) except Exception as ex: - self.model_info['error']=str(ex) + self.model_info['error'] = str(ex) def __handle_non_embedded_arguments(self, robot_argspec): result = [] @@ -260,14 +267,16 @@ def __handle_non_embedded_arguments(self, robot_argspec): @staticmethod def __validate_arguments(spec, positionals, nameds): - p, n = spec.map(positionals, nameds) # Robot uses a slightly different mapping for positional and named arguments. - # We keep the notation from the scenario (with or without the argument's name). - # Robot's mapping favours positional when possible, even when the name is used - # in the keyword call. The validator is sensitive to these differences. + # Robot uses a slightly different mapping for positional and named arguments. + # We keep the notation from the scenario (with or without the argument's name). + # Robot's mapping favours positional when possible, even when the name is used + # in the keyword call. The validator is sensitive to these differences. + p, n = spec.map(positionals, nameds) if p == [None]: # for some reason .map() returns [None] instead of the empty list when there are no arguments p = [] - ArgumentValidator(spec).validate(p, n) # Use the Robot mechanism for validation to yield familiar error messages + # Use the Robot mechanism for validation to yield familiar error messages + ArgumentValidator(spec).validate(p, n) def __parse_model_info(self, docu): model_info = dict() diff --git a/robotmbt/suiteprocessors.py b/robotmbt/suiteprocessors.py index c66ab1e3..669b64bd 100644 --- a/robotmbt/suiteprocessors.py +++ b/robotmbt/suiteprocessors.py @@ -42,6 +42,7 @@ from .tracestate import TraceState from .steparguments import StepArgument, StepArguments + class SuiteProcessors: def echo(self, in_suite): return in_suite @@ -85,7 +86,7 @@ def process_test_suite(self, in_suite, *, seed='new'): scenario.src_id = id self.scenarios = self.flat_suite.scenarios[:] logger.debug("Use these numbers to reference scenarios from traces\n\t" + - "\n\t".join([f"{s.src_id}: {s.name}" for s in self.scenarios])) + "\n\t".join([f"{s.src_id}: {s.name}" for s in self.scenarios])) self._init_randomiser(seed) random.shuffle(self.scenarios) @@ -113,7 +114,7 @@ def _try_to_reach_full_coverage(self, allow_duplicate_scenarios): break tail = self._rewind() logger.debug("Having to roll back up to " - f"{tail.scenario.name if tail else 'the beginning'}") + f"{tail.scenario.name if tail else 'the beginning'}") self._report_tracestate_to_user() else: self.active_model.new_scenario_scope() @@ -154,7 +155,7 @@ def _fail_on_step_errors(suite): if error_list: err_msg = "Steps with errors in their model info found:\n" err_msg += '\n'.join([f"{s.keyword} [{s.model_info['error']}] used in {s.parent.name}" - for s in error_list]) + for s in error_list]) raise Exception(err_msg) def _try_to_fit_in_scenario(self, index, candidate, retry_flag): @@ -193,7 +194,8 @@ def _try_to_fit_in_scenario(self, index, candidate, retry_flag): return False while i_refine is not None: self.active_model.new_scenario_scope() - m_inserted = self._try_to_fit_in_scenario(i_refine, self._scenario_with_repeat_counter(i_refine), retry_flag) + m_inserted = self._try_to_fit_in_scenario( + i_refine, self._scenario_with_repeat_counter(i_refine), retry_flag) if m_inserted: insert_valid_here = True try: @@ -201,8 +203,8 @@ def _try_to_fit_in_scenario(self, index, candidate, retry_flag): model_scratchpad = self.active_model.copy() for expr in exit_conditions: if model_scratchpad.process_expression(expr, part2.steps[1].args) is False: - insert_valid_here = False - break + insert_valid_here = False + break except Exception: insert_valid_here = False if insert_valid_here: @@ -261,7 +263,8 @@ def _split_candidate_if_refinement_needed(scenario, model): return no_split if refine_here: front, back = scenario.split_at_step(scenario.steps.index(step)) - remaining_steps = '\n\t'.join([step.full_keyword, '- '*35] + [s.full_keyword for s in back.steps[1:]]) + remaining_steps = '\n\t'.join([step.full_keyword, '- '*35] + + [s.full_keyword for s in back.steps[1:]]) remaining_steps = SuiteProcessors.escape_robot_vars(remaining_steps) edge_step = Step('Log', f"Refinement follows for step:\n\t{remaining_steps}", parent=scenario) edge_step.gherkin_kw = step.gherkin_kw @@ -302,7 +305,7 @@ def _process_scenario(scenario, model): @staticmethod def _relevant_expressions(step): if step.gherkin_kw is None and not step.model_info: - return [] # model info is optional for action keywords + return [] # model info is optional for action keywords expressions = [] if 'IN' not in step.model_info or 'OUT' not in step.model_info: raise Exception(f"Model info incomplete for step: {step}") @@ -334,7 +337,7 @@ def _generate_scenario_variant(self, scenario, model): if step.args[modded_arg].kind in [StepArgument.EMBEDDED, StepArgument.POSITIONAL, StepArgument.NAMED]: org_example = step.args[modded_arg].org_value if step.gherkin_kw == 'then': - constraint = None # No new constraints are processed for then-steps + constraint = None # No new constraints are processed for then-steps if org_example not in subs.substitutions: # if a then-step signals the first use of an example value, it is considered a new definition subs.substitute(org_example, [org_example]) @@ -342,7 +345,7 @@ def _generate_scenario_variant(self, scenario, model): if not constraint and org_example not in subs.substitutions: raise ValueError(f"No options to choose from at first assignment to {org_example}") if constraint and constraint != '.*': - options = m.process_expression(constraint, step.args) + options = m.process_expression(constraint, step.args) if options == 'exec': raise ValueError(f"Invalid constraint for argument substitution: {expr}") if not options: @@ -378,7 +381,8 @@ def _generate_scenario_variant(self, scenario, model): try: subs.solve() except ValueError as err: - logger.debug(f"Unable to insert scenario {scenario.src_id}, {scenario.name}, due to modifier\n {err}: {subs}") + logger.debug( + f"Unable to insert scenario {scenario.src_id}, {scenario.name}, due to modifier\n {err}: {subs}") return None # Update scenario with generated values @@ -403,7 +407,7 @@ def _parse_modifier_expression(expression, args): if expression.casefold().startswith(var.arg.casefold()): assignment_expr = expression.replace(var.arg, '', 1).strip() if not assignment_expr.startswith('=') or assignment_expr.startswith('=='): - break # not an assignment + break # not an assignment constraint = assignment_expr.replace('=', '', 1).strip() return var.arg, constraint raise ValueError(f"Invalid argument substitution: {expression}") @@ -428,7 +432,8 @@ def _init_randomiser(seed): if isinstance(seed, str): seed = seed.strip() if str(seed).lower() == 'none': - logger.info(f"Using system's random seed for trace generation. This trace cannot be rerun. Use `seed=new` to generate a reusable seed.") + logger.info( + f"Using system's random seed for trace generation. This trace cannot be rerun. Use `seed=new` to generate a reusable seed.") elif str(seed).lower() == 'new': new_seed = SuiteProcessors._generate_seed() logger.info(f"seed={new_seed} (use seed to rerun this trace)") @@ -441,14 +446,15 @@ def _init_randomiser(seed): def _generate_seed(): """Creates a random string of 5 words between 3 and 6 letters long""" vowels = ['a', 'e', 'i', 'o', 'u', 'y'] - consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'z'] + consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', + 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'z'] words = [] for word in range(5): prior_choice = random.choice([vowels, consonants]) - last_choice = random.choice([vowels, consonants]) - string = random.choice(prior_choice) + random.choice(last_choice) # add first two letters - for letter in range(random.randint(1, 4)): # add 1 to 4 more letters + last_choice = random.choice([vowels, consonants]) + string = random.choice(prior_choice) + random.choice(last_choice) # add first two letters + for letter in range(random.randint(1, 4)): # add 1 to 4 more letters if prior_choice is last_choice: new_choice = consonants if prior_choice is vowels else vowels else: diff --git a/robotmbt/suitereplacer.py b/robotmbt/suitereplacer.py index dec54104..d1073667 100644 --- a/robotmbt/suitereplacer.py +++ b/robotmbt/suitereplacer.py @@ -30,13 +30,14 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from robot.libraries.BuiltIn import BuiltIn;Robot = BuiltIn() -from robot.api.deco import keyword -from robot.api import logger +from .suitedata import Suite, Scenario, Step +from .suiteprocessors import SuiteProcessors import robot.running.model as rmodel +from robot.api import logger +from robot.api.deco import keyword +from robot.libraries.BuiltIn import BuiltIn +Robot = BuiltIn() -from .suiteprocessors import SuiteProcessors -from .suitedata import Suite, Scenario, Step class SuiteReplacer: ROBOT_LIBRARY_SCOPE = 'GLOBAL' @@ -56,14 +57,15 @@ def __init__(self, processor='process_test_suite', processor_lib=None): def processor_lib(self): if self._processor_lib is None: self._processor_lib = SuiteProcessors() if self.processor_lib_name is None \ - else Robot.get_library_instance(self.processor_lib_name) + else Robot.get_library_instance(self.processor_lib_name) return self._processor_lib @property def processor_method(self): if self._processor_method is None: if not hasattr(self.processor_lib, self.processor_name): - Robot.fail(f"Processor '{self.processor_name}' not available for model-based processor library {self.processor_lib_name}") + Robot.fail( + f"Processor '{self.processor_name}' not available for model-based processor library {self.processor_lib_name}") self._processor_method = getattr(self._processor_lib, self.processor_name) return self._processor_method @@ -113,7 +115,7 @@ def __process_robot_suite(self, in_suite, parent): step_info.add_robot_dependent_data(Robot._namespace.get_runner(step_info.org_step).keyword) out_suite.setup = step_info if in_suite.teardown and parent is not None: - step_info =Step(in_suite.teardown.name, *in_suite.teardown.args, parent=out_suite) + step_info = Step(in_suite.teardown.name, *in_suite.teardown.args, parent=out_suite) step_info.add_robot_dependent_data(Robot._namespace.get_runner(step_info.org_step).keyword) out_suite.teardown = step_info for st in in_suite.suites: @@ -166,13 +168,13 @@ def __generateRobotSuite(self, suite_model, target_suite): for tc in suite_model.scenarios: new_tc = target_suite.tests.create(name=tc.name) if tc.setup: - new_tc.setup= rmodel.Keyword(name=tc.setup.keyword, - args=tc.setup.posnom_args_str, - type='setup') + new_tc.setup = rmodel.Keyword(name=tc.setup.keyword, + args=tc.setup.posnom_args_str, + type='setup') if tc.teardown: - new_tc.teardown= rmodel.Keyword(name=tc.teardown.keyword, - args=tc.teardown.posnom_args_str, - type='teardown') + new_tc.teardown = rmodel.Keyword(name=tc.teardown.keyword, + args=tc.teardown.posnom_args_str, + type='teardown') for step in tc.steps: if step.keyword == 'VAR': new_tc.body.create_var(step.posnom_args_str[0], step.posnom_args_str[1:]) diff --git a/robotmbt/tracestate.py b/robotmbt/tracestate.py index 4d0e526c..9aaff77b 100644 --- a/robotmbt/tracestate.py +++ b/robotmbt/tracestate.py @@ -32,10 +32,10 @@ class TraceState: def __init__(self, n_scenarios): - self._c_pool = [False] * n_scenarios # coverage pool: True means scenario is in trace - self._tried = [[]] # Keeps track of the scenarios already tried at each step in the trace - self._trace = [] # Choice trace, when was which scenario inserted (e.g. ['1', '2.1', '3', '2.0']) - self._snapshots = [] # Keeps details for elements in trace + self._c_pool = [False] * n_scenarios # coverage pool: True means scenario is in trace + self._tried = [[]] # Keeps track of the scenarios already tried at each step in the trace + self._trace = [] # Choice trace, when was which scenario inserted (e.g. ['1', '2.1', '3', '2.0']) + self._snapshots = [] # Keeps details for elements in trace self._open_refinements = [] @property @@ -157,6 +157,7 @@ def __getitem__(self, key): def __len__(self): return len(self._snapshots) + class TraceSnapShot: def __init__(self, id, inserted_scenario, model_state, drought=0): self.id = id From 27b559b56a2c1bc3a96ffe512a2a706c9252015d Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 3 Dec 2025 11:48:12 +0100 Subject: [PATCH 05/18] added cicd --- .github/workflows/autopep8.yml | 34 +++++++++++++++++++++++++++++++++ .github/workflows/run-tests.yml | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/workflows/autopep8.yml create mode 100644 .github/workflows/run-tests.yml diff --git a/.github/workflows/autopep8.yml b/.github/workflows/autopep8.yml new file mode 100644 index 00000000..9940bbac --- /dev/null +++ b/.github/workflows/autopep8.yml @@ -0,0 +1,34 @@ +name: autopep8 Check + +on: [pull_request] + +jobs: + autopep8-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install autopep8 + run: pip install autopep8 + + - name: Check formatting with autopep8 + id: check + run: | + # Check if autopep8 would make changes + formatting_issues=$(autopep8 --diff --recursive .) + if [[ formatting_issues ]] then + echo "Formatting issues found:" + printf "%s\n" "$formatting_issues" + echo "------------------------------" + echo "-- Formatting issues found! --" + echo "------------------------------" + exit 1 + else + echo "No formatting issues found." + fi + diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 00000000..f989c58b --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,34 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Run Acceptance and Unit tests + +on: + # push: + # branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip # upgrade pip to latest version + pip install ".[visualization]" # install PyProject.toml dependencies (including optional dependencies) + - name: Test with pytest + run: | + python run_tests.py + #pytest # test unit tests only + From 3dee6dfe2bb7ed1cd9b53012b9830ac3bb20bf89 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Mon, 8 Dec 2025 10:13:01 +0100 Subject: [PATCH 06/18] added error propagation to run_tests.py --- run_tests.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/run_tests.py b/run_tests.py index c0c66bea..5387e5c8 100644 --- a/run_tests.py +++ b/run_tests.py @@ -27,8 +27,8 @@ if utest: utestrun = unittest.main(module=None, - argv=[__file__, 'discover', os.path.join(THIS_DIR, 'utest')], - exit=False) + argv=[__file__, 'discover', os.path.join(THIS_DIR, 'utest')], + exit=False) if not utestrun.result.wasSuccessful(): sys.exit(1) @@ -40,8 +40,10 @@ # Adding the robotframeworkMBT folder to the python path forces the development # version to be used instead of the one installed on your system. You will also # need to add this path to your IDE options when running from there. - robot.run_cli(['--outputdir', OUTPUT_ROOT, - '--pythonpath', THIS_DIR] - + sys.argv[1:], exit=False) + exit_code: int = robot.run_cli(['--outputdir', OUTPUT_ROOT, + '--pythonpath', THIS_DIR] + + sys.argv[1:], exit=False) if utest: print(f"Also ran {utestrun.result.testsRun} unit tests") + + sys.exit(exit_code) From ee3e03f06e76cc6faaf724154407a3cdaebdc8d9 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Mon, 8 Dec 2025 10:14:47 +0100 Subject: [PATCH 07/18] removed commented out comment in run-tests.yml CI/CD, fixed 'pip install' --- .github/workflows/run-tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f989c58b..c712f563 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -4,8 +4,6 @@ name: Run Acceptance and Unit tests on: - # push: - # branches: [ "main" ] pull_request: branches: [ "main" ] @@ -26,9 +24,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip # upgrade pip to latest version - pip install ".[visualization]" # install PyProject.toml dependencies (including optional dependencies) + pip install . # install PyProject.toml dependencies - excludes optional dependencies (such as visualisation)! - name: Test with pytest run: | python run_tests.py - #pytest # test unit tests only From 57e8d291bf5a6ffe0d7fd97f4a1564e2a6916d98 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Tue, 9 Dec 2025 14:55:14 +0100 Subject: [PATCH 08/18] format with autopep8 (default line length) --- .../03__parse_model_info/MyProcessor.py | 8 +- .../option_handling/suiterepeater.py | 4 +- .../01__generating_random_traces/traces.py | 4 +- demo/Titanic/domain_lib/JourneyLib.py | 22 ++- demo/Titanic/domain_lib/MapLib.py | 3 +- demo/Titanic/domain_lib/TitanicLib.py | 15 +- demo/Titanic/run_demo.py | 8 +- demo/Titanic/run_game.py | 27 ++-- demo/Titanic/simulation/journey.py | 2 + demo/Titanic/simulation/map_animation.py | 31 ++-- demo/Titanic/simulation/ocean.py | 12 +- demo/Titanic/simulation/titanic_in_ocean.py | 1 - demo/Titanic/system/titanic.py | 3 +- robotmbt/__init__.py | 2 + robotmbt/modelspace.py | 30 ++-- robotmbt/steparguments.py | 12 +- robotmbt/substitutionmap.py | 21 ++- robotmbt/suitedata.py | 134 +++++++++------- robotmbt/suiteprocessors.py | 148 ++++++++++++------ robotmbt/suitereplacer.py | 79 ++++++---- robotmbt/tracestate.py | 15 +- run_tests.py | 3 +- utest/test_modelspace.py | 43 +++-- utest/test_steparguments.py | 13 +- utest/test_substitutionmap.py | 24 +-- utest/test_suitedata.py | 112 ++++++++----- utest/test_suiteprocessors.py | 6 +- utest/test_tracestate.py | 9 +- utest/test_tracestate_refinement.py | 29 ++-- 29 files changed, 516 insertions(+), 304 deletions(-) diff --git a/atest/robotMBT tests/03__parse_model_info/MyProcessor.py b/atest/robotMBT tests/03__parse_model_info/MyProcessor.py index 1c954b70..1ce666bd 100644 --- a/atest/robotMBT tests/03__parse_model_info/MyProcessor.py +++ b/atest/robotMBT tests/03__parse_model_info/MyProcessor.py @@ -7,13 +7,15 @@ def process_test_suite(self, in_suite): for scenario in in_suite.scenarios: assert scenario.steps, msg for step in scenario.steps: - assert step.model_info['IN'] == ['Alfa'], f"{msg} in step {step.keyword}" - assert step.model_info['OUT'] == ['Beta', 'Gamma delta', 'Epsilon'], f"{msg} in step {step.keyword}" + assert step.model_info['IN'] == [ + 'Alfa'], f"{msg} in step {step.keyword}" + assert step.model_info['OUT'] == [ + 'Beta', 'Gamma delta', 'Epsilon'], f"{msg} in step {step.keyword}" return in_suite def _fail_on_step_errors(self): if self.in_suite.has_error(): msg = "\n".join(["Error(s) detected in at least one step"] + [f"{step.kw_wo_gherkin} FAILED: {step.model_info['error']}" - for step in self.in_suite.steps_with_errors()]) + for step in self.in_suite.steps_with_errors()]) raise Exception(msg) diff --git a/atest/robotMBT tests/07__processor_options/option_handling/suiterepeater.py b/atest/robotMBT tests/07__processor_options/option_handling/suiterepeater.py index ddc609f6..bdbfa9fb 100644 --- a/atest/robotMBT tests/07__processor_options/option_handling/suiterepeater.py +++ b/atest/robotMBT tests/07__processor_options/option_handling/suiterepeater.py @@ -2,6 +2,7 @@ from robot.api.deco import library + @library(auto_keywords=None, listener=True) class SuiteRepeater: """ @@ -9,10 +10,11 @@ class SuiteRepeater: Setting bonus_scenario=${True} repeats 1 additional time sub-suites are ignored """ + def process_test_suite(self, in_suite, repeat=1, **kwargs): n_repeats = int(repeat) if kwargs.get('bonus_scenario', False): - n_repeats +=1 + n_repeats += 1 out_suite = copy.deepcopy(in_suite) out_suite.scenarios = n_repeats*out_suite.scenarios for i in range(len(out_suite.scenarios)): diff --git a/atest/robotMBT tests/07__processor_options/random_seeds/01__generating_random_traces/traces.py b/atest/robotMBT tests/07__processor_options/random_seeds/01__generating_random_traces/traces.py index e9322442..8736b866 100644 --- a/atest/robotMBT tests/07__processor_options/random_seeds/01__generating_random_traces/traces.py +++ b/atest/robotMBT tests/07__processor_options/random_seeds/01__generating_random_traces/traces.py @@ -1,12 +1,14 @@ from robot.api.deco import keyword + class traces: ROBOT_LIBRARY_SCOPE = 'GLOBAL' + def reset_traces(self): self.traces = {} @keyword("Trace '${trace}', scenario number ${test_id} is executed") - def add_test(self, trace, test_id:str): + def add_test(self, trace, test_id: str): """*model info* :IN: None :OUT: None diff --git a/demo/Titanic/domain_lib/JourneyLib.py b/demo/Titanic/domain_lib/JourneyLib.py index cbe009b6..ba6263dd 100644 --- a/demo/Titanic/domain_lib/JourneyLib.py +++ b/demo/Titanic/domain_lib/JourneyLib.py @@ -10,6 +10,7 @@ from simulation.titanic_in_ocean import TitanicInOcean from simulation.journey import Journey + class JourneyLib: _journey = None @@ -31,11 +32,13 @@ def map_lib(self) -> MapLib: def start_journey(self, date: str): date = datetime.strptime(date, "%Y-%m-%d") self.journey.start_date = date - self.builtin.log(f"The journey has started at {self.journey.start_date.strftime('%Y-%m-%d')}") + self.builtin.log( + f"The journey has started at {self.journey.start_date.strftime('%Y-%m-%d')}") @keyword("Current date of Journey") def journey_ondate(self): - current_date = self.journey.start_date + timedelta(minutes=self.journey.time_in_journey) + current_date = self.journey.start_date + \ + timedelta(minutes=self.journey.time_in_journey) return current_date.strftime('%Y-%m-%d') @keyword("play out Journey for a duration of ${minutes} minutes") @@ -46,21 +49,26 @@ def pass_time(self, minutes: int): """ self.journey.passed_time(minutes) if self.call_count % 100 == 0: - map_animation.update_floating_objects(self.journey.ocean.floating_objects) + map_animation.update_floating_objects( + self.journey.ocean.floating_objects) self.call_count += 1 @keyword("Move Titanic out of current area") def move_titanic_out_of_current_area(self): titanic = TitanicInOcean.instance - current_area = self.builtin.run_keyword("Area of location Titanic's position") + current_area = self.builtin.run_keyword( + "Area of location Titanic's position") self.builtin.log(f"Titanic moving out of {current_area}") while (new_area := self.map_lib.get_area_of_location(titanic)) == current_area: if not titanic.speed > 0: - self.builtin.log(f"Titanic not moving. Still in area {new_area}") + self.builtin.log( + f"Titanic not moving. Still in area {new_area}") break self.pass_time(1) if titanic.fell_off_the_earth(): - raise Exception("Titanic at least did not sink. But where did it go?") + raise Exception( + "Titanic at least did not sink. But where did it go?") else: self.builtin.log(f"Titanic moved into {new_area}") - map_animation.update_floating_objects(self.journey.ocean.floating_objects) + map_animation.update_floating_objects( + self.journey.ocean.floating_objects) diff --git a/demo/Titanic/domain_lib/MapLib.py b/demo/Titanic/domain_lib/MapLib.py index 70a89898..d864036b 100644 --- a/demo/Titanic/domain_lib/MapLib.py +++ b/demo/Titanic/domain_lib/MapLib.py @@ -30,7 +30,8 @@ def __init__(self): 'Iceberg alley': AreaOnGrid(LocationOnGrid(latitude=43, longitude=-45), LocationOnGrid(latitude=48, longitude=-50)) } - atlantic_area = AreaOnGrid(LocationOnGrid(latitude=35, longitude=-1.41), LocationOnGrid(latitude=65, longitude=-74)) + atlantic_area = AreaOnGrid(LocationOnGrid( + latitude=35, longitude=-1.41), LocationOnGrid(latitude=65, longitude=-74)) LOCATION_AREA_THRESHOLD = 0.1 ATLANTIC_AREA = 'Atlantic' diff --git a/demo/Titanic/domain_lib/TitanicLib.py b/demo/Titanic/domain_lib/TitanicLib.py index 3b16f829..8b6d7c1b 100644 --- a/demo/Titanic/domain_lib/TitanicLib.py +++ b/demo/Titanic/domain_lib/TitanicLib.py @@ -24,7 +24,8 @@ def point_titanic_towards(self, location): if titanic.sunk: self.builtin.log(f"Pointing towards Davy Jones' locker") return - port_location = self.builtin.run_keyword(f"Location of port {location}") + port_location = self.builtin.run_keyword( + f"Location of port {location}") new_direction = titanic.calculate_direction(port_location) titanic.direction = new_direction @@ -34,7 +35,8 @@ def point_titanic_towards(self, location): def titanic_stops(self): titanic = TitanicInOcean.instance titanic.titanic.throttle = 0 - titanic.speed = 0 # TODO should happen over time (due to throttle being > 0) + # TODO should happen over time (due to throttle being > 0) + titanic.speed = 0 self.builtin.log("Now it is time for Titanic to stop at new location") @keyword("Titanic moves full speed ahead") @@ -44,13 +46,16 @@ def titanic_full_speed(self): self.builtin.log(f"There seems to be an issue with the throttle") return titanic.titanic.throttle = 1 - titanic.speed = 700 # TODO Figure out what this speed means. Does time calculation make sense?!?! - self.builtin.log(f"Here we go through the new location with speed {titanic.speed}") + # TODO Figure out what this speed means. Does time calculation make sense?!?! + titanic.speed = 700 + self.builtin.log( + f"Here we go through the new location with speed {titanic.speed}") @keyword("Titanic's position") def titanic_location(self): titanic = TitanicInOcean.instance - loc = LocationOnGrid(longitude=titanic.longitude, latitude=titanic.latitude) + loc = LocationOnGrid(longitude=titanic.longitude, + latitude=titanic.latitude) self.builtin.log(f"Titanic's current position is: {loc}") return loc diff --git a/demo/Titanic/run_demo.py b/demo/Titanic/run_demo.py index f5cb07ac..369e8b55 100644 --- a/demo/Titanic/run_demo.py +++ b/demo/Titanic/run_demo.py @@ -16,8 +16,10 @@ THIS_DIR = os.path.dirname(os.path.abspath(__file__)) OUTPUT_ROOT = os.path.join(THIS_DIR, 'results') SCENARIO_FOLDER = os.path.join(THIS_DIR, 'Titanic_scenarios') - HIT_MISS_TAG = 'hit' if len(sys.argv) == 1 or sys.argv[1].casefold() != 'hit' else 'miss' - EXTENDED_TAG = 'extended' if len(sys.argv) == 1 or sys.argv[1].casefold() != 'extended' else 'dummy' + HIT_MISS_TAG = 'hit' if len( + sys.argv) == 1 or sys.argv[1].casefold() != 'hit' else 'miss' + EXTENDED_TAG = 'extended' if len( + sys.argv) == 1 or sys.argv[1].casefold() != 'extended' else 'dummy' # The base folder needs to be added to the python path to resolve the dependencies. You # will also need to add this path to your IDE options when running from there. @@ -27,4 +29,4 @@ '--exclude', EXTENDED_TAG, '--loglevel', 'DEBUG:INFO', SCENARIO_FOLDER], - exit=False) + exit=False) diff --git a/demo/Titanic/run_game.py b/demo/Titanic/run_game.py index 6ae20920..ec6a4a11 100644 --- a/demo/Titanic/run_game.py +++ b/demo/Titanic/run_game.py @@ -22,7 +22,9 @@ 'Iceberg alley': AreaOnGrid(LocationOnGrid(latitude=43, longitude=-45), LocationOnGrid(latitude=48, longitude=-50)) } -atlantic_area = AreaOnGrid(LocationOnGrid(latitude=35, longitude=-1.41), LocationOnGrid(latitude=65, longitude=-74)) +atlantic_area = AreaOnGrid(LocationOnGrid( + latitude=35, longitude=-1.41), LocationOnGrid(latitude=65, longitude=-74)) + def run_game(map_animation, journey, tio: TitanicInOcean, atlantic_area): @@ -35,7 +37,8 @@ def main_game_loop(stdscr): # Set up the window stdscr.nodelay(True) # Non-blocking input stdscr.timeout(100) # Refresh every 100 milliseconds - stdscr.addstr(0, 0, "Q=Quit. 0=Stop Titanic. WASD-controls (WS control speed, AD control rotation, no need to press and hold)") + stdscr.addstr( + 0, 0, "Q=Quit. 0=Stop Titanic. WASD-controls (WS control speed, AD control rotation, no need to press and hold)") objective = 1 iceberg_alley_reached = False @@ -44,7 +47,8 @@ def main_game_loop(stdscr): while True: journey.passed_time(100) - map_animation.update_floating_objects(journey.ocean.floating_objects) + map_animation.update_floating_objects( + journey.ocean.floating_objects) if not atlantic_area.is_location_within_area(tio): tio.direction -= 180 @@ -55,15 +59,19 @@ def main_game_loop(stdscr): if areas['Iceberg alley'].is_location_within_area(tio): iceberg_alley_reached = True elif iceberg_alley_reached: - stdscr.addstr(objective, 0, "Objective 1: Safely cross Iceberg Alley [Achieved]") + stdscr.addstr( + objective, 0, "Objective 1: Safely cross Iceberg Alley [Achieved]") objective = 2 - stdscr.addstr(objective, 0, "Objective 2: Sail to New York") + stdscr.addstr( + objective, 0, "Objective 2: Sail to New York") elif objective == 2: if tio.distance_to(locations['New York']) < 0.5: tio.speed = 0 - stdscr.addstr(objective, 0, "Objective 2: Sail to New York [Achieved]") + stdscr.addstr( + objective, 0, "Objective 2: Sail to New York [Achieved]") objective = 4 - stdscr.addstr(objective, 0, "You made it to New York!! Press Q to exit.") + stdscr.addstr( + objective, 0, "You made it to New York!! Press Q to exit.") if tio.sunk: stdscr.addstr(objective+2, 0, @@ -97,7 +105,6 @@ def main_game_loop(stdscr): # Continue with the rest of the game logic - # Initialize curses stdscr = curses.initscr() curses.noecho() # Disable automatic echoing of pressed keys @@ -127,7 +134,8 @@ def main_game_loop(stdscr): location = locations["Southampton"] t = Titanic(0, steering_direction=0) - tio = TitanicInOcean(t, longitude=location.longitude - 1, latitude=location.latitude, speed=0, direction=270) + tio = TitanicInOcean(t, longitude=location.longitude - 1, + latitude=location.latitude, speed=0, direction=270) ocean.floating_objects.append(tio) iceberg = Iceberg(latitude=45.5, longitude=-47.5) @@ -138,4 +146,3 @@ def main_game_loop(stdscr): map_animation.update_floating_objects(ocean.floating_objects) run_game(map_animation, journey, tio, atlantic_area) - diff --git a/demo/Titanic/simulation/journey.py b/demo/Titanic/simulation/journey.py index 252f3cfc..44c4235f 100644 --- a/demo/Titanic/simulation/journey.py +++ b/demo/Titanic/simulation/journey.py @@ -6,6 +6,8 @@ class StatusOfJourney(Enum): ON_THE_WAY = 2 ARRIVED = 3 SUNK = 4 + + class Journey: _instance = None diff --git a/demo/Titanic/simulation/map_animation.py b/demo/Titanic/simulation/map_animation.py index 1483dfa6..008041d3 100644 --- a/demo/Titanic/simulation/map_animation.py +++ b/demo/Titanic/simulation/map_animation.py @@ -17,18 +17,22 @@ def _import_dependencies(self): def plot_static_elements(self, areas, locations): # Plot the areas as squares if not self.plot_initialized: - self._import_dependencies() # Import here, to avoid any missing dependency problems in case matplotlib is not installed + # Import here, to avoid any missing dependency problems in case matplotlib is not installed + self._import_dependencies() self.fig, self.ax = plt.subplots() self.ax.set_aspect('equal') for area_name, area in areas.items(): - width = abs(area.upper_left_bound.latitude - area.lower_right_bound.latitude) - height = abs(area.upper_left_bound.longitude - area.lower_right_bound.longitude) - rect = Rectangle((area.lower_right_bound.longitude, area.upper_left_bound.latitude), height, width, alpha=0.4) + width = abs(area.upper_left_bound.latitude - + area.lower_right_bound.latitude) + height = abs(area.upper_left_bound.longitude - + area.lower_right_bound.longitude) + rect = Rectangle((area.lower_right_bound.longitude, + area.upper_left_bound.latitude), height, width, alpha=0.4) self.ax.add_patch(rect) - self.ax.annotate(area_name, (area.lower_right_bound.longitude, area.upper_left_bound.latitude), color='black') - + self.ax.annotate(area_name, (area.lower_right_bound.longitude, + area.upper_left_bound.latitude), color='black') # Plot the locations colors = [ @@ -43,7 +47,8 @@ def plot_static_elements(self, areas, locations): ] ci = 0 for location_name, location in locations.items(): - self.ax.plot(location.longitude, location.latitude, colors[ci % len(colors)], label=location_name) + self.ax.plot(location.longitude, location.latitude, + colors[ci % len(colors)], label=location_name) ci += 1 # # Set the Atlantic area bounds atlantic_area = MapLib.atlantic_area width = abs( @@ -85,7 +90,7 @@ def update_floating_objects(self, floating_objects): # Plot the floating objects for obj in floating_objects: - if isinstance(obj, TitanicInOcean):# Set the rotation angle in degrees + if isinstance(obj, TitanicInOcean): # Set the rotation angle in degrees angle_degrees = obj.direction # Convert angle to radians @@ -98,7 +103,7 @@ def update_floating_objects(self, floating_objects): # Draw the arrow self.ax.annotate("", xy=(obj.longitude + dx, obj.latitude + dy), xytext=(obj.longitude, obj.latitude), - arrowprops=dict(arrowstyle="->"), gid='floating_object') + arrowprops=dict(arrowstyle="->"), gid='floating_object') if obj.sunk: icon = 'rs' # red square @@ -106,10 +111,12 @@ def update_floating_objects(self, floating_objects): icon = 'ys' # yellow square # Draw the arrow self.ax.annotate("", xy=(obj.longitude + dx, obj.latitude + dy), xytext=(obj.longitude, obj.latitude), - arrowprops=dict(arrowstyle='->'), gid='floating_object') - self.ax.plot(obj.longitude, obj.latitude, icon, label='Titanic', gid='floating_object') + arrowprops=dict(arrowstyle='->'), gid='floating_object') + self.ax.plot(obj.longitude, obj.latitude, icon, + label='Titanic', gid='floating_object') elif isinstance(obj, Iceberg): - self.ax.plot(obj.longitude, obj.latitude, 'w^', label='Iceberg', gid='floating_object') + self.ax.plot(obj.longitude, obj.latitude, 'w^', + label='Iceberg', gid='floating_object') # Redraw the plot plt.draw() diff --git a/demo/Titanic/simulation/ocean.py b/demo/Titanic/simulation/ocean.py index 68319189..0cba43f6 100644 --- a/demo/Titanic/simulation/ocean.py +++ b/demo/Titanic/simulation/ocean.py @@ -3,10 +3,12 @@ from simulation.floating_object import FloatingObject from simulation.location_on_grid import LocationOnGrid, AreaOnGrid -SECONDS_IN_MINUTE = 60 -COLLISION_INTERVAL = 10 # Following should hold True; SECONDS_IN_MINUTE % COLLISION_INTERVAL == 0 +SECONDS_IN_MINUTE = 60 +# Following should hold True; SECONDS_IN_MINUTE % COLLISION_INTERVAL == 0 +COLLISION_INTERVAL = 10 COLLISION_THRESHOLD = 0.6 + class Ocean: _instance = None @@ -28,8 +30,10 @@ def minute_passes(self): objects_collided = set() while seconds_passed < SECONDS_IN_MINUTE: for floating_object in floating_objects: - floating_object.update_coordinates(time_passed=COLLISION_INTERVAL) - objects_collided.update(self.detect_collisions(collision_threshold=COLLISION_THRESHOLD)) + floating_object.update_coordinates( + time_passed=COLLISION_INTERVAL) + objects_collided.update(self.detect_collisions( + collision_threshold=COLLISION_THRESHOLD)) if objects_collided: floating_objects.difference(objects_collided) seconds_passed += COLLISION_INTERVAL diff --git a/demo/Titanic/simulation/titanic_in_ocean.py b/demo/Titanic/simulation/titanic_in_ocean.py index 10990711..b41dd042 100644 --- a/demo/Titanic/simulation/titanic_in_ocean.py +++ b/demo/Titanic/simulation/titanic_in_ocean.py @@ -41,4 +41,3 @@ def __str__(self): def __repr__(self): return super().__repr__() + f", sunk={self.sunk}))" - diff --git a/demo/Titanic/system/titanic.py b/demo/Titanic/system/titanic.py index b1a3ac62..2d9b98e6 100644 --- a/demo/Titanic/system/titanic.py +++ b/demo/Titanic/system/titanic.py @@ -3,7 +3,8 @@ class Titanic: def __init__(self, throttle, steering_direction): self.throttle = throttle # percentage - self.steering_direction = steering_direction # degrees (0 means steering straight) + # degrees (0 means steering straight) + self.steering_direction = steering_direction self.damaged = False def __repr__(self): diff --git a/robotmbt/__init__.py b/robotmbt/__init__.py index 6ea102f8..15f9b3e0 100644 --- a/robotmbt/__init__.py +++ b/robotmbt/__init__.py @@ -33,9 +33,11 @@ from .version import VERSION from .suitereplacer import SuiteReplacer + class robotmbt(SuiteReplacer): """ Process test suites on-the-fly to optimise test suite execution """ + __version__ = VERSION diff --git a/robotmbt/modelspace.py b/robotmbt/modelspace.py index b01d2a66..d010514e 100644 --- a/robotmbt/modelspace.py +++ b/robotmbt/modelspace.py @@ -34,15 +34,17 @@ from .steparguments import StepArguments + class ModellingError(Exception): pass + class ModelSpace: def __init__(self, reference_id=None): self.ref_id = str(reference_id) self.std_attrs = [] self.props = dict() - self.values = dict() # For using literals without having to use quotes (abc='abc') + self.values = dict() # For using literals without having to use quotes (abc='abc') self.scenario_vars = [] self.std_attrs = dir(self) @@ -65,7 +67,8 @@ def add_prop(self, name): def del_prop(self, name): if name == 'scenario': - raise ModellingError(f"scenario is a reserved attribute and cannot be removed.") + raise ModellingError( + f"scenario is a reserved attribute and cannot be removed.") if name not in self.props: raise ModellingError(f"Delete failed, '{name}' is not defined.") self.props.pop(name) @@ -78,11 +81,13 @@ def __dir__(self, recurse=True): return self.__dict__.keys() def new_scenario_scope(self): - self.scenario_vars.append(RecursiveScope(self.scenario_vars[-1] if len(self.scenario_vars) else None)) + self.scenario_vars.append(RecursiveScope( + self.scenario_vars[-1] if len(self.scenario_vars) else None)) self.props['scenario'] = self.scenario_vars[-1] def end_scenario_scope(self): - assert len(self.scenario_vars) > 0, ".end_scenario_scope() called, but there is no scenario scope open." + assert len( + self.scenario_vars) > 0, ".end_scenario_scope() called, but there is no scenario scope open." self.scenario_vars.pop() if len(self.scenario_vars): self.props['scenario'] = self.scenario_vars[-1] @@ -103,7 +108,8 @@ def process_expression(self, expression, step_args=StepArguments()): for p in self.props: exec(f"{p} = self.props['{p}']", local_locals) for v in self.values: - value = f"'{self.values[v]}'" if isinstance(self.values[v], str) else self.values[v] + value = f"'{self.values[v]}'" if isinstance( + self.values[v], str) else self.values[v] exec(f"{v} = {value}", local_locals) try: result = eval(expr, local_locals) @@ -118,7 +124,7 @@ def process_expression(self, expression, step_args=StepArguments()): self.__handle_attribute_error(err) except NameError as missing: if missing.name == expr: - raise # Putting only a name in an expression can be used as exists check + raise # Putting only a name in an expression can be used as exists check self.__add_alias(missing.name, step_args) result = self.process_expression(expression, step_args) except AttributeError as err: @@ -141,12 +147,14 @@ def __add_alias(self, missing_name, step_args): if missing_name == 'scenario': raise ModellingError("Accessing scenario scope while there is no scenario active.\n" "If you intended this to be a literal, please use quotes ('scenario' or \"scenario\").") - matching_args = [arg.value for arg in step_args if arg.codestring == missing_name] + matching_args = [ + arg.value for arg in step_args if arg.codestring == missing_name] value = matching_args[0] if matching_args else missing_name if isinstance(value, str): - for esc_char in "$@&=": # Prevent "Syntaxwarning: invalid escape sequence" on Robot escapes like '\$' and '\=' + for esc_char in "$@&=": # Prevent "Syntaxwarning: invalid escape sequence" on Robot escapes like '\$' and '\=' value = value.replace(f'\\{esc_char}', f'\\\\{esc_char}') - value = value.replace("'", r"\'") # Needed because we use single quotes in low level processing later on + # Needed because we use single quotes in low level processing later on + value = value.replace("'", r"\'") self.values[missing_name] = value @staticmethod @@ -177,6 +185,7 @@ def get_status_text(self): status += f" {attr}={value}\n" return status + class RecursiveScope: """ Generic scoping object with the properties needed for handling scenario variables with refinement. @@ -190,6 +199,7 @@ class RecursiveScope: executed on the highest available level. Creating new attributes, will make the current level the highest available level for that atrribute. """ + def __init__(self, outer): super().__setattr__('_outer_scope', outer) @@ -206,7 +216,7 @@ def __setattr__(self, attr, value): def __iter__(self): return iter([(attr, getattr(self, attr)) for attr in dir(self._outer_scope) + dir(self) - if not attr.startswith('__') and attr != '_outer_scope']) + if not attr.startswith('__') and attr != '_outer_scope']) def __bool__(self): return any(True for _ in self) diff --git a/robotmbt/steparguments.py b/robotmbt/steparguments.py index aa62d604..c265d4a8 100644 --- a/robotmbt/steparguments.py +++ b/robotmbt/steparguments.py @@ -55,6 +55,7 @@ def __getitem__(self, key): def modified(self): return any([arg.modified for arg in self]) + class StepArgument: # kind list EMBEDDED = 'EMBEDDED' @@ -66,13 +67,13 @@ class StepArgument: def __init__(self, arg_name, value, kind=None, is_default=False): self.name = arg_name self.org_value = value - self.kind = kind # one of the values from the kind list + self.kind = kind # one of the values from the kind list self._value = None self._codestr = None self.value = value - self.is_default = is_default # indicates that the argument was not - # filled in from the scenario. This argment's value is taken - # from the keyword's default as provided by Robot. + self.is_default = is_default # indicates that the argument was not + # filled in from the scenario. This argment's value is taken + # from the keyword's default as provided by Robot. @property def arg(self): @@ -97,7 +98,8 @@ def codestring(self): return self._codestr def copy(self): - cp = StepArgument(self.arg.strip('${}'), self.value, self.kind, self.is_default) + cp = StepArgument(self.arg.strip('${}'), self.value, + self.kind, self.is_default) cp.org_value = self.org_value return cp diff --git a/robotmbt/substitutionmap.py b/robotmbt/substitutionmap.py index 29ba0a25..4bc736e9 100644 --- a/robotmbt/substitutionmap.py +++ b/robotmbt/substitutionmap.py @@ -41,8 +41,9 @@ class SubstitutionMap: constraints. solve() takes the current set of example values and assigns a unique concrete value to each. """ + def __init__(self): - self.substitutions = {} # {example_value:Constraint} + self.substitutions = {} # {example_value:Constraint} self.solution = {} # {example_value:solution_value} def __str__(self): @@ -51,7 +52,8 @@ def __str__(self): def copy(self): new = SubstitutionMap() - new.substitutions = {k: v.copy() for k,v in self.substitutions.items()} + new.substitutions = {k: v.copy() + for k, v in self.substitutions.items()} new.solution = self.solution.copy() return new @@ -71,7 +73,8 @@ def solve(self): while unsolved_subs: unsolved_subs.sort(key=lambda i: len(substitutions[i].optionset)) example_value = unsolved_subs[0] - solution[example_value] = random.choice(substitutions[example_value].optionset) + solution[example_value] = random.choice( + substitutions[example_value].optionset) subs_stack.append(example_value) others_list = [] try: @@ -95,13 +98,15 @@ def solve(self): subs_stack.pop() except IndexError: # nothing left to roll back, no options remaining - raise ValueError("No solution found within the set of given constraints") + raise ValueError( + "No solution found within the set of given constraints") last_item = subs_stack[-1] unsolved_subs.insert(0, last_item) for other in [e for e in substitutions if e != last_item]: substitutions[other].undo_remove() try: - substitutions[last_item].remove_option(solution.pop(last_item)) + substitutions[last_item].remove_option( + solution.pop(last_item)) rollback_done = True except ValueError: # next level must also be rolled back @@ -119,7 +124,8 @@ def __init__(self, constraint): except: self.optionset = None if not self.optionset or isinstance(constraint, str): - raise ValueError(f"Invalid option set for initial constraint: {constraint}") + raise ValueError( + f"Invalid option set for initial constraint: {constraint}") self.removed_stack = [] def __repr__(self): @@ -132,7 +138,8 @@ def copy(self): return Constraint(self.optionset) def add_constraint(self, constraint): - if constraint is None: return + if constraint is None: + return self.optionset = [opt for opt in self.optionset if opt in constraint] if not len(self.optionset): raise ValueError('No options left after adding constraint') diff --git a/robotmbt/suitedata.py b/robotmbt/suitedata.py index b9ca651a..5c3d43a4 100644 --- a/robotmbt/suitedata.py +++ b/robotmbt/suitedata.py @@ -36,6 +36,7 @@ from .steparguments import StepArgument, StepArguments + class Suite: def __init__(self, name, parent=None): self.name = name @@ -43,32 +44,35 @@ def __init__(self, name, parent=None): self.parent = parent self.suites = [] self.scenarios = [] - self.setup = None # Can be a single step or None - self.teardown = None # Can be a single step or None + self.setup = None # Can be a single step or None + self.teardown = None # Can be a single step or None @property def longname(self): return f"{self.parent.longname}.{self.name}" if self.parent else self.name def has_error(self): - return ( (self.setup.has_error() if self.setup else False) - or any([s.has_error() for s in self.suites]) - or any([s.has_error() for s in self.scenarios]) - or (self.teardown.has_error() if self.teardown else False)) + return ((self.setup.has_error() if self.setup else False) + or any([s.has_error() for s in self.suites]) + or any([s.has_error() for s in self.scenarios]) + or (self.teardown.has_error() if self.teardown else False)) def steps_with_errors(self): - return ( ([self.setup] if self.setup and self.setup.has_error() else []) - + [e for s in map(Suite.steps_with_errors, self.suites) for e in s] - + [e for s in map(Scenario.steps_with_errors, self.scenarios) for e in s] - + ([self.teardown] if self.teardown and self.teardown.has_error() else [])) + return (([self.setup] if self.setup and self.setup.has_error() else []) + + [e for s in map(Suite.steps_with_errors, self.suites) + for e in s] + + [e for s in map(Scenario.steps_with_errors, + self.scenarios) for e in s] + + ([self.teardown] if self.teardown and self.teardown.has_error() else [])) + class Scenario: def __init__(self, name, parent=None): self.name = name - self.parent = parent # Parent scenario for easy searching, processing and referencing - # after steps and scenarios have been potentially moved around + self.parent = parent # Parent scenario for easy searching, processing and referencing + # after steps and scenarios have been potentially moved around self.setup = None # Can be a single step or None - self.teardown = None # Can be a single step or None + self.teardown = None # Can be a single step or None self.steps = [] self.src_id = None self.data_choices = {} @@ -79,13 +83,13 @@ def longname(self): def has_error(self): return ((self.setup.has_error() if self.setup else False) - or any([s.has_error() for s in self.steps]) - or (self.teardown.has_error() if self.teardown else False)) + or any([s.has_error() for s in self.steps]) + or (self.teardown.has_error() if self.teardown else False)) def steps_with_errors(self): - return ( ([self.setup] if self.setup and self.setup.has_error() else []) - + [s for s in self.steps if s.has_error()] - + ([self.teardown] if self.teardown and self.teardown.has_error() else [])) + return (([self.setup] if self.setup and self.setup.has_error() else []) + + [s for s in self.steps if s.has_error()] + + ([self.teardown] if self.teardown and self.teardown.has_error() else [])) def copy(self): duplicate = copy.copy(self) @@ -99,7 +103,8 @@ def split_at_step(self, stepindex): With stepindex 0 the first part has no steps and all steps are in the last part. With stepindex 1 the first step is in the first part, the other in the last part, and so on. """ - assert stepindex <= len(self.steps), "Split index out of range. Not enough steps in scenario." + assert stepindex <= len( + self.steps), "Split index out of range. Not enough steps in scenario." front = self.copy() front.teardown = None front.steps = self.steps[:stepindex] @@ -108,27 +113,36 @@ def split_at_step(self, stepindex): back.setup = None return front, back + class Step: def __init__(self, steptext, *args, parent, assign=(), prev_gherkin_kw=None): self.org_step = steptext # first keyword cell of the Robot line, including step_kw, - # excluding positional args, excluding variable assignment. - self.org_pn_args = args # positional and named arguments as parsed from Robot text ('posA' , 'posB', 'named1=namedA') - self.parent = parent # Parent scenario for easy searching and processing. - self.assign = assign # For when a keyword's return value is assigned to a variable. - # Taken directly from Robot. - self.gherkin_kw = self.step_kw if str(self.step_kw).lower() in ['given', 'when', 'then', 'none'] else prev_gherkin_kw - # 'given', 'when', 'then' or None for non-bdd keywords. - self.signature = None # Robot keyword with its embedded arguments in ${...} notation. - self.args = StepArguments() # embedded arguments list of StepArgument objects. - self.detached = False # Decouples StepArguments from the step text (refinement use case) - self.model_info = dict() # Modelling information is available as a dictionary. - # The standard format is dict(IN=[], OUT=[]) and can - # optionally contain an error field. - # IN and OUT are lists of Python evaluatable expressions. - # The `new vocab` form can be used to create new domain objects. - # The `vocab.attribute` form can then be used to express relations - # between properties from the domain vocabulaire. - # Custom processors can define their own attributes. + # excluding positional args, excluding variable assignment. + # positional and named arguments as parsed from Robot text ('posA' , 'posB', 'named1=namedA') + self.org_pn_args = args + # Parent scenario for easy searching and processing. + self.parent = parent + # For when a keyword's return value is assigned to a variable. + self.assign = assign + # Taken directly from Robot. + self.gherkin_kw = self.step_kw if str(self.step_kw).lower( + ) in ['given', 'when', 'then', 'none'] else prev_gherkin_kw + # 'given', 'when', 'then' or None for non-bdd keywords. + # Robot keyword with its embedded arguments in ${...} notation. + self.signature = None + # embedded arguments list of StepArgument objects. + self.args = StepArguments() + # Decouples StepArguments from the step text (refinement use case) + self.detached = False + # Modelling information is available as a dictionary. + self.model_info = dict() + # The standard format is dict(IN=[], OUT=[]) and can + # optionally contain an error field. + # IN and OUT are lists of Python evaluatable expressions. + # The `new vocab` form can be used to create new domain objects. + # The `vocab.attribute` form can then be used to express relations + # between properties from the domain vocabulaire. + # Custom processors can define their own attributes. def __str__(self): return self.keyword @@ -137,7 +151,8 @@ def __repr__(self): return f"Step: '{self}' with model info: {self.model_info}" def copy(self): - cp = Step(self.org_step, *self.org_pn_args, parent=self.parent, assign=self.assign) + cp = Step(self.org_step, *self.org_pn_args, + parent=self.parent, assign=self.assign) cp.gherkin_kw = self.gherkin_kw cp.signature = self.signature cp.args = StepArguments(self.args) @@ -197,7 +212,7 @@ def gherkin_kw(self, value): @property def step_kw(self): first_word = self.org_step.split()[0] - return first_word if first_word.lower() in ['given','when','then','and','but'] else None + return first_word if first_word.lower() in ['given', 'when', 'then', 'and', 'but'] else None @property def kw_wo_gherkin(self): @@ -216,34 +231,41 @@ def add_robot_dependent_data(self, robot_kw): self.signature = robot_kw.name self.model_info = self.__parse_model_info(robot_kw._doc) except Exception as ex: - self.model_info['error']=str(ex) + self.model_info['error'] = str(ex) def __handle_non_embedded_arguments(self, robot_argspec): result = [] p_args = [a for a in self.org_pn_args if '=' not in a or r'\=' in a] - n_args = [a.split('=', 1) for a in self.org_pn_args if '=' in a and r'\=' not in a] + n_args = [a.split('=', 1) + for a in self.org_pn_args if '=' in a and r'\=' not in a] self.__validate_arguments(robot_argspec, p_args, n_args) robot_args = [a for a in robot_argspec] - argument_names = [a for a in robot_argspec.argument_names if a not in robot_argspec.embedded] + argument_names = [ + a for a in robot_argspec.argument_names if a not in robot_argspec.embedded] for arg in robot_argspec: if not p_args or (arg.kind != arg.POSITIONAL_ONLY and arg.kind != arg.POSITIONAL_OR_NAMED): break - result.append(StepArgument(argument_names.pop(0), p_args.pop(0), kind=StepArgument.POSITIONAL)) + result.append(StepArgument(argument_names.pop( + 0), p_args.pop(0), kind=StepArgument.POSITIONAL)) robot_args.pop(0) if p_args and robot_args[0].kind == robot_args[0].VAR_POSITIONAL: - result.append(StepArgument(argument_names.pop(0), p_args, kind=StepArgument.VAR_POS)) + result.append(StepArgument(argument_names.pop(0), + p_args, kind=StepArgument.VAR_POS)) free = {} for name, value in n_args: if name in argument_names: - result.append(StepArgument(name, value, kind=StepArgument.NAMED)) + result.append(StepArgument( + name, value, kind=StepArgument.NAMED)) argument_names.remove(name) else: free[name] = value if free: - result.append(StepArgument(argument_names.pop(-1), free, kind=StepArgument.FREE_NAMED)) + result.append(StepArgument(argument_names.pop(-1), + free, kind=StepArgument.FREE_NAMED)) for unmentioned_arg in argument_names: - arg = next(arg for arg in robot_args if arg.name == unmentioned_arg) + arg = next(arg for arg in robot_args if arg.name == + unmentioned_arg) default_value = arg.default if default_value is robot.utils.notset.NOT_SET: if arg.kind == arg.VAR_POSITIONAL: @@ -255,19 +277,22 @@ def __handle_non_embedded_arguments(self, robot_argspec): # but use different names in the method signature. Robot Framework implementation is incomplete for this # aspect and differs between library and user keywords. assert False, f"No default argument expected to be needed for '{unmentioned_arg}' here" - result.append(StepArgument(unmentioned_arg, default_value, kind=StepArgument.NAMED, is_default=True)) + result.append(StepArgument(unmentioned_arg, default_value, + kind=StepArgument.NAMED, is_default=True)) return result @staticmethod def __validate_arguments(spec, positionals, nameds): - p, n = spec.map(positionals, nameds) # Robot uses a slightly different mapping for positional and named arguments. - # We keep the notation from the scenario (with or without the argument's name). - # Robot's mapping favours positional when possible, even when the name is used - # in the keyword call. The validator is sensitive to these differences. + # Robot uses a slightly different mapping for positional and named arguments. + p, n = spec.map(positionals, nameds) + # We keep the notation from the scenario (with or without the argument's name). + # Robot's mapping favours positional when possible, even when the name is used + # in the keyword call. The validator is sensitive to these differences. if p == [None]: # for some reason .map() returns [None] instead of the empty list when there are no arguments p = [] - ArgumentValidator(spec).validate(p, n) # Use the Robot mechanism for validation to yield familiar error messages + # Use the Robot mechanism for validation to yield familiar error messages + ArgumentValidator(spec).validate(p, n) def __parse_model_info(self, docu): model_info = dict() @@ -289,7 +314,8 @@ def __parse_model_info(self, docu): key = elms[1].strip() expressions = [e.strip() for e in elms[-1].split("|") if e] while lines and not lines[0].startswith(":"): - expressions.extend([e.strip() for e in lines.pop(0).split("|") if e]) + expressions.extend([e.strip() + for e in lines.pop(0).split("|") if e]) model_info[key] = expressions if not model_info: raise ValueError("When present, *model info* cannot be empty") diff --git a/robotmbt/suiteprocessors.py b/robotmbt/suiteprocessors.py index c66ab1e3..a7aa919a 100644 --- a/robotmbt/suiteprocessors.py +++ b/robotmbt/suiteprocessors.py @@ -42,6 +42,7 @@ from .tracestate import TraceState from .steparguments import StepArgument, StepArguments + class SuiteProcessors: def echo(self, in_suite): return in_suite @@ -85,7 +86,7 @@ def process_test_suite(self, in_suite, *, seed='new'): scenario.src_id = id self.scenarios = self.flat_suite.scenarios[:] logger.debug("Use these numbers to reference scenarios from traces\n\t" + - "\n\t".join([f"{s.src_id}: {s.name}" for s in self.scenarios])) + "\n\t".join([f"{s.src_id}: {s.name}" for s in self.scenarios])) self._init_randomiser(seed) random.shuffle(self.scenarios) @@ -94,7 +95,8 @@ def process_test_suite(self, in_suite, *, seed='new'): self._try_to_reach_full_coverage(allow_duplicate_scenarios=False) if not self.tracestate.coverage_reached(): - logger.debug("Direct trace not available. Allowing repetition of scenarios") + logger.debug( + "Direct trace not available. Allowing repetition of scenarios") self._try_to_reach_full_coverage(allow_duplicate_scenarios=True) if not self.tracestate.coverage_reached(): raise Exception("Unable to compose a consistent suite") @@ -107,13 +109,14 @@ def _try_to_reach_full_coverage(self, allow_duplicate_scenarios): self.tracestate = TraceState(len(self.scenarios)) self.active_model = ModelSpace() while not self.tracestate.coverage_reached(): - i_candidate = self.tracestate.next_candidate(retry=allow_duplicate_scenarios) + i_candidate = self.tracestate.next_candidate( + retry=allow_duplicate_scenarios) if i_candidate is None: if not self.tracestate.can_rewind(): break tail = self._rewind() logger.debug("Having to roll back up to " - f"{tail.scenario.name if tail else 'the beginning'}") + f"{tail.scenario.name if tail else 'the beginning'}") self._report_tracestate_to_user() else: self.active_model.new_scenario_scope() @@ -122,14 +125,16 @@ def _try_to_reach_full_coverage(self, allow_duplicate_scenarios): if inserted: self.DROUGHT_LIMIT = 50 if self.__last_candidate_changed_nothing(): - logger.debug("Repeated scenario did not change the model's state. Stop trying.") + logger.debug( + "Repeated scenario did not change the model's state. Stop trying.") self._rewind() elif self.tracestate.coverage_drought > self.DROUGHT_LIMIT: logger.debug(f"Went too long without new coverage (>{self.DROUGHT_LIMIT}x). " "Roll back to last coverage increase and try something else.") self._rewind(drought_recovery=True) self._report_tracestate_to_user() - logger.debug(f"last state:\n{self.active_model.get_status_text()}") + logger.debug( + f"last state:\n{self.active_model.get_status_text()}") def __last_candidate_changed_nothing(self): if len(self.tracestate) < 2: @@ -154,28 +159,33 @@ def _fail_on_step_errors(suite): if error_list: err_msg = "Steps with errors in their model info found:\n" err_msg += '\n'.join([f"{s.keyword} [{s.model_info['error']}] used in {s.parent.name}" - for s in error_list]) + for s in error_list]) raise Exception(err_msg) def _try_to_fit_in_scenario(self, index, candidate, retry_flag): - candidate = self._generate_scenario_variant(candidate, self.active_model) + candidate = self._generate_scenario_variant( + candidate, self.active_model) if not candidate: self.active_model.end_scenario_scope() self.tracestate.reject_scenario(index) self._report_tracestate_to_user() return False - confirmed_candidate, new_model = self._process_scenario(candidate, self.active_model) + confirmed_candidate, new_model = self._process_scenario( + candidate, self.active_model) if confirmed_candidate: self.active_model = new_model self.active_model.end_scenario_scope() - self.tracestate.confirm_full_scenario(index, confirmed_candidate, self.active_model) - logger.debug(f"Inserted scenario {confirmed_candidate.src_id}, {confirmed_candidate.name}") + self.tracestate.confirm_full_scenario( + index, confirmed_candidate, self.active_model) + logger.debug( + f"Inserted scenario {confirmed_candidate.src_id}, {confirmed_candidate.name}") self._report_tracestate_to_user() logger.debug(f"last state:\n{self.active_model.get_status_text()}") return True - part1, part2 = self._split_candidate_if_refinement_needed(candidate, self.active_model) + part1, part2 = self._split_candidate_if_refinement_needed( + candidate, self.active_model) if part2: exit_conditions = part2.steps[1].model_info['OUT'] part1.name = f"{part1.name} (part {self.tracestate.highest_part(index)+1})" @@ -187,13 +197,15 @@ def _try_to_fit_in_scenario(self, index, candidate, retry_flag): i_refine = self.tracestate.next_candidate(retry=retry_flag) if i_refine is None: - logger.debug("Refinement needed, but there are no scenarios left") + logger.debug( + "Refinement needed, but there are no scenarios left") self._rewind() self._report_tracestate_to_user() return False while i_refine is not None: self.active_model.new_scenario_scope() - m_inserted = self._try_to_fit_in_scenario(i_refine, self._scenario_with_repeat_counter(i_refine), retry_flag) + m_inserted = self._try_to_fit_in_scenario( + i_refine, self._scenario_with_repeat_counter(i_refine), retry_flag) if m_inserted: insert_valid_here = True try: @@ -201,18 +213,22 @@ def _try_to_fit_in_scenario(self, index, candidate, retry_flag): model_scratchpad = self.active_model.copy() for expr in exit_conditions: if model_scratchpad.process_expression(expr, part2.steps[1].args) is False: - insert_valid_here = False - break + insert_valid_here = False + break except Exception: insert_valid_here = False if insert_valid_here: - m_finished = self._try_to_fit_in_scenario(index, part2, retry_flag) + m_finished = self._try_to_fit_in_scenario( + index, part2, retry_flag) if m_finished: return True else: - logger.debug(f"Scenario did not meet refinement conditions {exit_conditions}") - logger.debug(f"last state:\n{self.active_model.get_status_text()}") - logger.debug(f"Reconsidering {self.scenarios[i_refine].name}, scenario excluded") + logger.debug( + f"Scenario did not meet refinement conditions {exit_conditions}") + logger.debug( + f"last state:\n{self.active_model.get_status_text()}") + logger.debug( + f"Reconsidering {self.scenarios[i_refine].name}, scenario excluded") self._rewind() self._report_tracestate_to_user() i_refine = self.tracestate.next_candidate(retry=retry_flag) @@ -253,23 +269,30 @@ def _split_candidate_if_refinement_needed(scenario, model): try: if m.process_expression(expr, step.args) is False: if step.gherkin_kw in ['when', None]: - logger.debug(f"Refinement needed for scenario: {scenario.name}\nat step: {step}") + logger.debug( + f"Refinement needed for scenario: {scenario.name}\nat step: {step}") refine_here = True else: return no_split except Exception: return no_split if refine_here: - front, back = scenario.split_at_step(scenario.steps.index(step)) - remaining_steps = '\n\t'.join([step.full_keyword, '- '*35] + [s.full_keyword for s in back.steps[1:]]) - remaining_steps = SuiteProcessors.escape_robot_vars(remaining_steps) - edge_step = Step('Log', f"Refinement follows for step:\n\t{remaining_steps}", parent=scenario) + front, back = scenario.split_at_step( + scenario.steps.index(step)) + remaining_steps = '\n\t'.join( + [step.full_keyword, '- '*35] + [s.full_keyword for s in back.steps[1:]]) + remaining_steps = SuiteProcessors.escape_robot_vars( + remaining_steps) + edge_step = Step( + 'Log', f"Refinement follows for step:\n\t{remaining_steps}", parent=scenario) edge_step.gherkin_kw = step.gherkin_kw - edge_step.model_info = dict(IN=step.model_info['IN'], OUT=[]) + edge_step.model_info = dict( + IN=step.model_info['IN'], OUT=[]) edge_step.detached = True edge_step.args = StepArguments(step.args) front.steps.append(edge_step) - back.steps.insert(0, Step('Log', f"Refinement ready, completing step", parent=scenario)) + back.steps.insert( + 0, Step('Log', f"Refinement ready, completing step", parent=scenario)) back.steps[1] = back.steps[1].copy() back.steps[1].model_info['IN'] = [] return (front, back) @@ -287,7 +310,8 @@ def _process_scenario(scenario, model): scenario = scenario.copy() for step in scenario.steps: if 'error' in step.model_info: - logger.debug(f"Error in scenario {scenario.name} at step {step}: {step.model_info['error']}") + logger.debug( + f"Error in scenario {scenario.name} at step {step}: {step.model_info['error']}") return None, None for expr in SuiteProcessors._relevant_expressions(step): try: @@ -302,7 +326,7 @@ def _process_scenario(scenario, model): @staticmethod def _relevant_expressions(step): if step.gherkin_kw is None and not step.model_info: - return [] # model info is optional for action keywords + return [] # model info is optional for action keywords expressions = [] if 'IN' not in step.model_info or 'OUT' not in step.model_info: raise Exception(f"Model info incomplete for step: {step}") @@ -328,48 +352,59 @@ def _generate_scenario_variant(self, scenario, model): for step in scenario.steps: if 'MOD' in step.model_info: for expr in step.model_info['MOD']: - modded_arg, constraint = self._parse_modifier_expression(expr, step.args) + modded_arg, constraint = self._parse_modifier_expression( + expr, step.args) if step.args[modded_arg].is_default: continue if step.args[modded_arg].kind in [StepArgument.EMBEDDED, StepArgument.POSITIONAL, StepArgument.NAMED]: org_example = step.args[modded_arg].org_value if step.gherkin_kw == 'then': - constraint = None # No new constraints are processed for then-steps + constraint = None # No new constraints are processed for then-steps if org_example not in subs.substitutions: # if a then-step signals the first use of an example value, it is considered a new definition subs.substitute(org_example, [org_example]) continue if not constraint and org_example not in subs.substitutions: - raise ValueError(f"No options to choose from at first assignment to {org_example}") + raise ValueError( + f"No options to choose from at first assignment to {org_example}") if constraint and constraint != '.*': - options = m.process_expression(constraint, step.args) + options = m.process_expression( + constraint, step.args) if options == 'exec': - raise ValueError(f"Invalid constraint for argument substitution: {expr}") + raise ValueError( + f"Invalid constraint for argument substitution: {expr}") if not options: - raise ValueError(f"Constraint on modifer did not yield any options: {expr}") + raise ValueError( + f"Constraint on modifer did not yield any options: {expr}") if not is_list_like(options): - raise ValueError(f"Constraint on modifer did not yield a set of options: {expr}") + raise ValueError( + f"Constraint on modifer did not yield a set of options: {expr}") else: options = None subs.substitute(org_example, options) elif step.args[modded_arg].kind == StepArgument.VAR_POS: if step.args[modded_arg].value: - modded_varargs = m.process_expression(constraint, step.args) + modded_varargs = m.process_expression( + constraint, step.args) if not is_list_like(modded_varargs): - raise ValueError(f"Modifying varargs must yield a list of arguments") + raise ValueError( + f"Modifying varargs must yield a list of arguments") # Varargs are not added to the substitution map, but are used directly as-is. A modifier can # change the number of arguments in the list, making it impossible to decide which values to # match and which to drop and/or duplicate. step.args[modded_arg].value = modded_varargs elif step.args[modded_arg].kind == StepArgument.FREE_NAMED: if step.args[modded_arg].value: - modded_free_args = m.process_expression(constraint, step.args) + modded_free_args = m.process_expression( + constraint, step.args) if not isinstance(modded_free_args, dict): - raise ValueError("Modifying free named arguments must yield a dict") + raise ValueError( + "Modifying free named arguments must yield a dict") # Similar to varargs, modified free named arguments are used directly as-is. step.args[modded_arg].value = modded_free_args else: - raise AssertionError(f"Unknown argument kind for {modded_arg}") + raise AssertionError( + f"Unknown argument kind for {modded_arg}") except Exception as err: logger.debug(f"Unable to insert scenario {scenario.src_id}, {scenario.name}, due to modifier\n" f" In step {step}: {err}") @@ -378,17 +413,20 @@ def _generate_scenario_variant(self, scenario, model): try: subs.solve() except ValueError as err: - logger.debug(f"Unable to insert scenario {scenario.src_id}, {scenario.name}, due to modifier\n {err}: {subs}") + logger.debug( + f"Unable to insert scenario {scenario.src_id}, {scenario.name}, due to modifier\n {err}: {subs}") return None # Update scenario with generated values if subs.solution: - logger.debug(f"Example variant generated with argument substitution: {subs}") + logger.debug( + f"Example variant generated with argument substitution: {subs}") scenario.data_choices = subs for step in scenario.steps: if 'MOD' in step.model_info: for expr in step.model_info['MOD']: - modded_arg, _ = self._parse_modifier_expression(expr, step.args) + modded_arg, _ = self._parse_modifier_expression( + expr, step.args) if step.args[modded_arg].is_default: continue org_example = step.args[modded_arg].org_value @@ -401,9 +439,10 @@ def _parse_modifier_expression(expression, args): if expression.startswith('${'): for var in args: if expression.casefold().startswith(var.arg.casefold()): - assignment_expr = expression.replace(var.arg, '', 1).strip() + assignment_expr = expression.replace( + var.arg, '', 1).strip() if not assignment_expr.startswith('=') or assignment_expr.startswith('=='): - break # not an assignment + break # not an assignment constraint = assignment_expr.replace('=', '', 1).strip() return var.arg, constraint raise ValueError(f"Invalid argument substitution: {expression}") @@ -414,7 +453,8 @@ def _report_tracestate_to_user(self): part = f".{snapshot.id.split('.')[1]}" if '.' in snapshot.id else "" user_trace += f"{snapshot.scenario.src_id}{part}, " user_trace = user_trace[:-2] + "]" if ',' in user_trace else "[]" - reject_trace = [self.scenarios[i].src_id for i in self.tracestate.tried] + reject_trace = [ + self.scenarios[i].src_id for i in self.tracestate.tried] logger.debug(f"Trace: {user_trace} Reject: {reject_trace}") def _report_tracestate_wrapup(self): @@ -428,7 +468,8 @@ def _init_randomiser(seed): if isinstance(seed, str): seed = seed.strip() if str(seed).lower() == 'none': - logger.info(f"Using system's random seed for trace generation. This trace cannot be rerun. Use `seed=new` to generate a reusable seed.") + logger.info( + f"Using system's random seed for trace generation. This trace cannot be rerun. Use `seed=new` to generate a reusable seed.") elif str(seed).lower() == 'new': new_seed = SuiteProcessors._generate_seed() logger.info(f"seed={new_seed} (use seed to rerun this trace)") @@ -441,14 +482,17 @@ def _init_randomiser(seed): def _generate_seed(): """Creates a random string of 5 words between 3 and 6 letters long""" vowels = ['a', 'e', 'i', 'o', 'u', 'y'] - consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'z'] + consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', + 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'z'] words = [] for word in range(5): prior_choice = random.choice([vowels, consonants]) - last_choice = random.choice([vowels, consonants]) - string = random.choice(prior_choice) + random.choice(last_choice) # add first two letters - for letter in range(random.randint(1, 4)): # add 1 to 4 more letters + last_choice = random.choice([vowels, consonants]) + # add first two letters + string = random.choice(prior_choice) + random.choice(last_choice) + # add 1 to 4 more letters + for letter in range(random.randint(1, 4)): if prior_choice is last_choice: new_choice = consonants if prior_choice is vowels else vowels else: diff --git a/robotmbt/suitereplacer.py b/robotmbt/suitereplacer.py index 8a2e79fc..509c07de 100644 --- a/robotmbt/suitereplacer.py +++ b/robotmbt/suitereplacer.py @@ -30,13 +30,14 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from robot.libraries.BuiltIn import BuiltIn;Robot = BuiltIn() -from robot.api.deco import keyword -from robot.api import logger +from .suitedata import Suite, Scenario, Step +from .suiteprocessors import SuiteProcessors import robot.running.model as rmodel +from robot.api import logger +from robot.api.deco import keyword +from robot.libraries.BuiltIn import BuiltIn +Robot = BuiltIn() -from .suiteprocessors import SuiteProcessors -from .suitedata import Suite, Scenario, Step class SuiteReplacer: ROBOT_LIBRARY_SCOPE = 'GLOBAL' @@ -56,15 +57,17 @@ def __init__(self, processor='process_test_suite', processor_lib=None): def processor_lib(self): if self._processor_lib is None: self._processor_lib = SuiteProcessors() if self.processor_lib_name is None \ - else Robot.get_library_instance(self.processor_lib_name) + else Robot.get_library_instance(self.processor_lib_name) return self._processor_lib @property def processor_method(self): if self._processor_method is None: if not hasattr(self.processor_lib, self.processor_name): - Robot.fail(f"Processor '{self.processor_name}' not available for model-based processor library {self.processor_lib_name}") - self._processor_method = getattr(self._processor_lib, self.processor_name) + Robot.fail( + f"Processor '{self.processor_name}' not available for model-based processor library {self.processor_lib_name}") + self._processor_method = getattr( + self._processor_lib, self.processor_name) return self._processor_method @keyword(name="Treat this test suite Model-based") @@ -84,11 +87,14 @@ def treat_model_based(self, **kwargs): """ self.robot_suite = self.current_suite - logger.info(f"Analysing Robot test suite '{self.robot_suite.name}' for model-based execution.") + logger.info( + f"Analysing Robot test suite '{self.robot_suite.name}' for model-based execution.") local_settings = self.processor_options.copy() local_settings.update(kwargs) - master_suite = self.__process_robot_suite(self.robot_suite, parent=None) - modelbased_suite = self.processor_method(master_suite, **local_settings) + master_suite = self.__process_robot_suite( + self.robot_suite, parent=None) + modelbased_suite = self.processor_method( + master_suite, **local_settings) self.__clearTestSuite(self.robot_suite) self.__generateRobotSuite(modelbased_suite, self.robot_suite) @@ -113,36 +119,47 @@ def __process_robot_suite(self, in_suite, parent): out_suite.filename = in_suite.source if in_suite.setup and parent is not None: - step_info = Step(in_suite.setup.name, *in_suite.setup.args, parent=out_suite) - step_info.add_robot_dependent_data(Robot._namespace.get_runner(step_info.org_step).keyword) + step_info = Step(in_suite.setup.name, * + in_suite.setup.args, parent=out_suite) + step_info.add_robot_dependent_data( + Robot._namespace.get_runner(step_info.org_step).keyword) out_suite.setup = step_info if in_suite.teardown and parent is not None: - step_info =Step(in_suite.teardown.name, *in_suite.teardown.args, parent=out_suite) - step_info.add_robot_dependent_data(Robot._namespace.get_runner(step_info.org_step).keyword) + step_info = Step(in_suite.teardown.name, * + in_suite.teardown.args, parent=out_suite) + step_info.add_robot_dependent_data( + Robot._namespace.get_runner(step_info.org_step).keyword) out_suite.teardown = step_info for st in in_suite.suites: - out_suite.suites.append(self.__process_robot_suite(st, parent=out_suite)) + out_suite.suites.append( + self.__process_robot_suite(st, parent=out_suite)) for tc in in_suite.tests: scenario = Scenario(tc.name, parent=out_suite) if tc.setup: - step_info = Step(tc.setup.name, *tc.setup.args, parent=scenario) - step_info.add_robot_dependent_data(Robot._namespace.get_runner(step_info.org_step).keyword) + step_info = Step( + tc.setup.name, *tc.setup.args, parent=scenario) + step_info.add_robot_dependent_data( + Robot._namespace.get_runner(step_info.org_step).keyword) scenario.setup = step_info if tc.teardown: - step_info = Step(tc.teardown.name, *tc.teardown.args, parent=scenario) - step_info.add_robot_dependent_data(Robot._namespace.get_runner(step_info.org_step).keyword) + step_info = Step(tc.teardown.name, * + tc.teardown.args, parent=scenario) + step_info.add_robot_dependent_data( + Robot._namespace.get_runner(step_info.org_step).keyword) scenario.teardown = step_info last_gwt = None for step_def in tc.body: if isinstance(step_def, rmodel.Keyword): step_info = Step(step_def.name, *step_def.args, parent=scenario, assign=step_def.assign, prev_gherkin_kw=last_gwt) - step_info.add_robot_dependent_data(Robot._namespace.get_runner(step_info.org_step).keyword) + step_info.add_robot_dependent_data( + Robot._namespace.get_runner(step_info.org_step).keyword) scenario.steps.append(step_info) if step_info.gherkin_kw: last_gwt = step_info.gherkin_kw elif isinstance(step_def, rmodel.Var): - scenario.steps.append(Step('VAR', step_def.name, *step_def.value, parent=scenario)) + scenario.steps.append( + Step('VAR', step_def.name, *step_def.value, parent=scenario)) else: unsupported_step = Step(str(step_def), parent=scenario) unsupported_step.model_info['error'] = f"Robot construct not supported" @@ -170,18 +187,20 @@ def __generateRobotSuite(self, suite_model, target_suite): for tc in suite_model.scenarios: new_tc = target_suite.tests.create(name=tc.name) if tc.setup: - new_tc.setup= rmodel.Keyword(name=tc.setup.keyword, - args=tc.setup.posnom_args_str, - type='setup') + new_tc.setup = rmodel.Keyword(name=tc.setup.keyword, + args=tc.setup.posnom_args_str, + type='setup') if tc.teardown: - new_tc.teardown= rmodel.Keyword(name=tc.teardown.keyword, - args=tc.teardown.posnom_args_str, - type='teardown') + new_tc.teardown = rmodel.Keyword(name=tc.teardown.keyword, + args=tc.teardown.posnom_args_str, + type='teardown') for step in tc.steps: if step.keyword == 'VAR': - new_tc.body.create_var(step.posnom_args_str[0], step.posnom_args_str[1:]) + new_tc.body.create_var( + step.posnom_args_str[0], step.posnom_args_str[1:]) else: - new_tc.body.create_keyword(name=step.keyword, assign=step.assign, args=step.posnom_args_str) + new_tc.body.create_keyword( + name=step.keyword, assign=step.assign, args=step.posnom_args_str) def _start_suite(self, suite, result): self.current_suite = suite diff --git a/robotmbt/tracestate.py b/robotmbt/tracestate.py index 4d0e526c..8dbb04ae 100644 --- a/robotmbt/tracestate.py +++ b/robotmbt/tracestate.py @@ -32,10 +32,13 @@ class TraceState: def __init__(self, n_scenarios): - self._c_pool = [False] * n_scenarios # coverage pool: True means scenario is in trace - self._tried = [[]] # Keeps track of the scenarios already tried at each step in the trace - self._trace = [] # Choice trace, when was which scenario inserted (e.g. ['1', '2.1', '3', '2.0']) - self._snapshots = [] # Keeps details for elements in trace + # coverage pool: True means scenario is in trace + self._c_pool = [False] * n_scenarios + # Keeps track of the scenarios already tried at each step in the trace + self._tried = [[]] + # Choice trace, when was which scenario inserted (e.g. ['1', '2.1', '3', '2.0']) + self._trace = [] + self._snapshots = [] # Keeps details for elements in trace self._open_refinements = [] @property @@ -124,7 +127,8 @@ def push_partial_scenario(self, index, scenario, model): self._tried.append([]) self._open_refinements.append(index) self._trace.append(id) - self._snapshots.append(TraceSnapShot(id, scenario, model, self.coverage_drought)) + self._snapshots.append(TraceSnapShot( + id, scenario, model, self.coverage_drought)) def can_rewind(self): return len(self._trace) > 0 @@ -157,6 +161,7 @@ def __getitem__(self, key): def __len__(self): return len(self._snapshots) + class TraceSnapShot: def __init__(self, id, inserted_scenario, model_state, drought=0): self.id = id diff --git a/run_tests.py b/run_tests.py index 5387e5c8..7a3ad645 100644 --- a/run_tests.py +++ b/run_tests.py @@ -27,7 +27,8 @@ if utest: utestrun = unittest.main(module=None, - argv=[__file__, 'discover', os.path.join(THIS_DIR, 'utest')], + argv=[__file__, 'discover', + os.path.join(THIS_DIR, 'utest')], exit=False) if not utestrun.result.wasSuccessful(): sys.exit(1) diff --git a/utest/test_modelspace.py b/utest/test_modelspace.py index 7e8e537e..a992a4c6 100644 --- a/utest/test_modelspace.py +++ b/utest/test_modelspace.py @@ -163,16 +163,19 @@ def test_nested_attributes(self): self.m.process_expression('foo1.add_prop(bar1)') self.m.process_expression('foo1.bar1.foo2 = barbar') self.m.process_expression('foo1.bar1.foo3 = barbar') - self.assertIs(self.m.process_expression('foo1.bar1.foo2 == foo1.bar1.foo3'), True) + self.assertIs(self.m.process_expression( + 'foo1.bar1.foo2 == foo1.bar1.foo3'), True) def test_fail_on_naming_conflict_property_exists(self): self.m.process_expression('new foo1') - self.assertRaises(ModellingError, self.m.process_expression, 'new foo1') + self.assertRaises( + ModellingError, self.m.process_expression, 'new foo1') def test_fail_on_naming_conflict_literal_exists(self): self.m.process_expression('new foo1') self.m.process_expression('foo1.bar = foo2') - self.assertRaises(ModellingError, self.m.process_expression, 'new foo2') + self.assertRaises( + ModellingError, self.m.process_expression, 'new foo2') def test_list_comprehension_name_error(self): """ @@ -185,7 +188,8 @@ def test_list_comprehension_name_error(self): """ self.m.process_expression('new foo') self.m.process_expression("foo.bar = ['A', 'B', 'C']") - self.assertIs(self.m.process_expression("any(elm == A for elm in foo.bar)"), True) + self.assertIs(self.m.process_expression( + "any(elm == A for elm in foo.bar)"), True) def test_fail_exists_check_before_using_new(self): self.assertRaises(NameError, self.m.process_expression, 'foo') @@ -222,7 +226,8 @@ def test_fail_when_assigning_to_undefined_name(self): if sys.version_info >= (3, 13): self.assertEqual(str(cm.exception), "foo used before definition") else: - self.assertEqual(str(cm.exception), "None used before assignment") # <-- Known issue in Python 3.10/11/12 + # <-- Known issue in Python 3.10/11/12 + self.assertEqual(str(cm.exception), "None used before assignment") def test_fail_when_comparing_unknown_property(self): self.m.add_prop('foo') @@ -275,10 +280,12 @@ def setUp(self): self.m = ModelSpace() def test_scenario_scope_var_cannot_be_user_defined(self): - self.assertRaises(ModellingError, self.m.process_expression, 'new scenario') + self.assertRaises( + ModellingError, self.m.process_expression, 'new scenario') def test_scenario_scope_var_cannot_be_user_removed(self): - self.assertRaises(ModellingError, self.m.process_expression, 'del scenario') + self.assertRaises( + ModellingError, self.m.process_expression, 'del scenario') def test_initial_scenario_scope_cannot_be_ended(self): self.assertRaises(AssertionError, self.m.end_scenario_scope) @@ -288,11 +295,13 @@ def test_initial_scenario_scope_cannot_be_ended(self): def test_scenario_scope_is_unavailable_outside_scenarios(self): self.assertRaises(NameError, self.m.process_expression, 'scenario') - self.assertRaises(ModellingError, self.m.process_expression, 'scenario.foo = bar') + self.assertRaises( + ModellingError, self.m.process_expression, 'scenario.foo = bar') self.m.new_scenario_scope() self.m.end_scenario_scope() self.assertRaises(NameError, self.m.process_expression, 'scenario') - self.assertRaises(ModellingError, self.m.process_expression, 'scenario.foo = bar') + self.assertRaises( + ModellingError, self.m.process_expression, 'scenario.foo = bar') def test_scenario_scope_is_available_inside_scenarios(self): self.m.new_scenario_scope() @@ -302,7 +311,8 @@ def test_scenario_scope_is_available_inside_scenarios(self): def test_scenario_used_as_literal(self): self.m.process_expression('new foo') - self.assertRaises(ModellingError, self.m.process_expression, 'foo.bar = scenario') + self.assertRaises( + ModellingError, self.m.process_expression, 'foo.bar = scenario') self.m.process_expression('foo.bar = "scenario"') self.assertEqual(self.m.process_expression('foo.bar'), "scenario") @@ -311,17 +321,20 @@ def test_scenario_used_as_attribute_name(self): self.m.process_expression('foo.scenario = bar') self.m.new_scenario_scope() self.m.process_expression('scenario.foo = bar') - self.assertEqual(self.m.process_expression('scenario.foo'), self.m.process_expression('foo.scenario')) + self.assertEqual(self.m.process_expression( + 'scenario.foo'), self.m.process_expression('foo.scenario')) def test_scenario_var_is_unavailable_outside_scenario(self): self.m.new_scenario_scope() self.m.process_expression('scenario.foo = bar') self.m.end_scenario_scope() - self.assertRaises(ModellingError, self.m.process_expression, 'scenario.bar == bar') + self.assertRaises( + ModellingError, self.m.process_expression, 'scenario.bar == bar') with self.assertRaises(ModellingError) as cm: self.m.process_expression('scenario.bar == bar') self.assertIsInstance(cm.exception, ModellingError) - self.assertTrue(str(cm.exception).startswith("Accessing scenario scope while there is no scenario active")) + self.assertTrue(str(cm.exception).startswith( + "Accessing scenario scope while there is no scenario active")) def test_scenario_var_is_unavailable_in_next_scenario(self): self.m.new_scenario_scope() @@ -380,8 +393,8 @@ def test_scenario_vars_appear_last_in_status_text(self): self.m.process_expression('scenario.foo = bar') self.m.process_expression('new zz') self.m.process_expression('zz.Z = 26') - self.assertTrue(self.m.get_status_text().endswith( "scenario:\n" - " foo=bar\n")) + self.assertTrue(self.m.get_status_text().endswith("scenario:\n" + " foo=bar\n")) def test_exclude_scenario_vars_from_status_text_when_empty(self): self.m.new_scenario_scope() diff --git a/utest/test_steparguments.py b/utest/test_steparguments.py index da1bdce5..c260d9c6 100644 --- a/utest/test_steparguments.py +++ b/utest/test_steparguments.py @@ -164,7 +164,7 @@ def test_other_values_become_unique_identifiers(self): ' ', '\t', '\n', ' ', ' \n', '\a', # whitespace/non-printable '#', '+-', '-+', '"', "'", 'パイ', # special characters max, 'elif', 'import', 'new', 'del', # reserved words - lambda x: x/2, self, unittest.TestCase] # functions and objects + lambda x: x/2, self, unittest.TestCase] # functions and objects argsset = set() for v in valuelist: arg = StepArgument('foo', v) @@ -206,8 +206,10 @@ def test_number_bool_none_identifiers(self): def test_keep_identifier_names_close_to_original(self): self.assertEqual(StepArgument.make_identifier('foo bar'), 'foo_bar') - self.assertTrue(StepArgument.make_identifier('import').startswith('import')) - self.assertTrue(StepArgument.make_identifier('4foo2bar').endswith('foo2bar')) + self.assertTrue(StepArgument.make_identifier( + 'import').startswith('import')) + self.assertTrue(StepArgument.make_identifier( + '4foo2bar').endswith('foo2bar')) class TestStepArguments(unittest.TestCase): @@ -235,10 +237,10 @@ def test_arguments_can_be_replaced_in_any_string(self): argset = StepArguments([StepArgument('foo1', 'bar1'), StepArgument('foo2', 'bar2')]) self.assertEqual(argset.fill_in_args("\t${foo1} and ${foo2}@#$%s $$$$${foo2}${foo1}}"), - "\tbar1 and bar2@#$%s $$$$bar2bar1}") + "\tbar1 and bar2@#$%s $$$$bar2bar1}") def test_can_use_robot_arguments_in_code_fragments(self): - args = StepArguments([StepArgument('foo1', '3bar'), # 3bar needs to be converted to a valid identifier + args = StepArguments([StepArgument('foo1', '3bar'), # 3bar needs to be converted to a valid identifier StepArgument('foo2', '3bar')]) assignment = "${foo1} = 'magic'" lc = locals() @@ -290,5 +292,6 @@ def test_set_is_modified_if_any_arg_is_modified(self): argset['${foo2}'].value = 'bar3' self.assertTrue(argset.modified) + if __name__ == '__main__': unittest.main() diff --git a/utest/test_substitutionmap.py b/utest/test_substitutionmap.py index 02e05137..8c93171d 100644 --- a/utest/test_substitutionmap.py +++ b/utest/test_substitutionmap.py @@ -44,8 +44,8 @@ def test_single_distinct_options_are_the_solution(self): sm = SubstitutionMap() sm.substitute('A', [1]) sm.substitute('B', [2]) - self.assertEqual(sm.solve(), {'A':1, 'B':2}) - self.assertEqual(sm.solution, {'A':1, 'B':2}) + self.assertEqual(sm.solve(), {'A': 1, 'B': 2}) + self.assertEqual(sm.solution, {'A': 1, 'B': 2}) def test_single_overlapping_options_have_no_solution(self): sm = SubstitutionMap() @@ -124,14 +124,14 @@ def test_wrong_choice_blocks_solution_repeated(self): with varying data helps to detect randomisation flukes and algorithmic blind spots, like order preference. """ - variations = [{'A':[1, 2], 'B':[2, 3], 'C':[3, 2]}, - {'A':[2, 1], 'B':[2, 3], 'C':[2, 3]}, - {'A':[1, 2], 'B':[3, 2], 'C':[2, 3]}, - {'A':[2, 1], 'B':[3, 2], 'C':[3, 2]}, - {'A':[2, 3], 'B':[1, 2], 'C':[2, 3]}, - {'A':[2, 3], 'B':[2, 3], 'C':[1, 2]}, - {'A':[3, 2], 'B':[2, 1], 'C':[2, 3]}, - {'A':[3, 2], 'B':[3, 2], 'C':[2, 1]}] + variations = [{'A': [1, 2], 'B': [2, 3], 'C': [3, 2]}, + {'A': [2, 1], 'B': [2, 3], 'C': [2, 3]}, + {'A': [1, 2], 'B': [3, 2], 'C': [2, 3]}, + {'A': [2, 1], 'B': [3, 2], 'C': [3, 2]}, + {'A': [2, 3], 'B': [1, 2], 'C': [2, 3]}, + {'A': [2, 3], 'B': [2, 3], 'C': [1, 2]}, + {'A': [3, 2], 'B': [2, 1], 'C': [2, 3]}, + {'A': [3, 2], 'B': [3, 2], 'C': [2, 1]}] for variant in variations: sm = SubstitutionMap() for example_value, constraint in variant.items(): @@ -355,8 +355,8 @@ def test_adding_constraint_does_not_affect_undo_remove_stack(self): c.remove_option('four') c.add_constraint(['one', 'two']) self.assertCountEqual(c.optionset, ['two']) - c.undo_remove() # four was never in there, so isn't added, and three - # was removed by adding a constraint and is ignored. + c.undo_remove() # four was never in there, so isn't added, and three + # was removed by adding a constraint and is ignored. self.assertCountEqual(c.optionset, ['two']) c.undo_remove() self.assertCountEqual(c.optionset, ['one', 'two']) diff --git a/utest/test_suitedata.py b/utest/test_suitedata.py index a8bc1025..72922d06 100644 --- a/utest/test_suitedata.py +++ b/utest/test_suitedata.py @@ -78,7 +78,7 @@ def test_error_in_suite_setup_is_detected(self): def test_error_in_scenario_is_detected(self): self.topsuite.scenarios[0].steps[1].model_info = dict(error='oops') self.assertIs(self.topsuite.has_error(), True) - errorsteps = self.topsuite.steps_with_errors() + errorsteps = self.topsuite.steps_with_errors() self.assertEqual(len(errorsteps), 1) self.assertEqual(errorsteps[0].model_info['error'], 'oops') @@ -103,9 +103,10 @@ def test_error_in_subsuite_setup_is_detected(self): self.assertEqual(errorsteps[0].model_info['error'], 'oops') def test_error_in_subsuite_scenario_is_detected(self): - self.topsuite.suites[0].scenarios[0].steps[1].model_info = dict(error='oops') + self.topsuite.suites[0].scenarios[0].steps[1].model_info = dict( + error='oops') self.assertIs(self.topsuite.has_error(), True) - errorsteps = self.topsuite.steps_with_errors() + errorsteps = self.topsuite.steps_with_errors() self.assertEqual(len(errorsteps), 1) self.assertEqual(errorsteps[0].model_info['error'], 'oops') @@ -156,7 +157,7 @@ def test_multiple_errors_are_listed(self): errorsteps = self.topsuite.steps_with_errors() self.assertEqual(len(errorsteps), 4) self.assertEqual(set([e.model_info['error'] for e in errorsteps]), - {'setup oops','scenario oops', 'sub scenario oops', 'sub teardown oops'}) + {'setup oops', 'scenario oops', 'sub scenario oops', 'sub teardown oops'}) class TestScenarios(unittest.TestCase): @@ -173,7 +174,7 @@ def test_longname_without_parent_is_just_the_name(self): self.assertEqual(self.scenario.longname, self.scenario.name) def test_longname_with_parent_includes_both_names(self): - p = lambda:None # Create an object to assign the name attribute to + def p(): return None # Create an object to assign the name attribute to p.longname = 'long' scenario = Scenario('name', p) self.assertEqual(scenario.longname, 'long.name') @@ -185,14 +186,15 @@ def test_no_errors_when_ok(self): def test_step_errors_are_reported(self): self.scenario.steps[0].model_info = dict(error='oops') self.assertIs(self.scenario.has_error(), True) - errorsteps = self.scenario.steps_with_errors() + errorsteps = self.scenario.steps_with_errors() self.assertEqual(len(errorsteps), 1) self.assertEqual(errorsteps[0].model_info['error'], 'oops') self.scenario.steps[1].model_info = dict(error='oh ow') self.assertIs(self.scenario.has_error(), True) - errorsteps = self.scenario.steps_with_errors() + errorsteps = self.scenario.steps_with_errors() self.assertEqual(len(errorsteps), 2) - self.assertEqual([s.model_info['error'] for s in errorsteps], ['oops', 'oh ow']) + self.assertEqual([s.model_info['error'] + for s in errorsteps], ['oops', 'oh ow']) def test_by_default_setup_and_teardown_are_empty(self): self.assertIs(self.scenario.setup, None) @@ -203,14 +205,16 @@ def test_setup_errors(self): step.model_info = dict(error='oops') self.scenario.setup = step self.assertIs(self.scenario.has_error(), True) - self.assertEqual(self.scenario.steps_with_errors(), [self.scenario.setup]) + self.assertEqual(self.scenario.steps_with_errors(), + [self.scenario.setup]) def test_teardown_errors(self): step = Step('my teardown', parent=self.scenario) step.model_info = dict(error='oops') self.scenario.teardown = step self.assertIs(self.scenario.has_error(), True) - self.assertEqual(self.scenario.steps_with_errors(), [self.scenario.teardown]) + self.assertEqual(self.scenario.steps_with_errors(), + [self.scenario.teardown]) def test_combined_errors(self): setup_step = Step('my setup', parent=self.scenario) @@ -222,7 +226,7 @@ def test_combined_errors(self): self.scenario.steps[0].model_info = dict(error='oops in scenario 1') self.scenario.steps[7].model_info = dict(error='oops in scenario 2') self.assertIs(self.scenario.has_error(), True) - errorsteps = self.scenario.steps_with_errors() + errorsteps = self.scenario.steps_with_errors() self.assertEqual(len(errorsteps), 4) self.assertEqual([e.model_info['error'] for e in self.scenario.steps_with_errors()], @@ -280,10 +284,12 @@ def test_copies_are_independent(self): self.assertNotEqual(dup.name, self.scenario.name) self.assertIsNot(dup.steps[0], self.scenario.steps[0]) self.assertEqual(dup.steps[0].keyword, self.scenario.steps[0].keyword) - self.assertNotEqual(dup.steps[-1].keyword, self.scenario.steps[-1].keyword) + self.assertNotEqual(dup.steps[-1].keyword, + self.scenario.steps[-1].keyword) def test_exteranally_determined_attributes_are_copied_along(self): self.scenario.src_id = 7 + class Dummy: def copy(self): return 'dummy' @@ -304,15 +310,15 @@ def create_steps(parent=None): Gg1 = Step('Given step Gg1', parent=parent) Ga1 = Step('and step Ga1', parent=parent) Gb1 = Step('but step Gb1', parent=parent) - Gg1.gherkin_kw= Ga1.gherkin_kw= Gb1.gherkin_kw= 'given' + Gg1.gherkin_kw = Ga1.gherkin_kw = Gb1.gherkin_kw = 'given' Ww1 = Step('When step Ww1', parent=parent) Wa1 = Step('and step Wa1', parent=parent) Wb1 = Step('BUT step Wb1', parent=parent) - Ww1.gherkin_kw= Wa1.gherkin_kw= Wb1.gherkin_kw= 'when' + Ww1.gherkin_kw = Wa1.gherkin_kw = Wb1.gherkin_kw = 'when' Tt1 = Step('Then step Tt1', parent=parent) Ta1 = Step('And step Ta1', parent=parent) Tb1 = Step('but step Tb1', parent=parent) - Tt1.gherkin_kw= Ta1.gherkin_kw= Tb1.gherkin_kw= 'then' + Tt1.gherkin_kw = Ta1.gherkin_kw = Tb1.gherkin_kw = 'then' return [Kw1, Gg1, Ga1, Gb1, Ww1, Wa1, Wb1, Tt1, Ta1, Tb1] def test_full_names(self, mock): @@ -337,8 +343,8 @@ def test_gherkin_keywords(self, mock): def test_gherkin_keywords_are_lower_case(self, mock): source = [None, 'given', 'Given', 'GIVEN', - 'wHEN' , 'wHEn', 'WHEn', - 'TheN' , 'theN', 'thEN'] + 'wHEN', 'wHEn', 'WHEn', + 'TheN', 'theN', 'thEN'] expected = [None] + 3*['given'] + 3*['when'] + 3*['then'] for s, gkw in zip(self.steps, source): s.gherkin_kw = gkw @@ -347,8 +353,8 @@ def test_gherkin_keywords_are_lower_case(self, mock): def test_step_keywords_are_kept_as_is(self, mock): expected = [None, 'Given', 'and', 'but', - 'When' , 'and', 'BUT', - 'Then' , 'And', 'but'] + 'When', 'and', 'BUT', + 'Then', 'And', 'but'] for s, e in zip(self.steps, expected): self.assertEqual(s.step_kw, e) @@ -372,59 +378,78 @@ def test_modified_embedded_arguments_are_part_of_the_step_str(self, mock): step.add_robot_dependent_data(RobotKwStub()) self.assertNotIn('error', step.model_info) step.args['${bar}'].value = 'new bar' - self.assertEqual(str(step), RobotKwStub.STEPTEXT.replace('bar_value', 'new bar')) + self.assertEqual(str(step), RobotKwStub.STEPTEXT.replace( + 'bar_value', 'new bar')) def test_all_arguments_are_part_of_the_full_keyword_text(self, mock): - step = Step(RobotKwStub.STEPTEXT, 'posA', 'pos2=posB', 'named1=namedA', parent=None) - self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB named1=namedA") + step = Step(RobotKwStub.STEPTEXT, 'posA', + 'pos2=posB', 'named1=namedA', parent=None) + self.assertEqual( + step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB named1=namedA") def test_return_value_assignment_is_part_of_the_full_keyword_text(self, mock): step = Step(RobotKwStub.STEPTEXT, assign=('${output}',), parent=None) - self.assertEqual(step.full_keyword, "${output} " + RobotKwStub.STEPTEXT) + self.assertEqual(step.full_keyword, + "${output} " + RobotKwStub.STEPTEXT) def test_return_value_assignment_with_eq_is_part_of_the_full_keyword_text(self, mock): step = Step(RobotKwStub.STEPTEXT, assign=('${output}=',), parent=None) - self.assertEqual(step.full_keyword, "${output}= " + RobotKwStub.STEPTEXT) + self.assertEqual(step.full_keyword, + "${output}= " + RobotKwStub.STEPTEXT) def test_return_value_assignment_with_eq_after_space_is_part_of_the_full_keyword_text(self, mock): step = Step(RobotKwStub.STEPTEXT, assign=('${output} =',), parent=None) - self.assertEqual(step.full_keyword, "${output} = " + RobotKwStub.STEPTEXT) + self.assertEqual(step.full_keyword, + "${output} = " + RobotKwStub.STEPTEXT) def test_return_value_multi_assignment_is_part_of_the_full_keyword_text(self, mock): - step = Step(RobotKwStub.STEPTEXT, assign=('${output1}', '${output2}='), parent=None) - self.assertEqual(step.full_keyword, "${output1} ${output2}= " + RobotKwStub.STEPTEXT) + step = Step(RobotKwStub.STEPTEXT, assign=( + '${output1}', '${output2}='), parent=None) + self.assertEqual( + step.full_keyword, "${output1} ${output2}= " + RobotKwStub.STEPTEXT) def test_argument_with_default_is_omitted_from_keyword_when_not_mentioned_named(self, mock): step = Step(RobotKwStub.STEPTEXT, 'posA', 'pos2=posB', parent=None) step.args = StubStepArguments([StubArgument(name='pos1', value='posA', is_default=False, kind='POSITIONAL'), - StubArgument(name='pos2', value='posB', is_default=False, kind='NAMED'), + StubArgument( + name='pos2', value='posB', is_default=False, kind='NAMED'), StubArgument(name='named1', value='namedA', is_default=True, kind='NAMED')]) self.assertTupleEqual(step.posnom_args_str, ('posA', 'pos2=posB')) - self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB") + self.assertEqual(step.full_keyword, + f"{RobotKwStub.STEPTEXT} posA pos2=posB") def test_argument_with_default_is_omitted_from_keyword_when_not_mentioned_positional(self, mock): step = Step(RobotKwStub.STEPTEXT, 'posA', 'posB', parent=None) step.args = StubStepArguments([StubArgument(name='pos1', value='posA', is_default=False, kind='POSITIONAL'), - StubArgument(name='pos2', value='posB', is_default=False, kind='POSITIONAL'), + StubArgument( + name='pos2', value='posB', is_default=False, kind='POSITIONAL'), StubArgument(name='named1', value='namedA', is_default=True, kind='POSITIONAL')]) self.assertTupleEqual(step.posnom_args_str, ('posA', 'posB')) - self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA posB") + self.assertEqual(step.full_keyword, + f"{RobotKwStub.STEPTEXT} posA posB") def test_argument_with_default_is_included_in_keyword_when_mentioned_named(self, mock): - step = Step(RobotKwStub.STEPTEXT, 'posA', 'pos2=posB', 'named1=namedA', parent=None) + step = Step(RobotKwStub.STEPTEXT, 'posA', + 'pos2=posB', 'named1=namedA', parent=None) step.args = StubStepArguments([StubArgument(name='pos1', value='posA', is_default=False, kind='POSITIONAL'), - StubArgument(name='pos2', value='posB', is_default=False, kind='NAMED'), + StubArgument( + name='pos2', value='posB', is_default=False, kind='NAMED'), StubArgument(name='named1', value='namedA', is_default=False, kind='NAMED')]) - self.assertTupleEqual(step.posnom_args_str, ('posA', 'pos2=posB', 'named1=namedA')) - self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB named1=namedA") + self.assertTupleEqual(step.posnom_args_str, + ('posA', 'pos2=posB', 'named1=namedA')) + self.assertEqual( + step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB named1=namedA") def test_argument_with_default_is_included_in_keyword_when_mentioned_positional(self, mock): - step = Step(RobotKwStub.STEPTEXT, 'posA', 'posB', 'namedA', parent=None) + step = Step(RobotKwStub.STEPTEXT, 'posA', + 'posB', 'namedA', parent=None) step.args = StubStepArguments([StubArgument(name='pos1', value='posA', is_default=False, kind='POSITIONAL'), - StubArgument(name='pos2', value='posB', is_default=False, kind='POSITIONAL'), + StubArgument( + name='pos2', value='posB', is_default=False, kind='POSITIONAL'), StubArgument(name='named1', value='namedA', is_default=False, kind='POSITIONAL')]) self.assertTupleEqual(step.posnom_args_str, ('posA', 'posB', 'namedA')) - self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA posB namedA") + self.assertEqual(step.full_keyword, + f"{RobotKwStub.STEPTEXT} posA posB namedA") def test_positional_and_named_arguments_are_available_in_robot_tuple_format(self, mock): argtuple = 'posA', 'pos2=posB', 'named1=namedA' @@ -446,7 +471,7 @@ def test_model_info_is_loaded(self, mock): :OUT: expr3 | expr4 """ step.add_robot_dependent_data(kw) - self.assertEqual(step.model_info, dict( IN=['expr1', 'expr2'], + self.assertEqual(step.model_info, dict(IN=['expr1', 'expr2'], OUT=['expr3', 'expr4'])) def test_model_info_errors_are_reported(self, mock): @@ -461,22 +486,23 @@ def test_model_info_errors_are_reported(self, mock): class RobotKwStub: STEPTEXT = "Given step with foo_value and bar_value as arguments" + def __init__(self): self.name = "step with ${foo} and ${bar} as arguments" self._doc = "*model info*\n:IN: None\n:OUT: None" self.args = self.argstub() self.error = False self.embedded = SimpleNamespace(args=['${foo}', '${bar}'], - parse_args= lambda _: ['foo_value', 'bar_value']) + parse_args=lambda _: ['foo_value', 'bar_value']) class argstub: argument_names = [] - map = lambda x,y,z: ([], []) - __iter__ = lambda _: iter([]) + def map(x, y, z): return ([], []) + def __iter__(_): return iter([]) class StubStepArguments(list): - modified = True # trigger modified status to get arguments processed, rather then just echoed + modified = True # trigger modified status to get arguments processed, rather then just echoed class StubArgument(SimpleNamespace): diff --git a/utest/test_suiteprocessors.py b/utest/test_suiteprocessors.py index 2b4f3e24..cfa7888f 100644 --- a/utest/test_suiteprocessors.py +++ b/utest/test_suiteprocessors.py @@ -79,8 +79,10 @@ def test_generated_seeds_have_max_2_consecutive_vowels_or_consonants(self, mock) SuiteProcessors._init_randomiser("new") new_seed = mock.call_args.args[0] self._is_generated_seed(new_seed) - self.assertNotIn('***', new_seed.translate({ord(c):'*' for c in 'aeiouy'})) - self.assertNotIn('***', new_seed.translate({ord(c):'*' for c in 'bcdfghjklmnpqrstvwxz'})) + self.assertNotIn( + '***', new_seed.translate({ord(c): '*' for c in 'aeiouy'})) + self.assertNotIn( + '***', new_seed.translate({ord(c): '*' for c in 'bcdfghjklmnpqrstvwxz'})) def _is_generated_seed(self, arg): """ diff --git a/utest/test_tracestate.py b/utest/test_tracestate.py index 3352b743..d91410d8 100644 --- a/utest/test_tracestate.py +++ b/utest/test_tracestate.py @@ -72,7 +72,7 @@ def test_scenario_still_excluded_from_candidacy_after_rewind(self): def test_candidates_come_in_order_when_accepted(self): ts = TraceState(3) candidates = [] - for scenario in range(3): + for scenario in range(3): candidates.append(ts.next_candidate()) ts.confirm_full_scenario(candidates[-1], scenario, {}) candidates.append(ts.next_candidate()) @@ -81,7 +81,7 @@ def test_candidates_come_in_order_when_accepted(self): def test_candidates_come_in_order_when_rejected(self): ts = TraceState(3) candidates = [] - for _ in range(3): + for _ in range(3): candidates.append(ts.next_candidate()) ts.reject_scenario(candidates[-1]) candidates.append(ts.next_candidate()) @@ -91,7 +91,7 @@ def test_rejected_scenarios_are_candidates_for_new_positions(self): ts = TraceState(3) candidates = [] ts.reject_scenario(0) - for scenario in range(3): + for scenario in range(3): candidates.append(ts.next_candidate()) ts.confirm_full_scenario(candidates[-1], scenario, {}) candidates.append(ts.next_candidate()) @@ -208,7 +208,8 @@ def test_completing_size_three_trace_after_rewind(self): retry_third = ts.next_candidate() ts.confirm_full_scenario(retry_third, retry_third, {}) self.assertIs(ts.coverage_reached(), True) - self.assertEqual(ts.get_trace(), [retry_first, retry_second, retry_third]) + self.assertEqual(ts.get_trace(), [ + retry_first, retry_second, retry_third]) def test_highest_part_when_index_not_present(self): ts = TraceState(1) diff --git a/utest/test_tracestate_refinement.py b/utest/test_tracestate_refinement.py index a64b86d7..497a81a8 100644 --- a/utest/test_tracestate_refinement.py +++ b/utest/test_tracestate_refinement.py @@ -160,7 +160,8 @@ def test_nested_refinement(self): self.assertIs(ts.coverage_reached(), False) ts.confirm_full_scenario(top_level, 'T1.0', {}) self.assertIs(ts.coverage_reached(), True) - self.assertEqual(ts.get_trace(), ['T1.1', 'M1.1', 'B1', 'M1.0', 'T1.0']) + self.assertEqual(ts.get_trace(), [ + 'T1.1', 'M1.1', 'B1', 'M1.0', 'T1.0']) def test_rewind_to_swap_nested_refinement(self): ts = TraceState(3) @@ -180,7 +181,8 @@ def test_rewind_to_swap_nested_refinement(self): self.assertIs(ts.coverage_reached(), False) ts.confirm_full_scenario(top_level, 'T1.0', {}) self.assertIs(ts.coverage_reached(), True) - self.assertEqual(ts.get_trace(), ['T1.1', 'M1.1', 'B1', 'M1.0', 'T1.0']) + self.assertEqual(ts.get_trace(), [ + 'T1.1', 'M1.1', 'B1', 'M1.0', 'T1.0']) def test_rewind_nested_refinement_as_one(self): ts = TraceState(4) @@ -297,16 +299,19 @@ def test_nested_refinements_are_all_reported_as_in_refinement(self): ts.push_partial_scenario(0, 'T1.1', {}) ts.push_partial_scenario(1, 'M1.1', {}) ts.push_partial_scenario(2, 'B1.1', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1', 'B1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), [ + 'T1.1', 'M1.1', 'B1.1']) def test_closing_refinement_removes_it_from_list(self): ts = TraceState(4) ts.push_partial_scenario(0, 'T1.1', {}) ts.push_partial_scenario(1, 'M1.1', {}) ts.push_partial_scenario(2, 'B1.1', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1', 'B1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), [ + 'T1.1', 'M1.1', 'B1.1']) ts.confirm_full_scenario(2, 'B1.0', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) + self.assertEqual( + ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) def test_multi_step_refinement_is_reported_only_once(self): ts = TraceState(4) @@ -315,16 +320,19 @@ def test_multi_step_refinement_is_reported_only_once(self): ts.confirm_full_scenario(2, 'B1', {}) ts.push_partial_scenario(1, 'M1.2', {}) ts.confirm_full_scenario(3, 'B2', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) + self.assertEqual( + ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) def test_rewind_open_refinement_removes_it_from_list(self): ts = TraceState(4) ts.push_partial_scenario(0, 'T1.1', {}) ts.push_partial_scenario(1, 'M1.1', {}) ts.push_partial_scenario(2, 'B1.1', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1', 'B1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), [ + 'T1.1', 'M1.1', 'B1.1']) ts.rewind() - self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) + self.assertEqual( + ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) def test_rewind_finished_scenario_with_refinement_removes_enclosed_refinements(self): ts = TraceState(5) @@ -333,11 +341,12 @@ def test_rewind_finished_scenario_with_refinement_removes_enclosed_refinements(s ts.push_partial_scenario(2, 'M1.1', {}) ts.push_partial_scenario(3, 'B1.1', {}) ts.confirm_full_scenario(4, 'S1', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T2.1', 'M1.1', 'B1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), [ + 'T2.1', 'M1.1', 'B1.1']) ts.confirm_full_scenario(3, 'B1.0', {}) ts.confirm_full_scenario(2, 'M1.0', {}) self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T2.1']) - ts.rewind() # Middle including its Bottom refinement + ts.rewind() # Middle including its Bottom refinement self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T2.1']) From 212345c126249ff197e393c96c8ce53d48e5fa13 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:06:20 +0100 Subject: [PATCH 09/18] reformat with new max line length setting --- .../03__parse_model_info/MyProcessor.py | 6 +- demo/Titanic/domain_lib/JourneyLib.py | 21 ++---- demo/Titanic/domain_lib/MapLib.py | 3 +- demo/Titanic/domain_lib/TitanicLib.py | 15 ++-- demo/Titanic/run_demo.py | 6 +- demo/Titanic/run_game.py | 21 ++---- demo/Titanic/simulation/location_on_grid.py | 2 +- demo/Titanic/simulation/map_animation.py | 19 ++--- demo/Titanic/simulation/ocean.py | 9 +-- demo/Titanic/system/titanic.py | 3 +- robotmbt/suitereplacer.py | 10 ++- utest/test_modelspace.py | 39 ++++------ utest/test_steparguments.py | 14 ++-- utest/test_substitutionmap.py | 3 +- utest/test_suitedata.py | 72 +++++++------------ utest/test_suiteprocessors.py | 6 +- utest/test_tracestate.py | 3 +- utest/test_tracestate_refinement.py | 27 +++---- 18 files changed, 100 insertions(+), 179 deletions(-) diff --git a/atest/robotMBT tests/03__parse_model_info/MyProcessor.py b/atest/robotMBT tests/03__parse_model_info/MyProcessor.py index 1ce666bd..09cb13bb 100644 --- a/atest/robotMBT tests/03__parse_model_info/MyProcessor.py +++ b/atest/robotMBT tests/03__parse_model_info/MyProcessor.py @@ -7,10 +7,8 @@ def process_test_suite(self, in_suite): for scenario in in_suite.scenarios: assert scenario.steps, msg for step in scenario.steps: - assert step.model_info['IN'] == [ - 'Alfa'], f"{msg} in step {step.keyword}" - assert step.model_info['OUT'] == [ - 'Beta', 'Gamma delta', 'Epsilon'], f"{msg} in step {step.keyword}" + assert step.model_info['IN'] == ['Alfa'], f"{msg} in step {step.keyword}" + assert step.model_info['OUT'] == ['Beta', 'Gamma delta', 'Epsilon'], f"{msg} in step {step.keyword}" return in_suite def _fail_on_step_errors(self): diff --git a/demo/Titanic/domain_lib/JourneyLib.py b/demo/Titanic/domain_lib/JourneyLib.py index ba6263dd..9c02c08c 100644 --- a/demo/Titanic/domain_lib/JourneyLib.py +++ b/demo/Titanic/domain_lib/JourneyLib.py @@ -32,13 +32,11 @@ def map_lib(self) -> MapLib: def start_journey(self, date: str): date = datetime.strptime(date, "%Y-%m-%d") self.journey.start_date = date - self.builtin.log( - f"The journey has started at {self.journey.start_date.strftime('%Y-%m-%d')}") + self.builtin.log(f"The journey has started at {self.journey.start_date.strftime('%Y-%m-%d')}") @keyword("Current date of Journey") def journey_ondate(self): - current_date = self.journey.start_date + \ - timedelta(minutes=self.journey.time_in_journey) + current_date = self.journey.start_date + timedelta(minutes=self.journey.time_in_journey) return current_date.strftime('%Y-%m-%d') @keyword("play out Journey for a duration of ${minutes} minutes") @@ -49,26 +47,21 @@ def pass_time(self, minutes: int): """ self.journey.passed_time(minutes) if self.call_count % 100 == 0: - map_animation.update_floating_objects( - self.journey.ocean.floating_objects) + map_animation.update_floating_objects(self.journey.ocean.floating_objects) self.call_count += 1 @keyword("Move Titanic out of current area") def move_titanic_out_of_current_area(self): titanic = TitanicInOcean.instance - current_area = self.builtin.run_keyword( - "Area of location Titanic's position") + current_area = self.builtin.run_keyword("Area of location Titanic's position") self.builtin.log(f"Titanic moving out of {current_area}") while (new_area := self.map_lib.get_area_of_location(titanic)) == current_area: if not titanic.speed > 0: - self.builtin.log( - f"Titanic not moving. Still in area {new_area}") + self.builtin.log(f"Titanic not moving. Still in area {new_area}") break self.pass_time(1) if titanic.fell_off_the_earth(): - raise Exception( - "Titanic at least did not sink. But where did it go?") + raise Exception("Titanic at least did not sink. But where did it go?") else: self.builtin.log(f"Titanic moved into {new_area}") - map_animation.update_floating_objects( - self.journey.ocean.floating_objects) + map_animation.update_floating_objects(self.journey.ocean.floating_objects) diff --git a/demo/Titanic/domain_lib/MapLib.py b/demo/Titanic/domain_lib/MapLib.py index d864036b..70a89898 100644 --- a/demo/Titanic/domain_lib/MapLib.py +++ b/demo/Titanic/domain_lib/MapLib.py @@ -30,8 +30,7 @@ def __init__(self): 'Iceberg alley': AreaOnGrid(LocationOnGrid(latitude=43, longitude=-45), LocationOnGrid(latitude=48, longitude=-50)) } - atlantic_area = AreaOnGrid(LocationOnGrid( - latitude=35, longitude=-1.41), LocationOnGrid(latitude=65, longitude=-74)) + atlantic_area = AreaOnGrid(LocationOnGrid(latitude=35, longitude=-1.41), LocationOnGrid(latitude=65, longitude=-74)) LOCATION_AREA_THRESHOLD = 0.1 ATLANTIC_AREA = 'Atlantic' diff --git a/demo/Titanic/domain_lib/TitanicLib.py b/demo/Titanic/domain_lib/TitanicLib.py index 8b6d7c1b..3b16f829 100644 --- a/demo/Titanic/domain_lib/TitanicLib.py +++ b/demo/Titanic/domain_lib/TitanicLib.py @@ -24,8 +24,7 @@ def point_titanic_towards(self, location): if titanic.sunk: self.builtin.log(f"Pointing towards Davy Jones' locker") return - port_location = self.builtin.run_keyword( - f"Location of port {location}") + port_location = self.builtin.run_keyword(f"Location of port {location}") new_direction = titanic.calculate_direction(port_location) titanic.direction = new_direction @@ -35,8 +34,7 @@ def point_titanic_towards(self, location): def titanic_stops(self): titanic = TitanicInOcean.instance titanic.titanic.throttle = 0 - # TODO should happen over time (due to throttle being > 0) - titanic.speed = 0 + titanic.speed = 0 # TODO should happen over time (due to throttle being > 0) self.builtin.log("Now it is time for Titanic to stop at new location") @keyword("Titanic moves full speed ahead") @@ -46,16 +44,13 @@ def titanic_full_speed(self): self.builtin.log(f"There seems to be an issue with the throttle") return titanic.titanic.throttle = 1 - # TODO Figure out what this speed means. Does time calculation make sense?!?! - titanic.speed = 700 - self.builtin.log( - f"Here we go through the new location with speed {titanic.speed}") + titanic.speed = 700 # TODO Figure out what this speed means. Does time calculation make sense?!?! + self.builtin.log(f"Here we go through the new location with speed {titanic.speed}") @keyword("Titanic's position") def titanic_location(self): titanic = TitanicInOcean.instance - loc = LocationOnGrid(longitude=titanic.longitude, - latitude=titanic.latitude) + loc = LocationOnGrid(longitude=titanic.longitude, latitude=titanic.latitude) self.builtin.log(f"Titanic's current position is: {loc}") return loc diff --git a/demo/Titanic/run_demo.py b/demo/Titanic/run_demo.py index 369e8b55..d6936c06 100644 --- a/demo/Titanic/run_demo.py +++ b/demo/Titanic/run_demo.py @@ -16,10 +16,8 @@ THIS_DIR = os.path.dirname(os.path.abspath(__file__)) OUTPUT_ROOT = os.path.join(THIS_DIR, 'results') SCENARIO_FOLDER = os.path.join(THIS_DIR, 'Titanic_scenarios') - HIT_MISS_TAG = 'hit' if len( - sys.argv) == 1 or sys.argv[1].casefold() != 'hit' else 'miss' - EXTENDED_TAG = 'extended' if len( - sys.argv) == 1 or sys.argv[1].casefold() != 'extended' else 'dummy' + HIT_MISS_TAG = 'hit' if len(sys.argv) == 1 or sys.argv[1].casefold() != 'hit' else 'miss' + EXTENDED_TAG = 'extended' if len(sys.argv) == 1 or sys.argv[1].casefold() != 'extended' else 'dummy' # The base folder needs to be added to the python path to resolve the dependencies. You # will also need to add this path to your IDE options when running from there. diff --git a/demo/Titanic/run_game.py b/demo/Titanic/run_game.py index ec6a4a11..bd46fd2f 100644 --- a/demo/Titanic/run_game.py +++ b/demo/Titanic/run_game.py @@ -22,8 +22,7 @@ 'Iceberg alley': AreaOnGrid(LocationOnGrid(latitude=43, longitude=-45), LocationOnGrid(latitude=48, longitude=-50)) } -atlantic_area = AreaOnGrid(LocationOnGrid( - latitude=35, longitude=-1.41), LocationOnGrid(latitude=65, longitude=-74)) +atlantic_area = AreaOnGrid(LocationOnGrid(latitude=35, longitude=-1.41), LocationOnGrid(latitude=65, longitude=-74)) def run_game(map_animation, journey, tio: TitanicInOcean, atlantic_area): @@ -47,8 +46,7 @@ def main_game_loop(stdscr): while True: journey.passed_time(100) - map_animation.update_floating_objects( - journey.ocean.floating_objects) + map_animation.update_floating_objects(journey.ocean.floating_objects) if not atlantic_area.is_location_within_area(tio): tio.direction -= 180 @@ -59,19 +57,15 @@ def main_game_loop(stdscr): if areas['Iceberg alley'].is_location_within_area(tio): iceberg_alley_reached = True elif iceberg_alley_reached: - stdscr.addstr( - objective, 0, "Objective 1: Safely cross Iceberg Alley [Achieved]") + stdscr.addstr(objective, 0, "Objective 1: Safely cross Iceberg Alley [Achieved]") objective = 2 - stdscr.addstr( - objective, 0, "Objective 2: Sail to New York") + stdscr.addstr(objective, 0, "Objective 2: Sail to New York") elif objective == 2: if tio.distance_to(locations['New York']) < 0.5: tio.speed = 0 - stdscr.addstr( - objective, 0, "Objective 2: Sail to New York [Achieved]") + stdscr.addstr(objective, 0, "Objective 2: Sail to New York [Achieved]") objective = 4 - stdscr.addstr( - objective, 0, "You made it to New York!! Press Q to exit.") + stdscr.addstr(objective, 0, "You made it to New York!! Press Q to exit.") if tio.sunk: stdscr.addstr(objective+2, 0, @@ -134,8 +128,7 @@ def main_game_loop(stdscr): location = locations["Southampton"] t = Titanic(0, steering_direction=0) - tio = TitanicInOcean(t, longitude=location.longitude - 1, - latitude=location.latitude, speed=0, direction=270) + tio = TitanicInOcean(t, longitude=location.longitude - 1, latitude=location.latitude, speed=0, direction=270) ocean.floating_objects.append(tio) iceberg = Iceberg(latitude=45.5, longitude=-47.5) diff --git a/demo/Titanic/simulation/location_on_grid.py b/demo/Titanic/simulation/location_on_grid.py index d44923d3..ee58ae5b 100644 --- a/demo/Titanic/simulation/location_on_grid.py +++ b/demo/Titanic/simulation/location_on_grid.py @@ -11,7 +11,7 @@ def __init__(self, longitude, latitude): def __str__(self): return f"{'N' if self.latitude >= 0 else 'S'}{abs(self.latitude):08.5f} "\ - f"{'E' if self.longitude >= 0 else 'W'}{abs(self.longitude):08.5f}" + f"{'E' if self.longitude >= 0 else 'W'}{abs(self.longitude):08.5f}" def distance_to(self, other_object: 'LocationOnGrid'): """ diff --git a/demo/Titanic/simulation/map_animation.py b/demo/Titanic/simulation/map_animation.py index 008041d3..4470246b 100644 --- a/demo/Titanic/simulation/map_animation.py +++ b/demo/Titanic/simulation/map_animation.py @@ -24,12 +24,10 @@ def plot_static_elements(self, areas, locations): self.ax.set_aspect('equal') for area_name, area in areas.items(): - width = abs(area.upper_left_bound.latitude - - area.lower_right_bound.latitude) - height = abs(area.upper_left_bound.longitude - - area.lower_right_bound.longitude) - rect = Rectangle((area.lower_right_bound.longitude, - area.upper_left_bound.latitude), height, width, alpha=0.4) + width = abs(area.upper_left_bound.latitude - area.lower_right_bound.latitude) + height = abs(area.upper_left_bound.longitude - area.lower_right_bound.longitude) + rect = Rectangle((area.lower_right_bound.longitude, area.upper_left_bound.latitude), + height, width, alpha=0.4) self.ax.add_patch(rect) self.ax.annotate(area_name, (area.lower_right_bound.longitude, area.upper_left_bound.latitude), color='black') @@ -47,8 +45,7 @@ def plot_static_elements(self, areas, locations): ] ci = 0 for location_name, location in locations.items(): - self.ax.plot(location.longitude, location.latitude, - colors[ci % len(colors)], label=location_name) + self.ax.plot(location.longitude, location.latitude, colors[ci % len(colors)], label=location_name) ci += 1 # # Set the Atlantic area bounds atlantic_area = MapLib.atlantic_area width = abs( @@ -112,11 +109,9 @@ def update_floating_objects(self, floating_objects): # Draw the arrow self.ax.annotate("", xy=(obj.longitude + dx, obj.latitude + dy), xytext=(obj.longitude, obj.latitude), arrowprops=dict(arrowstyle='->'), gid='floating_object') - self.ax.plot(obj.longitude, obj.latitude, icon, - label='Titanic', gid='floating_object') + self.ax.plot(obj.longitude, obj.latitude, icon, label='Titanic', gid='floating_object') elif isinstance(obj, Iceberg): - self.ax.plot(obj.longitude, obj.latitude, 'w^', - label='Iceberg', gid='floating_object') + self.ax.plot(obj.longitude, obj.latitude, 'w^', label='Iceberg', gid='floating_object') # Redraw the plot plt.draw() diff --git a/demo/Titanic/simulation/ocean.py b/demo/Titanic/simulation/ocean.py index 0cba43f6..7b62e193 100644 --- a/demo/Titanic/simulation/ocean.py +++ b/demo/Titanic/simulation/ocean.py @@ -4,8 +4,7 @@ from simulation.location_on_grid import LocationOnGrid, AreaOnGrid SECONDS_IN_MINUTE = 60 -# Following should hold True; SECONDS_IN_MINUTE % COLLISION_INTERVAL == 0 -COLLISION_INTERVAL = 10 +COLLISION_INTERVAL = 10 # Following should hold True; SECONDS_IN_MINUTE % COLLISION_INTERVAL == 0 COLLISION_THRESHOLD = 0.6 @@ -30,10 +29,8 @@ def minute_passes(self): objects_collided = set() while seconds_passed < SECONDS_IN_MINUTE: for floating_object in floating_objects: - floating_object.update_coordinates( - time_passed=COLLISION_INTERVAL) - objects_collided.update(self.detect_collisions( - collision_threshold=COLLISION_THRESHOLD)) + floating_object.update_coordinates(time_passed=COLLISION_INTERVAL) + objects_collided.update(self.detect_collisions(collision_threshold=COLLISION_THRESHOLD)) if objects_collided: floating_objects.difference(objects_collided) seconds_passed += COLLISION_INTERVAL diff --git a/demo/Titanic/system/titanic.py b/demo/Titanic/system/titanic.py index 2d9b98e6..b1a3ac62 100644 --- a/demo/Titanic/system/titanic.py +++ b/demo/Titanic/system/titanic.py @@ -3,8 +3,7 @@ class Titanic: def __init__(self, throttle, steering_direction): self.throttle = throttle # percentage - # degrees (0 means steering straight) - self.steering_direction = steering_direction + self.steering_direction = steering_direction # degrees (0 means steering straight) self.damaged = False def __repr__(self): diff --git a/robotmbt/suitereplacer.py b/robotmbt/suitereplacer.py index d1073667..943b36ca 100644 --- a/robotmbt/suitereplacer.py +++ b/robotmbt/suitereplacer.py @@ -79,14 +79,18 @@ def treat_model_based(self, **kwargs): model info that is included in the test steps, the test cases are modifed, mixed and matched to create unique traces and achieve more test coverage quicker. - Any arguments are handled as if using keyword `Update model-based options` + Any arguments must be named arguments. They are passed on as options to the selected model-based + processor. If an option was already set on library level (See: `Set model-based options` and + `Update model-based options`, then these arguments take precedence over the library option and + affect only the current test suite. """ self.robot_suite = self.current_suite logger.info(f"Analysing Robot test suite '{self.robot_suite.name}' for model-based execution.") - self.update_model_based_options(**kwargs) + local_settings = self.processor_options.copy() + local_settings.update(kwargs) master_suite = self.__process_robot_suite(self.robot_suite, parent=None) - modelbased_suite = self.processor_method(master_suite, **self.processor_options) + modelbased_suite = self.processor_method(master_suite, **local_settings) self.__clearTestSuite(self.robot_suite) self.__generateRobotSuite(modelbased_suite, self.robot_suite) diff --git a/utest/test_modelspace.py b/utest/test_modelspace.py index a992a4c6..2c34e6f8 100644 --- a/utest/test_modelspace.py +++ b/utest/test_modelspace.py @@ -163,19 +163,16 @@ def test_nested_attributes(self): self.m.process_expression('foo1.add_prop(bar1)') self.m.process_expression('foo1.bar1.foo2 = barbar') self.m.process_expression('foo1.bar1.foo3 = barbar') - self.assertIs(self.m.process_expression( - 'foo1.bar1.foo2 == foo1.bar1.foo3'), True) + self.assertIs(self.m.process_expression('foo1.bar1.foo2 == foo1.bar1.foo3'), True) def test_fail_on_naming_conflict_property_exists(self): self.m.process_expression('new foo1') - self.assertRaises( - ModellingError, self.m.process_expression, 'new foo1') + self.assertRaises(ModellingError, self.m.process_expression, 'new foo1') def test_fail_on_naming_conflict_literal_exists(self): self.m.process_expression('new foo1') self.m.process_expression('foo1.bar = foo2') - self.assertRaises( - ModellingError, self.m.process_expression, 'new foo2') + self.assertRaises(ModellingError, self.m.process_expression, 'new foo2') def test_list_comprehension_name_error(self): """ @@ -188,8 +185,7 @@ def test_list_comprehension_name_error(self): """ self.m.process_expression('new foo') self.m.process_expression("foo.bar = ['A', 'B', 'C']") - self.assertIs(self.m.process_expression( - "any(elm == A for elm in foo.bar)"), True) + self.assertIs(self.m.process_expression("any(elm == A for elm in foo.bar)"), True) def test_fail_exists_check_before_using_new(self): self.assertRaises(NameError, self.m.process_expression, 'foo') @@ -226,8 +222,7 @@ def test_fail_when_assigning_to_undefined_name(self): if sys.version_info >= (3, 13): self.assertEqual(str(cm.exception), "foo used before definition") else: - # <-- Known issue in Python 3.10/11/12 - self.assertEqual(str(cm.exception), "None used before assignment") + self.assertEqual(str(cm.exception), "None used before assignment") # <-- Known issue in Python 3.10/11/12 def test_fail_when_comparing_unknown_property(self): self.m.add_prop('foo') @@ -280,12 +275,10 @@ def setUp(self): self.m = ModelSpace() def test_scenario_scope_var_cannot_be_user_defined(self): - self.assertRaises( - ModellingError, self.m.process_expression, 'new scenario') + self.assertRaises(ModellingError, self.m.process_expression, 'new scenario') def test_scenario_scope_var_cannot_be_user_removed(self): - self.assertRaises( - ModellingError, self.m.process_expression, 'del scenario') + self.assertRaises(ModellingError, self.m.process_expression, 'del scenario') def test_initial_scenario_scope_cannot_be_ended(self): self.assertRaises(AssertionError, self.m.end_scenario_scope) @@ -295,13 +288,11 @@ def test_initial_scenario_scope_cannot_be_ended(self): def test_scenario_scope_is_unavailable_outside_scenarios(self): self.assertRaises(NameError, self.m.process_expression, 'scenario') - self.assertRaises( - ModellingError, self.m.process_expression, 'scenario.foo = bar') + self.assertRaises(ModellingError, self.m.process_expression, 'scenario.foo = bar') self.m.new_scenario_scope() self.m.end_scenario_scope() self.assertRaises(NameError, self.m.process_expression, 'scenario') - self.assertRaises( - ModellingError, self.m.process_expression, 'scenario.foo = bar') + self.assertRaises(ModellingError, self.m.process_expression, 'scenario.foo = bar') def test_scenario_scope_is_available_inside_scenarios(self): self.m.new_scenario_scope() @@ -311,8 +302,7 @@ def test_scenario_scope_is_available_inside_scenarios(self): def test_scenario_used_as_literal(self): self.m.process_expression('new foo') - self.assertRaises( - ModellingError, self.m.process_expression, 'foo.bar = scenario') + self.assertRaises(ModellingError, self.m.process_expression, 'foo.bar = scenario') self.m.process_expression('foo.bar = "scenario"') self.assertEqual(self.m.process_expression('foo.bar'), "scenario") @@ -321,20 +311,17 @@ def test_scenario_used_as_attribute_name(self): self.m.process_expression('foo.scenario = bar') self.m.new_scenario_scope() self.m.process_expression('scenario.foo = bar') - self.assertEqual(self.m.process_expression( - 'scenario.foo'), self.m.process_expression('foo.scenario')) + self.assertEqual(self.m.process_expression('scenario.foo'), self.m.process_expression('foo.scenario')) def test_scenario_var_is_unavailable_outside_scenario(self): self.m.new_scenario_scope() self.m.process_expression('scenario.foo = bar') self.m.end_scenario_scope() - self.assertRaises( - ModellingError, self.m.process_expression, 'scenario.bar == bar') + self.assertRaises(ModellingError, self.m.process_expression, 'scenario.bar == bar') with self.assertRaises(ModellingError) as cm: self.m.process_expression('scenario.bar == bar') self.assertIsInstance(cm.exception, ModellingError) - self.assertTrue(str(cm.exception).startswith( - "Accessing scenario scope while there is no scenario active")) + self.assertTrue(str(cm.exception).startswith("Accessing scenario scope while there is no scenario active")) def test_scenario_var_is_unavailable_in_next_scenario(self): self.m.new_scenario_scope() diff --git a/utest/test_steparguments.py b/utest/test_steparguments.py index c260d9c6..6c906ca6 100644 --- a/utest/test_steparguments.py +++ b/utest/test_steparguments.py @@ -160,10 +160,10 @@ def test_spaces_and_underscores_are_interchangable(self): self.assertEqual(arg1.codestring, arg2.codestring) def test_other_values_become_unique_identifiers(self): - valuelist = ['bar', 'foo bar', 'foo2bar', '${bar}', # strings - ' ', '\t', '\n', ' ', ' \n', '\a', # whitespace/non-printable - '#', '+-', '-+', '"', "'", 'パイ', # special characters - max, 'elif', 'import', 'new', 'del', # reserved words + valuelist = ['bar', 'foo bar', 'foo2bar', '${bar}', # strings + ' ', '\t', '\n', ' ', ' \n', '\a', # whitespace/non-printable + '#', '+-', '-+', '"', "'", 'パイ', # special characters + max, 'elif', 'import', 'new', 'del', # reserved words lambda x: x/2, self, unittest.TestCase] # functions and objects argsset = set() for v in valuelist: @@ -206,10 +206,8 @@ def test_number_bool_none_identifiers(self): def test_keep_identifier_names_close_to_original(self): self.assertEqual(StepArgument.make_identifier('foo bar'), 'foo_bar') - self.assertTrue(StepArgument.make_identifier( - 'import').startswith('import')) - self.assertTrue(StepArgument.make_identifier( - '4foo2bar').endswith('foo2bar')) + self.assertTrue(StepArgument.make_identifier('import').startswith('import')) + self.assertTrue(StepArgument.make_identifier('4foo2bar').endswith('foo2bar')) class TestStepArguments(unittest.TestCase): diff --git a/utest/test_substitutionmap.py b/utest/test_substitutionmap.py index 8c93171d..b8e78a47 100644 --- a/utest/test_substitutionmap.py +++ b/utest/test_substitutionmap.py @@ -355,7 +355,8 @@ def test_adding_constraint_does_not_affect_undo_remove_stack(self): c.remove_option('four') c.add_constraint(['one', 'two']) self.assertCountEqual(c.optionset, ['two']) - c.undo_remove() # four was never in there, so isn't added, and three + c.undo_remove() + # four was never in there, so isn't added, and three # was removed by adding a constraint and is ignored. self.assertCountEqual(c.optionset, ['two']) c.undo_remove() diff --git a/utest/test_suitedata.py b/utest/test_suitedata.py index 72922d06..ef6e3866 100644 --- a/utest/test_suitedata.py +++ b/utest/test_suitedata.py @@ -103,8 +103,7 @@ def test_error_in_subsuite_setup_is_detected(self): self.assertEqual(errorsteps[0].model_info['error'], 'oops') def test_error_in_subsuite_scenario_is_detected(self): - self.topsuite.suites[0].scenarios[0].steps[1].model_info = dict( - error='oops') + self.topsuite.suites[0].scenarios[0].steps[1].model_info = dict(error='oops') self.assertIs(self.topsuite.has_error(), True) errorsteps = self.topsuite.steps_with_errors() self.assertEqual(len(errorsteps), 1) @@ -193,8 +192,7 @@ def test_step_errors_are_reported(self): self.assertIs(self.scenario.has_error(), True) errorsteps = self.scenario.steps_with_errors() self.assertEqual(len(errorsteps), 2) - self.assertEqual([s.model_info['error'] - for s in errorsteps], ['oops', 'oh ow']) + self.assertEqual([s.model_info['error'] for s in errorsteps], ['oops', 'oh ow']) def test_by_default_setup_and_teardown_are_empty(self): self.assertIs(self.scenario.setup, None) @@ -205,16 +203,14 @@ def test_setup_errors(self): step.model_info = dict(error='oops') self.scenario.setup = step self.assertIs(self.scenario.has_error(), True) - self.assertEqual(self.scenario.steps_with_errors(), - [self.scenario.setup]) + self.assertEqual(self.scenario.steps_with_errors(), [self.scenario.setup]) def test_teardown_errors(self): step = Step('my teardown', parent=self.scenario) step.model_info = dict(error='oops') self.scenario.teardown = step self.assertIs(self.scenario.has_error(), True) - self.assertEqual(self.scenario.steps_with_errors(), - [self.scenario.teardown]) + self.assertEqual(self.scenario.steps_with_errors(), [self.scenario.teardown]) def test_combined_errors(self): setup_step = Step('my setup', parent=self.scenario) @@ -284,8 +280,7 @@ def test_copies_are_independent(self): self.assertNotEqual(dup.name, self.scenario.name) self.assertIsNot(dup.steps[0], self.scenario.steps[0]) self.assertEqual(dup.steps[0].keyword, self.scenario.steps[0].keyword) - self.assertNotEqual(dup.steps[-1].keyword, - self.scenario.steps[-1].keyword) + self.assertNotEqual(dup.steps[-1].keyword, self.scenario.steps[-1].keyword) def test_exteranally_determined_attributes_are_copied_along(self): self.scenario.src_id = 7 @@ -378,78 +373,59 @@ def test_modified_embedded_arguments_are_part_of_the_step_str(self, mock): step.add_robot_dependent_data(RobotKwStub()) self.assertNotIn('error', step.model_info) step.args['${bar}'].value = 'new bar' - self.assertEqual(str(step), RobotKwStub.STEPTEXT.replace( - 'bar_value', 'new bar')) + self.assertEqual(str(step), RobotKwStub.STEPTEXT.replace('bar_value', 'new bar')) def test_all_arguments_are_part_of_the_full_keyword_text(self, mock): - step = Step(RobotKwStub.STEPTEXT, 'posA', - 'pos2=posB', 'named1=namedA', parent=None) - self.assertEqual( - step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB named1=namedA") + step = Step(RobotKwStub.STEPTEXT, 'posA', 'pos2=posB', 'named1=namedA', parent=None) + self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB named1=namedA") def test_return_value_assignment_is_part_of_the_full_keyword_text(self, mock): step = Step(RobotKwStub.STEPTEXT, assign=('${output}',), parent=None) - self.assertEqual(step.full_keyword, - "${output} " + RobotKwStub.STEPTEXT) + self.assertEqual(step.full_keyword, "${output} " + RobotKwStub.STEPTEXT) def test_return_value_assignment_with_eq_is_part_of_the_full_keyword_text(self, mock): step = Step(RobotKwStub.STEPTEXT, assign=('${output}=',), parent=None) - self.assertEqual(step.full_keyword, - "${output}= " + RobotKwStub.STEPTEXT) + self.assertEqual(step.full_keyword, "${output}= " + RobotKwStub.STEPTEXT) def test_return_value_assignment_with_eq_after_space_is_part_of_the_full_keyword_text(self, mock): step = Step(RobotKwStub.STEPTEXT, assign=('${output} =',), parent=None) - self.assertEqual(step.full_keyword, - "${output} = " + RobotKwStub.STEPTEXT) + self.assertEqual(step.full_keyword, "${output} = " + RobotKwStub.STEPTEXT) def test_return_value_multi_assignment_is_part_of_the_full_keyword_text(self, mock): - step = Step(RobotKwStub.STEPTEXT, assign=( - '${output1}', '${output2}='), parent=None) - self.assertEqual( - step.full_keyword, "${output1} ${output2}= " + RobotKwStub.STEPTEXT) + step = Step(RobotKwStub.STEPTEXT, assign=('${output1}', '${output2}='), parent=None) + self.assertEqual(step.full_keyword, "${output1} ${output2}= " + RobotKwStub.STEPTEXT) def test_argument_with_default_is_omitted_from_keyword_when_not_mentioned_named(self, mock): step = Step(RobotKwStub.STEPTEXT, 'posA', 'pos2=posB', parent=None) step.args = StubStepArguments([StubArgument(name='pos1', value='posA', is_default=False, kind='POSITIONAL'), - StubArgument( - name='pos2', value='posB', is_default=False, kind='NAMED'), + StubArgument(name='pos2', value='posB', is_default=False, kind='NAMED'), StubArgument(name='named1', value='namedA', is_default=True, kind='NAMED')]) self.assertTupleEqual(step.posnom_args_str, ('posA', 'pos2=posB')) - self.assertEqual(step.full_keyword, - f"{RobotKwStub.STEPTEXT} posA pos2=posB") + self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB") def test_argument_with_default_is_omitted_from_keyword_when_not_mentioned_positional(self, mock): step = Step(RobotKwStub.STEPTEXT, 'posA', 'posB', parent=None) step.args = StubStepArguments([StubArgument(name='pos1', value='posA', is_default=False, kind='POSITIONAL'), - StubArgument( - name='pos2', value='posB', is_default=False, kind='POSITIONAL'), + StubArgument(name='pos2', value='posB', is_default=False, kind='POSITIONAL'), StubArgument(name='named1', value='namedA', is_default=True, kind='POSITIONAL')]) self.assertTupleEqual(step.posnom_args_str, ('posA', 'posB')) - self.assertEqual(step.full_keyword, - f"{RobotKwStub.STEPTEXT} posA posB") + self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA posB") def test_argument_with_default_is_included_in_keyword_when_mentioned_named(self, mock): - step = Step(RobotKwStub.STEPTEXT, 'posA', - 'pos2=posB', 'named1=namedA', parent=None) + step = Step(RobotKwStub.STEPTEXT, 'posA', 'pos2=posB', 'named1=namedA', parent=None) step.args = StubStepArguments([StubArgument(name='pos1', value='posA', is_default=False, kind='POSITIONAL'), - StubArgument( - name='pos2', value='posB', is_default=False, kind='NAMED'), + StubArgument(name='pos2', value='posB', is_default=False, kind='NAMED'), StubArgument(name='named1', value='namedA', is_default=False, kind='NAMED')]) - self.assertTupleEqual(step.posnom_args_str, - ('posA', 'pos2=posB', 'named1=namedA')) - self.assertEqual( - step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB named1=namedA") + self.assertTupleEqual(step.posnom_args_str, ('posA', 'pos2=posB', 'named1=namedA')) + self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA pos2=posB named1=namedA") def test_argument_with_default_is_included_in_keyword_when_mentioned_positional(self, mock): - step = Step(RobotKwStub.STEPTEXT, 'posA', - 'posB', 'namedA', parent=None) + step = Step(RobotKwStub.STEPTEXT, 'posA', 'posB', 'namedA', parent=None) step.args = StubStepArguments([StubArgument(name='pos1', value='posA', is_default=False, kind='POSITIONAL'), - StubArgument( - name='pos2', value='posB', is_default=False, kind='POSITIONAL'), + StubArgument(name='pos2', value='posB', is_default=False, kind='POSITIONAL'), StubArgument(name='named1', value='namedA', is_default=False, kind='POSITIONAL')]) self.assertTupleEqual(step.posnom_args_str, ('posA', 'posB', 'namedA')) - self.assertEqual(step.full_keyword, - f"{RobotKwStub.STEPTEXT} posA posB namedA") + self.assertEqual(step.full_keyword, f"{RobotKwStub.STEPTEXT} posA posB namedA") def test_positional_and_named_arguments_are_available_in_robot_tuple_format(self, mock): argtuple = 'posA', 'pos2=posB', 'named1=namedA' diff --git a/utest/test_suiteprocessors.py b/utest/test_suiteprocessors.py index cfa7888f..6989470e 100644 --- a/utest/test_suiteprocessors.py +++ b/utest/test_suiteprocessors.py @@ -79,10 +79,8 @@ def test_generated_seeds_have_max_2_consecutive_vowels_or_consonants(self, mock) SuiteProcessors._init_randomiser("new") new_seed = mock.call_args.args[0] self._is_generated_seed(new_seed) - self.assertNotIn( - '***', new_seed.translate({ord(c): '*' for c in 'aeiouy'})) - self.assertNotIn( - '***', new_seed.translate({ord(c): '*' for c in 'bcdfghjklmnpqrstvwxz'})) + self.assertNotIn('***', new_seed.translate({ord(c): '*' for c in 'aeiouy'})) + self.assertNotIn('***', new_seed.translate({ord(c): '*' for c in 'bcdfghjklmnpqrstvwxz'})) def _is_generated_seed(self, arg): """ diff --git a/utest/test_tracestate.py b/utest/test_tracestate.py index d91410d8..5bfb63a3 100644 --- a/utest/test_tracestate.py +++ b/utest/test_tracestate.py @@ -208,8 +208,7 @@ def test_completing_size_three_trace_after_rewind(self): retry_third = ts.next_candidate() ts.confirm_full_scenario(retry_third, retry_third, {}) self.assertIs(ts.coverage_reached(), True) - self.assertEqual(ts.get_trace(), [ - retry_first, retry_second, retry_third]) + self.assertEqual(ts.get_trace(), [retry_first, retry_second, retry_third]) def test_highest_part_when_index_not_present(self): ts = TraceState(1) diff --git a/utest/test_tracestate_refinement.py b/utest/test_tracestate_refinement.py index 497a81a8..2cbdca61 100644 --- a/utest/test_tracestate_refinement.py +++ b/utest/test_tracestate_refinement.py @@ -160,8 +160,7 @@ def test_nested_refinement(self): self.assertIs(ts.coverage_reached(), False) ts.confirm_full_scenario(top_level, 'T1.0', {}) self.assertIs(ts.coverage_reached(), True) - self.assertEqual(ts.get_trace(), [ - 'T1.1', 'M1.1', 'B1', 'M1.0', 'T1.0']) + self.assertEqual(ts.get_trace(), ['T1.1', 'M1.1', 'B1', 'M1.0', 'T1.0']) def test_rewind_to_swap_nested_refinement(self): ts = TraceState(3) @@ -181,8 +180,7 @@ def test_rewind_to_swap_nested_refinement(self): self.assertIs(ts.coverage_reached(), False) ts.confirm_full_scenario(top_level, 'T1.0', {}) self.assertIs(ts.coverage_reached(), True) - self.assertEqual(ts.get_trace(), [ - 'T1.1', 'M1.1', 'B1', 'M1.0', 'T1.0']) + self.assertEqual(ts.get_trace(), ['T1.1', 'M1.1', 'B1', 'M1.0', 'T1.0']) def test_rewind_nested_refinement_as_one(self): ts = TraceState(4) @@ -299,19 +297,16 @@ def test_nested_refinements_are_all_reported_as_in_refinement(self): ts.push_partial_scenario(0, 'T1.1', {}) ts.push_partial_scenario(1, 'M1.1', {}) ts.push_partial_scenario(2, 'B1.1', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), [ - 'T1.1', 'M1.1', 'B1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1', 'B1.1']) def test_closing_refinement_removes_it_from_list(self): ts = TraceState(4) ts.push_partial_scenario(0, 'T1.1', {}) ts.push_partial_scenario(1, 'M1.1', {}) ts.push_partial_scenario(2, 'B1.1', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), [ - 'T1.1', 'M1.1', 'B1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1', 'B1.1']) ts.confirm_full_scenario(2, 'B1.0', {}) - self.assertEqual( - ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) def test_multi_step_refinement_is_reported_only_once(self): ts = TraceState(4) @@ -320,19 +315,16 @@ def test_multi_step_refinement_is_reported_only_once(self): ts.confirm_full_scenario(2, 'B1', {}) ts.push_partial_scenario(1, 'M1.2', {}) ts.confirm_full_scenario(3, 'B2', {}) - self.assertEqual( - ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) def test_rewind_open_refinement_removes_it_from_list(self): ts = TraceState(4) ts.push_partial_scenario(0, 'T1.1', {}) ts.push_partial_scenario(1, 'M1.1', {}) ts.push_partial_scenario(2, 'B1.1', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), [ - 'T1.1', 'M1.1', 'B1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1', 'B1.1']) ts.rewind() - self.assertEqual( - ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T1.1', 'M1.1']) def test_rewind_finished_scenario_with_refinement_removes_enclosed_refinements(self): ts = TraceState(5) @@ -341,8 +333,7 @@ def test_rewind_finished_scenario_with_refinement_removes_enclosed_refinements(s ts.push_partial_scenario(2, 'M1.1', {}) ts.push_partial_scenario(3, 'B1.1', {}) ts.confirm_full_scenario(4, 'S1', {}) - self.assertEqual(ts.find_scenarios_with_active_refinement(), [ - 'T2.1', 'M1.1', 'B1.1']) + self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T2.1', 'M1.1', 'B1.1']) ts.confirm_full_scenario(3, 'B1.0', {}) ts.confirm_full_scenario(2, 'M1.0', {}) self.assertEqual(ts.find_scenarios_with_active_refinement(), ['T2.1']) From 3c05f68bba1257076b4aed481688246352f84a22 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Sun, 14 Dec 2025 17:28:43 +0100 Subject: [PATCH 10/18] Use consistent casing in naming RobotMBT --- demo/Titanic/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/demo/Titanic/README.md b/demo/Titanic/README.md index 5b91f9f9..a1739ba1 100644 --- a/demo/Titanic/README.md +++ b/demo/Titanic/README.md @@ -1,10 +1,10 @@ -# robotMBT Titanic demo +# RobotMBT Titanic demo ## What is it? -The purpose of this demo is to showcase the Model-Based Testing concepts available from the [robotMBT](https://github.com/JFoederer/robotframeworkMBT) library using a [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) style project. It is based on the principle of [specification by example](https://en.wikipedia.org/wiki/Specification_by_example), using _given-when-then_ style scenarios. +The purpose of this demo is to showcase the Model-Based Testing concepts available from the [RobotMBT](https://github.com/JFoederer/robotframeworkMBT) library using a [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) style project. It is based on the principle of [specification by example](https://en.wikipedia.org/wiki/Specification_by_example), using _given-when-then_ style scenarios. -Given-steps typically describe preconditions, i.e. state, but classically given-steps are implemented as actions to get to that desired precondition. Now, consider using [specification by example](https://en.wikipedia.org/wiki/Specification_by_example). If your specification is complete, and your examples are consistent, then any given-state must be reachable by operating the system within specification, following the examples. In this demo we use [robotMBT](https://github.com/JFoederer/robotframeworkMBT) to specify a complete story, on varying levels of detail, using small consise scenarios. Then we let [robotMBT](https://github.com/JFoederer/robotframeworkMBT) construct a complete storyline, so we don't have to worry about how to reach all the correct preconditions. +Given-steps typically describe preconditions, i.e. state, but classically given-steps are implemented as actions to get to that desired precondition. Now, consider using [specification by example](https://en.wikipedia.org/wiki/Specification_by_example). If your specification is complete, and your examples are consistent, then any given-state must be reachable by operating the system within specification, following the examples. In this demo we use [RobotMBT](https://github.com/JFoederer/robotframeworkMBT) to specify a complete story, on varying levels of detail, using small consise scenarios. Then we let [RobotMBT](https://github.com/JFoederer/robotframeworkMBT) construct a complete storyline, so we don't have to worry about how to reach all the correct preconditions. Please keep in mind that the library and this demo are still in the early development phases and offered functionality is still limited. However, in good agile spirit, we still wanted to publish the results. @@ -18,7 +18,7 @@ There are a total of 7 scenarios in this demo, 10 if you use the extended varian It might seem odd at first, seeing the [story of Titanic](https://en.wikipedia.org/wiki/Sinking_of_the_Titanic) in a context that is typically used in more technical environments. However, the [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) process is mostly non-technical and using a topic like Titanic helps to prevent technical bias. Another important point is that we want to stick to writing and maintaining short, to-the-point scenarios. From these, we want to compose larger scenarios, describing behaviour of complex systems, start to end, like telling a story. What better use for that than a well known story? -The [story of Titanic](https://en.wikipedia.org/wiki/Sinking_of_the_Titanic) fits these criteria and, since it already happened, there should be little discussion on the specification. It will be interesting to see if the test case generation mechanism from [robotMBT](https://github.com/JFoederer/robotframeworkMBT) can reconstruct the familiar story, and then some variations thereof. After all, the maiden voyage of Titanic was just one example of what could have happened... +The [story of Titanic](https://en.wikipedia.org/wiki/Sinking_of_the_Titanic) fits these criteria and, since it already happened, there should be little discussion on the specification. It will be interesting to see if the test case generation mechanism from [RobotMBT](https://github.com/JFoederer/robotframeworkMBT) can reconstruct the familiar story, and then some variations thereof. After all, the maiden voyage of Titanic was just one example of what could have happened... ## Running the demo From a79fe34167d3d5d0db815595171cd4b3be8fabc3 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Sun, 14 Dec 2025 17:29:49 +0100 Subject: [PATCH 11/18] Add contribution guidelines --- CONTRIBUTING.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++ 2 files changed, 103 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..7f9b224e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,99 @@ +# Contribution guidelines RobotMBT + +Thank you for considering to contribute to this project. Welcome! Your contribution already starts when you use this software and share your experience with the people around you. The [Robot Framework Slack](http://slack.robotframework.org/) channels are good place to share, ask questions and if you can, answer them. + +If you want to get involved on GitHub, you can so by submitting issues or offering code improvements. These guidelines will help you to find your way. These guidelines expect readers to have a basic knowledge about open source as well as why and how to contribute to an open source project. If you are new to these topics, please have a look at the generic [Open Source Guides](https://opensource.guide/) first. + +## Code of conduct + +If you want to be part of this community, then we expect you to respect our norms and values. These are in line with the [GitHub code of conduct](https://docs.github.com/en/site-policy/github-terms/github-community-code-of-conduct) and the [Slack code of conduct](https://docs.slack.dev/community-code-of-conduct/). In short, we expect you to: + +- Be welcoming. +- Be kind. +- Look out for each other. + +## Submitting issues + +Defects and enhancements are tracked in the [issue tracker](https://github.com/JFoederer/robotframeworkMBT/issues). Take care to narrow down any issue to this project before submitting an issue here. RobotMBT cannot fix your computer! If you are unsure if something is worth submitting, you can first ask on [Slack](http://slack.robotframework.org/). Before submitting a new issue, it is always a good idea to check if something similar was already reported. If it is, please add your comments to the existing issue instead of creating a new one. Communication in issues on GitHub is done in English. + +Take notice that issues do not get resolved by themselves. Someone will need to spend time on the topic. Be prepared to wait, contribute yourself or arrange budget to hire someone for the job. + +### Reporting defects + +When reporting a defect, be precise and concise in your description and write in way that helps others understand, and preferably reproduce, the issue. Screenshots can be very helpful, but when adding logging or other textual information, please keep the textual form. + +Note that all information in the issue tracker is public. Do not include any confidential information there. + +Be sure to add information about: + +- The applicable version(s) of RobotMBT (use `pip list` and check for `robotframework-mbt`) +- Your Robot Framework version (use `pip list` and check for `robotframework`) +- Your Python version (check using `python --version`) +- Your Operating System +- Used custom settings for RobotMBT (at library and test suite level) + +Version information about Robot Framework, Python and the Operating System are also reported at the start of Robot's `output.xml` file. + +### Enhancement requests + +When proposing an enhancement, a feature request, be clear about the use cases. Who will benefit from the enhancement and in what way? Describe the expected behaviour and use concrete examples to illustrate the intent. + +## Code contributions + +If you have fixed a defect or implemented an enhancement, you can contribute your changes via [GitHub's pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). This is not restricted to code, on the contrary, fixes and enhancements to documentation and tests alone are also very valuable! + +### first steps + +- [Clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) and/or [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) the RobotMBT repo. If you are not a fan of command line tools, [GitHub Desktop](https://github.com/apps/desktop) can help you. +- [Run the tests](#running-tests) to check your starting point. +- Write new failing tests to cover your intended changes. +- Implement your changes. + +### Definition of Done + +The Definition of Done is there to ensure that pull requests are fully self-contained, and leave no open ends. In other words: When the pull request is merged, it is 100% done. This keeps the main branch ready for release, at all times. + +That means that for each pull request you need to ensure: + +- No regression is introduced. +- New functionality is covered by tests. +- Code style follows the standard. +- Documentation is up to date. +- The PR branch is 0 commits behind. + +### Running tests + +Tests can be executed from the command line by running `python run_tests.py`. This will run all unit tests, followed by the Robot acceptance tests. Use `--help` for additional info. + +### Non-regression criteria + +The criteria for proving non-regression are: + +- All automated regression tests pass +- All supported Python, Robot Framework and OS versions still work (see `pyproject.toml` for supported versions). +- The [demos](https://github.com/JFoederer/robotframeworkMBT/tree/main/demo/Titanic) still work. +- Manual checks are executed to cover the automation's blind spots and subjective elements (e.g. some visual inspection on layout and assessing overall look and feel). + +### Guidelines for writing new tests + +For this project we are not maintaining separate requirements documentation. The user documentation explains the software's purpose and scope, the tests further specify its concrete behaviour. Keep this in mind when writing tests and pay extra attention to documenting your test cases. They are more than just bug catchers. If code exists due to a technical limitation rather than a requirement, be sure to document your design decision. + +Tests are located in the _atest_ and _utest_ folders, which stands for _acceptance test_ and _unit test_ respectively. The acceptance tests are Robot tests that cover user-visible behaviour using black box testing techniques. They typically do not cover all details, unless some Robot Framework interaction is involved. The unit tests do go in depth, which includes white box techniques to include coverage on the dark corners of the code. Choose the right type of test for what you are covering. + +A specific challenge for this project is that there is a lot of test case generation going on. Take care that variations in the generation process can not alter the intended coverage of a test and do not yield false positives. False positives being pass results without proof for passing, like checking 'all' results in an empty list. And lastly, keep the resulting number of test cases in a run deterministic, so that we keep comparable results. + +### Coding style + +Maintainability is the main driver for coding style. Always write your code with the mindset that you are writing it for someone else, and that this person's experience level is slightly below the average in the project. Code is written following the [PEP 8](https://peps.python.org/pep-0008) Style guide and [SOLID](https://en.wikipedia.org/wiki/SOLID) principles. + +#### Formatting + +Formatting follows the default rules of [autopep8](https://pypi.org/project/autopep8/) with the exception that the maximum line length is set to 120. Note however, that the extended line length is not an invite to always write long lines. + +Researchers have suggested [[ref.](https://www.academia.edu/6232736/The_influence_of_font_type_and_line_length_on_visual_search_and_information_retrieval_in_web_pages)] that longer lines are better suited for cases when the information will likely be scanned, while shorter lines (45-75 characters) are appropriate when the information is meant to be read thoroughly. Keep this in mind when writing code and documentation, taking the current indentation level into account. + +#### Docstrings, comments and logging + +Docstrings are written using a black box approach. One should not need to know the inside of a class or function in order to use it. Use comments to annotate code for maintainers. Prevent trivial comments and use descriptive names to make your code self-explanatory. When documenting external interfaces, also check whether the user documentation requires an update. + +Useful information that is runtime dependent should be logged. Information that is useful after a passing test run is logged at info-level. Information that is useful for analysing failed tests is logged at debug-level. Be careful not to make assumptions in what you log. Recheck log statements if your changes affect the context in which the code is run. Only report about what you know to be true. diff --git a/README.md b/README.md index 009e20f2..6c3e9309 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,10 @@ If you want to set configuration options for use in multiple test suites without Tip: [Robot dictionaries](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#dictionary-variable) (`&{ }`) can be used to group related options and pass them as one set. +## Contributing + +If you have feedback, ideas, or want to get involved in coding, then check out the [Contribution guidelines](https://github.com/JFoederer/robotframeworkMBT/blob/main/CONTRIBUTING.md). + ## Disclaimer Please note that this library is in a premature state and hasn't reached its first official (1.0) release yet. Developments are ongoing within the context of the [TiCToC](https://tictoc.cs.ru.nl/) research project. Interface changes are still frequent, and no deprecation warnings are being issued yet. From 04011bbd1946d3b7abfd22553932064d1c20e33e Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:40:41 +0100 Subject: [PATCH 12/18] add workflow to check demo --- .github/workflows/run-demo.yml | 35 +++++++++++++++++++++++++++++++++ .github/workflows/run-tests.yml | 9 ++++----- 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/run-demo.yml diff --git a/.github/workflows/run-demo.yml b/.github/workflows/run-demo.yml new file mode 100644 index 00000000..a3cfe5b6 --- /dev/null +++ b/.github/workflows/run-demo.yml @@ -0,0 +1,35 @@ +# This workflow installs required Python dependencies and then runs the demo. +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Run Demo + +on: + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: windows-latest # Deliberately different OS compared to the test run. + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.14" # Deliberately different Python version compared to the test run. + - name: Install dependencies + run: | + python -m pip install --upgrade pip # upgrade pip to latest version + pip install . # install pyproject.toml dependencies - excludes optional dependencies (such as visualisation) + - name: Run basic demo (no repeating scenarios) + working-directory: ./demo/Titanic + run: | + python run_demo.py miss + - name: Run extended demo + working-directory: ./demo/Titanic + run: | + python run_demo.py extended diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c712f563..8e75f096 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,4 +1,4 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python +# This workflow installs required Python dependencies and then runs the available tests. # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python name: Run Acceptance and Unit tests @@ -20,12 +20,11 @@ jobs: - name: Set up Python uses: actions/setup-python@v3 with: - python-version: "3.10" + python-version: "3.10" # Only the oldest supported Python version is included here - name: Install dependencies run: | python -m pip install --upgrade pip # upgrade pip to latest version - pip install . # install PyProject.toml dependencies - excludes optional dependencies (such as visualisation)! - - name: Test with pytest + pip install . # install pyproject.toml dependencies - excludes optional dependencies (such as visualisation) + - name: Run tests run: | python run_tests.py - From ad79db56d0bfa3d27d541f6b8476a1ead1b778e7 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Mon, 15 Dec 2025 14:11:03 +0100 Subject: [PATCH 13/18] added exit code propagation to demo --- demo/Titanic/run_demo.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/demo/Titanic/run_demo.py b/demo/Titanic/run_demo.py index d6936c06..37977ff8 100644 --- a/demo/Titanic/run_demo.py +++ b/demo/Titanic/run_demo.py @@ -21,10 +21,12 @@ # The base folder needs to be added to the python path to resolve the dependencies. You # will also need to add this path to your IDE options when running from there. - robot.run_cli(['--outputdir', OUTPUT_ROOT, - '--pythonpath', THIS_DIR, - '--exclude', HIT_MISS_TAG, - '--exclude', EXTENDED_TAG, - '--loglevel', 'DEBUG:INFO', - SCENARIO_FOLDER], - exit=False) + exitcode = robot.run_cli(['--outputdir', OUTPUT_ROOT, + '--pythonpath', THIS_DIR, + '--exclude', HIT_MISS_TAG, + '--exclude', EXTENDED_TAG, + '--loglevel', 'DEBUG:INFO', + SCENARIO_FOLDER], + exit=False) + + sys.exit(exitcode) From 084b7726f71da694b954619de6c51e50efcfc505 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Wed, 17 Dec 2025 08:30:05 +0100 Subject: [PATCH 14/18] install local requirements.txt for demo --- .github/workflows/run-demo.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-demo.yml b/.github/workflows/run-demo.yml index a3cfe5b6..b65bb89f 100644 --- a/.github/workflows/run-demo.yml +++ b/.github/workflows/run-demo.yml @@ -10,6 +10,10 @@ on: permissions: contents: read +defaults: + run: + working-directory: ./demo/Titanic + jobs: build: @@ -23,13 +27,11 @@ jobs: python-version: "3.14" # Deliberately different Python version compared to the test run. - name: Install dependencies run: | - python -m pip install --upgrade pip # upgrade pip to latest version - pip install . # install pyproject.toml dependencies - excludes optional dependencies (such as visualisation) + python -m pip install --upgrade pip # upgrade pip to latest version + pip install -r requirements.txt # install pyproject.toml dependencies - excludes optional dependencies (such as visualisation) - name: Run basic demo (no repeating scenarios) - working-directory: ./demo/Titanic run: | python run_demo.py miss - name: Run extended demo - working-directory: ./demo/Titanic run: | python run_demo.py extended From 177468a796d601fa29ede39250fd7fe6ad00de89 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 17 Dec 2025 09:27:23 +0100 Subject: [PATCH 15/18] refactor comments and yaml syntax --- .github/workflows/run-demo.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-demo.yml b/.github/workflows/run-demo.yml index b65bb89f..a4171e43 100644 --- a/.github/workflows/run-demo.yml +++ b/.github/workflows/run-demo.yml @@ -28,10 +28,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip # upgrade pip to latest version - pip install -r requirements.txt # install pyproject.toml dependencies - excludes optional dependencies (such as visualisation) + pip install -r requirements.txt # install demo dependencies - name: Run basic demo (no repeating scenarios) - run: | - python run_demo.py miss + run: python run_demo.py miss - name: Run extended demo - run: | - python run_demo.py extended + run: python run_demo.py extended From b680d925eb161e6de1d1dd49c432d765893c6e09 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 17 Dec 2025 16:39:35 +0100 Subject: [PATCH 16/18] adjusted contributing (style, formatting, sentence structure --- CONTRIBUTING.md | 82 +++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f9b224e..feaef627 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,99 +1,113 @@ # Contribution guidelines RobotMBT -Thank you for considering to contribute to this project. Welcome! Your contribution already starts when you use this software and share your experience with the people around you. The [Robot Framework Slack](http://slack.robotframework.org/) channels are good place to share, ask questions and if you can, answer them. +Welcome! Thank you for considering to contribute to this project. Your contribution already starts when you use this software and share your experiences with people around you. -If you want to get involved on GitHub, you can so by submitting issues or offering code improvements. These guidelines will help you to find your way. These guidelines expect readers to have a basic knowledge about open source as well as why and how to contribute to an open source project. If you are new to these topics, please have a look at the generic [Open Source Guides](https://opensource.guide/) first. +## Communication channels +### Slack +If you want to share/ask/answer any questions, the [Robot Framework Slack](http://slack.robotframework.org/) channels are good place to do so. -## Code of conduct +### GitHub +If you want to get involved on GitHub, you can so by submitting issues or offering code improvements. These guidelines will help you to find your way. These guidelines expect readers to have a basic knowledge about open source as well as why and how to contribute to an open source project. If you are new to these topics, please have a look at the generic [Open Source Guides](https://opensource.guide/) first. -If you want to be part of this community, then we expect you to respect our norms and values. These are in line with the [GitHub code of conduct](https://docs.github.com/en/site-policy/github-terms/github-community-code-of-conduct) and the [Slack code of conduct](https://docs.slack.dev/community-code-of-conduct/). In short, we expect you to: +## Code of Conduct (CoC) +If you want to be part of this community, then we expect you to respect our norms and values. These are in line with the [GitHub CoC](https://docs.github.com/en/site-policy/github-terms/github-community-code-of-conduct) and the [Slack CoC](https://docs.slack.dev/community-code-of-conduct/). In short, we expect you to: - Be welcoming. - Be kind. - Look out for each other. -## Submitting issues -Defects and enhancements are tracked in the [issue tracker](https://github.com/JFoederer/robotframeworkMBT/issues). Take care to narrow down any issue to this project before submitting an issue here. RobotMBT cannot fix your computer! If you are unsure if something is worth submitting, you can first ask on [Slack](http://slack.robotframework.org/). Before submitting a new issue, it is always a good idea to check if something similar was already reported. If it is, please add your comments to the existing issue instead of creating a new one. Communication in issues on GitHub is done in English. +## Submitting issues +Defects and enhancements are tracked in [GitHub Issues](https://github.com/JFoederer/robotframeworkMBT/issues). Please make sure the issue is caused by this project in particular before submitting an issue here. If you are unsure if something is worth submitting, you can first ask on [Slack](http://slack.robotframework.org/). Before submitting a new issue, it is always a good idea to check if something similar was already reported. If it is, please add your comments to the existing issue instead of creating a new one. Communication in issues on GitHub is done in English. Take notice that issues do not get resolved by themselves. Someone will need to spend time on the topic. Be prepared to wait, contribute yourself or arrange budget to hire someone for the job. -### Reporting defects -When reporting a defect, be precise and concise in your description and write in way that helps others understand, and preferably reproduce, the issue. Screenshots can be very helpful, but when adding logging or other textual information, please keep the textual form. +### Reporting defects +When reporting a defect, be precise and concise in your description. Write in way that helps others understand and reproduce the issue. Screenshots can be very helpful, but when adding logging or other textual information, please keep the textual form. -Note that all information in the issue tracker is public. Do not include any confidential information there. +Note that all information in the issue tracker is public. *Do not include any confidential information there*. Be sure to add information about: - The applicable version(s) of RobotMBT (use `pip list` and check for `robotframework-mbt`) - Your Robot Framework version (use `pip list` and check for `robotframework`) - Your Python version (check using `python --version`) -- Your Operating System -- Used custom settings for RobotMBT (at library and test suite level) +- Your operating system +- Your custom settings for RobotMBT (at the library and test suite level) -Version information about Robot Framework, Python and the Operating System are also reported at the start of Robot's `output.xml` file. +Version information about Robot Framework, Python and the operating system are also reported at the start of Robot's `output.xml` file. -### Enhancement requests +### Enhancement requests When proposing an enhancement, a feature request, be clear about the use cases. Who will benefit from the enhancement and in what way? Describe the expected behaviour and use concrete examples to illustrate the intent. -## Code contributions -If you have fixed a defect or implemented an enhancement, you can contribute your changes via [GitHub's pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). This is not restricted to code, on the contrary, fixes and enhancements to documentation and tests alone are also very valuable! +## Code contributions +If you have fixed a defect or implemented an enhancement, you can contribute your changes via a [GitHub Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). This is not restricted to implementation code: on the contrary, fixes and enhancements to documentation and tests alone are also very valuable! -### first steps +### First steps - [Clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) and/or [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) the RobotMBT repo. If you are not a fan of command line tools, [GitHub Desktop](https://github.com/apps/desktop) can help you. - [Run the tests](#running-tests) to check your starting point. - Write new failing tests to cover your intended changes. - Implement your changes. +- Verify that your tests pass with your implementation. + ### Definition of Done +The Definition of Done for RobotMBT is when a pull request is merged. This is to ensure that pull requests are fully self-contained, and leave no open ends. -The Definition of Done is there to ensure that pull requests are fully self-contained, and leave no open ends. In other words: When the pull request is merged, it is 100% done. This keeps the main branch ready for release, at all times. +In other words: when the pull request is merged, it is 100% done. This keeps the main branch ready for release at all times. -That means that for each pull request you need to ensure: +This means that for each pull request you need to ensure that: -- No regression is introduced. -- New functionality is covered by tests. -- Code style follows the standard. +- [No regression](#non-regression-criteria) is introduced. +- New functionality is covered by [tests](#guidelines-for-writing-new-tests). +- [Code style](#code-style) follows the standard. - Documentation is up to date. -- The PR branch is 0 commits behind. +- The PR branch is 0 commits behind the main branch. -### Running tests +### Running tests Tests can be executed from the command line by running `python run_tests.py`. This will run all unit tests, followed by the Robot acceptance tests. Use `--help` for additional info. -### Non-regression criteria +### Non-regression criteria The criteria for proving non-regression are: - All automated regression tests pass - All supported Python, Robot Framework and OS versions still work (see `pyproject.toml` for supported versions). -- The [demos](https://github.com/JFoederer/robotframeworkMBT/tree/main/demo/Titanic) still work. +- The [demo](https://github.com/JFoederer/robotframeworkMBT/tree/main/demo/Titanic) still works. - Manual checks are executed to cover the automation's blind spots and subjective elements (e.g. some visual inspection on layout and assessing overall look and feel). -### Guidelines for writing new tests -For this project we are not maintaining separate requirements documentation. The user documentation explains the software's purpose and scope, the tests further specify its concrete behaviour. Keep this in mind when writing tests and pay extra attention to documenting your test cases. They are more than just bug catchers. If code exists due to a technical limitation rather than a requirement, be sure to document your design decision. +### Guidelines for writing new tests +For this project, we are not maintaining separate requirements documentation. The user documentation explains the software's purpose and scope, while tests further specify its concrete behaviour. Keep this in mind when writing tests and pay extra attention to documenting your test cases: they are more than just bug catchers. If code exists due to a technical limitation rather than a requirement, be sure to document your design decision. -Tests are located in the _atest_ and _utest_ folders, which stands for _acceptance test_ and _unit test_ respectively. The acceptance tests are Robot tests that cover user-visible behaviour using black box testing techniques. They typically do not cover all details, unless some Robot Framework interaction is involved. The unit tests do go in depth, which includes white box techniques to include coverage on the dark corners of the code. Choose the right type of test for what you are covering. +Tests are located in the `atest` and `utest` folders, which stands for _acceptance test_ and _unit test_ respectively. The acceptance tests are Robot tests that cover user-visible behaviour using black-box testing techniques. They typically do not cover all details, unless some Robot Framework interaction is involved. The unit tests go more in-depth, including white box techniques to cover the _dark corners_ of the code. Choose the right type of test for what you are covering. -A specific challenge for this project is that there is a lot of test case generation going on. Take care that variations in the generation process can not alter the intended coverage of a test and do not yield false positives. False positives being pass results without proof for passing, like checking 'all' results in an empty list. And lastly, keep the resulting number of test cases in a run deterministic, so that we keep comparable results. +A specific challenge for this project is that there is a lot of test case generation going on. Be wary that variations in the generation process can not alter the intended coverage of a test and do not yield false positives (passing results without proof for passing), such as checking `all` results in an empty list. Lastly: keep the resulting number of test cases in a run deterministic, so that we prevent flakey tests (tests that sometimes fail or pass). -### Coding style +### Code style Maintainability is the main driver for coding style. Always write your code with the mindset that you are writing it for someone else, and that this person's experience level is slightly below the average in the project. Code is written following the [PEP 8](https://peps.python.org/pep-0008) Style guide and [SOLID](https://en.wikipedia.org/wiki/SOLID) principles. + #### Formatting +Formatting follows the default rules of [autopep8](https://pypi.org/project/autopep8/) with the exception of the maximum line length (see https://github.com/JFoederer/robotframeworkMBT/tree/main/.github/workflows/autopep8.yml). Note however, that the extended line length is not an invite to always write long lines. -Formatting follows the default rules of [autopep8](https://pypi.org/project/autopep8/) with the exception that the maximum line length is set to 120. Note however, that the extended line length is not an invite to always write long lines. +Researchers have suggested that longer lines are better suited for cases when the information will likely be scanned, while shorter lines (45-75 characters) are appropriate when the information is meant to be read thoroughly [[ref.](https://www.academia.edu/6232736/The_influence_of_font_type_and_line_length_on_visual_search_and_information_retrieval_in_web_pages)]. Keep this in mind when writing code and documentation, taking the current indentation level into account. -Researchers have suggested [[ref.](https://www.academia.edu/6232736/The_influence_of_font_type_and_line_length_on_visual_search_and_information_retrieval_in_web_pages)] that longer lines are better suited for cases when the information will likely be scanned, while shorter lines (45-75 characters) are appropriate when the information is meant to be read thoroughly. Keep this in mind when writing code and documentation, taking the current indentation level into account. #### Docstrings, comments and logging +Docstrings are written using a black-box approach. One should not need to know the inside of a class or function in order to use it. -Docstrings are written using a black box approach. One should not need to know the inside of a class or function in order to use it. Use comments to annotate code for maintainers. Prevent trivial comments and use descriptive names to make your code self-explanatory. When documenting external interfaces, also check whether the user documentation requires an update. +- Use comments to annotate code for maintainers. +- Prevent trivial comments and use descriptive names to make your code self-explanatory. +- When documenting external interfaces, also check whether the user documentation requires an update. +- Log useful information that is runtime-dependent. + - Information that is useful after a passing test run is logged at info-level. + - Information that is useful for analysing failed tests is logged at debug-level. -Useful information that is runtime dependent should be logged. Information that is useful after a passing test run is logged at info-level. Information that is useful for analysing failed tests is logged at debug-level. Be careful not to make assumptions in what you log. Recheck log statements if your changes affect the context in which the code is run. Only report about what you know to be true. +- Be careful not to make assumptions in what you log: Recheck log statements if your changes affect the context in which the code is run, and only report about what you know to be true. From fa71a6d9185642492e96234d05d3b7a88c0158a0 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:57:20 +0100 Subject: [PATCH 17/18] Text clarifications CONTRIBUTING.md --- CONTRIBUTING.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index feaef627..e51012ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contribution guidelines RobotMBT -Welcome! Thank you for considering to contribute to this project. Your contribution already starts when you use this software and share your experiences with people around you. +Welcome! Thank you for considering to contribute to this project. Your contribution already starts when you use this software and share your experiences with people around you. ## Communication channels ### Slack @@ -9,8 +9,8 @@ If you want to share/ask/answer any questions, the [Robot Framework Slack](http: ### GitHub If you want to get involved on GitHub, you can so by submitting issues or offering code improvements. These guidelines will help you to find your way. These guidelines expect readers to have a basic knowledge about open source as well as why and how to contribute to an open source project. If you are new to these topics, please have a look at the generic [Open Source Guides](https://opensource.guide/) first. -## Code of Conduct (CoC) -If you want to be part of this community, then we expect you to respect our norms and values. These are in line with the [GitHub CoC](https://docs.github.com/en/site-policy/github-terms/github-community-code-of-conduct) and the [Slack CoC](https://docs.slack.dev/community-code-of-conduct/). In short, we expect you to: +## Code of Conduct +If you want to be part of this community, then we expect you to respect our norms and values. These are in line with the [GitHub Code of Conduct](https://docs.github.com/en/site-policy/github-terms/github-community-code-of-conduct) and the [Slack Code of Conduct](https://docs.slack.dev/community-code-of-conduct/). In short, we expect you to: - Be welcoming. - Be kind. @@ -18,7 +18,7 @@ If you want to be part of this community, then we expect you to respect our norm ## Submitting issues -Defects and enhancements are tracked in [GitHub Issues](https://github.com/JFoederer/robotframeworkMBT/issues). Please make sure the issue is caused by this project in particular before submitting an issue here. If you are unsure if something is worth submitting, you can first ask on [Slack](http://slack.robotframework.org/). Before submitting a new issue, it is always a good idea to check if something similar was already reported. If it is, please add your comments to the existing issue instead of creating a new one. Communication in issues on GitHub is done in English. +Defects and enhancements are tracked in [GitHub Issues](https://github.com/JFoederer/robotframeworkMBT/issues). Before submitting an issue here, please make sure the issue is caused by this project in particular. If you are unsure if something is worth submitting, you can first ask on [Slack](http://slack.robotframework.org/). Before submitting a new issue, it is always a good idea to check if something similar was already reported. If it is, please add your comments to the existing issue instead of creating a new one. Communication in issues on GitHub is done in English. Take notice that issues do not get resolved by themselves. Someone will need to spend time on the topic. Be prepared to wait, contribute yourself or arrange budget to hire someone for the job. @@ -87,7 +87,7 @@ For this project, we are not maintaining separate requirements documentation. Th Tests are located in the `atest` and `utest` folders, which stands for _acceptance test_ and _unit test_ respectively. The acceptance tests are Robot tests that cover user-visible behaviour using black-box testing techniques. They typically do not cover all details, unless some Robot Framework interaction is involved. The unit tests go more in-depth, including white box techniques to cover the _dark corners_ of the code. Choose the right type of test for what you are covering. -A specific challenge for this project is that there is a lot of test case generation going on. Be wary that variations in the generation process can not alter the intended coverage of a test and do not yield false positives (passing results without proof for passing), such as checking `all` results in an empty list. Lastly: keep the resulting number of test cases in a run deterministic, so that we prevent flakey tests (tests that sometimes fail or pass). +A specific challenge for this project is that there is a lot of test case generation going on. Be wary that variations in the generation process do not alter the intended coverage of a test and do not yield false positives (passing results without proof for passing), such as checking "_all_" results in an empty list. Lastly: keep the resulting total number of test cases in a run deterministic. This allows for a quick check that all test cases are still being generated. ### Code style @@ -101,13 +101,12 @@ Researchers have suggested that longer lines are better suited for cases when th #### Docstrings, comments and logging -Docstrings are written using a black-box approach. One should not need to know the inside of a class or function in order to use it. - -- Use comments to annotate code for maintainers. -- Prevent trivial comments and use descriptive names to make your code self-explanatory. +- Docstrings are written using a black-box approach. One should not need to know the inside of a class or function in order to use it. +- Use comments to annotate code for maintainers. +- Prevent trivial comments and use descriptive names to make your code self-explanatory. - When documenting external interfaces, also check whether the user documentation requires an update. -- Log useful information that is runtime-dependent. - - Information that is useful after a passing test run is logged at info-level. - - Information that is useful for analysing failed tests is logged at debug-level. +- Log useful information that is runtime-dependent. + - Information that is useful after a passing test run is logged at info-level. + - Information that is useful for analysing failed tests is logged at debug-level. - Be careful not to make assumptions in what you log: Recheck log statements if your changes affect the context in which the code is run, and only report about what you know to be true. From 5f67137a60692793107c51f7047c3b5e45617095 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Thu, 18 Dec 2025 22:12:40 +0100 Subject: [PATCH 18/18] fix md linting issues --- CONTRIBUTING.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e51012ce..8c7559fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,29 +1,33 @@ # Contribution guidelines RobotMBT -Welcome! Thank you for considering to contribute to this project. Your contribution already starts when you use this software and share your experiences with people around you. +Welcome! Thank you for considering to contribute to this project. If you haven't already, because your contribution already starts when you use this software and share your experiences with the people around you. These guidelines will help you to further connect with the online community. ## Communication channels + ### Slack -If you want to share/ask/answer any questions, the [Robot Framework Slack](http://slack.robotframework.org/) channels are good place to do so. + +If you want to ask or answer questions and participate in discussions, then the [Robot Framework Slack](http://slack.robotframework.org/) channels are a good place to do so. ### GitHub + If you want to get involved on GitHub, you can so by submitting issues or offering code improvements. These guidelines will help you to find your way. These guidelines expect readers to have a basic knowledge about open source as well as why and how to contribute to an open source project. If you are new to these topics, please have a look at the generic [Open Source Guides](https://opensource.guide/) first. ## Code of Conduct + If you want to be part of this community, then we expect you to respect our norms and values. These are in line with the [GitHub Code of Conduct](https://docs.github.com/en/site-policy/github-terms/github-community-code-of-conduct) and the [Slack Code of Conduct](https://docs.slack.dev/community-code-of-conduct/). In short, we expect you to: - Be welcoming. - Be kind. - Look out for each other. - ## Submitting issues + Defects and enhancements are tracked in [GitHub Issues](https://github.com/JFoederer/robotframeworkMBT/issues). Before submitting an issue here, please make sure the issue is caused by this project in particular. If you are unsure if something is worth submitting, you can first ask on [Slack](http://slack.robotframework.org/). Before submitting a new issue, it is always a good idea to check if something similar was already reported. If it is, please add your comments to the existing issue instead of creating a new one. Communication in issues on GitHub is done in English. Take notice that issues do not get resolved by themselves. Someone will need to spend time on the topic. Be prepared to wait, contribute yourself or arrange budget to hire someone for the job. - ### Reporting defects + When reporting a defect, be precise and concise in your description. Write in way that helps others understand and reproduce the issue. Screenshots can be very helpful, but when adding logging or other textual information, please keep the textual form. Note that all information in the issue tracker is public. *Do not include any confidential information there*. @@ -38,24 +42,24 @@ Be sure to add information about: Version information about Robot Framework, Python and the operating system are also reported at the start of Robot's `output.xml` file. - ### Enhancement requests -When proposing an enhancement, a feature request, be clear about the use cases. Who will benefit from the enhancement and in what way? Describe the expected behaviour and use concrete examples to illustrate the intent. +When proposing an enhancement, a feature request, be clear about the use cases. Who will benefit from the enhancement and in what way? Describe the expected behaviour and use concrete examples to illustrate the intent. ## Code contributions -If you have fixed a defect or implemented an enhancement, you can contribute your changes via a [GitHub Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). This is not restricted to implementation code: on the contrary, fixes and enhancements to documentation and tests alone are also very valuable! +If you have fixed a defect or implemented an enhancement, you can contribute your changes via a [GitHub Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). This is not restricted to implementation code: on the contrary, fixes and enhancements to documentation and tests alone are also very valuable! ### First steps + - [Clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) and/or [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) the RobotMBT repo. If you are not a fan of command line tools, [GitHub Desktop](https://github.com/apps/desktop) can help you. - [Run the tests](#running-tests) to check your starting point. - Write new failing tests to cover your intended changes. - Implement your changes. - Verify that your tests pass with your implementation. - ### Definition of Done + The Definition of Done for RobotMBT is when a pull request is merged. This is to ensure that pull requests are fully self-contained, and leave no open ends. In other words: when the pull request is merged, it is 100% done. This keeps the main branch ready for release at all times. @@ -68,12 +72,12 @@ This means that for each pull request you need to ensure that: - Documentation is up to date. - The PR branch is 0 commits behind the main branch. - ### Running tests -Tests can be executed from the command line by running `python run_tests.py`. This will run all unit tests, followed by the Robot acceptance tests. Use `--help` for additional info. +Tests can be executed from the command line by running `python run_tests.py`. This will run all unit tests, followed by the Robot acceptance tests. Use `--help` for additional info. ### Non-regression criteria + The criteria for proving non-regression are: - All automated regression tests pass @@ -81,27 +85,27 @@ The criteria for proving non-regression are: - The [demo](https://github.com/JFoederer/robotframeworkMBT/tree/main/demo/Titanic) still works. - Manual checks are executed to cover the automation's blind spots and subjective elements (e.g. some visual inspection on layout and assessing overall look and feel). - ### Guidelines for writing new tests + For this project, we are not maintaining separate requirements documentation. The user documentation explains the software's purpose and scope, while tests further specify its concrete behaviour. Keep this in mind when writing tests and pay extra attention to documenting your test cases: they are more than just bug catchers. If code exists due to a technical limitation rather than a requirement, be sure to document your design decision. Tests are located in the `atest` and `utest` folders, which stands for _acceptance test_ and _unit test_ respectively. The acceptance tests are Robot tests that cover user-visible behaviour using black-box testing techniques. They typically do not cover all details, unless some Robot Framework interaction is involved. The unit tests go more in-depth, including white box techniques to cover the _dark corners_ of the code. Choose the right type of test for what you are covering. A specific challenge for this project is that there is a lot of test case generation going on. Be wary that variations in the generation process do not alter the intended coverage of a test and do not yield false positives (passing results without proof for passing), such as checking "_all_" results in an empty list. Lastly: keep the resulting total number of test cases in a run deterministic. This allows for a quick check that all test cases are still being generated. - ### Code style -Maintainability is the main driver for coding style. Always write your code with the mindset that you are writing it for someone else, and that this person's experience level is slightly below the average in the project. Code is written following the [PEP 8](https://peps.python.org/pep-0008) Style guide and [SOLID](https://en.wikipedia.org/wiki/SOLID) principles. +Maintainability is the main driver for coding style. Always write your code with the mindset that you are writing it for someone else, and that this person's experience level is slightly below the average in the project. Code is written following the [PEP 8](https://peps.python.org/pep-0008) Style guide and [SOLID](https://en.wikipedia.org/wiki/SOLID) principles. #### Formatting + Formatting follows the default rules of [autopep8](https://pypi.org/project/autopep8/) with the exception of the maximum line length (see https://github.com/JFoederer/robotframeworkMBT/tree/main/.github/workflows/autopep8.yml). Note however, that the extended line length is not an invite to always write long lines. Researchers have suggested that longer lines are better suited for cases when the information will likely be scanned, while shorter lines (45-75 characters) are appropriate when the information is meant to be read thoroughly [[ref.](https://www.academia.edu/6232736/The_influence_of_font_type_and_line_length_on_visual_search_and_information_retrieval_in_web_pages)]. Keep this in mind when writing code and documentation, taking the current indentation level into account. - #### Docstrings, comments and logging -- Docstrings are written using a black-box approach. One should not need to know the inside of a class or function in order to use it. + +- Docstrings are written using a black-box approach. One should not need to know the inside of a class or function in order to use it. - Use comments to annotate code for maintainers. - Prevent trivial comments and use descriptive names to make your code self-explanatory. - When documenting external interfaces, also check whether the user documentation requires an update.