Merge branch 'stable-3.0' into stable-3.1 * stable-3.0: (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: Iae62d42a6448c132521df7d384ae06945bafef0a
diff --git a/BUILD b/BUILD index 80bf4a3..e721ac7 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_DEPS + [plugin_name], +) + sh_test( name = "docker-tests", size = "medium",
diff --git a/WORKSPACE b/WORKSPACE index 4bbfd4c..f6120db 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..2d44757 --- /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.entities.BranchNameKey; + +/** An immutable reference to a fully qualified file in gerrit repo. */ +@AutoValue +public abstract class FileKey { + public static FileKey create(BranchNameKey branch, String file) { + return new AutoValue_FileKey(branch, file); + } + + public abstract BranchNameKey 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 028762c..b7284a1 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().branch()); - 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().branch(); + 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 5e987ca..81bc735 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.entities.BranchNameKey; 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(BranchNameKey branch, String fileName, boolean isVisible, boolean isTrusted) { - super(branch, fileName); + public TaskConfig( + BranchNameKey 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 f790ccc..f8b1dda 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 BranchNameKey.create(allProjects, "refs/meta/config"); } - public TaskConfig getTaskConfig(BranchNameKey branch, String fileName, boolean isTrusted) + public TaskConfig getTaskConfig(FileKey file, boolean isTrusted) throws ConfigInvalidException, IOException { + BranchNameKey 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 = BranchNameKey.create(psa.change.getProject(), psa.patchSet.refName()); } - Project.NameKey project = branch.project(); - TaskConfig cfg = new TaskConfig(branch, fileName, visible, isTrusted); + Project.NameKey project = file.branch().project(); + 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..7f1426a --- /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.entities.BranchNameKey; + +/** 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 BranchNameKey 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 9c4b476..8e4fd37 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(BranchNameKey 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 BranchNameKey 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().account().id(); - } catch (UnprocessableEntityException e) { - throw new ConfigInvalidException("Cannot resolve user: " + user); - } - return BranchNameKey.create(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 BranchNameKey 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().account().id(); + } catch (UnprocessableEntityException e) { + throw new ConfigInvalidException("Cannot resolve user: " + user); + } + return BranchNameKey.create(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..1bece85 --- /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.entities.BranchNameKey; +import com.google.gerrit.entities.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(BranchNameKey.create(Project.NameKey.parse(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