blob: c1140c861f8e9d4c1b74b34a1ae8e6b83aa02ab0 [file] [log] [blame]
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.api.booter.MasterProcessChannelDecoder;
import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
import org.apache.maven.surefire.api.report.StackTraceWriter;
import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
import org.apache.maven.surefire.booter.spi.AbstractMasterProcessChannelProcessorFactory;
import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelDecoder;
import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder;
import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelProcessorFactory;
import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
import org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils;
import org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory;
import org.junit.Rule;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.junit.rules.ErrorCollector;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.maven.surefire.api.util.internal.Channels.newBufferedChannel;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.powermock.api.mockito.PowerMockito.doAnswer;
import static org.powermock.api.mockito.PowerMockito.doCallRealMethod;
import static org.powermock.api.mockito.PowerMockito.doNothing;
import static org.powermock.api.mockito.PowerMockito.doThrow;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.verifyNoMoreInteractions;
import static org.powermock.api.mockito.PowerMockito.verifyPrivate;
import static org.powermock.api.mockito.PowerMockito.when;
import static org.powermock.reflect.Whitebox.invokeMethod;
import static org.powermock.reflect.Whitebox.setInternalState;
/**
* PowerMock tests for {@link ForkedBooter}.
*/
@RunWith( PowerMockRunner.class )
@PrepareForTest( {
PpidChecker.class,
ForkedBooter.class,
LegacyMasterProcessChannelEncoder.class,
ShutdownHookUtils.class
} )
@PowerMockIgnore( { "org.jacoco.agent.rt.*", "com.vladium.emma.rt.*" } )
public class ForkedBooterMockTest
{
@Rule
public final ErrorCollector errorCollector = new ErrorCollector();
@Mock
private PpidChecker pluginProcessChecker;
@Mock
private ForkedBooter booter;
@Mock
private MasterProcessChannelProcessorFactory channelProcessorFactory;
@Mock
private LegacyMasterProcessChannelEncoder eventChannel;
@Captor
private ArgumentCaptor<StackTraceWriter> capturedStackTraceWriter;
@Captor
private ArgumentCaptor<Boolean> capturedBoolean;
@Captor
private ArgumentCaptor<String[]> capturedArgs;
@Captor
private ArgumentCaptor<ForkedBooter> capturedBooter;
@Test
public void shouldCheckNewPingMechanism() throws Exception
{
boolean canUse = invokeMethod( ForkedBooter.class, "canUseNewPingMechanism", (PpidChecker) null );
assertThat( canUse ).isFalse();
when( pluginProcessChecker.canUse() ).thenReturn( false );
canUse = invokeMethod( ForkedBooter.class, "canUseNewPingMechanism", pluginProcessChecker );
assertThat( canUse ).isFalse();
when( pluginProcessChecker.canUse() ).thenReturn( true );
canUse = invokeMethod( ForkedBooter.class, "canUseNewPingMechanism", pluginProcessChecker );
assertThat( canUse ).isTrue();
}
@Test
public void testMain() throws Exception
{
mockStatic( ForkedBooter.class );
doCallRealMethod()
.when( ForkedBooter.class, "run", capturedBooter.capture(), capturedArgs.capture() );
String[] args = new String[]{ "/", "dump", "surefire.properties", "surefire-effective.properties" };
invokeMethod( ForkedBooter.class, "run", booter, args );
assertThat( capturedBooter.getAllValues() )
.hasSize( 1 )
.contains( booter );
assertThat( capturedArgs.getAllValues() )
.hasSize( 1 );
assertThat( capturedArgs.getAllValues().get( 0 )[0] )
.isEqualTo( "/" );
assertThat( capturedArgs.getAllValues().get( 0 )[1] )
.isEqualTo( "dump" );
assertThat( capturedArgs.getAllValues().get( 0 )[2] )
.isEqualTo( "surefire.properties" );
assertThat( capturedArgs.getAllValues().get( 0 )[3] )
.isEqualTo( "surefire-effective.properties" );
verifyPrivate( booter, times( 1 ) )
.invoke( "setupBooter", same( args[0] ), same( args[1] ), same( args[2] ), same( args[3] ) );
verifyPrivate( booter, times( 1 ) )
.invoke( "execute" );
verifyNoMoreInteractions( booter );
}
@Test
public void testMainWithError() throws Exception
{
mockStatic( ForkedBooter.class );
doCallRealMethod()
.when( ForkedBooter.class, "run", any( ForkedBooter.class ), any( String[].class ) );
doThrow( new RuntimeException( "dummy exception" ) )
.when( booter, "execute" );
doNothing()
.when( booter, "setupBooter",
any( String.class ), any( String.class ), any( String.class ), any( String.class ) );
setInternalState( booter, "eventChannel", eventChannel );
String[] args = new String[]{ "/", "dump", "surefire.properties", "surefire-effective.properties" };
invokeMethod( ForkedBooter.class, "run", booter, args );
verifyPrivate( booter, times( 1 ) )
.invoke( "setupBooter", same( args[0] ), same( args[1] ), same( args[2] ), same( args[3] ) );
verifyPrivate( booter, times( 1 ) )
.invoke( "execute" );
verify( eventChannel, times( 1 ) )
.consoleErrorLog( capturedStackTraceWriter.capture(), capturedBoolean.capture() );
assertThat( capturedStackTraceWriter.getValue() )
.isNotNull();
assertThat( capturedStackTraceWriter.getValue().smartTrimmedStackTrace() )
.isEqualTo( "test subsystem#no method RuntimeException dummy exception" );
assertThat( capturedStackTraceWriter.getValue().getThrowable().getTarget() )
.isNotNull()
.isInstanceOf( RuntimeException.class );
assertThat( capturedStackTraceWriter.getValue().getThrowable().getTarget().getMessage() )
.isEqualTo( "dummy exception" );
verifyPrivate( booter, times( 1 ) )
.invoke( "cancelPingScheduler" );
verifyPrivate( booter, times( 1 ) )
.invoke( "exit1" );
verifyNoMoreInteractions( booter );
}
@Test
public void shouldNotCloseChannelProcessorFactory() throws Exception
{
setInternalState( booter, "channelProcessorFactory", (MasterProcessChannelProcessorFactory) null );
doCallRealMethod()
.when( booter, "closeForkChannel" );
invokeMethod( booter, "closeForkChannel" );
verifyZeroInteractions( channelProcessorFactory );
}
@Test
public void shouldCloseChannelProcessorFactory() throws Exception
{
setInternalState( booter, "channelProcessorFactory", channelProcessorFactory );
doCallRealMethod()
.when( booter, "closeForkChannel" );
invokeMethod( booter, "closeForkChannel" );
verify( channelProcessorFactory, times( 1 ) )
.close();
verifyNoMoreInteractions( channelProcessorFactory );
}
@Test
public void shouldFailOnCloseChannelProcessorFactory() throws Exception
{
setInternalState( booter, "channelProcessorFactory", channelProcessorFactory );
doThrow( new IOException() )
.when( channelProcessorFactory )
.close();
doCallRealMethod()
.when( booter, "closeForkChannel" );
invokeMethod( booter, "closeForkChannel" );
verify( channelProcessorFactory, times( 1 ) )
.close();
verifyNoMoreInteractions( channelProcessorFactory );
}
@Test
public void shouldLookupLegacyDecoderFactory() throws Exception
{
mockStatic( ForkedBooter.class );
doCallRealMethod()
.when( ForkedBooter.class, "lookupDecoderFactory", anyString() );
try ( final MasterProcessChannelProcessorFactory factory =
invokeMethod( ForkedBooter.class, "lookupDecoderFactory", "pipe://3" ) )
{
assertThat( factory ).isInstanceOf( LegacyMasterProcessChannelProcessorFactory.class );
assertThat( factory.canUse( "pipe://3" ) ).isTrue();
assertThat( factory.canUse( "-- whatever --" ) ).isFalse();
errorCollector.checkThrows( MalformedURLException.class, new ThrowingRunnable()
{
@Override
public void run() throws Throwable
{
factory.connect( "tcp://localhost:123" );
fail();
}
} );
factory.connect( "pipe://3" );
MasterProcessChannelDecoder decoder = factory.createDecoder();
assertThat( decoder ).isInstanceOf( LegacyMasterProcessChannelDecoder.class );
MasterProcessChannelEncoder encoder = factory.createEncoder();
assertThat( encoder ).isInstanceOf( LegacyMasterProcessChannelEncoder.class );
}
}
@Test
@SuppressWarnings( "checkstyle:magicnumber" )
public void shouldScheduleFlushes() throws Exception
{
final ByteArrayOutputStream out = new ByteArrayOutputStream();
class Factory extends AbstractMasterProcessChannelProcessorFactory
{
@Override
public boolean canUse( String channelConfig )
{
return false;
}
@Override
public void connect( String channelConfig )
{
}
@Override
public MasterProcessChannelDecoder createDecoder()
{
return null;
}
@Override
public MasterProcessChannelEncoder createEncoder()
{
return null;
}
public void runScheduler() throws InterruptedException
{
final WritableBufferedByteChannel channel = newBufferedChannel( out );
schedulePeriodicFlusher( 100, channel );
Thread t = new Thread()
{
@Override
public void run()
{
for ( int i = 0; i < 10; i++ )
{
try
{
channel.write( ByteBuffer.wrap( new byte[] {1} ) );
Thread.sleep( 75 );
}
catch ( Exception e )
{
//
}
}
}
};
t.setDaemon( true );
t.start();
t.join( 5000L );
}
}
Factory factory = new Factory();
factory.runScheduler();
factory.close();
assertThat( out.size() ).isPositive();
assertThat( out.size() ).isLessThanOrEqualTo( 10 );
}
@Test
public void shouldLookupSurefireDecoderFactory() throws Exception
{
mockStatic( ForkedBooter.class );
doCallRealMethod()
.when( ForkedBooter.class, "lookupDecoderFactory", anyString() );
try ( ServerSocketChannel server = ServerSocketChannel.open() )
{
server.bind( new InetSocketAddress( 0 ) );
int serverPort = ( (InetSocketAddress) server.getLocalAddress() ).getPort();
try ( MasterProcessChannelProcessorFactory factory =
invokeMethod( ForkedBooter.class, "lookupDecoderFactory", "tcp://localhost:" + serverPort ) )
{
assertThat( factory )
.isInstanceOf( SurefireMasterProcessChannelProcessorFactory.class );
assertThat( factory.canUse( "tcp://localhost:" + serverPort ) )
.isTrue();
assertThat( factory.canUse( "-- whatever --" ) )
.isFalse();
errorCollector.checkThrows( MalformedURLException.class, new ThrowingRunnable()
{
@Override
public void run() throws Throwable
{
factory.connect( "pipe://1" );
fail();
}
} );
errorCollector.checkThrows( IOException.class, new ThrowingRunnable()
{
@Override
public void run() throws Throwable
{
factory.connect( "tcp://localhost:123\u0000\u0000\u0000" );
fail();
}
} );
factory.connect( "tcp://localhost:" + serverPort );
MasterProcessChannelDecoder decoder = factory.createDecoder();
assertThat( decoder )
.isInstanceOf( LegacyMasterProcessChannelDecoder.class );
MasterProcessChannelEncoder encoder = factory.createEncoder();
assertThat( encoder )
.isInstanceOf( LegacyMasterProcessChannelEncoder.class );
}
}
}
@Test
public void shouldAuthenticate() throws Exception
{
mockStatic( ForkedBooter.class );
doCallRealMethod()
.when( ForkedBooter.class, "lookupDecoderFactory", anyString() );
try ( final ServerSocketChannel server = ServerSocketChannel.open() )
{
server.bind( new InetSocketAddress( 0 ) );
int serverPort = ( (InetSocketAddress) server.getLocalAddress() ).getPort();
final String uuid = UUID.randomUUID().toString();
String url = "tcp://localhost:" + serverPort + "?sessionId=" + uuid;
try ( final MasterProcessChannelProcessorFactory factory =
invokeMethod( ForkedBooter.class, "lookupDecoderFactory", url ) )
{
assertThat( factory )
.isInstanceOf( SurefireMasterProcessChannelProcessorFactory.class );
FutureTask<Boolean> task = new FutureTask<>( new Callable<Boolean>()
{
@Override
public Boolean call()
{
try
{
SocketChannel channel = server.accept();
ByteBuffer bb = ByteBuffer.allocate( uuid.length() );
int read = channel.read( bb );
assertThat( read )
.isEqualTo( uuid.length() );
bb.flip();
assertThat( new String( bb.array(), US_ASCII ) )
.isEqualTo( uuid );
return true;
}
catch ( IOException e )
{
return false;
}
}
} );
Thread t = new Thread( task );
t.setDaemon( true );
t.start();
factory.connect( url );
try
{
task.get( 10, SECONDS );
}
finally
{
factory.close();
}
}
}
}
@Test
public void testFlushEventChannelOnExit() throws Exception
{
mockStatic( ShutdownHookUtils.class );
final MasterProcessChannelEncoder eventChannel = mock( MasterProcessChannelEncoder.class );
ForkedBooter booter = new ForkedBooter();
setInternalState( booter, "eventChannel", eventChannel );
doAnswer( new Answer<Object>()
{
@Override
public Object answer( InvocationOnMock invocation )
{
Thread t = invocation.getArgument( 0 );
assertThat( t.isDaemon() ).isTrue();
t.run();
verify( eventChannel, times( 1 ) ).onJvmExit();
return null;
}
} ).when( ShutdownHookUtils.class, "addShutDownHook", any( Thread.class ) );
invokeMethod( booter, "flushEventChannelOnExit" );
}
@Test
public void shouldParseUUID() throws Exception
{
UUID uuid = UUID.randomUUID();
URI uri = new URI( "tcp://localhost:12345?sessionId=" + uuid );
String parsed = invokeMethod( SurefireMasterProcessChannelProcessorFactory.class, "extractSessionId", uri );
assertThat( parsed )
.isEqualTo( uuid.toString() );
}
@Test
public void shouldNotParseUUID() throws Exception
{
UUID uuid = UUID.randomUUID();
URI uri = new URI( "tcp://localhost:12345?xxx=" + uuid );
String parsed = invokeMethod( SurefireMasterProcessChannelProcessorFactory.class, "extractSessionId", uri );
assertThat( parsed )
.isNull();
}
}