]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/analyze.py
Merge pull request #13599 from LabNConsulting/chopps/analyze-search
[mirror_frr.git] / tests / topotests / analyze.py
CommitLineData
49549fe2
CH
1#!/usr/bin/env python3
2# -*- coding: utf-8 eval: (blacken-mode 1) -*-
acddc0ed 3# SPDX-License-Identifier: GPL-2.0-or-later
49549fe2
CH
4#
5# July 9 2021, Christian Hopps <chopps@labn.net>
6#
7# Copyright (c) 2021, LabN Consulting, L.L.C.
8#
49549fe2 9import argparse
054d6bdc 10import atexit
49549fe2
CH
11import logging
12import os
49549fe2
CH
13import re
14import subprocess
15import sys
054d6bdc 16import tempfile
49549fe2
CH
17from collections import OrderedDict
18
19import xmltodict
20
21
054d6bdc
CH
22def get_range_list(rangestr):
23 result = []
24 for e in rangestr.split(","):
25 e = e.strip()
26 if not e:
27 continue
28 if e.find("-") == -1:
29 result.append(int(e))
30 else:
31 start, end = e.split("-")
32 result.extend(list(range(int(start), int(end) + 1)))
33 return result
34
35
36def dict_range_(dct, rangestr, dokeys):
37 keys = list(dct.keys())
38 if not rangestr or rangestr == "all":
39 for key in keys:
40 if dokeys:
41 yield key
42 else:
43 yield dct[key]
44 return
45
46 dlen = len(keys)
47 for index in get_range_list(rangestr):
48 if index >= dlen:
49 break
50 key = keys[index]
51 if dokeys:
52 yield key
53 else:
54 yield dct[key]
55
56
57def dict_range_keys(dct, rangestr):
58 return dict_range_(dct, rangestr, True)
59
60
61def dict_range_values(dct, rangestr):
62 return dict_range_(dct, rangestr, False)
63
64
49549fe2
CH
65def get_summary(results):
66 ntest = int(results["@tests"])
67 nfail = int(results["@failures"])
68 nerror = int(results["@errors"])
69 nskip = int(results["@skipped"])
70 npass = ntest - nfail - nskip - nerror
71 return ntest, npass, nfail, nerror, nskip
72
73
74def print_summary(results, args):
75 ntest, npass, nfail, nerror, nskip = (0, 0, 0, 0, 0)
76 for group in results:
77 _ntest, _npass, _nfail, _nerror, _nskip = get_summary(results[group])
78 if args.verbose:
a53c08bc
CH
79 print(
80 f"Group: {group} Total: {_ntest} PASSED: {_npass}"
81 " FAIL: {_nfail} ERROR: {_nerror} SKIP: {_nskip}"
82 )
49549fe2
CH
83 ntest += _ntest
84 npass += _npass
85 nfail += _nfail
86 nerror += _nerror
87 nskip += _nskip
88 print(f"Total: {ntest} PASSED: {npass} FAIL: {nfail} ERROR: {nerror} SKIP: {nskip}")
89
90
91def get_global_testcase(results):
92 for group in results:
93 for testcase in results[group]["testcase"]:
94 if "@file" not in testcase:
95 return testcase
96 return None
97
98
99def get_filtered(tfilters, results, args):
100 if isinstance(tfilters, str) or tfilters is None:
101 tfilters = [tfilters]
102 found_files = OrderedDict()
103 for group in results:
104 if isinstance(results[group]["testcase"], list):
105 tlist = results[group]["testcase"]
106 else:
107 tlist = [results[group]["testcase"]]
108 for testcase in tlist:
109 for tfilter in tfilters:
110 if tfilter is None:
111 if (
112 "failure" not in testcase
113 and "error" not in testcase
114 and "skipped" not in testcase
115 ):
116 break
117 elif tfilter in testcase:
118 break
119 else:
120 continue
a53c08bc 121 # cname = testcase["@classname"]
49549fe2
CH
122 fname = testcase.get("@file", "")
123 cname = testcase.get("@classname", "")
124 if not fname and not cname:
125 name = testcase.get("@name", "")
126 if not name:
127 continue
128 # If we had a failure at the module level we could be here.
129 fname = name.replace(".", "/") + ".py"
130 tcname = fname
131 else:
132 if not fname:
133 fname = cname.replace(".", "/") + ".py"
054d6bdc 134 if "@name" not in testcase:
49549fe2
CH
135 tcname = fname
136 else:
137 tcname = fname + "::" + testcase["@name"]
138 found_files[tcname] = testcase
139 return found_files
140
141
054d6bdc
CH
142def search_testcase(testcase, regexp):
143 for key, val in testcase.items():
144 if regexp.search(str(val)):
145 return True
146 return False
147
49549fe2 148
054d6bdc 149def dump_testcase(testcase):
49549fe2
CH
150 s = ""
151 for key, val in testcase.items():
152 if isinstance(val, str) or isinstance(val, float) or isinstance(val, int):
153 s += "{}: {}\n".format(key, val)
160910ec
CH
154 elif isinstance(val, list):
155 for k2, v2 in enumerate(val):
156 s += "{}: {}\n".format(k2, v2)
49549fe2
CH
157 else:
158 for k2, v2 in val.items():
159 s += "{}: {}\n".format(k2, v2)
160 return s
161
162
163def main():
164 parser = argparse.ArgumentParser()
054d6bdc
CH
165 parser.add_argument(
166 "-a",
167 "--save-xml",
168 action="store_true",
169 help=(
170 "Move [container:]/tmp/topotests/topotests.xml "
171 "to --results value if --results does not exist yet"
172 ),
173 )
a53c08bc
CH
174 parser.add_argument(
175 "-A",
176 "--save",
177 action="store_true",
054d6bdc
CH
178 help=(
179 "Move [container:]/tmp/topotests{,.xml} "
180 "to --results value if --results does not exist yet"
181 ),
a53c08bc
CH
182 )
183 parser.add_argument(
054d6bdc
CH
184 "-C",
185 "--container",
186 help="specify docker/podman container of the run",
187 )
188 parser.add_argument(
189 "--use-podman",
a53c08bc 190 action="store_true",
054d6bdc 191 help="Use `podman` instead of `docker` for saving container data",
a53c08bc
CH
192 )
193 parser.add_argument(
194 "-S",
195 "--select",
054d6bdc
CH
196 help=(
197 "select results combination of letters: "
198 "'e'rrored 'f'ailed 'p'assed 's'kipped. "
199 "Default is 'fe', unless --search or --time which default to 'efps'"
200 ),
201 )
202 parser.add_argument(
203 "-R",
204 "--search",
205 help=(
206 "filter results to those which match a regex. "
207 "All test text is search unless restricted by --errmsg or --errtext"
208 ),
a53c08bc
CH
209 )
210 parser.add_argument(
211 "-r",
212 "--results",
213 help="xml results file or directory containing xml results file",
214 )
49549fe2
CH
215 parser.add_argument("--rundir", help=argparse.SUPPRESS)
216 parser.add_argument(
217 "-E",
218 "--enumerate",
219 action="store_true",
220 help="enumerate each item (results scoped)",
221 )
054d6bdc
CH
222 parser.add_argument(
223 "-T", "--test", help="select testcase at given ordinal from the enumerated list"
224 )
49549fe2
CH
225 parser.add_argument(
226 "--errmsg", action="store_true", help="print testcase error message"
227 )
228 parser.add_argument(
229 "--errtext", action="store_true", help="print testcase error text"
230 )
054d6bdc
CH
231 parser.add_argument(
232 "--full", action="store_true", help="print all logging for selected testcases"
233 )
a53c08bc 234 parser.add_argument("--time", action="store_true", help="print testcase run times")
49549fe2
CH
235
236 parser.add_argument("-s", "--summary", action="store_true", help="print summary")
237 parser.add_argument("-v", "--verbose", action="store_true", help="be verbose")
238 args = parser.parse_args()
239
054d6bdc
CH
240 if args.save and args.save_xml:
241 logging.critical("Only one of --save or --save-xml allowed")
242 sys.exit(1)
243
244 scount = bool(args.save) + bool(args.save_xml)
245
246 #
247 # Saving/Archiving results
248 #
249
250 docker_bin = "podman" if args.use_podman else "docker"
251 contid = ""
252 if args.container:
253 # check for container existence
254 contid = args.container
255 try:
256 # p =
257 subprocess.run(
258 f"{docker_bin} inspect {contid}",
259 check=True,
260 shell=True,
261 errors="ignore",
262 capture_output=True,
263 )
264 except subprocess.CalledProcessError:
265 print(f"{docker_bin} container '{contid}' does not exist")
49549fe2 266 sys.exit(1)
054d6bdc
CH
267 # If you need container info someday...
268 # cont_info = json.loads(p.stdout)
269
270 cppath = "/tmp/topotests"
271 if args.save_xml or scount == 0:
272 cppath += "/topotests.xml"
273 if contid:
274 cppath = contid + ":" + cppath
275
276 tresfile = None
277
278 if scount and args.results and not os.path.exists(args.results):
279 if not contid:
280 if not os.path.exists(cppath):
281 print(f"'{cppath}' doesn't exist to save")
282 sys.exit(1)
283 if args.save_xml:
284 subprocess.run(["cp", cppath, args.results])
285 else:
286 subprocess.run(["mv", cppath, args.results])
287 else:
288 try:
289 subprocess.run(
290 f"{docker_bin} cp {cppath} {args.results}",
291 check=True,
292 shell=True,
293 errors="ignore",
294 capture_output=True,
295 )
296 except subprocess.CalledProcessError as error:
297 print(f"Can't {docker_bin} cp '{cppath}': %s", str(error))
298 sys.exit(1)
299
47e52c47
CH
300 if "SUDO_USER" in os.environ:
301 subprocess.run(["chown", "-R", os.environ["SUDO_USER"], args.results])
054d6bdc
CH
302 elif not args.results:
303 # User doesn't want to save results just use them inplace
304 if not contid:
305 if not os.path.exists(cppath):
306 print(f"'{cppath}' doesn't exist")
307 sys.exit(1)
308 args.results = cppath
309 else:
310 tresfile, tresname = tempfile.mkstemp(
311 suffix=".xml", prefix="topotests-", text=True
312 )
313 atexit.register(lambda: os.unlink(tresname))
314 os.close(tresfile)
315 try:
316 subprocess.run(
317 f"{docker_bin} cp {cppath} {tresname}",
318 check=True,
319 shell=True,
320 errors="ignore",
321 capture_output=True,
322 )
323 except subprocess.CalledProcessError as error:
324 print(f"Can't {docker_bin} cp '{cppath}': %s", str(error))
325 sys.exit(1)
326 args.results = tresname
49549fe2 327
054d6bdc
CH
328 #
329 # Result option validation
330 #
331
332 count = 0
333 if args.errmsg:
334 count += 1
335 if args.errtext:
336 count += 1
337 if args.full:
338 count += 1
339 if count > 1:
340 logging.critical("Only one of --full, --errmsg or --errtext allowed")
341 sys.exit(1)
342
343 if args.time and count:
344 logging.critical("Can't use --full, --errmsg or --errtext with --time")
345 sys.exit(1)
346
347 if args.enumerate and (count or args.time or args.test):
348 logging.critical(
349 "Can't use --enumerate with --errmsg, --errtext, --full, --test or --time"
350 )
351 sys.exit(1)
49549fe2
CH
352
353 results = {}
354 ttfiles = []
49549fe2 355
054d6bdc
CH
356 if os.path.exists(os.path.join(args.results, "topotests.xml")):
357 args.results = os.path.join(args.results, "topotests.xml")
358 if not os.path.exists(args.results):
359 logging.critical("%s doesn't exist", args.results)
360 sys.exit(1)
361
362 ttfiles = [args.results]
49549fe2
CH
363
364 for f in ttfiles:
365 m = re.match(r"tt-group-(\d+)/topotests.xml", f)
366 group = int(m.group(1)) if m else 0
367 with open(f) as xml_file:
368 results[group] = xmltodict.parse(xml_file.read())["testsuites"]["testsuite"]
369
054d6bdc
CH
370 search_re = re.compile(args.search) if args.search else None
371
372 if args.select is None:
373 if search_re or args.time:
374 args.select = "efsp"
375 else:
376 args.select = "fe"
377
49549fe2
CH
378 filters = []
379 if "e" in args.select:
380 filters.append("error")
381 if "f" in args.select:
382 filters.append("failure")
383 if "s" in args.select:
384 filters.append("skipped")
385 if "p" in args.select:
386 filters.append(None)
387
388 found_files = get_filtered(filters, results, args)
054d6bdc
CH
389
390 if search_re:
391 found_files = {
392 k: v for k, v in found_files.items() if search_testcase(v, search_re)
393 }
394
395 if args.enumerate:
396 # print the selected test names with ordinal
397 print("\n".join(["{} {}".format(i, x) for i, x in enumerate(found_files)]))
398 elif args.test is None and count == 0 and not args.time:
399 # print the selected test names
400 print("\n".join([str(x) for x in found_files]))
401 else:
402 rangestr = args.test if args.test else "all"
403 for key in dict_range_keys(found_files, rangestr):
404 testcase = found_files[key]
405 if args.time:
406 text = testcase["@time"]
407 s = "{}: {}".format(text, key)
408 elif args.errtext:
409 if "error" in testcase:
410 errmsg = testcase["error"]["#text"]
411 elif "failure" in testcase:
412 errmsg = testcase["failure"]["#text"]
49549fe2 413 else:
054d6bdc
CH
414 errmsg = "none found"
415 s = "{}: {}".format(key, errmsg)
416 elif args.errmsg:
417 if "error" in testcase:
418 errmsg = testcase["error"]["@message"]
419 elif "failure" in testcase:
420 errmsg = testcase["failure"]["@message"]
421 else:
422 errmsg = "none found"
423 s = "{}: {}".format(key, errmsg)
49549fe2 424 else:
054d6bdc
CH
425 s = dump_testcase(testcase)
426 print(s)
49549fe2
CH
427
428 if args.summary:
429 print_summary(results, args)
430
431
432if __name__ == "__main__":
433 main()