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.plugin.surefire.booterclient.output;
20  
21  import javax.annotation.Nonnull;
22  
23  import java.io.File;
24  import java.util.Map;
25  import java.util.Queue;
26  import java.util.Set;
27  import java.util.TreeSet;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.concurrent.ConcurrentLinkedQueue;
30  import java.util.concurrent.atomic.AtomicLong;
31  
32  import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.NotifiableTestStream;
33  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
34  import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
35  import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
36  import org.apache.maven.surefire.api.event.Event;
37  import org.apache.maven.surefire.api.report.ReportEntry;
38  import org.apache.maven.surefire.api.report.RunListener;
39  import org.apache.maven.surefire.api.report.RunMode;
40  import org.apache.maven.surefire.api.report.StackTraceWriter;
41  import org.apache.maven.surefire.api.report.TestOutputReceiver;
42  import org.apache.maven.surefire.api.report.TestOutputReportEntry;
43  import org.apache.maven.surefire.api.report.TestReportListener;
44  import org.apache.maven.surefire.api.report.TestSetReportEntry;
45  import org.apache.maven.surefire.extensions.EventHandler;
46  
47  import static java.lang.System.currentTimeMillis;
48  import static java.util.Collections.unmodifiableMap;
49  import static org.apache.maven.surefire.api.booter.Shutdown.KILL;
50  import static org.apache.maven.surefire.api.report.CategorizedReportEntry.reportEntry;
51  
52  // todo move to the same package with ForkStarter
53  
54  /**
55   * Knows how to reconstruct *all* the state transmitted over stdout by the forked process.
56   *
57   * @author Kristian Rosenvold
58   */
59  public final class ForkClient implements EventHandler<Event> {
60      private static final long START_TIME_ZERO = 0L;
61      private static final long START_TIME_NEGATIVE_TIMEOUT = -1L;
62  
63      private final DefaultReporterFactory defaultReporterFactory;
64  
65      private final Map<String, String> testVmSystemProperties = new ConcurrentHashMap<>();
66  
67      private final NotifiableTestStream notifiableTestStream;
68  
69      private final Queue<String> testsInProgress = new ConcurrentLinkedQueue<>();
70  
71      /**
72       * <em>testSetStartedAt</em> is set to non-zero after received
73       * {@link MasterProcessChannelEncoder#testSetStarting(TestSetReportEntry, boolean)}.
74       */
75      private final AtomicLong testSetStartedAt = new AtomicLong(START_TIME_ZERO);
76  
77      private final ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
78  
79      private final int forkNumber;
80  
81      private volatile TestReportListener<TestOutputReportEntry> testSetReporter;
82  
83      /**
84       * Written by one Thread and read by another: Main Thread and ForkStarter's Thread.
85       */
86      private volatile boolean saidGoodBye;
87  
88      private volatile StackTraceWriter errorInFork;
89  
90      public ForkClient(
91              DefaultReporterFactory defaultReporterFactory, NotifiableTestStream notifiableTestStream, int forkNumber) {
92          this.defaultReporterFactory = defaultReporterFactory;
93          this.notifiableTestStream = notifiableTestStream;
94          this.forkNumber = forkNumber;
95          notifier.setTestSetStartingListener(new TestSetStartingListener());
96          notifier.setTestSetCompletedListener(new TestSetCompletedListener());
97          notifier.setTestStartingListener(new TestStartingListener());
98          notifier.setTestSucceededListener(new TestSucceededListener());
99          notifier.setTestFailedListener(new TestFailedListener());
100         notifier.setTestSkippedListener(new TestSkippedListener());
101         notifier.setTestErrorListener(new TestErrorListener());
102         notifier.setTestAssumptionFailureListener(new TestAssumptionFailureListener());
103         notifier.setSystemPropertiesListener(new SystemPropertiesListener());
104         notifier.setStdOutListener(new StdOutListener());
105         notifier.setStdErrListener(new StdErrListener());
106         notifier.setConsoleInfoListener(new ConsoleListener());
107         notifier.setAcquireNextTestListener(new AcquireNextTestListener());
108         notifier.setConsoleErrorListener(new ErrorListener());
109         notifier.setByeListener(new ByeListener());
110         notifier.setConsoleDebugListener(new DebugListener());
111         notifier.setConsoleWarningListener(new WarningListener());
112         notifier.setExitErrorEventListener(new ExitErrorEventListener());
113     }
114 
115     public void setStopOnNextTestListener(ForkedProcessEventListener listener) {
116         notifier.setStopOnNextTestListener(listener);
117     }
118 
119     private final class TestSetStartingListener implements ForkedProcessReportEventListener<TestSetReportEntry> {
120         @Override
121         public void handle(TestSetReportEntry reportEntry) {
122             getTestSetReporter().testSetStarting(reportEntry);
123             setCurrentStartTime();
124         }
125     }
126 
127     private final class TestSetCompletedListener implements ForkedProcessReportEventListener<TestSetReportEntry> {
128         @Override
129         public void handle(TestSetReportEntry reportEntry) {
130             testsInProgress.clear();
131             TestSetReportEntry entry = reportEntry(
132                     reportEntry.getRunMode(),
133                     reportEntry.getTestRunId(),
134                     reportEntry.getSourceName(),
135                     reportEntry.getSourceText(),
136                     reportEntry.getName(),
137                     reportEntry.getNameText(),
138                     reportEntry.getGroup(),
139                     reportEntry.getStackTraceWriter(),
140                     reportEntry.getElapsed(),
141                     reportEntry.getMessage(),
142                     getTestVmSystemProperties());
143             getTestSetReporter().testSetCompleted(entry);
144         }
145     }
146 
147     private final class TestStartingListener implements ForkedProcessReportEventListener<ReportEntry> {
148         @Override
149         public void handle(ReportEntry reportEntry) {
150             testsInProgress.offer(reportEntry.getSourceName());
151             getTestSetReporter().testStarting(reportEntry);
152         }
153     }
154 
155     private final class TestSucceededListener implements ForkedProcessReportEventListener<ReportEntry> {
156         @Override
157         public void handle(ReportEntry reportEntry) {
158             testsInProgress.remove(reportEntry.getSourceName());
159             getTestSetReporter().testSucceeded(reportEntry);
160         }
161     }
162 
163     private final class TestFailedListener implements ForkedProcessReportEventListener<ReportEntry> {
164         @Override
165         public void handle(ReportEntry reportEntry) {
166             testsInProgress.remove(reportEntry.getSourceName());
167             getTestSetReporter().testFailed(reportEntry);
168         }
169     }
170 
171     private final class TestSkippedListener implements ForkedProcessReportEventListener<ReportEntry> {
172         @Override
173         public void handle(ReportEntry reportEntry) {
174             testsInProgress.remove(reportEntry.getSourceName());
175             getTestSetReporter().testSkipped(reportEntry);
176         }
177     }
178 
179     private final class TestErrorListener implements ForkedProcessReportEventListener<ReportEntry> {
180         @Override
181         public void handle(ReportEntry reportEntry) {
182             testsInProgress.remove(reportEntry.getSourceName());
183             getTestSetReporter().testError(reportEntry);
184         }
185     }
186 
187     private final class TestAssumptionFailureListener implements ForkedProcessReportEventListener<ReportEntry> {
188         @Override
189         public void handle(ReportEntry reportEntry) {
190             testsInProgress.remove(reportEntry.getSourceName());
191             getTestSetReporter().testAssumptionFailure(reportEntry);
192         }
193     }
194 
195     private final class SystemPropertiesListener implements ForkedProcessPropertyEventListener {
196         @Override
197         public void handle(String key, String value, RunMode runMode, Long testRunId) {
198             testVmSystemProperties.put(key, value);
199         }
200     }
201 
202     private final class StdOutListener implements ForkedProcessStandardOutErrEventListener {
203         @Override
204         public void handle(String output, boolean newLine, RunMode runMode, Long testRunId) {
205             writeTestOutput(output, true, newLine, runMode, testRunId);
206         }
207     }
208 
209     private final class StdErrListener implements ForkedProcessStandardOutErrEventListener {
210         @Override
211         public void handle(String output, boolean newLine, RunMode runMode, Long testRunId) {
212             writeTestOutput(output, false, newLine, runMode, testRunId);
213         }
214     }
215 
216     private final class ConsoleListener implements ForkedProcessStringEventListener {
217         @Override
218         public void handle(String msg) {
219             getOrCreateConsoleLogger().info(msg);
220         }
221     }
222 
223     private final class AcquireNextTestListener implements ForkedProcessEventListener {
224         @Override
225         public void handle() {
226             notifiableTestStream.provideNewTest();
227         }
228     }
229 
230     private class ErrorListener implements ForkedProcessStackTraceEventListener {
231         @Override
232         public void handle(@Nonnull StackTraceWriter stackTrace) {
233             String msg = stackTrace.getThrowable().getMessage();
234             if (errorInFork == null) {
235                 errorInFork = stackTrace.writeTraceToString() != null ? stackTrace : null;
236                 if (msg != null) {
237                     getOrCreateConsoleLogger().error(msg);
238                 }
239             }
240             dumpToLoFile(msg);
241         }
242     }
243 
244     private final class ByeListener implements ForkedProcessEventListener {
245         @Override
246         public void handle() {
247             saidGoodBye = true;
248             notifiableTestStream.acknowledgeByeEventReceived();
249         }
250     }
251 
252     private final class DebugListener implements ForkedProcessStringEventListener {
253         @Override
254         public void handle(String msg) {
255             getOrCreateConsoleLogger().debug(msg);
256         }
257     }
258 
259     private final class WarningListener implements ForkedProcessStringEventListener {
260         @Override
261         public void handle(String msg) {
262             getOrCreateConsoleLogger().warning(msg);
263         }
264     }
265 
266     private final class ExitErrorEventListener implements ForkedProcessExitErrorListener {
267         @Override
268         public void handle(StackTraceWriter stackTrace) {
269             getOrCreateConsoleLogger().error("The surefire booter JVM" + forkNumber + " was interrupted and exits.");
270         }
271     }
272 
273     public void kill() {
274         if (!saidGoodBye) {
275             notifiableTestStream.shutdown(KILL);
276         }
277     }
278 
279     /**
280      * Called in concurrent Thread.
281      * Will shutdown if timeout was reached.
282      *
283      * @param currentTimeMillis    current time in millis seconds
284      * @param forkedProcessTimeoutInSeconds timeout in seconds given by MOJO
285      */
286     public void tryToTimeout(long currentTimeMillis, int forkedProcessTimeoutInSeconds) {
287         if (forkedProcessTimeoutInSeconds > 0) {
288             final long forkedProcessTimeoutInMillis = 1000L * forkedProcessTimeoutInSeconds;
289             final long startedAt = testSetStartedAt.get();
290             if (startedAt > START_TIME_ZERO && currentTimeMillis - startedAt >= forkedProcessTimeoutInMillis) {
291                 testSetStartedAt.set(START_TIME_NEGATIVE_TIMEOUT);
292                 notifiableTestStream.shutdown(KILL);
293             }
294         }
295     }
296 
297     public DefaultReporterFactory getDefaultReporterFactory() {
298         return defaultReporterFactory;
299     }
300 
301     @Override
302     public void handleEvent(@Nonnull Event event) {
303         notifier.notifyEvent(event);
304     }
305 
306     private void setCurrentStartTime() {
307         if (testSetStartedAt.get() == START_TIME_ZERO) // JIT can optimize <= no JNI call
308         {
309             // Not necessary to call JNI library library #currentTimeMillis
310             // which may waste 10 - 30 machine cycles in callback. Callbacks should be fast.
311             testSetStartedAt.compareAndSet(START_TIME_ZERO, currentTimeMillis());
312         }
313     }
314 
315     public boolean hadTimeout() {
316         return testSetStartedAt.get() == START_TIME_NEGATIVE_TIMEOUT;
317     }
318 
319     /**
320      * Only {@link #getConsoleOutputReceiver()} may call this method in another Thread.
321      */
322     private TestReportListener<TestOutputReportEntry> getTestSetReporter() {
323         if (testSetReporter == null) {
324             synchronized (this) {
325                 if (testSetReporter == null) {
326                     testSetReporter = defaultReporterFactory.createTestReportListener();
327                 }
328             }
329         }
330         return testSetReporter;
331     }
332 
333     void dumpToLoFile(String msg) {
334         File reportsDir = defaultReporterFactory.getReportsDirectory();
335         InPluginProcessDumpSingleton util = InPluginProcessDumpSingleton.getSingleton();
336         util.dumpStreamText(msg, reportsDir, forkNumber);
337     }
338 
339     private void writeTestOutput(String output, boolean isStdout, boolean newLine, RunMode runMode, Long testRunId) {
340         getConsoleOutputReceiver()
341                 .writeTestOutput(new TestOutputReportEntry(output, isStdout, newLine, runMode, testRunId));
342     }
343 
344     public Map<String, String> getTestVmSystemProperties() {
345         return unmodifiableMap(testVmSystemProperties);
346     }
347 
348     /**
349      * Used when getting reporters on the plugin side of a fork.
350      * Used by testing purposes only. May not be volatile variable.
351      *
352      * @return A mock provider reporter
353      */
354     public RunListener getReporter() {
355         return getTestSetReporter();
356     }
357 
358     public TestOutputReceiver<TestOutputReportEntry> getConsoleOutputReceiver() {
359         return getTestSetReporter();
360     }
361 
362     private ConsoleLogger getOrCreateConsoleLogger() {
363         return getTestSetReporter();
364     }
365 
366     public void close(boolean hadTimeout) {
367         // no op
368     }
369 
370     public boolean isSaidGoodBye() {
371         return saidGoodBye;
372     }
373 
374     public StackTraceWriter getErrorInFork() {
375         return errorInFork;
376     }
377 
378     public boolean isErrorInFork() {
379         return errorInFork != null;
380     }
381 
382     public Set<String> testsInProgress() {
383         return new TreeSet<>(testsInProgress);
384     }
385 
386     public boolean hasTestsInProgress() {
387         return !testsInProgress.isEmpty();
388     }
389 }