Coverage for manila/tests/db/migrations/alembic/test_migration.py: 95%

105 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2010-2011 OpenStack, LLC 

2# All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16""" 

17Tests for database migrations. 

18""" 

19 

20from unittest import mock 

21 

22from alembic import script 

23from oslo_db.sqlalchemy import enginefacade 

24from oslo_db.sqlalchemy import test_fixtures 

25from oslo_log import log 

26from oslotest import base as test_base 

27from sqlalchemy.sql import text 

28 

29from manila.db.migrations.alembic import migration 

30from manila.tests.db.migrations.alembic import migrations_data_checks 

31from manila.tests import utils as test_utils 

32 

33LOG = log.getLogger('manila.tests.test_migrations') 

34 

35 

36class ManilaMigrationsCheckers(migrations_data_checks.DbMigrationsData): 

37 """Test alembic migrations.""" 

38 

39 def setUp(self): 

40 super().setUp() 

41 self.engine = enginefacade.writer.get_engine() 

42 

43 @property 

44 def snake_walk(self): 

45 return True 

46 

47 @property 

48 def downgrade(self): 

49 return True 

50 

51 @property 

52 def INIT_VERSION(self): 

53 pass 

54 

55 @property 

56 def REPOSITORY(self): 

57 pass 

58 

59 @property 

60 def migration_api(self): 

61 return migration 

62 

63 @property 

64 def migrate_engine(self): 

65 return self.engine 

66 

67 def _walk_versions(self, snake_walk=False, downgrade=True): 

68 # Determine latest version script from the repo, then 

69 # upgrade from 1 through to the latest, with no data 

70 # in the databases. This just checks that the schema itself 

71 # upgrades successfully. 

72 

73 # Place the database under version control 

74 alembic_cfg = migration._alembic_config() 

75 script_directory = script.ScriptDirectory.from_config(alembic_cfg) 

76 

77 self.assertIsNone(self.migration_api.version()) 

78 

79 versions = [ver for ver in script_directory.walk_revisions()] 

80 

81 LOG.debug('latest version is %s', versions[0].revision) 

82 

83 for version in reversed(versions): 

84 self._migrate_up(version.revision, with_data=True) 

85 if snake_walk: 

86 downgraded = self._migrate_down( 

87 version, with_data=True) 

88 if downgraded: 

89 self._migrate_up(version.revision) 

90 

91 if downgrade: 

92 for version in versions: 

93 downgraded = self._migrate_down(version) 

94 if snake_walk and downgraded: 

95 self._migrate_up(version.revision) 

96 self._migrate_down(version) 

97 

98 def _migrate_down(self, version, with_data=False): 

99 try: 

100 self.migration_api.downgrade(version.down_revision) 

101 except NotImplementedError: 

102 # NOTE(sirp): some migrations, namely release-level 

103 # migrations, don't support a downgrade. 

104 return False 

105 

106 self.assertEqual(version.down_revision, self.migration_api.version()) 

107 

108 if with_data: 

109 post_downgrade = getattr( 

110 self, "_post_downgrade_%s" % version.revision, None) 

111 if post_downgrade: 

112 with self.engine.begin() as conn: 

113 post_downgrade(conn) 

114 

115 return True 

116 

117 def _migrate_up(self, version, with_data=False): 

118 """migrate up to a new version of the db. 

119 

120 We allow for data insertion and post checks at every 

121 migration version with special _pre_upgrade_### and 

122 _check_### functions in the main test. 

123 """ 

124 # NOTE(sdague): try block is here because it's impossible to debug 

125 # where a failed data migration happens otherwise 

126 try: 

127 if with_data: 

128 data = None 

129 pre_upgrade = getattr( 

130 self, "_pre_upgrade_%s" % version, None) 

131 if pre_upgrade: 

132 with self.engine.begin() as conn: 

133 data = pre_upgrade(conn) 

134 

135 self.migration_api.upgrade(version) 

136 self.assertEqual(version, self.migration_api.version()) 

137 

138 if with_data: 

139 check = getattr(self, "_check_%s" % version, None) 

140 if check: 

141 with self.engine.begin() as conn: 

142 check(conn, data) 

143 except Exception as e: 

144 LOG.error("Failed to migrate to version %(version)s on engine " 

145 "%(engine)s. Exception while running the migration: " 

146 "%(exception)s", {'version': version, 

147 'engine': self.engine, 

148 'exception': e}) 

149 raise 

150 

151 # NOTE(vponomaryov): set 12 minutes timeout for case of running it on 

152 # very slow nodes/VMs. Note, that this test becomes slower with each 

153 # addition of new DB migration. On fast nodes it can take about 5-10 secs 

154 # having Mitaka set of migrations. 

155 # 'pymysql' works much slower on slow nodes than 'psycopg2'. And such 

156 # timeout mostly required for testing of 'mysql' backend. 

157 @test_utils.set_timeout(720) 

158 def test_walk_versions(self): 

159 """Walks all version scripts for each tested database. 

160 

161 While walking, ensure that there are no errors in the version 

162 scripts for each engine. 

163 """ 

164 with mock.patch('manila.db.sqlalchemy.api.get_engine', 

165 return_value=self.engine): 

166 self._walk_versions(snake_walk=self.snake_walk, 

167 downgrade=self.downgrade) 

168 

169 def test_single_branch(self): 

170 alembic_cfg = migration._alembic_config() 

171 script_directory = script.ScriptDirectory.from_config(alembic_cfg) 

172 

173 actual_result = script_directory.get_heads() 

174 

175 self.assertEqual(1, len(actual_result), 

176 "Db migrations should have only one branch.") 

177 

178 

179class TestManilaMigrationsMySQL( 

180 ManilaMigrationsCheckers, 

181 test_fixtures.OpportunisticDBTestMixin, 

182 test_base.BaseTestCase, 

183): 

184 """Run migration tests on MySQL backend.""" 

185 FIXTURE = test_fixtures.MySQLOpportunisticFixture 

186 

187 @test_utils.set_timeout(300) 

188 def test_mysql_innodb(self): 

189 """Test that table creation on mysql only builds InnoDB tables.""" 

190 with mock.patch('manila.db.sqlalchemy.api.get_engine', 

191 return_value=self.engine): 

192 self._walk_versions(snake_walk=False, downgrade=False) 

193 

194 with self.engine.begin() as conn: 

195 # sanity check 

196 sanity_check = """SELECT count(*) 

197 FROM information_schema.tables 

198 WHERE table_schema = :database;""" 

199 total = conn.execute( 

200 text(sanity_check), 

201 {"database": self.engine.url.database}) 

202 

203 self.assertGreater( 

204 total.scalar(), 0, "No tables found. Wrong schema?") 

205 

206 noninnodb_query = """ 

207 SELECT count(*) 

208 FROM information_schema.TABLES 

209 WHERE table_schema = :database 

210 AND engine != 'InnoDB' 

211 AND table_name != 'alembic_version';""" 

212 

213 count = conn.execute( 

214 text(noninnodb_query), 

215 {"database": self.engine.url.database} 

216 ).scalar() 

217 self.assertEqual(0, count, "%d non InnoDB tables created" % count) 

218 

219 

220class TestManilaMigrationsPostgreSQL( 

221 ManilaMigrationsCheckers, 

222 test_fixtures.OpportunisticDBTestMixin, 

223 test_base.BaseTestCase, 

224): 

225 """Run migration tests on PostgreSQL backend.""" 

226 FIXTURE = test_fixtures.PostgresqlOpportunisticFixture