test: cover CrashingReader and read
This commit is contained in:
1 parent
8a381f8703
commit
d882371038
1 file changed
+110
@@ -1,11 +1,17 @@
|
||||
package kestrel
|
||||
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.Reader
|
||||
import java.io.StringReader
|
||||
import java.nio.file.Files
|
||||
import kestrel.cli.Repl
|
||||
import kestrel.persistence.Persistence
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContains
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class PersistenceTest {
|
||||
@@ -17,6 +23,34 @@ class PersistenceTest {
|
||||
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
|
||||
fun dataSurvivesReopenAfterCheckpoint() {
|
||||
val dir = tempDir()
|
||||
@@ -74,6 +108,82 @@ class PersistenceTest {
|
||||
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
|
||||
fun walReplayRecoversWithoutCheckpoint() {
|
||||
val dir = tempDir()
|
||||
|
||||
Reference in new issue
Block a user