# update_test.ludic - the newest version and its notes, the plan (deltas or the whole package), and # the install step by step, against a fake network, fake children and a stand-in root on disk import "ludic.update" import "ludic.base" program UpdateTest { numbers float state UpdateTestState { net_answer: int = -1 net_text: string = "" net_urls: []string = null net_paths: []string = null net_got: int = 0 proc_code: int = -1 proc_paths: []string = null proc_args: []string = null exe: string = "" } function fake_platform() -> string { return "macos" } function fake_exe(update_test_st: UpdateTestState) -> string { return update_test_st.exe } function fake_pid() -> int { return 4242 } function fake_ms() -> int { return 0 } function fake_get(update_test_st: mut UpdateTestState, url: string) -> int { push(update_test_st.net_urls, url) push(update_test_st.net_paths, "") return len(update_test_st.net_urls) - 1 } function fake_download(update_test_st: mut UpdateTestState, url: string, path: string) -> int { push(update_test_st.net_urls, url) push(update_test_st.net_paths, path) return len(update_test_st.net_urls) - 1 } function fake_poll(update_test_st: UpdateTestState, h: int) -> int { return update_test_st.net_answer } function fake_text(update_test_st: UpdateTestState, h: int) -> string { return update_test_st.net_text } function fake_received(update_test_st: UpdateTestState, h: int) -> int { return update_test_st.net_got } function fake_free(h: int) -> void { } function fake_spawn(update_test_st: mut UpdateTestState, path: string, args: []string) -> int { push(update_test_st.proc_paths, path) push(update_test_st.proc_args, Text.join(args, " ")) return len(update_test_st.proc_paths) - 1 } function fake_proc_poll(update_test_st: UpdateTestState, h: int) -> int { return update_test_st.proc_code } function fake_proc_free(h: int) -> void { } bind UpdateWorld { platform: fn fake_platform, exe: fn fake_exe, pid: fn fake_pid, now_ms: fn fake_ms } bind UpdateNet { get: fn fake_get, download: fn fake_download, poll: fn fake_poll, text: fn fake_text, received: fn fake_received, free: fn fake_free } bind UpdateProc { spawn: fn fake_spawn, poll: fn fake_proc_poll, free: fn fake_proc_free } function dir() -> string { return Os.temp_dir() + "/ludic-update-test" } function pkgs() -> string { return dir() + "/packages" } function hashf() -> string { return dir() + "/hash.txt" } function fresh(update_st: mut UpdateState, update_test_st: mut UpdateTestState, root: bool) -> void { update_test_st.net_answer = -1 update_test_st.net_text = "" update_test_st.net_urls = new []string update_test_st.net_paths = new []string update_test_st.net_got = 0 update_test_st.proc_code = -1 update_test_st.proc_paths = new []string update_test_st.proc_args = new []string update_test_st.exe = dir() + "/Game.app/Contents/MacOS/Game" System.run("rm -rf '" + dir() + "'") Fs.mkdir(dir()) Fs.mkdir(pkgs()) configure(update_st, root) update_reset(update_st) } # whether this copy was installed is worked out when the updater is configured, so a test that # changes the disk or the executable's place configures again function configure(update_st: mut UpdateState, root: bool) -> void { let c = new UpdateConfig c.feed = "http://feed" c.app_id = "Game" c.version = "1.0.0" if root { c.root = dir() + "/Game.app" } c.packages = pkgs() c.scratch = hashf() update_config(update_st, c) } function asset(ty: string, ver: string, size: int) -> string { let file = "Game-" + ver + "-osx-" + Text.lower(ty) + ".nupkg" return "{\"PackageId\":\"Game\",\"Type\":\"" + ty + "\",\"Version\":\"" + ver + "\",\"FileName\":\"" + file + "\",\"SHA256\":\"ab" + ver + "\",\"Size\":" + string(size) + "}" } # 1.0.0 installed; 1.1.0 and 1.2.0 out, each with a delta function feed(with_delta_11: bool) -> string { var s = "{\"Assets\":[" + asset("Full", "1.0.0", 900) + "," + asset("Full", "1.1.0", 1000) + "," + asset("Full", "1.2.0", 1000) if with_delta_11 { s = s + "," + asset("Delta", "1.1.0", 50) } return s + "," + asset("Delta", "1.2.0", 60) + "]}" } function seed_base() -> void { Fs.write_text(pkgs() + "/Game-1.0.0-osx-full.nupkg", "base") } function plan_of(update_st: mut UpdateState, text: string) -> bool { return update_plan(update_st, Json.parse(text)) } function facts_of(update_st: UpdateState, what: int) -> int { let fs = q_drain(update_facts(update_st)) var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } function hash_is(h: string) -> void { Fs.write_text(hashf(), h + "\n") } function last_args(update_test_st: UpdateTestState) -> string { return update_test_st.proc_args[len(update_test_st.proc_args) - 1] } function last_url(update_test_st: UpdateTestState) -> string { return update_test_st.net_urls[len(update_test_st.net_urls) - 1] } test "versions compare a number at a time" { expect(update_newer("0.10.2", "0.9.9")) expect(not update_newer("0.5.1", "0.5.1")) expect(update_newer("1.0", "0.99.99")) expect(not update_newer("0.4.9", "0.5.0")) } test "a newer version is shown with its notes, in the language asked for or English" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) update_check_tick(update_st, false) expect(last_url(update_test_st) == "http://feed/latest.json") update_test_st.net_text = "{\"version\":\"1.2.0\",\"notes\":{\"en\":[\"Fish.\",\"Boats.\"],\"tr\":[\"Balik.\"]}}" update_test_st.net_answer = 1 update_check_tick(update_st, false) expect(update_fresh(update_st)) expect(update_latest(update_st) == "1.2.0") update_notes_for(update_st, "tr") # the game's tick keeps them for its language expect_eq(len(update_notes(update_st, "tr")), 1) expect_eq(len(update_notes(update_st, "de")), 0) # not kept for German yet update_notes_for(update_st, "de") expect_eq(len(update_notes(update_st, "de")), 2) # no German: English } test "the version being played is shown only when asked" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) update_test_st.net_text = "{\"version\":\"1.0.0\",\"notes\":{\"en\":[\"x\"]}}" update_test_st.net_answer = 1 update_check_tick(update_st, false) update_check_tick(update_st, false) expect(not update_shown(update_st)) update_reset(update_st) update_check_tick(update_st, true) update_check_tick(update_st, true) expect_eq(update_ask_state(update_st), UPDATE_CURRENT) update_reset(update_st) update_test_st.net_answer = 0 update_check_tick(update_st, true) update_check_tick(update_st, true) expect_eq(update_ask_state(update_st), UPDATE_NONE) } test "with the base on disk and every delta there, the plan is the chain, oldest first" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) seed_base() expect(plan_of(update_st, feed(true))) expect_eq(update_package_count(update_st), 2) expect(update_plan_is_delta(update_st, 0)) expect(update_plan_version(update_st, 0) == "1.1.0") expect(update_plan_version(update_st, 1) == "1.2.0") expect(update_target(update_st) == "1.2.0") } test "no base, or a missing delta, is the whole newest package" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) expect(plan_of(update_st, feed(true))) expect_eq(update_package_count(update_st), 1) expect(not update_plan_is_delta(update_st, 0)) expect(update_plan_file(update_st, 0) == "Game-1.2.0-osx-full.nupkg") seed_base() expect(plan_of(update_st, feed(false))) expect_eq(update_package_count(update_st), 1) } test "nothing newer is no plan" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) expect(not plan_of(update_st, "{\"Assets\":[" + asset("Full", "1.0.0", 900) + "]}")) expect(not plan_of(update_st, "{}")) } test "a copy Velopack did not install does not start" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, false) expect(not update_installed(update_st)) expect(not update_start(update_st)) expect_eq(update_error(update_st), UPDATE_ERR_NOT_INSTALLED) Fs.mkdir(dir() + "/Game.app") Fs.mkdir(dir() + "/Game.app/Contents") Fs.mkdir(dir() + "/Game.app/Contents/MacOS") Fs.write_text(dir() + "/Game.app/Contents/MacOS/UpdateMac", "x") configure(update_st, false) expect(update_installed(update_st)) update_test_st.exe = "/private/var/folders/x/AppTranslocation/y/Game.app/Contents/MacOS/Game" configure(update_st, false) expect(not update_installed(update_st)) } test "the whole package: fetched, checked, applied, and the game told to go" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) expect(update_start(update_st)) expect(last_url(update_test_st) == "http://feed/releases.osx.json") update_test_st.net_text = feed(true) update_test_st.net_answer = 1 update_tick(update_st) expect_eq(update_state(update_st), UPDATE_FETCH) expect_eq(facts_of(update_st, UPDATE_STARTED), 1) expect(last_url(update_test_st) == "http://feed/Game-1.2.0-osx-full.nupkg") update_test_st.net_got = 500 expect(update_progress(update_st) >= 490 and update_progress(update_st) <= 510) update_tick(update_st) expect_eq(update_state(update_st), UPDATE_CHECK) expect(Text.contains(last_args(update_test_st), "shasum -a 256")) hash_is("AB1.2.0") update_test_st.proc_code = 0 update_tick(update_st) expect_eq(update_state(update_st), UPDATE_APPLY) expect(Text.contains(last_args(update_test_st), "apply --silent --waitPid 4242 --package")) expect(Text.contains(last_args(update_test_st), "--rootDir")) expect(Text.ends_with(update_test_st.proc_paths[len(update_test_st.proc_paths) - 1], "Game.app/Contents/MacOS/UpdateMac")) expect_eq(facts_of(update_st, UPDATE_APPLYING), 1) } test "a wrong hash stops it and deletes the file" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) update_start(update_st) update_test_st.net_text = feed(true) update_test_st.net_answer = 1 update_tick(update_st) Fs.write_text(pkgs() + "/Game-1.2.0-osx-full.nupkg", "junk") update_tick(update_st) hash_is("FFFF") update_test_st.proc_code = 0 update_tick(update_st) expect(update_failed(update_st)) expect_eq(update_error(update_st), UPDATE_ERR_CORRUPT) expect(not Fs.exists(pkgs() + "/Game-1.2.0-osx-full.nupkg")) expect_eq(facts_of(update_st, UPDATE_STOPPED), 1) } test "a failed download stops it" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) update_start(update_st) update_test_st.net_text = feed(true) update_test_st.net_answer = 1 update_tick(update_st) update_test_st.net_answer = 0 update_tick(update_st) expect_eq(update_error(update_st), UPDATE_ERR_DOWNLOAD) } test "the delta chain: each fetched, checked and patched onto the last, then applied" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) seed_base() update_start(update_st) update_test_st.net_text = feed(true) update_test_st.net_answer = 1 update_tick(update_st) update_tick(update_st) hash_is("AB1.1.0") update_test_st.proc_code = 0 update_tick(update_st) expect_eq(update_state(update_st), UPDATE_PATCH) expect(Text.contains(last_args(update_test_st), "--old " + pkgs() + "/Game-1.0.0-osx-full.nupkg")) expect(Text.contains(last_args(update_test_st), "--output " + pkgs() + "/Game-1.1.0-osx-full.nupkg")) Fs.write_text(pkgs() + "/Game-1.1.0-osx-full.nupkg", "patched") update_tick(update_st) expect_eq(update_state(update_st), UPDATE_FETCH) expect_eq(update_index(update_st), 1) update_tick(update_st) hash_is("AB1.2.0") update_tick(update_st) Fs.write_text(pkgs() + "/Game-1.2.0-osx-full.nupkg", "patched") update_tick(update_st) expect_eq(update_state(update_st), UPDATE_APPLY) expect(Text.contains(last_args(update_test_st), "--package " + pkgs() + "/Game-1.2.0-osx-full.nupkg")) } test "a package already on disk at its size is checked, not fetched again" (update_st: mut UpdateState, update_test_st: mut UpdateTestState) { fresh(update_st, update_test_st, true) update_start(update_st) update_test_st.net_text = feed(true) update_test_st.net_answer = 1 Fs.write_text(pkgs() + "/Game-1.2.0-osx-full.nupkg", Text.repeat("x", 1000)) update_tick(update_st) expect_eq(update_state(update_st), UPDATE_CHECK) } }