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

pantsbuild / pants / 25443604553

06 May 2026 03:05PM UTC coverage: 92.879% (-0.04%) from 92.915%
25443604553

push

github

web-flow
[pants_ng] Scaffolding for a pants_ng mode. (#23319)

In this mode the command line is parsed as an
NG invocation, and dispatched appropriately.

Of course at the moment there are no
implementations to dispatch to. That will follow.

This does expose a new option, `pants_ng` to users. 
There is a big warning not to set it, but we're not trying
to hide that we're working on a new thing, so I am
comfortable with this.

25 of 76 new or added lines in 9 files covered. (32.89%)

1294 existing lines in 76 files now uncovered.

92234 of 99306 relevant lines covered (92.88%)

4.05 hits per line

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

93.18
/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 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  # type: ignore[assignment]
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 ''}"
1✔
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
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.")
2✔
262
async def collect_coverage_reports(
2✔
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(
2✔
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]:
2✔
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