| // 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.PushOneCommit; |
| import com.google.gerrit.acceptance.TestPlugin; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.extensions.api.changes.SubmitInput; |
| 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 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.Optional; |
| 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 FindOwners { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| @Rule public Watcher watcher = new Watcher(logger); |
| |
| 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 = Project.builder(Project.nameKey(project)).build(); |
| this.revWalk = revWalk; |
| this.commit = commit; |
| this.refName = "master"; |
| } |
| } |
| |
| private static class MockedEmails extends Emails { |
| Set<String> registered; |
| |
| MockedEmails() { |
| super(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, 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 = PluginConfig.create("", new Config(), null); |
| private static final PluginConfig ENABLED_CONFIG = createEnabledConfig(); // use OWNERS |
| private static final PluginConfig DISABLED_CONFIG = createDisabledConfig(); |
| |
| private OwnersValidator newOwnersValidator(PluginConfig cfg) { |
| return new OwnersValidator(cfg, accountCache, patchListCache, repoManager, new MockedEmails()); |
| } |
| |
| @Test |
| public void chekIsActiveAndFileName() throws Exception { |
| // This check should be enabled in project.config, default is not active. |
| assertThat(newOwnersValidator(EMPTY_CONFIG).isActive()).isFalse(); |
| assertThat(newOwnersValidator(ENABLED_CONFIG).isActive()).isTrue(); |
| assertThat(newOwnersValidator(ANDROID_CONFIG).isActive()).isTrue(); |
| assertThat(newOwnersValidator(DISABLED_CONFIG).isActive()).isFalse(); |
| // Default file name is "OWNERS". |
| assertThat(newOwnersValidator(EMPTY_CONFIG).getOwnersFileName()).isEqualTo(OWNERS); |
| assertThat(newOwnersValidator(ENABLED_CONFIG).getOwnersFileName()).isEqualTo(OWNERS); |
| assertThat(newOwnersValidator(DISABLED_CONFIG).getOwnersFileName()).isEqualTo(OWNERS); |
| assertThat(newOwnersValidator(ANDROID_CONFIG).getOwnersFileName()).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); |
| } |
| |
| @Test |
| public void simpleIncludeACLTest() throws Exception { |
| // Even if the user cannot read an included file, |
| // the upload validator should still check the included file. |
| // Use pA/pB, because addProjectFile cannot create the same project again. |
| addProjectFile("pA", "d2/owners", "wrong\nxyz\n"); |
| addProjectFile("pB", "d2/owners", "x@g.com\nerr\ninclude ../d2/owners\n"); |
| blockRead(project); // current project is pB; set it to not readable |
| ImmutableMap<String, String> files = |
| ImmutableMap.of( |
| "d1/" + OWNERS, |
| "include ../d2/owners\ninclude /d2/owners\n" |
| + "include pA:/d2/owners\ninclude pB:/d2/owners\n"); |
| ImmutableSet<String> expected = |
| ImmutableSet.of( |
| // "MSG: unchecked: d1/OWNERS:4: include pB:/d2/owners", // cannot read pB |
| "ERROR: unknown: x@g.com at pB:d2/owners:1", |
| "ERROR: syntax: pB:d2/owners:2: err", |
| "ERROR: syntax: d2/owners:1: wrong", |
| "ERROR: syntax: d2/owners:2: xyz"); |
| CommitReceivedEvent event = makeCommitEvent("pA", "T", files); |
| assertThat(validate(event, false, ENABLED_CONFIG)).containsExactlyElementsIn(expected); |
| } |
| |
| private static PluginConfig createEnabledConfig() { |
| PluginConfig.Update update = new PluginConfig.Update("", new Config(), Optional.empty()); |
| update.setBoolean(REJECT_ERROR_IN_OWNERS, true); |
| return update.asPluginConfig(); |
| } |
| |
| private static PluginConfig createDisabledConfig() { |
| PluginConfig.Update update = new PluginConfig.Update("", new Config(), Optional.empty()); |
| update.setBoolean(REJECT_ERROR_IN_OWNERS, false); |
| return update.asPluginConfig(); |
| } |
| |
| private static PluginConfig createAndroidConfig() { |
| PluginConfig.Update update = new PluginConfig.Update("", new Config(), Optional.empty()); |
| update.setBoolean(REJECT_ERROR_IN_OWNERS, true); |
| update.setString(OWNERS_FILE_NAME, OWNERS_ANDROID); |
| return update.asPluginConfig(); |
| } |
| |
| 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 = newOwnersValidator(cfg); |
| OwnersValidator.Checker checker = validator.new Checker(event, verbose); |
| checker.check(validator.getOwnersFileName()); |
| 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); |
| } |
| } |