Skip to content

Commit 285734a

Browse files
authored
support --black as arg for testing (#25271)
fixes #24966
1 parent 0986bdc commit 285734a

File tree

8 files changed

+292
-10
lines changed

8 files changed

+292
-10
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
---
2+
applyTo: 'python_files/tests/pytestadapter/test_discovery.py'
3+
description: 'A guide for adding new tests for pytest discovery and JSON formatting in the test_pytest_collect suite.'
4+
---
5+
6+
# How to Add New Pytest Discovery Tests
7+
8+
This guide explains how to add new tests for pytest discovery and JSON formatting in the `test_pytest_collect` suite. Follow these steps to ensure your tests are consistent and correct.
9+
10+
---
11+
12+
## 1. Add Your Test File
13+
14+
- Place your new test file/files in the appropriate subfolder under:
15+
```
16+
python_files/tests/pytestadapter/.data/
17+
```
18+
- Organize folders and files to match the structure you want to test. For example, to test nested folders, create the corresponding directory structure.
19+
- In your test file, mark each test function with a comment:
20+
```python
21+
def test_function(): # test_marker--test_function
22+
...
23+
```
24+
25+
**Root Node Matching:**
26+
27+
- The root node in your expected output must match the folder or file you pass to pytest discovery. For example, if you run discovery on a subfolder, the root `"name"`, `"path"`, and `"id_"` in your expected output should be that subfolder, not the parent `.data` folder.
28+
- Only use `.data` as the root if you are running discovery on the entire `.data` folder.
29+
30+
**Example:**
31+
If you run:
32+
33+
```python
34+
helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"])
35+
```
36+
37+
then your expected output root should be:
38+
39+
```python
40+
{
41+
"name": "myfolder",
42+
"path": os.fspath(TEST_DATA_PATH / "myfolder"),
43+
"type_": "folder",
44+
...
45+
}
46+
```
47+
48+
---
49+
50+
## 2. Update `expected_discovery_test_output.py`
51+
52+
- Open `expected_discovery_test_output.py` in the same test suite.
53+
- Add a new expected output dictionary for your test file, following the format of existing entries.
54+
- Use the helper functions and path conventions:
55+
- Use `os.fspath()` for all paths.
56+
- Use `find_test_line_number("function_name", file_path)` for the `lineno` field.
57+
- Use `get_absolute_test_id("relative_path::function_name", file_path)` for `id_` and `runID`.
58+
- Always use current path concatenation (e.g., `TEST_DATA_PATH / "your_folder" / "your_file.py"`).
59+
- Create new constants as needed to keep the code clean and maintainable.
60+
61+
**Important:**
62+
63+
- Do **not** read the entire `expected_discovery_test_output.py` file if you only need to add or reference a single constant. This file is very large; prefer searching for the relevant section or appending to the end.
64+
65+
**Example:**
66+
If you run discovery on a subfolder:
67+
68+
```python
69+
helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"])
70+
```
71+
72+
then your expected output root should be:
73+
74+
```python
75+
myfolder_path = TEST_DATA_PATH / "myfolder"
76+
my_expected_output = {
77+
"name": "myfolder",
78+
"path": os.fspath(myfolder_path),
79+
"type_": "folder",
80+
...
81+
}
82+
```
83+
84+
- Add a comment above your dictionary describing the structure, as in the existing examples.
85+
86+
---
87+
88+
## 3. Add Your Test to `test_discovery.py`
89+
90+
- In `test_discovery.py`, add your new test as a parameterized case to the main `test_pytest_collect` function. Do **not** create a standalone test function for new discovery cases.
91+
- Reference your new expected output constant from `expected_discovery_test_output.py`.
92+
93+
**Example:**
94+
95+
```python
96+
@pytest.mark.parametrize(
97+
("file", "expected_const"),
98+
[
99+
("myfolder", my_expected_output),
100+
# ... other cases ...
101+
],
102+
)
103+
def test_pytest_collect(file, expected_const):
104+
...
105+
```
106+
107+
---
108+
109+
## 4. Run and Verify
110+
111+
- Run the test suite to ensure your new test is discovered and passes.
112+
- If the test fails, check your expected output dictionary for path or structure mismatches.
113+
114+
---
115+
116+
## 5. Tips
117+
118+
- Always use the helper functions for line numbers and IDs.
119+
- Match the folder/file structure in `.data` to the expected JSON structure.
120+
- Use comments to document the expected output structure for clarity.
121+
- Ensure all `"path"` and `"id_"` fields in your expected output match exactly what pytest returns, including absolute paths and root node structure.
122+
123+
---
124+
125+
**Reference:**
126+
See `expected_discovery_test_output.py` for more examples and formatting. Use search or jump to the end of the file to avoid reading the entire file when possible.

build/test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ pytest-describe
3939

4040
# for pytest-ruff related tests
4141
pytest-ruff
42+
pytest-black
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def add(a, b):
2+
return a + b
3+
4+
5+
def subtract(a, b):
6+
return a - b
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import pytest
2+
from app import add, subtract
3+
4+
5+
def test_add(): # test_marker--test_add
6+
assert add(2, 3) == 5
7+
assert add(-1, 1) == 0
8+
assert add(0, 0) == 0
9+
10+
11+
def test_subtract(): # test_marker--test_subtract
12+
assert subtract(5, 3) == 2
13+
assert subtract(0, 0) == 0
14+
assert subtract(-1, -1) == 0

python_files/tests/pytestadapter/expected_discovery_test_output.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,3 +1719,110 @@
17191719
],
17201720
"id_": TEST_DATA_PATH_STR,
17211721
}
1722+
1723+
# This is the expected output for the 2496-black-formatter folder when run with black plugin
1724+
# └── .data
1725+
# └── 2496-black-formatter
1726+
# └── app.py
1727+
# └── black
1728+
# └── test_app.py
1729+
# └── black
1730+
# └── test_add
1731+
# └── test_subtract
1732+
black_formatter_folder_path = TEST_DATA_PATH / "2496-black-formatter"
1733+
black_app_path = black_formatter_folder_path / "app.py"
1734+
black_test_app_path = black_formatter_folder_path / "test_app.py"
1735+
black_formatter_expected_output = {
1736+
"name": ".data",
1737+
"path": TEST_DATA_PATH_STR,
1738+
"type_": "folder",
1739+
"children": [
1740+
{
1741+
"name": "2496-black-formatter",
1742+
"path": os.fspath(black_formatter_folder_path),
1743+
"type_": "folder",
1744+
"id_": os.fspath(black_formatter_folder_path),
1745+
"children": [
1746+
{
1747+
"name": "app.py",
1748+
"path": os.fspath(black_app_path),
1749+
"type_": "file",
1750+
"id_": os.fspath(black_app_path),
1751+
"children": [
1752+
{
1753+
"name": "black",
1754+
"path": os.fspath(black_app_path),
1755+
"lineno": "0",
1756+
"type_": "test",
1757+
"id_": get_absolute_test_id(
1758+
"2496-black-formatter/app.py::black",
1759+
black_app_path,
1760+
),
1761+
"runID": get_absolute_test_id(
1762+
"2496-black-formatter/app.py::black",
1763+
black_app_path,
1764+
),
1765+
}
1766+
],
1767+
},
1768+
{
1769+
"name": "test_app.py",
1770+
"path": os.fspath(black_test_app_path),
1771+
"type_": "file",
1772+
"id_": os.fspath(black_test_app_path),
1773+
"children": [
1774+
{
1775+
"name": "black",
1776+
"path": os.fspath(black_test_app_path),
1777+
"lineno": "0",
1778+
"type_": "test",
1779+
"id_": get_absolute_test_id(
1780+
"2496-black-formatter/test_app.py::black",
1781+
black_test_app_path,
1782+
),
1783+
"runID": get_absolute_test_id(
1784+
"2496-black-formatter/test_app.py::black",
1785+
black_test_app_path,
1786+
),
1787+
},
1788+
{
1789+
"name": "test_add",
1790+
"path": os.fspath(black_test_app_path),
1791+
"lineno": find_test_line_number(
1792+
"test_add",
1793+
black_test_app_path,
1794+
),
1795+
"type_": "test",
1796+
"id_": get_absolute_test_id(
1797+
"2496-black-formatter/test_app.py::test_add",
1798+
black_test_app_path,
1799+
),
1800+
"runID": get_absolute_test_id(
1801+
"2496-black-formatter/test_app.py::test_add",
1802+
black_test_app_path,
1803+
),
1804+
},
1805+
{
1806+
"name": "test_subtract",
1807+
"path": os.fspath(black_test_app_path),
1808+
"lineno": find_test_line_number(
1809+
"test_subtract",
1810+
black_test_app_path,
1811+
),
1812+
"type_": "test",
1813+
"id_": get_absolute_test_id(
1814+
"2496-black-formatter/test_app.py::test_subtract",
1815+
black_test_app_path,
1816+
),
1817+
"runID": get_absolute_test_id(
1818+
"2496-black-formatter/test_app.py::test_subtract",
1819+
black_test_app_path,
1820+
),
1821+
},
1822+
],
1823+
},
1824+
],
1825+
}
1826+
],
1827+
"id_": TEST_DATA_PATH_STR,
1828+
}

