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

@@ -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);
}
}