com.google.gerrit.acceptance.server.notedb.ChangeRebuilderIT.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.acceptance.server.notedb.ChangeRebuilderIT.java

Source

// Copyright (C) 2016 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.notedb;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;

import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchLineCommentsUtil;
import com.google.gerrit.server.change.PostReview;
import com.google.gerrit.server.change.Rebuild;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.notedb.ChangeBundle;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NoteDbChangeState;
import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
import com.google.gerrit.testutil.NoteDbChecker;
import com.google.gerrit.testutil.NoteDbMode;
import com.google.gerrit.testutil.TestChanges;
import com.google.gerrit.testutil.TestTimeUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;

import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.sql.Timestamp;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class ChangeRebuilderIT extends AbstractDaemonTest {
    @Inject
    private AllUsersName allUsers;

    @Inject
    private NoteDbChecker checker;

    @Inject
    private Rebuild rebuildHandler;

    @Inject
    private Provider<ReviewDb> dbProvider;

    @Inject
    private PatchLineCommentsUtil plcUtil;

    @Inject
    private Provider<PostReview> postReview;

    @Before
    public void setUp() {
        assume().that(NoteDbMode.readWrite()).isFalse();
        TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
        notesMigration.setAllEnabled(false);
    }

    @After
    public void tearDown() {
        TestTimeUtil.useSystemTime();
    }

    @Test
    public void changeFields() throws Exception {
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        gApi.changes().id(id.get()).topic(name("a-topic"));
        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void patchSets() throws Exception {
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        r = amendChange(r.getChangeId());
        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void publishedComment() throws Exception {
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        putComment(user, id, 1, "comment");
        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void patchSetWithNullGroups() throws Exception {
        Timestamp ts = TimeUtil.nowTs();
        @SuppressWarnings("deprecation")
        Change c = TestChanges.newChange(project, user.getId(), db.nextChangeId());
        c.setCreatedOn(ts);
        c.setLastUpdatedOn(ts);
        PatchSet ps = TestChanges.newPatchSet(c.currentPatchSetId(), "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
                user.getId());
        ps.setCreatedOn(ts);
        db.changes().insert(Collections.singleton(c));
        db.patchSets().insert(Collections.singleton(ps));

        assertThat(ps.getGroups()).isEmpty();
        checker.rebuildAndCheckChanges(c.getId());
    }

    @Test
    public void draftComment() throws Exception {
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        putDraft(user, id, 1, "comment");
        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void draftAndPublishedComment() throws Exception {
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        putDraft(user, id, 1, "draft comment");
        putComment(user, id, 1, "published comment");
        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void publishDraftComment() throws Exception {
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        putDraft(user, id, 1, "draft comment");
        publishDrafts(user, id);
        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void nullAccountId() throws Exception {
        PushOneCommit.Result r = createChange();
        PatchSet.Id psId = r.getPatchSetId();
        Change.Id id = psId.getParentKey();

        // Events need to be otherwise identical for the account ID to be compared.
        ChangeMessage msg1 = insertMessage(id, psId, user.getId(), TimeUtil.nowTs(), "message 1");
        insertMessage(id, psId, null, msg1.getWrittenOn(), "message 2");

        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void nullPatchSetId() throws Exception {
        PushOneCommit.Result r = createChange();
        PatchSet.Id psId1 = r.getPatchSetId();
        Change.Id id = psId1.getParentKey();

        // Events need to be otherwise identical for the PatchSet.ID to be compared.
        ChangeMessage msg1 = insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 1");
        insertMessage(id, null, user.getId(), msg1.getWrittenOn(), "message 2");

        PatchSet.Id psId2 = amendChange(r.getChangeId()).getPatchSetId();

        ChangeMessage msg3 = insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 3");
        insertMessage(id, null, user.getId(), msg3.getWrittenOn(), "message 4");

        checker.rebuildAndCheckChanges(id);

        notesMigration.setWriteChanges(true);
        notesMigration.setReadChanges(true);

        ChangeNotes notes = notesFactory.create(db, project, id);
        Map<String, PatchSet.Id> psIds = new HashMap<>();
        for (ChangeMessage msg : notes.getChangeMessages()) {
            PatchSet.Id psId = msg.getPatchSetId();
            assertThat(psId).named("patchset for " + msg).isNotNull();
            psIds.put(msg.getMessage(), psId);
        }
        // Patch set IDs were replaced during conversion process.
        assertThat(psIds).containsEntry("message 1", psId1);
        assertThat(psIds).containsEntry("message 2", psId1);
        assertThat(psIds).containsEntry("message 3", psId2);
        assertThat(psIds).containsEntry("message 4", psId2);
    }

    @Test
    public void noWriteToNewRef() throws Exception {
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        checker.assertNoChangeRef(project, id);

        notesMigration.setWriteChanges(true);
        gApi.changes().id(id.get()).topic(name("a-topic"));

        // First write doesn't create the ref, but rebuilding works.
        checker.assertNoChangeRef(project, id);
        assertThat(unwrapDb().changes().get(id).getNoteDbState()).isNull();
        checker.rebuildAndCheckChanges(id);

        // Now that there is a ref, writes are "turned on" for this change, and
        // NoteDb stays up to date without explicit rebuilding.
        gApi.changes().id(id.get()).topic(name("new-topic"));
        assertThat(unwrapDb().changes().get(id).getNoteDbState()).isNotNull();
        checker.checkChanges(id);
    }

    @Test
    public void restApiNotFoundWhenNoteDbDisabled() throws Exception {
        PushOneCommit.Result r = createChange();
        exception.expect(ResourceNotFoundException.class);
        rebuildHandler.apply(parseChangeResource(r.getChangeId()), new Rebuild.Input());
    }

    @Test
    public void rebuildViaRestApi() throws Exception {
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        notesMigration.setWriteChanges(true);

        checker.assertNoChangeRef(project, id);
        rebuildHandler.apply(parseChangeResource(r.getChangeId()), new Rebuild.Input());
        checker.checkChanges(id);
    }

    @Test
    public void writeToNewRefForNewChange() throws Exception {
        PushOneCommit.Result r1 = createChange();
        Change.Id id1 = r1.getPatchSetId().getParentKey();

        notesMigration.setWriteChanges(true);
        gApi.changes().id(id1.get()).topic(name("a-topic"));
        PushOneCommit.Result r2 = createChange();
        Change.Id id2 = r2.getPatchSetId().getParentKey();

        // Second change was created after NoteDb writes were turned on, so it was
        // allowed to write to a new ref.
        checker.checkChanges(id2);

        // First change was created before NoteDb writes were turned on, so its meta
        // ref doesn't exist until a manual rebuild.
        checker.assertNoChangeRef(project, id1);
        checker.rebuildAndCheckChanges(id1);
    }

    @Test
    public void noteDbChangeState() throws Exception {
        notesMigration.setAllEnabled(true);
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();

        ObjectId changeMetaId = getMetaRef(project, changeMetaRef(id));
        assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(changeMetaId.name());

        putDraft(user, id, 1, "comment by user");
        ObjectId userDraftsId = getMetaRef(allUsers, RefNames.refsDraftComments(id, user.getId()));
        assertThat(unwrapDb().changes().get(id).getNoteDbState())
                .isEqualTo(changeMetaId.name() + "," + user.getId() + "=" + userDraftsId.name());

        putDraft(admin, id, 2, "comment by admin");
        ObjectId adminDraftsId = getMetaRef(allUsers, RefNames.refsDraftComments(id, admin.getId()));
        assertThat(admin.getId().get()).isLessThan(user.getId().get());
        assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(changeMetaId.name() + ","
                + admin.getId() + "=" + adminDraftsId.name() + "," + user.getId() + "=" + userDraftsId.name());

        putDraft(admin, id, 2, "revised comment by admin");
        adminDraftsId = getMetaRef(allUsers, RefNames.refsDraftComments(id, admin.getId()));
        assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(changeMetaId.name() + ","
                + admin.getId() + "=" + adminDraftsId.name() + "," + user.getId() + "=" + userDraftsId.name());
    }

    @Test
    public void rebuildAutomaticallyWhenChangeOutOfDate() throws Exception {
        notesMigration.setAllEnabled(true);

        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        assertChangeUpToDate(true, id);

        // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
        notesMigration.setAllEnabled(false);
        gApi.changes().id(id.get()).topic(name("a-topic"));
        setInvalidNoteDbState(id);
        assertChangeUpToDate(false, id);

        // On next NoteDb read, the change is transparently rebuilt.
        notesMigration.setAllEnabled(true);
        assertThat(gApi.changes().id(id.get()).info().topic).isEqualTo(name("a-topic"));
        assertChangeUpToDate(true, id);

        // Check that the bundles are equal.
        ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notesFactory.create(dbProvider.get(), project, id));
        ChangeBundle expected = ChangeBundle.fromReviewDb(unwrapDb(), id);
        assertThat(actual.differencesFrom(expected)).isEmpty();
    }

    @Test
    public void rebuildAutomaticallyWhenDraftsOutOfDate() throws Exception {
        notesMigration.setAllEnabled(true);
        setApiUser(user);

        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        putDraft(user, id, 1, "comment");
        assertDraftsUpToDate(true, id, user);

        // Make a ReviewDb change behind NoteDb's back and ensure it's detected.
        notesMigration.setAllEnabled(false);
        putDraft(user, id, 1, "comment");
        setInvalidNoteDbState(id);
        assertDraftsUpToDate(false, id, user);

        // On next NoteDb read, the drafts are transparently rebuilt.
        notesMigration.setAllEnabled(true);
        assertThat(gApi.changes().id(id.get()).current().drafts()).containsKey(PushOneCommit.FILE_NAME);
        assertDraftsUpToDate(true, id, user);
    }

    @Test
    public void pushCert() throws Exception {
        // We don't have the code in our test harness to do signed pushes, so just
        // use a hard-coded cert. This cert was actually generated by C git 2.2.0
        // (albeit not for sending to Gerrit).
        String cert = "certificate version 0.1\n" + "pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
                + "pushee git://localhost/repo.git\n" + "nonce 1433954361-bde756572d665bba81d8\n" + "\n"
                + "0000000000000000000000000000000000000000" + "b981a177396fb47345b7df3e4d3f854c6bea7"
                + "s/heads/master\n" + "-----BEGIN PGP SIGNATURE-----\n" + "Version: GnuPG v1\n" + "\n"
                + "iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
                + "9tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
                + "htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
                + "4ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
                + "IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
                + "+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n" + "=XFeC\n"
                + "-----END PGP SIGNATURE-----\n";

        PushOneCommit.Result r = createChange();
        PatchSet.Id psId = r.getPatchSetId();
        Change.Id id = psId.getParentKey();

        PatchSet ps = db.patchSets().get(psId);
        ps.setPushCertificate(cert);
        db.patchSets().update(Collections.singleton(ps));
        indexer.index(db, project, id);

        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void emptyTopic() throws Exception {
        PushOneCommit.Result r = createChange();
        Change.Id id = r.getPatchSetId().getParentKey();
        Change c = db.changes().get(id);
        assertThat(c.getTopic()).isNull();
        c.setTopic("");
        db.changes().update(Collections.singleton(c));

        checker.rebuildAndCheckChanges(id);

        notesMigration.setWriteChanges(true);
        notesMigration.setReadChanges(true);

        // Rebuild and check was successful, but NoteDb doesn't support storing an
        // empty topic, so it comes out as null.
        ChangeNotes notes = notesFactory.create(db, project, id);
        assertThat(notes.getChange().getTopic()).isNull();
    }

    @Test
    public void commentBeforeFirstPatchSet() throws Exception {
        PushOneCommit.Result r = createChange();
        PatchSet.Id psId = r.getPatchSetId();
        Change.Id id = psId.getParentKey();

        Change c = db.changes().get(id);
        c.setCreatedOn(new Timestamp(c.getCreatedOn().getTime() - 5000));
        db.changes().update(Collections.singleton(c));
        indexer.index(db, project, id);

        ReviewInput rin = new ReviewInput();
        rin.message = "comment";

        Timestamp ts = new Timestamp(c.getCreatedOn().getTime() + 2000);
        RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
        postReview.get().apply(revRsrc, rin, ts);

        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void commentPredatingChangeBySomeoneOtherThanOwner() throws Exception {
        PushOneCommit.Result r = createChange();
        PatchSet.Id psId = r.getPatchSetId();
        Change.Id id = psId.getParentKey();
        Change c = db.changes().get(id);

        ReviewInput rin = new ReviewInput();
        rin.message = "comment";

        Timestamp ts = new Timestamp(c.getCreatedOn().getTime() - 10000);
        RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
        setApiUser(user);
        postReview.get().apply(revRsrc, rin, ts);

        checker.rebuildAndCheckChanges(id);
    }

    @Test
    public void noteDbUsesOriginalSubjectFromPatchSetAndIgnoresChangeField() throws Exception {
        PushOneCommit.Result r = createChange();
        String orig = r.getChange().change().getSubject();
        r = pushFactory.create(db, admin.getIdent(), testRepo, orig + " v2", PushOneCommit.FILE_NAME,
                "new contents", r.getChangeId()).to("refs/heads/master");
        r.assertOkStatus();

        PatchSet.Id psId = r.getPatchSetId();
        Change.Id id = psId.getParentKey();
        Change c = db.changes().get(id);

        c.setCurrentPatchSet(psId, c.getSubject(), "Bogus original subject");
        db.changes().update(Collections.singleton(c));

        checker.rebuildAndCheckChanges(id);

        notesMigration.setAllEnabled(true);
        ChangeNotes notes = notesFactory.create(db, project, id);
        Change nc = notes.getChange();
        assertThat(nc.getSubject()).isEqualTo(c.getSubject());
        assertThat(nc.getSubject()).isEqualTo(orig + " v2");
        assertThat(nc.getOriginalSubject()).isNotEqualTo(c.getOriginalSubject());
        assertThat(nc.getOriginalSubject()).isEqualTo(orig);
    }

    @Test
    public void deleteDraftPS1WithNoOtherEntities() throws Exception {
        PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
        PushOneCommit.Result r = push.to("refs/drafts/master");
        push = pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711",
                r.getChangeId());
        r = push.to("refs/drafts/master");
        PatchSet.Id psId = r.getPatchSetId();
        Change.Id id = psId.getParentKey();

        gApi.changes().id(r.getChangeId()).revision(1).delete();

        checker.rebuildAndCheckChanges(id);

        notesMigration.setAllEnabled(true);
        ChangeNotes notes = notesFactory.create(db, project, id);
        assertThat(notes.getPatchSets().keySet()).containsExactly(psId);
    }

    private void setInvalidNoteDbState(Change.Id id) throws Exception {
        ReviewDb db = unwrapDb();
        Change c = db.changes().get(id);
        // In reality we would have NoteDb writes enabled, which would write a real
        // state into this field. For tests however, we turn NoteDb writes off, so
        // just use a dummy state to force ChangeNotes to view the notes as
        // out-of-date.
        c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
        db.changes().update(Collections.singleton(c));
    }

    private void assertChangeUpToDate(boolean expected, Change.Id id) throws Exception {
        try (Repository repo = repoManager.openRepository(project)) {
            Change c = unwrapDb().changes().get(id);
            assertThat(c).isNotNull();
            assertThat(c.getNoteDbState()).isNotNull();
            assertThat(NoteDbChangeState.parse(c).isChangeUpToDate(repo)).isEqualTo(expected);
        }
    }

    private void assertDraftsUpToDate(boolean expected, Change.Id changeId, TestAccount account) throws Exception {
        try (Repository repo = repoManager.openRepository(allUsers)) {
            Change c = unwrapDb().changes().get(changeId);
            assertThat(c).isNotNull();
            assertThat(c.getNoteDbState()).isNotNull();
            NoteDbChangeState state = NoteDbChangeState.parse(c);
            assertThat(state.areDraftsUpToDate(repo, account.getId())).isEqualTo(expected);
        }
    }

    private ObjectId getMetaRef(Project.NameKey p, String name) throws Exception {
        try (Repository repo = repoManager.openRepository(p)) {
            Ref ref = repo.exactRef(name);
            return ref != null ? ref.getObjectId() : null;
        }
    }

    private void putDraft(TestAccount account, Change.Id id, int line, String msg) throws Exception {
        DraftInput in = new DraftInput();
        in.line = line;
        in.message = msg;
        in.path = PushOneCommit.FILE_NAME;
        AcceptanceTestRequestScope.Context old = setApiUser(account);
        try {
            gApi.changes().id(id.get()).current().createDraft(in);
        } finally {
            atrScope.set(old);
        }
    }

    private void putComment(TestAccount account, Change.Id id, int line, String msg) throws Exception {
        CommentInput in = new CommentInput();
        in.line = line;
        in.message = msg;
        ReviewInput rin = new ReviewInput();
        rin.comments = new HashMap<>();
        rin.comments.put(PushOneCommit.FILE_NAME, ImmutableList.of(in));
        rin.drafts = ReviewInput.DraftHandling.KEEP;
        AcceptanceTestRequestScope.Context old = setApiUser(account);
        try {
            gApi.changes().id(id.get()).current().review(rin);
        } finally {
            atrScope.set(old);
        }
    }

    private void publishDrafts(TestAccount account, Change.Id id) throws Exception {
        ReviewInput rin = new ReviewInput();
        rin.drafts = ReviewInput.DraftHandling.PUBLISH_ALL_REVISIONS;
        AcceptanceTestRequestScope.Context old = setApiUser(account);
        try {
            gApi.changes().id(id.get()).current().review(rin);
        } finally {
            atrScope.set(old);
        }
    }

    private ChangeMessage insertMessage(Change.Id id, PatchSet.Id psId, Account.Id author, Timestamp ts,
            String message) throws Exception {
        ChangeMessage msg = new ChangeMessage(new ChangeMessage.Key(id, ChangeUtil.messageUUID(db)), author, ts,
                psId);
        msg.setMessage(message);
        db.changeMessages().insert(Collections.singleton(msg));

        Change c = db.changes().get(id);
        if (ts.compareTo(c.getLastUpdatedOn()) > 0) {
            c.setLastUpdatedOn(ts);
            db.changes().update(Collections.singleton(c));
        }

        return msg;
    }

    private ReviewDb unwrapDb() {
        ReviewDb db = dbProvider.get();
        if (db instanceof DisabledChangesReviewDbWrapper) {
            db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
        }
        return db;
    }
}