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

pantsbuild / pants / 18812500213

26 Oct 2025 03:42AM UTC coverage: 80.284% (+0.005%) from 80.279%
18812500213

Pull #22804

github

web-flow
Merge 2a56fdb46 into 4834308dc
Pull Request #22804: test_shell_command: use correct default cache scope for a test's environment

29 of 31 new or added lines in 2 files covered. (93.55%)

1314 existing lines in 64 files now uncovered.

77900 of 97030 relevant lines covered (80.28%)

3.35 hits per line

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

64.66
/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
2✔
4

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

11
from pants.backend.javascript import install_node_package, nodejs_project_environment
2✔
12
from pants.backend.javascript.install_node_package import (
2✔
13
    InstalledNodePackageRequest,
14
    install_node_packages_for_address,
15
)
16
from pants.backend.javascript.nodejs_project_environment import (
2✔
17
    NodeJsProjectEnvironmentProcess,
18
    setup_nodejs_project_environment_process,
19
)
20
from pants.backend.javascript.package_json import (
2✔
21
    NodePackageNameField,
22
    NodePackageTestScriptField,
23
    NodeTestScript,
24
    OwningNodePackageRequest,
25
    find_owning_package,
26
)
27
from pants.backend.javascript.subsystems.nodejstest import NodeJSTest
2✔
28
from pants.backend.javascript.target_types import JSRuntimeSourceField, JSTestRuntimeSourceField
2✔
29
from pants.backend.typescript.target_types import TypeScriptSourceField
2✔
30
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
2✔
31
from pants.build_graph.address import Address
2✔
32
from pants.core.goals.test import (
2✔
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
2✔
47
from pants.core.util_rules import source_files
2✔
48
from pants.core.util_rules.distdir import DistDir
2✔
49
from pants.core.util_rules.env_vars import environment_vars_subset
2✔
50
from pants.core.util_rules.partitions import Partition, PartitionerType, Partitions
2✔
51
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
2✔
52
from pants.engine.env_vars import EnvironmentVarsRequest
2✔
53
from pants.engine.fs import DigestSubset, GlobExpansionConjunction
2✔
54
from pants.engine.internals import graph, platform_rules
2✔
55
from pants.engine.internals.graph import transitive_targets
2✔
56
from pants.engine.internals.native_engine import MergeDigests, Snapshot
2✔
57
from pants.engine.internals.selectors import concurrently
2✔
58
from pants.engine.intrinsics import digest_to_snapshot, execute_process_with_retry, merge_digests
2✔
59
from pants.engine.process import ProcessCacheScope, ProcessWithRetries
2✔
60
from pants.engine.rules import Rule, collect_rules, implicitly, rule
2✔
61
from pants.engine.target import Dependencies, SourcesField, Target, TransitiveTargetsRequest
2✔
62
from pants.engine.unions import UnionRule
2✔
63
from pants.util.dirutil import fast_relpath
2✔
64
from pants.util.frozendict import FrozenDict
2✔
65
from pants.util.logging import LogLevel
2✔
66
from pants.util.strutil import pluralize
2✔
67

68

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

77

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

81

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

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

92

93
class JSTestRequest(TestRequest):
2✔
94
    tool_subsystem = NodeJSTest
2✔
95
    field_set_type = JSTestFieldSet
2✔
96

97
    partitioner_type = PartitionerType.CUSTOM
2✔
98

99

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

106
    __test__ = False
2✔
107

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

112

113
@rule(desc="Partition NodeJS tests", level=LogLevel.DEBUG)
2✔
114
async def partition_nodejs_tests(
2✔
115
    request: JSTestRequest.PartitionRequest[JSTestFieldSet],
116
) -> Partitions[JSTestFieldSet, TestMetadata]:
UNCOV
117
    partitions = []
1✔
UNCOV
118
    compatible_tests = defaultdict(list)
1✔
UNCOV
119
    owning_packages = await concurrently(
1✔
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):
1✔
UNCOV
124
        metadata = TestMetadata(
1✔
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:
1✔
UNCOV
131
            partitions.append(Partition((field_set,), metadata))
1✔
132
        else:
UNCOV
133
            compatible_tests[metadata].append(field_set)
1✔
134

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

UNCOV
138
    return Partitions(partitions)
1✔
139

140

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

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

166
    sources = await determine_source_files(
×
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(
×
178
        MergeDigests([sources.snapshot.digest, installation.digest])
179
    )
180

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

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

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

196
    timeout_seconds: int | None = None
×
197
    for field_set in field_sets:
×
198
        timeout = field_set.timeout.calculate_from_global_options(test)
×
199
        if timeout:
×
200
            if timeout_seconds:
×
201
                timeout_seconds += timeout
×
202
            else:
203
                timeout_seconds = timeout
×
204
    file_description = field_sets[0].address.spec
×
205
    if len(field_sets) > 1:
×
206
        file_description += f"+ {pluralize(len(field_sets) - 1, 'other file')}"
×
207
    process = await setup_nodejs_project_environment_process(
×
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
    if test.force:
×
233
        process = dataclasses.replace(process, cache_scope=ProcessCacheScope.PER_SESSION)
×
234

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

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

257
    return TestResult.from_batched_fallible_process_result(
×
258
        results.results, batch, test.output, coverage_data=coverage_data
259
    )
260

261

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

303

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

323

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