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.commons.utils.Shorts.*; 020import static org.apache.juneau.terminal.report.ReportJson.*; 021import static org.apache.juneau.terminal.report.ReportText.*; 022 023import java.nio.file.*; 024import java.util.*; 025import java.util.stream.*; 026 027import org.apache.juneau.terminal.report.TerminalEvent.*; 028 029/** 030 * Reads Jest's <c>--json</c> output. 031 * 032 * <p> 033 * The framework key is <c>jest</c>. A test file that failed to run (no assertions, a non-empty message) becomes one 034 * <c>error</c> test named <c>(suite failed to run)</c>, so a crashed file is a visible red block rather than silence. 035 * ANSI colour codes are stripped from messages. A truncated file yields no tests and a warning. 036 */ 037public final class JestJsonReportReader implements ReportReader { 038 039 private static final String CRASHED = "(suite failed to run)"; 040 041 @Override /* ReportReader */ 042 public String fw() { 043 return "jest"; 044 } 045 046 @Override /* ReportReader */ 047 @SuppressWarnings({ 048 "java:S135", // Guard clauses keep the loop readable. 049 "java:S3776" // Walking files and their assertions is inherently nested. 050 }) 051 public ReportResult read(Path path, ReportLimits lim) { 052 var out = new ReportBuilder(); 053 var root = readRoot(path, lim, out); 054 if (root == null) 055 return out.build(fw()); 056 057 var files = new ArrayList<String>(); 058 var rows = new ArrayList<ReportTest>(); 059 var rowFiles = new ArrayList<String>(); 060 for (var tr : list(root.get("testResults"))) { 061 var file = map(tr); 062 if (file == null) 063 continue; 064 var raw = str(file.get("name")) == null ? "(unknown file)" : str(file.get("name")); 065 if (! files.contains(raw)) 066 files.add(raw); 067 var assertions = list(file.get("assertionResults")); 068 var fileMsg = str(file.get("message")); 069 if (assertions.isEmpty() && "failed".equals(str(file.get("status"))) && inb(fileMsg)) { 070 if (rows.size() >= lim.maxTests()) { 071 out.truncated = true; 072 out.warnings.add("report truncated at " + lim.maxTests() + " tests"); 073 break; 074 } 075 var body = stripAnsi(fileMsg).strip(); 076 rows.add(new ReportTest(null, CRASHED, TestStatus.ERROR, null, clip(firstLine(body), lim.maxMsgChars()), clip(body, lim.maxTraceChars()))); 077 rowFiles.add(raw); 078 continue; 079 } 080 if (! addAssertions(assertions, raw, lim, out, rows, rowFiles)) 081 break; 082 } 083 var display = stripCommonDir(files); 084 for (var i = 0; i < rows.size(); i++) { 085 var r = rows.get(i); 086 var suite = clip(display.get(files.indexOf(rowFiles.get(i))), lim.maxNameChars()); 087 out.tests.add(new ReportTest(suite, r.name(), r.status(), r.ms(), r.msg(), r.trace())); 088 } 089 return out.build(fw()); 090 } 091 092 // Returns false when the test limit was reached. 093 private static boolean addAssertions(List<?> assertions, String raw, ReportLimits lim, ReportBuilder out, List<ReportTest> rows, List<String> rowFiles) { 094 for (var a : assertions) { 095 var am = map(a); 096 if (am == null) 097 continue; 098 if (rows.size() >= lim.maxTests()) { 099 out.truncated = true; 100 out.warnings.add("report truncated at " + lim.maxTests() + " tests"); 101 return false; 102 } 103 rows.add(assertion(am, lim)); 104 rowFiles.add(raw); 105 } 106 return true; 107 } 108 109 private static ReportTest assertion(Map<String,Object> a, ReportLimits lim) { 110 var name = str(a.get("fullName")); 111 if (ie(name)) { 112 var parts = list(a.get("ancestorTitles")).stream().filter(String.class::isInstance).map(String.class::cast) 113 .collect(Collectors.toCollection(ArrayList::new)); 114 var title = str(a.get("title")); 115 if (title != null) 116 parts.add(title); 117 name = String.join(" ", parts); 118 } 119 var status = switch (String.valueOf(a.get("status"))) { 120 case "passed" -> TestStatus.PASS; 121 case "failed" -> TestStatus.FAIL; 122 default -> TestStatus.SKIP; 123 }; 124 String msg = null; 125 String trace = null; 126 var fm = list(a.get("failureMessages")).stream().filter(String.class::isInstance).map(String.class::cast) 127 .map(ReportText::stripAnsi).toList(); 128 if (! fm.isEmpty()) { 129 var first = firstLine(fm.get(0).strip()); 130 msg = first.isEmpty() ? null : clip(first, lim.maxMsgChars()); 131 trace = clip(String.join("\n", fm), lim.maxTraceChars()); 132 } 133 return new ReportTest(null, clip(name, lim.maxNameChars()), status, ms(a.get("duration")), msg, trace); 134 } 135 136 private static String firstLine(String s) { 137 return TerminalViewChecks.firstLine(s); 138 } 139}