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}