diff --git a/CHANGELOG.md b/CHANGELOG.md index dd43cf9..59254c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.6.0] - Oct 09, 2021 + +### Added + +* Add test / tester *IDs* (see https://github.com/qtc-de/tricot/tree/main/docs#selective-testing) +* Add *test groups* (see https://github.com/qtc-de/tricot/tree/main/docs#selective-testing) + +### Changed + +* Full help menu is now always displayed on argparse errors +* Small bugfixes and formatting changes + +### Removed + +* The `--tester`, `--exclude` and `--number` options were removed. + They are replaced by the `--ids`, `--groups`, `--exclude-ids` and + `--exclude-groups` options. + + ## [1.5.0] - Sep 11, 2021 ### Added diff --git a/bin/tricot b/bin/tricot index 2b72c71..3802b26 100644 --- a/bin/tricot +++ b/bin/tricot @@ -9,22 +9,39 @@ import docker import tricot import argparse +from tricot.constants import VERSION -parser = argparse.ArgumentParser(description='''tricot v1.5.0 - a trivial command tester that allows you to verify that certain - commands or executables behave as expected. It uses .yml files for test - definitions and can be used from the command line or as a python library.''', - formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=40)) + +class FullHelpParser(argparse.ArgumentParser): + ''' + Custom ArgumentParser class to show more helpful help messages per default. + Taken from: https://stackoverflow.com/a/4042861 + ''' + def error(self, message): + ''' + Show the whole help menu per default. + ''' + sys.stderr.write(f'error: {message}\n\n') + self.print_help() + sys.exit(2) + + +parser = FullHelpParser(description=f'''tricot v{VERSION} - a trivial command tester to verify that commands, + scripts or executables behave as expected. It uses .yml files for test + definitions and can be used from the command line or as a python library.''', + formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=60)) parser.add_argument('--debug', dest='debug', action='store_true', help='enable debug output') -parser.add_argument('--exclude', metavar='name', nargs='+', default=[], help='exclude the specified testers') +parser.add_argument('--exclude-ids', dest='eids', metavar='id', nargs='+', default=[], help='exclude the specified test / tester IDs') +parser.add_argument('--exclude-groups', dest='egroups', metavar='group', nargs='+', default=[], help='exclude the specified test groups') parser.add_argument('file', metavar='file', nargs='+', help='test definition (.yml file)') -parser.add_argument('--logfile', dest='log', type=argparse.FileType('w'), help='mirror output into a logfile') +parser.add_argument('--groups', metavar='group', nargs='+', default=[], help='only run the specified test groups') +parser.add_argument('--ids', metavar='id', nargs='+', default=[], help='only run the specified test / tester IDs') +parser.add_argument('--logfile', dest='log', metavar='file', type=argparse.FileType('w'), help='mirror output into a logfile') parser.add_argument('--load', dest='load', metavar='file', nargs='+', default=[], type=argparse.FileType('r'), help='custom validators, extractors and plugins') -parser.add_argument('--numbers', dest='numbers', metavar='int', type=int, nargs='+', default=[], help='only run the specified test numbers') -parser.add_argument('--positionals', dest='pos', metavar='pos', nargs='+', default=[], help='positional variables (accessible by $1, $2, ...)') +parser.add_argument('--positionals', dest='pos', metavar='var', nargs='+', default=[], help='positional variables (accessible by $1, $2, ...)') parser.add_argument('-q', '--quite', dest='quite', action='store_true', help='disable verbose output during tests') parser.add_argument('--template', dest='template', choices=['tester', 'plugin', 'validator', 'extractor'], help='write a template file') -parser.add_argument('--testers', dest='testers', metavar='name', nargs='+', default=[], help='only run the specified testers') parser.add_argument('--variables', dest='vars', metavar='vars', nargs='+', default=[], help='runtime variables') parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='enable verbose logging during tests') @@ -152,11 +169,16 @@ def main(): load(args.load) variables = prepare_variables(args) + groups = tricot.utils.parse_groups(args.groups) + egroups = tricot.utils.parse_groups(args.egroups) + + tricot.Logger.print_mixed_yellow('tricot', f'v{VERSION}', '- Starting tests...') + for yml_file in args.file: try: tester = tricot.Tester.from_file(yml_file, runtime_vars=variables) - tester.run(args.testers, args.numbers, args.exclude) + tester.run(set(args.ids), groups, set(args.eids), egroups) except tricot.ValidatorError as e: tricot.Logger.print_mixed_red('Caught', 'ValidatorError', 'while parsing test configuration.', e=True) @@ -249,6 +271,11 @@ def main(): tricot.Logger.print_with_indent_blue(str(e), e=True) sys.exit(tricot.constants.DOCKER_API) + except tricot.DuplicateIDError as e: + tricot.Logger.print_mixed_yellow('Caught', 'DuplicateIDError', 'while parsing test configuration.', e=True) + tricot.Logger.print_with_indent_blue(str(e), e=True) + sys.exit(tricot.constants.DUPLICATE_ID_ERROR) + except tricot.TricotRuntimeError as e: tricot.Logger.print_mixed_blue('Caught', 'unexpected Exception', 'while running the test command.', e=True) tricot.Logger.print_yellow('Original Error:', e=True) @@ -256,6 +283,13 @@ def main(): tricot.Logger.print_with_indent_blue(str(e.original), e=True) sys.exit(tricot.constants.RUNTIME_ERROR) + except yaml.scanner.ScannerError as e: + tricot.Logger.print_mixed_yellow('Caught', 'yaml.scanner.ScannerError', 'while parsing test configuration.', e=True) + tricot.Logger.print('Seems that there is a syntax error within your test configuration.', e=True) + tricot.Logger.increase_indent() + tricot.Logger.print_with_indent_blue(str(e), e=True) + sys.exit(tricot.constants.YAML_SYNTAX_ERROR) + except KeyboardInterrupt: tricot.Logger.reset_indent() tricot.Logger.print('') @@ -267,7 +301,7 @@ def main(): if args.debug: raise e - tricot.Logger.print_mixed_yellow('Caught', 'unexpected Exception', e=True) + tricot.Logger.print_mixed_yellow('Caught', 'unexpected Exception', 'while running tricot.', e=True) tricot.Logger.increase_indent() tricot.Logger.print_with_indent_blue(str(e), e=True) sys.exit(tricot.constants.UNKNOWN) diff --git a/docs/README.md b/docs/README.md index 5dd6ce3..9f2c443 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,8 +9,9 @@ on it's own. - [Extractor, Validator and Plugin List](#extractor-validator-and-plugin-list) - [Writing Custom Plugins](#writing-custom-plugins) - [Writing Custom Validators](#writing-custom-validators) -- [Writing Custom Extactors](#writing-custom-extractors) +- [Writing Custom Extractors](#writing-custom-extractors) - [Accessing Command Information from an Validator](#accessing-command-information-from-an-validator) +- [Selective Testing](#selective-testing) - [Environment Variables](#environment-variables) - [Runtime Variables](#runtime-variables) - [Nesting Variables](#nesting-variables) @@ -354,7 +355,7 @@ plugins: key2: 22 ``` -* ``param_type`` can be used to specify the python type that is expected for the toplevel argument of a +* ``param_type`` can be used to specify the python type that is expected for the top level argument of a plugin or validator. In the example test configuration above, the ``param_type`` values should be set like this: * ``example_one``: ``param_type = str`` @@ -372,6 +373,108 @@ The parameter validation described above is very basic and has obviously limitat want a parameter validation that is easier to use and has an arbitrary recursion depth. +### Selective Testing + +---- + +A common testing scenario is that you just changed a portion of a program and only want to run tests for the affected +component. *tricot* supports this *selective testing* approach by using *IDs* and *test groups*. + +*IDs* are exactly what the name suggests, a unique identifier for each test / tester. You can assign them by using the +`id` key within of test definitions. *IDs* are ordinary strings and can contain any characters. If a test / tester +is defined without an *ID*, it's *title* (Test) or *name* (Tester) attribute is used as an *ID*. However, in this case +*tricot* does not check for duplicate *IDs* and you may end up with multiple tests / testers having the same *ID*. + +```yaml +tester: + id: '001' + name: Basic Usage + description: |- + Demonstrate the basic usage of tricot + +tests: + + - id: '001-1' + title: Test passwd File + description: |- + ... +``` + +To launch tests based on an *ID* you can use the command line switches ``--ids`` and ``--exclude-ids``. When using +``--ids``, *tricot* only runs the tests / testers that match the specified *IDs*. If the *ID* belongs to a tester, +all nested testers and tests are run, independent of their *ID*. The ``--exclude-ids`` can be used to exclude certain +test / tester *IDs* from a test. Notice that ``--exclude-ids`` triggers before ``--ids``, so if you specify the same +*ID* for both command line options, it is not run. On the other hand, this allows you to exclude nested test / tester +*IDs* that are contained within a tester specified with the ``--ids`` option. + +*Test groups* can be used to group tests / testers together. Each test / tester definition can contain a ``groups`` key, +which is a list within the *YAML* configuration. The contained items are the groups for the corresponding test / tester. +*Test groups* are inherited from parent testers, but stacked instead of being merged together. E.g. when a parent tester +is in the group `io` and the child tester in the group `logging`, the resulting group for the child tester is `io,logging`. + +```yaml +# [io.yml] +tester: + group: + - io + name: Test IO Modules + description: |- + Test IO Modules for the Software + +testers: + - ./nested.yml + + +# [nested.yml] +tester: + group: + - logging + name: Test IO Modules - logging + description: |- + Test IO Modules for the Software - logging + +tests: + + title: Test Error Log + description: |- + ... +``` + +As for *IDs*, you can use the ``--groups`` and ``--exclude-groups`` command line options to run selective tests on *test +groups*. However, group specifications on the command line support some special syntax. The easiest case is that you just +want to run a single test group. E.g. taking the example above, to run the `io,logging` test you could use: + +```console +tricot -v example.yml --groups io,logging +``` + +This is straight forwards, but it can get annoying if you defined `logging` groups in other parent testers than `io`. +To make runs of a single test group, that is contained within different parent test groups easier, it is possible +to specify wildcards. + +* `*` can be used to match an arbitrary group +* `**` can be used to match an arbitrary number of arbitrary groups + +Running all tests from the `logging` group, independent of the parent test groups can be done like this: + +```console +tricot -v example.yml --groups **,logging +``` + +In addition to wildcards, *tricot* also support *brace expressions*. These can be used to constructed *or-like* test +groups. E.g. to run the `logging` module from the `io` and `networking` parent test groups, you could use: + +```console +tricot -v example.yml --groups {io,networking},logging +``` + +Wildcards and *brace expressions* can also be used together within a group specifications. Whereas *brace expressions* can +be placed at any location of a group specification, wildcards are not allowed within the last comma separated value. Also +for group matching, the `--exclude-groups` option triggers before the `--groups` option. + +Both, *IDs* and *test groups* are case sensitive. + + ### Environment Variables ---- diff --git a/resources/bash_completion.d/tricot b/resources/bash_completion.d/tricot index fdf9ab2..b616992 100644 --- a/resources/bash_completion.d/tricot +++ b/resources/bash_completion.d/tricot @@ -12,7 +12,7 @@ function _tricot() { COMPREPLY=() # No completion - if _comp_contains "--exclude --numbers --testers --positionals --variables" $prev; then + if _comp_contains "--exclude-ids --exclude-groups --groups --ids --positionals --variables" $prev; then return 0 # Complete template modes @@ -28,14 +28,15 @@ function _tricot() { elif [[ "$cur" == -* ]]; then opts="--help" opts="$opts --debug" - opts="$opts --exclude" + opts="$opts --exclude-ids" + opts="$opts --exclude-groups" + opts="$opts --groups" + opts="$opts --ids" opts="$opts --logfile" opts="$opts --load" - opts="$opts --numbers" opts="$opts --positionals" opts="$opts --quite" opts="$opts --template" - opts="$opts --testers" opts="$opts --variables" opts="$opts --verbose" diff --git a/setup.py b/setup.py index d91cb95..17e83c0 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ url='https://github.com/qtc-de/tricot', name='tricot', author='Tobias Neitzel (@qtc_de)', - version='1.5.0', + version='1.6.0', author_email='', description='Trivial Command Testser', diff --git a/tests/pytest/Misc/group_matching_test.py b/tests/pytest/Misc/group_matching_test.py new file mode 100644 index 0000000..21ab50a --- /dev/null +++ b/tests/pytest/Misc/group_matching_test.py @@ -0,0 +1,51 @@ +#!/usr/bin/python3 + +import tricot +import pytest + + +config_list = [['this,is,a,simple,group,def']] +config_list.append(['now,lets,try,{conditional,orlike},group,def']) +config_list.append(['{this,it},works,also,{with,using},more,than,one']) +config_list.append(['multiple,lists', 'can,also,be,defined']) +config_list.append(['{this,it},works', 'also,{with,using},orlike']) +config_list.append(['lets,add,*,some,wildcards']) +config_list.append(['lets,add,**,some,wildcards']) +config_list.append(['lets,*,**,some,wildcards']) +config_list.append(['**,some,wildcards']) + +match_list = [[([['this', 'is', 'a', 'simple', 'group', 'def']], True), ([['nope']], False)]] +match_list.append([([['now', 'lets', 'try', 'conditional', 'group', 'def']], True), + ([['now', 'lets', 'try', 'orlike', 'group', 'def']], True), + ([['now', 'lets', 'tri', 'orlike', 'group', 'def']], False)]) +match_list.append([([['this', 'works', 'also', 'with', 'more', 'than', 'one']], True), + ([['this', 'works', 'also', 'using', 'more', 'than', 'one']], True), + ([['it', 'works', 'also', 'with', 'more', 'than', 'one']], True), + ([['it', 'works', 'also', 'using', 'more', 'than', 'one']], True), + ([['ot', 'works', 'also', 'using', 'more', 'than', 'one']], False)]) +match_list.append([([['aaaa', 'bbbb', 'cccc'], ['dddd', 'eeee']], False), + ([['aaaa', 'bbbb', 'cccc'], ['multiple', 'lists']], True), + ([['can', 'also', 'be', 'defined'], ['aaaa', 'bbbb']], True)]) +match_list.append([([['aaaa', 'bbbb', 'cccc'], ['dddd', 'eeee']], False), + ([['this', 'works'], ['multiple', 'lists']], True), + ([['also', 'using', 'orlike'], ['aaaa', 'bbbb']], True)]) +match_list.append([([['lets', 'add', 'hi :)', 'some', 'wildcards'], ['dddd', 'eeee']], True), + ([['lets', 'hi :)', 'some', 'wildcards'], ['dddd', 'eeee']], False)]) +match_list.append([([['lets', 'add', 'hi :)', 'hi :D', 'some', 'wildcards'], ['dddd', 'eeee']], True), + ([['lets', 'hi :)', 'some', 'wildcards'], ['dddd', 'eeee']], False)]) +match_list.append([([['lets', 'add', 'hi :)', 'hi :D', 'some', 'wildcards'], ['dddd', 'eeee']], True), + ([['lets', 'hi :)', 'some', 'wildcards'], ['dddd', 'eeee']], True)]) +match_list.append([([['lets', 'add', 'hi :)', 'hi :D', 'some', 'wildcards'], ['dddd', 'eeee']], True), + ([['lets', 'hi :)', 'some', 'wildcards'], ['dddd', 'eeee']], True), + ([['aaaaa', 'hi :)', 'some', 'wildcards'], ['dddd', 'eeee']], True)]) + + +@pytest.mark.parametrize('config, match', zip(config_list, match_list)) +def test_group_matching(config, match): + ''' + Check whether group matches are matching as expected + ''' + for tupl in match: + + groups = tricot.utils.parse_groups(config) + assert tricot.utils.groups_contain(groups, tupl[0]) == tupl[1] diff --git a/tests/pytest/Misc/group_parsing_test.py b/tests/pytest/Misc/group_parsing_test.py new file mode 100644 index 0000000..45539cd --- /dev/null +++ b/tests/pytest/Misc/group_parsing_test.py @@ -0,0 +1,35 @@ +#!/usr/bin/python3 + +import tricot +import pytest + + +config_list = [['this,is,a,simple,group,def']] +config_list.append(['now,lets,try,{conditional,orlike},group,def']) +config_list.append(['{this,it},works,also,{with,using},more,than,one']) +config_list.append(['multiple,lists', 'can,also,be,defined']) +config_list.append(['{this,it},works', 'also,{with,using},orlike']) + +result_list = [[['this', 'is', 'a', 'simple', 'group', 'def']]] +result_list.append([['now', 'lets', 'try', 'conditional', 'group', 'def'], + ['now', 'lets', 'try', 'orlike', 'group', 'def']]) +result_list.append([['this', 'works', 'also', 'with', 'more', 'than', 'one'], + ['this', 'works', 'also', 'using', 'more', 'than', 'one'], + ['it', 'works', 'also', 'with', 'more', 'than', 'one'], + ['it', 'works', 'also', 'using', 'more', 'than', 'one']]) +result_list.append([['multiple', 'lists'], ['can', 'also', 'be', 'defined']]) +result_list.append([['this', 'works'], ['also', 'with', 'orlike'], + ['this', 'works'], ['also', 'using', 'orlike'], + ['it', 'works'], ['also', 'with', 'orlike'], + ['it', 'works'], ['also', 'using', 'orlike']]) + + +@pytest.mark.parametrize('config, results', zip(config_list, result_list)) +def test_group_parsing(config, results): + ''' + Check whether group specifications are parsed correctly. + ''' + groups = tricot.utils.parse_groups(config) + + for group in groups: + assert group in results diff --git a/tests/tricot/test-cases/extra/Groups.yml b/tests/tricot/test-cases/extra/Groups.yml new file mode 100644 index 0000000..fe9dfd5 --- /dev/null +++ b/tests/tricot/test-cases/extra/Groups.yml @@ -0,0 +1,9 @@ +tester: + name: groups + title: Group Tests + description: |- + "Performs different checks on tricot's test group feature" + +testers: + - Groups/nested_one.yml + - Groups/nested_two.yml diff --git a/tests/tricot/test-cases/extra/Groups/nested_four.yml b/tests/tricot/test-cases/extra/Groups/nested_four.yml new file mode 100644 index 0000000..2387d2f --- /dev/null +++ b/tests/tricot/test-cases/extra/Groups/nested_four.yml @@ -0,0 +1,36 @@ +tester: + name: group_nested_four + title: Nested Group Test Four + description: |- + "Performs different checks on tricot's test group feature" + + groups: + - group_four + +tests: + - title: Test Group Four - One + groups: + - one + - first + description: > + 'First test in test group four' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + + - title: Test Group Four - Two + groups: + - two + description: > + 'First test in test group four' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' diff --git a/tests/tricot/test-cases/extra/Groups/nested_one.yml b/tests/tricot/test-cases/extra/Groups/nested_one.yml new file mode 100644 index 0000000..611903f --- /dev/null +++ b/tests/tricot/test-cases/extra/Groups/nested_one.yml @@ -0,0 +1,40 @@ +tester: + name: group_nested_one + title: Nested Group Test One + description: |- + "Performs different checks on tricot's test group feature" + + groups: + - group_one + - merge + +tests: + - title: Test Group One - One + groups: + - one + - first + description: > + 'First test in test group one' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + + - title: Test Group One - Two + groups: + - two + description: > + 'First test in test group one' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + +testers: + - ./nested_three.yml diff --git a/tests/tricot/test-cases/extra/Groups/nested_three.yml b/tests/tricot/test-cases/extra/Groups/nested_three.yml new file mode 100644 index 0000000..4b1e048 --- /dev/null +++ b/tests/tricot/test-cases/extra/Groups/nested_three.yml @@ -0,0 +1,36 @@ +tester: + name: group_nested_three + title: Nested Group Test Three + description: |- + "Performs different checks on tricot's test group feature" + + groups: + - group_three + +tests: + - title: Test Group Three - One + groups: + - one + - first + description: > + 'First test in test group three' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + + - title: Test Group Three - Two + groups: + - two + description: > + 'First test in test group three' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' diff --git a/tests/tricot/test-cases/extra/Groups/nested_two.yml b/tests/tricot/test-cases/extra/Groups/nested_two.yml new file mode 100644 index 0000000..1c3143d --- /dev/null +++ b/tests/tricot/test-cases/extra/Groups/nested_two.yml @@ -0,0 +1,40 @@ +tester: + name: group_nested_two + title: Nested Group Test Two + description: |- + "Performs different checks on tricot's test group feature" + + groups: + - group_two + - merge + +tests: + - title: Test Group Two - One + groups: + - one + - first + description: > + 'First test in test group two' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + + - title: Test Group Two - Two + groups: + - two + description: > + 'First test in test group two' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + +testers: + - ./nested_four.yml diff --git a/tests/tricot/test-cases/extra/IDs.yml b/tests/tricot/test-cases/extra/IDs.yml new file mode 100644 index 0000000..8b1dc18 --- /dev/null +++ b/tests/tricot/test-cases/extra/IDs.yml @@ -0,0 +1,9 @@ +tester: + name: ids + title: ID Tests + description: |- + "Performs different checks on tricot's ID feature" + +testers: + - IDs/nested_one.yml + - IDs/nested_two.yml diff --git a/tests/tricot/test-cases/extra/IDs/nested_four.yml b/tests/tricot/test-cases/extra/IDs/nested_four.yml new file mode 100644 index 0000000..c2c1f09 --- /dev/null +++ b/tests/tricot/test-cases/extra/IDs/nested_four.yml @@ -0,0 +1,39 @@ +tester: + name: id_nested_four + title: Nested ID Test Four + description: |- + "Performs different checks on tricot's ID feature" + + id: group_four + groups: + - group_four + +tests: + - title: Test Group Four - One + id: group_four_one + groups: + - one + - first + description: > + 'First test in test group four' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + + - title: Test Group Four - Two + id: group_four_two + groups: + - two + description: > + 'Second test in test group four' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' diff --git a/tests/tricot/test-cases/extra/IDs/nested_one.yml b/tests/tricot/test-cases/extra/IDs/nested_one.yml new file mode 100644 index 0000000..c456d53 --- /dev/null +++ b/tests/tricot/test-cases/extra/IDs/nested_one.yml @@ -0,0 +1,42 @@ +tester: + name: id_nested_one + title: Nested ID Test One + description: |- + "Performs different checks on tricot's id feature" + + id: group_one + groups: + - group_one + +tests: + - title: Test Group One - One + id: group_one_one + groups: + - one + - first + description: > + 'First test in test group one' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + + - title: Test Group One - Two + id: group_one_two + groups: + - two + description: > + 'Second test in test group one' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + +testers: + - ./nested_three.yml diff --git a/tests/tricot/test-cases/extra/IDs/nested_three.yml b/tests/tricot/test-cases/extra/IDs/nested_three.yml new file mode 100644 index 0000000..eae503b --- /dev/null +++ b/tests/tricot/test-cases/extra/IDs/nested_three.yml @@ -0,0 +1,39 @@ +tester: + name: id_nested_three + title: Nested ID Test Three + description: |- + "Performs different checks on tricot's ID feature" + + id: group_three + groups: + - group_three + +tests: + - title: Test Group Three - One + id: group_three_one + groups: + - one + - first + description: > + 'First test in test group three' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + + - title: Test Group Three - Two + id: group_three_two + groups: + - two + description: > + 'Second test in test group three' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' diff --git a/tests/tricot/test-cases/extra/IDs/nested_two.yml b/tests/tricot/test-cases/extra/IDs/nested_two.yml new file mode 100644 index 0000000..3b24716 --- /dev/null +++ b/tests/tricot/test-cases/extra/IDs/nested_two.yml @@ -0,0 +1,42 @@ +tester: + name: id_nested_two + title: Nested ID Test Two + description: |- + "Performs different checks on tricot's id feature" + + id: group_two + groups: + - group_two + +tests: + - title: Test Group Two - One + id: group_two_one + groups: + - one + - first + description: > + 'First test in test group two' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + + - title: Test Group Two - Two + id: group_two_two + groups: + - two + description: > + 'Second test in test group two' + command: + - echo + - 'Hi :D' + validators: + - contains: + values: + - 'Hi :D' + +testers: + - ./nested_four.yml diff --git a/tests/tricot/tricot-tests/extra-tests.yml b/tests/tricot/tricot-tests/extra-tests.yml index 64c8346..db1219c 100644 --- a/tests/tricot/tricot-tests/extra-tests.yml +++ b/tests/tricot/tricot-tests/extra-tests.yml @@ -5,6 +5,9 @@ tester: 'Perfors some additional tests on tricot, not related to plugins or validators.' + id: 04 + groups: + - extra tests: - title: Docker @@ -272,3 +275,555 @@ tests: invert: - Fail... success - Success... failed + + + - title: Groups All + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + + validators: + - error: False + - contains: + values: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + + + - title: Groups One + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --groups + - group_one + validators: + - error: False + - contains: + values: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + invert: + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + + + - title: Groups Two + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --groups + - group_two + validators: + - error: False + - contains: + values: + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + + + - title: Groups One - One + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --groups + - group_one,one + validators: + - error: False + - contains: + values: + - Test Group One - One... success + invert: + - Test Group One - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + + + - title: Groups One / Two - One + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --groups + - '{group_one,group_two},one' + validators: + - error: False + - contains: + values: + - Test Group One - One... success + - Test Group Two - One... success + invert: + - Test Group One - Two... success + - Test Group Two - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + + + - title: Groups One / Two - One (explicit) + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --groups + - 'group_one,one' + - 'group_two,one' + validators: + - error: False + - contains: + values: + - Test Group One - One... success + - Test Group Two - One... success + invert: + - Test Group One - Two... success + - Test Group Two - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + + + - title: Groups One / Two * - Two + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --groups + - '{group_one,group_two},*,two' + validators: + - error: False + - contains: + values: + - Test Group Three - Two... success + - Test Group Four - Two... success + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Three - One... success + - Test Group Four - One... success + + + - title: Groups ** - One + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --groups + - '**,one' + validators: + - error: False + - contains: + values: + - Test Group One - One... success + - Test Group Two - One... success + - Test Group Three - One... success + - Test Group Four - One... success + invert: + - Test Group One - Two... success + - Test Group Two - Two... success + - Test Group Three - Two... success + - Test Group Four - Two... success + + + - title: Exclude group_one + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --exclude-groups + - group_one + + validators: + - error: False + - contains: + values: + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + + + - title: Exclude ** - One + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --exclude-groups + - '**,one' + + validators: + - error: False + - contains: + values: + - Test Group One - Two... success + - Test Group Two - Two... success + - Test Group Three - Two... success + - Test Group Four - Two... success + invert: + - Test Group One - One... success + - Test Group Two - One... success + - Test Group Three - One... success + - Test Group Four - One... success + + + - title: Exclude vs Include + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --exclude-groups + - 'group_one' + - --groups + - 'group_one' + + validators: + - error: False + - contains: + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + + + - title: Exclude vs Include 2 + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --exclude-groups + - 'group_one' + - --groups + - '**,one' + + validators: + - error: False + - contains: + values: + - Test Group Two - One... success + - Test Group Four - One... success + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Two - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Four - Two... success + + + - title: Exclude vs Include 3 + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --exclude-groups + - 'group_one,one' + - --groups + - 'group_one' + + validators: + - error: False + - contains: + values: + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + invert: + - Test Group One - One... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + + + - title: Group Merge + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --groups + - 'merge' + + validators: + - error: False + - contains: + values: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + + + - title: Group Merge Exclude + description: |- + "Test tricot's test group feature" + + command: + - tricot + - ${EXTRA}/Groups.yml + - --exclude-groups + - 'group_one' + - --groups + - 'merge' + + validators: + - error: False + - contains: + values: + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + + + - title: ID Test - One Two + description: |- + "Test tricot's ID feature" + + command: + - tricot + - ${EXTRA}/IDs.yml + - --ids + - 'group_one' + - 'group_two' + + validators: + - error: False + - contains: + values: + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + + + - title: ID Test - One + description: |- + "Test tricot's ID feature" + + command: + - tricot + - ${EXTRA}/IDs.yml + - --ids + - 'group_one' + + validators: + - error: False + - contains: + values: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + invert: + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success + - Test Group Four - Two... success + + + - title: ID Test - FourOne + description: |- + "Test tricot's ID feature" + + command: + - tricot + - ${EXTRA}/IDs.yml + - --ids + - 'group_four_one' + + validators: + - error: False + - contains: + values: + - Test Group Four - One... success + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - Two... success + + + - title: Exclude ID + description: |- + "Test tricot's ID feature" + + command: + - tricot + - ${EXTRA}/IDs.yml + - --exclude-ids + - 'group_four_one' + + validators: + - error: False + - contains: + values: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - Two... success + invert: + - Test Group Four - One... success + + + - title: Exclude vs Include (ID) + description: |- + "Test tricot's ID feature" + + command: + - tricot + - ${EXTRA}/IDs.yml + - --exclude-ids + - 'group_four_one' + - --ids + - 'group_four_one' + + validators: + - error: False + - contains: + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - Two... success + - Test Group Four - One... success + + + - title: Exclude vs Include 2 (ID) + description: |- + "Test tricot's ID feature" + + command: + - tricot + - ${EXTRA}/IDs.yml + - --exclude-ids + - 'group_four_one' + - --ids + - 'group_four' + + validators: + - error: False + - contains: + values: + - Test Group Four - Two... success + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success + + + - title: Exclude vs Include (Groups vs ID) + description: |- + "Test tricot's ID feature" + + command: + - tricot + - ${EXTRA}/IDs.yml + - --exclude-ids + - 'group_four_one' + - --groups + - 'group_two,group_four' + + validators: + - error: False + - contains: + values: + - Test Group Four - Two... success + invert: + - Test Group One - One... success + - Test Group One - Two... success + - Test Group Three - One... success + - Test Group Three - Two... success + - Test Group Two - One... success + - Test Group Two - Two... success + - Test Group Four - One... success diff --git a/tests/tricot/tricot-tests/extractor-tests.yml b/tests/tricot/tricot-tests/extractor-tests.yml index 0c21d4b..2359f5f 100644 --- a/tests/tricot/tricot-tests/extractor-tests.yml +++ b/tests/tricot/tricot-tests/extractor-tests.yml @@ -4,6 +4,10 @@ tester: description: |- "Perform tests on all tricot extractors" + id: 03 + groups: + - extractor + tests: - title: RegexExtractor diff --git a/tests/tricot/tricot-tests/plugin-tests.yml b/tests/tricot/tricot-tests/plugin-tests.yml index 23ca3b7..d694f30 100644 --- a/tests/tricot/tricot-tests/plugin-tests.yml +++ b/tests/tricot/tricot-tests/plugin-tests.yml @@ -4,6 +4,9 @@ tester: description: |- "Performs tests on all tricot plugins" + id: 02 + groups: + - plugin tests: - title: CleanupPlugin diff --git a/tests/tricot/tricot-tests/validator-tests.yml b/tests/tricot/tricot-tests/validator-tests.yml index de7e458..fed5557 100644 --- a/tests/tricot/tricot-tests/validator-tests.yml +++ b/tests/tricot/tricot-tests/validator-tests.yml @@ -4,6 +4,9 @@ tester: description: |- "Performs tests on all tricot validators" + id: 01 + groups: + - validator tests: - title: ContainsValidator diff --git a/tricot/constants.py b/tricot/constants.py index 2ed4d0d..9c68b71 100644 --- a/tricot/constants.py +++ b/tricot/constants.py @@ -17,5 +17,8 @@ CONDITION_FORMAT_ERROR = 16 EXTRACT_EXCEPTION = 17 EXTRACTOR_ERROR = 18 +DUPLICATE_ID_ERROR = 19 +YAML_SYNTAX_ERROR = 20 LAST_ERROR = 0 +VERSION = '1.6.0' diff --git a/tricot/logging.py b/tricot/logging.py index 4c741d6..79d43e6 100644 --- a/tricot/logging.py +++ b/tricot/logging.py @@ -59,6 +59,12 @@ def print_plain_red(string: str, end: str = None) -> None: ''' cprint(string, color='red', end=end) + def print_plain_blue(string: str, end: str = None) -> None: + ''' + Print without indent or prefix, text in blue. + ''' + cprint(string, color='blue', end=end) + def print_yellow(string: str, e: bool = False, end: str = None) -> None: ''' Print with prefix and indent, text in yellow. diff --git a/tricot/tricot.py b/tricot/tricot.py index cf7a30d..a1d6daf 100644 --- a/tricot/tricot.py +++ b/tricot/tricot.py @@ -16,6 +16,15 @@ from tricot.condition import Condition +assigned_ids = set() + + +class DuplicateIDError(Exception): + ''' + Custom exception that is raised if two testers/tests use the same ID. + ''' + + class TricotException(Exception): ''' Custom exception class for general purpose and tricot related exceptions. @@ -107,11 +116,11 @@ class Test: evaluated by executing their 'run' method. ''' expected_keys = ['title', 'description', 'command', 'arguments', 'validators', 'timeout', 'env', 'conditions', - 'logfile', 'shell', 'extractors'] + 'logfile', 'shell', 'extractors', 'id', 'groups'] def __init__(self, path: Path, title: str, error_mode: str, variables: dict[str, Any], command: Command, timeout: int, validators: list[Validator], extractors: list[Extractor], env: dict, conditions: dict, - conditionals: set[Condition]) -> None: + conditionals: set[Condition], test_id: str, test_groups: list[list[str]]) -> None: ''' Initializer for a Test object. @@ -127,6 +136,8 @@ def __init__(self, path: Path, title: str, error_mode: str, variables: dict[str, env Environment variables conditions Conditions for running the current test conditionals Conditionals defined by upper testers + test_id Identifikation number of the test + test_groups Test groups that the test belongs to Returns: None @@ -143,6 +154,19 @@ def __init__(self, path: Path, title: str, error_mode: str, variables: dict[str, self.conditions = conditions self.conditionals = conditionals + if test_id is None: + self.id = self.title + + else: + self.id = str(test_id) + + if self.id in assigned_ids: + raise DuplicateIDError(f"ID '{self.id}' was assigned twice.") + + assigned_ids.add(self.id) + + self.groups = test_groups + self.logfile = None self.success_string = 'success' self.failure_string = 'failed' @@ -236,7 +260,8 @@ def apply_variables(val: Union(str, list), variables: dict[str, Any], k: str = ' return val def from_dict(path: Path, input_dict: dict, variables: dict[str, Any] = {}, error_mode: str = 'continue', - environment: dict = {}, conditionals: set[Condition] = set(), output_conf: dict = {}) -> Test: + environment: dict = {}, conditionals: set[Condition] = set(), output_conf: dict = {}, + parent_groups: list[list[str]] = list()) -> Test: ''' Creates a Test object from a dictionary. The dictionary is expected to be the content read in of a .yml file and needs all keys that are required for a test (validators, @@ -250,6 +275,7 @@ def from_dict(path: Path, input_dict: dict, variables: dict[str, Any] = {}, erro environment Environment variables conditionals Conditionals for running the test output_conf Output configuration inherited by the tester + parent_groups Test groups inherited from the parent tester Returns: Test Newly generated Test object @@ -259,6 +285,7 @@ def from_dict(path: Path, input_dict: dict, variables: dict[str, Any] = {}, erro var = tricot.utils.merge(variables, j.get('variables', {}), 'variables', path) validators = Validator.from_list(path, j['validators'], var) extractors = Extractor.from_list(path, j.get('extractors', []), var) + groups = tricot.utils.merge_groups(parent_groups, list(map(lambda x: str(x), j.get('groups', [])))) e_mode = j.get('error_mode') or error_mode env = tricot.utils.merge_environment(j.get('env'), environment, path) @@ -284,7 +311,7 @@ def from_dict(path: Path, input_dict: dict, variables: dict[str, Any] = {}, erro tricot.utils.check_keys(Test.expected_keys, input_dict) test = Test(path, j['title'], e_mode, var, command, j.get('timeout'), validators, extractors, env, - conditions, conditionals) + conditions, conditionals, j.get('id'), groups) test.set_logfile(j.get('logfile')) test.set_output(tricot.utils.merge(output_conf, j.get('output', {}), 'output', path)) @@ -298,7 +325,8 @@ def from_dict(path: Path, input_dict: dict, variables: dict[str, Any] = {}, erro raise TestKeyError(None, path, str(e)) def from_list(path: Path, input_list: list, variables: dict[str, Any] = {}, error_mode: str = 'continue', - env: dict = {}, conditionals: set[Condition] = set(), output_conf: dict = {}) -> list[Test]: + env: dict = {}, conditionals: set[Condition] = set(), output_conf: dict = {}, + parent_groups: list[list[str]] = list()) -> list[Test]: ''' Within .yml files, Tests are specified in form of a list. This function takes such a list, that contains each single test definition as another dictionary (like it is created when @@ -312,6 +340,7 @@ def from_list(path: Path, input_list: list, variables: dict[str, Any] = {}, erro env Environment variables conditionals Conditionals specified by the upper tester output_conf Output configuration inherited by the tester + parent_groups Test groups inherited from the parent tester Returns list[Test] List of Test objects created from the .yml input @@ -324,7 +353,8 @@ def from_list(path: Path, input_list: list, variables: dict[str, Any] = {}, erro for ctr in range(len(input_list)): try: - test = Test.from_dict(path, input_list[ctr], variables, error_mode, env, conditionals, output_conf) + test = Test.from_dict(path, input_list[ctr], variables, error_mode, env, conditionals, + output_conf, parent_groups) tests.append(test) except TestKeyError as e: @@ -348,7 +378,12 @@ def run(self, prefix: str = '-', hotplug_variables: dict[str, Any] = None) -> No None ''' Logger.add_logfile(self.logfile) - Logger.print_blue(f'{prefix} {self.title}...', end=' ', flush=True) + + if self.id and self.id != self.title: + Logger.print_blue(f'{prefix} [{self.id}] {self.title}...', end=' ', flush=True) + else: + Logger.print_blue(f'{prefix} {self.title}...', end=' ', flush=True) + success = True if not Condition.check_conditions(self.conditions, self.conditionals): @@ -421,6 +456,25 @@ def run(self, prefix: str = '-', hotplug_variables: dict[str, Any] = None) -> No hotplug_variables['$prev'] = self.command Logger.remove_logfile(self.logfile) + def skip_test(self, exclude: set[str], exclude_groups: list[list[str]]) -> bool: + ''' + Checks whether the current test is contained within the exclude lists. + + Parameters: + exclude Set of Test / Tester IDs to exclude + exclude_groups List of group sets to exclude + + Returns: + bool + ''' + if exclude and self.id in exclude: + return True + + elif exclude_groups and tricot.utils.groups_contain(exclude_groups, self.groups): + return True + + return False + class Tester: ''' @@ -433,7 +487,7 @@ class Tester: def __init__(self, path: Path, name: str, title: str, variables: dict[str, Any], tests: list[Test], testers: list[Tester], containers: list[TricotContainer], plugins: list[Plugin], conditions: dict, conditionals: set[Condition], - error_mode: str) -> None: + error_mode: str, tester_id: str, test_groups: list[list[str]]) -> None: ''' Initializes a new Tester object. @@ -449,6 +503,11 @@ def __init__(self, path: Path, name: str, title: str, variables: dict[str, Any], conditions Conditions for running the current tester conditionals Conditionals defined by the tester or upper testers error_mode Decides what to do if a plugin fails (break|continue) + tester_id Unique identifikation number of the tester + test_groups Test groups that the tester belongs to + + Returns: + None ''' self.name = name self.title = title or name @@ -461,11 +520,26 @@ def __init__(self, path: Path, name: str, title: str, variables: dict[str, Any], self.conditionals = conditionals self.error_mode = error_mode + if tester_id is None: + self.id = self.name + + else: + self.id = str(tester_id) + + if self.id in assigned_ids: + raise DuplicateIDError(f"ID '{self.id}' was assigned twice.") + + assigned_ids.add(self.id) + + self.groups = test_groups + self.logfile = None + self.runall = False def from_dict(input_dict: dict, initial_vars: dict[str, Any] = dict(), path: Path = None, e_mode: str = None, environment: dict = {}, - conditionals: set[Condition] = set(), output_conf: dict = {}) -> Tester: + conditionals: set[Condition] = set(), output_conf: dict = {}, + test_groups: list[list[str]] = []) -> Tester: ''' Creates a new Tester object from a python dictionary. The dictionary is expected to be created by reading a .yml file that contains test defintions. It requires all keys that @@ -480,6 +554,7 @@ def from_dict(input_dict: dict, initial_vars: dict[str, Any] = dict(), environment Dictionary of environment variables to use within the test conditionals Conditions inherited from the upper tester output_conf Output configuration inherited from the upper tester + test_groups List of test groups inherited from the upper tester Returns: Tester Tester object created from the dictionary @@ -498,6 +573,7 @@ def from_dict(input_dict: dict, initial_vars: dict[str, Any] = dict(), testers = g.get('testers') definitions = g.get('tests') + groups = tricot.utils.merge_groups(test_groups, list(map(lambda x: str(x), t.get('groups', [])))) variables = tricot.utils.merge(initial_vars, g.get('variables', {}), 'variables', path) variables['cwd'] = path.parent @@ -515,18 +591,18 @@ def from_dict(input_dict: dict, initial_vars: dict[str, Any] = dict(), f = path.parent.joinpath(f) for ff in glob.glob(str(f)): - tester = Tester.from_file(ff, variables, None, error_mode, env, conds, output_c) + tester = Tester.from_file(ff, variables, None, error_mode, env, conds, output_c, groups) tester_list.append(tester) tests = None if definitions and type(definitions) is list: - tests = Test.from_list(path, definitions, variables, error_mode, env, conds, output_c) + tests = Test.from_list(path, definitions, variables, error_mode, env, conds, output_c, groups) elif not tester_list: raise TesterKeyError('tests', path, optional='testers') new_tester = Tester(path, t['name'], t.get('title'), variables, tests, tester_list, containers, plugins, - run_conds, conds, error_mode) + run_conds, conds, error_mode, t.get('id'), groups) new_tester.set_logfile(t.get('logfile')) return new_tester @@ -552,7 +628,7 @@ def set_logfile(self, logfile: str) -> None: def from_file(filename: str, initial_vars: dict[str, Any] = dict(), runtime_vars: dict[str, Any] = None, error_mode: str = None, env: dict = {}, conditionals: set[Condition] = set(), - output_conf: dict = {}) -> Tester: + output_conf: dict = {}, test_groups: list[list[str]] = []) -> Tester: ''' Creates a new Tester object from a .yml file. The .yml file obviously needs to be in the expected format and requires all keys that are needed to construct a Tester object. @@ -565,6 +641,7 @@ def from_file(filename: str, initial_vars: dict[str, Any] = dict(), runtime_vars env Current environment variables conditionals Conditions inherited from the previous tester output_conf Output configuration inherited from upper tester + test_groups List of test groups inherited from the upper tester Returns: Tester Tester object created from the file @@ -578,61 +655,142 @@ def from_file(filename: str, initial_vars: dict[str, Any] = dict(), runtime_vars if runtime_vars is not None and '$runtime' not in initial_vars: initial_vars['$runtime'] = runtime_vars - return Tester.from_dict(config_dict, initial_vars, Path(filename), error_mode, env, conditionals, output_conf) + return Tester.from_dict(config_dict, initial_vars, Path(filename), error_mode, env, conditionals, + output_conf, test_groups) + + def set_runall(self, value: bool) -> None: + ''' + Sets the runall property of a tester. This is required when users specify a tetser ID on the command + line. In this case, all tests and nested testers should be run, although their ID is not contained + within the IDs to run. Setting the runall property on a tester disabled ID checking and runs everything + inside of it anyway. + + Setting the runall property is done recursively for all nested testers. + + Parameters: + value Value to set for the runall property. + + Returns: + None + ''' + self.runall = value - def contains_testers(self, testers: list[str]) -> bool: + for tester in self.testers: + tester.set_runall(value) + + def contains_id(self, t_ids: set[str]) -> bool: ''' - Checks whether the specified tester name is contained within the current test tree. + Checks whether the specified Test / Tester IDs are contained within this tester. Parameters: - testers List of tester names to look for + t_ids Test / Tester IDs to look for Returns: - bool True if tester is contained within test tree + bool True if one of the ids is contained within the tester ''' - if len(testers) == 0: + if not t_ids or self.runall: return True - for tester in testers: + if self.id and {self.id}.issubset(t_ids): + self.set_runall(True) + return True - if self.name == tester: - return True + if self.tests: + + for test in self.tests: + if test.id and {test.id}.issubset(t_ids): + return True for tester in self.testers: + if tester.contains_id(t_ids): + return True + + return False + + def contains_group(self, t_groups: list[list[str]]) -> bool: + ''' + Checks whether the specified Group set exists within the tester. - if tester.contains_testers(testers): + Parameters: + t_groups List of group lists to check in + + Returns: + bool True if set of groups is contained within the tester + ''' + if not t_groups or self.runall: + return True + + if tricot.utils.groups_contain(t_groups, self.groups): + self.set_runall(True) + return True + + if self.tests: + + for test in self.tests: + if tricot.utils.groups_contain(t_groups, test.groups): + return True + + for tester in self.testers: + if tester.contains_group(t_groups): return True return False - def run(self, testers: list[str] = (), numbers: list[str] = (), exclude: list[str] = (), - hotplug_variables: dict[str, Any] = {}) -> None: + def skip_test(self, exclude: set[str], exclude_groups: list[list[str]]) -> bool: + ''' + Checks whether the current test is contained within the exclude lists. + + Parameters: + exclude Set of Test / Tester IDs to exclude + exclude_groups List of group lists to exclude + + Returns: + bool + ''' + if exclude and self.id in exclude: + return True + + elif exclude_groups and tricot.utils.groups_contain(exclude_groups, self.groups): + return True + + return False + + def run(self, ids: set[str], groups: list[list[str]], exclude: set[str], + exclude_groups: list[list[str]], hotplug_variables: dict[str, Any] = dict()) -> None: ''' Runs the test: Prints the title of the tester and iterates over all contained Test objects and calls their 'run' method. Parameters: - tester Only run testers that match the specified names - numbers Only run tests that match the specified numbers - exclude Exclude the specified tester names - hotplug_variables Dictionary of variables that are applied at runtime. + ids Set of Test / Tester IDs to run + groups List of group lists to run + exclude Set of Test / Tester IDs to exclude + exclude_groups List of group lists to exclude + hotplug_variables Hotplug variables to use during the test Returns: None ''' - if not self.contains_testers(testers) or self.name in exclude: + if self.skip_test(exclude, exclude_groups): return - Tester.increase() + if not self.contains_id(ids) or not self.contains_group(groups): + return if not Condition.check_conditions(self.conditions, self.conditionals): Logger.print_mixed_yellow('Skipping test:', self.title) return + tricot.Logger.print('') Logger.add_logfile(self.logfile) - Logger.print_mixed_yellow('Starting test:', self.title) - hotplug = hotplug_variables.copy() + Logger.print_mixed_yellow('Starting test:', self.title, end=' ') + + if self.id and self.id != self.name: + Logger.print_plain_blue(f'[{self.id}]') + else: + print() + hotplug = hotplug_variables.copy() Logger.increase_indent() for plugin in self.plugins: @@ -642,13 +800,8 @@ def run(self, testers: list[str] = (), numbers: list[str] = (), exclude: list[st container.start_container() hotplug = {**hotplug, **container.get_container_variables()} - if self.tests: - Logger.print('') - for ctr in range(len(self.tests)): - if len(numbers) == 0 or (ctr+1) in numbers: - self.tests[ctr].run(f'{ctr+1}.', hotplug) - - self.run_childs(testers, numbers, exclude, hotplug) + self.run_tests(ids, groups, exclude, exclude_groups, hotplug) + self.run_childs(ids, groups, exclude, exclude_groups, hotplug) for container in self.containers: container.stop_container() @@ -659,28 +812,70 @@ def run(self, testers: list[str] = (), numbers: list[str] = (), exclude: list[st Logger.remove_logfile(self.logfile) Logger.decrease_indent() - def run_childs(self, testers: list[str] = (), numbers: list[str] = (), exclude: list[str] = (), - hotplug_variables: dict[str, Any] = {}) -> None: + def run_tests(self, ids: set[str], groups: list[list[str]], exclude: set[str], + exclude_groups: list[list[str]], hotplug_variables: dict[str, Any]) -> None: + ''' + Wrapper function that executes the tests specified in a tester. + + Parameters: + ids Set of Test / Tester IDs to run + groups List of group lists to run + exclude Set of Test / Tester IDs to exclude + exclude_groups List of group lists to exclude + hotplug_variables Hotplug variables to use during the test + + Returns: + None + ''' + if not self.tests: + return + + Logger.print('') + runall = tricot.utils.groups_contain(groups, self.groups) or (ids and {self.id}.issubset(ids)) + + for ctr in range(len(self.tests)): + + test = self.tests[ctr] + + if test.skip_test(exclude, exclude_groups): + continue + + elif self.runall or runall: + pass + + elif not ids and not groups: + pass + + elif ids and {test.id}.issubset(ids): + pass + + elif groups and tricot.utils.groups_contain(groups, test.groups): + pass + + else: + continue + + test.run(f'{ctr+1}.', hotplug_variables) + + def run_childs(self, ids: set[str], groups: list[list[str]], exclude: set[str], + exclude_groups: list[list[str]], hotplug_variables: dict[str, Any]) -> None: ''' Runs the child testers of the current tester. Parameters: - tester Only run testers that match the specified names - numbers Only run tests that match the specified numbers - exclude Exclude the specified tester names - hotplug_variables Dictionary of variables that are applied at runtime. + ids Set of Test / Tester IDs to run + groups List of group lists to run + exclude Set of Test / Tester IDs to exclude + exclude_groups List of group lists to exclude + hotplug_variables Hotplug variables to use during the test Returns: None ''' for tester in self.testers: - try: - if self.name in testers: - tester.run(numbers=numbers, exclude=exclude, hotplug_variables=hotplug_variables) - - else: - tester.run(testers, numbers, exclude, hotplug_variables) + try: + tester.run(ids, groups, exclude, exclude_groups, hotplug_variables) except tricot.PluginException as e: diff --git a/tricot/utils.py b/tricot/utils.py index 8514eb7..ba494cf 100644 --- a/tricot/utils.py +++ b/tricot/utils.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import re import tricot from typing import Any from pathlib import Path @@ -275,3 +276,146 @@ def merge(dict1: dict, dict2: dict, name: str = None, path: Path = None) -> dict raise tricot.TricotException('Invalid type specified during merge operation.', path) return {**dict1, **dict2} + + +def parse_groups(groups: list[str]) -> list[list[str]]: + ''' + Parses group specifications. Groups should be specified as comma separated strings. Each + comma separated part is interpreted as a group. All groups within a string are mandatory + for a test / tester to match. Braces can be used for or-like statements. + + E.g.: + + java8,networking,filter -> list(java8, networking, filter) + java8,{networking,io},filter -> list(list(java8, networking, filter), + list(java8, io, filter)) + + Parameters: + groups List of group specifications + + Returns: + list List of group lists + ''' + lists = list() + regex = re.compile(r'\{([^}]+)\}') + + for group_spec in groups: + + or_like = regex.findall(group_spec) + group_spec = regex.sub('$ORLIKE$', group_spec) + + split = list(filter(None, group_spec.split(','))) + group_lists = [split] + + for match in or_like: + + new = [] + + for group_list in group_lists: + + split = list(filter(None, match.split(','))) + + for item in split: + + new_list = group_list.copy() + + for ctr in range(len(new_list)): + + if new_list[ctr] == '$ORLIKE$': + new_list[ctr] = item + break + + new.append(new_list) + + group_lists = new + + lists += group_lists + + return lists + + +def groups_contain(groups_list: list[list[str]], groups: list[list[str]]) -> bool: + ''' + Checks whether a specified list of groups contains a particular group of a + list of specified groups. This separate function is required, as group + comparison supports wildcards as * or **. + + Parameters: + groups_list List of group lists to search in + groups Group list to look for + + Returns: + bool True if group is contained in groups + ''' + for group in groups: + + for items in groups_list: + + ctr = 0 + match = True + items = items.copy() + + try: + + while len(items) != 0: + + item = items.pop(0) + + if item == '*' or group[ctr] == item: + + ctr += 1 + continue + + if item == '**': + + item = items.pop(0) + while group[ctr] != item and ctr != len(group): + ctr += 1 + + if ctr == len(group): + match = False + break + + ctr += 1 + continue + + match = False + break + + if match: + return True + + except IndexError: + pass + + return False + + +def merge_groups(parent_groups: list[list[str]], new_groups: list[str]) -> list[list[str]]: + ''' + This function is called by tests and testers to join groups that are defined within + the test / tester definition with group lists that have been specified for upper testers. + Each group in the test / tester specification is appened to the parent defined groups. + + Paramaters: + parent_groups Group lists inherited by the parent + new_groups Groups specified for the test / tester + + Returns: + merged Merge result + ''' + merged = list() + + for parent_group in (parent_groups or [[]]): + + if new_groups: + + for new_group in new_groups: + copy = parent_group.copy() + copy.append(new_group) + merged.append(copy) + + else: + merged.append(parent_group.copy()) + + return merged