diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.java deleted file mode 100644 index 3e3661f1d..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.teamscale.test_impacted.engine.executor; - -import com.teamscale.client.PrioritizableTest; -import com.teamscale.client.PrioritizableTestCluster; -import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.UniqueId; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static com.teamscale.test_impacted.engine.ImpactedTestEngine.LOGGER; - -/** - * Test sorter that requests impacted tests from Teamscale and rewrites the {@link TestDescriptor} to take the returned - * order into account when executing the tests. - */ -public class ImpactedTestsSorter implements ITestSorter { - - private final ImpactedTestsProvider impactedTestsProvider; - - public ImpactedTestsSorter(ImpactedTestsProvider impactedTestsProvider) { - this.impactedTestsProvider = impactedTestsProvider; - } - - @Override - public void selectAndSort(TestDescriptor rootTestDescriptor) { - AvailableTests availableTests = TestDescriptorUtils - .getAvailableTests(rootTestDescriptor, impactedTestsProvider.partition); - - List testClusters = impactedTestsProvider.getImpactedTestsFromTeamscale( - availableTests.getTestList()); - - if (testClusters == null) { - LOGGER.fine(() -> "Falling back to execute all!"); - return; - } - - Set testRepresentatives = Collections.newSetFromMap(new IdentityHashMap<>()); - Set seenDescriptors = Collections.newSetFromMap(new IdentityHashMap<>()); - for (PrioritizableTestCluster testCluster : testClusters) { - for (PrioritizableTest test : testCluster.tests) { - Optional uniqueId = availableTests.convertToUniqueId(test); - if (!uniqueId.isPresent()) { - LOGGER.severe(() -> "Falling back to execute all..."); - return; - } - Optional testDescriptor = rootTestDescriptor.findByUniqueId(uniqueId.get()); - if (!testDescriptor.isPresent()) { - LOGGER.severe(() -> "Falling back to execute all..."); - return; - } - testRepresentatives.add(testDescriptor.get()); - reinsertIntoHierarchy(testDescriptor.get(), seenDescriptors); - } - } - - removeNonImpactedTests(rootTestDescriptor, testRepresentatives); - } - - /** - * Reinserts the given testDescriptor into the hierarchy by walking up the parents chain. By doing this in order - * with all tests we end up with our intended order. This is continued until we reach a node that has already been - * reinserted in a previous run, because parents should be sorted according to the order of their most important - * child descriptors. - */ - private static void reinsertIntoHierarchy(TestDescriptor testDescriptor, - Set seenDescriptors) { - Optional currentTestDescriptor = Optional.of(testDescriptor); - while (currentTestDescriptor.isPresent() && !seenDescriptors.contains(currentTestDescriptor.get())) { - seenDescriptors.add(currentTestDescriptor.get()); - reinsertIntoParent(currentTestDescriptor.get()); - currentTestDescriptor = currentTestDescriptor.get().getParent(); - } - } - - /** - * Removes the test descriptor from its parent and inserts it again. The TestDescriptor internally uses a - * {@link java.util.LinkedHashSet} so remove and insert moves the testDescriptor to the last position in the - * iteration order. By doing this in order with all tests we end up with our intended order. - */ - private static void reinsertIntoParent(TestDescriptor testDescriptor) { - Optional parent = testDescriptor.getParent(); - if (parent.isPresent()) { - parent.get().removeChild(testDescriptor); - parent.get().addChild(testDescriptor); - } - } - - private void removeNonImpactedTests(TestDescriptor testDescriptor, Set testDescriptors) { - if (testDescriptors.contains(testDescriptor)) { - return; - } - for (TestDescriptor descriptor : new ArrayList<>(testDescriptor.getChildren())) { - removeNonImpactedTests(descriptor, testDescriptors); - } - if (testDescriptor.getChildren().isEmpty() && !testDescriptor.isRoot()) { - testDescriptor.removeFromHierarchy(); - } - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/NOPTestSorter.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/NOPTestSorter.java deleted file mode 100644 index c7e04a27d..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/NOPTestSorter.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.teamscale.test_impacted.engine.executor; - -import org.junit.platform.engine.TestDescriptor; - -/** - * NOP sorter that does nothing for when the engine is configured to only collect testwise coverage, but not using - * Teamscale to select or prioritize tests. - */ -public class NOPTestSorter implements ITestSorter { - @Override - public void selectAndSort(TestDescriptor rootTestDescriptor) { - // Nothing to do - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListener.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListener.java deleted file mode 100644 index 5beea9f21..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListener.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.teamscale.test_impacted.engine.executor; - -import com.teamscale.report.testwise.model.ETestExecutionResult; -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.test_impacted.commons.LoggerUtils; -import com.teamscale.test_impacted.test_descriptor.ITestDescriptorResolver; -import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils; -import org.junit.platform.engine.EngineExecutionListener; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.TestExecutionResult.Status; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.reporting.ReportEntry; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Logger; - -import static com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.isRepresentative; - -/** - * An execution listener which delegates events to another {@link EngineExecutionListener} and notifies Teamscale agents - * collecting test wise coverage. - */ -public class TestwiseCoverageCollectingExecutionListener implements EngineExecutionListener { - - private static final Logger LOGGER = LoggerUtils.getLogger(TestwiseCoverageCollectingExecutionListener.class); - - /** An API to signal test start and end to the agent. */ - private final TeamscaleAgentNotifier teamscaleAgentNotifier; - - /** List of tests that have been executed, skipped or failed. */ - private final List testExecutions = new ArrayList<>(); - - /** Time when the current test execution started. */ - private long executionStartTime; - - private final ITestDescriptorResolver testDescriptorResolver; - - private final EngineExecutionListener delegateEngineExecutionListener; - - private final Map> testResultCache = new HashMap<>(); - - public TestwiseCoverageCollectingExecutionListener(TeamscaleAgentNotifier teamscaleAgentNotifier, - ITestDescriptorResolver testDescriptorResolver, - EngineExecutionListener engineExecutionListener) { - this.teamscaleAgentNotifier = teamscaleAgentNotifier; - this.testDescriptorResolver = testDescriptorResolver; - this.delegateEngineExecutionListener = engineExecutionListener; - } - - @Override - public void dynamicTestRegistered(TestDescriptor testDescriptor) { - delegateEngineExecutionListener.dynamicTestRegistered(testDescriptor); - } - - @Override - public void executionSkipped(TestDescriptor testDescriptor, String reason) { - if (!TestDescriptorUtils.isRepresentative(testDescriptor)) { - delegateEngineExecutionListener.executionStarted(testDescriptor); - testDescriptor.getChildren().forEach(child -> executionSkipped(child, reason)); - delegateEngineExecutionListener.executionFinished(testDescriptor, TestExecutionResult.successful()); - return; - } - - testDescriptorResolver.getUniformPath(testDescriptor).ifPresent(testUniformPath -> { - testExecutions.add(new TestExecution(testUniformPath, 0L, ETestExecutionResult.SKIPPED, reason)); - delegateEngineExecutionListener.executionSkipped(testDescriptor, reason); - }); - } - - @Override - public void executionStarted(TestDescriptor testDescriptor) { - if (isRepresentative(testDescriptor)) { - testDescriptorResolver.getUniformPath(testDescriptor).ifPresent(teamscaleAgentNotifier::startTest); - executionStartTime = System.currentTimeMillis(); - } - delegateEngineExecutionListener.executionStarted(testDescriptor); - } - - @Override - public void executionFinished(TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) { - if (isRepresentative(testDescriptor)) { - Optional uniformPath = testDescriptorResolver.getUniformPath(testDescriptor); - if (!uniformPath.isPresent()) { - return; - } - - TestExecution testExecution = getTestExecution(testDescriptor, testExecutionResult, - uniformPath.get()); - if (testExecution != null) { - testExecutions.add(testExecution); - } - teamscaleAgentNotifier.endTest(uniformPath.get(), testExecution); - } else if (testDescriptor.getParent().isPresent()) { - List testExecutionResults = testResultCache.computeIfAbsent( - testDescriptor.getParent().get().getUniqueId(), (key) -> new ArrayList<>()); - testExecutionResults.add(testExecutionResult); - } - - delegateEngineExecutionListener.executionFinished(testDescriptor, testExecutionResult); - } - - private TestExecution getTestExecution(TestDescriptor testDescriptor, - TestExecutionResult testExecutionResult, String testUniformPath) { - List testExecutionResults = getTestExecutionResults(testDescriptor, testExecutionResult); - - long executionEndTime = System.currentTimeMillis(); - long duration = executionEndTime - executionStartTime; - StringBuilder message = new StringBuilder(); - Status status = Status.SUCCESSFUL; - for (TestExecutionResult executionResult : testExecutionResults) { - if (message.length() > 0) { - message.append("\n\n"); - } - message.append(getStacktrace(executionResult.getThrowable())); - // Aggregate status here to most severe status according to SUCCESSFUL < ABORTED < FAILED - if (status.ordinal() < executionResult.getStatus().ordinal()) { - status = executionResult.getStatus(); - } - } - - return buildTestExecution(testUniformPath, duration, status, message.toString()); - } - - private List getTestExecutionResults(TestDescriptor testDescriptor, - TestExecutionResult testExecutionResult) { - List testExecutionResults = new ArrayList<>(); - List childTestExecutionResult = testResultCache.remove(testDescriptor.getUniqueId()); - if (childTestExecutionResult != null) { - testExecutionResults.addAll(childTestExecutionResult); - } - testExecutionResults.add(testExecutionResult); - return testExecutionResults; - } - - private TestExecution buildTestExecution(String testUniformPath, long duration, - Status status, String message) { - switch (status) { - case SUCCESSFUL: - return new TestExecution(testUniformPath, duration, ETestExecutionResult.PASSED); - case ABORTED: - return new TestExecution(testUniformPath, duration, ETestExecutionResult.ERROR, - message); - case FAILED: - return new TestExecution(testUniformPath, duration, ETestExecutionResult.FAILURE, - message); - default: - LOGGER.severe(() -> "Got unexpected test execution result status: " + status); - return null; - } - } - - /** Extracts the stacktrace from the given {@link Throwable} into a string or returns null if no throwable is given. */ - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private String getStacktrace(Optional throwable) { - if (!throwable.isPresent()) { - return null; - } - - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - throwable.get().printStackTrace(pw); - return sw.toString(); - } - - @Override - public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { - delegateEngineExecutionListener.reportingEntryPublished(testDescriptor, entry); - } - - /** @see #testExecutions */ - public List getTestExecutions() { - return testExecutions; - } -} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.kt new file mode 100644 index 000000000..17787859f --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.kt @@ -0,0 +1,91 @@ +package com.teamscale.test_impacted.engine.executor + +import com.teamscale.test_impacted.engine.ImpactedTestEngine +import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getAvailableTests +import org.junit.platform.engine.TestDescriptor +import java.util.* + +/** +* Test sorter that requests impacted tests from Teamscale and rewrites the [TestDescriptor] to take the returned +* order into account when executing the tests. +*/ +class ImpactedTestsSorter(private val impactedTestsProvider: ImpactedTestsProvider) : ITestSorter { + + override fun selectAndSort(testDescriptor: TestDescriptor) { + val availableTests = getAvailableTests(testDescriptor, impactedTestsProvider.partition) + + val testClusters = impactedTestsProvider.getImpactedTestsFromTeamscale(availableTests.testList) + + if (testClusters == null) { + ImpactedTestEngine.LOGGER.fine { "Falling back to execute all!" } + return + } + + val testRepresentatives = Collections.newSetFromMap(IdentityHashMap()) + val seenDescriptors = Collections.newSetFromMap(IdentityHashMap()) + + val allTests = testClusters.asSequence() + .flatMap { it.tests?.asSequence() ?: emptySequence() } + + allTests.forEach { test -> + val uniqueId = availableTests.convertToUniqueId(test) + if (!uniqueId.isPresent) { + ImpactedTestEngine.LOGGER.severe { "Falling back to execute all..." } + return + } + val availableTest = testDescriptor.findByUniqueId(uniqueId.get()) + if (!availableTest.isPresent) { + ImpactedTestEngine.LOGGER.severe { "Falling back to execute all..." } + return + } + val descriptor = availableTest.get() + testRepresentatives.add(descriptor) + reinsertIntoHierarchy(descriptor, seenDescriptors) + } + + removeNonImpactedTests(testDescriptor, testRepresentatives) + } + + private fun removeNonImpactedTests( + testDescriptor: TestDescriptor, + impactedTestDescriptors: Set + ) { + if (testDescriptor in impactedTestDescriptors) return + + testDescriptor.children.toList().forEach { child -> + removeNonImpactedTests(child, impactedTestDescriptors) + } + + if (testDescriptor.children.isEmpty() && !testDescriptor.isRoot) { + testDescriptor.removeFromHierarchy() + } + } + + /** + * Reinserts the given [testDescriptor] into the hierarchy by walking up the parent chain recursively. + * This ensures that parent descriptors are sorted according to the order of their most important child descriptors. + */ + private tailrec fun reinsertIntoHierarchy( + testDescriptor: TestDescriptor, + seenDescriptors: MutableSet + ) { + if (!seenDescriptors.add(testDescriptor)) return + testDescriptor.reinsertIntoParent() + val parentDescriptor = testDescriptor.parent.orElse(null) + if (parentDescriptor != null) { + reinsertIntoHierarchy(parentDescriptor, seenDescriptors) + } + } + + /** + * Removes the [this@reinsertIntoParent] from its parent and re-inserts it. + * This moves the descriptor to the end of the iteration order in the parent's children, + * effectively reordering it. + */ + private fun TestDescriptor.reinsertIntoParent() { + parent.ifPresent { parent -> + parent.removeChild(this) + parent.addChild(this) + } + } +} \ No newline at end of file diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/NOPTestSorter.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/NOPTestSorter.kt new file mode 100644 index 000000000..07f86acdc --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/NOPTestSorter.kt @@ -0,0 +1,13 @@ +package com.teamscale.test_impacted.engine.executor + +import org.junit.platform.engine.TestDescriptor + +/** + * NOP sorter that does nothing for when the engine is configured to only collect testwise coverage, but not using + * Teamscale to select or prioritize tests. + */ +class NOPTestSorter : ITestSorter { + override fun selectAndSort(testDescriptor: TestDescriptor) { + // Nothing to do + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListener.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListener.kt new file mode 100644 index 000000000..a455862f6 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListener.kt @@ -0,0 +1,175 @@ +package com.teamscale.test_impacted.engine.executor + +import com.teamscale.report.testwise.model.ETestExecutionResult +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger +import com.teamscale.test_impacted.test_descriptor.ITestDescriptorResolver +import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.isRepresentative +import org.junit.platform.engine.EngineExecutionListener +import org.junit.platform.engine.TestDescriptor +import org.junit.platform.engine.TestExecutionResult +import org.junit.platform.engine.UniqueId +import org.junit.platform.engine.reporting.ReportEntry +import java.io.PrintWriter +import java.io.StringWriter +import java.util.* + +/** + * An execution listener which delegates events to another [EngineExecutionListener] and notifies Teamscale agents + * collecting test wise coverage. + */ +class TestwiseCoverageCollectingExecutionListener( + /** An API to signal test start and end to the agent. */ + private val teamscaleAgentNotifier: TeamscaleAgentNotifier, + private val testDescriptorResolver: ITestDescriptorResolver, + private val delegateEngineExecutionListener: EngineExecutionListener +) : EngineExecutionListener { + private val LOGGER = createLogger() + + /** List of tests that have been executed, skipped or failed. */ + val testExecutions = mutableListOf() + + /** Time when the current test execution started. */ + private var executionStartTime = 0L + + private val testResultCache = mutableMapOf>() + + override fun dynamicTestRegistered(testDescriptor: TestDescriptor) { + delegateEngineExecutionListener.dynamicTestRegistered(testDescriptor) + } + + override fun executionSkipped(testDescriptor: TestDescriptor, reason: String) { + if (!testDescriptor.isRepresentative()) { + delegateEngineExecutionListener.executionStarted(testDescriptor) + testDescriptor.children.forEach { executionSkipped(it, reason) } + delegateEngineExecutionListener.executionFinished(testDescriptor, TestExecutionResult.successful()) + return + } + + testDescriptorResolver.getUniformPath(testDescriptor).ifPresent { testUniformPath -> + testExecutions.add( + TestExecution( + testUniformPath, + 0L, + ETestExecutionResult.SKIPPED, + reason + ) + ) + delegateEngineExecutionListener.executionSkipped(testDescriptor, reason) + } + } + + override fun executionStarted(testDescriptor: TestDescriptor) { + if (testDescriptor.isRepresentative()) { + testDescriptorResolver.getUniformPath(testDescriptor).ifPresent { testUniformPath -> + teamscaleAgentNotifier.startTest(testUniformPath) + } + executionStartTime = System.currentTimeMillis() + } + delegateEngineExecutionListener.executionStarted(testDescriptor) + } + + override fun executionFinished(testDescriptor: TestDescriptor, testExecutionResult: TestExecutionResult) { + if (testDescriptor.isRepresentative()) { + val uniformPath = testDescriptorResolver.getUniformPath(testDescriptor) + if (!uniformPath.isPresent) { + return + } + + val testExecution = getTestExecution( + testDescriptor, testExecutionResult, uniformPath.get() + ) + if (testExecution != null) { + testExecutions.add(testExecution) + } + teamscaleAgentNotifier.endTest(uniformPath.get(), testExecution) + } else if (testDescriptor.parent.isPresent) { + val testExecutionResults = testResultCache.computeIfAbsent( + testDescriptor.parent.get().uniqueId + ) { mutableListOf() } + testExecutionResults.add(testExecutionResult) + } + + delegateEngineExecutionListener.executionFinished(testDescriptor, testExecutionResult) + } + + private fun getTestExecution( + testDescriptor: TestDescriptor, + testExecutionResult: TestExecutionResult, + testUniformPath: String + ): TestExecution? { + val testExecutionResults = getTestExecutionResults(testDescriptor, testExecutionResult) + + val executionEndTime = System.currentTimeMillis() + val duration = executionEndTime - executionStartTime + val message = StringBuilder() + var status = TestExecutionResult.Status.SUCCESSFUL + testExecutionResults.forEach { executionResult -> + if (message.isNotEmpty()) { + message.append("\n\n") + } + message.append(executionResult.throwable.buildStacktrace()) + // Aggregate status here to most severe status according to SUCCESSFUL < ABORTED < FAILED + if (status.ordinal < executionResult.status.ordinal) { + status = executionResult.status + } + } + + return buildTestExecution(testUniformPath, duration, status, message.toString()) + } + + private fun getTestExecutionResults( + testDescriptor: TestDescriptor, + testExecutionResult: TestExecutionResult + ): List { + val testExecutionResults = mutableListOf() + val childTestExecutionResult = testResultCache.remove(testDescriptor.uniqueId) + if (childTestExecutionResult != null) { + testExecutionResults.addAll(childTestExecutionResult) + } + testExecutionResults.add(testExecutionResult) + return testExecutionResults + } + + private fun buildTestExecution( + testUniformPath: String, + duration: Long, + status: TestExecutionResult.Status, + message: String + ): TestExecution? { + when (status) { + TestExecutionResult.Status.SUCCESSFUL -> return TestExecution( + testUniformPath, duration, ETestExecutionResult.PASSED + ) + + TestExecutionResult.Status.ABORTED -> return TestExecution( + testUniformPath, duration, ETestExecutionResult.ERROR, message + ) + + TestExecutionResult.Status.FAILED -> return TestExecution( + testUniformPath, duration, ETestExecutionResult.FAILURE, message + ) + + else -> { + LOGGER.severe { "Got unexpected test execution result status: $status" } + return null + } + } + } + + /** Extracts the stacktrace from the given [Throwable] into a string or returns null if no throwable is given. */ + private fun Optional.buildStacktrace(): String? { + if (!isPresent) { + return null + } + + val sw = StringWriter() + val pw = PrintWriter(sw) + get().printStackTrace(pw) + return sw.toString() + } + + override fun reportingEntryPublished(testDescriptor: TestDescriptor, entry: ReportEntry) { + delegateEngineExecutionListener.reportingEntryPublished(testDescriptor, entry) + } +}