Merge "Adapt to change in Gerrit core"
diff --git a/gr-checks/gr-checkers-list.html b/gr-checks/gr-checkers-list.html
index 36e8f85..6c62e4d 100644
--- a/gr-checks/gr-checkers-list.html
+++ b/gr-checks/gr-checkers-list.html
@@ -121,7 +121,7 @@
</template>
</nav>
- <gr-overlay id="createOverlay" with-backdrop>
+ <gr-overlay id="createOverlay">
<gr-dialog
id="createDialog"
confirm-label="Create"
@@ -138,7 +138,7 @@
</div>
</gr-dialog>
</gr-overlay>
- <gr-overlay id="editOverlay" with-backdrop>
+ <gr-overlay id="editOverlay">
<gr-dialog
id="editDialog"
confirm-label="Save"
diff --git a/gr-checks/gr-checkers-list.js b/gr-checks/gr-checkers-list.js
index 2542832..4914274 100644
--- a/gr-checks/gr-checkers-list.js
+++ b/gr-checks/gr-checkers-list.js
@@ -9,7 +9,15 @@
Polymer({
is: 'gr-checkers-list',
properties: {
- pluginRestApi: Object,
+ /**
+ * Add observer on pluginRestApi to call getCheckers when it's defined
+ * as initially getCheckers was being called before pluginRestApi was
+ * initialised by gr-checks-view
+ */
+ pluginRestApi: {
+ type: Object,
+ observer: '_getCheckers'
+ },
// Checker that will be passed to the editOverlay modal
checker: Object,
_checkers: Array,
@@ -51,10 +59,6 @@
'_showCheckers(_checkers, _filter)',
],
- attached() {
- this._getCheckers();
- },
-
_contains(target, keyword) {
return target.toLowerCase().includes(keyword.toLowerCase().trim());
},
@@ -117,11 +121,9 @@
}
},
- _getCheckers() {
- this.pluginRestApi.fetchJSON({
- method: 'GET',
- url: GET_CHECKERS_URL,
- }).then(checkers => {
+ _getCheckers(pluginRestApi) {
+ if (!pluginRestApi) return;
+ pluginRestApi.get(GET_CHECKERS_URL).then(checkers => {
if (!checkers) { return; }
this._checkers = checkers;
this._startingIndex = 0;
diff --git a/gr-checks/gr-checkers-list_test.html b/gr-checks/gr-checkers-list_test.html
new file mode 100644
index 0000000..6b5e0ad
--- /dev/null
+++ b/gr-checks/gr-checkers-list_test.html
@@ -0,0 +1,244 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="/bower_components/polymer/polymer.html">
+
+<title>gr-checkers-list-view</title>
+<link rel="import" href="gr-checkers-list.html">
+
+<test-fixture id="basic">
+ <template is="dom-template">
+ <gr-checkers-list
+ plugin-rest-api="[[pluginRestApi]]"
+ >
+ </gr-checkers-list>
+</template>
+</test-fixture>
+
+<script>
+ const CHECKERS = [
+ {
+ "uuid":"C:D",
+ "name":"A",
+ "description":"B",
+ "repository":"Backend",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-25 13:08:43.000000000",
+ "updated":"2019-07-25 13:08:43.000000000"
+ },
+ {
+ "uuid":"aa:bb",
+ "name":"n1",
+ "description":"d1",
+ "repository":"All-Users",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-29 13:07:17.000000000",
+ "updated":"2019-07-29 13:07:17.000000000"
+ },
+ {
+ "uuid":"adsf:asdasdas",
+ "name":"ds",
+ "description":"s",
+ "repository":"Scripts",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-29 13:28:09.000000000",
+ "updated":"2019-07-29 13:28:09.000000000"
+ },
+ {
+ "uuid":"ijkl:mnop",
+ "name":"abcd",
+ "description":"efgh",
+ "repository":"All-Projects",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-29 09:33:25.000000000",
+ "updated":"2019-07-29 09:33:25.000000000"
+ },
+ {
+ "uuid":"ngfnf:mhghgnhghn",
+ "name":"nbvfg",
+ "description":"fjhgj",
+ "repository":"All-Users",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-08-06 14:21:34.000000000",
+ "updated":"2019-08-06 14:21:34.000000000"
+ },
+ {
+ "uuid":"sdfsdf--:sdfsdf333",
+ "name":"sdfsdf",
+ "description":"sdfsdfsd",
+ "repository":"Scripts",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-30 13:00:19.000000000",
+ "updated":"2019-07-30 13:00:19.000000000"
+ },
+ {
+ "uuid":"test:checker1",
+ "name":"Unit Tests",
+ "description":"Random description that should be improved at some point",
+ "repository":"Backend",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-22 13:16:52.000000000",
+ "updated":"2019-07-22 14:21:14.000000000"
+ },
+ {
+ "uuid":"test:checker2",
+ "name":"Code Style",
+ "repository":"Backend",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-22 13:26:56.000000000",
+ "updated":"2019-07-22 13:26:56.000000000"
+ },
+ {
+ "uuid":"xddf:sdfsdfsdf",
+ "name":"sdfsdf",
+ "description":"sdfsdf",
+ "repository":"Scripts",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-29 14:11:59.000000000",
+ "updated":"2019-07-29 14:11:59.000000000"
+ },
+ {
+ "uuid":"zxczxc:bnvnbvnbvn",
+ "name":"zxc",
+ "description":"zxc",
+ "repository":"Scripts",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-29 14:00:24.000000000",
+ "updated":"2019-07-29 14:00:24.000000000"
+ },
+ {
+ "uuid":"zxczxc:sdfsdf",
+ "name":"zxc",
+ "description":"zxc",
+ "repository":"Scripts",
+ "status":"ENABLED",
+ "blocking":[
+
+ ],
+ "query":"status:open",
+ "created":"2019-07-29 13:30:47.000000000",
+ "updated":"2019-07-29 13:30:47.000000000"
+ }
+ ];
+</script>
+
+<script>
+
+ suite('gr-checkers-list tests', () => {
+ let element;
+ let sandbox;
+ let fetchJSONSpy, fetchJSONPromise, fetchJSONResolve;
+
+ setup((done) => {
+ sandbox = sinon.sandbox.create();
+
+ fetchJSONSpy = sinon.stub();
+ const fetchJSONPromise = new Promise((resolve, reject) => {
+ fetchJSONResolve = resolve;
+ })
+ fetchJSONSpy.returns(fetchJSONPromise)
+
+ const pluginRestApi = {
+ fetchJSON: fetchJSONSpy,
+ };
+
+ element = fixture('basic', {
+ pluginRestApi
+ });
+ flush(done);
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('renders checker list table headings', () => {
+ const checkersList = element.$$('table')
+ const headings = checkersList.firstElementChild.firstElementChild.
+ children;
+ const expectedHeadings = ["Checker Name", "Repository", "Status",
+ "Required", "Checker Description", "Edit"];
+ for (let i = 0;i < headings.length;i++) {
+ assert(headings[i].innerText === expectedHeadings[i]);
+ }
+ });
+
+ test('create checker button renders', () => {
+ assert(element.querySelector('#createNewContainer'));
+ const button = element.querySelector('#createNewContainer')
+ .querySelector('gr-button');
+ assert(button);
+ assert(button.innerText === 'Create New');
+ });
+
+ suite('with checkers', () => {
+ setup((done) => {
+ fetchJSONResolve(CHECKERS);
+ flush(done);
+ })
+ test('renders correct number of checkers', () => {
+ const checkers = element.$$('table > tbody:nth-child(2)')
+ .querySelectorAll('tr');
+ assert(checkers.length === CHECKERS.length);
+ })
+ test('renders correct checkers', () => {
+ const checkers = element.$$('table > tbody:nth-child(2)')
+ .querySelectorAll('tr');
+ for (let i = 0;i < checkers.length;i++) {
+ const checkerDetails = checkers[i].querySelectorAll('td');
+ assert(CHECKERS[i].name === checkerDetails[0].innerText);
+ assert(CHECKERS[i].repository === checkerDetails[1].innerText);
+ const status = CHECKERS[i].status || "NO";
+ assert(status === checkerDetails[2].innerText);
+ const checkerRequired = (CHECKERS[i].blocking &&
+ CHECKERS[i].blocking.length > 0)
+ ? "YES": "NO";
+ assert(checkerRequired === checkerDetails[3].innerText);
+ const description = CHECKERS[i].description || '';
+ assert(description === checkerDetails[4].innerText);
+ }
+ })
+ });
+
+ });
+</script>
diff --git a/java/com/google/gerrit/plugins/checks/CheckerQuery.java b/java/com/google/gerrit/plugins/checks/CheckerQuery.java
index 71f7ff2..accab2a 100644
--- a/java/com/google/gerrit/plugins/checks/CheckerQuery.java
+++ b/java/com/google/gerrit/plugins/checks/CheckerQuery.java
@@ -25,6 +25,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -45,9 +46,11 @@
import com.google.gerrit.server.update.RetryHelper.ActionType;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import org.antlr.runtime.tree.Tree;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -234,6 +237,24 @@
return query;
}
+ public List<List<ChangeData>> queryMatchingChanges(List<Checker> checkers)
+ throws ConfigInvalidException {
+
+ try {
+ List<Predicate<ChangeData>> predicateList = new ArrayList<>();
+ for (Checker checker : checkers) {
+ predicateList.add(
+ createQueryPredicate(checker.getUuid(), checker.getRepository(), checker.getQuery()));
+ }
+ return executeIndexQueryWithRetry(qp -> {}, predicateList);
+ } catch (QueryParseException e) {
+ throw new ConfigInvalidException(
+ String.format(
+ "A checker in scheme %s has an invalid query (%s)",
+ checkers.get(0).getUuid().scheme(), e.getMessage()));
+ }
+ }
+
public List<ChangeData> queryMatchingChanges(Checker checker)
throws ConfigInvalidException, StorageException {
return queryMatchingChanges(
@@ -300,13 +321,21 @@
private List<ChangeData> executeIndexQueryWithRetry(
Consumer<ChangeQueryProcessor> queryProcessorSetup, Predicate<ChangeData> predicate)
throws StorageException, QueryParseException {
+ return executeIndexQueryWithRetry(queryProcessorSetup, ImmutableList.of(predicate)).get(0);
+ }
+
+ private List<List<ChangeData>> executeIndexQueryWithRetry(
+ Consumer<ChangeQueryProcessor> queryProcessorSetup, List<Predicate<ChangeData>> predicateList)
+ throws StorageException, QueryParseException {
try {
return retryHelper.execute(
ActionType.INDEX_QUERY,
() -> {
ChangeQueryProcessor qp = changeQueryProcessorProvider.get();
queryProcessorSetup.accept(qp);
- return qp.query(predicate).entities();
+ return qp.query(predicateList).stream()
+ .map(predicate -> predicate.entities())
+ .collect(Collectors.toList());
},
StorageException.class::isInstance);
} catch (Exception e) {
diff --git a/java/com/google/gerrit/plugins/checks/Checkers.java b/java/com/google/gerrit/plugins/checks/Checkers.java
index 9c00528..d08f1f1 100644
--- a/java/com/google/gerrit/plugins/checks/Checkers.java
+++ b/java/com/google/gerrit/plugins/checks/Checkers.java
@@ -73,6 +73,16 @@
ImmutableList<Checker> listCheckers() throws IOException;
/**
+ * Returns a list with all checkers of the given scheme.
+ *
+ * <p>Checkers with invalid configuration are silently ignored.
+ *
+ * @param scheme the name of the scheme of the relevant checkers
+ * @return all checkers with that scheme, sorted by UUID
+ * @throws IOException if any checker couldn't be retrieved from the storage
+ */
+ ImmutableList<Checker> listCheckers(String scheme) throws IOException;
+ /**
* Returns the checkers that apply to the given repository.
*
* <p>Never returns disabled checkers. Checkers with invalid configuration are silently ignored.
diff --git a/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java b/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
index 7c0647e..f78d143 100644
--- a/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
@@ -14,12 +14,12 @@
package com.google.gerrit.plugins.checks.api;
-import static com.google.common.base.Preconditions.checkState;
-
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
@@ -35,9 +35,11 @@
import com.google.gerrit.plugins.checks.Checkers;
import com.google.gerrit.plugins.checks.Checks;
import com.google.gerrit.plugins.checks.Checks.GetCheckOptions;
+import com.google.gerrit.plugins.checks.index.CheckPredicate;
import com.google.gerrit.plugins.checks.index.CheckQueryBuilder;
import com.google.gerrit.plugins.checks.index.CheckStatePredicate;
import com.google.gerrit.plugins.checks.index.CheckerPredicate;
+import com.google.gerrit.plugins.checks.index.CheckerSchemePredicate;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.query.change.ChangeData;
@@ -55,7 +57,6 @@
private final Checkers checkers;
private final Checks checks;
private final Provider<CheckerQuery> checkerQueryProvider;
-
private String queryString;
@Option(
@@ -91,21 +92,53 @@
throw new BadRequestException("query is required");
}
- Predicate<Check> query = validateQuery(parseQuery(queryString));
- if (!hasStatePredicate(query)) {
- query = Predicate.and(new CheckStatePredicate(CheckState.NOT_STARTED), query);
+ Predicate<Check> predicate = validateQuery(parseQuery(queryString));
+ if (!hasStatePredicate(predicate)) {
+ predicate = Predicate.and(new CheckStatePredicate(CheckState.NOT_STARTED), predicate);
}
+ // this variable is for the lambda expressions when using orElseThrow
+ final Predicate<Check> finalPredicate = predicate;
- Optional<Checker> checker = checkers.getChecker(getCheckerUuidFromQuery(query));
- if (!checker.isPresent() || checker.get().isDisabled()) {
- return Response.ok(ImmutableList.of());
+ if (countPredicates(predicate, CheckerPredicate.class) == 1) {
+ // Checker query
+ Optional<Checker> checker =
+ checkers.getChecker(
+ getCheckerUuidFromQuery(predicate)
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ String.format("no checker predicate found: %s", finalPredicate))));
+ if (!checker.isPresent() || checker.get().isDisabled()) {
+ return Response.ok(ImmutableList.of());
+ }
+ List<ChangeData> changes = checkerQueryProvider.get().queryMatchingChanges(checker.get());
+ return Response.ok(getPendingChecksOfChecker(checker.get(), predicate, changes));
}
+ // Scheme query
+ String scheme =
+ getSchemeFromQuery(predicate)
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ String.format("no checker scheme predicate found: %s", finalPredicate)));
+ ImmutableList<Checker> checkersOfScheme = checkers.listCheckers(scheme);
+ List<List<ChangeData>> changes =
+ checkerQueryProvider.get().queryMatchingChanges(checkersOfScheme);
+ List<PendingChecksInfo> pendingChecks = new ArrayList<>();
+ for (int i = 0; i < changes.size(); i++) {
+ pendingChecks.addAll(
+ getPendingChecksOfChecker(checkersOfScheme.get(i), predicate, changes.get(i)));
+ }
+ return Response.ok(pendingChecks);
+ }
+
+ private List<PendingChecksInfo> getPendingChecksOfChecker(
+ Checker checker, Predicate<Check> query, List<ChangeData> changes) throws IOException {
// The query system can only match against the current patch set; ignore non-current patch sets
// for now.
- List<ChangeData> changes = checkerQueryProvider.get().queryMatchingChanges(checker.get());
- CheckerUuid checkerUuid = checker.get().getUuid();
- List<PendingChecksInfo> pendingChecks = new ArrayList<>(changes.size());
+ List<PendingChecksInfo> pendingChecks = new ArrayList<>();
+ CheckerUuid checkerUuid = checker.getUuid();
for (ChangeData cd : changes) {
PatchSet patchSet = cd.currentPatchSet();
CheckKey checkKey = CheckKey.create(cd.project(), patchSet.id(), checkerUuid);
@@ -117,13 +150,13 @@
Check check =
checks
.getCheck(checkKey, GetCheckOptions.defaults())
- .orElseGet(() -> Check.newBackfilledCheck(cd.project(), patchSet, checker.get()));
+ .orElseGet(() -> Check.newBackfilledCheck(cd.project(), patchSet, checker));
if (query.asMatchable().match(check)) {
pendingChecks.add(createPendingChecksInfo(cd.project(), patchSet, checkerUuid, check));
}
}
- return Response.ok(pendingChecks);
+ return pendingChecks;
}
private Predicate<Check> parseQuery(String query) throws BadRequestException {
@@ -136,28 +169,36 @@
private static Predicate<Check> validateQuery(Predicate<Check> predicate)
throws BadRequestException {
- if (countCheckerPredicates(predicate) != 1)
+ int numCheckPredicates = countPredicates(predicate, CheckerPredicate.class);
+ int numSchemePredicates = countPredicates(predicate, CheckerSchemePredicate.class);
+ String exceptionMessage =
+ String.format(
+ "query must be '%s:<checker-uuid>' or '%s:<checker-uuid> AND <other-operators>' or '%s:<checker-scheme>' or '%s:<checker-scheme> AND <other-operators>'",
+ CheckQueryBuilder.FIELD_CHECKER,
+ CheckQueryBuilder.FIELD_CHECKER,
+ CheckQueryBuilder.FIELD_SCHEME,
+ CheckQueryBuilder.FIELD_SCHEME);
+ if (numCheckPredicates + numSchemePredicates != 1) {
throw new BadRequestException(
String.format(
- "query must contain exactly 1 '%s' operator", CheckQueryBuilder.FIELD_CHECKER));
+ "query must contain exactly 1 '%s' operator or '%s' operator",
+ CheckQueryBuilder.FIELD_CHECKER, CheckQueryBuilder.FIELD_SCHEME));
+ }
// the root predicate must either be an AndPredicate ....
if (predicate instanceof AndPredicate) {
// if the root predicate is an AndPredicate, any of its direct children must be a
- // CheckerPredicate, the other child predicates can be anything (including any combination of
+ // CheckerPredicate or CheckerSchemePredicate, the other child predicates can be anything
+ // (including any combination of
// AndPredicate, OrPredicate and NotPredicate).
- if (predicate.getChildren().stream().noneMatch(CheckerPredicate.class::isInstance)) {
- throw new BadRequestException(
- String.format(
- "query must be '%s:<checker-uuid>' or '%s:<checker-uuid> AND <other-operators>'",
- CheckQueryBuilder.FIELD_CHECKER, CheckQueryBuilder.FIELD_CHECKER));
+ if (predicate.getChildren().stream().noneMatch(CheckerPredicate.class::isInstance)
+ && predicate.getChildren().stream().noneMatch(CheckerSchemePredicate.class::isInstance)) {
+ throw new BadRequestException(exceptionMessage);
}
- // ... or a CheckerPredicate
- } else if (!(predicate instanceof CheckerPredicate)) {
- throw new BadRequestException(
- String.format(
- "query must be '%s:<checker-uuid>' or '%s:<checker-uuid> AND <other-operators>'",
- CheckQueryBuilder.FIELD_CHECKER, CheckQueryBuilder.FIELD_CHECKER));
+ // ... or a CheckerPredicate / CheckerSchemePredicate
+ } else if (!(predicate instanceof CheckerPredicate
+ || predicate instanceof CheckerSchemePredicate)) {
+ throw new BadRequestException(exceptionMessage);
}
return predicate;
}
@@ -178,40 +219,54 @@
* <p>This method doesn't validate that the checker predicates appear in any particular location.
*
* @param predicate the predicate in which the checker predicates should be counted
+ * @param predicateClass the class of the predicate (schema or checker)
* @return the number of checker predicates in the given predicate
*/
- private static int countCheckerPredicates(Predicate<Check> predicate) {
- if (predicate instanceof CheckerPredicate) {
+ private static int countPredicates(
+ Predicate<Check> predicate, Class<? extends CheckPredicate> predicateClass) {
+ if (predicateClass.isInstance(predicate)) {
return 1;
}
if (predicate.getChildCount() == 0) {
return 0;
}
return predicate.getChildren().stream()
- .mapToInt(QueryPendingChecks::countCheckerPredicates)
+ .mapToInt(p -> QueryPendingChecks.countPredicates(p, predicateClass))
.sum();
}
- private static CheckerUuid getCheckerUuidFromQuery(Predicate<Check> predicate) {
+ private static Optional<CheckerUuid> getCheckerUuidFromQuery(Predicate<Check> predicate) {
// the query validation (see #validateQuery(Predicate<Check>)) ensures that there is exactly 1
- // CheckerPredicate and that it is on the first or second level of the predicate tree.
+ // CheckerPredicate or CheckerSchemePredicate and that it is on the first or second level of the
+ // predicate tree.
if (predicate instanceof CheckerPredicate) {
- return ((CheckerPredicate) predicate).getCheckerUuid();
+ return Optional.of(((CheckerPredicate) predicate).getCheckerUuid());
}
- checkState(predicate.getChildCount() > 0, "no checker predicate found: %s", predicate);
Optional<CheckerPredicate> checkerPredicate =
predicate.getChildren().stream()
.filter(CheckerPredicate.class::isInstance)
.map(p -> (CheckerPredicate) p)
.findAny();
- return checkerPredicate
- .map(CheckerPredicate::getCheckerUuid)
- .orElseThrow(
- () ->
- new IllegalStateException(
- String.format("no checker predicate found: %s", predicate)));
+ return checkerPredicate.map(CheckerPredicate::getCheckerUuid);
+ }
+
+ private static Optional<String> getSchemeFromQuery(Predicate<Check> predicate) {
+ // the query validation (see #validateQuery(Predicate<Check>)) ensures that there is exactly 1
+ // CheckerPredicate or CheckerSchemePredicate and that it is on the first or second level of the
+ // predicate tree.
+
+ if (predicate instanceof CheckerSchemePredicate) {
+ return Optional.of(((CheckerSchemePredicate) predicate).getCheckerScheme());
+ }
+
+ Optional<CheckerSchemePredicate> checkerSchemePredicate =
+ predicate.getChildren().stream()
+ .filter(CheckerSchemePredicate.class::isInstance)
+ .map(p -> (CheckerSchemePredicate) p)
+ .findAny();
+ return checkerSchemePredicate.map(CheckerSchemePredicate::getCheckerScheme);
}
private static PendingChecksInfo createPendingChecksInfo(
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java
index be8bd8e..717a793 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java
@@ -62,8 +62,17 @@
@Override
public ImmutableList<Checker> listCheckers() throws IOException {
+ return listCheckers("");
+ }
+
+ @Override
+ public ImmutableList<Checker> listCheckers(String scheme) throws IOException {
+ if (scheme != null && !scheme.isEmpty() && !scheme.endsWith("/")) {
+ scheme = scheme + "/";
+ }
try (Repository allProjectsRepo = repoManager.openRepository(allProjectsName)) {
- return allProjectsRepo.getRefDatabase().getRefsByPrefix(CheckerRef.REFS_CHECKERS).stream()
+ return allProjectsRepo.getRefDatabase().getRefsByPrefix(CheckerRef.REFS_CHECKERS + scheme)
+ .stream()
.flatMap(ref -> Streams.stream(tryLoadChecker(allProjectsRepo, ref)))
.sorted(comparing(Checker::getUuid))
.collect(toImmutableList());
diff --git a/java/com/google/gerrit/plugins/checks/index/CheckQueryBuilder.java b/java/com/google/gerrit/plugins/checks/index/CheckQueryBuilder.java
index ff6d1f5..e970e86 100644
--- a/java/com/google/gerrit/plugins/checks/index/CheckQueryBuilder.java
+++ b/java/com/google/gerrit/plugins/checks/index/CheckQueryBuilder.java
@@ -27,6 +27,7 @@
public class CheckQueryBuilder extends QueryBuilder<Check, CheckQueryBuilder> {
public static final String FIELD_CHECKER = "checker";
public static final String FIELD_STATE = "state";
+ public static final String FIELD_SCHEME = "scheme";
private static final QueryBuilder.Definition<Check, CheckQueryBuilder> mydef =
new QueryBuilder.Definition<>(CheckQueryBuilder.class);
@@ -60,4 +61,9 @@
public Predicate<Check> state(String state) throws QueryParseException {
return CheckStatePredicate.parse(state);
}
+
+ @Operator
+ public Predicate<Check> scheme(String scheme) {
+ return new CheckerSchemePredicate(scheme);
+ }
}
diff --git a/java/com/google/gerrit/plugins/checks/index/CheckerSchemePredicate.java b/java/com/google/gerrit/plugins/checks/index/CheckerSchemePredicate.java
new file mode 100644
index 0000000..d2eea3a
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/index/CheckerSchemePredicate.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2019 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.google.gerrit.plugins.checks.index;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.plugins.checks.Check;
+
+public class CheckerSchemePredicate extends CheckPredicate {
+ private final String checkerScheme;
+
+ public CheckerSchemePredicate(String checkerScheme) {
+ super(CheckQueryBuilder.FIELD_SCHEME, checkerScheme);
+ this.checkerScheme = requireNonNull(checkerScheme, "checkerScheme");
+ }
+
+ @Override
+ public boolean match(Check check) throws StorageException {
+ return checkerScheme.equals(check.key().checkerUuid().scheme());
+ }
+
+ public String getCheckerScheme() {
+ return checkerScheme;
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/QueryPendingChecksIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/QueryPendingChecksIT.java
index 2727203..d0656ac 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/QueryPendingChecksIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/QueryPendingChecksIT.java
@@ -27,6 +27,7 @@
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.plugins.checks.CheckKey;
import com.google.gerrit.plugins.checks.CheckerUuid;
@@ -35,6 +36,7 @@
import com.google.gerrit.plugins.checks.api.CheckState;
import com.google.gerrit.plugins.checks.api.PendingCheckInfo;
import com.google.gerrit.plugins.checks.api.PendingChecksInfo;
+import com.google.gerrit.plugins.checks.api.QueryPendingChecks;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.testing.TestTimeUtil;
import com.google.gson.reflect.TypeToken;
@@ -85,8 +87,10 @@
}
@Test
- public void specifyingCheckerIsRequired() throws Exception {
- assertInvalidQuery("state:NOT_STARTED", "query must contain exactly 1 'checker' operator");
+ public void specifyingCheckerOrSchemeIsRequired() throws Exception {
+ assertInvalidQuery(
+ "state:NOT_STARTED",
+ "query must contain exactly 1 'checker' operator or 'scheme' operator");
}
@Test
@@ -97,11 +101,11 @@
}
@Test
- public void cannotSpecifyingMultipleCheckers() throws Exception {
+ public void cannotSpecifyMultipleCheckers() throws Exception {
CheckerUuid checkerUuid1 = checkerOperations.newChecker().repository(project).create();
CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
- String expectedMessage = "query must contain exactly 1 'checker' operator";
+ String expectedMessage = "query must contain exactly 1 'checker' operator or 'scheme' operator";
assertInvalidQuery(
String.format("checker:\"%s\" checker:\"%s\"", checkerUuid1, checkerUuid2),
expectedMessage);
@@ -115,6 +119,26 @@
}
@Test
+ public void cannotSpecifyMultipleSchemes() throws Exception {
+ CheckerUuid checkerUuid1 = checkerOperations.newChecker().repository(project).create();
+ CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
+
+ String expectedMessage = "query must contain exactly 1 'checker' operator or 'scheme' operator";
+ assertInvalidQuery(
+ String.format("scheme:\"%s\" scheme:\"%s\"", checkerUuid1.scheme(), checkerUuid2.scheme()),
+ expectedMessage);
+ assertInvalidQuery(
+ String.format(
+ "scheme:\"%s\" OR scheme:\"%s\"", checkerUuid1.scheme(), checkerUuid2.scheme()),
+ expectedMessage);
+ assertInvalidQuery(
+ String.format(
+ "scheme:\"%s\" (state:NOT_STARTED scheme:\"%s\")",
+ checkerUuid1.scheme(), checkerUuid2.scheme()),
+ expectedMessage);
+ }
+
+ @Test
public void canSpecifyCheckerAsRootPredicate() throws Exception {
CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
assertThat(queryPendingChecks(String.format("checker:\"%s\"", checkerUuid))).hasSize(1);
@@ -133,7 +157,7 @@
CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
String expectedMessage =
- "query must be 'checker:<checker-uuid>' or 'checker:<checker-uuid> AND <other-operators>'";
+ "query must be 'checker:<checker-uuid>' or 'checker:<checker-uuid> AND <other-operators>' or 'scheme:<checker-scheme>' or 'scheme:<checker-scheme> AND <other-operators>'";
assertInvalidQuery(
String.format("state:NOT_STARTED AND (checker:\"%s\" OR state:NOT_STARTED)", checkerUuid),
expectedMessage);
@@ -142,6 +166,21 @@
}
@Test
+ public void cannotSpecifySchemeInAndConditionIfNotImmediateChild() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+ String expectedMessage =
+ "query must be 'checker:<checker-uuid>' or 'checker:<checker-uuid> AND <other-operators>' or 'scheme:<checker-scheme>' or 'scheme:<checker-scheme> AND <other-operators>'";
+ assertInvalidQuery(
+ String.format(
+ "state:NOT_STARTED AND (scheme:\"%s\" OR state:NOT_STARTED)", checkerUuid.scheme()),
+ expectedMessage);
+ assertInvalidQuery(
+ String.format("state:NOT_STARTED AND NOT scheme:\"%s\"", checkerUuid.scheme()),
+ expectedMessage);
+ }
+
+ @Test
public void andConditionAtRootCanContainAnyCombinationOfOtherPredicates() throws Exception {
CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
@@ -168,7 +207,7 @@
CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
String expectedMessage =
- "query must be 'checker:<checker-uuid>' or 'checker:<checker-uuid> AND <other-operators>'";
+ "query must be 'checker:<checker-uuid>' or 'checker:<checker-uuid> AND <other-operators>' or 'scheme:<checker-scheme>' or 'scheme:<checker-scheme> AND <other-operators>'";
assertInvalidQuery(
String.format("checker:\"%s\" OR state:NOT_STARTED", checkerUuid), expectedMessage);
assertInvalidQuery(
@@ -176,11 +215,31 @@
}
@Test
+ public void cannotSpecifySchemeInOrCondition() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+ String expectedMessage =
+ "query must be 'checker:<checker-uuid>' or 'checker:<checker-uuid> AND <other-operators>' or 'scheme:<checker-scheme>' or 'scheme:<checker-scheme> AND <other-operators>'";
+ assertInvalidQuery(
+ String.format("scheme:\"%s\" OR state:NOT_STARTED", checkerUuid.scheme()), expectedMessage);
+ assertInvalidQuery(
+ String.format("state:NOT_STARTED OR scheme:\"%s\"", checkerUuid.scheme()), expectedMessage);
+ }
+
+ @Test
public void cannotSpecifyCheckerInNotCondition() throws Exception {
CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
assertInvalidQuery(
String.format("NOT checker:\"%s\"", checkerUuid),
- "query must be 'checker:<checker-uuid>' or 'checker:<checker-uuid> AND <other-operators>'");
+ "query must be 'checker:<checker-uuid>' or 'checker:<checker-uuid> AND <other-operators>' or 'scheme:<checker-scheme>' or 'scheme:<checker-scheme> AND <other-operators>'");
+ }
+
+ @Test
+ public void cannotSpecifySchemeInNotCondition() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+ assertInvalidQuery(
+ String.format("NOT scheme:\"%s\"", checkerUuid.scheme()),
+ "query must be 'checker:<checker-uuid>' or 'checker:<checker-uuid> AND <other-operators>' or 'scheme:<checker-scheme>' or 'scheme:<checker-scheme> AND <other-operators>'");
}
@Test
@@ -646,6 +705,114 @@
assertThat(pendingChecksList).isNotEmpty();
}
+ @Test
+ public void queryPendingChecksWithScheme() throws Exception {
+ // create a check with a scheme that we expect to never be returned.
+ CheckerUuid checkerUuidOtherScheme =
+ checkerOperations
+ .newChecker()
+ .uuid(CheckerUuid.parse("otherscheme:checker-1"))
+ .repository(project)
+ .create();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuidOtherScheme))
+ .state(CheckState.NOT_STARTED)
+ .upsert();
+
+ CheckerUuid checkerUuid =
+ checkerOperations
+ .newChecker()
+ .uuid(CheckerUuid.parse("test:checker-1"))
+ .repository(project)
+ .create();
+ // Create a check with state "NOT_STARTED" that we expect to be returned.
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .state(CheckState.NOT_STARTED)
+ .upsert();
+
+ // Create a check with state "SCHEDULED" that we expect to be returned.
+ PatchSet.Id patchSetId2 = createChange().getPatchSetId();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId2, checkerUuid))
+ .state(CheckState.SCHEDULED)
+ .upsert();
+
+ // Create a check with state "SUCCESSFUL" that we expect to be ignored.
+ PatchSet.Id patchSetId3 = createChange().getPatchSetId();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId3, checkerUuid))
+ .state(CheckState.SUCCESSFUL)
+ .upsert();
+
+ // Create a check with state "SUCCESSFUL" that we expect to be ignored, for patchsetId.
+ // by default, patchSetId2 and patchSetId3 should be NOT_STARTED, and should be returned.
+ CheckerUuid checkerUuid2 =
+ checkerOperations
+ .newChecker()
+ .uuid(CheckerUuid.parse("test:checker-2"))
+ .repository(project)
+ .create();
+ assertThat(checkerUuid2.scheme()).isEqualTo("test");
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid2))
+ .state(CheckState.FAILED)
+ .upsert();
+
+ List<PendingChecksInfo> pendingChecksList =
+ queryPendingChecks("test", CheckState.NOT_STARTED, CheckState.SCHEDULED);
+ assertThat(pendingChecksList).hasSize(4);
+
+ // The sorting of the pendingChecksList matches the sorting in which the matching changes are
+ // returned from the change index, which is by last updated timestamp. Use this knowledge here
+ // to do the assertions although the REST endpoint doesn't document a guaranteed sort order.
+ PendingChecksInfo pendingChecksChange = pendingChecksList.get(0);
+ assertThat(pendingChecksChange).hasRepository(project);
+ assertThat(pendingChecksChange).hasPatchSet(patchSetId2);
+ assertThat(pendingChecksChange)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid.get(), new PendingCheckInfo(CheckState.SCHEDULED));
+
+ pendingChecksChange = pendingChecksList.get(1);
+ assertThat(pendingChecksChange).hasRepository(project);
+ assertThat(pendingChecksChange).hasPatchSet(patchSetId);
+ assertThat(pendingChecksChange)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid.get(), new PendingCheckInfo(CheckState.NOT_STARTED));
+
+ pendingChecksChange = pendingChecksList.get(2);
+ assertThat(pendingChecksChange).hasRepository(project);
+ assertThat(pendingChecksChange).hasPatchSet(patchSetId3);
+ assertThat(pendingChecksChange)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid2.get(), new PendingCheckInfo(CheckState.NOT_STARTED));
+
+ pendingChecksChange = pendingChecksList.get(3);
+ assertThat(pendingChecksChange).hasRepository(project);
+ assertThat(pendingChecksChange).hasPatchSet(patchSetId2);
+ assertThat(pendingChecksChange)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid2.get(), new PendingCheckInfo(CheckState.NOT_STARTED));
+ }
+
+ @Test
+ public void queryOnlyExactSchemas() throws Exception {
+ CheckerUuid checkerUuid =
+ checkerOperations
+ .newChecker()
+ .uuid(CheckerUuid.parse("foobar:checker-1"))
+ .repository(project)
+ .create();
+ // Create a check with state "NOT_STARTED" that we expect to be returned.
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .state(CheckState.NOT_STARTED)
+ .upsert();
+ assertThat(queryPendingChecks("foo", CheckState.NOT_STARTED, CheckState.SCHEDULED)).isEmpty();
+ assertThat(queryPendingChecks("bar", CheckState.NOT_STARTED, CheckState.RUNNING)).isEmpty();
+ assertThat(queryPendingChecks("foobar", CheckState.NOT_STARTED)).hasSize(1);
+ }
+
private void assertInvalidQuery(String query, String expectedMessage) {
BadRequestException thrown =
assertThrows(BadRequestException.class, () -> pendingChecksApi.query(query).get());
@@ -661,6 +828,11 @@
return pendingChecksApi.query(buildQueryString(checkerUuid, checkStates)).get();
}
+ private List<PendingChecksInfo> queryPendingChecks(String scheme, CheckState... checkStates)
+ throws RestApiException {
+ return pendingChecksApi.query(buildQueryString(scheme, checkStates)).get();
+ }
+
private String buildQueryString(CheckerUuid checkerUuid, CheckState... checkStates) {
StringBuilder queryString = new StringBuilder();
queryString.append(String.format("checker:%s", checkerUuid));
@@ -671,4 +843,15 @@
return queryString.toString();
}
+
+ private String buildQueryString(String scheme, CheckState... checkStates) {
+ StringBuilder queryString = new StringBuilder();
+ queryString.append(String.format("scheme:%s", scheme));
+
+ StringJoiner stateJoiner = new StringJoiner(" OR state:", " (state:", ")");
+ Stream.of(checkStates).map(CheckState::name).forEach(stateJoiner::add);
+ queryString.append(stateJoiner.toString());
+
+ return queryString.toString();
+ }
}
diff --git a/resources/Documentation/rest-api-checks.md b/resources/Documentation/rest-api-checks.md
index 4395644..1c64598 100644
--- a/resources/Documentation/rest-api-checks.md
+++ b/resources/Documentation/rest-api-checks.md
@@ -9,7 +9,7 @@
## <a id="check-endpoints"> Check Endpoints
### <a id="list-checks"> List Checks
-_'GET /changes/[\{change-id\}](../../../Documentation/rest-api-changes.html#change-id)/revisions/[\{revision-id\}](../../../Documentation/rest-api-changes.html#revision-id)/revisions/checks'_
+_'GET /changes/[\{change-id\}](../../../Documentation/rest-api-changes.html#change-id)/revisions/[\{revision-id\}](../../../Documentation/rest-api-changes.html#revision-id)/checks'_
Retrieves all checks for a given revision and change.
@@ -56,7 +56,7 @@
```
### <a id="get-check"> Get Check
-_'GET /changes/[\{change-id\}](../../../Documentation/rest-api-changes.html#change-id)/revisions/[\{revision-id\}](../../../Documentation/rest-api-changes.html#revision-id)/revisions/checks/[\{checker-id\}](./rest-api-checkers.md#checker-id)'_
+_'GET /changes/[\{change-id\}](../../../Documentation/rest-api-changes.html#change-id)/revisions/[\{revision-id\}](../../../Documentation/rest-api-changes.html#revision-id)/checks/[\{checker-id\}](./rest-api-checkers.md#checker-id)'_
Retrieves a check.
diff --git a/resources/Documentation/rest-api-pending-checks.md b/resources/Documentation/rest-api-pending-checks.md
index 91771e3..2974410 100644
--- a/resources/Documentation/rest-api-pending-checks.md
+++ b/resources/Documentation/rest-api-pending-checks.md
@@ -128,3 +128,5 @@
`RUNNING`).
* <a id="state-operator"></a> `state:'STATE'`:
Matches checks with the state 'STATE'.
+* <a id="scheme-operator"></a> `scheme:'SCHEME'`:
+ Matches checks with the scheme 'SCHEME'.