View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.surefire.junitplatform;
20  
21  import java.util.Collections;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Objects;
25  import java.util.Optional;
26  import java.util.concurrent.ConcurrentHashMap;
27  import java.util.concurrent.ConcurrentMap;
28  import java.util.stream.Stream;
29  
30  import org.apache.maven.surefire.api.report.OutputReportEntry;
31  import org.apache.maven.surefire.api.report.RunMode;
32  import org.apache.maven.surefire.api.report.SafeThrowable;
33  import org.apache.maven.surefire.api.report.SimpleReportEntry;
34  import org.apache.maven.surefire.api.report.StackTraceWriter;
35  import org.apache.maven.surefire.api.report.TestOutputReceiver;
36  import org.apache.maven.surefire.api.report.TestOutputReportEntry;
37  import org.apache.maven.surefire.api.report.TestReportListener;
38  import org.apache.maven.surefire.report.ClassMethodIndexer;
39  import org.apache.maven.surefire.report.PojoStackTraceWriter;
40  import org.apache.maven.surefire.report.RunModeSetter;
41  import org.junit.platform.engine.TestExecutionResult;
42  import org.junit.platform.engine.TestSource;
43  import org.junit.platform.engine.support.descriptor.ClassSource;
44  import org.junit.platform.engine.support.descriptor.MethodSource;
45  import org.junit.platform.launcher.TestExecutionListener;
46  import org.junit.platform.launcher.TestIdentifier;
47  import org.junit.platform.launcher.TestPlan;
48  
49  import static java.util.Collections.emptyMap;
50  import static java.util.stream.Collectors.joining;
51  import static java.util.stream.Collectors.toList;
52  import static org.apache.maven.surefire.api.util.internal.ObjectUtils.systemProps;
53  import static org.apache.maven.surefire.shared.lang3.StringUtils.isNotBlank;
54  import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
55  
56  /**
57   * @since 2.22.0
58   */
59  final class RunListenerAdapter implements TestExecutionListener, TestOutputReceiver<OutputReportEntry>, RunModeSetter {
60      private final ClassMethodIndexer classMethodIndexer = new ClassMethodIndexer();
61      private final ConcurrentMap<TestIdentifier, Long> testStartTime = new ConcurrentHashMap<>();
62      private final ConcurrentMap<TestIdentifier, TestExecutionResult> failures = new ConcurrentHashMap<>();
63      private final ConcurrentMap<String, TestIdentifier> runningTestIdentifiersByUniqueId = new ConcurrentHashMap<>();
64      private final TestReportListener<TestOutputReportEntry> runListener;
65      private volatile TestPlan testPlan;
66      private volatile RunMode runMode;
67  
68      RunListenerAdapter(TestReportListener<TestOutputReportEntry> runListener) {
69          this.runListener = runListener;
70      }
71  
72      @Override
73      public void setRunMode(RunMode runMode) {
74          this.runMode = runMode;
75      }
76  
77      @Override
78      public void testPlanExecutionStarted(TestPlan testPlan) {
79          this.testPlan = testPlan;
80      }
81  
82      @Override
83      public void testPlanExecutionFinished(TestPlan testPlan) {
84          this.testPlan = null;
85          testStartTime.clear();
86      }
87  
88      @Override
89      public void executionStarted(TestIdentifier testIdentifier) {
90          runningTestIdentifiersByUniqueId.put(testIdentifier.getUniqueId(), testIdentifier);
91  
92          if (testIdentifier.isContainer()
93                  && testIdentifier
94                          .getSource()
95                          .filter(ClassSource.class::isInstance)
96                          .isPresent()) {
97              testStartTime.put(testIdentifier, System.currentTimeMillis());
98              runListener.testSetStarting(createReportEntry(testIdentifier));
99          } else if (testIdentifier.isTest()) {
100             testStartTime.put(testIdentifier, System.currentTimeMillis());
101             runListener.testStarting(createReportEntry(testIdentifier));
102         }
103     }
104 
105     @Override
106     public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
107         boolean isClass = testIdentifier.isContainer()
108                 && testIdentifier
109                         .getSource()
110                         .filter(ClassSource.class::isInstance)
111                         .isPresent();
112 
113         boolean isTest = testIdentifier.isTest();
114 
115         boolean failed = testExecutionResult.getStatus() == FAILED;
116 
117         boolean isAssertionError = testExecutionResult
118                 .getThrowable()
119                 .filter(AssertionError.class::isInstance)
120                 .isPresent();
121 
122         boolean isRootContainer =
123                 testIdentifier.isContainer() && !testIdentifier.getParentId().isPresent();
124 
125         if (failed || isClass || isTest) {
126             Integer elapsed = computeElapsedTime(testIdentifier);
127             switch (testExecutionResult.getStatus()) {
128                 case ABORTED:
129                     if (isTest) {
130                         runListener.testAssumptionFailure(
131                                 createReportEntry(testIdentifier, testExecutionResult, elapsed));
132                     } else {
133                         runListener.testSetCompleted(
134                                 createReportEntry(testIdentifier, testExecutionResult, systemProps(), null, elapsed));
135                     }
136                     break;
137                 case FAILED:
138                     String reason =
139                             safeGetMessage(testExecutionResult.getThrowable().orElse(null));
140                     SimpleReportEntry reportEntry =
141                             createReportEntry(testIdentifier, testExecutionResult, reason, elapsed);
142                     if (isAssertionError) {
143                         runListener.testFailed(reportEntry);
144                     } else {
145                         runListener.testError(reportEntry);
146                     }
147                     if (isClass || isRootContainer) {
148                         runListener.testSetCompleted(
149                                 createReportEntry(testIdentifier, null, systemProps(), null, elapsed));
150                     }
151                     failures.put(testIdentifier, testExecutionResult);
152                     break;
153                 default:
154                     if (isTest) {
155                         runListener.testSucceeded(createReportEntry(testIdentifier, null, elapsed));
156                     } else {
157                         runListener.testSetCompleted(
158                                 createReportEntry(testIdentifier, null, systemProps(), null, elapsed));
159                     }
160             }
161         }
162 
163         runningTestIdentifiersByUniqueId.remove(testIdentifier.getUniqueId());
164     }
165 
166     private Integer computeElapsedTime(TestIdentifier testIdentifier) {
167         Long startTime = testStartTime.remove(testIdentifier);
168         long endTime = System.currentTimeMillis();
169         return startTime == null ? null : (int) (endTime - startTime);
170     }
171 
172     private Stream<TestIdentifier> collectAllTestIdentifiersInHierarchy(TestIdentifier testIdentifier) {
173         return testIdentifier
174                 .getParentId()
175                 .map(runningTestIdentifiersByUniqueId::get)
176                 .map(parentTestIdentifier -> Stream.concat(
177                         Stream.of(parentTestIdentifier), collectAllTestIdentifiersInHierarchy(parentTestIdentifier)))
178                 .orElseGet(Stream::empty);
179     }
180 
181     private String safeGetMessage(Throwable throwable) {
182         try {
183             SafeThrowable t = throwable == null ? null : new SafeThrowable(throwable);
184             return t == null ? null : t.getMessage();
185         } catch (Throwable t) {
186             return t.getMessage();
187         }
188     }
189 
190     @Override
191     public void executionSkipped(TestIdentifier testIdentifier, String reason) {
192         boolean isClass = testIdentifier.isContainer()
193                 && testIdentifier
194                         .getSource()
195                         .filter(ClassSource.class::isInstance)
196                         .isPresent();
197 
198         testStartTime.remove(testIdentifier);
199 
200         if (isClass) {
201             SimpleReportEntry report = createReportEntry(testIdentifier);
202             runListener.testSetStarting(report);
203             for (TestIdentifier child : testPlan.getChildren(testIdentifier)) {
204                 runListener.testSkipped(createReportEntry(child, null, emptyMap(), reason, null));
205             }
206             runListener.testSetCompleted(report);
207         } else {
208             runListener.testSkipped(createReportEntry(testIdentifier, null, emptyMap(), reason, null));
209         }
210     }
211 
212     private SimpleReportEntry createReportEntry(
213             TestIdentifier testIdentifier,
214             TestExecutionResult testExecutionResult,
215             Map<String, String> systemProperties,
216             String reason,
217             Integer elapsedTime) {
218         String[] classMethodName = toClassMethodName(testIdentifier);
219         String className = classMethodName[0];
220         String classText = classMethodName[1];
221         if (Objects.equals(className, classText)) {
222             classText = null;
223         }
224         boolean failed = testExecutionResult == null || testExecutionResult.getStatus() == FAILED;
225         String methodName = failed || testIdentifier.isTest() ? classMethodName[2] : null;
226         String methodText = failed || testIdentifier.isTest() ? classMethodName[3] : null;
227         if (Objects.equals(methodName, methodText)) {
228             methodText = null;
229         }
230         StackTraceWriter stw =
231                 testExecutionResult == null ? null : toStackTraceWriter(className, methodName, testExecutionResult);
232         return new SimpleReportEntry(
233                 runMode,
234                 classMethodIndexer.indexClassMethod(className, methodName),
235                 className,
236                 classText,
237                 methodName,
238                 methodText,
239                 stw,
240                 elapsedTime,
241                 reason,
242                 systemProperties);
243     }
244 
245     private SimpleReportEntry createReportEntry(TestIdentifier testIdentifier) {
246         return createReportEntry(testIdentifier, null, null);
247     }
248 
249     private SimpleReportEntry createReportEntry(
250             TestIdentifier testIdentifier, TestExecutionResult testExecutionResult, Integer elapsedTime) {
251         return createReportEntry(testIdentifier, testExecutionResult, emptyMap(), null, elapsedTime);
252     }
253 
254     private SimpleReportEntry createReportEntry(
255             TestIdentifier testIdentifier,
256             TestExecutionResult testExecutionResult,
257             String reason,
258             Integer elapsedTime) {
259         return createReportEntry(testIdentifier, testExecutionResult, emptyMap(), reason, elapsedTime);
260     }
261 
262     private StackTraceWriter toStackTraceWriter(
263             String realClassName, String realMethodName, TestExecutionResult testExecutionResult) {
264         switch (testExecutionResult.getStatus()) {
265             case ABORTED:
266             case FAILED:
267                 // Failed tests must have a StackTraceWriter, otherwise Surefire will fail
268                 Throwable exception = testExecutionResult.getThrowable().orElse(null);
269                 return toStackTraceWriter(realClassName, realMethodName, exception);
270             default:
271                 return testExecutionResult
272                         .getThrowable()
273                         .map(t -> toStackTraceWriter(realClassName, realMethodName, t))
274                         .orElse(null);
275         }
276     }
277 
278     private StackTraceWriter toStackTraceWriter(String realClassName, String realMethodName, Throwable throwable) {
279         return new PojoStackTraceWriter(realClassName, realMethodName, throwable);
280     }
281 
282     /**
283      * <ul>
284      *     <li>[0] class name - used in stacktrace parser</li>
285      *     <li>[1] class display name</li>
286      *     <li>[2] method signature - used in stacktrace parser</li>
287      *     <li>[3] method display name</li>
288      * </ul>
289      *
290      * @param testIdentifier a class or method
291      * @return 4 elements string array
292      */
293     private String[] toClassMethodName(TestIdentifier testIdentifier) {
294         Optional<TestSource> testSource = testIdentifier.getSource();
295         String display = testIdentifier.getDisplayName();
296 
297         if (testSource.filter(MethodSource.class::isInstance).isPresent()) {
298             MethodSource methodSource = testSource.map(MethodSource.class::cast).get();
299             String realClassName = methodSource.getClassName();
300 
301             String[] source = collectAllTestIdentifiersInHierarchy(testIdentifier)
302                     .filter(i ->
303                             i.getSource().map(ClassSource.class::isInstance).orElse(false))
304                     .findFirst()
305                     .map(this::toClassMethodName)
306                     .map(s -> new String[] {s[0], s[1]})
307                     .orElse(new String[] {realClassName, realClassName});
308 
309             String parentDisplay = collectAllTestIdentifiersInHierarchy(testIdentifier)
310                     .filter(identifier -> identifier
311                             .getSource()
312                             .filter(MethodSource.class::isInstance)
313                             .isPresent())
314                     .map(TestIdentifier::getDisplayName)
315                     .collect(joining(" "));
316 
317             boolean needsSpaceSeparator = isNotBlank(parentDisplay) && !display.startsWith("[");
318             String methodDisplay = parentDisplay + (needsSpaceSeparator ? " " : "") + display;
319 
320             boolean hasParameterizedParent = collectAllTestIdentifiersInHierarchy(testIdentifier)
321                     .filter(identifier -> !identifier.getSource().isPresent())
322                     .map(TestIdentifier::getLegacyReportingName)
323                     .anyMatch(legacyReportingName -> legacyReportingName.matches("^\\[.+]$"));
324 
325             boolean parameterized = isNotBlank(methodSource.getMethodParameterTypes()) || hasParameterizedParent;
326             String methodName = methodSource.getMethodName();
327             String description = testIdentifier.getLegacyReportingName();
328             boolean equalDescriptions = methodDisplay.equals(description);
329             boolean hasLegacyDescription = description.startsWith(methodName + '(');
330             boolean hasDisplayName = !equalDescriptions || !hasLegacyDescription;
331             String methodDesc = parameterized ? description : methodName;
332             String methodDisp = hasDisplayName ? methodDisplay : methodDesc;
333 
334             // The behavior of methods getLegacyReportingName() and getDisplayName().
335             //     junit4    ||  legacy  |  display
336             // ==============||==========|==========
337             //     normal    ||     m    |     m
338             //     param     ||   m[0]   |   m[0]
339             //  param+displ  || m[displ] | m[displ]
340 
341             //     junit5    ||  legacy  |  display
342             // ==============||==========|==========
343             //    normal     ||    m()   |    m()
344             //  normal+displ ||    m()   |   displ
345             //     param     ||  m()[1]  | [1] <param>
346             //  param+displ  ||  m()[1]  |   displ
347 
348             return new String[] {source[0], source[1], methodDesc, methodDisp};
349         } else if (testSource.filter(ClassSource.class::isInstance).isPresent()) {
350             List<String> parentClassDisplays = collectAllTestIdentifiersInHierarchy(testIdentifier)
351                     .filter(identifier -> identifier
352                             .getSource()
353                             .filter(ClassSource.class::isInstance)
354                             .isPresent())
355                     .map(TestIdentifier::getDisplayName)
356                     .collect(toList());
357 
358             Collections.reverse(parentClassDisplays);
359             String classDisplay = Stream.concat(parentClassDisplays.stream(), Stream.of(display))
360                     .collect(joining(" "));
361 
362             ClassSource classSource = testSource.map(ClassSource.class::cast).get();
363             String className = classSource.getClassName();
364             String simpleClassName = className.substring(1 + className.lastIndexOf('.'));
365             String source = classDisplay.replace(' ', '$').equals(simpleClassName) ? className : classDisplay;
366             return new String[] {className, source, null, null};
367         } else {
368             String source = testPlan.getParent(testIdentifier)
369                     .map(TestIdentifier::getDisplayName)
370                     .orElse(display);
371             return new String[] {source, source, display, display};
372         }
373     }
374 
375     /**
376      * @return Map of tests that failed.
377      */
378     Map<TestIdentifier, TestExecutionResult> getFailures() {
379         return failures;
380     }
381 
382     boolean hasFailingTests() {
383         return !getFailures().isEmpty();
384     }
385 
386     void reset() {
387         getFailures().clear();
388         testPlan = null;
389     }
390 
391     @Override
392     public void writeTestOutput(OutputReportEntry reportEntry) {
393         Long testRunId = classMethodIndexer.getLocalIndex();
394         runListener.writeTestOutput(new TestOutputReportEntry(reportEntry, runMode, testRunId));
395     }
396 }