• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 19250292619

11 Nov 2025 12:09AM UTC coverage: 77.865% (-2.4%) from 80.298%
19250292619

push

github

web-flow
flag non-runnable targets used with `code_quality_tool` (#22875)

2 of 5 new or added lines in 2 files covered. (40.0%)

1487 existing lines in 72 files now uncovered.

71448 of 91759 relevant lines covered (77.86%)

3.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

44.86
/src/python/pants/backend/python/goals/coverage_py_integration_test.py
1
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
2✔
5

6
import platform
2✔
7
import sqlite3
2✔
8
from collections.abc import Sequence
2✔
9
from pathlib import Path
2✔
10
from textwrap import dedent
2✔
11

12
import pytest
2✔
13

14
from pants.backend.python.goals.coverage_py import CoverageSubsystem
2✔
15
from pants.base.build_environment import get_buildroot
2✔
16
from pants.testutil.pants_integration_test import PantsResult, run_pants, setup_tmpdir
2✔
17
from pants.testutil.python_interpreter_selection import all_major_minor_python_versions
2✔
18

19

20
def sources(batched: bool) -> dict[str, str]:
2✔
21
    return {
2✔
22
        # Only `lib.py` will actually be tested, but we still expect `random.py` t`o show up in
23
        # the final report correctly.
24
        "src/python/project/__init__.py": "",
25
        "src/python/project/lib.py": dedent(
26
            """\
27
        def add(x, y):
28
            return x + y
29

30
        def subtract(x, y):
31
            return x - y
32

33
        def multiply(x, y):
34
            return x * y
35
        """
36
        ),
37
        # Include a type stub to ensure that we can handle it. We expect it to be ignored because the
38
        # test run does not use the file.
39
        "src/python/project/lib.pyi": dedent(
40
            """\
41
        def add(x: int, y: int) -> None:
42
            return x + y
43
        """
44
        ),
45
        "src/python/project/random.py": dedent(
46
            """\
47
        def capitalize(s):
48
            return s.capitalize()
49
        """
50
        ),
51
        # Only test half of the library.
52
        "src/python/project/lib_test.py": dedent(
53
            """\
54
        from project.lib import add
55

56
        def test_add():
57
            assert add(2, 3) == 5
58
        """
59
        ),
60
        "src/python/project/BUILD": dedent(
61
            f"""\
62
        python_sources()
63
        python_tests(
64
            name="tests",
65
            dependencies=[":project"],
66
            {'batch_compatibility_tag="default",' if batched else ""}
67
        )
68
        """
69
        ),
70
        "src/python/core/BUILD": "python_sources()",
71
        "src/python/core/__init__.py": "",
72
        "src/python/core/untested.py": "CONSTANT = 42",
73
        "foo/bar.py": "BAZ = True",
74
        # Test that a `tests/` source root accurately gets coverage data for the `src/`
75
        # root.
76
        "tests/python/project_test/__init__.py": "",
77
        "tests/python/project_test/test_multiply.py": dedent(
78
            """\
79
        from project.lib import multiply
80

81
        def test_multiply():
82
            assert multiply(2, 3) == 6
83
        """
84
        ),
85
        "tests/python/project_test/test_arithmetic.py": dedent(
86
            """\
87
        from project.lib import add, subtract
88

89
        def test_arithmetic():
90
            assert add(4, 3) == 7 == subtract(10, 3)
91
        """
92
        ),
93
        "tests/python/project_test/BUILD": dedent(
94
            """\
95
        python_tests(
96
            name="multiply",
97
            sources=["test_multiply.py"],
98
            dependencies=['{tmpdir}/src/python/project'],
99
        )
100

101
        python_tests(
102
            name="arithmetic",
103
            sources=["test_arithmetic.py"],
104
            dependencies=['{tmpdir}/src/python/project'],
105
        )
106
        """
107
        ),
108
        # Test a file that does not cover any src code. While this is unlikely to happen,
109
        # this tests that we can properly handle the edge case.
110
        "tests/python/project_test/no_src/__init__.py": "",
111
        "tests/python/project_test/no_src/test_no_src.py": dedent(
112
            """\
113
        def test_true():
114
           assert True is True
115
        """
116
        ),
117
        "tests/python/project_test/no_src/BUILD.py": f"""python_tests({'batch_compatibility_tag="default"' if batched else ""})""",
118
    }
119

120

121
def run_coverage_that_may_fail(tmpdir: str, *extra_args: str) -> PantsResult:
2✔
122
    command = [
2✔
123
        "--backend-packages=pants.backend.python",
124
        "test",
125
        "--use-coverage",
126
        f"{tmpdir}/src/python/project:tests",
127
        f"{tmpdir}/tests/python/project_test:multiply",
128
        f"{tmpdir}/tests/python/project_test:arithmetic",
129
        f"{tmpdir}/tests/python/project_test/no_src",
130
        f"--source-root-patterns=['/{tmpdir}/src/python', '{tmpdir}/tests/python', '{tmpdir}/foo']",
131
        *extra_args,
132
    ]
133
    result = run_pants(command)
2✔
134
    return result
2✔
135

136

137
def run_coverage(tmpdir: str, *extra_args: str) -> PantsResult:
2✔
138
    result = run_coverage_that_may_fail(tmpdir, *extra_args)
2✔
139
    result.assert_success()
2✔
140
    # Regression test: make sure that individual tests do not complain about failing to
141
    # generate reports. This was showing up at test-time, even though the final merged
142
    # report would work properly.
143
    assert "Failed to generate report" not in result.stderr
2✔
144
    return result
2✔
145

146

147
@pytest.mark.platform_specific_behavior
2✔
148
@pytest.mark.parametrize(
2✔
149
    "major_minor_interpreter",
150
    all_major_minor_python_versions(CoverageSubsystem.default_interpreter_constraints),
151
)
152
def test_coverage(major_minor_interpreter: str) -> None:
2✔
153
    with setup_tmpdir(sources(False)) as tmpdir:
2✔
154
        result = run_coverage(
2✔
155
            tmpdir,
156
            f"--python-interpreter-constraints=['=={major_minor_interpreter}.*']",
157
            f"--coverage-py-interpreter-constraints=['=={major_minor_interpreter}.*']",
158
        )
159
    assert (
2✔
160
        dedent(
161
            f"""\
162
            Name                                                          Stmts   Miss  Cover
163
            ---------------------------------------------------------------------------------
164
            {tmpdir}/src/python/project/__init__.py                        0      0   100%
165
            {tmpdir}/src/python/project/lib.py                             6      0   100%
166
            {tmpdir}/src/python/project/lib_test.py                        3      0   100%
167
            {tmpdir}/src/python/project/random.py                          2      2     0%
168
            {tmpdir}/tests/python/project_test/__init__.py                 0      0   100%
169
            {tmpdir}/tests/python/project_test/no_src/__init__.py          0      0   100%
170
            {tmpdir}/tests/python/project_test/no_src/test_no_src.py       2      0   100%
171
            {tmpdir}/tests/python/project_test/test_arithmetic.py          3      0   100%
172
            {tmpdir}/tests/python/project_test/test_multiply.py            3      0   100%
173
            ---------------------------------------------------------------------------------
174
            TOTAL                                                            19      2    89%
175
            """
176
        )
177
        in result.stderr
178
    )
179

180

181
@pytest.mark.platform_specific_behavior
2✔
182
@pytest.mark.parametrize(
2✔
183
    "major_minor_interpreter",
184
    all_major_minor_python_versions(CoverageSubsystem.default_interpreter_constraints),
185
)
186
def test_coverage_batched(major_minor_interpreter: str) -> None:
2✔
187
    with setup_tmpdir(sources(True)) as tmpdir:
2✔
188
        result = run_coverage(
2✔
189
            tmpdir,
190
            f"--python-interpreter-constraints=['=={major_minor_interpreter}.*']",
191
            f"--coverage-py-interpreter-constraints=['=={major_minor_interpreter}.*']",
192
        )
193
    assert (
2✔
194
        dedent(
195
            f"""\
196
            Name                                                          Stmts   Miss  Cover
197
            ---------------------------------------------------------------------------------
198
            {tmpdir}/src/python/project/__init__.py                        0      0   100%
199
            {tmpdir}/src/python/project/lib.py                             6      0   100%
200
            {tmpdir}/src/python/project/lib_test.py                        3      0   100%
201
            {tmpdir}/src/python/project/random.py                          2      2     0%
202
            {tmpdir}/tests/python/project_test/__init__.py                 0      0   100%
203
            {tmpdir}/tests/python/project_test/no_src/__init__.py          0      0   100%
204
            {tmpdir}/tests/python/project_test/no_src/test_no_src.py       2      0   100%
205
            {tmpdir}/tests/python/project_test/test_arithmetic.py          3      0   100%
206
            {tmpdir}/tests/python/project_test/test_multiply.py            3      0   100%
207
            ---------------------------------------------------------------------------------
208
            TOTAL                                                            19      2    89%
209
            """
210
        )
211
        in result.stderr
212
    )
213

214

215
@pytest.mark.parametrize("batched", (True, False))
2✔
216
def test_coverage_fail_under(batched: bool) -> None:
2✔
UNCOV
217
    with setup_tmpdir(sources(batched)) as tmpdir:
×
UNCOV
218
        result = run_coverage(tmpdir, "--coverage-py-fail-under=89")
×
UNCOV
219
        result.assert_success()
×
UNCOV
220
        result = run_coverage_that_may_fail(tmpdir, "--coverage-py-fail-under=90")
×
UNCOV
221
        result.assert_failure()
×
222

223

224
@pytest.mark.parametrize("batched", (True, False))
2✔
225
def test_coverage_global(batched: bool) -> None:
2✔
UNCOV
226
    with setup_tmpdir(sources(batched)) as tmpdir:
×
UNCOV
227
        result = run_coverage(tmpdir, "--coverage-py-global-report")
×
UNCOV
228
    assert (
×
229
        dedent(
230
            f"""\
231
            Name                                                          Stmts   Miss  Cover
232
            ---------------------------------------------------------------------------------
233
            {tmpdir}/foo/bar.py                                            1      1     0%
234
            {tmpdir}/src/python/core/__init__.py                           0      0   100%
235
            {tmpdir}/src/python/core/untested.py                           1      1     0%
236
            {tmpdir}/src/python/project/__init__.py                        0      0   100%
237
            {tmpdir}/src/python/project/lib.py                             6      0   100%
238
            {tmpdir}/src/python/project/lib_test.py                        3      0   100%
239
            {tmpdir}/src/python/project/random.py                          2      2     0%
240
            {tmpdir}/tests/python/project_test/__init__.py                 0      0   100%
241
            {tmpdir}/tests/python/project_test/no_src/BUILD.py             1      1     0%
242
            {tmpdir}/tests/python/project_test/no_src/__init__.py          0      0   100%
243
            {tmpdir}/tests/python/project_test/no_src/test_no_src.py       2      0   100%
244
            {tmpdir}/tests/python/project_test/test_arithmetic.py          3      0   100%
245
            {tmpdir}/tests/python/project_test/test_multiply.py            3      0   100%
246
            ---------------------------------------------------------------------------------
247
            TOTAL                                                            22      5    77%
248
            """
249
        )
250
        in result.stderr
251
    ), result.stderr
252

253

254
@pytest.mark.parametrize(
2✔
255
    ("setting", "expected"),
256
    [
257
        (
258
            "",
259
            [
260
                "/tests/python/namespace/bar_test.py       2      0   100%",
261
                "TOTAL                                                2      0   100%",
262
            ],
263
        ),
264
        (
265
            "include_namespace_packages = false",
266
            [
267
                "/tests/python/namespace/bar_test.py       2      0   100%",
268
                "TOTAL                                                2      0   100%",
269
            ],
270
        ),
271
        (
272
            "include_namespace_packages = true",
273
            [
274
                "/src/python/namespace/foo.py              1      1     0%",
275
                "/tests/python/namespace/bar_test.py       2      0   100%",
276
                "TOTAL                                                3      1    67%",
277
            ],
278
        ),
279
    ],
280
)
281
def test_coverage_global_with_namespaced_package(setting: str, expected: Sequence[str]) -> None:
2✔
UNCOV
282
    files = {
×
283
        "pyproject.toml": f"[tool.coverage.report]\n{setting}",
284
        "src/python/namespace/BUILD": "python_sources()",
285
        "src/python/namespace/foo.py": "BAR = True",
286
        "tests/python/namespace/BUILD": "python_tests()",
287
        "tests/python/namespace/bar_test.py": "def test_true():\n    assert True is True",
288
    }
UNCOV
289
    with setup_tmpdir(files) as tmpdir:
×
UNCOV
290
        command = [
×
291
            "--backend-packages=pants.backend.python",
292
            "test",
293
            "--use-coverage",
294
            f"{tmpdir}/tests/python/namespace/bar_test.py",
295
            f"--source-root-patterns=['/{tmpdir}/src', '/{tmpdir}/tests']",
296
            "--coverage-py-global-report",
297
            f"--coverage-py-config={tmpdir}/pyproject.toml",
298
        ]
UNCOV
299
        result = run_pants(command)
×
300

UNCOV
301
    for line in expected:
×
UNCOV
302
        assert line in result.stderr, result.stderr
×
303

304

305
@pytest.mark.parametrize("batched", (True, False))
2✔
306
def test_coverage_with_filter(batched: bool) -> None:
2✔
UNCOV
307
    with setup_tmpdir(sources(batched)) as tmpdir:
×
UNCOV
308
        result = run_coverage(tmpdir, "--coverage-py-filter=['project.lib', 'project_test.no_src']")
×
UNCOV
309
    assert (
×
310
        dedent(
311
            f"""\
312
            Name                                                          Stmts   Miss  Cover
313
            ---------------------------------------------------------------------------------
314
            {tmpdir}/src/python/project/lib.py                             6      0   100%
315
            {tmpdir}/tests/python/project_test/no_src/__init__.py          0      0   100%
316
            {tmpdir}/tests/python/project_test/no_src/test_no_src.py       2      0   100%
317
            ---------------------------------------------------------------------------------
318
            TOTAL                                                             8      0   100%
319
            """
320
        )
321
        in result.stderr
322
    )
323

324

325
@pytest.mark.parametrize("batched", (True, False))
2✔
326
def test_coverage_raw(batched: bool) -> None:
2✔
UNCOV
327
    with setup_tmpdir(sources(batched)) as tmpdir:
×
UNCOV
328
        result = run_coverage(tmpdir, "--coverage-py-report=raw")
×
UNCOV
329
    assert "Wrote raw coverage report to `dist/coverage/python`" in result.stderr
×
UNCOV
330
    coverage_data = Path(get_buildroot(), "dist", "coverage", "python", ".coverage")
×
UNCOV
331
    assert coverage_data.exists() is True
×
UNCOV
332
    conn = sqlite3.connect(coverage_data.as_posix())
×
UNCOV
333
    cursor = conn.cursor()
×
UNCOV
334
    cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
×
UNCOV
335
    assert {row[0] for row in cursor.fetchall()} == {
×
336
        "arc",
337
        "context",
338
        "coverage_schema",
339
        "file",
340
        "line_bits",
341
        "meta",
342
        "tracer",
343
    }
344

345

346
@pytest.mark.parametrize("batched", (True, False))
2✔
347
def test_coverage_html_xml_json_lcov(batched: bool) -> None:
2✔
UNCOV
348
    with setup_tmpdir(sources(batched)) as tmpdir:
×
UNCOV
349
        result = run_coverage(tmpdir, "--coverage-py-report=['xml', 'html', 'json', 'lcov']")
×
UNCOV
350
    coverage_path = Path(get_buildroot(), "dist", "coverage", "python")
×
UNCOV
351
    assert coverage_path.exists() is True
×
352

UNCOV
353
    assert "Wrote xml coverage report to `dist/coverage/python`" in result.stderr
×
UNCOV
354
    xml_coverage = coverage_path / "coverage.xml"
×
UNCOV
355
    assert xml_coverage.exists() is True
×
356

UNCOV
357
    assert "Wrote html coverage report to `dist/coverage/python`" in result.stderr
×
UNCOV
358
    html_cov_dir = coverage_path / "htmlcov"
×
UNCOV
359
    assert html_cov_dir.exists() is True
×
UNCOV
360
    assert (html_cov_dir / "index.html").exists() is True
×
361

UNCOV
362
    assert "Wrote json coverage report to `dist/coverage/python`" in result.stderr
×
UNCOV
363
    json_coverage = coverage_path / "coverage.json"
×
UNCOV
364
    assert json_coverage.exists() is True
×
365

UNCOV
366
    assert "Wrote lcov coverage report to `dist/coverage/python`" in result.stderr
×
UNCOV
367
    json_coverage = coverage_path / "coverage.lcov"
×
UNCOV
368
    assert json_coverage.exists() is True
×
369

370

371
def test_default_coverage_issues_12390() -> None:
2✔
372
    # N.B.: This ~replicates the repo used to reproduce this issue at
373
    # https://github.com/alexey-tereshenkov-oxb/monorepo-coverage-pants.
UNCOV
374
    if platform.system() == "Darwin" and platform.machine() == "arm64":
×
375
        pytest.skip(reason="No PySide2 wheels available for macOS M1")
×
UNCOV
376
    if platform.system() == "Linux" and platform.machine() == "aarch64":
×
377
        pytest.skip(reason="No PySide2 wheels available for Linux ARM")
×
UNCOV
378
    files = {
×
379
        "requirements.txt": "PySide2==5.15.2",
380
        "BUILD": dedent(
381
            """\
382
            python_requirements(
383
                module_mapping={{
384
                    "PySide2": ["PySide2"],
385
                }},
386
            )
387
            """
388
        ),
389
        "minimalcov/minimalcov/src/foo.py": 'print("In the foo module!")',
390
        "minimalcov/minimalcov/src/BUILD": "python_sources()",
391
        "minimalcov/minimalcov/tests/test_foo.py": dedent(
392
            """\
393
            import minimalcov.src.foo
394

395
            from PySide2.QtWidgets import QApplication
396

397
            def test_1():
398
                assert True
399
            """
400
        ),
401
        "minimalcov/minimalcov/tests/BUILD": "python_tests()",
402
    }
UNCOV
403
    with setup_tmpdir(files) as tmpdir:
×
UNCOV
404
        command = [
×
405
            "--backend-packages=pants.backend.python",
406
            "test",
407
            "--use-coverage",
408
            "::",
409
            f"--source-root-patterns=['/{tmpdir}/minimalcov']",
410
            "--coverage-py-report=raw",
411
        ]
UNCOV
412
        result = run_pants(command)
×
UNCOV
413
        result.assert_success()
×
414

UNCOV
415
    assert (
×
416
        dedent(
417
            f"""\
418
            Name                                                  Stmts   Miss  Cover
419
            -------------------------------------------------------------------------
420
            {tmpdir}/minimalcov/minimalcov/src/foo.py              1      0   100%
421
            {tmpdir}/minimalcov/minimalcov/tests/test_foo.py       4      0   100%
422
            -------------------------------------------------------------------------
423
            TOTAL                                                     5      0   100%
424
            """
425
        )
426
        in result.stderr
427
    ), result.stderr
428

429

430
def test_coverage_filter_issue_18057() -> None:
2✔
UNCOV
431
    files = {
×
432
        # Coverage report should include all the files.
433
        "minimalcov/minimalcov/tests/BUILD": "python_tests()",
434
        "minimalcov/minimalcov/__init__.py": "",
435
        "minimalcov/minimalcov/src/__init__.py": "",
436
        "minimalcov/minimalcov/src/foo.py": 'print("In the foo module!")',
437
        "minimalcov/minimalcov/src/foo_not_covered.py": 'print("No test coverage!")',
438
        "minimalcov/minimalcov/src/BUILD": "python_sources()",
439
        "minimalcov/minimalcov/tests/test_foo.py": dedent(
440
            """\
441
            import minimalcov.src.foo
442

443
            def test_1():
444
                assert True
445
            """
446
        ),
447
        "minimalcov/minimalcov/tests/BUILD": "python_tests()",
448
        # Coverage report should only include those files which are imported in a test module.
449
        "minimalcov/minimalcov2/__init__.py": "",
450
        "minimalcov/minimalcov2/src/__init__.py": "",
451
        "minimalcov/minimalcov2/src/foo.py": 'print("In the foo module!")',
452
        "minimalcov/minimalcov2/src/foo_not_covered.py": 'print("No test coverage!")',
453
        "minimalcov/minimalcov2/src/BUILD": "python_sources()",
454
        "minimalcov/minimalcov2/tests/test_foo.py": dedent(
455
            """\
456
            import minimalcov2.src.foo
457

458
            def test_2():
459
                assert True
460
            """
461
        ),
462
        "minimalcov/minimalcov2/tests/BUILD": "python_tests()",
463
    }
464

UNCOV
465
    with setup_tmpdir(files) as tmpdir:
×
UNCOV
466
        command = [
×
467
            "--backend-packages=pants.backend.python",
468
            "test",
469
            "--use-coverage",
470
            "::",
471
            f"--source-root-patterns=['/{tmpdir}/minimalcov']",
472
            f"--coverage-py-filter=['{tmpdir}/minimalcov/minimalcov', 'minimalcov2']",
473
            "--coverage-py-report=raw",
474
        ]
UNCOV
475
        result = run_pants(command)
×
UNCOV
476
        result.assert_success()
×
UNCOV
477
    assert (
×
478
        dedent(
479
            f"""\
480
            Name                                                       Stmts   Miss  Cover
481
            ------------------------------------------------------------------------------
482
            {tmpdir}/minimalcov/minimalcov2/__init__.py                 0      0   100%
483
            {tmpdir}/minimalcov/minimalcov2/src/__init__.py             0      0   100%
484
            {tmpdir}/minimalcov/minimalcov2/src/foo.py                  1      0   100%
485
            {tmpdir}/minimalcov/minimalcov/__init__.py                  0      0   100%
486
            {tmpdir}/minimalcov/minimalcov/src/__init__.py              0      0   100%
487
            {tmpdir}/minimalcov/minimalcov/src/foo.py                   1      0   100%
488
            {tmpdir}/minimalcov/minimalcov/src/foo_not_covered.py       1      1     0%
489
            {tmpdir}/minimalcov/minimalcov/tests/test_foo.py            3      0   100%
490
            ------------------------------------------------------------------------------
491
            TOTAL                                                          6      1    83%
492
            """
493
        )
494
        in result.stderr
495
    ), result.stderr
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc