Skip to content

Commit

Permalink
Introduce a Unique ID tracking TestExecutionListener
Browse files Browse the repository at this point in the history
This commit introduces UniqueIdTrackingListener which is a
TestExecutionListener that tracks the unique IDs of all tests that were
executed during the TestPlan and generates a file containing the unique
IDs.

The generated file can be used to rerun those tests -- for example, in
order to run the same set of tests executed on the JVM subsequently
within a GraalVM native image.

See the Javadoc for UniqueIdTrackingListener for configuration options
and details.

Closes junit-team#2619
  • Loading branch information
sbrannen authored and runningcode committed Feb 15, 2023
1 parent 0f27a18 commit bc52510
Show file tree
Hide file tree
Showing 13 changed files with 604 additions and 2 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ subprojects {
}

kotlin {
targetExclude("**/src/test/resources/**")
ktlint(libs.versions.ktlint.get())
licenseHeaderFile(headerFile)
trimTrailingWhitespace()
Expand Down
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ endif::[]
:SummaryGeneratingListener: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/listeners/SummaryGeneratingListener.html[SummaryGeneratingListener]
:TestExecutionListener: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/TestExecutionListener.html[TestExecutionListener]
:TestPlan: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/TestPlan.html[TestPlan]
:UniqueIdTrackingListener: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/listeners/UniqueIdTrackingListener.html[UniqueIdTrackingListener]
// Platform Reporting
:LegacyXmlReportGeneratingListener: {javadoc-root}/org.junit.platform.reporting/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListener.html[LegacyXmlReportGeneratingListener]
// Platform Runner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ on GitHub.

==== New Features and Improvements

* ❓
* New `UniqueIdTrackingListener` which is a `TestExecutionListener` that tracks the unique
IDs of all tests that were executed during the `TestPlan` and generates a file
containing the unique IDs. The generated file can be used to rerun those tests -- for
example, in order to run the same set of tests executed on the JVM subsequently within a
GraalVM native image. See the Javadoc for `{UniqueIdTrackingListener}` for details.


[[release-notes-5.8.0-M2-junit-jupiter]]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* Copyright 2015-2021 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.launcher.listeners;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import org.apiguardian.api.API;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.util.StringUtils;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;

/**
* {@code UniqueIdTrackingListener} is a {@link TestExecutionListener} that tracks
* the {@linkplain TestIdentifier#getUniqueId() unique IDs} of all
* {@linkplain TestIdentifier#isTest() tests} that were
* {@linkplain #executionFinished executed} during the execution of the
* {@link TestPlan} and generates a file containing the unique IDs once execution
* of the {@code TestPlan} has {@linkplain #testPlanExecutionFinished(TestPlan)
* finished}.
*
* <p>Tests are tracked regardless of their {@link TestExecutionResult}, and the
* unique IDs are written to the output file, one ID per line, encoding using UTF-8.
*
* <p>The output file can be used to execute the same set of tests again without
* having to query the user configuration for the test plan and without having to
* perform test discovery again. This can be useful for test environments such as
* within a native image &mdash; for example, a GraalVM native image &mdash; in
* order to rerun the exact same tests from a standard JVM test run within a
* native image.
*
* <h3>Configuration and Defaults</h3>
*
* <p>The {@code OUTPUT_DIR} is the directory in which this listener generates
* the output file (the {@code OUTPUT_FILE}). The exact path of the generated file
* is {@code OUTPUT_DIR}/{@code OUTPUT_FILE}.
*
* <p>The name of the {@code OUTPUT_FILE} defaults to {@link #DEFAULT_FILE_NAME},
* but a custom file name can be configured via the {@link #OUTPUT_FILE_PROPERTY_NAME}
* JVM system property.
*
* <p>The {@code OUTPUT_DIR} can be set to a custom directory via the
* {@link #OUTPUT_DIR_PROPERTY_NAME} JVM system property. Otherwise the following
* algorithm is used to select a default output directory.
*
* <ul>
* <li>If the current working directory of the Java process contains a file named
* {@code pom.xml}, the output directory will be {@code ./target}, following the
* conventions of Maven.</li>
* <li>If the current working directory of the Java process contains a file with
* the extension {@code .gradle} or {@code .gradle.kts}, the output directory
* will be {@code ./build}, following the conventions of Gradle.</li>
* <li>Otherwise, the current working directory of the Java process will be used
* as the output directory.</li>
* </ul>
*
* <p>For example, in a project using Gradle as the build tool, the file generated
* by this listener would be {@code ./build/junit-platform-unique-test-ids.txt}
* by default.
*
* @since 1.8
*/
@API(status = EXPERIMENTAL, since = "1.8")
public class UniqueIdTrackingListener implements TestExecutionListener {

/**
* Property name used to enable the {@code UniqueIdTrackingListener}: {@value}
*
* <p>The {@code UniqueIdTrackingListener} is registered automatically via
* Java's {@link java.util.ServiceLoader} mechanism but disabled by default.
*
* <p>Set the value of this property to {@code true} to enable this listener.
*/
public static final String LISTENER_ENABLED_PROPERTY_NAME = "junit.platform.listeners.uid.tracking.enabled";

/**
* Property name used to set the path to the output directory for the file
* generated by the {@code UniqueIdTrackingListener}: {@value}
*
* <p>For details on the default output directory, see the
* {@linkplain UniqueIdTrackingListener class-level Javadoc}.
*/
public static final String OUTPUT_DIR_PROPERTY_NAME = "junit.platform.listeners.uid.tracking.output.dir";

/**
* Property name used to set the name of the file generated by the
* {@code UniqueIdTrackingListener}: {@value}
*
* <p>Defaults to {@link #DEFAULT_FILE_NAME}.
*/
public static final String OUTPUT_FILE_PROPERTY_NAME = "junit.platform.listeners.uid.tracking.output.file";

/**
* The default name of the file generated by the {@code UniqueIdTrackingListener}: {@value}
*
* @see #OUTPUT_FILE_PROPERTY_NAME
*/
public static final String DEFAULT_FILE_NAME = "junit-platform-unique-test-ids.txt";

private final Logger logger = LoggerFactory.getLogger(UniqueIdTrackingListener.class);

private final List<String> uniqueIds = new ArrayList<>();

private final boolean enabled;

public UniqueIdTrackingListener() {
this.enabled = Boolean.getBoolean(LISTENER_ENABLED_PROPERTY_NAME);
}

@Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
if (this.enabled && testIdentifier.isTest()) {
this.uniqueIds.add(testIdentifier.getUniqueId());
}
}

@Override
public void testPlanExecutionFinished(TestPlan testPlan) {
if (this.enabled) {
Path outputFile;
try {
outputFile = getOutputFile();
}
catch (IOException ex) {
logger.error(ex, () -> "Failed to create output file");
// Abort since we cannot generate the file.
return;
}

logger.debug(() -> "Writing unique IDs to output file " + outputFile.toAbsolutePath());
try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8))) {
this.uniqueIds.forEach(writer::println);
writer.flush();
}
catch (IOException ex) {
logger.error(ex, () -> "Failed to write unique IDs to output file " + outputFile.toAbsolutePath());
}
}
}

