Commit b8ea9454 authored by Christian Wulf's avatar Christian Wulf

closes #359

parent b8474a2e
......@@ -62,6 +62,10 @@ public class AbstractPort<T> {
this.pipe = pipe;
}
public String getName() {
return name;
}
@SuppressWarnings("PMD.ConfusingTernary")
@Override
public String toString() {
......
......@@ -15,6 +15,9 @@
*/
package teetime.framework;
import java.util.ArrayList;
import java.util.List;
import teetime.framework.pipe.DummyPipe;
import teetime.framework.pipe.InstantiationPipe;
......@@ -34,6 +37,9 @@ public class CompositeStage {
*/
protected static final int DEFAULT_PIPE_CAPACITY = 512;
private final List<InputPort<?>> inputPorts = new ArrayList<>();
private final List<OutputPort<?>> outputPorts = new ArrayList<>();
protected CompositeStage() {
// prohibit direct instantiation of this class
}
......@@ -77,4 +83,24 @@ public class CompositeStage {
new InstantiationPipe<T>(sourcePort, targetPort, capacity);
}
protected <T> InputPort<T> createInputPort(final InputPort<T> subStageInputPort) {
// InputPort<T> inputPort = new InputPort<>(inputPort.getType(), this, inputPort.getName());
inputPorts.add(subStageInputPort);
return subStageInputPort;
}
protected <T> OutputPort<T> createOutputPort(final OutputPort<T> subStageOutputPort) {
// InputPort<T> inputPort = new InputPort<>(inputPort.getType(), this, inputPort.getName());
outputPorts.add(subStageOutputPort);
return subStageOutputPort;
}
public List<InputPort<?>> getInputPorts() {
return inputPorts;
}
public List<OutputPort<?>> getOutputPorts() {
return outputPorts;
}
}
package teetime.framework.test;
import java.util.List;
import teetime.framework.CompositeStage;
import teetime.framework.InputPort;
import teetime.framework.OutputPort;
class CompositeStageUnderTest implements StageUnderTest {
private final CompositeStage compositeStage;
public CompositeStageUnderTest(final CompositeStage compositeStage) {
this.compositeStage = compositeStage;
}
@Override
public List<InputPort<?>> getInputPorts() {
return compositeStage.getInputPorts();
}
@Override
public List<OutputPort<?>> getOutputPorts() {
return compositeStage.getOutputPorts();
}
@Override
public void declareActive() {
// TODO Auto-generated method stub
}
}
......@@ -15,40 +15,31 @@
*/
package teetime.framework.test;
import java.util.Collection;
import java.util.List;
import teetime.framework.AbstractStage;
import teetime.framework.InputPort;
public class InputHolder<I> {
private final StageTester stageTester;
private final AbstractStage stage;
private final Collection<I> inputElements;
private final List<Object> inputElements;
private InputPort<? super I> port;
InputHolder(final StageTester stageTester, final AbstractStage stage, final Collection<I> inputElements) {
@SuppressWarnings("unchecked")
InputHolder(final StageTester stageTester, final List<I> inputElements) {
this.stageTester = stageTester;
this.stage = stage;
this.inputElements = inputElements;
this.inputElements = (List<Object>) inputElements;
}
public StageTestSetup to(final InputPort<? super I> port) { // NOPMD deliberately chosen name
if (port.getOwningStage() != stage) {
@SuppressWarnings("unchecked")
public StageTester to(final InputPort<? super I> inputPort) { // NOPMD deliberately chosen name
List<InputPort<?>> inputPorts = this.stageTester.getStageUnderTest().getInputPorts();
if (!inputPorts.contains(inputPort)) {
throw new InvalidTestCaseSetupException("The given input port does not belong to the stage which should be tested.");
}
this.port = port;
return new StageTestSetup(stageTester);
}
/* default */ Collection<I> getInputElements() {
return inputElements;
}
InputPort<Object> castedPort = (InputPort<Object>) inputPort;
this.stageTester.getInputElementsByPort().put(castedPort, inputElements); // overwrite
/* default */ InputPort<? super I> getPort() {
return port;
return stageTester;
}
}
package teetime.framework.test;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
abstract class MinimalStageTestSetup {
protected MinimalStageTestSetup() {
// do nothing
}
/**
* @param elements
* which serve as input. If nothing should be sent, pass
*/
@SafeVarargs
public final <I> InputHolder<I> send(final I... elements) {
return this.send(Arrays.asList(elements));
}
/**
* @param elements
* which serve as input. If nothing should be sent, pass
*
* <pre>
* Collections.&lt;your type&gt;emptyList().
* </pre>
*/
public abstract <I> InputHolder<I> send(final Collection<I> elements);
/**
* @param actualElements
* which should be tested against the expected elements.
*/
public abstract <O> OutputHolder<O> receive(final List<O> actualElements);
}
......@@ -24,27 +24,22 @@ public class OutputHolder<O> {
private final StageTester stageTester;
private final List<Object> outputElements;
private OutputPort<Object> port;
@SuppressWarnings("unchecked")
OutputHolder(final StageTester stageTester, final List<O> outputList) {
OutputHolder(final StageTester stageTester, final List<O> outputElements) {
this.stageTester = stageTester;
this.outputElements = (List<Object>) outputList;
this.outputElements = (List<Object>) outputElements;
}
@SuppressWarnings("unchecked")
public StageTestSetup from(final OutputPort<O> port) {
this.port = (OutputPort<Object>) port;
return new StageTestSetup(stageTester);
}
/* default */ List<Object> getOutputElements() {
return outputElements;
}
/* default */ OutputPort<Object> getPort() {
return port;
public StageTester from(final OutputPort<O> outputPort) {
List<OutputPort<?>> outputPorts = this.stageTester.getStageUnderTest().getOutputPorts();
if (!outputPorts.contains(outputPort)) {
throw new InvalidTestCaseSetupException("The given output port does not belong to the stage which should be tested.");
}
OutputPort<Object> castedPort = (OutputPort<Object>) outputPort;
this.stageTester.getOutputElementsByPort().put(castedPort, outputElements); // overwrite
return stageTester;
}
}
package teetime.framework.test;
import java.util.List;
import teetime.framework.AbstractStage;
import teetime.framework.InputPort;
import teetime.framework.OutputPort;
import teetime.framework.StageFacade;
class PrimitiveStageUnderTest implements StageUnderTest {
private final AbstractStage stage;
public PrimitiveStageUnderTest(final AbstractStage stage) {
this.stage = stage;
}
@Override
public List<InputPort<?>> getInputPorts() {
return StageFacade.INSTANCE.getInputPorts(stage);
}
@Override
public List<OutputPort<?>> getOutputPorts() {
return StageFacade.INSTANCE.getOutputPorts(stage);
}
@Override
public void declareActive() {
stage.declareActive();
}
}
package teetime.framework.test;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import teetime.framework.OutputPort;
import teetime.stage.CollectorSink;
class Produces<T, O extends OutputPort<T>> extends BaseMatcher<O> {
private final T[] expectedElements;
private final Matcher<Iterable<? extends T>> matcher;
@SafeVarargs
public Produces(final T... expectedElements) {
this.expectedElements = expectedElements;
this.matcher = Matchers.contains(expectedElements);
}
@Override
public boolean matches(final Object item) {
if (!(item instanceof OutputPort)) {
String message = String.format("%s", item);
throw new IllegalArgumentException(message);
}
@SuppressWarnings("unchecked")
OutputPort<T> outputPort = (OutputPort<T>) item;
CollectorSink<T> collectorSink = StageFactory.getSinkFromOutputPort(outputPort);
return matcher.matches(collectorSink.getElements());
// the following invocation does not work with List<int[]>
// return expectedElementsList.equals(collectorSink.getElements());
}
@Override
public void describeTo(final Description description) {
description.appendText("to produce ").appendValueList("", ", ", "", expectedElements);
}
@Override
public void describeMismatch(final Object item, final Description description) {
if (!(item instanceof OutputPort)) {
String message = String.format("%s", item);
throw new IllegalArgumentException(message);
}
@SuppressWarnings("unchecked")
OutputPort<T> outputPort = (OutputPort<T>) item;
CollectorSink<T> collectorSink = StageFactory.getSinkFromOutputPort(outputPort);
description.appendText("has produced ").appendValueList("", ", ", "", collectorSink.getElements());
}
}
package teetime.framework.test;
import java.util.List;
import teetime.framework.AbstractStage;
import teetime.framework.InputPort;
import teetime.framework.OutputPort;
import teetime.stage.CollectorSink;
import teetime.stage.InitialElementProducer;
class StageFactory {
private StageFactory() {
// factory class
}
static <T> InitialElementProducer<T> createProducer(final List<T> inputElements) {
InitialElementProducer<T> producer = new InitialElementProducer<T>(inputElements);
return producer;
}
@SuppressWarnings("unchecked")
static <T> InitialElementProducer<T> getProducerFromInputPort(final InputPort<T> inputPort) {
OutputPort<?> sourcePort = inputPort.getPipe().getSourcePort();
AbstractStage owningStage = sourcePort.getOwningStage();
if (owningStage instanceof InitialElementProducer) {
return (InitialElementProducer<T>) owningStage;
}
String message = String.format("%s", owningStage);
throw new IllegalArgumentException(message);
}
static <T> CollectorSink<T> createSink(final List<T> outputElements) {
CollectorSink<T> sink = new CollectorSink<T>(outputElements);
return sink;
}
@SuppressWarnings("unchecked")
static <T> CollectorSink<T> getSinkFromOutputPort(final OutputPort<T> outputPort) {
InputPort<?> targetPort = outputPort.getPipe().getTargetPort();
AbstractStage owningStage = targetPort.getOwningStage();
if (owningStage instanceof CollectorSink) {
return (CollectorSink<T>) owningStage;
}
String message = String.format("%s", owningStage);
throw new IllegalArgumentException(message);
}
}
/**
* Copyright © 2015 Christian Wulf, Nelson Tavares de Sousa (http://teetime-framework.github.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package teetime.framework.test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import teetime.framework.OutputPort;
public class StageTestResult {
private final Map<OutputPort<?>, List<?>> elementsPerPort = new HashMap<>();
<T> void add(final OutputPort<T> port, final List<T> outputElements) {
elementsPerPort.put(port, outputElements);
}
@SuppressWarnings("unchecked")
public <T> List<? extends T> getElementsFrom(final OutputPort<T> outputPort) {
return (List<T>) elementsPerPort.get(outputPort);
}
}
package teetime.framework.test;
import java.util.Collection;
import java.util.List;
import teetime.framework.Configuration;
import teetime.framework.Execution;
import teetime.framework.ExecutionException;
public class StageTestSetup extends MinimalStageTestSetup {
private final StageTester stageTester;
/* default */ StageTestSetup(final StageTester stageTester) {
this.stageTester = stageTester;
}
public StageTestSetup and() {
return this;
}
/**
* This method will start the test and block until it is finished.
*
* @return
*
* @throws ExecutionException
* if at least one exception in one thread has occurred within the analysis.
* The exception contains the pairs of thread and throwable.
*
*/
public StageTestResult start() {
@SuppressWarnings({ "unchecked", "rawtypes" })
final Configuration configuration = new TestConfiguration(stageTester.getInputHolders(), stageTester.getStage(), stageTester.getOutputHolders());
final Execution<Configuration> analysis = new Execution<Configuration>(configuration);
analysis.executeBlocking();
StageTestResult result = new StageTestResult();
for (OutputHolder<?> outputHolder : stageTester.getOutputHolders()) {
result.add(outputHolder.getPort(), outputHolder.getOutputElements());
}
return result;
}
@Override
public <I> InputHolder<I> send(final Collection<I> elements) {
return stageTester.send(elements);
}
@Override
public <O> OutputHolder<O> receive(final List<O> actualElements) {
return stageTester.receive(actualElements);
}
}
......@@ -15,33 +15,79 @@
*/
package teetime.framework.test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import teetime.framework.AbstractStage;
import teetime.framework.StageState;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import teetime.framework.*;
/**
* This class can be used to test single stages in JUnit test cases.
*
* @author Nils Christian Ehmke
* @author Christian Wulf, Nils Christian Ehmke
*/
public class StageTester extends MinimalStageTestSetup {
public class StageTester {
private final List<InputHolder<?>> inputHolders = new ArrayList<InputHolder<?>>();
private final List<OutputHolder<?>> outputHolders = new ArrayList<OutputHolder<?>>();
private final AbstractStage stage;
private final StageUnderTest stageUnderTest;
private final Map<InputPort<Object>, List<Object>> inputElementsByPort = new HashMap<>();
private final Map<OutputPort<Object>, List<Object>> outputElementsByPort = new HashMap<>();
private StageTester(final AbstractStage stage) {
this.stage = stage;
private StageTester(final StageUnderTest stageUnderTest) {
this.stageUnderTest = stageUnderTest;
}
public static StageTester test(final AbstractStage stage) { // NOPMD
/**
* Prepares to test the given stage.
*
* @param stage
* to be tested
* @return
* a stage test builder
*/
public static StageTester test(final AbstractStage stage) {
StageUnderTest stageUnderTest = new PrimitiveStageUnderTest(stage);
if (stage.getCurrentState() != StageState.CREATED) {
throw new InvalidTestCaseSetupException("This stage has already been tested in this test method. Move this test into a new test method.");
String message = "This stage has already been tested in this test method. Move this test into a new test method.";
throw new InvalidTestCaseSetupException(message);
}
return new StageTester(stageUnderTest);
}
/**
* Prepares to test the given composite stage.
*
* @param compositeStage
* to be tested
* @return
* a stage test builder
*/
public static StageTester test(final CompositeStage compositeStage) {
StageUnderTest stageUnderTest = new CompositeStageUnderTest(compositeStage);
for (InputPort<?> inputPort : compositeStage.getInputPorts()) {
AbstractStage stage = inputPort.getOwningStage();
if (stage.getCurrentState() != StageState.CREATED) {
String message = "This stage has already been tested in this test method. Move this test into a new test method.";
throw new InvalidTestCaseSetupException(message);
}
}
return new StageTester(stage);
return new StageTester(stageUnderTest);
}
/**
* @param elements
* which serve as input. If nothing should be sent, pass
*/
@SafeVarargs
public final <I> InputHolder<I> send(final I... elements) {
return this.send(Arrays.asList(elements));
}
/**
......@@ -52,21 +98,29 @@ public class StageTester extends MinimalStageTestSetup {
* Collections.&lt;your type&gt;emptyList().
* </pre>
*/
@Override
public <I> InputHolder<I> send(final Collection<I> elements) {
final InputHolder<I> inputHolder = new InputHolder<I>(this, stage, elements);
this.inputHolders.add(inputHolder);
public <I> InputHolder<I> send(final List<I> elements) {
final InputHolder<I> inputHolder = new InputHolder<I>(this, elements);
return inputHolder;
}
/**
* @param actualElements
* which should be tested against the expected elements.
*
* @deprecated since 3.0. Use the following code instead:
*
* <pre>
* {@code
* import static StageTester.*;
* ...
* assertThat(stage.getOutputPort(), produces(1,2,3));
* }
* </pre>
*
*/
@Override
@Deprecated
public <O> OutputHolder<O> receive(final List<O> actualElements) {
final OutputHolder<O> outputHolder = new OutputHolder<O>(this, actualElements);
this.outputHolders.add(outputHolder);
return outputHolder;
}
......@@ -77,16 +131,36 @@ public class StageTester extends MinimalStageTestSetup {
return this;
}
/* default */ List<InputHolder<?>> getInputHolders() {
return inputHolders;
/**
* This method will start the test and block until it is finished.
*
* @throws ExecutionException
* if at least one exception in one thread has occurred within the analysis.
* The exception contains the pairs of thread and throwable.
*
*/
public void start() {
final Configuration configuration = new TestConfiguration(this);
final Execution<Configuration> analysis = new Execution<Configuration>(configuration);
analysis.executeBlocking();
}
/* default */ StageUnderTest getStageUnderTest() {
return stageUnderTest;
}
/* default */ Map<InputPort<Object>, List<Object>> getInputElementsByPort() {
return inputElementsByPort;