@@ -656,3 +656,96 @@ def test_get_detected_hardware(
656656 self .assertIn ("gpu_count" , hardware_info )
657657 self .assertIn ("gpu_model" , hardware_info )
658658 self .assertIn ("gpu_ids" , hardware_info )
659+
660+
661+ class TestVariableEmissions (unittest .TestCase ):
662+ @mock .patch ("codecarbon.emissions_tracker.EmissionsTracker._get_geo_metadata" )
663+ @mock .patch ("codecarbon.emissions_tracker.EmissionsTracker._get_cloud_metadata" )
664+ @mock .patch ("codecarbon.core.electricitymaps_api.requests.get" )
665+ @mock .patch ("codecarbon.emissions_tracker.ResourceTracker" )
666+ @mock .patch (
667+ "codecarbon.emissions_tracker.BaseEmissionsTracker.get_detected_hardware"
668+ )
669+ @mock .patch ("codecarbon.emissions_tracker.PeriodicScheduler" )
670+ def test_cumulative_emissions_with_varying_intensity (
671+ self ,
672+ mock_scheduler ,
673+ mock_get_hw ,
674+ mock_resource_tracker ,
675+ mock_get ,
676+ mock_cloud ,
677+ mock_geo ,
678+ ):
679+ # Setup mocks
680+ mock_geo .return_value = mock .MagicMock (
681+ latitude = 1.0 ,
682+ longitude = 1.0 ,
683+ country_iso_code = "USA" ,
684+ country_2letter_iso_code = "US" ,
685+ )
686+ mock_cloud .return_value = mock .MagicMock (
687+ is_on_private_infra = True , provider = "azure" , region = "eastus"
688+ )
689+ mock_get_hw .return_value = {
690+ "ram_total_size" : 16.0 ,
691+ "cpu_count" : 8 ,
692+ "cpu_physical_count" : 4 ,
693+ "cpu_model" : "Mock CPU" ,
694+ "gpu_count" : 0 ,
695+ "gpu_model" : "None" ,
696+ "gpu_ids" : None ,
697+ }
698+
699+ # Mock Electricity Maps API responses with different intensities
700+ # 1st call: 100 g/kWh, 2nd call: 200 g/kWh, 3rd call: 300 g/kWh
701+ responses = [
702+ mock .MagicMock (status_code = 200 , json = lambda : {"carbonIntensity" : 100 }),
703+ mock .MagicMock (status_code = 200 , json = lambda : {"carbonIntensity" : 200 }),
704+ mock .MagicMock (status_code = 200 , json = lambda : {"carbonIntensity" : 300 }),
705+ ]
706+ mock_get .side_effect = responses
707+
708+ tracker = EmissionsTracker (
709+ electricitymaps_api_token = "test-token" ,
710+ save_to_file = False ,
711+ measure_power_secs = 1 ,
712+ allow_multiple_runs = True ,
713+ )
714+
715+ # Manually inject a mock hardware component
716+ mock_cpu = mock .MagicMock ()
717+ from codecarbon .external .hardware import CPU
718+
719+ mock_cpu .__class__ = CPU
720+ # Mock measure_power_and_energy: return 1kWh delta each time
721+ mock_cpu .measure_power_and_energy .return_value = (
722+ Power .from_watts (100 ),
723+ Energy .from_energy (kWh = 1.0 ),
724+ )
725+ tracker ._hardware = [mock_cpu ]
726+
727+ # Start tracking
728+ tracker .start ()
729+
730+ # Step 1
731+ tracker ._measure_power_and_energy ()
732+ # total_energy = 1.0, intensity = 100 => emissions = 0.1 kg
733+ data1 = tracker ._prepare_emissions_data ()
734+ self .assertAlmostEqual (data1 .emissions , 0.1 )
735+
736+ # Step 2
737+ tracker ._measure_power_and_energy ()
738+ # total_energy = 2.0, delta_energy = 1.0, intensity = 200 => delta_emissions = 0.2 kg
739+ # total_emissions = 0.1 + 0.2 = 0.3 kg
740+ data2 = tracker ._prepare_emissions_data ()
741+ self .assertAlmostEqual (data2 .emissions , 0.3 )
742+
743+ # Step 3
744+ tracker ._measure_power_and_energy ()
745+ # total_energy = 3.0, delta_energy = 1.0, intensity = 300 => delta_emissions = 0.3 kg
746+ # total_emissions = 0.3 + 0.3 = 0.6 kg
747+ data3 = tracker ._prepare_emissions_data ()
748+ self .assertAlmostEqual (data3 .emissions , 0.6 )
749+
750+ # Verification: If it wasn't cumulative, it would be 3.0 kWh * 300 g/kWh = 0.9 kg
751+ self .assertLess (data3 .emissions , 0.8 )
0 commit comments