Skip to content

Commit

Permalink
Dependency inclusion (#127)
Browse files Browse the repository at this point in the history
* simplified api

* Added validation
  • Loading branch information
therealryan authored Oct 26, 2022
1 parent cb27843 commit 2a8f15f
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.mastercard.test.flow.Model;
import com.mastercard.test.flow.validation.check.ChainOverlapCheck;
import com.mastercard.test.flow.validation.check.DependencyChronologyCheck;
import com.mastercard.test.flow.validation.check.DependencyInclusionCheck;
import com.mastercard.test.flow.validation.check.DependencyLoopCheck;
import com.mastercard.test.flow.validation.check.FlowIdentityCheck;
import com.mastercard.test.flow.validation.check.InteractionIdentityCheck;
Expand All @@ -38,6 +39,7 @@ public static final Validation[] defaultChecks() {
new ChainOverlapCheck(),
new DependencyChronologyCheck(),
new DependencyLoopCheck(),
new DependencyInclusionCheck(),
new FlowIdentityCheck(),
new InteractionIdentityCheck(),
new MessageSharingCheck(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,22 @@ public class Violation {
private final String actual;

/**
* Constructs a new {@link Violation} that signals a general failure
*
* @param validation The {@link Validation} that has been violated
* @param details A human-readable description of the violation
*/
public Violation( Validation validation, String details ) {
this( validation, details, null, null );
}

/**
* Constructs a new {@link Violation} that signals an expected-equality mismatch
*
* @param validation The {@link Validation} that has been violated
* @param details A human-readable description of the violation
* @param expected Expected half of a comparison test, or <code>null</code> to
* just signal a general test failure
* @param actual Actual half of a comparison test, or <code>null</code>to
* just signal a general test failure
* @param expected Expected half of an equality test
* @param actual Actual half of an equality test
*/
public Violation( Validation validation, String details, String expected, String actual ) {
this.validation = validation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public Stream<Check> checks( Model model ) {
.collect( Collectors.toCollection( TreeSet::new ) );

if( chains.size() > 1 ) {
return new Violation( this, "Overlapping chains " + chains, null, null )
return new Violation( this, "Overlapping chains " + chains )
.offender( flow );
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,10 @@ public Stream<Check> checks( Model model ) {
+ "\nto\n"
+ "%s",
delorean.source().getMessage().map( Message::assertable ).orElse( "???" ),
delorean.sink().getMessage().map( Message::assertable ).orElse( "???" ) ),
null, null )
.offender( flow,
delorean.source().getInteraction().orElse( null ),
delorean.sink().getInteraction().orElse( null ) );
delorean.sink().getMessage().map( Message::assertable ).orElse( "???" ) ) )
.offender( flow,
delorean.source().getInteraction().orElse( null ),
delorean.sink().getInteraction().orElse( null ) );
}
return null;
} ) );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.mastercard.test.flow.validation.check;

import static java.util.stream.Collectors.toSet;

import java.util.Set;
import java.util.stream.Stream;

import com.mastercard.test.flow.Dependency;
import com.mastercard.test.flow.Flow;
import com.mastercard.test.flow.Model;
import com.mastercard.test.flow.validation.Check;
import com.mastercard.test.flow.validation.Validation;
import com.mastercard.test.flow.validation.Violation;

