blob: 2cec298222097ab268be45d486111da12e8e7685 [file] [log] [blame]
package org.apache.maven.surefire.api.stream;
/*
* 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 javax.annotation.Nonnull;
import java.io.EOFException;
import java.io.File;
import java.math.BigInteger;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.CharsetDecoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
import org.apache.maven.surefire.api.booter.Constants;
import org.apache.maven.surefire.api.booter.ForkedProcessEventType;
import org.apache.maven.surefire.api.event.Event;
import org.apache.maven.surefire.api.fork.ForkNodeArguments;
import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.MalformedFrameException;
import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.Memento;
import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.Segment;
import org.junit.BeforeClass;
import org.junit.Test;
import static java.lang.Math.min;
import static java.lang.System.arraycopy;
import static java.nio.charset.CodingErrorAction.REPLACE;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonMap;
import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDOUT;
import static org.apache.maven.surefire.api.stream.SegmentType.END_OF_FRAME;
import static org.assertj.core.api.Assertions.assertThat;
import static org.powermock.reflect.Whitebox.invokeMethod;
/**
* The performance of "get( Integer )" is 13.5 nano seconds on i5/2.6GHz:
* <pre>
* {@code
* TreeMap<Integer, ForkedProcessEventType> map = new TreeMap<>();
* map.get( hash );
* }
* </pre>
*
* <br> The performance of getting event type by Segment is 33.7 nano seconds:
* <pre>
* {@code
* Map<Segment, ForkedProcessEventType> map = new HashMap<>();
* byte[] array = ForkedProcessEventType.BOOTERCODE_STDOUT.getOpcode().getBytes( UTF_8 );
* map.get( new Segment( array, 0, array.length ) );
* }
* </pre>
*
* <br> The performance of decoder:
* <pre>
* {@code
* CharsetDecoder decoder = STREAM_ENCODING.newDecoder()
* .onMalformedInput( REPLACE )
* .onUnmappableCharacter( REPLACE );
* ByteBuffer buffer = ByteBuffer.wrap( ForkedProcessEventType.BOOTERCODE_STDOUT.getOpcode().getBytes( UTF_8 ) );
* CharBuffer chars = CharBuffer.allocate( 100 );
* decoder.reset().decode( buffer, chars, true );
*
* String s = chars.flip().toString(); // 37 nanos = CharsetDecoder + toString
*
* buffer.clear();
* chars.clear();
*
* ForkedProcessEventType.byOpcode( s ); // 65 nanos = CharsetDecoder + toString + byOpcode
* }
* </pre>
*
* <br> The performance of decoding 100 bytes via CharacterDecoder - 71 nano seconds:
* <pre>
* {@code
* decoder.reset()
* .decode( buffer, chars, true ); // CharsetDecoder 71 nanos
* chars.flip().toString(); // CharsetDecoder + toString = 91 nanos
* }
* </pre>
*
* <br> The performance of a pure string creation (instead of decoder) - 31.5 nano seconds:
* <pre>
* {@code
* byte[] b = {};
* new String( b, UTF_8 );
* }
* </pre>
*
* <br> The performance of CharsetDecoder with empty ByteBuffer:
* <pre>
* {@code
* CharsetDecoder + ByteBuffer.allocate( 0 ) makes 11.5 nanos
* CharsetDecoder + ByteBuffer.allocate( 0 ) + toString() makes 16.1 nanos
* }
* </pre>
*/
@SuppressWarnings( "checkstyle:magicnumber" )
public class AbstractStreamDecoderTest
{
private static final Map<Segment, ForkedProcessEventType> EVENTS = new HashMap<>();
private static final String PATTERN1 =
"0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
private static final String PATTERN2 = "€ab©c";
private static final byte[] PATTERN2_BYTES =
new byte[] {(byte) -30, (byte) -126, (byte) -84, 'a', 'b', (byte) 0xc2, (byte) 0xa9, 'c'};
@BeforeClass
public static void setup()
{
for ( ForkedProcessEventType event : ForkedProcessEventType.values() )
{
byte[] array = event.getOpcodeBinary();
EVENTS.put( new Segment( array, 0, array.length ), event );
}
}
@Test
public void shouldDecodeHappyCase() throws Exception
{
CharsetDecoder decoder = UTF_8.newDecoder().onMalformedInput( REPLACE ).onUnmappableCharacter( REPLACE );
ByteBuffer input = ByteBuffer.allocate( 1024 );
( (Buffer) input.put( PATTERN2_BYTES ) ).flip();
int bytesToDecode = PATTERN2_BYTES.length;
Buffer output = CharBuffer.allocate( 1024 );
int readBytes = invokeMethod( AbstractStreamDecoder.class, "decodeString", decoder, input, output,
bytesToDecode, true, 0 );
assertThat( readBytes )
.isEqualTo( bytesToDecode );
assertThat( output.flip().toString() )
.isEqualTo( PATTERN2 );
}
@Test
public void shouldDecodeShifted() throws Exception
{
CharsetDecoder decoder = UTF_8.newDecoder().onMalformedInput( REPLACE ).onUnmappableCharacter( REPLACE );
ByteBuffer input = ByteBuffer.allocate( 1024 );
( (Buffer) input.put( PATTERN1.getBytes( UTF_8 ) )
.put( 90, (byte) 'A' )
.put( 91, (byte) 'B' )
.put( 92, (byte) 'C' ) )
.position( 90 );
Buffer output = CharBuffer.allocate( 1024 );
int readBytes =
invokeMethod( AbstractStreamDecoder.class, "decodeString", decoder, input, output, 2, true, 0 );
assertThat( readBytes ).isEqualTo( 2 );
assertThat( output.flip().toString() )
.isEqualTo( "AB" );
}
@Test( expected = IllegalArgumentException.class )
public void shouldNotDecode() throws Exception
{
CharsetDecoder decoder = UTF_8.newDecoder();
ByteBuffer input = ByteBuffer.allocate( 100 );
int bytesToDecode = 101;
CharBuffer output = CharBuffer.allocate( 1000 );
invokeMethod( AbstractStreamDecoder.class, "decodeString", decoder, input, output, bytesToDecode, true, 0 );
}
@Test
public void shouldReadInt() throws Exception
{
Channel channel = new Channel( new byte[] {0x01, 0x02, 0x03, 0x04, ':'}, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
assertThat( thread.readInt( memento ) )
.isEqualTo( new BigInteger( new byte[] {0x01, 0x02, 0x03, 0x04} ).intValue() );
}
@Test
public void shouldReadInteger() throws Exception
{
Channel channel = new Channel( new byte[] {(byte) 0xff, 0x01, 0x02, 0x03, 0x04, ':'}, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
assertThat( thread.readInteger( memento ) )
.isEqualTo( new BigInteger( new byte[] {0x01, 0x02, 0x03, 0x04} ).intValue() );
}
@Test
public void shouldReadNullInteger() throws Exception
{
Channel channel = new Channel( new byte[] {(byte) 0x00, ':'}, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
assertThat( thread.readInteger( memento ) )
.isNull();
}
@Test( expected = EOFException.class )
public void shouldNotReadString() throws Exception
{
Channel channel = new Channel( PATTERN1.getBytes(), PATTERN1.length() );
channel.read( ByteBuffer.allocate( 100 ) );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
invokeMethod( thread, "readString", memento, 10 );
}
@Test
public void shouldReadString() throws Exception
{
Channel channel = new Channel( PATTERN1.getBytes(), PATTERN1.length() );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
String s = invokeMethod( thread, "readString", memento, 10 );
assertThat( s )
.isEqualTo( "0123456789" );
}
@Test
public void shouldReadStringShiftedBuffer() throws Exception
{
StringBuilder s = new StringBuilder( 1100 );
for ( int i = 0; i < 11; i++ )
{
s.append( PATTERN1 );
}
Channel channel = new Channel( s.toString().getBytes( UTF_8 ), s.length() );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
// whatever position will be compacted to 0
( (Buffer) ( (Buffer) memento.getByteBuffer() ).limit( 974 ) ).position( 974 );
assertThat( (String) invokeMethod( thread, "readString", memento, PATTERN1.length() + 3 ) )
.isEqualTo( PATTERN1 + "012" );
}
@Test
public void shouldReadStringShiftedInput() throws Exception
{
StringBuilder s = new StringBuilder( 1100 );
for ( int i = 0; i < 11; i++ )
{
s.append( PATTERN1 );
}
Channel channel = new Channel( s.toString().getBytes( UTF_8 ), s.length() );
channel.read( ByteBuffer.allocate( 997 ) );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
assertThat( (String) invokeMethod( thread, "readString", memento, PATTERN1.length() ) )
.isEqualTo( "789" + PATTERN1.substring( 0, 97 ) );
}
@Test
public void shouldReadMultipleStringsAndShiftedInput() throws Exception
{
StringBuilder s = new StringBuilder( 5000 );
for ( int i = 0; i < 50; i++ )
{
s.append( PATTERN1 );
}
Channel channel = new Channel( s.toString().getBytes( UTF_8 ), s.length() );
channel.read( ByteBuffer.allocate( 1997 ) );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
// whatever position will be compacted to 0
( (Buffer) memento.getByteBuffer() ).limit( 974 ).position( 974 );
StringBuilder expected = new StringBuilder( "789" );
for ( int i = 0; i < 11; i++ )
{
expected.append( PATTERN1 );
}
expected.setLength( 1100 );
assertThat( (String) invokeMethod( thread, "readString", memento, 1100 ) )
.isEqualTo( expected.toString() );
}
@Test
public void shouldDecode3BytesEncodedSymbol() throws Exception
{
byte[] encodedSymbol = new byte[] {(byte) -30, (byte) -126, (byte) -84};
int countSymbols = 1024;
byte[] input = new byte[encodedSymbol.length * countSymbols];
for ( int i = 0; i < countSymbols; i++ )
{
arraycopy( encodedSymbol, 0, input, encodedSymbol.length * i, encodedSymbol.length );
}
Channel channel = new Channel( input, 64 * 1024 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
String decodedOutput = invokeMethod( thread, "readString", memento, input.length );
assertThat( decodedOutput )
.isEqualTo( new String( input, 0, input.length, UTF_8 ) );
}
@Test
public void shouldDecode100Bytes() throws Exception
{
CharsetDecoder decoder = DEFAULT_STREAM_ENCODING.newDecoder()
.onMalformedInput( REPLACE )
.onUnmappableCharacter( REPLACE );
// empty stream: CharsetDecoder + ByteBuffer.allocate( 0 ) makes 11.5 nanos
// empty stream: CharsetDecoder + ByteBuffer.allocate( 0 ) + toString() makes 16.1 nanos
ByteBuffer buffer = ByteBuffer.wrap( PATTERN1.getBytes( UTF_8 ) );
CharBuffer chars = CharBuffer.allocate( 100 );
// uncomment this section for a proper measurement of the exec time
TimeUnit.SECONDS.sleep( 2 );
System.gc();
TimeUnit.SECONDS.sleep( 5 );
String s = null;
long l1 = System.currentTimeMillis();
for ( int i = 0; i < 10_000_000; i++ )
{
decoder.reset()
.decode( buffer, chars, true ); // CharsetDecoder 71 nanos
s = ( (Buffer) chars ).flip().toString(); // CharsetDecoder + toString = 91 nanos
( (Buffer) buffer ).clear();
( (Buffer) chars ).clear();
}
long l2 = System.currentTimeMillis();
System.out.println( "decoded 100 bytes within " + ( l2 - l1 ) + " millis (10 million cycles)" );
assertThat( s )
.isEqualTo( PATTERN1 );
}
@Test
public void shouldReadEventType() throws Exception
{
byte[] array = BOOTERCODE_STDOUT.getOpcodeBinary();
Map<Segment, ForkedProcessEventType> messageType =
singletonMap( new Segment( array, 0, array.length ), BOOTERCODE_STDOUT );
byte[] stream = ":maven-surefire-event:\u000E:std-out-stream:".getBytes( UTF_8 );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(), messageType );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
ForkedProcessEventType eventType = thread.readMessageType( memento );
assertThat( eventType )
.isEqualTo( BOOTERCODE_STDOUT );
}
@Test( expected = EOFException.class )
public void shouldEventTypeReachedEndOfStream() throws Exception
{
byte[] stream = ":maven-surefire-event:\u000E:xxx".getBytes( UTF_8 );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(), EVENTS );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
thread.readMessageType( memento );
}
@Test( expected = MalformedFrameException.class )
public void shouldEventTypeReachedMalformedHeader() throws Exception
{
byte[] stream = ":xxxxx-xxxxxxxx-xxxxx:\u000E:xxx".getBytes( UTF_8 );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
thread.readMessageType( memento );
}
@Test
public void shouldReadEmptyString() throws Exception
{
byte[] stream = "\u0000\u0000\u0000\u0000::".getBytes( UTF_8 );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
assertThat( thread.readString( memento ) )
.isEmpty();
}
@Test
public void shouldReadNullString() throws Exception
{
byte[] stream = "\u0000\u0000\u0000\u0001:\u0000:".getBytes( UTF_8 );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
assertThat( thread.readString( memento ) )
.isNull();
}
@Test
public void shouldReadSingleCharString() throws Exception
{
byte[] stream = "\u0000\u0000\u0000\u0001:A:".getBytes( UTF_8 );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
assertThat( thread.readString( memento ) )
.isEqualTo( "A" );
}
@Test
public void shouldReadThreeCharactersString() throws Exception
{
byte[] stream = "\u0000\u0000\u0000\u0003:ABC:".getBytes( UTF_8 );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
assertThat( thread.readString( memento ) )
.isEqualTo( "ABC" );
}
@Test
public void shouldReadDefaultCharset() throws Exception
{
byte[] stream = "\u0005:UTF-8:".getBytes( US_ASCII );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
assertThat( thread.readCharset( memento ) )
.isNotNull()
.isEqualTo( UTF_8 );
}
@Test
public void shouldReadNonDefaultCharset() throws Exception
{
byte[] stream = ( (char) 10 + ":ISO_8859_1:" ).getBytes( US_ASCII );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
assertThat( thread.readCharset( memento ) )
.isNotNull()
.isEqualTo( ISO_8859_1 );
}
@Test
public void shouldSetNonDefaultCharset()
{
byte[] stream = {};
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
memento.setCharset( ISO_8859_1 );
assertThat( memento.getDecoder().charset() ).isEqualTo( ISO_8859_1 );
memento.setCharset( UTF_8 );
assertThat( memento.getDecoder().charset() ).isEqualTo( UTF_8 );
memento.reset();
assertThat( memento.getDecoder() ).isNotNull();
assertThat( memento.getDecoder().charset() ).isEqualTo( UTF_8 );
}
@Test( expected = MalformedFrameException.class )
public void malformedCharset() throws Exception
{
byte[] stream = ( (char) 8 + ":ISO_8859:" ).getBytes( US_ASCII );
Channel channel = new Channel( stream, 1 );
Mock thread = new Mock( channel, new MockForkNodeArguments(),
Collections.<Segment, ForkedProcessEventType>emptyMap() );
Memento memento = thread.new Memento();
memento.setCharset( UTF_8 );
thread.readCharset( memento );
}
private static class Channel implements ReadableByteChannel
{
private final byte[] bytes;
private final int chunkSize;
protected int i;
Channel( byte[] bytes, int chunkSize )
{
this.bytes = bytes;
this.chunkSize = chunkSize;
}
@Override
public int read( ByteBuffer dst )
{
if ( i == bytes.length )
{
return -1;
}
else if ( dst.hasRemaining() )
{
int length = min( min( chunkSize, bytes.length - i ), dst.remaining() ) ;
dst.put( bytes, i, length );
i += length;
return length;
}
else
{
return 0;
}
}
@Override
public boolean isOpen()
{
return false;
}
@Override
public void close()
{
}
}
private static class MockForkNodeArguments implements ForkNodeArguments
{
@Nonnull
@Override
public String getSessionId()
{
return null;
}
@Override
public int getForkChannelId()
{
return 0;
}
@Nonnull
@Override
public File dumpStreamText( @Nonnull String text )
{
return null;
}
@Nonnull
@Override
public File dumpStreamException( @Nonnull Throwable t )
{
return null;
}
@Override
public void logWarningAtEnd( @Nonnull String text )
{
}
@Nonnull
@Override
public ConsoleLogger getConsoleLogger()
{
return null;
}
@Nonnull
@Override
public Object getConsoleLock()
{
return new Object();
}
@Override
public File getEventStreamBinaryFile()
{
return null;
}
@Override
public File getCommandStreamBinaryFile()
{
return null;
}
}
private static class Mock extends AbstractStreamDecoder<Event, ForkedProcessEventType, SegmentType>
{
protected Mock( @Nonnull ReadableByteChannel channel, @Nonnull ForkNodeArguments arguments,
@Nonnull Map<Segment, ForkedProcessEventType> messageTypes )
{
super( channel, arguments, messageTypes );
}
@Override
public Event decode() throws MalformedChannelException
{
throw new MalformedChannelException();
}
@Nonnull
@Override
protected byte[] getEncodedMagicNumber()
{
return Constants.MAGIC_NUMBER_FOR_EVENTS_BYTES;
}
@Nonnull
@Override
protected SegmentType[] nextSegmentType( @Nonnull ForkedProcessEventType messageType )
{
return new SegmentType[] {END_OF_FRAME};
}
@Nonnull
@Override
protected Event toMessage( @Nonnull ForkedProcessEventType messageType, @Nonnull Memento memento )
{
return null;
}
@Override
public void close() throws Exception
{
}
}
}