Merge branch 'stable-3.5' into stable-3.9

* stable-3.5:
  Consider change task siblings & their descendants for duplicates
  UI: Remove 'show all'/'hide all' button from the task
  UI: Add task icons to chips
  Fix plugin type names-factory nodes

Icons were not getting displayed in the task chips when merging 3.5
to 3.9. Fix the icon type to work in 3.9.

Screenshots: https://imgur.com/a/ZQQbY47

Change-Id: Id2fee7c96e3d41f278e3e6df611f750eb56b55e5
diff --git a/gr-task-plugin/gr-task-chip.js b/gr-task-plugin/gr-task-chip.js
index 81489f5..8c5a399 100644
--- a/gr-task-plugin/gr-task-chip.js
+++ b/gr-task-plugin/gr-task-chip.js
@@ -16,6 +16,7 @@
  */
 
 import './gr-task-plugin.js';
+import {GrTaskPlugin} from './gr-task-plugin.js';
 import {htmlTemplate} from './gr-task-chip_html.js';
 
 export class GrTaskChip extends Polymer.Element {
@@ -34,6 +35,10 @@
         notify: true,
         value: 'ready',
       },
+      text: {
+        type: String,
+        notify: true,
+      },
     };
   }
 
@@ -62,6 +67,10 @@
   _onChipClick() {
     this._setTasksTabActive();
   }
+
+  _computeIconId() {
+    return GrTaskPlugin._computeIcon(this.chip_style).id;
+  }
 }
 
 customElements.define(GrTaskChip.is, GrTaskChip);
\ No newline at end of file
diff --git a/gr-task-plugin/gr-task-chip_html.js b/gr-task-plugin/gr-task-chip_html.js
index 27cee0f..f1d3ae0 100644
--- a/gr-task-plugin/gr-task-chip_html.js
+++ b/gr-task-plugin/gr-task-chip_html.js
@@ -46,21 +46,27 @@
     .taskSummaryChip.loading:focus-within {
       background: var(--gray-background-focus);
     }