/**
* Checks that the dependency flows are actually part of the system model
*/
public class DependencyInclusionCheck implements Validation {

@Override
public String name() {
return "Dependency inclusion";
}

@Override
public String explanation() {
return "Dependency sources must be included in the system model";
}

@Override
public Stream<Check> checks( Model model ) {
Set<Flow> allFlows = model.flows().collect( toSet() );
return model.flows()
.flatMap( Flow::dependencies )
.map( d -> new Check( this,
name( d ),
() -> d.source().getFlow()
.filter( src -> !allFlows.contains( src ) )
.map( src -> new Violation( this,
String.format( "Dependency source '%s' not presented in system model",
src.meta().id() ) )
.offender( d.sink().flow() ) )
.orElse( null ) ) );
}

private static String name( Dependency d ) {
return String.format( "%s → %s",
d.source().getFlow().map( f -> f.meta().id() ).orElse( null ),
d.sink().getFlow().map( f -> f.meta().id() ).orElse( null ) );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public Stream<Check> checks( Model model ) {
checks.add( new Check( this,
left.meta().id() + " x " + right.meta().id(),
() -> violation( left, right )
.map( v -> new Violation( this, v, null, null )
.map( v -> new Violation( this, v )
.offender( left )
.offender( right ) )
.orElse( null ) ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public Stream<Check> checks( Model model ) {
String id = String.format( "%s->%s %s",
ntr.requester().name(), ntr.responder().name(), ntr.tags() );
if( ids.containsKey( id ) ) {
return new Violation( this, "Shared interaction ID", null, null )
return new Violation( this, "Shared interaction ID" )
.offender( flow, ids.get( id ), ntr );
}
ids.put( id, ntr );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,9 @@ public Stream<Check> checks( Model model ) {
MessageOwner current = new MessageOwner( flow, tx.source() );
MessageOwner previous = messageIdentities.get( objectId );
if( previous != null ) {
return new Violation( this, "Shared message:\n" + tx.message().assertable(),
null, null )
.offender( previous.flow, previous.interaction )
.offender( current.flow, current.interaction );
return new Violation( this, "Shared message:\n" + tx.message().assertable() )
.offender( previous.flow, previous.interaction )
.offender( current.flow, current.interaction );
}
messageIdentities.put( objectId, current );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public Stream<Check> checks( Model model ) {
.collect( Collectors.joining( ", " ) );

if( !misused.isEmpty() ) {
return new Violation( this, "Use of reserved tags: " + misused, null, null )
return new Violation( this, "Use of reserved tags: " + misused )
.offender( flow );
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ void with() {
Assertions.assertEquals( ""
+ "Chain overlap\n"
+ "Dependency chronology\n"
+ "Dependency inclusion\n"
+ "Dependency loop\n"
+ "Flow Identity\n"
+ "Interaction Identity\n"
Expand Down Expand Up @@ -62,9 +63,9 @@ void acceptance() {
.accepting( v -> v.details().contains( "minor" ) );

Assertions.assertFalse( tv.accepted(
new Violation( null, "gadzooks! this is a major problem!", null, null ) ) );
new Violation( null, "gadzooks! this is a major problem!" ) ) );
Assertions.assertTrue( tv.accepted(
new Violation( null, "meh, this is a minor problem", null, null ) ) );
new Violation( null, "meh, this is a minor problem" ) ) );

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package com.mastercard.test.flow.validation.check;

import static java.util.Collections.emptySet;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import com.mastercard.test.flow.Dependency;
import com.mastercard.test.flow.FieldAddress;
import com.mastercard.test.flow.Flow;
import com.mastercard.test.flow.Metadata;
import com.mastercard.test.flow.Model;

/**
* Exercises {@link DependencyInclusionCheck}
*/
class DependencyInclusionCheckTest extends AbstractValidationTest {

/***/
DependencyInclusionCheckTest() {
super( new DependencyInclusionCheck(),
"Dependency inclusion",
"Dependency sources must be included in the system model" );
}

/**
* No flows
*/
@Test
void empty() {
test( mdl( flws().values() ) );
}

/**
* A single flow
*/
@Test
void lonesome() {
test( mdl( flws( "lonely" ).values() ) );
}

/**
* Two flows, but no dependencies
*/
@Test
void noDependencies() {
test( mdl( flws( "abc", "def" ).values() ) );
}

/**
* Two flows, a dependency, the sink is included in the model
*/
@Test
void included() {
test( mdl( flws( "abc", "def abc" ).values() ),
"abc [] → def [] : pass" );
}

/**
* Two flows, a dependency, the sink is <i>not</i> included in the model
*/
@Test
void excluded() {
Map<String, Flow> flows = flws( "abc", "def abc" );
flows.remove( "abc" );
test( mdl( flows.values() ),
" details: Dependency source 'abc []' not presented in system model\n"
+ " expected: null\n"
+ " actual: null\n"
+ "offenders: def []\n"
+ "null" );
}

/**
* One valid dependency, one violation
*/
@Test
void mixed() {
Map<String, Flow> flows = flws(
"abc", "def abc",
"ghi", "jkl ghi" );
flows.remove( "ghi" );
test( mdl( flows.values() ),
"abc [] → def [] : pass",
" details: Dependency source 'ghi []' not presented in system model\n"
+ " expected: null\n"
+ " actual: null\n"
+ "offenders: jkl []\n"
+ "null" );
}

/**
* Multiple valid dependencies
*/
@Test
void multiIncluded() {
test( mdl( flws( "abc", "def abc", "ghi abc def", "jkl abc def ghi" ).values() ),
"abc [] → def [] : pass",
"abc [] → ghi [] : pass",
"def [] → ghi [] : pass",
"abc [] → jkl [] : pass",
"def [] → jkl [] : pass",
"ghi [] → jkl [] : pass" );
}

/**
* Multiple invalid dependencies
*/
@Test
void multiExcluded() {
Map<String, Flow> flows = flws( "abc", "def abc", "ghi abc def", "jkl abc def ghi" );
flows.remove( "def" );
test( mdl( flows.values() ),
"abc [] → ghi [] : pass",
" details: Dependency source 'def []' not presented in system model\n"
+ " expected: null\n"
+ " actual: null\n"
+ "offenders: ghi []\n"
+ "null",
"abc [] → jkl [] : pass",
" details: Dependency source 'def []' not presented in system model\n"
+ " expected: null\n"
+ " actual: null\n"
+ "offenders: jkl []\n"
+ "null",
"ghi [] → jkl [] : pass" );
}

/**
* @param flows A set of strings, each specifying one flow. The strings are
* space-separated lists. The first element is the flow name, the
* following elements are the names of dependency flows
* @return A model, with the specified flows and dependency structure
*/
private static Map<String, Flow> flws( String... flows ) {
Map<String, Flow> names = new HashMap<>();

for( String flow : flows ) {
String[] tkns = flow.split( " " );

Flow flw = Mockito.mock( Flow.class );
Metadata mtdt = Mockito.mock( Metadata.class );

when( mtdt.description() ).thenReturn( tkns[0] );
when( mtdt.tags() ).thenReturn( emptySet() );
when( mtdt.id() ).thenCallRealMethod();
when( flw.meta() ).thenReturn( mtdt );

List<Dependency> deps = new ArrayList<>();
for( int i = 1; i < tkns.length; i++ ) {
String depName = tkns[i];

FieldAddress src = mock( FieldAddress.class );
when( src.getFlow() ).thenReturn( Optional.ofNullable( names.get( depName ) ) );
when( src.flow() ).thenAnswer( a -> names.get( depName ) );

FieldAddress snk = mock( FieldAddress.class );
when( snk.getFlow() ).thenReturn( Optional.of( flw ) );
when( snk.flow() ).thenReturn( flw );

Dependency dep = mock( Dependency.class );
when( dep.source() ).thenReturn( src );
when( dep.sink() ).thenReturn( snk );

deps.add( dep );
}
when( flw.dependencies() ).thenAnswer( a -> deps.stream() );

names.put( tkns[0], flw );
}

return names;
}

/**
* @param flows Some {@link Flow}s
* @return a {@link Model} that produces the supplied {@link Flow}s
*/
private static Model mdl( Collection<Flow> flows ) {
Model mdl = Mockito.mock( Model.class );
when( mdl.flows() ).thenAnswer( a -> flows.stream() );
return mdl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ void lollipop() {
}

/**
* @param flows A set of string, each specifying one flow. The strings are
* @param flows A set of strings, each specifying one flow. The strings are
* space-separated lists. The first element is the flow name, the
* following elements are the names of dependency flows
* @return A model, with the specified flows and dependency structure
Expand Down
Loading

0 comments on commit 2a8f15f

Please sign in to comment.