Skip to content

Commit

Permalink
Beginning of Surefire Emulator (#61)
Browse files Browse the repository at this point in the history
* makes all sleep ms different from each other

* create surefire emulator

* inject logger in the emulator and add our first assertion
  • Loading branch information
fabriciorby authored Dec 1, 2024
1 parent f942700 commit 83e7b0a
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 28 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,15 @@ If you ever want to debug the code, please use the following command
mvnDebug test
```
Then attach a remote JVM debugger on port 8000

### Using SurefireEmulator

This SurefireEmulator class was developed so it's easier to debug the code.

```java
new SurefireEmulator(NestedExampleTest.class).run();
```

By running this command it's possible to debug the code almost as the actual
Surefire was running. And it also returns a List with all the lines that were
printed during the test execution.
10 changes: 3 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,9 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package org.apache.maven.plugin.surefire;

import org.junit.jupiter.api.*;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

import static org.junit.jupiter.api.Assertions.assertEquals;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package org.apache.maven.plugin.surefire;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package org.apache.maven.plugin.surefire;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package org.apache.maven.plugin.surefire;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand All @@ -17,7 +19,7 @@ class FirstInnerTest {
@Test
@DisplayName("FirstInnerTest should show up")
void test() throws InterruptedException {
Thread.sleep(100);
Thread.sleep(600);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package org.apache.maven.plugin.surefire;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package org.apache.maven.plugin.surefire.report;

import org.apache.maven.plugin.surefire.AppTest;
import org.apache.maven.plugin.surefire.NestedExampleTest;
import org.apache.maven.plugin.surefire.log.PluginConsoleLogger;
import org.apache.maven.surefire.api.report.RunMode;
import org.apache.maven.surefire.api.report.SimpleReportEntry;
import org.codehaus.plexus.DefaultPlexusContainer;
import org.codehaus.plexus.PlexusContainerException;
import org.codehaus.plexus.logging.Logger;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class ConsoleTreeReporterTest {

//Test for NestedExampleTest
//Test for org.apache.maven.plugin.surefire.NestedExampleTest
Utf8RecodingDeferredFileOutputStream stdout = new Utf8RecodingDeferredFileOutputStream("stdout");
Utf8RecodingDeferredFileOutputStream stderr = new Utf8RecodingDeferredFileOutputStream("stderr");

Expand All @@ -28,13 +30,23 @@ static void setupContainer() throws PlexusContainerException {
logger = container.getLogger();
}

@Test
void testEmulator() {
// Now we can check the output of any Test class using this
// TODO: Add some proxy before the logger or something so we can assert the output
// TODO: Add some objects with relevant information inside the emulator
SurefireEmulator surefireEmulator = new SurefireEmulator(NestedExampleTest.class);
List<String> logs = surefireEmulator.run();
assertThat(logs).isNotEmpty();
}

@Test
void testSetStarting() {
//Runs 4 times for this class
SimpleReportEntry simpleReportEntry1 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest", "Nested Sample", null, null);
SimpleReportEntry simpleReportEntry2 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$InnerTest", "Inner Test", null, null);
SimpleReportEntry simpleReportEntry3 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$InnerTest$InnerInnerTest", "Inner Inner Test", null, null);
SimpleReportEntry simpleReportEntry4 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$InnerTest$InnerInnerTest$InnerInnerInnerTest", "Inner Inner Inner Test", null, null);
SimpleReportEntry simpleReportEntry1 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest", "Nested Sample", null, null);
SimpleReportEntry simpleReportEntry2 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$InnerTest", "Inner Test", null, null);
SimpleReportEntry simpleReportEntry3 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$InnerTest$InnerInnerTest", "Inner Inner Test", null, null);
SimpleReportEntry simpleReportEntry4 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$InnerTest$InnerInnerTest$InnerInnerInnerTest", "Inner Inner Inner Test", null, null);

ConsoleTreeReporter consoleTreeReporter = new ConsoleTreeReporter(new PluginConsoleLogger(logger), ReporterOptions.builder().build());
consoleTreeReporter.testSetStarting(simpleReportEntry1);
Expand All @@ -47,19 +59,20 @@ void testSetStarting() {
void testSetCompleted() {

//TestStarting parameters
SimpleReportEntry simpleReportEntry1 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest", "Nested Sample", null, null);
SimpleReportEntry simpleReportEntry2 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$InnerTest", "Inner Test", null, null);
SimpleReportEntry simpleReportEntry3 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$InnerTest$InnerInnerTest", "Inner Inner Test", null, null);
SimpleReportEntry simpleReportEntry4 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$InnerTest$InnerInnerTest$InnerInnerInnerTest", "Inner Inner Inner Test", null, null);
SimpleReportEntry simpleReportEntry1 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest", "Nested Sample", null, null);
SimpleReportEntry simpleReportEntry2 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$InnerTest", "Inner Test", null, null);
SimpleReportEntry simpleReportEntry3 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$InnerTest$InnerInnerTest", "Inner Inner Test", null, null);
SimpleReportEntry simpleReportEntry4 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$InnerTest$InnerInnerTest$InnerInnerInnerTest", "Inner Inner Inner Test", null, null);
SimpleReportEntry simpleReportEntry5 = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$FirstInnerTest", "First Inner Test", null, null);

//Runs 1 time with all the information
//Gets all SingleReportEntries with test names and add on a list of WrapperReportEntries to create a TestSetStats
SimpleReportEntry firstTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest", "Nested Sample", "test", "Should pass");
SimpleReportEntry secondTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest", "Nested Sample", "test2", "Should pass2");
SimpleReportEntry thirdTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$InnerTest", "Inner Test", "test", "Inner test should pass");
SimpleReportEntry fourthTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$InnerTest$InnerInnerTest", "Inner Inner Test", "test", "Inner Inner Test should pass");
SimpleReportEntry fifthTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$InnerTest$InnerInnerTest$InnerInnerInnerTest", "Inner Inner Inner Test", "test", "Inner Inner Inner Test should pass");
SimpleReportEntry sixthTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "NestedExampleTest$FirstInnerTest", "First Inner Test", "test", "FirstInnerTest should show up");
SimpleReportEntry firstTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest", "Nested Sample", "test", "Should pass");
SimpleReportEntry secondTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest", "Nested Sample", "test2", "Should pass2");
SimpleReportEntry thirdTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$InnerTest", "Inner Test", "test", "Inner test should pass");
SimpleReportEntry fourthTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$InnerTest$InnerInnerTest", "Inner Inner Test", "test", "Inner Inner Test should pass");
SimpleReportEntry fifthTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$InnerTest$InnerInnerTest$InnerInnerInnerTest", "Inner Inner Inner Test", "test", "Inner Inner Inner Test should pass");
SimpleReportEntry sixthTest = new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, "org.apache.maven.plugin.surefire.NestedExampleTest$FirstInnerTest", "First Inner Test", "test", "FirstInnerTest should show up");

WrappedReportEntry wrappedReportEntry1 = new WrappedReportEntry(firstTest, ReportEntryType.SUCCESS, 1, stdout, stderr);
WrappedReportEntry wrappedReportEntry2 = new WrappedReportEntry(secondTest, ReportEntryType.SUCCESS, 1, stdout, stderr);
Expand All @@ -79,14 +92,18 @@ void testSetCompleted() {
TestSetStats testSetStatsForClass = new TestSetStats(false, true);

ConsoleTreeReporter consoleTreeReporter = new ConsoleTreeReporter(new PluginConsoleLogger(logger), ReporterOptions.builder().build());
//This prepares the nested tests by filling the classNames
consoleTreeReporter.testSetStarting(simpleReportEntry1);
consoleTreeReporter.testSetStarting(simpleReportEntry2);
consoleTreeReporter.testSetStarting(simpleReportEntry3);
consoleTreeReporter.testSetStarting(simpleReportEntry4);
consoleTreeReporter.testSetStarting(simpleReportEntry5);
//As soon as it finished to add tests for all the nested classes that were prepared, then it prints
consoleTreeReporter.testSetCompleted(wrappedReportEntry5, testSetStats, null);
consoleTreeReporter.testSetCompleted(wrappedReportEntry4, testSetStatsForClass, null);
consoleTreeReporter.testSetCompleted(wrappedReportEntry3, testSetStatsForClass, null);
consoleTreeReporter.testSetCompleted(wrappedReportEntry2, testSetStatsForClass, null);
consoleTreeReporter.testSetCompleted(wrappedReportEntry6, testSetStatsForClass, null);

//TODO see how to unit test this
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.apache.maven.plugin.surefire.report;

import org.codehaus.plexus.logging.AbstractLogger;
import org.codehaus.plexus.logging.Logger;

import java.util.ArrayList;
import java.util.List;

// Copy from org.codehaus.plexus.logging.console.ConsoleLogger so we can extend it

public class EmulatorLogger extends AbstractLogger {
private static final String[] TAGS = {"[DEBUG] ", "[INFO] ", "[WARNING] ", "[ERROR] ", "[FATAL ERROR] "};
private final ArrayList<String> logs = new ArrayList<>();

public EmulatorLogger() {
this(Logger.LEVEL_INFO, "console");
}

public EmulatorLogger(int threshold, String name) {
super(threshold, name);
}

public void debug(final String message, final Throwable throwable) {
if (isDebugEnabled()) log(LEVEL_DEBUG, message, throwable);
}

public void info(final String message, final Throwable throwable) {
if (isInfoEnabled()) log(LEVEL_INFO, message, throwable);
}

public void warn(final String message, final Throwable throwable) {
if (isWarnEnabled()) log(LEVEL_WARN, message, throwable);
}

public void error(final String message, final Throwable throwable) {
if (isErrorEnabled()) log(LEVEL_ERROR, message, throwable);
}

public void fatalError(final String message, final Throwable throwable) {
if (isFatalErrorEnabled()) log(LEVEL_FATAL, message, throwable);
}

public Logger getChildLogger(final String name) {
return this;
}

public void clearLogs() {
System.out.println("Cleaning logs...");
logs.clear();
}

public List<String> getLogList() {
return logs;
}

private void log(final int level, final String message, final Throwable throwable) {
logs.add(message);
// System.out.println(TAGS[level].concat(message));
System.out.println(message);
if (throwable != null) {
throwable.printStackTrace(System.out);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package org.apache.maven.plugin.surefire.report;

import org.apache.maven.plugin.surefire.log.PluginConsoleLogger;
import org.apache.maven.surefire.api.report.RunMode;
import org.apache.maven.surefire.api.report.SimpleReportEntry;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.platform.commons.util.StringUtils;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toList;
import static org.junit.jupiter.api.DisplayNameGenerator.getDisplayNameGenerator;
import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation;

public class SurefireEmulator {

private final EmulatorLogger emulatorLogger = new EmulatorLogger();
private final DisplayNameGenerator displayNameGenerator = getDisplayNameGenerator(DisplayNameGenerator.Standard.class);
private final Utf8RecodingDeferredFileOutputStream stdout = new Utf8RecodingDeferredFileOutputStream("stdout");
private final Utf8RecodingDeferredFileOutputStream stderr = new Utf8RecodingDeferredFileOutputStream("stderr");
private final Class<?> clazz;
private final ConsoleTreeReporter consoleTreeReporter;

public SurefireEmulator(Class<?> clazz) {
this(ReporterOptions.builder().build(), clazz);
}

public SurefireEmulator(ReporterOptions reporterOptions, Class<?> clazz) {
this.clazz = clazz;
this.consoleTreeReporter = new ConsoleTreeReporter(new PluginConsoleLogger(emulatorLogger), reporterOptions);
}

public List<String> run() {
testsStarting();
testsCompleted(testsSucceeded());
return emulatorLogger.getLogList();
}

private void testsCompleted(TestSetStats testSetStats) {
List<WrappedReportEntry> completedWrappedEntries =
getAllInnerClasses(clazz).stream()
.map(this::simpleReportEntryGenerator)
.map(this::wrappedReportEntryGenerator)
.collect(toList());

//List's head needs to be with complete testSetStats
completedWrappedEntries.stream().findFirst()
.ifPresent(i -> consoleTreeReporter.testSetCompleted(i, testSetStats, null));

//List's tail goes with empty testSetStats
completedWrappedEntries.stream().skip(1)
.forEachOrdered(i -> consoleTreeReporter.testSetCompleted(i, new TestSetStats(false, false), null));
}

private TestSetStats testsSucceeded() {
TestSetStats testSetStats = new TestSetStats(false, true);
getAllMethods(getAllInnerClasses(clazz))
.entrySet().stream()
.flatMap((k) -> k.getValue().stream()
.map(i -> this.simpleReportEntryGenerator(k.getKey(), i))
.map(this::wrappedReportEntryGenerator))
.forEachOrdered(testSetStats::testSucceeded);
return testSetStats;
}

private void testsStarting() {
getAllInnerClasses(clazz).stream()
.map(this::simpleReportEntryGenerator)
.forEachOrdered(consoleTreeReporter::testSetStarting);
}

private <T> SimpleReportEntry simpleReportEntryGenerator(Class<T> clazz) {
return new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, clazz.getName(), getClassDisplayName(clazz), null, null);
}

private <T> SimpleReportEntry simpleReportEntryGenerator(Class<T> clazz, Method method) {
return new SimpleReportEntry(RunMode.NORMAL_RUN, 123L, clazz.getName(), getClassDisplayName(clazz), method.getName(), getMethodDisplayName(clazz, method));
}

private WrappedReportEntry wrappedReportEntryGenerator(SimpleReportEntry simpleReportEntry) {
return new WrappedReportEntry(simpleReportEntry, ReportEntryType.SUCCESS, 1, stdout, stderr);
}

private List<Class<?>> getAllInnerClasses(Class<?> clazz) {
return getAllInnerClasses(clazz, new ArrayList<>());
}

private List<Class<?>> getAllInnerClasses(Class<?> clazz, List<Class<?>> acc) {
if (clazz.getDeclaredClasses().length == 0) {
acc.add(clazz);
return acc;
}
acc.add(clazz);
acc.addAll(Arrays.stream(clazz.getDeclaredClasses())
.flatMap(i -> getAllInnerClasses(i, new ArrayList<>()).stream())
.collect(toList()));
return acc;
}

private Map<Class<?>, List<Method>> getAllMethods(List<Class<?>> classes) {
return classes.stream()
.collect(Collectors.toMap(Function.identity(),
i -> Arrays.asList(i.getDeclaredMethods()),
(x, y) -> y, LinkedHashMap::new));
}

// Got the methods below from JUnit Jupiter codebase DisplayNameUtils.java
private String getDisplayName(AnnotatedElement element, Supplier<String> displayNameSupplier) {
Optional<DisplayName> displayNameAnnotation = findAnnotation(element, DisplayName.class);
if (displayNameAnnotation.isPresent()) {
String displayName = displayNameAnnotation.get().value().trim();
if (!StringUtils.isBlank(displayName)) return displayName;
}
return displayNameSupplier.get();
}

private <T> String getClassDisplayName(Class<T> clazz) {
if (clazz.getEnclosingClass() == null) {
return getDisplayName(clazz, () -> displayNameGenerator.generateDisplayNameForClass(clazz));
} else {
return getDisplayName(clazz, () -> displayNameGenerator.generateDisplayNameForNestedClass(clazz));
}
}

private <T> String getMethodDisplayName(Class<T> clazz, Method method) {
return getDisplayName(method, () -> displayNameGenerator.generateDisplayNameForMethod(clazz, method));
}
}

0 comments on commit 83e7b0a

Please sign in to comment.