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

pantsbuild / pants / 24604025132

18 Apr 2026 11:49AM UTC coverage: 92.478% (-0.4%) from 92.924%
24604025132

Pull #23268

github

web-flow
Merge c60f47029 into a92bc34b6
Pull Request #23268: perf: Remove python coroutine/trampoline overhead in awaits for ~22% faster `dependencies` goal

31 of 37 new or added lines in 4 files covered. (83.78%)

443 existing lines in 21 files now uncovered.

91210 of 98629 relevant lines covered (92.48%)

4.03 hits per line

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

94.15
/src/python/pants/engine/internals/build_files_test.py
1
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
1✔
5

6
import logging
1✔
7
import re
1✔
8
from collections.abc import Mapping
1✔
9
from textwrap import dedent
1✔
10
from typing import Any
1✔
11

12
import pytest
1✔
13

14
from pants.build_graph.address import BuildFileAddressRequest, MaybeAddress, ResolveError
1✔
15
from pants.build_graph.build_file_aliases import BuildFileAliases
1✔
16
from pants.core.target_types import GenericTarget, ResourceTarget
1✔
17
from pants.engine.addresses import Address, AddressInput, BuildFileAddress
1✔
18
from pants.engine.env_vars import CompleteEnvironmentVars, EnvironmentVars
1✔
19
from pants.engine.fs import DigestContents, FileContent
1✔
20
from pants.engine.internals.build_files import (
1✔
21
    AddressFamilyDir,
22
    BUILDFileEnvVarExtractor,
23
    BuildFileOptions,
24
    BuildFileSyntaxError,
25
    OptionalAddressFamily,
26
    evaluate_preludes,
27
    parse_address_family,
28
)
29
from pants.engine.internals.defaults import BuildFileDefaults, ParametrizeDefault
1✔
30
from pants.engine.internals.dep_rules import MaybeBuildFileDependencyRulesImplementation
1✔
31
from pants.engine.internals.mapper import AddressFamily
1✔
32
from pants.engine.internals.parametrize import Parametrize
1✔
33
from pants.engine.internals.parser import BuildFilePreludeSymbols, BuildFileSymbolInfo, Parser
1✔
34
from pants.engine.internals.scheduler import ExecutionError
1✔
35
from pants.engine.internals.session import SessionValues
1✔
36
from pants.engine.internals.synthetic_targets import SyntheticAddressMap, SyntheticAddressMaps
1✔
37
from pants.engine.internals.target_adaptor import TargetAdaptor, TargetAdaptorRequest
1✔
38
from pants.engine.target import (
1✔
39
    Dependencies,
40
    MultipleSourcesField,
41
    OverridesField,
42
    RegisteredTargetTypes,
43
    SingleSourceField,
44
    StringField,
45
    Tags,
46
    Target,
47
    TargetFilesGenerator,
48
)
49
from pants.engine.unions import UnionMembership
1✔
50
from pants.init.bootstrap_scheduler import BootstrapStatus
1✔
51
from pants.testutil.pytest_util import assert_logged
1✔
52
from pants.testutil.rule_runner import QueryRule, RuleRunner, engine_error, run_rule_with_mocks
1✔
53
from pants.util.frozendict import FrozenDict
1✔
54
from pants.util.strutil import softwrap
1✔
55

56

57
def test_parse_address_family_empty() -> None:
1✔
58
    """Test that parsing an empty BUILD file results in an empty AddressFamily."""
59
    optional_af = run_rule_with_mocks(
1✔
60
        parse_address_family,
61
        rule_args=[
62
            AddressFamilyDir("/dev/null"),
63
            Parser(
64
                build_root="",
65
                registered_target_types=RegisteredTargetTypes({}),
66
                union_membership=UnionMembership.empty(),
67
                object_aliases=BuildFileAliases(),
68
                ignore_unrecognized_symbols=False,
69
            ),
70
            BootstrapStatus(in_progress=False),
71
            BuildFileOptions(("BUILD",)),
72
            BuildFilePreludeSymbols(FrozenDict(), ()),
73
            RegisteredTargetTypes({}),
74
            UnionMembership.empty(),
75
            MaybeBuildFileDependencyRulesImplementation(None),
76
            SessionValues({CompleteEnvironmentVars: CompleteEnvironmentVars({})}),
77
        ],
78
        mock_calls={
79
            "pants.engine.intrinsics.get_digest_contents": lambda __implicitly: DigestContents(
80
                [FileContent(path="/dev/null/BUILD", content=b"")]
81
            ),
82
            "pants.engine.internals.synthetic_targets.get_synthetic_address_maps": lambda _: SyntheticAddressMaps(),
83
            "pants.engine.internals.build_files.parse_address_family": lambda *_: OptionalAddressFamily(
84
                "/dev"
85
            ),
86
            "pants.core.util_rules.env_vars.environment_vars_subset": lambda _1,
87
            _2: EnvironmentVars({}),
88
        },
89
    )
90
    assert optional_af.path == "/dev/null"
1✔
UNCOV
91
    assert optional_af.address_family is not None
×
UNCOV
92
    af = optional_af.address_family
×
UNCOV
93
    assert af.namespace == "/dev/null"
×
UNCOV
94
    assert len(af.name_to_target_adaptors) == 0
×
95

96

