diff options
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.java | 675 |
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; + } +} |