/*
** Module   : Harness.java
** Abstract : test execution master driver 
**
** Copyright (c) 2008-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090126 Test execution master driver.
** 002 CA  20130410 Added support to read variables values and host/port/userid/password from 
**                  command line arguments. If the variable/setting can't be read from the 
**                  arguments or from the console, read it from standard input.
**                  If the testing has failed, exit with the ExitCode.FAILED code.
** 003 GES 20161017 Avoid NPE and improve logging of non-testing failure case.
** 004 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.
** 005 CA  20210906 Allow override via command line arguments, for: web endpoint, variable initial values and 
**                  oauth1 parameters. 
** 006 CA  20220118 Added Basic authorization.
** 007 TJD 20220803 Added logging, updated for Java 11
** 008 TJD 20220808 Changed log format, added "-d" option
** 009 TJD 20220815 Abort anytime if RC failed
** 010 TJD 20220817 Implement performance test mode and some logging tweaks
** 011 TJD 20220817 Added missing javadoc, include completionMsg
** 012 TJD 20220819 Allow configuring max timeout for single test that require idle waiting
** 013 TJD 20230621 Timeout multiplier and trace mode support, Allow restarting of Driver threads, Tests white
**                  list support
** 014 TJD 20240201 Benchmark mode enhancements
** 015 CA  20240306 Added flag to omit the response body in the results (as for performance testing, the body
**                  may be huge and also not needed).
*/

/*
** 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.function.*;
import com.goldencode.harness.test.*;
import com.goldencode.util.*;

/**
 * Test execution master driver which provides a command line interface. See
 * {@link #main} for syntax. 
 */
public class Harness
{
   /** Format specification for summary output. */
   private static final String SUMMARY_SPEC = "%s Status = %s.\nGenerating " +
                                              "HTML reports... ";
                                      
   /** Format specification for delegated variable prompts. */
   private static final String PROMPT_SPEC = "%s (variable '%s', ENTER for " +
                                             "current value '%s') : ";
                                              
   /** Format specification for delegated secret variable prompts. */
   private static final String SECRET_SPEC = "%s (variable '%s', type '%s', " +
                                             "ENTER for no change) : ";
   
   /** Format specification for the uninitialized variable message. */
   private static final String UNINIT_VAR_SPEC = "Variable '%s' is uninitialized.";
                                              
   /** Syntax help text. */
   private final static String[] helptxt;
   
   /** Single test execution limit in benchmark mode in ms */
   private static int testMaxTimeout = 60000;
   
   /** Flag indicating need to fail if any pre fails, default *false* */
   private static boolean failOnRCFailure = false;
   
   /** Performance mode enabled flag, enabled with "-m" option, default *false* */
   private static boolean benchmarkMode = false;
   
   /** Global limit on number of threads */
   private static double timeoutMultiplier = 1.0;
   
   /** Set of names of test to skip */
   private static HashSet<String> skipFileSet = new HashSet<>();
   
   /** Debug mode enabled flag, enabled with "-d" option, default *false* */
   private static boolean debugMode = false;

   /** Debug mode enabled flag, enabled with "-d -d" option, default *false* */
   private static boolean traceMode = false;
   /** requested maximum for number of parallel test */
   private static int maximumParallelTests = Integer.MAX_VALUE;
   /** tests white list - tests names allowed to execute */
   private static Set<String> testsListWhite = null;
   
