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

pantsbuild / pants / 24791541718

22 Apr 2026 05:01PM UTC coverage: 92.911% (-0.02%) from 92.926%
24791541718

Pull #23133

github

web-flow
Merge d56db518e into 3d0987454
Pull Request #23133: Add buildctl engine

408 of 440 new or added lines in 13 files covered. (92.73%)

2 existing lines in 2 files now uncovered.

91882 of 98892 relevant lines covered (92.91%)

4.05 hits per line

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

97.98
/src/python/pants/backend/docker/util_rules/docker_binary_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 collections import namedtuple
1✔
5
from hashlib import sha256
1✔
6
from typing import cast
1✔
7
from unittest import mock
1✔
8

9
import pytest
1✔
10

11
from pants.backend.docker.subsystems.docker_options import DockerOptions
1✔
12
from pants.backend.docker.util_rules.binaries import (
1✔
13
    BuildctlBinary,
14
    DockerBinary,
15
    PodmanBinary,
16
    get_buildctl,
17
    get_docker,
18
    get_podman,
19
)
20
from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs
1✔
21
from pants.core.util_rules.system_binaries import (
1✔
22
    BinaryNotFoundError,
23
    BinaryPath,
24
    BinaryPathRequest,
25
    BinaryPaths,
26
    BinaryShims,
27
    BinaryShimsRequest,
28
)
29
from pants.engine.fs import EMPTY_DIGEST, Digest
1✔
30
from pants.engine.process import Process, ProcessCacheScope
1✔
31
from pants.testutil.option_util import create_subsystem
1✔
32
from pants.testutil.rule_runner import run_rule_with_mocks
1✔
33

34
BinaryInfo = namedtuple("BinaryInfo", ["cls", "path", "name"])
1✔
35

36

37
@pytest.fixture(
1✔
38
    params=[
39
        BinaryInfo(DockerBinary, "/bin/docker", "docker"),
40
        BinaryInfo(PodmanBinary, "/bin/podman", "podman"),
41
    ]
42
)
43
def binary_info(request) -> BinaryInfo:
1✔
44
    return cast(BinaryInfo, request.param)
1✔
45

46

47
@pytest.fixture
1✔
48
def binary_path(binary_info: BinaryInfo) -> str:
1✔
49
    return cast(str, binary_info.path)
1✔
50

51

52
@pytest.fixture
1✔
53
def binary(binary_info: BinaryInfo) -> DockerBinary | PodmanBinary:
1✔
54
    return cast(DockerBinary | PodmanBinary, binary_info.cls(binary_info.path))
1✔
55

56

57
@pytest.fixture
1✔
58
def buildctl_path() -> str:
1✔
59
    return "/bin/buildctl"
1✔
60

61

62
@pytest.fixture
1✔
63
def buildctl(buildctl_path: str) -> BuildctlBinary:
1✔
64
    return BuildctlBinary(buildctl_path)
1✔
65

66

67
def test_binary_build_image(binary_path: str, binary: DockerBinary | PodmanBinary) -> None:
1✔
68
    dockerfile = "src/test/repo/Dockerfile"
1✔
69
    digest = Digest(sha256().hexdigest(), 123)
1✔
70
    tags = (
1✔
71
        "test:0.1.0",
72
        "test:latest",
73
    )
74
    env = {"DOCKER_HOST": "tcp://127.0.0.1:1234"}
1✔
75
    build_request = binary.build_image(
1✔
76
        tags=tags,
77
        digest=digest,
78
        dockerfile=dockerfile,
79
        build_args=DockerBuildArgs.from_strings("arg1=2"),
80
        context_root="build/context",
81
        env=env,
82
        extra_args=("--pull", "--squash"),
83
        output={"type": "docker"},
84
    )
85

86
    assert build_request == Process(
1✔
87
        argv=(
88
            binary_path,
89
            "build",
90
            "--pull",
91
            "--squash",
92
            "--output",
93
            "type=docker",
94
            "--tag",
95
            tags[0],
96
            "--tag",
97
            tags[1],
98
            "--build-arg",
99
            "arg1=2",
100
            "--file",
101
            dockerfile,
102
            "build/context",
103
        ),
104
        env=env,
105
        input_digest=digest,
106
        cache_scope=ProcessCacheScope.PER_SESSION,
107
        description="",  # The description field is marked `compare=False`
108
    )
109
    assert build_request.description == "Building docker image test:0.1.0 +1 additional tag."
1✔
110

111

112
def test_binary_push_image(binary_path: str, binary: DockerBinary | PodmanBinary) -> None:
1✔
113
    image_ref = "registry/repo/name:tag"
1✔
114
    push_request = binary.push_image(image_ref)
