| // Copyright (C) 2018 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.server.schema; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth8.assertThat; |
| import static com.google.gerrit.server.schema.NoteDbSchemaUpdater.requiredUpgrades; |
| import static com.google.gerrit.testing.GerritJUnit.assertThrows; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSortedMap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.server.Sequence; |
| import com.google.gerrit.server.config.AllProjectsName; |
| import com.google.gerrit.server.config.AllUsersName; |
| import com.google.gerrit.server.extensions.events.GitReferenceUpdated; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.notedb.IntBlob; |
| import com.google.gerrit.server.notedb.RepoSequence; |
| import com.google.gerrit.testing.InMemoryRepositoryManager; |
| import com.google.gerrit.testing.TestUpdateUI; |
| import java.io.IOException; |
| import java.util.Optional; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.junit.TestRepository; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.Repository; |
| import org.junit.Test; |
| |
| public class NoteDbSchemaUpdaterTest { |
| @Test |
| public void requiredUpgradesFromNoVersion() throws Exception { |
| assertThat(requiredUpgrades(0, versions(10))).containsExactly(10).inOrder(); |
| assertThat(requiredUpgrades(0, versions(10, 11, 12))).containsExactly(10, 11, 12).inOrder(); |
| } |
| |
| @Test |
| public void requiredUpgradesFromExistingVersion() throws Exception { |
| ImmutableSortedSet<Integer> versions = versions(10, 11, 12, 13); |
| assertThat(requiredUpgrades(10, versions)).containsExactly(11, 12, 13).inOrder(); |
| assertThat(requiredUpgrades(11, versions)).containsExactly(12, 13).inOrder(); |
| assertThat(requiredUpgrades(12, versions)).containsExactly(13).inOrder(); |
| assertThat(requiredUpgrades(13, versions)).isEmpty(); |
| } |
| |
| @Test |
| public void downgradeNotSupported() throws Exception { |
| StorageException thrown = |
| assertThrows(StorageException.class, () -> requiredUpgrades(14, versions(10, 11, 12, 13))); |
| assertThat(thrown) |
| .hasMessageThat() |
| .contains("Cannot downgrade NoteDb schema from version 14 to 13"); |
| } |
| |
| @Test |
| public void skipToFirstVersionNotSupported() throws Exception { |
| ImmutableSortedSet<Integer> versions = versions(10, 11, 12); |
| assertThat(requiredUpgrades(9, versions)).containsExactly(10, 11, 12).inOrder(); |
| StorageException thrown = |
| assertThrows(StorageException.class, () -> requiredUpgrades(8, versions)); |
| assertThat(thrown).hasMessageThat().contains("Cannot skip NoteDb schema from version 8 to 10"); |
| } |
| |
| private static class TestUpdate { |
| protected final Config cfg; |
| protected final AllProjectsName allProjectsName; |
| protected final AllUsersName allUsersName; |
| protected final NoteDbSchemaUpdater updater; |
| protected final GitRepositoryManager repoManager; |
| protected final NoteDbSchemaVersion.Arguments args; |
| private final ImmutableList.Builder<String> messages; |
| |
| TestUpdate(Optional<Integer> initialVersion) { |
| cfg = new Config(); |
| allProjectsName = new AllProjectsName("The-Projects"); |
| allUsersName = new AllUsersName("The-Users"); |
| repoManager = new InMemoryRepositoryManager(); |
| |
| args = |
| new NoteDbSchemaVersion.Arguments( |
| repoManager, allProjectsName, allUsersName, null, null, null, null); |
| NoteDbSchemaVersionManager versionManager = |
| new NoteDbSchemaVersionManager(allProjectsName, repoManager); |
| updater = |
| new NoteDbSchemaUpdater( |
| cfg, |
| allUsersName, |
| repoManager, |
| new TestSchemaCreator(initialVersion), |
| versionManager, |
| args, |
| ImmutableSortedMap.of(10, TestSchema_10.class, 11, TestSchema_11.class)); |
| messages = ImmutableList.builder(); |
| } |
| |
| private class TestSchemaCreator implements SchemaCreator { |
| private final Optional<Integer> initialVersion; |
| |
| TestSchemaCreator(Optional<Integer> initialVersion) { |
| this.initialVersion = initialVersion; |
| } |
| |
| @Override |
| public void create() throws IOException { |
| try (Repository repo = repoManager.createRepository(allProjectsName); |
| TestRepository<Repository> tr = new TestRepository<>(repo)) { |
| if (initialVersion.isPresent()) { |
| tr.update(RefNames.REFS_VERSION, tr.blob(initialVersion.get().toString())); |
| } |
| } catch (Exception e) { |
| throw new StorageException(e); |
| } |
| repoManager.createRepository(allUsersName).close(); |
| setUp(); |
| } |
| |
| @Override |
| public void ensureCreated() throws IOException { |
| try { |
| repoManager.openRepository(allProjectsName).close(); |
| } catch (RepositoryNotFoundException e) { |
| create(); |
| } |
| } |
| } |
| |
| protected void setNotesMigrationConfig() { |
| cfg.setString("noteDb", "changes", "write", "true"); |
| cfg.setString("noteDb", "changes", "read", "true"); |
| cfg.setString("noteDb", "changes", "primaryStorage", "NOTE_DB"); |
| cfg.setString("noteDb", "changes", "disableReviewDb", "true"); |
| } |
| |
| protected void seedGroupSequenceRef() { |
| new RepoSequence( |
| repoManager, |
| GitReferenceUpdated.DISABLED, |
| allUsersName, |
| Sequence.NAME_GROUPS, |
| () -> 1, |
| 1) |
| .next(); |
| } |
| |
| /** Test-specific setup. */ |
| protected void setUp() {} |
| |
| ImmutableList<String> update() throws Exception { |
| updater.update( |
| new TestUpdateUI() { |
| @Override |
| public void message(String m) { |
| messages.add(m); |
| } |
| }); |
| return getMessages(); |
| } |
| |
| ImmutableList<String> getMessages() { |
| return messages.build(); |
| } |
| |
| Optional<Integer> readVersion() throws Exception { |
| try (Repository repo = repoManager.openRepository(allProjectsName)) { |
| return IntBlob.parse(repo, RefNames.REFS_VERSION).map(IntBlob::value); |
| } |
| } |
| |
| static class TestSchema_10 implements NoteDbSchemaVersion { |
| @Override |
| public void upgrade(Arguments args, UpdateUI ui) { |
| ui.message("body of 10"); |
| } |
| } |
| |
| static class TestSchema_11 implements NoteDbSchemaVersion { |
| @Override |
| public void upgrade(Arguments args, UpdateUI ui) { |
| ui.message("BODY OF 11"); |
| } |
| } |
| } |
| |
| @Test |
| public void bootstrapUpdateWith216Prerequisites() throws Exception { |
| TestUpdate u = |
| new TestUpdate(Optional.empty()) { |
| @Override |
| public void setUp() { |
| setNotesMigrationConfig(); |
| seedGroupSequenceRef(); |
| } |
| }; |
| assertThat(u.update()) |
| .containsExactly( |
| "Migrating data to schema 10 ...", |
| "body of 10", |
| "Migrating data to schema 11 ...", |
| "BODY OF 11") |
| .inOrder(); |
| assertThat(u.readVersion()).hasValue(11); |
| } |
| |
| @Test |
| public void bootstrapUpdateFailsWithoutNotesMigrationConfig() throws Exception { |
| TestUpdate u = |
| new TestUpdate(Optional.empty()) { |
| @Override |
| public void setUp() { |
| seedGroupSequenceRef(); |
| } |
| }; |
| StorageException thrown = assertThrows(StorageException.class, () -> u.update()); |
| assertThat(thrown).hasMessageThat().contains("NoteDb change migration was not completed"); |
| assertThat(u.getMessages()).isEmpty(); |
| assertThat(u.readVersion()).isEmpty(); |
| } |
| |
| @Test |
| public void bootstrapUpdateFailsWithoutGroupSequenceRef() throws Exception { |
| TestUpdate u = |
| new TestUpdate(Optional.empty()) { |
| @Override |
| public void setUp() { |
| setNotesMigrationConfig(); |
| } |
| }; |
| StorageException thrown = assertThrows(StorageException.class, () -> u.update()); |
| assertThat(thrown).hasMessageThat().contains("upgrade to 2.16.x first"); |
| assertThat(u.getMessages()).isEmpty(); |
| assertThat(u.readVersion()).isEmpty(); |
| } |
| |
| @Test |
| public void updateTwoVersions() throws Exception { |
| TestUpdate u = new TestUpdate(Optional.of(9)); |
| assertThat(u.update()) |
| .containsExactly( |
| "Migrating data to schema 10 ...", |
| "body of 10", |
| "Migrating data to schema 11 ...", |
| "BODY OF 11") |
| .inOrder(); |
| assertThat(u.readVersion()).hasValue(11); |
| } |
| |
| @Test |
| public void updateOneVersion() throws Exception { |
| TestUpdate u = new TestUpdate(Optional.of(10)); |
| assertThat(u.update()) |
| .containsExactly("Migrating data to schema 11 ...", "BODY OF 11") |
| .inOrder(); |
| assertThat(u.readVersion()).hasValue(11); |
| } |
| |
| @Test |
| public void updateNoOp() throws Exception { |
| // This test covers the state when running the updater after initializing a new 3.x site, which |
| // seeds the schema version ref with the latest version. |
| TestUpdate u = new TestUpdate(Optional.of(11)); |
| assertThat(u.update()).isEmpty(); |
| assertThat(u.readVersion()).hasValue(11); |
| } |
| |
| private static ImmutableSortedSet<Integer> versions(Integer... versions) { |
| return ImmutableSortedSet.copyOf(versions); |
| } |
| } |