From 4d5a82013325d7633191f3d3db68a0b2708fd780 Mon Sep 17 00:00:00 2001
From: Gero Lueckemeyer <gero.lueckemeyer@hft-stuttgart.de>
Date: Thu, 19 Oct 2023 18:44:43 +0200
Subject: [PATCH] initial commit of jdk17 junit5 jupiter example test runner

---
 CHANGELOG.md                                  |  33 ++
 Dockerfile                                    |  25 ++
 Jenkinsfile                                   |  74 ++++
 pom.xml                                       |  88 +++++
 .../java/de/hftstuttgart/dta/Testrunner.java  | 374 ++++++++++++++++++
 .../de/hftstuttgart/dta/model/Result.java     |  24 ++
 .../hftstuttgart/dta/model/ResultSummary.java |  11 +
 .../dta/util/MySummaryGeneratingListener.java |  31 ++
 .../dta/util/logging/CustomFormatter.java     |  20 +
 src/main/resources/logging.properties         |   4 +
 10 files changed, 684 insertions(+)
 create mode 100644 CHANGELOG.md
 create mode 100644 Dockerfile
 create mode 100644 Jenkinsfile
 create mode 100644 pom.xml
 create mode 100644 src/main/java/de/hftstuttgart/dta/Testrunner.java
 create mode 100644 src/main/java/de/hftstuttgart/dta/model/Result.java
 create mode 100644 src/main/java/de/hftstuttgart/dta/model/ResultSummary.java
 create mode 100644 src/main/java/de/hftstuttgart/dta/util/MySummaryGeneratingListener.java
 create mode 100644 src/main/java/de/hftstuttgart/dta/util/logging/CustomFormatter.java
 create mode 100644 src/main/resources/logging.properties

diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4cc1c22
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,33 @@
+# 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)
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..7733b95
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,25 @@
+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
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..9f7c36d
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,74 @@
+
+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
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..bbfb737
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,88 @@
+<?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
diff --git a/src/main/java/de/hftstuttgart/dta/Testrunner.java b/src/main/java/de/hftstuttgart/dta/Testrunner.java
new file mode 100644
index 0000000..d7503d7
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/dta/Testrunner.java
@@ -0,0 +1,374 @@
+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);
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/dta/model/Result.java b/src/main/java/de/hftstuttgart/dta/model/Result.java
new file mode 100644
index 0000000..8432c66
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/dta/model/Result.java
@@ -0,0 +1,24 @@
+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,
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/dta/model/ResultSummary.java b/src/main/java/de/hftstuttgart/dta/model/ResultSummary.java
new file mode 100644
index 0000000..295af95
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/dta/model/ResultSummary.java
@@ -0,0 +1,11 @@
+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<>();
+}
diff --git a/src/main/java/de/hftstuttgart/dta/util/MySummaryGeneratingListener.java b/src/main/java/de/hftstuttgart/dta/util/MySummaryGeneratingListener.java
new file mode 100644
index 0000000..55788ab
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/dta/util/MySummaryGeneratingListener.java
@@ -0,0 +1,31 @@
+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;
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/dta/util/logging/CustomFormatter.java b/src/main/java/de/hftstuttgart/dta/util/logging/CustomFormatter.java
new file mode 100644
index 0000000..488b0be
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/dta/util/logging/CustomFormatter.java
@@ -0,0 +1,20 @@
+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()
+        );
+    }
+}
diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties
new file mode 100644
index 0000000..ba6bc52
--- /dev/null
+++ b/src/main/resources/logging.properties
@@ -0,0 +1,4 @@
+handlers=java.util.logging.ConsoleHandler
+.level = INFO
+java.util.logging.ConsoleHandler.level=INFO
+java.util.logging.ConsoleHandler.formatter=de.hftstuttgart.dtt.util.logging.CustomFormatter
-- 
GitLab