blob: 017c8e1b20566dbb472d55cab7205c5a74dfd49c [file] [log] [blame]
saul-romero-arm1c97ecc2021-04-08 12:09:19 +00001##############################################################################
2
3# Copyright (c) 2021, ARM Limited and Contributors. All rights reserved.
4
5#
6
7# SPDX-License-Identifier: BSD-3-Clause
8
9##############################################################################
10import re
11import yaml
12import argparse
13import os
14import logging
saul-romero-armce968d62021-05-18 15:01:12 +000015import subprocess
16import sys
17import json
18from adaptors.sql.yaml_parser import YAMLParser
19import glob
20
21HTML_TEMPLATE = "html.tar.gz"
saul-romero-arm1c97ecc2021-04-08 12:09:19 +000022
23
24class TCReport(object):
25 """
26 Class definition for objects to build report files in a
27 pipeline stage
28 """
29 STATUS_VALUES = ["PASS", "FAIL", "SKIP"]
30
31 def __init__(self, metadata=None, test_environments=None,
32 test_configuration=None, target=None,
33 test_suites=None, report_file=None):
34 """
35 Constructor for the class. Initialise the report object and loads
36 an existing report(yaml) if defined.
37
38 :param metadata: Initial metadata report object
39 :param test_environments: Initial test environment object
40 :param test_configuration: Initial test configuration object
41 :param target: Initial target object
42 :param test_suites: Initial test suites object
43 :param report_file: If defined then an existing yaml report is loaded
44 as the initial report object
45 """
46 if test_suites is None:
47 test_suites = {}
48 if target is None:
49 target = {}
50 if test_configuration is None:
51 test_configuration = {'test-assets': {}}
52 if test_environments is None:
53 test_environments = {}
54 if metadata is None:
55 metadata = {}
56 self._report_name = "Not-defined"
57 # Define if is a new report or an existing report
58 if report_file:
59 # The structure of the report must follow:
60 # - report name:
61 # {report properties}
62 try:
63 with open(report_file) as f:
saul-romero-armce968d62021-05-18 15:01:12 +000064 full_report = yaml.load(f)
saul-romero-arm1c97ecc2021-04-08 12:09:19 +000065 self._report_name, \
66 self.report = list(full_report.items())[0]
saul-romero-armce968d62021-05-18 15:01:12 +000067 except Exception as ex:
saul-romero-arm1c97ecc2021-04-08 12:09:19 +000068 logging.exception(
69 f"Exception loading existing report '{report_file}'")
saul-romero-armce968d62021-05-18 15:01:12 +000070 raise ex
saul-romero-arm1c97ecc2021-04-08 12:09:19 +000071 else:
72 self.report = {'metadata': metadata,
73 'test-environments': test_environments,
74 'test-config': test_configuration,
75 'target': target,
76 'test-suites': test_suites
77 }
saul-romero-armce968d62021-05-18 15:01:12 +000078 self.report_file = report_file
saul-romero-arm1c97ecc2021-04-08 12:09:19 +000079
80 def dump(self, file_name):
81 """
82 Method that dumps the report object with the report name as key in
83 a yaml format in a given file.
84
85 :param file_name: File name to dump the yaml report
86 :return: Nothing
87 """
88 with open(file_name, 'w') as f:
89 yaml.dump({self._report_name: self.report}, f)
90
91 @property
92 def test_suites(self):
93 return self.report['test-suites']
94
95 @test_suites.setter
96 def test_suites(self, value):
97 self.test_suites = value
98
99 @property
100 def test_environments(self):
101 return self.report['test-environments']
102
103 @test_environments.setter
104 def test_environments(self, value):
105 self.test_environments = value
106
107 @property
108 def test_config(self):
109 return self.report['test-config']
110
111 @test_config.setter
112 def test_config(self, value):
113 self.test_config = value
114
115 def add_test_suite(self, name: str, test_results, metadata=None):
116 """
117 Public method to add a test suite object to a report object.
118
119 :param name: Unique test suite name
120 :param test_results: Object with the tests results
121 :param metadata: Metadata object for the test suite
122 """
123 if metadata is None:
124 metadata = {}
125 if name in self.test_suites:
126 logging.error("Duplicated test suite:{}".format(name))
127 else:
128 self.test_suites[name] = {'test-results': test_results,
129 'metadata': metadata}
130
131 def add_test_environment(self, name: str, values=None):
132 """
133 Public method to add a test environment object to a report object.
134
135 :param name: Name (key) of the test environment object
136 :param values: Object assigned to the test environment object
137 :return: Nothing
138 """
139 if values is None:
140 values = {}
141 self.test_environments[name] = values
142
143 def add_test_asset(self, name: str, values=None):
144 """
145 Public method to add a test asset object to a report object.
146
147 :param name: Name (key) of the test asset object
148 :param values: Object assigned to the test asset object
149 :return: Nothing
150 """
151 if values is None:
152 values = {}
153 if 'test-assets' not in self.test_config:
154 self.test_config['test-assets'] = {}
155 self.test_config['test-assets'][name] = values
156
157 @staticmethod
158 def process_ptest_results(lava_log_string="",
159 results_pattern=r"(?P<status>("
160 r"PASS|FAIL|SKIP)): ("
161 r"?P<id>.+)"):
162 """
163 Method that process ptest-runner results from a lava log string and
164 converts them to a test results object.
165
166 :param lava_log_string: Lava log string
167 :param results_pattern: Regex used to capture the test results
168 :return: Test results object
169 """
170 pattern = re.compile(results_pattern)
171 if 'status' not in pattern.groupindex or \
172 'id' not in pattern.groupindex:
173 raise Exception(
174 "Status and/or id must be defined in the results pattern")
175 results = {}
176 lines = lava_log_string.split("\n")
177 it = iter(lines)
178 stop_found = False
179 for line in it:
180 fields = line.split(" ", 1)
181 if len(fields) > 1 and fields[1] == "START: ptest-runner":
182 for report_line in it:
183 timestamp, *rest = report_line.split(" ", 1)
184 if not rest:
185 continue
186 if rest[0] == "STOP: ptest-runner":
187 stop_found = True
188 break
189 p = pattern.match(rest[0])
190 if p:
saul-romero-armce968d62021-05-18 15:01:12 +0000191 id = re.sub("[ :]+", "_", p.groupdict()['id'])
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000192 status = p.groupdict()['status']
193 if not id:
194 print("Warning: missing 'id'")
195 elif status not in TCReport.STATUS_VALUES:
196 print("Warning: Status unknown")
197 elif id in results:
198 print("Warning: duplicated id")
199 else:
200 metadata = {k: p.groupdict()[k]
201 for k in p.groupdict().keys()
202 if k not in ('id', 'status')}
203 results[id] = {'status': status,
204 'metadata': metadata}
205 break
206 if not stop_found:
Monalisa Jena15426822021-12-02 16:40:55 +0530207 logging.warning("End of ptest-runner not found")
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000208 return results
209
210 def parse_fvp_model_version(self, lava_log_string):
211 """
212 Obtains the FVP model and version from a lava log string.
213
214 :param lava_log_string: Lava log string
215 :return: Tuple with FVP model and version
216 """
217 result = re.findall(r"opt/model/(.+) --version", lava_log_string)
218 model = "" if not result else result[0]
219 result = re.findall(r"Fast Models \[(.+?)\]\n", lava_log_string)
220 version = "" if not result else result[0]
221 self.report['target'] = {'platform': model, 'version': version}
222 return model, version
223
224 @property
225 def report_name(self):
226 return self._report_name
227
228 @report_name.setter
229 def report_name(self, value):
230 self._report_name = value
231
232 @property
233 def metadata(self):
234 return self.report['metadata']
235
236 @metadata.setter
237 def metadata(self, metadata):
238 self.report['metadata'] = metadata
239
saul-romero-armce968d62021-05-18 15:01:12 +0000240 @property
241 def target(self):
242 return self.report['target']
243
244 @target.setter
245 def target(self, target):
246 self.report['target'] = target
247
248 def merge_into(self, other):
249 """
250 Merge one report object with this.
251
252 :param other: Report object to be merged to this
253 :return:
254 """
255 try:
256 if not self.report_name or self.report_name == "Not-defined":
257 self.report_name = other.report_name
258 if self.report_name != other.report_name:
259 logging.warning(
260 f'Report name \'{other.report_name}\' does not match '
261 f'original report name')
262 # Merge metadata where 'other' report will overwrite common key
263 # values
264 self.metadata.update(other.metadata)
265 self.target.update(other.target)
266 self.test_config['test-assets'].update(other.test_config['test'
267 '-assets'])
268 self.test_environments.update(other.test_environments)
269 self.test_suites.update(other.test_suites)
270 except Exception as ex:
271 logging.exception("Failed to merge reports")
272 raise ex
saurom018e7c54d2021-12-06 14:44:52 +0000273
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000274
275class KvDictAppendAction(argparse.Action):
276 """
277 argparse action to split an argument into KEY=VALUE form
278 on the first = and append to a dictionary.
279 """
280
281 def __call__(self, parser, args, values, option_string=None):
282 d = getattr(args, self.dest) or {}
283 for value in values:
284 try:
285 (k, v) = value.split("=", 2)
286 except ValueError as ex:
287 raise \
saul-romero-armce968d62021-05-18 15:01:12 +0000288 argparse.ArgumentError(self,
289 f"Could not parse argument '{values[0]}' as k=v format")
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000290 d[k] = v
291 setattr(args, self.dest, d)
292
293
294def read_metadata(metadata_file):
295 """
296 Function that returns a dictionary object from a KEY=VALUE lines file.
297
298 :param metadata_file: Filename with the KEY=VALUE pairs
299 :return: Dictionary object with key and value pairs
300 """
301 if not metadata_file:
302 return {}
303 with open(metadata_file) as f:
304 d = dict([line.strip().split("=", 1) for line in f])
305 return d
306
307
308def import_env(env_names):
309 """
310 Function that matches a list of regex expressions against all the
311 environment variables keys and returns an object with the matched key
312 and the value of the environment variable.
313
314 :param env_names: List of regex expressions to match env keys
315 :return: Object with the matched env variables
316 """
317 env_list = list(os.environ.keys())
318 keys = []
319 for expression in env_names:
320 r = re.compile(expression)
321 keys = keys + list(filter(r.match, env_list))
322 d = {key: os.environ[key] for key in keys}
323 return d
324
325
326def merge_dicts(*dicts):
327 """
328 Function to merge a list of dictionaries.
329
330 :param dicts: List of dictionaries
331 :return: A merged dictionary
332 """
333 merged = {}
334 for d in dicts:
335 merged.update(d)
336 return merged
337
338
339def process_lava_log(_report, _args):
340 """
341 Function to adapt user arguments to process test results and add properties
342 to the report object.
343
344 :param _report: Report object
345 :param _args: User arguments
346 :return: Nothing
347 """
348 with open(_args.lava_log, "r") as f:
349 lava_log = f.read()
350 # Get the test results
351 results = {}
352 if _args.type == 'ptest-report':
saul-romero-armce968d62021-05-18 15:01:12 +0000353 results_pattern = None
354 suite = _args.suite or _args.test_suite_name
355 if suite == "optee-test":
356 results_pattern = r"(?P<status>(PASS|FAIL|SKIP)): (?P<id>.+ .+) " \
357 r"- (?P<description>.+)"
358 elif suite == "kernel-selftest":
359 results_pattern = r"(?P<status>(PASS|FAIL|SKIP)): (" \
360 r"?P<description>selftests): (?P<id>.+: .+)"
Mohan Kumari Munivenkatappac6928c72021-06-10 08:34:32 +0000361 elif suite == "ltp":
Mohan Kumari Munivenkatappad86f6622021-08-05 09:18:48 +0000362 results_pattern = r"(?P<status>(PASS|FAIL|SKIP)): (?P<id>.+)"
Riju Aryad183f7f2021-09-03 08:40:24 +0000363 elif suite == "pm-qa":
Riju Aryaef591912021-09-09 12:51:53 +0000364 results_pattern = r"(?P<status>(PASS|FAIL|SKIP)): (?P<id>.+)" \
Riju Aryad183f7f2021-09-03 08:40:24 +0000365 r"- (?P<description>.+)"
Monalisa Jena15426822021-12-02 16:40:55 +0530366 elif suite == "scmi":
367 results_pattern = r"(?P<status>(PASS|FAIL|SKIP)): (?P<id>.+)"
saul-romero-armce968d62021-05-18 15:01:12 +0000368 else:
369 logging.error(f"Suite type uknown or not defined:'{suite}'")
370 sys.exit(-1)
371
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000372 results = TCReport.process_ptest_results(lava_log,
saul-romero-armce968d62021-05-18 15:01:12 +0000373 results_pattern=results_pattern)
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000374 if _args.report_name:
375 _report.report_name = _args.report_name
376 _report.parse_fvp_model_version(lava_log)
377 metadata = {}
378 if _args.metadata_pairs or _args.metadata_env or _args.metadata_file:
379 metadata = _args.metadata_pairs or import_env(
380 _args.metadata_env) or read_metadata(_args.metadata_file)
381 _report.add_test_suite(_args.test_suite_name, test_results=results,
382 metadata=metadata)
383
384
saul-romero-armce968d62021-05-18 15:01:12 +0000385def merge_reports(reportObj, list_reports):
386 """
387 Function to merge a list of yaml report files into a report object
388
389 :param reportObj: Instance of an initial report object to merge the reports
390 :param list_reports: List of yaml report files or file patterns
391 :return: Updated report object
392 """
393 for report_pattern in list_reports:
394 for report_file in glob.glob(report_pattern):
395 to_merge = TCReport(report_file=report_file)
396 reportObj.merge_into(to_merge)
397 return reportObj
398
399
400def generate_html(report_obj, user_args):
401 """
402 Generate html output for the given report_file
403
404 :param report_obj: report object
405 :param user_args: Arguments from user
406 :return: Nothing
407 """
408 script_path = os.path.dirname(sys.argv[0])
409 report_file = user_args.report
410 try:
411 with open(script_path + "/html/js/reporter.js", "a") as write_file:
412 for key in args.html_output:
413 print(f'\nSetting html var "{key}"...')
414 write_file.write(f"\nlet {key}='{args.html_output[key]}'")
415 j = json.dumps({report_obj.report_name: report_obj.report},
416 indent=4)
417 write_file.write(f"\nlet textReport=`\n{j}\n`")
418 subprocess.run(f'cp -f {report_file} {script_path}/html/report.yaml',
419 shell=True)
420 except subprocess.CalledProcessError as ex:
421 logging.exception("Error at generating html")
422 raise ex
423
424
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000425if __name__ == '__main__':
426 # Defining logger
427 logging.basicConfig(
428 level=logging.INFO,
429 format="%(asctime)s [%(levelname)s] %(message)s",
430 handlers=[
saul-romero-armce968d62021-05-18 15:01:12 +0000431 logging.FileHandler("debug.log"),
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000432 logging.StreamHandler()
433 ])
434 """
435 The main aim of this script is to be called with different options to build
436 a report object that can be dumped into a yaml format
437 """
438 parser = argparse.ArgumentParser(description='Generic yaml report for TC')
439 parser.add_argument("--report", "-r", help="Report filename")
440 parser.add_argument("-f", help="Force new report", action='store_true',
441 dest='new_report')
442 parser.add_argument("command", help="Command: process-results")
443 group_results = parser.add_argument_group("Process results")
444 group_results.add_argument('--test-suite-name', type=str,
445 help='Test suite name')
446 group_results.add_argument('--lava-log', type=str, help='Lava log file')
447 group_results.add_argument('--type', type=str, help='Type of report log',
448 default='ptest-report')
449 group_results.add_argument('--report-name', type=str, help='Report name',
450 default="")
saul-romero-armce968d62021-05-18 15:01:12 +0000451 group_results.add_argument("--suite", required=False,
452 default=None,
453 help="Suite type. If not defined takes the "
454 "suite name value")
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000455 test_env = parser.add_argument_group("Test environments")
456 test_env.add_argument('--test-env-name', type=str,
457 help='Test environment type')
458 test_env.add_argument("--test-env-values",
459 nargs="+",
460 action=KvDictAppendAction,
461 default={},
462 metavar="KEY=VALUE",
463 help="Set a number of key-value pairs "
464 "(do not put spaces before or after the = "
465 "sign). "
466 "If a value contains spaces, you should define "
467 "it with double quotes: "
468 'key="Value with spaces". Note that '
469 "values are always treated as strings.")
470 test_env.add_argument("--test-env-env",
471 nargs="+",
472 default={},
473 help="Import environment variables values with the "
474 "given name.")
475 parser.add_argument("--metadata-pairs",
476 nargs="+",
477 action=KvDictAppendAction,
478 default={},
479 metavar="KEY=VALUE",
480 help="Set a number of key-value pairs "
481 "(do not put spaces before or after the = sign). "
482 "If a value contains spaces, you should define "
483 "it with double quotes: "
484 'key="Value with spaces". Note that '
485 "values are always treated as strings.")
486
487 test_config = parser.add_argument_group("Test config")
488 test_config.add_argument('--test-asset-name', type=str,
489 help='Test asset type')
490 test_config.add_argument("--test-asset-values",
491 nargs="+",
492 action=KvDictAppendAction,
493 default={},
494 metavar="KEY=VALUE",
495 help="Set a number of key-value pairs "
496 "(do not put spaces before or after the = "
497 "sign). "
498 "If a value contains spaces, you should "
499 "define "
500 "it with double quotes: "
501 'key="Value with spaces". Note that '
502 "values are always treated as strings.")
503 test_config.add_argument("--test-asset-env",
504 nargs="+",
505 default=None,
506 help="Import environment variables values with "
507 "the given name.")
508
509 parser.add_argument("--metadata-env",
510 nargs="+",
511 default=None,
512 help="Import environment variables values with the "
513 "given name.")
514 parser.add_argument("--metadata-file",
515 type=str,
516 default=None,
517 help="File with key-value pairs lines i.e"
518 "key1=value1\nkey2=value2")
saurom018e7c54d2021-12-06 14:44:52 +0000519
saul-romero-armce968d62021-05-18 15:01:12 +0000520 parser.add_argument("--list",
521 nargs="+",
522 default={},
523 help="List of report files.")
524 parser.add_argument("--html-output",
525 required=False,
526 nargs="*",
527 action=KvDictAppendAction,
528 default={},
529 metavar="KEY=VALUE",
530 help="Set a number of key-value pairs i.e. key=value"
531 "(do not put spaces before or after the = "
532 "sign). "
533 "If a value contains spaces, you should define "
534 "it with double quotes: "
535 "Valid keys: title, logo_img, logo_href.")
536 parser.add_argument("--sql-output",
537 required=False,
538 action="store_true",
539 help='Sql output produced from the report file')
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000540
541 args = parser.parse_args()
542 report = None
saul-romero-armce968d62021-05-18 15:01:12 +0000543
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000544 # Check if report exists (that can be overwritten) or is a new report
545 if os.path.exists(args.report) and not args.new_report:
546 report = TCReport(report_file=args.report) # load existing report
547 else:
548 report = TCReport()
549
550 # Possible list of commands:
551 # process-results: To parse test results from stream into a test suite obj
552 if args.command == "process-results":
553 # Requires the test suite name and the log file, lava by the time being
554 if not args.test_suite_name:
555 parser.error("Test suite name required")
556 elif not args.lava_log:
557 parser.error("Lava log file required")
558 process_lava_log(report, args)
559 # set-report-metadata: Set the report's metadata
560 elif args.command == "set-report-metadata":
561 # Various options to load metadata into the report object
562 report.metadata = merge_dicts(args.metadata_pairs,
563 read_metadata(args.metadata_file),
564 import_env(args.metadata_env))
565 # add-test-environment: Add a test environment to the report's object
566 elif args.command == "add-test-environment":
567 # Various options to load environment data into the report object
568 report.add_test_environment(args.test_env_name,
569 merge_dicts(args.test_env_values,
570 import_env(args.test_env_env)))
571 # add-test-asset: Add a test asset into the report's object (test-config)
572 elif args.command == "add-test-asset":
573 report.add_test_asset(args.test_asset_name,
574 merge_dicts(args.test_asset_values,
575 import_env(args.test_asset_env)))
saul-romero-armce968d62021-05-18 15:01:12 +0000576 elif args.command == "merge-reports":
577 report = merge_reports(report, args.list)
saul-romero-arm1c97ecc2021-04-08 12:09:19 +0000578 report.dump(args.report)
saul-romero-armce968d62021-05-18 15:01:12 +0000579 if args.html_output:
580 generate_html(report, args)
581
582 if args.sql_output:
583 yaml_obj = YAMLParser(args.report)
584 yaml_obj.create_table()
585 yaml_obj.parse_file()
586 yaml_obj.update_test_config_table()