UnitTestServer.java
/*
** Module : UnitTestServer.java
** Abstract : FWD unit test remote protocol implementation
**
** Copyright (c) 2023-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description---------------------------------
** 001 VVT 20230318 Created initial version.
** 002 VVT 20230412 OEUnit support added. See #6237. Also minor style fixes.
** 003 VVT 20230418 All JUnit5 discovery selector types are now passed to the server
** for possible future extensions.
** 004 GBB 20230427 Adding LegacyLogManagerConfigs arg to AppServerManager.startAppServer after signature
** changed.
** 005 VVT 20230428 Fixed error propagation (See #3827-350) and source formatting.
** VVT 20230505 Fixed: individual method name extraction in discover(..).
** VVT 20230511 Multiple JUnit5 session support added: the initialization
** at the beginning of the first discovery call, and shutdown
** at the end of the last cleanUp() call.
** VVT 20230522 Legacy stack trace is now converted to a Java stack trace. See #3827-448.
** 006 GBB 20230608 Added a flag to LegacyLogManagerConfigs for server-side filesystem in use.
** 007 CA 20231214 Push a bogus external procedure as the global scope.
** 008 VVT 20231215 Fixed RuntimeException error when converting error stack trace, consisting of
** multiple parts: only the last part is now converted. See #8126.
** VVT 20231215 Throwing RuntimeException error when converting invalid error stack trace element
** replaced by logging and ignoring the element. See #8126-5.
** 009 CA 20231221 Add ObjectOps scopeable support for the global external procedure.
** 010 VVT 20240111 Private methods are now filtered out in test discovery. See #8176.
** CentralLogger is now used for logging.
** VVT 20240113 Severity decreased for the "Private method cannot be used..." log message.
** See #8176-19
** 011 VVT 20240117 LegacyLogManager is now initialized properly. See #8195.
** VVT 20240117 Call to AppServerManager.startAppServer() removed. See #8195-15.
** 012 CA 20240308 Use client session initialization code from StandardServer.
** 003 VVT 20240325 Unused 'testInstance' parameter removed from addOptionally() andf testAndCreate().
** 013 VVT 20240408 Minor style fixes. See #8406-70.
** 014 VVT 20240718 Factory is now used to create descriptor instances. See #8874.
** 015 CA 20250503 'pushCalleeInfo' must match the signature of calling an external program.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.testengine;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.util.function.*;
import java.util.regex.*;
import org.junit.platform.commons.support.*;
import org.junit.platform.commons.util.*;
import org.junit.platform.engine.*;
import org.opentest4j.AssertionFailedError;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.oo.core.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.oo.oeunit.runner.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.trigger.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.testengine.api.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.google.common.collect.*;
/**
* FWD unit testing engine remote operation server-side implementation.
*
* This class contains static methods matching the {@link UnitTestEngine} interface.
*/
public class UnitTestServer
{
/**
* Logger for this test engine
*/
final static CentralLogger LOG = CentralLogger.get(UnitTestServer.class.getName());
/** Context local work area. */
private final static ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
{
@Override
protected WorkArea initialValue()
{
return new WorkArea();
};
};
/**
* Filter only class types which can contain tests or are test providers:
* public non-abstract non-internal.
*
*/
private final static Predicate<Class<?>> isTestClassCandidate = candidate -> {
try
{
final int modifiers = candidate.getModifiers();
return !(Modifier.isPrivate(modifiers) || Modifier.isAbstract(modifiers)
|| Modifier.isStatic(modifiers) || candidate.isLocalClass()
|| candidate.isAnonymousClass() || candidate.isMemberClass());
}
catch (final NoClassDefFoundError cnf)
{
// Note: we get here, if the application classpath is incomplete,
LOG.warning("Cannot load test class candidate: " + candidate + ": " + cnf.getMessage());
return false;
}
};
/**
* Legacy stack trace entry pattern
*/
private final static Pattern legacyErrorStackTraceElementRegexp = Pattern
.compile("([^\\[]+)(\\[([^\\]]+)])? at line (\\d+) \\(([^)]+)\\)$");
/**
* Execute the <em>after</em> behavior of this node.
* <p>
* This method will be called once <em>after</em> {@linkplain #execute execution}
* of this node.
*
* @param testId
* test descriptor ID
*
* @throws ErrorConditionException
* remote code may throw this
* @throws AssertionFailedError
* remote legacy errors are converted to this
* exception type
*/
public static void after(final UniqueId testId)
throws ErrorConditionException,
AssertionFailedError
{
final AbstractFWDTestDescriptor desc = getDescriptor(testId);
LOG.fine("after: " + testId + " desc: " + desc);
convertError(() -> desc.afterImpl());
}
/**
* Execute the <em>before</em> behavior of this node.
* <p>
* This method will be called once <em>before</em> {@linkplain #execute execution}
* of this node.
*
* @param testId
* test descriptor ID
*
* @throws ErrorConditionException
* remote code may throw this
* @throws AssertionFailedError
* remote legacy errors are converted to this
* exception type
*/
public static void before(final UniqueId testId)
throws ErrorConditionException,
AssertionFailedError
{
final AbstractFWDTestDescriptor desc = getDescriptor(testId);
LOG.fine("before: " + testId + " desc: " + desc);
convertError(() -> desc.beforeImpl());
}
/**
* Clean up the supplied {@code context} after execution.
* <p>
* Reset the test class instance.
*
* @param testId
* test descriptor ID
*
* @see #prepare
*/
public static void cleanUp(final UniqueId testId)
{
final AbstractFWDTestDescriptor desc = getDescriptor(testId);
LOG.fine("cleanUp : " + testId + " desc: " + desc);
try
{
desc.cleanUpImpl();
}
finally
{
if (desc instanceof FWDEngineDescriptor)
{
final WorkArea workArea = context.get();
workArea.unprocessed--;
if (workArea.unprocessed == 0)
{
// Cleanup
TransactionManager.setProcessingQuit(true);
// The topmost scope was pushed on in the beginning of discovery
TransactionManager.popScope();
}
}
}
}
/**
* Discover test sources provided as arguments.
* <p>
* Note: only the classroots, packages, classes, methods and modules are supported
* by this engine. All other parameters can have no relation neither to ABLUnit nor OEUnit, and
* are preserved for possible future extensions.
*
* @param uniqueId
* the engine unique ID
* @param classroots
* the list of class root URLs
* @param packages
* the list of packages
* @param classes
* the list of class names
* @param methods
* the list of method specifications
* @param uris
* the list of URIs
* @param files
* the list of files
* @param directories
* the list of directories
* @param modules
* the list of modules
* @param resources
* the list of resources
* @param iterations
* the list of iterations (currently not supported)
*
* @return the test descriptor discovered or {@code null} if no match was found.
*
* @throws ErrorConditionException
* remote code may throw this
* @throws AssertionFailedError
* remote legacy errors are converted to this
* exception type
*/
public static TestDescriptor discover(final UniqueId uniqueId,
final URI[] classroots,
final String[] packages,
final String[] classes,
final String[] methods,
@SuppressWarnings("unused")
final URI[] uris,
@SuppressWarnings("unused")
final String[] files,
@SuppressWarnings("unused")
final String[] directories,
final String[] modules,
@SuppressWarnings("unused")
final String[] resources,
@SuppressWarnings("unused")
final String[] iterations)
throws ErrorConditionException,
AssertionFailedError
{
final FWDEngineDescriptor engineDescriptor = new FWDEngineDescriptor(uniqueId);
register(engineDescriptor);
final WorkArea wa = context.get();
if (wa.unprocessed == 0)
{
// If we are here for the first time, do the initialization
// force DatabaseTriggerManager context-local instantiation before the new scope is pushed
DatabaseTriggerManager.get();
// add a bogus scope
Object bogus = new Object();
ProcedureManager.getProcedureHelper().pushCalleeInfo(UnitTestServer.class, false, bogus, bogus, ProcedureManager.EXTERNAL_PROGRAM, "bogus-ut.p", false, false, false, false);
ObjectOps.getObjectHelper().registerPendingScopeable();
TransactionManager.pushScope("bogus", TransactionManager.SUB_TRANSACTION, true, true, false, false, BlockType.EXTERNAL_PROC);
// make sure that the global block gets a special notification - but only for non-appserver sessions.
LogicalTerminal.registerCleaner();
}
convertError(() -> {
try
{
for (final String methodId : methods)
{
final int delimeterIdx = methodId.indexOf('#');
final String className = methodId.substring(0, delimeterIdx);
final Class<?> clazz = Class.forName(className);
final String methodName = methodId.substring(delimeterIdx + 1);
final Method method = clazz.getMethod(methodName);
addOptionally(engineDescriptor, clazz, method);
}
for (final String selector : classes)
{
scanClassRecursively(engineDescriptor, Class.forName(selector));
}
}
catch (final ClassNotFoundException | NoSuchMethodException | SecurityException e)
{
throw new RuntimeException(e);
}
for (final String pkg : packages)
{
// Note: we need to restrict the search by only the classes immediately located in the package,
// excluding all classes in sub-packages
final List<Class<?>> packageClasses = ReflectionSupport.findAllClassesInPackage(pkg,
(Class<?> c) -> pkg.equals(c.getPackage().getName()), name -> true);
for (Class<?> javaClass : packageClasses)
{
scanClassRecursively(engineDescriptor, javaClass);
}
}
for (final URI uri : classroots)
{
final List<Class<?>> classrootClasses = ReflectionSupport
.findAllClassesInClasspathRoot(uri, isTestClassCandidate, name -> true);
for (Class<?> javaClass : classrootClasses)
{
scanClassRecursively(engineDescriptor, javaClass);
}
}
for (final String module : modules)
{
// FIXME: this is not tested due to the lack of modules in FWD.
final List<Class<?>> uriClasses = ReflectionSupport
.findAllClassesInModule(module, isTestClassCandidate, name -> true);
for (Class<?> javaClass : uriClasses)
{
scanClassRecursively(engineDescriptor, javaClass);
}
}
});
wa.unprocessed++;
return engineDescriptor;
}
/**
* Execute the <em>behavior</em> of this node.
* <p>
* Locate the test descriptor by its unique ID and calls
* {@link AbstractFWDTestDescriptor#executeImpl()}
*
* @param testId
* test descriptor ID
*
* @throws ErrorConditionException
* remote code may throw this
* @throws AssertionFailedError
* remote legacy errors are converted to this
* exception type
*/
public static void execute(final UniqueId testId)
throws ErrorConditionException,
AssertionFailedError
{
final AbstractFWDTestDescriptor desc = getDescriptor(testId);
LOG.fine("execute: " + testId + " desc: " + desc);
final String expectedException = (desc instanceof AbstractMethodTestDescriptor)
? ((AbstractMethodTestDescriptor) desc).expectedException
: "";
convertError(() -> desc.executeImpl(), expectedException);
}
/**
* Initialize FWD server.
*/
public static void initialize()
{
StandardServer.initializeClientSession();
DatabaseManager.autoConnect();
SessionUtils.setErrorStackTrace(true);
}
/**
* Prepare test descriptor.
* <p>
* Locate the test descriptor by its unique ID and calls
* {@link AbstractFWDTestDescriptor#prepareImpl()}
*
* @param testId
* test descriptor ID
*
* @throws ErrorConditionException
* remote code may throw this
* @throws AssertionFailedError
* remote legacy errors are converted to this
* exception type
*/
public static void prepare(final UniqueId testId)
throws ErrorConditionException,
AssertionFailedError
{
final AbstractFWDTestDescriptor desc = getDescriptor(testId);
LOG.fine("prepare : " + testId + " desc: " + desc);
desc.prepareImpl();
}
/**
* Run the provided code, convert legacy errors thrown into {@link AbstractAssertionFailedError}
* along with legacy error stack trace, throw the result.
* <p>
* This is a FWD extension to support test runners (e.g. Eclipse), which "know" how to display
* {@link AssertionFailedError} specially.
*
* @param action
* the action to run, must return the legacy error thrown or {@code null}
* @param expectedExceptionClassName
* the expected legacy error class name or an empty string if no legacy error was expected
*
* @throws ErrorConditionException
* remote code may throw this
* @throws AssertionFailedError
* remote legacy errors are converted to this
* exception type
*/
protected final static void convertError(final Runnable action, final String expectedExceptionClassName)
throws ErrorConditionException,
AssertionFailedError
{
// Run the code, catch legacy error
LegacyError legacyError;
try
{
action.run();
legacyError = null;
}
catch (final LegacyErrorException e)
{
legacyError = e.getErrorRef();
}
final Class<?> expectedException;
if (!expectedExceptionClassName.isEmpty())
{
expectedException = ObjectOps.resolveClass(expectedExceptionClassName);
if (expectedException == null)
{
throw new RuntimeException(
"Cannot resolve legacy class to Java class: " + expectedExceptionClassName);
}
}
else
{
expectedException = null;
}
final AssertionFailedError error;
if (expectedException == null)
{
if (legacyError == null)
{
// Success: no error, and none expected
return;
}
// An error occurred, but non expected
if (legacyError instanceof AbstractAssertionFailedError)
{
final AbstractAssertionFailedError afe = (AbstractAssertionFailedError) legacyError;
final BaseDataType expected = afe.getExpected();
final BaseDataType actual = afe.getActual();
final String message = afe.getMessageInt();
error = new AssertionFailedError(message,
expected == null ? "null" : expected.toStringMessage(),
actual == null ? "null" : actual.toStringMessage());
}
else
{
error = new AssertionFailedError(legacyError.getMessage(new integer(1)).toStringMessage());
}
}
else if (legacyError == null)
{
// Legacy exception expected
throw new AssertionFailedError(
"Throwing " + expectedExceptionClassName + " was expected, but nothing was thrown");
}
else if (!expectedException.isAssignableFrom(legacyError.getClass()))
{
error = new AssertionFailedError(
"Throwing " + expectedExceptionClassName + " was expected, but " + legacyError.toString()
+ " was thrown");
}
else
{
// An expected exception was thrown
return;
}
// Transform legacy error call stack into a Java call stack
final character callStack = legacyError.getCallStack();
if (!callStack.isUnknown())
{
final String stackString = callStack.toStringMessage();
final String sep = System.getProperty("line.separator");
final String[] chunks = stackString.split(sep);
final List<StackTraceElement> trace = new ArrayList<>();
for (final String chunk : chunks)
{
if ("Server StackTrace:".equals(chunk))
{
// FIXME: we do not know what to do with a multi-part stack traces,
// so for now will ignore all parts but the last part.
trace.clear();
continue;
}
final Matcher matcher = legacyErrorStackTraceElementRegexp.matcher(chunk);
if (!matcher.matches())
{
LOG.severe("Cannot parse legacy error stack trace element: " + chunk);
continue;
}
final String javaClassMethod = matcher.group(1);
final int separator = javaClassMethod.lastIndexOf('.');
final String javaClass = javaClassMethod.substring(0, separator);
final String javaMethod = javaClassMethod.substring(separator + 1);
// The part containing legacy class and method data between squares might be missing.
final int group = matcher.groupCount() == 5 ? 4 : 3;
final String lineNumber = matcher.group(group);
final String fileName = matcher.group(group + 1);
trace.add(new StackTraceElement(javaClass, javaMethod, fileName,
Integer.valueOf(lineNumber)));
}
error.setStackTrace(trace.toArray(new StackTraceElement[trace.size()]));
}
throw error;
}
/**
* Scan a class for test methods, declared in the class.
*
* @param parentDescriptor
* the parent descriptor to register found test descriptors in
* @param javaClass
* the Java class to scan
* @param explicitMethods
* optional list of explicit test methods to execute, can be {@code null}
*/
private final static void addOptionally(final TestDescriptor parentDescriptor,
final Class<?> javaClass,
final Method explicitMethods)
{
final AbstractClassTestDescriptor instance = testAndCreate(javaClass, parentDescriptor,
explicitMethods);
if (instance != null)
{
parentDescriptor.addChild(instance);
}
}
/**
* Test if a method is annotated with the given annotation,
* and is not annotated with the {@link Disabled} annotation,
* and is public.
*
* Log methods, which are properly annotated, but are not public.
*
* @param method
* the method to test
* @param annoClass
* the annotation to test
*
* @return {@code true} is the method argument matches the criteria
*/
private final static boolean methodIsAnnotatedAndPublic(
final Method method,
final Class<? extends Annotation> annoClass)
{
final boolean isAnnotated = AnnotationUtils.isAnnotated(method, annoClass)
&& !AnnotationUtils.isAnnotated(method, Disabled.class);
if (!isAnnotated)
{
return false;
}
if (!Modifier.isPublic(method.getModifiers()))
{
LOG.fine("Private method cannot be used for unit testing: " + method);
return false;
}
return true;
}
/**
* Collect all methods annotated with the given class, but not annotated as disabled.
*
* @param clazz
* the class to scan for methods
* @param annoClass
* the annotation class to match
* @param topDown
* if {@code true}, then traverse class hierarchy top-down, otherwise traverse bottom-up.
*
* @return the list of methods found; never {@code null}
*/
private final static List<Method> collectAnnotatedMethods(final Class<?> clazz,
final Class<? extends Annotation> annoClass,
final boolean topDown)
{
try
{
return ReflectionUtils.findMethods(clazz,
m -> methodIsAnnotatedAndPublic(m, annoClass),
topDown ? ReflectionUtils.HierarchyTraversalMode.TOP_DOWN
: ReflectionUtils.HierarchyTraversalMode.BOTTOM_UP);
}
catch (final NoClassDefFoundError ncd)
{
// We get here if the class hierarchy is incomplete.
LOG.warning("Cannot load test class: " + clazz + ": " + ncd.getMessage());
return Collections.emptyList();
}
}
/**
* Run the provided code, convert legacy errors thrown to {@link AbstractAssertionFailedError}
* and throw the result.
* <p>
* This is a FWD extension to support test runners (e.g. Eclipse), which "knows" how to display
* {@link AbstractAssertionFailedError} specially.
*
* @param action
* the action to run, must return the legacy error thrown or {@code null}
*
* @throws ErrorConditionException
* remote code may throw this
* @throws AssertionFailedError
* remote legacy errors are converted to this
* exception type
*/
private final static void convertError(final Runnable action)
throws ErrorConditionException,
AssertionFailedError
{
convertError(action, "");
}
/**
* Find descriptor by its ID.
*
* @param testId
* unique ID
*
* @return the test descriptor.
*/
private final static AbstractFWDTestDescriptor getDescriptor(final UniqueId testId)
{
return context.get().testDescriptorRegistry.get(testId);
}
/**
* Register a new descriptor.
*
* @param descriptor
* the descriptor to register
*/
private final static void register(final AbstractFWDTestDescriptor descriptor)
{
final Map<UniqueId, AbstractFWDTestDescriptor> registry = context
.get().testDescriptorRegistry;
final UniqueId uniqueId = descriptor.getUniqueId();
// The same class can be collected more than once in case test suites are present
if (!registry.containsKey(uniqueId))
{
registry.put(uniqueId, descriptor);
}
}
/**
* Recursively scan a class for all test methods, including whose defined in the class
* and referenced classes, if the class is marked as test suite.
*
* @param engineDescriptor
* the engine descriptor to register found test descriptors in
* @param javaClass
* the Java class to scan
*
* @throws ErrorConditionException
* remote code may throw this
* @throws AssertionFailedError
* remote legacy errors are converted to this
* exception type
*/
private final static void scanClassRecursively(final TestDescriptor engineDescriptor,
final Class<?> javaClass)
throws ErrorConditionException,
AssertionFailedError
{
if (!isTestClassCandidate.test(javaClass))
{
// Class cannot relate to unit tests.
return;
}
addOptionally(engineDescriptor, javaClass, null);
// Add suites
if (AnnotationSupport.isAnnotated(javaClass, Suite.class))
{
final SelectClasses classesAnnotation = javaClass.getAnnotation(SelectClasses.class);
if (classesAnnotation != null)
{
for (final String className : classesAnnotation.value())
{
final Class<?> referencedClass = ObjectOps.resolveClass(className);
if (referencedClass != null)
{
scanClassRecursively(engineDescriptor, referencedClass);
}
else
{
LOG.severe("Cannot resolve legacy class " + className + " referenced in suite "
+ javaClass);
}
}
}
final SelectProcedures proceduresAnnotation = javaClass
.getAnnotation(SelectProcedures.class);
if (proceduresAnnotation != null)
{
for (final String procedureName : proceduresAnnotation.value())
{
try
{
final String className = SourceNameMapper.convertNameToClass(procedureName);
if (className == null)
{
throw new RuntimeException("Cannot resolve class for procedure: "
+ procedureName
+ " referenced from " + javaClass);
}
addOptionally(engineDescriptor, Class.forName(className), null);
}
catch (ClassNotFoundException e)
{
throw new RuntimeException("Cannot find class: " + e);
}
}
}
}
}
/**
* Test a Java class candidate for existent tests methods. If found, create a new class
* test descriptor.
*
* @param clazz
* the class under test
* @param parentDescriptor
* the parent descriptor
* @param optionalMethod
* the explicit method or {@code null} to scan the class for all test methods
* @return the class test descriptor or {@code null} if this class has no test methods.
*/
private final static AbstractClassTestDescriptor testAndCreate(final Class<?> clazz,
final TestDescriptor parentDescriptor,
final Method optionalMethod)
{
if (!isTestClassCandidate.test(clazz))
{
return null;
}
final List<Method> test = optionalMethod != null ? Lists.newArrayList(optionalMethod)
: collectAnnotatedMethods(clazz, Test.class, true);
final boolean isTestSuite = TestSuite.class.isAssignableFrom(clazz);
if (test.isEmpty() && !isTestSuite)
{
return null;
}
final List<Method> beforeEach = collectAnnotatedMethods(clazz, BeforeEach.class, true);
final List<Method> afterEach = collectAnnotatedMethods(clazz, AfterEach.class, false);
final List<Method> beforeAll = collectAnnotatedMethods(clazz, BeforeAll.class, true);
final List<Method> afterAll = collectAnnotatedMethods(clazz, AfterAll.class, false);
final AbstractClassTestDescriptor classDescriptor;
if (BaseObject.class.isAssignableFrom(clazz))
{
// Legacy class, either ABLUnit or OEUnit
classDescriptor = new FWDClassTestDescriptor(clazz, parentDescriptor, test, beforeEach, afterEach,
beforeAll, afterAll, null);
if (isTestSuite)
{
final Class c = clazz;
BlockManager.doBlock("method-caller", new Block((Body) () -> {
final object<? extends TestSuite> inst = new ObjectVar<>(c);
inst.assign(ObjectOps.newInstance(c));
final TestSuite suite = inst.ref();
for (final _BaseObject_ childTestInstance : suite.getTests())
{
addOptionally(classDescriptor, childTestInstance.getClass(), null);
}
}));
}
}
else
{
// Legacy ABLUnit procedure
classDescriptor = FWDDescriptorFactory.newProcedureTestDescriptor(clazz, parentDescriptor, test,
beforeEach, afterEach, beforeAll, afterAll);
}
register(classDescriptor);
for (final TestDescriptor child : classDescriptor.getChildren())
{
register((AbstractFWDTestDescriptor) child);
}
return classDescriptor;
}
/**
* Context local work area.
*/
private static class WorkArea
{
/**
* Test descriptors registered by their UIDs.
*/
private final Map<UniqueId, AbstractFWDTestDescriptor> testDescriptorRegistry = new HashMap<>();
/**
* The number of root test descriptors which were not processed yet.
*/
private int unprocessed;
}
}