diff --git a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/CheckerTest.java b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/CheckerTest.java index a5be796816..dd2ae8624e 100644 --- a/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/CheckerTest.java +++ b/assert/assert-core/src/test/java/com/mastercard/test/flow/assrt/CheckerTest.java @@ -186,11 +186,21 @@ void fullyMasked() { } ) ) ); } + /** + * Sources of unpredictability + */ enum Nprdct implements Unpredictable { - DIGITS, DIGITS_AND_SUFFIX, + /***/ + DIGITS, + /***/ + DIGITS_AND_SUFFIX, } + /** + * A {@link Checker} for {@link TestResidue}s + */ static class TestChecker extends Checker { + /***/ public TestChecker() { super( TestResidue.class ); } diff --git a/report/report-core/src/main/java/com/mastercard/test/flow/report/Writer.java b/report/report-core/src/main/java/com/mastercard/test/flow/report/Writer.java index e9b6b6b42e..cea74d4478 100644 --- a/report/report-core/src/main/java/com/mastercard/test/flow/report/Writer.java +++ b/report/report-core/src/main/java/com/mastercard/test/flow/report/Writer.java @@ -13,7 +13,10 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -80,6 +83,8 @@ public class Writer { private final Map data = new LinkedHashMap<>(); private final JsApp app; + private final Map> missingBases = new HashMap<>(); + /** * @param modelTitle A human-readable title for the model that supplied the test * data @@ -105,7 +110,8 @@ public Writer( String modelTitle, String testTitle, Path root ) { */ @SafeVarargs public final Writer with( Flow flow, Consumer... extra ) { - IndexedFlowData idf = data.computeIfAbsent( flow, IndexedFlowData::new ); + IndexedFlowData idf = data.computeIfAbsent( flow, + f -> new IndexedFlowData( flow, data.keySet(), missingBases ) ); String oldname = idf.indexEntry().detail; idf.update( extra ); @@ -113,9 +119,10 @@ public final Writer with( Flow flow, Consumer... extra ) { QuietFiles.recursiveDelete( root.resolve( "detail/" + oldname + ".html" ) ); } - Path detailPath = root.resolve( DETAIL_DIR_NAME ) - .resolve( idf.indexEntry().detail + ".html" ); - app.write( idf.detail, detailPath ); + // write the new detail + idf.writeTo( root, app ); + + // refresh the index app.write( new Index( new Meta( modelTitle, testTitle, System.currentTimeMillis() ), @@ -124,6 +131,32 @@ public final Writer with( Flow flow, Consumer... extra ) { .collect( toList() ) ), root.resolve( INDEX_FILE_NAME ) ); + // refresh the details of those who were waiting for that flow as a better basis + // candidate + missingBases.forEach( ( unhappy, preferred ) -> { + Iterator pi = preferred.iterator(); + boolean found = false; + while( pi.hasNext() ) { + Flow betterBase = pi.next(); + if( found ) { + pi.remove(); + } + else if( betterBase == flow ) { + found = true; + pi.remove(); + IndexedFlowData toUpdate = data.get( unhappy ); + toUpdate.detail = toUpdate.detail.withBasis( detailFilename( flow ) ); + toUpdate.writeTo( root, app ); + } + } + } ); + + // prune satisfied flows + missingBases.entrySet().removeAll( + missingBases.entrySet().stream() + .filter( e -> e.getValue().isEmpty() ) + .collect( toSet() ) ); + return this; } @@ -151,24 +184,50 @@ public void browse() { } } - private class IndexedFlowData { + /** + * @return A map from {@link Flow}s that are missing their ideal bases to list + * of those bases in preference order + */ + public Map> missingBases() { + return missingBases; + } + + private static class IndexedFlowData { private Entry indexEntry; - public final FlowData detail; + FlowData detail; + + public IndexedFlowData( Flow flow, + Set flowsInReport, + Map> missingBases ) { + + // walk up the basis chain until we find one that exists in the report + Flow closesBasis = flow.basis(); + List desiredBases = new ArrayList<>(); + while( closesBasis != null + && !flowsInReport.contains( closesBasis ) ) { + desiredBases.add( closesBasis ); + closesBasis = closesBasis.basis(); + } + + if( !desiredBases.isEmpty() ) { + // keep track of the missing ones. If those get added to the rpeort later we'll + // want to update this flow to point at them + missingBases.put( flow, desiredBases ); + } - public IndexedFlowData( Flow flow ) { detail = new FlowData( flow.meta().description(), new TreeSet<>( flow.meta().tags() ), flow.meta().motivation(), flow.meta().trace(), - Optional.ofNullable( flow.basis() ) - .map( b -> detailFilename( b.meta().description(), b.meta().tags() ) ) + Optional.ofNullable( closesBasis ) + .map( Writer::detailFilename ) .orElse( null ), flow.dependencies() .map( d -> d.source().flow() ) .filter( d -> d != flow ) .collect( toMap( - k -> detailFilename( k.meta().description(), k.meta().tags() ), + Writer::detailFilename, v -> new DependencyData( v.meta().description(), v.meta().tags() ), @@ -189,12 +248,44 @@ public final void update( Consumer... extra ) { e.accept( detail ); } indexEntry = new Entry( detail.description, detail.tags, - detailFilename( detail.description, detail.tags ) ); + detailFilename( detail ) ); } Entry indexEntry() { return indexEntry; } + + /** + * Writes the detail data + * + * @param root The report root directory + * @param app The application + */ + void writeTo( Path root, JsApp app ) { + app.write( detail, root + .resolve( DETAIL_DIR_NAME ) + .resolve( indexEntry().detail + ".html" ) ); + } + } + + /** + * Computes the filename for a flow identity + * + * @param flow The {@link Flow} + * @return The file name under which the flow's details should be saved + */ + public static String detailFilename( Flow flow ) { + return detailFilename( flow.meta().description(), flow.meta().tags() ); + } + + /** + * Computes the filename for a flow identity + * + * @param flow The {@link Flow} + * @return The file name under which the flow's details should be saved + */ + public static String detailFilename( FlowData flow ) { + return detailFilename( flow.description, flow.tags ); } /** @@ -204,7 +295,7 @@ Entry indexEntry() { * @param tags {@link Metadata#tags()} * @return The file name under which the flow's details should be saved */ - public static String detailFilename( String description, Set tags ) { + private static String detailFilename( String description, Set tags ) { try { Set toHash = new TreeSet<>( tags ); toHash.removeAll( RESULT_TAGS ); diff --git a/report/report-core/src/main/java/com/mastercard/test/flow/report/data/FlowData.java b/report/report-core/src/main/java/com/mastercard/test/flow/report/data/FlowData.java index be051eb4c5..e455bdb96e 100644 --- a/report/report-core/src/main/java/com/mastercard/test/flow/report/data/FlowData.java +++ b/report/report-core/src/main/java/com/mastercard/test/flow/report/data/FlowData.java @@ -109,4 +109,17 @@ public FlowData( this.logs = logs; } + /** + * Updates the {@link #basis} field + * + * @param b The new basis value + * @return a new {@link FlowData} instance, copying this one in all aspects + * except the {@link #basis} + */ + public FlowData withBasis( String b ) { + return new FlowData( description, tags, motivation, trace, + b, + dependencies, root, context, + residue, logs ); + } } diff --git a/report/report-core/src/test/java/com/mastercard/test/flow/report/WriterTest.java b/report/report-core/src/test/java/com/mastercard/test/flow/report/WriterTest.java index 73ae0594b2..e9aa68d8a3 100644 --- a/report/report-core/src/test/java/com/mastercard/test/flow/report/WriterTest.java +++ b/report/report-core/src/test/java/com/mastercard/test/flow/report/WriterTest.java @@ -1,17 +1,25 @@ package com.mastercard.test.flow.report; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.joining; import static org.junit.jupiter.api.Assertions.assertEquals; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Map; import java.util.stream.Collectors; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; +import com.mastercard.test.flow.Flow; +import com.mastercard.test.flow.builder.Creator; +import com.mastercard.test.flow.builder.Deriver; +import com.mastercard.test.flow.msg.txt.Text; +import com.mastercard.test.flow.report.Mdl.Actrs; + /** * Exercises {@link Writer} */ @@ -154,4 +162,96 @@ void write() throws Exception { detail.indexOf( "// END_JSON_DATA" ) + "// END_JSON_DATA".length() ) .replaceAll( ":\\d+", ":##" ) ); } + + /** + * Demonstrates that the report will cope with a missing basis - we'll hoist to + * the nearest ancestor that is available. + */ + @Test + void basisChaining() { + Flow gramps = Creator.build( flow -> flow + .meta( data -> data + .description( "gramps" ) ) + .call( a -> a.from( Actrs.AVA ).to( Actrs.BEN ) + .request( new Text( "hi ben!" ) ) + .response( new Text( "hi ava!" ) ) ) ); + + Flow pops = Deriver.build( gramps, flow -> flow + .meta( data -> data + .description( "pops" ) ) ); + + Flow junior = Deriver.build( pops, flow -> flow + .meta( data -> data + .description( "junior" ) ) ); + + Flow zygote = Deriver.build( junior, flow -> flow + .meta( data -> data + .description( "zygote" ) ) ); + + Path dir = Paths.get( "target", "WriterTest", "basisChaining" ); + + Writer writer = new Writer( "model", "test", dir ); + Reader reader = new Reader( dir ); + + writer.with( zygote ); + assertHierarchy( reader, "" + + "zygote's basis is unknown basis:null", + "zygote has an ancestry, but none of them are in the report" ); + assertWriterMissingBases( writer, "" + + "zygote : [ junior, pops, gramps ]" ); + + writer.with( pops ); + assertHierarchy( reader, "" + + "zygote's basis is pops\n" + + " pops's basis is unknown basis:null", + "An ancestor has been added, zygote is updated" ); + assertWriterMissingBases( writer, "" + + " pops : [ gramps ]\n" + + "zygote : [ junior ]" ); + + writer.with( gramps ); + assertHierarchy( reader, "" + + "zygote's basis is pops\n" + + " pops's basis is gramps\n" + + "gramps's basis is unknown basis:null", + "A more distant ancestor has been added, no update" ); + assertWriterMissingBases( writer, "" + + "zygote : [ junior ]" ); + + writer.with( junior ); + assertHierarchy( reader, "" + + "zygote's basis is junior\n" + + " pops's basis is gramps\n" + + "gramps's basis is unknown basis:null\n" + + "junior's basis is pops", + "The direct basis is added, zygote is updated again" ); + assertWriterMissingBases( writer, "" ); + } + + private void assertHierarchy( Reader reader, String expected, String comment ) { + Map idx = reader.read().entries.stream() + .collect( Collectors.toMap( e -> e.detail, e -> e.description ) ); + + assertEquals( expected, + reader.read().entries.stream() + .map( reader::detail ) + .map( d -> String.format( + "%6s's basis is %s", + d.description, + idx.getOrDefault( d.basis, "unknown basis:" + d.basis ) ) ) + .collect( joining( "\n" ) ), + comment ); + } + + private void assertWriterMissingBases( Writer writer, String expected ) { + assertEquals( expected, + writer.missingBases().entrySet().stream() + .map( e -> String.format( + "%6s : [ %s ]", + e.getKey().meta().description(), + e.getValue().stream().map( f -> f.meta().description() ) + .collect( joining( ", " ) ) ) ) + .sorted() + .collect( joining( "\n" ) ) ); + } } diff --git a/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/AbstractSequence.java b/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/AbstractSequence.java index 0747a98aeb..eca110f261 100644 --- a/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/AbstractSequence.java +++ b/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/AbstractSequence.java @@ -91,10 +91,10 @@ protected AbstractSequence( String url ) { /** * Navigates the browser * - * @param url The destination + * @param destination Where we want to browse to * @return this */ - protected S get( String url ) { + protected S get( String destination ) { // I really hate having to do this, but something seems to have changed on my // system such that it's necessary - a single get call usually just leaves you @@ -105,13 +105,13 @@ protected S get( String url ) { int attempts = 0; do { attempts++; - driver.get( url ); + driver.get( destination ); } while( limit > System.currentTimeMillis() && "data:,".equals( driver.getCurrentUrl() ) ); Assertions.assertNotEquals( "data:,", driver.getCurrentUrl(), String.format( "Failed to achieve destination '%s' after %.2fs and %d attempts", - url, (System.currentTimeMillis() - start) / 1000.0, attempts ) ); + destination, (System.currentTimeMillis() - start) / 1000.0, attempts ) ); return self(); } diff --git a/report/report-ng/projects/report/src/app/msg-search-input/msg-search-input.component.html b/report/report-ng/projects/report/src/app/msg-search-input/msg-search-input.component.html index e48bd59cd2..a22067e56b 100644 --- a/report/report-ng/projects/report/src/app/msg-search-input/msg-search-input.component.html +++ b/report/report-ng/projects/report/src/app/msg-search-input/msg-search-input.component.html @@ -5,7 +5,7 @@
Message search -