| // 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.QueryParseException; |
| import com.google.gerrit.server.query.change.ChangeData; |
| 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.List; |
| import java.util.Map; |
| import java.util.Optional; |
| 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 PredicateCache predicateCache; |
| |
| protected Modules.MyOptions options; |
| |
| @Inject |
| public TaskAttributeFactory(TaskTree definitions, PredicateCache predicateCache) { |
| this.definitions = definitions; |
| this.predicateCache = predicateCache; |
| } |
| |
| @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); |
| } |
| return createWithExceptions(c); |
| } |
| return null; |
| } |
| |
| protected PluginDefinedInfo createWithExceptions(ChangeData c) { |
| TaskPluginAttribute a = new TaskPluginAttribute(); |
| try { |
| for (Node node : definitions.getRootNodes()) { |
| new AttributeFactory(c, node).create().ifPresent(t -> a.roots.add(t)); |
| } |
| } catch (ConfigInvalidException | IOException e) { |
| a.roots.add(invalid()); |
| } |
| |
| if (a.roots.isEmpty()) { |
| return null; |
| } |
| return a; |
| } |
| |
| protected class AttributeFactory { |
| public ChangeData changeData; |
| public Node node; |
| public MatchCache matchCache; |
| protected Task definition; |
| protected TaskAttribute attribute; |
| |
| protected AttributeFactory(ChangeData changeData, Node node) { |
| this(changeData, node, new MatchCache(predicateCache, changeData)); |
| } |
| |
| protected AttributeFactory(ChangeData changeData, Node node, MatchCache matchCache) { |
| this.changeData = changeData; |
| this.node = node; |
| this.matchCache = matchCache; |
| this.definition = node.definition; |
| this.attribute = new TaskAttribute(definition.name); |
| } |
| |
| public Optional<TaskAttribute> create() { |
| try { |
| if (options.evaluationTime) { |
| attribute.evaluationMilliSeconds = millis(); |
| } |
| |
| boolean applicable = matchCache.match(definition.applicable); |
| if (!definition.isVisible) { |
| if (!definition.isTrusted || (!applicable && !options.onlyApplicable)) { |
| return Optional.of(unknown()); |
| } |
| } |
| |
| if (applicable || !options.onlyApplicable) { |
| attribute.hasPass = definition.pass != null || definition.fail != null; |
| attribute.subTasks = getSubTasks(); |
| attribute.status = getStatus(); |
| if (options.onlyInvalid && !isValidQueries()) { |
| attribute.status = Status.INVALID; |
| } |
| 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 (definition.inProgress != null) { |
| attribute.inProgress = matchCache.matchOrNull(definition.inProgress); |
| } |
| attribute.hint = getHint(attribute.status, definition); |
| attribute.exported = definition.exported; |
| |
| if (options.evaluationTime) { |
| attribute.evaluationMilliSeconds = millis() - attribute.evaluationMilliSeconds; |
| } |
| return Optional.of(attribute); |
| } |
| } |
| } |
| } catch (OrmException | QueryParseException | RuntimeException e) { |
| return Optional.of(invalid()); // bad applicability query |
| } |
| return Optional.empty(); |
| } |
| |
| protected Status getStatusWithExceptions() throws OrmException, QueryParseException { |
| if (isAllNull(definition.pass, definition.fail, attribute.subTasks)) { |
| // A leaf def has no defined subdefs. |
| boolean hasDefinedSubtasks = |
| !(definition.subTasks.isEmpty() |
| && definition.subTasksFiles.isEmpty() |
| && definition.subTasksExternals.isEmpty() |
| && definition.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 (definition.fail != null) { |
| if (matchCache.match(definition.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, 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 (definition.pass != null && !matchCache.match(definition.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 (OrmException | QueryParseException | RuntimeException e) { |
| return Status.INVALID; |
| } |
| } |
| |
| protected List<TaskAttribute> getSubTasks() throws OrmException { |
| List<TaskAttribute> subTasks = new ArrayList<>(); |
| for (Node subNode : node.getSubNodes()) { |
| if (subNode == null) { |
| subTasks.add(invalid()); |
| } else { |
| new AttributeFactory(changeData, subNode, matchCache) |
| .create() |
| .ifPresent(t -> subTasks.add(t)); |
| } |
| } |
| if (subTasks.isEmpty()) { |
| return null; |
| } |
| return subTasks; |
| } |
| |
| protected boolean isValidQueries() { |
| try { |
| matchCache.match(definition.inProgress); |
| matchCache.match(definition.fail); |
| matchCache.match(definition.pass); |
| return true; |
| } catch (OrmException | QueryParseException | RuntimeException e) { |
| return false; |
| } |
| } |
| } |
| |
| protected long millis() { |
| return System.nanoTime() / 1000000; |
| } |
| |
| protected 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 TaskAttribute unknown() { |
| TaskAttribute a = new TaskAttribute("UNKNOWN"); |
| a.status = Status.UNKNOWN; |
| return a; |
| } |
| |
| protected String getHint(Status status, Task def) { |
| if (status == Status.READY) { |
| return def.readyHint; |
| } else if (status == Status.FAIL) { |
| return def.failHint; |
| } |
| 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, Status state) { |
| for (TaskAttribute att : atts) { |
| if (att.status != state) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |