From 45b1a65ddaad8bce0b6b170ae75558defeacc097 Mon Sep 17 00:00:00 2001 From: Romain Grecourt Date: Mon, 23 Dec 2024 18:07:01 -0800 Subject: [PATCH] save work --- .../microprofile/testing/HelidonTestInfo.java | 1 + .../testing/HelidonTestSynchronizer.java | 156 ------------ .../testing/testng/ClassContext.java | 222 ++++++++++++++++++ ...lassDecorator.java => ClassDecorator.java} | 8 +- .../testing/testng/HelidonTestNgListener.java | 92 ++++---- .../testng/HelidonTestNgListenerBase.java | 158 ++----------- microprofile/tests/testing/testng/pom.xml | 21 ++ .../tests/testing/testng/TestPerMethod.java | 2 +- .../src/test/resources/logging.properties | 21 ++ 9 files changed, 328 insertions(+), 353 deletions(-) delete mode 100644 microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestSynchronizer.java create mode 100644 microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassContext.java rename microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/{HelidonTestNgClassDecorator.java => ClassDecorator.java} (93%) create mode 100644 microprofile/tests/testing/testng/src/test/resources/logging.properties diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestInfo.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestInfo.java index c0682ed4002..4117615c15a 100644 --- a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestInfo.java +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestInfo.java @@ -358,6 +358,7 @@ public String toString() { return "MethodInfo{" + "method=" + element().getName() + ", class=" + classInfo.element().getName() + + ", requiresReset=" + requiresReset() + '}'; } diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestSynchronizer.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestSynchronizer.java deleted file mode 100644 index 08fa2fc302f..00000000000 --- a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestSynchronizer.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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; - -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Semaphore; -import java.util.concurrent.locks.AbstractQueuedSynchronizer; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * Test synchronizer. - * A concurrent utility to help enforce sequentiality. - */ -public class HelidonTestSynchronizer { - - private static final int CLASS_MASK = 1 << 31; - private static final int CONTAINER_MASK = 1 << 30; - private static final int METHOD_MASK = 1; - - private final Sync sync = new Sync(); - - private final Semaphore classSemaphore = new Semaphore(1); - private final Semaphore containerSemaphore = new Semaphore(1); - private final Lock methodsLock = new ReentrantReadWriteLock().readLock(); - private final Map> methodsFutures = new ConcurrentHashMap<>(); - - /** - * Acquire the "class" permit. - */ - public void acquireClass() { - try { - classSemaphore.acquire(); - // sync.acquireSharedInterruptibly(CLASS_MASK); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - /** - * Release the "class" permit. - */ - public void releaseClass() { - classSemaphore.release(); - // sync.releaseShared(CLASS_MASK); - } - - /** - * Acquire the "container" permit. - */ - public void acquireContainer() { - try { - containerSemaphore.acquire(); - // sync.acquireSharedInterruptibly(CONTAINER_MASK); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - /** - * Release the "container" permit. - */ - public void releaseContainer() { - containerSemaphore.release(); - // sync.releaseShared(CONTAINER_MASK); - } - - /** - * Wait the "methods" is zero. - */ - public void awaitMethods() { - try { - CompletableFuture.allOf(methodsFutures.values().toArray(new CompletableFuture[0])).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } finally { - methodsFutures.clear(); - } - } - - /** - * Increment the "methods" counter. - */ - public void startMethod(Object key) { - methodsFutures.put(key, new CompletableFuture<>()); - // sync.release(1); - } - - /** - * Decrement the "methods" counter. - */ - public void completeMethod(Object key) { - CompletableFuture future = methodsFutures.get(key); - if (future != null) { - future.complete(null); - } - // if (methodsCount.decrementAndGet() == 0) { - // methodsLock.unlock(); - // } - // sync.release(-1); - } - - private static final class Sync extends AbstractQueuedSynchronizer { - - @Override - protected int tryAcquireShared(int acquires) { - return (getState() & acquires) == 0 ? 0 : -1; - } - - @Override - protected boolean tryReleaseShared(int releases) { - for (; ; ) { - int c = getState(); - switch (releases) { - case CLASS_MASK -> { - if ((c & CLASS_MASK) == CLASS_MASK) { - return compareAndSetState(c, c | (c ^ CLASS_MASK)); - } - return false; - } - case CONTAINER_MASK -> { - if ((c & CONTAINER_MASK) == CONTAINER_MASK) { - return compareAndSetState(c, c | (c ^ CONTAINER_MASK)); - } - return false; - } - default -> { - if (c == 0) { - return false; - } - int nextc = c + 1; - if (compareAndSetState(c, nextc)) { - return nextc == 0; - } - } - } - } - } - } -} diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassContext.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassContext.java new file mode 100644 index 00000000000..f49e485038a --- /dev/null +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassContext.java @@ -0,0 +1,222 @@ +/* + * 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.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.microprofile.testing.HelidonTestInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo; + +import org.testng.ITestClass; +import org.testng.ITestNGMethod; + +/** + * Class context. + */ +class ClassContext { + + private final Semaphore semaphore; + private final HelidonTestNgListenerBase listener; + private final ClassInfo classInfo; + private final List methods; + private final AtomicInteger invocationCount = new AtomicInteger(); + private final AtomicBoolean afterClass = new AtomicBoolean(); + private final Map, List> graph = new ConcurrentHashMap<>(); + private final Map> methodsFutures = new ConcurrentHashMap<>(); + + /** + * Create a new instance. + * + * @param tc test class + * @param semaphore class semaphore + * @param listener listener + */ + ClassContext(ITestClass tc, Semaphore semaphore, HelidonTestNgListenerBase listener) { + this.listener = listener; + this.semaphore = semaphore; + this.classInfo = classInfo(tc.getRealClass()); + this.methods = methodInfos(classInfo, + tc.getTestMethods(), + tc.getBeforeTestMethods(), + tc.getBeforeTestMethods(), + tc.getBeforeClassMethods(), + tc.getAfterClassMethods()); + } + + /** + * Wait for the running methods. + */ + public void awaitMethods() { + try { + for (CompletableFuture future : Set.copyOf(methodsFutures.values())) { + future.get(); + } + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + /** + * Process a before-invocation event. + * + * @param invokedMethod invoked method + * @param testMethod corresponding test method, may be {@code null} + * @see org.testng.IInvokedMethodListener#beforeInvocation(org.testng.IInvokedMethod, org.testng.ITestResult) + * @see org.testng.IConfigurationListener#beforeConfiguration(org.testng.ITestResult, org.testng.ITestNGMethod) + */ + void beforeInvocation(ITestNGMethod invokedMethod, ITestNGMethod testMethod) { + MethodInfo methodInfo = methodInfo(classInfo, realMethod(invokedMethod)); + HelidonTestInfo testInfo = testMethod != null ? methodInfo(classInfo, realMethod(testMethod)) : classInfo; + graph.compute(testInfo, (k, v) -> { + if (v == null) { + v = new ArrayList<>(); + } + v.add(methodInfo); + return v; + }); + listener.onBeforeInvocation(this, methodInfo, testInfo); + if (invokedMethod.isTest()) { + methodsFutures.put(methodInfo, new CompletableFuture<>()); + } + } + + /** + * Process an after-invocation event. + * + * @param invokedMethod invoked method + * @param testMethod corresponding test method, may be {@code null} + * @see org.testng.IInvokedMethodListener#afterInvocation(org.testng.IInvokedMethod, org.testng.ITestResult) + * @see org.testng.IConfigurationListener#onConfigurationSuccess(org.testng.ITestResult, org.testng.ITestNGMethod) + * @see org.testng.IConfigurationListener#onConfigurationFailure(org.testng.ITestResult, org.testng.ITestNGMethod) + */ + void afterInvocation(ITestNGMethod invokedMethod, ITestNGMethod testMethod) { + try { + MethodInfo methodInfo = methodInfo(classInfo, realMethod(invokedMethod)); + HelidonTestInfo testInfo = testMethod != null ? methodInfo(classInfo, realMethod(testMethod)) : classInfo; + List deps = graph.compute(testInfo, (k, v) -> { + if (v == null) { + v = new ArrayList<>(); + } + v.remove(methodInfo); + return v; + }); + boolean last = deps.isEmpty(); + listener.onAfterInvocation(methodInfo, testInfo, last); + CompletableFuture future = methodsFutures.get(methodInfo); + if (future != null) { + future.complete(null); + } + } finally { + afterClass(AtomicInteger::incrementAndGet); + } + } + + /** + * Process a before-class event. + * + * @see org.testng.IClassListener#onBeforeClass(org.testng.ITestClass) + */ + void beforeClass() { + try { + semaphore.acquire(); + listener.onBeforeClass(classInfo); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Process an after-class event. + * + * @see org.testng.IClassListener#onAfterClass(org.testng.ITestClass) + */ + void afterClass() { + afterClass(AtomicInteger::get); + } + + private void afterClass(Function op) { + if (op.apply(invocationCount) == methods.size() + && afterClass.compareAndSet(false, true)) { + try { + listener.onAfterClass(classInfo); + methodsFutures.clear(); + } finally { + semaphore.release(); + } + } + } + + @Override + public String toString() { + return "ClassContext{" + + "class=" + classInfo.element().getName() + + ", methods: " + methodNames(methods) + + '}'; + } + + /** + * Get a class info. + * + * @param cls class + * @return ClassInfo + */ + static ClassInfo classInfo(Class cls) { + return HelidonTestInfo.classInfo(cls, HelidonTestDescriptorImpl::new); + } + + /** + * 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); + } + + private static List methodInfos(ClassInfo classInfo, ITestNGMethod[]... methods) { + return Stream.of(methods) + .flatMap(Stream::of) + .map(m -> methodInfo(classInfo, realMethod(m))) + .toList(); + } + + private static String methodNames(List methodInfos) { + return methodInfos.stream() + .map(MethodInfo::element) + .map(Method::getName) + .collect(Collectors.joining(",", "[", "]")); + } + + private static Method realMethod(ITestNGMethod tm) { + return tm.getConstructorOrMethod().getMethod(); + } +} diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgClassDecorator.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassDecorator.java similarity index 93% rename from microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgClassDecorator.java rename to microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassDecorator.java index b615624e2ba..d7f4e373b02 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgClassDecorator.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassDecorator.java @@ -33,9 +33,9 @@ * * @param delegate delegate */ -record HelidonTestNgClassDecorator(ITestClass delegate) implements ITestClass, ITestClassConfigInfo { +record ClassDecorator(ITestClass delegate) implements ITestClass, ITestClassConfigInfo { - private static final Map CACHE = new ConcurrentHashMap<>(); + private static final Map CACHE = new ConcurrentHashMap<>(); /** * Decorate the given test class. @@ -44,8 +44,8 @@ record HelidonTestNgClassDecorator(ITestClass delegate) implements ITestClass, I * @return decorated test class */ static ITestClass decorate(ITestClass tc) { - if (!(tc instanceof HelidonTestNgClassDecorator)) { - return CACHE.computeIfAbsent(tc, HelidonTestNgClassDecorator::new); + if (!(tc instanceof ClassDecorator)) { + return CACHE.computeIfAbsent(tc, ClassDecorator::new); } return tc; } 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 16d94403fdf..2ad5fa6fe97 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 @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; +import java.util.concurrent.Semaphore; import io.helidon.microprofile.testing.HelidonTestContainer; import io.helidon.microprofile.testing.HelidonTestInfo; @@ -29,13 +30,13 @@ import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo; import io.helidon.microprofile.testing.HelidonTestScope; import io.helidon.microprofile.testing.Proxies; -import io.helidon.microprofile.testing.HelidonTestSynchronizer; import org.testng.IAlterSuiteListener; import org.testng.ISuite; import org.testng.ISuiteListener; import org.testng.ITestListener; import org.testng.ITestNGMethod; +import org.testng.annotations.BeforeSuite; import org.testng.annotations.BeforeTest; import org.testng.annotations.Guice; import org.testng.xml.XmlClass; @@ -44,6 +45,7 @@ import static io.helidon.microprofile.testing.Instrumented.instrument; import static io.helidon.microprofile.testing.Instrumented.isInstrumented; +import static io.helidon.microprofile.testing.testng.ClassContext.classInfo; /** * A TestNG listener that integrates CDI with TestNG to support Helidon MP. @@ -97,9 +99,11 @@ public class HelidonTestNgListener extends HelidonTestNgListenerBase implements return null; })); - private static final List> METHOD_EXCLUDES = List.of(BeforeTest.class); + private static final List> METHOD_EXCLUDES = List.of( + BeforeTest.class, + BeforeSuite.class); - private final HelidonTestSynchronizer sync = new HelidonTestSynchronizer(); + private final Semaphore semaphore = new Semaphore(1); private volatile HelidonTestContainer container; @Override @@ -114,7 +118,7 @@ public void alter(List suites) { continue; } if (Modifier.isFinal(testClass.getModifiers())) { - LOGGER.log(Level.WARNING, "Cannot instrument final class: " + testClass.getName()); + LOGGER.log(Level.WARNING, "Cannot instrument final class: {0}", testClass.getName()); continue; } // Instrument the test class @@ -133,7 +137,7 @@ public void onStart(ISuite suite) { if (isInstrumented(tm.getTestClass().getRealClass())) { // replace the built-in ITestClass with a decorator // to hide the instrumented class name in the test results - tm.setTestClass(HelidonTestNgClassDecorator.decorate(tm.getTestClass())); + tm.setTestClass(ClassDecorator.decorate(tm.getTestClass())); } } } @@ -143,78 +147,66 @@ boolean filterClass(Class cls) { return isInstrumented(cls); } - // TODO use LOGGER instead of system out (it may produce better output ordering) - @Override - void onBeforeInvocation(MethodInfo methodInfo, HelidonTestInfo testInfo, boolean last) { - if (testInfo.requiresReset()) { - sync.awaitMethods(); - System.out.println("onBeforeInvocation: " + testInfo + ", thread: " + Thread.currentThread().getName()); - close(testInfo); - sync.acquireContainer(); - container = new HelidonTestContainer(testInfo, HelidonTestScope.ofContainer(), HelidonTestExtensionImpl::new); - } else { - System.out.println("onBeforeInvocation: " + testInfo + ", thread: " + Thread.currentThread().getName()); - if (container == null) { - try { - sync.acquireContainer(); - if (container == null) { - HelidonTestScope scope = HelidonTestScope.ofContainer(); - HelidonTestInfo containerInfo = testInfo.requiresReset() ? testInfo : testInfo.classInfo(); - container = new HelidonTestContainer(containerInfo, scope, HelidonTestExtensionImpl::new); - } - } finally { - // container is shared - sync.releaseContainer(); - } + void onBeforeInvocation(ClassContext classContext, MethodInfo methodInfo, HelidonTestInfo testInfo) { + LOGGER.log(Level.DEBUG, "onBeforeInvocation: {0}", testInfo); + try { + if (testInfo.requiresReset()) { + semaphore.acquire(); + classContext.awaitMethods(); + closeContainer(testInfo); + initContainer(testInfo); + } else { + semaphore.acquire(); + initContainer(testInfo.classInfo()); + semaphore.release(); } - } - if (last) { - sync.startMethod(testInfo); + } catch (InterruptedException e) { + throw new RuntimeException(e); } } @Override void onAfterInvocation(MethodInfo methodInfo, HelidonTestInfo testInfo, boolean last) { - System.out.println("onAfterInvocation: " + methodInfo + ", thread: " + Thread.currentThread().getName()); + LOGGER.log(Level.DEBUG, "onAfterInvocation: {0}", methodInfo); if (last) { if (testInfo.requiresReset()) { - close(testInfo); - sync.releaseContainer(); + closeContainer(testInfo); + semaphore.release(); } - sync.completeMethod(testInfo); } } @Override - public void onBeforeClass(ClassInfo classInfo) { - try { - sync.acquireClass(); - } finally { - System.out.println("onBeforeClass: " + classInfo + ", thread: " + Thread.currentThread().getName()); - } + void onBeforeClass(ClassInfo classInfo) { + LOGGER.log(Level.DEBUG, "onBeforeClass: {0}", classInfo); } @Override - public void onAfterClass(ClassInfo classInfo) { - try { - System.out.println("onAfterClass: " + classInfo + ", thread: " + Thread.currentThread().getName()); - close(classInfo); - } finally { - sync.releaseClass(); + void onAfterClass(ClassInfo classInfo) { + LOGGER.log(Level.DEBUG, "onAfterClass: {0}", classInfo); + closeContainer(classInfo); + semaphore.drainPermits(); + semaphore.release(); + } + + private void initContainer(HelidonTestInfo testInfo) { + if (container == null) { + LOGGER.log(Level.DEBUG, "initContainer: {0}", testInfo); + HelidonTestScope testScope = HelidonTestScope.ofContainer(); + container = new HelidonTestContainer(testInfo, testScope, HelidonTestExtensionImpl::new); } } - private void close(HelidonTestInfo testInfo) { + private void closeContainer(HelidonTestInfo testInfo) { if (container != null) { - System.out.println("close: " + testInfo + ", thread: " + Thread.currentThread().getName()); + LOGGER.log(Level.DEBUG, "closeContainer: {0}", testInfo); container.close(); container = null; } } private T resolve(Class type, Method method) { - System.out.println("resolve: " + method + ", thread: " + Thread.currentThread().getName()); if (container == null) { throw new IllegalStateException("Container not set"); } 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 index c9481f20610..a8e7280551b 100644 --- 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 @@ -16,16 +16,9 @@ package io.helidon.microprofile.testing.testng; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.concurrent.Semaphore; import io.helidon.microprofile.testing.HelidonTestInfo; import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo; @@ -41,13 +34,20 @@ import org.testng.xml.XmlTest; /** - * Base listener that implements before/after invoke methods. + * Base listener. + * Implements the following features: + *
    + *
  • Only processes {@link io.helidon.microprofile.testing.Instrumented intrumented} classes
  • + *
  • Single test class running at a time
  • + *
  • {@link #onAfterClass(ClassInfo)} invoked last
  • + *
*/ abstract class HelidonTestNgListenerBase implements IInvokedMethodListener, IConfigurationListener, IClassListener { private final Map> contexts = new ConcurrentHashMap<>(); + private final Semaphore semaphore = new Semaphore(1); /** * Filter the given class. @@ -60,18 +60,18 @@ abstract class HelidonTestNgListenerBase implements IInvokedMethodListener, /** * Before invocation. * - * @param methodInfo invoked method info - * @param testInfo test info - * @param last {@code true} if this method is the last one, {@code false} otherwise + * @param classContext class context + * @param methodInfo invoked method info + * @param testInfo test info */ - abstract void onBeforeInvocation(MethodInfo methodInfo, HelidonTestInfo testInfo, boolean last); + abstract void onBeforeInvocation(ClassContext classContext, MethodInfo methodInfo, HelidonTestInfo testInfo); /** * 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 + * @param last {@code true} if this is the last invocation of {@code testInfo}, {@code false} otherwise */ abstract void onAfterInvocation(MethodInfo methodInfo, HelidonTestInfo testInfo, boolean last); @@ -133,7 +133,7 @@ public void afterInvocation(IInvokedMethod im, ITestResult tr) { public void onBeforeClass(ITestClass tc) { Class cls = tc.getRealClass(); if (filterClass(cls)) { - onBeforeClass(classInfo(cls)); + classContext(tc).beforeClass(); } } @@ -144,138 +144,12 @@ public void onAfterClass(ITestClass tc) { } } - /** - * Get a class info. - * - * @param cls class - * @return ClassInfo - */ - static ClassInfo classInfo(Class cls) { - return HelidonTestInfo.classInfo(cls, HelidonTestDescriptorImpl::new); - } - - /** - * 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); - } - private ClassContext classContext(ITestClass tc) { return contexts.computeIfAbsent(tc.getXmlTest(), k -> new ConcurrentHashMap<>()) - .computeIfAbsent(tc, ClassContext::new); + .computeIfAbsent(tc, k -> new ClassContext(k, semaphore, this)); } private static Class realClass(ITestResult tr) { return tr.getTestClass().getRealClass(); } - - private static Method realMethod(ITestNGMethod tm) { - return tm.getConstructorOrMethod().getMethod(); - } - - private final class ClassContext { - private final ClassInfo classInfo; - private final List testMethods; - private final List eachTestMethods; - private final List otherMethods; - private final List scheduledMethods = new ArrayList<>(); - private final List invokedMethods = new ArrayList<>(); - private final Map, List> graph = new HashMap<>(); - private final ReentrantLock lock = new ReentrantLock(); - private final AtomicBoolean afterClass = new AtomicBoolean(); - - ClassContext(ITestClass tc) { - classInfo = classInfo(tc.getRealClass()); - testMethods = methodInfos(classInfo, tc.getTestMethods()); - eachTestMethods = methodInfos(classInfo, tc.getBeforeTestMethods(), tc.getBeforeTestMethods()); - otherMethods = methodInfos(classInfo, tc.getBeforeClassMethods(), tc.getAfterClassMethods()); - } - - void beforeInvocation(ITestNGMethod invokedMethod, ITestNGMethod testMethod) { - // TODO assert afterClass == false - MethodInfo methodInfo = methodInfo(classInfo, realMethod(invokedMethod)); - HelidonTestInfo testInfo = testMethod != null ? methodInfo(classInfo, realMethod(testMethod)) : classInfo; - try { - lock.lock(); - scheduledMethods.add(methodInfo); - graph.compute(testInfo, (k, v) -> { - if (v == null) { - v = new ArrayList<>(); - } - v.add(methodInfo); - return v; - }); - } finally { - lock.unlock(); - } - onBeforeInvocation(methodInfo, testInfo, invokedMethod.isTest()); - } - - void afterInvocation(ITestNGMethod invokedMethod, ITestNGMethod testMethod) { - // TODO assert afterClass == false - MethodInfo methodInfo = methodInfo(classInfo, realMethod(invokedMethod)); - HelidonTestInfo testInfo = testMethod != null ? methodInfo(classInfo, realMethod(testMethod)) : classInfo; - List deps; - try { - lock.lock(); - invokedMethods.add(methodInfo); - deps = graph.compute(testInfo, (k, v) -> { - if (v == null) { - v = new ArrayList<>(); - } - v.remove(methodInfo); - return v; - }); - } finally { - lock.unlock(); - } - onAfterInvocation(methodInfo, testInfo, deps.isEmpty()); - afterClass(); - } - - void afterClass() { - try { - lock.lock(); - if (scheduledMethods.size() == invokedMethods.size()) { - int total = testMethods.size() + otherMethods.size() + eachTestMethods.size(); - if (invokedMethods.size() >= total && afterClass.compareAndSet(false, true)) { - onAfterClass(classInfo); - } - } - } finally { - lock.unlock(); - } - } - - @Override - public String toString() { - return "ClassContext{" - + "class=" + classInfo.element().getName() - + ", testMethods: " + methodNames(testMethods) - + ", otherMethods: " + methodNames(otherMethods) - + ", eachTestMethods: " + methodNames(eachTestMethods) - + ", scheduledMethods: " + methodNames(scheduledMethods) - + ", invokedMethods: " + methodNames(invokedMethods) - + '}'; - } - - private static List methodInfos(ClassInfo classInfo, ITestNGMethod[]... methods) { - return Stream.of(methods) - .flatMap(Stream::of) - .map(m -> methodInfo(classInfo, realMethod(m))) - .toList(); - } - - private static String methodNames(List methodInfos) { - return methodInfos.stream() - .map(MethodInfo::element) - .map(Method::getName) - .collect(Collectors.joining(",", "[", "]")); - } - } } diff --git a/microprofile/tests/testing/testng/pom.xml b/microprofile/tests/testing/testng/pom.xml index bd5fa8ba8ef..aec7a076455 100644 --- a/microprofile/tests/testing/testng/pom.xml +++ b/microprofile/tests/testing/testng/pom.xml @@ -32,6 +32,10 @@ so the module can be used in MP config implementation + + true + + org.testng @@ -74,4 +78,21 @@ test + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + ${project.build.testOutputDirectory}/logging.properties + + + ${redirectTestOutputToFile} + + + + diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPerMethod.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPerMethod.java index ce3c23d2fcb..ef8015429ed 100644 --- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPerMethod.java +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. diff --git a/microprofile/tests/testing/testng/src/test/resources/logging.properties b/microprofile/tests/testing/testng/src/test/resources/logging.properties new file mode 100644 index 00000000000..5d0332c296d --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/resources/logging.properties @@ -0,0 +1,21 @@ +# +# 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. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s !thread!: %5$s%6$s%n + +.level=SEVERE +io.helidon.microprofile.testing.level=FINEST