Fix subtask-factory lookup to not fail when preloaded from another file

Before this change, plugin failed to lookup a subtasks-factory when
a task with this subtasks-factory was preloaded using a task
reference [1]. While preloading a task, all the subtasks are copied
to the current task. Since the subtasks-factory property was a String,
it does not maintain the information regarding the location of the
subtasks-factory. Thus, fix this issue by using ConfigSourcedValue as
the type for subtasks-factory property, which helps in tracking the
location of the subtasks-factory. Add tests for the same.

[1]
file: `All-Projects:refs/meta/config:task.config`
```
[root "root task"]
    applicable = status:open
    preload-task = //common.config^simple task with subtasks
```

file: `All-Projects:refs/meta/config:task/common.config`
```
[task "simple task with subtasks"]
  applicable = is:open
  subtasks-factory = simple static tasks-factory

[tasks-factory "simple static tasks-factory"]
    names-factory = names-factory static list
    pass = True

[names-factory "names-factory static list"]
    type = static
    name = my a task
```

Change-Id: Ie173ad1e29c511bb028a49d4c9b54108461bbe67
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 2f1c358..ca1ae2c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -255,7 +255,7 @@
             }
           }
         }
-      } catch (IOException | RuntimeException e) {
+      } catch (IOException | RuntimeException | ConfigInvalidException e) {
         return Optional.of(invalid()); // bad applicability query
       }
       return Optional.empty();
@@ -356,7 +356,8 @@
       }
     }
 
-    protected List<TaskAttribute> getSubTasks() throws IOException, StorageException {
+    protected List<TaskAttribute> getSubTasks()
+        throws IOException, StorageException, ConfigInvalidException {
       List<TaskAttribute> subTasks = new ArrayList<>();
       for (Node subNode :
           options.onlyApplicable ? node.getApplicableSubNodes() : node.getSubNodes()) {
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 138bcd6..aa25e7f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -66,7 +66,7 @@
     public String readyHint;
     public List<ConfigSourcedValue> subTasks;
     public List<String> subTasksExternals;
-    public List<String> subTasksFactories;
+    public List<ConfigSourcedValue> subTasksFactories;
     public List<String> subTasksFiles;
 
     public boolean isVisible;
@@ -91,7 +91,10 @@
               .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
               .collect(Collectors.toList());
       subTasksExternals = getStringList(s, KEY_SUBTASKS_EXTERNAL);
-      subTasksFactories = getStringList(s, KEY_SUBTASKS_FACTORY);
+      subTasksFactories =
+          getStringList(s, KEY_SUBTASKS_FACTORY).stream()
+              .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
+              .collect(Collectors.toList());
       subTasksFiles = getStringList(s, KEY_SUBTASKS_FILE);
     }
 
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 5d4d773..01afb89 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -94,6 +94,7 @@
   protected final PredicateCache predicateCache;
   protected final MatchCache matchCache;
   protected final Preloader preloader;
+  protected final TaskConfigCache taskConfigCache;
   protected final TaskExpression.Factory taskExpressionFactory;
   protected final NodeList root = new NodeList();
   protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
@@ -127,6 +128,7 @@
     this.changeQueryBuilderProvider = changeQueryBuilderProvider;
     this.predicateCache = predicateCache;
     this.matchCache = new MatchCache(predicateCache);
+    this.taskConfigCache = taskConfigCache;
     this.taskExpressionFactory = taskExpressionFactory;
     this.preloader = preloaderFactory.create(taskConfigCache);
   }
@@ -263,7 +265,7 @@
       return String.valueOf(getChangeData().getId().get()) + TaskConfig.SEP + taskKey;
     }
 
