Start adding some tests for SpringBootApp discovery and actuator client

This commit is contained in:
Kris De Volder
2017-10-19 15:55:18 -07:00
parent 5f7e964301
commit 53ef047560
5 changed files with 313 additions and 1 deletions

View File

@@ -22,5 +22,10 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -32,6 +32,12 @@
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<dependency>
<artifactId>commons-util</artifactId>
<groupId>org.springframework.ide.vscode</groupId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -35,7 +35,6 @@ import com.sun.tools.attach.VirtualMachineDescriptor;
/**
* @author Martin Lippert
*/
@SuppressWarnings("restriction")
public class SpringBootApp {
private VirtualMachine vm;
@@ -321,4 +320,9 @@ public class SpringBootApp {
return null;
}
@Override
public String toString() {
return "SpringBootApp [" +vmd.id() + ", "+vmd.displayName()+"]";
}
}

View File

@@ -0,0 +1,69 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.boot.app.cli;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.net.URL;
import java.util.Map;
import java.util.Optional;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.ide.vscode.commons.util.AsyncProcess;
import org.springframework.ide.vscode.commons.util.ExternalCommand;
public class SpringBootAppTest {
private static final String appName = "actuator-client-test-subject";
private static AsyncProcess testAppRunner;
@BeforeClass
public static void setupClass() throws Exception {
testAppRunner = startTestApplication(SpringBootAppTest.class.getResource("/"+appName+"-0.0.1-SNAPSHOT.jar"));
//TODO: add some wait here until the boot app is 'ready'. Otherwise some of our test will just fail trying to read stuff that's not there yet.
}
private static AsyncProcess startTestApplication(URL jarUrl) throws Exception {
File jarFile = new File(jarUrl.toURI());
return new AsyncProcess(new File("."), new ExternalCommand(
"java",
"-Dserver.port=0", //let spring boot pick randomized free port
"-jar",
jarFile.getAbsolutePath()
));
}
@AfterClass
public static void tearDownClass() throws Exception {
testAppRunner.kill();
}
@Test
public void getAllJavaApps() throws Exception {
Map<String, SpringBootApp> allApps = SpringBootApp.getAllRunningJavaApps();
Optional<SpringBootApp> myProcess = allApps.values().stream().filter(app -> app.getProcessName().contains(appName)).findAny();
assertTrue(myProcess.isPresent());
}
@Ignore //Failing... not sure how to fix.
@Test public void getAllBootApps() throws Exception {
Map<String, SpringBootApp> allApps = SpringBootApp.getAllRunningSpringApps();
Optional<SpringBootApp> myProcess = allApps.values().stream().filter(app -> app.getProcessName().contains(appName)).findAny();
assertTrue(myProcess.isPresent());
}
}

View File

