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
|
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()
|
||||||
|
|||||||
Reference in new issue
Block a user