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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

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

0.0
/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).
UNCOV
3
from __future__ import annotations
×
4

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

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

68

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

77

UNCOV
78
class JSCoverageDataCollection(CoverageDataCollection[JSCoverageData]):
×
UNCOV
79
    element_type = JSCoverageData
×
80

81

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

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

92

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

UNCOV
97
    partitioner_type = PartitionerType.CUSTOM
×
98

99

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

UNCOV
106
    __test__ = False
×
107

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

112

UNCOV
113
@rule(desc="Partition NodeJS tests", level=LogLevel.DEBUG)
×
UNCOV
114
async def partition_nodejs_tests(
×
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

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

UNCOV
156
    field_set_source_files_get = determine_source_files(
×
157
        SourceFilesRequest(field_set.source for field_set in field_sets)
158
    )
UNCOV
159
    target_env_vars_get = environment_vars_subset(
×
160
        EnvironmentVarsRequest(metadata.extra_env_vars), **implicitly()
161
    )
UNCOV
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

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

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

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

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

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

UNCOV
234
    results = await execute_process_with_retry(ProcessWithRetries(process, test.attempts_default))
×
UNCOV
235
    coverage_data: JSCoverageData | None = None
×
UNCOV
236
    if test.use_coverage:
×
UNCOV
237
        coverage_snapshot = await digest_to_snapshot(
×
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

UNCOV
248
        coverage_data = JSCoverageData(
×
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

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

260

UNCOV
261
@rule(desc="Collecting coverage reports.")
×
UNCOV
262
async def collect_coverage_reports(
×
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

UNCOV
303
def _get_report(
×
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

UNCOV
323
def rules() -> Iterable[Rule | UnionRule]:
×
UNCOV
324
    return [
×
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