Minimize and centralize assumptions about build output

Closes gh-15471
This commit is contained in:
Andy Wilkinson
2018-12-14 17:50:20 +00:00
parent 7d0a7a65c8
commit 61d04db0d7
57 changed files with 778 additions and 337 deletions

View File

@@ -25,7 +25,7 @@ public class DevToolsTestApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(DevToolsTestApplication.class)
.listeners(new WebServerPortFileWriter("target/server.port")).run(args);
.listeners(new WebServerPortFileWriter(args[0])).run(args);
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2018 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
*
* http://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.IOException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.util.FileSystemUtils;
/**
* Base class for all {@link ApplicationLauncher} implementations.
*
* @author Andy Wilkinson
*/
abstract class AbstractApplicationLauncher implements ApplicationLauncher {
private final Directories directories;
AbstractApplicationLauncher(Directories directories) {
this.directories = directories;
}
protected final void copyApplicationTo(File location) throws IOException {
FileSystemUtils.deleteRecursively(location);
location.mkdirs();
FileSystemUtils.copyRecursively(
new File(this.directories.getTestClassesDirectory(), "com"),
new File(location, "com"));
}
protected final List<String> getDependencyJarPaths() {
return Stream.of(this.directories.getDependenciesDirectory().listFiles())
.map(File::getAbsolutePath).collect(Collectors.toList());
}
protected final Directories getDirectories() {
return this.directories;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@@ -16,6 +16,8 @@
package org.springframework.boot.devtools.tests;
import java.io.File;
/**
* Launches an application with DevTools.
*
@@ -23,6 +25,7 @@ package org.springframework.boot.devtools.tests;
*/
public interface ApplicationLauncher {
LaunchedApplication launchApplication(JvmLauncher javaLauncher) throws Exception;
LaunchedApplication launchApplication(JvmLauncher javaLauncher, File serverPortFile)
throws Exception;
}

View File

@@ -18,6 +18,8 @@ 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;
@@ -28,13 +30,16 @@ import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.implementation.FixedValue;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
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;
@@ -50,9 +55,15 @@ import static org.assertj.core.api.Assertions.assertThat;
@RunWith(Parameterized.class)
public class DevToolsIntegrationTests {
@ClassRule
public static final TemporaryFolder temp = new TemporaryFolder();
private static final BuildOutput buildOutput = new BuildOutput(
DevToolsIntegrationTests.class);
private LaunchedApplication launchedApplication;
private final File serverPortFile = new File("target/server.port");
private final File serverPortFile;
private final ApplicationLauncher applicationLauncher;
@@ -61,14 +72,15 @@ public class DevToolsIntegrationTests {
public DevToolsIntegrationTests(ApplicationLauncher applicationLauncher) {
this.applicationLauncher = applicationLauncher;
this.serverPortFile = new File(
DevToolsIntegrationTests.buildOutput.getRootLocation(), "server.port");
}
@Before
public void launchApplication() throws Exception {
this.serverPortFile.delete();
System.out.println("Launching " + this.javaLauncher.getClass());
this.launchedApplication = this.applicationLauncher
.launchApplication(this.javaLauncher);
.launchApplication(this.javaLauncher, this.serverPortFile);
}
@After
@@ -205,13 +217,15 @@ public class DevToolsIntegrationTests {
}
private int awaitServerPort() throws Exception {
long end = System.currentTimeMillis() + 40000;
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) {
System.out.println("Getting server port " + this.serverPortFile.length());
if (System.currentTimeMillis() > end) {
throw new IllegalStateException(String.format(
"server.port file was not written within 30 seconds. "
+ "Application output:%n%s%s",
"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(
@@ -234,10 +248,11 @@ public class DevToolsIntegrationTests {
}
@Parameters(name = "{0}")
public static Object[] parameters() {
return new Object[] { new Object[] { new LocalApplicationLauncher() },
new Object[] { new ExplodedRemoteApplicationLauncher() },
new Object[] { new JarFileRemoteApplicationLauncher() } };
public static Object[] parameters() throws IOException {
Directories directories = new Directories(buildOutput, temp);
return new Object[] { new Object[] { new LocalApplicationLauncher(directories) },
new Object[] { new ExplodedRemoteApplicationLauncher(directories) },
new Object[] { new JarFileRemoteApplicationLauncher(directories) } };
}
private static final class ControllerBuilder {

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2018 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
*
* http://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.junit.rules.TemporaryFolder;
import org.springframework.boot.testsupport.BuildOutput;
/**
* Various directories used by the {@link ApplicationLauncher ApplicationLaunchers}.
*
* @author Andy Wilkinson
*/
class Directories {
private final BuildOutput buildOutput;
private final TemporaryFolder temp;
Directories(BuildOutput buildOutput, TemporaryFolder temp) {
this.buildOutput = buildOutput;
this.temp = temp;
}
File getTestClassesDirectory() {
return this.buildOutput.getTestClassesLocation();
}
File getRemoteAppDirectory() {
return new File(this.temp.getRoot(), "remote");
}
File getDependenciesDirectory() {
return new File(this.buildOutput.getRootLocation(), "dependencies");
}
File getAppDirectory() {
return new File(this.temp.getRoot(), "app");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@@ -20,7 +20,6 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
/**
@@ -31,18 +30,17 @@ import org.springframework.util.StringUtils;
*/
public class ExplodedRemoteApplicationLauncher extends RemoteApplicationLauncher {
public ExplodedRemoteApplicationLauncher(Directories directories) {
super(directories);
}
@Override
protected String createApplicationClassPath() throws Exception {
File appDirectory = new File("target/app");
FileSystemUtils.deleteRecursively(appDirectory);
appDirectory.mkdirs();
FileSystemUtils.copyRecursively(new File("target/test-classes/com"),
new File("target/app/com"));
File appDirectory = getDirectories().getAppDirectory();
copyApplicationTo(appDirectory);
List<String> entries = new ArrayList<>();
entries.add("target/app");
for (File jar : new File("target/dependencies").listFiles()) {
entries.add(jar.getAbsolutePath());
}
entries.add(appDirectory.getAbsolutePath());
entries.addAll(getDependencyJarPaths());
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@@ -27,7 +27,6 @@ import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
@@ -39,28 +38,24 @@ import org.springframework.util.StringUtils;
*/
public class JarFileRemoteApplicationLauncher extends RemoteApplicationLauncher {
public JarFileRemoteApplicationLauncher(Directories directories) {
super(directories);
}
@Override
protected String createApplicationClassPath() throws Exception {
File appDirectory = new File("target/app");
if (appDirectory.isDirectory()
&& !FileSystemUtils.deleteRecursively(appDirectory.toPath())) {
throw new IllegalStateException(
"Failed to delete '" + appDirectory.getAbsolutePath() + "'");
}
appDirectory.mkdirs();
File appDirectory = getDirectories().getAppDirectory();
copyApplicationTo(appDirectory);
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
JarOutputStream output = new JarOutputStream(
new FileOutputStream(new File(appDirectory, "app.jar")), manifest);
FileSystemUtils.copyRecursively(new File("target/test-classes/com"),
new File("target/app/com"));
addToJar(output, new File("target/app/"), new File("target/app/"));
File appJar = new File(appDirectory, "app.jar");
JarOutputStream output = new JarOutputStream(new FileOutputStream(appJar),
manifest);
addToJar(output, appDirectory, appDirectory);
output.close();
List<String> entries = new ArrayList<>();
entries.add("target/app/app.jar");
for (File jar : new File("target/dependencies").listFiles()) {
entries.add(jar.getAbsolutePath());
}
entries.add(appJar.getAbsolutePath());
entries.addAll(getDependencyJarPaths());
String classpath = StringUtils.collectionToDelimitedString(entries,
File.pathSeparator);
return classpath;

View File

@@ -27,6 +27,7 @@ import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.testsupport.BuildOutput;
import org.springframework.util.StringUtils;
/**
@@ -39,12 +40,15 @@ class JvmLauncher implements TestRule {
private static final Pattern NON_ALPHABET_PATTERN = Pattern.compile("[^A-Za-z]+");
private final BuildOutput buildOutput = new BuildOutput(getClass());
private File outputDirectory;
@Override
public Statement apply(Statement base, Description description) {
this.outputDirectory = new File("target/output/" + NON_ALPHABET_PATTERN
.matcher(description.getMethodName()).replaceAll(""));
this.outputDirectory = new File(this.buildOutput.getRootLocation(),
"output/" + NON_ALPHABET_PATTERN.matcher(description.getMethodName())
.replaceAll(""));
this.outputDirectory.mkdirs();
return base;
}

View File

@@ -21,7 +21,6 @@ import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.devtools.tests.JvmLauncher.LaunchedJvm;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
/**
@@ -29,28 +28,29 @@ import org.springframework.util.StringUtils;
*
* @author Andy Wilkinson
*/
public class LocalApplicationLauncher implements ApplicationLauncher {
public class LocalApplicationLauncher extends AbstractApplicationLauncher {
LocalApplicationLauncher(Directories directories) {
super(directories);
}
@Override
public LaunchedApplication launchApplication(JvmLauncher jvmLauncher)
throws Exception {
public LaunchedApplication launchApplication(JvmLauncher jvmLauncher,
File serverPortFile) throws Exception {
LaunchedJvm jvm = jvmLauncher.launch("local", createApplicationClassPath(),
"com.example.DevToolsTestApplication", "--server.port=0");
return new LaunchedApplication(new File("target/app"), jvm.getStandardOut(),
jvm.getStandardError(), jvm.getProcess(), null, null);
"com.example.DevToolsTestApplication", serverPortFile.getAbsolutePath(),
"--server.port=0");
return new LaunchedApplication(getDirectories().getAppDirectory(),
jvm.getStandardOut(), jvm.getStandardError(), jvm.getProcess(), null,
null);
}
protected String createApplicationClassPath() throws Exception {
File appDirectory = new File("target/app");
FileSystemUtils.deleteRecursively(appDirectory);
appDirectory.mkdirs();
FileSystemUtils.copyRecursively(new File("target/test-classes/com"),
new File("target/app/com"));
File appDirectory = getDirectories().getAppDirectory();
copyApplicationTo(appDirectory);
List<String> entries = new ArrayList<>();
entries.add("target/app");
for (File jar : new File("target/dependencies").listFiles()) {
entries.add(jar.getAbsolutePath());
}
entries.add(appDirectory.getAbsolutePath());
entries.addAll(getDependencyJarPaths());
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}

View File

@@ -25,7 +25,6 @@ import java.util.function.BiFunction;
import org.springframework.boot.devtools.RemoteSpringApplication;
import org.springframework.boot.devtools.tests.JvmLauncher.LaunchedJvm;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
/**
@@ -34,18 +33,23 @@ import org.springframework.util.StringUtils;
*
* @author Andy Wilkinson
*/
abstract class RemoteApplicationLauncher implements ApplicationLauncher {
abstract class RemoteApplicationLauncher extends AbstractApplicationLauncher {
RemoteApplicationLauncher(Directories directories) {
super(directories);
}
@Override
public LaunchedApplication launchApplication(JvmLauncher javaLauncher)
throws Exception {
public LaunchedApplication launchApplication(JvmLauncher javaLauncher,
File serverPortFile) throws Exception {
LaunchedJvm applicationJvm = javaLauncher.launch("app",
createApplicationClassPath(), "com.example.DevToolsTestApplication",
"--server.port=0", "--spring.devtools.remote.secret=secret");
int port = awaitServerPort(applicationJvm.getStandardOut());
serverPortFile.getAbsolutePath(), "--server.port=0",
"--spring.devtools.remote.secret=secret");
int port = awaitServerPort(applicationJvm.getStandardOut(), serverPortFile);
BiFunction<Integer, File, Process> remoteRestarter = getRemoteRestarter(
javaLauncher);
return new LaunchedApplication(new File("target/remote"),
return new LaunchedApplication(getDirectories().getRemoteAppDirectory(),
applicationJvm.getStandardOut(), applicationJvm.getStandardError(),
applicationJvm.getProcess(), remoteRestarter.apply(port, null),
remoteRestarter);
@@ -74,24 +78,18 @@ abstract class RemoteApplicationLauncher implements ApplicationLauncher {
private String createRemoteSpringApplicationClassPath(File classesDirectory)
throws Exception {
File remoteAppDirectory = getDirectories().getRemoteAppDirectory();
if (classesDirectory == null) {
File remoteDirectory = new File("target/remote");
FileSystemUtils.deleteRecursively(remoteDirectory);
remoteDirectory.mkdirs();
FileSystemUtils.copyRecursively(new File("target/test-classes/com"),
new File("target/remote/com"));
copyApplicationTo(remoteAppDirectory);
}
List<String> entries = new ArrayList<>();
entries.add("target/remote");
for (File jar : new File("target/dependencies").listFiles()) {
entries.add(jar.getAbsolutePath());
}
entries.add(remoteAppDirectory.getAbsolutePath());
entries.addAll(getDependencyJarPaths());
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}
private int awaitServerPort(File standardOut) throws Exception {
private int awaitServerPort(File standardOut, File serverPortFile) throws Exception {
long end = System.currentTimeMillis() + 30000;
File serverPortFile = new File("target/server.port");
while (serverPortFile.length() == 0) {
if (System.currentTimeMillis() > end) {
throw new IllegalStateException(String.format(