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

pantsbuild / pants / 23710389144

29 Mar 2026 01:42PM UTC coverage: 92.849% (-0.07%) from 92.917%
23710389144

Pull #23200

github

web-flow
Merge 7a0639d44 into da60c6486
Pull Request #23200: perf: Port FrozenOrderedSet to rust

22 of 26 new or added lines in 6 files covered. (84.62%)

77 existing lines in 13 files now uncovered.

91400 of 98439 relevant lines covered (92.85%)

4.04 hits per line

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

95.48
/src/python/pants/backend/docker/util_rules/docker_build_context_test.py
1
# Copyright 2021 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 json
1✔
7
import os
1✔
8
import zipfile
1✔
9
from platform import machine
1✔
10
from textwrap import dedent
1✔
11
from typing import Any, ContextManager
1✔
12

13
import pytest
1✔
14

15
from pants.backend.docker.goals import package_image
1✔
16
from pants.backend.docker.subsystems import dockerfile_parser
1✔
17
from pants.backend.docker.subsystems.dockerfile_parser import DockerfileInfo
1✔
18
from pants.backend.docker.target_types import DockerImageTarget
1✔
19
from pants.backend.docker.util_rules import (
1✔
20
    dependencies,
21
    docker_binary,
22
    docker_build_args,
23
    docker_build_context,
24
    docker_build_env,
25
    dockerfile,
26
)
27
from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs
1✔
28
from pants.backend.docker.util_rules.docker_build_context import (
1✔
29
    DockerBuildContext,
30
    DockerBuildContextRequest,
31
)
32
from pants.backend.docker.util_rules.docker_build_env import DockerBuildEnvironment
1✔
33
from pants.backend.docker.value_interpolation import DockerBuildArgsInterpolationValue
1✔
34
from pants.backend.python import target_types_rules
1✔
35
from pants.backend.python.goals import package_pex_binary
1✔
36
from pants.backend.python.goals.package_pex_binary import PexBinaryFieldSet
1✔
37
from pants.backend.python.target_types import PexBinary, PythonRequirementTarget
1✔
38
from pants.backend.python.util_rules import pex_from_targets
1✔
39
from pants.backend.shell.target_types import ShellSourcesGeneratorTarget, ShellSourceTarget
1✔
40
from pants.backend.shell.target_types import rules as shell_target_types_rules
1✔
41
from pants.core.environments.target_types import DockerEnvironmentTarget
1✔
42
from pants.core.goals import package
1✔
43
from pants.core.goals.package import BuiltPackage
1✔
44
from pants.core.target_types import FilesGeneratorTarget, FileTarget
1✔
45
from pants.core.target_types import rules as core_target_types_rules
1✔
46
from pants.engine.addresses import Address
1✔
47
from pants.engine.fs import EMPTY_DIGEST, EMPTY_SNAPSHOT, Snapshot
1✔
48
from pants.engine.internals.scheduler import ExecutionError
1✔
49
from pants.testutil.pytest_util import no_exception
1✔
50
from pants.testutil.rule_runner import QueryRule, RuleRunner
1✔
51
from pants.util.value_interpolation import InterpolationContext, InterpolationValue
1✔
52

53

54
def create_rule_runner() -> RuleRunner:
1✔
55
    rule_runner = RuleRunner(
1✔
56
        rules=[
57
            *core_target_types_rules(),
58
            *dependencies.rules(),
59
            *docker_binary.rules(),
60
            *docker_build_args.rules(),
61
            *docker_build_context.rules(),
62
            *docker_build_env.rules(),
63
            *dockerfile.rules(),
64
            *dockerfile_parser.rules(),
65
            *package_image.rules(),
66
            *package_pex_binary.rules(),
67
            *pex_from_targets.rules(),
68
            *shell_target_types_rules(),
69
            *target_types_rules.rules(),
70
            package.environment_aware_package,
71
            package.find_all_packageable_targets,
72
            QueryRule(BuiltPackage, [PexBinaryFieldSet]),
73
            QueryRule(DockerBuildContext, (DockerBuildContextRequest,)),
74
        ],
75
        target_types=[
76
            PythonRequirementTarget,
77
            DockerEnvironmentTarget,
78
            DockerImageTarget,
79
            FilesGeneratorTarget,
80
            FileTarget,
81
            PexBinary,
82
            ShellSourcesGeneratorTarget,
83
            ShellSourceTarget,
84
        ],
85
    )
