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

pantsbuild / pants / 23173232554

17 Mar 2026 12:55AM UTC coverage: 92.932% (-0.001%) from 92.933%
23173232554

push

github

web-flow
un bitrot flaky tests (#23166)

The goal isn't to fix the flakyness, just to make them runnable again as
a prerequisite to eventual fixing.

For `pantsd_integration_test.py` the files were moved around in
2d1794f582.

For `jdk_rules_test.py` the prefix switch pattern was already intoduced
in 45c51ae6de.

This was accomplished by having an LLM go through the list of all flaky
tests and uncomment the 'skip' one by one and then try to fix any
bitrot.

0 of 2 new or added lines in 2 files covered. (0.0%)

1 existing line in 1 file now uncovered.

91172 of 98106 relevant lines covered (92.93%)

4.05 hits per line

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

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

4
from __future__ import annotations
1✔
5

6
import glob
1✔
7
import os
1✔
8
import shutil
1✔
9
import signal
1✔
10
import sys
1✔
11
import threading
1✔
12
import time
1✔
13
import unittest
1✔
14
from pathlib import Path
1✔
15
from textwrap import dedent
1✔
16

17
import psutil
1✔
18
import pytest
1✔
19

20
from pants.pantsd.pantsd_integration_test_base import PantsDaemonIntegrationTestBase, launch_waiter
1✔
21
from pants.testutil.pants_integration_test import read_pants_log, setup_tmpdir, temporary_workdir
1✔
22
from pants.util.contextutil import environment_as, temporary_dir, temporary_file
1✔
23
from pants.util.dirutil import rm_rf, safe_file_dump, safe_mkdir, safe_open, safe_rmtree, touch
1✔
24

25

26
def launch_file_toucher(f):
1✔
27
    """Launch a loop to touch the given file, and return a function to call to stop and join it."""
28
    if not os.path.isfile(f):
×
29
        raise AssertionError("Refusing to touch a non-file.")
×
30

31
    halt = threading.Event()
×
32

33
    def file_toucher():
×
34
        while not halt.isSet():
×
35
            touch(f)
×
36
            time.sleep(1)
×
37

38
    thread = threading.Thread(target=file_toucher)
×
39
    thread.daemon = True
×
40
    thread.start()
×
41

42
    def join():
×
43
        halt.set()
×
44
        thread.join(timeout=10)
×
45

46
    return join
×
47

48

49
compilation_failure_dir_layout = {
1✔
50
    os.path.join("compilation_failure", "main.py"): "if __name__ == '__main__':\n    import sys¡",
51
    os.path.join(
52
        "compilation_failure", "BUILD"
53
    ): "python_sources()\npex_binary(name='bin', entry_point='main.py')",
54
}
55

56

57
class TestPantsDaemonIntegration(PantsDaemonIntegrationTestBase):
1✔
58
    hermetic = False
1✔
59

60
    def test_pantsd_run(self):
1✔
61
        with self.pantsd_successful_run_context(log_level="debug") as ctx:
1✔
62
            with setup_tmpdir({"foo/BUILD": "target()"}) as tmpdir:
1✔
63
                ctx.runner(["list", f"{tmpdir}/foo::"])
1✔
64
                ctx.checker.assert_started()
1✔
65

66
                ctx.runner(["list", f"{tmpdir}/foo::"])
1✔
67
                ctx.checker.assert_running()
1✔
68

69
    def test_pantsd_broken_pipe(self):
1✔
70
        with self.pantsd_test_context() as (workdir, pantsd_config, checker):
1✔
71
            run = self.run_pants_with_workdir(
1✔
72
                "help | head -1",
73
                workdir=workdir,
74
                config=pantsd_config,
75
                shell=True,
76
                # FIXME: Why is this necessary to set?
77
                set_pants_ignore=False,
78
            )
79
            self.assertNotIn("broken pipe", run.stderr.lower())
1✔
80
            checker.assert_started()
1✔
81

82
    def test_pantsd_pantsd_runner_doesnt_die_after_failed_run(self):
1✔
83
        with self.pantsd_test_context() as (workdir, pantsd_config, checker):
1✔
84
            # Run target that throws an exception in pants.
85
            with setup_tmpdir(compilation_failure_dir_layout) as tmpdir:
1✔
86
                self.run_pants_with_workdir(
1✔
87
                    ["lint", os.path.join(tmpdir, "compilation_failure", "main.py")],
88
                    workdir=workdir,
89
                    config=pantsd_config,
90
                ).assert_failure()
91
            checker.assert_started()
1✔
92

93
            # Assert pantsd is in a good functional state.
94
            self.run_pants_with_workdir(
1✔
95
                ["help"], workdir=workdir, config=pantsd_config
96
            ).assert_success()
97
            checker.assert_running()
1✔
98

99
    def test_pantsd_lifecycle_invalidation(self):
1✔
100
        """Run with different values of daemon=True options, which should trigger restarts."""
101
        with self.pantsd_successful_run_context() as ctx:
1✔
102
            last_pid = None
1✔
103
            for idx in range(3):
1✔
104
                # Run with a different value of a daemon=True option in each iteration.
105
                ctx.runner([f"--pantsd-invalidation-globs=ridiculous{idx}", "help"])
1✔
106
                next_pid = ctx.checker.assert_started()
1✔
107
                if last_pid is not None:
1✔
108
                    assert last_pid != next_pid
1✔
109
                last_pid = next_pid
1✔
110

111
    def test_pantsd_lifecycle_invalidation_from_auth_plugin(self) -> None:
1✔
112
        """If the dynamic remote options changed, we should reinitialize the scheduler but not
113
        restart the daemon."""
114
        plugin = dedent(
1✔
115
            """\
116
            from datetime import datetime
117
            from pants.option.bootstrap_options import AuthPluginState, AuthPluginResult
118

119
            def remote_auth(
120
                initial_execution_headers, initial_store_headers, options, env, prior_result
121
            ):
122
                # If the first run, don't change the headers, but use the `expiration` as a
123
                # sentinel so that future runs know to change it.
124
                if prior_result is None:
125
                    return AuthPluginResult(
126
                        state=AuthPluginState.OK,
127
                        execution_headers=initial_execution_headers,
128
                        store_headers=initial_store_headers,
129
                        expiration=datetime.min,
130
                    )
131

132
                # If second run, still don't change the headers, but update the `expiration` as a
133
                # sentinel for the next run.
134
                if prior_result.expiration == datetime.min:
135
                    return AuthPluginResult(
136
                        state=AuthPluginState.OK,
137
                        execution_headers=initial_execution_headers,
138
                        store_headers=initial_store_headers,
139
                        expiration=datetime.max,
140
                    )
141

142
                # Finally, on the third run, change the headers.
143
                if prior_result.expiration == datetime.max:
144
                    return AuthPluginResult(
145
                        state=AuthPluginState.OK,
146
                        execution_headers={"custom": "foo"},
147
                        store_headers=initial_store_headers,
148
                    )
149

150
                # If there was a fourth run, or `prior_result` didn't preserve the `expiration`
151
                # field properly, error.
152
                raise AssertionError(f"Unexpected prior_result: {prior_result}")
153
            """
154
        )
155
        with self.pantsd_successful_run_context() as ctx:
1✔
156

157
            def run_auth_plugin() -> tuple[str, int]:
1✔
158
                # This very hackily traverses up to the process's parent directory, rather than the
159
                # workdir.
160
                plugin_dir = Path(ctx.workdir).parent.parent / "auth_plugin"
1✔
161
                plugin_dir.mkdir(parents=True, exist_ok=True)
1✔
162
                (plugin_dir / "__init__.py").touch()
1✔
163
                (plugin_dir / "register.py").write_text(plugin)
1✔
164
                sys.path.append(str(plugin_dir))
1✔
165
                try:
1✔
166
                    result = ctx.runner(
1✔
167
                        [
168
                            "--pythonpath=auth_plugin",
169
                            "--backend-packages=auth_plugin",
170
                            "--remote-cache-read",
171
                            "--remote-store-address=grpc://fake",
172
                            "help",
173
                        ]
174
                    )
175
                finally:
176
                    sys.path.pop()
1✔
177
                    shutil.rmtree(plugin_dir)
1✔
178
                return result.stderr, ctx.checker.assert_started()
1✔
179

180
            first_stderr, first_pid = run_auth_plugin()
1✔
181
            assert (
1✔
182
                "Initializing scheduler" in first_stderr
183
                or "Reinitializing scheduler" in first_stderr
184
            )
185

186
            second_stderr, second_pid = run_auth_plugin()
1✔
187
            assert "Reinitializing scheduler" not in second_stderr
1✔
188
            assert first_pid == second_pid
1✔
189

190
            third_stderr, third_pid = run_auth_plugin()
1✔
191
            assert "Remote cache/execution options updated" in third_stderr
1✔
192
            assert "execution_headers: {}" in third_stderr and "'custom': 'foo'" in third_stderr
1✔
193
            assert "Reinitializing scheduler" in third_stderr
1✔
194
            assert second_pid == third_pid
1✔
195

196
    def test_pantsd_lifecycle_invalidation_bootstrap_options(self) -> None:
1✔
197
        """Test that changing bootstrap options triggers scheduler reinitialization with diff
198
        logs."""
199
        with self.pantsd_successful_run_context() as ctx:
1✔
200
            ctx.runner(["help"])
1✔
201
            first_pid = ctx.checker.assert_started()
1✔
202

203
            second_result = ctx.runner(["help"])
1✔
204
            second_pid = ctx.checker.assert_running()
1✔
205
            assert first_pid == second_pid
1✔
206
            assert "Reinitializing scheduler" not in second_result.stderr
1✔
207

208
            third_result = ctx.runner(["--pantsd-max-memory-usage=1000000", "help"])
1✔
209
            third_pid = ctx.checker.assert_running()
1✔
210
            assert second_pid == third_pid
1✔
211
            assert "Initialization options changed" in third_result.stderr
1✔
212
            assert "pantsd_max_memory_usage" in third_result.stderr
1✔
213
            assert "Reinitializing scheduler" in third_result.stderr
1✔
214

215
    def test_pantsd_lifecycle_non_invalidation(self):
1✔
216
        with self.pantsd_successful_run_context() as ctx:
1✔
217
            cmds = (["help"], ["--no-colors", "help"], ["help"])
1✔
218
            last_pid = None
1✔
219
            for cmd in cmds:
1✔
220
                # Run with a CLI flag.
221
                ctx.runner(cmd)
1✔
222
                next_pid = ctx.checker.assert_started()
1✔
223
                if last_pid is not None:
1✔
224
                    self.assertEqual(last_pid, next_pid)
1✔
225
                last_pid = next_pid
1✔
226

227
    def test_pantsd_lifecycle_non_invalidation_on_config_string(self):
1✔
228
        with temporary_dir() as dist_dir_root, temporary_dir() as config_dir:
1✔
229
            # Create a variety of config files that change an option that does _not_ affect the
230
            # daemon's fingerprint (only the Scheduler's), and confirm that it stays up.
231
            config_files = [
1✔
232
                os.path.abspath(os.path.join(config_dir, f"pants.{i}.toml")) for i in range(3)
233
            ]
234
            for idx, config_file in enumerate(config_files):
1✔
235
                print(f"writing {config_file}")
1✔
236
                with open(config_file, "w") as fh:
1✔
237
                    fh.write(
1✔
238
                        f"""[GLOBAL]\npants_distdir = "{os.path.join(dist_dir_root, str(idx))}"\n"""
239
                    )
240

241
            with self.pantsd_successful_run_context() as ctx:
1✔
242
                cmds = [[f"--pants-config-files={f}", "help"] for f in config_files]
1✔
243
                last_pid = None
1✔
244
                for cmd in cmds:
1✔
245
                    ctx.runner(cmd)
1✔
246
                    next_pid = ctx.checker.assert_started()
1✔
247
                    if last_pid is not None:
1✔
248
                        self.assertEqual(last_pid, next_pid)
1✔
249
                    last_pid = next_pid
1✔
250

251
    def test_pantsd_lifecycle_shutdown_for_broken_scheduler(self):
1✔
252
        with self.pantsd_test_context() as (workdir, config, checker):
1✔
253
            # Run with valid options.
254
            self.run_pants_with_workdir(["help"], workdir=workdir, config=config).assert_success()
1✔
255
            checker.assert_started()
1✔
256

257
            # And again with invalid scheduler-fingerprinted options that trigger a re-init.
258
            self.run_pants_with_workdir(
1✔
259
                ["--backend-packages=nonsensical", "help"], workdir=workdir, config=config
260
            ).assert_failure()
261
            checker.assert_stopped()
1✔
262

263
    def test_pantsd_aligned_output(self) -> None:
1✔
264
        # Set for pytest output display.
265
        self.maxDiff = None
1✔
266

267
        cmds = [["help", "goals"], ["help", "targets"], ["roots"]]
1✔
268

269
        config = {
1✔
270
            "GLOBAL": {
271
                # These must match the ones we configure in pantsd_integration_test_base.py.
272
                "backend_packages": ["pants.backend.python", "pants.backend.python.lint.flake8"],
273
            }
274
        }
275
        non_daemon_runs = [self.run_pants(cmd, config=config) for cmd in cmds]
1✔
276

277
        with self.pantsd_successful_run_context() as ctx:
1✔
278
            daemon_runs = [ctx.runner(cmd) for cmd in cmds]
1✔
279
            ctx.checker.assert_started()
1✔
280

281
        for cmd, run in zip(cmds, daemon_runs):
1✔
282
            print(f"(cmd, run) = ({cmd}, {run.stdout}, {run.stderr})")
1✔
283
            self.assertNotEqual(run.stdout, "", f"Empty stdout for {cmd}")
1✔
284

285
        for run_pair in zip(non_daemon_runs, daemon_runs):
1✔
286
            non_daemon_stdout = run_pair[0].stdout
1✔
287
            daemon_stdout = run_pair[1].stdout
1✔
288

289
            for line_pair in zip(non_daemon_stdout.splitlines(), daemon_stdout.splitlines()):
1✔
290
                assert line_pair[0] == line_pair[1]
1✔
291

292
    @unittest.skip("flaky: https://github.com/pantsbuild/pants/issues/7622")
1✔
293
    @pytest.mark.no_error_if_skipped
1✔
294
    def test_pantsd_filesystem_invalidation(self):
1✔
295
        """Runs with pantsd enabled, in a loop, while another thread invalidates files."""
296
        with self.pantsd_successful_run_context() as ctx:
×
297
            cmd = ["list", "::"]
×
298
            ctx.runner(cmd)
×
299
            ctx.checker.assert_started()
×
300

301
            # Launch a separate thread to poke files in 3rdparty.
NEW
302
            join = launch_file_toucher("testprojects/src/python/hello/BUILD")
×
303

304
            # Repeatedly re-list 3rdparty while the file is being invalidated.
305
            for _ in range(0, 16):
×
306
                ctx.runner(cmd)
×
307
                ctx.checker.assert_running()
×
308

309
            join()
×
310

311
    def test_pantsd_client_env_var_is_inherited_by_pantsd_runner_children(self):
1✔
312
        expected_key = "TEST_ENV_VAR_FOR_PANTSD_INTEGRATION_TEST"
1✔
313
        expected_value = "333"
1✔
314
        with self.pantsd_successful_run_context() as ctx:
1✔
315
            # First, launch the daemon without any local env vars set.
316
            ctx.runner(["help"])
1✔
317
            ctx.checker.assert_started()
1✔
318

319
            # Then, set an env var on the secondary call.
320
            # We additionally set the `HERMETIC_ENV` env var to allow the integration test harness
321
            # to pass this variable through.
322
            env = {
1✔
323
                expected_key: expected_value,
324
                "HERMETIC_ENV": expected_key,
325
            }
326
            with environment_as(**env):
1✔
327
                result = ctx.runner(
1✔
328
                    ["run", "testprojects/src/python/print_env:binary", "--", expected_key]
329
                )
330
                ctx.checker.assert_running()
1✔
331

332
            self.assertEqual(expected_value, "".join(result.stdout).strip())
1✔
333

334
    def test_pantsd_launch_env_var_is_not_inherited_by_pantsd_runner_children(self):
1✔
335
        with self.pantsd_test_context() as (workdir, pantsd_config, checker):
1✔
336
            with environment_as(NO_LEAKS="33"):
1✔
337
                self.run_pants_with_workdir(
1✔
338
                    ["help"], workdir=workdir, config=pantsd_config
339
                ).assert_success()
340
                checker.assert_started()
1✔
341

342
            self.run_pants_with_workdir(
1✔
343
                ["run", "testprojects/src/python/print_env:binary", "--", "NO_LEAKS"],
344
                workdir=workdir,
345
                config=pantsd_config,
346
            ).assert_failure()
347
            checker.assert_running()
1✔
348

349
    def test_pantsd_touching_a_file_does_not_restart_daemon(self):
1✔
350
        test_file = "testprojects/src/python/print_env/main.py"
1✔
351
        config = {
1✔
352
            "GLOBAL": {"pantsd_invalidation_globs": '["testprojects/src/python/print_env/*"]'}
353
        }
354
        with self.pantsd_successful_run_context(extra_config=config) as ctx:
1✔
355
            ctx.runner(["help"])
1✔
356
            ctx.checker.assert_started()
1✔
357

358
            # Let any fs events quiesce.
359
            time.sleep(5)
1✔
360

361
            ctx.checker.assert_running()
1✔
362

363
            touch(test_file)
1✔
364
            # Permit ample time for the async file event propagate in CI.
365
            time.sleep(10)
1✔
366
            ctx.checker.assert_running()
1✔
367

368
    @unittest.skip("flaky: https://github.com/pantsbuild/pants/issues/18664")
1✔
369
    @pytest.mark.no_error_if_skipped
1✔
370
    def test_pantsd_invalidation_file_tracking(self):
1✔
371
        test_dir = "testprojects/src/python/print_env"
×
372
        config = {"GLOBAL": {"pantsd_invalidation_globs": f'["{test_dir}/*"]'}}
×
373
        with self.pantsd_successful_run_context(extra_config=config) as ctx:
×
374
            ctx.runner(["help"])
×
375
            ctx.checker.assert_started()
×
376

377
            # See comment in `test_pantsd_invalidation_pants_toml_file`.
378
            time.sleep(15)
×
379
            ctx.checker.assert_running()
×
380

381
            def full_pants_log():
×
382
                return "\n".join(read_pants_log(ctx.workdir))
×
383

384
            # Create a new file in test_dir
385
            with temporary_file(suffix=".py", binary_mode=False, root_dir=test_dir) as temp_f:
×
386
                temp_f.write("import that\n")
×
387
                temp_f.close()
×
388

389
                ctx.checker.assert_stopped()
×
390

391
            self.assertIn("saw filesystem changes covered by invalidation globs", full_pants_log())
×
392

393
    @unittest.skip("flaky: https://github.com/pantsbuild/pants/issues/18664")
1✔
394
    @pytest.mark.no_error_if_skipped
1✔
395
    def test_pantsd_invalidation_pants_toml_file(self):
1✔
396
        # Test tmp_pants_toml (--pants-config-files=$tmp_pants_toml)'s removal
397
        tmp_pants_toml = os.path.abspath("testprojects/test_pants.toml")
×
398

399
        # Create tmp_pants_toml file
400
        with safe_open(tmp_pants_toml, "w") as f:
×
401
            f.write("[DEFAULT]\n")
×
402

403
        with self.pantsd_successful_run_context() as ctx:
×
404
            ctx.runner([f"--pants-config-files={tmp_pants_toml}", "help"])
×
405
            ctx.checker.assert_started()
×
406
            # This accounts for the amount of time it takes for the SchedulerService to begin watching
407
            # these files. That happens asynchronously after `pantsd` startup, and may take a long
408
            # time in a heavily loaded test environment.
409
            time.sleep(15)
×
410

411
            # Delete tmp_pants_toml
412
            os.unlink(tmp_pants_toml)
×
413
            ctx.checker.assert_stopped()
×
414

415
    def test_pantsd_pid_deleted(self):
1✔
416
        with self.pantsd_successful_run_context() as ctx:
1✔
417
            ctx.runner(["help"])
1✔
418
            ctx.checker.assert_started()
1✔
419

420
            # Let any fs events quiesce.
421
            time.sleep(10)
1✔
422

423
            ctx.checker.assert_running()
1✔
424
            subprocess_dir = ctx.pantsd_config["GLOBAL"]["pants_subprocessdir"]
1✔
425
            safe_rmtree(subprocess_dir)
1✔
426

427
            ctx.checker.assert_stopped()
1✔
428

429
    def test_pantsd_pid_change(self):
1✔
430
        with self.pantsd_successful_run_context() as ctx:
1✔
431
            ctx.runner(["help"])
1✔
432
            ctx.checker.assert_started()
1✔
433

434
            # Let any fs events quiesce.
435
            time.sleep(10)
1✔
436

437
            ctx.checker.assert_running()
1✔
438
            subprocess_dir = ctx.pantsd_config["GLOBAL"]["pants_subprocessdir"]
1✔
439
            (pidpath,) = glob.glob(os.path.join(subprocess_dir, "*", "pantsd", "pid"))
1✔
440
            with open(pidpath, "w") as f:
1✔
441
                f.write("9")
1✔
442

443
            ctx.checker.assert_stopped()
1✔
444

445
            # Remove the pidfile so that the teardown script doesn't try to kill process 9.
446
            os.unlink(pidpath)
1✔
447

448
    @pytest.mark.skip(reason="flaky: https://github.com/pantsbuild/pants/issues/8193")
1✔
449
    @pytest.mark.no_error_if_skipped
1✔
450
    def test_pantsd_memory_usage(self):
1✔
451
        """Validates that after N runs, memory usage has increased by no more than X percent."""
452
        number_of_runs = 10
×
453
        max_memory_increase_fraction = 0.40  # TODO https://github.com/pantsbuild/pants/issues/7647
×
454
        with self.pantsd_successful_run_context() as ctx:
×
455
            # NB: This doesn't actually run against all testprojects, only those that are in the chroot,
456
            # i.e. explicitly declared in this test file's BUILD.
457
            cmd = ["list", "testprojects::"]
×
458
            ctx.runner(cmd).assert_success()
×
459
            initial_memory_usage = ctx.checker.current_memory_usage()
×
460
            for _ in range(number_of_runs):
×
461
                ctx.runner(cmd).assert_success()
×
462
                ctx.checker.assert_running()
×
463

464
            final_memory_usage = ctx.checker.current_memory_usage()
×
465
            self.assertTrue(
×
466
                initial_memory_usage <= final_memory_usage,
467
                f"Memory usage inverted unexpectedly: {initial_memory_usage} > {final_memory_usage}",
468
            )
469

470
            increase_fraction = (float(final_memory_usage) / initial_memory_usage) - 1.0
×
471
            self.assertTrue(
×
472
                increase_fraction <= max_memory_increase_fraction,
473
                f"Memory usage increased more than expected: {initial_memory_usage} -> {final_memory_usage}: {increase_fraction} actual increase (expected < {max_memory_increase_fraction})",
474
            )
475

476
    def test_pantsd_max_memory_usage(self):
1✔
477
        """Validates that the max_memory_usage setting is respected."""
478
        # We set a very, very low max memory usage, which forces pantsd to restart immediately.
479
        max_memory_usage_bytes = 130
1✔
480
        with self.pantsd_successful_run_context() as ctx:
1✔
481
            # TODO: We run the command, but we expect it to race pantsd shutting down, so we don't
482
            # assert success. https://github.com/pantsbuild/pants/issues/8200 will address waiting
483
            # until after the current command completes to invalidate the scheduler, at which point
484
            # we can assert success here.
485
            ctx.runner(
1✔
486
                [f"--pantsd-max-memory-usage={max_memory_usage_bytes}", "list", "testprojects::"]
487
            )
488

489
            # Assert that a pid file is written, but that the server stops afterward.
490
            ctx.checker.assert_started_and_stopped()
1✔
491

492
    def test_pantsd_invalidation_stale_sources(self):
1✔
493
        test_path = "daemon_correctness_test_0001"
1✔
494
        test_build_file = os.path.join(test_path, "BUILD")
1✔
495
        test_src_file = os.path.join(test_path, "some_file.py")
1✔
496
        filedeps_cmd = ["--unmatched-build-file-globs=warn", "filedeps", test_path]
1✔
497

498
        try:
1✔
499
            with self.pantsd_successful_run_context() as ctx:
1✔
500
                safe_mkdir(test_path, clean=True)
1✔
501

502
                ctx.runner(["help"])
1✔
503
                ctx.checker.assert_started()
1✔
504

505
                safe_file_dump(
1✔
506
                    test_build_file, "python_sources(sources=['some_non_existent_file.py'])"
507
                )
508
                non_existent_file = os.path.join(test_path, "some_non_existent_file.py")
1✔
509

510
                result = ctx.runner(filedeps_cmd)
1✔
511
                ctx.checker.assert_running()
1✔
512
                assert non_existent_file not in result.stdout
1✔
513

514
                safe_file_dump(test_build_file, "python_sources(sources=['*.py'])")
1✔
515
                result = ctx.runner(filedeps_cmd)
1✔
516
                ctx.checker.assert_running()
1✔
517
                assert non_existent_file not in result.stdout
1✔
518

519
                safe_file_dump(test_src_file, "print('hello')\n")
1✔
520
                result = ctx.runner(filedeps_cmd)
1✔
521
                ctx.checker.assert_running()
1✔
522
                assert test_src_file in result.stdout
1✔
523
        finally:
524
            rm_rf(test_path)
1✔
525

526
    def _assert_pantsd_keyboardinterrupt_signal(
1✔
527
        self,
528
        signum: int,
529
        regexps: list[str] | None = None,
530
        not_regexps: list[str] | None = None,
531
        cleanup_wait_time: int = 0,
532
    ):
533
        """Send a signal to the thin pailgun client and observe the error messaging.
534

535
        :param signum: The signal to send.
536
        :param regexps: Assert that all of these regexps match somewhere in stderr.
537
        :param not_regexps: Assert that all of these regexps do not match somewhere in stderr.
538
        :param cleanup_wait_time: passed through to waiter, dictated how long simulated cleanup will take
539
        """
540
        with self.pantsd_test_context() as (workdir, config, checker):
1✔
541
            client_handle, waiter_pid, child_pid, _ = launch_waiter(
1✔
542
                workdir=workdir, config=config, cleanup_wait_time=cleanup_wait_time
543
            )
544
            client_pid = client_handle.process.pid
1✔
545
            waiter_process = psutil.Process(waiter_pid)
1✔
546
            child_process = psutil.Process(waiter_pid)
1✔
547

548
            assert waiter_process.is_running()
1✔
549
            assert child_process.is_running()
1✔
550
            checker.assert_started()
1✔
551

552
            # give time to enter the try/finally block in the child process
553
            time.sleep(5)
1✔
554

555
            # This should kill the client, which will cancel the run on the server, which will
556
            # kill the waiting process and its child.
557
            os.kill(client_pid, signum)
1✔
558
            client_run = client_handle.join()
1✔
559
            client_run.assert_failure()
1✔
560

561
            for regexp in regexps or []:
1✔
562
                self.assertRegex(client_run.stderr, regexp)
1✔
563

564
            for regexp in not_regexps or []:
1✔
565
                self.assertNotRegex(client_run.stderr, regexp)
1✔
566

567
            # pantsd should still be running, but the waiter process and child should have been
568
            # killed.
569
            time.sleep(5)
1✔
570
            assert not waiter_process.is_running()
1✔
571
            assert not child_process.is_running()
1✔
572
            checker.assert_running()
1✔
573

574
    def test_pantsd_graceful_shutdown(self):
1✔
575
        """Test that SIGINT is propagated to child processes and they are given time to shutdown."""
576
        self._assert_pantsd_keyboardinterrupt_signal(
1✔
577
            signal.SIGINT,
578
            regexps=[
579
                "Interrupted by user.",
580
                "keyboard int received",
581
                "waiter cleaning up",
582
                "waiter cleanup complete",
583
            ],
584
            cleanup_wait_time=0,
585
        )
586

587
    def test_pantsd_graceful_shutdown_deadline(self):
1✔
588
        """Test that a child process that does not respond to SIGINT within 5 seconds, is forcibly
589
        cleaned up with a SIGKILL."""
590
        self._assert_pantsd_keyboardinterrupt_signal(
1✔
591
            signal.SIGINT,
592
            regexps=[
593
                "Interrupted by user.",
594
                "keyboard int received",
595
                "waiter cleaning up",
596
            ],
597
            not_regexps=[
598
                "waiter cleanup complete",
599
            ],
600
            cleanup_wait_time=6,
601
        )
602

603
    def test_sigint_kills_request_waiting_for_lock(self):
1✔
604
        """Test that, when a pailgun request is blocked waiting for another one to end, sending
605
        SIGINT to the blocked run will kill it."""
606
        config = {"GLOBAL": {"pantsd_timeout_when_multiple_invocations": -1.0, "level": "debug"}}
1✔
607
        with self.pantsd_test_context(extra_config=config) as (workdir, config, checker):
1✔
608
            # Run a process that will wait forever.
609
            first_run_handle, _, _, file_to_create = launch_waiter(workdir=workdir, config=config)
1✔
610

611
            checker.assert_started()
1✔
612
            checker.assert_running()
1✔
613

614
            # And another that will block on the first.
615
            blocking_run_handle = self.run_pants_with_workdir_without_waiting(
1✔
616
                command=["goals"], workdir=workdir, config=config
617
            )
618

619
            # Block until the second request is waiting for the lock.
620
            time.sleep(10)
1✔
621

622
            # Sends SIGINT to the run that is waiting.
623
            blocking_run_client_pid = blocking_run_handle.process.pid
1✔
624
            os.kill(blocking_run_client_pid, signal.SIGINT)
1✔
625
            blocking_run_handle.join()
1✔
626

627
            # Check that pantsd is still serving the other request.
628
            checker.assert_running()
1✔
629

630
            # Exit the second run by writing the file it is waiting for, and confirm that it
631
            # exited, and that pantsd is still running.
632
            safe_file_dump(file_to_create, "content!")
1✔
633
            result = first_run_handle.join()
1✔
634
            result.assert_success()
1✔
635
            checker.assert_running()
1✔
636

637
    def test_pantsd_unicode_environment(self):
1✔
638
        with self.pantsd_successful_run_context(extra_env={"XXX": "¡"}) as ctx:
1✔
639
            result = ctx.runner(["help"])
1✔
640
            ctx.checker.assert_started()
1✔
641
            result.assert_success()
1✔
642

643
    # This is a regression test for a bug where we would incorrectly detect a cycle if two targets swapped their
644
    # dependency relationship (#7404).
645
    def test_dependencies_swap(self):
1✔
646
        template = dedent(
1✔
647
            """
648
            python_source(
649
              name='A',
650
              source='A.py',
651
              {a_deps}
652
            )
653

654
            python_source(
655
              name='B',
656
              source='B.py',
657
              {b_deps}
658
            )
659
            """
660
        )
661

662
        with self.pantsd_successful_run_context() as ctx:
1✔
663
            tmp_path = Path(ctx.workdir).parent
1✔
664
            relative_target_path = tmp_path.relative_to(tmp_path.parent)
1✔
665

666
            safe_file_dump(os.path.join(relative_target_path, "A.py"), mode="w")
1✔
667
            safe_file_dump(os.path.join(relative_target_path, "B.py"), mode="w")
1✔
668

669
            def list_and_verify(a_deps: str, b_deps: str) -> None:
1✔
670
                Path(relative_target_path, "BUILD").write_text(
1✔
671
                    template.format(a_deps=a_deps, b_deps=b_deps)
672
                )
673
                result = ctx.runner(["list", f"{relative_target_path}:"])
1✔
674
                ctx.checker.assert_started()
1✔
675
                result.assert_success()
1✔
676
                expected_targets = {f"{relative_target_path}:{target}" for target in ("A", "B")}
1✔
677
                assert expected_targets == set(result.stdout.strip().split("\n"))
1✔
678

679
            list_and_verify(a_deps='dependencies = [":B"],', b_deps="")
1✔
680
            list_and_verify(a_deps="", b_deps='dependencies = [":A"],')
1✔
681

682
    def test_concurrent_overrides_pantsd(self):
1✔
683
        """Tests that the --concurrent flag overrides the --pantsd flag, because we don't allow
684
        concurrent runs under pantsd."""
685
        config = {"GLOBAL": {"concurrent": True, "pantsd": True}}
1✔
686
        with temporary_workdir() as workdir:
1✔
687
            pants_run = self.run_pants_with_workdir(
1✔
688
                ["-ldebug", "help", "goals"], workdir=workdir, config=config
689
            )
690
            pants_run.assert_success()
1✔
691
            self.assertNotIn("Connecting to pantsd", pants_run.stderr)
1✔
692

693
    def test_unhandled_exceptions_only_log_exceptions_once(self):
1✔
694
        """Tests that the unhandled exceptions triggered by LocalPantsRunner instances don't
695
        manifest as a PantsRunFinishedWithFailureException.
696

697
        That is, that we unset the global Exiter override set by LocalPantsRunner before we try to log the exception.
698

699
        This is a regression test for the most glaring case of https://github.com/pantsbuild/pants/issues/7597.
700
        """
701
        with self.pantsd_run_context(success=False) as ctx:
1✔
702
            tmp_path = Path(ctx.workdir).parent
1✔
703
            relative_target_path = tmp_path.relative_to(tmp_path.parent)
1✔
704

705
            Path(relative_target_path, "BUILD").write_text(
1✔
706
                dedent(
707
                    """\
708
                    python_requirement(name="badreq", requirements=["badreq==99.99.99"])
709
                    pex_binary(name="pex", dependencies=[":badreq"])
710
                    """
711
                )
712
            )
713
            result = ctx.runner(["package", f"{relative_target_path}:pex"])
1✔
714
            ctx.checker.assert_running()
1✔
715
            result.assert_failure()
1✔
716
            # Assert that the desired exception has been triggered once.
717
            self.assertRegex(result.stderr, r"ERROR:.*badreq==99.99.99")
1✔
718
            # Assert that it has only been triggered once.
719
            assert (
1✔
720
                "During handling of the above exception, another exception occurred:"
721
                not in result.stderr
722
            )
723
            assert (
1✔
724
                "pants.bin.daemon_pants_runner._PantsRunFinishedWithFailureException: Terminated with 1"
725
                not in result.stderr
726
            )
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