Skip to content

Commit 002534a

Browse files
bhelekertimja
andauthored
Add start time fields and logic for setting start time of results in epoch milliseconds (#596)
Co-authored-by: Tim Jacomb <[email protected]>
1 parent 67297b3 commit 002534a

File tree

8 files changed

+262
-1
lines changed

8 files changed

+262
-1
lines changed

src/main/java/hudson/tasks/junit/CaseResult.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
public class CaseResult extends TestResult implements Comparable<CaseResult> {
6767
private static final Logger LOGGER = Logger.getLogger(CaseResult.class.getName());
6868
private final float duration;
69+
/**
70+
* Start time in epoch milliseconds - default is -1 for unset
71+
*/
72+
private long startTime;
6973
/**
7074
* In JUnit, a test is a method of a class. This field holds the fully qualified class name
7175
* that the test was in.
@@ -130,6 +134,7 @@ public CaseResult(SuiteResult parent, String testName, String errorStackTrace, S
130134
this.stdout = null;
131135
this.stderr = null;
132136
this.duration = 0.0f;
137+
this.startTime = -1;
133138
this.skipped = false;
134139
this.skippedMessage = null;
135140
this.properties = Collections.emptyMap();
@@ -155,6 +160,7 @@ public CaseResult(
155160
this.stdout = fixNULs(stdout);
156161
this.stderr = fixNULs(stderr);
157162
this.duration = duration;
163+
this.startTime = -1;
158164

159165
this.skipped = skippedMessage != null;
160166
this.skippedMessage = skippedMessage;
@@ -190,6 +196,7 @@ public CaseResult(
190196
errorDetails = getErrorMessage(testCase);
191197
this.parent = parent;
192198
duration = parseTime(testCase);
199+
this.startTime = -1;
193200
skipped = isMarkedAsSkipped(testCase);
194201
skippedMessage = getSkippedMessage(testCase);
195202
@SuppressWarnings("LeakingThisInConstructor")
@@ -382,6 +389,13 @@ public String getTitle() {
382389
public float getDuration() {
383390
return duration;
384391
}
392+
393+
/**
394+
* Gets the start time of the test, in epoch milliseconds
395+
*/
396+
public long getStartTime() {
397+
return startTime;
398+
}
385399

386400
/**
387401
* Gets the version of {@link #getName()} that's URL-safe.
@@ -784,6 +798,10 @@ public Status getStatus() {
784798
public void setClass(ClassResult classResult) {
785799
this.classResult = classResult;
786800
}
801+
802+
public void setStartTime(long start) {
803+
startTime = start;
804+
}
787805

788806
void replaceParent(SuiteResult parent) {
789807
this.parent = parent;

src/main/java/hudson/tasks/junit/ClassResult.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,15 @@ public final class ClassResult extends TabulatedResult implements Comparable<Cla
4949
private int passCount,failCount,skipCount;
5050

5151
private float duration;
52+
53+
private long startTime;
5254

5355
private final PackageResult parent;
5456

5557
public ClassResult(PackageResult parent, String className) {
5658
this.parent = parent;
5759
this.className = className;
60+
this.startTime = -1;
5861
}
5962

6063
@Override
@@ -158,6 +161,10 @@ public float getDuration() {
158161
return duration;
159162
}
160163

164+
public long getStartTime() {
165+
return startTime;
166+
}
167+
161168
@Exported
162169
@Override
163170
public int getPassCount() {
@@ -177,6 +184,11 @@ public int getSkipCount() {
177184
}
178185

179186
public void add(CaseResult r) {
187+
if (startTime == -1) {
188+
startTime = r.getStartTime();
189+
} else if (r.getStartTime() != -1) {
190+
startTime = Math.min(startTime, r.getStartTime());
191+
}
180192
cases.add(r);
181193
}
182194

@@ -267,6 +279,10 @@ public String getRelativePathFrom(TestObject it) {
267279
return super.getRelativePathFrom(it);
268280
}
269281
}
282+
283+
public void setStartTime(long start) {
284+
this.startTime = start;
285+
}
270286

271287
private static final long serialVersionUID = 1L;
272288
}

src/main/java/hudson/tasks/junit/PackageResult.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ public final class PackageResult extends MetaTabulatedResult implements Comparab
4949
private int passCount,failCount,skipCount;
5050
private final hudson.tasks.junit.TestResult parent;
5151
private float duration;
52+
private long startTime;
5253

5354
public PackageResult(hudson.tasks.junit.TestResult parent, String packageName) {
5455
this.packageName = packageName;
5556
this.parent = parent;
57+
this.startTime = -1;
5658
}
5759

5860
@Override
@@ -132,6 +134,10 @@ public float getDuration() {
132134
return duration;
133135
}
134136

137+
public long getStartTime() {
138+
return startTime;
139+
}
140+
135141
@Exported
136142
@Override
137143
public int getPassCount() {
@@ -354,6 +360,10 @@ public int hashCode() {
354360
public String getDisplayName() {
355361
return TestNameTransformer.getTransformedName(packageName);
356362
}
363+
364+
public void setStartTime(long time) {
365+
startTime = time;
366+
}
357367

358368
private static final long serialVersionUID = 1L;
359369
}

src/main/java/hudson/tasks/junit/SuiteResult.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.io.FileInputStream;
4242
import java.io.IOException;
4343
import java.io.Serializable;
44+
import java.time.OffsetDateTime;
4445
import java.util.ArrayList;
4546
import java.util.Collections;
4647
import java.util.HashMap;
@@ -73,6 +74,7 @@ public final class SuiteResult implements Serializable {
7374
private final String stdout;
7475
private final String stderr;
7576
private float duration;
77+
private long startTime;
7678
private final Map<String, String> properties;
7779

7880
/**
@@ -242,6 +244,14 @@ private SuiteResult(File xmlReport, Element suite, boolean keepLongStdio, boolea
242244
this.enclosingBlocks.addAll(pipelineTestDetails.getEnclosingBlocks());
243245
this.enclosingBlockNames.addAll(pipelineTestDetails.getEnclosingBlockNames());
244246
}
247+
248+
// check for timestamp attribute and set start time if present
249+
if (timestamp != null && !timestamp.equals("")) {
250+
this.startTime = parseTime(timestamp);
251+
}
252+
else {
253+
this.startTime = -1;
254+
}
245255

246256
// check for test suite time attribute
247257
if ((this.time = suite.attributeValue("time")) != null) {
@@ -254,6 +264,8 @@ private SuiteResult(File xmlReport, Element suite, boolean keepLongStdio, boolea
254264
addCase(new CaseResult(this, suite, "<init>", keepLongStdio, keepProperties));
255265
}
256266

267+
// offset for start time of cases if none is case timestamp is not specified
268+
long caseStartOffset = 0;
257269
List<Element> testCases = suite.elements("testcase");
258270
for (Element e : testCases) {
259271
// https://issues.jenkins-ci.org/browse/JENKINS-1233 indicates that
@@ -274,8 +286,20 @@ private SuiteResult(File xmlReport, Element suite, boolean keepLongStdio, boolea
274286
// are at odds with each other --- when both are present,
275287
// one wants to use @name from <testsuite>,
276288
// the other wants to use @classname from <testcase>.
289+
290+
CaseResult caze = new CaseResult(this, e, classname, keepLongStdio, keepProperties);
277291

278-
addCase(new CaseResult(this, e, classname, keepLongStdio, keepProperties));
292+
// If timestamp is present for <testcase> set startTime of new CaseResult.
293+
String caseStart = e.attributeValue("timestamp");
294+
if (caseStart != null && !caseStart.equals("")) {
295+
caze.setStartTime(parseTime(caseStart));
296+
}
297+
// Else estimate start time using sum of previous case durations in suite
298+
else if (startTime != -1) {
299+
caze.setStartTime(startTime + caseStartOffset);
300+
caseStartOffset += (long)(caze.getDuration() * 1000);
301+
}
302+
addCase(caze);
279303
}
280304

281305
String stdout = CaseResult.possiblyTrimStdio(cases, keepLongStdio, suite.elementText("system-out"));
@@ -437,6 +461,14 @@ public hudson.tasks.junit.TestResult getParent() {
437461
public String getTimestamp() {
438462
return timestamp;
439463
}
464+
465+
public long getStartTime() {
466+
return startTime;
467+
}
468+
469+
public void setStartTime(long start) {
470+
this.startTime = start;
471+
}
440472

441473
@Exported(visibility=9)
442474
public String getId() {
@@ -495,6 +527,25 @@ public void setParent(hudson.tasks.junit.TestResult parent) {
495527
c.freeze(this);
496528
return true;
497529
}
530+
531+
/**
532+
* Parses time as epoch milli from time string
533+
* @param time
534+
* @return time in epoch milli
535+
*/
536+
public long parseTime(String time) {
537+
try {
538+
// if time is not in supported format due to missing zulu and offset
539+
if (time.charAt(time.length() - 1) != 'Z' && !time.contains("+") && time.lastIndexOf("-") <= 7)
540+
time += 'Z';
541+
OffsetDateTime odt = OffsetDateTime.parse(time.replace(" ", ""));
542+
return odt.toInstant().toEpochMilli();
543+
} catch (Exception e) {
544+
// If time format causes error in parsing print message and return -1
545+
LOGGER.warning("Could not parse start time from timestamp.");
546+
return -1;
547+
}
548+
}
498549

499550
private static final long serialVersionUID = 1L;
500551

src/main/java/hudson/tasks/junit/TestResult.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ public final class TestResult extends MetaTabulatedResult {
114114
private float duration;
115115

116116
private boolean skipOldReports;
117+
118+
private long startTime = -1;
117119

118120
/**
119121
* Number of failed/error leafNodes.
@@ -322,6 +324,7 @@ private void parsePossiblyEmpty(File reportFile, PipelineTestDetails pipelineTes
322324
}
323325

324326
private void add(SuiteResult sr) {
327+
long suiteStart = sr.getStartTime();
325328
for (SuiteResult s : suites) {
326329
// JENKINS-12457: If a testsuite is distributed over multiple files, merge it into a single SuiteResult:
327330
if(s.getName().equals(sr.getName()) &&
@@ -330,12 +333,26 @@ private void add(SuiteResult sr) {
330333
nullSafeEq(s.getEnclosingBlocks(),sr.getEnclosingBlocks()) &&
331334
nullSafeEq(s.getEnclosingBlockNames(),sr.getEnclosingBlockNames())) {
332335

336+
// Set start time to earliest set start of a suite
337+
if (startTime == -1) {
338+
startTime = suiteStart;
339+
} else if (suiteStart != -1){
340+
startTime = Math.min(startTime, suiteStart);
341+
}
342+
333343
duration += sr.getDuration();
334344
s.merge(sr);
335345
return;
336346
}
337347
}
338348

349+
// Set start time to earliest set start of a suite
350+
if (startTime == -1) {
351+
startTime = suiteStart;
352+
} else if (suiteStart != -1){
353+
startTime = Math.min(startTime, suiteStart);
354+
}
355+
339356
suites.add(sr);
340357
duration += sr.getDuration();
341358
}
@@ -499,6 +516,14 @@ public float getDuration() {
499516

500517
return duration;
501518
}
519+
520+
public void setStartTime(long start) {
521+
startTime = start;
522+
}
523+
524+
public long getStartTime() {
525+
return startTime;
526+
}
502527

503528
@Exported(visibility=999)
504529
@Override
@@ -845,6 +870,12 @@ public void tally() {
845870
PackageResult pr = byPackage(spkg);
846871
if(pr==null)
847872
byPackages.put(spkg,pr=new PackageResult(this,pkg));
873+
874+
if (pr.getStartTime() == -1) {
875+
pr.setStartTime(s.getStartTime());
876+
} else if (s.getStartTime() != -1){
877+
pr.setStartTime(Math.min(pr.getStartTime(), s.getStartTime()));
878+
}
848879
pr.add(cr);
849880
}
850881
}
@@ -909,6 +940,12 @@ public void freeze(TestResultAction parent) {
909940
PackageResult pr = byPackage(spkg);
910941
if(pr==null)
911942
byPackages.put(spkg,pr=new PackageResult(this,pkg));
943+
944+
if (pr.getStartTime() == -1) {
945+
pr.setStartTime(s.getStartTime());
946+
} else if (s.getStartTime() != -1){
947+
pr.setStartTime(Math.min(pr.getStartTime(), s.getStartTime()));
948+
}
912949
pr.add(cr);
913950
}
914951
}

src/test/java/hudson/tasks/junit/SuiteResultTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,23 @@ public void testTestSuiteTimeAttribute() throws Exception {
360360
assertEquals(20.0f, results.get(3).getDuration(), 2); //sum of test cases time
361361
}
362362

363+
@Test
364+
public void testTestParseTimeMethod() throws Exception {
365+
// Tests parseTime() with various valid and invalid datetimes
366+
SuiteResult emptyResult = new SuiteResult("Test parseTime", "", "", null);
367+
assertEquals(0, emptyResult.parseTime("1970-01-01T00:00:00.00"));
368+
assertEquals(1704280980000L, emptyResult.parseTime("2024-01-03T11:23:00.00"));
369+
assertEquals(1704284831000L, emptyResult.parseTime("2024-01-03T12:27:11"));
370+
assertEquals(1704285613000L, emptyResult.parseTime("2024-01-03T 12:40:13"));
371+
assertEquals(1704284864000L, emptyResult.parseTime("2024-01-03T12:27:44Z"));
372+
assertEquals(1704281235000L, emptyResult.parseTime("2024-01-03T12:27:15+01:00"));
373+
assertEquals(1704288431210L, emptyResult.parseTime("2024-01-03T12:27:11.21-01:00"));
374+
assertEquals(-1, emptyResult.parseTime("2024-01-03T12:27:11.21+1:00"));
375+
assertEquals(-1, emptyResult.parseTime("2024-01-03 12:27:54Z"));
376+
assertEquals(-1, emptyResult.parseTime("2024-01-03"));
377+
assertEquals(-1, emptyResult.parseTime(""));
378+
}
379+
363380
@Test
364381
public void testProperties() throws Exception {
365382
SuiteResult sr = parseOneWithProperties(getDataFile("junit-report-with-properties.xml"));

0 commit comments

Comments
 (0)