1✔
115
    assert push_request == Process(
1✔
116
        argv=(binary_path, "push", image_ref),
117
        cache_scope=ProcessCacheScope.PER_SESSION,
118
        description="",  # The description field is marked `compare=False`
119
    )
120
    assert push_request.description == f"Pushing docker image {image_ref}"
1✔
121

122

123
def test_binary_run_image(binary_path: str, binary: DockerBinary | PodmanBinary) -> None:
1✔
124
    image_ref = "registry/repo/name:tag"
1✔
125
    port_spec = "127.0.0.1:80:8080/tcp"
1✔
126
    run_request = binary.run_image(
1✔
127
        image_ref, docker_run_args=("-p", port_spec), image_args=("test-input",)
128
    )
129
    assert run_request == Process(
1✔
130
        argv=(binary_path, "run", "-p", port_spec, image_ref, "test-input"),
131
        cache_scope=ProcessCacheScope.PER_SESSION,
132
        description="",  # The description field is marked `compare=False`
133
    )
134
    assert run_request.description == f"Running docker image {image_ref}"
1✔
135

136

137
def test_buildctl_binary_build_image(buildctl_path: str, buildctl: BuildctlBinary) -> None:
1✔
138
    dockerfile = "src/test/repo/Dockerfile"
1✔
139
    digest = Digest(sha256().hexdigest(), 123)
1✔
140
    tags = (
1✔
141
        "test:0.1.0",
142
        "test:latest",
143
    )
144
    env = {"BUILDKIT_HOST": "tcp://127.0.0.1:1234"}
1✔
145
    build_request = buildctl.build_image(
1✔
146
        tags=tags,
147
        digest=digest,
148
        dockerfile=dockerfile,
149
        build_args=DockerBuildArgs.from_strings("arg1=2"),
150
        context_root="build/context",
151
        env=env,
152
        extra_args=("--progress=plain",),
153
        output=None,
154
    )
155

156
    assert build_request == Process(
1✔
157
        argv=(
158
            buildctl_path,
159
            "build",
160
            "--frontend",
161
            "dockerfile.v0",
162
            "--local",
163
            "context=build/context",
164
            "--local",
165
            "dockerfile=src/test/repo",
166
            "--opt",
167
            "filename=Dockerfile",
168
            "--progress=plain",
169
            "--opt",
170
            "build-arg:arg1=2",
171
            "--output",
172
            "type=image,name=test:0.1.0",
173
            "--output",
174
            "type=image,name=test:latest",
175
        ),
176
        env=env,
177
        input_digest=digest,
178
        cache_scope=ProcessCacheScope.PER_SESSION,
179
        description="",  # The description field is marked `compare=False`
180
    )
181
    assert build_request.description == "Building docker image test:0.1.0 +1 additional tag."
1✔
182

183

184
def test_buildctl_binary_build_image_publish(buildctl_path: str, buildctl: BuildctlBinary) -> None:
1✔
185
    dockerfile = "src/test/repo/Dockerfile"
1✔
186
    digest = Digest(sha256().hexdigest(), 123)
1✔
187
    tags = (
1✔
188
        "test:0.1.0",
189
        "test:latest",
190
    )
191
    build_request = buildctl.build_image(
1✔
192
        tags=tags,
193
        digest=digest,
194
        dockerfile=dockerfile,
195
        build_args=DockerBuildArgs(()),
196
        context_root="build/context",
197
        env={},
198
        output=None,
199
        is_publish=True,
200
    )
201

202
    assert build_request == Process(
1✔
203
        argv=(
204
            buildctl_path,
205
            "build",
206
            "--frontend",
207
            "dockerfile.v0",
208
            "--local",
209
            "context=build/context",
210
            "--local",
211
            "dockerfile=src/test/repo",
212
            "--opt",
213
            "filename=Dockerfile",
214
            "--output",
215
            "type=image,name=test:0.1.0,push=true",
216
            "--output",
217
            "type=image,name=test:latest,push=true",
218
        ),
219
        input_digest=digest,
220
        cache_scope=ProcessCacheScope.PER_SESSION,
221
        description="",
222
    )
223

224

225
def test_buildctl_binary_build_image_custom_output(
1✔
226
    buildctl_path: str, buildctl: BuildctlBinary
227
) -> None:
228
    dockerfile = "src/test/repo/Dockerfile"
1✔
229
    digest = Digest(sha256().hexdigest(), 123)
1✔
230
    tags = ("test:0.1.0",)
1✔
231
    build_request = buildctl.build_image(
1✔
232
        tags=tags,
233
        digest=digest,
234
        dockerfile=dockerfile,
235
        build_args=DockerBuildArgs(()),
236
        context_root=".",
237
        env={},
238
        output={"type": "image", "name": "custom:tag", "push": "true"},
239
    )
