Merge branch 'stable-2.16'
Change-Id: Iea5317873e16cbe794c66e851e56afecdb496e6a
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 1eab7e4..6ef2ba6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -51,6 +51,9 @@
usage = "Include only invalid tasks and the tasks referencing them in the output")
public boolean onlyInvalid = false;
+ @Option(name = "--evaluation-time", usage = "Include elapsed evaluation time on each task")
+ boolean evaluationTime = false;
+
@Option(
name = "--preview",
metaVar = "{CHANGE,PATCHSET}",
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
new file mode 100644
index 0000000..e0b76c1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -0,0 +1,90 @@
+// 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.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import java.lang.IllegalAccessException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Use to pre-load a task definition with values from its preload-task definition. */
+public class Preloader {
+ public static void preload(Task definition) throws ConfigInvalidException {
+ String name = definition.preloadTask;
+ if (name != null) {
+ Task task = definition.config.getTaskOptional(name);
+ if (task != null) {
+ preload(task);
+ preloadFrom(definition, task);
+ }
+ }
+ }
+
+ 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")) {
+ 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
+ }
+ } catch (IllegalAccessException | IllegalArgumentException e) {
+ throw new RuntimeException();
+ }
+ }
+ }
+
+ protected static List preloadListFrom(List list, List preList) {
+ List extended = list;
+ if (!preList.isEmpty()) {
+ extended = preList;
+ if (!list.isEmpty()) {
+ extended = new ArrayList(list.size() + preList.size());
+ extended.addAll(preList);
+ extended.addAll(list);
+ }
+ }
+ return extended;
+ }
+
+ protected static Map preloadMapFrom(Map map, Map preMap) {
+ Map extended = map;
+ if (!preMap.isEmpty()) {
+ extended = preMap;
+ if (!map.isEmpty()) {
+ extended = new HashMap(map.size() + preMap.size());
+ extended.putAll(preMap);
+ extended.putAll(map);
+ }
+ }
+ return extended;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
new file mode 100644
index 0000000..8af38b5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
@@ -0,0 +1,132 @@
+// 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.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** 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;
+ unexpanded.putAll(definition.exported);
+ expandAllUnexpanded();
+ definition.properties = expanded;
+
+ if (definition.exported.isEmpty()) {
+ definition.exported = null;
+ } else {
+ for (String property : definition.exported.keySet()) {
+ definition.exported.put(property, expanded.get(property));
+ }
+ }
+
+ this.definition = definition;
+ expandNonPropertyFields();
+ }
+
+ 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);
+ }
+ } 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()));
+ }
+ }
+ }
+
+ protected String expandLiteral(String literal) {
+ if (literal == null) {
+ return null;
+ }
+ 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
+ }
+ 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 b55cd05..1a5c702 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -48,12 +48,14 @@
public static class TaskAttribute {
public Boolean applicable;
+ public Map<String, String> exported;
public Boolean hasPass;
public String hint;
public Boolean inProgress;
public String name;
public Status status;
public List<TaskAttribute> subTasks;
+ public Long evaluationMilliSeconds;
public TaskAttribute(String name) {
this.name = name;
@@ -109,45 +111,58 @@
return a;
}
- protected void addApplicableTasks(List<TaskAttribute> tasks, ChangeData c, Node node) {
+ 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();
+ }
+
boolean applicable = match(c, def.applicable);
if (!def.isVisible) {
if (!def.isTrusted || (!applicable && !options.onlyApplicable)) {
- tasks.add(unknown());
+ atts.add(unknown());
return;
}
}
if (applicable || !options.onlyApplicable) {
- TaskAttribute task = new TaskAttribute(def.name);
- task.hasPass = def.pass != null || def.fail != null;
- task.subTasks = getSubTasks(c, node);
- task.status = getStatus(c, def, task);
+ 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)) {
- task.status = Status.INVALID;
+ att.status = Status.INVALID;
}
- boolean groupApplicable = task.status != null;
+ boolean groupApplicable = att.status != null;
if (groupApplicable || !options.onlyApplicable) {
- if (!options.onlyInvalid || task.status == Status.INVALID || task.subTasks != null) {
+ if (!options.onlyInvalid || att.status == Status.INVALID || att.subTasks != null) {
if (!options.onlyApplicable) {
- task.applicable = applicable;
+ att.applicable = applicable;
}
if (def.inProgress != null) {
- task.inProgress = matchOrNull(c, def.inProgress);
+ att.inProgress = matchOrNull(c, def.inProgress);
}
- task.hint = getHint(task.status, def);
- tasks.add(task);
+ att.hint = getHint(att.status, def);
+ att.exported = def.exported;
+
+ if (options.evaluationTime) {
+ att.evaluationMilliSeconds = millis() - att.evaluationMilliSeconds;
+ }
+ atts.add(att);
}
}
}
} catch (QueryParseException e) {
- tasks.add(invalid()); // bad applicability query
+ atts.add(invalid()); // bad applicability query
}
}
+ protected long millis() {
+ return System.nanoTime() / 1000000;
+ }
+
protected List<TaskAttribute> getSubTasks(ChangeData c, Node node) {
List<TaskAttribute> subTasks = new ArrayList<>();
for (Node subNode : node.getSubNodes()) {
@@ -177,33 +192,33 @@
return a;
}
- protected boolean isValidQueries(ChangeData c, Task task) {
+ protected boolean isValidQueries(ChangeData c, Task def) {
try {
- match(c, task.inProgress);
- match(c, task.fail);
- match(c, task.pass);
+ match(c, def.inProgress);
+ match(c, def.fail);
+ match(c, def.pass);
return true;
} catch (StorageException | QueryParseException e) {
return false;
}
}
- protected Status getStatus(ChangeData c, Task task, TaskAttribute a) {
+ protected Status getStatus(ChangeData c, Task def, TaskAttribute a) {
try {
- return getStatusWithExceptions(c, task, a);
+ return getStatusWithExceptions(c, def, a);
} catch (QueryParseException e) {
return Status.INVALID;
}
}
- protected Status getStatusWithExceptions(ChangeData c, Task task, TaskAttribute a)
+ protected Status getStatusWithExceptions(ChangeData c, Task def, TaskAttribute a)
throws QueryParseException {
- if (isAllNull(task.pass, task.fail, a.subTasks)) {
- // A leaf task has no defined subtasks.
+ if (isAllNull(def.pass, def.fail, a.subTasks)) {
+ // A leaf def has no defined subdefs.
boolean hasDefinedSubtasks =
- !(task.subTasks.isEmpty()
- && task.subTasksFiles.isEmpty()
- && task.subTasksExternals.isEmpty());
+ !(def.subTasks.isEmpty()
+ && def.subTasksFiles.isEmpty()
+ && def.subTasksExternals.isEmpty());
if (hasDefinedSubtasks) {
// Remove 'Grouping" tasks (tasks with subtasks but no PASS
// or FAIL criteria) from the output if none of their subtasks
@@ -217,8 +232,8 @@
return Status.INVALID;
}
- if (task.fail != null) {
- if (match(c, task.fail)) {
+ 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.
@@ -230,7 +245,7 @@
// to make a task have a FAIL status.
return Status.FAIL;
}
- if (task.pass == null) {
+ 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;
@@ -249,7 +264,7 @@
return Status.WAITING;
}
- if (task.pass != null && !match(c, task.pass)) {
+ 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
@@ -262,18 +277,18 @@
return Status.PASS;
}
- protected String getHint(Status status, Task task) {
+ protected String getHint(Status status, Task def) {
if (status == Status.READY) {
- return task.readyHint;
+ return def.readyHint;
} else if (status == Status.FAIL) {
- return task.failHint;
+ return def.failHint;
}
return null;
}
- protected static boolean isAll(Iterable<TaskAttribute> tasks, Status state) {
- for (TaskAttribute task : tasks) {
- if (task.status != state) {
+ 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 64add65..5fcc8f1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -19,7 +19,14 @@
import com.google.gerrit.server.git.meta.AbstractVersionedMetaData;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
/** Task Configuration file living in git */
public class TaskConfig extends AbstractVersionedMetaData {
@@ -33,11 +40,14 @@
public class Task extends Section {
public String applicable;
+ public Map<String, String> exported;
public String fail;
public String failHint;
public String inProgress;
public String name;
public String pass;
+ public String preloadTask;
+ public Map<String, String> properties;
public String readyHint;
public List<String> subTasks;
public List<String> subTasksExternals;
@@ -50,11 +60,14 @@
this.isVisible = isVisible;
this.isTrusted = isTrusted;
applicable = getString(s, KEY_APPLICABLE, null);
+ exported = getProperties(s, KEY_EXPORT_PREFIX);
fail = getString(s, KEY_FAIL, null);
failHint = getString(s, KEY_FAIL_HINT, null);
inProgress = getString(s, KEY_IN_PROGRESS, null);
- name = getString(s, KEY_NAME, s.subSection);
+ name = s.subSection;
pass = getString(s, KEY_PASS, null);
+ preloadTask = getString(s, KEY_PRELOAD_TASK, null);
+ properties = getProperties(s, KEY_PROPERTIES_PREFIX);
readyHint = getString(s, KEY_READY_HINT, null);
subTasks = getStringList(s, KEY_SUBTASK);
subTasksExternals = getStringList(s, KEY_SUBTASKS_EXTERNAL);
@@ -74,16 +87,22 @@
}
}
+ protected static final Pattern OPTIONAL_TASK_PATTERN =
+ Pattern.compile("([^ |]*( *[^ |])*) *\\| *");
+
protected static final String SECTION_EXTERNAL = "external";
protected static final String SECTION_ROOT = "root";
protected static final String SECTION_TASK = "task";
protected static final String KEY_APPLICABLE = "applicable";
+ protected static final String KEY_EXPORT_PREFIX = "export-";
protected static final String KEY_FAIL = "fail";
protected static final String KEY_FAIL_HINT = "fail-hint";
protected static final String KEY_FILE = "file";
protected static final String KEY_IN_PROGRESS = "in-progress";
protected static final String KEY_NAME = "name";
protected static final String KEY_PASS = "pass";
+ protected static final String KEY_PRELOAD_TASK = "preload-task";
+ protected static final String KEY_PROPERTIES_PREFIX = "set-";
protected static final String KEY_READY_HINT = "ready-hint";
protected static final String KEY_SUBTASK = "subtask";
protected static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
@@ -125,8 +144,33 @@
return externals;
}
- public Task getTask(String name) {
- return new Task(new SubSection(SECTION_TASK, name), isVisible, isTrusted);
+ /* returs null only if optional and not found */
+ public Task getTaskOptional(String name) throws ConfigInvalidException {
+ int end = 0;
+ Matcher m = OPTIONAL_TASK_PATTERN.matcher(name);
+ while (m.find()) {
+ end = m.end();
+ Task task = getTaskOrNull(m.group(1));
+ if (task != null) {
+ return task;
+ }
+ }
+
+ String last = name.substring(end);
+ if (!"".equals(last)) { // Last entry was not optional
+ Task task = getTaskOrNull(last);
+ if (task != null) {
+ return task;
+ }
+ throw new ConfigInvalidException("task not defined");
+ }
+ return null;
+ }
+
+ /* returns null if not found */
+ protected Task getTaskOrNull(String name) {
+ SubSection subSection = new SubSection(SECTION_TASK, name);
+ return getNames(subSection).isEmpty() ? null : new Task(subSection, isVisible, isTrusted);
}
public External getExternal(String name) {
@@ -137,11 +181,47 @@
return new External(s);
}
+ protected Map<String, String> getProperties(SubSection s, String prefix) {
+ Map<String, String> valueByName = new HashMap<>();
+ for (Map.Entry<String, String> e :
+ getStringByName(s, getMatchingNames(s, prefix + ".+")).entrySet()) {
+ String name = e.getKey();
+ valueByName.put(name.substring(prefix.length()), e.getValue());
+ }
+ return valueByName;
+ }
+
+ protected Map<String, String> getStringByName(SubSection s, Iterable<String> names) {
+ Map<String, String> valueByName = new HashMap<>();
+ for (String name : names) {
+ valueByName.put(name, getString(s, name));
+ }
+ return valueByName;
+ }
+
+ protected Set<String> getMatchingNames(SubSection s, String match) {
+ Set<String> matched = new HashSet<>();
+ for (String name : getNames(s)) {
+ if (name.matches(match)) {
+ matched.add(name);
+ }
+ }
+ return matched;
+ }
+
+ protected Set<String> getNames(SubSection s) {
+ return cfg.getNames(s.section, s.subSection);
+ }
+
protected String getString(SubSection s, String key, String def) {
- String v = cfg.getString(s.section, s.subSection, key);
+ String v = getString(s, key);
return v != null ? v : def;
}
+ protected String getString(SubSection s, String key) {
+ return cfg.getString(s.section, s.subSection, key);
+ }
+
protected List<String> getStringList(SubSection s, String key) {
return Arrays.asList(cfg.getStringList(s.section, s.subSection, key));
}
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 8485f6c..9a0770e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -30,10 +30,20 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
+/**
+ * Add structure to access the task definitions from the config as a tree.
+ *
+ * <p>This class is a "middle" representation of the task tree. The task config is represented as a
+ * lazily loaded tree, and much of the tree validity is enforced at this layer.
+ */
public class TaskTree {
protected static final String TASK_DIR = "task";
@@ -65,13 +75,22 @@
}
protected class NodeList {
- protected LinkedList<Task> path = new LinkedList<>();
+ protected LinkedList<String> path = new LinkedList<>();
protected List<Node> nodes;
+ protected Set<String> names = new HashSet<>();
- protected void addSubDefinitions(List<Task> tasks) {
- for (Task task : tasks) {
- // path check detects looping definitions
- nodes.add(path.contains(task) ? null : new Node(task, path));
+ protected void addSubDefinitions(List<Task> defs, Map<String, String> parentProperties) {
+ 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);
}
}
}
@@ -80,12 +99,12 @@
public List<Node> getRootNodes() throws ConfigInvalidException, IOException {
if (nodes == null) {
nodes = new ArrayList<>();
- addSubDefinitions(getRootTasks());
+ addSubDefinitions(getRootDefinitions(), new HashMap<String, String>());
}
return nodes;
}
- protected List<Task> getRootTasks() throws ConfigInvalidException, IOException {
+ protected List<Task> getRootDefinitions() throws ConfigInvalidException, IOException {
return taskFactory.getRootConfig().getRootTasks();
}
}
@@ -93,10 +112,13 @@
public class Node extends NodeList {
public final Task definition;
- public Node(Task definition, List<Task> path) {
+ public Node(Task definition, List<String> path, Map<String, String> parentProperties)
+ throws ConfigInvalidException {
this.definition = definition;
this.path.addAll(path);
- this.path.add(definition);
+ this.path.add(definition.name);
+ Preloader.preload(definition);
+ new Properties(definition, parentProperties);
}
public List<Node> getSubNodes() {
@@ -108,11 +130,15 @@
}
protected void addSubDefinitions() {
- addSubDefinitions(getSubTasks());
+ addSubDefinitions(getSubDefinitions());
addSubFileDefinitions();
addExternalDefinitions();
}
+ protected void addSubDefinitions(List<Task> defs) {
+ addSubDefinitions(defs, definition.properties);
+ }
+
protected void addSubFileDefinitions() {
for (String file : definition.subTasksFiles) {
try {
@@ -130,7 +156,7 @@
if (ext == null) {
nodes.add(null);
} else {
- addSubDefinitions(getTasks(ext));
+ addSubDefinitions(getTaskDefinitions(ext));
}
} catch (ConfigInvalidException | IOException e) {
nodes.add(null);
@@ -138,15 +164,23 @@
}
}
- protected List<Task> getSubTasks() {
- List<Task> tasks = new ArrayList<>();
- for (String subTask : definition.subTasks) {
- tasks.add(definition.config.getTask(subTask));
+ 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 tasks;
+ return defs;
}
- protected List<Task> getTasks(External external) throws ConfigInvalidException, IOException {
+ protected List<Task> getTaskDefinitions(External external)
+ throws ConfigInvalidException, IOException {
return getTasks(resolveUserBranch(external.user), external.file);
}
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 35ede41..62297c5 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -146,17 +146,39 @@
fail-hint = Blocked by a negative review score
```
+`preload-task`
+
+: This key defines a task whose attributes will be preloaded into the current
+task before the current task's attributes are set. Most attributes defined
+in the preload-task will be loaded first, and will be overridden by attributes
+from the current task if they redefined in the current task. Attributes
+which are lists (such as subtasks) or maps (such as properties), will be
+preloaded by the preload-task and then extended with the attributes from the
+current task. See [Optional Tasks](#optional_tasks) for how to define optional
+preload-tasks.
+
+Example:
+```
+ preload-task = Base Jenkins Verification # has a pass criteria and hints
+```
+
`subtask`
: This key lists the name of a subtask of the current task. This key may be
used several times in a task section to define more than one subtask for a
-particular task.
+particular task. See [Optional Tasks](#optional_tasks) for how to define
+optional subtasks.
Example:
```
subtask = "Code Review"
subtask = "License Approval"
+ ...
+ [task "Code Review"]
+ ...
+ [task "License Approval"]
+ ...
```
`subtasks-external`
@@ -232,6 +254,27 @@
fail = label:code-review-2
```
+<a id="optional_tasks"/>
+Optional Tasks
+--------------
+To define a task that may not exist and that will not cause the task referencing
+it to be INVALID, follow the task name with pipe (`|`) character. This feature
+is particularly useful when a property is used in the task name.
+
+```
+ preload-task = Optional Subtask {$_name} |
+```
+
+To define an alternate task to load when an optional task does not exist,
+list the alterante task name after the pipe (`|`) character. This feature
+may be chained together as many times as needed.
+
+```
+ subtask = Optional Subtask {$_name} |
+ Backup Optional Subtask {$_name} Backup |
+ Default Subtask # Must exist if the above two don't!
+```
+
External Entries
----------------
A name for external task files on other projects and branches may be given
@@ -262,6 +305,60 @@
user = first-user # references the sharded user ref refs/users/01/1000001
```
+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.
+
+Example:
+```
+ fail-hint = {$_name} needs to be fixed
+```
+
+Custom properties may be defined on a task using the following syntax:
+```
+ set-<property-name> = <property-value>
+```
+
+Subtasks inherit all custom properties from their parents. A task is invalid
+if it attempts to override an already set property.
+
+Example:
+```
+ [task "foo-project"]
+ set-project-name = foo
+ subtask = common-to-many-projects
+
+ [task "common-to-many-projects"]
+ fail-hint = ${project-name} needs to be fixed
+ ...
+```
+
+It is possible to define a custom property value and to export that value
+to the json on the current task by using the following syntax:
+```
+ export-<property-name> = <property-value>
+```
+
+Example:
+```
+ [task "foo"]
+ export-ci-system = jenkins
+```
+
+```
+ "subTasks" : [
+ {
+ "exported" : {
+ "ci-system" : "jenkins"
+ },
+ ...
+ "name" : "foo",
+ ...
+ }
+ ]
+```
+
Change Query Output
-------------------
It is possible to add a task section to the query output of changes using
@@ -297,6 +394,12 @@
not output anything. This switch is particularly useful in combination
with the **\-\-@PLUGIN@\-\-preview** switch.
+**\-\-@PLUGIN@\-\-task\-\-evaluation-time**
+
+This switch is meant as a debug switch to evaluate task performance. This
+switch outputs an elapsed time value on every task indicating how much time
+it took to evaluate a task and its subtasks.
+
When tasks are appended to changes, they will have a "task" section under
the plugins section like below:
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
index 50df58f..721032f 100644
--- a/src/main/resources/Documentation/task_states.md
+++ b/src/main/resources/Documentation/task_states.md
@@ -8,14 +8,17 @@
```
[root "Root N/A"]
- applicable = is:closed
+ applicable = is:closed # Assumes test query is "is:open"
+
+[root "Root APPLICABLE"]
+ applicable = is:open # Assumes test query is "is:open"
+ pass = True
+ subtask = Subtask APPLICABLE
[root "Root PASS"]
- applicable = is:open
pass = True
[root "Root FAIL"]
- applicable = is:open
fail = True
[root "Root straight PASS"]
@@ -36,24 +39,21 @@
fail = is:open
[root "Root grouping PASS (subtask PASS)"]
- applicable = is:open
subtask = Subtask PASS
[root "Root grouping WAITING (subtask READY)"]
- applicable = is:open
subtask = Subtask READY
[root "Root grouping WAITING (subtask FAIL)"]
- applicable = is:open
subtask = Subtask FAIL
[root "Root grouping NA (subtask NA)"]
- applicable = is:open
+ applicable = is:open # Assumes Subtask NA has "applicable = NOT is:open"
subtask = Subtask NA
[root "Root READY (subtask PASS)"]
applicable = is:open
- pass = -is:open
+ pass = NOT is:open
subtask = Subtask PASS
ready-hint = You must now run the ready task
@@ -70,57 +70,69 @@
[root "Root IN PROGRESS"]
applicable = is:open
in-progress = is:open
- pass = -is:open
+ pass = NOT is:open
[root "Root NOT IN PROGRESS"]
applicable = is:open
- in-progress = -is:open
- pass = -is:open
+ in-progress = NOT is:open
+ pass = NOT is:open
+
+[root "Root Optional subtasks"]
+ subtask = OPTIONAL MISSING |
+ subtask = Subtask Optional |
[root "Subtasks File"]
- applicable = is:open
subtasks-file = common.config
[root "Subtasks File (Missing)"]
- applicable = is:open
subtasks-file = common.config
subtasks-file = missing
[root "Subtasks External"]
- applicable = is:open
subtasks-external = user special
[root "Subtasks External (Missing)"]
- applicable = is:open
subtasks-external = user special
subtasks-external = missing
[root "Subtasks External (User Missing)"]
- applicable = is:open
subtasks-external = user special
subtasks-external = user missing
[root "Subtasks External (File Missing)"]
- applicable = is:open
subtasks-external = user special
subtasks-external = file missing
+[root "Root Properties"]
+ set-root-property = root-value
+ export-root = ${_name}
+ fail = True
+ fail-hint = Name(${_name})
+ subtask = Subtask Properties
+
+[root "Root Preload"]
+ preload-task = Subtask FAIL
+ subtask = Subtask Preload
+
[root "INVALIDS"]
- applicable = is:open
subtasks-file = invalids.config
[root "Root NA Pass"]
- applicable = -is:open
+ applicable = NOT is:open # Assumes test query is "is:open"
pass = True
[root "Root NA Fail"]
- applicable = -is:open
+ applicable = NOT is:open # Assumes test query is "is:open"
fail = True
[root "NA INVALIDS"]
- applicable = -is:open
+ applicable = NOT is:open # Assumes test query is "is:open"
subtasks-file = invalids.config
+[task "Subtask APPLICABLE"]
+ applicable = is:open
+ pass = True
+
[task "Subtask FAIL"]
applicable = is:open
fail = is:open
@@ -128,15 +140,91 @@
[task "Subtask READY"]
applicable = is:open
- pass = -is:open
+ pass = NOT is:open
subtask = Subtask PASS
[task "Subtask PASS"]
applicable = is:open
pass = is:open
+[task "Subtask Optional"]
+ subtask = Subtask PASS |
+ subtask = OPTIONAL MISSING | Subtask FAIL
+ subtask = OPTIONAL MISSING | OPTIONAL MISSING |
+ subtask = OPTIONAL MISSING | OPTIONAL MISSING | Subtask READY
+
[task "Subtask NA"]
- applicable = NOT is:open
+ applicable = NOT is:open # Assumes test query is "is:open"
+
+[task "Subtask Properties"]
+ export-subtask = ${_name}
+ subtask = Subtask Properties Hints
+ subtask = Chained ${_name}
+ subtask = Subtask Properties Reset
+
+[task "Subtask Properties Hints"]
+ set-first-property = first-value
+ set-second-property = ${first-property} second-extra ${third-property}
+ set-third-property = third-value
+ fail = True
+ fail-hint = Name(${_name}) root-property(${root-property}) first-property(${first-property}) second-property(${second-property}) root(${root})
+
+[task "Chained Subtask Properties"]
+ pass = True
+
+[task "Subtask Properties Reset"]
+ pass = True
+ set-first-property = reset-first-value
+ fail-hint = first-property(${first-property})
+
+[task "Subtask Preload"]
+ preload-task = Subtask READY
+ subtask = Subtask Preload Preload
+ subtask = Subtask Preload Hints PASS
+ subtask = Subtask Preload Hints FAIL
+ subtask = Subtask Preload Override Pass
+ subtask = Subtask Preload Override Fail
+ subtask = Subtask Preload Extend Subtasks
+ subtask = Subtask Preload Optional
+ subtask = Subtask Preload Properties
+
+[task "Subtask Preload Preload"]
+ preload-task = Subtask Preload with Preload
+
+[task "Subtask Preload with Preload"]
+ preload-task = Subtask PASS
+
+[task "Subtask Preload Hints PASS"]
+ preload-task = Subtask Hints
+ pass = False
+
+[task "Subtask Preload Hints FAIL"]
+ preload-task = Subtask Hints
+ fail = True
+
+[task "Subtask Preload Override Pass"]
+ preload-task = Subtask PASS
+ pass = False
+
+[task "Subtask Preload Override Fail"]
+ preload-task = Subtask FAIL
+ fail = False
+
+[task "Subtask Preload Extend Subtasks"]
+ preload-task = Subtask READY
+ subtask = Subtask APPLICABLE
+
+[task "Subtask Preload Optional"]
+ preload-task = Missing | Subtask PASS
+
+[task "Subtask Preload Properties"]
+ preload-task = Subtask Properties Hints
+ set-fourth-property = fourth-value
+ fail-hint = second-property(${second-property}) fourth-property(${fourth-property})
+
+[task "Subtask Hints"] # meant to be preloaded, not a test case in itself
+ ready-hint = Task is ready
+ fail-hint = Task failed
[external "user special"]
user = testuser
@@ -161,51 +249,60 @@
[task "file task/common.config FAIL"]
applicable = is:open
fail = is:open
- pass = is:open
```
`task/invalids.config` file in project `All-Projects` on ref `refs/meta/config`.
```
[task "No PASS criteria"]
- applicable = is:open
+ fail-hint = Invalid without Pass criteria and without subtasks
[task "WAITING (subtask INVALID)"]
- applicable = is:open
pass = is:open
subtask = Subtask INVALID
+[task "WAITING (subtask duplicate)"]
+ subtask = Subtask INVALID
+ subtask = Subtask INVALID
+
[task "WAITING (subtask missing)"]
- applicable = is:open
pass = is:open
subtask = MISSING # security bug: subtask name appears in output
[task "Grouping WAITING (subtask INVALID)"]
- applicable = is:open
subtask = Subtask INVALID
[task "Grouping WAITING (subtask missing)"]
- applicable = is:open
subtask = MISSING # security bug: subtask name appears in output
[task "Subtask INVALID"]
- applicable = is:open
+ fail-hint = Use when an INVALID subtask is needed, not meant as a test case in itself
+
+[task "Subtask Optional"]
+ subtask = MISSING | MISSING
[task "NA Bad PASS query"]
- applicable = -is:open
+ applicable = NOT is:open # Assumes test query is "is:open"
fail = True
pass = has:bad
[task "NA Bad FAIL query"]
- applicable = -is:open
+ applicable = NOT is:open # Assumes test query is "is:open"
pass = True
fail = has:bad
[task "NA Bad INPROGRESS query"]
- applicable = -is:open
+ applicable = NOT is:open # Assumes test query is "is:open"
fail = True
in-progress = has:bad
+[task "Looping"]
+ subtask = Looping
+
+[task "Looping Properties"]
+ set-A = ${B}
+ set-B = ${A}
+ fail = True
```
`task/special.config` file in project `All-Users` on ref `refs/users/self`.
@@ -218,7 +315,6 @@
[task "userfile task/special.config FAIL"]
applicable = is:open
fail = is:open
- pass = is:open
```
The expected output for the above task config looks like:
@@ -234,6 +330,18 @@
"roots" : [
{
"hasPass" : true,
+ "name" : "Root APPLICABLE",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "hasPass" : true,
+ "name" : "Subtask APPLICABLE",
+ "status" : "PASS"
+ }
+ ]
+ },
+ {
+ "hasPass" : true,
"name" : "Root PASS",
"status" : "PASS"
},
@@ -363,6 +471,42 @@
},
{
"hasPass" : false,
+ "name" : "Root Optional subtasks",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ },
+ {
+ "hasPass" : true,
+ "name" : "Subtask FAIL",
+ "status" : "FAIL"
+ },
+ {
+ "hasPass" : true,
+ "name" : "Subtask READY",
+ "status" : "READY",
+ "subTasks" : [
+ {
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "hasPass" : false,
"name" : "Subtasks File",
"status" : "WAITING",
"subTasks" : [
@@ -472,6 +616,117 @@
]
},
{
+ "exported" : {
+ "root" : "Root Properties"
+ },
+ "hasPass" : true,
+ "hint" : "Name(Root Properties)",
+ "name" : "Root Properties",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "exported" : {
+ "subtask" : "Subtask Properties"
+ },
+ "hasPass" : false,
+ "name" : "Subtask Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "hasPass" : true,
+ "hint" : "Name(Subtask Properties Hints) root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value) root(Root Properties)",
+ "name" : "Subtask Properties Hints",
+ "status" : "FAIL"
+ },
+ {
+ "hasPass" : true,
+ "name" : "Chained Subtask Properties",
+ "status" : "PASS"
+ },
+ {
+ "hasPass" : true,
+ "name" : "Subtask Properties Reset",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "hasPass" : true,
+ "name" : "Root Preload",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "hasPass" : true,
+ "name" : "Subtask Preload",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ },
+ {
+ "hasPass" : true,
+ "name" : "Subtask Preload Preload",
+ "status" : "PASS"
+ },
+ {
+ "hasPass" : true,
+ "hint" : "Task is ready",
+ "name" : "Subtask Preload Hints PASS",
+ "status" : "READY"
+ },
+ {
+ "hasPass" : true,
+ "hint" : "Task failed",
+ "name" : "Subtask Preload Hints FAIL",
+ "status" : "FAIL"
+ },
+ {
+ "hasPass" : true,
+ "name" : "Subtask Preload Override Pass",
+ "status" : "READY"
+ },
+ {
+ "hasPass" : true,
+ "name" : "Subtask Preload Override Fail",
+ "status" : "PASS"
+ },
+ {
+ "hasPass" : true,
+ "name" : "Subtask Preload Extend Subtasks",
+ "status" : "READY",
+ "subTasks" : [
+ {
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ },
+ {
+ "hasPass" : true,
+ "name" : "Subtask APPLICABLE",
+ "status" : "PASS"
+ }
+ ]
+ },
+ {
+ "hasPass" : true,
+ "name" : "Subtask Preload Optional",
+ "status" : "PASS"
+ },
+ {
+ "hasPass" : true,
+ "hint" : "second-property(first-value second-extra third-value) fourth-property(fourth-value)",
+ "name" : "Subtask Preload Properties",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+ },
+ {
"hasPass" : false,
"name" : "INVALIDS",
"status" : "WAITING",
@@ -494,13 +749,28 @@
]
},
{
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "hasPass" : false,
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"hasPass" : true,
"name" : "WAITING (subtask missing)",
"status" : "WAITING",
"subTasks" : [
{
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -523,8 +793,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -533,6 +802,32 @@
"hasPass" : false,
"name" : "Subtask INVALID",
"status" : "INVALID"
+ },
+ {
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
}
diff --git a/test/all b/test/all
index fb98668..82347ab 100644
--- a/test/all
+++ b/test/all
@@ -11,6 +11,20 @@
{
"applicable" : true,
"hasPass" : true,
+ "name" : "Root APPLICABLE",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask APPLICABLE",
+ "status" : "PASS"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
"name" : "Root PASS",
"status" : "PASS"
},
@@ -176,6 +190,48 @@
{
"applicable" : true,
"hasPass" : false,
+ "name" : "Root Optional subtasks",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask FAIL",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask READY",
+ "status" : "READY",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
"name" : "Subtasks File",
"status" : "WAITING",
"subTasks" : [
@@ -303,6 +359,135 @@
},
{
"applicable" : true,
+ "exported" : {
+ "root" : "Root Properties"
+ },
+ "hasPass" : true,
+ "hint" : "Name(Root Properties)",
+ "name" : "Root Properties",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "exported" : {
+ "subtask" : "Subtask Properties"
+ },
+ "hasPass" : false,
+ "name" : "Subtask Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "Name(Subtask Properties Hints) root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value) root(Root Properties)",
+ "name" : "Subtask Properties Hints",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Chained Subtask Properties",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Properties Reset",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Preload",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Preload Preload",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "Task is ready",
+ "name" : "Subtask Preload Hints PASS",
+ "status" : "READY"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "Task failed",
+ "name" : "Subtask Preload Hints FAIL",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Preload Override Pass",
+ "status" : "READY"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Preload Override Fail",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Preload Extend Subtasks",
+ "status" : "READY",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask APPLICABLE",
+ "status" : "PASS"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Preload Optional",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "second-property(first-value second-extra third-value) fourth-property(fourth-value)",
+ "name" : "Subtask Preload Properties",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "applicable" : true,
"hasPass" : false,
"name" : "INVALIDS",
"status" : "WAITING",
@@ -329,14 +514,30 @@
},
{
"applicable" : true,
- "hasPass" : true,
- "name" : "WAITING (subtask missing)",
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : false,
- "name" : "MISSING",
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "WAITING (subtask missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -362,9 +563,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "applicable" : true,
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -376,6 +575,18 @@
"status" : "INVALID"
},
{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -392,6 +603,22 @@
"hasPass" : true,
"name" : "NA Bad INPROGRESS query",
"status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
},
@@ -435,14 +662,30 @@
},
{
"applicable" : true,
- "hasPass" : true,
- "name" : "WAITING (subtask missing)",
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : false,
- "name" : "MISSING",
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "WAITING (subtask missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -468,9 +711,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "applicable" : true,
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -482,6 +723,18 @@
"status" : "INVALID"
},
{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -498,6 +751,22 @@
"hasPass" : true,
"name" : "NA Bad INPROGRESS query",
"status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
}
diff --git a/test/invalid b/test/invalid
index 2fa594e..840c3da 100644
--- a/test/invalid
+++ b/test/invalid
@@ -74,14 +74,30 @@
},
{
"applicable" : true,
- "hasPass" : true,
- "name" : "WAITING (subtask missing)",
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : false,
- "name" : "MISSING",
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "WAITING (subtask missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -107,9 +123,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "applicable" : true,
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -121,6 +135,18 @@
"status" : "INVALID"
},
{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -137,6 +163,22 @@
"hasPass" : true,
"name" : "NA Bad INPROGRESS query",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
},
@@ -168,14 +210,30 @@
},
{
"applicable" : true,
- "hasPass" : true,
- "name" : "WAITING (subtask missing)",
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : false,
- "name" : "MISSING",
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "WAITING (subtask missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -201,9 +259,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "applicable" : true,
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -215,6 +271,18 @@
"status" : "INVALID"
},
{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -231,6 +299,22 @@
"hasPass" : true,
"name" : "NA Bad INPROGRESS query",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
}
diff --git a/test/invalid-applicable b/test/invalid-applicable
index c1c756a..8e52fa0 100644
--- a/test/invalid-applicable
+++ b/test/invalid-applicable
@@ -47,13 +47,28 @@
]
},
{
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "hasPass" : false,
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"hasPass" : true,
"name" : "WAITING (subtask missing)",
"status" : "WAITING",
"subTasks" : [
{
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -76,8 +91,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -86,6 +100,32 @@
"hasPass" : false,
"name" : "Subtask INVALID",
"status" : "INVALID"
+ },
+ {
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
}
diff --git a/test/preview b/test/preview
index a885f11..e2706ea 100644
--- a/test/preview
+++ b/test/preview
@@ -30,14 +30,30 @@
},
{
"applicable" : true,
- "hasPass" : true,
- "name" : "WAITING (subtask missing)",
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : false,
- "name" : "MISSING",
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "WAITING (subtask missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -63,9 +79,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "applicable" : true,
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -77,6 +91,18 @@
"status" : "INVALID"
},
{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -93,6 +119,22 @@
"hasPass" : true,
"name" : "NA Bad INPROGRESS query",
"status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
},
@@ -171,14 +213,30 @@
},
{
"applicable" : true,
- "hasPass" : true,
- "name" : "WAITING (subtask missing)",
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : false,
- "name" : "MISSING",
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "WAITING (subtask missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -204,9 +262,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "applicable" : true,
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -218,6 +274,18 @@
"status" : "INVALID"
},
{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -234,6 +302,22 @@
"hasPass" : true,
"name" : "NA Bad INPROGRESS query",
"status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
}
diff --git a/test/preview.invalid b/test/preview.invalid
index 2ddcb1c..86d12f1 100644
--- a/test/preview.invalid
+++ b/test/preview.invalid
@@ -30,14 +30,30 @@
},
{
"applicable" : true,
- "hasPass" : true,
- "name" : "WAITING (subtask missing)",
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : false,
- "name" : "MISSING",
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "WAITING (subtask missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -63,9 +79,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "applicable" : true,
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -77,6 +91,18 @@
"status" : "INVALID"
},
{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -93,6 +119,22 @@
"hasPass" : true,
"name" : "NA Bad INPROGRESS query",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
},
@@ -124,14 +166,30 @@
},
{
"applicable" : true,
- "hasPass" : true,
- "name" : "WAITING (subtask missing)",
+ "hasPass" : false,
+ "name" : "WAITING (subtask duplicate)",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : false,
- "name" : "MISSING",
+ "name" : "Subtask INVALID",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "WAITING (subtask missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -157,9 +215,7 @@
"status" : "WAITING",
"subTasks" : [
{
- "applicable" : true,
- "hasPass" : false,
- "name" : "MISSING",
+ "name" : "UNKNOWN",
"status" : "INVALID"
}
]
@@ -171,6 +227,18 @@
"status" : "INVALID"
},
{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Optional",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -187,6 +255,22 @@
"hasPass" : true,
"name" : "NA Bad INPROGRESS query",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Looping",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
}
]
}
diff --git a/test/root.change b/test/root.change
index e0d4187..99dea09 100644
--- a/test/root.change
+++ b/test/root.change
@@ -1,27 +1,24 @@
[root "INVALIDS Preview"]
- applicable = is:open
subtasks-file = invalids.config
[root "Root PASS Preview"]
- applicable = is:open
pass = True
[root "Root READY (subtask PASS) Preview"]
applicable = is:open
- pass = -is:open
+ pass = NOT is:open
subtask = Subtask PASS Preview
ready-hint = You must now run the ready task
[root "Subtasks External Preview"]
- applicable = is:open
subtasks-external = user special Preview
[root "Root NA Pass Preview"]
- applicable = -is:open
+ applicable = NOT is:open
pass = True
[root "NA INVALIDS Preview"]
- applicable = -is:open
+ applicable = NOT is:open
subtasks-file = invalids.config
[task "Subtask PASS Preview"]