summaryrefslogtreecommitdiffstats
path: root/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
diff options
context:
space:
mode:
Diffstat (limited to 'gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java')
-rw-r--r--gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java675
1 files changed, 675 insertions, 0 deletions
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
new file mode 100644
index 0000000000..135809082f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
@@ -0,0 +1,675 @@
+// 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.google.gerrit.acceptance.server.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.NOTE_DB_PRIMARY_STATE;
+import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB;
+import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE;
+import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY;
+import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY;
+import static com.google.gerrit.server.notedb.NotesMigrationState.REVIEW_DB;
+import static com.google.gerrit.server.notedb.NotesMigrationState.WRITE;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.reviewdb.client.Change;
+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.CommentsUtil;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.notedb.ChangeBundle;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
+import com.google.gerrit.server.notedb.NoteDbChangeState;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
+import com.google.gerrit.server.notedb.NotesMigrationState;
+import com.google.gerrit.server.notedb.rebuild.MigrationException;
+import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
+import com.google.gerrit.server.notedb.rebuild.NotesMigrationStateListener;
+import com.google.gerrit.server.schema.ReviewDbFactory;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.NoteDbMode;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@Sandboxed
+@UseLocalDisk
+@NoHttpd
+public class OnlineNoteDbMigrationIT extends AbstractDaemonTest {
+ private static final String INVALID_STATE = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ Config cfg = new Config();
+ cfg.setInt("noteDb", "changes", "sequenceBatchSize", 10);
+ cfg.setInt("noteDb", "changes", "initialSequenceGap", 500);
+ return cfg;
+ }
+
+ // Tests in this class are generally interested in the actual ReviewDb contents, but the shifting
+ // migration state may result in various kinds of wrappers showing up unexpectedly.
+ @Inject @ReviewDbFactory private SchemaFactory<ReviewDb> schemaFactory;
+
+ @Inject private ChangeBundleReader changeBundleReader;
+ @Inject private CommentsUtil commentsUtil;
+ @Inject private DynamicSet<NotesMigrationStateListener> listeners;
+ @Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
+ @Inject private Sequences sequences;
+ @Inject private SitePaths sitePaths;
+
+ private FileBasedConfig noteDbConfig;
+ private List<RegistrationHandle> addedListeners;
+
+ @Before
+ public void setUp() throws Exception {
+ assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.OFF);
+ // Unlike in the running server, for tests, we don't stack notedb.config on gerrit.config.
+ noteDbConfig = new FileBasedConfig(sitePaths.notedb_config.toFile(), FS.detect());
+ assertNotesMigrationState(REVIEW_DB, false, false);
+ addedListeners = new ArrayList<>();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (addedListeners != null) {
+ addedListeners.forEach(RegistrationHandle::remove);
+ addedListeners = null;
+ }
+ }
+
+ @Test
+ public void preconditionsFail() throws Exception {
+ List<Change.Id> cs = ImmutableList.of(new Change.Id(1));
+ List<Project.NameKey> ps = ImmutableList.of(new Project.NameKey("p"));
+ assertMigrationException(
+ "Cannot rebuild without noteDb.changes.write=true", b -> b, NoteDbMigrator::rebuild);
+ assertMigrationException(
+ "Cannot combine changes, projects and skipProjects",
+ b -> b.setChanges(cs).setProjects(ps),
+ m -> {});
+ assertMigrationException(
+ "Cannot combine changes, projects and skipProjects",
+ b -> b.setChanges(cs).setSkipProjects(ps),
+ m -> {});
+ assertMigrationException(
+ "Cannot combine changes, projects and skipProjects",
+ b -> b.setProjects(ps).setSkipProjects(ps),
+ m -> {});
+ assertMigrationException(
+ "Cannot set changes or projects or skipProjects during full migration",
+ b -> b.setChanges(cs),
+ NoteDbMigrator::migrate);
+ assertMigrationException(
+ "Cannot set changes or projects or skipProjects during full migration",
+ b -> b.setProjects(ps),
+ NoteDbMigrator::migrate);
+ assertMigrationException(
+ "Cannot set changes or projects or skipProjects during full migration",
+ b -> b.setSkipProjects(ps),
+ NoteDbMigrator::migrate);
+
+ setNotesMigrationState(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY);
+ assertMigrationException(
+ "Migration has already progressed past the endpoint of the \"trial mode\" state",
+ b -> b.setTrialMode(true),
+ NoteDbMigrator::migrate);
+
+ setNotesMigrationState(READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY);
+ assertMigrationException(
+ "Cannot force rebuild changes; NoteDb is already the primary storage for some changes",
+ b -> b.setForceRebuild(true),
+ NoteDbMigrator::migrate);
+ }
+
+ @Test
+ @GerritConfig(name = "noteDb.changes.initialSequenceGap", value = "-7")
+ public void initialSequenceGapMustBeNonNegative() throws Exception {
+ setNotesMigrationState(READ_WRITE_NO_SEQUENCE);
+ assertMigrationException("Sequence gap must be non-negative: -7", b -> b, m -> {});
+ }
+
+ @Test
+ public void rebuildOneChangeTrialModeAndForceRebuild() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+
+ migrate(b -> b.setTrialMode(true));
+ assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, false, true);
+
+ ObjectId oldMetaId;
+ try (Repository repo = repoManager.openRepository(project);
+ ReviewDb db = schemaFactory.open()) {
+ Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
+ assertThat(ref).isNotNull();
+ oldMetaId = ref.getObjectId();
+
+ Change c = db.changes().get(id);
+ assertThat(c).isNotNull();
+ NoteDbChangeState state = NoteDbChangeState.parse(c);
+ assertThat(state).isNotNull();
+ assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
+ assertThat(state.getRefState()).hasValue(RefState.create(oldMetaId, ImmutableMap.of()));
+
+ // Force change to be out of date, and change topic so it will get rebuilt as something other
+ // than oldMetaId.
+ c.setNoteDbState(INVALID_STATE);
+ c.setTopic(name("a-new-topic"));
+ db.changes().update(ImmutableList.of(c));
+ }
+
+ migrate(b -> b.setTrialMode(true));
+ assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, false, true);
+
+ try (Repository repo = repoManager.openRepository(project);
+ ReviewDb db = schemaFactory.open()) {
+ // Change is out of date, but was not rebuilt without forceRebuild.
+ assertThat(repo.exactRef(RefNames.changeMetaRef(id)).getObjectId()).isEqualTo(oldMetaId);
+ Change c = db.changes().get(id);
+ assertThat(c.getNoteDbState()).isEqualTo(INVALID_STATE);
+ }
+
+ migrate(b -> b.setTrialMode(true).setForceRebuild(true));
+ assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, false, true);
+
+ try (Repository repo = repoManager.openRepository(project);
+ ReviewDb db = schemaFactory.open()) {
+ Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
+ assertThat(ref).isNotNull();
+ ObjectId newMetaId = ref.getObjectId();
+ assertThat(newMetaId).isNotEqualTo(oldMetaId);
+
+ NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
+ assertThat(state).isNotNull();
+ assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
+ assertThat(state.getRefState()).hasValue(RefState.create(newMetaId, ImmutableMap.of()));
+ }
+ }
+
+ @Test
+ public void autoMigrateTrialMode() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+
+ migrate(b -> b.setAutoMigrate(true).setTrialMode(true).setStopAtStateForTesting(WRITE));
+ assertNotesMigrationState(WRITE, true, true);
+
+ migrate(b -> b);
+ // autoMigrate is still enabled so that we can continue the migration by only unsetting trial.
+ assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, true, true);
+
+ ObjectId metaId;
+ try (Repository repo = repoManager.openRepository(project);
+ ReviewDb db = schemaFactory.open()) {
+ Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
+ assertThat(ref).isNotNull();
+ metaId = ref.getObjectId();
+ NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
+ assertThat(state).isNotNull();
+ assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
+ assertThat(state.getRefState()).hasValue(RefState.create(metaId, ImmutableMap.of()));
+ }
+
+ // Unset trial mode and the next migration runs to completion.
+ noteDbConfig.load();
+ NoteDbMigrator.setTrialMode(noteDbConfig, false);
+ noteDbConfig.save();
+
+ migrate(b -> b);
+ assertNotesMigrationState(NOTE_DB, false, false);
+
+ try (Repository repo = repoManager.openRepository(project);
+ ReviewDb db = schemaFactory.open()) {
+ Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
+ assertThat(ref).isNotNull();
+ assertThat(ref.getObjectId()).isEqualTo(metaId);
+ NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
+ assertThat(state).isNotNull();
+ assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.NOTE_DB);
+ }
+ }
+
+ @Test
+ public void rebuildSubsetOfChanges() throws Exception {
+ setNotesMigrationState(WRITE);
+
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+ Change.Id id1 = r1.getChange().getId();
+ Change.Id id2 = r2.getChange().getId();
+
+ invalidateNoteDbState(id1, id2);
+ migrate(b -> b.setChanges(ImmutableList.of(id2)), NoteDbMigrator::rebuild);
+ assertNotRebuilt(id1);
+ assertRebuilt(id2);
+ }
+
+ @Test
+ public void rebuildSubsetOfProjects() throws Exception {
+ setNotesMigrationState(WRITE);
+
+ Project.NameKey p2 = createProject("project2");
+ TestRepository<?> tr2 = cloneProject(p2, admin);
+
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = pushFactory.create(db, admin.getIdent(), tr2).to("refs/for/master");
+ Change.Id id1 = r1.getChange().getId();
+ Change.Id id2 = r2.getChange().getId();
+
+ invalidateNoteDbState(id1, id2);
+ migrate(b -> b.setProjects(ImmutableList.of(p2)), NoteDbMigrator::rebuild);
+ assertNotRebuilt(id1);
+ assertRebuilt(id2);
+ }
+
+ @Test
+ public void rebuildNonSkippedProjects() throws Exception {
+ setNotesMigrationState(WRITE);
+
+ Project.NameKey p2 = createProject("project2");
+ TestRepository<?> tr2 = cloneProject(p2, admin);
+ Project.NameKey p3 = createProject("project3");
+ TestRepository<?> tr3 = cloneProject(p3, admin);
+
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = pushFactory.create(db, admin.getIdent(), tr2).to("refs/for/master");
+ PushOneCommit.Result r3 = pushFactory.create(db, admin.getIdent(), tr3).to("refs/for/master");
+ Change.Id id1 = r1.getChange().getId();
+ Change.Id id2 = r2.getChange().getId();
+ Change.Id id3 = r3.getChange().getId();
+
+ invalidateNoteDbState(id1, id2, id3);
+ migrate(b -> b.setSkipProjects(ImmutableList.of(p3)), NoteDbMigrator::rebuild);
+ assertRebuilt(id1, id2);
+ assertNotRebuilt(id3);
+ }
+
+ private void invalidateNoteDbState(Change.Id... ids) throws OrmException {
+ List<Change> list = new ArrayList<>(ids.length);
+ try (ReviewDb db = schemaFactory.open()) {
+ for (Change.Id id : ids) {
+ Change c = db.changes().get(id);
+ c.setNoteDbState(INVALID_STATE);
+ list.add(c);
+ }
+ db.changes().update(list);
+ }
+ }
+
+ private void assertRebuilt(Change.Id... ids) throws OrmException {
+ try (ReviewDb db = schemaFactory.open()) {
+ for (Change.Id id : ids) {
+ NoteDbChangeState s = NoteDbChangeState.parse(db.changes().get(id));
+ assertThat(s.getChangeMetaId().name()).isNotEqualTo(INVALID_STATE);
+ }
+ }
+ }
+
+ private void assertNotRebuilt(Change.Id... ids) throws OrmException {
+ try (ReviewDb db = schemaFactory.open()) {
+ for (Change.Id id : ids) {
+ NoteDbChangeState s = NoteDbChangeState.parse(db.changes().get(id));
+ assertThat(s.getChangeMetaId().name()).isEqualTo(INVALID_STATE);
+ }
+ }
+ }
+
+ @Test
+ public void enableSequencesNoGap() throws Exception {
+ testEnableSequences(0, 3, "13");
+ }
+
+ @Test
+ public void enableSequencesWithGap() throws Exception {
+ testEnableSequences(-1, 502, "512");
+ }
+
+ private void testEnableSequences(int builderOption, int expectedFirstId, String expectedRefValue)
+ throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+ assertThat(id.get()).isEqualTo(1);
+
+ migrate(
+ b ->
+ b.setSequenceGap(builderOption)
+ .setStopAtStateForTesting(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY));
+
+ assertThat(sequences.nextChangeId()).isEqualTo(expectedFirstId);
+ assertThat(sequences.nextChangeId()).isEqualTo(expectedFirstId + 1);
+
+ try (Repository repo = repoManager.openRepository(allProjects);
+ ObjectReader reader = repo.newObjectReader()) {
+ Ref ref = repo.exactRef("refs/sequences/changes");
+ assertThat(ref).isNotNull();
+ ObjectLoader loader = reader.open(ref.getObjectId());
+ assertThat(loader.getType()).isEqualTo(Constants.OBJ_BLOB);
+ // Acquired a block of 10 to serve the first nextChangeId call after migration.
+ assertThat(new String(loader.getCachedBytes(), UTF_8)).isEqualTo(expectedRefValue);
+ }
+
+ try (ReviewDb db = schemaFactory.open()) {
+ // Underlying, unused ReviewDb is still on its own sequence.
+ @SuppressWarnings("deprecation")
+ int nextFromReviewDb = db.nextChangeId();
+ assertThat(nextFromReviewDb).isEqualTo(3);
+ }
+ }
+
+ @Test
+ public void fullMigrationSameThread() throws Exception {
+ testFullMigration(1);
+ }
+
+ @Test
+ public void fullMigrationMultipleThreads() throws Exception {
+ testFullMigration(2);
+ }
+
+ private void testFullMigration(int threads) throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+ Change.Id id1 = r1.getChange().getId();
+ Change.Id id2 = r2.getChange().getId();
+
+ Set<String> objectFiles = getObjectFiles(project);
+ assertThat(objectFiles).isNotEmpty();
+
+ migrate(b -> b.setThreads(threads));
+
+ assertNotesMigrationState(NOTE_DB, false, false);
+ assertThat(sequences.nextChangeId()).isEqualTo(503);
+ assertThat(getObjectFiles(project)).containsExactlyElementsIn(objectFiles);
+
+ ObjectId oldMetaId = null;
+ int rowVersion = 0;
+ try (ReviewDb db = schemaFactory.open();
+ Repository repo = repoManager.openRepository(project)) {
+ for (Change.Id id : ImmutableList.of(id1, id2)) {
+ String refName = RefNames.changeMetaRef(id);
+ Ref ref = repo.exactRef(refName);
+ assertThat(ref).named(refName).isNotNull();
+
+ Change c = db.changes().get(id);
+ assertThat(c.getTopic()).named("topic of change %s", id).isNull();
+ NoteDbChangeState s = NoteDbChangeState.parse(c);
+ assertThat(s.getPrimaryStorage())
+ .named("primary storage of change %s", id)
+ .isEqualTo(PrimaryStorage.NOTE_DB);
+ assertThat(s.getRefState()).named("ref state of change %s").isEmpty();
+
+ if (id.equals(id1)) {
+ oldMetaId = ref.getObjectId();
+ rowVersion = c.getRowVersion();
+ }
+ }
+ }
+
+ // Do not open a new context, to simulate races with other threads that opened a context earlier
+ // in the migration process; this needs to work.
+ gApi.changes().id(id1.get()).topic(name("a-topic"));
+
+ // Of course, it should also work with a new context.
+ resetCurrentApiUser();
+ gApi.changes().id(id1.get()).topic(name("another-topic"));
+
+ try (ReviewDb db = schemaFactory.open();
+ Repository repo = repoManager.openRepository(project)) {
+ assertThat(repo.exactRef(RefNames.changeMetaRef(id1)).getObjectId()).isNotEqualTo(oldMetaId);
+
+ Change c = db.changes().get(id1);
+ assertThat(c.getTopic()).isNull();
+ assertThat(c.getRowVersion()).isEqualTo(rowVersion);
+ }
+ }
+
+ @Test
+ public void fullMigrationOneChangeWithNoPatchSets() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+ Change.Id id1 = r1.getChange().getId();
+ Change.Id id2 = r2.getChange().getId();
+
+ db.changes().beginTransaction(id2);
+ try {
+ db.patchSets().delete(db.patchSets().byChange(id2));
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+
+ migrate(b -> b);
+ assertNotesMigrationState(NOTE_DB, false, false);
+
+ try (ReviewDb db = schemaFactory.open();
+ Repository repo = repoManager.openRepository(project)) {
+ assertThat(repo.exactRef(RefNames.changeMetaRef(id1))).isNotNull();
+ assertThat(db.changes().get(id1).getNoteDbState()).isEqualTo(NOTE_DB_PRIMARY_STATE);
+
+ // A change with no patch sets is so corrupt that it is completely skipped by the migration
+ // process.
+ assertThat(repo.exactRef(RefNames.changeMetaRef(id2))).isNull();
+ assertThat(db.changes().get(id2).getNoteDbState()).isNull();
+ }
+ }
+
+ @Test
+ public void fullMigrationMissingPatchSetRefs() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+
+ try (Repository repo = repoManager.openRepository(project)) {
+ RefUpdate u = repo.updateRef(new PatchSet.Id(id, 1).toRefName());
+ u.setForceUpdate(true);
+ assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+
+ ChangeBundle reviewDbBundle;
+ try (ReviewDb db = schemaFactory.open()) {
+ reviewDbBundle = changeBundleReader.fromReviewDb(db, id);
+ }
+
+ migrate(b -> b);
+ assertNotesMigrationState(NOTE_DB, false, false);
+
+ try (ReviewDb db = schemaFactory.open();
+ Repository repo = repoManager.openRepository(project)) {
+ // Change migrated successfully even though it was missing patch set refs.
+ assertThat(repo.exactRef(RefNames.changeMetaRef(id))).isNotNull();
+ assertThat(db.changes().get(id).getNoteDbState()).isEqualTo(NOTE_DB_PRIMARY_STATE);
+
+ ChangeBundle noteDbBundle =
+ ChangeBundle.fromNotes(commentsUtil, notesFactory.createChecked(db, project, id));
+ assertThat(noteDbBundle.differencesFrom(reviewDbBundle)).isEmpty();
+ }
+ }
+
+ @Test
+ public void autoMigrationConfig() throws Exception {
+ createChange();
+
+ migrate(b -> b.setStopAtStateForTesting(WRITE));
+ assertNotesMigrationState(WRITE, false, false);
+
+ migrate(b -> b.setAutoMigrate(true).setStopAtStateForTesting(READ_WRITE_NO_SEQUENCE));
+ assertNotesMigrationState(READ_WRITE_NO_SEQUENCE, true, false);
+
+ migrate(b -> b);
+ assertNotesMigrationState(NOTE_DB, false, false);
+ }
+
+ @Test
+ public void notesMigrationStateListener() throws Exception {
+ NotesMigrationStateListener listener = createStrictMock(NotesMigrationStateListener.class);
+ listener.preStateChange(REVIEW_DB, WRITE);
+ expectLastCall();
+ listener.preStateChange(WRITE, READ_WRITE_NO_SEQUENCE);
+ expectLastCall();
+ listener.preStateChange(READ_WRITE_NO_SEQUENCE, READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY);
+ expectLastCall();
+ listener.preStateChange(
+ READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY, READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY);
+ listener.preStateChange(READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY, NOTE_DB);
+ expectLastCall();
+ replay(listener);
+ addListener(listener);
+
+ createChange();
+ migrate(b -> b);
+ assertNotesMigrationState(NOTE_DB, false, false);
+ verify(listener);
+ }
+
+ @Test
+ public void notesMigrationStateListenerFails() throws Exception {
+ NotesMigrationStateListener listener = createStrictMock(NotesMigrationStateListener.class);
+ listener.preStateChange(REVIEW_DB, WRITE);
+ expectLastCall();
+ listener.preStateChange(WRITE, READ_WRITE_NO_SEQUENCE);
+ IOException listenerException = new IOException("Listener failed");
+ expectLastCall().andThrow(listenerException);
+ replay(listener);
+ addListener(listener);
+
+ createChange();
+ try {
+ migrate(b -> b);
+ assert_().fail("expected IOException");
+ } catch (IOException e) {
+ assertThat(e).isSameAs(listenerException);
+ }
+ assertNotesMigrationState(WRITE, false, false);
+ verify(listener);
+ }
+
+ private void assertNotesMigrationState(
+ NotesMigrationState expected, boolean autoMigrate, boolean trialMode) throws Exception {
+ assertThat(NotesMigrationState.forNotesMigration(notesMigration)).hasValue(expected);
+ noteDbConfig.load();
+ assertThat(NotesMigrationState.forConfig(noteDbConfig)).hasValue(expected);
+ assertThat(NoteDbMigrator.getAutoMigrate(noteDbConfig))
+ .named("noteDb.changes.autoMigrate")
+ .isEqualTo(autoMigrate);
+ assertThat(NoteDbMigrator.getTrialMode(noteDbConfig))
+ .named("noteDb.changes.trial")
+ .isEqualTo(trialMode);
+ }
+
+ private void setNotesMigrationState(NotesMigrationState state) throws Exception {
+ noteDbConfig.load();
+ state.setConfigValues(noteDbConfig);
+ noteDbConfig.save();
+ notesMigration.setFrom(state);
+ }
+
+ @FunctionalInterface
+ interface PrepareBuilder {
+ NoteDbMigrator.Builder prepare(NoteDbMigrator.Builder b) throws Exception;
+ }
+
+ @FunctionalInterface
+ interface RunMigration {
+ void run(NoteDbMigrator m) throws Exception;
+ }
+
+ private void migrate(PrepareBuilder b) throws Exception {
+ migrate(b, NoteDbMigrator::migrate);
+ }
+
+ private void migrate(PrepareBuilder b, RunMigration m) throws Exception {
+ try (NoteDbMigrator migrator = b.prepare(migratorBuilderProvider.get()).build()) {
+ m.run(migrator);
+ }
+ }
+
+ private void assertMigrationException(
+ String expectMessageContains, PrepareBuilder b, RunMigration m) throws Exception {
+ try {
+ migrate(b, m);
+ } catch (MigrationException e) {
+ assertThat(e).hasMessageThat().contains(expectMessageContains);
+ }
+ }
+
+ private void addListener(NotesMigrationStateListener listener) {
+ addedListeners.add(listeners.add(listener));
+ }
+
+ private SortedSet<String> getObjectFiles(Project.NameKey project) throws Exception {
+ SortedSet<String> files = new TreeSet<>();
+ try (Repository repo = repoManager.openRepository(project)) {
+ Files.walkFileTree(
+ ((FileRepository) repo).getObjectDatabase().getDirectory().toPath(),
+ new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
+ String name = file.getFileName().toString();
+ if (!attrs.isDirectory() && !name.endsWith(".pack") && !name.endsWith(".idx")) {
+ files.add(name);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+ return files;
+ }
+}