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

pantsbuild / pants / 18517631058

15 Oct 2025 04:18AM UTC coverage: 69.207% (-11.1%) from 80.267%
18517631058

Pull #22745

github

web-flow
Merge 642a76ca1 into 99919310e
Pull Request #22745: [windows] Add windows support in the stdio crate.

53815 of 77759 relevant lines covered (69.21%)

2.42 hits per line

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

50.76
/src/python/pants/backend/python/lint/ruff/rules_integration_test.py
1
# Copyright 2023 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
from pathlib import Path
2✔
7

8
import pytest
2✔
9

10
from pants.backend.python import target_types_rules
2✔
11
from pants.backend.python.lint.ruff import skip_field
2✔
12
from pants.backend.python.lint.ruff.check import skip_field as ruff_check_skip_field
2✔
13
from pants.backend.python.lint.ruff.check.rules import (
2✔
14
    RuffCheckFieldSet,
15
    RuffFixRequest,
16
    RuffLintRequest,
17
)
18
from pants.backend.python.lint.ruff.check.rules import rules as ruff_check_rules
2✔
19
from pants.backend.python.lint.ruff.check.skip_field import SkipRuffCheckField
2✔
20
from pants.backend.python.lint.ruff.format import skip_field as ruff_format_skip_field
2✔
21
from pants.backend.python.lint.ruff.format.rules import RuffFormatFieldSet, RuffFormatRequest
2✔
22
from pants.backend.python.lint.ruff.format.rules import rules as ruff_fmt_rules
2✔
23
from pants.backend.python.lint.ruff.format.skip_field import SkipRuffFormatField
2✔
24
from pants.backend.python.lint.ruff.skip_field import SkipRuffField
2✔
25
from pants.backend.python.lint.ruff.subsystem import rules as ruff_subsystem_rules
2✔
26
from pants.backend.python.target_types import PythonSourcesGeneratorTarget
2✔
27
from pants.core.goals.fix import FixResult
2✔
28
from pants.core.goals.fmt import FmtResult
2✔
29
from pants.core.goals.lint import LintResult
2✔
30
from pants.core.util_rules import config_files
2✔
31
from pants.core.util_rules.partitions import _EmptyMetadata
2✔
32
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
2✔
33
from pants.engine.addresses import Address
2✔
34
from pants.engine.fs import DigestContents
2✔
35
from pants.engine.target import Target
2✔
36
from pants.testutil.python_interpreter_selection import all_major_minor_python_versions
2✔
37
from pants.testutil.rule_runner import QueryRule, RuleRunner
2✔
38

39
GOOD_FILE = 'a = "string without any placeholders"\n'
2✔
40
BAD_FILE = 'a = f"string without any placeholders"\n'
2✔
41
UNFORMATTED_FILE = 'a ="string without any placeholders"\n'
2✔
42

43

44
@pytest.fixture
2✔
45
def rule_runner() -> RuleRunner:
2✔
46
    return RuleRunner(
2✔
47
        rules=[
48
            *ruff_check_rules(),
49
            *ruff_fmt_rules(),
50
            *skip_field.rules(),
51
            *ruff_check_skip_field.rules(),
52
            *ruff_format_skip_field.rules(),
53
            *ruff_subsystem_rules(),
54
            *config_files.rules(),
55
            *target_types_rules.rules(),
56
            QueryRule(FixResult, [RuffFixRequest.Batch]),
57
            QueryRule(LintResult, [RuffLintRequest.Batch]),
58
            QueryRule(FmtResult, [RuffFormatRequest.Batch]),
59
            QueryRule(SourceFiles, (SourceFilesRequest,)),
60
        ],
61
        target_types=[PythonSourcesGeneratorTarget],
62
    )
63

64

65
def run_ruff(
2✔
66
    rule_runner: RuleRunner,
67
    targets: list[Target],
68
    *,
69
    extra_args: list[str] | None = None,
70
) -> tuple[FixResult, LintResult, FmtResult]:
71
    args = ["--backend-packages=pants.backend.python.lint.ruff", *(extra_args or ())]
