Merge changes Ifc67b7cb,Iadc9f544,I3e62af9e,I7ee1b63d,I61224c57, ... into stable-2.16

* changes:
  fixup! refactor predicate cache into its own class
  fixup! Add support for tasks-factory and names-factory keywords
  fixup! Support outputting elapsed evaluation time on tasks
  fixup! Revert "Revert "plugin:task Adds support for names-factory of type change""
  Add a Container.toString() to help when debugging.
  Harden inputs to basename
  Fix to alter change context for TaskChangeFactories
diff --git a/src/main/java/com/google/gerrit/common/Container.java b/src/main/java/com/google/gerrit/common/Container.java
index eb0a0f6..6e11534 100644
--- a/src/main/java/com/google/gerrit/common/Container.java
+++ b/src/main/java/com/google/gerrit/common/Container.java
@@ -55,4 +55,21 @@
     }
     return Objects.hash(values);
   }
+
+  @Override
+  public String toString() {
+    List<String> fieldStrings = new ArrayList<>();
+    try {
+      for (Field field : getClass().getDeclaredFields()) {
+        field.setAccessible(true);
+        fieldStrings.add(field.getName() + ": " + Objects.toString(field.get(this)));
+      }
+    } catch (IllegalArgumentException | IllegalAccessException e) {
+    }
+    String fields = String.join(", ", fieldStrings);
+    if (!"".equals(fields)) {
+      fields = "{" + fields + "}";
+    }
+    return getClass().toString() + fields;
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
index f927d51..6346a73 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -68,7 +68,7 @@
     public boolean onlyInvalid = false;
 
     @Option(name = "--evaluation-time", usage = "Include elapsed evaluation time on each task")
-    boolean evaluationTime = false;
+    public boolean evaluationTime = false;
 
     @Option(
         name = "--preview",
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
index 0921db1..aa3da13 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
@@ -59,7 +59,7 @@
     if ("true".equalsIgnoreCase(query)) {
       return true;
     }
-    return cqb.parse(query).asMatchable().match(c);
+    return getPredicate(query).asMatchable().match(c);
   }
 
   protected Predicate<ChangeData> getPredicate(String query) throws QueryParseException {
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 80effb5..8cd8022 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -44,7 +44,7 @@
     }
   }
 
