From ac964a88dad18e6ffac3df0d6e4d4a31b7dc7560 Mon Sep 17 00:00:00 2001 From: Tibor Digana Date: Tue, 2 Sep 2014 00:44:15 +0200 Subject: [PATCH] Exceptional parallel execution on @NotThreadSafe type. --- ...fork-options-and-parallel-execution.apt.vm | 23 +- .../test/resources/junit47-parallel/pom.xml | 26 +- surefire-providers/surefire-junit47/pom.xml | 6 + .../junitcore/pc/ExecutionStatus.java | 35 +++ .../junitcore/pc/InvokerStrategy.java | 26 +- .../junitcore/pc/ParallelComputer.java | 143 ++++++---- .../junitcore/pc/ParallelComputerBuilder.java | 118 +++++--- .../surefire/junitcore/pc/Scheduler.java | 8 +- .../surefire/junitcore/pc/ShutdownStatus.java | 68 +++++ .../junitcore/pc/SingleThreadScheduler.java | 70 +++++ .../surefire/junitcore/pc/WrappedRunners.java | 7 +- .../pc/ParallelComputerBuilderTest.java | 260 +++++++++++++++++- .../pc/ParallelComputerUtilTest.java | 54 +++- 13 files changed, 739 insertions(+), 105 deletions(-) create mode 100644 surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ExecutionStatus.java create mode 100644 surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ShutdownStatus.java create mode 100644 surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/SingleThreadScheduler.java diff --git a/maven-surefire-plugin/src/site/apt/examples/fork-options-and-parallel-execution.apt.vm b/maven-surefire-plugin/src/site/apt/examples/fork-options-and-parallel-execution.apt.vm index 02b22ed936..77b4b48629 100644 --- a/maven-surefire-plugin/src/site/apt/examples/fork-options-and-parallel-execution.apt.vm +++ b/maven-surefire-plugin/src/site/apt/examples/fork-options-and-parallel-execution.apt.vm @@ -93,7 +93,7 @@ Fork Options and Parallel Test Execution The surefire is always trying to reuse threads, optimize the thread-counts, and prefers thread fairness. - + <> with the <<>> option is: the concurrency happens within the same JVM process. That is efficient in terms of memory and execution time, but you may be more vulnerable towards race @@ -103,6 +103,25 @@ Fork Options and Parallel Test Execution <<>> to a value higher than 1. The next section covers the details about this and the related <<>> property. + * Parallel Test Execution and Single Thread Execution + + As mentioned above the <<>> test execution is used with specific + thread count. Since of Surefire 2.18, you can apply the JCIP annotation + <<<@net.jcip.annotations.NotThreadSafe>>> on the Java class of JUnit test + (test class, Suite, Parameterized, etc.) in order to execute it in single + Thread instance. The Thread has name "maven-surefire-plugin@NotThreadSafe". + Just use the dependency net.jcip:jcip-annotations:1.0, or another Artifact + with Apache License com.github.stephenc.jcip:jcip-annotations:1.0-1. This + way parallel execution of tests classes annotated with <<<@NotThreadSafe>>> + are forked in single thread instance (don't mean forked JVM process). + If the Suite or Parameterized is annotated with @NotThreadSafe, the + suite classes are executed in single thread. + You can also annotate test class referenced by Suite, and the other + unannotated test classes in the Suite can be subject to run in parallel. + Note: As designed by JUnit runners, the static methods annotated with + @BeforeClass and @AfterClass are called in parent thread. Assign classes + to the @NotThreadSafe Suite to prevent from this trouble. + * Parallel Surefire Execution in Multi-Module Maven Parallel Build Maven core allows building modules of multi-module projects in parallel with @@ -113,7 +132,7 @@ Fork Options and Parallel Test Execution The parameter <<>> defines the maximum number of JVM processes that Surefire will spawn to execute the tests. It supports the - same syntax as <<<-T>>> in maven-core: if you termniate the value with a 'C', + same syntax as <<<-T>>> in maven-core: if you terminate the value with a 'C', that value will be multiplied with the number of available CPU cores in your system. For example <<>> on a Quad-Core system will result in forking up to ten concurrent JVM processes that execute tests. diff --git a/surefire-integration-tests/src/test/resources/junit47-parallel/pom.xml b/surefire-integration-tests/src/test/resources/junit47-parallel/pom.xml index b4baa02363..4f697de69d 100644 --- a/surefire-integration-tests/src/test/resources/junit47-parallel/pom.xml +++ b/surefire-integration-tests/src/test/resources/junit47-parallel/pom.xml @@ -1,6 +1,31 @@ + + 4.0.0 + + org.apache.maven.surefire + it-parent + 1.0 + ../pom.xml + org.apache.maven.plugins.surefire junit47-parallel 1.0-SNAPSHOT @@ -32,7 +57,6 @@ maven-surefire-plugin - ${surefire.version} diff --git a/surefire-providers/surefire-junit47/pom.xml b/surefire-providers/surefire-junit47/pom.xml index 6b5a9321eb..6dcbad7ab7 100644 --- a/surefire-providers/surefire-junit47/pom.xml +++ b/surefire-providers/surefire-junit47/pom.xml @@ -32,6 +32,11 @@ SureFire JUnitCore Runner + + com.github.stephenc.jcip + jcip-annotations + 1.0-1 + junit junit @@ -74,6 +79,7 @@ ${java.home}/bin/java true + -server -Xmx128m -XX:+UseParallelGC -XX:+UseNUMA -XX:MaxGCPauseMillis=50 diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ExecutionStatus.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ExecutionStatus.java new file mode 100644 index 0000000000..fbc05f0626 --- /dev/null +++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ExecutionStatus.java @@ -0,0 +1,35 @@ +package org.apache.maven.surefire.junitcore.pc; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Status of {@link ParallelComputer ParallelComputer runtime}.

+ * Used together with shutdown hook. + * + * @author Tibor Digana (tibor17) + * @see ParallelComputer + * @since 2.18 + */ +enum ExecutionStatus +{ + STARTED, + FINISHED, + TIMEOUT +} diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/InvokerStrategy.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/InvokerStrategy.java index 29a6624368..4dd7f10a37 100644 --- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/InvokerStrategy.java +++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/InvokerStrategy.java @@ -19,6 +19,8 @@ * under the License. */ +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -33,12 +35,23 @@ final class InvokerStrategy { private final AtomicBoolean canSchedule = new AtomicBoolean( true ); + private final Queue activeThreads = new ConcurrentLinkedQueue(); + @Override public void schedule( Runnable task ) { if ( canSchedule() ) { - task.run(); + final Thread currentThread = Thread.currentThread(); + try + { + activeThreads.add( currentThread ); + task.run(); + } + finally + { + activeThreads.remove( currentThread ); + } } } @@ -48,6 +61,17 @@ protected boolean stop() return canSchedule.getAndSet( false ); } + @Override + protected boolean stopNow() + { + final boolean stopped = stop(); + for ( Thread activeThread; ( activeThread = activeThreads.poll() ) != null; ) + { + activeThread.interrupt(); + } + return stopped; + } + @Override public boolean hasSharedThreadPool() { diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ParallelComputer.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ParallelComputer.java index 79ca87aaab..bf28c70e51 100644 --- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ParallelComputer.java +++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ParallelComputer.java @@ -24,14 +24,14 @@ import org.junit.runner.Description; import java.util.Collection; -import java.util.Collections; import java.util.TreeSet; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; -import static java.util.concurrent.TimeUnit.*; +import static java.util.concurrent.TimeUnit.NANOSECONDS; /** * ParallelComputer extends JUnit {@link Computer} and has a shutdown functionality. @@ -43,17 +43,17 @@ public abstract class ParallelComputer extends Computer { + private final ShutdownStatus shutdownStatus = new ShutdownStatus(); + + private final ShutdownStatus forcedShutdownStatus = new ShutdownStatus(); + private final long timeoutNanos; private final long timeoutForcedNanos; private ScheduledExecutorService shutdownScheduler; - private Future> testsBeforeShutdown; - - private Future> testsBeforeForcedShutdown; - - public ParallelComputer( double timeoutInSeconds, double timeoutForcedInSeconds ) + public ParallelComputer( double timeoutInSeconds, double timeoutForcedInSeconds ) { this.timeoutNanos = secondsToNanos( timeoutInSeconds ); this.timeoutForcedNanos = secondsToNanos( timeoutForcedInSeconds ); @@ -81,71 +81,96 @@ else if ( timeout2 == 0 ) } } - private static Collection printShutdownHook( Future> future ) - throws TestSetFailedException + private static void printShutdownHook( Collection executedTests, + Future> testsBeforeShutdown ) + throws ExecutionException, InterruptedException { - if ( !future.isCancelled() && future.isDone() ) + if ( testsBeforeShutdown != null ) { - try + for ( final Description executedTest : testsBeforeShutdown.get() ) { - TreeSet executedTests = new TreeSet(); - for ( Description executedTest : future.get() ) + if ( executedTest != null && executedTest.getDisplayName() != null ) { - if ( executedTest != null && executedTest.getDisplayName() != null ) - { - executedTests.add( executedTest.getDisplayName() ); - } + executedTests.add( executedTest.getDisplayName() ); } - return executedTests; - } - catch ( Exception e ) - { - throw new TestSetFailedException( e ); } } - return Collections.emptySet(); } public abstract Collection shutdown( boolean shutdownNow ); protected final void beforeRunQuietly() { - testsBeforeShutdown = timeoutNanos > 0 ? scheduleShutdown() : null; - testsBeforeForcedShutdown = timeoutForcedNanos > 0 ? scheduleForcedShutdown() : null; + shutdownStatus.setDescriptionsBeforeShutdown( hasTimeout() ? scheduleShutdown() : null ); + forcedShutdownStatus.setDescriptionsBeforeShutdown( hasTimeoutForced() ? scheduleForcedShutdown() : null ); } - protected final void afterRunQuietly() + protected final boolean afterRunQuietly() { + shutdownStatus.tryFinish(); + forcedShutdownStatus.tryFinish(); if ( shutdownScheduler != null ) { shutdownScheduler.shutdownNow(); + /** + * Clear interrupted status of the (main) Thread. + * Could be previously interrupted by {@link InvokerStrategy} after triggering immediate shutdown. + */ + Thread.interrupted(); + try + { + shutdownScheduler.awaitTermination( Long.MAX_VALUE, NANOSECONDS ); + } + catch ( InterruptedException e ) + { + return false; + } } + return true; } public String describeElapsedTimeout() throws TestSetFailedException { - TreeSet executedTests = new TreeSet(); - if ( testsBeforeShutdown != null ) - { - executedTests.addAll( printShutdownHook( testsBeforeShutdown ) ); - } - - if ( testsBeforeForcedShutdown != null ) - { - executedTests.addAll( printShutdownHook( testsBeforeForcedShutdown ) ); - } - - StringBuilder msg = new StringBuilder(); - if ( !executedTests.isEmpty() ) + final StringBuilder msg = new StringBuilder(); + final boolean isShutdownTimeout = shutdownStatus.isTimeoutElapsed(); + final boolean isForcedShutdownTimeout = forcedShutdownStatus.isTimeoutElapsed(); + if ( isShutdownTimeout || isForcedShutdownTimeout ) { msg.append( "The test run has finished abruptly after timeout of " ); msg.append( nanosToSeconds( minTimeout( timeoutNanos, timeoutForcedNanos ) ) ); msg.append( " seconds.\n" ); - msg.append( "These tests were executed in prior of the shutdown operation:\n" ); - for ( String executedTest : executedTests ) + + try + { + final TreeSet executedTests = new TreeSet(); + + if ( isShutdownTimeout ) + { + printShutdownHook( executedTests, shutdownStatus.getDescriptionsBeforeShutdown() ); + } + + if ( isForcedShutdownTimeout ) + { + printShutdownHook( executedTests, forcedShutdownStatus.getDescriptionsBeforeShutdown() ); + } + + if ( !executedTests.isEmpty() ) + { + msg.append( "These tests were executed in prior to the shutdown operation:\n" ); + for ( String executedTest : executedTests ) + { + msg.append( executedTest ).append( '\n' ); + } + } + } + catch ( InterruptedException e ) { - msg.append( executedTest ).append( '\n' ); + throw new TestSetFailedException( "Timed termination was interrupted.", e ); + } + catch ( ExecutionException e ) + { + throw new TestSetFailedException( e.getLocalizedMessage(), e.getCause() ); } } return msg.toString(); @@ -153,12 +178,12 @@ public String describeElapsedTimeout() private Future> scheduleShutdown() { - return getShutdownScheduler().schedule( createShutdownTask( false ), timeoutNanos, NANOSECONDS ); + return getShutdownScheduler().schedule( createShutdownTask(), timeoutNanos, NANOSECONDS ); } private Future> scheduleForcedShutdown() { - return getShutdownScheduler().schedule( createShutdownTask( true ), timeoutForcedNanos, NANOSECONDS ); + return getShutdownScheduler().schedule( createForcedShutdownTask(), timeoutForcedNanos, NANOSECONDS ); } private ScheduledExecutorService getShutdownScheduler() @@ -170,14 +195,28 @@ private ScheduledExecutorService getShutdownScheduler() return shutdownScheduler; } - private Callable> createShutdownTask( final boolean isForced ) + private Callable> createShutdownTask() + { + return new Callable>() + { + public Collection call() + throws Exception + { + boolean stampedStatusWithTimeout = ParallelComputer.this.shutdownStatus.tryTimeout(); + return stampedStatusWithTimeout ? ParallelComputer.this.shutdown( false ) : null; + } + }; + } + + private Callable> createForcedShutdownTask() { return new Callable>() { public Collection call() throws Exception { - return ParallelComputer.this.shutdown( isForced ); + boolean stampedStatusWithTimeout = ParallelComputer.this.forcedShutdownStatus.tryTimeout(); + return stampedStatusWithTimeout ? ParallelComputer.this.shutdown( true ) : null; } }; } @@ -186,4 +225,14 @@ private double nanosToSeconds( long nanos ) { return (double) nanos / 1E9; } -} \ No newline at end of file + + private boolean hasTimeout() + { + return timeoutNanos > 0; + } + + private boolean hasTimeoutForced() + { + return timeoutForcedNanos > 0; + } +} diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerBuilder.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerBuilder.java index 04813ad1f4..ca7cc60f54 100644 --- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerBuilder.java +++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerBuilder.java @@ -19,6 +19,7 @@ * under the License. */ +import net.jcip.annotations.NotThreadSafe; import org.apache.maven.surefire.junitcore.JUnitCoreParameters; import org.apache.maven.surefire.testset.TestSetFailedException; import org.junit.internal.runners.ErrorReportingRunner; @@ -33,12 +34,15 @@ import org.junit.runners.model.RunnerBuilder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -71,6 +75,8 @@ */ public final class ParallelComputerBuilder { + private static final Set NULL_SINGLETON = Collections.singleton( null ); + static final int TOTAL_POOL_SIZE_UNDEFINED = 0; private final Map parallelGroups = new EnumMap( Type.class ); @@ -217,6 +223,8 @@ private double parallelTestsTimeoutForcedInSeconds() final class PC extends ParallelComputer { + private final SingleThreadScheduler notThreadSafeTests = new SingleThreadScheduler(); + final Collection suites = new LinkedHashSet(); final Collection nestedSuites = new LinkedHashSet(); @@ -225,7 +233,7 @@ final class PC final Collection nestedClasses = new LinkedHashSet(); - final Collection unscheduledRunners = new LinkedHashSet(); + final Collection notParallelRunners = new LinkedHashSet(); int poolCapacity; @@ -248,8 +256,13 @@ private PC() @Override public Collection shutdown( boolean shutdownNow ) { - final Scheduler master = this.master; - return master == null ? Collections.emptyList() : master.shutdown( shutdownNow ); + Collection startedTests = notThreadSafeTests.shutdown( shutdownNow ); + final Scheduler m = this.master; + if ( m != null ) + { + startedTests.addAll( m.shutdown( shutdownNow ) ); + } + return startedTests; } @Override @@ -287,7 +300,12 @@ protected Runner getRunner( RunnerBuilder builder, Class testClass ) Runner runner = super.getRunner( builder, testClass ); if ( canSchedule( runner ) ) { - if ( runner instanceof Suite ) + if ( !isThreadSafe( runner ) ) + { + ( ( ParentRunner ) runner ).setScheduler( notThreadSafeTests.newRunnerScheduler() ); + notParallelRunners.add( runner ); + } + else if ( runner instanceof Suite ) { suites.add( (Suite) runner ); } @@ -298,7 +316,7 @@ protected Runner getRunner( RunnerBuilder builder, Class testClass ) } else { - unscheduledRunners.add( runner ); + notParallelRunners.add( runner ); } return runner; } @@ -335,11 +353,7 @@ private WrappedRunners wrapRunners( Collection runners ) } } } - - Suite wrapper = runs.isEmpty() ? null : new Suite( null, runs ) - { - }; - return new WrappedRunners( wrapper, childrenCounter ); + return runs.isEmpty() ? new WrappedRunners() : new WrappedRunners( createSuite( runs ), childrenCounter ); } private int countChildren( Runner runner ) @@ -358,7 +372,8 @@ private ExecutorService createPool( int poolSize ) private Scheduler createMaster( ExecutorService pool, int poolSize ) { - if ( !areSuitesAndClassesParallel() || poolSize <= 1 ) + final int finalRunnersCounter = countFinalRunners(); // can be 0, 1, 2 or 3 + if ( finalRunnersCounter <= 1 || poolSize <= 1 ) { return new Scheduler( null, new InvokerStrategy() ); } @@ -368,14 +383,25 @@ else if ( pool != null && poolSize == Integer.MAX_VALUE ) } else { - return new Scheduler( null, SchedulingStrategies.createParallelStrategy( 2 ) ); + return new Scheduler( null, SchedulingStrategies.createParallelStrategy( finalRunnersCounter ) ); } } - private boolean areSuitesAndClassesParallel() + private int countFinalRunners() { - return !suites.isEmpty() && allGroups.get( SUITES ) > 0 && !classes.isEmpty() - && allGroups.get( CLASSES ) > 0; + int counter = notParallelRunners.isEmpty() ? 0 : 1; + + if ( !suites.isEmpty() && allGroups.get( SUITES ) > 0 ) + { + ++counter; + } + + if ( !classes.isEmpty() && allGroups.get( CLASSES ) > 0 ) + { + ++counter; + } + + return counter; } private void populateChildrenFromSuites() @@ -463,24 +489,17 @@ private Runner setSchedulers( ParentRunner suiteSuites, ParentRunner suiteClasse } // resulting runner for Computer#getSuite() scheduled by master scheduler - ParentRunner all = createFinalRunner( suiteSuites, suiteClasses ); + ParentRunner all = createFinalRunner( removeNullRunners( + Arrays.asList( suiteSuites, suiteClasses, createSuite( notParallelRunners ) ) + ) ); all.setScheduler( master ); return all; } - private ParentRunner createFinalRunner( Runner... runners ) + private ParentRunner createFinalRunner( List runners ) throws InitializationError { - ArrayList all = new ArrayList( unscheduledRunners ); - for ( Runner runner : runners ) - { - if ( runner != null ) - { - all.add( runner ); - } - } - - return new Suite( null, all ) + return new Suite( null, runners ) { @Override public void run( RunNotifier notifier ) @@ -559,6 +578,11 @@ private boolean canSchedule( Runner runner ) return !( runner instanceof ErrorReportingRunner ) && runner instanceof ParentRunner; } + private boolean isThreadSafe( Runner runner ) + { + return runner.getDescription().getAnnotation( NotThreadSafe.class ) == null; + } + private class SuiteFilter extends Filter { @@ -575,15 +599,23 @@ public void apply( Object child ) throws NoTestsRemainException { super.apply( child ); - if ( child instanceof Suite ) + if ( child instanceof ParentRunner ) { - nestedSuites.add( (Suite) child ); - } - else if ( child instanceof ParentRunner ) - { - ParentRunner parentRunner = (ParentRunner) child; - nestedClasses.add( parentRunner ); - nestedClassesChildren += parentRunner.getDescription().getChildren().size(); + ParentRunner runner = ( ParentRunner ) child; + if ( !isThreadSafe( runner ) ) + { + runner.setScheduler( notThreadSafeTests.newRunnerScheduler() ); + } + else if ( child instanceof Suite ) + { + nestedSuites.add( (Suite) child ); + } + else + { + ParentRunner parentRunner = (ParentRunner) child; + nestedClasses.add( parentRunner ); + nestedClassesChildren += parentRunner.getDescription().getChildren().size(); + } } } @@ -594,4 +626,20 @@ public String describe() } } } + + private static Suite createSuite( Collection runners ) + throws InitializationError + { + final List onlyRunners = removeNullRunners( runners ); + return onlyRunners.isEmpty() ? null : new Suite( null, onlyRunners ) + { + }; + } + + private static List removeNullRunners( Collection runners ) + { + final List onlyRunners = new ArrayList( runners ); + onlyRunners.removeAll( NULL_SINGLETON ); + return onlyRunners; + } } diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/Scheduler.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/Scheduler.java index d3c41337b9..a16f1d23ec 100644 --- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/Scheduler.java +++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/Scheduler.java @@ -22,9 +22,9 @@ import org.junit.runner.Description; import org.junit.runners.model.RunnerScheduler; -import java.util.ArrayList; import java.util.Collection; import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; @@ -190,12 +190,12 @@ private boolean canSchedule() protected void logQuietly( Throwable t ) { - t.printStackTrace( System.err ); + t.printStackTrace( System.out ); } protected void logQuietly( String msg ) { - System.err.println( msg ); + System.out.println( msg ); } /** @@ -211,7 +211,7 @@ protected void logQuietly( String msg ) public Collection shutdown( boolean shutdownNow ) { shutdown = true; - ArrayList activeChildren = new ArrayList(); + Collection activeChildren = new ConcurrentLinkedQueue(); if ( started && description != null ) { diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ShutdownStatus.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ShutdownStatus.java new file mode 100644 index 0000000000..1bed62dda4 --- /dev/null +++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/ShutdownStatus.java @@ -0,0 +1,68 @@ +package org.apache.maven.surefire.junitcore.pc; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.junit.runner.Description; + +import java.util.Collection; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +import static org.apache.maven.surefire.junitcore.pc.ExecutionStatus.*; + +/** + * Wrapper of {@link ParallelComputer ParallelComputer status information} and tests been populated before + * a shutdown hook has been triggered. + * + * @author Tibor Digana (tibor17) + * @see ParallelComputer + * @since 2.18 + */ +final class ShutdownStatus +{ + private final AtomicReference status = new AtomicReference( STARTED ); + + private Future> descriptionsBeforeShutdown; + + boolean tryFinish() + { + return status.compareAndSet( STARTED, FINISHED ); + } + + boolean tryTimeout() + { + return status.compareAndSet( STARTED, TIMEOUT ); + } + + boolean isTimeoutElapsed() + { + return status.get() == TIMEOUT; + } + + Future> getDescriptionsBeforeShutdown() + { + return descriptionsBeforeShutdown; + } + + void setDescriptionsBeforeShutdown( Future> descriptionsBeforeShutdown ) + { + this.descriptionsBeforeShutdown = descriptionsBeforeShutdown; + } +} diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/SingleThreadScheduler.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/SingleThreadScheduler.java new file mode 100644 index 0000000000..79b3197060 --- /dev/null +++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/SingleThreadScheduler.java @@ -0,0 +1,70 @@ +package org.apache.maven.surefire.junitcore.pc; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.junit.runner.Description; +import org.junit.runners.model.RunnerScheduler; + +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Used to execute tests annotated with {@link net.jcip.annotations.NotThreadSafe}. + *

+ * + * @author Tibor Digana (tibor17) + * @see ParallelComputerBuilder + * @since 2.18 + */ +final class SingleThreadScheduler +{ + private final ExecutorService pool = newPool(); + + private final Scheduler master = new Scheduler( null, SchedulingStrategies.createParallelSharedStrategy( pool ) ); + + RunnerScheduler newRunnerScheduler() + { + return new Scheduler( null, master, SchedulingStrategies.createParallelSharedStrategy( pool ) ); + } + + /** + * @see Scheduler#shutdown(boolean) + */ + Collection shutdown( boolean shutdownNow ) + { + return master.shutdown( shutdownNow ); + } + + private static ExecutorService newPool() + { + final ThreadFactory factory = new ThreadFactory() + { + public Thread newThread( Runnable r ) + { + return new Thread( r, "maven-surefire-plugin@NotThreadSafe" ); + } + }; + return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), factory); + } +} \ No newline at end of file diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/WrappedRunners.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/WrappedRunners.java index 4550b5e91c..ba45e747dd 100644 --- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/WrappedRunners.java +++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/pc/WrappedRunners.java @@ -43,4 +43,9 @@ final class WrappedRunners this.wrappingSuite = wrappingSuite; this.embeddedChildrenCount = embeddedChildrenCount; } -} \ No newline at end of file + + WrappedRunners() + { + this( null, 0 ); + } +} diff --git a/surefire-providers/surefire-junit47/src/test/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerBuilderTest.java b/surefire-providers/surefire-junit47/src/test/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerBuilderTest.java index e3fbf3a410..cad3062cdc 100644 --- a/surefire-providers/surefire-junit47/src/test/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerBuilderTest.java +++ b/surefire-providers/surefire-junit47/src/test/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerBuilderTest.java @@ -19,6 +19,7 @@ * under the License. */ +import net.jcip.annotations.NotThreadSafe; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -35,14 +36,11 @@ import java.util.Iterator; import java.util.concurrent.ConcurrentLinkedQueue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.hamcrest.core.AnyOf.anyOf; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; import static org.apache.maven.surefire.junitcore.pc.RangeMatcher.between; +import static org.junit.Assert.*; /** * @author Tibor Digana (tibor17) @@ -84,6 +82,11 @@ public void beforeTest() Class1.maxConcurrentMethods = 0; Class1.concurrentMethods = 0; shutdownTask = null; + NotThreadSafeTest1.t = null; + NotThreadSafeTest2.t = null; + NotThreadSafeTest3.t = null; + NormalTest1.t = null; + NormalTest2.t = null; } @Test @@ -394,6 +397,115 @@ public void keepBeforeAfterTwoParallelClasses() assertThat( methods.subList( 9, 12 ), is( not( Arrays.asList( "deinit", "deinit", "deinit" ) ) ) ); } + @Test + public void notThreadSafeTest() + { + ParallelComputerBuilder builder = new ParallelComputerBuilder() + .useOnePool( 6 ).optimize( true ).parallelClasses( 3 ).parallelMethods( 3 ); + ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer(); + Result result = new JUnitCore().run( computer, NotThreadSafeTest1.class, NotThreadSafeTest2.class ); + assertTrue( result.wasSuccessful() ); + assertThat( result.getRunCount(), is( 2 ) ); + assertNotNull( NotThreadSafeTest1.t ); + assertNotNull( NotThreadSafeTest2.t ); + assertSame( NotThreadSafeTest1.t, NotThreadSafeTest2.t ); + assertThat( computer.notParallelRunners.size(), is( 2 ) ); + assertTrue( computer.suites.isEmpty() ); + assertTrue( computer.classes.isEmpty() ); + assertTrue( computer.nestedClasses.isEmpty() ); + assertTrue( computer.nestedSuites.isEmpty() ); + assertFalse( computer.splitPool ); + assertThat( computer.poolCapacity, is( 6 ) ); + } + + @Test + public void mixedThreadSafety() + { + ParallelComputerBuilder builder = new ParallelComputerBuilder() + .useOnePool( 6 ).optimize( true ).parallelClasses( 3 ).parallelMethods( 3 ); + ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer(); + Result result = new JUnitCore().run( computer, NotThreadSafeTest1.class, NormalTest1.class ); + assertTrue( result.wasSuccessful() ); + assertThat( result.getRunCount(), is( 2 ) ); + assertNotNull( NotThreadSafeTest1.t ); + assertNotNull( NormalTest1.t ); + assertThat( NormalTest1.t.getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + assertNotSame( NotThreadSafeTest1.t, NormalTest1.t ); + assertThat( computer.notParallelRunners.size(), is( 1 ) ); + assertTrue( computer.suites.isEmpty() ); + assertThat( computer.classes.size(), is( 1 ) ); + assertTrue( computer.nestedClasses.isEmpty() ); + assertTrue( computer.nestedSuites.isEmpty() ); + assertFalse( computer.splitPool ); + assertThat( computer.poolCapacity, is( 6 ) ); + } + + @Test + public void notThreadSafeTestsInSuite() + { + ParallelComputerBuilder builder = new ParallelComputerBuilder().useOnePool( 5 ).parallelMethods( 3 ); + assertFalse( builder.isOptimized() ); + ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer(); + Result result = new JUnitCore().run( computer, NotThreadSafeTestSuite.class ); + assertTrue( result.wasSuccessful() ); + assertNotNull( NormalTest1.t ); + assertNotNull( NormalTest2.t ); + assertSame( NormalTest1.t, NormalTest2.t ); + assertThat( NormalTest1.t.getName(), is( "maven-surefire-plugin@NotThreadSafe" ) ); + assertThat( NormalTest2.t.getName(), is( "maven-surefire-plugin@NotThreadSafe" ) ); + assertThat( computer.notParallelRunners.size(), is( 1 ) ); + assertTrue( computer.suites.isEmpty() ); + assertTrue( computer.classes.isEmpty() ); + assertTrue( computer.nestedClasses.isEmpty() ); + assertTrue( computer.nestedSuites.isEmpty() ); + assertFalse( computer.splitPool ); + assertThat( computer.poolCapacity, is( 5 ) ); + } + + @Test + public void mixedThreadSafetyInSuite() + { + ParallelComputerBuilder builder = new ParallelComputerBuilder() + .useOnePool( 10 ).optimize( true ).parallelSuites( 2 ).parallelClasses( 3 ).parallelMethods( 3 ); + ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer(); + Result result = new JUnitCore().run( computer, MixedSuite.class ); + assertTrue( result.wasSuccessful() ); + assertThat( result.getRunCount(), is( 2 ) ); + assertNotNull( NotThreadSafeTest1.t ); + assertNotNull( NormalTest1.t ); + assertThat( NormalTest1.t.getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + assertNotSame( NotThreadSafeTest1.t, NormalTest1.t ); + assertTrue( computer.notParallelRunners.isEmpty() ); + assertThat( computer.suites.size(), is( 1 ) ); + assertTrue( computer.classes.isEmpty() ); + assertThat( computer.nestedClasses.size(), is( 1 ) ); + assertTrue( computer.nestedSuites.isEmpty() ); + assertFalse( computer.splitPool ); + assertThat( computer.poolCapacity, is( 10 ) ); + } + + @Test + public void inheritanceWithNotThreadSafe() + { + ParallelComputerBuilder builder = new ParallelComputerBuilder() + .useOnePool( 10 ).optimize( true ).parallelSuites( 2 ).parallelClasses( 3 ).parallelMethods( 3 ); + ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer(); + Result result = new JUnitCore().run( computer, OverMixedSuite.class ); + assertTrue( result.wasSuccessful() ); + assertThat( result.getRunCount(), is( 2 ) ); + assertNotNull( NotThreadSafeTest3.t ); + assertNotNull( NormalTest1.t ); + assertThat( NormalTest1.t.getName(), is( "maven-surefire-plugin@NotThreadSafe" ) ); + assertSame( NotThreadSafeTest3.t, NormalTest1.t ); + assertThat( computer.notParallelRunners.size(), is( 1 ) ); + assertTrue( computer.suites.isEmpty() ); + assertTrue( computer.classes.isEmpty() ); + assertTrue( computer.nestedClasses.isEmpty() ); + assertTrue( computer.nestedSuites.isEmpty() ); + assertFalse( computer.splitPool ); + assertThat( computer.poolCapacity, is( 10 ) ); + } + private static class ShutdownTest { Result run( final boolean useInterrupt ) @@ -511,7 +623,145 @@ public static class NothingDoingSuite @RunWith( Suite.class ) @Suite.SuiteClasses( { Class2.class, Class1.class } ) - public class TestSuite + public static class TestSuite + { + } + + @NotThreadSafe + public static class NotThreadSafeTest1 + { + static volatile Thread t; + + @BeforeClass + public static void beforeSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } + + @AfterClass + public static void afterSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } + + @Test + public void test() + { + t = Thread.currentThread(); + assertThat( t.getName(), is( "maven-surefire-plugin@NotThreadSafe" ) ); + } + } + + @NotThreadSafe + public static class NotThreadSafeTest2 + { + static volatile Thread t; + + @BeforeClass + public static void beforeSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } + + @AfterClass + public static void afterSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } + + @Test + public void test() + { + t = Thread.currentThread(); + assertThat( t.getName(), is( "maven-surefire-plugin@NotThreadSafe" ) ); + } + } + + @NotThreadSafe + public static class NotThreadSafeTest3 + { + static volatile Thread t; + + @Test + public void test() + { + t = Thread.currentThread(); + assertThat( t.getName(), is( "maven-surefire-plugin@NotThreadSafe" ) ); + } + } + + @RunWith( Suite.class ) + @Suite.SuiteClasses( { NormalTest1.class, NormalTest2.class } ) + @NotThreadSafe + public static class NotThreadSafeTestSuite { + @BeforeClass + public static void beforeSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } + + @AfterClass + public static void afterSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } + } + + public static class NormalTest1 + { + static volatile Thread t; + + @Test + public void test() + { + t = Thread.currentThread(); + } + } + + public static class NormalTest2 + { + static volatile Thread t; + + @Test + public void test() + { + t = Thread.currentThread(); + } + } + + @RunWith( Suite.class ) + @Suite.SuiteClasses( { NotThreadSafeTest1.class, NormalTest1.class } ) + public static class MixedSuite + { + @BeforeClass + public static void beforeSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } + + @AfterClass + public static void afterSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } + } + + @RunWith( Suite.class ) + @Suite.SuiteClasses( { NotThreadSafeTest3.class, NormalTest1.class } ) + @NotThreadSafe + public static class OverMixedSuite + { + @BeforeClass + public static void beforeSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } + + @AfterClass + public static void afterSuite() + { + assertThat( Thread.currentThread().getName(), is( not( "maven-surefire-plugin@NotThreadSafe" ) ) ); + } } } diff --git a/surefire-providers/surefire-junit47/src/test/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerUtilTest.java b/surefire-providers/surefire-junit47/src/test/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerUtilTest.java index 68e16e1b90..e094e116e8 100644 --- a/surefire-providers/surefire-junit47/src/test/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerUtilTest.java +++ b/surefire-providers/surefire-junit47/src/test/java/org/apache/maven/surefire/junitcore/pc/ParallelComputerUtilTest.java @@ -23,6 +23,7 @@ import org.apache.maven.surefire.testset.TestSetFailedException; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.theories.DataPoint; @@ -76,6 +77,12 @@ public static void afterClass() ParallelComputerUtil.setDefaultAvailableProcessors(); } + @Before + public void beforeTest() + { + assertFalse( Thread.currentThread().isInterrupted() ); + } + private static Properties parallel( String parallel ) { Properties properties = new Properties(); @@ -989,9 +996,10 @@ public void shutdown() long deltaTime = 500L; assertEquals( 2500L, timeSpent, deltaTime ); - assertTrue( pc.describeElapsedTimeout().contains( - "The test run has finished abruptly after timeout of 2.5 seconds." ) ); - assertTrue( pc.describeElapsedTimeout().contains( TestClass.class.getName() ) ); + String description = pc.describeElapsedTimeout(); + assertTrue( description.contains( "The test run has finished abruptly after timeout of 2.5 seconds.") ); + assertTrue( description.contains( "These tests were executed in prior to the shutdown operation:\n" + + TestClass.class.getName() ) ); } @Test @@ -1011,9 +1019,34 @@ public void forcedShutdown() long deltaTime = 500L; assertEquals( 2500L, timeSpent, deltaTime ); - assertTrue( pc.describeElapsedTimeout().contains( - "The test run has finished abruptly after timeout of 2.5 seconds." ) ); - assertTrue( pc.describeElapsedTimeout().contains( TestClass.class.getName() ) ); + String description = pc.describeElapsedTimeout(); + assertTrue( description.contains( "The test run has finished abruptly after timeout of 2.5 seconds.") ); + assertTrue( description.contains( "These tests were executed in prior to the shutdown operation:\n" + + TestClass.class.getName() ) ); + } + + @Test + public void timeoutAndForcedShutdown() + throws TestSetFailedException, ExecutionException, InterruptedException + { + // The JUnitCore returns after 2.5s and the test-methods in TestClass are interrupted after 3.5s. + Properties properties = new Properties(); + properties.setProperty( PARALLEL_KEY, "methods" ); + properties.setProperty( THREADCOUNTMETHODS_KEY, "2" ); + properties.setProperty( PARALLEL_TIMEOUT_KEY, Double.toString( 2.5d ) ); + properties.setProperty( PARALLEL_TIMEOUTFORCED_KEY, Double.toString( 3.5d ) ); + JUnitCoreParameters params = new JUnitCoreParameters( properties ); + ParallelComputerBuilder pcBuilder = new ParallelComputerBuilder( params ); + ParallelComputer pc = pcBuilder.buildComputer(); + new JUnitCore().run( pc, TestClass.class ); + long timeSpent = runtime.stop(); + long deltaTime = 500L; + + assertEquals( 2500L, timeSpent, deltaTime ); + String description = pc.describeElapsedTimeout(); + assertTrue( description.contains( "The test run has finished abruptly after timeout of 2.5 seconds.") ); + assertTrue( description.contains( "These tests were executed in prior to the shutdown operation:\n" + + TestClass.class.getName() ) ); } public static class TestClass @@ -1023,7 +1056,8 @@ public void a() throws InterruptedException { long t1 = System.currentTimeMillis(); - try{ + try + { Thread.sleep( 5000L ); } finally @@ -1037,7 +1071,8 @@ public void b() throws InterruptedException { long t1 = System.currentTimeMillis(); - try{ + try + { Thread.sleep( 5000L ); } finally @@ -1051,7 +1086,8 @@ public void c() throws InterruptedException { long t1 = System.currentTimeMillis(); - try{ + try + { Thread.sleep( 5000L ); } finally