-    .taskSummaryChip.success {
+    .taskSummaryChip.pass {
       border-color: var(--success-foreground);
       background: var(--success-background);
     }
-    .taskSummaryChip.success:hover {
+    .taskSummaryChip.pass gr-icon {
+      color: var(--success-foreground);
+    }
+    .taskSummaryChip.pass:hover {
       background: var(--success-background-hover);
       box-shadow: var(--elevation-level-1);
     }
-    .taskSummaryChip.success:focus-within {
+    .taskSummaryChip.pass:focus-within {
       background: var(--success-background-focus);
     }
     .taskSummaryChip.waiting {
       border-color: var(--warning-foreground);
       background: var(--warning-background);
     }
+    .taskSummaryChip.waiting gr-icon {
+      color: var(--warning-foreground);
+    }
     .taskSummaryChip.waiting:hover {
       background: var(--warning-background-hover);
       box-shadow: var(--elevation-level-1);
@@ -72,6 +78,9 @@
       border-color: var(--success-foreground);
       background: var(--success-background);
     }
+    .taskSummaryChip.ready gr-icon {
+      color: var(--success-foreground);
+    }
     .taskSummaryChip.ready:hover {
       background: var(--success-background-hover);
       box-shadow: var(--elevation-level-1);
@@ -84,6 +93,9 @@
       border-color: var(--error-foreground);
       background: var(--error-background);
     }
+    .taskSummaryChip.invalid gr-icon {
+      color: var(--error-foreground);
+    }
     .taskSummaryChip.invalid:hover {
       background: var(--error-background-hover);
       box-shadow: var(--elevation-level-1);
@@ -96,6 +108,9 @@
       border-color: var(--success-foreground);
       background: var(--success-background);
     }
+    .taskSummaryChip.duplicate gr-icon {
+      color: var(--success-foreground);
+    }
     .taskSummaryChip.duplicate:hover {
       background: var(--success-background-hover);
       box-shadow: var(--elevation-level-1);
@@ -108,6 +123,9 @@
       border-color: var(--error-foreground);
       background: var(--error-background);
     }
+    .taskSummaryChip.fail gr-icon {
+      color: var(--error-foreground);
+    }
     .taskSummaryChip.fail:hover {
       background: var(--error-background-hover);
       box-shadow: var(--elevation-level-1);
@@ -120,10 +138,20 @@
       font-weight: var(--font-weight-normal);
       line-height: var(--line-height-small);
     }
+    div {
+      display: flex;
+      justify-content: space-between;
+    }
+    gr-icon {
+      font-size: var(--line-height-small);
+    }
   </style>
   <button
     class$="taskSummaryChip font-small [[chip_style]]"
     on-click="_onChipClick">
-    <slot></slot>
+    <div tabindex="0">
+      <gr-icon icon="[[_computeIconId()]]" filled></gr-icon>
+      <div class="text">[[text]]</div>
+    </div>
   </button>
 `;
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js
index 97eb5de..29eb96b 100644
--- a/gr-task-plugin/gr-task-plugin.js
+++ b/gr-task-plugin/gr-task-plugin.js
@@ -31,7 +31,7 @@
  */
 Defs.Task;
 
-class GrTaskPlugin extends Polymer.Element {
+export class GrTaskPlugin extends Polymer.Element {
   static get is() {
     return 'gr-task-plugin';
   }
@@ -131,9 +131,9 @@
     }
   }
 
-  _computeIcon(task) {
+  static _computeIcon(taskStatus) {
     const icon = {};
-    switch (task.status) {
+    switch (taskStatus.toUpperCase()) {
       case 'FAIL':
         icon.id = 'close';
         icon.tooltip = 'Failed';
@@ -193,7 +193,7 @@
   _addTasks(tasks) { // rename to process, remove DOM bits
     if (!tasks) return [];
     tasks.forEach(task => {
-      task.icon = this._computeIcon(task);
+      task.icon = GrTaskPlugin._computeIcon(task.status.toString());
       task.showOnFilter = this._computeShowOnNeededAndBlockedFilter(task);
       this._compute_counts(task);
       this._addTasks(task.sub_tasks);
@@ -210,18 +210,6 @@
     this._show_all = 'false';
     this._expand_all = 'true';
   }
-
-  _switch_expand() {
-    this._expand_all = !this._expand_all;
-  }
-
-  _computeShowAllLabelText(showAllSections) {
-    if (showAllSections) {
-      return 'Hide all';
-    } else {
-      return 'Show all';
-    }
-  }
 }
 
 customElements.define(GrTaskPlugin.is, GrTaskPlugin);
diff --git a/gr-task-plugin/gr-task-plugin_html.js b/gr-task-plugin/gr-task-plugin_html.js
index 015fb45..4e1a9b5 100644
--- a/gr-task-plugin/gr-task-plugin_html.js
+++ b/gr-task-plugin/gr-task-plugin_html.js
@@ -64,11 +64,6 @@
       cursor: pointer;
       text-decoration: underline;
     }
-    .show-all-button gr-icon {
-      color: inherit;
-      --gr-icon-height: 18px;
-      --gr-icon-width: 18px;
-    }
     .no-margins { margin: 0 0 0 0; }
     .task-list-item {
       display: flex;
@@ -96,11 +91,6 @@
             on-click="_show_all_tap">All ([[_all_count]])</span>
         &nbsp;| Needed + Blocked ([[_ready_count]], [[_fail_count]])</p>
     </template>
-    <gr-button link="" class="show-all-button" on-click="_switch_expand">
-      [[_computeShowAllLabelText(_expand_all)]]
-      <gr-icon icon="expand_more" hidden$="[[_expand_all]]"></gr-icon>
-      <gr-icon icon="expand_less" hidden$="[[!_expand_all]]"></gr-icon>
-    </gr-button>
   </div>
   <div hidden$="[[!_expand_all]]" style="padding-bottom: 12px">
     <ul style="list-style-type:none;">
diff --git a/gr-task-plugin/gr-task-summary_html.js b/gr-task-plugin/gr-task-summary_html.js
index df59590..bb6df40 100644
--- a/gr-task-plugin/gr-task-summary_html.js
+++ b/gr-task-plugin/gr-task-summary_html.js
@@ -72,13 +72,13 @@
         <template is="dom-if" if="[[_can_show_summary(is_loading, ready_count, fail_count, invalid_count, waiting_count, duplicate_count, pass_count)]]">
           <td class="key">Tasks</td>
           <td class="value">
-            <gr-task-chip chip_style="loading" hidden$="[[!is_loading]]">loading...</gr-task-chip>
-            <gr-task-chip chip_style="fail" hidden$="[[!fail_count]]">[[fail_count]] blocked</gr-task-chip>
-            <gr-task-chip chip_style="invalid" hidden$="[[!invalid_count]]">[[invalid_count]] invalid</gr-task-chip>
-            <gr-task-chip chip_style="waiting" hidden$="[[!waiting_count]]">[[waiting_count]] waiting</gr-task-chip>
-            <gr-task-chip chip_style="ready" hidden$="[[!ready_count]]">[[ready_count]] needed</gr-task-chip>
-            <gr-task-chip chip_style="success" hidden$="[[!pass_count]]">[[pass_count]] passed</gr-task-chip>
-            <gr-task-chip chip_style="duplicate" hidden$="[[!duplicate_count]]">[[duplicate_count]] duplicate</gr-task-chip>
+            <gr-task-chip chip_style="loading" text="loading..." hidden$="[[!is_loading]]"></gr-task-chip>
+            <gr-task-chip chip_style="fail" text="[[fail_count]] blocked" hidden$="[[!fail_count]]"></gr-task-chip>
+            <gr-task-chip chip_style="invalid" text="[[invalid_count]] invalid" hidden$="[[!invalid_count]]"></gr-task-chip>
+            <gr-task-chip chip_style="waiting" text="[[waiting_count]] waiting" hidden$="[[!waiting_count]]"></gr-task-chip>
+            <gr-task-chip chip_style="ready" text="[[ready_count]] needed" hidden$="[[!ready_count]]"></gr-task-chip>
+            <gr-task-chip chip_style="pass" text="[[pass_count]] passed" hidden$="[[!pass_count]]"></gr-task-chip>
+            <gr-task-chip chip_style="duplicate" text="[[duplicate_count]] duplicate" hidden$="[[!duplicate_count]]"></gr-task-chip>
           </td>
         </template>
       </tr>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginDefinedInfoFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginDefinedInfoFactory.java
index 2e3dfa8..7cb083f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginDefinedInfoFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPluginDefinedInfoFactory.java
@@ -175,17 +175,24 @@
     public Node node;
     protected Task task;
     protected TaskAttribute attribute;
+    protected boolean isDuplicate;
 
     protected AttributeFactory(Node node) {
       this.node = node;
       this.task = node.task;
       attribute = new TaskAttribute(task.name());
+
+      isDuplicate =
+          node.isDuplicate
+              || (node.isChange()
+                  && node.getNodeSetByBaseTasksFactory().get(task.subSection).contains(node.key()));
+
       if (options.includeStatistics) {
         statistics.numberOfNodes++;
         if (node.isChange()) {
           statistics.numberOfChangeNodes++;
         }
-        if (node.isDuplicate) {
+        if (isDuplicate) {
           statistics.numberOfDuplicates++;
         }
         attribute.statistics = new TaskAttribute.Statistics();
@@ -215,8 +222,8 @@
           if (node.isChange()) {
             attribute.change = node.getChangeData().getId().get();
           }
-          attribute.hasPass = !node.isDuplicate && (task.pass != null || task.fail != null);
-          if (!node.isDuplicate) {
+          attribute.hasPass = !(isDuplicate || isAllNull(task.pass, task.fail));
+          if (!isDuplicate) {
             attribute.subTasks = getSubTasks();
           }
           attribute.status = getStatus();
@@ -239,7 +246,7 @@
               if (!options.onlyApplicable) {
                 attribute.applicable = applicable;
               }
-              if (!node.isDuplicate) {
+              if (!isDuplicate) {
                 if (task.inProgress != null) {
                   attribute.inProgress = node.matchOrNull(task.inProgress);
                 }
@@ -251,6 +258,9 @@
                 attribute.evaluationMilliSeconds = millis() - attribute.evaluationMilliSeconds;
               }
               addStatistics(attribute.statistics);
+              if (node.isChange()) {
+                node.getNodeSetByBaseTasksFactory().get(task.subSection).add(node.key());
+              }
               return Optional.of(attribute);
             }
           }
@@ -284,7 +294,7 @@
     }
 
     protected Status getStatusWithExceptions() throws StorageException, QueryParseException {
-      if (node.isDuplicate) {
+      if (isDuplicate) {
         return Status.DUPLICATE;
       }
       if (isAllNull(task.pass, task.fail, attribute.subTasks)) {
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 bca5ccc..4785885 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -89,6 +89,32 @@
     public transient int summaryCount;
   }
 
+  public class NodeSet {
+    protected ChangeData changeData;
+    protected Set<String> nodeKeys = new HashSet<>();
+
+    public NodeSet() {
+      changeData = TaskTree.this.changeData;
+    }
+
+    public boolean contains(String nodeKey) {
+      clearIfNeeded();
+      return nodeKeys.contains(nodeKey);
+    }
+
+    public boolean add(String nodeKey) {
+      clearIfNeeded();
+      return nodeKeys.add(nodeKey);
+    }
+
+    protected void clearIfNeeded() {
+      if (changeData != TaskTree.this.changeData) {
+        nodeKeys.clear();
+        changeData = TaskTree.this.changeData;
+      }
+    }
+  }
+
   protected static final String TASK_DIR = "task";
 
   protected final AccountResolver accountResolver;
@@ -105,6 +131,7 @@
   protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
   protected final StatisticsMap<String, List<ChangeData>> changesByNamesFactoryQuery =
       new HitHashMap<>();
+  protected final Map<SubSectionKey, NodeSet> nodeSetByBaseTasksFactory = new HashMap<>();
   protected final StatisticsMap<SubSectionKey, List<Task>> definitionsBySubSection =
       new HitHashMapOfCollection<>();
   protected final StatisticsMap<SubSectionKey, Map<BranchNameKey, List<Task>>>
@@ -144,6 +171,7 @@
     this.changeData = changeData;
     root.path = Collections.emptyList();
     root.duplicateKeys = Collections.emptyList();
+    nodeSetByBaseTasksFactory.clear();
     return root.getSubNodes();
   }
 
@@ -171,6 +199,10 @@
       return TaskTree.this.changeData;
     }
 
+    protected Map<SubSectionKey, NodeSet> getNodeSetByBaseTasksFactory() {
+      return TaskTree.this.nodeSetByBaseTasksFactory;
+    }
+
     protected boolean isTrusted() {
       return true;
     }
@@ -190,7 +222,8 @@
         return createFromPreloaded(def, (parent, definition) -> new Node(parent, definition));
       }
 
-      public Node createFromPreloaded(Task def, ChangeData changeData) {
+      public Node createFromPreloaded(
+          Task def, ChangeData changeData, Map<SubSectionKey, NodeSet> nodeSetByBaseTasksFactory) {
         return createFromPreloaded(
             def,
             (parent, definition) ->
@@ -204,6 +237,11 @@
                   public boolean isChange() {
                     return true;
                   }
+
+                  @Override
+                  protected Map<SubSectionKey, NodeSet> getNodeSetByBaseTasksFactory() {
+                    return nodeSetByBaseTasksFactory;
+                  }
                 });
       }
 
@@ -338,6 +376,20 @@
     }
 
     @Override
+    protected Map<SubSectionKey, NodeSet> getNodeSetByBaseTasksFactory() {
+      return parent.getNodeSetByBaseTasksFactory();
+    }
+
+    protected Map<SubSectionKey, NodeSet> getNodeSetByBaseTasksFactory(SubSectionKey subSection) {
+      Map<SubSectionKey, NodeSet> nodeSetByBaseTasksFactory = getNodeSetByBaseTasksFactory();
+      if (!nodeSetByBaseTasksFactory.containsKey(subSection)) {
+        nodeSetByBaseTasksFactory = new HashMap<>(nodeSetByBaseTasksFactory);
+        nodeSetByBaseTasksFactory.put(subSection, new NodeSet());
+      }
+      return nodeSetByBaseTasksFactory;
+    }
+
+    @Override
     protected boolean isTrusted() {
       return parent.isTrusted() && !task.isMasqueraded;
     }
@@ -473,11 +525,14 @@
           throws IOException {
         try {
           if (namesFactory.changes != null) {
+            Map<SubSectionKey, NodeSet> nodeSetByBaseTasksFactory =
+                getNodeSetByBaseTasksFactory(tasksFactory.subSection);
             for (ChangeData changeData : query(namesFactory.changes, task.isVisible)) {
               addPreloaded(
                   preloader.preload(
                       task.config.new Task(tasksFactory, changeData.getId().toString())),
-                  changeData);
+                  changeData,
+                  nodeSetByBaseTasksFactory);
             }
             return;
           }
@@ -497,7 +552,7 @@
             PluginProvidedTaskNamesFactory providedTaskNamesFactory =
                 PluginProvidedTaskNamesFactory.getProxyInstance(
                     dynamicBeans, namesFactory.plugin, namesFactory.provider);
-            names = providedTaskNamesFactory.getNames(getChangeData(), namesFactory.args);
+            names = providedTaskNamesFactory.getNames(namesFactory.args);
           } catch (Exception e) {
             log.atSevere().withCause(e).log("Failed to get plugin provided task names");
             addInvalidNode();
@@ -519,8 +574,9 @@
         nodes.addAll(factory.createFromPreloaded(defs));
       }
 
-      public void addPreloaded(Task def, ChangeData changeData) {
-        nodes.add(factory.createFromPreloaded(def, changeData));
+      public void addPreloaded(
+          Task def, ChangeData changeData, Map<SubSectionKey, NodeSet> nodeSetByBaseTasksFactory) {
+        nodes.add(factory.createFromPreloaded(def, changeData, nodeSetByBaseTasksFactory));
       }
 
       public void addPreloaded(Task def) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java
index 949db10..0907978 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/extensions/PluginProvidedTaskNamesFactory.java
@@ -17,7 +17,6 @@
 import com.google.common.reflect.Reflection;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
-import com.google.gerrit.server.query.change.ChangeData;
 import java.lang.reflect.Method;
 import java.util.List;
 
@@ -44,5 +43,5 @@
                 .invoke(bean, args));
   }
 
-  List<String> getNames(ChangeData change, List<String> args) throws Exception;
+  List<String> getNames(List<String> args) throws Exception;
 }
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 4cf8c71..931b3f9 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -57,10 +57,17 @@
 A task with a `READY` status is ready to be executed. All of its subtasks are
 in the `PASS` state.
 
-A task with a `DUPLICATE` status has the same task key as one of its ancestors.
+A task can have `DUPLICATE` status in one of the following scenarios:
+
+- It has the same task key as one of its ancestors
+- If the task is generated using a task-factory with change type names-factory,
+  and it has the same task key as another change task descendant of the same
+  ancestor task-factory
+
 Task keys are generally made up of the canonical task name and the change to
-which it applies. To avoid infinite loops, subtasks are ignored on duplicate
-tasks.
+which it applies. To avoid infinite loops, and to potentially reduce needless
+duplication, subtasks are ignored on duplicate tasks.
+Also see [duplicate-key](#duplicate-key).
 
 A task with a `PASS` status meets all the criteria for `READY`, and has
 executed and was successful.
@@ -238,6 +245,8 @@
     subtasks-file = common.config  # references the file named task/common.config
 ```
 
+<a id="duplicate-key"></a>
+
 `duplicate-key`
 
 : This key defines an identifier to help identify tasks which should be
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index 54ef4fb..aea074e 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -784,15 +784,43 @@
    ]
 }
 
-[root "Root tasks-factory PLUGIN"]
+[root "Root tasks-factory PLUGIN no args"]
   applicable = status:new
-  subtasks-factory = tasks-factory plugin
+  subtasks-factory = tasks-factory plugin no args
 
-[tasks-factory "tasks-factory plugin"]
-  names-factory = names-factory plugin list
+[tasks-factory "tasks-factory plugin no args"]
+  names-factory = names-factory plugin no args names
   fail = True
 
-[names-factory "names-factory plugin list"]
+[names-factory "names-factory plugin no args names"]
+  type = plugin
+  plugin = names-factory-provider
+  provider = foobar_provider
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root tasks-factory PLUGIN no args",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "foobar",
+         "status" : "FAIL"
+      }
+   ]
+}
+
+[root "Root tasks-factory PLUGIN no properties"]
+  applicable = status:new
+  subtasks-factory = tasks-factory plugin no properties
+
+[tasks-factory "tasks-factory plugin no properties"]
+  names-factory = names-factory plugin no properties names
+  fail = True
+
+[names-factory "names-factory plugin no properties names"]
   type = plugin
   plugin = names-factory-provider
   provider = foobar_provider
