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

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

1.81 hits per line

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

79.43
/src/python/pants/pantsd/pantsd_integration_test_base.py
1
# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
2✔
5

6
import functools
2✔
7
import os
2✔
8
import time
2✔
9
import unittest
2✔
10
from collections.abc import Callable, Iterator, Mapping
2✔
11
from contextlib import contextmanager
2✔
12
from dataclasses import dataclass
2✔
13
from typing import Any
2✔
14

15
from colors import bold, cyan, magenta
2✔
16

17
from pants.pantsd.process_manager import ProcessManager
2✔
18
from pants.testutil.pants_integration_test import (
2✔
19
    PantsJoinHandle,
20
    PantsResult,
21
    kill_daemon,
22
    read_pants_log,
23
    run_pants,
24
    run_pants_with_workdir,
25
    run_pants_with_workdir_without_waiting,
26
)
27
from pants.util.collections import recursively_update
2✔
28
from pants.util.contextutil import temporary_dir
2✔
29
from pants.util.dirutil import maybe_read_file
2✔
30

31

32
def banner(s):
2✔
33
    print(cyan("=" * 63))
2✔
34
    print(cyan(f"- {s} {('-' * (60 - len(s)))}"))
2✔
35
    print(cyan("=" * 63))
2✔
36

37

38
def attempts(
2✔
39
    msg: str,
40
    *,
41
    delay: float = 0.5,
42
    timeout: float = 30,
43
    backoff: float = 1.2,
44
) -> Iterator[None]:
45
    """A generator that yields a number of times before failing.
46

47
    A caller should break out of a loop on the generator in order to succeed.
48
    """
49
    count = 0
2✔
50
    deadline = time.time() + timeout
2✔
51
    while time.time() < deadline:
2✔
52
        count += 1
2✔
53
        yield
2✔
UNCOV
54
        time.sleep(delay)
×
UNCOV
55
        delay *= backoff
×
56
    raise AssertionError(f"After {count} attempts in {timeout} seconds: {msg}")
×
57

58

59
def launch_waiter(
2✔
60
    *, workdir: str, config: Mapping | None = None, cleanup_wait_time: int = 0
61
) -> tuple[PantsJoinHandle, int, int, str]:
62
    """Launch a process that will wait forever for a file to be created.
63

64
    Returns the pants client handle, the pid of the waiting process, the pid of a child of the
65
    waiting process, and the file to create to cause the waiting child to exit.
66
    """
UNCOV
67
    file_to_make = os.path.join(workdir, "some_magic_file")
×
UNCOV
68
    waiter_pid_file = os.path.join(workdir, "pid_file")
×
UNCOV
69
    child_pid_file = os.path.join(workdir, "child_pid_file")
×
70

UNCOV
71
    argv = [
×
72
        "run",
73
        "testprojects/src/python/coordinated_runs:waiter",
74
        "--",
75
        file_to_make,
76
        waiter_pid_file,
77
        child_pid_file,
78
        str(cleanup_wait_time),
79
    ]
UNCOV
80
    client_handle = run_pants_with_workdir_without_waiting(argv, workdir=workdir, config=config)
×
UNCOV
81
    waiter_pid = -1
×
UNCOV
82
    for _ in attempts("The waiter process should have written its pid."):
×
UNCOV
83
        waiter_pid_str = maybe_read_file(waiter_pid_file)
×
UNCOV
84
        child_pid_str = maybe_read_file(child_pid_file)
×
UNCOV
85
        if waiter_pid_str and child_pid_str:
×
UNCOV
86
            waiter_pid = int(waiter_pid_str)
×
UNCOV
87
            child_pid = int(child_pid_str)
×
UNCOV
88
            break
×
UNCOV
89
    return client_handle, waiter_pid, child_pid, file_to_make
×
90

91

92
class PantsDaemonMonitor(ProcessManager):
2✔
93
    def __init__(self, metadata_base_dir: str):
2✔
94
        super().__init__(name="pantsd", metadata_base_dir=metadata_base_dir)
2✔
95
        self._started = False