   static
   {
      ArrayList<String> array = new ArrayList<String>();
      
      String[] ht = new String[]
      {
         "------------------Automated TTY Test Harness-------------------",
         "",
         "Syntax:",
         "",
         "java com.goldencode.harness.Harness [options] <test_plan_filename>",
         "",
         "Options:",
         "",
         "-?                    = display this help screen",
         "-d                    = enable verbose output",
         "-d -d                 = enable even more verbose output",
         "-l <log_level>        = set log level: WARN,INFO,FINE,FINER",
         "-g <float>            = timeout multiplier",
         "-q <integer>          = maximum parallel TestSet, set 1 for sequential ",
         "-c                    = enable dumping reports on CTRL-C",
         "-x                    = abort anytime when RC fails",
         "-m [timeout]          = enable performance test mode with optional user I/O wait timeout {60s}",
         "-f <test_list>        = comma separated names of tests to be included in the whitelist",
         "-b <path_list>        = specify a default set of baseline search",
         "                        paths; <path_list> is a list of one or more",
         "                        paths separated by the platform specific",
         "                        path separator character (e.g. on UNIX or",
         "                        Linux this would be the colon ':')",
         "-e                    = force separate host/port/userid/password",
         "                        prompts for each target system (the default",
         "                        is to only prompt once and to use the same",
         "                        responses for each of the target definitions",
         "                        which don't have the values defined",
         "-t <path_list>        = specify a default set of test/include search",
         "                        paths; <path_list> has the same format as",
         "                        the -b option",
         "-v <var=value>        = specify values for variables which otherwise",
         "                        would need to be read from the console",
         "-h <host>             = specify the host to connect",
         "-p <port>             = specify the port to connect",
         "-u <userid>           = specify the userid used to connect",
         "-w <password>         = specify the password used to connect",
         "                        WARNING! Setting the password in the command",
         "                        line will expose it in the process list.",
         "                        Use this option with care, as it can pose",
         "                        security problems.",
         "-n <endpoint>         = the override endpoint for web tests.",
         "-oauth1 <param=value> = list of override parameters for oauth1 authorization (web tests only).",
         "-basic  <param=value> = list of override parameters for basic authorization (web tests only).",
         "-o <var=value>        = list of override variable initialization values.",
         "-omitBody             = omit the response body in the reports, useful for performance runs.",
         "",
         "Parameters:",
         "",
         "test_plan_filename    = specific test plan to run (this is the",
         "                        relative or absolute filename of the XML",
         "                        test plan definition)",
         "",
         "Exit Codes:",
         ""
      };
      
      for (String next : ht)
      {
         array.add(next);
      }
      
      for (ExitCode code : ExitCode.values())
      {
         array.add(String.format(" %25s = %d",
                                 code.toString(),
                                 code.getCode()));
      }
      
      helptxt = array.toArray(new String[0]);
   }
   
   /**
    * Print syntax on <code>STDOUT</code>.
    * <p>
    * <b>This method does NOT return! It calls <code>System.exit</code>
    * with the given exit code.</b>
    *
    * @param    rc
    *           The exit code to use with <code>System.exit</code>.
    * @param    extra
    *           Other details of a failure.
    */
   public static void syntax(ExitCode rc, String extra)
   {
      if (rc != ExitCode.PASSED)
      {
         System.err.println(rc.getDescription());
         
         if (extra != null)
         {
            System.err.println(extra);
         }
         
         System.err.println();
      }
      
      if (helptxt != null)
      {
         for (int i = 0; i < helptxt.length; i++)
         {
            System.err.println(helptxt[i]);
         }
      }
      
      System.exit(rc.getCode());
   }
   
   /**
    * Read a session setting from the argument list, console or standard input, in this order.
    * When reading from standard input, no helper text will be shown.
    * 
    * @param    console
    *           The interactive console to prompt for the session setting. May be 
    *           <code>null</code> if a console can't be established.
    * @param    reader
    *           A reader linked to the standard input. Will be <code>null</code> if the console is
    *           not null.
    * @param    varVals
    *           A map containing variable values set in the command line arguments.
    * @param    vname
    *           The variable's name.
    * @param    txt
    *           The text to be shown when prompting for a value using the console. 
    * @param    secret
    *           <code>true</code> when the read value must not echo to the terminal (in case of
    *           passwords or other such variables).
    * 
    * @return   The read value for the given variable.
    */
   private static String readSessionSetting(Console             console,
                                            BufferedReader      reader,
                                            Map<String, String> varVals,
                                            String              vname,
                                            String              txt,
                                            boolean             secret)
   {
      String val = null;
      if (varVals.containsKey(vname))
      {
         val = varVals.get(vname);
      }
      else if (console != null)
      {
         if (secret)
         {
            char[] sval = console.readPassword(txt);
            if (sval != null && sval.length > 0)
            {
               val = new String(sval);
            }
         }
         else
         {
            val = console.readLine(txt);
         }
      }
      else
      {
         try
         {
            // no output is needed on STDOUT, as console will be null only when an interactive 
            // console can't be initialized (i.e. STDOUT is redirected)
            val = reader.readLine();
         }
         catch (IOException e)
         {
            final String spec = "Error reading %s: %s";
            syntax(ExitCode.VARIABLE_READ_ERROR, String.format(spec, vname, e.getMessage()));
         }
      }
      
      return val;
   }
   