@@ -802,19 +830,93 @@
 {
    "applicable" : true,
    "hasPass" : false,
-   "name" : "Root tasks-factory PLUGIN",
+   "name" : "Root tasks-factory PLUGIN no properties",
    "status" : "WAITING",
    "subTasks" : [
       {
          "applicable" : true,
          "hasPass" : true,
-         "name" : "foobar-test-baz",
+         "name" : "foobar-baz",
          "status" : "FAIL"
       },
       {
          "applicable" : true,
          "hasPass" : true,
-         "name" : "foobar-test-qux",
+         "name" : "foobar-qux",
+         "status" : "FAIL"
+      }
+   ]
+}
+
+[root "Root tasks-factory PLUGIN non-change properties"]
+  applicable = status:new
+  set-first-non-change-property = non-change-value
+  set-second-non-change-property = another-${first-non-change-property}
+  subtasks-factory = tasks-factory plugin non-change properties
+
+[tasks-factory "tasks-factory plugin non-change properties"]
+  names-factory = names-factory plugin non-change properties names
+  fail = True
+
+[names-factory "names-factory plugin non-change properties names"]
+  type = plugin
+  plugin = names-factory-provider
+  provider = foobar_provider
+  arg = ${first-non-change-property}-baz
+  arg = ${second-non-change-property}-qux
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root tasks-factory PLUGIN non-change properties",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "foobar-non-change-value-baz",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "foobar-another-non-change-value-qux",
+         "status" : "FAIL"
+      }
+   ]
+}
+
+[root "Root tasks-factory PLUGIN change properties"]
+  applicable = status:new
+  subtasks-factory = tasks-factory plugin change properties
+
+[tasks-factory "tasks-factory plugin change properties"]
+  names-factory = names-factory plugin change properties names
+  fail = True
+
+[names-factory "names-factory plugin change properties names"]
+  type = plugin
+  plugin = names-factory-provider
+  provider = foobar_provider
+  arg = ${_change_number}-baz
+  arg = ${_change_branch}-qux
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root tasks-factory PLUGIN change properties",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "foobar-_change_number-baz",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "foobar-_change_branch-qux",
          "status" : "FAIL"
       }
    ]
