package de.hftstuttgart.dtt; import de.hftstuttgart.dtt.model.Result; import de.hftstuttgart.dtt.model.ResultSummary; import de.hftstuttgart.dtt.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 sourceFiles = new HashSet<>(); for (String folder : sourceFolders) { sourceFiles.addAll(getAllJavaFilesInFolder(Paths.get(folder).toFile())); } // call compilation and generate Results for failed compiles Set 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 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 buildClassPath(String... paths) { Set 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 compile(Set files, File outputDir) { classPathItems = buildClassPathItems(false); LOG.info("compilation started"); List 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 fileObjects = fileManager.getJavaFileObjects(files.toArray(new File[0])); if (!outputDir.exists()) { outputDir.mkdir(); } // Set the compiler option for a specific output path List options = new ArrayList<>(); options.add("-d"); // output dir options.add(outputDir.getAbsolutePath()); options.add("-cp"); // custom classpath String cp = buildClassPath(classPathItems).stream() .map(f -> f.getPath()) .reduce((s1, s2) -> s1 + ":" + 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 generateCompileResults(List 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()); 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 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 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 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 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); } }