Skip to content

Commit 71bfdfd

Browse files
Fix --ignore-related clobbering when breeze tests providers (#41967)
1 parent 824e85a commit 71bfdfd

2 files changed

Lines changed: 147 additions & 26 deletions

File tree

dev/breeze/src/airflow_breeze/utils/run_tests.py

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -108,38 +108,27 @@ def test_paths(test_type: str, backend: str, helm_test_package: str | None) -> t
108108
return result_log_file, warnings_file, coverage_file
109109

110110

111-
def get_suspended_provider_args() -> list[str]:
112-
pytest_args = []
113-
suspended_folders = get_suspended_provider_folders()
114-
for providers in suspended_folders:
115-
pytest_args.extend(
111+
def get_ignore_switches_for_provider(provider_folders: list[str]) -> list[str]:
112+
args = []
113+
for providers in provider_folders:
114+
args.extend(
116115
[
117-
"--ignore",
118-
f"tests/providers/{providers}",
119-
"--ignore",
120-
f"tests/system/providers/{providers}",
121-
"--ignore",
122-
f"tests/integration/providers/{providers}",
116+
f"--ignore=tests/providers/{providers}",
117+
f"--ignore=tests/system/providers/{providers}",
118+
f"--ignore=tests/integration/providers/{providers}",
123119
]
124120
)
125-
return pytest_args
121+
return args
122+
123+
124+
def get_suspended_provider_args() -> list[str]:
125+
suspended_folders = get_suspended_provider_folders()
126+
return get_ignore_switches_for_provider(suspended_folders)
126127

127128

128129
def get_excluded_provider_args(python_version: str) -> list[str]:
129-
pytest_args = []
130130
excluded_folders = get_excluded_provider_folders(python_version)
131-
for providers in excluded_folders:
132-
pytest_args.extend(
133-
[
134-
"--ignore",
135-
f"tests/providers/{providers}",
136-
"--ignore",
137-
f"tests/system/providers/{providers}",
138-
"--ignore",
139-
f"tests/integration/providers/{providers}",
140-
]
141-
)
142-
return pytest_args
131+
return get_ignore_switches_for_provider(excluded_folders)
143132

144133

145134
TEST_TYPE_MAP_TO_PYTEST_ARGS: dict[str, list[str]] = {
@@ -288,7 +277,7 @@ def convert_test_type_to_pytest_args(
288277
return find_all_other_tests()
289278
test_dirs = TEST_TYPE_MAP_TO_PYTEST_ARGS.get(test_type)
290279
if test_dirs:
291-
return test_dirs
280+
return test_dirs.copy()
292281
get_console().print(f"[error]Unknown test type: {test_type}[/]")
293282
sys.exit(1)
294283

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
from __future__ import annotations
18+
19+
import re
20+
from unittest.mock import patch
21+
22+
import pytest
23+
24+
from airflow_breeze.commands.testing_commands import _run_test
25+
from airflow_breeze.params.shell_params import ShellParams
26+
27+
28+
@pytest.fixture(autouse=True)
29+
def mock_run_command():
30+
"""We mock run_command to capture its call args; it returns nothing so mock training is unnecessary."""
31+
with patch("airflow_breeze.commands.testing_commands.run_command") as mck:
32+
yield mck
33+
34+
35+
@pytest.fixture(autouse=True)
36+
def mock_get_suspended_provider_folders():
37+
with patch("airflow_breeze.utils.run_tests.get_suspended_provider_folders") as mck:
38+
mck.return_value = []
39+
yield mck
40+
41+
42+
@pytest.fixture(autouse=True)
43+
def mock_get_excluded_provider_folders():
44+
with patch("airflow_breeze.utils.run_tests.get_excluded_provider_folders") as mck:
45+
mck.return_value = []
46+
yield mck
47+
48+
49+
@pytest.fixture(autouse=True)
50+
def mock_sleep():
51+
"""_run_test does a 10-second sleep in CI, so we mock the sleep function to save CI test time."""
52+
with patch("airflow_breeze.commands.testing_commands.sleep"):
53+
yield
54+
55+
56+
@pytest.fixture(autouse=True)
57+
def mock_remove_docker_networks():
58+
"""We mock remove_docker_networks to avoid making actual docker calls during these tests;
59+
it returns nothing so mock training is unnecessary."""
60+
with patch("airflow_breeze.commands.testing_commands.remove_docker_networks") as mck:
61+
yield mck
62+
63+
64+
@pytest.mark.parametrize(
65+
"mock_fixture",
66+
["mock_get_suspended_provider_folders", "mock_get_excluded_provider_folders"],
67+
ids=["suspended provider", "excluded provider"],
68+
)
69+
def test_irregular_provider_with_extra_ignore_should_be_valid_cmd(mock_run_command, mock_fixture, request):
70+
"""When a provider is suspended or excluded, we expect "--ignore" switches to wind up in the cmd args"""
71+
fake_provider_name = "unittestingprovider"
72+
73+
mock_to_train = request.getfixturevalue(mock_fixture)
74+
mock_to_train.return_value = [fake_provider_name]
75+
76+
_run_test(
77+
shell_params=ShellParams(test_type="Providers"),
78+
extra_pytest_args=(f"--ignore=tests/providers/{fake_provider_name}",),
79+
python_version="3.8",
80+
output=None,
81+
test_timeout=60,
82+
skip_docker_compose_down=True,
83+
)
84+
85+
# the docker run command is the second call to run_command; the command arg list is the first
86+
# positional arg of the command call
87+
run_cmd_call = mock_run_command.call_args_list[1]
88+
arg_str = " ".join(run_cmd_call.args[0])
89+
90+
# The command pattern we look for is "<container id> <tests directory arg> \
91+
# <*other args we don't care about*> --ignore tests/providers/<provider name> \
92+
# --ignore tests/system/providers/<provider name> --ignore tests/integration/providers/<provider name>"
93+
# (the container id is simply to anchor the pattern so we know where we are starting; _run_tests should
94+
# be refactored to make arg testing easier but until then we have to regex-test the entire command string
95+
match_pattern = re.compile(
96+
f" airflow tests/providers .+ --ignore=tests/providers/{fake_provider_name} --ignore=tests/system/providers/{fake_provider_name} --ignore=tests/integration/providers/{fake_provider_name}"
97+
)
98+
99+
assert match_pattern.search(arg_str)
100+
101+
102+
def test_primary_test_arg_is_excluded_by_extra_pytest_arg(mock_run_command):
103+
"""This code scenario currently has a bug - if a test type resolves to a single test directory,
104+
but the same directory is also set to be ignored (either by extra_pytest_args or because a provider is
105+
suspended or excluded), the _run_test function removes the test directory from the argument list,
106+
which has the effect of running all of the tests pytest can find. Not good!
107+
108+
NB: this test accurately describes the buggy behavior; IOW when fixing the bug the test must be changed.
109+
110+
TODO: fix this bug that runs unintended tests; probably the correct behavior is to skip the run."""
111+
test_provider = "http" # "Providers[<id>]" scans the source tree so we need to use a real provider id
112+
_run_test(
113+
shell_params=ShellParams(test_type=f"Providers[{test_provider}]"),
114+
extra_pytest_args=(f"--ignore=tests/providers/{test_provider}",),
115+
python_version="3.8",
116+
output=None,
117+
test_timeout=60,
118+
skip_docker_compose_down=True,
119+
)
120+
121+
run_cmd_call = mock_run_command.call_args_list[1]
122+
arg_str = " ".join(run_cmd_call.args[0])
123+
124+
# The command pattern we look for is "<container id> --verbosity=0 \
125+
# <*other args we don't care about*> --ignore=tests/providers/<provider name>"
126+
# The tests/providers/http argument has been eliminated by the code that preps the args; this is a bug,
127+
# bc without a directory or module arg, pytest tests everything (which we don't want!)
128+
# We check "--verbosity=0" to ensure nothing is between the airflow container id and the verbosity arg,
129+
# IOW that the primary test arg is removed
130+
match_pattern = re.compile(f"airflow --verbosity=0 .+ --ignore=tests/providers/{test_provider}")
131+
132+
assert match_pattern.search(arg_str)

0 commit comments

Comments
 (0)