Skip to content

Commit bf552fa

Browse files
committed
Add --tag-filters option
1 parent 40ea70a commit bf552fa

4 files changed

Lines changed: 146 additions & 3 deletions

File tree

src/buildkite_test_collector/pytest_plugin/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,10 @@ def pytest_addoption(parser):
8989
dest="mergejson",
9090
help='merge json output with existing file, if it exists'
9191
)
92+
group.addoption(
93+
'--tag-filters',
94+
default=None,
95+
action='store',
96+
dest="tag_filters",
97+
help='filter tests by execution_tag with `key:value`, e.g. `--tag-filters color:red`'
98+
)

src/buildkite_test_collector/pytest_plugin/buildkite_plugin.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ def __init__(self, payload):
1717
self.in_flight = {}
1818
self.spans = {}
1919

20+
def pytest_collection_modifyitems(self, config, items):
21+
"""pytest_collection_modifyitems hook callback to filter tests by execution_tag markers"""
22+
tag_filter = config.getoption("tag_filters")
23+
if not tag_filter:
24+
return
25+
26+
filtered_items, unfiltered_items = self._filter_tests_by_tag(items, tag_filter)
27+
28+
config.hook.pytest_deselected(items=unfiltered_items)
29+
items[:] = filtered_items
30+
2031
def pytest_runtest_logstart(self, nodeid, location):
2132
"""pytest_runtest_logstart hook callback"""
2233
logger.debug('hook=pytest_runtest_logstart nodeid=%s', nodeid)
@@ -144,3 +155,32 @@ def save_payload_as_json(self, path, merge=False):
144155

145156
with open(path, "w", encoding="utf-8") as f:
146157
json.dump(data, f)
158+
159+
def _filter_tests_by_tag(self, items, tag_filter):
160+
"""
161+
Filters tests based on the tag_filter option.
162+
Supports filtering by a single tag in the format key:value.
163+
Only equality comparison is supported.
164+
Returns a tuple of (filtered_items, unfiltered_items).
165+
"""
166+
key, _, value = tag_filter.partition(":")
167+
168+
filtered_items = []
169+
unfiltered_items = []
170+
for item in items:
171+
# Extract all execution_tag markers and store them in a dict
172+
tags = {}
173+
markers = item.iter_markers("execution_tag")
174+
for tag_marker in markers:
175+
# Ensure the marker has exactly two arguments: key and value
176+
if len(tag_marker.args) != 2:
177+
continue
178+
179+
tags[tag_marker.args[0]] = tag_marker.args[1]
180+
181+
if tags.get(key) == value:
182+
filtered_items.append(item)
183+
else:
184+
unfiltered_items.append(item)
185+
186+
return filtered_items, unfiltered_items
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# This is sample test file used for integration testing of execution_tag marker
2+
import pytest
3+
4+
5+
@pytest.mark.execution_tag("color", "red")
6+
@pytest.mark.execution_tag("size", "medium")
7+
def test_apple():
8+
assert True
9+
10+
@pytest.mark.execution_tag("color", "orange")
11+
@pytest.mark.execution_tag("size", "medium")
12+
def test_orange():
13+
assert True
14+
15+
@pytest.mark.execution_tag("color", "yellow")
16+
@pytest.mark.execution_tag("size", "large")
17+
def test_banana():
18+
assert True
19+
20+
@pytest.mark.execution_tag("color", "purple")
21+
@pytest.mark.execution_tag("size", "small")
22+
def test_grape():
23+
assert True
24+
25+
@pytest.mark.execution_tag("color", "red")
26+
@pytest.mark.execution_tag("size", "small")
27+
def test_strawberry():
28+
assert True
29+

tests/buildkite_test_collector/test_integration.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import json
22
import os
33
import pytest
4+
import subprocess
5+
import sys
6+
47
from pathlib import Path
58

69
def test_add_tag_to_execution_data(tmp_path, fake_env):
710
"""Verify that tags added via the execution_tag marker are correctly captured in the test data."""
8-
import subprocess
9-
import sys
10-
1111
test_file = Path(__file__).parent / "data" / "test_sample_execution_tag.py"
1212
json_output_file = tmp_path / "test_results.json"
1313

@@ -41,3 +41,70 @@ def test_add_tag_to_execution_data(tmp_path, fake_env):
4141

4242
no_tag_test = tests_by_name["test_without_tags"]
4343
assert "tags" not in no_tag_test or no_tag_test.get("tags") == {}
44+
45+
class TestTagFiltering:
46+
import subprocess
47+
import sys
48+
49+
def test_filter_by_single_tag(self,tmp_path, fake_env):
50+
test_file = Path(__file__).parent / "data" / "test_sample_execution_tag_filter.py"
51+
52+
cmd = [
53+
sys.executable, "-m", "pytest", "--co", "-q", "--tag-filters", "color:red",
54+
str(test_file),
55+
]
56+
57+
# Run pytest in a subprocess
58+
result = subprocess.run(cmd, cwd=str(tmp_path), capture_output=True, text=True)
59+
60+
assert "2/5 tests collected" in result.stdout, "collect count mismatch"
61+
62+
# Parse the output to find collected tests
63+
lines = result.stdout.strip().splitlines()
64+
collected_tests = []
65+
for line in lines:
66+
if "::" in line:
67+
collected_tests.append(line)
68+
69+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_apple" in collected_tests
70+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_strawberry" in collected_tests
71+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_orange" not in collected_tests
72+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_banana" not in collected_tests
73+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_grape" not in collected_tests
74+
75+
def test_wrong_filter_format(self,tmp_path, fake_env):
76+
test_file = Path(__file__).parent / "data" / "test_sample_execution_tag_filter.py"
77+
78+
cmd = [
79+
sys.executable, "-m", "pytest", "--co", "-q", "--tag-filters", "foobar",
80+
str(test_file),
81+
]
82+
83+
# Run pytest in a subprocess
84+
result = subprocess.run(cmd, cwd=str(tmp_path), capture_output=True, text=True)
85+
86+
assert "no tests collected" in result.stdout
87+
88+
def test_no_filter(self,tmp_path, fake_env):
89+
test_file = Path(__file__).parent / "data" / "test_sample_execution_tag_filter.py"
90+
91+
cmd = [
92+
sys.executable, "-m", "pytest", "--co", "-q",
93+
str(test_file),
94+
]
95+
96+
# Run pytest in a subprocess
97+
result = subprocess.run(cmd, cwd=str(tmp_path), capture_output=True, text=True)
98+
99+
# Parse the output to find collected tests
100+
lines = result.stdout.strip().splitlines()
101+
collected_tests = []
102+
for line in lines:
103+
if "::" in line:
104+
collected_tests.append(line)
105+
106+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_apple" in collected_tests
107+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_strawberry" in collected_tests
108+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_orange" in collected_tests
109+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_banana" in collected_tests
110+
assert "tests/buildkite_test_collector/data/test_sample_execution_tag_filter.py::test_grape" in collected_tests

0 commit comments

Comments
 (0)