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

pantsbuild / pants / 19250292619

11 Nov 2025 12:09AM UTC coverage: 77.865% (-2.4%) from 80.298%
19250292619

push

github

web-flow
flag non-runnable targets used with `code_quality_tool` (#22875)

2 of 5 new or added lines in 2 files covered. (40.0%)

1487 existing lines in 72 files now uncovered.

71448 of 91759 relevant lines covered (77.86%)

3.22 hits per line

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

28.52
/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
4✔
5

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

15
from pants.backend.go.subsystems.gotest import GoTestSubsystem
4✔
16
from pants.backend.go.target_type_rules import (
4✔
17
    GoImportPathMappingRequest,
18
    map_import_paths_to_packages,
19
)
20
from pants.backend.go.target_types import (
4✔
21
    GoPackageSourcesField,
22
    GoTestExtraEnvVarsField,
23
    GoTestTimeoutField,
24
    SkipGoTestsField,
25
)
26
from pants.backend.go.util_rules.build_opts import (
4✔
27
    GoBuildOptionsFromTargetRequest,
28
    go_extract_build_options_from_target,
29
)
30
from pants.backend.go.util_rules.build_pkg import (
4✔
31
    BuildGoPackageRequest,
32
    build_go_package,
33
    required_built_go_package,
34
)
35
from pants.backend.go.util_rules.build_pkg_target import (
4✔
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 (
4✔
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 (
4✔
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
4✔
58
from pants.backend.go.util_rules.goroot import GoRoot
4✔
59
from pants.backend.go.util_rules.import_analysis import (
4✔
60
    GoStdLibPackagesRequest,
61
    analyze_go_stdlib_packages,
62
)
63
from pants.backend.go.util_rules.link import LinkGoBinaryRequest, link_go_binary
4✔
64
from pants.backend.go.util_rules.pkg_analyzer import PackageAnalyzerSetup
4✔
65
from pants.backend.go.util_rules.tests_analysis import (
4✔
66
    GeneratedTestMain,
67
    GenerateTestMainRequest,
68
    generate_testmain,
69
)
70
from pants.build_graph.address import Address
4✔
71
from pants.core.goals.test import TestExtraEnv, TestFieldSet, TestRequest, TestResult, TestSubsystem
4✔
72
from pants.core.target_types import FileSourceField
4✔
73
from pants.core.util_rules.env_vars import environment_vars_subset
4✔
74
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
4✔
75
from pants.engine.env_vars import EnvironmentVarsRequest
4✔
76
from pants.engine.fs import EMPTY_FILE_DIGEST, AddPrefix, Digest, MergeDigests
4✔
77
from pants.engine.internals.graph import resolve_targets
4✔
78
from pants.engine.internals.native_engine import EMPTY_DIGEST, Snapshot
4✔
79
from pants.engine.intrinsics import (
4✔
80
    add_prefix,
81
    digest_to_snapshot,
82
    execute_process_with_retry,
83
    merge_digests,
84
)
85
from pants.engine.process import (
4✔
86
    Process,
87
    ProcessCacheScope,
88
    ProcessWithRetries,
89
    execute_process_or_raise,
90
)
91
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
4✔
92
from pants.engine.target import Dependencies, DependenciesRequest, SourcesField, Target
4✔
93
from pants.util.logging import LogLevel
4✔
94
from pants.util.ordered_set import FrozenOrderedSet
4✔
95

96
logger = logging.getLogger(__name__)
4✔
97

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

132

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

137
    sources: GoPackageSourcesField
4✔
138
    dependencies: Dependencies
4✔
139
    timeout: GoTestTimeoutField
4✔
140
    extra_env_vars: GoTestExtraEnvVarsField
4✔
141

142
    @classmethod
4✔
143
    def opt_out(cls, tgt: Target) -> bool:
4✔
UNCOV
144
        return tgt.get(SkipGoTestsField).value
×
145

146

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

151

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

157

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

163

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

172

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

180

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

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

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

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

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

UNCOV
216
        if arg_name in TEST_FLAGS:
×
UNCOV
217
            if arg_name == "timeout":
×
UNCOV
218
                timeout_is_set = True
×
219

UNCOV
220
            rewritten_arg = f"{arg[0:start_index]}test.{arg_name}{option_value}"
×
UNCOV
221
            result.append(rewritten_arg)
×
222

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

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

UNCOV
232
    result.extend(args[i:])
×
UNCOV
233
    return tuple(result)
×
234

235

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

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

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

254
    return result
×
255

256

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

559

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

569

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

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

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

589

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

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

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

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

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

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

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

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

664
    cache_scope = (
×
665
        ProcessCacheScope.PER_SESSION if test_subsystem.force else ProcessCacheScope.SUCCESSFUL
666
    )
667

668
    test_flags = transform_test_args(
×
669
        go_test_subsystem.args,
670
        field_set.timeout.calculate_from_global_options(test_subsystem),
671
    )
672

673
    _ensure_no_profile_options(test_flags)
×
674

675
    output_files = []
×
676
    maybe_profile_args = []
×
677
    output_test_binary = go_test_subsystem.output_test_binary
×
678

679
    if test_subsystem.use_coverage:
×
680
        maybe_profile_args.append("-test.coverprofile=cover.out")
×
681
        output_files.append("cover.out")
×
682

683
    if go_test_subsystem.block_profile:
×
684
        maybe_profile_args.append("-test.blockprofile=block.out")
×
685
        output_files.append("block.out")
×
686
        output_test_binary = True
×
687

688
    if go_test_subsystem.cpu_profile:
×
689
        maybe_profile_args.append("-test.cpuprofile=cpu.out")
×
690
        output_files.append("cpu.out")
×
691
        output_test_binary = True
×
692

693
    if go_test_subsystem.mem_profile:
×
694
        maybe_profile_args.append("-test.memprofile=mem.out")
×
695
        output_files.append("mem.out")
×
696
        output_test_binary = True
×
697

698
    if go_test_subsystem.mutex_profile:
×
699
        maybe_profile_args.append("-test.mutexprofile=mutex.out")
×
700
        output_files.append("mutex.out")
×
701
        output_test_binary = True
×
702

703
    if go_test_subsystem.trace:
×
704
        maybe_profile_args.append("-test.trace=trace.out")
×
705
        output_files.append("trace.out")
×
706

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

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

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

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

754

755
def rules():
4✔
756
    return [
4✔
757
        *collect_rules(),
758
        *GoTestRequest.rules(),
759
    ]
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