View Javadoc
1   /*
2    *    Copyright 2010-2025 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://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,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.apache.ibatis.migration;
17  
18  import static org.junit.jupiter.api.Assertions.assertEquals;
19  import static org.junit.jupiter.api.Assertions.assertFalse;
20  import static org.junit.jupiter.api.Assertions.assertNotEquals;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertTrue;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.PrintWriter;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.StandardCopyOption;
30  import java.sql.Connection;
31  import java.sql.ResultSet;
32  import java.sql.Statement;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.List;
36  import java.util.Properties;
37  import java.util.Scanner;
38  import java.util.TreeSet;
39  
40  import org.apache.ibatis.migration.io.Resources;
41  import org.apache.ibatis.migration.utils.TestUtil;
42  import org.junit.jupiter.api.BeforeAll;
43  import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
44  import org.junit.jupiter.api.Order;
45  import org.junit.jupiter.api.Test;
46  import org.junit.jupiter.api.TestMethodOrder;
47  import org.junit.jupiter.api.condition.EnabledForJreRange;
48  
49  import uk.org.webcompere.systemstubs.SystemStubs;
50  
51  @TestMethodOrder(OrderAnnotation.class)
52  class MigratorTest {
53  
54    private static File dir;
55  
56    private static Properties env;
57  
58    @BeforeAll
59    static void setup() throws IOException {
60      dir = Resources.getResourceAsFile("org/apache/ibatis/migration/example");
61      env = Resources.getResourceAsProperties("org/apache/ibatis/migration/example/environments/development.properties");
62    }
63  
64    @Test
65    @Order(1)
66    void testBootstrapCommand() throws Exception {
67      String output = SystemStubs.tapSystemOut(() -> {
68        Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "bootstrap", "--env=development"));
69      });
70      assertFalse(output.contains("FAILURE"));
71      assertTrue(output.contains("-- // Bootstrap.sql"));
72    }
73  
74    @Test
75    @Order(2)
76    void testStatusContainsNoPendingEntriesUsingStatusShorthand() throws Exception {
77      String output = SystemStubs.tapSystemOut(() -> {
78        Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "sta"));
79      });
80      assertFalse(output.contains("FAILURE"));
81      assertTrue(output.contains("...pending..."));
82    }
83  
84    @Test
85    @Order(3)
86    void testUpCommandWithSpecifiedSteps() throws Exception {
87      String output = SystemStubs.tapSystemOut(() -> {
88        Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "up", "3000"));
89      });
90      assertFalse(output.contains("FAILURE"));
91    }
92  
93    @Test
94    @Order(4)
95    void assertAuthorEmailContainsPlaceholder() throws Exception {
96      try (Connection conn = TestUtil.getConnection(env); Statement stmt = conn.createStatement();
97          ResultSet rs = stmt.executeQuery("select EMAIL from author where id = 1")) {
98        assertTrue(rs.next());
99        assertEquals("jim@${url}", rs.getString("EMAIL"));
100     }
101   }
102 
103   @Test
104   @Order(5)
105   void testDownCommandGiven2Steps() throws Exception {
106     testStatusContainsNoPendingMigrations();
107     String output = SystemStubs.tapSystemOut(() -> {
108       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "down", "2"));
109     });
110     assertFalse(output.contains("FAILURE"));
111     testStatusContainsPendingMigrations();
112   }
113 
114   @Test
115   @Order(6)
116   void testRedoCommand() throws Exception {
117     String output = SystemStubs.tapSystemOut(() -> {
118       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "status"));
119     });
120     assertFalse(output.contains("20080827200214    ...pending..."));
121     assertTrue(output.contains("20080827200216    ...pending..."));
122 
123     output = SystemStubs.tapSystemOut(() -> {
124       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "redo"));
125     });
126     assertFalse(output.contains("FAILURE"));
127     assertEquals(-1, output.indexOf("DROP TABLE post_tag"), "Should down be just one step");
128     int dropIdx = output.indexOf("DROP TABLE comment");
129     int createIdx = output.indexOf("CREATE TABLE comment (");
130     assertNotEquals(-1, dropIdx);
131     assertNotEquals(-1, createIdx);
132     assertTrue(dropIdx < createIdx);
133 
134     output = SystemStubs.tapSystemOut(() -> {
135       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "status"));
136     });
137     assertFalse(output.contains("20080827200214    ...pending..."));
138     assertTrue(output.contains("20080827200216    ...pending..."));
139 
140     output = SystemStubs.tapSystemOut(() -> {
141       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "redo", "2"));
142     });
143     assertFalse(output.contains("FAILURE"));
144     assertEquals(-1, output.indexOf("DROP TABLE blog"), "Should down be two steps");
145     List<Integer> lineNums = new ArrayList<>();
146     lineNums.add(output.indexOf("DROP TABLE comment"));
147     lineNums.add(output.indexOf("DROP TABLE post"));
148     lineNums.add(output.indexOf("CREATE TABLE post ("));
149     lineNums.add(output.indexOf("CREATE TABLE comment ("));
150     assertEquals(new TreeSet<>(lineNums).toString(), lineNums.toString());
151 
152     output = SystemStubs.tapSystemOut(() -> {
153       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "status"));
154     });
155     assertFalse(output.contains("20080827200214    ...pending..."));
156     assertTrue(output.contains("20080827200216    ...pending..."));
157   }
158 
159   @Test
160   @Order(7)
161   void testDoPendingScriptCommand() throws Exception {
162     String output = SystemStubs.tapSystemOut(() -> {
163       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "script", "pending"));
164     });
165     assertTrue(output.contains("INSERT"));
166     assertTrue(output.contains("CHANGELOG"));
167     assertFalse(output.contains("-- @UNDO"));
168 
169     output = SystemStubs.tapSystemOut(() -> {
170       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "script", "pending_undo"));
171     });
172     assertTrue(output.contains("DELETE"));
173     assertTrue(output.contains("CHANGELOG"));
174     assertTrue(output.contains("-- @UNDO"));
175   }
176 
177   @Test
178   @Order(8)
179   void testVersionCommand() throws Exception {
180     String output = SystemStubs.tapSystemOut(() -> {
181       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "version", "20080827200217"));
182     });
183     assertFalse(output.contains("FAILURE"));
184   }
185 
186   @Test
187   @Order(9)
188   void testSkippedScript() throws Exception {
189     testStatusContainsNoPendingMigrations();
190     File skipped = Path.of(dir.getCanonicalPath(), "scripts", "20080827200215_skipped_migration.sql").toFile();
191     assertTrue(skipped.createNewFile());
192     try {
193       String output = SystemStubs.tapSystemOut(() -> {
194         Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "up"));
195       });
196       assertFalse(output.contains("FAILURE"));
197       assertEquals(1, TestUtil.countStr(output, "WARNING"));
198       assertTrue(output.contains(
199           "WARNING: Migration script '20080827200215_skipped_migration.sql' was not applied to the database."));
200     } finally {
201       skipped.delete();
202     }
203   }
204 
205   @Test
206   @Order(10)
207   void testMissingScript() throws Exception {
208     Path original = Path.of(dir + File.separator + "scripts", "20080827200216_create_procs.sql");
209     Path renamed = Path.of(dir + File.separator + "scripts", "20080827200216_create_procs._sql");
210     assertEquals(renamed, Files.move(original, renamed, StandardCopyOption.REPLACE_EXISTING));
211     try {
212       String output = SystemStubs.tapSystemOut(() -> {
213         Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "up"));
214       });
215       assertFalse(output.contains("FAILURE"), "Output contains: \n" + output);
216       assertTrue(output.contains("WARNING: Missing migration script. id='20080827200216', description='create procs'."),
217           "Output contains: \n" + output);
218     } finally {
219       assertEquals(original, Files.move(renamed, original, StandardCopyOption.REPLACE_EXISTING));
220     }
221   }
222 
223   @Test
224   @Order(11)
225   void testDownCommand() throws Exception {
226     String output = SystemStubs.tapSystemOut(() -> {
227       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "down"));
228     });
229     assertFalse(output.contains("FAILURE"));
230     testStatusContainsPendingMigrations();
231   }
232 
233   @Test
234   @Order(12)
235   void testPendingCommand() throws Exception {
236     String output = SystemStubs.tapSystemOut(() -> {
237       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "pending"));
238     });
239     assertFalse(output.contains("FAILURE"));
240     testStatusContainsNoPendingMigrations();
241   }
242 
243   @Test
244   @Order(13)
245   void testHelpCommand() throws Exception {
246     String output = SystemStubs.tapSystemOut(() -> {
247       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "--help"));
248     });
249     assertFalse(output.contains("FAILURE"));
250     assertTrue(output.contains("--help"));
251   }
252 
253   @Test
254   @Order(14)
255   void testDoScriptCommand() throws Exception {
256     String output = SystemStubs.tapSystemOut(() -> {
257       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "script", "20080827200212", "20080827200214"));
258     });
259     assertFalse(output.contains("FAILURE"));
260     assertFalse(output.contains("20080827200210"));
261     assertFalse(output.contains("20080827200211"));
262     assertFalse(output.contains("20080827200212"));
263     assertTrue(output.contains("20080827200213"));
264     assertTrue(output.contains("20080827200214"));
265     assertFalse(output.contains("20080827200216"));
266     assertFalse(output.contains("-- @UNDO"));
267 
268     output = SystemStubs.tapSystemOut(() -> {
269       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "script", "0", "20080827200211"));
270     });
271     assertFalse(output.contains("FAILURE"));
272     assertTrue(output.contains("20080827200210"));
273     assertTrue(output.contains("20080827200211"));
274     assertFalse(output.contains("20080827200212"));
275     assertFalse(output.contains("20080827200213"));
276     assertFalse(output.contains("20080827200214"));
277     assertFalse(output.contains("20080827200216"));
278     assertFalse(output.contains("-- @UNDO"));
279   }
280 
281   @Test
282   @Order(15)
283   void testUndoScriptCommand() throws Exception {
284     String output = SystemStubs.tapSystemOut(() -> {
285       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "script", "20080827200216", "20080827200213"));
286     });
287     assertFalse(output.contains("FAILURE"));
288     assertFalse(output.contains("20080827200210"));
289     assertFalse(output.contains("20080827200211"));
290     assertFalse(output.contains("20080827200212"));
291     assertFalse(output.contains("20080827200213"));
292     assertTrue(output.contains("20080827200214"));
293     assertTrue(output.contains("20080827200216"));
294     assertTrue(output.contains("-- @UNDO"));
295 
296     output = SystemStubs.tapSystemOut(() -> {
297       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "script", "20080827200211", "0"));
298     });
299     assertFalse(output.contains("FAILURE"));
300     assertTrue(output.contains("20080827200210"));
301     assertTrue(output.contains("20080827200211"));
302     assertFalse(output.contains("20080827200212"));
303     assertFalse(output.contains("20080827200213"));
304     assertFalse(output.contains("20080827200214"));
305     assertFalse(output.contains("20080827200216"));
306     assertFalse(output.contains("DELETE FROM CHANGELOG WHERE ID = 20080827200210;"));
307     assertTrue(output.contains("-- @UNDO"));
308   }
309 
310   @EnabledForJreRange(maxVersion = 23)
311   @Test
312   void shouldScriptCommandFailIfSameVersion() throws Exception {
313     String output = SystemStubs.tapSystemOut(() -> {
314       int exitCode = SystemStubs.catchSystemExit(() -> {
315         Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "script", "20080827200211", "20080827200211"));
316       });
317       assertEquals(1, exitCode);
318     });
319     assertTrue(output.contains("FAILURE"));
320   }
321 
322   @Test
323   void shouldInitTempDirectory() throws Exception {
324     File basePath = TestUtil.getTempDir();
325     Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "init"));
326     assertNotNull(basePath.list());
327     assertEquals(4, basePath.list().length);
328     File scriptPath = Path.of(basePath.getCanonicalPath(), "scripts").toFile();
329     assertEquals(3, scriptPath.list().length);
330     Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "new", "test new migration"));
331     assertEquals(4, scriptPath.list().length);
332     assertTrue(TestUtil.deleteDirectory(basePath), "delete temp dir");
333   }
334 
335   @Test
336   void shouldRespectIdPattern() throws Exception {
337     String idPattern = "000";
338     File basePath = TestUtil.getTempDir();
339     Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "--idpattern=" + idPattern, "init"));
340     File changelog = Path.of(basePath.getCanonicalPath(), "scripts", "001_create_changelog.sql").toFile();
341     assertTrue(changelog.exists());
342     Migrator.main(
343         TestUtil.args("--path=" + basePath.getAbsolutePath(), "--idpattern=" + idPattern, "new", "new migration"));
344     File newMigration = Path.of(basePath.getCanonicalPath(), "scripts", "003_new_migration.sql").toFile();
345     assertTrue(newMigration.exists());
346     assertTrue(TestUtil.deleteDirectory(basePath), "delete temp dir");
347   }
348 
349   @Test
350   void useCustomTemplate() throws Exception {
351     String desc = "test new migration";
352     File basePath = TestUtil.getTempDir();
353     Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "init"));
354     assertNotNull(basePath.list());
355     assertEquals(4, basePath.list().length);
356     File scriptPath = Path.of(basePath.getCanonicalPath(), "scripts").toFile();
357     assertEquals(3, scriptPath.list().length);
358 
359     File templatePath = File.createTempFile("customTemplate", "sql");
360     try (PrintWriter writer = new PrintWriter(templatePath)) {
361       writer.println("// ${description}");
362     }
363     Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "new", desc,
364         "--template=" + templatePath.getAbsolutePath()));
365     String[] scripts = scriptPath.list();
366     Arrays.sort(scripts);
367     assertEquals(4, scripts.length);
368     try (Scanner scanner = new Scanner(Path.of(scriptPath.getCanonicalPath(), scripts[scripts.length - 2]))) {
369       if (scanner.hasNextLine()) {
370         assertEquals("// " + desc, scanner.nextLine());
371       }
372     }
373     templatePath.delete();
374     assertTrue(TestUtil.deleteDirectory(basePath), "delete temp dir");
375   }
376 
377   @Test
378   void useCustomTemplateWithNoValue() throws Exception {
379     File basePath = TestUtil.getTempDir();
380     Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "init"));
381     assertNotNull(basePath.list());
382     assertEquals(4, basePath.list().length);
383     File scriptPath = Path.of(basePath.getCanonicalPath(), "scripts").toFile();
384     assertEquals(3, scriptPath.list().length);
385 
386     File templatePath = File.createTempFile("customTemplate", "sql");
387     templatePath.createNewFile();
388     Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "new", "test new migration", "--template="));
389     assertEquals(4, scriptPath.list().length);
390     templatePath.delete();
391     assertTrue(TestUtil.deleteDirectory(basePath), "delete temp dir");
392   }
393 
394   @Test
395   void useCustomTemplateWithBadPath() throws Exception {
396     System.setProperty("migrationsHome", TestUtil.getTempDir().getAbsolutePath());
397     File basePath = TestUtil.getTempDir();
398     Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "init"));
399     assertNotNull(basePath.list());
400     assertEquals(4, basePath.list().length);
401     File scriptPath = Path.of(basePath.getCanonicalPath(), "scripts").toFile();
402     assertEquals(3, scriptPath.list().length);
403 
404     String output = SystemStubs.tapSystemOut(() -> {
405       Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "new", "test new migration"));
406     });
407     assertEquals(4, scriptPath.list().length);
408     assertTrue(output
409         .contains("Your migrations configuration did not find your custom template.  Using the default template."));
410     assertTrue(TestUtil.deleteDirectory(basePath), "delete temp dir");
411   }
412 
413   @Test
414   void shouldSuppressOutputIfQuietOptionEnabled() throws Throwable {
415     System.setProperty("migrationsHome", TestUtil.getTempDir().getAbsolutePath());
416     File basePath = TestUtil.getTempDir();
417     String output = SystemStubs.tapSystemOut(() -> {
418       Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "--quiet", "init"));
419     });
420     assertFalse(output.contains("Initializing:"));
421     assertNotNull(basePath.list());
422     assertTrue(TestUtil.deleteDirectory(basePath), "delete temp dir");
423   }
424 
425   @Test
426   void shouldColorizeSuccessOutputIfColorOptionEnabled() throws Throwable {
427     System.setProperty("migrationsHome", TestUtil.getTempDir().getAbsolutePath());
428     File basePath = TestUtil.getTempDir();
429     String output = SystemStubs.tapSystemOut(() -> {
430       Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "--color", "init"));
431     });
432     assertTrue(output.contains(ConsoleColors.GREEN + "SUCCESS"));
433     assertNotNull(basePath.list());
434     assertTrue(TestUtil.deleteDirectory(basePath), "delete temp dir");
435   }
436 
437   @EnabledForJreRange(maxVersion = 23)
438   @Test
439   void shouldColorizeFailureOutputIfColorOptionEnabled() throws Throwable {
440     System.setProperty("migrationsHome", TestUtil.getTempDir().getAbsolutePath());
441     File basePath = TestUtil.getTempDir();
442     String output = SystemStubs.tapSystemOut(() -> {
443       int exitCode = SystemStubs.catchSystemExit(() -> {
444         Migrator.main(TestUtil.args("--path=" + basePath.getAbsolutePath(), "--color", "new"));
445       });
446       assertEquals(1, exitCode);
447     });
448     assertTrue(output.contains(ConsoleColors.RED + "FAILURE"));
449     assertTrue(TestUtil.deleteDirectory(basePath), "delete temp dir");
450   }
451 
452   @EnabledForJreRange(maxVersion = 23)
453   @Test
454   void shouldShowErrorOnMissingChangelog() throws Throwable {
455     // gh-220
456     try {
457       System.setProperty("migrations_changelog", "changelog1");
458       String output = SystemStubs.tapSystemOut(() -> {
459         Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "up", "1"));
460       });
461       assertFalse(output.contains("FAILURE"));
462 
463       System.setProperty("migrations_changelog", "changelog2");
464       output = SystemStubs.tapSystemOut(() -> {
465         int exitCode = SystemStubs.catchSystemExit(() -> {
466           Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "pending"));
467         });
468         assertEquals(1, exitCode);
469       });
470       assertTrue(output.contains("FAILURE"));
471       assertTrue(output.contains("Change log doesn't exist, no migrations applied.  Try running 'up' instead."));
472     } finally {
473       System.clearProperty("migrations_changelog");
474     }
475   }
476 
477   @Test
478   void testInfoCommand() throws Exception {
479     String output = SystemStubs.tapSystemOut(() -> {
480       Migrator.main(TestUtil.args("info"));
481     });
482     assertFalse(output.contains("null"), output);
483   }
484 
485   @Test
486   void testInfoWithNonExistentBasePath() throws Exception {
487     File baseDir = TestUtil.getTempDir();
488     assertTrue(baseDir.delete()); // remove empty dir
489     assertFalse(baseDir.exists(), "directory does not exist");
490     String output = SystemStubs.tapSystemOut(() -> {
491       Migrator.main(TestUtil.args("info", "--path=" + baseDir.getAbsolutePath()));
492     });
493     assertFalse(output.contains("Migrations path must be a directory"), "base path not required for info");
494     assertFalse(output.contains("null"), output);
495   }
496 
497   @Test
498   void testInitWithNonExistentBasePath() throws Exception {
499     File baseDir = TestUtil.getTempDir();
500     assertTrue(baseDir.delete()); // remove empty dir
501     assertFalse(baseDir.exists(), "directory does not exist");
502     String output = SystemStubs
503         .tapSystemOut(() -> Migrator.main(TestUtil.args("init", "--path=" + baseDir.getAbsolutePath())));
504     assertFalse(output.contains("Migrations path must be a directory"), output);
505     assertTrue(Files.exists(Path.of(baseDir.getCanonicalPath(), "README")), "README created");
506     assertTrue(Files.isDirectory(Path.of(baseDir.getCanonicalPath(), "environments")),
507         "environments directory created");
508     assertTrue(TestUtil.deleteDirectory(baseDir), "delete temp dir");
509   }
510 
511   private void testStatusContainsPendingMigrations() throws Exception {
512     String output = SystemStubs.tapSystemOut(() -> {
513       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "status"));
514     });
515     assertFalse(output.contains("FAILURE"));
516     assertTrue(output.contains("...pending..."));
517   }
518 
519   private void testStatusContainsNoPendingMigrations() throws Exception {
520     String output = SystemStubs.tapSystemOut(() -> {
521       Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "status"));
522     });
523     assertFalse(output.contains("FAILURE"));
524     assertFalse(output.contains("...pending..."));
525   }
526 
527 }