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

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

89.16
/src/python/pants/backend/shell/target_types.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
3✔
5

6
import re
3✔
7
from enum import Enum
3✔
8

9
from pants.backend.adhoc.target_types import (
3✔
10
    AdhocToolCacheScopeField,
11
    AdhocToolDependenciesField,
12
    AdhocToolExecutionDependenciesField,
13
    AdhocToolExtraEnvVarsField,
14
    AdhocToolLogOutputField,
15
    AdhocToolNamedCachesField,
16
    AdhocToolOutputDependenciesField,
17
    AdhocToolOutputDirectoriesField,
18
    AdhocToolOutputFilesField,
19
    AdhocToolOutputRootDirField,
20
    AdhocToolOutputsMatchMode,
21
    AdhocToolPathEnvModifyModeField,
22
    AdhocToolRunnableDependenciesField,
23
    AdhocToolTimeoutField,
24
    AdhocToolWorkdirField,
25
    AdhocToolWorkspaceInvalidationSourcesField,
26
)
27
from pants.backend.shell.subsystems.shell_setup import ShellSetup
3✔
28
from pants.core.environments.target_types import EnvironmentField
3✔
29
from pants.core.goals.test import RuntimePackageDependenciesField, TestTimeoutField
3✔
30
from pants.core.util_rules.system_binaries import BinaryPathTest
3✔
31
from pants.engine.rules import collect_rules, rule
3✔
32
from pants.engine.target import (
3✔
33
    COMMON_TARGET_FIELDS,
34
    BoolField,
35
    MultipleSourcesField,
36
    OverridesField,
37
    SingleSourceField,
38
    StringField,
39
    StringSequenceField,
40
    Target,
41
    TargetFilesGenerator,
42
    TargetFilesGeneratorSettings,
43
    TargetFilesGeneratorSettingsRequest,
44
    generate_file_based_overrides_field_help_message,
45
    generate_multiple_sources_field_help_message,
46
)
47
from pants.engine.unions import UnionRule
3✔
48
from pants.util.enums import match
3✔
49
from pants.util.strutil import help_text
3✔
50

51

52
class ShellDependenciesField(AdhocToolDependenciesField):
3✔
53
    pass
3✔
54

55

56
class ShellSourceField(SingleSourceField):
3✔
57
    # Normally, we would add `expected_file_extensions = ('.sh',)`, but Bash scripts don't need a
58
    # file extension, so we don't use this.
59
    uses_source_roots = False
3✔
60

61

62
class ShellGeneratingSourcesBase(MultipleSourcesField):
3✔
63
    uses_source_roots = False
3✔
64

65

66
class ShellGeneratorSettingsRequest(TargetFilesGeneratorSettingsRequest):
3✔
67
    pass
3✔
68

69

70
@rule
3✔
71
async def generator_settings(
3✔
72
    _: ShellGeneratorSettingsRequest,
73
    shell_setup: ShellSetup,
74
) -> TargetFilesGeneratorSettings:
75
    return TargetFilesGeneratorSettings(
×
76
        add_dependencies_on_all_siblings=not shell_setup.dependency_inference
77
    )
78

79

80
# -----------------------------------------------------------------------------------------------
81
# `shunit2_test` target
82
# -----------------------------------------------------------------------------------------------
83

84

85
class Shunit2Shell(Enum):
3✔
86
    sh = "sh"
3✔
87
    bash = "bash"
3✔
88
    dash = "dash"
3✔
89
    ksh = "ksh"
3✔
90
    pdksh = "pdksh"
3✔
91
    zsh = "zsh"
3✔
92

93
    @classmethod
3✔
94
    def parse_shebang(cls, shebang: bytes) -> Shunit2Shell | None:
3✔
95
        if not shebang:
×
96
            return None
×
97
        first_line = shebang.splitlines()[0]
×
98
        matches = re.match(rb"^#! *[/\w]*/(?P<program>\w+) *(?P<arg>\w*)", first_line)
×
99
        if not matches:
×
100
            return None
×
101
        program = matches.group("program")
×
102
        if program == b"env":