private Path getOutputFile() throws IOException {
String filename = System.getProperty(OUTPUT_FILE_PROPERTY_NAME, DEFAULT_FILE_NAME);
Path outputFile = getOutputDir().resolve(filename);

if (Files.exists(outputFile)) {
Files.delete(outputFile);
}

Files.createFile(outputFile);

return outputFile;
}

Path getOutputDir() throws IOException {
Path cwd = currentWorkingDir();
Path outputDir;

String customDir = System.getProperty(OUTPUT_DIR_PROPERTY_NAME);
if (StringUtils.isNotBlank(customDir)) {
outputDir = cwd.resolve(customDir);
}
else if (Files.exists(cwd.resolve("pom.xml"))) {
outputDir = cwd.resolve("target");
}
else if (containsFilesWithExtensions(cwd, ".gradle", ".gradle.kts")) {
outputDir = cwd.resolve("build");
}
else {
outputDir = cwd;
}

if (!Files.exists(outputDir)) {
Files.createDirectories(outputDir);
}

return outputDir;
}

/**
* Get the current working directory.
* <p>Package private for testing purposes.
*/
Path currentWorkingDir() {
return Paths.get(".");
}

/**
* Determine if the supplied directory contains files with any of the
* supplied extensions.
*/
private boolean containsFilesWithExtensions(Path dir, String... extensions) throws IOException {
return Files.find(dir, 1, //
(path, basicFileAttributes) -> {
for (String extension : extensions) {
if (path.getFileName().toString().endsWith(extension)) {
return true;
}
}
return false;
}).findFirst().isPresent();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.junit.platform.launcher.listeners.UniqueIdTrackingListener
Loading

0 comments on commit bc52510

Please sign in to comment.