Skip to content

Commit

Permalink
Add QuarkusMainTest and QuarkusMainIntegrationTest
Browse files Browse the repository at this point in the history
These allow you to test running a command mode application to
completion, and verify the outcome.

Also coverts the Picocli tests to use this, rather than testing using a
rest endpoint.
  • Loading branch information
stuartwdouglas committed Sep 7, 2021
1 parent 5cc29c8 commit c3baf65
Show file tree
Hide file tree
Showing 52 changed files with 1,460 additions and 500 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ int countLines(String s, int cursorPos) {
return lines;
}

public void write(String s) {
public void write(boolean errorStream, String s) {
if (IN_WRITE.get()) {
return;
}
Expand Down Expand Up @@ -319,10 +319,8 @@ public void write(String s) {

StringBuilder buffer = new StringBuilder();
synchronized (this) {
if (outputFilter != null) {
if (!outputFilter.test(s)) {
return;
}
if (!shouldWrite(errorStream, s)) {
return;
}
if (totalStatusLines == 0) {
bottomBlankSpace = 0; //just to be safe, will only happen if status is added then removed, which is not really likely
Expand Down Expand Up @@ -384,8 +382,8 @@ public void write(String s) {
deadlockSafeWrite();
}

public void write(byte[] buf, int off, int len) {
write(new String(buf, off, len, connection.outputEncoding()));
public void write(boolean errorStream, byte[] buf, int off, int len) {
write(errorStream, new String(buf, off, len, connection.outputEncoding()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.deployment.console;

import java.io.IOException;
import java.io.PrintStream;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.Consumer;
import java.util.function.Supplier;
Expand All @@ -17,6 +18,24 @@

public class ConsoleHelper {

static boolean redirectsInstalled = false;

final static PrintStream out = System.out;
final static PrintStream err = System.err;

public synchronized static void installRedirects() {
if (redirectsInstalled) {
return;
}
redirectsInstalled = true;

//force console init
//otherwise you can get a stack overflow as it sees the redirected output
QuarkusConsole.INSTANCE.isInputSupported();
System.setOut(new RedirectPrintStream(false));
System.setErr(new RedirectPrintStream(true));
}

public static synchronized void installConsole(TestConfig config, ConsoleConfig consoleConfig,
ConsoleRuntimeConfig consoleRuntimeConfig, io.quarkus.runtime.logging.ConsoleConfig logConfig, boolean test) {
if (QuarkusConsole.installed) {
Expand All @@ -31,7 +50,7 @@ public static synchronized void installConsole(TestConfig config, ConsoleConfig
if (!inputSupport) {
//note that in this case we don't hold onto anything from this class loader
//which is important for the test suite
QuarkusConsole.INSTANCE = new BasicConsole(colorEnabled, false, System.out, System.console());
QuarkusConsole.INSTANCE = new BasicConsole(colorEnabled, false, out, System.console());
return;
}
try {
Expand Down Expand Up @@ -90,11 +109,8 @@ public Integer get() {
}
});
} catch (IOException e) {
QuarkusConsole.INSTANCE = new BasicConsole(colorEnabled, false, System.out, System.console());
QuarkusConsole.INSTANCE = new BasicConsole(colorEnabled, false, out, System.console());
}

RedirectPrintStream ps = new RedirectPrintStream();
System.setOut(ps);
System.setErr(ps);
installRedirects();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package io.quarkus.deployment.dev.testing;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand All @@ -26,7 +24,6 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -86,6 +83,7 @@ public class JunitTestRunner {

private static final Logger log = Logger.getLogger(JunitTestRunner.class);
public static final DotName QUARKUS_TEST = DotName.createSimple("io.quarkus.test.junit.QuarkusTest");
public static final DotName QUARKUS_MAIN_TEST = DotName.createSimple("io.quarkus.test.junit.main.QuarkusMainTest");
public static final DotName QUARKUS_INTEGRATION_TEST = DotName.createSimple("io.quarkus.test.junit.QuarkusIntegrationTest");
public static final DotName NATIVE_IMAGE_TEST = DotName.createSimple("io.quarkus.test.junit.NativeImageTest");
public static final DotName TEST_PROFILE = DotName.createSimple("io.quarkus.test.junit.TestProfile");
Expand Down Expand Up @@ -134,6 +132,8 @@ public JunitTestRunner(Builder builder) {
public void runTests() {
long start = System.currentTimeMillis();
ClassLoader old = Thread.currentThread().getContextClassLoader();
LogCapturingOutputFilter logHandler = new LogCapturingOutputFilter(testApplication, true, true,
TestSupport.instance().get()::isDisplayTestOutput);
try (QuarkusClassLoader tcl = testApplication.createDeploymentClassLoader()) {
synchronized (this) {
if (aborted) {
Expand Down Expand Up @@ -198,8 +198,7 @@ public FilterResult apply(TestDescriptor testDescriptor) {
listener.runStarted(toRun);
}
log.debug("Starting test run with " + testPlan.countTestIdentifiers((s) -> true) + " tests");
TestLogCapturingHandler logHandler = new TestLogCapturingHandler();
QuarkusConsole.INSTANCE.setOutputFilter(logHandler);
QuarkusConsole.INSTANCE.addOutputFilter(logHandler);

final Deque<Set<String>> touchedClasses = new LinkedBlockingDeque<>();
Map<TestIdentifier, Long> startTimes = new HashMap<>();
Expand Down Expand Up @@ -379,7 +378,7 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e
testState.classesRemoved(classScanResult.getDeletedClassNames());
}

QuarkusConsole.INSTANCE.setOutputFilter(null);
QuarkusConsole.INSTANCE.removeOutputFilter(logHandler);

//this has to happen before notifying the listeners
Map<String, Boolean> watched = TestWatchedFiles.retrieveWatchedFilePaths();
Expand All @@ -398,7 +397,7 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e
} finally {
try {
TracingHandler.setTracingHandler(null);
QuarkusConsole.INSTANCE.setOutputFilter(null);
QuarkusConsole.INSTANCE.removeOutputFilter(logHandler);
Thread.currentThread().setContextClassLoader(old);
} finally {
synchronized (this) {
Expand Down Expand Up @@ -547,12 +546,14 @@ private DiscoveryResult discoverTestClasses(DevModeContext devModeContext) {
}
}
Set<String> quarkusTestClasses = new HashSet<>();
for (AnnotationInstance i : index.getAnnotations(QUARKUS_TEST)) {
DotName name = i.target().asClass().name();
quarkusTestClasses.add(name.toString());
for (ClassInfo clazz : index.getAllKnownSubclasses(name)) {
if (!integrationTestClasses.contains(clazz.name().toString())) {
quarkusTestClasses.add(clazz.name().toString());
for (var a : Arrays.asList(QUARKUS_TEST, QUARKUS_MAIN_TEST)) {
for (AnnotationInstance i : index.getAnnotations(a)) {
DotName name = i.target().asClass().name();
quarkusTestClasses.add(name.toString());
for (ClassInfo clazz : index.getAllKnownSubclasses(name)) {
if (!integrationTestClasses.contains(clazz.name().toString())) {
quarkusTestClasses.add(clazz.name().toString());
}
}
}
}
Expand Down Expand Up @@ -681,51 +682,6 @@ public boolean isRunning() {
return testsRunning;
}

private class TestLogCapturingHandler implements Predicate<String> {

private final List<String> logOutput;

public TestLogCapturingHandler() {
this.logOutput = new ArrayList<>();
}

public List<String> captureOutput() {
List<String> ret = new ArrayList<>(logOutput);
logOutput.clear();
return ret;
}

@Override
public boolean test(String logRecord) {
Thread thread = Thread.currentThread();
ClassLoader cl = thread.getContextClassLoader();
if (cl == null) {
return true;
}
while (cl.getParent() != null) {
if (cl == testApplication.getAugmentClassLoader()
|| cl == testApplication.getBaseRuntimeClassLoader()) {
//TODO: for convenience we save the log records as HTML rather than ANSI here
synchronized (logOutput) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
HtmlAnsiOutputStream outputStream = new HtmlAnsiOutputStream(out) {
};
try {
outputStream.write(logRecord.getBytes(StandardCharsets.UTF_8));
logOutput.add(new String(out.toByteArray(), StandardCharsets.UTF_8));
} catch (IOException e) {
log.error("Failed to capture log record", e);
logOutput.add(logRecord);
}
}
return TestSupport.instance().get().isDisplayTestOutput();
}
cl = cl.getParent();
}
return true;
}
}

static class Builder {
private TestType testType = TestType.ALL;
private TestState testState;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.quarkus.deployment.dev.testing;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiPredicate;
import java.util.function.Supplier;

import org.jboss.logging.Logger;

import io.quarkus.bootstrap.app.CuratedApplication;

public class LogCapturingOutputFilter implements BiPredicate<String, Boolean> {
private static final Logger log = Logger.getLogger(LogCapturingOutputFilter.class);

private final CuratedApplication application;
private final List<String> logOutput = new ArrayList<>();
private final List<String> errorOutput = new ArrayList<>();
private final boolean mergeErrorStream;
private final boolean convertToHtml;
private final Supplier<Boolean> finalPredicate;

public LogCapturingOutputFilter(CuratedApplication application, boolean mergeErrorStream, boolean convertToHtml,
Supplier<Boolean> finalPredicate) {
this.application = application;
this.mergeErrorStream = mergeErrorStream;
this.convertToHtml = convertToHtml;
this.finalPredicate = finalPredicate;
}

public List<String> captureOutput() {
List<String> ret = new ArrayList<>(logOutput);
logOutput.clear();
return ret;
}

public List<String> captureErrorOutput() {
List<String> ret = new ArrayList<>(errorOutput);
errorOutput.clear();
return ret;
}

@Override
public boolean test(String logRecord, Boolean errorStream) {
Thread thread = Thread.currentThread();
ClassLoader cl = thread.getContextClassLoader();
if (cl == null) {
return true;
}
while (cl.getParent() != null) {
if (cl == application.getAugmentClassLoader()
|| cl == application.getBaseRuntimeClassLoader()) {
//TODO: for convenience we save the log records as HTML rather than ANSI here
synchronized (logOutput) {
if (convertToHtml) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
HtmlAnsiOutputStream outputStream = new HtmlAnsiOutputStream(out) {
};
try {
outputStream.write(logRecord.getBytes(StandardCharsets.UTF_8));
if (mergeErrorStream || !errorStream) {
logOutput.add(out.toString(StandardCharsets.UTF_8));
} else {
errorOutput.add(out.toString(StandardCharsets.UTF_8));
}
} catch (IOException e) {
log.error("Failed to capture log record", e);
logOutput.add(logRecord);
}
} else {
if (mergeErrorStream || !errorStream) {
logOutput.add(logRecord);
} else {
errorOutput.add(logRecord);
}
}
}
return finalPredicate.get();
}
cl = cl.getParent();
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

import org.jboss.logging.Logger;
Expand All @@ -33,6 +34,7 @@
import io.quarkus.deployment.builditem.TransformedClassesBuildItem;
import io.quarkus.deployment.configuration.RunTimeConfigurationGenerator;
import io.quarkus.dev.appstate.ApplicationStateNotification;
import io.quarkus.runtime.ApplicationLifecycleManager;
import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.configuration.RuntimeOverrideConfigSource;

Expand Down Expand Up @@ -141,7 +143,37 @@ public void close() throws IOException {
} finally {
Thread.currentThread().setContextClassLoader(old);
}
}

@Override
public int runMainClassBlocking(String... args) throws Exception {
//we have our class loaders
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(runtimeClassLoader);
final String className = buildResult.consume(MainClassBuildItem.class).getClassName();
try {
CompletableFuture<Integer> result = new CompletableFuture<>();
Class<?> lifecycleManager = Class.forName(ApplicationLifecycleManager.class.getName(), true, runtimeClassLoader);
lifecycleManager.getDeclaredMethod("setDefaultExitCodeHandler", Consumer.class).invoke(null,
new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
result.complete(integer);
}
});
// force init here
Class<?> appClass = Class.forName(className, true, runtimeClassLoader);
Method start = appClass.getMethod("main", String[].class);
start.invoke(null, (Object) (args == null ? new String[0] : args));
Integer returnVal = result.get();
Class<?> q = Class.forName(Quarkus.class.getName(), true, runtimeClassLoader);
q.getMethod("blockingExit").invoke(null);

return returnVal;
} finally {
runtimeClassLoader.close();
Thread.currentThread().setContextClassLoader(old);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,11 @@ public void setPromptMessage(String prompt) {
}

@Override
public void write(String s) {
if (outputFilter != null) {
if (!outputFilter.test(s)) {
//we still test, the output filter may be recording output
if (!DISABLE_FILTER.get()) {
return;
}
public void write(boolean errorStream, String s) {
if (!shouldWrite(errorStream, s)) {
//we still test, the output filter may be recording output
if (!DISABLE_FILTER.get()) {
return;
}
}
if (!color) {
Expand All @@ -171,8 +169,8 @@ public void write(String s) {
}

@Override
public void write(byte[] buf, int off, int len) {
write(new String(buf, off, len, StandardCharsets.UTF_8));
public void write(boolean errorStream, byte[] buf, int off, int len) {
write(errorStream, new String(buf, off, len, StandardCharsets.UTF_8));
}

@Override
Expand Down
Loading

0 comments on commit c3baf65

Please sign in to comment.