# FWDTestEngine.java

    /*
    ** Module   : FWDTestEngine.java
    ** Abstract : Test engine.
    **
    ** Copyright (c) 2023-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------Description----------------------
    ** 001 VVT 20230318 Created initial version.
    ** 002 VVT 20230418 All JUnit5 discovery selector types are now passed to the server
    **                  for possible future extensions.
    ** 003 VVT 20230428 Fixed error propagation (See #3827-350) and source formatting.
    ** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 005 CA  20240311 Allow server:clientConfig:outputToFile to be set for the FWD unit test client, too.
    ** 006 VVT 20240618 Forced ConsoleHelper.terminate() after test engine initialization failure. See #8529-7.
    ** 007 GBB 20240709 Hard-coded config name replaced by ConfigItem constant.
    **                  Diagnostics supplier moved to processTemporaryClient.
    ** 008 VVT 20240703 Fixed: ClientCore.loadNativeLibrary() was called too late. See #8921.
    ** 009 GBB 20240826 Removing single client.
    ** 010 VVT 20250217 discover() method fixed: initialization failure must not throw exceptions in order not
    **                  to abort the test running process and let other test engines to work. See #9649.
    */
    /*
    ** 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 org.junit.platform.engine.TestExecutionResult.*;

    import java.net.*;
    import java.util.logging.*;

    import com.goldencode.p2j.util.*;
    import org.junit.platform.commons.util.*;
    import org.junit.platform.engine.*;
    import org.junit.platform.engine.discovery.*;
    import org.junit.platform.engine.support.hierarchical.*;
    import org.junit.platform.engine.support.hierarchical.ThrowableCollector.*;
    import org.opentest4j.*;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.ui.client.chui.driver.console.*;
    import com.goldencode.p2j.ui.client.driver.*;

    /**
     * FWD Test engine.
     *
     */
    public class FWDTestEngine
    extends HierarchicalTestEngine<FWDEngineExecutionContext>
    {

       /**
        * Engine state
        *
        */
       private enum State
       {
          /**
           * Engine was initialized successfully.
           */
          INITIALIZED,

          /**
           * Engine initialization failed, engine is inoperable
           *
           * Inoperable engines will never discover any tests.
           *
           * This feature gives other test engines a chance to work.
           */
          INOPERABLE,

          /**
           * Engine just created and is uninitialized.
           */
          NONE
       }

       /**
        * Logger for this test engine
        */
       final static Logger LOG = Logger.getLogger(FWDTestEngine.class.getName());

       /**
        * The name of an optional JUnit5 configuration parameter which contains FWD
        * configuration file path.
        */
       private static final String CFG_FWD_CONFIG_FILE = "fwd_config_file";

       /**
        * Default FWD configuration file value.
        */
       private static final String DEFAULT_FWD_CONFIG_FILE = "unit_test.xml";

       /**
        * Engine initialization state: initially {@link State#NONE}, when after initialization attempt
        * changes either to either {@link State#INITIALIZED} or {@link State#INOPERABLE},
        * depending on whether initialization was successful or not.
        *
        * Keeping state prevents multiple engine initialization attempts.
        */
       private static State state = State.NONE;

       /**
        * Unit test support on the server proxy instance.
        */
       private static UnitTestEngine unitTestSupport;

       /**
        * Initialize the engine.
        *
        * @param  fwdConfigFile
        *         FWD client config file, never null
        *
        * @return {@code true} if initialization was successful.
        */
       private static boolean initialize(final String fwdConfigFile)
       {

          ClientCore.loadNativeLibrary();

          // process configuration. In most use cases (e.g. running from IDE), it is not possible to pass
          // any command line arguments: so only config files can be used for the purpose.
          try
          {
             final BootstrapConfig bc = new BootstrapConfig(fwdConfigFile, null, null, null);

             final String uuid = bc.getString(ConfigItem.SPAWNER_UUID, null);

             // save off the UI driver so the same instance can be used across all
             // iterations of the core loop
             ScreenDriver<?> driver = (uuid == null) ? null : ClientCore.processTemporaryClient(uuid,
                                                                                                bc,
                                                                                                () -> "");
             final ClientCore.InitStruct is = new ClientCore.InitStruct();
             ClientCore.setOutputToFile(bc);
             is.driver = driver;
             ClientCore.initialize(is, bc, uuid);

             unitTestSupport = (UnitTestEngine) RemoteObject
                      .obtainNetworkInstance(UnitTestEngine.class, null);

             // Do server-side initialization
             unitTestSupport.initialize();

             return true;
          }
          catch (Throwable thr)
          {
             ConsoleHelper.terminate();

             LOG.warning("Test engine cannot initialize and will be disabled: " + thr);

             return false;
          }
       }

       /**
        * Discover tests according to the supplied {@link EngineDiscoveryRequest}.
        * <p>
        * The supplied {@link UniqueId} must be used as the unique ID of the
        * returned root {@link TestDescriptor}. In addition, the {@code UniqueId}
        * must be used to create unique IDs for children of the root's descriptor
        * by calling {@link UniqueId#append}.
        *
        * @param  discoveryRequest the discovery request; never {@code null}
        * @param  uniqueId the unique ID to be used for this test engine's
        *         {@code TestDescriptor}; never {@code null}
        *
        * @return the root {@code TestDescriptor} of this engine, typically an
        *         instance of {@code EngineDescriptor}
        *
        * @see    org.junit.platform.engine.support.descriptor.EngineDescriptor
        */
       @Override
       public TestDescriptor discover(final EngineDiscoveryRequest discoveryRequest,
                                      final UniqueId uniqueId)
       {
          switch (state)
          {
             case INITIALIZED:
                break;

             case NONE:
                final ConfigurationParameters configurationParameters = discoveryRequest
                         .getConfigurationParameters();
                try
                {
                   if (initialize(configurationParameters.get(CFG_FWD_CONFIG_FILE)
                            .orElse(DEFAULT_FWD_CONFIG_FILE)))
                   {
                      state = State.INITIALIZED;
                      break;
                   }
                }
                catch (@SuppressWarnings("unused") final Throwable t)
                {
                   // no-op, continue as inoperable
                }

                state = State.INOPERABLE;
                //$FALL-THROUGH$

             case INOPERABLE:
                return new NoopDescriptor(UniqueId.forEngine(getId()), "No-op");

          }

          return unitTestSupport.discover(uniqueId,
                   discoveryRequest.getSelectorsByType(ClasspathRootSelector.class).stream()
                            .map(ClasspathRootSelector::getClasspathRoot).toArray(URI[]::new),
                   discoveryRequest.getSelectorsByType(PackageSelector.class).stream()
                            .map(PackageSelector::getPackageName).toArray(String[]::new),
                   discoveryRequest.getSelectorsByType(ClassSelector.class).stream()
                            .map(ClassSelector::getClassName).toArray(String[]::new),
                   discoveryRequest.getSelectorsByType(MethodSelector.class).stream()
                            .map(s -> s.getClassName() + "#" + s.getMethodName())
                            .toArray(String[]::new),
                   discoveryRequest.getSelectorsByType(UriSelector.class).stream()
                            .map(s -> s.getUri())
                            .toArray(URI[]::new),
                   discoveryRequest.getSelectorsByType(FileSelector.class).stream()
                            .map(s -> s.getRawPath())
                            .toArray(String[]::new),
                   discoveryRequest.getSelectorsByType(DirectorySelector.class).stream()
                            .map(s -> s.getRawPath())
                            .toArray(String[]::new),
                   discoveryRequest.getSelectorsByType(ModuleSelector.class).stream()
                            .map(s -> s.getModuleName())
                            .toArray(String[]::new),
                   discoveryRequest.getSelectorsByType(ClasspathResourceSelector.class).stream()
                            .map(s -> s.getClasspathResourceName())
                            .toArray(String[]::new),
                   discoveryRequest.getSelectorsByType(IterationSelector.class).stream()
                            .map(s -> s.getIterationIndices())
                            .toArray(String[]::new)

          );
       }

       /**
        * Get the ID that uniquely identifies this test engine.
        * <p>
        * Each test engine must provide a unique ID. For example, JUnit Vintage
        * and JUnit Jupiter use {@code "junit-vintage"} and {@code "junit-jupiter"},
        * respectively. When in doubt, you may use the fully qualified name of your
        * custom {@code TestEngine} implementation class.
        */
       @Override
       public String getId()
       {
          // Note: "junit-fwd" would be more natural, but the "junit-" prefix
          // is reserved by JUnit5
          return "fwd-junit";
       }

       /**
        * Create the initial execution context for executing the supplied
        * {@linkplain ExecutionRequest request}.
        *
        * @param  request the request about to be executed
        *
        * @return the initial context that will be passed to nodes in the hierarchy
        */
       @Override
       protected FWDEngineExecutionContext createExecutionContext(ExecutionRequest request)
       {
          return new FWDEngineExecutionContext(unitTestSupport);
       }

       /**
        * Create the {@linkplain org.junit.platform.engine.support.hierarchical.ThrowableCollector.Factory factory} for creating
        * {@link ThrowableCollector} instances used to handle exceptions that occur
        * during execution of this engine's tests.
        * <p>
        * An engine may use the information in the supplied <em>request</em>
        * such as the contained
        * {@linkplain ExecutionRequest#getConfigurationParameters() configuration parameters}
        * to decide what kind of factory to return or how to configure it.
        * <p>
        * By default, this method returns a factory that always creates instances of
        * {@link OpenTest4JAwareThrowableCollector}.
        *
        * @param request the request about to be executed
        *
        * @see   OpenTest4JAwareThrowableCollector
        * @see   ThrowableCollector
        */
       @Override
       protected Factory createThrowableCollectorFactory(ExecutionRequest request)
       {
          return FWDThrowableCollector::new;
       }

       /**
        * {@link ThrowableCollector} which handles {@link RuntimeException}s which wrap
        * {@link AssertionFailedError} in the exception cause chain specially.
        *
        * Note: unfortunately, the data code in this class duplicates most of the parent class
        * data and code.
        */
       public final static class FWDThrowableCollector
       extends OpenTest4JAwareThrowableCollector
       {
          /**
           * Store the first {@link Throwable} thrown, all subsequent
           * throwables if any, are added to the first one as suppressed.
           */
          private Throwable throwable;

          /**
           * Default constructor.
           */
          public FWDThrowableCollector()
          {
             // no-op
          }

          /**
           * Assert that this {@code ThrowableCollector} is <em>empty</em> (i.e.,
           * has not collected any {@code Throwables}).
           * <p>
           * If this collector is not empty, the first collected {@code Throwable}
           * will be thrown with any additional {@code Throwables}
           * {@linkplain Throwable#addSuppressed(Throwable) suppressed} in the
           * first {@code Throwable}. Note, however, that the {@code Throwable}
           * will not be wrapped. Rather, it will be
           * {@linkplain ExceptionUtils#throwAsUncheckedException masked}
           * as an unchecked exception.
           *
           * @see #getThrowable()
           * @see ExceptionUtils#throwAsUncheckedException(Throwable)
           */
          @Override
          public void assertEmpty()
          {
             if (!isEmpty())
             {
                ExceptionUtils.throwAsUncheckedException(throwable);
             }
          }

          /**
           * Execute the supplied {@link Executable} and collect any {@link Throwable}
           * thrown during the execution.
           * <p>
           * If the {@code Executable} throws an <em>unrecoverable</em> exception
           * &mdash; for example, an {@link OutOfMemoryError} &mdash; this method will
           * rethrow it.
           *
           * @param executable the {@code Executable} to execute
           *
           * @see   #assertEmpty()
           */
          @Override
          public void execute(final Executable executable)
          {
             try
             {
                executable.execute();
             }
             catch (final Throwable t)
             {
                UnrecoverableExceptions.rethrowIfUnrecoverable(t);

                final Throwable cause = t.getCause();

                final Throwable effThrowable = cause instanceof AssertionFailedError ? cause : t;
                if (throwable == null)
                {
                   throwable = effThrowable;
                }
                else if (throwable != effThrowable)
                {
                   throwable.addSuppressed(effThrowable);
                }
             }
          }

          /**
           * Get the first {@link Throwable} collected by this
           * {@code ThrowableCollector}.
           * <p>
           * If this collector is not empty, the first collected {@code Throwable}
           * will be returned with any additional {@code Throwables}
           * {@linkplain Throwable#addSuppressed(Throwable) suppressed} in the
           * first {@code Throwable}.
           * <p>
           * If the first collected {@code Throwable} <em>aborted</em> execution
           * and at least one later collected {@code Throwable} <em>failed</em>
           * execution, the first <em>failing</em> {@code Throwable} will be returned
           * with the previous <em>aborting</em> and any additional {@code Throwables}
           * {@linkplain Throwable#addSuppressed(Throwable) suppressed} inside.
           *
           * @return the first collected {@code Throwable} or {@code null} if this
           *         {@code ThrowableCollector} is empty
           *
           * @see    #isEmpty()
           * @see    #assertEmpty()
           */
          @Override
          public Throwable getThrowable()
          {
             return throwable;
          }

          /**
           * Determine if this {@code ThrowableCollector} is <em>empty</em> (i.e.,
           * has not collected any {@code Throwables}).
           */
          @Override
          public boolean isEmpty()
          {
             return throwable == null;
          }

          /**
           * Determine if this {@code ThrowableCollector} is <em>not empty</em> (i.e.,
           * has collected at least one {@code Throwable}).
           */
          @Override
          public boolean isNotEmpty()
          {
             return throwable != null;
          }

          /**
           * Convert the collected {@link Throwable Throwables} into a {@link TestExecutionResult}.
           *
           * @return {@linkplain TestExecutionResult#aborted aborted} if the collected
           *         {@code Throwable} <em>aborted</em> execution;
           *         {@linkplain TestExecutionResult#failed failed} if it <em>failed</em>
           *         execution; and {@linkplain TestExecutionResult#successful successful}
           *         otherwise
           */
          @Override
          public TestExecutionResult toTestExecutionResult()
          {
             if (isEmpty())
             {
                return successful();
             }

             if (throwable instanceof TestAbortedException)
             {
                return aborted(throwable);
             }

             return failed(throwable);
          }

       }

    }