@@ -0,0 +1,228 @@
/*******************************************************************************
* Copyright (c) 2012, 2016 Pivotal Software, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Convenient wrapper around a {@link Process}. Simplifies the asynchronous execution of external
* commands by handling reading from out and error streams and either buffering the result output
* for later retrieval, or forwarding the to output to designated streams.
* <p>
* This provides similar apis than {@link ExternalProcess}. But has been adapted so that creation
* of the instance starts the process asynchronously. Additional apis are also provided to
* wait for the process and/or terminate it forcibly when no longer needed.
*
* @author Kris De Volder
*/
public class AsyncProcess {
//TODO: much of this code copied from ExternalProcess. It should be relatively easy
// to make 'ExternalProcess inherit from this one and just add some additional 'waitFor' call
// in its 'init' method.
/**
* A thread that keeps reading input from a Stream until the end is reached
* or there's some error reading the Stream.
*/
public static class StreamGobler extends Thread {
private final OutputStream echo;
private InputStream toRead; //Stream to read. This is nulled after all input has been consumed.
/**
* Creates a StreamGobler that reads input from an input stream
* and buffers up all input it has read for later retrieval via
* the getOut() method.
*/
public StreamGobler(InputStream toRead) {
this(toRead, new ByteArrayOutputStream());
}
/**
* Creates a StreamGobler that reads input from an input stream
* and writes it out to an outputstream.
*/
public StreamGobler(InputStream toRead, OutputStream forwardTo) {
this.toRead = toRead;
this.echo = forwardTo;
start();
}
@Override
public void run() {
byte[] buf = new byte[256];
while (toRead!=null) {
try {
int i = toRead.read(buf);
if (i==-1) {
//EOF
toRead = null; //Done!
} else {
append(buf, i);
}
} catch (IOException e) {
toRead = null;
ByteArrayOutputStream errMsg = new ByteArrayOutputStream();
e.printStackTrace(new PrintStream(errMsg));
append(errMsg.toByteArray());
}
}
}
private void append(byte[] buf) {
append(buf, buf.length);
}
private void append(byte[] buf, int len) {
if (echo!=null) {
try {
echo.write(buf, 0, len);
} catch (IOException e) {
}
}
}
public String getContents() throws InterruptedException {
try {
this.join();
if (echo instanceof ByteArrayOutputStream) {
return ((ByteArrayOutputStream)echo).toString();
} else {
return null;
}
} finally {
toRead = null;
}
}
}
private Process process;
private StreamGobler err; // Standard error is to be read from here
private StreamGobler out; // Standard out is to be read from here
private int exitValue = -9999;
private ExternalCommand cmd;
/**
* Creates an external process and waits for it to terminate. The output and error streams
* will be read and forwarded to System.out and System.err
*/
public AsyncProcess(File workingDir, ExternalCommand cmd) throws IOException {
this(workingDir, cmd, false);
}
private void init(File workingDir, ExternalCommand cmd,
OutputStream outStream, OutputStream errStream
) throws IOException {
this.cmd = cmd;
ProcessBuilder processBuilder = new ProcessBuilder(cmd.getProgramAndArgs());
processBuilder.directory(workingDir);
cmd.configure(processBuilder);
process = processBuilder.start();
err = new StreamGobler(process.getErrorStream(), errStream);
out = new StreamGobler(process.getInputStream(), outStream);
}
public AsyncProcess(File workingDir, ExternalCommand cmd, boolean captureStreams) throws IOException {
if (captureStreams) {
init(workingDir, cmd, new ByteArrayOutputStream(), new ByteArrayOutputStream());
} else {
init(workingDir, cmd, System.out, System.err);
}
}
public String getOut() throws InterruptedException {
return out.getContents();
}
public String getErr() throws InterruptedException {
return err.getContents();
}
/**
* Wait for the process to exit. If a timeout is specified and the process
* does not terminate within that time, the process is destroyed forcibly.
* <p>
* If timeout is not specified (i.e. null) then this waits for as long as necessary
* (which is potentially forever if the process does not terminate).
*/
public int waitFor(Duration timeout) throws InterruptedException, TimeoutException {
try {
if (timeout==null) {
exitValue = process.waitFor();
} else {
if (process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
exitValue = process.exitValue();
} else {
process.destroy();
exitValue = 999; //Set some non-0 value as that is what some callers might use to determine 'failure' occurred.
throw new TimeoutException("Command timed out: "+this);
}
}
return exitValue;
} finally {
try {
//Try destroying unresponsive processes no matter what.
process.destroyForcibly();
} catch (Exception e) {
//ignore
}
}
}
@Override
public String toString() {
StringBuffer result = new StringBuffer();
try {
process.exitValue();
result.append(">>>> ExternalProcess: ");
result.append(cmd+"\n");
result.append("exitValue = "+exitValue+"\n");
String strOut = getOut();
if (strOut!=null) {
result.append("------- System.out -------\n");
result.append(strOut);
}
String strErr = getErr();
if (strErr!=null) {
result.append("------- System.err -------\n");
result.append(strErr);
}
result.append("<<<< ExternalProcess");
return result.toString();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return result.toString();
} catch (IllegalThreadStateException e) {
return "ExternalProcess(RUNNING, "+cmd+")";
}
}
public int getExitValue() {
return exitValue;
}
public void kill() {
try {
waitFor(Duration.ofSeconds(0));
} catch (Exception e) {
}
}
}