diff --git a/settings.gradle b/settings.gradle index 6b0f36416..957b8c40d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,7 +20,8 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") // e.g micronaut-test-resources-micronaut-test-extensions-core // is just micronaut-test-resources-extensions-core def extensionModules = [ - 'core' + 'core', + 'junit-platform' ] def jdbcModules = [ diff --git a/test-resources-build-tools/src/main/java/io/micronaut/testresources/buildtools/ServerUtils.java b/test-resources-build-tools/src/main/java/io/micronaut/testresources/buildtools/ServerUtils.java index f4ec80e4b..4b6bb564f 100644 --- a/test-resources-build-tools/src/main/java/io/micronaut/testresources/buildtools/ServerUtils.java +++ b/test-resources-build-tools/src/main/java/io/micronaut/testresources/buildtools/ServerUtils.java @@ -295,6 +295,9 @@ private static void startAndWait(ServerFactory serverFactory, startAndWait(serverFactory, explicitPort, portFilePath, accessToken, serverClasspath, cdsDirectory); return; } + if (explicitPort != null) { + return; + } while (!Files.exists(portFilePath)) { try { serverFactory.waitFor(Duration.of(STARTUP_TIME_WAIT_MS, ChronoUnit.MILLIS)); diff --git a/test-resources-extensions/test-resources-extensions-core/src/main/resources/META-INF/native-image/micronaut-test-resources-extensions-core/native-image.properties b/test-resources-extensions/test-resources-extensions-core/src/main/resources/META-INF/native-image/micronaut-test-resources-extensions-core/native-image.properties new file mode 100644 index 000000000..a09699da5 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-core/src/main/resources/META-INF/native-image/micronaut-test-resources-extensions-core/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-run-time=io.micronaut.test.extensions.testresources.TestResourcesClientHolder diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/build.gradle b/test-resources-extensions/test-resources-extensions-junit-platform/build.gradle new file mode 100644 index 000000000..2206ba441 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/build.gradle @@ -0,0 +1,60 @@ +plugins { + id 'io.micronaut.build.internal.test-resources-mntest-extension' + id 'jvm-test-suite' + id("org.jetbrains.kotlin.jvm") version "1.8.21" + id("com.google.devtools.ksp") version "1.8.21-1.0.11" +} + +description = """ +Provides JUnit 5 extensions to extend the capabiities of tests when +test resources are present. +""" + +testing { + suites { + spockTest(JvmTestSuite) { + dependencies { + implementation project(project.path) + implementation testFixtures(project(project.path)) + implementation(mnTest.micronaut.test.spock) + compileOnly(mn.micronaut.inject.groovy) + } + targets.all { + tasks.named("check") { + dependsOn(testTask) + } + } + } + koTest(JvmTestSuite) { + dependencies { + implementation project(project.path) + implementation testFixtures(project(project.path)) + implementation(mnTest.micronaut.test.kotest5) + } + targets.all { + tasks.named("check") { + dependsOn(testTask) + } + } + } + } +} + +dependencies { + annotationProcessor(mn.micronaut.inject.java) + api(projects.micronautTestResourcesExtensionsCore) + api(projects.micronautTestResourcesClient) + implementation(libs.junit.jupiter.api) + implementation(libs.junit.platform.launcher) + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(mnTest.micronaut.test.junit5) + testFixturesAnnotationProcessor(mn.micronaut.inject.java) + testFixturesApi(libs.junit.platform.launcher) + testFixturesApi(platform(mnTest.micronaut.test.bom)) + testFixturesImplementation(projects.micronautTestResourcesClient) + testFixturesImplementation(projects.micronautTestResourcesExtensionsCore) + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(mn.micronaut.context) + testRuntimeOnly(mn.snakeyaml) + kspKoTest(mn.micronaut.inject.kotlin) +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/AbstractScopedTest.kt b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/AbstractScopedTest.kt new file mode 100644 index 000000000..02b75d500 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/AbstractScopedTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.kotest.core.spec.Spec +import io.kotest.core.spec.style.StringSpec +import io.micronaut.test.extensions.testresources.junit5.FakeTestResourcesClient +import org.junit.jupiter.api.AfterAll + +abstract class AbstractScopedTest(body: StringSpec.() -> Unit = {}) : StringSpec(body) { + override fun afterSpec(f: suspend (Spec) -> Unit) { + FakeTestResourcesClient.reset() + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/ClassScopeTest.kt b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/ClassScopeTest.kt new file mode 100644 index 000000000..995b592e7 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/ClassScopeTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.kotest.matchers.shouldBe +import io.micronaut.test.extensions.junit5.annotation.ScopeNamingStrategy +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope +import io.micronaut.test.extensions.kotest5.annotation.MicronautTest + +@MicronautTest +@TestResourcesScope(namingStrategy = ScopeNamingStrategy.TestClassName::class) +internal class ClassScopeTest : ParentTestWithScope({ + "scope name is the current test class name"() { + ScopeHolder.get().orElse(null) shouldBe ClassScopeTest::class.java.name + } +}) diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/InheritedScopeTest.kt b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/InheritedScopeTest.kt new file mode 100644 index 000000000..953fbce47 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/InheritedScopeTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.kotest.matchers.shouldBe +import io.micronaut.test.extensions.kotest5.annotation.MicronautTest + +@MicronautTest +internal class InheritedScopeTest : ParentTestWithScope({ + + "scope from parent class is visible" { + ScopeHolder.get().orElse(null) shouldBe "from parent" + } + +}) diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/PackageScopeTest.kt b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/PackageScopeTest.kt new file mode 100644 index 000000000..7001e7319 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/PackageScopeTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.kotest.matchers.shouldBe +import io.micronaut.test.extensions.junit5.annotation.ScopeNamingStrategy +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope +import io.micronaut.test.extensions.kotest5.annotation.MicronautTest + +@MicronautTest +@TestResourcesScope(namingStrategy = ScopeNamingStrategy.PackageName::class) +internal class PackageScopeTest : ParentTestWithScope({ + "scope name is the current package" { + ScopeHolder.get().orElse(null) shouldBe PackageScopeTest::class.java.packageName + } +}) diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/ParentTestWithScope.kt b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/ParentTestWithScope.kt new file mode 100644 index 000000000..137d09f8f --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/koTest/kotlin/io/micronaut/test/extensions/junit5/ParentTestWithScope.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.kotest.core.spec.style.StringSpec +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope + +@TestResourcesScope("from parent") +internal abstract class ParentTestWithScope(body: StringSpec.() -> Unit = {}) : AbstractScopedTest(body) diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/ScopeHolder.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/ScopeHolder.java new file mode 100644 index 000000000..60291c4c8 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/ScopeHolder.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5; + +import io.micronaut.core.annotation.Internal; + +import java.util.Optional; + +/** + * Our JUnit listener needs to communicate with the test resources + * provider factory. Because they are both instantiated with + * service loading, we are using a thread local to share information. + */ +@Internal +final class ScopeHolder { + private static final ThreadLocal CURRENT_SCOPE = ThreadLocal.withInitial(() -> null); + + private ScopeHolder() { + } + + public static Optional get() { + return Optional.ofNullable(CURRENT_SCOPE.get()); + } + + public static void set(String scope) { + CURRENT_SCOPE.set(scope); + } + + public static void remove() { + CURRENT_SCOPE.remove(); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/ScopeTestPropertyProviderFactory.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/ScopeTestPropertyProviderFactory.java new file mode 100644 index 000000000..b05c49c5d --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/ScopeTestPropertyProviderFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5; + +import io.micronaut.test.support.TestPropertyProvider; +import io.micronaut.test.support.TestPropertyProviderFactory; +import io.micronaut.testresources.core.Scope; + +import java.util.Map; + +public class ScopeTestPropertyProviderFactory implements TestPropertyProviderFactory { + @Override + public TestPropertyProvider create(Map availableProperties, Class testClass) { + return () -> ScopeHolder.get() + .map(value -> Map.of(Scope.PROPERTY_KEY, value)) + .orElseGet(Map::of); + } + +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/TestResourcesScopeListener.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/TestResourcesScopeListener.java new file mode 100644 index 000000000..f4d612cc3 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/TestResourcesScopeListener.java @@ -0,0 +1,186 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5; + +import io.micronaut.test.extensions.junit5.annotation.ScopeNamingStrategy; +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; +import io.micronaut.test.extensions.testresources.TestResourcesClientHolder; +import io.micronaut.testresources.client.TestResourcesClient; +import io.micronaut.testresources.client.TestResourcesClientFactory; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; + +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * A listener which is responsible for shutting down test resources for a + * particular scope once the last test which uses this scope is finished. + */ +public class TestResourcesScopeListener implements TestExecutionListener { + private final Map> testsUsingResources = new ConcurrentHashMap<>(); + private TestResourcesClient testResourcesClient; + private final Deque nestedScopes = new ArrayDeque<>(); + + /** + * Creation of the test resources client is deferred at execution time, + * so that the test resources client holder is not initialized at build + * time in native image. + */ + private void assertTestResourcesClient() { + testResourcesClient = TestResourcesClientFactory.fromSystemProperties() + .orElse(TestResourcesClientHolder.lazy()); + } + + @Override + public void testPlanExecutionStarted(TestPlan testPlan) { + assertTestResourcesClient(); + Set roots = testPlan.getRoots(); + visitTestIdentifiers(roots, testPlan); + } + + @Override + public void executionStarted(TestIdentifier id) { + assertTestResourcesClient(); + visitTestIdentifier(id, EventKind.TEST_STARTED); + } + + @Override + public void executionFinished(TestIdentifier id, TestExecutionResult testExecutionResult) { + assertTestResourcesClient(); + visitTestIdentifier(id, EventKind.TEST_FINISHED); + } + + private void visitTestIdentifiers(Set ids, TestPlan testPlan) { + for (TestIdentifier id : ids) { + visitTestIdentifier(id, EventKind.TEST_REGISTERED); + visitTestIdentifiers(testPlan.getChildren(id), testPlan); + } + } + + private void visitTestIdentifier(TestIdentifier id, EventKind kind) { + id.getSource().ifPresent(source -> { + if (source instanceof ClassSource classSource) { + findTestResourceScopeAnnotation(classSource.getJavaClass()).ifPresent(testResourcesScope -> { + visitTestIdentifierWithAnnotation(id, kind, testResourcesScope); + }); + } + }); + } + + private void visitTestIdentifierWithAnnotation(TestIdentifier id, EventKind kind, TestResourcesScope testResourcesScope) { + if (testResourcesScope != null) { + String scopeName = testResourcesScope.value(); + if (scopeName == null || scopeName.isEmpty()) { + Class namingStrategy = testResourcesScope.namingStrategy(); + if (!namingStrategy.equals(ScopeNamingStrategy.class)) { + var scopeNamingStrategy = instantitateStrategy(namingStrategy); + scopeName = scopeNamingStrategy.scopeNameFor(id); + } + } + if (scopeName != null && !scopeName.isEmpty()) { + visitRequiredScope(id, kind, scopeName); + } + } + } + + private void visitRequiredScope(TestIdentifier id, EventKind kind, String scopeName) { + Set testIdentifiers = testsUsingResources.computeIfAbsent(scopeName, scope -> new ConcurrentSkipListSet<>()); + String testId = id.getUniqueId(); + switch (kind) { + case TEST_REGISTERED -> testIdentifiers.add(testId); + case TEST_STARTED -> { + nestedScopes.push(scopeName); + ScopeHolder.set(scopeName); + } + case TEST_FINISHED -> { + // We need to make sure the test id was known, because kotest + // can issue new test ids which weren't known at registration + if (testIdentifiers.remove(testId)) { + nestedScopes.pop(); + ScopeHolder.set(nestedScopes.peek()); + if (nestedScopes.isEmpty()) { + ScopeHolder.remove(); + } + if (testIdentifiers.isEmpty()) { + testResourcesClient.closeScope(scopeName); + } + } + } + } + } + + private static Optional findTestResourceScopeAnnotation(Class clazz) { + return Optional.ofNullable(clazz.getAnnotation(TestResourcesScope.class)).or(() -> findTestResourceScopeAnnotationFromInterfaces(clazz)); + } + + private static Optional findTestResourceScopeAnnotationFromInterfaces(Class clazz) { + var annotatedInterfaces = clazz.getAnnotatedInterfaces(); + Map, TestResourcesScope> foundScopes = new LinkedHashMap<>(); + collectScopes(annotatedInterfaces, foundScopes); + if (foundScopes.size() > 1) { + // We use System.err instead of an exception here, because the error will happen + // in the context of JUnit Platform listeners, which will _not_ fail the tests. + // Neither can we use a logger, since there's no logging library on classpath + // to reduce the risks of conflicts with user defined logging libraries. + var first = foundScopes.entrySet().stream().findFirst().get(); + System.err.println("[WARNING] Multiple interfaces declare a test resources scope. " + + "Only one can be used, make sure to annotate your class instead. " + + "Using scope declared in " + first.getKey()); + return Optional.of(first.getValue()); + } + return foundScopes.values().stream().findFirst(); + } + + private static void collectScopes(AnnotatedType[] annotatedInterfaces, Map, TestResourcesScope> foundScopes) { + for (AnnotatedType annotatedInterface : annotatedInterfaces) { + if (annotatedInterface.getType() instanceof Class annotatedClass) { + var annotation = annotatedClass.getAnnotation(TestResourcesScope.class); + if (annotation != null) { + foundScopes.put(annotatedClass, annotation); + } else { + collectScopes(annotatedClass.getAnnotatedInterfaces(), foundScopes); + } + } + } + } + + private static ScopeNamingStrategy instantitateStrategy(Class type) { + try { + return type.getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException("Scope naming strategy must have a public constructor without arguments", e); + } + } + + private enum EventKind { + TEST_REGISTERED, + TEST_STARTED, + TEST_FINISHED + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/annotation/ScopeNamingStrategy.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/annotation/ScopeNamingStrategy.java new file mode 100644 index 000000000..43efe64ae --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/annotation/ScopeNamingStrategy.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5.annotation; + +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.launcher.TestIdentifier; + +/** + * Provides the name of a test resources scope to + * be used in a test class. + */ +@FunctionalInterface +public interface ScopeNamingStrategy { + String scopeNameFor(TestIdentifier testId); + + class TestClassName implements ScopeNamingStrategy { + @Override + public String scopeNameFor(TestIdentifier testId) { + var source = testId.getSource(); + if (source.isPresent()) { + var testSource = source.get(); + if (testSource instanceof ClassSource classSource) { + return classSource.getClassName(); + } + } + return null; + } + } + + class PackageName implements ScopeNamingStrategy { + + @Override + public String scopeNameFor(TestIdentifier testId) { + var source = testId.getSource(); + if (source.isPresent()) { + var testSource = source.get(); + if (testSource instanceof ClassSource classSource) { + return classSource.getJavaClass().getPackageName(); + } + } + return null; + } + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/annotation/TestResourcesScope.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/annotation/TestResourcesScope.java new file mode 100644 index 000000000..f8a466ef0 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/java/io/micronaut/test/extensions/junit5/annotation/TestResourcesScope.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines the test resources scope to use in a test + * class. By default, all tests execute in the same + * (root) scope, which means that they share the same + * containers, for example. + *

+ * In some cases, it may be needed to isolate a test + * from others, by making sure it runs with its own + * test resources (e.g containers). + *

+ * When multiple tests are using the same scope, then + * the resource will be shared between those tests, + * and disposed whenever the last test which needed + * that resource has finished. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Inherited +public @interface TestResourcesScope { + /** + * The name of the test resources scope to use. + * A test can only use a single scope at once. + * Once the last test using this scope is executed, the + * scope will be automatically closed. If not set, + * you must provide a {@link #namingStrategy()}. + * + * @return the name of the scope + */ + String value() default ""; + + /** + * The name of the strategy to use, instead of an explicit + * name provided with {@link #value()}. + * @return the naming strategy + */ + Class namingStrategy() default ScopeNamingStrategy.class; +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/main/resources/META-INF/native-image/micronaut-test-resources-extension-junit-platform/reflect-config.json b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/resources/META-INF/native-image/micronaut-test-resources-extension-junit-platform/reflect-config.json new file mode 100644 index 000000000..07e3c1f97 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/resources/META-INF/native-image/micronaut-test-resources-extension-junit-platform/reflect-config.json @@ -0,0 +1,20 @@ +[ + { + "name" : "io.micronaut.test.extensions.junit5.annotation.ScopeNamingStrategy$TestClassName", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name" : "io.micronaut.test.extensions.junit5.annotation.ScopeNamingStrategy$PackageName", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + } +] diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/main/resources/META-INF/services/io.micronaut.test.support.TestPropertyProviderFactory b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/resources/META-INF/services/io.micronaut.test.support.TestPropertyProviderFactory new file mode 100644 index 000000000..a4c00e524 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/resources/META-INF/services/io.micronaut.test.support.TestPropertyProviderFactory @@ -0,0 +1 @@ +io.micronaut.test.extensions.junit5.ScopeTestPropertyProviderFactory diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener new file mode 100644 index 000000000..e94890f0d --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener @@ -0,0 +1 @@ +io.micronaut.test.extensions.junit5.TestResourcesScopeListener diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/AbstractScopedTest.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/AbstractScopedTest.groovy new file mode 100644 index 000000000..455efd534 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/AbstractScopedTest.groovy @@ -0,0 +1,26 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.micronaut.test.extensions.testresources.junit5.FakeTestResourcesClient +import spock.lang.Specification + +abstract class AbstractScopedTest extends Specification { + + void cleanupSpec() { + FakeTestResourcesClient.reset() + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/BasicScopeTest.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/BasicScopeTest.groovy new file mode 100644 index 000000000..d73a9396d --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/BasicScopeTest.groovy @@ -0,0 +1,33 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope +import io.micronaut.test.extensions.spock.annotation.MicronautTest + +@MicronautTest +@TestResourcesScope("hello") +class BasicScopeTest extends AbstractScopedTest { + + def "sees the current class scope"() { + expect: + "hello" == currentScope() + } + + private static String currentScope() { + return ScopeHolder.get().orElse(null); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/InheritedScopeTest.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/InheritedScopeTest.groovy new file mode 100644 index 000000000..e870f8f2f --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/InheritedScopeTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.micronaut.test.extensions.spock.annotation.MicronautTest + +@MicronautTest +class InheritedScopeTest extends ParentTestWithScope { + + void "scope from parent class is visible"() { + expect: + ScopeHolder.get().orElse(null) == "from parent" + } + +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/InheritedTraitScopeTest.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/InheritedTraitScopeTest.groovy new file mode 100644 index 000000000..ecadfaf38 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/InheritedTraitScopeTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +class InheritedTraitScopeTest extends AbstractScopedTest implements TraitWithScope { + + def "sees the scope declared in the trait"() { + expect: + "from trait" == currentScope() + } + + private static String currentScope() { + return ScopeHolder.get().orElse(null); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/InterfaceWithScope.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/InterfaceWithScope.java new file mode 100644 index 000000000..bd9fbf82e --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/InterfaceWithScope.java @@ -0,0 +1,22 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5; + +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; + +@TestResourcesScope("from interface") +public interface InterfaceWithScope { +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/MultipleInheritanceOrder2ScopeTest.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/MultipleInheritanceOrder2ScopeTest.groovy new file mode 100644 index 000000000..75de95afd --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/MultipleInheritanceOrder2ScopeTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +class MultipleInheritanceOrder2ScopeTest extends AbstractScopedTest implements InterfaceWithScope, TraitWithScope { + + def "sees the scope declared in the interface"() { + expect: + "from interface" == currentScope() + } + + private static String currentScope() { + return ScopeHolder.get().orElse(null); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/MultipleInheritanceOrderScopeTest.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/MultipleInheritanceOrderScopeTest.groovy new file mode 100644 index 000000000..ac5285b2b --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/MultipleInheritanceOrderScopeTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +class MultipleInheritanceOrderScopeTest extends AbstractScopedTest implements TraitWithScope, InterfaceWithScope { + + def "sees the scope declared in the trait"() { + expect: + "from trait" == currentScope() + } + + private static String currentScope() { + return ScopeHolder.get().orElse(null); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/MultipleInheritanceScopeTest.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/MultipleInheritanceScopeTest.groovy new file mode 100644 index 000000000..369db3d38 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/MultipleInheritanceScopeTest.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope + +@TestResourcesScope("this one") +class MultipleInheritanceScopeTest extends AbstractScopedTest implements TraitWithScope, InterfaceWithScope { + + def "sees the scope declared in the class"() { + expect: + "this one" == currentScope() + } + + private static String currentScope() { + return ScopeHolder.get().orElse(null); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/OverriddenScopeTest.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/OverriddenScopeTest.groovy new file mode 100644 index 000000000..36e07ea89 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/OverriddenScopeTest.groovy @@ -0,0 +1,33 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope +import io.micronaut.test.extensions.spock.annotation.MicronautTest + +@MicronautTest +@TestResourcesScope("hello") +class OverriddenScopeTest extends ParentTestWithScope { + + def "sees the current class scope instead of the parent"() { + expect: + "hello" == currentScope() + } + + private static String currentScope() { + return ScopeHolder.get().orElse(null); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/ParentTestWithScope.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/ParentTestWithScope.groovy new file mode 100644 index 000000000..afc6cea3b --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/ParentTestWithScope.groovy @@ -0,0 +1,22 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope + +@TestResourcesScope("from parent") +abstract class ParentTestWithScope extends AbstractScopedTest { +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/TraitWithScope.groovy b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/TraitWithScope.groovy new file mode 100644 index 000000000..375f4dad7 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/spockTest/groovy/io/micronaut/test/extensions/junit5/TraitWithScope.groovy @@ -0,0 +1,22 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope + +@TestResourcesScope("from trait") +trait TraitWithScope { +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/AbstractScopedTest.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/AbstractScopedTest.java new file mode 100644 index 000000000..05dab82b7 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/AbstractScopedTest.java @@ -0,0 +1,26 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5; + +import io.micronaut.test.extensions.testresources.junit5.FakeTestResourcesClient; +import org.junit.jupiter.api.AfterAll; + +public abstract class AbstractScopedTest { + @AfterAll + static void reset() { + FakeTestResourcesClient.reset(); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/ClassScopeTest.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/ClassScopeTest.java new file mode 100644 index 000000000..2898c974b --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/ClassScopeTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.extensions.junit5.annotation.ScopeNamingStrategy; +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@MicronautTest +@TestResourcesScope(namingStrategy = ScopeNamingStrategy.TestClassName.class) +public class ClassScopeTest extends ParentTestWithScope { + + @Test + @DisplayName("scope name is the current test class name") + void test() { + assertEquals(ClassScopeTest.class.getName(), ScopeHolder.get().orElse(null)); + } + +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/InheritedScopeTest.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/InheritedScopeTest.java new file mode 100644 index 000000000..7297af998 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/InheritedScopeTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@MicronautTest +public class InheritedScopeTest extends ParentTestWithScope { + + @Test + @DisplayName("scope from parent class is visible") + void test() { + assertEquals("from parent", ScopeHolder.get().orElse(null)); + } + +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/PackageScopeTest.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/PackageScopeTest.java new file mode 100644 index 000000000..f0302f3de --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/PackageScopeTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.extensions.junit5.annotation.ScopeNamingStrategy; +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@MicronautTest +@TestResourcesScope(namingStrategy = ScopeNamingStrategy.PackageName.class) +public class PackageScopeTest extends ParentTestWithScope { + + @Test + @DisplayName("scope name is the current package") + void test() { + assertEquals(PackageScopeTest.class.getPackageName(), ScopeHolder.get().orElse(null)); + } + +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/ParentTestWithScope.kt b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/ParentTestWithScope.kt new file mode 100644 index 000000000..185b36c44 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/ParentTestWithScope.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5 + +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope + +@TestResourcesScope("from parent") +internal abstract class ParentTestWithScope : AbstractScopedTest() diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/ScopeWithinClassTest.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/ScopeWithinClassTest.java new file mode 100644 index 000000000..7e1858ad2 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/java/io/micronaut/test/extensions/junit5/ScopeWithinClassTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.junit5; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; +import org.junit.jupiter.api.TestMethodOrder; + +import java.util.Set; + +import static io.micronaut.test.extensions.testresources.junit5.FakeTestResourcesClient.closedScopes; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@MicronautTest +@TestClassOrder(ClassOrderer.OrderAnnotation.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@TestResourcesScope("hello") +public class ScopeWithinClassTest extends AbstractScopedTest { + @Nested + @TestResourcesScope("scope1") + @Order(1) + class UsesCustomScopes { + @Test + @Order(1) + @DisplayName("Current scope is scope 1") + public void test1() { + assertEquals("scope1", currentScope()); + assertEquals(Set.of(), closedScopes()); + } + } + + @Nested + @TestResourcesScope("scope2") + @Order(2) + class ReusesScopes { + @Test + @Order(1) + @DisplayName("Current scope is scope 2") + public void test1() { + assertEquals("scope2", currentScope()); + assertEquals(Set.of("scope1"), closedScopes()); + } + + @Test + @Order(2) + @DisplayName("Current scope is still scope 2") + public void test2() { + assertEquals("scope2", currentScope()); + assertEquals(Set.of("scope1"), closedScopes()); + } + } + + @Nested + @Order(3) + class AllScopesMustBeClosed { + + @Test + @Order(1) + @DisplayName("Current scope is the scope of the top level class") + public void test1() { + assertEquals("hello", currentScope()); + assertEquals(Set.of("scope1", "scope2"), closedScopes()); + } + } + + private static String currentScope() { + return ScopeHolder.get().orElse(null); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/test/resources/application-test.yml b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/resources/application-test.yml new file mode 100644 index 000000000..10bfe6c0f --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/test/resources/application-test.yml @@ -0,0 +1,12 @@ +test-resources: + containers: + nats: + startup-timeout: 600s + image-name: nats:latest + exposed-ports: + - nats.port: 4222 + command: "--js" + wait-strategy: + log: + regex: ".*Server is ready.*" +toto: 40 diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClient.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClient.java new file mode 100644 index 000000000..5e293f288 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClient.java @@ -0,0 +1,72 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.testresources.junit5; + +import io.micronaut.testresources.client.TestResourcesClient; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class FakeTestResourcesClient implements TestResourcesClient { + private static final Map MOCK_PROPERTIES = Map.of( + "some-property", "supplied by test resources" + ); + + private static final ThreadLocal> CLOSED_SCOPES = ThreadLocal.withInitial(HashSet::new); + + @Override + public List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { + return MOCK_PROPERTIES.keySet().stream().toList(); + } + + @Override + public Optional resolve(String name, Map properties, Map testResourcesConfiguration) { + return Optional.ofNullable(MOCK_PROPERTIES.get(name)); + } + + @Override + public List getRequiredProperties(String expression) { + return List.of(); + } + + @Override + public List getRequiredPropertyEntries() { + return List.of(); + } + + @Override + public boolean closeAll() { + return true; + } + + @Override + public boolean closeScope(String id) { + CLOSED_SCOPES.get().add(id); + return true; + } + + public static Set closedScopes() { + return CLOSED_SCOPES.get(); + } + + public static void reset() { + CLOSED_SCOPES.remove(); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClientInjector.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClientInjector.java new file mode 100644 index 000000000..24cab195d --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClientInjector.java @@ -0,0 +1,32 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.test.extensions.testresources.junit5; + +import io.micronaut.test.extensions.testresources.TestResourcesClientHolder; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; + +public class FakeTestResourcesClientInjector implements TestExecutionListener { + @Override + public void testPlanExecutionStarted(TestPlan testPlan) { + TestResourcesClientHolder.set(new FakeTestResourcesClient()); + } + + @Override + public void testPlanExecutionFinished(TestPlan testPlan) { + TestResourcesClientHolder.set(null); + } +} diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener new file mode 100644 index 000000000..d03bf62a1 --- /dev/null +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener @@ -0,0 +1 @@ +io.micronaut.test.extensions.testresources.junit5.FakeTestResourcesClientInjector diff --git a/test-resources-server/src/main/java/io/micronaut/testresources/server/TestResourcesController.java b/test-resources-server/src/main/java/io/micronaut/testresources/server/TestResourcesController.java index d45b0f85a..e33df3690 100644 --- a/test-resources-server/src/main/java/io/micronaut/testresources/server/TestResourcesController.java +++ b/test-resources-server/src/main/java/io/micronaut/testresources/server/TestResourcesController.java @@ -98,11 +98,13 @@ public Optional resolve(String name, @Get("/close/all") public boolean closeAll() { + LOGGER.debug("Closing all test resources"); return TestContainers.closeAll(); } @Get("/close/{id}") public boolean closeScope(@Nullable String id) { + LOGGER.info("Closing test resources of scope {}", id); return TestContainers.closeScope(id); } diff --git a/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/TestContainers.java b/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/TestContainers.java index 7581f39b6..9e5ba0d21 100644 --- a/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/TestContainers.java +++ b/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/TestContainers.java @@ -164,9 +164,11 @@ public static boolean closeScope(String id) { Iterator>> iterator = CONTAINERS_BY_KEY.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry> entry = iterator.next(); - if (entry.getKey().scope.includes(scope)) { + var existingScope = entry.getKey().scope; + if (scope.includes(existingScope)) { iterator.remove(); GenericContainer container = entry.getValue(); + LOGGER.debug("Stopping container {}", container.getContainerId()); container.close(); closed = true; for (Set> value : CONTAINERS_BY_PROPERTY.values()) { diff --git a/test-resources-testcontainers/src/test/groovy/io/micronaut/testresources/testcontainers/TestContainersTest.groovy b/test-resources-testcontainers/src/test/groovy/io/micronaut/testresources/testcontainers/TestContainersTest.groovy new file mode 100644 index 000000000..3a62570b7 --- /dev/null +++ b/test-resources-testcontainers/src/test/groovy/io/micronaut/testresources/testcontainers/TestContainersTest.groovy @@ -0,0 +1,71 @@ +package io.micronaut.testresources.testcontainers + +import io.micronaut.testresources.core.Scope +import org.testcontainers.containers.GenericContainer +import spock.lang.Specification + +class TestContainersTest extends Specification { + + def cleanup() { + TestContainers.closeAll() + } + + def "closing root scope closes all"() { + def container1 = Stub(GenericContainer) + def container2 = Stub(GenericContainer) + def container3 = Stub(GenericContainer) + create("c1", null, container1) + create("c2", "child", container2) + create("c3", "child.nested", container3) + + when: + TestContainers.closeScope(null) + + then: + TestContainers.listAll() == [:] + } + + def "closing one scope closes nested scopes"() { + def container1 = Stub(GenericContainer) + def container2 = Stub(GenericContainer) + def container3 = Stub(GenericContainer) + create("c1", null, container1) + create("c2", "child", container2) + create("c3", "child.nested", container3) + + when: + TestContainers.closeScope("child") + + then: + TestContainers.listAll() == [ + (Scope.of(null)): [container1] + ] + } + + def "closing a leaf doesn't close parents"() { + def container1 = Stub(GenericContainer) + def container2 = Stub(GenericContainer) + def container3 = Stub(GenericContainer) + create("c1", null, container1) + create("c2", "child", container2) + create("c3", "child.nested", container3) + + when: + TestContainers.closeScope("child.nested") + + + then: + TestContainers.listAll() == [ + (Scope.of(null)): [container1], + (Scope.of("child")) : [container2] + ] + } + + void create(String name, String scope, GenericContainer container) { + TestContainers.getOrCreate("foo", TestContainersTest, name, [ + (Scope.PROPERTY_KEY): scope + ]) { + container + } + } +} diff --git a/test-resources-testcontainers/src/test/resources/logback.xml b/test-resources-testcontainers/src/test/resources/logback.xml index 80643c909..475b3e5e7 100644 --- a/test-resources-testcontainers/src/test/resources/logback.xml +++ b/test-resources-testcontainers/src/test/resources/logback.xml @@ -1,7 +1,6 @@ - true