|
| 1 | +package me.rosuh.easywatermark.data.db |
| 2 | + |
| 3 | +import kotlinx.cinterop.ExperimentalForeignApi |
| 4 | +import kotlinx.coroutines.Dispatchers |
| 5 | +import kotlinx.coroutines.flow.first |
| 6 | +import kotlinx.coroutines.runBlocking |
| 7 | +import me.rosuh.easywatermark.data.repo.TemplateRepository |
| 8 | +import me.rosuh.easywatermark.domain.TemplateEditor |
| 9 | +import okio.FileSystem |
| 10 | +import okio.Path.Companion.toPath |
| 11 | +import platform.Foundation.NSBundle |
| 12 | +import platform.Foundation.NSFileManager |
| 13 | +import platform.Foundation.NSTemporaryDirectory |
| 14 | +import platform.Foundation.NSUUID |
| 15 | +import kotlin.test.Test |
| 16 | +import kotlin.test.assertEquals |
| 17 | +import kotlin.test.assertFailsWith |
| 18 | +import kotlin.test.assertTrue |
| 19 | + |
| 20 | +/** |
| 21 | + * S4d-232: iOS runtime proof of the **seeded** template DB path (the iOS analogue of the desktopTest |
| 22 | + * `TemplateDatabaseSeedingTest`). RUNS on `iosSimulatorArm64Test`. |
| 23 | + * |
| 24 | + * A Kotlin/Native test executable's bundle does NOT carry the app's Copy Bundle Resources (see |
| 25 | + * `IosFontLoaderTest`), so the test cannot read the bundled `ewm-db-*.db` via `NSBundle.mainBundle`. |
| 26 | + * Instead it proves the platform-agnostic **seed-copy-then-open** mechanism end-to-end with a real Room |
| 27 | + * SQLite file as the seed: build an empty DB, add rows, close (so the on-disk file is a valid, complete |
| 28 | + * Room DB matching the commonMain schema + identity hash), read its bytes, then seed a FRESH dir from those |
| 29 | + * bytes via `buildTemplateDatabase(dir, seedBytes)` and assert the rows are present. This is exactly the |
| 30 | + * mechanism the production no-arg `buildTemplateDatabase()` uses with the bundled Android seed; that the |
| 31 | + * specific Android `ewm-db-{ch,eng}.db` files open under `BundledSQLiteDriver` is already proven on Desktop |
| 32 | + * (S4d-224, identical driver + commonMain schema). The bundled-resource RUN itself is exercised by a real |
| 33 | + * `iosApp.app` (xcodebuild-packaged), not by this test executable. |
| 34 | + */ |
| 35 | +class TemplateSeedRoundtripTest { |
| 36 | + |
| 37 | + @OptIn(ExperimentalForeignApi::class) |
| 38 | + private fun uniqueDir(tag: String): String { |
| 39 | + val dir = NSTemporaryDirectory() + "s4d232_${tag}_" + NSUUID().UUIDString() |
| 40 | + NSFileManager.defaultManager.createDirectoryAtPath( |
| 41 | + path = dir, |
| 42 | + withIntermediateDirectories = true, |
| 43 | + attributes = null, |
| 44 | + error = null, |
| 45 | + ) |
| 46 | + return dir |
| 47 | + } |
| 48 | + |
| 49 | + @Test |
| 50 | + fun ios_seeded_db_opens_with_seeded_rows() = runBlocking { |
| 51 | + // 1) Produce a valid Room DB file (correct schema + identity hash) to use as the seed. |
| 52 | + val seedDir = uniqueDir("seedsrc") |
| 53 | + val seedDb = buildTemplateDatabase(seedDir) |
| 54 | + try { |
| 55 | + val seedEditor = TemplateEditor(TemplateRepository(seedDb.templateDao(), Dispatchers.Default)) |
| 56 | + seedEditor.add("seed template A") |
| 57 | + seedEditor.add("seed template B") |
| 58 | + } finally { |
| 59 | + seedDb.close() // close checkpoints so the on-disk `ewm-db` file is complete |
| 60 | + } |
| 61 | + val seedBytes = FileSystem.SYSTEM.read("$seedDir/ewm-db".toPath()) { readByteArray() } |
| 62 | + assertTrue(seedBytes.isNotEmpty(), "produced seed DB file must be non-empty") |
| 63 | + |
| 64 | + // 2) Seed a FRESH dir from those bytes and assert the rows load through TemplateRepository. |
| 65 | + val freshDir = uniqueDir("seeded") |
| 66 | + val db = buildTemplateDatabase(freshDir, seedBytes = seedBytes) |
| 67 | + try { |
| 68 | + val repo = TemplateRepository(db.templateDao(), Dispatchers.Default) |
| 69 | + val rows = repo.getAllTemplate().first() |
| 70 | + assertEquals(2, rows.size, "seeded DB must expose the 2 seeded templates") |
| 71 | + assertTrue(rows.any { it.content == "seed template A" }, "seeded row A present") |
| 72 | + assertTrue(rows.any { it.content == "seed template B" }, "seeded row B present") |
| 73 | + } finally { |
| 74 | + db.close() |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + @Test |
| 79 | + fun ios_seeded_overload_with_null_bytes_is_empty_store() = runBlocking { |
| 80 | + // Parity with the empty-store builder: null seed bytes → a fresh empty DB. |
| 81 | + val db = buildTemplateDatabase(uniqueDir("nullseed"), seedBytes = null) |
| 82 | + try { |
| 83 | + val repo = TemplateRepository(db.templateDao(), Dispatchers.Default) |
| 84 | + assertTrue(repo.getAllTemplate().first().isEmpty(), "null seed bytes → empty store") |
| 85 | + } finally { |
| 86 | + db.close() |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + @Test |
| 91 | + fun template_seed_loader_loud_failure_for_missing_resource() { |
| 92 | + // The test executable's bundle has no packaged seed → loud failure (mirrors IosFontLoaderTest). |
| 93 | + val e = assertFailsWith<IllegalStateException> { |
| 94 | + IosTemplateSeed.loadSeedBytes(language = "definitely-missing-lang-xyz", bundle = NSBundle.mainBundle) |
| 95 | + } |
| 96 | + assertTrue( |
| 97 | + e.message?.contains("definitely-missing-lang-xyz") == true, |
| 98 | + "error must name the missing seed resource; was: ${e.message}", |
| 99 | + ) |
| 100 | + } |
| 101 | + |
| 102 | + @Test |
| 103 | + fun template_seed_language_selection_matches_android_rule() { |
| 104 | + // Default-language rule mirrors Android/Desktop: only "zh*" → ch, else eng. Pure value check. |
| 105 | + assertEquals(IosTemplateSeed.LANGUAGE_CH, "ch") |
| 106 | + assertEquals(IosTemplateSeed.LANGUAGE_ENG, "eng") |
| 107 | + } |
| 108 | +} |
0 commit comments