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

pantsbuild / pants / 23177125175

17 Mar 2026 03:32AM UTC coverage: 52.677% (-40.3%) from 92.932%
23177125175

Pull #23177

github

web-flow
Merge 1824dfbf4 into 0b9fdfb0e
Pull Request #23177: Bump the gha-deps group across 1 directory with 4 updates

31687 of 60153 relevant lines covered (52.68%)

1.05 hits per line

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

0.0
/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

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

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

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

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

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

145

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

150

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

156

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

162

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

171

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

179

180
def transform_test_args(args: Sequence[str], timeout_field_value: int | None) -> tuple[str, ...]:
×
181
    result = []
×
182
    i = 0
×
183
    next_arg_is_option_value = False
×
184
    timeout_is_set = False
×
185
    while i < len(args):
×
186
        arg = args[i]
×
187
        i += 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:
×
192
            result.append(arg)
×
193
            next_arg_is_option_value = False
×
194
            continue
×
195

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

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

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

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

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

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

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

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

234

235
def _lift_build_requests_with_coverage(
×
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)
×
257
async def prepare_go_test_binary(
×
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] = {
×
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:
×
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)
×
590
async def run_go_tests(
×
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

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():
×
753
    return [
×
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