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

pantsbuild / pants / 23173035367

17 Mar 2026 12:47AM UTC coverage: 91.371% (-1.6%) from 92.933%
23173035367

push

github

web-flow
update helm (and friends) to a recent 3.x seies (#23143)

v4 is a major breaking change, so holding off on that. (There was also
just a 3.20, but 3.19 has this reassuring series of bug fixes and was
also quite recent.)

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

1263 existing lines in 73 files now uncovered.

86196 of 94336 relevant lines covered (91.37%)

3.87 hits per line

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

84.85
/src/python/pants/backend/javascript/goals/test.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
from __future__ import annotations
1✔
4

5
import dataclasses
1✔
6
from collections import defaultdict
1✔
7
from collections.abc import Iterable
1✔
8
from dataclasses import dataclass
1✔
9
from pathlib import PurePath
1✔
10

11
from pants.backend.javascript import install_node_package, nodejs_project_environment
1✔
12
from pants.backend.javascript.install_node_package import (
1✔
13
    InstalledNodePackageRequest,
14
    install_node_packages_for_address,
15
)
16
from pants.backend.javascript.nodejs_project_environment import (
1✔
17
    NodeJsProjectEnvironmentProcess,
18
    setup_nodejs_project_environment_process,
19
)
20
from pants.backend.javascript.package_json import (
1✔
21
    NodePackageNameField,
22
    NodePackageTestScriptField,
23
    NodeTestScript,
24
    OwningNodePackageRequest,
25
    find_owning_package,
26
)
27
from pants.backend.javascript.subsystems.nodejstest import NodeJSTest
1✔
28
from pants.backend.javascript.target_types import JSRuntimeSourceField, JSTestRuntimeSourceField
1✔
29
from pants.backend.typescript.target_types import TypeScriptSourceField
1✔
30
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
1✔
31
from pants.build_graph.address import Address
1✔
32
from pants.core.goals.test import (
1✔
33
    CoverageData,
34
    CoverageDataCollection,
35
    CoverageReports,
36
    FilesystemCoverageReport,
37
    TestExtraEnv,
38
    TestExtraEnvVarsField,
39
    TestFieldSet,
40
    TestRequest,
41
    TestResult,
42
    TestsBatchCompatibilityTagField,
43
    TestSubsystem,
44
    TestTimeoutField,
45
)
46
from pants.core.target_types import AssetSourceField
1✔
47
from pants.core.util_rules import source_files
1✔
48
from pants.core.util_rules.distdir import DistDir
1✔
49
from pants.core.util_rules.env_vars import environment_vars_subset
1✔
50
from pants.core.util_rules.partitions import Partition, PartitionerType, Partitions
1✔
51
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
1✔
52
from pants.engine.env_vars import EnvironmentVarsRequest
1✔
53
from pants.engine.fs import DigestSubset, GlobExpansionConjunction
1✔
54
from pants.engine.internals import graph, platform_rules
1✔
55
from pants.engine.internals.graph import transitive_targets
1✔
56
from pants.engine.internals.native_engine import MergeDigests, Snapshot
1✔
57
from pants.engine.internals.selectors import concurrently
1✔
58
from pants.engine.intrinsics import digest_to_snapshot, execute_process_with_retry, merge_digests
1✔
59
from pants.engine.process import ProcessWithRetries
1✔
60
from pants.engine.rules import Rule, collect_rules, implicitly, rule
1✔
61
from pants.engine.target import Dependencies, SourcesField, Target, TransitiveTargetsRequest
1✔
62
from pants.engine.unions import UnionRule
1✔
63
from pants.util.dirutil import fast_relpath
1✔
64
from pants.util.frozendict import FrozenDict
1✔
65
from pants.util.logging import LogLevel
1✔
66
from pants.util.strutil import pluralize
1✔
67

68

69
@dataclass(frozen=True)
1✔
70
class JSCoverageData(CoverageData):
1✔
71
    snapshot: Snapshot
1✔
72
    addresses: tuple[Address, ...]
1✔
73
    output_files: tuple[str, ...]
1✔
74
    output_directories: tuple[str, ...]
1✔
75
    working_directory: str
1✔
76

77

78
class JSCoverageDataCollection(CoverageDataCollection[JSCoverageData]):
1✔
79
    element_type = JSCoverageData
1✔
80

81

82
@dataclass(frozen=True)
1✔
83
class JSTestFieldSet(TestFieldSet):
1✔
84
    required_fields = (JSTestRuntimeSourceField,)
1✔
85

86
    batch_compatibility_tag: TestsBatchCompatibilityTagField
1✔
87
    source: JSTestRuntimeSourceField
1✔
88
    dependencies: Dependencies
1✔
89
    timeout: TestTimeoutField
1✔
90
    extra_env_vars: TestExtraEnvVarsField
1✔
91

92

93
class JSTestRequest(TestRequest):
1✔
94
    tool_subsystem = NodeJSTest  # type: ignore[assignment]
1✔
95
    field_set_type = JSTestFieldSet
1✔
96

97
    partitioner_type = PartitionerType.CUSTOM
1✔
98

99

100
@dataclass(frozen=True)
1✔
101
class TestMetadata:
1✔
102
    extra_env_vars: tuple[str, ...]
1✔
103
    owning_target: Target
1✔
104
    compatibility_tag: str | None = None
1✔
105

106
    __test__ = False
1✔
107

108
    @property
1✔
109
    def description(self) -> str:
1✔
110
        return f"{self.owning_target[NodePackageNameField].value} {self.compatibility_tag or ''}"
1✔
111

112

113
@rule(desc="Partition NodeJS tests", level=LogLevel.DEBUG)
1✔
114
async def partition_nodejs_tests(
1✔
115
    request: JSTestRequest.PartitionRequest[JSTestFieldSet],
116
) -> Partitions[JSTestFieldSet, TestMetadata]:
UNCOV
117
    partitions = []
×
UNCOV
118
    compatible_tests = defaultdict(list)
×
UNCOV
119
    owning_packages = await concurrently(
×
120
        find_owning_package(OwningNodePackageRequest(field_set.address))
121
        for field_set in request.field_sets
122
    )
UNCOV
123
    for field_set, owning_package in zip(request.field_sets, owning_packages):
×
UNCOV
124
        metadata = TestMetadata(
×
125
            extra_env_vars=field_set.extra_env_vars.sorted(),
126
            owning_target=owning_package.ensure_owner(),
127
            compatibility_tag=field_set.batch_compatibility_tag.value,
128
        )
129

UNCOV
130
        if not metadata.compatibility_tag:
×
UNCOV
131
            partitions.append(Partition((field_set,), metadata))
×
132
        else:
UNCOV
133
            compatible_tests[metadata].append(field_set)
×
134

UNCOV
135
    for metadata, field_sets in compatible_tests.items():
×
UNCOV
136
        partitions.append(Partition(tuple(field_sets), metadata))
×
137

UNCOV
138
    return Partitions(partitions)
×
139

140

141
@rule(level=LogLevel.DEBUG, desc="Run javascript tests")
1✔
142
async def run_javascript_tests(
1✔
143
    batch: JSTestRequest.Batch[JSTestFieldSet, TestMetadata],
144
    test: TestSubsystem,
145
    test_extra_env: TestExtraEnv,
146
) -> TestResult:
147
    field_sets = batch.elements
1✔
148
    metadata = batch.partition_metadata
1✔
149
    installation_get = install_node_packages_for_address(
1✔
150
        InstalledNodePackageRequest(metadata.owning_target.address), **implicitly()
151
    )
152
    transitive_tgts_get = transitive_targets(
1✔
153
        TransitiveTargetsRequest(field_set.address for field_set in field_sets), **implicitly()
154
    )
155

156
    field_set_source_files_get = determine_source_files(
1✔
157
        SourceFilesRequest(field_set.source for field_set in field_sets)
158
    )
159
    target_env_vars_get = environment_vars_subset(
1✔
160
        EnvironmentVarsRequest(metadata.extra_env_vars), **implicitly()
161
    )
162
    installation, transitive_tgts, field_set_source_files, target_env_vars = await concurrently(
1✔
163
        installation_get, transitive_tgts_get, field_set_source_files_get, target_env_vars_get
164
    )
165

166
    sources = await determine_source_files(
1✔
167
        SourceFilesRequest(
168
            (tgt.get(SourcesField) for tgt in transitive_tgts.closure),
169
            enable_codegen=True,
170
            for_sources_types=[
171
                JSRuntimeSourceField,
172
                TypeScriptSourceField,
173
                AssetSourceField,
174
            ],
175
        )
176
    )
177
    merged_digest = await merge_digests(
1✔
178
        MergeDigests([sources.snapshot.digest, installation.digest])
179
    )
180

181
    def relative_package_dir(file: str) -> str:
1✔
182
        return fast_relpath(file, installation.project_env.package_dir())
1✔
183

184
    test_script = installation.project_env.ensure_target()[NodePackageTestScriptField].value
1✔
185
    entry_point = test_script.entry_point
1✔
186

187
    coverage_args: tuple[str, ...] = ()
1✔
188
    output_files: list[str] = []
1✔
189
    output_directories: list[str] = []
1✔
190
    if test.use_coverage and test_script.supports_coverage():
1✔
191
        coverage_args = test_script.coverage_args
1✔
192
        output_files.extend(test_script.coverage_output_files)
1✔
193
        output_directories.extend(test_script.coverage_output_directories)
1✔
194
        entry_point = test_script.coverage_entry_point or entry_point
1✔
195

196
    timeout_seconds: int | None = None
1✔
197
    for field_set in field_sets:
1✔
198
        timeout = field_set.timeout.calculate_from_global_options(test)
1✔
199
        if timeout:
1✔
200
            if timeout_seconds:
×
201
                timeout_seconds += timeout
×
202
            else:
203
                timeout_seconds = timeout
×
204
    file_description = field_sets[0].address.spec
1✔
205
    if len(field_sets) > 1:
1✔
206
        file_description += f"+ {pluralize(len(field_sets) - 1, 'other file')}"
1✔
207
    process = await setup_nodejs_project_environment_process(
1✔
208
        NodeJsProjectEnvironmentProcess(
209
            installation.project_env,
210
            args=(
211
                "run",
212
                entry_point,
213
                *installation.project_env.project.args_separator,
214
                *sorted(relative_package_dir(file) for file in field_set_source_files.files),
215
                *coverage_args,
216
            ),
217
            description=f"Running npm tests for {file_description}.",
218
            input_digest=merged_digest,
219
            level=LogLevel.INFO,
220
            extra_env=FrozenDict(**test_extra_env.env, **target_env_vars),
221
            timeout_seconds=timeout_seconds,
222
            output_files=tuple(
223
                installation.join_relative_workspace_directory(file) for file in output_files or ()
224
            ),
225
            output_directories=tuple(
226
                installation.join_relative_workspace_directory(directory)
227
                for directory in output_directories or ()
228
            ),
229
        ),
230
        **implicitly(),
231
    )
232
    process = dataclasses.replace(process, cache_scope=test.default_process_cache_scope)
1✔
233

234
    results = await execute_process_with_retry(ProcessWithRetries(process, test.attempts_default))
1✔
235
    coverage_data: JSCoverageData | None = None
1✔
236
    if test.use_coverage:
1✔
237
        coverage_snapshot = await digest_to_snapshot(
1✔
238
            **implicitly(
239
                DigestSubset(
240
                    results.last.output_digest,
241
                    test_script.coverage_globs(
242
                        installation.project_env.relative_workspace_directory()
243
                    ),
244
                )
245
            )
246
        )
247

248
        coverage_data = JSCoverageData(
1✔
249
            coverage_snapshot,
250
            tuple(field_set.address for field_set in field_sets),
251
            output_files=test_script.coverage_output_files,
252
            output_directories=test_script.coverage_output_directories,
253
            working_directory=installation.project_env.relative_workspace_directory(),
254
        )
255

256
    return TestResult.from_batched_fallible_process_result(
1✔
257
        results.results, batch, test.output, coverage_data=coverage_data
258
    )
259

260

261
@rule(desc="Collecting coverage reports.")
1✔
262
async def collect_coverage_reports(
1✔
263
    coverage_reports: JSCoverageDataCollection,
264
    dist_dir: DistDir,
265
    nodejs_test: NodeJSTest,
266
) -> CoverageReports:
267
    gets_per_data = [
×
268
        (
269
            file,
270
            report,
271
            digest_to_snapshot(
272
                **implicitly(
273
                    DigestSubset(
274
                        report.snapshot.digest,
275
                        NodeTestScript.coverage_globs_for(
276
                            report.working_directory,
277
                            (file,),
278
                            report.output_directories,
279
                            GlobMatchErrorBehavior.error,
280
                            GlobExpansionConjunction.all_match,
281
                            description_of_origin="the JS coverage report collection rule",
282
                        ),
283
                    )
284
                )
285
            ),
286
        )
287
        for report in coverage_reports
288
        for file in report.output_files
289
    ]
290
    snapshots = await concurrently(get for _, _, get in gets_per_data)
×
291
    return CoverageReports(
×
292
        tuple(
293
            _get_report(
294
                nodejs_test, dist_dir, snapshot, data.addresses, file, data.working_directory
295
            )
296
            for (file, data), snapshot in zip(
297
                ((file, report) for file, report, _ in gets_per_data), snapshots
298
            )
299
        )
300
    )
301

302

303
def _get_report(
1✔
304
    nodejs_test: NodeJSTest,
305
    dist_dir: DistDir,
306
    snapshot: Snapshot,
307
    addresses: tuple[Address, ...],
308
    file: str,
309
    working_directory: str,
310
) -> FilesystemCoverageReport:
311
    # It is up to the user to configure the output coverage reports.
312
    file_path = PurePath(file)
×
313
    output_dir = nodejs_test.render_coverage_output_dir(dist_dir, addresses)
×
314
    return FilesystemCoverageReport(
×
315
        coverage_insufficient=False,
316
        result_snapshot=snapshot,
317
        directory_to_materialize_to=output_dir,
318
        report_file=output_dir / working_directory / file_path,
319
        report_type=file_path.suffix,
320
    )
321

322

323
def rules() -> Iterable[Rule | UnionRule]:
1✔
324
    return [
1✔
325
        *platform_rules.rules(),
326
        *graph.rules(),
327
        *nodejs_project_environment.rules(),
328
        *install_node_package.rules(),
329
        *source_files.rules(),
330
        *JSTestRequest.rules(),
331
        UnionRule(CoverageDataCollection, JSCoverageDataCollection),
332
        *collect_rules(),
333
    ]
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