@@ -59,6 +59,14 @@ def __init__(self, csv_path):
5959 self .commit = get_git_commit ()
6060 self .date = datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
6161 self .test_metrics = {} # test_name -> {metric_name -> [values]}
62+ self ._initialize_csv ()
63+
64+ def _initialize_csv (self ):
65+ """Initialize CSV file - will be written incrementally as tests complete"""
66+ self .csv_path .parent .mkdir (parents = True , exist_ok = True )
67+ # Clear the file at the start of a new test run
68+ with open (self .csv_path , "w" , newline = "" ) as f :
69+ pass # Create empty file, header will be written with first result
6270
6371 def add_result (self , test_name , passed , captured_output , metric_patterns ):
6472 self .test_metrics .setdefault (test_name , {}).setdefault ("passed" , []).append (
@@ -72,40 +80,70 @@ def add_result(self, test_name, passed, captured_output, metric_patterns):
7280 value = float (match .group ("value" ))
7381 self .test_metrics [test_name ].setdefault (metric_name , []).append (value )
7482
75- def finalize_results (self ):
76- """Compute statistics for all collected metrics"""
77- for test_name , data in self .test_metrics .items ():
78- row = {
79- "Commit" : self .commit ,
80- "Date" : self .date ,
81- "Test" : test_name ,
82- "Checks" : f"{ sum (data ['passed' ])} /{ len (data ['passed' ])} " ,
83- }
84- for metric_name , values in data .items ():
85- if metric_name == "passed" :
86- continue
87- if values :
88- row [f"{ metric_name } (mean)" ] = statistics .mean (values )
89- row [f"{ metric_name } (median)" ] = statistics .median (values )
90- row [f"{ metric_name } (min)" ] = min (values )
91- row [f"{ metric_name } (max)" ] = max (values )
92- row [f"{ metric_name } (stddev)" ] = (
93- statistics .stdev (values ) if len (values ) > 1 else 0.0
94- )
83+ def _compute_row (self , test_name , data ):
84+ """Compute statistics row for a single test"""
85+ row = {
86+ "Commit" : self .commit ,
87+ "Date" : self .date ,
88+ "Test" : test_name ,
89+ "Checks" : f"{ sum (data ['passed' ])} /{ len (data ['passed' ])} " ,
90+ }
91+ for metric_name , values in data .items ():
92+ if metric_name == "passed" :
93+ continue
94+ if values :
95+ row [f"{ metric_name } (mean)" ] = statistics .mean (values )
96+ row [f"{ metric_name } (median)" ] = statistics .median (values )
97+ row [f"{ metric_name } (min)" ] = min (values )
98+ row [f"{ metric_name } (max)" ] = max (values )
99+ row [f"{ metric_name } (stddev)" ] = (
100+ statistics .stdev (values ) if len (values ) > 1 else 0.0
101+ )
102+ return row
103+
104+ def write_test_result (self , test_name ):
105+ """Write or update result for a specific test incrementally"""
106+ if test_name not in self .test_metrics :
107+ return
108+
109+ data = self .test_metrics [test_name ]
110+ row = self ._compute_row (test_name , data )
111+
112+ # Update or add to results
113+ existing_idx = None
114+ for idx , existing_row in enumerate (self .results ):
115+ if existing_row ["Test" ] == test_name :
116+ existing_idx = idx
117+ break
118+
119+ if existing_idx is not None :
120+ self .results [existing_idx ] = row
121+ else :
95122 self .results .append (row )
96123
97- def write_csv (self ):
98- self .results .sort (key = lambda x : (x ["Test" ], x ["Date" ]))
124+ # Rewrite entire CSV to handle column changes and updates
125+ self ._write_csv_internal ()
126+
127+ def _write_csv_internal (self ):
128+ """Internal method to write CSV file"""
129+ if not self .results :
130+ return
131+
132+ sorted_results = sorted (self .results , key = lambda x : (x ["Test" ], x ["Date" ]))
99133
100134 cols = {}
101- for row in self . results :
135+ for row in sorted_results :
102136 cols .update ({k : None for k in row .keys ()})
103137
104138 self .csv_path .parent .mkdir (parents = True , exist_ok = True )
105139 with open (self .csv_path , "w" , newline = "" ) as f :
106140 writer = csv .DictWriter (f , cols .keys ())
107141 writer .writeheader ()
108- writer .writerows (self .results )
142+ writer .writerows (sorted_results )
143+
144+ def write_csv (self ):
145+ """Final write at session end - ensures all results are flushed"""
146+ self ._write_csv_internal ()
109147
110148
111149# Initialize the CSV writer once at test session setup
@@ -147,6 +185,9 @@ def pytest_runtest_makereport(item, call):
147185
148186 csv_reporter .add_result (test_name , passed , captured , metric_patterns )
149187
188+ # Write results incrementally after each test
189+ csv_reporter .write_test_result (test_name )
190+
150191
151192def pytest_configure (config ):
152193 csv_path = config .getoption ("--csv-output" )
@@ -170,7 +211,6 @@ def pytest_collection_modifyitems(config, items):
170211
171212def pytest_sessionfinish (session , exitstatus ):
172213 if hasattr (session .config , "_csv_reporter" ):
173- session .config ._csv_reporter .finalize_results ()
174214 session .config ._csv_reporter .write_csv ()
175215
176216
0 commit comments