blob: 1eef944c5eeb4dec72c88552fb48acd1c4b66d86 [file] [log] [blame]
// Copyright (C) 2022 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.server.change;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.server.notedb.ChangeNoteJson;
import com.google.gerrit.server.notedb.DeleteZombieCommentsRefs;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gson.JsonParser;
import com.google.inject.Inject;
import java.util.List;
import org.apache.commons.lang3.reflect.TypeLiteral;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.Before;
import org.junit.Test;
/** Test for {@link com.google.gerrit.server.notedb.DeleteZombieCommentsRefs}. */
public class DeleteZombieDraftIT extends AbstractDaemonTest {
private static final String TEST_PARAMETER_MARKER = "test_only_parameter";
@Inject private DeleteZombieCommentsRefs.Factory deleteZombieDraftsFactory;
@Inject private ChangeNoteJson changeNoteJson;
private boolean dryRun;
@ConfigSuite.Default
public static Config dryRunMode() {
Config config = new Config();
config.setBoolean(TEST_PARAMETER_MARKER, null, "dryRun", true);
return config;
}
@ConfigSuite.Config
public static Config deleteMode() {
Config config = new Config();
config.setBoolean(TEST_PARAMETER_MARKER, null, "dryRun", false);
return config;
}
@Before
public void setUp() throws Exception {
dryRun = baseConfig.getBoolean(TEST_PARAMETER_MARKER, "dryRun", true);
}
@Test
public void draftRefWithOneZombie() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
// Create a draft. A draft ref is created for this draft comment.
addDraft(changeId, revId, "comment 1");
Ref draftRef = getOnlyDraftRef();
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).hasSize(1);
// Publish the draft. The draft ref is deleted.
publishAllDrafts(r);
assertNumDrafts(changeId, 0);
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).isEmpty();
assertNumPublishedComments(changeId, 1);
// Restore the draft ref. Now the same comment exists as draft and published -> zombie.
restoreRef(draftRef.getName(), draftRef.getObjectId());
draftRef = getOnlyDraftRef();
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).hasSize(1);
// Run the cleanup logic. The zombie draft is cleared. The published comment is untouched.
DeleteZombieCommentsRefs worker =
deleteZombieDraftsFactory.create(/* cleanupPercentage= */ 100, dryRun);
assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(1);
if (dryRun) {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).hasSize(1);
} else {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).isEmpty();
}
assertNumPublishedComments(changeId, 1);
}
@Test
public void draftRefWithOneDraftAndOneZombie() throws Exception {
PushOneCommit.Result r1 = createChange();
String changeId = r1.getChangeId();
PushOneCommit.Result r2 = amendChange(changeId);
// Add two draft comments: one on PS1, the other on PS2
addDraft(changeId, r1.getCommit().getName(), "comment 1");
CommentInfo c2 = addDraft(changeId, r2.getCommit().getName(), "comment 2");
Ref draftRef = getOnlyDraftRef();
// Publish the draft on PS2. Now PS1 still has one draft, PS2 has no drafts
publishDraft(r2, c2.id);
assertNumDrafts(changeId, 1);
assertNumPublishedComments(changeId, 1);
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r1.getCommit().name())).hasSize(1);
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).isEmpty();
// Restore the draft ref for PS2 draft. Now draft on PS2 is zombie because it is also published.
restoreRef(draftRef.getName(), draftRef.getObjectId());
draftRef = getOnlyDraftRef();
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r1.getCommit().name())).hasSize(1);
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).hasSize(1);
// Run the zombie cleanup logic. Zombie draft ref for PS2 will be removed.
DeleteZombieCommentsRefs worker =
deleteZombieDraftsFactory.create(/* cleanupPercentage= */ 100, dryRun);
assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(1);
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r1.getCommit().name())).hasSize(1);
if (dryRun) {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).hasSize(1);
} else {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).isEmpty();
}
assertNumPublishedComments(changeId, 1);
// Re-run the worker: nothing happens.
assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(dryRun ? 1 : 0);
assertNumDrafts(changeId, 1);
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r1.getCommit().name())).hasSize(1);
if (dryRun) {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).hasSize(1);
} else {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).isEmpty();
}
assertNumPublishedComments(changeId, 1);
}
private Ref getOnlyDraftRef() throws Exception {
try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
return Iterables.getOnlyElement(
allUsersRepo.getRefDatabase().getRefsByPrefix(RefNames.REFS_DRAFT_COMMENTS));
}
}
private void publishAllDrafts(PushOneCommit.Result r) throws Exception {
ReviewInput reviewInput = new ReviewInput();
reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
reviewInput.message = "foo";
revision(r).review(reviewInput);
}
private void publishDraft(PushOneCommit.Result r, String draftId) throws Exception {
ReviewInput reviewInput = new ReviewInput();
reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
reviewInput.message = "foo";
reviewInput.draftIdsToPublish = ImmutableList.of(draftId);
revision(r).review(reviewInput);
}
private List<CommentInfo> getDraftComments(String changeId) throws Exception {
return gApi.changes().id(changeId).draftsRequest().getAsList();
}
private List<CommentInfo> getPublishedComments(String changeId) throws Exception {
return gApi.changes().id(changeId).commentsRequest().getAsList();
}
private CommentInfo addDraft(String changeId, String revId, String commentText) throws Exception {
DraftInput comment = CommentsUtil.newDraft("f1.txt", Side.REVISION, /* line= */ 1, commentText);
return gApi.changes().id(changeId).revision(revId).createDraft(comment).get();
}
private void restoreRef(String refName, ObjectId id) throws Exception {
try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
RefUpdate u = allUsersRepo.updateRef(refName);
u.setNewObjectId(id);
u.forceUpdate();
}
}
/**
* Returns all draft comments that are stored in {@code draftRefStr} for a specific revision
* (patchset) identified by its {@code blobFile} SHA-1.
*
* <p>Background: This ref points to a tree containing one or more blob files, each named after
* the patchset revision SHA-1, that is drafts for each patchset are stored in a separate blob
* file.
*/
private List<HumanComment> getDraftsByParsingDraftRef(String draftRefStr, String blobFile)
throws Exception {
try (Repository allUsersRepo = repoManager.openRepository(allUsers);
RevWalk rw = new RevWalk(allUsersRepo)) {
Ref draftRef = allUsersRepo.exactRef(draftRefStr);
if (draftRef == null) {
// draft ref does not exist, i.e. no draft comments stored for this ref.
return ImmutableList.of();
}
RevTree revTree = rw.parseTree(draftRef.getObjectId());
TreeWalk tw = TreeWalk.forPath(allUsersRepo, blobFile, revTree);
if (tw == null) {
// blobFile does not exist, i.e. no draft comments for this revision.
return ImmutableList.of();
}
ObjectLoader open = allUsersRepo.open(tw.getObjectId(0));
String content = new String(open.getBytes(), UTF_8);
List<HumanComment> drafts =
changeNoteJson
.getGson()
.fromJson(
JsonParser.parseString(content)
.getAsJsonObject()
.getAsJsonArray("comments")
.toString(),
new TypeLiteral<ImmutableList<HumanComment>>() {}.getType());
return drafts;
}
}
private void assertNumDrafts(String changeId, int num) throws Exception {
assertThat(getDraftComments(changeId)).hasSize(num);
}
private void assertNumPublishedComments(String changeId, int num) throws Exception {
assertThat(getPublishedComments(changeId)).hasSize(num);
}
}