/*
** Module   : TestPlanFactory.java
** Abstract : instantiates a test plan from an XML definition 
**
** Copyright (c) 2009-2021, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090122 Instantiates a test plan from an XML definition.
** 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.
** 005 CA  20210906 Allow override via command line arguments, for: web endpoint, variable initial values and 
**                  oauth1 parameters. 
*/

/*
** 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 org.w3c.dom.*;
import com.goldencode.harness.transport.*;
import com.goldencode.harness.test.*;

/**
 * Instantiates a test plan from an XML definition.
 */
public class TestPlanFactory
implements DefinitionConstants
{
   /**
    * Read the test plan definition stored in the specified XML file and
    * instantiate a list of test sets using the parsed content.
    *
    * @param    filename
    *           Path and filename of the test plan definition.
    * @param    testPaths
    *           List of default test paths for the project index or
    *           <code>null</code> for no defaults. Each path must be separated
    *           using the platform specific path separator.
    * @param    basePaths
    *           List of default baseline paths for the project index or
    *           <code>null</code> for no defaults. Each path must be separated
    *           using the platform specific path separator.
    * @param    rc
    *           Object in which to store the details of any failure.
    *
    * @return   The test plan or <code>null</code> if any failure has occurred.
    */
   public static TestPlan read(String              filename,
                               String              testPaths,
                               String              basePaths,
                               FailureNotification rc)
   {
      Document dom = XmlHelper.loadDocument(filename, rc);
      
      if (dom == null)
      {
         // any failures have already been logged in the rc
         return null;
      }
      
      return loadTestSets(dom, testPaths, basePaths, rc);
   }
   
   /**
    * Read the contents of any <code>ELEM_VARIABLE</code> element and
    * load the test from the contents.
    *
    * @param    elem
    *           A test-set node of the XML tree.
    * @param    index
    *           The project index to use for resolving paths or other project
    *           definitions.
    * @param    pool
    *           The test-set resource pool.
    * @param    rc
    *           Object in which to store the details of any failure.
    */
   public static void loadVariable(Element             elem,
                                   ProjectIndex        index,
                                   ResourcePool        pool,
                                   FailureNotification rc)
   {
      String sname = elem.getAttribute(ATTR_NAME);
      
      if (sname == null || sname.length() == 0)
      {
         rc.setResult(ExitCode.MISSING_CONTENT);
         rc.setDetails("Missing variable name.");
         return;
      }
      
      String type = index.readAttr(elem, ATTR_TYPE);
      
      if (type == null || type.length() == 0)
      {
         rc.setResult(ExitCode.MISSING_CONTENT);
         rc.setDetails(String.format("Missing variable type for %s.", sname));
         return;
      }
      
      Class<?> cls = Variable.getSupportedType(type);
      
      if (cls == null)
      {
         rc.setResult(ExitCode.INVALID_CONTENT);
         rc.setDetails(String.format("Unsupported type %s for %s.",
                                     type,
                                     sname));
         return;
      }
      
      String initial = index.readAttr(elem, ATTR_INITIAL);
      
      Variable var = null;
      
      if (initial == null || initial.length() == 0)
      {
         var = pool.createVariable(sname, cls);
      }
      else
      {
         var = pool.createVariable(sname, cls, initial);
      }
      
      Prompt prompt = null;
      
      if (elem.hasAttribute(ATTR_PROMPT))
      {
         prompt = new Prompt(var, index.readAttr(elem, ATTR_PROMPT), false);
      }
      else if (elem.hasAttribute(ATTR_PROMPT_SECRET))
      {
         prompt = new Prompt(var,
                             index.readAttr(elem, ATTR_PROMPT_SECRET),
                             true);
      }
      
      if (prompt != null)
      {
         pool.addPrompt(prompt);
      }
   }
   
   /**
    * Read the parsed document and create the list of steps that are
    * defined there.
    *
    * @param    dom
    *           The document repsenting the XML file.
    * @param    testPaths
    *           List of default test paths for the project index or
    *           <code>null</code> for no defaults. Each path must be separated
    *           using the platform specific path separator.
    * @param    basePaths
    *           List of default baseline paths for the project index or
    *           <code>null</code> for no defaults. Each path must be separated
    *           using the platform specific path separator.
    * @param    rc
    *           Object in which to store the details of any failure.
    *
    * @return   The test plan or <code>null</code> on any failure.
    */
   private static TestPlan loadTestSets(Document            dom,
                                        String              testPaths,
                                        String              basePaths,
                                        FailureNotification rc)
   {
      TestPlan      plan   = null;
      List<TestSet> list   = new ArrayList<TestSet>();
      ProjectIndex  index  = new ProjectIndex();
      ResourcePool  pool   = null;
      
      if (dom != null)
      {
         Element root = dom.getDocumentElement();
         
         if (root != null)
         {
            String pname = root.getAttribute(ATTR_NAME);
            String descr = root.getAttribute(ATTR_DESCR);
            
            plan = new TestPlan(pname, descr, null);
            pool = plan.getResourcePool();
            
            processOutputDirectory(root, plan);
            
            // preload default search paths
            if (testPaths != null)
               index.addTestPathList(testPaths);
            if (basePaths != null)
               index.addBaseLinePathList(basePaths);
            
            processProjectIndex(root, index, plan, rc);
            
            if (rc.isFailed())
               return null;
            
            processResources(root, index, pool, rc);
            
            if (rc.isFailed())
               return null;
            
            // process the global default target configuration (may be null)
            Map<String, SessionSettings> cfgs = processTargets(root, index);
            
            processTestSets(root, list, index, pool, cfgs, rc);
            
            if (rc.isFailed())
               return null;
         }
      }
      
      if (plan != null)
      {
         plan.setSets(list);
         
         // pass
         rc.setResult(null);
      }
      
      return plan;
   }
   
   /**
    * Read the contents of any <code>ELEM_OUTPUT_DIRECTORY</code> element and
    * configure the test plan accordingly.
    *
    * @param    root
    *           The root node of the XML tree.
    * @param    plan
    *           The test plan to modify.
    */
   private static void processOutputDirectory(Element root, TestPlan plan)
   {
      NodeList out = root.getElementsByTagName(ELEM_OUTPUT_DIRECTORY);
      
      if (out.getLength() > 0)
      {
         // we only honor the first target node
         Element od = (Element) out.item(0);
         
         String path = od.getAttribute(ATTR_PATH);
         
         if (path == null || path.length() == 0)
         {
            path = ".";
         }
         
         boolean unique = XmlHelper.getAttributeBoolean(od, ATTR_UNIQUE, false);
         
         plan.setOutputDirectory(path, unique);
      }
   }
   
   /**
    * Read the contents of any <code>ELEM_PROJECT_INDEX</code> element and
    * load the project index with the contents. Note that this will also
    * utilize any configured output directory that exists in the test plan
    * to create a download directory as a subdirectory of that location.
    *
    * @param    root
    *           The root node of the XML tree.
    * @param    index
    *           The index to load.
    * @param    plan
    *           The test plan to modify.
    * @param    rc
    *           Object in which to store the details of any failure.
    */
   private static void processProjectIndex(Element             root,
                                           ProjectIndex        index,
                                           TestPlan            plan,
                                           FailureNotification rc)
   {
      NodeList proj = root.getElementsByTagName(ELEM_PROJECT_INDEX);
      
      if (proj.getLength() > 0)
      {
         // we only honor the first target node
         Element pi    = (Element) proj.item(0);
         String  fname = pi.getAttribute(ATTR_FILENAME);
         
         if (fname == null || fname.length() == 0)
         {
            rc.setResult(ExitCode.MISSING_FILENAME);
            rc.setDetails("The project-index requires a filename.");
            return;
         }
         
         ProjectIndexLoader.load(index, fname, rc);
         
         if (rc.isFailed())
         {
            return;
         }
         
         // create a unique directory for reports that are downloaded
         String rpts = plan.getOutputDirectory();
         
         if (rpts != null)
         {
            StringBuilder sb = new StringBuilder(rpts);
            
            if (!rpts.endsWith(File.separator))
               sb.append(File.separator);
            
            sb.append("downloads").append(File.separator);
            
            index.setDownloadDirectory(sb.toString());
         }
      }
   }
   
   /**
    * Read the contents of any <code>ELEM_RESOURCE</code> element and
    * load the resource pool with the contents.
    *
    * @param    root
    *           The root node of the XML tree.
    * @param    index
    *           The project index to use for resolving paths or other project
    *           definitions.
    * @param    pool
    *           The resource pool to load.
    * @param    rc
    *           Object in which to store the details of any failure.
    */
   private static void processResources(Element             root,
                                        ProjectIndex        index,
                                        ResourcePool        pool,
                                        FailureNotification rc)
   {
      NodeList stuff = root.getElementsByTagName(ELEM_RESOURCES);
      
      int rlen = stuff.getLength();
      
      for (int i = 0; i < rlen; i++)
      {
         Element elem = (Element) stuff.item(i);
         
         NodeList resources = elem.getChildNodes();
         
         if (resources != null)
         {
            int len = resources.getLength();
            
            for (int j = 0; j < len; j++)
            {
               Node next = resources.item(j);
               
               if (next.getNodeType() != Node.ELEMENT_NODE)
                  continue;
               
               Element res = (Element) next;
               String  name = res.getTagName().toLowerCase();
               
               if (ELEM_CREATE_MUTEX.equals(name))
               {
                  String sname = res.getAttribute(ATTR_NAME);
                  
                  if (sname == null || sname.length() == 0)
                  {
                     rc.setResult(ExitCode.MISSING_CONTENT);
                     rc.setDetails("Missing mutex name.");
                     return;
                  }
                  
                  pool.createMutex(sname);
               }
               else if (ELEM_CREATE_EVENT_SEM.equals(name))
               {
                  String sname = res.getAttribute(ATTR_NAME);
                  
                  if (sname == null || sname.length() == 0)
                  {
                     rc.setResult(ExitCode.MISSING_CONTENT);
                     rc.setDetails("Missing event semaphore name.");
                     return;
                  }
                  
                  boolean set = index.readAttrBool(res, ATTR_SET, true);
                  
                  pool.createEventSem(sname, set);
               }
               else if (ELEM_VARIABLE.equals(name))
               {
                  loadVariable(res, index, pool, rc);
                  
                  if (rc.isFailed())
                     return;
               }
               else
               {
                  rc.setResult(ExitCode.INVALID_CONTENT);
                  rc.setDetails(String.format("Unknown resource %s.", name));
                  return;
               }
            }
         }
      }
   }
   
   /**
    * Read the contents of any <code>ELEM_TEST_SET</code> element and
    * load the list with the contents.
    *
    * @param    root
    *           The root node of the XML tree.
    * @param    list
    *           The list of test sets to which to add.
    * @param    index
    *           The project index to use for resolving paths or other project
    *           definitions.
    * @param    pool
    *           The test-plan level resource pool.
    * @param    defCfgs
    *           Default target configuration.
    * @param    rc
    *           Object in which to store the details of any failure.
    */
   private static void processTestSets(Element                      root,
                                       List<TestSet>                list,
                                       ProjectIndex                 index,
                                       ResourcePool                 pool,
                                       Map<String, SessionSettings> defCfgs,
                                       FailureNotification          rc)
   {
      NodeList setlist = root.getElementsByTagName(ELEM_TEST_SET);
      
      int len = setlist.getLength();
      
      for (int i = 0; i < len; i++)
      {
         Element elem = (Element) setlist.item(i);
            
         TestSet set = null;
            
         String sname   = elem.getAttribute(ATTR_NAME);
         String sdescr  = elem.getAttribute(ATTR_DESCR);
         String sdepend = index.readAttr(elem, ATTR_DEPENDENCY);
               
         // create a test-set resource pool that starts as a
         // duplicate of the test plan pool
         ResourcePool spool = new ResourcePool(pool);
         
         // add a scope for the test plan
         spool.push(false);
         
         // load up the test set resources
         processResources(elem, index, spool, rc);
         
         if (rc.isFailed())
            return;
         
         int numthr = index.readAttrInt(elem, ATTR_THREADS, 1);
               
         // process the target configuration first so that it
         // is available during test definition processing
         Map<String, SessionSettings> cfgs = processTargets(elem, index);
         
         // if there was no test-set specific target for a certain cfg type, use the global default
         for (String cfgType : defCfgs.keySet())
         {
            cfgs.computeIfAbsent(cfgType, (k) -> defCfgs.get(cfgType).duplicate());
         }
         
         // if there is no config add 'terminal' as default
         if (cfgs.isEmpty())
         {
            cfgs.computeIfAbsent("terminal", (k) -> new TerminalSessionSettings());
         }
         
         // process the test definitions next
         List<Test>           testlist = new ArrayList<Test>();
         List<SubQueue<Test>> queues   = new ArrayList<SubQueue<Test>>();
         
         processTests(elem, queues, testlist, index, spool, cfgs, numthr, rc);
               
         if (rc.isFailed())
            return;
         
         set = new TestSet(sname, sdescr, cfgs, numthr, queues, testlist, spool);
         
         // optionally process dependencies
         if (sdepend != null && sdepend.length() > 0)
         {
            Dependency dtype = Enum.valueOf(Dependency.class,
                                            sdepend.toUpperCase());
            
            if (dtype == Dependency.NONE          ||
                dtype == Dependency.PRE_CONDITION ||
                dtype == Dependency.POST_CONDITION)
            {
               set.setDependency(dtype);
            }
            else
            {
               rc.setResult(ExitCode.INVALID_CONTENT);
               rc.setDetails(String.format("Unknown dependency '%s' for " +
                                           "test set '%s'.",
                                           sdepend,
                                           sname));
               return;
            }
         }
         
         list.add(set);
      }
      
      // sort the test sets
      Comparator<TestSet> compare = new Comparator<TestSet>()
      {
         // sorts in Dependency order
         public int compare(TestSet set1, TestSet set2)
         {
            Dependency d1 = set1.getDependency();
            Dependency d2 = set2.getDependency();
            
            int value = 0;
            
            if (d1 != d2)
            {
               if (d1 == Dependency.PRE_CONDITION)
               {
                  // always earlier
                  value = -1;
               }
               else if (d1 == Dependency.NONE)
               {
                  if (d2 == Dependency.PRE_CONDITION)
                  {
                     value = 1;
                  }
                  else
                  {
                     value = -1;
                  }
               }
               else
               {
                  // always later
                  value = 1;
               }
            }
            
            return value;
         }
      };         
      
      Collections.sort(list, compare);
   }
   
   /**
    * Read the contents of any <code>ELEM_TARGET</code> elements and
    * load the session configuration with the contents.
    *
    * @param    elem
    *           The target node of the XML tree.
    * @param    index
    *           The project index to use for resolving paths or other project
    *           definitions.
    *
    * @return   The session configuration(s) or an empty map if no target child node exists to process.
    */
   private static Map<String, SessionSettings> processTargets(Element elem, ProjectIndex index)
   {
      Map<String, SessionSettings> cfgs = new HashMap<>();
      
      NodeList targets = elem.getElementsByTagName(ELEM_TARGET);
      
      for (int i = 0; i < targets.getLength(); i++)
      {
         Element target = (Element) targets.item(i);
         
         String type = target.getAttribute(ATTR_TYPE);
         if (type == null || type.isEmpty())
         {
            type = "terminal";
         }
         
         if (cfgs.containsKey(type))
         {
            // we honor only the first target for each type
            continue;
         }

         if ("web".equals(type))
         {
            WebSessionSettings cfg = new WebSessionSettings();
            cfgs.put(type, cfg);
            
            if (target.hasAttribute(ATTR_ENDPOINT))
            {
               cfg.endpoint = index.readAttr(target, ATTR_ENDPOINT);
               if (WebSessionSettings.overrideEndpoint != null)
               {
                  cfg.endpoint = WebSessionSettings.overrideEndpoint;
               }
            }
            
            if (target.hasAttribute(ATTR_SSL_ACCEPT_ALL))
            {
               cfg.sslAcceptAll = Boolean.parseBoolean(index.readAttr(target, ATTR_SSL_ACCEPT_ALL));
            }
            if (target.hasAttribute(ATTR_SSL_ACCEPT_SELF_SIGNED))
            {
               cfg.sslAcceptSelfSigned = Boolean.parseBoolean(index.readAttr(target, ATTR_SSL_ACCEPT_SELF_SIGNED));
            }
            if (target.hasAttribute(ATTR_SSL_TRUSTSTORE_FILE))
            {
               String fname = index.readAttr(target, ATTR_SSL_TRUSTSTORE_FILE);
               cfg.sslTrustStoreFile = fname.isEmpty() ? null : fname;
            }
            if (target.hasAttribute(ATTR_SSL_TRUSTSTORE_PASSWORD))
            {
               cfg.sslTrustStorePassword = index.readAttr(target, ATTR_SSL_TRUSTSTORE_PASSWORD);
            }

            if (target.hasAttribute(ATTR_PROMPT))
            {
               cfg.prompts = index.readAttr(target, ATTR_PROMPT);
            }

            readAuthorizations(target, cfg);
         }
         else if ("terminal".equals(type))
         {
            TerminalSessionSettings cfg = new TerminalSessionSettings();
            cfgs.put(type, cfg);
            
            if (target.hasAttribute(ATTR_HOST))
            {
               cfg.host = index.readAttr(target, ATTR_HOST);
            }
            
            if (target.hasAttribute(ATTR_PORT))
            {
               cfg.port = index.readAttrInt(target, ATTR_PORT, cfg.port);
            }
            
            if (target.hasAttribute(ATTR_USERID))
            {
               cfg.userid = index.readAttr(target, ATTR_USERID);
            }
            
            if (target.hasAttribute(ATTR_PASSWORD))
            {
               cfg.password = index.readAttr(target, ATTR_PASSWORD);
            }
            
            if (target.hasAttribute(ATTR_TERM_TYPE))
            {
               cfg.termType = index.readAttr(target, ATTR_TERM_TYPE);
            }
            
            if (target.hasAttribute(ATTR_TERM_COLS))
            {
               cfg.termColumns = index.readAttrInt(target,
                                                   ATTR_TERM_COLS,
                                                   cfg.termColumns);
            }
            
            if (target.hasAttribute(ATTR_TERM_ROWS))
            {
               cfg.termRows = index.readAttrInt(target,
                                                ATTR_TERM_ROWS,
                                                cfg.termRows);
            }
            
            if (target.hasAttribute(ATTR_PROTOCOL))
            {
               String protocol = index.readAttr(target, ATTR_PROTOCOL);
               cfg.protocol = TransportType.valueOf(protocol.toUpperCase());
            }
            
            if (target.hasAttribute(ATTR_PROMPT))
            {
               cfg.prompts = index.readAttr(target, ATTR_PROMPT);
            }
         }
         else
         {
            throw new IllegalStateException("Unknown target type: " + type);
         }
      }
      
      return cfgs;
   }
   
   /**
    * Read the <code>authorization</code> configuration from the target.
    * 
    * @param    target
    *           The target node.
    * @param    cfg
    *           The web configuration.
    */
   private static void readAuthorizations(Element target, WebSessionSettings cfg)
   {
      NodeList list = target.getElementsByTagName(ELEM_AUTHORIZATION);
      for (int i = 0; i < list.getLength(); i++)
      {
         Element elAuth = (Element) list.item(i);
         String authType = elAuth.getAttribute(ATTR_TYPE).toLowerCase();
         if (!"oauth1".equalsIgnoreCase(authType))
         {
            throw new IllegalArgumentException("Authorization type " + authType + " is not supported!");
         }

         NodeList elParams = elAuth.getElementsByTagName(ELEM_PARAMETER);
         for (int j = 0; j < elParams.getLength(); j++)
         {
            Element elParam = (Element) elParams.item(j);
            String paramName = elParam.getAttribute(ATTR_NAME);
            String paramValue = elParam.getAttribute(ATTR_VALUE);
            cfg.addAuthorizationParameter(authType, paramName, paramValue);
         }
      }
   }
   
   /**
    * Read the contents of any <code>ELEM_TEST</code> element and
    * load the test from the contents.
    *
    * @param    elem
    *           A test-set node of the XML tree.
    * @param    queues
    *           List of sub-queues to which to add.
    * @param    testlist
    *           The list of tests to which to add.
    * @param    index
    *           The project index to use for resolving paths.
    * @param    pool
    *           The test-set resource pool.
    * @param    cfgs
    *           The session settings to configure,
    * @param    threads
    *           The number of threads in this test-set.
    * @param    rc
    *           Object in which to store the details of any failure.
    */
   private static void processTests(Element              elem,
                                    List<SubQueue<Test>> queues,
                                    List<Test>           testlist,
                                    ProjectIndex         index,
                                    ResourcePool         pool,
                                    Map<String, SessionSettings> cfgs,
                                    int                  threads,
                                    FailureNotification  rc)
   {
      NodeList contents = elem.getElementsByTagName(ELEM_TEST);
      
      if (contents != null)
      {
         int numPre  = 0;
         int numPost = 0;
         
         HashMap<String, ConcurrentElements<Test>> simul =
            new HashMap<String, ConcurrentElements<Test>>();
            
         HashMap<String, SequentialElements<Test>> seqs =
            new HashMap<String, SequentialElements<Test>>();
            
         HashMap<String, Test> lookup = new HashMap<String, Test>();
            
         int children = contents.getLength();
         
         // iterate through each test (which must be immediate
         // children of the test-set node)
         for (int j = 0; j < children; j++)
         {
            Element child = (Element) contents.item(j);
            String  fname = child.getAttribute(ATTR_FILENAME);
            
            // honor search paths
            fname = index.findTest(fname);
            
            // read dependency cfg
            Dependency dtype  = Dependency.NONE;
            String     depend = index.readAttr(child, ATTR_DEPENDENCY);
            
            if (depend != null && depend.length() > 0)
            {
               dtype = Enum.valueOf(Dependency.class, depend.toUpperCase());
            }
            
            // read failure mode
            FailureMode fmode  = FailureMode.IGNORE;
            String      ftxt   = index.readAttr(child, ATTR_FAILURE_MODE);
            String      faux   = index.readAttr(child, ATTR_BACKOUT);
            
            if (ftxt != null && ftxt.length() > 0)
            {
               fmode = Enum.valueOf(FailureMode.class, ftxt.toUpperCase());
               faux  = index.findTest(faux);
            }
            
            int repeat = index.readAttrInt(child, ATTR_REPEAT, 1);
            
            // support duplicating tests by iterating (this is
            // much easier than trying to make copies of the
            // same test, since all the test steps have to
            // be duplicated too... it is better to just load
            // the test multiple times)
            while (repeat > 0)
            {
               SubQueue<Test> subQueue   = null;
               SubQueue<Test> extraQueue = null;
               
               int idx = 0;
               
               if (dtype == Dependency.PRE_CONDITION ||
                   dtype == Dependency.POST_CONDITION)
               {
                  List<Test> dups = loadDuplicates(index,
                                                   pool,
                                                   cfgs,
                                                   fname,
                                                   threads,
                                                   dtype,
                                                   fmode,
                                                   faux,
                                                   rc);
                  
                  if (dups != null)
                  {
                     subQueue = new PerThreadElement<Test>(dups);
                     testlist.addAll(dups);
                     
                     if (dtype == Dependency.PRE_CONDITION)
                     {
                        idx = numPre;
                        numPre++;
                     }
                     else
                     {
                        idx = queues.size();
                        numPost++;
                     }
                  }
                  else         
                  {
                     // any failures have already been logged
                     return;
                  }
               }
               else
               {
                  Test test = TestFactory.read(index, pool, cfgs, fname, rc);
                  
                  if (test != null)
                  {
                     String group = null;
                     
                     // maintain a lookup table of tests by filename (warning:
                     // only the last test loaded of a given filename will be
                     // listed, so manual or automated repeats may have strange
                     // results)
                     lookup.put(fname, test);
                     
                     switch (dtype)
                     {
                        case CRITICAL_PATH:
                           subQueue   = new CriticalPathElement<Test>(test);
                           extraQueue = new BarrierElement<Test>();
                           break;
                           
                        case CONCURRENT:
                           group = index.readAttr(child, ATTR_GROUP);
                           
                           if (group != null && group.length() > 0)
                           {
                              ConcurrentElements<Test> ce = simul.get(group);
                              
                              if (ce == null)
                              {
                                 // first time
                                 ce = new ConcurrentElements<Test>();
                                 simul.put(group, ce);
                              }
                              
                              ce.add(test);
                              subQueue = ce;
                           }
                           
                           break;
                           
                        case SEQUENTIAL:
                           group = index.readAttr(child, ATTR_GROUP);
                           
                           if (group != null && group.length() > 0)
                           {
                              SequentialElements<Test> se = seqs.get(group);
                              
                              if (se == null)
                              {
                                 // first time
                                 se = new SequentialElements<Test>();
                                 seqs.put(group, se);
                              }
                              
                              se.add(test);
                              subQueue = se;
                           }
                           
                           break;
                           
                        case SPECIFIC:
                           String referent = index.readAttr(child,
                                                            ATTR_REFERENT);
                           Test   dependee = null;
                           
                           if (referent != null && referent.length() > 0)
                           {
                              dependee = lookup.get(referent);
                           }
                           else
                           {
                              String rspec = "Missing referent attribute for " +
                                             "SPECIFIC dependency in %s.";
                              rc.setResult(ExitCode.MISSING_CONTENT);
                              rc.setDetails(String.format(rspec, fname));
                              return;
                           }
                           
                           subQueue = new DependentElement<Test>(test,
                                                                 dependee);
                           break;
                           
                        case NONE:
                           subQueue = new SingleElement<Test>(test);
                           break;
                           
                        default:
                           String unknown = "Unknown dependency %s in test %s.";
                           rc.setResult(ExitCode.INVALID_CONTENT);
                           rc.setDetails(String.format(unknown,
                                                       dtype.toString(),
                                                       fname));
                           return;
                     }
                     
                     idx = queues.size() - numPost;
                     test.setDependency(dtype);
                     test.setFailureMode(fmode);
                     testlist.add(test);
                     
                     if (fmode == FailureMode.BACKOUT)
                     {
                        Test backout = TestFactory.read(index,
                                                        pool,
                                                        cfgs,
                                                        faux,
                                                        rc);
                        
                        if (backout != null)
                        {
                           test.setBackout(backout);
                        }
                     }
                  }
                  else         
                  {
                     // any failures have already been logged
                     return;
                  }
               }
               
               queues.add(idx, subQueue);
               
               if (extraQueue != null)
               {
                  // some cases need a special barrier element to be added
                  // after
                  queues.add(++idx, extraQueue);
               }
               
               repeat--;
            }
         }
         
         // implement a barrier element if needed (to ensure that post
         // conditions do not execute until all prior processing has completed)
         if (numPost > 0)
         {
            queues.add(queues.size() - numPost, new BarrierElement<Test>());
         }
      }
      else
      {
         String sname = elem.getAttribute(ATTR_NAME);
         String spec  = "Test set %s has no tests defined.";
         rc.setResult(ExitCode.MISSING_CONTENT);
         rc.setDetails(String.format(spec, sname));
      }
   }
   
   /**
    * Read the given test definition <code>num</code> times and return the
    * list of the resulting instances.
    *
    * @param    index
    *           The project index to use for resolving paths.
    * @param    pool
    *           The test-set resource pool.
    * @param    cfgs
    *           The session settings to configure,
    * @param    fname
    *           Filename to load.
    * @param    num
    *           The number of threads for this test set.
    * @param    dtype
    *           The dependency type for this test.
    * @param    fmode
    *           The failure mode for this test.
    * @param    faux
    *           Auxilliary filename for failure mode processing. May be
    *           <code>null</code> for certain modes.
    * @param    rc
    *           Object in which to store the details of any failure.
    *
    * @return   The duplicated tests.
    */
   private static List<Test> loadDuplicates(ProjectIndex        index,
                                            ResourcePool        pool,
                                            Map<String, SessionSettings> cfgs,
                                            String              fname,
                                            int                 num,
                                            Dependency          dtype,
                                            FailureMode         fmode,
                                            String              faux,
                                            FailureNotification rc)
   {
      List<Test> list = new ArrayList<Test>();
      
      while (num > 0)
      {
         Test test = TestFactory.read(index, pool, cfgs, fname, rc);
         
         if (test == null)
         {
            // failure is already logged
            return null;
         }
         
         test.setDependency(dtype);
         test.setFailureMode(fmode);
         list.add(test);
         
         if (fmode == FailureMode.BACKOUT)
         {
            Test backout = TestFactory.read(index,
                                            pool,
                                            cfgs,
                                            faux,
                                            rc);
            
            if (backout != null)
            {
               test.setBackout(backout);
            }
         }
         
         num--;
      }
      
      return list;
   }
}