python_files/tests/pytestadapter/test_discovery.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -337,15 +337,37 @@ def test_config_sub_folder():
337337
assert tests.get("name") == "config_sub_folder"
338338

339339

340-
def test_ruff_plugin():
341-
"""Here the session node will be a subfolder of the workspace root and the test are in another subfolder.
340+
@pytest.mark.parametrize(
341+
("file", "expected_const", "extra_arg"),
342+
[
343+
(
344+
"folder_with_script",
345+
expected_discovery_test_output.ruff_test_expected_output,
346+
"--ruff",
347+
),
348+
(
349+
"2496-black-formatter",
350+
expected_discovery_test_output.black_formatter_expected_output,
351+
"--black",
352+
),
353+
],
354+
)
355+
def test_plugin_collect(file, expected_const, extra_arg):
356+
"""Test pytest discovery on a folder with a plugin argument (e.g., --ruff, --black).
342357
343-
This tests checks to see if test node path are under the session node and if so the
344-
session node is correctly updated to the common path.
358+
Uses variables from expected_discovery_test_output.py to store the expected
359+
dictionary return. Only handles discovery and therefore already contains the arg
360+
--collect-only. All test discovery will succeed, be in the correct cwd, and match
361+
expected test output.
362+
363+
Keyword arguments:
364+
file -- a string with the file or folder to run pytest discovery on.
365+
expected_const -- the expected output from running pytest discovery on the file.
366+
extra_arg -- the extra plugin argument to pass (e.g., --ruff, --black)
345367
"""
346-
file_path = helpers.TEST_DATA_PATH / "folder_with_script"
368+
file_path = helpers.TEST_DATA_PATH / file
347369
actual = helpers.runner(
348-
[os.fspath(file_path), "--collect-only", "--ruff"],
370+
[os.fspath(file_path), "--collect-only", extra_arg],
349371
)
350372

351373
assert actual
@@ -359,8 +381,8 @@ def test_ruff_plugin():
359381
assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH)
360382
assert is_same_tree(
361383
actual_item.get("tests"),
362-
expected_discovery_test_output.ruff_test_expected_output,
384+
expected_const,
363385
["id_", "lineno", "name", "runID"],
364386
), (
365-
f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.ruff_test_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}"
387+
f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}"
366388
)

python_files/vscode_pytest/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
if TYPE_CHECKING:
1818
from pluggy import Result
1919

20-
2120
USES_PYTEST_DESCRIBE = False
2221

2322
with contextlib.suppress(ImportError):

src/client/testing/testController/common/utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,14 @@ export function populateTestTree(
209209

210210
let range: Range | undefined;
211211
if (child.lineno) {
212-
range = new Range(new Position(Number(child.lineno) - 1, 0), new Position(Number(child.lineno), 0));
212+
if (Number(child.lineno) === 0) {
213+
range = new Range(new Position(0, 0), new Position(0, 0));
214+
} else {
215+
range = new Range(
216+
new Position(Number(child.lineno) - 1, 0),
217+
new Position(Number(child.lineno), 0),
218+
);
219+
}
213220
}
214221
testItem.canResolveChildren = false;
215222
testItem.range = range;

0 commit comments

Comments
 (0)