From ef02c55fa25556d2b7bdd09947b0537bc6511149 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Fri, 13 Feb 2026 07:44:53 +0900 Subject: [PATCH 1/4] =?UTF-8?q?LegacyMigrator=E3=81=AE=E8=A4=87=E6=95=B0?= =?UTF-8?q?=E3=83=90=E3=82=B0=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - userId にサーバーIDが代入される致命的バグを修正 (entry.getKey()を使用) - loadJson()でJsonSyntaxExceptionを捕捉し、空JSONファイルのNPEを防止 - 辞書マイグレーションを冪等化し、再実行時の二重登録を防止 - ファイル移動処理を改善し、部分的失敗時の整合性を確保 --- .../core/savedata/legacy/LegacyMigrator.java | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java b/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java index b98b39c..fdbb3b0 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java @@ -3,6 +3,7 @@ import com.google.common.io.Files; import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import dev.felnull.fnjl.util.FNDataUtil; import dev.felnull.fnjl.util.FNStringUtil; import dev.felnull.itts.core.dict.CustomDictionaryEntry; @@ -139,9 +140,9 @@ private void migrateServerUserData(DataRepository repo) { .forEach((entry) -> { long userId; try { - userId = Long.parseLong(name); + userId = Long.parseLong(entry.getKey()); } catch (NumberFormatException e) { - LOGGER.error("Invalid data name {}", file.getName()); + LOGGER.error("Invalid data name {}", entry.getKey()); return; } JsonObject entryJo = entry.getValue().getAsJsonObject(); @@ -238,7 +239,9 @@ private void migrateServerDictData(DataRepository repo) { Map dictEntry = loadDict(jo); CustomDictionaryData dictionaryData = repo.getServerCustomDictionaryData(serverId); dictEntry.forEach((target, read) -> { - dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD)); + if (dictionaryData.getByTarget(target).isEmpty()) { + dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD)); + } }); }); } @@ -255,7 +258,11 @@ private void migrateGlobalDictData(DataRepository repo) { Map dictEntry = loadDict(jo); CustomDictionaryData dictionaryData = repo.getGlobalCustomDictionaryData(); - dictEntry.forEach((target, read) -> dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD))); + dictEntry.forEach((target, read) -> { + if (dictionaryData.getByTarget(target).isEmpty()) { + dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD)); + } + }); } private Map loadDict(JsonObject jo) { @@ -280,11 +287,16 @@ private JsonObject loadJson(File file) { try (Reader reader = new FileReader(file); Reader bufReader = new BufferedReader(reader)) { jo = gson.fromJson(bufReader, JsonObject.class); - } catch (IOException e) { + } catch (IOException | JsonSyntaxException e) { LOGGER.error("Loading failed {}", file.getName(), e); return null; } + if (jo == null) { + LOGGER.error("Empty json file {}", file.getName()); + return null; + } + int version = JsonUtils.getInt(jo, "version", -1); if (version != 0) { LOGGER.error("Unsupported config version {}", file.getName()); @@ -324,13 +336,14 @@ public static void checkAndExecution(Supplier daoProvider) { File moveDir = new File("old_save_data-" + timeText); try { + FNDataUtil.wishMkdir(moveDir); + if (JSON_SAVE_DIR.exists()) { - Files.move(JSON_SAVE_DIR, moveDir); + Files.move(JSON_SAVE_DIR, new File(moveDir, JSON_SAVE_DIR.getName())); } if (GLOBAL_DICT_DIR.exists()) { - FNDataUtil.wishMkdir(moveDir); - Files.move(GLOBAL_DICT_DIR, new File(moveDir, "global_dict.json")); + Files.move(GLOBAL_DICT_DIR, new File(moveDir, GLOBAL_DICT_DIR.getName())); } } catch (IOException e) { throw new IllegalStateException("Failed to move Old SaveData", e); From b01ab9a7a099a14bc49f04dd262d5ec6e6f862a3 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Fri, 13 Feb 2026 08:13:34 +0900 Subject: [PATCH 2/4] =?UTF-8?q?LegacyMigrator=E3=81=AE=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E5=8F=AF=E8=83=BD=E3=81=AA=E6=A7=8B=E9=80=A0=E3=81=AB?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - パッケージプライベートコンストラクタとインスタンスフィールドでパス注入可能に - ファイル移動ロジックをmoveOldData()に切り出し - 全バグ修正をカバーするテストケースを追加 --- .../core/savedata/legacy/LegacyMigrator.java | 53 ++- .../savedata/legacy/LegacyMigratorTest.java | 354 ++++++++++++++++++ 2 files changed, 391 insertions(+), 16 deletions(-) create mode 100644 core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java b/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java index fdbb3b0..275bdc9 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java @@ -48,10 +48,39 @@ public class LegacyMigrator { */ private final Gson gson = new Gson(); + + /** + * Jsonの保存先ディレクトリ + */ + private final File jsonSaveDir; + + /** + * グローバル辞書ファイル + */ + private final File globalDictFile; + private LegacyMigrator() { + this(JSON_SAVE_DIR, GLOBAL_DICT_DIR); } - private void execute(DataRepository repo) { + LegacyMigrator(File jsonSaveDir, File globalDictFile) { + this.jsonSaveDir = jsonSaveDir; + this.globalDictFile = globalDictFile; + } + + void moveOldData(File moveDir) throws IOException { + FNDataUtil.wishMkdir(moveDir); + + if (jsonSaveDir.exists()) { + Files.move(jsonSaveDir, new File(moveDir, jsonSaveDir.getName())); + } + + if (globalDictFile.exists()) { + Files.move(globalDictFile, new File(moveDir, globalDictFile.getName())); + } + } + + void execute(DataRepository repo) { migrateServerData(repo); migrateServerUserData(repo); migrateServerDictUseData(repo); @@ -60,7 +89,7 @@ private void execute(DataRepository repo) { } private void migrateServerData(DataRepository repo) { - File dir = new File(JSON_SAVE_DIR, "server"); + File dir = new File(jsonSaveDir, "server"); if (!dir.exists()) { return; } @@ -105,7 +134,7 @@ private void migrateServerData(DataRepository repo) { } private void migrateServerUserData(DataRepository repo) { - File dir = new File(JSON_SAVE_DIR, "server_users"); + File dir = new File(jsonSaveDir, "server_users"); if (!dir.exists()) { return; } @@ -160,7 +189,7 @@ private void migrateServerUserData(DataRepository repo) { } private void migrateServerDictUseData(DataRepository repo) { - File dir = new File(JSON_SAVE_DIR, "dict_use"); + File dir = new File(jsonSaveDir, "dict_use"); if (!dir.exists()) { return; } @@ -211,7 +240,7 @@ private void migrateServerDictUseData(DataRepository repo) { } private void migrateServerDictData(DataRepository repo) { - File dir = new File(JSON_SAVE_DIR, "server_dict"); + File dir = new File(jsonSaveDir, "server_dict"); if (!dir.exists()) { return; } @@ -247,11 +276,11 @@ private void migrateServerDictData(DataRepository repo) { } private void migrateGlobalDictData(DataRepository repo) { - if (!GLOBAL_DICT_DIR.exists()) { + if (!globalDictFile.exists()) { return; } - JsonObject jo = loadJson(GLOBAL_DICT_DIR); + JsonObject jo = loadJson(globalDictFile); if (jo == null) { return; } @@ -336,15 +365,7 @@ public static void checkAndExecution(Supplier daoProvider) { File moveDir = new File("old_save_data-" + timeText); try { - FNDataUtil.wishMkdir(moveDir); - - if (JSON_SAVE_DIR.exists()) { - Files.move(JSON_SAVE_DIR, new File(moveDir, JSON_SAVE_DIR.getName())); - } - - if (GLOBAL_DICT_DIR.exists()) { - Files.move(GLOBAL_DICT_DIR, new File(moveDir, GLOBAL_DICT_DIR.getName())); - } + migrator.moveOldData(moveDir); } catch (IOException e) { throw new IllegalStateException("Failed to move Old SaveData", e); } diff --git a/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java b/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java new file mode 100644 index 0000000..3e57c5d --- /dev/null +++ b/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java @@ -0,0 +1,354 @@ +package dev.felnull.itts.core.savedata.legacy; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import dev.felnull.itts.core.dict.ReplaceType; +import dev.felnull.itts.core.savedata.dao.DAOFactory; +import dev.felnull.itts.core.savedata.repository.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.*; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +class LegacyMigratorTest { + + private static final long SERVER_ID = 1219628077353668688L; + private static final long USER_ID_1 = 1346500316383805532L; + private static final long USER_ID_2 = 328520268274204673L; + + private final Gson gson = new Gson(); + + private DataRepository createRepository(Path tempDir) { + File dbFile = new File(tempDir.toFile(), "test.db"); + DataRepository repo = DataRepository.create( + DAOFactory.getInstance().createSQLiteDAO(dbFile)); + repo.init(); + return repo; + } + + private void writeJson(File file, JsonObject jo) throws IOException { + file.getParentFile().mkdirs(); + try (Writer writer = new FileWriter(file)) { + gson.toJson(jo, writer); + } + } + + private void writeRawFile(File file, String content) throws IOException { + file.getParentFile().mkdirs(); + try (Writer writer = new FileWriter(file)) { + writer.write(content); + } + } + + private JsonObject createVersionedJson() { + JsonObject jo = new JsonObject(); + jo.addProperty("version", 0); + return jo; + } + + @Test + void testMigrateServerData(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject serverJson = createVersionedJson(); + serverJson.addProperty("ignore_regex", "(!|/|\\\\$|`).*"); + serverJson.addProperty("need_join", true); + serverJson.addProperty("overwrite_aloud", true); + serverJson.addProperty("notify_move", false); + serverJson.addProperty("read_limit", 100); + serverJson.addProperty("name_read_limit", 10); + + writeJson(new File(saveDir, "server/" + SERVER_ID + ".json"), serverJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + ServerData serverData = repo.getServerData(SERVER_ID); + assertNull(serverData.getDefaultVoiceType()); + assertEquals("(!|/|\\\\$|`).*", serverData.getIgnoreRegex()); + assertTrue(serverData.isNeedJoin()); + assertTrue(serverData.isOverwriteAloud()); + assertFalse(serverData.isNotifyMove()); + assertEquals(100, serverData.getReadLimit()); + assertEquals(10, serverData.getNameReadLimit()); + + repo.dispose(); + } + + @Test + void testMigrateServerUserData_userIdNotServerId(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject user1 = new JsonObject(); + user1.addProperty("deny", true); + user1.addProperty("nick_name", "テストユーザー1"); + + JsonObject user2 = new JsonObject(); + user2.addProperty("deny", false); + user2.addProperty("voice_type", "katyou"); + + JsonObject data = new JsonObject(); + data.add(String.valueOf(USER_ID_1), user1); + data.add(String.valueOf(USER_ID_2), user2); + + JsonObject usersJson = createVersionedJson(); + usersJson.add("data", data); + + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), usersJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + ServerUserData userData1 = repo.getServerUserData(SERVER_ID, USER_ID_1); + assertTrue(userData1.isDeny()); + assertEquals("テストユーザー1", userData1.getNickName()); + assertNull(userData1.getVoiceType()); + + ServerUserData userData2 = repo.getServerUserData(SERVER_ID, USER_ID_2); + assertFalse(userData2.isDeny()); + assertEquals("katyou", userData2.getVoiceType()); + assertNull(userData2.getNickName()); + + repo.dispose(); + } + + @Test + void testMigrateServerDictData(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject data = new JsonObject(); + data.addProperty("test", "テスト"); + data.addProperty("hello", "こんにちは"); + + JsonObject dictJson = createVersionedJson(); + dictJson.add("data", data); + + writeJson(new File(saveDir, "server_dict/" + SERVER_ID + ".json"), dictJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + CustomDictionaryData dictData = repo.getServerCustomDictionaryData(SERVER_ID); + List entries = dictData.getAll(); + assertEquals(2, entries.size()); + + Set targets = entries.stream() + .map(e -> e.entry().target()) + .collect(Collectors.toSet()); + assertTrue(targets.contains("test")); + assertTrue(targets.contains("hello")); + + repo.dispose(); + } + + @Test + void testMigrateGlobalDictData(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject data = new JsonObject(); + data.addProperty("world", "せかい"); + data.addProperty("abc", "えーびーしー"); + + JsonObject dictJson = createVersionedJson(); + dictJson.add("data", data); + + writeJson(globalDict, dictJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + CustomDictionaryData dictData = repo.getGlobalCustomDictionaryData(); + List entries = dictData.getAll(); + assertEquals(2, entries.size()); + + Set targets = entries.stream() + .map(e -> e.entry().target()) + .collect(Collectors.toSet()); + assertTrue(targets.contains("world")); + assertTrue(targets.contains("abc")); + + entries.forEach(e -> assertEquals(ReplaceType.WORD, e.entry().replaceType())); + + repo.dispose(); + } + + @Test + void testMigrateServerDictUseData(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject data = new JsonObject(); + data.addProperty("server", 2); + data.addProperty("global", 3); + data.addProperty("unit", -1); + + JsonObject dictUseJson = createVersionedJson(); + dictUseJson.add("data", data); + + writeJson(new File(saveDir, "dict_use/" + SERVER_ID + ".json"), dictUseJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + DictionaryUseData serverDict = repo.getDictionaryUseData(SERVER_ID, "server"); + assertEquals(true, serverDict.isEnable()); + assertEquals(2, serverDict.getPriority()); + + DictionaryUseData globalDictUse = repo.getDictionaryUseData(SERVER_ID, "global"); + assertEquals(true, globalDictUse.isEnable()); + assertEquals(3, globalDictUse.getPriority()); + + DictionaryUseData unitDict = repo.getDictionaryUseData(SERVER_ID, "unit"); + assertEquals(false, unitDict.isEnable()); + assertNull(unitDict.getPriority()); + + repo.dispose(); + } + + @Test + void testMalformedJsonSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + writeRawFile(new File(saveDir, "server/" + SERVER_ID + ".json"), "{invalid json!!!"); + + JsonObject validUsersJson = createVersionedJson(); + JsonObject userData = new JsonObject(); + JsonObject user = new JsonObject(); + user.addProperty("deny", true); + userData.add(String.valueOf(USER_ID_1), user); + validUsersJson.add("data", userData); + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), validUsersJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + ServerUserData serverUserData = repo.getServerUserData(SERVER_ID, USER_ID_1); + assertTrue(serverUserData.isDeny()); + + repo.dispose(); + } + + @Test + void testEmptyJsonFileSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + writeRawFile(new File(saveDir, "server/" + SERVER_ID + ".json"), ""); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + repo.dispose(); + } + + @Test + void testDictMigrationIdempotent(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject serverDictData = new JsonObject(); + serverDictData.addProperty("aaa", "えーえーえー"); + serverDictData.addProperty("bbb", "びーびーびー"); + + JsonObject serverDictJson = createVersionedJson(); + serverDictJson.add("data", serverDictData); + writeJson(new File(saveDir, "server_dict/" + SERVER_ID + ".json"), serverDictJson); + + JsonObject globalDictData = new JsonObject(); + globalDictData.addProperty("xxx", "えっくすえっくすえっくす"); + + JsonObject globalDictJson = createVersionedJson(); + globalDictJson.add("data", globalDictData); + writeJson(globalDict, globalDictJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + migrator.execute(repo); + migrator.execute(repo); + + CustomDictionaryData serverDict = repo.getServerCustomDictionaryData(SERVER_ID); + assertEquals(2, serverDict.getAll().size()); + + CustomDictionaryData globalDictionary = repo.getGlobalCustomDictionaryData(); + assertEquals(1, globalDictionary.getAll().size()); + + repo.dispose(); + } + + @Test + void testMoveOldData_bothExist(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + File moveDir = new File(tempDir.toFile(), "old_save_data"); + + new File(saveDir, "server").mkdirs(); + writeRawFile(new File(saveDir, "server/123.json"), "{}"); + writeRawFile(globalDict, "{}"); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.moveOldData(moveDir); + + assertTrue(moveDir.exists()); + assertTrue(new File(moveDir, "save_data").exists()); + assertTrue(new File(moveDir, "save_data/server/123.json").exists()); + assertTrue(new File(moveDir, "global_dict.json").exists()); + + assertFalse(saveDir.exists()); + assertFalse(globalDict.exists()); + } + + @Test + void testMoveOldData_onlySaveDir(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + File moveDir = new File(tempDir.toFile(), "old_save_data"); + + saveDir.mkdirs(); + writeRawFile(new File(saveDir, "server/123.json"), "{}"); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.moveOldData(moveDir); + + assertTrue(new File(moveDir, "save_data").exists()); + assertFalse(new File(moveDir, "global_dict.json").exists()); + assertFalse(saveDir.exists()); + } + + @Test + void testMoveOldData_onlyGlobalDict(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + File moveDir = new File(tempDir.toFile(), "old_save_data"); + + writeRawFile(globalDict, "{}"); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.moveOldData(moveDir); + + assertTrue(moveDir.exists()); + assertFalse(new File(moveDir, "save_data").exists()); + assertTrue(new File(moveDir, "global_dict.json").exists()); + assertFalse(globalDict.exists()); + } +} From d741c092c3ed3ba31134d0339bd6b780a42a22ed Mon Sep 17 00:00:00 2001 From: yuu111 Date: Fri, 13 Feb 2026 08:31:45 +0900 Subject: [PATCH 3/4] =?UTF-8?q?LegacyMigrator=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AE=E3=83=96=E3=83=A9=E3=83=B3=E3=83=81=E3=82=AB=E3=83=90?= =?UTF-8?q?=E3=83=AC=E3=83=83=E3=82=B8=E3=82=92=E6=8B=A1=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ディレクトリ未存在時のスキップ - 不正ファイル名のスキップ - 不正ユーザーIDのスキップ - 未対応バージョンのスキップ - dataキー欠落時のスキップ --- .../savedata/legacy/LegacyMigratorTest.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java b/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java index 3e57c5d..9f357dc 100644 --- a/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java +++ b/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java @@ -296,6 +296,119 @@ void testDictMigrationIdempotent(@TempDir Path tempDir) throws IOException { repo.dispose(); } + @Test + void testNonExistentDirectoriesSkipped(@TempDir Path tempDir) { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + repo.dispose(); + } + + @Test + void testInvalidFileNameSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject validJson = createVersionedJson(); + validJson.add("data", new JsonObject()); + + writeJson(new File(saveDir, "server/not_a_number.json"), validJson); + writeJson(new File(saveDir, "server_users/invalid.json"), validJson); + writeJson(new File(saveDir, "dict_use/abc.json"), validJson); + writeJson(new File(saveDir, "server_dict/xyz.json"), validJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + repo.dispose(); + } + + @Test + void testInvalidUserIdSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject invalidUser = new JsonObject(); + invalidUser.addProperty("deny", true); + + JsonObject validUser = new JsonObject(); + validUser.addProperty("deny", true); + + JsonObject data = new JsonObject(); + data.add("not_a_number", invalidUser); + data.add(String.valueOf(USER_ID_1), validUser); + + JsonObject usersJson = createVersionedJson(); + usersJson.add("data", data); + + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), usersJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + ServerUserData userData = repo.getServerUserData(SERVER_ID, USER_ID_1); + assertTrue(userData.isDeny()); + + repo.dispose(); + } + + @Test + void testUnsupportedVersionSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject serverJson = new JsonObject(); + serverJson.addProperty("version", 1); + serverJson.addProperty("need_join", true); + + writeJson(new File(saveDir, "server/" + SERVER_ID + ".json"), serverJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + ServerData serverData = repo.getServerData(SERVER_ID); + assertFalse(serverData.isNeedJoin()); + + repo.dispose(); + } + + @Test + void testMissingDataKeySkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject noDataJson = createVersionedJson(); + + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), noDataJson); + writeJson(new File(saveDir, "dict_use/" + SERVER_ID + ".json"), noDataJson); + writeJson(new File(saveDir, "server_dict/" + SERVER_ID + ".json"), noDataJson); + writeJson(globalDict, noDataJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + CustomDictionaryData serverDict = repo.getServerCustomDictionaryData(SERVER_ID); + assertTrue(serverDict.getAll().isEmpty()); + + CustomDictionaryData globalDictData = repo.getGlobalCustomDictionaryData(); + assertTrue(globalDictData.getAll().isEmpty()); + + repo.dispose(); + } + @Test void testMoveOldData_bothExist(@TempDir Path tempDir) throws IOException { File saveDir = new File(tempDir.toFile(), "save_data"); From bf69f7c5789ecb46f2584d16acc8f1d4d982e6e1 Mon Sep 17 00:00:00 2001 From: yuu1111 Date: Sun, 8 Mar 2026 22:52:42 +0900 Subject: [PATCH 4/4] =?UTF-8?q?LegacyMigrator=E3=81=AB=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E5=88=86=E9=9B=A2=E5=87=A6=E7=90=86=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 各マイグレーションフェーズおよびファイル単位でエラーをキャッチし、 1件の失敗が他のマイグレーションを停止しないように改善。 repo.dispose()をfinallyブロックに移動しリソースリークを防止。 対応するエラー分離テストを追加。 --- .../core/savedata/legacy/LegacyMigrator.java | 267 ++++++++++-------- .../savedata/legacy/LegacyMigratorTest.java | 79 ++++++ 2 files changed, 226 insertions(+), 120 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java b/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java index 275bdc9..0170fe4 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java @@ -81,11 +81,19 @@ void moveOldData(File moveDir) throws IOException { } void execute(DataRepository repo) { - migrateServerData(repo); - migrateServerUserData(repo); - migrateServerDictUseData(repo); - migrateServerDictData(repo); - migrateGlobalDictData(repo); + runMigration("ServerData", () -> migrateServerData(repo)); + runMigration("ServerUserData", () -> migrateServerUserData(repo)); + runMigration("ServerDictUseData", () -> migrateServerDictUseData(repo)); + runMigration("ServerDictData", () -> migrateServerDictData(repo)); + runMigration("GlobalDictData", () -> migrateGlobalDictData(repo)); + } + + private void runMigration(String name, Runnable migration) { + try { + migration.run(); + } catch (Exception e) { + LOGGER.error("Migration failed for {}", name, e); + } } private void migrateServerData(DataRepository repo) { @@ -100,36 +108,40 @@ private void migrateServerData(DataRepository repo) { } Arrays.stream(files).forEach((file) -> { - String name = FNStringUtil.removeExtension(file.getName()); - long serverId; try { - serverId = Long.parseLong(name); - } catch (NumberFormatException e) { - LOGGER.error("Invalid file name {}", file.getName()); - return; - } + String name = FNStringUtil.removeExtension(file.getName()); + long serverId; + try { + serverId = Long.parseLong(name); + } catch (NumberFormatException e) { + LOGGER.error("Invalid file name {}", file.getName()); + return; + } - JsonObject jo = loadJson(file); - if (jo == null) { - return; - } + JsonObject jo = loadJson(file); + if (jo == null) { + return; + } - String defaultVoiceType = JsonUtils.getString(jo, "default_voice_type", null); - String ignoreRegex = JsonUtils.getString(jo, "ignore_regex", "(!|/|\\\\$|`).*"); - boolean needJoin = JsonUtils.getBoolean(jo, "need_join", false); - boolean overwriteAloud = JsonUtils.getBoolean(jo, "overwrite_aloud", false); - boolean notifyMove = JsonUtils.getBoolean(jo, "notify_move", true); - int readLimit = JsonUtils.getInt(jo, "read_limit", 200); - int nameReadLimit = JsonUtils.getInt(jo, "name_read_limit", 20); - - ServerData serverData = repo.getServerData(serverId); - serverData.setDefaultVoiceType(defaultVoiceType); - serverData.setIgnoreRegex(ignoreRegex); - serverData.setNeedJoin(needJoin); - serverData.setOverwriteAloud(overwriteAloud); - serverData.setNotifyMove(notifyMove); - serverData.setReadLimit(readLimit); - serverData.setNameReadLimit(nameReadLimit); + String defaultVoiceType = JsonUtils.getString(jo, "default_voice_type", null); + String ignoreRegex = JsonUtils.getString(jo, "ignore_regex", "(!|/|\\\\$|`).*"); + boolean needJoin = JsonUtils.getBoolean(jo, "need_join", false); + boolean overwriteAloud = JsonUtils.getBoolean(jo, "overwrite_aloud", false); + boolean notifyMove = JsonUtils.getBoolean(jo, "notify_move", true); + int readLimit = JsonUtils.getInt(jo, "read_limit", 200); + int nameReadLimit = JsonUtils.getInt(jo, "name_read_limit", 20); + + ServerData serverData = repo.getServerData(serverId); + serverData.setDefaultVoiceType(defaultVoiceType); + serverData.setIgnoreRegex(ignoreRegex); + serverData.setNeedJoin(needJoin); + serverData.setOverwriteAloud(overwriteAloud); + serverData.setNotifyMove(notifyMove); + serverData.setReadLimit(readLimit); + serverData.setNameReadLimit(nameReadLimit); + } catch (Exception e) { + LOGGER.error("Migration failed for file {}", file.getName(), e); + } }); } @@ -145,46 +157,50 @@ private void migrateServerUserData(DataRepository repo) { } Arrays.stream(files).forEach((file) -> { - String name = FNStringUtil.removeExtension(file.getName()); - long serverId; try { - serverId = Long.parseLong(name); - } catch (NumberFormatException e) { - LOGGER.error("Invalid file name {}", file.getName()); - return; - } + String name = FNStringUtil.removeExtension(file.getName()); + long serverId; + try { + serverId = Long.parseLong(name); + } catch (NumberFormatException e) { + LOGGER.error("Invalid file name {}", file.getName()); + return; + } - JsonObject jo = loadJson(file); - if (jo == null) { - return; - } + JsonObject jo = loadJson(file); + if (jo == null) { + return; + } - JsonObject dataJo = jo.getAsJsonObject("data"); - if (dataJo == null) { - return; - } + JsonObject dataJo = jo.getAsJsonObject("data"); + if (dataJo == null) { + return; + } - dataJo.entrySet().stream() - .filter(it -> it.getValue().isJsonObject()) - .forEach((entry) -> { - long userId; - try { - userId = Long.parseLong(entry.getKey()); - } catch (NumberFormatException e) { - LOGGER.error("Invalid data name {}", entry.getKey()); - return; - } - JsonObject entryJo = entry.getValue().getAsJsonObject(); - - String voiceType = JsonUtils.getString(entryJo, "voice_type", null); - boolean deny = JsonUtils.getBoolean(entryJo, "deny", false); - String nickName = JsonUtils.getString(entryJo, "nick_name", null); - - ServerUserData serverUserData = repo.getServerUserData(serverId, userId); - serverUserData.setVoiceType(voiceType); - serverUserData.setDeny(deny); - serverUserData.setNickName(nickName); - }); + dataJo.entrySet().stream() + .filter(it -> it.getValue().isJsonObject()) + .forEach((entry) -> { + long userId; + try { + userId = Long.parseLong(entry.getKey()); + } catch (NumberFormatException e) { + LOGGER.error("Invalid data name {}", entry.getKey()); + return; + } + JsonObject entryJo = entry.getValue().getAsJsonObject(); + + String voiceType = JsonUtils.getString(entryJo, "voice_type", null); + boolean deny = JsonUtils.getBoolean(entryJo, "deny", false); + String nickName = JsonUtils.getString(entryJo, "nick_name", null); + + ServerUserData serverUserData = repo.getServerUserData(serverId, userId); + serverUserData.setVoiceType(voiceType); + serverUserData.setDeny(deny); + serverUserData.setNickName(nickName); + }); + } catch (Exception e) { + LOGGER.error("Migration failed for file {}", file.getName(), e); + } }); } @@ -200,42 +216,46 @@ private void migrateServerDictUseData(DataRepository repo) { } Arrays.stream(files).forEach((file) -> { - String name = FNStringUtil.removeExtension(file.getName()); - long serverId; try { - serverId = Long.parseLong(name); - } catch (NumberFormatException e) { - LOGGER.error("Invalid file name {}", file.getName()); - return; - } + String name = FNStringUtil.removeExtension(file.getName()); + long serverId; + try { + serverId = Long.parseLong(name); + } catch (NumberFormatException e) { + LOGGER.error("Invalid file name {}", file.getName()); + return; + } - JsonObject jo = loadJson(file); - if (jo == null) { - return; - } + JsonObject jo = loadJson(file); + if (jo == null) { + return; + } - JsonObject dataJo = jo.getAsJsonObject("data"); - if (dataJo == null) { - return; - } + JsonObject dataJo = jo.getAsJsonObject("data"); + if (dataJo == null) { + return; + } - dataJo.entrySet().stream() - .filter(it -> it.getValue().isJsonPrimitive()) - .filter(it -> it.getValue().getAsJsonPrimitive().isNumber()) - .forEach((entry) -> { - String dictName = entry.getKey(); - int priority = entry.getValue().getAsInt(); - - DictionaryUseData dictionaryUseData = repo.getDictionaryUseData(serverId, dictName); - - if (priority >= 0) { - dictionaryUseData.setEnable(true); - dictionaryUseData.setPriority(priority); - } else { - dictionaryUseData.setEnable(false); - dictionaryUseData.setPriority(null); - } - }); + dataJo.entrySet().stream() + .filter(it -> it.getValue().isJsonPrimitive()) + .filter(it -> it.getValue().getAsJsonPrimitive().isNumber()) + .forEach((entry) -> { + String dictName = entry.getKey(); + int priority = entry.getValue().getAsInt(); + + DictionaryUseData dictionaryUseData = repo.getDictionaryUseData(serverId, dictName); + + if (priority >= 0) { + dictionaryUseData.setEnable(true); + dictionaryUseData.setPriority(priority); + } else { + dictionaryUseData.setEnable(false); + dictionaryUseData.setPriority(null); + } + }); + } catch (Exception e) { + LOGGER.error("Migration failed for file {}", file.getName(), e); + } }); } @@ -251,27 +271,31 @@ private void migrateServerDictData(DataRepository repo) { } Arrays.stream(files).forEach((file) -> { - String name = FNStringUtil.removeExtension(file.getName()); - long serverId; try { - serverId = Long.parseLong(name); - } catch (NumberFormatException e) { - LOGGER.error("Invalid file name {}", file.getName()); - return; - } - - JsonObject jo = loadJson(file); - if (jo == null) { - return; - } + String name = FNStringUtil.removeExtension(file.getName()); + long serverId; + try { + serverId = Long.parseLong(name); + } catch (NumberFormatException e) { + LOGGER.error("Invalid file name {}", file.getName()); + return; + } - Map dictEntry = loadDict(jo); - CustomDictionaryData dictionaryData = repo.getServerCustomDictionaryData(serverId); - dictEntry.forEach((target, read) -> { - if (dictionaryData.getByTarget(target).isEmpty()) { - dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD)); + JsonObject jo = loadJson(file); + if (jo == null) { + return; } - }); + + Map dictEntry = loadDict(jo); + CustomDictionaryData dictionaryData = repo.getServerCustomDictionaryData(serverId); + dictEntry.forEach((target, read) -> { + if (dictionaryData.getByTarget(target).isEmpty()) { + dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD)); + } + }); + } catch (Exception e) { + LOGGER.error("Migration failed for file {}", file.getName(), e); + } }); } @@ -355,9 +379,12 @@ public static void checkAndExecution(Supplier daoProvider) { // Jsonを読み取ってDBに書き込む LegacyMigrator migrator = new LegacyMigrator(); - migrator.execute(repo); - repo.dispose(); + try { + migrator.execute(repo); + } finally { + repo.dispose(); + } // 旧データを退避 DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); diff --git a/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java b/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java index 9f357dc..95da5b5 100644 --- a/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java +++ b/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java @@ -3,10 +3,12 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; import dev.felnull.itts.core.dict.ReplaceType; +import dev.felnull.itts.core.savedata.dao.DAO; import dev.felnull.itts.core.savedata.dao.DAOFactory; import dev.felnull.itts.core.savedata.repository.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; import java.io.*; import java.nio.file.Path; @@ -464,4 +466,81 @@ void testMoveOldData_onlyGlobalDict(@TempDir Path tempDir) throws IOException { assertTrue(new File(moveDir, "global_dict.json").exists()); assertFalse(globalDict.exists()); } + + @Test + void testFileErrorIsolation(@TempDir Path tempDir) throws Exception { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + long serverId1 = 111111111111111111L; + long serverId2 = 222222222222222222L; + + JsonObject server1 = createVersionedJson(); + server1.addProperty("need_join", true); + writeJson(new File(saveDir, "server/" + serverId1 + ".json"), server1); + + JsonObject server2 = createVersionedJson(); + server2.addProperty("need_join", true); + writeJson(new File(saveDir, "server/" + serverId2 + ".json"), server2); + + File dbFile = new File(tempDir.toFile(), "test.db"); + DAO dao = DAOFactory.getInstance().createSQLiteDAO(dbFile); + DAO spyDao = Mockito.spy(dao); + DAO.ServerDataTable spyTable = Mockito.spy(spyDao.serverDataTable()); + Mockito.doThrow(new IllegalStateException("test")) + .doCallRealMethod() + .when(spyTable) + .insertRecordIfNotExists(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.when(spyDao.serverDataTable()).thenReturn(spyTable); + + DataRepository repo = DataRepository.create(spyDao); + repo.init(); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + assertDoesNotThrow(() -> migrator.execute(repo)); + + boolean s1 = repo.getServerData(serverId1).isNeedJoin(); + boolean s2 = repo.getServerData(serverId2).isNeedJoin(); + assertTrue(s1 || s2, "At least one server should have been migrated"); + + repo.dispose(); + } + + @Test + void testPhaseErrorIsolation(@TempDir Path tempDir) throws Exception { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject serverJson = createVersionedJson(); + serverJson.addProperty("need_join", true); + writeJson(new File(saveDir, "server/" + SERVER_ID + ".json"), serverJson); + + JsonObject user = new JsonObject(); + user.addProperty("deny", true); + JsonObject data = new JsonObject(); + data.add(String.valueOf(USER_ID_1), user); + JsonObject usersJson = createVersionedJson(); + usersJson.add("data", data); + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), usersJson); + + File dbFile = new File(tempDir.toFile(), "test.db"); + DAO dao = DAOFactory.getInstance().createSQLiteDAO(dbFile); + DAO spyDao = Mockito.spy(dao); + DAO.ServerDataTable spyTable = Mockito.spy(spyDao.serverDataTable()); + Mockito.doThrow(new IllegalStateException("test")) + .when(spyTable) + .insertRecordIfNotExists(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.when(spyDao.serverDataTable()).thenReturn(spyTable); + + DataRepository repo = DataRepository.create(spyDao); + repo.init(); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + assertDoesNotThrow(() -> migrator.execute(repo)); + + ServerUserData userData = repo.getServerUserData(SERVER_ID, USER_ID_1); + assertTrue(userData.isDeny()); + + repo.dispose(); + } }