Fix subtasks-external lookup to not fail when preloaded from another file

Before this change, plugin failed to lookup a subtasks-external when
a task with this subtasks-external was preloaded using a task
reference [1]. While preloading a task, all the subtasks are copied
to the current task. Since the subtasks-external property was a String,
it does not maintain the information regarding the location of the
subtasks-external. Thus, fix this issue by using ConfigSourcedValue as
the type for subtasks-external property, which helps in tracking the
location of the subtasks-external. 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-external = user special tasks

[external "user special tasks"]
  user = testuser
  file = special.config
```

Change-Id: I1aff32ae5885c3969ef8fdce7c019d3b448109f5
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 aa25e7f..55d2f48 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -65,7 +65,7 @@
     public Map<String, String> properties;
     public String readyHint;
     public List<ConfigSourcedValue> subTasks;
-    public List<String> subTasksExternals;
+    public List<ConfigSourcedValue> subTasksExternals;
     public List<ConfigSourcedValue> subTasksFactories;
     public List<String> subTasksFiles;
 
@@ -90,7 +90,10 @@
           getStringList(s, KEY_SUBTASK).stream()
               .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
               .collect(Collectors.toList());
-      subTasksExternals = getStringList(s, KEY_SUBTASKS_EXTERNAL);
+      subTasksExternals =
+          getStringList(s, KEY_SUBTASKS_EXTERNAL).stream()
+              .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
+              .collect(Collectors.toList());
       subTasksFactories =
           getStringList(s, KEY_SUBTASKS_FACTORY).stream()
               .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
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 01afb89..b34dccd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -397,9 +397,12 @@
       }
 
       protected void addSubTasksExternals() throws StorageException {
-        for (String external : task.subTasksExternals) {
+        for (ConfigSourcedValue configSourcedValue : task.subTasksExternals) {
           try {
-            External ext = task.config.getExternal(external);
+            External ext =
+                taskConfigCache
+                    .getTaskConfig(configSourcedValue.sourceFile())
+                    .getExternal(configSourcedValue.value());
             if (ext == null) {
               addInvalidNode();
             } else {
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index 4b296b2..273e21d 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -2553,6 +2553,68 @@
    ]
 }
 
+[root "Root Preload from all-projects sub-dir which has subtasks-external in same file"]
+  preload-task = //dir/common.config^Sample relative task in sub dir with subtasks-external from same file
+
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preload from all-projects sub-dir which has subtasks-external in same file",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "userfile task/special.config PASS",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "userfile task/special.config FAIL",
+         "status" : "FAIL"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "file task/common.config Preload PASS",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Referencing single task from same user ref",
+         "status" : "PASS",
+         "subTasks" : [
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "Relative Task",
+                 "status" : "PASS"
+             },
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "Relative Task in sub dir",
+                 "status" : "PASS"
+             },
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "task in user root config file",
+                 "status" : "PASS"
+             },
+             {
+                 "applicable" : true,
+                 "hasPass" : true,
+                 "name" : "Absolute Task",
+                 "status" : "PASS"
+             }
+         ]
+      }
+   ]
+}
+
 [root "Root Preload from user ref"]
   preload-task = @testuser/dir/relative.config^Relative Task
 
@@ -3299,6 +3361,21 @@
     type = static
     name = my d task
 
+[task "Sample relative task in sub dir with subtasks-external from same file"]
+    applicable = is:open
+    pass = is:open
+    set-my-external-prop = user sample config
+    subtasks-external = user special tasks
+    subtasks-external = ${my-external-prop}
+
+[external "user special tasks"]
+  user = testuser
+  file = special.config
+
+[external "user sample config"]
+  user = testuser
+  file = dir/sample.config
+
 [task "Sample relative task in sub dir with subtask from different file"]
     applicable = is:open
     pass = is:open