/*
** Module   : TestSet.java
** Abstract : stores the data needed to execute a set of tests on a given
**            target host
**
** Copyright (c) 2009-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090122 Stores the data needed to execute a set of tests on a given target host.
** 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 TJD 20220803 Added logging, updated for Java 11
** 004 TJD 20220808 Changed log format, added option to abort TestSet
** 004 TJD 20220815 Logging tweaks
** 005 TJD 20230119 Updated logging data, 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.*;

/**
 * Stores the data needed to execute a set of tests on a given target host.
 */
public class TestSet
extends Report
implements Runnable
{
   /** Configuration required to access the target system. */
   private Map<String, SessionSettings> cfgs = null;
   
   /** Number of threads which will concurrently run the tests. */
   private int threads = 1;
   
   /** Job queue from which test elements are obtained. */
   private JobQueue<Test> queue = null;
   
   /** Tests to execute. */
   private List<Test> tests = null;
   
   /** Store shared, scoped resources. */
   private ResourcePool pool = null;
   
   /** Semaphore to notify when complete. */
   private Semaphore sem = null;
   
   /** Object in which to log non-testing failures. */
   private FailureNotification rc = null;
   
   /**
    * Construct a default instance. The contained data must be set subsequently
    * using the explicit setter methods.
    */                    
   public TestSet()
   {
   }
   
   /**
    * Construct an instance containing the given data.
    *
    * @param    name
    *           Short name of the test set.
    * @param    descr
    *           Verbose description of the test set.
    * @param    cfgs
    *           Configuration required to access the target system.
    * @param    threads
    *           Number of threads which will concurrently run the tests. If
    *           non-positive, the number of threads will default to 1.
    * @param    queues
    *           List of sub-queues from which test elements are obtained.
    * @param    tests
    *           Tests to execute.
    * @param    pool
    *           Stores scoped named resources.
    */                    
   public TestSet(String                       name, 
                  String                       descr,
                  Map<String, SessionSettings> cfgs,
                  int                          threads,
                  List<SubQueue<Test>>         queues,
                  List<Test>                   tests,
                  ResourcePool                 pool)
   {
      super(name, descr);
      this.cfgs    = cfgs;
      this.threads = (threads > 0) ? threads : 1;
      this.queue   = new JobQueue<Test>(queues);
      this.tests   = tests;
      this.pool    = pool;
   }
   
   /**
    * Get the configurations to access the target system.
    *
    * @return   The configuration or <code>null</code> if none has been set.
    */
   public Map<String, SessionSettings> getCfgs()
   {
      return cfgs;
   }
   
   /**
    * Get the number of threads to concurrently execute tests.
    *
    * @return   The number of threads.
    */
   public int getThreads()
   {
      return threads;
   }
   
   /**
    * Set the number of threads to concurrently execute tests.
    *
    * @param    threads
    *           Number of threads.
    */
   public void setThreads(int threads)
   {
      this.threads = threads;
   }
   
   /**
    * Get the list of tests to run.
    *
    * @return   The list of tests or <code>null</code> if none have been set.
    */
   public List<Test> getTests()
   {
      return tests;
   }
   
   /**
    * Set the list of tests to run.
    *
    * @param    tests
    *           The list of tests to run.
    */
   public void setTests(List<Test> tests)
   {
      this.tests = tests;
   }
   
   /**
    * Get the scoped resource pool.
    *
    * @return   The pool.
    */
   public ResourcePool getResourcePool()
   {
      return pool;
   }
   
   /**
    * Set the scoped resource pool.
    *
    * @param    pool
    *           The new pool.
    */
   public void setResourcePool(ResourcePool pool)
   {
      this.pool = pool;
   }
   
   /**
    * Get the number of sub-elements of the testable activity.
    *
    * @return   If the testable activity is a single atomic activity, then
    *           this will return 1. Otherwise, it will return the number of
    *           individual sub-elements to be executed in this activity.
    */
   public int getNumberOfElements()
   {
      return tests.size();
   }
   
   /**
    * Start the thread that executes this test set. See {@link #run}.
    *
    * @param    sem2
    *           Semaphore to notify when the test set completes processing.
    * @param    rc
    *           Object in which to store the details of non-testing failures.
    */
   public void execute(Semaphore sem2, FailureNotification rc)
   {
      this.sem = sem2;
      this.rc  = rc;

      if (cfgs == null || cfgs.isEmpty())
      {
         if (sem != null)
            sem.release();;
         
         rc.setResult(ExitCode.MISSING_CONTENT);
         rc.setDetails("Missing session configuration for test set execution.");
         return;
      }
      
      if (sem2 == null)
      {
         rc.setResult(ExitCode.MISSING_CONTENT);
         rc.setDetails("Missing semaphore for test set execution.");
         return;
      }
      
      // start a new thread for this set so that this method can return and
      // the caller can continue in parallel to test set execution
      Thread thread = new Thread(this);
      thread.setName(getName());
      thread.start();
   }
   
   /**
    * Worker to create the job queue, start all of the {@link Driver} threads
    * and execute the tests in this set.
    */
   public void run()
   {
      setThreadName(Thread.currentThread().getName());
      
      StringBuilder  sb     = new StringBuilder();
      Phaser         phaser = new Phaser();
      int num = 0;
      int threadIndex = 0;
      long startTime = System.currentTimeMillis(); 
      // start test set timer
      setStartTime(startTime);
      LogHelper.finer("TESTSETSTART|" + this.getName() + "| Starting testset");

      // start each of the driver threads (there will be at least 1 started)
      for (int i = 0; i < threads; i++)
      {
         if (phaser.register() == 0)
         {
            Driver driver = new Driver(cfgs, queue, phaser, rc);
            driver.setName(String.format("%s/%d", this.getName(), threadIndex++));
            driver.start();
         }
      }
      
      try
      {
         while (true)
         {
            try
            {
               // wait for each driver thread to finish
               phaser.awaitAdvanceInterruptibly(0, 1, TimeUnit.MINUTES);
               // all threads arrived, we are done
               break;
            }
            catch (TimeoutException e)
            {
               // we have timeout so we can check how many threads has already arrived
               if (!queue.isEmpty() && phaser.getRegisteredParties() < threads)
               {
                  for (int i = phaser.getRegisteredParties(); i < threads; i++)
                  {
                     if (phaser.register() == 0)
                     {
                        LogHelper.finer("DRIVERSTART|" + this.getName() + "| Starting new driver");
                        Driver driver = new Driver(cfgs, queue, phaser, rc);
                        driver.setName(String.format("%s/%d", this.getName(), threadIndex++));
                        driver.start();
                     }
                     else
                     {
                        LogHelper.finer("DRIVERSTART|" + this.getName() + "| Not starting new driver, phased out");
                     }
                  }
               }
            }
         }
      }
      
      catch (InterruptedException ie)
      {
         sb.append("test set thread interrupted before driver ")
           .append(" threads finished");
         num = 1;
      }
      
      // end test set timer
      long endTime = System.currentTimeMillis();
      setEndTime(endTime);
      LogHelper.finer("TESTSETEND|" + this.getName() + "|" + (endTime - startTime) + "| Ending testset");

      num = checkResults(tests, sb, num);
      
      setStatus((num > 0) ? Status.FAILED : Status.PASSED,
                (sb.length() > 0) ? sb.toString() : null);
      
      sem.release();;
   }
}

