blob: 7c70050f86045c6d23649c7e89f623cfa6e7da42 [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
15import time
16
17
18class TCReport(object):
19 """
20 Class definition for objects to build report files in a
21 pipeline stage
22 """
23 STATUS_VALUES = ["PASS", "FAIL", "SKIP"]
24
25 def __init__(self, metadata=None, test_environments=None,
26 test_configuration=None, target=None,
27 test_suites=None, report_file=None):
28 """
29 Constructor for the class. Initialise the report object and loads
30 an existing report(yaml) if defined.
31
32 :param metadata: Initial metadata report object
33 :param test_environments: Initial test environment object
34 :param test_configuration: Initial test configuration object
35 :param target: Initial target object
36 :param test_suites: Initial test suites object
37 :param report_file: If defined then an existing yaml report is loaded
38 as the initial report object
39 """
40 if test_suites is None:
41 test_suites = {}
42 if target is None:
43 target = {}
44 if test_configuration is None:
45 test_configuration = {'test-assets': {}}
46 if test_environments is None:
47 test_environments = {}
48 if metadata is None:
49 metadata = {}
50 self._report_name = "Not-defined"
51 # Define if is a new report or an existing report
52 if report_file:
53 # The structure of the report must follow:
54 # - report name:
55 # {report properties}
56 try:
57 with open(report_file) as f:
58 full_report = yaml.full_load(f)
59 self._report_name, \
60 self.report = list(full_report.items())[0]
61 except Exception as e:
62 logging.exception(
63 f"Exception loading existing report '{report_file}'")
64 raise Exception(e)
65 else:
66 self.report = {'metadata': metadata,
67 'test-environments': test_environments,
68 'test-config': test_configuration,
69 'target': target,
70 'test-suites': test_suites
71 }
72
73 def dump(self, file_name):
74 """
75 Method that dumps the report object with the report name as key in
76 a yaml format in a given file.
77
78 :param file_name: File name to dump the yaml report
79 :return: Nothing
80 """
81 with open(file_name, 'w') as f:
82 yaml.dump({self._report_name: self.report}, f)
83
84 @property
85 def test_suites(self):
86 return self.report['test-suites']
87
88 @test_suites.setter
89 def test_suites(self, value):
90 self.test_suites = value
91
92 @property
93 def test_environments(self):
94 return self.report['test-environments']
95
96 @test_environments.setter
97 def test_environments(self, value):
98 self.test_environments = value
99
100 @property
101 def test_config(self):
102 return self.report['test-config']
103
104 @test_config.setter
105 def test_config(self, value):
106 self.test_config = value
107
108 def add_test_suite(self, name: str, test_results, metadata=None):
109 """
110 Public method to add a test suite object to a report object.
111
112 :param name: Unique test suite name
113 :param test_results: Object with the tests results
114 :param metadata: Metadata object for the test suite
115 """
116 if metadata is None:
117 metadata = {}
118 if name in self.test_suites:
119 logging.error("Duplicated test suite:{}".format(name))
120 else:
121 self.test_suites[name] = {'test-results': test_results,
122 'metadata': metadata}
123
124 def add_test_environment(self, name: str, values=None):
125 """
126 Public method to add a test environment object to a report object.
127
128 :param name: Name (key) of the test environment object
129 :param values: Object assigned to the test environment object
130 :return: Nothing
131 """
132 if values is None:
133 values = {}
134 self.test_environments[name] = values
135
136 def add_test_asset(self, name: str, values=None):
137 """
138 Public method to add a test asset object to a report object.
139
140 :param name: Name (key) of the test asset object
141 :param values: Object assigned to the test asset object
142 :return: Nothing
143 """
144 if values is None:
145 values = {}
146 if 'test-assets' not in self.test_config:
147 self.test_config['test-assets'] = {}
148 self.test_config['test-assets'][name] = values
149
150 @staticmethod
151 def process_ptest_results(lava_log_string="",
152 results_pattern=r"(?P<status>("
153 r"PASS|FAIL|SKIP)): ("
154 r"?P<id>.+)"):
155 """
156 Method that process ptest-runner results from a lava log string and
157 converts them to a test results object.
158
159 :param lava_log_string: Lava log string
160 :param results_pattern: Regex used to capture the test results
161 :return: Test results object
162 """
163 pattern = re.compile(results_pattern)
164 if 'status' not in pattern.groupindex or \
165 'id' not in pattern.groupindex:
166 raise Exception(
167 "Status and/or id must be defined in the results pattern")
168 results = {}
169 lines = lava_log_string.split("\n")
170 it = iter(lines)
171 stop_found = False
172 for line in it:
173 fields = line.split(" ", 1)
174 if len(fields) > 1 and fields[1] == "START: ptest-runner":
175 for report_line in it:
176 timestamp, *rest = report_line.split(" ", 1)
177 if not rest:
178 continue
179 if rest[0] == "STOP: ptest-runner":
180 stop_found = True
181 break
182 p = pattern.match(rest[0])
183 if p:
184 id = p.groupdict()['id'].replace(" ", "_")
185 status = p.groupdict()['status']
186 if not id:
187 print("Warning: missing 'id'")
188 elif status not in TCReport.STATUS_VALUES:
189 print("Warning: Status unknown")
190 elif id in results:
191 print("Warning: duplicated id")
192 else:
193 metadata = {k: p.groupdict()[k]
194 for k in p.groupdict().keys()
195 if k not in ('id', 'status')}
196 results[id] = {'status': status,
197 'metadata': metadata}
198 break
199 if not stop_found:
200 logger.warning("End of ptest-runner not found")
201 return results
202
203 def parse_fvp_model_version(self, lava_log_string):
204 """
205 Obtains the FVP model and version from a lava log string.
206
207 :param lava_log_string: Lava log string
208 :return: Tuple with FVP model and version
209 """
210 result = re.findall(r"opt/model/(.+) --version", lava_log_string)
211 model = "" if not result else result[0]
212 result = re.findall(r"Fast Models \[(.+?)\]\n", lava_log_string)
213 version = "" if not result else result[0]
214 self.report['target'] = {'platform': model, 'version': version}
215 return model, version
216
217 @property
218 def report_name(self):
219 return self._report_name
220
221 @report_name.setter
222 def report_name(self, value):
223 self._report_name = value
224
225 @property
226 def metadata(self):
227 return self.report['metadata']
228
229 @metadata.setter
230 def metadata(self, metadata):
231 self.report['metadata'] = metadata
232
233
234class KvDictAppendAction(argparse.Action):
235 """
236 argparse action to split an argument into KEY=VALUE form
237 on the first = and append to a dictionary.
238 """
239
240 def __call__(self, parser, args, values, option_string=None):
241 d = getattr(args, self.dest) or {}
242 for value in values:
243 try:
244 (k, v) = value.split("=", 2)
245 except ValueError as ex:
246 raise \
247 argparse.ArgumentError(self, f"Could not parse argument '{values[0]}' as k=v format")
248 d[k] = v
249 setattr(args, self.dest, d)
250
251
252def read_metadata(metadata_file):
253 """
254 Function that returns a dictionary object from a KEY=VALUE lines file.
255
256 :param metadata_file: Filename with the KEY=VALUE pairs
257 :return: Dictionary object with key and value pairs
258 """
259 if not metadata_file:
260 return {}
261 with open(metadata_file) as f:
262 d = dict([line.strip().split("=", 1) for line in f])
263 return d
264
265
266def import_env(env_names):
267 """
268 Function that matches a list of regex expressions against all the
269 environment variables keys and returns an object with the matched key
270 and the value of the environment variable.
271
272 :param env_names: List of regex expressions to match env keys
273 :return: Object with the matched env variables
274 """
275 env_list = list(os.environ.keys())
276 keys = []
277 for expression in env_names:
278 r = re.compile(expression)
279 keys = keys + list(filter(r.match, env_list))
280 d = {key: os.environ[key] for key in keys}
281 return d
282
283
284def merge_dicts(*dicts):
285 """
286 Function to merge a list of dictionaries.
287
288 :param dicts: List of dictionaries
289 :return: A merged dictionary
290 """
291 merged = {}
292 for d in dicts:
293 merged.update(d)
294 return merged
295
296
297def process_lava_log(_report, _args):
298 """
299 Function to adapt user arguments to process test results and add properties
300 to the report object.
301
302 :param _report: Report object
303 :param _args: User arguments
304 :return: Nothing
305 """
306 with open(_args.lava_log, "r") as f:
307 lava_log = f.read()
308 # Get the test results
309 results = {}
310 if _args.type == 'ptest-report':
311 results = TCReport.process_ptest_results(lava_log,
312 results_pattern=_args.results_pattern)
313 if _args.report_name:
314 _report.report_name = _args.report_name
315 _report.parse_fvp_model_version(lava_log)
316 metadata = {}
317 if _args.metadata_pairs or _args.metadata_env or _args.metadata_file:
318 metadata = _args.metadata_pairs or import_env(
319 _args.metadata_env) or read_metadata(_args.metadata_file)
320 _report.add_test_suite(_args.test_suite_name, test_results=results,
321 metadata=metadata)
322
323
324if __name__ == '__main__':
325 # Defining logger
326 logging.basicConfig(
327 level=logging.INFO,
328 format="%(asctime)s [%(levelname)s] %(message)s",
329 handlers=[
330 logging.FileHandler("log_parser_{}.log".format(time.time())),
331 logging.StreamHandler()
332 ])
333 """
334 The main aim of this script is to be called with different options to build
335 a report object that can be dumped into a yaml format
336 """
337 parser = argparse.ArgumentParser(description='Generic yaml report for TC')
338 parser.add_argument("--report", "-r", help="Report filename")
339 parser.add_argument("-f", help="Force new report", action='store_true',
340 dest='new_report')
341 parser.add_argument("command", help="Command: process-results")
342 group_results = parser.add_argument_group("Process results")
343 group_results.add_argument('--test-suite-name', type=str,
344 help='Test suite name')
345 group_results.add_argument('--lava-log', type=str, help='Lava log file')
346 group_results.add_argument('--type', type=str, help='Type of report log',
347 default='ptest-report')
348 group_results.add_argument('--report-name', type=str, help='Report name',
349 default="")
350 group_results.add_argument('--results-pattern', type=str,
351 help='Regex pattern to extract test results',
352 default=r"(?P<status>(PASS|FAIL|SKIP)): ("
353 r"?P<id>.+ .+) - ( "
354 r"?P<description>.+)")
355 test_env = parser.add_argument_group("Test environments")
356 test_env.add_argument('--test-env-name', type=str,
357 help='Test environment type')
358 test_env.add_argument("--test-env-values",
359 nargs="+",
360 action=KvDictAppendAction,
361 default={},
362 metavar="KEY=VALUE",
363 help="Set a number of key-value pairs "
364 "(do not put spaces before or after the = "
365 "sign). "
366 "If a value contains spaces, you should define "
367 "it with double quotes: "
368 'key="Value with spaces". Note that '
369 "values are always treated as strings.")
370 test_env.add_argument("--test-env-env",
371 nargs="+",
372 default={},
373 help="Import environment variables values with the "
374 "given name.")
375 parser.add_argument("--metadata-pairs",
376 nargs="+",
377 action=KvDictAppendAction,
378 default={},
379 metavar="KEY=VALUE",
380 help="Set a number of key-value pairs "
381 "(do not put spaces before or after the = sign). "
382 "If a value contains spaces, you should define "
383 "it with double quotes: "
384 'key="Value with spaces". Note that '
385 "values are always treated as strings.")
386
387 test_config = parser.add_argument_group("Test config")
388 test_config.add_argument('--test-asset-name', type=str,
389 help='Test asset type')
390 test_config.add_argument("--test-asset-values",
391 nargs="+",
392 action=KvDictAppendAction,
393 default={},
394 metavar="KEY=VALUE",
395 help="Set a number of key-value pairs "
396 "(do not put spaces before or after the = "
397 "sign). "
398 "If a value contains spaces, you should "
399 "define "
400 "it with double quotes: "
401 'key="Value with spaces". Note that '
402 "values are always treated as strings.")
403 test_config.add_argument("--test-asset-env",
404 nargs="+",
405 default=None,
406 help="Import environment variables values with "
407 "the given name.")
408
409 parser.add_argument("--metadata-env",
410 nargs="+",
411 default=None,
412 help="Import environment variables values with the "
413 "given name.")
414 parser.add_argument("--metadata-file",
415 type=str,
416 default=None,
417 help="File with key-value pairs lines i.e"
418 "key1=value1\nkey2=value2")
419
420 args = parser.parse_args()
421 report = None
422 # Check if report exists (that can be overwritten) or is a new report
423 if os.path.exists(args.report) and not args.new_report:
424 report = TCReport(report_file=args.report) # load existing report
425 else:
426 report = TCReport()
427
428 # Possible list of commands:
429 # process-results: To parse test results from stream into a test suite obj
430 if args.command == "process-results":
431 # Requires the test suite name and the log file, lava by the time being
432 if not args.test_suite_name:
433 parser.error("Test suite name required")
434 elif not args.lava_log:
435 parser.error("Lava log file required")
436 process_lava_log(report, args)
437 # set-report-metadata: Set the report's metadata
438 elif args.command == "set-report-metadata":
439 # Various options to load metadata into the report object
440 report.metadata = merge_dicts(args.metadata_pairs,
441 read_metadata(args.metadata_file),
442 import_env(args.metadata_env))
443 # add-test-environment: Add a test environment to the report's object
444 elif args.command == "add-test-environment":
445 # Various options to load environment data into the report object
446 report.add_test_environment(args.test_env_name,
447 merge_dicts(args.test_env_values,
448 import_env(args.test_env_env)))
449 # add-test-asset: Add a test asset into the report's object (test-config)
450 elif args.command == "add-test-asset":
451 report.add_test_asset(args.test_asset_name,
452 merge_dicts(args.test_asset_values,
453 import_env(args.test_asset_env)))
454
455 # Dump the report object as a yaml report
456 report.dump(args.report)