97
def test_extend_synthetic_target() -> None:
1✔
98
    optional_af = run_rule_with_mocks(
1✔
99
        parse_address_family,
100
        rule_args=[
101
            AddressFamilyDir("/foo"),
102
            Parser(
103
                build_root="",
104
                registered_target_types=RegisteredTargetTypes({"resource": ResourceTarget}),
105
                union_membership=UnionMembership.empty(),
106
                object_aliases=BuildFileAliases(),
107
                ignore_unrecognized_symbols=False,
108
            ),
109
            BootstrapStatus(in_progress=False),
110
            BuildFileOptions(("BUILD",)),
111
            BuildFilePreludeSymbols(FrozenDict(), ()),
112
            RegisteredTargetTypes({"resource": ResourceTarget}),
113
            UnionMembership.empty(),
114
            MaybeBuildFileDependencyRulesImplementation(None),
115
            SessionValues({CompleteEnvironmentVars: CompleteEnvironmentVars({})}),
116
        ],
117
        mock_calls={
118
            "pants.engine.intrinsics.get_digest_contents": lambda __implicitly: DigestContents(
119
                [
120
                    FileContent(
121
                        path="/foo/BUILD.1", content=b"resource(name='aaa', description='a')"
122
                    ),
123
                    FileContent(
124
                        path="/foo/BUILD.2",
125
                        content=b"resource(name='bar', description='b', _extend_synthetic=True)",
126
                    ),
127
                ]
128
            ),
129
            "pants.engine.internals.synthetic_targets.get_synthetic_address_maps": lambda __implicitly: SyntheticAddressMaps(
130
                [
131
                    SyntheticAddressMap.create(
132
                        "/foo/synthetic1",
133
                        [
134
                            TargetAdaptor("resource", "xxx", "", description="x"),
135
                        ],
136
                    ),
137
                    SyntheticAddressMap.create(
138
                        "/foo/synthetic2",
139
                        [
140
                            TargetAdaptor("resource", "yyy", ""),
141
                            TargetAdaptor("resource", "bar", "", extend=42),
142
                        ],
143
                    ),
144
                ]
145
            ),
146
            "pants.engine.internals.build_files.parse_address_family": lambda __implicitly: OptionalAddressFamily(
147
                "/",
148
                address_family=AddressFamily.create(
149
                    "/",
150
                    [],
151
                    defaults=BuildFileDefaults(
152
                        FrozenDict({"resource": FrozenDict({"description": "q"})})
153
                    ),
154
                ),
155
            ),
156
            "pants.core.util_rules.env_vars.environment_vars_subset": lambda _1,
157
            _2: EnvironmentVars({}),
158
        },
159
    )
160
    assert optional_af.path == "/foo"
1✔
UNCOV
161
    assert optional_af.address_family is not None
×
UNCOV
162
    af = optional_af.address_family
×
UNCOV
163
    assert af.namespace == "/foo"
×
164

UNCOV
165
    path, tgt = af.name_to_target_adaptors["aaa"]
×
UNCOV
166
    assert path == "/foo/BUILD.1"
×
UNCOV
167
    assert tgt.kwargs == FrozenDict({"description": "a"})
×
168

UNCOV
169
    path, tgt = af.name_to_target_adaptors["xxx"]
×
UNCOV
170
    assert path == "/foo/synthetic1"
×
UNCOV
171
    assert tgt.kwargs == FrozenDict({"description": "x"})
×
172

UNCOV
173
    path, tgt = af.name_to_target_adaptors["yyy"]
×
UNCOV
174
    assert path == "/foo/synthetic2"
×
UNCOV
175
    assert tgt.kwargs == FrozenDict({"description": "q"})
×
176

UNCOV
177
    path, tgt = af.name_to_target_adaptors["bar"]
×
UNCOV
178
    assert path == "/foo/BUILD.2"
×
UNCOV
179
    assert tgt.kwargs == FrozenDict({"description": "b", "extend": 42})
×
180

181

182
def run_prelude_parsing_rule(prelude_content: str) -> BuildFilePreludeSymbols:
1✔
183
    symbols = run_rule_with_mocks(
1✔
184
        evaluate_preludes,
185
        rule_args=[
186
            BuildFileOptions((), prelude_globs=("prelude",)),
187
            Parser(
188
                build_root="",
189
                registered_target_types=RegisteredTargetTypes({"target": GenericTarget}),
190
                union_membership=UnionMembership.empty(),
191
                object_aliases=BuildFileAliases(),
192
                ignore_unrecognized_symbols=False,
193
            ),
194
        ],
195
        mock_calls={
196
            "pants.engine.intrinsics.get_digest_contents": lambda __implicitly: DigestContents(
197
                [FileContent(path="/dev/null/prelude", content=prelude_content.encode())]
198
            )
199
        },
200
    )
201
    return symbols
1✔
202

203

204
def test_prelude_parsing_good() -> None:
1✔
205
    prelude_content = dedent(
1✔
206
        """
207
        def bar():
208
            __defaults__(all=dict(ok=123))
209
            return build_file_dir()
210

211
        def foo():
212
            return 1
213
        """
214
    )
215
    result = run_prelude_parsing_rule(prelude_content)
1✔
216
    assert result.symbols["foo"]() == 1
1✔
217

218

219
def test_prelude_parsing_syntax_error() -> None:
1✔
220
    with pytest.raises(
1✔
221
        Exception, match="Error parsing prelude file /dev/null/prelude: name 'blah' is not defined"
222
    ):
223
        run_prelude_parsing_rule("blah")
1✔
224

225

226
def test_prelude_parsing_illegal_import() -> None:
1✔
227
    prelude_content = dedent(
1✔
228
        """\
229
        import os
230
        def make_target():
231
            python_sources()
232
        """
233
    )
234
    with pytest.raises(
1✔
235
        Exception,
236
        match="Import used in /dev/null/prelude at line 1\\. Import statements are banned",
237
    ):
238
        run_prelude_parsing_rule(prelude_content)
1✔
239

240

241
def test_prelude_check_filepath() -> None:
1✔
242
    prelude_content = dedent(
1✔
243
        """
244
        build_file_dir()
245
        """
246
    )
247
    with pytest.raises(
1✔
248
        Exception,
249
        match="The BUILD file symbol `build_file_dir` may only be used in BUILD files\\. If used",
250
    ):
251
        run_prelude_parsing_rule(prelude_content)
1✔
252

253

254
def test_prelude_check_defaults() -> None:
1✔
255
    prelude_content = dedent(
1✔
256
        """
257
        __defaults__(all=dict(bad=123))
258
        """
259
    )
260
    with pytest.raises(
1✔
261
        Exception,
262
        match="The BUILD file symbol `__defaults__` may only be used in BUILD files\\. If used",
263
    ):
264
        run_prelude_parsing_rule(prelude_content)
1✔
265

266

267
def test_prelude_check_env() -> None:
1✔
268
    prelude_content = dedent(
1✔
269
        """
270
        env("nope")
271
        """
272
    )
273
    with pytest.raises(
1✔
274
        Exception,
275
        match="The BUILD file symbol `env` may only be used in BUILD files\\. If used",
276
    ):
