mirror of https://github.com/hyperledger/besu
[NC-1449] test docker quickstart (#41)
* Added docker quickstart test - quickstart folder is now a module - added junit tests that runs the bash scripts - added new gradle task for testing quickstart scripts - added jenkinsfile step for the gradle task - switched the build image to one with docker in it - split into nodes and docker env - parallel jobs - modified host usage in bash scripts to take the docker-in-docker architecture in account
parent
9058ce743d
commit
2b098f5840
@ -0,0 +1,20 @@ |
|||||||
|
dependencies { |
||||||
|
testRuntime 'org.apache.logging.log4j:log4j-core' |
||||||
|
testRuntime 'org.apache.logging.log4j:log4j-slf4j-impl' |
||||||
|
|
||||||
|
testImplementation 'junit:junit' |
||||||
|
testImplementation 'org.assertj:assertj-core' |
||||||
|
testImplementation 'org.web3j:core' |
||||||
|
testImplementation 'org.awaitility:awaitility' |
||||||
|
testImplementation 'com.squareup.okhttp3:okhttp' |
||||||
|
testImplementation 'io.vertx:vertx-core' |
||||||
|
testImplementation project( path: ':pantheon') |
||||||
|
} |
||||||
|
|
||||||
|
test.enabled = false |
||||||
|
|
||||||
|
task dockerQuickstartTest(type: Test) { |
||||||
|
mustRunAfter rootProject.subprojects*.test |
||||||
|
description = 'Runs Pantheon Docker quickstart tests.' |
||||||
|
group = 'verification' |
||||||
|
} |
@ -0,0 +1,339 @@ |
|||||||
|
package tech.pegasys.pantheon.tests.quickstart; |
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8; |
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.PantheonInfo; |
||||||
|
import tech.pegasys.pantheon.tests.quickstart.DockerQuickstartTest.Service.ExposedPort; |
||||||
|
|
||||||
|
import java.io.BufferedReader; |
||||||
|
import java.io.File; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStreamReader; |
||||||
|
import java.lang.ProcessBuilder.Redirect; |
||||||
|
import java.nio.file.Paths; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.EnumMap; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
import java.util.regex.Matcher; |
||||||
|
import java.util.regex.Pattern; |
||||||
|
|
||||||
|
import io.vertx.core.Vertx; |
||||||
|
import io.vertx.core.http.HttpClientOptions; |
||||||
|
import io.vertx.core.http.RequestOptions; |
||||||
|
import io.vertx.core.http.WebSocket; |
||||||
|
import okhttp3.OkHttpClient; |
||||||
|
import okhttp3.Request; |
||||||
|
import okhttp3.Response; |
||||||
|
import org.awaitility.Awaitility; |
||||||
|
import org.junit.After; |
||||||
|
import org.junit.AfterClass; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.BeforeClass; |
||||||
|
import org.junit.Test; |
||||||
|
import org.web3j.protocol.Web3j; |
||||||
|
import org.web3j.protocol.http.HttpService; |
||||||
|
import org.web3j.utils.Async; |
||||||
|
|
||||||
|
public class DockerQuickstartTest { |
||||||
|
|
||||||
|
private static final String PROJECT_ROOT = |
||||||
|
Paths.get(System.getProperty("user.dir")).getParent().toString(); |
||||||
|
private static final String DOCKER_COMPOSE_PROJECT_PREFIX = "quickstart_"; |
||||||
|
private static final int DEFAULT_HTTP_RPC_PORT = 8545; |
||||||
|
private static final int DEFAULT_WS_RPC_PORT = 8546; |
||||||
|
private static final String DEFAULT_RPC_HOST = |
||||||
|
Optional.ofNullable(System.getenv("DOCKER_PORT_2375_TCP_ADDR")).orElse("localhost"); |
||||||
|
private static final String DEFAULT_HTTP_RPC_HOST = "http://" + DEFAULT_RPC_HOST; |
||||||
|
private final Map<ServicesIdentifier, Service> services = new EnumMap<>(ServicesIdentifier.class); |
||||||
|
private final Map<EndpointsIdentifier, String> endpoints = |
||||||
|
new EnumMap<>(EndpointsIdentifier.class); |
||||||
|
private Web3j web3HttpClient; |
||||||
|
|
||||||
|
@BeforeClass |
||||||
|
public static void runPantheonPrivateNetwork() throws IOException, InterruptedException { |
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder("quickstart/runPantheonPrivateNetwork.sh"); |
||||||
|
processBuilder.directory(new File(PROJECT_ROOT)); // going up one level is the project root
|
||||||
|
processBuilder.inheritIO(); // redirect all output to logs
|
||||||
|
Process process = processBuilder.start(); |
||||||
|
|
||||||
|
int exitValue = process.waitFor(); |
||||||
|
|
||||||
|
if (exitValue != 0) { |
||||||
|
// check for errors, error messages and causes are redirected to logs already
|
||||||
|
throw new RuntimeException("execution of script failed!"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Before |
||||||
|
public void listQuickstartServices() throws IOException, InterruptedException { |
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder("quickstart/listQuickstartServices.sh"); |
||||||
|
processBuilder.directory(new File(PROJECT_ROOT)); // going up one level is the project root
|
||||||
|
// redirect only error output to logs as we want to be able to
|
||||||
|
// keep the standard output available for reading
|
||||||
|
processBuilder.redirectError(Redirect.INHERIT); |
||||||
|
Process process = processBuilder.start(); |
||||||
|
|
||||||
|
int exitValue = process.waitFor(); |
||||||
|
|
||||||
|
if (exitValue != 0) { |
||||||
|
// check for errors, error messages and causes are redirected to logs already
|
||||||
|
throw new RuntimeException("execution of script failed!"); |
||||||
|
} |
||||||
|
|
||||||
|
BufferedReader reader = |
||||||
|
new BufferedReader(new InputStreamReader(process.getInputStream(), UTF_8)); |
||||||
|
|
||||||
|
reader.lines().forEach(this::populateServicesAndEndpoints); |
||||||
|
reader.close(); |
||||||
|
|
||||||
|
assertThat(services).isNotNull().isNotEmpty(); |
||||||
|
assertThat(endpoints).isNotNull().isNotEmpty(); |
||||||
|
|
||||||
|
web3HttpClient = |
||||||
|
Web3j.build( |
||||||
|
new HttpService( |
||||||
|
DEFAULT_HTTP_RPC_HOST |
||||||
|
+ ":" |
||||||
|
+ services |
||||||
|
.get(ServicesIdentifier.RPCNODE) |
||||||
|
.exposedPorts |
||||||
|
.get(DEFAULT_HTTP_RPC_PORT) |
||||||
|
.externalPort), |
||||||
|
2000, |
||||||
|
Async.defaultExecutorService()); |
||||||
|
|
||||||
|
assertThat(web3HttpClient).isNotNull(); |
||||||
|
} |
||||||
|
|
||||||
|
private void populateServicesAndEndpoints(final String line) { |
||||||
|
// We check that the output of the script displays the right endpoints and services states
|
||||||
|
// each endpoint and service will be stored in a map for later use.
|
||||||
|
|
||||||
|
for (ServicesIdentifier servicesIdentifier : ServicesIdentifier.values()) { |
||||||
|
Matcher regexMatcher = servicesIdentifier.pattern.matcher(line); |
||||||
|
if (regexMatcher.find()) { |
||||||
|
Service service = new Service(); |
||||||
|
service.name = regexMatcher.group(1); |
||||||
|
service.state = regexMatcher.group(2).toLowerCase(); |
||||||
|
String portMappings[] = regexMatcher.group(3).split(",", -1); |
||||||
|
for (String mapping : portMappings) { |
||||||
|
ExposedPort port = new ExposedPort(mapping); |
||||||
|
service.exposedPorts.put(port.internalPort, port); |
||||||
|
} |
||||||
|
services.put(servicesIdentifier, service); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (EndpointsIdentifier endpointsIdentifier : EndpointsIdentifier.values()) { |
||||||
|
Matcher regexMatcher = endpointsIdentifier.pattern.matcher(line); |
||||||
|
if (regexMatcher.find()) { |
||||||
|
String endpoint = regexMatcher.group(1); |
||||||
|
endpoints.put(endpointsIdentifier, endpoint); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@After |
||||||
|
public void closeConnections() { |
||||||
|
assertThat(web3HttpClient).isNotNull(); |
||||||
|
web3HttpClient.shutdown(); |
||||||
|
} |
||||||
|
|
||||||
|
@AfterClass |
||||||
|
public static void removePantheonPrivateNetwork() throws IOException, InterruptedException { |
||||||
|
ProcessBuilder processBuilder = |
||||||
|
new ProcessBuilder("quickstart/removePantheonPrivateNetwork.sh"); |
||||||
|
processBuilder.inheritIO(); // redirect all output to logs
|
||||||
|
processBuilder.directory(new File(PROJECT_ROOT)); // going up one level is the project root
|
||||||
|
Process process = processBuilder.start(); |
||||||
|
|
||||||
|
int exitValue = process.waitFor(); |
||||||
|
if (exitValue != 0) { |
||||||
|
// check for errors, all output and then also error messages and causes are redirected to logs
|
||||||
|
throw new RuntimeException("execution of script failed!"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void servicesShouldBeUp() { |
||||||
|
Awaitility.await() |
||||||
|
.ignoreExceptions() |
||||||
|
.atMost(60, TimeUnit.SECONDS) |
||||||
|
.untilAsserted( |
||||||
|
() -> |
||||||
|
Arrays.stream(ServicesIdentifier.values()) |
||||||
|
.forEach( |
||||||
|
servicesIdentifier -> |
||||||
|
assertThat(services.get(servicesIdentifier).state).isEqualTo("up"))); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void servicesEndpointsShouldBeExposed() { |
||||||
|
Awaitility.await() |
||||||
|
.ignoreExceptions() |
||||||
|
.atMost(60, TimeUnit.SECONDS) |
||||||
|
.untilAsserted( |
||||||
|
() -> |
||||||
|
Arrays.stream(EndpointsIdentifier.values()) |
||||||
|
.forEach( |
||||||
|
endpointsIdentifier -> |
||||||
|
assertThat(endpoints.get(endpointsIdentifier)) |
||||||
|
.isNotNull() |
||||||
|
.isNotEmpty())); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void rpcNodeShouldFindPeers() { |
||||||
|
// Peers are those defined in docker-compose.yml and launched with scaling of 4 regular nodes
|
||||||
|
// which gives us 6 peers of the RPC node: bootnode, minernode and 4 regular nodes.
|
||||||
|
int expectecNumberOfPeers = 6; |
||||||
|
|
||||||
|
Awaitility.await() |
||||||
|
.ignoreExceptions() |
||||||
|
.atMost(60, TimeUnit.SECONDS) |
||||||
|
.untilAsserted( |
||||||
|
() -> |
||||||
|
assertThat(web3HttpClient.netPeerCount().send().getQuantity().intValueExact()) |
||||||
|
.isEqualTo(expectecNumberOfPeers)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void rpcNodeShouldReturnCorrectVersion() { |
||||||
|
String expectedVersion = PantheonInfo.version(); |
||||||
|
Awaitility.await() |
||||||
|
.ignoreExceptions() |
||||||
|
.atMost(60, TimeUnit.SECONDS) |
||||||
|
.untilAsserted( |
||||||
|
() -> |
||||||
|
assertThat(web3HttpClient.web3ClientVersion().send().getWeb3ClientVersion()) |
||||||
|
.isEqualTo(expectedVersion)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mustMineSomeBlocks() { |
||||||
|
// A bug occurred that failed mining after 2 blocks, so testing at least 10.
|
||||||
|
int expectedAtLeastBlockNumber = 10; |
||||||
|
Awaitility.await() |
||||||
|
.ignoreExceptions() |
||||||
|
.atMost(5, TimeUnit.MINUTES) |
||||||
|
.untilAsserted( |
||||||
|
() -> |
||||||
|
assertThat(web3HttpClient.ethBlockNumber().send().getBlockNumber().intValueExact()) |
||||||
|
.isGreaterThan(expectedAtLeastBlockNumber)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void webSocketRpcServiceMustConnect() { |
||||||
|
RequestOptions options = new RequestOptions(); |
||||||
|
options.setPort( |
||||||
|
services |
||||||
|
.get(ServicesIdentifier.RPCNODE) |
||||||
|
.exposedPorts |
||||||
|
.get(DEFAULT_WS_RPC_PORT) |
||||||
|
.externalPort); |
||||||
|
options.setHost(DEFAULT_RPC_HOST); |
||||||
|
|
||||||
|
final WebSocket[] wsConnection = new WebSocket[1]; |
||||||
|
final Vertx vertx = Vertx.vertx(); |
||||||
|
try { |
||||||
|
vertx |
||||||
|
.createHttpClient(new HttpClientOptions()) |
||||||
|
.websocket(options, websocket -> wsConnection[0] = websocket); |
||||||
|
|
||||||
|
Awaitility.await() |
||||||
|
.ignoreExceptions() |
||||||
|
.atMost(30, TimeUnit.SECONDS) |
||||||
|
.untilAsserted(() -> assertThat(wsConnection[0]).isNotNull()); |
||||||
|
} finally { |
||||||
|
assertThat(wsConnection[0]).isNotNull(); |
||||||
|
wsConnection[0].close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void explorerShouldHaveModifiedHttpRpcEndpoint() throws IOException { |
||||||
|
// we have to check that the sed command well replaced the endpoint placeholder with the
|
||||||
|
// real dynamic endpoint. But as this is a react app, we have to search for the value in the JS
|
||||||
|
// as we can't find it in the HTML page because source code is rendered dynamically.
|
||||||
|
Request request = |
||||||
|
new Request.Builder() |
||||||
|
.get() |
||||||
|
.url(endpoints.get(EndpointsIdentifier.EXPLORER) + "/static/js/bundle.js") |
||||||
|
.build(); |
||||||
|
|
||||||
|
OkHttpClient httpClient = new OkHttpClient(); |
||||||
|
try (Response resp = httpClient.newCall(request).execute()) { |
||||||
|
assertThat(resp.code()).isEqualTo(200); |
||||||
|
assertThat(resp.body()).isNotNull(); |
||||||
|
assertThat(resp.body().string()) |
||||||
|
.containsOnlyOnce( |
||||||
|
"var fallbackUrl = '" + endpoints.get(EndpointsIdentifier.HTTP_RPC) + "';"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private enum ServicesIdentifier { |
||||||
|
BOOTNODE, |
||||||
|
EXPLORER, |
||||||
|
MINERNODE, |
||||||
|
NODE, |
||||||
|
RPCNODE; |
||||||
|
|
||||||
|
final Pattern pattern; |
||||||
|
|
||||||
|
ServicesIdentifier() { |
||||||
|
pattern = |
||||||
|
Pattern.compile( |
||||||
|
"(^" |
||||||
|
+ DOCKER_COMPOSE_PROJECT_PREFIX |
||||||
|
+ this.name().toLowerCase() |
||||||
|
+ "_[0-9]+)\\s+.+\\s{3,}(\\w+)\\s+(.+)", |
||||||
|
Pattern.DOTALL); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private enum EndpointsIdentifier { |
||||||
|
EXPLORER("Web block explorer address"), |
||||||
|
HTTP_RPC("JSON-RPC.+HTTP.+service endpoint"), |
||||||
|
WS_RPC("JSON-RPC.+WebSocket.+service endpoint"); |
||||||
|
|
||||||
|
final Pattern pattern; |
||||||
|
|
||||||
|
EndpointsIdentifier(final String lineLabel) { |
||||||
|
pattern = Pattern.compile(lineLabel + ".+(http://.+:[0-9]+)", Pattern.DOTALL); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static class Service { |
||||||
|
|
||||||
|
String name; |
||||||
|
String state; |
||||||
|
Map<Integer, ExposedPort> exposedPorts = new HashMap<>(); |
||||||
|
|
||||||
|
static class ExposedPort { |
||||||
|
|
||||||
|
final Integer internalPort; |
||||||
|
final Integer externalPort; |
||||||
|
private final Pattern pattern = Pattern.compile("[0-9]+", Pattern.DOTALL); |
||||||
|
|
||||||
|
ExposedPort(final String portDescription) { |
||||||
|
if (portDescription.contains("->")) { |
||||||
|
String[] ports = portDescription.split("->", 2); |
||||||
|
|
||||||
|
String[] internalPortInfos = ports[1].split("/", 2); |
||||||
|
internalPort = Integer.valueOf(internalPortInfos[0]); |
||||||
|
String[] externalPortInfos = ports[0].split(":", 2); |
||||||
|
|
||||||
|
externalPort = Integer.valueOf(externalPortInfos[1]); |
||||||
|
} else { |
||||||
|
Matcher regexMatcher = pattern.matcher(portDescription); |
||||||
|
internalPort = regexMatcher.find() ? Integer.valueOf(regexMatcher.group(0)) : null; |
||||||
|
externalPort = null; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<Configuration status="INFO" monitorInterval="30"> |
||||||
|
<Properties> |
||||||
|
<Property name="root.log.level">INFO</Property> |
||||||
|
</Properties> |
||||||
|
<Appenders> |
||||||
|
<Console name="Console" target="SYSTEM_OUT"> |
||||||
|
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSSZZZ} | %t | %-5level | %c{1} | %msg%n" /> |
||||||
|
</Console> |
||||||
|
</Appenders> |
||||||
|
<Loggers> |
||||||
|
<Root level="${sys:root.log.level}"> |
||||||
|
<AppenderRef ref="Console" /> |
||||||
|
</Root> |
||||||
|
</Loggers> |
||||||
|
</Configuration> |
Loading…
Reference in new issue