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

pantsbuild / pants / 18252174847

05 Oct 2025 01:36AM UTC coverage: 43.382% (-36.9%) from 80.261%
18252174847

push

github

web-flow
run tests on mac arm (#22717)

Just doing the minimal to pull forward the x86_64 pattern.

ref #20993

25776 of 59416 relevant lines covered (43.38%)

1.3 hits per line

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

60.19
/src/python/pants/backend/shell/shunit2_test_runner.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
import os
3✔
5
import re
3✔
6
from dataclasses import dataclass
3✔
7
from typing import Any
3✔
8

9
from pants.backend.shell.subsystems.shell_setup import ShellSetup
3✔
10
from pants.backend.shell.subsystems.shunit2 import Shunit2
3✔
11
from pants.backend.shell.target_types import (
3✔
12
    ShellSourceField,
13
    Shunit2Shell,
14
    Shunit2ShellField,
15
    Shunit2TestsGeneratorTarget,
16
    Shunit2TestSourceField,
17
    Shunit2TestTimeoutField,
18
    SkipShunit2TestsField,
19
)
20
from pants.core.goals.test import (
3✔
21
    BuildPackageDependenciesRequest,
22
    RuntimePackageDependenciesField,
23
    TestDebugRequest,
24
    TestExtraEnv,
25
    TestFieldSet,
26
    TestRequest,
27
    TestResult,
28
    TestSubsystem,
29
    build_runtime_package_dependencies,
30
)
31
from pants.core.target_types import FileSourceField, ResourceSourceField
3✔
32
from pants.core.util_rules.external_tool import download_external_tool
3✔
33
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
3✔
34
from pants.core.util_rules.system_binaries import (
3✔
35
    BinaryNotFoundError,
36
    BinaryPath,
37
    BinaryPathRequest,
38
    find_binary,
39
)
40
from pants.engine.addresses import Address
3✔
41
from pants.engine.fs import CreateDigest, FileContent, MergeDigests
3✔
42
from pants.engine.internals.graph import transitive_targets as transitive_targets_get
3✔
43
from pants.engine.intrinsics import (
3✔
44
    create_digest,
45
    execute_process_with_retry,
46
    get_digest_contents,
47
    merge_digests,
48
)
49
from pants.engine.platform import Platform
3✔
50
from pants.engine.process import InteractiveProcess, Process, ProcessCacheScope, ProcessWithRetries
3✔
51
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
3✔
52
from pants.engine.target import SourcesField, Target, TransitiveTargetsRequest
3✔
53
from pants.option.global_options import GlobalOptions
3✔
54
from pants.util.docutil import bin_name
3✔
55
from pants.util.logging import LogLevel
3✔
56

57

58
@dataclass(frozen=True)
3✔
59
class Shunit2FieldSet(TestFieldSet):
3✔
60
    required_fields = (Shunit2TestSourceField,)
3✔
61

62
    sources: Shunit2TestSourceField
3✔
63
    timeout: Shunit2TestTimeoutField
3✔
64
    shell: Shunit2ShellField
3✔
65
    runtime_package_dependencies: RuntimePackageDependenciesField
3✔
66

67
    @classmethod
3✔
68
    def opt_out(cls, tgt: Target) -> bool:
3✔
69
        return tgt.get(SkipShunit2TestsField).value
×
70

71

72
class Shunit2TestRequest(TestRequest):
3✔
73
    tool_subsystem = Shunit2
3✔
74
    field_set_type = Shunit2FieldSet
3✔
75
    supports_debug = True
3✔
76

77

78
@dataclass(frozen=True)
3✔
79
class TestSetupRequest:
3✔
80
    field_set: Shunit2FieldSet
3✔
81

82

83
@dataclass(frozen=True)
3✔
84
class TestSetup:
3✔
85
    process: Process
3✔
86

87

88
class ShellNotConfigured(Exception):
3✔
89
    pass
3✔
90

91

92
SOURCE_SHUNIT2_REGEX = re.compile(rb"(?:source|\.)\s+[.${}/'\"\w]*shunit2\b['\"]?")
3✔
93

94

95
def add_source_shunit2(fc: FileContent, binary_name: str) -> FileContent:
3✔
96
    # NB: We always run tests from the build root, so we source `shunit2` relative to there.
97
    source_line = f"source ./{binary_name}".encode()
×
98

99
    lines: list[bytes] = []
×
100
    already_had_source = False
×
101
    for line in fc.content.splitlines():
×
102
        if SOURCE_SHUNIT2_REGEX.search(line):
×
103
            lines.append(SOURCE_SHUNIT2_REGEX.sub(source_line, line))
×
104
            already_had_source = True
×
105
        else:
106
            lines.append(line)
×
107

108
    if not already_had_source:
×
109
        lines.append(source_line)
×
110

111
    return FileContent(fc.path, b"\n".join(lines))
×
112

113

114
@dataclass(frozen=True)
3✔
115
class Shunit2RunnerRequest:
3✔
116
    address: Address
3✔
117
    test_file_content: FileContent
3✔
118
    shell_field: Shunit2ShellField
3✔
119

120

121
@dataclass(frozen=True)
3✔
122
class Shunit2Runner:
3✔
123
    shell: Shunit2Shell
3✔
124
    binary_path: BinaryPath
3✔
125

126

127
@rule(desc="Determine shunit2 shell")
3✔
128
async def determine_shunit2_shell(
3✔
129
    request: Shunit2RunnerRequest,
130
    shell_setup: ShellSetup.EnvironmentAware,
131
) -> Shunit2Runner:
132
    if request.shell_field.value is not None:
×
133
        tgt_shell = Shunit2Shell(request.shell_field.value)
×
134
    else:
135
        parse_result = Shunit2Shell.parse_shebang(request.test_file_content.content)
×
136
        if parse_result is None:
×
137
            raise ShellNotConfigured(
×
138
                f"Could not determine which shell to use to run shunit2 on {request.address}.\n\n"
139
                f"Please either specify the `{Shunit2ShellField.alias}` field or add a "
140
                f"shebang to {request.test_file_content.path} with one of the supported shells in "
141
                f"the format `!#/path/to/shell` or `!#/path/to/env shell`"
142
                f"(run `{bin_name()} help {Shunit2TestsGeneratorTarget.alias}` for valid shells)."
143
            )
144
        tgt_shell = parse_result
×
145

146
    path_request = BinaryPathRequest(
×
147
        binary_name=tgt_shell.name,
148
        search_path=shell_setup.executable_search_path,
149
        test=tgt_shell.binary_path_test,
150
    )
151
    paths = await find_binary(path_request, **implicitly())
×
152
    first_path = paths.first_path
×
153
    if not first_path:
×
154
        raise BinaryNotFoundError.from_request(
×
155
            path_request, rationale=f"run shunit2 on {request.address}"
156
        )
157
    return Shunit2Runner(tgt_shell, first_path)
×
158

159

160
@rule(desc="Setup shunit2", level=LogLevel.DEBUG)
3✔
161
async def setup_shunit2_for_target(
3✔
162
    request: TestSetupRequest,
163
    shell_setup: ShellSetup.EnvironmentAware,
164
    test_subsystem: TestSubsystem,
165
    test_extra_env: TestExtraEnv,
166
    shunit2: Shunit2,
167
    platform: Platform,
168
) -> TestSetup:
169
    shunit2_script, transitive_targets, built_package_dependencies = await concurrently(
×
170
        download_external_tool(shunit2.get_request(platform)),
171
        transitive_targets_get(
172
            TransitiveTargetsRequest([request.field_set.address]), **implicitly()
173
        ),
174
        build_runtime_package_dependencies(
175
            BuildPackageDependenciesRequest(request.field_set.runtime_package_dependencies)
176
        ),
177
    )
178

179
    dependencies_source_files_request = determine_source_files(
×
180
        SourceFilesRequest(
181
            (tgt.get(SourcesField) for tgt in transitive_targets.dependencies),
182
            for_sources_types=(ShellSourceField, FileSourceField, ResourceSourceField),
183
            enable_codegen=True,
184
        )
185
    )
186
    dependencies_source_files, field_set_sources = await concurrently(
×
187
        dependencies_source_files_request,
188
        determine_source_files(SourceFilesRequest([request.field_set.sources])),
189
    )
190

191
    field_set_digest_content = await get_digest_contents(field_set_sources.snapshot.digest)
×
192
    # `ShellTestSourceField` validates that there's exactly one file.
193
    test_file_content = field_set_digest_content[0]
×
194
    updated_test_file_content = add_source_shunit2(test_file_content, shunit2_script.exe)
×
195

196
    updated_test_digest, runner = await concurrently(
×
197
        create_digest(CreateDigest([updated_test_file_content])),
198
        determine_shunit2_shell(
199
            Shunit2RunnerRequest(
200
                request.field_set.address, test_file_content, request.field_set.shell
201
            ),
202
            **implicitly(),
203
        ),
204
    )
205

206
    input_digest = await merge_digests(
×
207
        MergeDigests(
208
            (
209
                shunit2_script.digest,
210
                updated_test_digest,
211
                dependencies_source_files.snapshot.digest,
212
                *(pkg.digest for pkg in built_package_dependencies),
213
            )
214
        )
215
    )
216

217
    env_dict = {
×
218
        "PATH": os.pathsep.join(shell_setup.executable_search_path),
219
        # Always include colors and strip them out for display below (if required), for better cache
220
        # hit rates
221
        "SHUNIT_COLOR": "always",
222
        **test_extra_env.env,
223
    }
224
    argv = (
×
225
        # Zsh requires extra args. See https://github.com/kward/shunit2/#-zsh.
226
        [runner.binary_path.path, "-o", "shwordsplit", "--", *field_set_sources.snapshot.files]
227
        if runner.shell == Shunit2Shell.zsh
228
        else [runner.binary_path.path, *field_set_sources.snapshot.files]
229
    )
230
    cache_scope = (
×
231
        ProcessCacheScope.PER_SESSION if test_subsystem.force else ProcessCacheScope.SUCCESSFUL
232
    )
233
    process = Process(
×
234
        argv=argv,
235
        input_digest=input_digest,
236
        description=f"Run shunit2 for {request.field_set.address}.",
237
        level=LogLevel.DEBUG,
238
        env=env_dict,
239
        timeout_seconds=request.field_set.timeout.calculate_from_global_options(test_subsystem),
240
        cache_scope=cache_scope,
241
    )
242
    return TestSetup(process)
×
243

244

245
@rule(desc="Run tests with Shunit2", level=LogLevel.DEBUG)
3✔
246
async def run_tests_with_shunit2(
3✔
247
    batch: Shunit2TestRequest.Batch[Shunit2FieldSet, Any],
248
    test_subsystem: TestSubsystem,
249
    global_options: GlobalOptions,
250
) -> TestResult:
251
    field_set = batch.single_element
×
252

253
    setup = await setup_shunit2_for_target(TestSetupRequest(field_set), **implicitly())
×
254
    results = await execute_process_with_retry(
×
255
        ProcessWithRetries(setup.process, test_subsystem.attempts_default)
256
    )
257
    return TestResult.from_fallible_process_result(
×
258
        process_results=results.results,
259
        address=field_set.address,
260
        output_setting=test_subsystem.output,
261
        output_simplifier=global_options.output_simplifier(),
262
    )
263

264

265
@rule(desc="Setup Shunit2 to run interactively", level=LogLevel.DEBUG)
3✔
266
async def setup_shunit2_debug_test(
3✔
267
    batch: Shunit2TestRequest.Batch[Shunit2FieldSet, Any],
268
) -> TestDebugRequest:
269
    setup = await setup_shunit2_for_target(TestSetupRequest(batch.single_element), **implicitly())
×
270
    return TestDebugRequest(
×
271
        InteractiveProcess.from_process(
272
            setup.process, forward_signals_to_process=False, restartable=True
273
        )
274
    )
275

276

277
def rules():
3✔
278
    return (
3✔
279
        *collect_rules(),
280
        *Shunit2TestRequest.rules(),
281
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc