Skip to content

Commit 4b9b14d

Browse files
committed
Add runtime info to report. #39
1 parent 9831b91 commit 4b9b14d

File tree

4 files changed

+55
-24
lines changed

4 files changed

+55
-24
lines changed

ci/ci.py

+38-20
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ class CI(SetEnvs):
231231
def __init__(self) -> None:
232232
super().__init__() # Init the SetEnvs object.
233233
self.logger = logging.getLogger("LSIO CI")
234+
self.start_time: float = 0.0
235+
self.total_runtime: float = 0.0
234236
logging.getLogger("botocore.auth").setLevel(logging.INFO) # Don't log the S3 authentication steps.
235237

236238
self.client: DockerClient = docker.from_env()
@@ -249,13 +251,15 @@ def run(self,tags: list) -> None:
249251
`tags` (list): All the tags we will test on the image.
250252
251253
"""
254+
self.start_time = time.time()
252255
thread_pool = ThreadPool(processes=10)
253256
thread_pool.map(self.container_test,tags)
254257
display = Display(size=(1920, 1080)) # Setup an x virtual frame buffer (Xvfb) that Selenium can use during the tests.
255258
display.start()
256259
thread_pool.close()
257260
thread_pool.join()
258261
display.stop()
262+
self.total_runtime = time.time() - self.start_time
259263

260264
def container_test(self, tag: str) -> None:
261265
"""Main container test logic.
@@ -286,30 +290,31 @@ def container_test(self, tag: str) -> None:
286290
logsfound: bool = self.watch_container_logs(container, tag) # Watch the logs for no more than 5 minutes
287291
if not logsfound:
288292
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)
290295
return
291296

292297
# build_version: str = self.get_build_version(container,tag) # Get the image build version
293298
build_info: dict = self.get_build_info(container,tag) # Get the image build info
294299
if build_info["version"] == "ERROR":
295300
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)
297302
return
298303

299304
sbom: str = self.generate_sbom(tag)
300305
if sbom == "ERROR":
301306
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)
303308
return
304309

305310
# Screenshot the web interface and check connectivity
306311
self.take_screenshot(container, tag)
307312

308-
self._endtest(container, tag, build_info, sbom, True)
313+
self._endtest(container, tag, build_info, sbom, True, start_time)
309314
self.logger.success("Test of %s PASSED after %.2f seconds", tag, time.time() - start_time)
310315
return
311316

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:
313318
"""End the test with as much info as we have and append to the report.
314319
315320
Args:
@@ -318,7 +323,12 @@ def _endtest(self, container:Container, tag:str, build_info:dict[str,str], packa
318323
`build_info` (str): Information about the build (version, size etc)
319324
`packages` (str): SBOM dump from the container
320325
`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.
321327
"""
328+
if not start_time:
329+
runtime = "-"
330+
if isinstance(start_time,(float, int)):
331+
runtime = f"{time.time() - start_time:.2f}s"
322332
logblob: Any = container.logs().decode("utf-8")
323333
self.create_html_ansi_file(logblob, tag, "log") # Generate an html container log file based on the latest logs
324334
try:
@@ -338,9 +348,9 @@ def _endtest(self, container:Container, tag:str, build_info:dict[str,str], packa
338348
"uwsgi": warning_texts["uwsgi"] if "uwsgi" in packages and "arm" in tag else ""
339349
},
340350
"build_info": build_info,
341-
# "build_version": build_version,
342351
"test_results": self.tag_report_tests[tag]["test"],
343352
"test_success": test_success,
353+
"runtime": runtime
344354
}
345355
self.report_containers[tag]["has_warnings"] = any(warning[1] for warning in self.report_containers[tag]["warnings"].items())
346356

