Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Headless coppice #90

Merged
merged 5 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ graph LR
api --> builder
api --> model
api --> validation-core
api --> coppice
api --> report-core
assert-core --> assert-junit4
assert-core --> assert-junit5
Expand All @@ -67,6 +66,7 @@ graph LR
report-ng --> report-core
validation-core --> validation-junit4
validation-core --> validation-junit5
validation-core --> coppice
end
```

Expand Down
23 changes: 23 additions & 0 deletions doc/src/main/markdown/further.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,29 @@ The assertion components supplied in this framework will exhibit some default be
* Flows will be skipped during processing if their basis flow has suffered an assertion failure. This behaviour is based on the assumption that the child flow is likely to suffer the same assertion failure as its parent, and there's little point in spamming the test results with duplicates of the same failure. This check can be suppressed by setting `mctf.suppress.basis=true`
* Flows will be skipped if their dependency flows suffered an error during processing. The assumption here is that the dependent flow has no hope of success if the dependency failed. This check can be suppressed by setting `mctf.suppress.dependency=true`

## Inheritance health

This framework offers an inheritance mechanism to compress message data - instead of each flow carrying a complete copy of every message, they will typically hold a reference to an existing flow (the basis) and a set of updates that distinguish the new flow from the basis.
At runtime a flow's data is constructed on request by taking a copy of the basis and applying the updates.

This mechanism reduces runtime memory usage and reduces the effort required for widely-scoped changes to shared message fields, but it introduces a new risk - choosing the wrong inheritance basis.
If an inappropriate basis flow is chosen then many message updates must be made to get to the intended goal. Every message update is technical debt that should be avoided where possible.

The buildup of this debt can be monitored by adding a test that uses the [`InheritanceHealth`][InheritanceHealth] class, [for example][ExampleSystemTest].
Such a test computes metrics and a cost histogram about the actual inheritance hierarchy and a theoretical optimal hierarchy.
Every flow that is added will cause these metrics to increase, but if the actual metric increases more than the optimal then that's an indicator that a better inheritance basis exists for the new flow.

The [coppice tool](../../../../validation/coppice) can visualise this process and offers a way to find the best choice of inheritance basis.

Bear in mind that the inheritance health metrics are extremely rough guides - human considerations of code organisation should take precedence.

<!-- code_link_start -->

[InheritanceHealth]: ../../../../validation/validation-core/src/main/java/com/mastercard/test/flow/validation/InheritanceHealth.java
[ExampleSystemTest]: ../../../../example/app-model/src/test/java/com/mastercard/test/flow/example/app/model/ExampleSystemTest.java

<!-- code_link_end -->

## Beyond testing

As the system model exists in its own right independent of any test assertion mechanism, it can be used for more than just testing.
Expand Down
2 changes: 1 addition & 1 deletion example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ graph LR
api --> builder
api --> model
api --> validation-core
api --> coppice
api --> report-core
assert-core --> assert-junit4
assert-core --> assert-junit5
Expand All @@ -84,6 +83,7 @@ graph LR
report-ng --> report-core
validation-core --> validation-junit4
validation-core --> validation-junit5
validation-core --> coppice
end
subgraph com.mastercard.test.flow.example
app-api --> app-web-ui
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import org.junit.jupiter.api.TestFactory;

import com.mastercard.test.flow.Actor;
import com.mastercard.test.flow.Flow;
import com.mastercard.test.flow.example.app.model.ExampleSystem.Actors;
import com.mastercard.test.flow.validation.InheritanceHealth;
import com.mastercard.test.flow.validation.MessageHash;
import com.mastercard.test.flow.validation.junit5.Validator;

Expand Down Expand Up @@ -43,7 +45,7 @@ Stream<DynamicNode> checks() {
* </ul>
*/
@Test
void hashes() {
void messageHashes() {
MessageHash mh = new MessageHash( Assertions::assertEquals )
.hashingEverything();

Expand Down Expand Up @@ -92,4 +94,51 @@ void hashes() {
"RESPONSES <-- HISTOGRAM",
"4C0832B1D6CB88891B5D6C902D35F7B1 0009 1.6 KiB" );
}

/**
* <p>
* Checks the health of the inheritance structure. Choosing an inappropriate
* inheritance basis for a new {@link Flow} creates technical debt - the
* extraneous message updates that could have been avoided with a better basis
* choice.
* </p>
* <p>
* This test will be updated every time a flow is added. If the increase in the
* "Actual" cost metric is higher than the rise in the "Optimal" metric, then
* that's an indicator that a better basis choice is available.
* </p>
* <p>
* Note that this is only a very rough guide - human considerations of code
* organisation almost certainly take precedence.
* </p>
*/
@Test
void inheritanceHealth() {
new InheritanceHealth( 0, 150, 20, Assertions::assertEquals )
.expect( ExampleSystem.MODEL,
"Actual | Optimal ",
"roots 354 | roots 117",
"edges 393 | edges 433",
"total 747 | total 550",
" 3 25.00% | 3 21.43%",
" 1 8.33% | 2 14.29%",
" 1 8.33% | 3 21.43%",
" 2 16.67% | 3 21.43%",
" 1 8.33% | 0 0.00%",
" 0 0.00% | 0 0.00%",
" 2 16.67% | 0 0.00%",
" 1 8.33% | 1 7.14%",
" 0 0.00% | 0 0.00%",
" 0 0.00% | 0 0.00%",
" 0 0.00% | 1 7.14%",
" 1 8.33% | 0 0.00%",
" 0 0.00% | 0 0.00%",
" 0 0.00% | 1 7.14%",
" 0 0.00% | 0 0.00%",
" 0 0.00% | 0 0.00%",
" 0 0.00% | 0 0.00%",
" 0 0.00% | 0 0.00%",
" 0 0.00% | 0 0.00%",
" 0 0.00% | 0 0.00%" );
}
}
16 changes: 2 additions & 14 deletions validation/coppice/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,12 @@
<dependencies>

<dependency>
<!-- flow types -->
<!-- flow api, validation utils -->
<groupId>${project.groupId}</groupId>
<artifactId>api</artifactId>
<artifactId>validation-core</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<!-- text diffs -->
<groupId>io.github.java-diff-utils</groupId>
<artifactId>java-diff-utils</artifactId>
</dependency>

<dependency>
<!-- Context serialisation -->
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
<groupId>org.graphstream</groupId>
<artifactId>gs-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,16 @@

package com.mastercard.test.flow.validation.coppice;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import javax.swing.AbstractAction;
import javax.swing.Action;
Expand All @@ -44,12 +38,9 @@

import org.graphstream.ui.graphicGraph.GraphPosLengthUtils;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.mastercard.test.flow.Flow;
import com.mastercard.test.flow.Interaction;
import com.mastercard.test.flow.Model;
import com.mastercard.test.flow.validation.coppice.graph.CachingDiffDistance;
import com.mastercard.test.flow.validation.InheritanceHealth;
import com.mastercard.test.flow.validation.coppice.ui.Animation;
import com.mastercard.test.flow.validation.coppice.ui.DiffView;
import com.mastercard.test.flow.validation.coppice.ui.FlowList;
Expand All @@ -59,15 +50,16 @@
import com.mastercard.test.flow.validation.coppice.ui.Progress;
import com.mastercard.test.flow.validation.coppice.ui.Range;
import com.mastercard.test.flow.validation.coppice.ui.SelectionManager;
import com.mastercard.test.flow.validation.graph.CachingDiffDistance;

/**
* A GUI tool to examine the internal structure of a system model
*/
public class Coppice {

private final CachingDiffDistance<Flow> diffDistance = new CachingDiffDistance<>(
Coppice::flatten,
Diff::diffDistance );
InheritanceHealth::flatten,
InheritanceHealth::diffDistance );

private List<Flow> flows = new ArrayList<>();

Expand Down Expand Up @@ -377,66 +369,4 @@ private void runTask( GraphTree gt, Runnable task ) {
t.start();
}

private static final ObjectMapper JSON = new ObjectMapper()
.enable( SerializationFeature.INDENT_OUTPUT );

/**
* Dumps a flow's data to a string such that it can be usefully compared
*
* @param flow A flow
* @return A string representation of the flow
*/
private static String flatten( Flow flow ) {
List<String> lines = new ArrayList<>();

lines.add( "Identity:" );
lines.add( " " + flow.meta().description() );
flow.meta().tags().forEach( t -> lines.add( " " + t ) );

lines.add( "Motivation:" );
lines.add( " " + flow.meta().motivation() );

lines.add( "Context:" );
flow.context().forEach( ctx -> {
lines.add( " " + ctx.name() + ":" );
try {
Stream.of( JSON.writeValueAsString( ctx ).replace( "\r", "" ).split( "\n" ) )
.map( l -> " " + l )
.forEach( lines::add );
}
catch( IOException ioe ) {
throw new UncheckedIOException( "Failed to serialise " + ctx, ioe );
}
} );

lines.add( "Interactions:" );
flatten( flow.root(), lines, " " );

return lines.stream().collect( joining( "\n" ) );
}

private static void flatten( Interaction ntr, List<String> lines, String indent ) {
lines.add( String.format( "%s┌REQUEST %s 🠖 %s %s", indent, ntr.requester(), ntr.responder(),
ntr.tags() ) );
Stream.of( ntr.request().assertable().split( "\n" ) )
.map( l -> indent + "│" + l )
.forEach( lines::add );

List<Interaction> children = ntr.children().collect( toList() );
if( children.isEmpty() ) {
lines.add( indent + "└" );
}
else {
lines.add( indent + "╘ Provokes:" );
children.forEach( c -> flatten( c, lines, indent + " " ) );
}

lines.add( String.format( "%s┌RESPONSE %s 🠔 %s %s", indent, ntr.requester(), ntr.responder(),
ntr.tags() ) );
Stream.of( ntr.response().assertable().split( "\n" ) )
.map( l -> indent + "│" + l )
.forEach( lines::add );
lines.add( indent + "└" );
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,6 @@ private Diff() {
// no instances
}

/**
* @param from A string
* @param to Another string
* @return The number of lines of difference between the two
*/
public static int diffDistance( String from, String to ) {

return DiffUtils.diff(
Arrays.asList( from.split( "\n" ) ),
Arrays.asList( to.split( "\n" ) ), false ).getDeltas()
.stream()
.mapToInt( delta -> {
switch( delta.getType() ) {
case DELETE:
return delta.getSource().getLines().size();
case INSERT:
return delta.getTarget().getLines().size();
case CHANGE:
return Math.max(
delta.getSource().getLines().size(),
delta.getTarget().getLines().size() );
default:
return 0;
}
} )
.sum();

}

/**
* @param from A string
* @param to Another string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import javax.swing.Timer;

import com.mastercard.test.flow.Flow;
import com.mastercard.test.flow.validation.coppice.graph.CachingDiffDistance;
import com.mastercard.test.flow.validation.coppice.graph.DiffGraph;
import com.mastercard.test.flow.validation.coppice.ui.Animation;
import com.mastercard.test.flow.validation.coppice.ui.GraphTree;
import com.mastercard.test.flow.validation.coppice.ui.GraphView;
import com.mastercard.test.flow.validation.coppice.ui.Progress;
import com.mastercard.test.flow.validation.graph.CachingDiffDistance;
import com.mastercard.test.flow.validation.graph.DiffGraph;

/**
* This task optimises the entire inheritance tree for the targeted flow:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
import org.graphstream.graph.Node;

import com.mastercard.test.flow.Flow;
import com.mastercard.test.flow.validation.coppice.graph.CachingDiffDistance;
import com.mastercard.test.flow.validation.coppice.ui.Animation;
import com.mastercard.test.flow.validation.coppice.ui.GraphTree;
import com.mastercard.test.flow.validation.coppice.ui.GraphView;
import com.mastercard.test.flow.validation.coppice.ui.Progress;
import com.mastercard.test.flow.validation.graph.CachingDiffDistance;

/**
* This task will, for a single flow, find the most similar flow in the corpus.
Expand Down
Loading