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

pantsbuild / pants / 23838324561

01 Apr 2026 07:59AM UTC coverage: 60.966% (-31.9%) from 92.907%
23838324561

Pull #23204

github

web-flow
Merge fd6066932 into 0c78ceb96
Pull Request #23204: Port ScalarField, AsyncFieldMixin and friends to rust

8 of 12 new or added lines in 2 files covered. (66.67%)

19216 existing lines in 560 files now uncovered.

39119 of 64165 relevant lines covered (60.97%)

0.96 hits per line

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

70.1
/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
1✔
5

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

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

95
logger = logging.getLogger(__name__)
1✔
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 = {
1✔
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)
1✔
133
class GoTestFieldSet(TestFieldSet):
1✔
134
    required_fields = (GoPackageSourcesField,)
1✔
135

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

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

145

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

150

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

156

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

162

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

171

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

179

180
def transform_test_args(args: Sequence[str], timeout_field_value: int | None) -> tuple[str, ...]:
1✔
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✔
UNCOV
192
            result.append(arg)
×
UNCOV
193
            next_arg_is_option_value = False
×
UNCOV
194
            continue
×
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✔
UNCOV
203
            result.append(arg)
×
UNCOV
204
            break
×
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✔
UNCOV
217
                timeout_is_set = True
×
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✔
UNCOV
224
                next_arg_is_option_value = True
×
225
        else:
UNCOV
226
            result.append(arg)
×
227

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

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

234

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

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

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

UNCOV
253
    return result
×
254

255

256
@rule(desc="Prepare Go test binary", level=LogLevel.DEBUG)
1✔
257
async def prepare_go_test_binary(
1✔
258
    request: PrepareGoTestBinaryRequest,
259
    analyzer: PackageAnalyzerSetup,
260
) -> FalliblePrepareGoTestBinaryResult:
261
    go_mod_addr = await find_owning_go_mod(
1✔
262
        OwningGoModRequest(request.field_set.address), **implicitly()
263
    )
