From 3dfeb90e2d6f28e6fbad7cb13771cfe3ab6f6e78 Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Thu, 12 May 2022 10:53:31 +0200 Subject: [PATCH 01/80] Allow disabling test name mangling (keepTestNames). --- .../java/hudson/tasks/junit/CaseResult.java | 11 ++++-- .../java/hudson/tasks/junit/JUnitParser.java | 34 +++++++++++++------ .../tasks/junit/JUnitResultArchiver.java | 30 +++++++++++++--- .../java/hudson/tasks/junit/JUnitTask.java | 2 ++ .../java/hudson/tasks/junit/SuiteResult.java | 16 ++++----- .../java/hudson/tasks/junit/TestResult.java | 20 ++++++----- .../junit/pipeline/JUnitResultsStep.java | 23 +++++++++++++ .../junit/JUnitResultArchiver/config.jelly | 3 ++ 8 files changed, 105 insertions(+), 34 deletions(-) diff --git a/src/main/java/hudson/tasks/junit/CaseResult.java b/src/main/java/hudson/tasks/junit/CaseResult.java index ffa60f0e2..0c5b9f4b4 100644 --- a/src/main/java/hudson/tasks/junit/CaseResult.java +++ b/src/main/java/hudson/tasks/junit/CaseResult.java @@ -75,6 +75,7 @@ public class CaseResult extends TestResult implements Comparable { private final String testName; private transient String safeName; private final boolean skipped; + private final boolean keepTestNames; private final String skippedMessage; private final String errorStackTrace; private final String errorDetails; @@ -129,6 +130,7 @@ public CaseResult(SuiteResult parent, String testName, String errorStackTrace, S this.duration = 0.0f; this.skipped = false; this.skippedMessage = null; + this.keepTestNames = false; } @Restricted(Beta.class) @@ -141,7 +143,8 @@ public CaseResult( float duration, String stdout, String stderr, - String stacktrace + String stacktrace, + boolean keepTestNames ) { this.className = className; this.testName = testName; @@ -154,9 +157,10 @@ public CaseResult( this.skipped = skippedMessage != null; this.skippedMessage = skippedMessage; + this.keepTestNames = keepTestNames; } - CaseResult(SuiteResult parent, Element testCase, String testClassName, boolean keepLongStdio) { + CaseResult(SuiteResult parent, Element testCase, String testClassName, boolean keepLongStdio, boolean keepTestNames) { // schema for JUnit report XML format is not available in Ant, // so I don't know for sure what means what. // reports in http://www.nabble.com/difference-in-junit-publisher-and-ant-junitreport-tf4308604.html#a12265700 @@ -191,6 +195,7 @@ public CaseResult( Collection _this = Collections.singleton(this); stdout = possiblyTrimStdio(_this, keepLongStdio, testCase.elementText("system-out")); stderr = possiblyTrimStdio(_this, keepLongStdio, testCase.elementText("system-err")); + this.keepTestNames = keepTestNames; } static String possiblyTrimStdio(Collection results, boolean keepLongStdio, String stdio) { // HUDSON-6516 @@ -311,7 +316,7 @@ public String getDisplayName() { private String getNameWithEnclosingBlocks(String rawName) { // Only prepend the enclosing flow node names if there are any and the run this is in has multiple blocks directly containing // test results. - if (!getEnclosingFlowNodeNames().isEmpty()) { + if (!keepTestNames && !getEnclosingFlowNodeNames().isEmpty()) { Run r = getRun(); if (r != null) { TestResultAction action = r.getAction(TestResultAction.class); diff --git a/src/main/java/hudson/tasks/junit/JUnitParser.java b/src/main/java/hudson/tasks/junit/JUnitParser.java index 6fe25d3b7..a1121fdee 100644 --- a/src/main/java/hudson/tasks/junit/JUnitParser.java +++ b/src/main/java/hudson/tasks/junit/JUnitParser.java @@ -54,13 +54,14 @@ public class JUnitParser extends TestResultParser { private final boolean keepLongStdio; private final boolean allowEmptyResults; + private final boolean keepTestNames; private final boolean skipOldReports; /** Generally unused, but present for extension compatibility. */ @Deprecated public JUnitParser() { - this(false, false); + this(false, false, false); } /** @@ -70,6 +71,7 @@ public JUnitParser() { @Deprecated public JUnitParser(boolean keepLongStdio) { this(keepLongStdio , false, false); + this.keepTestNames = false; } /** @@ -85,9 +87,21 @@ public JUnitParser(boolean keepLongStdio, boolean allowEmptyResults) { public JUnitParser(boolean keepLongStdio, boolean allowEmptyResults, boolean skipOldReports) { this.keepLongStdio = keepLongStdio; this.allowEmptyResults = allowEmptyResults; + this.keepTestNames = false; this.skipOldReports = skipOldReports; } + /** + * @param keepLongStdio if true, retain a suite's complete stdout/stderr even if this is huge and the suite passed + * @param allowEmptyResults if true, empty results are allowed + * @since 1.10 + */ + public JUnitParser(boolean keepLongStdio, boolean allowEmptyResults, boolean keepTestNames) { + this.keepLongStdio = keepLongStdio; + this.allowEmptyResults = allowEmptyResults; + this.keepTestNames = keepTestNames; + } + @Override public String getDisplayName() { return Messages.JUnitParser_DisplayName(); @@ -115,15 +129,13 @@ public TestResult parseResult(String testResultLocations, Run build, FilePa public TestResult parseResult(String testResultLocations, Run build, PipelineTestDetails pipelineTestDetails, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException { - return workspace.act(new DirectParseResultCallable(testResultLocations, build, keepLongStdio, allowEmptyResults, - pipelineTestDetails, listener, skipOldReports)); + return workspace.act(new DirectParseResultCallable(testResultLocations, build, keepLongStdio, allowEmptyResults, keepTestNames, pipelineTestDetails, listener, skipOldReports)); } public TestResultSummary summarizeResult(String testResultLocations, Run build, PipelineTestDetails pipelineTestDetails, FilePath workspace, Launcher launcher, TaskListener listener, JunitTestResultStorage storage) throws InterruptedException, IOException { - return workspace.act(new StorageParseResultCallable(testResultLocations, build, keepLongStdio, allowEmptyResults, - pipelineTestDetails, listener, storage.createRemotePublisher(build), skipOldReports)); + return workspace.act(new StorageParseResultCallable(testResultLocations, build, keepLongStdio, allowEmptyResults, keepTestNames, pipelineTestDetails, listener, storage.createRemotePublisher(build), skipOldReports)); } private static abstract class ParseResultCallable extends MasterToSlaveFileCallable { @@ -136,6 +148,7 @@ private static abstract class ParseResultCallable extends MasterToSlaveFileCa private final String testResults; private final long nowMaster; private final boolean keepLongStdio; + private final boolean keepTestNames; private final boolean allowEmptyResults; private final PipelineTestDetails pipelineTestDetails; private final TaskListener listener; @@ -143,7 +156,7 @@ private static abstract class ParseResultCallable extends MasterToSlaveFileCa private boolean skipOldReports; private ParseResultCallable(String testResults, Run build, - boolean keepLongStdio, boolean allowEmptyResults, + boolean keepLongStdio, boolean allowEmptyResults, boolean keepTestNames, PipelineTestDetails pipelineTestDetails, TaskListener listener, boolean skipOldReports) { this.buildStartTimeInMillis = build.getStartTimeInMillis(); @@ -151,6 +164,7 @@ private ParseResultCallable(String testResults, Run build, this.testResults = testResults; this.nowMaster = System.currentTimeMillis(); this.keepLongStdio = keepLongStdio; + this.keepTestNames = keepTestNames; this.allowEmptyResults = allowEmptyResults; this.pipelineTestDetails = pipelineTestDetails; this.listener = listener; @@ -173,7 +187,7 @@ public T invoke(File ws, VirtualChannel channel) throws IOException { + ",buildTimeInMillis:" + buildTimeInMillis + ",filesTimestamp:" + filesTimestamp + ",nowSlave:" + nowSlave + ",nowMaster:" + nowMaster); } - result = new TestResult(filesTimestamp, ds, keepLongStdio, pipelineTestDetails, skipOldReports); + result = new TestResult(filesTimestamp, ds, keepLongStdio, keepTestNames, pipelineTestDetails, skipOldReports); result.tally(); } else { if (this.allowEmptyResults) { @@ -193,8 +207,7 @@ public T invoke(File ws, VirtualChannel channel) throws IOException { private static final class DirectParseResultCallable extends ParseResultCallable { - DirectParseResultCallable(String testResults, Run build, boolean keepLongStdio, boolean allowEmptyResults, - PipelineTestDetails pipelineTestDetails, TaskListener listener, boolean skipOldReports) { + DirectParseResultCallable(String testResults, Run build, boolean keepLongStdio, boolean allowEmptyResults, boolean keepTestNames, PipelineTestDetails pipelineTestDetails, TaskListener listener, boolean skipOldReports) { super(testResults, build, keepLongStdio, allowEmptyResults, pipelineTestDetails, listener, skipOldReports); } @@ -209,8 +222,7 @@ private static final class StorageParseResultCallable extends ParseResultCallabl private final RemotePublisher publisher; - StorageParseResultCallable(String testResults, Run build, boolean keepLongStdio, boolean allowEmptyResults, - PipelineTestDetails pipelineTestDetails, TaskListener listener, RemotePublisher publisher, boolean skipOldReports) { + StorageParseResultCallable(String testResults, Run build, boolean keepLongStdio, boolean allowEmptyResults, boolean keepTestNames, PipelineTestDetails pipelineTestDetails, TaskListener listener, RemotePublisher publisher, boolean skipOldReports) { super(testResults, build, keepLongStdio, allowEmptyResults, pipelineTestDetails, listener, skipOldReports); this.publisher = publisher; } diff --git a/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java b/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java index a7381d9b9..47e16d3ec 100644 --- a/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java +++ b/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java @@ -85,6 +85,12 @@ public class JUnitResultArchiver extends Recorder implements SimpleBuildStep, JU */ private boolean keepLongStdio; + /** + * If true, retain a suite's complete stdout/stderr even if this is huge and the suite passed. + * @since 1.358 + */ + private boolean keepTestNames; + /** * {@link TestDataPublisher}s configured for this archiver, to process the recorded data. * For compatibility reasons, can be null. @@ -153,7 +159,7 @@ private static TestResult parse(@NonNull JUnitTask task, PipelineTestDetails pip String expandedTestResults, Run run, @NonNull FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { - return new JUnitParser(task.isKeepLongStdio(), task.isAllowEmptyResults(), task.isSkipOldReports()) + return new JUnitParser(task.isKeepLongStdio(), task.isAllowEmptyResults(), task.isKeepTestNames(), task.isSkipOldReports()) .parseResult(expandedTestResults, run, pipelineTestDetails, workspace, launcher, listener); } @@ -252,7 +258,7 @@ public static TestResultSummary parseAndSummarize(@NonNull JUnitTask task, Pipel summary = null; // see below } else { result = new TestResult(storage.load(build.getParent().getFullName(), build.getNumber())); // irrelevant - summary = new JUnitParser(task.isKeepLongStdio(), task.isAllowEmptyResults(), task.isSkipOldReports()) + summary = new JUnitParser(task.isKeepLongStdio(), task.isAllowEmptyResults(), task.isKeepTestNames(), task.isSkipOldReports()) .summarizeResult(testResults, build, pipelineTestDetails, workspace, launcher, listener, storage); } @@ -314,10 +320,10 @@ public static TestResultSummary parseAndSummarize(@NonNull JUnitTask task, Pipel checksName = DEFAULT_CHECKS_NAME; } try { - new JUnitChecksPublisher(build, checksName, result, summary).publishChecks(listener); + new JUnitChecksPublisher(build, checksName, result, summary).publishChecks(listener); } catch (Exception x) { Functions.printStackTrace(x, listener.error("Publishing JUnit checks failed:")); - } + } } return summary; @@ -395,6 +401,22 @@ public boolean isKeepLongStdio() { this.keepLongStdio = keepLongStdio; } + /** + * @return the keepTestNames. + */ + public boolean isKeepTestNames() { + return keepTestNames; + } + + /** + * @param keepTestNames Whether to keep long stdio. + * + * @since 1.2-beta-1 + */ + @DataBoundSetter public final void setKeepTestNames(boolean v) { + this.keepTestNames = v; + } + /** * * @return the allowEmptyResults diff --git a/src/main/java/hudson/tasks/junit/JUnitTask.java b/src/main/java/hudson/tasks/junit/JUnitTask.java index e099066c1..5cbcbcf00 100644 --- a/src/main/java/hudson/tasks/junit/JUnitTask.java +++ b/src/main/java/hudson/tasks/junit/JUnitTask.java @@ -11,6 +11,8 @@ public interface JUnitTask { boolean isKeepLongStdio(); + boolean isKeepTestNames(); + boolean isAllowEmptyResults(); boolean isSkipPublishingChecks(); diff --git a/src/main/java/hudson/tasks/junit/SuiteResult.java b/src/main/java/hudson/tasks/junit/SuiteResult.java index 8fc5d2e37..22a066dae 100644 --- a/src/main/java/hudson/tasks/junit/SuiteResult.java +++ b/src/main/java/hudson/tasks/junit/SuiteResult.java @@ -157,7 +157,7 @@ public static class SuiteResultParserConfigurationContext { * This method returns a collection, as a single XML may have multiple <testsuite> * elements wrapped into the top-level <testsuites>. */ - static List parse(File xmlReport, boolean keepLongStdio, PipelineTestDetails pipelineTestDetails) + static List parse(File xmlReport, boolean keepLongStdio, boolean keepTestNames, PipelineTestDetails pipelineTestDetails) throws DocumentException, IOException, InterruptedException { List r = new ArrayList<>(); @@ -177,7 +177,7 @@ static List parse(File xmlReport, boolean keepLongStdio, PipelineTe Document result = saxReader.read(xmlReportStream); Element root = result.getRootElement(); - parseSuite(xmlReport, keepLongStdio, r, root, pipelineTestDetails); + parseSuite(xmlReport, keepLongStdio, keepTestNames, r, root, pipelineTestDetails); } return r; @@ -192,24 +192,24 @@ private static void setFeatureQuietly(SAXReader reader, String feature, boolean } } - private static void parseSuite(File xmlReport, boolean keepLongStdio, List r, Element root, + private static void parseSuite(File xmlReport, boolean keepLongStdio, boolean keepTestNames, List r, Element root, PipelineTestDetails pipelineTestDetails) throws DocumentException, IOException { // nested test suites List testSuites = root.elements("testsuite"); for (Element suite : testSuites) - parseSuite(xmlReport, keepLongStdio, r, suite, pipelineTestDetails); + parseSuite(xmlReport, keepLongStdio, keepTestNames, r, suite, pipelineTestDetails); // child test cases // FIXME: do this also if no testcases! if (root.element("testcase") != null || root.element("error") != null) - r.add(new SuiteResult(xmlReport, root, keepLongStdio, pipelineTestDetails)); + r.add(new SuiteResult(xmlReport, root, keepLongStdio, keepTestNames, pipelineTestDetails)); } /** * @param xmlReport A JUnit XML report file whose top level element is 'testsuite'. * @param suite The parsed result of {@code xmlReport} */ - private SuiteResult(File xmlReport, Element suite, boolean keepLongStdio, @CheckForNull PipelineTestDetails pipelineTestDetails) + private SuiteResult(File xmlReport, Element suite, boolean keepLongStdio, boolean keepTestNames, @CheckForNull PipelineTestDetails pipelineTestDetails) throws DocumentException, IOException { this.file = xmlReport.getAbsolutePath(); String name = suite.attributeValue("name"); @@ -238,7 +238,7 @@ private SuiteResult(File xmlReport, Element suite, boolean keepLongStdio, @Check Element ex = suite.element("error"); if (ex != null) { // according to junit-noframes.xsl l.229, this happens when the test class failed to load - addCase(new CaseResult(this, suite, "", keepLongStdio)); + addCase(new CaseResult(this, suite, "", keepLongStdio, keepTestNames)); } List testCases = suite.elements("testcase"); @@ -262,7 +262,7 @@ private SuiteResult(File xmlReport, Element suite, boolean keepLongStdio, @Check // one wants to use @name from , // the other wants to use @classname from . - addCase(new CaseResult(this, e, classname, keepLongStdio)); + addCase(new CaseResult(this, e, classname, keepLongStdio, keepTestNames)); } String stdout = CaseResult.possiblyTrimStdio(cases, keepLongStdio, suite.elementText("system-out")); diff --git a/src/main/java/hudson/tasks/junit/TestResult.java b/src/main/java/hudson/tasks/junit/TestResult.java index 0eb11f5fc..83abe8cbf 100644 --- a/src/main/java/hudson/tasks/junit/TestResult.java +++ b/src/main/java/hudson/tasks/junit/TestResult.java @@ -121,6 +121,7 @@ public final class TestResult extends MetaTabulatedResult { private transient List failedTests; private final boolean keepLongStdio; + private final boolean keepTestNames; // default 3s as it depends on OS some can be good some not really.... public static final long FILE_TIME_PRECISION_MARGIN = Long.getLong(TestResult.class.getName() + "filetime.precision.margin", 3000); @@ -137,6 +138,7 @@ public TestResult() { */ public TestResult(boolean keepLongStdio) { this.keepLongStdio = keepLongStdio; + this.keepTestNames = false; impl = null; } @@ -147,7 +149,7 @@ public TestResult(long filesTimestamp, DirectoryScanner results) throws IOExcept @Deprecated public TestResult(long filesTimestamp, DirectoryScanner results, boolean keepLongStdio) throws IOException { - this(filesTimestamp, results, keepLongStdio, null); + this(filesTimestamp, results, keepLongStdio, false, null); } @Deprecated @@ -166,9 +168,10 @@ public TestResult(long filesTimestamp, DirectoryScanner results, boolean keepLon * @param skipOldReports to parse or not test files older than filesTimestamp * @since 1.22 */ - public TestResult(long filesTimestamp, DirectoryScanner results, boolean keepLongStdio, + public TestResult(long filesTimestamp, DirectoryScanner results, boolean keepLongStdio, boolean keepTestNames, PipelineTestDetails pipelineTestDetails, boolean skipOldReports) throws IOException { this.keepLongStdio = keepLongStdio; + this.keepTestNames = keepTestNames; impl = null; this.skipOldReports = skipOldReports; parse(filesTimestamp, results, pipelineTestDetails); @@ -177,6 +180,7 @@ public TestResult(long filesTimestamp, DirectoryScanner results, boolean keepLon public TestResult(TestResultImpl impl) { this.impl = impl; keepLongStdio = false; // irrelevant + keepTestNames = false; // irrelevant } @CheckForNull @@ -253,10 +257,10 @@ private void parse(long filesTimestamp, PipelineTestDetails pipelineTestDetails, continue; } // only count files that were actually updated during this build - parsePossiblyEmpty(reportFile, pipelineTestDetails); - } + parsePossiblyEmpty(reportFile, pipelineTestDetails); + } if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("testSuites size:" + this.getSuites().size()); - } + } @Override public hudson.tasks.test.TestResult getPreviousResult() { @@ -299,9 +303,9 @@ public void parse(long filesTimestamp, Iterable reportFiles, PipelineTestD public void parse(Iterable reportFiles, PipelineTestDetails pipelineTestDetails) throws IOException { for (File reportFile : reportFiles) { // only count files that were actually updated during this build - parsePossiblyEmpty(reportFile, pipelineTestDetails); + parsePossiblyEmpty(reportFile, pipelineTestDetails); + } } - } private void parsePossiblyEmpty(File reportFile, PipelineTestDetails pipelineTestDetails) throws IOException { if(reportFile.length()==0) { @@ -381,7 +385,7 @@ public void parse(File reportFile, PipelineTestDetails pipelineTestDetails) thro throw new IllegalStateException("Cannot reparse using a pluggable impl"); } try { - List suiteResults = SuiteResult.parse(reportFile, keepLongStdio, pipelineTestDetails); + List suiteResults = SuiteResult.parse(reportFile, keepLongStdio, keepTestNames, pipelineTestDetails); for (SuiteResult suiteResult : suiteResults) add(suiteResult); diff --git a/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java b/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java index 199c00907..9922bd491 100644 --- a/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java +++ b/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java @@ -23,6 +23,8 @@ import org.kohsuke.stapler.QueryParameter; import edu.umd.cs.findbugs.annotations.NonNull; +import javax.swing.tree.VariableHeightLayoutCache; + import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -40,6 +42,11 @@ public class JUnitResultsStep extends Step implements JUnitTask { */ private boolean keepLongStdio; + /** + * If true, mangle test names in case running in multiple stages or parallel steps. + */ + private boolean keepTestNames; + /** * {@link TestDataPublisher}s configured for this archiver, to process the recorded data. * For compatibility reasons, can be null. @@ -120,6 +127,22 @@ public boolean isKeepLongStdio() { this.keepLongStdio = keepLongStdio; } + /** + * @return the keepTestNames. + */ + public boolean isKeepTestNames() { + return keepTestNames; + } + + /** + * @param keepTestNames Whether to keep long stdio. + * + * @since 1.2-beta-1 + */ + @DataBoundSetter public final void setKeepTestNames(boolean v) { + this.keepTestNames = v; + } + /** * * @return the allowEmptyResults diff --git a/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly b/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly index ba3127151..c16093696 100644 --- a/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly +++ b/src/main/resources/hudson/tasks/junit/JUnitResultArchiver/config.jelly @@ -33,6 +33,9 @@ THE SOFTWARE. + + + From 589bcc11a080c95167105aebf14f4e07ec804b09 Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Thu, 12 May 2022 11:42:46 +0200 Subject: [PATCH 02/80] Fix tests. --- src/main/java/hudson/tasks/junit/CaseResult.java | 5 ++--- src/test/java/hudson/tasks/junit/SuiteResult2Test.java | 2 +- src/test/java/hudson/tasks/junit/SuiteResultTest.java | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/hudson/tasks/junit/CaseResult.java b/src/main/java/hudson/tasks/junit/CaseResult.java index 0c5b9f4b4..296530c01 100644 --- a/src/main/java/hudson/tasks/junit/CaseResult.java +++ b/src/main/java/hudson/tasks/junit/CaseResult.java @@ -143,8 +143,7 @@ public CaseResult( float duration, String stdout, String stderr, - String stacktrace, - boolean keepTestNames + String stacktrace ) { this.className = className; this.testName = testName; @@ -157,7 +156,7 @@ public CaseResult( this.skipped = skippedMessage != null; this.skippedMessage = skippedMessage; - this.keepTestNames = keepTestNames; + this.keepTestNames = false; } CaseResult(SuiteResult parent, Element testCase, String testClassName, boolean keepLongStdio, boolean keepTestNames) { diff --git a/src/test/java/hudson/tasks/junit/SuiteResult2Test.java b/src/test/java/hudson/tasks/junit/SuiteResult2Test.java index 152a99255..00657f225 100644 --- a/src/test/java/hudson/tasks/junit/SuiteResult2Test.java +++ b/src/test/java/hudson/tasks/junit/SuiteResult2Test.java @@ -97,7 +97,7 @@ public class SuiteResult2Test { } private SuiteResult parseOne(File file) throws Exception { - List results = SuiteResult.parse(file, false, null); + List results = SuiteResult.parse(file, false, false, null); assertEquals(1,results.size()); return results.get(0); } diff --git a/src/test/java/hudson/tasks/junit/SuiteResultTest.java b/src/test/java/hudson/tasks/junit/SuiteResultTest.java index a791af418..da43a2e8d 100644 --- a/src/test/java/hudson/tasks/junit/SuiteResultTest.java +++ b/src/test/java/hudson/tasks/junit/SuiteResultTest.java @@ -56,13 +56,13 @@ private File getDataFile(String name) throws URISyntaxException { } private SuiteResult parseOne(File file) throws Exception { - List results = SuiteResult.parse(file, false, null); + List results = SuiteResult.parse(file, false, false, null); assertEquals(1,results.size()); return results.get(0); } private List parseSuites(File file) throws Exception { - return SuiteResult.parse(file, false, null); + return SuiteResult.parse(file, false, false, null); } @Issue("JENKINS-1233") @@ -103,7 +103,7 @@ public void testIssue1463() throws Exception { @Issue("JENKINS-1472") @Test public void testIssue1472() throws Exception { - List results = SuiteResult.parse(getDataFile("junit-report-1472.xml"), false, null); + List results = SuiteResult.parse(getDataFile("junit-report-1472.xml"), false, false, null); assertTrue(results.size()>20); // lots of data here SuiteResult sr0 = results.get(0); From 5deff15bef49140350127ae9fe60f1f75d6a6c3a Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Mon, 30 May 2022 09:18:31 +0200 Subject: [PATCH 03/80] Improve comment. --- src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java b/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java index 9922bd491..c76fe5a60 100644 --- a/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java +++ b/src/main/java/hudson/tasks/junit/pipeline/JUnitResultsStep.java @@ -43,7 +43,7 @@ public class JUnitResultsStep extends Step implements JUnitTask { private boolean keepLongStdio; /** - * If true, mangle test names in case running in multiple stages or parallel steps. + * If true, do not mangle test names in case running in multiple stages or parallel steps. */ private boolean keepTestNames; From 84367f81a0ee43aa0e067de0295d2728788fc8d5 Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Wed, 21 Sep 2022 10:20:43 +0200 Subject: [PATCH 04/80] Assume history always available. More logging. Parallel history stream. --- src/main/java/hudson/tasks/junit/History.java | 6 ++++-- src/main/java/hudson/tasks/junit/TestResultAction.java | 7 +++++++ .../java/hudson/tasks/test/TestResultProjectAction.java | 6 +++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/hudson/tasks/junit/History.java b/src/main/java/hudson/tasks/junit/History.java index f5b7c070a..dfdce43a8 100644 --- a/src/main/java/hudson/tasks/junit/History.java +++ b/src/main/java/hudson/tasks/junit/History.java @@ -64,14 +64,15 @@ public TestObject getTestObject() { @SuppressWarnings("unused") // Called by jelly view public boolean historyAvailable() { - if (testObject instanceof hudson.tasks.junit.TestResult) { + return true; + /*if (testObject instanceof hudson.tasks.junit.TestResult) { TestResultImpl pluggableStorage = ((hudson.tasks.junit.TestResult) testObject).getPluggableStorage(); if (pluggableStorage != null) { return pluggableStorage.getCountOfBuildsWithTestResults() > 1; } } - return testObject.getRun().getParent().getBuilds().size() > 1; + return testObject.getRun().getParent().getBuilds().size() > 1;*/ } @JavaScriptMethod @@ -161,6 +162,7 @@ private List getHistoryFromFileStorage() { RunList builds = testObject.getRun().getParent().getBuilds(); return builds .stream() + .parallel() .map(build -> { hudson.tasks.test.TestResult resultInRun = testObject.getResultInRun(build); if (resultInRun == null) { diff --git a/src/main/java/hudson/tasks/junit/TestResultAction.java b/src/main/java/hudson/tasks/junit/TestResultAction.java index 3d3da4c03..0f4f812fa 100644 --- a/src/main/java/hudson/tasks/junit/TestResultAction.java +++ b/src/main/java/hudson/tasks/junit/TestResultAction.java @@ -65,6 +65,7 @@ */ @SuppressFBWarnings(value = "UG_SYNC_SET_UNSYNC_GET", justification = "False positive") public class TestResultAction extends AbstractTestResultAction implements StaplerProxy, SimpleBuildStep.LastBuildAction { + private static final Logger LOGGER = Logger.getLogger(TestResultAction.class.getName()); private transient WeakReference result; /** null only if there is a {@link JunitTestResultStorage} */ @@ -141,6 +142,8 @@ private XmlFile getDataFile() { @Override public synchronized TestResult getResult() { + long started = System.currentTimeMillis(); + //LOGGER.info("TestResultAction.load started"); JunitTestResultStorage storage = JunitTestResultStorage.find(); if (!(storage instanceof FileJunitTestResultStorage)) { return new TestResult(storage.load(run.getParent().getFullName(), run.getNumber())); @@ -162,6 +165,10 @@ public synchronized TestResult getResult() { failCount = r.getFailCount(); skipCount = r.getSkipCount(); } + long d = System.currentTimeMillis() - started; + if (d > 100) { + LOGGER.info("TestResultAction.load took " + d + " ms."); + } return r; } diff --git a/src/main/java/hudson/tasks/test/TestResultProjectAction.java b/src/main/java/hudson/tasks/test/TestResultProjectAction.java index b88d4665a..a35157627 100644 --- a/src/main/java/hudson/tasks/test/TestResultProjectAction.java +++ b/src/main/java/hudson/tasks/test/TestResultProjectAction.java @@ -104,16 +104,16 @@ public String getUrlName() { } public AbstractTestResultAction getLastTestResultAction() { - final Run tb = job.getLastSuccessfulBuild(); + //final Run tb = job.getLastSuccessfulBuild(); Run b = job.getLastBuild(); while(b!=null) { AbstractTestResultAction a = b.getAction(AbstractTestResultAction.class); if(a!=null && (!b.isBuilding())) return a; - if(b==tb) + //if(b==tb) // if even the last successful build didn't produce the test result, // that means we just don't have any tests configured. - return null; + //return null; b = b.getPreviousBuild(); } From b11b825eac51e460e027ece0c7a311e82d50b016 Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Wed, 21 Sep 2022 13:19:22 +0200 Subject: [PATCH 05/80] Paralellize test history handling to unbreak it with large amounts of tests. Add timeout and build count limit. --- pom.xml | 5 ++ src/main/java/hudson/tasks/junit/History.java | 47 +++++++++++-------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index d421de1fe..e90ffac45 100644 --- a/pom.xml +++ b/pom.xml @@ -188,6 +188,11 @@ org.jenkins-ci.plugins jackson2-api + + com.pivovarit + parallel-collectors + 2.5.0 + org.jenkins-ci.plugins pipeline-utility-steps diff --git a/src/main/java/hudson/tasks/junit/History.java b/src/main/java/hudson/tasks/junit/History.java index dfdce43a8..92d08dcf0 100644 --- a/src/main/java/hudson/tasks/junit/History.java +++ b/src/main/java/hudson/tasks/junit/History.java @@ -26,6 +26,10 @@ import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import com.pivovarit.collectors.*; import edu.hm.hafner.echarts.ChartModelConfiguration; import edu.hm.hafner.echarts.JacksonFacade; @@ -156,28 +160,33 @@ public HistoryTableResult retrieveHistorySummary(int userOffset) { } return new HistoryTableResult(getHistoryFromFileStorage()); } - + ExecutorService executor = Executors.newFixedThreadPool(Math.max(4, (int)(Runtime.getRuntime().availableProcessors() * 0.75 * 0.75))); private List getHistoryFromFileStorage() { TestObject testObject = getTestObject(); RunList builds = testObject.getRun().getParent().getBuilds(); - return builds - .stream() - .parallel() - .map(build -> { - hudson.tasks.test.TestResult resultInRun = testObject.getResultInRun(build); - if (resultInRun == null) { - return null; - } - - return new HistoryTestResultSummary(build, resultInRun.getDuration(), - resultInRun.getFailCount(), - resultInRun.getSkipCount(), - resultInRun.getPassCount(), - resultInRun.getDescription() - ); - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + int parallelism = Math.min(Runtime.getRuntime().availableProcessors(), Math.max(4, (int)(Runtime.getRuntime().availableProcessors() * 0.75 * 0.75))); + final AtomicInteger count = new AtomicInteger(0); + final long startedMs = java.lang.System.currentTimeMillis(); + return builds.stream() + .collect(ParallelCollectors.parallel(build -> { + if (count.incrementAndGet() > 1000 || (java.lang.System.currentTimeMillis() - startedMs) > 15000) { // Do not navigate too far or for too long, we need to finish the request this year + return null; + } + hudson.tasks.test.TestResult resultInRun = testObject.getResultInRun(build); + if (resultInRun == null) { + return null; + } + + return new HistoryTestResultSummary(build, resultInRun.getDuration(), + resultInRun.getFailCount(), + resultInRun.getSkipCount(), + resultInRun.getPassCount(), + resultInRun.getDescription() + ); + }, executor, parallelism)) + .join() + .filter(Objects::nonNull) + .collect(Collectors.toList()); } @SuppressWarnings("unused") // Called by jelly view From c29f0cc86f4da37c7cb9cf057681fbcfc35090fa Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Mon, 26 Sep 2022 14:27:50 +0200 Subject: [PATCH 06/80] Implement explicit XML parsing to avoid slow reflection access. --- .../java/hudson/tasks/junit/CaseResult.java | 73 ++++++++-- .../java/hudson/tasks/junit/SuiteResult.java | 126 +++++++++++++++++- .../java/hudson/tasks/junit/TestResult.java | 76 ++++++++++- .../hudson/tasks/junit/TestResultAction.java | 8 +- 4 files changed, 261 insertions(+), 22 deletions(-) diff --git a/src/main/java/hudson/tasks/junit/CaseResult.java b/src/main/java/hudson/tasks/junit/CaseResult.java index 296530c01..7496d4050 100644 --- a/src/main/java/hudson/tasks/junit/CaseResult.java +++ b/src/main/java/hudson/tasks/junit/CaseResult.java @@ -54,6 +54,9 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import javax.xml.stream.*; +import javax.xml.stream.events.*; + /** * One test result. * @@ -63,22 +66,22 @@ */ public class CaseResult extends TestResult implements Comparable { private static final Logger LOGGER = Logger.getLogger(CaseResult.class.getName()); - private final float duration; + private float duration; /** * In JUnit, a test is a method of a class. This field holds the fully qualified class name * that the test was in. */ - private final String className; + private String className; /** * This field retains the method name. */ - private final String testName; + private String testName; private transient String safeName; - private final boolean skipped; - private final boolean keepTestNames; - private final String skippedMessage; - private final String errorStackTrace; - private final String errorDetails; + private boolean skipped; + private boolean keepTestNames; + private String skippedMessage; + private String errorStackTrace; + private String errorDetails; @SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "Specific method to restore it") private transient SuiteResult parent; @@ -92,14 +95,14 @@ public class CaseResult extends TestResult implements Comparable { * If these information are reported at the test case level, these fields are set, * otherwise null, in which case {@link SuiteResult#stdout}. */ - private final String stdout,stderr; + private String stdout,stderr; /** * This test has been failing since this build number (not id.) * * If {@link #isPassed() passing}, this field is left unused to 0. */ - private /*final*/ int failedSince; + private int failedSince; private static float parseTime(Element testCase) { String time = testCase.attributeValue("time"); @@ -197,6 +200,56 @@ public CaseResult( this.keepTestNames = keepTestNames; } + public static CaseResult parse(SuiteResult parent, final XMLEventReader reader, String ver) throws XMLStreamException { + CaseResult r = new CaseResult(parent, null, null, null); + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals("case")) { + return r; + } + if (event.isStartElement()) { + final StartElement element = event.asStartElement(); + final String elementName = element.getName().getLocalPart(); + switch (elementName) { + case "duration": + r.duration = new TimeToFloat(reader.getElementText()).parse(); + break; + case "className": + r.className = reader.getElementText(); + break; + case "testName": + r.testName = reader.getElementText(); + break; + case "skippedMessage": + r.skippedMessage = reader.getElementText(); + break; + case "skipped": + r.skipped = Boolean.parseBoolean(reader.getElementText()); + break; + case "keepTestNames": + r.keepTestNames = Boolean.parseBoolean(reader.getElementText()); + break; + case "errorStackTrace": + r.errorStackTrace = reader.getElementText(); + break; + case "errorDetails": + r.errorDetails = reader.getElementText(); + break; + case "failedSince": + r.failedSince = Integer.parseInt(reader.getElementText()); + break; + case "stdout": + r.stdout = reader.getElementText(); + break; + case "stderr": + r.stderr = reader.getElementText(); + break; + } + } + } + return r; + } + static String possiblyTrimStdio(Collection results, boolean keepLongStdio, String stdio) { // HUDSON-6516 if (stdio == null) { return null; diff --git a/src/main/java/hudson/tasks/junit/SuiteResult.java b/src/main/java/hudson/tasks/junit/SuiteResult.java index 22a066dae..6a74f6cda 100644 --- a/src/main/java/hudson/tasks/junit/SuiteResult.java +++ b/src/main/java/hudson/tasks/junit/SuiteResult.java @@ -40,6 +40,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.Reader; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; @@ -52,6 +53,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.xml.stream.*; +import javax.xml.stream.events.*; + /** * Result of one test suite. * @@ -68,10 +72,10 @@ @ExportedBean public final class SuiteResult implements Serializable { private static final Logger LOGGER = Logger.getLogger(SuiteResult.class.getName()); - private final String file; - private final String name; - private final String stdout; - private final String stderr; + private String file; + private String name; + private String stdout; + private String stderr; private float duration; /** * The 'timestamp' attribute of the test suite. @@ -93,14 +97,14 @@ public final class SuiteResult implements Serializable { */ private String nodeId; - private final List enclosingBlocks = new ArrayList<>(); + private List enclosingBlocks = new ArrayList<>(); - private final List enclosingBlockNames = new ArrayList<>(); + private List enclosingBlockNames = new ArrayList<>(); /** * All test cases. */ - private final List cases = new ArrayList<>(); + private List cases = new ArrayList<>(); private transient Map casesByName; private transient hudson.tasks.junit.TestResult parent; @@ -127,6 +131,114 @@ public SuiteResult(String name, String stdout, String stderr, @CheckForNull Pipe this.file = null; } + public static SuiteResult parse(final XMLEventReader reader, String ver) throws XMLStreamException { + SuiteResult r = new SuiteResult("", "", "", null); + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals("suite")) { + return r; + } + if (event.isStartElement()) { + final StartElement element = event.asStartElement(); + final String elementName = element.getName().getLocalPart(); + switch (elementName) { + case "cases": + parseCases(r, reader, ver); + break; + case "file": + r.file = reader.getElementText(); + break; + case "name": + r.name = reader.getElementText(); + break; + case "id": + r.id = reader.getElementText(); + break; + case "duration": + r.duration = new TimeToFloat(reader.getElementText()).parse(); + break; + case "timestamp": + r.timestamp = reader.getElementText(); + break; + case "time": + r.time = reader.getElementText(); + break; + case "nodeId": + r.nodeId = reader.getElementText(); + break; + case "enclosingBlocks": + parseEnclosingBlocks(r, reader, ver); + break; + case "enclosingBlockNames": + parseEnclosingBlockNames(r, reader, ver); + break; + case "stdout": + r.stdout = reader.getElementText(); + break; + case "stderr": + r.stderr = reader.getElementText(); + break; + } + } + } + return r; + } + + public static void parseEnclosingBlocks(SuiteResult r, final XMLEventReader reader, String ver) throws XMLStreamException { + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals("enclosingBlocks")) { + return; + } + if (event.isStartElement()) { + final StartElement element = event.asStartElement(); + final String elementName = element.getName().getLocalPart(); + switch (elementName) { + case "string": + r.enclosingBlocks.add(reader.getElementText()); + break; + } + } + } + } + + public static void parseEnclosingBlockNames(SuiteResult r, final XMLEventReader reader, String ver) throws XMLStreamException { + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals("enclosingBlockNames")) { + return; + } + if (event.isStartElement()) { + final StartElement element = event.asStartElement(); + final String elementName = element.getName().getLocalPart(); + switch (elementName) { + case "string": + r.enclosingBlockNames.add(reader.getElementText()); + break; + } + } + } + } + + public static void parseCases(SuiteResult r, final XMLEventReader reader, String ver) throws XMLStreamException { + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals("cases")) { + return; + } + if (event.isStartElement()) { + final StartElement element = event.asStartElement(); + final String elementName = element.getName().getLocalPart(); + switch (elementName) { + case "case": + r.cases.add(CaseResult.parse(r, reader, ver)); + break; + } + } + } + + } + private synchronized Map casesByName() { if (casesByName == null) { casesByName = new HashMap<>(); diff --git a/src/main/java/hudson/tasks/junit/TestResult.java b/src/main/java/hudson/tasks/junit/TestResult.java index 83abe8cbf..7fd49d804 100644 --- a/src/main/java/hudson/tasks/junit/TestResult.java +++ b/src/main/java/hudson/tasks/junit/TestResult.java @@ -24,6 +24,7 @@ package hudson.tasks.junit; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.XmlFile; import hudson.model.Run; import io.jenkins.plugins.junit.storage.TestResultImpl; import hudson.tasks.test.AbstractTestResultAction; @@ -36,6 +37,7 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.io.Reader; import java.io.StringWriter; import java.nio.file.Files; import java.util.ArrayList; @@ -53,12 +55,19 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; + +import javax.xml.namespace.QName; +import javax.xml.stream.*; +import javax.xml.stream.events.*; + import org.apache.tools.ant.DirectoryScanner; import org.dom4j.DocumentException; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; +import com.thoughtworks.xstream.XStream; + import edu.umd.cs.findbugs.annotations.NonNull; /** @@ -120,8 +129,8 @@ public final class TestResult extends MetaTabulatedResult { */ private transient List failedTests; - private final boolean keepLongStdio; - private final boolean keepTestNames; + private boolean keepLongStdio; + private boolean keepTestNames; // default 3s as it depends on OS some can be good some not really.... public static final long FILE_TIME_PRECISION_MARGIN = Long.getLong(TestResult.class.getName() + "filetime.precision.margin", 3000); @@ -202,6 +211,69 @@ public void setParent(TestObject parent) { public TestResult getTestResult() { return this; } + private static final XMLInputFactory factory = XMLInputFactory.newInstance(); + public void parse(XmlFile f) throws XMLStreamException, IOException { + try (Reader r = f.readRaw()){ + final XMLEventReader reader = factory.createXMLEventReader(r); + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isStartElement() && event.asStartElement().getName() + .getLocalPart().equals("result")) { + parseXmlResult(reader, event.asStartElement()); + } + } + r.close(); + } /*catch (Exception e) { + e.printStackTrace(); + }*/ + } + + private void parseXmlResult(final XMLEventReader reader, StartElement startEvent) throws XMLStreamException { + Attribute attr = startEvent.getAttributeByName(QName.valueOf("plugin")); + String ver = attr == null ? null : attr.getValue(); + while (reader.hasNext()) { + XMLEvent event = reader.nextEvent(); + if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals("result")) { + return; + } + if (event.isStartElement()) { + final StartElement element = event.asStartElement(); + final String elementName = element.getName().getLocalPart(); + switch (elementName) { + case "suites": + parseXmlSuites(reader, ver); + break; + case "duration": + duration = new TimeToFloat(reader.getElementText()).parse(); + break; + case "keepLongStdio": + keepLongStdio = Boolean.parseBoolean(reader.getElementText()); + break; + case "keepTestNames": + keepTestNames = Boolean.parseBoolean(reader.getElementText()); + break; + } + } + } + } + + private void parseXmlSuites(final XMLEventReader reader, String ver) throws XMLStreamException { + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals("suites")) { + return; + } + if (event.isStartElement()) { + final StartElement element = event.asStartElement(); + final String elementName = element.getName().getLocalPart(); + switch (elementName) { + case "suite": + suites.add(SuiteResult.parse(reader, ver)); + break; + } + } + } + } @Deprecated public void parse(long filesTimestamp, DirectoryScanner results) throws IOException { diff --git a/src/main/java/hudson/tasks/junit/TestResultAction.java b/src/main/java/hudson/tasks/junit/TestResultAction.java index 0f4f812fa..983d26c79 100644 --- a/src/main/java/hudson/tasks/junit/TestResultAction.java +++ b/src/main/java/hudson/tasks/junit/TestResultAction.java @@ -236,10 +236,12 @@ public List getSkippedTests() { */ private TestResult load() { TestResult r; + XmlFile df = getDataFile(); try { - r = (TestResult)getDataFile().read(); - } catch (IOException e) { - logger.log(Level.WARNING, "Failed to load "+getDataFile(),e); + r = new TestResult(); + r.parse(df); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to load " + df, e); r = new TestResult(); // return a dummy } r.freeze(this); From 379ef56c056a7bf99f08bd6ca1ad080cec79d33f Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Mon, 26 Sep 2022 16:10:43 +0200 Subject: [PATCH 07/80] Make test history dynamic and increase table size. --- src/main/java/hudson/tasks/junit/History.java | 15 ++++++------- .../hudson/tasks/junit/History/index.jelly | 21 +++++++++++++++---- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/main/java/hudson/tasks/junit/History.java b/src/main/java/hudson/tasks/junit/History.java index 92d08dcf0..3a8f1d3b1 100644 --- a/src/main/java/hudson/tasks/junit/History.java +++ b/src/main/java/hudson/tasks/junit/History.java @@ -147,9 +147,9 @@ public List getHistorySummaries() { } } - public HistoryTableResult retrieveHistorySummary(int userOffset) { - int offset = userOffset; - if (userOffset > 1000 || userOffset < 0) { + public HistoryTableResult retrieveHistorySummary(int start, int end) { + int offset = start; + if (start > 1000 || start < 0) { offset = 0; } @@ -158,18 +158,19 @@ public HistoryTableResult retrieveHistorySummary(int userOffset) { if (pluggableStorage != null) { return new HistoryTableResult(pluggableStorage.getHistorySummary(offset)); } - return new HistoryTableResult(getHistoryFromFileStorage()); + return new HistoryTableResult(getHistoryFromFileStorage(start, end)); } ExecutorService executor = Executors.newFixedThreadPool(Math.max(4, (int)(Runtime.getRuntime().availableProcessors() * 0.75 * 0.75))); - private List getHistoryFromFileStorage() { + private List getHistoryFromFileStorage(int start, int end) { TestObject testObject = getTestObject(); RunList builds = testObject.getRun().getParent().getBuilds(); int parallelism = Math.min(Runtime.getRuntime().availableProcessors(), Math.max(4, (int)(Runtime.getRuntime().availableProcessors() * 0.75 * 0.75))); final AtomicInteger count = new AtomicInteger(0); final long startedMs = java.lang.System.currentTimeMillis(); - return builds.stream() + return builds.stream().skip(start) .collect(ParallelCollectors.parallel(build -> { - if (count.incrementAndGet() > 1000 || (java.lang.System.currentTimeMillis() - startedMs) > 15000) { // Do not navigate too far or for too long, we need to finish the request this year + int c = count.incrementAndGet(); + if (c > end - start || (java.lang.System.currentTimeMillis() - startedMs) > 15000) { // Do not navigate too far or for too long, we need to finish the request this year and have to think about RAM return null; } hudson.tasks.test.TestResult resultInRun = testObject.getResultInRun(build); diff --git a/src/main/resources/hudson/tasks/junit/History/index.jelly b/src/main/resources/hudson/tasks/junit/History/index.jelly index 6f633981b..3ff6afa03 100644 --- a/src/main/resources/hudson/tasks/junit/History/index.jelly +++ b/src/main/resources/hudson/tasks/junit/History/index.jelly @@ -27,7 +27,7 @@ THE SOFTWARE. - + @@ -72,9 +72,22 @@ THE SOFTWARE. - +
+
+ + ${%Older} + + + + + ${%Newer} + + +
@@ -89,7 +102,7 @@ THE SOFTWARE. - +
@@ -116,7 +129,7 @@ THE SOFTWARE. - ${%Newer} From 8ba78e740fc5de1dcd445162167c40d8944936ba Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Tue, 27 Sep 2022 11:16:29 +0200 Subject: [PATCH 08/80] Fix trend charts not shown after carousel slide. --- src/main/webapp/history/history.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/history/history.js b/src/main/webapp/history/history.js index f2e99132c..c0ef7d85d 100644 --- a/src/main/webapp/history/history.js +++ b/src/main/webapp/history/history.js @@ -50,20 +50,23 @@ * */ function storeAndRestoreCarousel (carouselId) { - const carousel = $('#' + carouselId); + // jQuery does not work for some reason + //const carousel = $('#' + carouselId); + const carousel = document.getElementById("trend-carousel") + const activeCarousel = localStorage.getItem(carouselId); + if (activeCarousel) { + const carouselControl = new bootstrap5.Carousel(carousel[0]); + carouselControl.to(parseInt(activeCarousel)); + carouselControl.pause(); + } carousel.on('slid.bs.carousel', function (e) { + //alert("something slid.bs.carousel") localStorage.setItem(carouselId, e.to); const chart = $(e.relatedTarget).find('>:first-child')[0].echart; if (chart) { chart.resize(); } }); - const activeCarousel = localStorage.getItem(carouselId); - if (activeCarousel) { - const carouselControl = new bootstrap5.Carousel(carousel[0]); - carouselControl.to(parseInt(activeCarousel)); - carouselControl.pause(); - } } }) })(jQuery3); From 0bba40822b2e7e9d208ed3b1e030f91dfe54523d Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Tue, 27 Sep 2022 14:32:26 +0200 Subject: [PATCH 09/80] Fix carousel not redrawing. Reduce trend chart loading times. Display same count of builds in trend chart as in table. Workaround for jQuery issue. --- src/main/java/hudson/tasks/junit/History.java | 62 +++++++++++-------- .../tasks/junit/HistoryTestResultSummary.java | 4 ++ .../hudson/tasks/junit/History/index.jelly | 19 +++--- src/main/webapp/history/history.js | 20 +++--- 4 files changed, 63 insertions(+), 42 deletions(-) diff --git a/src/main/java/hudson/tasks/junit/History.java b/src/main/java/hudson/tasks/junit/History.java index 3a8f1d3b1..cec0fa800 100644 --- a/src/main/java/hudson/tasks/junit/History.java +++ b/src/main/java/hudson/tasks/junit/History.java @@ -25,25 +25,26 @@ import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; -import java.util.concurrent.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; -import com.pivovarit.collectors.*; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.bind.JavaScriptMethod; + +import com.pivovarit.collectors.ParallelCollectors; import edu.hm.hafner.echarts.ChartModelConfiguration; import edu.hm.hafner.echarts.JacksonFacade; import edu.hm.hafner.echarts.LinesChartModel; - -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; -import org.kohsuke.stapler.bind.JavaScriptMethod; +import hudson.model.Run; import hudson.tasks.test.TestObject; import hudson.tasks.test.TestObjectIterable; import hudson.tasks.test.TestResultDurationChart; import hudson.tasks.test.TestResultTrendChart; import hudson.util.RunList; - import io.jenkins.plugins.junit.storage.TestResultImpl; /** @@ -81,37 +82,48 @@ public boolean historyAvailable() { @JavaScriptMethod @SuppressWarnings("unused") // Called by jelly view - public String getTestResultTrend(String configuration) { - return JACKSON_FACADE.toJson(createTestResultTrend(ChartModelConfiguration.fromJson(configuration))); + public String getTestResultTrend(int start, int end, String configuration) { + return JACKSON_FACADE.toJson(createTestResultTrend(start, end, ChartModelConfiguration.fromJson(configuration))); } - private LinesChartModel createTestResultTrend(ChartModelConfiguration chartModelConfiguration) { + private LinesChartModel createTestResultTrend(int start, int end, ChartModelConfiguration chartModelConfiguration) { TestResultImpl pluggableStorage = getPluggableStorage(); if (pluggableStorage != null) { return new TestResultTrendChart().create(pluggableStorage.getTrendTestResultSummary()); } - - return new TestResultTrendChart().createFromTestObject(createBuildHistory(testObject), chartModelConfiguration); + return new TestResultTrendChart().createFromTestObject(createBuildHistory(testObject, start, end), chartModelConfiguration); } @JavaScriptMethod @SuppressWarnings("unused") // Called by jelly view - public String getTestDurationTrend(String configuration) { - return JACKSON_FACADE.toJson(createTestDurationResultTrend(ChartModelConfiguration.fromJson(configuration))); + public String getTestDurationTrend(int start, int end, String configuration) { + return JACKSON_FACADE.toJson(createTestDurationResultTrend(start, end, ChartModelConfiguration.fromJson(configuration))); } - private LinesChartModel createTestDurationResultTrend(ChartModelConfiguration chartModelConfiguration) { + private LinesChartModel createTestDurationResultTrend(int start, int end, ChartModelConfiguration chartModelConfiguration) { TestResultImpl pluggableStorage = getPluggableStorage(); if (pluggableStorage != null) { return new TestResultDurationChart().create(pluggableStorage.getTestDurationResultSummary()); } - return new TestResultDurationChart().create(createBuildHistory(testObject), chartModelConfiguration); + return new TestResultDurationChart().create(createBuildHistory(testObject, start, end), chartModelConfiguration); } - private TestObjectIterable createBuildHistory(final TestObject testObject) { - return new TestObjectIterable(testObject); + private TestObjectIterable createBuildHistory(final TestObject testObject, int start, int end) { + HistoryTableResult r = retrieveHistorySummary(start, start); + if (r.getHistorySummaries().size() != 0) { // Fast + Run build = r.getHistorySummaries().get(0).getRun(); + return new TestObjectIterable(testObject.getResultInRun(build)); + } + return null; + /*TestObject pos = testObject; + TestObject prev = testObject; + while (start-- > 0 && pos != null) { + prev = pos; + pos = pos.getPreviousResult(); + } + return new TestObjectIterable(prev);*/ } private TestResultImpl getPluggableStorage() { @@ -148,14 +160,12 @@ public List getHistorySummaries() { } public HistoryTableResult retrieveHistorySummary(int start, int end) { - int offset = start; - if (start > 1000 || start < 0) { - offset = 0; - } - TestResultImpl pluggableStorage = getPluggableStorage(); - if (pluggableStorage != null) { + int offset = start; + if (start > 1000 || start < 0) { + offset = 0; + } return new HistoryTableResult(pluggableStorage.getHistorySummary(offset)); } return new HistoryTableResult(getHistoryFromFileStorage(start, end)); @@ -170,7 +180,7 @@ private List getHistoryFromFileStorage(int start, int return builds.stream().skip(start) .collect(ParallelCollectors.parallel(build -> { int c = count.incrementAndGet(); - if (c > end - start || (java.lang.System.currentTimeMillis() - startedMs) > 15000) { // Do not navigate too far or for too long, we need to finish the request this year and have to think about RAM + if (c > end - start + 1 || (java.lang.System.currentTimeMillis() - startedMs) > 15000) { // Do not navigate too far or for too long, we need to finish the request this year and have to think about RAM return null; } hudson.tasks.test.TestResult resultInRun = testObject.getResultInRun(build); diff --git a/src/main/java/hudson/tasks/junit/HistoryTestResultSummary.java b/src/main/java/hudson/tasks/junit/HistoryTestResultSummary.java index e4f250652..19af6d4fe 100644 --- a/src/main/java/hudson/tasks/junit/HistoryTestResultSummary.java +++ b/src/main/java/hudson/tasks/junit/HistoryTestResultSummary.java @@ -29,6 +29,10 @@ public String getDescription() { return description; } + public Run getRun() { + return run; + } + public String getDurationString() { return Util.getTimeSpanString((long) (duration * 1000)); } diff --git a/src/main/resources/hudson/tasks/junit/History/index.jelly b/src/main/resources/hudson/tasks/junit/History/index.jelly index 3ff6afa03..164bac355 100644 --- a/src/main/resources/hudson/tasks/junit/History/index.jelly +++ b/src/main/resources/hudson/tasks/junit/History/index.jelly @@ -26,8 +26,9 @@ THE SOFTWARE. + - + @@ -35,8 +36,10 @@ THE SOFTWARE. - - + + + + + +
- ${%Older} + ${%Older} - ${%Newer} + ${%Newer}
-
+
-
builds.
- +
builds.
+
+
+
Sample interval:
+
-
@@ -105,14 +120,14 @@ THE SOFTWARE.
- ${%Older} - + ${%Newer} @@ -153,14 +168,14 @@ THE SOFTWARE.
- ${%Older} - + ${%Newer} From 2764ca3dfdb1241c3e31a1828cf38f472f6568a0 Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Mon, 22 Jul 2024 12:26:27 +0200 Subject: [PATCH 77/80] Hide smooth and trend graphs on narrow screens. Remove JS logging. --- src/main/java/hudson/tasks/junit/History.java | 4 +- .../hudson/tasks/junit/History/history.js | 50 +++++++++++++------ 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/main/java/hudson/tasks/junit/History.java b/src/main/java/hudson/tasks/junit/History.java index ecf591729..1d11aa3ca 100644 --- a/src/main/java/hudson/tasks/junit/History.java +++ b/src/main/java/hudson/tasks/junit/History.java @@ -232,6 +232,7 @@ private void createLinearTrend(ObjectMapper mapper, ArrayNode series, List histor double k = (double)counts.length / smoothPts; final double splineRoundMul = 1000.0; for (double z = minDuration; z < maxDuration; z += step * k) { - // Use float for smaller JSONs. double v = Math.round(splineRoundMul * Math.max(0.0, scs.evaluate(z / scale * 100.0))) / splineRoundMul; durationData.add((float)v); maxBuilds = Math.max(maxBuilds, (int)Math.ceil(v)); + // Use float for smaller JSONs. domainAxisLabels.add((float)(Math.round(mul * z * roundMul) / roundMul)); } diff --git a/src/main/resources/hudson/tasks/junit/History/history.js b/src/main/resources/hudson/tasks/junit/History/history.js index 6f627fb4c..b38b34e25 100644 --- a/src/main/resources/hudson/tasks/junit/History/history.js +++ b/src/main/resources/hudson/tasks/junit/History/history.js @@ -6,6 +6,9 @@ var interval var trendChartJson var appRootUrl var testObjectUrl +var resultSeries +var durationSeries +var trendChartId = 'test-trend-chart' function onBuildWindowChange(selectObj) { let idx = selectObj.selectedIndex; @@ -31,8 +34,6 @@ function onBuildIntervalChange(selectObj) { appRootUrl = dataEl.getAttribute("data-appRootUrl") testObjectUrl = dataEl.getAttribute("data-testObjectUrl") - console.log(`start: ${start}, end: ${end}, count: ${count}, trendChartJsonStr.length: ${trendChartJsonStr.length}, trendChartJson: ${trendChartJson}`) - trendChartJsonStr = null dataEl.setAttribute("data-trendChartJson", "") @@ -47,7 +48,6 @@ function onBuildIntervalChange(selectObj) { document.getElementById('history-window').value = count document.getElementById('history-interval').value = interval - console.log("status: " + JSON.stringify(trendChartJson?.status)) if (trendChartJson?.status && trendChartJson?.status.buildsWithTestResult < trendChartJson?.status.buildsRequested) { let s if (trendChartJson.status.hasTimedOut) { @@ -67,6 +67,27 @@ function onBuildIntervalChange(selectObj) { }); }); + function filterTrendSeries() { + let model = trendChartJson + const chartPlaceHolder = document.getElementById(trendChartId); + if (resultSeries === undefined) { + resultSeries = model.result.series + } + if (durationSeries === undefined) { + durationSeries = model.duration.series + } + let r = chartPlaceHolder.getBoundingClientRect() + let aspect = r.width / r.height + let series = durationSeries.concat(resultSeries) + if (aspect < 1.75) { + series = series.filter((s) => !s.preferScreenOrient || s.preferScreenOrient != "landscape") + } + series.forEach(s => s.emphasis = { + disabled: true + }); + return series + } + function renderTrendChart(chartDivId, model, settingsDialogId, chartClickedEventHandler) { const chartPlaceHolder = document.getElementById(chartDivId); const chart = echarts.init(chartPlaceHolder); @@ -76,11 +97,6 @@ function onBuildIntervalChange(selectObj) { const showSettings = document.getElementById(settingsDialogId); let darkMode = style.getPropertyValue('--darkreader-bg--background') darkMode = darkMode !== undefined && darkMode !== null && darkMode !== '' - console.log('darkMode: ' + darkMode) - let series = model.duration.series.concat(model.result.series) - series.forEach(s => s.emphasis = { - disabled: true - }); const options = { animation: false, darkMode: darkMode, @@ -139,7 +155,7 @@ function onBuildIntervalChange(selectObj) { type: 'plain', x: 'center', y: 'top', - width: '80%', + width: '75%', textStyle: { color: textColor }, @@ -222,7 +238,7 @@ function onBuildIntervalChange(selectObj) { } } ], - series: series + series: filterTrendSeries() }; chart.setOption(options); chart.resize(); @@ -249,7 +265,6 @@ function onBuildIntervalChange(selectObj) { let darkMode = style.getPropertyValue('--darkreader-bg--background') darkMode = darkMode !== undefined && darkMode !== null && darkMode !== '' - console.log('darkMode: ' + darkMode) let series = model.distribution.series series.forEach(s => s.emphasis = { disabled: true @@ -355,10 +370,8 @@ function onBuildIntervalChange(selectObj) { function applyCssColors(chartData) { let style = getComputedStyle(document.body) chartData.series.forEach((s) => { - //console.log('s[' + s.name + '].itemStyle.color = ' + s.itemStyle.color) if (s?.itemStyle?.color && s.itemStyle.color.startsWith('--')) { s.itemStyle.color = style.getPropertyValue(s.itemStyle.color) - //console.log('color => ' + s.itemStyle.color) } }) } @@ -374,9 +387,8 @@ function onBuildIntervalChange(selectObj) { * Creates the charts that show the test results, duration and distribution across a number of builds. */ // TODO: Improve ECharts plugin to allow more direct interaction with ECharts - renderTrendChart('test-trend-chart', trendChartJson, trendConfigurationDialogId, + renderTrendChart(trendChartId, trendChartJson, trendConfigurationDialogId, function (buildDisplayName) { - console.log(buildDisplayName + ' clicked on chart') if (trendChartJson.buildMap[buildDisplayName]) { window.open(rootUrl + trendChartJson.buildMap[buildDisplayName].url); } @@ -384,7 +396,13 @@ function onBuildIntervalChange(selectObj) { renderDistributionChart('test-distribution-chart', trendChartJson, trendConfigurationDialogId, null); } jQuery3(window).resize(function () { - document.getElementById('test-trend-chart').echart.resize(); + let trendEchart = document.getElementById(trendChartId).echart + trendEchart.setOption({ + series: filterTrendSeries() + }, { + replaceMerge: ['series'] + }) + trendEchart.resize(); document.getElementById('test-distribution-chart').echart.resize(); }); }) From f22740fff4e04b1cd2cbd3bfa1a7383dd56a1d7f Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Tue, 23 Jul 2024 07:21:47 +0200 Subject: [PATCH 78/80] Improve backward compatibility. --- .../hudson/tasks/junit/pipeline/JUnitResultsStepTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/hudson/tasks/junit/pipeline/JUnitResultsStepTest.java b/src/test/java/hudson/tasks/junit/pipeline/JUnitResultsStepTest.java index be69e05e0..9fa4c249c 100644 --- a/src/test/java/hudson/tasks/junit/pipeline/JUnitResultsStepTest.java +++ b/src/test/java/hudson/tasks/junit/pipeline/JUnitResultsStepTest.java @@ -495,6 +495,11 @@ private static Predicate stageForName(final String name) { input.getDisplayName().equals(name); } + public static void assertBranchResults(WorkflowRun run, int suiteCount, int testCount, int failCount, String branchName, String stageName, + String innerStageName) { + assertBranchResults(run, suiteCount, testCount, failCount, branchName, stageName, innerStageName, false); + } + public static void assertBranchResults(WorkflowRun run, int suiteCount, int testCount, int failCount, String branchName, String stageName, String innerStageName, boolean keepTestNames) { FlowExecution execution = run.getExecution(); From 6cb42d17e0f3cabd813dac676448ab0ab95a738e Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Tue, 23 Jul 2024 14:05:02 +0200 Subject: [PATCH 79/80] Fix properties not loaded. --- .../java/hudson/tasks/junit/CaseResult.java | 56 ++++++++++++++++++- .../java/hudson/tasks/junit/SuiteResult.java | 6 +- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/tasks/junit/CaseResult.java b/src/main/java/hudson/tasks/junit/CaseResult.java index d39b218fb..fdf1e33d8 100644 --- a/src/main/java/hudson/tasks/junit/CaseResult.java +++ b/src/main/java/hudson/tasks/junit/CaseResult.java @@ -89,7 +89,7 @@ public class CaseResult extends TestResult implements Comparable { private String skippedMessage; private String errorStackTrace; private String errorDetails; - private final Map properties; + private Map properties; @SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "Specific method to restore it") private transient SuiteResult parent; @@ -299,6 +299,10 @@ public static CaseResult parse(SuiteResult parent, final XMLStreamReader reader, case "stderr": r.stderr = reader.getElementText(); break; + case "properties": + r.properties = new HashMap<>(); + parseProperties(r.properties, reader, ver); + break; default: if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("CaseResult.parse encountered an unknown field: " + elementName); @@ -309,6 +313,56 @@ public static CaseResult parse(SuiteResult parent, final XMLStreamReader reader, return r; } + public static void parseProperties(Map r, final XMLStreamReader reader, String ver) throws XMLStreamException { + while (reader.hasNext()) { + final int event = reader.next(); + if (event == XMLStreamReader.END_ELEMENT && reader.getLocalName().equals("properties")) { + return; + } + if (event == XMLStreamReader.START_ELEMENT) { + final String elementName = reader.getLocalName(); + switch (elementName) { + case "entry": + parseProperty(r, reader, ver); + break; + default: + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.finest("CaseResult.parseProperties encountered an unknown field: " + elementName); + } + } + } + } + } + + public static void parseProperty(Map r, final XMLStreamReader reader, String ver) throws XMLStreamException { + while (reader.hasNext()) { + String name = null, value = null; + final int event = reader.next(); + if (event == XMLStreamReader.END_ELEMENT && reader.getLocalName().equals("entry")) { + if (name != null || value != null) { + r.put(name, value); + } + return; + } + if (event == XMLStreamReader.START_ELEMENT) { + final String elementName = reader.getLocalName(); + switch (elementName) { + case "name": + name = reader.getElementText(); + break; + case "value": + value = reader.getElementText(); + break; + default: + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.finest("CaseResult.parseProperty encountered an unknown field: " + elementName); + } + } + } + } + + } + static String possiblyTrimStdio(Collection results, StdioRetention stdioRetention, String stdio) { // HUDSON-6516 if (stdio == null) { return null; diff --git a/src/main/java/hudson/tasks/junit/SuiteResult.java b/src/main/java/hudson/tasks/junit/SuiteResult.java index c11919105..c729f203d 100644 --- a/src/main/java/hudson/tasks/junit/SuiteResult.java +++ b/src/main/java/hudson/tasks/junit/SuiteResult.java @@ -81,7 +81,7 @@ public final class SuiteResult implements Serializable { private String stderr; private float duration; private long startTime; - private final Map properties; + private Map properties; /** * The 'timestamp' attribute of the test suite. @@ -208,6 +208,10 @@ public static SuiteResult parse(final XMLStreamReader reader, String ver) throws case "stderr": r.stderr = reader.getElementText(); break; + case "properties": + r.properties = new HashMap<>(); + CaseResult.parseProperties(r.properties, reader, ver); + break; default: if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("SuiteResult.parse encountered an unknown field: " + elementName); From 31b862c023926407b81d38bbe7f45103db3151b4 Mon Sep 17 00:00:00 2001 From: Edgars Batna Date: Tue, 23 Jul 2024 15:58:28 +0200 Subject: [PATCH 80/80] Implement XML parsing for more fields (including properties). Implement related test. --- .../java/hudson/tasks/junit/CaseResult.java | 18 +- .../java/hudson/tasks/junit/SuiteResult.java | 5 +- .../java/hudson/tasks/junit/TestResult.java | 6 + .../hudson/tasks/junit/TestResultTest.java | 40 ++- .../hudson/tasks/junit/junit-report-huge.xml | 244 ++++++++++++++++++ 5 files changed, 298 insertions(+), 15 deletions(-) create mode 100644 src/test/resources/hudson/tasks/junit/junit-report-huge.xml diff --git a/src/main/java/hudson/tasks/junit/CaseResult.java b/src/main/java/hudson/tasks/junit/CaseResult.java index fdf1e33d8..9c4e8fa27 100644 --- a/src/main/java/hudson/tasks/junit/CaseResult.java +++ b/src/main/java/hudson/tasks/junit/CaseResult.java @@ -269,6 +269,9 @@ public static CaseResult parse(SuiteResult parent, final XMLStreamReader reader, case "duration": r.duration = clampDuration(new TimeToFloat(reader.getElementText()).parse()); break; + case "startTime": + r.startTime = Long.parseLong(reader.getElementText()); + break; case "className": r.className = reader.getElementText(); break; @@ -335,11 +338,11 @@ public static void parseProperties(Map r, final XMLStreamReader } public static void parseProperty(Map r, final XMLStreamReader reader, String ver) throws XMLStreamException { + String name = null, value = null; while (reader.hasNext()) { - String name = null, value = null; final int event = reader.next(); if (event == XMLStreamReader.END_ELEMENT && reader.getLocalName().equals("entry")) { - if (name != null || value != null) { + if (name != null && value != null) { r.put(name, value); } return; @@ -347,11 +350,12 @@ public static void parseProperty(Map r, final XMLStreamReader re if (event == XMLStreamReader.START_ELEMENT) { final String elementName = reader.getLocalName(); switch (elementName) { - case "name": - name = reader.getElementText(); - break; - case "value": - value = reader.getElementText(); + case "string": + if (name == null) { + name = reader.getElementText(); + } else { + value = reader.getElementText(); + } break; default: if (LOGGER.isLoggable(Level.FINEST)) { diff --git a/src/main/java/hudson/tasks/junit/SuiteResult.java b/src/main/java/hudson/tasks/junit/SuiteResult.java index c729f203d..8de90c8cd 100644 --- a/src/main/java/hudson/tasks/junit/SuiteResult.java +++ b/src/main/java/hudson/tasks/junit/SuiteResult.java @@ -163,7 +163,7 @@ public SuiteResult(SuiteResult src) { } public static SuiteResult parse(final XMLStreamReader reader, String ver) throws XMLStreamException { - SuiteResult r = new SuiteResult("", "", "", null); + SuiteResult r = new SuiteResult("", null, null, null); while (reader.hasNext()) { final int event = reader.next(); if (event == XMLStreamReader.END_ELEMENT && reader.getLocalName().equals("suite")) { @@ -187,6 +187,9 @@ public static SuiteResult parse(final XMLStreamReader reader, String ver) throws case "duration": r.duration = Math.max(0.0f, Math.min(365 * 24 * 60 * 60, new TimeToFloat(reader.getElementText()).parse())); break; + case "startTime": + r.startTime = Long.parseLong(reader.getElementText()); + break; case "timestamp": r.timestamp = reader.getElementText(); break; diff --git a/src/main/java/hudson/tasks/junit/TestResult.java b/src/main/java/hudson/tasks/junit/TestResult.java index c215e9077..9c633e5b0 100644 --- a/src/main/java/hudson/tasks/junit/TestResult.java +++ b/src/main/java/hudson/tasks/junit/TestResult.java @@ -324,6 +324,12 @@ private void parseXmlResult(final XMLStreamReader reader) throws XMLStreamExcept case "keepProperties": keepProperties = Boolean.parseBoolean(reader.getElementText()); break; + case "skipOldReports": + skipOldReports = Boolean.parseBoolean(reader.getElementText()); + break; + case "startTime": + startTime = Long.parseLong(reader.getElementText()); + break; default: if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("TestResult.parseXmlResult encountered an unknown field: " + elementName); diff --git a/src/test/java/hudson/tasks/junit/TestResultTest.java b/src/test/java/hudson/tasks/junit/TestResultTest.java index f3993a2c4..eec0fde91 100644 --- a/src/test/java/hudson/tasks/junit/TestResultTest.java +++ b/src/test/java/hudson/tasks/junit/TestResultTest.java @@ -35,14 +35,10 @@ import java.util.Collection; import java.util.List; -import org.apache.tools.ant.DirectoryScanner; -import org.junit.Test; +import org.apache.commons.io.FileUtils;import org.apache.tools.ant.DirectoryScanner; +import org.junit.Rule;import org.junit.Test; -import org.jvnet.hudson.test.Issue; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; +import org.junit.rules.TemporaryFolder;import org.jvnet.hudson.test.Issue;import static org.junit.Assert.*; /** * Tests the JUnit result XML file parsing in {@link TestResult}. @@ -50,6 +46,9 @@ * @author dty */ public class TestResultTest { + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + protected static File getDataFile(String name) throws URISyntaxException { return new File(TestResultTest.class.getResource(name).toURI()); } @@ -415,4 +414,31 @@ public void testStartTimes() throws Exception { } + /* + For performance reasons, we parse the XML directly. + Make sure parser handles all the fields. + */ + @Test + public void bigResultReadWrite() throws Exception { + List results = SuiteResult.parse(getDataFile("junit-report-huge.xml"), StdioRetention.ALL, true, true, null); + assertEquals(1, results.size()); + SuiteResult sr = results.get(0); + + TestResult tr = new TestResult(); + tr.getSuites().add(sr); + XmlFile f = new XmlFile(TestResultAction.XSTREAM, tmp.newFile("junitResult.xml")); + f.write(tr); + + TestResult tr2 = new TestResult(); + tr2.parse(f); + XmlFile f2 = new XmlFile(TestResultAction.XSTREAM, tmp.newFile("junitResult2.xml")); + f2.write(tr2); + + assertEquals(2, tr.getSuites().stream().findFirst().get().getProperties().size()); + assertEquals(2, tr2.getSuites().stream().findFirst().get().getProperties().size()); + + boolean isTwoEqual = FileUtils.contentEquals(f.getFile(), f2.getFile()); + assertTrue("Forgot to implement XML parsing for something?", isTwoEqual); + } + } diff --git a/src/test/resources/hudson/tasks/junit/junit-report-huge.xml b/src/test/resources/hudson/tasks/junit/junit-report-huge.xml new file mode 100644 index 000000000..19cde5f24 --- /dev/null +++ b/src/test/resources/hudson/tasks/junit/junit-report-huge.xml @@ -0,0 +1,244 @@ + + + + + Config line 1 + Config line 2 + Config line 3 + + + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + + + + + Config line 1 + Config line 2 + Config line 3 + + + This is stdout +[[ATTACHMENT|attachment.txt]] +This is stderr +[[ATTACHMENT|attachment.txt]] + \ No newline at end of file