Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.acceptance;
16 :
17 : import static com.google.common.truth.Truth.assertThat;
18 : import static com.google.common.truth.Truth.assertWithMessage;
19 : import static com.google.gerrit.acceptance.GitUtil.pushHead;
20 : import static org.junit.Assert.assertEquals;
21 :
22 : import com.google.common.base.Strings;
23 : import com.google.common.collect.ImmutableList;
24 : import com.google.common.collect.ImmutableMap;
25 : import com.google.common.collect.Iterables;
26 : import com.google.common.collect.Sets;
27 : import com.google.errorprone.annotations.CanIgnoreReturnValue;
28 : import com.google.gerrit.common.Nullable;
29 : import com.google.gerrit.common.UsedAt;
30 : import com.google.gerrit.common.UsedAt.Project;
31 : import com.google.gerrit.entities.Account;
32 : import com.google.gerrit.entities.Change;
33 : import com.google.gerrit.entities.PatchSet;
34 : import com.google.gerrit.server.approval.ApprovalsUtil;
35 : import com.google.gerrit.server.notedb.ChangeNotes;
36 : import com.google.gerrit.server.notedb.ReviewerStateInternal;
37 : import com.google.gerrit.server.query.change.ChangeData;
38 : import com.google.gerrit.server.query.change.InternalChangeQuery;
39 : import com.google.inject.Provider;
40 : import com.google.inject.assistedinject.Assisted;
41 : import com.google.inject.assistedinject.AssistedInject;
42 : import java.util.Arrays;
43 : import java.util.List;
44 : import java.util.Map;
45 : import java.util.concurrent.atomic.AtomicInteger;
46 : import org.eclipse.jgit.api.TagCommand;
47 : import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
48 : import org.eclipse.jgit.dircache.DirCacheEntry;
49 : import org.eclipse.jgit.junit.TestRepository;
50 : import org.eclipse.jgit.lib.FileMode;
51 : import org.eclipse.jgit.lib.ObjectId;
52 : import org.eclipse.jgit.lib.PersonIdent;
53 : import org.eclipse.jgit.revwalk.RevBlob;
54 : import org.eclipse.jgit.revwalk.RevCommit;
55 : import org.eclipse.jgit.transport.PushResult;
56 : import org.eclipse.jgit.transport.RemoteRefUpdate;
57 : import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
58 :
59 : public class PushOneCommit {
60 : public static final String SUBJECT = "test commit";
61 : public static final String FILE_NAME = "a.txt";
62 : public static final String FILE_CONTENT = "some content";
63 : public static final String PATCH_FILE_ONLY =
64 : "diff --git a/a.txt b/a.txt\n"
65 : + "new file mode 100644\n"
66 : + "index 0000000..f0eec86\n"
67 : + "--- /dev/null\n"
68 : + "+++ b/a.txt\n"
69 : + "@@ -0,0 +1 @@\n"
70 : + "+some content\n"
71 : + "\\ No newline at end of file\n";
72 : public static final String PATCH =
73 : "From %s Mon Sep 17 00:00:00 2001\n"
74 : + "From: Administrator <admin@example.com>\n"
75 : + "Date: %s\n"
76 : + "Subject: [PATCH] test commit\n"
77 : + "\n"
78 : + "Change-Id: %s\n"
79 : + "---\n"
80 : + "\n"
81 : + PATCH_FILE_ONLY;
82 :
83 : public interface Factory {
84 : PushOneCommit create(PersonIdent i, TestRepository<?> testRepo);
85 :
86 : PushOneCommit create(
87 : PersonIdent i, TestRepository<?> testRepo, @Assisted("changeId") String changeId);
88 :
89 : PushOneCommit create(
90 : PersonIdent i,
91 : TestRepository<?> testRepo,
92 : @Assisted("subject") String subject,
93 : @Assisted("fileName") String fileName,
94 : @Assisted("content") String content);
95 :
96 : PushOneCommit create(
97 : PersonIdent i,
98 : TestRepository<?> testRepo,
99 : @Assisted String subject,
100 : @Assisted Map<String, String> files);
101 :
102 : @UsedAt(Project.PLUGIN_CODE_OWNERS)
103 : PushOneCommit create(
104 : PersonIdent i,
105 : TestRepository<?> testRepo,
106 : @Assisted("subject") String subject,
107 : @Assisted Map<String, String> files,
108 : @Assisted("changeId") String changeId);
109 :
110 : PushOneCommit create(
111 : PersonIdent i,
112 : TestRepository<?> testRepo,
113 : @Assisted("subject") String subject,
114 : @Assisted("fileName") String fileName,
115 : @Assisted("content") String content,
116 : @Assisted("changeId") String changeId);
117 : }
118 :
119 : public static class Tag {
120 : public String name;
121 :
122 0 : public Tag(String name) {
123 0 : this.name = name;
124 0 : }
125 : }
126 :
127 : public static class AnnotatedTag extends Tag {
128 : public String message;
129 : public PersonIdent tagger;
130 :
131 : public AnnotatedTag(String name, String message, PersonIdent tagger) {
132 0 : super(name);
133 0 : this.message = message;
134 0 : this.tagger = tagger;
135 0 : }
136 : }
137 :
138 88 : private static final AtomicInteger CHANGE_ID_COUNTER = new AtomicInteger();
139 :
140 : private static String nextChangeId() {
141 : // Tests use a variety of mechanisms for setting temporary timestamps, so we can't guarantee
142 : // that the PersonIdent (or any other field used by the Change-Id generator) for any two test
143 : // methods in the same acceptance test class are going to be different. But tests generally
144 : // assume that Change-Ids are unique unless otherwise specified. So, don't even bother trying to
145 : // reuse JGit's Change-Id generator, just do the simplest possible thing and convert a counter
146 : // to hex.
147 88 : return String.format("%040x", CHANGE_ID_COUNTER.incrementAndGet());
148 : }
149 :
150 : private final ChangeNotes.Factory notesFactory;
151 : private final ApprovalsUtil approvalsUtil;
152 : private final Provider<InternalChangeQuery> queryProvider;
153 : private final TestRepository<?> testRepo;
154 :
155 : private final String subject;
156 : private final Map<String, String> files;
157 : private String changeId;
158 : private Tag tag;
159 : private boolean force;
160 : private List<String> pushOptions;
161 :
162 : private final TestRepository<?>.CommitBuilder commitBuilder;
163 :
164 : @AssistedInject
165 : PushOneCommit(
166 : ChangeNotes.Factory notesFactory,
167 : ApprovalsUtil approvalsUtil,
168 : Provider<InternalChangeQuery> queryProvider,
169 : @Assisted PersonIdent i,
170 : @Assisted TestRepository<?> testRepo)
171 : throws Exception {
172 71 : this(notesFactory, approvalsUtil, queryProvider, i, testRepo, SUBJECT, FILE_NAME, FILE_CONTENT);
173 71 : }
174 :
175 : @AssistedInject
176 : PushOneCommit(
177 : ChangeNotes.Factory notesFactory,
178 : ApprovalsUtil approvalsUtil,
179 : Provider<InternalChangeQuery> queryProvider,
180 : @Assisted PersonIdent i,
181 : @Assisted TestRepository<?> testRepo,
182 : @Assisted("changeId") String changeId)
183 : throws Exception {
184 6 : this(
185 : notesFactory,
186 : approvalsUtil,
187 : queryProvider,
188 : i,
189 : testRepo,
190 : SUBJECT,
191 : FILE_NAME,
192 : FILE_CONTENT,
193 : changeId);
194 6 : }
195 :
196 : @AssistedInject
197 : PushOneCommit(
198 : ChangeNotes.Factory notesFactory,
199 : ApprovalsUtil approvalsUtil,
200 : Provider<InternalChangeQuery> queryProvider,
201 : @Assisted PersonIdent i,
202 : @Assisted TestRepository<?> testRepo,
203 : @Assisted("subject") String subject,
204 : @Assisted("fileName") String fileName,
205 : @Assisted("content") String content)
206 : throws Exception {
207 83 : this(notesFactory, approvalsUtil, queryProvider, i, testRepo, subject, fileName, content, null);
208 83 : }
209 :
210 : @AssistedInject
211 : PushOneCommit(
212 : ChangeNotes.Factory notesFactory,
213 : ApprovalsUtil approvalsUtil,
214 : Provider<InternalChangeQuery> queryProvider,
215 : @Assisted PersonIdent i,
216 : @Assisted TestRepository<?> testRepo,
217 : @Assisted String subject,
218 : @Assisted Map<String, String> files)
219 : throws Exception {
220 22 : this(notesFactory, approvalsUtil, queryProvider, i, testRepo, subject, files, null);
221 22 : }
222 :
223 : @AssistedInject
224 : PushOneCommit(
225 : ChangeNotes.Factory notesFactory,
226 : ApprovalsUtil approvalsUtil,
227 : Provider<InternalChangeQuery> queryProvider,
228 : @Assisted PersonIdent i,
229 : @Assisted TestRepository<?> testRepo,
230 : @Assisted("subject") String subject,
231 : @Assisted("fileName") String fileName,
232 : @Assisted("content") String content,
233 : @Nullable @Assisted("changeId") String changeId)
234 : throws Exception {
235 84 : this(
236 : notesFactory,
237 : approvalsUtil,
238 : queryProvider,
239 : i,
240 : testRepo,
241 : subject,
242 84 : ImmutableMap.of(fileName, content),
243 : changeId);
244 84 : }
245 :
246 : @AssistedInject
247 : PushOneCommit(
248 : ChangeNotes.Factory notesFactory,
249 : ApprovalsUtil approvalsUtil,
250 : Provider<InternalChangeQuery> queryProvider,
251 : @Assisted PersonIdent i,
252 : @Assisted TestRepository<?> testRepo,
253 : @Assisted("subject") String subject,
254 : @Assisted Map<String, String> files,
255 : @Nullable @Assisted("changeId") String changeId)
256 88 : throws Exception {
257 88 : this.testRepo = testRepo;
258 88 : this.notesFactory = notesFactory;
259 88 : this.approvalsUtil = approvalsUtil;
260 88 : this.queryProvider = queryProvider;
261 88 : this.subject = subject;
262 88 : this.files = files;
263 88 : this.changeId = changeId;
264 88 : if (changeId != null) {
265 37 : commitBuilder = testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
266 : } else {
267 88 : if (subject.contains("\nChange-Id: ")) {
268 1 : commitBuilder = testRepo.amendRef("HEAD");
269 : } else {
270 88 : commitBuilder = testRepo.branch("HEAD").commit().insertChangeId(nextChangeId());
271 : }
272 : }
273 88 : commitBuilder.message(subject).author(i).committer(new PersonIdent(i, testRepo.getDate()));
274 88 : }
275 :
276 : public PushOneCommit setParents(List<RevCommit> parents) throws Exception {
277 21 : commitBuilder.noParents();
278 21 : for (RevCommit p : parents) {
279 19 : commitBuilder.parent(p);
280 19 : }
281 21 : return this;
282 : }
283 :
284 : @CanIgnoreReturnValue
285 : public PushOneCommit setTopLevelTreeId(ObjectId treeId) throws Exception {
286 1 : commitBuilder.setTopLevelTree(treeId);
287 1 : return this;
288 : }
289 :
290 : public PushOneCommit setParent(RevCommit parent) throws Exception {
291 3 : commitBuilder.noParents();
292 3 : commitBuilder.parent(parent);
293 3 : return this;
294 : }
295 :
296 : public PushOneCommit noParent() throws Exception {
297 2 : commitBuilder.noParents();
298 2 : return this;
299 : }
300 :
301 : public PushOneCommit addFile(String path, String content, int fileMode) throws Exception {
302 2 : RevBlob blobId = testRepo.blob(content);
303 2 : commitBuilder.edit(
304 2 : new PathEdit(path) {
305 : @Override
306 : public void apply(DirCacheEntry ent) {
307 2 : ent.setFileMode(FileMode.fromBits(fileMode));
308 2 : ent.setObjectId(blobId);
309 2 : }
310 : });
311 2 : return this;
312 : }
313 :
314 : public PushOneCommit addSymlink(String path, String target) throws Exception {
315 2 : RevBlob blobId = testRepo.blob(target);
316 2 : commitBuilder.edit(
317 2 : new PathEdit(path) {
318 : @Override
319 : public void apply(DirCacheEntry ent) {
320 2 : ent.setFileMode(FileMode.SYMLINK);
321 2 : ent.setObjectId(blobId);
322 2 : }
323 : });
324 2 : return this;
325 : }
326 :
327 : public PushOneCommit addGitSubmodule(String modulePath, ObjectId commitId) {
328 2 : commitBuilder.edit(
329 2 : new PathEdit(modulePath) {
330 : @Override
331 : public void apply(DirCacheEntry ent) {
332 2 : ent.setFileMode(FileMode.GITLINK);
333 2 : ent.setObjectId(commitId);
334 2 : }
335 : });
336 2 : return this;
337 : }
338 :
339 : public PushOneCommit rmFile(String filename) {
340 3 : commitBuilder.rm(filename);
341 3 : return this;
342 : }
343 :
344 : public Result to(String ref) throws Exception {
345 88 : for (Map.Entry<String, String> e : files.entrySet()) {
346 88 : commitBuilder.add(e.getKey(), e.getValue());
347 88 : }
348 88 : return execute(ref);
349 : }
350 :
351 : public Result rm(String ref) throws Exception {
352 5 : for (String fileName : files.keySet()) {
353 5 : commitBuilder.rm(fileName);
354 5 : }
355 5 : return execute(ref);
356 : }
357 :
358 : public Result execute(String ref) throws Exception {
359 88 : RevCommit c = commitBuilder.create();
360 88 : if (changeId == null) {
361 88 : changeId = GitUtil.getChangeId(testRepo, c).get();
362 : }
363 88 : if (tag != null) {
364 0 : TagCommand tagCommand = testRepo.git().tag().setName(tag.name);
365 0 : if (tag instanceof AnnotatedTag) {
366 0 : AnnotatedTag annotatedTag = (AnnotatedTag) tag;
367 0 : tagCommand
368 0 : .setAnnotated(true)
369 0 : .setMessage(annotatedTag.message)
370 0 : .setTagger(annotatedTag.tagger);
371 0 : } else {
372 0 : tagCommand.setAnnotated(false);
373 : }
374 0 : tagCommand.call();
375 : }
376 88 : return new Result(ref, pushHead(testRepo, ref, tag != null, force, pushOptions), c, subject);
377 : }
378 :
379 : public void setTag(Tag tag) {
380 0 : this.tag = tag;
381 0 : }
382 :
383 : public void setForce(boolean force) {
384 8 : this.force = force;
385 8 : }
386 :
387 : public List<String> getPushOptions() {
388 3 : return pushOptions;
389 : }
390 :
391 : public void setPushOptions(List<String> pushOptions) {
392 4 : this.pushOptions = pushOptions;
393 4 : }
394 :
395 : public void noParents() {
396 2 : commitBuilder.noParents();
397 2 : }
398 :
399 : public class Result {
400 : private final String ref;
401 : private final PushResult result;
402 : private final RevCommit commit;
403 : private final String resSubj;
404 :
405 88 : private Result(String ref, PushResult resSubj, RevCommit commit, String subject) {
406 88 : this.ref = ref;
407 88 : this.result = resSubj;
408 88 : this.commit = commit;
409 88 : this.resSubj = subject;
410 88 : }
411 :
412 : public ChangeData getChange() {
413 53 : return Iterables.getOnlyElement(queryProvider.get().byKeyPrefix(changeId));
414 : }
415 :
416 : public PatchSet getPatchSet() {
417 8 : return getChange().currentPatchSet();
418 : }
419 :
420 : public PatchSet.Id getPatchSetId() {
421 16 : return getChange().change().currentPatchSetId();
422 : }
423 :
424 : public String getChangeId() {
425 73 : return changeId;
426 : }
427 :
428 : public RevCommit getCommit() {
429 54 : return commit;
430 : }
431 :
432 : public void assertPushOptions(List<String> pushOptions) {
433 3 : assertEquals(pushOptions, getPushOptions());
434 3 : }
435 :
436 : public void assertChange(
437 : Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers) {
438 11 : assertChange(
439 11 : expectedStatus, expectedTopic, Arrays.asList(expectedReviewers), ImmutableList.of());
440 11 : }
441 :
442 : public void assertChange(
443 : Change.Status expectedStatus,
444 : String expectedTopic,
445 : List<TestAccount> expectedReviewers,
446 : List<TestAccount> expectedCcs) {
447 11 : Change c = getChange().change();
448 11 : assertThat(c.getSubject()).isEqualTo(resSubj);
449 11 : assertThat(c.getStatus()).isEqualTo(expectedStatus);
450 11 : assertThat(Strings.emptyToNull(c.getTopic())).isEqualTo(expectedTopic);
451 11 : assertReviewers(c, ReviewerStateInternal.REVIEWER, expectedReviewers);
452 11 : assertReviewers(c, ReviewerStateInternal.CC, expectedCcs);
453 11 : }
454 :
455 : private void assertReviewers(
456 : Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers) {
457 11 : Iterable<Account.Id> actualIds =
458 11 : approvalsUtil.getReviewers(notesFactory.createChecked(c)).byState(state);
459 11 : assertThat(actualIds)
460 11 : .containsExactlyElementsIn(Sets.newHashSet(TestAccount.ids(expectedReviewers)));
461 11 : }
462 :
463 : public void assertOkStatus() {
464 78 : assertStatus(Status.OK, null);
465 78 : }
466 :
467 : public void assertErrorStatus(String expectedMessage) {
468 15 : assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage);
469 15 : }
470 :
471 : public void assertErrorStatus() {
472 10 : RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
473 10 : assertThat(refUpdate).isNotNull();
474 10 : assertWithMessage(message(refUpdate))
475 10 : .that(refUpdate.getStatus())
476 10 : .isEqualTo(Status.REJECTED_OTHER_REASON);
477 10 : }
478 :
479 : private void assertStatus(Status expectedStatus, String expectedMessage) {
480 78 : RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
481 78 : assertThat(refUpdate).isNotNull();
482 78 : assertWithMessage(message(refUpdate)).that(refUpdate.getStatus()).isEqualTo(expectedStatus);
483 78 : if (expectedMessage == null) {
484 78 : assertThat(refUpdate.getMessage()).isNull();
485 : } else {
486 15 : assertThat(refUpdate.getMessage()).contains(expectedMessage);
487 : }
488 78 : }
489 :
490 : public void assertMessage(String expectedMessage) {
491 11 : RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
492 11 : assertThat(refUpdate).isNotNull();
493 11 : assertThat(message(refUpdate).toLowerCase()).contains(expectedMessage.toLowerCase());
494 11 : }
495 :
496 : public void assertNotMessage(String message) {
497 4 : RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
498 4 : assertThat(message(refUpdate).toLowerCase()).doesNotContain(message.toLowerCase());
499 4 : }
500 :
501 : public String getMessage() {
502 4 : RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
503 4 : assertThat(refUpdate).isNotNull();
504 4 : return message(refUpdate);
505 : }
506 :
507 : private String message(RemoteRefUpdate refUpdate) {
508 79 : StringBuilder b = new StringBuilder();
509 79 : if (refUpdate.getMessage() != null) {
510 18 : b.append(refUpdate.getMessage());
511 18 : b.append("\n");
512 : }
513 79 : b.append(result.getMessages());
514 79 : return b.toString();
515 : }
516 : }
517 : }
|