@@ -231,6 +231,8 @@ class CI(SetEnvs):
231
231
def __init__ (self ) -> None :
232
232
super ().__init__ () # Init the SetEnvs object.
233
233
self .logger = logging .getLogger ("LSIO CI" )
234
+ self .start_time : float = 0.0
235
+ self .total_runtime : float = 0.0
234
236
logging .getLogger ("botocore.auth" ).setLevel (logging .INFO ) # Don't log the S3 authentication steps.
235
237
236
238
self .client : DockerClient = docker .from_env ()
@@ -249,13 +251,15 @@ def run(self,tags: list) -> None:
249
251
`tags` (list): All the tags we will test on the image.
250
252
251
253
"""
254
+ self .start_time = time .time ()
252
255
thread_pool = ThreadPool (processes = 10 )
253
256
thread_pool .map (self .container_test ,tags )
254
257
display = Display (size = (1920 , 1080 )) # Setup an x virtual frame buffer (Xvfb) that Selenium can use during the tests.
255
258
display .start ()
256
259
thread_pool .close ()
257
260
thread_pool .join ()
258
261
display .stop ()
262
+ self .total_runtime = time .time () - self .start_time
259
263
260
264
def container_test (self , tag : str ) -> None :
261
265
"""Main container test logic.
@@ -286,30 +290,31 @@ def container_test(self, tag: str) -> None:
286
290
logsfound : bool = self .watch_container_logs (container , tag ) # Watch the logs for no more than 5 minutes
287
291
if not logsfound :
288
292
self .logger .error ("Test of %s FAILED after %.2f seconds" , tag , time .time () - start_time )
289
- self ._endtest (container , tag , "ERROR" , "ERROR" , False )
293
+ build_info = {"version" : "-" , "created" : "-" , "size" : "-" , "maintainer" : "-" }
294
+ self ._endtest (container , tag , build_info , "ERROR" , False , start_time )
290
295
return
291
296
292
297
# build_version: str = self.get_build_version(container,tag) # Get the image build version
293
298
build_info : dict = self .get_build_info (container ,tag ) # Get the image build info
294
299
if build_info ["version" ] == "ERROR" :
295
300
self .logger .error ("Test of %s FAILED after %.2f seconds" , tag , time .time () - start_time )
296
- self ._endtest (container , tag , build_info , "ERROR" , False )
301
+ self ._endtest (container , tag , build_info , "ERROR" , False , start_time )
297
302
return
298
303
299
304
sbom : str = self .generate_sbom (tag )
300
305
if sbom == "ERROR" :
301
306
self .logger .error ("Test of %s FAILED after %.2f seconds" , tag , time .time () - start_time )
302
- self ._endtest (container , tag , build_info , sbom , False )
307
+ self ._endtest (container , tag , build_info , sbom , False , start_time )
303
308
return
304
309
305
310
# Screenshot the web interface and check connectivity
306
311
self .take_screenshot (container , tag )
307
312
308
- self ._endtest (container , tag , build_info , sbom , True )
313
+ self ._endtest (container , tag , build_info , sbom , True , start_time )
309
314
self .logger .success ("Test of %s PASSED after %.2f seconds" , tag , time .time () - start_time )
310
315
return
311
316
312
- def _endtest (self , container :Container , tag :str , build_info :dict [str ,str ], packages :str , test_success : bool ) -> None :
317
+ def _endtest (self , container :Container , tag :str , build_info :dict [str ,str ], packages :str , test_success : bool , start_time : float | int = 0.0 ) -> None :
313
318
"""End the test with as much info as we have and append to the report.
314
319
315
320
Args:
@@ -318,7 +323,12 @@ def _endtest(self, container:Container, tag:str, build_info:dict[str,str], packa
318
323
`build_info` (str): Information about the build (version, size etc)
319
324
`packages` (str): SBOM dump from the container
320
325
`test_success` (bool): If the testing of the container failed or not
326
+ `start_time` (float, optional): The start time of the test. Defaults to 0.0. Used to calculate the runtime of the test.
321
327
"""
328
+ if not start_time :
329
+ runtime = "-"
330
+ if isinstance (start_time ,(float , int )):
331
+ runtime = f"{ time .time () - start_time :.2f} s"
322
332
logblob : Any = container .logs ().decode ("utf-8" )
323
333
self .create_html_ansi_file (logblob , tag , "log" ) # Generate an html container log file based on the latest logs
324
334
try :
@@ -338,9 +348,9 @@ def _endtest(self, container:Container, tag:str, build_info:dict[str,str], packa
338
348
"uwsgi" : warning_texts ["uwsgi" ] if "uwsgi" in packages and "arm" in tag else ""
339
349
},
340
350
"build_info" : build_info ,
341
- # "build_version": build_version,
342
351
"test_results" : self .tag_report_tests [tag ]["test" ],
343
352
"test_success" : test_success ,
353
+ "runtime" : runtime
344
354
}
345
355
self .report_containers [tag ]["has_warnings" ] = any (warning [1 ] for warning in self .report_containers [tag ]["warnings" ].items ())
346
356
@@ -427,7 +437,7 @@ def generate_sbom(self, tag:str) -> str:
427
437
logblob : str = syft .logs ().decode ("utf-8" )
428
438
if "VERSION" in logblob :
429
439
self .logger .info ("Get package versions for %s completed" , tag )
430
- self ._add_test_result (tag , test , "PASS" , "-" )
440
+ self ._add_test_result (tag , test , "PASS" , "-" , start_time )
431
441
self .logger .success ("%s package list %s: PASSED after %.2f seconds" , test , tag , time .time () - start_time )
432
442
self .create_html_ansi_file (str (logblob ),tag ,"sbom" )
433
443
try :
@@ -440,7 +450,7 @@ def generate_sbom(self, tag:str) -> str:
440
450
self .logger .exception ("Creating SBOM package list on %s: FAIL" , tag )
441
451
self .logger .error ("Failed to generate SBOM output on tag %s. SBOM output:\n %s" ,tag , logblob )
442
452
self .report_status = "FAIL"
443
- self ._add_test_result (tag , test , "FAIL" , str (error_message ))
453
+ self ._add_test_result (tag , test , "FAIL" , str (error_message ), start_time )
444
454
try :
445
455
syft .remove (force = True )
446
456
except Exception :
@@ -496,6 +506,7 @@ def get_build_info(self,container:Container,tag:str) -> dict[str,str]:
496
506
```
497
507
"""
498
508
test = "Get build info"
509
+ start_time = time .time ()
499
510
try :
500
511
self .logger .info ("Fetching build info on tag: %s" ,tag )
501
512
build_info : dict [str ,str ] = {
@@ -504,14 +515,14 @@ def get_build_info(self,container:Container,tag:str) -> dict[str,str]:
504
515
"size" : "%.2f" % float (int (container .image .attrs ["Size" ])/ 1000000 ) + "MB" ,
505
516
"maintainer" : container .attrs ["Config" ]["Labels" ]["maintainer" ],
506
517
}
507
- self ._add_test_result (tag , test , "PASS" , "-" )
518
+ self ._add_test_result (tag , test , "PASS" , "-" , start_time )
508
519
self .logger .success ("Get build info on tag '%s': PASS" , tag )
509
520
except (APIError ,KeyError ) as error :
510
521
self .logger .exception ("Get build info on tag '%s': FAIL" , tag )
511
522
build_info = {"version" : "ERROR" , "created" : "ERROR" , "size" : "ERROR" , "maintainer" : "ERROR" }
512
523
if isinstance (error ,KeyError ):
513
524
error : str = f"KeyError: { error } "
514
- self ._add_test_result (tag , test , "FAIL" , str (error ))
525
+ self ._add_test_result (tag , test , "FAIL" , str (error ), start_time )
515
526
self .report_status = "FAIL"
516
527
return build_info
517
528
@@ -535,17 +546,17 @@ def watch_container_logs(self, container:Container, tag:str) -> bool:
535
546
logblob : str = container .logs ().decode ("utf-8" )
536
547
if "[services.d] done." in logblob or "[ls.io-init] done." in logblob :
537
548
self .logger .info ("%s completed for %s" ,test , tag )
538
- self ._add_test_result (tag , test , "PASS" , "-" )
549
+ self ._add_test_result (tag , test , "PASS" , "-" , start_time )
539
550
self .logger .success ("%s %s: PASSED after %.2f seconds" , test , tag , time .time () - start_time )
540
551
return True
541
552
time .sleep (1 )
542
553
except APIError as error :
543
554
self .logger .exception ("%s %s: FAIL - INIT NOT FINISHED" , test , tag )
544
- self ._add_test_result (tag , test , "FAIL" , f"INIT NOT FINISHED: { str (error )} " )
555
+ self ._add_test_result (tag , test , "FAIL" , f"INIT NOT FINISHED: { str (error )} " , start_time )
545
556
self .report_status = "FAIL"
546
557
return False
547
558
self .logger .error ("%s failed for %s" , test , tag )
548
- self ._add_test_result (tag , test , "FAIL" , "INIT NOT FINISHED" )
559
+ self ._add_test_result (tag , test , "FAIL" , "INIT NOT FINISHED" , start_time )
549
560
self .logger .error ("%s %s: FAIL - INIT NOT FINISHED" , test , tag )
550
561
self .report_status = "FAIL"
551
562
return False
@@ -565,7 +576,8 @@ def report_render(self) -> None:
565
576
image = self .image ,
566
577
bucket = self .bucket ,
567
578
region = self .region ,
568
- screenshot = self .screenshot
579
+ screenshot = self .screenshot ,
580
+ total_runtime = f"{ self .total_runtime :.2f} s" ,
569
581
))
570
582
571
583
def badge_render (self ) -> None :
@@ -668,22 +680,28 @@ def log_upload(self) -> None:
668
680
except (S3UploadFailedError , ClientError ):
669
681
self .logger .exception ("Failed to upload the CI logs!" )
670
682
671
- def _add_test_result (self , tag :str , test :str , status :str , message :str ) -> None :
683
+ def _add_test_result (self , tag :str , test :str , status :str , message :str , start_time : float | int = 0.0 ) -> None :
672
684
"""Add a test result to the report
673
685
674
686
Args:
675
687
tag (str): The tag we are testing
676
688
test (str): The test we are running
677
689
status (str): The status of the test
678
690
message (str): The message of the test
691
+ start_time (str, optional): The start time of the test. Defaults to 0.0. Used to calculate the runtime of the test.
679
692
"""
680
693
if status not in ["PASS" ,"FAIL" ]:
681
694
raise ValueError ("Status must be either PASS or FAIL" )
682
695
if tag not in self .tags :
683
696
raise ValueError ("Tag not in the list of tags" )
697
+ if not start_time :
698
+ runtime = "-"
699
+ if isinstance (start_time ,(float , int )):
700
+ runtime : str = f"{ time .time () - start_time :.2f} s"
684
701
self .tag_report_tests [tag ]["test" ][test ] = (dict (sorted ({
685
702
"status" :status ,
686
- "message" :message }.items ())))
703
+ "message" :message ,
704
+ "runtime" : runtime }.items ())))
687
705
688
706
def take_screenshot (self , container : Container , tag :str ) -> None :
689
707
"""Take a screenshot and save it to self.outdir if self.screenshot is True
@@ -717,7 +735,7 @@ def take_screenshot(self, container: Container, tag:str) -> None:
717
735
driver .get_screenshot_as_file (f"{ self .outdir } /{ tag } .png" )
718
736
if not os .path .isfile (f"{ self .outdir } /{ tag } .png" ):
719
737
raise FileNotFoundError (f"Screenshot '{ self .outdir } /{ tag } .png' not found" )
720
- self ._add_test_result (tag , test , "PASS" , "-" )
738
+ self ._add_test_result (tag , test , "PASS" , "-" , start_time )
721
739
self .logger .success ("Screenshot %s: PASSED after %.2f seconds" , tag , time .time () - start_time )
722
740
return
723
741
except Exception as error :
@@ -728,13 +746,13 @@ def take_screenshot(self, container: Container, tag:str) -> None:
728
746
raise error
729
747
raise TimeoutException ("Timeout taking screenshot" )
730
748
except (requests .Timeout , requests .ConnectionError , KeyError ) as error :
731
- self ._add_test_result (tag , test , "FAIL" , f"CONNECTION ERROR: { str (error )} " )
749
+ self ._add_test_result (tag , test , "FAIL" , f"CONNECTION ERROR: { str (error )} " , start_time )
732
750
self .logger .exception ("Screenshot %s FAIL CONNECTION ERROR" , tag )
733
751
except TimeoutException as error :
734
- self ._add_test_result (tag , test , "FAIL" , f"TIMEOUT: { str (error )} " )
752
+ self ._add_test_result (tag , test , "FAIL" , f"TIMEOUT: { str (error )} " , start_time )
735
753
self .logger .exception ("Screenshot %s FAIL TIMEOUT" , tag )
736
754
except (WebDriverException , Exception ) as error :
737
- self ._add_test_result (tag , test , "FAIL" , f"UNKNOWN: { str (error )} " )
755
+ self ._add_test_result (tag , test , "FAIL" , f"UNKNOWN: { str (error )} " , start_time )
738
756
self .logger .exception ("Screenshot %s FAIL UNKNOWN" , tag )
739
757
finally :
740
758
try :
0 commit comments