×
103
            program = matches.group("arg")
×
104
        try:
×
105
            return cls(program.decode())
×
106
        except ValueError:
×
107
            return None
×
108

109
    @property
3✔
110
    def binary_path_test(self) -> BinaryPathTest | None:
3✔
111
        arg = match(  # type: ignore[misc]
×
112
            self,
113
            {
114
                self.sh: None,
115
                self.bash: "--version",
116
                self.dash: None,
117
                self.ksh: "--version",
118
                self.pdksh: None,
119
                self.zsh: "--version",
120
            },
121
        )
122
        if not arg:
×
123
            return None
×
124
        return BinaryPathTest((arg,))
×
125

126

127
class Shunit2TestDependenciesField(ShellDependenciesField):
3✔
128
    supports_transitive_excludes = True
3✔
129

130

131
class Shunit2TestTimeoutField(TestTimeoutField):
3✔
132
    pass
3✔
133

134

135
class SkipShunit2TestsField(BoolField):
3✔
136
    alias = "skip_tests"
3✔
137
    default = False
3✔
138
    help = "If true, don't run this target's tests."
3✔
139

140

141
class Shunit2TestSourceField(ShellSourceField):
3✔
142
    pass
3✔
143

144

145
class Shunit2ShellField(StringField):
3✔
146
    alias = "shell"
3✔
147
    valid_choices = Shunit2Shell
3✔
148
    help = "Which shell to run the tests with. If unspecified, Pants will look for a shebang line."
3✔
149

150

151
class Shunit2TestTarget(Target):
3✔
152
    alias = "shunit2_test"
3✔
153
    core_fields = (
3✔
154
        *COMMON_TARGET_FIELDS,
155
        Shunit2TestSourceField,
156
        Shunit2TestDependenciesField,
157
        Shunit2TestTimeoutField,
158
        SkipShunit2TestsField,
159
        Shunit2ShellField,
160
        RuntimePackageDependenciesField,
161
    )
162
    help = help_text(
3✔
163
        f"""
164
        A single test file for Bourne-based shell scripts using the shunit2 test framework.
165

166
        To use, add tests to your file per https://github.com/kward/shunit2/. Specify the shell
167
        to run with by either setting the field `{Shunit2ShellField.alias}` or including a
168
        shebang. To test the same file with multiple shells, create multiple `shunit2_tests`
169
        targets, one for each shell.
170

171
        Pants will automatically download the `shunit2` bash script and add
172
        `source ./shunit2` to your test for you. If you already have `source ./shunit2`,
173
        Pants will overwrite it to use the correct relative path.
174
        """
175
    )
176

177

178
# -----------------------------------------------------------------------------------------------
179
# `shunit2_tests` target generator
180
# -----------------------------------------------------------------------------------------------
181

182

183
class Shunit2TestsGeneratorSourcesField(ShellGeneratingSourcesBase):
3✔
184
    default = ("*_test.sh", "test_*.sh", "tests.sh")
3✔
185
    help = generate_multiple_sources_field_help_message(
3✔
186
        "Example: `sources=['test.sh', 'test_*.sh', '!test_ignore.sh']`"
187
    )
188

189

190
class Shunit2TestsOverrideField(OverridesField):
3✔
191
    help = generate_file_based_overrides_field_help_message(
3✔
192
        Shunit2TestTarget.alias,
193
        """
194
        overrides={
195
            "foo_test.sh": {"timeout": 120},
196
            "bar_test.sh": {"timeout": 200},
197
            ("foo_test.sh", "bar_test.sh"): {"tags": ["slow_tests"]},
198
        }
199
        """,
200
    )
201

202

203
class Shunit2TestsGeneratorTarget(TargetFilesGenerator):
3✔
204
    alias = "shunit2_tests"
3✔
205
    core_fields = (
3✔
206
        *COMMON_TARGET_FIELDS,
207
        Shunit2TestsGeneratorSourcesField,
208
        Shunit2TestsOverrideField,
209
    )
210
    generated_target_cls = Shunit2TestTarget
3✔
211
    copied_fields = COMMON_TARGET_FIELDS
