feat: add --workers flag for concurrent scenario execution#4616
Merged
feat: add --workers flag for concurrent scenario execution#4616
Conversation
Introduce a new `--workers N` CLI option (supports integer, `cpus`, `cpus-1`) that runs collection scenarios concurrently using a ProcessPoolExecutor. The default scenario's create/destroy lifecycle runs serially in the main process while worker processes handle individual scenario test sequences. Key changes: - New `src/molecule/worker.py` with `run_one_scenario` and `run_scenarios_parallel` functions - `--continue-on-failure` flag to run all scenarios even after failures (default is fail-fast) - `--parallel` flag marked deprecated in help text with runtime warning - State file loading made resilient to incomplete/stale state data - Worker processes explicitly set MOLECULE_PROJECT_DIRECTORY and chdir to handle Python 3.14's forkserver multiprocessing start method - Updated docs/guides/parallel.md with new --workers documentation - Comprehensive unit tests (tests/unit/test_worker.py) and integration tests (tests/integration/test_workers.py) with ansible-native collection generator Co-authored-by: Cursor <[email protected]>
With shared_state, all scenarios share a single state file. The first
scenario's prepare sets prepared=True, causing all subsequent scenarios
to skip their prepare playbook ("Skipping, instances already prepared").
This is wrong when each scenario has its own prepare playbook that seeds
different test data.
Fix by injecting force=True into worker command_args so Prepare.execute()
bypasses the shared "prepared" flag. Integration tests now assert that
each scenario's prepare actually executes in both serial and parallel
modes via unique STEP_PREPARE_SCENARIO markers in playbook output.
Co-authored-by: Cursor <[email protected]>
The unit tests for run_one_scenario set MOLECULE_PROJECT_DIRECTORY in os.environ (via the function under test) but didn't clean it up. This leaked '/path/to' into integration test subprocesses, causing workers to crash with "No such file or directory: '/path/to'". Fix by wrapping unit tests with @patch.dict(os.environ) to isolate env changes, and stripping MOLECULE_PROJECT_DIRECTORY from the integration test subprocess environment as a safety net. Co-authored-by: Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
- Add failure output formatting with bordered blocks showing "Failed: scenario > step" after destroy phase completes - Suppress verbose ansible output in workers by default (MOLECULE_QUIET_ANSIBLE), show only on -v - Capture ansible stdout/stderr on failure via ScenarioFailureError - Refactor CommandBorders into reusable module-level functions - Rewrite unit tests: remove classes, use monkeypatch/mocker instead of @patch decorators, extract shared helpers - Flatten integration tests: remove class, use static fixtures from tests/fixtures/integration/test_workers/ - Consolidate 9 integration tests into 4 for faster execution - Fix all lint issues (ruff, pylint, pydoclint, mypy, cspell) - Add R0401 cyclic-import to pylint global disable list Co-authored-by: Cursor <[email protected]>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces concurrent scenario execution for Ansible collections via the --workers N flag for molecule test, check, and destroy commands. The implementation uses Python's ProcessPoolExecutor to run non-default scenarios in parallel while the default scenario's infrastructure lifecycle (create/destroy) remains serial. The existing sequential code path is completely preserved when --workers is 1 or omitted.
Changes:
- Added
--workersand--continue-on-failureCLI options with support for integer values,cpus, orcpus-1for worker count resolution - Implemented parallel execution in a new
worker.pymodule with fail-fast and continue-on-failure modes - Extended error handling to capture and display Ansible output from failed worker scenarios
Reviewed changes
Copilot reviewed 43 out of 49 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/molecule/worker.py |
New module implementing parallel scenario execution with process pool management |
src/molecule/command/base.py |
Added early return in _run_scenarios to delegate to parallel path when workers > 1 |
src/molecule/click_cfg.py |
Added workers and continue_on_failure CLI options plus resolve_workers() function |
src/molecule/command/test.py |
Integrated new CLI options and deprecation warning for --parallel |
src/molecule/command/check.py |
Integrated new CLI options and deprecation warning for --parallel |
src/molecule/command/destroy.py |
Integrated new CLI options and deprecation warning for --parallel |
src/molecule/types.py |
Extended CommandArgs TypedDict with workers and continue_on_failure fields |
src/molecule/state.py |
Fixed state loading to merge with defaults for backward compatibility |
src/molecule/exceptions.py |
Added ansible_output parameter to ScenarioFailureError |
src/molecule/provisioner/ansible_playbook.py |
Captures ansible stdout+stderr and passes to exception |
src/molecule/app.py |
Added MOLECULE_QUIET_ANSIBLE env var handling to suppress worker output |
src/molecule/ansi_output.py |
Extracted border rendering functions for reuse in worker failure output |
pyproject.toml |
Disabled R0401 pylint rule for intentional lazy imports |
docs/guides/parallel.md |
Updated documentation for --workers and deprecated --parallel |
tests/unit/test_worker.py |
Unit tests for worker validation, execution, and parallel orchestration |
tests/unit/test_click_cfg.py |
Unit tests for resolve_workers() and new CLI options |
tests/integration/test_workers.py |
End-to-end integration tests with static fixture collection |
tests/fixtures/integration/test_workers/* |
Static test collection with 6 scenarios for integration testing |
Comments suppressed due to low confidence (3)
tests/unit/test_worker.py:1
- The assertion
call_count >= 2is weak and may pass even if the behavior is incorrect. Consider asserting the exact expected call count (2 in this case) or verifying that both specific results were appended. This would make the test more precise and less prone to false positives.
tests/integration/test_workers.py:1 - Using a hardcoded
/tmp/path in test fixtures can lead to security issues and conflicts in shared environments. Consider usingtempfile.mkdtemp()or a test-specific temporary directory to ensure isolation and avoid predictable paths.
src/molecule/click_cfg.py:1 - This test is checking implementation details (the presence of 'EXPERIMENTAL:' prefix) rather than the actual behavior of the
experimentalflag. Consider testing that the flag is correctly set toTrueon the option object instead of parsing generated help text.
"""New Click Configuration System with CliOption Architecture.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
alisonlhart
approved these changes
Feb 25, 2026
rija
added a commit
to rmenage/ansible-role-hardened-docker
that referenced
this pull request
Mar 12, 2026
This MR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [molecule](https://github.com/ansible-community/molecule) ([changelog](https://github.com/ansible-community/molecule/releases)) | `>=26.2.0,<26.3.0` → `>=26.3.0,<26.4.0` |  |  | --- ### Release Notes <details> <summary>ansible-community/molecule (molecule)</summary> ### [`v26.3.0`](https://github.com/ansible/molecule/releases/tag/v26.3.0) [Compare Source](ansible/molecule@v26.2.0...v26.3.0) #### Features - feat: add --workers flag for concurrent scenario execution ([#​4616](ansible/molecule#4616)) [@​cidrblock](https://github.com/cidrblock) #### Maintenance - chore(deps): update pep621 ([#​4619](ansible/molecule#4619)) @​[renovate\[bot\]](https://github.com/apps/renovate) - chore(deps): update all dependencies ([#​4618](ansible/molecule#4618)) @​[renovate\[bot\]](https://github.com/apps/renovate) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this MR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box --- This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi45OS4wIiwidXBkYXRlZEluVmVyIjoiNDIuOTkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Refs: rmenage/ansible-role-hardened-docker!27 See-also:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
--workers Ntomolecule test,check, anddestroyfor running collection scenarios concurrently usingconcurrent.futures.ProcessPoolExecutor. The existing sequential code path is completely untouched when--workersis absent or set to 1.Design
The parallel path runs only in collection mode with
shared_state. The main process handles the default scenario'screateanddestroylifecycle serially. All non-default scenarios are submitted to a process pool and execute theirprepare → converge → idempotence → verify → cleanupsequence in isolated workers. Results are collected via futures.Failure modes:
executor.shutdown(cancel_futures=True)is called, remaining workers are cancelled, destroy runs, and collected failures are reported.--continue-on-failure: all scenarios run to completion, failures are buffered and displayed after destroy.Ansible output is suppressed in workers by default (Molecule INFO logs still stream freely). On failure, the captured stdout+stderr is displayed in bordered blocks after the default scenario's destroy phase completes. Pass
-vto stream all ansible output from workers.File-by-file changes
Core runtime (minimal changes, no behavioral change for existing users)
src/molecule/command/base.py— 8 lines added._run_scenariosgains an early return: ifworkers > 1, it lazy-imports and delegates torun_scenarios_parallel. The entire existing sequential path is untouched; it only runs whenworkers <= 1(the default).src/molecule/app.py— 4 lines changed.run_commandchecksMOLECULE_QUIET_ANSIBLEenv var. When set,teeis disabled (ansible stdout is captured but not streamed) andCommandBordersinitialization is skipped. This env var is only set by worker processes; normal runs are unaffected.src/molecule/exceptions.py— 3 lines added.ScenarioFailureError.__init__accepts an optionalansible_outputstring to carry captured ansible stdout+stderr from the failing step. Fully backward-compatible; defaults to"".src/molecule/provisioner/ansible_playbook.py— 2 lines added. On non-zero ansible exit, the combinedresult.stdout + result.stderris passed asansible_outputtoScenarioFailureError. No change to the raise itself or any other behavior.src/molecule/state.py— 4 lines changed._get_datanow merges loaded state on top of defaults instead of returning loaded state directly. Handles the case where a state file written by an older molecule version is missing keys that newer code expects. Defensive fix; no impact on current sequential runs.src/molecule/types.py— 6 lines added.CommandArgsTypedDict extended withworkers: intandcontinue_on_failure: bool. Non-breaking sincetotal=False.New module
src/molecule/worker.py— 278 lines (new). Contains:run_one_scenario(): reconstructs aConfigfrom picklable args in a worker process, runsexecute_scenariowithshared_state=True, returns a 4-tuple of(ScenarioResults, error_msg, ansible_output, failed_step).run_scenarios_parallel(): orchestrates the full lifecycle — default create, prerun, pool dispatch, result collection, destroy, failure reporting.validate_worker_args(): ensures--workers > 1is only used in collection mode and not with--destroy=never._print_failed_output(): renders bordered failure blocks to stderr after destroy.Forces
prepare=Trueon every worker Config to avoid the shared-state "already prepared" flag race condition. Comment in code explains why this is necessary vs sequential mode.CLI plumbing
src/molecule/click_cfg.py— 56 lines added. Newworkersandcontinue_on_failureCLI options. Newresolve_workers()function that parses"cpus","cpus-1", or an integer string.src/molecule/command/test.py,check.py,destroy.py— Each adds--workersand--continue-on-failureto its option list, callsresolve_workers(), emits aDeprecationWarningif--parallelis used.Output formatting
src/molecule/ansi_output.py— 3 new module-level functions extracted fromCommandBorders:create_border_header(),create_border_footer(),write_bordered_block(). Used by_print_failed_outputinworker.pyto render failure output. ExistingCommandBordersclass is untouched.Documentation
docs/guides/parallel.md— Updated to document--workers,--continue-on-failure, and the deprecation of--parallel.Configuration
pyproject.toml— 1 line: addedR0401(cyclic-import) to pylint's global disable list. Thebase.py → worker.pylazy import is intentional and unavoidable..config/dictionary.txt— cspell dictionary update.Tests
tests/unit/test_worker.py— 430 lines (new). Unit tests forvalidate_worker_args,run_one_scenario, andrun_scenarios_parallel. Usesmonkeypatch/mocker.patch(no@patchdecorators, no test classes). Covers success, failure, fail-fast, continue-on-failure, and quiet-ansible env var behavior.tests/unit/test_click_cfg.py— 118 lines added. Tests forresolve_workers: integers,cpus,cpus-1, case insensitivity,Nonefallback, invalid input, zero, negative values.tests/integration/test_workers.py— 168 lines (new). 4 end-to-end tests against a static fixture collection (tests/fixtures/integration/test_workers/): parallel success with output suppression and report, verbose mode ansible streaming, continue-on-failure with grouped failure output, and fail-fast withcpus-1resolution.tests/fixtures/integration/test_workers/— Static ansible-native collection with a default scenario (create/destroy) and 6 lightweight test scenarios usingansible.builtin.debugtasks.Test plan
tox -e lint— all 22 pre-commit checks pass (ruff, pylint, mypy, pydoclint, cspell, etc.)tox -e py— unit and integration tests pass--workers 1or omitted) is completely unaffectedMade with Cursor