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

pantsbuild / pants / 21919838070

11 Feb 2026 07:27PM UTC coverage: 80.351% (+0.001%) from 80.35%
21919838070

Pull #23096

github

web-flow
Merge 9f45c9e39 into 9a67b81d3
Pull Request #23096: partially DRY out cache scope for test runners

8 of 15 new or added lines in 7 files covered. (53.33%)

1 existing line in 1 file now uncovered.

78767 of 98029 relevant lines covered (80.35%)

3.36 hits per line

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

67.47
/src/python/pants/jvm/test/junit.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
5✔
5

6
import logging
5✔
7
from dataclasses import dataclass
5✔
8
from typing import Any
5✔
9

10
from pants.backend.java.subsystems.junit import JUnit
5✔
11
from pants.core.goals.resolves import ExportableTool
5✔
12
from pants.core.goals.test import (
5✔
13
    TestDebugRequest,
14
    TestExtraEnv,
15
    TestExtraEnvVarsField,
16
    TestFieldSet,
17
    TestRequest,
18
    TestResult,
19
    TestSubsystem,
20
)
21
from pants.core.target_types import FileSourceField
5✔
22
from pants.core.util_rules.env_vars import environment_vars_subset
5✔
23
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
5✔
24
from pants.engine.addresses import Addresses
5✔
25
from pants.engine.env_vars import EnvironmentVarsRequest
5✔
26
from pants.engine.fs import DigestSubset, MergeDigests, PathGlobs, RemovePrefix
5✔
27
from pants.engine.internals.graph import transitive_targets
5✔
28
from pants.engine.intrinsics import (
5✔
29
    digest_subset_to_digest,
30
    digest_to_snapshot,
31
    execute_process_with_retry,
32
    merge_digests,
33
)
34
from pants.engine.process import InteractiveProcess, ProcessWithRetries
5✔
35
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
5✔
36
from pants.engine.target import SourcesField, TransitiveTargetsRequest
5✔
37
from pants.engine.unions import UnionRule
5✔
38
from pants.jvm.classpath import classpath as classpath_get
5✔
39
from pants.jvm.goals import lockfile
5✔
40
from pants.jvm.jdk_rules import JdkRequest, JvmProcess, jvm_process, prepare_jdk_environment
5✔
41
from pants.jvm.resolve.coursier_fetch import ToolClasspathRequest, materialize_classpath_for_tool
5✔
42
from pants.jvm.resolve.jvm_tool import GenerateJvmLockfileFromTool
5✔
43
from pants.jvm.subsystems import JvmSubsystem
5✔
44
from pants.jvm.target_types import (
5✔
45
    JunitTestSourceField,
46
    JunitTestTimeoutField,
47
    JvmDependenciesField,
48
    JvmJdkField,
49
)
50
from pants.util.logging import LogLevel
5✔
51

52
logger = logging.getLogger(__name__)
5✔
53

54

55
@dataclass(frozen=True)
5✔
56
class JunitTestFieldSet(TestFieldSet):
5✔
57
    required_fields = (
5✔
58
        JunitTestSourceField,
59
        JvmJdkField,
60
    )
61

62
    sources: JunitTestSourceField
5✔
63
    timeout: JunitTestTimeoutField
5✔
64
    jdk_version: JvmJdkField
5✔
65
    dependencies: JvmDependenciesField
5✔
66
    extra_env_vars: TestExtraEnvVarsField
5✔
67

68

69
class JunitTestRequest(TestRequest):
5✔
70
    tool_subsystem = JUnit  # type: ignore[assignment]
5✔
71
    field_set_type = JunitTestFieldSet
5✔
72
    supports_debug = True
5✔
73

74

75
@dataclass(frozen=True)
5✔
76
class TestSetupRequest:
5✔
77
    field_set: JunitTestFieldSet
5✔
78
    is_debug: bool
5✔
79

80

81
@dataclass(frozen=True)
5✔
82
class TestSetup:
5✔
83
    process: JvmProcess
5✔
84
    reports_dir_prefix: str
5✔
85

86

87
@rule(level=LogLevel.DEBUG)
5✔
88
async def setup_junit_for_target(
5✔
89
    request: TestSetupRequest,
90
    jvm: JvmSubsystem,
91
    junit: JUnit,
92
    test_subsystem: TestSubsystem,
93
    test_extra_env: TestExtraEnv,
94
) -> TestSetup:
95
    jdk, transitive_tgts = await concurrently(
×
96
        prepare_jdk_environment(**implicitly(JdkRequest.from_field(request.field_set.jdk_version))),
97
        transitive_targets(TransitiveTargetsRequest([request.field_set.address]), **implicitly()),
98
    )
99

100
    lockfile_request = GenerateJvmLockfileFromTool.create(junit)
