/*
** Module   : Driver.java
** Abstract : thread that reads tests from a job queue and executes them
**
** Copyright (c) 2009-2021, 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.
*/

/*
** 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.*;


/**
 * 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 CountDownLatch latch = 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    latch
    *           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, 
                 CountDownLatch               latch, 
                 FailureNotification          rc)
   {
      this.queue = queue;
      this.latch = latch;
      
      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
      {
         executors = buildExecutors(cfgs, rc);
         
         if (!pre())
         {
            return;
         }
         
         loop:
         while (true)
         {
            Test test = queue.dequeue();
            
            if (test == null)
            {
               break;
            }
            
            test.setDriver(this);
            test.execute();
            
            Status status = test.getStatus();
            
            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();
                     }
                     
                     break;
               }
            }
         }
      }
      
      finally
      {
         post();
         
         latch.countDown();
      }
   }
   
   /**
    * 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;
   }
}
