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

pantsbuild / pants / 21919838070

11 Feb 2026 07:27PM UTC coverage: 80.351% (+0.001%) from 80.35%
21919838070

Pull #23096

github

web-flow
Merge 9f45c9e39 into 9a67b81d3
Pull Request #23096: partially DRY out cache scope for test runners

8 of 15 new or added lines in 7 files covered. (53.33%)

1 existing line in 1 file now uncovered.

78767 of 98029 relevant lines covered (80.35%)

3.36 hits per line

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

40.89
/src/python/pants/backend/go/goals/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
5✔
5

6
import dataclasses
5✔
7
import json
5✔
8
import logging
5✔
9
import os
5✔
10
from collections import deque
5✔
11
from collections.abc import Iterable, Sequence
5✔
12
from dataclasses import dataclass
5✔
13
from typing import Any
5✔
14

15
from pants.backend.go.subsystems.gotest import GoTestSubsystem
5✔
16
from pants.backend.go.target_type_rules import (
5✔
17
    GoImportPathMappingRequest,
18
    map_import_paths_to_packages,
19
)
20
from pants.backend.go.target_types import (
5✔
21
    GoPackageSourcesField,
22
    GoTestExtraEnvVarsField,
23
    GoTestTimeoutField,
24
    SkipGoTestsField,
25
)
26
from pants.backend.go.util_rules.build_opts import (
5✔
27
    GoBuildOptionsFromTargetRequest,
28
    go_extract_build_options_from_target,
29
)
30
from pants.backend.go.util_rules.build_pkg import (
5✔
31
    BuildGoPackageRequest,
32
    build_go_package,
33
    required_built_go_package,
34
)
35
from pants.backend.go.util_rules.build_pkg_target import (
5✔
36
    BuildGoPackageRequestForStdlibRequest,
37
    BuildGoPackageTargetRequest,
38
    setup_build_go_package_target_request,
39
    setup_build_go_package_target_request_for_stdlib,
40
)
41
from pants.backend.go.util_rules.coverage import (
5✔
42
    GenerateCoverageSetupCodeRequest,
43
    GenerateCoverageSetupCodeResult,
44
    GoCoverageConfig,
45
    GoCoverageData,
46
    GoCoverMode,
47
    generate_go_coverage_setup_code,
48
)
49
from pants.backend.go.util_rules.first_party_pkg import (
5✔
50
    FirstPartyPkgAnalysis,
51
    FirstPartyPkgAnalysisRequest,
52
    FirstPartyPkgDigest,
53
    FirstPartyPkgDigestRequest,
54
    analyze_first_party_package,
55
    setup_first_party_pkg_digest,
56
)
57
from pants.backend.go.util_rules.go_mod import OwningGoModRequest, find_owning_go_mod
5✔
58
from pants.backend.go.util_rules.goroot import GoRoot
5✔
59
from pants.backend.go.util_rules.import_analysis import (
5✔
60
    GoStdLibPackagesRequest,
61
    analyze_go_stdlib_packages,
62
)
63
from pants.backend.go.util_rules.link import LinkGoBinaryRequest, link_go_binary
5✔
64
from pants.backend.go.util_rules.pkg_analyzer import PackageAnalyzerSetup
5✔
65
from pants.backend.go.util_rules.tests_analysis import (
5✔
66
    GeneratedTestMain,
67
    GenerateTestMainRequest,
68
    generate_testmain,
69
)
70
from pants.build_graph.address import Address
5✔
71
from pants.core.goals.test import TestExtraEnv, TestFieldSet, TestRequest, TestResult, TestSubsystem
5✔
72
from pants.core.target_types import FileSourceField
5✔
73
from pants.core.util_rules.env_vars import environment_vars_subset
5✔
74
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
5✔
75
from pants.engine.env_vars import EnvironmentVarsRequest
5✔
76
from pants.engine.fs import EMPTY_FILE_DIGEST, AddPrefix, Digest, MergeDigests
5✔
77
from pants.engine.internals.graph import resolve_targets
5✔
78
from pants.engine.internals.native_engine import EMPTY_DIGEST, Snapshot
5✔
79
from pants.engine.intrinsics import (
5✔
80
    add_prefix,
81
    digest_to_snapshot,
82
    execute_process_with_retry,
83
    merge_digests,
84
)
85
from pants.engine.process import (
5✔
86
    Process,
87
    ProcessWithRetries,
88
    execute_process_or_raise,
89
)
90
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
5✔
91
from pants.engine.target import Dependencies, DependenciesRequest, SourcesField, Target
5✔
92
from pants.util.logging import LogLevel
5✔
93
from pants.util.ordered_set import FrozenOrderedSet
5✔
94