277
        run_prelude_parsing_rule(prelude_content)
1✔
278

279

280
def test_prelude_exceptions() -> None:
1✔
281
    prelude_content = dedent(
1✔
282
        """\
283
        def abort():
284
            raise ValueError
285
        """
286
    )
287
    result = run_prelude_parsing_rule(prelude_content)
1✔
288
    assert "ValueError" not in result.symbols
1✔
289
    with pytest.raises(ValueError):
1✔
290
        result.symbols["abort"]()
1✔
291

292

293
def test_prelude_references_builtin_symbols() -> None:
1✔
294
    prelude_content = dedent(
1✔
295
        """\
296
        def make_a_target():
297
            # Can't call it outside of the context of a BUILD file, less we get internal errors
298
            target
299
        """
300
    )
301
    result = run_prelude_parsing_rule(prelude_content)
1✔
302
    # In the real world, this would define the target (note it doesn't need to return, as BUILD files
303
    # don't). In the test we're just ensuring we don't get a `NameError`
304
    result.symbols["make_a_target"]()
1✔
305

306

307
def test_prelude_type_hint_code() -> None:
1✔
308
    # Issue 18435
309
    prelude_content = dedent(
1✔
310
        """\
311
        def ecr_docker_image(
312
            *,
313
            name: Optional[str] = None,
314
            dependencies: Optional[List[str]] = None,
315
            image_tags: Optional[List[str]] = None,
316
            git_tag_prefix: Optional[str] = None,
317
            latest_tag_prefix: Optional[str] = None,
318
            buildcache_tag: str = "buildcache",
319
            image_labels: Optional[Mapping[str, str]] = None,
320
            tags: Optional[List[str]] = None,
321
            extra_build_args: Optional[List[str]] = None,
322
            source: Optional[str] = None,
323
            target_stage: Optional[str] = None,
324
            instructions: Optional[list[str]] = None,
325
            repository: Optional[str] = None,
326
            context_root: Union[str, None] = None,
327
            push_in_pants_ci: bool = True,
328
            push_latest: bool = False,
329
        ) -> int:
330
            return 42
331
        """
332
    )
333
    result = run_prelude_parsing_rule(prelude_content)
1✔
334
    ecr_docker_image = result.info["ecr_docker_image"]
1✔
335
    assert ecr_docker_image.signature == (
1✔
336
        "(*,"
337
        " name: str | None = None,"
338
        " dependencies: List[str] | None = None,"
339
        " image_tags: List[str] | None = None,"
340
        " git_tag_prefix: str | None = None,"
341
        " latest_tag_prefix: str | None = None,"
342
        " buildcache_tag: str = 'buildcache',"
343
        " image_labels: Mapping[str, str] | None = None,"
344
        " tags: List[str] | None = None,"
345
        " extra_build_args: List[str] | None = None,"
346
        " source: str | None = None,"
347
        " target_stage: str | None = None,"
348
        " instructions: list[str] | None = None,"
349
        " repository: str | None = None,"
350
        " context_root: str | None = None,"
351
        " push_in_pants_ci: bool = True,"
352
        " push_latest: bool = False"
353
        ") -> int"
354
    )
355
    assert 42 == ecr_docker_image.value()
1✔
356

357

358
def test_prelude_docstring_on_function() -> None:
1✔
359
    macro_docstring = "This is the doc-string for `macro_func`."
1✔
360
    prelude_content = dedent(
1✔
361
        f"""
362
        def macro_func(arg: int) -> str:
363
            '''{macro_docstring}'''
364
            pass
365
        """
366
    )
367
    result = run_prelude_parsing_rule(prelude_content)
1✔
368
    info = result.info["macro_func"]
1✔
369
    assert BuildFileSymbolInfo("macro_func", result.symbols["macro_func"]) == info
1✔
370
    assert macro_docstring == info.help
1✔
371
    assert "(arg: int) -> str" == info.signature
1✔
372
    assert {"macro_func"} == set(result.info)
1✔
373

374

375
def test_prelude_docstring_on_constant() -> None:
1✔
376
    macro_docstring = """This is the doc-string for `MACRO_CONST`.
1✔
377

378
    Use weird indentations.
379

380
    On purpose.
381
    """
382
    prelude_content = dedent(
1✔
383
        f"""
384
        Number = NewType("Number", int)
385
        MACRO_CONST: Annotated[str, Doc({macro_docstring!r})] = "value"
386
        MULTI_HINTS: Annotated[Number, "unrelated", Doc("this is it"), 24] = 42
387
        ANON: str = "undocumented"
388
        _PRIVATE: int = 42
389
        untyped = True
390
        """
391
    )
392
    result = run_prelude_parsing_rule(prelude_content)
1✔
393

394
    assert {"MACRO_CONST", "ANON", "Number", "MULTI_HINTS", "_PRIVATE", "untyped"} == set(
1✔
395
        result.info
396
    )
397

398
    info = result.info["MACRO_CONST"]
1✔
399
    assert info.value == "value"
1✔
400
    assert info.help == softwrap(macro_docstring)
1✔
401
    assert info.signature == ": str"
1✔
402
    assert info.hide_from_help is False
1✔
403

404
    multi = result.info["MULTI_HINTS"]
1✔
405
    assert multi.value == 42
1✔
406
    assert multi.help == "this is it"
1✔
407
    assert multi.signature == ": Number"
1✔
408
    assert multi.hide_from_help is False
1✔
409

410
    anon = result.info["ANON"]
1✔
411
    assert anon.value == "undocumented"
1✔
412
    assert anon.help is None
1✔
413
    assert anon.signature == ": str"
1✔
414
    assert anon.hide_from_help is False
1✔
415

416
    private = result.info["_PRIVATE"]
1✔
417
    assert private.value == 42
1✔
418
    assert private.help is None
1✔
419
    assert private.signature == ": int"
1✔
420
    assert private.hide_from_help is True
1✔
421

422

423
def test_prelude_reference_env_vars() -> None:
1✔
424
    prelude_content = dedent(
1✔
425
        """
426
        def macro():
427
            env("MY_ENV")
428
        """
429
    )
430
    result = run_prelude_parsing_rule(prelude_content)
