001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.terminal.report; 018 019import static org.apache.juneau.terminal.report.ReportJson.*; 020import static org.apache.juneau.terminal.report.ReportText.*; 021 022import java.nio.file.*; 023import java.util.*; 024 025import org.apache.juneau.terminal.report.TerminalEvent.*; 026 027/** 028 * Reads Playwright's <c>--reporter=json</c> output. 029 * 030 * <p> 031 * The framework key is <c>playwright</c>. Suites and specs are walked recursively; a suite whose title equals its file 032 * is the file-level suite and does not contribute to the test name. A <c>flaky</c> test (failed, then passed on retry) 033 * is a pass. A truncated file yields no tests and a warning. 034 */ 035@SuppressWarnings({ 036 "java:S1192", // JSON keys of the Playwright report are protocol literals. 037 "java:S3776" // Walking the suite tree and folding retries is inherently branchy. 038}) 039public final class PlaywrightJsonReportReader implements ReportReader { 040 041 private static final int MAX_DEPTH = 100; 042 private static final String SEP = " › "; 043 044 @Override /* ReportReader */ 045 public String fw() { 046 return "playwright"; 047 } 048 049 @Override /* ReportReader */ 050 public ReportResult read(Path path, ReportLimits lim) { 051 var out = new ReportBuilder(); 052 var root = readRoot(path, lim, out); 053 if (root == null) 054 return out.build(fw()); 055 var files = new ArrayList<String>(); 056 var rows = new ArrayList<ReportTest>(); 057 var rowFiles = new ArrayList<String>(); 058 for (var s : list(root.get("suites"))) { 059 var sm = map(s); 060 if (sm != null && ! walk(sm, List.of(), null, 0, lim, out, files, rows, rowFiles)) 061 break; 062 } 063 var display = stripCommonDir(files); 064 for (var i = 0; i < rows.size(); i++) { 065 var r = rows.get(i); 066 var suite = clip(display.get(files.indexOf(rowFiles.get(i))), lim.maxNameChars()); 067 out.tests.add(new ReportTest(suite, r.name(), r.status(), r.ms(), r.msg(), r.trace())); 068 } 069 return out.build(fw()); 070 } 071 072 /** Returns <jk>false</jk> when the test limit stopped the walk. */ 073 @SuppressWarnings({ 074 "java:S107" // Internal recursive walker; parameters are the traversal state. 075 }) 076 private boolean walk(Map<String,Object> suite, List<String> titles, String parentFile, int depth, ReportLimits lim, ReportBuilder out, 077 List<String> files, List<ReportTest> rows, List<String> rowFiles) { 078 if (depth > MAX_DEPTH) 079 return true; 080 var file = str(suite.get("file")) != null ? str(suite.get("file")) : parentFile; 081 var title = str(suite.get("title")); 082 var path = titles; 083 if (title != null && ! title.isEmpty() && ! title.equals(str(suite.get("file")))) { 084 path = new ArrayList<>(titles); 085 path.add(title); 086 } 087 for (var sp : list(suite.get("specs"))) { 088 var spec = map(sp); 089 if (spec == null) 090 continue; 091 var specFile = str(spec.get("file")) != null ? str(spec.get("file")) : file; 092 var key = specFile == null ? "(unknown file)" : specFile; 093 if (! files.contains(key)) 094 files.add(key); 095 var names = new ArrayList<>(path); 096 if (str(spec.get("title")) != null) 097 names.add(str(spec.get("title"))); 098 for (var t : list(spec.get("tests"))) { 099 var tm = map(t); 100 if (tm == null) 101 continue; 102 if (rows.size() >= lim.maxTests()) { 103 out.truncated = true; 104 out.warnings.add("report truncated at " + lim.maxTests() + " tests"); 105 return false; 106 } 107 rows.add(test(names, tm, lim)); 108 rowFiles.add(key); 109 } 110 } 111 for (var child : list(suite.get("suites"))) { 112 var cm = map(child); 113 if (cm != null && ! walk(cm, path, file, depth + 1, lim, out, files, rows, rowFiles)) 114 return false; 115 } 116 return true; 117 } 118 119 private static ReportTest test(List<String> names, Map<String,Object> t, ReportLimits lim) { 120 var name = String.join(SEP, names); 121 var project = str(t.get("projectName")); 122 if (project != null && ! project.isEmpty()) 123 name += " [" + project + "]"; 124 var results = list(t.get("results")); 125 Map<String,Object> last = null; 126 long total = 0; 127 var hasMs = false; 128 Map<String,Object> failing = null; 129 for (var r : results) { 130 var rm = map(r); 131 if (rm == null) 132 continue; 133 last = rm; 134 var d = ms(rm.get("duration")); 135 if (d != null) { 136 total += d; 137 hasMs = true; 138 } 139 if (failing == null && isFailed(str(rm.get("status")))) 140 failing = rm; 141 } 142 var status = status(str(t.get("status")), last == null ? null : str(last.get("status"))); 143 String msg = null; 144 String trace = null; 145 if (status == TestStatus.FAIL && failing != null) { 146 var text = errorText(failing); 147 if (text != null) { 148 var body = stripAnsi(text).strip(); 149 var first = TerminalViewChecks.firstLine(body); 150 msg = first.isEmpty() ? null : clip(first, lim.maxMsgChars()); 151 trace = clip(body, lim.maxTraceChars()); 152 } 153 } 154 return new ReportTest(null, clip(name, lim.maxNameChars()), status, hasMs ? Long.valueOf(total) : null, msg, trace); 155 } 156 157 private static boolean isFailed(String s) { 158 return "failed".equals(s) || "timedOut".equals(s) || "interrupted".equals(s); 159 } 160 161 private static TestStatus status(String testStatus, String lastResult) { 162 if (testStatus != null) { 163 switch (testStatus) { 164 case "expected", "flaky": return TestStatus.PASS; 165 case "unexpected": return TestStatus.FAIL; 166 case "skipped": return TestStatus.SKIP; 167 default: break; 168 } 169 } 170 if (lastResult == null) 171 return TestStatus.SKIP; 172 if ("passed".equals(lastResult)) 173 return TestStatus.PASS; 174 return isFailed(lastResult) ? TestStatus.FAIL : TestStatus.SKIP; 175 } 176 177 private static String errorText(Map<String,Object> result) { 178 var e = map(result.get("error")); 179 if (e != null && str(e.get("message")) != null) 180 return str(e.get("message")); 181 for (var x : list(result.get("errors"))) { 182 var xm = map(x); 183 if (xm != null && str(xm.get("message")) != null) 184 return str(xm.get("message")); 185 } 186 return null; 187 } 188}