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
37 changes: 36 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,39 @@ Before you build this project, you need to generate GraphQL client codes. All yo
mvn generate-sources
```

After generated sources, you can check out sources in the `/target/generated-sources`
After generated sources, you can check out sources in the `/target/generated-sources`

### **3. Run tests**

The default test suite must be deterministic and should not require a running
LitmusChaos server:

```git
./mvnw test
./mvnw package
./mvnw javadoc:javadoc
```

Tests that call a live LitmusChaos server are tagged as `integration` and are
excluded from the default Maven test run. Run them only when you have a local or
test LitmusChaos instance and a disposable test account/token:

```git
export LITMUS_TEST_HOST=http://localhost:9091
export LITMUS_TEST_TOKEN=<api-token>
export LITMUS_TEST_USER_ID=<test-user-id>
./mvnw test -Dgroups=integration -DexcludedGroups=
```

Tests that delete an API token also require `LITMUS_TEST_DELETE_TOKEN`.

To verify only the experiment-run polling helper against a live LitmusChaos
server, provide a project ID and an existing experiment run ID:

```git
export LITMUS_TEST_HOST=http://localhost:9091
export LITMUS_TEST_TOKEN=<api-token>
export LITMUS_TEST_PROJECT_ID=<project-id>
export LITMUS_TEST_EXPERIMENT_RUN_ID=<experiment-run-id>
./mvnw test -Dgroups=integration -DexcludedGroups= -Dtest=ExperimentRunLiveTest
```
6 changes: 5 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<maven-surefire-plugin.version>3.4.0</maven-surefire-plugin.version>
<build-helper-maven-plugin.version>3.2.0</build-helper-maven-plugin.version>
<maven-javadoc-plugin.version>3.11.2</maven-javadoc-plugin.version>
<excludedGroups>integration</excludedGroups>
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -154,6 +155,9 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<excludedGroups>${excludedGroups}</excludedGroups>
</configuration>
</plugin>
</plugins>
</build>
Expand Down Expand Up @@ -207,4 +211,4 @@
</plugins>
</build>

</project>
</project>
40 changes: 39 additions & 1 deletion src/main/java/io/litmuschaos/LitmusClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import io.litmuschaos.http.LitmusHttpClient;
import io.litmuschaos.request.*;
import io.litmuschaos.response.*;
import io.litmuschaos.util.ExperimentRunPoller;
import okhttp3.OkHttpClient;

import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -938,6 +940,42 @@ public ExperimentRun getExperimentRun(GetExperimentRunGraphQLQuery query, GetExp
return graphQLClient.query(request, GET_EXPERIMENT_RUN, new TypeRef<ExperimentRun>(){});
}

/**
* Waits for a chaos experiment run to reach a terminal phase.
*
* @param projectId project ID that owns the experiment run
* @param experimentRunId experiment run ID to poll
* @param timeout maximum time to wait
* @param interval delay between state checks
* @return the latest experiment run once it reaches a terminal phase
* @throws LitmusApiException when fetching fails, waiting is interrupted, or the timeout expires
* @throws IOException when fetching the experiment run fails
*/
public ExperimentRun waitForExperimentRun(String projectId, String experimentRunId, Duration timeout,
Duration interval) throws LitmusApiException, IOException {
GetExperimentRunGraphQLQuery query = new GetExperimentRunGraphQLQuery.Builder()
.projectID(projectId)
.experimentRunID(experimentRunId)
.build();

GetExperimentRunProjectionRoot projectionRoot = new GetExperimentRunProjectionRoot<>()
.projectID()
.experimentRunID()
.experimentID()
.experimentName()
.experimentType()
.notifyID()
.phase().root()
.resiliencyScore()
.runSequence();

return new ExperimentRunPoller().waitFor(
() -> getExperimentRun(query, projectionRoot),
timeout,
interval
);
}

/**
* Get the chaos experiment run stats.
*
Expand Down Expand Up @@ -1356,4 +1394,4 @@ public void close() throws Exception {
this.okHttpClient.cache().close();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.litmuschaos.exception;

public class ExperimentRunTimeoutException extends LitmusApiException {

public ExperimentRunTimeoutException(String message) {
super(message);
}
}
115 changes: 115 additions & 0 deletions src/main/java/io/litmuschaos/util/ExperimentRunPoller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package io.litmuschaos.util;

import io.litmuschaos.exception.ExperimentRunTimeoutException;
import io.litmuschaos.exception.LitmusApiException;
import io.litmuschaos.generated.types.ExperimentRun;
import io.litmuschaos.generated.types.ExperimentRunStatus;

import java.io.IOException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

public class ExperimentRunPoller {

private static final Set<ExperimentRunStatus> TERMINAL_PHASES = EnumSet.of(
ExperimentRunStatus.Completed,
ExperimentRunStatus.Completed_With_Error,
ExperimentRunStatus.Stopped,
ExperimentRunStatus.Skipped,
ExperimentRunStatus.Error,
ExperimentRunStatus.Timeout,
ExperimentRunStatus.Terminated
);

private final Clock clock;
private final Sleeper sleeper;

public ExperimentRunPoller() {
this(Clock.systemUTC(), duration -> Thread.sleep(duration.toMillis()));
}

ExperimentRunPoller(Clock clock, Sleeper sleeper) {
this.clock = Objects.requireNonNull(clock, "clock must not be null");
this.sleeper = Objects.requireNonNull(sleeper, "sleeper must not be null");
}

/**
* Waits until an experiment run reaches a terminal phase.
*
* @param fetcher fetches the latest experiment run state
* @param timeout maximum time to wait
* @param interval delay between state checks
* @return the latest experiment run once it reaches a terminal phase
* @throws IOException when fetching the experiment run fails
* @throws LitmusApiException when fetching fails, waiting is interrupted, or the timeout expires
*/
public ExperimentRun waitFor(ExperimentRunFetcher fetcher, Duration timeout, Duration interval)
throws IOException, LitmusApiException {
validate(fetcher, timeout, interval);

Instant deadline = clock.instant().plus(timeout);
while (true) {
ExperimentRun experimentRun = fetcher.fetch();
if (experimentRun == null) {
throw new LitmusApiException("Experiment run fetcher returned null.");
}
if (isTerminal(experimentRun.getPhase())) {
return experimentRun;
}

Instant now = clock.instant();
if (!now.isBefore(deadline)) {
throw timeoutException(timeout);
}

sleepUntilNextPoll(now, deadline, interval);
}
}

public static boolean isTerminal(ExperimentRunStatus phase) {
return TERMINAL_PHASES.contains(phase);
}

private void sleepUntilNextPoll(Instant now, Instant deadline, Duration interval) throws LitmusApiException {
Duration remaining = Duration.between(now, deadline);
Duration sleepDuration = interval.compareTo(remaining) < 0 ? interval : remaining;
try {
sleeper.sleep(sleepDuration);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LitmusApiException("Interrupted while waiting for experiment run.", e);
}
}

private void validate(ExperimentRunFetcher fetcher, Duration timeout, Duration interval) {
Objects.requireNonNull(fetcher, "fetcher must not be null");
Objects.requireNonNull(timeout, "timeout must not be null");
Objects.requireNonNull(interval, "interval must not be null");
if (timeout.isZero() || timeout.isNegative()) {
throw new IllegalArgumentException("timeout must be positive");
}
if (interval.isZero() || interval.isNegative()) {
throw new IllegalArgumentException("interval must be positive");
}
}

private ExperimentRunTimeoutException timeoutException(Duration timeout) {
return new ExperimentRunTimeoutException(
"Experiment run did not reach a terminal phase within " + timeout + "."
);
}

@FunctionalInterface
public interface ExperimentRunFetcher {
ExperimentRun fetch() throws IOException, LitmusApiException;
}

@FunctionalInterface
interface Sleeper {
void sleep(Duration duration) throws InterruptedException;
}
}
7 changes: 4 additions & 3 deletions src/test/java/io/litmuschaos/AuthTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
import io.litmuschaos.exception.LitmusApiException;
import io.litmuschaos.response.CapabilityResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;

@Tag("integration")
public class AuthTest {

private static final String HOST_URL = "http://localhost:3000";
private static final String TEST_TOKEN = "Bearer token"; // Put your token here
private LitmusClient authClient;

@BeforeEach
public void setup() {
this.authClient = new LitmusClient(HOST_URL, TEST_TOKEN);
IntegrationTestConfig.assumeConfigured();
this.authClient = new LitmusClient(IntegrationTestConfig.hostUrl(), IntegrationTestConfig.token());
}

@Test
Expand Down
7 changes: 4 additions & 3 deletions src/test/java/io/litmuschaos/BuildRequestTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@
import io.litmuschaos.request.ListProjectRequest;
import io.litmuschaos.response.ListProjectsResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;

@Tag("integration")
public class BuildRequestTest {

private static final String HOST_URL = "http://localhost:3000";
private static final String TEST_TOKEN = "Bearer token"; // Put your token here
private LitmusClient litmusClient;

@BeforeEach
public void setup() throws IOException, LitmusApiException {
this.litmusClient = new LitmusClient(HOST_URL, TEST_TOKEN);
IntegrationTestConfig.assumeConfigured();
this.litmusClient = new LitmusClient(IntegrationTestConfig.hostUrl(), IntegrationTestConfig.token());
}

@Test
Expand Down
51 changes: 51 additions & 0 deletions src/test/java/io/litmuschaos/ExperimentRunLiveTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.litmuschaos;

import io.litmuschaos.exception.LitmusApiException;
import io.litmuschaos.generated.types.ExperimentRun;
import io.litmuschaos.generated.types.ExperimentRunStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.time.Duration;
import java.util.EnumSet;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

@Tag("integration")
class ExperimentRunLiveTest {

private static final Set<ExperimentRunStatus> TERMINAL_STATUSES = EnumSet.of(
ExperimentRunStatus.Completed,
ExperimentRunStatus.Completed_With_Error,
ExperimentRunStatus.Stopped,
ExperimentRunStatus.Skipped,
ExperimentRunStatus.Error,
ExperimentRunStatus.Timeout,
ExperimentRunStatus.Terminated
);

private LitmusClient litmusClient;

@BeforeEach
void setup() {
IntegrationTestConfig.assumeConfigured();
litmusClient = new LitmusClient(IntegrationTestConfig.hostUrl(), IntegrationTestConfig.token());
}

@Test
void waitForExperimentRunReturnsTerminalExperimentRunFromLiveServer()
throws LitmusApiException, IOException {
ExperimentRun experimentRun = litmusClient.waitForExperimentRun(
IntegrationTestConfig.projectId(),
IntegrationTestConfig.experimentRunId(),
Duration.ofSeconds(30),
Duration.ofSeconds(2)
);

assertThat(experimentRun.getExperimentRunID()).isEqualTo(IntegrationTestConfig.experimentRunId());
assertThat(experimentRun.getPhase()).isIn(TERMINAL_STATUSES);
}
}
Loading