diff --git a/src/main/java/hudson/tasks/junit/JUnitParser.java b/src/main/java/hudson/tasks/junit/JUnitParser.java index a604a0a72..e1133d886 100644 --- a/src/main/java/hudson/tasks/junit/JUnitParser.java +++ b/src/main/java/hudson/tasks/junit/JUnitParser.java @@ -37,6 +37,10 @@ import io.jenkins.plugins.junit.storage.JunitTestResultStorage; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.MasterToSlaveFileCallable; @@ -58,11 +62,12 @@ public class JUnitParser extends TestResultParser { private final boolean keepTestNames; private final boolean skipOldReports; + private final boolean sortTestResultsByTimestamp; /** Generally unused, but present for extension compatibility. */ @Deprecated public JUnitParser() { - this(false, false, false, false); + this(StdioRetention.DEFAULT, false, false, false, false, false); } /** @@ -71,7 +76,7 @@ public JUnitParser() { */ @Deprecated public JUnitParser(boolean keepLongStdio) { - this(keepLongStdio, false, false, false); + this(StdioRetention.fromKeepLongStdio(keepLongStdio), false, false, false, false); } /** @@ -81,32 +86,50 @@ public JUnitParser(boolean keepLongStdio) { */ @Deprecated public JUnitParser(boolean keepLongStdio, boolean allowEmptyResults) { - this(StdioRetention.fromKeepLongStdio(keepLongStdio), false, allowEmptyResults, false, false); + this(StdioRetention.fromKeepLongStdio(keepLongStdio), false, allowEmptyResults, false, false, false); } @Deprecated public JUnitParser( boolean keepLongStdio, boolean keepProperties, boolean allowEmptyResults, boolean skipOldReports) { - this(StdioRetention.fromKeepLongStdio(keepLongStdio), keepProperties, allowEmptyResults, skipOldReports, false); + this( + StdioRetention.fromKeepLongStdio(keepLongStdio), + keepProperties, + allowEmptyResults, + skipOldReports, + false, + false); } @Deprecated public JUnitParser( StdioRetention stdioRetention, boolean keepProperties, boolean allowEmptyResults, boolean skipOldReports) { - this(stdioRetention, keepProperties, allowEmptyResults, skipOldReports, false); + this(stdioRetention, keepProperties, allowEmptyResults, skipOldReports, false, false); } + @Deprecated public JUnitParser( StdioRetention stdioRetention, boolean keepProperties, boolean allowEmptyResults, boolean skipOldReports, boolean keepTestNames) { + this(stdioRetention, keepProperties, allowEmptyResults, skipOldReports, keepTestNames, false); + } + // New Constructor with the additional parameter + public JUnitParser( + StdioRetention stdioRetention, + boolean keepProperties, + boolean allowEmptyResults, + boolean skipOldReports, + boolean keepTestNames, + boolean sortTestResultsByTimestamp) { this.stdioRetention = stdioRetention; this.keepProperties = keepProperties; this.allowEmptyResults = allowEmptyResults; - this.keepTestNames = keepTestNames; this.skipOldReports = skipOldReports; + this.keepTestNames = keepTestNames; + this.sortTestResultsByTimestamp = sortTestResultsByTimestamp; } @Override @@ -152,7 +175,8 @@ public TestResult parseResult( keepTestNames, pipelineTestDetails, listener, - skipOldReports)); + skipOldReports, + sortTestResultsByTimestamp)); } public TestResultSummary summarizeResult( @@ -174,7 +198,8 @@ public TestResultSummary summarizeResult( pipelineTestDetails, listener, storage.createRemotePublisher(build), - skipOldReports)); + skipOldReports, + sortTestResultsByTimestamp)); } private abstract static class ParseResultCallable extends MasterToSlaveFileCallable { @@ -194,6 +219,7 @@ private abstract static class ParseResultCallable extends MasterToSlaveFileCa private final TaskListener listener; private boolean skipOldReports; + private final boolean sortTestResultsByTimestamp; private ParseResultCallable( String testResults, @@ -204,7 +230,8 @@ private ParseResultCallable( boolean keepTestNames, PipelineTestDetails pipelineTestDetails, TaskListener listener, - boolean skipOldReports) { + boolean skipOldReports, + boolean sortTestResultsByTimestamp) { this.buildStartTimeInMillis = build.getStartTimeInMillis(); this.buildTimeInMillis = build.getTimeInMillis(); this.testResults = testResults; @@ -216,6 +243,7 @@ private ParseResultCallable( this.pipelineTestDetails = pipelineTestDetails; this.listener = listener; this.skipOldReports = skipOldReports; + this.sortTestResultsByTimestamp = sortTestResultsByTimestamp; } @Override @@ -226,7 +254,35 @@ public T invoke(File ws, VirtualChannel channel) throws IOException { TestResult result; String[] files = ds.getIncludedFiles(); if (files.length > 0) { - // not sure we can rely seriously on those timestamp so let's take the smaller one... + // New sorting logic starts here + List fileList = new ArrayList<>(); + for (String fileName : files) { + fileList.add(new File(ds.getBasedir(), fileName)); + } + + if (sortTestResultsByTimestamp) { + Collections.sort(fileList, Comparator.comparingLong(File::lastModified)); + } else { + Collections.sort(fileList, Comparator.comparing(File::getName)); + } + + // Convert back to String array with paths relative to the base directory + String[] sortedFiles = new String[fileList.size()]; + String baseDirPath = ds.getBasedir().getAbsolutePath(); + for (int i = 0; i < fileList.size(); i++) { + String absolutePath = fileList.get(i).getAbsolutePath(); + if (absolutePath.startsWith(baseDirPath)) { + sortedFiles[i] = absolutePath.substring(baseDirPath.length() + 1); + } else { + sortedFiles[i] = absolutePath; + } + } + + // Update the DirectoryScanner with the sorted files + ds.setIncludes(sortedFiles); + + // Continue with existing processing logic + // Not sure we can rely seriously on those timestamps so let's take the smaller one... long filesTimestamp = Math.min(buildStartTimeInMillis, buildTimeInMillis); // previous mode buildStartTimeInMillis + (nowSlave - nowMaster); if (LOGGER.isLoggable(Level.FINE)) { @@ -270,7 +326,8 @@ private static final class DirectParseResultCallable extends ParseResultCallable boolean keepTestNames, PipelineTestDetails pipelineTestDetails, TaskListener listener, - boolean skipOldReports) { + boolean skipOldReports, + boolean sortTestResultsByTimestamp) { super( testResults, build, @@ -280,7 +337,8 @@ private static final class DirectParseResultCallable extends ParseResultCallable keepTestNames, pipelineTestDetails, listener, - skipOldReports); + skipOldReports, + sortTestResultsByTimestamp); } @Override @@ -303,7 +361,8 @@ private static final class StorageParseResultCallable extends ParseResultCallabl PipelineTestDetails pipelineTestDetails, TaskListener listener, JunitTestResultStorage.RemotePublisher publisher, - boolean skipOldReports) { + boolean skipOldReports, + boolean sortTestResultsByTimestamp) { super( testResults, build, @@ -313,7 +372,8 @@ private static final class StorageParseResultCallable extends ParseResultCallabl keepTestNames, pipelineTestDetails, listener, - skipOldReports); + skipOldReports, + sortTestResultsByTimestamp); this.publisher = publisher; } diff --git a/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java b/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java index 2051c9e9a..f6e3fc8c8 100644 --- a/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java +++ b/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java @@ -173,7 +173,8 @@ private static TestResult parse( task.isKeepProperties(), task.isAllowEmptyResults(), task.isSkipOldReports(), - task.isKeepTestNames()) + task.isKeepTestNames(), + task.isSortTestResultsByTimestamp()) .parseResult(expandedTestResults, run, pipelineTestDetails, workspace, launcher, listener); } @@ -189,7 +190,7 @@ protected TestResult parse( } @Override - public void perform(Run build, FilePath workspace, Launcher launcher, TaskListener listener) + public void perform(Run build, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException { if (parseAndSummarize(this, null, build, workspace, launcher, listener).getFailCount() > 0 && !skipMarkingBuildUnstable) { @@ -288,7 +289,8 @@ public static TestResultSummary parseAndSummarize( task.isKeepProperties(), task.isAllowEmptyResults(), task.isSkipOldReports(), - task.isKeepTestNames()) + task.isKeepTestNames(), + task.isSortTestResultsByTimestamp()) .summarizeResult(testResults, build, pipelineTestDetails, workspace, launcher, listener, storage); } @@ -498,6 +500,11 @@ public boolean isSkipPublishingChecks() { return skipPublishingChecks; } + @DataBoundSetter + public final void setAllowEmptyResults(boolean allowEmptyResults) { + this.allowEmptyResults = allowEmptyResults; + } + @DataBoundSetter public void setSkipPublishingChecks(boolean skipPublishingChecks) { this.skipPublishingChecks = skipPublishingChecks; @@ -513,11 +520,6 @@ public void setChecksName(String checksName) { this.checksName = checksName; } - @DataBoundSetter - public final void setAllowEmptyResults(boolean allowEmptyResults) { - this.allowEmptyResults = allowEmptyResults; - } - public boolean isSkipMarkingBuildUnstable() { return skipMarkingBuildUnstable; } @@ -539,6 +541,20 @@ public void setSkipOldReports(boolean skipOldReports) { private static final long serialVersionUID = 1L; + @Override + public boolean isSortTestResultsByTimestamp() { + // Return the appropriate value or a default (e.g., false) + return sortTestResultsByTimestamp; + } + + // Add the field and setter if needed + private boolean sortTestResultsByTimestamp; + + @DataBoundSetter + public void setSortTestResultsByTimestamp(boolean sortTestResultsByTimestamp) { + this.sortTestResultsByTimestamp = sortTestResultsByTimestamp; + } + @Extension public static class DescriptorImpl extends BuildStepDescriptor { @Override diff --git a/src/main/java/hudson/tasks/junit/JUnitTask.java b/src/main/java/hudson/tasks/junit/JUnitTask.java index 209db156c..b11d6c687 100644 --- a/src/main/java/hudson/tasks/junit/JUnitTask.java +++ b/src/main/java/hudson/tasks/junit/JUnitTask.java @@ -29,4 +29,6 @@ default StdioRetention getParsedStdioRetention() { String getChecksName(); boolean isSkipOldReports(); + + boolean isSortTestResultsByTimestamp(); } diff --git a/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java b/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java index c721427b5..3cf0916ba 100644 --- a/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java +++ b/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java @@ -55,6 +55,8 @@ public class JUnitResultsStep extends Step implements JUnitTask { private Double healthScaleFactor; + private boolean sortTestResultsByTimestamp; + /** * If true, don't throw exception on missing test results or no files found. */ @@ -94,6 +96,16 @@ public final void setHealthScaleFactor(double healthScaleFactor) { this.healthScaleFactor = Math.max(0.0, healthScaleFactor); } + @Override + public boolean isSortTestResultsByTimestamp() { + return sortTestResultsByTimestamp; + } + + @DataBoundSetter + public void setSortTestResultsByTimestamp(boolean sortTestResultsByTimestamp) { + this.sortTestResultsByTimestamp = sortTestResultsByTimestamp; + } + @NonNull @Override public List getTestDataPublishers() { diff --git a/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly b/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly index 8a5024c18..d4bbb4ada 100644 --- a/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly +++ b/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly @@ -65,4 +65,7 @@ THE SOFTWARE. + + + diff --git a/src/test/java/hudson/tasks/junit/JUnitResultArchiverTest.java b/src/test/java/hudson/tasks/junit/JUnitResultArchiverTest.java index 167f1b8a8..37634e072 100644 --- a/src/test/java/hudson/tasks/junit/JUnitResultArchiverTest.java +++ b/src/test/java/hudson/tasks/junit/JUnitResultArchiverTest.java @@ -58,9 +58,11 @@ import hudson.tasks.test.helper.WebClientFactory; import hudson.util.HttpResponses; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; @@ -482,16 +484,24 @@ public void testXxe() throws Exception { String oobInUserContentLink = j.getURL() + "userContent/oob.xml"; String triggerLink = j.getURL() + "triggerMe"; - String xxeFile = this.getClass().getResource("testXxe-xxe.xml").getFile(); - String xxeFileContent = FileUtils.readFileToString(new File(xxeFile), StandardCharsets.UTF_8); + URL xxeResourceUrl = this.getClass().getResource("testXxe-xxe.xml"); + if (xxeResourceUrl == null) { + throw new FileNotFoundException("Resource 'testXxe-xxe.xml' not found"); + } + File xxeFile = new File(xxeResourceUrl.toURI()); + String xxeFileContent = FileUtils.readFileToString(xxeFile, StandardCharsets.UTF_8); String adaptedXxeFileContent = xxeFileContent.replace("$OOB_LINK$", oobInUserContentLink); - String oobFile = this.getClass().getResource("testXxe-oob.xml").getFile(); - String oobFileContent = FileUtils.readFileToString(new File(oobFile), StandardCharsets.UTF_8); + URL oobResourceUrl = this.getClass().getResource("testXxe-oob.xml"); + if (oobResourceUrl == null) { + throw new FileNotFoundException("Resource 'testXxe-oob.xml' not found"); + } + File oobFile = new File(oobResourceUrl.toURI()); + String oobFileContent = FileUtils.readFileToString(oobFile, StandardCharsets.UTF_8); String adaptedOobFileContent = oobFileContent.replace("$TARGET_URL$", triggerLink); File userContentDir = new File(j.jenkins.getRootDir(), "userContent"); - FileUtils.writeStringToFile(new File(userContentDir, "oob.xml"), adaptedOobFileContent); + FileUtils.writeStringToFile(new File(userContentDir, "oob.xml"), adaptedOobFileContent, StandardCharsets.UTF_8); FreeStyleProject project = j.createFreeStyleProject(); DownloadBuilder builder = new DownloadBuilder();