86
    return rule_runner
1✔
87

88

89
@pytest.fixture
1✔
90
def rule_runner() -> RuleRunner:
1✔
91
    return create_rule_runner()
1✔
92

93

94
def assert_build_context(
1✔
95
    rule_runner: RuleRunner,
96
    address: Address,
97
    *,
98
    build_upstream_images: bool = False,
99
    expected_files: list[str],
100
    expected_interpolation_context: (
101
        dict[str, str | dict[str, str] | InterpolationValue] | None
102
    ) = None,
103
    expected_num_upstream_images: int = 0,
104
    pants_args: list[str] | None = None,
105
    runner_options: dict[str, Any] | None = None,
106
) -> DockerBuildContext:
107
    if runner_options is None:
1✔
108
        runner_options = {}
1✔
109
    runner_options.setdefault("env_inherit", set()).update({"PATH", "PYENV_ROOT", "HOME"})
1✔
110
    rule_runner.set_options(pants_args or [], **runner_options)
1✔
111
    context = rule_runner.request(
1✔
112
        DockerBuildContext,
113
        [
114
            DockerBuildContextRequest(
115
                address=address,
116
                build_upstream_images=build_upstream_images,
117
            )
118
        ],
119
    )
120

121
    snapshot = rule_runner.request(Snapshot, [context.digest])
1✔
122
    assert sorted(expected_files) == sorted(snapshot.files)
1✔
123
    if expected_interpolation_context is not None:
1✔
124
        build_args = expected_interpolation_context.get("build_args")
1✔
125
        if isinstance(build_args, dict):
1✔
126
            expected_interpolation_context["build_args"] = DockerBuildArgsInterpolationValue(
1✔
127
                build_args
128
            )
129

130
        if "pants" not in expected_interpolation_context:
1✔
131
            expected_interpolation_context["pants"] = context.interpolation_context["pants"]
1✔
132

133
        # Converting to `dict` to avoid the fact that FrozenDict is sensitive to the order of the keys.
134
        assert dict(context.interpolation_context) == dict(
1✔
135
            InterpolationContext.from_dict(expected_interpolation_context)
136
        )
137

138
    if build_upstream_images:
1✔
139
        assert len(context.upstream_image_ids) == expected_num_upstream_images
1✔
140
    return context
1✔
141

142

143
def test_pants_hash(rule_runner: RuleRunner) -> None:
1✔
144
    rule_runner.write_files(
1✔
145
        {
146
            "test/BUILD": "docker_image()",
147
            "test/Dockerfile": "FROM base",
148
        }
149
    )
150

151
    assert_build_context(
1✔
152
        rule_runner,
153
        Address("test"),
154
        expected_files=["test/Dockerfile"],
155
        expected_interpolation_context={
156
            "tags": {
157
                "baseimage": "latest",
158
                "stage0": "latest",
159
            },
160
            "build_args": {},
161
            "pants": {"hash": "87e90685c07ac302bbff8f9d846b4015621255f741008485fd3ce72253ce54f4"},
162
        },
163
    )
164

165

166
def test_file_dependencies(rule_runner: RuleRunner) -> None:
1✔
167
    rule_runner.write_files(
1✔
168
        {
169
            # img_A -> files_A
170
            # img_A -> img_B
171
            "src/a/BUILD": dedent(
172
                """\
173
                docker_image(name="img_A", dependencies=[":files_A", "src/b:img_B"])
174
                files(name="files_A", sources=["files/**"])
175
                """
176
            ),
177
            "src/a/Dockerfile": "FROM base",
178
            "src/a/files/a01": "",
179
            "src/a/files/a02": "",
180
            # img_B -> files_B
181
            "src/b/BUILD": dedent(
182
                """\
183
                docker_image(name="img_B", dependencies=[":files_B"])
184
                files(name="files_B", sources=["files/**"])
185
                """
186
            ),
187
            "src/b/Dockerfile": "FROM base",
188
            "src/b/files/b01": "",
189
            "src/b/files/b02": "",
190
            # Mixed
191
            "src/c/BUILD": dedent(
192
                """\
193
                docker_image(name="img_C", dependencies=["src/a:files_A", "src/b:files_B"])
194
                """
195
            ),
196
            "src/c/Dockerfile": "FROM base",
197
        }
198
    )
