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

Replaced manual diagram with mermaid generated from test data #57

Merged
merged 2 commits into from
Aug 13, 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
10 changes: 10 additions & 0 deletions doc/src/main/markdown/further.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,13 @@ 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`

## 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.
For example, the [example system diagram](../../../../example/README.md) is [generated][SystemDiagramTest] from the system model.

<!-- code_link_start -->

[SystemDiagramTest]: ../../../../example/app-model/src/test/java/com/mastercard/test/flow/example/app/model/SystemDiagramTest.java

<!-- code_link_end -->
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ boolean compliant( String section ) {
return section.contains( name() )
&& section.contains( description )
&& section.contains( parentLink() )
&& childLinks().allMatch( section::contains );
&& childLinks()
.map( String::trim )
.allMatch( section::contains );
}

/**
Expand Down
34 changes: 24 additions & 10 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
Service constellation to exercise the flow framework

* [../flow](https://github.com/Mastercard/flow) Testing framework

Application libraries:
* [app-framework](app-framework) Library providing a simple microservice framework
* [app-api](app-api) Library providing interfaces that define the REST API for each service in the example application

Services:
* [app-web-ui](app-web-ui) Front-end service that presents an HTML interface to the system
* [app-ui](app-ui) Front-end service that accepts external requests
* [app-core](app-core) Service that orchestrates functionality between services in the example application
* [app-histogram](app-histogram) Service that counts characters in a given set of text
* [app-queue](app-queue) Service that handles deferred processing
* [app-store](app-store) Service that provides data storage

Test modules:
* [app-model](app-model) Library providing system description flows for creating tests with the Flows framework
* [app-assert](app-assert) Test library providing shared assertion components
* [app-itest](app-itest) System integration test suite for exercising app instances
Expand All @@ -29,17 +35,25 @@ the data flows between them.

The diagram below illustrates the services and their interdependencies.

![Example application service diagrm](src/main/doc/example-app-services.svg)

## Sub-Modules

The sub-modules in this project can be grouped into the following
categories:
<!-- system_diagram_start -->

```mermaid
graph TD
OPS(OPS<br>Provokes queue) -- POST --> QUEUE[QUEUE<br>Stores and processes<br>deferred operations]
USER([USER<br>Needs characters counted]) -- GET/POST --> UI[UI<br>HTTP interface]
USER -- browser --> WEB_UI[WEB_UI<br>Browser interface]
subgraph example system
CORE[CORE<br>Orchestrates processing] -- POST --> HISTOGRAM[HISTOGRAM<br>Counts characters]
CORE -- GET/POST --> QUEUE
QUEUE -- POST --> CORE
QUEUE -- DELETE/GET/PUT --> STORE[STORE<br>Key/Value store]
STORE -- SQL --> DB[(DB<br>)]
UI -- GET/POST --> CORE
WEB_UI -- POST --> UI
end
```

* Application libraries: [app-framework](app-framework), [app-api](app-api)
* Services: [app-ui](app-ui), [app-core](app-core), [app-queue](app-queue),
[app-store](app-store), [app-histogram](app-histogram)
* Test modules: [app-model](app-model), [app-assert](app-assert), [app-itest](app-itest)
<!-- system_diagram_end -->

## Starting the services

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package com.mastercard.test.flow.example.app.model;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import com.mastercard.test.flow.Actor;
import com.mastercard.test.flow.Message;
import com.mastercard.test.flow.example.app.model.ExampleSystem.Actors;
import com.mastercard.test.flow.msg.http.HttpReq;
import com.mastercard.test.flow.msg.sql.Query;
import com.mastercard.test.flow.msg.web.WebSequence;
import com.mastercard.test.flow.util.Flows;

/**
* Ensures that the diagram in the main example readme accurately reflects the
* system model that drives testing
*/
@SuppressWarnings("static-method")
class SystemDiagramTest {

/**
* Extra data about system actors for the purposes of the diagram
*/
private enum Meta {
USER(false, "([", "])", "Needs characters counted"),
WEB_UI(true, "[", "]", "Browser interface"),
UI(true, "[", "]", "HTTP interface"),
CORE(true, "[", "]", "Orchestrates processing"),
HISTOGRAM(true, "[", "]", "Counts characters"),
OPS(false, "(", ")", "Provokes queue"),
QUEUE(true, "[", "]", "Stores and processes<br>deferred operations"),
STORE(true, "[", "]", "Key/Value store"),
DB(true, "[(", ")]", ""),
;

public final boolean internal;
public final String left;
public final String right;
public final String description;

Meta( boolean internal, String left, String right, String description ) {
this.internal = internal;
this.left = left;
this.right = right;
this.description = description;
}
}

/**
* Generates mermaid markup from the system model and inserts it into the main
* example system readme. The test will fail if that insertion made any changes
*
* @throws Exception If the insertion fails
*/
@Test
void regenerate() throws Exception {
// map from requester to responder to a set of call characterisations
Map<Actor,
Map<Actor, Set<String>>> calls = new TreeMap<>( Comparator.comparing( Actor::name ) );

ExampleSystem.MODEL.flows()
.flatMap( Flows::interactions )
.forEach( ntr -> calls
.computeIfAbsent( ntr.requester(),
q -> new TreeMap<>( Comparator.comparing( Actor::name ) ) )
.computeIfAbsent( ntr.responder(),
s -> new TreeSet<>() )
.add( characterise( ntr.request() ) ) );

StringBuilder mm = new StringBuilder( "```mermaid\ngraph TD\n" );
Set<Actor> defined = new HashSet<>();

