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