240

241
    assert build_request == Process(
1✔
242
        argv=(
243
            buildctl_path,
244
            "build",
245
            "--frontend",
246
            "dockerfile.v0",
247
            "--local",
248
            "context=.",
249
            "--local",
250
            "dockerfile=src/test/repo",
251
            "--opt",
252
            "filename=Dockerfile",
253
            "--output",
254
            "type=image,name=custom:tag,push=true",
255
        ),
256
        input_digest=digest,
257
        cache_scope=ProcessCacheScope.PER_SESSION,
258
        description="",
259
    )
260

261

262
@pytest.mark.parametrize(
1✔
263
    ["binary_name", "rule_func", "binary_cls"],
264
    [
265
        ("docker", get_docker, DockerBinary),
266
        ("podman", get_podman, PodmanBinary),
267
        ("buildctl", get_buildctl, BuildctlBinary),
268
    ],
269
)
270
def test_get_binary(binary_name, rule_func, binary_cls) -> None:
1✔
271
    docker_options = create_subsystem(
1✔
272
        DockerOptions,
273
        tools=[],
274
        optional_tools=[],
275
    )
276
    docker_options_env_aware = mock.MagicMock(spec=DockerOptions.EnvironmentAware)
1✔
277

278
    def mock_find_binary(request: BinaryPathRequest) -> BinaryPaths:
1✔
279
        if request.binary_name == binary_name:
1✔
280
            return BinaryPaths(binary_name, [BinaryPath(f"/bin/{binary_name}")])
1✔
NEW
281
        return BinaryPaths(request.binary_name, ())
×
282

283
    def mock_create_binary_shims(_request: BinaryShimsRequest) -> BinaryShims:
1✔
UNCOV
284
        return BinaryShims(EMPTY_DIGEST, "cache_name")
×
285

286
    result = run_rule_with_mocks(
1✔
287
        rule_func,
288
        rule_args=[docker_options, docker_options_env_aware],
289
        mock_calls={
290
            "pants.core.util_rules.system_binaries.find_binary": mock_find_binary,
291
            "pants.core.util_rules.system_binaries.create_binary_shims": mock_create_binary_shims,
292
        },
293
    )
294

295
    assert isinstance(result, binary_cls)
1✔
296
    assert result.path == f"/bin/{binary_name}"
1✔
297

298

299
@pytest.mark.parametrize(
1✔
300
    ["binary_name", "rule_func"],
301
    [
302
        ("docker", get_docker),
303
        ("podman", get_podman),
304
        ("buildctl", get_buildctl),
305
    ],
306
)
307
def test_get_binary_with_tools(binary_name, rule_func) -> None:
1✔
308
    def mock_find_binary(request: BinaryPathRequest) -> BinaryPaths:
1✔
309
        if request.binary_name == binary_name:
1✔
310
            return BinaryPaths(binary_name, paths=[BinaryPath(f"/bin/{binary_name}")])
1✔
311
        elif request.binary_name == "real-tool":
1✔
312
            return BinaryPaths("real-tool", paths=[BinaryPath("/bin/a-real-tool")])
1✔
313
        else:
314
            return BinaryPaths(request.binary_name, ())
1✔
315

316
    def mock_create_binary_shims(_request: BinaryShimsRequest) -> BinaryShims:
1✔
317
        return BinaryShims(EMPTY_DIGEST, "cache_name")
1✔
318

319
    def run(tools: list[str], optional_tools: list[str]) -> None:
1✔
320
        docker_options = create_subsystem(
1✔
321
            DockerOptions,
322
            tools=tools,
323
            optional_tools=optional_tools,
324
        )
325
        docker_options_env_aware = mock.MagicMock(spec=DockerOptions.EnvironmentAware)
1✔
326

327
        nonlocal mock_find_binary
328
        nonlocal mock_create_binary_shims
329

330
        run_rule_with_mocks(
1✔
331
            rule_func,
332
            rule_args=[docker_options, docker_options_env_aware],
333
            mock_calls={
334
                "pants.core.util_rules.system_binaries.find_binary": mock_find_binary,
335
                "pants.core.util_rules.system_binaries.create_binary_shims": mock_create_binary_shims,
336
            },
337
        )
338

339
    run(tools=["real-tool"], optional_tools=[])
1✔
340

341
    with pytest.raises(BinaryNotFoundError, match="Cannot find `nonexistent-tool`"):
1✔
342
        run(tools=["real-tool", "nonexistent-tool"], optional_tools=[])
1✔
343

344
    # Optional non-existent tool should still succeed.
345
    run(tools=[], optional_tools=["real-tool", "nonexistent-tool"])
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