2✔
72
    rule_runner.set_options(args, env_inherit={"PATH", "PYENV_ROOT", "HOME"})
2✔
73

74
    check_field_sets = [
2✔
75
        RuffCheckFieldSet.create(tgt) for tgt in targets if RuffCheckFieldSet.is_applicable(tgt)
76
    ]
77
    check_source_reqs = [SourceFilesRequest(field_set.source for field_set in check_field_sets)]
2✔
78
    check_input_sources = rule_runner.request(SourceFiles, check_source_reqs)
2✔
79

80
    format_field_sets = [
2✔
81
        RuffFormatFieldSet.create(tgt) for tgt in targets if RuffFormatFieldSet.is_applicable(tgt)
82
    ]
83
    format_source_reqs = [SourceFilesRequest(field_set.source for field_set in format_field_sets)]
2✔
84
    format_input_sources = rule_runner.request(SourceFiles, format_source_reqs)
2✔
85

86
    fix_result = rule_runner.request(
2✔
87
        FixResult,
88
        [
89
            RuffFixRequest.Batch(
90
                "",
91
                tuple(check_field_sets),
92
                partition_metadata=_EmptyMetadata(),
93
                snapshot=check_input_sources.snapshot,
94
            ),
95
        ],
96
    )
97
    lint_result = rule_runner.request(
2✔
98
        LintResult,
99
        [
100
            RuffLintRequest.Batch(
101
                "",
102
                tuple(check_field_sets),
103
                partition_metadata=_EmptyMetadata(),
104
            ),
105
        ],
106
    )
107
    fmt_result = rule_runner.request(
2✔
108
        FmtResult,
109
        [
110
            RuffFormatRequest.Batch(
111
                "",
112
                tuple(format_field_sets),
113
                partition_metadata=_EmptyMetadata(),
114
                snapshot=format_input_sources.snapshot,
115
            )
116
        ],
117
    )
118

119
    return fix_result, lint_result, fmt_result
2✔
120

121

122
@pytest.mark.platform_specific_behavior
2✔
123
@pytest.mark.parametrize(
2✔
124
    "major_minor_interpreter",
125
    all_major_minor_python_versions(["CPython>=3.9,<4"]),
126
)
127
def test_passing(rule_runner: RuleRunner, major_minor_interpreter: str) -> None:
2✔
128
    rule_runner.write_files({"f.py": GOOD_FILE, "BUILD": "python_sources(name='t')"})
2✔
129
    tgt = rule_runner.get_target(Address("", target_name="t", relative_file_path="f.py"))
2✔
130
    fix_result, lint_result, fmt_result = run_ruff(
2✔
131
        rule_runner,
132
        [tgt],
133
        extra_args=[f"--python-interpreter-constraints=['=={major_minor_interpreter}.*']"],
134
    )
135
    assert lint_result.exit_code == 0
2✔
136
    assert fix_result.stderr == ""
2✔
137
    assert fix_result.stdout == "All checks passed!\n"
2✔
138
    assert not fix_result.did_change
2✔
139
    assert fix_result.output == rule_runner.make_snapshot({"f.py": GOOD_FILE})
2✔
140
    assert not fmt_result.did_change
2✔
141
    assert fmt_result.output == rule_runner.make_snapshot({"f.py": GOOD_FILE})
2✔
142

143

144
def test_failing(rule_runner: RuleRunner) -> None:
2✔
145
    rule_runner.write_files({"f.py": BAD_FILE, "BUILD": "python_sources(name='t')"})
×
146
    tgt = rule_runner.get_target(Address("", target_name="t", relative_file_path="f.py"))
×
147
    fix_result, lint_result, fmt_result = run_ruff(rule_runner, [tgt])
×
148
    assert lint_result.exit_code == 1
×
149
    assert fix_result.stdout == "Found 1 error (1 fixed, 0 remaining).\n"
×
150
    assert fix_result.stderr == ""
×
151
    assert fix_result.did_change
×
152
    assert fix_result.output == rule_runner.make_snapshot({"f.py": GOOD_FILE})
