Commit 4d5a8201 authored by Lückemeyer's avatar Lückemeyer
Browse files

initial commit of jdk17 junit5 jupiter example test runner

parent d2f28c3c
Pipeline #8684 passed with stage
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.2.0](https://transfer.hft-stuttgart.de///compare/1.1.0...1.2.0) (2020-12-30)
### Features
* **ci:** set git describe as build number ([d0c16d1](https://transfer.hft-stuttgart.de///commit/d0c16d15786a9971e9ca0b6a0814ac42af5f253e))
### Bug Fixes
* **app:** buildClasspath: build classpath differently for compile and runtime ([695f4e4](https://transfer.hft-stuttgart.de///commit/695f4e40829100a00a51c28f9a30e0745bf5ceb3))
* **app:** buildClasspath: check if folder exists before walk-through ([7c08633](https://transfer.hft-stuttgart.de///commit/7c0863382a1514856d4c2fe936844a12a74e1f76))
* add missing junit dependencies ([0c57620](https://transfer.hft-stuttgart.de///commit/0c576205318a827935975898964479d984234d03))
## [1.1.0](https://transfer.hft-stuttgart.de///compare/1.0.1...1.1.0) (2020-12-15)
### Features
* store compilation errors separately and with line,col and pos number ([29b68ed](https://transfer.hft-stuttgart.de///commit/29b68edb3fa6afdfcf237c0d5f8a277129550746))
### [1.0.1](https://transfer.hft-stuttgart.de///compare/1.0.0...1.0.1) (2020-12-14)
### Bug Fixes
* **ci:** registry name had a typo ([a3e5b2e](https://transfer.hft-stuttgart.de///commit/a3e5b2e21bd40389598f212b25561c0fdc50fb94))
## 1.0.0 (2020-12-14)
FROM amazoncorretto:17.0.3-alpine as corretto-jdk
env BASEDIR /data
env TESTDIR $BASEDIR/test
env SOURCEDIR $BASEDIR/src
env RESULTDIR $BASEDIR/result
env LIBSDIR $BASEDIR/libs
run mkdir -p $TESTDIR \
&& mkdir $SOURCEDIR \
&& mkdir $RESULTDIR \
&& mkdir -p /$LIBSDIR/additional
add target/dta-jdk17-junit5-runner-jar-with-dependencies.jar /$BASEDIR/app.jar
add https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-api/5.10.0/junit-jupiter-api-5.10.0.jar /$LIBSDIR/
add https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-engine/5.10.0/junit-jupiter-engine-5.10.0.jar /$LIBSDIR/
add https://repo1.maven.org/maven2/org/junit/platform/junit-platform-engine/1.10.0/junit-platform-engine-1.10.0.jar /$LIBSDIR/
add https://repo1.maven.org/maven2/org/junit/platform/junit-platform-commons/1.10.0/junit-platform-commons-1.10.0.jar /$LIBSDIR/
add https://repo1.maven.org/maven2/org/junit/platform/junit-platform-launcher/1.10.0/junit-platform-launcher-1.10.0.jar /$LIBSDIR/
add https://repo1.maven.org/maven2/org/apiguardian/apiguardian-api/1.1.1/apiguardian-api-1.1.1.jar /$LIBSDIR/
add https://repo1.maven.org/maven2/org/opentest4j/opentest4j/1.2.0/opentest4j-1.2.0.jar /$LIBSDIR/
workdir $BASEDIR
entrypoint java -Djava.security.egd=file:/dev/./urandom -jar /$BASEDIR/app.jar "$SOURCEDIR/src:$TESTDIR/test" "$LIBSDIR/*:$TESTDIR/libs/*" $RESULTDIR
def version = ""
pipeline {
environment {
registry = "hftstuttgart/dtt-openjdk11-junit5-testrunner"
registryCredential = 'Dockerhub'
dockerImage = ''
}
agent any
tools {
jdk 'Java11'
maven 'Maven_Home'
}
stages {
stage('prepare') {
steps {
checkout ([
$class: 'GitSCM',
branches: scm.branches,
extensions: scm.extensions + [[$class: 'CloneOption', noTags: false, reference: '', shallow: false]],
userRemoteConfigs: scm.userRemoteConfigs
])
script {
version = sh(script: 'git describe --tags --always', returnStdout: true).trim()
echo sh(script: 'env|sort', returnStdout: true)
}
}
}
stage('compile') {
steps {
sh "BUILD_NUMBER=${version} mvn clean package"
}
}
stage('build Docker image') {
steps {
script {
dockerImage = docker.build registry
}
}
}
stage('push development image') {
steps {
script {
docker.withRegistry( '', registryCredential ) {
dockerImage.push("${env.GIT_BRANCH}")
}
}
}
}
stage('release') {
when {
expression { version ==~ /[0-9]+.[0-9]+.[0-9]+/ }
}
steps {
script {
docker.withRegistry( '', registryCredential ) {
dockerImage.push("latest")
dockerImage.push("${version}")
}
}
}
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.hftstuttgart</groupId>
<artifactId>dta-jdk17-junit5-runner</artifactId>
<version>${env.BUILD_NUMBER}</version>
<packaging>jar</packaging>
<properties>
<buildNumber>${env.BUILD_NUMBER}</buildNumber>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<profiles>
<profile>
<id>ci</id>
<activation>
<property><name>env.BUILD_NUMBER</name></property>
</activation>
<properties>
<buildNumber>${env.BUILD_NUMBER}</buildNumber>
</properties>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.3</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>de.hftstuttgart.dta.Testrunner</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package de.hftstuttgart.dta;
import de.hftstuttgart.dta.model.Result;
import de.hftstuttgart.dta.model.ResultSummary;
import de.hftstuttgart.dta.util.MySummaryGeneratingListener;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
import javax.tools.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Testrunner
{
static
{
InputStream stream = Testrunner.class.getClassLoader()
.getResourceAsStream("logging.properties");
try
{
LogManager.getLogManager().readConfiguration(stream);
}
catch (IOException e)
{
e.printStackTrace();
}
}
private static final Logger LOG = Logger.getLogger(Testrunner.class.getName());
public static String[] sourceFolders; // folders with Java Files to compile
public static String[] libraryFolders; // folders with libraries in jar-file format
public static String classFolder; // folder to put compiled class files into
public static String[] classPathItems; // items for the classpath
public static String resultFolder; // folder the result file gets serialized to
public static void main(String[] args) throws Exception
{
LOG.info("OpenJDK11 JUnit5/Jupiter Testrunner started");
LOG.info("initializing fields...");
sourceFolders = args[0].split(":");
libraryFolders = args[1].split(":");
resultFolder = args[2];
classFolder = Files.createTempDirectory("testrunner").toAbsolutePath().toString();
// finding all source files
Set<File> sourceFiles = new HashSet<>();
for (String folder : sourceFolders)
{
sourceFiles.addAll(getAllJavaFilesInFolder(Paths.get(folder).toFile()));
}
// call compilation and generate Results for failed compiles
Set<Result> compilationErrors = generateCompileResults(compile(sourceFiles, new File(classFolder)));
// run unit tests found in the compiled class files
ResultSummary resultSummary = runTests();
// add compilation errors to summary
resultSummary.results.addAll(compilationErrors);
// serialize result
writeResult(resultSummary);
}
public static String[] buildClassPathItems(boolean runtime)
{
Set<String> classPathItemsBuild = new HashSet<>(Arrays.asList(libraryFolders));
classPathItemsBuild.add(classFolder);
if (runtime)
{
classPathItemsBuild.addAll(Arrays.stream(sourceFolders).collect(Collectors.toSet()));
}
return classPathItemsBuild.toArray(new String[0]);
}
public static Set<File> buildClassPath(String... paths)
{
Set<File> files = new HashSet<>();
for (String path : paths)
{
if (path.endsWith("*"))
{
path = path.substring(0, path.length() - 1);
File pathFile = new File(path);
if (!pathFile.exists() || !pathFile.isDirectory())
{
continue;
}
for (File file : Objects.requireNonNull(pathFile.listFiles()))
{
if (file.isFile() && file.getName().endsWith(".jar"))
{
files.add(file);
} else
{
files.addAll(buildClassPath(Paths.get(file.getPath(), "*").toString()));
}
}
} else
{
File file = new File(path);
if (file.exists())
{
files.add(file);
}
}
}
return files;
}
public static List<Diagnostic> compile(Set<File> files, File outputDir)
{
classPathItems = buildClassPathItems(false);
LOG.info("compilation started");
List<Diagnostic> compilationErrors = new LinkedList<>();
// Create the compiler and add a diagnostic listener to get the compilation errors
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticListener listener = compilationErrors::add;
StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, null, StandardCharsets.UTF_8);
Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjects(files.toArray(new File[0]));
if (!outputDir.exists())
{
outputDir.mkdir();
}
// Set the compiler option for a specific output path
List<String> options = new ArrayList<>();
options.add("-d"); // output dir
options.add(outputDir.getAbsolutePath());
options.add("-cp"); // custom classpath
String os=System.getProperty("os.name");
final String osSpecificCpDelim=(os.indexOf("Windows")>-1?";":":");
String cp = buildClassPath(classPathItems).stream()
.map(f -> f.getPath())
.reduce((s1, s2) -> s1 + osSpecificCpDelim + s2).orElse("");
LOG.info("classpath for compilation: " + cp);
options.add(cp);
// compile it
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, listener, options, null, fileObjects);
boolean compileResult = task.call();
// If the compilation failed, remove the failed file from the pathsToCompile list and try to compile again without this file
if (!compileResult)
{
File currentFile = new File(((JavaFileObject) compilationErrors.get(compilationErrors.size() - 1).getSource()).toUri().getPath());
LOG.log(Level.WARNING,"compilation of file '" + currentFile.getAbsolutePath() + "' failed");
files.removeIf(file -> file.getAbsolutePath().equalsIgnoreCase(currentFile.getAbsolutePath()));
if (files.size() > 0)
{
LOG.info(String.format("retry compilation without %s", currentFile.getName()));
compile(files, outputDir);
}
} else {
LOG.info("compilation finished");
}
return compilationErrors;
}
public static ClassLoader createCustomClassLoader() throws MalformedURLException
{
LOG.info("creating custom class loader for testing");
URL[] urls = buildClassPath(classPathItems).stream().map(f -> {
try {
return f.toURI().toURL();
} catch (MalformedURLException e) {
LOG.log(Level.SEVERE, e.getMessage(), e);
throw new RuntimeException(e.getMessage(), e);
}
}).toArray(URL[]::new);
LOG.info(String.format("JUnit ClassLoader context classpath: %s", Arrays.deepToString(urls)));
ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
return URLClassLoader.newInstance(urls, parentClassLoader);
}
public static Set<Result> generateCompileResults(List<Diagnostic> compilationErrors)
{
return compilationErrors.stream().map(e ->
{
Result result = new Result();
// Pattern pattern = Pattern.compile(String.format("^.*%s(.*\\.java).*$", File.separator));
// Matcher matcher = pattern.matcher(String.valueOf(e.getSource()));
//
// result.name = (matcher.matches() && matcher.group(1) != null) ? matcher.group(1) : String.valueOf(e.getSource());
String sourcePath=String.valueOf(e.getSource());
result.name=sourcePath.substring(sourcePath.lastIndexOf(File.separator)+1, sourcePath.length()-1);
result.state = Result.State.COMPILATIONERROR.ordinal();
result.failureReason = e.getMessage(Locale.ENGLISH);
result.failureType = "Compilation Failed";
result.stacktrace = e.toString();
result.lineNumber = (int) e.getLineNumber();
result.columnNumber = (int) e.getColumnNumber();
result.position = (int) e.getPosition();
return result;
})
.collect(Collectors.toCollection(HashSet::new));
}
public static ResultSummary generateResultSummary(TestExecutionSummary summary, Set<TestIdentifier> successes)
{
LOG.info("JUnit results:");
LOG.info(String.format(
"Number of Tests: %d, Number of fails: %d, Successful tests: %s, Failed tests: %s",
summary.getTestsFoundCount(),
summary.getTestsFailedCount(),
successes.stream()
.map(s -> s.getDisplayName())
.reduce((s1, s2) -> s1 + ":" + s2).orElse("-"),
summary.getFailures().stream()
.map(f -> f.getTestIdentifier().getDisplayName())
.reduce((s1, s2) -> s1 + ":" + s2).orElse("-")
));
ResultSummary resultSummary = new ResultSummary();
resultSummary.results.addAll(successes.stream().map(s ->
{
Result result = new Result();
result.name = s.getDisplayName();
result.state = Result.State.SUCCESS.ordinal();
return result;
})
.collect(Collectors.toCollection(HashSet::new)));
resultSummary.results.addAll(summary.getFailures().stream().map(f ->
{
Result result = new Result();
result.name = f.getTestIdentifier().getDisplayName();
result.state = Result.State.FAILURE.ordinal();
result.failureReason = f.getException().getMessage();
result.failureType = f.getException().getClass().getName();
result.stacktrace = Arrays.stream(f.getException().getStackTrace())
.map(s -> s.toString())
.reduce((s1, s2) -> s1 + "\n" + s2)
.orElse(null);
return result;
})
.collect(Collectors.toCollection(HashSet::new)));
return resultSummary;
}
public static List<File> getAllJavaFilesInFolder(File path)
{
// check if provided path is a directory, otherwise throw a IllegalArgumentException
if (!path.isDirectory())
{
String error = path.getAbsolutePath() + " is not a path";
LOG.severe(error);
throw new IllegalArgumentException(error);
}
List<File> files = new LinkedList<>();
// recursively check for java files
Stream.of(Objects.requireNonNull(path.listFiles()))
.forEach(file ->
{
// if directory, make recursion
if (file.isDirectory())
{
files.addAll(getAllJavaFilesInFolder(file));
}
// if java file add to list
else if (file.getAbsolutePath().endsWith(".java"))
{
files.add(file);
}
});
return files;
}
public static ResultSummary runTests() throws MalformedURLException
{
classPathItems = buildClassPathItems(true);
LOG.info("saving original class loader");
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
// get custom one
ClassLoader customClassLoader = createCustomClassLoader();
TestExecutionSummary summary;
Set<TestIdentifier> successes;
try
{
LOG.info("changing classloader");
Thread.currentThread().setContextClassLoader(customClassLoader);
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClasspathRoots(Collections.singleton(Paths.get(classFolder).toAbsolutePath())))
.build();
MySummaryGeneratingListener listener = new MySummaryGeneratingListener();
Launcher launcher = LauncherFactory.create();
launcher.registerTestExecutionListeners(listener);
LOG.info("discovering UnitTests...");
TestPlan plan = launcher.discover(request);
for (TestIdentifier root : plan.getRoots())
{
for (TestIdentifier test : plan.getChildren(root))
{
LOG.info(String.format("Testclass identified: %s", test.getDisplayName()));
}
}
LOG.info("launching tests");
launcher.execute(plan);
LOG.info("catching test results");
summary = listener.getSummary();
successes = listener.getSuccessfulTestidentifiers();
} finally
{
LOG.info("restore original classloader");
Thread.currentThread().setContextClassLoader(originalClassLoader);
}
LOG.info("generate result summary from junit");
return generateResultSummary(summary, successes);
}
public static void writeResult(ResultSummary resultSummary) throws IOException
{
Path fileName = Paths.get(resultFolder, "result.json");
LOG.info(String.format("serializing result as json into %s", fileName.toAbsolutePath().toString()));
ObjectMapper objectMapper = new ObjectMapper();
objectMapper
.writerWithDefaultPrettyPrinter()
.writeValue(fileName.toFile(), resultSummary);
}
}
package de.hftstuttgart.dta.model;
public class Result
{
public String name;
public int state;
public String failureType;
public String failureReason;
public String stacktrace;
// only for compilation Errors
public int columnNumber;
public int lineNumber;
public int position;
public static enum State
{
UNKNOWN,
SUCCESS,
FAILURE,
COMPILATIONERROR,
}
}
package de.hftstuttgart.dta.model;
import java.util.HashSet;
import java.util.Set;
public class ResultSummary
{
public long timestamp = System.currentTimeMillis() / 1000;
public String globalStacktrace = null;
public Set<Result> results = new HashSet<>();
}
package de.hftstuttgart.dta.util;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import java.util.HashSet;
import java.util.Set;
public class MySummaryGeneratingListener extends org.junit.platform.launcher.listeners.SummaryGeneratingListener
{
protected Set<TestIdentifier> successful = new HashSet<>();
@Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult)
{
super.executionFinished(testIdentifier, testExecutionResult);
if (testExecutionResult.getStatus().equals(TestExecutionResult.Status.SUCCESSFUL)
&& testIdentifier.getType().equals(TestDescriptor.Type.TEST))
{
successful.add(testIdentifier);
}
}
public Set<TestIdentifier> getSuccessfulTestidentifiers()
{
return successful;
}
}
package de.hftstuttgart.dta.util.logging;
import java.util.Date;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
public class CustomFormatter extends SimpleFormatter
{
final String format = "[%1$tFT%1$tT%1$tz] [%2$-7s] [%3$s.%4$s] %5$s%n";
@Override
public synchronized String format(LogRecord lr) {
return String.format(format,
new Date(lr.getMillis()),
lr.getLevel(),
lr.getSourceClassName(),
lr.getSourceMethodName(),
lr.getMessage()
);
}
}
handlers=java.util.logging.ConsoleHandler
.level = INFO
java.util.logging.ConsoleHandler.level=INFO
java.util.logging.ConsoleHandler.formatter=de.hftstuttgart.dtt.util.logging.CustomFormatter
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment