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

pantsbuild / pants / 24055979590

06 Apr 2026 11:17PM UTC coverage: 52.37% (-40.5%) from 92.908%
24055979590

Pull #23225

github

web-flow
Merge 67474653c into 542ca048d
Pull Request #23225: Add --test-show-all-batch-targets to expose all targets in batched pytest

6 of 17 new or added lines in 2 files covered. (35.29%)

23030 existing lines in 605 files now uncovered.

31643 of 60422 relevant lines covered (52.37%)

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

UNCOV
4
from __future__ import annotations
×
5

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

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

UNCOV
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.
UNCOV
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

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

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

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

145

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

150

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

156

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

162

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

171

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

179

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

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

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

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

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

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

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

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

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

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

234

UNCOV
235
def _lift_build_requests_with_coverage(
×
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

UNCOV
256
@rule(desc="Prepare Go test binary", level=LogLevel.DEBUG)
×
UNCOV
257
async def prepare_go_test_binary(
×
258
    request: PrepareGoTestBinaryRequest,
259
    analyzer: PackageAnalyzerSetup,
260
) -> FalliblePrepareGoTestBinaryResult:
UNCOV
261
    go_mod_addr = await find_owning_go_mod(
×
262
        OwningGoModRequest(request.field_set.address), **implicitly()
263
    )
UNCOV
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

UNCOV
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

UNCOV
284
    def compilation_failure(
×
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

UNCOV
294
    if maybe_pkg_analysis.analysis is None:
×
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)
×
UNCOV
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

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

UNCOV
305
    with_coverage = False
×
UNCOV
306
    if request.coverage is not None:
×
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

UNCOV
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

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

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

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

UNCOV
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
    )
UNCOV
360
    testmain_analysis_json = json.loads(testmain_analysis.stdout.decode())
×
361

UNCOV
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

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

UNCOV
375
        if dep_import_path in stdlib_packages:
×
UNCOV
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
            )
UNCOV
385
            continue
×
386

UNCOV
387
        candidate_packages = package_mapping.mapping.get(dep_import_path)
×
UNCOV
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:
UNCOV
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

UNCOV
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

UNCOV
421
    testmain_import_build_requests: list[BuildGoPackageRequest] = []
×
UNCOV
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

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

432
    # Construct the build request for the package under test.
UNCOV
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
    )
UNCOV
442
    if maybe_test_pkg_build_request.request is None:
×
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
        )
UNCOV
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.
UNCOV
451
    main_direct_deps = [test_pkg_build_request, *testmain_import_build_requests]
×
UNCOV
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.
UNCOV
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
        )
UNCOV
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
            )
UNCOV
469
        xtest_pkg_build_request = maybe_xtest_pkg_build_request.request
×
UNCOV
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.
UNCOV
478
    coverage_setup_digest = EMPTY_DIGEST
×
UNCOV
479
    coverage_setup_files = []
×
UNCOV
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.
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

UNCOV
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.
UNCOV
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
    )
UNCOV
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
        )
UNCOV
529
    built_main_pkg = maybe_built_main_pkg.output
×
530

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

UNCOV
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

UNCOV
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

UNCOV
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

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

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

UNCOV
579
        for go_name, pants_name in _PROFILE_OPTIONS.items():
×
UNCOV
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

UNCOV
589
@rule(desc="Test with Go", level=LogLevel.DEBUG)
×
UNCOV
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:
UNCOV
597
    field_set = batch.single_element
×
598

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

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

UNCOV
610
    if fallible_test_binary.exit_code != 0:
×
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

UNCOV
622
    test_binary = fallible_test_binary.binary
×
UNCOV
623
    if test_binary is None:
×
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.
UNCOV
629
    working_dir = field_set.address.spec_path
×
UNCOV
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
    )
UNCOV
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
    )
UNCOV
644
    test_input_digest = await merge_digests(
×
645
        MergeDigests((binary_with_prefix, files_sources.snapshot.digest))
646
    )
647

UNCOV
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
UNCOV
657
    goroot_bin_path = os.path.join(goroot.path, "bin")
×
UNCOV
658
    if "PATH" in extra_env:
×
659
        extra_env["PATH"] = f"{goroot_bin_path}:{extra_env['PATH']}"
×
660
    else:
UNCOV
661
        extra_env["PATH"] = goroot_bin_path
×
662

UNCOV
663
    cache_scope = test_subsystem.default_process_cache_scope
×
664

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

UNCOV
670
    _ensure_no_profile_options(test_flags)
×
671

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

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

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

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

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

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

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

UNCOV
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
    )
UNCOV
718
    results = await execute_process_with_retry(
×
719
        ProcessWithRetries(go_test_process, test_subsystem.attempts_default)
720
    )
721

UNCOV
722
    coverage_data: GoCoverageData | None = None
×
UNCOV
723
    if test_subsystem.use_coverage:
×
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

UNCOV
732
    output_files = [x for x in output_files if x != "cover.out"]
×
UNCOV
733
    extra_output: Snapshot | None = None
×
UNCOV
734
    if output_files or output_test_binary:
×
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

UNCOV
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

UNCOV
752
def rules():
×
UNCOV
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