# FWDClassTestDescriptor.java

    /*
    ** Module   : FWDClassTestDescriptor.java
    ** Abstract : Test descriptor for a legacy class.
    **
    ** Copyright (c) 2023-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description---------------------------------
    ** 001 VVT 20230318 Created initial version.
    ** 002 VVT 20230412 OEUnit support added. See #6237.
    ** 003 VVT 20230418 DataProvider and Fixture method name matching fixed:
    **                  legacy names must be used instead of Java method names.
    ** 004 VVT 20230428 Fixed error propagation (See #3827-350) and source formatting.
    **                  Full paths to legacy procedures and fully qualified legacy class names are
    **                  now used as JUnit5 test display names. See #3827-360, item 3.
    **                  Minor non-functional fixes.
    ** 005 CA  20231221 Do not use UndoableFactory.object or TypeFactory.object for getting an ObjectVar instance;
    **                  instead, create the instance as ObjectVar as needed.
    ** 006 VVT 20240325 Javadocs updated for getTestObject(). See #8406.
    **                  Unused 'test', 'beforeEach' and 'afterEach' constructor parameters removed.
    **                  The @Test methods ordered before method test descriptors are created. See #8440.
    ** 007 VVT 20240408 Minor style fixes. See #8406-70.
    */
    /*
    ** 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 static com.goldencode.p2j.testengine.TestExecutionSupport.doInBlock;

    import java.lang.reflect.*;
    import java.util.*;

    import org.junit.platform.commons.support.*;
    import org.junit.platform.engine.*;

    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.oo.oeunit.data.*;
    import com.goldencode.p2j.testengine.api.*;
    import com.goldencode.p2j.util.*;

    /**
     * Test descriptor for a legacy class.
     */
    public class FWDClassTestDescriptor
    extends AbstractClassTestDescriptor
    {

       /**
        * Test class instance to invoke the method on.
        * Unlike JUnit5, the same instance is used to run all test methods
        * for a class.
        */
       transient object<?> testInstance;

       /**
        * Compute the display name as the fully qualified legacy class name.
        *
        * @param clazz
        *        the Java class
        *
        * @return see above
        */
       private final static String displayName(final Class<?> clazz)
       {
          return ObjectOps.getLegacyName((Class<? extends _BaseObject_>) clazz);
       }

       /**
        * Test if a method marked as using a fixture, if yes find the referenced fixture method.
        * <p>
        * Throw an error if the fixture cannot be found
        *
        * @param  clazz
        *         the class of the test method
        * @param  test
        *         the {@link Test} annotation
        *
        * @return the method found or {@code null} if the argument method does not use a data
        *         provider or no unique matching provider found
        */
       private static Method lookupFixture(final Class<?> clazz, final Test test)
       {
          final String fixture = test.fixture();
          if (fixture.isEmpty())
          {
             return null;
          }

          final List<Method> matchedMethods = ReflectionSupport.findMethods(clazz, m -> {
             final Fixture fixtureAnnotation = m.getAnnotation(Fixture.class);
             if (fixtureAnnotation == null)
             {
                // The method *must* be annotated to be considered a candidate
                return false;
             }

             // From now on we consider any method feature mismatch as an error.
             if ((m.getModifiers() & Modifier.PUBLIC) == 0)
             {
                ErrorManager.recordOrShowError(-1, "Fixture method should be public");
                return false;
             }

             if (!object.class.isAssignableFrom(m.getReturnType()))
             {
                ErrorManager.recordOrShowError(-1, "Fixture method should return an object");
                return false;
             }

             // Assert the annotated method has no parameters
             if (m.getParameterCount() > 0)
             {
                ErrorManager.recordOrShowError(-1, "Fixture method should have no arguments");
                return false;
             }

             // Get the target method name
             final String name = fixtureAnnotation.name();
             final String effMethodName;
             if (name.isEmpty())
             {
                final LegacySignature legacySignature = m.getDeclaredAnnotation(LegacySignature.class);
                if (legacySignature == null)
                {
                   return false;
                }
                effMethodName = legacySignature.name();
             }
             else
             {
                effMethodName = name;
             }

             return fixture.equalsIgnoreCase(effMethodName);
          },
                   // This should be considered as FWD extensions: class hierarchy is not searched
                   // in the original OEUnit.
                   HierarchyTraversalMode.BOTTOM_UP);
          switch (matchedMethods.size())
          {
             case 0:
                return null;
             case 1:
                return matchedMethods.get(0);
             default:
                // There Can Be Only One!
                ErrorManager.recordOrShowError(-1, "Multiple matches found for the fixture method");
                return null;
          }
       }

       /**
        * Default constructor, required for de-serialization
        */
       public FWDClassTestDescriptor()
       {
          // no-op
       }

       /**
        * The constructor.
        *
        * @param clazz
        *        the Java class
        * @param parent
        *        the parent descriptor
        * @param test
        *        class methods marked as tests
        * @param beforeEach
        *        class methods marked to execute before each test
        * @param afterEach
        *        class methods marked to execute after each test
        * @param beforeAll
        *        class methods marked to execute before all tests
        * @param afterAll
        *        class methods marked to execute after all tests
        * @param instance
        *        the test instance if it is known at the moment, otherwise {@code null}
        */
       FWDClassTestDescriptor(final Class clazz,
                              final TestDescriptor parent,
                              final List<Method> test,
                              final List<Method> beforeEach,
                              final List<Method> afterEach,
                              final List<Method> beforeAll,
                              final List<Method> afterAll,
                              final object<?> instance)
       {
          super(clazz, displayName(clazz), parent, beforeAll, afterAll);

          testInstance = instance;

          for (final Method m : sort(test))
          {
             // Create method descriptors for "each" type methods
             final Test annotation = m.getAnnotation(Test.class);
             final String dataProvider = annotation.dataProvider();

             // For a test marked with dataProvider, create as many descriptor,
             // as the data provider parameter record count
             if (!dataProvider.isEmpty())
             {
                /**
                 * Test if a method uses data provider, if yes lookup the data provider method.
                 */
                final List<Method> matchedMethods = ReflectionSupport.findMethods(clazz,
                         candidate -> {
                            final com.goldencode.p2j.testengine.api.DataProvider dataProviderAnnotation = candidate
                                     .getAnnotation(DataProvider.class);
                            if (dataProviderAnnotation == null)
                            {
                               // The method *must* be annotated to be considered a candidate
                               return false;
                            }

                            // From now on we consider any method feature mismatch as an error.

                            // Assert the method is public
                            if ((candidate.getModifiers() & Modifier.PUBLIC) == 0)
                            {
                               ErrorManager.recordOrShowError(-1,
                                        "Data provider method should be public");
                               return false;
                            }

                            // Assert the method returns a DataProvider
                            if (!object.class.isAssignableFrom(candidate.getReturnType()))
                            {
                               ErrorManager.recordOrShowError(-1,
                                        "Data provider method should return an object");
                               return false;
                            }

                            // Assert the annotated method has no parameters
                            if (candidate.getParameterCount() > 0)
                            {
                               ErrorManager.recordOrShowError(-1,
                                        "Data provider method should have no arguments");
                               return false;
                            }

                            // Get the target method name
                            final String name = dataProviderAnnotation.name();
                            final String effMethodName;
                            if (name.isEmpty())
                            {
                               final LegacySignature legacySignature = candidate
                                        .getDeclaredAnnotation(LegacySignature.class);
                               if (legacySignature == null)
                               {
                                  return false;
                               }
                               effMethodName = legacySignature.name();
                            }
                            else
                            {
                               effMethodName = name;
                            }

                            return dataProvider.equalsIgnoreCase(effMethodName);
                         },
                         // This should be considered as FWD extensions: class hierarchy is not searched
                         // in the original OEUnit.
                         HierarchyTraversalMode.BOTTOM_UP);
                switch (matchedMethods.size())
                {
                   case 0:
                      ErrorManager.recordOrShowError(-1, "No matching data provider method found");
                      break;
                   case 1:
                   {
                      final Method dpMethod = matchedMethods.get(0);

                      prepareImpl();
                      beforeImpl();

                      final BaseDataType result = TestExecutionSupport.callClassMethod(
                               dpMethod,
                               testInstance, null);

                      final object dataProviderObject = (object) result;
                      final _BaseObject_ dpRef = dataProviderObject.ref();
                      final DataProvider_ dp = (DataProvider_) dpRef;

                      final logical opResult = dp.moveFirst();
                      final String expected = annotation.expected();
                      final String methodName = m.getName();

                      // Create artificial record numbers to create test descriptor names
                      int serial = 1;
                      while (opResult.booleanValue())
                      {
                         final CallParameter[] parameters = dp.getParameterList().ref()
                                  .getParameters();
                         final String name = serial == 1 ? methodName
                                  : (methodName + "/" + serial);
                         final UniqueId id = getUniqueId().append("method", name);
                         addChild(new FWDClassMethodTestDescriptor(id, name, clazz, m, parameters,
                                  null, beforeEach, afterEach, expected));
                         opResult.assign(dp.moveNext());
                         serial++;
                      }
                      afterImpl();
                      break;
                   }

                   default:
                      // There Can Be Only One!
                      ErrorManager.recordOrShowError(-1,
                               "Multiple matches found for the data provider method");
                }
             }
             else
             {
                addChild(new FWDClassMethodTestDescriptor(this, clazz, m, null, lookupFixture(clazz, annotation),
                         beforeEach, afterEach, annotation.expected()));
             }
          }

       }

       /**
        * Clean up the supplied {@code context} after execution implementation.
        * <p>
        * Reset the test class instance.
        *
        * @see #prepare
        */
       @Override
       public void cleanUpImpl()
       {
          // TODO do we need to do any additional cleanup?
          testInstance = null;
       }

       /**
        * Prepare the supplied {@code context} prior to execution implementation.
        *
        * @see #cleanUpImpl
        */
       @Override
       public void prepareImpl()
       {
          if (testInstance == null)
          {
             final object<?> inst = new ObjectVar(clazz);
             inst.assign(doInBlock(() -> ObjectOps.newInstance(clazz)));
             testInstance = inst;
          }
       }

       /**
        * Call 4gl class instance or static method.
        *
        * @param  method
        *         the Java method to call
        * @param  o
        *         the class instance to invoke the method on, or {@code null} to call a static method
        * @param  args
        *         method call arguments or {@code null} for no parameters
        *
        * @return the result of the call or {@code null} if method returns VOID
        */
       @Override
       protected BaseDataType callMethodImpl(final Method method,
                                             final Object o,
                                             final CallParameter[] args)
       {
          return TestExecutionSupport.callClassMethod(method, (object<?>) o, args);
       }

       /**
        * Get test object. The test object is either a legacy class instance if tests are implemented
        * as legacy class methods or a persistent procedure instance it tests are implemented as
        * internal procedures.
        * <p>
        * The value should be {@code null} if the test method is a static legacy class method.
        *
        * @return the test object or {@code null}
        */
       @Override
       protected Object getTestObject()
       {
          return testInstance;
       }
    }