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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

0.0
/src/python/pants/backend/go/util_rules/coverage.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
UNCOV
3
from __future__ import annotations
×
4

UNCOV
5
import enum
×
UNCOV
6
import hashlib
×
UNCOV
7
import os
×
UNCOV
8
from dataclasses import dataclass
×
UNCOV
9
from pathlib import PurePath
×
10

UNCOV
11
import chevron
×
12

UNCOV
13
from pants.backend.go.util_rules.sdk import GoSdkProcess, GoSdkToolIDRequest, compute_go_tool_id
×
UNCOV
14
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
×
UNCOV
15
from pants.build_graph.address import Address
×
UNCOV
16
from pants.core.goals.test import CoverageData
×
UNCOV
17
from pants.engine.fs import CreateDigest, DigestSubset, FileContent, PathGlobs
×
UNCOV
18
from pants.engine.internals.native_engine import Digest, MergeDigests
×
UNCOV
19
from pants.engine.internals.selectors import concurrently
×
UNCOV
20
from pants.engine.intrinsics import create_digest, digest_subset_to_digest, merge_digests
×
UNCOV
21
from pants.engine.process import fallible_to_exec_result_or_raise
×
UNCOV
22
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
23
from pants.util.ordered_set import FrozenOrderedSet
×
24

25

UNCOV
26
@dataclass(frozen=True)
×
UNCOV
27
class GoCoverageData(CoverageData):
×
UNCOV
28
    coverage_digest: Digest
×
UNCOV
29
    import_path: str
×
UNCOV
30
    sources_digest: Digest
×
UNCOV
31
    sources_dir_path: str
×
UNCOV
32
    pkg_target_address: Address
×
33

34

UNCOV
35
class GoCoverMode(enum.Enum):
×
UNCOV
36
    SET = "set"
×
UNCOV
37
    COUNT = "count"
×
UNCOV
38
    ATOMIC = "atomic"
×
39

40

UNCOV
41
@dataclass(frozen=True)
×
UNCOV
42
class GoCoverageConfig:
×
43
    # How to count the code usage.
UNCOV
44
    cover_mode: GoCoverMode
×
45

46
    # Import path patterns for packages which should be instrumented for code coverage.
UNCOV
47
    import_path_include_patterns: tuple[str, ...] = ()
×
48

49

UNCOV
50
@dataclass(frozen=True)
×
UNCOV
51
class ApplyCodeCoverageRequest:
×
52
    """Apply code coverage to a package using `go tool cover`."""
53

UNCOV
54
    digest: Digest
×
UNCOV
55
    dir_path: str
×
UNCOV
56
    go_files: tuple[str, ...]
×
UNCOV
57
    cgo_files: tuple[str, ...]
×
UNCOV
58
    cover_mode: GoCoverMode
×
UNCOV
59
    import_path: str
×
60

61

UNCOV
62
@dataclass(frozen=True)
×
UNCOV
63
class FileCodeCoverageMetadata:
×
64
    """Metadata for code coverage applied to a single Go file."""
65

UNCOV
66
    file_id: str
×
UNCOV
67
    go_file: str
×
UNCOV
68
    cover_go_file: str
×
UNCOV
69
    cover_var: str
×
70

71

UNCOV
72
@dataclass(frozen=True)
×
UNCOV
73
class BuiltGoPackageCodeCoverageMetadata:
×
UNCOV
74
    import_path: str
×
UNCOV
75
    cover_file_metadatas: tuple[FileCodeCoverageMetadata, ...]
×
UNCOV
76
    sources_digest: Digest
×
UNCOV
77
    sources_dir_path: str
×
78

79

UNCOV
80
@dataclass(frozen=True)
×
UNCOV
81
class ApplyCodeCoverageResult:
×
UNCOV
82
    digest: Digest
×
UNCOV
83
    cover_file_metadatas: tuple[FileCodeCoverageMetadata, ...]
×
UNCOV
84
    go_files: tuple[str, ...]
×
UNCOV
85
    cgo_files: tuple[str, ...]
×
86

87

UNCOV
88
@dataclass(frozen=True)
×
UNCOV
89
class ApplyCodeCoverageToFileRequest:
×
UNCOV
90
    digest: Digest
×
UNCOV
91
    go_file: str
×
UNCOV
92
    cover_go_file: str