-  private class Section extends Container {
+  protected class Section extends Container {
     public TaskConfig config;
 
     public Section() {
@@ -58,7 +58,6 @@
     public String fail;
     public String failHint;
     public String inProgress;
-    public String name;
     public String pass;
     public String preloadTask;
     public Map<String, String> properties;
@@ -79,7 +78,6 @@
       fail = getString(s, KEY_FAIL, null);
       failHint = getString(s, KEY_FAIL_HINT, null);
       inProgress = getString(s, KEY_IN_PROGRESS, null);
-      name = s.subSection;
       pass = getString(s, KEY_PASS, null);
       preloadTask = getString(s, KEY_PRELOAD_TASK, null);
       properties = getProperties(s, KEY_PROPERTIES_PREFIX);
@@ -133,7 +131,7 @@
 
     public Task(SubSection s, boolean isVisible, boolean isTrusted) {
       super(s, isVisible, isTrusted);
-      name = getString(s, KEY_NAME, s.subSection);
+      name = s.subSection;
     }
 
     protected Task(TaskBase base) {
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 c023f04..ed9e7ee 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -43,6 +43,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
+import java.util.function.BiFunction;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /**
@@ -91,6 +92,14 @@
     return root.getRootNodes();
   }
 
+  public Node createNodeOrNull(NodeList parent, Task definition) {
+    try {
+      return new Node(parent, definition);
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
   protected class NodeList {
     protected NodeList parent = null;
     protected LinkedList<String> path = new LinkedList<>();
@@ -104,20 +113,21 @@
     }
 
     protected void addSubDefinition(Task def) {
+      addSubDefinition(def, (d, c) -> createNodeOrNull(d, c));
+    }
+
+    protected void addSubDefinition(Task def, BiFunction<NodeList, Task, Node> nodeConstructor) {
       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(this, def);
-        } catch (Exception e) {
-        } // bad definition, handled with null
+        node = nodeConstructor.apply(this, def);
       }
       nodes.add(node);
     }
 
     public ChangeData getChangeData() {
-      return TaskTree.this.changeData;
+      return parent == null ? TaskTree.this.changeData : parent.getChangeData();
     }
 
     protected Properties.Task getProperties() {
@@ -244,7 +254,9 @@
                   .query(changeQueryBuilderProvider.get().parse(namesFactory.changes))
                   .entities();
           for (ChangeData changeData : changeDataList) {
-            addSubDefinition(task.config.createTask(tasksFactory, changeData.getId().toString()));
+            addSubDefinition(
+                task.config.createTask(tasksFactory, changeData.getId().toString()),
+                new ChangeNodeFactory(changeData)::createChangeNodeOrNull);
           }
           return;
         }
@@ -295,4 +307,31 @@
       return new Branch.NameKey(allUsers.get(), RefNames.refsUsers(acct.getId()));
     }
   }
+
+  public class ChangeNodeFactory {
+    public class ChangeNode extends Node {
+      public ChangeNode(NodeList parent, Task definition)
+          throws ConfigInvalidException, OrmException {
+        super(parent, definition);
+      }
+
+      public ChangeData getChangeData() {
+        return ChangeNodeFactory.this.changeData;
+      }
+    }
+
+    protected ChangeData changeData;
+
+    public ChangeNodeFactory(ChangeData changeData) {
+      this.changeData = changeData;
+    }
+
+    public ChangeNode createChangeNodeOrNull(NodeList parent, Task definition) {
+      try {
+        return new ChangeNode(parent, definition);
+      } catch (Exception e) {
+        return null;
+      }
+    }
+  }
 }
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index fd7fb37..ea09541 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -739,7 +739,7 @@
 
 [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
+  # Grouping task since it has no pass criteria, not output since it has no subtasks
 
 [tasks-factory "tasks-factory static (empty name)"]
   names-factory = names-factory static (empty name list)
@@ -971,13 +971,13 @@
 
 [tasks-factory "tasks-factory CHANGE Properties"]
   set-welcome-message = Welcome to the pleasuredome
-  names-factory = names-factory my change
+  names-factory = names-factory a change
   fail-hint = ${welcome-message} 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 "names-factory my change"]
+[names-factory "names-factory a change"]
   type = change
-  changes = change:${_change_number}
+  changes = change:_change1_number OR change:_change2_number
 
 {
    "applicable" : true,
@@ -988,8 +988,15 @@
       {
          "applicable" : true,
          "hasPass" : true,
-         "hint" : "Welcome to the pleasuredome Name(_change_number) Change Number(_change_number) Change Id(_change_id) Change Project(_change_project) Change Branch(_change_branch) Change Status(_change_status) Change Topic(_change_topic)",
-         "name" : "_change_number",
+         "hint" : "Welcome to the pleasuredome Name(_change1_number) Change Number(_change1_number) Change Id(_change1_id) Change Project(_change1_project) Change Branch(_change1_branch) Change Status(_change1_status) Change Topic(_change1_topic)",
+         "name" : "_change1_number",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "hint" : "Welcome to the pleasuredome Name(_change2_number) Change Number(_change2_number) Change Id(_change2_id) Change Project(_change2_project) Change Branch(_change2_branch) Change Status(_change2_status) Change Topic(_change2_topic)",
+         "name" : "_change2_number",
          "status" : "FAIL"
       }
    ]
diff --git a/test/docker/run_tests/wait-for-it.sh b/test/docker/run_tests/wait-for-it.sh
index d7b6e3c..17436f2 100755
--- a/test/docker/run_tests/wait-for-it.sh
+++ b/test/docker/run_tests/wait-for-it.sh
@@ -2,7 +2,7 @@
 # https://github.com/vishnubob/wait-for-it/blob/master/wait-for-it.sh
 #   Use this script to test if a given TCP host/port are available
 
-cmdname=$(basename $0)
+cmdname=$(basename -- "$0")
 
 echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }