View Javadoc
1   /*
2    * Copyright 2023 Adrian Herscu
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package dev.aherscu.qa.jgiven.reporter;
18  
19  import static dev.aherscu.qa.testing.utils.StringUtilsExtensions.*;
20  import static java.lang.Double.*;
21  import static java.lang.Long.*;
22  import static java.text.MessageFormat.format;
23  import static java.time.format.DateTimeFormatter.*;
24  
25  import java.io.*;
26  import java.nio.charset.*;
27  import java.time.*;
28  import java.util.*;
29  import java.util.Base64;
30  
31  import org.apache.commons.codec.binary.*;
32  import org.apache.commons.io.output.*;
33  
34  import com.samskivert.mustache.*;
35  import com.tngtech.jgiven.report.model.*;
36  
37  import dev.aherscu.qa.testing.utils.*;
38  import lombok.*;
39  import lombok.experimental.*;
40  import lombok.extern.slf4j.*;
41  
42  /**
43   * Reporting model, comprising JGiven's report model, additional fields and
44   * <a href="https://github.com/samskivert/jmustache">Mustache</a> template
45   * methods. The reporting model contains all information that can be used to
46   * generate a report. Template methods can be used for more advanced
47   * manipulation that cannot be handled by out-of-the-box Mustache engine
48   * functions.
49   * <p>
50   * Reporters may use it as is, or extend it with even more fields and template
51   * methods per need.
52   * </p>
53   * <p>
54   * Reports are generated by invoking the Mustache engine with a specific
55   * template and a report model.
56   * </p>
57   *
58   * @see AbstractQaJgivenReporter
59   *
60   * @param <T>
61   *            one of JGiven's report models: {@link CompleteReportModel},
62   *            {@link ScenarioModel}, or {@link ReportModelFile}
63   */
64  @SuperBuilder(toBuilder = true)
65  @Slf4j
66  @SuppressWarnings("ClassWithTooManyFields")
67  @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(
68      value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD",
69      justification = "referenced from JMustache template at runtime")
70  public class QaJGivenReportModel<T> {
71      public final String          screenshotScale;
72      public final String          testDocumentId;
73      public final String          testDocumentRev;
74      public final String          specDocumentId;
75      public final String          specDocumentRev;
76      public final String          planDocumentId;
77      public final String          planDocumentRev;
78      public final String          traceabilityDocumentId;
79      public final String          traceabilityDocumentRev;
80      public final String          productName;
81      public final String          productVersion;
82      public final T               jgivenReport;
83      public final String          datePattern;
84      public final File            targetReportFile;
85  
86      public final Mustache.Lambda shorten            =
87          (frag, out) -> out.write(abbreviateMiddle(prettified(frag.execute()),
88              ELLIPSIS, 1024));
89  
90      public final Mustache.Lambda deleteEOL          =
91          (frag, out) -> out.write(normalizeSpace(frag.execute()
92              .replaceAll("[\\r\\n]", SPACE)));
93  
94      public final Mustache.Lambda nanoToMillis       =
95          (frag, out) -> out.write(Long
96              .toString(parseLong(frag.execute()) / 1_000_000));
97  
98      public final Mustache.Lambda asId               =
99          (frag, out) -> out.write(Integer
100             .toHexString(frag.execute().hashCode())
101             .toUpperCase(Locale.ENGLISH));
102 
103     public final Mustache.Lambda simpleName         =
104         (frag, out) -> out.write(substringAfterLast(frag.execute(), DOT));
105 
106     public final Mustache.Lambda translateIntroWord =
107         this::translateIntroWord;
108 
109     public final Mustache.Lambda scaleImage         =
110         this::scaleImage;
111 
112     public final Mustache.Lambda isStepFailed       =
113         (frag, out) -> out.write(
114             StepStatus.FAILED.equals(((StepModel) frag.context()).getStatus())
115                 ? frag.execute()
116                 : EMPTY);
117 
118     public final Mustache.Lambda isStepPassed       =
119         (frag, out) -> out.write(
120             StepStatus.PASSED.equals(((StepModel) frag.context()).getStatus())
121                 ? frag.execute()
122                 : EMPTY);
123 
124     private final ZonedDateTime  date               =
125         ZonedDateTime.now();
126 
127     public final String date() {
128         return date.format(ofPattern(datePattern));
129     }
130 
131     @SuppressWarnings("resource")
132     public void scaleImage(
133         final Template.Fragment frag,
134         final Writer out) {
135         ImageUtils.Pipeline
136             .from(new ByteArrayInputStream(Base64
137                 .getMimeDecoder()
138                 .decode(frag.execute().getBytes(StandardCharsets.UTF_8))))
139             .scale(parseDouble(screenshotScale), parseDouble(screenshotScale))
140             // NOTE JMustache requires the output stream to be left open
141             .into(new Base64OutputStream(
142                 new WriterOutputStream(out, StandardCharsets.UTF_8),
143                 true, -1, null),
144                 "png");
145     }
146 
147     @SneakyThrows
148     private void translateIntroWord(
149         final Template.Fragment frag,
150         final Writer out) {
151 
152         val introWord = frag.execute().toLowerCase(Locale.US);
153         switch (introWord) {
154         case "given":
155             out.write("Pre-condition(s)");
156             break;
157         case "when":
158             out.write("Operation(s)");
159             break;
160         case "then":
161             out.write("Verification(s)");
162             break;
163         case "and":
164         case "with":
165             out.write("and");
166             break;
167         default:
168             log.warn(format("unrecognized introduction word [{0}]",
169                 introWord));
170         }
171     }
172 }