Use Awaitility in our own tests

Closes gh-18227
This commit is contained in:
Andy Wilkinson
2019-09-12 15:50:53 +01:00
parent 568caa1206
commit 1b237de5f5
28 changed files with 403 additions and 348 deletions

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.devtools.tests;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.testsupport.BuildOutput;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Base class for DevTools integration tests.
*
* @author Andy Wilkinson
*/
abstract class AbstractDevToolsIntegrationTests {
protected static final BuildOutput buildOutput = new BuildOutput(AbstractDevToolsIntegrationTests.class);
protected final File serverPortFile = new File(buildOutput.getRootLocation(), "server.port");
@RegisterExtension
protected final JvmLauncher javaLauncher = new JvmLauncher();
@TempDir
protected static File temp;
protected LaunchedApplication launchedApplication;
protected void launchApplication(ApplicationLauncher applicationLauncher, String... args) throws Exception {
this.serverPortFile.delete();
this.launchedApplication = applicationLauncher.launchApplication(this.javaLauncher, this.serverPortFile, args);
}
@AfterEach
void stopApplication() throws InterruptedException {
this.launchedApplication.stop();
}
protected int awaitServerPort() throws Exception {
int port = Awaitility.waitAtMost(Duration.ofSeconds(30))
.until(() -> new ApplicationState(this.serverPortFile, this.launchedApplication),
ApplicationState::hasServerPort)
.getServerPort();
this.serverPortFile.delete();
System.out.println("Got port " + port);
this.launchedApplication.restartRemote(port);
Thread.sleep(1000);
return port;
}
protected ControllerBuilder controller(String name) {
return new ControllerBuilder(name, this.launchedApplication.getClassesDirectory());
}
protected static final class ControllerBuilder {
private final List<String> mappings = new ArrayList<>();
private final String name;
private final File classesDirectory;
protected ControllerBuilder(String name, File classesDirectory) {
this.name = name;
this.classesDirectory = classesDirectory;
}
protected ControllerBuilder withRequestMapping(String mapping) {
this.mappings.add(mapping);
return this;
}
protected void build() throws Exception {
DynamicType.Builder<Object> builder = new ByteBuddy().subclass(Object.class).name(this.name)
.annotateType(AnnotationDescription.Builder.ofType(RestController.class).build());
for (String mapping : this.mappings) {
builder = builder.defineMethod(mapping, String.class, Visibility.PUBLIC)
.intercept(FixedValue.value(mapping)).annotateMethod(AnnotationDescription.Builder
.ofType(RequestMapping.class).defineArray("value", mapping).build());
}
builder.make().saveIn(this.classesDirectory);
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.devtools.tests;
import java.io.File;
import org.springframework.boot.devtools.tests.JvmLauncher.LaunchedJvm;
/**
* State of an application.
*
* @author Andy Wilkinson
*/
final class ApplicationState {
private final Integer serverPort;
private final FileContents out;
private final FileContents err;
ApplicationState(File serverPortFile, LaunchedJvm jvm) {
this(serverPortFile, jvm.getStandardOut(), jvm.getStandardError());
}
ApplicationState(File serverPortFile, LaunchedApplication application) {
this(serverPortFile, application.getStandardOut(), application.getStandardError());
}
private ApplicationState(File serverPortFile, File out, File err) {
this.serverPort = new FileContents(serverPortFile).get(Integer::parseInt);
this.out = new FileContents(out);
this.err = new FileContents(err);
}
boolean hasServerPort() {
return this.serverPort != null;
}
int getServerPort() {
return this.serverPort;
}
@Override
public String toString() {
return String.format("Application output:%n%s%n%s", this.out, this.err);
}
}

View File

@@ -17,29 +17,13 @@
package org.springframework.boot.devtools.tests;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.implementation.FixedValue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.testsupport.BuildOutput;
import org.springframework.http.HttpStatus;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
@@ -48,29 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Andy Wilkinson
*/
public class DevToolsIntegrationTests {
@TempDir
static File temp;
private static final BuildOutput buildOutput = new BuildOutput(DevToolsIntegrationTests.class);
private LaunchedApplication launchedApplication;
private final File serverPortFile = new File(buildOutput.getRootLocation(), "server.port");
@RegisterExtension
final JvmLauncher javaLauncher = new JvmLauncher();
private void launchApplication(ApplicationLauncher applicationLauncher) throws Exception {
this.serverPortFile.delete();
this.launchedApplication = applicationLauncher.launchApplication(this.javaLauncher, this.serverPortFile);
}
@AfterEach
void stopApplication() throws InterruptedException {
this.launchedApplication.stop();
}
public class DevToolsIntegrationTests extends AbstractDevToolsIntegrationTests {
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
@@ -191,33 +153,6 @@ public class DevToolsIntegrationTests {
.isEqualTo(HttpStatus.NOT_FOUND);
}
private int awaitServerPort() throws Exception {
Duration timeToWait = Duration.ofSeconds(40);
long end = System.currentTimeMillis() + timeToWait.toMillis();
System.out.println("Reading server port from '" + this.serverPortFile + "'");
while (this.serverPortFile.length() == 0) {
if (System.currentTimeMillis() > end) {
throw new IllegalStateException(String.format(
"server.port file '" + this.serverPortFile + "' was not written within " + timeToWait.toMillis()
+ "ms. Application output:%n%s%s",
FileCopyUtils.copyToString(new FileReader(this.launchedApplication.getStandardOut())),
FileCopyUtils.copyToString(new FileReader(this.launchedApplication.getStandardError()))));
}
Thread.sleep(100);
}
FileReader portReader = new FileReader(this.serverPortFile);
int port = Integer.valueOf(FileCopyUtils.copyToString(portReader));
this.serverPortFile.delete();
System.out.println("Got port " + port);
this.launchedApplication.restartRemote(port);
Thread.sleep(1000);
return port;
}
private ControllerBuilder controller(String name) {
return new ControllerBuilder(name, this.launchedApplication.getClassesDirectory());
}
static Object[] parameters() throws IOException {
Directories directories = new Directories(buildOutput, temp);
return new Object[] { new Object[] { new LocalApplicationLauncher(directories) },
@@ -225,35 +160,4 @@ public class DevToolsIntegrationTests {
new Object[] { new JarFileRemoteApplicationLauncher(directories) } };
}
private static final class ControllerBuilder {
private final List<String> mappings = new ArrayList<>();
private final String name;
private final File classesDirectory;
private ControllerBuilder(String name, File classesDirectory) {
this.name = name;
this.classesDirectory = classesDirectory;
}
ControllerBuilder withRequestMapping(String mapping) {
this.mappings.add(mapping);
return this;
}
void build() throws Exception {
Builder<Object> builder = new ByteBuddy().subclass(Object.class).name(this.name)
.annotateType(AnnotationDescription.Builder.ofType(RestController.class).build());
for (String mapping : this.mappings) {
builder = builder.defineMethod(mapping, String.class, Visibility.PUBLIC)
.intercept(FixedValue.value(mapping)).annotateMethod(AnnotationDescription.Builder
.ofType(RequestMapping.class).defineArray("value", mapping).build());
}
builder.make().saveIn(this.classesDirectory);
}
}
}

View File

@@ -16,30 +16,13 @@
package org.springframework.boot.devtools.tests;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.testsupport.BuildOutput;
import org.springframework.http.HttpStatus;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
@@ -48,36 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Madhura Bhave
*/
public class DevToolsWithLazyInitializationIntegrationTests {
@TempDir
static File temp;
private static final BuildOutput buildOutput = new BuildOutput(DevToolsIntegrationTests.class);
private LaunchedApplication launchedApplication;
private final File serverPortFile = new File(buildOutput.getRootLocation(), "server.port");
@RegisterExtension
final JvmLauncher javaLauncher = new JvmLauncher();
private void launchApplication(ApplicationLauncher applicationLauncher) throws Exception {
this.serverPortFile.delete();
this.launchedApplication = applicationLauncher.launchApplication(this.javaLauncher, this.serverPortFile,
"--spring.main.lazy-initialization=true");
}
@AfterEach
void stopApplication() throws InterruptedException {
this.launchedApplication.stop();
}
public class DevToolsWithLazyInitializationIntegrationTests extends AbstractDevToolsIntegrationTests {
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
public void addARequestMappingToAnExistingControllerWhenLazyInit(ApplicationLauncher applicationLauncher)
throws Exception {
launchApplication(applicationLauncher);
launchApplication(applicationLauncher, "--spring.main.lazy-initialization=true");
TestRestTemplate template = new TestRestTemplate();
String urlBase = "http://localhost:" + awaitServerPort();
assertThat(template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
@@ -89,33 +49,6 @@ public class DevToolsWithLazyInitializationIntegrationTests {
assertThat(template.getForObject(urlBase + "/two", String.class)).isEqualTo("two");
}
private int awaitServerPort() throws Exception {
Duration timeToWait = Duration.ofSeconds(40);
long end = System.currentTimeMillis() + timeToWait.toMillis();
System.out.println("Reading server port from '" + this.serverPortFile + "'");
while (this.serverPortFile.length() == 0) {
if (System.currentTimeMillis() > end) {
throw new IllegalStateException(String.format(
"server.port file '" + this.serverPortFile + "' was not written within " + timeToWait.toMillis()
+ "ms. Application output:%n%s%s",
FileCopyUtils.copyToString(new FileReader(this.launchedApplication.getStandardOut())),
FileCopyUtils.copyToString(new FileReader(this.launchedApplication.getStandardError()))));
}
Thread.sleep(100);
}
FileReader portReader = new FileReader(this.serverPortFile);
int port = Integer.valueOf(FileCopyUtils.copyToString(portReader));
this.serverPortFile.delete();
System.out.println("Got port " + port);
this.launchedApplication.restartRemote(port);
Thread.sleep(1000);
return port;
}
private ControllerBuilder controller(String name) {
return new ControllerBuilder(name, this.launchedApplication.getClassesDirectory());
}
static Object[] parameters() throws IOException {
Directories directories = new Directories(buildOutput, temp);
return new Object[] { new Object[] { new LocalApplicationLauncher(directories) },
@@ -124,35 +57,4 @@ public class DevToolsWithLazyInitializationIntegrationTests {
}
private static final class ControllerBuilder {
private final List<String> mappings = new ArrayList<>();
private final String name;
private final File classesDirectory;
private ControllerBuilder(String name, File classesDirectory) {
this.name = name;
this.classesDirectory = classesDirectory;
}
ControllerBuilder withRequestMapping(String mapping) {
this.mappings.add(mapping);
return this;
}
void build() throws Exception {
DynamicType.Builder<Object> builder = new ByteBuddy().subclass(Object.class).name(this.name)
.annotateType(AnnotationDescription.Builder.ofType(RestController.class).build());
for (String mapping : this.mappings) {
builder = builder.defineMethod(mapping, String.class, Visibility.PUBLIC)
.intercept(FixedValue.value(mapping)).annotateMethod(AnnotationDescription.Builder
.ofType(RequestMapping.class).defineArray("value", mapping).build());
}
builder.make().saveIn(this.classesDirectory);
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.devtools.tests;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.function.Function;
import org.springframework.util.FileCopyUtils;
/**
* Provides access to the contents of a file.
*
* @author Andy Wilkinson
*/
class FileContents {
private final File file;
FileContents(File file) {
this.file = file;
}
String get() {
return get(Function.identity());
}
<T> T get(Function<String, T> transformer) {
if ((!this.file.exists()) || this.file.length() == 0) {
return null;
}
try {
return transformer.apply(FileCopyUtils.copyToString(new FileReader(this.file)));
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
@Override
public String toString() {
return get();
}
}

View File

@@ -17,17 +17,20 @@
package org.springframework.boot.devtools.tests;
import java.io.File;
import java.io.FileReader;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import org.awaitility.Awaitility;
import org.springframework.boot.devtools.RemoteSpringApplication;
import org.springframework.boot.devtools.tests.JvmLauncher.LaunchedJvm;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import static org.hamcrest.Matchers.containsString;
/**
* Base class for {@link ApplicationLauncher} implementations that use
* {@link RemoteSpringApplication}.
@@ -45,7 +48,7 @@ abstract class RemoteApplicationLauncher extends AbstractApplicationLauncher {
LaunchedJvm applicationJvm = javaLauncher.launch("app", createApplicationClassPath(),
"com.example.DevToolsTestApplication", serverPortFile.getAbsolutePath(), "--server.port=0",
"--spring.devtools.remote.secret=secret");
int port = awaitServerPort(applicationJvm.getStandardOut(), serverPortFile);
int port = awaitServerPort(applicationJvm, serverPortFile);
BiFunction<Integer, File, Process> remoteRestarter = getRemoteRestarter(javaLauncher);
return new LaunchedApplication(getDirectories().getRemoteAppDirectory(), applicationJvm.getStandardOut(),
applicationJvm.getStandardError(), applicationJvm.getProcess(), remoteRestarter.apply(port, null),
@@ -60,7 +63,7 @@ abstract class RemoteApplicationLauncher extends AbstractApplicationLauncher {
args.addAll(Arrays.asList(additionalArgs));
LaunchedJvm applicationJvm = javaLauncher.launch("app", createApplicationClassPath(),
args.toArray(new String[] {}));
int port = awaitServerPort(applicationJvm.getStandardOut(), serverPortFile);
int port = awaitServerPort(applicationJvm, serverPortFile);
BiFunction<Integer, File, Process> remoteRestarter = getRemoteRestarter(javaLauncher);
return new LaunchedApplication(getDirectories().getRemoteAppDirectory(), applicationJvm.getStandardOut(),
applicationJvm.getStandardError(), applicationJvm.getProcess(), remoteRestarter.apply(port, null),
@@ -96,35 +99,16 @@ abstract class RemoteApplicationLauncher extends AbstractApplicationLauncher {
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}
private int awaitServerPort(File standardOut, File serverPortFile) throws Exception {
long end = System.currentTimeMillis() + 30000;
while (serverPortFile.length() == 0) {
if (System.currentTimeMillis() > end) {
throw new IllegalStateException(
String.format("server.port file was not written within 30 seconds. Application output:%n%s",
FileCopyUtils.copyToString(new FileReader(standardOut))));
}
Thread.sleep(100);
}
FileReader portReader = new FileReader(serverPortFile);
int port = Integer.valueOf(FileCopyUtils.copyToString(portReader));
return port;
private int awaitServerPort(LaunchedJvm jvm, File serverPortFile) throws Exception {
return Awaitility.waitAtMost(Duration.ofSeconds(30))
.until(() -> new ApplicationState(serverPortFile, jvm), ApplicationState::hasServerPort)
.getServerPort();
}
private void awaitRemoteSpringApplication(File standardOut) throws Exception {
long end = System.currentTimeMillis() + 30000;
while (!standardOut.exists()) {
if (System.currentTimeMillis() > end) {
throw new IllegalStateException("Standard out file was not written within 30 seconds");
}
Thread.sleep(100);
}
while (!FileCopyUtils.copyToString(new FileReader(standardOut)).contains("Started RemoteSpringApplication")) {
if (System.currentTimeMillis() > end) {
throw new IllegalStateException("RemoteSpringApplication did not start within 30 seconds");
}
Thread.sleep(100);
}
FileContents contents = new FileContents(standardOut);
Awaitility.waitAtMost(Duration.ofSeconds(30)).until(contents::get,
containsString("Started RemoteSpringApplication"));
}
}