/*
** Module   : TestPlan.java
** Abstract : stores the data needed to execute a list of test sets
**
** Copyright (c) 2009-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- -----------------------Description------------------------
** 001 GES 20090122 Stores the data needed to execute a list of test sets.
** 002 TJD 20220803 Added logging, updated for Java 11
** 003 TJD 20220811 Progress reports support
** 004 TJD 20220815 Abort anytime if RC failed
** 005 TJD 20220817 Logging tweaks
** 006 TJD 20220819 TestSet are executing sequentially in benchmark mode
** 007 TJD 20230612 Support for IN_PROGRESS test status
** 008 TJD 20240201 Benchmark mode corrections
*/

/*
** 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.io.*;
import java.util.*;
import java.util.concurrent.*;

import com.goldencode.harness.test.*;
import com.goldencode.util.*;
/**
 * Stores the data needed to execute a list of test sets.
 */
public class TestPlan
extends Report
{
   /** Default results directory name. */
   private static final String RESULTS_DIR = "results";
   
   /** Test sets to execute. */
   private List<TestSet> sets = null;
   
   /** Store shared, scoped resources. */
   private ResourcePool pool = new ResourcePool();
   
   /** Output directory for all result reports. */
   private String dir = null;
   
   /**
    * Construct a default instance. The contained data must be set subsequently
    * using the explicit setter methods.
    */                    
   public TestPlan()
   {
   }
   
   /**
    * Construct an instance containing the given data.
    *
    * @param    name
    *           Short name for the test plan.
    * @param    descr
    *           Verbose description of the test plan.
    * @param    sets
    *           The list of test sets to execute.
    */                    
   public TestPlan(String name, String descr, List<TestSet> sets)
   {
      super(name, descr);
      this.sets = sets;
      setOutputDirectory(RESULTS_DIR, false);
   }
   
   /**
    * Get the list of test sets to execute.
    *
    * @return   The test sets or <code>null</code> if none have been set.
    */
   public List<TestSet> getSets()
   {
      return sets;
   }
   
   /**
    * Set the list of test sets to execute.
    *
    * @param    sets
    *           The list of test sets.
    */
   public void setSets(List<TestSet> sets)
   {
      this.sets = sets;
   }
   
   /**
    * 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 output directory.
    *
    * @return   The output directory or <code>null</code> if none have been set.
    */
   public String getOutputDirectory()
   {
      return dir;
   }
   
   /**
    * Set the output directory.
    *
    * @param    dir
    *           The output directory path.
    * @param    unique
    *           Create a unique subdirectory inside the given path such that
    *           the test plan can be run multiple times and each time will
    *           have a unique output directory.
    */
   public void setOutputDirectory(String dir, boolean unique)
   {
      StringBuilder sb = new StringBuilder(dir);
      
      if (!dir.endsWith(File.separator))
         sb.append(File.separator);
      
      if (unique)
      {
         // create a timestamp like 20090223_135650 which is (YYYYMMDD_HHMMSS)
         String ts = String.format("%1$TY%1$Tm%1$Td_%1$TH%1$TM%1$TS",
                                   System.currentTimeMillis());
         sb.append(ts).append(File.separator);
      }
      
      this.dir = sb.toString();
   }
   
   /**
    * 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 sets.size();
   }
   
   /**
    * Iterate through each test set and execute each one (which in turn runs
    * all the tests in the test set). After all execution is complete, this
    * method will trigger {@link #postProcess} to calculate all needed
    * statistics on the results.
    *
    * @param    rc
    *           Object in which to store the details of non-testing failures.
    */
   public void execute(FailureNotification rc)
   {
      setThreadName(Thread.currentThread().getName());
      
      int num = 0;
      
      StringBuilder  sb  = new StringBuilder();
      
      // start test plan timer
      setStartTime(System.currentTimeMillis());
      
      num = processTestSets(sb, Dependency.PRE_CONDITION, rc);
      
      // if the pre-conditions failed, the rest of the test sets are aborted
      if (num == 0)
      {
         num  = processTestSets(sb, Dependency.NONE, rc);
         num += processTestSets(sb, Dependency.POST_CONDITION, rc);
      }
      
      // end test plan timer
      setEndTime(System.currentTimeMillis());
      
      setStatus((num > 0) ? Status.FAILED : Status.PASSED,
                (sb.length() > 0) ? sb.toString() : null);
      
      // this must happen after the status is set for the plan
      postProcess();
   }
   
   /**
    * Handle any post-execution tasks such as statistics or other summary
    * data calculation. 
    */
   public void postProcess()
   {
      calculateStatusCounter(sets);
      
      Iterator<TestSet> iter = sets.iterator();
      
      while (iter.hasNext())
      {
         TestSet set = iter.next();
         set.calculateStatusCounter(set.getTests());
      }
   }
   
   /**
    * Execute each of the test sets that belong to a specific dependency
    * type.
    *
    * @param    sb
    *           Buffer in which to accumulate error text from failures.
    * @param    type
    *           Dependency group to execute.
    * @param    rc
    *           Object in which to store the details of non-testing failures.
    *
    * @return   The number of failures.
    */
   private int processTestSets(StringBuilder       sb,
                               Dependency          type,
                               FailureNotification rc)
   {
      int num = 0;
      LogHelper.info("STARTSET|" + this.getName() + "/" + type.toString());
      
      List<TestSet> list = new ArrayList<TestSet>();
      
      Iterator<TestSet> iter = sets.iterator();
      
      // subset the list of test-sets from the specified dependency group
      while (iter.hasNext())
      {
         TestSet set = iter.next();
         
         if (set.getDependency() == type)
            list.add(set);
      }
      
      // create a specific latch of the right size
      
      iter = list.iterator();
      // when in Benchmark mode tests are executed in sequence
      int maxParallelTests = Harness.isBenchmarkMode() ? 1 : Harness.getMaxParallelTests();
      // execute each of the sets in the group in sequence
      Semaphore sem = new Semaphore(maxParallelTests);
      try            
      {
         while(iter.hasNext())
         {
            if (rc.isFailed() && Harness.isFailOnRCFailure())
            {
               LogHelper.warn("RC failed, stopping TestPlan");
               break;
            }
            // wait for test set to finish
            while(!sem.tryAcquire(60, TimeUnit.SECONDS))
            {
               // dump statistics every 60 seconds
               dumpStatistics(list, type);
            }
            TestSet set = iter.next();
            set.execute(sem, rc);
         }
         //wait for all to finish
         while(!sem.tryAcquire(maxParallelTests, 60, TimeUnit.SECONDS))
         {
            // dump statistics every 60 seconds
            dumpStatistics(list, type);
         }
      }
      catch (InterruptedException ie)
      {
         sb.append("test plan thread interrupted before driver ")
         .append(" threads finished");
         num = 1;
      }
      
      LogHelper.info("ENDSET|" + this.getName() + "/" + type.toString());
      dumpStatistics(list, type);
      
      return checkResults(sets, sb, num);
   }
   
   /**
    * Dump current test plan statistics
    *
    * @param    list
    *           TestSet of the current TestPlan
    * @param type 
    */
   
   private void dumpStatistics(List<TestSet> list, Dependency type)
   {
      if (!LogHelper.isFinerLoggable())
         return;
      int testsPassed = 0;
      int testsFailed = 0;
      int testsNotRun = 0;
      int testsTotal = 0;
      int testsInProgress = 0;
      Set<String> testsInProgressNames = new HashSet<String>();
      for(TestSet testSet:list)
      {
         List<Test> tests = testSet.getTests();
         for(Test t:tests)
         {
            testsTotal ++;
            switch(t.getStatus())
            {
               case FAILED:
               case FAILED_DEPENDENCY:
                  testsFailed ++;
                  break;
               case NOT_RUN:
                  testsNotRun ++;
                  break;
               case IN_PROGRESS:
                  testsInProgress ++;
                  testsInProgressNames.add(t.getName());
                  break;
               case PASSED:
                  testsPassed ++;
                  break;
               default:
                  break;
               
            }
         }
      }
      double successRate = (testsTotal == testsNotRun) 
                              ? 0.0d 
                              : (double) testsPassed/ ((double) testsTotal - (double) testsNotRun 
                                                                       - (double) testsInProgress) * 100.0d;
      double executionRate = (testsTotal == testsNotRun) 
                                ? 0.0d 
                                :(double) (testsPassed + testsFailed)/((double)testsTotal) * 100.0d;
      LogHelper.finer("TESTPLANREPORT|" + this.getName()+"/"+type.toString()
                        + " Total: "       + testsTotal
                        + " left: "        + testsNotRun
                        + " in progress: " + testsInProgress
                        + " failed: "      + testsFailed
                        + " passed: "      + testsPassed
                        + " successful: "  + String.format("%2.2f%%", successRate)
                        + " progress: "   + String.format("%2.2f%%", executionRate));
      LogHelper.finer("TESTPLANREPORT|" + this.getName()+"/"+type.toString()
                        + " Tests in progress: "       
                        + Arrays.toString(testsInProgressNames.toArray()));
   }
}