199

200
    # We want files_B in build context for img_B
201
    assert_build_context(
1✔
202
        rule_runner,
203
        Address("src/b", target_name="img_B"),
204
        expected_files=["src/b/Dockerfile", "src/b/files/b01", "src/b/files/b02"],
205
    )
206

207
    # We want files_A in build context for img_A, but not files_B
208
    assert_build_context(
1✔
209
        rule_runner,
210
        Address("src/a", target_name="img_A"),
211
        expected_files=["src/a/Dockerfile", "src/a/files/a01", "src/a/files/a02"],
212
    )
213

214
    # Mixed.
215
    assert_build_context(
1✔
216
        rule_runner,
217
        Address("src/c", target_name="img_C"),
218
        expected_files=[
219
            "src/c/Dockerfile",
220
            "src/a/files/a01",
221
            "src/a/files/a02",
222
            "src/b/files/b01",
223
            "src/b/files/b02",
224
        ],
225
    )
226

227

228
def test_from_image_build_arg_dependency(rule_runner: RuleRunner) -> None:
1✔
229
    rule_runner.write_files(
1✔
230
        {
231
            "src/upstream/BUILD": dedent(
232
                """\
233
                docker_image(
234
                  name="image",
235
                  repository="upstream/{name}",
236
                  image_tags=["1.0"],
237
                  instructions=["FROM alpine:3.16.1"],
238
                )
239
                """
240
            ),
241
            "src/downstream/BUILD": "docker_image(name='image')",
242
            "src/downstream/Dockerfile": dedent(
243
                """\
244
                ARG BASE_IMAGE=src/upstream:image
245
                FROM $BASE_IMAGE
246
                """
247
            ),
248
        }
249
    )
250

251
    assert_build_context(
1✔
252
        rule_runner,
253
        Address("src/downstream", target_name="image"),
254
        expected_files=["src/downstream/Dockerfile", "src.upstream/image.docker-info.json"],
255
        build_upstream_images=True,
256
        expected_interpolation_context={
257
            "tags": {
258
                "baseimage": "1.0",
259
                "stage0": "1.0",
260
            },
261
            "build_args": {
262
                "BASE_IMAGE": "upstream/image:1.0",
263
            },
264
        },
265
        expected_num_upstream_images=1,
266
    )
267

268

269
def test_from_image_build_arg_dependency_overwritten(rule_runner: RuleRunner) -> None:
1✔
270
    rule_runner.write_files(
1✔
271
        {
272
            "src/upstream/BUILD": dedent(
273
                """\
274
                docker_image(
275
                  name="image",
276
                  repository="upstream/{name}",
277
                  image_tags=["1.0"],
278
                  instructions=["FROM alpine:3.16.1"],
279
                )
280
                """
281
            ),
282
            "src/downstream/BUILD": "docker_image(name='image')",
283
            "src/downstream/Dockerfile": dedent(
284
                """\
285
                ARG BASE_IMAGE=src/upstream:image
286
                FROM $BASE_IMAGE
287
                """
288
            ),
289
        }
290
    )
291

292
    assert_build_context(
1✔
293
        rule_runner,
294
        Address("src/downstream", target_name="image"),
295
        expected_files=["src/downstream/Dockerfile"],
296
        build_upstream_images=True,
297
        expected_interpolation_context={
298
            "tags": {
299
                "baseimage": "3.10-slim",
300
                "stage0": "3.10-slim",
301
            },
302
            "build_args": {
303
                "BASE_IMAGE": "python:3.10-slim",
304
            },
305
        },
306
        expected_num_upstream_images=0,
307
        pants_args=["--docker-build-args=BASE_IMAGE=python:3.10-slim"],
308
    )
309

310

311
def test_from_image_build_arg_not_in_repo_issue_15585(rule_runner: RuleRunner) -> None:
1✔
312
    rule_runner.write_files(
1✔
313
        {
314
            "test/image/BUILD": "docker_image()",
315
            "test/image/Dockerfile": dedent(
316
                """\
317
                ARG PYTHON_VERSION="python:3.10.2-slim"
318
                FROM $PYTHON_VERSION
319
                """
320
            ),
321
        }
322
    )
