Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ public enum CorrelationLocation {
*/
public Long replyTimeoutMs;

/**
* How long to keep the action open once the driver has returned, in milliseconds. The SUT
* handles a message on a thread of its own, possibly after the driver saw the publish
* acknowledged or the reply arrive, and what it covers meanwhile must be credited to this
* action, not to the next one or to none.
*
* The wait is done by the controller, so drivers need not implement it. Null, zero or
* negative mean no wait. Only set when the SUT is instrumented.
*/
public Long settleMs;

/**
* The language to render {@link AsyncApiReplyDto#testScript} in. Only Java and Kotlin are
* ever asked for.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ public class AsyncApiReplyDto {
*/
public Long waitedMs;

/**
* How long the controller kept the action open after the driver returned, in milliseconds,
* as asked by {@link AsyncApiActionDto#settleMs}. It can be shorter than what was asked when
* the wait was interrupted.
*
* Filled in by the controller rather than by the driver, which never sees this field set.
* Null when no wait was asked for, or when the message could not be published.
*/
public Long settledMs;

/**
* Why publishing failed, when it did.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.evomaster.client.java.controller.internal;

import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiActionDto;
import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiReplyDto;

import java.util.Objects;

/**
* Keeps an AsyncAPI action open for a while after the driver has returned from publishing it,
* so that what the service does with the message, on a thread of its own, is still attributed
* to that action. Done in the controller so that drivers need not implement it, and the core
* pays no extra round trip.
*/
public class AsyncApiSettle {

private static final long NANOS_PER_MILLI = 1_000_000L;

/**
* Where the time comes from, in milliseconds. Only the difference between two readings is
* ever used, so it does not have to be wall-clock time.
*/
interface Clock {
long nowMs();
}

/**
* How the waiting thread is put to sleep.
*/
interface Sleeper {
void sleep(long ms) throws InterruptedException;
}

private final Clock clock;

private final Sleeper sleeper;

/**
* Sleeps for real, and measures with {@link System#nanoTime()}, which does not jump when the
* system clock changes.
*/
public AsyncApiSettle() {
this(() -> System.nanoTime() / NANOS_PER_MILLI, Thread::sleep);
}

AsyncApiSettle(Clock clock, Sleeper sleeper) {
this.clock = Objects.requireNonNull(clock);
this.sleeper = Objects.requireNonNull(sleeper);
}

/**
* Wait as long as {@link AsyncApiActionDto#settleMs} asks, and record in
* {@link AsyncApiReplyDto#settledMs} how long that actually was. Nothing is waited, and
* nothing recorded, when no wait is asked for.
*
* The thread calling this is a request thread of the controller, whose code is never
* instrumented, so the kill switch does not interrupt it. If it is interrupted all the same,
* the wait ends there, the interrupt is left set for whoever owns the thread, and the time
* actually waited is what gets recorded.
*
* @param dto the action that was just published
* @param reply what the driver reported about it, to which the wait is added
*/
public void settle(AsyncApiActionDto dto, AsyncApiReplyDto reply) {
Objects.requireNonNull(dto);
Objects.requireNonNull(reply);

Long settleMs = dto.settleMs;
if (settleMs == null || settleMs <= 0) {
return;
}

long start = clock.nowMs();
try {
sleeper.sleep(settleMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
reply.settledMs = clock.nowMs() - start;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public class EMController {

private String baseUrlOfSUT;

/**
* Keeps an AsyncAPI action open after the driver has published it, as the core asks.
*/
private final AsyncApiSettle asyncApiSettle;

/**
* Keep track of all host:port clients connect so far.
* This is the mainly done for debugging, to check that we are using
Expand All @@ -79,7 +84,15 @@ public class EMController {
new Scanner(EMController.class.getResourceAsStream("/warning.html"), "UTF-8").useDelimiter("\\A").next();

public EMController(SutController sutController) {
this(sutController, new AsyncApiSettle());
}

/**
* Only for tests, to wait on a clock they control.
*/
EMController(SutController sutController, AsyncApiSettle asyncApiSettle) {
this.sutController = Objects.requireNonNull(sutController);
this.asyncApiSettle = Objects.requireNonNull(asyncApiSettle);
}

private boolean trackRequestSource(HttpServletRequest request) {
Expand Down Expand Up @@ -853,7 +866,6 @@ public Response newAction(

try {
sutController.executeAsyncApiAction(dto.asyncApiCall, replyDto);
return Response.status(200).entity(WrappedResponseDto.withData(replyDto)).build();
} catch (Exception e) {
/*
Failing to publish is not a finding about the service, it is a broken
Expand All @@ -866,6 +878,26 @@ public Response newAction(
replyDto.errorMessage = msg;
return Response.status(500).entity(WrappedResponseDto.withData(replyDto)).build();
}

if (!Boolean.TRUE.equals(replyDto.published)) {
/*
The driver says the message was not published, or does not say: nothing
to wait for. The core stops the test here, as after a failure.
*/
return Response.status(200).entity(WrappedResponseDto.withData(replyDto)).build();
}

/*
The driver is done, but the service may not be: it handles the message on a
thread of its own. The action stays open while it does, so that what it
covers is still credited to this action rather than to the next one, or
lost once the results are collected. Not inside a noKillSwitch: the switch
was already turned off for this action by newAction above, and must stay
off for the service's threads, not only this one.
*/
asyncApiSettle.settle(dto.asyncApiCall, replyDto);

return Response.status(200).entity(WrappedResponseDto.withData(replyDto)).build();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.evomaster.client.java.controller.internal;

import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiActionDto;
import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiReplyDto;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

public class AsyncApiSettleTest {

/**
* A clock that only moves when the fake sleeper sleeps, so that nothing here depends on how
* long anything really takes.
*/
private long now = 1_000;

/**
* Every sleep asked for, in order.
*/
private final List<Long> sleeps = new ArrayList<>();

private AsyncApiSettle settleThatSleeps() {
return new AsyncApiSettle(() -> now, ms -> {
sleeps.add(ms);
now += ms;
});
}

private static AsyncApiActionDto asking(Long settleMs) {
AsyncApiActionDto dto = new AsyncApiActionDto();
dto.settleMs = settleMs;
return dto;
}

@AfterEach
public void clearInterrupt() {
//a test that leaves the flag set would interrupt whatever runs next on this thread
Thread.interrupted();
}

@Test
public void testTheActionIsKeptOpenForAsLongAsAsked() {

AsyncApiReplyDto reply = new AsyncApiReplyDto();

settleThatSleeps().settle(asking(250L), reply);

assertEquals(1, sleeps.size());
assertEquals(250L, sleeps.get(0));
assertEquals(250L, reply.settledMs);
}

@Test
public void testNothingIsWaitedNorRecordedWhenNoWaitIsAskedFor() {

AsyncApiSettle settle = settleThatSleeps();

for (Long none : new Long[]{null, 0L, -5L}) {
AsyncApiReplyDto reply = new AsyncApiReplyDto();
settle.settle(asking(none), reply);
assertNull(reply.settledMs, "a wait was recorded for settleMs=" + none);
}

assertTrue(sleeps.isEmpty(), "slept although no wait was asked for: " + sleeps);
}

@Test
public void testAnInterruptedWaitEndsThereAndKeepsTheInterrupt() {

/*
The thread belongs to the HTTP server, so the interrupt is not this class's to
swallow: it ends the wait, and stays set for whoever owns the thread.
*/
AsyncApiSettle settle = new AsyncApiSettle(() -> now, ms -> {
now += 40;
throw new InterruptedException();
});

AsyncApiReplyDto reply = new AsyncApiReplyDto();
settle.settle(asking(250L), reply);

assertTrue(Thread.currentThread().isInterrupted(), "the interrupt was swallowed");
assertEquals(40L, reply.settledMs, "what was recorded is not the time actually waited");
}

@Test
public void testWhatTheDriverReportedIsLeftAlone() {

AsyncApiReplyDto reply = new AsyncApiReplyDto();
reply.published = true;
reply.replyExpected = true;
reply.replyReceived = true;
reply.waitedMs = 12L;

settleThatSleeps().settle(asking(100L), reply);

assertTrue(reply.published);
assertTrue(reply.replyReceived);
assertEquals(12L, reply.waitedMs, "the driver's wait for the reply is not the controller's settle");
assertEquals(100L, reply.settledMs);
}

@Test
public void testTheActionAndTheReplyCannotBeNull() {

AsyncApiSettle settle = settleThatSleeps();

assertThrows(NullPointerException.class, () -> settle.settle(null, new AsyncApiReplyDto()));
assertThrows(NullPointerException.class, () -> settle.settle(asking(1L), null));
}

@Test
public void testTheRealClockAndSleeperWait() {

/*
Only that a wait is recorded, never how long it took: a loaded machine can always
take longer, and an assertion on that would be flaky.
*/
AsyncApiReplyDto reply = new AsyncApiReplyDto();

new AsyncApiSettle().settle(asking(1L), reply);

assertNotNull(reply.settledMs);
assertTrue(reply.settledMs >= 0, "negative wait: " + reply.settledMs);
}
}
Loading
Loading