|
2 | 2 | # |
3 | 3 | # SPDX-License-Identifier: Apache-2.0 |
4 | 4 |
|
| 5 | +import gc |
| 6 | +import os |
5 | 7 | import re |
6 | 8 | import shutil |
7 | 9 | import subprocess |
8 | 10 | import sys |
| 11 | +import tempfile |
9 | 12 | import warnings |
10 | 13 |
|
11 | 14 | import pytest |
@@ -406,6 +409,55 @@ def test_program_init_invalid_code_format(): |
406 | 409 | Program(code, "c++") |
407 | 410 |
|
408 | 411 |
|
| 412 | +@pytest.mark.agent_authored(model="claude-fable-5-1") |
| 413 | +@pytest.mark.thread_unsafe(reason="replaces the process-global sys.unraisablehook and tempfile.mkstemp") |
| 414 | +@pytest.mark.parametrize("cleared_first", ["options", "program"]) |
| 415 | +def test_program_collected_in_reference_cycle_is_quiet(cleared_first, monkeypatch): |
| 416 | + """A Program collected as part of a reference cycle is destroyed quietly and removes its debug source (#2876). |
| 417 | +
|
| 418 | + The cyclic collector clears the attributes of every object in the cycle |
| 419 | + before it runs __dealloc__, in allocation order: either the ProgramOptions |
| 420 | + loses its fields first, or the Program loses its _options first. Neither |
| 421 | + may be consulted in __dealloc__. A failure there is only visible through |
| 422 | + sys.unraisablehook, so the hook is replaced to observe it. |
| 423 | + """ |
| 424 | + written = [] |
| 425 | + real_mkstemp = tempfile.mkstemp |
| 426 | + |
| 427 | + def recording_mkstemp(*args, **kwargs): |
| 428 | + fd, path = real_mkstemp(*args, **kwargs) |
| 429 | + written.append(path) |
| 430 | + return fd, path |
| 431 | + |
| 432 | + monkeypatch.setattr(tempfile, "mkstemp", recording_mkstemp) |
| 433 | + unraisable = [] |
| 434 | + monkeypatch.setattr(sys, "unraisablehook", unraisable.append) |
| 435 | + |
| 436 | + code = 'extern "C" __global__ void my_kernel() {}' |
| 437 | + # arch is passed explicitly so the current device is not queried; debug=True |
| 438 | + # makes the Program write its source to a temp file for cuda-gdb. |
| 439 | + options = ProgramOptions(arch="sm_90", debug=True) |
| 440 | + if cleared_first == "options": |
| 441 | + # The cycle is allocated between the options and the Program, so the |
| 442 | + # collector clears the ProgramOptions before it frees the Program. |
| 443 | + cycle = [] |
| 444 | + cycle.append(cycle) |
| 445 | + program = Program(code, "c++", options) |
| 446 | + cycle.append(program) |
| 447 | + else: |
| 448 | + # The cycle is allocated after the Program, so the collector clears the |
| 449 | + # Program's own attributes, including _options, before it frees it. |
| 450 | + program = Program(code, "c++", options) |
| 451 | + cycle = [program] |
| 452 | + cycle.append(cycle) |
| 453 | + assert written and all(os.path.exists(path) for path in written) |
| 454 | + del options, program, cycle |
| 455 | + gc.collect() |
| 456 | + |
| 457 | + assert unraisable == [] |
| 458 | + assert not any(os.path.exists(path) for path in written) |
| 459 | + |
| 460 | + |
409 | 461 | # arch is passed explicitly so the current device is not queried. |
410 | 462 | @pytest.mark.agent_authored(model="claude-opus-5") |
411 | 463 | @pytest.mark.parametrize("name", [None, "my_program"]) |
|
0 commit comments