/*
** Module   : Driver.java
** Abstract : thread that reads tests from a job queue and executes them
**
** Copyright (c) 2009-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090122 Thread that reads tests from a job queue and executes them.
** 002 CA  20210827 Refactored to allow other test types, beside terminal (which can be executed from the same 
**                  test suite).  Added SOAP and REST web service tests.
** 003 CA  20210906 The executors need to be built when the thread starts, as they are unique instances for 
**                  each thread.
** 004 TJD 20220803 Added logging, updated for Java 11
** 005 TJD 20220808 Changed log format, added option to abort TestSet
** 006 TJD 20220815 Abort anytime if RC failed
** 007 TJD 20220817 Implement test skip list feature, logging tweaks
** 008 TJD 20230119 Terminate thread when in backout fails to prevent further failures, Allow restarting of 
**                  Driver threads
*/

/*
** 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 should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.harness;

import java.util.*;
import java.util.concurrent.*;

import com.goldencode.harness.test.*;
import com.goldencode.util.*;


/**
 * Thread that reads tests from a job queue and executes them.
 * <p>
 * Implements the core logic of a session worker thread. This provides a
 * single thread that reads from the job queue (which provides tests to
 * execute) and executes each test in order against a specific target back
 * end system.
 */
public class Driver
extends Thread
{
   /** Job queue containing the tests to run. */
   private JobQueue<Test> queue = null;
   
   /** Object to notify when complete. */
   private Phaser phaser = null;

   /** Mapping of test executors based on their type. */
   private Map<Class<TestExecutor>, TestExecutor> executors;

   /** The driver's configurations, to build the executors when the thread starts. .*/
   private Map<String, SessionSettings> cfgs;

   /** Object in which to store the details of non-testing failures. */
   private FailureNotification rc;
   
   /**
    * Construct an instance of a worker thread and establish the connection
    * to the back-end. This includes the creation of the terminal emulator
    * instance which is used for test interaction.
    *
    * @param    cfgs
    *           The driver's configurations.
    * @param    queue
    *           Job queue from which tests to be executed are obtained.
    * @param    phaser
    *           Object to notify when complete.
    * @param    rc
    *           Object in which to store the details of non-testing failures.
    */
   public Driver(Map<String, SessionSettings> cfgs, 
                 JobQueue<Test>               queue, 
                 Phaser                       phaser, 
                 FailureNotification          rc)
   {
      this.queue = queue;
      this.phaser = phaser;
      this.cfgs = cfgs;
      this.rc = rc;
   }
   
   /**
    * Execute the specified test using its configured executor.
    * 
    * @param    test
    *           The test to execute.
    */
   public void execute(Testable test)
   {
      executors.get(test.getExecutor()).execute(test);
   }
   
   /**
    * Execute tests obtained from the job queue.
    */
   public void run()
   {
      try
      {
         LogHelper.finer("DRIVERSTART|" + this.getName());
         executors = buildExecutors(cfgs, rc);
         
         if (!pre())
         {
            LogHelper.warn(this.getName() + " TestExecutor pre-condition failed, aborting TestPlan");
            rc.setResult(ExitCode.PRE_FAILED);
            rc.setDetails(this.getName() + " TestExecutor pre-condition failed failed");
            queue.abort();
         }
         
         loop:
         while (true)
         {
            Test test = queue.dequeue();
            
            if (test == null)
            {
               LogHelper.finer(this.getName() + " Queue is empty, exiting");
               break;
            }
            
            if (rc.isFailed() && Harness.isFailOnRCFailure())
            {
               LogHelper.warn(this.getName() + "RC failed, stopping TestSet");
               break;
            }
            // check it test is to be skipped
            if (Harness.isTestOnSkipList(test.getName()) || !Harness.isTestOnWhiteList(test.getName()))
            {
               // Skipped tests are considered as passed
               LogHelper.info("Skipping test: "+ test.getName());
               test.setStatus(Status.PASSED, "Test skipped (on skip list)");
               continue loop;
            }
            
            test.setDriver(this);
            LogHelper.finer("DRIVER|" + this.getName() + "|" + test.getName() + "/" 
                           + test.getDependency().toString() + "| execute");
            test.execute();
            
            Status status = test.getStatus();
            LogHelper.finer("RESULT|" + this.getName() + "|" + test.getName() + "|" + status.toString() 
                           + "|" + (status.equals(Status.FAILED)?" reason: " + test.getReason():""));
            if (status == Status.FAILED)
            {
               FailureMode fail = test.getFailureMode();
               
               switch (fail)
               {
                  case IGNORE:
                     continue loop;
                     
                  case ABORT_THREAD:
                     break loop;
                     
                  case BACKOUT:
                     Test backout = test.getBackout();
                     
                     if (backout != null)
                     {
                        backout.setDriver(this);
                        backout.execute();
                        Status backoutStatus = backout.getStatus();
                        if (backoutStatus != Status.PASSED && Harness.isDebugMode())
                        {
                           LogHelper.warn("BACKOUTFAIL|" + this.getName() + "|" + test.getName() 
                                          + "|" + test.getDependency().toString() 
                                          + "| Backout failed or not run, terminating thread");
                           break loop;
                        }
                     }
                     
                     break;
               }
            }
         }
      }
      catch (Throwable e)
      {
         LogHelper.warn("DRIVERERROR|" + this.getName() + "| Exception:", e);
      }
      finally
      {
         LogHelper.finer("DRIVEREND|" + this.getName() + "| End");
         post();
         
         phaser.arriveAndDeregister();
      }
   }
   
   /**
    * Check all {@link #executors}'s {@link TestExecutor#pre() pre-condition}.
    * 
    * @return   <code>true</code> if all executors have initialized properly.
    */
   protected boolean pre()
   {
      int ok = 0;
      
      for (TestExecutor ex : executors.values())
      {
         if (ex.pre())
         {
            ok = ok + 1;
         }
      }
      
      return ok == executors.size();
   }

   /**
    * Run the post-processing for all executors. 
    */
   protected void post()
   {
      for (TestExecutor ex : executors.values())
      {
         ex.post();
      }
   }
   
   /**
    * Build the executors for all configured test types.
    * 
    * @param    cfgs
    *           The driver's configurations.
    * @param    rc
    *           Object in which to store the details of non-testing failures.
    *           
    * @return   A mapping for each test type to its executor.
    */
   private Map<Class<TestExecutor>, TestExecutor> buildExecutors(Map<String, SessionSettings> cfgs,
                                                                 FailureNotification          rc)
   {
      Map<Class<TestExecutor>, TestExecutor> executors = new IdentityHashMap<>();
      
      // add the default executor
      executors.put(TestExecutor.class, new TestExecutor());
      
      for (SessionSettings cfg : cfgs.values())
      {
         TestExecutor executor = cfg.getExecutor(rc);
         
         executors.put((Class<TestExecutor>) executor.getClass(), executor);
      }
      
      return executors;
   }
}
