ludic/packages/ludic.save/tests/save_test.ludic

154 lines
6.3 KiB
Text

# save_test.ludic - a toy "note" format at version 3 with two steps: a torn file told from a whole
# one, the chain walked from 1, a newer file refused and read-only, the backup standing in, and a
# write that backs up only a whole file and is read back
import "ludic.save"
program SaveTest {
numbers float
# 1 -> 2: "text" renamed "body"; 2 has no step; 3 is today
function mig_1_2(v: Val) -> void {
if value_has(v, "text") != 0 { value_put(v, "body", value_get(v, "text")) }
value_put(v, "moved", value_int(1))
}
function mig_2_3(v: Val) -> void { value_put(v, "pages", value_int(save_int(v, "pages", 0) + 10)) }
def SaveMigrations note_1_2 { format: "note", from: 1, run: fn mig_1_2 }
def SaveMigrations note_2_3 { format: "note", from: 2, run: fn mig_2_3 }
def SaveMigrations other_1_2 { format: "other", from: 1, run: fn mig_2_3 }
function is_note(v: Val) -> bool { return value_has(v, "body") != 0 or value_has(v, "text") != 0 }
function fmt() -> SaveFormat {
let f = save_format("note", 3)
f.is_one = fn is_note
return f
}
function dir() -> string { return Os.temp_dir() + "/ludic-save-test" }
function path() -> string { return dir() + "/note.json" }
function bak() -> string { return dir() + "/note.bak.json" }
function fresh() -> void {
Fs.mkdir(dir())
Fs.remove(path())
Fs.remove(bak())
}
test "a torn file is told from a whole one" {
expect(save_text_intact("{\"a\": [1, 2], \"b\": \"}{\"}"))
expect(not save_text_intact("{\"day\":6,\"money\":7"))
expect(not save_text_intact("{\"a\": \"cut"))
expect(not save_text_intact("]["))
expect(not save_text_intact("plain"))
expect(save_text_intact("{\"q\": \"a \\\" b\"}"))
}
test "nothing, rubbish and another kind of file" {
expect_eq(save_parse(null, fmt()).status, SAVE_NONE)
expect_eq(save_parse("[1, 2]", fmt()).status, SAVE_CORRUPT)
expect_eq(save_parse("{\"day\": 4}", fmt()).status, SAVE_CORRUPT)
expect_eq(save_parse("{\"body\": \"hi\", \"version\": 3}", fmt()).status, SAVE_OK)
}
test "a file with no version is 1, and walks every step of its own format" {
let r = save_parse("{\"text\": \"hi\"}", fmt())
expect(save_upgrade(r, fmt()))
expect_eq(r.from, 1)
expect(r.migrated)
expect(value_as_str(value_get(r.value, "body")) == "hi")
expect_eq(save_int(r.value, "moved", 0), 1)
expect_eq(save_int(r.value, "pages", 0), 10)
expect_eq(save_int(r.value, "version", 0), 3)
expect_eq(save_steps_doubled(fmt()), 0)
}
test "today's file is not migrated, and a step is not run twice" {
let r = save_parse("{\"body\": \"x\", \"version\": 2, \"pages\": 1}", fmt())
expect(save_upgrade(r, fmt()))
expect_eq(save_int(r.value, "pages", 0), 11)
expect_eq(save_int(r.value, "moved", 0), 0)
let t = save_parse("{\"body\": \"x\", \"version\": 3}", fmt())
expect(save_upgrade(t, fmt()))
expect(not t.migrated)
}
test "a newer build's file is refused and read-only, an older than the oldest is corrupt" {
let r = save_parse("{\"body\": \"x\", \"version\": 9}", fmt())
expect(not save_upgrade(r, fmt()))
expect_eq(r.status, SAVE_FUTURE)
expect(r.readonly)
expect(not save_write_to(r, path(), bak(), "{}"))
let f = fmt()
f.oldest = 2
let o = save_parse("{\"body\": \"x\", \"version\": 1}", f)
expect(not save_upgrade(o, f))
expect_eq(o.status, SAVE_CORRUPT)
}
test "a write backs up the old file only when it is whole, and is read back" {
fresh()
expect(save_write(path(), bak(), "{\"body\": \"one\", \"version\": 3}"))
expect(not Fs.exists(bak()))
expect(save_write(path(), bak(), "{\"body\": \"two\", \"version\": 3}"))
expect(Fs.read_text(bak()) == "{\"body\": \"one\", \"version\": 3}")
Fs.write_text(path(), "{\"body\": \"thr")
expect(save_write(path(), bak(), "{\"body\": \"four\", \"version\": 3}"))
expect(Fs.read_text(bak()) == "{\"body\": \"one\", \"version\": 3}")
}
test "a tree is written straight to the file, and the old file backed up only when whole" {
fresh()
let v = value_object()
value_put(v, "body", value_str("one"))
value_put(v, "version", value_int(3))
expect(save_write_tree(path(), bak(), v))
expect(not Fs.exists(bak()))
expect(Fs.read_text(path()) == Json.encode(v))
value_put(v, "body", value_str("two"))
expect(save_write_tree(path(), bak(), v))
expect(save_file_intact(bak()))
expect(save_open(path(), bak(), fmt()).status == SAVE_OK)
Fs.write_text(path(), "{\"body\": \"thr")
expect(save_write_tree(path(), bak(), v))
expect(save_int(Json.parse(Fs.read_text(bak())), "version", 0) == 3)
let r = save_open(path(), bak(), fmt())
r.readonly = true
expect(not save_write_tree_to(r, path(), bak(), v))
}
test "the backup stands in for a damaged file, and both damaged is read-only" {
fresh()
Fs.write_text(path(), "{\"body\": \"cut")
Fs.write_text(bak(), "{\"body\": \"good\", \"version\": 3}")
let r = save_open(path(), bak(), fmt())
expect_eq(r.status, SAVE_OK)
expect(r.from_backup)
expect(value_as_str(value_get(r.value, "body")) == "good")
Fs.write_text(bak(), "{\"body\": ")
let d = save_open(path(), bak(), fmt())
expect_eq(d.status, SAVE_CORRUPT)
expect(d.readonly)
Fs.remove(path())
let none = save_open(path(), bak(), fmt())
expect_eq(none.status, SAVE_NONE)
expect(not none.readonly)
}
test "a newer file is never replaced by its backup" {
fresh()
Fs.write_text(path(), "{\"body\": \"new\", \"version\": 7}")
Fs.write_text(bak(), "{\"body\": \"old\", \"version\": 3}")
let r = save_open(path(), bak(), fmt())
expect_eq(r.status, SAVE_FUTURE)
expect(r.readonly)
expect(not r.from_backup)
}
test "the helpers a migration uses, and the fingerprint moves with a name" {
let v = Json.parse("{\"a\": 1, \"b\": [5, 6, 7]}")
expect_eq(value_has(save_strip(v, "a"), "a"), 0)
let l = save_list_with(value_get(v, "b"), 1, 9)
expect_eq(value_as_int(value_at(l, 1)), 9)
let w = save_list_without(value_get(v, "b"), 0)
expect_eq(value_count(w), 2)
expect_eq(value_as_int(value_at(w, 0)), 6)
let h1 = save_fold(save_fold(17, "rope"), "cloth")
let h2 = save_fold(save_fold(17, "cloth"), "rope")
expect(h1 != h2)
expect(save_fold_int(17, 3) != save_fold_int(17, 4))
}
}