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