-    public List<Node> getSubNodes() throws IOException, StorageException {
+    public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException {
       if (cachedNodes != null) {
         return refresh(cachedNodes);
       }
@@ -286,12 +288,14 @@
       return nodes;
     }
 
-    public List<Node> getApplicableSubNodes() throws IOException, StorageException {
+    public List<Node> getApplicableSubNodes()
+        throws IOException, StorageException, ConfigInvalidException {
       return hasUnfilterableSubNodes ? getSubNodes() : new ApplicableNodeFilter().getSubNodes();
     }
 
     @Override
-    protected List<Node> loadSubNodes() throws IOException, StorageException {
+    protected List<Node> loadSubNodes()
+        throws IOException, StorageException, ConfigInvalidException {
       List<Task> cachedDefinitions = definitionsBySubSection.get(task.key().subSection());
       if (cachedDefinitions != null) {
         return new SubNodeFactory().createFromPreloaded(cachedDefinitions);
@@ -357,7 +361,7 @@
       protected List<Node> nodes = new ArrayList<>();
       protected SubNodeFactory factory = new SubNodeFactory();
 
-      public List<Node> getSubNodes() throws IOException, StorageException {
+      public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException {
         addSubTasks();
         addSubTasksFactoryTasks();
         addSubTasksFiles();
@@ -407,11 +411,18 @@
         }
       }
 
-      protected void addSubTasksFactoryTasks() throws IOException, StorageException {
-        for (String tasksFactoryName : task.subTasksFactories) {
-          TasksFactory tasksFactory = task.config.getTasksFactory(tasksFactoryName);
+      protected void addSubTasksFactoryTasks()
+          throws IOException, StorageException, ConfigInvalidException {
+        for (ConfigSourcedValue configSourcedValue : task.subTasksFactories) {
+          TasksFactory tasksFactory =
+              taskConfigCache
+                  .getTaskConfig(configSourcedValue.sourceFile())
+                  .getTasksFactory(configSourcedValue.value());
           if (tasksFactory != null) {
-            NamesFactory namesFactory = task.config.getNamesFactory(tasksFactory.namesFactory);
+            NamesFactory namesFactory =
+                taskConfigCache
+                    .getTaskConfig(configSourcedValue.sourceFile())
+                    .getNamesFactory(tasksFactory.namesFactory);
             if (namesFactory != null && namesFactory.type != null) {
               namesFactory = properties.getNamesFactory(namesFactory);
               switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) {
@@ -493,7 +504,7 @@
 
       public ApplicableNodeFilter() throws StorageException {}
 
-      public List<Node> getSubNodes() throws IOException, StorageException {
+      public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException {
         if (nodesByBranch != null) {
           List<Node> nodes = nodesByBranch.get(branch);
           if (nodes != null) {
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index 880b955..4b296b2 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -2517,6 +2517,42 @@
    ]
 }
 
+[root "Root Preload from all-projects sub-dir which has subtasks-factory in same file"]
+  preload-task = //dir/common.config^Sample relative task in sub dir with subtasks-factory from same file
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from all-projects sub-dir which has subtasks-factory in same file",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my a task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my b task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my c task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "my d task",
+         "status" : "PASS"
+      }
+   ]
+}
+
 [root "Root Preload from user ref"]
   preload-task = @testuser/dir/relative.config^Relative Task
 
@@ -3238,6 +3274,31 @@
     pass = is:open
     subtask = Sample relative task in sub dir
 
+[task "Sample relative task in sub dir with subtasks-factory from same file"]
+    applicable = is:open
+    pass = is:open
+    set-my-factory-prop = simple static tasks-factory 2
+    subtasks-factory = simple static tasks-factory 1
+    subtasks-factory = ${my-factory-prop}
+
+[tasks-factory "simple static tasks-factory 1"]
+    names-factory = names-factory static list 1
+    pass = True
+
+[names-factory "names-factory static list 1"]
+    type = static
+    name = my a task
+    name = my b task
+    name = my c task
+
+[tasks-factory "simple static tasks-factory 2"]
+    names-factory = names-factory static list 2
+    pass = True
+
+[names-factory "names-factory static list 2"]
+    type = static
+    name = my d task
+
 [task "Sample relative task in sub dir with subtask from different file"]
     applicable = is:open
     pass = is:open