323

324
    assert_build_context(
1✔
325
        rule_runner,
326
        Address("test/image", target_name="image"),
327
        expected_files=["test/image/Dockerfile"],
328
        build_upstream_images=True,
329
        expected_interpolation_context={
330
            "tags": {
331
                "baseimage": "3.10.2-slim",
332
                "stage0": "3.10.2-slim",
333
            },
334
            # PYTHON_VERSION will be treated like any other build ARG.
335
            "build_args": {},
336
        },
337
    )
338

339

340
def test_build_args_for_copy(rule_runner: RuleRunner) -> None:
1✔
341
    rule_runner.write_files(
1✔
342
        {
343
            "testprojects/src/docker/BUILD": dedent(
344
                """\
345
            docker_image()
346
            file(name="file", source="file.txt")
347
            file(name="file_as_arg", source="file_as_arg.txt")
348
            """
349
            ),
350
            "testprojects/src/docker/Dockerfile": dedent(
351
                """\
352
            FROM                python:3.9
353

354
            ARG PEX_BIN="testprojects/src/python:hello"
355
            ARG PEX_BIN_DOTTED_PATH="testprojects.src.python/hello_dotted.pex"
356
            ARG FILE_AS_ARG="testprojects/src/docker/file_as_arg.txt"
357

358
            COPY                ${PEX_BIN} /app/pex_bin
359
            COPY                $PEX_BIN_DOTTED_PATH /app/pex_var
360
            COPY                testprojects.src.python/hello_inline.pex /app/pex_bin_dotted_path
361
            COPY                ${FILE_AS_ARG} /app/
362
            COPY                testprojects/src/docker/file.txt /app/
363
            """
364
            ),
365
            "testprojects/src/docker/file_as_arg.txt": "",
366
            "testprojects/src/docker/file.txt": "",
367
            "testprojects/src/python/BUILD": dedent(
368
                """\
369
            pex_binary(name="hello", entry_point="hello.py")
370
            pex_binary(name="hello_dotted", entry_point="hello.py")
371
            pex_binary(name="hello_inline", entry_point="hello.py")
372
            """
373
            ),
374
            "testprojects/src/python/hello.py": "",
375
        }
376
    )
377

378
    assert_build_context(
1✔
379
        rule_runner,
380
        Address("testprojects/src/docker", target_name="docker"),
381
        expected_files=[
382
            "testprojects/src/docker/Dockerfile",
383
            "testprojects/src/docker/file.txt",
384
            "testprojects/src/docker/file_as_arg.txt",
385
            "testprojects.src.python/hello_inline.pex",
386
            "testprojects.src.python/hello_dotted.pex",
387
            "testprojects.src.python/hello.pex",
388
        ],
389
        expected_interpolation_context={
390
            "build_args": {
391
                "FILE_AS_ARG": "testprojects/src/docker/file_as_arg.txt",
392
                "PEX_BIN": "testprojects.src.python/hello.pex",
393
            },
394
            "tags": {"baseimage": "3.9", "stage0": "3.9"},
395
        },
396
    )
397

398

399
def test_files_out_of_tree(rule_runner: RuleRunner) -> None:
1✔
400
    # src/a:img_A -> res/static:files
401
    rule_runner.write_files(
1✔
402
        {
403
            "src/a/BUILD": dedent(
404
                """\
405
                docker_image(name="img_A", dependencies=["res/static:files"])
406
                """
407
            ),
408
            "res/static/BUILD": dedent(
409
                """\
410
                files(name="files", sources=["!BUILD", "**/*"])
411
                """
412
            ),
413
            "src/a/Dockerfile": "FROM base",
414
            "res/static/s01": "",
415
            "res/static/s02": "",
416
            "res/static/sub/s03": "",
417
        }
418
    )
419

420
    assert_build_context(
1✔
421
        rule_runner,
422
        Address("src/a", target_name="img_A"),
423
        expected_files=[
424
            "src/a/Dockerfile",
425
            "res/static/s01",
426
            "res/static/s02",
427
            "res/static/sub/s03",
428
        ],
429
    )
430

431

