Merge branch 'stable-3.3'

* stable-3.3: (27 commits)
  Apply gjf
  Apply gjf
  UI: Indent sub-tasks
  create getInternalProperties() method
  create a Properties.RecursiveExpander
  move Field expansion to the Expander
  create a Properties.Expander
  Handle invalid task roots gracefully
  task: Rewrite method to make it look less buggy
  fixup! Support exporting properties to task json
  Add TaskTree definitions more directly
  Rename TaskTree.Node.definition to task
  Fix to apply task properties to names-factory fields
  Add change task properties
  Fix major sonar issue related to rule "squid : S1132"
  Adjust margins around the task header
  Display task counts next to each header
  Stop double adding Gerrit-ApiVersion
  Task plugin: Track ChangeData in TaskTree
  Store properties at the TaskTree.NodeList level
  ...

Change-Id: I7ddf5f124c6aaf15cfb856f2519c4ab0ec4403f6
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..abdea9a
--- /dev/null
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding/<project>=UTF-8
diff --git a/.settings/org.eclipse.jdt.apt.core.prefs b/.settings/org.eclipse.jdt.apt.core.prefs
new file mode 100644
index 0000000..d4313d4
--- /dev/null
+++ b/.settings/org.eclipse.jdt.apt.core.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.apt.aptEnabled=false
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 40e022d..1b6e1ef 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -1,126 +1,9 @@
 eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
-org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
-org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
-org.eclipse.jdt.core.compiler.annotation.nonnull.secondary=
-org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
-org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
-org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
-org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
-org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
-org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
-org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
 org.eclipse.jdt.core.compiler.compliance=1.8
-org.eclipse.jdt.core.compiler.debug.lineNumber=generate
-org.eclipse.jdt.core.compiler.debug.localVariable=generate
-org.eclipse.jdt.core.compiler.debug.sourceFile=generate
-org.eclipse.jdt.core.compiler.doc.comment.support=enabled
-org.eclipse.jdt.core.compiler.problem.APILeak=warning
-org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
-org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
-org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
-org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
-org.eclipse.jdt.core.compiler.problem.deadCode=warning
-org.eclipse.jdt.core.compiler.problem.deprecation=warning
-org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
-org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
-org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
-org.eclipse.jdt.core.compiler.problem.emptyStatement=warning
-org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
-org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning
-org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
-org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
-org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
-org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
-org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
 org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
-org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
-org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
-org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
-org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
-org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning
-org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled
-org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled
-org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled
-org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=private
-org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
-org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
-org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
-org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
-org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=enabled
-org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
-org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore
-org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled
-org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public
-org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag
-org.eclipse.jdt.core.compiler.problem.missingJavadocTags=ignore
-org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled
-org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled
-org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=protected
-org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning
-org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
-org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
-org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
-org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
-org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
-org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
-org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
-org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning
-org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
-org.eclipse.jdt.core.compiler.problem.nullReference=warning
-org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
-org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
-org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
-org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
-org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning
-org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
-org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
-org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
-org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
-org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
-org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning
-org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning
-org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
-org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
-org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
-org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
-org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
-org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
-org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
-org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
-org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
-org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning
-org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
-org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
-org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
-org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
-org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
-org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
-org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning
-org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled
-org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=warning
-org.eclipse.jdt.core.compiler.problem.unnecessaryElse=warning
-org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
-org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
-org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
-org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
-org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
-org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
-org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore
-org.eclipse.jdt.core.compiler.problem.unusedImport=warning
-org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
-org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
-org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
-org.eclipse.jdt.core.compiler.problem.unusedParameter=warning
-org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
-org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
-org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
-org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
-org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
-org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
-org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
-org.eclipse.jdt.core.compiler.processAnnotations=enabled
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
+org.eclipse.jdt.core.compiler.processAnnotations=disabled
+org.eclipse.jdt.core.compiler.release=disabled
 org.eclipse.jdt.core.compiler.source=1.8
diff --git a/BUILD b/BUILD
index 171e9da..58f5464 100644
--- a/BUILD
+++ b/BUILD
@@ -8,13 +8,13 @@
     srcs = glob(["src/main/java/**/*.java"]),
     manifest_entries = [
         "Gerrit-PluginName: " + plugin_name,
-        "Gerrit-ApiVersion: 3.4.0",
         "Implementation-Title: Task Plugin",
         "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/" + plugin_name,
         "Gerrit-Module: com.googlesource.gerrit.plugins.task.Modules$Module",
     ],
     resource_jars = [":gr-task-plugin"],
     resources = glob(["src/main/resources/**/*"]),
+    javacopts = [ "-Werror", "-Xlint:all", "-Xlint:-classfile", "-Xlint:-processing"],
 )
 
 gerrit_js_bundle(
diff --git a/gr-task-plugin/gr-task-plugin.html b/gr-task-plugin/gr-task-plugin.html
new file mode 100644
index 0000000..c6c3746
--- /dev/null
+++ b/gr-task-plugin/gr-task-plugin.html
@@ -0,0 +1,146 @@
+<!--
+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.
+-->
+
+<dom-module id="gr-task-plugin">
+  <template>
+      <style>
+        ul {
+          padding-left: 0.5em;
+          margin-top: 0;
+        }
+        h3 { padding-left: 0.1em; }
+        .cursor { cursor: pointer; }
+        #tasks_header {
+          align-items: center;
+          background-color: #fafafa;
+          border-top: 1px solid #ddd;
+          display: flex;
+          padding: 6px 1rem;
+        }
+        .links {
+          color: blue;
+          cursor: pointer;
+          text-decoration: underline;
+        }
+        .no-margins { margin: 0 0 0 0; }
+      </style>
+
+      <div id="tasks" hidden$="[[!_tasks.length]]">
+        <div id="tasks_header" style="display: flex;">
+          <iron-icon
+              icon="gr-icons:expand-less"
+              hidden$="[[!_expand_all]]"
+              on-tap="_switch_expand"
+              class="cursor"> </iron-icon>
+          <iron-icon
+              icon="gr-icons:expand-more"
+              hidden$="[[_expand_all]]"
+              on-tap="_switch_expand"
+              class="cursor"> </iron-icon>
+          <div style="display: flex; align-items: center; column-gap: 1em;">
+          <h3 class="no-margins" on-tap="_switch_expand" class="cursor"> Tasks </h3>
+          <template is="dom-if" if="[[_is_show_all(_show_all)]]">
+            <p class="no-margins">All ([[_all_count]]) |&nbsp;
+              <span
+                  on-click="_needs_and_blocked_tap"
+                  class="links">Needs ([[_ready_count]]) + Blocked ([[_fail_count]])</span>
+            <p>
+          </template>
+          <template is="dom-if" if="[[!_is_show_all(_show_all)]]">
+            <p class="no-margins"> <span
+                  class="links"
+                  on-click="_show_all_tap">All ([[_all_count]])</span>
+              &nbsp;| Needs ([[_ready_count]]) + Blocked ([[_fail_count]])</p>
+          </template>
+        </div>
+        </div>
+        <div hidden$="[[!_expand_all]]">
+          <ul style="list-style-type:none;">
+            <gr-task-plugin-tasks
+                tasks="[[_tasks]]"
+                show_all$="[[_show_all]]"> </gr-task-plugin-tasks>
+          </ul>
+        </div>
+      </div>
+  </template>
+  <script src="gr-task-plugin.js"></script>
+</dom-module>
+
+<dom-module id="gr-task-plugin-tasks">
+  <template>
+    <template is="dom-repeat" as="task" items="[[tasks]]">
+      <template is="dom-if" if="[[_can_show(show_all, task)]]">
+        <li style="padding: 0.2em;">
+          <style>
+            /* Matching colors with core code. */
+            .green {
+              color: #9fcc6b;
+            }
+            .red {
+              color: #FFA62F;
+            }
+          </style>
+          <template is="dom-if" if="[[task.icon.id]]">
+            <gr-tooltip-content
+                has-tooltip
+                title="In Progress">
+                <iron-icon
+                  icon="gr-icons:hourglass"
+                  class="green"
+                  hidden$="[[!task.in_progress]]">
+                </iron-icon>
+            </gr-tooltip-content>
+            <gr-tooltip-content
+                has-tooltip
+                title$="[[task.icon.tooltip]]">
+                <iron-icon
+                  icon="[[task.icon.id]]"
+                  class$="[[task.icon.color]]">
+                </iron-icon>
+            </gr-tooltip-content>
+          </template>
+          [[task.message]]
+        </li>
+      </template>
+      <ul style="list-style-type:none; margin: 0 0 0 0; padding: 0 0 0 2em;">
+      <gr-task-plugin-tasks
+          tasks="[[task.sub_tasks]]"
+          show_all$="[[show_all]]"> </gr-task-plugin-tasks>
+      </ul>
+    </template>
+  </template>
+  <script>
+      Polymer({
+        is: 'gr-task-plugin-tasks',
+        properties: {
+          tasks: {
+            type: Array,
+            notify: true,
+            value() { return []; },
+          },
+
+          show_all: {
+            type: String,
+            notify: true,
+          },
+        },
+
+        _can_show(show, task) {
+          return show === 'true' || task.showOnFilter;
+        },
+      });
+  </script>
+</dom-module>
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js
index 096a408..1cfe322 100644
--- a/gr-task-plugin/gr-task-plugin.js
+++ b/gr-task-plugin/gr-task-plugin.js
@@ -147,7 +147,7 @@
     return icon;
   }
 
