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, )