From c9af95246d9ba56816a85a55212413fdaf6f1ef3 Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Fri, 22 May 2026 23:42:55 +0200 Subject: [PATCH 1/3] Fix OSX psutil detection --- codecarbon/core/cpu.py | 12 ++++++------ tests/test_cpu.py | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/codecarbon/core/cpu.py b/codecarbon/core/cpu.py index 74549a14f..085480dc9 100644 --- a/codecarbon/core/cpu.py +++ b/codecarbon/core/cpu.py @@ -217,19 +217,19 @@ def is_psutil_available(): return True else: logger.debug( - f"is_psutil_available(): psutil.cpu_times().nice is too small: {nice}" + f"is_psutil_available(): psutil.cpu_times().nice is too small: {nice}; " + "using fallback check." ) - return False else: - # Fallback: check if psutil works by calling cpu_percent logger.debug( "is_psutil_available(): no 'nice' attribute, using fallback check." ) - # check CPU utilization usable - psutil.cpu_percent(interval=0.0, percpu=False) - return True + # Fallback: check if psutil CPU utilization is usable. This covers + # platforms like Windows and macOS where cpu_times().nice is absent or 0. + psutil.cpu_percent(interval=0.0, percpu=False) + return True except Exception as e: logger.debug( diff --git a/tests/test_cpu.py b/tests/test_cpu.py index 1e1308812..167f6c7d7 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -46,12 +46,24 @@ def test_is_psutil_available_with_nice(self, mock_cpu_times): self.assertTrue(is_psutil_available()) @mock.patch("psutil.cpu_times") - def test_is_psutil_available_with_small_nice(self, mock_cpu_times): - # Test when nice attribute is too small + def test_is_psutil_available_with_small_nice_uses_fallback(self, mock_cpu_times): + # Test when nice attribute is too small, as on macOS mock_times = mock.Mock() mock_times.nice = 0.00001 mock_cpu_times.return_value = mock_times - self.assertFalse(is_psutil_available()) + with mock.patch("psutil.cpu_percent") as mock_cpu_percent: + self.assertTrue(is_psutil_available()) + mock_cpu_percent.assert_called_once_with(interval=0.0, percpu=False) + + @mock.patch("psutil.cpu_times") + def test_is_psutil_not_available_when_small_nice_fallback_fails( + self, mock_cpu_times + ): + mock_times = mock.Mock() + mock_times.nice = 0.00001 + mock_cpu_times.return_value = mock_times + with mock.patch("psutil.cpu_percent", side_effect=Exception("Test error")): + self.assertFalse(is_psutil_available()) @mock.patch("psutil.cpu_times") def test_is_psutil_available_without_nice(self, mock_cpu_times): From 507821be322f4daa073c73797fce08ddf6257474 Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Sat, 23 May 2026 00:00:16 +0200 Subject: [PATCH 2/3] Coverage --- tests/test_cpu.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_cpu.py b/tests/test_cpu.py index 167f6c7d7..39234b729 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -78,6 +78,12 @@ def test_is_psutil_available_without_nice(self, mock_cpu_times): def test_is_psutil_not_available_on_exception(self, mock_cpu_times): self.assertFalse(is_psutil_available()) + def test_cpu_repr_includes_generic_tdp_marker(self): + cpu = CPU(output_dir="", mode="constant", model="My CPU", tdp=12) + cpu._is_generic_tdp = True + + assert repr(cpu) == "CPU(My CPU > 12W [generic])" + class TestRAPLHelperFunctions(unittest.TestCase): def test_get_candidate_bases_for_custom_dir(self): From 9aa8dd0ece17a918a5a75363a72d838e4aa91e78 Mon Sep 17 00:00:00 2001 From: benoit-cty Date: Sat, 23 May 2026 15:44:04 +0200 Subject: [PATCH 3/3] Coverage --- tests/test_cpu.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/test_cpu.py b/tests/test_cpu.py index 39234b729..b9acb5b59 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -78,12 +78,6 @@ def test_is_psutil_available_without_nice(self, mock_cpu_times): def test_is_psutil_not_available_on_exception(self, mock_cpu_times): self.assertFalse(is_psutil_available()) - def test_cpu_repr_includes_generic_tdp_marker(self): - cpu = CPU(output_dir="", mode="constant", model="My CPU", tdp=12) - cpu._is_generic_tdp = True - - assert repr(cpu) == "CPU(My CPU > 12W [generic])" - class TestRAPLHelperFunctions(unittest.TestCase): def test_get_candidate_bases_for_custom_dir(self): @@ -377,6 +371,18 @@ def test_get_cpu_power_from_registry(self): model = "AMD Ryzen Threadripper 1950X 16-Core Processor" self.assertEqual(tdp._get_cpu_power_from_registry(model), 180) + def test_get_cpu_power_from_registry_returns_none_without_match(self): + tdp = TDP.__new__(TDP) + with ( + mock.patch("codecarbon.core.cpu.DataSource") as mock_data_source, + mock.patch.object(tdp, "_get_matching_cpu", return_value=None), + ): + mock_data_source.return_value.get_cpu_power_data.return_value = ( + mock.sentinel.cpu_power_df + ) + + self.assertIsNone(tdp._get_cpu_power_from_registry("Mystery CPU")) + def test_get_matching_cpu(self): tdp = TDP() cpu_data = DataSource().get_cpu_power_data() @@ -564,6 +570,13 @@ def test_main_fallback_default_power_when_unknown_cpu(self): self.assertEqual(tdp.model, "Mystery CPU") self.assertEqual(tdp.tdp, 8 * DEFAULT_POWER_PER_CORE) + def test_main_returns_unknown_when_cpu_detection_fails(self): + with mock.patch("codecarbon.core.cpu.detect_cpu_model", return_value=None): + tdp = TDP() + + self.assertEqual(tdp.model, "Unknown") + self.assertIsNone(tdp.tdp) + class TestResourceTrackerCPUTracking(unittest.TestCase): def test_set_cpu_tracking_skips_tdp_when_rapl_available(self):