From c4561a3dc3ca11316dacb250d00c8abf07b1844d Mon Sep 17 00:00:00 2001 From: NoiceHax Date: Sat, 15 Aug 2026 13:26:23 +0530 Subject: [PATCH] test: stop the flush and logging output tests sharing files tests/test_logging_output.py only asserts on the logger output, but its trackers were left with the default CSV output, so every run also appended to emissions.csv in the working directory. _persist_data calls the handlers in a loop and the CSV handler runs first, so once anything went wrong with that shared file the LoggerOutput handler was never reached. flush() and stop() are wrapped in @suppress(Exception), so the failure was silent and showed up later as a row count assertion, which is what #1371 reports. Pass save_to_file=False to the three trackers there and check in tearDown that emissions.csv in the working directory was not touched. tests/test_emissions_tracker_flush.py pointed every test at one fixed file name under tempfile.gettempdir(), so two tests counting rows could count in the same file. Give each test its own temporary directory instead. Also add an autouse fixture that stops PeriodicScheduler timers a test left armed. That is only a net, tests are still expected to stop their trackers. --- tests/conftest.py | 20 ++++++++++++++++++++ tests/test_emissions_tracker_flush.py | 12 +++++------- tests/test_logging_output.py | 27 ++++++++++++++++++++++----- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5d2ddbfe0..ae847a8b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,11 @@ """Shared pytest fixtures for the CodeCarbon test suite.""" +import threading + import pytest from codecarbon.core.hardware_cache import clear_cache as clear_hardware_cache +from codecarbon.external.scheduler import PeriodicScheduler @pytest.fixture(autouse=True) @@ -20,3 +23,20 @@ def _reset_process_hardware_cache(): yield clear_hardware_cache() detect_cpu_model.cache_clear() + + +@pytest.fixture(autouse=True) +def _stop_leaked_schedulers(): + """Stop schedulers a test left running. + + A tracker that is never stopped keeps re-arming a timer that then fires + into whatever test runs next. This is only a safety net: tests are still + expected to stop the trackers they start. + """ + yield + for thread in threading.enumerate(): + if not isinstance(thread, threading.Timer): + continue + scheduler = getattr(thread.function, "__self__", None) + if isinstance(scheduler, PeriodicScheduler): + scheduler.stop() diff --git a/tests/test_emissions_tracker_flush.py b/tests/test_emissions_tracker_flush.py index 63c394e76..6c568b374 100644 --- a/tests/test_emissions_tracker_flush.py +++ b/tests/test_emissions_tracker_flush.py @@ -24,16 +24,14 @@ class TestCarbonTrackerFlush(unittest.TestCase): def setUp(self) -> None: self.project_name = "project_TestCarbonTrackerFlush" self.emissions_file = "emissions-test-TestCarbonTrackerFlush.csv" - self.emissions_path = tempfile.gettempdir() + # A per-test directory, so that two tests counting rows in "their" file + # can never end up counting rows in the same one, see #1371. + self._temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self._temp_dir.cleanup) + self.emissions_path = self._temp_dir.name self.emissions_file_path = os.path.join( self.emissions_path, self.emissions_file ) - if os.path.isfile(self.emissions_file_path): - os.remove(self.emissions_file_path) - - def tearDown(self) -> None: - if os.path.isfile(self.emissions_file_path): - os.remove(self.emissions_file_path) def test_carbon_tracker_online_flush(self): tracker = EmissionsTracker( diff --git a/tests/test_logging_output.py b/tests/test_logging_output.py index a06fd7a5e..bdf10a52e 100644 --- a/tests/test_logging_output.py +++ b/tests/test_logging_output.py @@ -4,6 +4,7 @@ import tempfile import time import unittest +from pathlib import Path from codecarbon.emissions_tracker import ( EmissionsTracker, @@ -25,28 +26,42 @@ class TestCarbonTrackerFlush(unittest.TestCase): def setUp(self) -> None: self.project_name = "project_TestCarbonLoggingOutput" self.emissions_logfile = "emissions-test-TestCarbonLoggingOutput.log" - self.emissions_path = tempfile.gettempdir() + self._temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self._temp_dir.cleanup) + self.emissions_path = self._temp_dir.name self.emissions_file_path = os.path.join( self.emissions_path, self.emissions_logfile ) - if os.path.isfile(self.emissions_file_path): - os.remove(self.emissions_file_path) self._test_logger = logging.getLogger(self.project_name) _channel = logging.FileHandler(self.emissions_file_path) self._test_logger.addHandler(_channel) self._test_logger.setLevel(logging.INFO) self.external_logger = LoggerOutput(self._test_logger, logging.INFO) + self._working_dir_csv = Path.cwd() / "emissions.csv" + self._working_dir_csv_size = self._read_working_dir_csv_size() def tearDown(self) -> None: for handler in self._test_logger.handlers[:]: self._test_logger.removeHandler(handler) handler.close() - if os.path.isfile(self.emissions_file_path): - os.remove(self.emissions_file_path) + # These tests only assert on the logger output, so they must not write + # the CSV in the working directory too. Sharing that file with the rest + # of the suite is what made them order dependent, see #1371. + self.assertEqual( + self._working_dir_csv_size, + self._read_working_dir_csv_size(), + "the tracker wrote to emissions.csv in the working directory", + ) + + def _read_working_dir_csv_size(self): + if not self._working_dir_csv.is_file(): + return None + return self._working_dir_csv.stat().st_size def test_carbon_tracker_online_logging_output(self): tracker = EmissionsTracker( project_name=self.project_name, + save_to_file=False, save_to_logger=True, logging_logger=self.external_logger, ) @@ -64,6 +79,7 @@ def test_carbon_tracker_offline_logging_output(self): tracker = OfflineEmissionsTracker( project_name=self.project_name, country_iso_code="USA", + save_to_file=False, save_to_logger=True, logging_logger=self.external_logger, ) @@ -80,6 +96,7 @@ def test_carbon_tracker_offline_logging_output(self): def test_decorator_flush(self): @track_emissions( project_name=self.project_name, + save_to_file=False, save_to_logger=True, logging_logger=self.external_logger, )