Merge branch 'stable-3.0'

* stable-3.0:
  Upgrade bazlets to latest stable-3.0 to build with 3.0.2 API
  Upgrade bazlets to latest stable-2.16 to build with 2.16.11.1 API
  Fixes to build, run, and test on Gerrit 3.0
  Support outputting elapsed evaluation time on tasks
  Rename some local task variable to be more consistent
  Support optional preload-task
  Add optional chainable subtask support
  Allow tasks attributes to be preloaded from other tasks
  Detect and handle non-existing tasks more explicitly.
  Use NOT notation in task tests instead of "-"
  Clarify applicable queries in task tests
  Support exporting properties to task json
  task: add support to set custom properties
  Add duplicate subtask handling and tests
  Move invalid and NA task tests back to bottom.
  Add basic task loop detection test
  task: create property expansion framework
  Tasks: add support for a ${_name} property
  Remove buggy undocumented support to redefine task name
  Upgrade bazlets to latest stable-3.0
  Upgrade bazlets to latest stable-2.16 to build with 2.16.10 API
  Upgrade bazlets to latest stable-3.0 to build with 3.0.1 API
  Upgrade bazlets to latest stable-2.16
  Upgrade bazlets to latest stable-2.16 to build with 2.16.9 API

Change-Id: I58b2615f30d0c588609fad8fe00a752e9c808f9e
diff --git a/BUILD b/BUILD
index 5164a47..614546f 100644
--- a/BUILD
+++ b/BUILD
@@ -9,8 +9,6 @@
         "Implementation-Title: Task Plugin",
         "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/task",
         "Gerrit-Module: com.googlesource.gerrit.plugins.task.Modules$Module",
-        "Gerrit-SshModule: com.googlesource.gerrit.plugins.task.Modules$SshModule",
-        "Gerrit-HttpModule: com.googlesource.gerrit.plugins.task.Modules$HttpModule",
     ],
     resources = glob(["src/main/resources/**/*"]),
 )
diff --git a/pom.xml b/pom.xml
index ed03985..8a9ac3f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,12 +22,12 @@
   <groupId>com.googlesource.gerrit.plugins.task</groupId>
   <artifactId>task</artifactId>
   <packaging>jar</packaging>
-  <version>3.0-SNAPSHOT</version>
+  <version>3.0</version>
   <name>task</name>
 
   <properties>
     <Gerrit-ApiType>plugin</Gerrit-ApiType>
-    <Gerrit-ApiVersion>3.0-SNAPSHOT</Gerrit-ApiVersion>
+    <Gerrit-ApiVersion>3.0.0</Gerrit-ApiVersion>
   </properties>
 
   <build>
@@ -40,8 +40,6 @@
           <archive>
             <manifestEntries>
               <Gerrit-Module>com.googlesource.gerrit.plugins.task.Modules$Module</Gerrit-Module>
-              <Gerrit-HttpModule>com.googlesource.gerrit.plugins.task.Modules$HttpModule</Gerrit-HttpModule>
-              <Gerrit-SshModule>com.googlesource.gerrit.plugins.task.Modules$SshModule</Gerrit-SshModule>
               <Implementation-Vendor>Gerrit Code Review</Implementation-Vendor>
               <Implementation-URL>http://code.google.com/p/gerrit/</Implementation-URL>
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
index a3e4fb6..6ef2ba6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -33,19 +33,8 @@
       bind(ChangeAttributeFactory.class)
           .annotatedWith(Exports.named("task"))
           .to(TaskAttributeFactory.class);
-    }
-  }
 
-  public static class SshModule extends AbstractModule {
-    @Override
-    protected void configure() {
       bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
-    }
-  }
-
-  public static class HttpModule extends AbstractModule {
-    @Override
-    protected void configure() {
       bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
     }
   }
@@ -62,6 +51,9 @@
         usage = "Include only invalid tasks and the tasks referencing them in the output")
     public boolean onlyInvalid = false;
 
