From ab8629faa6befef8258f1b714c543cff3e867813 Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Thu, 25 Aug 2022 11:00:17 +0100 Subject: [PATCH 1/8] Usability updates --- doc/src/main/markdown/further.md | 4 +- example/app-framework/pom.xml | 6 ++ message/message-core/pom.xml | 2 +- .../test/flow/msg/AbstractMessage.java | 59 ++++++++++++------ .../test/flow/msg/AbstractMessageTest.java | 60 +++++++++++++++++-- .../mastercard/test/flow/msg/json/Json.java | 8 +-- .../test/flow/msg/json/JsonTest.java | 3 +- .../mastercard/test/flow/msg/sql/Result.java | 7 +-- .../test/flow/msg/sql/QueryTest.java | 3 +- .../test/flow/msg/sql/ResultTest.java | 3 +- 10 files changed, 117 insertions(+), 38 deletions(-) diff --git a/doc/src/main/markdown/further.md b/doc/src/main/markdown/further.md index ddaf83b54c..2f4264477f 100644 --- a/doc/src/main/markdown/further.md +++ b/doc/src/main/markdown/further.md @@ -229,12 +229,12 @@ Consider the following worked example: [flow.Unpredictable]: ../../../../api/src/main/java/com/mastercard/test/flow/Unpredictable.java -[AbstractMessage.masking(Unpredictable,UnaryOperator)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java#L45-L52,45-52 +[AbstractMessage.masking(Unpredictable,UnaryOperator)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java#L46-L53,46-53 [AbstractFlocessor.masking(Unpredictable...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L167-L174,167-174 [mask.BenSys]: ../../test/java/com/mastercard/test/flow/doc/mask/BenSys.java [mask.DieSys]: ../../test/java/com/mastercard/test/flow/doc/mask/DieSys.java [mask.Unpredictables]: ../../test/java/com/mastercard/test/flow/doc/mask/Unpredictables.java -[AbstractMessage.masking(Unpredictable,UnaryOperator)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java#L45-L52,45-52 +[AbstractMessage.masking(Unpredictable,UnaryOperator)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java#L46-L53,46-53 [Rolling?d\+]: ../../test/java/com/mastercard/test/flow/doc/mask/Rolling.java#L30,30 [msg.Mask]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java [msg.Mask.andThen(Consumer)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java#L289-L291,289-291 diff --git a/example/app-framework/pom.xml b/example/app-framework/pom.xml index e2b0607a43..92318423fb 100644 --- a/example/app-framework/pom.xml +++ b/example/app-framework/pom.xml @@ -36,6 +36,12 @@ picocli + + + org.junit.jupiter + junit-jupiter + diff --git a/message/message-core/pom.xml b/message/message-core/pom.xml index 939b7de293..742d90d0d0 100644 --- a/message/message-core/pom.xml +++ b/message/message-core/pom.xml @@ -43,7 +43,7 @@ pitest-maven + we shouldn't let them slip down by accident --> 100 100 diff --git a/message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java b/message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java index 22fd2036d8..afcd5693c2 100644 --- a/message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java +++ b/message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java @@ -2,6 +2,7 @@ import static java.util.stream.Collectors.toCollection; +import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; @@ -105,8 +106,8 @@ public String assertable( Unpredictable... sources ) { @Override public T set( String field, Object value ) { - validateValueType( field, value ); - updates.add( new Update( field, value ) ); + Object validValue = validateValueType( field, value ); + updates.add( new Update( field, validValue ) ); return self(); } @@ -119,27 +120,47 @@ public T set( String field, Object value ) { .collect( toCollection( HashSet::new ) ); /** + * This method is called for every field update. This gives implementors the + * opportunity to validate and alter values (e.g.: taking a defensive copy of + * mutable types) + * * @param field The field address that we're trying to populate * @param value The value that we've been asked to populate into the message + * @return A validated value for that field */ - @SuppressWarnings("static-method") - protected void validateValueType( String field, Object value ) { - if( value != null && value != DELETE && !value.getClass().isPrimitive() - && !immutableTypes.contains( value.getClass() ) ) { - throw new IllegalArgumentException( "" - + "Field '" + field + "' - Possibly-mutable value type " + value.getClass() + "\n" - + "If you're sure that this type is immutable, then you can call\n" - + " AbstractMessage.registerImmutable( " + value.getClass().getSimpleName() - + ".class )\n" - + "to suppress this error.\n" - + "You can also override\n" - + " AbstractMessage.validateValueType( field, value )\n" - + "in your subclass implementation to do validation more suitable\n" - + "for your requirements. Please be fully aware of the implications\n" - + "of mutable value types in your message fields: the interface\n" - + "contract of child() is put at risk and the resulting failures\n" - + "can be painful to debug" ); + protected Object validateValueType( String field, Object value ) { + if( value != null ) { + // we can take a defensive copy + if( value.getClass().isArray() ) { + Object copy = Array.newInstance( + value.getClass().getComponentType(), + Array.getLength( value ) ); + for( int i = 0; i < Array.getLength( copy ); i++ ) { + Array.set( copy, i, + validateValueType( field + "[" + i + "]", Array.get( value, i ) ) ); + } + return copy; + } + if( value != DELETE + && !value.getClass().isPrimitive() + && !value.getClass().isEnum() + && !immutableTypes.contains( value.getClass() ) ) { + throw new IllegalArgumentException( "" + + "Field '" + field + "' - Possibly-mutable value type " + value.getClass() + "\n" + + "If you're sure that this type is immutable, then you can call\n" + + " AbstractMessage.registerImmutable( " + value.getClass().getSimpleName() + + ".class )\n" + + "to suppress this error.\n" + + "You can also override\n" + + " AbstractMessage.validateValueType( field, value )\n" + + "in your subclass implementation to do validation more suitable\n" + + "for your requirements. Please be fully aware of the implications\n" + + "of mutable value types in your message fields: the interface\n" + + "contract of child() is put at risk and the resulting failures\n" + + "can be painful to debug" ); + } } + return value; } /** diff --git a/message/message-core/src/test/java/com/mastercard/test/flow/msg/AbstractMessageTest.java b/message/message-core/src/test/java/com/mastercard/test/flow/msg/AbstractMessageTest.java index af5ffdde22..a9e91ee6c4 100644 --- a/message/message-core/src/test/java/com/mastercard/test/flow/msg/AbstractMessageTest.java +++ b/message/message-core/src/test/java/com/mastercard/test/flow/msg/AbstractMessageTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; @@ -25,7 +26,7 @@ private enum NPrdctbl implements Unpredictable { } /** - * The minimal concrete type we'll use to test the functiuonality of + * The minimal concrete type we'll use to test the functionality of * {@link AbstractMessage} */ private static class ConcreteMessage extends AbstractMessage { @@ -46,11 +47,27 @@ protected String asHuman() { u.field(), u.value() == AbstractMessage.DELETE ? "The special deletion value" - : u.value() ) ) + : valueString( u.value() ) ) ) .collect( joining( "\n " ) )) .trim(); } + private static final String valueString( Object value ) { + if( value.getClass().isArray() ) { + StringBuilder sb = new StringBuilder(); + sb.append( "[" ); + for( int i = 0; i < Array.getLength( value ); i++ ) { + if( i != 0 ) { + sb.append( "," ); + } + sb.append( valueString( Array.get( value, i ) ) ); + } + sb.append( "]" ); + return sb.toString(); + } + return String.valueOf( value ); + } + @Override public byte[] content() { throw new UnsupportedOperationException(); @@ -83,11 +100,13 @@ public ConcreteMessage peer( byte[] content ) { @Test void set() { ConcreteMessage msg = new ConcreteMessage( "msg" ) - .set( "foo", "bar" ); + .set( "foo", "bar" ) + .set( "enum", NPrdctbl.FOO ); Assertions.assertEquals( "" + "msg\n" - + " foo=bar", + + " foo=bar\n" + + " enum=FOO", msg.asHuman() ); } @@ -171,4 +190,37 @@ void mutableValues() throws Exception { AbstractMessage.registerImmutable( ArrayList.class ); msg.set( "field", new ArrayList<>() ); } + + /** + * Shows that setting arrays takes a defensive copy + */ + @Test + void arrayDefense() { + ConcreteMessage msg = new ConcreteMessage( "msg" ); + + Object[][] array = { { 1 } }; + msg.set( "field", array ); + array[0][0] = 2; + assertEquals( "msg\n" + + " field=[[1]]", + msg.asHuman(), + "The value set in the message has not changed" ); + + array[0][0] = new Object(); + IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, + () -> msg.set( "field", array ) ); + assertEquals( "" + + "Field 'field[0][0]' - Possibly-mutable value type class java.lang.Object\n" + + "If you're sure that this type is immutable, then you can call\n" + + " AbstractMessage.registerImmutable( Object.class )\n" + + "to suppress this error.\n" + + "You can also override\n" + + " AbstractMessage.validateValueType( field, value )\n" + + "in your subclass implementation to do validation more suitable\n" + + "for your requirements. Please be fully aware of the implications\n" + + "of mutable value types in your message fields: the interface\n" + + "contract of child() is put at risk and the resulting failures\n" + + "can be painful to debug", iae.getMessage(), + "Array contents are searched for suspicious types" ); + } } diff --git a/message/message-json/src/main/java/com/mastercard/test/flow/msg/json/Json.java b/message/message-json/src/main/java/com/mastercard/test/flow/msg/json/Json.java index f17e69dae4..9537d901de 100644 --- a/message/message-json/src/main/java/com/mastercard/test/flow/msg/json/Json.java +++ b/message/message-json/src/main/java/com/mastercard/test/flow/msg/json/Json.java @@ -104,13 +104,11 @@ public Json peer( byte[] content ) { } @Override - protected void validateValueType( String field, Object value ) { + protected Object validateValueType( String field, Object value ) { if( value == EMPTY_MAP || value == EMPTY_LIST ) { - // this is fine - } - else { - super.validateValueType( field, value ); + return value; } + return super.validateValueType( field, value ); } private Object data() { diff --git a/message/message-json/src/test/java/com/mastercard/test/flow/msg/json/JsonTest.java b/message/message-json/src/test/java/com/mastercard/test/flow/msg/json/JsonTest.java index c72228cbc3..81914053ea 100644 --- a/message/message-json/src/test/java/com/mastercard/test/flow/msg/json/JsonTest.java +++ b/message/message-json/src/test/java/com/mastercard/test/flow/msg/json/JsonTest.java @@ -322,8 +322,9 @@ void emptyStructureTokens() { void badContent() { Json json = new Json() { @Override - protected void validateValueType( String field, Object value ) { + protected Object validateValueType( String field, Object value ) { // allow everything + return value; } }; json.set( "field", new Object() ); diff --git a/message/message-sql/src/main/java/com/mastercard/test/flow/msg/sql/Result.java b/message/message-sql/src/main/java/com/mastercard/test/flow/msg/sql/Result.java index 35e1fb6542..2d943141d7 100644 --- a/message/message-sql/src/main/java/com/mastercard/test/flow/msg/sql/Result.java +++ b/message/message-sql/src/main/java/com/mastercard/test/flow/msg/sql/Result.java @@ -132,7 +132,7 @@ else if( update.field().matches( "\\d+" ) ) { } @Override - protected void validateValueType( String field, Object value ) { + protected Object validateValueType( String field, Object value ) { // We want to allow adding rows with a list. We copy the list values anyway, so // have no fears about mutability if( field.matches( "\\d+" ) && value instanceof List ) { @@ -142,10 +142,9 @@ protected void validateValueType( String field, Object value ) { super.validateValueType( field + ":" + idx, o ); idx++; } + return value; } - else { - super.validateValueType( field, value ); - } + return super.validateValueType( field, value ); } @Override diff --git a/message/message-sql/src/test/java/com/mastercard/test/flow/msg/sql/QueryTest.java b/message/message-sql/src/test/java/com/mastercard/test/flow/msg/sql/QueryTest.java index 23a4cddde9..281f085df8 100644 --- a/message/message-sql/src/test/java/com/mastercard/test/flow/msg/sql/QueryTest.java +++ b/message/message-sql/src/test/java/com/mastercard/test/flow/msg/sql/QueryTest.java @@ -215,8 +215,9 @@ void peer() { void badContent() { Query query = new Query( "SELECT 1" ) { @Override - protected void validateValueType( String field, Object value ) { + protected Object validateValueType( String field, Object value ) { // allow everything + return value; } }; query.set( "1", new Object() ); diff --git a/message/message-sql/src/test/java/com/mastercard/test/flow/msg/sql/ResultTest.java b/message/message-sql/src/test/java/com/mastercard/test/flow/msg/sql/ResultTest.java index c604f73643..5a738fd845 100644 --- a/message/message-sql/src/test/java/com/mastercard/test/flow/msg/sql/ResultTest.java +++ b/message/message-sql/src/test/java/com/mastercard/test/flow/msg/sql/ResultTest.java @@ -223,8 +223,9 @@ void peer() { void badContent() { Result res = new Result( "abc" ) { @Override - protected void validateValueType( String field, Object value ) { + protected Object validateValueType( String field, Object value ) { // allow everything + return value; } }; From 6877ad612f580252f6ee46d2f271f3fb7b90c755 Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Fri, 26 Aug 2022 16:49:58 +0100 Subject: [PATCH 2/8] convenient interation access --- .../com/mastercard/test/flow/util/Flows.java | 30 +++ .../test/flow/util/InteractionPredicate.java | 144 ++++++++++++++ .../mastercard/test/flow/util/FlowsTest.java | 62 +++++- .../flow/util/InteractionPredicateTest.java | 179 ++++++++++++++++++ 4 files changed, 409 insertions(+), 6 deletions(-) create mode 100644 api/src/main/java/com/mastercard/test/flow/util/InteractionPredicate.java create mode 100644 api/src/test/java/com/mastercard/test/flow/util/InteractionPredicateTest.java diff --git a/api/src/main/java/com/mastercard/test/flow/util/Flows.java b/api/src/main/java/com/mastercard/test/flow/util/Flows.java index df025684d7..3900c73235 100644 --- a/api/src/main/java/com/mastercard/test/flow/util/Flows.java +++ b/api/src/main/java/com/mastercard/test/flow/util/Flows.java @@ -8,7 +8,9 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -70,6 +72,34 @@ public static Stream interactions( Flow flow ) { return Stream.concat( Stream.of( flow.root() ), descendents( flow.root() ) ); } + /** + * Looks for an {@link Interaction} in a {@link Flow} + * + * @param flow The {@link Flow} to search + * @param interaction How to recognise the {@link Interaction} of interest + * @return The chronologically first {@link Interaction} in the {@link Flow} + * that satisfies the supplied condition + * @see InteractionPredicate + */ + public static Optional find( Flow flow, Predicate interaction ) { + return interactions( flow ).filter( interaction ).findFirst(); + } + + /** + * Extracts a single {@link Interaction} from a {@link Flow} + * + * @param flow The {@link Flow} to search + * @param interaction How to recognise the {@link Interaction} of interest + * @return The chronologically first {@link Interaction} in the {@link Flow} + * that satisfies the supplied condition + * @throws IllegalArgumentException if there is no such {@link Interaction} + * @see InteractionPredicate + */ + public static Interaction get( Flow flow, Predicate interaction ) { + return find( flow, interaction ).orElseThrow( () -> new IllegalArgumentException( + "No interaction matching '" + interaction + "' in " + flow.meta().id() ) ); + } + /** * Collects the consequences of an interaction * diff --git a/api/src/main/java/com/mastercard/test/flow/util/InteractionPredicate.java b/api/src/main/java/com/mastercard/test/flow/util/InteractionPredicate.java new file mode 100644 index 0000000000..96ff35edfb --- /dev/null +++ b/api/src/main/java/com/mastercard/test/flow/util/InteractionPredicate.java @@ -0,0 +1,144 @@ +package com.mastercard.test.flow.util; + +import static java.util.Collections.emptySet; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.joining; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; + +import com.mastercard.test.flow.Actor; +import com.mastercard.test.flow.Interaction; + +/** + *

+ * A convenience implementation of {@link Predicate} that offers friendlier + * {@link #toString()} behaviour. + *

+ *

+ * Note that this type is immutable - the methods that appear to be mutators + * actually return new instances. + *

+ */ +public class InteractionPredicate implements Predicate { + + private final Set tx; + private final Set rx; + private final Set include; + private final Set exclude; + + /** + * Constructs an empty {@link Predicate} that will match every + * {@link Interaction} + */ + public InteractionPredicate() { + this( emptySet(), emptySet(), emptySet(), emptySet() ); + } + + private InteractionPredicate( Set tx, Set rx, Set include, + Set exclude ) { + this.tx = tx; + this.rx = rx; + this.include = include; + this.exclude = exclude; + } + + /** + * Defines the allowed set of requesting {@link Actor}s + * + * @param senders {@link Interaction}s that originate from these {@link Actor}s + * will be matched + * @return A new {@link InteractionPredicate} based on this, but + * with the new condition + */ + public InteractionPredicate from( Actor... senders ) { + Set nf = new TreeSet<>( comparing( Actor::name ) ); + Collections.addAll( nf, senders ); + return new InteractionPredicate( nf, rx, include, exclude ); + } + + /** + * Defines the allowed set of responding {@link Actor}s + * + * @param receivers interactions that destinate(?) at the {@link Actor}s will be + * matched + * @return A new {@link InteractionPredicate} based on this, but + * with the new condition + */ + public InteractionPredicate to( Actor... receivers ) { + Set nt = new TreeSet<>( comparing( Actor::name ) ); + Collections.addAll( nt, receivers ); + return new InteractionPredicate( tx, nt, include, exclude ); + } + + /** + * Defines the set of tags that must be present + * + * @param tags interactions that bear all of these tags will be matched + * @return A new {@link InteractionPredicate} based on this, but + * with the new condition + */ + public InteractionPredicate with( String... tags ) { + Set ni = new TreeSet<>(); + Collections.addAll( ni, tags ); + return new InteractionPredicate( tx, rx, ni, exclude ); + } + + /** + * Defines the set of tags that must be absent + * + * @param tags interactions that bear none of these tags will be matched + * @return A new {@link InteractionPredicate} based on this, but + * with the new condition + */ + public InteractionPredicate without( String... tags ) { + Set ne = new TreeSet<>(); + Collections.addAll( ne, tags ); + return new InteractionPredicate( tx, rx, include, ne ); + } + + @Override + public boolean test( Interaction t ) { + boolean match = true; + if( !tx.isEmpty() ) { + match &= tx.contains( t.requester() ); + } + if( !rx.isEmpty() ) { + match &= rx.contains( t.responder() ); + } + if( !include.isEmpty() ) { + match &= include.stream().allMatch( t.tags()::contains ); + } + if( !exclude.isEmpty() ) { + match &= exclude.stream().noneMatch( t.tags()::contains ); + } + return match; + } + + @Override + public String toString() { + List conditions = new ArrayList<>(); + if( !tx.isEmpty() ) { + conditions.add( "from " + tx ); + } + if( !rx.isEmpty() ) { + conditions.add( "to " + rx ); + } + if( !include.isEmpty() ) { + conditions.add( "has tags " + include ); + } + if( !exclude.isEmpty() ) { + conditions.add( "lacks tags " + exclude ); + } + + if( conditions.isEmpty() ) { + return "anything"; + } + + return conditions.stream().collect( joining( " and " ) ); + } +} diff --git a/api/src/test/java/com/mastercard/test/flow/util/FlowsTest.java b/api/src/test/java/com/mastercard/test/flow/util/FlowsTest.java index 8987a20e7c..dd7eb75f46 100644 --- a/api/src/test/java/com/mastercard/test/flow/util/FlowsTest.java +++ b/api/src/test/java/com/mastercard/test/flow/util/FlowsTest.java @@ -3,6 +3,8 @@ import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toSet; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.reflect.Constructor; @@ -13,6 +15,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -82,14 +85,14 @@ private static final Actor actor( String name ) { private static final Interaction interaction( Actor from, String request, Actor to, String response ) { - Interaction m = Mockito.mock( Interaction.class ); - Mockito.when( m.requester() ).thenReturn( from ); + Interaction ntr = Mockito.mock( Interaction.class ); + Mockito.when( ntr.requester() ).thenReturn( from ); Message req = message( request ); - Mockito.when( m.request() ).thenReturn( req ); - Mockito.when( m.responder() ).thenReturn( to ); + Mockito.when( ntr.request() ).thenReturn( req ); + Mockito.when( ntr.responder() ).thenReturn( to ); Message res = message( response ); - Mockito.when( m.response() ).thenReturn( res ); - return m; + Mockito.when( ntr.response() ).thenReturn( res ); + return ntr; } private static final Message message( String name ) { @@ -191,6 +194,53 @@ void interactions() { .collect( joining( "\n" ) ) ); } + /** + * Exercises {@link Flows#find(Flow, java.util.function.Predicate)} + */ + @Test + void find() { + Flow flow = flow( "flow", "", null ); + + assertFalse( Flows.find( flow, i -> false ).isPresent() ); + + assertEquals( "AVA > abc | BEN < cba", + Flows.find( flow, i -> true ) + .map( i -> String.format( "%s > %s | %s < %s", + i.requester().name(), i.request().assertable(), + i.responder().name(), i.response().assertable() ) ) + .get() ); + } + + /** + * Exercises {@link Flows#get(Flow, java.util.function.Predicate)} + */ + @Test + void get() { + Flow flow = flow( "flow", "", null ); + + Interaction i = Flows.get( flow, n -> true ); + assertEquals( "AVA > abc | BEN < cba", + String.format( "%s > %s | %s < %s", + i.requester().name(), i.request().assertable(), + i.responder().name(), i.response().assertable() ) ); + + Predicate none = new Predicate() { + @Override + public boolean test( Interaction t ) { + return false; + } + + @Override + public String toString() { + return "predicate tostring"; + } + }; + + IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, + () -> Flows.get( flow, none ) ); + assertEquals( "No interaction matching 'predicate tostring' in flow []", iae.getMessage() ); + } + /** * Exercises {@link Flows#transmissions(Flow)} */ diff --git a/api/src/test/java/com/mastercard/test/flow/util/InteractionPredicateTest.java b/api/src/test/java/com/mastercard/test/flow/util/InteractionPredicateTest.java new file mode 100644 index 0000000000..6404eb2ee9 --- /dev/null +++ b/api/src/test/java/com/mastercard/test/flow/util/InteractionPredicateTest.java @@ -0,0 +1,179 @@ +package com.mastercard.test.flow.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.mastercard.test.flow.Actor; +import com.mastercard.test.flow.Interaction; + +/** + * Exercises {@link InteractionPredicate} + */ +@SuppressWarnings("static-method") +class InteractionPredicateTest { + + private enum Actrs implements Actor { + AVA, BEN, CHE, DAN, EFA + } + + private static final Interaction interaction( Actrs from, Actrs to, String... tags ) { + Interaction ntr = Mockito.mock( Interaction.class ); + Mockito.when( ntr.requester() ).thenReturn( from ); + Mockito.when( ntr.responder() ).thenReturn( to ); + Set ts = new TreeSet<>(); + Collections.addAll( ts, tags ); + Mockito.when( ntr.tags() ).thenReturn( ts ); + return ntr; + } + + /** + * The empty predicate accepts anything + */ + @Test + void empty() { + InteractionPredicate empty = new InteractionPredicate(); + assertTrue( empty.test( null ) ); + assertTrue( empty.test( interaction( Actrs.AVA, Actrs.BEN, "tag" ) ) ); + + assertEquals( "anything", empty.toString() ); + } + + /** + * Exercising {@link InteractionPredicate#from(Actor...)} + */ + @Test + void from() { + InteractionPredicate empty = new InteractionPredicate(); + InteractionPredicate single = empty.from( Actrs.AVA ); + + assertNotSame( empty, single ); + + assertTrue( single.test( interaction( Actrs.AVA, null ) ) ); + assertFalse( single.test( interaction( Actrs.BEN, null ) ) ); + assertFalse( single.test( interaction( Actrs.CHE, null ) ) ); + assertFalse( single.test( interaction( Actrs.DAN, null ) ) ); + assertFalse( single.test( interaction( Actrs.EFA, null ) ) ); + + InteractionPredicate duble = single.from( Actrs.BEN, Actrs.CHE ); + + assertNotSame( single, duble ); + + assertFalse( duble.test( interaction( Actrs.AVA, null ) ) ); + assertTrue( duble.test( interaction( Actrs.BEN, null ) ) ); + assertTrue( duble.test( interaction( Actrs.CHE, null ) ) ); + assertFalse( duble.test( interaction( Actrs.DAN, null ) ) ); + assertFalse( duble.test( interaction( Actrs.EFA, null ) ) ); + + assertEquals( "from [AVA]", single.toString() ); + assertEquals( "from [BEN, CHE]", duble.toString() ); + } + + /** + * Exercising {@link InteractionPredicate#to(Actor...)} + */ + @Test + void to() { + InteractionPredicate empty = new InteractionPredicate(); + InteractionPredicate single = empty.to( Actrs.AVA ); + + assertNotSame( empty, single ); + + assertTrue( single.test( interaction( null, Actrs.AVA ) ) ); + assertFalse( single.test( interaction( null, Actrs.BEN ) ) ); + assertFalse( single.test( interaction( null, Actrs.CHE ) ) ); + assertFalse( single.test( interaction( null, Actrs.DAN ) ) ); + assertFalse( single.test( interaction( null, Actrs.EFA ) ) ); + + InteractionPredicate duble = single.to( Actrs.BEN, Actrs.CHE ); + + assertNotSame( single, duble ); + + assertFalse( duble.test( interaction( null, Actrs.AVA ) ) ); + assertTrue( duble.test( interaction( null, Actrs.BEN ) ) ); + assertTrue( duble.test( interaction( null, Actrs.CHE ) ) ); + assertFalse( duble.test( interaction( null, Actrs.DAN ) ) ); + assertFalse( duble.test( interaction( null, Actrs.EFA ) ) ); + + assertEquals( "to [AVA]", single.toString() ); + assertEquals( "to [BEN, CHE]", duble.toString() ); + } + + /** + * Exercises {@link InteractionPredicate#with(String...)} + */ + @Test + void with() { + + InteractionPredicate empty = new InteractionPredicate(); + InteractionPredicate with = empty.with( "b", "a", "c" ); + + assertNotSame( empty, with ); + assertEquals( "has tags [a, b, c]", with.toString() ); + + assertFalse( with.test( interaction( null, null ) ) ); + assertTrue( with.test( interaction( null, null, "a", "b", "c" ) ) ); + assertTrue( with.test( interaction( null, null, "a", "b", "c", "d" ) ) ); + assertFalse( with.test( interaction( null, null, "a", "b" ) ) ); + assertFalse( with.test( interaction( null, null, "a", "c" ) ) ); + assertFalse( with.test( interaction( null, null, "b", "c" ) ) ); + } + + /** + * Exercises {@link InteractionPredicate#without(String...)} + */ + @Test + void without() { + + InteractionPredicate empty = new InteractionPredicate(); + InteractionPredicate without = empty.without( "b", "a", "c" ); + + assertNotSame( empty, without ); + assertEquals( "lacks tags [a, b, c]", without.toString() ); + + assertTrue( without.test( interaction( null, null ) ) ); + assertFalse( without.test( interaction( null, null, "a" ) ) ); + assertFalse( without.test( interaction( null, null, "b" ) ) ); + assertFalse( without.test( interaction( null, null, "c" ) ) ); + assertFalse( without.test( interaction( null, null, "a", "b", "c" ) ) ); + + assertTrue( without.test( interaction( null, null, "d" ) ) ); + } + + /** + * Demonstrates a predicate with multiple conditions + */ + @Test + void combined() { + InteractionPredicate ip = new InteractionPredicate() + .from( Actrs.BEN ) + .to( Actrs.DAN, Actrs.EFA ) + .with( "yes" ) + .without( "no" ); + + assertEquals( "from [BEN] and to [DAN, EFA] and has tags [yes] and lacks tags [no]", + ip.toString() ); + + assertTrue( ip.test( interaction( Actrs.BEN, Actrs.DAN, "yes" ) ) ); + assertTrue( ip.test( interaction( Actrs.BEN, Actrs.EFA, "yes" ) ) ); + assertTrue( ip.test( interaction( Actrs.BEN, Actrs.DAN, "yes", "extra" ) ) ); + assertTrue( ip.test( interaction( Actrs.BEN, Actrs.EFA, "yes", "tags" ) ) ); + + assertFalse( ip.test( interaction( Actrs.BEN, Actrs.EFA, "extra" ) ), + "missing tag" ); + assertFalse( ip.test( interaction( Actrs.BEN, Actrs.EFA, "yes", "no" ) ), + "verboten tag" ); + assertFalse( ip.test( interaction( Actrs.AVA, Actrs.DAN, "yes" ) ), + "bad source" ); + assertFalse( ip.test( interaction( Actrs.BEN, Actrs.CHE, "yes" ) ), + "bad destination" ); + } +} From e55bc7e40810ea0b1f85a97c1b40b7e598c81805 Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Fri, 26 Aug 2022 20:29:57 +0100 Subject: [PATCH 3/8] Used the new type --- .../flow/example/app/model/Interactions.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/example/app-model/src/main/java/com/mastercard/test/flow/example/app/model/Interactions.java b/example/app-model/src/main/java/com/mastercard/test/flow/example/app/model/Interactions.java index 20ca52459e..fb7cdf2fc4 100644 --- a/example/app-model/src/main/java/com/mastercard/test/flow/example/app/model/Interactions.java +++ b/example/app-model/src/main/java/com/mastercard/test/flow/example/app/model/Interactions.java @@ -7,6 +7,7 @@ import com.mastercard.test.flow.Interaction; import com.mastercard.test.flow.builder.mutable.MutableInteraction; import com.mastercard.test.flow.example.app.model.ExampleSystem.Actors; +import com.mastercard.test.flow.util.InteractionPredicate; /** * Utilities for working with the interactions in our system @@ -20,37 +21,43 @@ private Interactions() { /** * Identifies all interactions */ - public static final Predicate ALL = i -> true; + public static final Predicate ALL = new InteractionPredicate(); /** * Identifies interactions with the {@link Actors#WEB_UI} */ - public static final Predicate WEB_UI = ntr -> ntr.responder() == Actors.WEB_UI; + public static final Predicate WEB_UI = new InteractionPredicate() + .to( Actors.WEB_UI ); /** * Identifies interactions with the {@link Actors#UI} */ - public static final Predicate UI = ntr -> ntr.responder() == Actors.UI; + public static final Predicate UI = new InteractionPredicate() + .to( Actors.UI ); /** * Identifies interactions with the {@link Actors#CORE} */ - public static final Predicate CORE = ntr -> ntr.responder() == Actors.CORE; + public static final Predicate CORE = new InteractionPredicate() + .to( Actors.CORE ); /** * Identifies interactions with the {@link Actors#QUEUE} */ - public static final Predicate QUEUE = ntr -> ntr.responder() == Actors.QUEUE; + public static final Predicate QUEUE = new InteractionPredicate() + .to( Actors.QUEUE ); /** * Identifies interactions with the {@link Actors#STORE} */ - public static final Predicate STORE = ntr -> ntr.responder() == Actors.STORE; + public static final Predicate STORE = new InteractionPredicate() + .to( Actors.STORE ); /** * Identifies interactions with the {@link Actors#HISTOGRAM} */ - public static final Predicate HISTOGRAM = ntr -> ntr.responder() == Actors.HISTOGRAM; + public static final Predicate HISTOGRAM = new InteractionPredicate() + .to( Actors.HISTOGRAM ); /** * Changes actors in an existing interaction structure From 38c7dac0fb946a2096278a2bd21ee9a8c4ac46ec Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Tue, 13 Sep 2022 14:05:05 +0100 Subject: [PATCH 4/8] these are ready for merge now --- assert/assert-core/README.md | 4 +- .../test/flow/assrt/AbstractFlocessor.java | 46 ++++++++-- .../mastercard/test/flow/assrt/Assertion.java | 39 ++++++--- .../{Options.java => AssertionOptions.java} | 10 +-- .../mastercard/test/flow/assrt/History.java | 4 +- .../mastercard/test/flow/assrt/Progress.java | 81 +++++++++++++++++ .../mastercard/test/flow/assrt/Replay.java | 21 ++--- .../mastercard/test/flow/assrt/log/Tail.java | 3 +- ...onsTest.java => AssertionOptionsTest.java} | 16 ++-- .../test/flow/assrt/AssertionTest.java | 40 +++++++++ .../test/flow/assrt/HistoryTest.java | 8 +- .../test/flow/assrt/ReplayTest.java | 35 ++++---- .../test/flow/assrt/SupressionTest.java | 19 ++-- .../test/flow/assrt/log/TailTest.java | 13 +-- assert/assert-filter/README.md | 8 +- .../test/flow/assrt/filter/Filter.java | 34 ++++++-- .../test/flow/assrt/filter/FilterOptions.java | 2 +- doc/src/main/markdown/further.md | 18 ++-- .../test/flow/doc/AssertOptionsTest.java | 4 +- .../test/flow/doc/ModuleDiagramTest.java | 86 +++++++++---------- .../app/assrt/AbstractServiceTest.java | 7 +- .../example/app/itest/IntegrationTest.java | 8 +- .../flow/example/app/store/QueryTest.java | 8 +- .../com/mastercard/test/flow/msg/Mask.java | 1 + .../test/flow/msg/http/HttpMsg.java | 10 ++- .../test/flow/msg/http/HttpReq.java | 5 +- .../test/flow/msg/http/HttpRes.java | 11 ++- .../test/flow/msg/http/HttpResTest.java | 25 +++++- message/message-web/README.md | 2 +- 29 files changed, 407 insertions(+), 161 deletions(-) rename assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/{Options.java => AssertionOptions.java} (90%) create mode 100644 assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Progress.java rename assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/{OptionsTest.java => AssertionOptionsTest.java} (65%) diff --git a/assert/assert-core/README.md b/assert/assert-core/README.md index 9597c34c76..3bdee79c82 100644 --- a/assert/assert-core/README.md +++ b/assert/assert-core/README.md @@ -40,11 +40,11 @@ Some aspects of assertion behaviour can be controlled by system properties: | `mctf.filter.exclude` | A comma-separated list of tags values that flows must not have | | `mctf.filter.fails` | Configures filters to repeat flows that did not pass assertion in a previous run. Supply the location of a report from which to extract results, or `latest` to extract from the most recent local report | | `mctf.filter.include` | A comma-separated list of tags values that flows must have | -| `mctf.filter.indices` | A comma-separated list of indices for flows to process | +| `mctf.filter.indices` | A comma-separated list of indices and index ranges for flows to process | | `mctf.filter.repeat` | Supply `true` to use the previous filters again | | `mctf.filter.update` | Supply `true` to update filter values at runtime in the most appropriate interface.Supply `cli` to force use of the command-line interface or `gui` to force use of the graphical interface | | `mctf.replay` | The location of a report to replay, or `latest` to replay the most recent local report | -| `mctf.report.name` | The path from the artifact directory to the report destination | +| `mctf.report.dir` | The path from the artifact directory to the report destination | | `mctf.suppress.assertion` | Set to `true` to continue processing a flow in the face of assertion failure | | `mctf.suppress.basis` | Set to `true` to process flows whose basis flows have suffered assertion failure | | `mctf.suppress.dependency` | Set to `true` to process flows whose dependency flows have suffered errors | diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java index 18903b54bc..e4b8343729 100644 --- a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt; import static java.time.Instant.now; @@ -142,6 +143,13 @@ public enum State { */ private LogCapture logCapture = LogCapture.NO_OP; + /** + * How processing progress is reported + */ + private Progress progress = new Progress() { + // default to no-op behaviour + }; + /** * @param title The title of this test * @param model The model to process @@ -229,6 +237,17 @@ public T logs( LogCapture lc ) { return self(); } + /** + * Configures progress listening behaviour + * + * @param prg An object that will be informed as processing proceeds + * @return this + */ + public T listening( Progress prg ) { + progress = prg; + return self(); + } + /** * Defines subset of the system that is under test * @@ -253,6 +272,7 @@ public T behaviour( Consumer t ) { * @return The {@link Flow}s to process, in order */ protected Stream flows() { + progress.filtering(); // per system properties, find out which flows we want to exercise and save // those settings for future runs Filter fltr = new Filter( model ) @@ -270,6 +290,7 @@ protected Stream flows() { } toRun.addAll( deps ); + progress.ordering(); // find the execution order Order order = new Order( toRun.stream(), applicators.values() ); return order.order(); @@ -284,6 +305,7 @@ protected void process( Flow flow ) { if( reporting.writing() ) { logCapture.start( flow ); } + progress.flow( flow ); Collection toExercise = Flows.interactions( flow ) // exercise interactions that *enter* the system, not intra-system @@ -331,6 +353,8 @@ protected void process( Flow flow ) { finaliseReport( flow, reportUpdates, comparisonFailures, executionFailures, assertionCount.get() ); + progress.flowComplete( flow ); + // throw any deferred failures if( !executionFailures.isEmpty() ) { throw executionFailures.get( 0 ); @@ -346,12 +370,14 @@ protected void process( Flow flow ) { // but this will make things more obvious to whatever is driving the test skip( "No assertions made" ); } + } private int processInteraction( Flow flow, Interaction ntr, List actualMessages, List> reportUpdates, List skipReasons, List comparisonFailures, List executionFailures ) throws AssertionError { + progress.interaction( ntr ); // provoke the system with input data and capture the outputs Assertion assrt = new Assertion( flow, ntr, this ); @@ -427,7 +453,7 @@ private int processMessage( Flow flow, Assertion assertion, } ) ) ); } catch( AssertionError e ) { - if( !reporting.writing() && !Options.SUPPRESS_ASSERTION_FAILURE.isTrue() ) { + if( !reporting.writing() && !AssertionOptions.SUPPRESS_ASSERTION_FAILURE.isTrue() ) { // we're not generating a report or suppressing failures, so fail immediately throw e; } @@ -436,7 +462,7 @@ private int processMessage( Flow flow, Assertion assertion, comparisonFailures.add( e ); } catch( RuntimeException e ) { - if( !reporting.writing() && !Options.SUPPRESS_ASSERTION_FAILURE.isTrue() ) { + if( !reporting.writing() && !AssertionOptions.SUPPRESS_ASSERTION_FAILURE.isTrue() ) { // we're not generating a report, so fail immediately throw e; } @@ -479,7 +505,7 @@ private void finaliseReport( Flow flow, * @see #skip(String) */ private void checkPreconditions( Flow flow ) { - if( !Options.SUPPRESS_SYSTEM_CHECK.isTrue() ) { + if( !AssertionOptions.SUPPRESS_SYSTEM_CHECK.isTrue() ) { // If there are implied system dependencies that the system cannot satisfy... flow.implicit() .filter( a -> !systemUnderTest.contains( a ) ) @@ -537,6 +563,7 @@ private void applyContexts( Flow flow, List executionFailures @SuppressWarnings("unchecked") private void updateContext( C ctx ) { + progress.context( ctx ); Class ctxt = ctx.getClass(); Applicator apl = (Applicator) applicator( ctxt ); C current = (C) currentContext.get( ctxt ); @@ -548,6 +575,7 @@ private void updateContext( C ctx ) { private void removeContext( Class ctxt ) { Applicator apl = applicator( ctxt ); C current = (C) currentContext.remove( ctxt ); + progress.context( current ); apl.transition( current, null ); } @@ -564,7 +592,10 @@ private Map expectedResidue( Flow flow ) { Map expected = new HashMap<>(); flow.residue() .filter( r -> checkers.containsKey( r.getClass() ) ) - .forEach( r -> expected.put( r, checker( r ).expected( r ) ) ); + .forEach( r -> { + progress.before( r ); + expected.put( r, checker( r ).expected( r ) ); + } ); return expected; } @@ -580,6 +611,7 @@ private int checkResidue( AtomicInteger assertionCount = new AtomicInteger(); expectedResidue.forEach( ( residue, expected ) -> { + progress.after( residue ); byte[] harvested = null; try { harvested = checker( residue ).actual( residue, actualMessages ); @@ -686,6 +718,7 @@ private LogEvent error( String msg ) { } private enum MessageAssertion { + REQUEST( a -> a.actual().request(), a -> a.expected().request(), @@ -744,6 +777,7 @@ private void checkResult( Flow flow, Interaction interaction, String type, Messa } private static class CheckMessages { + public final String fullActual; public final String maskedExpect; public final String maskedActual; @@ -775,7 +809,7 @@ private void report( Consumer data, boolean error ) { String testTitle = title; // work out what the report directory should be called - String name = Options.REPORT_NAME.value(); + String name = AssertionOptions.REPORT_NAME.value(); if( name == null ) { name = RUN_DATETIME.get(); } @@ -791,7 +825,7 @@ private void report( Consumer data, boolean error ) { } report = new Writer( model.title(), testTitle, - Paths.get( Options.ARTIFACT_DIR.value() ).resolve( name ) ); + Paths.get( AssertionOptions.ARTIFACT_DIR.value() ).resolve( name ) ); alreadyBrowsing = false; } data.accept( report ); diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Assertion.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Assertion.java index 9abae1e9c0..a41d240f79 100644 --- a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Assertion.java +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Assertion.java @@ -1,12 +1,15 @@ + package com.mastercard.test.flow.assrt; import static java.nio.charset.StandardCharsets.UTF_8; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -27,7 +30,10 @@ public class Assertion { private final AbstractFlocessor flocessor; private final Actual actual = new Actual(); - private final List children = new ArrayList<>(); + /** + * Assertion peers for interaction structure + */ + private final Map children = new HashMap<>(); /** * @param flow The context for the {@link Interaction} @@ -68,7 +74,7 @@ public Actual actual() { } /** - * Allows assertion on downstream interactions + * Allows assertion on child interactions * * @param selector Returns true for the child interaction that you * want to assert on @@ -77,11 +83,25 @@ public Actual actual() { public Stream assertChildren( Predicate selector ) { return expected.children() .filter( selector ) - .map( i -> { - Assertion a = new Assertion( flow, i, flocessor ); - children.add( a ); - return a; - } ); + .map( i -> children.computeIfAbsent( i, + ntr -> new Assertion( flow, ntr, flocessor ) ) ); + } + + /** + * Allows assertion on all downstream interactions + * + * @return every downstream interaction as an {@link Assertion}, in + * breadth-first order and ready for population + */ + public Stream assertDownstream() { + return assertDownstream( new ArrayList<>() ).stream(); + } + + private Collection assertDownstream( Collection accumulator ) { + Set childPeers = assertChildren( i -> true ).collect( Collectors.toSet() ); + accumulator.addAll( childPeers ); + childPeers.forEach( c -> c.assertDownstream( accumulator ) ); + return accumulator; } /** @@ -142,8 +162,7 @@ public Assertion assertConsequests( Function> data ) { return this; } - private Map> - findSystemBoundaries( Map> exits ) { + private Map> findSystemBoundaries( Map> exits ) { if( !flocessor.system().contains( expected().responder() ) ) { exits.computeIfAbsent( expected().responder(), a -> new ArrayList<>() ).add( this ); @@ -162,7 +181,7 @@ public Assertion assertConsequests( Function> data ) { */ List collect( List l ) { l.add( this ); - children.forEach( c -> c.collect( l ) ); + children.values().forEach( c -> c.collect( l ) ); return l; } } diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Options.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AssertionOptions.java similarity index 90% rename from assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Options.java rename to assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AssertionOptions.java index d1bfa6b08a..1237dae5eb 100644 --- a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Options.java +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AssertionOptions.java @@ -9,13 +9,13 @@ /** * Options for {@link Flow} assertion behaviour */ -public enum Options implements Option { +public enum AssertionOptions implements Option { /** - * Controls where the name under which the execution report is saved + * Controls where the directory name under which the execution report is saved */ REPORT_NAME(b -> b - .property( "mctf.report.name" ) + .property( "mctf.report.dir" ) .description( "The path from the artifact directory to the report destination" )), /** @@ -71,13 +71,13 @@ public enum Options implements Option { private final Option delegate; - Options( Consumer def ) { + AssertionOptions( Consumer def ) { Builder b = new Builder(); def.accept( b ); delegate = b; } - Options( Option delegate ) { + AssertionOptions( Option delegate ) { this.delegate = delegate; } diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/History.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/History.java index 660c91b226..9c1cf53f29 100644 --- a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/History.java +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/History.java @@ -95,7 +95,7 @@ public void clear() { */ public Optional skipReason( Flow flow, State statefulness, Set system ) { - if( !Options.SUPPRESS_BASIS_CHECK.isTrue() ) { + if( !AssertionOptions.SUPPRESS_BASIS_CHECK.isTrue() ) { Optional basisFailure = Flows.ancestors( flow ) .map( this::get ) // One of our ancestors failed, so we are likely to fail in the same way. Let's @@ -109,7 +109,7 @@ public Optional skipReason( Flow flow, State statefulness, Set sy } } - if( statefulness == State.FUL && !Options.SUPPRESS_DEPENDENCY_CHECK.isTrue() ) { + if( statefulness == State.FUL && !AssertionOptions.SUPPRESS_DEPENDENCY_CHECK.isTrue() ) { // the system is stateful and the check has not been suppressed... Optional depFailure = flow.dependencies() .map( d -> d.source().flow() ) diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Progress.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Progress.java new file mode 100644 index 0000000000..914b34e4a8 --- /dev/null +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Progress.java @@ -0,0 +1,81 @@ + +package com.mastercard.test.flow.assrt; + +import com.mastercard.test.flow.Context; +import com.mastercard.test.flow.Flow; +import com.mastercard.test.flow.Interaction; +import com.mastercard.test.flow.Residue; + +/** + * Implementations will be informed as {@link Flow} are processed + */ +public interface Progress { + + /** + * Called when {@link Flow} filtering is being applied + */ + default void filtering() { + // no-op + } + + /** + * Called when a processing order is being computed + */ + default void ordering() { + // no-op + } + + /** + * Called when a {@link Flow} begins processing + * + * @param flow The {@link Flow} that is about to be processed + */ + default void flow( Flow flow ) { + // no-op + } + + /** + * Called when a {@link Context} is about to be applied + * + * @param context The {@link Context} that is about to be applied + */ + default void context( Context context ) { + // no-op + } + + /** + * Called when an {@link Interaction} is about to be processed + * + * @param interaction The {@link Interaction} that is about to be processed + */ + default void interaction( Interaction interaction ) { + // no-op + } + + /** + * Called the initial state of a {@link Residue} is about to be extracted + * + * @param residue The {@link Residue} that is about to be checked + */ + default void before( Residue residue ) { + // no-op + } + + /** + * Called when the final state of a {@link Residue} is about to be checked + * + * @param residue The {@link Residue} that is about to be checked + */ + default void after( Residue residue ) { + // no-op + } + + /** + * Called when a {@link Flow} has completed processing + * + * @param flow The {@link Flow} that has completed + */ + default void flowComplete( Flow flow ) { + // no-op + } +} diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java index ccb063ecf2..a66c6c0a3a 100644 --- a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt; import java.nio.file.Path; @@ -31,8 +32,8 @@ public class Replay { public static final String REPLAYED_SUFFIX = "_replay"; /** - * The value to supply to {@link Options#REPLAY} to replay from the latest local - * execution report + * The value to supply to {@link AssertionOptions#REPLAY} to replay from the + * latest local execution report */ public static final String LATEST = "latest"; @@ -44,7 +45,7 @@ public class Replay { * @return true if the test will be replaying historical data */ public static boolean isActive() { - return Options.REPLAY.value() != null; + return AssertionOptions.REPLAY.value() != null; } /** @@ -54,7 +55,7 @@ public static boolean isActive() { * null if there is no such report */ public static String source() { - String src = Options.REPLAY.value(); + String src = AssertionOptions.REPLAY.value(); if( LATEST.equals( src ) ) { src = mostRecent(); } @@ -113,8 +114,8 @@ public String populate( Assertion t ) { // Check the interactions match if( !matches( t.expected(), fd.root ) ) { return String.format( "No data for interaction %s -> %s %s", - t.expected().requester(), - t.expected().responder(), + t.expected().requester().name(), + t.expected().responder().name(), t.expected().tags() ); } @@ -130,12 +131,12 @@ public String populate( Assertion t ) { * @return The most recent execution report */ private static final String mostRecent() { - Path report = Reader.mostRecent( Options.ARTIFACT_DIR.value(), + Path report = Reader.mostRecent( AssertionOptions.ARTIFACT_DIR.value(), // let's stick to primary sources of data p -> !p.getFileName().toString().endsWith( REPLAYED_SUFFIX ) ); if( report == null ) { throw new IllegalStateException( - "Failed to find execution report in " + Options.ARTIFACT_DIR.value() ); + "Failed to find execution report in " + AssertionOptions.ARTIFACT_DIR.value() ); } return report.toString(); } @@ -147,8 +148,8 @@ private static final String mostRecent() { */ private static boolean matches( Interaction ntr, InteractionData id ) { return id != null - && ntr.requester().toString().equals( id.requester ) - && ntr.responder().toString().equals( id.responder ) + && ntr.requester().name().equals( id.requester ) + && ntr.responder().name().equals( id.responder ) && ntr.tags().equals( id.tags ); } diff --git a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/log/Tail.java b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/log/Tail.java index e0a2afa0c9..8263586f4a 100644 --- a/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/log/Tail.java +++ b/assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/log/Tail.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt.log; import static java.nio.charset.StandardCharsets.UTF_8; @@ -163,6 +164,6 @@ private static String uncapturedContent( String line, Matcher m, String... group // everything after the last group unmatched.append( line.substring( ranges[ranges.length - 1] ) ); - return unmatched.toString(); + return unmatched.toString().trim(); } } diff --git a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/OptionsTest.java b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/AssertionOptionsTest.java similarity index 65% rename from assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/OptionsTest.java rename to assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/AssertionOptionsTest.java index 1b77c7dcfd..4da8bef441 100644 --- a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/OptionsTest.java +++ b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/AssertionOptionsTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -9,17 +10,18 @@ * Ensures that the table of system properties in the readme is accurate */ @SuppressWarnings("static-method") -class OptionsTest { +class AssertionOptionsTest { + /** * Exercises property access */ @Test void access() { - assertEquals( "mctf.report.name", - Options.REPORT_NAME.property() ); + assertEquals( "mctf.report.dir", + AssertionOptions.REPORT_NAME.property() ); assertEquals( "The path from the artifact directory to the report destination", - Options.REPORT_NAME.description() ); - assertEquals( null, Options.REPORT_NAME.defaultValue() ); + AssertionOptions.REPORT_NAME.description() ); + assertEquals( null, AssertionOptions.REPORT_NAME.defaultValue() ); } /** @@ -27,7 +29,7 @@ void access() { */ @Test void prefix() { - for( Options op : Options.values() ) { + for( AssertionOptions op : AssertionOptions.values() ) { assertTrue( op.property().startsWith( "mctf." ), "bad prefix on " + op ); } } @@ -37,6 +39,6 @@ void prefix() { */ @Test void artifactDir() { - assertEquals( "target/mctf", Options.ARTIFACT_DIR.value() ); + assertEquals( "target/mctf", AssertionOptions.ARTIFACT_DIR.value() ); } } diff --git a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/AssertionTest.java b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/AssertionTest.java index 9cb452d1bb..2f36371bb4 100644 --- a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/AssertionTest.java +++ b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/AssertionTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt; import static com.mastercard.test.flow.assrt.AbstractFlocessorTest.copypasta; @@ -86,4 +87,43 @@ void extraConsequests() { " | | extra |" ), copypasta( tf.events() ) ); } + + /** + * Adding assertions on the grandchild of the entrypoint + */ + @Test + void assertGrandchild() { + TestFlocessor tf = new TestFlocessor( "assertGrandchild", TestModel.abc() ) + .system( State.LESS, B, C ) + .behaviour( asrt -> { + // everything goes well at the entrypoint + asrt.actual() + .request( asrt.expected().request().content() ) + .response( asrt.expected().response().content() ); + + // and we've got a window into the system internals, so we can add intra-system + // assertions + // too + asrt.assertDownstream() + .peek( a -> System.out.println( "HEY! " + a ) ) + .filter( a -> a.expected().responder() == C ) + .forEach( a -> a.actual().request( a.expected().request().content() ) ); + } ); + + tf.execute(); + + assertEquals( copypasta( + "COMPARE abc []", + "com.mastercard.test.flow.assrt.TestModel.abc(TestModel.java:_) A->B [] request", + " | A request to B | A request to B |", + "", + "COMPARE abc []", + "com.mastercard.test.flow.assrt.TestModel.abc(TestModel.java:_) B->C [] request", + " | B request to C | B request to C |", + "", + "COMPARE abc []", + "com.mastercard.test.flow.assrt.TestModel.abc(TestModel.java:_) A->B [] response", + " | B response to A | B response to A |" ), + copypasta( tf.events() ) ); + } } diff --git a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/HistoryTest.java b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/HistoryTest.java index 44475760fa..febb6df548 100644 --- a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/HistoryTest.java +++ b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/HistoryTest.java @@ -67,13 +67,13 @@ void basisFailure() { "basis failure" ); try { - Options.SUPPRESS_BASIS_CHECK.set( "true" ); + AssertionOptions.SUPPRESS_BASIS_CHECK.set( "true" ); assertEquals( null, hst.skipReason( child, State.FUL, Collections.emptySet() ) .orElse( null ), "supressed basis failure" ); } finally { - Options.SUPPRESS_BASIS_CHECK.clear(); + AssertionOptions.SUPPRESS_BASIS_CHECK.clear(); } } @@ -115,14 +115,14 @@ void dependencyFailure() { .orElse( null ) ); try { - Options.SUPPRESS_DEPENDENCY_CHECK.set( "true" ); + AssertionOptions.SUPPRESS_DEPENDENCY_CHECK.set( "true" ); assertEquals( null, hst.skipReason( dependent, State.FUL, Collections.singleton( Actors.BEN ) ) .orElse( null ), "suppressed dependency failure" ); } finally { - Options.SUPPRESS_DEPENDENCY_CHECK.clear(); + AssertionOptions.SUPPRESS_DEPENDENCY_CHECK.clear(); } } diff --git a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReplayTest.java b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReplayTest.java index a6802649a5..1d370ff949 100644 --- a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReplayTest.java +++ b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/ReplayTest.java @@ -45,7 +45,7 @@ private static TestFlocessor build( String title, List behaviourLog ) { } private static Path generateReport( String title ) { - System.setProperty( Options.REPORT_NAME.property(), title ); + System.setProperty( AssertionOptions.REPORT_NAME.property(), title ); List behaviourLog = new ArrayList<>(); TestFlocessor tf = build( title, behaviourLog ); tf.execute(); @@ -78,8 +78,8 @@ private static Path generateReport( String title ) { */ @AfterEach void clearReplayProperty() { - System.clearProperty( Options.REPLAY.property() ); - System.clearProperty( Options.REPORT_NAME.property() ); + System.clearProperty( AssertionOptions.REPLAY.property() ); + System.clearProperty( AssertionOptions.REPORT_NAME.property() ); } /** @@ -89,7 +89,7 @@ void clearReplayProperty() { void isActive() { Assertions.assertFalse( Replay.isActive(), "before" ); - System.setProperty( Options.REPLAY.property(), "foo" ); + System.setProperty( AssertionOptions.REPLAY.property(), "foo" ); Assertions.assertTrue( Replay.isActive(), "after" ); } @@ -100,19 +100,19 @@ void isActive() { */ @Test void noLatest() { - QuietFiles.recursiveDelete( Paths.get( Options.ARTIFACT_DIR.value() ) ); - System.setProperty( Options.REPLAY.property(), Replay.LATEST ); + QuietFiles.recursiveDelete( Paths.get( AssertionOptions.ARTIFACT_DIR.value() ) ); + System.setProperty( AssertionOptions.REPLAY.property(), Replay.LATEST ); IllegalStateException ise = assertThrows( IllegalStateException.class, () -> build( "bar", null ) ); - assertEquals( "Failed to find execution report in " + Options.ARTIFACT_DIR.value(), + assertEquals( "Failed to find execution report in " + AssertionOptions.ARTIFACT_DIR.value(), ise.getMessage() ); - QuietFiles.createDirectories( Paths.get( Options.ARTIFACT_DIR.value() ) ); + QuietFiles.createDirectories( Paths.get( AssertionOptions.ARTIFACT_DIR.value() ) ); ise = assertThrows( IllegalStateException.class, () -> build( "bar", null ) ); - assertEquals( "Failed to find execution report in " + Options.ARTIFACT_DIR.value(), + assertEquals( "Failed to find execution report in " + AssertionOptions.ARTIFACT_DIR.value(), ise.getMessage() ); assertEquals( null, ise.getCause() ); } @@ -143,7 +143,7 @@ void latest() throws Exception { QuietFiles.recursiveDelete( noIndex.resolve( "index.html" ) ); // activate replay mode - System.setProperty( Options.REPLAY.property(), Replay.LATEST ); + System.setProperty( AssertionOptions.REPLAY.property(), Replay.LATEST ); // run again TestFlocessor tf = build( "bar", new ArrayList<>() ); @@ -175,7 +175,7 @@ void happy() { Path p = generateReport( "happy" ); // activate replay mode - System.setProperty( Options.REPLAY.property(), p.toString() ); + System.setProperty( AssertionOptions.REPLAY.property(), p.toString() ); // run again List behaviourLog = new ArrayList<>(); @@ -223,7 +223,7 @@ void missingInteraction() throws Exception { "\"tags\" : \\[ \\],", "\"tags\" : [ \"disguise\" ]," ); Files.write( detailPage, content.getBytes( UTF_8 ) ); - System.setProperty( Options.REPLAY.property(), p.toString() ); + System.setProperty( AssertionOptions.REPLAY.property(), p.toString() ); // run again List behaviourLog = new ArrayList<>(); @@ -265,7 +265,7 @@ void missingChildInteraction() throws Exception { " \"tags\" : \\[ \\],", " \"tags\" : [ \"disguise\" ]," ); Files.write( detailPage, content.getBytes( UTF_8 ) ); - System.setProperty( Options.REPLAY.property(), p.toString() ); + System.setProperty( AssertionOptions.REPLAY.property(), p.toString() ); // run again List behaviourLog = new ArrayList<>(); @@ -303,7 +303,7 @@ void missingFlowData() throws Exception { .resolve( "56B672A62688DE11DE0F318C151A98B6.html" ); Files.delete( detailPage ); - System.setProperty( Options.REPLAY.property(), p.toString() ); + System.setProperty( AssertionOptions.REPLAY.property(), p.toString() ); // run again List behaviourLog = new ArrayList<>(); @@ -342,7 +342,7 @@ void missingIndexEntry() throws Exception { "\"description\" : \"abc\"", "\"description\" : \"hidden\"" ); Files.write( indexPage, content.getBytes( UTF_8 ) ); - System.setProperty( Options.REPLAY.property(), p.toString() ); + System.setProperty( AssertionOptions.REPLAY.property(), p.toString() ); // run again List behaviourLog = new ArrayList<>(); @@ -376,12 +376,13 @@ void missingIndex() throws Exception { // delete the report index Files.delete( p.resolve( "index.html" ) ); - System.setProperty( Options.REPLAY.property(), p.toString() ); + System.setProperty( AssertionOptions.REPLAY.property(), p.toString() ); // try to run again IllegalStateException ise = assertThrows( IllegalStateException.class, () -> build( "blah", null ) ); - assertEquals( "No index data found in " + Options.ARTIFACT_DIR.value() + "/missingIndexEntry", + assertEquals( + "No index data found in " + AssertionOptions.ARTIFACT_DIR.value() + "/missingIndexEntry", ise.getMessage().replace( '\\', '/' ) ); } } diff --git a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/SupressionTest.java b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/SupressionTest.java index cb780301fc..5b0f7824ae 100644 --- a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/SupressionTest.java +++ b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/SupressionTest.java @@ -11,7 +11,8 @@ import com.mastercard.test.flow.assrt.AbstractFlocessor.State; /** - * Demonstrates the effects of the various behaviour suppression {@link Options} + * Demonstrates the effects of the various behaviour suppression + * {@link AssertionOptions} */ @SuppressWarnings("static-method") class SupressionTest { @@ -32,11 +33,11 @@ void assertionFailure() { } ); try { - Options.SUPPRESS_ASSERTION_FAILURE.set( "true" ); + AssertionOptions.SUPPRESS_ASSERTION_FAILURE.set( "true" ); tf.execute(); } finally { - Options.SUPPRESS_ASSERTION_FAILURE.clear(); + AssertionOptions.SUPPRESS_ASSERTION_FAILURE.clear(); } assertEquals( copypasta( @@ -65,11 +66,11 @@ void missingImplicit() { } ); try { - Options.SUPPRESS_SYSTEM_CHECK.set( "true" ); + AssertionOptions.SUPPRESS_SYSTEM_CHECK.set( "true" ); tf.execute(); } finally { - Options.SUPPRESS_SYSTEM_CHECK.clear(); + AssertionOptions.SUPPRESS_SYSTEM_CHECK.clear(); } assertEquals( copypasta( @@ -94,11 +95,11 @@ void failedBasis() { } ); try { - Options.SUPPRESS_BASIS_CHECK.set( "true" ); + AssertionOptions.SUPPRESS_BASIS_CHECK.set( "true" ); tf.execute(); } finally { - Options.SUPPRESS_BASIS_CHECK.clear(); + AssertionOptions.SUPPRESS_BASIS_CHECK.clear(); } assertEquals( copypasta( @@ -127,11 +128,11 @@ void failedDependency() { } ); try { - Options.SUPPRESS_DEPENDENCY_CHECK.set( "true" ); + AssertionOptions.SUPPRESS_DEPENDENCY_CHECK.set( "true" ); tf.execute(); } finally { - Options.SUPPRESS_DEPENDENCY_CHECK.clear(); + AssertionOptions.SUPPRESS_DEPENDENCY_CHECK.clear(); } assertEquals( copypasta( diff --git a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/log/TailTest.java b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/log/TailTest.java index 41275fb6b8..ef85bbc729 100644 --- a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/log/TailTest.java +++ b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/log/TailTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt.log; import static java.nio.charset.StandardCharsets.UTF_8; @@ -64,20 +65,20 @@ void tail() { log( "ERROR 007 bazsrc No-one sees this one" ); Assertions.assertEquals( "" - + "INFO/foosrc/002/ This line is after the tail started\n" - + "WARN/barsrc/003/ there's about to be some multline content!\n" + + "INFO/foosrc/002/This line is after the tail started\n" + + "WARN/barsrc/003/there's about to be some multline content!\n" + " Here is content from the previous line's event!\n" + " and there's more!\n" - + "INFO/foosrc/004/ here's another event\n" + + "INFO/foosrc/004/here's another event\n" + " here is more content from the previous event!\n" - + "TRACE/bazsrc/005/ This event will be shared by both flows", + + "TRACE/bazsrc/005/This event will be shared by both flows", ae.map( e -> String.format( "%s/%s/%s/%s", e.level, e.source, e.time, e.message ) ) .collect( Collectors.joining( "\n" ) ) ); Assertions.assertEquals( "" + "?/?/?/ here is more content from the previous event!\n" - + "TRACE/bazsrc/005/ This event will be shared by both flows\n" - + "TRACE/bazsrc/006/ This event is just for b", + + "TRACE/bazsrc/005/This event will be shared by both flows\n" + + "TRACE/bazsrc/006/This event is just for b", be.map( e -> String.format( "%s/%s/%s/%s", e.level, e.source, e.time, e.message ) ) .collect( Collectors.joining( "\n" ) ) ); } diff --git a/assert/assert-filter/README.md b/assert/assert-filter/README.md index 8d230dba80..7f7f2a9d6a 100644 --- a/assert/assert-filter/README.md +++ b/assert/assert-filter/README.md @@ -27,7 +27,7 @@ Some aspects of filtering behaviour can be controlled by system properties: | -------- | ----------- | | `mctf.filter.exclude` | A comma-separated list of tags values that flows must not have | | `mctf.filter.include` | A comma-separated list of tags values that flows must have | -| `mctf.filter.indices` | A comma-separated list of indices for flows to process | +| `mctf.filter.indices` | A comma-separated list of indices and index ranges for flows to process | | `mctf.filter.update` | Supply `true` to update filter values at runtime in the most appropriate interface.Supply `cli` to force use of the command-line interface or `gui` to force use of the graphical interface | | `mctf.filter.repeat` | Supply `true` to use the previous filters again | | `mctf.filter.fails` | Configures filters to repeat flows that did not pass assertion in a previous run. Supply the location of a report from which to extract results, or `latest` to extract from the most recent local report | @@ -42,7 +42,7 @@ Calling [`blockForUpdates()`][Filter!.blockForUpdates()] on a `Filter` instance -[Filter!.blockForUpdates()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L107-L120,107-120 +[Filter!.blockForUpdates()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L131-L144,131-144 @@ -57,7 +57,7 @@ Calling [`load()`][Filter!.load()] on a filter (when system property `mctf.filte -[Filter!.save()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L131-L137,131-137 -[Filter!.load()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L77-L91,77-91 +[Filter!.save()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L155-L161,155-161 +[Filter!.load()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L101-L115,101-115 diff --git a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java index abcddcde2d..2d09503f09 100644 --- a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java +++ b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java @@ -14,7 +14,10 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import com.fasterxml.jackson.annotation.JsonProperty; @@ -47,11 +50,32 @@ public class Filter { private final Set excludeTags = FilterOptions.EXCLUDE_TAGS.asList() .map( String::trim ) .collect( toCollection( TreeSet::new ) ); - private final Set indices = FilterOptions.INDICES.asList() - .map( String::trim ) - .filter( s -> s.matches( "\\d+" ) ) - .map( Integer::valueOf ) - .collect( toCollection( TreeSet::new ) ); + private static final Pattern INDEX_PTRN = Pattern.compile( "(\\d+)" ); + private static final Pattern RANGE_PTRN = Pattern.compile( "(\\d+)-(\\d+)" ); + private final Set indices = parseIndices( FilterOptions.INDICES.value() ); + + private static final Set parseIndices( String property ) { + Set indices = new TreeSet<>(); + if( property != null ) { + for( String s : property.split( "," ) ) { + s = s.trim(); + Matcher im = INDEX_PTRN.matcher( s ); + if( im.matches() ) { + indices.add( Integer.parseInt( im.group( 1 ) ) ); + } + else { + Matcher rm = RANGE_PTRN.matcher( s ); + if( rm.matches() ) { + IntStream.rangeClosed( + Integer.parseInt( rm.group( 1 ) ), + Integer.parseInt( rm.group( 2 ) ) ) + .forEach( indices::add ); + } + } + } + } + return indices; + } /** * The set of flows that pass the tag filters. This gets used a lot so we cache diff --git a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/FilterOptions.java b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/FilterOptions.java index 90dfe0bc40..e0d7ec53dc 100644 --- a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/FilterOptions.java +++ b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/FilterOptions.java @@ -32,7 +32,7 @@ public enum FilterOptions implements Option { */ INDICES(b -> b .property( "mctf.filter.indices" ) - .description( "A comma-separated list of indices for flows to process" )), + .description( "A comma-separated list of indices and index ranges for flows to process" )), /** * Determines if the flow {@link Filter} CLI is shown diff --git a/doc/src/main/markdown/further.md b/doc/src/main/markdown/further.md index 2f4264477f..362967d48b 100644 --- a/doc/src/main/markdown/further.md +++ b/doc/src/main/markdown/further.md @@ -13,7 +13,7 @@ By default the report will be saved to a timestamped directory under `target/mct -[AbstractFlocessor.reporting(Reporting)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L155-L162,155-162 +[AbstractFlocessor.reporting(Reporting)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L163-L170,163-170 [Reporting]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Reporting.java @@ -35,7 +35,7 @@ You can use [`Replay.isActive()`][Replay.isActive()] in your assertion component -[Replay.isActive()]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java#L39-L46,39-46 +[Replay.isActive()]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Replay.java#L40-L47,40-47 @@ -169,14 +169,14 @@ Interaction structure can be changed when deriving a flow via the following meth One of the implications of a more complicated interaction structure is that each actor in the system has more outputs in need of assertion: when an actor is provoked with a request it will produce a response, but it will _also_ produce requests to other actors that should be checked against the system model. -A convenient way to perform this assertion is to capture the downstream requests in a [`Consequests`][Consequests] instance, then provide the captured data to the assertion component by calling [`Assertion.assertConsequests()`][Assertion.assertConsequests(Consequests)]. +A convenient way to perform this assertion is to capture the downstream requests in a [`Consequests`][Consequests] instance, then provide the captured data to the assertion component by calling [`Assertion.assertConsequests()`][Assertion!.assertConsequests(Consequests)]. You can see this happening in [`BenTest`][BenTest]. [Consequests]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Consequests.java -[Assertion.assertConsequests(Consequests)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Assertion.java#L87-L93,87-93 +[Assertion!.assertConsequests(Consequests)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Assertion.java#L107-L113,107-113 @@ -230,17 +230,17 @@ Consider the following worked example: [flow.Unpredictable]: ../../../../api/src/main/java/com/mastercard/test/flow/Unpredictable.java [AbstractMessage.masking(Unpredictable,UnaryOperator)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java#L46-L53,46-53 -[AbstractFlocessor.masking(Unpredictable...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L167-L174,167-174 +[AbstractFlocessor.masking(Unpredictable...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L175-L182,175-182 [mask.BenSys]: ../../test/java/com/mastercard/test/flow/doc/mask/BenSys.java [mask.DieSys]: ../../test/java/com/mastercard/test/flow/doc/mask/DieSys.java [mask.Unpredictables]: ../../test/java/com/mastercard/test/flow/doc/mask/Unpredictables.java [AbstractMessage.masking(Unpredictable,UnaryOperator)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/AbstractMessage.java#L46-L53,46-53 [Rolling?d\+]: ../../test/java/com/mastercard/test/flow/doc/mask/Rolling.java#L30,30 [msg.Mask]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java -[msg.Mask.andThen(Consumer)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java#L289-L291,289-291 +[msg.Mask.andThen(Consumer)]: ../../../../message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java#L290-L292,290-292 [BenDiceTest?masking]: ../../test/java/com/mastercard/test/flow/doc/mask/BenDiceTest.java#L31,31 [BenTest]: ../../test/java/com/mastercard/test/flow/doc/mask/BenTest.java -[AbstractFlocessor.masking(Unpredictable...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L167-L174,167-174 +[AbstractFlocessor.masking(Unpredictable...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L175-L182,175-182 @@ -260,7 +260,7 @@ You can see usage of these types in the example system: [flow.Context]: ../../../../api/src/main/java/com/mastercard/test/flow/Context.java [Builder.context(Context)]: ../../../../builder/src/main/java/com/mastercard/test/flow/builder/Builder.java#L225-L232,225-232 [assrt.Applicator]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Applicator.java -[AbstractFlocessor.applicators(Applicator...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L194-L200,194-200 +[AbstractFlocessor.applicators(Applicator...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L202-L208,202-208 [model.ctx.QueueProcessing]: ../../../../example/app-model/src/main/java/com/mastercard/test/flow/example/app/model/ctx/QueueProcessing.java [QueueProcessingApplicator]: ../../../../example/app-assert/src/main/java/com/mastercard/test/flow/example/app/assrt/ctx/QueueProcessingApplicator.java @@ -283,7 +283,7 @@ You can see usage of these types in the example system: [flow.Residue]: ../../../../api/src/main/java/com/mastercard/test/flow/Residue.java [assrt.Checker]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/Checker.java -[AbstractFlocessor.checkers(Checker...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L207-L213,207-213 +[AbstractFlocessor.checkers(Checker...)]: ../../../../assert/assert-core/src/main/java/com/mastercard/test/flow/assrt/AbstractFlocessor.java#L215-L221,215-221 [model.rsd.DBItems]: ../../../../example/app-model/src/main/java/com/mastercard/test/flow/example/app/model/rsd/DBItems.java [DBItemsChecker]: ../../../../example/app-assert/src/main/java/com/mastercard/test/flow/example/app/assrt/rsd/DBItemsChecker.java diff --git a/doc/src/test/java/com/mastercard/test/flow/doc/AssertOptionsTest.java b/doc/src/test/java/com/mastercard/test/flow/doc/AssertOptionsTest.java index 8050eef235..b07045f273 100644 --- a/doc/src/test/java/com/mastercard/test/flow/doc/AssertOptionsTest.java +++ b/doc/src/test/java/com/mastercard/test/flow/doc/AssertOptionsTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test; -import com.mastercard.test.flow.assrt.Options; +import com.mastercard.test.flow.assrt.AssertionOptions; import com.mastercard.test.flow.assrt.filter.FilterOptions; import com.mastercard.test.flow.util.Option; @@ -37,7 +37,7 @@ void core() throws Exception { // overlaps, collate them together before regenerating the documentation Map aggregated = new TreeMap<>(); Stream.of( - Options.values(), + AssertionOptions.values(), FilterOptions.values() ) .flatMap( Stream::of ) .forEach( o -> aggregated.put( o.property(), o ) ); diff --git a/doc/src/test/java/com/mastercard/test/flow/doc/ModuleDiagramTest.java b/doc/src/test/java/com/mastercard/test/flow/doc/ModuleDiagramTest.java index 25b4cf0d50..e44d2cbea7 100644 --- a/doc/src/test/java/com/mastercard/test/flow/doc/ModuleDiagramTest.java +++ b/doc/src/test/java/com/mastercard/test/flow/doc/ModuleDiagramTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.doc; import static java.util.stream.Collectors.toSet; @@ -17,22 +18,20 @@ import org.junit.jupiter.api.Test; /** - * Ensures that the project diagram in the main readme accurately reflects the - * POM structure. Spiders over the pom structure and regenerates the mermaid - * diagrams. These tests will fail if that regeneration makes any changes. It - * should pass if you just run it again, but do be sure to check the changes are - * desired before you commit them. + * Ensures that the project diagram in the main readme accurately reflects the POM structure. + * Spiders over the pom structure and regenerates the mermaid diagrams. These tests will fail if + * that regeneration makes any changes. It should pass if you just run it again, but do be sure to + * check the changes are desired before you commit them. */ -@SuppressWarnings("static-method") +@SuppressWarnings( "static-method" ) class ModuleDiagramTest { /** - * Projects that aren't really interesting but that stink up the diagram with - * all their links + * Projects that aren't really interesting but that stink up the diagram with all their links */ private static final Set OCCULTED = Stream.of( - "com.mastercard.test.flow:aggregator", - "com.mastercard.test.flow:doc" ).collect( toSet() ); + "com.mastercard.test.flow:aggregator", + "com.mastercard.test.flow:doc" ).collect( toSet() ); private static final Map> SCOPES; static { @@ -50,9 +49,9 @@ class ModuleDiagramTest { @Test void framework() throws Exception { Util.insert( Paths.get( "../README.md" ), - "", - s -> diagram( false, "com.mastercard.test.flow" ), - "" ); + "", + s -> diagram( false, "com.mastercard.test.flow" ), + "" ); } /** @@ -63,14 +62,14 @@ void framework() throws Exception { @Test void example() throws Exception { Util.insert( Paths.get( "../example/README.md" ), - "", - s -> diagram( true, - "com.mastercard.test.flow", - "com.mastercard.test.flow.example" ), - "" ); + "", + s -> diagram( true, + "com.mastercard.test.flow", + "com.mastercard.test.flow.example" ), + "" ); } - private static String diagram( boolean intergroupLinks, String... groupIDs ) { + private static String diagram( boolean intergroupLinks, String ... groupIDs ) { PomData root = new PomData( null, Paths.get( "../pom.xml" ) ); Set artifacts = new HashSet<>(); @@ -82,37 +81,37 @@ private static String diagram( boolean intergroupLinks, String... groupIDs ) { Map> links = new HashMap<>(); root.visit( pd -> pd.dependencies() - .filter( dd -> artifacts.contains( dd.coords() ) ) - .filter( dd -> artifacts.contains( pd.coords() ) ) - .filter( dd -> SCOPES.get( pd.groupId() ).contains( dd.scope() ) ) - .forEach( dd -> links - .computeIfAbsent( dd.coords(), g -> new ArrayList<>() ) - .add( new Link( - dd.groupId(), - dd.artifactId(), - "compile".equals( dd.scope() ) ? " --> " : " -.-> ", - pd.groupId(), - pd.artifactId() ) ) ) ); + .filter( dd -> artifacts.contains( dd.coords() ) ) + .filter( dd -> artifacts.contains( pd.coords() ) ) + .filter( dd -> SCOPES.get( pd.groupId() ).contains( dd.scope() ) ) + .forEach( dd -> links + .computeIfAbsent( dd.coords(), g -> new ArrayList<>() ) + .add( new Link( + dd.groupId(), + dd.artifactId(), + "compile".equals( dd.scope() ) ? " --> " : " -.-> ", + pd.groupId(), + pd.artifactId() ) ) ) ); StringBuilder mermaid = new StringBuilder( "```mermaid\ngraph LR\n" ); - for( String groupID : groupIDs ) { + for ( String groupID : groupIDs ) { mermaid.append( " subgraph " ).append( groupID ).append( "\n" ); groups.get( groupID ) - .stream() - .sorted( Comparator.comparing( PomData::artifactId ) ) - .forEach( pom -> links.getOrDefault( pom.coords(), Collections.emptyList() ).stream() - .filter( Link::intraGroup ) - .forEach( l -> mermaid.append( " " ).append( l ) ) ); + .stream() + .sorted( Comparator.comparing( PomData::artifactId ) ) + .forEach( pom -> links.getOrDefault( pom.coords(), Collections.emptyList() ).stream() + .filter( Link::intraGroup ) + .forEach( l -> mermaid.append( " " ).append( l ) ) ); mermaid.append( " end\n" ); } - if( intergroupLinks ) { + if ( intergroupLinks ) { links.values().stream() - .flatMap( List::stream ) - .sorted( Comparator.comparing( Link::fromArtifactId ) ) - .filter( l -> !l.intraGroup() ) - .forEach( l -> mermaid.append( l ) ); + .flatMap( List::stream ) + .sorted( Comparator.comparing( Link::fromArtifactId ) ) + .filter( l -> !l.intraGroup() ) + .forEach( l -> mermaid.append( l ) ); } mermaid.append( "```" ); @@ -120,6 +119,7 @@ private static String diagram( boolean intergroupLinks, String... groupIDs ) { } private static class Link { + private final String fromGroup; private final String fromArtifact; private final String type; @@ -127,14 +127,12 @@ private static class Link { private final String toArtifact; protected Link( String fromGroup, String fromArtifact, String type, String toGroup, - String toArtifact ) { + String toArtifact ) { this.fromGroup = fromGroup; this.fromArtifact = fromArtifact; this.type = type; this.toGroup = toGroup; this.toArtifact = toArtifact; - - System.out.println( this + " " + intraGroup() ); } public boolean intraGroup() { diff --git a/example/app-assert/src/main/java/com/mastercard/test/flow/example/app/assrt/AbstractServiceTest.java b/example/app-assert/src/main/java/com/mastercard/test/flow/example/app/assrt/AbstractServiceTest.java index 624f891da5..9ebc065f99 100644 --- a/example/app-assert/src/main/java/com/mastercard/test/flow/example/app/assrt/AbstractServiceTest.java +++ b/example/app-assert/src/main/java/com/mastercard/test/flow/example/app/assrt/AbstractServiceTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.example.app.assrt; import static com.mastercard.test.flow.assrt.Reporting.FAILURES; @@ -21,7 +22,7 @@ import com.mastercard.test.flow.assrt.AbstractFlocessor.State; import com.mastercard.test.flow.assrt.Assertion; -import com.mastercard.test.flow.assrt.Options; +import com.mastercard.test.flow.assrt.AssertionOptions; import com.mastercard.test.flow.assrt.Replay; import com.mastercard.test.flow.assrt.junit5.Flocessor; import com.mastercard.test.flow.example.app.assrt.ctx.UpnessApplicator; @@ -45,8 +46,8 @@ public abstract class AbstractServiceTest { private final Logger logger; static { - if( Options.REPORT_NAME.value() == null ) { - Options.REPORT_NAME.set( "latest" ); + if( AssertionOptions.REPORT_NAME.value() == null ) { + AssertionOptions.REPORT_NAME.set( "latest" ); } } diff --git a/example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java b/example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java index e4e8320701..47d316302d 100644 --- a/example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java +++ b/example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.example.app.itest; import static com.mastercard.test.flow.assrt.Reporting.FAILURES; @@ -30,7 +31,7 @@ import com.mastercard.test.flow.Actor; import com.mastercard.test.flow.assrt.AbstractFlocessor.State; -import com.mastercard.test.flow.assrt.Options; +import com.mastercard.test.flow.assrt.AssertionOptions; import com.mastercard.test.flow.assrt.Replay; import com.mastercard.test.flow.assrt.junit5.Flocessor; import com.mastercard.test.flow.example.app.assrt.Browser; @@ -53,13 +54,14 @@ @SuppressWarnings("static-method") @ExtendWith(Browser.class) class IntegrationTest { + private static final Logger LOG = LoggerFactory.getLogger( IntegrationTest.class ); private static final ClusterManager clusterManager = new ClusterManager(); static { - if( Options.REPORT_NAME.value() == null ) { - Options.REPORT_NAME.set( "latest" ); + if( AssertionOptions.REPORT_NAME.value() == null ) { + AssertionOptions.REPORT_NAME.set( "latest" ); } } diff --git a/example/app-store/src/test/java/com/mastercard/test/flow/example/app/store/QueryTest.java b/example/app-store/src/test/java/com/mastercard/test/flow/example/app/store/QueryTest.java index a7bd90a027..3bb70983b1 100644 --- a/example/app-store/src/test/java/com/mastercard/test/flow/example/app/store/QueryTest.java +++ b/example/app-store/src/test/java/com/mastercard/test/flow/example/app/store/QueryTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.example.app.store; import static com.mastercard.test.flow.assrt.Reporting.FAILURES; @@ -38,8 +39,8 @@ import com.mastercard.test.flow.Interaction; import com.mastercard.test.flow.assrt.AbstractFlocessor.State; +import com.mastercard.test.flow.assrt.AssertionOptions; import com.mastercard.test.flow.assrt.Consequests; -import com.mastercard.test.flow.assrt.Options; import com.mastercard.test.flow.assrt.Replay; import com.mastercard.test.flow.assrt.junit5.Flocessor; import com.mastercard.test.flow.example.app.Store; @@ -60,6 +61,7 @@ */ @SuppressWarnings("static-method") class QueryTest { + private static final Logger LOG = LoggerFactory.getLogger( QueryTest.class ); private static final DataSource db = Mockito.mock( DataSource.class ); @@ -67,8 +69,8 @@ class QueryTest { private static final Instance service = new Main( db ).build(); static { - if( Options.REPORT_NAME.value() == null ) { - Options.REPORT_NAME.set( "query_latest" ); + if( AssertionOptions.REPORT_NAME.value() == null ) { + AssertionOptions.REPORT_NAME.set( "query_latest" ); } } diff --git a/message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java b/message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java index 040b8c2643..9843aa6b85 100644 --- a/message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java +++ b/message/message-core/src/main/java/com/mastercard/test/flow/msg/Mask.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.msg; import static java.util.stream.Collectors.toList; diff --git a/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpMsg.java b/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpMsg.java index b7e011c361..71e94b3941 100644 --- a/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpMsg.java +++ b/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpMsg.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.msg.http; import static java.nio.charset.StandardCharsets.UTF_8; @@ -273,9 +274,16 @@ public String version() { } /** - * @return message body + * @return message body as a {@link Message} object */ public Optional body() { return body; } + + /** + * @return The text of the message body as a string + */ + public String bodyText() { + return body().map( ExposedMasking::assertable ).orElse( "" ); + } } diff --git a/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpReq.java b/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpReq.java index 4f8e734e10..49464c73b1 100644 --- a/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpReq.java +++ b/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpReq.java @@ -75,7 +75,10 @@ public HttpReq( byte[] content, Function bodyParse ) { } } - private HttpReq( HttpReq parent ) { + /** + * @param parent content basis + */ + protected HttpReq( HttpReq parent ) { super( parent ); } diff --git a/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpRes.java b/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpRes.java index 4c90989072..1a16437f68 100644 --- a/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpRes.java +++ b/message/message-http/src/main/java/com/mastercard/test/flow/msg/http/HttpRes.java @@ -20,7 +20,7 @@ public class HttpRes extends HttpMsg { private static final Pattern RES_PATTERN = Pattern - .compile( "^(?\\S+?) (?\\S+?) (?.*?)\r\n" + .compile( "^(?\\S+?) (?\\S+) ?(?.*?)\r\n" + "(?.*?)\r\n" + "\r\n" + "(?.*)$", Pattern.DOTALL ); @@ -64,7 +64,10 @@ public HttpRes( byte[] content, Function bodyParse ) { } } - private HttpRes( HttpRes parent ) { + /** + * @param parent content basis + */ + protected HttpRes( HttpRes parent ) { super( parent ); } @@ -120,11 +123,11 @@ protected boolean isHttpField( String field ) { @Override protected String serialise( String bodyContent, boolean wireFormat ) { return String.format( "" - + "%s %s %s\r\n" // response line + + "%s %s%s%s\r\n" // response line + "%s" // headers (will supply their own line endings) + "\r\n" // empty line + "%s", // body, - version(), status(), statusText(), + version(), status(), statusText().isEmpty() ? "" : " ", statusText(), headers().entrySet().stream() .map( e -> String.format( "%s: %s\r\n", e.getKey(), diff --git a/message/message-http/src/test/java/com/mastercard/test/flow/msg/http/HttpResTest.java b/message/message-http/src/test/java/com/mastercard/test/flow/msg/http/HttpResTest.java index 77ff1bf971..cc6af5fbc2 100644 --- a/message/message-http/src/test/java/com/mastercard/test/flow/msg/http/HttpResTest.java +++ b/message/message-http/src/test/java/com/mastercard/test/flow/msg/http/HttpResTest.java @@ -23,7 +23,7 @@ class HttpResTest { @Test void empty() { assertEquals( "" - + " \r\n" + + " \r\n" + "\r\n", new HttpRes().assertable() ); } @@ -99,6 +99,29 @@ void parse() { new HttpRes( bytes, Json::new ).assertable() ); } + /** + * Shows that we can cope with responses that lack the status text + */ + @Test + void missingStatusText() { + byte[] bytes = ("" + + "HTTP/1.1 418\r\n" + + "key: value\r\n" + + "\r\n" + + "{\n" + + " \"body\" : \"content\"\n" + + "}").getBytes( UTF_8 ); + + assertEquals( "" + + "HTTP/1.1 418\r\n" + + "key: value\r\n" + + "\r\n" + + "{\n" + + " \"body\" : \"content\"\n" + + "}", + new HttpRes( bytes, Json::new ).assertable() ); + } + /** * What happens when the body is not parseable as the expected type */ diff --git a/message/message-web/README.md b/message/message-web/README.md index 2de7d27137..617d4158b6 100644 --- a/message/message-web/README.md +++ b/message/message-web/README.md @@ -126,7 +126,7 @@ if( assrt.expected().request() instanceof WebSequence response = results.process( driver ); } ``` -[Snippet context](../../example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java#L152-L163,152-163) +[Snippet context](../../example/app-itest/src/test/java/com/mastercard/test/flow/example/app/itest/IntegrationTest.java#L154-L165,154-165) From 3963f65d8ec1da2209e3d0a6f3cbd605c6919dd3 Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Tue, 13 Sep 2022 14:19:52 +0100 Subject: [PATCH 5/8] mutant --- .../test/flow/doc/ModuleDiagramTest.java | 82 ++++++++++--------- .../test/flow/msg/http/HttpReqTest.java | 9 ++ 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/doc/src/test/java/com/mastercard/test/flow/doc/ModuleDiagramTest.java b/doc/src/test/java/com/mastercard/test/flow/doc/ModuleDiagramTest.java index 2ed7e09d38..1b6f973281 100644 --- a/doc/src/test/java/com/mastercard/test/flow/doc/ModuleDiagramTest.java +++ b/doc/src/test/java/com/mastercard/test/flow/doc/ModuleDiagramTest.java @@ -18,20 +18,22 @@ import org.junit.jupiter.api.Test; /** - * Ensures that the project diagram in the main readme accurately reflects the POM structure. - * Spiders over the pom structure and regenerates the mermaid diagrams. These tests will fail if - * that regeneration makes any changes. It should pass if you just run it again, but do be sure to - * check the changes are desired before you commit them. + * Ensures that the project diagram in the main readme accurately reflects the + * POM structure. Spiders over the pom structure and regenerates the mermaid + * diagrams. These tests will fail if that regeneration makes any changes. It + * should pass if you just run it again, but do be sure to check the changes are + * desired before you commit them. */ -@SuppressWarnings( "static-method" ) +@SuppressWarnings("static-method") class ModuleDiagramTest { /** - * Projects that aren't really interesting but that stink up the diagram with all their links + * Projects that aren't really interesting but that stink up the diagram with + * all their links */ private static final Set OCCULTED = Stream.of( - "com.mastercard.test.flow:aggregator", - "com.mastercard.test.flow:doc" ).collect( toSet() ); + "com.mastercard.test.flow:aggregator", + "com.mastercard.test.flow:doc" ).collect( toSet() ); /** * Maps from the groupID to the scope of links that we're interested in within @@ -59,9 +61,9 @@ class ModuleDiagramTest { @Test void framework() throws Exception { Util.insert( Paths.get( "../README.md" ), - "", - s -> diagram( false, "com.mastercard.test.flow" ), - "" ); + "", + s -> diagram( false, "com.mastercard.test.flow" ), + "" ); } /** @@ -72,14 +74,14 @@ void framework() throws Exception { @Test void example() throws Exception { Util.insert( Paths.get( "../example/README.md" ), - "", - s -> diagram( true, - "com.mastercard.test.flow", - "com.mastercard.test.flow.example" ), - "" ); + "", + s -> diagram( true, + "com.mastercard.test.flow", + "com.mastercard.test.flow.example" ), + "" ); } - private static String diagram( boolean intergroupLinks, String ... groupIDs ) { + private static String diagram( boolean intergroupLinks, String... groupIDs ) { PomData root = new PomData( null, Paths.get( "../pom.xml" ) ); Set artifacts = new HashSet<>(); @@ -91,37 +93,37 @@ private static String diagram( boolean intergroupLinks, String ... groupIDs ) { Map> links = new HashMap<>(); root.visit( pd -> pd.dependencies() - .filter( dd -> artifacts.contains( dd.coords() ) ) - .filter( dd -> artifacts.contains( pd.coords() ) ) - .filter( dd -> SCOPES.get( pd.groupId() ).contains( dd.scope() ) ) - .forEach( dd -> links - .computeIfAbsent( dd.coords(), g -> new ArrayList<>() ) - .add( new Link( - dd.groupId(), - dd.artifactId(), - "compile".equals( dd.scope() ) ? " --> " : " -.-> ", - pd.groupId(), - pd.artifactId() ) ) ) ); + .filter( dd -> artifacts.contains( dd.coords() ) ) + .filter( dd -> artifacts.contains( pd.coords() ) ) + .filter( dd -> SCOPES.get( pd.groupId() ).contains( dd.scope() ) ) + .forEach( dd -> links + .computeIfAbsent( dd.coords(), g -> new ArrayList<>() ) + .add( new Link( + dd.groupId(), + dd.artifactId(), + "compile".equals( dd.scope() ) ? " --> " : " -.-> ", + pd.groupId(), + pd.artifactId() ) ) ) ); StringBuilder mermaid = new StringBuilder( "```mermaid\ngraph LR\n" ); - for ( String groupID : groupIDs ) { + for( String groupID : groupIDs ) { mermaid.append( " subgraph " ).append( groupID ).append( "\n" ); groups.get( groupID ) - .stream() - .sorted( Comparator.comparing( PomData::artifactId ) ) - .forEach( pom -> links.getOrDefault( pom.coords(), Collections.emptyList() ).stream() - .filter( Link::intraGroup ) - .forEach( l -> mermaid.append( " " ).append( l ) ) ); + .stream() + .sorted( Comparator.comparing( PomData::artifactId ) ) + .forEach( pom -> links.getOrDefault( pom.coords(), Collections.emptyList() ).stream() + .filter( Link::intraGroup ) + .forEach( l -> mermaid.append( " " ).append( l ) ) ); mermaid.append( " end\n" ); } - if ( intergroupLinks ) { + if( intergroupLinks ) { links.values().stream() - .flatMap( List::stream ) - .sorted( Comparator.comparing( Link::fromArtifactId ) ) - .filter( l -> !l.intraGroup() ) - .forEach( l -> mermaid.append( l ) ); + .flatMap( List::stream ) + .sorted( Comparator.comparing( Link::fromArtifactId ) ) + .filter( l -> !l.intraGroup() ) + .forEach( l -> mermaid.append( l ) ); } mermaid.append( "```" ); @@ -137,7 +139,7 @@ private static class Link { private final String toArtifact; protected Link( String fromGroup, String fromArtifact, String type, String toGroup, - String toArtifact ) { + String toArtifact ) { this.fromGroup = fromGroup; this.fromArtifact = fromArtifact; this.type = type; diff --git a/message/message-http/src/test/java/com/mastercard/test/flow/msg/http/HttpReqTest.java b/message/message-http/src/test/java/com/mastercard/test/flow/msg/http/HttpReqTest.java index c9aa83693d..114111d802 100644 --- a/message/message-http/src/test/java/com/mastercard/test/flow/msg/http/HttpReqTest.java +++ b/message/message-http/src/test/java/com/mastercard/test/flow/msg/http/HttpReqTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.msg.http; import static java.nio.charset.StandardCharsets.UTF_8; @@ -27,6 +28,8 @@ void empty() { + "\r\n", new HttpReq().assertable() ); + assertEquals( "", new HttpReq().bodyText() ); + HttpReq withBody = new HttpReq() .set( HttpMsg.BODY, new Json().set( "foo", "bar" ) ); assertEquals( "" @@ -89,6 +92,11 @@ void populated() { + "key: value\r\n" + "\r\n" + "{\"body\":\"content\"}", new String( msg.content(), UTF_8 ) ); + + assertEquals( "" + + "{\n" + + " \"body\" : \"content\"\n" + + "}", msg.bodyText() ); } /** @@ -157,6 +165,7 @@ void parseFailure() { + "\r\n"; HttpReq req = new HttpReq( reqdata.getBytes(), b -> new Json() { + @Override public Json peer( byte[] content ) { throw new IllegalArgumentException(); From 740820731e0c226a04b51ecb593e99623ba21a36 Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Tue, 13 Sep 2022 14:55:48 +0100 Subject: [PATCH 6/8] moar test --- .../test/flow/assrt/filter/Filter.java | 10 ++++++- .../test/flow/assrt/filter/FilterTest.java | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java index 2d09503f09..bf6841bc37 100644 --- a/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java +++ b/assert/assert-filter/src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt.filter; import static java.util.stream.Collectors.toCollection; @@ -54,7 +55,13 @@ public class Filter { private static final Pattern RANGE_PTRN = Pattern.compile( "(\\d+)-(\\d+)" ); private final Set indices = parseIndices( FilterOptions.INDICES.value() ); - private static final Set parseIndices( String property ) { + /** + * Extracts numeric indices from a string + * + * @param property The string, containing index lists and ranges + * @return The index values + */ + static final Set parseIndices( String property ) { Set indices = new TreeSet<>(); if( property != null ) { for( String s : property.split( "," ) ) { @@ -415,6 +422,7 @@ public Stream flows() { } private static class Persistence { + private static final Path persistencePath = Paths.get( FilterOptions.ARTIFACT_DIR.value(), "filters.json" ); diff --git a/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/FilterTest.java b/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/FilterTest.java index c9b4dd863a..ca006f5872 100644 --- a/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/FilterTest.java +++ b/assert/assert-filter/src/test/java/com/mastercard/test/flow/assrt/filter/FilterTest.java @@ -1,3 +1,4 @@ + package com.mastercard.test.flow.assrt.filter; import static java.util.stream.Collectors.joining; @@ -17,6 +18,7 @@ import java.util.Deque; import java.util.Set; import java.util.TreeSet; +import java.util.function.BiConsumer; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; @@ -274,6 +276,7 @@ void indexUpdates() { } private static class FltrTst { + private final Filter filter; public FltrTst( Model model ) { @@ -340,4 +343,28 @@ public FltrTst expectIndices( String description, int... idxs ) { return this; } } + + /** + * Exercises {@link Filter#parseIndices(String)} + */ + @Test + void parseIndices() { + BiConsumer test = ( in, out ) -> assertEquals( + out, Filter.parseIndices( in ).toString(), "for " + in ); + + test.accept( null, "[]" ); + test.accept( "", "[]" ); + test.accept( "-1", "[]" ); + test.accept( "foobar", "[]" ); + test.accept( "foo1bar", "[]" ); + + test.accept( "1", "[1]" ); + test.accept( " 1\t", "[1]" ); + test.accept( "1234", "[1234]" ); + test.accept( "1,2,3", "[1, 2, 3]" ); + test.accept( " 1 ,\n2,3\t", "[1, 2, 3]" ); + test.accept( "3,2,1", "[1, 2, 3]" ); + test.accept( "0-4", "[0, 1, 2, 3, 4]" ); + test.accept( "0-2,5,blah,8-9", "[0, 1, 2, 5, 8, 9]" ); + } } From 32ea5a6c3e22f72dacf838bef198d05edd6feef3 Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Tue, 13 Sep 2022 15:04:16 +0100 Subject: [PATCH 7/8] doc fix --- assert/assert-filter/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assert/assert-filter/README.md b/assert/assert-filter/README.md index 7f7f2a9d6a..c4112463fd 100644 --- a/assert/assert-filter/README.md +++ b/assert/assert-filter/README.md @@ -42,7 +42,7 @@ Calling [`blockForUpdates()`][Filter!.blockForUpdates()] on a `Filter` instance -[Filter!.blockForUpdates()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L131-L144,131-144 +[Filter!.blockForUpdates()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L138-L151,138-151 @@ -57,7 +57,7 @@ Calling [`load()`][Filter!.load()] on a filter (when system property `mctf.filte -[Filter!.save()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L155-L161,155-161 -[Filter!.load()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L101-L115,101-115 +[Filter!.save()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L162-L168,162-168 +[Filter!.load()]: src/main/java/com/mastercard/test/flow/assrt/filter/Filter.java#L108-L122,108-122 From 704711d06b7a338a7f5130b84bcc9f55f20bd862 Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Tue, 13 Sep 2022 15:25:43 +0100 Subject: [PATCH 8/8] Added profile for quick testing --- pom.xml | 27 ++++++++++++++++++++++++++- report/report-ng/pom.xml | 9 +++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e11c1ac928..db099e1d3c 100644 --- a/pom.xml +++ b/pom.xml @@ -469,7 +469,7 @@ from the terminal --> 0 - + true true @@ -504,6 +504,31 @@ + + + + + ush + + + gui + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + true + + + + + + + diff --git a/report/report-ng/pom.xml b/report/report-ng/pom.xml index 6f9d495583..8215371c11 100644 --- a/report/report-ng/pom.xml +++ b/report/report-ng/pom.xml @@ -16,6 +16,15 @@ ${basedir}/target/classes/com/mastercard/test/flow/report + + + + org.junit.jupiter + junit-jupiter + + +