95
logger = logging.getLogger(__name__)
5✔
96

97
# Known options to Go test binaries. Only these options will be transformed by `transform_test_args`.
98
# The bool value represents whether the option is expected to take a value or not.
99
# To regenerate this list, run `go run ./gentestflags.go` and copy the output below.
100
TEST_FLAGS = {
5✔
101
    "bench": True,
102
    "benchmem": False,
103
    "benchtime": True,
104
    "blockprofile": True,
105
    "blockprofilerate": True,
106
    "count": True,
107
    "coverprofile": True,
108
    "cpu": True,
109
    "cpuprofile": True,
110
    "failfast": False,
111
    "fullpath": False,
112
    "fuzz": True,
113
    "fuzzminimizetime": True,
114
    "fuzztime": True,
115
    "list": True,
116
    "memprofile": True,
117
    "memprofilerate": True,
118
    "mutexprofile": True,
119
    "mutexprofilefraction": True,
120
    "outputdir": True,
121
    "parallel": True,
122
    "run": True,
123
    "short": False,
124
    "shuffle": True,
125
    "skip": True,
126
    "timeout": True,
127
    "trace": True,
128
    "v": False,
129
}
130

131

132
@dataclass(frozen=True)
5✔
133
class GoTestFieldSet(TestFieldSet):
5✔
134
    required_fields = (GoPackageSourcesField,)
5✔
135

136
    sources: GoPackageSourcesField
5✔
137
    dependencies: Dependencies
5✔
138
    timeout: GoTestTimeoutField
5✔
139
    extra_env_vars: GoTestExtraEnvVarsField
5✔
140

141
    @classmethod
5✔
142
    def opt_out(cls, tgt: Target) -> bool:
5✔
143
        return tgt.get(SkipGoTestsField).value
1✔
144

145

146
class GoTestRequest(TestRequest):
5✔
147
    tool_subsystem = GoTestSubsystem  # type: ignore[assignment]
5✔
148
    field_set_type = GoTestFieldSet
5✔
149

150

151
@dataclass(frozen=True)
5✔
152
class PrepareGoTestBinaryCoverageConfig:
5✔
153
    coverage_mode: GoCoverMode
5✔
154
    coverage_packages: tuple[str, ...]
5✔
155

156

157
@dataclass(frozen=True)
5✔
158
class PrepareGoTestBinaryRequest:
5✔
159
    field_set: GoTestFieldSet
5✔
160
    coverage: PrepareGoTestBinaryCoverageConfig | None
5✔
161

162

163
@dataclass(frozen=True)
5✔
164
class PrepareGoTestBinaryResult:
5✔
165
    test_binary_digest: Digest
5✔
166
    test_binary_path: str
5✔
167
    import_path: str
5✔
168
    pkg_digest: FirstPartyPkgDigest
5✔
169
    pkg_analysis: FirstPartyPkgAnalysis
5✔
170

171

172
@dataclass(frozen=True)
5✔
173
class FalliblePrepareGoTestBinaryResult:
5✔
174
    binary: PrepareGoTestBinaryResult | None
5✔
175
    stdout: str
5✔
176
    stderr: str
5✔
177
    exit_code: int
5✔
178

179

180
def transform_test_args(args: Sequence[str], timeout_field_value: int | None) -> tuple[str, ...]:
5✔
181
    result = []
1✔
182
    i = 0
1✔
183
    next_arg_is_option_value = False
1✔
184
    timeout_is_set = False
1✔
185
    while i < len(args):
1✔
186
        arg = args[i]
1✔
187
        i += 1
1✔
188

189
        # If this argument is an option value, then append it to the result and continue to next
