# Copyright 2022 Thales Silicon Security # # Licensed under the Solderpad Hardware Licence, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # SPDX-License-Identifier: Apache-2.0 WITH SHL-2.0 # You may obtain a copy of the License at https://solderpad.org/licenses/ # # Original Author: Côme Allart """ Helpers to build CI reports """ import os import re from datetime import datetime as dt import yaml class Metric: "A metric is a part of the body of the report" def __init__(self, name): self.name = name self.sort_index = None self.failed = False self.values = [] def to_doc(self): "Transform to a dictionary" return { 'display_name': self.name, 'sort_index': self.sort_index, 'type': self._t(), 'status': "fail" if self.failed else "pass", 'value': self._values_to_doc(), } def fail(self): "Mark metric as failed" self.failed = True def _values_to_doc(self): raise NotImplementedError() def _t(self): raise NotImplementedError() class LogMetric(Metric): "Log lines" def add_value(self, line): "Insert a line in the log" self.values.append(line) def _values_to_doc(self): return self.values def _t(self): return 'log' class TableMetric(Metric): "Table" def add_value(self, *col): "Insert a line in the table" self.values.append(list(col)) def _values_to_doc(self): return [{'col': v} for v in self.values] def _t(self): return 'table' class TableStatusMetric(Metric): "Table with status label for each line" class _TableStatusMetricColumn(): def __init__(self, title, col_type): self.title = title self.col_type = col_type def to_doc(self): return { "title": self.title, "col_type": self.col_type } def __init__(self, name): super().__init__(name) self.columns = [] def add_column(self, title, col_type): "Set the table columns titles" self.columns.append(TableStatusMetric._TableStatusMetricColumn(title, col_type)) def add_pass_label(self, label, *col): "Insert a 'pass' line with given label in the table" self._add_value('pass', label, *col) def add_fail_label(self, label, *col): "Insert a 'fail' line with given label in the table" self._add_value('fail', label, *col) self.fail() def add_pass(self, *col): "Insert a 'pass' line in the table" self.add_pass_label("PASS", *col) def add_fail(self, *col): "Insert a 'fail' line in the table" self.add_fail_label("FAIL", *col) def to_doc(self): doc = super().to_doc() if len(self.columns) > 0: doc['columns'] = list(map(lambda col: col.to_doc(), self.columns)) return doc def _add_value(self, status, label, *col): self.values.append((status, label, list(col))) def _values_to_doc(self): return [{'status': s, 'label': l, 'col': c} for (s,l,c) in self.values] def _t(self): return 'table_status' class Report: "A report is the top level entity of the document" def __init__(self, label=None): self.label = label self.failed = False self.metrics = [] def add_metric(self, *metric): "Add one or more metric(s) to the report body" for m in metric: if m.sort_index is None: if len(self.metrics) > 0: m.sort_index = self.metrics[-1].sort_index + 1 else: m.sort_index = 1 self.metrics.append(m) if m.failed: self.fail() def fail(self): "Mark report as failed" self.failed = True def to_doc(self): "Transform to a dictionary" assert len(self.metrics) > 0, "A report must have at least one metric" start = os.environ['CI_JOB_STARTED_AT'] start_fmt = '%Y-%m-%dT%H:%M:%S%z' start = dt.strptime(start, start_fmt).timestamp() pass_label = "FAIL" if self.failed else "PASS" label = pass_label if self.label is None else self.label return { 'title': os.environ["DASHBOARD_JOB_TITLE"], 'description': os.environ["DASHBOARD_JOB_DESCRIPTION"], 'category': os.environ["DASHBOARD_JOB_CATEGORY"], 'sort_index': os.environ["DASHBOARD_SORT_INDEX"], 'job_id': os.environ["CI_JOB_ID"], 'job_url': os.environ["CI_JOB_URL"], 'job_stage_name': os.environ["CI_JOB_STAGE"], 'job_started_at': int(start), 'job_end_at': int(dt.now().timestamp()), 'token': 'YC' + str(dt.now().timestamp()).replace('.', ''), 'status': "fail" if self.failed else "pass", 'metrics': [m.to_doc() for m in self.metrics], 'label': label, } def dump(self, path=None): """ Print results and create report file By default the output path is build from $CI_JOB_NAME """ for metric in self.metrics: print(metric.values) if path is None: ci_job_name = os.environ.get("CI_JOB_NAME") if ci_job_name is not None: filename = re.sub(r'[^\w\.\\\/]', '_', ci_job_name) path = 'artifacts/reports/'+filename+'.yml' if path is not None: with open(path, 'w') as f: yaml.dump(self.to_doc(), f)