×
101
    classpath, junit_classpath, files = await concurrently(
×
102
        classpath_get(**implicitly(Addresses([request.field_set.address]))),
103
        materialize_classpath_for_tool(ToolClasspathRequest(lockfile=lockfile_request)),
104
        determine_source_files(
105
            SourceFilesRequest(
106
                (dep.get(SourcesField) for dep in transitive_tgts.dependencies),
107
                for_sources_types=(FileSourceField,),
108
                enable_codegen=True,
109
            )
110
        ),
111
    )
112

113
    input_digest = await merge_digests(MergeDigests((*classpath.digests(), files.snapshot.digest)))
×
114

115
    toolcp_relpath = "__toolcp"
×
116
    extra_immutable_input_digests = {
×
117
        toolcp_relpath: junit_classpath.digest,
118
    }
119

120
    reports_dir_prefix = "__reports_dir"
×
121
    reports_dir = f"{reports_dir_prefix}/{request.field_set.address.path_safe_spec}"
×
122

123
    # Classfiles produced by the root `junit_test` targets are the only ones which should run.
124
    user_classpath_arg = ":".join(classpath.root_args())
×
125

126
    # Cache test runs only if they are successful, or not at all if `--test-force`.
NEW
127
    cache_scope = test_subsystem.default_process_cache_scope
×
128

129
    extra_jvm_args: list[str] = []
×
130
    if request.is_debug:
×
131
        extra_jvm_args.extend(jvm.debug_args)
×
132

133
    field_set_extra_env = await environment_vars_subset(
×
134
        EnvironmentVarsRequest(request.field_set.extra_env_vars.value or ()), **implicitly()
135
    )
136

137
    process = JvmProcess(
×
138
        jdk=jdk,
139
        classpath_entries=[
140
            *classpath.args(),
141
            *junit_classpath.classpath_entries(toolcp_relpath),
142
        ],
143
        argv=[
144
            *extra_jvm_args,
145
            "org.junit.platform.console.ConsoleLauncher",
146
            *(("--classpath", user_classpath_arg) if user_classpath_arg else ()),
147
            *(("--scan-class-path", user_classpath_arg) if user_classpath_arg else ()),
148
            "--reports-dir",
149
            reports_dir,
150
            *junit.args,
151
        ],
152
        input_digest=input_digest,
153
        extra_env={**test_extra_env.env, **field_set_extra_env},
154
        extra_jvm_options=junit.jvm_options,
155
        extra_immutable_input_digests=extra_immutable_input_digests,
156
        output_directories=(reports_dir,),
157
        description=f"Run JUnit 5 ConsoleLauncher against {request.field_set.address}",
158
        timeout_seconds=request.field_set.timeout.calculate_from_global_options(test_subsystem),
159
        level=LogLevel.DEBUG,
160
        cache_scope=cache_scope,
161
        use_nailgun=False,
162
    )
163
    return TestSetup(process=process, reports_dir_prefix=reports_dir_prefix)
×
164

165

166
@rule(desc="Run JUnit", level=LogLevel.DEBUG)
5✔
167
async def run_junit_test(
5✔
168
    test_subsystem: TestSubsystem,
169
    batch: JunitTestRequest.Batch[JunitTestFieldSet, Any],
170
) -> TestResult:
171
    field_set = batch.single_element
×
172

173
    test_setup = await setup_junit_for_target(
×
174
        TestSetupRequest(field_set, is_debug=False), **implicitly()
175
    )
176
    process = await jvm_process(**implicitly(test_setup.process))
×
177
    process_results = await execute_process_with_retry(
×
178
        ProcessWithRetries(process, test_subsystem.attempts_default)
179
    )
180
    reports_dir_prefix = test_setup.reports_dir_prefix
×
181

182
    xml_result_subset = await digest_subset_to_digest(
×
183
        DigestSubset(process_results.last.output_digest, PathGlobs([f"{reports_dir_prefix}/**"]))
184
    )
185
    xml_results = await digest_to_snapshot(
×
186
        **implicitly(RemovePrefix(xml_result_subset, reports_dir_prefix))
187
    )
188

189
    return TestResult.from_fallible_process_result(
×
190
        process_results=process_results.results,
191
        address=field_set.address,
192
        output_setting=test_subsystem.output,
193
        xml_results=xml_results,
194
    )
195

196

197
@rule(level=LogLevel.DEBUG)
5✔
198
async def setup_junit_debug_request(
5✔
199
    batch: JunitTestRequest.Batch[JunitTestFieldSet, Any],
200
) -> TestDebugRequest:
201
    setup = await setup_junit_for_target(
×
202
        TestSetupRequest(batch.single_element, is_debug=True), **implicitly()
203
    )
204
    process = await jvm_process(**implicitly(setup.process))
×
205
    return TestDebugRequest(
×
206
        InteractiveProcess.from_process(process, forward_signals_to_process=False, restartable=True)
207
    )
208

209

210
def rules():
5✔
211
    return [
5✔
212
        *collect_rules(),
213
        *lockfile.rules(),
214
        UnionRule(ExportableTool, JUnit),
215
        *JunitTestRequest.rules(),
216
    ]
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