Skip to content

Commit dad93ce

Browse files
authored
Merge pull request #965 from cqse/ts/46357_no_container_found
TS-46357 Collect the tests of a `@ParameterizedClass`
2 parents 616d62d + fd4bb9a commit dad93ce

32 files changed

Lines changed: 1169 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ We use [semantic versioning](http://semver.org/):
55
- PATCH version when you make backwards compatible bug fixes.
66

77
# Next version
8+
- [fix] _impacted-test-engine_: Tests in a `@ParameterizedClass` are now collected.
9+
- [fix] _impacted-test-engine_: A test failure inside a nested test container (e.g. an invocation of a `@ParameterizedClass`) is no longer swallowed.
10+
- [fix] _teamscale-jacoco-agent_, _teamscale-maven-plugin_: A test that was executed more than once (e.g. once per parameter set of a `@ParameterizedClass`) is now reported once in the testwise coverage report, with the coverage of all of its executions merged, their durations summed up and the most severe of their results. Previously each execution overwrote the previous one.
811

912
# 38.1.0
1013
- [feature] _agent_: `git-properties-jar` now also accepts a folder, which is searched for `git.properties` files. Previously, a folder was rejected with a warning and no commit was auto-detected.

agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/Converter.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,7 @@ class Converter
9494
arguments.getOutputFile(),
9595
arguments.splitAfter, null
9696
).use { coverageWriter ->
97-
jacocoExecutionDataList.forEach { executionDataFile ->
98-
generator.convertAndConsume(executionDataFile, coverageWriter)
99-
}
97+
generator.convertAndConsumePerTest(jacocoExecutionDataList, coverageWriter)
10098
}
10199
}
102100
}

