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;