@@ -1422,6 +1524,7 @@
   type = plugin
   plugin = names-factory-provider
   provider = foobar_provider
+  arg = ${_change_number}
 
 {
    "applicable" : true,
@@ -1432,8 +1535,8 @@
       {
          "applicable" : true,
          "hasPass" : true,
-         "hint" : "Welcome to the party Name(foobar-test) 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" : "foobar-test",
+         "hint" : "Welcome to the party Name(foobar-_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" : "foobar-_change_number",
          "status" : "FAIL"
       }
    ]
@@ -2181,60 +2284,10 @@
             {
                "applicable" : true,
                "change" : _change2_number,
-               "hasPass" : true,
+               "hasPass" : false,
+               "hint" : "Duplicate task is non blocking and empty to break the loop",
                "name" : "_change2_number",
-               "status" : "FAIL",
-               "subTasks" : [
-                  {
-                     "applicable" : true,
-                     "hasPass" : false,
-                     "name" : "task (tasks-factory changes loop)",
-                     "status" : "WAITING",
-                     "subTasks" : [
-                        {
-                           "applicable" : true,
-                           "change" : _change1_number,
-                           "hasPass" : true,
-                           "name" : "_change1_number",
-                           "status" : "FAIL",
-                           "subTasks" : [
-                              {
-                                 "applicable" : true,
-                                 "hasPass" : false,
-                                 "name" : "task (tasks-factory changes loop)",
-                                 "status" : "PASS",
-                                 "subTasks" : [
-                                    {
-                                       "applicable" : true,
-                                       "change" : _change1_number,
-                                       "hasPass" : false,
-                                       "hint" : "Duplicate task is non blocking and empty to break the loop",
-                                       "name" : "_change1_number",
-                                       "status" : "DUPLICATE"
-                                    },
-                                    {
-                                       "applicable" : true,
-                                       "change" : _change2_number,
-                                       "hasPass" : false,
-                                       "hint" : "Duplicate task is non blocking and empty to break the loop",
-                                       "name" : "_change2_number",
-                                       "status" : "DUPLICATE"
-                                    }
-                                 ]
-                              }
-                           ]
-                        },
-                        {
-                           "applicable" : true,
-                           "change" : _change2_number,
-                           "hasPass" : false,
-                           "hint" : "Duplicate task is non blocking and empty to break the loop",
-                           "name" : "_change2_number",
-                           "status" : "DUPLICATE"
-                        }
-                     ]
-                  }
-               ]
+               "status" : "DUPLICATE"
             }
          ]
       }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java
index 0ea2535..a05ca8c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/names_factory_provider/FoobarProvider.java
@@ -14,7 +14,6 @@
 
 package com.googlesource.gerrit.plugins.names_factory_provider;
 
-import com.google.gerrit.server.query.change.ChangeData;
 import com.googlesource.gerrit.plugins.task.extensions.PluginProvidedTaskNamesFactory;
 import java.util.ArrayList;
 import java.util.List;
@@ -24,8 +23,8 @@
   public static final String DELIMITER = "-";
 
   @Override
-  public List<String> getNames(ChangeData changeData, List<String> args) throws Exception {
-    String name = String.join(DELIMITER, "foobar", changeData.project().get());
+  public List<String> getNames(List<String> args) throws Exception {
+    String name = String.join(DELIMITER, "foobar");
     if (args == null || args.isEmpty()) {
       return List.of(name);
     }