190
        # argument.
191
        if next_arg_is_option_value:
1✔
192
            result.append(arg)
1✔
193
            next_arg_is_option_value = False
1✔
194
            continue
1✔
195

196
        # Non-arguments stop option processing.
197
        if arg[0] != "-":
1✔
198
            result.append(arg)
×
199
            break
×
200

201
        # Stop processing since "-" is a non-argument and "--" is terminator.
202
        if arg == "-" or arg == "--":
1✔
203
            result.append(arg)
1✔
204
            break
1✔
205

206
        start_index = 2 if arg[1] == "-" else 1
1✔
207
        equals_index = arg.find("=", start_index)
1✔
208
        if equals_index != -1:
1✔
209
            arg_name = arg[start_index:equals_index]
1✔
210
            option_value = arg[equals_index:]
1✔
211
        else:
212
            arg_name = arg[start_index:]
1✔
213
            option_value = ""
1✔
214

215
        if arg_name in TEST_FLAGS:
1✔
216
            if arg_name == "timeout":
1✔
217
                timeout_is_set = True
1✔
218

219
            rewritten_arg = f"{arg[0:start_index]}test.{arg_name}{option_value}"
1✔
220
            result.append(rewritten_arg)
1✔
221

222
            no_opt_provided = TEST_FLAGS[arg_name] and option_value == ""
1✔
223
            if no_opt_provided:
1✔
224
                next_arg_is_option_value = True
1✔
225
        else:
226
            result.append(arg)
1✔
227

228
    if not timeout_is_set and timeout_field_value is not None:
1✔
229
        result.append(f"-test.timeout={timeout_field_value}s")
1✔
230

231
    result.extend(args[i:])
1✔
232
    return tuple(result)
1✔
233

234

235
def _lift_build_requests_with_coverage(
5✔
236
    roots: Iterable[BuildGoPackageRequest],
237
) -> list[BuildGoPackageRequest]:
238
    result: list[BuildGoPackageRequest] = []
×
239

240
    queue: deque[BuildGoPackageRequest] = deque()
×
241
    seen: set[BuildGoPackageRequest] = set()
×
242
    queue.extend(roots)
×
243
    seen.update(roots)
×
244

245
    while queue:
×
246
        build_request = queue.popleft()
×
247
        if build_request.with_coverage:
×
248
            result.append(build_request)
×
249
        unseen = [dd for dd in build_request.direct_dependencies if dd not in seen]
×
250
        queue.extend(unseen)
×
251
        seen.update(unseen)
×
252

253
    return result
×
254

255

256
@rule(desc="Prepare Go test binary", level=LogLevel.DEBUG)
5✔
257
async def prepare_go_test_binary(
5✔
258
    request: PrepareGoTestBinaryRequest,
259
    analyzer: PackageAnalyzerSetup,
260
) -> FalliblePrepareGoTestBinaryResult:
261
    go_mod_addr = await find_owning_go_mod(
×
262
        OwningGoModRequest(request.field_set.address), **implicitly()
263
    )
264
    package_mapping, build_opts = await concurrently(
×
265
        map_import_paths_to_packages(
266
            GoImportPathMappingRequest(go_mod_addr.address), **implicitly()
267
        ),
268
        go_extract_build_options_from_target(
269
            GoBuildOptionsFromTargetRequest(request.field_set.address), **implicitly()
270
        ),
271
    )
272

273
    maybe_pkg_analysis, maybe_pkg_digest, dependencies = await concurrently(
×
274
        analyze_first_party_package(
275
            FirstPartyPkgAnalysisRequest(request.field_set.address, build_opts=build_opts),
276
            **implicitly(),
277
        ),
278
        setup_first_party_pkg_digest(
279
            FirstPartyPkgDigestRequest(request.field_set.address, build_opts=build_opts)
280
        ),
281
        resolve_targets(**implicitly(DependenciesRequest(request.field_set.dependencies))),
282
    )
283

284
    def compilation_failure(
×
285
        exit_code: int, stdout: str | None, stderr: str | None
286
    ) -> FalliblePrepareGoTestBinaryResult:
287
        return FalliblePrepareGoTestBinaryResult(
×
288
            binary=None,
289
            stdout=stdout or "",
290
            stderr=stderr or "",
291
            exit_code=exit_code,
292
        )
293

294
    if maybe_pkg_analysis.analysis is None:
×
295
        assert maybe_pkg_analysis.stderr is not None
×
296
        return compilation_failure(maybe_pkg_analysis.exit_code, None, maybe_pkg_analysis.stderr)
×
297
    if maybe_pkg_digest.pkg_digest is None:
×
298
        assert maybe_pkg_digest.stderr is not None
×
299
        return compilation_failure(maybe_pkg_digest.exit_code, None, maybe_pkg_digest.stderr)
×
300

301
    pkg_analysis = maybe_pkg_analysis.analysis
×
302
    pkg_digest = maybe_pkg_digest.pkg_digest
×
303
    import_path = pkg_analysis.import_path
×
304

305
    with_coverage = False
×
306
    if request.coverage is not None:
×
307
        with_coverage = True
×
308
        build_opts = dataclasses.replace(
×
309
            build_opts,
310
            coverage_config=GoCoverageConfig(
311
                cover_mode=request.coverage.coverage_mode,
312
                import_path_include_patterns=request.coverage.coverage_packages,
313
            ),
314
        )
315

316
    testmain = await generate_testmain(
×
317
        GenerateTestMainRequest(
318
            digest=pkg_digest.digest,
319
            test_paths=FrozenOrderedSet(
320
                os.path.join(pkg_analysis.dir_path, name) for name in pkg_analysis.test_go_files
321
            ),
322
            xtest_paths=FrozenOrderedSet(
323
                os.path.join(pkg_analysis.dir_path, name) for name in pkg_analysis.xtest_go_files
324
            ),
325
            import_path=import_path,
326
            register_cover=with_coverage,
327
            address=request.field_set.address,
328
        ),
329
    )
330

331
    if testmain.failed_exit_code_and_stderr is not None:
×
332
        _exit_code, _stderr = testmain.failed_exit_code_and_stderr
×
333
        return compilation_failure(_exit_code, None, _stderr)
×
334

335
    if not testmain.has_tests and not testmain.has_xtests:
×
336
        return FalliblePrepareGoTestBinaryResult(
×
337
            binary=None,
338
            stdout="",
339
            stderr="",
340
            exit_code=0,
341
        )
342

343
    testmain_analysis_input_digest = await merge_digests(
×
344
        MergeDigests([testmain.digest, analyzer.digest])
345
    )
346

347
    testmain_analysis = await execute_process_or_raise(
×
348
        **implicitly(
349
            Process(
350
                (analyzer.path, "."),
351
                input_digest=testmain_analysis_input_digest,
352
                description=f"Determine metadata for testmain for {request.field_set.address}",
353
                level=LogLevel.DEBUG,
354
                env={
355
                    "CGO_ENABLED": "1" if build_opts.cgo_enabled else "0",
356
                },
357
            ),
358
        )
359
    )
360
    testmain_analysis_json = json.loads(testmain_analysis.stdout.decode())
×
361

362
    stdlib_packages = await analyze_go_stdlib_packages(
×
363
        GoStdLibPackagesRequest(
364
            with_race_detector=build_opts.with_race_detector,
365
            cgo_enabled=build_opts.cgo_enabled,
366
        ),
367
    )
368

369
    inferred_dependencies: set[Address] = set()
×
370
    stdlib_build_request_gets = []
×
371
    for dep_import_path in testmain_analysis_json.get("Imports", []):
×
372
        if dep_import_path == import_path:
×
373
            continue  # test pkg dep added manually later
×
374

375
        if dep_import_path in stdlib_packages:
×
376
            stdlib_build_request_gets.append(
×
377
                setup_build_go_package_target_request_for_stdlib(
378
                    BuildGoPackageRequestForStdlibRequest(
379
                        import_path=dep_import_path,
380
                        build_opts=build_opts,
381
                    ),
382
                    **implicitly(),
383
                )
384
            )
385
            continue
×
386

387
        candidate_packages = package_mapping.mapping.get(dep_import_path)
×
388
        if candidate_packages:
×
389
            if candidate_packages.infer_all:
×
390
                inferred_dependencies.update(candidate_packages.addresses)
×
391
            else:
392
                if len(candidate_packages.addresses) > 1:
×
393
                    # TODO(#12761): Use ExplicitlyProvidedDependencies for disambiguation.
394
                    logger.warning(
×
395
                        f"Ambiguous mapping for import path {dep_import_path} on packages at addresses: {candidate_packages}"
396
                    )
397
                elif len(candidate_packages.addresses) == 1:
×
398
                    inferred_dependencies.add(candidate_packages.addresses[0])
×
399
                else:
400
                    logger.debug(
×
401
                        f"Unable to infer dependency for import path '{dep_import_path}' "
402
                        f"in go_package at address '{request.field_set.address}'."
403
                    )
404
        else:
405
            logger.debug(
×
406
                f"Unable to infer dependency for import path '{dep_import_path}' "
407
                f"in go_package at address '{request.field_set.address}'."
408
            )
409

410
    fallible_testmain_import_build_requests = await concurrently(
×
411
        setup_build_go_package_target_request(
412
            BuildGoPackageTargetRequest(
413
                address=address,
414
                build_opts=build_opts,
415
            ),
416
            **implicitly(),
417
        )
418
        for address in sorted(inferred_dependencies)
419
    )
420

421
    testmain_import_build_requests: list[BuildGoPackageRequest] = []
×
422
    for build_request in fallible_testmain_import_build_requests:
×
423
        if build_request.request is None:
×
424
            return compilation_failure(build_request.exit_code, None, build_request.stderr)
×
425
        testmain_import_build_requests.append(build_request.request)
×
426

427
    stdlib_build_requests = await concurrently(stdlib_build_request_gets)
×
428
    for build_request in stdlib_build_requests:
×
429
        assert build_request.request is not None
×
430
        testmain_import_build_requests.append(build_request.request)
×
431

432
    # Construct the build request for the package under test.
433
    maybe_test_pkg_build_request = await setup_build_go_package_target_request(
×
434
        BuildGoPackageTargetRequest(
435
            request.field_set.address,
436
            for_tests=True,
437
            with_coverage=with_coverage,
438
            build_opts=build_opts,
439
        ),
440
        **implicitly(),
441
    )
442
    if maybe_test_pkg_build_request.request is None:
×
443
        assert maybe_test_pkg_build_request.stderr is not None
×
444
        return compilation_failure(
×
445
            maybe_test_pkg_build_request.exit_code, None, maybe_test_pkg_build_request.stderr
446
        )
447
    test_pkg_build_request = maybe_test_pkg_build_request.request
×
448

449
    # Determine the direct dependencies of the generated main package. The test package itself is always a
450
    # dependency. Add the xtests package as well if any xtests exist.
451
    main_direct_deps = [test_pkg_build_request, *testmain_import_build_requests]
×
452
    if testmain.has_xtests:
×
453
        # Build a synthetic package for xtests where the import path is the same as the package under test
454
        # but with "_test" appended.
455
        maybe_xtest_pkg_build_request = await setup_build_go_package_target_request(
×
456
            BuildGoPackageTargetRequest(
457
                request.field_set.address,
458
                for_xtests=True,
459
                with_coverage=with_coverage,
460
                build_opts=build_opts,
461
            ),
462
            **implicitly(),
463
        )
464
        if maybe_xtest_pkg_build_request.request is None:
×
465
            assert maybe_xtest_pkg_build_request.stderr is not None
×
466
            return compilation_failure(
×
467
                maybe_xtest_pkg_build_request.exit_code, None, maybe_xtest_pkg_build_request.stderr
468
            )
469
        xtest_pkg_build_request = maybe_xtest_pkg_build_request.request
×
470
        main_direct_deps.append(xtest_pkg_build_request)
×
471

472
    # Generate coverage setup code for the test main if coverage is enabled.
473
    #
474
    # Note: Go coverage analysis is a form of codegen. It rewrites the Go source code at issue to include explicit
475
    # references to "coverage variables" which contain the statement counts for coverage analysis. The test main
476
    # generated for a Go test binary has to explicitly reference the coverage variables generated by this codegen and
477
    # register them with the coverage runtime.
478
    coverage_setup_digest = EMPTY_DIGEST
×
479
    coverage_setup_files = []
×
480
    if with_coverage:
×
481
        # Scan the tree of BuildGoPackageRequest's and lift any packages with coverage enabled to be direct
482
        # dependencies of the generated main package. This facilitates registration of the code coverage
483
        # setup functions.
484
        coverage_transitive_deps = _lift_build_requests_with_coverage(main_direct_deps)
×
485
        coverage_transitive_deps.sort(key=lambda build_req: build_req.import_path)
×
486
        main_direct_deps.extend(coverage_transitive_deps)
×
487

488
        # Build the `main_direct_deps` when in coverage mode to obtain the "coverage variables" for those packages.
489
        built_main_direct_deps = await concurrently(
×
490
            required_built_go_package(**implicitly({build_req: BuildGoPackageRequest}))
491
            for build_req in main_direct_deps
492
        )
493
        coverage_metadata = [
×
494
            pkg.coverage_metadata for pkg in built_main_direct_deps if pkg.coverage_metadata
495
        ]
496
        coverage_setup_result = await generate_go_coverage_setup_code(
×
497
            GenerateCoverageSetupCodeRequest(
498
                packages=FrozenOrderedSet(coverage_metadata),
499
                cover_mode=request.coverage.coverage_mode,  # type: ignore[union-attr] # gated on with_coverage
500
            ),
501
        )
502
        coverage_setup_digest = coverage_setup_result.digest
×
503
        coverage_setup_files = [GenerateCoverageSetupCodeResult.PATH]
×
504

505
    testmain_input_digest = await merge_digests(
×
506
        MergeDigests([testmain.digest, coverage_setup_digest])
507
    )
508

509
    # Generate the synthetic main package which imports the test and/or xtest packages.
510
    maybe_built_main_pkg = await build_go_package(
×
511
        BuildGoPackageRequest(
512
            import_path="main",
513
            pkg_name="main",
514
            digest=testmain_input_digest,
515
            dir_path="",
516
            build_opts=build_opts,
517
            go_files=(GeneratedTestMain.TEST_MAIN_FILE, *coverage_setup_files),
518
            s_files=(),
519
            direct_dependencies=tuple(main_direct_deps),
520
            minimum_go_version=pkg_analysis.minimum_go_version,
521
        ),
522
        **implicitly(),
523
    )
524
    if maybe_built_main_pkg.output is None:
×
525
        assert maybe_built_main_pkg.stderr is not None
×
526
        return compilation_failure(
×
527
            maybe_built_main_pkg.exit_code, maybe_built_main_pkg.stdout, maybe_built_main_pkg.stderr
528
        )
529
    built_main_pkg = maybe_built_main_pkg.output
×
530

531
    main_pkg_a_file_path = built_main_pkg.import_paths_to_pkg_a_files["main"]
×
532

533
    binary = await link_go_binary(
×
534
        LinkGoBinaryRequest(
535
            input_digest=built_main_pkg.digest,
536
            archives=(main_pkg_a_file_path,),
537
            build_opts=build_opts,
538
            import_paths_to_pkg_a_files=built_main_pkg.import_paths_to_pkg_a_files,
539
            output_filename="./test_runner",  # TODO: Name test binary the way that `go` does?
540
            description=f"Link Go test binary for {request.field_set.address}",
541
        ),
542
        **implicitly(),
543
    )
544

545
    return FalliblePrepareGoTestBinaryResult(
×
546
        binary=PrepareGoTestBinaryResult(
547
            test_binary_digest=binary.digest,
548
            test_binary_path="./test_runner",
549
            import_path=import_path,
550
            pkg_digest=pkg_digest,
551
            pkg_analysis=pkg_analysis,
552
        ),
553
        stdout="",
554
        stderr="",
555
        exit_code=0,
556
    )
557

558

559
_PROFILE_OPTIONS: dict[str, str] = {
5✔
560
    "blockprofile": "--go-test-block-profile",
561
    "coverprofile": "--test-use-coverage",
562
    "cpuprofie": "--go-test-cpu-profile",
563
    "memprofile": "--go-test-mem-profile",
564
    "mutexprofile": "--go-test-mutex-profile",
565
    "trace": "--go-test-trace",
566
}
567

568

569
def _ensure_no_profile_options(args: Sequence[str]) -> None:
5✔
570
    for arg in args:
×
571
        # Non-arguments stop option processing.
572
        if arg[0] != "-":
×
573
            break
×
574

575
        # Stop processing since "-" is a non-argument and "--" is terminator.
576
        if arg == "-" or arg == "--":
×
577
            break
×
578

579
        for go_name, pants_name in _PROFILE_OPTIONS.items():
×
580
            if arg == f"-test.{go_name}" or arg.startswith(f"-test.{go_name}="):
×
581
                raise ValueError(
×
582
                    f"The `[go-test].args` option contains the Go test option `-{go_name}`. "
583
                    "This is not supported because Pants needs to manage that option in order to know to "
584
                    "extract the applicable output file from the execution sandbox. "
585
                    f"Please use the Pants `{pants_name}` option instead."
586
                )
587

588

589
@rule(desc="Test with Go", level=LogLevel.DEBUG)
5✔
590
async def run_go_tests(
5✔
591
    batch: GoTestRequest.Batch[GoTestFieldSet, Any],
592
    test_subsystem: TestSubsystem,
593
    go_test_subsystem: GoTestSubsystem,
594
    test_extra_env: TestExtraEnv,
595
    goroot: GoRoot,
596
) -> TestResult:
597
    field_set = batch.single_element
×
598

599
    coverage: PrepareGoTestBinaryCoverageConfig | None = None
×
600
    if test_subsystem.use_coverage:
×
601
        coverage = PrepareGoTestBinaryCoverageConfig(
×
602
            coverage_mode=go_test_subsystem.coverage_mode,
603
            coverage_packages=go_test_subsystem.coverage_packages,
604
        )
605

606
    fallible_test_binary = await prepare_go_test_binary(
×
607
        PrepareGoTestBinaryRequest(field_set=field_set, coverage=coverage), **implicitly()
608
    )
609

610
    if fallible_test_binary.exit_code != 0:
×
611
        return TestResult(
×
612
            exit_code=fallible_test_binary.exit_code,
613
            stdout_bytes=fallible_test_binary.stdout.encode(),
614
            stderr_bytes=fallible_test_binary.stderr.encode(),
615
            stdout_digest=EMPTY_FILE_DIGEST,
616
            stderr_digest=EMPTY_FILE_DIGEST,
617
            addresses=(field_set.address,),
618
            output_setting=test_subsystem.output,
619
            result_metadata=None,
620
        )
621

622
    test_binary = fallible_test_binary.binary
×
623
    if test_binary is None:
×
624
        return TestResult.no_tests_found(field_set.address, output_setting=test_subsystem.output)
×
625

626
    # To emulate Go's test runner, we set the working directory to the path of the `go_package`.
627
    # This allows tests to open dependencies on `file` targets regardless of where they are
628
    # located. See https://dave.cheney.net/2016/05/10/test-fixtures-in-go.
629
    working_dir = field_set.address.spec_path
×
630
    field_set_extra_env, dependencies, binary_with_prefix = await concurrently(
×
631
        environment_vars_subset(
632
            EnvironmentVarsRequest(field_set.extra_env_vars.value or ()), **implicitly()
633
        ),
634
        resolve_targets(**implicitly(DependenciesRequest(field_set.dependencies))),
635
        add_prefix(AddPrefix(test_binary.test_binary_digest, working_dir)),
636
    )
637
    files_sources = await determine_source_files(
×
638
        SourceFilesRequest(
639
            (dep.get(SourcesField) for dep in dependencies),
640
            for_sources_types=(FileSourceField,),
641
            enable_codegen=True,
642
        )
643
    )
644
    test_input_digest = await merge_digests(
×
645
        MergeDigests((binary_with_prefix, files_sources.snapshot.digest))
646
    )
647

648
    extra_env = {
×
649
        **test_extra_env.env,
650
        # NOTE: field_set_extra_env intentionally after `test_extra_env` to allow overriding within
651
        # `go_package`.
652
        **field_set_extra_env,
653
    }
654

655
    # Add $GOROOT/bin to the PATH just as `go test` does.
656
    # See https://github.com/golang/go/blob/master/src/cmd/go/internal/test/test.go#L1384
657
    goroot_bin_path = os.path.join(goroot.path, "bin")
×
658
    if "PATH" in extra_env:
×
659
        extra_env["PATH"] = f"{goroot_bin_path}:{extra_env['PATH']}"
×
660
    else:
661
        extra_env["PATH"] = goroot_bin_path
×
662

NEW
663
    cache_scope = test_subsystem.default_process_cache_scope
×
664

665
    test_flags = transform_test_args(
×
666
        go_test_subsystem.args,
667
        field_set.timeout.calculate_from_global_options(test_subsystem),
668
    )
669

670
    _ensure_no_profile_options(test_flags)
×
671

672
    output_files = []
×
673
    maybe_profile_args = []
×
674
    output_test_binary = go_test_subsystem.output_test_binary
×
675

676
    if test_subsystem.use_coverage:
×
677
        maybe_profile_args.append("-test.coverprofile=cover.out")
×
678
        output_files.append("cover.out")
×
679

680
    if go_test_subsystem.block_profile:
×
681
        maybe_profile_args.append("-test.blockprofile=block.out")
×
682
        output_files.append("block.out")
×
683
        output_test_binary = True
×
684

685
    if go_test_subsystem.cpu_profile:
×
686
        maybe_profile_args.append("-test.cpuprofile=cpu.out")
×
687
        output_files.append("cpu.out")
×
688
        output_test_binary = True
×
689

690
    if go_test_subsystem.mem_profile:
×
691
        maybe_profile_args.append("-test.memprofile=mem.out")
×
692
        output_files.append("mem.out")
×
693
        output_test_binary = True
×
694

695
    if go_test_subsystem.mutex_profile:
×
696
        maybe_profile_args.append("-test.mutexprofile=mutex.out")
×
697
        output_files.append("mutex.out")
×
698
        output_test_binary = True
×
699

700
    if go_test_subsystem.trace:
×
701
        maybe_profile_args.append("-test.trace=trace.out")
×
702
        output_files.append("trace.out")
×
703

704
    go_test_process = Process(
×
705
        argv=(
706
            test_binary.test_binary_path,
707
            *test_flags,
708
            *maybe_profile_args,
709
        ),
710
        env=extra_env,
711
        input_digest=test_input_digest,
712
        description=f"Run Go tests: {field_set.address}",
713
        cache_scope=cache_scope,
714
        working_directory=working_dir,
715
        output_files=output_files,
716
        level=LogLevel.DEBUG,
717
    )
718
    results = await execute_process_with_retry(
×
719
        ProcessWithRetries(go_test_process, test_subsystem.attempts_default)
720
    )
721

722
    coverage_data: GoCoverageData | None = None
×
723
    if test_subsystem.use_coverage:
×
724
        coverage_data = GoCoverageData(
×
725
            coverage_digest=results.last.output_digest,
726
            import_path=test_binary.import_path,
727
            sources_digest=test_binary.pkg_digest.digest,
728
            sources_dir_path=test_binary.pkg_analysis.dir_path,
729
            pkg_target_address=field_set.address,
730
        )
731

732
    output_files = [x for x in output_files if x != "cover.out"]
×
733
    extra_output: Snapshot | None = None
×
734
    if output_files or output_test_binary:
×
735
        output_digest = results.last.output_digest
×
736
        if output_test_binary:
×
737
            output_digest = await merge_digests(
×
738
                MergeDigests([output_digest, test_binary.test_binary_digest])
739
            )
740
        extra_output = await digest_to_snapshot(output_digest)
×
741

742
    return TestResult.from_fallible_process_result(
×
743
        process_results=results.results,
744
        address=field_set.address,
745
        output_setting=test_subsystem.output,
746
        coverage_data=coverage_data,
747
        extra_output=extra_output,
748
        log_extra_output=True,
749
    )
750

751

752
def rules():
5✔
753
    return [
5✔
754
        *collect_rules(),
755
        *GoTestRequest.rules(),
756
    ]
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