// Start with external actors
Stream.of( Meta.values() )
.filter( m -> !m.internal )
.sorted( Comparator.comparing( Meta::name ) )
.map( m -> Actors.valueOf( m.name() ) )
.forEach( ext -> {
Map<Actor, Set<String>> cts = calls.remove( ext );
for( Map.Entry<Actor, Set<String>> ct : cts.entrySet() ) {
mm.append( link( ext, ct.getKey(), defined, ct.getValue() ) );
}
} );

// then the rest
mm.append( " subgraph example system\n" );
for( Map.Entry<Actor, Map<Actor, Set<String>>> e : calls.entrySet() ) {
for( Map.Entry<Actor, Set<String>> rs : e.getValue().entrySet() ) {
mm.append( link( e.getKey(), rs.getKey(), defined, rs.getValue() ) );
}
}
mm.append( " end\n```" );

insert( Paths.get( "../README.md" ),
"<!-- system_diagram_start -->",
mm.toString(),
"<!-- system_diagram_end -->" );
}

private static String link( Actor from, Actor to, Set<Actor> defined,
Set<String> characterisations ) {
return String.format( " %s -- %s --> %s\n",
node( from, defined ),
characterisations.stream().collect( joining( "/" ) ),
node( to, defined ) );
}

private static String node( Actor a, Set<Actor> defined ) {
if( defined.contains( a ) ) {
return a.name();
}
defined.add( a );
Meta m = Meta.valueOf( a.name() );
return String.format( "%s%s%s<br>%s%s",
a, m.left, a, m.description, m.right );
}

private static String characterise( Message request ) {
if( request instanceof HttpReq ) {
HttpReq hr = (HttpReq) request;
return hr.method();
}
if( request instanceof WebSequence ) {
return "browser";
}
if( request instanceof Query ) {
return "SQL";
}
return request.getClass().getSimpleName();
}

/**
* Regenerates a section of a file and throws a failed comparison test if the
* file was altered by that act.
*
* @param path The file to operate on
* @param start The line at which to insert the content
* @param content The content to insert
* @param end The line at which the content ends
* @throws Exception IO failure
*/
private static void insert( Path path, String start, String content, String end )
throws Exception {

String existing = "";
if( Files.exists( path ) ) {
existing = new String( Files.readAllBytes( path ), UTF_8 );
}
List<String> regenerated = new ArrayList<>();

Iterator<String> exi = Arrays.asList( existing.split( "\n", -1 ) ).iterator();
while( exi.hasNext() ) {
boolean found = false;
while( exi.hasNext() && !found ) {
String line = exi.next();
if( line.trim().equals( start ) ) {
found = true;
break;
}
regenerated.add( line );
}
if( found ) {

boolean endFound = false;
while( exi.hasNext() && !endFound ) {
String line = exi.next();
if( line.trim().equals( end ) ) {
endFound = true;
}
}

// add the new content
regenerated.add( start );
regenerated.add( "" );
regenerated.add( content );
regenerated.add( "" );
regenerated.add( end );
}
}

// write the file
String newContent = regenerated.stream().collect( Collectors.joining( "\n" ) );
Files.write( path, newContent.getBytes( UTF_8 ) );

Assertions.assertEquals( existing, newContent,
path + " has been updated and needs to be committed to git" );
}
}
Loading