432
def test_packaged_pex_path(rule_runner: RuleRunner) -> None:
1✔
433
    # This test is here to ensure that we catch if there is any change in the generated path where
434
    # built pex binaries go, as we rely on that for dependency inference in the Dockerfile.
435
    rule_runner.write_files(
1✔
436
        {
437
            "src/docker/BUILD": """docker_image(dependencies=["src/python/proj/cli:bin"])""",
438
            "src/docker/Dockerfile": """FROM python:3.8""",
439
            "src/python/proj/cli/BUILD": """pex_binary(name="bin", entry_point="main.py")""",
440
            "src/python/proj/cli/main.py": """print("cli main")""",
441
        }
442
    )
443

444
    assert_build_context(
1✔
445
        rule_runner,
446
        Address("src/docker", target_name="docker"),
447
        expected_files=["src/docker/Dockerfile", "src.python.proj.cli/bin.pex"],
448
    )
449

450

451
def test_packaged_pex_environment(rule_runner: RuleRunner) -> None:
1✔
452
    image = (
1✔
453
        "python:3.11-buster@sha256:3a19b4d6ce4402d11bb19aa11416e4a262a60a57707a5cda5787a81285df2666"
454
    )
455
    if machine() == "x86_64":
1✔
456
        platform = "linux_x86_64"
1✔
457
        expected_dist = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
1✔
458
    elif machine() == "aarch64":
×
459
        platform = "linux_arm64"
×
460
        expected_dist = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
×
461
    else:
462
        pytest.skip("This test only runs on amd64 and arm64.")
×
463

464
    rule_runner.write_files(
1✔
465
        {
466
            "BUILD": dedent(
467
                f"""
468
              docker_environment(
469
                name="python_311",
470
                image="{image}",
471
                platform="{platform}",
472
                python_bootstrap_search_path=["<PATH>"],
473
              )
474

475
              python_requirement(name="psutil", requirements=["psutil==7.0.0"])
476
              """
477
            ),
478
            "src/docker/BUILD": """docker_image(dependencies=["src/python/proj/cli:bin"])""",
479
            "src/docker/Dockerfile": """FROM python:3.11""",
480
            "src/python/proj/cli/BUILD": dedent(
481
                """
482
              pex_binary(
483
                name="bin",
484
                entry_point="main.py",
485
                environment="python_311",
486
                dependencies=["//:psutil"],
487
              )
488
              """
489
            ),
490
            "src/python/proj/cli/main.py": """import psutil; assert psutil.Process().is_running()""",
491
        }
492
    )
493

494
    pex_file = "src.python.proj.cli/bin.pex"
1✔
495
    context = assert_build_context(
1✔
496
        rule_runner,
497
        Address("src/docker", target_name="docker"),
498
        pants_args=["--environments-preview-names={'python_311': '//:python_311'}"],
499
        expected_files=["src/docker/Dockerfile", pex_file],
500
    )
501

502
    # Confirm that the context contains a PEX for the appropriate platform.
UNCOV
503
    rule_runner.write_digest(context.digest, path_prefix="contents")
×
UNCOV
504
    with zipfile.ZipFile(os.path.join(rule_runner.build_root, "contents", pex_file), "r") as zf:
×
UNCOV
505
        assert json.loads(zf.read("PEX-INFO"))["distributions"].keys() == {
×
506
            expected_dist,
507
        }
508

509

510
def test_interpolation_context_from_dockerfile(rule_runner: RuleRunner) -> None:
1✔
511
    rule_runner.write_files(
1✔
512
        {
513
            "src/docker/BUILD": "docker_image()",
514
            "src/docker/Dockerfile": dedent(
515
                """\
516
                FROM python:3.8
517
                FROM alpine:3.16.1 as interim
518
                FROM interim
519
                FROM scratch:1-1 as output
520
                """
521
            ),
522
        }
523
    )
524

525
    assert_build_context(
1✔
526
        rule_runner,
527
        Address("src/docker"),
528
        expected_files=["src/docker/Dockerfile"],
529
        expected_interpolation_context={
530
            "tags": {
531
                "baseimage": "3.8",
532
                "stage0": "3.8",
533
                "interim": "3.16.1",
534
                "stage2": "latest",
535
                "output": "1-1",
536
            },
537
            "build_args": {},
538
        },
539
    )
