Skip to content

Commit

Permalink
Adding option to batch validation checks (#374)
Browse files Browse the repository at this point in the history
  • Loading branch information
therealryan authored May 15, 2023
1 parent 2edb64a commit 4c26a0a
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.mastercard.test.flow.validation;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Stream;

Expand Down Expand Up @@ -49,9 +53,10 @@ public static final Validation[] defaultChecks() {
};
}

private final Set<Validation> checks = new TreeSet<>(
private final Set<Validation> validations = new TreeSet<>(
Comparator.comparing( Validation::name ) );
private Model model;
private int batchSize = 1;

private List<Predicate<Violation>> accepted = new ArrayList<>();

Expand All @@ -63,7 +68,7 @@ public static final Validation[] defaultChecks() {
* @see #defaultChecks()
*/
public T with( Validation... check ) {
Collections.addAll( checks, check );
Collections.addAll( validations, check );
return self();
}

Expand All @@ -89,6 +94,19 @@ public T checking( Model m ) {
return self();
}

/**
* Controls how individual {@link Validation} {@link Check} instances are
* batched up to reduce overhead in the downstream test harness
*
* @param size The maximum number of {@link Check}s that should be executed in a
* single test case
* @return <code>this</code>
*/
public T batching( int size ) {
batchSize = size;
return self();
}

/**
* Typesafe self-reference
*
Expand All @@ -104,8 +122,50 @@ protected T self() {
*
* @return The checks to perform
*/
protected Stream<Validation> checks() {
return checks.stream();
protected Stream<Validation> validations() {
return validations.stream();
}

/**
* Extracts {@link Check} instances by applying the supplied {@link Validation}
* to the {@link Model}, then grouping them up according to the
* {@link #batching(int)} size
*
* @param validation The {@link Validation} to apply
* @return A sequence of batched {@link Check} instances
*/
protected Stream<Check> batchedChecks( Validation validation ) {
if( batchSize < 2 ) {
// early exit for no-batching case
return validation.checks( model );
}

Deque<List<Check>> batches = new ArrayDeque<>();
validation.checks( model )
.forEach( check -> {
if( batches.isEmpty() || batches.getLast().size() >= batchSize ) {
batches.add( new ArrayList<>() );
}
batches.getLast().add( check );
} );

AtomicInteger checkCount = new AtomicInteger( 1 );
return batches.stream()
.map( batch -> {
if( batch.size() == 1 ) {
return batch.get( 0 );
}
return new Check(
validation,
String.format( "%s (%s-%s)",
validation.name(), checkCount.get(), checkCount.addAndGet( batch.size() ) - 1 ),
() -> batch.stream()
.map( Check::check )
.filter( Optional::isPresent )
.map( Optional::get )
.findFirst()
.orElse( null ) );
} );
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.mastercard.test.flow.Model;

/**
* A validation check that can be applied to a {@link Model} or constituents
* A validation behaviour that can be applied to a {@link Model} or constituents
* thereof
*/
public interface Validation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

import static com.mastercard.test.flow.validation.AbstractValidator.defaultChecks;
import static java.util.stream.Collectors.joining;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Arrays;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -39,7 +46,7 @@ void with() {
+ "Model tagging\n"
+ "Result tag misuse\n"
+ "Trace uniqueness",
tv.checks()
tv.validations()
.map( Validation::name )
.collect( joining( "\n" ) ) );
}
Expand Down Expand Up @@ -68,4 +75,113 @@ void acceptance() {
new Violation( null, "meh, this is a minor problem" ) ) );

}

/**
* Illustrates the effect of {@link AbstractValidator#batching(int)}
*/
@Test
void batchedChecks() {
TestValidator tv = new TestValidator()
.with(
validation( "empty", 0 ),
validation( "single", 1 ),
validation( "pair", 2 ),
validation( "triple", 3 ),
validation( "quad", 4 ) );

BiConsumer<Integer, String> test = ( size, expected ) -> assertEquals(
expected,
tv.batching( size )
.validations()
.flatMap( tv::batchedChecks )
.map( Check::name )
.collect( joining( "\n" ) ),
"batch size " + size );

// batch sizes of 1 and lower disable batching behaviour
IntStream.of( -1, 0, 1 )
.forEach( i -> test.accept( i, ""
+ "check pair [0]\n"
+ "check pair [1]\n"
+ "check quad [0]\n"
+ "check quad [1]\n"
+ "check quad [2]\n"
+ "check quad [3]\n"
+ "check single [0]\n"
+ "check triple [0]\n"
+ "check triple [1]\n"
+ "check triple [2]" ) );

test.accept( 2, ""
+ "pair (1-2)\n"
+ "quad (1-2)\n"
+ "quad (3-4)\n"
+ "check single [0]\n"
+ "triple (1-2)\n"
+ "check triple [2]" );

test.accept( 3, ""
+ "pair (1-2)\n"
+ "quad (1-3)\n"
+ "check quad [3]\n"
+ "check single [0]\n"
+ "triple (1-3)" );

test.accept( 10, ""
+ "pair (1-2)\n"
+ "quad (1-4)\n"
+ "check single [0]\n"
+ "triple (1-3)" );
}

/**
* Shows the results of failing batched checks
*/
@Test
void batchedFailures() {
TestValidator tv = new TestValidator()
.with( validation( "pent", 5, 2, 3 ) )
.batching( 5 );

assertEquals(
"Failure on index 2",
tv.validations()
.flatMap( tv::batchedChecks )
.map( Check::check )
.filter( Optional::isPresent )
.map( Optional::get )
.findFirst()
.map( Violation::details )
.orElse( null ),
"Checks 2 and 3 will both fail, but only the first violation is reported" );
}

private static Validation validation( String name, int count, int... failureIndices ) {
return new Validation() {

@Override
public String name() {
return name;
}

@Override
public String explanation() {
return String.format(
"Produces %s checks, %s of which will fail",
count, failureIndices.length );
}

@Override
public Stream<Check> checks( Model model ) {
Arrays.sort( failureIndices );
return IntStream.range( 0, count )
.mapToObj( i -> new Check(
this,
"check " + name + " [" + i + "]",
() -> Arrays.binarySearch( failureIndices, i ) >= 0
? new Violation( this, "Failure on index " + i )
: null ) );
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ public class Validator extends AbstractValidator<Validator> {
* @return A list of <code>{test name, runnable}</code> test parameter pairs
*/
public Collection<Object[]> parameters() {
return checks()
.flatMap( validation -> validation.checks( model() ) )
return validations()
.flatMap( this::batchedChecks )
.map( this::parameterPair )
.collect( toList() );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ public class Validator extends AbstractValidator<Validator> {
* @return A stream of test cases that validate the model
*/
public Stream<DynamicNode> tests() {
return checks()
return validations()
.map( this::container );
}

private DynamicContainer container( Validation validation ) {
return DynamicContainer.dynamicContainer(
validation.name(),
validation.checks( model() )
batchedChecks( validation )
.map( this::test ) );
}

Expand Down

0 comments on commit 4c26a0a

Please sign in to comment.