aType) {
classInfo.annotations(aType));
}
- /**
- * Indicate if the container should be reset.
- * For a class this is resolved via {@code HelidonTest#resetPerTest()}.
- * For a method this is inferred if any of the following annotations is used:
- *
- * - {@link Configuration}
- * - {@link AddExtension}
- * - {@link AddBean}
- * - {@link AddJaxRs}
- * - {@link DisableDiscovery}
- *
- *
- * @return {@code true} if reset is required, {@code false} otherwise
- */
+ @Override
public boolean requiresReset() {
return classInfo.resetPerTest()
|| descriptor.configuration().isPresent()
diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java
index c344e528372..301b9191802 100644
--- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java
+++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java
@@ -16,47 +16,42 @@
package io.helidon.microprofile.testing.testng;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
import io.helidon.microprofile.testing.HelidonTestContainer;
import io.helidon.microprofile.testing.HelidonTestInfo;
import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo;
import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo;
import io.helidon.microprofile.testing.HelidonTestScope;
+import io.helidon.microprofile.testing.Instrumented;
import io.helidon.microprofile.testing.Proxies;
import org.testng.IAlterSuiteListener;
-import org.testng.IClass;
import org.testng.IClassListener;
-import org.testng.IConfigurationListener;
-import org.testng.IInvokedMethod;
-import org.testng.IInvokedMethodListener;
import org.testng.ISuite;
import org.testng.ISuiteListener;
import org.testng.ITestClass;
import org.testng.ITestListener;
import org.testng.ITestNGMethod;
-import org.testng.ITestResult;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
import org.testng.xml.XmlClass;
import org.testng.xml.XmlSuite;
import org.testng.xml.XmlTest;
-import static io.helidon.microprofile.testing.HelidonTestInfo.classInfo;
-import static io.helidon.microprofile.testing.HelidonTestInfo.methodInfo;
-import static io.helidon.microprofile.testing.Instrumented.instrument;
-
/**
* A TestNG listener that integrates CDI with TestNG to support Helidon MP.
*
- * This extension starts a CDI container and adds the test class as a bean with support for injection. The test class uses
- * a CDI scope that follows the test lifecycle as defined by {@code TODO}.
+ * This extension starts a CDI container and adds the test class as a bean with support for injection.
*
* The container is started lazily during test execution to ensure that it is started after all other extensions.
*
@@ -91,12 +86,12 @@
*
* @see HelidonTest
*/
-public class HelidonTestNgListener implements ITestListener,
- IClassListener,
- IInvokedMethodListener,
- IConfigurationListener,
- ISuiteListener,
- IAlterSuiteListener {
+public class HelidonTestNgListener extends HelidonTestNgListenerBase implements ITestListener,
+ IClassListener,
+ ISuiteListener,
+ IAlterSuiteListener {
+
+ private static final Logger LOGGER = System.getLogger(HelidonTestNgListener.class.getName());
private static final List TYPE_ANNOTATIONS = List.of(
Proxies.annotation(Guice.class, attr -> {
@@ -106,12 +101,9 @@ public class HelidonTestNgListener implements ITestListener,
return null;
}));
- private static final List> METHOD_EXCLUDES = List.of(
- BeforeTest.class);
-
- // TODO remove thread local
- private static final ThreadLocal CONTAINER = new ThreadLocal<>();
+ private static final List> METHOD_EXCLUDES = List.of(BeforeTest.class);
+ private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Map, HelidonTestContainer> containers = new ConcurrentHashMap<>();
@Override
@@ -119,18 +111,22 @@ public void alter(List suites) {
for (XmlSuite suite : suites) {
for (XmlTest test : suite.getTests()) {
for (XmlClass xmlClass : test.getXmlClasses()) {
- ClassInfo classInfo = classInfo(xmlClass.getSupportClass(), HelidonTestDescriptorImpl::new);
- if (Modifier.isAbstract(classInfo.element().getModifiers())
- || Modifier.isFinal(classInfo.element().getModifiers())) {
- // TODO check if contains @Test
- // TODO warning if final ?
- continue;
+ ClassInfo classInfo = classInfo(xmlClass.getSupportClass());
+ if (classInfo.isTestClass(Test.class)) {
+ Class> testClass = classInfo.element();
+ if (Modifier.isAbstract(testClass.getModifiers())) {
+ continue;
+ }
+ if (Modifier.isFinal(testClass.getModifiers())) {
+ LOGGER.log(Level.WARNING, "Cannot instrument final class: " + classInfo.id());
+ continue;
+ }
+ // Instrument the test class
+ // Add a @Guice annotation to install HelidonTestNgModuleFactory
+ // Use a proxy to start the container lazily
+ xmlClass.setClass(Instrumented.instrument(testClass,
+ TYPE_ANNOTATIONS, METHOD_EXCLUDES, this::resolveInstance));
}
- // Use a proxy to start the container after the test instance creation
- // The container is started lazily when invoking a method
- // We also add @Guice(moduleFactory = HelidonTestNgModuleFactory.class)
- xmlClass.setClass(instrument(classInfo.element(),
- TYPE_ANNOTATIONS, METHOD_EXCLUDES, this::testInstance));
}
}
}
@@ -139,82 +135,77 @@ public void alter(List suites) {
@Override
public void onStart(ISuite suite) {
for (ITestNGMethod tm : suite.getAllMethods()) {
- // replace the test class with a decorator to customize the name
+ // replace the built-in ITestClass with a decorator
// to hide the instrumented class name in the test results
tm.setTestClass(TestClassDecorator.decorate(tm.getTestClass()));
}
}
@Override
- public void beforeInvocation(IInvokedMethod im, ITestResult tr) {
- initContainer(tr, im.getTestMethod());
- }
-
- @Override
- public void afterInvocation(IInvokedMethod im, ITestResult tr) {
- HelidonTestInfo> testInfo = testInfo(tr, im.getTestMethod());
- if (requiresReset(testInfo)) {
- closeContainer(testInfo);
+ void onBeforeInvocation(ClassInfo classInfo, MethodInfo methodInfo, HelidonTestInfo> testInfo) {
+ HelidonTestContainer container;
+ try {
+ lock.readLock().lock();
+
+ // current container for the test class
+ container = containers.get(classInfo);
+
+ // close the container if the method requires a reset
+ if (methodInfo.requiresReset() && container != null) {
+ container.close();
+ containers.remove(classInfo);
+ container = null;
+ }
+ } finally {
+ lock.readLock().unlock();
}
- CONTAINER.remove();
- }
-
- @Override
- public void onAfterClass(ITestClass tc) {
- closeContainer(testInfo(tc));
- }
- private T testInstance(Class type, Method method) {
- HelidonTestContainer container = CONTAINER.get();
+ // create the container
if (container == null) {
- throw new IllegalStateException("Container not set");
- }
- return container.resolveInstance(type);
- }
-
- private void initContainer(ITestResult tr, ITestNGMethod tm) {
- HelidonTestInfo> testInfo = testInfo(tr, tm);
- HelidonTestContainer container = containers.compute(testInfo.classInfo(),
- (i, c) -> {
- boolean requireReset = requiresReset(testInfo);
- if (requireReset && c != null) {
- c.close();
- }
- if (c == null || c.closed()) {
- HelidonTestScope scope = HelidonTestScope.ofContainer();
- if (requireReset) {
- c = new HelidonTestContainer(testInfo, scope, HelidonTestExtensionImpl::new);
- } else {
- c = new HelidonTestContainer(testInfo.classInfo(), scope, HelidonTestExtensionImpl::new);
- }
+ try {
+ lock.writeLock().lock();
+ container = containers.get(classInfo);
+ if (container == null) {
+ HelidonTestScope scope = HelidonTestScope.ofContainer();
+ if (methodInfo.requiresReset()) {
+ container = new HelidonTestContainer(methodInfo, scope, HelidonTestExtensionImpl::new);
+ } else {
+ container = new HelidonTestContainer(classInfo, scope, HelidonTestExtensionImpl::new);
}
- return c;
- });
- containers.putIfAbsent(testInfo, container);
- CONTAINER.set(container);
- }
-
- private void closeContainer(HelidonTestInfo> testInfo) {
- HelidonTestContainer container = containers.remove(testInfo);
- if (container != null) {
- container.close();
+ containers.put(classInfo, container);
+ }
+ } finally {
+ lock.writeLock().unlock();
+ }
}
+ containers.putIfAbsent(methodInfo, container);
+ containers.putIfAbsent(testInfo, container);
}
- private HelidonTestInfo> testInfo(ITestResult tr, ITestNGMethod tm) {
- ClassInfo classInfo = testInfo(tr.getTestClass());
- return tm != null ? testInfo(tm.getConstructorOrMethod().getMethod(), classInfo) : classInfo;
+ @Override
+ void onAfterInvocation(MethodInfo methodInfo, HelidonTestInfo> testInfo, boolean last) {
+ onAfter(methodInfo, last && testInfo.requiresReset());
}
- private ClassInfo testInfo(IClass ic) {
- return classInfo(ic.getRealClass(), HelidonTestDescriptorImpl::new);
+ @Override
+ public void onAfterClass(ITestClass tc) {
+ onAfter(classInfo(tc.getRealClass()), true);
}
- private MethodInfo testInfo(Method method, ClassInfo classInfo) {
- return methodInfo(method, classInfo, HelidonTestDescriptorImpl::new);
+ private void onAfter(HelidonTestInfo> testInfo, boolean closeContainer) {
+ containers.computeIfPresent(testInfo, (k, v) -> {
+ if (closeContainer) {
+ v.close();
+ }
+ return null;
+ });
}
- private static boolean requiresReset(HelidonTestInfo> testInfo) {
- return testInfo instanceof MethodInfo methodInfo && methodInfo.requiresReset();
+ private T resolveInstance(Class type, Method method) {
+ HelidonTestContainer container = containers.get(methodInfo(type, method));
+ if (container == null) {
+ throw new IllegalStateException("Container not set");
+ }
+ return container.resolveInstance(type);
}
}
diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListenerBase.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListenerBase.java
new file mode 100644
index 00000000000..4195d19339f
--- /dev/null
+++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListenerBase.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * 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
+ *
+ * http://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.helidon.microprofile.testing.testng;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import io.helidon.microprofile.testing.HelidonTestInfo;
+import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo;
+import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo;
+
+import org.testng.IConfigurationListener;
+import org.testng.IInvokedMethod;
+import org.testng.IInvokedMethodListener;
+import org.testng.ITestNGMethod;
+import org.testng.ITestResult;
+
+/**
+ * Base listener that implements before/after invoke methods.
+ */
+class HelidonTestNgListenerBase implements IInvokedMethodListener,
+ IConfigurationListener {
+
+ private final Map, List> methods = new ConcurrentHashMap<>();
+
+ /**
+ * Before invocation.
+ *
+ * @param classInfo test class info
+ * @param methodInfo invoked method info
+ * @param testInfo test info
+ */
+ void onBeforeInvocation(ClassInfo classInfo, MethodInfo methodInfo, HelidonTestInfo> testInfo) {
+ // no-op
+ }
+
+ /**
+ * After invocation.
+ *
+ * @param methodInfo invoked method info
+ * @param testInfo test info
+ * @param last {@code true} if this method is the last one, {@code false} otherwise
+ */
+ void onAfterInvocation(MethodInfo methodInfo, HelidonTestInfo> testInfo, boolean last) {
+ // no-op
+ }
+
+ @Override
+ public void beforeConfiguration(ITestResult tr, ITestNGMethod tm) {
+ ClassInfo classInfo = classInfo(tr);
+ MethodInfo methodInfo = methodInfo(classInfo, tr.getMethod());
+ HelidonTestInfo> testInfo = tm != null ? methodInfo(classInfo, tm) : classInfo;
+ methods.compute(testInfo, (k, v) -> {
+ if (v == null) {
+ v = new ArrayList<>();
+ }
+ v.add(tm);
+ return v;
+ });
+ onBeforeInvocation(classInfo, methodInfo, testInfo);
+ }
+
+ @Override
+ public void onConfigurationFailure(ITestResult tr, ITestNGMethod tm) {
+ afterConfiguration(tr, tm);
+ }
+
+ @Override
+ public void onConfigurationSuccess(ITestResult tr, ITestNGMethod tm) {
+ afterConfiguration(tr, tm);
+ }
+
+ /**
+ * After configuration.
+ *
+ * @param tr test result
+ * @param tm test method, may be {@code null}
+ */
+ void afterConfiguration(ITestResult tr, ITestNGMethod tm) {
+ ClassInfo classInfo = classInfo(tr);
+ MethodInfo methodInfo = methodInfo(classInfo, tr.getMethod());
+ HelidonTestInfo> testInfo = tm != null ? methodInfo(classInfo, tm) : classInfo;
+ List deps = methods.compute(testInfo, (k, v) -> {
+ if (v == null) {
+ return List.of();
+ }
+ v.remove(tm);
+ return v;
+ });
+ onAfterInvocation(methodInfo, testInfo, deps.isEmpty());
+ }
+
+ @Override
+ public void beforeInvocation(IInvokedMethod im, ITestResult tr) {
+ if (im.isTestMethod()) {
+ ClassInfo classInfo = classInfo(tr);
+ MethodInfo methodInfo = methodInfo(classInfo, im.getTestMethod());
+ onBeforeInvocation(classInfo, methodInfo, methodInfo);
+ }
+ }
+
+ @Override
+ public void afterInvocation(IInvokedMethod im, ITestResult tr) {
+ if (im.isTestMethod()) {
+ ClassInfo classInfo = classInfo(tr);
+ MethodInfo methodInfo = methodInfo(classInfo, im.getTestMethod());
+ List deps = methods.getOrDefault(methodInfo, List.of());
+ onAfterInvocation(methodInfo, methodInfo, deps.isEmpty());
+ }
+ }
+
+ /**
+ * Get a class info.
+ *
+ * @param clazz class
+ * @return ClassInfo
+ */
+ static ClassInfo classInfo(Class> clazz) {
+ return HelidonTestInfo.classInfo(clazz, HelidonTestDescriptorImpl::new);
+ }
+
+ /**
+ * Get a class info.
+ *
+ * @param tr test result
+ * @return ClassInfo
+ */
+ static ClassInfo classInfo(ITestResult tr) {
+ return classInfo(tr.getTestClass().getRealClass());
+ }
+
+ /**
+ * Get a method info.
+ *
+ * @param clazz class
+ * @param method method
+ * @return MethodInfo
+ */
+ static MethodInfo methodInfo(Class> clazz, Method method) {
+ return methodInfo(classInfo(clazz), method);
+ }
+
+ /**
+ * Get a methodInfo info.
+ *
+ * @param classInfo class info
+ * @param tm method
+ * @return MethodInfo
+ */
+ static MethodInfo methodInfo(ClassInfo classInfo, ITestNGMethod tm) {
+ return methodInfo(classInfo, tm.getConstructorOrMethod().getMethod());
+ }
+
+ /**
+ * Get a method info.
+ *
+ * @param classInfo class info
+ * @param method method
+ * @return MethodInfo
+ */
+ static MethodInfo methodInfo(ClassInfo classInfo, Method method) {
+ return HelidonTestInfo.methodInfo(method, classInfo, HelidonTestDescriptorImpl::new);
+ }
+}
diff --git a/microprofile/testing/testng/src/main/java/module-info.java b/microprofile/testing/testng/src/main/java/module-info.java
index b579de86b5d..1ad14b0b8c2 100644
--- a/microprofile/testing/testng/src/main/java/module-info.java
+++ b/microprofile/testing/testng/src/main/java/module-info.java
@@ -18,8 +18,6 @@
* TestNG extension module to run CDI tests.
*/
module io.helidon.microprofile.testing.testng {
- uses org.testng.xml.ISuiteParser;
-
requires org.testng;
requires transitive io.helidon.microprofile.testing;
requires static io.helidon.microprofile.server;