540

541

542
def test_synthetic_dockerfile(rule_runner: RuleRunner) -> None:
1✔
543
    rule_runner.write_files(
1✔
544
        {
545
            "src/docker/BUILD": dedent(
546
                """\
547
                docker_image(
548
                  instructions=[
549
                    "FROM python:3.8",
550
                    "FROM alpine:3.16.1 as interim",
551
                    "FROM interim",
552
                    "FROM scratch:1-1 as output",
553
                  ]
554
                )
555
                """
556
            ),
557
        }
558
    )
559

560
    assert_build_context(
1✔
561
        rule_runner,
562
        Address("src/docker"),
563
        expected_files=["src/docker/Dockerfile.docker"],
564
        expected_interpolation_context={
565
            "tags": {
566
                "baseimage": "3.8",
567
                "stage0": "3.8",
568
                "interim": "3.16.1",
569
                "stage2": "latest",
570
                "output": "1-1",
571
            },
572
            "build_args": {},
573
        },
574
    )
575

576

577
def test_shell_source_dependencies(rule_runner: RuleRunner) -> None:
1✔
578
    rule_runner.write_files(
1✔
579
        {
580
            "src/docker/BUILD": dedent(
581
                """\
582
                docker_image(dependencies=[":entrypoint", ":shell"])
583
                shell_source(name="entrypoint", source="entrypoint.sh")
584
                shell_sources(name="shell", sources=["scripts/**/*.sh"])
585
                """
586
            ),
587
            "src/docker/Dockerfile": "FROM base",
588
            "src/docker/entrypoint.sh": "",
589
            "src/docker/scripts/s01.sh": "",
590
            "src/docker/scripts/s02.sh": "",
591
            "src/docker/scripts/random.file": "",
592
        }
593
    )
594
    assert_build_context(
1✔
595
        rule_runner,
596
        Address("src/docker"),
597
        expected_files=[
598
            "src/docker/Dockerfile",
599
            "src/docker/entrypoint.sh",
600
            "src/docker/scripts/s01.sh",
601
            "src/docker/scripts/s02.sh",
602
        ],
603
    )
604

605

606
def test_build_arg_defaults_from_dockerfile(rule_runner: RuleRunner) -> None:
1✔
607
    # Test that only explicitly defined build args in the BUILD file or pants configuration use the
608
    # environment for its values.
609
    rule_runner.write_files(
1✔
610
        {
611
            "src/docker/BUILD": dedent(
612
                """\
613
                docker_image(
614
                  extra_build_args=[
615
                    "base_version",
616
                  ]
617
                )
618
                """
619
            ),
620
            "src/docker/Dockerfile": dedent(
621
                """\
622
                ARG base_name=python
623
                ARG base_version=3.8
624
                FROM ${base_name}:${base_version}
625
                ARG NO_DEF
626
                ENV opt=${NO_DEF}
627
                """
628
            ),
629
        }
630
    )
631

632
    assert_build_context(
1✔
633
        rule_runner,
634
        Address("src/docker"),
635
        runner_options={
636
            "env": {
637
                "base_name": "no-effect",
638
                "base_version": "3.9",
639
            },
640
        },
641
        expected_files=["src/docker/Dockerfile"],
642
        expected_interpolation_context={
643
            "tags": {
644
                "baseimage": "${base_version}",
645
                "stage0": "${base_version}",
646
            },
647
            "build_args": {
648
                # `base_name` is not listed here, as it was not an explicitly defined build arg.
649
                "base_version": "3.9",
650
            },
651
        },
652
    )
653

654

