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.*;
020
021import java.util.*;
022
023import org.apache.juneau.terminal.report.TerminalEvent.*;
024
025/**
026 * What a {@link ReportReader} recovered from a report.
027 *
028 * @param fw The framework key.
029 * @param tests The recovered tests, in report order.
030 * @param warnings Human-readable warnings, for example <c>report truncated: unexpected end of file after 4120 tests</c>.
031 * @param truncated Whether the file ended early or the test limit was hit.
032 * @param skippedFiles The number of files that were not read (over the size limit, unreadable).
033 */
034public record ReportResult(String fw, List<ReportTest> tests, List<String> warnings, boolean truncated, int skippedFiles) {
035
036   private static final String UNNAMED = "(unnamed)";
037
038   /**
039    * Constructor.
040    *
041    * @param fw The framework key.
042    * @param tests The recovered tests.
043    * @param warnings Human-readable warnings.
044    * @param truncated Whether the file ended early or the test limit was hit.
045    * @param skippedFiles The number of files that were not read.
046    */
047   public ReportResult {
048      if (! TerminalViewChecks.isFw(fw))
049         throw iaex("ReportResult fw must match ^[a-z0-9][a-z0-9._-]{0,31}$; got '%s'.", fw);
050      tests = List.copyOf(tests);
051      warnings = List.copyOf(warnings);
052   }
053
054   /**
055    * Returns the events a producer appends at the end of a step.
056    *
057    * <p>
058    * The sequence is a <c>replace</c> for the step, one <c>test</c> per recovered test, then one <c>warn</c> note per
059    * warning.  When no test was recovered and there are warnings, only the notes are returned, so the step keeps its
060    * live <c>suite</c> placeholders.  A valid but empty report yields just the <c>replace</c>.
061    *
062    * @param step The id of the step the results belong to.
063    * @return The events, not yet numbered.
064    */
065   public List<TerminalEvent> toEvents(String step) {
066      var out = new ArrayList<TerminalEvent>();
067      if (! (tests.isEmpty() && ! warnings.isEmpty()))
068         out.add(TerminalEvent.replace(step));
069      out.addAll(tests.stream().map(t -> toEvent(step, t)).toList());
070      out.addAll(warnings.stream().map(w -> TerminalEvent.note(Level.WARN, clip(w, TerminalEvent.MAX_NOTE)).withStep(step)).toList());
071      return out;
072   }
073
074   private TerminalEvent toEvent(String step, ReportTest t) {
075      var e = TerminalEvent.test(step, fw, name(t.suite()), name(t.name()), t.status());
076      if (t.ms() != null && t.ms() >= 0 && t.ms() <= TerminalEvent.MAX_SAFE_INT)
077         e = e.withMs(t.ms());
078      var withMsg = t.msg() == null || t.msg().isEmpty() ? e : e.withMsg(clip(t.msg(), TerminalEvent.MAX_MSG));
079      var full = t.trace() == null || t.trace().isEmpty() ? withMsg : withMsg.withTrace(clip(t.trace(), TerminalEvent.MAX_TRACE));
080      // Control characters expand six-fold when serialized, so an extreme test can exceed the event size cap; shed the
081      // bulkiest members rather than lose the test.
082      for (var candidate : List.of(full, withMsg, e)) {
083         try {
084            return candidate.validate();
085         } catch (IllegalArgumentException ex) {
086            // try the next, smaller candidate
087         }
088      }
089      return e;
090   }
091
092   private static String name(String s) {
093      return s == null || s.isEmpty() ? UNNAMED : clip(s, TerminalEvent.MAX_NAME);
094   }
095
096   private static String clip(String s, int max) {
097      return ReportText.clip(s, max);
098   }
099}