   /**
    * Flag indicating if shutdown hook be installed
    */
   private boolean enableShutdownHook;
   
   /**
    * Enable shutdown hook to write out current report information.
    *
    */
   public void enableShutdownHook()
   {
      this.enableShutdownHook = true;
   }
   
   /**
    * Test plan execution driver.
    *
    * @param    testPlan
    *           Test plan to execute.
    * @param    rc
    *           Object in which to store the details of non-testing failures.
    */
   public void execute(TestPlan testPlan, FailureNotification rc)
   {
      Thread shutdownHook = null;
      
      if (enableShutdownHook)
      {
         shutdownHook = new Thread() 
         {
            public void run() 
            {
               LogHelper.info("Dumping reports on CTRL-C");
               testPlan.setStatus(Status.FAILED);
               testPlan.setDescription("CTRL-C interrupted");
               testPlan.postProcess();
               ReportGenerator reports = new ReportGenerator(testPlan, rc);
               reports.generate();
            };
         };
         Runtime.getRuntime().addShutdownHook(shutdownHook);
      }
      try
      {
         if (testPlan == null)
         {
            rc.setResult(ExitCode.MISSING_CONTENT);
            rc.setDetails("No test plan specified.");
            return;
         }
         
         // notify the user that processing is starting
         LogHelper.info("Running test plan... ");
         
         // run the tests
         testPlan.execute(rc);
         
         if (rc.isFailed())
         {
            // details are already logged
            return;
         }
         // remove CTRL-C shutdown hook
         if (shutdownHook != null)
            Runtime.getRuntime().removeShutdownHook(shutdownHook);
         
         // output a summary of the results to the console
         LogHelper.info(String.format(SUMMARY_SPEC,
                                        Harness.completionMsg(testPlan.getElapsed()),
                                        testPlan.getStatus().toString()));
                                                 
         long start = System.currentTimeMillis();
         
         // generate HTML reports for the results
         ReportGenerator reports = new ReportGenerator(testPlan, rc);
         reports.generate();
         
         long end = System.currentTimeMillis();
         
         // notify that report generation is complete
         LogHelper.info("Reports generation " + Harness.completionMsg(end - start));
         
         if (rc.isFailed())
         {
            // details are already logged
            return;
         }
         
         // passed
         rc.setResult(null);
      }
      
      catch (Exception excp)
      {
         rc.setResult(ExitCode.UNKNOWN_FAILURE);
         rc.setDetails(LogHelper.dumpThrowable(excp));
      }
      finally
      {
         if (enableShutdownHook)
            Runtime.getRuntime().removeShutdownHook(shutdownHook);
      }
   }
   
   /**
    * Read <code>key=value</code> pairs until a new option is found or the second-to-last index is reached.
    * 
    * @param    args
    *           The command-line arguments.
    * @param    idx
    *           Current index in the arguments array.
    * @param    c
    *           The consumer to process the pair.
    *           
    * @return   The new index.
    */
   private static int readPairs(String[] args, int idx, BiConsumer<String, String> c)
   {
      while (!args[idx].startsWith("-") && idx < args.length - 1)
      {
         String pair = args[idx++];
         int eqIdx = pair.indexOf('=');
         if (eqIdx < 0)
         {
            final String spec = "The value for '%s' is not set.";
            syntax(ExitCode.MISSING_OPTION_DATA, String.format(spec, pair));
         }
         else if (eqIdx == 0)
         {
            final String spec = "The '%s' data does not contain a name.";
            syntax(ExitCode.MISSING_OPTION_DATA, String.format(spec, pair));
         }
         
         String name = pair.substring(0, eqIdx);
         String value = pair.substring(eqIdx + 1);

         c.accept(name, value);
      }
      
      return idx - 1;
   }
   
