# 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;
       }

    }