| // 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; |
| } |
| } |