# 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)) } }