[SUREFIRE-580] [SUREFIRE-524] new mechanism to dispatch commands to forked jvm
Project: http://git-wip-us.apache.org/repos/asf/maven-surefire/repo Commit: http://git-wip-us.apache.org/repos/asf/maven-surefire/commit/78dca27e Tree: http://git-wip-us.apache.org/repos/asf/maven-surefire/tree/78dca27e Diff: http://git-wip-us.apache.org/repos/asf/maven-surefire/diff/78dca27e Branch: refs/heads/master Commit: 78dca27ef03bd939e700d7b00babaac44e7d1707 Parents: 7907ade Author: Tibor17 <tibo...@lycos.com> Authored: Thu Jul 23 23:21:14 2015 +0200 Committer: Tibor17 <tibo...@lycos.com> Committed: Thu Jul 23 23:28:19 2015 +0200 ---------------------------------------------------------------------- maven-surefire-common/pom.xml | 3 + .../surefire/booterclient/ForkStarter.java | 22 +- .../TestProvidingInputStream.java | 105 ++++---- .../TestProvidingInputStreamTest.java | 141 ++++++++++ .../apache/maven/surefire/JUnit4SuiteTest.java | 76 ++++++ pom.xml | 11 + .../apache/maven/surefire/booter/Command.java | 50 ++++ .../surefire/booter/MasterProcessCommand.java | 199 +++++++++++++++ .../surefire/testset/TestListResolver.java | 15 +- .../surefire/util/internal/StringUtils.java | 20 ++ .../booter/MasterProcessCommandTest.java | 155 +++++++++++ surefire-booter/pom.xml | 1 + .../maven/surefire/booter/ForkedBooter.java | 2 +- .../maven/surefire/booter/LazyTestsToRun.java | 113 +------- .../surefire/booter/MasterProcessListener.java | 28 ++ .../surefire/booter/MasterProcessReader.java | 255 +++++++++++++++++++ .../maven/surefire/booter/JUnit4SuiteTest.java | 1 + .../booter/MasterProcessReaderTest.java | 193 ++++++++++++++ .../surefire/booter/NewClassLoaderRunner.java | 191 ++++++++++++++ .../maven/surefire/junit/JUnit3Provider.java | 1 - .../maven/surefire/junit4/JUnit4Provider.java | 8 +- 21 files changed, 1413 insertions(+), 177 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/maven-surefire-common/pom.xml ---------------------------------------------------------------------- diff --git a/maven-surefire-common/pom.xml b/maven-surefire-common/pom.xml index b49798a..c6303e0 100644 --- a/maven-surefire-common/pom.xml +++ b/maven-surefire-common/pom.xml @@ -119,6 +119,9 @@ <artifactId>maven-surefire-plugin</artifactId> <configuration> <redirectTestOutputToFile>true</redirectTestOutputToFile> + <includes> + <include>**/JUnit4SuiteTest.java</include> + </includes> </configuration> <dependencies> <dependency> http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java ---------------------------------------------------------------------- diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java index 804a08e..4b2e184 100644 --- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java +++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java @@ -35,6 +35,7 @@ import org.apache.maven.shared.utils.cli.CommandLineUtils; import org.apache.maven.shared.utils.cli.ShutdownHookUtils; import org.apache.maven.surefire.booter.Classpath; import org.apache.maven.surefire.booter.ClasspathConfiguration; +import org.apache.maven.surefire.booter.Command; import org.apache.maven.surefire.booter.KeyValueSource; import org.apache.maven.surefire.booter.PropertiesWrapper; import org.apache.maven.surefire.booter.ProviderConfiguration; @@ -72,6 +73,8 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicReference; import static org.apache.maven.surefire.booter.Classpath.join; +import static org.apache.maven.surefire.booter.MasterProcessCommand.RUN_CLASS; +import static java.lang.StrictMath.min; /** * Starts the fork or runs in-process. @@ -210,18 +213,17 @@ public class ForkStarter ThreadPoolExecutor executorService = new ThreadPoolExecutor( forkCount, forkCount, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>( forkCount ) ); executorService.setThreadFactory( threadFactory ); - Collection<TestProvidingInputStream> testStreams = new ArrayList<TestProvidingInputStream>(); try { - final Queue<String> messageQueue = new ConcurrentLinkedQueue<String>(); + final Queue<Command> commands = new ConcurrentLinkedQueue<Command>(); for ( Class<?> clazz : getSuitesIterator() ) { - messageQueue.add( clazz.getName() ); + commands.add( new Command( RUN_CLASS, clazz.getName() ) ); } - - for ( int forkNum = 0, total = messageQueue.size(); forkNum < forkCount && forkNum < total; forkNum++ ) + Collection<TestProvidingInputStream> testStreams = new ArrayList<TestProvidingInputStream>(); + for ( int forkNum = 0, total = min( forkCount, commands.size() ); forkNum < total; forkNum++ ) { - final TestProvidingInputStream testProvidingInputStream = new TestProvidingInputStream( messageQueue ); + final TestProvidingInputStream testProvidingInputStream = new TestProvidingInputStream( commands ); testStreams.add( testProvidingInputStream ); Callable<RunResult> pf = new Callable<RunResult>() { @@ -241,11 +243,11 @@ public class ForkStarter }; results.add( executorService.submit( pf ) ); } + dispatchTestSetFinished( testStreams ); return awaitResultsDone( results, executorService ); } finally { - closeTestStreams( testStreams ); closeExecutor( executorService ); } } @@ -318,11 +320,11 @@ public class ForkStarter return globalResult; } - private static void closeTestStreams( Iterable<TestProvidingInputStream> testStreams ) + private static void dispatchTestSetFinished( Iterable<TestProvidingInputStream> testStreams ) { - for ( TestProvidingInputStream testStream: testStreams ) + for ( TestProvidingInputStream testStream : testStreams ) { - testStream.close(); + testStream.testSetFinished(); } } http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStream.java ---------------------------------------------------------------------- diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStream.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStream.java index 31ca4d7..97bf24d 100644 --- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStream.java +++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStream.java @@ -19,14 +19,17 @@ package org.apache.maven.plugin.surefire.booterclient.lazytestprovider; * under the License. */ -import java.io.EOFException; +import org.apache.maven.surefire.booter.Command; +import org.apache.maven.surefire.booter.MasterProcessCommand; + import java.io.IOException; import java.io.InputStream; import java.util.Queue; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; -import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication; +import static org.apache.maven.surefire.booter.MasterProcessCommand.TEST_SET_FINISHED; +import static org.apache.maven.surefire.util.internal.StringUtils.requireNonNull; /** * An {@link InputStream} that, when read, provides test class names out of a queue. @@ -45,7 +48,7 @@ public class TestProvidingInputStream { private final Semaphore semaphore = new Semaphore( 0 ); - private final Queue<String> testItemQueue; + private final Queue<Command> commands; private final AtomicBoolean closed = new AtomicBoolean(); @@ -53,16 +56,18 @@ public class TestProvidingInputStream private int currentPos; + private MasterProcessCommand lastCommand; + private volatile FlushReceiverProvider flushReceiverProvider; /** * C'tor * - * @param testItemQueue source of the tests to be read from this stream + * @param commands source of the tests to be read from this stream */ - public TestProvidingInputStream( Queue<String> testItemQueue ) + public TestProvidingInputStream( Queue<Command> commands ) { - this.testItemQueue = testItemQueue; + this.commands = commands; } /** @@ -70,7 +75,12 @@ public class TestProvidingInputStream */ public void setFlushReceiverProvider( FlushReceiverProvider flushReceiverProvider ) { - this.flushReceiverProvider = flushReceiverProvider; + this.flushReceiverProvider = requireNonNull( flushReceiverProvider ); + } + + public void testSetFinished() + { + commands.add( new Command( TEST_SET_FINISHED ) ); } /** @@ -84,63 +94,45 @@ public class TestProvidingInputStream public int read() throws IOException { - if ( closed.get() ) - { - // help GC to free this object because StreamFeeder Thread cannot read it after IOE - currentBuffer = null; - throw new EOFException( "closed unexpectedly" ); - } - else + byte[] buffer = currentBuffer; + if ( buffer == null ) { - // isolation of instance variable in Thread stack - byte[] buffer = currentBuffer; - - if ( buffer == null ) + if ( flushReceiverProvider != null ) { - if ( flushReceiverProvider != null ) - { - FlushReceiver flushing = flushReceiverProvider.getFlushReceiver(); - if ( flushing != null ) - { - flushing.flush(); - } - } - - awaitNextTest(); - - if ( closed.get() ) + FlushReceiver flushReceiver = flushReceiverProvider.getFlushReceiver(); + if ( flushReceiver != null ) { - // help GC to free this object because StreamFeeder Thread cannot read it after IOE - currentBuffer = null; - throw new EOFException( "closed unexpectedly" ); - } - - String currentElement = testItemQueue.poll(); - if ( currentElement != null ) - { - buffer = encodeStringForForkCommunication( currentElement ); - // may override NULL from close(), therefore setting explicitly to NULL if IOE elsewhere - currentBuffer = buffer; - currentPos = 0; - } - else - { - // help GC to free this object since it's not needed as there's no new test - currentBuffer = null; - return -1; + flushReceiver.flush(); } } - if ( currentPos < buffer.length ) + if ( lastCommand == TEST_SET_FINISHED || closed.get() ) { - return buffer[currentPos++] & 0xff; + close(); + return -1; } - else + + awaitNextTest(); + + if ( closed.get() ) { - currentBuffer = null; - return '\n' & 0xff; + return -1; } + + Command command = commands.poll(); + lastCommand = command.getCommandType(); + String test = command.getData(); + buffer = lastCommand == TEST_SET_FINISHED ? lastCommand.encode() : lastCommand.encode( test ); + } + + int b = buffer[currentPos++] & 0xff; + if ( currentPos == buffer.length ) + { + buffer = null; + currentPos = 0; } + currentBuffer = buffer; + return b; } private void awaitNextTest() @@ -175,11 +167,8 @@ public class TestProvidingInputStream if ( closed.compareAndSet( false, true ) ) { currentBuffer = null; - int permits = semaphore.drainPermits(); - if ( permits == 0 ) - { - semaphore.release(); - } + semaphore.drainPermits(); + semaphore.release(); } } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStreamTest.java ---------------------------------------------------------------------- diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStreamTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStreamTest.java new file mode 100644 index 0000000..feca48b --- /dev/null +++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStreamTest.java @@ -0,0 +1,141 @@ +package org.apache.maven.plugin.surefire.booterclient.lazytestprovider; + +/* + * 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.apache.maven.surefire.booter.Command; +import org.apache.maven.surefire.booter.MasterProcessCommand; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Asserts that this stream properly reads bytes from queue. + * + * @author <a href="mailto:tibordig...@apache.org">Tibor Digana (tibor17)</a> + * @since 2.19 + */ +public class TestProvidingInputStreamTest +{ + @Test + public void closedStreamShouldReturnEndOfStream() + throws IOException + { + Queue<Command> commands = new ArrayDeque<Command>(); + TestProvidingInputStream is = new TestProvidingInputStream( commands ); + is.close(); + assertThat( is.read(), is( -1 ) ); + } + + @Test + public void emptyStreamShouldWaitUntilClosed() + throws Exception + { + Queue<Command> commands = new ArrayDeque<Command>(); + final TestProvidingInputStream is = new TestProvidingInputStream( commands ); + final Thread streamThread = Thread.currentThread(); + FutureTask<Thread.State> futureTask = new FutureTask<Thread.State>( new Callable<Thread.State>() + { + public Thread.State call() + { + sleep( 1000 ); + Thread.State state = streamThread.getState(); + is.close(); + return state; + } + } ); + Thread assertionThread = new Thread( futureTask ); + assertionThread.start(); + assertThat( is.read(), is( -1 ) ); + Thread.State state = futureTask.get(); + assertThat( state, is( Thread.State.WAITING ) ); + } + + @Test + public void finishedTestsetShouldNotBlock() + throws IOException + { + Queue<Command> commands = new ArrayDeque<Command>(); + commands.add( new Command( MasterProcessCommand.TEST_SET_FINISHED ) ); + final TestProvidingInputStream is = new TestProvidingInputStream( commands ); + new Thread( new Runnable() + { + public void run() + { + is.provideNewTest(); + } + } ).start(); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 1 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( -1 ) ); + } + + @Test + public void shouldReadTest() + throws IOException + { + Queue<Command> commands = new ArrayDeque<Command>(); + commands.add( new Command( MasterProcessCommand.RUN_CLASS, "Test" ) ); + final TestProvidingInputStream is = new TestProvidingInputStream( commands ); + new Thread( new Runnable() + { + public void run() + { + is.provideNewTest(); + } + } ).start(); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 0 ) ); + assertThat( is.read(), is( 4 ) ); + assertThat( is.read(), is( (int) 'T' ) ); + assertThat( is.read(), is( (int) 'e' ) ); + assertThat( is.read(), is( (int) 's' ) ); + assertThat( is.read(), is( (int) 't' ) ); + } + + private static void sleep( long millis ) + { + try + { + TimeUnit.MILLISECONDS.sleep( millis ); + } + catch ( InterruptedException e ) + { + } + } +} http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java ---------------------------------------------------------------------- diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java b/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java new file mode 100644 index 0000000..995e2a1 --- /dev/null +++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java @@ -0,0 +1,76 @@ +package org.apache.maven.surefire; + +/* + * 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 junit.framework.JUnit4TestAdapter; +import junit.framework.Test; +import org.apache.maven.plugin.surefire.SurefirePropertiesTest; +import org.apache.maven.plugin.surefire.booterclient.BooterDeserializerProviderConfigurationTest; +import org.apache.maven.plugin.surefire.booterclient.BooterDeserializerStartupConfigurationTest; +import org.apache.maven.plugin.surefire.booterclient.ForkConfigurationTest; +import org.apache.maven.plugin.surefire.booterclient.ForkingRunListenerTest; +import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestProvidingInputStreamTest; +import org.apache.maven.plugin.surefire.report.DefaultReporterFactoryTest; +import org.apache.maven.plugin.surefire.report.StatelessXmlReporterTest; +import org.apache.maven.plugin.surefire.report.WrappedReportEntryTest; +import org.apache.maven.plugin.surefire.runorder.RunEntryStatisticsMapTest; +import org.apache.maven.plugin.surefire.util.DependenciesScannerTest; +import org.apache.maven.plugin.surefire.util.DirectoryScannerTest; +import org.apache.maven.plugin.surefire.util.SpecificFileFilterTest; +import org.apache.maven.surefire.report.ConsoleOutputFileReporterTest; +import org.apache.maven.surefire.report.FileReporterTest; +import org.apache.maven.surefire.report.RunStatisticsTest; +import org.apache.maven.surefire.util.RelocatorTest; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** + * Adapt the JUnit4 tests which use only annotations to the JUnit3 test suite. + * + * @author Tibor Digana (tibor17) + * @since 2.19 + */ +@Suite.SuiteClasses( { + RelocatorTest.class, + RunStatisticsTest.class, + FileReporterTest.class, + ConsoleOutputFileReporterTest.class, + SurefirePropertiesTest.class, + SpecificFileFilterTest.class, + DirectoryScannerTest.class, + DependenciesScannerTest.class, + RunEntryStatisticsMapTest.class, + WrappedReportEntryTest.class, + StatelessXmlReporterTest.class, + DefaultReporterFactoryTest.class, + ForkingRunListenerTest.class, + ForkConfigurationTest.class, + BooterDeserializerStartupConfigurationTest.class, + BooterDeserializerProviderConfigurationTest.class, + TestProvidingInputStreamTest.class, +} ) +@RunWith( Suite.class ) +public class JUnit4SuiteTest +{ + public static Test suite() + { + return new JUnit4TestAdapter( JUnit4SuiteTest.class ); + } +} http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index c802a4f..5eb15e3 100644 --- a/pom.xml +++ b/pom.xml @@ -226,6 +226,12 @@ <scope>test</scope> </dependency> <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <version>1.3</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>com.google.code.findbugs</groupId> <artifactId>jsr305</artifactId> <version>2.0.1</version> @@ -239,6 +245,11 @@ <artifactId>junit</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-api/src/main/java/org/apache/maven/surefire/booter/Command.java ---------------------------------------------------------------------- diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/Command.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/Command.java new file mode 100644 index 0000000..992c78c --- /dev/null +++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/Command.java @@ -0,0 +1,50 @@ +package org.apache.maven.surefire.booter; + +/* + * 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. + */ + +/** + * Encapsulates data and command sent from master to forked process. + */ +public final class Command +{ + private final MasterProcessCommand command; + private final String data; + + public Command( MasterProcessCommand command, String data ) + { + this.command = command; + this.data = data; + } + + public Command( MasterProcessCommand command ) + { + this( command, null ); + } + + public MasterProcessCommand getCommandType() + { + return command; + } + + public String getData() + { + return data; + } +} http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java ---------------------------------------------------------------------- diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java new file mode 100644 index 0000000..0a553f7 --- /dev/null +++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java @@ -0,0 +1,199 @@ +package org.apache.maven.surefire.booter; + +/* + * 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 java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import static org.apache.maven.surefire.util.internal.StringUtils.FORK_STREAM_CHARSET_NAME; +import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication; +import static org.apache.maven.surefire.util.internal.StringUtils.requireNonNull; +import static java.lang.String.format; + +/** + * Commands which are sent from plugin to the forked jvm. + * Support and methods related to the commands. + * + * @author <a href="mailto:tibordig...@apache.org">Tibor Digana (tibor17)</a> + * @since 2.19 + */ +public enum MasterProcessCommand +{ + RUN_CLASS( 0, String.class ), + TEST_SET_FINISHED( 1, Void.class ), + STOP_ON_NEXT_TEST( 2, Void.class ), + SHUTDOWN( 3, Void.class ), + /** To tell a forked process that the master process is still alive. Repeated after 30 seconds. */ + NOOP( 4, Void.class ); + + private final int id; + + private final Class<?> dataType; + + MasterProcessCommand( int id, Class<?> dataType ) + { + this.id = id; + this.dataType = requireNonNull( dataType, "dataType cannot be null" ); + } + + public int getId() + { + return id; + } + + public Class<?> getDataType() + { + return dataType; + } + + @SuppressWarnings( "checkstyle:magicnumber" ) + public byte[] encode( String data ) + { + if ( getDataType() != String.class ) + { + throw new IllegalArgumentException( "Data type can be only " + getDataType() ); + } + byte[] dataBytes = fromDataType( data ); + byte[] encoded = new byte[8 + dataBytes.length]; + int command = getId(); + int len = dataBytes.length; + setCommandAndDataLength( command, len, encoded ); + System.arraycopy( dataBytes, 0, encoded, 8, dataBytes.length ); + return encoded; + } + + @SuppressWarnings( "checkstyle:magicnumber" ) + public byte[] encode() + { + if ( getDataType() != Void.class ) + { + throw new IllegalArgumentException( "Data type can be only " + getDataType() ); + } + byte[] encoded = new byte[8]; + int command = getId(); + setCommandAndDataLength( command, 0, encoded ); + return encoded; + } + + public static Command decode( DataInputStream is ) + throws IOException + { + MasterProcessCommand command = resolve( is.readInt() ); + if ( command == null ) + { + return null; + } + else + { + int dataLength = is.readInt(); + if ( dataLength > 0 ) + { + byte[] buffer = new byte[dataLength]; + int read = 0; + int total = 0; + do + { + total += read; + read = is.read( buffer, total, dataLength - total ); + } while ( read > 0 ); + + if ( command.getDataType() == Void.class ) + { + // must read entire sequence to get to the next command; cannot be above the loop + throw new IOException( "Command " + command + " read Void data with length " + dataLength ); + } + + if ( total != dataLength ) + { + if ( read == -1 ) + { + throw new EOFException( "stream closed" ); + } + + throw new EOFException( format( "%s read %d out of %d bytes", + MasterProcessCommand.class, total, dataLength ) ); + } + + String data = command.toDataTypeAsString( buffer ); + return new Command( command, data ); + } + else + { + return new Command( command ); + } + } + } + + String toDataTypeAsString( byte... data ) + { + try + { + switch ( this ) + { + case RUN_CLASS: + return new String( data, FORK_STREAM_CHARSET_NAME ); + default: + return null; + } + } + catch ( UnsupportedEncodingException e ) + { + throw new IllegalStateException( e ); + } + } + + byte[] fromDataType( String data ) + { + switch ( this ) + { + case RUN_CLASS: + return encodeStringForForkCommunication( data ); + default: + return new byte[0]; + } + } + + static MasterProcessCommand resolve( int id ) + { + for ( MasterProcessCommand command : values() ) + { + if ( id == command.id ) + { + return command; + } + } + return null; + } + + @SuppressWarnings( "checkstyle:magicnumber" ) + static void setCommandAndDataLength( int command, int dataLength, byte... encoded ) + { + encoded[0] = (byte) ( command >>> 24 ); + encoded[1] = (byte) ( command >>> 16 ); + encoded[2] = (byte) ( command >>> 8 ); + encoded[3] = (byte) command; + encoded[4] = (byte) ( dataLength >>> 24 ); + encoded[5] = (byte) ( dataLength >>> 16 ); + encoded[6] = (byte) ( dataLength >>> 8 ); + encoded[7] = (byte) dataLength; + } +} http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-api/src/main/java/org/apache/maven/surefire/testset/TestListResolver.java ---------------------------------------------------------------------- diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/testset/TestListResolver.java b/surefire-api/src/main/java/org/apache/maven/surefire/testset/TestListResolver.java index b233147..6bda2ad 100644 --- a/surefire-api/src/main/java/org/apache/maven/surefire/testset/TestListResolver.java +++ b/surefire-api/src/main/java/org/apache/maven/surefire/testset/TestListResolver.java @@ -180,8 +180,7 @@ public class TestListResolver public boolean shouldRun( Class<?> testClass, String methodName ) { - String clsFile = testClass == null ? null : testClass.getName().replace( '.', '/' ) + JAVA_CLASS_FILE_EXTENSION; - return shouldRun( clsFile, methodName ); + return shouldRun( toClassFileName( testClass ), methodName ); } public boolean shouldRun( String testClassFile, String methodName ) @@ -285,6 +284,18 @@ public class TestListResolver return getPluginParameterTest(); } + public static String toClassFileName( Class<?> test ) + { + return test == null ? null : toClassFileName( test.getName() ); + } + + public static String toClassFileName( String fullyQualifiedTestClass ) + { + return fullyQualifiedTestClass == null + ? null + : fullyQualifiedTestClass.replace( '.', '/' ) + JAVA_CLASS_FILE_EXTENSION; + } + static String removeExclamationMark( String s ) { return s.length() != 0 && s.charAt( 0 ) == '!' ? s.substring( 1 ) : s; http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-api/src/main/java/org/apache/maven/surefire/util/internal/StringUtils.java ---------------------------------------------------------------------- diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/util/internal/StringUtils.java b/surefire-api/src/main/java/org/apache/maven/surefire/util/internal/StringUtils.java index 8158fb7..043b87b 100644 --- a/surefire-api/src/main/java/org/apache/maven/surefire/util/internal/StringUtils.java +++ b/surefire-api/src/main/java/org/apache/maven/surefire/util/internal/StringUtils.java @@ -350,4 +350,24 @@ public class StringUtils throw new RuntimeException( "The JVM must support Charset " + FORK_STREAM_CHARSET_NAME, e ); } } + + /* + * In JDK7 use java.util.Objects instead. + * */ + public static <T> T requireNonNull( T obj, String message ) + { + if ( obj == null ) + { + throw new NullPointerException( message ); + } + return obj; + } + + /* + * In JDK7 use java.util.Objects instead. + * */ + public static <T> T requireNonNull( T obj ) + { + return requireNonNull( obj, null ); + } } http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-api/src/test/java/org/apache/maven/surefire/booter/MasterProcessCommandTest.java ---------------------------------------------------------------------- diff --git a/surefire-api/src/test/java/org/apache/maven/surefire/booter/MasterProcessCommandTest.java b/surefire-api/src/test/java/org/apache/maven/surefire/booter/MasterProcessCommandTest.java new file mode 100644 index 0000000..00e7bbc --- /dev/null +++ b/surefire-api/src/test/java/org/apache/maven/surefire/booter/MasterProcessCommandTest.java @@ -0,0 +1,155 @@ +package org.apache.maven.surefire.booter; +/* + * 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 junit.framework.TestCase; +import org.junit.Rule; +import org.junit.rules.ExpectedException; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; + +import static org.apache.maven.surefire.booter.MasterProcessCommand.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.junit.Assert.assertNotNull; + +/** + * @author <a href="mailto:tibordig...@apache.org">Tibor Digana (tibor17)</a> + * @since 2.19 + */ +public class MasterProcessCommandTest + extends TestCase +{ + @Rule + public final ExpectedException exception = ExpectedException.none(); + + public void testEncodedStreamSequence() + { + byte[] streamSequence = new byte[10]; + streamSequence[8] = (byte) 'T'; + streamSequence[9] = (byte) 'e'; + setCommandAndDataLength( 256, 2, streamSequence ); + assertEquals( streamSequence[0], (byte) 0 ); + assertEquals( streamSequence[1], (byte) 0 ); + assertEquals( streamSequence[2], (byte) 1 ); + assertEquals( streamSequence[3], (byte) 0 ); + assertEquals( streamSequence[4], (byte) 0 ); + assertEquals( streamSequence[5], (byte) 0 ); + assertEquals( streamSequence[6], (byte) 0 ); + assertEquals( streamSequence[7], (byte) 2 ); + // remain unchanged + assertEquals( streamSequence[8], (byte) 'T' ); + assertEquals( streamSequence[9], (byte) 'e' ); + } + + public void testResolved() + { + for ( MasterProcessCommand command : MasterProcessCommand.values() ) + { + assertThat( command, is( resolve( command.getId() ) ) ); + } + } + + public void testDataToByteArrayAndBack() + { + String dummyData = "pkg.Test"; + for ( MasterProcessCommand command : MasterProcessCommand.values() ) + { + switch ( command ) + { + case RUN_CLASS: + assertEquals( String.class, command.getDataType() ); + byte[] encoded = command.fromDataType( dummyData ); + assertThat( encoded.length, is( 8 ) ); + assertThat( encoded[0], is( (byte) 'p' ) ); + assertThat( encoded[1], is( (byte) 'k' ) ); + assertThat( encoded[2], is( (byte) 'g' ) ); + assertThat( encoded[3], is( (byte) '.' ) ); + assertThat( encoded[4], is( (byte) 'T' ) ); + assertThat( encoded[5], is( (byte) 'e' ) ); + assertThat( encoded[6], is( (byte) 's' ) ); + assertThat( encoded[7], is( (byte) 't' ) ); + String decoded = command.toDataTypeAsString( encoded ); + assertThat( decoded, is( dummyData ) ); + break; + case TEST_SET_FINISHED: + assertEquals( Void.class, command.getDataType() ); + encoded = command.fromDataType( dummyData ); + assertThat( encoded.length, is( 0 ) ); + decoded = command.toDataTypeAsString( encoded ); + assertNull( decoded ); + break; + case STOP_ON_NEXT_TEST: + assertEquals( Void.class, command.getDataType() ); + encoded = command.fromDataType( dummyData ); + assertThat( encoded.length, is( 0 ) ); + decoded = command.toDataTypeAsString( encoded ); + assertNull( decoded ); + break; + case SHUTDOWN: + assertEquals( Void.class, command.getDataType() ); + encoded = command.fromDataType( dummyData ); + assertThat( encoded.length, is( 0 ) ); + decoded = command.toDataTypeAsString( encoded ); + assertNull( decoded ); + break; + case NOOP: + assertEquals( Void.class, command.getDataType() ); + encoded = command.fromDataType( dummyData ); + assertThat( encoded.length, is( 0 ) ); + decoded = command.toDataTypeAsString( encoded ); + assertNull( decoded ); + break; + default: + fail(); + } + assertThat( command, is( resolve( command.getId() ) ) ); + } + } + + public void testEncodedDecodedIsSameForRunClass() + throws IOException + { + byte[] encoded = RUN_CLASS.encode( "pkg.Test" ); + assertThat( encoded.length, is( 16 ) ); + assertThat( encoded[0], is( (byte) 0 ) ); + assertThat( encoded[1], is( (byte) 0 ) ); + assertThat( encoded[2], is( (byte) 0 ) ); + assertThat( encoded[3], is( (byte) 0 ) ); + assertThat( encoded[4], is( (byte) 0 ) ); + assertThat( encoded[5], is( (byte) 0 ) ); + assertThat( encoded[6], is( (byte) 0 ) ); + assertThat( encoded[7], is( (byte) 8 ) ); + assertThat( encoded[8], is( (byte) 'p' ) ); + assertThat( encoded[9], is( (byte) 'k' ) ); + assertThat( encoded[10], is( (byte) 'g' ) ); + assertThat( encoded[11], is( (byte) '.' ) ); + assertThat( encoded[12], is( (byte) 'T' ) ); + assertThat( encoded[13], is( (byte) 'e' ) ); + assertThat( encoded[14], is( (byte) 's' ) ); + assertThat( encoded[15], is( (byte) 't' ) ); + Command command = decode( new DataInputStream( new ByteArrayInputStream( encoded ) ) ); + assertNotNull( command ); + assertThat( command.getCommandType(), is( RUN_CLASS ) ); + assertThat( command.getData(), is( "pkg.Test" ) ); + } +} http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-booter/pom.xml ---------------------------------------------------------------------- diff --git a/surefire-booter/pom.xml b/surefire-booter/pom.xml index 50598bf..bf37966 100644 --- a/surefire-booter/pom.xml +++ b/surefire-booter/pom.xml @@ -50,6 +50,7 @@ </dependency> </dependencies> <configuration> + <redirectTestOutputToFile>true</redirectTestOutputToFile> <includes> <include>**/JUnit4SuiteTest.java</include> </includes> http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java ---------------------------------------------------------------------- diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java index ca5dd26..518c523 100644 --- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java +++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java @@ -98,7 +98,7 @@ public final class ForkedBooter } else if ( readTestsFromInputStream ) { - testSet = new LazyTestsToRun( System.in, originalOut ); + testSet = new LazyTestsToRun( originalOut ); } else { http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-booter/src/main/java/org/apache/maven/surefire/booter/LazyTestsToRun.java ---------------------------------------------------------------------- diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/LazyTestsToRun.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/LazyTestsToRun.java index a5611c0..d3e770c 100644 --- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/LazyTestsToRun.java +++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/LazyTestsToRun.java @@ -19,135 +19,55 @@ package org.apache.maven.surefire.booter; * under the License. */ -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; -import java.util.List; import org.apache.maven.surefire.util.ReflectionUtils; import org.apache.maven.surefire.util.TestsToRun; -// CHECKSTYLE_OFF: imports -import static org.apache.maven.surefire.util.internal.StringUtils.FORK_STREAM_CHARSET_NAME; -import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication; - /** * A variant of TestsToRun that is provided with test class names - * from an {@link InputStream} (e.g. {@code System.in}). The method - * {@link #iterator()} returns an Iterator that blocks on calls to - * {@link Iterator#hasNext()} until new classes are available, or no more - * classes will be available. + * from an {@code System.in}. + * The method {@link #iterator()} returns an Iterator that blocks on calls to + * {@link Iterator#hasNext()} or {@link Iterator#next()} until new classes are available, or no more + * classes will be available or the internal stream is closed. * * @author Andreas Gudian + * @author Tibor Digana */ final class LazyTestsToRun extends TestsToRun { - private final List<Class<?>> workQueue = new ArrayList<Class<?>>(); - - private final BufferedReader inputReader; - private final PrintStream originalOutStream; - private boolean streamClosed; - /** * C'tor * - * @param testSource source to read the tests from * @param originalOutStream the output stream to use when requesting new new tests */ - public LazyTestsToRun( InputStream testSource, PrintStream originalOutStream ) + LazyTestsToRun( PrintStream originalOutStream ) { - super( Collections.<Class<?>>emptyList() ); + super( Collections.<Class<?>>emptySet() ); this.originalOutStream = originalOutStream; - - try - { - inputReader = new BufferedReader( new InputStreamReader( testSource, FORK_STREAM_CHARSET_NAME ) ); - } - catch ( UnsupportedEncodingException e ) - { - throw new RuntimeException( "The JVM must support Charset " + FORK_STREAM_CHARSET_NAME, e ); - } - } - - private void addWorkItem( String className ) - { - synchronized ( workQueue ) - { - workQueue.add( ReflectionUtils.loadClass( Thread.currentThread().getContextClassLoader(), className ) ); - } - } - - private void requestNextTest() - { - byte[] encoded = - encodeStringForForkCommunication( ( (char) ForkingRunListener.BOOTERCODE_NEXT_TEST ) + ",0,want more!\n" ); - originalOutStream.write( encoded, 0, encoded.length ); } private final class BlockingIterator implements Iterator<Class<?>> { - private int lastPos = -1; + private final Iterator<String> it = + MasterProcessReader.getReader().getIterableClasses( originalOutStream ).iterator(); public boolean hasNext() { - int nextPos = lastPos + 1; - synchronized ( workQueue ) - { - if ( workQueue.size() > nextPos ) - { - return true; - } - else - { - if ( needsToWaitForInput( nextPos ) ) - { - requestNextTest(); - try - { - String nextClassName = inputReader.readLine(); - if ( nextClassName == null ) - { - streamClosed = true; - } - else - { - addWorkItem( nextClassName ); - } - } - catch ( IOException e ) - { - streamClosed = true; - return false; - } - } - - return workQueue.size() > nextPos; - } - } - } - - private boolean needsToWaitForInput( int nextPos ) - { - return workQueue.size() == nextPos && !streamClosed; + return it.hasNext(); } public Class<?> next() { - synchronized ( workQueue ) - { - return workQueue.get( ++lastPos ); - } + String clazz = it.next(); + return ReflectionUtils.loadClass( Thread.currentThread().getContextClassLoader(), clazz ); } public void remove() @@ -170,14 +90,7 @@ final class LazyTestsToRun */ public String toString() { - StringBuilder sb = new StringBuilder( "LazyTestsToRun " ); - synchronized ( workQueue ) - { - sb.append( "(more items expected: " ).append( !streamClosed ).append( "): " ); - sb.append( workQueue ); - } - - return sb.toString(); + return "LazyTestsToRun"; } /* (non-Javadoc) http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-booter/src/main/java/org/apache/maven/surefire/booter/MasterProcessListener.java ---------------------------------------------------------------------- diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/MasterProcessListener.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/MasterProcessListener.java new file mode 100644 index 0000000..89a4f89 --- /dev/null +++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/MasterProcessListener.java @@ -0,0 +1,28 @@ +package org.apache.maven.surefire.booter; + +/* + * 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. + */ + +/** + * listener interface + */ +public interface MasterProcessListener +{ + void update( Command command ); +} http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-booter/src/main/java/org/apache/maven/surefire/booter/MasterProcessReader.java ---------------------------------------------------------------------- diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/MasterProcessReader.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/MasterProcessReader.java new file mode 100644 index 0000000..1d86167 --- /dev/null +++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/MasterProcessReader.java @@ -0,0 +1,255 @@ +package org.apache.maven.surefire.booter; + +/* + * 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.apache.maven.surefire.util.internal.DaemonThreadFactory; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicReference; + +import static java.lang.Thread.State.NEW; +import static java.lang.Thread.State.RUNNABLE; +import static java.lang.Thread.State.TERMINATED; +import static org.apache.maven.surefire.booter.MasterProcessCommand.TEST_SET_FINISHED; +import static org.apache.maven.surefire.booter.MasterProcessCommand.RUN_CLASS; +import static org.apache.maven.surefire.booter.MasterProcessCommand.decode; +import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication; +import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank; +import static org.apache.maven.surefire.util.internal.StringUtils.isBlank; +import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_NEXT_TEST; + +/** + * Testing singleton {@code MasterProcessReader}. + * + * @author <a href="mailto:tibordig...@apache.org">Tibor Digana (tibor17)</a> + * @since 2.19 + */ +public final class MasterProcessReader +{ + private static final MasterProcessReader READER = new MasterProcessReader(); + + private final BlockingQueue<Command> classes = new LinkedBlockingQueue<Command>(); + + private final Queue<MasterProcessListener> listeners = new ConcurrentLinkedQueue<MasterProcessListener>(); + + private final Thread commandThread = + DaemonThreadFactory.newDaemonThread( new CommandRunnable(), "surefire-forkedjvm-command-thread" ); + + private final AtomicReference<Thread.State> state = new AtomicReference<Thread.State>( NEW ); + + public static MasterProcessReader getReader() + { + if ( READER.state.compareAndSet( NEW, RUNNABLE ) ) + { + READER.commandThread.start(); + } + return READER; + } + + private MasterProcessReader() + { + commandThread.setDaemon( true ); + } + + Iterable<String> getIterableClasses( PrintStream originalOutStream ) + { + return new ClassesIterable( originalOutStream ); + } + + void stop( boolean interruptCurrentThread ) + { + if ( state.compareAndSet( NEW, TERMINATED ) || state.compareAndSet( RUNNABLE, TERMINATED ) ) + { + classes.drainTo( new ArrayList<Command>() ); + listeners.clear(); + commandThread.interrupt(); + if ( interruptCurrentThread ) + { + Thread.currentThread().interrupt(); + } + } + } + + private final class ClassesIterable + implements Iterable<String> + { + private final ClassesIterator it; + + public ClassesIterable( PrintStream originalOutStream ) + { + it = new ClassesIterator( originalOutStream ); + } + + public Iterator<String> iterator() + { + return it; + } + } + + private final class ClassesIterator + implements Iterator<String> + { + private final PrintStream originalOutStream; + + private String clazz; + + private ClassesIterator( PrintStream originalOutStream ) + { + this.originalOutStream = originalOutStream; + } + + public boolean hasNext() + { + popUnread(); + return isNotBlank( clazz ); + } + + public String next() + { + popUnread(); + try + { + if ( isBlank( clazz ) ) + { + throw new NoSuchElementException(); + } + else + { + return clazz; + } + } + finally + { + clazz = null; + } + } + + public void remove() + { + throw new UnsupportedOperationException(); + } + + private void popUnread() + { + if ( state.get() == TERMINATED ) + { + clazz = null; + return; + } + + if ( isBlank( clazz ) ) + { + requestNextTest(); + try + { + do + { + Command command = MasterProcessReader.this.classes.take(); + clazz = command.getCommandType() == TEST_SET_FINISHED ? null : command.getData(); + } + while ( tryNullWhiteClass() ); + } + catch ( InterruptedException e ) + { + Thread.currentThread().interrupt(); + clazz = null; + } + } + + if ( state.get() == TERMINATED ) + { + clazz = null; + } + } + + private boolean tryNullWhiteClass() + { + if ( clazz != null && isBlank( clazz ) ) + { + clazz = null; + return true; + } + else + { + return false; + } + } + + private void requestNextTest() + { + byte[] encoded = encodeStringForForkCommunication( ( (char) BOOTERCODE_NEXT_TEST ) + ",0,want more!\n" ); + originalOutStream.write( encoded, 0, encoded.length ); + } + } + + private final class CommandRunnable + implements Runnable + { + public void run() + { + DataInputStream stdIn = new DataInputStream( System.in ); + try + { + while ( MasterProcessReader.this.state.get() == RUNNABLE ) + { + Command command = decode( stdIn ); + if ( command != null ) + { + // command is null if stream is corrupted, i.e. the first sequence could not be recognized + insert( command ); + } + } + } + catch ( IOException e ) + { + // ensure fail-safe iterator as well as safe to finish in for-each loop using ClassesIterator + insert( new Command( TEST_SET_FINISHED ) ); + // and let us know what has happened with the stream + throw new IllegalStateException( e.getLocalizedMessage(), e ); + } + } + + @SuppressWarnings( "unchecked" ) + private void insert( Command cmd ) + { + MasterProcessCommand commandType = cmd.getCommandType(); + if ( commandType == RUN_CLASS || commandType == TEST_SET_FINISHED ) + { + MasterProcessReader.this.classes.add( cmd ); + } + else + { + for ( MasterProcessListener listener : MasterProcessReader.this.listeners ) + { + listener.update( cmd ); + } + } + } + } +} http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java ---------------------------------------------------------------------- diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java index d426d27..5e84775 100644 --- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java +++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java @@ -32,6 +32,7 @@ import org.junit.runners.Suite; */ @Suite.SuiteClasses( { ClasspathTest.class, + MasterProcessReaderTest.class, PropertiesWrapperTest.class, SurefireReflectorTest.class } ) http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-booter/src/test/java/org/apache/maven/surefire/booter/MasterProcessReaderTest.java ---------------------------------------------------------------------- diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/MasterProcessReaderTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/MasterProcessReaderTest.java new file mode 100644 index 0000000..9440e0d --- /dev/null +++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/MasterProcessReaderTest.java @@ -0,0 +1,193 @@ +package org.apache.maven.surefire.booter; + +/* + * 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.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.apache.maven.surefire.util.internal.StringUtils.FORK_STREAM_CHARSET_NAME; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Testing singleton {@code MasterProcessReader} in multiple class loaders. + * + * @author <a href="mailto:tibordig...@apache.org">Tibor Digana (tibor17)</a> + * @since 2.19 + */ +@RunWith( NewClassLoaderRunner.class ) +public class MasterProcessReaderTest +{ + private final BlockingQueue<Byte> blockingStream = new LinkedBlockingQueue<Byte>(); + private InputStream realInputStream; + + @Before + public void init() + throws UnsupportedEncodingException + { + Thread.interrupted(); + realInputStream = System.in; + addThisTestToPipeline( MasterProcessReaderTest.class.getName() ); + System.setIn( new SystemInputStream() ); + } + + @After + public void deinit() + { + System.setIn( realInputStream ); + } + + @Test + public void readJustOneClass() throws Exception + { + MasterProcessReader reader = MasterProcessReader.getReader(); + Iterator<String> it = reader.getIterableClasses( new PrintStream( new ByteArrayOutputStream() ) ) + .iterator(); + assertTrue( it.hasNext() ); + assertThat( it.next(), is( getClass().getName() ) ); + reader.stop( true ); + assertFalse( it.hasNext() ); + try + { + it.next(); + fail(); + } + catch ( NoSuchElementException e ) + { + // expected + } + } + + @Test( expected = NoSuchElementException.class ) + public void stopBeforeReadInThread() + throws Throwable + { + final MasterProcessReader reader = MasterProcessReader.getReader(); + Runnable runnable = new Runnable() + { + public void run() + { + Iterator<String> it = reader.getIterableClasses( new PrintStream( new ByteArrayOutputStream() ) ) + .iterator(); + assertThat( it.next(), is( MasterProcessReaderTest.class.getName() ) ); + } + }; + FutureTask<Object> futureTask = new FutureTask<Object>( runnable, null ); + Thread t = new Thread( futureTask ); + reader.stop( false ); + t.start(); + try + { + futureTask.get(); + } + catch ( ExecutionException e ) + { + throw e.getCause(); + } + } + + @Test + public void readTwoClassesInThread() + throws Throwable + { + final MasterProcessReader reader = MasterProcessReader.getReader(); + final CountDownLatch counter = new CountDownLatch( 1 ); + Runnable runnable = new Runnable() + { + public void run() + { + Iterator<String> it = reader.getIterableClasses( new PrintStream( new ByteArrayOutputStream() ) ) + .iterator(); + assertThat( it.next(), is( MasterProcessReaderTest.class.getName() ) ); + counter.countDown(); + assertThat( it.next(), is( PropertiesWrapperTest.class.getName() ) ); + } + }; + FutureTask<Object> futureTask = new FutureTask<Object>( runnable, null ); + Thread t = new Thread( futureTask ); + t.start(); + counter.await(); + addThisTestToPipeline( PropertiesWrapperTest.class.getName() ); + try + { + futureTask.get(); + } + catch ( ExecutionException e ) + { + throw e.getCause(); + } + finally + { + reader.stop( false ); + } + } + + private class SystemInputStream + extends InputStream + { + @Override + public int read() + throws IOException + { + try + { + return MasterProcessReaderTest.this.blockingStream.take(); + } + catch ( InterruptedException e ) + { + throw new IOException( e ); + } + } + } + + private void addThisTestToPipeline( String cls ) + throws UnsupportedEncodingException + { + byte[] clazz = cls.getBytes( FORK_STREAM_CHARSET_NAME ); + ByteBuffer buffer = ByteBuffer.allocate( 8 + clazz.length ) + .putInt( MasterProcessCommand.RUN_CLASS.getId() ) + .putInt( clazz.length ) + .put( clazz ); + buffer.rewind(); + for ( ; buffer.hasRemaining(); ) + { + blockingStream.add( buffer.get() ); + } + } +} http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-booter/src/test/java/org/apache/maven/surefire/booter/NewClassLoaderRunner.java ---------------------------------------------------------------------- diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/NewClassLoaderRunner.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/NewClassLoaderRunner.java new file mode 100644 index 0000000..4342ec7 --- /dev/null +++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/NewClassLoaderRunner.java @@ -0,0 +1,191 @@ +package org.apache.maven.surefire.booter; + +/* + * 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.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.internal.runners.model.ReflectiveCallable; +import org.junit.internal.runners.statements.ExpectException; +import org.junit.internal.runners.statements.Fail; +import org.junit.internal.runners.statements.RunAfters; +import org.junit.internal.runners.statements.RunBefores; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; + +import java.lang.annotation.Annotation; +import java.net.URLClassLoader; +import java.util.List; + +/** + * JUnit runner testing methods in a separate class loader. + * + * @author Tibor Digana (tibor17) + * @since 2.19 + */ +public class NewClassLoaderRunner + extends BlockJUnit4ClassRunner +{ + private Class<?> cls; + + public NewClassLoaderRunner( Class<?> clazz ) + throws InitializationError + { + super( clazz ); + } + + @Override + protected void runChild( FrameworkMethod method, RunNotifier notifier ) + { + ClassLoader backup = Thread.currentThread().getContextClassLoader(); + try + { + TestClassLoader loader = new TestClassLoader(); + Thread.currentThread().setContextClassLoader( loader ); + cls = getFromTestClassLoader( getTestClass().getName(), loader ); + method = new FrameworkMethod( cls.getMethod( method.getName() ) ); + super.runChild( method, notifier ); + } + catch ( NoSuchMethodException e ) + { + throw new IllegalStateException( e ); + } + finally + { + Thread.currentThread().setContextClassLoader( backup ); + } + } + + @Override + protected Statement methodBlock( FrameworkMethod method ) + { + try + { + Object test = new ReflectiveCallable() + { + @Override + protected Object runReflectiveCall() + throws Throwable + { + return createTest(); + } + }.run(); + + Statement statement = methodInvoker( method, test ); + statement = possiblyExpectingExceptions( method, test, statement ); + statement = withBefores( method, test, statement ); + statement = withAfters( method, test, statement ); + return statement; + } + catch ( Throwable e ) + { + return new Fail( e ); + } + } + + @Override + @SuppressWarnings( "unchecked" ) + protected Statement possiblyExpectingExceptions( FrameworkMethod method, Object test, Statement next ) + { + try + { + Class<? extends Annotation> t = + (Class<? extends Annotation>) Thread.currentThread().getContextClassLoader().loadClass( + Test.class.getName() ); + Annotation annotation = method.getAnnotation( t ); + Class<? extends Throwable> exp = + (Class<? extends Throwable>) t.getMethod( "expected" ).invoke( annotation ); + boolean isException = exp != null && !Test.None.class.getName().equals( exp.getName() ); + return isException ? new ExpectException( next, exp ) : next; + } + catch ( Exception e ) + { + throw new IllegalStateException( e ); + } + } + + @Override + @SuppressWarnings( "unchecked" ) + protected Statement withBefores( FrameworkMethod method, Object target, Statement statement ) + { + try + { + Class<? extends Annotation> before = + (Class<? extends Annotation>) Thread.currentThread().getContextClassLoader().loadClass( + Before.class.getName() ); + List<FrameworkMethod> befores = new TestClass( target.getClass() ).getAnnotatedMethods( before ); + return befores.isEmpty() ? statement : new RunBefores( statement, befores, target ); + } + catch ( ClassNotFoundException e ) + { + throw new IllegalStateException( e ); + } + } + + @Override + @SuppressWarnings( "unchecked" ) + protected Statement withAfters( FrameworkMethod method, Object target, Statement statement ) + { + try + { + Class<? extends Annotation> after = + (Class<? extends Annotation>) Thread.currentThread().getContextClassLoader().loadClass( + After.class.getName() ); + List<FrameworkMethod> afters = new TestClass( target.getClass() ).getAnnotatedMethods( after ); + return afters.isEmpty() ? statement : new RunAfters( statement, afters, target ); + } + catch ( ClassNotFoundException e ) + { + throw new IllegalStateException( e ); + } + } + + @Override + protected Object createTest() + throws Exception + { + return cls == null ? super.createTest() : cls.getConstructor().newInstance(); + } + + private static Class<?> getFromTestClassLoader( String clazz, TestClassLoader loader ) + { + try + { + return Class.forName( clazz, true, loader ); + } + catch ( ClassNotFoundException e ) + { + throw new IllegalStateException( e ); + } + } + + public static class TestClassLoader + extends URLClassLoader + { + public TestClassLoader() + { + super( ( (URLClassLoader) Thread.currentThread().getContextClassLoader() ).getURLs(), null ); + } + } +} http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-providers/surefire-junit3/src/main/java/org/apache/maven/surefire/junit/JUnit3Provider.java ---------------------------------------------------------------------- diff --git a/surefire-providers/surefire-junit3/src/main/java/org/apache/maven/surefire/junit/JUnit3Provider.java b/surefire-providers/surefire-junit3/src/main/java/org/apache/maven/surefire/junit/JUnit3Provider.java index 470922f..e403a64 100644 --- a/surefire-providers/surefire-junit3/src/main/java/org/apache/maven/surefire/junit/JUnit3Provider.java +++ b/surefire-providers/surefire-junit3/src/main/java/org/apache/maven/surefire/junit/JUnit3Provider.java @@ -115,7 +115,6 @@ public class JUnit3Provider return reflector.isJUnit3Available() && jUnit3TestChecker.accept( clazz ) ? new JUnitTestSet( clazz, reflector ) : new PojoTestSet( clazz ); - } private void executeTestSet( SurefireTestSet testSet, RunListener reporter, ClassLoader classLoader ) http://git-wip-us.apache.org/repos/asf/maven-surefire/blob/78dca27e/surefire-providers/surefire-junit4/src/main/java/org/apache/maven/surefire/junit4/JUnit4Provider.java ---------------------------------------------------------------------- diff --git a/surefire-providers/surefire-junit4/src/main/java/org/apache/maven/surefire/junit4/JUnit4Provider.java b/surefire-providers/surefire-junit4/src/main/java/org/apache/maven/surefire/junit4/JUnit4Provider.java index e3b87af..3f0e57f 100644 --- a/surefire-providers/surefire-junit4/src/main/java/org/apache/maven/surefire/junit4/JUnit4Provider.java +++ b/surefire-providers/surefire-junit4/src/main/java/org/apache/maven/surefire/junit4/JUnit4Provider.java @@ -55,6 +55,8 @@ import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; import org.junit.runner.notification.RunNotifier; +import static org.apache.maven.surefire.testset.TestListResolver.toClassFileName; + /** * @author Kristian Rosenvold */ @@ -267,8 +269,6 @@ public class JUnit4Provider private static void execute( Class<?> testClass, RunNotifier notifier, Filter filter ) { final int classModifiers = testClass.getModifiers(); - - // filter.shouldRunClass( testClass ) if ( !Modifier.isAbstract( classModifiers ) && !Modifier.isInterface( classModifiers ) ) { Runner runner = Request.aClass( testClass ).filterWith( filter ).getRunner(); @@ -349,8 +349,6 @@ public class JUnit4Provider { private final TestListResolver methodFilter = JUnit4Provider.this.testResolver.createMethodFilters(); - private final TestsToRun testsToRun = JUnit4Provider.this.testsToRun; - @Override public boolean shouldRun( Description description ) { @@ -360,7 +358,7 @@ public class JUnit4Provider final boolean isValidTest = description.isTest() && cm.isValid(); final String clazz = cm.getClazz(); final String method = cm.getMethod(); - return isSuite || isValidTest && methodFilter.shouldRun( testsToRun.getClassByName( clazz ), method ); + return isSuite || isValidTest && methodFilter.shouldRun( toClassFileName( clazz ), method ); } @Override