]>
Commit | Line | Data |
---|---|---|
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 | 9 | import argparse |
054d6bdc | 10 | import atexit |
49549fe2 CH |
11 | import logging |
12 | import os | |
49549fe2 CH |
13 | import re |
14 | import subprocess | |
15 | import sys | |
054d6bdc | 16 | import tempfile |
49549fe2 CH |
17 | from collections import OrderedDict |
18 | ||
19 | import xmltodict | |
20 | ||
21 | ||
054d6bdc CH |
22 | def 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 | ||
36 | def 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 | ||
57 | def dict_range_keys(dct, rangestr): | |
58 | return dict_range_(dct, rangestr, True) | |
59 | ||
60 | ||
61 | def dict_range_values(dct, rangestr): | |
62 | return dict_range_(dct, rangestr, False) | |
63 | ||
64 | ||
49549fe2 CH |
65 | def 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 | ||
74 | def 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 | ||
91 | def 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 | ||
99 | def 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 |
142 | def 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 | 149 | def 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 | ||
163 | def 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 | ||
432 | if __name__ == "__main__": | |
433 | main() |