agent/src/main/kotlin/com/teamscale/jacoco/agent/testimpact/CoverageViaHttpStrategy.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ class CoverageViaHttpStrategy(
2828
val builder = TestInfoBuilder(test)
2929
val dump = controller.dumpAndReset()
3030
reportGenerator.updateClassDirCache()
31-
reportGenerator.convert(dump)?.let { builder.setCoverage(it) }
32-
builder.setExecution(testExecution)
31+
reportGenerator.convert(dump)?.let { builder.addCoverage(it) }
32+
builder.addExecution(testExecution)
3333
val testInfo = builder.build()
3434
logger.debug("Generated test info {}", testInfo)
3535
return testInfo

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ log4j-core = { module = "org.apache.logging.log4j:log4j-core", version = "2.26.1
5757
junit-bom = { module = "org.junit:junit-bom", version = "6.1.3" }
5858
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
5959
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" }
60+
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" }
6061
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
6162
junit-platform-engine = { module = "org.junit.platform:junit-platform-engine" }
6263
junit-platform-commons = { module = "org.junit.platform:junit-platform-commons" }

impacted-test-engine/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ dependencies {
3232
compileOnly(libs.junit.platform.engine)
3333
compileOnly(libs.junit.platform.commons)
3434
testImplementation(libs.junit.platform.engine)
35+
testImplementation(libs.junit.platform.launcher)
3536
testImplementation(libs.junit.jupiter.params)
37+
testImplementation(libs.junit.jupiter.engine)
3638
testImplementation(libs.mockito.kotlin)
3739
}
40+
41+
tasks.test {
42+
// The sample tests are discovered explicitly by JupiterClassTemplateTest and must not be run by Gradle itself.
43+
exclude("**/test_descriptor/samples/**")
44+
}

impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.teamscale.test_impacted.engine
33
import com.teamscale.test_impacted.commons.LoggerUtils.createLogger
44
import com.teamscale.test_impacted.engine.ImpactedTestEngine.Companion.ENGINE_NAME
55
import com.teamscale.test_impacted.engine.executor.TestwiseCoverageCollectingExecutionListener
6+
import com.teamscale.test_impacted.test_descriptor.ClassTemplateRegistry
67
import com.teamscale.test_impacted.test_descriptor.TestDescriptorResolverRegistry.getTestDescriptorResolver
78
import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getAvailableTests
89
import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getTestDescriptorAsString
@@ -30,6 +31,12 @@ internal class InternalImpactedTestEngine(
3031
private val teamscaleAgentNotifier = configuration.teamscaleAgentNotifier
3132
private val testDataWriter = configuration.testDataWriter
3233

34+
/**
35+
* The tests of the `@ParameterizedClass`es in the test tree, recorded during discovery because the JUnit platform
36+
* prunes them from the tree before the tests are executed.
37+
*/
38+
private val classTemplateRegistry = ClassTemplateRegistry()
39+
3340
/**
3441
* Performs test discovery by aggregating the result of all [TestEngine]s from the [TestEngineRegistry]
3542
* in a single engine [TestDescriptor].
@@ -47,6 +54,7 @@ internal class InternalImpactedTestEngine(
4754
)
4855

4956
engineDescriptor.addChild(delegateEngineDescriptor)
57+
classTemplateRegistry.record(delegateEngineDescriptor)
5058
}
5159

5260
LOG.fine {
@@ -64,15 +72,15 @@ internal class InternalImpactedTestEngine(
6472
*/
6573
fun execute(request: ExecutionRequest) {
6674
val rootTestDescriptor = request.rootTestDescriptor
67-
val availableTests = getAvailableTests(rootTestDescriptor)
75+
val availableTests = getAvailableTests(rootTestDescriptor, classTemplateRegistry)
6876

6977
LOG.fine {
7078
"Starting selection and sorting ${ImpactedTestEngine.ENGINE_ID}:\n${
7179
getTestDescriptorAsString(rootTestDescriptor)
7280
}"
7381
}
7482

75-
testSorter.selectAndSort(rootTestDescriptor)
83+
testSorter.selectAndSort(rootTestDescriptor, availableTests)
7684

7785
LOG.fine {
7886
"Starting execution of request for engine ${ImpactedTestEngine.ENGINE_ID}:\n${
@@ -105,7 +113,8 @@ internal class InternalImpactedTestEngine(
105113
TestwiseCoverageCollectingExecutionListener(
106114
teamscaleAgentNotifier,
107115
testDescriptorResolver,
108-
request.engineExecutionListener
116+
request.engineExecutionListener,
117+
classTemplateRegistry
109118
)
110119

111120
testEngine.execute(

impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ITestSorter.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ interface ITestSorter {
77
/**
88
* Removes any tests from the test descriptor that should not be executed and changes the execution order of the
99
* remaining tests.
10+
*
11+
* @param availableTests The tests contained in the given [testDescriptor], as determined during discovery.
1012
*/
11-
fun selectAndSort(testDescriptor: TestDescriptor)
13+
fun selectAndSort(testDescriptor: TestDescriptor, availableTests: AvailableTests)
1214
}

impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.teamscale.test_impacted.engine.executor
22

33
import com.teamscale.client.TestWithClusterId.Companion.fromClusteredTestDetails
44
import com.teamscale.test_impacted.engine.ImpactedTestEngine
5-
import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getAvailableTests
65
import org.junit.platform.engine.TestDescriptor
76
import java.util.*
87

@@ -12,9 +11,7 @@ import java.util.*
1211
*/
1312
class ImpactedTestsSorter(private val impactedTestsProvider: ImpactedTestsProvider) : ITestSorter {
1413

15-
override fun selectAndSort(testDescriptor: TestDescriptor) {
16-
val availableTests = getAvailableTests(testDescriptor)
17-
14+
override fun selectAndSort(testDescriptor: TestDescriptor, availableTests: AvailableTests) {
1815
val testClusters = impactedTestsProvider.getImpactedTestsFromTeamscale(
1916
availableTests.testList
2017
.map { fromClusteredTestDetails(it, impactedTestsProvider.partition) })

impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/NOPTestSorter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import org.junit.platform.engine.TestDescriptor
77
* Teamscale to select or prioritize tests.
88
*/
99
class NOPTestSorter : ITestSorter {
10-
override fun selectAndSort(testDescriptor: TestDescriptor) {
10+
override fun selectAndSort(testDescriptor: TestDescriptor, availableTests: AvailableTests) {
1111
// Nothing to do
1212
}
1313
}

impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListener.kt

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package com.teamscale.test_impacted.engine.executor
33
import com.teamscale.report.testwise.model.ETestExecutionResult
44
import com.teamscale.report.testwise.model.TestExecution
55
import com.teamscale.test_impacted.commons.LoggerUtils.createLogger
6+
import com.teamscale.test_impacted.test_descriptor.ClassTemplateRegistry
67
import com.teamscale.test_impacted.test_descriptor.ITestDescriptorResolver
8+
import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.isClassTemplate
79
import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.isRepresentative
810
import org.junit.platform.engine.EngineExecutionListener
911
import org.junit.platform.engine.TestDescriptor
@@ -24,11 +26,13 @@ import java.io.StringWriter
2426
* @param teamscaleAgentNotifier The notifier responsible for signaling test events to the Teamscale JaCoCo agent.
2527
* @param testDescriptorResolver A resolver interface used to map [TestDescriptor] objects to uniform paths.
2628
* @param delegateEngineExecutionListener The underlying [EngineExecutionListener] to which events are delegated.
29+
* @param classTemplateRegistry The tests of the `@ParameterizedClass`es as recorded during discovery.
2730
*/
2831
class TestwiseCoverageCollectingExecutionListener(
2932
private val teamscaleAgentNotifier: TeamscaleAgentNotifier,
3033
private val testDescriptorResolver: ITestDescriptorResolver,
31-
private val delegateEngineExecutionListener: EngineExecutionListener
34+
private val delegateEngineExecutionListener: EngineExecutionListener,
35+
private val classTemplateRegistry: ClassTemplateRegistry
3236
) : EngineExecutionListener {
3337
companion object {
3438
private val LOG = createLogger()
@@ -47,6 +51,14 @@ class TestwiseCoverageCollectingExecutionListener(
4751
}
4852

4953
override fun executionSkipped(testDescriptor: TestDescriptor, reason: String) {
54+
if (testDescriptor.isClassTemplate()) {
55+
// The tests of a @ParameterizedClass were pruned from the test tree, so report the ones that were
56+
// recorded during discovery instead of descending into the now empty descriptor. They are not forwarded
57+
// to the delegate listener, which only knows the descriptors that are still part of the tree.
58+
testDescriptor.reportSkipped(reason)
59+
delegateEngineExecutionListener.executionSkipped(testDescriptor, reason)
60+
return
61+
}
5062
if (!testDescriptor.isRepresentative()) {
5163
delegateEngineExecutionListener.executionStarted(testDescriptor)
5264
testDescriptor.children.forEach { executionSkipped(it, reason) }
@@ -67,6 +79,18 @@ class TestwiseCoverageCollectingExecutionListener(
6779
}
6880
}
6981

82+
/** Records a [ETestExecutionResult.SKIPPED] execution for every test below the given descriptor. */
83+
private fun TestDescriptor.reportSkipped(reason: String) {
84+
if (isRepresentative()) {
85+
testDescriptorResolver.getUniformPath(this)?.let { testUniformPath ->
86+
testExecutions.add(TestExecution(testUniformPath, 0L, ETestExecutionResult.SKIPPED, reason))
87+
}
88+
return
89+
}
90+
val tests = if (isClassTemplate()) classTemplateRegistry.testsOf(this) else children
91+
tests.forEach { it.reportSkipped(reason) }
92+
}
93+
7094
override fun executionStarted(testDescriptor: TestDescriptor) {
7195
if (testDescriptor.isRepresentative()) {
7296
testDescriptorResolver.getUniformPath(testDescriptor)?.let { testUniformPath ->
@@ -90,6 +114,10 @@ class TestwiseCoverageCollectingExecutionListener(
90114
val testExecutionResults = testResultCache.computeIfAbsent(
91115
testDescriptor.parent.get().uniqueId
92116
) { mutableListOf() }
117+
// Containers may be nested arbitrarily deep below their representative, e.g. a @ParameterizedClass
118+
// contains one invocation per parameter set which in turn contains the test methods. Hand the results
119+
// collected for this container up to its parent so that they reach the representative.
120+
testResultCache.remove(testDescriptor.uniqueId)?.let { testExecutionResults.addAll(it) }
93121
testExecutionResults.add(testExecutionResult)
94122
}
95123

@@ -108,14 +136,16 @@ class TestwiseCoverageCollectingExecutionListener(
108136
val message = StringBuilder()
109137
var status = TestExecutionResult.Status.SUCCESSFUL
110138
testExecutionResults.forEach { executionResult ->
111-
if (message.isNotEmpty()) {
112-
message.append("\n\n")
113-
}
114-
message.append(executionResult.throwable.orElse(null).buildStacktrace())
115139
// Aggregate status here to most severe status according to SUCCESSFUL < ABORTED < FAILED
116140
if (status.ordinal < executionResult.status.ordinal) {
117141
status = executionResult.status
118142
}
143+
144+
val stacktrace = executionResult.throwable.orElse(null).buildStacktrace() ?: return@forEach
145+
if (message.isNotEmpty()) {
146+
message.append("\n\n")
147+
}
148+
message.append(stacktrace)
119149
}
120150

121151
return buildTestExecution(testUniformPath, duration, status, message.toString())

0 commit comments

Comments
 (0)