JUnit 5 extension for testing mvnd

This commit is contained in:
Peter Palaga
2020-05-28 10:12:25 +02:00
parent 7cdb2bafb8
commit 97ebe989e1
22 changed files with 1048 additions and 174 deletions

View File

@@ -18,7 +18,6 @@
<groupId>org.apache.maven</groupId>
<artifactId>maven-embedder</artifactId>
<version>${mavenVersion}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.takari.aether</groupId>
@@ -135,7 +134,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${mavenSurefirePluginVersion}</version>
<executions>
<execution>
<id>integration-test</id>

View File

@@ -15,23 +15,22 @@
*/
package org.jboss.fuse.mvnd.daemon;
import java.io.BufferedWriter;
import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import java.util.function.Supplier;
import org.apache.commons.cli.UnrecognizedOptionException;
import org.apache.maven.cli.CLIReportingUtils;
import org.jboss.fuse.mvnd.daemon.ClientOutput.TerminalOutput;
import org.jboss.fuse.mvnd.daemon.Message.BuildEvent;
import org.jboss.fuse.mvnd.daemon.Message.BuildException;
import org.jboss.fuse.mvnd.daemon.Message.BuildMessage;
@@ -39,26 +38,46 @@ import org.jboss.fuse.mvnd.daemon.Message.MessageSerializer;
import org.jboss.fuse.mvnd.jpm.Process;
import org.jboss.fuse.mvnd.jpm.ProcessImpl;
import org.jboss.fuse.mvnd.jpm.ScriptUtils;
import org.jline.terminal.Size;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.jline.utils.Display;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
public class Client {
private static final Logger LOGGER = LoggerFactory.getLogger(Client.class);
public static final String DAEMON_DEBUG = "daemon.debug";
private final Layout layout;
public static void main(String[] argv) throws Exception {
LOGGER.debug("Starting client");
List<String> args = new ArrayList<>(Arrays.asList(argv));
final List<String> args = new ArrayList<>(Arrays.asList(argv));
Path logFile = null;
for (int i = 0; i < args.size() - 2; i++) {
String arg = args.get(i);
if ("-l".equals(arg) || "--log-file".equals(arg)) {
logFile = Paths.get(args.get(i + 1));
args.remove(i);
args.remove(i);
break;
}
}
try (TerminalOutput output = new TerminalOutput(logFile)) {
new Client(Layout.getEnvInstance()).execute(output, args);
}
}
public Client(Layout layout) {
this.layout = layout;
}
public <O extends ClientOutput> ClientResult<O> execute(O output, String... argv) throws IOException {
return execute(output, Arrays.asList(argv));
}
public <O extends ClientOutput> ClientResult<O> execute(O output, List<String> argv) throws IOException {
output.debug("Starting client");
final List<String> args = new ArrayList<>(argv);
// Print version if needed
boolean version = args.remove("-v") || args.remove("-version") || args.remove("--version");
@@ -72,148 +91,81 @@ public class Client {
String v = buffer().strong( "Maven Daemon " + props.getProperty("version") ).toString()
+ System.getProperty( "line.separator" )
+ CLIReportingUtils.showVersion();
System.out.println(v);
output.log(v);
if (version) {
return;
return new ClientResult<O>(argv, true, output);
}
}
Path javaHome = Layout.javaHome();
DaemonRegistry registry = DaemonRegistry.getDefault();
boolean status = args.remove("--status");
if (status) {
System.out.println(String.format(" %36s %5s %5s %7s %s",
"UUID", "PID", "Port", "Status", "Timestamp"));
registry.getAll().forEach(d ->
System.out.println(String.format(" %36s %5s %5s %7s %s",
d.getUid(), d.getPid(), d.getAddress(), d.getState(),
new Date(Math.max(d.getLastIdle(), d.getLastBusy())).toString())));
return;
}
boolean stop = args.remove("--stop");
if (stop) {
DaemonInfo[] dis = registry.getAll().toArray(new DaemonInfo[0]);
if (dis.length > 0) {
System.out.println("Stopping " + dis.length + " running daemons");
for (DaemonInfo di : dis) {
try {
new ProcessImpl(di.getPid()).destroy();
} catch (IOException t) {
System.out.println("Daemon " + di.getUid() + ": " + t.getMessage());
} catch (Exception t) {
System.out.println("Daemon " + di.getUid() + ": " + t);
} finally {
registry.remove(di.getUid());
final Path javaHome = layout.javaHome();
try (DaemonRegistry registry = new DaemonRegistry(layout.registry())) {
boolean status = args.remove("--status");
if (status) {
output.log(String.format(" %36s %5s %5s %7s %s",
"UUID", "PID", "Port", "Status", "Timestamp"));
registry.getAll().forEach(d -> output.log(String.format(" %36s %5s %5s %7s %s",
d.getUid(), d.getPid(), d.getAddress(), d.getState(),
new Date(Math.max(d.getLastIdle(), d.getLastBusy())).toString())));
return new ClientResult<O>(argv, true, output);
}
boolean stop = args.remove("--stop");
if (stop) {
DaemonInfo[] dis = registry.getAll().toArray(new DaemonInfo[0]);
if (dis.length > 0) {
output.log("Stopping " + dis.length + " running daemons");
for (DaemonInfo di : dis) {
try {
new ProcessImpl(di.getPid()).destroy();
} catch (IOException t) {
System.out.println("Daemon " + di.getUid() + ": " + t.getMessage());
} catch (Exception t) {
System.out.println("Daemon " + di.getUid() + ": " + t);
} finally {
registry.remove(di.getUid());
}
}
}
return new ClientResult<O>(argv, true, output);
}
return;
}
String logFile = null;
for (int i = 0; i < args.size() - 2; i++) {
String arg = args.get(i);
if ("-l".equals(arg) || "--log-file".equals(arg)) {
logFile = args.get(i + 1);
args.remove(i);
args.remove(i);
break;
}
}
setDefaultArgs(args);
setDefaultArgs(args);
DaemonConnector connector = new DaemonConnector(layout, registry, this::startDaemon, new MessageSerializer());
List<String> opts = new ArrayList<>();
DaemonClientConnection daemon = connector.connect(new DaemonCompatibilitySpec(javaHome.toString(), opts));
DaemonConnector connector = new DaemonConnector(registry, Client::startDaemon, new MessageSerializer());
List<String> opts = new ArrayList<>();
DaemonClientConnection daemon = connector.connect(new DaemonCompatibilitySpec(javaHome.toString(), opts));
daemon.dispatch(new Message.BuildRequest(
args,
layout.userDir().toString(),
layout.multiModuleProjectDirectory().toString()));
daemon.dispatch(new Message.BuildRequest(
args,
Layout.getProperty("user.dir"),
Layout.getProperty("maven.multiModuleProjectDirectory")));
List<String> log = new ArrayList<>();
LinkedHashMap<String, String> projects = new LinkedHashMap<>();
Terminal terminal = TerminalBuilder.terminal();
Display display = new Display(terminal, false);
boolean exit = false;
BuildException error = null;
long lastUpdate = 0;
while (!exit) {
Message m = daemon.receive();
if (m instanceof BuildException) {
error = (BuildException) m;
exit = true;
} else if (m instanceof BuildEvent) {
BuildEvent be = (BuildEvent) m;
switch (be.getType()) {
case BuildStarted:
break;
case BuildStopped:
exit = true;
break;
case ProjectStarted:
case MojoStarted:
case MojoStopped:
projects.put(be.projectId, be.display);
break;
case ProjectStopped:
projects.remove(be.projectId);
}
// no need to refresh the display at every single step
long curTime = System.currentTimeMillis();
if (curTime - lastUpdate >= 10) {
Size size = terminal.getSize();
display.resize(size.getRows(), size.getColumns());
List<AttributedString> lines = new ArrayList<>();
projects.values().stream()
.map(AttributedString::fromAnsi)
.map(s -> s.columnSubSequence(0, size.getColumns() - 1))
.forEachOrdered(lines::add);
// Make sure we don't try to display more lines than the terminal height
int rem = 0;
while (lines.size() >= terminal.getHeight()) {
lines.remove(0);
rem++;
while (true) {
Message m = daemon.receive();
if (m instanceof BuildException) {
output.error((BuildException) m);
return new ClientResult<O>(argv, false, output);
} else if (m instanceof BuildEvent) {
BuildEvent be = (BuildEvent) m;
switch (be.getType()) {
case BuildStarted:
break;
case BuildStopped:
return new ClientResult<O>(argv, true, output);
case ProjectStarted:
case MojoStarted:
case MojoStopped:
output.projectStateChanged(be.projectId, be.display);
break;
case ProjectStopped:
output.projectFinished(be.projectId);
}
lines.add(0, new AttributedString("Building..." + (rem > 0 ? " (" + rem + " more)" : "")));
display.update(lines, -1);
lastUpdate = curTime;
}
} else if (m instanceof BuildMessage) {
BuildMessage bm = (BuildMessage) m;
log.add(bm.getMessage());
}
}
display.update(Collections.emptyList(), 0);
if (error != null) {
AttributedStyle s = new AttributedStyle().bold().foreground(AttributedStyle.RED);
String msg;
if (UnrecognizedOptionException.class.getName().equals(error.getClassName())) {
msg = "Unable to parse command line options: " + error.getMessage();
} else {
msg = error.getClassName() + ": " + error.getMessage();
}
terminal.writer().println(new AttributedString(msg, s).toAnsi());
}
terminal.flush();
LOGGER.debug("Done receiving, printing log");
if (logFile != null) {
try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(logFile))) {
for (String l : log) {
bw.write(l);
bw.newLine();
} else if (m instanceof BuildMessage) {
BuildMessage bm = (BuildMessage) m;
output.log(bm.getMessage());
}
}
} else {
log.forEach(terminal.writer()::println);
terminal.flush();
}
LOGGER.debug("Done !");
}
static void setDefaultArgs(List<String> args) {
@@ -225,7 +177,7 @@ public class Client {
}
}
public static String startDaemon() {
String startDaemon() {
// DaemonParameters parms = new DaemonParameters();
// for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
//
@@ -237,9 +189,9 @@ public class Client {
// args.add(classpath);
String uid = UUID.randomUUID().toString();
Path mavenHome = Layout.mavenHome();
Path javaHome = Layout.javaHome();
Path workingDir = Layout.userDir();
Path mavenHome = layout.mavenHome();
Path javaHome = layout.javaHome();
Path workingDir = layout.userDir();
String command = "";
try {
String url = ServerMain.class.getClassLoader().getResource(Server.class.getName().replace('.', '/') + ".class").toString();
@@ -259,6 +211,7 @@ public class Client {
if (System.getProperty(Server.DAEMON_IDLE_TIMEOUT) != null) {
args.add("-D" + Server.DAEMON_IDLE_TIMEOUT + "=" + System.getProperty(Server.DAEMON_IDLE_TIMEOUT));
}
args.add("\"-Dmaven.multiModuleProjectDirectory=" + layout.multiModuleProjectDirectory().toString() + "\"");
args.add(ServerMain.class.getName());
command = String.join(" ", args);

View File

@@ -0,0 +1,192 @@
package org.jboss.fuse.mvnd.daemon;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.function.Consumer;
import org.apache.commons.cli.UnrecognizedOptionException;
import org.jboss.fuse.mvnd.daemon.Message.BuildException;
import org.jline.terminal.Size;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.jline.utils.Display;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A sink for various kinds of events sent by the daemon.
*/
public interface ClientOutput extends AutoCloseable {
public void projectStateChanged(String projectId, String display);
public void projectFinished(String projectId);
public void log(String message);
public void error(BuildException m);
public void debug(String string);
/**
* A terminal {@link ClientOutput} based on JLine.
*/
static class TerminalOutput implements ClientOutput {
private static final Logger LOGGER = LoggerFactory.getLogger(TerminalOutput.class);
private final Terminal terminal;
private final Display display;
private final LinkedHashMap<String, String> projects = new LinkedHashMap<>();
private long lastUpdate = 0;
private final Log log;
public TerminalOutput(Path logFile) throws IOException {
this.terminal = TerminalBuilder.terminal();
this.display = new Display(terminal, false);
this.log = logFile == null ? new ClientOutput.Log.MessageCollector(terminal)
: new ClientOutput.Log.FileLog(logFile);
}
public void projectStateChanged(String projectId, String task) {
projects.put(projectId, task);
update();
}
private void update() {
// no need to refresh the display at every single step
long curTime = System.currentTimeMillis();
if (curTime - lastUpdate >= 10) {
Size size = terminal.getSize();
display.resize(size.getRows(), size.getColumns());
List<AttributedString> lines = new ArrayList<>();
projects.values().stream()
.map(AttributedString::fromAnsi)
.map(s -> s.columnSubSequence(0, size.getColumns() - 1))
.forEachOrdered(lines::add);
// Make sure we don't try to display more lines than the terminal height
int rem = 0;
while (lines.size() >= terminal.getHeight()) {
lines.remove(0);
rem++;
}
lines.add(0, new AttributedString("Building..." + (rem > 0 ? " (" + rem + " more)" : "")));
display.update(lines, -1);
lastUpdate = curTime;
}
}
public void projectFinished(String projectId) {
projects.remove(projectId);
update();
}
@Override
public void log(String message) {
log.accept(message);
}
@Override
public void close() throws Exception {
display.update(Collections.emptyList(), 0);
LOGGER.debug("Done receiving, printing log");
log.close();
LOGGER.debug("Done !");
terminal.flush();
}
@Override
public void error(BuildException error) {
display.update(Collections.emptyList(), 0);
final AttributedStyle s = new AttributedStyle().bold().foreground(AttributedStyle.RED);
final String msg;
if (UnrecognizedOptionException.class.getName().equals(error.getClassName())) {
msg = "Unable to parse command line options: " + error.getMessage();
} else {
msg = error.getClassName() + ": " + error.getMessage();
}
terminal.writer().println(new AttributedString(msg, s).toAnsi());
terminal.flush();
}
@Override
public void debug(String msg) {
LOGGER.debug(msg);
}
}
/**
* A closeable string message consumer.
*/
interface Log extends Consumer<String>, AutoCloseable {
/**
* A {@link Log} backed by a file.
*/
public static class FileLog implements Log {
private final Writer out;
private Path logFile;
public FileLog(Path logFile) throws IOException {
super();
this.out = Files.newBufferedWriter(logFile, StandardCharsets.UTF_8);
this.logFile = logFile;
}
@Override
public void accept(String message) {
try {
out.write(message);
out.write('\n');
} catch (IOException e) {
throw new RuntimeException("Could not write to " + logFile, e);
}
}
@Override
public void close() throws IOException {
out.close();
}
}
/**
* A {@link Log} that first collects all incoming messages in a {@link List} and outputs them to a JLine
* {@link Terminal} upon {@link #close()}.
*/
public static class MessageCollector implements Log {
private final List<String> messages = new ArrayList<>();
private final Terminal terminal;
public MessageCollector(Terminal terminal) {
super();
this.terminal = terminal;
}
@Override
public void accept(String message) {
messages.add(message);
}
@Override
public void close() {
messages.forEach(terminal.writer()::println);
terminal.flush();
}
}
}
}

View File

@@ -0,0 +1,54 @@
package org.jboss.fuse.mvnd.daemon;
import java.util.ArrayList;
import java.util.List;
/**
* A result of a {@code mvnd} build.
*
* @param <O> the type of the {@link ClientOutput}.
*/
public class ClientResult<O extends ClientOutput> {
private final boolean success;
private final O clientOutput;
private final List<String> args;
public ClientResult(List<String> args, boolean success, O clientOutput) {
super();
this.args = new ArrayList<>(args);
this.success = success;
this.clientOutput = clientOutput;
}
public ClientResult<O> assertSuccess() {
if (!this.success) {
throw new AssertionError(appendCommand(new StringBuilder("Build failed: ")));
}
return this;
}
public ClientResult<O> assertFailure() {
if (this.success) {
throw new AssertionError(appendCommand(new StringBuilder("Build did not fail: ")));
}
return this;
}
public O getClientOutput() {
return clientOutput;
}
public boolean isSuccess() {
return success;
}
StringBuilder appendCommand(StringBuilder sb) {
sb.append("mvnd");
for (String arg : args) {
sb.append(" \"").append(arg).append('"');
}
return sb;
}
}

View File

@@ -43,10 +43,12 @@ public class DaemonConnector {
private static final Logger LOGGER = LoggerFactory.getLogger(DaemonConnector.class);
private final DaemonRegistry registry;
private final Layout layout;
private final DaemonStarter daemonStarter;
private final Serializer<Message> serializer;
public DaemonConnector(DaemonRegistry registry, DaemonStarter daemonStarter, Serializer<Message> serializer) {
public DaemonConnector(Layout layout, DaemonRegistry registry, DaemonStarter daemonStarter, Serializer<Message> serializer) {
this.layout = layout;
this.registry = registry;
this.daemonStarter = daemonStarter;
this.serializer = serializer;
@@ -214,7 +216,7 @@ public class DaemonConnector {
throw new DaemonException.InterruptedException(e);
}
} while (System.currentTimeMillis() - start < DEFAULT_CONNECT_TIMEOUT);
DaemonDiagnostics diag = new DaemonDiagnostics(daemon);
DaemonDiagnostics diag = new DaemonDiagnostics(daemon, layout.daemonLog(daemon));
throw new DaemonException.ConnectException("Timeout waiting to connect to the Maven daemon.\n" + diag.describe());
}
@@ -225,7 +227,7 @@ public class DaemonConnector {
try {
return connectToDaemon(daemonInfo, new CleanupOnStaleAddress(daemonInfo, false));
} catch (DaemonException.ConnectException e) {
DaemonDiagnostics diag = new DaemonDiagnostics(daemon);
DaemonDiagnostics diag = new DaemonDiagnostics(daemon, layout.daemonLog(daemon));
throw new DaemonException.ConnectException("Could not connect to the Maven daemon.\n" + diag.describe(), e);
}
}

View File

@@ -32,9 +32,9 @@ public class DaemonDiagnostics {
private final String uid;
private final Path daemonLog;
public DaemonDiagnostics(String uid) {
public DaemonDiagnostics(String uid, Path daemonLog) {
this.uid = uid;
this.daemonLog = Layout.daemonLog(uid);
this.daemonLog = daemonLog;
}
@Override

View File

@@ -89,10 +89,6 @@ public class DaemonRegistry implements AutoCloseable {
}
}
public static DaemonRegistry getDefault() {
return new DaemonRegistry(Layout.registry());
}
public Path getRegistryFile() {
return registryFile;
}

View File

@@ -17,34 +17,61 @@ package org.jboss.fuse.mvnd.daemon;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class Layout {
public static Path javaHome() {
return Paths.get(getProperty("java.home")).toAbsolutePath().normalize();
private static Layout ENV_INSTANCE;
private final Path javaHome;
private final Path mavenHome;
private final Path userDir;
private final Path multiModuleProjectDirectory;
public Layout(Path javaHome, Path mavenHome, Path userDir, Path multiModuleProjectDirectory) {
super();
this.javaHome = javaHome;
this.mavenHome = mavenHome;
this.userDir = userDir;
this.multiModuleProjectDirectory = multiModuleProjectDirectory;
}
public static Path mavenHome() {
return Paths.get(getProperty("maven.home")).toAbsolutePath().normalize();
public Path javaHome() {
return javaHome;
}
public static Path userDir() {
return Paths.get(getProperty("user.dir")).toAbsolutePath().normalize();
public Path mavenHome() {
return mavenHome;
}
public static Path registry() {
return mavenHome().resolve("daemon/registry.bin");
public Path userDir() {
return userDir;
}
public static Path daemonLog(String daemon) {
return mavenHome().resolve("daemon/daemon-" + daemon + ".log");
public Path registry() {
return mavenHome.resolve("daemon/registry.bin");
}
public static String getProperty(String key) {
public Path daemonLog(String daemon) {
return mavenHome.resolve("daemon/daemon-" + daemon + ".log");
}
public Path multiModuleProjectDirectory() {
return multiModuleProjectDirectory;
}
private static String getProperty(String key) {
return Objects.requireNonNull(System.getProperty(key), "Undefined system property: " + key);
}
public static Layout getEnvInstance() {
if (ENV_INSTANCE == null) {
ENV_INSTANCE = new Layout(Paths.get(getProperty("java.home")).toAbsolutePath().normalize(),
Paths.get(getProperty("maven.home")).toAbsolutePath().normalize(),
Paths.get(getProperty("user.dir")).toAbsolutePath().normalize(),
Paths.get(getProperty("maven.multiModuleProjectDirectory")).toAbsolutePath().normalize());
}
return ENV_INSTANCE;
}
}

View File

@@ -70,6 +70,7 @@ public class Server implements AutoCloseable, Runnable {
private DaemonMavenCli cli;
private DaemonInfo info;
private DaemonRegistry registry;
private final Layout layout;
private ScheduledExecutorService executor;
private DaemonExpirationStrategy strategy;
@@ -79,9 +80,11 @@ public class Server implements AutoCloseable, Runnable {
public Server(String uid) throws IOException {
this.uid = uid;
this.layout = Layout.getEnvInstance();
try {
cli = new DaemonMavenCli();
registry = DaemonRegistry.getDefault();
registry = new DaemonRegistry(layout.registry());
socket = ServerSocketChannel.open().bind(new InetSocketAddress(0));
int idleTimeout;
@@ -95,7 +98,7 @@ public class Server implements AutoCloseable, Runnable {
List<String> opts = new ArrayList<>();
long cur = System.currentTimeMillis();
info = new DaemonInfo(uid, Layout.javaHome().toString(), Layout.mavenHome().toString(),
info = new DaemonInfo(uid, layout.javaHome().toString(), layout.mavenHome().toString(),
DaemonRegistry.getProcessId(), socket.socket().getLocalPort(),
idleTimeout, Locale.getDefault().toLanguageTag(), opts,
Busy, cur, cur);

View File

@@ -23,7 +23,7 @@
exec 1>/dev/null
exec 2>/dev/null
if [ "x${dir}" != "x" ]; then
cd ${dir}
cd "${dir}"
fi
nohup ${command} &
echo $! > "${pid.file}"

62
integration-tests/pom.xml Normal file
View File

@@ -0,0 +1,62 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jboss.fuse.mvnd</groupId>
<artifactId>mvnd</artifactId>
<version>0.1-SNAPSHOT</version>
</parent>
<artifactId>mvnd-integration-tests</artifactId>
<name>Maven Daemon - Integration Tests</name>
<dependencies>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.fuse.mvnd</groupId>
<artifactId>mvnd-daemon</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<project.version>${project.version}</project.version>
<mvnd.home>${project.basedir}/../daemon/target/maven-distro</mvnd.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,40 @@
package org.jboss.fuse.mvnd.assertj;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.assertj.core.api.Condition;
/**
* An AssertJ {@link Condition} to assert that each item of a collection of regular expressions matches some item in
* a list of strings exactly once in the order given by the pattern collection. The input list may contain other
* non-matching items.
*
* @param <T> the type of the tested {@link List}.
*/
public class MatchInOrderAmongOthers<T extends List<? extends String>> extends Condition<T> {
public MatchInOrderAmongOthers(String... expectedItems) {
this(Stream.of(expectedItems).map(Pattern::compile).collect(Collectors.toList()));
}
public MatchInOrderAmongOthers(final Collection<Pattern> patterns) {
super(
messages -> messages.stream()
/* map each message to the matching pattern or null of none matches */
.map(m -> patterns.stream()
.filter(pat -> pat.matcher(m).find())
.findFirst()
.orElse(null))
.filter(pat -> pat != null) /* remove null patterns */
.collect(Collectors.toList())
/* if the mapped patterns equal the input patterns then each pattern matched exactly once */
.equals(patterns),
"Match in order: " + patterns.stream().map(Pattern::pattern).collect(Collectors.joining(", ")),
patterns);
}
}

View File

@@ -0,0 +1,31 @@
package org.jboss.fuse.mvnd.it;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
public class MvndTestUtil {
private MvndTestUtil() {
}
public static String plugin(Properties props, String artifactId) {
return artifactId + ":" + props.getProperty(artifactId + ".version");
}
public static Properties properties(Path pomXmlPath) {
try (Reader runtimeReader = Files.newBufferedReader(pomXmlPath, StandardCharsets.UTF_8)) {
final MavenXpp3Reader rxppReader = new MavenXpp3Reader();
return rxppReader.read(runtimeReader).getProperties();
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException("Could not read or parse " + pomXmlPath);
}
}
}

View File

@@ -0,0 +1,106 @@
package org.jboss.fuse.mvnd.it;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import javax.inject.Inject;
import org.assertj.core.api.Assertions;
import org.jboss.fuse.mvnd.assertj.MatchInOrderAmongOthers;
import org.jboss.fuse.mvnd.daemon.Client;
import org.jboss.fuse.mvnd.daemon.ClientOutput;
import org.jboss.fuse.mvnd.daemon.Layout;
import org.jboss.fuse.mvnd.junit.MvndTest;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
@MvndTest(projectDir = "src/test/projects/single-module")
public class SingleModuleTest {
@Inject
Client client;
@Inject
Layout layout;
@Test
void cleanTest() throws IOException {
final Path helloFilePath = layout.multiModuleProjectDirectory().resolve("target/hello.txt");
if (Files.exists(helloFilePath)) {
Files.delete(helloFilePath);
}
final ClientOutput output = Mockito.mock(ClientOutput.class);
client.execute(output, "clean", "test").assertSuccess();
final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class);
Mockito.verify(output, Mockito.atLeast(1)).log(logMessage.capture());
Assertions.assertThat(logMessage.getAllValues())
.is(new MatchInOrderAmongOthers<>(
"Building single-module",
"maven-clean-plugin:[^:]+:clean",
"maven-compiler-plugin:[^:]+:compile",
"maven-compiler-plugin:[^:]+:testCompile",
"maven-surefire-plugin:[^:]+:test",
"SUCCESS build of project org.jboss.fuse.mvnd.test.single-module:single-module"));
final Properties props = MvndTestUtil.properties(layout.multiModuleProjectDirectory().resolve("pom.xml"));
final InOrder inOrder = Mockito.inOrder(output);
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module:org.apache.maven.plugins:" + MvndTestUtil.plugin(props, "maven-clean-plugin")
+ ":clean {execution: default-clean}");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module:org.apache.maven.plugins:" + MvndTestUtil.plugin(props, "maven-resources-plugin")
+ ":resources {execution: default-resources}");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module:org.apache.maven.plugins:" + MvndTestUtil.plugin(props, "maven-compiler-plugin")
+ ":compile {execution: default-compile}");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module:org.apache.maven.plugins:" + MvndTestUtil.plugin(props, "maven-resources-plugin")
+ ":testResources {execution: default-testResources}");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module:org.apache.maven.plugins:" + MvndTestUtil.plugin(props, "maven-compiler-plugin")
+ ":testCompile {execution: default-testCompile}");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module:org.apache.maven.plugins:" + MvndTestUtil.plugin(props, "maven-surefire-plugin")
+ ":test {execution: default-test}");
inOrder.verify(output).projectStateChanged(
"single-module",
":single-module");
inOrder.verify(output).projectFinished("single-module");
/* The target/hello.txt is created by HelloTest */
Assertions.assertThat(helloFilePath).exists();
}
}

View File

@@ -0,0 +1,57 @@
package org.jboss.fuse.mvnd.it;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.Condition;
import org.jboss.fuse.mvnd.assertj.MatchInOrderAmongOthers;
import org.jboss.fuse.mvnd.daemon.Client;
import org.jboss.fuse.mvnd.daemon.ClientOutput;
import org.jboss.fuse.mvnd.daemon.DaemonInfo;
import org.jboss.fuse.mvnd.daemon.DaemonRegistry;
import org.jboss.fuse.mvnd.junit.MvndTest;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
@MvndTest(projectDir = "src/test/projects/single-module")
public class StopStatusTest {
@Inject
Client client;
@Inject
DaemonRegistry registry;
@Test
void stopStatus() throws IOException {
/* The registry should be empty before we run anything */
Assertions.assertThat(registry.getAll()).isEmpty();
client.execute(Mockito.mock(ClientOutput.class), "clean").assertSuccess();
/* There should be exactly one item in the registry after the first build */
Assertions.assertThat(registry.getAll().size()).isEqualTo(1);
final ClientOutput output = Mockito.mock(ClientOutput.class);
client.execute(output, "--status").assertSuccess();
final DaemonInfo d = registry.getAll().get(0);
final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class);
Mockito.verify(output, Mockito.atLeast(1)).log(logMessage.capture());
Assertions.assertThat(logMessage.getAllValues())
.is(new MatchInOrderAmongOthers<>(
d.getUid() + " +" + d.getPid() + " +" + d.getAddress() + " +" + d.getState()));
client.execute(Mockito.mock(ClientOutput.class), "clean").assertSuccess();
/* There should still be exactly one item in the registry after the second build */
Assertions.assertThat(registry.getAll().size()).isEqualTo(1);
client.execute(Mockito.mock(ClientOutput.class), "--stop").assertSuccess();
/* No items in the registry after we have killed all daemons */
Assertions.assertThat(registry.getAll()).isEmpty();
}
}

View File

@@ -0,0 +1,34 @@
package org.jboss.fuse.mvnd.it;
import java.io.IOException;
import javax.inject.Inject;
import org.assertj.core.api.Assertions;
import org.jboss.fuse.mvnd.assertj.MatchInOrderAmongOthers;
import org.jboss.fuse.mvnd.daemon.Client;
import org.jboss.fuse.mvnd.daemon.ClientOutput;
import org.jboss.fuse.mvnd.junit.MvndTest;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
@MvndTest(projectDir = "src/test/projects/single-module")
public class VersionTest {
@Inject
Client client;
@Test
void version() throws IOException {
final ClientOutput output = Mockito.mock(ClientOutput.class);
client.execute(output, "-v").assertSuccess();
final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class);
Mockito.verify(output, Mockito.atLeast(1)).log(logMessage.capture());
Assertions.assertThat(logMessage.getAllValues())
.is(new MatchInOrderAmongOthers<>("Maven Daemon " + System.getProperty("project.version")));
}
}

View File

@@ -0,0 +1,19 @@
package org.jboss.fuse.mvnd.junit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(MvndTestExtension.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MvndTest {
/**
* The path to the root directory of a test project relative to the current maven module directory. E.g.
* <code>@MvndTest(projectDir = "src/test/projects/my-project")</code>
*/
String projectDir();
}

View File

@@ -0,0 +1,153 @@
package org.jboss.fuse.mvnd.junit;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.jboss.fuse.mvnd.daemon.Client;
import org.jboss.fuse.mvnd.daemon.DaemonInfo;
import org.jboss.fuse.mvnd.daemon.DaemonRegistry;
import org.jboss.fuse.mvnd.daemon.Layout;
import org.jboss.fuse.mvnd.jpm.ProcessImpl;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
public class MvndTestExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback {
private volatile Exception bootException;
public MvndTestExtension() {
super();
}
@Override
public void beforeAll(ExtensionContext context) throws Exception {
try {
final Store store = context.getRoot().getStore(ExtensionContext.Namespace.GLOBAL);
final Class<?> testClass = context.getRequiredTestClass();
final MvndTest mnvdTest = testClass.getAnnotation(MvndTest.class);
store.put(MvndResource.class.getName(), MvndResource.create(context.getRequiredTestClass().getSimpleName(), mnvdTest.projectDir()));
} catch (Exception e) {
this.bootException = e;
}
}
@Override
public void beforeEach(ExtensionContext context) throws Exception {
if (bootException != null) {
throw new Exception("Could not init " + context.getRequiredTestClass(), bootException);
}
final Store store = context.getRoot().getStore(ExtensionContext.Namespace.GLOBAL);
final MvndResource resource = (MvndResource) store.get(MvndResource.class.getName());
final Object testInstance = context.getRequiredTestInstance();
Class<?> c = testInstance.getClass();
while (c != Object.class) {
for (Field f : c.getDeclaredFields()) {
javax.inject.Inject inject = f.getAnnotation(javax.inject.Inject.class);
if (inject != null) {
f.setAccessible(true);
if (f.getType() == DaemonRegistry.class) {
f.set(testInstance, resource.registry);
} else if (f.getType() == Layout.class) {
f.set(testInstance, resource.layout);
} else if (f.getType() == Client.class) {
f.set(testInstance, new Client(resource.layout));
}
}
}
c = c.getSuperclass();
}
}
@Override
public void afterAll(ExtensionContext context) throws Exception {
final Store store = context.getRoot().getStore(ExtensionContext.Namespace.GLOBAL);
final MvndResource resource = (MvndResource) store.remove(MvndResource.class.getName());
if (resource != null) {
resource.close();
}
}
static class MvndResource implements ExtensionContext.Store.CloseableResource {
private final Layout layout;
private final DaemonRegistry registry;
public static MvndResource create(String className, String rawProjectDir) throws IOException {
if (rawProjectDir == null) {
throw new IllegalStateException("rawProjectDir of @MvndTest must be set");
}
final Path mvndTestSrcDir = Paths.get(rawProjectDir).toAbsolutePath().normalize();
if (!Files.exists(mvndTestSrcDir)) {
throw new IllegalStateException("@MvndTest(projectDir = \""+ rawProjectDir +"\") points at a path that does not exist: " + mvndTestSrcDir);
}
final Path testDir = Paths.get("target/mvnd-tests/" + className).toAbsolutePath();
try (Stream<Path> files = Files.walk(mvndTestSrcDir)) {
files.forEach(source -> {
final Path dest = testDir.resolve(mvndTestSrcDir.relativize(source));
try {
if (Files.isDirectory(source)) {
Files.createDirectories(dest);
} else {
Files.createDirectories(dest.getParent());
Files.copy(source, dest);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
final Path mvndHome = Paths.get(Objects.requireNonNull(System.getProperty("mvnd.home"), "System property mvnd.home must be set")).normalize().toAbsolutePath();
if (!Files.isDirectory(mvndHome)) {
throw new IllegalStateException("The value of mvnd.home system property points at a path that does not exist or is not a directory");
}
final Layout layout = new Layout(Paths.get(System.getProperty("java.home")).toAbsolutePath().normalize(),
mvndHome,
testDir,
testDir);
final DaemonRegistry registry = new DaemonRegistry(layout.registry());
return new MvndResource(layout, registry);
}
public MvndResource(Layout layout, DaemonRegistry registry) {
super();
this.layout = layout;
this.registry = registry;
}
@Override
public void close() throws Exception {
List<DaemonInfo> daemons;
final int timeout = 5000;
final long deadline = System.currentTimeMillis() + timeout;
while (!(daemons = registry.getAll()).isEmpty()) {
for (DaemonInfo di : daemons) {
try {
new ProcessImpl(di.getPid()).destroy();
} catch (IOException t) {
System.out.println("Daemon " + di.getUid() + ": " + t.getMessage());
} catch (Exception t) {
System.out.println("Daemon " + di.getUid() + ": " + t);
} finally {
registry.remove(di.getUid());
}
}
if (deadline < System.currentTimeMillis() && !registry.getAll().isEmpty()) {
throw new RuntimeException("Could not stop all mvnd daemons within " + timeout + " ms");
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.jboss.fuse.mvnd.test.single-module</groupId>
<artifactId>single-module</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven-clean-plugin.version>2.5</maven-clean-plugin.version>
<maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
<maven-resources-plugin.version>2.6</maven-resources-plugin.version>
<maven-surefire-plugin.version>3.0.0-M4</maven-surefire-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>${maven-clean-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>${maven-resources-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@@ -0,0 +1,9 @@
package org.jboss.fuse.mvnd.test.single.module;
public class Hello {
public String sayHello() {
return "Hello";
}
}

View File

@@ -0,0 +1,18 @@
package org.jboss.fuse.mvnd.test.single.module;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class HelloTest {
@Test
void hello() throws IOException {
final String actual = new Hello().sayHello();
Files.write(Paths.get("target/hello.txt"), actual.getBytes(StandardCharsets.UTF_8));
Assertions.assertEquals("Hello", actual);
}
}

68
pom.xml
View File

@@ -12,23 +12,89 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<assertj.version>3.16.1</assertj.version>
<groovyVersion>3.0.0</groovyVersion>
<jakarta.inject.version>1.0</jakarta.inject.version>
<jlineVersion>3.12.1</jlineVersion>
<junitVersion>4.12</junitVersion>
<junit.jupiter.version>5.6.0</junit.jupiter.version>
<logbackVersion>1.2.3</logbackVersion>
<mavenVersion>3.6.3</mavenVersion>
<mockito.version>3.3.3</mockito.version>
<pluginTestingVersion>2.9.2</pluginTestingVersion>
<slf4jVersion>1.7.25</slf4jVersion>
<takariLifecycleVersion>1.13.9</takariLifecycleVersion>
<takariProvisioVersion>0.1.56</takariProvisioVersion>
<takariLocalRepositoryVersion>0.11.2</takariLocalRepositoryVersion>
<mavenCompilerPluginVersion>3.8.1</mavenCompilerPluginVersion>
<mavenSurefirePluginVersion>2.22.2</mavenSurefirePluginVersion>
<surefire.version>3.0.0-M4</surefire.version>
</properties>
<modules>
<module>daemon</module>
<module>integration-tests</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.jupiter.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>${jakarta.inject.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<version>${mavenVersion}</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.fuse.mvnd</groupId>
<artifactId>mvnd-daemon</artifactId>
<version>0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.jboss.fuse.mvnd</groupId>
<artifactId>mvnd-junit5</artifactId>
<version>0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>