Merge branch 'stable-2.16' into stable-3.0
* stable-2.16: (49 commits)
Add change number to task output
Only copy required folders for testing
Add missing copyright/license header
Use a bash associative array to speedup task tests
plugins/task: Move TaskExpression iteration to Preloader
Add bazel rule for junit tests
Introduce TaskKey, SubSectionKey and FileKey
plugins/task: Fix cache name in Preloader
Only reload nodes when needed
Skip re-expanding properties for local properties
Use a lazy loading task property expansion model
Refresh TaskTree.Nodes when getting them
Cache preloaded tasks
Rename a bunch of TaskTree addNode() methods
Add preload-task to external file tests
Add Root Preload tasks-factory test
Create a TaskExpression with unit tests
Do not modify definition during preload
Return Optional<Task> in Task.Config
Make task config collection fields immutable
...
Change-Id: I4908973e957bba36f81ac2d7c836320faba18b35
diff --git a/BUILD b/BUILD
index 80bf4a3..39b3d0d 100644
--- a/BUILD
+++ b/BUILD
@@ -1,13 +1,34 @@
load(
"//tools/bzl:plugin.bzl",
"PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
"gerrit_plugin",
)
load("//tools/bzl:genrule2.bzl", "genrule2")
load("//tools/bzl:js.bzl", "polygerrit_plugin")
+load("//tools/bzl:junit.bzl", "junit_tests")
+load("@rules_java//java:defs.bzl", "java_library", "java_plugin")
plugin_name = "task"
+java_plugin(
+ name = "auto-value-plugin",
+ processor_class = "com.google.auto.value.processor.AutoValueProcessor",
+ deps = [
+ "@auto-value-annotations//jar",
+ "@auto-value//jar",
+ ],
+)
+
+java_library(
+ name = "auto-value",
+ exported_plugins = [
+ ":auto-value-plugin",
+ ],
+ visibility = ["//visibility:public"],
+ exports = ["@auto-value//jar"],
+)
+
gerrit_plugin(
name = plugin_name,
srcs = glob(["src/main/java/**/*.java"]),
@@ -19,6 +40,7 @@
],
resource_jars = [":gr-task-plugin-static"],
resources = glob(["src/main/resources/**/*"]),
+ deps = [":auto-value"],
javacopts = [ "-Werror", "-Xlint:all", "-Xlint:-classfile", "-Xlint:-processing"],
)
@@ -43,6 +65,13 @@
app = "plugin.html",
)
+junit_tests(
+ name = "junit-tests",
+ size = "small",
+ srcs = glob(["src/test/java/**/*Test.java"]),
+ deps = PLUGIN_TEST_DEPS + [plugin_name],
+)
+
sh_test(
name = "docker-tests",
size = "medium",
diff --git a/WORKSPACE b/WORKSPACE
index c047530..0c9c56f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -35,10 +35,27 @@
repository = GERRIT,
)
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+AUTO_VALUE_VERSION = "1.7.4"
+
+maven_jar(
+ name = "auto-value",
+ artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
+ sha1 = "6b126cb218af768339e4d6e95a9b0ae41f74e73d",
+)
+
+maven_jar(
+ name = "auto-value-annotations",
+ artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
+ sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86",
+)
+
# Load plugin API
load(
"@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
"gerrit_api",
)
+# Release Plugin API
gerrit_api()
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Copier.java b/src/main/java/com/googlesource/gerrit/plugins/task/Copier.java
new file mode 100644
index 0000000..2c0ccad
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Copier.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2021 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.google.common.primitives.Primitives;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Copier {
+ protected static <T> void deepCopyDeclaredFields(
+ Class<T> cls,
+ T from,
+ T to,
+ boolean includeInaccessible,
+ Collection<Class<?>> copyReferenceOnly) {
+ for (Field field : cls.getDeclaredFields()) {
+ try {
+ if (includeInaccessible) {
+ field.setAccessible(true);
+ }
+ Class<?> fieldCls = field.getType();
+ Object val = field.get(from);
+ if (field.getType().isPrimitive()
+ || Primitives.isWrapperType(fieldCls)
+ || (val instanceof String)
+ || val == null
+ || copyReferenceOnly.contains(fieldCls)) {
+ field.set(to, val);
+ } else if (val instanceof List) {
+ List<?> list = List.class.cast(val);
+ field.set(to, new ArrayList<>(list));
+ } else if (val instanceof Map) {
+ Map<?, ?> map = Map.class.cast(val);
+ field.set(to, new HashMap<>(map));
+ } else if (field.getName().equals("this$0")) { // Can't copy internal final field
+ } else {
+ throw new RuntimeException(
+ "Don't know how to deep copy " + fieldValueToString(field, val));
+ }
+ } catch (IllegalAccessException e) {
+ if (includeInaccessible) {
+ throw new RuntimeException(
+ "Cannot access field to copy it " + fieldValueToString(field, "unknown"));
+ }
+ }
+ }
+ }
+
+ protected static String fieldValueToString(Field field, Object val) {
+ return "field:" + field.getName() + " value:" + val + " type:" + field.getType();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/CopyOnWrite.java b/src/main/java/com/googlesource/gerrit/plugins/task/CopyOnWrite.java
new file mode 100644
index 0000000..efafd57
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/CopyOnWrite.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2021 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 java.util.function.Function;
+
+public class CopyOnWrite<T> {
+ protected Function<T, T> copier;
+ protected T original;
+ protected T copy;
+
+ public CopyOnWrite(T original, Function<T, T> copier) {
+ this.original = original;
+ this.copier = copier;
+ }
+
+ public T getOriginal() {
+ return original;
+ }
+
+ public T getForRead() {
+ return copy != null ? copy : original;
+ }
+
+ public T getForWrite() {
+ if (copy == null) {
+ copy = copier.apply(original);
+ }
+ return copy;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
new file mode 100644
index 0000000..c320aa7
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 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.google.auto.value.AutoValue;
+import com.google.gerrit.reviewdb.client.Branch;
+
+/** An immutable reference to a fully qualified file in gerrit repo. */
+@AutoValue
+public abstract class FileKey {
+ public static FileKey create(Branch.NameKey branch, String file) {
+ return new AutoValue_FileKey(branch, file);
+ }
+
+ public abstract Branch.NameKey branch();
+
+ public abstract String file();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
index 8babe1c..c48ed6e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -20,62 +20,125 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
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 final Map<TaskExpressionKey, Optional<Task>> optionalTaskByExpression = new HashMap<>();
+
+ public List<Task> getRootTasks(TaskConfig cfg) {
+ return getTasks(cfg, TaskConfig.SECTION_ROOT);
}
- protected static void preloadFrom(Task definition, Task preloadFrom) {
+ public List<Task> getTasks(TaskConfig cfg) {
+ return getTasks(cfg, TaskConfig.SECTION_TASK);
+ }
+
+ protected List<Task> getTasks(TaskConfig cfg, String type) {
+ List<Task> preloaded = new ArrayList<>();
+ for (Task task : cfg.getTasks(type)) {
+ try {
+ preloaded.add(preload(task));
+ } catch (ConfigInvalidException e) {
+ preloaded.add(null);
+ }
+ }
+ return preloaded;
+ }
+
+ /**
+ * Get a preloaded Task for this TaskExpression.
+ *
+ * @param expression
+ * @return Optional<Task> which is empty if the expression is optional and no tasks are resolved
+ * @throws ConfigInvalidException if the expression requires a task and no tasks are resolved
+ */
+ public Optional<Task> getOptionalTask(TaskConfig cfg, TaskExpression expression)
+ throws ConfigInvalidException {
+ Optional<Task> task = optionalTaskByExpression.get(expression.key);
+ if (task == null) {
+ task = preloadOptionalTask(cfg, expression);
+ optionalTaskByExpression.put(expression.key, task);
+ }
+ return task;
+ }
+
+ protected Optional<Task> preloadOptionalTask(TaskConfig cfg, TaskExpression expression)
+ throws ConfigInvalidException {
+ Optional<Task> definition = loadOptionalTask(cfg, expression);
+ return definition.isPresent() ? Optional.of(preload(definition.get())) : definition;
+ }
+
+ public Task preload(Task definition) throws ConfigInvalidException {
+ String expression = definition.preloadTask;
+ if (expression != null) {
+ Optional<Task> preloadFrom =
+ getOptionalTask(definition.config, new TaskExpression(definition.file(), expression));
+ if (preloadFrom.isPresent()) {
+ return preloadFrom(definition, preloadFrom.get());
+ }
+ }
+ return definition;
+ }
+
+ protected Optional<Task> loadOptionalTask(TaskConfig cfg, TaskExpression expression)
+ throws ConfigInvalidException {
+ try {
+ for (String name : expression) {
+ Optional<Task> task = cfg.getOptionalTask(name);
+ if (task.isPresent()) {
+ return task;
+ }
+ }
+ } catch (NoSuchElementException e) {
+ // expression was not optional but we ran out of names to try
+ throw new ConfigInvalidException("task not defined");
+ }
+ return Optional.empty();
+ }
+
+ protected static Task preloadFrom(Task definition, Task preloadFrom) {
+ Task preloadTo = definition.config.new Task(definition.subSection);
for (Field field : definition.getClass().getFields()) {
String name = field.getName();
- if ("isVisible".equals(name) || "isTrusted".equals(name) || "config".equals(name)) {
+ if ("config".equals(name)) {
continue;
}
try {
field.setAccessible(true);
- preloadField(field.getType(), field, definition, preloadFrom);
+ preloadField(field, definition, preloadFrom, preloadTo);
} catch (IllegalAccessException | IllegalArgumentException e) {
throw new RuntimeException();
}
}
+ return preloadTo;
}
- protected static <T, S, K, V> void preloadField(
- Class<T> clz, Field field, Task definition, Task preloadFrom)
+ protected static <S, K, V> void preloadField(
+ Field field, Task definition, Task preloadFrom, Task preloadTo)
throws IllegalArgumentException, IllegalAccessException {
- T pre = getField(clz, field, preloadFrom);
- if (pre != null) {
- T val = getField(clz, field, definition);
- if (val == null) {
- field.set(definition, pre);
- } else if (val instanceof List) {
- List<?> valList = List.class.cast(val);
- List<?> preList = List.class.cast(pre);
- field.set(definition, preloadListFrom(castUnchecked(valList), castUnchecked(preList)));
- } else if (val instanceof Map) {
- Map<?, ?> valMap = Map.class.cast(val);
- Map<?, ?> preMap = Map.class.cast(pre);
- field.set(definition, preloadMapFrom(castUnchecked(valMap), castUnchecked(preMap)));
- } // nothing to do for overridden preloaded scalars
+ Object pre = field.get(preloadFrom);
+ Object val = field.get(definition);
+ if (val == null) {
+ field.set(preloadTo, pre);
+ } else if (pre == null) {
+ field.set(preloadTo, val);
+ } else if (val instanceof List) {
+ List<?> valList = List.class.cast(val);
+ List<?> preList = List.class.cast(pre);
+ field.set(preloadTo, preloadListFrom(castUnchecked(valList), castUnchecked(preList)));
+ } else if (val instanceof Map) {
+ Map<?, ?> valMap = Map.class.cast(val);
+ Map<?, ?> preMap = Map.class.cast(pre);
+ field.set(preloadTo, preloadMapFrom(castUnchecked(valMap), castUnchecked(preMap)));
+ } else {
+ field.set(preloadTo, val);
}
}
- protected static <T> T getField(Class<T> clz, Field field, Object obj)
- throws IllegalArgumentException, IllegalAccessException {
- return clz.cast(field.get(obj));
- }
-
@SuppressWarnings("unchecked")
protected static <S> List<S> castUnchecked(List<?> list) {
List<S> forceCheck = (List<S>) list;
@@ -89,28 +152,30 @@
}
protected static <T> List<T> preloadListFrom(List<T> list, List<T> preList) {
- List<T> extended = list;
- if (!preList.isEmpty()) {
- extended = preList;
- if (!list.isEmpty()) {
- extended = new ArrayList<>(list.size() + preList.size());
- extended.addAll(preList);
- extended.addAll(list);
- }
+ if (preList.isEmpty()) {
+ return list;
}
+ if (list.isEmpty()) {
+ return preList;
+ }
+
+ List<T> extended = new ArrayList<>(list.size() + preList.size());
+ extended.addAll(preList);
+ extended.addAll(list);
return extended;
}
protected static <K, V> Map<K, V> preloadMapFrom(Map<K, V> map, Map<K, V> preMap) {
- Map<K, V> extended = map;
- if (!preMap.isEmpty()) {
- extended = preMap;
- if (!map.isEmpty()) {
- extended = new HashMap<>(map.size() + preMap.size());
- extended.putAll(preMap);
- extended.putAll(map);
- }
+ if (preMap.isEmpty()) {
+ return map;
}
+ if (map.isEmpty()) {
+ return preMap;
+ }
+
+ Map<K, V> 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
index 555f8df..5f9a8d5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
@@ -26,61 +26,157 @@
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.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Use to expand properties like ${_name} in the text of various definitions. */
public class Properties {
+ public static final Properties EMPTY_PARENT = new Properties();
+
+ protected final Properties parentProperties;
+ protected final Task origTask;
+ protected final CopyOnWrite<Task> task;
+ protected Expander expander;
+ protected Loader loader;
+ protected boolean init = true;
+ protected boolean isTaskRefreshNeeded;
+ protected boolean isSubNodeReloadRequired;
+
+ public Properties() {
+ this(null, null);
+ expander = new Expander(n -> "");
+ }
+
+ public Properties(Task origTask, Properties parentProperties) {
+ this.origTask = origTask;
+ this.parentProperties = parentProperties;
+ task = new CopyOnWrite<>(origTask, t -> origTask.config.new Task(t));
+ }
+
/** Use to expand properties specifically for Tasks. */
- public static class Task extends Expander {
- public static final Task EMPTY_PARENT = new Task();
-
- public Task() {
- super(Collections.emptyMap());
+ public Task getTask(ChangeData changeData) throws StorageException {
+ if (loader != null && loader.isNonTaskDefinedPropertyLoaded()) {
+ // To detect NamesFactories dependent on non task defined properties, the checking must be
+ // done after subnodes are fully loaded, which unfortunately happens after getTask() is
+ // called. However, these non task property uses from the last change are still detectable
+ // here before we replace the old Loader with a new one.
+ isSubNodeReloadRequired = true;
}
- public Task(ChangeData changeData, TaskConfig.Task definition, Task parentProperties)
- throws StorageException {
- super(parentProperties.forDescendants());
- valueByName.putAll(getInternalProperties(definition, changeData));
- new RecursiveExpander(valueByName).expand(definition.getAllProperties());
+ loader = new Loader(changeData);
+ expander = new Expander(n -> loader.load(n));
- definition.setExpandedProperties(valueByName);
+ if (isTaskRefreshNeeded || init) {
+ Map<String, String> exported = expander.expand(origTask.exported);
+ if (exported != origTask.exported) {
+ task.getForWrite().exported = exported;
+ }
- expandFieldValues(definition, Collections.emptySet());
+ expander.expand(task, Collections.emptySet());
+
+ if (init) {
+ init = false;
+ isTaskRefreshNeeded = loader.isNonTaskDefinedPropertyLoaded();
+ }
}
+ return task.getForRead();
+ }
- protected Map<String, String> forDescendants() {
- return new HashMap<>(valueByName);
- }
+ public boolean isSubNodeReloadRequired() {
+ return isSubNodeReloadRequired;
}
/** Use to expand properties specifically for NamesFactories. */
- public static class NamesFactory extends Expander {
- public NamesFactory(TaskConfig.NamesFactory namesFactory, Task properties) {
- super(properties.valueByName);
- expandFieldValues(namesFactory, Sets.newHashSet(TaskConfig.KEY_TYPE));
- }
+ public NamesFactory getNamesFactory(NamesFactory namesFactory) {
+ return expander.expand(
+ namesFactory,
+ nf -> namesFactory.config.new NamesFactory(nf),
+ Sets.newHashSet(TaskConfig.KEY_TYPE));
}
- protected static Map<String, String> getInternalProperties(
- TaskConfig.Task definition, ChangeData changeData) throws StorageException {
- Map<String, String> properties = new HashMap<>();
+ protected class Loader {
+ protected final ChangeData changeData;
+ protected final Function<String, String> inheritedMapper;
+ protected Change change;
+ protected boolean isInheritedPropertyLoaded;
- properties.put("_name", definition.name);
+ public Loader(ChangeData changeData) {
+ this.changeData = changeData;
+ if (parentProperties == null || parentProperties.expander == null) {
+ inheritedMapper = n -> "";
+ } else {
+ inheritedMapper = n -> parentProperties.expander.getValueForName(n);
+ }
+ }
- Change c = changeData.change();
- properties.put("_change_number", String.valueOf(c.getId().get()));
- properties.put("_change_id", c.getKey().get());
- properties.put("_change_project", c.getProject().get());
- properties.put("_change_branch", c.getDest().get());
- properties.put("_change_status", c.getStatus().toString());
- properties.put("_change_topic", c.getTopic());
+ public boolean isNonTaskDefinedPropertyLoaded() {
+ return change != null || isInheritedPropertyLoaded;
+ }
- return properties;
+ public String load(String name) {
+ if (name.startsWith("_")) {
+ return internal(name);
+ }
+ String value = origTask.exported.get(name);
+ if (value == null) {
+ value = origTask.properties.get(name);
+ if (value == null) {
+ value = inheritedMapper.apply(name);
+ if (!value.isEmpty()) {
+ isInheritedPropertyLoaded = true;
+ }
+ }
+ }
+ return value;
+ }
+
+ protected String internal(String name) {
+ if ("_name".equals(name)) {
+ return origTask.name();
+ }
+ String changeProp = name.replace("_change_", "");
+ if (changeProp != name) {
+ try {
+ return change(changeProp);
+ } catch (StorageException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return "";
+ }
+
+ protected String change(String changeProp) throws StorageException {
+ switch (changeProp) {
+ case "number":
+ return String.valueOf(change().getId().get());
+ case "id":
+ return change().getKey().get();
+ case "project":
+ return change().getProject().get();
+ case "branch":
+ return change().getDest().get();
+ case "status":
+ return change().getStatus().toString();
+ case "topic":
+ return change().getTopic();
+ default:
+ return "";
+ }
+ }
+
+ protected Change change() {
+ if (change == null) {
+ try {
+ change = changeData.change();
+ } catch (StorageException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return change;
+ }
}
/**
@@ -100,116 +196,162 @@
*
* <p>will expand to: <code>"The brown fox jumped over the fence."</code>
*/
- protected static class RecursiveExpander {
- protected final Expander expander;
- protected Map<String, String> unexpandedByName;
- protected Set<String> expanding;
+ protected static class Expander extends AbstractExpander {
+ protected final Function<String, String> loadingFunction;
+ protected final Map<String, String> valueByName = new HashMap<>();
+ protected final Set<String> expanding = new HashSet<>();
- public RecursiveExpander(Map<String, String> valueByName) {
- expander =
- new Expander(valueByName) {
- @Override
- protected String getValueForName(String name) {
- expandUnexpanded(name); // recursive call
- return super.getValueForName(name);
- }
- };
+ public Expander(Function<String, String> loadingFunction) {
+ this.loadingFunction = loadingFunction;
}
- public void expand(Map<String, String> unexpandedByName) {
- this.unexpandedByName = unexpandedByName;
-
- // Copy keys to allow out of order removals during iteration
- for (String unexpanedName : new ArrayList<>(unexpandedByName.keySet())) {
- expanding = new HashSet<>();
- expandUnexpanded(unexpanedName);
+ /**
+ * Expand all properties (${property_name} -> property_value) in the given text. Returns same
+ * object if no expansions occured.
+ */
+ public Map<String, String> expand(Map<String, String> map) {
+ if (map != null) {
+ boolean hasProperty = false;
+ Map<String, String> expandedMap = new HashMap<>(map.size());
+ for (Map.Entry<String, String> e : map.entrySet()) {
+ String name = e.getKey();
+ String value = e.getValue();
+ String expanded = getValueForName(name);
+ hasProperty = hasProperty || value != expanded;
+ expandedMap.put(name, expanded);
+ }
+ return hasProperty ? Collections.unmodifiableMap(expandedMap) : map;
}
+ return null;
}
- protected void expandUnexpanded(String name) {
- if (!expanding.add(name)) {
- throw new RuntimeException("Looping property definitions.");
- }
- String value = unexpandedByName.remove(name);
+ @Override
+ public String getValueForName(String name) {
+ String value = valueByName.get(name);
if (value != null) {
- expander.valueByName.put(name, expander.expandText(value));
+ return value;
}
+ value = loadingFunction.apply(name);
+ if (value == null) {
+ value = "";
+ } else if (!value.isEmpty()) {
+ if (!expanding.add(name)) {
+ throw new RuntimeException("Looping property definitions.");
+ }
+ value = expandText(value);
+ expanding.remove(name);
+ }
+ valueByName.put(name, value);
+ return value;
}
}
/**
* Use to expand properties like ${property} in Strings into their values.
*
- * <p>Given some property name/value asssociations defined like this:
+ * <p>Given some property name/value associations like this:
*
* <p><code>
- * valueByName.put("animal", "fox");
- * valueByName.put("bar", "foo");
- * valueByName.put("obstacle", "fence");
+ * "animal" -> "fox"
+ * "bar" -> "foo"
+ * "obstacle" -> "fence"
* </code>
*
* <p>a String like: <code>"The brown ${animal} jumped over the ${obstacle}."</code>
*
- * <p>will expand to: <code>"The brown fox jumped over the fence."</code>
+ * <p>will expand to: <code>"The brown fox jumped over the fence."</code> This class is meant to
+ * be used as a building block for other full featured expanders and thus must be overriden to
+ * provide the name/value associations via the getValueForName() method.
*/
- protected static class Expander {
+ protected abstract static class AbstractExpander {
// "${_name}" -> group(1) = "_name"
protected static final Pattern PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
- public final Map<String, String> valueByName;
-
- public Expander(Map<String, String> valueByName) {
- this.valueByName = valueByName;
+ /**
+ * Returns expanded object if property found in the Strings in the object's Fields (except the
+ * excluded ones). Returns same object if no expansions occured.
+ */
+ public <T> T expand(T object, Function<T, T> copier, Set<String> excludedFieldNames) {
+ return expand(new CopyOnWrite<>(object, copier), excludedFieldNames);
}
- /** Expand all properties in the Strings in the object's Fields (except the exclude ones) */
- protected void expandFieldValues(Object object, Set<String> excludedFieldNames) {
- for (Field field : object.getClass().getFields()) {
+ /**
+ * Returns expanded object if property found in the Strings in the object's Fields (except the
+ * excluded ones). Returns same object if no expansions occured.
+ */
+ public <T> T expand(CopyOnWrite<T> cow, Set<String> excludedFieldNames) {
+ for (Field field : cow.getOriginal().getClass().getFields()) {
try {
if (!excludedFieldNames.contains(field.getName())) {
field.setAccessible(true);
- Object o = field.get(object);
+ Object o = field.get(cow.getOriginal());
if (o instanceof String) {
- field.set(object, expandText((String) o));
+ String expanded = expandText((String) o);
+ if (expanded != o) {
+ field.set(cow.getForWrite(), expanded);
+ }
} else if (o instanceof List) {
@SuppressWarnings("unchecked")
List<String> forceCheck = List.class.cast(o);
- expandElements(forceCheck);
+ List<String> expanded = expand(forceCheck);
+ if (expanded != o) {
+ field.set(cow.getForWrite(), expanded);
+ }
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
+ return cow.getForRead();
}
- /** Expand all properties in the Strings in the List */
- public void expandElements(List<String> list) {
+ /**
+ * Returns expanded unmodifiable List if property found. Returns same object if no expansions
+ * occured.
+ */
+ public List<String> expand(List<String> list) {
if (list != null) {
- for (ListIterator<String> it = list.listIterator(); it.hasNext(); ) {
- it.set(expandText(it.next()));
+ boolean hasProperty = false;
+ List<String> expandedList = new ArrayList<>(list.size());
+ for (String value : list) {
+ String expanded = expandText(value);
+ hasProperty = hasProperty || value != expanded;
+ expandedList.add(expanded);
}
+ return hasProperty ? Collections.unmodifiableList(expandedList) : list;
}
+ return null;
}
- /** Expand all properties (${property_name} -> property_value) in the given text */
+ /**
+ * Expand all properties (${property_name} -> property_value) in the given text . Returns same
+ * object if no expansions occured.
+ */
public String expandText(String text) {
if (text == null) {
return null;
}
StringBuffer out = new StringBuffer();
Matcher m = PATTERN.matcher(text);
- while (m.find()) {
- m.appendReplacement(out, Matcher.quoteReplacement(getValueForName(m.group(1))));
+ if (!m.find()) {
+ return text;
}
+ do {
+ m.appendReplacement(out, Matcher.quoteReplacement(getValueForName(m.group(1))));
+ } while (m.find());
m.appendTail(out);
return out.toString();
}
- /** Get the replacement value for the property identified by name */
- protected String getValueForName(String name) {
- String value = valueByName.get(name);
- return value == null ? "" : value;
- }
+ /**
+ * Get the replacement value for the property identified by name
+ *
+ * @param name of the property to get the replacement value for
+ * @return the replacement value. Since the expandText() method alwyas needs a String to replace
+ * '${property-name}' reference with, even when the property does not exist, this will never
+ * return null, instead it will returns the empty string if the property is not found.
+ */
+ protected abstract String getValueForName(String name);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java
new file mode 100644
index 0000000..54db1e4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2021 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.google.auto.value.AutoValue;
+
+/** An immutable reference to a SubSection in fully qualified task config file. */
+@AutoValue
+public abstract class SubSectionKey {
+ public static SubSectionKey create(FileKey file, String section, String subSection) {
+ return new AutoValue_SubSectionKey(file, section, subSection == null ? "" : subSection);
+ }
+
+ public abstract FileKey file();
+
+ public abstract String section();
+
+ public abstract String subSection();
+}
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 2ac26d8..08add9f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -51,6 +51,7 @@
public String hint;
public Boolean inProgress;
public String name;
+ public Integer change;
public Status status;
public List<TaskAttribute> subTasks;
public Long evaluationMilliSeconds;
@@ -97,7 +98,7 @@
new AttributeFactory(node).create().ifPresent(t -> a.roots.add(t));
}
}
- } catch (ConfigInvalidException | IOException e) {
+ } catch (ConfigInvalidException | IOException | StorageException e) {
a.roots.add(invalid());
}
@@ -121,7 +122,7 @@
this.node = node;
this.matchCache = matchCache;
this.task = node.task;
- this.attribute = new TaskAttribute(task.name);
+ this.attribute = new TaskAttribute(task.name());
}
public Optional<TaskAttribute> create() {
@@ -138,6 +139,9 @@
}
if (applicable || !options.onlyApplicable) {
+ if (node.isChange()) {
+ attribute.change = node.getChangeData().getId().get();
+ }
attribute.hasPass = task.pass != null || task.fail != null;
attribute.subTasks = getSubTasks();
attribute.status = getStatus();
@@ -166,7 +170,10 @@
}
}
}
- } catch (QueryParseException | RuntimeException e) {
+ } catch (ConfigInvalidException
+ | IOException
+ | QueryParseException
+ | RuntimeException e) {
return Optional.of(invalid()); // bad applicability query
}
return Optional.empty();
@@ -241,13 +248,18 @@
}
}
- protected List<TaskAttribute> getSubTasks() throws StorageException {
+ protected List<TaskAttribute> getSubTasks()
+ throws ConfigInvalidException, IOException, StorageException {
List<TaskAttribute> subTasks = new ArrayList<>();
for (Node subNode : node.getSubNodes()) {
if (subNode == null) {
subTasks.add(invalid());
} else {
- new AttributeFactory(subNode, matchCache).create().ifPresent(t -> subTasks.add(t));
+ MatchCache subMatchCache = matchCache;
+ if (!matchCache.changeData.getId().equals(subNode.getChangeData().getId())) {
+ subMatchCache = new MatchCache(predicateCache, subNode.getChangeData());
+ }
+ new AttributeFactory(subNode, subMatchCache).create().ifPresent(t -> subTasks.add(t));
}
}
if (subTasks.isEmpty()) {
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 8cd8022..698b33a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -14,21 +14,20 @@
package com.googlesource.gerrit.plugins.task;
-import com.google.common.primitives.Primitives;
+import com.google.common.collect.Sets;
import com.google.gerrit.common.Container;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.server.git.meta.AbstractVersionedMetaData;
-import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
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 {
@@ -44,15 +43,17 @@
}
}
- protected class Section extends Container {
+ protected class SubSection extends Container {
public TaskConfig config;
+ public final SubSectionKey subSection;
- public Section() {
+ public SubSection(SubSectionKey s) {
this.config = TaskConfig.this;
+ this.subSection = s;
}
}
- public class TaskBase extends Section {
+ public class TaskBase extends SubSection {
public String applicable;
public Map<String, String> exported;
public String fail;
@@ -70,7 +71,8 @@
public boolean isVisible;
public boolean isTrusted;
- public TaskBase(SubSection s, boolean isVisible, boolean isTrusted) {
+ public TaskBase(SubSectionKey s, boolean isVisible, boolean isTrusted) {
+ super(s);
this.isVisible = isVisible;
this.isTrusted = isTrusted;
applicable = getString(s, KEY_APPLICABLE, null);
@@ -89,53 +91,39 @@
}
protected TaskBase(TaskBase base) {
- copyDeclaredFields(TaskBase.class, base);
+ this(base.subSection);
+ Copier.deepCopyDeclaredFields(TaskBase.class, base, this, false, copyOnlyReferencesFor());
}
- protected <T> void copyDeclaredFields(Class<T> cls, T from) {
- for (Field field : cls.getDeclaredFields()) {
- try {
- field.setAccessible(true);
- Class<?> fieldCls = field.getType();
- Object val = field.get(from);
- if (field.getType().isPrimitive()
- || Primitives.isWrapperType(fieldCls)
- || (val instanceof String)
- || val == null) {
- field.set(this, val);
- } else if (val instanceof List) {
- List<?> list = List.class.cast(val);
- field.set(this, new ArrayList<>(list));
- } else if (val instanceof Map) {
- Map<?, ?> map = Map.class.cast(val);
- field.set(this, new HashMap<>(map));
- } else if (field.getName().equals("this$0")) { // Don't copy internal final field
- } else {
- throw new RuntimeException(
- "Don't know how to deep copy " + fieldValueToString(field, val));
- }
- } catch (IllegalAccessException e) {
- throw new RuntimeException(
- "Cannot access field to copy it " + fieldValueToString(field, "unknown"));
- }
- }
- }
-
- protected String fieldValueToString(Field field, Object val) {
- return "field:" + field.getName() + " value:" + val + " type:" + field.getType();
+ protected TaskBase(SubSectionKey s) {
+ super(s);
}
}
public class Task extends TaskBase {
- public String name;
+ public final TaskKey key;
- public Task(SubSection s, boolean isVisible, boolean isTrusted) {
+ public Task(SubSectionKey s, boolean isVisible, boolean isTrusted) {
super(s, isVisible, isTrusted);
- name = s.subSection;
+ key = TaskKey.create(s);
}
- protected Task(TaskBase base) {
- super(base);
+ public Task(Task task) {
+ super(task);
+ // Despite being copied in Copier.deepCopyDeclaredFields this
+ // is needed to avoid the final variable initialization warning.
+ this.key = task.key;
+ Copier.deepCopyDeclaredFields(Task.class, task, this, false, copyOnlyReferencesFor());
+ }
+
+ public Task(TasksFactory tasks, String name) {
+ super(tasks);
+ key = TaskKey.create(tasks.subSection, name);
+ }
+
+ public Task(SubSectionKey s) {
+ super(s);
+ key = TaskKey.create(s);
}
protected Map<String, String> getAllProperties() {
@@ -144,50 +132,60 @@
return all;
}
- protected void setExpandedProperties(Map<String, String> expanded) {
- properties.clear();
- properties.putAll(expanded);
- for (String property : exported.keySet()) {
- exported.put(property, properties.get(property));
- }
+ public String name() {
+ return key.task();
+ }
+
+ public FileKey file() {
+ return key.subSection().file();
+ }
+
+ public TaskKey key() {
+ return key;
}
}
public class TasksFactory extends TaskBase {
public String namesFactory;
- public TasksFactory(SubSection s, boolean isVisible, boolean isTrusted) {
+ public TasksFactory(SubSectionKey s, boolean isVisible, boolean isTrusted) {
super(s, isVisible, isTrusted);
namesFactory = getString(s, KEY_NAMES_FACTORY, null);
}
}
- public class NamesFactory extends Section {
+ public class NamesFactory extends SubSection {
public String changes;
public List<String> names;
public String type;
- public NamesFactory(SubSection s) {
+ public NamesFactory(SubSectionKey s) {
+ super(s);
changes = getString(s, KEY_CHANGES, null);
names = getStringList(s, KEY_NAME);
type = getString(s, KEY_TYPE, null);
}
+
+ public NamesFactory(NamesFactory n) {
+ super(n.subSection);
+ Copier.deepCopyDeclaredFields(NamesFactory.class, n, this, false, copyOnlyReferencesFor());
+ }
}
- public class External extends Section {
+ public class External extends SubSection {
public String name;
public String file;
public String user;
- public External(SubSection s) {
- name = s.subSection;
+ public External(SubSectionKey s) {
+ super(s);
+ name = s.subSection();
file = getString(s, KEY_FILE, null);
user = getString(s, KEY_USER, null);
}
}
- protected static final Pattern OPTIONAL_TASK_PATTERN =
- Pattern.compile("([^ |]*( *[^ |])*) *\\| *");
+ public static final String SEP = "\0";
protected static final String SECTION_EXTERNAL = "external";
protected static final String SECTION_NAMES_FACTORY = "names-factory";
@@ -214,34 +212,27 @@
protected static final String KEY_TYPE = "type";
protected static final String KEY_USER = "user";
+ protected final FileKey file;
public boolean isVisible;
public boolean isTrusted;
- public Task createTask(TasksFactory tasks, String name) {
- Task task = new Task(tasks);
- task.name = name;
- return task;
+ public TaskConfig(FileKey file, boolean isVisible, boolean isTrusted) {
+ this(file.branch(), file, isVisible, isTrusted);
}
- public TaskConfig(Branch.NameKey branch, String fileName, boolean isVisible, boolean isTrusted) {
- super(branch, fileName);
+ public TaskConfig(
+ Branch.NameKey masqueraded, FileKey file, boolean isVisible, boolean isTrusted) {
+ super(masqueraded, file.file());
+ this.file = file;
this.isVisible = isVisible;
this.isTrusted = isTrusted;
}
- public List<Task> getRootTasks() {
- return getTasks(SECTION_ROOT);
- }
-
- public List<Task> getTasks() {
- return getTasks(SECTION_TASK);
- }
-
protected List<Task> getTasks(String type) {
List<Task> tasks = new ArrayList<>();
// No need to get a task with no name (what would we call it?)
for (String task : cfg.getSubsections(type)) {
- tasks.add(new Task(new SubSection(type, task), isVisible, isTrusted));
+ tasks.add(new Task(subSectionKey(type, task), isVisible, isTrusted));
}
return tasks;
}
@@ -255,62 +246,40 @@
return externals;
}
- /* 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);
+ protected Optional<Task> getOptionalTask(String name) {
+ SubSectionKey subSection = subSectionKey(SECTION_TASK, name);
+ return getNames(subSection).isEmpty()
+ ? Optional.empty()
+ : Optional.of(new Task(subSection, isVisible, isTrusted));
}
public TasksFactory getTasksFactory(String name) {
- return new TasksFactory(new SubSection(SECTION_TASKS_FACTORY, name), isVisible, isTrusted);
+ return new TasksFactory(subSectionKey(SECTION_TASKS_FACTORY, name), isVisible, isTrusted);
}
public NamesFactory getNamesFactory(String name) {
- return new NamesFactory(new SubSection(SECTION_NAMES_FACTORY, name));
+ return new NamesFactory(subSectionKey(SECTION_NAMES_FACTORY, name));
}
public External getExternal(String name) {
- return getExternal(new SubSection(SECTION_EXTERNAL, name));
+ return getExternal(subSectionKey(SECTION_EXTERNAL, name));
}
- protected External getExternal(SubSection s) {
+ protected External getExternal(SubSectionKey s) {
return new External(s);
}
- protected Map<String, String> getProperties(SubSection s, String prefix) {
+ protected Map<String, String> getProperties(SubSectionKey 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;
+ return Collections.unmodifiableMap(valueByName);
}
- protected Map<String, String> getStringByName(SubSection s, Iterable<String> names) {
+ protected Map<String, String> getStringByName(SubSectionKey s, Iterable<String> names) {
Map<String, String> valueByName = new HashMap<>();
for (String name : names) {
valueByName.put(name, getString(s, name));
@@ -318,7 +287,7 @@
return valueByName;
}
- protected Set<String> getMatchingNames(SubSection s, String match) {
+ protected Set<String> getMatchingNames(SubSectionKey s, String match) {
Set<String> matched = new HashSet<>();
for (String name : getNames(s)) {
if (name.matches(match)) {
@@ -328,30 +297,29 @@
return matched;
}
- protected Set<String> getNames(SubSection s) {
- return cfg.getNames(s.section, s.subSection);
+ protected Set<String> getNames(SubSectionKey s) {
+ return cfg.getNames(s.section(), s.subSection());
}
- protected String getString(SubSection s, String key, String def) {
+ protected String getString(SubSectionKey s, String key, String def) {
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 String getString(SubSectionKey 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));
+ protected List<String> getStringList(SubSectionKey s, String key) {
+ return Collections.unmodifiableList(
+ Arrays.asList(cfg.getStringList(s.section(), s.subSection(), key)));
}
- protected static class SubSection {
- public final String section;
- public final String subSection;
+ protected SubSectionKey subSectionKey(String section, String subSection) {
+ return SubSectionKey.create(file, section, subSection);
+ }
- protected SubSection(String section, String subSection) {
- this.section = section;
- this.subSection = subSection;
- }
+ protected Collection<Class<?>> copyOnlyReferencesFor() {
+ return Sets.newHashSet(TaskKey.class, SubSectionKey.class);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java
index d5003b4..bbf424b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java
@@ -59,7 +59,7 @@
}
public TaskConfig getRootConfig() throws ConfigInvalidException, IOException {
- return getTaskConfig(getRootBranch(), DEFAULT, true);
+ return getTaskConfig(FileKey.create(getRootBranch(), DEFAULT), true);
}
public void masquerade(PatchSetArgument psa) {
@@ -70,8 +70,9 @@
return new Branch.NameKey(allProjects, "refs/meta/config");
}
- public TaskConfig getTaskConfig(Branch.NameKey branch, String fileName, boolean isTrusted)
+ public TaskConfig getTaskConfig(FileKey file, boolean isTrusted)
throws ConfigInvalidException, IOException {
+ Branch.NameKey branch = file.branch();
PatchSetArgument psa = psaMasquerades.get(branch);
boolean visible = true; // invisible psas are filtered out by commandline
if (psa == null) {
@@ -81,12 +82,15 @@
branch = new Branch.NameKey(psa.change.getProject(), psa.patchSet.getId().toRefName());
}
- Project.NameKey project = branch.getParentKey();
- TaskConfig cfg = new TaskConfig(branch, fileName, visible, isTrusted);
+ Project.NameKey project = file.branch().getParentKey();
+ TaskConfig cfg =
+ psa == null
+ ? new TaskConfig(file, visible, isTrusted)
+ : new TaskConfig(branch, file, visible, isTrusted);
try (Repository git = gitMgr.openRepository(project)) {
cfg.load(project, git);
} catch (IOException e) {
- log.atWarning().withCause(e).log("Failed to load %s for %s", fileName, project);
+ log.atWarning().withCause(e).log("Failed to load %s for %s", file.file(), project);
throw e;
} catch (ConfigInvalidException e) {
throw e;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
new file mode 100644
index 0000000..90dffff
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2021 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 java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A TaskExpression represents a config string pointing to an expression which includes zero or more
+ * task names separated by a '|', and potentially termintated by a '|'. If the expression is not
+ * terminated by a '|' it indicates that task resolution of at least one task is required. Task
+ * selection priority is from left to right. This can be expressed as: <code>
+ * EXPR = [ TASK_NAME '|' ] TASK_NAME [ '|' ]</code>
+ *
+ * <p>Example expressions to prioritized names and requirements:
+ *
+ * <ul>
+ * <li><code> "simple" -> ("simple") required</code>
+ * <li><code> "world | peace" -> ("world", "peace") required</code>
+ * <li><code> "shadenfreud |" -> ("shadenfreud") optional</code>
+ * <li><code> "foo | bar |" -> ("foo", "bar") optional</code>
+ * </ul>
+ */
+public class TaskExpression implements Iterable<String> {
+ protected static final Pattern EXPRESSION_PATTERN = Pattern.compile("([^ |]+[^|]*)(\\|)?");
+ protected final TaskExpressionKey key;
+
+ public TaskExpression(FileKey key, String expression) {
+ this.key = TaskExpressionKey.create(key, expression);
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return new Iterator<String>() {
+ Matcher m = EXPRESSION_PATTERN.matcher(key.expression());
+ Boolean hasNext;
+ boolean optional;
+
+ @Override
+ public boolean hasNext() {
+ if (hasNext == null) {
+ hasNext = m.find();
+ if (hasNext) {
+ optional = m.group(2) != null;
+ }
+ }
+ if (!hasNext && !optional) {
+ return true; // fake it so next() throws an Exception
+ }
+ return hasNext;
+ }
+
+ @Override
+ public String next() {
+ hasNext(); // in case next() was (re)called w/o calling hasNext()
+ if (!hasNext) {
+ throw new NoSuchElementException("No more names, yet expression was not optional");
+ }
+ hasNext = null;
+ return m.group(1).trim();
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java
new file mode 100644
index 0000000..0a05b2e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2022 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.google.auto.value.AutoValue;
+
+/** A key for TaskExpression. */
+@AutoValue
+public abstract class TaskExpressionKey {
+ public static TaskExpressionKey create(FileKey file, String expression) {
+ return new AutoValue_TaskExpressionKey(file, expression);
+ }
+
+ public abstract FileKey file();
+
+ public abstract String expression();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
new file mode 100644
index 0000000..0af75c8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 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.google.auto.value.AutoValue;
+import com.google.gerrit.reviewdb.client.Branch;
+
+/** An immutable reference to a task in task config file. */
+@AutoValue
+public abstract class TaskKey {
+ /** Creates a TaskKey with task name as the name of sub section. */
+ public static TaskKey create(SubSectionKey section) {
+ return create(section, section.subSection());
+ }
+
+ /** Creates a TaskKey from a sub section and task name, generally used by TasksFactory. */
+ public static TaskKey create(SubSectionKey section, String task) {
+ return new AutoValue_TaskKey(section, task);
+ }
+
+ public Branch.NameKey branch() {
+ return subSection().file().branch();
+ }
+
+ public abstract SubSectionKey subSection();
+
+ public abstract String task();
+}
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 311c07e..2bd8cf6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -40,11 +40,15 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.Set;
-import java.util.function.BiFunction;
import org.eclipse.jgit.errors.ConfigInvalidException;
/**
@@ -55,13 +59,20 @@
*/
public class TaskTree {
private static final FluentLogger log = FluentLogger.forEnclosingClass();
+
+ @FunctionalInterface
+ public interface NodeFactory {
+ Node create(NodeList parent, Task definition) throws Exception;
+ }
+
protected static final String TASK_DIR = "task";
protected final AccountResolver accountResolver;
protected final AllUsersNameProvider allUsers;
protected final CurrentUser user;
protected final TaskConfigFactory taskFactory;
- protected final Root root = new Root();
+ protected final Preloader preloader;
+ protected final NodeList root = new NodeList();
protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
@@ -75,177 +86,219 @@
CurrentUser user,
TaskConfigFactory taskFactory,
Provider<ChangeQueryBuilder> changeQueryBuilderProvider,
- Provider<ChangeQueryProcessor> changeQueryProcessorProvider) {
+ Provider<ChangeQueryProcessor> changeQueryProcessorProvider,
+ Preloader preloader) {
this.accountResolver = accountResolver;
this.allUsers = allUsers;
this.user = user != null ? user : anonymousUser;
this.taskFactory = taskFactory;
this.changeQueryProcessorProvider = changeQueryProcessorProvider;
this.changeQueryBuilderProvider = changeQueryBuilderProvider;
+ this.preloader = preloader;
}
public void masquerade(PatchSetArgument psa) {
taskFactory.masquerade(psa);
}
- public List<Node> getRootNodes(ChangeData changeData) throws ConfigInvalidException, IOException {
+ public List<Node> getRootNodes(ChangeData changeData)
+ throws ConfigInvalidException, IOException, StorageException {
this.changeData = changeData;
- return root.getRootNodes();
- }
-
- public Node createNodeOrNull(NodeList parent, Task definition) {
- try {
- return new Node(parent, definition);
- } catch (Exception e) {
- return null;
- }
+ root.path = Collections.emptyList();
+ return root.getSubNodes();
}
protected class NodeList {
protected NodeList parent = null;
- protected LinkedList<String> path = new LinkedList<>();
+ protected Collection<String> path;
+ protected Map<TaskKey, Node> cachedNodeByTask = new HashMap<>();
protected List<Node> nodes;
protected Set<String> names = new HashSet<>();
- protected void addSubDefinitions(List<Task> defs) {
+ protected void addSubNodes() throws ConfigInvalidException, IOException, StorageException {
+ addPreloaded(preloader.getRootTasks(taskFactory.getRootConfig()));
+ }
+
+ protected void addPreloaded(List<Task> defs) throws ConfigInvalidException, StorageException {
for (Task def : defs) {
- addSubDefinition(def);
+ addPreloaded(def);
}
}
- protected void addSubDefinition(Task def) {
- addSubDefinition(def, (d, c) -> createNodeOrNull(d, c));
+ protected void addPreloaded(Task def) throws ConfigInvalidException, StorageException {
+ addPreloaded(def, (parent, definition) -> new Node(parent, definition));
}
- protected void addSubDefinition(Task def, BiFunction<NodeList, Task, Node> nodeConstructor) {
- Node node = null;
- if (def != null && !path.contains(def.name) && names.add(def.name)) {
- // path check above detects looping definitions
- // names check above detects duplicate subtasks
- node = nodeConstructor.apply(this, def);
+ protected void addPreloaded(Task def, NodeFactory nodeFactory)
+ throws ConfigInvalidException, StorageException {
+ if (def != null) {
+ try {
+ Node node = cachedNodeByTask.get(def.key());
+ boolean isRefreshNeeded = node != null;
+ if (node == null) {
+ node = nodeFactory.create(this, def);
+ }
+
+ if (!path.contains(node.key()) && names.add(def.name())) {
+ // path check above detects looping definitions
+ // names check above detects duplicate subtasks
+ if (isRefreshNeeded) {
+ node.refreshTask();
+ }
+ nodes.add(node);
+ return;
+ }
+ } catch (Exception e) {
+ }
}
- nodes.add(node);
+ addInvalidNode();
+ }
+
+ protected void addInvalidNode() {
+ nodes.add(null); // null node indicates invalid
+ }
+
+ protected List<Node> getSubNodes() throws ConfigInvalidException, IOException, StorageException {
+ if (nodes == null) {
+ nodes = new ArrayList<>();
+ addSubNodes();
+ } else {
+ refreshSubNodes();
+ }
+ return nodes;
+ }
+
+ public void refreshSubNodes() throws ConfigInvalidException, StorageException {
+ if (nodes != null) {
+ for (Node node : nodes) {
+ if (node != null) {
+ node.refreshTask();
+ }
+ }
+ }
}
public ChangeData getChangeData() {
return parent == null ? TaskTree.this.changeData : parent.getChangeData();
}
- protected Properties.Task getProperties() {
- return Properties.Task.EMPTY_PARENT;
- }
- }
-
- protected class Root extends NodeList {
- public List<Node> getRootNodes() throws ConfigInvalidException, IOException {
- if (nodes == null) {
- nodes = new ArrayList<>();
- addSubDefinitions(getRootDefinitions());
- }
- return nodes;
- }
-
- protected List<Task> getRootDefinitions() throws ConfigInvalidException, IOException {
- return taskFactory.getRootConfig().getRootTasks();
+ protected Properties getProperties() {
+ return Properties.EMPTY_PARENT;
}
}
public class Node extends NodeList {
- public final Task task;
- protected final Properties.Task properties;
+ public Task task;
- public Node(NodeList parent, Task definition) throws ConfigInvalidException, StorageException {
+ protected final Properties properties;
+ protected final TaskKey taskKey;
+
+ public Node(NodeList parent, Task task) throws ConfigInvalidException, StorageException {
this.parent = parent;
- this.task = definition;
- this.path.addAll(parent.path);
- this.path.add(definition.name);
- Preloader.preload(definition);
- properties = new Properties.Task(getChangeData(), definition, parent.getProperties());
+ taskKey = task.key();
+ properties = new Properties(task, parent.getProperties());
+ refreshTask();
}
- public List<Node> getSubNodes() {
- if (nodes == null) {
- nodes = new ArrayList<>();
- addSubDefinitions();
+ public String key() {
+ return String.valueOf(getChangeData().getId().get()) + TaskConfig.SEP + taskKey;
+ }
+
+ /* The task needs to be refreshed before a node is used, however
+ subNode refreshing can wait until they are fetched since they may
+ not be needed. */
+ public void refreshTask() throws ConfigInvalidException, StorageException {
+ this.path = new LinkedList<>(parent.path);
+ this.path.add(key());
+
+ this.task = properties.getTask(getChangeData());
+
+ if (nodes != null && properties.isSubNodeReloadRequired()) {
+ cachedNodeByTask.clear();
+ nodes.stream().filter(n -> n != null).forEach(n -> cachedNodeByTask.put(n.task.key(), n));
+ names.clear();
+ nodes = null;
}
- return nodes;
}
- protected void addSubDefinitions() throws StorageException {
- addSubTaskDefinitions();
- addSubTasksFactoryDefinitions();
- addSubFileDefinitions();
- addExternalDefinitions();
+ @Override
+ protected void addSubNodes() throws ConfigInvalidException, StorageException {
+ addSubTasks();
+ addSubTasksFactoryTasks();
+ addSubTasksFiles();
+ addSubTasksExternals();
}
- protected void addSubTaskDefinitions() {
- for (String name : task.subTasks) {
+ protected void addSubTasks() throws ConfigInvalidException, StorageException {
+ for (String expression : task.subTasks) {
try {
- Task def = task.config.getTaskOptional(name);
- if (def != null) {
- addSubDefinition(def);
+ Optional<Task> def =
+ preloader.getOptionalTask(task.config, new TaskExpression(task.file(), expression));
+ if (def.isPresent()) {
+ addPreloaded(def.get());
}
} catch (ConfigInvalidException e) {
- addSubDefinition(null);
+ addInvalidNode();
}
}
}
- protected void addSubFileDefinitions() {
+ protected void addSubTasksFiles() throws ConfigInvalidException, StorageException {
for (String file : task.subTasksFiles) {
try {
- addSubDefinitions(getTaskDefinitions(task.config.getBranch(), file));
+ addPreloaded(
+ getPreloadedTasks(FileKey.create(task.key().branch(), resolveTaskFileName(file))));
} catch (ConfigInvalidException | IOException e) {
- addSubDefinition(null);
+ addInvalidNode();
}
}
}
- protected void addExternalDefinitions() throws StorageException {
+ protected void addSubTasksExternals() throws ConfigInvalidException, StorageException {
for (String external : task.subTasksExternals) {
try {
External ext = task.config.getExternal(external);
if (ext == null) {
- addSubDefinition(null);
+ addInvalidNode();
} else {
- addSubDefinitions(getTaskDefinitions(ext));
+ addPreloaded(getPreloadedTasks(ext));
}
} catch (ConfigInvalidException | IOException e) {
- addSubDefinition(null);
+ addInvalidNode();
}
}
}
- protected void addSubTasksFactoryDefinitions() throws StorageException {
- for (String taskFactoryName : task.subTasksFactories) {
- TasksFactory tasksFactory = task.config.getTasksFactory(taskFactoryName);
+ protected void addSubTasksFactoryTasks() throws ConfigInvalidException, StorageException {
+ for (String tasksFactoryName : task.subTasksFactories) {
+ TasksFactory tasksFactory = task.config.getTasksFactory(tasksFactoryName);
if (tasksFactory != null) {
NamesFactory namesFactory = task.config.getNamesFactory(tasksFactory.namesFactory);
if (namesFactory != null && namesFactory.type != null) {
- new Properties.NamesFactory(namesFactory, getProperties());
+ namesFactory = getProperties().getNamesFactory(namesFactory);
switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) {
case STATIC:
- addStaticTypeTasksDefinitions(tasksFactory, namesFactory);
+ addStaticTypeTasks(tasksFactory, namesFactory);
continue;
case CHANGE:
- addChangesTypeTaskDefinitions(tasksFactory, namesFactory);
+ addChangeTypeTasks(tasksFactory, namesFactory);
continue;
}
}
}
- addSubDefinition(null);
+ addInvalidNode();
}
}
- protected void addStaticTypeTasksDefinitions(
- TasksFactory tasksFactory, NamesFactory namesFactory) {
+ protected void addStaticTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
+ throws ConfigInvalidException, StorageException {
for (String name : namesFactory.names) {
- addSubDefinition(task.config.createTask(tasksFactory, name));
+ addPreloaded(preloader.preload(task.config.new Task(tasksFactory, name)));
}
}
- protected void addChangesTypeTaskDefinitions(
- TasksFactory tasksFactory, NamesFactory namesFactory) {
+ protected void addChangeTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
+ throws ConfigInvalidException, StorageException {
try {
if (namesFactory.changes != null) {
List<ChangeData> changeDataList =
@@ -254,9 +307,21 @@
.query(changeQueryBuilderProvider.get().parse(namesFactory.changes))
.entities();
for (ChangeData changeData : changeDataList) {
- addSubDefinition(
- task.config.createTask(tasksFactory, changeData.getId().toString()),
- new ChangeNodeFactory(changeData)::createChangeNodeOrNull);
+ addPreloaded(
+ preloader.preload(
+ task.config.new Task(tasksFactory, changeData.getId().toString())),
+ (parent, definition) ->
+ new Node(parent, definition) {
+ @Override
+ public ChangeData getChangeData() {
+ return changeData;
+ }
+
+ @Override
+ public boolean isChange() {
+ return true;
+ }
+ });
}
return;
}
@@ -264,75 +329,52 @@
log.atSevere().withCause(e).log("ERROR: running changes query: " + namesFactory.changes);
} catch (QueryParseException e) {
}
- addSubDefinition(null);
+ addInvalidNode();
}
- protected List<Task> getTaskDefinitions(External external)
+ protected List<Task> getPreloadedTasks(External external)
throws ConfigInvalidException, IOException, StorageException {
- return getTaskDefinitions(resolveUserBranch(external.user), external.file);
+ return getPreloadedTasks(
+ FileKey.create(resolveUserBranch(external.user), resolveTaskFileName(external.file)));
}
- protected List<Task> getTaskDefinitions(Branch.NameKey branch, String file)
+ protected List<Task> getPreloadedTasks(FileKey file)
throws ConfigInvalidException, IOException {
- return taskFactory
- .getTaskConfig(branch, resolveTaskFileName(file), task.isTrusted)
- .getTasks();
+ return preloader.getTasks(taskFactory.getTaskConfig(file, task.isTrusted));
}
@Override
- protected Properties.Task getProperties() {
+ protected Properties getProperties() {
return properties;
}
- protected String resolveTaskFileName(String file) throws ConfigInvalidException {
- if (file == null) {
- throw new ConfigInvalidException("External file not defined");
- }
- Path p = Paths.get(TASK_DIR, file);
- if (!p.startsWith(TASK_DIR)) {
- throw new ConfigInvalidException("task file not under " + TASK_DIR + " directory: " + file);
- }
- return p.toString();
- }
-
- protected Branch.NameKey resolveUserBranch(String user)
- throws ConfigInvalidException, IOException, StorageException {
- if (user == null) {
- throw new ConfigInvalidException("External user not defined");
- }
- Account.Id acct;
- try {
- acct = accountResolver.resolve(user).asUnique().getAccount().getId();
- } catch (UnprocessableEntityException e) {
- throw new ConfigInvalidException("Cannot resolve user: " + user);
- }
- return new Branch.NameKey(allUsers.get(), RefNames.refsUsers(acct));
+ public boolean isChange() {
+ return false;
}
}
- public class ChangeNodeFactory {
- public class ChangeNode extends Node {
- public ChangeNode(NodeList parent, Task definition) throws ConfigInvalidException {
- super(parent, definition);
- }
-
- public ChangeData getChangeData() {
- return ChangeNodeFactory.this.changeData;
- }
+ protected String resolveTaskFileName(String file) throws ConfigInvalidException {
+ if (file == null) {
+ throw new ConfigInvalidException("External file not defined");
}
-
- protected ChangeData changeData;
-
- public ChangeNodeFactory(ChangeData changeData) {
- this.changeData = changeData;
+ Path p = Paths.get(TASK_DIR, file);
+ if (!p.startsWith(TASK_DIR)) {
+ throw new ConfigInvalidException("task file not under " + TASK_DIR + " directory: " + file);
}
+ return p.toString();
+ }
- public ChangeNode createChangeNodeOrNull(NodeList parent, Task definition) {
- try {
- return new ChangeNode(parent, definition);
- } catch (Exception e) {
- return null;
- }
+ protected Branch.NameKey resolveUserBranch(String user)
+ throws ConfigInvalidException, IOException, StorageException {
+ if (user == null) {
+ throw new ConfigInvalidException("External user not defined");
}
+ Account.Id acct;
+ try {
+ acct = accountResolver.resolve(user).asUnique().getAccount().getId();
+ } catch (UnprocessableEntityException e) {
+ throw new ConfigInvalidException("Cannot resolve user: " + user);
+ }
+ return new Branch.NameKey(allUsers.get(), RefNames.refsUsers(acct));
}
}
diff --git a/src/main/resources/Documentation/test/preview.md b/src/main/resources/Documentation/test/preview.md
index e53b7d8..1aeb649 100644
--- a/src/main/resources/Documentation/test/preview.md
+++ b/src/main/resources/Documentation/test/preview.md
@@ -235,25 +235,93 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"name" : "_change1_number",
"status" : "FAIL",
"subTasks" : [
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "change" : _change2_number,
+ "hasPass" : true,
+ "name" : "_change2_number",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
]
},
{
"applicable" : true,
+ "change" : _change2_number,
"hasPass" : true,
"name" : "_change2_number",
"status" : "FAIL",
"subTasks" : [
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "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" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
@@ -322,6 +390,12 @@
"hasPass" : true,
"name" : "userfile task/special.config FAIL",
"status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
}
]
}
@@ -575,25 +649,93 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"name" : "_change1_number",
"status" : "FAIL",
"subTasks" : [
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "change" : _change2_number,
+ "hasPass" : true,
+ "name" : "_change2_number",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
]
},
{
"applicable" : true,
+ "change" : _change2_number,
"hasPass" : true,
"name" : "_change2_number",
"status" : "FAIL",
"subTasks" : [
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "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" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index ea09541..e9cc8b6 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -570,6 +570,12 @@
"hasPass" : true,
"name" : "userfile task/special.config FAIL",
"status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
}
]
}
@@ -597,6 +603,12 @@
"status" : "FAIL"
},
{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
+ },
+ {
"name" : "UNKNOWN",
"status" : "INVALID"
}
@@ -630,6 +642,12 @@
"status" : "FAIL"
},
{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
+ },
+ {
"name" : "UNKNOWN",
"status" : "INVALID"
}
@@ -661,6 +679,12 @@
"hasPass" : true,
"name" : "userfile task/special.config FAIL",
"status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
}
]
}
@@ -724,12 +748,14 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"name" : "_change1_number",
"status" : "FAIL"
},
{
"applicable" : true,
+ "change" : _change2_number,
"hasPass" : true,
"name" : "_change2_number",
"status" : "FAIL"
@@ -765,37 +791,192 @@
"status" : "PASS"
}
-[root "Root Properties"]
- set-root-property = root-value
- subtask = Subtask Properties
+[root "Root Same Name - Different Tasks-Factory"]
+ subtasks-factory = parent tasks-factory Same Name - Different Tasks-Factory
-[task "Subtask Properties"]
- subtask = Subtask Properties Hints
-
-[task "Subtask Properties Hints"]
- set-first-property = first-value
- set-second-property = ${first-property} second-extra ${third-property}
- set-third-property = third-value
+[tasks-factory "parent tasks-factory Same Name - Different Tasks-Factory"]
+ names-factory = parent names-factory Same Name - Different Tasks-Factory
fail = True
- fail-hint = root-property(${root-property}) first-property(${first-property}) second-property(${second-property})
+ subtasks-factory = child tasks-factory Same Name - Different Tasks-Factory
+
+[names-factory "parent names-factory Same Name - Different Tasks-Factory"]
+ type = static
+ name = Same Name
+
+[tasks-factory "child tasks-factory Same Name - Different Tasks-Factory"]
+ names-factory = child names-factory Same Name - Different Tasks-Factory
+ fail = False
+
+[names-factory "child names-factory Same Name - Different Tasks-Factory"]
+ type = static
+ name = Same Name
{
"applicable" : true,
"hasPass" : false,
- "name" : "Root Properties",
+ "name" : "Root Same Name - Different Tasks-Factory",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
+ "hasPass" : true,
+ "name" : "Same Name",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Same Name",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Same Name - Different Change"]
+ subtasks-factory = init tasks-factory Same Name - Different Change
+
+[tasks-factory "init tasks-factory Same Name - Different Change"]
+ names-factory = init names-factory Same Name - Different Change
+ subtask = Same Name - Different Change
+
+[names-factory "init names-factory Same Name - Different Change"]
+ type = change
+ changes = change:_change2_number
+
+[task "Same Name - Different Change"]
+ subtasks-factory = tasks-factory Same Name - Different Change
+ pass = False
+ ready-hint = continues on to change _change1_number
+ fail-hint = stops here since we are change _change1_number
+ fail = change:_change1_number
+
+[tasks-factory "tasks-factory Same Name - Different Change"]
+ names-factory = names-factory Same Name - Different Change
+ subtask = Same Name - Different Change
+
+[names-factory "names-factory Same Name - Different Change"]
+ type = change
+ changes = change:_change1_number NOT change:${_change_number}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Same Name - Different Change",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change2_number,
"hasPass" : false,
- "name" : "Subtask Properties",
+ "name" : "_change2_number",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : true,
- "hint" : "root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value)",
- "name" : "Subtask Properties Hints",
+ "name" : "Same Name - Different Change",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : false,
+ "name" : "_change1_number",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "stops here since we are change _change1_number",
+ "name" : "Same Name - Different Change",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Property References"]
+ set-first-property = first-value
+ set-backward-reference = first-[${first-property}]
+ set-forward-reference = last-[${last-property}]
+ set-last-property = last-value
+ fail = True
+ fail-hint = backward-reference(${backward-reference}) forward-reference(${forward-reference})
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "backward-reference(first-[first-value]) forward-reference(last-[last-value])",
+ "name" : "Root Property References",
+ "status" : "FAIL"
+}
+
+[root "Root Deep Property References"]
+ set-first-property = first-value
+ set-direct-reference = first-[${first-property}]
+ set-deep-reference = deep-{${direct-reference}}
+ fail = True
+ fail-hint = deep-reference(${deep-reference})
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "deep-reference(deep-{first-[first-value]})",
+ "name" : "Root Deep Property References",
+ "status" : "FAIL"
+}
+
+[root "Root Properties Referenced Twice"]
+ set-first-property = first-value
+ set-referenced-twice = first-[${first-property}] first-[${first-property}]
+ fail = True
+ fail-hint = first-[${first-property}] referenced-twice(${referenced-twice}) referenced-twice(${referenced-twice})
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "first-[first-value] referenced-twice(first-[first-value] first-[first-value]) referenced-twice(first-[first-value] first-[first-value])",
+ "name" : "Root Properties Referenced Twice",
+ "status" : "FAIL"
+}
+
+[root "Root Inherited Properties"]
+ set-root-property = root-value
+ subtask = Subtask Parent Inherited Properties
+
+[task "Subtask Parent Inherited Properties"]
+ set-parent-property = parent-value
+ subtask = Subtask Inherited Properties
+
+[task "Subtask Inherited Properties"]
+ set-my-property = my-value
+ fail = True
+ fail-hint = root-property(${root-property}) parent-property(${parent-property}) my-property(${my-property})
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Inherited Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Parent Inherited Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "root-property(root-value) parent-property(parent-value) my-property(my-value)",
+ "name" : "Subtask Inherited Properties",
"status" : "FAIL"
}
]
@@ -803,6 +984,41 @@
]
}
+[root "Root Inherited Distant Properties"]
+ set-root-property = root-value
+ set-root-change-property = ${_change_number}
+ subtask = Subtask Parent Inherited Distant Properties
+
+[task "Subtask Parent Inherited Distant Properties"]
+ subtask = Subtask Inherited Distant Properties
+
+[task "Subtask Inherited Distant Properties"]
+ fail = True
+ fail-hint = root-property(${root-property}) root-change-property(${root-change-property})
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Inherited Distant Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Parent Inherited Distant Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "root-property(root-value) root-change-property(_change_number)",
+ "name" : "Subtask Inherited Distant Properties",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+}
[root "Root Properties Reset By Subtask"]
set-root-to-reset-by-subtask = reset-my-root-value
@@ -829,6 +1045,46 @@
]
}
+[root "Root Inherited Property References"]
+ set-root-property = root-value
+ subtask = Subtask Parent Inherited Property References
+
+[task "Subtask Parent Inherited Property References"]
+ set-parent-property = parent-value
+ set-parent-inherited-root-reference = root-property(${root-property})
+ subtask = Subtask Inherited Property References
+
+[task "Subtask Inherited Property References"]
+ set-inherited-root-reference = root-[${root-property}]
+ set-inherited-parent-reference = parent-[${parent-property}]
+ set-inherited-root-deep-reference = parent-inherited-root-reference-[${parent-inherited-root-reference}]
+ fail = True
+ fail-hint = inherited-root-reference(${inherited-root-reference}) inherited-parent-reference(${inherited-parent-reference}) inherited-root-deep-reference(${inherited-root-deep-reference})
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Inherited Property References",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Parent Inherited Property References",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "inherited-root-reference(root-[root-value]) inherited-parent-reference(parent-[parent-value]) inherited-root-deep-reference(parent-inherited-root-reference-[root-property(root-value)])",
+ "name" : "Subtask Inherited Property References",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+}
+
[root "Root Properties Exports"]
export-root-exported = ${_name}
subtask = Subtask Properties Exports
@@ -973,7 +1229,7 @@
set-welcome-message = Welcome to the pleasuredome
names-factory = names-factory a change
fail-hint = ${welcome-message} Name(${_name}) Change Number(${_change_number}) Change Id(${_change_id}) Change Project(${_change_project}) Change Branch(${_change_branch}) Change Status(${_change_status}) Change Topic(${_change_topic})
- fail = True
+ fail = change:_change1_number
[names-factory "names-factory a change"]
type = change
@@ -987,6 +1243,7 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"hint" : "Welcome to the pleasuredome Name(_change1_number) Change Number(_change1_number) Change Id(_change1_id) Change Project(_change1_project) Change Branch(_change1_branch) Change Status(_change1_status) Change Topic(_change1_topic)",
"name" : "_change1_number",
@@ -994,9 +1251,48 @@
},
{
"applicable" : true,
+ "change" : _change2_number,
"hasPass" : true,
- "hint" : "Welcome to the pleasuredome Name(_change2_number) Change Number(_change2_number) Change Id(_change2_id) Change Project(_change2_project) Change Branch(_change2_branch) Change Status(_change2_status) Change Topic(_change2_topic)",
"name" : "_change2_number",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root tasks-factory _name Property Reference"]
+ subtasks-factory = Properties tasks-factory _name Property Reference
+
+[tasks-factory "Properties tasks-factory _name Property Reference"]
+ set-name-reference = first-property ${_name}
+ fail-hint = ${name-reference}
+ fail = true
+ names-factory = names-factory static list
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root tasks-factory _name Property Reference",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "first-property my a task",
+ "name" : "my a task",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "first-property my b task",
+ "name" : "my b task",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "first-property my c task",
+ "name" : "my c task",
"status" : "FAIL"
}
]
@@ -1082,12 +1378,14 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change_number,
"hasPass" : true,
"name" : "_change_number",
"status" : "FAIL"
},
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"name" : "_change1_number",
"status" : "FAIL"
@@ -1095,57 +1393,6 @@
]
}
-[root "Root Properties Expansion"]
- applicable = status:open
- subtask = Subtask Property Expansion fail-hint
-
-[task "Subtask Property Expansion fail-hint"]
- subtasks-factory = tasks-factory Property Expansion fail-hint
-
-[tasks-factory "tasks-factory Property Expansion fail-hint"]
- set-first-property = first-property ${_name}
- fail-hint = ${first-property}
- fail = true
- names-factory = names-factory static list
-
-{
- "applicable" : true,
- "hasPass" : false,
- "name" : "Root Properties Expansion",
- "status" : "WAITING",
- "subTasks" : [
- {
- "applicable" : true,
- "hasPass" : false,
- "name" : "Subtask Property Expansion fail-hint",
- "status" : "WAITING",
- "subTasks" : [
- {
- "applicable" : true,
- "hasPass" : true,
- "hint" : "first-property my a task",
- "name" : "my a task",
- "status" : "FAIL"
- },
- {
- "applicable" : true,
- "hasPass" : true,
- "hint" : "first-property my b task",
- "name" : "my b task",
- "status" : "FAIL"
- },
- {
- "applicable" : true,
- "hasPass" : true,
- "hint" : "first-property my c task",
- "name" : "my c task",
- "status" : "FAIL"
- }
- ]
- }
- ]
-}
-
[root "Root Preload"]
preload-task = Subtask FAIL
subtask = Subtask Preload
@@ -1176,6 +1423,158 @@
]
}
+[root "Root Properties names-factory Reference"]
+ subtasks-factory = tasks-factory Properties names-factory Reference
+ set-predicate = change:_change1_number
+
+[tasks-factory "tasks-factory Properties names-factory Reference"]
+ names-factory = Properties names-factory Reference
+ fail = True
+
+[names-factory "Properties names-factory Reference"]
+ type = change
+ changes = ${predicate} OR change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Properties names-factory Reference",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change_number,
+ "hasPass" : true,
+ "name" : "_change_number",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL"
+ }
+ ]
+}
+
+[root "Root Properties names-factory Deep Reference"]
+ subtasks-factory = tasks-factory Properties names-factory Deep Reference
+ set-predicate-reference = ${predicate}
+ set-predicate = change:_change1_number
+
+[tasks-factory "tasks-factory Properties names-factory Deep Reference"]
+ names-factory = Properties names-factory Deep Reference
+ fail = True
+
+[names-factory "Properties names-factory Deep Reference"]
+ type = change
+ changes = ${predicate-reference} OR change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Properties names-factory Deep Reference",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change_number,
+ "hasPass" : true,
+ "name" : "_change_number",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL"
+ }
+ ]
+}
+
+[root "Root Properties names-factory Reference Internal"]
+ subtasks-factory = tasks-factory Properties names-factory Reference Internal
+ set-predicate = change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+[tasks-factory "tasks-factory Properties names-factory Reference Internal"]
+ names-factory = Properties names-factory Reference Internal
+ fail = True
+
+[names-factory "Properties names-factory Reference Internal"]
+ type = change
+ changes = change:_change1_number OR ${predicate}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Properties names-factory Reference Internal",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change_number,
+ "hasPass" : true,
+ "name" : "_change_number",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL"
+ }
+ ]
+}
+
+[root "Root Properties names-factory Reference Inherited"]
+ subtask = task Properties names-factory Reference Inherited
+ set-predicate = change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+[task "task Properties names-factory Reference Inherited"]
+ subtasks-factory = tasks-factory Properties names-factory Reference Inherited
+
+[tasks-factory "tasks-factory Properties names-factory Reference Inherited"]
+ names-factory = Properties names-factory Reference Inherited
+ fail = True
+
+[names-factory "Properties names-factory Reference Inherited"]
+ type = change
+ changes = change:_change1_number OR ${predicate}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Properties names-factory Reference Inherited",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task Properties names-factory Reference Inherited",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change_number,
+ "hasPass" : true,
+ "name" : "_change_number",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+}
+
[root "Root Preload Preload"]
subtask = Subtask Preload Preload
@@ -1354,10 +1753,17 @@
subtask = Subtask Preload Properties
[task "Subtask Preload Properties"]
- preload-task = Subtask Properties Hints
+ preload-task = Subtask Preload Properties Hints
set-fourth-property = fourth-value
fail-hint = second-property(${second-property}) fourth-property(${fourth-property})
+[task "Subtask Preload 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 = root-property(${root-property}) first-property(${first-property}) second-property(${second-property})
+
{
"applicable" : true,
"hasPass" : false,
@@ -1374,6 +1780,40 @@
]
}
+[root "Root Preload tasks-factory"]
+ subtasks-factory = tasks-factory Preload tasks-factory
+
+[tasks-factory "tasks-factory Preload tasks-factory"]
+ names-factory = names-factory static list
+ preload-task = Subtask PASS
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Preload tasks-factory",
+ "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"
+ }
+ ]
+}
+
[root "Root INVALID Preload"]
preload-task = missing
@@ -1619,25 +2059,93 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"name" : "_change1_number",
"status" : "FAIL",
"subTasks" : [
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "change" : _change2_number,
+ "hasPass" : true,
+ "name" : "_change2_number",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
]
},
{
"applicable" : true,
+ "change" : _change2_number,
"hasPass" : true,
"name" : "_change2_number",
"status" : "FAIL",
"subTasks" : [
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "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" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
@@ -1906,25 +2414,93 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"name" : "_change1_number",
"status" : "FAIL",
"subTasks" : [
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "change" : _change2_number,
+ "hasPass" : true,
+ "name" : "_change2_number",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
]
},
{
"applicable" : true,
+ "change" : _change2_number,
"hasPass" : true,
"name" : "_change2_number",
"status" : "FAIL",
"subTasks" : [
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "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" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
@@ -1998,6 +2574,7 @@
[task "Looping Properties"]
set-A = ${B}
set-B = ${A}
+ fail-hint = ${A}
fail = True
[task "task (tasks-factory missing)"]
@@ -2090,4 +2667,7 @@
[task "userfile task/special.config FAIL"]
applicable = is:open
fail = is:open
+
+[task "file task/common.config Preload PASS"]
+ preload-task = userfile task/special.config PASS
```
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
new file mode 100644
index 0000000..ac4ee88
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
@@ -0,0 +1,186 @@
+// Copyright (C) 2021 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.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import junit.framework.TestCase;
+
+/*
+ * <ul>
+ * <li><code> "simple" -> ("simple") required</code>
+ * <li><code> "world | peace" -> ("world", "peace") required</code>
+ * <li><code> "shadenfreud |" -> ("shadenfreud") optional</code>
+ * <li><code> "foo | bar |" -> ("foo", "bar") optional</code>
+ * </ul>
+ */
+public class TaskExpressionTest extends TestCase {
+ public static String SIMPLE = "simple";
+ public static String WORLD = "world";
+ public static String PEACE = "peace";
+ public static FileKey file = createFileKey("foo", "bar", "baz");
+
+ public void testBlank() {
+ TaskExpression exp = getTaskExpression("");
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testRequiredSingleName() {
+ TaskExpression exp = getTaskExpression(SIMPLE);
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testOptionalSingleName() {
+ TaskExpression exp = getTaskExpression(SIMPLE + "|");
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE);
+ assertFalse(it.hasNext());
+ }
+
+ public void testRequiredTwoNames() {
+ TaskExpression exp = getTaskExpression(WORLD + "|" + PEACE);
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), WORLD);
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), PEACE);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testOptionalTwoNames() {
+ TaskExpression exp = getTaskExpression(WORLD + "|" + PEACE + "|");
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), WORLD);
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), PEACE);
+ assertFalse(it.hasNext());
+ }
+
+ public void testBlankSpaces() {
+ TaskExpression exp = getTaskExpression(" ");
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testRequiredSingleNameLeadingSpaces() {
+ TaskExpression exp = getTaskExpression(" " + SIMPLE);
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testRequiredSingleNameTrailingSpaces() {
+ TaskExpression exp = getTaskExpression(SIMPLE + " ");
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testOptionalSingleNameLeadingSpaces() {
+ TaskExpression exp = getTaskExpression(" " + SIMPLE + "|");
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE);
+ assertFalse(it.hasNext());
+ }
+
+ public void testOptionalSingleNameTrailingSpaces() {
+ TaskExpression exp = getTaskExpression(SIMPLE + "| ");
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE);
+ assertFalse(it.hasNext());
+ }
+
+ public void testOptionalSingleNameMiddleSpaces() {
+ TaskExpression exp = getTaskExpression(SIMPLE + " |");
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE);
+ assertFalse(it.hasNext());
+ }
+
+ public void testRequiredTwoNamesMiddleSpaces() {
+ TaskExpression exp = getTaskExpression(WORLD + " | " + PEACE);
+ Iterator<String> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), WORLD);
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), PEACE);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testDifferentKeyOnDifferentFile() {
+ TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE);
+ TaskExpression otherExp = getTaskExpression(createFileKey("foo", "bar", "other"), SIMPLE);
+ assertFalse(exp.key.equals(otherExp.key));
+ }
+
+ public void testDifferentKeyOnDifferentBranch() {
+ TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE);
+ TaskExpression otherExp = getTaskExpression(createFileKey("foo", "other", "baz"), SIMPLE);
+ assertFalse(exp.key.equals(otherExp.key));
+ }
+
+ public void testDifferentKeyOnDifferentProject() {
+ TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE);
+ TaskExpression otherExp = getTaskExpression(createFileKey("other", "bar", "baz"), SIMPLE);
+ assertFalse(exp.key.equals(otherExp.key));
+ }
+
+ public void testDifferentKeyOnDifferentExpression() {
+ TaskExpression exp = getTaskExpression(SIMPLE);
+ TaskExpression otherExp = getTaskExpression(PEACE);
+ assertFalse(exp.key.equals(otherExp.key));
+ }
+
+ protected static void assertNoSuchElementException(Iterator<String> it) {
+ try {
+ it.next();
+ assertTrue(false);
+ } catch (NoSuchElementException e) {
+ assertTrue(true);
+ }
+ }
+
+ protected TaskExpression getTaskExpression(String expression) {
+ return getTaskExpression(file, expression);
+ }
+
+ protected TaskExpression getTaskExpression(FileKey file, String expression) {
+ return new TaskExpression(file, expression);
+ }
+
+ protected static FileKey createFileKey(String project, String branch, String file) {
+ return FileKey.create(new Branch.NameKey(new Project.NameKey(project), branch), file);
+ }
+}
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index 5b7e161..1d3efbd 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -37,9 +37,9 @@
result "$name" "$(diff <(echo "$expected") <(echo "$actual"))"
}
-result_root() { # group root expected_file actual_file
+result_root() { # group root
local name="$1 - $(echo "$2" | sed -es'/Root //')"
- result_out "$name" "$(get_root "$2" < "$3")" "$(get_root "$2" < "$4")"
+ result_out "$name" "${EXPECTED_ROOTS[$2]}" "${OUTPUT_ROOTS[$2]}"
}
# -------- Git Config
@@ -189,17 +189,36 @@
strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json
strip_non_invalid() { ensure "$MYDIR"/strip_non_invalid.py ; } # < json > json
-get_root() { # root < task_plugin_ouptut > root_json
- python -c "if True: # NOP to start indent
+define_jsonByRoot() { # task_plugin_ouptut > jsonByRoot_array_definition
+ local record root=''
+ local -A jsonByRoot
+ while IFS= read -r -d '' record ; do
+ if [ -z "$root" ] ; then
+ root=$record
+ else
+ jsonByRoot[$root]=$record
+ root=''
+ fi
+ done < <(python -c "if True: # NOP to start indent
import sys, json
roots=json.loads(sys.stdin.read())['plugins'][0]['roots']
for root in roots:
- if 'name' in root.keys() and root['name']=='$1':
- print json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True)"
+ root_json = json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True)
+ print root['name'] + '\x00' + root_json + '\x00',"
+ )
+
+ local def=$(declare -p jsonByRoot)
+ echo "${def#*=}" # declare -A jsonByRoot='(...)' > '(...)'
}
-example() { # example_num
+get_plugins() { # < change_json > plugins_json
+ python -c "import sys, json; \
+ plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \
+ print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)"
+}
+
+example() { # example_num > text_for_example_num
echo "$DOC_STATES" | awk '/```/{Q++;E=(Q+1)/2};E=='"$1" | grep -v '```' | replace_user
}
@@ -243,7 +262,7 @@
local repo=$1 remote=$2 ref=$3 change_id=$4 msg="Test change"
(
q cd "$repo"
- date > file
+ uuidgen > file
q git add .
[ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id")
q git commit -m "$msg"
@@ -251,28 +270,37 @@
)
}
-query_plugins() { # query
- gssh query "$@" --format json | head -1 | python -c "import sys, json; \
- plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \
- print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)"
-}
+query() { gssh query "$@" --format json ; } # query > json lines
-test_tasks() { # name expected_file task_args...
- local name=$1 expected=$2 ; shift 2
- local output=$STATUSES.$name out root
+# N < json lines > changeN_json
+change_plugins() { awk "NR==$1" | get_plugins | json_pp ; }
- query_plugins "$@" > "$output"
+results_suite() { # name expected_file plugins_json
+ local name=$1 expected=$2 actual=$3
+
+ local -A EXPECTED_ROOTS=$(define_jsonByRoot < "$expected")
+ local -A OUTPUT_ROOTS=$(echo "$actual" | define_jsonByRoot)
+
+ local out root
echo "$ROOTS" | while read root ; do
- result_root "$name" "$root" "$expected" "$output"
+ result_root "$name" "$root"
done
- out=$(diff "$expected" "$output" | head -15)
+ out=$(diff "$expected" <(echo "$actual") | head -15)
[ -z "$out" ]
result "$name - Full Test Suite" "$out"
}
+test_2generated() { # name task_args...
+ local name=$1 ; shift
+ local out=$(query "$@")
+ results_suite "$name" "$EXPECTED.$name" "$(echo "$out" | change_plugins 1)"
+ results_suite "$name 2nd change" "$EXPECTED.$name"2 "$(echo "$out" | change_plugins 2)"
+}
+
test_generated() { # name task_args...
local name=$1 ; shift
- test_tasks "$name" "$EXPECTED.$name" "$@"
+ query "$@" | change_plugins 1 > "$ACTUAL.$name"
+ results_suite "$name" "$EXPECTED.$name" "$( < "$ACTUAL.$name")"
}
test_file() { # name task_args...
@@ -297,7 +325,7 @@
DOC_PREVIEW=$DOCS/preview.md
EXPECTED=$OUT/expected
-STATUSES=$OUT/statuses
+ACTUAL=$OUT/actual
ROOT_CFG=$ALL/task.config
COMMON_CFG=$ALL_TASKS/common.config
@@ -342,9 +370,12 @@
q_setup update_repo "$USERS" "$REMOTE_USERS" "$REF_USERS"
change3_id=$(gen_change_id)
+change4_id=$(gen_change_id)
+change4_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change4_id")
change3_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change3_id")
-all_pjson=$(example 2 | testdoc_2_pjson | \
+ex2_pjson=$(example 2 | testdoc_2_pjson)
+all_pjson=$(echo "$ex2_pjson" | \
replace_change_properties \
"" \
"$change3_number" \
@@ -354,10 +385,23 @@
"NEW" \
"")
+all2_pjson=$(echo "$ex2_pjson" | \
+ replace_change_properties \
+ "" \
+ "$change4_number" \
+ "$change4_id" \
+ "$PROJECT" \
+ "refs\/heads\/$BRANCH" \
+ "NEW" \
+ "")
+
no_all_json=$(echo "$all_pjson" | remove_suite all)
+no_all2_json=$(echo "$all2_pjson" | remove_suite all)
echo "$no_all_json" | strip_non_applicable | \
grep -v "\"applicable\" :" > "$EXPECTED".applicable
+echo "$no_all2_json" | strip_non_applicable | \
+ grep -v "\"applicable\" :" > "$EXPECTED".applicable2
echo "$all_pjson" | remove_not_suite all | ensure json_pp > "$EXPECTED".all
@@ -376,8 +420,8 @@
RESULT=0
-query="change:$change3_number status:open"
-test_generated applicable --task--applicable "$query"
+query="(change:$change3_number OR change:$change4_number) status:open"
+test_2generated applicable --task--applicable "$query"
test_generated all --task--all "$query"
test_generated invalid --task--invalid "$query"
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
index ac185d8..dd2cb63 100755
--- a/test/docker/run_tests/start.sh
+++ b/test/docker/run_tests/start.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR"
-cp -r /task "$USER_HOME"/
+mkdir "$USER_HOME"/task && cp -r /task/{src,test} "$USER_HOME"/task
if [ "$1" = "retest" ] ; then
cd "$USER_RUN_TESTS_DIR"/../../ && ./check_task_statuses.sh "$GERRIT_HOST"
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..5df79bb
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,6 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+ _junit_tests = "junit_tests",
+)
+
+junit_tests = _junit_tests
\ No newline at end of file
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..4871c7b
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1,4 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", _gerrit = "GERRIT", _maven_jar = "maven_jar")
+
+maven_jar = _maven_jar
+GERRIT = _gerrit
\ No newline at end of file
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 89a1643..67536ef 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -2,7 +2,9 @@
"@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
_gerrit_plugin = "gerrit_plugin",
_plugin_deps = "PLUGIN_DEPS",
+ _plugin_test_deps = "PLUGIN_TEST_DEPS",
)
gerrit_plugin = _gerrit_plugin
PLUGIN_DEPS = _plugin_deps
+PLUGIN_TEST_DEPS = _plugin_test_deps
\ No newline at end of file