2✔
96

97
    def _log(self):
2✔
98
        print(magenta(f"PantsDaemonMonitor: pid is {self.pid} is_alive={self.is_alive()}"))
2✔
99

100
    def assert_started_and_stopped(self, timeout: int = 30) -> None:
2✔
101
        """Asserts that pantsd was alive (it wrote a pid file), but that it stops afterward."""
UNCOV
102
        self.await_pid(timeout)
×
UNCOV
103
        self._started = True
×
UNCOV
104
        self.assert_stopped()
×
105

106
    def assert_started(self, timeout=30):
2✔
107
        self.await_pid(timeout)
2✔
108
        self._started = True
2✔
109
        self._check_pantsd_is_alive()
2✔
110
        return self.pid
2✔
111

112
    def _check_pantsd_is_alive(self):
2✔
113
        self._log()
2✔
114
        assert self._started, (
2✔
115
            "cannot assert that pantsd is running. Try calling assert_started before calling this method."
116
        )
117
        assert self.is_alive(), "pantsd was not alive."
2✔
118
        return self.pid
2✔
119

120
    def current_memory_usage(self):
2✔
121
        """Return the current memory usage of the pantsd process (which must be running)
122

123
        :return: memory usage in bytes
124
        """
125
        self.assert_running()
×
126
        return self._as_process().memory_info()[0]
×
127

128
    def assert_running(self):
2✔
UNCOV
129
        if not self._started:
×
UNCOV
130
            return self.assert_started()
×
131
        else:
UNCOV
132
            return self._check_pantsd_is_alive()
×
133

134
    def assert_stopped(self):
2✔
135
        self._log()
2✔
136
        assert self._started, (
2✔
137
            "cannot assert pantsd stoppage. Try calling assert_started before calling this method."
138
        )
139
        for _ in attempts("pantsd should be stopped!"):
2✔
140
            if self.is_dead():
2✔
141
                break
2✔
142

143

144
@dataclass(frozen=True)
2✔
145
class PantsdRunContext:
2✔
146
    runner: Callable[..., Any]
2✔
147
    checker: PantsDaemonMonitor
2✔
148
    workdir: str
2✔
149
    pantsd_config: dict[str, Any]
2✔
150

151

152
class PantsDaemonIntegrationTestBase(unittest.TestCase):
2✔
153
    @staticmethod
2✔
154
    def run_pants(*args, **kwargs) -> PantsResult:
2✔
155
        # We set our own ad-hoc pantsd configuration in most of these tests.
UNCOV
156
        return run_pants(*args, **{**kwargs, **{"use_pantsd": False}})
×
157

158
    @staticmethod
2✔
159
    def run_pants_with_workdir(*args, **kwargs) -> PantsResult:
2✔
160
        # We set our own ad-hoc pantsd configuration in most of these tests.
161
        return run_pants_with_workdir(*args, **{**kwargs, **{"use_pantsd": False}})
2✔
162

163
    @staticmethod
2✔
164
    def run_pants_with_workdir_without_waiting(*args, **kwargs) -> PantsJoinHandle:
2✔
165
        # We set our own ad-hoc pantsd configuration in most of these tests.
UNCOV
166
        return run_pants_with_workdir_without_waiting(*args, **{**kwargs, **{"use_pantsd": False}})
×
167

168
    @contextmanager
2✔
169
    def pantsd_test_context(
2✔
170
        self, *, log_level: str = "info", extra_config: dict[str, Any] | None = None
171
    ) -> Iterator[tuple[str, dict[str, Any], PantsDaemonMonitor]]:
172
        with temporary_dir(root_dir=os.getcwd()) as dot_pants_dot_d:
2✔
173
            pid_dir = os.path.join(dot_pants_dot_d, "pids")
2✔
174
            workdir = os.path.join(dot_pants_dot_d, "workdir")
2✔
175
            print(f"\npantsd log is {workdir}/pants.log")