×
UNCOV
93
    mode: GoCoverMode
×
UNCOV
94
    cover_var: str
×
95

96

UNCOV
97
@dataclass(frozen=True)
×
UNCOV
98
class ApplyCodeCoverageToFileResult:
×
UNCOV
99
    digest: Digest
×
UNCOV
100
    cover_go_file: str
×
101

102

UNCOV
103
@rule
×
UNCOV
104
async def go_apply_code_coverage_to_file(
×
105
    request: ApplyCodeCoverageToFileRequest,
106
) -> ApplyCodeCoverageToFileResult:
107
    cover_tool_id = await compute_go_tool_id(GoSdkToolIDRequest("cover"))
×
108

109
    result = await fallible_to_exec_result_or_raise(
×
110
        **implicitly(
111
            GoSdkProcess(
112
                input_digest=request.digest,
113
                command=[
114
                    "tool",
115
                    "cover",
116
                    "-mode",
117
                    request.mode.value,
118
                    "-var",
119
                    request.cover_var,
120
                    "-o",
121
                    request.cover_go_file,
122
                    request.go_file,
123
                ],
124
                description=f"Apply Go coverage to: {request.go_file}",
125
                output_files=(str(request.cover_go_file),),
126
                env={"__PANTS_GO_COVER_TOOL_ID": cover_tool_id.tool_id},
127
            )
128
        )
129
    )
130

131
    return ApplyCodeCoverageToFileResult(
×
132
        digest=result.output_digest,
133
        cover_go_file=request.cover_go_file,
134
    )
135

136

UNCOV
137
def _hash_string(s: str) -> str:
×
138
    h = hashlib.sha256(s.encode())
×
139
    return h.hexdigest()[:12]
×
140

141

UNCOV
142
def _is_test_file(s: str) -> bool:
×
143
    return s.endswith("_test.go")
×
144

145

UNCOV
146
@rule
×
UNCOV
147
async def go_apply_code_coverage(request: ApplyCodeCoverageRequest) -> ApplyCodeCoverageResult:
×
148
    # Setup metadata for each file to which code coverage will be applied by assigning the name of the exported
149
    # variable which holds coverage counters for each file.
150
    file_metadatas: list[FileCodeCoverageMetadata] = []
×
151
    output_go_files = []
×
152
    output_cgo_files = []
×
153
    import_path_hash = _hash_string(request.import_path)
×
154
    for i, go_file in enumerate(request.go_files + request.cgo_files):
×
155
        if _is_test_file(go_file):
×
156
            if i < len(request.go_files):
×
157
                output_go_files.append(go_file)
×
158
            else:
159
                output_cgo_files.append(go_file)
×
160
            continue
×
161

162
        p = PurePath(go_file)
×
163
        cover_go_file = str(p.with_name(f"{p.stem}.cover.go"))
×
164
        file_metadatas.append(
×
165
            FileCodeCoverageMetadata(
166
                file_id=f"{request.import_path}/{go_file}",
167
                go_file=go_file,
168
                cover_go_file=cover_go_file,
169
                cover_var=f"GoCover_{import_path_hash}_{i}",
170
            )
171
        )
172
        if i < len(request.go_files):
×
173
            output_go_files.append(cover_go_file)
×
174
        else:
175
            output_cgo_files.append(cover_go_file)
×
176

177
    subsetted_digests = await concurrently(
×
178
        digest_subset_to_digest(
179
            DigestSubset(
180
                request.digest,
181
                PathGlobs(
182
                    [os.path.join(request.dir_path, file_metadata.go_file)],
183
                    glob_match_error_behavior=GlobMatchErrorBehavior.error,
184
                    description_of_origin="coverage",
185
                ),
186
            )
187
        )
188
        for file_metadata in file_metadatas
189
    )
190

191
    # Apply code coverage codegen to each file that will be analyzed.
192
    cover_results = await concurrently(
×
193
        go_apply_code_coverage_to_file(
194
            ApplyCodeCoverageToFileRequest(
195
                digest=go_file_digest,
196
                go_file=os.path.join(request.dir_path, file_metadata.go_file),
197
                cover_go_file=os.path.join(request.dir_path, file_metadata.cover_go_file),
198
                mode=request.cover_mode,
199
                cover_var=file_metadata.cover_var,
200
            )
201
        )
202
        for file_metadata, go_file_digest in zip(file_metadatas, subsetted_digests)
203
    )