655
@pytest.mark.parametrize(
1✔
656
    "dockerfile_arg_value, extra_build_arg_value, expect",
657
    [
658
        pytest.param(None, None, no_exception(), id="No args defined"),
659
        pytest.param(
660
            None,
661
            "",
662
            pytest.raises(ExecutionError, match=r"variable 'MY_ARG' is undefined"),
663
            id="No default value for build arg",
664
        ),
665
        pytest.param(None, "some default value", no_exception(), id="Default value for build arg"),
666
        pytest.param("", None, no_exception(), id="No build arg defined, and ARG without default"),
667
        pytest.param(
668
            "",
669
            "",
670
            pytest.raises(ExecutionError, match=r"variable 'MY_ARG' is undefined"),
671
            id="No default value from ARG",
672
        ),
673
        pytest.param(
674
            "", "some default value", no_exception(), id="Default value for build arg, ARG present"
675
        ),
676
        pytest.param(
677
            "some default value", None, no_exception(), id="No build arg defined, only ARG"
678
        ),
679
        pytest.param("some default value", "", no_exception(), id="Default value from ARG"),
680
        pytest.param(
681
            "some default value",
682
            "some other default",
683
            no_exception(),
684
            id="Default value for build arg, ARG default",
685
        ),
686
    ],
687
)
688
def test_undefined_env_var_behavior(
1✔
689
    rule_runner: RuleRunner,
690
    dockerfile_arg_value: str | None,
691
    extra_build_arg_value: str | None,
692
    expect: ContextManager,
693
) -> None:
694
    dockerfile_arg = ""
1✔
695
    if dockerfile_arg_value is not None:
1✔
696
        dockerfile_arg = "ARG MY_ARG"
1✔
697
        if dockerfile_arg_value:
1✔
698
            dockerfile_arg += f"={dockerfile_arg_value}"
1✔
699

700
    extra_build_args = ""
1✔
701
    if extra_build_arg_value is not None:
1✔
702
        extra_build_args = 'extra_build_args=["MY_ARG'
1✔
703
        if extra_build_arg_value:
1✔
704
            extra_build_args += f"={extra_build_arg_value}"
1✔
705
        extra_build_args += '"],'
1✔
706

707
    rule_runner.write_files(
1✔
708
        {
709
            "src/docker/BUILD": dedent(
710
                f"""\
711
                docker_image(
712
                  {extra_build_args}
713
                )
714
                """
715
            ),
716
            "src/docker/Dockerfile": dedent(
717
                f"""\
718
                FROM python:3.8
719
                {dockerfile_arg}
720
                """
721
            ),
722
        }
723
    )
724

725
    with expect:
1✔
726
        assert_build_context(
1✔
727
            rule_runner,
728
            Address("src/docker"),
729
            expected_files=["src/docker/Dockerfile"],
730
        )
731

732

733
@pytest.fixture(scope="session")
1✔
734
def build_context() -> DockerBuildContext:
1✔
735
    rule_runner = create_rule_runner()
1✔
736
    rule_runner.write_files(
1✔
737
        {
738
            "src/docker/BUILD": dedent(
739
                """\
740
                docker_image(
741
                  extra_build_args=["DEF_ARG"],
742
                  instructions=[
743
                    "FROM python:3.8",
744
                    "ARG MY_ARG",
745
                    "ARG DEF_ARG=some-value",
746
                  ],
747
                )
748
                """
749
            ),
750
        }
751
    )
752

753
    return assert_build_context(
1✔
754
        rule_runner,
755
        Address("src/docker"),
756
        expected_files=["src/docker/Dockerfile.docker"],
757
    )
758

759

760
@pytest.mark.parametrize(
1✔
761
    "fmt_string, result, expectation",
762
    [
763
        pytest.param(
764
            "{build_args.MY_ARG}",
765
            None,
766
            pytest.raises(
767
                ValueError,
768
                match=(r"The build arg 'MY_ARG' is undefined\. Defined build args are: DEF_ARG\."),
769
            ),
770
            id="ARG_NAME",
771
        ),
772
        pytest.param(
773
            "{build_args.DEF_ARG}",
774
            "some-value",
775
            no_exception(),
776
            id="DEF_ARG",
777
        ),
778
    ],
779
)
780
def test_build_arg_behavior(
1✔
781
    build_context: DockerBuildContext,
782
    fmt_string: str,
783
    result: str | None,
784
    expectation: ContextManager,
785
) -> None:
786
    with expectation:
1✔
787
        assert fmt_string.format(**build_context.interpolation_context) == result
1✔
788

789