×
153
    assert not fmt_result.did_change
×
154
    assert fmt_result.output == rule_runner.make_snapshot({"f.py": BAD_FILE})
×
155

156

157
def test_multiple_targets(rule_runner: RuleRunner) -> None:
2✔
158
    rule_runner.write_files(
×
159
        {
160
            "good.py": GOOD_FILE,
161
            "bad.py": BAD_FILE,
162
            "unformatted.py": UNFORMATTED_FILE,
163
            "BUILD": "python_sources(name='t')",
164
        }
165
    )
166
    tgts = [
×
167
        rule_runner.get_target(Address("", target_name="t", relative_file_path="good.py")),
168
        rule_runner.get_target(Address("", target_name="t", relative_file_path="bad.py")),
169
        rule_runner.get_target(Address("", target_name="t", relative_file_path="unformatted.py")),
170
    ]
171
    fix_result, lint_result, fmt_result = run_ruff(rule_runner, tgts)
×
172
    assert lint_result.exit_code == 1
×
173
    assert fix_result.output == rule_runner.make_snapshot(
×
174
        {"good.py": GOOD_FILE, "bad.py": GOOD_FILE, "unformatted.py": UNFORMATTED_FILE}
175
    )
176
    assert fix_result.did_change is True
×
177
    assert fmt_result.output == rule_runner.make_snapshot(
×
178
        {"good.py": GOOD_FILE, "bad.py": BAD_FILE, "unformatted.py": GOOD_FILE}
179
    )
180
    assert fmt_result.did_change is True
×
181

182

183
def test_skip_field(rule_runner: RuleRunner) -> None:
2✔
184
    rule_runner.write_files(
×
185
        {
186
            "good.py": GOOD_FILE,
187
            "bad.py": BAD_FILE,
188
            "unformatted.py": UNFORMATTED_FILE,
189
            "BUILD": "python_sources(name='t', skip_ruff=True)",
190
        }
191
    )
192
    tgts = [
×
193
        rule_runner.get_target(Address("", target_name="t", relative_file_path="good.py")),
194
        rule_runner.get_target(Address("", target_name="t", relative_file_path="bad.py")),
195
        rule_runner.get_target(Address("", target_name="t", relative_file_path="unformatted.py")),
196
    ]
197
    for tgt in tgts:
×
198
        assert tgt.get(SkipRuffField).value is True
×
199

200
    fix_result, lint_result, fmt_result = run_ruff(rule_runner, tgts)
×
201

202
    assert lint_result.exit_code == 0
×
203
    assert fix_result.output == rule_runner.make_snapshot({})
×
204
    assert fix_result.did_change is False
×
205
    assert fmt_result.output == rule_runner.make_snapshot({})
×
206
    assert fmt_result.did_change is False
×
207

208

209
def test_skip_check_field(rule_runner: RuleRunner) -> None:
2✔
210
    rule_runner.write_files(
×
211
        {
212
            "good.py": GOOD_FILE,
213
            "bad.py": BAD_FILE,
214
            "unformatted.py": UNFORMATTED_FILE,
215
            "BUILD": "python_sources(name='t', skip_ruff_check=True)",
216
        }
217
    )
218
    tgts = [
×
219
        rule_runner.get_target(Address("", target_name="t", relative_file_path="good.py")),
220
        rule_runner.get_target(Address("", target_name="t", relative_file_path="bad.py")),
221
        rule_runner.get_target(Address("", target_name="t", relative_file_path="unformatted.py")),
222
    ]
223
    for tgt in tgts:
×
224
        assert tgt.get(SkipRuffCheckField).value is True
×
225

226
    fix_result, lint_result, fmt_result = run_ruff(rule_runner, tgts)
×
227

228
    assert lint_result.exit_code == 0
×
229
    assert fix_result.output == rule_runner.make_snapshot({})
×
230
    assert fix_result.did_change is False
×
231
    assert fmt_result.output == rule_runner.make_snapshot(
×
232
        {"good.py": GOOD_FILE, "bad.py": BAD_FILE, "unformatted.py": GOOD_FILE}
233
    )