204

205
    # Merge the coverage codegen back into the original digest so that non-covered and covered sources are in
206
    # the same digest.
207
    digest = await merge_digests(MergeDigests([request.digest, *(r.digest for r in cover_results)]))
×
208

209
    return ApplyCodeCoverageResult(
×
210
        digest=digest,
211
        cover_file_metadatas=tuple(file_metadatas),
212
        go_files=tuple(output_go_files),
213
        cgo_files=tuple(output_cgo_files),
214
    )
215

216

UNCOV
217
@dataclass(frozen=True)
×
UNCOV
218
class GenerateCoverageSetupCodeRequest:
×
UNCOV
219
    packages: FrozenOrderedSet[BuiltGoPackageCodeCoverageMetadata]
×
UNCOV
220
    cover_mode: GoCoverMode
×
221

222

UNCOV
223
@dataclass(frozen=True)
×
UNCOV
224
class GenerateCoverageSetupCodeResult:
×
UNCOV
225
    PATH = "pants_cover_setup.go"
×
UNCOV
226
    digest: Digest
×
227

228

UNCOV
229
COVERAGE_SETUP_CODE = """\
×
230
package main
231

232
import (
233
    "testing"
234

235
{{#imports}}
236
    _cover{{i}} "{{import_path}}"
237
{{/imports}}
238
)
239

240
var (
241
    coverCounters = make(map[string][]uint32)
242
    coverBlocks = make(map[string][]testing.CoverBlock)
243
)
244

245
func coverRegisterFile(fileName string, counter []uint32, pos []uint32, numStmts []uint16) {
246
    if 3*len(counter) != len(pos) || len(counter) != len(numStmts) {
247
        panic("coverage: mismatched sizes")
248
    }
249
    if coverCounters[fileName] != nil {
250
        // Already registered.
251
        return
252
    }
253
    coverCounters[fileName] = counter
254
    block := make([]testing.CoverBlock, len(counter))
255
    for i := range counter {
256
        block[i] = testing.CoverBlock{
257
            Line0: pos[3*i+0],
258
            Col0: uint16(pos[3*i+2]),
259
            Line1: pos[3*i+1],
260
            Col1: uint16(pos[3*i+2]>>16),
261
            Stmts: numStmts[i],
262
        }
263
    }
264
    coverBlocks[fileName] = block
265
}
266

267
func init() {
268
{{#registrations}}
269
    coverRegisterFile("{{file_id}}", _cover{{i}}.{{cover_var}}.Count[:], _cover{{i}}.{{cover_var}}.Pos[:], _cover{{i}}.{{cover_var}}.NumStmt[:])
270
{{/registrations}}
271
}
272

273
func registerCover() {
274
    testing.RegisterCover(testing.Cover{
275
        Mode: "{{cover_mode}}",
276
        Counters: coverCounters,
277
        Blocks: coverBlocks,
278
        CoveredPackages: "",
279
    })
280
}
281
"""
282

283

UNCOV
284
@rule
×
UNCOV
285
async def generate_go_coverage_setup_code(
×
286
    request: GenerateCoverageSetupCodeRequest,
287
) -> GenerateCoverageSetupCodeResult:
288
    content = chevron.render(
×
289
        template=COVERAGE_SETUP_CODE,
290
        data={
291
            "imports": [
292
                {"i": i, "import_path": pkg.import_path} for i, pkg in enumerate(request.packages)
293
            ],
294
            "registrations": [
295
                {
296
                    "i": i,
297
                    "file_id": m.file_id,
298
                    "cover_var": m.cover_var,
299
                }
300
                for i, pkg in enumerate(request.packages)
301
                for m in pkg.cover_file_metadatas
302
            ],
303
            "cover_mode": request.cover_mode.value,
304
        },
305
    )
306

307
    digest = await create_digest(
×
308
        CreateDigest([FileContent(GenerateCoverageSetupCodeResult.PATH, content.encode())])
309
    )
310
    return GenerateCoverageSetupCodeResult(digest=digest)
×
311

312

UNCOV
313
def rules():
×
UNCOV
314
    return collect_rules()
×
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

© 2025 Coveralls, Inc