Merge branch 'stable-3.0' into stable-3.1
Update Docker image to 3.1.12 matching plugin API for build.
* stable-3.0: (21 commits)
fixup! test/docker: Use gerritcodereview docker image
Add counts to Task headers
Task UI: Add links to see full view of all tasks
Add Bazel support for running docker tests
test/docker: Use gerritcodereview docker image
Revert "plugin:task Adds support for names-factory of type change"
Remove Maven build
Task UI: Add status icons
TaskAttributeFactory: Consistently cache predicates
task: Remove unthrown exception
TaskAttributeFactory turn more Exceptions into INVALID tasks
plugin:task Adds support for names-factory of type change
Add support for tasks-factory and names-factory keywords
task plugin tests: handle missing refs/users/self
Task UI In Progress
Refactor docker test setup
Use a shorter docker test script name
Support to run shell script tests in docker environment
Upgrade bazlets to latest stable-3.0 to build with 3.0.15 API
Upgrade bazlets to latest stable-2.16 to build with 2.16.26 API
...
Change-Id: I8ed747be43a330e5e49ca55c01a95b771d62bdfe
diff --git a/.gitignore b/.gitignore
index da5cb24..c13cf52 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@
/package-lock.json
/task.iml
/node_modules/
+/test/docker/gerrit/artifacts
diff --git a/.zuul.yaml b/.zuul.yaml
new file mode 100644
index 0000000..e95b300
--- /dev/null
+++ b/.zuul.yaml
@@ -0,0 +1,10 @@
+- job:
+ name: plugins-task-build
+ parent: gerrit-plugin-build
+ pre-run:
+ tools/playbooks/install_docker.yaml
+
+- project:
+ check:
+ jobs:
+ - plugins-task-build
diff --git a/BUILD b/BUILD
index 5a2eb90..b2b57ae 100644
--- a/BUILD
+++ b/BUILD
@@ -6,14 +6,16 @@
load("//tools/bzl:genrule2.bzl", "genrule2")
load("//tools/bzl:js.bzl", "polygerrit_plugin")
+plugin_name = "task"
+
gerrit_plugin(
- name = "task",
+ name = plugin_name,
srcs = glob(["src/main/java/**/*.java"]),
manifest_entries = [
- "Gerrit-PluginName: task",
- "Gerrit-ApiVersion: 3.0-SNAPSHOT",
+ "Gerrit-PluginName: " + plugin_name,
+ "Gerrit-ApiVersion: 3.1.12",
"Implementation-Title: Task Plugin",
- "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/task",
+ "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/" + plugin_name,
"Gerrit-Module: com.googlesource.gerrit.plugins.task.Modules$Module",
],
resource_jars = [":gr-task-plugin-static"],
@@ -40,3 +42,12 @@
]),
app = "plugin.html",
)
+
+sh_test(
+ name = "docker-tests",
+ size = "medium",
+ srcs = ["test/docker/run.sh"],
+ args = ["--task-plugin-jar", "$(location :task)"],
+ data = [plugin_name] + glob(["test/**"]),
+ local = True,
+)
diff --git a/gr-task-plugin/gr-task-plugin.html b/gr-task-plugin/gr-task-plugin.html
index 67c1319..c5c04ff 100644
--- a/gr-task-plugin/gr-task-plugin.html
+++ b/gr-task-plugin/gr-task-plugin.html
@@ -17,15 +17,50 @@
<dom-module id="gr-task-plugin">
<template>
<style>
- ul { padding-left: 30px; }
- h3 { padding-left: 5px; }
+ ul { padding-left: 0.5em; }
+ h3 { padding-left: 0.1em; }
+ .cursor { cursor: pointer; }
+ #tasks_header {
+ align-items: center;
+ background-color: #fafafa;
+ border-top: 1px solid #ddd;
+ display: flex;
+ padding: 6px 1rem;
+ }
</style>
<div id="tasks" hidden$="[[!_tasks.length]]">
- <h3>Tasks: (Needs + Blocked)</h3>
- <ul>
- <gr-task-plugin-tasks tasks="[[_tasks]]"></gr-task-plugin-tasks>
- </ul>
+ <div id="tasks_header" style="display: flex;">
+ <iron-icon
+ icon="gr-icons:expand-less"
+ hidden$="[[!_expand_all]]"
+ on-tap="_switch_expand"
+ class="cursor"> </iron-icon>
+ <iron-icon
+ icon="gr-icons:expand-more"
+ hidden$="[[_expand_all]]"
+ on-tap="_switch_expand"
+ class="cursor"> </iron-icon>
+ <div style="display: flex; align-items: center; column-gap: 1em;">
+ <h3 on-tap="_switch_expand" class="cursor"> Tasks </h3>
+ <template is="dom-if" if="[[_expand_all]]">
+ <gr-button
+ on-tap="_show_all_tap"
+ disabled="[[_is_show_all(_show_all)]]"> Show All ([[_all_count]]) </gr-button>
+ <gr-button
+ on-tap="_needs_and_blocked_tap"
+ disabled="[[!_is_show_all(_show_all)]]">
+ Needs + Blocked ([[_ready_count]], [[_fail_count]]) </gr-button>
+ </template>
+ </div>
+ </div>
+ <div hidden$="[[!_expand_all]]">
+ <ul style="list-style-type:none;">
+ <gr-task-plugin-tasks
+ tasks="[[_tasks]]"
+ show_all$="[[_show_all]]"> </gr-task-plugin-tasks>
+ </ul>
+ </div>
</div>
</template>
<script src="gr-task-plugin.js"></script>
@@ -34,10 +69,42 @@
<dom-module id="gr-task-plugin-tasks">
<template>
<template is="dom-repeat" as="task" items="[[tasks]]">
- <template is="dom-if" if="[[task.message]]">
- <li>[[task.message]]</li>
+ <template is="dom-if" if="[[_can_show(show_all, task)]]">
+ <li>
+ <style>
+ /* Matching colors with core code. */
+ .green {
+ color: #9fcc6b;
+ }
+ .red {
+ color: #FFA62F;
+ }
+ </style>
+ <template is="dom-if" if="[[task.icon.id]]">
+ <gr-tooltip-content
+ has-tooltip
+ title="In Progress">
+ <iron-icon
+ icon="gr-icons:hourglass"
+ class="green"
+ hidden$="[[!task.in_progress]]">
+ </iron-icon>
+ </gr-tooltip-content>
+ <gr-tooltip-content
+ has-tooltip
+ title$="[[task.icon.tooltip]]">
+ <iron-icon
+ icon="[[task.icon.id]]"
+ class$="[[task.icon.color]]">
+ </iron-icon>
+ </gr-tooltip-content>
+ </template>
+ [[task.message]]
+ </li>
</template>
- <gr-task-plugin-tasks tasks="[[task.sub_tasks]]"></gr-task-plugin-tasks>
+ <gr-task-plugin-tasks
+ tasks="[[task.sub_tasks]]"
+ show_all$="[[show_all]]"> </gr-task-plugin-tasks>
</template>
</template>
<script>
@@ -49,6 +116,15 @@
notify: true,
value() { return []; },
},
+
+ show_all: {
+ type: String,
+ notify: true,
+ },
+ },
+
+ _can_show(show, task) {
+ return show === 'true' || task.showOnFilter;
},
});
</script>
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js
index 9fca732..3f284de 100644
--- a/gr-task-plugin/gr-task-plugin.js
+++ b/gr-task-plugin/gr-task-plugin.js
@@ -39,6 +39,40 @@
notify: true,
value() { return []; },
},
+
+ _show_all: {
+ type: String,
+ notify: true,
+ value: 'false',
+ },
+
+ _expand_all: {
+ type: Boolean,
+ notify: true,
+ value: true,
+ },
+
+ _all_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+
+ _ready_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+
+ _fail_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+ },
+
+ _is_show_all(show_all) {
+ return show_all === 'true';
},
attached() {
@@ -64,26 +98,82 @@
});
},
- _getTaskDescription(task) {
- return task.hint || task.name;
+ _computeIcon(task) {
+ const icon = {};
+ switch (task.status) {
+ case 'FAIL':
+ icon.id = 'gr-icons:close';
+ icon.color = 'red';
+ icon.tooltip = 'Failed';
+ break;
+ case 'READY':
+ icon.id = 'gr-icons:rebase';
+ icon.color = 'green';
+ icon.tooltip = 'Ready';
+ break;
+ case 'INVALID':
+ icon.id = 'gr-icons:abandon';
+ icon.color = 'red';
+ icon.tooltip = 'Invalid';
+ break;
+ case 'WAITING':
+ icon.id = 'gr-icons:side-by-side';
+ icon.color = 'red';
+ icon.tooltip = 'Waiting';
+ break;
+ case 'PASS':
+ icon.id = 'gr-icons:check';
+ icon.color = 'green';
+ icon.tooltip = 'Passed';
+ break;
+ }
+ return icon;
},
- _computeMessage(task) {
+ _computeShowOnNeedsAndBlockedFilter(task) {
switch (task.status) {
case 'FAIL':
case 'READY':
case 'INVALID':
- return this._getTaskDescription(task);
+ return true;
+ }
+ return false;
+ },
+
+ _compute_counts(task) {
+ this._all_count++;
+ switch (task.status) {
+ case 'FAIL':
+ this._fail_count++;
+ break;
+ case 'READY':
+ this._ready_count++;
+ break;
}
},
_addTasks(tasks) { // rename to process, remove DOM bits
if (!tasks) return [];
tasks.forEach(task => {
- task.message = this._computeMessage(task);
+ task.message = task.hint || task.name;
+ task.icon = this._computeIcon(task);
+ task.showOnFilter = this._computeShowOnNeedsAndBlockedFilter(task);
+ this._compute_counts(task);
this._addTasks(task.sub_tasks);
});
return tasks;
},
+
+ _show_all_tap() {
+ this._show_all = 'true';
+ },
+
+ _needs_and_blocked_tap() {
+ this._show_all = 'false';
+ },
+
+ _switch_expand() {
+ this._expand_all = !this._expand_all;
+ },
});
})();
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index bb66e53..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,88 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2016 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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <groupId>com.googlesource.gerrit.plugins.task</groupId>
- <artifactId>task</artifactId>
- <packaging>jar</packaging>
- <version>3.2.0-SNAPSHOT</version>
- <name>task</name>
-
- <properties>
- <Gerrit-ApiType>plugin</Gerrit-ApiType>
- <Gerrit-ApiVersion>${project.version}</Gerrit-ApiVersion>
- </properties>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <version>2.4</version>
- <configuration>
- <archive>
- <manifestEntries>
- <Gerrit-Module>com.googlesource.gerrit.plugins.task.Modules$Module</Gerrit-Module>
- <Implementation-Vendor>Gerrit Code Review</Implementation-Vendor>
- <Implementation-URL>http://code.google.com/p/gerrit/</Implementation-URL>
-
- <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
- <Implementation-Version>${project.version}</Implementation-Version>
-
- <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
- <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
- </manifestEntries>
- </archive>
- </configuration>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <version>2.3.2</version>
- <configuration>
- <source>1.8</source>
- <target>1.8</target>
- <encoding>UTF-8</encoding>
- <fork>true</fork>
- <compilerArgs>
- <arg>-XX:MaxPermSize=256m</arg>
- </compilerArgs>
- </configuration>
- </plugin>
- </plugins>
- </build>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
- <version>${Gerrit-ApiVersion}</version>
- <scope>provided</scope>
- </dependency>
- </dependencies>
-
- <repositories>
- <repository>
- <id>gerrit-api-repository</id>
- <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
- </repository>
- </repositories>
-</project>
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 1a5c702..54da2af 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -69,7 +69,8 @@
protected final TaskTree definitions;
protected final ChangeQueryBuilder cqb;
- protected final Map<String, Predicate<ChangeData>> predicatesByQuery = new HashMap<>();
+ protected final Map<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>>
+ predicatesByQuery = new HashMap<>();
protected Modules.MyOptions options;
@@ -86,11 +87,7 @@
for (PatchSetArgument psa : options.patchSetArguments) {
definitions.masquerade(psa);
}
- try {
- return createWithExceptions(c);
- } catch (StorageException e) {
- log.atSevere().withCause(e).log("Cannot load tasks for: %s", c);
- }
+ return createWithExceptions(c);
}
return null;
}
@@ -154,7 +151,7 @@
}
}
}
- } catch (QueryParseException e) {
+ } catch (QueryParseException | RuntimeException e) {
atts.add(invalid()); // bad applicability query
}
}
@@ -198,7 +195,7 @@
match(c, def.fail);
match(c, def.pass);
return true;
- } catch (StorageException | QueryParseException e) {
+ } catch (QueryParseException | RuntimeException e) {
return false;
}
}
@@ -206,7 +203,7 @@
protected Status getStatus(ChangeData c, Task def, TaskAttribute a) {
try {
return getStatusWithExceptions(c, def, a);
- } catch (QueryParseException e) {
+ } catch (QueryParseException | RuntimeException e) {
return Status.INVALID;
}
}
@@ -218,7 +215,8 @@
boolean hasDefinedSubtasks =
!(def.subTasks.isEmpty()
&& def.subTasksFiles.isEmpty()
- && def.subTasksExternals.isEmpty());
+ && def.subTasksExternals.isEmpty()
+ && def.subTasksFactories.isEmpty());
if (hasDefinedSubtasks) {
// Remove 'Grouping" tasks (tasks with subtasks but no PASS
// or FAIL criteria) from the output if none of their subtasks
@@ -295,31 +293,48 @@
return true;
}
- protected boolean match(ChangeData c, String query) throws QueryParseException {
- if (query == null || query.equalsIgnoreCase("true")) {
+ protected boolean match(ChangeData c, String query) throws StorageException, QueryParseException {
+ if (query == null) {
return true;
}
- Predicate<ChangeData> pred = predicatesByQuery.get(query);
- if (pred == null) {
- pred = cqb.parse(query);
- predicatesByQuery.put(query, pred);
- }
- return pred.asMatchable().match(c);
+ return matchWithExceptions(c, query);
}
protected Boolean matchOrNull(ChangeData c, String query) {
if (query != null) {
try {
- if (query.equalsIgnoreCase("true")) {
- return true;
- }
- return cqb.parse(query).asMatchable().match(c);
- } catch (StorageException | QueryParseException e) {
+ return matchWithExceptions(c, query);
+ } catch (QueryParseException | RuntimeException e) {
}
}
return null;
}
+ protected boolean matchWithExceptions(ChangeData c, String query)
+ throws QueryParseException, StorageException {
+ if ("true".equalsIgnoreCase(query)) {
+ return true;
+ }
+ return getPredicate(query).asMatchable().match(c);
+ }
+
+ protected Predicate<ChangeData> getPredicate(String query) throws QueryParseException {
+ ThrowingProvider<Predicate<ChangeData>, QueryParseException> predProvider =
+ predicatesByQuery.get(query);
+ if (predProvider != null) {
+ return predProvider.get();
+ }
+ // never seen 'query' before
+ try {
+ Predicate<ChangeData> pred = cqb.parse(query);
+ predicatesByQuery.put(query, new ThrowingProvider.Entry<>(pred));
+ return pred;
+ } catch (QueryParseException e) {
+ predicatesByQuery.put(query, new ThrowingProvider.Thrown<>(e));
+ throw e;
+ }
+ }
+
protected static boolean isAllNull(Object... vals) {
for (Object val : vals) {
if (val != null) {
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 6047616..a60569c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -17,6 +17,7 @@
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.HashMap;
@@ -38,7 +39,7 @@
}
}
- public class Task extends Section {
+ public class TaskBase extends Section {
public String applicable;
public Map<String, String> exported;
public String fail;
@@ -51,12 +52,13 @@
public String readyHint;
public List<String> subTasks;
public List<String> subTasksExternals;
+ public List<String> subTasksFactories;
public List<String> subTasksFiles;
public boolean isVisible;
public boolean isTrusted;
- public Task(SubSection s, boolean isVisible, boolean isTrusted) {
+ public TaskBase(SubSection s, boolean isVisible, boolean isTrusted) {
this.isVisible = isVisible;
this.isTrusted = isTrusted;
applicable = getString(s, KEY_APPLICABLE, null);
@@ -71,8 +73,52 @@
readyHint = getString(s, KEY_READY_HINT, null);
subTasks = getStringList(s, KEY_SUBTASK);
subTasksExternals = getStringList(s, KEY_SUBTASKS_EXTERNAL);
+ subTasksFactories = getStringList(s, KEY_SUBTASKS_FACTORY);
subTasksFiles = getStringList(s, KEY_SUBTASKS_FILE);
}
+
+ protected TaskBase(TaskBase base) {
+ for (Field field : TaskBase.class.getDeclaredFields()) {
+ try {
+ field.setAccessible(true);
+ field.set(this, field.get(base));
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ public class Task extends TaskBase {
+ public String name;
+
+ public Task(SubSection s, boolean isVisible, boolean isTrusted) {
+ super(s, isVisible, isTrusted);
+ name = getString(s, KEY_NAME, s.subSection);
+ }
+
+ protected Task(TaskBase base) {
+ super(base);
+ }
+ }
+
+ public class TasksFactory extends TaskBase {
+ public String namesFactory;
+
+ public TasksFactory(SubSection s, boolean isVisible, boolean isTrusted) {
+ super(s, isVisible, isTrusted);
+ namesFactory = getString(s, KEY_NAMES_FACTORY, null);
+ }
+ }
+
+ public class NamesFactory extends Section {
+ public List<String> names;
+ public String type;
+
+ public NamesFactory(SubSection s) {
+ names = getStringList(s, KEY_NAME);
+ type = getString(s, KEY_TYPE, null);
+ }
}
public class External extends Section {
@@ -91,8 +137,10 @@
Pattern.compile("([^ |]*( *[^ |])*) *\\| *");
protected static final String SECTION_EXTERNAL = "external";
+ protected static final String SECTION_NAMES_FACTORY = "names-factory";
protected static final String SECTION_ROOT = "root";
protected static final String SECTION_TASK = "task";
+ protected static final String SECTION_TASKS_FACTORY = "tasks-factory";
protected static final String KEY_APPLICABLE = "applicable";
protected static final String KEY_EXPORT_PREFIX = "export-";
protected static final String KEY_FAIL = "fail";
@@ -100,18 +148,27 @@
protected static final String KEY_FILE = "file";
protected static final String KEY_IN_PROGRESS = "in-progress";
protected static final String KEY_NAME = "name";
+ protected static final String KEY_NAMES_FACTORY = "names-factory";
protected static final String KEY_PASS = "pass";
protected static final String KEY_PRELOAD_TASK = "preload-task";
protected static final String KEY_PROPERTIES_PREFIX = "set-";
protected static final String KEY_READY_HINT = "ready-hint";
protected static final String KEY_SUBTASK = "subtask";
protected static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
+ protected static final String KEY_SUBTASKS_FACTORY = "subtasks-factory";
protected static final String KEY_SUBTASKS_FILE = "subtasks-file";
+ protected static final String KEY_TYPE = "type";
protected static final String KEY_USER = "user";
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(BranchNameKey branch, String fileName, boolean isVisible, boolean isTrusted) {
super(branch, fileName);
this.isVisible = isVisible;
@@ -173,6 +230,14 @@
return getNames(subSection).isEmpty() ? null : new Task(subSection, isVisible, isTrusted);
}
+ public TasksFactory getTasksFactory(String name) {
+ return new TasksFactory(new SubSection(SECTION_TASKS_FACTORY, name), isVisible, isTrusted);
+ }
+
+ public NamesFactory getNamesFactory(String name) {
+ return new NamesFactory(new SubSection(SECTION_NAMES_FACTORY, name));
+ }
+
public External getExternal(String name) {
return getExternal(new SubSection(SECTION_EXTERNAL, name));
}
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 5cbb112..a15e8f5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -24,7 +24,9 @@
import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.task.TaskConfig.External;
+import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import com.googlesource.gerrit.plugins.task.TaskConfig.TasksFactory;
import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
import java.io.IOException;
import java.nio.file.Path;
@@ -131,6 +133,7 @@
protected void addSubDefinitions() {
addSubDefinitions(getSubDefinitions());
+ addSubDefinitions(getTasksFactoryDefinitions());
addSubFileDefinitions();
addExternalDefinitions();
}
@@ -142,7 +145,7 @@
protected void addSubFileDefinitions() {
for (String file : definition.subTasksFiles) {
try {
- addSubDefinitions(getTasks(definition.config.getBranch(), file));
+ addSubDefinitions(getTaskDefinitions(definition.config.getBranch(), file));
} catch (ConfigInvalidException | IOException e) {
nodes.add(null);
}
@@ -179,12 +182,30 @@
return defs;
}
- protected List<Task> getTaskDefinitions(External external)
- throws ConfigInvalidException, IOException {
- return getTasks(resolveUserBranch(external.user), external.file);
+ protected List<Task> getTasksFactoryDefinitions() {
+ List<Task> taskList = new ArrayList<>();
+ for (String taskFactoryName : definition.subTasksFactories) {
+ TasksFactory tasksFactory = definition.config.getTasksFactory(taskFactoryName);
+ if (tasksFactory != null) {
+ NamesFactory namesFactory = definition.config.getNamesFactory(tasksFactory.namesFactory);
+ if (namesFactory != null && "static".equals(namesFactory.type)) {
+ for (String name : namesFactory.names) {
+ taskList.add(definition.config.createTask(tasksFactory, name));
+ }
+ continue;
+ }
+ }
+ taskList.add(null);
+ }
+ return taskList;
}
- protected List<Task> getTasks(BranchNameKey branch, String file)
+ protected List<Task> getTaskDefinitions(External external)
+ throws ConfigInvalidException, IOException {
+ return getTaskDefinitions(resolveUserBranch(external.user), external.file);
+ }
+
+ protected List<Task> getTaskDefinitions(BranchNameKey branch, String file)
throws ConfigInvalidException, IOException {
return taskFactory
.getTaskConfig(branch, resolveTaskFileName(file), definition.isTrusted)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java b/src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java
new file mode 100644
index 0000000..7644143
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2020 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;
+
+public interface ThrowingProvider<V, E extends Exception> {
+ public V get() throws E;
+
+ public static class Entry<V, E extends Exception> implements ThrowingProvider<V, E> {
+ protected V entry;
+
+ public Entry(V entry) {
+ this.entry = entry;
+ }
+
+ @Override
+ public V get() {
+ return entry;
+ }
+ }
+
+ public static class Thrown<V, E extends Exception> implements ThrowingProvider<V, E> {
+ protected E exception;
+
+ public Thrown(E exception) {
+ this.exception = exception;
+ }
+
+ @Override
+ public V get() throws E {
+ throw exception;
+ }
+ }
+}
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 62297c5..9fe4e3d 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -181,6 +181,21 @@
...
```
+`subtasks-factory`
+
+: A subtasks-factory key specifies a task-factory, which generates zero or more
+tasks that are subtasks of the current task. This key may be used several times
+in a task section to reference tasks-factory sections.
+
+Example:
+
+```
+ subtasks-factory = "static tasks factory"
+ ...
+ [tasks-factory "static tasks factory"]
+ ...
+```
+
`subtasks-external`
: This key defines a file containing subtasks of the current task. This
@@ -274,6 +289,69 @@
Backup Optional Subtask {$_name} Backup |
Default Subtask # Must exist if the above two don't!
```
+Tasks-Factory
+-------------
+A tasks-factory section supports all the keys supported by task sections. In
+addition, this section must have a names-factory key which refers to a
+names-factory section. In conjunction with the names-factory, a tasks-factory
+section creates zero or more task definitions that look like regular tasks,
+each with a name provided by the names-factory, and all using the task definition
+set in the tasks-factory.
+
+A tasks-factory section is referenced by a subtasks-factory key in a "task"
+section. A sample task.config which defines a tasks-factory section might look
+like this:
+
+```
+[task "static task list"]
+ subtasks-factory = static tasks factory
+ ...
+
+[tasks-factory "static tasks factory"]
+ names-factory = static names factory list
+ ...
+```
+
+Names-Factory
+-------------
+A names-factory section defines a collection of name keys which are used to
+generate the names for task definitions. The section should contain a "type"
+key that specifies the type.
+
+A names-factory section is referenced by a names-factory key in a "tasks-factory"
+section. A sample task.config which defines a names-factory section might look like
+this:
+
+```
+[names-factory "static names factory list"]
+ name = my a task
+ name = my b task
+ type = static
+```
+
+The following keys may be defined in any names-factory section:
+
+`name`
+
+: This key defines the name of the tasks. This key may be used several times
+in order to define more than one task. The name key can only be used along with
+names-factory of type `static`.
+
+Example:
+```
+ name = my a task
+ name = 12345
+```
+
+`type`
+
+: This key defines the type of the names-factory section. The only
+accepted value is `static`.
+
+Example:
+```
+ type = static
+```
External Entries
----------------
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
index 721032f..58d6ca7 100644
--- a/src/main/resources/Documentation/task_states.md
+++ b/src/main/resources/Documentation/task_states.md
@@ -103,6 +103,16 @@
subtasks-external = user special
subtasks-external = file missing
+[root "Root tasks-factory"]
+ subtasks-factory = tasks-factory static
+
+[root "Root tasks-factory static (empty name)"]
+ subtasks-factory = tasks-factory static (empty name)
+
+[root "Root tasks-factory static (empty name PASS)"]
+ pass = True
+ subtasks-factory = tasks-factory static (empty name)
+
[root "Root Properties"]
set-root-property = root-value
export-root = ${_name}
@@ -129,6 +139,14 @@
applicable = NOT is:open # Assumes test query is "is:open"
subtasks-file = invalids.config
+[tasks-factory "tasks-factory static"]
+ names-factory = names-factory static list
+ fail = True
+
+[tasks-factory "tasks-factory static (empty name)"]
+ names-factory = names-factory static (empty name list)
+ fail = True
+
[task "Subtask APPLICABLE"]
applicable = is:open
pass = True
@@ -237,6 +255,16 @@
[external "file missing"]
user = testuser
file = missing
+
+[names-factory "names-factory static list"]
+ name = my a task
+ name = my b task
+ name = my c task
+ type = static
+
+[names-factory "names-factory static (empty name list)"]
+ type = static
+
```
`task/common.config` file in project `All-Projects` on ref `refs/meta/config`.
@@ -303,6 +331,30 @@
set-A = ${B}
set-B = ${A}
fail = True
+
+[task "task (tasks-factory missing)"]
+ subtasks-factory = missing
+
+[task "task (names-factory type missing)"]
+ subtasks-factory = tasks-factory (names-factory type missing)
+
+[task "task (names-factory type INVALID)"]
+ subtasks-factory = tasks-factory (names-factory type INVALID)
+
+[tasks-factory "tasks-factory (names-factory type missing)"]
+ names-factory = names-factory (type missing)
+ fail = True
+
+[names-factory "names-factory (type missing)"]
+ name = no type test
+
+[tasks-factory "tasks-factory (names-factory type INVALID)"]
+ names-factory = name-factory (type INVALID)
+
+[names-factory "names-factory (type INVALID)"]
+ name = invalid type test
+ type = invalid
+
```
`task/special.config` file in project `All-Users` on ref `refs/users/self`.
@@ -616,6 +668,33 @@
]
},
{
+ "hasPass" : false,
+ "name" : "Root tasks-factory",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "hasPass" : true,
+ "name" : "my a task",
+ "status" : "FAIL"
+ },
+ {
+ "hasPass" : true,
+ "name" : "my b task",
+ "status" : "FAIL"
+ },
+ {
+ "hasPass" : true,
+ "name" : "my c task",
+ "status" : "FAIL"
+ }
+ ]
+ },
+ {
+ "hasPass" : true,
+ "name" : "Root tasks-factory static (empty name PASS)",
+ "status" : "PASS"
+ },
+ {
"exported" : {
"root" : "Root Properties"
},
@@ -828,6 +907,39 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
diff --git a/test/all b/test/all
index 82347ab..6e29127 100644
--- a/test/all
+++ b/test/all
@@ -359,6 +359,43 @@
},
{
"applicable" : true,
+ "hasPass" : false,
+ "name" : "Root tasks-factory",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my a task",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my b task",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my c task",
+ "status" : "FAIL"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root tasks-factory static (empty name)"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root tasks-factory static (empty name PASS)",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
"exported" : {
"root" : "Root Properties"
},
@@ -619,6 +656,42 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
},
@@ -767,6 +840,42 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index 44bb05b..0ed0dcb 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -1,7 +1,6 @@
#!/bin/bash
# Usage:
-# All-Users.git - refs/users/self must already exist
# All-Projects.git - must have 'Push' rights on refs/meta/config
# ---- TEST RESULTS ----
@@ -44,14 +43,18 @@
chmod +x "$hook"
}
-setup_repo() { # repo remote ref
- local repo=$1 remote=$2 ref=$3
+setup_repo() { # repo remote ref [--initial-commit]
+ local repo=$1 remote=$2 ref=$3 init=$4
git init "$repo"
(
cd "$repo"
install_changeid_hook "$repo"
git fetch "$remote" "$ref"
- git checkout FETCH_HEAD
+ if ! git checkout FETCH_HEAD ; then
+ if [ "$init" = "--initial-commit" ] ; then
+ git commit --allow-empty -a -m "Initial Commit"
+ fi
+ fi
)
}
@@ -128,10 +131,11 @@
REF_ALL=refs/meta/config
REF_USERS=refs/users/self
+RESULT=0
mkdir -p "$OUT"
q_setup setup_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
-q_setup setup_repo "$USERS" "$REMOTE_USERS" "$REF_USERS"
+q_setup setup_repo "$USERS" "$REMOTE_USERS" "$REF_USERS" --initial-commit
mkdir -p "$ALL_TASKS" "$USER_TASKS"
@@ -156,3 +160,5 @@
test_file invalid --task--invalid "$query"
test_file invalid-applicable --task--applicable --task--invalid "$query"
+
+exit $RESULT
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
new file mode 100755
index 0000000..634cde4
--- /dev/null
+++ b/test/docker/docker-compose.yaml
@@ -0,0 +1,32 @@
+version: '3'
+services:
+
+ gerrit-01:
+ build:
+ context: gerrit
+ args:
+ - GERRIT_WAR
+ - TASK_PLUGIN_JAR
+ networks:
+ - gerrit-net
+ volumes:
+ - "gerrit-site-etc:/var/gerrit/etc"
+
+ run_tests:
+ build: run_tests
+ networks:
+ - gerrit-net
+ volumes:
+ - "../../:/task:ro"
+ - "gerrit-site-etc:/server-ssh-key:ro"
+ depends_on:
+ - gerrit-01
+ environment:
+ - GERRIT_HOST=gerrit-01
+
+networks:
+ gerrit-net:
+ driver: bridge
+
+volumes:
+ gerrit-site-etc:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
new file mode 100755
index 0000000..041e1dd
--- /dev/null
+++ b/test/docker/gerrit/Dockerfile
@@ -0,0 +1,13 @@
+FROM gerritcodereview/gerrit:3.1.12-ubuntu18
+
+USER root
+
+ENV GERRIT_SITE /var/gerrit
+RUN git config -f "$GERRIT_SITE/etc/gerrit.config" auth.type \
+ DEVELOPMENT_BECOME_ANY_ACCOUNT
+
+COPY artifacts /tmp/
+RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.jar"
+RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
+
+USER gerrit
diff --git a/test/docker/run.sh b/test/docker/run.sh
new file mode 100755
index 0000000..9c1f5d9
--- /dev/null
+++ b/test/docker/run.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+
+readlink --canonicalize / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
+MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+ARTIFACTS=$MYDIR/gerrit/artifacts
+
+die() { echo -e "\nERROR: $@" ; kill $$ ; exit 1 ; } # error_message
+
+progress() { # message cmd [args]...
+ local message=$1 ; shift
+ echo -n "$message"
+ "$@" &
+ local pid=$!
+ while kill -0 $pid 2> /dev/null ; do
+ echo -n "."
+ sleep 2
+ done
+ echo
+ wait "$pid"
+}
+
+usage() { # [error_message]
+ local prog=$(basename -- "$0")
+
+ cat <<-EOF
+Usage:
+ $prog [--task-plugin-jar|-t <FILE_PATH>] [--gerrit-war|-g <FILE_PATH>]
+
+ This tool runs the plugin functional tests in a Docker environment built
+ from the gerritcodereview/gerrit base Docker image.
+
+ The task plugin JAR and optionally a Gerrit WAR are expected to be in the
+ $ARTIFACTS dir;
+ however, the --task-plugin-jar and --gerrit-war switches may be used as
+ helpers to specify which files to copy there.
+
+ Options:
+ --help|-h
+ --gerrit-war|-g path to Gerrit WAR file
+ --task-plugin-jar|-t path to task plugin JAR file
+
+EOF
+
+ [ -n "$1" ] && echo -e "\nERROR: $1" && exit 1
+ exit 0
+}
+
+check_prerequisite() {
+ docker --version > /dev/null || die "docker is not installed"
+ docker-compose --version > /dev/null || die "docker-compose is not installed"
+}
+
+build_images() {
+ docker-compose "${COMPOSE_ARGS[@]}" build --quiet
+}
+
+run_task_plugin_tests() {
+ docker-compose "${COMPOSE_ARGS[@]}" up --detach
+ docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=gerrit_admin run_tests \
+ '/task/test/docker/run_tests/start.sh'
+}
+
+cleanup() {
+ docker-compose "${COMPOSE_ARGS[@]}" down -v --rmi local 2>/dev/null
+}
+
+while (( "$#" )) ; do
+ case "$1" in
+ --help|-h) usage ;;
+ --gerrit-war|-g) shift ; GERRIT_WAR=$1 ;;
+ --task-plugin-jar|-t) shift ; TASK_PLUGIN_JAR=$1 ;;
+ *) usage "invalid argument $1" ;;
+ esac
+ shift
+done
+PROJECT_NAME="task_$$"
+COMPOSE_YAML="$MYDIR/docker-compose.yaml"
+COMPOSE_ARGS=(--project-name "$PROJECT_NAME" -f "$COMPOSE_YAML")
+check_prerequisite
+mkdir -p -- "$ARTIFACTS"
+[ -n "$TASK_PLUGIN_JAR" ] && cp -f "$TASK_PLUGIN_JAR" "$ARTIFACTS/task.jar"
+if [ ! -e "$ARTIFACTS/task.jar" ] ; then
+ MISSING="Missing $ARTIFACTS/task.jar"
+ [ -n "$TASK_PLUGIN_JAR" ] && die "$MISSING, check for copy failure?"
+ usage "$MISSING, did you forget --task-plugin-jar?"
+fi
+[ -n "$GERRIT_WAR" ] && cp -f "$GERRIT_WAR" "$ARTIFACTS/gerrit.war"
+progress "Building docker images" build_images
+run_task_plugin_tests ; RESULT=$?
+cleanup
+
+exit "$RESULT"
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile
new file mode 100755
index 0000000..aaeeb1d
--- /dev/null
+++ b/test/docker/run_tests/Dockerfile
@@ -0,0 +1,26 @@
+FROM alpine:3.11
+
+ARG UID=1000
+ARG GID=1000
+ENV USER gerrit_admin
+ENV USER_HOME /home/$USER
+ENV RUN_TESTS_DIR task/test/docker/run_tests
+ENV WORKSPACE $USER_HOME/workspace
+
+RUN apk --update add --no-cache openssh bash git python2 shadow
+RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config
+
+RUN groupadd -f -g $GID users2
+RUN useradd -u $UID -g $GID $USER
+RUN mkdir -p $WORKSPACE $USER_HOME/.ssh
+RUN chown -R $USER $USER_HOME
+
+USER $USER
+
+RUN ssh-keygen -P '' -f "$USER_HOME"/.ssh/id_rsa
+RUN chmod 400 "$USER_HOME"/.ssh/id_rsa
+RUN chmod 400 "$USER_HOME"/.ssh/id_rsa.pub
+RUN git config --global user.name "Gerrit Admin"
+RUN git config --global user.email "gerrit_admin@localdomain"
+
+ENTRYPOINT ["tail", "-f", "/dev/null"]
diff --git a/test/docker/run_tests/create-test-project-and-changes.sh b/test/docker/run_tests/create-test-project-and-changes.sh
new file mode 100755
index 0000000..159882b
--- /dev/null
+++ b/test/docker/run_tests/create-test-project-and-changes.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+PORT=29418
+
+gssh() { ssh -x -p "$PORT" "$GERRIT_HOST" gerrit "$@" ; } # cmd [args]...
+
+create_project() { # project
+ echo "Creating a test project ..."
+ gssh create-project "$1" --owner "Administrators" --submit-type "MERGE_IF_NECESSARY"
+ cd "$WORKSPACE" && git clone ssh://"$GERRIT_HOST":"$PORT"/"$1" "$1" && cd "$1"
+ gitdir=$(git rev-parse --git-dir)
+ scp -p -P "$PORT" "$USER"@"$GERRIT_HOST":hooks/commit-msg "$gitdir"/hooks/
+}
+
+create_change() { # subject project
+ touch readme.txt && echo "$(date)" >> readme.txt
+ git add . && git commit -m "$1"
+ git push ssh://"$GERRIT_HOST":"$PORT"/"$2" HEAD:refs/for/master
+ commitRevision=$(git rev-parse HEAD)
+}
+
+submit_change() { # commit_revision
+ gssh review --code-review +2 --submit "$1"
+}
+
+create_project 'test'
+create_change 'Change 1' 'test'
+commit1Revision=$commitRevision
+create_change 'Change 2' 'test'
+#sleep to avoid race conditions
+sleep 60
+submit_change "$commit1Revision"
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
new file mode 100755
index 0000000..9b40537
--- /dev/null
+++ b/test/docker/run_tests/start.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR"
+cp -r /task "$USER_HOME"/
+
+./"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$GERRIT_HOST":29418 -t 60 -- echo "gerrit is up"
+
+echo "Creating a default user account ..."
+
+cat "$USER_HOME"/.ssh/id_rsa.pub | ssh -p 29418 -i /server-ssh-key/ssh_host_rsa_key \
+ "Gerrit Code Review@$GERRIT_HOST" suexec --as "admin@example.com" -- gerrit create-account \
+ --ssh-key - --email "gerrit_admin@localdomain" --group "Administrators" "gerrit_admin"
+
+./"$USER_RUN_TESTS_DIR"/create-test-project-and-changes.sh
+./"$USER_RUN_TESTS_DIR"/update-all-users-project.sh
+
+echo "Running Task plugin tests ..."
+cd "$USER_RUN_TESTS_DIR"/../../ && ./check_task_statuses.sh "$GERRIT_HOST"
diff --git a/test/docker/run_tests/update-all-users-project.sh b/test/docker/run_tests/update-all-users-project.sh
new file mode 100755
index 0000000..d0e1527
--- /dev/null
+++ b/test/docker/run_tests/update-all-users-project.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+echo "Updating All-Users project ..."
+
+cd "$WORKSPACE" && git clone ssh://"$GERRIT_HOST":29418/All-Users allusers && cd allusers
+git fetch origin refs/meta/config && git checkout FETCH_HEAD
+git config -f project.config access."refs/users/*".read "group Administrators"
+git config -f project.config access."refs/users/*".push "group Administrators"
+git config -f project.config access."refs/users/*".create "group Administrators"
+git add . && git commit -m "project config update" && git push origin HEAD:refs/meta/config
diff --git a/test/docker/run_tests/wait-for-it.sh b/test/docker/run_tests/wait-for-it.sh
new file mode 100755
index 0000000..d7b6e3c
--- /dev/null
+++ b/test/docker/run_tests/wait-for-it.sh
@@ -0,0 +1,162 @@
+#!/usr/bin/env bash
+# https://github.com/vishnubob/wait-for-it/blob/master/wait-for-it.sh
+# Use this script to test if a given TCP host/port are available
+
+cmdname=$(basename $0)
+
+echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+ cat << USAGE >&2
+Usage:
+ $cmdname host:port [-s] [-t timeout] [-- command args]
+ -h HOST | --host=HOST Host or IP under test
+ -p PORT | --port=PORT TCP port under test
+ Alternatively, you specify the host and port as host:port
+ -s | --strict Only execute subcommand if the test succeeds
+ -q | --quiet Don't output any status messages
+ -t TIMEOUT | --timeout=TIMEOUT
+ Timeout in seconds, zero for no timeout
+ -- COMMAND ARGS Execute command with args after the test finishes
+USAGE
+ exit 1
+}
+
+wait_for()
+{
+ if [[ $TIMEOUT -gt 0 ]]; then
+ echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
+ else
+ echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
+ fi
+ start_ts=$(date +%s)
+ while :
+ do
+ (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1
+ result=$?
+ if [[ $result -eq 0 ]]; then
+ end_ts=$(date +%s)
+ echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds"
+ break
+ fi
+ sleep 1
+ done
+ return $result
+}
+
+wait_for_wrapper()
+{
+ # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+ if [[ $QUIET -eq 1 ]]; then
+ timeout $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+ else
+ timeout $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+ fi
+ PID=$!
+ trap "kill -INT -$PID" INT
+ wait $PID
+ RESULT=$?
+ if [[ $RESULT -ne 0 ]]; then
+ echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT"
+ fi
+ return $RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+ case "$1" in
+ *:* )
+ hostport=(${1//:/ })
+ HOST=${hostport[0]}
+ PORT=${hostport[1]}
+ shift 1
+ ;;
+ --child)
+ CHILD=1
+ shift 1
+ ;;
+ -q | --quiet)
+ QUIET=1
+ shift 1
+ ;;
+ -s | --strict)
+ STRICT=1
+ shift 1
+ ;;
+ -h)
+ HOST="$2"
+ if [[ $HOST == "" ]]; then break; fi
+ shift 2
+ ;;
+ --host=*)
+ HOST="${1#*=}"
+ shift 1
+ ;;
+ -p)
+ PORT="$2"
+ if [[ $PORT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --port=*)
+ PORT="${1#*=}"
+ shift 1
+ ;;
+ -t)
+ TIMEOUT="$2"
+ if [[ $TIMEOUT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --timeout=*)
+ TIMEOUT="${1#*=}"
+ shift 1
+ ;;
+ --)
+ shift
+ CLI="$@"
+ break
+ ;;
+ --help)
+ usage
+ ;;
+ *)
+ echoerr "Unknown argument: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ "$HOST" == "" || "$PORT" == "" ]]; then
+ echoerr "Error: you need to provide a host and port to test."
+ usage
+fi
+
+TIMEOUT=${TIMEOUT:-15}
+STRICT=${STRICT:-0}
+CHILD=${CHILD:-0}
+QUIET=${QUIET:-0}
+
+if [[ $CHILD -gt 0 ]]; then
+ wait_for
+ RESULT=$?
+ exit $RESULT
+else
+ if [[ $TIMEOUT -gt 0 ]]; then
+ wait_for_wrapper
+ RESULT=$?
+ else
+ wait_for
+ RESULT=$?
+ fi
+fi
+
+if [[ $CLI != "" ]]; then
+ if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then
+ echoerr "$cmdname: strict mode, refusing to execute subprocess"
+ exit $RESULT
+ fi
+ exec $CLI
+else
+ exit $RESULT
+fi
diff --git a/test/invalid b/test/invalid
index 840c3da..adab1d1 100644
--- a/test/invalid
+++ b/test/invalid
@@ -179,6 +179,42 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
},
@@ -315,6 +351,42 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
diff --git a/test/invalid-applicable b/test/invalid-applicable
index 8e52fa0..449e595 100644
--- a/test/invalid-applicable
+++ b/test/invalid-applicable
@@ -126,6 +126,39 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
diff --git a/test/preview b/test/preview
index e2706ea..e74d35d 100644
--- a/test/preview
+++ b/test/preview
@@ -135,6 +135,42 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
},
@@ -318,6 +354,42 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
diff --git a/test/preview.invalid b/test/preview.invalid
index 86d12f1..056bb80 100644
--- a/test/preview.invalid
+++ b/test/preview.invalid
@@ -135,6 +135,42 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
},
@@ -271,6 +307,42 @@
{
"name" : "UNKNOWN",
"status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (names-factory type INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
}
]
}
diff --git a/tools/playbooks/install_docker.yaml b/tools/playbooks/install_docker.yaml
new file mode 100644
index 0000000..89cf315
--- /dev/null
+++ b/tools/playbooks/install_docker.yaml
@@ -0,0 +1,8 @@
+- hosts: all
+ roles:
+ - name: ensure-docker
+ tasks:
+ - name: Install compose
+ shell: |
+ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
+ sudo chmod +x /usr/local/bin/docker-compose