diff --git a/assert/assert-junit5/src/main/java/com/mastercard/test/flow/assrt/junit5/Flocessor.java b/assert/assert-junit5/src/main/java/com/mastercard/test/flow/assrt/junit5/Flocessor.java index d4d5f8ec34..ca4757c24a 100644 --- a/assert/assert-junit5/src/main/java/com/mastercard/test/flow/assrt/junit5/Flocessor.java +++ b/assert/assert-junit5/src/main/java/com/mastercard/test/flow/assrt/junit5/Flocessor.java @@ -1,23 +1,30 @@ package com.mastercard.test.flow.assrt.junit5; -import static org.junit.jupiter.api.DynamicTest.dynamicTest; - import java.net.URI; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; import org.opentest4j.IncompleteExecutionException; import org.opentest4j.TestAbortedException; +import static com.mastercard.test.flow.assrt.Order.CHAIN_TAG_PREFIX; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + import com.mastercard.test.flow.Flow; import com.mastercard.test.flow.Model; import com.mastercard.test.flow.assrt.AbstractFlocessor; import com.mastercard.test.flow.assrt.History.Result; +import com.mastercard.test.flow.util.Tags; /** * Integrates {@link Flow} processing into junit 5. This should be used as the @@ -53,33 +60,88 @@ public Flocessor( String title, Model model ) { * @return A stream of test cases */ public Stream tests() { - return flows().map( flow -> dynamicTest( - flow.meta().id(), - testSource( flow ), - () -> { - try { - process( flow ); - history.recordResult( flow, Result.SUCCESS ); - } - catch( IncompleteExecutionException iee ) { - // not strictly required to record the skipped outcome in the history, as it - // does not inform the processing of later flows. That may change in the future - // though, so for now we're going to live with the mutation testing complaint - history.recordResult( flow, Result.SKIP ); - throw iee; - } - catch( AssertionError ae ) { - history.recordResult( flow, Result.UNEXPECTED ); - throw ae; - } - catch( Exception e ) { - // not strictly required to record the error outcome in the history, as it - // does not inform the processing of later flows. That may change in the future - // though, so for now we're going to live with the mutation testing complaint - history.recordResult( flow, Result.ERROR ); - throw e; - } - } ) ); + List nodes = new ArrayList<>(); + List currentChain = new ArrayList<>(); + String currentChainId = null; + + // Iterate over flows once and separate them into chained and non-chained + for( Flow flow : flows().collect( Collectors.toList() ) ) { + Optional chainSuffix = Tags.suffix( flow.meta().tags(), CHAIN_TAG_PREFIX ); + if( chainSuffix.isPresent() ) { + String chainId = chainSuffix.get(); + if( currentChainId == null || currentChainId.equals( chainId ) ) { + currentChainId = chainId; + currentChain.add( flow ); + } + else { + // End of the previous chain, add the current chain to nodes + nodes.add( createDynamicContainer( currentChain ) ); + currentChain.clear(); + currentChainId = chainId; + currentChain.add( flow ); + } + } + else { + if( currentChainId != null ) { + // End of the current chain, add the current chain to nodes + nodes.add( createDynamicContainer( currentChain ) ); + currentChain.clear(); + currentChainId = null; + } + nodes.add( dynamicTest( + flow.meta().id(), + testSource( flow ), + () -> processFlow( flow ) ) ); + } + } + + // If the last flows were part of a chain, add them as well + if( !currentChain.isEmpty() ) { + nodes.add( createDynamicContainer( currentChain ) ); + } + return nodes.stream(); + } + + private void processFlow( Flow flow ) { + try { + process( flow ); + history.recordResult( flow, Result.SUCCESS ); + } + catch( IncompleteExecutionException iee ) { + // not strictly required to record the skipped outcome in the history, as it + // does not inform the processing of later flows. That may change in the future + // though, so for now we're going to live with the mutation testing complaint + history.recordResult( flow, Result.SKIP ); + throw iee; + } + catch( AssertionError ae ) { + history.recordResult( flow, Result.UNEXPECTED ); + throw ae; + } + catch( Exception e ) { + // not strictly required to record the error outcome in the history, as it + // does not inform the processing of later flows. That may change in the future + // though, so for now we're going to live with the mutation testing complaint + history.recordResult( flow, Result.ERROR ); + throw e; + } + } + + private DynamicContainer createDynamicContainer( List chain ) { + List tests = chain.stream() + .map( flow -> dynamicTest( + flow.meta().id(), + testSource( flow ), + () -> processFlow( flow ) ) ) + .collect( Collectors.toList() ); + + // Use the chain tag for the display name + String chainTag = chain.stream() + .findFirst() + .flatMap( flow -> Tags.suffix( flow.meta().tags(), CHAIN_TAG_PREFIX ) ) + .orElse( chain.get( 0 ).meta().id() ); + + return DynamicContainer.dynamicContainer( "chain:" + chainTag, tests ); } /** diff --git a/assert/assert-junit5/src/test/java/com/mastercard/test/flow/assrt/junit5/FlocessorTest.java b/assert/assert-junit5/src/test/java/com/mastercard/test/flow/assrt/junit5/FlocessorTest.java new file mode 100644 index 0000000000..4b580d68bd --- /dev/null +++ b/assert/assert-junit5/src/test/java/com/mastercard/test/flow/assrt/junit5/FlocessorTest.java @@ -0,0 +1,175 @@ +package com.mastercard.test.flow.assrt.junit5; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.mastercard.test.flow.Flow; +import com.mastercard.test.flow.Model; +import com.mastercard.test.flow.builder.Chain; +import com.mastercard.test.flow.builder.Creator; +import com.mastercard.test.flow.util.Tags; + +/** + * Validates the {@link Flocessor} class for DynamicContainer creation of + * chained flows + */ +@SuppressWarnings("static-method") +class FlocessorTest { + + /** + * A simple sequence of flows with no chains + */ + @Test + void simple() { + expectNodes( model( null, null, null, null, null ), + "test : 0 []", + "test : 1 []", + "test : 2 []", + "test : 3 []", + "test : 4 []" ); + } + + /** + * A single chain of a single flow + */ + @Test + void link() { + expectNodes( model( null, "a", null ), + "test : 0 []", + "container : chain:a", + " test : 1 [chain:a]", + "test : 2 []" ); + } + + /** + * Consecutive single-flow chains + */ + @Test + void links() { + expectNodes( model( "a", "b", "c" ), + "container : chain:a", + " test : 0 [chain:a]", + "container : chain:b", + " test : 1 [chain:b]", + "container : chain:c", + " test : 2 [chain:c]" ); + } + + /** + * A single multi-flow chain + */ + @Test + void chain() { + // in the middle + expectNodes( model( null, "a", "a", "a", null ), + "test : 0 []", + "container : chain:a", + " test : 1 [chain:a]", + " test : 2 [chain:a]", + " test : 3 [chain:a]", + "test : 4 []" ); + + // at the start + expectNodes( model( "a", "a", "a", null ), + "container : chain:a", + " test : 0 [chain:a]", + " test : 1 [chain:a]", + " test : 2 [chain:a]", + "test : 3 []" ); + + // at the end + expectNodes( model( null, "a", "a", "a" ), + "test : 0 []", + "container : chain:a", + " test : 1 [chain:a]", + " test : 2 [chain:a]", + " test : 3 [chain:a]" ); + } + + /** + * Multiple multi-flow chains + */ + @Test + void chains() { + expectNodes( model( "a", "a", null, "b", "b", "c" ), + "container : chain:a", + " test : 0 [chain:a]", + " test : 1 [chain:a]", + "test : 2 []", + "container : chain:b", + " test : 3 [chain:b]", + " test : 4 [chain:b]", + "container : chain:c", + " test : 5 [chain:c]" ); + } + + private static Model model( String... chains ) { + List flows = new ArrayList<>(); + for( int i = 0; i < chains.length; i++ ) { + int idx = i; + Flow flow = Creator + .build( f -> f.meta( data -> data + .description( String.valueOf( idx ) ) + .tags( Tags.add( Optional.ofNullable( chains[idx] ) + .map( v -> Chain.PREFIX + v ) + .orElse( "" ) ) ) ) ); + flows.add( flow ); + } + Model model = mock( Model.class ); + when( model.flows( anySet(), anySet() ) ) + .thenReturn( flows.stream() ); + + return model; + } + + private static void expectNodes( Model model, String... expected ) { + Flocessor flocessor = new Flocessor( "", model ); + List actual = new ArrayList<>(); + flocessor.tests() + .forEach( node -> stringify( node, "", actual ) ); + assertEquals( + copypasta( Stream.of( expected ) ), + copypasta( actual.stream() ) ); + } + + private static void stringify( DynamicNode node, String prefix, List lines ) { + if( node instanceof DynamicTest ) { + DynamicTest test = (DynamicTest) node; + lines.add( prefix + "test : " + test.getDisplayName() ); + } + else if( node instanceof DynamicContainer ) { + DynamicContainer container = (DynamicContainer) node; + lines.add( prefix + "container : " + container.getDisplayName() ); + container.getChildren() + .forEach( child -> stringify( child, prefix + " ", lines ) ); + } + else { + throw new IllegalStateException( "unexpected node " + node.getClass() ); + } + } + + /** + * @param content Some strings + * @return A string that can be trivially copy/pasted into java source + */ + private static String copypasta( Stream content ) { + return content + .map( s -> s.replaceAll( "\r", "" ) ) + .flatMap( s -> Stream.of( s.split( "\n" ) ) ) + .map( s -> s.replaceAll( "\"", "'" ) ) + .collect( Collectors.joining( "\",\n\"", "\"", "\"" ) ); + } +} diff --git a/assert/pom.xml b/assert/pom.xml index 9a660404da..fa824f9b8b 100644 --- a/assert/pom.xml +++ b/assert/pom.xml @@ -18,6 +18,13 @@ + + + org.mockito + mockito-core + test + +