| // Copyright (C) 2013 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.acceptance; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.common.truth.Truth.assertWithMessage; | |
| import static com.google.gerrit.acceptance.GitUtil.pushHead; | |
| import static org.junit.Assert.assertEquals; | |
| import com.google.common.base.Strings; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.Sets; | |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.common.UsedAt; | |
| import com.google.gerrit.common.UsedAt.Project; | |
| import com.google.gerrit.entities.Account; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.server.approval.ApprovalsUtil; | |
| import com.google.gerrit.server.notedb.ChangeNotes; | |
| import com.google.gerrit.server.notedb.ReviewerStateInternal; | |
| import com.google.gerrit.server.query.change.ChangeData; | |
| import com.google.gerrit.server.query.change.InternalChangeQuery; | |
| import com.google.inject.Provider; | |
| import com.google.inject.assistedinject.Assisted; | |
| import com.google.inject.assistedinject.AssistedInject; | |
| import java.util.Arrays; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.Map; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import org.eclipse.jgit.api.TagCommand; | |
| import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; | |
| import org.eclipse.jgit.dircache.DirCacheEntry; | |
| import org.eclipse.jgit.junit.TestRepository; | |
| import org.eclipse.jgit.lib.FileMode; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.PersonIdent; | |
| import org.eclipse.jgit.revwalk.RevBlob; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| import org.eclipse.jgit.transport.PushResult; | |
| import org.eclipse.jgit.transport.RemoteRefUpdate; | |
| import org.eclipse.jgit.transport.RemoteRefUpdate.Status; | |
| public class PushOneCommit { | |
| public static final String SUBJECT = "test commit"; | |
| public static final String FILE_NAME = "a.txt"; | |
| public static final String FILE_CONTENT = "some content"; | |
| public static final String PATCH_FILE_ONLY = | |
| "diff --git a/a.txt b/a.txt\n" | |
| + "new file mode 100644\n" | |
| + "index 0000000..f0eec86\n" | |
| + "--- /dev/null\n" | |
| + "+++ b/a.txt\n" | |
| + "@@ -0,0 +1 @@\n" | |
| + "+some content\n" | |
| + "\\ No newline at end of file\n"; | |
| public static final String PATCH = | |
| "From %s Mon Sep 17 00:00:00 2001\n" | |
| + "From: Administrator <admin@example.com>\n" | |
| + "Date: %s\n" | |
| + "Subject: [PATCH] test commit\n" | |
| + "\n" | |
| + "Change-Id: %s\n" | |
| + "---\n" | |
| + "\n" | |
| + PATCH_FILE_ONLY; | |
| public interface Factory { | |
| PushOneCommit create(PersonIdent i, TestRepository<?> testRepo); | |
| PushOneCommit create( | |
| PersonIdent i, TestRepository<?> testRepo, @Assisted("changeId") String changeId); | |
| PushOneCommit create( | |
| PersonIdent i, | |
| TestRepository<?> testRepo, | |
| @Assisted("subject") String subject, | |
| @Assisted("fileName") String fileName, | |
| @Assisted("content") String content); | |
| PushOneCommit create( | |
| PersonIdent i, | |
| TestRepository<?> testRepo, | |
| @Assisted String subject, | |
| @Assisted Map<String, String> files); | |
| @UsedAt(Project.PLUGIN_CODE_OWNERS) | |
| PushOneCommit create( | |
| PersonIdent i, | |
| TestRepository<?> testRepo, | |
| @Assisted("subject") String subject, | |
| @Assisted Map<String, String> files, | |
| @Assisted("changeId") String changeId); | |
| PushOneCommit create( | |
| PersonIdent i, | |
| TestRepository<?> testRepo, | |
| @Assisted("subject") String subject, | |
| @Assisted("fileName") String fileName, | |
| @Assisted("content") String content, | |
| @Assisted("changeId") String changeId); | |
| } | |
| public static class Tag { | |
| public String name; | |
| public Tag(String name) { | |
| this.name = name; | |
| } | |
| } | |
| public static class AnnotatedTag extends Tag { | |
| public String message; | |
| public PersonIdent tagger; | |
| public AnnotatedTag(String name, String message, PersonIdent tagger) { | |
| super(name); | |
| this.message = message; | |
| this.tagger = tagger; | |
| } | |
| } | |
| private static final AtomicInteger CHANGE_ID_COUNTER = new AtomicInteger(); | |
| private static String nextChangeId() { | |
| // Tests use a variety of mechanisms for setting temporary timestamps, so we can't guarantee | |
| // that the PersonIdent (or any other field used by the Change-Id generator) for any two test | |
| // methods in the same acceptance test class are going to be different. But tests generally | |
| // assume that Change-Ids are unique unless otherwise specified. So, don't even bother trying to | |
| // reuse JGit's Change-Id generator, just do the simplest possible thing and convert a counter | |
| // to hex. | |
| return String.format("%040x", CHANGE_ID_COUNTER.incrementAndGet()); | |
| } | |
| private final ChangeNotes.Factory notesFactory; | |
| private final ApprovalsUtil approvalsUtil; | |
| private final Provider<InternalChangeQuery> queryProvider; | |
| private final TestRepository<?> testRepo; | |
| private final String subject; | |
| private final Map<String, String> files; | |
| private String changeId; | |
| private Tag tag; | |
| private boolean force; | |
| private List<String> pushOptions; | |
| private final TestRepository<?>.CommitBuilder commitBuilder; | |
| @AssistedInject | |
| PushOneCommit( | |
| ChangeNotes.Factory notesFactory, | |
| ApprovalsUtil approvalsUtil, | |
| Provider<InternalChangeQuery> queryProvider, | |
| @Assisted PersonIdent i, | |
| @Assisted TestRepository<?> testRepo) | |
| throws Exception { | |
| this(notesFactory, approvalsUtil, queryProvider, i, testRepo, SUBJECT, FILE_NAME, FILE_CONTENT); | |
| } | |
| @AssistedInject | |
| PushOneCommit( | |
| ChangeNotes.Factory notesFactory, | |
| ApprovalsUtil approvalsUtil, | |
| Provider<InternalChangeQuery> queryProvider, | |
| @Assisted PersonIdent i, | |
| @Assisted TestRepository<?> testRepo, | |
| @Assisted("changeId") String changeId) | |
| throws Exception { | |
| this( | |
| notesFactory, | |
| approvalsUtil, | |
| queryProvider, | |
| i, | |
| testRepo, | |
| SUBJECT, | |
| FILE_NAME, | |
| FILE_CONTENT, | |
| changeId); | |
| } | |
| @AssistedInject | |
| PushOneCommit( | |
| ChangeNotes.Factory notesFactory, | |
| ApprovalsUtil approvalsUtil, | |
| Provider<InternalChangeQuery> queryProvider, | |
| @Assisted PersonIdent i, | |
| @Assisted TestRepository<?> testRepo, | |
| @Assisted("subject") String subject, | |
| @Assisted("fileName") String fileName, | |
| @Assisted("content") String content) | |
| throws Exception { | |
| this(notesFactory, approvalsUtil, queryProvider, i, testRepo, subject, fileName, content, null); | |
| } | |
| @AssistedInject | |
| PushOneCommit( | |
| ChangeNotes.Factory notesFactory, | |
| ApprovalsUtil approvalsUtil, | |
| Provider<InternalChangeQuery> queryProvider, | |
| @Assisted PersonIdent i, | |
| @Assisted TestRepository<?> testRepo, | |
| @Assisted String subject, | |
| @Assisted Map<String, String> files) | |
| throws Exception { | |
| this(notesFactory, approvalsUtil, queryProvider, i, testRepo, subject, files, null); | |
| } | |
| @AssistedInject | |
| PushOneCommit( | |
| ChangeNotes.Factory notesFactory, | |
| ApprovalsUtil approvalsUtil, | |
| Provider<InternalChangeQuery> queryProvider, | |
| @Assisted PersonIdent i, | |
| @Assisted TestRepository<?> testRepo, | |
| @Assisted("subject") String subject, | |
| @Assisted("fileName") String fileName, | |
| @Assisted("content") String content, | |
| @Nullable @Assisted("changeId") String changeId) | |
| throws Exception { | |
| this( | |
| notesFactory, | |
| approvalsUtil, | |
| queryProvider, | |
| i, | |
| testRepo, | |
| subject, | |
| ImmutableMap.of(fileName, content), | |
| changeId); | |
| } | |
| @AssistedInject | |
| PushOneCommit( | |
| ChangeNotes.Factory notesFactory, | |
| ApprovalsUtil approvalsUtil, | |
| Provider<InternalChangeQuery> queryProvider, | |
| @Assisted PersonIdent i, | |
| @Assisted TestRepository<?> testRepo, | |
| @Assisted("subject") String subject, | |
| @Assisted Map<String, String> files, | |
| @Nullable @Assisted("changeId") String changeId) | |
| throws Exception { | |
| this.testRepo = testRepo; | |
| this.notesFactory = notesFactory; | |
| this.approvalsUtil = approvalsUtil; | |
| this.queryProvider = queryProvider; | |
| this.subject = subject; | |
| this.files = files; | |
| this.changeId = changeId; | |
| if (changeId != null) { | |
| commitBuilder = testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1)); | |
| } else { | |
| if (subject.contains("\nChange-Id: ")) { | |
| commitBuilder = testRepo.amendRef("HEAD"); | |
| } else { | |
| commitBuilder = testRepo.branch("HEAD").commit().insertChangeId(nextChangeId()); | |
| } | |
| } | |
| commitBuilder.message(subject).author(i).committer(new PersonIdent(i, testRepo.getDate())); | |
| } | |
| public PushOneCommit setParents(List<RevCommit> parents) throws Exception { | |
| commitBuilder.noParents(); | |
| for (RevCommit p : parents) { | |
| commitBuilder.parent(p); | |
| } | |
| return this; | |
| } | |
| @CanIgnoreReturnValue | |
| public PushOneCommit setTopLevelTreeId(ObjectId treeId) throws Exception { | |
| commitBuilder.setTopLevelTree(treeId); | |
| return this; | |
| } | |
| public PushOneCommit setParent(RevCommit parent) throws Exception { | |
| commitBuilder.noParents(); | |
| commitBuilder.parent(parent); | |
| return this; | |
| } | |
| public PushOneCommit noParent() throws Exception { | |
| commitBuilder.noParents(); | |
| return this; | |
| } | |
| public PushOneCommit addFile(String path, String content, int fileMode) throws Exception { | |
| RevBlob blobId = testRepo.blob(content); | |
| commitBuilder.edit( | |
| new PathEdit(path) { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.fromBits(fileMode)); | |
| ent.setObjectId(blobId); | |
| } | |
| }); | |
| return this; | |
| } | |
| public PushOneCommit addSymlink(String path, String target) throws Exception { | |
| RevBlob blobId = testRepo.blob(target); | |
| commitBuilder.edit( | |
| new PathEdit(path) { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.SYMLINK); | |
| ent.setObjectId(blobId); | |
| } | |
| }); | |
| return this; | |
| } | |
| public PushOneCommit addGitSubmodule(String modulePath, ObjectId commitId) { | |
| commitBuilder.edit( | |
| new PathEdit(modulePath) { | |
| @Override | |
| public void apply(DirCacheEntry ent) { | |
| ent.setFileMode(FileMode.GITLINK); | |
| ent.setObjectId(commitId); | |
| } | |
| }); | |
| return this; | |
| } | |
| public PushOneCommit rmFile(String filename) { | |
| commitBuilder.rm(filename); | |
| return this; | |
| } | |
| public Result to(String ref) throws Exception { | |
| for (Map.Entry<String, String> e : files.entrySet()) { | |
| commitBuilder.add(e.getKey(), e.getValue()); | |
| } | |
| return execute(ref); | |
| } | |
| public Result rm(String ref) throws Exception { | |
| for (String fileName : files.keySet()) { | |
| commitBuilder.rm(fileName); | |
| } | |
| return execute(ref); | |
| } | |
| public Result execute(String ref) throws Exception { | |
| RevCommit c = commitBuilder.create(); | |
| if (changeId == null) { | |
| changeId = GitUtil.getChangeId(testRepo, c).get(); | |
| } | |
| if (tag != null) { | |
| TagCommand tagCommand = testRepo.git().tag().setName(tag.name); | |
| if (tag instanceof AnnotatedTag) { | |
| AnnotatedTag annotatedTag = (AnnotatedTag) tag; | |
| tagCommand | |
| .setAnnotated(true) | |
| .setMessage(annotatedTag.message) | |
| .setTagger(annotatedTag.tagger); | |
| } else { | |
| tagCommand.setAnnotated(false); | |
| } | |
| tagCommand.call(); | |
| } | |
| return new Result(ref, pushHead(testRepo, ref, tag != null, force, pushOptions), c, subject); | |
| } | |
| public void setTag(Tag tag) { | |
| this.tag = tag; | |
| } | |
| public void setForce(boolean force) { | |
| this.force = force; | |
| } | |
| public List<String> getPushOptions() { | |
| return pushOptions; | |
| } | |
| public void setPushOptions(List<String> pushOptions) { | |
| this.pushOptions = pushOptions; | |
| } | |
| public void noParents() { | |
| commitBuilder.noParents(); | |
| } | |
| public class Result { | |
| private final String ref; | |
| private final PushResult result; | |
| private final RevCommit commit; | |
| private final String resSubj; | |
| private Result(String ref, PushResult resSubj, RevCommit commit, String subject) { | |
| this.ref = ref; | |
| this.result = resSubj; | |
| this.commit = commit; | |
| this.resSubj = subject; | |
| } | |
| public ChangeData getChange() { | |
| return Iterables.getOnlyElement(queryProvider.get().byKeyPrefix(changeId)); | |
| } | |
| public PatchSet getPatchSet() { | |
| return getChange().currentPatchSet(); | |
| } | |
| public PatchSet.Id getPatchSetId() { | |
| return getChange().change().currentPatchSetId(); | |
| } | |
| public String getChangeId() { | |
| return changeId; | |
| } | |
| public RevCommit getCommit() { | |
| return commit; | |
| } | |
| public void assertPushOptions(List<String> pushOptions) { | |
| assertEquals(pushOptions, getPushOptions()); | |
| } | |
| public void assertChange( | |
| Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers) { | |
| assertChange( | |
| expectedStatus, expectedTopic, Arrays.asList(expectedReviewers), ImmutableList.of()); | |
| } | |
| public void assertChange( | |
| Change.Status expectedStatus, | |
| String expectedTopic, | |
| List<TestAccount> expectedReviewers, | |
| List<TestAccount> expectedCcs) { | |
| Change c = getChange().change(); | |
| assertThat(c.getSubject()).isEqualTo(resSubj); | |
| assertThat(c.getStatus()).isEqualTo(expectedStatus); | |
| assertThat(Strings.emptyToNull(c.getTopic())).isEqualTo(expectedTopic); | |
| assertReviewers(c, ReviewerStateInternal.REVIEWER, expectedReviewers); | |
| assertReviewers(c, ReviewerStateInternal.CC, expectedCcs); | |
| } | |
| private void assertReviewers( | |
| Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers) { | |
| Iterable<Account.Id> actualIds = | |
| approvalsUtil.getReviewers(notesFactory.createChecked(c)).byState(state); | |
| assertThat(actualIds) | |
| .containsExactlyElementsIn(Sets.newHashSet(TestAccount.ids(expectedReviewers))); | |
| } | |
| public void assertOkStatus() { | |
| assertStatus(Status.OK, null); | |
| } | |
| public void assertErrorStatus(String expectedMessage) { | |
| assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage); | |
| } | |
| public void assertErrorStatus() { | |
| RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref); | |
| assertThat(refUpdate).isNotNull(); | |
| assertWithMessage(message(refUpdate)) | |
| .that(refUpdate.getStatus()) | |
| .isEqualTo(Status.REJECTED_OTHER_REASON); | |
| } | |
| private void assertStatus(Status expectedStatus, String expectedMessage) { | |
| RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref); | |
| assertThat(refUpdate).isNotNull(); | |
| assertWithMessage(message(refUpdate)).that(refUpdate.getStatus()).isEqualTo(expectedStatus); | |
| if (expectedMessage == null) { | |
| assertThat(refUpdate.getMessage()).isNull(); | |
| } else { | |
| assertThat(refUpdate.getMessage()).contains(expectedMessage); | |
| } | |
| } | |
| public void assertMessage(String expectedMessage) { | |
| RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref); | |
| assertThat(refUpdate).isNotNull(); | |
| assertThat(message(refUpdate).toLowerCase(Locale.US)) | |
| .contains(expectedMessage.toLowerCase(Locale.US)); | |
| } | |
| public void assertNotMessage(String message) { | |
| RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref); | |
| assertThat(message(refUpdate).toLowerCase(Locale.US)) | |
| .doesNotContain(message.toLowerCase(Locale.US)); | |
| } | |
| public String getMessage() { | |
| RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref); | |
| assertThat(refUpdate).isNotNull(); | |
| return message(refUpdate); | |
| } | |
| private String message(RemoteRefUpdate refUpdate) { | |
| StringBuilder b = new StringBuilder(); | |
| if (refUpdate.getMessage() != null) { | |
| b.append(refUpdate.getMessage()); | |
| b.append("\n"); | |
| } | |
| b.append(result.getMessages()); | |
| return b.toString(); | |
| } | |
| } | |
| } |