3✔
212
    moved_fields = (
3✔
213
        Shunit2TestDependenciesField,
214
        Shunit2TestTimeoutField,
215
        SkipShunit2TestsField,
216
        Shunit2ShellField,
217
        RuntimePackageDependenciesField,
218
    )
219
    help = "Generate a `shunit2_test` target for each file in the `sources` field."
3✔
220

221

222
# -----------------------------------------------------------------------------------------------
223
# `shell_source` and `shell_sources` targets
224
# -----------------------------------------------------------------------------------------------
225

226

227
class ShellSourceTarget(Target):
3✔
228
    alias = "shell_source"
3✔
229
    core_fields = (*COMMON_TARGET_FIELDS, ShellDependenciesField, ShellSourceField)
3✔
230
    help = "A single Bourne-based shell script, e.g. a Bash script."
3✔
231

232

233
class ShellSourcesGeneratingSourcesField(ShellGeneratingSourcesBase):
3✔
234
    default = ("*.sh",) + tuple(f"!{pat}" for pat in Shunit2TestsGeneratorSourcesField.default)
3✔
235
    help = generate_multiple_sources_field_help_message(
3✔
236
        "Example: `sources=['example.sh', 'new_*.sh', '!old_ignore.sh']`"
237
    )
238

239

240
class ShellSourcesOverridesField(OverridesField):
3✔
241
    help = generate_file_based_overrides_field_help_message(
3✔
242
        ShellSourceTarget.alias,
243
        """
244
        overrides={
245
            "foo.sh": {"skip_shellcheck": True]},
246
            "bar.sh": {"skip_shfmt": True]},
247
            ("foo.sh", "bar.sh"): {"tags": ["linter_disabled"]},
248
        }
249
        """,
250
    )
251

252

253
class ShellSourcesGeneratorTarget(TargetFilesGenerator):
3✔
254
    alias = "shell_sources"
3✔
255
    core_fields = (
3✔
256
        *COMMON_TARGET_FIELDS,
257
        ShellSourcesGeneratingSourcesField,
258
        ShellSourcesOverridesField,
259
    )
260
    generated_target_cls = ShellSourceTarget
3✔
261
    copied_fields = COMMON_TARGET_FIELDS
3✔
262
    moved_fields = (ShellDependenciesField,)
3✔
263
    help = "Generate a `shell_source` target for each file in the `sources` field."
3✔
264

265

266
# -----------------------------------------------------------------------------------------------
267
# `shell_command` target
268
# -----------------------------------------------------------------------------------------------
269

270

271
class ShellCommandCommandField(StringField):
3✔
272
    alias = "command"
3✔
273
    required = True
3✔
274
    help = help_text(
3✔
275
        """
276
        Shell command to execute.
277

278
        The command is executed as `'bash -c <command>'` by default. If you want to invoke a binary
279
        use `exec -a $0 <binary> <args>` as the command so that the binary gets the correct `argv[0]`
280
        set.
281
        """
282
    )
283

284

285
class ShellCommandOutputFilesField(AdhocToolOutputFilesField):
3✔
286
    pass
3✔
287

288

289
class ShellCommandOutputDirectoriesField(AdhocToolOutputDirectoriesField):
3✔
290
    pass
3✔
291

292

293
class ShellCommandOutputDependenciesField(AdhocToolOutputDependenciesField):
3✔
294
    pass
3✔
295

296

297
class ShellCommandExecutionDependenciesField(AdhocToolExecutionDependenciesField):
3✔
298
    pass
3✔
299

300

301
class RunShellCommandExecutionDependenciesField(ShellCommandExecutionDependenciesField):
3✔
302
    help = help_text(
3✔
303
        lambda: f"""
304
        The execution dependencies for this command.
305

306
        Dependencies specified here are those required to make the command complete successfully
307
        (e.g. file inputs, packages compiled from other targets, etc), but NOT required to make
308
        the outputs of the command useful.
309

310
        See also `{RunShellCommandRunnableDependenciesField.alias}`.
311
        """
312
    )
313

314