@@ -427,7 +437,7 @@ def generate_sbom(self, tag:str) -> str:
427437
logblob: str = syft.logs().decode("utf-8")
428438
if "VERSION" in logblob:
429439
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)
431441
self.logger.success("%s package list %s: PASSED after %.2f seconds", test, tag, time.time() - start_time)
432442
self.create_html_ansi_file(str(logblob),tag,"sbom")
433443
try:
@@ -440,7 +450,7 @@ def generate_sbom(self, tag:str) -> str:
440450
self.logger.exception("Creating SBOM package list on %s: FAIL", tag)
441451
self.logger.error("Failed to generate SBOM output on tag %s. SBOM output:\n%s",tag, logblob)
442452
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)
444454
try:
445455
syft.remove(force=True)
446456
except Exception:
@@ -496,6 +506,7 @@ def get_build_info(self,container:Container,tag:str) -> dict[str,str]:
496506
```
497507
"""
498508
test = "Get build info"
509+
start_time = time.time()
499510
try:
500511
self.logger.info("Fetching build info on tag: %s",tag)
501512
build_info: dict[str,str] = {
@@ -504,14 +515,14 @@ def get_build_info(self,container:Container,tag:str) -> dict[str,str]:
504515
"size": "%.2f" % float(int(container.image.attrs["Size"])/1000000) + "MB",
505516
"maintainer": container.attrs["Config"]["Labels"]["maintainer"],
506517
}
507-
self._add_test_result(tag, test, "PASS", "-")
518+
self._add_test_result(tag, test, "PASS", "-", start_time)
508519
self.logger.success("Get build info on tag '%s': PASS", tag)
509520
except (APIError,KeyError) as error:
510521
self.logger.exception("Get build info on tag '%s': FAIL", tag)
511522
build_info = {"version": "ERROR", "created": "ERROR", "size": "ERROR", "maintainer": "ERROR"}
512523
if isinstance(error,KeyError):
513524
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)
515526
self.report_status = "FAIL"
516527
return build_info
517528

@@ -535,17 +546,17 @@ def watch_container_logs(self, container:Container, tag:str) -> bool:
535546
logblob: str = container.logs().decode("utf-8")
536547
if "[services.d] done." in logblob or "[ls.io-init] done." in logblob:
537548
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)
539550
self.logger.success("%s %s: PASSED after %.2f seconds", test, tag, time.time() - start_time)
540551
return True
541552
time.sleep(1)
542553
except APIError as error:
543554
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)
545556
self.report_status = "FAIL"
546557
return False
547558
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)
549560
self.logger.error("%s %s: FAIL - INIT NOT FINISHED", test, tag)
550561
self.report_status = "FAIL"
551562
return False
@@ -565,7 +576,8 @@ def report_render(self) -> None:
565576
image=self.image,
566577
bucket=self.bucket,
567578
region=self.region,
568-
screenshot=self.screenshot
579+
screenshot=self.screenshot,
580+
total_runtime=f"{self.total_runtime:.2f}s",
569581
))
570582

571583
def badge_render(self) -> None:
@@ -668,22 +680,28 @@ def log_upload(self) -> None:
668680
except (S3UploadFailedError, ClientError):
669681
self.logger.exception("Failed to upload the CI logs!")
670682

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:
672684
"""Add a test result to the report
673685
674686
Args:
675687
tag (str): The tag we are testing
676688
test (str): The test we are running
677689
status (str): The status of the test
678690
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.
679692
"""
680693
if status not in ["PASS","FAIL"]:
681694
raise ValueError("Status must be either PASS or FAIL")
682695
if tag not in self.tags:
683696
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"
684701
self.tag_report_tests[tag]["test"][test] = (dict(sorted({
685702
"status":status,
686-
"message":message}.items())))
703+
"message":message,
704+
"runtime": runtime}.items())))
687705

688706
def take_screenshot(self, container: Container, tag:str) -> None:
689707
"""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:
717735
driver.get_screenshot_as_file(f"{self.outdir}/{tag}.png")
718736
if not os.path.isfile(f"{self.outdir}/{tag}.png"):
719737
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)
721739
self.logger.success("Screenshot %s: PASSED after %.2f seconds", tag, time.time() - start_time)
722740
return
723741
except Exception as error:
@@ -728,13 +746,13 @@ def take_screenshot(self, container: Container, tag:str) -> None:
728746
raise error
729747
raise TimeoutException("Timeout taking screenshot")
730748
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)
732750
self.logger.exception("Screenshot %s FAIL CONNECTION ERROR", tag)
733751
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)
735753
self.logger.exception("Screenshot %s FAIL TIMEOUT", tag)
736754
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)
738756
self.logger.exception("Screenshot %s FAIL UNKNOWN", tag)
739757
finally:
740758
try:

ci/logger.py

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def formatMessage(self, record) -> str:
9595
def configure_logging(log_level:str) -> None:
9696
"""Setup console and file logging"""
9797

98+
log_level = log_level.upper()
9899
logger.handlers = []
99100
logger.setLevel(log_level)
100101

ci/template.html

+15-1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@
121121
color:rgba(218, 59, 138);
122122
}
123123

124+
.runtime {
125+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
126+
background: rgba(0, 0, 0, 0.25);
127+
}
128+
124129
strong {
125130
color: rgba(218, 59, 138);
126131
}
@@ -206,6 +211,11 @@
206211
background: #f5f5f5;
207212
}
208213

214+
.runtime {
215+
border-bottom: 1px solid #dcdcdc;
216+
background: #f5f5f5;
217+
}
218+
209219
.build-header {
210220
color:#738694;
211221
}
@@ -553,7 +563,8 @@ <h1>Linux<span>Server</span>.io</h1>
553563
</header>
554564
<div id="results">
555565
<h1>Test Results <strong>{{ image }}:{{ meta_tag }}</strong></h1>
556-
<h2>Cumulative: <span class="report-status-{{ report_status.lower() }}">{{ report_status }}</span></h2>
566+
<h2 style="margin-bottom: 0">Cumulative: <span class="report-status-{{ report_status.lower() }}">{{ report_status }}</span></h2>
567+
<span>Total Runtime: {{ total_runtime }}</span>
557568
<main>
558569
{% for tag in report_containers %}
559570
<section>
@@ -565,6 +576,7 @@ <h3 class="section-header-status"><span class="report-status-fail">FAIL</span></
565576
{% endif %}
566577
<h2 class="section-header-h2"> {{ image }}:{{ tag }} </h2>
567578
</div>
579+
<div class="runtime build-section">Runtime: {{ report_containers[tag]["runtime"] }}</div>
568580
{% if screenshot %}
569581
<a href="{{ tag }}.png">
570582
<img src="{{ tag }}.png" alt="{{ tag }}" width="600" height="auto" onerror="this.onerror=null; this.src='404.jpg'; this.parentElement.setAttribute('href','#')">
@@ -626,6 +638,7 @@ <h2 class="section-header-h2"> {{ image }}:{{ tag }} </h2>
626638
<th>Test</th>
627639
<th>Result</th>
628640
<th>Message</th>
641+
<th>Runtime</th>
629642
</tr>
630643
</thead>
631644
<tbody>
@@ -638,6 +651,7 @@ <h2 class="section-header-h2"> {{ image }}:{{ tag }} </h2>
638651
<td class="result-cell">{{ report_containers[tag]["test_results"][test]['status'] }} <i class="fas fa-exclamation-circle"></i></td>
639652
{% endif %}
640653
<td>{{ report_containers[tag]["test_results"][test]["message"] }}</td>
654+
<td>{{ report_containers[tag]["test_results"][test]["runtime"] }}</td>
641655
</tr>
642656
{% endfor %}
643657
</tbody>

test_build.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@
77

88
def run_test() -> None:
99
"""Run tests on container tags then build and upload reports"""
10-
start_time = time.time()
1110
ci.run(ci.tags)
1211
# Don't set the whole report as failed if any of the ARM tag fails.
1312
for tag in ci.report_containers.keys():
1413
if tag.startswith("amd64") and ci.report_containers[tag]['test_success'] == True:
1514
ci.report_status = 'PASS' # Override the report_status if an ARM tag failed, but the amd64 tag passed.
1615
if ci.report_status == 'PASS':
17-
logger.success('All tests PASSED after %.2f seconds', time.time() - start_time)
18-
16+
logger.success('All tests PASSED after %.2f seconds', ci.total_runtime)
1917
ci.report_render()
2018
ci.badge_render()
2119
ci.json_render()

0 commit comments

Comments
 (0)