1✔
431
    assert ("MY_ENV",) == result.referenced_env_vars
1✔
432

433

434
class ResolveField(StringField):
1✔
435
    alias = "resolve"
1✔
436

437

438
class MockDepsField(Dependencies):
1✔
439
    pass
1✔
440

441

442
class MockMultipleSourcesField(MultipleSourcesField):
1✔
443
    default = ("*.mock",)
1✔
444

445

446
class MockTgt(Target):
1✔
447
    alias = "mock_tgt"
1✔
448
    core_fields = (MockDepsField, MockMultipleSourcesField, Tags, ResolveField)
1✔
449

450

451
class MockSingleSourceField(SingleSourceField):
1✔
452
    pass
1✔
453

454

455
class MockGeneratedTarget(Target):
1✔
456
    alias = "generated"
1✔
457
    core_fields = (MockDepsField, Tags, MockSingleSourceField, ResolveField)
1✔
458

459

460
class MockTargetGenerator(TargetFilesGenerator):
1✔
461
    alias = "generator"
1✔
462
    core_fields = (MockMultipleSourcesField, OverridesField)
1✔
463
    generated_target_cls = MockGeneratedTarget
1✔
464
    copied_fields = ()
1✔
465
    moved_fields = (MockDepsField, Tags, ResolveField)
1✔
466

467

468
def test_resolve_address() -> None:
1✔
469
    rule_runner = RuleRunner(
1✔
470
        rules=[QueryRule(Address, [AddressInput]), QueryRule(MaybeAddress, [AddressInput])]
471
    )
472
    rule_runner.write_files({"a/b/c.txt": "", "f.txt": ""})
1✔
473

474
    def assert_is_expected(address_input: AddressInput, expected: Address) -> None:
1✔
475
        assert rule_runner.request(Address, [address_input]) == expected
1✔
476

477
    assert_is_expected(
1✔
478
        AddressInput.parse("a/b/c.txt", description_of_origin="tests"),
479
        Address("a/b", target_name=None, relative_file_path="c.txt"),
480
    )
481
    assert_is_expected(
1✔
482
        AddressInput.parse("a/b", description_of_origin="tests"),
483
        Address("a/b", target_name=None, relative_file_path=None),
484
    )
485

486
    assert_is_expected(
1✔
487
        AddressInput.parse("a/b:c", description_of_origin="tests"),
488
        Address("a/b", target_name="c"),
489
    )
490
    assert_is_expected(
1✔
491
        AddressInput.parse("a/b/c.txt:c", description_of_origin="tests"),
492
        Address("a/b", relative_file_path="c.txt", target_name="c"),
493
    )
494

495
    # Top-level addresses will not have a path_component, unless they are a file address.
496
    assert_is_expected(
1✔
497
        AddressInput.parse("f.txt:original", description_of_origin="tests"),
498
        Address("", relative_file_path="f.txt", target_name="original"),
499
    )
500
    assert_is_expected(
1✔
501
        AddressInput.parse("//:t", description_of_origin="tests"),
502
        Address("", target_name="t"),
503
    )
504

505
    bad_address_input = AddressInput.parse("a/b/fake", description_of_origin="tests")
1✔
506
    expected_err = "'a/b/fake' does not exist on disk"
1✔
507
    with engine_error(ResolveError, contains=expected_err):
1✔
508
        rule_runner.request(Address, [bad_address_input])
1✔
509
    maybe_addr = rule_runner.request(MaybeAddress, [bad_address_input])
1✔
510
    assert isinstance(maybe_addr.val, ResolveError)
1✔
511
    assert expected_err in str(maybe_addr.val)
1✔
512

513

514
@pytest.fixture
1✔
515
def target_adaptor_rule_runner() -> RuleRunner:
1✔
516
    return RuleRunner(
1✔
517
        rules=[QueryRule(TargetAdaptor, (TargetAdaptorRequest,))],
518
        target_types=[MockTgt, MockGeneratedTarget, MockTargetGenerator],
519
        objects={"parametrize": Parametrize},
520
    )
521

522