315
class ShellCommandRunnableDependenciesField(AdhocToolRunnableDependenciesField):
3✔
316
    pass
3✔
317

318

319
class RunShellCommandRunnableDependenciesField(ShellCommandRunnableDependenciesField):
3✔
320
    help = help_text(
3✔
321
        lambda: f"""
322
        The runnable dependencies for this command.
323

324
        Dependencies specified here are those required to exist on the `PATH` to make the command
325
        complete successfully (interpreters specified in a `#!` command, etc). Note that these
326
        dependencies will be made available on the `PATH` with the name of the target.
327

328
        See also `{RunShellCommandExecutionDependenciesField.alias}`.
329
        """
330
    )
331

332

333
class ShellCommandSourcesField(MultipleSourcesField):
3✔
334
    # We solely register this field for codegen to work.
335
    alias = "_sources"
3✔
336
    uses_source_roots = False
3✔
337
    expected_num_files = 0
3✔
338

339

340
class ShellCommandTimeoutField(AdhocToolTimeoutField):
3✔
341
    pass
3✔
342

343

344
class ShellCommandToolsField(StringSequenceField):
3✔
345
    alias = "tools"
3✔
346
    default = ()
3✔
347
    help = help_text(
3✔
348
        """
349
        Specify required executable tools that might be used.
350

351
        Only the tools explicitly provided will be available on the search PATH,
352
        and these tools must be found on the paths provided by
353
        `[shell-setup].executable_search_paths` (which defaults to the system PATH).
354
        """
355
    )
356

357

358
class ShellCommandExtraEnvVarsField(AdhocToolExtraEnvVarsField):
3✔
359
    pass
3✔
360

361

362
class ShellCommandLogOutputField(AdhocToolLogOutputField):
3✔
363
    pass
3✔
364

365

366
class ShellCommandWorkdirField(AdhocToolWorkdirField):
3✔
367
    pass
3✔
368

369

370
class RunShellCommandWorkdirField(AdhocToolWorkdirField):
3✔
371
    pass
3✔
372

373

374
class ShellCommandOutputRootDirField(AdhocToolOutputRootDirField):
3✔
375
    pass
3✔
376

377

378
class ShellCommandTestDependenciesField(ShellCommandExecutionDependenciesField):
3✔
379
    pass
3✔
380

381

382
class ShellCommandNamedCachesField(AdhocToolNamedCachesField):
3✔
383
    pass
3✔
384

385

386
class ShellCommandWorkspaceInvalidationSourcesField(AdhocToolWorkspaceInvalidationSourcesField):
3✔
387
    pass
3✔
388

389

390
class ShellCommandPathEnvModifyModeField(AdhocToolPathEnvModifyModeField):
3✔
391
    pass
3✔
392

393

394
class ShellCommandOutputsMatchMode(AdhocToolOutputsMatchMode):
3✔
395
    pass
3✔
396

397

398
class ShellCommandCacheScopeField(AdhocToolCacheScopeField):
3✔
399
    pass
3✔
400

401

402
class SkipShellCommandTestsField(BoolField):
3✔
403
    alias = "skip_tests"
3✔
404
    default = False
3✔
405
    help = "If true, don't run this tests for target."
3✔
406

407

408
class ShellCommandTarget(Target):
3✔
409
    alias = "shell_command"
3✔
410
    core_fields = (
3✔
411
        *COMMON_TARGET_FIELDS,
412
        ShellCommandOutputDependenciesField,
413
        ShellCommandExecutionDependenciesField,
414
        ShellCommandRunnableDependenciesField,
415
        ShellCommandCommandField,
416
        ShellCommandLogOutputField,
417
        ShellCommandOutputFilesField,
418
        ShellCommandOutputDirectoriesField,
419
        ShellCommandSourcesField,
420
        ShellCommandTimeoutField,
421
        ShellCommandToolsField,
422
        ShellCommandExtraEnvVarsField,
423
        ShellCommandWorkdirField,
424
        ShellCommandNamedCachesField,
425
        ShellCommandOutputRootDirField,
426
        ShellCommandWorkspaceInvalidationSourcesField,
427
        ShellCommandPathEnvModifyModeField,
428
        ShellCommandOutputsMatchMode,
429
        ShellCommandCacheScopeField,
430
        EnvironmentField,
431
    )
