|
| 1 | +# Licensed to the Apache Software Foundation (ASF) under one |
| 2 | +# or more contributor license agreements. See the NOTICE file |
| 3 | +# distributed with this work for additional information |
| 4 | +# regarding copyright ownership. The ASF licenses this file |
| 5 | +# to you under the Apache License, Version 2.0 (the |
| 6 | +# "License"); you may not use this file except in compliance |
| 7 | +# with the License. You may obtain a copy of the License at |
| 8 | +# |
| 9 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +# |
| 11 | +# Unless required by applicable law or agreed to in writing, |
| 12 | +# software distributed under the License is distributed on an |
| 13 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 14 | +# KIND, either express or implied. See the License for the |
| 15 | +# specific language governing permissions and limitations |
| 16 | +# under the License. |
| 17 | + |
| 18 | +"""Merge Marvin suite results into summary.tsv, summary.json, and summary.txt.""" |
| 19 | + |
| 20 | +from __future__ import print_function |
| 21 | + |
| 22 | +import argparse |
| 23 | +import json |
| 24 | +import re |
| 25 | +import sys |
| 26 | +from datetime import datetime, timezone |
| 27 | + |
| 28 | + |
| 29 | +def parse_results_txt(path): |
| 30 | + """Return list of (tag, label, test_id, status, detail) from Marvin results.txt.""" |
| 31 | + rows = [] |
| 32 | + with open(path, encoding="utf-8") as fh: |
| 33 | + for line in fh: |
| 34 | + line = line.rstrip("\n") |
| 35 | + m = re.search(r"TestName: (\S+) \| Status : (\S+)", line) |
| 36 | + if m: |
| 37 | + rows.append(("", "", m.group(1), m.group(2), "")) |
| 38 | + continue |
| 39 | + m = re.match(r"(.+?) \.\.\. SKIP: (.+)$", line) |
| 40 | + if m: |
| 41 | + name = m.group(1).strip() |
| 42 | + detail = m.group(2).strip() |
| 43 | + rows.append(("", "", name, "SKIP", detail)) |
| 44 | + return rows |
| 45 | + |
| 46 | + |
| 47 | +def parse_tsv_line(line): |
| 48 | + parts = line.rstrip("\n").split("\t", 4) |
| 49 | + while len(parts) < 5: |
| 50 | + parts.append("") |
| 51 | + return tuple(parts) |
| 52 | + |
| 53 | + |
| 54 | +def normalize_status(status): |
| 55 | + st = (status or "").upper() |
| 56 | + if st == "SUCCESS": |
| 57 | + return "PASS", "pass" |
| 58 | + if st == "SKIP": |
| 59 | + return "SKIP", "skip" |
| 60 | + return "FAIL", "fail" |
| 61 | + |
| 62 | + |
| 63 | +def format_summary_text(rows): |
| 64 | + lines = [ |
| 65 | + "================================================================", |
| 66 | + " TEST SUMMARY", |
| 67 | + "================================================================", |
| 68 | + ] |
| 69 | + pass_n = fail_n = skip_n = 0 |
| 70 | + current_label = None |
| 71 | + for tag, label, test_id, status, detail in rows: |
| 72 | + group = "[%s] %s" % (tag, label) if tag else label |
| 73 | + if group != current_label: |
| 74 | + if current_label is not None: |
| 75 | + lines.append("") |
| 76 | + lines.append(" %s" % group) |
| 77 | + current_label = group |
| 78 | + mark, bucket = normalize_status(status) |
| 79 | + if bucket == "pass": |
| 80 | + pass_n += 1 |
| 81 | + elif bucket == "skip": |
| 82 | + skip_n += 1 |
| 83 | + else: |
| 84 | + fail_n += 1 |
| 85 | + suffix = "" |
| 86 | + st = (status or "").upper() |
| 87 | + if st == "SKIP" and detail: |
| 88 | + suffix = " — %s" % detail |
| 89 | + elif st not in ("SUCCESS", "SKIP"): |
| 90 | + suffix = " — %s" % status |
| 91 | + lines.append(" %-4s %s%s" % (mark, test_id, suffix)) |
| 92 | + |
| 93 | + total = pass_n + fail_n + skip_n |
| 94 | + lines.extend([ |
| 95 | + "", |
| 96 | + "================================================================", |
| 97 | + " TOTAL: %d passed, %d failed, %d skipped (%d tests)" % ( |
| 98 | + pass_n, fail_n, skip_n, total), |
| 99 | + "================================================================", |
| 100 | + ]) |
| 101 | + return "\n".join(lines) + "\n", pass_n, fail_n, skip_n |
| 102 | + |
| 103 | + |
| 104 | +def rows_to_json(rows, meta=None): |
| 105 | + suites = {} |
| 106 | + tests = [] |
| 107 | + pass_n = fail_n = skip_n = 0 |
| 108 | + for tag, label, test_id, status, detail in rows: |
| 109 | + mark, bucket = normalize_status(status) |
| 110 | + if bucket == "pass": |
| 111 | + pass_n += 1 |
| 112 | + elif bucket == "skip": |
| 113 | + skip_n += 1 |
| 114 | + else: |
| 115 | + fail_n += 1 |
| 116 | + suite_key = tag or label |
| 117 | + if suite_key not in suites: |
| 118 | + suites[suite_key] = { |
| 119 | + "tag": tag, |
| 120 | + "label": label, |
| 121 | + "passed": 0, |
| 122 | + "failed": 0, |
| 123 | + "skipped": 0, |
| 124 | + "tests": [], |
| 125 | + } |
| 126 | + suites[suite_key]["tests"].append({ |
| 127 | + "name": test_id, |
| 128 | + "status": mark, |
| 129 | + "detail": detail or None, |
| 130 | + }) |
| 131 | + if bucket == "pass": |
| 132 | + suites[suite_key]["passed"] += 1 |
| 133 | + elif bucket == "skip": |
| 134 | + suites[suite_key]["skipped"] += 1 |
| 135 | + else: |
| 136 | + suites[suite_key]["failed"] += 1 |
| 137 | + tests.append({ |
| 138 | + "tag": tag, |
| 139 | + "label": label, |
| 140 | + "name": test_id, |
| 141 | + "status": mark, |
| 142 | + "detail": detail or None, |
| 143 | + }) |
| 144 | + |
| 145 | + payload = { |
| 146 | + "generatedAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), |
| 147 | + "totals": { |
| 148 | + "passed": pass_n, |
| 149 | + "failed": fail_n, |
| 150 | + "skipped": skip_n, |
| 151 | + "total": pass_n + fail_n + skip_n, |
| 152 | + }, |
| 153 | + "suites": list(suites.values()), |
| 154 | + "tests": tests, |
| 155 | + } |
| 156 | + if meta: |
| 157 | + payload["run"] = meta |
| 158 | + return payload |
| 159 | + |
| 160 | + |
| 161 | +def load_rows_from_tsv(path): |
| 162 | + rows = [] |
| 163 | + with open(path, encoding="utf-8") as fh: |
| 164 | + for line in fh: |
| 165 | + if line.strip(): |
| 166 | + rows.append(parse_tsv_line(line)) |
| 167 | + return rows |
| 168 | + |
| 169 | + |
| 170 | +def load_rows_from_suite_specs(specs): |
| 171 | + rows = [] |
| 172 | + for spec in specs: |
| 173 | + parts = spec.split(":", 2) |
| 174 | + if len(parts) != 3: |
| 175 | + print("Invalid --suite spec (want tag:label:path): %s" % spec, |
| 176 | + file=sys.stderr) |
| 177 | + sys.exit(1) |
| 178 | + tag, label, path = parts |
| 179 | + for _tag, _label, test_id, status, detail in parse_results_txt(path): |
| 180 | + rows.append((tag, label, test_id, status, detail)) |
| 181 | + return rows |
| 182 | + |
| 183 | + |
| 184 | +def write_outputs(out_dir, rows, meta=None): |
| 185 | + tsv_path = out_dir + "/summary.tsv" |
| 186 | + json_path = out_dir + "/summary.json" |
| 187 | + txt_path = out_dir + "/summary.txt" |
| 188 | + |
| 189 | + with open(tsv_path, "w", encoding="utf-8") as fh: |
| 190 | + for row in rows: |
| 191 | + fh.write("\t".join(row) + "\n") |
| 192 | + |
| 193 | + summary_text, pass_n, fail_n, skip_n = format_summary_text(rows) |
| 194 | + with open(txt_path, "w", encoding="utf-8") as fh: |
| 195 | + fh.write(summary_text) |
| 196 | + |
| 197 | + payload = rows_to_json(rows, meta=meta) |
| 198 | + with open(json_path, "w", encoding="utf-8") as fh: |
| 199 | + json.dump(payload, fh, indent=2) |
| 200 | + fh.write("\n") |
| 201 | + |
| 202 | + return pass_n, fail_n, skip_n, summary_text |
| 203 | + |
| 204 | + |
| 205 | +def main(): |
| 206 | + parser = argparse.ArgumentParser( |
| 207 | + description="Aggregate Marvin test results into summary files.") |
| 208 | + parser.add_argument( |
| 209 | + "--out-dir", required=True, |
| 210 | + help="Directory for summary.tsv, summary.json, summary.txt") |
| 211 | + parser.add_argument( |
| 212 | + "--summary-tsv", |
| 213 | + help="Read existing tab-separated summary (from run_tests.sh)") |
| 214 | + parser.add_argument( |
| 215 | + "--suite", action="append", default=[], |
| 216 | + help="Suite spec tag:label:path/to/results.txt (repeatable)") |
| 217 | + parser.add_argument( |
| 218 | + "--meta-json", |
| 219 | + help="JSON string or path to run metadata merged into summary.json") |
| 220 | + parser.add_argument( |
| 221 | + "--print", dest="print_summary", action="store_true", |
| 222 | + help="Print human-readable summary to stdout") |
| 223 | + args = parser.parse_args() |
| 224 | + |
| 225 | + if args.summary_tsv: |
| 226 | + rows = load_rows_from_tsv(args.summary_tsv) |
| 227 | + elif args.suite: |
| 228 | + rows = load_rows_from_suite_specs(args.suite) |
| 229 | + else: |
| 230 | + print("Provide --summary-tsv or at least one --suite", file=sys.stderr) |
| 231 | + sys.exit(1) |
| 232 | + |
| 233 | + meta = None |
| 234 | + if args.meta_json: |
| 235 | + if args.meta_json.startswith("{"): |
| 236 | + meta = json.loads(args.meta_json) |
| 237 | + else: |
| 238 | + with open(args.meta_json, encoding="utf-8") as fh: |
| 239 | + meta = json.load(fh) |
| 240 | + |
| 241 | + pass_n, fail_n, skip_n, summary_text = write_outputs( |
| 242 | + args.out_dir.rstrip("/"), rows, meta=meta) |
| 243 | + |
| 244 | + if args.print_summary: |
| 245 | + print(summary_text, end="") |
| 246 | + |
| 247 | + return 0 if fail_n == 0 else 1 |
| 248 | + |
| 249 | + |
| 250 | +if __name__ == "__main__": |
| 251 | + sys.exit(main()) |
0 commit comments