   /**
    * Simple command-line interface to invoke the harness.
    * <pre>
    * Syntax:
    * 
    * java com.goldencode.harness.Harness [options] &lt;test_plan_filename&gt;
    *
    * Options:
    *
    * -?                    = display this help screen
    * -d                    = enable debug mode and verbose output (log level FINE)
    * -l &lt;logLevel&gt;         = setup given log level: WARN,INFO,FINE,FINER",
    * -c                    = enable dump reports on CTRL-C
    * -m [timeout]          = enable performance test mode with optional user I/O wait timeout {60s}
    * -x                    = abort anytime when RC fails,
    * -f &lt;test_list&gt;  = comma separated names of tests to be included in the whitelist
    * -b &lt;path_list&gt;  = specify a default set of baseline search
    *                         paths; &lt;path_list&gt; is a list of one or more
    *                         paths separated by the platform specific
    *                         path separator character (e.g. on UNIX or
    *                         Linux this would be the colon ':')
    * -g &lt;integer&gt;    = timeout multiplier",
    * -q &lt;integer&gt;    = maximum parallel TestSet, set 1 for sequential ",
    * -e                    = force separate host/port/userid/password prompts
    *                         for each target system (the default is to only
    *                         prompt once and to use the same responses
    *                         for each of the target definitions which
    *                         don't have the values defined
    * -t &lt;path_list&gt;        = specify a default set of test/include search
    *                         paths; &lt;path_list&gt; has the same format as
    *                         the -b option
    * -v &lt;var=value&gt;        = specify values for variables which otherwise
    *                         would need to be read from the console
    * -h &lt;host&gt;             = specify the host to connect
    * -p &lt;port&gt;             = specify the port to connect
    * -u &lt;userid&gt;           = specify the userid used to connect
    * -w &lt;password&gt;         = specify the password used to connect
    *                         WARNING! Setting the password in the command
    *                         line will expose it in the process list.
    *                         Use this option with care, as it can pose
    *                         security problems.
    * -n &lt;endpoint&gt;         = the override endpoint for web tests
    * -oauth1 &lt;param=value&gt; = list of override parameters for oauth1 authorization (web tests only)
    * -basic  &lt;param=value&gt; = list of override parameters for basic authorization (web tests only)
    * -o &lt;var=value&gt;        = list of override variable initialization values
    * -omitBody                   = omit the response body in the reports, useful for performance tests.
    * 
    * Parameters:
    *
    * test_plan_filename    = specific test plan to run (this is the
    *                         relative or absolute filename of the XML
    *                         test plan definition)
    * </pre>
    * <p>
    * See {@link ExitCode#getCode} for the list of possible exit codes which
    * can be seen from <code>System.exit</code>.
    *
    * @param    args
    *           Command line arguments.
    */
   public static void main(String[] args)
   {
      try
      {
         int     idx       = 0;
         boolean explicit  = false;
         boolean dumpCtrlc = false;
         String  basepaths = null;
         String  testpaths = null;
         
         if (args.length < 1)
         {
            // doesn't return
            syntax(ExitCode.MISSING_FILENAME, "A test plan name is required.");
         }
         Map<String, String> varVals = new HashMap<String, String>();
         // process options
         while (idx < args.length && args[idx].charAt(0) == '-')
         {
            if (args[idx].length() > 1)
            {
               String opt  = args[idx];
               opt = opt.substring(1); // skip the '-' char
               
               switch (opt)
               {
                  case "?":
                     // doesn't return
                     syntax(ExitCode.PASSED, null);
                     break;
                  case "m":
                     // enable benchmark mode
                     benchmarkMode = true;
                     if (idx != args.length - 1)
                     {
                        String singleTestTimeLimit = args[idx+1];
                        if ((singleTestTimeLimit != null) && (singleTestTimeLimit.length() > 0))
                        {
                           try
                           {
                              // try reading timeout in seconds
                              testMaxTimeout = Integer.parseInt(singleTestTimeLimit) * 1000;
                              idx ++;
                           }
                           catch (NumberFormatException e)
                           {
                           }
                        }
                        
                     }
                     LogHelper.info("Enable Benchmark mode");
                     
                     break;
                  case "d":
                     if (debugMode)
                     {
                        // enable trace mode if debug mode is already enabled
                        LogHelper.enableTrace();
                        traceMode = true;
                        LogHelper.info("Enable trace mode");
                     }
                     else
                     {
                        // enable debug output
                        LogHelper.enableDebug();
                        debugMode = true;
                        LogHelper.info("Enable debug mode");
                     }
                     break;
                  case "s":
                     // skip file
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     String skipFileName = args[++idx];
                     if ((skipFileName != null) && (skipFileName.length() > 0))
                     {
                        LogHelper.fine("Reading names of tests to skip from file: " + skipFileName);
                        if (!readSkipFile(skipFileName))
                        {
                           LogHelper.warn("Problem reading tests to skip from file: " + skipFileName);
                           syntax(ExitCode.MISSING_SKIP_FILE, skipFileName);
                        }
                     }
                     break;
                  case "x":
                     // terminate when any pre- fails
                     failOnRCFailure = true;
                     LogHelper.info("Enable immediate fail on any non-testing error mode");
                     break;
                  case "f":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     String testWhiteList = args[++idx];
                     testsListWhite = new HashSet<String>();
                     testsListWhite.addAll( Arrays.asList(testWhiteList.split(",")));
                     break;
                  case "q":
                     // set maximum number of allowed parallel TestSets
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     String parallelTestSetsParam = args[++idx];
                     if ((parallelTestSetsParam != null) && (parallelTestSetsParam.length() > 0))
                     {
                        try
                        {
                           maximumParallelTests = Integer.parseInt(parallelTestSetsParam);
                           LogHelper.fine("Setting maximum number of allowed parallel TestSets: " + maximumParallelTests);
                        }
                        catch(NumberFormatException e)
                        {
                           final String spec = "Invalid value for maximum number of allowed parallel TestSets";
                           syntax(ExitCode.MISSING_OPTION_DATA, String.format(spec, parallelTestSetsParam));
                        }
                     }                  
                     break;
                  case "g":
                     // set timeout multiplier
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     String timeoutMultiplierString = args[++idx];
                     if ((timeoutMultiplierString != null) && (timeoutMultiplierString.length() > 0))
                     {
                        try
                        {
                           timeoutMultiplier = Double.parseDouble(timeoutMultiplierString);
                           LogHelper.fine("Setting timeout multiplier: " + timeoutMultiplier);
                        }
                        catch(NumberFormatException e)
                        {
                           final String spec = "Invalid value for timeout multiplier";
                           syntax(ExitCode.MISSING_OPTION_DATA, String.format(spec, timeoutMultiplierString));
                        }
                     }
                     break;
                  case "l":
                     // set log level from string
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     String logLevel = args[++idx];
                     if ((logLevel != null) && (logLevel.length() > 0))
                     {
                        LogHelper.setLogLevel(logLevel);
                     }
                     break;
                  case "c":
                     // enable dump on CTRL-C
                     dumpCtrlc = true;
                     LogHelper.info("Enable Dump on CTRL-C");
                     break;
                  case "b":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     basepaths = args[++idx];
                     break;
                  case "e":
                     explicit = true;
                     break;
                  case "t":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     testpaths = args[++idx];
                     break;
                  case "h":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     varVals.put("host", args[++idx]);
                     break;
                  case "p":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     varVals.put("port", args[++idx]);
                     break;
                  case "u":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     varVals.put("userid", args[++idx]);
                     break;
                  case "w":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     varVals.put("password", args[++idx]);
                     break;
                  case "v":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }

                     String pair = args[++idx];
                     int eqIdx = pair.indexOf('=');
                     if (eqIdx < 0)
                     {
                        final String spec = "The value for variable '%s' is not set.";
                        syntax(ExitCode.MISSING_OPTION_DATA, String.format(spec, pair));
                     }
                     else if (eqIdx == 0)
                     {
                        final String spec = "The '%s' data does not contain a variable name.";
                        syntax(ExitCode.MISSING_OPTION_DATA, String.format(spec, pair));
                     }
                     
                     String var = pair.substring(0, eqIdx);
                     String value = pair.substring(eqIdx + 1);
                     varVals.put(var, value);
                     break;
                  case "omitBody":
                     WebSessionSettings.overrideOmitResponseBody = true;
                     break;
                  case "n":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     
                     WebSessionSettings.overrideEndpoint = args[++idx];
                     break;
                  case "o":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     
                     idx = readPairs(args, idx + 1, (k, v) -> Variable.overrideVariables.put(k, v));
                     break;
                  case "oauth1":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     idx = readPairs(args, idx + 1, (k, v) -> OAuth1.overrideParameters.put(k, v));
                     break;
                  case "basic":
                     if (idx == args.length - 1)
                     {
                        syntax(ExitCode.MISSING_OPTION_DATA, null);
                     }
                     idx = readPairs(args, idx + 1, (k, v) -> BasicAuth.overrideParameters.put(k, v));
                     break;
                  default:
                     syntax(ExitCode.INVALID_OPTION,
                            String.format("Unknown option '%s'.", args[idx]));
                     break;
               }
            }
            
            idx++;
         }
         
         LogHelper.info("Start");
         if (idx == args.length)
         {
            // doesn't return
            syntax(ExitCode.MISSING_FILENAME, "A test plan name is required.");
         }
         
         FailureNotification rc = new FailureNotification();
         
         TestPlan testPlan = TestPlanFactory.read(args[idx],
                                                  testpaths,
                                                  basepaths,
                                                  rc);
         
         if (testPlan == null)
         {
            // doesn't return
            syntax(rc.getResult(), rc.getDetails());
         }
         
         Console console = System.console();
         BufferedReader reader = null;
         if (console == null)
         {
            reader = new BufferedReader(new InputStreamReader(System.in));
         }

         // prompt for data as needed (vars are declared here so they can be read only once)
         // terminal settings
         String            user    = null;
         String            pw      = null;
         String            host    = null;
         int               port    = -1;
         // web settings
         String endpoint = WebSessionSettings.overrideEndpoint;
         
         List<TestSet>     sets    = testPlan.getSets();
         Iterator<TestSet> iter    = sets.iterator();
            
         // if we are missing any userid or password data, prompt for or get the value from the 
         // corresponding option, it if possible
         // fail if the variable is not initialized from the default value, console or standard
         // input
         while (iter.hasNext())
         {
            TestSet                      set  = iter.next();
            Map<String, SessionSettings> cfgs = set.getCfgs();
            if (cfgs != null)
            {
               for (SessionSettings acfg : cfgs.values())
               {
                  String prompts = acfg.prompts == null ? "" : acfg.prompts;

                  if (acfg instanceof TerminalSessionSettings)
                  {
                     TerminalSessionSettings cfg = (TerminalSessionSettings) acfg;
                     
                     if (cfg.host == null       ||
                         cfg.host.length() == 0 ||
                         prompts.indexOf("host") != -1)
                     {
                        if (explicit || host == null)
                        {
                           String txt = String.format("Host     (%s) : ", cfg.host);
                           host = readSessionSetting(console, reader, varVals, "host", txt, false);
                        }
                        
                        if (host != null && host.length() > 0)
                        {
                           cfg.host = host;
                        }
                        else if (cfg.host == null || cfg.host.length() == 0)
                        {
                           // this var is not set
                           syntax(ExitCode.UNINITIALIZED_VARIABLE, String.format(UNINIT_VAR_SPEC, "host"));
                        }
                     }
                     
                     if (cfg.port == -1 || prompts.indexOf("port") != -1)
                     {
                        if (explicit || port == -1)
                        {
                           String txt = String.format("Port     (%s) : ", cfg.host);
                           String pt = readSessionSetting(console, reader, varVals, "port", txt, false);
                           
                           if (pt.length() > 0)
                           {
                              port = Integer.parseInt(pt);
                           }
                        }
                        
                        if (port != -1)
                        {
                           cfg.port = port;
                        }
                        else if (cfg.port == -1)
                        {
                           // this var is not set
                           syntax(ExitCode.UNINITIALIZED_VARIABLE, String.format(UNINIT_VAR_SPEC, "port"));
                        }
                     }
                     
                     if (cfg.userid == null       ||
                         cfg.userid.length() == 0 ||
                         prompts.indexOf("userid") != -1)
                     {
                        if (explicit || user == null)
                        {
                           String txt = String.format("Userid   (%s) : ", cfg.host);
                           user = readSessionSetting(console, reader, varVals, "userid", txt, false);
                        }
                        
                        if (user != null && user.length() > 0)
                        {
                           cfg.userid = user;
                        }
                        else if (cfg.userid == null || cfg.userid.length() == 0)
                        {
                           // this var is not set
                           syntax(ExitCode.UNINITIALIZED_VARIABLE, 
                                  String.format(UNINIT_VAR_SPEC, "userid"));
                        }
                     }
                     
                     if (cfg.password == null        ||
                         cfg.password.length() == 0  ||
                         prompts.indexOf("password") != -1)
                     {
                        if (explicit || pw == null)
                        {
                           String txt = String.format("Password (%s) : ", cfg.host);
                           pw = readSessionSetting(console, reader, varVals, "password", txt, true);
                        }
                        
                        if (pw != null && pw.length() > 0)
                        {
                           cfg.password = pw;
                        }
                        else if (cfg.password == null || cfg.password.length() == 0)
                        {
                           // this var is not set
                           syntax(ExitCode.UNINITIALIZED_VARIABLE, 
                                  String.format(UNINIT_VAR_SPEC, "password"));
                        }
                     }
                  }
                  else if (acfg instanceof WebSessionSettings)
                  {
                     WebSessionSettings cfg = (WebSessionSettings) acfg;
                     
                     if (cfg.endpoint == null        ||
                         cfg.endpoint.length() == 0  ||
                         prompts.indexOf("endpoint") != -1)
                     {
                        if (explicit || endpoint == null)
                        {
                           String txt = String.format("Endpoint (%s) : ", cfg.endpoint);
                           endpoint = readSessionSetting(console, reader, varVals, "endpoint", txt, false);
                        }
                        
                        if (endpoint != null && endpoint.length() > 0)
                        {
                           cfg.endpoint = endpoint;
                        }
                        else if (cfg.endpoint == null || cfg.endpoint.length() == 0)
                        {
                           // this var is not set
                           syntax(ExitCode.UNINITIALIZED_VARIABLE, 
                                  String.format(UNINIT_VAR_SPEC, "endpoint"));
                        }
                     }
                  }
               }
            }
         }
         
         ResourcePool pool    = testPlan.getResourcePool();
         Prompt[]     prompts = pool.getPrompts();
         
         // prompt to optionally override initial values for variables, but only if the variable
         // is not set via an -v option.
         // if the variable ends uninitialized (from the -v option, prompt or its default value),
         // fail with an error message.
         if (prompts != null)
         {
            for (int z = 0; z < prompts.length; z++)
            {
               if (z == 0 && console != null)
               {
                  console.printf("\n");
               }
               
               Variable var    = prompts[z].getVar();
               String   val    = null;
               String   vname  = var.getName();
               Object   vval   = var.getValue();

               if (varVals.containsKey(vname))
               {
                  val = varVals.get(vname);
               }
               else
               {
                  boolean  secret = prompts[z].isSecret();
                  String   vdescr = prompts[z].getDescr();
                  String   vtype  = var.getTypeName();
                  String   vcurr  = (vval == null) ? "null" : vval.toString();
                  String   txt    = null;
                  
                  if (secret)
                  {
                     txt = String.format(SECRET_SPEC, vdescr, vname, vtype);
                     char[] data = null;
                     if (console != null)
                     {
                        data = console.readPassword(txt);
                        if (data != null)
                           val = new String(data);
                     }
                     else
                     {
                        // no output is needed on STDOUT, as console will be null only when an
                        // interactive console can't be initialized (i.e. STDOUT is redirected)
                        val = reader.readLine();
                     }
                  }
                  else
                  {
                     txt = String.format(PROMPT_SPEC, vdescr, vname, vcurr);
                     if (console != null)
                     {
                        val = console.readLine(txt);
                     }
                     else
                     {
                        // no output is needed on STDOUT, as console will be null only when an
                        // interactive console can't be initialized (i.e. STDOUT is redirected)
                        val = reader.readLine();
                     }
                  }
               }
               
               if (val != null && val.length() > 0)
               {
                  var.fromString(val);
               }
               else if (vval != null && vval.toString().length() > 0)
               {
                  // set from the default value
                  var.fromString(vval.toString());
               }
               else
               {
                  // could not set this variable
                  syntax(ExitCode.UNINITIALIZED_VARIABLE, String.format(UNINIT_VAR_SPEC, vname));
               }
            }
         }
         
         rc = new FailureNotification();
         
         Harness harness = new Harness();
         if (dumpCtrlc)
            harness.enableShutdownHook();
         harness.execute(testPlan, rc);
         
         if (rc.isFailed())
         {
            ExitCode code = rc.getResult();
            String   dets = rc.getDetails();
            
            String err = String.format("Non-testing errors (%s, complete == %b) occured " +
                                       "during test execution:\n%s",
                                       code == null ? "NO EXIT CODE" : code.toString(),
                                       rc.isComplete(),
                                       dets == null ? "MISSING DETAILS" : dets);
            
            LogHelper.warn(err);
            System.exit(code == null ? -999 : code.getCode());
         }
         LogHelper.info("End");
         // exit with a proper code, depending on the result (failed or passed)
         System.exit(testPlan.getStatus() == Status.PASSED ? ExitCode.PASSED.getCode()
                                                           : ExitCode.FAILED.getCode());
      }
      
