Answering Requests¶
So far in our examples, we have used some form of a request-response pattern where we verify that the Pinger
sends a Ping
request with some ìd
and the Ponger
responds with a Pong
of the same id
.
Since we know the value of id
that each ping-pong exchange should agree on, we could easily specify matchings for them - e.g using the new Ping(8)
or an equivalent matchesPing8
predicate).
However, in some cases, the id
might be some randomly generated value, in which case we can not accurately specify matchings for the request and response events (we could match by class but that doesn’t tell us much).
Kompics offers some constructs for supporting a specialized instance of such scenarios - where request events go out of the CUT and response events come into it.
Here, instead of a dependency component, you provide a response for outgoing requests.
For the examples in this section we change our Pinger
component definition to the following. It triggers three Ping
events with id
s 1-3 on start.
package se.sics.test;
import se.sics.kompics.ComponentDefinition;
import se.sics.kompics.Handler;
import se.sics.kompics.Positive;
import se.sics.kompics.Start;
public class Pinger extends ComponentDefinition {
public static int pongsReceived = 0;
Positive<PingPongPort> ppp = requires(PingPongPort.class);
Handler<Start> startHandler = new Handler<Start>(){
public void handle(Start event) {
for (int i = 1; i <= 3; i++) {
trigger(new Ping(i), ppp);
}
}
};
Handler<Pong> pongHandler = new Handler<Pong>() {
public void handle(Pong pong) {
pongsReceived++;
}
};
{
subscribe(startHandler, control);
subscribe(pongHandler, ppp);
}
}
Answering Requests Immediately¶
The first construct is the answerRequest
statement that matches an outgoing event of a specified class by calling a provided com.google.common.base.Function<RQ, RS>
instance (called the mapper) with an observed event as argument.
The mapper contains your logic for verifying that the event is indeed the expected request event and returns a response event in that case otherwise null
is returned.
The framework immediately triggers the response event on a specified port, presumably as an incoming event to the CUT.
The following example shows a test case matching the three triggered events of our new component under test Pinger
.
Here we treat Ping
1 and 3 as requests, triggering the response provided by our pingToPongMapper
back into the response port pingerPort
(the last argument to answerRequest
).
The Ping
2 event on the other hand is simply matched normally and in this case, thrown away since our port is not connected to any other ports.
package se.sics.test;
import com.google.common.base.Function;
import se.sics.kompics.Component;
import se.sics.kompics.Negative;
import se.sics.kompics.testing.Direction;
import se.sics.kompics.testing.TestContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class Main {
static Function<Ping, Pong> pingToPongMapper = new Function<Ping, Pong> () {
public Pong apply(Ping ping) {
return new Pong(ping.id);
}
};
public static void main(String[] args) { // answerRequestExample
TestContext<Pinger> tc = TestContext.newInstance(Pinger.class);
Component pinger = tc.getComponentUnderTest();
Negative<PingPongPort> pingerPort = pinger.getNegative( PingPongPort.class);
tc.setComparator(Ping.class, Ping.comparator);
// setup done
tc.body()
// treat ping(1) as a request and trigger pong(2) on the same port
.answerRequest(Ping.class, pingerPort, pingToPongMapper, pingerPort)
// treat ping(2) as a normal event
.expect(Ping.class, pingerPort, Direction.OUT)
// treat ping(3) as a request and trigger pong(3) on the same port
.answerRequest(Ping.class, pingerPort, pingToPongMapper, pingerPort)
;
assertTrue(tc.check());
assertEquals(2, Pinger.pongsReceived);
}
}
The entire test case can also be downloaded here
.
Answering Requests After a While¶
By default, answerRequest
triggers the generated response event right away on the response port but this might not always be desired.
In some cases we might want to wait a while before triggering the responses.
You can force the events to be triggered only after all requests have been received by using the answerRequests
construct of the form answerRequests() - end()
.
Here, -
is a sequence of answerRequest
statements that will be answered in that order only after all requests have been received.
The following example shows such a usage.
Calling answerRequests()
enters ANSWER_REQUEST
mode while the matching end()
restores the previous mode.
public static void main(String[] args) { // answerRequestsExample
// ... setup code as in answerRequestExample ...
// setup done
tc.body()
// treat all three pings as requests
.answerRequests()
.answerRequest(Ping.class, pingerPort, pingToPongMapper, pingerPort) // ping 1
.answerRequest(Ping.class, pingerPort, pingToPongMapper, pingerPort) // ping 2
.answerRequest(Ping.class, pingerPort, pingToPongMapper, pingerPort) // ping 3
// After receiving ping 3, trigger pongs 1,2,3 in that order back on the response port 'pingerPort'
.end()
;
assertTrue(tc.check());
assertEquals(3, Pinger.pongsReceived);
}
Answering Requests In any Order¶
To match outgoing requests in any order, you can group the answerRequest
statements within an unordered
statement.
This statement also has a variant unordered(boolean)
where supplying true
as argument triggers each response event immediately after the request is matched and false
triggers them in the order that they were received, after all requests have been matched.
Answering Requests In the Future¶
Using a mapper function, answerRequest
gives limited options as to when to trigger the response events (immediately or when all requests have been received).
If you need better control over which and when response events to requests are triggered, you can provide se.sics.kompics.testing.Future
instance instead of a mapper.
The Future
abstract class declares a set
and get
method.
The set
method when called with an event as argument returns true
if the event was matched and in which case, a subsequent call to get
should provide a response event.
However, get
is only called if that same Future
instance is used later on in a trigger
statement - the framework triggers the provided response event on the specified port.
Consequently, whether or not a response is triggered and in what order is entirely up to you.
In the following example, we match our requests in any order and choose to only trigger one response back to our CUT.
class PingPongFuture extends Future<Ping, Pong> {
private Pong pong;
private int id;
PingPongFuture(int id) { this.id = id; }
public boolean set(Ping ping) {
if (ping.id != id) {
return false;
}
// success
pong = new Pong(ping.id);
return true;
}
public Pong get() {
// after a successful call to set(), get must succeed.
return pong;
}
}
PingPongFuture future1 = new PingPongFuture(1);
PingPongFuture future2 = new PingPongFuture(2);
PingPongFuture future3 = new PingPongFuture(3);
public static void main(String[] args) { // futureExample
// ... setup code as in answerRequestExample ...
// setup done
tc.body()
// treat all three pings as requests and match them in any order
.unordered()
.answerRequest(Ping.class, pingerPort, future3) // ping 3
.answerRequest(Ping.class, pingerPort, future2) // ping 2
.answerRequest(Ping.class, pingerPort, future1) // ping 1
.end()
// set() for all futures must have succeeded
// trigger only pong 2 back as response
.trigger(future2, pingerPort)
;
assertTrue(tc.check());
assertEquals(1, Pinger.pongsReceived);
}