Merge branch 'stable-2.16'

Change-Id: Iea5317873e16cbe794c66e851e56afecdb496e6a
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 1eab7e4..6ef2ba6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -51,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 64add65..5fcc8f1 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 8485f6c..9a0770e 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/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"]