+    @Option(name = "--evaluation-time", usage = "Include elapsed evaluation time on each task")
+    boolean evaluationTime = false;
+
     @Option(
         name = "--preview",
         metaVar = "{CHANGE,PATCHSET}",
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
new file mode 100644
index 0000000..e0b76c1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import java.lang.IllegalAccessException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Use to pre-load a task definition with values from its preload-task definition. */
+public class Preloader {
+  public static void preload(Task definition) throws ConfigInvalidException {
+    String name = definition.preloadTask;
+    if (name != null) {
+      Task task = definition.config.getTaskOptional(name);
+      if (task != null) {
+        preload(task);
+        preloadFrom(definition, task);
+      }
+    }
+  }
+
+  protected static void preloadFrom(Task definition, Task preloadFrom) {
+    for (Field field : definition.getClass().getFields()) {
+      String name = field.getName();
+      if (name.equals("isVisible") || name.equals("isTrusted") || name.equals("config")) {
+        continue;
+      }
+
+      try {
+        field.setAccessible(true);
+        Object pre = field.get(preloadFrom);
+        if (pre != null) {
+          Object val = field.get(definition);
+          if (val == null) {
+            field.set(definition, pre);
+          } else if (val instanceof List) {
+            field.set(definition, preloadListFrom((List) val, (List) pre));
+          } else if (val instanceof Map) {
+            field.set(definition, preloadMapFrom((Map) val, (Map) pre));
+          } // nothing to do for overridden preloaded scalars
+        }
+      } catch (IllegalAccessException | IllegalArgumentException e) {
+        throw new RuntimeException();
+      }
+    }
+  }
+
+  protected static List preloadListFrom(List list, List preList) {
+    List extended = list;
+    if (!preList.isEmpty()) {
+      extended = preList;
+      if (!list.isEmpty()) {
+        extended = new ArrayList(list.size() + preList.size());
+        extended.addAll(preList);
+        extended.addAll(list);
+      }
+    }
+    return extended;
+  }
+
+  protected static Map preloadMapFrom(Map map, Map preMap) {
+    Map extended = map;
+    if (!preMap.isEmpty()) {
+      extended = preMap;
+      if (!map.isEmpty()) {
+        extended = new HashMap(map.size() + preMap.size());
+        extended.putAll(preMap);
+        extended.putAll(map);
+      }
+    }
+    return extended;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
new file mode 100644
index 0000000..8af38b5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
@@ -0,0 +1,132 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Use to expand properties like ${_name} for a Task Definition. */
+public class Properties {
+  // "${_name}" -> group(1) = "_name"
+  protected static final Pattern PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
+
+  protected Task definition;
+  protected Map<String, String> expanded = new HashMap<>();
+  protected Map<String, String> unexpanded;
+  protected boolean expandingNonPropertyFields;
+  protected Set<String> expanding;
+
+  public Properties(Task definition, Map<String, String> parentProperties) {
+    expanded.putAll(parentProperties);
+    expanded.put("_name", definition.name);
+
+    unexpanded = definition.properties;
+    unexpanded.putAll(definition.exported);
+    expandAllUnexpanded();
+    definition.properties = expanded;
+
+    if (definition.exported.isEmpty()) {
+      definition.exported = null;
+    } else {
+      for (String property : definition.exported.keySet()) {
+        definition.exported.put(property, expanded.get(property));
+      }
+    }
+
+    this.definition = definition;
+    expandNonPropertyFields();
+  }
+
+  protected void expandNonPropertyFields() {
+    expandingNonPropertyFields = true;
+    for (Field field : Task.class.getFields()) {
+      try {
+        field.setAccessible(true);
+        Object o = field.get(definition);
+        if (o instanceof String) {
+          field.set(definition, expandLiteral((String) o));
+        } else if (o instanceof List) {
+          expandInPlace((List<String>) o);
+        }
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  protected void expandAllUnexpanded() {
+    String property;
+    // A traditional iterator won't work because the recursive expansion may end up
+    // expanding more than one property per iteration behind the iterator's back.
+    while ((property = getFirstUnexpandedProperty()) != null) {
+      expanding = new HashSet<>();
+      expandProperty(property);
+    }
+  }
+
+  protected void expandProperty(String property) {
+    if (!expanding.add(property)) {
+      throw new RuntimeException("Looping property definitions.");
+    }
+    String value = unexpanded.remove(property);
+    if (value != null) {
+      expanded.put(property, expandLiteral(value));
+    }
+  }
+
+  protected String getFirstUnexpandedProperty() {
+    for (String property : unexpanded.keySet()) {
+      return property;
+    }
+    return null;
+  }
+
+  protected void expandInPlace(List<String> list) {
+    if (list != null) {
+      for (ListIterator<String> it = list.listIterator(); it.hasNext(); ) {
+        it.set(expandLiteral(it.next()));
+      }
+    }
+  }
+
+  protected String expandLiteral(String literal) {
+    if (literal == null) {
+      return null;
+    }
+    StringBuffer out = new StringBuffer();
+    Matcher m = PATTERN.matcher(literal);
+    while (m.find()) {
+      m.appendReplacement(out, Matcher.quoteReplacement(getExpandedValue(m.group(1))));
+    }
+    m.appendTail(out);
+    return out.toString();
+  }
+
+  protected String getExpandedValue(String property) {
+    if (!expandingNonPropertyFields) {
+      expandProperty(property); // recursive call
+    }
+    String value = expanded.get(property);
+    return value == null ? "" : value;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
index b55cd05..1a5c702 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -48,12 +48,14 @@
 
   public static class TaskAttribute {
     public Boolean applicable;
+    public Map<String, String> exported;
     public Boolean hasPass;
     public String hint;
     public Boolean inProgress;
     public String name;
     public Status status;
     public List<TaskAttribute> subTasks;
+    public Long evaluationMilliSeconds;
 
     public TaskAttribute(String name) {
       this.name = name;
@@ -109,45 +111,58 @@
     return a;
   }
 
-  protected void addApplicableTasks(List<TaskAttribute> tasks, ChangeData c, Node node) {
+  protected void addApplicableTasks(List<TaskAttribute> atts, ChangeData c, Node node) {
     try {
       Task def = node.definition;
+      TaskAttribute att = new TaskAttribute(def.name);
+      if (options.evaluationTime) {
+        att.evaluationMilliSeconds = millis();
+      }
+
       boolean applicable = match(c, def.applicable);
       if (!def.isVisible) {
         if (!def.isTrusted || (!applicable && !options.onlyApplicable)) {
-          tasks.add(unknown());
+          atts.add(unknown());
           return;
         }
       }
 
       if (applicable || !options.onlyApplicable) {
-        TaskAttribute task = new TaskAttribute(def.name);
-        task.hasPass = def.pass != null || def.fail != null;
-        task.subTasks = getSubTasks(c, node);
-        task.status = getStatus(c, def, task);
+        att.hasPass = def.pass != null || def.fail != null;
+        att.subTasks = getSubTasks(c, node);
+        att.status = getStatus(c, def, att);
         if (options.onlyInvalid && !isValidQueries(c, def)) {
-          task.status = Status.INVALID;
+          att.status = Status.INVALID;
         }
-        boolean groupApplicable = task.status != null;
+        boolean groupApplicable = att.status != null;
 
         if (groupApplicable || !options.onlyApplicable) {
-          if (!options.onlyInvalid || task.status == Status.INVALID || task.subTasks != null) {
+          if (!options.onlyInvalid || att.status == Status.INVALID || att.subTasks != null) {
             if (!options.onlyApplicable) {
-              task.applicable = applicable;
+              att.applicable = applicable;
             }
             if (def.inProgress != null) {
-              task.inProgress = matchOrNull(c, def.inProgress);
+              att.inProgress = matchOrNull(c, def.inProgress);
             }
-            task.hint = getHint(task.status, def);
-            tasks.add(task);
+            att.hint = getHint(att.status, def);
+            att.exported = def.exported;
+
+            if (options.evaluationTime) {
+              att.evaluationMilliSeconds = millis() - att.evaluationMilliSeconds;
+            }
+            atts.add(att);
           }
         }
       }
     } catch (QueryParseException e) {
-      tasks.add(invalid()); // bad applicability query
+      atts.add(invalid()); // bad applicability query
     }
   }
 
+  protected long millis() {
+    return System.nanoTime() / 1000000;
+  }
+
   protected List<TaskAttribute> getSubTasks(ChangeData c, Node node) {
     List<TaskAttribute> subTasks = new ArrayList<>();
     for (Node subNode : node.getSubNodes()) {
@@ -177,33 +192,33 @@
     return a;
   }
 
-  protected boolean isValidQueries(ChangeData c, Task task) {
+  protected boolean isValidQueries(ChangeData c, Task def) {
     try {
-      match(c, task.inProgress);
-      match(c, task.fail);
-      match(c, task.pass);
+      match(c, def.inProgress);
+      match(c, def.fail);
+      match(c, def.pass);
       return true;
     } catch (StorageException | QueryParseException e) {
       return false;
     }
   }
 
-  protected Status getStatus(ChangeData c, Task task, TaskAttribute a) {
+  protected Status getStatus(ChangeData c, Task def, TaskAttribute a) {
     try {
-      return getStatusWithExceptions(c, task, a);
+      return getStatusWithExceptions(c, def, a);
     } catch (QueryParseException e) {
       return Status.INVALID;
     }
   }
 
-  protected Status getStatusWithExceptions(ChangeData c, Task task, TaskAttribute a)
+  protected Status getStatusWithExceptions(ChangeData c, Task def, TaskAttribute a)
       throws QueryParseException {
-    if (isAllNull(task.pass, task.fail, a.subTasks)) {
-      // A leaf task has no defined subtasks.
+    if (isAllNull(def.pass, def.fail, a.subTasks)) {
+      // A leaf def has no defined subdefs.
       boolean hasDefinedSubtasks =
-          !(task.subTasks.isEmpty()
-              && task.subTasksFiles.isEmpty()
-              && task.subTasksExternals.isEmpty());
+          !(def.subTasks.isEmpty()
+              && def.subTasksFiles.isEmpty()
+              && def.subTasksExternals.isEmpty());
       if (hasDefinedSubtasks) {
         // Remove 'Grouping" tasks (tasks with subtasks but no PASS
         // or FAIL criteria) from the output if none of their subtasks
@@ -217,8 +232,8 @@
       return Status.INVALID;
     }
 
-    if (task.fail != null) {
-      if (match(c, task.fail)) {
+    if (def.fail != null) {
+      if (match(c, def.fail)) {
         // A FAIL definition is meant to be a hard blocking criteria
         // (like a CodeReview -2).  Thus, if hard blocked, it is
         // irrelevant what the subtask states, or the PASS criteria are.
@@ -230,7 +245,7 @@
         // to make a task have a FAIL status.
         return Status.FAIL;
       }
-      if (task.pass == null) {
+      if (def.pass == null) {
         // A task with a FAIL but no PASS criteria is a PASS-FAIL task
         // (they are never "READY").  It didn't fail, so pass.
         return Status.PASS;
@@ -249,7 +264,7 @@
       return Status.WAITING;
     }
 
-    if (task.pass != null && !match(c, task.pass)) {
+    if (def.pass != null && !match(c, def.pass)) {
       // Non-leaf tasks with no PASS criteria are supported in order
       // to support "grouping tasks" (tasks with no function aside from
       // organizing tasks).  A task without a PASS criteria, cannot ever
@@ -262,18 +277,18 @@
     return Status.PASS;
   }
 
-  protected String getHint(Status status, Task task) {
+  protected String getHint(Status status, Task def) {
     if (status == Status.READY) {
-      return task.readyHint;
+      return def.readyHint;
     } else if (status == Status.FAIL) {
-      return task.failHint;
+      return def.failHint;
     }
     return null;
   }
 
-  protected static boolean isAll(Iterable<TaskAttribute> tasks, Status state) {
-    for (TaskAttribute task : tasks) {
-      if (task.status != state) {
+  protected static boolean isAll(Iterable<TaskAttribute> atts, Status state) {
+    for (TaskAttribute att : atts) {
+      if (att.status != state) {
         return false;
       }
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
index d73ebdb..62abfcb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -19,7 +19,14 @@
 import com.google.gerrit.server.git.meta.AbstractVersionedMetaData;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /** Task Configuration file living in git */
 public class TaskConfig extends AbstractVersionedMetaData {
@@ -33,11 +40,14 @@
 
   public class Task extends Section {
     public String applicable;
+    public Map<String, String> exported;
     public String fail;
     public String failHint;
     public String inProgress;
     public String name;
     public String pass;
+    public String preloadTask;
+    public Map<String, String> properties;
     public String readyHint;
     public List<String> subTasks;
     public List<String> subTasksExternals;
@@ -50,11 +60,14 @@
       this.isVisible = isVisible;
       this.isTrusted = isTrusted;
       applicable = getString(s, KEY_APPLICABLE, null);
+      exported = getProperties(s, KEY_EXPORT_PREFIX);
       fail = getString(s, KEY_FAIL, null);
       failHint = getString(s, KEY_FAIL_HINT, null);
       inProgress = getString(s, KEY_IN_PROGRESS, null);
-      name = getString(s, KEY_NAME, s.subSection);
+      name = s.subSection;
       pass = getString(s, KEY_PASS, null);
+      preloadTask = getString(s, KEY_PRELOAD_TASK, null);
+      properties = getProperties(s, KEY_PROPERTIES_PREFIX);
       readyHint = getString(s, KEY_READY_HINT, null);
       subTasks = getStringList(s, KEY_SUBTASK);
       subTasksExternals = getStringList(s, KEY_SUBTASKS_EXTERNAL);
@@ -74,16 +87,22 @@
     }
   }
 
+  protected static final Pattern OPTIONAL_TASK_PATTERN =
+      Pattern.compile("([^ |]*( *[^ |])*) *\\| *");
+
   protected static final String SECTION_EXTERNAL = "external";
   protected static final String SECTION_ROOT = "root";
   protected static final String SECTION_TASK = "task";
   protected static final String KEY_APPLICABLE = "applicable";
+  protected static final String KEY_EXPORT_PREFIX = "export-";
   protected static final String KEY_FAIL = "fail";
   protected static final String KEY_FAIL_HINT = "fail-hint";
   protected static final String KEY_FILE = "file";
   protected static final String KEY_IN_PROGRESS = "in-progress";
   protected static final String KEY_NAME = "name";
   protected static final String KEY_PASS = "pass";
+  protected static final String KEY_PRELOAD_TASK = "preload-task";
+  protected static final String KEY_PROPERTIES_PREFIX = "set-";
   protected static final String KEY_READY_HINT = "ready-hint";
   protected static final String KEY_SUBTASK = "subtask";
   protected static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
@@ -125,8 +144,33 @@
     return externals;
   }
 
-  public Task getTask(String name) {
-    return new Task(new SubSection(SECTION_TASK, name), isVisible, isTrusted);
+  /* returs null only if optional and not found */
+  public Task getTaskOptional(String name) throws ConfigInvalidException {
+    int end = 0;
+    Matcher m = OPTIONAL_TASK_PATTERN.matcher(name);
+    while (m.find()) {
+      end = m.end();
+      Task task = getTaskOrNull(m.group(1));
+      if (task != null) {
+        return task;
+      }
+    }
+
+    String last = name.substring(end);
+    if (!"".equals(last)) { // Last entry was not optional
+      Task task = getTaskOrNull(last);
+      if (task != null) {
+        return task;
+      }
+      throw new ConfigInvalidException("task not defined");
+    }
+    return null;
+  }
+
+  /* returns null if not found */
+  protected Task getTaskOrNull(String name) {
+    SubSection subSection = new SubSection(SECTION_TASK, name);
+    return getNames(subSection).isEmpty() ? null : new Task(subSection, isVisible, isTrusted);
   }
 
   public External getExternal(String name) {
@@ -137,11 +181,47 @@
     return new External(s);
   }
 
+  protected Map<String, String> getProperties(SubSection s, String prefix) {
+    Map<String, String> valueByName = new HashMap<>();
+    for (Map.Entry<String, String> e :
+        getStringByName(s, getMatchingNames(s, prefix + ".+")).entrySet()) {
+      String name = e.getKey();
+      valueByName.put(name.substring(prefix.length()), e.getValue());
+    }
+    return valueByName;
+  }
+
+  protected Map<String, String> getStringByName(SubSection s, Iterable<String> names) {
+    Map<String, String> valueByName = new HashMap<>();
+    for (String name : names) {
+      valueByName.put(name, getString(s, name));
+    }
+    return valueByName;
+  }
+
+  protected Set<String> getMatchingNames(SubSection s, String match) {
+    Set<String> matched = new HashSet<>();
+    for (String name : getNames(s)) {
+      if (name.matches(match)) {
+        matched.add(name);
+      }
+    }
+    return matched;
+  }
+
+  protected Set<String> getNames(SubSection s) {
+    return cfg.getNames(s.section, s.subSection);
+  }
+
   protected String getString(SubSection s, String key, String def) {
-    String v = cfg.getString(s.section, s.subSection, key);
+    String v = getString(s, key);
     return v != null ? v : def;
   }
 
+  protected String getString(SubSection s, String key) {
+    return cfg.getString(s.section, s.subSection, key);
+  }
+
   protected List<String> getStringList(SubSection s, String key) {
     return Arrays.asList(cfg.getStringList(s.section, s.subSection, key));
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
index dc902db..4157e38 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -30,10 +30,20 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * Add structure to access the task definitions from the config as a tree.
+ *
+ * <p>This class is a "middle" representation of the task tree. The task config is represented as a
+ * lazily loaded tree, and much of the tree validity is enforced at this layer.
+ */
 public class TaskTree {
   protected static final String TASK_DIR = "task";
 
@@ -65,13 +75,22 @@
   }
 
   protected class NodeList {
-    protected LinkedList<Task> path = new LinkedList<>();
+    protected LinkedList<String> path = new LinkedList<>();
     protected List<Node> nodes;
+    protected Set<String> names = new HashSet<>();
 
-    protected void addSubDefinitions(List<Task> tasks) {
-      for (Task task : tasks) {
-        // path check detects looping definitions
-        nodes.add(path.contains(task) ? null : new Node(task, path));
+    protected void addSubDefinitions(List<Task> defs, Map<String, String> parentProperties) {
+      for (Task def : defs) {
+        if (def != null && !path.contains(def.name) && names.add(def.name)) {
+          // path check above detects looping definitions
+          // names check above detects duplicate subtasks
+          try {
+            nodes.add(new Node(def, path, parentProperties));
+            continue;
+          } catch (Exception e) {
+          } // bad definition, handled below
+        }
+        nodes.add(null);
       }
     }
   }
@@ -80,12 +99,12 @@
     public List<Node> getRootNodes() throws ConfigInvalidException, IOException {
       if (nodes == null) {
         nodes = new ArrayList<>();
-        addSubDefinitions(getRootTasks());
+        addSubDefinitions(getRootDefinitions(), new HashMap<String, String>());
       }
       return nodes;
     }
 
-    protected List<Task> getRootTasks() throws ConfigInvalidException, IOException {
+    protected List<Task> getRootDefinitions() throws ConfigInvalidException, IOException {
       return taskFactory.getRootConfig().getRootTasks();
     }
   }
@@ -93,10 +112,13 @@
   public class Node extends NodeList {
     public final Task definition;
 
-    public Node(Task definition, List<Task> path) {
+    public Node(Task definition, List<String> path, Map<String, String> parentProperties)
+        throws ConfigInvalidException {
       this.definition = definition;
       this.path.addAll(path);
-      this.path.add(definition);
+      this.path.add(definition.name);
+      Preloader.preload(definition);
+      new Properties(definition, parentProperties);
     }
 
     public List<Node> getSubNodes() {
@@ -108,11 +130,15 @@
     }
 
     protected void addSubDefinitions() {
-      addSubDefinitions(getSubTasks());
+      addSubDefinitions(getSubDefinitions());
       addSubFileDefinitions();
       addExternalDefinitions();
     }
 
+    protected void addSubDefinitions(List<Task> defs) {
+      addSubDefinitions(defs, definition.properties);
+    }
+
     protected void addSubFileDefinitions() {
       for (String file : definition.subTasksFiles) {
         try {
@@ -130,7 +156,7 @@
           if (ext == null) {
             nodes.add(null);
           } else {
-            addSubDefinitions(getTasks(ext));
+            addSubDefinitions(getTaskDefinitions(ext));
           }
         } catch (ConfigInvalidException | IOException e) {
           nodes.add(null);
@@ -138,15 +164,23 @@
       }
     }
 
-    protected List<Task> getSubTasks() {
-      List<Task> tasks = new ArrayList<>();
-      for (String subTask : definition.subTasks) {
-        tasks.add(definition.config.getTask(subTask));
+    protected List<Task> getSubDefinitions() {
+      List<Task> defs = new ArrayList<>();
+      for (String name : definition.subTasks) {
+        try {
+          Task def = definition.config.getTaskOptional(name);
+          if (def != null) {
+            defs.add(def);
+          }
+        } catch (ConfigInvalidException e) {
+          defs.add(null);
+        }
       }
-      return tasks;
+      return defs;
     }
 
-    protected List<Task> getTasks(External external) throws ConfigInvalidException, IOException {
+    protected List<Task> getTaskDefinitions(External external)
+        throws ConfigInvalidException, IOException {
       return getTasks(resolveUserBranch(external.user), external.file);
     }
 
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 35ede41..62297c5 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -146,17 +146,39 @@
     fail-hint = Blocked by a negative review score
 ```
 
+`preload-task`
+
+: This key defines a task whose attributes will be preloaded into the current
+task before the current task's attributes are set. Most attributes defined
+in the preload-task will be loaded first, and will be overridden by attributes
+from the current task if they redefined in the current task. Attributes
+which are lists (such as subtasks) or maps (such as properties), will be
+preloaded by the preload-task and then extended with the attributes from the
+current task. See [Optional Tasks](#optional_tasks) for how to define optional
+preload-tasks.
+
+Example:
+```
+    preload-task = Base Jenkins Verification # has a pass criteria and hints
+```
+
 `subtask`
 
 : This key lists the name of a subtask of the current task. This key may be
 used several times in a task section to define more than one subtask for a
-particular task.
+particular task. See [Optional Tasks](#optional_tasks) for how to define
+optional subtasks.
 
 Example:
 
 ```
     subtask = "Code Review"
     subtask = "License Approval"
+    ...
+    [task "Code Review"]
+    ...
+    [task "License Approval"]
+    ...
 ```
 
 `subtasks-external`
@@ -232,6 +254,27 @@
     fail = label:code-review-2
 ```
 
+<a id="optional_tasks"/>
+Optional Tasks
+--------------
+To define a task that may not exist and that will not cause the task referencing
+it to be INVALID, follow the task name with pipe (`|`) character. This feature
+is particularly useful when a property is used in the task name.
+
+```
+    preload-task = Optional Subtask {$_name} |
+```
+
+To define an alternate task to load when an optional task does not exist,
+list the alterante task name after the pipe (`|`) character. This feature
+may be chained together as many times as needed.
+
+```
+    subtask = Optional Subtask {$_name} |
+              Backup Optional Subtask {$_name} Backup |
+              Default Subtask # Must exist if the above two don't!
+```
+
 External Entries
 ----------------
 A name for external task files on other projects and branches may be given
@@ -262,6 +305,60 @@
     user = first-user # references the sharded user ref refs/users/01/1000001
 ```
 
+Properties
+----------
+The task plugin supplies the `${_name}` property which may be used anywhere in
+a task definition as a token representing the name of the current task.
+
+Example:
+```
+    fail-hint = {$_name} needs to be fixed
+```
+
+Custom properties may be defined on a task using the following syntax:
+```
+    set-<property-name> = <property-value>
+```
+
+Subtasks inherit all custom properties from their parents. A task is invalid
+if it attempts to override an already set property.
+
+Example:
+```
+    [task "foo-project"]
+        set-project-name = foo
+        subtask = common-to-many-projects
+
+    [task "common-to-many-projects"]
+        fail-hint = ${project-name} needs to be fixed
+        ...
+```
+
+It is possible to define a custom property value and to export that value
+to the json on the current task by using the following syntax:
+```
+    export-<property-name> = <property-value>
+```
+
+Example:
+```
+    [task "foo"]
+        export-ci-system = jenkins
+```
+
+```
+     "subTasks" : [
+        {
+           "exported" : {
+              "ci-system" : "jenkins"
+           },
+           ...
+           "name" : "foo",
+           ...
+        }
+     ]
+```
+
 Change Query Output
 -------------------
 It is possible to add a task section to the query output of changes using
@@ -297,6 +394,12 @@
 not output anything. This switch is particularly useful in combination
 with the **\-\-@PLUGIN@\-\-preview** switch.
 
+**\-\-@PLUGIN@\-\-task\-\-evaluation-time**
+
+This switch is meant as a debug switch to evaluate task performance. This
+switch outputs an elapsed time value on every task indicating how much time
+it took to evaluate a task and its subtasks.
+
 When tasks are appended to changes, they will have a "task" section under
 the plugins section like below:
 
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
index 50df58f..721032f 100644
--- a/src/main/resources/Documentation/task_states.md
+++ b/src/main/resources/Documentation/task_states.md
@@ -8,14 +8,17 @@
 
 ```
 [root "Root N/A"]
-  applicable = is:closed
+  applicable = is:closed # Assumes test query is "is:open"
+
+[root "Root APPLICABLE"]
+  applicable = is:open # Assumes test query is "is:open"
+  pass = True
+  subtask = Subtask APPLICABLE
 
 [root "Root PASS"]
-  applicable = is:open
   pass = True
 
 [root "Root FAIL"]
-  applicable = is:open
   fail = True
 
 [root "Root straight PASS"]
@@ -36,24 +39,21 @@
   fail = is:open
 
 [root "Root grouping PASS (subtask PASS)"]
-  applicable = is:open
   subtask = Subtask PASS
 
 [root "Root grouping WAITING (subtask READY)"]
-  applicable = is:open
   subtask = Subtask READY
 
 [root "Root grouping WAITING (subtask FAIL)"]
-  applicable = is:open
   subtask = Subtask FAIL
 
 [root "Root grouping NA (subtask NA)"]
-  applicable = is:open
+  applicable = is:open # Assumes Subtask NA has "applicable = NOT is:open"
   subtask = Subtask NA
 
 [root "Root READY (subtask PASS)"]
   applicable = is:open
-  pass = -is:open
+  pass = NOT is:open
   subtask = Subtask PASS
   ready-hint = You must now run the ready task
 
@@ -70,57 +70,69 @@
 [root "Root IN PROGRESS"]
    applicable = is:open
    in-progress = is:open
-   pass = -is:open
+   pass = NOT is:open
 
 [root "Root NOT IN PROGRESS"]
    applicable = is:open
-   in-progress = -is:open
-   pass = -is:open
+   in-progress = NOT is:open
+   pass = NOT is:open
+
+[root "Root Optional subtasks"]
+   subtask = OPTIONAL MISSING |
+   subtask = Subtask Optional |
 
 [root "Subtasks File"]
-  applicable = is:open
   subtasks-file = common.config
 
 [root "Subtasks File (Missing)"]
-  applicable = is:open
   subtasks-file = common.config
   subtasks-file = missing
 
 [root "Subtasks External"]
-  applicable = is:open
   subtasks-external = user special
 
 [root "Subtasks External (Missing)"]
-  applicable = is:open
   subtasks-external = user special
   subtasks-external = missing
 
 [root "Subtasks External (User Missing)"]
-  applicable = is:open
   subtasks-external = user special
   subtasks-external = user missing
 
 [root "Subtasks External (File Missing)"]
-  applicable = is:open
   subtasks-external = user special
   subtasks-external = file missing
 
+[root "Root Properties"]
+  set-root-property = root-value
+  export-root = ${_name}
+  fail = True
+  fail-hint = Name(${_name})
+  subtask = Subtask Properties
+
+[root "Root Preload"]
+   preload-task = Subtask FAIL
+   subtask = Subtask Preload
+
 [root "INVALIDS"]
-  applicable = is:open
   subtasks-file = invalids.config
 
 [root "Root NA Pass"]
-  applicable = -is:open
+  applicable = NOT is:open # Assumes test query is "is:open"
   pass = True
 
 [root "Root NA Fail"]
-  applicable = -is:open
+  applicable = NOT is:open # Assumes test query is "is:open"
   fail = True
 
 [root "NA INVALIDS"]
-  applicable = -is:open
+  applicable = NOT is:open # Assumes test query is "is:open"
   subtasks-file = invalids.config
 
+[task "Subtask APPLICABLE"]
+  applicable = is:open
+  pass = True
+
 [task "Subtask FAIL"]
   applicable = is:open
   fail = is:open
@@ -128,15 +140,91 @@
 
 [task "Subtask READY"]
   applicable = is:open
-  pass = -is:open
+  pass = NOT is:open
   subtask = Subtask PASS
 
 [task "Subtask PASS"]
   applicable = is:open
   pass = is:open
 
+[task "Subtask Optional"]
+   subtask = Subtask PASS |
+   subtask = OPTIONAL MISSING | Subtask FAIL
+   subtask = OPTIONAL MISSING | OPTIONAL MISSING |
+   subtask = OPTIONAL MISSING | OPTIONAL MISSING | Subtask READY
+
 [task "Subtask NA"]
-  applicable = NOT is:open
+  applicable = NOT is:open # Assumes test query is "is:open"
+
+[task "Subtask Properties"]
+  export-subtask = ${_name}
+  subtask = Subtask Properties Hints
+  subtask = Chained ${_name}
+  subtask = Subtask Properties Reset
+
+[task "Subtask Properties Hints"]
+  set-first-property = first-value
+  set-second-property = ${first-property} second-extra ${third-property}
+  set-third-property = third-value
+  fail = True
+  fail-hint = Name(${_name}) root-property(${root-property}) first-property(${first-property}) second-property(${second-property}) root(${root})
+
+[task "Chained Subtask Properties"]
+  pass = True
+
+[task "Subtask Properties Reset"]
+  pass = True
+  set-first-property = reset-first-value
+  fail-hint = first-property(${first-property})
+
+[task "Subtask Preload"]
+  preload-task = Subtask READY
+  subtask = Subtask Preload Preload
+  subtask = Subtask Preload Hints PASS
+  subtask = Subtask Preload Hints FAIL
+  subtask = Subtask Preload Override Pass
+  subtask = Subtask Preload Override Fail
+  subtask = Subtask Preload Extend Subtasks
+  subtask = Subtask Preload Optional
+  subtask = Subtask Preload Properties
+
+[task "Subtask Preload Preload"]
+  preload-task = Subtask Preload with Preload
+
+[task "Subtask Preload with Preload"]
+  preload-task = Subtask PASS
+
+[task "Subtask Preload Hints PASS"]
+  preload-task = Subtask Hints
+  pass = False
+
+[task "Subtask Preload Hints FAIL"]
+  preload-task = Subtask Hints
+  fail = True
+
+[task "Subtask Preload Override Pass"]
+  preload-task = Subtask PASS
+  pass = False
+
+[task "Subtask Preload Override Fail"]
+  preload-task = Subtask FAIL
+  fail = False
+
+[task "Subtask Preload Extend Subtasks"]
+  preload-task = Subtask READY
+  subtask = Subtask APPLICABLE
+
+[task "Subtask Preload Optional"]
+  preload-task = Missing | Subtask PASS
+
+[task "Subtask Preload Properties"]
+  preload-task = Subtask Properties Hints
+  set-fourth-property = fourth-value
+  fail-hint = second-property(${second-property}) fourth-property(${fourth-property})
+
+[task "Subtask Hints"] # meant to be preloaded, not a test case in itself
+  ready-hint = Task is ready
+  fail-hint = Task failed
 
 [external "user special"]
   user = testuser
@@ -161,51 +249,60 @@
 [task "file task/common.config FAIL"]
   applicable = is:open
   fail = is:open
-  pass = is:open
 ```
 
 `task/invalids.config` file in project `All-Projects` on ref `refs/meta/config`.
 
 ```
 [task "No PASS criteria"]
-  applicable = is:open
+  fail-hint = Invalid without Pass criteria and without subtasks
 
 [task "WAITING (subtask INVALID)"]
-  applicable = is:open
   pass = is:open
   subtask = Subtask INVALID
 
+[task "WAITING (subtask duplicate)"]
+  subtask = Subtask INVALID
+  subtask = Subtask INVALID
+
 [task "WAITING (subtask missing)"]
-  applicable = is:open
   pass = is:open
   subtask = MISSING # security bug: subtask name appears in output
 
 [task "Grouping WAITING (subtask INVALID)"]
-  applicable = is:open
   subtask = Subtask INVALID
 
 [task "Grouping WAITING (subtask missing)"]
-  applicable = is:open
   subtask = MISSING  # security bug: subtask name appears in output
 
 [task "Subtask INVALID"]
-  applicable = is:open
+  fail-hint = Use when an INVALID subtask is needed, not meant as a test case in itself
+
+[task "Subtask Optional"]
+   subtask = MISSING | MISSING
 
 [task "NA Bad PASS query"]
-  applicable = -is:open
+  applicable = NOT is:open # Assumes test query is "is:open"
   fail = True
   pass = has:bad
 
 [task "NA Bad FAIL query"]
-  applicable = -is:open
+  applicable = NOT is:open # Assumes test query is "is:open"
   pass = True
   fail = has:bad
 
 [task "NA Bad INPROGRESS query"]
-  applicable = -is:open
+  applicable = NOT is:open # Assumes test query is "is:open"
   fail = True
   in-progress = has:bad
 
+[task "Looping"]
+  subtask = Looping
+
+[task "Looping Properties"]
+  set-A = ${B}
+  set-B = ${A}
+  fail = True
 ```
 
 `task/special.config` file in project `All-Users` on ref `refs/users/self`.
@@ -218,7 +315,6 @@
 [task "userfile task/special.config FAIL"]
   applicable = is:open
   fail = is:open
-  pass = is:open
 ```
 
 The expected output for the above task config looks like:
@@ -234,6 +330,18 @@
          "roots" : [
             {
                "hasPass" : true,
+               "name" : "Root APPLICABLE",
+               "status" : "PASS",
+               "subTasks" : [
+                  {
+                     "hasPass" : true,
+                     "name" : "Subtask APPLICABLE",
+                     "status" : "PASS"
+                  }
+               ]
+            },
+            {
+               "hasPass" : true,
                "name" : "Root PASS",
                "status" : "PASS"
             },
@@ -363,6 +471,42 @@
             },
             {
                "hasPass" : false,
+               "name" : "Root Optional subtasks",
+               "status" : "WAITING",
+               "subTasks" : [
+                  {
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask PASS",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask FAIL",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask READY",
+                           "status" : "READY",
+                           "subTasks" : [
+                              {
+                                 "hasPass" : true,
+                                 "name" : "Subtask PASS",
+                                 "status" : "PASS"
+                              }
+                           ]
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "hasPass" : false,
                "name" : "Subtasks File",
                "status" : "WAITING",
                "subTasks" : [
@@ -472,6 +616,117 @@
                ]
             },
             {
+               "exported" : {
+                  "root" : "Root Properties"
+               },
+               "hasPass" : true,
+               "hint" : "Name(Root Properties)",
+               "name" : "Root Properties",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "exported" : {
+                        "subtask" : "Subtask Properties"
+                     },
+                     "hasPass" : false,
+                     "name" : "Subtask Properties",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : true,
+                           "hint" : "Name(Subtask Properties Hints) root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value) root(Root Properties)",
+                           "name" : "Subtask Properties Hints",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Chained Subtask Properties",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Properties Reset",
+                           "status" : "PASS"
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "hasPass" : true,
+               "name" : "Root Preload",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "hasPass" : true,
+                     "name" : "Subtask Preload",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask PASS",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Preload",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "hint" : "Task is ready",
+                           "name" : "Subtask Preload Hints PASS",
+                           "status" : "READY"
+                        },
+                        {
+                           "hasPass" : true,
+                           "hint" : "Task failed",
+                           "name" : "Subtask Preload Hints FAIL",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Override Pass",
+                           "status" : "READY"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Override Fail",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Extend Subtasks",
+                           "status" : "READY",
+                           "subTasks" : [
+                              {
+                                 "hasPass" : true,
+                                 "name" : "Subtask PASS",
+                                 "status" : "PASS"
+                              },
+                              {
+                                 "hasPass" : true,
+                                 "name" : "Subtask APPLICABLE",
+                                 "status" : "PASS"
+                              }
+                           ]
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Optional",
+                           "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "hint" : "second-property(first-value second-extra third-value) fourth-property(fourth-value)",
+                           "name" : "Subtask Preload Properties",
+                           "status" : "FAIL"
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
                "hasPass" : false,
                "name" : "INVALIDS",
                "status" : "WAITING",
@@ -494,13 +749,28 @@
                      ]
                   },
                   {
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : false,
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "hasPass" : true,
                      "name" : "WAITING (subtask missing)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -523,8 +793,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -533,6 +802,32 @@
                      "hasPass" : false,
                      "name" : "Subtask INVALID",
                      "status" : "INVALID"
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             }
diff --git a/test/all b/test/all
index fb98668..82347ab 100644
--- a/test/all
+++ b/test/all
@@ -11,6 +11,20 @@
             {
                "applicable" : true,
                "hasPass" : true,
+               "name" : "Root APPLICABLE",
+               "status" : "PASS",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "Subtask APPLICABLE",
+                     "status" : "PASS"
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
+               "hasPass" : true,
                "name" : "Root PASS",
                "status" : "PASS"
             },
@@ -176,6 +190,48 @@
             {
                "applicable" : true,
                "hasPass" : false,
+               "name" : "Root Optional subtasks",
+               "status" : "WAITING",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask PASS",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask FAIL",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask READY",
+                           "status" : "READY",
+                           "subTasks" : [
+                              {
+                                 "applicable" : true,
+                                 "hasPass" : true,
+                                 "name" : "Subtask PASS",
+                                 "status" : "PASS"
+                              }
+                           ]
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
+               "hasPass" : false,
                "name" : "Subtasks File",
                "status" : "WAITING",
                "subTasks" : [
@@ -303,6 +359,135 @@
             },
             {
                "applicable" : true,
+               "exported" : {
+                  "root" : "Root Properties"
+               },
+               "hasPass" : true,
+               "hint" : "Name(Root Properties)",
+               "name" : "Root Properties",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "exported" : {
+                        "subtask" : "Subtask Properties"
+                     },
+                     "hasPass" : false,
+                     "name" : "Subtask Properties",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "Name(Subtask Properties Hints) root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value) root(Root Properties)",
+                           "name" : "Subtask Properties Hints",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Chained Subtask Properties",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Properties Reset",
+                           "status" : "PASS"
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Root Preload",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "Subtask Preload",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask PASS",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Preload",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "Task is ready",
+                           "name" : "Subtask Preload Hints PASS",
+                           "status" : "READY"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "Task failed",
+                           "name" : "Subtask Preload Hints FAIL",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Override Pass",
+                           "status" : "READY"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Override Fail",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Extend Subtasks",
+                           "status" : "READY",
+                           "subTasks" : [
+                              {
+                                 "applicable" : true,
+                                 "hasPass" : true,
+                                 "name" : "Subtask PASS",
+                                 "status" : "PASS"
+                              },
+                              {
+                                 "applicable" : true,
+                                 "hasPass" : true,
+                                 "name" : "Subtask APPLICABLE",
+                                 "status" : "PASS"
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "Subtask Preload Optional",
+                           "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "second-property(first-value second-extra third-value) fourth-property(fourth-value)",
+                           "name" : "Subtask Preload Properties",
+                           "status" : "FAIL"
+                        }
+                     ]
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
                "hasPass" : false,
                "name" : "INVALIDS",
                "status" : "WAITING",
@@ -329,14 +514,30 @@
                   },
                   {
                      "applicable" : true,
-                     "hasPass" : true,
-                     "name" : "WAITING (subtask missing)",
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
                            "applicable" : true,
                            "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "WAITING (subtask missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -362,9 +563,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "applicable" : true,
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -376,6 +575,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -392,6 +603,22 @@
                      "hasPass" : true,
                      "name" : "NA Bad INPROGRESS query",
                      "status" : "FAIL"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             },
@@ -435,14 +662,30 @@
                   },
                   {
                      "applicable" : true,
-                     "hasPass" : true,
-                     "name" : "WAITING (subtask missing)",
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
                            "applicable" : true,
                            "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "WAITING (subtask missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -468,9 +711,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "applicable" : true,
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -482,6 +723,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -498,6 +751,22 @@
                      "hasPass" : true,
                      "name" : "NA Bad INPROGRESS query",
                      "status" : "FAIL"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             }
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index 31e2d46..44bb05b 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -34,7 +34,7 @@
 }
 
 get_change_num() { # < gerrit_push_response > changenum
-    local url=$(awk '/New Changes:/ { getline; print $2 }')
+    local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }')
     echo "${url##*\/}" | tr -d -c '[:digit:]'
 }
 
diff --git a/test/invalid b/test/invalid
index 2fa594e..840c3da 100644
--- a/test/invalid
+++ b/test/invalid
@@ -74,14 +74,30 @@
                   },
                   {
                      "applicable" : true,
-                     "hasPass" : true,
-                     "name" : "WAITING (subtask missing)",
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
                            "applicable" : true,
                            "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "WAITING (subtask missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -107,9 +123,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "applicable" : true,
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -121,6 +135,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -137,6 +163,22 @@
                      "hasPass" : true,
                      "name" : "NA Bad INPROGRESS query",
                      "status" : "INVALID"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             },
@@ -168,14 +210,30 @@
                   },
                   {
                      "applicable" : true,
-                     "hasPass" : true,
-                     "name" : "WAITING (subtask missing)",
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
                            "applicable" : true,
                            "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "WAITING (subtask missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -201,9 +259,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "applicable" : true,
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -215,6 +271,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -231,6 +299,22 @@
                      "hasPass" : true,
                      "name" : "NA Bad INPROGRESS query",
                      "status" : "INVALID"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             }
diff --git a/test/invalid-applicable b/test/invalid-applicable
index c1c756a..8e52fa0 100644
--- a/test/invalid-applicable
+++ b/test/invalid-applicable
@@ -47,13 +47,28 @@
                      ]
                   },
                   {
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : false,
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "hasPass" : true,
                      "name" : "WAITING (subtask missing)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -76,8 +91,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -86,6 +100,32 @@
                      "hasPass" : false,
                      "name" : "Subtask INVALID",
                      "status" : "INVALID"
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             }
diff --git a/test/preview b/test/preview
index a885f11..e2706ea 100644
--- a/test/preview
+++ b/test/preview
@@ -30,14 +30,30 @@
                   },
                   {
                      "applicable" : true,
-                     "hasPass" : true,
-                     "name" : "WAITING (subtask missing)",
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
                            "applicable" : true,
                            "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "WAITING (subtask missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -63,9 +79,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "applicable" : true,
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -77,6 +91,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -93,6 +119,22 @@
                      "hasPass" : true,
                      "name" : "NA Bad INPROGRESS query",
                      "status" : "FAIL"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             },
@@ -171,14 +213,30 @@
                   },
                   {
                      "applicable" : true,
-                     "hasPass" : true,
-                     "name" : "WAITING (subtask missing)",
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
                            "applicable" : true,
                            "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "WAITING (subtask missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -204,9 +262,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "applicable" : true,
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -218,6 +274,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -234,6 +302,22 @@
                      "hasPass" : true,
                      "name" : "NA Bad INPROGRESS query",
                      "status" : "FAIL"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             }
diff --git a/test/preview.invalid b/test/preview.invalid
index 2ddcb1c..86d12f1 100644
--- a/test/preview.invalid
+++ b/test/preview.invalid
@@ -30,14 +30,30 @@
                   },
                   {
                      "applicable" : true,
-                     "hasPass" : true,
-                     "name" : "WAITING (subtask missing)",
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
                            "applicable" : true,
                            "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "WAITING (subtask missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -63,9 +79,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "applicable" : true,
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -77,6 +91,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -93,6 +119,22 @@
                      "hasPass" : true,
                      "name" : "NA Bad INPROGRESS query",
                      "status" : "INVALID"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             },
@@ -124,14 +166,30 @@
                   },
                   {
                      "applicable" : true,
-                     "hasPass" : true,
-                     "name" : "WAITING (subtask missing)",
+                     "hasPass" : false,
+                     "name" : "WAITING (subtask duplicate)",
                      "status" : "WAITING",
                      "subTasks" : [
                         {
                            "applicable" : true,
                            "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "Subtask INVALID",
+                           "status" : "INVALID"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "WAITING (subtask missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -157,9 +215,7 @@
                      "status" : "WAITING",
                      "subTasks" : [
                         {
-                           "applicable" : true,
-                           "hasPass" : false,
-                           "name" : "MISSING",
+                           "name" : "UNKNOWN",
                            "status" : "INVALID"
                         }
                      ]
@@ -171,6 +227,18 @@
                      "status" : "INVALID"
                   },
                   {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Subtask Optional",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
                      "applicable" : false,
                      "hasPass" : true,
                      "name" : "NA Bad PASS query",
@@ -187,6 +255,22 @@
                      "hasPass" : true,
                      "name" : "NA Bad INPROGRESS query",
                      "status" : "INVALID"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "Looping",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "name" : "UNKNOWN",
+                     "status" : "INVALID"
                   }
                ]
             }
diff --git a/test/root.change b/test/root.change
index e0d4187..99dea09 100644
--- a/test/root.change
+++ b/test/root.change
@@ -1,27 +1,24 @@
 [root "INVALIDS Preview"]
-  applicable = is:open
   subtasks-file = invalids.config
 
 [root "Root PASS Preview"]
-  applicable = is:open
   pass = True
 
 [root "Root READY (subtask PASS) Preview"]
   applicable = is:open
-  pass = -is:open
+  pass = NOT is:open
   subtask = Subtask PASS Preview
   ready-hint = You must now run the ready task
 
 [root "Subtasks External Preview"]
-  applicable = is:open
   subtasks-external = user special Preview
 
 [root "Root NA Pass Preview"]
-  applicable = -is:open
+  applicable = NOT is:open
   pass = True
 
 [root "NA INVALIDS Preview"]
-  applicable = -is:open
+  applicable = NOT is:open
   subtasks-file = invalids.config
 
 [task "Subtask PASS Preview"]