blob: ee40d5291bf39a27232626feb18bc5aee7e15f1d [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.common.flogger.FluentLogger;
import com.google.gerrit.extensions.common.PluginDefinedInfo;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryProcessor;
import com.google.gerrit.server.query.change.ChangeQueryProcessor.ChangeAttributeFactory;
import com.google.gwtorm.server.OrmException;
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.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jgit.errors.ConfigInvalidException;
public class TaskAttributeFactory implements ChangeAttributeFactory {
private static final FluentLogger log = FluentLogger.forEnclosingClass();
public enum Status {
INVALID,
UNKNOWN,
WAITING,
READY,
PASS,
FAIL;
}
public static class TaskAttribute {
public Boolean applicable;
public Map<String, String> exported;
public Boolean hasPass;
public String hint;
public Boolean inProgress;
public String name;
public Status status;
public List<TaskAttribute> subTasks;
public Long evaluationMilliSeconds;
public TaskAttribute(String name) {
this.name = name;
}
}
public static class TaskPluginAttribute extends PluginDefinedInfo {
public List<TaskAttribute> roots = new ArrayList<>();
}
protected final TaskTree definitions;
protected final ChangeQueryBuilder cqb;
protected final Map<String, Predicate<ChangeData>> predicatesByQuery = new HashMap<>();
protected Modules.MyOptions options;
@Inject
public TaskAttributeFactory(TaskTree definitions, ChangeQueryBuilder cqb) {
this.definitions = definitions;
this.cqb = cqb;
}
@Override
public PluginDefinedInfo create(ChangeData c, ChangeQueryProcessor qp, String plugin) {
options = (Modules.MyOptions) qp.getDynamicBean(plugin);
if (options.all || options.onlyApplicable || options.onlyInvalid) {
for (PatchSetArgument psa : options.patchSetArguments) {
definitions.masquerade(psa);
}
try {
return createWithExceptions(c);
} catch (OrmException e) {
log.atSevere().withCause(e).log("Cannot load tasks for: %s", c);
}
}
return null;
}
protected PluginDefinedInfo createWithExceptions(ChangeData c) throws OrmException {
TaskPluginAttribute a = new TaskPluginAttribute();
try {
for (Node node : definitions.getRootNodes()) {
addApplicableTasks(a.roots, c, node);
}
} catch (ConfigInvalidException | IOException e) {
a.roots.add(invalid());
}
if (a.roots.isEmpty()) {
return null;
}
return a;
}
protected void addApplicableTasks(List<TaskAttribute> atts, ChangeData c, Node node)
throws OrmException {
try {
Task def = node.definition;
TaskAttribute att = new TaskAttribute(def.name);
if (options.evaluationTime) {
att.evaluationMilliSeconds = millis();
}
boolean applicable = match(c, def.applicable);
if (!def.isVisible) {
if (!def.isTrusted || (!applicable && !options.onlyApplicable)) {
atts.add(unknown());
return;
}
}
if (applicable || !options.onlyApplicable) {
att.hasPass = def.pass != null || def.fail != null;
att.subTasks = getSubTasks(c, node);
att.status = getStatus(c, def, att);
if (options.onlyInvalid && !isValidQueries(c, def)) {
att.status = Status.INVALID;
}
boolean groupApplicable = att.status != null;
if (groupApplicable || !options.onlyApplicable) {
if (!options.onlyInvalid || att.status == Status.INVALID || att.subTasks != null) {
if (!options.onlyApplicable) {
att.applicable = applicable;
}
if (def.inProgress != null) {
att.inProgress = matchOrNull(c, def.inProgress);
}
att.hint = getHint(att.status, def);
att.exported = def.exported;
if (options.evaluationTime) {
att.evaluationMilliSeconds = millis() - att.evaluationMilliSeconds;
}
atts.add(att);
}
}
}
} catch (QueryParseException e) {
atts.add(invalid()); // bad applicability query
}
}
protected long millis() {
return System.nanoTime() / 1000000;
}
protected List<TaskAttribute> getSubTasks(ChangeData c, Node node) throws OrmException {
List<TaskAttribute> subTasks = new ArrayList<>();
for (Node subNode : node.getSubNodes()) {
if (subNode == null) {
subTasks.add(invalid());
} else {
addApplicableTasks(subTasks, c, subNode);
}
}
if (subTasks.isEmpty()) {
return null;
}
return subTasks;
}
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 boolean isValidQueries(ChangeData c, Task def) {
try {
match(c, def.inProgress);
match(c, def.fail);
match(c, def.pass);
return true;
} catch (OrmException | QueryParseException e) {
return false;
}
}
protected Status getStatus(ChangeData c, Task def, TaskAttribute a) throws OrmException {
try {
return getStatusWithExceptions(c, def, a);
} catch (QueryParseException e) {
return Status.INVALID;
}
}
protected Status getStatusWithExceptions(ChangeData c, Task def, TaskAttribute a)
throws OrmException, QueryParseException {
if (isAllNull(def.pass, def.fail, a.subTasks)) {
// A leaf def has no defined subdefs.
boolean hasDefinedSubtasks =
!(def.subTasks.isEmpty()
&& def.subTasksFiles.isEmpty()
&& def.subTasksExternals.isEmpty());
if (hasDefinedSubtasks) {
// Remove 'Grouping" tasks (tasks with subtasks but no PASS
// or FAIL criteria) from the output if none of their subtasks
// 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 (def.fail != null) {
if (match(c, def.fail)) {
// A FAIL definition is meant to be a hard blocking criteria
// (like a CodeReview -2). Thus, if hard blocked, it is
// irrelevant what the subtask states, or the PASS criteria are.
//
// 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 (def.pass == null) {
// A task with a FAIL but no PASS criteria is a PASS-FAIL task
// (they are never "READY"). It didn't fail, so pass.
return Status.PASS;
}
}
if (a.subTasks != null && !isAll(a.subTasks, Status.PASS)) {
// 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 (def.pass != null && !match(c, def.pass)) {
// Non-leaf tasks with no PASS criteria are supported in order
// to support "grouping tasks" (tasks with no function aside from
// organizing tasks). A task without a PASS criteria, cannot ever
// 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 String getHint(Status status, Task def) {
if (status == Status.READY) {
return def.readyHint;
} else if (status == Status.FAIL) {
return def.failHint;
}
return null;
}
protected static boolean isAll(Iterable<TaskAttribute> atts, Status state) {
for (TaskAttribute att : atts) {
if (att.status != state) {
return false;
}
}
return true;
}
protected boolean match(ChangeData c, String query) throws OrmException, QueryParseException {
if (query == null || query.equalsIgnoreCase("true")) {
return true;
}
Predicate<ChangeData> pred = predicatesByQuery.get(query);
if (pred == null) {
pred = cqb.parse(query);
predicatesByQuery.put(query, pred);
}
return pred.asMatchable().match(c);
}
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 (OrmException | QueryParseException e) {
}
}
return null;
}
protected static boolean isAllNull(Object... vals) {
for (Object val : vals) {
if (val != null) {
return false;
}
}
return true;
}
}