432
    help = help_text(
3✔
433
        """
434
        Execute any external tool for its side effects.
435

436
        Example BUILD file:
437

438
            shell_command(
439
                command="./my-script.sh --flag",
440
                tools=["tar", "curl", "cat", "bash", "env"],
441
                execution_dependencies=[":scripts"],
442
                output_files=["logs/my-script.log"],
443
                output_directories=["results"],
444
            )
445

446
            shell_sources(name="scripts")
447

448
        Remember to add this target to the dependencies of each consumer, such as your
449
        `python_tests` or `docker_image`. When relevant, Pants will run your `command` and
450
        insert the `outputs` into that consumer's context.
451

452
        The command may be retried and/or cancelled, so ensure that it is idempotent.
453
        """
454
    )
455

456

457
class ShellCommandRunTarget(Target):
3✔
458
    alias = "run_shell_command"
3✔
459
    core_fields = (
3✔
460
        *COMMON_TARGET_FIELDS,
461
        RunShellCommandExecutionDependenciesField,
462
        RunShellCommandRunnableDependenciesField,
463
        ShellCommandCommandField,
464
        RunShellCommandWorkdirField,
465
    )
466
    help = help_text(
3✔
467
        """
468
        Run a script in the workspace, with all dependencies packaged/copied into a chroot.
469

470
        Example BUILD file:
471

472
            run_shell_command(
473
                command="./scripts/my-script.sh --data-files-dir={chroot}",
474
                execution_dependencies=["src/project/files:data"],
475
            )
476

477
        The `command` may use either `{chroot}` on the command line, or the `$CHROOT`
478
        environment variable to get the root directory for where any dependencies are located.
479

480
        In contrast to the `shell_command`, in addition to `workdir` you only have
481
        the `command` and `execution_dependencies` fields as the `tools` you are going to use are
482
        already on the PATH which is inherited from the Pants environment. Also, the `outputs` does
483
        not apply, as any output files produced will end up directly in your project tree.
484
        """
485
    )
486

487

488
class ShellCommandTestTarget(Target):
3✔
489
    alias = "test_shell_command"
3✔
490

491
    core_fields = (
3✔
492
        *COMMON_TARGET_FIELDS,
493
        ShellCommandTestDependenciesField,
494
        ShellCommandRunnableDependenciesField,
495
        ShellCommandCommandField,
496
        ShellCommandLogOutputField,
497
        ShellCommandSourcesField,
498
        ShellCommandTimeoutField,
499
        ShellCommandToolsField,
500
        ShellCommandExtraEnvVarsField,
501
        ShellCommandPathEnvModifyModeField,
502
        EnvironmentField,
503
        SkipShellCommandTestsField,
504
        ShellCommandWorkdirField,
505
        ShellCommandOutputFilesField,
506
        ShellCommandOutputDirectoriesField,
507
        ShellCommandOutputRootDirField,
508
        ShellCommandOutputsMatchMode,
509
    )
510
    help = help_text(
3✔
511
        """
512
        Run a script as a test via the `test` goal, with all dependencies packaged/copied available in the chroot.
513

514
        Example BUILD file:
515

516
            test_shell_command(
517
                name="test",
518
                tools=["test"],
519
                command="test -r $CHROOT/some-data-file.txt",
520
                execution_dependencies=["src/project/files:data"],
521
            )
522

523
        The `command` may use the `{chroot}` marker on the command line or in environment variables
524
        to get the root directory where any dependencies are materialized during execution.
525

526
        In contrast to the `run_shell_command`, this target is intended to run shell commands as tests
527
        and will only run them via the `test` goal.
528
        """
529
    )
530

531

532
def rules():
3✔
533
    return [
3✔
534
        *collect_rules(),
535
        UnionRule(TargetFilesGeneratorSettingsRequest, ShellGeneratorSettingsRequest),
536
    ]
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