From ff3c5eddf897635c60a1e9e7ed431a5be0b9d549 Mon Sep 17 00:00:00 2001 From: Alex Sullivan <115666116+asullivan-blze@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:01:02 -0700 Subject: [PATCH 1/2] [SVRENG-609] boardwalk (bug): correct job class initialization Boardwalk release 0.8.22 introduced a bug where options supplied to Jobs would cause a TypeError, due to the options parameter not being included in the declaration of the init method. This has been fixed, and to guard against similar mishaps, a test suite to exercise these regressions was added into the overall test suite, with focus on the options, as well as testing preconditions. --- Makefile | 6 +- pyproject.toml | 2 +- src/boardwalk/ansible.py | 5 +- src/boardwalk/cli_run.py | 2 + src/boardwalk/host.py | 2 + src/boardwalk/manifest.py | 19 +- test/integration/test_workspaces.py | 27 ++ test/server-client/Boardwalkfile.py | 12 +- .../playbook-job-test-echo-variable.yml | 7 + .../playbook-job-test-should-be-skipped.yml | 0 .../playbook-job-test-should-fail.yml | 0 .../playbook-job-test-should-succeed.yml | 0 .../pylib/regression_bz_svreng_609.py | 249 ++++++++++++++++++ 13 files changed, 316 insertions(+), 15 deletions(-) create mode 100644 test/server-client/playbooks/playbook-job-test-echo-variable.yml rename test/server-client/{ => playbooks}/playbook-job-test-should-be-skipped.yml (100%) rename test/server-client/{ => playbooks}/playbook-job-test-should-fail.yml (100%) rename test/server-client/{ => playbooks}/playbook-job-test-should-succeed.yml (100%) create mode 100644 test/server-client/pylib/regression_bz_svreng_609.py diff --git a/Makefile b/Makefile index bbd2fde..4fb50a7 100644 --- a/Makefile +++ b/Makefile @@ -84,10 +84,14 @@ render-d2: .PHONY: test test: test-pytest test-ruff test-pyright test-semgrep -# Run pytest +# Run pytest verbosely if we're running manually, but normally if we're in a CI environment. .PHONY: test-pytest test-pytest: develop +ifndef CI + poetry run pytest --verbose +else poetry run pytest +endif # Run all available Ruff checks .PHONY: test-ruff diff --git a/pyproject.toml b/pyproject.toml index c22acac..d1c4b3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ requires-python = ">=3.11" [tool.poetry] name = "boardwalk" -version = "0.8.22" +version = "0.8.23" description = "Boardwalk is a linear Ansible workflow engine" readme = "README.md" authors = [ diff --git a/src/boardwalk/ansible.py b/src/boardwalk/ansible.py index 3ac998f..7eaaf34 100644 --- a/src/boardwalk/ansible.py +++ b/src/boardwalk/ansible.py @@ -47,7 +47,7 @@ class RunnerKwargs(TypedDict, total=False): suppress_env_files: bool playbook: RunnerPlaybook | AnsibleTasksType verbosity: int - extravars: dict | None + extravars: dict[str, Any] | None class RunnerKwargsEnvvars(TypedDict, total=False): ANSIBLE_BECOME_ASK_PASS: str @@ -120,6 +120,7 @@ def ansible_runner_run_tasks( quiet: bool = True, timeout: int | None = None, verbosity: int = 0, + extra_vars: dict = {}, ) -> ansible_runner.Runner: """ Wraps ansible_runner.run to run Ansible tasks with some defaults for @@ -161,7 +162,7 @@ def ansible_runner_run_tasks( if job_type == boardwalk.manifest.JobTypes.PLAYBOOK: # Executing a (list of) playbook(s) requires some different settings runner_kwargs["limit"] = hosts - runner_kwargs["extravars"] = {"boardwalk_operation": True} + runner_kwargs["extravars"] = {"boardwalk_operation": True} | extra_vars runner_kwargs["playbook"] = tasks output_msg_prefix = f"{hosts}: ansible_runner invocation" diff --git a/src/boardwalk/cli_run.py b/src/boardwalk/cli_run.py index 1e24c77..704431a 100644 --- a/src/boardwalk/cli_run.py +++ b/src/boardwalk/cli_run.py @@ -697,6 +697,7 @@ def execute_workflow_jobs(host: Host, workspace: Workspace, job_kind: str, verbo invocation_msg=f"{job_kind}_{job.job_type.name}_Job_{job.name}", quiet=False, tasks=tasks, + extra_vars=job.options, ) @@ -754,6 +755,7 @@ def execute_host_workflow(host: Host, workspace: Workspace, verbosity: int): ) host.set_remote_state(remote_state, become_password, _check_mode) + logger.success(f"{host.name}: Host completed successfully; wrapping up") if boardwalkd_client: boardwalkd_client.queue_event( WorkspaceEvent( diff --git a/src/boardwalk/host.py b/src/boardwalk/host.py index 8052e81..63b3c2d 100644 --- a/src/boardwalk/host.py +++ b/src/boardwalk/host.py @@ -49,6 +49,7 @@ def ansible_run( check: bool = False, gather_facts: bool = True, quiet: bool = True, + extra_vars: dict = {}, ) -> Runner: """ Wraps ansible_runner_run_tasks for performing Ansible tasks against this host @@ -64,6 +65,7 @@ def ansible_run( check=check, gather_facts=gather_facts, quiet=quiet, + extra_vars=extra_vars, ) def is_locked(self) -> str | bool: diff --git a/src/boardwalk/manifest.py b/src/boardwalk/manifest.py index fe4f3b2..fedf082 100644 --- a/src/boardwalk/manifest.py +++ b/src/boardwalk/manifest.py @@ -154,8 +154,8 @@ def _check_options(self, options: dict[str, Any]): class TaskJob(BaseJob): """Defines a single Job as methods, used to execute Tasks""" - def __init__(self): - super().__init__() + def __init__(self, options: dict[str, Any] = dict()): + super().__init__(options=options) self.job_type = JobTypes.TASK def tasks(self) -> AnsibleTasksType: @@ -171,20 +171,27 @@ class Job(TaskJob): Deprecated in favor of TaskJob. """ - def __init__(self): + def __init__(self, options: dict[str, Any] = dict()): warnings.warn( "The job type Job is deprecated, and will be removed in a future release. Use TaskJob or PlaybookJob, as appropriate.", DeprecationWarning, ) - super().__init__() + super().__init__(options=options) + + def tasks(self) -> AnsibleTasksType: + """Optional user method. Return list of Ansible tasks to run. If an + empty list is returned, then the workflow doesn't connect to a host, + however, any code in this method still runs""" + return [] class PlaybookJob(BaseJob): """Defines a single Job as methods, used to execute Playbooks""" - def __init__(self): - super().__init__() + def __init__(self, options: dict[str, Any] = dict()): + super().__init__(options=options) self.job_type = JobTypes.PLAYBOOK + self.extra_vars = options def tasks(self) -> AnsibleTasksType: """Helper method used to return the contents of the self.playbooks() function.""" diff --git a/test/integration/test_workspaces.py b/test/integration/test_workspaces.py index 9e70175..3a90a0e 100644 --- a/test/integration/test_workspaces.py +++ b/test/integration/test_workspaces.py @@ -1,4 +1,5 @@ import os +import platform import signal from pathlib import Path from typing import Any @@ -16,6 +17,28 @@ pytest.param("ShouldSucceedTestWorkspace", False, ""), pytest.param("ShouldSucceedPlaybookExecutionTestWorkspace", False, ""), pytest.param("UITestVeryVeryLongWorkSpaceNameWorkspace", False, ""), + # Next four are from test/server-client/pylib/regression_bz_svreng_608.py + pytest.param("TaskJobWithOptionsShouldSucceedWorkspace", False, ""), + pytest.param("PlaybookJobWithOptionsShouldSucceedWorkspace", False, ""), + pytest.param( + "TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSXWorkspace", + False, + "", + marks=pytest.mark.skipif( + condition="macos" not in platform.platform().lower(), + reason="Workspace's preconditions depends on the host being MacOS.", + ), + ), + # Technically this one doesn't _fail_, but this lets it fit neatly into the parameterized tests. + pytest.param( + "TaskJobWithPreconditionsShouldBeSkippedIfHostIsMacOSXWorkspace", + True, + "No hosts meet preconditions", + marks=pytest.mark.skipif( + condition="macos" not in platform.platform().lower(), + reason="Workspace's preconditions depends on the host being MacOS.", + ), + ), pytest.param( "ShouldFailTestWorkspace", True, @@ -54,10 +77,13 @@ async def test_development_workspaces( with fail_after(delay=90) as scope: async with await open_process(command=command, env=new_environ) as process: async for text in TextReceiveStream(process.stdout): # type:ignore + # To allow for reading what was received, if the test ends up failing. + print(text) output_stdout.append(text) if failure_expected and "Waiting for release before continuing" in text: process.send_signal(signal.SIGINT) async for text in TextReceiveStream(process.stderr): # type:ignore + print(text) output_stderr.append(text) assert not scope.cancelled_caught @@ -65,4 +91,5 @@ async def test_development_workspaces( if failure_expected: assert failure_msg in "".join(output_stdout) else: + assert "Host completed successfully; wrapping up" in "".join(output_stdout) assert process.returncode == 0 diff --git a/test/server-client/Boardwalkfile.py b/test/server-client/Boardwalkfile.py index 48924e9..c22c220 100644 --- a/test/server-client/Boardwalkfile.py +++ b/test/server-client/Boardwalkfile.py @@ -3,6 +3,8 @@ import os from typing import TYPE_CHECKING +from pylib.regression_bz_svreng_609 import * # noqa: F403 + from boardwalk import PlaybookJob, TaskJob, Workflow, Workspace, WorkspaceConfig, path if TYPE_CHECKING: @@ -147,8 +149,8 @@ class ShouldSucceedPlaybookExecutionTestJob(PlaybookJob): def playbooks(self) -> AnsibleTasksType: return [ - {"ansible.builtin.import_playbook": path("playbook-job-test-should-succeed.yml")}, - {"ansible.builtin.import_playbook": path("playbook-job-test-should-be-skipped.yml")}, + {"ansible.builtin.import_playbook": path("playbooks/playbook-job-test-should-succeed.yml")}, + {"ansible.builtin.import_playbook": path("playbooks/playbook-job-test-should-be-skipped.yml")}, ] @@ -159,7 +161,7 @@ class ShouldFailPlaybookExecutionTestJob(PlaybookJob): def playbooks(self) -> AnsibleTasksType: return [ - {"ansible.builtin.import_playbook": path("playbook-job-test-should-succeed.yml")}, - {"ansible.builtin.import_playbook": path("playbook-job-test-should-be-skipped.yml")}, - {"ansible.builtin.import_playbook": path("playbook-job-test-should-fail.yml")}, + {"ansible.builtin.import_playbook": path("playbooks/playbook-job-test-should-succeed.yml")}, + {"ansible.builtin.import_playbook": path("playbooks/playbook-job-test-should-be-skipped.yml")}, + {"ansible.builtin.import_playbook": path("playbooks/playbook-job-test-should-fail.yml")}, ] diff --git a/test/server-client/playbooks/playbook-job-test-echo-variable.yml b/test/server-client/playbooks/playbook-job-test-echo-variable.yml new file mode 100644 index 0000000..d8cf476 --- /dev/null +++ b/test/server-client/playbooks/playbook-job-test-echo-variable.yml @@ -0,0 +1,7 @@ +--- +- name: Test echoing a set variable + hosts: localhost + tasks: + - name: Tell me my variable, please! + ansible.builtin.debug: + msg: "Your variable is: {{ boardwalk_playbookjob_test_variable }}" diff --git a/test/server-client/playbook-job-test-should-be-skipped.yml b/test/server-client/playbooks/playbook-job-test-should-be-skipped.yml similarity index 100% rename from test/server-client/playbook-job-test-should-be-skipped.yml rename to test/server-client/playbooks/playbook-job-test-should-be-skipped.yml diff --git a/test/server-client/playbook-job-test-should-fail.yml b/test/server-client/playbooks/playbook-job-test-should-fail.yml similarity index 100% rename from test/server-client/playbook-job-test-should-fail.yml rename to test/server-client/playbooks/playbook-job-test-should-fail.yml diff --git a/test/server-client/playbook-job-test-should-succeed.yml b/test/server-client/playbooks/playbook-job-test-should-succeed.yml similarity index 100% rename from test/server-client/playbook-job-test-should-succeed.yml rename to test/server-client/playbooks/playbook-job-test-should-succeed.yml diff --git a/test/server-client/pylib/regression_bz_svreng_609.py b/test/server-client/pylib/regression_bz_svreng_609.py new file mode 100644 index 0000000..d50eff6 --- /dev/null +++ b/test/server-client/pylib/regression_bz_svreng_609.py @@ -0,0 +1,249 @@ +"""Regression testing suite for the issue introduced with 0.8.22 with TaskJobs. + +Explanation: At the release of Boardwalk 0.8.22, there had not been any test +suites/workspaces which actually made use of the `options` that a Job could +have. Options, in this case, referencing additional variables that can be used, +such as when computing preconditions and are typically also passed through to +the underlying `ansible-playbook` invocation, whether through a `set_fact` task, +or via `--extra_vars/-e` with `ansible-runner` if the PlaybookJob is being used. + +The issue here, however, was that we missed having the subclassed TaskJob, (the +deprecated) Job, and the PlaybookJob have the `options` parameter accepted in +the initialization method of the class. + +In the case of PlaybookJobs, however, in the interest of trying to keep the code +simple, we have opted to add an `extra_vars` item onto PlaybookJob class +instances, consisting of the `options` passed to the PlaybookJob. These options +-- now extra variables -- are then passed through to the `ansible-runner` +invocation, and will be supplied to `ansible-playbook` as if they were passed to +the `-e`/`--extra-vars` command-line option. This method of providing extra +variables was selected, as it is not possible to include raw tasks in the same +"playbook before/after a full playbook. As such, the following example _does not +work_, and would result in an error propagating up from Ansible which -- +effectively -- states that tasks cannot be used in a Play (or similar). + +``` +class AnExampleWhichDoesNotWork(PlaybookJob): + def required_options(self) -> tuple[str]: + return ("boardwalk_playbookjob_test_variable",) + + def tasks(self): + return [ + {"set_fact": {"boardwalk_playbookjob_test_variable": + self.options["boardwalk_playbookjob_test_variable"]}}, + {"ansible.builtin.import_playbook": + path("playbooks/playbook-job-test-echo-variable.yml")}, + ] +``` + +Example of the error which led to this discovery: +======== +asullivan@MBP-NT9RPG2XV7 server-client % boardwalk workspace use +TaskJobsWithOptionsShouldSucceedWorkspace 2024-10-25 13:35:55.995 | INFO | +boardwalk.cli:cli:77 - Log level is INFO Traceback (most recent call last): + File "/Users/asullivan/.local/bin/boardwalk", line 10, in + sys.exit(cli()) + ^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/click/core.py", line + 1157, in __call__ + return self.main(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/click/core.py", line + 1078, in main + rv = self.invoke(ctx) + ^^^^^^^^^^^^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/click/core.py", line + 1688, in invoke + return _process_result(sub_ctx.command.invoke(sub_ctx)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/click/core.py", line + 1688, in invoke + return _process_result(sub_ctx.command.invoke(sub_ctx)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/click/core.py", line + 1434, in invoke + return ctx.invoke(self.callback, **ctx.params) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/click/core.py", line + 783, in invoke + return __callback(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/boardwalk/cli_workspace.py", + line 48, in workspace_use + Workspace.use(workspace_name) + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/boardwalk/manifest.py", + line 440, in use + ws = get_ws() + ^^^^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/boardwalk/manifest.py", + line 90, in get_ws + return Workspace.fetch_subclass(active_workspace_name)() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/boardwalk/manifest.py", + line 337, in __init__ + self.cfg = self.config() + ^^^^^^^^^^^^^ + File + "/Users/asullivan/backblaze/github_repos/boardwalk/test/server-client/pylib/regression_svreng_608.py", + line 18, in config + workflow=TaskJobsWithOptionsShouldSucceedWorkflow(), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/asullivan/Library/Application + Support/pipx/venvs/ansible/lib/python3.12/site-packages/boardwalk/manifest.py", + line 223, in __init__ + workflow_jobs = self.jobs() + ^^^^^^^^^^^ + File + "/Users/asullivan/backblaze/github_repos/boardwalk/test/server-client/pylib/regression_svreng_608.py", + line 25, in jobs + ZabbixEnterMaintenance({"zabbix_api_token": zabbix_api_token}), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +TypeError: TaskJob.__init__() takes 1 positional argument but 2 were given +asullivan@MBP-NT9RPG2XV7 server-client % ======== +""" + +from __future__ import annotations + +from boardwalk import Job, PlaybookJob, TaskJob, Workflow, Workspace, WorkspaceConfig, path + +zabbix_api_token = "NotARealToken" + +__all__ = [ + "TaskJobWithOptionsShouldSucceedWorkspace", + "PlaybookJobWithOptionsShouldSucceedWorkspace", + "TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSXWorkspace", + "TaskJobWithPreconditionsShouldBeSkippedIfHostIsMacOSXWorkspace", +] + + +class TaskJobWithOptionsShouldSucceedWorkspace(Workspace): + def config(self): + return WorkspaceConfig( + default_sort_order="ascending", + host_pattern="localhost", + workflow=TaskJobWithOptionsShouldSucceedWorkflow(), + ) + + +class PlaybookJobWithOptionsShouldSucceedWorkspace(Workspace): + def config(self): + return WorkspaceConfig( + default_sort_order="ascending", + host_pattern="localhost", + workflow=PlaybookJobWithOptionsShouldSucceedWorkflow(), + ) + + +class TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSXWorkspace(Workspace): + def config(self): + return WorkspaceConfig( + default_sort_order="ascending", + host_pattern="localhost", + workflow=TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSXWorkflow(), + ) + + +class TaskJobWithPreconditionsShouldBeSkippedIfHostIsMacOSXWorkspace(Workspace): + def config(self): + return WorkspaceConfig( + default_sort_order="ascending", + host_pattern="localhost", + workflow=TaskJobWithPreconditionsShouldBeSkippedIfHostIsMacOSXWorkflow(), + ) + + +class TaskJobWithOptionsShouldSucceedWorkflow(Workflow): + def jobs(self): + return ( + TaskJobWithOption({"zabbix_api_token": zabbix_api_token}), + DeprecatedJobWithOption({"zabbix_api_token": zabbix_api_token}), + ) + + def exit_jobs(self): + return (TaskJobWithOption({"zabbix_api_token": zabbix_api_token}),) + + +class TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSXWorkflow(Workflow): + def jobs(self): + return (TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSX({"target_version": 9001}),) + + def exit_jobs(self): + return (TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSX({"target_version": 9001}),) + + +class TaskJobWithPreconditionsShouldBeSkippedIfHostIsMacOSXWorkflow(Workflow): + def jobs(self): + return (TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSX({"target_version": 1}),) + + def exit_jobs(self): + return (TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSX({"target_version": 1}),) + + +class TaskJobWithPreconditionsShouldSucceedIfHostIsMacOSX(TaskJob): + def required_options(self): + return "target_version" + + def preconditions(self, facts: dict[str, str], inventory_vars: dict[str, str]): + return facts["ansible_distribution"] == "MacOSX" and int(facts["ansible_distribution_major_version"]) < int( + self.options["target_version"] + ) + + def tasks(self): + return [ + {"set_fact": {"target_version": self.options["target_version"]}}, + { + "ansible.builtin.debug": { + "msg": r"Your version of {{ ansible_distribution }} {{ ansible_distribution_major_version }} is less than the targeted version of {{ target_version }}, so you get to see this message!" + } + }, + ] + + +class PlaybookJobWithOptionsShouldSucceedWorkflow(Workflow): + def jobs(self): + return (TestPlaybookJobEchoVariable({"boardwalk_playbookjob_test_variable": zabbix_api_token}),) + + def exit_jobs(self): + return (TestPlaybookJobEchoVariable({"boardwalk_playbookjob_test_variable": zabbix_api_token}),) + + +class DeprecatedJobWithOption(Job): + def required_options(self) -> tuple[str]: + return ("zabbix_api_token",) + + def tasks(self): + return [ + {"set_fact": {"zabbix_api_token": self.options["zabbix_api_token"]}}, + {"ansible.builtin.debug": {"msg": r"{{ zabbix_api_token }}"}}, + ] + + +class TaskJobWithOption(TaskJob): + def required_options(self) -> tuple[str]: + return ("zabbix_api_token",) + + def tasks(self): + return [ + {"set_fact": {"zabbix_api_token": self.options["zabbix_api_token"]}}, + {"ansible.builtin.debug": {"msg": r"{{ zabbix_api_token }}"}}, + ] + + +class TestPlaybookJobEchoVariable(PlaybookJob): + def required_options(self) -> tuple[str]: + return ("boardwalk_playbookjob_test_variable",) + + def tasks(self): + return [ + {"ansible.builtin.import_playbook": path("playbooks/playbook-job-test-echo-variable.yml")}, + ] From 955b636e9351b1dd32b80a7eedf5a8417234a5fd Mon Sep 17 00:00:00 2001 From: Alex Sullivan <115666116+asullivan-blze@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:43:57 -0700 Subject: [PATCH 2/2] Remove shadowed tasks() function, as the superclass has it already. Add a few more manifest tests. --- src/boardwalk/manifest.py | 6 ------ test/boardwalk/test_manifest.py | 35 ++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/boardwalk/manifest.py b/src/boardwalk/manifest.py index fedf082..fabd589 100644 --- a/src/boardwalk/manifest.py +++ b/src/boardwalk/manifest.py @@ -178,12 +178,6 @@ def __init__(self, options: dict[str, Any] = dict()): ) super().__init__(options=options) - def tasks(self) -> AnsibleTasksType: - """Optional user method. Return list of Ansible tasks to run. If an - empty list is returned, then the workflow doesn't connect to a host, - however, any code in this method still runs""" - return [] - class PlaybookJob(BaseJob): """Defines a single Job as methods, used to execute Playbooks""" diff --git a/test/boardwalk/test_manifest.py b/test/boardwalk/test_manifest.py index 2f6cfd0..f582e9c 100644 --- a/test/boardwalk/test_manifest.py +++ b/test/boardwalk/test_manifest.py @@ -1,6 +1,39 @@ import pytest -from boardwalk import Job +from boardwalk import Job, PlaybookJob, TaskJob +from boardwalk.manifest import JobTypes + + +@pytest.mark.parametrize( + ("job_class", "job_type"), + [ + pytest.param(TaskJob, JobTypes.TASK), + pytest.param( + Job, + JobTypes.TASK, + marks=pytest.mark.filterwarnings("ignore:The job type Job is deprecated:DeprecationWarning"), + ), + pytest.param(PlaybookJob, JobTypes.PLAYBOOK), + ], +) +def test_verify_job_types_match_expected_types(job_class, job_type): + job = job_class() + assert job_type == job.job_type + + +@pytest.mark.parametrize( + ("job_class", "function_name"), + [ + pytest.param(TaskJob, "tasks"), + pytest.param(Job, "tasks"), + pytest.param(PlaybookJob, "playbooks"), + pytest.param(PlaybookJob, "tasks"), + ], +) +def test_verify_job_classes_have_expected_task_functions(job_class, function_name): + # The Job type needs to have the expected function name, and also be a callable function + assert hasattr(job_class, function_name) + assert callable(eval(f"{job_class.__name__}.{function_name}")) def test_using_not_differentiated_Job_class_warns_about_deprecation():