790
def test_create_docker_build_context() -> None:
1✔
791
    context = DockerBuildContext.create(
1✔
792
        build_args=DockerBuildArgs.from_strings("ARGNAME=value1"),
793
        snapshot=EMPTY_SNAPSHOT,
794
        build_env=DockerBuildEnvironment.create({"ENVNAME": "value2"}),
795
        upstream_image_ids=["def", "abc"],
796
        dockerfile_info=DockerfileInfo(
797
            address=Address("test"),
798
            digest=EMPTY_DIGEST,
799
            source="test/Dockerfile",
800
            build_args=DockerBuildArgs.from_strings(),
801
            copy_source_paths=(),
802
            copy_build_args=DockerBuildArgs.from_strings(),
803
            from_image_build_args=DockerBuildArgs.from_strings(),
804
            # Stage without tags tests regression of #22108
805
            version_tags=("base latest", "stage1 1.2", "dev 2.0", "prod 2.0", "stage4"),
806
        ),
807
    )
808
    assert list(context.build_args) == ["ARGNAME=value1"]
1✔
809
    assert dict(context.build_env.environment) == {"ENVNAME": "value2"}
1✔
810
    assert context.upstream_image_ids == ("abc", "def")
1✔
811
    assert context.dockerfile == "test/Dockerfile"
1✔
812
    assert context.stages == ("base", "dev", "prod")
1✔
813
    assert context.interpolation_context["tags"] == {
1✔
814
        "baseimage": "latest",
815
        "base": "latest",
816
        "stage1": "1.2",
817
        "dev": "2.0",
818
        "prod": "2.0",
819
    }
820

821

822
def test_create_docker_build_context_digest_only() -> None:
1✔
823
    context = DockerBuildContext.create(
1✔
824
        build_args=DockerBuildArgs.from_strings("ARGNAME=value1"),
825
        snapshot=EMPTY_SNAPSHOT,
826
        build_env=DockerBuildEnvironment.create({"ENVNAME": "value2"}),
827
        upstream_image_ids=["def", "abc"],
828
        dockerfile_info=DockerfileInfo(
829
            address=Address("test"),
830
            digest=EMPTY_DIGEST,
831
            source="test/Dockerfile",
832
            build_args=DockerBuildArgs.from_strings(),
833
            copy_source_paths=(),
834
            copy_build_args=DockerBuildArgs.from_strings(),
835
            from_image_build_args=DockerBuildArgs.from_strings(),
836
            # Stage without tags tests regression of #22108
837
            version_tags=("bydigest",),
838
        ),
839
    )
840
    assert context.stages == ("bydigest",)
1✔
841
    assert context.interpolation_context["tags"] == {}
1✔
842

843

844
def test_pex_custom_output_path_issue14031(rule_runner: RuleRunner) -> None:
1✔
845
    rule_runner.write_files(
1✔
846
        {
847
            "project/test/BUILD": dedent(
848
                """\
849
                pex_binary(
850
                  name="test",
851
                  entry_point="main.py",
852
                  output_path="project/test.pex",
853
                )
854

855
                docker_image(
856
                  name="test-image",
857
                  dependencies=[":test"],
858
                )
859
                """
860
            ),
861
            "project/test/main.py": "print('Hello')",
862
            "project/test/Dockerfile": dedent(
863
                """\
864
                FROM python:3.8
865
                COPY project/test.pex .
866
                CMD ["./test.pex"]
867
                """
868
            ),
869
        }
870
    )
871

872
    assert_build_context(
1✔
873
        rule_runner,
874
        Address("project/test", target_name="test-image"),
875
        expected_files=["project/test/Dockerfile", "project/test.pex"],
876
    )
877

878

879
def test_dockerfile_instructions_issue_17571(rule_runner: RuleRunner) -> None:
1✔
880
    rule_runner.write_files(
1✔
881
        {
882
            "src/docker/Dockerfile": "do not use this file",
883
            "src/docker/BUILD": dedent(
884
                """\
885
                docker_image(
886
                  source=None,
887
                  instructions=[
888
                    "FROM python:3.8",
889
                  ]
890
                )
891
                """
892
            ),
893
        }
894
    )
895

896
    assert_build_context(
1✔
897
        rule_runner,
898
        Address("src/docker"),
899
        expected_files=["src/docker/Dockerfile.docker"],
900
        expected_interpolation_context={
901
            "tags": {
902
                "baseimage": "3.8",
903
                "stage0": "3.8",
904
            },
905
            "build_args": {},
906
        },
907
    )
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