Skip to content

Commit 77e0ba4

Browse files
FindHaometa-codesync[bot]
authored andcommitted
Keep bisect log handlers isolated when instance IDs are reused (#439)
Summary: Pull Request resolved: #439 Creating a BisectLogger with a reused timestamp and object ID can retrieve a previous instance's logger from Python's global logging registry. Its closed handlers then reopen an old log file, mixing output between sessions or raising FileNotFoundError after the old temporary directory has been removed. This caused three setUp errors in GitHub Actions Tests #927: https://github.com/meta-pytorch/tritonparse/actions/runs/35882624122/job/107254643368 Give each BisectLogger its own logging.Logger and detach and close test handlers before removing temporary directories. Add a deterministic regression that forces object ID reuse and covers both retained and deleted previous log directories. ___ Differential Revision: D121418371 fbshipit-source-id: 5f784470861c3cf86c1fb7b51e465364eb1ed500
1 parent 0735330 commit 77e0ba4

4 files changed

Lines changed: 56 additions & 18 deletions

File tree

‎tests/cpu/test_bisect_executor.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ class ShellExecutorTest(unittest.TestCase):
6565
def setUp(self):
6666
"""Set up test fixtures."""
6767
self.temp_dir = tempfile.mkdtemp()
68+
self.addCleanup(shutil.rmtree, self.temp_dir)
6869
self.logger = BisectLogger(self.temp_dir)
70+
for handler in self.logger.logger.handlers:
71+
self.addCleanup(handler.close)
72+
self.addCleanup(self.logger.logger.removeHandler, handler)
6973
self.executor = ShellExecutor(self.logger)
7074

71-
def tearDown(self):
72-
"""Clean up test fixtures."""
73-
shutil.rmtree(self.temp_dir)
74-
7575
def test_run_command_success(self):
7676
"""Test run_command executes command and returns result."""
7777
result = self.executor.run_command(["echo", "hello"])

‎tests/cpu/test_bisect_logger.py‎

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import shutil
66
import tempfile
77
import unittest
8+
from pathlib import Path
9+
from unittest.mock import patch
810

911
from tritonparse.bisect import BisectLogger
1012

@@ -15,40 +17,48 @@ class BisectLoggerTest(unittest.TestCase):
1517
def setUp(self):
1618
"""Set up test fixtures."""
1719
self.temp_dir = tempfile.mkdtemp()
20+
self.addCleanup(shutil.rmtree, self.temp_dir)
1821

19-
def tearDown(self):
20-
"""Clean up test fixtures."""
21-
shutil.rmtree(self.temp_dir)
22+
def _make_logger(self, log_dir=None, **kwargs):
23+
logger = BisectLogger(str(log_dir or self.temp_dir), **kwargs)
24+
self.addCleanup(self._close_logger, logger)
25+
return logger
26+
27+
@staticmethod
28+
def _close_logger(logger):
29+
for handler in logger.logger.handlers[:]:
30+
logger.logger.removeHandler(handler)
31+
handler.close()
2232

2333
def test_creates_log_directory_and_files(self):
2434
"""Test that logger creates log directory and module log file."""
25-
logger = BisectLogger(self.temp_dir)
35+
logger = self._make_logger()
2636
self.assertTrue(logger.module_log_path.exists())
2737
self.assertTrue(logger.log_dir.exists())
2838

2939
def test_auto_generates_session_name(self):
3040
"""Test that session name is auto-generated if not provided."""
31-
logger = BisectLogger(self.temp_dir)
41+
logger = self._make_logger()
3242
self.assertIsNotNone(logger.session_name)
3343
self.assertIn(logger.session_name, str(logger.module_log_path))
3444

3545
def test_uses_provided_session_name(self):
3646
"""Test that provided session name is used."""
37-
logger = BisectLogger(self.temp_dir, session_name="test_session")
47+
logger = self._make_logger(session_name="test_session")
3848
self.assertEqual(logger.session_name, "test_session")
3949
self.assertIn("test_session", str(logger.module_log_path))
4050

4151
def test_output_callback_receives_lines(self):
4252
"""Test that output_callback receives each line of command output."""
4353
captured = []
44-
logger = BisectLogger(self.temp_dir, output_callback=captured.append)
54+
logger = self._make_logger(output_callback=captured.append)
4555
logger.log_command_output("test_cmd", "line1\nline2\nline3", 0)
4656
self.assertEqual(captured, ["line1", "line2", "line3"])
4757

4858
def test_tui_callback_on_info(self):
4959
"""Test that info() triggers TUI callback when configured."""
5060
tui_msgs = []
51-
logger = BisectLogger(self.temp_dir)
61+
logger = self._make_logger()
5262
logger.configure_for_tui(tui_msgs.append)
5363
logger.info("test message")
5464
self.assertEqual(len(tui_msgs), 1)
@@ -57,21 +67,49 @@ def test_tui_callback_on_info(self):
5767
def test_tui_callback_not_triggered_by_debug(self):
5868
"""Test that debug() does NOT trigger TUI callback (too verbose)."""
5969
tui_msgs = []
60-
logger = BisectLogger(self.temp_dir)
70+
logger = self._make_logger()
6171
logger.configure_for_tui(tui_msgs.append)
6272
logger.debug("debug message")
6373
# TUI callback should not be called for debug messages
6474
self.assertEqual(len(tui_msgs), 0)
6575

6676
def test_log_command_output_writes_to_file(self):
6777
"""Test that log_command_output writes to command log file."""
68-
logger = BisectLogger(self.temp_dir)
78+
logger = self._make_logger()
6979
logger.log_command_output("echo hello", "hello world", 0)
7080
self.assertTrue(logger.command_log_path.exists())
7181
content = logger.command_log_path.read_text()
7282
self.assertIn("echo hello", content)
7383
self.assertIn("hello world", content)
7484

85+
def test_reused_instance_id_keeps_logs_isolated(self):
86+
"""A replacement logger must not reopen a previous instance's log file."""
87+
for remove_previous_directory in (False, True):
88+
with self.subTest(remove_previous_directory=remove_previous_directory):
89+
previous_dir = Path(self.temp_dir) / f"old_{remove_previous_directory}"
90+
current_dir = Path(self.temp_dir) / f"new_{remove_previous_directory}"
91+
session_name = f"reused_id_{remove_previous_directory}"
92+
# Force ID reuse without depending on allocator timing.
93+
with patch("tritonparse.bisect.logger.id", return_value=1, create=True):
94+
previous = self._make_logger(
95+
previous_dir, session_name=session_name
96+
)
97+
# Reproduce the original cleanup: close without detaching.
98+
for handler in previous.logger.handlers:
99+
handler.close()
100+
if remove_previous_directory:
101+
shutil.rmtree(previous_dir)
102+
103+
current = self._make_logger(current_dir, session_name=session_name)
104+
message = "message from the replacement logger"
105+
current.info(message)
106+
107+
self.assertIn(message, current.module_log_path.read_text())
108+
if remove_previous_directory:
109+
self.assertFalse(previous_dir.exists())
110+
else:
111+
self.assertNotIn(message, previous.module_log_path.read_text())
112+
75113

76114
if __name__ == "__main__":
77115
unittest.main()

‎tests/cpu/test_bisect_results.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def setUp(self):
142142
self.logger = BisectLogger(str(self.root / "logs"))
143143
for handler in self.logger.logger.handlers:
144144
self.addCleanup(handler.close)
145+
self.addCleanup(self.logger.logger.removeHandler, handler)
145146
self.executor = ShellExecutor(self.logger)
146147
self._git("init", "-q")
147148
self._git("config", "user.name", "Tritonparse test")

‎tritonparse/bisect/logger.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,10 @@ def set_output_callback(self, callback: Optional[Callable[[str], None]]) -> None
8888

8989
def _setup_module_logger(self) -> None:
9090
"""Configure the Python logging system with file and stdout handlers."""
91-
# Use unique logger name with instance id to avoid sharing handlers
92-
# between different BisectLogger instances with the same session_name.
93-
# This ensures each instance logs to its own file path.
91+
# Own the logger instead of using the global getLogger() registry:
92+
# object IDs can be reused while that registry retains old handlers.
9493
self._logger_name = f"bisect.{self.session_name}.{id(self)}"
95-
self.logger = logging.getLogger(self._logger_name)
94+
self.logger = logging.Logger(self._logger_name)
9695
self.logger.setLevel(logging.DEBUG)
9796

9897
# Prevent propagation to root logger to avoid duplicate output

0 commit comments

Comments
 (0)