blob: b074992f5337939aee9c8f53b2657db24937b85d [file] [log] [blame]
// Copyright (C) 2017 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.findowners;
import static com.google.common.truth.Truth.assertThat;
import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS;
import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS_FILE_NAME;
import static com.googlesource.gerrit.plugins.findowners.Config.REJECT_ERROR_IN_OWNERS;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestPlugin;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
/** Test OwnersValidator, which checks syntax of changed OWNERS files. */
@TestPlugin(name = "find-owners", sysModule = "com.googlesource.gerrit.plugins.findowners.Module")
public class OwnersValidatorIT extends LightweightPluginDaemonTest {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Rule public Watcher watcher = new Watcher(logger);
@Inject private ProjectOperations projectOperations;
protected Project.NameKey newProject(String name) {
return projectOperations
.newProject()
.parent(new Project.NameKey("All-Projects"))
.name(name)
.create();
}
private void switchProject(Project.NameKey p) throws Exception {
project = p;
testRepo = cloneProject(project);
}
private void addProjectFile(String project, String file, String content) throws Exception {
switchProject(newProject(project));
PushOneCommit.Result c = createChange("c", file, content);
approve(c.getChangeId());
gApi.changes().id(c.getChangeId()).current().submit(new SubmitInput());
}
private static class MockedCommitReceivedEvent extends CommitReceivedEvent {
MockedCommitReceivedEvent(String project, RevWalk revWalk, RevCommit commit) {
this.project = new Project(new Project.NameKey(project));
this.revWalk = revWalk;
this.commit = commit;
this.refName = "master";
}
}
private static class MockedEmails extends Emails {
Set<String> registered;
MockedEmails() {
super(null, null, null);
registered =
ImmutableSet.of(
"u1@g.com", "u2@g.com", "u2.m@g.com", "user1@google.com", "u1+review@g.com");
}
@Override
public ImmutableSetMultimap<String, Account.Id> getAccountsFor(String... emails) {
// Used by checkEmails; each email should have exactly one Account.Id
ImmutableSetMultimap.Builder<String, Account.Id> builder = ImmutableSetMultimap.builder();
int id = 1000000;
for (String s : registered) {
builder.put(s, new Account.Id(++id));
}
return builder.build();
}
}
private File repoFolder;
private Repository repo;
@Before
public void init() throws IOException {
repoFolder = File.createTempFile("Git", "");
repoFolder.delete();
repo = FileRepositoryBuilder.create(new File(repoFolder, ".git"));
repo.create();
}
@After
public void cleanup() throws IOException {
repo.close();
if (repoFolder.exists()) {
FileUtils.deleteDirectory(repoFolder);
}
}
private static final String OWNERS_ANDROID = "OWNERS.android"; // alternative OWNERS file name
private static final PluginConfig ANDROID_CONFIG = createAndroidConfig(); // use OWNERS_ANDROID
private static final PluginConfig EMPTY_CONFIG = new PluginConfig("", new Config());
private static final PluginConfig ENABLED_CONFIG = createEnabledConfig(); // use OWNERS
private static final PluginConfig DISABLED_CONFIG = createDisabledConfig();
@Test
public void chekIsActiveAndFileName() throws Exception {
// This check should be enabled in project.config, default is not active.
assertThat(OwnersValidator.isActive(EMPTY_CONFIG)).isFalse();
assertThat(OwnersValidator.isActive(ENABLED_CONFIG)).isTrue();
assertThat(OwnersValidator.isActive(ANDROID_CONFIG)).isTrue();
assertThat(OwnersValidator.isActive(DISABLED_CONFIG)).isFalse();
// Default file name is "OWNERS".
assertThat(OwnersValidator.getOwnersFileName(EMPTY_CONFIG)).isEqualTo(OWNERS);
assertThat(OwnersValidator.getOwnersFileName(ENABLED_CONFIG)).isEqualTo(OWNERS);
assertThat(OwnersValidator.getOwnersFileName(DISABLED_CONFIG)).isEqualTo(OWNERS);
assertThat(OwnersValidator.getOwnersFileName(ANDROID_CONFIG)).isEqualTo(OWNERS_ANDROID);
}
private static final ImmutableMap<String, String> FILES_WITHOUT_OWNERS =
ImmutableMap.of("README", "any\n", "d1/test.c", "int x;\n");
@Test
public void testNoOwners() throws Exception {
CommitReceivedEvent event = makeCommitEvent("myTest", "no OWNERS.", FILES_WITHOUT_OWNERS);
assertThat(validate(event, false, ENABLED_CONFIG)).isEmpty();
assertThat(validate(event, true, ENABLED_CONFIG)).isEmpty();
}
private static final String[] INCLUDE_STMTS =
new String[] {
" include p1/p2 : /d1/owners",
"include p2/p1://d1/owners #comment",
"include ../d2/owners",
" per-file *.java = file: //d2/OWNERS.java #",
"per-file *.cpp,*cc=file:p1/p2:/c++owners",
" file: p1/p2 : /d1/owners ", // repeated
"file: p3/p2://d1/owners #comment",
"file: ../d2/owners", // repeated
"file: //OWNERS", // recursive inclusion
"file:///OWNERS.android"
};
private static final ImmutableSet<String> SKIP_INCLUDE_STMTS =
ImmutableSet.of(" file: p1/p2 : /d1/owners ", "file: ../d2/owners", "file: //OWNERS");
private static String allIncludeStatements() {
String statement = "";
for (String s : INCLUDE_STMTS) {
statement += s + "\n";
}
return statement;
}
private static Set<String> allIncludeMsgs() {
Set<String> msgs = new HashSet<>();
for (int i = 0; i < INCLUDE_STMTS.length; i++) {
if (!SKIP_INCLUDE_STMTS.contains(INCLUDE_STMTS[i])) {
msgs.add(
"MSG: unchecked: OWNERS:" + (i + 1) + ": " + Parser.getIncludeOrFile(INCLUDE_STMTS[i]));
}
}
return msgs;
}
private static final ImmutableMap<String, String> FILES_WITH_NO_ERROR =
ImmutableMap.of(
OWNERS,
allIncludeStatements()
+ "\n\n#comments ...\n ### more comments\n"
+ " user1@google.com # comment\n"
+ "u1+review@g.com###\n"
+ " * # everyone can approve\n"
+ "per-file *.py=u2.m@g.com \n"
+ "per-file *.c, *.java ,A*bp = u1@g.com, u1+review@g.com ,u2.m@g.com#comment\n"
+ " per-file *.txt = * # everyone can approve # \n"
+ "per-file *.java = set noparent # \n"
+ " set noparent # comment#\n");
private static Set<String> allVerboseMsgs() {
Set<String> msgs = allIncludeMsgs();
msgs.add("MSG: checking " + OWNERS);
msgs.add("MSG: owner: u1+review@g.com");
msgs.add("MSG: owner: u1@g.com");
msgs.add("MSG: owner: u2.m@g.com");
msgs.add("MSG: owner: user1@google.com");
String[] missing =
new String[] {
"p1/p2:OWNERS.android",
"p1/p2:c++owners",
"p1/p2:d1/owners",
"p1/p2:d2/OWNERS.java",
"p1/p2:d2/owners",
"p2/p1:d1/owners",
"p3/p2:d1/owners",
};
for (String s : missing) {
msgs.add("MSG: cannot find file: " + s);
msgs.add("MSG: check repo file " + s);
}
String[] skips =
new String[] {
"p1/p2:OWNERS", "p1/p2:d1/owners", "p1/p2:d2/owners",
};
for (String s : skips) {
msgs.add("MSG: skip repeated include of " + s);
}
return msgs;
}
private static final ImmutableSet<String> EXPECTED_VERBOSE_OUTPUT =
ImmutableSet.copyOf(allVerboseMsgs());
private static final ImmutableSet<String> EXPECTED_NON_VERBOSE_OUTPUT =
ImmutableSet.copyOf(allIncludeMsgs());
@Test
public void testGoodInput() throws Exception {
CommitReceivedEvent event = makeCommitEvent("p1/p2", "good files", FILES_WITH_NO_ERROR);
assertThat(validate(event, false, ENABLED_CONFIG))
.containsExactlyElementsIn(EXPECTED_NON_VERBOSE_OUTPUT);
assertThat(validate(event, true, ENABLED_CONFIG))
.containsExactlyElementsIn(EXPECTED_VERBOSE_OUTPUT);
}
private static final ImmutableMap<String, String> FILES_WITH_WRONG_SYNTAX =
ImmutableMap.of(
"README",
"# some content\nu2@g.com\n",
OWNERS,
"\n\n\nwrong syntax\n#comment\nuser1@google.com\n",
"d2/" + OWNERS,
"u1@g.com\n\nu3@g.com\n*\n");
private static final ImmutableSet<String> EXPECTED_WRONG_SYNTAX =
ImmutableSet.of(
"ERROR: syntax: " + OWNERS + ":4: wrong syntax",
"ERROR: unknown: u3@g.com at d2/" + OWNERS + ":3");
private static final ImmutableSet<String> EXPECTED_VERBOSE_WRONG_SYNTAX =
ImmutableSet.of(
"MSG: checking d2/" + OWNERS,
"MSG: checking " + OWNERS,
"MSG: owner: user1@google.com",
"MSG: owner: u1@g.com",
"MSG: owner: u3@g.com",
"ERROR: syntax: " + OWNERS + ":4: wrong syntax",
"ERROR: unknown: u3@g.com at d2/" + OWNERS + ":3");
@Test
public void testWrongSyntax() throws Exception {
CommitReceivedEvent event = makeCommitEvent("p1/p2", "wrong syntax", FILES_WITH_WRONG_SYNTAX);
assertThat(validate(event, false, ENABLED_CONFIG))
.containsExactlyElementsIn(EXPECTED_WRONG_SYNTAX);
assertThat(validate(event, true, ENABLED_CONFIG))
.containsExactlyElementsIn(EXPECTED_VERBOSE_WRONG_SYNTAX);
}
private static final ImmutableMap<String, String> FILES_WITH_WRONG_EMAILS =
ImmutableMap.of("d1/" + OWNERS, "u1@g.com\n", "d2/" + OWNERS_ANDROID, "u2@g.com\n");
private static final ImmutableSet<String> EXPECTED_VERBOSE_DEFAULT =
ImmutableSet.of("MSG: checking d1/" + OWNERS, "MSG: owner: u1@g.com");
private static final ImmutableSet<String> EXPECTED_VERBOSE_ANDROID =
ImmutableSet.of("MSG: checking d2/" + OWNERS_ANDROID, "MSG: owner: u2@g.com");
@Test
public void checkWrongEmails() throws Exception {
CommitReceivedEvent event = makeCommitEvent("test", "default", FILES_WITH_WRONG_EMAILS);
assertThat(validate(event, true, ENABLED_CONFIG))
.containsExactlyElementsIn(EXPECTED_VERBOSE_DEFAULT);
}
@Test
public void checkAndroidOwners() throws Exception {
CommitReceivedEvent event = makeCommitEvent("test", "Android", FILES_WITH_WRONG_EMAILS);
assertThat(validate(event, true, ANDROID_CONFIG))
.containsExactlyElementsIn(EXPECTED_VERBOSE_ANDROID);
}
@Test
public void simpleIncludeTest() throws Exception {
addProjectFile("p1", "d2/owners", "wrong\nxyz\n");
addProjectFile("p2", "d2/owners", "x@g.com\nerr\ninclude ../d2/owners\n");
ImmutableMap<String, String> files =
ImmutableMap.of(
"d1/" + OWNERS,
"include ../d2/owners\ninclude /d2/owners\n"
+ "include p1:/d2/owners\ninclude p2:/d2/owners\n");
ImmutableSet<String> expected =
ImmutableSet.of(
"ERROR: unknown: x@g.com at p2:d2/owners:1",
"ERROR: syntax: p2:d2/owners:2: err",
"ERROR: syntax: d2/owners:1: wrong",
"ERROR: syntax: d2/owners:2: xyz");
CommitReceivedEvent event = makeCommitEvent("p1", "T", files);
assertThat(validate(event, false, ENABLED_CONFIG)).containsExactlyElementsIn(expected);
}
private static PluginConfig createEnabledConfig() {
PluginConfig c = new PluginConfig("", new Config());
c.setBoolean(REJECT_ERROR_IN_OWNERS, true);
return c;
}
private static PluginConfig createDisabledConfig() {
PluginConfig c = new PluginConfig("", new Config());
c.setBoolean(REJECT_ERROR_IN_OWNERS, false);
return c;
}
private static PluginConfig createAndroidConfig() {
PluginConfig c = createEnabledConfig();
c.setString(OWNERS_FILE_NAME, OWNERS_ANDROID);
return c;
}
private RevCommit makeCommit(RevWalk rw, String message, Map<String, String> fileStrings)
throws IOException, GitAPIException {
Map<File, byte[]> fileBytes = new HashMap<>();
for (String path : fileStrings.keySet()) {
fileBytes.put(
new File(repo.getDirectory().getParent(), path),
fileStrings.get(path).getBytes(StandardCharsets.UTF_8));
}
return makeCommit(rw, repo, message, fileBytes);
}
private static RevCommit makeCommit(
RevWalk rw, Repository repo, String message, Map<File, byte[]> files)
throws IOException, GitAPIException {
try (Git git = new Git(repo)) {
if (files != null) {
addFiles(git, files);
}
return rw.parseCommit(git.commit().setMessage(message).call());
}
}
private CommitReceivedEvent makeCommitEvent(
String project, String message, Map<String, String> fileStrings) throws Exception {
RevWalk rw = new RevWalk(repo);
return new MockedCommitReceivedEvent(project, rw, makeCommit(rw, message, fileStrings));
}
private List<String> validate(CommitReceivedEvent event, boolean verbose, PluginConfig cfg)
throws Exception {
OwnersValidator validator =
new OwnersValidator("find-owners", pluginConfig, repoManager, new MockedEmails());
OwnersValidator.Checker checker = validator.new Checker(event, verbose);
checker.check(OwnersValidator.getOwnersFileName(cfg));
return transformMessages(checker.messages);
}
private static String generateFilePattern(File f, Git git) {
return f.getAbsolutePath()
.replace(git.getRepository().getWorkTree().getAbsolutePath(), "")
.substring(1);
}
private static void addFiles(Git git, Map<File, byte[]> files)
throws IOException, GitAPIException {
AddCommand ac = git.add();
for (File f : files.keySet()) {
if (!f.exists()) {
FileUtils.touch(f);
}
if (files.get(f) != null) {
FileUtils.writeByteArrayToFile(f, files.get(f));
}
ac = ac.addFilepattern(generateFilePattern(f, git));
}
ac.call();
}
private static List<String> transformMessages(List<CommitValidationMessage> messages) {
return Lists.transform(
messages,
new Function<CommitValidationMessage, String>() {
@Override
public String apply(CommitValidationMessage input) {
String pre = input.isError() ? "ERROR: " : "MSG: ";
return pre + input.getMessage();
}
});
}
@Test
public void testTransformer() {
List<CommitValidationMessage> messages = new ArrayList<>();
messages.add(new CommitValidationMessage("a message", false));
messages.add(new CommitValidationMessage("an error", true));
Set<String> expected = ImmutableSet.of("ERROR: an error", "MSG: a message");
assertThat(transformMessages(messages)).containsExactlyElementsIn(expected);
}
}