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 5ee5538c10..265d48a61d 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,9 @@ import org.apache.maven.plugin.surefire.report.DefaultReporterFactory; import org.apache.maven.shared.utils.cli.CommandLineCallable; import org.apache.maven.shared.utils.cli.CommandLineException; +import org.apache.maven.shared.utils.cli.CommandLineUtils; +import org.apache.maven.shared.utils.cli.Commandline; +import org.apache.maven.shared.utils.cli.StreamConsumer; import org.apache.maven.surefire.booter.AbstractPathConfiguration; import org.apache.maven.surefire.booter.KeyValueSource; import org.apache.maven.surefire.booter.PropertiesWrapper; @@ -51,9 +54,15 @@ import org.apache.maven.surefire.util.DefaultScanResult; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.Closeable; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Map; @@ -86,16 +95,14 @@ import static org.apache.maven.plugin.surefire.SurefireHelper.replaceForkThreadsInPath; import static org.apache.maven.plugin.surefire.booterclient.ForkNumberBucket.drawNumber; import static org.apache.maven.plugin.surefire.booterclient.ForkNumberBucket.returnNumber; -import static org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream - .TestLessInputStreamBuilder; -import static org.apache.maven.shared.utils.cli.CommandLineUtils.executeCommandLineAsCallable; +import static org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream.TestLessInputStreamBuilder; import static org.apache.maven.shared.utils.cli.ShutdownHookUtils.addShutDownHook; import static org.apache.maven.shared.utils.cli.ShutdownHookUtils.removeShutdownHook; import static org.apache.maven.surefire.booter.SystemPropertyManager.writePropertiesFile; import static org.apache.maven.surefire.cli.CommandLineOption.SHOW_ERRORS; import static org.apache.maven.surefire.suite.RunResult.SUCCESS; -import static org.apache.maven.surefire.suite.RunResult.failure; import static org.apache.maven.surefire.suite.RunResult.timeout; +import static org.apache.maven.surefire.suite.RunResult.failure; import static org.apache.maven.surefire.util.internal.ConcurrencyUtils.countDownToZero; import static org.apache.maven.surefire.util.internal.DaemonThreadFactory.newDaemonThread; import static org.apache.maven.surefire.util.internal.DaemonThreadFactory.newDaemonThreadFactory; @@ -417,7 +424,7 @@ private RunResult runSuitesForkPerTestSet( final SurefireProperties effectiveSys int failFastCount = providerConfiguration.getSkipAfterFailureCount(); final AtomicInteger notifyStreamsToSkipTestsJustNow = new AtomicInteger( failFastCount ); final AtomicBoolean printedErrorStream = new AtomicBoolean(); - for ( final Object testSet : getSuitesIterator() ) + for ( final Class testSet : getSuitesIterator() ) { Callable pf = new Callable() { @@ -540,7 +547,7 @@ private void closeExecutor( ExecutorService executorService ) } } - private RunResult fork( Object testSet, KeyValueSource providerProperties, ForkClient forkClient, + private RunResult fork( Class testSet, KeyValueSource providerProperties, ForkClient forkClient, SurefireProperties effectiveSystemProperties, int forkNumber, AbstractForkInputStream testProvidingInputStream, boolean readTestsFromInStream ) throws SurefireBooterForkException @@ -585,13 +592,33 @@ private RunResult fork( Object testSet, KeyValueSource providerProperties, ForkC { testProvidingInputStream.setFlushReceiverProvider( cli ); } - + // setup server + ServerSocket serverSocket; + try + { + // auto-assign port randomly + serverSocket = new ServerSocket( 0 ); + } + catch ( IOException e ) + { + throw new IllegalStateException( e ); + } + // + // pass arguments to booter + // + // index-sensitive arguments cli.createArg().setValue( tempDir ); cli.createArg().setValue( DUMP_FILE_PREFIX + forkNumber ); cli.createArg().setValue( surefireProperties.getName() ); + cli.createArg().setValue( String.valueOf( serverSocket.getLocalPort() ) ); + // optional arguments if ( systPropsFile != null ) { - cli.createArg().setValue( systPropsFile.getName() ); + cli.createArg().setValue( "-props:" + systPropsFile.getName() ); + } + if ( testSet != null && !forkConfiguration.isReuseForks() && forkConfiguration.getForkCount() == 1 ) + { + cli.createArg().setValue( "-testClass:" + testSet.getName() ); } final ThreadedStreamConsumer threadedStreamConsumer = new ThreadedStreamConsumer( forkClient ); @@ -609,8 +636,8 @@ private RunResult fork( Object testSet, KeyValueSource providerProperties, ForkC new NativeStdErrStreamConsumer( forkClient.getDefaultReporterFactory() ); CommandLineCallable future = - executeCommandLineAsCallable( cli, testProvidingInputStream, threadedStreamConsumer, - stdErrConsumer, 0, closer, ISO_8859_1 ); + executeCommandLineAsCallableWithSocketWrapping( cli, testProvidingInputStream, + threadedStreamConsumer, stdErrConsumer, 0, serverSocket, closer, ISO_8859_1 ); currentForkClients.add( forkClient ); @@ -819,4 +846,126 @@ public void run() } }, 0, TIMEOUT_CHECK_PERIOD_MILLIS, MILLISECONDS ); } + + /** + * Wrapper call to allow redirection of the ForkedBooter's process's streams to the socket ICP streams.
+ * + * NOTE: A more proper approach would be allowing "CommandLineUtils.executeCommandLineAsCallable" to pass + * the desired input/output streams. + */ + public CommandLineCallable executeCommandLineAsCallableWithSocketWrapping( @Nonnull final Commandline cl, + @Nullable final InputStream systemIn, + final StreamConsumer systemOut, + final StreamConsumer systemErr, + final int timeoutInSeconds, + final ServerSocket serverSocket, + @Nullable final Runnable runAfterProcessTermination, + @Nullable final Charset streamCharset ) + throws CommandLineException + { + Commandline wrappingCommandLine = new Commandline() + { + @Override + public Process execute() throws CommandLineException + { + try + { + return new ProcessSocketHook( cl.execute(), serverSocket ); + } + catch ( IOException e ) + { + throw new IllegalStateException( "Failed to hook process for ICP" ); + } + } + }; + CommandLineCallable clc = CommandLineUtils.executeCommandLineAsCallable( wrappingCommandLine, systemIn, + systemOut, systemErr, timeoutInSeconds, runAfterProcessTermination, streamCharset ); + try + { + serverSocket.close(); + } + catch ( Exception e ) + { + // Well, we tried! + } + return clc; + } + + /** + * Process wrapper to allow redirection of the streams to a socket's streams. + */ + private static class ProcessSocketHook extends Process + { + private final Process wrapped; + private final Socket clientSocket; + + ProcessSocketHook( Process wrapped, ServerSocket serverSocket ) throws IOException + { + this.wrapped = wrapped; + this.clientSocket = serverSocket.accept(); + clientSocket.setTcpNoDelay( true ); + } + + @Override + public OutputStream getOutputStream() + { + try + { + // Hooked stream for ICP communication + return clientSocket.getOutputStream(); + } + catch ( IOException e ) + { + throw new IllegalStateException( "Failed to hook OutputStream:out" ); + } + } + + @Override + public InputStream getInputStream() + { + try + { + // Hooked stream for ICP communication + return clientSocket.getInputStream(); + } + catch ( IOException e ) + { + throw new IllegalStateException( "Failed to hook InputStream" ); + } + } + + @Override + public InputStream getErrorStream() + { + // Cannot share socket with output/input. + // However, due to the infrequently of usage this doesn't warrant conversion to sockets. + return wrapped.getErrorStream(); + } + + @Override + public int waitFor() throws InterruptedException + { + return wrapped.waitFor(); + } + + @Override + public int exitValue() + { + return wrapped.exitValue(); + } + + @Override + public void destroy() + { + wrapped.destroy(); + try + { + clientSocket.close(); + } + catch ( IOException e ) + { + throw new IllegalStateException( "Failed to close process ICP socket" ); + } + } + } } diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/CommandReader.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/CommandReader.java index 1500bbf147..66010b2921 100644 --- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/CommandReader.java +++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/CommandReader.java @@ -26,6 +26,8 @@ import java.io.DataInputStream; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Queue; @@ -62,7 +64,7 @@ public final class CommandReader { private static final String LAST_TEST_SYMBOL = ""; - private static final CommandReader READER = new CommandReader(); + private static CommandReader reader; private final Queue> listeners = new ConcurrentLinkedQueue<>(); @@ -82,13 +84,34 @@ public final class CommandReader private volatile ConsoleLogger logger = new NullConsoleLogger(); - private CommandReader() + private InputStream inputStream; + + public CommandReader( InputStream inputStream ) { + this.inputStream = inputStream; + reader = this; } + public static CommandReader getReader( Socket socket ) throws IOException + { + // initialize if needed + if ( reader == null ) + { + reader = new CommandReader( socket.getInputStream() ); + } + return getReader(); + } + + public static CommandReader getReader() { - final CommandReader reader = READER; + // initialize if needed + if ( reader == null ) + { + reader = new CommandReader( System.in ); + } + // get and start + CommandReader reader = CommandReader.reader; if ( reader.state.compareAndSet( NEW, RUNNABLE ) ) { reader.commandThread.start(); @@ -374,10 +397,10 @@ private final class CommandRunnable public void run() { CommandReader.this.startMonitor.countDown(); - DataInputStream stdIn = new DataInputStream( System.in ); boolean isTestSetFinished = false; try { + DataInputStream stdIn = new DataInputStream( inputStream ); while ( CommandReader.this.state.get() == RUNNABLE ) { Command command = decode( stdIn ); diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CmdParser.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CmdParser.java new file mode 100644 index 0000000000..5625a34ba4 --- /dev/null +++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CmdParser.java @@ -0,0 +1,110 @@ +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.util.HashMap; +import java.util.Map; + +/** + * Minimal command line parser to support optional arguments passed to the booter.
+ * + * Expected Format:
+ *
+ *     arg1 arg2 -optionalArg=value
+ * 
+ * + * @author Matt Coley + */ +public class CmdParser +{ + private static final String OPT_PREFIX = "-"; + private static final String OPT_SPLITTER = ":"; + /** + * Initial command line arguments string + */ + private final String[] args; + /** + * Map of indexed arguments. Keys are the index. + */ + private final Map argsIndexed = new HashMap(); + /** + * Map of optional arguments. Keys are the arg name. + */ + private final Map argsOptional = new HashMap(); + + public CmdParser( String[] args ) + { + this.args = args; + } + + public boolean parse() + { + int index = 0; + for ( String arg : args ) + { + // check optional format + if ( arg.startsWith( OPT_PREFIX ) ) + { + String[] parts = arg.substring( OPT_PREFIX.length() ).split( OPT_SPLITTER ); + argsOptional.put( parts[0], parts[1] ); + } + else + { + argsIndexed.put( index++, arg ); + } + } + + return true; + } + + /** + * @return Map of indexed arguments. + */ + public Map getArgsIndexed() + { + return argsIndexed; + } + + /** + * @param key Argument name. + * @return Optional argument via arg name. + */ + public String getOptionalArg( String key ) + { + if ( !argsOptional.containsKey( key ) ) + { + return null; + } + return argsOptional.get( key ); + } + + /** + * @param key Index. + * @return Argument via arg index. + */ + public String getIndexArg( int key ) + { + if ( !argsIndexed.containsKey( key ) ) + { + return null; + } + return argsIndexed.get( key ); + } +} 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 194dc88bcd..89f6dbe2be 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 @@ -34,9 +34,11 @@ import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.lang.reflect.InvocationTargetException; +import java.net.Socket; import java.security.AccessControlException; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.Arrays; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.Semaphore; @@ -70,12 +72,15 @@ public final class ForkedBooter private static final String LAST_DITCH_SHUTDOWN_THREAD = "surefire-forkedjvm-last-ditch-daemon-shutdown-thread-"; private static final String PING_THREAD = "surefire-forkedjvm-ping-"; - private final CommandReader commandReader = CommandReader.getReader(); - private final ForkedChannelEncoder eventChannel = new ForkedChannelEncoder( System.out ); + private final CommandReader commandReader; + private final ForkedChannelEncoder eventChannel; private final Semaphore exitBarrier = new Semaphore( 0 ); private volatile long systemExitTimeoutInSeconds = DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS; + private long timeoutMillis; private volatile PingScheduler pingScheduler; + private final Socket clientSocket; + private ScheduledThreadPoolExecutor jvmTerminator; private ProviderConfiguration providerConfiguration; @@ -83,8 +88,16 @@ public final class ForkedBooter private StartupConfiguration startupConfiguration; private Object testSet; + private ForkedBooter( Socket clientSocket ) throws IOException + { + this.clientSocket = clientSocket; + commandReader = CommandReader.getReader( clientSocket ); + eventChannel = new ForkedChannelEncoder( clientSocket.getOutputStream() ); + timeoutMillis = max( systemExitTimeoutInSeconds * ONE_SECOND_IN_MILLIS, ONE_SECOND_IN_MILLIS ); + } + private void setupBooter( String tmpDir, String dumpFileName, String surefirePropsFileName, - String effectiveSystemPropertiesFileName ) + String effectiveSystemPropertiesFileName, String singleTestClassName ) throws IOException { BooterDeserializer booterDeserializer = @@ -124,8 +137,15 @@ private void setupBooter( String tmpDir, String dumpFileName, String surefirePro ClassLoader classLoader = currentThread().getContextClassLoader(); classLoader.setDefaultAssertionStatus( classpathConfiguration.isEnableAssertions() ); - boolean readTestsFromCommandReader = providerConfiguration.isReadTestsFromInStream(); - testSet = createTestSet( providerConfiguration.getTestForFork(), readTestsFromCommandReader, classLoader ); + if ( singleTestClassName != null ) + { + testSet = createTestSet( singleTestClassName, classLoader ); + } + else + { + boolean readTestsFromCommandReader = providerConfiguration.isReadTestsFromInStream(); + testSet = createTestSet( providerConfiguration.getTestForFork(), readTestsFromCommandReader, classLoader ); + } } private void execute() @@ -151,6 +171,18 @@ private void execute() } } + private Object createTestSet( String singleTestClassName, ClassLoader cl ) + { + try + { + return cl.loadClass( singleTestClassName ); + } + catch ( Exception ex ) + { + throw new IllegalStateException( "Passed test class does not exist: " + singleTestClassName ); + } + } + private Object createTestSet( TypeEncodedValue forkedTestSet, boolean readTestsFromCommandReader, ClassLoader cl ) { if ( forkedTestSet != null ) @@ -355,10 +387,17 @@ public void update( Command command ) ); eventChannel.bye(); launchLastDitchDaemonShutdownThread( 0 ); - long timeoutMillis = max( systemExitTimeoutInSeconds * ONE_SECOND_IN_MILLIS, ONE_SECOND_IN_MILLIS ); acquireOnePermit( exitBarrier, timeoutMillis ); cancelPingScheduler(); commandReader.stop(); + try + { + clientSocket.close(); + } + catch ( Exception e ) + { + + } System.exit( 0 ); } @@ -438,29 +477,40 @@ private SurefireProvider createProviderInCurrentClassloader( ForkingReporterFact */ public static void main( String[] args ) { - ForkedBooter booter = new ForkedBooter(); - run( booter, args ); - } - - /** - * created for testing purposes. - * - * @param booter booter in JVM - * @param args arguments passed to JVM - */ - private static void run( ForkedBooter booter, String[] args ) - { + CmdParser parser = new CmdParser( args ); + if ( !parser.parse() ) + { + throw new IllegalStateException( "Invalid arguments given: " + Arrays.toString( args ) ); + } + String tmpDir = parser.getIndexArg( 0 ); + String dumpFileName = parser.getIndexArg( 1 ); + String surefirePropsName = parser.getIndexArg( 2 ); + int forkStarterPort = Integer.parseInt( parser.getIndexArg( 3 ) ); + String effectiveSystemPropertiesFile = parser.getOptionalArg( "props" ); + String singleTestClassName = parser.getOptionalArg( "testClass" ); try { - booter.setupBooter( args[0], args[1], args[2], args.length > 3 ? args[3] : null ); - booter.execute(); + Socket clientSocket = new Socket( "127.0.0.1", forkStarterPort ); + clientSocket.setTcpNoDelay( true ); + ForkedBooter booter = new ForkedBooter( clientSocket ); + try + { + booter.setupBooter( tmpDir, dumpFileName, surefirePropsName, + effectiveSystemPropertiesFile, singleTestClassName ); + booter.execute(); + } + catch ( Throwable t ) + { + DumpErrorSingleton.getSingleton().dumpException( t ); + t.printStackTrace(); + booter.cancelPingScheduler(); + booter.exit1(); + } } - catch ( Throwable t ) + catch ( IOException e ) { - DumpErrorSingleton.getSingleton().dumpException( t ); - t.printStackTrace(); - booter.cancelPingScheduler(); - booter.exit1(); + DumpErrorSingleton.getSingleton().dumpException( e ); + e.printStackTrace(); } }