@ -18,7 +18,6 @@ package org.springframework.core.codec;
@@ -18,7 +18,6 @@ package org.springframework.core.codec;
import java.util.Map ;
import java.util.function.Consumer ;
import java.util.stream.Stream ;
import org.junit.Test ;
import org.reactivestreams.Publisher ;
@ -28,7 +27,6 @@ import reactor.test.StepVerifier;
@@ -28,7 +27,6 @@ import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType ;
import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase ;
import org.springframework.core.io.buffer.DataBuffer ;
import org.springframework.core.io.buffer.DataBufferFactory ;
import org.springframework.core.io.buffer.DataBufferUtils ;
import org.springframework.lang.Nullable ;
import org.springframework.util.Assert ;
@ -36,193 +34,245 @@ import org.springframework.util.MimeType;
@@ -36,193 +34,245 @@ import org.springframework.util.MimeType;
import static java.nio.charset.StandardCharsets.UTF_8 ;
import static org.junit.Assert.* ;
import static org.springframework.core.io.buffer.DataBufferUtils.release ;
/ * *
* Abstract base class for { @link Encoder } unit tests . Subclasses need to implement
* { @link # input ( ) } and { @link # outputConsumers ( ) } , from which { @link # encode ( ) } ,
* { @link # encodeError ( ) } and { @link # encodeCancel ( ) } are run .
* { @link # canEncode ( ) } and { @link # encode ( ) } , possibly using the wide
* * variety of helper methods like { @link # testEncodeAll } .
*
* @author Arjen Poutsma
* @since 5 . 1 . 3
* /
@SuppressWarnings ( "ProtectedField" )
public abstract class AbstractEncoderTestCase < T , E extends Encoder < T > > extends
AbstractLeakCheckingTestCase {
public abstract class AbstractEncoderTestCase < E extends Encoder < ? > >
extends AbstractLeakCheckingTestCase {
/ * *
* The encoder to test .
* /
protected final E encoder ;
/ * *
* The type used for
* { @link Encoder # encode ( Publisher , DataBufferFactory , ResolvableType , MimeType , Map ) } .
* Construct a new { @code AbstractEncoderTestCase } for the given parameters .
* @param encoder the encoder
* /
protected final ResolvableType elementType ;
protected AbstractEncoderTestCase ( E encoder ) {
Assert . notNull ( encoder , "Encoder must not be null" ) ;
this . encoder = encoder ;
}
/ * *
* The mime type used for
* { @link Encoder # encode ( Publisher , DataBufferFactory , ResolvableType , MimeType , Map ) } .
* May be { @code null } .
* Subclasses should implement this method to test { @link Encoder # canEncode } .
* /
@Nullable
protected final MimeType mimeType ;
@Test
public abstract void canEncode ( ) throws Exception ;
/ * *
* The hints used for
* { @link Encoder # encode ( Publisher , DataBufferFactory , ResolvableType , MimeType , Map ) } .
* May be { @code null } .
* Subclasses should implement this method to test { @link Encoder # encode } , possibly using
* { @link # testEncodeAll } or other helper methods .
* /
@Nullable
protected final Map < String , Object > hints ;
@Test
public abstract void encode ( ) throws Exception ;
/ * *
* Construct a new { @code AbstractEncoderTestCase } for the given encoder and element class .
* @param encoder the encoder
* @param elementClass the element class
* Helper methods that tests for a variety of encoding scenarios . This methods
* invokes :
* < ul >
* < li > { @link # testEncode ( Publisher , ResolvableType , Consumer , MimeType , Map ) } < / li >
* < li > { @link # testEncodeError ( Publisher , ResolvableType , MimeType , Map ) } < / li >
* < li > { @link # testEncodeCancel ( Publisher , ResolvableType , MimeType , Map ) } < / li >
* < li > { @link # testEncodeEmpty ( ResolvableType , MimeType , Map ) } < / li >
* < / ul >
*
* @param input the input to be provided to the encoder
* @param inputClass the input class
* @param stepConsumer a consumer to { @linkplain StepVerifier verify } the output
* @param < T > the output type
* /
protected AbstractEncoderTestCase ( E encoder , Class < ? > elementClass ) {
this ( encoder , ResolvableType . forClass ( elementClass ) , null , null ) ;
protected < T > void testEncodeAll ( Publisher < ? extends T > input , Class < ? extends T > inputClass ,
Consumer < StepVerifier . FirstStep < DataBuffer > > stepConsumer ) {
testEncodeAll ( input , ResolvableType . forClass ( inputClass ) , stepConsumer , null , null ) ;
}
/ * *
* Construct a new { @code AbstractEncoderTestCase } for the given parameters .
* @param encoder the encoder
* @param elementType the element type
* @param mimeType the mime type . May be { @code null } .
* @param hints the hints . May be { @code null } .
* Helper methods that tests for a variety of decoding scenarios . This methods
* invokes :
* < ul >
* < li > { @link # testEncode ( Publisher , ResolvableType , Consumer , MimeType , Map ) } < / li >
* < li > { @link # testEncodeError ( Publisher , ResolvableType , MimeType , Map ) } < / li >
* < li > { @link # testEncodeCancel ( Publisher , ResolvableType , MimeType , Map ) } < / li >
* < li > { @link # testEncodeEmpty ( ResolvableType , MimeType , Map ) } < / li >
* < / ul >
*
* @param input the input to be provided to the encoder
* @param inputType the input type
* @param stepConsumer a consumer to { @linkplain StepVerifier verify } the output
* @param mimeType the mime type to use for decoding . May be { @code null } .
* @param hints the hints used for decoding . May be { @code null } .
* @param < T > the output type
* /
protected AbstractEncoderTestCase ( E encoder , ResolvableType elementType ,
protected < T > void testEncodeAll ( Publisher < ? extends T > input , ResolvableType inputType ,
Consumer < StepVerifier . FirstStep < DataBuffer > > stepConsumer ,
@Nullable MimeType mimeType , @Nullable Map < String , Object > hints ) {
testEncode ( input , inputType , stepConsumer , mimeType , hints ) ;
testEncodeError ( input , inputType , mimeType , hints ) ;
testEncodeCancel ( input , inputType , mimeType , hints ) ;
testEncodeEmpty ( inputType , mimeType , hints ) ;
}
Assert . notNull ( encoder , "Encoder must not be null" ) ;
Assert . notNull ( elementType , "ElementType must not be null" ) ;
/ * *
* Test a standard { @link Encoder # encode encode } scenario .
*
* @param input the input to be provided to the encoder
* @param inputClass the input class
* @param stepConsumer a consumer to { @linkplain StepVerifier verify } the output
* @param < T > the output type
* /
protected < T > void testEncode ( Publisher < ? extends T > input , Class < ? extends T > inputClass ,
Consumer < StepVerifier . FirstStep < DataBuffer > > stepConsumer ) {
testEncode ( input , ResolvableType . forClass ( inputClass ) , stepConsumer , null , null ) ;
}
this . encoder = encoder ;
this . elementType = elementType ;
this . mimeType = mimeType ;
this . hints = hints ;
/ * *
* Test a standard { @link Encoder # encode encode } scenario .
*
* @param input the input to be provided to the encoder
* @param inputType the input type
* @param stepConsumer a consumer to { @linkplain StepVerifier verify } the output
* @param mimeType the mime type to use for decoding . May be { @code null } .
* @param hints the hints used for decoding . May be { @code null } .
* @param < T > the output type
* /
@SuppressWarnings ( "unchecked" )
protected < T > void testEncode ( Publisher < ? extends T > input , ResolvableType inputType ,
Consumer < StepVerifier . FirstStep < DataBuffer > > stepConsumer ,
@Nullable MimeType mimeType , @Nullable Map < String , Object > hints ) {
Flux < DataBuffer > result = encoder ( ) . encode ( input , this . bufferFactory , inputType ,
mimeType , hints ) ;
StepVerifier . FirstStep < DataBuffer > step = StepVerifier . create ( result ) ;
stepConsumer . accept ( step ) ;
}
/ * *
* Abstract template method that provides input for the encoder .
* Used for { @link # encode ( ) } , { @link # encodeError ( ) } , and { @link # encodeCancel ( ) } .
* Test a { @link Encoder # encode encode } scenario where the input stream contains an error .
* This test method will feed the first element of the { @code input } stream to the encoder ,
* followed by an { @link InputException } .
* The result is expected to contain one "normal" element , followed by the error .
*
* @param input the input to be provided to the encoder
* @param inputType the input type
* @param mimeType the mime type to use for decoding . May be { @code null } .
* @param hints the hints used for decoding . May be { @code null } .
* @see InputException
* /
protected abstract Flux < T > input ( ) ;
protected void testEncodeError ( Publisher < ? > input , ResolvableType inputType ,
@Nullable MimeType mimeType , @Nullable Map < String , Object > hints ) {
input = Flux . concat (
Flux . from ( input ) . take ( 1 ) ,
Flux . error ( new InputException ( ) ) ) ;
Flux < DataBuffer > result = encoder ( ) . encode ( input , this . bufferFactory , inputType ,
mimeType , hints ) ;
StepVerifier . create ( result )
. consumeNextWith ( DataBufferUtils : : release )
. expectError ( InputException . class )
. verify ( ) ;
}
/ * *
* Abstract template method that verifies the output of the encoder .
* The returned stream should contain a buffer consumer for each expected output , given
* the { @linkplain # input ( ) } .
* Test a { @link Encoder # encode encode } scenario where the input stream is canceled .
* This test method will feed the first element of the { @code input } stream to the decoder ,
* followed by a cancel signal .
* The result is expected to contain one "normal" element .
*
* @param input the input to be provided to the encoder
* @param inputType the input type
* @param mimeType the mime type to use for decoding . May be { @code null } .
* @param hints the hints used for decoding . May be { @code null } .
* /
protected abstract Stream < Consumer < DataBuffer > > outputConsumers ( ) ;
protected void testEncodeCancel ( Publisher < ? > input , ResolvableType inputType ,
@Nullable MimeType mimeType , @Nullable Map < String , Object > hints ) {
private Stream < Consumer < DataBuffer > > outputAndReleaseConsumers ( ) {
return outputConsumers ( )
. map ( consumer - > consumer . andThen ( DataBufferUtils : : release ) ) ;
Flux < DataBuffer > result = encoder ( ) . encode ( input , this . bufferFactory , inputType , mimeType ,
hints ) ;
StepVerifier . create ( result )
. consumeNextWith ( DataBufferUtils : : release )
. thenCancel ( )
. verify ( ) ;
}
/ * *
* Create a result consumer that expects the given String in UTF - 8 encoding .
* @param expected the expected string
* @return a consumer that expects the given data buffer to be equal to { @code expected }
* Test a { @link Encoder # encode encode } scenario where the input stream is empty .
* The output is expected to be empty as well .
*
* @param inputType the input type
* @param mimeType the mime type to use for decoding . May be { @code null } .
* @param hints the hints used for decoding . May be { @code null } .
* /
protected final Consumer < DataBuffer > resultConsumer ( String expected ) {
return dataBuffer - > {
byte [ ] resultBytes = new byte [ dataBuffer . readableByteCount ( ) ] ;
dataBuffer . read ( resultBytes ) ;
String actual = new String ( resultBytes , UTF_8 ) ;
assertEquals ( expected , actual ) ;
} ;
protected void testEncodeEmpty ( ResolvableType inputType , @Nullable MimeType mimeType ,
@Nullable Map < String , Object > hints ) {
Flux < ? > input = Flux . empty ( ) ;
Flux < DataBuffer > result = encoder ( ) . encode ( input , this . bufferFactory , inputType ,
mimeType , hints ) ;
StepVerifier . create ( result )
. verifyComplete ( ) ;
}
/ * *
* Create a result consumer that expects the given bytes .
* @param expected the expected string
* @param expected the expected byte s
* @return a consumer that expects the given data buffer to be equal to { @code expected }
* /
protected final Consumer < DataBuffer > resultConsumer ( byte [ ] expected ) {
protected final Consumer < DataBuffer > expectBytes ( byte [ ] expected ) {
return dataBuffer - > {
byte [ ] resultBytes = new byte [ dataBuffer . readableByteCount ( ) ] ;
dataBuffer . read ( resultBytes ) ;
release ( dataBuffer ) ;
assertArrayEquals ( expected , resultBytes ) ;
} ;
}
/ * *
* Tests whether passing { @link # input ( ) } to the encoder can be consumed with
* { @link # outputConsumers ( ) } .
* Create a result consumer that expects the given string , using the UTF - 8 encoding .
* @param expected the expected string
* @return a consumer that expects the given data buffer to be equal to { @code expected }
* /
@Test
public final void encode ( ) {
Flux < T > input = input ( ) ;
Flux < DataBuffer > output = this . encoder . encode ( input , this . bufferFactory ,
this . elementType , this . mimeType , this . hints ) ;
StepVerifier . Step < DataBuffer > step = StepVerifier . create ( output ) ;
outputAndReleaseConsumers ( ) . forEach ( step : : consumeNextWith ) ;
protected Consumer < DataBuffer > expectString ( String expected ) {
return dataBuffer - > {
byte [ ] resultBytes = new byte [ dataBuffer . readableByteCount ( ) ] ;
dataBuffer . read ( resultBytes ) ;
release ( dataBuffer ) ;
String actual = new String ( resultBytes , UTF_8 ) ;
assertEquals ( expected , actual ) ;
} ;
step . expectComplete ( )
. verify ( ) ;
}
/ * *
* Tests whether passing an error to the encoder can be consumed with
* { @link # outputConsumers ( ) } .
* /
@Test
public final void encodeError ( ) {
boolean singleValue = this . encoder instanceof AbstractSingleValueEncoder ;
Flux < T > input ;
if ( singleValue ) {
input = Flux . error ( new RuntimeException ( ) ) ;
}
else {
input = Flux . concat (
input ( ) . take ( 1 ) ,
Flux . error ( new RuntimeException ( ) ) ) ;
}
Flux < DataBuffer > output = this . encoder . encode ( input , this . bufferFactory ,
this . elementType , this . mimeType , this . hints ) ;
if ( singleValue ) {
StepVerifier . create ( output )
. expectError ( RuntimeException . class )
. verify ( ) ;
}
else {
Consumer < DataBuffer > firstResultConsumer = outputAndReleaseConsumers ( ) . findFirst ( )
. orElseThrow ( IllegalArgumentException : : new ) ;
StepVerifier . create ( output )
. consumeNextWith ( firstResultConsumer )
. expectError ( RuntimeException . class )
. verify ( ) ;
}
@SuppressWarnings ( "unchecked" )
private < T > Encoder < T > encoder ( ) {
return ( Encoder < T > ) this . encoder ;
}
/ * *
* Tests whether canceling the output of the encoder can be consumed with
* { @link # outputConsumers ( ) } .
* Exception used in { @link # testEncodeError } .
* /
@Test
public final void encodeCancel ( ) {
Flux < T > input = input ( ) ;
@SuppressWarnings ( "serial" )
public static class InputException extends RuntimeException {
Flux < DataBuffer > output = this . encoder . encode ( input , this . bufferFactory ,
this . elementType , this . mimeType , this . hints ) ;
Consumer < DataBuffer > firstResultConsumer = outputAndReleaseConsumers ( ) . findFirst ( )
. orElseThrow ( IllegalArgumentException : : new ) ;
StepVerifier . create ( output )
. consumeNextWith ( firstResultConsumer )
. thenCancel ( )
. verify ( ) ;
}
}