Skip to content

Commit 7aad28f

Browse files
committed
add testcase statistics update to parsing test results
1 parent b1652e6 commit 7aad28f

File tree

3 files changed

+246
-1
lines changed

3 files changed

+246
-1
lines changed

server/application-server/src/main/java/de/tum/cit/aet/helios/tests/TestCase.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import jakarta.persistence.Id;
1111
import jakarta.persistence.JoinColumn;
1212
import jakarta.persistence.ManyToOne;
13+
import jakarta.persistence.Transient;
1314
import lombok.Getter;
1415
import lombok.NoArgsConstructor;
1516
import lombok.Setter;
@@ -58,6 +59,18 @@ public void setClassName(String className) {
5859
@Column(name = "error_type")
5960
private String errorType;
6061

62+
/**
63+
* Transient field indicating whether this test is flaky. Determined based on test case
64+
* statistics.
65+
*/
66+
@Transient private boolean isFlaky;
67+
68+
/**
69+
* Transient field for the failure rate of this test on its branch. Retrieved from test case
70+
* statistics.
71+
*/
72+
@Transient private double failureRate;
73+
6174
public static enum TestStatus {
6275
PASSED,
6376
FAILED,
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package de.tum.cit.aet.helios.tests;
2+
3+
import de.tum.cit.aet.helios.branch.Branch;
4+
import de.tum.cit.aet.helios.branch.BranchRepository;
5+
import de.tum.cit.aet.helios.gitrepo.GitRepository;
6+
import jakarta.transaction.Transactional;
7+
import java.time.OffsetDateTime;
8+
import java.util.List;
9+
import java.util.Optional;
10+
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.log4j.Log4j2;
12+
import org.springframework.stereotype.Service;
13+
14+
/** Service for managing test case statistics and detecting flaky tests. */
15+
@Service
16+
@Log4j2
17+
@RequiredArgsConstructor
18+
public class TestCaseStatisticsService {
19+
20+
private final TestCaseStatisticsRepository statisticsRepository;
21+
private final BranchRepository branchRepository;
22+
23+
/**
24+
* Updates statistics for a test case, creating a new entry if it doesn't exist.
25+
*
26+
* @param testName the name of the test
27+
* @param className the class name of the test
28+
* @param testSuiteName the test suite name
29+
* @param branchName the branch name
30+
* @param hasFailed whether the test failed in this run
31+
* @return the updated statistics
32+
*/
33+
@Transactional
34+
public TestCaseStatistics updateStatistics(
35+
String testName,
36+
String className,
37+
String testSuiteName,
38+
String branchName,
39+
boolean hasFailed) {
40+
Optional<TestCaseStatistics> existingStats =
41+
statisticsRepository.findByTestNameAndClassNameAndTestSuiteNameAndBranchName(
42+
testName, className, testSuiteName, branchName);
43+
44+
TestCaseStatistics statistics;
45+
if (existingStats.isPresent()) {
46+
statistics = existingStats.get();
47+
} else {
48+
statistics = new TestCaseStatistics();
49+
statistics.setTestName(testName);
50+
statistics.setClassName(className);
51+
statistics.setTestSuiteName(testSuiteName);
52+
statistics.setBranchName(branchName);
53+
statistics.setTotalRuns(0);
54+
statistics.setFailedRuns(0);
55+
statistics.setFailureRate(0.0);
56+
statistics.setFlaky(false);
57+
statistics.setLastUpdated(OffsetDateTime.now());
58+
}
59+
60+
statistics.addRun(hasFailed);
61+
return statisticsRepository.save(statistics);
62+
}
63+
64+
/**
65+
* Updates statistics for multiple test cases from a test suite.
66+
*
67+
* @param testSuite the test suite containing test cases
68+
* @param branchName the branch name
69+
*/
70+
@Transactional
71+
public void updateStatisticsForTestSuite(TestSuite testSuite, String branchName) {
72+
String testSuiteName = testSuite.getName();
73+
74+
for (TestCase testCase : testSuite.getTestCases()) {
75+
boolean hasFailed =
76+
testCase.getStatus() == TestCase.TestStatus.FAILED
77+
|| testCase.getStatus() == TestCase.TestStatus.ERROR;
78+
79+
updateStatistics(
80+
testCase.getName(), testCase.getClassName(), testSuiteName, branchName, hasFailed);
81+
}
82+
}
83+
84+
/**
85+
* Gets statistics for a specific test case on a specific branch.
86+
*
87+
* @param testName the name of the test
88+
* @param className the class name of the test
89+
* @param testSuiteName the test suite name
90+
* @param branchName the branch name
91+
* @return the statistics if found
92+
*/
93+
public Optional<TestCaseStatistics> getStatistics(
94+
String testName, String className, String testSuiteName, String branchName) {
95+
return statisticsRepository.findByTestNameAndClassNameAndTestSuiteNameAndBranchName(
96+
testName, className, testSuiteName, branchName);
97+
}
98+
99+
/**
100+
* Gets all statistics for a specific branch.
101+
*
102+
* @param branchName the branch name
103+
* @return list of statistics for all tests on the branch
104+
*/
105+
public List<TestCaseStatistics> getStatisticsForBranch(String branchName) {
106+
return statisticsRepository.findByBranchName(branchName);
107+
}
108+
109+
/**
110+
* Gets flaky or non-flaky tests for a specific branch.
111+
*
112+
* @param branchName the branch name
113+
* @param isFlaky whether to find flaky or non-flaky tests
114+
* @return list of flaky or non-flaky test statistics
115+
*/
116+
public List<TestCaseStatistics> getTestsByFlakinessForBranch(String branchName, boolean isFlaky) {
117+
return statisticsRepository.findByBranchNameAndIsFlaky(branchName, isFlaky);
118+
}
119+
120+
/**
121+
* Gets flaky or non-flaky tests for the default branch of a repository.
122+
*
123+
* @param repository the repository
124+
* @param isFlaky whether to find flaky or non-flaky tests
125+
* @return list of flaky or non-flaky test statistics for the default branch
126+
*/
127+
public List<TestCaseStatistics> getTestsByFlakinessForDefaultBranch(
128+
GitRepository repository, boolean isFlaky) {
129+
Optional<Branch> defaultBranch =
130+
branchRepository.findAll().stream()
131+
.filter(branch -> branch.getRepository().equals(repository) && branch.isDefault())
132+
.findFirst();
133+
134+
if (defaultBranch.isPresent()) {
135+
return getTestsByFlakinessForBranch(defaultBranch.get().getName(), isFlaky);
136+
} else {
137+
log.warn("No default branch found for repository {}", repository.getNameWithOwner());
138+
return List.of();
139+
}
140+
}
141+
142+
/**
143+
* Gets all statistics for the default branch of a repository.
144+
*
145+
* @param repository the repository
146+
* @return list of statistics for all tests on the default branch
147+
*/
148+
public List<TestCaseStatistics> getStatisticsForDefaultBranch(GitRepository repository) {
149+
Optional<Branch> defaultBranch =
150+
branchRepository.findAll().stream()
151+
.filter(branch -> branch.getRepository().equals(repository) && branch.isDefault())
152+
.findFirst();
153+
154+
if (defaultBranch.isPresent()) {
155+
return getStatisticsForBranch(defaultBranch.get().getName());
156+
} else {
157+
log.warn("No default branch found for repository {}", repository.getNameWithOwner());
158+
return List.of();
159+
}
160+
}
161+
}

server/application-server/src/main/java/de/tum/cit/aet/helios/tests/TestResultProcessor.java

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package de.tum.cit.aet.helios.tests;
22

33
import de.tum.cit.aet.helios.github.GitHubService;
4+
import de.tum.cit.aet.helios.gitrepo.GitRepoRepository;
5+
import de.tum.cit.aet.helios.gitrepo.GitRepository;
46
import de.tum.cit.aet.helios.tests.parsers.JunitParser;
57
import de.tum.cit.aet.helios.tests.parsers.TestResultParseException;
68
import de.tum.cit.aet.helios.tests.parsers.TestResultParser;
@@ -12,6 +14,7 @@
1214
import java.time.OffsetDateTime;
1315
import java.util.ArrayList;
1416
import java.util.List;
17+
import java.util.Optional;
1518
import java.util.zip.ZipEntry;
1619
import java.util.zip.ZipInputStream;
1720
import lombok.RequiredArgsConstructor;
@@ -21,14 +24,17 @@
2124
import org.springframework.beans.factory.annotation.Value;
2225
import org.springframework.scheduling.annotation.Async;
2326
import org.springframework.stereotype.Service;
27+
import org.springframework.transaction.annotation.Transactional;
2428

2529
@Service
2630
@RequiredArgsConstructor
2731
@Log4j2
2832
public class TestResultProcessor {
2933
private final GitHubService gitHubService;
3034
private final WorkflowRunRepository workflowRunRepository;
35+
private final GitRepoRepository gitRepoRepository;
3136
private final JunitParser junitParser;
37+
private final TestCaseStatisticsService statisticsService;
3238

3339
@Value("${tests.artifactName:JUnit Test Results}")
3440
private String testArtifactName;
@@ -72,11 +78,15 @@ public void processRun(WorkflowRun workflowRun) {
7278
this.workflowRunRepository.save(workflowRun);
7379

7480
try {
75-
workflowRun.setTestSuites(this.processRunSync(workflowRun));
81+
List<TestSuite> testSuites = this.processRunSync(workflowRun);
82+
workflowRun.setTestSuites(testSuites);
7683
workflowRun.setTestProcessingStatus(WorkflowRun.TestProcessingStatus.PROCESSED);
7784
log.debug(
7885
"Successfully persisted test results for workflow run, workflow name: {}",
7986
workflowRun.getName());
87+
88+
// Update test statistics if the workflow run is on the default branch
89+
updateTestStatisticsIfDefaultBranch(testSuites, workflowRun);
8090
} catch (Exception e) {
8191
log.error("Failed to process test results for workflow run {}", workflowRun.getName(), e);
8292
workflowRun.setTestProcessingStatus(WorkflowRun.TestProcessingStatus.FAILED);
@@ -215,4 +225,65 @@ public void close() throws IOException {
215225
return results;
216226
});
217227
}
228+
229+
/**
230+
* Updates test statistics if the workflow run is on the default branch. This method safely
231+
* retrieves the repository and default branch information.
232+
*
233+
* @param testSuites the test suites containing test cases
234+
* @param workflowRun the workflow run
235+
*/
236+
@Transactional
237+
protected void updateTestStatisticsIfDefaultBranch(
238+
List<TestSuite> testSuites, WorkflowRun workflowRun) {
239+
try {
240+
String headBranch = workflowRun.getHeadBranch();
241+
if (headBranch == null) {
242+
log.debug("Skipping test statistics update: head branch is null");
243+
return;
244+
}
245+
246+
// Safely retrieve repository with its properties in a transaction
247+
Optional<GitRepository> repository =
248+
gitRepoRepository.findById(workflowRun.getRepository().getRepositoryId());
249+
250+
if (repository.isEmpty()) {
251+
log.debug("Skipping test statistics update: repository not found");
252+
return;
253+
}
254+
255+
String defaultBranch = repository.get().getDefaultBranch();
256+
257+
if (headBranch.equals(defaultBranch)) {
258+
log.debug("Updating test statistics for default branch: {}", headBranch);
259+
updateTestStatistics(testSuites, headBranch);
260+
} else {
261+
log.debug(
262+
"Skipping test statistics update for non-default branch: {}, default branch: {}",
263+
headBranch,
264+
defaultBranch);
265+
}
266+
} catch (Exception e) {
267+
log.error("Error while trying to update test statistics", e);
268+
// Don't fail the overall process if statistics update fails
269+
}
270+
}
271+
272+
/**
273+
* Updates test case statistics for all test cases in the given test suites.
274+
*
275+
* @param testSuites the test suites containing test cases
276+
* @param branchName the branch name where the tests were run
277+
*/
278+
private void updateTestStatistics(List<TestSuite> testSuites, String branchName) {
279+
try {
280+
for (TestSuite testSuite : testSuites) {
281+
statisticsService.updateStatisticsForTestSuite(testSuite, branchName);
282+
}
283+
log.debug("Successfully updated test statistics for branch: {}", branchName);
284+
} catch (Exception e) {
285+
log.error("Failed to update test statistics for branch: {}", branchName, e);
286+
// Don't fail the overall process if statistics update fails
287+
}
288+
}
218289
}

0 commit comments

Comments
 (0)