Skip to content

Commit b340d0d

Browse files
authored
Improved molecule support (#147)
* Improved molecule support Present molecule scenarios using a fixture.
1 parent 014be7c commit b340d0d

File tree

6 files changed

+180
-12
lines changed

6 files changed

+180
-12
lines changed

README.md

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,41 @@ Install this plugin using `pip`
1919
pip install pytest-ansible
2020
```
2121

22-
## Usage
22+
## Running molecule scenarios using pytest
23+
24+
Molecule scenarios can be tested using 2 different methods if molecule is installed.
25+
26+
**Recommended:**
27+
28+
Add a `test_integration.py` file to the `tests/integration` directory of the ansible collection:
29+
30+
```
31+
"""Tests for molecule scenarios."""
32+
from __future__ import absolute_import, division, print_function
33+
34+
from pytest_ansible.molecule import MoleculeScenario
35+
36+
37+
def test_integration(molecule_scenario: MoleculeScenario) -> None:
38+
"""Run molecule for each scenario.
39+
40+
:param molecule_scenario: The molecule scenario object
41+
"""
42+
proc = molecule_scenario.test()
43+
assert proc.returncode == 0
44+
```
45+
46+
The `molecule_scenario` fixture provides parameterized molecule scenarios discovered in the collection's `extensions/molecule` directory, as well as other directories within the collection.
47+
48+
`molecule test -s <scenario>` will be run for each scenario and a completed subprocess returned from the `test()` call.
49+
50+
**Legacy:**
51+
52+
Run molecule with the `--molecule` command line parameter to inject each molecule directory found in the current working directory. Each scenario will be injected as an external test in the the tests available for pytest. Due to the nature of this approach, the molecule scenarios are not represented as python tests and may not show in the IDE's pytest test tree.
53+
54+
## Fixtures and helpers for use in tests
55+
56+
### Usage
2357

2458
Once installed, the following `pytest` command-line parameters are available:
2559

@@ -43,7 +77,7 @@ pytest \
4377
[--check]
4478
```
4579

46-
## Inventory
80+
### Inventory
4781

4882
Using ansible first starts with defining your inventory. This can be done in
4983
several ways, but to start, we'll use the `ansible_adhoc` fixture.
@@ -85,7 +119,7 @@ tests. To accomplish this, the fixture `ansible_adhoc` allows you to customize
85119
the inventory parameters. Read on for more detail on using the `ansible_adhoc`
86120
fixture.
87121

88-
## Extra Inventory
122+
### Extra Inventory
89123

90124
Using ansible first starts with defining your extra inventory. This feature was added in version 2.3.0, and is intended
91125
to allow the user to work with two different inventories. This can be done in several ways, but to start,
@@ -97,7 +131,7 @@ For example,
97131
pytest --inventory my_inventory.ini --extra-inventory my_second_inventory.ini --host-pattern host_in_second_inventory
98132
```
99133

100-
### Fixture `ansible_adhoc`
134+
#### Fixture `ansible_adhoc`
101135

102136
The `ansible_adhoc` fixture returns a function used to initialize
103137
a `HostManager` object. The `ansible_adhoc` fixture will default to parameters
@@ -151,7 +185,7 @@ def test_host_manager(ansible_adhoc):
151185
a_host.ping()
152186
```
153187

154-
### Fixture `localhost`
188+
#### Fixture `localhost`
155189

156190
The `localhost` fixture is a convenience fixture that surfaces
157191
a `ModuleDispatcher` instance for ansible host running `pytest`. This is
@@ -179,7 +213,7 @@ def test_do_something_cloudy(localhost, ansible_adhoc):
179213
localhost.ec2(**params)
180214
```
181215

182-
### Fixture `ansible_module`
216+
#### Fixture `ansible_module`
183217

184218
The `ansible_module` fixture allows tests and fixtures to call [ansible
185219
modules](http://docs.ansible.com/modules.html). Unlike the `ansible_adhoc`
@@ -224,7 +258,7 @@ def test_sshd_config(ansible_module):
224258
# do other stuff ...
225259
```
226260

227-
### Fixture `ansible_facts`
261+
#### Fixture `ansible_facts`
228262

229263
The `ansible_facts` fixture returns a JSON structure representing the system
230264
facts for the associated inventory. Sample fact data is available in the
@@ -256,7 +290,7 @@ def test_terminate_us_east_1_instances(ansible_adhoc):
256290
'''do some testing'''
257291
```
258292

259-
### Parameterizing with `pytest.mark.ansible`
293+
#### Parameterizing with `pytest.mark.ansible`
260294

261295
Perhaps the `--ansible-inventory=<inventory>` includes many systems, but you
262296
only wish to interact with a subset. The `pytest.mark.ansible` marker can be
@@ -306,7 +340,7 @@ class Test_Local(object):
306340
'''do some testing'''
307341
```
308342

309-
### Inspecting results
343+
#### Inspecting results
310344

311345
When using the `ansible_adhoc`, `localhost` or `ansible_module` fixtures, the
312346
object returned will be an instance of class `AdHocResult`. The
@@ -371,7 +405,7 @@ The contents of the JSON returned by an ansible module differs from module to
371405
module. For guidance, consult the documentation and examples for the specific
372406
[ansible module](http://docs.ansible.com/modules_by_category.html).
373407

374-
### Exception handling
408+
#### Exception handling
375409

376410
If `ansible` is unable to connect to any inventory, an exception will be raised.
377411

src/pytest_ansible/molecule.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import subprocess
99
import sys
1010
import warnings
11+
from pathlib import Path
1112
from shlex import quote
1213

1314
import pkg_resources
@@ -234,3 +235,34 @@ def __str__(self):
234235

235236
class MoleculeExceptionError(Exception):
236237
"""Custom exception for error reporting."""
238+
239+
240+
class MoleculeScenario:
241+
"""Molecule subprocess wrapper."""
242+
243+
# pylint: disable=too-few-public-methods
244+
245+
def __init__(self, name: str, parent_directory: Path, test_id: str):
246+
"""Initialize the MoleculeScenario class.
247+
248+
:param molecule_parent: The parent directory of 'molecule'
249+
:param scenario_name: The name of the molecule scenario
250+
:param test_id: The test id
251+
"""
252+
self.parent_directory = parent_directory
253+
self.name = name
254+
self.test_id = test_id
255+
256+
def test(self) -> subprocess.CompletedProcess:
257+
"""Run molecule test for the scenario.
258+
259+
:returns: The completed process
260+
"""
261+
return subprocess.run(
262+
args=[sys.executable, "-m", "molecule", "test", "-s", self.name],
263+
capture_output=False,
264+
check=False,
265+
cwd=self.parent_directory,
266+
shell=False,
267+
text=True,
268+
)

src/pytest_ansible/plugin.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""PyTest Ansible Plugin."""
22
from __future__ import annotations
33

4+
import contextlib
45
import logging
6+
import subprocess
57
from pathlib import Path
68
from typing import TYPE_CHECKING
79

@@ -21,12 +23,13 @@
2123
from pytest_ansible.host_manager import get_host_manager
2224

2325
try:
24-
from .molecule import MoleculeFile
26+
from .molecule import MoleculeFile, MoleculeScenario
2527

2628
HAS_MOLECULE = True
2729
except ImportError:
2830
HAS_MOLECULE = False
2931

32+
3033
from .units import inject, inject_only
3134

3235
if TYPE_CHECKING:
@@ -217,7 +220,6 @@ def pytest_collect_file(
217220
parent: pytest.Collector,
218221
) -> Node | None:
219222
"""Transform each found molecule.yml into a pytest test."""
220-
221223
if not parent.config.option.molecule:
222224
return None
223225
if not HAS_MOLECULE:
@@ -268,6 +270,53 @@ def pytest_generate_tests(metafunc):
268270
metafunc.parametrize("ansible_group", iter(hosts[g] for g in groups))
269271
metafunc.parametrize("ansible_group", iter(hosts[g] for g in extra_groups))
270272

273+
if "molecule_scenario" in metafunc.fixturenames:
274+
if not HAS_MOLECULE:
275+
pytest.exit("molecule not installed or found.")
276+
277+
# Find all molecule scenarios not gitignored
278+
# Replace this with molecule --list in the future if json output is available
279+
rootpath = metafunc.config.rootpath
280+
281+
scenarios = []
282+
283+
candidates = list(rootpath.glob("**/molecule/*/molecule.yml"))
284+
command = ["git", "check-ignore"] + candidates
285+
with contextlib.suppress(subprocess.CalledProcessError, FileNotFoundError):
286+
proc = subprocess.run(
287+
args=command,
288+
capture_output=True,
289+
check=True,
290+
text=True,
291+
shell=False,
292+
)
293+
294+
try:
295+
ignored = proc.stdout.splitlines()
296+
scenario_paths = [
297+
candidate for candidate in candidates if str(candidate) not in ignored
298+
]
299+
except NameError:
300+
scenario_paths = candidates
301+
302+
for fs_entry in scenario_paths:
303+
scenario = fs_entry.parent
304+
molecule_parent = scenario.parent.parent
305+
scenarios.append(
306+
MoleculeScenario(
307+
parent_directory=molecule_parent,
308+
name=scenario.name,
309+
test_id=f"{molecule_parent.name}-{scenario.name}",
310+
),
311+
)
312+
if not scenarios:
313+
pytest.exit(f"No molecule scenarions found in: {rootpath}")
314+
metafunc.parametrize(
315+
"molecule_scenario",
316+
scenarios,
317+
ids=[scenario.test_id for scenario in scenarios],
318+
)
319+
271320

272321
class PyTestAnsiblePlugin:
273322
"""Ansible PyTest Plugin Class."""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
- name: Converge
3+
hosts: localhost
4+
gather_facts: false
5+
tasks:
6+
- name: Print out information
7+
ansible.builtin.debug:
8+
msg: "converge ran successfully"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
# dependency:
3+
# name: galaxy
4+
# state: present
5+
6+
# driver:
7+
# name: delegated
8+
9+
platforms:
10+
- name: instance
11+
12+
# verifier:
13+
# name: ansible
14+
15+
provisioner:
16+
name: ansible
17+
# log: true
18+
# config_options:
19+
# defaults:
20+
# interpreter_python: auto
21+
# verbosity: 1
22+
23+
scenario:
24+
name: default
25+
test_sequence:
26+
# - dependency
27+
# - syntax
28+
# - create
29+
# - prepare
30+
- converge
31+
# - verify

tests/integration/test_molecule.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
import os
66
import subprocess
77
import sys
8+
from typing import TYPE_CHECKING
89

910
import pytest
1011

12+
if TYPE_CHECKING:
13+
from pytest_ansible.molecule import MoleculeScenario
14+
1115

1216
def test_molecule_collect() -> None:
1317
"""Test pytest collection of molecule scenarios."""
@@ -56,3 +60,13 @@ def test_molecule_runtest() -> None:
5660
assert "collected 1 item" in proc.stdout
5761
assert "tests/fixtures/molecule/default/molecule.yml::test " in proc.stdout
5862
assert "1 passed" in proc.stdout
63+
64+
65+
def test_molecule_fixture(molecule_scenario: MoleculeScenario) -> None:
66+
"""Test the scenario fixture.
67+
68+
:param molecule_scenario: One scenario
69+
"""
70+
assert molecule_scenario.test_id in ["fixtures-default", "extensions-default"]
71+
assert molecule_scenario.name == "default"
72+
molecule_scenario.test()

0 commit comments

Comments
 (0)