CopyrightReviewApi to update gerrit review threads.
Copyright plugin part 1 of 3: CopyrightReviewApi manages and updates
gerrit code review threads with findings found in new pushed revisions.
CopyrightConfig is a stripped down shell of only what is needed for
CopyrightReviewApi and its tests to build and run. Part 2 will expand
CoprightConfig with listeners and code to parse, validate and load
plugin/scanner configurations.
Part 3 will introduce CopyrightValidator to scan the files in pushed
revisions.
Change-Id: I54f1f69edff19404ecec4f09f21e4ba0051a0b1c
diff --git a/BUILD b/BUILD
index 7585fef..73cc5c5 100644
--- a/BUILD
+++ b/BUILD
@@ -34,7 +34,9 @@
TEST_DEPS = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
":copyright_scanner",
+ "//lib/mockito",
"@guava//jar",
+ "@mockito//jar",
]
junit_tests(
diff --git a/WORKSPACE b/WORKSPACE
index 0bd8981..c8ab81d 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,7 +1,7 @@
workspace(name = "copyright")
load("//:bazlets.bzl", "load_bazlets")
load_bazlets(
- commit = "738ddb525810a50c792736d7115a1bb5289bd3d3",
+ commit = "f3b3eecf061193e8909298bc9c35027df3bf9d44",
#local_path = "/home/<user>/projects/bazlets",
)
@@ -66,3 +66,30 @@
artifact = "com.google.j2objc:j2objc-annotations:1.1",
sha1 = "ed28ded51a8b1c6b112568def5f4b455e6809019",
)
+
+maven_jar(
+ name = "mockito",
+ artifact = "org.mockito:mockito-core:2.24.0",
+ sha1 = "969a7bcb6f16e076904336ebc7ca171d412cc1f9",
+)
+
+BYTE_BUDDY_VERSION = "1.9.7"
+
+maven_jar(
+ name = "byte-buddy",
+ artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
+ sha1 = "8fea78fea6449e1738b675cb155ce8422661e237",
+)
+
+maven_jar(
+ name = "byte-buddy-agent",
+ artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
+ sha1 = "8e7d1b599f4943851ffea125fd9780e572727fc0",
+)
+
+maven_jar(
+ name = "objenesis",
+ artifact = "org.objenesis:objenesis:2.6",
+ sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
+)
+
diff --git a/lib/mockito/BUILD b/lib/mockito/BUILD
new file mode 100644
index 0000000..89a1400
--- /dev/null
+++ b/lib/mockito/BUILD
@@ -0,0 +1,34 @@
+package(
+ default_testonly = True,
+ default_visibility = ["//visibility:public"],
+)
+
+java_library(
+ name = "mockito",
+ data = ["//lib:LICENSE-Apache2.0"],
+ # Only exposed for plugin tests; core tests should use Easymock
+ exports = ["@mockito//jar"],
+ runtime_deps = [
+ ":byte-buddy",
+ ":byte-buddy-agent",
+ ":objenesis",
+ ],
+)
+
+java_library(
+ name = "byte-buddy",
+ data = ["//lib:LICENSE-Apache2.0"],
+ exports = ["@byte-buddy//jar"],
+)
+
+java_library(
+ name = "byte-buddy-agent",
+ data = ["//lib:LICENSE-Apache2.0"],
+ exports = ["@byte-buddy-agent//jar"],
+)
+
+java_library(
+ name = "objenesis",
+ data = ["//lib:LICENSE-Apache2.0"],
+ exports = ["@objenesis//jar"],
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightConfig.java b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightConfig.java
new file mode 100644
index 0000000..c7b0e18
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightConfig.java
@@ -0,0 +1,222 @@
+// 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.googlesource.gerrit.plugins.copyright;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.RevisionCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.AbstractModule;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightPatterns;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightPatterns.UnknownPatternName;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.Match;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.MatchType;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType;
+import com.googlesource.gerrit.plugins.copyright.lib.IndexedLineReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import javax.inject.Inject;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+/** Listener to manage configuration for enforcing review of copyright declarations and licenses. */
+@Singleton
+class CopyrightConfig {
+
+ private static final String DEFAULT_REVIEW_LABEL = "Copyright-Review";
+
+ private final Metrics metrics;
+ private final AllProjectsName allProjectsName;
+ private final String pluginName;
+ private final GitRepositoryManager repoManager;
+ private final ProjectCache projectCache;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final PluginConfigFactory pluginConfigFactory;
+ private final CopyrightReviewApi reviewApi;
+
+ private ScannerConfig scannerConfig;
+
+ @Singleton
+ private static class Metrics {
+ @Inject
+ Metrics(MetricMaker metricMaker) {}
+ }
+
+ @Inject
+ CopyrightConfig(
+ Metrics metrics,
+ AllProjectsName allProjectsName,
+ @PluginName String pluginName,
+ GitRepositoryManager repoManager,
+ ProjectCache projectCache,
+ ProjectConfig.Factory projectConfigFactory,
+ PluginConfigFactory pluginConfigFactory,
+ CopyrightReviewApi reviewApi) throws IOException {
+ this.metrics = metrics;
+ this.allProjectsName = allProjectsName;
+ this.pluginName = pluginName;
+ this.repoManager = repoManager;
+ this.projectCache = projectCache;
+ this.projectConfigFactory = projectConfigFactory;
+ this.pluginConfigFactory = pluginConfigFactory;
+ this.reviewApi = reviewApi;
+ }
+
+ private CopyrightConfig(MetricMaker metricMaker, CopyrightReviewApi reviewApi) {
+ metrics = new Metrics(metricMaker);
+ allProjectsName = new AllProjectsName("All-Projects");
+ pluginName = "copyright";
+ repoManager = null;
+ projectCache = null;
+ projectConfigFactory = null;
+ pluginConfigFactory = null;
+ this.reviewApi = reviewApi;
+ scannerConfig = this.new ScannerConfig();
+ }
+
+ @VisibleForTesting
+ static CopyrightConfig createTestInstance(MetricMaker metricMaker, CopyrightReviewApi reviewApi) {
+ return new CopyrightConfig(metricMaker, reviewApi);
+ }
+
+ @VisibleForTesting
+ ScannerConfig getScannerConfig() {
+ return scannerConfig;
+ }
+
+ /** Returns true if any pattern in {@code regexes} found in {@code text}. */
+ private static boolean matchesAny(String text, Collection<Pattern> regexes) {
+ requireNonNull(regexes);
+ for (Pattern pattern : regexes) {
+ if (pattern.matcher(text).find()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Configuration state for {@link CopyrightValidator}. */
+ class ScannerConfig {
+ CopyrightScanner scanner;
+ final ArrayList<CommitValidationMessage> messages;
+ final LinkedHashSet<Pattern> alwaysReviewPath;
+ final LinkedHashSet<Pattern> matchProjects;
+ final LinkedHashSet<Pattern> excludeProjects;
+ final LinkedHashSet<Pattern> thirdPartyAllowedProjects;
+ final LinkedHashSet<String> reviewers;
+ final LinkedHashSet<String> ccs;
+ String reviewLabel;
+ boolean defaultEnable;
+ int fromAccountId;
+
+ ScannerConfig() {
+ this.messages = new ArrayList<>();
+ this.alwaysReviewPath = new LinkedHashSet<>();
+ this.matchProjects = new LinkedHashSet<>();
+ this.excludeProjects = new LinkedHashSet<>();
+ this.thirdPartyAllowedProjects = new LinkedHashSet<>();
+ this.reviewers = new LinkedHashSet<>();
+ this.ccs = new LinkedHashSet<>();
+ this.reviewLabel = DEFAULT_REVIEW_LABEL;
+ this.defaultEnable = false;
+ this.fromAccountId = 0;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ if (other instanceof ScannerConfig) {
+ ScannerConfig otherConfig = (ScannerConfig) other;
+ return defaultEnable == otherConfig.defaultEnable
+ && messages.equals(otherConfig.messages)
+ && alwaysReviewPath.equals(otherConfig.alwaysReviewPath)
+ && matchProjects.equals(otherConfig.matchProjects)
+ && excludeProjects.equals(otherConfig.excludeProjects)
+ && thirdPartyAllowedProjects.equals(otherConfig.thirdPartyAllowedProjects)
+ && reviewers.equals(otherConfig.reviewers)
+ && ccs.equals(otherConfig.ccs)
+ && Objects.equals(reviewLabel, otherConfig.reviewLabel)
+ && Objects.equals(scanner, otherConfig.scanner);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ defaultEnable,
+ messages,
+ alwaysReviewPath,
+ matchProjects,
+ excludeProjects,
+ thirdPartyAllowedProjects,
+ reviewers,
+ ccs,
+ reviewLabel,
+ scanner);
+ }
+
+ /** Returns true if {@code project} repository allows third-party code. */
+ boolean isThirdPartyAllowed(String project) {
+ return matchesAny(project, thirdPartyAllowedProjects);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApi.java b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApi.java
new file mode 100644
index 0000000..89c9c78
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApi.java
@@ -0,0 +1,567 @@
+// 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.googlesource.gerrit.plugins.copyright;
+
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.MatchType.AUTHOR_OWNER;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.MatchType.LICENSE;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType.FIRST_PARTY;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType.THIRD_PARTY;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType.FORBIDDEN;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType.UNKNOWN;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.events.RevisionCreatedListener;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.restapi.change.ListChangeComments;
+import com.google.gerrit.server.restapi.change.PostReview;
+import com.google.inject.Singleton;
+import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.Match;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.MatchType;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+
+/** Utility to report revision findings on the review thread. */
+@Singleton
+public class CopyrightReviewApi {
+ static final ImmutableList<Match> ALWAYS_REVIEW = ImmutableList.of();
+
+ private final Metrics metrics;
+ private final Provider<PluginUser> pluginUserProvider;
+ private final Provider<CurrentUser> userProvider;
+ private final IdentifiedUser.GenericFactory identifiedUserFactory;
+ private final ChangeNotes.Factory changeNotesFactory;
+ private final ChangeResource.Factory changeResourceFactory;
+ private final PatchSetUtil psUtil;
+ private final PostReview postReview;
+ private final ListChangeComments listChangeComments;
+
+ @Singleton
+ static class Metrics {
+ final Counter0 reviewCount;
+ final Counter0 commentCount;
+ final Timer0 reviewTimer;
+ final Counter1 reviewCountByProject;
+ final Counter1 commentCountByProject;
+ final Timer1 reviewTimerByProject;
+
+ @Inject
+ Metrics(MetricMaker metricMaker) {
+ Field<String> project = Field.ofString("project", "project name");
+ reviewCount =
+ metricMaker.newCounter(
+ "plugin/copyright/review_count",
+ new Description("Total number of posted reviews").setRate().setUnit("reviews"));
+ commentCount =
+ metricMaker.newCounter(
+ "plugin/copyright/comment_count",
+ new Description("Total number of posted review comments")
+ .setRate()
+ .setUnit("comments"));
+ reviewTimer =
+ metricMaker.newTimer(
+ "plugin/copyright/review_latency",
+ new Description("Time spent posting reviews to revisions")
+ .setCumulative()
+ .setUnit(Units.MICROSECONDS));
+ reviewCountByProject =
+ metricMaker.newCounter(
+ "plugin/copyright/review_count_by_project",
+ new Description("Total number of posted reviews").setRate().setUnit("reviews"),
+ project);
+ commentCountByProject =
+ metricMaker.newCounter(
+ "plugin/copyright/comment_count_by_project",
+ new Description("Total number of posted review comments")
+ .setRate()
+ .setUnit("comments"),
+ project);
+ reviewTimerByProject =
+ metricMaker.newTimer(
+ "plugin/copyright/review_latency_by_project",
+ new Description("Time spent posting reviews to revisions")
+ .setCumulative()
+ .setUnit(Units.MICROSECONDS),
+ project);
+ }
+ }
+
+ @Inject
+ CopyrightReviewApi(
+ Metrics metrics,
+ Provider<PluginUser> pluginUserProvider,
+ Provider<CurrentUser> userProvider,
+ IdentifiedUser.GenericFactory identifiedUserFactory,
+ ChangeNotes.Factory changeNotesFactory,
+ ChangeResource.Factory changeResourceFactory,
+ PatchSetUtil psUtil,
+ PostReview postReview,
+ ListChangeComments listChangeComments) {
+ this.metrics = metrics;
+ this.pluginUserProvider = pluginUserProvider;
+ this.userProvider = userProvider;
+ this.identifiedUserFactory = identifiedUserFactory;
+ this.changeNotesFactory = changeNotesFactory;
+ this.changeResourceFactory = changeResourceFactory;
+ this.psUtil = psUtil;
+ this.postReview = postReview;
+ this.listChangeComments = listChangeComments;
+ }
+
+ /**
+ * Reports validation findings for a proposed new plugin configuration to the review thread for
+ * the newly pushed revision.
+ *
+ * @param pluginName as installed in gerrit
+ * @param project identifies the All-Projects config for recording metrics
+ * @param path identifies the file to attach messages to (@code gerrit.config or project.config}
+ * @param oldConfig the prior state of the plugin configuration
+ * @param newConfig the new state of the plugin configuration
+ * @param event describes the pushed revision with the new configuration
+ * @throws RestApiException if an error occurs updating the review thread
+ */
+ ReviewResult reportConfigMessages(
+ String pluginName,
+ String project,
+ String path,
+ CopyrightConfig.ScannerConfig oldConfig,
+ CopyrightConfig.ScannerConfig newConfig,
+ RevisionCreatedListener.Event event) throws RestApiException {
+ Preconditions.checkNotNull(newConfig);
+ Preconditions.checkNotNull(newConfig.messages);
+ Preconditions.checkArgument(!newConfig.messages.isEmpty());
+
+ long startNanos = System.nanoTime();
+ metrics.reviewCount.increment();
+ metrics.reviewCountByProject.increment(project);
+
+ try {
+ int fromAccountId =
+ oldConfig != null && oldConfig.fromAccountId > 0
+ ? oldConfig.fromAccountId
+ : newConfig.fromAccountId;
+ ChangeResource change = getChange(event, fromAccountId);
+ StringBuilder message = new StringBuilder();
+ message.append(pluginName);
+ message.append(" plugin issues parsing new configuration");
+ ReviewInput ri = new ReviewInput()
+ .message(message.toString());
+
+ Map<String, List<CommentInfo>> priorComments = getComments(change);
+ if (priorComments == null) {
+ priorComments = ImmutableMap.of();
+ }
+
+ int numComments = 0;
+ ImmutableMap.Builder<String, List<CommentInput>> comments = ImmutableMap.builder();
+ for (CommitValidationMessage m : newConfig.messages) {
+ message.setLength(0);
+ message.append(m.getType().toString());
+ message.append(" ");
+ message.append(m.getMessage());
+ CommentInput ci = new CommentInput();
+ ci.line = 0;
+ ci.unresolved = true;
+ ci.message = message.toString();
+ if (containsComment(priorComments.get(path), ci)) {
+ continue;
+ }
+ comments.put(path, ImmutableList.of(ci));
+ numComments++;
+ }
+ if (numComments > 0) {
+ ri.comments = comments.build();
+ }
+ metrics.commentCount.incrementBy((long) numComments);
+ metrics.commentCountByProject.incrementBy(project, (long) numComments);
+ return review(change, ri);
+ } finally {
+ long elapsedMicros = (System.nanoTime() - startNanos) / 1000;
+ metrics.reviewTimer.record(elapsedMicros, TimeUnit.MICROSECONDS);
+ metrics.reviewTimerByProject.record(project, elapsedMicros, TimeUnit.MICROSECONDS);
+ }
+ }
+
+ /**
+ * Reports {@link CopyrightValidator} findings from a scanned revision on its review thread.
+ *
+ * @param project identifies the project where change pushed for recording metrics
+ * @param scannerConfig the state of the plugin configuration and scanner
+ * @param event describes the pushed revision scanned by the plugin
+ * @param findings maps each scanned file to the copyright matches found by the scanner
+ * @throws RestApiException if an error occurs updating the review thread
+ */
+ ReviewResult reportScanFindings(
+ String project,
+ CopyrightConfig.ScannerConfig scannerConfig,
+ RevisionCreatedListener.Event event,
+ Map<String, ImmutableList<Match>> findings) throws RestApiException {
+ long startNanos = System.nanoTime();
+ metrics.reviewCount.increment();
+ metrics.reviewCountByProject.increment(project);
+
+ try {
+ boolean tpAllowed = scannerConfig.isThirdPartyAllowed(project);
+ boolean reviewRequired = false;
+ for (Map.Entry<String, ImmutableList<Match>> entry : findings.entrySet()) {
+ if (entry.getValue() == ALWAYS_REVIEW) {
+ reviewRequired = true;
+ break;
+ }
+ PartyType pt = partyType(entry.getValue());
+ if (pt.compareTo(THIRD_PARTY) > 0) {
+ reviewRequired = true;
+ break;
+ }
+ if (pt == THIRD_PARTY && !tpAllowed) {
+ reviewRequired = true;
+ break;
+ }
+ }
+ ChangeResource change = getChange(event, scannerConfig.fromAccountId);
+ ReviewInput ri = new ReviewInput()
+ .message("Copyright scan")
+ .label(scannerConfig.reviewLabel, reviewRequired ? -1 : +2);
+
+ if (reviewRequired) {
+ ri = addReviewers(ri, scannerConfig.ccs, ReviewerState.CC);
+ ri = addReviewers(ri, scannerConfig.reviewers, ReviewerState.REVIEWER);
+ }
+ ImmutableMap.Builder<String, List<CommentInput>> comments = ImmutableMap.builder();
+ if (reviewRequired) {
+ ri = ri.message("This change requires copyright review.");
+ } else {
+ ri = ri.message("This change appears to comply with copyright requirements.");
+ }
+ Map<String, List<CommentInfo>> priorComments = getComments(change);
+ if (priorComments == null) {
+ priorComments = ImmutableMap.of();
+ }
+
+ int numCommentsAdded = 0;
+ for (Map.Entry<String, ImmutableList<Match>> entry : findings.entrySet()) {
+ ImmutableList<CommentInput> newComments = null;
+ if (entry.getValue() == ALWAYS_REVIEW) {
+ CommentInput ci = new CommentInput();
+ ci.line = 0;
+ ci.unresolved = true;
+ ci.message = entry.getKey() + " always requires copyright review";
+ if (containsComment(priorComments.get(entry.getKey()), ci)) {
+ continue;
+ }
+ newComments = ImmutableList.of(ci);
+ } else {
+ PartyType pt = partyType(entry.getValue());
+ newComments = reviewComments(project, pt, tpAllowed, entry.getValue());
+ List<CommentInfo> prior = priorComments.get(entry.getKey());
+ newComments = ImmutableList.copyOf(
+ newComments.stream().filter(ci -> !containsComment(prior, ci))
+ .toArray(i -> new CommentInput[i]));
+ if (newComments.isEmpty()) {
+ continue;
+ }
+ }
+ numCommentsAdded += newComments.size();
+ comments.put(entry.getKey(), newComments);
+ }
+
+ if (numCommentsAdded > 0) {
+ ri.comments = comments.build();
+ }
+ metrics.commentCount.incrementBy((long) numCommentsAdded);
+ metrics.commentCountByProject.incrementBy(project, (long) numCommentsAdded);
+ return review(change, ri);
+ } finally {
+ long elapsedMicros = (System.nanoTime() - startNanos) / 1000;
+ metrics.reviewTimer.record(elapsedMicros, TimeUnit.MICROSECONDS);
+ metrics.reviewTimerByProject.record(project, elapsedMicros, TimeUnit.MICROSECONDS);
+ }
+ }
+
+ /**
+ * Returns the {@link com.google.gerrit.server.CurrentUser} seeming to send the review comments.
+ *
+ * Impersonates {@code fromAccountId} if configured by {@code fromAccountId =} in plugin
+ * configuration -- falling back to the identity of the user pushing the revision.
+ */
+ CurrentUser getSendingUser(int fromAccountId) {
+ PluginUser pluginUser = pluginUserProvider.get();
+ return fromAccountId <= 0 ? userProvider.get() :
+ identifiedUserFactory.runAs(null, new Account.Id(fromAccountId), pluginUser);
+ }
+
+ /**
+ * Constructs a {@link com.google.gerrit.server.change.ChangeResource} from the notes log for
+ * the change onto which a new revision was pushed.
+ *
+ * @param event describes the newly pushed revision
+ * @param fromAccountId identifies the configured user to impersonate when sending review comments
+ * @throws RestApiException if an error occurs looking up the notes log for the change
+ */
+ private ChangeResource getChange(RevisionCreatedListener.Event event, int fromAccountId)
+ throws RestApiException {
+ try {
+ CurrentUser fromUser = getSendingUser(fromAccountId);
+ ChangeNotes notes =
+ changeNotesFactory.createChecked(new Change.Id(event.getChange()._number));
+ return changeResourceFactory.create(notes, fromUser);
+ } catch (Exception e) {
+ Throwables.throwIfUnchecked(e);
+ throw e instanceof RestApiException
+ ? (RestApiException) e : new RestApiException("Cannot load change", e);
+ }
+ }
+
+ /**
+ * Returns a modified {@link com.google.gerrit.extensions.api.changes.ReviewInput} after adding
+ * {@code reviewers} as {@code type} CC or REVIEWER.
+ */
+ @VisibleForTesting
+ ReviewInput addReviewers(ReviewInput ri, Iterable<String> reviewers, ReviewerState type) {
+ for (String reviewer : reviewers) {
+ ri = ri.reviewer(reviewer, type, true);
+ }
+ return ri;
+ }
+
+ /**
+ * Retrieves all of the prior review comments already attached to {@code change}.
+ *
+ * @throws RestApiException if an error occurs retrieving the comments
+ */
+ private Map<String, List<CommentInfo>> getComments(ChangeResource change)
+ throws RestApiException {
+ try {
+ return listChangeComments.apply(change);
+ } catch (Exception e) {
+ Throwables.throwIfUnchecked(e);
+ throw e instanceof RestApiException
+ ? (RestApiException) e : new RestApiException("Cannot list comments", e);
+ }
+ }
+
+ /**
+ * Adds the code review described by {@code ri} to the review thread of {@code change}.
+ *
+ * @throws RestApiException if an error occurs updating the review thread
+ */
+ private ReviewResult review(ChangeResource change, ReviewInput ri) throws RestApiException {
+ try {
+ PatchSet ps = psUtil.current(change.getNotes());
+ if (ps == null) {
+ throw new ResourceNotFoundException(IdString.fromDecoded("current"));
+ }
+ RevisionResource revision = RevisionResource.createNonCacheable(change, ps);
+ return postReview.apply(revision, ri).value();
+ } catch (Exception e) {
+ Throwables.throwIfUnchecked(e);
+ throw e instanceof RestApiException
+ ? (RestApiException) e : new RestApiException("Cannot post review", e);
+ }
+ }
+
+ /** Returns true if {@code priorComments} already includes a comment identical to {@code ci}. */
+ @VisibleForTesting
+ boolean containsComment(Iterable<? extends Comment> priorComments, CommentInput ci) {
+ if (priorComments == null) {
+ return false;
+ }
+ for (Comment prior : priorComments) {
+ if (Objects.equals(prior.line, ci.line)
+ && Objects.equals(prior.range, ci.range)
+ && Objects.equals(prior.message, ci.message)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Puts the pieces together from a scanner finding to construct a coherent human-reable message.
+ *
+ * @param project describes the project or repository where the revision was pushed
+ * @param overallPt identifies the calculated
+ * {@link com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType} for all of
+ * the findings in the file. e.g. 1p license + 3p owner == 1p, no license + 3p owner == 3p
+ * @param pt identifies the {@code PartyType} of the current finding
+ * @param mt identifies the
+ * {@link com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.MatchType} of the
+ * current finding. i.e. AUTHOR_OWNER or LICENSE
+ * @param tpAllowed is true if {@code project} allows third-party code
+ * @param text of the message for the finding
+ */
+ private String buildMessage(
+ String project,
+ PartyType overallPt,
+ PartyType pt,
+ MatchType mt,
+ boolean tpAllowed,
+ StringBuilder text) {
+ StringBuilder message = new StringBuilder();
+ switch (pt) {
+ case FIRST_PARTY:
+ message.append("First-party ");
+ break;
+ case THIRD_PARTY:
+ message.append("Third-party ");
+ break;
+ case FORBIDDEN:
+ message.append("Disapproved ");
+ break;
+ default:
+ message.append("Unrecognized ");
+ break;
+ }
+ message.append(mt == AUTHOR_OWNER ? "author or owner " : "license ");
+ if (pt == THIRD_PARTY && overallPt != FIRST_PARTY) {
+ if (!tpAllowed) {
+ message.append("dis");
+ }
+ message.append("allowed in repository ");
+ message.append(project);
+ }
+ message.append(":\n\n");
+ message.append(text);
+ return message.toString();
+ }
+
+ /**
+ * Converts the scanner findings in {@code matches} into human-readable review comments.
+ *
+ * @param project the project or repository to which the revision was pushed
+ * @param pt the calculated overall
+ * {@link com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType} for the
+ * findings e.g. 1p license + 3p owner = 1p, no license + 3p owner = 3p
+ * @param tpAllowed is true if {@code project} allows third-party code
+ * @param matches describes the location and types of matches found in a file
+ * @return a list of {@link com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput} to
+ * add to the {@link com.google.gerrit.extensions.api.changes.ReviewInput} to add to the
+ * review thread for the current patch set.
+ */
+ @VisibleForTesting
+ ImmutableList<CommentInput> reviewComments(
+ String project, PartyType pt, boolean tpAllowed, Iterable<Match> matches) {
+ ImmutableList.Builder<CommentInput> builder = ImmutableList.builder();
+ CommentInput ci = null;
+ PartyType previousPt = UNKNOWN;
+ MatchType previousMt = LICENSE;
+ StringBuilder text = new StringBuilder();
+ for (Match m : matches) {
+ if (ci == null
+ || previousPt != m.partyType
+ || m.endLine > ci.range.startLine + 60
+ || m.startLine > ci.range.endLine + 40) {
+ if (ci != null) {
+ ci.message = buildMessage(project, pt, previousPt, previousMt, tpAllowed, text);
+ builder.add(ci);
+ }
+ ci = new CommentInput();
+ boolean allowed = m.partyType == FIRST_PARTY
+ || (m.partyType == THIRD_PARTY && tpAllowed)
+ || (m.partyType == THIRD_PARTY && m.matchType == AUTHOR_OWNER && pt == FIRST_PARTY);
+ ci.unresolved = !allowed;
+ ci.range = new Comment.Range();
+ ci.line = m.endLine;
+ ci.range.startLine = m.startLine;
+ ci.range.endLine = m.endLine;
+ ci.range.startCharacter = 0;
+ ci.range.endCharacter = 0;
+ previousPt = m.partyType;
+ previousMt = m.matchType;
+ text.setLength(0);
+ text.append(m.text);
+ continue;
+ }
+ text.append("...");
+ text.append(m.text);
+ // an author or owner inside a license declaration is a normal part of a license declaration
+ if (m.matchType == LICENSE) {
+ previousMt = LICENSE;
+ }
+ if (m.startLine < ci.range.startLine) {
+ ci.range.startLine = m.startLine;
+ }
+ if (m.endLine > ci.range.endLine) {
+ ci.range.endLine = m.endLine;
+ ci.line = m.endLine;
+ }
+ }
+ if (ci != null) {
+ ci.message = buildMessage(project, pt, previousPt, previousMt, tpAllowed, text);
+ builder.add(ci);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Calculates and returns the overall
+ * {@link com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType} for the
+ * copyright scanner findings in {@code matches}.
+ */
+ @VisibleForTesting
+ PartyType partyType(Iterable<Match> matches) {
+ PartyType pt = PartyType.FIRST_PARTY;
+ boolean hasThirdPartyOwner = false;
+ boolean hasFirstPartyLicense = false;
+ for (Match match : matches) {
+ if (match.partyType == PartyType.THIRD_PARTY && match.matchType == MatchType.AUTHOR_OWNER) {
+ hasThirdPartyOwner = true;
+ } else if (match.partyType == PartyType.FIRST_PARTY && match.matchType == MatchType.LICENSE) {
+ hasFirstPartyLicense = true;
+ } else if (match.partyType.compareTo(pt) > 0) {
+ pt = match.partyType;
+ }
+ }
+ if (pt == PartyType.FIRST_PARTY && hasThirdPartyOwner && !hasFirstPartyLicense) {
+ return PartyType.THIRD_PARTY;
+ }
+ return pt;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/copyright/lib/CopyrightScanner.java b/src/main/java/com/googlesource/gerrit/plugins/copyright/lib/CopyrightScanner.java
index e5e2888..ddc5f78 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/copyright/lib/CopyrightScanner.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/copyright/lib/CopyrightScanner.java
@@ -301,6 +301,26 @@
this.copyright = buildPattern();
}
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ if (other instanceof CopyrightScanner) {
+ CopyrightScanner otherScanner = (CopyrightScanner) other;
+ return copyright.equals(otherScanner.copyright);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return copyright.hashCode();
+ }
+
/**
* Scans `source` for copyright notices returning found license/author/owner information.
*
@@ -979,8 +999,8 @@
public enum PartyType {
FIRST_PARTY,
THIRD_PARTY,
- FORBIDDEN,
UNKNOWN,
+ FORBIDDEN,
}
/** Identifies whether text matched by author/owner pattern or by license pattern. */
@@ -1014,11 +1034,11 @@
/** The character offset into the file where the match ends. */
public int end;
- Match(PartyType partyType, String text, int startLine, int endLine, int start, int end) {
+ public Match(PartyType partyType, String text, int startLine, int endLine, int start, int end) {
this(partyType, MatchType.AUTHOR_OWNER, text, startLine, endLine, start, end);
}
- Match(
+ public Match(
PartyType partyType,
MatchType matchType,
String text,
diff --git a/src/test/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApiTest.java b/src/test/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApiTest.java
new file mode 100644
index 0000000..4c0b4b6
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/copyright/CopyrightReviewApiTest.java
@@ -0,0 +1,449 @@
+// 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.googlesource.gerrit.plugins.copyright;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.MatchType.AUTHOR_OWNER;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.MatchType.LICENSE;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType.FIRST_PARTY;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType.THIRD_PARTY;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType.FORBIDDEN;
+import static com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType.UNKNOWN;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Correspondence;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.GroupMembership;
+import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.Match;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CopyrightReviewApiTest {
+ private static final PluginUser pluginUser = new FakePluginUser();
+ private static final CurrentUser currentUser = new FakeCurrentUser();
+
+ @Mock private IdentifiedUser.GenericFactory identifiedUserFactory;
+
+ private CopyrightReviewApi reviewApi;
+
+ @Before
+ public void setUp() throws Exception {
+ reviewApi = new CopyrightReviewApi(
+ null,
+ () -> pluginUser,
+ () -> currentUser,
+ identifiedUserFactory,
+ null,
+ null,
+ null,
+ null,
+ null);
+ }
+
+ @Test
+ public void testGetSendingUser_fromAccountIdConfigured() throws Exception {
+ CurrentUser from = reviewApi.getSendingUser(808);
+ verify(identifiedUserFactory, times(1))
+ .runAs(eq(null), any(Account.Id.class), any(PluginUser.class));
+ }
+
+ @Test
+ public void testGetSendingUser_noFromAccountIdConfigured() throws Exception {
+ CurrentUser from = reviewApi.getSendingUser(0);
+ verify(identifiedUserFactory, never())
+ .runAs(eq(null), any(Account.Id.class), any(PluginUser.class));
+ assertThat(from).isSameAs(currentUser);
+ }
+
+ @Test
+ public void testAddReviewers_addNone() throws Exception {
+ ReviewInput ri = new ReviewInput();
+ ri = reviewApi.addReviewers(ri, ImmutableList.of(), ReviewerState.CC);
+ assertThat(ri.reviewers).isNull();
+ ri = reviewApi.addReviewers(ri, ImmutableList.of(), ReviewerState.REVIEWER);
+ assertThat(ri.reviewers).isNull();
+ }
+
+ @Test
+ public void testAddReviewers_addCC() throws Exception {
+ ReviewInput ri = new ReviewInput();
+ ri = reviewApi.addReviewers(ri, ImmutableList.of("someone"), ReviewerState.CC);
+ assertThat(ri.reviewers)
+ .comparingElementsUsing(addressedTo())
+ .containsExactly("CC:someone");
+ }
+
+ @Test
+ public void testAddReviewers_addReviewer() throws Exception {
+ ReviewInput ri = new ReviewInput();
+ ri = reviewApi.addReviewers(ri, ImmutableList.of("someone"), ReviewerState.REVIEWER);
+ assertThat(ri.reviewers)
+ .comparingElementsUsing(addressedTo())
+ .containsExactly("REVIEWER:someone");
+ }
+
+ @Test
+ public void testAddReviewers_addMultiple() throws Exception {
+ ReviewInput ri = new ReviewInput();
+ ri = reviewApi.addReviewers(
+ ri, ImmutableList.of("someone", "someone else"), ReviewerState.REVIEWER);
+ ri = reviewApi.addReviewers(ri, ImmutableList.of("another", "and another"), ReviewerState.CC);
+ assertThat(ri.reviewers)
+ .comparingElementsUsing(addressedTo())
+ .containsExactly(
+ "REVIEWER:someone", "REVIEWER:someone else", "CC:another", "CC:and another");
+ }
+
+ @Test
+ public void testContainsComment_empty() throws Exception {
+ assertThat(reviewApi.containsComment(ImmutableList.of(), CI("text", 1, 2))).isFalse();
+ }
+
+ @Test
+ public void testContainsComment_identity() throws Exception {
+ CommentInput ci = CI("test text", 2, 3);
+ assertThat(reviewApi.containsComment(ImmutableList.of(ci), ci)).isTrue();
+ }
+
+ @Test
+ public void testContainsComment_sameValues() throws Exception {
+ CommentInput ci = CI("test text", 2, 3);
+ CommentInput twin = CI("test text", 2, 3);
+ assertThat(reviewApi.containsComment(ImmutableList.of(ci), twin)).isTrue();
+ }
+
+ @Test
+ public void testContainsComment_singleDifferentRangeStart() throws Exception {
+ CommentInput ci = CI("a comment", 4, 5);
+ CommentInput otherRange = CI("a comment", 3, 5);
+ assertThat(reviewApi.containsComment(ImmutableList.of(ci), otherRange)).isFalse();
+ }
+
+ @Test
+ public void testContainsComment_singleDifferentRangeEnd() throws Exception {
+ CommentInput ci = CI("a comment", 6, 7);
+ CommentInput otherRange = CI("a comment", 6, 8);
+ assertThat(reviewApi.containsComment(ImmutableList.of(ci), otherRange)).isFalse();
+ }
+
+ @Test
+ public void testContainsComment_singleDifferentText() throws Exception {
+ CommentInput ci = CI("a comment", 9, 9);
+ CommentInput otherText = CI("another comment", 9, 9);
+ assertThat(reviewApi.containsComment(ImmutableList.of(ci), otherText)).isFalse();
+ }
+
+ @Test
+ public void testContainsComment_multipleDoContain() throws Exception {
+ ImmutableList<CommentInput> comments = ImmutableList.of(
+ CI("one", 1, 2),
+ CI("two", 806, 808),
+ CI("three", 3, 14));
+ assertThat(reviewApi.containsComment(comments, CI("three", 3, 14))).isTrue();
+ assertThat(reviewApi.containsComment(comments, CI("two", 806, 808))).isTrue();
+ assertThat(reviewApi.containsComment(comments, CI("one", 1, 2))).isTrue();
+ }
+
+ @Test
+ public void testContainsComment_multipleDoNotContain() throws Exception {
+ ImmutableList<CommentInput> comments = ImmutableList.of(
+ CI("one", 1, 2),
+ CI("two", 806, 808),
+ CI("three", 3, 14));
+ assertThat(reviewApi.containsComment(comments, CI("four", 806, 808))).isFalse();
+ }
+
+ @Test
+ public void testReviewComments_firstParty() throws Exception {
+ assertThat(
+ reviewApi.reviewComments(
+ "project",
+ FIRST_PARTY, // 1p license with 3p author is 1p license
+ false,
+ ImmutableList.of(lic1p(2), auth1p(3), lic1p(4), lic1p(120), auth3p(121), auth1p(122))
+ ))
+ .comparingElementsUsing(startsWithAndRangesMatch())
+ .containsExactly(
+ CI("First-party license :", 2, 4),
+ CI("First-party license :", 120, 120),
+ CI("Third-party author or owner :", 121, 121),
+ CI("First-party author or owner :", 122, 122));
+ }
+
+ @Test
+ public void testReviewComments_thirdPartyAllowed() throws Exception {
+ assertThat(
+ reviewApi.reviewComments(
+ "project",
+ THIRD_PARTY, // 3p license and 1p license or author is 3p
+ true,
+ ImmutableList.of(lic3p(2), auth3p(3), lic3p(10), auth3p(200), auth3p(210), lic1p(211))
+ ))
+ .comparingElementsUsing(startsWithAndRangesMatch())
+ .containsExactly(
+ CI("Third-party license allowed", 2, 10),
+ CI("Third-party author or owner allowed", 200, 210),
+ CI("First-party license :", 211, 211));
+ }
+
+ @Test
+ public void testReviewComments_thirdPartyNotAllowed() throws Exception {
+ assertThat(
+ reviewApi.reviewComments(
+ "project",
+ THIRD_PARTY, // 3p license and 1p license or author is 3p
+ false,
+ ImmutableList.of(lic3p(2), auth3p(3), lic3p(10), auth3p(200), auth3p(210), auth1p(211))
+ ))
+ .comparingElementsUsing(startsWithAndRangesMatch())
+ .containsExactly(
+ CI("Third-party license disallowed", 2, 10),
+ CI("Third-party author or owner disallowed", 200, 210),
+ CI("First-party author or owner :", 211, 211));
+ }
+
+ @Test
+ public void testReviewComments_forbiddenAuthor() throws Exception {
+ assertThat(
+ reviewApi.reviewComments(
+ "project",
+ FORBIDDEN, // forbidden author and anything else is still forbidden
+ false,
+ ImmutableList.of(lic1p(2), auth1p(3), lic1p(4), lic1p(120), authForbidden(121))
+ ))
+ .comparingElementsUsing(startsWithAndRangesMatch())
+ .containsExactly(
+ CI("First-party license :", 2, 4),
+ CI("First-party license :", 120, 120),
+ CI("Disapproved author or owner :", 121, 121));
+ }
+
+ @Test
+ public void testReviewComments_forbiddenLicense() throws Exception {
+ assertThat(
+ reviewApi.reviewComments(
+ "project",
+ FORBIDDEN, // forbidden license and anything else is still forbidden
+ false,
+ ImmutableList.of(lic1p(2), auth1p(3), lic1p(4), lic1p(120), licForbidden(121))
+ ))
+ .comparingElementsUsing(startsWithAndRangesMatch())
+ .containsExactly(
+ CI("First-party license :", 2, 4),
+ CI("First-party license :", 120, 120),
+ CI("Disapproved license :", 121, 121));
+ }
+
+ @Test
+ public void testReviewComments_unknownLicense() throws Exception {
+ assertThat(
+ reviewApi.reviewComments(
+ "project",
+ FORBIDDEN, // an unknown license could be forbidden so always requires review
+ false,
+ ImmutableList.of(lic1p(2), auth1p(3), lic1p(4), lic1p(120), licUnknown(121))
+ ))
+ .comparingElementsUsing(startsWithAndRangesMatch())
+ .containsExactly(
+ CI("First-party license :", 2, 4),
+ CI("First-party license :", 120, 120),
+ CI("Unrecognized license :", 121, 121));
+ }
+
+ @Test
+ public void testPartyType_firstPartyLicense() throws Exception {
+ // 1p license with 3p author is 1p in open-source
+ assertThat(
+ reviewApi.partyType(ImmutableList.of(lic1p(1), auth3p(2), lic1p(3)))
+ ).isEqualTo(FIRST_PARTY);
+ }
+
+ @Test
+ public void testPartyType_firstPartyOwner() throws Exception {
+ assertThat(
+ reviewApi.partyType(ImmutableList.of(auth1p(1), auth1p(2)))
+ ).isEqualTo(FIRST_PARTY);
+ }
+
+ @Test
+ public void testPartyType_thirdPartyLicense() throws Exception {
+ // 3p license with 1p license or author is 3p
+ assertThat(
+ reviewApi.partyType(ImmutableList.of(lic3p(1), lic1p(3), auth1p(4)))
+ ).isEqualTo(THIRD_PARTY);
+ }
+
+ @Test
+ public void testPartyType_thirdPartyOwner() throws Exception {
+ // 3p author and 1p author without any license is 3p
+ assertThat(
+ reviewApi.partyType(ImmutableList.of(auth3p(1), auth1p(2)))
+ ).isEqualTo(THIRD_PARTY);
+ }
+
+ @Test
+ public void testPartyType_forbiddenLicense() throws Exception {
+ // forbidden anything with anything else in any combination is forbidden
+ assertThat(
+ reviewApi.partyType(
+ ImmutableList.of(
+ licForbidden(1),
+ licUnknown(2),
+ lic3p(3),
+ auth3p(4),
+ lic1p(5),
+ auth1p(6)))
+ ).isEqualTo(FORBIDDEN);
+ }
+
+ @Test
+ public void testPartyType_forbiddenOwner() throws Exception {
+ // forbidden anything with anything else in any combination is forbidden
+ assertThat(
+ reviewApi.partyType(
+ ImmutableList.of(
+ authForbidden(1),
+ licUnknown(2),
+ lic3p(3),
+ auth3p(4),
+ lic1p(5),
+ auth1p(6)))
+ ).isEqualTo(FORBIDDEN);
+ }
+
+ @Test
+ public void testPartyType_unknownLicense() throws Exception {
+ // unknown license with anything but forbidden is unknown (possibly forbidden)
+ assertThat(
+ reviewApi.partyType(
+ ImmutableList.of(
+ licUnknown(2),
+ lic3p(3),
+ auth3p(4),
+ lic1p(5),
+ auth1p(6)))
+ ).isEqualTo(UNKNOWN);
+ }
+
+ private static class FakePluginUser extends PluginUser {
+ FakePluginUser() {
+ super("copyright-test");
+ }
+ }
+
+ private static class FakeCurrentUser extends CurrentUser {
+ @Override
+ public Object getCacheKey() {
+ return "31415966";
+ }
+ @Override
+ public GroupMembership getEffectiveGroups() {
+ return null;
+ }
+ }
+
+ private static Correspondence<CommentInput, CommentInput> startsWithAndRangesMatch() {
+ return new Correspondence<CommentInput, CommentInput>() {
+ @Override
+ public boolean compare(CommentInput actual, CommentInput expected) {
+ return actual.range.startLine == expected.range.startLine
+ && actual.range.endLine == expected.range.endLine
+ && actual.message.startsWith(expected.message);
+ }
+
+ @Override
+ public String toString() {
+ return "starts with and ranges match";
+ }
+ };
+ }
+
+ private static Correspondence<AddReviewerInput, String> addressedTo() {
+ return new Correspondence<AddReviewerInput, String>() {
+ @Override
+ public boolean compare(AddReviewerInput actual, String expected) {
+ return expected.equals(actual.state().toString() + ":" + actual.reviewer);
+ }
+
+ @Override
+ public String toString() {
+ return "addressed to";
+ }
+ };
+ }
+
+ /** Comment input {@code text} from {@code start} line to {@code end} line. */
+ private CommentInput CI(String text, int start, int end) {
+ CommentInput.Range r = new CommentInput.Range();
+ r.startLine = start;
+ r.endLine = end;
+ CommentInput ci = new CommentInput();
+ ci.message = text;
+ ci.range = r;
+ return ci;
+ }
+
+ /** First-party license at {@code line} */
+ private Match lic1p(int line) {
+ return new Match(FIRST_PARTY, LICENSE, "1p license", line, line, line, line);
+ }
+
+ /** Third-party license at {@code line} */
+ private Match lic3p(int line) {
+ return new Match(THIRD_PARTY, LICENSE, "3p license", line, line, line, line);
+ }
+
+ /** Forbidden license at {@code line} */
+ private Match licForbidden(int line) {
+ return new Match(FORBIDDEN, LICENSE, "forbidden license", line, line, line, line);
+ }
+
+ /** Unknown license at {@code line} */
+ private Match licUnknown(int line) {
+ return new Match(UNKNOWN, LICENSE, "unknown license", line, line, line, line);
+ }
+
+ /** First-party author/owner at {@code line} */
+ private Match auth1p(int line) {
+ return new Match(FIRST_PARTY, AUTHOR_OWNER, "1p author", line, line, line, line);
+ }
+
+ /** Third-party author/owner at {@code line} */
+ private Match auth3p(int line) {
+ return new Match(THIRD_PARTY, AUTHOR_OWNER, "3p author", line, line, line, line);
+ }
+
+ /** Forbidden author/owner at {@code line} */
+ private Match authForbidden(int line) {
+ return new Match(FORBIDDEN, AUTHOR_OWNER, "forbidden author", line, line, line, line);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/copyright/lib/CopyrightScannerTest.java b/src/test/java/com/googlesource/gerrit/plugins/copyright/lib/CopyrightScannerTest.java
index 393b3b1..3aa379b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/copyright/lib/CopyrightScannerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/copyright/lib/CopyrightScannerTest.java
@@ -23,14 +23,11 @@
import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.Match;
import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.MatchType;
import com.googlesource.gerrit.plugins.copyright.lib.CopyrightScanner.PartyType;
-import com.sun.management.HotSpotDiagnosticMXBean;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
-import java.lang.management.ManagementFactory;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import org.junit.Before;
-import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;