264
    package_mapping, build_opts = await concurrently(
1✔
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(
1✔
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(
1✔
285
        exit_code: int, stdout: str | None, stderr: str | None
286
    ) -> FalliblePrepareGoTestBinaryResult:
UNCOV
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:
1✔
UNCOV
295
        assert maybe_pkg_analysis.stderr is not None
×
UNCOV
296
        return compilation_failure(maybe_pkg_analysis.exit_code, None, maybe_pkg_analysis.stderr)
×
297
    if maybe_pkg_digest.pkg_digest is None:
1✔
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
1✔
302
    pkg_digest = maybe_pkg_digest.pkg_digest
1✔
303
    import_path = pkg_analysis.import_path
1✔
304

305
    with_coverage = False
1✔
306
    if request.coverage is not None:
1✔
UNCOV
307
        with_coverage = True
×
UNCOV
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(
1✔
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:
1✔
UNCOV
332
        _exit_code, _stderr = testmain.failed_exit_code_and_stderr
×
UNCOV
333
        return compilation_failure(_exit_code, None, _stderr)
×
334

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

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

347
    testmain_analysis = await execute_process_or_raise(
1✔
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())
1✔
361

362
    stdlib_packages = await analyze_go_stdlib_packages(
1✔
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()
1✔
370
    stdlib_build_request_gets = []
1✔
371
    for dep_import_path in testmain_analysis_json.get("Imports", []):
1✔
372
        if dep_import_path == import_path:
1✔
373
            continue  # test pkg dep added manually later
1✔
374

375
        if dep_import_path in stdlib_packages:
1✔
376
            stdlib_build_request_gets.append(
1✔
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
1✔
386

387
        candidate_packages = package_mapping.mapping.get(dep_import_path)
1✔
388
        if candidate_packages:
1✔
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(
1✔
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(
1✔
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] = []
1✔
422
    for build_request in fallible_testmain_import_build_requests:
1✔
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)
1✔
428
    for build_request in stdlib_build_requests:
1✔
429
        assert build_request.request is not None
1✔
430
        testmain_import_build_requests.append(build_request.request)
1✔
431

432
    # Construct the build request for the package under test.
433
    maybe_test_pkg_build_request = await setup_build_go_package_target_request(
1✔
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:
1✔
UNCOV
443
        assert maybe_test_pkg_build_request.stderr is not None
×
UNCOV
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
1✔
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]
1✔
452
    if testmain.has_xtests:
1✔
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(
1✔
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:
1✔
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
1✔
470
        main_direct_deps.append(xtest_pkg_build_request)
1✔
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
1✔
479
    coverage_setup_files = []
1✔
480
    if with_coverage:
1✔
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.
UNCOV
484
        coverage_transitive_deps = _lift_build_requests_with_coverage(main_direct_deps)
×
UNCOV
485
        coverage_transitive_deps.sort(key=lambda build_req: build_req.import_path)
×
UNCOV
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.
UNCOV
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
        )
UNCOV
493
        coverage_metadata = [
×
494
            pkg.coverage_metadata for pkg in built_main_direct_deps if pkg.coverage_metadata
495
        ]
UNCOV
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
        )
UNCOV
502
        coverage_setup_digest = coverage_setup_result.digest
×
UNCOV
503
        coverage_setup_files = [GenerateCoverageSetupCodeResult.PATH]
×
504

505
    testmain_input_digest = await merge_digests(
1✔
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(
1✔
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:
1✔
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
1✔
530

531
    main_pkg_a_file_path = built_main_pkg.import_paths_to_pkg_a_files["main"]
1✔
532

533
    binary = await link_go_binary(
1✔
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(
1✔
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] = {
1✔
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:
1✔
570
    for arg in args:
1✔
571
        # Non-arguments stop option processing.
572
        if arg[0] != "-":
1✔
573
            break
×
574

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

579
        for go_name, pants_name in _PROFILE_OPTIONS.items():
1✔
580
            if arg == f"-test.{go_name}" or arg.startswith(f"-test.{go_name}="):
1✔
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)
1✔
590
async def run_go_tests(
1✔
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
1✔
598

599
    coverage: PrepareGoTestBinaryCoverageConfig | None = None
1✔
600
    if test_subsystem.use_coverage:
1✔
UNCOV
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(
1✔
607
        PrepareGoTestBinaryRequest(field_set=field_set, coverage=coverage), **implicitly()
608
    )
609

610
    if fallible_test_binary.exit_code != 0:
1✔
UNCOV
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
1✔
623
    if test_binary is None:
1✔
UNCOV
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
1✔
630
    field_set_extra_env, dependencies, binary_with_prefix = await concurrently(
1✔
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(
1✔
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(
1✔
645
        MergeDigests((binary_with_prefix, files_sources.snapshot.digest))
646
    )
647

648
    extra_env = {
1✔
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")
1✔
658
    if "PATH" in extra_env:
1✔
659
        extra_env["PATH"] = f"{goroot_bin_path}:{extra_env['PATH']}"
×
660
    else:
661
        extra_env["PATH"] = goroot_bin_path
1✔
662

663
    cache_scope = test_subsystem.default_process_cache_scope
1✔
664

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

670
    _ensure_no_profile_options(test_flags)
1✔
671

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

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

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

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

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

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

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

704
    go_test_process = Process(
1✔
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(
1✔
719
        ProcessWithRetries(go_test_process, test_subsystem.attempts_default)
720
    )
721

722
    coverage_data: GoCoverageData | None = None
1✔
723
    if test_subsystem.use_coverage:
1✔
UNCOV
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"]
1✔
733
    extra_output: Snapshot | None = None
1✔
734
    if output_files or output_test_binary:
1✔
UNCOV
735
        output_digest = results.last.output_digest
×
UNCOV
736
        if output_test_binary:
×
UNCOV
737
            output_digest = await merge_digests(
×
738
                MergeDigests([output_digest, test_binary.test_binary_digest])
739
            )
UNCOV
740
        extra_output = await digest_to_snapshot(output_digest)
×
741

742
    return TestResult.from_fallible_process_result(
1✔
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():
1✔
753
    return [
1✔
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