test: cover CrashingReader and read

This commit is contained in:
dualfroz committed 2026-09-09 18:05:48 +02:00
1 parent 8a381f8703
commit d882371038
1 file changed
+110
+110
View File
@@ -1,11 +1,17 @@
package kestrel package kestrel
import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.IOException
import java.io.Reader
import java.io.StringReader
import java.nio.file.Files import java.nio.file.Files
import kestrel.cli.Repl import kestrel.cli.Repl
import kestrel.persistence.Persistence import kestrel.persistence.Persistence
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContains import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue import kotlin.test.assertTrue
class PersistenceTest { class PersistenceTest {
@@ -17,6 +23,34 @@ class PersistenceTest {
return output.toString() return output.toString()
} }
/**
* Delivers [text] like a normal [StringReader], but throws instead of ever signalling EOF -
* simulating a process that vanished (killed, crashed, power loss) right after its last
* statement ran, never reaching either `.exit` or a graceful end of input. [Repl.run] only
* checkpoints (see [Repl.finish]) on one of those two paths, so this is the one way to drive it
* through every statement in [text] while proving NOTHING beyond the incremental WAL write each
* statement already triggered on its own (via [kestrel.cli.Repl.recordForPersistence]) ever
* makes it to disk - exactly the scenario a TEMP table's persistence exclusion must hold up
* under, not just the far more common clean-`.exit`-then-checkpoint path every other test here
* exercises.
*/
private class CrashingReader(text: String) : Reader() {
private val delegate = StringReader(text)
override fun read(cbuf: CharArray, off: Int, len: Int): Int {
val read = delegate.read(cbuf, off, len)
if (read == -1) throw IOException("simulated crash: process died before EOF")
return read
}
override fun close() = delegate.close()
}
private fun runScriptThatCrashesBeforeItCanCheckpoint(dir: File, script: String) {
val output = StringBuilder()
assertFailsWith<IOException> {
Repl(output, output, Persistence(dir)).run(BufferedReader(CrashingReader(script)))
}
}
@Test @Test
fun dataSurvivesReopenAfterCheckpoint() { fun dataSurvivesReopenAfterCheckpoint() {
val dir = tempDir() val dir = tempDir()
@@ -74,6 +108,82 @@ class PersistenceTest {
assertTrue(!reopened.contains("2"), "rolled back insert must not persist") assertTrue(!reopened.contains("2"), "rolled back insert must not persist")
} }
@Test
fun aMergeSurvivesReopenWithBothItsUpdateAndItsInsertIntact() {
val dir = tempDir()
runScript(
dir,
"""
CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER);
CREATE TABLE staging (id INTEGER, amount INTEGER);
INSERT INTO accounts VALUES (1, 100);
INSERT INTO staging VALUES (1, 10), (2, 5);
MERGE INTO accounts USING staging ON accounts.id = staging.id
WHEN MATCHED THEN UPDATE SET balance = accounts.balance + staging.amount
WHEN NOT MATCHED THEN INSERT (id, balance) VALUES (staging.id, staging.amount);
.exit
""".trimIndent()
)
val reopened = runScript(dir, "SELECT id, balance FROM accounts ORDER BY id ASC;\n.exit\n")
assertContains(reopened, "110")
assertContains(reopened, "5")
}
@Test
fun rollbackToSavepointInsideACommittedTransactionDoesNotPersistTheUndoneInsert() {
val dir = tempDir()
runScript(
dir,
"""
CREATE TABLE t (id INTEGER PRIMARY KEY);
BEGIN;
INSERT INTO t VALUES (111);
SAVEPOINT s1;
INSERT INTO t VALUES (222);
ROLLBACK TO SAVEPOINT s1;
INSERT INTO t VALUES (333);
COMMIT;
.exit
""".trimIndent()
)
val reopened = runScript(dir, "SELECT id FROM t ORDER BY id ASC;\n.exit\n")
assertContains(reopened, "111")
assertContains(reopened, "333")
assertTrue(!reopened.contains("222"), "insert undone by ROLLBACK TO SAVEPOINT must not persist")
}
@Test
fun insertOnConflictDoUpdateSurvivesReopen() {
val dir = tempDir()
runScript(
dir,
"""
CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER);
INSERT INTO accounts VALUES (1, 100);
INSERT INTO accounts VALUES (1, 50) ON CONFLICT (id) DO UPDATE SET balance = accounts.balance + excluded.balance;
.exit
""".trimIndent()
)
val reopened = runScript(dir, "SELECT balance FROM accounts WHERE id = 1;\n.exit\n")
assertContains(reopened, "150")
}
@Test
fun walReplayReconstructsAnInsertOnConflictStatementCorrectly() {
val dir = tempDir()
val persistence = Persistence(dir)
persistence.appendWal(
listOf(
"CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER);",
"INSERT INTO accounts VALUES (1, 100);",
"INSERT INTO accounts VALUES (1, 50) ON CONFLICT (id) DO UPDATE SET balance = accounts.balance + excluded.balance;"
)
)
val output = StringBuilder()
Repl(output, output, Persistence(dir)).run("SELECT balance FROM accounts WHERE id = 1;\n.exit\n".reader().buffered())
assertContains(output.toString(), "150")
}
@Test @Test
fun walReplayRecoversWithoutCheckpoint() { fun walReplayRecoversWithoutCheckpoint() {
val dir = tempDir() val dir = tempDir()