From 20d098229859e38c8aa297109fd5fd91b8e39fcf Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 1 Dec 2024 04:49:42 +0100 Subject: [PATCH 01/21] TS-38628 Impacted Test Engine Tests migration --- impacted-test-engine/build.gradle.kts | 1 + .../engine/ImpactedTestEngineTestBase.java | 90 --------- ...mpactedTestEngineWithDynamicTestsTest.java | 70 ------- .../ImpactedTestEngineWithTwoEnginesTest.java | 171 ---------------- .../engine/NoImpactedTestsTest.java | 53 ----- .../engine/executor/DummyEngine.java | 64 ------ .../engine/executor/SimpleTestDescriptor.java | 93 --------- ...verageCollectingExecutionListenerTest.java | 130 ------------ .../CucumberPickleDescriptorResolverTest.java | 52 ----- .../engine/ImpactedTestEngineTestBase.kt | 83 ++++++++ .../ImpactedTestEngineWithDynamicTestsTest.kt | 69 +++++++ .../ImpactedTestEngineWithTwoEnginesTest.kt | 190 ++++++++++++++++++ .../engine/NoImpactedTestsTest.kt | 45 +++++ .../engine/executor/DummyEngine.kt | 45 +++++ .../engine/executor/SimpleTestDescriptor.kt | 72 +++++++ ...CoverageCollectingExecutionListenerTest.kt | 137 +++++++++++++ .../CucumberPickleDescriptorResolverTest.kt | 48 +++++ 17 files changed, 690 insertions(+), 723 deletions(-) delete mode 100644 impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.java delete mode 100644 impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.java delete mode 100644 impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.java delete mode 100644 impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/NoImpactedTestsTest.java delete mode 100644 impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/DummyEngine.java delete mode 100644 impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.java delete mode 100644 impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java delete mode 100644 impacted-test-engine/src/test/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.java create mode 100644 impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt create mode 100644 impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt create mode 100644 impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt create mode 100644 impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt create mode 100644 impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/DummyEngine.kt create mode 100644 impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.kt create mode 100644 impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt create mode 100644 impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.kt diff --git a/impacted-test-engine/build.gradle.kts b/impacted-test-engine/build.gradle.kts index 31793ff02..d00a85ae9 100644 --- a/impacted-test-engine/build.gradle.kts +++ b/impacted-test-engine/build.gradle.kts @@ -1,4 +1,5 @@ plugins { + kotlin("jvm") `java-library` com.teamscale.`java-convention` com.teamscale.coverage diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.java deleted file mode 100644 index 43a027b44..000000000 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.teamscale.test_impacted.engine; - -import com.teamscale.client.PrioritizableTestCluster; -import com.teamscale.test_impacted.engine.executor.ImpactedTestsProvider; -import com.teamscale.test_impacted.engine.executor.ImpactedTestsSorter; -import com.teamscale.test_impacted.engine.executor.TeamscaleAgentNotifier; -import org.junit.jupiter.api.Test; -import org.junit.platform.engine.EngineDiscoveryRequest; -import org.junit.platform.engine.EngineExecutionListener; -import org.junit.platform.engine.ExecutionRequest; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.UniqueId; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -/** Base class for testing specific scenarios in the impacted test engine. */ -public abstract class ImpactedTestEngineTestBase { - - private final TestEngineRegistry testEngineRegistry = mock(TestEngineRegistry.class); - - private final TestDataWriter testDataWriter = mock(TestDataWriter.class); - - private final ImpactedTestsProvider impactedTestsProvider = mock(ImpactedTestsProvider.class); - - private final EngineDiscoveryRequest discoveryRequest = mock(EngineDiscoveryRequest.class); - - private final ExecutionRequest executionRequest = mock(ExecutionRequest.class); - - private final EngineExecutionListener executionListener = mock(EngineExecutionListener.class); - - private final TeamscaleAgentNotifier teamscaleAgentNotifier = mock(TeamscaleAgentNotifier.class); - - @Test - void testEngineExecution() { - InternalImpactedTestEngine internalImpactedTestEngine = createInternalImpactedTestEngine( - getEngines()); - - TestDescriptor impactedTestEngineDescriptor = internalImpactedTestEngine - .discover(discoveryRequest, UniqueId.forEngine(ImpactedTestEngine.ENGINE_ID)); - assertThat(impactedTestEngineDescriptor.getUniqueId()) - .isEqualTo(UniqueId.forEngine(ImpactedTestEngine.ENGINE_ID)); - - when(executionRequest.getEngineExecutionListener()).thenReturn(executionListener); - when(executionRequest.getRootTestDescriptor()).thenReturn(impactedTestEngineDescriptor); - when(impactedTestsProvider.getImpactedTestsFromTeamscale(any())).thenReturn( - getImpactedTests()); - - internalImpactedTestEngine.execute(executionRequest); - - verifyCallbacks(executionListener); - - // Ensure test data is written. - verify(testDataWriter).dumpTestDetails(any()); - verify(testDataWriter).dumpTestExecutions(any()); - - verifyNoMoreInteractions(executionListener); - verifyNoMoreInteractions(testDataWriter); - } - - /** Returns the available engines that should be assumed by the impacted test engine. */ - abstract List getEngines(); - - /** Returns the result that Teamscale should return when asked for impacted tests. */ - abstract List getImpactedTests(); - - /** Verifies that the interactions with the executionListener are the ones we would expect. */ - abstract void verifyCallbacks(EngineExecutionListener executionListener); - - private InternalImpactedTestEngine createInternalImpactedTestEngine(List engines) { - for (TestEngine engine : engines) { - when(testEngineRegistry.getTestEngine(eq(engine.getId()))).thenReturn(engine); - } - when(testEngineRegistry.iterator()).thenReturn(engines.iterator()); - - return new InternalImpactedTestEngine( - new ImpactedTestEngineConfiguration(testDataWriter, testEngineRegistry, - new ImpactedTestsSorter(impactedTestsProvider), - teamscaleAgentNotifier), - impactedTestsProvider.partition); - } -} diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.java deleted file mode 100644 index 367a66f8b..000000000 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.teamscale.test_impacted.engine; - -import com.teamscale.client.PrioritizableTest; -import com.teamscale.client.PrioritizableTestCluster; -import com.teamscale.test_impacted.engine.executor.DummyEngine; -import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver; -import org.junit.platform.engine.EngineExecutionListener; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.UniqueId; -import org.mockito.Mockito; - -import java.util.List; - -import static com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor.dynamicTestCase; -import static com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor.testCase; -import static com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor.testContainer; -import static java.util.Collections.singletonList; -import static org.junit.platform.engine.TestExecutionResult.successful; - -/** Test setup for JUnit Jupiter dynamic tests. */ -class ImpactedTestEngineWithDynamicTestsTest extends ImpactedTestEngineTestBase { - /** - * For this test setup we rely on the {@link JUnitJupiterTestDescriptorResolver} for resolving uniform paths and - * cluster ids. Therefore, the engine root is set accordingly. - */ - private final UniqueId engineRootId = UniqueId.forEngine("junit-jupiter"); - - private final UniqueId dynamicTestClassId = engineRootId.append(JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, "example.DynamicTest"); - private final UniqueId dynamicTestId = dynamicTestClassId.append(JUnitJupiterTestDescriptorResolver.TEST_FACTORY_SEGMENT_TYPE, "testFactory()"); - private final UniqueId dynamicallyRegisteredTestId = dynamicTestId.append(JUnitJupiterTestDescriptorResolver.DYNAMIC_TEST_SEGMENT_TYPE, "#1"); - - private final TestDescriptor dynamicallyRegisteredTestCase = testCase(dynamicallyRegisteredTestId); - private final TestDescriptor dynamicTestCase = dynamicTestCase(dynamicTestId, - dynamicallyRegisteredTestCase); - private final TestDescriptor dynamicTestClassCase = testContainer(dynamicTestClassId, - dynamicTestCase); - private final TestDescriptor testRoot = testContainer(engineRootId, dynamicTestClassCase); - - @Override - public List getEngines() { - return singletonList(new DummyEngine(testRoot)); - } - - @Override - public List getImpactedTests() { - return singletonList( - new PrioritizableTestCluster("example/DynamicTest", - singletonList(new PrioritizableTest("example/DynamicTest/testFactory()")))); - } - - @Override - public void verifyCallbacks(EngineExecutionListener executionListener) { - // First the parents test descriptors are started in order. - Mockito.verify(executionListener).executionStarted(testRoot); - Mockito.verify(executionListener).executionStarted(dynamicTestClassCase); - Mockito.verify(executionListener).executionStarted(dynamicTestCase); - - // Test case is added dynamically and executed. - dynamicTestCase.addChild(dynamicallyRegisteredTestCase); - Mockito.verify(executionListener).dynamicTestRegistered(dynamicallyRegisteredTestCase); - Mockito.verify(executionListener).executionStarted(dynamicallyRegisteredTestCase); - Mockito.verify(executionListener).executionFinished(dynamicallyRegisteredTestCase, successful()); - - // Parent test descriptors are also finished. - Mockito.verify(executionListener).executionFinished(dynamicTestCase, successful()); - Mockito.verify(executionListener).executionFinished(dynamicTestClassCase, successful()); - Mockito.verify(executionListener).executionFinished(testRoot, successful()); - } -} diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.java deleted file mode 100644 index 73d010153..000000000 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.teamscale.test_impacted.engine; - -import com.teamscale.client.PrioritizableTest; -import com.teamscale.client.PrioritizableTestCluster; -import com.teamscale.test_impacted.engine.executor.DummyEngine; -import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver; -import org.junit.jupiter.api.Disabled; -import org.junit.platform.engine.EngineExecutionListener; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.UniqueId; - -import java.util.Arrays; -import java.util.List; - -import static com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor.testCase; -import static com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor.testContainer; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.junit.platform.engine.TestExecutionResult.failed; -import static org.junit.platform.engine.TestExecutionResult.successful; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; - -/** Test setup for a mixture of impacted and no impacted tests and two test engines. */ -class ImpactedTestEngineWithTwoEnginesTest extends ImpactedTestEngineTestBase { - - private static final String FIRST_TEST_CLASS = "FirstTestClass"; - private static final String OTHER_TEST_CLASS = "OtherTestClass"; - private static final String IGNORED_TEST_CLASS = "IgnoredTestClass"; - private static final String SECOND_TEST_CLASS = "SecondTestClass"; - private static final String IMPACTED_TEST_CASE_1 = "impactedTestCase1()"; - private static final String IMPACTED_TEST_CASE_2 = "impactedTestCase2()"; - private static final String IMPACTED_TEST_CASE_3 = "impactedTestCase3()"; - private static final String IMPACTED_TEST_CASE_4 = "impactedTestCase4()"; - private static final String SKIPPED_IMPACTED_TEST_CASE_ID = "skippedImpactedTestCaseId()"; - private static final String NON_IMPACTED_TEST_CASE_1 = "nonImpactedTestCase1()"; - private static final String NON_IMPACTED_TEST_CASE_2 = "nonImpactedTestCase2()"; - /** - * For this test setup we rely on the {@link JUnitJupiterTestDescriptorResolver} for resolving uniform paths and - * cluster ids. Therefore, the engine root is set accordingly. - */ - private final UniqueId engine1RootId = UniqueId.forEngine("junit-jupiter"); - private final UniqueId engine2RootId = UniqueId.forEngine("exotic-engine"); - - /** FirstTestClass contains one impacted and one non-impacted test. */ - private final UniqueId firstTestClassId = engine1RootId.append( - JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, FIRST_TEST_CLASS); - private final UniqueId impactedTestCase1Id = firstTestClassId.append( - JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, - IMPACTED_TEST_CASE_1); - private final UniqueId nonImpactedTestCase1Id = firstTestClassId - .append(JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, NON_IMPACTED_TEST_CASE_1); - - /** - * IgnoredTestClass is ignored (e.g. class is annotated with {@link Disabled}). Hence, it'll be impacted since it - * was previously skipped. - */ - private final UniqueId ignoredTestClassId = engine1RootId.append( - JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, IGNORED_TEST_CLASS); - private final UniqueId impactedTestCase2Id = ignoredTestClassId.append( - JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, - IMPACTED_TEST_CASE_2); - private final UniqueId nonImpactedTestCase2Id = ignoredTestClassId - .append(JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, NON_IMPACTED_TEST_CASE_2); - - /** - * ImpactedTestClassWithSkippedTest contains two impacted tests of which one is skipped. - */ - private final UniqueId secondTestClassId = engine1RootId.append( - JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, SECOND_TEST_CLASS); - private final UniqueId impactedTestCase3Id = secondTestClassId.append( - JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, - IMPACTED_TEST_CASE_3); - private final UniqueId skippedImpactedTestCaseId = secondTestClassId - .append(JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, SKIPPED_IMPACTED_TEST_CASE_ID); - - /** OtherTestClass contains one impacted and one non-impacted test. */ - private final UniqueId otherTestClassId = engine2RootId.append( - JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, OTHER_TEST_CLASS); - private final UniqueId impactedTestCase4Id = otherTestClassId.append( - JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, - IMPACTED_TEST_CASE_4); - - private final TestDescriptor impactedTestCase1 = testCase(impactedTestCase1Id); - private final TestDescriptor nonImpactedTestCase1 = testCase(nonImpactedTestCase1Id); - private final TestDescriptor firstTestClass = testContainer(firstTestClassId, - impactedTestCase1, nonImpactedTestCase1); - - private final TestDescriptor impactedTestCase2 = testCase(impactedTestCase2Id); - private final TestDescriptor nonImpactedTestCase2 = testCase(nonImpactedTestCase2Id); - private final TestDescriptor ignoredTestClass = testContainer(ignoredTestClassId, - impactedTestCase2, nonImpactedTestCase2).skip(); - - private final TestExecutionResult failed = failed(new NullPointerException()); - private final TestDescriptor impactedTestCase3 = testCase(impactedTestCase3Id).result(failed); - private final TestDescriptor skippedImpactedTestCase = testCase(skippedImpactedTestCaseId).skip(); - private final TestDescriptor secondTestClass = testContainer(secondTestClassId, - impactedTestCase3, skippedImpactedTestCase); - - private final TestDescriptor impactedTestCase4 = testCase(impactedTestCase4Id); - private final TestDescriptor otherTestClass = testContainer(otherTestClassId, impactedTestCase4); - - private final TestDescriptor testEngine1Root = testContainer(engine1RootId, firstTestClass, - ignoredTestClass, secondTestClass); - - private final TestDescriptor testEngine2Root = testContainer(engine2RootId, otherTestClass); - - @Override - public List getEngines() { - return Arrays.asList( - new DummyEngine(testEngine1Root), - new DummyEngine(testEngine2Root)); - } - - @Override - public List getImpactedTests() { - return Arrays.asList( - new PrioritizableTestCluster(FIRST_TEST_CLASS, - singletonList(new PrioritizableTest(FIRST_TEST_CLASS + "/" + IMPACTED_TEST_CASE_1))), - new PrioritizableTestCluster(OTHER_TEST_CLASS, - singletonList(new PrioritizableTest(OTHER_TEST_CLASS + "/" + IMPACTED_TEST_CASE_4))), - new PrioritizableTestCluster(IGNORED_TEST_CLASS, - singletonList(new PrioritizableTest(IGNORED_TEST_CLASS + "/" + IMPACTED_TEST_CASE_2))), - new PrioritizableTestCluster(SECOND_TEST_CLASS, - asList(new PrioritizableTest(SECOND_TEST_CLASS + "/" + IMPACTED_TEST_CASE_3), - new PrioritizableTest(SECOND_TEST_CLASS + "/" + SKIPPED_IMPACTED_TEST_CASE_ID)))); - } - - @Override - public void verifyCallbacks(EngineExecutionListener executionListener) { - // Start of engine 1 - verify(executionListener).executionStarted(testEngine1Root); - - // Execute FirstTestClass. - verify(executionListener).executionStarted(firstTestClass); - verify(executionListener).executionStarted(impactedTestCase1); - verify(executionListener).executionFinished(eq(impactedTestCase1), any()); - verify(executionListener).executionFinished(eq(firstTestClass), any()); - - // Execute IgnoredTestClass. - verify(executionListener).executionStarted(ignoredTestClass); - verify(executionListener).executionSkipped(eq(impactedTestCase2), any()); - verify(executionListener).executionFinished(ignoredTestClass, successful()); - - // Execute SecondTestClass. - verify(executionListener).executionStarted(secondTestClass); - verify(executionListener).executionStarted(eq(impactedTestCase3)); - verify(executionListener).executionFinished(impactedTestCase3, - failed); - verify(executionListener).executionSkipped(eq(skippedImpactedTestCase), any()); - verify(executionListener).executionFinished(secondTestClass, successful()); - - // Finish test engine 1 - verify(executionListener).executionFinished(testEngine1Root, successful()); - - // Start of engine 2 - verify(executionListener).executionStarted(testEngine2Root); - - // Execute OtherTestClass. - verify(executionListener).executionStarted(otherTestClass); - verify(executionListener).executionStarted(impactedTestCase4); - verify(executionListener).executionFinished(impactedTestCase4, successful()); - verify(executionListener).executionFinished(otherTestClass, successful()); - - // Finish test engine 2 - verify(executionListener).executionFinished(testEngine2Root, successful()); - } -} diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/NoImpactedTestsTest.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/NoImpactedTestsTest.java deleted file mode 100644 index 409237f09..000000000 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/NoImpactedTestsTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.teamscale.test_impacted.engine; - -import com.teamscale.client.PrioritizableTestCluster; -import com.teamscale.test_impacted.engine.executor.DummyEngine; -import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver; -import org.junit.platform.engine.EngineExecutionListener; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.UniqueId; - -import java.util.Collections; -import java.util.List; - -import static com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor.testCase; -import static com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor.testContainer; -import static java.util.Collections.singletonList; - -/** Test setup where no test is impacted. */ -class NoImpactedTestsTest extends ImpactedTestEngineTestBase { - - private static final String FIRST_TEST_CLASS = "FirstTestClass"; - private static final String NON_IMPACTED_TEST_CASE_1 = "nonImpactedTestCase1()"; - /** - * For this test setup we rely on the {@link JUnitJupiterTestDescriptorResolver} for resolving uniform paths and - * cluster ids. Therefore, the engine root is set accordingly. - */ - private final UniqueId engine1RootId = UniqueId.forEngine("junit-jupiter"); - - /** FirstTestClass contains one non-impacted test. */ - private final UniqueId firstTestClassId = engine1RootId.append( - JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, FIRST_TEST_CLASS); - private final UniqueId nonImpactedTestCase1Id = firstTestClassId - .append(JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, NON_IMPACTED_TEST_CASE_1); - private final TestDescriptor nonImpactedTestCase1 = testCase(nonImpactedTestCase1Id); - private final TestDescriptor firstTestClass = testContainer(firstTestClassId, nonImpactedTestCase1); - - private final TestDescriptor testEngine1Root = testContainer(engine1RootId, firstTestClass); - - @Override - public List getEngines() { - return singletonList(new DummyEngine(testEngine1Root)); - } - - @Override - public List getImpactedTests() { - return Collections.emptyList(); - } - - @Override - public void verifyCallbacks(EngineExecutionListener executionListener) { - - } -} diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/DummyEngine.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/DummyEngine.java deleted file mode 100644 index 5a22eea4a..000000000 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/DummyEngine.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.teamscale.test_impacted.engine.executor; - -import org.junit.platform.engine.EngineDiscoveryRequest; -import org.junit.platform.engine.EngineExecutionListener; -import org.junit.platform.engine.ExecutionRequest; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.UniqueId; - -/** - * A test engine that simulates the behavior of the vintage and jupiter engine that the impacted test engine invokes - * under the hood. - */ -public class DummyEngine implements TestEngine { - - private final TestDescriptor descriptor; - - public DummyEngine(TestDescriptor descriptor) { - this.descriptor = descriptor; - } - - @Override - public String getId() { - return descriptor.getUniqueId().getEngineId().get(); - } - - @Override - public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { - return descriptor; - } - - @Override - public void execute(ExecutionRequest request) { - EngineExecutionListener executionListener = request.getEngineExecutionListener(); - this.executeDescriptor(executionListener, request.getRootTestDescriptor()); - } - - /** - * Calls the {@link EngineExecutionListener} callbacks in the expected order. The information whether tests should - * be skipped, should fail or have dynamic executions is attached to the {@link SimpleTestDescriptor}. - */ - private void executeDescriptor(EngineExecutionListener executionListener, TestDescriptor testDescriptor) { - if (!(testDescriptor instanceof SimpleTestDescriptor)) { - throw new IllegalArgumentException("Expected TestDescriptor to be of type SimpleTestDescriptor"); - } - SimpleTestDescriptor simpleTestDescriptor = (SimpleTestDescriptor) testDescriptor; - if (simpleTestDescriptor.shouldBeSkipped()) { - executionListener.executionSkipped(testDescriptor, "Tests class is disabled."); - return; - } - executionListener.executionStarted(testDescriptor); - for (TestDescriptor child : testDescriptor.getChildren()) { - executeDescriptor(executionListener, child); - } - for (TestDescriptor dynamicTest : simpleTestDescriptor.getDynamicTests()) { - testDescriptor.addChild(dynamicTest); - executionListener.dynamicTestRegistered(dynamicTest); - executeDescriptor(executionListener, dynamicTest); - } - TestExecutionResult executionResult = simpleTestDescriptor.getExecutionResult(); - executionListener.executionFinished(testDescriptor, executionResult); - } -} diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.java deleted file mode 100644 index 73f7aaad1..000000000 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.teamscale.test_impacted.engine.executor; - -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static org.junit.platform.engine.TestExecutionResult.successful; - -/** A basic implementation of a {@link TestDescriptor} that can be used during tests. */ -public class SimpleTestDescriptor extends AbstractTestDescriptor { - - private final Type type; - - private boolean shouldSkip = false; - - private final List dynamicTests = new ArrayList<>(); - - private TestExecutionResult executionResult = successful(); - - private SimpleTestDescriptor(UniqueId uniqueId, Type type, String displayName) { - super(uniqueId, displayName); - this.type = type; - } - - @Override - public Type getType() { - return type; - } - - /** Marks the test as being skipped. */ - public SimpleTestDescriptor skip() { - this.shouldSkip = true; - return this; - } - - /** Whether the test should be skipped. */ - public boolean shouldBeSkipped() { - return shouldSkip; - } - - /** - * The dynamic child tests that should be simulated or just an empty list if the test does not contain dynamic - * tests. - */ - public List getDynamicTests() { - return dynamicTests; - } - - /** Sets the execution result that the engine should report when simulating the test's execution. */ - public SimpleTestDescriptor result(TestExecutionResult executionResult) { - this.executionResult = executionResult; - return this; - } - - public TestExecutionResult getExecutionResult() { - return this.executionResult; - } - - /** Creates a {@link TestDescriptor} for a concrete test case without children. */ - public static SimpleTestDescriptor testCase(UniqueId uniqueId) { - return new SimpleTestDescriptor(uniqueId, Type.TEST, getSimpleDisplayName(uniqueId)); - } - - private static String getSimpleDisplayName(UniqueId uniqueId) { - return uniqueId.getSegments().get(uniqueId.getSegments().size() - 1).getValue(); - } - - /** Creates a {@link TestDescriptor} for a dynamic test case which registers children during test execution. */ - public static TestDescriptor dynamicTestCase(UniqueId uniqueId, TestDescriptor... dynamicTestCases) { - SimpleTestDescriptor simpleTestDescriptor = new SimpleTestDescriptor(uniqueId, Type.CONTAINER_AND_TEST, - getSimpleDisplayName(uniqueId)); - simpleTestDescriptor.dynamicTests.addAll(Arrays.asList(dynamicTestCases)); - return simpleTestDescriptor; - } - - /** - * Creates a {@link TestDescriptor} for a test container (e.g. a test class or test engine) containing other - * {@link TestDescriptor} children. - */ - public static SimpleTestDescriptor testContainer(UniqueId uniqueId, TestDescriptor... children) { - SimpleTestDescriptor result = new SimpleTestDescriptor(uniqueId, Type.CONTAINER, - getSimpleDisplayName(uniqueId)); - for (TestDescriptor child : children) { - result.addChild(child); - } - return result; - } -} diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java deleted file mode 100644 index e03b4a784..000000000 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java +++ /dev/null @@ -1,130 +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.test_descriptor.ITestDescriptorResolver; -import org.junit.jupiter.api.Test; -import org.junit.platform.engine.EngineExecutionListener; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.UniqueId; - -import java.util.List; -import java.util.Optional; - -import static com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor.testCase; -import static com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor.testContainer; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.platform.engine.TestExecutionResult.successful; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -/** Tests for {@link TestwiseCoverageCollectingExecutionListener}. */ -class TestwiseCoverageCollectingExecutionListenerTest { - - private final TeamscaleAgentNotifier mockApi = mock(TeamscaleAgentNotifier.class); - - private final ITestDescriptorResolver resolver = mock(ITestDescriptorResolver.class); - - private final EngineExecutionListener executionListenerMock = mock(EngineExecutionListener.class); - - private final TestwiseCoverageCollectingExecutionListener executionListener = new TestwiseCoverageCollectingExecutionListener( - mockApi, resolver, executionListenerMock); - - private final UniqueId rootId = UniqueId.forEngine("dummy"); - - @Test - void testInteractionWithListenersAndCoverageApi() { - UniqueId testClassId = rootId.append("TEST_CONTAINER", "MyClass"); - UniqueId impactedTestCaseId = testClassId.append("TEST_CASE", "impactedTestCase()"); - UniqueId regularSkippedTestCaseId = testClassId.append("TEST_CASE", "regularSkippedTestCase()"); - - TestDescriptor impactedTestCase = testCase(impactedTestCaseId); - TestDescriptor regularSkippedTestCase = testCase(regularSkippedTestCaseId); - TestDescriptor testClass = testContainer(testClassId, impactedTestCase, - regularSkippedTestCase); - TestDescriptor testRoot = testContainer(rootId, testClass); - - when(resolver.getUniformPath(impactedTestCase)).thenReturn(Optional.of("MyClass/impactedTestCase()")); - when(resolver.getClusterId(impactedTestCase)).thenReturn(Optional.of("MyClass")); - when(resolver.getUniformPath(regularSkippedTestCase)) - .thenReturn(Optional.of("MyClass/regularSkippedTestCase()")); - when(resolver.getClusterId(regularSkippedTestCase)).thenReturn(Optional.of("MyClass")); - - // Start engine and class. - executionListener.executionStarted(testRoot); - verify(executionListenerMock).executionStarted(testRoot); - executionListener.executionStarted(testClass); - verify(executionListenerMock).executionStarted(testClass); - - // Execution of impacted test case. - executionListener.executionStarted(impactedTestCase); - verify(mockApi).startTest("MyClass/impactedTestCase()"); - verify(executionListenerMock).executionStarted(impactedTestCase); - executionListener.executionFinished(impactedTestCase, successful()); - verify(mockApi).endTest(eq("MyClass/impactedTestCase()"), any()); - verify(executionListenerMock).executionFinished(impactedTestCase, successful()); - - // Ignored or disabled impacted test case is skipped. - executionListener.executionSkipped(regularSkippedTestCase, "Test is disabled."); - verify(executionListenerMock).executionSkipped(regularSkippedTestCase, "Test is disabled."); - - // Finish class and engine. - executionListener.executionFinished(testClass, successful()); - verify(executionListenerMock).executionFinished(testClass, successful()); - executionListener.executionFinished(testRoot, successful()); - verify(executionListenerMock).executionFinished(testRoot, successful()); - - verifyNoMoreInteractions(mockApi); - verifyNoMoreInteractions(executionListenerMock); - - List testExecutions = executionListener.getTestExecutions(); - - assertThat(testExecutions).hasSize(2); - assertThat(testExecutions).anySatisfy(testExecution -> - assertThat(testExecution.uniformPath).isEqualTo("MyClass/impactedTestCase()")); - assertThat(testExecutions).anySatisfy(testExecution -> - assertThat(testExecution.uniformPath).isEqualTo("MyClass/regularSkippedTestCase()")); - } - - @Test - void testSkipOfTestClass() { - UniqueId testClassId = rootId.append("TEST_CONTAINER", "MyClass"); - UniqueId testCase1Id = testClassId.append("TEST_CASE", "testCase1()"); - UniqueId testCase2Id = testClassId.append("TEST_CASE", "testCase2()"); - - TestDescriptor testCase1 = testCase(testCase1Id); - TestDescriptor testCase2 = testCase(testCase2Id); - TestDescriptor testClass = testContainer(testClassId, testCase1, testCase2); - TestDescriptor testRoot = testContainer(rootId, testClass); - - when(resolver.getUniformPath(testCase1)).thenReturn(Optional.of("MyClass/testCase1()")); - when(resolver.getClusterId(testCase1)).thenReturn(Optional.of("MyClass")); - when(resolver.getUniformPath(testCase2)).thenReturn(Optional.of("MyClass/testCase2()")); - when(resolver.getClusterId(testCase2)).thenReturn(Optional.of("MyClass")); - - // Start engine and class. - executionListener.executionStarted(testRoot); - verify(executionListenerMock).executionStarted(testRoot); - - executionListener.executionSkipped(testClass, "Test class is disabled."); - verify(executionListenerMock).executionStarted(testClass); - verify(executionListenerMock).executionSkipped(testCase1, "Test class is disabled."); - verify(executionListenerMock).executionSkipped(testCase2, "Test class is disabled."); - verify(executionListenerMock).executionFinished(testClass, successful()); - - executionListener.executionFinished(testRoot, successful()); - verify(executionListenerMock).executionFinished(testRoot, successful()); - - verifyNoMoreInteractions(executionListenerMock); - - List testExecutions = executionListener.getTestExecutions(); - - assertThat(testExecutions).hasSize(2); - assertThat(testExecutions) - .allMatch(testExecution -> testExecution.result.equals(ETestExecutionResult.SKIPPED)); - } -} \ No newline at end of file diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.java deleted file mode 100644 index 288c41963..000000000 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.teamscale.test_impacted.test_descriptor; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.LinkedHashMap; - -class CucumberPickleDescriptorResolverTest { - - @Test - void escapeSlashes() { - LinkedHashMap expectedByInput = new LinkedHashMap<>(); - expectedByInput.put("abc", "abc"); - expectedByInput.put("ab/c", "ab\\/c"); - expectedByInput.put("ab\\/c", "ab\\/c"); // don't escape what is already escaped - expectedByInput.put("/abc", "\\/abc"); - expectedByInput.put("/abc/", "\\/abc\\/"); - expectedByInput.put("/", "\\/"); - expectedByInput.put("/a/", "\\/a\\/"); - expectedByInput.put("a//", "a\\/\\/"); - expectedByInput.put("//a", "\\/\\/a"); - expectedByInput.put("//", "\\/\\/"); - expectedByInput.put("///", "\\/\\/\\/"); - expectedByInput.put("\\", "\\"); - expectedByInput.put("http://link", "http:\\/\\/link"); - - expectedByInput.forEach((input, expected) -> Assertions.assertEquals(expected, - CucumberPickleDescriptorResolver.escapeSlashes(input))); - } - - @Test - void testNoDuplicatedSlashesInUniformPath() { - CucumberPickleDescriptorResolver cucumberPickleDescriptorResolver = new CucumberPickleDescriptorResolver(); - LinkedHashMap expectedByInput = new LinkedHashMap<>(); - expectedByInput.put("abc", "abc"); - expectedByInput.put("ab/c", "ab/c"); - expectedByInput.put("ab//c", "ab/c"); - expectedByInput.put("ab///c", "ab/c"); - expectedByInput.put("ab\\/\\//c", "ab\\/\\//c"); - expectedByInput.put("a/", "a/"); - expectedByInput.put("a//", "a/"); - expectedByInput.put("/a", "/a"); - expectedByInput.put("//a", "/a"); - expectedByInput.put("/", "/"); - expectedByInput.put("\\/", "\\/"); - expectedByInput.put("\\", "\\"); - expectedByInput.put("\\\\", "\\\\"); - - expectedByInput.forEach((input, expected) -> Assertions.assertEquals(expected, - cucumberPickleDescriptorResolver.removeDuplicatedSlashes(input))); - } -} \ No newline at end of file diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt new file mode 100644 index 000000000..20132ac16 --- /dev/null +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt @@ -0,0 +1,83 @@ +package com.teamscale.test_impacted.engine + +import com.teamscale.client.PrioritizableTestCluster +import com.teamscale.test_impacted.engine.executor.ImpactedTestsProvider +import com.teamscale.test_impacted.engine.executor.ImpactedTestsSorter +import com.teamscale.test_impacted.engine.executor.TeamscaleAgentNotifier +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.platform.engine.* +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.Mockito.mock + +/** Base class for testing specific scenarios in the impacted test engine. */ +abstract class ImpactedTestEngineTestBase { + private val testEngineRegistry = mock() + + private val testDataWriter = mock() + + private val impactedTestsProvider = mock() + + private val discoveryRequest = mock() + + private val executionRequest = mock() + + private val executionListener = mock() + + private val teamscaleAgentNotifier = mock() + + @Test + fun testEngineExecution() { + val testEngine = createInternalImpactedTestEngine(engines) + + val engineDescriptor = testEngine + .discover(discoveryRequest, UniqueId.forEngine(ImpactedTestEngine.ENGINE_ID)) + Assertions.assertThat(engineDescriptor.uniqueId) + .isEqualTo(UniqueId.forEngine(ImpactedTestEngine.ENGINE_ID)) + + Mockito.`when`(executionRequest.engineExecutionListener) + .thenReturn(executionListener) + Mockito.`when`(executionRequest.rootTestDescriptor) + .thenReturn(engineDescriptor) + Mockito.`when`(impactedTestsProvider.getImpactedTestsFromTeamscale(ArgumentMatchers.any())) + .thenReturn(impactedTests) + + testEngine.execute(executionRequest) + + verifyCallbacks(executionListener) + + // Ensure test data is written. + Mockito.verify(testDataWriter).dumpTestDetails(ArgumentMatchers.any()) + Mockito.verify(testDataWriter).dumpTestExecutions(ArgumentMatchers.any()) + + Mockito.verifyNoMoreInteractions(executionListener) + Mockito.verifyNoMoreInteractions(testDataWriter) + } + + /** Returns the available engines that should be assumed by the impacted test engine. */ + abstract val engines: List + + /** Returns the result that Teamscale should return when asked for impacted tests. */ + abstract val impactedTests: List + + /** Verifies that the interactions with the executionListener are the ones we would expect. */ + abstract fun verifyCallbacks(executionListener: EngineExecutionListener) + + private fun createInternalImpactedTestEngine(engines: List): InternalImpactedTestEngine { + engines.forEach { engine -> + Mockito.`when`(testEngineRegistry.getTestEngine(ArgumentMatchers.eq(engine.id))) + .thenReturn(engine) + } + Mockito.`when`>(testEngineRegistry.iterator()).thenReturn(engines.iterator()) + + return InternalImpactedTestEngine( + ImpactedTestEngineConfiguration( + testDataWriter, testEngineRegistry, + ImpactedTestsSorter(impactedTestsProvider), + teamscaleAgentNotifier + ), + impactedTestsProvider.partition + ) + } +} diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt new file mode 100644 index 000000000..acdbd10f6 --- /dev/null +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt @@ -0,0 +1,69 @@ +package com.teamscale.test_impacted.engine + +import com.teamscale.client.PrioritizableTest +import com.teamscale.client.PrioritizableTestCluster +import com.teamscale.test_impacted.engine.executor.DummyEngine +import com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor +import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver +import org.junit.platform.engine.* +import org.mockito.Mockito + +/** Test setup for JUnit Jupiter dynamic tests. */ +internal class ImpactedTestEngineWithDynamicTestsTest : ImpactedTestEngineTestBase() { + /** + * For this test setup we rely on the [JUnitJupiterTestDescriptorResolver] for resolving uniform paths and + * cluster ids. Therefore, the engine root is set accordingly. + */ + private val engineRootId = UniqueId.forEngine("junit-jupiter") + + private val dynamicTestClassId = + engineRootId.append(JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, "example.DynamicTest") + private val dynamicTestId = + dynamicTestClassId.append(JUnitJupiterTestDescriptorResolver.TEST_FACTORY_SEGMENT_TYPE, "testFactory()") + private val dynamicallyRegisteredTestId = + dynamicTestId.append(JUnitJupiterTestDescriptorResolver.DYNAMIC_TEST_SEGMENT_TYPE, "#1") + + private val dynamicallyRegisteredTestCase = + SimpleTestDescriptor.testCase(dynamicallyRegisteredTestId) + private val dynamicTestCase = SimpleTestDescriptor.dynamicTestCase( + dynamicTestId, + dynamicallyRegisteredTestCase + ) + private val dynamicTestClassCase = SimpleTestDescriptor.testContainer( + dynamicTestClassId, + dynamicTestCase + ) + private val testRoot = SimpleTestDescriptor.testContainer(engineRootId, dynamicTestClassCase) + + override val engines: List by lazy { + listOf(DummyEngine(testRoot)) + } + + override val impactedTests: List by lazy { + listOf( + PrioritizableTestCluster( + "example/DynamicTest", + listOf(PrioritizableTest("example/DynamicTest/testFactory()")) + ) + ) + } + + override fun verifyCallbacks(executionListener: EngineExecutionListener) { + // First the parents test descriptors are started in order. + Mockito.verify(executionListener).executionStarted(testRoot) + Mockito.verify(executionListener).executionStarted(dynamicTestClassCase) + Mockito.verify(executionListener).executionStarted(dynamicTestCase) + + // Test case is added dynamically and executed. + dynamicTestCase.addChild(dynamicallyRegisteredTestCase) + Mockito.verify(executionListener).dynamicTestRegistered(dynamicallyRegisteredTestCase) + Mockito.verify(executionListener).executionStarted(dynamicallyRegisteredTestCase) + Mockito.verify(executionListener) + .executionFinished(dynamicallyRegisteredTestCase, TestExecutionResult.successful()) + + // Parent test descriptors are also finished. + Mockito.verify(executionListener).executionFinished(dynamicTestCase, TestExecutionResult.successful()) + Mockito.verify(executionListener).executionFinished(dynamicTestClassCase, TestExecutionResult.successful()) + Mockito.verify(executionListener).executionFinished(testRoot, TestExecutionResult.successful()) + } +} diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt new file mode 100644 index 000000000..89af6af95 --- /dev/null +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt @@ -0,0 +1,190 @@ +package com.teamscale.test_impacted.engine + +import com.teamscale.client.PrioritizableTest +import com.teamscale.client.PrioritizableTestCluster +import com.teamscale.test_impacted.engine.executor.DummyEngine +import com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor +import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver +import org.junit.platform.engine.* +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import java.util.* + +/** Test setup for a mixture of impacted and no impacted tests and two test engines. */ +internal class ImpactedTestEngineWithTwoEnginesTest : ImpactedTestEngineTestBase() { + /** + * For this test setup we rely on the [JUnitJupiterTestDescriptorResolver] for resolving uniform paths and + * cluster ids. Therefore, the engine root is set accordingly. + */ + private val engine1RootId = UniqueId.forEngine("junit-jupiter") + private val engine2RootId = UniqueId.forEngine("exotic-engine") + + /** FirstTestClass contains one impacted and one non-impacted test. */ + private val firstTestClassId = engine1RootId.append( + JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, FIRST_TEST_CLASS + ) + private val impactedTestCase1Id = firstTestClassId.append( + JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, + IMPACTED_TEST_CASE_1 + ) + private val nonImpactedTestCase1Id = firstTestClassId + .append(JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, NON_IMPACTED_TEST_CASE_1) + + /** + * IgnoredTestClass is ignored (e.g. class is annotated with [Disabled]). Hence, it'll be impacted since it + * was previously skipped. + */ + private val ignoredTestClassId = engine1RootId.append( + JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, IGNORED_TEST_CLASS + ) + private val impactedTestCase2Id = ignoredTestClassId.append( + JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, + IMPACTED_TEST_CASE_2 + ) + private val nonImpactedTestCase2Id = ignoredTestClassId + .append(JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, NON_IMPACTED_TEST_CASE_2) + + /** + * ImpactedTestClassWithSkippedTest contains two impacted tests of which one is skipped. + */ + private val secondTestClassId = engine1RootId.append( + JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, SECOND_TEST_CLASS + ) + private val impactedTestCase3Id = secondTestClassId.append( + JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, + IMPACTED_TEST_CASE_3 + ) + private val skippedImpactedTestCaseId = secondTestClassId + .append(JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, SKIPPED_IMPACTED_TEST_CASE_ID) + + /** OtherTestClass contains one impacted and one non-impacted test. */ + private val otherTestClassId = engine2RootId.append( + JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, OTHER_TEST_CLASS + ) + private val impactedTestCase4Id = otherTestClassId.append( + JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, + IMPACTED_TEST_CASE_4 + ) + + private val impactedTestCase1 = SimpleTestDescriptor.testCase(impactedTestCase1Id) + private val nonImpactedTestCase1 = SimpleTestDescriptor.testCase(nonImpactedTestCase1Id) + private val firstTestClass = SimpleTestDescriptor.testContainer( + firstTestClassId, + impactedTestCase1, nonImpactedTestCase1 + ) + + private val impactedTestCase2 = SimpleTestDescriptor.testCase(impactedTestCase2Id) + private val nonImpactedTestCase2 = SimpleTestDescriptor.testCase(nonImpactedTestCase2Id) + private val ignoredTestClass = SimpleTestDescriptor.testContainer( + ignoredTestClassId, + impactedTestCase2, nonImpactedTestCase2 + ).skip() + + private val failed = TestExecutionResult.failed(NullPointerException()) + private val impactedTestCase3 = SimpleTestDescriptor.testCase(impactedTestCase3Id).result(failed) + private val skippedImpactedTestCase = + SimpleTestDescriptor.testCase(skippedImpactedTestCaseId).skip() + private val secondTestClass = SimpleTestDescriptor.testContainer( + secondTestClassId, + impactedTestCase3, skippedImpactedTestCase + ) + + private val impactedTestCase4 = SimpleTestDescriptor.testCase(impactedTestCase4Id) + private val otherTestClass = SimpleTestDescriptor.testContainer(otherTestClassId, impactedTestCase4) + + private val testEngine1Root = SimpleTestDescriptor.testContainer( + engine1RootId, firstTestClass, + ignoredTestClass, secondTestClass + ) + + private val testEngine2Root = SimpleTestDescriptor.testContainer(engine2RootId, otherTestClass) + + override val engines: List by lazy { + listOf( + DummyEngine(testEngine1Root), + DummyEngine(testEngine2Root) + ) + } + + override val impactedTests: List by lazy { + listOf( + PrioritizableTestCluster( + FIRST_TEST_CLASS, + listOf(PrioritizableTest("$FIRST_TEST_CLASS/$IMPACTED_TEST_CASE_1")) + ), + PrioritizableTestCluster( + OTHER_TEST_CLASS, + listOf(PrioritizableTest("$OTHER_TEST_CLASS/$IMPACTED_TEST_CASE_4")) + ), + PrioritizableTestCluster( + IGNORED_TEST_CLASS, + listOf(PrioritizableTest("$IGNORED_TEST_CLASS/$IMPACTED_TEST_CASE_2")) + ), + PrioritizableTestCluster( + SECOND_TEST_CLASS, + listOf( + PrioritizableTest("$SECOND_TEST_CLASS/$IMPACTED_TEST_CASE_3"), + PrioritizableTest("$SECOND_TEST_CLASS/$SKIPPED_IMPACTED_TEST_CASE_ID") + ) + ) + ) + } + + override fun verifyCallbacks(executionListener: EngineExecutionListener) { + // Start of engine 1 + Mockito.verify(executionListener).executionStarted(testEngine1Root) + + // Execute FirstTestClass. + Mockito.verify(executionListener).executionStarted(firstTestClass) + Mockito.verify(executionListener).executionStarted(impactedTestCase1) + Mockito.verify(executionListener) + .executionFinished(ArgumentMatchers.eq(impactedTestCase1), ArgumentMatchers.any()) + Mockito.verify(executionListener).executionFinished(ArgumentMatchers.eq(firstTestClass), ArgumentMatchers.any()) + + // Execute IgnoredTestClass. + Mockito.verify(executionListener).executionStarted(ignoredTestClass) + Mockito.verify(executionListener) + .executionSkipped(ArgumentMatchers.eq(impactedTestCase2), ArgumentMatchers.any()) + Mockito.verify(executionListener).executionFinished(ignoredTestClass, TestExecutionResult.successful()) + + // Execute SecondTestClass. + Mockito.verify(executionListener).executionStarted(secondTestClass) + Mockito.verify(executionListener).executionStarted(ArgumentMatchers.eq(impactedTestCase3)) + Mockito.verify(executionListener).executionFinished( + impactedTestCase3, + failed + ) + Mockito.verify(executionListener) + .executionSkipped(ArgumentMatchers.eq(skippedImpactedTestCase), ArgumentMatchers.any()) + Mockito.verify(executionListener).executionFinished(secondTestClass, TestExecutionResult.successful()) + + // Finish test engine 1 + Mockito.verify(executionListener).executionFinished(testEngine1Root, TestExecutionResult.successful()) + + // Start of engine 2 + Mockito.verify(executionListener).executionStarted(testEngine2Root) + + // Execute OtherTestClass. + Mockito.verify(executionListener).executionStarted(otherTestClass) + Mockito.verify(executionListener).executionStarted(impactedTestCase4) + Mockito.verify(executionListener).executionFinished(impactedTestCase4, TestExecutionResult.successful()) + Mockito.verify(executionListener).executionFinished(otherTestClass, TestExecutionResult.successful()) + + // Finish test engine 2 + Mockito.verify(executionListener).executionFinished(testEngine2Root, TestExecutionResult.successful()) + } + + companion object { + private const val FIRST_TEST_CLASS = "FirstTestClass" + private const val OTHER_TEST_CLASS = "OtherTestClass" + private const val IGNORED_TEST_CLASS = "IgnoredTestClass" + private const val SECOND_TEST_CLASS = "SecondTestClass" + private const val IMPACTED_TEST_CASE_1 = "impactedTestCase1()" + private const val IMPACTED_TEST_CASE_2 = "impactedTestCase2()" + private const val IMPACTED_TEST_CASE_3 = "impactedTestCase3()" + private const val IMPACTED_TEST_CASE_4 = "impactedTestCase4()" + private const val SKIPPED_IMPACTED_TEST_CASE_ID = "skippedImpactedTestCaseId()" + private const val NON_IMPACTED_TEST_CASE_1 = "nonImpactedTestCase1()" + private const val NON_IMPACTED_TEST_CASE_2 = "nonImpactedTestCase2()" + } +} diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt new file mode 100644 index 000000000..ca84332e5 --- /dev/null +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt @@ -0,0 +1,45 @@ +package com.teamscale.test_impacted.engine + +import com.teamscale.client.PrioritizableTestCluster +import com.teamscale.test_impacted.engine.executor.DummyEngine +import com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor +import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver +import org.junit.platform.engine.EngineExecutionListener +import org.junit.platform.engine.TestDescriptor +import org.junit.platform.engine.TestEngine +import org.junit.platform.engine.UniqueId + +/** Test setup where no test is impacted. */ +internal class NoImpactedTestsTest : ImpactedTestEngineTestBase() { + /** + * For this test setup we rely on the [JUnitJupiterTestDescriptorResolver] for resolving uniform paths and + * cluster ids. Therefore, the engine root is set accordingly. + */ + private val engine1RootId = UniqueId.forEngine("junit-jupiter") + + /** FirstTestClass contains one non-impacted test. */ + private val firstTestClassId = engine1RootId.append( + JUnitJupiterTestDescriptorResolver.CLASS_SEGMENT_TYPE, FIRST_TEST_CLASS + ) + private val nonImpactedTestCase1Id = firstTestClassId + .append(JUnitJupiterTestDescriptorResolver.METHOD_SEGMENT_TYPE, NON_IMPACTED_TEST_CASE_1) + private val nonImpactedTestCase1 = SimpleTestDescriptor.testCase(nonImpactedTestCase1Id) + private val firstTestClass = + SimpleTestDescriptor.testContainer(firstTestClassId, nonImpactedTestCase1) + + private val testEngine1Root = SimpleTestDescriptor.testContainer(engine1RootId, firstTestClass) + + override val engines: List by lazy { + listOf(DummyEngine(testEngine1Root)) + } + + override val impactedTests: List + get() = emptyList() + + override fun verifyCallbacks(executionListener: EngineExecutionListener) {} + + companion object { + private const val FIRST_TEST_CLASS = "FirstTestClass" + private const val NON_IMPACTED_TEST_CASE_1 = "nonImpactedTestCase1()" + } +} diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/DummyEngine.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/DummyEngine.kt new file mode 100644 index 000000000..92a1dce66 --- /dev/null +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/DummyEngine.kt @@ -0,0 +1,45 @@ +package com.teamscale.test_impacted.engine.executor + +import org.junit.platform.engine.* + +/** + * A test engine that simulates the behavior of the vintage and jupiter engine that the impacted test engine invokes + * under the hood. + */ +class DummyEngine(private val descriptor: TestDescriptor) : TestEngine { + override fun getId() = descriptor.uniqueId.engineId.get() + + override fun discover(discoveryRequest: EngineDiscoveryRequest, uniqueId: UniqueId) = + descriptor + + override fun execute(request: ExecutionRequest) { + val executionListener = request.engineExecutionListener + this.executeDescriptor(executionListener, request.rootTestDescriptor) + } + + /** + * Calls the [EngineExecutionListener] callbacks in the expected order. The information whether tests should + * be skipped, should fail or have dynamic executions is attached to the [SimpleTestDescriptor]. + */ + private fun executeDescriptor(executionListener: EngineExecutionListener, testDescriptor: TestDescriptor?) { + require(testDescriptor is SimpleTestDescriptor) { + "Expected TestDescriptor to be of type SimpleTestDescriptor" + } + if (testDescriptor.shouldBeSkipped()) { + executionListener.executionSkipped(testDescriptor, "Tests class is disabled.") + return + } + executionListener.executionStarted(testDescriptor) + testDescriptor.getChildren().forEach { child -> + executeDescriptor(executionListener, child) + } + testDescriptor.dynamicTests.forEach { dynamicTest -> + testDescriptor.addChild(dynamicTest) + executionListener.dynamicTestRegistered(dynamicTest) + executeDescriptor(executionListener, dynamicTest) + } + executionListener.executionFinished( + testDescriptor, testDescriptor.executionResult + ) + } +} diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.kt new file mode 100644 index 000000000..37a10d12d --- /dev/null +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.kt @@ -0,0 +1,72 @@ +package com.teamscale.test_impacted.engine.executor + +import org.junit.platform.engine.TestDescriptor +import org.junit.platform.engine.TestExecutionResult +import org.junit.platform.engine.UniqueId +import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor +import java.util.* + +/** A basic implementation of a [TestDescriptor] that can be used during tests. */ +class SimpleTestDescriptor private constructor( + uniqueId: UniqueId, + private val type: TestDescriptor.Type, + displayName: String +) : AbstractTestDescriptor(uniqueId, displayName) { + private var shouldSkip = false + + val dynamicTests = mutableListOf() + + var executionResult: TestExecutionResult = TestExecutionResult.successful() + private set + + override fun getType() = type + + /** Marks the test as being skipped. */ + fun skip(): SimpleTestDescriptor { + this.shouldSkip = true + return this + } + + /** Whether the test should be skipped. */ + fun shouldBeSkipped() = shouldSkip + + /** Sets the execution result that the engine should report when simulating the test's execution. */ + fun result(executionResult: TestExecutionResult): SimpleTestDescriptor { + this.executionResult = executionResult + return this + } + + companion object { + /** Creates a [TestDescriptor] for a concrete test case without children. */ + fun testCase(uniqueId: UniqueId) = + SimpleTestDescriptor(uniqueId, TestDescriptor.Type.TEST, getSimpleDisplayName(uniqueId)) + + private fun getSimpleDisplayName(uniqueId: UniqueId) = + uniqueId.segments[uniqueId.segments.size - 1].value + + /** Creates a [TestDescriptor] for a dynamic test case which registers children during test execution. */ + fun dynamicTestCase(uniqueId: UniqueId, vararg dynamicTestCases: TestDescriptor): TestDescriptor { + val simpleTestDescriptor = SimpleTestDescriptor( + uniqueId, TestDescriptor.Type.CONTAINER_AND_TEST, + getSimpleDisplayName(uniqueId) + ) + simpleTestDescriptor.dynamicTests.addAll(listOf(*dynamicTestCases)) + return simpleTestDescriptor + } + + /** + * Creates a [TestDescriptor] for a test container (e.g., a test class or test engine) containing other + * [TestDescriptor] children. + */ + fun testContainer(uniqueId: UniqueId, vararg children: TestDescriptor): SimpleTestDescriptor { + val result = SimpleTestDescriptor( + uniqueId, TestDescriptor.Type.CONTAINER, + getSimpleDisplayName(uniqueId) + ) + children.forEach { child -> + result.addChild(child) + } + return result + } + } +} diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt new file mode 100644 index 000000000..bac8c9e57 --- /dev/null +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt @@ -0,0 +1,137 @@ +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.test_descriptor.ITestDescriptorResolver +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +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.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.Mockito.mock +import java.util.* + +/** Tests for [TestwiseCoverageCollectingExecutionListener]. */ +internal class TestwiseCoverageCollectingExecutionListenerTest { + private val mockApi = mock() + + private val resolver = mock() + + private val executionListenerMock = mock() + + private val executionListener = TestwiseCoverageCollectingExecutionListener( + mockApi, resolver, executionListenerMock + ) + + private val rootId = UniqueId.forEngine("dummy") + + @Test + fun testInteractionWithListenersAndCoverageApi() { + val testClassId = rootId.append("TEST_CONTAINER", "MyClass") + val impactedTestCaseId = testClassId.append("TEST_CASE", "impactedTestCase()") + val regularSkippedTestCaseId = testClassId.append("TEST_CASE", "regularSkippedTestCase()") + + val impactedTestCase = SimpleTestDescriptor.testCase(impactedTestCaseId) + val regularSkippedTestCase = SimpleTestDescriptor.testCase(regularSkippedTestCaseId) + val testClass = SimpleTestDescriptor.testContainer( + testClassId, impactedTestCase, + regularSkippedTestCase + ) + val testRoot = SimpleTestDescriptor.testContainer(rootId, testClass) + + Mockito.`when`(resolver.getUniformPath(impactedTestCase)) + .thenReturn(Optional.of("MyClass/impactedTestCase()")) + Mockito.`when`(resolver.getClusterId(impactedTestCase)) + .thenReturn(Optional.of("MyClass")) + Mockito.`when`(resolver.getUniformPath(regularSkippedTestCase)) + .thenReturn(Optional.of("MyClass/regularSkippedTestCase()")) + Mockito.`when`(resolver.getClusterId(regularSkippedTestCase)) + .thenReturn(Optional.of("MyClass")) + + // Start engine and class. + executionListener.executionStarted(testRoot) + Mockito.verify(executionListenerMock).executionStarted(testRoot) + executionListener.executionStarted(testClass) + Mockito.verify(executionListenerMock).executionStarted(testClass) + + // Execution of impacted test case. + executionListener.executionStarted(impactedTestCase) + Mockito.verify(mockApi).startTest("MyClass/impactedTestCase()") + Mockito.verify(executionListenerMock).executionStarted(impactedTestCase) + executionListener.executionFinished(impactedTestCase, TestExecutionResult.successful()) + Mockito.verify(mockApi).endTest(ArgumentMatchers.eq("MyClass/impactedTestCase()"), ArgumentMatchers.any()) + Mockito.verify(executionListenerMock).executionFinished(impactedTestCase, TestExecutionResult.successful()) + + // Ignored or disabled impacted test case is skipped. + executionListener.executionSkipped(regularSkippedTestCase, "Test is disabled.") + Mockito.verify(executionListenerMock).executionSkipped(regularSkippedTestCase, "Test is disabled.") + + // Finish class and engine. + executionListener.executionFinished(testClass, TestExecutionResult.successful()) + Mockito.verify(executionListenerMock).executionFinished(testClass, TestExecutionResult.successful()) + executionListener.executionFinished(testRoot, TestExecutionResult.successful()) + Mockito.verify(executionListenerMock).executionFinished(testRoot, TestExecutionResult.successful()) + + Mockito.verifyNoMoreInteractions(mockApi) + Mockito.verifyNoMoreInteractions(executionListenerMock) + + val testExecutions = executionListener.testExecutions + + Assertions.assertThat(testExecutions).hasSize(2) + Assertions.assertThat(testExecutions).anySatisfy { testExecution: TestExecution -> + Assertions.assertThat( + testExecution.uniformPath + ).isEqualTo("MyClass/impactedTestCase()") + } + Assertions.assertThat(testExecutions).anySatisfy { testExecution: TestExecution -> + Assertions.assertThat( + testExecution.uniformPath + ).isEqualTo("MyClass/regularSkippedTestCase()") + } + } + + @Test + fun testSkipOfTestClass() { + val testClassId = rootId.append("TEST_CONTAINER", "MyClass") + val testCase1Id = testClassId.append("TEST_CASE", "testCase1()") + val testCase2Id = testClassId.append("TEST_CASE", "testCase2()") + + val testCase1: TestDescriptor = SimpleTestDescriptor.testCase(testCase1Id) + val testCase2: TestDescriptor = SimpleTestDescriptor.testCase(testCase2Id) + val testClass: TestDescriptor = SimpleTestDescriptor.testContainer(testClassId, testCase1, testCase2) + val testRoot: TestDescriptor = SimpleTestDescriptor.testContainer(rootId, testClass) + + Mockito.`when`(resolver.getUniformPath(testCase1)) + .thenReturn(Optional.of("MyClass/testCase1()")) + Mockito.`when`(resolver.getClusterId(testCase1)) + .thenReturn(Optional.of("MyClass")) + Mockito.`when`(resolver.getUniformPath(testCase2)) + .thenReturn(Optional.of("MyClass/testCase2()")) + Mockito.`when`(resolver.getClusterId(testCase2)) + .thenReturn(Optional.of("MyClass")) + + // Start engine and class. + executionListener.executionStarted(testRoot) + Mockito.verify(executionListenerMock).executionStarted(testRoot) + + executionListener.executionSkipped(testClass, "Test class is disabled.") + Mockito.verify(executionListenerMock).executionStarted(testClass) + Mockito.verify(executionListenerMock).executionSkipped(testCase1, "Test class is disabled.") + Mockito.verify(executionListenerMock).executionSkipped(testCase2, "Test class is disabled.") + Mockito.verify(executionListenerMock).executionFinished(testClass, TestExecutionResult.successful()) + + executionListener.executionFinished(testRoot, TestExecutionResult.successful()) + Mockito.verify(executionListenerMock).executionFinished(testRoot, TestExecutionResult.successful()) + + Mockito.verifyNoMoreInteractions(executionListenerMock) + + val testExecutions = executionListener.testExecutions + + Assertions.assertThat(testExecutions).hasSize(2) + Assertions.assertThat(testExecutions) + .allMatch { it.result == ETestExecutionResult.SKIPPED } + } +} \ No newline at end of file diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.kt new file mode 100644 index 000000000..fc99686e3 --- /dev/null +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.kt @@ -0,0 +1,48 @@ +package com.teamscale.test_impacted.test_descriptor + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +internal class CucumberPickleDescriptorResolverTest { + @Test + fun escapeSlashes() { + mapOf( + "abc" to "abc", + "ab/c" to "ab\\/c", + "ab\\/c" to "ab\\/c", // don't escape what is already escaped + "/abc" to "\\/abc", + "/abc/" to "\\/abc\\/", + "/" to "\\/", + "/a/" to "\\/a\\/", + "a//" to "a\\/\\/", + "//a" to "\\/\\/a", + "//" to "\\/\\/", + "///" to "\\/\\/\\/", + "\\" to "\\", + "http://link" to "http:\\/\\/link" + ).forEach { (input, expected) -> + Assertions.assertEquals(expected, CucumberPickleDescriptorResolver.escapeSlashes(input)) + } + } + + @Test + fun testNoDuplicatedSlashesInUniformPath() { + mapOf( + "abc" to "abc", + "ab/c" to "ab/c", + "ab//c" to "ab/c", + "ab///c" to "ab/c", + "ab\\/\\//c" to "ab\\/\\//c", + "a/" to "a/", + "a//" to "a/", + "/a" to "/a", + "//a" to "/a", + "/" to "/", + "\\/" to "\\/", + "\\" to "\\", + "\\\\" to "\\\\" + ).forEach { (input, expected) -> + Assertions.assertEquals(expected, CucumberPickleDescriptorResolver().removeDuplicatedSlashes(input)) + } + } +} \ No newline at end of file From b8c1d3b23627324e6912abb1185ab48b2208c946 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 1 Dec 2024 05:18:20 +0100 Subject: [PATCH 02/21] TS-38628 Migrate commons --- .../commons/IndentingWriter.java | 29 -------- .../test_impacted/commons/LoggerUtils.java | 72 ------------------- .../test_impacted/commons/IndentingWriter.kt | 24 +++++++ .../test_impacted/commons/LoggerUtils.kt | 62 ++++++++++++++++ 4 files changed, 86 insertions(+), 101 deletions(-) delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/IndentingWriter.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/IndentingWriter.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/IndentingWriter.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/IndentingWriter.java deleted file mode 100644 index b426438ef..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/IndentingWriter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.teamscale.test_impacted.commons; - -/** Utility class for writing lines with tab indentation. */ -public class IndentingWriter { - - private StringBuilder builder = new StringBuilder(); - - private int indent = 0; - - /** Indents all {@link #writeLine(String)} calls in the indented writes by one more tab. */ - public void indent(Runnable indentedWrites) { - indent++; - indentedWrites.run(); - indent--; - } - - /** Writes a new line. */ - public void writeLine(String line) { - for (int i = 0; i < indent; i++) { - builder.append("\t"); - } - builder.append(line).append("\n"); - } - - @Override - public String toString() { - return builder.toString(); - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java deleted file mode 100644 index edf438f52..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.teamscale.test_impacted.commons; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.logging.ConsoleHandler; -import java.util.logging.LogManager; -import java.util.logging.LogRecord; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; - -/** - * Provides access to a JUL Logger which is configured to print to the console in a not too noisy format as this appears - * in the console when executing tests. - */ -public class LoggerUtils { - - private static final Logger MAIN_LOGGER; - private static final String JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY = "java.util.logging.config.file"; - - static { - // Needs to be at the very top so it also takes affect when setting the log level for Console handlers - useDefaultJULConfigFile(); - - MAIN_LOGGER = Logger.getLogger("com.teamscale"); - MAIN_LOGGER.setUseParentHandlers(false); - ConsoleHandler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter() { - - @Override - public synchronized String format(LogRecord lr) { - return String.format("[%1$s] %2$s%n", lr.getLevel().getLocalizedName(), lr.getMessage()); - } - }); - - MAIN_LOGGER.addHandler(handler); - } - - /** - * Normally, the java util logging framework picks up the config file specified via the system property - * {@value #JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY}. For some reason, this does not work here, so we need to - * teach the log manager to use it. - */ - private static void useDefaultJULConfigFile() { - String loggingPropertiesFilePathString = System.getProperty(JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY); - if (loggingPropertiesFilePathString == null) { - return; - } - - Logger logger = Logger.getLogger(LoggerUtils.class.getName()); - try { - Path loggingPropertiesFilePath = Paths.get(loggingPropertiesFilePathString); - if (!loggingPropertiesFilePath.toFile().exists()) { - logger.warning( - "Cannot find the file specified via " + JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY + ": " + loggingPropertiesFilePathString); - return; - } - LogManager.getLogManager().readConfiguration(Files.newInputStream(loggingPropertiesFilePath)); - } catch (IOException e) { - logger.warning( - "Cannot load the file specified via " + JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY + ": " + loggingPropertiesFilePathString + ". " + e.getMessage()); - } - } - - /** - * Returns a logger for the given class. - */ - public static Logger getLogger(Class clazz) { - return Logger.getLogger(clazz.getName()); - } -} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/IndentingWriter.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/IndentingWriter.kt new file mode 100644 index 000000000..97bf75ae2 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/IndentingWriter.kt @@ -0,0 +1,24 @@ +package com.teamscale.test_impacted.commons + +/** Utility class for writing lines with tab indentation. */ +class IndentingWriter { + private val builder = StringBuilder() + + private var indent = 0 + + /** Indents all [writeLine] calls in the indented writes by one more tab. */ + fun indent(indentedWrites: Runnable) { + indent++ + indentedWrites.run() + indent-- + } + + /** Writes a new line. */ + fun writeLine(line: String) { + builder.append("\t".repeat(indent)) + .append(line) + .append("\n") + } + + override fun toString() = builder.toString() +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt new file mode 100644 index 000000000..ee423775f --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt @@ -0,0 +1,62 @@ +package com.teamscale.test_impacted.commons + +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths +import java.util.logging.* + +/** + * Provides access to a JUL Logger which is configured to print to the console in a not too noisy format as this appears + * in the console when executing tests. + */ +object LoggerUtils { + private val MAIN_LOGGER = Logger.getLogger("com.teamscale") + private const val JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY = "java.util.logging.config.file" + + init { + // Needs to be at the very top so it also takes affect when setting the log level for Console handlers + useDefaultJULConfigFile() + + MAIN_LOGGER.useParentHandlers = false + val handler = ConsoleHandler() + handler.formatter = object : SimpleFormatter() { + @Synchronized + override fun format(lr: LogRecord) = + String.format("[%1\$s] %2\$s%n", lr.level.localizedName, lr.message) + } + MAIN_LOGGER.addHandler(handler) + } + + /** + * Normally, the java util logging framework picks up the config file specified via the system property + * {@value #JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY}. For some reason, this does not work here, so we need to + * teach the log manager to use it. + */ + private fun useDefaultJULConfigFile() { + val loggingPropertiesFilePathString = + System.getProperty(JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY) + ?: return + + val logger = getLogger(LoggerUtils::class.java) + try { + val propertiesFilePath = Paths.get(loggingPropertiesFilePathString) + if (!propertiesFilePath.toFile().exists()) { + logger.warning( + "Cannot find the file specified via $JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY: $loggingPropertiesFilePathString" + ) + return + } + LogManager.getLogManager().readConfiguration(Files.newInputStream(propertiesFilePath)) + } catch (e: IOException) { + logger.warning( + "Cannot load the file specified via $JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY: $loggingPropertiesFilePathString. ${e.message}" + ) + } + } + + /** + * Returns a logger for the given class. + */ + @JvmStatic + fun getLogger(clazz: Class<*>) = Logger.getLogger(clazz.name) +} From d5b37d1c522cefff90dbd9c0ba6fcbe9e161ce2b Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 3 Dec 2024 13:42:23 +0100 Subject: [PATCH 03/21] TS-38628 Resolver migration --- .../CucumberPickleDescriptorResolver.java | 187 ------------------ .../ITestDescriptorResolver.java | 25 --- ...tClassBasedTestDescriptorResolverBase.java | 41 ---- .../JUnitJupiterTestDescriptorResolver.java | 35 ---- .../JUnitPlatformSuiteDescriptorResolver.java | 85 -------- .../JUnitVintageTestDescriptorResolver.java | 23 --- .../TestDescriptorResolverRegistry.java | 50 ----- .../test_descriptor/TestDescriptorUtils.java | 147 -------------- .../CucumberPickleDescriptorResolver.kt | 175 ++++++++++++++++ .../ITestDescriptorResolver.kt | 24 +++ ...nitClassBasedTestDescriptorResolverBase.kt | 37 ++++ .../JUnitJupiterTestDescriptorResolver.kt | 31 +++ .../JUnitPlatformSuiteDescriptorResolver.kt | 83 ++++++++ .../JUnitVintageTestDescriptorResolver.kt | 18 ++ .../TestDescriptorResolverRegistry.kt | 49 +++++ .../test_descriptor/TestDescriptorUtils.kt | 148 ++++++++++++++ .../CucumberPickleDescriptorResolverTest.kt | 6 +- 17 files changed, 569 insertions(+), 595 deletions(-) delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.java create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java deleted file mode 100644 index cb75e6c1b..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.teamscale.test_impacted.test_descriptor; - -import com.teamscale.test_impacted.commons.LoggerUtils; -import org.junit.platform.engine.TestDescriptor; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * Test descriptor resolver for Cucumber. For details how we extract the uniform path, see comment in - * {@link #getPickleName(TestDescriptor)}. The cluster id is the .feature file in which the tests are defined. - */ -public class CucumberPickleDescriptorResolver implements ITestDescriptorResolver { - /** Name of the cucumber test engine as used in the unique id of the test descriptor */ - public static final String CUCUMBER_ENGINE_ID = "cucumber"; - /** Type of the unique id segment of a test descriptor representing a cucumber feature file */ - public static final String FEATURE_SEGMENT_TYPE = "feature"; - - private static final Logger LOGGER = LoggerUtils.getLogger(CucumberPickleDescriptorResolver.class); - - @Override - public Optional getUniformPath(TestDescriptor testDescriptor) { - Optional featurePath = getFeaturePath(testDescriptor); - LOGGER.fine(() -> "Resolved feature: " + featurePath); - if (!featurePath.isPresent()) { - LOGGER.severe(() -> "Cannot resolve the feature classpath for " + - testDescriptor + ". This is probably a bug. Please report to CQSE"); - return Optional.empty(); - } - Optional pickleName = getPickleName(testDescriptor); - LOGGER.fine(() -> "Resolved pickle name: " + pickleName); - if (!pickleName.isPresent()) { - LOGGER.severe(() -> "Cannot resolve the pickle name for " + - testDescriptor + ". This is probably a bug. Please report to CQSE"); - return Optional.empty(); - } - String picklePath = featurePath.get() + "/" + pickleName.get(); - - // Add an index to the end of the name in case multiple tests have the same name in the same feature file - Optional featureFileTestDescriptor = getFeatureFileTestDescriptor(testDescriptor); - String indexSuffix; - if (!featureFileTestDescriptor.isPresent()) { - indexSuffix = ""; - } else { - List siblingTestsWithTheSameName = flatListOfAllTestDescriptorChildrenWithPickleName( - featureFileTestDescriptor.get(), pickleName.get()); - int indexOfCurrentTest = siblingTestsWithTheSameName.indexOf(testDescriptor) + 1; - indexSuffix = " #" + indexOfCurrentTest; - } - - String uniformPath = removeDuplicatedSlashes(picklePath + indexSuffix); - LOGGER.fine(() -> "Resolved uniform path: " + uniformPath); - return Optional.of(uniformPath); - } - - @Override - public Optional getClusterId(TestDescriptor testDescriptor) { - return getFeaturePath(testDescriptor).map(this::removeDuplicatedSlashes); - } - - @Override - public String getEngineId() { - return CUCUMBER_ENGINE_ID; - } - - /** - * Transform unique id segments from something like - * [feature:classpath%3Ahellocucumber%2Fcalculator.feature]/[scenario:11]/[examples:16]/[example:21] to - * hellocucumber/calculator.feature/11/16/21 - */ - private Optional getFeaturePath(TestDescriptor testDescriptor) { - LOGGER.fine((() -> "Unique ID of cucumber test descriptor: " + testDescriptor.getUniqueId())); - Optional featureSegment = TestDescriptorUtils.getUniqueIdSegment(testDescriptor, - FEATURE_SEGMENT_TYPE); - LOGGER.fine(() -> "Resolved feature segment: " + featureSegment); - return featureSegment.map(featureClasspathString -> featureClasspathString.replaceAll("classpath:", "")); - } - - /** - * Remove duplicated "/" with one (due to TS-39915) - */ - String removeDuplicatedSlashes(String string) { - return string.replaceAll("(? getPickleName(TestDescriptor testDescriptor) { - // The PickleDescriptor test descriptor class is not public, so we can't import and use it to get access to the pickle attribute containing the name => reflection - // https://github.com/cucumber/cucumber-jvm/blob/main/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NodeDescriptor.java#L90 - // We want to use the name, though, because the unique id of the test descriptor can easily result in inconsistencies, - // e.g. for - // - // Scenario Outline: Add two numbers - // Given I have a calculator - // When I add and - // Then the result should be - // - // Examples: - // | num1 | num2 | total | - // | -2 | 3 | 1 | - // | 10 | 15 | 25 | - // | 12 | 13 | 25 | - // - // tests will be executed for every line of the examples table. The unique id refers to the line number (!) of the example in the .feature file. - // unique id: [...][feature:classpath%3Ahellocucumber%2Fcalculator.feature]/[scenario:11]/[examples:16]/[example:18] <- the latter numbers are line numbers in the file!! - // This means, everytime the line numbers change the test would not be recognised as the same in Teamscale anymore. - // So we use the pickle name (testDescriptor.pickle.getName()) to get the descriptive name "Add two numbers". - // This is not unique yet, as all the executions of the test (all examples) will have the same name then => may not be the case in Teamscale. - // To resolve this, we add an index afterwards in getUniformPath() - - Field pickleField = null; - try { - pickleField = testDescriptor.getClass().getDeclaredField("pickle"); - } catch (NoSuchFieldException e) { - // Pre cucumber 7.11.2, the field was called pickleEvent (see NodeDescriptor in this merge request: https://github.com/cucumber/cucumber-jvm/pull/2711/files) - // ... - } - try { - if (pickleField == null) { - // ... so try again with "pickleEvent" - pickleField = testDescriptor.getClass().getDeclaredField("pickleEvent"); - } - pickleField.setAccessible(true); - Object pickle = pickleField.get(testDescriptor); - // getName() is required by the pickle interface https://github.com/cucumber/cucumber-jvm/blob/main/cucumber-gherkin/src/main/java/io/cucumber/core/gherkin/Pickle.java#L14 - Method getNameMethod = pickle.getClass().getDeclaredMethod("getName"); - getNameMethod.setAccessible(true); - String name = getNameMethod.invoke(pickle).toString(); - - return Optional - .of(name) - .map(CucumberPickleDescriptorResolver::escapeSlashes); - } catch (NoSuchFieldException | InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { - return Optional.empty(); - } - } - - /** - * Escapes slashes (/) in a given input (usually a scenario name) with a backslash (\). - *

- * If a slash is already escaped, no additional escaping is done. - *
    - *
  • {@code / -> \/}
  • - *
  • {@code \/ -> \/}
  • - *
- */ - static String escapeSlashes(String input) { - return input.replaceAll("(? getFeatureFileTestDescriptor(TestDescriptor testDescriptor) { - if (!isFeatureFileTestDescriptor(testDescriptor)) { - if (!testDescriptor.getParent().isPresent()) { - return Optional.empty(); - } - return getFeatureFileTestDescriptor(testDescriptor.getParent().get()); - } - return Optional.of(testDescriptor); - } - - private boolean isFeatureFileTestDescriptor(TestDescriptor cucumberTestDescriptor) { - return cucumberTestDescriptor.getUniqueId().getLastSegment().getType().equals(FEATURE_SEGMENT_TYPE); - } - - private List flatListOfAllTestDescriptorChildrenWithPickleName(TestDescriptor testDescriptor, - String pickleName) { - if (testDescriptor.getChildren().isEmpty()) { - Optional pickleId = getPickleName(testDescriptor); - if (pickleId.isPresent() && pickleName.equals(pickleId.get())) { - return Collections.singletonList(testDescriptor); - } - return Collections.emptyList(); - } - List flattenedChildDescriptors = new ArrayList<>(); - for (TestDescriptor childDescriptor : testDescriptor.getChildren()) { - flattenedChildDescriptors.addAll( - flatListOfAllTestDescriptorChildrenWithPickleName(childDescriptor, pickleName)); - } - return flattenedChildDescriptors; - - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.java deleted file mode 100644 index fe04f90af..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.teamscale.test_impacted.test_descriptor; - -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; - -import java.util.Optional; - -/** Interface for implementation of mappings from {@link TestDescriptor}s to uniform paths. */ -public interface ITestDescriptorResolver { - - /** Type of the unique id segment of a test descriptor representing a test engine */ - String ENGINE_SEGMENT_TYPE = "engine"; - - /** Returns the uniform path or {@link Optional#empty()} if no uniform path could be determined. */ - Optional getUniformPath(TestDescriptor testDescriptor); - - /** Returns the uniform path or {@link Optional#empty()} if no cluster id could be determined. */ - Optional getClusterId(TestDescriptor testDescriptor); - - /** - * Returns the {@link TestEngine#getId()} of the {@link TestEngine} to use this {@link ITestDescriptorResolver} - * for. - */ - String getEngineId(); -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.java deleted file mode 100644 index c8e36cc2a..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.teamscale.test_impacted.test_descriptor; - -import com.teamscale.test_impacted.commons.LoggerUtils; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; - -import java.util.Optional; -import java.util.logging.Logger; - -/** Test descriptor resolver for JUnit based {@link TestEngine}s. */ -public abstract class JUnitClassBasedTestDescriptorResolverBase implements ITestDescriptorResolver { - - private static final Logger LOGGER = LoggerUtils.getLogger(JUnitClassBasedTestDescriptorResolverBase.class); - - @Override - public Optional getUniformPath(TestDescriptor testDescriptor) { - return getClassName(testDescriptor).map(className -> { - String classNameUniformPath = className.replace(".", "/"); - return classNameUniformPath + "/" + testDescriptor.getLegacyReportingName().trim(); - }); - } - - @Override - public Optional getClusterId(TestDescriptor testDescriptor) { - Optional classSegmentName = getClassName(testDescriptor); - - if (!classSegmentName.isPresent()) { - LOGGER.severe( - () -> "Falling back to unique ID as cluster id because class segment name could not be " + - "determined for test descriptor: " + testDescriptor); - // Default to uniform path. - return Optional.of(testDescriptor.getUniqueId().toString()); - } - - return classSegmentName; - } - - /** Returns the test class containing the test. */ - protected abstract Optional getClassName(TestDescriptor testDescriptor); - -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.java deleted file mode 100644 index a6352dab5..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.teamscale.test_impacted.test_descriptor; - -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; - -import java.util.Optional; - -/** Test default test descriptor resolver for the JUnit jupiter {@link TestEngine}. */ -public class JUnitJupiterTestDescriptorResolver extends JUnitClassBasedTestDescriptorResolverBase { - - /** The segment type name that the jupiter engine uses for the class descriptor nodes. */ - public static final String CLASS_SEGMENT_TYPE = "class"; - - /** The segment type name that the jupiter engine uses for the method descriptor nodes. */ - public static final String METHOD_SEGMENT_TYPE = "method"; - - /** The segment type name that the jupiter engine uses for the test factory method descriptor nodes. */ - public static final String TEST_FACTORY_SEGMENT_TYPE = "test-factory"; - - /** The segment type name that the jupiter engine uses for the test template descriptor nodes. */ - public static final String TEST_TEMPLATE_SEGMENT_TYPE = "test-template"; - - /** The segment type name that the jupiter engine uses for dynamic test descriptor nodes. */ - public static final String DYNAMIC_TEST_SEGMENT_TYPE = "dynamic-test"; - - @Override - protected Optional getClassName(TestDescriptor testDescriptor) { - return TestDescriptorUtils.getUniqueIdSegment(testDescriptor, CLASS_SEGMENT_TYPE); - } - - @Override - public String getEngineId() { - return "junit-jupiter"; - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.java deleted file mode 100644 index 7fcb7bdfb..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.teamscale.test_impacted.test_descriptor; - -import com.teamscale.test_impacted.commons.LoggerUtils; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.UniqueId; - -import java.util.List; -import java.util.Optional; -import java.util.function.Function; -import java.util.logging.Logger; - -/** - * Test descriptor resolver for JUnit Platform Suite test (c.f. - * https://junit.org/junit5/docs/current/user-guide/#junit-platform-suite-engine) - */ -public class JUnitPlatformSuiteDescriptorResolver implements ITestDescriptorResolver { - - private static final Logger LOGGER = LoggerUtils.getLogger(JUnitPlatformSuiteDescriptorResolver.class); - /** Type of the unique id segment of a test descriptor representing a test suite */ - public static final String SUITE_SEGMENT_TYPE = "suite"; - - @Override - public Optional getUniformPath(TestDescriptor testDescriptor) { - return extractUniformPathOrClusterId(testDescriptor, "uniform path", - testDescriptorResolver -> testDescriptorResolver.getUniformPath(testDescriptor)); - } - - @Override - public Optional getClusterId(TestDescriptor testDescriptor) { - return extractUniformPathOrClusterId(testDescriptor, "cluster id", - testDescriptorResolver -> testDescriptorResolver.getClusterId(testDescriptor)); - } - - private static Optional extractUniformPathOrClusterId(TestDescriptor testDescriptor, - String nameOfValueToExtractForLogs, - Function> uniformPathOrClusterIdExtractor) { - List segments = testDescriptor.getUniqueId().getSegments(); - if (verifySegments(segments)) { - LOGGER.severe( - () -> "Assuming structure [engine:junit-platform-suite]/[suite:mySuite]/[engine:anotherEngine] " + - "for junit-platform-suite tests. Using " - + testDescriptor.getUniqueId() - .toString() + " as " + nameOfValueToExtractForLogs + " as fallback."); - return Optional.of(testDescriptor.getUniqueId().toString()); - } - - String suite = segments.get(1).getValue().replace('.', '/'); - List secondaryEngineSegments = segments.subList(2, segments.size()); - - ITestDescriptorResolver secondaryTestDescriptorResolver = TestDescriptorResolverRegistry.getTestDescriptorResolver( - secondaryEngineSegments.get(0).getValue()); - if (secondaryTestDescriptorResolver == null) { - LOGGER.severe(() -> "Cannot find a secondary engine nested under the junit-platform-suite engine " + - "(assuming structure [engine:junit-platform-suite]/[suite:mySuite]/[engine:anotherEngine]). " + - "Using " + testDescriptor.getUniqueId() - .toString() + " as " + nameOfValueToExtractForLogs + " as fallback."); - return Optional.of(testDescriptor.getUniqueId().toString()); - } - - Optional secondaryClusterIdOrUniformPath = uniformPathOrClusterIdExtractor.apply( - secondaryTestDescriptorResolver); - if (!secondaryClusterIdOrUniformPath.isPresent()) { - LOGGER.severe(() -> "Secondary test descriptor resolver for engine " + - secondaryEngineSegments.get(0) - .getValue() + " was not able to resolve the " + nameOfValueToExtractForLogs + ". " + - "Using " + testDescriptor.getUniqueId().toString() + " as fallback."); - return Optional.of(testDescriptor.getUniqueId().toString()); - } - - return Optional.of(suite + "/" + secondaryClusterIdOrUniformPath.get()); - } - - private static boolean verifySegments(List segments) { - return segments.size() < 3 || !segments.get(0).getType() - .equals(ITestDescriptorResolver.ENGINE_SEGMENT_TYPE) || !segments.get(1) - .getType() - .equals(SUITE_SEGMENT_TYPE) || !segments.get(2).getType().equals( - ITestDescriptorResolver.ENGINE_SEGMENT_TYPE); - } - - @Override - public String getEngineId() { - return "junit-platform-suite"; - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.java deleted file mode 100644 index 7d7441d76..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.teamscale.test_impacted.test_descriptor; - -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; - -import java.util.Optional; - -/** Test default test descriptor resolver for the JUnit vintage {@link TestEngine}. */ -public class JUnitVintageTestDescriptorResolver extends JUnitClassBasedTestDescriptorResolverBase { - - /** The segment type name that the vintage engine uses for the class descriptor nodes. */ - public static final String RUNNER_SEGMENT_TYPE = "runner"; - - @Override - protected Optional getClassName(TestDescriptor testDescriptor) { - return TestDescriptorUtils.getUniqueIdSegment(testDescriptor, RUNNER_SEGMENT_TYPE); - } - - @Override - public String getEngineId() { - return "junit-vintage"; - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.java deleted file mode 100644 index a6416e8a3..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.teamscale.test_impacted.test_descriptor; - -import com.teamscale.test_impacted.commons.LoggerUtils; -import org.junit.platform.commons.util.ClassLoaderUtils; - -import java.util.HashMap; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.logging.Logger; - -/** - * Registry containing the default and custom {@link ITestDescriptorResolver}s discovered by the java - * {@link ServiceLoader}. - */ -public class TestDescriptorResolverRegistry { - - private static final Logger LOGGER = LoggerUtils.getLogger(TestDescriptorResolverRegistry.class); - - private static final Map TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID = new HashMap<>(); - - static { - // Register default test descriptor resolvers - registerTestDescriptorResolver(new JUnitJupiterTestDescriptorResolver()); - registerTestDescriptorResolver(new JUnitVintageTestDescriptorResolver()); - registerTestDescriptorResolver(new JUnitPlatformSuiteDescriptorResolver()); - registerTestDescriptorResolver(new CucumberPickleDescriptorResolver()); - - // Override existing or register new test descriptor resolvers - for (ITestDescriptorResolver testDescriptorResolver : ServiceLoader - .load(ITestDescriptorResolver.class, ClassLoaderUtils.getDefaultClassLoader())) { - registerTestDescriptorResolver(testDescriptorResolver); - } - } - - private static void registerTestDescriptorResolver(ITestDescriptorResolver testDescriptorResolver) { - TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID.put(testDescriptorResolver.getEngineId(), testDescriptorResolver); - } - - /** Returns the test descriptor resolver or null if none exists for the test engine. */ - public static ITestDescriptorResolver getTestDescriptorResolver(String testEngineId) { - if (!TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID.containsKey(testEngineId)) { - LOGGER.warning(() -> testEngineId + " is not officially supported! You can add support by " + - "implementing the ITestDescriptorResolver interface and making the implementation " + - "discoverable via the Java Service Loader mechanism!"); - return TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID.get("junit-jupiter"); - } - return TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID.get(testEngineId); - } - -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.java deleted file mode 100644 index db88cb0ae..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.teamscale.test_impacted.test_descriptor; - -import com.teamscale.client.ClusteredTestDetails; -import com.teamscale.client.TestDetails; -import com.teamscale.test_impacted.commons.IndentingWriter; -import com.teamscale.test_impacted.commons.LoggerUtils; -import com.teamscale.test_impacted.engine.executor.AvailableTests; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestSource; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.UniqueId.Segment; -import org.junit.platform.engine.support.descriptor.ClassSource; -import org.junit.platform.engine.support.descriptor.MethodSource; - -import java.util.List; -import java.util.Optional; -import java.util.logging.Logger; -import java.util.stream.Stream; - -/** Class containing utility methods for {@link TestDescriptor}s. */ -public class TestDescriptorUtils { - - private static final Logger LOGGER = LoggerUtils.getLogger(TestDescriptorUtils.class); - - /** Returns the test descriptor as a formatted string with indented children. */ - public static String getTestDescriptorAsString(TestDescriptor testDescriptor) { - IndentingWriter writer = new IndentingWriter(); - printTestDescriptor(writer, testDescriptor); - return writer.toString(); - } - - private static void printTestDescriptor(IndentingWriter writer, TestDescriptor testDescriptor) { - writer.writeLine(testDescriptor.getUniqueId().toString()); - writer.indent(() -> { - for (TestDescriptor child : testDescriptor.getChildren()) { - printTestDescriptor(writer, child); - } - }); - } - - /** - * Returns true if the {@link TestDescriptor} is an actual representative of a test. A representative of a test is - * either a regular test that was not dynamically generated or a test container that dynamically registers multiple - * test cases. - */ - public static boolean isTestRepresentative(TestDescriptor testDescriptor) { - boolean isTestTemplateOrTestFactory = isTestTemplateOrTestFactory(testDescriptor); - boolean isNonParameterizedTest = testDescriptor.isTest() && !isTestTemplateOrTestFactory( - testDescriptor.getParent().get()); - return isNonParameterizedTest || isTestTemplateOrTestFactory; - } - - /** - * Returns true if a {@link TestDescriptor} represents a test template or a test factory. - *

- * An example of a {@link UniqueId} of the {@link TestDescriptor} is: - *

- * {@code - * [engine:junit-jupiter]/[class:com.example.project.JUnit5Test]/[test-template:withValueSource(java.lang.String)]} - */ - public static boolean isTestTemplateOrTestFactory(TestDescriptor testDescriptor) { - if (testDescriptor == null) { - return false; - } - List segments = testDescriptor.getUniqueId().getSegments(); - - if (segments.isEmpty()) { - return false; - } - - String lastSegmentType = segments.get(segments.size() - 1).getType(); - return JUnitJupiterTestDescriptorResolver.TEST_TEMPLATE_SEGMENT_TYPE.equals( - lastSegmentType) || JUnitJupiterTestDescriptorResolver.TEST_FACTORY_SEGMENT_TYPE.equals( - lastSegmentType); - } - - /** Creates a stream of the test representatives contained by the {@link TestDescriptor}. */ - public static Stream streamTestRepresentatives(TestDescriptor testDescriptor) { - if (isTestRepresentative(testDescriptor)) { - return Stream.of(testDescriptor); - } - return testDescriptor.getChildren().stream().flatMap(TestDescriptorUtils::streamTestRepresentatives); - } - - /** - * Returns the {@link Segment#getValue()} matching the type or {@link Optional#empty()} if no matching segment can - * be found. - */ - public static Optional getUniqueIdSegment(TestDescriptor testDescriptor, String type) { - return testDescriptor.getUniqueId().getSegments().stream().filter(segment -> segment.getType().equals(type)) - .findFirst().map( - Segment::getValue); - } - - /** Returns {@link TestDetails#sourcePath} for a {@link TestDescriptor}. */ - public static String getSource(TestDescriptor testDescriptor) { - Optional source = testDescriptor.getSource(); - if (source.isPresent() && source.get() instanceof MethodSource) { - MethodSource ms = (MethodSource) source.get(); - return ms.getClassName().replace('.', '/'); - } - if (source.isPresent() && source.get() instanceof ClassSource) { - ClassSource classSource = (ClassSource) source.get(); - return classSource.getClassName().replace('.', '/'); - } - return null; - } - - /** Returns the {@link AvailableTests} contained within the root {@link TestDescriptor}. */ - public static AvailableTests getAvailableTests(TestDescriptor rootTestDescriptor, - String partition) { - AvailableTests availableTests = new AvailableTests(); - - TestDescriptorUtils.streamTestRepresentatives(rootTestDescriptor) - .forEach(testDescriptor -> { - Optional engineId = testDescriptor.getUniqueId().getEngineId(); - if (!engineId.isPresent()) { - LOGGER.severe(() -> "Unable to determine engine ID for " + testDescriptor + "!"); - return; - } - - ITestDescriptorResolver testDescriptorResolver = TestDescriptorResolverRegistry - .getTestDescriptorResolver(engineId.get()); - Optional clusterId = testDescriptorResolver.getClusterId(testDescriptor); - Optional uniformPath = testDescriptorResolver.getUniformPath(testDescriptor); - String source = TestDescriptorUtils.getSource(testDescriptor); - - if (!uniformPath.isPresent()) { - LOGGER.severe(() -> "Unable to determine uniform path for test descriptor: " + testDescriptor); - return; - } - - if (!clusterId.isPresent()) { - LOGGER.severe( - () -> "Unable to determine cluster id path for test descriptor: " + testDescriptor); - return; - } - - ClusteredTestDetails testDetails = new ClusteredTestDetails(uniformPath.get(), source, null, - clusterId.get(), partition); - availableTests.add(testDescriptor.getUniqueId(), testDetails); - }); - - - return availableTests; - } -} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt new file mode 100644 index 000000000..03da06622 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt @@ -0,0 +1,175 @@ +package com.teamscale.test_impacted.test_descriptor + +import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import org.junit.platform.engine.TestDescriptor +import java.lang.reflect.Field +import java.lang.reflect.InvocationTargetException +import java.util.* +import java.util.logging.Logger + +/** + * Test descriptor resolver for Cucumber. For details how we extract the uniform path, see comment in + * [getPickleName]. The cluster id is the .feature file in which the tests are defined. + */ +class CucumberPickleDescriptorResolver : ITestDescriptorResolver { + override fun getUniformPath(testDescriptor: TestDescriptor): Optional { + val featurePath = testDescriptor.featurePath() + LOGGER.fine { "Resolved feature: $featurePath" } + if (!featurePath.isPresent) { + LOGGER.severe { + "Cannot resolve the feature classpath for $testDescriptor. This is probably a bug. Please report to CQSE" + } + return Optional.empty() + } + val pickleName = testDescriptor.getPickleName() + LOGGER.fine { "Resolved pickle name: $pickleName" } + if (!pickleName.isPresent) { + LOGGER.severe { + "Cannot resolve the pickle name for $testDescriptor. This is probably a bug. Please report to CQSE" + } + return Optional.empty() + } + val picklePath = "${featurePath.get()}/${pickleName.get()}" + + // Add an index to the end of the name in case multiple tests have the same name in the same feature file + val featureFileTestDescriptor = getFeatureFileTestDescriptor(testDescriptor) + val indexSuffix = if (!featureFileTestDescriptor.isPresent) { + "" + } else { + val testsWithTheSameName = flatListOfAllTestDescriptorChildrenWithPickleName( + featureFileTestDescriptor.get(), pickleName.get() + ) + val indexOfCurrentTest = testsWithTheSameName.indexOf(testDescriptor) + 1 + " #$indexOfCurrentTest" + } + + val uniformPath = (picklePath + indexSuffix).removeDuplicatedSlashes() + LOGGER.fine { "Resolved uniform path: $uniformPath" } + return Optional.of(uniformPath) + } + + override fun getClusterId(testDescriptor: TestDescriptor): Optional = + testDescriptor.featurePath().map { it.removeDuplicatedSlashes() } + + override val engineId = CUCUMBER_ENGINE_ID + + /** + * Transform unique id segments from something like + * [feature:classpath%3Ahellocucumber%2Fcalculator.feature]/[scenario:11]/[examples:16]/[example:21] to + * hellocucumber/calculator.feature/11/16/21 + */ + private fun TestDescriptor.featurePath(): Optional { + LOGGER.fine { "Unique ID of cucumber test descriptor: $uniqueId" } + val featureSegment = TestDescriptorUtils.getUniqueIdSegment( + this, FEATURE_SEGMENT_TYPE + ) + LOGGER.fine { "Resolved feature segment: $featureSegment" } + return featureSegment.map { it.replace("classpath:".toRegex(), "") } + } + + private fun TestDescriptor.getPickleName(): Optional { + // The PickleDescriptor test descriptor class is not public, so we can't import and use it to get access to the pickle attribute containing the name => reflection + // https://github.com/cucumber/cucumber-jvm/blob/main/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NodeDescriptor.java#L90 + // We want to use the name, though, because the unique id of the test descriptor can easily result in inconsistencies, + // e.g. for + // + // Scenario Outline: Add two numbers + // Given I have a calculator + // When I add and + // Then the result should be + // + // Examples: + // | num1 | num2 | total | + // | -2 | 3 | 1 | + // | 10 | 15 | 25 | + // | 12 | 13 | 25 | + // + // tests will be executed for every line of the examples table. The unique id refers to the line number (!) of the example in the .feature file. + // unique id: [...][feature:classpath%3Ahellocucumber%2Fcalculator.feature]/[scenario:11]/[examples:16]/[example:18] <- the latter numbers are line numbers in the file!! + // This means, everytime the line numbers change the test would not be recognised as the same in Teamscale anymore. + // So we use the pickle name (testDescriptor.pickle.getName()) to get the descriptive name "Add two numbers". + // This is not unique yet, as all the executions of the test (all examples) will have the same name then => may not be the case in Teamscale. + // To resolve this, we add an index afterwards in getUniformPath() + + var pickleField: Field? = null + try { + pickleField = javaClass.getDeclaredField("pickle") + } catch (e: NoSuchFieldException) { + // Pre cucumber 7.11.2, the field was called pickleEvent (see NodeDescriptor in this merge request: https://github.com/cucumber/cucumber-jvm/pull/2711/files) + // ... + } + return runCatching { + if (pickleField == null) { + // ... so try again with "pickleEvent" + pickleField = javaClass.getDeclaredField("pickleEvent") + } + pickleField?.let { field -> + field.isAccessible = true + val pickle = field.get(this) + // getName() is required by the pickle interface + val getNameMethod = pickle.javaClass.getDeclaredMethod("getName") + getNameMethod.isAccessible = true + val name = getNameMethod.invoke(pickle).toString() + Optional.of(name) + .map { input -> input.escapeSlashes() } + } ?: Optional.empty() + }.getOrNull() ?: Optional.empty() + } + + private fun getFeatureFileTestDescriptor(testDescriptor: TestDescriptor): Optional { + if (!isFeatureFileTestDescriptor(testDescriptor)) { + if (!testDescriptor.parent.isPresent) { + return Optional.empty() + } + return getFeatureFileTestDescriptor(testDescriptor.parent.get()) + } + return Optional.of(testDescriptor) + } + + private fun isFeatureFileTestDescriptor(cucumberTestDescriptor: TestDescriptor) = + cucumberTestDescriptor.uniqueId.lastSegment.type == FEATURE_SEGMENT_TYPE + + private fun flatListOfAllTestDescriptorChildrenWithPickleName( + testDescriptor: TestDescriptor, + pickleName: String + ): List { + if (testDescriptor.children.isEmpty()) { + val pickleId = testDescriptor.getPickleName() + if (pickleId.isPresent && pickleName == pickleId.get()) { + return listOf(testDescriptor) + } + return emptyList() + } + return testDescriptor.children.flatMap { childDescriptor -> + flatListOfAllTestDescriptorChildrenWithPickleName(childDescriptor, pickleName) + } + } + + companion object { + /** Name of the cucumber test engine as used in the unique id of the test descriptor */ + const val CUCUMBER_ENGINE_ID = "cucumber" + + /** Type of the unique id segment of a test descriptor representing a cucumber feature file */ + const val FEATURE_SEGMENT_TYPE = "feature" + + private val LOGGER: Logger = getLogger(CucumberPickleDescriptorResolver::class.java) + + /** + * Escapes slashes (/) in a given input (usually a scenario name) with a backslash (\). + *



+ * If a slash is already escaped, no additional escaping is done. + * + * * `/ -> \/` + * * `\/ -> \/` + * + */ + fun String.escapeSlashes() = + replace("(? + + /** Returns the uniform path or [Optional.empty] if no cluster id could be determined. */ + fun getClusterId(testDescriptor: TestDescriptor): Optional + + /** + * Returns the [org.junit.platform.engine.TestEngine.getId] of the [org.junit.platform.engine.TestEngine] to use this [ITestDescriptorResolver] + * for. + */ + val engineId: String + + companion object { + /** Type of the unique id segment of a test descriptor representing a test engine */ + const val ENGINE_SEGMENT_TYPE = "engine" + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt new file mode 100644 index 000000000..1ebee1826 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt @@ -0,0 +1,37 @@ +package com.teamscale.test_impacted.test_descriptor + +import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import org.junit.platform.engine.TestDescriptor +import java.util.* +import java.util.logging.Logger + +/** Test descriptor resolver for JUnit based [org.junit.platform.engine.TestEngine]s. */ +abstract class JUnitClassBasedTestDescriptorResolverBase : ITestDescriptorResolver { + override fun getUniformPath(testDescriptor: TestDescriptor): Optional = + getClassName(testDescriptor).map { className -> + val dotName = className.replace(".", "/") + "$dotName/${testDescriptor.legacyReportingName.trim { it <= ' ' }}" + } + + override fun getClusterId(testDescriptor: TestDescriptor): Optional { + val classSegmentName = getClassName(testDescriptor) + + if (!classSegmentName.isPresent) { + LOGGER.severe { + "Falling back to unique ID as cluster id because class segment name could not be " + + "determined for test descriptor: " + testDescriptor + } + // Default to uniform path. + return Optional.of(testDescriptor.uniqueId.toString()) + } + + return classSegmentName + } + + /** Returns the test class containing the test. */ + protected abstract fun getClassName(testDescriptor: TestDescriptor): Optional + + companion object { + private val LOGGER = getLogger(JUnitClassBasedTestDescriptorResolverBase::class.java) + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.kt new file mode 100644 index 000000000..34c35f51a --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.kt @@ -0,0 +1,31 @@ +package com.teamscale.test_impacted.test_descriptor + +import org.junit.platform.engine.TestDescriptor +import java.util.* + +/** Test default test descriptor resolver for the JUnit jupiter [TestEngine]. */ +class JUnitJupiterTestDescriptorResolver : JUnitClassBasedTestDescriptorResolverBase() { + override fun getClassName(testDescriptor: TestDescriptor): Optional { + return TestDescriptorUtils.getUniqueIdSegment(testDescriptor, CLASS_SEGMENT_TYPE) + } + + override val engineId: String + get() = "junit-jupiter" + + companion object { + /** The segment type name that the jupiter engine uses for the class descriptor nodes. */ + const val CLASS_SEGMENT_TYPE = "class" + + /** The segment type name that the jupiter engine uses for the method descriptor nodes. */ + const val METHOD_SEGMENT_TYPE = "method" + + /** The segment type name that the jupiter engine uses for the test factory method descriptor nodes. */ + const val TEST_FACTORY_SEGMENT_TYPE = "test-factory" + + /** The segment type name that the jupiter engine uses for the test template descriptor nodes. */ + const val TEST_TEMPLATE_SEGMENT_TYPE = "test-template" + + /** The segment type name that the jupiter engine uses for dynamic test descriptor nodes. */ + const val DYNAMIC_TEST_SEGMENT_TYPE = "dynamic-test" + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt new file mode 100644 index 000000000..6e8f9f352 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt @@ -0,0 +1,83 @@ +package com.teamscale.test_impacted.test_descriptor + +import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import org.junit.platform.engine.TestDescriptor +import org.junit.platform.engine.UniqueId +import java.util.* +import java.util.function.Function +import java.util.logging.Logger + +/** + * Test descriptor resolver for JUnit Platform Suite test (c.f. + * https://junit.org/junit5/docs/current/user-guide/#junit-platform-suite-engine) + */ +class JUnitPlatformSuiteDescriptorResolver : ITestDescriptorResolver { + override fun getUniformPath(testDescriptor: TestDescriptor) = + extractUniformPathOrClusterId( + testDescriptor, "uniform path" + ) { it.getUniformPath(testDescriptor) } + + override fun getClusterId(testDescriptor: TestDescriptor) = + extractUniformPathOrClusterId( + testDescriptor, "cluster id" + ) { it.getClusterId(testDescriptor) } + + override val engineId: String + get() = "junit-platform-suite" + + companion object { + private val LOGGER = getLogger(JUnitPlatformSuiteDescriptorResolver::class.java) + + /** Type of the unique id segment of a test descriptor representing a test suite */ + private const val SUITE_SEGMENT_TYPE: String = "suite" + + private fun extractUniformPathOrClusterId( + testDescriptor: TestDescriptor, + nameOfValueToExtractForLogs: String, + uniformPathOrClusterIdExtractor: (ITestDescriptorResolver) -> Optional + ): Optional { + val segments = testDescriptor.uniqueId.segments + if (verifySegments(segments)) { + LOGGER.severe { + "Assuming structure [engine:junit-platform-suite]/[suite:mySuite]/[engine:anotherEngine] for junit-platform-suite tests. Using ${testDescriptor.uniqueId} as $nameOfValueToExtractForLogs as fallback." + } + return Optional.of(testDescriptor.uniqueId.toString()) + } + + val suite = segments[1].value.replace('.', '/') + val secondaryEngineSegments = segments.subList(2, segments.size) + + val secondaryTestDescriptorResolver = TestDescriptorResolverRegistry.getTestDescriptorResolver( + secondaryEngineSegments[0].value + ) + if (secondaryTestDescriptorResolver == null) { + LOGGER.severe { + "Cannot find a secondary engine nested under the junit-platform-suite engine " + + "(assuming structure [engine:junit-platform-suite]/[suite:mySuite]/[engine:anotherEngine]). " + + "Using " + testDescriptor.uniqueId + .toString() + " as " + nameOfValueToExtractForLogs + " as fallback." + } + return Optional.of(testDescriptor.uniqueId.toString()) + } + + val secondaryClusterIdOrUniformPath = uniformPathOrClusterIdExtractor(secondaryTestDescriptorResolver) + if (!secondaryClusterIdOrUniformPath.isPresent) { + LOGGER.severe { + "Secondary test descriptor resolver for engine " + + secondaryEngineSegments[0] + .value + " was not able to resolve the " + nameOfValueToExtractForLogs + ". " + + "Using " + testDescriptor.uniqueId.toString() + " as fallback." + } + return Optional.of(testDescriptor.uniqueId.toString()) + } + + return Optional.of(suite + "/" + secondaryClusterIdOrUniformPath.get()) + } + + private fun verifySegments(segments: List) = + segments.size < 3 + || (segments[0].type != ITestDescriptorResolver.ENGINE_SEGMENT_TYPE) + || (segments[1].type != SUITE_SEGMENT_TYPE) + || (segments[2].type != ITestDescriptorResolver.ENGINE_SEGMENT_TYPE) + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt new file mode 100644 index 000000000..fa4aa2293 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt @@ -0,0 +1,18 @@ +package com.teamscale.test_impacted.test_descriptor + +import org.junit.platform.engine.TestDescriptor +import java.util.* + +/** Test default test descriptor resolver for the JUnit vintage [org.junit.platform.engine.TestEngine]. */ +class JUnitVintageTestDescriptorResolver : JUnitClassBasedTestDescriptorResolverBase() { + override fun getClassName(testDescriptor: TestDescriptor) = + TestDescriptorUtils.getUniqueIdSegment(testDescriptor, RUNNER_SEGMENT_TYPE) + + override val engineId: String + get() = "junit-vintage" + + companion object { + /** The segment type name that the vintage engine uses for the class descriptor nodes. */ + const val RUNNER_SEGMENT_TYPE = "runner" + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt new file mode 100644 index 000000000..2b23d766a --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt @@ -0,0 +1,49 @@ +package com.teamscale.test_impacted.test_descriptor + +import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import org.junit.platform.commons.util.ClassLoaderUtils +import java.util.* +import java.util.logging.Logger + +/** + * Registry containing the default and custom [ITestDescriptorResolver]s discovered by the java + * [ServiceLoader]. + */ +object TestDescriptorResolverRegistry { + private val LOGGER = getLogger(TestDescriptorResolverRegistry::class.java) + + private val TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID = mutableMapOf() + + init { + // Register default test descriptor resolvers + registerTestDescriptorResolver(JUnitJupiterTestDescriptorResolver()) + registerTestDescriptorResolver(JUnitVintageTestDescriptorResolver()) + registerTestDescriptorResolver(JUnitPlatformSuiteDescriptorResolver()) + registerTestDescriptorResolver(CucumberPickleDescriptorResolver()) + + // Override existing or register new test descriptor resolvers + for (testDescriptorResolver in ServiceLoader + .load(ITestDescriptorResolver::class.java, ClassLoaderUtils.getDefaultClassLoader())) { + registerTestDescriptorResolver(testDescriptorResolver) + } + } + + private fun registerTestDescriptorResolver(testDescriptorResolver: ITestDescriptorResolver) { + TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID[testDescriptorResolver.engineId] = + testDescriptorResolver + } + + /** Returns the test descriptor resolver or null if none exists for the test engine. */ + @JvmStatic + fun getTestDescriptorResolver(testEngineId: String): ITestDescriptorResolver? { + if (!TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID.containsKey(testEngineId)) { + LOGGER.warning { + testEngineId + " is not officially supported! You can add support by " + + "implementing the ITestDescriptorResolver interface and making the implementation " + + "discoverable via the Java Service Loader mechanism!" + } + return TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID["junit-jupiter"] + } + return TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID[testEngineId] + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt new file mode 100644 index 000000000..32bdf0a19 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt @@ -0,0 +1,148 @@ +package com.teamscale.test_impacted.test_descriptor + +import com.teamscale.client.ClusteredTestDetails +import com.teamscale.test_impacted.commons.IndentingWriter +import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import com.teamscale.test_impacted.engine.executor.AvailableTests +import org.junit.platform.engine.TestDescriptor +import org.junit.platform.engine.UniqueId +import org.junit.platform.engine.support.descriptor.ClassSource +import org.junit.platform.engine.support.descriptor.MethodSource +import java.util.* +import java.util.logging.Logger +import java.util.stream.Stream + +/** Class containing utility methods for [TestDescriptor]s. */ +object TestDescriptorUtils { + private val LOGGER = getLogger(TestDescriptorUtils::class.java) + + /** Returns the test descriptor as a formatted string with indented children. */ + @JvmStatic + fun getTestDescriptorAsString(testDescriptor: TestDescriptor): String { + val writer = IndentingWriter() + printTestDescriptor(writer, testDescriptor) + return writer.toString() + } + + private fun printTestDescriptor(writer: IndentingWriter, testDescriptor: TestDescriptor) { + writer.writeLine(testDescriptor.uniqueId.toString()) + writer.indent { + testDescriptor.children.forEach { child -> + printTestDescriptor(writer, child) + } + } + } + + /** + * Returns true if the [TestDescriptor] is an actual representative of a test. A representative of a test is + * either a regular test that was not dynamically generated or a test container that dynamically registers multiple + * test cases. + */ + @JvmStatic + fun isTestRepresentative(testDescriptor: TestDescriptor): Boolean { + val isTestTemplateOrTestFactory = isTestTemplateOrTestFactory(testDescriptor) + val isNonParameterizedTest = testDescriptor.isTest && !isTestTemplateOrTestFactory( + testDescriptor.parent.get() + ) + return isNonParameterizedTest || isTestTemplateOrTestFactory + } + + /** + * Returns true if a [TestDescriptor] represents a test template or a test factory. + * + * + * An example of a [UniqueId] of the [TestDescriptor] is: + * + * + * `[engine:junit-jupiter]/[class:com.example.project.JUnit5Test]/[test-template:withValueSource(java.lang.String)]` + */ + private fun isTestTemplateOrTestFactory(testDescriptor: TestDescriptor?): Boolean { + if (testDescriptor == null) { + return false + } + val segments = testDescriptor.uniqueId.segments + + if (segments.isEmpty()) { + return false + } + + val lastSegmentType = segments[segments.size - 1].type + return JUnitJupiterTestDescriptorResolver.TEST_TEMPLATE_SEGMENT_TYPE == lastSegmentType + || JUnitJupiterTestDescriptorResolver.TEST_FACTORY_SEGMENT_TYPE == lastSegmentType + } + + /** Creates a stream of the test representatives contained by the [TestDescriptor]. */ + private fun streamTestRepresentatives(testDescriptor: TestDescriptor): Stream { + if (isTestRepresentative(testDescriptor)) { + return Stream.of(testDescriptor) + } + return testDescriptor.children.stream().flatMap { + streamTestRepresentatives(it) + } + } + + /** + * Returns the [Segment.getValue] matching the type or [Optional.empty] if no matching segment can + * be found. + */ + fun getUniqueIdSegment(testDescriptor: TestDescriptor, type: String): Optional = + testDescriptor.uniqueId.segments.stream() + .filter { it.type == type } + .findFirst().map { it.value } + + /** Returns [TestDetails.sourcePath] for a [TestDescriptor]. */ + private fun getSource(testDescriptor: TestDescriptor): String? { + val source = testDescriptor.source + if (source.isPresent && source.get() is MethodSource) { + val ms = source.get() as MethodSource + return ms.className.replace('.', '/') + } + if (source.isPresent && source.get() is ClassSource) { + val classSource = source.get() as ClassSource + return classSource.className.replace('.', '/') + } + return null + } + + /** Returns the [AvailableTests] contained within the root [TestDescriptor]. */ + @JvmStatic + fun getAvailableTests( + rootTestDescriptor: TestDescriptor, + partition: String? + ): AvailableTests { + val availableTests = AvailableTests() + + streamTestRepresentatives(rootTestDescriptor) + .forEach { testDescriptor: TestDescriptor -> + val engineId = testDescriptor.uniqueId.engineId + if (!engineId.isPresent) { + LOGGER.severe { "Unable to determine engine ID for $testDescriptor!" } + return@forEach + } + + val testDescriptorResolver = TestDescriptorResolverRegistry.getTestDescriptorResolver(engineId.get()) + val clusterId = testDescriptorResolver!!.getClusterId(testDescriptor) + val uniformPath = testDescriptorResolver.getUniformPath(testDescriptor) + val source = getSource(testDescriptor) + + if (!uniformPath.isPresent) { + LOGGER.severe { "Unable to determine uniform path for test descriptor: $testDescriptor" } + return@forEach + } + + if (!clusterId.isPresent) { + LOGGER.severe { "Unable to determine cluster id path for test descriptor: $testDescriptor" } + return@forEach + } + + val testDetails = ClusteredTestDetails( + uniformPath.get(), source, null, + clusterId.get(), partition + ) + availableTests.add(testDescriptor.uniqueId, testDetails) + } + + + return availableTests + } +} diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.kt index fc99686e3..2f1c88690 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.kt @@ -1,5 +1,7 @@ package com.teamscale.test_impacted.test_descriptor +import com.teamscale.test_impacted.test_descriptor.CucumberPickleDescriptorResolver.Companion.escapeSlashes +import com.teamscale.test_impacted.test_descriptor.CucumberPickleDescriptorResolver.Companion.removeDuplicatedSlashes import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -21,7 +23,7 @@ internal class CucumberPickleDescriptorResolverTest { "\\" to "\\", "http://link" to "http:\\/\\/link" ).forEach { (input, expected) -> - Assertions.assertEquals(expected, CucumberPickleDescriptorResolver.escapeSlashes(input)) + Assertions.assertEquals(expected, input.escapeSlashes()) } } @@ -42,7 +44,7 @@ internal class CucumberPickleDescriptorResolverTest { "\\" to "\\", "\\\\" to "\\\\" ).forEach { (input, expected) -> - Assertions.assertEquals(expected, CucumberPickleDescriptorResolver().removeDuplicatedSlashes(input)) + Assertions.assertEquals(expected, input.removeDuplicatedSlashes()) } } } \ No newline at end of file From b171c3de85aeeeed489f0365403694eda6535638 Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 3 Dec 2024 14:36:41 +0100 Subject: [PATCH 04/21] TS-38628 Cleanup tests --- .../CucumberPickleDescriptorResolver.kt | 1 - .../JUnitClassBasedTestDescriptorResolverBase.kt | 1 - .../JUnitPlatformSuiteDescriptorResolver.kt | 2 -- .../JUnitVintageTestDescriptorResolver.kt | 1 - .../TestDescriptorResolverRegistry.kt | 1 - .../test_descriptor/TestDescriptorUtils.kt | 1 - .../ImpactedTestEngineWithDynamicTestsTest.kt | 7 ++----- .../engine/ImpactedTestEngineWithTwoEnginesTest.kt | 13 +++++-------- .../test_impacted/engine/NoImpactedTestsTest.kt | 8 ++------ 9 files changed, 9 insertions(+), 26 deletions(-) diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt index 03da06622..0c87d5fae 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt @@ -3,7 +3,6 @@ package com.teamscale.test_impacted.test_descriptor import com.teamscale.test_impacted.commons.LoggerUtils.getLogger import org.junit.platform.engine.TestDescriptor import java.lang.reflect.Field -import java.lang.reflect.InvocationTargetException import java.util.* import java.util.logging.Logger diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt index 1ebee1826..705fc44e3 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt @@ -3,7 +3,6 @@ package com.teamscale.test_impacted.test_descriptor import com.teamscale.test_impacted.commons.LoggerUtils.getLogger import org.junit.platform.engine.TestDescriptor import java.util.* -import java.util.logging.Logger /** Test descriptor resolver for JUnit based [org.junit.platform.engine.TestEngine]s. */ abstract class JUnitClassBasedTestDescriptorResolverBase : ITestDescriptorResolver { diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt index 6e8f9f352..e69eef447 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt @@ -4,8 +4,6 @@ import com.teamscale.test_impacted.commons.LoggerUtils.getLogger import org.junit.platform.engine.TestDescriptor import org.junit.platform.engine.UniqueId import java.util.* -import java.util.function.Function -import java.util.logging.Logger /** * Test descriptor resolver for JUnit Platform Suite test (c.f. diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt index fa4aa2293..de5f5aa1b 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt @@ -1,7 +1,6 @@ package com.teamscale.test_impacted.test_descriptor import org.junit.platform.engine.TestDescriptor -import java.util.* /** Test default test descriptor resolver for the JUnit vintage [org.junit.platform.engine.TestEngine]. */ class JUnitVintageTestDescriptorResolver : JUnitClassBasedTestDescriptorResolverBase() { diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt index 2b23d766a..d1873a360 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt @@ -3,7 +3,6 @@ package com.teamscale.test_impacted.test_descriptor import com.teamscale.test_impacted.commons.LoggerUtils.getLogger import org.junit.platform.commons.util.ClassLoaderUtils import java.util.* -import java.util.logging.Logger /** * Registry containing the default and custom [ITestDescriptorResolver]s discovered by the java diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt index 32bdf0a19..f8eec77ce 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt @@ -9,7 +9,6 @@ import org.junit.platform.engine.UniqueId import org.junit.platform.engine.support.descriptor.ClassSource import org.junit.platform.engine.support.descriptor.MethodSource import java.util.* -import java.util.logging.Logger import java.util.stream.Stream /** Class containing utility methods for [TestDescriptor]s. */ diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt index acdbd10f6..daa425c82 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt @@ -35,18 +35,15 @@ internal class ImpactedTestEngineWithDynamicTestsTest : ImpactedTestEngineTestBa ) private val testRoot = SimpleTestDescriptor.testContainer(engineRootId, dynamicTestClassCase) - override val engines: List by lazy { - listOf(DummyEngine(testRoot)) - } + override val engines get() = listOf(DummyEngine(testRoot)) - override val impactedTests: List by lazy { + override val impactedTests get() = listOf( PrioritizableTestCluster( "example/DynamicTest", listOf(PrioritizableTest("example/DynamicTest/testFactory()")) ) ) - } override fun verifyCallbacks(executionListener: EngineExecutionListener) { // First the parents test descriptors are started in order. diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt index 89af6af95..02f5e7b0b 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt @@ -99,14 +99,12 @@ internal class ImpactedTestEngineWithTwoEnginesTest : ImpactedTestEngineTestBase private val testEngine2Root = SimpleTestDescriptor.testContainer(engine2RootId, otherTestClass) - override val engines: List by lazy { - listOf( - DummyEngine(testEngine1Root), - DummyEngine(testEngine2Root) - ) - } + override val engines get() = listOf( + DummyEngine(testEngine1Root), + DummyEngine(testEngine2Root) + ) - override val impactedTests: List by lazy { + override val impactedTests get() = listOf( PrioritizableTestCluster( FIRST_TEST_CLASS, @@ -128,7 +126,6 @@ internal class ImpactedTestEngineWithTwoEnginesTest : ImpactedTestEngineTestBase ) ) ) - } override fun verifyCallbacks(executionListener: EngineExecutionListener) { // Start of engine 1 diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt index ca84332e5..be21cbd55 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt @@ -29,12 +29,8 @@ internal class NoImpactedTestsTest : ImpactedTestEngineTestBase() { private val testEngine1Root = SimpleTestDescriptor.testContainer(engine1RootId, firstTestClass) - override val engines: List by lazy { - listOf(DummyEngine(testEngine1Root)) - } - - override val impactedTests: List - get() = emptyList() + override val engines get() = listOf(DummyEngine(testEngine1Root)) + override val impactedTests get() = emptyList() override fun verifyCallbacks(executionListener: EngineExecutionListener) {} From 2b76f2a299a74f0e4514e0be69950f1f34734a16 Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 3 Dec 2024 23:39:23 +0100 Subject: [PATCH 05/21] TS-38628 Refactor test descriptors --- ...seCoverageCollectingExecutionListener.java | 10 +-- .../CucumberPickleDescriptorResolver.kt | 66 ++++++++--------- .../ITestDescriptorResolver.kt | 4 +- ...nitClassBasedTestDescriptorResolverBase.kt | 17 +++-- .../JUnitJupiterTestDescriptorResolver.kt | 6 +- .../JUnitPlatformSuiteDescriptorResolver.kt | 51 ++++++------- .../JUnitVintageTestDescriptorResolver.kt | 5 +- .../TestDescriptorResolverRegistry.kt | 22 +++--- .../test_descriptor/TestDescriptorUtils.kt | 71 +++++++++---------- ...JUnit5TestwiseCoverageExecutionListener.kt | 24 +++---- 10 files changed, 127 insertions(+), 149 deletions(-) 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 index 367d2043b..5beea9f21 100644 --- 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 @@ -21,7 +21,7 @@ import java.util.Optional; import java.util.logging.Logger; -import static com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.isTestRepresentative; +import static com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.isRepresentative; /** * An execution listener which delegates events to another {@link EngineExecutionListener} and notifies Teamscale agents @@ -61,9 +61,9 @@ public void dynamicTestRegistered(TestDescriptor testDescriptor) { @Override public void executionSkipped(TestDescriptor testDescriptor, String reason) { - if (!TestDescriptorUtils.isTestRepresentative(testDescriptor)) { + if (!TestDescriptorUtils.isRepresentative(testDescriptor)) { delegateEngineExecutionListener.executionStarted(testDescriptor); - testDescriptor.getChildren().forEach(child -> this.executionSkipped(child, reason)); + testDescriptor.getChildren().forEach(child -> executionSkipped(child, reason)); delegateEngineExecutionListener.executionFinished(testDescriptor, TestExecutionResult.successful()); return; } @@ -76,7 +76,7 @@ public void executionSkipped(TestDescriptor testDescriptor, String reason) { @Override public void executionStarted(TestDescriptor testDescriptor) { - if (isTestRepresentative(testDescriptor)) { + if (isRepresentative(testDescriptor)) { testDescriptorResolver.getUniformPath(testDescriptor).ifPresent(teamscaleAgentNotifier::startTest); executionStartTime = System.currentTimeMillis(); } @@ -85,7 +85,7 @@ public void executionStarted(TestDescriptor testDescriptor) { @Override public void executionFinished(TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) { - if (isTestRepresentative(testDescriptor)) { + if (isRepresentative(testDescriptor)) { Optional uniformPath = testDescriptorResolver.getUniformPath(testDescriptor); if (!uniformPath.isPresent()) { return; diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt index 0c87d5fae..ed7f2275b 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt @@ -1,6 +1,7 @@ package com.teamscale.test_impacted.test_descriptor import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getUniqueIdSegment import org.junit.platform.engine.TestDescriptor import java.lang.reflect.Field import java.util.* @@ -11,44 +12,41 @@ import java.util.logging.Logger * [getPickleName]. The cluster id is the .feature file in which the tests are defined. */ class CucumberPickleDescriptorResolver : ITestDescriptorResolver { - override fun getUniformPath(testDescriptor: TestDescriptor): Optional { - val featurePath = testDescriptor.featurePath() + override fun getUniformPath(descriptor: TestDescriptor): Optional { + val featurePath = descriptor.featurePath() LOGGER.fine { "Resolved feature: $featurePath" } if (!featurePath.isPresent) { LOGGER.severe { - "Cannot resolve the feature classpath for $testDescriptor. This is probably a bug. Please report to CQSE" + "Cannot resolve the feature classpath for ${descriptor}. This is probably a bug. Please report to CQSE" } return Optional.empty() } - val pickleName = testDescriptor.getPickleName() + val pickleName = descriptor.getPickleName() LOGGER.fine { "Resolved pickle name: $pickleName" } if (!pickleName.isPresent) { LOGGER.severe { - "Cannot resolve the pickle name for $testDescriptor. This is probably a bug. Please report to CQSE" + "Cannot resolve the pickle name for ${descriptor}. This is probably a bug. Please report to CQSE" } return Optional.empty() } - val picklePath = "${featurePath.get()}/${pickleName.get()}" // Add an index to the end of the name in case multiple tests have the same name in the same feature file - val featureFileTestDescriptor = getFeatureFileTestDescriptor(testDescriptor) - val indexSuffix = if (!featureFileTestDescriptor.isPresent) { + val featureDescriptor = descriptor.getFeatureFileTestDescriptor() + val indexSuffix = if (!featureDescriptor.isPresent) { "" } else { - val testsWithTheSameName = flatListOfAllTestDescriptorChildrenWithPickleName( - featureFileTestDescriptor.get(), pickleName.get() - ) - val indexOfCurrentTest = testsWithTheSameName.indexOf(testDescriptor) + 1 - " #$indexOfCurrentTest" + val testsWithTheSameName = featureDescriptor.get().childrenWithPickleName(pickleName.get()) + " #${testsWithTheSameName.indexOf(descriptor) + 1}" } + val picklePath = "${featurePath.get()}/${pickleName.get()}" val uniformPath = (picklePath + indexSuffix).removeDuplicatedSlashes() LOGGER.fine { "Resolved uniform path: $uniformPath" } return Optional.of(uniformPath) } - override fun getClusterId(testDescriptor: TestDescriptor): Optional = - testDescriptor.featurePath().map { it.removeDuplicatedSlashes() } + override fun getClusterId(descriptor: TestDescriptor): Optional = + descriptor.featurePath().map { it.removeDuplicatedSlashes() } override val engineId = CUCUMBER_ENGINE_ID @@ -59,9 +57,7 @@ class CucumberPickleDescriptorResolver : ITestDescriptorResolver { */ private fun TestDescriptor.featurePath(): Optional { LOGGER.fine { "Unique ID of cucumber test descriptor: $uniqueId" } - val featureSegment = TestDescriptorUtils.getUniqueIdSegment( - this, FEATURE_SEGMENT_TYPE - ) + val featureSegment = getUniqueIdSegment(FEATURE_SEGMENT_TYPE) LOGGER.fine { "Resolved feature segment: $featureSegment" } return featureSegment.map { it.replace("classpath:".toRegex(), "") } } @@ -94,7 +90,8 @@ class CucumberPickleDescriptorResolver : ITestDescriptorResolver { try { pickleField = javaClass.getDeclaredField("pickle") } catch (e: NoSuchFieldException) { - // Pre cucumber 7.11.2, the field was called pickleEvent (see NodeDescriptor in this merge request: https://github.com/cucumber/cucumber-jvm/pull/2711/files) + // Pre cucumber 7.11.2, the field was called pickleEvent + // (see NodeDescriptor in this merge request: https://github.com/cucumber/cucumber-jvm/pull/2711/files) // ... } return runCatching { @@ -110,37 +107,34 @@ class CucumberPickleDescriptorResolver : ITestDescriptorResolver { getNameMethod.isAccessible = true val name = getNameMethod.invoke(pickle).toString() Optional.of(name) - .map { input -> input.escapeSlashes() } + .map { it.escapeSlashes() } } ?: Optional.empty() }.getOrNull() ?: Optional.empty() } - private fun getFeatureFileTestDescriptor(testDescriptor: TestDescriptor): Optional { - if (!isFeatureFileTestDescriptor(testDescriptor)) { - if (!testDescriptor.parent.isPresent) { + private fun TestDescriptor.getFeatureFileTestDescriptor(): Optional { + if (!isFeatureFileTestDescriptor()) { + if (!parent.isPresent) { return Optional.empty() } - return getFeatureFileTestDescriptor(testDescriptor.parent.get()) + return parent.get().getFeatureFileTestDescriptor() } - return Optional.of(testDescriptor) + return Optional.of(this) } - private fun isFeatureFileTestDescriptor(cucumberTestDescriptor: TestDescriptor) = - cucumberTestDescriptor.uniqueId.lastSegment.type == FEATURE_SEGMENT_TYPE + private fun TestDescriptor.isFeatureFileTestDescriptor() = + uniqueId.lastSegment.type == FEATURE_SEGMENT_TYPE - private fun flatListOfAllTestDescriptorChildrenWithPickleName( - testDescriptor: TestDescriptor, - pickleName: String - ): List { - if (testDescriptor.children.isEmpty()) { - val pickleId = testDescriptor.getPickleName() + private fun TestDescriptor.childrenWithPickleName(pickleName: String): List { + if (children.isEmpty()) { + val pickleId = getPickleName() if (pickleId.isPresent && pickleName == pickleId.get()) { - return listOf(testDescriptor) + return listOf(this) } return emptyList() } - return testDescriptor.children.flatMap { childDescriptor -> - flatListOfAllTestDescriptorChildrenWithPickleName(childDescriptor, pickleName) + return children.flatMap { childDescriptor -> + childDescriptor.childrenWithPickleName(pickleName) } } diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.kt index 8bda9f549..67e1b772b 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.kt @@ -6,10 +6,10 @@ import java.util.* /** Interface for implementation of mappings from [TestDescriptor]s to uniform paths. */ interface ITestDescriptorResolver { /** Returns the uniform path or [Optional.empty] if no uniform path could be determined. */ - fun getUniformPath(testDescriptor: TestDescriptor): Optional + fun getUniformPath(descriptor: TestDescriptor): Optional /** Returns the uniform path or [Optional.empty] if no cluster id could be determined. */ - fun getClusterId(testDescriptor: TestDescriptor): Optional + fun getClusterId(descriptor: TestDescriptor): Optional /** * Returns the [org.junit.platform.engine.TestEngine.getId] of the [org.junit.platform.engine.TestEngine] to use this [ITestDescriptorResolver] diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt index 705fc44e3..db5d15487 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt @@ -6,29 +6,28 @@ import java.util.* /** Test descriptor resolver for JUnit based [org.junit.platform.engine.TestEngine]s. */ abstract class JUnitClassBasedTestDescriptorResolverBase : ITestDescriptorResolver { - override fun getUniformPath(testDescriptor: TestDescriptor): Optional = - getClassName(testDescriptor).map { className -> + override fun getUniformPath(descriptor: TestDescriptor): Optional = + descriptor.getClassName().map { className -> val dotName = className.replace(".", "/") - "$dotName/${testDescriptor.legacyReportingName.trim { it <= ' ' }}" + "$dotName/${descriptor.legacyReportingName.trim { it <= ' ' }}" } - override fun getClusterId(testDescriptor: TestDescriptor): Optional { - val classSegmentName = getClassName(testDescriptor) + override fun getClusterId(descriptor: TestDescriptor): Optional { + val classSegmentName = descriptor.getClassName() if (!classSegmentName.isPresent) { LOGGER.severe { - "Falling back to unique ID as cluster id because class segment name could not be " + - "determined for test descriptor: " + testDescriptor + "Falling back to unique ID as cluster id because class segment name could not be determined for test descriptor: $descriptor" } // Default to uniform path. - return Optional.of(testDescriptor.uniqueId.toString()) + return Optional.of(descriptor.uniqueId.toString()) } return classSegmentName } /** Returns the test class containing the test. */ - protected abstract fun getClassName(testDescriptor: TestDescriptor): Optional + protected abstract fun TestDescriptor.getClassName(): Optional companion object { private val LOGGER = getLogger(JUnitClassBasedTestDescriptorResolverBase::class.java) diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.kt index 34c35f51a..e912e3eb4 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitJupiterTestDescriptorResolver.kt @@ -1,13 +1,13 @@ package com.teamscale.test_impacted.test_descriptor +import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getUniqueIdSegment import org.junit.platform.engine.TestDescriptor import java.util.* /** Test default test descriptor resolver for the JUnit jupiter [TestEngine]. */ class JUnitJupiterTestDescriptorResolver : JUnitClassBasedTestDescriptorResolverBase() { - override fun getClassName(testDescriptor: TestDescriptor): Optional { - return TestDescriptorUtils.getUniqueIdSegment(testDescriptor, CLASS_SEGMENT_TYPE) - } + override fun TestDescriptor.getClassName() = + getUniqueIdSegment(CLASS_SEGMENT_TYPE) override val engineId: String get() = "junit-jupiter" diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt index e69eef447..c182460ce 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt @@ -10,15 +10,15 @@ import java.util.* * https://junit.org/junit5/docs/current/user-guide/#junit-platform-suite-engine) */ class JUnitPlatformSuiteDescriptorResolver : ITestDescriptorResolver { - override fun getUniformPath(testDescriptor: TestDescriptor) = - extractUniformPathOrClusterId( - testDescriptor, "uniform path" - ) { it.getUniformPath(testDescriptor) } + override fun getUniformPath(descriptor: TestDescriptor) = + descriptor.extractUniformPathOrClusterId("uniform path") { + it.getUniformPath(descriptor) + } - override fun getClusterId(testDescriptor: TestDescriptor) = - extractUniformPathOrClusterId( - testDescriptor, "cluster id" - ) { it.getClusterId(testDescriptor) } + override fun getClusterId(descriptor: TestDescriptor) = + descriptor.extractUniformPathOrClusterId("cluster id") { + it.getClusterId(descriptor) + } override val engineId: String get() = "junit-platform-suite" @@ -29,47 +29,40 @@ class JUnitPlatformSuiteDescriptorResolver : ITestDescriptorResolver { /** Type of the unique id segment of a test descriptor representing a test suite */ private const val SUITE_SEGMENT_TYPE: String = "suite" - private fun extractUniformPathOrClusterId( - testDescriptor: TestDescriptor, + private fun TestDescriptor.extractUniformPathOrClusterId( nameOfValueToExtractForLogs: String, uniformPathOrClusterIdExtractor: (ITestDescriptorResolver) -> Optional ): Optional { - val segments = testDescriptor.uniqueId.segments + val segments = uniqueId.segments if (verifySegments(segments)) { LOGGER.severe { - "Assuming structure [engine:junit-platform-suite]/[suite:mySuite]/[engine:anotherEngine] for junit-platform-suite tests. Using ${testDescriptor.uniqueId} as $nameOfValueToExtractForLogs as fallback." + "Assuming structure [engine:junit-platform-suite]/[suite:mySuite]/[engine:anotherEngine] for junit-platform-suite tests. Using $uniqueId as $nameOfValueToExtractForLogs as fallback." } - return Optional.of(testDescriptor.uniqueId.toString()) + return Optional.of(uniqueId.toString()) } val suite = segments[1].value.replace('.', '/') val secondaryEngineSegments = segments.subList(2, segments.size) - val secondaryTestDescriptorResolver = TestDescriptorResolverRegistry.getTestDescriptorResolver( - secondaryEngineSegments[0].value + val descriptorResolver = TestDescriptorResolverRegistry.getTestDescriptorResolver( + secondaryEngineSegments.first().value ) - if (secondaryTestDescriptorResolver == null) { + if (descriptorResolver == null) { LOGGER.severe { - "Cannot find a secondary engine nested under the junit-platform-suite engine " + - "(assuming structure [engine:junit-platform-suite]/[suite:mySuite]/[engine:anotherEngine]). " + - "Using " + testDescriptor.uniqueId - .toString() + " as " + nameOfValueToExtractForLogs + " as fallback." + "Cannot find a secondary engine nested under the junit-platform-suite engine (assuming structure [engine:junit-platform-suite]/[suite:mySuite]/[engine:anotherEngine]). Using $uniqueId as $nameOfValueToExtractForLogs as fallback." } - return Optional.of(testDescriptor.uniqueId.toString()) + return Optional.of(uniqueId.toString()) } - val secondaryClusterIdOrUniformPath = uniformPathOrClusterIdExtractor(secondaryTestDescriptorResolver) - if (!secondaryClusterIdOrUniformPath.isPresent) { + val idOrUniformPath = uniformPathOrClusterIdExtractor(descriptorResolver) + if (!idOrUniformPath.isPresent) { LOGGER.severe { - "Secondary test descriptor resolver for engine " + - secondaryEngineSegments[0] - .value + " was not able to resolve the " + nameOfValueToExtractForLogs + ". " + - "Using " + testDescriptor.uniqueId.toString() + " as fallback." + "Secondary test descriptor resolver for engine ${secondaryEngineSegments.first().value} was not able to resolve the $nameOfValueToExtractForLogs. Using $uniqueId as fallback." } - return Optional.of(testDescriptor.uniqueId.toString()) + return Optional.of(uniqueId.toString()) } - return Optional.of(suite + "/" + secondaryClusterIdOrUniformPath.get()) + return Optional.of("$suite/${idOrUniformPath.get()}") } private fun verifySegments(segments: List) = diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt index de5f5aa1b..a27f3fab2 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitVintageTestDescriptorResolver.kt @@ -1,11 +1,12 @@ package com.teamscale.test_impacted.test_descriptor +import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getUniqueIdSegment import org.junit.platform.engine.TestDescriptor /** Test default test descriptor resolver for the JUnit vintage [org.junit.platform.engine.TestEngine]. */ class JUnitVintageTestDescriptorResolver : JUnitClassBasedTestDescriptorResolverBase() { - override fun getClassName(testDescriptor: TestDescriptor) = - TestDescriptorUtils.getUniqueIdSegment(testDescriptor, RUNNER_SEGMENT_TYPE) + override fun TestDescriptor.getClassName() = + getUniqueIdSegment(RUNNER_SEGMENT_TYPE) override val engineId: String get() = "junit-vintage" diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt index d1873a360..09e642d19 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt @@ -15,21 +15,21 @@ object TestDescriptorResolverRegistry { init { // Register default test descriptor resolvers - registerTestDescriptorResolver(JUnitJupiterTestDescriptorResolver()) - registerTestDescriptorResolver(JUnitVintageTestDescriptorResolver()) - registerTestDescriptorResolver(JUnitPlatformSuiteDescriptorResolver()) - registerTestDescriptorResolver(CucumberPickleDescriptorResolver()) + JUnitJupiterTestDescriptorResolver().register() + JUnitVintageTestDescriptorResolver().register() + JUnitPlatformSuiteDescriptorResolver().register() + CucumberPickleDescriptorResolver().register() // Override existing or register new test descriptor resolvers - for (testDescriptorResolver in ServiceLoader - .load(ITestDescriptorResolver::class.java, ClassLoaderUtils.getDefaultClassLoader())) { - registerTestDescriptorResolver(testDescriptorResolver) - } + ServiceLoader + .load(ITestDescriptorResolver::class.java, ClassLoaderUtils.getDefaultClassLoader()) + .forEach { testDescriptorResolver -> + testDescriptorResolver.register() + } } - private fun registerTestDescriptorResolver(testDescriptorResolver: ITestDescriptorResolver) { - TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID[testDescriptorResolver.engineId] = - testDescriptorResolver + private fun ITestDescriptorResolver.register() { + TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID[engineId] = this } /** Returns the test descriptor resolver or null if none exists for the test engine. */ diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt index f8eec77ce..a9a02250c 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt @@ -19,15 +19,15 @@ object TestDescriptorUtils { @JvmStatic fun getTestDescriptorAsString(testDescriptor: TestDescriptor): String { val writer = IndentingWriter() - printTestDescriptor(writer, testDescriptor) + writer.printTestDescriptor(testDescriptor) return writer.toString() } - private fun printTestDescriptor(writer: IndentingWriter, testDescriptor: TestDescriptor) { - writer.writeLine(testDescriptor.uniqueId.toString()) - writer.indent { + private fun IndentingWriter.printTestDescriptor(testDescriptor: TestDescriptor) { + writeLine(testDescriptor.uniqueId.toString()) + indent { testDescriptor.children.forEach { child -> - printTestDescriptor(writer, child) + printTestDescriptor(child) } } } @@ -38,11 +38,9 @@ object TestDescriptorUtils { * test cases. */ @JvmStatic - fun isTestRepresentative(testDescriptor: TestDescriptor): Boolean { - val isTestTemplateOrTestFactory = isTestTemplateOrTestFactory(testDescriptor) - val isNonParameterizedTest = testDescriptor.isTest && !isTestTemplateOrTestFactory( - testDescriptor.parent.get() - ) + fun TestDescriptor.isRepresentative(): Boolean { + val isTestTemplateOrTestFactory = isTestTemplateOrTestFactory() + val isNonParameterizedTest = isTest && !parent.get().isTestTemplateOrTestFactory() return isNonParameterizedTest || isTestTemplateOrTestFactory } @@ -55,11 +53,8 @@ object TestDescriptorUtils { * * `[engine:junit-jupiter]/[class:com.example.project.JUnit5Test]/[test-template:withValueSource(java.lang.String)]` */ - private fun isTestTemplateOrTestFactory(testDescriptor: TestDescriptor?): Boolean { - if (testDescriptor == null) { - return false - } - val segments = testDescriptor.uniqueId.segments + private fun TestDescriptor.isTestTemplateOrTestFactory(): Boolean { + val segments = uniqueId.segments if (segments.isEmpty()) { return false @@ -71,36 +66,32 @@ object TestDescriptorUtils { } /** Creates a stream of the test representatives contained by the [TestDescriptor]. */ - private fun streamTestRepresentatives(testDescriptor: TestDescriptor): Stream { - if (isTestRepresentative(testDescriptor)) { - return Stream.of(testDescriptor) + private fun TestDescriptor.streamTestRepresentatives(): Stream { + if (isRepresentative()) { + return Stream.of(this) } - return testDescriptor.children.stream().flatMap { - streamTestRepresentatives(it) + return children.stream().flatMap { + it.streamTestRepresentatives() } } /** - * Returns the [Segment.getValue] matching the type or [Optional.empty] if no matching segment can + * Returns the [org.junit.platform.engine.UniqueId.Segment.getValue] matching the type or [Optional.empty] if no matching segment can * be found. */ - fun getUniqueIdSegment(testDescriptor: TestDescriptor, type: String): Optional = - testDescriptor.uniqueId.segments.stream() + fun TestDescriptor.getUniqueIdSegment(type: String): Optional = + uniqueId.segments.stream() .filter { it.type == type } .findFirst().map { it.value } - /** Returns [TestDetails.sourcePath] for a [TestDescriptor]. */ - private fun getSource(testDescriptor: TestDescriptor): String? { - val source = testDescriptor.source - if (source.isPresent && source.get() is MethodSource) { - val ms = source.get() as MethodSource - return ms.className.replace('.', '/') - } - if (source.isPresent && source.get() is ClassSource) { - val classSource = source.get() as ClassSource - return classSource.className.replace('.', '/') + /** Returns [com.teamscale.client.TestDetails.sourcePath] for a [TestDescriptor]. */ + private fun TestDescriptor.source(): String? { + val source = source.orElse(null) ?: return null + return when (source) { + is MethodSource -> source.className.replace('.', '/') + is ClassSource -> source.className.replace('.', '/') + else -> null } - return null } /** Returns the [AvailableTests] contained within the root [TestDescriptor]. */ @@ -111,8 +102,8 @@ object TestDescriptorUtils { ): AvailableTests { val availableTests = AvailableTests() - streamTestRepresentatives(rootTestDescriptor) - .forEach { testDescriptor: TestDescriptor -> + rootTestDescriptor.streamTestRepresentatives() + .forEach { testDescriptor -> val engineId = testDescriptor.uniqueId.engineId if (!engineId.isPresent) { LOGGER.severe { "Unable to determine engine ID for $testDescriptor!" } @@ -122,7 +113,6 @@ object TestDescriptorUtils { val testDescriptorResolver = TestDescriptorResolverRegistry.getTestDescriptorResolver(engineId.get()) val clusterId = testDescriptorResolver!!.getClusterId(testDescriptor) val uniformPath = testDescriptorResolver.getUniformPath(testDescriptor) - val source = getSource(testDescriptor) if (!uniformPath.isPresent) { LOGGER.severe { "Unable to determine uniform path for test descriptor: $testDescriptor" } @@ -135,8 +125,11 @@ object TestDescriptorUtils { } val testDetails = ClusteredTestDetails( - uniformPath.get(), source, null, - clusterId.get(), partition + uniformPath.get(), + testDescriptor.source(), + null, + clusterId.get(), + partition ) availableTests.add(testDescriptor.uniqueId, testDetails) } diff --git a/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.kt b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.kt index 0cc022695..ef31bd57a 100644 --- a/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.kt +++ b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.kt @@ -10,7 +10,6 @@ import org.junit.platform.launcher.TestExecutionListener import org.junit.platform.launcher.TestIdentifier import org.junit.platform.launcher.TestPlan import java.util.* -import java.util.function.Function /** * [TestExecutionListener] that uses the [TiaAgent] to record test-wise coverage. @@ -20,21 +19,20 @@ class JUnit5TestwiseCoverageExecutionListener : TestExecutionListener { override fun executionStarted(testIdentifier: TestIdentifier) { if (!testIdentifier.isTest) return - val uniformPath = getUniformPath(testIdentifier) + val uniformPath = testIdentifier.getUniformPath() bridge.testStarted(uniformPath) } - private fun getUniformPath(testIdentifier: TestIdentifier) = - testIdentifier.source.flatMap { source -> - parseTestSource(source) - }.orElse(testIdentifier.displayName) + private fun TestIdentifier.getUniformPath() = + source.flatMap { source -> + source.parse() + }.orElse(displayName) - private fun parseTestSource(source: TestSource) = - when (source) { - is ClassSource -> Optional.of(source.className.replace('.', '/')) + private fun TestSource.parse() = + when (this) { + is ClassSource -> Optional.of(className.replace('.', '/')) is MethodSource -> Optional.of( - source.className.replace('.', '/') + "/" + - source.methodName + "(" + source.methodParameterTypes + ")" + "${className.replace('.', '/')}/$methodName($methodParameterTypes)" ) else -> Optional.empty() } @@ -43,7 +41,7 @@ class JUnit5TestwiseCoverageExecutionListener : TestExecutionListener { if (!testIdentifier.isTest) { return } - val uniformPath = getUniformPath(testIdentifier) + val uniformPath = testIdentifier.getUniformPath() val result = when (testExecutionResult.status) { TestExecutionResult.Status.SUCCESSFUL -> ETestExecutionResult.PASSED TestExecutionResult.Status.ABORTED -> ETestExecutionResult.ERROR @@ -56,7 +54,7 @@ class JUnit5TestwiseCoverageExecutionListener : TestExecutionListener { override fun executionSkipped(testIdentifier: TestIdentifier, reason: String) { if (testIdentifier.isContainer) return - val uniformPath = getUniformPath(testIdentifier) + val uniformPath = testIdentifier.getUniformPath() bridge.testSkipped(uniformPath, reason) } From c8ec448425dbaf2ebcdf966c0164dc099024b012 Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 4 Dec 2024 00:41:11 +0100 Subject: [PATCH 06/21] TS-38628 Option migration --- .../engine/options/ServerOptions.java | 85 ------ .../engine/options/TestEngineOptionUtils.java | 18 -- .../engine/options/TestEngineOptions.java | 269 ------------------ .../engine/options/ServerOptions.kt | 68 +++++ .../engine/options/TestEngineOptions.kt | 230 +++++++++++++++ 5 files changed, 298 insertions(+), 372 deletions(-) delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/ServerOptions.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptions.java create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/ServerOptions.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/ServerOptions.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/ServerOptions.java deleted file mode 100644 index 094703b37..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/ServerOptions.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.teamscale.test_impacted.engine.options; - -/** Represents options for the connection to the Teamscale server. */ -public class ServerOptions { - - /** The Teamscale url. */ - private String url; - - /** The Teamscale project id for which artifacts should be uploaded. */ - private String project; - - /** The user name of the Teamscale user. */ - private String userName; - - /** The access token of the user. */ - private String userAccessToken; - - /** @see #url */ - public String getUrl() { - return url; - } - - /** @see #project */ - public String getProject() { - return project; - } - - /** @see #userName */ - public String getUserName() { - return userName; - } - - /** @see #userAccessToken */ - public String getUserAccessToken() { - return userAccessToken; - } - - /** Returns the builder for {@link ServerOptions}. */ - public static Builder builder() { - return new Builder(); - } - - /** The builder for {@link ServerOptions}. */ - public static class Builder { - - private final ServerOptions serverOptions = new ServerOptions(); - - private Builder() { - // Just needed to make the constructor private - } - - /** @see #url */ - public Builder url(String url) { - serverOptions.url = url; - return this; - } - - /** @see #project */ - public Builder project(String project) { - serverOptions.project = project; - return this; - } - - /** @see #userName */ - public Builder userName(String userName) { - serverOptions.userName = userName; - return this; - } - - /** @see #userAccessToken */ - public Builder userAccessToken(String userAccessToken) { - serverOptions.userAccessToken = userAccessToken; - return this; - } - - /** Checks field conditions and returns the built {@link ServerOptions}. */ - public ServerOptions build() { - TestEngineOptionUtils.assertNotBlank(serverOptions.url, "The server URL must be set."); - TestEngineOptionUtils.assertNotBlank(serverOptions.project, "The Teamscale project must be set."); - TestEngineOptionUtils.assertNotBlank(serverOptions.userName, "The user name must be set."); - TestEngineOptionUtils.assertNotBlank(serverOptions.userAccessToken, "The user access token must be set."); - return serverOptions; - } - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.java index 2cd63145d..7411b3516 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.java @@ -47,24 +47,6 @@ public static TestEngineOptions getEngineOptions(ConfigurationParameters configu .build(); } - /** - * Throws an {@link AssertionError} if the given value is blank. - */ - static void assertNotBlank(String value, String message) { - if (StringUtils.isBlank(value)) { - throw new AssertionError(message); - } - } - - /** - * Throws an {@link AssertionError} if the given value is null. - */ - static void assertNotNull(Object value, String message) { - if (value == null) { - throw new AssertionError(message); - } - } - private static class PrefixingPropertyReader { private final ConfigurationParameters configurationParameters; diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptions.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptions.java deleted file mode 100644 index c1bc5a052..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptions.java +++ /dev/null @@ -1,269 +0,0 @@ -package com.teamscale.test_impacted.engine.options; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.junit.platform.engine.TestEngine; - -import com.teamscale.client.CommitDescriptor; -import com.teamscale.client.TeamscaleClient; -import com.teamscale.test_impacted.engine.ImpactedTestEngine; -import com.teamscale.test_impacted.engine.ImpactedTestEngineConfiguration; -import com.teamscale.test_impacted.engine.TestDataWriter; -import com.teamscale.test_impacted.engine.TestEngineRegistry; -import com.teamscale.test_impacted.engine.executor.ITestSorter; -import com.teamscale.test_impacted.engine.executor.ImpactedTestsProvider; -import com.teamscale.test_impacted.engine.executor.ImpactedTestsSorter; -import com.teamscale.test_impacted.engine.executor.NOPTestSorter; -import com.teamscale.test_impacted.engine.executor.TeamscaleAgentNotifier; -import com.teamscale.tia.client.ITestwiseCoverageAgentApi; - -import okhttp3.HttpUrl; - -/** Represents options for the {@link ImpactedTestEngine}. */ -public class TestEngineOptions { - - /** The server options. May not be null. */ - private ServerOptions serverOptions; - - /** The partition to upload test details to and get impacted tests from. If null all partitions are used. */ - private String partition; - - /** Executes all tests, not only impacted ones if set. Defaults to false. */ - private boolean runAllTests = false; - - /** Executes only impacted tests, not all ones if set. Defaults to true. */ - private boolean runImpacted = true; - - /** Includes added tests in the list of tests to execute. Defaults to true */ - private boolean includeAddedTests = true; - - /** Includes failed and skipped tests in the list of tests to execute. Defaults to true */ - private boolean includeFailedAndSkipped = true; - - /** - * The baseline. Only code changes after the baseline are considered for determining impacted tests. May be null to - * indicate no baseline. - */ - private String baseline; - - /** - * Can be used instead of {@link #baseline} by using a revision (e.g. git SHA1) instead of a branch and timestamp. - */ - private String baselineRevision; - - /** The end commit used for TIA and for uploading the coverage. May not be null. */ - private CommitDescriptor endCommit; - - /** - * Can be used instead of {@link #endCommit} by using a revision (e.g. git SHA1) instead of a branch and timestamp. - */ - private String endRevision; - - /** - * The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. - * Null or empty will lead to a lookup in all repositories in the Teamscale project. - */ - private String repository; - - /** The URLs (including port) at which the agents listen. May be empty but not null. */ - private List testwiseCoverageAgentApis = Collections.emptyList(); - - /** The test engine ids of all {@link TestEngine}s to use. If empty all available {@link TestEngine}s are used. */ - private Set includedTestEngineIds = Collections.emptySet(); - - /** The test engine ids of all {@link TestEngine}s to exclude. */ - private Set excludedTestEngineIds = Collections.emptySet(); - - /** The directory used to store test-wise coverage reports. Must be a writeable directory. */ - private File reportDirectory; - - /** @see #runAllTests */ - private boolean isRunAllTests() { - return runAllTests; - } - - /** @see #includeAddedTests */ - private boolean isIncludeAddedTests() { - return includeAddedTests; - } - - /** @see #includeFailedAndSkipped */ - private boolean isIncludeFailedAndSkipped() { - return includeFailedAndSkipped; - } - - /** @see #partition */ - public String getPartition() { - return partition; - } - - public ImpactedTestEngineConfiguration createTestEngineConfiguration() { - ITestSorter testSorter = createTestSorter(); - TeamscaleAgentNotifier teamscaleAgentNotifier = createTeamscaleAgentNotifier(); - TestEngineRegistry testEngineRegistry = new TestEngineRegistry(includedTestEngineIds, excludedTestEngineIds); - TestDataWriter testDataWriter = new TestDataWriter(reportDirectory); - - return new ImpactedTestEngineConfiguration(testDataWriter, testEngineRegistry, testSorter, teamscaleAgentNotifier); - } - - private ITestSorter createTestSorter() { - if (!runImpacted) { - return new NOPTestSorter(); - } - - ImpactedTestsProvider testsProvider = createImpactedTestsProvider(); - return new ImpactedTestsSorter(testsProvider); - } - - private ImpactedTestsProvider createImpactedTestsProvider() { - TeamscaleClient client = new TeamscaleClient(serverOptions.getUrl(), serverOptions.getUserName(), - serverOptions.getUserAccessToken(), serverOptions.getProject(), - new File(reportDirectory, "server-request.txt")); - return new ImpactedTestsProvider(client, baseline, baselineRevision, endCommit, endRevision, repository, partition, - isRunAllTests(), isIncludeAddedTests(), isIncludeFailedAndSkipped()); - } - - private TeamscaleAgentNotifier createTeamscaleAgentNotifier() { - return new TeamscaleAgentNotifier(testwiseCoverageAgentApis, - runImpacted && !runAllTests); - } - - /** Returns the builder for {@link TestEngineOptions}. */ - public static Builder builder() { - return new Builder(); - } - - /** The builder for {@link TestEngineOptions}. */ - public static class Builder { - - private final TestEngineOptions testEngineOptions = new TestEngineOptions(); - - private Builder() { - // Only needed to make constructor private - } - - /** @see #serverOptions */ - public Builder serverOptions(ServerOptions serverOptions) { - testEngineOptions.serverOptions = serverOptions; - return this; - } - - /** @see #partition */ - public Builder partition(String partition) { - testEngineOptions.partition = partition; - return this; - } - - /** @see #runImpacted */ - public Builder runImpacted(boolean runImpacted) { - testEngineOptions.runImpacted = runImpacted; - return this; - } - - /** @see #runAllTests */ - public Builder runAllTests(boolean runAllTests) { - testEngineOptions.runAllTests = runAllTests; - return this; - } - - /** @see #includeAddedTests */ - public Builder includeAddedTests(boolean includeAddedTests) { - testEngineOptions.includeAddedTests = includeAddedTests; - return this; - } - - /** @see #includeFailedAndSkipped */ - public Builder includeFailedAndSkipped(boolean includeFailedAndSkipped) { - testEngineOptions.includeFailedAndSkipped = includeFailedAndSkipped; - return this; - } - - /** @see #endCommit */ - public Builder endCommit(CommitDescriptor endCommit) { - testEngineOptions.endCommit = endCommit; - return this; - } - - /** @see #endRevision */ - public Builder endRevision(String endRevision) { - testEngineOptions.endRevision = endRevision; - return this; - } - - /** @see #repository */ - public Builder repository(String repository) { - testEngineOptions.repository = repository; - return this; - } - - /** @see #baseline */ - public Builder baseline(String baseline) { - testEngineOptions.baseline = baseline; - return this; - } - - /** @see #baselineRevision */ - public Builder baselineRevision(String baselineRevision) { - testEngineOptions.baselineRevision = baselineRevision; - return this; - } - - /** @see #testwiseCoverageAgentApis */ - public Builder agentUrls(List agentUrls) { - testEngineOptions.testwiseCoverageAgentApis = agentUrls.stream() - .map(HttpUrl::parse) - .map(ITestwiseCoverageAgentApi::createService) - .collect(Collectors.toList()); - return this; - } - - /** @see #includedTestEngineIds */ - public Builder includedTestEngineIds(List testEngineIds) { - testEngineOptions.includedTestEngineIds = new HashSet<>(testEngineIds); - return this; - } - - /** @see #excludedTestEngineIds */ - public Builder excludedTestEngineIds(List testEngineIds) { - testEngineOptions.excludedTestEngineIds = new HashSet<>(testEngineIds); - return this; - } - - /** @see #reportDirectory */ - public Builder reportDirectory(String reportDirectory) { - if (reportDirectory != null) { - testEngineOptions.reportDirectory = new File(reportDirectory); - } - return this; - } - - /** Checks field conditions and returns the built {@link TestEngineOptions}. */ - public TestEngineOptions build() { - if (testEngineOptions.endCommit == null && testEngineOptions.endRevision == null) { - throw new AssertionError("End commit must be set via endCommit or endRevision."); - } - if (testEngineOptions.runImpacted) { - TestEngineOptionUtils.assertNotNull(testEngineOptions.serverOptions, "Server options must be set."); - } - TestEngineOptionUtils.assertNotNull(testEngineOptions.testwiseCoverageAgentApis, - "Agent urls may be empty but not null."); - TestEngineOptionUtils.assertNotNull(testEngineOptions.reportDirectory, "Report directory must be set."); - if (!testEngineOptions.reportDirectory.isDirectory() || !testEngineOptions.reportDirectory.canWrite()) { - try { - Files.createDirectories(testEngineOptions.reportDirectory.toPath()); - } catch (IOException e) { - throw new AssertionError( - "Report directory could not be created: " + testEngineOptions.reportDirectory, e); - } - } - return testEngineOptions; - } - } -} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/ServerOptions.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/ServerOptions.kt new file mode 100644 index 000000000..ac4e153ee --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/ServerOptions.kt @@ -0,0 +1,68 @@ +package com.teamscale.test_impacted.engine.options + +/** Represents options for the connection to the Teamscale server. */ +class ServerOptions { + /** The Teamscale url. + * @see [Builder.url] */ + var url: String? = null + private set + + /** The Teamscale project id for which artifacts should be uploaded. + * @see [Builder.project] */ + var project: String? = null + private set + + /** The username of the Teamscale user. + * @see [Builder.userName] */ + var userName: String? = null + private set + + /** The access token of the user. + * @see [Builder.userAccessToken] */ + var userAccessToken: String? = null + private set + + /** The builder for [ServerOptions]. */ + class Builder { + private val serverOptions = ServerOptions() + + /** @see [ServerOptions.url] */ + fun url(url: String): Builder { + serverOptions.url = url + return this + } + + /** @see [ServerOptions.project] */ + fun project(project: String): Builder { + serverOptions.project = project + return this + } + + /** @see [ServerOptions.userName] */ + fun userName(userName: String): Builder { + serverOptions.userName = userName + return this + } + + /** @see [ServerOptions.userAccessToken] */ + fun userAccessToken(userAccessToken: String): Builder { + serverOptions.userAccessToken = userAccessToken + return this + } + + /** Checks field conditions and returns the built [ServerOptions]. */ + fun build(): ServerOptions { + check(!serverOptions.url.isNullOrBlank()) { "The server URL must be set." } + check(!serverOptions.project.isNullOrBlank()) { "The Teamscale project must be set." } + check(!serverOptions.userName.isNullOrBlank()) { "The user name must be set." } + check(!serverOptions.userAccessToken.isNullOrBlank()) { "The user access token must be set." } + return serverOptions + } + } + + companion object { + /** Returns the builder for [ServerOptions]. */ + @JvmStatic + fun builder() = Builder() + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt new file mode 100644 index 000000000..1f3ae1e4c --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt @@ -0,0 +1,230 @@ +package com.teamscale.test_impacted.engine.options + +import com.teamscale.client.CommitDescriptor +import com.teamscale.client.TeamscaleClient +import com.teamscale.test_impacted.engine.ImpactedTestEngineConfiguration +import com.teamscale.test_impacted.engine.TestDataWriter +import com.teamscale.test_impacted.engine.TestEngineRegistry +import com.teamscale.test_impacted.engine.executor.* +import com.teamscale.tia.client.ITestwiseCoverageAgentApi +import okhttp3.HttpUrl +import java.io.File +import java.io.IOException +import java.nio.file.Files + +/** Represents options for the [com.teamscale.test_impacted.engine.ImpactedTestEngine]. */ +class TestEngineOptions { + /** The server options. May not be null. */ + private var serverOptions: ServerOptions? = null + + /** @see .partition + */ + /** The partition to upload test details to and get impacted tests from. If null all partitions are used. */ + var partition: String? = null + private set + + /** @see .runAllTests + */ + /** Executes all tests, not only impacted ones if set. Defaults to false. */ + private var runAllTests = false + + /** Executes only impacted tests, not all ones if set. Defaults to true. */ + private var runImpacted = true + + /** @see .includeAddedTests + */ + /** Includes added tests in the list of tests to execute. Defaults to true */ + private var includeAddedTests = true + + /** @see .includeFailedAndSkipped + */ + /** Includes failed and skipped tests in the list of tests to execute. Defaults to true */ + private var includeFailedAndSkipped = true + + /** + * The baseline. Only code changes after the baseline are considered for determining impacted tests. May be null to + * indicate no baseline. + */ + private var baseline: String? = null + + /** + * Can be used instead of [.baseline] by using a revision (e.g. git SHA1) instead of a branch and timestamp. + */ + private var baselineRevision: String? = null + + /** The end commit used for TIA and for uploading the coverage. May not be null. */ + private var endCommit: CommitDescriptor? = null + + /** + * Can be used instead of [.endCommit] by using a revision (e.g. git SHA1) instead of a branch and timestamp. + */ + private var endRevision: String? = null + + /** + * The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. + * Null or empty will lead to a lookup in all repositories in the Teamscale project. + */ + private var repository: String? = null + + /** The URLs (including port) at which the agents listen to. Maybe empty but not null. */ + private var testwiseCoverageAgentApis = emptyList() + + /** The test engine ids of all [org.junit.platform.engine.TestEngine]s to use. + * If empty all available [org.junit.platform.engine.TestEngine]s are used. */ + private var includedTestEngineIds = emptySet() + + /** The test engine ids of all [org.junit.platform.engine.TestEngine]s to exclude. */ + private var excludedTestEngineIds = emptySet() + + /** The directory used to store test-wise coverage reports. Must be a writeable directory. */ + private var reportDirectory: File? = null + + fun createTestEngineConfiguration(): ImpactedTestEngineConfiguration { + val testSorter = createTestSorter() + val teamscaleAgentNotifier = createTeamscaleAgentNotifier() + val testEngineRegistry = TestEngineRegistry(includedTestEngineIds, excludedTestEngineIds) + val testDataWriter = TestDataWriter(reportDirectory) + + return ImpactedTestEngineConfiguration(testDataWriter, testEngineRegistry, testSorter, teamscaleAgentNotifier) + } + + private fun createTestSorter(): ITestSorter { + if (!runImpacted) { + return NOPTestSorter() + } + + val testsProvider = createImpactedTestsProvider() + return ImpactedTestsSorter(testsProvider) + } + + private fun createImpactedTestsProvider(): ImpactedTestsProvider { + val client = TeamscaleClient( + serverOptions?.url, serverOptions?.userName!!, + serverOptions?.userAccessToken!!, serverOptions?.project!!, + File(reportDirectory, "server-request.txt") + ) + return ImpactedTestsProvider( + client, baseline, baselineRevision, endCommit, endRevision, repository, partition, + runAllTests, includeAddedTests, includeFailedAndSkipped + ) + } + + private fun createTeamscaleAgentNotifier() = + TeamscaleAgentNotifier(testwiseCoverageAgentApis, runImpacted && !runAllTests) + + /** The builder for [TestEngineOptions]. */ + class Builder { + private val testEngineOptions = TestEngineOptions() + + fun serverOptions(serverOptions: ServerOptions): Builder { + testEngineOptions.serverOptions = serverOptions + return this + } + + fun partition(partition: String): Builder { + testEngineOptions.partition = partition + return this + } + + fun runImpacted(runImpacted: Boolean): Builder { + testEngineOptions.runImpacted = runImpacted + return this + } + + fun runAllTests(runAllTests: Boolean): Builder { + testEngineOptions.runAllTests = runAllTests + return this + } + + fun includeAddedTests(includeAddedTests: Boolean): Builder { + testEngineOptions.includeAddedTests = includeAddedTests + return this + } + + fun includeFailedAndSkipped(includeFailedAndSkipped: Boolean): Builder { + testEngineOptions.includeFailedAndSkipped = includeFailedAndSkipped + return this + } + + fun endCommit(endCommit: CommitDescriptor): Builder { + testEngineOptions.endCommit = endCommit + return this + } + + fun endRevision(endRevision: String): Builder { + testEngineOptions.endRevision = endRevision + return this + } + + fun repository(repository: String): Builder { + testEngineOptions.repository = repository + return this + } + + fun baseline(baseline: String): Builder { + testEngineOptions.baseline = baseline + return this + } + + fun baselineRevision(baselineRevision: String): Builder { + testEngineOptions.baselineRevision = baselineRevision + return this + } + + fun agentUrls(agentUrls: List): Builder { + testEngineOptions.testwiseCoverageAgentApis = agentUrls + .map { HttpUrl.parse(it) } + .mapNotNull { + if (it != null) { + ITestwiseCoverageAgentApi.createService(it) + } else null + } + return this + } + + fun includedTestEngineIds(testEngineIds: List): Builder { + testEngineOptions.includedTestEngineIds = HashSet(testEngineIds) + return this + } + + fun excludedTestEngineIds(testEngineIds: List): Builder { + testEngineOptions.excludedTestEngineIds = HashSet(testEngineIds) + return this + } + + fun reportDirectory(reportDirectory: String): Builder { + testEngineOptions.reportDirectory = File(reportDirectory) + return this + } + + /** Checks field conditions and returns the built [TestEngineOptions]. */ + fun build(): TestEngineOptions { + if (testEngineOptions.endCommit == null && testEngineOptions.endRevision == null) { + throw AssertionError("End commit must be set via endCommit or endRevision.") + } + if (testEngineOptions.runImpacted) { + checkNotNull(testEngineOptions.serverOptions) { "Server options must be set." } + } + checkNotNull(testEngineOptions.testwiseCoverageAgentApis) { "Agent urls may be empty but not null." } + checkNotNull(testEngineOptions.reportDirectory) { "Report directory must be set." } + if (!testEngineOptions.reportDirectory!!.isDirectory || !testEngineOptions.reportDirectory!!.canWrite()) { + try { + Files.createDirectories(testEngineOptions.reportDirectory!!.toPath()) + } catch (e: IOException) { + throw AssertionError( + "Report directory could not be created: ${testEngineOptions.reportDirectory}", e + ) + } + } + return testEngineOptions + } + } + + companion object { + /** Returns the builder for [TestEngineOptions]. */ + @JvmStatic + fun builder(): Builder { + return Builder() + } + } +} From 0f6cbf003b678242f72e777812fec65db752c90b Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 4 Dec 2024 00:56:58 +0100 Subject: [PATCH 07/21] TS-38628 AvailableTests migration --- .../engine/executor/AvailableTests.java | 61 ------------------- .../test_impacted/commons/LoggerUtils.kt | 2 +- .../engine/executor/AvailableTests.kt | 50 +++++++++++++++ 3 files changed, 51 insertions(+), 62 deletions(-) delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/AvailableTests.java create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/AvailableTests.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/AvailableTests.java deleted file mode 100644 index abaf9074f..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/AvailableTests.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.teamscale.test_impacted.engine.executor; - -import com.teamscale.client.ClusteredTestDetails; -import com.teamscale.client.PrioritizableTest; -import com.teamscale.client.StringUtils; -import com.teamscale.client.TestDetails; -import com.teamscale.test_impacted.commons.LoggerUtils; -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.UniqueId; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * Holds a list of test details that can currently be executed. Provides the ability to translate uniform paths returned - * by the Teamscale server to unique IDs used in JUnit Platform. - */ -public class AvailableTests { - - private static final Logger LOGGER = LoggerUtils.getLogger(AvailableTests.class); - - /** - * A mapping from the tests uniform path (Teamscale internal representation) to unique id (JUnit internal - * representation). - */ - private final Map uniformPathToUniqueIdMapping = new HashMap<>(); - - /** List of all test details. */ - private final List testList = new ArrayList<>(); - - /** Adds a new {@link TestDetails} object and the according uniqueId. */ - public void add(UniqueId uniqueId, ClusteredTestDetails details) { - uniformPathToUniqueIdMapping.put(details.uniformPath, uniqueId); - testList.add(details); - } - - /** Returns the list of available tests. */ - public List getTestList() { - return testList; - } - - /** - * Converts the {@link PrioritizableTest} to the {@link UniqueId} returned by the {@link TestEngine}. - */ - public Optional convertToUniqueId(PrioritizableTest test) { - UniqueId clusterUniqueId = uniformPathToUniqueIdMapping.get(test.testName); - if (clusterUniqueId == null) { - LOGGER.severe(() -> "Retrieved invalid test '" + test.testName + "' from Teamscale server!"); - LOGGER.severe(() -> "The following seem related:"); - uniformPathToUniqueIdMapping.keySet().stream().sorted(Comparator - .comparing(testPath -> StringUtils.levenshteinDistance(test.testName, testPath))).limit(5) - .forEach(testAlternative -> LOGGER.severe(() -> " - " + testAlternative)); - } - return Optional.ofNullable(clusterUniqueId); - } -} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt index ee423775f..9fd70df36 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt @@ -14,7 +14,7 @@ object LoggerUtils { private const val JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY = "java.util.logging.config.file" init { - // Needs to be at the very top so it also takes affect when setting the log level for Console handlers + // Needs to be at the very top, so it also takes effect when setting the log level for Console handlers useDefaultJULConfigFile() MAIN_LOGGER.useParentHandlers = false diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt new file mode 100644 index 000000000..7bade2049 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt @@ -0,0 +1,50 @@ +package com.teamscale.test_impacted.engine.executor + +import com.teamscale.client.ClusteredTestDetails +import com.teamscale.client.PrioritizableTest +import com.teamscale.client.StringUtils.levenshteinDistance +import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import org.junit.platform.engine.UniqueId +import java.util.* +import java.util.logging.Logger + +/** + * Holds a list of test details that can currently be executed. Provides the ability to translate uniform paths returned + * by the Teamscale server to unique IDs used in JUnit Platform. + */ +class AvailableTests { + /** + * A mapping from the tests uniform path (Teamscale internal representation) to unique id (JUnit internal + * representation). + */ + private val uniformPathToUniqueIdMapping = mutableMapOf() + + /** List of all test details. */ + val testList = mutableListOf() + + /** Adds a new [com.teamscale.client.TestDetails] object and the according uniqueId. */ + fun add(uniqueId: UniqueId, details: ClusteredTestDetails) { + uniformPathToUniqueIdMapping[details.uniformPath] = uniqueId + testList.add(details) + } + + /** + * Converts the [PrioritizableTest] to the [UniqueId] returned by the [org.junit.platform.engine.TestEngine]. + */ + fun convertToUniqueId(test: PrioritizableTest): Optional { + val clusterUniqueId = uniformPathToUniqueIdMapping[test.testName] + if (clusterUniqueId == null) { + LOGGER.severe { "Retrieved invalid test '${test.testName}' from Teamscale server!" } + LOGGER.severe { "The following seem related:" } + uniformPathToUniqueIdMapping.keys + .sortedBy { test.testName.levenshteinDistance(it) } + .take(5) + .forEach { LOGGER.severe { " - $it" } } + } + return Optional.ofNullable(clusterUniqueId) + } + + companion object { + private val LOGGER: Logger = getLogger(AvailableTests::class.java) + } +} From 9abca7edadd5f115a97c33b434c08f920b404437 Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 4 Dec 2024 02:29:28 +0100 Subject: [PATCH 08/21] TS-38628 Dynamic logger creation --- .../teamscale/test_impacted/commons/LoggerUtils.kt | 8 +++++++- .../test_impacted/engine/executor/AvailableTests.kt | 7 +++---- .../test_impacted/engine/executor/ITestSorter.kt} | 12 +++++------- .../engine/options/TestEngineOptions.kt | 2 +- .../CucumberPickleDescriptorResolver.kt | 7 +++---- .../JUnitClassBasedTestDescriptorResolverBase.kt | 8 +++----- .../JUnitPlatformSuiteDescriptorResolver.kt | 3 ++- .../TestDescriptorResolverRegistry.kt | 3 ++- .../test_descriptor/TestDescriptorUtils.kt | 3 ++- .../engine/ImpactedTestEngineTestBase.kt | 6 ------ .../engine/ImpactedTestEngineWithDynamicTestsTest.kt | 4 ++-- .../engine/ImpactedTestEngineWithTwoEnginesTest.kt | 4 ++-- .../test_impacted/engine/NoImpactedTestsTest.kt | 4 ++-- ...estwiseCoverageCollectingExecutionListenerTest.kt | 10 +++++----- 14 files changed, 39 insertions(+), 42 deletions(-) rename impacted-test-engine/src/main/{java/com/teamscale/test_impacted/engine/executor/ITestSorter.java => kotlin/com/teamscale/test_impacted/engine/executor/ITestSorter.kt} (52%) diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt index 9fd70df36..c5e8e8d2f 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/commons/LoggerUtils.kt @@ -37,7 +37,7 @@ object LoggerUtils { System.getProperty(JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY) ?: return - val logger = getLogger(LoggerUtils::class.java) + val logger = createLogger() try { val propertiesFilePath = Paths.get(loggingPropertiesFilePathString) if (!propertiesFilePath.toFile().exists()) { @@ -59,4 +59,10 @@ object LoggerUtils { */ @JvmStatic fun getLogger(clazz: Class<*>) = Logger.getLogger(clazz.name) + + /** + * Creates a logger for the given class. + */ + @JvmStatic + fun Any.createLogger(): Logger = Logger.getLogger(this::class.java.name) } diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt index 7bade2049..67e7d7a51 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/AvailableTests.kt @@ -3,6 +3,7 @@ package com.teamscale.test_impacted.engine.executor import com.teamscale.client.ClusteredTestDetails import com.teamscale.client.PrioritizableTest import com.teamscale.client.StringUtils.levenshteinDistance +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger import com.teamscale.test_impacted.commons.LoggerUtils.getLogger import org.junit.platform.engine.UniqueId import java.util.* @@ -13,6 +14,8 @@ import java.util.logging.Logger * by the Teamscale server to unique IDs used in JUnit Platform. */ class AvailableTests { + private val LOGGER = createLogger() + /** * A mapping from the tests uniform path (Teamscale internal representation) to unique id (JUnit internal * representation). @@ -43,8 +46,4 @@ class AvailableTests { } return Optional.ofNullable(clusterUniqueId) } - - companion object { - private val LOGGER: Logger = getLogger(AvailableTests::class.java) - } } diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ITestSorter.java b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ITestSorter.kt similarity index 52% rename from impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ITestSorter.java rename to impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ITestSorter.kt index 4bbb8f7a1..2fb604a3f 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ITestSorter.java +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ITestSorter.kt @@ -1,14 +1,12 @@ -package com.teamscale.test_impacted.engine.executor; +package com.teamscale.test_impacted.engine.executor -import org.junit.platform.engine.TestDescriptor; - -/** Interface for implementing different ways of ordering tests. */ -public interface ITestSorter { +import org.junit.platform.engine.TestDescriptor +/** Interface for implementing different ways of ordering tests. */ +interface ITestSorter { /** * Removes any tests from the test descriptor that should not be executed and changes the execution order of the * remaining tests. */ - void selectAndSort(TestDescriptor testDescriptor); - + fun selectAndSort(testDescriptor: TestDescriptor) } diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt index 1f3ae1e4c..d95910578 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt @@ -104,7 +104,7 @@ class TestEngineOptions { File(reportDirectory, "server-request.txt") ) return ImpactedTestsProvider( - client, baseline, baselineRevision, endCommit, endRevision, repository, partition, + client, baseline!!, baselineRevision!!, endCommit!!, endRevision!!, repository!!, partition!!, runAllTests, includeAddedTests, includeFailedAndSkipped ) } diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt index ed7f2275b..b99e3b8d2 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.kt @@ -1,17 +1,18 @@ package com.teamscale.test_impacted.test_descriptor -import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getUniqueIdSegment import org.junit.platform.engine.TestDescriptor import java.lang.reflect.Field import java.util.* -import java.util.logging.Logger /** * Test descriptor resolver for Cucumber. For details how we extract the uniform path, see comment in * [getPickleName]. The cluster id is the .feature file in which the tests are defined. */ class CucumberPickleDescriptorResolver : ITestDescriptorResolver { + private val LOGGER = createLogger() + override fun getUniformPath(descriptor: TestDescriptor): Optional { val featurePath = descriptor.featurePath() LOGGER.fine { "Resolved feature: $featurePath" } @@ -145,8 +146,6 @@ class CucumberPickleDescriptorResolver : ITestDescriptorResolver { /** Type of the unique id segment of a test descriptor representing a cucumber feature file */ const val FEATURE_SEGMENT_TYPE = "feature" - private val LOGGER: Logger = getLogger(CucumberPickleDescriptorResolver::class.java) - /** * Escapes slashes (/) in a given input (usually a scenario name) with a backslash (\). *



diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt index db5d15487..0832f6b56 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitClassBasedTestDescriptorResolverBase.kt @@ -1,11 +1,13 @@ package com.teamscale.test_impacted.test_descriptor -import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger import org.junit.platform.engine.TestDescriptor import java.util.* /** Test descriptor resolver for JUnit based [org.junit.platform.engine.TestEngine]s. */ abstract class JUnitClassBasedTestDescriptorResolverBase : ITestDescriptorResolver { + private val LOGGER = createLogger() + override fun getUniformPath(descriptor: TestDescriptor): Optional = descriptor.getClassName().map { className -> val dotName = className.replace(".", "/") @@ -28,8 +30,4 @@ abstract class JUnitClassBasedTestDescriptorResolverBase : ITestDescriptorResolv /** Returns the test class containing the test. */ protected abstract fun TestDescriptor.getClassName(): Optional - - companion object { - private val LOGGER = getLogger(JUnitClassBasedTestDescriptorResolverBase::class.java) - } } diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt index c182460ce..bf823ac45 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/JUnitPlatformSuiteDescriptorResolver.kt @@ -1,5 +1,6 @@ package com.teamscale.test_impacted.test_descriptor +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger import com.teamscale.test_impacted.commons.LoggerUtils.getLogger import org.junit.platform.engine.TestDescriptor import org.junit.platform.engine.UniqueId @@ -24,7 +25,7 @@ class JUnitPlatformSuiteDescriptorResolver : ITestDescriptorResolver { get() = "junit-platform-suite" companion object { - private val LOGGER = getLogger(JUnitPlatformSuiteDescriptorResolver::class.java) + private val LOGGER = createLogger() /** Type of the unique id segment of a test descriptor representing a test suite */ private const val SUITE_SEGMENT_TYPE: String = "suite" diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt index 09e642d19..1e3a602e2 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorResolverRegistry.kt @@ -1,5 +1,6 @@ package com.teamscale.test_impacted.test_descriptor +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger import com.teamscale.test_impacted.commons.LoggerUtils.getLogger import org.junit.platform.commons.util.ClassLoaderUtils import java.util.* @@ -9,7 +10,7 @@ import java.util.* * [ServiceLoader]. */ object TestDescriptorResolverRegistry { - private val LOGGER = getLogger(TestDescriptorResolverRegistry::class.java) + private val LOGGER = createLogger() private val TEST_DESCRIPTOR_RESOLVER_BY_ENGINE_ID = mutableMapOf() diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt index a9a02250c..032f8769a 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt @@ -2,6 +2,7 @@ package com.teamscale.test_impacted.test_descriptor import com.teamscale.client.ClusteredTestDetails import com.teamscale.test_impacted.commons.IndentingWriter +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger import com.teamscale.test_impacted.commons.LoggerUtils.getLogger import com.teamscale.test_impacted.engine.executor.AvailableTests import org.junit.platform.engine.TestDescriptor @@ -13,7 +14,7 @@ import java.util.stream.Stream /** Class containing utility methods for [TestDescriptor]s. */ object TestDescriptorUtils { - private val LOGGER = getLogger(TestDescriptorUtils::class.java) + private val LOGGER = createLogger() /** Returns the test descriptor as a formatted string with indented children. */ @JvmStatic diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt index 20132ac16..09252463f 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt @@ -14,17 +14,11 @@ import org.mockito.Mockito.mock /** Base class for testing specific scenarios in the impacted test engine. */ abstract class ImpactedTestEngineTestBase { private val testEngineRegistry = mock() - private val testDataWriter = mock() - private val impactedTestsProvider = mock() - private val discoveryRequest = mock() - private val executionRequest = mock() - private val executionListener = mock() - private val teamscaleAgentNotifier = mock() @Test diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt index daa425c82..bf086de98 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt @@ -35,9 +35,9 @@ internal class ImpactedTestEngineWithDynamicTestsTest : ImpactedTestEngineTestBa ) private val testRoot = SimpleTestDescriptor.testContainer(engineRootId, dynamicTestClassCase) - override val engines get() = listOf(DummyEngine(testRoot)) + override val engines = listOf(DummyEngine(testRoot)) - override val impactedTests get() = + override val impactedTests = listOf( PrioritizableTestCluster( "example/DynamicTest", diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt index 02f5e7b0b..a527cab2f 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt @@ -99,12 +99,12 @@ internal class ImpactedTestEngineWithTwoEnginesTest : ImpactedTestEngineTestBase private val testEngine2Root = SimpleTestDescriptor.testContainer(engine2RootId, otherTestClass) - override val engines get() = listOf( + override val engines = listOf( DummyEngine(testEngine1Root), DummyEngine(testEngine2Root) ) - override val impactedTests get() = + override val impactedTests = listOf( PrioritizableTestCluster( FIRST_TEST_CLASS, diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt index be21cbd55..7e5bbd385 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt @@ -29,8 +29,8 @@ internal class NoImpactedTestsTest : ImpactedTestEngineTestBase() { private val testEngine1Root = SimpleTestDescriptor.testContainer(engine1RootId, firstTestClass) - override val engines get() = listOf(DummyEngine(testEngine1Root)) - override val impactedTests get() = emptyList() + override val engines = listOf(DummyEngine(testEngine1Root)) + override val impactedTests = emptyList() override fun verifyCallbacks(executionListener: EngineExecutionListener) {} diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt index bac8c9e57..921decf29 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt @@ -57,7 +57,7 @@ internal class TestwiseCoverageCollectingExecutionListenerTest { executionListener.executionStarted(testClass) Mockito.verify(executionListenerMock).executionStarted(testClass) - // Execution of impacted test case. + // Execution of an impacted test case. executionListener.executionStarted(impactedTestCase) Mockito.verify(mockApi).startTest("MyClass/impactedTestCase()") Mockito.verify(executionListenerMock).executionStarted(impactedTestCase) @@ -99,10 +99,10 @@ internal class TestwiseCoverageCollectingExecutionListenerTest { val testCase1Id = testClassId.append("TEST_CASE", "testCase1()") val testCase2Id = testClassId.append("TEST_CASE", "testCase2()") - val testCase1: TestDescriptor = SimpleTestDescriptor.testCase(testCase1Id) - val testCase2: TestDescriptor = SimpleTestDescriptor.testCase(testCase2Id) - val testClass: TestDescriptor = SimpleTestDescriptor.testContainer(testClassId, testCase1, testCase2) - val testRoot: TestDescriptor = SimpleTestDescriptor.testContainer(rootId, testClass) + val testCase1 = SimpleTestDescriptor.testCase(testCase1Id) + val testCase2 = SimpleTestDescriptor.testCase(testCase2Id) + val testClass = SimpleTestDescriptor.testContainer(testClassId, testCase1, testCase2) + val testRoot = SimpleTestDescriptor.testContainer(rootId, testClass) Mockito.`when`(resolver.getUniformPath(testCase1)) .thenReturn(Optional.of("MyClass/testCase1()")) From 557989560237ba5c02adf59b37c7e885079147b9 Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 4 Dec 2024 20:13:59 +0100 Subject: [PATCH 09/21] TS-38628 Sorter listener migration --- .../engine/executor/ImpactedTestsSorter.java | 105 ---------- .../engine/executor/NOPTestSorter.java | 14 -- ...seCoverageCollectingExecutionListener.java | 181 ------------------ .../engine/executor/ImpactedTestsSorter.kt | 91 +++++++++ .../engine/executor/NOPTestSorter.kt | 13 ++ ...wiseCoverageCollectingExecutionListener.kt | 175 +++++++++++++++++ 6 files changed, 279 insertions(+), 300 deletions(-) delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/NOPTestSorter.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListener.java create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsSorter.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/NOPTestSorter.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListener.kt 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) + } +} From 1f80e95d6c45939a8a03d1f87e91e08ea2f75d4f Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 4 Dec 2024 21:01:36 +0100 Subject: [PATCH 10/21] TS-38628 Broken mockito --- impacted-test-engine/build.gradle.kts | 1 + .../executor/TeamscaleAgentNotifier.java | 68 ------------------- .../engine/executor/TeamscaleAgentNotifier.kt | 65 ++++++++++++++++++ settings.gradle.kts | 2 +- 4 files changed, 67 insertions(+), 69 deletions(-) delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.java create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.kt diff --git a/impacted-test-engine/build.gradle.kts b/impacted-test-engine/build.gradle.kts index d00a85ae9..500c27a91 100644 --- a/impacted-test-engine/build.gradle.kts +++ b/impacted-test-engine/build.gradle.kts @@ -21,4 +21,5 @@ dependencies { compileOnly(libs.junit.platform.commons) testImplementation(libs.junit.platform.engine) testImplementation(libs.junit.jupiter.params) + testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") } diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.java deleted file mode 100644 index 21e9710cb..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.teamscale.test_impacted.engine.executor; - -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.test_impacted.commons.LoggerUtils; -import com.teamscale.tia.client.ITestwiseCoverageAgentApi; -import com.teamscale.tia.client.UrlUtils; - -import java.io.IOException; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** Communicates test start and end to the agent and the end of the overall test execution. */ -public class TeamscaleAgentNotifier { - - private static final Logger LOGGER = LoggerUtils.getLogger(TeamscaleAgentNotifier.class); - - /** A list of API services to signal test start and end to the agent. */ - private final List testwiseCoverageAgentApis; - - /** - * Whether only a part of the tests is being executed (true) or whether all tests are executed - * (false). - */ - private final boolean partial; - - public TeamscaleAgentNotifier(List testwiseCoverageAgentApis, boolean partial) { - this.testwiseCoverageAgentApis = testwiseCoverageAgentApis; - this.partial = partial; - } - - /** Reports the start of a test to the Teamscale JaCoCo agent. */ - public void startTest(String testUniformPath) { - try { - for (ITestwiseCoverageAgentApi apiService : testwiseCoverageAgentApis) { - apiService.testStarted(UrlUtils.encodeUrl(testUniformPath)).execute(); - } - } catch (IOException e) { - LOGGER.log(Level.SEVERE, e, () -> "Error while calling service api."); - } - } - - /** Reports the end of a test to the Teamscale JaCoCo agent. */ - public void endTest(String testUniformPath, TestExecution testExecution) { - try { - for (ITestwiseCoverageAgentApi apiService : testwiseCoverageAgentApis) { - if (testExecution == null) { - apiService.testFinished(UrlUtils.encodeUrl(testUniformPath)).execute(); - } else { - apiService.testFinished(UrlUtils.encodeUrl(testUniformPath), testExecution).execute(); - } - } - } catch (IOException e) { - LOGGER.log(Level.SEVERE, e, () -> "Error contacting test wise coverage agent."); - } - } - - /** Reports the end of the test run to the Teamscale JaCoCo agent. */ - public void testRunEnded() { - try { - for (ITestwiseCoverageAgentApi apiService : testwiseCoverageAgentApis) { - apiService.testRunFinished(partial).execute(); - } - } catch (IOException e) { - LOGGER.log(Level.SEVERE, e, () -> "Error contacting test wise coverage agent."); - } - } -} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.kt new file mode 100644 index 000000000..6a98dd41b --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.kt @@ -0,0 +1,65 @@ +package com.teamscale.test_impacted.engine.executor + +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger +import com.teamscale.tia.client.ITestwiseCoverageAgentApi +import com.teamscale.tia.client.UrlUtils.encodeUrl +import java.io.IOException +import java.util.logging.Level + +/** Communicates test start and end to the agent and the end of the overall test execution. */ +open class TeamscaleAgentNotifier( + /** A list of API services to signal test start and end to the agent. */ + private val testwiseCoverageAgentApis: List, + /** + * Whether only a part of the tests is being executed (`true`) or whether all tests are executed + * (`false`). + */ + private val partial: Boolean +) { + private val logger = createLogger() + + /** Reports the start of a test to the Teamscale JaCoCo agent. */ + open fun startTest(testUniformPath: String) { + try { + testwiseCoverageAgentApis.forEach { apiService -> + apiService.testStarted(testUniformPath.encodeUrl()).execute() + } + } catch (e: IOException) { + logger.log( + Level.SEVERE, e + ) { "Error while calling service api." } + } + } + + /** Reports the end of a test to the Teamscale JaCoCo agent. */ + open fun endTest(testUniformPath: String, testExecution: TestExecution?) { + try { + testwiseCoverageAgentApis.forEach { apiService -> + val url = testUniformPath.encodeUrl() + if (testExecution == null) { + apiService.testFinished(url).execute() + } else { + apiService.testFinished(url, testExecution).execute() + } + } + } catch (e: IOException) { + logger.log( + Level.SEVERE, e + ) { "Error contacting test wise coverage agent." } + } + } + + /** Reports the end of the test run to the Teamscale JaCoCo agent. */ + open fun testRunEnded() { + try { + testwiseCoverageAgentApis.forEach { apiService -> + apiService.testRunFinished(partial).execute() + } + } catch (e: IOException) { + logger.log( + Level.SEVERE, e + ) { "Error contacting test wise coverage agent." } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c9295ad7e..4637f5a87 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,6 @@ include(":sample-debugging-app") include(":teamscale-maven-plugin") include(":installer") -file("system-tests").listFiles { file: File -> !file.isHidden && file.isDirectory }?.forEach { folder -> +file("system-tests").listFiles { file -> !file.isHidden && file.isDirectory }?.forEach { folder -> include(":system-tests:${folder.name}") } From 779dab132283ff0e928eca7c24ce9183e0cb58bb Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 4 Dec 2024 21:11:18 +0100 Subject: [PATCH 11/21] TS-38628 Remove mockito kotlin --- impacted-test-engine/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/impacted-test-engine/build.gradle.kts b/impacted-test-engine/build.gradle.kts index 500c27a91..d00a85ae9 100644 --- a/impacted-test-engine/build.gradle.kts +++ b/impacted-test-engine/build.gradle.kts @@ -21,5 +21,4 @@ dependencies { compileOnly(libs.junit.platform.commons) testImplementation(libs.junit.platform.engine) testImplementation(libs.junit.jupiter.params) - testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") } From 69c3273cb2436d12b80b75e08d5a10abdac5cc32 Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 4 Dec 2024 21:48:35 +0100 Subject: [PATCH 12/21] TS-38628 Update Mockito 4.11.0 -> 5.4.0 and Mockito Kotlin --- .../com.teamscale.java-convention.gradle.kts | 1 + gradle/libs.versions.toml | 4 +- impacted-test-engine/build.gradle.kts | 3 +- ...CoverageCollectingExecutionListenerTest.kt | 67 +++++++++---------- 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/buildSrc/src/main/kotlin/com.teamscale.java-convention.gradle.kts b/buildSrc/src/main/kotlin/com.teamscale.java-convention.gradle.kts index 5a9dc152c..e74bf7475 100644 --- a/buildSrc/src/main/kotlin/com.teamscale.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/com.teamscale.java-convention.gradle.kts @@ -33,6 +33,7 @@ dependencies { testImplementation(lib("assertj")) testImplementation(lib("mockito-core")) testImplementation(lib("mockito-junit")) + testImplementation(lib("mockito-kotlin")) testRuntimeOnly(lib("junit-platform-launcher")) testRuntimeOnly(lib("junit-jupiter-engine")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2526779e3..b2b2a8385 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,8 @@ retrofit = "2.11.0" junit = "5.11.3" junitPlatform = "1.11.3" okhttp = "4.12.0" -mockito = "4.11.0" +mockito = "5.12.0" +mockitoKotlin = "5.4.0" picocli = "4.7.6" [libraries] @@ -75,6 +76,7 @@ jsonassert = { module = "org.skyscreamer:jsonassert", version = "1.5.3" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } springboot-loader = { module = "org.springframework.boot:spring-boot-loader", version = "3.4.0" } diff --git a/impacted-test-engine/build.gradle.kts b/impacted-test-engine/build.gradle.kts index d00a85ae9..2cb328c3e 100644 --- a/impacted-test-engine/build.gradle.kts +++ b/impacted-test-engine/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { compileOnly(libs.junit.platform.engine) compileOnly(libs.junit.platform.commons) - testImplementation(libs.junit.platform.engine) + testImplementation(libs.junit.platform.engine) testImplementation(libs.junit.jupiter.params) + testImplementation(libs.mockito.kotlin) } diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt index 921decf29..a3d0de82a 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt @@ -6,16 +6,15 @@ import com.teamscale.test_impacted.test_descriptor.ITestDescriptorResolver import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test 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.mockito.ArgumentMatchers -import org.mockito.Mockito -import org.mockito.Mockito.mock +import org.mockito.kotlin.* import java.util.* +@Suppress("INLINE_FROM_HIGHER_PLATFORM") /** Tests for [TestwiseCoverageCollectingExecutionListener]. */ internal class TestwiseCoverageCollectingExecutionListenerTest { + private val mockApi = mock() private val resolver = mock() @@ -42,54 +41,50 @@ internal class TestwiseCoverageCollectingExecutionListenerTest { ) val testRoot = SimpleTestDescriptor.testContainer(rootId, testClass) - Mockito.`when`(resolver.getUniformPath(impactedTestCase)) + whenever(resolver.getUniformPath(impactedTestCase)) .thenReturn(Optional.of("MyClass/impactedTestCase()")) - Mockito.`when`(resolver.getClusterId(impactedTestCase)) + whenever(resolver.getClusterId(impactedTestCase)) .thenReturn(Optional.of("MyClass")) - Mockito.`when`(resolver.getUniformPath(regularSkippedTestCase)) + whenever(resolver.getUniformPath(regularSkippedTestCase)) .thenReturn(Optional.of("MyClass/regularSkippedTestCase()")) - Mockito.`when`(resolver.getClusterId(regularSkippedTestCase)) + whenever(resolver.getClusterId(regularSkippedTestCase)) .thenReturn(Optional.of("MyClass")) // Start engine and class. executionListener.executionStarted(testRoot) - Mockito.verify(executionListenerMock).executionStarted(testRoot) + verify(executionListenerMock).executionStarted(testRoot) executionListener.executionStarted(testClass) - Mockito.verify(executionListenerMock).executionStarted(testClass) + verify(executionListenerMock).executionStarted(testClass) // Execution of an impacted test case. executionListener.executionStarted(impactedTestCase) - Mockito.verify(mockApi).startTest("MyClass/impactedTestCase()") - Mockito.verify(executionListenerMock).executionStarted(impactedTestCase) + verify(mockApi).startTest("MyClass/impactedTestCase()") + verify(executionListenerMock).executionStarted(impactedTestCase) executionListener.executionFinished(impactedTestCase, TestExecutionResult.successful()) - Mockito.verify(mockApi).endTest(ArgumentMatchers.eq("MyClass/impactedTestCase()"), ArgumentMatchers.any()) - Mockito.verify(executionListenerMock).executionFinished(impactedTestCase, TestExecutionResult.successful()) + verify(mockApi).endTest(eq("MyClass/impactedTestCase()"), any()) + verify(executionListenerMock).executionFinished(impactedTestCase, TestExecutionResult.successful()) // Ignored or disabled impacted test case is skipped. executionListener.executionSkipped(regularSkippedTestCase, "Test is disabled.") - Mockito.verify(executionListenerMock).executionSkipped(regularSkippedTestCase, "Test is disabled.") + verify(executionListenerMock).executionSkipped(regularSkippedTestCase, "Test is disabled.") // Finish class and engine. executionListener.executionFinished(testClass, TestExecutionResult.successful()) - Mockito.verify(executionListenerMock).executionFinished(testClass, TestExecutionResult.successful()) + verify(executionListenerMock).executionFinished(testClass, TestExecutionResult.successful()) executionListener.executionFinished(testRoot, TestExecutionResult.successful()) - Mockito.verify(executionListenerMock).executionFinished(testRoot, TestExecutionResult.successful()) + verify(executionListenerMock).executionFinished(testRoot, TestExecutionResult.successful()) - Mockito.verifyNoMoreInteractions(mockApi) - Mockito.verifyNoMoreInteractions(executionListenerMock) + verifyNoMoreInteractions(mockApi) + verifyNoMoreInteractions(executionListenerMock) val testExecutions = executionListener.testExecutions Assertions.assertThat(testExecutions).hasSize(2) Assertions.assertThat(testExecutions).anySatisfy { testExecution: TestExecution -> - Assertions.assertThat( - testExecution.uniformPath - ).isEqualTo("MyClass/impactedTestCase()") + Assertions.assertThat(testExecution.uniformPath).isEqualTo("MyClass/impactedTestCase()") } Assertions.assertThat(testExecutions).anySatisfy { testExecution: TestExecution -> - Assertions.assertThat( - testExecution.uniformPath - ).isEqualTo("MyClass/regularSkippedTestCase()") + Assertions.assertThat(testExecution.uniformPath).isEqualTo("MyClass/regularSkippedTestCase()") } } @@ -104,29 +99,29 @@ internal class TestwiseCoverageCollectingExecutionListenerTest { val testClass = SimpleTestDescriptor.testContainer(testClassId, testCase1, testCase2) val testRoot = SimpleTestDescriptor.testContainer(rootId, testClass) - Mockito.`when`(resolver.getUniformPath(testCase1)) + whenever(resolver.getUniformPath(testCase1)) .thenReturn(Optional.of("MyClass/testCase1()")) - Mockito.`when`(resolver.getClusterId(testCase1)) + whenever(resolver.getClusterId(testCase1)) .thenReturn(Optional.of("MyClass")) - Mockito.`when`(resolver.getUniformPath(testCase2)) + whenever(resolver.getUniformPath(testCase2)) .thenReturn(Optional.of("MyClass/testCase2()")) - Mockito.`when`(resolver.getClusterId(testCase2)) + whenever(resolver.getClusterId(testCase2)) .thenReturn(Optional.of("MyClass")) // Start engine and class. executionListener.executionStarted(testRoot) - Mockito.verify(executionListenerMock).executionStarted(testRoot) + verify(executionListenerMock).executionStarted(testRoot) executionListener.executionSkipped(testClass, "Test class is disabled.") - Mockito.verify(executionListenerMock).executionStarted(testClass) - Mockito.verify(executionListenerMock).executionSkipped(testCase1, "Test class is disabled.") - Mockito.verify(executionListenerMock).executionSkipped(testCase2, "Test class is disabled.") - Mockito.verify(executionListenerMock).executionFinished(testClass, TestExecutionResult.successful()) + verify(executionListenerMock).executionStarted(testClass) + verify(executionListenerMock).executionSkipped(testCase1, "Test class is disabled.") + verify(executionListenerMock).executionSkipped(testCase2, "Test class is disabled.") + verify(executionListenerMock).executionFinished(testClass, TestExecutionResult.successful()) executionListener.executionFinished(testRoot, TestExecutionResult.successful()) - Mockito.verify(executionListenerMock).executionFinished(testRoot, TestExecutionResult.successful()) + verify(executionListenerMock).executionFinished(testRoot, TestExecutionResult.successful()) - Mockito.verifyNoMoreInteractions(executionListenerMock) + verifyNoMoreInteractions(executionListenerMock) val testExecutions = executionListener.testExecutions From 22a9e1a4edac1c9c0283cd292cd37f62a53033f9 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Thu, 5 Dec 2024 14:03:41 +0100 Subject: [PATCH 13/21] TS-38628 Run tests using Java 21 to allow newer Mockito versions --- .github/workflows/actions.yml | 4 ++-- .gitignore | 1 + agent/src/docker/Dockerfile | 2 +- .../git_properties/GitPropertiesLocatorUtils.java | 2 +- build.gradle.kts | 1 - buildSrc/build.gradle.kts | 1 + .../kotlin/com.teamscale.java-convention.gradle.kts | 11 ++++++++++- .../com.teamscale.kotlin-convention.gradle.kts | 13 +++++++++++++ impacted-test-engine/build.gradle.kts | 3 +-- installer/build.gradle.kts | 6 +++++- renovate.json | 6 ------ report-generator/build.gradle.kts | 3 +-- settings.gradle.kts | 6 ------ .../kotlin-inline-function-test/build.gradle.kts | 2 +- teamscale-client/build.gradle.kts | 3 +-- teamscale-gradle-plugin/build.gradle.kts | 4 ++++ tia-client/build.gradle.kts | 3 +-- tia-runlisteners/build.gradle.kts | 3 +-- 18 files changed, 44 insertions(+), 30 deletions(-) create mode 100644 buildSrc/src/main/kotlin/com.teamscale.kotlin-convention.gradle.kts diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index b00da9c2d..79351f72a 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Build with Gradle run: ./gradlew build - name: Upload Release Assets @@ -79,7 +79,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Build with Gradle run: ./gradlew build - name: Upload coverage to Teamscale diff --git a/.gitignore b/.gitignore index 35bfe9e03..4f18d4dab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .gradle/ +.kotlin/ **/build/** **/out/** /*/bin/ diff --git a/agent/src/docker/Dockerfile b/agent/src/docker/Dockerfile index 9ddea9b63..24d2841c6 100644 --- a/agent/src/docker/Dockerfile +++ b/agent/src/docker/Dockerfile @@ -1,6 +1,6 @@ # --- build image --- -FROM openjdk:17 as build +FROM openjdk:21 as build # install xargs which is needed during the build RUN microdnf install findutils ADD . /src diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.java b/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.java index b0f0d6819..f9b1608e7 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatorUtils.java @@ -343,7 +343,7 @@ static List> findGitPropertiesInArchive( } /** - * Returns the CommitInfo (revision & branch + timestmap) from a git properties file. The revision can be either in + * Returns the CommitInfo (revision and branch + timestmap) from a git properties file. The revision can be either in * {@link #GIT_PROPERTIES_GIT_COMMIT_ID} or {@link #GIT_PROPERTIES_GIT_COMMIT_ID_FULL}. The branch and timestamp in * {@link #GIT_PROPERTIES_GIT_BRANCH} + {@link #GIT_PROPERTIES_GIT_COMMIT_TIME} or in * {@link #GIT_PROPERTIES_TEAMSCALE_COMMIT_BRANCH} + {@link #GIT_PROPERTIES_TEAMSCALE_COMMIT_TIME}. By default, diff --git a/build.gradle.kts b/build.gradle.kts index 85abb933d..6c21ccbfa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,6 @@ plugins { alias(libs.plugins.versions) alias(libs.plugins.nexusPublish) - kotlin("jvm") apply false } group = "com.teamscale" diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 6c15dca0d..85e5900d2 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -18,4 +18,5 @@ dependencies { implementation("org.ow2.asm:asm:9.7.1") implementation("org.ow2.asm:asm-commons:9.7.1") + implementation("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:2.0.20") } diff --git a/buildSrc/src/main/kotlin/com.teamscale.java-convention.gradle.kts b/buildSrc/src/main/kotlin/com.teamscale.java-convention.gradle.kts index e74bf7475..bb1c9e4ba 100644 --- a/buildSrc/src/main/kotlin/com.teamscale.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/com.teamscale.java-convention.gradle.kts @@ -12,10 +12,19 @@ repositories { java { toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) + languageVersion.set(JavaLanguageVersion.of(21)) } } +tasks.compileJava { + options.release = 8 + options.compilerArgs.add("-Xlint:-options") +} + +tasks.compileTestJava { + options.release = 21 +} + tasks.test { useJUnitPlatform { excludeEngines("teamscale-test-impacted") diff --git a/buildSrc/src/main/kotlin/com.teamscale.kotlin-convention.gradle.kts b/buildSrc/src/main/kotlin/com.teamscale.kotlin-convention.gradle.kts new file mode 100644 index 000000000..2029127ce --- /dev/null +++ b/buildSrc/src/main/kotlin/com.teamscale.kotlin-convention.gradle.kts @@ -0,0 +1,13 @@ +import org.gradle.kotlin.dsl.kotlin +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("jvm") + id("com.teamscale.java-convention") +} + +tasks.compileKotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } +} diff --git a/impacted-test-engine/build.gradle.kts b/impacted-test-engine/build.gradle.kts index 2cb328c3e..cf4d681d7 100644 --- a/impacted-test-engine/build.gradle.kts +++ b/impacted-test-engine/build.gradle.kts @@ -1,7 +1,6 @@ plugins { - kotlin("jvm") `java-library` - com.teamscale.`java-convention` + com.teamscale.`kotlin-convention` com.teamscale.coverage com.teamscale.`shadow-convention` com.teamscale.publish diff --git a/installer/build.gradle.kts b/installer/build.gradle.kts index cd33e39a7..357d585a6 100644 --- a/installer/build.gradle.kts +++ b/installer/build.gradle.kts @@ -5,7 +5,7 @@ plugins { com.teamscale.`java-convention` com.teamscale.coverage com.teamscale.`system-test-convention` - id("org.beryx.jlink") version ("3.0.1") + id("org.beryx.jlink") version ("3.1.1") } tasks.jar { @@ -22,6 +22,10 @@ java { } } +tasks.withType { + options.release = 17 +} + application { applicationName = "installer" mainClass = "com.teamscale.profiler.installer.RootCommand" diff --git a/renovate.json b/renovate.json index 8335d78c2..18ed4b5ca 100644 --- a/renovate.json +++ b/renovate.json @@ -39,12 +39,6 @@ ], "allowedVersions": "< 2" }, - { - "matchPackagePrefixes": [ - "org.mockito" - ], - "allowedVersions": "< 5" - }, { "matchPaths": ["installer/build.gradle.kts"], "matchPackageNames": [ diff --git a/report-generator/build.gradle.kts b/report-generator/build.gradle.kts index e9d3e4b12..120e0ee1b 100644 --- a/report-generator/build.gradle.kts +++ b/report-generator/build.gradle.kts @@ -1,9 +1,8 @@ plugins { `java-library` - com.teamscale.`java-convention` + com.teamscale.`kotlin-convention` com.teamscale.coverage com.teamscale.publish - kotlin("jvm") } publishAs { diff --git a/settings.gradle.kts b/settings.gradle.kts index 4637f5a87..d4497209f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,9 +1,3 @@ -pluginManagement { - plugins { - kotlin("jvm") version "2.0.21" - } -} - plugins { id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0") } diff --git a/system-tests/kotlin-inline-function-test/build.gradle.kts b/system-tests/kotlin-inline-function-test/build.gradle.kts index fefee92b2..ad05829d7 100644 --- a/system-tests/kotlin-inline-function-test/build.gradle.kts +++ b/system-tests/kotlin-inline-function-test/build.gradle.kts @@ -1,6 +1,6 @@ plugins { + com.teamscale.`kotlin-convention` com.teamscale.`system-test-convention` - kotlin("jvm") } tasks.test { diff --git a/teamscale-client/build.gradle.kts b/teamscale-client/build.gradle.kts index 263410236..6873a0aee 100644 --- a/teamscale-client/build.gradle.kts +++ b/teamscale-client/build.gradle.kts @@ -1,7 +1,6 @@ plugins { - kotlin("jvm") `java-library` - com.teamscale.`java-convention` + com.teamscale.`kotlin-convention` com.teamscale.coverage com.teamscale.publish } diff --git a/teamscale-gradle-plugin/build.gradle.kts b/teamscale-gradle-plugin/build.gradle.kts index 10221696e..c64d9ce96 100644 --- a/teamscale-gradle-plugin/build.gradle.kts +++ b/teamscale-gradle-plugin/build.gradle.kts @@ -13,6 +13,10 @@ java { } } +tasks.withType { + options.release = 11 +} + publishAs { readableName.set("Teamscale Gradle Plugin") description.set("A Gradle plugin that supports collecting Testwise Coverage and uploading reports to Teamscale.") diff --git a/tia-client/build.gradle.kts b/tia-client/build.gradle.kts index d49504163..cfa2fdccc 100644 --- a/tia-client/build.gradle.kts +++ b/tia-client/build.gradle.kts @@ -1,7 +1,6 @@ plugins { - kotlin("jvm") `java-library` - com.teamscale.`java-convention` + com.teamscale.`kotlin-convention` com.teamscale.coverage com.teamscale.publish } diff --git a/tia-runlisteners/build.gradle.kts b/tia-runlisteners/build.gradle.kts index c8f590315..95384cdd1 100644 --- a/tia-runlisteners/build.gradle.kts +++ b/tia-runlisteners/build.gradle.kts @@ -1,7 +1,6 @@ plugins { - kotlin("jvm") `java-library` - com.teamscale.`java-convention` + com.teamscale.`kotlin-convention` com.teamscale.coverage com.teamscale.publish } From 01183b2d3c2a5a92c47f4bf216291b3bf939cea7 Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 9 Dec 2024 01:18:43 +0100 Subject: [PATCH 14/21] TS-38628 Fix some test failures --- .../test/commons/SystemTestUtils.java | 2 +- .../engine/options/TestEngineOptions.kt | 25 +++++++++++-------- ...CoverageCollectingExecutionListenerTest.kt | 1 - .../report/testwise/model/TestExecution.kt | 1 + 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java b/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java index eebc26d52..9dd32bff3 100644 --- a/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java +++ b/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java @@ -166,7 +166,7 @@ public static List getReportFileNames(String mavenProjectPath) throws IOEx } } - private interface AgentService { + public interface AgentService { /** Dumps coverage */ @POST("/dump") Call dump(); diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt index d95910578..37c45f861 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt @@ -99,12 +99,14 @@ class TestEngineOptions { private fun createImpactedTestsProvider(): ImpactedTestsProvider { val client = TeamscaleClient( - serverOptions?.url, serverOptions?.userName!!, - serverOptions?.userAccessToken!!, serverOptions?.project!!, + serverOptions?.url, + serverOptions?.userName!!, + serverOptions?.userAccessToken!!, + serverOptions?.project!!, File(reportDirectory, "server-request.txt") ) return ImpactedTestsProvider( - client, baseline!!, baselineRevision!!, endCommit!!, endRevision!!, repository!!, partition!!, + client, baseline, baselineRevision, endCommit, endRevision, repository, partition, runAllTests, includeAddedTests, includeFailedAndSkipped ) } @@ -116,12 +118,12 @@ class TestEngineOptions { class Builder { private val testEngineOptions = TestEngineOptions() - fun serverOptions(serverOptions: ServerOptions): Builder { + fun serverOptions(serverOptions: ServerOptions?): Builder { testEngineOptions.serverOptions = serverOptions return this } - fun partition(partition: String): Builder { + fun partition(partition: String?): Builder { testEngineOptions.partition = partition return this } @@ -146,27 +148,27 @@ class TestEngineOptions { return this } - fun endCommit(endCommit: CommitDescriptor): Builder { + fun endCommit(endCommit: CommitDescriptor?): Builder { testEngineOptions.endCommit = endCommit return this } - fun endRevision(endRevision: String): Builder { + fun endRevision(endRevision: String?): Builder { testEngineOptions.endRevision = endRevision return this } - fun repository(repository: String): Builder { + fun repository(repository: String?): Builder { testEngineOptions.repository = repository return this } - fun baseline(baseline: String): Builder { + fun baseline(baseline: String?): Builder { testEngineOptions.baseline = baseline return this } - fun baselineRevision(baselineRevision: String): Builder { + fun baselineRevision(baselineRevision: String?): Builder { testEngineOptions.baselineRevision = baselineRevision return this } @@ -192,7 +194,8 @@ class TestEngineOptions { return this } - fun reportDirectory(reportDirectory: String): Builder { + fun reportDirectory(reportDirectory: String?): Builder { + reportDirectory ?: return this testEngineOptions.reportDirectory = File(reportDirectory) return this } diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt index a3d0de82a..58e74ccd8 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt @@ -11,7 +11,6 @@ import org.junit.platform.engine.UniqueId import org.mockito.kotlin.* import java.util.* -@Suppress("INLINE_FROM_HIGHER_PLATFORM") /** Tests for [TestwiseCoverageCollectingExecutionListener]. */ internal class TestwiseCoverageCollectingExecutionListenerTest { diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt index 788165a64..22dde95a7 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt @@ -46,5 +46,6 @@ data class TestExecution @JvmOverloads constructor( private val duration: Double? = null val durationSeconds: Double + @Suppress("DEPRECATION") get() = duration ?: (durationMillis / 1000.0) } From be430f41aeaa87324c24300088be650c33331749 Mon Sep 17 00:00:00 2001 From: Constructor Date: Thu, 12 Dec 2024 22:01:39 +0100 Subject: [PATCH 15/21] TS-38628 Fix marker collisions --- .../java/com/teamscale/jacoco/agent/JaCoCoPreMain.java | 2 +- system-tests/multiple-agents-test/build.gradle.kts | 8 ++------ system-tests/sut-uses-logback-test/build.gradle.kts | 6 ------ 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/JaCoCoPreMain.java b/agent/src/main/java/com/teamscale/jacoco/agent/JaCoCoPreMain.java index b95071ebb..eca6771e4 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/JaCoCoPreMain.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/JaCoCoPreMain.java @@ -69,7 +69,7 @@ private static IRuntime createRuntime(final Instrumentation inst) final Class clazz = module .loadClassInModule(InjectedClassRuntime.class); return clazz.getConstructor(Class.class, String.class) - .newInstance(Object.class, "$JaCoCo"); + .newInstance(Object.class, "$TeamscaleJaCoCo"); } return ModifiedSystemClassRuntime.createFor(inst, diff --git a/system-tests/multiple-agents-test/build.gradle.kts b/system-tests/multiple-agents-test/build.gradle.kts index 32f2241be..e81005972 100644 --- a/system-tests/multiple-agents-test/build.gradle.kts +++ b/system-tests/multiple-agents-test/build.gradle.kts @@ -6,7 +6,7 @@ val jacocoAgent by configurations.creating dependencies { // This version should differ from the version we currently use for the Teamscale JaCoCo agent itself - jacocoAgent("org.jacoco:org.jacoco.agent:0.7.8:runtime") + jacocoAgent("org.jacoco:org.jacoco.agent:0.8.10:runtime") } tasks.test { @@ -19,10 +19,6 @@ tasks.test { delete(logFilePath) } - teamscaleAgent( - mapOf( - "debug" to logFilePath - ) - ) + teamscaleAgent(mapOf("debug" to logFilePath)) } diff --git a/system-tests/sut-uses-logback-test/build.gradle.kts b/system-tests/sut-uses-logback-test/build.gradle.kts index 0799e302a..34f5f9b66 100644 --- a/system-tests/sut-uses-logback-test/build.gradle.kts +++ b/system-tests/sut-uses-logback-test/build.gradle.kts @@ -18,12 +18,6 @@ tasks.test { } } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(11)) - } -} - tasks.withType { archiveFileName.set("app.jar") manifest { From 03e1dc813075841752dbd8ce55b8bcb56ac032f6 Mon Sep 17 00:00:00 2001 From: Constructor Date: Fri, 13 Dec 2024 03:44:21 +0100 Subject: [PATCH 16/21] TS-38628 Migrate ImpactedTestEngine mocks to kotlin mocks --- .../executor/ImpactedTestsProvider.java | 121 ------------------ .../engine/executor/ImpactedTestsProvider.kt | 105 +++++++++++++++ .../engine/options/TestEngineOptions.kt | 2 +- .../engine/ImpactedTestEngineTestBase.kt | 21 ++- .../ImpactedTestEngineWithDynamicTestsTest.kt | 20 +-- .../ImpactedTestEngineWithTwoEnginesTest.kt | 65 +++++----- .../engine/NoImpactedTestsTest.kt | 2 - .../engine/executor/SimpleTestDescriptor.kt | 1 - ...CoverageCollectingExecutionListenerTest.kt | 3 - 9 files changed, 156 insertions(+), 184 deletions(-) delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.java create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.java deleted file mode 100644 index 537d43fc8..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.teamscale.test_impacted.engine.executor; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.teamscale.client.ClusteredTestDetails; -import com.teamscale.client.CommitDescriptor; -import com.teamscale.client.PrioritizableTestCluster; -import com.teamscale.client.TeamscaleClient; -import com.teamscale.test_impacted.commons.LoggerUtils; - -import okhttp3.ResponseBody; -import retrofit2.Response; - -/** - * Class for retrieving the impacted {@link PrioritizableTestCluster}s corresponding to {@link ClusteredTestDetails} - * available for test execution. - */ -public class ImpactedTestsProvider { - - private static final Logger LOGGER = LoggerUtils.getLogger(ImpactedTestsProvider.class); - - private final TeamscaleClient client; - - private final String baseline; - - private final String baselineRevision; - - private final CommitDescriptor endCommit; - - private final String endRevision; - - private final String repository; - - /** - * The partition for the tests provided. - */ - public final String partition; - - private final boolean includeNonImpacted; - - private final boolean includeAddedTests; - - private final boolean includeFailedAndSkipped; - - public ImpactedTestsProvider(TeamscaleClient client, String baseline, String baselineRevision, CommitDescriptor endCommit, String endRevision, String repository, String partition, - boolean includeNonImpacted, boolean includeAddedTests, - boolean includeFailedAndSkipped) { - this.client = client; - this.baseline = baseline; - this.baselineRevision = baselineRevision; - this.endCommit = endCommit; - this.endRevision = endRevision; - this.repository = repository; - this.partition = partition; - this.includeNonImpacted = includeNonImpacted; - this.includeAddedTests = includeAddedTests; - this.includeFailedAndSkipped = includeFailedAndSkipped; - } - - /** Queries Teamscale for impacted tests. */ - public List getImpactedTestsFromTeamscale( - List availableTestDetails) { - try { - LOGGER.info(() -> "Getting impacted tests..."); - Response> response = client - .getImpactedTests(availableTestDetails, baseline, baselineRevision, endCommit, endRevision, repository, - Collections.singletonList(partition), - includeNonImpacted, includeAddedTests, includeFailedAndSkipped); - - if (response.isSuccessful()) { - List testClusters = response.body(); - if (testClusters != null && testCountIsPlausible(testClusters, availableTestDetails)) { - return testClusters; - - } - LOGGER.severe("Teamscale was not able to determine impacted tests:\n" + response.body()); - } else { - LOGGER.severe("Retrieval of impacted tests failed: " + response.code() + " " + response - .message() + "\n" + getErrorBody(response)); - } - } catch (IOException e) { - LOGGER.log(Level.SEVERE, e, () -> "Retrieval of impacted tests failed."); - } - return null; - } - - private static String getErrorBody(Response response) throws IOException { - try (ResponseBody error = response.errorBody()) { - if (error != null) { - return error.string(); - } - } - return ""; - } - - /** - * Checks that the number of tests returned by Teamscale matches the number of available tests when running with - * {@link #includeNonImpacted}. - */ - private boolean testCountIsPlausible(List testClusters, - List availableTestDetails) { - long returnedTests = testClusters.stream().mapToLong(g -> g.tests.size()).sum(); - if (!this.includeNonImpacted) { - LOGGER.info( - () -> "Received " + returnedTests + " impacted tests of " + availableTestDetails.size() + " available tests."); - return true; - } - if (returnedTests == availableTestDetails.size()) { - return true; - } else { - LOGGER.severe( - () -> "Retrieved " + returnedTests + " tests from Teamscale, but expected " + availableTestDetails - .size() + "."); - return false; - } - } -} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt new file mode 100644 index 000000000..087b8c151 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt @@ -0,0 +1,105 @@ +package com.teamscale.test_impacted.engine.executor + +import com.teamscale.client.ClusteredTestDetails +import com.teamscale.client.CommitDescriptor +import com.teamscale.client.PrioritizableTestCluster +import com.teamscale.client.TeamscaleClient +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger +import retrofit2.Response +import java.io.IOException +import java.util.logging.Level + +/** + * Class for retrieving the impacted [PrioritizableTestCluster]s corresponding to [ClusteredTestDetails] + * available for test execution. + */ +open class ImpactedTestsProvider( + private val client: TeamscaleClient, + private val baseline: String, + private val baselineRevision: String, + private val endCommit: CommitDescriptor, + private val endRevision: String, + private val repository: String, + @JvmField val partition: String, + private val includeNonImpacted: Boolean, + private val includeAddedTests: Boolean, + private val includeFailedAndSkipped: Boolean +) { + private val logger = createLogger() + + /** Queries Teamscale for impacted tests. */ + fun getImpactedTestsFromTeamscale( + availableTestDetails: List + ): List { + try { + logger.info { "Getting impacted tests..." } + val response = client + .getImpactedTests( + availableTestDetails, baseline, baselineRevision, endCommit, endRevision, repository, + listOf(partition), + includeNonImpacted, includeAddedTests, includeFailedAndSkipped + ) + + if (response.isSuccessful) { + val testClusters = response.body() + if (testClusters != null && testCountIsPlausible(testClusters, availableTestDetails)) { + return testClusters + } + logger.severe( + """ + Teamscale was not able to determine impacted tests: + ${response.body()} + """.trimIndent() + ) + } else { + logger.severe( + "Retrieval of impacted tests failed: ${response.code()} ${response.message()}\n${ + getErrorBody(response) + }" + ) + } + } catch (e: IOException) { + logger.log( + Level.SEVERE, e + ) { "Retrieval of impacted tests failed." } + } + return emptyList() + } + + /** + * Checks that the number of tests returned by Teamscale matches the number of available tests when running with + * [.includeNonImpacted]. + */ + private fun testCountIsPlausible( + testClusters: List, + availableTestDetails: List + ): Boolean { + val returnedTests = testClusters.stream().mapToLong { + it.tests!!.size.toLong() + }.sum() + if (!includeNonImpacted) { + logger.info { "Received $returnedTests impacted tests of ${availableTestDetails.size} available tests." } + return true + } + if (returnedTests == availableTestDetails.size.toLong()) { + return true + } else { + logger.severe { + "Retrieved $returnedTests tests from Teamscale, but expected ${availableTestDetails.size}." + } + return false + } + } + + companion object { + @Throws(IOException::class) + private fun getErrorBody(response: Response<*>): String { + response.errorBody().use { error -> + if (error != null) { + return error.string() + } + } + return "" + } + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt index 37c45f861..5bf81ac47 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt @@ -106,7 +106,7 @@ class TestEngineOptions { File(reportDirectory, "server-request.txt") ) return ImpactedTestsProvider( - client, baseline, baselineRevision, endCommit, endRevision, repository, partition, + client, baseline!!, baselineRevision!!, endCommit!!, endRevision!!, repository!!, partition!!, runAllTests, includeAddedTests, includeFailedAndSkipped ) } diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt index 09252463f..b46babe4b 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt @@ -7,9 +7,8 @@ import com.teamscale.test_impacted.engine.executor.TeamscaleAgentNotifier import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test import org.junit.platform.engine.* -import org.mockito.ArgumentMatchers -import org.mockito.Mockito import org.mockito.Mockito.mock +import org.mockito.kotlin.* /** Base class for testing specific scenarios in the impacted test engine. */ abstract class ImpactedTestEngineTestBase { @@ -30,11 +29,11 @@ abstract class ImpactedTestEngineTestBase { Assertions.assertThat(engineDescriptor.uniqueId) .isEqualTo(UniqueId.forEngine(ImpactedTestEngine.ENGINE_ID)) - Mockito.`when`(executionRequest.engineExecutionListener) + whenever(executionRequest.engineExecutionListener) .thenReturn(executionListener) - Mockito.`when`(executionRequest.rootTestDescriptor) + whenever(executionRequest.rootTestDescriptor) .thenReturn(engineDescriptor) - Mockito.`when`(impactedTestsProvider.getImpactedTestsFromTeamscale(ArgumentMatchers.any())) + whenever(impactedTestsProvider.getImpactedTestsFromTeamscale(any())) .thenReturn(impactedTests) testEngine.execute(executionRequest) @@ -42,11 +41,11 @@ abstract class ImpactedTestEngineTestBase { verifyCallbacks(executionListener) // Ensure test data is written. - Mockito.verify(testDataWriter).dumpTestDetails(ArgumentMatchers.any()) - Mockito.verify(testDataWriter).dumpTestExecutions(ArgumentMatchers.any()) + verify(testDataWriter).dumpTestDetails(any()) + verify(testDataWriter).dumpTestExecutions(any()) - Mockito.verifyNoMoreInteractions(executionListener) - Mockito.verifyNoMoreInteractions(testDataWriter) + verifyNoMoreInteractions(executionListener) + verifyNoMoreInteractions(testDataWriter) } /** Returns the available engines that should be assumed by the impacted test engine. */ @@ -60,10 +59,10 @@ abstract class ImpactedTestEngineTestBase { private fun createInternalImpactedTestEngine(engines: List): InternalImpactedTestEngine { engines.forEach { engine -> - Mockito.`when`(testEngineRegistry.getTestEngine(ArgumentMatchers.eq(engine.id))) + whenever(testEngineRegistry.getTestEngine(eq(engine.id))) .thenReturn(engine) } - Mockito.`when`>(testEngineRegistry.iterator()).thenReturn(engines.iterator()) + whenever>(testEngineRegistry.iterator()).thenReturn(engines.iterator()) return InternalImpactedTestEngine( ImpactedTestEngineConfiguration( diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt index bf086de98..36720890a 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithDynamicTestsTest.kt @@ -6,7 +6,7 @@ import com.teamscale.test_impacted.engine.executor.DummyEngine import com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver import org.junit.platform.engine.* -import org.mockito.Mockito +import org.mockito.kotlin.verify /** Test setup for JUnit Jupiter dynamic tests. */ internal class ImpactedTestEngineWithDynamicTestsTest : ImpactedTestEngineTestBase() { @@ -47,20 +47,20 @@ internal class ImpactedTestEngineWithDynamicTestsTest : ImpactedTestEngineTestBa override fun verifyCallbacks(executionListener: EngineExecutionListener) { // First the parents test descriptors are started in order. - Mockito.verify(executionListener).executionStarted(testRoot) - Mockito.verify(executionListener).executionStarted(dynamicTestClassCase) - Mockito.verify(executionListener).executionStarted(dynamicTestCase) + verify(executionListener).executionStarted(testRoot) + verify(executionListener).executionStarted(dynamicTestClassCase) + verify(executionListener).executionStarted(dynamicTestCase) // Test case is added dynamically and executed. dynamicTestCase.addChild(dynamicallyRegisteredTestCase) - Mockito.verify(executionListener).dynamicTestRegistered(dynamicallyRegisteredTestCase) - Mockito.verify(executionListener).executionStarted(dynamicallyRegisteredTestCase) - Mockito.verify(executionListener) + verify(executionListener).dynamicTestRegistered(dynamicallyRegisteredTestCase) + verify(executionListener).executionStarted(dynamicallyRegisteredTestCase) + verify(executionListener) .executionFinished(dynamicallyRegisteredTestCase, TestExecutionResult.successful()) // Parent test descriptors are also finished. - Mockito.verify(executionListener).executionFinished(dynamicTestCase, TestExecutionResult.successful()) - Mockito.verify(executionListener).executionFinished(dynamicTestClassCase, TestExecutionResult.successful()) - Mockito.verify(executionListener).executionFinished(testRoot, TestExecutionResult.successful()) + verify(executionListener).executionFinished(dynamicTestCase, TestExecutionResult.successful()) + verify(executionListener).executionFinished(dynamicTestClassCase, TestExecutionResult.successful()) + verify(executionListener).executionFinished(testRoot, TestExecutionResult.successful()) } } diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt index a527cab2f..1d80a30c5 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineWithTwoEnginesTest.kt @@ -6,8 +6,9 @@ import com.teamscale.test_impacted.engine.executor.DummyEngine import com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver import org.junit.platform.engine.* -import org.mockito.ArgumentMatchers -import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify import java.util.* /** Test setup for a mixture of impacted and no impacted tests and two test engines. */ @@ -68,16 +69,12 @@ internal class ImpactedTestEngineWithTwoEnginesTest : ImpactedTestEngineTestBase private val impactedTestCase1 = SimpleTestDescriptor.testCase(impactedTestCase1Id) private val nonImpactedTestCase1 = SimpleTestDescriptor.testCase(nonImpactedTestCase1Id) - private val firstTestClass = SimpleTestDescriptor.testContainer( - firstTestClassId, - impactedTestCase1, nonImpactedTestCase1 - ) + private val firstTestClass = SimpleTestDescriptor.testContainer(firstTestClassId, impactedTestCase1, nonImpactedTestCase1) private val impactedTestCase2 = SimpleTestDescriptor.testCase(impactedTestCase2Id) private val nonImpactedTestCase2 = SimpleTestDescriptor.testCase(nonImpactedTestCase2Id) private val ignoredTestClass = SimpleTestDescriptor.testContainer( - ignoredTestClassId, - impactedTestCase2, nonImpactedTestCase2 + ignoredTestClassId, impactedTestCase2, nonImpactedTestCase2 ).skip() private val failed = TestExecutionResult.failed(NullPointerException()) @@ -86,7 +83,8 @@ internal class ImpactedTestEngineWithTwoEnginesTest : ImpactedTestEngineTestBase SimpleTestDescriptor.testCase(skippedImpactedTestCaseId).skip() private val secondTestClass = SimpleTestDescriptor.testContainer( secondTestClassId, - impactedTestCase3, skippedImpactedTestCase + impactedTestCase3, + skippedImpactedTestCase ) private val impactedTestCase4 = SimpleTestDescriptor.testCase(impactedTestCase4Id) @@ -129,46 +127,43 @@ internal class ImpactedTestEngineWithTwoEnginesTest : ImpactedTestEngineTestBase override fun verifyCallbacks(executionListener: EngineExecutionListener) { // Start of engine 1 - Mockito.verify(executionListener).executionStarted(testEngine1Root) + verify(executionListener).executionStarted(testEngine1Root) // Execute FirstTestClass. - Mockito.verify(executionListener).executionStarted(firstTestClass) - Mockito.verify(executionListener).executionStarted(impactedTestCase1) - Mockito.verify(executionListener) - .executionFinished(ArgumentMatchers.eq(impactedTestCase1), ArgumentMatchers.any()) - Mockito.verify(executionListener).executionFinished(ArgumentMatchers.eq(firstTestClass), ArgumentMatchers.any()) + verify(executionListener).executionStarted(firstTestClass) + verify(executionListener).executionStarted(impactedTestCase1) + verify(executionListener) + .executionFinished(eq(impactedTestCase1), any()) + verify(executionListener).executionFinished(eq(firstTestClass), any()) // Execute IgnoredTestClass. - Mockito.verify(executionListener).executionStarted(ignoredTestClass) - Mockito.verify(executionListener) - .executionSkipped(ArgumentMatchers.eq(impactedTestCase2), ArgumentMatchers.any()) - Mockito.verify(executionListener).executionFinished(ignoredTestClass, TestExecutionResult.successful()) + verify(executionListener).executionStarted(ignoredTestClass) + verify(executionListener) + .executionSkipped(eq(impactedTestCase2), any()) + verify(executionListener).executionFinished(ignoredTestClass, TestExecutionResult.successful()) // Execute SecondTestClass. - Mockito.verify(executionListener).executionStarted(secondTestClass) - Mockito.verify(executionListener).executionStarted(ArgumentMatchers.eq(impactedTestCase3)) - Mockito.verify(executionListener).executionFinished( - impactedTestCase3, - failed - ) - Mockito.verify(executionListener) - .executionSkipped(ArgumentMatchers.eq(skippedImpactedTestCase), ArgumentMatchers.any()) - Mockito.verify(executionListener).executionFinished(secondTestClass, TestExecutionResult.successful()) + verify(executionListener).executionStarted(secondTestClass) + verify(executionListener).executionStarted(eq(impactedTestCase3)) + verify(executionListener).executionFinished(impactedTestCase3, failed) + verify(executionListener) + .executionSkipped(eq(skippedImpactedTestCase), any()) + verify(executionListener).executionFinished(secondTestClass, TestExecutionResult.successful()) // Finish test engine 1 - Mockito.verify(executionListener).executionFinished(testEngine1Root, TestExecutionResult.successful()) + verify(executionListener).executionFinished(testEngine1Root, TestExecutionResult.successful()) // Start of engine 2 - Mockito.verify(executionListener).executionStarted(testEngine2Root) + verify(executionListener).executionStarted(testEngine2Root) // Execute OtherTestClass. - Mockito.verify(executionListener).executionStarted(otherTestClass) - Mockito.verify(executionListener).executionStarted(impactedTestCase4) - Mockito.verify(executionListener).executionFinished(impactedTestCase4, TestExecutionResult.successful()) - Mockito.verify(executionListener).executionFinished(otherTestClass, TestExecutionResult.successful()) + verify(executionListener).executionStarted(otherTestClass) + verify(executionListener).executionStarted(impactedTestCase4) + verify(executionListener).executionFinished(impactedTestCase4, TestExecutionResult.successful()) + verify(executionListener).executionFinished(otherTestClass, TestExecutionResult.successful()) // Finish test engine 2 - Mockito.verify(executionListener).executionFinished(testEngine2Root, TestExecutionResult.successful()) + verify(executionListener).executionFinished(testEngine2Root, TestExecutionResult.successful()) } companion object { diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt index 7e5bbd385..10e4b5897 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt @@ -5,8 +5,6 @@ import com.teamscale.test_impacted.engine.executor.DummyEngine import com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver import org.junit.platform.engine.EngineExecutionListener -import org.junit.platform.engine.TestDescriptor -import org.junit.platform.engine.TestEngine import org.junit.platform.engine.UniqueId /** Test setup where no test is impacted. */ diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.kt index 37a10d12d..a31da433e 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/SimpleTestDescriptor.kt @@ -4,7 +4,6 @@ import org.junit.platform.engine.TestDescriptor import org.junit.platform.engine.TestExecutionResult import org.junit.platform.engine.UniqueId import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor -import java.util.* /** A basic implementation of a [TestDescriptor] that can be used during tests. */ class SimpleTestDescriptor private constructor( diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt index 58e74ccd8..75c884498 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.kt @@ -13,11 +13,8 @@ import java.util.* /** Tests for [TestwiseCoverageCollectingExecutionListener]. */ internal class TestwiseCoverageCollectingExecutionListenerTest { - private val mockApi = mock() - private val resolver = mock() - private val executionListenerMock = mock() private val executionListener = TestwiseCoverageCollectingExecutionListener( From 68181f56e7e8db16e7f25bc7be47b3b58a4ba794 Mon Sep 17 00:00:00 2001 From: Constructor Date: Fri, 13 Dec 2024 03:50:25 +0100 Subject: [PATCH 17/21] TS-38628 Migrate ImpactedTestEngine class --- .../engine/ImpactedTestEngine.java | 62 --------- .../ImpactedTestEngineConfiguration.java | 31 ----- .../engine/InternalImpactedTestEngine.java | 123 ------------------ .../engine/ImpactedTestEngine.kt | 50 +++++++ .../engine/ImpactedTestEngineConfiguration.kt | 16 +++ .../engine/InternalImpactedTestEngine.kt | 115 ++++++++++++++++ .../engine/options/TestEngineOptions.kt | 20 +-- .../multiple-agents-test/build.gradle.kts | 2 +- 8 files changed, 192 insertions(+), 227 deletions(-) delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/ImpactedTestEngine.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/ImpactedTestEngineConfiguration.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.java create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngine.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineConfiguration.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/ImpactedTestEngine.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/ImpactedTestEngine.java deleted file mode 100644 index d58aeebaa..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/ImpactedTestEngine.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.teamscale.test_impacted.engine; - -import com.teamscale.test_impacted.commons.LoggerUtils; -import com.teamscale.test_impacted.engine.options.TestEngineOptionUtils; -import com.teamscale.test_impacted.engine.options.TestEngineOptions; -import org.junit.platform.engine.EngineDiscoveryRequest; -import org.junit.platform.engine.ExecutionRequest; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.UniqueId; - -import java.util.Optional; -import java.util.logging.Logger; - -/** Test engine for executing impacted tests. */ -public class ImpactedTestEngine implements TestEngine { - - /** The id of the {@link ImpactedTestEngine}. */ - public static final String ENGINE_ID = "teamscale-test-impacted"; - - public static final Logger LOGGER = LoggerUtils.getLogger(ImpactedTestEngine.class); - - private InternalImpactedTestEngine internalImpactedTestEngine = null; - - @Override - public String getId() { - return ENGINE_ID; - } - - @Override - public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { - TestEngineOptions engineOptions = TestEngineOptionUtils - .getEngineOptions(discoveryRequest.getConfigurationParameters()); - ImpactedTestEngineConfiguration configuration = engineOptions.createTestEngineConfiguration(); - - // Re-initialize the configuration for this discovery (and optional following execution). - internalImpactedTestEngine = - new InternalImpactedTestEngine(configuration, engineOptions.getPartition()); - - return internalImpactedTestEngine.discover(discoveryRequest, uniqueId); - } - - @Override - public void execute(ExecutionRequest request) { - // According to the TestEngine interface the request must correspond to the last execution request. Therefore, we - // may re-use the configuration initialized during discovery. - if (internalImpactedTestEngine == null) { - throw new AssertionError("Can't execute request without discovering it first."); - } - internalImpactedTestEngine.execute(request); - } - - @Override - public Optional getGroupId() { - return Optional.of("com.teamscale"); - } - - @Override - public Optional getArtifactId() { - return Optional.of("impacted-test-engine"); - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/ImpactedTestEngineConfiguration.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/ImpactedTestEngineConfiguration.java deleted file mode 100644 index 61172a3ea..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/ImpactedTestEngineConfiguration.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.teamscale.test_impacted.engine; - -import com.teamscale.test_impacted.engine.executor.ITestSorter; -import com.teamscale.test_impacted.engine.executor.TeamscaleAgentNotifier; -import org.junit.platform.engine.TestEngine; - -/** Container for a configuration used by the {@link ImpactedTestEngine} */ -public class ImpactedTestEngineConfiguration { - - /** The directory to write testwise coverage and available tests to. */ - final TestDataWriter testDataWriter; - - /** The test engine registry used to determine the {@link TestEngine}s to use. */ - final TestEngineRegistry testEngineRegistry; - - /** The {@link ITestSorter} to use for execution of tests. */ - final ITestSorter testSorter; - - /** An API to signal test start and end to the agent. */ - final TeamscaleAgentNotifier teamscaleAgentNotifier; - - public ImpactedTestEngineConfiguration( - TestDataWriter testDataWriter, - TestEngineRegistry testEngineRegistry, - ITestSorter testSorter, TeamscaleAgentNotifier teamscaleAgentNotifier ) { - this.testDataWriter = testDataWriter; - this.testEngineRegistry = testEngineRegistry; - this.testSorter = testSorter; - this.teamscaleAgentNotifier = teamscaleAgentNotifier; - } -} \ No newline at end of file diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.java deleted file mode 100644 index cae52d9ed..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.teamscale.test_impacted.engine; - -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.test_impacted.commons.LoggerUtils; -import com.teamscale.test_impacted.engine.executor.AvailableTests; -import com.teamscale.test_impacted.engine.executor.ITestSorter; -import com.teamscale.test_impacted.engine.executor.TeamscaleAgentNotifier; -import com.teamscale.test_impacted.engine.executor.TestwiseCoverageCollectingExecutionListener; -import com.teamscale.test_impacted.test_descriptor.ITestDescriptorResolver; -import com.teamscale.test_impacted.test_descriptor.TestDescriptorResolverRegistry; -import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils; -import org.junit.platform.engine.EngineDiscoveryRequest; -import org.junit.platform.engine.ExecutionRequest; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.descriptor.EngineDescriptor; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.logging.Logger; - -import static com.teamscale.test_impacted.engine.ImpactedTestEngine.ENGINE_ID; - -/** - * Test engine called internally to allow testing without needing a {@link ServiceLoader} for {@link TestEngine} setup. - */ -class InternalImpactedTestEngine { - - private static final Logger LOGGER = LoggerUtils.getLogger(InternalImpactedTestEngine.class); - - private final TestEngineRegistry testEngineRegistry; - - private final ITestSorter testSorter; - - private final TeamscaleAgentNotifier teamscaleAgentNotifier; - - private final TestDataWriter testDataWriter; - - private final String partition; - - InternalImpactedTestEngine(ImpactedTestEngineConfiguration configuration, String partition) { - this.testEngineRegistry = configuration.testEngineRegistry; - this.testSorter = configuration.testSorter; - this.testDataWriter = configuration.testDataWriter; - this.teamscaleAgentNotifier = configuration.teamscaleAgentNotifier; - this.partition = partition; - } - - /** - * Performs test discovery by aggregating the result of all {@link TestEngine}s from the {@link TestEngineRegistry} - * in a single engine {@link TestDescriptor}. - */ - TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { - EngineDescriptor engineDescriptor = new EngineDescriptor(uniqueId, "Teamscale Impacted Tests"); - - LOGGER.fine(() -> "Starting test discovery for engine " + ENGINE_ID); - - for (TestEngine delegateTestEngine : testEngineRegistry) { - LOGGER.fine(() -> "Starting test discovery for delegate engine: " + delegateTestEngine.getId()); - TestDescriptor delegateEngineDescriptor = delegateTestEngine.discover(discoveryRequest, - UniqueId.forEngine(delegateTestEngine.getId())); - - engineDescriptor.addChild(delegateEngineDescriptor); - } - - LOGGER.fine(() -> "Discovered test descriptor for engine " + ENGINE_ID + ":\n" + TestDescriptorUtils - .getTestDescriptorAsString(engineDescriptor)); - - return engineDescriptor; - } - - /** - * Executes the request by requesting execution of the {@link TestDescriptor} children aggregated in - * {@link #discover(EngineDiscoveryRequest, UniqueId)} with the corresponding {@link TestEngine}. - */ - void execute(ExecutionRequest request) { - TestDescriptor rootTestDescriptor = request.getRootTestDescriptor(); - AvailableTests availableTests = TestDescriptorUtils.getAvailableTests(rootTestDescriptor, partition); - - LOGGER.fine(() -> "Starting selection and sorting " + ENGINE_ID + ":\n" + TestDescriptorUtils - .getTestDescriptorAsString(rootTestDescriptor)); - - testSorter.selectAndSort(rootTestDescriptor); - - LOGGER.fine(() -> "Starting execution of request for engine " + ENGINE_ID + ":\n" + TestDescriptorUtils - .getTestDescriptorAsString(rootTestDescriptor)); - - List testExecutions = executeTests(request, rootTestDescriptor); - - testDataWriter.dumpTestExecutions(testExecutions); - testDataWriter.dumpTestDetails(availableTests.getTestList()); - teamscaleAgentNotifier.testRunEnded(); - } - - private List executeTests(ExecutionRequest request, TestDescriptor rootTestDescriptor) { - List testExecutions = new ArrayList<>(); - for (TestDescriptor engineTestDescriptor : rootTestDescriptor.getChildren()) { - Optional engineId = engineTestDescriptor.getUniqueId().getEngineId(); - if (!engineId.isPresent()) { - LOGGER.severe( - () -> "Engine ID for test descriptor " + engineTestDescriptor + " not present. Skipping execution of the engine."); - continue; - } - - TestEngine testEngine = testEngineRegistry.getTestEngine(engineId.get()); - ITestDescriptorResolver testDescriptorResolver = TestDescriptorResolverRegistry - .getTestDescriptorResolver(testEngine.getId()); - TestwiseCoverageCollectingExecutionListener executionListener = - new TestwiseCoverageCollectingExecutionListener(teamscaleAgentNotifier, - testDescriptorResolver, - request.getEngineExecutionListener()); - - testEngine.execute(new ExecutionRequest(engineTestDescriptor, executionListener, - request.getConfigurationParameters())); - - testExecutions.addAll(executionListener.getTestExecutions()); - } - return testExecutions; - } -} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngine.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngine.kt new file mode 100644 index 000000000..33a086e8a --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngine.kt @@ -0,0 +1,50 @@ +package com.teamscale.test_impacted.engine + +import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import com.teamscale.test_impacted.engine.options.TestEngineOptionUtils +import org.junit.platform.engine.* +import java.util.* +import java.util.logging.Logger + +/** Test engine for executing impacted tests. */ +class ImpactedTestEngine : TestEngine { + private var internalImpactedTestEngine: InternalImpactedTestEngine? = null + + override fun getId() = ENGINE_ID + + override fun discover( + discoveryRequest: EngineDiscoveryRequest, + uniqueId: UniqueId + ): TestDescriptor { + val engineOptions = TestEngineOptionUtils + .getEngineOptions(discoveryRequest.configurationParameters) + val configuration = engineOptions.createTestEngineConfiguration() + val engine = InternalImpactedTestEngine(configuration, engineOptions.partition!!) + + // Re-initialize the configuration for this discovery (and optional following execution). + internalImpactedTestEngine = engine + + return engine.discover(discoveryRequest, uniqueId) + } + + override fun execute(request: ExecutionRequest) { + // According to the TestEngine interface the request must correspond to the last execution request. Therefore, we + // may re-use the configuration initialized during discovery. + check(internalImpactedTestEngine != null) { + "Can't execute request without discovering it first." + } + internalImpactedTestEngine?.execute(request) + } + + override fun getGroupId() = Optional.of("com.teamscale") + + override fun getArtifactId() = Optional.of("impacted-test-engine") + + companion object { + /** The id of the [ImpactedTestEngine]. */ + const val ENGINE_ID = "teamscale-test-impacted" + + @JvmField + val LOGGER: Logger = getLogger(ImpactedTestEngine::class.java) + } +} \ No newline at end of file diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineConfiguration.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineConfiguration.kt new file mode 100644 index 000000000..585b6d686 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineConfiguration.kt @@ -0,0 +1,16 @@ +package com.teamscale.test_impacted.engine + +import com.teamscale.test_impacted.engine.executor.ITestSorter +import com.teamscale.test_impacted.engine.executor.TeamscaleAgentNotifier + +/** Container for a configuration used by the [ImpactedTestEngine] */ +class ImpactedTestEngineConfiguration( + /** The directory to write testwise coverage and available tests to. */ + val testDataWriter: TestDataWriter, + /** The test engine registry used to determine the [TestEngine]s to use. */ + val testEngineRegistry: TestEngineRegistry, + /** The [ITestSorter] to use for execution of tests. */ + val testSorter: ITestSorter, + /** An API to signal test start and end to the agent. */ + val teamscaleAgentNotifier: TeamscaleAgentNotifier +) \ No newline at end of file diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt new file mode 100644 index 000000000..e06e16bc3 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt @@ -0,0 +1,115 @@ +package com.teamscale.test_impacted.engine + +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger +import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import com.teamscale.test_impacted.engine.executor.TestwiseCoverageCollectingExecutionListener +import com.teamscale.test_impacted.test_descriptor.TestDescriptorResolverRegistry.getTestDescriptorResolver +import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getAvailableTests +import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getTestDescriptorAsString +import org.junit.platform.engine.EngineDiscoveryRequest +import org.junit.platform.engine.ExecutionRequest +import org.junit.platform.engine.TestDescriptor +import org.junit.platform.engine.UniqueId +import org.junit.platform.engine.support.descriptor.EngineDescriptor +import java.util.logging.Logger + +/** + * Test engine called internally to allow testing without needing a [ServiceLoader] for [TestEngine] setup. + */ +internal class InternalImpactedTestEngine( + configuration: ImpactedTestEngineConfiguration, + private val partition: String +) { + private val testEngineRegistry = configuration.testEngineRegistry + private val testSorter = configuration.testSorter + private val teamscaleAgentNotifier = configuration.teamscaleAgentNotifier + private val testDataWriter = configuration.testDataWriter + + /** + * Performs test discovery by aggregating the result of all [TestEngine]s from the [TestEngineRegistry] + * in a single engine [TestDescriptor]. + */ + fun discover(discoveryRequest: EngineDiscoveryRequest?, uniqueId: UniqueId?): TestDescriptor { + val engineDescriptor = EngineDescriptor(uniqueId, "Teamscale Impacted Tests") + + LOGGER.fine { "Starting test discovery for engine " + ImpactedTestEngine.ENGINE_ID } + + testEngineRegistry.forEach { delegateTestEngine -> + LOGGER.fine { "Starting test discovery for delegate engine: " + delegateTestEngine.id } + val delegateEngineDescriptor = delegateTestEngine.discover( + discoveryRequest, + UniqueId.forEngine(delegateTestEngine.id) + ) + + engineDescriptor.addChild(delegateEngineDescriptor) + } + + LOGGER.fine { + "Discovered test descriptor for engine ${ImpactedTestEngine.ENGINE_ID}:\n${ + getTestDescriptorAsString(engineDescriptor) + }" + } + + return engineDescriptor + } + + /** + * Executes the request by requesting execution of the [TestDescriptor] children aggregated in + * [.discover] with the corresponding [org.junit.platform.engine.TestEngine]. + */ + fun execute(request: ExecutionRequest) { + val rootTestDescriptor = request.rootTestDescriptor + val availableTests = getAvailableTests(rootTestDescriptor, partition) + + LOGGER.fine { + "Starting selection and sorting ${ImpactedTestEngine.ENGINE_ID}:\n${ + getTestDescriptorAsString(rootTestDescriptor) + }" + } + + testSorter.selectAndSort(rootTestDescriptor) + + LOGGER.fine { + "Starting execution of request for engine ${ImpactedTestEngine.ENGINE_ID}:\n${ + getTestDescriptorAsString(rootTestDescriptor) + }" + } + + val testExecutions = executeTests(request, rootTestDescriptor) + + testDataWriter.dumpTestExecutions(testExecutions) + testDataWriter.dumpTestDetails(availableTests.testList) + teamscaleAgentNotifier.testRunEnded() + } + + private fun executeTests(request: ExecutionRequest, rootTestDescriptor: TestDescriptor) = + rootTestDescriptor.children.flatMap { engineTestDescriptor -> + val engineId = engineTestDescriptor.uniqueId.engineId + if (!engineId.isPresent) { + LOGGER.severe { "Engine ID for test descriptor $engineTestDescriptor not present. Skipping execution of the engine." } + return@flatMap emptyList() + } + + val testEngine = testEngineRegistry.getTestEngine(engineId.get()) ?: return@flatMap emptyList() + val testDescriptorResolver = getTestDescriptorResolver(testEngine.id) ?: return@flatMap emptyList() + val executionListener = + TestwiseCoverageCollectingExecutionListener( + teamscaleAgentNotifier, + testDescriptorResolver, + request.engineExecutionListener + ) + + testEngine.execute( + ExecutionRequest( + engineTestDescriptor, executionListener, + request.configurationParameters + ) + ) + + executionListener.testExecutions + } + + companion object { + private val LOGGER: Logger = createLogger() + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt index 5bf81ac47..ceffec536 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt @@ -17,28 +17,28 @@ class TestEngineOptions { /** The server options. May not be null. */ private var serverOptions: ServerOptions? = null - /** @see .partition + /** The partition to upload test details to and get impacted tests from. If null, all partitions are used. + * @see [partition] */ - /** The partition to upload test details to and get impacted tests from. If null all partitions are used. */ var partition: String? = null private set - /** @see .runAllTests + /** Executes all tests, not only impacted ones if set. Defaults to false. + * @see [runAllTests] */ - /** Executes all tests, not only impacted ones if set. Defaults to false. */ private var runAllTests = false /** Executes only impacted tests, not all ones if set. Defaults to true. */ private var runImpacted = true - /** @see .includeAddedTests + /** Includes added tests in the list of tests to execute. Defaults to true + * @see [includeAddedTests] */ - /** Includes added tests in the list of tests to execute. Defaults to true */ private var includeAddedTests = true - /** @see .includeFailedAndSkipped + /** Includes failed and skipped tests in the list of tests to execute. Defaults to true + * @see [includeFailedAndSkipped] */ - /** Includes failed and skipped tests in the list of tests to execute. Defaults to true */ private var includeFailedAndSkipped = true /** @@ -48,7 +48,7 @@ class TestEngineOptions { private var baseline: String? = null /** - * Can be used instead of [.baseline] by using a revision (e.g. git SHA1) instead of a branch and timestamp. + * Can be used instead of [baseline] by using a revision (e.g. git SHA1) instead of a branch and timestamp. */ private var baselineRevision: String? = null @@ -56,7 +56,7 @@ class TestEngineOptions { private var endCommit: CommitDescriptor? = null /** - * Can be used instead of [.endCommit] by using a revision (e.g. git SHA1) instead of a branch and timestamp. + * Can be used instead of [endCommit] by using a revision (e.g. git SHA1) instead of a branch and timestamp. */ private var endRevision: String? = null diff --git a/system-tests/multiple-agents-test/build.gradle.kts b/system-tests/multiple-agents-test/build.gradle.kts index e81005972..4cd625239 100644 --- a/system-tests/multiple-agents-test/build.gradle.kts +++ b/system-tests/multiple-agents-test/build.gradle.kts @@ -2,7 +2,7 @@ plugins { com.teamscale.`system-test-convention` } -val jacocoAgent by configurations.creating +val jacocoAgent: Configuration by configurations.creating dependencies { // This version should differ from the version we currently use for the Teamscale JaCoCo agent itself From 12d430892f84c7c4df1f2f10781e8a304462c558 Mon Sep 17 00:00:00 2001 From: Constructor Date: Fri, 13 Dec 2024 04:57:41 +0100 Subject: [PATCH 18/21] TS-38628 Registry and reworked builder pattern --- .../test_impacted/engine/TestDataWriter.java | 45 ---- .../engine/TestEngineRegistry.java | 65 ----- .../engine/options/TestEngineOptionUtils.java | 91 ------- .../engine/InternalImpactedTestEngine.kt | 1 - .../test_impacted/engine/TestDataWriter.kt | 39 +++ .../engine/TestEngineRegistry.kt | 45 ++++ .../engine/executor/ImpactedTestsProvider.kt | 7 +- .../engine/executor/ImpactedTestsSorter.kt | 2 +- .../engine/options/TestEngineOptionUtils.kt | 67 +++++ .../engine/options/TestEngineOptions.kt | 235 +++++------------- .../ITestDescriptorResolver.kt | 4 +- .../engine/ImpactedTestEngineTestBase.kt | 2 +- 12 files changed, 227 insertions(+), 376 deletions(-) delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/TestDataWriter.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/TestEngineRegistry.java delete mode 100644 impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.java create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/TestDataWriter.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/TestEngineRegistry.kt create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.kt diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/TestDataWriter.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/TestDataWriter.java deleted file mode 100644 index ca2b076f1..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/TestDataWriter.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.teamscale.test_impacted.engine; - -import com.teamscale.client.TestDetails; -import com.teamscale.report.ReportUtils; -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.test_impacted.commons.LoggerUtils; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** Class for writing test data to a report directory. */ -public class TestDataWriter { - - private static final Logger LOGGER = LoggerUtils.getLogger(TestDataWriter.class); - - private final File reportDirectory; - - public TestDataWriter(File reportDirectory) { - this.reportDirectory = reportDirectory; - } - - /** Writes the given test executions to a report file. */ - void dumpTestExecutions(List testExecutions) { - File file = new File(reportDirectory, "test-execution.json"); - try { - ReportUtils.writeTestExecutionReport(file, testExecutions); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, e, () -> "Error while writing report to file: " + file); - } - } - - /** Writes the given test details to a report file. */ - void dumpTestDetails(List testDetails) { - File file = new File(reportDirectory, "test-list.json"); - try { - ReportUtils.writeTestListReport(file, new ArrayList<>(testDetails)); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, e, () -> "Error while writing report to file: " + file); - } - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/TestEngineRegistry.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/TestEngineRegistry.java deleted file mode 100644 index 3f4a25ff2..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/TestEngineRegistry.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.teamscale.test_impacted.engine; - -import org.junit.platform.commons.util.ClassLoaderUtils; -import org.junit.platform.engine.TestEngine; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.stream.Collectors; - -import static java.util.Collections.unmodifiableMap; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; - -/** The test engine registry containing all */ -public class TestEngineRegistry implements Iterable { - - private final Map testEnginesById; - - public TestEngineRegistry(Set includedTestEngineIds, Set excludedTestEngineIds) { - List otherTestEngines = loadOtherTestEngines(excludedTestEngineIds); - - // If there are no test engines set we don't need to filter but simply use all other test engines. - if (!includedTestEngineIds.isEmpty()) { - otherTestEngines = otherTestEngines.stream() - .filter(testEngine -> includedTestEngineIds.contains(testEngine.getId())).collect( - Collectors.toList()); - } - - testEnginesById = unmodifiableMap(otherTestEngines.stream().collect(toMap(TestEngine::getId, identity()))); - } - - /** - * Uses the {@link ServiceLoader} to discover all {@link TestEngine}s but the {@link ImpactedTestEngine} and the - * excluded test engines. - */ - private List loadOtherTestEngines(Set excludedTestEngineIds) { - List testEngines = new ArrayList<>(); - - for (TestEngine testEngine : ServiceLoader.load(TestEngine.class, ClassLoaderUtils.getDefaultClassLoader())) { - if (!ImpactedTestEngine.ENGINE_ID.equals(testEngine.getId()) && !excludedTestEngineIds.contains( - testEngine.getId())) { - testEngines.add(testEngine); - } - } - - return testEngines; - } - - /** Returns the {@link TestEngine} for the engine id or null if none is present. */ - public TestEngine getTestEngine(String engineId) { - return testEnginesById.get(engineId); - } - - @Override - public Iterator iterator() { - List testEngines = new ArrayList<>(testEnginesById.values()); - testEngines.sort(Comparator.comparing(TestEngine::getId)); - return testEngines.iterator(); - } -} diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.java deleted file mode 100644 index 7411b3516..000000000 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.teamscale.test_impacted.engine.options; - -import com.teamscale.client.CommitDescriptor; -import com.teamscale.client.StringUtils; -import org.junit.platform.engine.ConfigurationParameters; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Properties; -import java.util.function.Function; - -/** Utility class for {@link TestEngineOptions}. */ -public class TestEngineOptionUtils { - - /** Returns the {@link TestEngineOptions} configured in the {@link Properties}. */ - public static TestEngineOptions getEngineOptions(ConfigurationParameters configurationParameters) { - PrefixingPropertyReader propertyReader = new PrefixingPropertyReader("teamscale.test.impacted.", - configurationParameters); - ServerOptions serverOptions = null; - Boolean runImpacted = propertyReader.getBoolean("runImpacted", true); - if (runImpacted) { - serverOptions = ServerOptions.builder() - .url(propertyReader.getString("server.url")) - .project(propertyReader.getString("server.project")) - .userName(propertyReader.getString("server.userName")) - .userAccessToken(propertyReader.getString("server.userAccessToken")) - .build(); - } - - return TestEngineOptions.builder() - .serverOptions(serverOptions) - .partition(propertyReader.getString("partition")) - .runImpacted(runImpacted) - .runAllTests(propertyReader.getBoolean("runAllTests", false)) - .includeAddedTests(propertyReader.getBoolean("includeAddedTests", true)) - .includeFailedAndSkipped(propertyReader.getBoolean("includeFailedAndSkipped", true)) - .endCommit(propertyReader.getCommitDescriptor("endCommit")) - .endRevision(propertyReader.getString("endRevision")) - .baseline(propertyReader.getString("baseline")) - .baselineRevision(propertyReader.getString("baselineRevision")) - .repository(propertyReader.getString("repository")) - .agentUrls(propertyReader.getStringList("agentsUrls")) - .includedTestEngineIds(propertyReader.getStringList("includedEngines")) - .excludedTestEngineIds(propertyReader.getStringList("excludedEngines")) - .reportDirectory(propertyReader.getString("reportDirectory")) - .build(); - } - - private static class PrefixingPropertyReader { - - private final ConfigurationParameters configurationParameters; - - private final String prefix; - - private PrefixingPropertyReader(String prefix, ConfigurationParameters configurationParameters) { - this.prefix = prefix; - this.configurationParameters = configurationParameters; - } - - private T getOrNull(String propertyName, Function mapper) { - return get(propertyName, mapper, null); - } - - private T get(String propertyName, Function mapper, T defaultValue) { - return configurationParameters.get(prefix + propertyName).map(mapper).orElse(defaultValue); - } - - private String getString(String propertyName) { - return getOrNull(propertyName, Function.identity()); - } - - private Boolean getBoolean(String propertyName, boolean defaultValue) { - return get(propertyName, Boolean::valueOf, defaultValue); - } - - private CommitDescriptor getCommitDescriptor(String propertyName) { - return getOrNull(propertyName, CommitDescriptor::parse); - } - - private List getStringList(String propertyName) { - return get(propertyName, listAsString -> { - if (StringUtils.isEmpty(listAsString)) { - return Collections.emptyList(); - } - - return Arrays.asList(listAsString.split(",")); - }, Collections.emptyList()); - } - } -} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt index e06e16bc3..21c22b7a8 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/InternalImpactedTestEngine.kt @@ -1,7 +1,6 @@ package com.teamscale.test_impacted.engine import com.teamscale.test_impacted.commons.LoggerUtils.createLogger -import com.teamscale.test_impacted.commons.LoggerUtils.getLogger import com.teamscale.test_impacted.engine.executor.TestwiseCoverageCollectingExecutionListener import com.teamscale.test_impacted.test_descriptor.TestDescriptorResolverRegistry.getTestDescriptorResolver import com.teamscale.test_impacted.test_descriptor.TestDescriptorUtils.getAvailableTests diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/TestDataWriter.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/TestDataWriter.kt new file mode 100644 index 000000000..b716fbaeb --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/TestDataWriter.kt @@ -0,0 +1,39 @@ +package com.teamscale.test_impacted.engine + +import com.teamscale.client.TestDetails +import com.teamscale.report.ReportUtils.writeTestExecutionReport +import com.teamscale.report.ReportUtils.writeTestListReport +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.test_impacted.commons.LoggerUtils.createLogger +import com.teamscale.test_impacted.commons.LoggerUtils.getLogger +import java.io.File +import java.io.IOException +import java.util.logging.Level +import java.util.logging.Logger + +/** Class for writing test data to a report directory. */ +open class TestDataWriter(private val reportDirectory: File) { + /** Writes the given test executions to a report file. */ + fun dumpTestExecutions(testExecutions: List) { + val file = File(reportDirectory, "test-execution.json") + try { + writeTestExecutionReport(file, testExecutions) + } catch (e: IOException) { + LOGGER.log(Level.SEVERE, e) { "Error while writing report to file: $file" } + } + } + + /** Writes the given test details to a report file. */ + fun dumpTestDetails(testDetails: List) { + val file = File(reportDirectory, "test-list.json") + try { + writeTestListReport(file, ArrayList(testDetails)) + } catch (e: IOException) { + LOGGER.log(Level.SEVERE, e) { "Error while writing report to file: $file" } + } + } + + companion object { + private val LOGGER = createLogger() + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/TestEngineRegistry.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/TestEngineRegistry.kt new file mode 100644 index 000000000..1c8e11d34 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/TestEngineRegistry.kt @@ -0,0 +1,45 @@ +package com.teamscale.test_impacted.engine + +import org.junit.platform.commons.util.ClassLoaderUtils +import org.junit.platform.engine.TestEngine +import java.util.* +import java.util.function.Function +import java.util.stream.Collectors + +/** The test engine registry containing all */ +open class TestEngineRegistry( + includedTestEngineIds: Set, + excludedTestEngineIds: Set +) : Iterable { + private val testEnginesById: Map + + init { + var otherTestEngines = loadOtherTestEngines(excludedTestEngineIds) + + // If there are no test engines set we don't need to filter but simply use all other test engines. + if (includedTestEngineIds.isNotEmpty()) { + otherTestEngines = otherTestEngines.filter { testEngine -> + includedTestEngineIds.contains(testEngine.id) + } + } + + testEnginesById = otherTestEngines.associateBy { it.id } + } + + /** + * Uses the [ServiceLoader] to discover all [TestEngine]s but the [ImpactedTestEngine] and the + * excluded test engines. + */ + private fun loadOtherTestEngines(excludedTestEngineIds: Set) = + ServiceLoader.load( + TestEngine::class.java, ClassLoaderUtils.getDefaultClassLoader() + ).filter { + ImpactedTestEngine.ENGINE_ID != it.id && !excludedTestEngineIds.contains(it.id) + } + + /** Returns the [TestEngine] for the engine id or null if none is present. */ + fun getTestEngine(engineId: String) = testEnginesById[engineId] + + override fun iterator() = + testEnginesById.values.sortedBy { it.id }.iterator() +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt index 087b8c151..b01d94d90 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/executor/ImpactedTestsProvider.kt @@ -20,7 +20,7 @@ open class ImpactedTestsProvider( private val endCommit: CommitDescriptor, private val endRevision: String, private val repository: String, - @JvmField val partition: String, + val partition: String, private val includeNonImpacted: Boolean, private val includeAddedTests: Boolean, private val includeFailedAndSkipped: Boolean @@ -36,8 +36,7 @@ open class ImpactedTestsProvider( val response = client .getImpactedTests( availableTestDetails, baseline, baselineRevision, endCommit, endRevision, repository, - listOf(partition), - includeNonImpacted, includeAddedTests, includeFailedAndSkipped + listOf(partition), includeNonImpacted, includeAddedTests, includeFailedAndSkipped ) if (response.isSuccessful) { @@ -75,7 +74,7 @@ open class ImpactedTestsProvider( availableTestDetails: List ): Boolean { val returnedTests = testClusters.stream().mapToLong { - it.tests!!.size.toLong() + it.tests?.size?.toLong() ?: 0 }.sum() if (!includeNonImpacted) { logger.info { "Received $returnedTests impacted tests of ${availableTestDetails.size} available tests." } 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 index 17787859f..b1fffdeaf 100644 --- 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 @@ -16,7 +16,7 @@ class ImpactedTestsSorter(private val impactedTestsProvider: ImpactedTestsProvid val testClusters = impactedTestsProvider.getImpactedTestsFromTeamscale(availableTests.testList) - if (testClusters == null) { + if (testClusters.isEmpty()) { ImpactedTestEngine.LOGGER.fine { "Falling back to execute all!" } return } diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.kt new file mode 100644 index 000000000..c6957a0df --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptionUtils.kt @@ -0,0 +1,67 @@ +package com.teamscale.test_impacted.engine.options + +import com.teamscale.client.CommitDescriptor +import com.teamscale.client.StringUtils.isEmpty +import org.junit.platform.engine.ConfigurationParameters +import java.util.* +import java.util.function.Function + +object TestEngineOptionUtils { + + private const val PREFIX = "teamscale.test.impacted." + + /** Returns the [TestEngineOptions] configured in the [Properties]. */ + fun getEngineOptions(configurationParameters: ConfigurationParameters): TestEngineOptions { + val propertyReader = PrefixingPropertyReader(PREFIX, configurationParameters) + val shouldRunImpactedTests = propertyReader.getBoolean("runImpacted", true) + + val serverOptions = if (shouldRunImpactedTests) { + createServerOptions(propertyReader) + } else null + + return TestEngineOptions.builder() + .serverOptions(serverOptions) + .partition(propertyReader.getString("partition")) + .runImpacted(shouldRunImpactedTests) + .runAllTests(propertyReader.getBoolean("runAllTests", false)) + .includeAddedTests(propertyReader.getBoolean("includeAddedTests", true)) + .includeFailedAndSkipped(propertyReader.getBoolean("includeFailedAndSkipped", true)) + .endCommit(propertyReader.getCommitDescriptor("endCommit")) + .endRevision(propertyReader.getString("endRevision")) + .baseline(propertyReader.getString("baseline")) + .baselineRevision(propertyReader.getString("baselineRevision")) + .repository(propertyReader.getString("repository")) + .testCoverageAgentUrls(propertyReader.getStringList("agentsUrls")) + .includedTestEngineIds(propertyReader.getStringList("includedEngines")) + .excludedTestEngineIds(propertyReader.getStringList("excludedEngines")) + .reportDirectory(propertyReader.getString("reportDirectory")) + .build() + } + + private fun createServerOptions(propertyReader: PrefixingPropertyReader) = + ServerOptions.builder() + .url(propertyReader.getString("server.url")) + .project(propertyReader.getString("server.project")) + .userName(propertyReader.getString("server.userName")) + .userAccessToken(propertyReader.getString("server.userAccessToken")) + .build() + + private class PrefixingPropertyReader( + private val prefix: String, + private val configurationParameters: ConfigurationParameters + ) { + fun getString(propertyName: String): String = + configurationParameters[prefix + propertyName].orElse("") + + fun getBoolean(propertyName: String, defaultValue: Boolean): Boolean = + configurationParameters[prefix + propertyName].map { it.toBoolean() }.orElse(defaultValue) + + fun getCommitDescriptor(propertyName: String): CommitDescriptor? = + configurationParameters[prefix + propertyName].map { CommitDescriptor.parse(it) }.orElse(null) + + fun getStringList(propertyName: String): List = + configurationParameters[prefix + propertyName] + .map { it.split(",").filterNot(String::isEmpty) } + .orElse(emptyList()) + } +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt index ceffec536..b75c39b71 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt @@ -9,92 +9,52 @@ import com.teamscale.test_impacted.engine.executor.* import com.teamscale.tia.client.ITestwiseCoverageAgentApi import okhttp3.HttpUrl import java.io.File -import java.io.IOException -import java.nio.file.Files +import kotlin.io.path.createDirectories /** Represents options for the [com.teamscale.test_impacted.engine.ImpactedTestEngine]. */ class TestEngineOptions { - /** The server options. May not be null. */ - private var serverOptions: ServerOptions? = null - - /** The partition to upload test details to and get impacted tests from. If null, all partitions are used. - * @see [partition] - */ - var partition: String? = null - private set - /** Executes all tests, not only impacted ones if set. Defaults to false. - * @see [runAllTests] - */ - private var runAllTests = false + companion object { + private const val DEFAULT_RUN_IMPACTED = true + private const val DEFAULT_INCLUDE_ADDED_TESTS = true + private const val DEFAULT_INCLUDE_FAILED_AND_SKIPPED = true - /** Executes only impacted tests, not all ones if set. Defaults to true. */ - private var runImpacted = true + /** Returns the builder for [TestEngineOptions]. */ + @JvmStatic + fun builder() = Builder() + } - /** Includes added tests in the list of tests to execute. Defaults to true - * @see [includeAddedTests] - */ - private var includeAddedTests = true + private var serverOptions: ServerOptions? = null + var partition: String? = null + private var repository: String? = null - /** Includes failed and skipped tests in the list of tests to execute. Defaults to true - * @see [includeFailedAndSkipped] - */ - private var includeFailedAndSkipped = true + private var runAllTests = false + private var runImpacted = DEFAULT_RUN_IMPACTED + private var includeAddedTests = DEFAULT_INCLUDE_ADDED_TESTS + private var includeFailedAndSkipped = DEFAULT_INCLUDE_FAILED_AND_SKIPPED - /** - * The baseline. Only code changes after the baseline are considered for determining impacted tests. May be null to - * indicate no baseline. - */ private var baseline: String? = null - - /** - * Can be used instead of [baseline] by using a revision (e.g. git SHA1) instead of a branch and timestamp. - */ private var baselineRevision: String? = null - - /** The end commit used for TIA and for uploading the coverage. May not be null. */ private var endCommit: CommitDescriptor? = null - - /** - * Can be used instead of [endCommit] by using a revision (e.g. git SHA1) instead of a branch and timestamp. - */ private var endRevision: String? = null - /** - * The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. - * Null or empty will lead to a lookup in all repositories in the Teamscale project. - */ - private var repository: String? = null - - /** The URLs (including port) at which the agents listen to. Maybe empty but not null. */ private var testwiseCoverageAgentApis = emptyList() - - /** The test engine ids of all [org.junit.platform.engine.TestEngine]s to use. - * If empty all available [org.junit.platform.engine.TestEngine]s are used. */ private var includedTestEngineIds = emptySet() - - /** The test engine ids of all [org.junit.platform.engine.TestEngine]s to exclude. */ private var excludedTestEngineIds = emptySet() - /** The directory used to store test-wise coverage reports. Must be a writeable directory. */ private var reportDirectory: File? = null + /** Creates the test engine configuration */ fun createTestEngineConfiguration(): ImpactedTestEngineConfiguration { val testSorter = createTestSorter() val teamscaleAgentNotifier = createTeamscaleAgentNotifier() val testEngineRegistry = TestEngineRegistry(includedTestEngineIds, excludedTestEngineIds) - val testDataWriter = TestDataWriter(reportDirectory) - + val testDataWriter = TestDataWriter(reportDirectory!!) return ImpactedTestEngineConfiguration(testDataWriter, testEngineRegistry, testSorter, teamscaleAgentNotifier) } private fun createTestSorter(): ITestSorter { - if (!runImpacted) { - return NOPTestSorter() - } - - val testsProvider = createImpactedTestsProvider() - return ImpactedTestsSorter(testsProvider) + return if (!runImpacted) NOPTestSorter() else ImpactedTestsSorter(createImpactedTestsProvider()) } private fun createImpactedTestsProvider(): ImpactedTestsProvider { @@ -114,120 +74,63 @@ class TestEngineOptions { private fun createTeamscaleAgentNotifier() = TeamscaleAgentNotifier(testwiseCoverageAgentApis, runImpacted && !runAllTests) - /** The builder for [TestEngineOptions]. */ + /** Builder for [TestEngineOptions]. */ class Builder { - private val testEngineOptions = TestEngineOptions() - - fun serverOptions(serverOptions: ServerOptions?): Builder { - testEngineOptions.serverOptions = serverOptions - return this - } - - fun partition(partition: String?): Builder { - testEngineOptions.partition = partition - return this - } - - fun runImpacted(runImpacted: Boolean): Builder { - testEngineOptions.runImpacted = runImpacted - return this - } - - fun runAllTests(runAllTests: Boolean): Builder { - testEngineOptions.runAllTests = runAllTests - return this - } - - fun includeAddedTests(includeAddedTests: Boolean): Builder { - testEngineOptions.includeAddedTests = includeAddedTests - return this - } - - fun includeFailedAndSkipped(includeFailedAndSkipped: Boolean): Builder { - testEngineOptions.includeFailedAndSkipped = includeFailedAndSkipped - return this - } - - fun endCommit(endCommit: CommitDescriptor?): Builder { - testEngineOptions.endCommit = endCommit - return this - } - - fun endRevision(endRevision: String?): Builder { - testEngineOptions.endRevision = endRevision - return this - } - - fun repository(repository: String?): Builder { - testEngineOptions.repository = repository - return this - } - - fun baseline(baseline: String?): Builder { - testEngineOptions.baseline = baseline - return this - } - - fun baselineRevision(baselineRevision: String?): Builder { - testEngineOptions.baselineRevision = baselineRevision - return this - } - - fun agentUrls(agentUrls: List): Builder { - testEngineOptions.testwiseCoverageAgentApis = agentUrls - .map { HttpUrl.parse(it) } - .mapNotNull { - if (it != null) { - ITestwiseCoverageAgentApi.createService(it) - } else null - } - return this - } - - fun includedTestEngineIds(testEngineIds: List): Builder { - testEngineOptions.includedTestEngineIds = HashSet(testEngineIds) - return this - } - - fun excludedTestEngineIds(testEngineIds: List): Builder { - testEngineOptions.excludedTestEngineIds = HashSet(testEngineIds) - return this - } - - fun reportDirectory(reportDirectory: String?): Builder { - reportDirectory ?: return this - testEngineOptions.reportDirectory = File(reportDirectory) - return this + private val options = TestEngineOptions() + + fun serverOptions(config: ServerOptions?): Builder = + apply { options.serverOptions = config } + fun partition(partition: String?): Builder = + apply { options.partition = partition } + fun repository(repository: String?): Builder = + apply { options.repository = repository } + fun runImpacted(flag: Boolean): Builder = + apply { options.runImpacted = flag } + fun runAllTests(flag: Boolean): Builder = + apply { options.runAllTests = flag } + fun includeAddedTests(flag: Boolean): Builder = + apply { options.includeAddedTests = flag } + fun includeFailedAndSkipped(flag: Boolean): Builder = + apply { options.includeFailedAndSkipped = flag } + fun baseline(baseline: String?): Builder = + apply { options.baseline = baseline } + fun baselineRevision(revision: String?): Builder = + apply { options.baselineRevision = revision } + fun endCommit(commit: CommitDescriptor?): Builder = + apply { options.endCommit = commit } + fun endRevision(revision: String?): Builder = + apply { options.endRevision = revision } + fun includedTestEngineIds(ids: List): Builder = + apply { options.includedTestEngineIds = ids.toSet() } + fun excludedTestEngineIds(ids: List): Builder = + apply { options.excludedTestEngineIds = ids.toSet() } + fun reportDirectory(path: String?): Builder = + apply { path?.let { options.reportDirectory = File(it) } } + + fun testCoverageAgentUrls(urls: List): Builder = apply { + options.testwiseCoverageAgentApis = urls.mapNotNull { + HttpUrl.parse(it)?.let(ITestwiseCoverageAgentApi::createService) + } } - /** Checks field conditions and returns the built [TestEngineOptions]. */ + /** Validates and builds the [TestEngineOptions]. */ fun build(): TestEngineOptions { - if (testEngineOptions.endCommit == null && testEngineOptions.endRevision == null) { - throw AssertionError("End commit must be set via endCommit or endRevision.") - } - if (testEngineOptions.runImpacted) { - checkNotNull(testEngineOptions.serverOptions) { "Server options must be set." } - } - checkNotNull(testEngineOptions.testwiseCoverageAgentApis) { "Agent urls may be empty but not null." } - checkNotNull(testEngineOptions.reportDirectory) { "Report directory must be set." } - if (!testEngineOptions.reportDirectory!!.isDirectory || !testEngineOptions.reportDirectory!!.canWrite()) { - try { - Files.createDirectories(testEngineOptions.reportDirectory!!.toPath()) - } catch (e: IOException) { - throw AssertionError( - "Report directory could not be created: ${testEngineOptions.reportDirectory}", e - ) - } - } - return testEngineOptions + options.validateBuildPreconditions() + return options } } - companion object { - /** Returns the builder for [TestEngineOptions]. */ - @JvmStatic - fun builder(): Builder { - return Builder() + /** Helper for build validation */ + private fun validateBuildPreconditions() { + require(endCommit != null || endRevision != null) { "End commit must be set via endCommit or endRevision." } + if (runImpacted) { + requireNotNull(serverOptions) { "Server options must be set." } + } + requireNotNull(reportDirectory) { "Report directory must be set." } + reportDirectory?.let { + if (!it.isDirectory || !it.canWrite()) { + it.toPath().createDirectories() + } } } } diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.kt index 67e1b772b..9df347710 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/ITestDescriptorResolver.kt @@ -12,8 +12,8 @@ interface ITestDescriptorResolver { fun getClusterId(descriptor: TestDescriptor): Optional /** - * Returns the [org.junit.platform.engine.TestEngine.getId] of the [org.junit.platform.engine.TestEngine] to use this [ITestDescriptorResolver] - * for. + * Returns the [org.junit.platform.engine.TestEngine.getId] of the [org.junit.platform.engine.TestEngine] + * to use this [ITestDescriptorResolver] for. */ val engineId: String diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt index b46babe4b..cf2373d1c 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt @@ -62,7 +62,7 @@ abstract class ImpactedTestEngineTestBase { whenever(testEngineRegistry.getTestEngine(eq(engine.id))) .thenReturn(engine) } - whenever>(testEngineRegistry.iterator()).thenReturn(engines.iterator()) + whenever(testEngineRegistry.iterator()).thenReturn(engines.iterator()) return InternalImpactedTestEngine( ImpactedTestEngineConfiguration( From 2c58ab9e9773bff9351107240ceb7423b4ea4c9b Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Fri, 13 Dec 2024 12:00:35 +0100 Subject: [PATCH 19/21] TS-38628 Fix null partition --- .../test_impacted/test_descriptor/TestDescriptorUtils.kt | 2 +- .../test_impacted/engine/ImpactedTestEngineTestBase.kt | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt index 032f8769a..bd57619ad 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/test_descriptor/TestDescriptorUtils.kt @@ -99,7 +99,7 @@ object TestDescriptorUtils { @JvmStatic fun getAvailableTests( rootTestDescriptor: TestDescriptor, - partition: String? + partition: String ): AvailableTests { val availableTests = AvailableTests() diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt index cf2373d1c..10559a08b 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt @@ -22,6 +22,11 @@ abstract class ImpactedTestEngineTestBase { @Test fun testEngineExecution() { + whenever(impactedTestsProvider.getImpactedTestsFromTeamscale(any())) + .thenReturn(impactedTests) + whenever(impactedTestsProvider.partition) + .thenReturn("partition") + val testEngine = createInternalImpactedTestEngine(engines) val engineDescriptor = testEngine @@ -33,8 +38,6 @@ abstract class ImpactedTestEngineTestBase { .thenReturn(executionListener) whenever(executionRequest.rootTestDescriptor) .thenReturn(engineDescriptor) - whenever(impactedTestsProvider.getImpactedTestsFromTeamscale(any())) - .thenReturn(impactedTests) testEngine.execute(executionRequest) From b8e441f41197a38023a17183318dd98057a864eb Mon Sep 17 00:00:00 2001 From: Constructor Date: Sat, 14 Dec 2024 17:29:52 +0100 Subject: [PATCH 20/21] TS-38628 Remove native Mockito mock call --- .../teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt index 10559a08b..7fa86b95e 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/ImpactedTestEngineTestBase.kt @@ -7,7 +7,6 @@ import com.teamscale.test_impacted.engine.executor.TeamscaleAgentNotifier import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test import org.junit.platform.engine.* -import org.mockito.Mockito.mock import org.mockito.kotlin.* /** Base class for testing specific scenarios in the impacted test engine. */ From c7cf5aceca932b4c71e5f82dcc78bbdd09345dc1 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sat, 14 Dec 2024 17:39:46 +0100 Subject: [PATCH 21/21] TS-38628 Enhance test verification for execution listener callbacks --- .../test_impacted/engine/NoImpactedTestsTest.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt index 10e4b5897..11f2a07a8 100644 --- a/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt +++ b/impacted-test-engine/src/test/kotlin/com/teamscale/test_impacted/engine/NoImpactedTestsTest.kt @@ -6,6 +6,7 @@ import com.teamscale.test_impacted.engine.executor.SimpleTestDescriptor import com.teamscale.test_impacted.test_descriptor.JUnitJupiterTestDescriptorResolver import org.junit.platform.engine.EngineExecutionListener import org.junit.platform.engine.UniqueId +import org.mockito.kotlin.* /** Test setup where no test is impacted. */ internal class NoImpactedTestsTest : ImpactedTestEngineTestBase() { @@ -30,7 +31,18 @@ internal class NoImpactedTestsTest : ImpactedTestEngineTestBase() { override val engines = listOf(DummyEngine(testEngine1Root)) override val impactedTests = emptyList() - override fun verifyCallbacks(executionListener: EngineExecutionListener) {} + override fun verifyCallbacks(executionListener: EngineExecutionListener) { + // Verify that the root container (engine) starts and finishes + verify(executionListener).executionStarted(testEngine1Root) + verify(executionListener).executionFinished(eq(testEngine1Root), any()) + + // Verify that each non-impacted test starts and finishes correctly + verify(executionListener).executionStarted(firstTestClass) + verify(executionListener).executionFinished(eq(firstTestClass), any()) + + verify(executionListener).executionStarted(nonImpactedTestCase1) + verify(executionListener).executionFinished(eq(nonImpactedTestCase1), any()) + } companion object { private const val FIRST_TEST_CLASS = "FirstTestClass"