523
def test_target_adaptor_parsed_correctly(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
524
    target_adaptor_rule_runner.write_files(
1✔
525
        {
526
            "helloworld/dir/BUILD": dedent(
527
                """\
528
                mock_tgt(
529
                    fake_field=42,
530
                    dependencies=[
531
                        # Because we don't follow dependencies or even parse dependencies, this
532
                        # self-cycle should be fine.
533
                        ":dir",
534
                        ":sibling",
535
                        "helloworld/util",
536
                        "helloworld/util:tests",
537
                    ],
538
                    build_file_dir=f"build file's dir is: {build_file_dir()}"
539
                )
540

541
                mock_tgt(name='t2')
542
                """
543
            )
544
        }
545
    )
546
    target_adaptor = target_adaptor_rule_runner.request(
1✔
547
        TargetAdaptor,
548
        [TargetAdaptorRequest(Address("helloworld/dir"), description_of_origin="tests")],
549
    )
550
    assert target_adaptor.name is None
1✔
551
    assert target_adaptor.type_alias == "mock_tgt"
1✔
552
    assert target_adaptor.kwargs["dependencies"] == (
1✔
553
        ":dir",
554
        ":sibling",
555
        "helloworld/util",
556
        "helloworld/util:tests",
557
    )
558
    # NB: TargetAdaptors do not validate what fields are valid. The Target API should error
559
    # when encountering this, but it's fine at this stage.
560
    assert target_adaptor.kwargs["fake_field"] == 42
1✔
561
    assert target_adaptor.kwargs["build_file_dir"] == "build file's dir is: helloworld/dir"
1✔
562

563
    target_adaptor = target_adaptor_rule_runner.request(
1✔
564
        TargetAdaptor,
565
        [
566
            TargetAdaptorRequest(
567
                Address("helloworld/dir", target_name="t2"), description_of_origin="tests"
568
            )
569
        ],
570
    )
571
    assert target_adaptor.name == "t2"
1✔
572
    assert target_adaptor.type_alias == "mock_tgt"
1✔
573

574

575
def test_target_adaptor_defaults_applied(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
576
    target_adaptor_rule_runner.write_files(
1✔
577
        {
578
            "helloworld/dir/BUILD": dedent(
579
                """\
580
                __defaults__({mock_tgt: dict(resolve="mock")}, all=dict(tags=["24"]))
581
                mock_tgt(tags=["42"])
582
                mock_tgt(name='t2')
583
                """
584
            )
585
        }
586
    )
587
    target_adaptor = target_adaptor_rule_runner.request(
1✔
588
        TargetAdaptor,
589
        [TargetAdaptorRequest(Address("helloworld/dir"), description_of_origin="tests")],
590
    )
591
    assert target_adaptor.name is None
1✔
592
    assert target_adaptor.kwargs["resolve"] == "mock"
1✔
593
    assert target_adaptor.kwargs["tags"] == ("42",)
1✔
594

595
    target_adaptor = target_adaptor_rule_runner.request(
1✔
596
        TargetAdaptor,
597
        [
598
            TargetAdaptorRequest(
599
                Address("helloworld/dir", target_name="t2"), description_of_origin="tests"
600
            )
601
        ],
602
    )
603
    assert target_adaptor.name == "t2"
1✔
604
    assert target_adaptor.kwargs["resolve"] == "mock"
1✔
605

606
    # The defaults are not frozen until after the BUILD file have been fully parsed, so this is a
607
    # list rather than a tuple at this time.
608
    assert target_adaptor.kwargs["tags"] == ("24",)
1✔
609

610

611
def test_generated_target_defaults(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
612
    target_adaptor_rule_runner.write_files(
1✔
613
        {
614
            "BUILD": dedent(
615
                """\
616
                __defaults__({generated: dict(resolve="mock")}, all=dict(tags=["24"]))
617
                generated(name="explicit", tags=["42"], source="e.txt")
618
                generator(name='gen', sources=["g*.txt"])
619
                """
620
            ),
621
            "e.txt": "",
622
            "g1.txt": "",
623
            "g2.txt": "",
624
        }
625
    )
626

627
    explicit_target = target_adaptor_rule_runner.get_target(Address("", target_name="explicit"))
1✔
628
    assert explicit_target.address.target_name == "explicit"
1✔
629
    assert explicit_target.get(ResolveField).value == "mock"
1✔
630
    assert explicit_target.get(Tags).value == ("42",)
1✔
631

632
    implicit_target = target_adaptor_rule_runner.get_target(
1✔
633
        Address("", target_name="gen", relative_file_path="g1.txt")
634
    )
635
    assert str(implicit_target.address) == "//g1.txt:gen"
1✔
636
    assert implicit_target.get(ResolveField).value == "mock"
1✔
637
    assert implicit_target.get(Tags).value == ("24",)
1✔
638

639

640
def test_inherit_defaults(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
641
    target_adaptor_rule_runner.write_files(
1✔
642
        {
643
            "BUILD": """__defaults__(all=dict(tags=["root"]))""",
644
            "helloworld/dir/BUILD": dedent(
645
                """\
646
                __defaults__({mock_tgt: dict(resolve="mock")}, extend=True)
647
                mock_tgt()
648
                """
649
            ),
650
        }
651
    )
652
    target_adaptor = target_adaptor_rule_runner.request(
1✔
653
        TargetAdaptor,
654
        [TargetAdaptorRequest(Address("helloworld/dir"), description_of_origin="tests")],
655
    )
656
    assert target_adaptor.name is None
1✔
657
    assert target_adaptor.kwargs["resolve"] == "mock"
1✔
658

659
    # The defaults originates from a parent BUILD file, and as such has been frozen.
660
    assert target_adaptor.kwargs["tags"] == ("root",)
1✔
661

662

663
def test_parametrize_defaults(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
664
    target_adaptor_rule_runner.write_files(
1✔
665
        {
666
            "BUILD": dedent(
667
                """\
668
                __defaults__(
669
                  all=dict(
670
                    tags=parametrize(a=["a", "root"], b=["non-root", "b"])
671
                  )
672
                )
673
                """
674
            ),
675
            "helloworld/dir/BUILD": "mock_tgt()",
676
        }
677
    )
678
    target_adaptor = target_adaptor_rule_runner.request(
1✔
679
        TargetAdaptor,
680
        [TargetAdaptorRequest(Address("helloworld/dir"), description_of_origin="tests")],
681
    )
682
    assert target_adaptor.kwargs["tags"] == ParametrizeDefault(a=("a", "root"), b=("non-root", "b"))
1✔
683

684

685
def test_parametrized_groups(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
686
    def _determenistic_parametrize_group_keys(value: Mapping[str, Any]) -> dict[str, Any]:
1✔
687
        # The `parametrize` object uses a unique generated field name when splatted onto a target
688
        # (in order to provide a helpful error message in case of non-unique group names), but the
689
        # part up until `:` is determenistic on the group name, which we need to exploit in the
690
        # tests using parametrize groups.
691
        return {key.rsplit(":", 1)[0]: val for key, val in value.items()}
1✔
692

693
    target_adaptor_rule_runner.write_files(
1✔
694
        {
695
            "hello/BUILD": dedent(
696
                """\
697
                mock_tgt(
698
                  description="desc for a and b",
699
                  **parametrize("a", tags=["opt-a"], resolve="lock-a"),
700
                  **parametrize("b", tags=["opt-b"], resolve="lock-b"),
701
                )
702
                """
703
            ),
704
        }
705
    )
706

707
    target_adaptor = target_adaptor_rule_runner.request(
1✔
708
        TargetAdaptor,
709
        [TargetAdaptorRequest(Address("hello"), description_of_origin="tests")],
710
    )
711
    assert _determenistic_parametrize_group_keys(
1✔
712
        target_adaptor.kwargs
713
    ) == _determenistic_parametrize_group_keys(
714
        dict(
715
            description="desc for a and b",
716
            **Parametrize("a", tags=["opt-a"], resolve="lock-a"),
717
            **Parametrize("b", tags=["opt-b"], resolve="lock-b"),
718
        )
719
    )
720

721

722
def test_default_parametrized_groups(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
723
    target_adaptor_rule_runner.write_files(
1✔
724
        {
725
            "hello/BUILD": dedent(
726
                """\
727
                __defaults__({mock_tgt: dict(**parametrize("a", tags=["from default"]))})
728
                mock_tgt(
729
                  tags=["from target"],
730
                  **parametrize("a"),
731
                  **parametrize("b", tags=["from b"]),
732
                )
733
                """
734
            ),
735
        }
736
    )
737
    address = Address("hello")
1✔
738
    target_adaptor = target_adaptor_rule_runner.request(
1✔
739
        TargetAdaptor,
740
        [TargetAdaptorRequest(address, description_of_origin="tests")],
741
    )
742
    targets = tuple(Parametrize.expand(address, target_adaptor.kwargs))
1✔
743
    assert targets == (
1✔
744
        (address.parametrize(dict(parametrize="a")), dict(tags=("from target",))),
745
        (address.parametrize(dict(parametrize="b")), dict(tags=("from b",))),
746
    )
747

748

749
def test_default_parametrized_groups_with_parametrizations(
1✔
750
    target_adaptor_rule_runner: RuleRunner,
751
) -> None:
752
    target_adaptor_rule_runner.write_files(
1✔
753
        {
754
            "src/BUILD": dedent(
755
                """
756
                __defaults__({
757
                  mock_tgt: dict(
758
                    **parametrize(
759
                      "py310-compat",
760
                      resolve="service-a",
761
                      tags=[
762
                        "CPython == 3.9.*",
763
                        "CPython == 3.10.*",
764
                      ]
765
                    ),
766
                    **parametrize(
767
                      "py39-compat",
768
                      resolve=parametrize(
769
                        "service-b",
770
                        "service-c",
771
                        "service-d",
772
                      ),
773
                      tags=[
774
                        "CPython == 3.9.*",
775
                      ]
776
                    )
777
                  )
778
                })
779
                mock_tgt()
780
                """
781
            ),
782
        }
783
    )
784
    address = Address("src")
1✔
785
    target_adaptor = target_adaptor_rule_runner.request(
1✔
786
        TargetAdaptor,
787
        [TargetAdaptorRequest(address, description_of_origin="tests")],
788
    )
789
    targets = tuple(Parametrize.expand(address, target_adaptor.kwargs))
1✔
790
    assert targets == (
1✔
791
        (
792
            address.parametrize(dict(parametrize="py310-compat")),
793
            dict(
794
                tags=("CPython == 3.9.*", "CPython == 3.10.*"),
795
                resolve="service-a",
796
            ),
797
        ),
798
        (
799
            address.parametrize(dict(parametrize="py39-compat", resolve="service-b")),
800
            dict(tags=("CPython == 3.9.*",), resolve="service-b"),
801
        ),
802
        (
803
            address.parametrize(dict(parametrize="py39-compat", resolve="service-c")),
804
            dict(tags=("CPython == 3.9.*",), resolve="service-c"),
805
        ),
806
        (
807
            address.parametrize(dict(parametrize="py39-compat", resolve="service-d")),
808
            dict(tags=("CPython == 3.9.*",), resolve="service-d"),
809
        ),
810
    )
811

812

813
def test_augment_target_field_defaults(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
814
    target_adaptor_rule_runner.write_files(
1✔
815
        {
816
            "BUILD": dedent(
817
                """
818
                __defaults__(all=dict(tags=["default-tag"]))
819
                mock_tgt(
820
                  sources=["*.added", *mock_tgt.sources.default],
821
                  tags=["custom-tag", *mock_tgt.tags.default],
822
                )
823
                """
824
            ),
825
        },
826
    )
827
    target_adaptor = target_adaptor_rule_runner.request(
1✔
828
        TargetAdaptor,
829
        [TargetAdaptorRequest(Address(""), description_of_origin="tests")],
830
    )
831
    assert target_adaptor.kwargs["sources"] == ("*.added", "*.mock")
1✔
832
    assert target_adaptor.kwargs["tags"] == ("custom-tag", "default-tag")
1✔
833

834

835
def test_target_adaptor_not_found(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
836
    with pytest.raises(ExecutionError) as exc:
1✔
837
        target_adaptor_rule_runner.request(
1✔
838
            TargetAdaptor,
839
            [TargetAdaptorRequest(Address("helloworld"), description_of_origin="tests")],
840
        )
841
    assert "Directory \\'helloworld\\' does not contain any BUILD files" in str(exc)
1✔
842

843
    target_adaptor_rule_runner.write_files({"helloworld/BUILD": "mock_tgt(name='other_tgt')"})
1✔
844
    expected_rx_str = re.escape(
1✔
845
        "The target name ':helloworld' is not defined in the directory helloworld"
846
    )
847
    with pytest.raises(ExecutionError, match=expected_rx_str):
1✔
848
        target_adaptor_rule_runner.request(
1✔
849
            TargetAdaptor,
850
            [TargetAdaptorRequest(Address("helloworld"), description_of_origin="tests")],
851
        )
852

853

854
def test_build_file_address() -> None:
1✔
855
    rule_runner = RuleRunner(
1✔
856
        rules=[QueryRule(BuildFileAddress, [BuildFileAddressRequest])], target_types=[MockTgt]
857
    )
858
    rule_runner.write_files({"helloworld/BUILD.ext": "mock_tgt()"})
1✔
859

860
    def assert_bfa_resolved(address: Address) -> None:
1✔
861
        expected_bfa = BuildFileAddress(address, "helloworld/BUILD.ext")
1✔
862
        bfa = rule_runner.request(
1✔
863
            BuildFileAddress, [BuildFileAddressRequest(address, description_of_origin="tests")]
864
        )
865
        assert bfa == expected_bfa
1✔
866

867
    assert_bfa_resolved(Address("helloworld"))
1✔
868
    # Generated targets should use their target generator's BUILD file.
869
    assert_bfa_resolved(Address("helloworld", generated_name="f.txt"))
1✔
870
    assert_bfa_resolved(Address("helloworld", relative_file_path="f.txt"))
1✔
871

872

873
def test_build_files_share_globals() -> None:
1✔
874
    """Test that a macro in a prelude can reference another macro in another prelude.
875

876
    At some point a change was made to separate the globals/locals dict (unintentional) which has
877
    the unintended side effect of having the `__globals__` of a macro not contain references to
878
    every other symbol in every other prelude.
879
    """
880

881
    symbols = run_rule_with_mocks(
1✔
882
        evaluate_preludes,
883
        rule_args=[
884
            BuildFileOptions((), prelude_globs=("prelude",)),
885
            Parser(
886
                build_root="",
887
                registered_target_types=RegisteredTargetTypes({}),
888
                union_membership=UnionMembership.empty(),
889
                object_aliases=BuildFileAliases(),
890
                ignore_unrecognized_symbols=False,
891
            ),
892
        ],
893
        mock_calls={
894
            "pants.engine.intrinsics.get_digest_contents": lambda _: DigestContents(
895
                [
896
                    FileContent(
897
                        path="/dev/null/prelude1",
898
                        content=dedent(
899
                            """\
900
                                def hello():
901
                                    pass
902
                                """
903
                        ).encode(),
904
                    ),
905
                    FileContent(
906
                        path="/dev/null/prelude2",
907
                        content=dedent(
908
                            """\
909
                                def world():
910
                                    pass
911
                                """
912
                        ).encode(),
913
                    ),
914
                ]
915
            ),
916
        },
917
    )
918
    assert symbols.symbols["hello"].__globals__ is symbols.symbols["world"].__globals__
1✔
919
    assert "world" in symbols.symbols["hello"].__globals__
1✔
920
    assert "hello" in symbols.symbols["world"].__globals__
1✔
921

922

923
def test_macro_undefined_symbol_bootstrap() -> None:
1✔
924
    # Tests that an undefined symbol in a macro is ignored while bootstrapping. Ignoring undeclared
925
    # symbols during parsing is insufficient, because we would need to re-evaluate the preludes after
926
    # adding each additional undefined symbol to scope.
927
    rule_runner = RuleRunner(
1✔
928
        rules=[QueryRule(AddressFamily, [AddressFamilyDir])],
929
        is_bootstrap=True,
930
    )
931
    rule_runner.set_options(
1✔
932
        args=("--build-file-prelude-globs=prelude.py",),
933
    )
934
    rule_runner.write_files(
1✔
935
        {
936
            "prelude.py": dedent(
937
                """
938
                def uses_undefined():
939
                    return this_is_undefined()
940
                """
941
            ),
942
            "BUILD": dedent(
943
                """
944
                uses_undefined()
945
                """
946
            ),
947
        }
948
    )
949

950
    # Parse the root BUILD file.
951
    address_family = rule_runner.request(AddressFamily, [AddressFamilyDir("")])
1✔
952
    assert not address_family.name_to_target_adaptors
1✔
953

954

955
def test_default_plugin_field_bootstrap() -> None:
1✔
956
    # Tests that an unknown field in `__defaults__` is ignored while bootstrapping.
957
    rule_runner = RuleRunner(
1✔
958
        rules=[QueryRule(AddressFamily, [AddressFamilyDir])],
959
        target_types=[MockTgt],
960
        is_bootstrap=True,
961
    )
962
    rule_runner.write_files(
1✔
963
        {
964
            "BUILD": dedent(
965
                """
966
                __defaults__({mock_tgt: dict(presumably_plugin_field="default", tags=["ok"])})
967
                """
968
            ),
969
        }
970
    )
971

972
    # Parse the root BUILD file.
973
    address_family = rule_runner.request(AddressFamily, [AddressFamilyDir("")])
1✔
974
    assert dict(tags=("ok",)) == dict(address_family.defaults["mock_tgt"])
1✔
975

976

977
def test_environment_target_macro_field_value() -> None:
1✔
978
    rule_runner = RuleRunner(
1✔
979
        rules=[QueryRule(AddressFamily, [AddressFamilyDir])],
980
        target_types=[MockTgt],
981
        is_bootstrap=True,
982
    )
983
    rule_runner.set_options(
1✔
984
        args=("--build-file-prelude-globs=prelude.py",),
985
    )
986
    rule_runner.write_files(
1✔
987
        {
988
            "prelude.py": dedent(
989
                """
990
                def tags():
991
                    return ["foo", "bar"]
992
                """
993
            ),
994
            "BUILD": dedent(
995
                """
996
                mock_tgt(name="tgt", tags=tags())
997
                """
998
            ),
999
        }
1000
    )
1001

1002
    # Parse the root BUILD file.
1003
    address_family = rule_runner.request(AddressFamily, [AddressFamilyDir("")])
1✔
1004
    tgt = address_family.name_to_target_adaptors["tgt"][1]
1✔
1005
    # We're pretending that field values returned from a called macro function doesn't exist during
1006
    # bootstrap. This is to allow the semi-dubios use of macro calls for environment target field
1007
    # values that are not required, and depending on how they are used, it may work to only have
1008
    # those field values set during normal lookup.
1009
    assert not tgt.kwargs
1✔
1010
    assert tgt == TargetAdaptor("mock_tgt", "tgt", "BUILD:2")
1✔
1011

1012

1013
def test_build_file_env_vars(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
1014
    target_adaptor_rule_runner.write_files(
1✔
1015
        {
1016
            "BUILD": dedent(
1017
                """
1018
                mock_tgt(
1019
                  description=env("MOCK_DESC"),
1020
                  tags=[
1021
                    env("DEF", "default"),
1022
                    env("TAG", "default"),
1023
                  ]
1024
                )
1025
                """
1026
            ),
1027
        },
1028
    )
1029
    target_adaptor_rule_runner.set_options([], env={"MOCK_DESC": "from env", "TAG": "tag"})
1✔
1030
    target_adaptor = target_adaptor_rule_runner.request(
1✔
1031
        TargetAdaptor,
1032
        [TargetAdaptorRequest(Address(""), description_of_origin="tests")],
1033
    )
1034
    assert target_adaptor.kwargs["description"] == "from env"
1✔
1035
    assert target_adaptor.kwargs["tags"] == ("default", "tag")
1✔
1036

1037

1038
def test_prelude_env_vars(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
1039
    target_adaptor_rule_runner.write_files(
1✔
1040
        {
1041
            "prelude.py": dedent(
1042
                """
1043
                def macro_val():
1044
                    return env("MACRO_ENV")
1045
                """
1046
            ),
1047
            "BUILD": dedent(
1048
                """
1049
                mock_tgt(
1050
                  description=macro_val(),
1051
                )
1052
                """
1053
            ),
1054
        },
1055
    )
1056
    target_adaptor_rule_runner.set_options(
1✔
1057
        args=("--build-file-prelude-globs=prelude.py",),
1058
        env={"MACRO_ENV": "from env"},
1059
    )
1060
    target_adaptor = target_adaptor_rule_runner.request(
1✔
1061
        TargetAdaptor,
1062
        [TargetAdaptorRequest(Address(""), description_of_origin="tests")],
1063
    )
1064
    assert target_adaptor.kwargs["description"] == "from env"
1✔
1065

1066

1067
def test_invalid_build_file_env_vars(caplog, target_adaptor_rule_runner: RuleRunner) -> None:
1✔
1068
    target_adaptor_rule_runner.write_files(
1✔
1069
        {
1070
            "src/bad/BUILD": dedent(
1071
                """
1072
                DOES_NOT_WORK = "var_name1"
1073
                DO_THIS_INSTEAD = env("var_name2")
1074

1075
                mock_tgt(description=env(DOES_NOT_WORK), tags=[DO_THIS_INSTEAD])
1076
                """
1077
            ),
1078
        },
1079
    )
1080
    target_adaptor_rule_runner.set_options(
1✔
1081
        [], env={"var_name1": "desc from env", "var_name2": "tag-from-env"}
1082
    )
1083
    target_adaptor = target_adaptor_rule_runner.request(
1✔
1084
        TargetAdaptor,
1085
        [TargetAdaptorRequest(Address("src/bad"), description_of_origin="tests")],
1086
    )
1087
    assert target_adaptor.kwargs["description"] is None
1✔
1088
    assert target_adaptor.kwargs["tags"] == ("tag-from-env",)
1✔
1089
    assert_logged(
1✔
1090
        caplog,
1091
        [
1092
            (
1093
                logging.WARNING,
1094
                softwrap(
1095
                    """
1096
                    src/bad/BUILD:5: Only constant string values as variable name to `env()` is
1097
                    currently supported. This `env()` call will always result in the default value
1098
                    only.
1099
                    """
1100
                ),
1101
            ),
1102
        ],
1103
    )
1104

1105

1106
def test_build_file_parse_error(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
1107
    target_adaptor_rule_runner.write_files(
1✔
1108
        {
1109
            "src/bad/BUILD": dedent(
1110
                """\
1111
                mock_tgt(
1112
                  name="foo"
1113
                  tags=[]
1114
                )
1115
                """
1116
            ),
1117
        },
1118
    )
1119
    with pytest.raises(ExecutionError, match='File "src/bad/BUILD", line 2'):
1✔
1120
        target_adaptor_rule_runner.request(
1✔
1121
            TargetAdaptor,
1122
            [
1123
                TargetAdaptorRequest(
1124
                    Address("src/bad", target_name="foo"), description_of_origin="test"
1125
                )
1126
            ],
1127
        )
1128

1129

1130
def test_build_file_description_of_origin(target_adaptor_rule_runner: RuleRunner) -> None:
1✔
1131
    target_adaptor_rule_runner.write_files(
1✔
1132
        {
1133
            "src/BUILD": dedent(
1134
                """\
1135
                # Define a target..
1136
                mock_tgt(name="foo")
1137
                """
1138
            ),
1139
        },
1140
    )
1141
    target_adaptor = target_adaptor_rule_runner.request(
1✔
1142
        TargetAdaptor,
1143
        [TargetAdaptorRequest(Address("src", target_name="foo"), description_of_origin="test")],
1144
    )
1145
    assert "src/BUILD:2" == target_adaptor.description_of_origin
1✔
1146

1147

1148
@pytest.mark.parametrize(
1✔
1149
    "filename, contents, expect_failure, expected_message",
1150
    [
1151
        ("BUILD", "data()", False, None),
1152
        (
1153
            "BUILD.qq",
1154
            "data()qq",
1155
            True,
1156
            "Error parsing BUILD file BUILD.qq:1: invalid syntax\n  data()qq\n        ^",
1157
        ),
1158
        (
1159
            "foo/BUILD",
1160
            "data()\nqwe asd",
1161
            True,
1162
            "Error parsing BUILD file foo/BUILD:2: invalid syntax\n  qwe asd\n      ^",
1163
        ),
1164
    ],
1165
)
1166
def test_build_file_syntax_error(filename, contents, expect_failure, expected_message):
1✔
1167
    class MockFileContent:
1✔
1168
        def __init__(self, path, content):
1✔
1169
            self.path = path
1✔
1170
            self.content = content
1✔
1171

1172
    if expect_failure:
1✔
1173
        with pytest.raises(BuildFileSyntaxError) as e:
1✔
1174
            BUILDFileEnvVarExtractor.get_env_vars(MockFileContent(filename, contents))
1✔
1175

1176
        formatted = str(e.value)
1✔
1177

1178
        assert formatted == expected_message
1✔
1179

1180
    else:
1181
        BUILDFileEnvVarExtractor.get_env_vars(MockFileContent(filename, contents))
1✔
1182

1183

1184
def test_build_file_duplicate_declared_names() -> None:
1✔
1185
    rule_runner = RuleRunner(
1✔
1186
        rules=[QueryRule(AddressFamily, [AddressFamilyDir])],
1187
        target_types=[MockTgt],
1188
    )
1189
    rule_runner.write_files(
1✔
1190
        {
1191
            "src/BUILD.foo": dedent(
1192
                """\
1193
                # Define a target.
1194
                mock_tgt(name="foo")
1195
                """
1196
            ),
1197
            "src/BUILD.bar": dedent(
1198
                """\
1199
                # Define a target..
1200
                mock_tgt(name="foo")
1201
                """
1202
            ),
1203
        },
1204
    )
1205
    with pytest.raises(
1✔
1206
        ExecutionError,
1207
        match="A target already exists at `src/BUILD.bar` with name `foo` and target type `mock_tgt`",
1208
    ):
1209
        _ = rule_runner.request(AddressFamily, [AddressFamilyDir("src")])
1✔
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

© 2026 Coveralls, Inc