-  _computeShowOnNeedsAndBlockedFilter(task) {
+  _isFailOrReadyOrInvalid(task) {
     switch (task.status) {
       case 'FAIL':
       case 'READY':
@@ -157,6 +157,12 @@
     return false;
   }
 
+  _computeShowOnNeedsAndBlockedFilter(task) {
+    return this._isFailOrReadyOrInvalid(task) ||
+      (task.sub_tasks && task.sub_tasks.some(t =>
+        this._computeShowOnNeedsAndBlockedFilter(t)));
+  }
+
   _compute_counts(task) {
     this._all_count++;
     switch (task.status) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java
new file mode 100644
index 0000000..45fe46d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2020 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.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MatchCache {
+  protected final PredicateCache predicateCache;
+  protected final ChangeData changeData;
+
+  protected final Map<String, Boolean> matchResultByQuery = new HashMap<>();
+
+  public MatchCache(PredicateCache predicateCache, ChangeData changeData) {
+    this.predicateCache = predicateCache;
+    this.changeData = changeData;
+  }
+
+  protected boolean match(String query) throws StorageException, QueryParseException {
+    if (query == null) {
+      return true;
+    }
+    Boolean isMatched = matchResultByQuery.get(query);
+    if (isMatched == null) {
+      isMatched = predicateCache.match(changeData, query);
+      matchResultByQuery.put(query, isMatched);
+    }
+    return isMatched;
+  }
+
+  protected Boolean matchOrNull(String query) {
+    if (query == null) {
+      return null;
+    }
+    Boolean isMatched = matchResultByQuery.get(query);
+    if (isMatched == null) {
+      isMatched = predicateCache.matchOrNull(changeData, query);
+      matchResultByQuery.put(query, isMatched);
+    }
+    return isMatched;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
new file mode 100644
index 0000000..93923e1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2020 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.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PredicateCache {
+  protected final ChangeQueryBuilder cqb;
+  protected final CurrentUser user;
+
+  protected final Map<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>>
+      predicatesByQuery = new HashMap<>();
+
+  @Inject
+  public PredicateCache(CurrentUser user, ChangeQueryBuilder cqb) {
+    this.user = user;
+    this.cqb = cqb;
+  }
+
+  public boolean match(ChangeData c, String query) throws StorageException, QueryParseException {
+    if (query == null) {
+      return true;
+    }
+    return matchWithExceptions(c, query);
+  }
+
+  public Boolean matchOrNull(ChangeData c, String query) {
+    if (query != null) {
+      try {
+        return matchWithExceptions(c, query);
+      } catch (QueryParseException | RuntimeException e) {
+      }
+    }
+    return null;
+  }
+
+  protected boolean matchWithExceptions(ChangeData c, String query)
+      throws QueryParseException, StorageException {
+    if ("true".equalsIgnoreCase(query)) {
+      return true;
+    }
+    return cqb.parse(query).asMatchable().match(c);
+  }
+
+  protected Predicate<ChangeData> getPredicate(String query) throws QueryParseException {
+    ThrowingProvider<Predicate<ChangeData>, QueryParseException> predProvider =
+        predicatesByQuery.get(query);
+    if (predProvider != null) {
+      return predProvider.get();
+    }
+    // never seen 'query' before
+    try {
+      Predicate<ChangeData> pred = cqb.parse(query);
+      predicatesByQuery.put(query, new ThrowingProvider.Entry<>(pred));
+      return pred;
+    } catch (QueryParseException e) {
+      predicatesByQuery.put(query, new ThrowingProvider.Thrown<>(e));
+      throw e;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
index df184b1..8babe1c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -38,35 +38,62 @@
   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")) {
+      if ("isVisible".equals(name) || "isTrusted".equals(name) || "config".equals(name)) {
         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
-        }
+        preloadField(field.getType(), field, definition, preloadFrom);
       } catch (IllegalAccessException | IllegalArgumentException e) {
         throw new RuntimeException();
       }
     }
   }
 
-  protected static List preloadListFrom(List list, List preList) {
-    List extended = list;
+  protected static <T, S, K, V> void preloadField(
+      Class<T> clz, Field field, Task definition, Task preloadFrom)
+      throws IllegalArgumentException, IllegalAccessException {
+    T pre = getField(clz, field, preloadFrom);
+    if (pre != null) {
+      T val = getField(clz, field, definition);
+      if (val == null) {
+        field.set(definition, pre);
+      } else if (val instanceof List) {
+        List<?> valList = List.class.cast(val);
+        List<?> preList = List.class.cast(pre);
+        field.set(definition, preloadListFrom(castUnchecked(valList), castUnchecked(preList)));
+      } else if (val instanceof Map) {
+        Map<?, ?> valMap = Map.class.cast(val);
+        Map<?, ?> preMap = Map.class.cast(pre);
+        field.set(definition, preloadMapFrom(castUnchecked(valMap), castUnchecked(preMap)));
+      } // nothing to do for overridden preloaded scalars
+    }
+  }
+
+  protected static <T> T getField(Class<T> clz, Field field, Object obj)
+      throws IllegalArgumentException, IllegalAccessException {
+    return clz.cast(field.get(obj));
+  }
+
+  @SuppressWarnings("unchecked")
+  protected static <S> List<S> castUnchecked(List<?> list) {
+    List<S> forceCheck = (List<S>) list;
+    return forceCheck;
+  }
+
+  @SuppressWarnings("unchecked")
+  protected static <K, V> Map<K, V> castUnchecked(Map<?, ?> map) {
+    Map<K, V> forceCheck = (Map<K, V>) map;
+    return forceCheck;
+  }
+
+  protected static <T> List<T> preloadListFrom(List<T> list, List<T> preList) {
+    List<T> extended = list;
     if (!preList.isEmpty()) {
       extended = preList;
       if (!list.isEmpty()) {
-        extended = new ArrayList(list.size() + preList.size());
+        extended = new ArrayList<>(list.size() + preList.size());
         extended.addAll(preList);
         extended.addAll(list);
       }
@@ -74,12 +101,12 @@
     return extended;
   }
 
-  protected static Map preloadMapFrom(Map map, Map preMap) {
-    Map extended = map;
+  protected static <K, V> Map<K, V> preloadMapFrom(Map<K, V> map, Map<K, V> preMap) {
+    Map<K, V> extended = map;
     if (!preMap.isEmpty()) {
       extended = preMap;
       if (!map.isEmpty()) {
-        extended = new HashMap(map.size() + preMap.size());
+        extended = new HashMap<>(map.size() + preMap.size());
         extended.putAll(preMap);
         extended.putAll(map);
       }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
index 8af38b5..b8bf285 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
@@ -14,8 +14,15 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.common.collect.Sets;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
 import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -27,106 +34,170 @@
 
 /** 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;
+  public Properties(ChangeData changeData, Task definition, Map<String, String> parentProperties)
+      throws StorageException {
+    Map<String, String> expanded = new HashMap<>(parentProperties);
+    expanded.putAll(getInternalProperties(definition, changeData));
+    Map<String, String> unexpanded = definition.properties;
     unexpanded.putAll(definition.exported);
-    expandAllUnexpanded();
+    new RecursiveExpander(expanded).expand(unexpanded);
+
     definition.properties = expanded;
+    for (String property : definition.exported.keySet()) {
+      definition.exported.put(property, expanded.get(property));
+    }
 
-    if (definition.exported.isEmpty()) {
-      definition.exported = null;
-    } else {
-      for (String property : definition.exported.keySet()) {
-        definition.exported.put(property, expanded.get(property));
+    new Expander(expanded).expandFieldValues(definition, Collections.emptySet());
+  }
+
+  public Properties(NamesFactory namesFactory, Map<String, String> properties) {
+    new Expander(properties).expandFieldValues(namesFactory, Sets.newHashSet(TaskConfig.KEY_TYPE));
+  }
+
+  protected static Map<String, String> getInternalProperties(Task definition, ChangeData changeData)
+      throws StorageException {
+    Map<String, String> properties = new HashMap<>();
+
+    properties.put("_name", definition.name);
+
+    Change c = changeData.change();
+    properties.put("_change_number", String.valueOf(c.getId().get()));
+    properties.put("_change_id", c.getKey().get());
+    properties.put("_change_project", c.getProject().get());
+    properties.put("_change_branch", c.getDest().branch());
+    properties.put("_change_status", c.getStatus().toString());
+    properties.put("_change_topic", c.getTopic());
+
+    return properties;
+  }
+
+  /**
+   * Use to expand properties whose values may contain other references to properties.
+   *
+   * <p>Using a recursive expansion approach makes order of evaluation unimportant as long as there
+   * are no looping definitions.
+   *
+   * <p>Given some property name/value asssociations defined like this:
+   *
+   * <p><code>
+   * valueByName.put("obstacle", "fence");
+   * valueByName.put("action", "jumped over the ${obstacle}");
+   * </code>
+   *
+   * <p>a String like: <code>"The brown fox ${action}."</code>
+   *
+   * <p>will expand to: <code>"The brown fox jumped over the fence."</code>
+   */
+  protected static class RecursiveExpander {
+    protected final Expander expander;
+    protected Map<String, String> unexpandedByName;
+    protected Set<String> expanding;
+
+    public RecursiveExpander(Map<String, String> valueByName) {
+      expander =
+          new Expander(valueByName) {
+            @Override
+            protected String getValueForName(String name) {
+              expandUnexpanded(name); // recursive call
+              return super.getValueForName(name);
+            }
+          };
+    }
+
+    public void expand(Map<String, String> unexpandedByName) {
+      this.unexpandedByName = unexpandedByName;
+
+      // Copy keys to allow out of order removals during iteration
+      for (String unexpanedName : new ArrayList<>(unexpandedByName.keySet())) {
+        expanding = new HashSet<>();
+        expandUnexpanded(unexpanedName);
       }
     }
 
-    this.definition = definition;
-    expandNonPropertyFields();
+    protected void expandUnexpanded(String name) {
+      if (!expanding.add(name)) {
+        throw new RuntimeException("Looping property definitions.");
+      }
+      String value = unexpandedByName.remove(name);
+      if (value != null) {
+        expander.valueByName.put(name, expander.expandText(value));
+      }
+    }
   }
 
-  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);
+  /**
+   * Use to expand properties like ${property} in Strings into their values.
+   *
+   * <p>Given some property name/value asssociations defined like this:
+   *
+   * <p><code>
+   * valueByName.put("animal", "fox");
+   * valueByName.put("bar", "foo");
+   * valueByName.put("obstacle", "fence");
+   * </code>
+   *
+   * <p>a String like: <code>"The brown ${animal} jumped over the ${obstacle}."</code>
+   *
+   * <p>will expand to: <code>"The brown fox jumped over the fence."</code>
+   */
+  protected static class Expander {
+    // "${_name}" -> group(1) = "_name"
+    protected static final Pattern PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
+
+    public final Map<String, String> valueByName;
+
+    public Expander(Map<String, String> valueByName) {
+      this.valueByName = valueByName;
+    }
+
+    /** Expand all properties in the Strings in the object's Fields (except the exclude ones) */
+    protected void expandFieldValues(Object object, Set<String> excludedFieldNames) {
+      for (Field field : object.getClass().getFields()) {
+        try {
+          if (!excludedFieldNames.contains(field.getName())) {
+            field.setAccessible(true);
+            Object o = field.get(object);
+            if (o instanceof String) {
+              field.set(object, expandText((String) o));
+            } else if (o instanceof List) {
+              @SuppressWarnings("unchecked")
+              List<String> forceCheck = List.class.cast(o);
+              expandElements(forceCheck);
+            }
+          }
+        } catch (IllegalAccessException e) {
+          throw new RuntimeException(e);
         }
-      } 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()));
+    /** Expand all properties in the Strings in the List */
+    public void expandElements(List<String> list) {
+      if (list != null) {
+        for (ListIterator<String> it = list.listIterator(); it.hasNext(); ) {
+          it.set(expandText(it.next()));
+        }
       }
     }
-  }
 
-  protected String expandLiteral(String literal) {
-    if (literal == null) {
-      return null;
+    /** Expand all properties (${property_name} -> property_value) in the given text */
+    public String expandText(String text) {
+      if (text == null) {
+        return null;
+      }
+      StringBuffer out = new StringBuffer();
+      Matcher m = PATTERN.matcher(text);
+      while (m.find()) {
+        m.appendReplacement(out, Matcher.quoteReplacement(getValueForName(m.group(1))));
+      }
+      m.appendTail(out);
+      return out.toString();
     }
-    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
+    /** Get the replacement value for the property identified by name */
+    protected String getValueForName(String name) {
+      String value = valueByName.get(name);
+      return value == null ? "" : value;
     }
-    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 4e84e67..1e19f7b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -18,12 +18,10 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
-import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.DynamicOptions.BeanProvider;
 import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
 import com.googlesource.gerrit.plugins.task.TaskTree.Node;
@@ -34,6 +32,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public class TaskAttributeFactory implements ChangePluginDefinedInfoFactory {
@@ -68,17 +67,14 @@
   }
 
   protected final TaskTree definitions;
-  protected final ChangeQueryBuilder cqb;
-
-  protected final Map<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>>
-      predicatesByQuery = new HashMap<>();
+  protected final PredicateCache predicateCache;
 
   protected Modules.MyOptions options;
 
   @Inject
-  public TaskAttributeFactory(TaskTree definitions, ChangeQueryBuilder cqb) {
+  public TaskAttributeFactory(TaskTree definitions, PredicateCache predicateCache) {
     this.definitions = definitions;
-    this.cqb = cqb;
+    this.predicateCache = predicateCache;
   }
 
   @Override
@@ -98,8 +94,12 @@
   protected PluginDefinedInfo createWithExceptions(ChangeData c) {
     TaskPluginAttribute a = new TaskPluginAttribute();
     try {
-      for (Node node : definitions.getRootNodes()) {
-        addApplicableTasks(a.roots, c, node);
+      for (Node node : definitions.getRootNodes(c)) {
+        if (node == null) {
+          a.roots.add(invalid());
+        } else {
+          new AttributeFactory(node).create().ifPresent(t -> a.roots.add(t));
+        }
       }
     } catch (ConfigInvalidException | IOException e) {
       a.roots.add(invalid());
@@ -111,51 +111,164 @@
     return a;
   }
 
-  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();
-      }
+  protected class AttributeFactory {
+    public Node node;
+    public MatchCache matchCache;
+    protected Task task;
+    protected TaskAttribute attribute;
 
-      boolean applicable = match(c, def.applicable);
-      if (!def.isVisible) {
-        if (!def.isTrusted || (!applicable && !options.onlyApplicable)) {
-          atts.add(unknown());
-          return;
+    protected AttributeFactory(Node node) {
+      this(node, new MatchCache(predicateCache, node.getChangeData()));
+    }
+
+    protected AttributeFactory(Node node, MatchCache matchCache) {
+      this.node = node;
+      this.matchCache = matchCache;
+      this.task = node.task;
+      this.attribute = new TaskAttribute(task.name);
+    }
+
+    public Optional<TaskAttribute> create() {
+      try {
+        if (options.evaluationTime) {
+          attribute.evaluationMilliSeconds = millis();
         }
-      }
 
-      if (applicable || !options.onlyApplicable) {
-        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)) {
-          att.status = Status.INVALID;
-        }
-        boolean groupApplicable = att.status != null;
-
-        if (groupApplicable || !options.onlyApplicable) {
-          if (!options.onlyInvalid || att.status == Status.INVALID || att.subTasks != null) {
-            if (!options.onlyApplicable) {
-              att.applicable = applicable;
-            }
-            if (def.inProgress != null) {
-              att.inProgress = matchOrNull(c, def.inProgress);
-            }
-            att.hint = getHint(att.status, def);
-            att.exported = def.exported;
-
-            if (options.evaluationTime) {
-              att.evaluationMilliSeconds = millis() - att.evaluationMilliSeconds;
-            }
-            atts.add(att);
+        boolean applicable = matchCache.match(task.applicable);
+        if (!task.isVisible) {
+          if (!task.isTrusted || (!applicable && !options.onlyApplicable)) {
+            return Optional.of(unknown());
           }
         }
+
+        if (applicable || !options.onlyApplicable) {
+          attribute.hasPass = task.pass != null || task.fail != null;
+          attribute.subTasks = getSubTasks();
+          attribute.status = getStatus();
+          if (options.onlyInvalid && !isValidQueries()) {
+            attribute.status = Status.INVALID;
+          }
+          boolean groupApplicable = attribute.status != null;
+
+          if (groupApplicable || !options.onlyApplicable) {
+            if (!options.onlyInvalid
+                || attribute.status == Status.INVALID
+                || attribute.subTasks != null) {
+              if (!options.onlyApplicable) {
+                attribute.applicable = applicable;
+              }
+              if (task.inProgress != null) {
+                attribute.inProgress = matchCache.matchOrNull(task.inProgress);
+              }
+              attribute.hint = getHint(attribute.status, task);
+              attribute.exported = task.exported.isEmpty() ? null : task.exported;
+
+              if (options.evaluationTime) {
+                attribute.evaluationMilliSeconds = millis() - attribute.evaluationMilliSeconds;
+              }
+              return Optional.of(attribute);
+            }
+          }
+        }
+      } catch (QueryParseException | RuntimeException e) {
+        return Optional.of(invalid()); // bad applicability query
       }
-    } catch (QueryParseException | RuntimeException e) {
-      atts.add(invalid()); // bad applicability query
+      return Optional.empty();
+    }
+
+    protected Status getStatusWithExceptions() throws StorageException, QueryParseException {
+      if (isAllNull(task.pass, task.fail, attribute.subTasks)) {
+        // A leaf def has no defined subdefs.
+        boolean hasDefinedSubtasks =
+            !(task.subTasks.isEmpty()
+                && task.subTasksFiles.isEmpty()
+                && task.subTasksExternals.isEmpty()
+                && task.subTasksFactories.isEmpty());
+        if (hasDefinedSubtasks) {
+          // Remove 'Grouping" tasks (tasks with subtasks but no PASS
+          // or FAIL criteria) from the output if none of their subtasks
+          // are applicable.  i.e. grouping tasks only really apply if at
+          // least one of their subtasks apply.
+          return null;
+        }
+        // A leaf configuration without a PASS or FAIL criteria is a
+        // missconfiguration.  Either someone forgot to add subtasks, or
+        // they forgot to add a PASS or FAIL criteria.
+        return Status.INVALID;
+      }
+
+      if (task.fail != null) {
+        if (matchCache.match(task.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.
+          //
+          // It is also important that FAIL be useable to indicate that
+          // the task has actually executed.  Thus subtask status,
+          // including a subtask FAIL should not appear as a FAIL on the
+          // parent task.  This means that this is should be the only path
+          // to make a task have a FAIL status.
+          return Status.FAIL;
+        }
+      }
+
+      if (attribute.subTasks != null && !isAll(attribute.subTasks, Status.PASS)) {
+        // It is possible for a subtask's PASS criteria to change while
+        // a parent task is executing, or even after the parent task
+        // completes.  This can result in the parent PASS criteria being
+        // met while one or more of its subtasks no longer meets its PASS
+        // criteria (the subtask may now even meet a FAIL criteria).  We
+        // never want the parent task to reflect a PASS criteria in these
+        // cases, thus we can safely return here without ever evaluating
+        // the task's PASS criteria.
+        return Status.WAITING;
+      }
+
+      if (task.pass != null && !matchCache.match(task.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
+        // be expected to execute (how would you know if it has?), thus a
+        // pass criteria is required to possibly even be considered for
+        // READY.
+        return Status.READY;
+      }
+
+      return Status.PASS;
+    }
+
+    protected Status getStatus() {
+      try {
+        return getStatusWithExceptions();
+      } catch (QueryParseException | RuntimeException e) {
+        return Status.INVALID;
+      }
+    }
+
+    protected List<TaskAttribute> getSubTasks() throws StorageException {
+      List<TaskAttribute> subTasks = new ArrayList<>();
+      for (Node subNode : node.getSubNodes()) {
+        if (subNode == null) {
+          subTasks.add(invalid());
+        } else {
+          new AttributeFactory(subNode, matchCache).create().ifPresent(t -> subTasks.add(t));
+        }
+      }
+      if (subTasks.isEmpty()) {
+        return null;
+      }
+      return subTasks;
+    }
+
+    protected boolean isValidQueries() {
+      try {
+        matchCache.match(task.inProgress);
+        matchCache.match(task.fail);
+        matchCache.match(task.pass);
+        return true;
+      } catch (QueryParseException | RuntimeException e) {
+        return false;
+      }
     }
   }
 
@@ -163,22 +276,7 @@
     return System.nanoTime() / 1000000;
   }
 
-  protected List<TaskAttribute> getSubTasks(ChangeData c, Node node) {
-    List<TaskAttribute> subTasks = new ArrayList<>();
-    for (Node subNode : node.getSubNodes()) {
-      if (subNode == null) {
-        subTasks.add(invalid());
-      } else {
-        addApplicableTasks(subTasks, c, subNode);
-      }
-    }
-    if (subTasks.isEmpty()) {
-      return null;
-    }
-    return subTasks;
-  }
-
-  protected static TaskAttribute invalid() {
+  protected TaskAttribute invalid() {
     // For security reasons, do not expose the task name without knowing
     // the visibility which is derived from its applicability.
     TaskAttribute a = unknown();
@@ -186,98 +284,12 @@
     return a;
   }
 
-  protected static TaskAttribute unknown() {
+  protected TaskAttribute unknown() {
     TaskAttribute a = new TaskAttribute("UNKNOWN");
     a.status = Status.UNKNOWN;
     return a;
   }
 
-  protected boolean isValidQueries(ChangeData c, Task def) {
-    try {
-      match(c, def.inProgress);
-      match(c, def.fail);
-      match(c, def.pass);
-      return true;
-    } catch (QueryParseException | RuntimeException e) {
-      return false;
-    }
-  }
-
-  protected Status getStatus(ChangeData c, Task def, TaskAttribute a) {
-    try {
-      return getStatusWithExceptions(c, def, a);
-    } catch (QueryParseException | RuntimeException e) {
-      return Status.INVALID;
-    }
-  }
-
-  protected Status getStatusWithExceptions(ChangeData c, Task def, TaskAttribute a)
-      throws QueryParseException {
-    if (isAllNull(def.pass, def.fail, a.subTasks)) {
-      // A leaf def has no defined subdefs.
-      boolean hasDefinedSubtasks =
-          !(def.subTasks.isEmpty()
-              && def.subTasksFiles.isEmpty()
-              && def.subTasksExternals.isEmpty()
-              && def.subTasksFactories.isEmpty());
-      if (hasDefinedSubtasks) {
-        // Remove 'Grouping" tasks (tasks with subtasks but no PASS
-        // or FAIL criteria) from the output if none of their subtasks
-        // are applicable.  i.e. grouping tasks only really apply if at
-        // least one of their subtasks apply.
-        return null;
-      }
-      // A leaf configuration without a PASS or FAIL criteria is a
-      // missconfiguration.  Either someone forgot to add subtasks, or
-      // they forgot to add a PASS or FAIL criteria.
-      return Status.INVALID;
-    }
-
-    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.
-        //
-        // It is also important that FAIL be useable to indicate that
-        // the task has actually executed.  Thus subtask status,
-        // including a subtask FAIL should not appear as a FAIL on the
-        // parent task.  This means that this is should be the only path
-        // to make a task have a FAIL status.
-        return Status.FAIL;
-      }
-      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;
-      }
-    }
-
-    if (a.subTasks != null && !isAll(a.subTasks, Status.PASS)) {
-      // It is possible for a subtask's PASS criteria to change while
-      // a parent task is executing, or even after the parent task
-      // completes.  This can result in the parent PASS criteria being
-      // met while one or more of its subtasks no longer meets its PASS
-      // criteria (the subtask may now even meet a FAIL criteria).  We
-      // never want the parent task to reflect a PASS criteria in these
-      // cases, thus we can safely return here without ever evaluating
-      // the task's PASS criteria.
-      return Status.WAITING;
-    }
-
-    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
-      // be expected to execute (how would you know if it has?), thus a
-      // pass criteria is required to possibly even be considered for
-      // READY.
-      return Status.READY;
-    }
-
-    return Status.PASS;
-  }
-
   protected String getHint(Status status, Task def) {
     if (status == Status.READY) {
       return def.readyHint;
@@ -287,60 +299,18 @@
     return null;
   }
 
-  protected static boolean isAll(Iterable<TaskAttribute> atts, Status state) {
-    for (TaskAttribute att : atts) {
-      if (att.status != state) {
+  public static boolean isAllNull(Object... vals) {
+    for (Object val : vals) {
+      if (val != null) {
         return false;
       }
     }
     return true;
   }
 
-  protected boolean match(ChangeData c, String query) throws StorageException, QueryParseException {
-    if (query == null) {
-      return true;
-    }
-    return matchWithExceptions(c, query);
-  }
-
-  protected Boolean matchOrNull(ChangeData c, String query) {
-    if (query != null) {
-      try {
-        return matchWithExceptions(c, query);
-      } catch (QueryParseException | RuntimeException e) {
-      }
-    }
-    return null;
-  }
-
-  protected boolean matchWithExceptions(ChangeData c, String query)
-      throws QueryParseException, StorageException {
-    if ("true".equalsIgnoreCase(query)) {
-      return true;
-    }
-    return getPredicate(query).asMatchable().match(c);
-  }
-
-  protected Predicate<ChangeData> getPredicate(String query) throws QueryParseException {
-    ThrowingProvider<Predicate<ChangeData>, QueryParseException> predProvider =
-        predicatesByQuery.get(query);
-    if (predProvider != null) {
-      return predProvider.get();
-    }
-    // never seen 'query' before
-    try {
-      Predicate<ChangeData> pred = cqb.parse(query);
-      predicatesByQuery.put(query, new ThrowingProvider.Entry<>(pred));
-      return pred;
-    } catch (QueryParseException e) {
-      predicatesByQuery.put(query, new ThrowingProvider.Thrown<>(e));
-      throw e;
-    }
-  }
-
-  protected static boolean isAllNull(Object... vals) {
-    for (Object val : vals) {
-      if (val != null) {
+  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 a60569c..77adee2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -31,7 +31,19 @@
 
 /** Task Configuration file living in git */
 public class TaskConfig extends AbstractVersionedMetaData {
-  protected class Section extends Container {
+  public enum NamesFactoryType {
+    CHANGE,
+    STATIC;
+
+    public static NamesFactoryType getNamesFactoryType(String str) {
+      for (NamesFactoryType type : NamesFactoryType.values()) {
+        if (type.name().equalsIgnoreCase(str)) return type;
+      }
+      return null;
+    }
+  }
+
+  private class Section extends Container {
     public TaskConfig config;
 
     public Section() {
@@ -112,10 +124,12 @@
   }
 
   public class NamesFactory extends Section {
+    public String changes;
     public List<String> names;
     public String type;
 
     public NamesFactory(SubSection s) {
+      changes = getString(s, KEY_CHANGES, null);
       names = getStringList(s, KEY_NAME);
       type = getString(s, KEY_TYPE, null);
     }
@@ -142,6 +156,7 @@
   protected static final String SECTION_TASK = "task";
   protected static final String SECTION_TASKS_FACTORY = "tasks-factory";
   protected static final String KEY_APPLICABLE = "applicable";
+  protected static final String KEY_CHANGES = "changes";
   protected static final String KEY_EXPORT_PREFIX = "export-";
   protected static final String KEY_FAIL = "fail";
   protected static final String KEY_FAIL_HINT = "fail-hint";
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 a15e8f5..461d14e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -14,17 +14,25 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.task.TaskConfig.External;
 import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
+import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactoryType;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
 import com.googlesource.gerrit.plugins.task.TaskConfig.TasksFactory;
 import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
@@ -47,6 +55,7 @@
  * lazily loaded tree, and much of the tree validity is enforced at this layer.
  */
 public class TaskTree {
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
   protected static final String TASK_DIR = "task";
 
   protected final AccountResolver accountResolver;
@@ -54,6 +63,10 @@
   protected final CurrentUser user;
   protected final TaskConfigFactory taskFactory;
   protected final Root root = new Root();
+  protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
+  protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
+
+  protected ChangeData changeData;
 
   @Inject
   public TaskTree(
@@ -61,18 +74,23 @@
       AllUsersNameProvider allUsers,
       AnonymousUser anonymousUser,
       CurrentUser user,
-      TaskConfigFactory taskFactory) {
+      TaskConfigFactory taskFactory,
+      Provider<ChangeQueryBuilder> changeQueryBuilderProvider,
+      Provider<ChangeQueryProcessor> changeQueryProcessorProvider) {
     this.accountResolver = accountResolver;
     this.allUsers = allUsers;
     this.user = user != null ? user : anonymousUser;
     this.taskFactory = taskFactory;
+    this.changeQueryProcessorProvider = changeQueryProcessorProvider;
+    this.changeQueryBuilderProvider = changeQueryBuilderProvider;
   }
 
   public void masquerade(PatchSetArgument psa) {
     taskFactory.masquerade(psa);
   }
 
-  public List<Node> getRootNodes() throws ConfigInvalidException, IOException {
+  public List<Node> getRootNodes(ChangeData changeData) throws ConfigInvalidException, IOException {
+    this.changeData = changeData;
     return root.getRootNodes();
   }
 
@@ -80,28 +98,41 @@
     protected LinkedList<String> path = new LinkedList<>();
     protected List<Node> nodes;
     protected Set<String> names = new HashSet<>();
+    protected Map<String, String> properties;
 
-    protected void addSubDefinitions(List<Task> defs, Map<String, String> parentProperties) {
+    protected void addSubDefinitions(List<Task> defs) {
       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);
+        addSubDefinition(def);
       }
     }
+
+    protected void addSubDefinition(Task def) {
+      Node node = null;
+      if (def != null && !path.contains(def.name) && names.add(def.name)) {
+        // path check above detects looping definitions
+        // names check above detects duplicate subtasks
+        try {
+          node = new Node(def, path, properties);
+        } catch (Exception e) {
+        } // bad definition, handled with null
+      }
+      nodes.add(node);
+    }
+
+    public ChangeData getChangeData() {
+      return TaskTree.this.changeData;
+    }
   }
 
   protected class Root extends NodeList {
+    protected Root() {
+      properties = new HashMap<String, String>();
+    }
+
     public List<Node> getRootNodes() throws ConfigInvalidException, IOException {
       if (nodes == null) {
         nodes = new ArrayList<>();
-        addSubDefinitions(getRootDefinitions(), new HashMap<String, String>());
+        addSubDefinitions(getRootDefinitions());
       }
       return nodes;
     }
@@ -112,15 +143,16 @@
   }
 
   public class Node extends NodeList {
-    public final Task definition;
+    public final Task task;
 
     public Node(Task definition, List<String> path, Map<String, String> parentProperties)
-        throws ConfigInvalidException {
-      this.definition = definition;
+        throws ConfigInvalidException, StorageException {
+      this.task = definition;
       this.path.addAll(path);
       this.path.add(definition.name);
       Preloader.preload(definition);
-      new Properties(definition, parentProperties);
+      new Properties(getChangeData(), definition, parentProperties);
+      properties = definition.properties;
     }
 
     public List<Node> getSubNodes() {
@@ -131,84 +163,110 @@
       return nodes;
     }
 
-    protected void addSubDefinitions() {
-      addSubDefinitions(getSubDefinitions());
-      addSubDefinitions(getTasksFactoryDefinitions());
+    protected void addSubDefinitions() throws StorageException {
+      addSubTaskDefinitions();
+      addSubTasksFactoryDefinitions();
       addSubFileDefinitions();
       addExternalDefinitions();
     }
 
-    protected void addSubDefinitions(List<Task> defs) {
-      addSubDefinitions(defs, definition.properties);
-    }
-
-    protected void addSubFileDefinitions() {
-      for (String file : definition.subTasksFiles) {
+    protected void addSubTaskDefinitions() {
+      for (String name : task.subTasks) {
         try {
-          addSubDefinitions(getTaskDefinitions(definition.config.getBranch(), file));
-        } catch (ConfigInvalidException | IOException e) {
-          nodes.add(null);
+          Task def = task.config.getTaskOptional(name);
+          if (def != null) {
+            addSubDefinition(def);
+          }
+        } catch (ConfigInvalidException e) {
+          addSubDefinition(null);
         }
       }
     }
 
-    protected void addExternalDefinitions() {
-      for (String external : definition.subTasksExternals) {
+    protected void addSubFileDefinitions() {
+      for (String file : task.subTasksFiles) {
         try {
-          External ext = definition.config.getExternal(external);
+          addSubDefinitions(getTaskDefinitions(task.config.getBranch(), file));
+        } catch (ConfigInvalidException | IOException e) {
+          addSubDefinition(null);
+        }
+      }
+    }
+
+    protected void addExternalDefinitions() throws StorageException {
+      for (String external : task.subTasksExternals) {
+        try {
+          External ext = task.config.getExternal(external);
           if (ext == null) {
-            nodes.add(null);
+            addSubDefinition(null);
           } else {
             addSubDefinitions(getTaskDefinitions(ext));
           }
         } catch (ConfigInvalidException | IOException e) {
-          nodes.add(null);
+          addSubDefinition(null);
         }
       }
     }
 
-    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 defs;
-    }
-
-    protected List<Task> getTasksFactoryDefinitions() {
+    protected void addSubTasksFactoryDefinitions() throws StorageException {
       List<Task> taskList = new ArrayList<>();
-      for (String taskFactoryName : definition.subTasksFactories) {
-        TasksFactory tasksFactory = definition.config.getTasksFactory(taskFactoryName);
+      for (String taskFactoryName : task.subTasksFactories) {
+        TasksFactory tasksFactory = task.config.getTasksFactory(taskFactoryName);
         if (tasksFactory != null) {
-          NamesFactory namesFactory = definition.config.getNamesFactory(tasksFactory.namesFactory);
-          if (namesFactory != null && "static".equals(namesFactory.type)) {
-            for (String name : namesFactory.names) {
-              taskList.add(definition.config.createTask(tasksFactory, name));
+          NamesFactory namesFactory = task.config.getNamesFactory(tasksFactory.namesFactory);
+          if (namesFactory != null && namesFactory.type != null) {
+            new Properties(namesFactory, task.properties);
+            switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) {
+              case STATIC:
+                addStaticTypeTasksDefinitions(tasksFactory, namesFactory);
+                continue;
+              case CHANGE:
+                addChangesTypeTaskDefinitions(tasksFactory, namesFactory);
+                continue;
             }
-            continue;
           }
         }
-        taskList.add(null);
+        addSubDefinition(null);
       }
-      return taskList;
+    }
+
+    protected void addStaticTypeTasksDefinitions(
+        TasksFactory tasksFactory, NamesFactory namesFactory) {
+      for (String name : namesFactory.names) {
+        addSubDefinition(task.config.createTask(tasksFactory, name));
+      }
+    }
+
+    protected void addChangesTypeTaskDefinitions(
+        TasksFactory tasksFactory, NamesFactory namesFactory) {
+      try {
+        if (namesFactory.changes != null) {
+          List<ChangeData> changeDataList =
+              changeQueryProcessorProvider
+                  .get()
+                  .query(changeQueryBuilderProvider.get().parse(namesFactory.changes))
+                  .entities();
+          for (ChangeData changeData : changeDataList) {
+            addSubDefinition(task.config.createTask(tasksFactory, changeData.getId().toString()));
+          }
+          return;
+        }
+      } catch (StorageException e) {
+        log.atSevere().withCause(e).log("ERROR: running changes query: " + namesFactory.changes);
+      } catch (QueryParseException e) {
+      }
+      addSubDefinition(null);
     }
 
     protected List<Task> getTaskDefinitions(External external)
-        throws ConfigInvalidException, IOException {
+        throws ConfigInvalidException, IOException, StorageException {
       return getTaskDefinitions(resolveUserBranch(external.user), external.file);
     }
 
     protected List<Task> getTaskDefinitions(BranchNameKey branch, String file)
         throws ConfigInvalidException, IOException {
       return taskFactory
-          .getTaskConfig(branch, resolveTaskFileName(file), definition.isTrusted)
+          .getTaskConfig(branch, resolveTaskFileName(file), task.isTrusted)
           .getTasks();
     }
 
@@ -224,7 +282,7 @@
     }
 
     protected BranchNameKey resolveUserBranch(String user)
-        throws ConfigInvalidException, IOException {
+        throws ConfigInvalidException, IOException, StorageException {
       if (user == null) {
         throw new ConfigInvalidException("External user not defined");
       }
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 9fe4e3d..8c1309e 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -119,6 +119,12 @@
 one subtask is applicable. Setting this to "True" is useful for defining
 informational tasks that are not really expected to execute.
 
+A task with a `fail` key but no pass key has an implied `pass` key which is
+the opposite of the `fail` key as if the fail had a `NOT` in front of it.
+Such tasks can only pass, fail, or be waiting for their subtasks, they
+can never be ready! If they have not failed, and their subtasks have
+passed, they have passed also.
+
 Example:
 ```
     pass = label:verified+1
@@ -331,6 +337,16 @@
 
 The following keys may be defined in any names-factory section:
 
+`changes`
+
+: This key defines a query that is used to fetch change numbers which will be used
+as the names of the task(s).
+
+Example:
+```
+    changes = change:1 OR change:2
+```
+
 `name`
 
 : This key defines the name of the tasks.  This key may be used several times
@@ -345,12 +361,15 @@
 
 `type`
 
-: This key defines the type of the names-factory section.  The only
-accepted value is `static`.
+: This key defines the type of the names-factory section.  The type
+can be either `static` or `change`. For names-factory of type `static`,
+`name` key(s) should be defined where as names-factory of type `change`
+needs a `change` key to be defined.
 
 Example:
 ```
     type = static
+    type = change
 ```
 
 External Entries
@@ -385,12 +404,25 @@
 
 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.
+The task plugin supplies the following properties which may be used anywhere in
+a task, tasks-factory, or names-factory definition.
 
-Example:
+```
+    ${_name}            represents the name of the current task
+    ${_change_number}   represents the change number of the current change
+    ${_change_id}       represents the change id of the current change
+    ${_change_project}  represents the project of the current change
+    ${_change_branch}   represents the branch of the current change
+    ${_change_status}   represents the status of the current change
+    ${_change_topic}    represents the topic of the current change
+```
+
+Examples:
 ```
     fail-hint = {$_name} needs to be fixed
+    fail-hint = {$_change_number} with {$_change_status} needs to be fixed
+    fail-hint = {$_change_id} on {$_change_project} and {$_change_branch} needs to be fixed
+    changes = parentof:${_change_number} project:${_change_project} branch:${_change_branch}
 ```
 
 Custom properties may be defined on a task using the following syntax:
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
index 58d6ca7..8b0c705 100644
--- a/src/main/resources/Documentation/task_states.md
+++ b/src/main/resources/Documentation/task_states.md
@@ -38,6 +38,21 @@
   applicable = is:open
   fail = is:open
 
+[root "Root PASS-waiting-fail"]
+  applicable = is:open
+  fail = NOT is:open
+  subtask = Subtask PASS
+
+[root "Root pass-WAITING-fail"]
+  applicable = is:open
+  fail = NOT is:open
+  subtask = Subtask FAIL
+
+[root "Root pass-waiting-FAIL"]
+  applicable = is:open
+  fail = is:open
+  subtask = Subtask PASS
+
 [root "Root grouping PASS (subtask PASS)"]
   subtask = Subtask PASS
 
@@ -105,9 +120,11 @@
 
 [root "Root tasks-factory"]
   subtasks-factory = tasks-factory static
+  subtasks-factory = tasks-factory change
 
 [root "Root tasks-factory static (empty name)"]
   subtasks-factory = tasks-factory static (empty name)
+# Grouping task since it has no pass criteria, not output since it has no subtasks
 
 [root "Root tasks-factory static (empty name PASS)"]
   pass = True
@@ -117,13 +134,16 @@
   set-root-property = root-value
   export-root = ${_name}
   fail = True
-  fail-hint = Name(${_name})
+  fail-hint = Name(${_name}) Change Number(${_change_number}) Change Id(${_change_id}) Change Project(${_change_project}) Change Branch(${_change_branch}) Change Status(${_change_status}) Change Topic(${_change_topic})
   subtask = Subtask Properties
 
 [root "Root Preload"]
    preload-task = Subtask FAIL
    subtask = Subtask Preload
 
+[root "Root INVALID Preload"]
+  preload-task = missing
+
 [root "INVALIDS"]
   subtasks-file = invalids.config
 
@@ -147,6 +167,10 @@
   names-factory = names-factory static (empty name list)
   fail = True
 
+[tasks-factory "tasks-factory change"]
+  names-factory = names-factory change list
+  fail = True
+
 [task "Subtask APPLICABLE"]
   applicable = is:open
   pass = True
@@ -179,6 +203,7 @@
   subtask = Subtask Properties Hints
   subtask = Chained ${_name}
   subtask = Subtask Properties Reset
+  subtasks-factory = TaskFactory Properties Hints
 
 [task "Subtask Properties Hints"]
   set-first-property = first-value
@@ -195,6 +220,15 @@
   set-first-property = reset-first-value
   fail-hint = first-property(${first-property})
 
+[tasks-factory "TaskFactory Properties Hints"]
+  names-factory = NamesFactory Properties
+  fail-hint = Name(${_name}) Change Number(${_change_number}) Change Id(${_change_id}) Change Project(${_change_project}) Change Branch(${_change_branch}) Change Status(${_change_status}) Change Topic(${_change_topic})
+  fail = True
+
+[names-factory "NamesFactory Properties"]
+  type = change
+  changes = change:_change1_number OR change:${_change_number} project:${_change_project} branch:${_change_branch}
+
 [task "Subtask Preload"]
   preload-task = Subtask READY
   subtask = Subtask Preload Preload
@@ -260,11 +294,16 @@
   name = my a task
   name = my b task
   name = my c task
+  name = my d task Change Number(${_change_number}) Change Id(${_change_id}) Change Project(${_change_project}) Change Branch(${_change_branch}) Change Status(${_change_status}) Change Topic(${_change_topic})
   type = static
 
 [names-factory "names-factory static (empty name list)"]
   type = static
 
+[names-factory "names-factory change list"]
+  changes = change:_change1_number OR change:_change2_number
+  type = change
+
 ```
 
 `task/common.config` file in project `All-Projects` on ref `refs/meta/config`.
@@ -341,20 +380,75 @@
 [task "task (names-factory type INVALID)"]
   subtasks-factory = tasks-factory (names-factory type INVALID)
 
+[task "task (names-factory duplicate)"]
+  subtasks-factory = tasks-factory (names-factory duplicate)
+
+[task "task (names-factory changes type missing)"]
+  subtasks-factory = tasks-factory change (names-factory type missing)
+
+[task "task (names-factory changes missing)"]
+  subtasks-factory = tasks-factory change (names-factory changes missing)
+
+[task "task (names-factory changes invalid)"]
+  subtasks-factory = tasks-factory change (names-factory changes invalid)
+
+[task "task (tasks-factory changes loop)"]
+  subtasks-factory = tasks-factory change loop
+
 [tasks-factory "tasks-factory (names-factory type missing)"]
   names-factory = names-factory (type missing)
   fail = True
 
+[tasks-factory "tasks-factory (names-factory type INVALID)"]
+  names-factory = name-factory (type INVALID)
+
+[tasks-factory "tasks-factory (names-factory duplicate)"]
+  names-factory = names-factory duplicate
+  fail = True
+
+[tasks-factory "tasks-factory change (names-factory type missing)"]
+  names-factory = names-factory change list (type missing)
+  fail = True
+
+[tasks-factory "tasks-factory change (names-factory changes missing)"]
+  names-factory = names-factory change list (changes missing)
+  fail = True
+
+[tasks-factory "tasks-factory change (names-factory changes invalid)"]
+  names-factory = names-factory change list (changes invalid)
+  fail = True
+
+[tasks-factory "tasks-factory change loop"]
+  names-factory = names-factory change constant
+  subtask = task (tasks-factory changes loop)
+  fail = true
+
 [names-factory "names-factory (type missing)"]
   name = no type test
 
-[tasks-factory "tasks-factory (names-factory type INVALID)"]
-  names-factory = name-factory (type INVALID)
+[names-factory "names-factory change list (type missing)"]
+  changes = change:_change1_number OR change:_change2_number
 
 [names-factory "names-factory (type INVALID)"]
   name = invalid type test
   type = invalid
 
+[names-factory "names-factory duplicate"]
+  name = duplicate
+  name = duplicate
+  type = static
+
+[names-factory "names-factory change list (changes missing)"]
+  type = change
+
+[names-factory "names-factory change list (changes invalid)"]
+  change = change:invalidChange
+  type = change
+
+[names-factory "names-factory change constant"]
+  changes = change:_change1_number OR change:_change2_number
+  type = change
+
 ```
 
 `task/special.config` file in project `All-Users` on ref `refs/users/self`.
@@ -423,6 +517,42 @@
                "status" : "FAIL"
             },
             {
+               "hasPass" : true,
+               "name" : "Root PASS-waiting-fail",
+               "status" : "PASS",
+               "subTasks" : [
+                  {
+                     "hasPass" : true,
+                     "name" : "Subtask PASS",
+                     "status" : "PASS"
+                  }
+               ]
+            },
+            {
+               "hasPass" : true,
+               "name" : "Root pass-WAITING-fail",
+               "status" : "WAITING",
+               "subTasks" : [
+                  {
+                     "hasPass" : true,
+                     "name" : "Subtask FAIL",
+                     "status" : "FAIL"
+                  }
+               ]
+            },
+            {
+               "hasPass" : true,
+               "name" : "Root pass-waiting-FAIL",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "hasPass" : true,
+                     "name" : "Subtask PASS",
+                     "status" : "PASS"
+                  }
+               ]
+            },
+            {
                "hasPass" : false,
                "name" : "Root grouping PASS (subtask PASS)",
                "status" : "PASS",
@@ -686,6 +816,21 @@
                      "hasPass" : true,
                      "name" : "my c task",
                      "status" : "FAIL"
+                  },
+                  {
+                     "hasPass" : true,
+                     "name" : "my d task Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
+                     "status" : "FAIL"
+                  },
+                  {
+                     "hasPass" : true,
+                     "name" : "_change1_number",
+                     "status" : "FAIL"
+                  },
+                  {
+                     "hasPass" : true,
+                     "name" : "_change2_number",
+                     "status" : "FAIL"
                   }
                ]
             },
@@ -699,7 +844,7 @@
                   "root" : "Root Properties"
                },
                "hasPass" : true,
-               "hint" : "Name(Root Properties)",
+               "hint" : "Name(Root Properties) Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
                "name" : "Root Properties",
                "status" : "FAIL",
                "subTasks" : [
@@ -726,6 +871,18 @@
                            "hasPass" : true,
                            "name" : "Subtask Properties Reset",
                            "status" : "PASS"
+                        },
+                        {
+                           "hasPass" : true,
+                           "hint" : "Name(_change3_number) Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
+                           "name" : "_change3_number",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "hasPass" : true,
+                           "hint" : "Name(_change1_number) Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
+                           "name" : "_change1_number",
+                           "status" : "FAIL"
                         }
                      ]
                   }
@@ -806,6 +963,10 @@
                ]
             },
             {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            },
+            {
                "hasPass" : false,
                "name" : "INVALIDS",
                "status" : "WAITING",
@@ -940,6 +1101,84 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : true,
+                           "name" : "duplicate",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             }
diff --git a/src/main/resources/Documentation/test.md b/src/main/resources/Documentation/test.md
new file mode 100644
index 0000000..d170e58
--- /dev/null
+++ b/src/main/resources/Documentation/test.md
@@ -0,0 +1,19 @@
+Manual Tests
+------------
+
+1. Test that task validation for more than one change provides different
+   results. This will ensure that the per-change match cache introduced
+   to avoid duplicate queries is being re-newed for each change.
+
+   Pick two changes which have different results for atleast one of the
+   task-applicable queries. For example, we can use a task with status:open
+   as applicability, one of the changes can be open and the other one merged.
+
+   For example, 12345 is open and 12346 is merged and atleast one task has
+   status:open as applicability. Below query should return different results
+   for both changes:
+
+    ssh -x -p 29418 review.example.com gerrit query
+      --format JSON 'change:12345 OR change:12346'
+      --task--all --task--preview 12347,1
+
diff --git a/test/all b/test/all
index 6e29127..4832bba 100644
--- a/test/all
+++ b/test/all
@@ -60,6 +60,48 @@
             },
             {
                "applicable" : true,
+               "hasPass" : true,
+               "name" : "Root PASS-waiting-fail",
+               "status" : "PASS",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "Subtask PASS",
+                     "status" : "PASS"
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Root pass-WAITING-fail",
+               "status" : "WAITING",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "Subtask FAIL",
+                     "status" : "FAIL"
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Root pass-waiting-FAIL",
+               "status" : "FAIL",
+               "subTasks" : [
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "Subtask PASS",
+                     "status" : "PASS"
+                  }
+               ]
+            },
+            {
+               "applicable" : true,
                "hasPass" : false,
                "name" : "Root grouping PASS (subtask PASS)",
                "status" : "PASS",
@@ -380,6 +422,24 @@
                      "hasPass" : true,
                      "name" : "my c task",
                      "status" : "FAIL"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "my d task Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
+                     "status" : "FAIL"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "_change1_number",
+                     "status" : "FAIL"
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : true,
+                     "name" : "_change2_number",
+                     "status" : "FAIL"
                   }
                ]
             },
@@ -400,7 +460,7 @@
                   "root" : "Root Properties"
                },
                "hasPass" : true,
-               "hint" : "Name(Root Properties)",
+               "hint" : "Name(Root Properties) Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
                "name" : "Root Properties",
                "status" : "FAIL",
                "subTasks" : [
@@ -431,6 +491,20 @@
                            "hasPass" : true,
                            "name" : "Subtask Properties Reset",
                            "status" : "PASS"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "Name(_change3_number) Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
+                           "name" : "_change3_number",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "hint" : "Name(_change1_number) Change Number(_change3_number) Change Id(_change3_id) Change Project(_change3_project) Change Branch(_change3_branch) Change Status(_change3_status) Change Topic(_change3_topic)",
+                           "name" : "_change1_number",
+                           "status" : "FAIL"
                         }
                      ]
                   }
@@ -524,6 +598,10 @@
                ]
             },
             {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            },
+            {
                "applicable" : true,
                "hasPass" : false,
                "name" : "INVALIDS",
@@ -692,6 +770,92 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "duplicate",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             },
@@ -876,6 +1040,92 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "duplicate",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             }
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index 0ed0dcb..beff75a 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -16,14 +16,29 @@
 }
 
 # --------
+gssh() { ssh -x -p "$PORT" "$SERVER" gerrit "$@" ; } # cmd [args]...
 
 q() { "$@" > /dev/null 2>&1 ; } # cmd [args...]  # quiet a command
 
+gen_change_id() { echo "I$(uuidgen | openssl dgst -sha1 -binary | xxd -p)"; } # > change_id
+
+commit_message() { printf "$1 \n\nChange-Id: $2" ; } # message change-id > commit_msg
+
 # Run a test setup command quietly, exit on failure
 q_setup() { # cmd [args...]
   local out ; out=$("$@" 2>&1) || { echo "$out" ; exit ; }
 }
 
+replace_change_properties() { # file change_token change_number change_id project branch status topic
+
+    sed -i -e "s/_change$2_number/$3/g" \
+              -e "s/_change$2_id/$4/g" \
+              -e "s/_change$2_project/$5/g" \
+              -e "s/_change$2_branch/$6/g" \
+              -e "s/_change$2_status/$7/g" \
+              -e "s/_change$2_topic/$8/g" "$1"
+}
+
 replace_user() { # < text_with_testuser > text_with_$USER
     sed -e"s/testuser/$USER/"
 }
@@ -68,21 +83,23 @@
     )
 }
 
-create_repo_change() { # repo remote ref > change_num
-    local repo=$1 remote=$2 ref=$3
+create_repo_change() { # repo remote ref [change_id] > change_num
+    local repo=$1 remote=$2 ref=$3 change_id=$4 msg="Test change"
     (
         q cd "$repo"
+        date > file
         q git add .
-        q git commit -m 'Testing task plugin'
+        [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id")
+        q git commit -m "$msg"
         git push "$remote" HEAD:"refs/for/$ref" 2>&1 | get_change_num
     )
 }
 
 query() { # query
-    ssh -x -p "$PORT" "$SERVER" gerrit query "$@" \
-            --format json | head -1 | python -c "import sys, json; \
-            print json.dumps(json.loads(sys.stdin.read()), indent=3, \
-            separators=(',', ' : '), sort_keys=True)"
+    gssh query "$@" \
+        --format json | head -1 | python -c "import sys, json; \
+        print json.dumps(json.loads(sys.stdin.read()), indent=3, \
+        separators=(',', ' : '), sort_keys=True)"
 }
 
 query_plugins() { query "$@" | awk '$0=="   \"plugins\" : [",$0=="   ],"' ; }
@@ -125,8 +142,11 @@
 [ -z "$SERVER" ] && { echo "You must specify a server" ; exit ; }
 
 PORT=29418
+PROJECT=test
+BRANCH=master
 REMOTE_ALL=ssh://$SERVER:$PORT/All-Projects
 REMOTE_USERS=ssh://$SERVER:$PORT/All-Users
+REMOTE_TEST=ssh://$SERVER:$PORT/$PROJECT
 
 REF_ALL=refs/meta/config
 REF_USERS=refs/users/self
@@ -136,9 +156,24 @@
 mkdir -p "$OUT"
 q_setup setup_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
 q_setup setup_repo "$USERS" "$REMOTE_USERS" "$REF_USERS" --initial-commit
+q_setup setup_repo "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH"
 
 mkdir -p "$ALL_TASKS" "$USER_TASKS"
 
+CHANGES=($(gssh query "status:open limit:2" | grep 'number:' | awk '{print $2}'))
+replace_change_properties "$DOC_STATES" "1" "${CHANGES[0]}"
+replace_change_properties "$DOC_STATES" "2" "${CHANGES[1]}"
+replace_change_properties "$MYDIR/all" "1" "${CHANGES[0]}"
+replace_change_properties "$MYDIR/all" "2" "${CHANGES[1]}"
+replace_change_properties "$MYDIR/preview" "1" "${CHANGES[0]}"
+replace_change_properties "$MYDIR/preview" "2" "${CHANGES[1]}"
+replace_change_properties "$MYDIR/preview.invalid" "1" "${CHANGES[0]}"
+replace_change_properties "$MYDIR/preview.invalid" "2" "${CHANGES[1]}"
+replace_change_properties "$MYDIR/invalid" "1" "${CHANGES[0]}"
+replace_change_properties "$MYDIR/invalid" "2" "${CHANGES[1]}"
+replace_change_properties "$MYDIR/invalid-applicable" "1" "${CHANGES[0]}"
+replace_change_properties "$MYDIR/invalid-applicable" "2" "${CHANGES[1]}"
+
 example 1 |sed -e"s/current-user/$USER/" > "$ROOT_CFG"
 example 2 > "$COMMON_CFG"
 example 3 > "$INVALIDS_CFG"
@@ -149,7 +184,12 @@
 
 example 5 |tail -n +5| awk 'NR>1{print P};{P=$0}' > "$EXPECTED"
 
-query="status:open limit:1"
+change3_id=$(gen_change_id)
+change3_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change3_id")
+replace_change_properties "$EXPECTED" "3" "$change3_number" "$change3_id" "$PROJECT" "refs\/heads\/$BRANCH" "NEW" ""
+replace_change_properties "$MYDIR/all" "3" "$change3_number" "$change3_id" "$PROJECT" "refs\/heads\/$BRANCH" "NEW" ""
+
+query="change:$change3_number status:open"
 test_tasks statuses "$EXPECTED" --task--applicable "$query"
 test_file all --task--all "$query"
 
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile
index aaeeb1d..06691e1 100755
--- a/test/docker/run_tests/Dockerfile
+++ b/test/docker/run_tests/Dockerfile
@@ -7,7 +7,7 @@
 ENV RUN_TESTS_DIR task/test/docker/run_tests
 ENV WORKSPACE $USER_HOME/workspace
 
-RUN apk --update add --no-cache openssh bash git python2 shadow
+RUN apk --update add --no-cache openssh bash git python2 shadow util-linux openssl xxd
 RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config
 
 RUN groupadd -f -g $GID users2
diff --git a/test/docker/run_tests/create-test-project-and-changes.sh b/test/docker/run_tests/create-test-project-and-changes.sh
index 159882b..6192461 100755
--- a/test/docker/run_tests/create-test-project-and-changes.sh
+++ b/test/docker/run_tests/create-test-project-and-changes.sh
@@ -16,17 +16,8 @@
     touch readme.txt && echo "$(date)" >> readme.txt
     git add . && git commit -m "$1"
     git push ssh://"$GERRIT_HOST":"$PORT"/"$2" HEAD:refs/for/master
-    commitRevision=$(git rev-parse HEAD)
-}
-
-submit_change() { # commit_revision
-    gssh review --code-review +2 --submit "$1"
 }
 
 create_project 'test'
 create_change 'Change 1' 'test'
-commit1Revision=$commitRevision
 create_change 'Change 2' 'test'
-#sleep to avoid race conditions
-sleep 60
-submit_change "$commit1Revision"
diff --git a/test/invalid b/test/invalid
index adab1d1..5b5bc32 100644
--- a/test/invalid
+++ b/test/invalid
@@ -47,6 +47,10 @@
                ]
             },
             {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            },
+            {
                "applicable" : true,
                "hasPass" : false,
                "name" : "INVALIDS",
@@ -215,6 +219,86 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             },
@@ -387,6 +471,86 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             }
diff --git a/test/invalid-applicable b/test/invalid-applicable
index 449e595..369ac47 100644
--- a/test/invalid-applicable
+++ b/test/invalid-applicable
@@ -25,6 +25,10 @@
                ]
             },
             {
+               "name" : "UNKNOWN",
+               "status" : "INVALID"
+            },
+            {
                "hasPass" : false,
                "name" : "INVALIDS",
                "status" : "WAITING",
@@ -159,6 +163,79 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             }
diff --git a/test/preview b/test/preview
index e74d35d..235eaca 100644
--- a/test/preview
+++ b/test/preview
@@ -171,6 +171,92 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "duplicate",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             },
@@ -390,6 +476,92 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "duplicate",
+                           "status" : "FAIL"
+                        },
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             }
diff --git a/test/preview.invalid b/test/preview.invalid
index 056bb80..3f0c844 100644
--- a/test/preview.invalid
+++ b/test/preview.invalid
@@ -171,6 +171,86 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             },
@@ -343,6 +423,86 @@
                            "status" : "INVALID"
                         }
                      ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory duplicate)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes type missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes missing)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (names-factory changes invalid)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "name" : "UNKNOWN",
+                           "status" : "INVALID"
+                        }
+                     ]
+                  },
+                  {
+                     "applicable" : true,
+                     "hasPass" : false,
+                     "name" : "task (tasks-factory changes loop)",
+                     "status" : "WAITING",
+                     "subTasks" : [
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change1_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        },
+                        {
+                           "applicable" : true,
+                           "hasPass" : true,
+                           "name" : "_change2_number",
+                           "status" : "FAIL",
+                           "subTasks" : [
+                              {
+                                 "name" : "UNKNOWN",
+                                 "status" : "INVALID"
+                              }
+                           ]
+                        }
+                     ]
                   }
                ]
             }