Skip to content

Commit 0c18c2a

Browse files
authored
Ensure ddev env test skips environments when disabled by e2e-env (#21119)
* Modify ddev env test to ensure that when an environment does not have e2e enabled the command does not run * Add chnagelog and remove previous fix based on hardcoded paths * Improve test coverage for ddev env test
1 parent 3c98036 commit 0c18c2a

File tree

5 files changed

+165
-43
lines changed

5 files changed

+165
-43
lines changed

ddev/changelog.d/21119.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix ddev env test to respect e2e-env config flag even when an environment is specified

ddev/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ target-version = ["py312"]
8686
extend-exclude = "src/ddev/_version.py"
8787

8888
[tool.ruff]
89+
extend = "../pyproject.toml"
8990
exclude = []
9091
target-version = "py312"
9192
line-length = 120

ddev/src/ddev/cli/env/test.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,21 @@ def test_command(
7878
from ddev.config.constants import AppEnvVars
7979
from ddev.e2e.config import EnvDataStorage
8080
from ddev.e2e.constants import E2EMetadata
81-
from ddev.repo.constants import NOT_E2E_TESTABLE
8281
from ddev.utils.ci import running_in_ci
8382
from ddev.utils.structures import EnvVars
8483

8584
app: Application = ctx.obj
8685
integration = app.repo.integrations.get(intg_name)
8786

88-
if integration.name in NOT_E2E_TESTABLE:
89-
app.display_info(f"Selected target {integration.name!r} does not have E2E tests to run. Skipping.")
90-
return
91-
9287
storage = EnvDataStorage(app.data_dir)
9388
active_envs = storage.get_environments(integration.name)
9489

9590
if environment is None:
9691
environment = 'all' if (not active_envs or running_in_ci()) else 'active'
9792

98-
if environment == 'all':
93+
if environment == 'active':
94+
env_names = active_envs
95+
else:
9996
import json
10097
import sys
10198

@@ -108,20 +105,20 @@ def test_command(
108105
except json.JSONDecodeError:
109106
app.abort(f'Failed to parse environments for `{integration.name}`:\n{repr(env_data_output)}')
110107

108+
no_python_filter = python_filter is None
109+
all_environments = environment == 'all'
110+
111111
env_names = [
112112
name
113113
for name, data in environments.items()
114-
if data.get('e2e-env')
114+
if data.get('e2e-env', False)
115115
and (not data.get('platforms') or app.platform.name in data['platforms'])
116-
and (python_filter is None or data.get('python') == python_filter)
116+
and (no_python_filter or data.get('python') == python_filter)
117+
and (name == environment or all_environments)
117118
]
118119

119-
elif environment == 'active':
120-
env_names = active_envs
121-
else:
122-
env_names = [environment]
123-
124120
if not env_names:
121+
app.display_info(f"Selected target {integration.name!r} disabled by e2e-env option.")
125122
return
126123

127124
app.display_header(integration.display_name)

ddev/src/ddev/repo/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44
CONFIG_DIRECTORY = '.ddev'
55
NOT_SHIPPABLE = frozenset(['datadog_checks_dev', 'datadog_checks_tests_helper', 'ddev'])
6-
NOT_E2E_TESTABLE = frozenset(['datadog_checks_dev', 'datadog_checks_base', 'datadog_checks_tests_helper', 'ddev'])
76
FULL_NAMES = {
87
'core': 'integrations-core',
98
'extras': 'integrations-extras',

ddev/tests/cli/env/test_test.py

Lines changed: 153 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,173 @@
11
# (C) Datadog, Inc. 2024-present
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
4-
from contextlib import nullcontext
54

6-
import mock
5+
import json
6+
from collections.abc import Callable, Generator, Mapping
7+
from typing import Any
8+
from unittest.mock import MagicMock
9+
710
import pytest
11+
from click.testing import Result
12+
from pytest_mock import MockerFixture, MockType
813

14+
from ddev.e2e.config import EnvData, EnvDataStorage
15+
from ddev.utils.fs import Path
916
from tests.helpers.mocks import MockPopen
17+
from tests.helpers.runner import CliRunner
1018

1119

12-
class MockEnvVars:
13-
def __init__(self, env_vars=None):
14-
assert env_vars['DDEV_REPO'] == 'core'
20+
def setup(
21+
mocker: MockerFixture,
22+
write_result_file: Callable[[Mapping[str, Any]], None],
23+
hatch_json_output: Mapping[str, Any] | str | None = None,
24+
):
25+
mocker.patch('subprocess.run', side_effect=write_result_file({'metadata': {}, 'config': {}}))
1526

16-
def __enter__(*_args, **_kwargs):
17-
pass
27+
if hatch_json_output is not None:
28+
if isinstance(hatch_json_output, str):
29+
hatch_output = hatch_json_output.encode()
30+
elif isinstance(hatch_json_output, dict):
31+
hatch_output = json.dumps(hatch_json_output).encode()
32+
else:
33+
pytest.fail('Invalid hatch_json_output type')
1834

19-
def __exit__(*_args, **_kwargs):
20-
pass
35+
mocker.patch('subprocess.Popen', return_value=MockPopen(returncode=0, stdout=hatch_output))
2136

2237

23-
def test_env_vars_repo(ddev, helpers, data_dir, write_result_file, mocker):
24-
mocker.patch('subprocess.run', side_effect=write_result_file({'metadata': {}, 'config': {}}))
25-
mocker.patch('subprocess.Popen', return_value=MockPopen(returncode=0))
26-
with mock.patch('ddev.utils.structures.EnvVars', side_effect=MockEnvVars):
27-
result = ddev('env', 'test', 'postgres', 'py3.12')
28-
assert result.exit_code == 0, result.output
29-
# Ensure test was not skipped
30-
assert "does not have E2E tests to run" not in result.output
38+
@pytest.fixture()
39+
def mock_commands(mocker: MockerFixture) -> Generator[tuple[MockType, MockType, MockType]]:
40+
start_mock = mocker.patch(
41+
'ddev.cli.env.start.start',
42+
return_value=Result(
43+
return_value=0,
44+
runner=MagicMock(),
45+
stdout_bytes=b'',
46+
stderr_bytes=b'',
47+
exit_code=0,
48+
exception=None,
49+
),
50+
)
51+
stop_mock = mocker.patch(
52+
'ddev.cli.env.stop.stop',
53+
return_value=Result(
54+
return_value=0,
55+
runner=MagicMock(),
56+
stdout_bytes=b'',
57+
stderr_bytes=b'',
58+
exit_code=0,
59+
exception=None,
60+
),
61+
)
62+
test_mock = mocker.patch(
63+
'ddev.cli.test.test',
64+
return_value=Result(
65+
return_value=0,
66+
runner=MagicMock(),
67+
stdout_bytes=b'',
68+
stderr_bytes=b'',
69+
exit_code=0,
70+
exception=None,
71+
),
72+
)
73+
yield start_mock, stop_mock, test_mock
74+
75+
76+
def assert_commands_run(mock_commands: tuple[MockType, MockType, MockType], call_count: int = 1):
77+
assert mock_commands[0].call_count == call_count
78+
assert mock_commands[1].call_count == call_count
79+
assert mock_commands[2].call_count == call_count
3180

3281

3382
@pytest.mark.parametrize(
34-
'target, expectation',
83+
'e2e_env, predicate',
3584
[
36-
('datadog_checks_dev', nullcontext()),
37-
('datadog_checks_base', nullcontext()),
38-
# This will raise an OSError because the package is not a valid integration
39-
('datadog_checks_tests_helper', pytest.raises(OSError)),
40-
('ddev', nullcontext()),
85+
(False, lambda result: "disabled by e2e-env option" in result.output),
86+
(True, lambda result: "disabled by e2e-env option" not in result.output),
4187
],
42-
ids=['datadog_checks_dev', 'datadog_checks_base', 'datadog_checks_tests_helper', 'ddev'],
88+
ids=['e2e-env-false', 'e2e-env-true'],
4389
)
44-
@pytest.mark.parametrize('env', ['py3.12', 'all', ''], ids=['py3.12', 'all', 'no-env'])
45-
def test_env_test_not_e2e_testable(ddev, target: str, env: str, expectation):
46-
with expectation:
47-
result = ddev('env', 'test', target, env)
90+
def test_env_vars_repo(
91+
ddev: CliRunner,
92+
data_dir: Path,
93+
write_result_file: Callable[[Mapping[str, Any]], None],
94+
mocker: MockerFixture,
95+
e2e_env: bool,
96+
predicate: Callable[[Result], bool],
97+
mock_commands: tuple[MockType, MockType, MockType],
98+
):
99+
setup(mocker, write_result_file, hatch_json_output={'py3.12': {'e2e-env': e2e_env}})
100+
mocker.patch.object(EnvData, 'read_metadata', return_value={})
101+
102+
result = ddev('env', 'test', 'postgres', 'py3.12')
103+
assert result.exit_code == 0, result.output
104+
# Ensure test was not skipped
105+
assert predicate(result)
106+
assert_commands_run(mock_commands, 1 if e2e_env else 0)
107+
108+
109+
@pytest.mark.parametrize('environment, command_call_count', [('active', 0), ('all', 2), ('py3.12', 1)])
110+
def test_environment_runs_for_enabled_environments(
111+
ddev: CliRunner,
112+
data_dir: Path,
113+
write_result_file: Callable[[Mapping[str, Any]], None],
114+
mocker: MockerFixture,
115+
environment: str,
116+
mock_commands: tuple[MockType, MockType, MockType],
117+
command_call_count: int,
118+
):
119+
setup(
120+
mocker,
121+
write_result_file,
122+
hatch_json_output={'py3.12': {'e2e-env': True}, 'py3.13': {'e2e-env': False}, 'py3.13-v1': {'e2e-env': True}},
123+
)
124+
with mocker.patch.object(EnvData, 'read_metadata', return_value={}):
125+
result = ddev('env', 'test', 'postgres', environment)
126+
assert result.exit_code == 0, result.output
127+
assert_commands_run(mock_commands, command_call_count)
128+
129+
130+
def test_command_errors_out_when_cannot_parse_json_output_from_hatch(
131+
ddev: CliRunner,
132+
data_dir: Path,
133+
write_result_file: Callable[[Mapping[str, Any]], None],
134+
mocker: MockerFixture,
135+
):
136+
setup(mocker, write_result_file, hatch_json_output='invalid json')
137+
result = ddev('env', 'test', 'postgres', 'py3.12')
138+
assert result.exit_code == 1, result.output
139+
140+
141+
def test_runningin_ci_triggers_all_environments_when_not_supplied(
142+
ddev: CliRunner,
143+
data_dir: Path,
144+
write_result_file: Callable[[Mapping[str, Any]], None],
145+
mocker: MockerFixture,
146+
mock_commands: tuple[MockType, MockType, MockType],
147+
):
148+
setup(mocker, write_result_file, hatch_json_output={'py3.12': {'e2e-env': True}, 'py3.13': {'e2e-env': True}})
149+
mocker.patch('ddev.utils.ci.running_in_ci', return_value=True)
150+
151+
with mocker.patch.object(EnvData, 'read_metadata', return_value={}):
152+
result = ddev('env', 'test', 'postgres')
153+
assert result.exit_code == 0, result.output
154+
assert_commands_run(mock_commands, 2)
155+
156+
157+
def test_run_only_active_environments_when_not_running_in_ci_and_active_environments_exist(
158+
ddev: CliRunner,
159+
data_dir: Path,
160+
write_result_file: Callable[[Mapping[str, Any]], None],
161+
mocker: MockerFixture,
162+
mock_commands: tuple[MockType, MockType, MockType],
163+
):
164+
setup(mocker, write_result_file, hatch_json_output={'py3.12': {'e2e-env': True}, 'py3.13': {'e2e-env': True}})
165+
mocker.patch('ddev.utils.ci.running_in_ci', return_value=False)
166+
167+
with (
168+
mocker.patch.object(EnvData, 'read_metadata', return_value={}),
169+
mocker.patch.object(EnvDataStorage, 'get_environments', return_value=['py3.12']),
170+
):
171+
result = ddev('env', 'test', 'postgres')
48172
assert result.exit_code == 0, result.output
49-
assert "does not have E2E tests to run" in result.output
173+
assert_commands_run(mock_commands, 1)

0 commit comments

Comments
 (0)