blob: 2e3dfa870e805ac149bc8c4fc681f3d992ca0095 [file] [log] [blame]
// 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.
package com.googlesource.gerrit.plugins.task;
import com.google.gerrit.entities.Change;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.access.PluginPermission;
import com.google.gerrit.extensions.common.PluginDefinedInfo;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.DynamicOptions.BeanProvider;
import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
import com.googlesource.gerrit.plugins.task.TaskTree.Node;
import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
public class TaskPluginDefinedInfoFactory implements ChangePluginDefinedInfoFactory {
public static final TaskPath MISSING_VIEW_PATH_CAPABILITY =
new TaskPath(
String.format(
"Can't perform operation, need %s capability", ViewPathsCapability.VIEW_PATHS));
public enum Status {
INVALID,
UNKNOWN,
DUPLICATE,
WAITING,
READY,
PASS,
FAIL;
}
public static class Statistics {
public long numberOfChanges;
public long numberOfChangeNodes;
public long numberOfDuplicates;
public long numberOfNodes;
public long numberOfTaskPluginAttributes;
public Object predicateCache;
public Object matchCache;
public Preloader.Statistics preloader;
public TaskTree.Statistics treeCaches;
}
public static class TaskAttribute {
public static class Statistics {
public boolean isApplicableRefreshRequired;
public boolean isSubNodeReloadRequired;
public boolean isTaskRefreshNeeded;
public Boolean hasUnfilterableSubNodes;
public Object nodesByBranchCache;
public Object properties;
}
public Boolean applicable;
public Map<String, String> exported;
public Boolean hasPass;
public String hint;
public Boolean inProgress;
public TaskPath path;
public String name;
public Integer change;
public Status status;
public List<TaskAttribute> subTasks;
public Long evaluationMilliSeconds;
public Statistics statistics;
public TaskAttribute(String name) {
this.name = name;
}
}
public static class TaskPluginAttribute extends PluginDefinedInfo {
public List<TaskAttribute> roots = new ArrayList<>();
public Statistics queryStatistics;
}
protected final String pluginName;
protected final TaskTree definitions;
protected final PredicateCache predicateCache;
protected final boolean hasViewPathsCapability;
protected final TaskPath.Factory taskPathFactory;
protected final TaskConfigCache taskConfigCache;
protected Modules.MyOptions options;
protected TaskPluginAttribute lastTaskPluginAttribute;
protected Statistics statistics;
@Inject
public TaskPluginDefinedInfoFactory(
String pluginName,
TaskTree.Factory taskTreeFactory,
PredicateCache predicateCache,
PermissionBackend permissionBackend,
TaskPath.Factory taskPathFactory,
TaskConfigCache taskConfigCache) {
this.pluginName = pluginName;
this.definitions = taskTreeFactory.create(taskConfigCache);
this.predicateCache = predicateCache;
this.hasViewPathsCapability =
permissionBackend
.currentUser()
.testOrFalse(new PluginPermission(this.pluginName, ViewPathsCapability.VIEW_PATHS));
this.taskPathFactory = taskPathFactory;
this.taskConfigCache = taskConfigCache;
}
@Override
public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) {
Map<Change.Id, PluginDefinedInfo> pluginInfosByChange = new HashMap<>();
options = (Modules.MyOptions) beanProvider.getDynamicBean(plugin);
if (options.all || options.onlyApplicable || options.onlyInvalid) {
initStatistics();
for (PatchSetArgument psa : options.patchSetArguments) {
taskConfigCache.masquerade(psa);
}
cds.forEach(cd -> pluginInfosByChange.put(cd.getId(), createWithExceptions(cd)));
if (lastTaskPluginAttribute != null) {
lastTaskPluginAttribute.queryStatistics = getStatistics(pluginInfosByChange);
}
}
return pluginInfosByChange;
}
protected PluginDefinedInfo createWithExceptions(ChangeData c) {
TaskPluginAttribute a = new TaskPluginAttribute();
try {
for (Node root : definitions.getRootNodes(c)) {
if (root instanceof Node.Invalid) {
a.roots.add(invalid());
} else {
if (options.shouldFilterRoot(root.task.name())) {
continue;
}
new AttributeFactory(root).create().ifPresent(t -> a.roots.add(t));
}
}
} catch (ConfigInvalidException | IOException | StorageException e) {
a.roots.add(invalid());
}
if (a.roots.isEmpty()) {
return null;
}
lastTaskPluginAttribute = a;
return a;
}
protected class AttributeFactory {
public Node node;
protected Task task;
protected TaskAttribute attribute;
protected AttributeFactory(Node node) {
this.node = node;
this.task = node.task;
attribute = new TaskAttribute(task.name());
if (options.includeStatistics) {
statistics.numberOfNodes++;
if (node.isChange()) {
statistics.numberOfChangeNodes++;
}
if (node.isDuplicate) {
statistics.numberOfDuplicates++;
}
attribute.statistics = new TaskAttribute.Statistics();
attribute.statistics.properties = node.propertiesStatistics;
}
}
public Optional<TaskAttribute> create() {
try {
if (options.evaluationTime) {
attribute.evaluationMilliSeconds = millis();
}
boolean applicable;
try {
applicable = node.match(task.applicable);
} catch (QueryParseException e) {
return Optional.of(invalid());
}
if (!task.isVisible) {
if (!node.isTrusted() || (!applicable && !options.onlyApplicable)) {
return Optional.of(unknown());
}
}
if (applicable || !options.onlyApplicable) {
if (node.isChange()) {
attribute.change = node.getChangeData().getId().get();
}
attribute.hasPass = !node.isDuplicate && (task.pass != null || task.fail != null);
if (!node.isDuplicate) {
attribute.subTasks = getSubTasks();
}
attribute.status = getStatus();
if (options.onlyInvalid && !isValidQueries()) {
attribute.status = Status.INVALID;
}
if (options.includePaths) {
if (hasViewPathsCapability) {
attribute.path = taskPathFactory.create(node.taskKey);
} else {
attribute.path = MISSING_VIEW_PATH_CAPABILITY;
}
}
boolean groupApplicable = attribute.status != null;
if (groupApplicable || !options.onlyApplicable) {
if (!options.onlyInvalid
|| attribute.status == Status.INVALID
|| attribute.subTasks != null) {
if (!options.onlyApplicable) {
attribute.applicable = applicable;
}
if (!node.isDuplicate) {
if (task.inProgress != null) {
attribute.inProgress = node.matchOrNull(task.inProgress);
}
attribute.exported = task.exported.isEmpty() ? null : task.exported;
}
attribute.hint = getHint(attribute.status, task);
if (options.evaluationTime) {
attribute.evaluationMilliSeconds = millis() - attribute.evaluationMilliSeconds;
}
addStatistics(attribute.statistics);
return Optional.of(attribute);
}
}
}
} catch (IOException | RuntimeException | ConfigInvalidException e) {
return Optional.of(invalid()); // bad applicability query
}
return Optional.empty();
}
protected TaskAttribute invalid() {
TaskAttribute invalid = TaskPluginDefinedInfoFactory.invalid();
if (task.isVisible) {
invalid.name = task.name();
}
return invalid;
}
public void addStatistics(TaskAttribute.Statistics statistics) {
if (statistics != null) {
statistics.isApplicableRefreshRequired = node.properties.isApplicableRefreshRequired();
statistics.isSubNodeReloadRequired = node.properties.isSubNodeReloadRequired();
statistics.isTaskRefreshNeeded = node.properties.isTaskRefreshRequired();
if (!statistics.isSubNodeReloadRequired) {
statistics.hasUnfilterableSubNodes = node.hasUnfilterableSubNodes;
}
if (node.nodesByBranch != null) {
statistics.nodesByBranchCache = node.nodesByBranch.getStatistics();
}
}
}
protected Status getStatusWithExceptions() throws StorageException, QueryParseException {
if (node.isDuplicate) {
return Status.DUPLICATE;
}
if (isAllNull(task.pass, task.fail, attribute.subTasks)) {
// A leaf def has no defined subdefs.
boolean hasDefinedSubtasks =
!(task.subTasks.isEmpty()
&& task.subTasksFiles.isEmpty()
&& task.subTasksExternals.isEmpty()
&& task.subTasksFactories.isEmpty());
if (hasDefinedSubtasks) {
// Remove 'Grouping" tasks (tasks with subtasks but no PASS
// or FAIL criteria) from the output if none of their subtasks
// are applicable. i.e. grouping tasks only really apply if at
// least one of their subtasks apply.
return null;
}
// A leaf configuration without a PASS or FAIL criteria is a
// missconfiguration. Either someone forgot to add subtasks, or
// they forgot to add a PASS or FAIL criteria.
return Status.INVALID;
}
if (task.fail != null) {
if (node.match(task.fail)) {
// A FAIL definition is meant to be a hard blocking criteria
// (like a CodeReview -2). Thus, if hard blocked, it is
// irrelevant what the subtask states, or the PASS criteria are.
//
// It is also important that FAIL be useable to indicate that
// the task has actually executed. Thus subtask status,
// including a subtask FAIL should not appear as a FAIL on the
// parent task. This means that this is should be the only path
// to make a task have a FAIL status.
return Status.FAIL;
}
}
if (attribute.subTasks != null
&& !isAll(attribute.subTasks, EnumSet.of(Status.PASS, Status.DUPLICATE))) {
// It is possible for a subtask's PASS criteria to change while
// a parent task is executing, or even after the parent task
// completes. This can result in the parent PASS criteria being
// met while one or more of its subtasks no longer meets its PASS
// criteria (the subtask may now even meet a FAIL criteria). We
// never want the parent task to reflect a PASS criteria in these
// cases, thus we can safely return here without ever evaluating
// the task's PASS criteria.
return Status.WAITING;
}
if (task.pass != null && !node.match(task.pass)) {
// Non-leaf tasks with no PASS criteria are supported in order
// to support "grouping tasks" (tasks with no function aside from
// organizing tasks). A task without a PASS criteria, cannot ever
// be expected to execute (how would you know if it has?), thus a
// pass criteria is required to possibly even be considered for
// READY.
return Status.READY;
}
return Status.PASS;
}
protected Status getStatus() {
try {
return getStatusWithExceptions();
} catch (QueryParseException | RuntimeException e) {
return Status.INVALID;
}
}
protected List<TaskAttribute> getSubTasks()
throws IOException, StorageException, ConfigInvalidException {
List<TaskAttribute> subTasks = new ArrayList<>();
for (Node subNode :
options.onlyApplicable ? node.getApplicableSubNodes() : node.getSubNodes()) {
if (subNode instanceof Node.Invalid) {
subTasks.add(TaskPluginDefinedInfoFactory.invalid());
} else {
new AttributeFactory(subNode).create().ifPresent(t -> subTasks.add(t));
}
}
if (subTasks.isEmpty()) {
return null;
}
return subTasks;
}
protected boolean isValidQueries() {
try {
node.match(task.inProgress);
node.match(task.fail);
node.match(task.pass);
return true;
} catch (QueryParseException | RuntimeException e) {
return false;
}
}
}
protected long millis() {
return System.nanoTime() / 1000000;
}
public void initStatistics() {
if (options.includeStatistics) {
statistics = new Statistics();
definitions.predicateCache.initStatistics(options.summaryCount);
definitions.matchCache.initStatistics(options.summaryCount);
definitions.preloader.initStatistics(options.summaryCount);
definitions.initStatistics(options.summaryCount);
}
}
public Statistics getStatistics(Map<Change.Id, PluginDefinedInfo> pluginInfosByChange) {
if (statistics != null) {
statistics.numberOfChanges = pluginInfosByChange.size();
statistics.numberOfTaskPluginAttributes =
pluginInfosByChange.values().stream().filter(tpa -> tpa != null).count();
statistics.predicateCache = definitions.predicateCache.getStatistics();
statistics.matchCache = definitions.matchCache.getStatistics();
statistics.preloader = definitions.preloader.getStatistics();
statistics.treeCaches = definitions.getStatistics();
}
return statistics;
}
protected static TaskAttribute invalid() {
// For security reasons, do not expose the task name without knowing
// the visibility which is derived from its applicability.
TaskAttribute a = unknown();
a.status = Status.INVALID;
return a;
}
protected static TaskAttribute unknown() {
TaskAttribute a = new TaskAttribute("UNKNOWN");
a.status = Status.UNKNOWN;
return a;
}
protected static String getHint(Status status, Task def) {
if (status != null) {
switch (status) {
case READY:
return def.readyHint;
case FAIL:
return def.failHint;
case DUPLICATE:
return "Duplicate task is non blocking and empty to break the loop";
default:
}
}
return null;
}
public static boolean isAllNull(Object... vals) {
for (Object val : vals) {
if (val != null) {
return false;
}
}
return true;
}
protected static boolean isAll(Iterable<TaskAttribute> atts, Set<Status> states) {
for (TaskAttribute att : atts) {
if (!states.contains(att.status)) {
return false;
}
}
return true;
}
}