Skip to content

Commit d0ff5e4

Browse files
authored
Merge pull request #989 from mlco2/bug/csv_ordering
Sort CSV headers before comparing so that we don't spuriously create backup output files.
2 parents be73aec + 5b8a47b commit d0ff5e4

8 files changed

Lines changed: 309 additions & 24 deletions

File tree

codecarbon/output_methods/file.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ def has_valid_headers(self, data: EmissionsData) -> bool:
6868
# No entries
6969
return True
7070
dict_from_csv = dict(csv_entries_list[0])
71-
list_of_column_names = list(dict_from_csv.keys())
72-
return list(data.values.keys()) == list_of_column_names
71+
list_of_column_names = sorted(dict_from_csv.keys())
72+
return sorted(data.values.keys()) == list_of_column_names
7373

74-
def out(self, total: EmissionsData, _: EmissionsData):
74+
def out(self, total: EmissionsData, _):
7575
"""
7676
Save the emissions data from a whole run to a CSV file.
7777

codecarbon/output_methods/http.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class HTTPOutput(BaseOutput):
1919
def __init__(self, endpoint_url: str):
2020
self.endpoint_url: str = endpoint_url
2121

22-
def out(self, total: EmissionsData, delta: EmissionsData):
22+
def out(self, total: EmissionsData, _: EmissionsData):
2323
try:
2424
payload = dataclasses.asdict(total)
2525
payload["user"] = getpass.getuser()
@@ -56,14 +56,14 @@ def __init__(
5656
)
5757
self.run_id = self.api.run_id
5858

59-
def live_out(self, total: EmissionsData, delta: EmissionsData):
59+
def live_out(self, _, delta: EmissionsData):
6060
# Called at regular intervals
6161
try:
6262
self.api.add_emission(dataclasses.asdict(delta))
6363
except Exception as e:
6464
logger.error(e, exc_info=True)
6565

66-
def out(self, total: EmissionsData, delta: EmissionsData):
66+
def out(self, _, delta: EmissionsData):
6767
# Called on exit
6868
try:
6969
self.api.add_emission(dataclasses.asdict(delta))

codecarbon/output_methods/metrics/logfire.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def __init__(self):
5959
"codecarbon_ram_energy", unit="(kWh)", description="Energy used per RAM"
6060
)
6161

62-
def out(self, total: EmissionsData, delta: EmissionsData):
62+
def out(self, _, delta: EmissionsData):
6363
try:
6464
self.duration.add(delta.duration)
6565
self.emissions.add(delta.emissions)
@@ -75,5 +75,5 @@ def out(self, total: EmissionsData, delta: EmissionsData):
7575
except Exception as e:
7676
logger.error(e, exc_info=True)
7777

78-
def live_out(self, total: EmissionsData, delta: EmissionsData):
79-
self.out(total, delta)
78+
def live_out(self, _: EmissionsData, delta: EmissionsData):
79+
self.out(None, delta)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ dev = [
9090
"requests",
9191
"requests-mock",
9292
"responses",
93+
"logfire>=1.0.1",
9394
]
9495
doc = [
9596
"sphinx",

tests/output_methods/test_file.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import shutil
33
import tempfile
44
import unittest
5-
from unittest.mock import MagicMock, patch
5+
from unittest.mock import patch
66

77
import pandas as pd
88

@@ -66,7 +66,7 @@ def test_file_output_initialization_invalid_dir(self):
6666

6767
def test_has_valid_headers_success(self):
6868
file_output = FileOutput("test.csv", self.temp_dir)
69-
file_output.out(self.emissions_data, MagicMock())
69+
file_output.out(self.emissions_data, None)
7070

7171
self.assertTrue(file_output.has_valid_headers(self.emissions_data))
7272

@@ -77,9 +77,19 @@ def test_has_valid_headers_success_with_empty_file(self):
7777

7878
self.assertTrue(file_output.has_valid_headers(self.emissions_data))
7979

80+
def test_has_valid_headers_different_order_success(self):
81+
file_output = FileOutput("test.csv", self.temp_dir)
82+
file_output.out(self.emissions_data, None)
83+
84+
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
85+
df = df[list(reversed(df.columns))]
86+
df.to_csv(os.path.join(self.temp_dir, "test.csv"), index=False)
87+
88+
self.assertTrue(file_output.has_valid_headers(self.emissions_data))
89+
8090
def test_has_valid_headers_failure(self):
8191
file_output = FileOutput("test.csv", self.temp_dir)
82-
file_output.out(self.emissions_data, MagicMock())
92+
file_output.out(self.emissions_data, None)
8393

8494
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
8595
df.rename(columns={"wue": "new_header"}, inplace=True)
@@ -90,10 +100,10 @@ def test_has_valid_headers_failure(self):
90100
@patch("codecarbon.output_methods.file.FileOutput.has_valid_headers")
91101
def test_file_output_out_file_exists_invalid_headers(self, mock_has_valid_headers):
92102
file_output = FileOutput("test.csv", self.temp_dir, on_csv_write="append")
93-
file_output.out(self.emissions_data, MagicMock())
103+
file_output.out(self.emissions_data, None)
94104

95105
mock_has_valid_headers.return_value = False
96-
file_output.out(self.emissions_data, MagicMock())
106+
file_output.out(self.emissions_data, None)
97107

98108
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv.bak"))
99109
self.assertEqual(len(df), 1)
@@ -102,63 +112,67 @@ def test_file_output_out_file_exists_invalid_headers(self, mock_has_valid_header
102112

103113
def test_file_output_out_update_no_file_exists(self):
104114
file_output = FileOutput("test.csv", self.temp_dir, on_csv_write="update")
105-
file_output.out(self.emissions_data, MagicMock())
115+
file_output.out(self.emissions_data, None)
106116

107117
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
108118
self.assertEqual(len(df), 1)
109119

110120
def test_file_output_out_append_no_file_exists(self):
111121
file_output = FileOutput("test.csv", self.temp_dir, on_csv_write="append")
112-
file_output.out(self.emissions_data, MagicMock())
122+
file_output.out(self.emissions_data, None)
113123

114124
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
115125
self.assertEqual(len(df), 1)
116126

117127
def test_file_output_out_append_file_exists(self):
118128
file_output = FileOutput("test.csv", self.temp_dir, on_csv_write="append")
119-
file_output.out(self.emissions_data, MagicMock())
120-
file_output.out(self.emissions_data, MagicMock())
129+
file_output.out(self.emissions_data, None)
130+
file_output.out(self.emissions_data, None)
121131

122132
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
123133
self.assertEqual(len(df), 2)
124134

125135
def test_file_output_out_update_file_exists_no_matching_row(self):
126136
file_output = FileOutput("test.csv", self.temp_dir, on_csv_write="update")
127-
file_output.out(self.emissions_data, MagicMock())
137+
file_output.out(self.emissions_data, None)
128138

129139
updated_emissions_data = self.emissions_data
130140
updated_emissions_data.run_id = "new_test_run_id"
131-
file_output.out(updated_emissions_data, MagicMock())
141+
file_output.out(updated_emissions_data, None)
132142

133143
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
134144
self.assertEqual(len(df), 2)
135145

136146
def test_file_output_out_update_file_exists_multiple_matching_rows(self):
137147
file_output = FileOutput("test.csv", self.temp_dir, on_csv_write="update")
138-
file_output.out(self.emissions_data, MagicMock())
148+
file_output.out(self.emissions_data, None)
139149

140150
# Manually add a duplicate row to simulate the condition
141151
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
142152
df = pd.concat([df, df])
143153
df.to_csv(os.path.join(self.temp_dir, "test.csv"), index=False)
144154

145-
file_output.out(self.emissions_data, MagicMock())
155+
file_output.out(self.emissions_data, None)
146156

147157
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
148158
self.assertEqual(len(df), 3)
149159

150160
def test_file_output_out_update_file_exists_one_matchingrows(self):
151161
file_output = FileOutput("test.csv", self.temp_dir, on_csv_write="update")
152-
file_output.out(self.emissions_data, MagicMock())
162+
file_output.out(self.emissions_data, None)
153163
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
154164
self.assertEqual(df["cpu_power"].iloc[0], 20)
155165

156166
new_data = self.emissions_data
157167
new_data.cpu_power = 2
158-
file_output.out(new_data, MagicMock())
168+
file_output.out(new_data, None)
159169
df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
160170
self.assertEqual(df["cpu_power"].iloc[0], 2)
161171

172+
# def test_file_output_out_consistent_column_ordering(self):
173+
# file_output = FileOutput("test.csv", self.temp_dir, on_csv_write="append")
174+
# file_output.out(self.emissions_data, None)
175+
162176
def test_file_output_task_out(self):
163177
task_emissions_data = [
164178
TaskEmissionsData(

tests/output_methods/test_http.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import unittest
2+
from unittest.mock import MagicMock, patch
3+
4+
from codecarbon.output_methods.emissions_data import EmissionsData
5+
from codecarbon.output_methods.http import CodeCarbonAPIOutput, HTTPOutput
6+
7+
8+
class TestHTTPOutput(unittest.TestCase):
9+
def setUp(self):
10+
self.emissions_data = EmissionsData(
11+
timestamp="2023-01-01T00:00:00",
12+
project_name="test_project",
13+
run_id="test_run_id",
14+
experiment_id="test_experiment_id",
15+
duration=10,
16+
emissions=0.5,
17+
emissions_rate=0.05,
18+
cpu_power=20,
19+
gpu_power=30,
20+
ram_power=5,
21+
cpu_energy=200,
22+
gpu_energy=300,
23+
ram_energy=50,
24+
energy_consumed=550,
25+
water_consumed=0.1,
26+
country_name="Testland",
27+
country_iso_code="TS",
28+
region="Test Region",
29+
cloud_provider="Test Cloud",
30+
cloud_region="test-cloud-1",
31+
os="TestOS",
32+
python_version="3.8",
33+
codecarbon_version="2.0",
34+
cpu_count=4,
35+
cpu_model="Test CPU",
36+
gpu_count=1,
37+
gpu_model="Test GPU",
38+
longitude=0,
39+
latitude=0,
40+
ram_total_size=16,
41+
tracking_mode="machine",
42+
on_cloud="true",
43+
pue=1.5,
44+
wue=0.5,
45+
)
46+
self.url = "http://test.com/emissions"
47+
self.http_output = HTTPOutput(endpoint_url=self.url)
48+
49+
@patch(
50+
"codecarbon.output_methods.http.requests.post",
51+
return_value=MagicMock(status_code=201),
52+
)
53+
def test_http_output_post_success(self, mock_post):
54+
self.http_output.out(self.emissions_data, self.emissions_data)
55+
56+
mock_post.assert_called_once()
57+
self.assertEqual(mock_post.call_args[0][0], self.url)
58+
59+
@patch("codecarbon.output_methods.http.logger.warning")
60+
@patch(
61+
"codecarbon.output_methods.http.requests.post",
62+
return_value=MagicMock(status_code=418),
63+
)
64+
def test_http_output_post_unexpected_status(self, mock_post, mock_logger):
65+
self.http_output.out(self.emissions_data, self.emissions_data)
66+
67+
mock_post.assert_called_once()
68+
mock_logger.assert_called_once()
69+
70+
@patch("codecarbon.output_methods.http.logger.error")
71+
@patch(
72+
"codecarbon.output_methods.http.requests.post",
73+
side_effect=Exception("Test exception"),
74+
)
75+
def test_http_output_post_exception(self, mock_post, mock_logger):
76+
self.http_output.out(self.emissions_data, self.emissions_data)
77+
mock_post.assert_called_once()
78+
mock_logger.assert_called_once()
79+
80+
81+
class TestCodeCarbonAPIOutput(unittest.TestCase):
82+
def setUp(self):
83+
self.emissions_data = EmissionsData(
84+
timestamp="2023-01-01T00:00:00",
85+
project_name="test_project",
86+
run_id="test_run_id",
87+
experiment_id="test_experiment_id",
88+
duration=10,
89+
emissions=0.5,
90+
emissions_rate=0.05,
91+
cpu_power=20,
92+
gpu_power=30,
93+
ram_power=5,
94+
cpu_energy=200,
95+
gpu_energy=300,
96+
ram_energy=50,
97+
energy_consumed=550,
98+
water_consumed=0.1,
99+
country_name="Testland",
100+
country_iso_code="TS",
101+
region="Test Region",
102+
cloud_provider="Test Cloud",
103+
cloud_region="test-cloud-1",
104+
os="TestOS",
105+
python_version="3.8",
106+
codecarbon_version="2.0",
107+
cpu_count=4,
108+
cpu_model="Test CPU",
109+
gpu_count=1,
110+
gpu_model="Test GPU",
111+
longitude=0,
112+
latitude=0,
113+
ram_total_size=16,
114+
tracking_mode="machine",
115+
on_cloud="true",
116+
pue=1.5,
117+
wue=0.5,
118+
)
119+
self.url = "http://test.com/emissions"
120+
self.experiment_id = (
121+
None # Set to None so that ApiClient won't attempt a run on initialisation
122+
)
123+
self.api_key = "test_key"
124+
125+
self.add_emission_patcher = patch(
126+
"codecarbon.output_methods.http.ApiClient.add_emission"
127+
)
128+
self.mock_add_emission = self.add_emission_patcher.start()
129+
self.addCleanup(self.add_emission_patcher.stop)
130+
131+
def test_codecarbon_api_output_initialization(self):
132+
CodeCarbonAPIOutput(
133+
endpoint_url=self.url,
134+
experiment_id=self.experiment_id,
135+
api_key=self.api_key,
136+
conf=None,
137+
)
138+
139+
def test_codecarbon_api_live_out(self):
140+
api_output = CodeCarbonAPIOutput(
141+
endpoint_url=self.url,
142+
experiment_id=self.experiment_id,
143+
api_key=self.api_key,
144+
conf=None,
145+
)
146+
147+
api_output.live_out(None, self.emissions_data)
148+
self.mock_add_emission.assert_called_once()
149+
150+
@patch("codecarbon.output_methods.http.logger.error")
151+
def test_codecarbon_live_out_api_call_failure(self, mock_logger):
152+
self.mock_add_emission.side_effect = Exception("Test exception")
153+
api_output = CodeCarbonAPIOutput(
154+
endpoint_url=self.url,
155+
experiment_id=self.experiment_id,
156+
api_key=self.api_key,
157+
conf=None,
158+
)
159+
api_output.live_out(None, self.emissions_data)
160+
mock_logger.assert_called_once()
161+
162+
def test_codecarbon_api_out(self):
163+
api_output = CodeCarbonAPIOutput(
164+
endpoint_url=self.url,
165+
experiment_id=self.experiment_id,
166+
api_key=self.api_key,
167+
conf=None,
168+
)
169+
170+
api_output.out(None, self.emissions_data)
171+
self.mock_add_emission.assert_called_once()
172+
173+
@patch("codecarbon.output_methods.http.logger.error")
174+
def test_codecarbon_out_api_call_failure(self, mock_logger):
175+
self.mock_add_emission.side_effect = Exception("Test exception")
176+
api_output = CodeCarbonAPIOutput(
177+
endpoint_url=self.url,
178+
experiment_id=self.experiment_id,
179+
api_key=self.api_key,
180+
conf=None,
181+
)
182+
api_output.out(None, self.emissions_data)
183+
mock_logger.assert_called_once()

0 commit comments

Comments
 (0)