From 404f2e5f217615faa4cebf75d99824bc03c83895 Mon Sep 17 00:00:00 2001 From: Ben Dalling Date: Thu, 21 Jul 2022 11:55:11 +0100 Subject: [PATCH 1/5] new: dev: Add tests for checking Python file size. --- .codeclimate.yml | 73 +++++++++++++++++++++++++++ tests/features/lines_of_code.feature | 8 +++ tests/step_defs/test_issue21.py | 9 ++-- tests/step_defs/test_lines_of_code.py | 48 ++++++++++++++++++ 4 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 .codeclimate.yml create mode 100644 tests/features/lines_of_code.feature create mode 100644 tests/step_defs/test_lines_of_code.py diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..a6beeba --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,73 @@ +--- +version: "2" # required to adjust maintainability checks + +checks: + argument-count: + enabled: true + config: + threshold: 4 + complex-logic: + enabled: true + config: + threshold: 4 + file-lines: + enabled: true + config: + threshold: 250 + method-complexity: + enabled: true + config: + threshold: 5 + method-count: + enabled: true + config: + threshold: 20 + method-lines: + enabled: true + config: + threshold: 25 + nested-control-flow: + enabled: true + config: + threshold: 4 + return-statements: + enabled: true + config: + threshold: 4 + similar-code: + enabled: true + config: + threshold: # language-specific defaults. overrides affect all languages. + identical-code: + enabled: true + config: + threshold: # language-specific defaults. overrides affect all languages. + +plugins: + bandit: + enabled: true + markdownlint: + enabled: true + radon: + enabled: true + sonar-python: + enabled: true + config: + tests_patterns: + - tests/** + +exclude_patterns: + - "config/" + - "db/" + - "dist/" + - "features/" + - "**/node_modules/" + - "script/" + - "**/spec/" + - "**/test/" + - "**/tests/" + - "Tests/" + - "**/vendor/" + - "**/*_test.go" + - "**/*.d.ts" + - "CHANGELOG.md" # This file is auto-generated. diff --git a/tests/features/lines_of_code.feature b/tests/features/lines_of_code.feature new file mode 100644 index 0000000..99827c3 --- /dev/null +++ b/tests/features/lines_of_code.feature @@ -0,0 +1,8 @@ +Feature: Lines of Code + Scenario Outline: Check Lines of Code in Radon Report + Given a Radon report + When Python source file is file + Then lines of code must not be greater than + Examples: + | max_lines_of_code | + | 250 | diff --git a/tests/step_defs/test_issue21.py b/tests/step_defs/test_issue21.py index c3bc7ad..402a173 100644 --- a/tests/step_defs/test_issue21.py +++ b/tests/step_defs/test_issue21.py @@ -1,12 +1,9 @@ """Fix Issue 21 feature tests.""" -from pytest_bdd import scenario +from pytest_bdd import scenarios + +scenarios('../features/issue21.feature') # Ensure that the PyTest fixtures provided in testinfra-bdd are available to # your test suite. pytest_plugins = ['testinfra_bdd'] - - -@scenario('../features/issue21.feature', 'Issue 21') -def test_issue_21(): - """Issue 21.""" diff --git a/tests/step_defs/test_lines_of_code.py b/tests/step_defs/test_lines_of_code.py new file mode 100644 index 0000000..cdeb8d0 --- /dev/null +++ b/tests/step_defs/test_lines_of_code.py @@ -0,0 +1,48 @@ +"""Lines of Code feature tests.""" +import json +import pytest +import subprocess + +from pytest_bdd import ( + given, + scenario, + then, + when, + parsers +) + +radon_report = subprocess.run(['radon', 'raw', '--json', '.'], capture_output=True) +radon_report = json.loads(radon_report.stdout) +python_files = [] + +for _ in radon_report: + python_files.append((_,)) + + +@pytest.mark.parametrize( + ["python_file"], + python_files, +) +@scenario('../features/lines_of_code.feature', 'Check Lines of Code in Radon Report') +def test_check_lines_of_code_in_radon_report(python_file): + """Check Lines of Code in Radon Report.""" + + +@given('a Radon report', target_fixture='radon_stats') +def a_radon_report(python_file): + """a Radon report.""" + return {} + + +@when('Python source file is file') +def python_source_file_is_file(python_file, radon_stats): + """Python source file is file.""" + radon_stats['file_name'] = python_file + radon_stats['lines_of_code'] = radon_report[python_file]['loc'] + + +@then(parsers.parse('lines of code must not be greater than {max_lines_of_code:d}')) +def lines_of_code_must_not_be_greater_than_max_lines_of_code(max_lines_of_code, radon_stats): + """lines of code must not be greater than .""" + message = f'{radon_stats["file_name"]} has {radon_stats["lines_of_code"]} which exceeds {max_lines_of_code}.' + assert radon_stats['lines_of_code'] <= max_lines_of_code, message From 9f69288533315594ab10d466484e104d05cd8134 Mon Sep 17 00:00:00 2001 From: Ben Dalling Date: Thu, 21 Jul 2022 12:55:16 +0100 Subject: [PATCH 2/5] fix: dev: Fix bandit related issues. --- .bandit | 2 +- tests/step_defs/test_lines_of_code.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.bandit b/.bandit index f783118..dfef5a6 100644 --- a/.bandit +++ b/.bandit @@ -1,2 +1,2 @@ [bandit] -skips: B101 +skips: B101,B404 diff --git a/tests/step_defs/test_lines_of_code.py b/tests/step_defs/test_lines_of_code.py index cdeb8d0..009f748 100644 --- a/tests/step_defs/test_lines_of_code.py +++ b/tests/step_defs/test_lines_of_code.py @@ -1,4 +1,5 @@ """Lines of Code feature tests.""" +import shutil import json import pytest import subprocess @@ -11,8 +12,16 @@ parsers ) -radon_report = subprocess.run(['radon', 'raw', '--json', '.'], capture_output=True) -radon_report = json.loads(radon_report.stdout) +radon_executable = shutil.which('radon') +radon_command = [ + radon_executable, + 'raw', + '--json', + '.' +] + +radon_report = subprocess.check_output(radon_command).decode('utf-8') # nosec +radon_report = json.loads(radon_report) python_files = [] for _ in radon_report: @@ -20,7 +29,7 @@ @pytest.mark.parametrize( - ["python_file"], + ['python_file'], python_files, ) @scenario('../features/lines_of_code.feature', 'Check Lines of Code in Radon Report') From 02c0770a69dae28b2cd8eb5f829d9b69835a046c Mon Sep 17 00:00:00 2001 From: Ben Dalling Date: Thu, 21 Jul 2022 19:03:52 +0100 Subject: [PATCH 3/5] fix: dev: Reduce the size of __init__.py to 717 lines of code. --- testinfra_bdd/__init__.py | 50 ------------------------------- testinfra_bdd/given.py | 53 +++++++++++++++++++++++++++++++++ testinfra_bdd/plugins.py | 23 ++++++++++++++ tests/step_defs/test_example.py | 4 ++- 4 files changed, 79 insertions(+), 51 deletions(-) create mode 100644 testinfra_bdd/given.py create mode 100644 testinfra_bdd/plugins.py diff --git a/testinfra_bdd/__init__.py b/testinfra_bdd/__init__.py index bd99f66..f4df56a 100644 --- a/testinfra_bdd/__init__.py +++ b/testinfra_bdd/__init__.py @@ -12,7 +12,6 @@ import testinfra_bdd.pip from pytest_bdd import ( - given, when, then, parsers @@ -26,55 +25,6 @@ __version__ = '1.0.6' -@given(parsers.parse('the host with URL "{hostspec}" is ready'), target_fixture='testinfra_bdd_host') -def the_host_is_ready(hostspec): - """ - Ensure that the host is ready within the specified number of seconds. - - If the host does not become ready within the specified number of seconds, - fail the tests. - - Parameters - ---------- - hostspec : str - The URL of the System Under Test (SUT). Must comply to the Testinfra - URL patterns. See - https://testinfra.readthedocs.io/en/latest/backends.html - - Returns - ------- - testinfra_bdd.fixture.TestinfraBDD - The object to return as a fixture. - """ - return testinfra_bdd.fixture.get_host_fixture(hostspec) - - -@given(parsers.parse('the host with URL "{hostspec}" is ready within {seconds:d} seconds'), - target_fixture='testinfra_bdd_host') -def the_host_is_ready_with_a_number_of_seconds(hostspec, seconds): - """ - Ensure that the host is ready within the specified number of seconds. - - If the host does not become ready within the specified number of seconds, - fail the tests. - - Parameters - ---------- - hostspec : str - The URL of the System Under Test (SUT). Must comply to the Testinfra - URL patterns. See - https://testinfra.readthedocs.io/en/latest/backends.html - seconds : int - The number of seconds that the host is expected to become ready in. - - Returns - ------- - testinfra_bdd.fixture.TestinfraBDD - The object to return as a fixture. - """ - return testinfra_bdd.fixture.get_host_fixture(hostspec, seconds) - - @when(parsers.parse('the {resource_type} is {resource_name}')) @when(parsers.parse('the {resource_type} is "{resource_name}"')) def the_resource_type_is(resource_type, resource_name, testinfra_bdd_host): diff --git a/testinfra_bdd/given.py b/testinfra_bdd/given.py new file mode 100644 index 0000000..2e72742 --- /dev/null +++ b/testinfra_bdd/given.py @@ -0,0 +1,53 @@ +"""The given steps of testinfra-bdd.""" +import testinfra_bdd +from pytest_bdd import given +from pytest_bdd import parsers + + +@given(parsers.parse('the host with URL "{hostspec}" is ready'), target_fixture='testinfra_bdd_host') +def the_host_is_ready(hostspec): + """ + Ensure that the host is ready within the specified number of seconds. + + If the host does not become ready within the specified number of seconds, + fail the tests. + + Parameters + ---------- + hostspec : str + The URL of the System Under Test (SUT). Must comply to the Testinfra + URL patterns. See + https://testinfra.readthedocs.io/en/latest/backends.html + + Returns + ------- + testinfra_bdd.fixture.TestinfraBDD + The object to return as a fixture. + """ + return testinfra_bdd.fixture.get_host_fixture(hostspec) + + +@given(parsers.parse('the host with URL "{hostspec}" is ready within {seconds:d} seconds'), + target_fixture='testinfra_bdd_host') +def the_host_is_ready_with_a_number_of_seconds(hostspec, seconds): + """ + Ensure that the host is ready within the specified number of seconds. + + If the host does not become ready within the specified number of seconds, + fail the tests. + + Parameters + ---------- + hostspec : str + The URL of the System Under Test (SUT). Must comply to the Testinfra + URL patterns. See + https://testinfra.readthedocs.io/en/latest/backends.html + seconds : int + The number of seconds that the host is expected to become ready in. + + Returns + ------- + testinfra_bdd.fixture.TestinfraBDD + The object to return as a fixture. + """ + return testinfra_bdd.fixture.get_host_fixture(hostspec, seconds) diff --git a/testinfra_bdd/plugins.py b/testinfra_bdd/plugins.py new file mode 100644 index 0000000..76b96ca --- /dev/null +++ b/testinfra_bdd/plugins.py @@ -0,0 +1,23 @@ +"""Configure pytest_plugins.""" +global pytest_plugins + + +def add_plugins(): + """ + Add missing plugins to pytest_plugins. + + Returns + ------- + list + The value to set pytest_plugins to. + """ + if 'pytest_plugins' in globals(): + plugins = globals() + else: + plugins = [] + + for plugin in ['testinfra_bdd.given']: + if plugin not in plugins: + plugins.append(plugin) + + return plugins diff --git a/tests/step_defs/test_example.py b/tests/step_defs/test_example.py index efadd16..0840ccf 100644 --- a/tests/step_defs/test_example.py +++ b/tests/step_defs/test_example.py @@ -5,12 +5,14 @@ from pytest_bdd import given from pytest_bdd import scenarios +import testinfra_bdd.plugins + scenarios('../features/example.feature') # Ensure that the PyTest fixtures provided in testinfra-bdd are available to # your test suite. -pytest_plugins = ['testinfra_bdd'] +pytest_plugins = testinfra_bdd.plugins.add_plugins() @given('on GitHub Actions we skip tests') From 0788ff68b4f87cb0a51fe235afad4a42d756df08 Mon Sep 17 00:00:00 2001 From: Ben Dalling Date: Thu, 21 Jul 2022 21:31:31 +0100 Subject: [PATCH 4/5] fix: dev: Major refactor of code. --- .github/workflows/ci.yml | 7 +- testinfra_bdd/__init__.py | 730 ++------------------------------ testinfra_bdd/file.py | 98 ----- testinfra_bdd/fixture.py | 31 -- testinfra_bdd/given.py | 6 +- testinfra_bdd/pip.py | 84 ---- testinfra_bdd/plugins.py | 23 - testinfra_bdd/then/__init__.py | 1 + testinfra_bdd/then/address.py | 62 +++ testinfra_bdd/then/command.py | 124 ++++++ testinfra_bdd/then/file.py | 198 +++++++++ testinfra_bdd/then/group.py | 59 +++ testinfra_bdd/then/package.py | 42 ++ testinfra_bdd/then/pip.py | 159 +++++++ testinfra_bdd/then/process.py | 31 ++ testinfra_bdd/then/service.py | 82 ++++ testinfra_bdd/then/socket.py | 34 ++ testinfra_bdd/then/user.py | 65 +++ testinfra_bdd/when.py | 40 ++ tests/step_defs/test_example.py | 6 +- tests/step_defs/test_issue21.py | 3 +- tests/test_exceptions.py | 18 +- 22 files changed, 956 insertions(+), 947 deletions(-) delete mode 100644 testinfra_bdd/file.py delete mode 100644 testinfra_bdd/pip.py delete mode 100644 testinfra_bdd/plugins.py create mode 100644 testinfra_bdd/then/__init__.py create mode 100644 testinfra_bdd/then/address.py create mode 100644 testinfra_bdd/then/command.py create mode 100644 testinfra_bdd/then/file.py create mode 100644 testinfra_bdd/then/group.py create mode 100644 testinfra_bdd/then/package.py create mode 100644 testinfra_bdd/then/pip.py create mode 100644 testinfra_bdd/then/process.py create mode 100644 testinfra_bdd/then/service.py create mode 100644 testinfra_bdd/then/socket.py create mode 100644 testinfra_bdd/then/user.py create mode 100644 testinfra_bdd/when.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c9455e..bd3c9a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,11 +28,8 @@ jobs: pip freeze pip check - - name: Bandit - run: bandit -r . - - - name: Test - run: make test + - name: Run Make + run: make - name: Publish Code Coverage uses: paambaati/codeclimate-action@v3.0.0 diff --git a/testinfra_bdd/__init__.py b/testinfra_bdd/__init__.py index f4df56a..91708e7 100644 --- a/testinfra_bdd/__init__.py +++ b/testinfra_bdd/__init__.py @@ -4,714 +4,62 @@ For documentation and examples, please go to https://github.com/locp/testinfra-bdd """ -import re -import pytest +from testinfra_bdd.fixture import TestinfraBDD -import testinfra_bdd.file -import testinfra_bdd.fixture -import testinfra_bdd.pip +"""PYTEST_MODULES. -from pytest_bdd import ( - when, - then, - parsers -) +A list of all testinfra-bdd packages that contain fixtures. +""" +PYTEST_MODULES = [ + 'testinfra_bdd', + 'testinfra_bdd.given', + 'testinfra_bdd.then.address', + 'testinfra_bdd.then.command', + 'testinfra_bdd.then.file', + 'testinfra_bdd.then.group', + 'testinfra_bdd.then.package', + 'testinfra_bdd.then.pip', + 'testinfra_bdd.then.process', + 'testinfra_bdd.then.service', + 'testinfra_bdd.then.socket', + 'testinfra_bdd.then.user', + 'testinfra_bdd.when' +] """The version of the module. This is used by setuptools and by gitchangelog to identify the name of the name of the release. """ -__version__ = '1.0.6' - - -@when(parsers.parse('the {resource_type} is {resource_name}')) -@when(parsers.parse('the {resource_type} is "{resource_name}"')) -def the_resource_type_is(resource_type, resource_name, testinfra_bdd_host): - """ - Get a resource of a specified type from the system under test. - - Parameters - ---------- - resource_type : str - The type of the resource. - resource_name : str - The name of the resource. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - """ - testinfra_bdd_host.get_resource_from_host(resource_type, resource_name) - - -@when(parsers.parse('the system property {property_name} is not "{expected_value}" skip tests')) -@when(parsers.parse('the system property {property_name} is not {expected_value} skip tests')) -def skip_tests_if_system_info_does_not_match(property_name, expected_value, testinfra_bdd_host): - """ - Skip tests if a system property does not patch the expected value. - - Parameters - ---------- - property_name : str - expected_value : str - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - """ - actual_value = testinfra_bdd_host.get_host_property(property_name) - if actual_value != expected_value: - pytest.skip(f'System {property_name} is {actual_value} which is not {expected_value}.') - - -############################################################################# -# Command checks. -############################################################################# -@then(parsers.parse('the command {command} exists in path')) -@then(parsers.parse('the command "{command}" exists in path')) -def check_command_exists_in_path(command, testinfra_bdd_host): - """ - Assert that a specified command is present on the host path. - - Parameters - ---------- - command : str - The name of the command to check for. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the command is not found on the path. - """ - message = f'Unable to find the command "{command}" on the path.' - assert testinfra_bdd_host.host.exists(command), message - - -@then(parsers.parse('the command {stream_name} contains "{text}"')) -def check_command_stream_contains(stream_name, text, testinfra_bdd_host): - """ - Check that the stdout or stderr stream contains a string. - - Parameters - ---------- - stream_name : str - The name of the stream to check. Must be "stdout" or "stderr". - text : str - The text to search for. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the specified stream does not contain the expected text. - """ - stream = testinfra_bdd_host.get_stream_from_command(stream_name) - message = f'The string "{text}" was not found in the {stream_name} ("{stream}") of the command.' - assert text in stream, message +__version__ = '2.0.0' -@then(parsers.parse('the command {stream_name} contains the regex "{pattern}"')) -def check_command_stream_contains_the_regex(stream_name, pattern, testinfra_bdd_host): +def get_host_fixture(hostspec, timeout=0): """ - Check that the stdout or stderr stream matches a regular expression pattern. + Return a host that is confirmed as ready. - Parameters - ---------- - stream_name : str - The name of the stream to be checked. Must be stdout or stderr. - pattern : str - The pattern to search for in the stream. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. + hostspec : str + The URL of the System Under Test (SUT). Must comply to the Testinfra + URL patterns. See + https://testinfra.readthedocs.io/en/latest/backends.html + timeout : int, optional + The number of seconds that the host is expected to become ready in. - Raises - ------ - AssertError - When the specified stream does not match the pattern. - ValueError - When the stream name is not recognized. - """ - stream = testinfra_bdd_host.get_stream_from_command(stream_name) - message = f'The regex "{pattern}" is not found in the {stream_name} "{stream}".' - # The parsers.parse function escapes the parsed string. We need to clean it up before using it. - pattern = pattern.encode('utf-8').decode('unicode_escape') - prog = re.compile(pattern) - assert prog.search(stream) is not None, message - - -@then(parsers.parse('the command return code is {expected_return_code:d}')) -def check_command_return_code(expected_return_code, testinfra_bdd_host): - """ - Check that the expected return code from a command matches the actual return code. - - Parameters - ---------- - expected_return_code : int - The expected return code (e.g. zero/0). - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. + Returns + ------- + testinfra_bdd.fixture.TestinfraBDD + The object to return as a fixture. Raises ------ AssertError - When the actual return code does not match the expected return code. - """ - cmd = testinfra_bdd_host.command - actual_return_code = cmd.rc - message = f'Expected a return code of {expected_return_code} but got {actual_return_code}.' - assert expected_return_code == actual_return_code, message - - -@then(parsers.parse('the command {stream_name} is empty')) -def command_stream_is_empty(stream_name, testinfra_bdd_host): - """ - Check that the specified command stream is empty. - - Parameters - ---------- - stream_name : str - The name of the stream to be checked. Must be stdout or stderr. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the specified stream does not match the pattern. - """ - stream = testinfra_bdd_host.get_stream_from_command(stream_name) - assert not stream, f'Expected {stream_name} to be empty ("{stream}").' - - -############################################################################# -# Service checks. -############################################################################# -@then('the service is not enabled') -def the_service_is_not_enabled(testinfra_bdd_host): - """ - Check that the service is not enabled. - - Parameters - ---------- - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the service is enabled. - """ - service = testinfra_bdd_host.service - message = f'Expected {service.name} on host {testinfra_bdd_host.hostname} to be disabled, but it is enabled.' - assert not service.is_enabled, message - - -@then('the service is enabled') -def the_service_is_enabled(testinfra_bdd_host): + When the host is not ready. """ - Check that the service is enabled. - - Parameters - ---------- - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the service is not enabled. - """ - service = testinfra_bdd_host.service - message = f'Expected {service.name} on host {testinfra_bdd_host.hostname} to be enabled, but it is disabled.' - assert service.is_enabled, message - - -@then('the service is not running') -def the_service_is_not_running(testinfra_bdd_host): - """ - Check that the service is not running. - - Parameters - ---------- - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the service is running. - """ - service = testinfra_bdd_host.service - message = f'Expected {service.name} on host {testinfra_bdd_host.hostname} to not be running.' - assert not service.is_running, message - - -@then('the service is running') -def the_service_is_running(testinfra_bdd_host): - """ - Check that the service is running. - - Parameters - ---------- - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the service is not running. - """ - service = testinfra_bdd_host.service - message = f'Expected {service.name} on host {testinfra_bdd_host.hostname} to be running.' - assert service.is_running, message - - -############################################################################# -# Pip package checks. -############################################################################# -@then('the pip check is OK') -def the_pip_check_is_ok(testinfra_bdd_host): - """ - Verify installed packages have compatible dependencies. - - Parameters - ---------- - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the packages have incompatible dependencies. - """ - host = testinfra_bdd_host.host - cmd = host.pip.check() - message = f'Incompatible Pip packages - {cmd.stdout} {cmd.stderr}' - assert cmd.rc == 0, message - - -@then(parsers.parse('the pip package state is {expected_state}')) -@then(parsers.parse('the pip package is {expected_state}')) -def the_pip_package_state_is(expected_state, testinfra_bdd_host): - """ - Check the state of a Pip package. - - Parameters - ---------- - expected_state : str - The expected state of the package. Can be absent, latest or installed. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the actual state doesn't match the expected state. - """ - (actual_state, message) = testinfra_bdd.pip.get_pip_package_actual_state( - testinfra_bdd_host.pip_package, - expected_state, - testinfra_bdd_host.host - ) - assert actual_state == expected_state, message - - -@then(parsers.parse('the pip package version is {expected_version}')) -def the_pip_package_version_is(expected_version, testinfra_bdd_host): - """ - Check the version of a Pip package. - - Parameters - ---------- - expected_version : str - The version of the package that is expected. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the actual version is not the expected version. - """ - pip_package = testinfra_bdd_host.pip_package - assert pip_package, 'Pip package not set. Have you missed a "When pip package is" step?' - actual_version = pip_package.version - message = f'Expected Pip package version to be {expected_version} but it was {actual_version}.' - assert actual_version == expected_version, message - - -############################################################################# -# System package checks. -############################################################################# -@then(parsers.parse('the package state is {expected_status}')) -@then(parsers.parse('the package is {expected_status}')) -def the_package_status_is(expected_status, testinfra_bdd_host): - """ - Check the status of a package (installed/absent). - - Parameters - ---------- - expected_status : str - Can be absent, installed or present. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the package is not in the expected state. - """ - status_lookup = { - 'absent': False, - 'installed': True, - 'present': True - } - expected_to_be_installed = status_lookup[expected_status] - pkg = testinfra_bdd_host.package - actual_status = pkg.is_installed - - if expected_to_be_installed: - message = f'Expected {pkg.name} to be {expected_status} on {testinfra_bdd_host.hostname} but it is absent.' - - if actual_status: - message = f'Expected {pkg.name} to be absent on {testinfra_bdd_host.hostname} ' - message += 'but it is installed ({pkg.version}).' - - assert actual_status == expected_to_be_installed, message - - -############################################################################# -# File checks. -############################################################################# -@then(parsers.parse('the file contents contains "{text}"')) -def the_file_contents_contains_text(text, testinfra_bdd_host): - """ - Check if the file contains a string. - - Parameters - ---------- - text : str - The string to search for in the file content. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the file does not contain the string. - """ - file = testinfra_bdd_host.file - assert file.contains(text), f'The file {testinfra_bdd_host.hostname}:{file.path} does not contain "{text}".' - - -@then(parsers.parse('the file contents contains the regex "{pattern}"')) -def the_file_contents_matches_the_regex(pattern, testinfra_bdd_host): - """ - Check if the file contains matches a regex pattern. - - Parameters - ---------- - pattern : str - The regular expression to match against the file content. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the regex does not match the file content. - """ - file = testinfra_bdd_host.file - file_name = f'{testinfra_bdd_host.hostname}:{file.path}' - message = f'The regex "{pattern}" does not match the content of {file_name} ("{file.content_string}").' - # The parsers.parse function escapes the parsed string. We need to clean it up before using it. - pattern = pattern.encode('utf-8').decode('unicode_escape') - assert re.search(pattern, file.content_string) is not None, message - - -@then(parsers.parse('the file is {expected_status}')) -def the_file_status(expected_status, testinfra_bdd_host): - """ - Check if the file is present or absent. - - Parameters - ---------- - expected_status : str - Should be present or absent. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - When the expected status does not match the actual status. - """ - the_file_property_is('state', expected_status, testinfra_bdd_host) - - -@then(parsers.parse('the file {property_name} is {expected_value}')) -def the_file_property_is(property_name, expected_value, testinfra_bdd_host): - """ - Check the property of a file. - - Parameters - ---------- - property_name : str - The name of the property to compare. - expected_value : str - The value that is expected. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - If the actual value does not match the expected value. - """ - (actual_value, exception_message) = testinfra_bdd.file.get_file_actual_state( - testinfra_bdd_host.file, - property_name, - expected_value - ) - assert actual_value == expected_value, exception_message - - -############################################################################# -# User checks. -############################################################################# -@then(parsers.parse('the user {property_name} is {expected_value}')) -def the_user_property_is(property_name, expected_value, testinfra_bdd_host): - """ - Check the property of a user. - - Parameters - ---------- - property_name : str - The name of the property to compare. - expected_value : str - The value that is expected. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - If the actual value does not match the expected value. - """ - user = testinfra_bdd_host.user - assert user, 'User not set. Have you missed a "When user is" step?' - - if testinfra_bdd_host.user.exists: - actual_state = 'present' - properties = { - 'gid': str(user.gid), - 'group': user.group, - 'home': user.home, - 'shell': user.shell, - 'state': actual_state, - 'uid': str(user.uid) - } + if timeout: + message = f'The host {hostspec} is not ready within {timeout} seconds.' else: - actual_state = 'absent' - properties = { - 'state': actual_state - } - - assert property_name in properties, f'Unknown user property "{property_name}".' - actual_value = properties[property_name] - message = f'Expected {property_name} for user {user.name} to be "{expected_value}" ' - message += f'but it was "{actual_value}".' - assert actual_value == expected_value, message - - -@then(parsers.parse('the user is {expected_state}')) -def check_the_user_state(expected_state, testinfra_bdd_host): - """ - Check that the actual state of a user matches the expected state. - - Parameters - ---------- - expected_state : str - The expected state (e.g. absent or present). - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - """ - the_user_property_is('state', expected_state, testinfra_bdd_host) - - -############################################################################# -# Group checks. -############################################################################# -@then(parsers.parse('the group {property_name} is {expected_value}')) -def the_group_property_is(property_name, expected_value, testinfra_bdd_host): - """ - Check the property of a group. - - Parameters - ---------- - property_name : str - The name of the property to compare. - expected_value : str - The value that is expected. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - If the actual value does not match the expected value. - """ - group = testinfra_bdd_host.group - assert group, 'Group not set. Have you missed a "When group is" step?' - - properties = { - 'gid': None, - 'state': 'absent' - } - - if group.exists: - properties = { - 'gid': str(group.gid), - 'state': 'present' - } - - assert property_name in properties, f'Unknown group property ({property_name}).' - actual_value = properties[property_name] - message = f'Expected group property to be {expected_value} but it was {actual_value}.' - assert actual_value == expected_value, message - - -@then(parsers.parse('the group is {expected_state}')) -def check_the_group_state(expected_state, testinfra_bdd_host): - """ - Check that the actual state of a group matches the expected state. - - Parameters - ---------- - expected_state : str - The expected state (e.g. absent or present). - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - """ - the_group_property_is('state', expected_state, testinfra_bdd_host) - - -############################################################################# -# Process checks. -############################################################################# -@then(parsers.parse('the process count is {expected_count:d}')) -def the_process_count_is(expected_count, testinfra_bdd_host): - """ - Check that the process count matches the expected count. - - Parameters - ---------- - expected_count : int - The expected number of processes. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - If the actual process count does not match the expected count. - """ - specification = testinfra_bdd_host.process_specification - processes = testinfra_bdd_host.processes - assert processes, 'No process set, did you forget a "When process filter" step?' - actual_process_count = len(processes) - message = f'Expected process specification "{specification}" to return {expected_count} ' - message += f'but found {actual_process_count} "{processes}".' - assert actual_process_count == expected_count, message - - -############################################################################# -# Process checks. -############################################################################# -@then(parsers.parse('the address is {expected_state}')) -def the_address_is(expected_state, testinfra_bdd_host): - """ - Check the actual state of an address against an expected state. - - Parameters - ---------- - expected_state : str - The expected state of the address. - - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - If the actual state does not match the state. - """ - address = testinfra_bdd_host.address - assert address, 'Address is not set. Did you miss a "When address is" step?' - properties = { - 'resolvable': address.is_resolvable, - 'reachable': address.is_reachable - } - assert expected_state in properties, f'Invalid state for {address.name} ("{expected_state}").' - message = f'Expected the address {address.name} to be {expected_state} but it is not.' - assert properties[expected_state], message - - -@then(parsers.parse('the port is {expected_state}')) -def the_port_is(expected_state, testinfra_bdd_host): - """ - Check the actual state of an address port against an expected state. - - Parameters - ---------- - expected_state : str - The expected state of the port. - - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - If the actual state does not match the state. - """ - port = testinfra_bdd_host.port - assert port, 'Port is not set. Did you miss a "When the address and port" step?' - properties = { - 'reachable': port.is_reachable - } - assert expected_state in properties, f'Unknown Port property ("{expected_state}").' - message = f'{testinfra_bdd_host.address.name}:{testinfra_bdd_host.port_number} is unreachable.' - assert properties['reachable'], message - - -############################################################################# -# Socket checks. -############################################################################# -@then(parsers.parse('the socket is {expected_state}')) -def the_socket_is(expected_state, testinfra_bdd_host): - """ - Check the state of a socket. - - Parameters - ---------- - expected_state : str - The expected state of the socket. - testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD - The test fixture. - - Raises - ------ - AssertError - If the actual state does not match the state. - """ - socket = testinfra_bdd_host.socket - socket_url = testinfra_bdd_host.socket_url - actual_state = 'not listening' - assert socket, 'Socket is not set. Have you missed a "When socket is" step?' - - if socket.is_listening: - actual_state = 'listening' + message = f'The host {hostspec} is not ready.' - message = f'Expected socket {socket_url} to be {expected_state} but it is {actual_state}.' - assert actual_state == expected_state, message + host = TestinfraBDD(hostspec) + assert host.is_host_ready(timeout), message + return host diff --git a/testinfra_bdd/file.py b/testinfra_bdd/file.py deleted file mode 100644 index 458a2ea..0000000 --- a/testinfra_bdd/file.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Functions required for files.""" -from testinfra_bdd.exception_message import exception_message - - -def get_file_actual_state(file, property_name, expected_state): - """ - Get the actual state of a file given the package and the expected state. - - Parameters - ---------- - file : testinfra.File - The file to be checked. - property_name : str - The name of the property to check (e.g. state). - expected_state : str - The expected state. - - Returns - ------- - tuple - str - The actual state (e.g. absent, latest, present or superseded). - str - A suitable message if the actual state doesn't match the actual state. - """ - properties = get_file_properties(file) - assert property_name in properties, f'Unknown user property "{property_name}".' - actual_state = properties[property_name] - return actual_state, exception_message(f'File {file.path} {property_name}', actual_state, expected_state) - - -def get_file_properties(file): - """ - Get the properties of the file. - - Parameters - ---------- - file : testinfra.File - The file to be checked. - - Returns - ------- - dict - A dictionary of the properties. - """ - assert file, 'File not set. Have you missed a "When file is" step?' - properties = { - 'group': None, - 'mode': None, - 'owner': None, - 'state': 'absent', - 'type': None, - 'user': None - } - - if file.exists: - properties = { - 'group': file.group, - 'mode': '0o%o' % file.mode, - 'owner': file.user, - 'state': 'present', - 'type': get_file_type(file), - 'user': file.user - } - - return properties - - -def get_file_type(file): - """ - Get the file type. - - Parameters - ---------- - file : testinfra.File - The file to be checked. - - Returns - ------- - str - The type of file. - """ - file_type = None - - type_lookup = { - 'file': file.is_file, - 'directory': file.is_directory, - 'pipe': file.is_pipe, - 'socket': file.is_socket, - 'symlink': file.is_symlink - } - - for key in type_lookup: - if type_lookup[key]: - file_type = key - break - - return file_type diff --git a/testinfra_bdd/fixture.py b/testinfra_bdd/fixture.py index 2e3caf7..0cb7ebe 100644 --- a/testinfra_bdd/fixture.py +++ b/testinfra_bdd/fixture.py @@ -238,34 +238,3 @@ def wait_until_is_host_ready(self, timeout=0): now = time.time() return is_ready - - -def get_host_fixture(hostspec, timeout=0): - """ - Return a host that is confirmed as ready. - - hostspec : str - The URL of the System Under Test (SUT). Must comply to the Testinfra - URL patterns. See - https://testinfra.readthedocs.io/en/latest/backends.html - timeout : int, optional - The number of seconds that the host is expected to become ready in. - - Returns - ------- - testinfra_bdd.fixture.TestinfraBDD - The object to return as a fixture. - - Raises - ------ - AssertError - When the host is not ready. - """ - if timeout: - message = f'The host {hostspec} is not ready within {timeout} seconds.' - else: - message = f'The host {hostspec} is not ready.' - - host = TestinfraBDD(hostspec) - assert host.is_host_ready(timeout), message - return host diff --git a/testinfra_bdd/given.py b/testinfra_bdd/given.py index 2e72742..ca94f8f 100644 --- a/testinfra_bdd/given.py +++ b/testinfra_bdd/given.py @@ -1,5 +1,5 @@ """The given steps of testinfra-bdd.""" -import testinfra_bdd +import testinfra_bdd.fixture from pytest_bdd import given from pytest_bdd import parsers @@ -24,7 +24,7 @@ def the_host_is_ready(hostspec): testinfra_bdd.fixture.TestinfraBDD The object to return as a fixture. """ - return testinfra_bdd.fixture.get_host_fixture(hostspec) + return testinfra_bdd.get_host_fixture(hostspec) @given(parsers.parse('the host with URL "{hostspec}" is ready within {seconds:d} seconds'), @@ -50,4 +50,4 @@ def the_host_is_ready_with_a_number_of_seconds(hostspec, seconds): testinfra_bdd.fixture.TestinfraBDD The object to return as a fixture. """ - return testinfra_bdd.fixture.get_host_fixture(hostspec, seconds) + return testinfra_bdd.get_host_fixture(hostspec, seconds) diff --git a/testinfra_bdd/pip.py b/testinfra_bdd/pip.py deleted file mode 100644 index c74ed5d..0000000 --- a/testinfra_bdd/pip.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Functions required for Pip packages.""" - -from testinfra_bdd.exception_message import exception_message - - -def check_entry_requirements(pip_package, expected_state): - """ - Check that the entry requirements are met for the test. - - Parameters - ---------- - pip_package : testinfra.Pip - The Pip package to be checked. - expected_state : str - The expected state. - - Raises - ------ - ValueError - If the expected state is invalid. - RuntimeError - If the Pip package has not been set. - """ - valid_expected_states = [ - 'absent', - 'latest', - 'present', - 'superseded' - ] - - if expected_state not in valid_expected_states: - raise ValueError(f'Unknown expected state "{expected_state}" for a Pip package.') - elif not pip_package: - raise RuntimeError('Pip package not set. Have you missed a "When pip package is" step?') - - -def get_pip_package_actual_state(pip_package, expected_state, host): - """ - Get the actual state of a Pip package given the package and the expected state. - - Parameters - ---------- - pip_package : testinfra.Pip - The Pip package to be checked. - expected_state : str - The expected state. - host : testinfra.host.Host - The host to be checked against. - - Returns - ------- - tuple - str - The actual state (e.g. absent, latest, present or superseded). - str - A suitable message if the actual state doesn't match the actual state. - """ - state_checks = [ - 'absent', - 'present' - ] - - check_entry_requirements(pip_package, expected_state) - - if expected_state in state_checks: - actual_state = 'absent' - - if pip_package.is_installed: - actual_state = 'present' - - return actual_state, exception_message( - f'Pip package {pip_package.name}', - actual_state, - expected_state - ) - - outdated_packages = host.pip.get_outdated_packages() - - if pip_package.name in outdated_packages: - actual_state = 'superseded' - else: - actual_state = 'latest' - - return actual_state, exception_message(f'Pip package {pip_package.name}', actual_state, expected_state) diff --git a/testinfra_bdd/plugins.py b/testinfra_bdd/plugins.py deleted file mode 100644 index 76b96ca..0000000 --- a/testinfra_bdd/plugins.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Configure pytest_plugins.""" -global pytest_plugins - - -def add_plugins(): - """ - Add missing plugins to pytest_plugins. - - Returns - ------- - list - The value to set pytest_plugins to. - """ - if 'pytest_plugins' in globals(): - plugins = globals() - else: - plugins = [] - - for plugin in ['testinfra_bdd.given']: - if plugin not in plugins: - plugins.append(plugin) - - return plugins diff --git a/testinfra_bdd/then/__init__.py b/testinfra_bdd/then/__init__.py new file mode 100644 index 0000000..43fa053 --- /dev/null +++ b/testinfra_bdd/then/__init__.py @@ -0,0 +1 @@ +"""The then packages/fixtures for testinfra-bdd.""" diff --git a/testinfra_bdd/then/address.py b/testinfra_bdd/then/address.py new file mode 100644 index 0000000..200d92e --- /dev/null +++ b/testinfra_bdd/then/address.py @@ -0,0 +1,62 @@ +"""Then address fixtures for testinfra-bdd.""" +from pytest_bdd import ( + then, + parsers +) + + +@then(parsers.parse('the address is {expected_state}')) +def the_address_is(expected_state, testinfra_bdd_host): + """ + Check the actual state of an address against an expected state. + + Parameters + ---------- + expected_state : str + The expected state of the address. + + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + If the actual state does not match the state. + """ + address = testinfra_bdd_host.address + assert address, 'Address is not set. Did you miss a "When address is" step?' + properties = { + 'resolvable': address.is_resolvable, + 'reachable': address.is_reachable + } + assert expected_state in properties, f'Invalid state for {address.name} ("{expected_state}").' + message = f'Expected the address {address.name} to be {expected_state} but it is not.' + assert properties[expected_state], message + + +@then(parsers.parse('the port is {expected_state}')) +def the_port_is(expected_state, testinfra_bdd_host): + """ + Check the actual state of an address port against an expected state. + + Parameters + ---------- + expected_state : str + The expected state of the port. + + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + If the actual state does not match the state. + """ + port = testinfra_bdd_host.port + assert port, 'Port is not set. Did you miss a "When the address and port" step?' + properties = { + 'reachable': port.is_reachable + } + assert expected_state in properties, f'Unknown Port property ("{expected_state}").' + message = f'{testinfra_bdd_host.address.name}:{testinfra_bdd_host.port_number} is unreachable.' + assert properties['reachable'], message diff --git a/testinfra_bdd/then/command.py b/testinfra_bdd/then/command.py new file mode 100644 index 0000000..f33add3 --- /dev/null +++ b/testinfra_bdd/then/command.py @@ -0,0 +1,124 @@ +"""Then command fixtures for testinfra-bdd.""" +import re + +from pytest_bdd import parsers +from pytest_bdd import then + + +@then(parsers.parse('the command {command} exists in path')) +@then(parsers.parse('the command "{command}" exists in path')) +def check_command_exists_in_path(command, testinfra_bdd_host): + """ + Assert that a specified command is present on the host path. + + Parameters + ---------- + command : str + The name of the command to check for. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the command is not found on the path. + """ + message = f'Unable to find the command "{command}" on the path.' + assert testinfra_bdd_host.host.exists(command), message + + +@then(parsers.parse('the command {stream_name} contains "{text}"')) +def check_command_stream_contains(stream_name, text, testinfra_bdd_host): + """ + Check that the stdout or stderr stream contains a string. + + Parameters + ---------- + stream_name : str + The name of the stream to check. Must be "stdout" or "stderr". + text : str + The text to search for. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the specified stream does not contain the expected text. + """ + stream = testinfra_bdd_host.get_stream_from_command(stream_name) + message = f'The string "{text}" was not found in the {stream_name} ("{stream}") of the command.' + assert text in stream, message + + +@then(parsers.parse('the command {stream_name} contains the regex "{pattern}"')) +def check_command_stream_contains_the_regex(stream_name, pattern, testinfra_bdd_host): + """ + Check that the stdout or stderr stream matches a regular expression pattern. + + Parameters + ---------- + stream_name : str + The name of the stream to be checked. Must be stdout or stderr. + pattern : str + The pattern to search for in the stream. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the specified stream does not match the pattern. + ValueError + When the stream name is not recognized. + """ + stream = testinfra_bdd_host.get_stream_from_command(stream_name) + message = f'The regex "{pattern}" is not found in the {stream_name} "{stream}".' + # The parsers.parse function escapes the parsed string. We need to clean it up before using it. + pattern = pattern.encode('utf-8').decode('unicode_escape') + prog = re.compile(pattern) + assert prog.search(stream) is not None, message + + +@then(parsers.parse('the command return code is {expected_return_code:d}')) +def check_command_return_code(expected_return_code, testinfra_bdd_host): + """ + Check that the expected return code from a command matches the actual return code. + + Parameters + ---------- + expected_return_code : int + The expected return code (e.g. zero/0). + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the actual return code does not match the expected return code. + """ + cmd = testinfra_bdd_host.command + actual_return_code = cmd.rc + message = f'Expected a return code of {expected_return_code} but got {actual_return_code}.' + assert expected_return_code == actual_return_code, message + + +@then(parsers.parse('the command {stream_name} is empty')) +def command_stream_is_empty(stream_name, testinfra_bdd_host): + """ + Check that the specified command stream is empty. + + Parameters + ---------- + stream_name : str + The name of the stream to be checked. Must be stdout or stderr. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the specified stream does not match the pattern. + """ + stream = testinfra_bdd_host.get_stream_from_command(stream_name) + assert not stream, f'Expected {stream_name} to be empty ("{stream}").' diff --git a/testinfra_bdd/then/file.py b/testinfra_bdd/then/file.py new file mode 100644 index 0000000..393027d --- /dev/null +++ b/testinfra_bdd/then/file.py @@ -0,0 +1,198 @@ +"""Then file fixtures for testinfra-bdd.""" +import re + +from pytest_bdd import ( + then, + parsers +) + +from testinfra_bdd.exception_message import exception_message + + +@then(parsers.parse('the file contents contains "{text}"')) +def the_file_contents_contains_text(text, testinfra_bdd_host): + """ + Check if the file contains a string. + + Parameters + ---------- + text : str + The string to search for in the file content. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the file does not contain the string. + """ + file = testinfra_bdd_host.file + assert file.contains(text), f'The file {testinfra_bdd_host.hostname}:{file.path} does not contain "{text}".' + + +@then(parsers.parse('the file contents contains the regex "{pattern}"')) +def the_file_contents_matches_the_regex(pattern, testinfra_bdd_host): + """ + Check if the file contains matches a regex pattern. + + Parameters + ---------- + pattern : str + The regular expression to match against the file content. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the regex does not match the file content. + """ + file = testinfra_bdd_host.file + file_name = f'{testinfra_bdd_host.hostname}:{file.path}' + message = f'The regex "{pattern}" does not match the content of {file_name} ("{file.content_string}").' + # The parsers.parse function escapes the parsed string. We need to clean it up before using it. + pattern = pattern.encode('utf-8').decode('unicode_escape') + assert re.search(pattern, file.content_string) is not None, message + + +@then(parsers.parse('the file is {expected_status}')) +def the_file_status(expected_status, testinfra_bdd_host): + """ + Check if the file is present or absent. + + Parameters + ---------- + expected_status : str + Should be present or absent. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the expected status does not match the actual status. + """ + the_file_property_is('state', expected_status, testinfra_bdd_host) + + +@then(parsers.parse('the file {property_name} is {expected_value}')) +def the_file_property_is(property_name, expected_value, testinfra_bdd_host): + """ + Check the property of a file. + + Parameters + ---------- + property_name : str + The name of the property to compare. + expected_value : str + The value that is expected. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + If the actual value does not match the expected value. + """ + (actual_value, exception_message) = get_file_actual_state( + testinfra_bdd_host.file, + property_name, + expected_value + ) + assert actual_value == expected_value, exception_message + + +def get_file_actual_state(file, property_name, expected_state): + """ + Get the actual state of a file given the package and the expected state. + + Parameters + ---------- + file : testinfra.File + The file to be checked. + property_name : str + The name of the property to check (e.g. state). + expected_state : str + The expected state. + + Returns + ------- + tuple + str + The actual state (e.g. absent, latest, present or superseded). + str + A suitable message if the actual state doesn't match the actual state. + """ + properties = get_file_properties(file) + assert property_name in properties, f'Unknown user property "{property_name}".' + actual_state = properties[property_name] + return actual_state, exception_message(f'File {file.path} {property_name}', actual_state, expected_state) + + +def get_file_properties(file): + """ + Get the properties of the file. + + Parameters + ---------- + file : testinfra.File + The file to be checked. + + Returns + ------- + dict + A dictionary of the properties. + """ + assert file, 'File not set. Have you missed a "When file is" step?' + properties = { + 'group': None, + 'mode': None, + 'owner': None, + 'state': 'absent', + 'type': None, + 'user': None + } + + if file.exists: + properties = { + 'group': file.group, + 'mode': '0o%o' % file.mode, + 'owner': file.user, + 'state': 'present', + 'type': get_file_type(file), + 'user': file.user + } + + return properties + + +def get_file_type(file): + """ + Get the file type. + + Parameters + ---------- + file : testinfra.File + The file to be checked. + + Returns + ------- + str + The type of file. + """ + file_type = None + + type_lookup = { + 'file': file.is_file, + 'directory': file.is_directory, + 'pipe': file.is_pipe, + 'socket': file.is_socket, + 'symlink': file.is_symlink + } + + for key in type_lookup: + if type_lookup[key]: + file_type = key + break + + return file_type diff --git a/testinfra_bdd/then/group.py b/testinfra_bdd/then/group.py new file mode 100644 index 0000000..bd4ebd7 --- /dev/null +++ b/testinfra_bdd/then/group.py @@ -0,0 +1,59 @@ +"""Then file fixtures for testinfra-bdd.""" +from pytest_bdd import ( + then, + parsers +) + + +@then(parsers.parse('the group {property_name} is {expected_value}')) +def the_group_property_is(property_name, expected_value, testinfra_bdd_host): + """ + Check the property of a group. + + Parameters + ---------- + property_name : str + The name of the property to compare. + expected_value : str + The value that is expected. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + If the actual value does not match the expected value. + """ + group = testinfra_bdd_host.group + assert group, 'Group not set. Have you missed a "When group is" step?' + + properties = { + 'gid': None, + 'state': 'absent' + } + + if group.exists: + properties = { + 'gid': str(group.gid), + 'state': 'present' + } + + assert property_name in properties, f'Unknown group property ({property_name}).' + actual_value = properties[property_name] + message = f'Expected group property to be {expected_value} but it was {actual_value}.' + assert actual_value == expected_value, message + + +@then(parsers.parse('the group is {expected_state}')) +def check_the_group_state(expected_state, testinfra_bdd_host): + """ + Check that the actual state of a group matches the expected state. + + Parameters + ---------- + expected_state : str + The expected state (e.g. absent or present). + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + """ + the_group_property_is('state', expected_state, testinfra_bdd_host) diff --git a/testinfra_bdd/then/package.py b/testinfra_bdd/then/package.py new file mode 100644 index 0000000..5a71c9c --- /dev/null +++ b/testinfra_bdd/then/package.py @@ -0,0 +1,42 @@ +"""Then system package fixtures for testinfra-bdd.""" +from pytest_bdd import ( + then, + parsers +) + + +@then(parsers.parse('the package state is {expected_status}')) +@then(parsers.parse('the package is {expected_status}')) +def the_package_status_is(expected_status, testinfra_bdd_host): + """ + Check the status of a package (installed/absent). + + Parameters + ---------- + expected_status : str + Can be absent, installed or present. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the package is not in the expected state. + """ + status_lookup = { + 'absent': False, + 'installed': True, + 'present': True + } + expected_to_be_installed = status_lookup[expected_status] + pkg = testinfra_bdd_host.package + actual_status = pkg.is_installed + + if expected_to_be_installed: + message = f'Expected {pkg.name} to be {expected_status} on {testinfra_bdd_host.hostname} but it is absent.' + + if actual_status: + message = f'Expected {pkg.name} to be absent on {testinfra_bdd_host.hostname} ' + message += 'but it is installed ({pkg.version}).' + + assert actual_status == expected_to_be_installed, message diff --git a/testinfra_bdd/then/pip.py b/testinfra_bdd/then/pip.py new file mode 100644 index 0000000..449e55b --- /dev/null +++ b/testinfra_bdd/then/pip.py @@ -0,0 +1,159 @@ +"""Then pip package fixtures for testinfra-bdd.""" +from pytest_bdd import ( + then, + parsers +) + +from testinfra_bdd.exception_message import exception_message + + +def check_entry_requirements(pip_package, expected_state): + """ + Check that the entry requirements are met for the test. + + Parameters + ---------- + pip_package : testinfra.Pip + The Pip package to be checked. + expected_state : str + The expected state. + + Raises + ------ + ValueError + If the expected state is invalid. + RuntimeError + If the Pip package has not been set. + """ + valid_expected_states = [ + 'absent', + 'latest', + 'present', + 'superseded' + ] + + if expected_state not in valid_expected_states: + raise ValueError(f'Unknown expected state "{expected_state}" for a Pip package.') + elif not pip_package: + raise RuntimeError('Pip package not set. Have you missed a "When pip package is" step?') + + +def get_pip_package_actual_state(pip_package, expected_state, host): + """ + Get the actual state of a Pip package given the package and the expected state. + + Parameters + ---------- + pip_package : testinfra.Pip + The Pip package to be checked. + expected_state : str + The expected state. + host : testinfra.host.Host + The host to be checked against. + + Returns + ------- + tuple + str + The actual state (e.g. absent, latest, present or superseded). + str + A suitable message if the actual state doesn't match the actual state. + """ + state_checks = [ + 'absent', + 'present' + ] + + check_entry_requirements(pip_package, expected_state) + + if expected_state in state_checks: + actual_state = 'absent' + + if pip_package.is_installed: + actual_state = 'present' + + return actual_state, exception_message( + f'Pip package {pip_package.name}', + actual_state, + expected_state + ) + + outdated_packages = host.pip.get_outdated_packages() + + if pip_package.name in outdated_packages: + actual_state = 'superseded' + else: + actual_state = 'latest' + + return actual_state, exception_message(f'Pip package {pip_package.name}', actual_state, expected_state) + + +@then('the pip check is OK') +def the_pip_check_is_ok(testinfra_bdd_host): + """ + Verify installed packages have compatible dependencies. + + Parameters + ---------- + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the packages have incompatible dependencies. + """ + host = testinfra_bdd_host.host + cmd = host.pip.check() + message = f'Incompatible Pip packages - {cmd.stdout} {cmd.stderr}' + assert cmd.rc == 0, message + + +@then(parsers.parse('the pip package state is {expected_state}')) +@then(parsers.parse('the pip package is {expected_state}')) +def the_pip_package_state_is(expected_state, testinfra_bdd_host): + """ + Check the state of a Pip package. + + Parameters + ---------- + expected_state : str + The expected state of the package. Can be absent, latest or installed. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the actual state doesn't match the expected state. + """ + (actual_state, message) = get_pip_package_actual_state( + testinfra_bdd_host.pip_package, + expected_state, + testinfra_bdd_host.host + ) + assert actual_state == expected_state, message + + +@then(parsers.parse('the pip package version is {expected_version}')) +def the_pip_package_version_is(expected_version, testinfra_bdd_host): + """ + Check the version of a Pip package. + + Parameters + ---------- + expected_version : str + The version of the package that is expected. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the actual version is not the expected version. + """ + pip_package = testinfra_bdd_host.pip_package + assert pip_package, 'Pip package not set. Have you missed a "When pip package is" step?' + actual_version = pip_package.version + message = f'Expected Pip package version to be {expected_version} but it was {actual_version}.' + assert actual_version == expected_version, message diff --git a/testinfra_bdd/then/process.py b/testinfra_bdd/then/process.py new file mode 100644 index 0000000..c72fd2c --- /dev/null +++ b/testinfra_bdd/then/process.py @@ -0,0 +1,31 @@ +"""Then process fixtures for testinfra-bdd.""" +from pytest_bdd import ( + then, + parsers +) + + +@then(parsers.parse('the process count is {expected_count:d}')) +def the_process_count_is(expected_count, testinfra_bdd_host): + """ + Check that the process count matches the expected count. + + Parameters + ---------- + expected_count : int + The expected number of processes. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + If the actual process count does not match the expected count. + """ + specification = testinfra_bdd_host.process_specification + processes = testinfra_bdd_host.processes + assert processes, 'No process set, did you forget a "When process filter" step?' + actual_process_count = len(processes) + message = f'Expected process specification "{specification}" to return {expected_count} ' + message += f'but found {actual_process_count} "{processes}".' + assert actual_process_count == expected_count, message diff --git a/testinfra_bdd/then/service.py b/testinfra_bdd/then/service.py new file mode 100644 index 0000000..552e5be --- /dev/null +++ b/testinfra_bdd/then/service.py @@ -0,0 +1,82 @@ +"""Then service fixtures for testinfra-bdd.""" +from pytest_bdd import then + + +@then('the service is not enabled') +def the_service_is_not_enabled(testinfra_bdd_host): + """ + Check that the service is not enabled. + + Parameters + ---------- + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the service is enabled. + """ + service = testinfra_bdd_host.service + message = f'Expected {service.name} on host {testinfra_bdd_host.hostname} to be disabled, but it is enabled.' + assert not service.is_enabled, message + + +@then('the service is enabled') +def the_service_is_enabled(testinfra_bdd_host): + """ + Check that the service is enabled. + + Parameters + ---------- + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the service is not enabled. + """ + service = testinfra_bdd_host.service + message = f'Expected {service.name} on host {testinfra_bdd_host.hostname} to be enabled, but it is disabled.' + assert service.is_enabled, message + + +@then('the service is not running') +def the_service_is_not_running(testinfra_bdd_host): + """ + Check that the service is not running. + + Parameters + ---------- + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the service is running. + """ + service = testinfra_bdd_host.service + message = f'Expected {service.name} on host {testinfra_bdd_host.hostname} to not be running.' + assert not service.is_running, message + + +@then('the service is running') +def the_service_is_running(testinfra_bdd_host): + """ + Check that the service is running. + + Parameters + ---------- + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + When the service is not running. + """ + service = testinfra_bdd_host.service + message = f'Expected {service.name} on host {testinfra_bdd_host.hostname} to be running.' + assert service.is_running, message diff --git a/testinfra_bdd/then/socket.py b/testinfra_bdd/then/socket.py new file mode 100644 index 0000000..ea2e9d1 --- /dev/null +++ b/testinfra_bdd/then/socket.py @@ -0,0 +1,34 @@ +"""Then socket fixtures for testinfra-bdd.""" +from pytest_bdd import ( + then, + parsers +) + + +@then(parsers.parse('the socket is {expected_state}')) +def the_socket_is(expected_state, testinfra_bdd_host): + """ + Check the state of a socket. + + Parameters + ---------- + expected_state : str + The expected state of the socket. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + If the actual state does not match the state. + """ + socket = testinfra_bdd_host.socket + socket_url = testinfra_bdd_host.socket_url + actual_state = 'not listening' + assert socket, 'Socket is not set. Have you missed a "When socket is" step?' + + if socket.is_listening: + actual_state = 'listening' + + message = f'Expected socket {socket_url} to be {expected_state} but it is {actual_state}.' + assert actual_state == expected_state, message diff --git a/testinfra_bdd/then/user.py b/testinfra_bdd/then/user.py new file mode 100644 index 0000000..aff84e9 --- /dev/null +++ b/testinfra_bdd/then/user.py @@ -0,0 +1,65 @@ +"""Then user fixtures for testinfra-bdd.""" +from pytest_bdd import ( + then, + parsers +) + + +@then(parsers.parse('the user {property_name} is {expected_value}')) +def the_user_property_is(property_name, expected_value, testinfra_bdd_host): + """ + Check the property of a user. + + Parameters + ---------- + property_name : str + The name of the property to compare. + expected_value : str + The value that is expected. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + + Raises + ------ + AssertError + If the actual value does not match the expected value. + """ + user = testinfra_bdd_host.user + assert user, 'User not set. Have you missed a "When user is" step?' + + if testinfra_bdd_host.user.exists: + actual_state = 'present' + properties = { + 'gid': str(user.gid), + 'group': user.group, + 'home': user.home, + 'shell': user.shell, + 'state': actual_state, + 'uid': str(user.uid) + } + else: + actual_state = 'absent' + properties = { + 'state': actual_state + } + + assert property_name in properties, f'Unknown user property "{property_name}".' + actual_value = properties[property_name] + message = f'Expected {property_name} for user {user.name} to be "{expected_value}" ' + message += f'but it was "{actual_value}".' + assert actual_value == expected_value, message + + +@then(parsers.parse('the user is {expected_state}')) +def check_the_user_state(expected_state, testinfra_bdd_host): + """ + Check that the actual state of a user matches the expected state. + + Parameters + ---------- + expected_state : str + The expected state (e.g. absent or present). + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + """ + the_user_property_is('state', expected_state, testinfra_bdd_host) diff --git a/testinfra_bdd/when.py b/testinfra_bdd/when.py new file mode 100644 index 0000000..af19b69 --- /dev/null +++ b/testinfra_bdd/when.py @@ -0,0 +1,40 @@ +"""The when steps of testinfra-bdd.""" +import pytest +from pytest_bdd import parsers +from pytest_bdd import when + + +@when(parsers.parse('the {resource_type} is {resource_name}')) +@when(parsers.parse('the {resource_type} is "{resource_name}"')) +def the_resource_type_is(resource_type, resource_name, testinfra_bdd_host): + """ + Get a resource of a specified type from the system under test. + + Parameters + ---------- + resource_type : str + The type of the resource. + resource_name : str + The name of the resource. + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + """ + testinfra_bdd_host.get_resource_from_host(resource_type, resource_name) + + +@when(parsers.parse('the system property {property_name} is not "{expected_value}" skip tests')) +@when(parsers.parse('the system property {property_name} is not {expected_value} skip tests')) +def skip_tests_if_system_info_does_not_match(property_name, expected_value, testinfra_bdd_host): + """ + Skip tests if a system property does not patch the expected value. + + Parameters + ---------- + property_name : str + expected_value : str + testinfra_bdd_host : testinfra_bdd.fixture.TestinfraBDD + The test fixture. + """ + actual_value = testinfra_bdd_host.get_host_property(property_name) + if actual_value != expected_value: + pytest.skip(f'System {property_name} is {actual_value} which is not {expected_value}.') diff --git a/tests/step_defs/test_example.py b/tests/step_defs/test_example.py index 0840ccf..a94d269 100644 --- a/tests/step_defs/test_example.py +++ b/tests/step_defs/test_example.py @@ -2,17 +2,17 @@ import os import pytest +import testinfra_bdd + from pytest_bdd import given from pytest_bdd import scenarios -import testinfra_bdd.plugins - scenarios('../features/example.feature') # Ensure that the PyTest fixtures provided in testinfra-bdd are available to # your test suite. -pytest_plugins = testinfra_bdd.plugins.add_plugins() +pytest_plugins = testinfra_bdd.PYTEST_MODULES @given('on GitHub Actions we skip tests') diff --git a/tests/step_defs/test_issue21.py b/tests/step_defs/test_issue21.py index 402a173..a364eb5 100644 --- a/tests/step_defs/test_issue21.py +++ b/tests/step_defs/test_issue21.py @@ -1,4 +1,5 @@ """Fix Issue 21 feature tests.""" +import testinfra_bdd from pytest_bdd import scenarios @@ -6,4 +7,4 @@ # Ensure that the PyTest fixtures provided in testinfra-bdd are available to # your test suite. -pytest_plugins = ['testinfra_bdd'] +pytest_plugins = testinfra_bdd.PYTEST_MODULES diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 796fc41..46ff645 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,7 +1,9 @@ """Test exceptions are raised as expected.""" import pytest import testinfra_bdd -import testinfra_bdd.fixture + +from testinfra_bdd.then.pip import the_pip_package_state_is +from testinfra_bdd import get_host_fixture def test_invalid_resource_type(): @@ -9,7 +11,7 @@ def test_invalid_resource_type(): exception_raised = False try: - host = testinfra_bdd.fixture.get_host_fixture('docker://sut') + host = testinfra_bdd.get_host_fixture('docker://sut') host.get_resource_from_host('foo', 'foo') except ValueError as ex: exception_raised = True @@ -23,7 +25,7 @@ def test_invalid_command_stream_name(): exception_raised = False try: - host = testinfra_bdd.fixture.get_host_fixture('docker://sut') + host = get_host_fixture('docker://sut') host.get_resource_from_host('command', 'ls') host.get_stream_from_command('foo') except ValueError as ex: @@ -38,7 +40,7 @@ def test_unready_host(): exception_raised = False try: - testinfra_bdd.fixture.get_host_fixture('docker://foo', 1) + get_host_fixture('docker://foo', 1) except AssertionError as ex: exception_raised = True assert str(ex) == 'The host docker://foo is not ready within 1 seconds.' @@ -57,13 +59,13 @@ def test_unready_host(): def test_pip_package(pip, expected_state, expected_exception_message): """Test that a superseded pip package is identified.""" exception_raised = False - host = testinfra_bdd.fixture.get_host_fixture('docker://sut') + host = get_host_fixture('docker://sut') if pip: host.get_resource_from_host('pip package', pip) try: - testinfra_bdd.the_pip_package_state_is(expected_state, host) + the_pip_package_state_is(expected_state, host) except (AssertionError, RuntimeError, ValueError) as ex: exception_raised = True assert str(ex) == expected_exception_message @@ -84,7 +86,7 @@ def test_invalid_process_specifications(process_specification): expected_message = f'Unable to parse process filters "{process_specification}".' try: - host = testinfra_bdd.fixture.get_host_fixture('docker://sut') + host = get_host_fixture('docker://sut') host.get_resource_from_host('process filter', process_specification) except ValueError as ex: exception_raised = True @@ -110,7 +112,7 @@ def test_invalid_addr_and_port_specifications(specification): } try: - host = testinfra_bdd.fixture.get_host_fixture('docker://sut') + host = get_host_fixture('docker://sut') host.get_resource_from_host('address and port', specification) except ValueError as ex: exception_raised = True From 0e1c245a9ca8155e5d09980bf432982882b00b67 Mon Sep 17 00:00:00 2001 From: Ben Dalling Date: Thu, 21 Jul 2022 21:46:50 +0100 Subject: [PATCH 5/5] fix: dev: Make imports consistent. --- .codeclimate.yml | 2 -- tests/test_exceptions.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index a6beeba..76004c5 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -44,8 +44,6 @@ checks: threshold: # language-specific defaults. overrides affect all languages. plugins: - bandit: - enabled: true markdownlint: enabled: true radon: diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 46ff645..ae64467 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,6 +1,5 @@ """Test exceptions are raised as expected.""" import pytest -import testinfra_bdd from testinfra_bdd.then.pip import the_pip_package_state_is from testinfra_bdd import get_host_fixture @@ -11,7 +10,7 @@ def test_invalid_resource_type(): exception_raised = False try: - host = testinfra_bdd.get_host_fixture('docker://sut') + host = get_host_fixture('docker://sut') host.get_resource_from_host('foo', 'foo') except ValueError as ex: exception_raised = True