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.junitcore.pc;
20  
21  import java.util.Collection;
22  import java.util.TreeSet;
23  import java.util.concurrent.Callable;
24  import java.util.concurrent.ExecutionException;
25  import java.util.concurrent.Executors;
26  import java.util.concurrent.Future;
27  import java.util.concurrent.ScheduledExecutorService;
28  import java.util.concurrent.ThreadFactory;
29  
30  import org.apache.maven.surefire.api.testset.TestSetFailedException;
31  import org.apache.maven.surefire.api.util.internal.DaemonThreadFactory;
32  import org.junit.runner.Computer;
33  import org.junit.runner.Description;
34  
35  import static java.util.concurrent.TimeUnit.NANOSECONDS;
36  
37  /**
38   * ParallelComputer extends JUnit {@link Computer} and has a shutdown functionality.
39   *
40   * @author Tibor Digana (tibor17)
41   * @see ParallelComputerBuilder
42   * @since 2.16
43   */
44  public abstract class ParallelComputer extends Computer {
45      private static final ThreadFactory DAEMON_THREAD_FACTORY = DaemonThreadFactory.newDaemonThreadFactory();
46  
47      private static final double NANOS_IN_A_SECOND = 1E9;
48  
49      private final ShutdownStatus shutdownStatus = new ShutdownStatus();
50  
51      private final ShutdownStatus forcedShutdownStatus = new ShutdownStatus();
52  
53      private final long timeoutNanos;
54  
55      private final long timeoutForcedNanos;
56  
57      private ScheduledExecutorService shutdownScheduler;
58  
59      public ParallelComputer(double timeoutInSeconds, double timeoutForcedInSeconds) {
60          this.timeoutNanos = secondsToNanos(timeoutInSeconds);
61          this.timeoutForcedNanos = secondsToNanos(timeoutForcedInSeconds);
62      }
63  
64      protected abstract ShutdownResult describeStopped(boolean shutdownNow);
65  
66      protected abstract boolean shutdownThreadPoolsAwaitingKilled();
67  
68      protected final void beforeRunQuietly() {
69          shutdownStatus.setDescriptionsBeforeShutdown(hasTimeout() ? scheduleShutdown() : null);
70          forcedShutdownStatus.setDescriptionsBeforeShutdown(hasTimeoutForced() ? scheduleForcedShutdown() : null);
71      }
72  
73      protected final boolean afterRunQuietly() {
74          shutdownStatus.tryFinish();
75          forcedShutdownStatus.tryFinish();
76          boolean notInterrupted = true;
77          if (shutdownScheduler != null) {
78              shutdownScheduler.shutdownNow();
79              /**
80               * Clear <i>interrupted status</i> of the (main) Thread.
81               * Could be previously interrupted by {@link InvokerStrategy} after triggering immediate shutdown.
82               */
83              Thread.interrupted();
84              try {
85                  shutdownScheduler.awaitTermination(Long.MAX_VALUE, NANOSECONDS);
86              } catch (InterruptedException e) {
87                  notInterrupted = false;
88              }
89          }
90          notInterrupted &= shutdownThreadPoolsAwaitingKilled();
91          return notInterrupted;
92      }
93  
94      public String describeElapsedTimeout() throws TestSetFailedException {
95          final StringBuilder msg = new StringBuilder();
96          final boolean isShutdownTimeout = shutdownStatus.isTimeoutElapsed();
97          final boolean isForcedShutdownTimeout = forcedShutdownStatus.isTimeoutElapsed();
98          if (isShutdownTimeout || isForcedShutdownTimeout) {
99              msg.append("The test run has finished abruptly after timeout of ");
100             msg.append(nanosToSeconds(minTimeout(timeoutNanos, timeoutForcedNanos)));
101             msg.append(" seconds.\n");
102 
103             try {
104                 final TreeSet<String> executedTests = new TreeSet<>();
105                 final TreeSet<String> incompleteTests = new TreeSet<>();
106 
107                 if (isShutdownTimeout) {
108                     printShutdownHook(executedTests, incompleteTests, shutdownStatus.getDescriptionsBeforeShutdown());
109                 }
110 
111                 if (isForcedShutdownTimeout) {
112                     printShutdownHook(
113                             executedTests, incompleteTests, forcedShutdownStatus.getDescriptionsBeforeShutdown());
114                 }
115 
116                 if (!executedTests.isEmpty()) {
117                     msg.append("These tests were executed in prior to the shutdown operation:\n");
118                     for (String executedTest : executedTests) {
119                         msg.append(executedTest).append('\n');
120                     }
121                 }
122 
123                 if (!incompleteTests.isEmpty()) {
124                     msg.append("These tests are incomplete:\n");
125                     for (String incompleteTest : incompleteTests) {
126                         msg.append(incompleteTest).append('\n');
127                     }
128                 }
129             } catch (InterruptedException e) {
130                 throw new TestSetFailedException("Timed termination was interrupted.", e);
131             } catch (ExecutionException e) {
132                 throw new TestSetFailedException(e.getLocalizedMessage(), e.getCause());
133             }
134         }
135         return msg.toString();
136     }
137 
138     private Future<ShutdownResult> scheduleShutdown() {
139         return getShutdownScheduler().schedule(createShutdownTask(), timeoutNanos, NANOSECONDS);
140     }
141 
142     private Future<ShutdownResult> scheduleForcedShutdown() {
143         return getShutdownScheduler().schedule(createForcedShutdownTask(), timeoutForcedNanos, NANOSECONDS);
144     }
145 
146     private ScheduledExecutorService getShutdownScheduler() {
147         if (shutdownScheduler == null) {
148             shutdownScheduler = Executors.newScheduledThreadPool(2, DAEMON_THREAD_FACTORY);
149         }
150         return shutdownScheduler;
151     }
152 
153     private Callable<ShutdownResult> createShutdownTask() {
154         return new Callable<ShutdownResult>() {
155             @Override
156             public ShutdownResult call() throws Exception {
157                 boolean stampedStatusWithTimeout = ParallelComputer.this.shutdownStatus.tryTimeout();
158                 return stampedStatusWithTimeout ? ParallelComputer.this.describeStopped(false) : null;
159             }
160         };
161     }
162 
163     private Callable<ShutdownResult> createForcedShutdownTask() {
164         return new Callable<ShutdownResult>() {
165             @Override
166             public ShutdownResult call() throws Exception {
167                 boolean stampedStatusWithTimeout = ParallelComputer.this.forcedShutdownStatus.tryTimeout();
168                 return stampedStatusWithTimeout ? ParallelComputer.this.describeStopped(true) : null;
169             }
170         };
171     }
172 
173     private double nanosToSeconds(long nanos) {
174         return (double) nanos / NANOS_IN_A_SECOND;
175     }
176 
177     private boolean hasTimeout() {
178         return timeoutNanos > 0;
179     }
180 
181     private boolean hasTimeoutForced() {
182         return timeoutForcedNanos > 0;
183     }
184 
185     private static long secondsToNanos(double seconds) {
186         double nanos = seconds > 0 ? seconds * NANOS_IN_A_SECOND : 0;
187         return Double.isInfinite(nanos) || nanos >= Long.MAX_VALUE ? 0 : (long) nanos;
188     }
189 
190     private static long minTimeout(long timeout1, long timeout2) {
191         if (timeout1 == 0) {
192             return timeout2;
193         } else if (timeout2 == 0) {
194             return timeout1;
195         } else {
196             return Math.min(timeout1, timeout2);
197         }
198     }
199 
200     private static void printShutdownHook(
201             Collection<String> executedTests,
202             Collection<String> incompleteTests,
203             Future<ShutdownResult> testsBeforeShutdown)
204             throws ExecutionException, InterruptedException {
205         if (testsBeforeShutdown != null) {
206             final ShutdownResult shutdownResult = testsBeforeShutdown.get();
207             if (shutdownResult != null) {
208                 for (final Description test : shutdownResult.getTriggeredTests()) {
209                     if (test != null && test.getDisplayName() != null) {
210                         executedTests.add(test.getDisplayName());
211                     }
212                 }
213 
214                 for (final Description test : shutdownResult.getIncompleteTests()) {
215                     if (test != null && test.getDisplayName() != null) {
216                         incompleteTests.add(test.getDisplayName());
217                     }
218                 }
219             }
220         }
221     }
222 }