234
    assert fmt_result.did_change is True
×
235

236

237
def test_skip_format_field(rule_runner: RuleRunner) -> None:
2✔
238
    rule_runner.write_files(
×
239
        {
240
            "good.py": GOOD_FILE,
241
            "bad.py": BAD_FILE,
242
            "unformatted.py": UNFORMATTED_FILE,
243
            "BUILD": "python_sources(name='t', skip_ruff_format=True)",
244
        }
245
    )
246
    tgts = [
×
247
        rule_runner.get_target(Address("", target_name="t", relative_file_path="good.py")),
248
        rule_runner.get_target(Address("", target_name="t", relative_file_path="bad.py")),
249
        rule_runner.get_target(Address("", target_name="t", relative_file_path="unformatted.py")),
250
    ]
251
    for tgt in tgts:
×
252
        assert tgt.get(SkipRuffFormatField).value is True
×
253

254
    fix_result, lint_result, fmt_result = run_ruff(rule_runner, tgts)
×
255

256
    assert lint_result.exit_code == 1
×
257
    assert fix_result.output == rule_runner.make_snapshot(
×
258
        {"good.py": GOOD_FILE, "bad.py": GOOD_FILE, "unformatted.py": UNFORMATTED_FILE}
259
    )
260
    assert fix_result.did_change is True
×
261
    assert fmt_result.output == rule_runner.make_snapshot({})
×
262
    assert fmt_result.did_change is False
×
263

264

265
@pytest.mark.parametrize(
2✔
266
    "file_path,config_path,extra_args,should_change",
267
    (
268
        [Path("f.py"), Path("pyproject.toml"), [], False],
269
        [Path("f.py"), Path("ruff.toml"), [], False],
270
        [Path("custom/f.py"), Path("custom/ruff.toml"), [], False],
271
        [Path("custom/f.py"), Path("custom/pyproject.toml"), [], False],
272
        [Path("f.py"), Path("custom/ruff.toml"), ["--ruff-config=custom/ruff.toml"], False],
273
        [Path("f.py"), Path("custom/ruff.toml"), [], True],
274
    ),
275
)
276
def test_config_file(
2✔
277
    rule_runner: RuleRunner,
278
    file_path: Path,
279
    config_path: Path,
280
    extra_args: list[str],
281
    should_change: bool,
282
) -> None:
283
    hierarchy = "[tool.ruff]\n" if config_path.stem == "pyproject" else ""
×
284
    rule_runner.write_files(
×
285
        {
286
            file_path: BAD_FILE,
287
            file_path.parent / "BUILD": "python_sources()",
288
            config_path: f'{hierarchy}ignore = ["F541"]',
289
        }
290
    )
291
    spec_path = str(file_path.parent).replace(".", "")
×
292
    rel_file_path = file_path.relative_to(*file_path.parts[:1]) if spec_path else file_path
×
293
    addr = Address(spec_path, relative_file_path=str(rel_file_path))
×
294
    tgt = rule_runner.get_target(addr)
×
295
    fix_result, lint_result, fmt_result = run_ruff(rule_runner, [tgt], extra_args=extra_args)
×
296
    assert lint_result.exit_code == bool(should_change)
×
297
    assert fix_result.did_change is should_change
×
298

299

300
def test_report_file(rule_runner: RuleRunner) -> None:
2✔
301
    rule_runner.write_files({"f.py": BAD_FILE, "BUILD": "python_sources(name='t')"})
×
302
    tgt = rule_runner.get_target(Address("", target_name="t", relative_file_path="f.py"))
×
303
    fix_result, lint_result, fmt_result = run_ruff(
×
304
        rule_runner,
305
        [tgt],
306
        extra_args=["--ruff-args='--output-file=reports/foo.txt'"],
307
    )
308
    assert lint_result.exit_code == 1
×
309
    report_files = rule_runner.request(DigestContents, [lint_result.report])
×
310
    assert len(report_files) == 1
×
311
    assert "f.py:1:5" in report_files[0].content.decode()
×
312
    assert "F541" in report_files[0].content.decode()
×
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