2✔
176
            pantsd_config = {
2✔
177
                "GLOBAL": {
178
                    "pantsd": True,
179
                    "level": log_level,
180
                    "pants_subprocessdir": pid_dir,
181
                    "backend_packages": [
182
                        # Provide goals used by various tests.
183
                        "pants.backend.python",
184
                        "pants.backend.python.lint.flake8",
185
                    ],
186
                },
187
                "python": {
188
                    "interpreter_constraints": "['>=3.11,<3.12']",
189
                },
190
            }
191

192
            if extra_config:
2✔
UNCOV
193
                recursively_update(pantsd_config, extra_config)
×
194
            print(f">>> config: \n{pantsd_config}\n")
2✔
195

196
            checker = PantsDaemonMonitor(pid_dir)
2✔
197
            kill_daemon(pid_dir)
2✔
198
            try:
2✔
199
                yield workdir, pantsd_config, checker
2✔
200
                kill_daemon(pid_dir)
2✔
201
                checker.assert_stopped()
2✔
202
            finally:
203
                banner("BEGIN pants.log")
2✔
204
                for line in read_pants_log(workdir):
2✔
205
                    print(line)
2✔
206
                banner("END pants.log")
2✔
207

208
    @contextmanager
2✔
209
    def pantsd_successful_run_context(self, *args, **kwargs) -> Iterator[PantsdRunContext]:
2✔
210
        with self.pantsd_run_context(*args, success=True, **kwargs) as context:  # type: ignore[misc]
2✔
211
            yield context
2✔
212

213
    @contextmanager
2✔
214
    def pantsd_run_context(
2✔
215
        self,
216
        log_level: str = "info",
217
        extra_config: dict[str, Any] | None = None,
218
        extra_env: dict[str, str] | None = None,
219
        success: bool = True,
220
    ) -> Iterator[PantsdRunContext]:
221
        with self.pantsd_test_context(log_level=log_level, extra_config=extra_config) as (
2✔
222
            workdir,
223
            pantsd_config,
224
            checker,
225
        ):
226
            runner = functools.partial(
2✔
227
                self.assert_runner,
228
                workdir,
229
                pantsd_config,
230
                extra_env=extra_env,
231
                success=success,
232
            )
233
            yield PantsdRunContext(
2✔
234
                runner=runner, checker=checker, workdir=workdir, pantsd_config=pantsd_config
235
            )
236

237
    def _run_count(self, workdir):
2✔
238
        run_tracker_dir = os.path.join(workdir, "run-tracker")
2✔
239
        if os.path.isdir(run_tracker_dir):
2✔
240
            return len([f for f in os.listdir(run_tracker_dir) if f != "latest"])
2✔
241
        else:
242
            return 0
2✔
243

244
    def assert_runner(
2✔
245
        self,
246
        workdir: str,
247
        config,
248
        cmd,
249
        extra_config=None,
250
        extra_env=None,
251
        success=True,
252
        expected_runs: int = 1,
253
    ):
254
        combined_config = config.copy()
2✔
255
        recursively_update(combined_config, extra_config or {})
2✔
256
        print(
2✔
257
            bold(
258
                cyan(
259
                    "\nrunning: ./pants {} (config={}) (extra_env={})".format(
260
                        " ".join(cmd), combined_config, extra_env
261
                    )
262
                )
263
            )
264
        )
265
        run_count = self._run_count(workdir)
2✔
266
        start_time = time.time()
2✔
267
        run = self.run_pants_with_workdir(
2✔
268
            cmd, workdir=workdir, config=combined_config, extra_env=extra_env or {}
269
        )
270
        elapsed = time.time() - start_time
2✔
271
        print(bold(cyan(f"\ncompleted in {elapsed} seconds")))
2✔
272

273
        if success:
2✔
274
            run.assert_success()
2✔
275
        else:
UNCOV
276
            run.assert_failure()
×
277

278
        runs_created = self._run_count(workdir) - run_count
2✔
279
        self.assertEqual(
2✔
280
            runs_created,
281
            expected_runs,
282
            f"Expected {expected_runs} RunTracker run(s) to be created per pantsd run: was {runs_created}",
283
        )
284

285
        return run
2✔
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