99from pathlib import Path
1010from typing import cast
1111
12+ import pandas as pd
1213from envoy .server .model .archive .site import ArchiveSiteDERSetting
1314from envoy .server .model .site import SiteDERSetting
1415from sqlalchemy import select
3536)
3637from cactus_runner .app .requests_archive import copy_request_response_files_to_archive
3738from cactus_runner .app .status import get_active_runner_status
38- from cactus_runner .models import RunnerState
39+ from cactus_runner .models import (
40+ CheckResult ,
41+ PackedReadings ,
42+ ReadingType ,
43+ ReportingData ,
44+ RunnerState ,
45+ Site ,
46+ )
47+
48+ # Cactus runner supports returning different versions of the reporting data
49+ # Define the currently preferred reporting data version
50+ CURRENT_REPORTING_DATA_VERSION : int = 1
3951
4052GENERATION_ERRORS_FILE_NAME = "generation-errors.txt"
4153
54+
4255logger = logging .getLogger (__name__ )
4356
4457
@@ -66,10 +79,12 @@ def get_file_name_no_extension(file_path: str) -> str:
6679
6780def get_zip_contents (
6881 json_status_summary : str | None ,
82+ json_reporting_data : str | None ,
6983 log_file_paths : list [str ],
7084 pdf_data : bytes | None ,
7185 errors : list [str ],
7286 filename_infix : str = "" ,
87+ reporting_data_filename_prefix : str | None = "ReportingData" ,
7388) -> bytes :
7489 """Returns the contents of the zipped test procedures artifacts in bytes."""
7590
@@ -89,6 +104,12 @@ def get_zip_contents(
89104 with open (file_path , "w" ) as f :
90105 f .write (json_status_summary )
91106
107+ # Create reporting data json file
108+ if json_reporting_data is not None and reporting_data_filename_prefix is not None :
109+ file_path = archive_dir / f"{ reporting_data_filename_prefix } { filename_infix } .json"
110+ with open (file_path , "w" ) as f :
111+ f .write (json_reporting_data )
112+
92113 # Copy all log files into the archive - preserving the names
93114 for log_file_path in log_file_paths :
94115 log_file_name = get_file_name_no_extension (log_file_path )
@@ -221,6 +242,44 @@ async def generate_pdf(
221242 return pdf_data
222243
223244
245+ async def generate_json_reporting_data (
246+ runner_state : RunnerState ,
247+ check_results : dict [str , CheckResult ],
248+ readings : dict [ReadingType , pd .DataFrame ],
249+ reading_counts : dict [ReadingType , int ],
250+ sites : list [Site ],
251+ timeline : timeline .Timeline | None ,
252+ errors ,
253+ version : int = 1 ,
254+ set_max_w_varied : bool = False ,
255+ ) -> str | None :
256+ created_at = datetime .now (timezone .utc )
257+
258+ try :
259+ # Repack readings into something serializable
260+ packed_readings = [
261+ PackedReadings (reading_type = k , readings_as_json = readings [k ].to_json (), reading_counts = v )
262+ for k , v in reading_counts .items ()
263+ ]
264+
265+ reporting_data = ReportingData .v (version )(
266+ created_at = created_at ,
267+ runner_state = runner_state ,
268+ check_results = check_results ,
269+ readings = packed_readings ,
270+ sites = sites ,
271+ timeline = timeline ,
272+ set_max_w_varied = set_max_w_varied ,
273+ )
274+ json_reporting_data = reporting_data .to_json ()
275+ except Exception as exc :
276+ logger .error ("Error generating reporting data. Omitting reporting data from final zip." , exc_info = exc )
277+ errors .append (f"Error generating reporting data: { exc } " )
278+ json_reporting_data = None
279+
280+ return json_reporting_data
281+
282+
224283async def finish_active_test (runner_state : RunnerState , session : AsyncSession ) -> bytes :
225284 """For the specified RunnerState - move the active test into a "Finished" state by calculating the final ZIP
226285 contents. Raises NoActiveTestProcedure if there isn't an active test procedure for the specified RunnerState
@@ -277,11 +336,11 @@ async def finish_active_test(runner_state: RunnerState, session: AsyncSession) -
277336 # Add a "virtual" check covering XSD errors in incoming requests
278337 xsd_error_counts = [len (rh .body_xml_errors ) for rh in runner_state .request_history if rh .body_xml_errors ]
279338 if xsd_error_counts :
280- xsd_check = check . CheckResult (
339+ xsd_check = CheckResult (
281340 False , f"Detected { sum (xsd_error_counts )} xsd errors over { len (xsd_error_counts )} request(s)."
282341 )
283342 else :
284- xsd_check = check . CheckResult (True , "No XSD errors detected in any requests." )
343+ xsd_check = CheckResult (True , "No XSD errors detected in any requests." )
285344 check_results ["all-requests-xsd-valid" ] = xsd_check
286345
287346 # Figure out the testing timeline
@@ -299,7 +358,7 @@ async def finish_active_test(runner_state: RunnerState, session: AsyncSession) -
299358 errors .append (f"Failed to generate test timeline: { exc } " )
300359 test_timeline = None
301360
302- # Fetch raw DB data and create PDF
361+ # Fetch raw DB data and create PDF
303362 try :
304363 sites = await get_sites (session )
305364 readings = await get_readings (session , reading_specifiers = MANDATORY_READING_SPECIFIERS )
@@ -329,15 +388,38 @@ async def finish_active_test(runner_state: RunnerState, session: AsyncSession) -
329388 errors = errors ,
330389 set_max_w_varied = set_max_w_varied ,
331390 )
391+
392+ # Convert to serialisable types
393+ serializable_readings = {ReadingType .from_site_reading_type (k ): v for k , v in readings .items ()}
394+ serializable_reading_counts = {ReadingType .from_site_reading_type (k ): v for k , v in reading_counts .items ()}
395+ serializable_sites = [Site .from_site (s ) for s in sites ]
396+
397+ # Collect reporting state into json object
398+ reporting_data_version = CURRENT_REPORTING_DATA_VERSION
399+ json_reporting_data = await generate_json_reporting_data (
400+ runner_state = runner_state ,
401+ check_results = check_results ,
402+ readings = serializable_readings ,
403+ reading_counts = serializable_reading_counts ,
404+ sites = serializable_sites ,
405+ timeline = test_timeline ,
406+ errors = errors ,
407+ version = reporting_data_version ,
408+ set_max_w_varied = set_max_w_varied ,
409+ )
410+ reporting_data_filename_prefix = f"ReportingData_v{ reporting_data_version } "
332411 except Exception as exc :
333412 logger .error ("Failed to generate PDF report" , exc_info = exc )
334413 errors .append (f"Failed to generate PDF report: { exc } " )
335414 pdf_data = None
415+ json_reporting_data = None
416+ reporting_data_filename_prefix = None
336417
337418 generation_timestamp = now .replace (microsecond = 0 )
338419
339420 active_test_procedure .finished_zip_data = get_zip_contents (
340421 json_status_summary = json_status_summary ,
422+ json_reporting_data = json_reporting_data ,
341423 log_file_paths = [
342424 LOG_FILE_ENVOY_SERVER ,
343425 LOG_FILE_ENVOY_ADMIN ,
@@ -346,6 +428,7 @@ async def finish_active_test(runner_state: RunnerState, session: AsyncSession) -
346428 ],
347429 pdf_data = pdf_data ,
348430 filename_infix = f"_{ int (generation_timestamp .timestamp ())} _{ active_test_procedure .name } " ,
431+ reporting_data_filename_prefix = reporting_data_filename_prefix ,
349432 errors = errors ,
350433 )
351434 return active_test_procedure .finished_zip_data
0 commit comments