      catch (Exception excp)
      {
         LogHelper.warn("Exception in main", excp);
         System.exit(-1000);
      }
   }
   
   /**
    * Return failOnRCFailure flag (fail on first non-functional failure feature) value.
    * 
    * @return   failOnRCFailure value.
    */
   public static boolean isFailOnRCFailure()
   {
      return failOnRCFailure;
   }
   
   /**
    * Return true if performance test mode is enabled
    * 
    * @return   performance test mode value.
    */
   public static boolean isBenchmarkMode()
   {
      return benchmarkMode;
   }
   
   /**
    * Return true if debugMode flag value.
    * 
    * @return   debugMode value.
    */
   public static boolean isDebugMode()
   {
      return debugMode;
   }
   
   /**
    * Return true if traceMode flag value.
    * 
    * @return   debugMode value.
    */
   public static boolean isTraceMode()
   {
      return traceMode;
   }

   /**
    * Check if test identified by name is to be skipped
    * @param    name
    *           test name to be checked
    * @return   if test is to be skipped.
    */
   public static boolean isTestOnSkipList(String name)
   {
      return (skipFileSet !=null && skipFileSet.contains(name));
   }
   
   /**
    * Return maximum wait timeout for single test in performance mode value.
    * 
    * @return   timeout for single test in performance mode value.
    */
   public static int getSingleTestTimout()
   {
      return testMaxTimeout;
   }
   
   /**
    * Return test timeout multiplier
    * 
    * @return   test timeout multiplier
    */
   public static int scaleTimeoutMultiplier(int timeout)
   {
      boolean timeoutIsZero = timeout == 0;
      timeout = (int) ((double) timeout * timeoutMultiplier);
      if (timeout == 0 && !timeoutIsZero)
      {
         return 1;
      }
      return timeout;
   }
   
   /**
    * Return requested maximum for number of parallel test
    * 
    * @return   requested maximum for number of parallel test
    */
   public static int getMaxParallelTests()
   {
      return maximumParallelTests;
   }

   /**
    * Generate a completion message that describes the length of time in
    * human readable terms.
    *
    * @param    elapsed
    *           The number of milliseconds of elapsed time.
    *
    * @return   The completion message.
    */
   public static String completionMsg(long elapsed)
   {
      StringBuilder sb = new StringBuilder();
      
      sb.append("Completed in ");
      
      long days   = elapsed / 86400000;
      long remain = elapsed % 86400000;
      
      if (days > 0)
      {
         sb.append(String.format("%d days, ", days));
      }
      
      long hours = remain / 3600000;
      remain = remain % 3600000;
      
      if (days > 0 || hours > 0)
      {
         sb.append(String.format("%d hours, ", hours));
      }
      
      long minutes = remain / 60000;
      remain = remain % 60000;
      
      if (days > 0 || hours > 0 || minutes > 0)
      {
         sb.append(String.format("%d minutes and ", minutes));
      }
      
      double seconds = remain / 1000.0D;
      sb.append(String.format("%.3f seconds.", seconds));
      
      return sb.toString();
   }
   
   /**
    * Read skip file
    * @param    skipFileName
    *           skip file name
    * @return   true if skip file has been correctly processed
    */
   private static boolean readSkipFile(String skipFileName)
   {
      skipFileSet = new HashSet<>();
      try (BufferedReader br = new BufferedReader(new FileReader(skipFileName))) 
      {
          String line = br.readLine();
          while (line != null) 
          {
             line = br.readLine();
             if ((line != null) && (line.trim().length() > 0) && !line.startsWith("#"))
             {
                skipFileSet.add(line);
             }
          }
          LogHelper.info("Skipping: " + skipFileSet.size() + " tests");
          LogHelper.fine("Skipped tests: " + skipFileSet);
          return true;
      } 
      catch (IOException e) 
      {
         LogHelper.warn("Problem opening: " + skipFileName, e);
         return false;
      }
   }

   /**
    * Check if a test with a given name is on whitelist
    * @param    testName
    *           name of test to check
    * @return   true if test with a given name is on whitelist 
    *                or whitelist is not configured at all
    */
   public static boolean isTestOnWhiteList(String testName)
   {
      return (testsListWhite == null || testsListWhite.contains(testName));
   }

}
