- every scratch file lives in `$TMPDIR/x_<pid>/` (tmp_dir/tmp_path in prelude.ludic), removed by main's new dispatch() → tmp_cleanup(); X_KEEP_TMP=1 keeps it. `x test` and `x check-*` may now run together. - `x test-lsp` (tools/x/lsp_test.ludic) replaces tools/test-lsp.py: the whole request stream is framed into one stdin file, the server runs to `exit`, and the response stream is parsed back by request id; adds a check that ludicc's own error is published on save - tools/x/forgejo.ludic replaces tools/ci/forgejo_release.py (curl with a 0600 header file; the token is no longer on the command line; LUDIC_FORGEJO_API for forks) - `x docs-palette --check` regenerates into scratch and compares, so the drift guard judges the working tree rather than git HEAD; the generator no longer emits a trailing blank line the formatter rejects - `x test-tools`: exit 2 from test-lsp / test-grammar.js is a visible skip, never a pass; the widget-prop test uses the `id: Root` syntax - tools/test-grammar.js: current vocabulary (property/model/handler/ prefab/scene/event/become/@Queries), LUDIC_NODE_MODULES, exit 2 on skip - tools/atlas.ludic rewritten in the current language (it did not compile) - operators.ludic / os.ludic registered in the suite - json.ludic: j_quote() writer helper Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
232 lines
17 KiB
Text
232 lines
17 KiB
Text
# pkg_test.ludic — the package-manager suite (issue #63), hermetic and offline.
|
|
#
|
|
# It stands up throwaway git repositories under /tmp and points the package
|
|
# manager at them with LUDIC_PKG_PROXY, so the whole fetch → MVS resolve →
|
|
# content-addressed store → project link → compile → run → verify pipeline runs
|
|
# with no network. Every check drives a fresh `bin/x` / `bin/ludicc` process the
|
|
# way a real project would.
|
|
|
|
# write `body` to `path`, creating parent directories
|
|
function pt_write(path: pointer, body: pointer) -> void {
|
|
run(`mkdir -p "$(dirname {path})"`)
|
|
write_file(path, body)
|
|
}
|
|
|
|
# commit the current tree of `dir` and tag it `tag`
|
|
function pt_commit_tag(dir: pointer, tag: pointer) -> void {
|
|
run(`git -C {dir} add -A`)
|
|
run(`git -C {dir} -c user.email=t@t.test -c user.name=tester commit -q -m {tag}`)
|
|
run(`git -C {dir} tag {tag}`)
|
|
}
|
|
|
|
function cmd_test_pkg() -> int {
|
|
PASS = 0
|
|
FAIL = 0
|
|
print("== package manager (issue #63): fetch + MVS resolve + store + link + build ==")
|
|
|
|
if not shq("command -v git >/dev/null 2>&1") {
|
|
skip("git unavailable — package-manager suite needs git")
|
|
return report()
|
|
}
|
|
|
|
let root = capture_line("pwd")
|
|
let work = `{tmp_dir()}/pkg_test`
|
|
let proxy = `{work}/proxy`
|
|
let store = `{work}/store`
|
|
let proj = `{work}/proj`
|
|
run(`rm -rf {work}`)
|
|
run(`mkdir -p {proxy} {store} {proj}`)
|
|
|
|
# ---- fixture packages -----------------------------------------------------
|
|
# example.test/util: a leaf source package, two published versions.
|
|
let util = `{proxy}/example.test/util`
|
|
run(`git -C {util} init -q 2>/dev/null || ( mkdir -p {util} && git -C {util} init -q )`)
|
|
pt_write(`{util}/package.ludic`, `package "example.test/util"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "Util"` + nl())
|
|
pt_write(`{util}/util.ludic`, "function util_tag() -> pointer { return \"u1\" }\n")
|
|
pt_commit_tag(util, "v1.0.0")
|
|
pt_write(`{util}/package.ludic`, `package "example.test/util"` + nl() + `version "1.2.0"` + nl() + "kind source" + nl() + `provides "Util"` + nl())
|
|
pt_write(`{util}/util.ludic`, "function util_tag() -> pointer { return \"u12\" }\n")
|
|
pt_commit_tag(util, "v1.2.0")
|
|
|
|
# example.test/greeter: requires util >=1.0.0, provides Greet.
|
|
let greeter = `{proxy}/example.test/greeter`
|
|
run(`mkdir -p {greeter} && git -C {greeter} init -q`)
|
|
pt_write(`{greeter}/package.ludic`, `package "example.test/greeter"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "Greet"` + nl() + `require "example.test/util" "1.0.0"` + nl())
|
|
pt_write(`{greeter}/greet.ludic`, "function greet_hello() -> pointer { return \"hi\" }\n")
|
|
pt_commit_tag(greeter, "v1.0.0")
|
|
|
|
# ---- the consumer project -------------------------------------------------
|
|
# requires util 1.2.0 directly; greeter (which requires util 1.0.0) is added
|
|
# too. MVS must pick util 1.2.0 — the greatest required minimum.
|
|
pt_write(`{proj}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl())
|
|
pt_write(`{proj}/app.ludic`, "program App {\n import \"example.test/greeter/greet.ludic\"\n import \"example.test/util/util.ludic\"\n entry { print(greet_hello()); print(util_tag()) }\n}\n")
|
|
|
|
let envp = `LUDIC_PKG_PROXY={proxy} LUDIC_STORE={store}`
|
|
|
|
# ---- x get: resolve, fetch, store, link, lock -----------------------------
|
|
if shq(`( cd {proj} && {envp} {root}/bin/x get > {work}/get.out 2>&1 )`) {
|
|
ok("x get resolves + fetches the dependency graph")
|
|
} else {
|
|
bad2("x get failed", capture_line(`tail -2 {work}/get.out`))
|
|
}
|
|
|
|
var lock = read_file(`{proj}/package.lock.ludic`)
|
|
if lock == null { lock = "" }
|
|
if s_contains(lock, "example.test/greeter") and s_contains(lock, "example.test/util") {
|
|
ok("package.lock.ludic pins the whole build list")
|
|
} else { bad("lockfile is missing a resolved package") }
|
|
|
|
# MVS: util resolves to 1.2.0 (max of the 1.0.0 and 1.2.0 minimums), not 1.0.0
|
|
if s_contains(lock, `"example.test/util" version "1.2.0"`) {
|
|
ok("MVS selects util 1.2.0 (greatest required minimum)")
|
|
} else { bad2("MVS picked the wrong util version", capture_line(`grep util {proj}/package.lock.ludic`)) }
|
|
|
|
if s_contains(lock, "sha256:") { ok("lockfile records content hashes") } else { bad("lockfile has no content hash") }
|
|
|
|
# the project view links into the content-addressed store
|
|
if file_exists(`{proj}/ludic_modules/example.test/greeter/greet.ludic`) and file_exists(`{proj}/ludic_modules/example.test/util/util.ludic`) {
|
|
ok("ludic_modules/ links the store entries into the project")
|
|
} else { bad("ludic_modules view was not linked") }
|
|
|
|
# ---- compile + run the consumer against the fetched packages --------------
|
|
if shq(`( cd {proj} && LUDIC_MODULES=ludic_modules {root}/bin/ludicc app.ludic -o app > {work}/build.out 2>&1 )`) {
|
|
ok("consumer compiles against the linked packages (do_import module-root fallback)")
|
|
let got = capture_line(`( cd {proj} && ./app )`)
|
|
if got == "hi u12" { ok("the running program uses the MVS-selected package code (\"hi u12\")") }
|
|
else { bad2("program output mismatch", `got [{got}] want [hi u12]`) }
|
|
} else {
|
|
bad2("consumer build failed", capture_line(`tail -2 {work}/build.out`))
|
|
}
|
|
|
|
# ---- x verify: content integrity against the store ------------------------
|
|
if shq(`( cd {proj} && {envp} {root}/bin/x verify > {work}/verify.out 2>&1 )`) {
|
|
ok("x verify passes on an untouched store")
|
|
} else { bad2("x verify failed unexpectedly", capture_line(`tail -2 {work}/verify.out`)) }
|
|
|
|
# tamper with a stored file — verify must now flag it
|
|
let uhash = capture_line(`grep 'example.test/util' {proj}/package.lock.ludic | sed 's/.*sha256://; s/\".*//'`)
|
|
run(`echo tampered >> {store}/{uhash}/util.ludic`)
|
|
if not shq(`( cd {proj} && {envp} {root}/bin/x verify > {work}/verify2.out 2>&1 )`) {
|
|
ok("x verify detects a tampered store entry")
|
|
} else { bad("x verify missed a tampered store entry") }
|
|
# heal the store: a content-addressed entry is keyed by its hash-named dir, so
|
|
# `x get` trusts an existing one — drop it first, then re-fetch a clean copy.
|
|
run(`rm -rf {store}/{uhash}`)
|
|
run(`( cd {proj} && {envp} {root}/bin/x get > /dev/null 2>&1 )`)
|
|
if shq(`( cd {proj} && {envp} {root}/bin/x verify > {work}/verify3.out 2>&1 )`) {
|
|
ok("re-fetching heals a tampered store entry (x verify green again)")
|
|
} else { bad2("store did not heal after re-fetch", capture_line(`tail -1 {work}/verify3.out`)) }
|
|
|
|
# ---- a package that exports ECS: a component, a model and a system --------
|
|
# Ludic is AOT, so a source package's declarations compile straight into the
|
|
# consumer's compile-time ECS — no ABI seam. This proves a consumer *game* can
|
|
# use a package's property (component), model, @OnSpawn handler (system) and a
|
|
# plain function together. The engine runtime (runtime/native/*.ludic) resolves
|
|
# relative to the compile CWD, so — exactly like `x app` — the game is compiled
|
|
# from the repo root with the package linked in via an absolute LUDIC_MODULES.
|
|
let epkg = `{proxy}/example.test/enginepkg`
|
|
run(`mkdir -p {epkg} && git -C {epkg} init -q`)
|
|
pt_write(`{epkg}/package.ludic`, `package "example.test/enginepkg"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "EnginePkg"` + nl())
|
|
pt_write(`{epkg}/lib.ludic`, "property Health { hp: int = 0, max: int = 0 }\nmodel Unit { Health }\n@Public @OnSpawn(Unit) handler Init { Health.hp = Health.max }\nfunction bonus() -> int { return 7 }\n")
|
|
pt_commit_tag(epkg, "v1.0.0")
|
|
let proj2 = `{work}/proj2`
|
|
pt_write(`{proj2}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/enginepkg" "1.0.0"` + nl())
|
|
pt_write(`{proj2}/game.ludic`, "program G {\n import \"example.test/enginepkg/lib.ludic\"\n entry {\n spawn Unit { Health { max: 50 } }\n let H = world_prop_id(\"Health\")\n let hp = world_field_id(H, \"hp\")\n let e = world_query_next(H, 0)\n print(world_get(e, H, hp))\n print(bonus())\n }\n}\n")
|
|
if shq(`( cd {proj2} && {envp} {root}/bin/x get > {work}/get2.out 2>&1 )`) {
|
|
if shq(`( cd {root} && LUDIC_MODULES={proj2}/ludic_modules {root}/bin/ludicc --headless {proj2}/game.ludic -o {proj2}/game > {work}/gbuild.out 2>&1 )`) {
|
|
let got = capture_line(`{proj2}/game`)
|
|
if got == "50 7" {
|
|
ok("a consumer game uses the package's component + model + system + function (\"50 7\")")
|
|
} else { bad2("imported ECS produced wrong output", `got [{got}] want [50 7]`) }
|
|
} else { bad2("game consuming an ECS package failed to build", capture_line(`tail -2 {work}/gbuild.out`)) }
|
|
} else { bad2("x get for the ECS package failed", capture_line(`tail -1 {work}/get2.out`)) }
|
|
|
|
# ---- namespace collision policy (v1: hard error) --------------------------
|
|
let dupe = `{proxy}/example.test/dupe`
|
|
run(`mkdir -p {dupe} && git -C {dupe} init -q`)
|
|
pt_write(`{dupe}/package.ludic`, `package "example.test/dupe"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "Util"` + nl())
|
|
pt_write(`{dupe}/dupe.ludic`, "function dupe_tag() -> pointer { return \"d\" }\n")
|
|
pt_commit_tag(dupe, "v1.0.0")
|
|
if not shq(`( cd {proj} && {envp} {root}/bin/x add example.test/dupe@1.0.0 > {work}/dupe.out 2>&1 )`) and shq(`grep -q collision {work}/dupe.out`) {
|
|
ok("two packages claiming the same Foo.* namespace is a hard error")
|
|
} else { bad2("collision not rejected", capture_line(`tail -1 {work}/dupe.out`)) }
|
|
# drop the bad dep again so the manifest is clean
|
|
pt_write(`{proj}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl())
|
|
|
|
# ---- prebuilt target matrix ----------------------------------------------
|
|
let pb = `{proxy}/example.test/prebuilt`
|
|
run(`mkdir -p {pb} && git -C {pb} init -q`)
|
|
pt_write(`{pb}/package.ludic`, `package "example.test/prebuilt"` + nl() + `version "1.0.0"` + nl() + "kind prebuilt" + nl() + `provides "Blob"` + nl() + `targets "made-up-target"` + nl())
|
|
pt_write(`{pb}/README`, "a prebuilt lib shipping no artifact for this host\n")
|
|
pt_commit_tag(pb, "v1.0.0")
|
|
if not shq(`( cd {proj} && {envp} {root}/bin/x add example.test/prebuilt@1.0.0 > {work}/pb.out 2>&1 )`) and shq(`grep -q 'prebuilt lib' {work}/pb.out`) {
|
|
ok("a prebuilt lib with no artifact for the build target is a hard error")
|
|
} else { bad2("prebuilt target matrix not enforced", capture_line(`tail -1 {work}/pb.out`)) }
|
|
pt_write(`{proj}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl())
|
|
|
|
# ---- x vendor: hermetic copy ---------------------------------------------
|
|
run(`( cd {proj} && {envp} {root}/bin/x get > /dev/null 2>&1 )`)
|
|
if shq(`( cd {proj} && {envp} {root}/bin/x vendor > {work}/vendor.out 2>&1 )`) and file_exists(`{proj}/vendor/example.test/util/util.ludic`) {
|
|
ok("x vendor copies the resolved packages into ./vendor")
|
|
} else { bad2("x vendor failed", capture_line(`tail -1 {work}/vendor.out`)) }
|
|
|
|
# ---- package-declarable engine-system + namespace (issue #62) -------------
|
|
# A source package registers a compile-time engine system (@EngineSystem) and a
|
|
# Foo.* namespace (@Namespace) with no compiler edit; a consumer game imports it
|
|
# and both take effect. Source path → runs everywhere (not dylib/macOS-gated).
|
|
let cp = `{proxy}/example.test/coachpack`
|
|
run(`mkdir -p {cp} && git -C {cp} init -q`)
|
|
pt_write(`{cp}/package.ludic`, `package "example.test/coachpack"` + nl() + `version "1.0.0"` + nl() + "kind source" + nl() + `provides "Coach"` + nl())
|
|
pt_write(`{cp}/coach.ludic`, "property Score { value: int = 0 }\n@EngineSystem(Score, Update) function esys_score() -> void {\n let P = world_prop_id(\"Score\")\n let e = world_query_next(P, 0)\n if e >= 0 { world_set(e, P, 0, world_get(e, P, 0) + 1) }\n}\n@Namespace(Coach) function coach_bonus() -> int { return 99 }\n")
|
|
pt_commit_tag(cp, "v1.0.0")
|
|
let proj4 = `{work}/proj4`
|
|
pt_write(`{proj4}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/coachpack" "1.0.0"` + nl())
|
|
pt_write(`{proj4}/game.ludic`, "program Game {\n import \"example.test/coachpack/coach.ludic\"\n model Player { Score }\n @OnSpawn(Player) handler Init { }\n var frames: int = 0\n handler Boot phase Start { let e = world_spawn(world_model_id(\"Player\")) }\n handler Run phase Update {\n frames = frames + 1\n if frames >= 5 {\n let P = world_prop_id(\"Score\")\n let e = world_query_next(P, 0)\n print(world_get(e, P, 0))\n print(Coach.bonus())\n quit()\n }\n }\n}\n")
|
|
if shq(`( cd {proj4} && {envp} {root}/bin/x get > {work}/get4.out 2>&1 )`) {
|
|
run(`LUDIC_HOME={root} LUDIC_MODULES={proj4}/ludic_modules {root}/bin/ludicc --headless {proj4}/game.ludic -o {proj4}/game 2>{work}/gc4.err`)
|
|
if file_exists(`{proj4}/game`) {
|
|
let got = capture_line(`printf 'aaaaa' | {proj4}/game`)
|
|
if got == "4 99" {
|
|
ok("a package registers an engine-system (@EngineSystem) + a namespace (@Namespace) with no compiler edit (\"4 99\")")
|
|
} else { bad2("package hooks produced wrong output", `got [{got}] want [4 99]`) }
|
|
} else { bad2("consumer of a @EngineSystem/@Namespace package failed to build", capture_line(`tail -1 {work}/gc4.err`)) }
|
|
} else { bad2("x get for the coach package failed", capture_line(`tail -1 {work}/get4.out`)) }
|
|
|
|
# ---- prebuilt binary package (issue #64): exported function + system + component
|
|
# A module is compiled to a dylib (x build-lib), fetched as a `kind prebuilt`
|
|
# dependency, and linked into a consumer game that never saw its source. At load
|
|
# the module registers a dynamic component and an @System(Update); the consumer
|
|
# also calls an exported function via `extern`. Native/dylib only → macOS-gated.
|
|
if not is_darwin() {
|
|
skip("prebuilt binary packages (dylib) — native/macOS only")
|
|
return report()
|
|
}
|
|
let tgt = `native-{capture_line("uname -m")}`
|
|
let mp = `{proxy}/example.test/manapack`
|
|
run(`mkdir -p {mp} && git -C {mp} init -q`)
|
|
pt_write(`{mp}/package.ludic`, `package "example.test/manapack"` + nl() + `version "1.0.0"` + nl() + "kind prebuilt" + nl() + `provides "Mana"` + nl() + `targets "{tgt}"` + nl())
|
|
pt_write(`{mp}/manapack.ludic`, "program ManaPack {\n function module_init() -> void { world_register_prop(\"Mana\", 2) }\n @System(Update) function mana_regen() -> void {\n let M = world_prop_id(\"Mana\")\n let e = world_query_next(M, 0)\n if e >= 0 { let v = world_get(e, M, 0); world_set(e, M, 0, v + 1) }\n }\n function mana_bonus() -> int { return 42 }\n}\n")
|
|
if shq(`( cd {mp} && LUDICC={root}/bin/ludicc {root}/bin/x build-lib manapack.ludic > {work}/bl.out 2>&1 )`) and file_exists(`{mp}/lib/{tgt}/libmanapack.dylib`) {
|
|
ok("x build-lib compiles a package module to a per-target dylib")
|
|
} else { bad2("x build-lib failed", capture_line(`tail -1 {work}/bl.out`)) }
|
|
pt_commit_tag(mp, "v1.0.0")
|
|
|
|
let proj3 = `{work}/proj3`
|
|
pt_write(`{proj3}/package.ludic`, `package "game"` + nl() + `version "0.0.0"` + nl() + `require "example.test/manapack" "1.0.0"` + nl())
|
|
pt_write(`{proj3}/game.ludic`, "program Game {\n property Dummy { x: int = 0 }\n model Ent { Dummy }\n @OnSpawn(Ent) handler Init { }\n extern function mana_bonus() -> int = \"fn_mana_bonus\"\n var frames: int = 0\n handler Boot phase Start {\n let e = world_spawn(world_model_id(\"Ent\"))\n let M = world_prop_id(\"Mana\")\n world_attach_dyn(e, M)\n world_set(e, M, 0, 0)\n }\n handler Run phase Update {\n frames = frames + 1\n if frames >= 4 {\n let M = world_prop_id(\"Mana\")\n let e = world_query_next(M, 0)\n print(world_get(e, M, 0))\n print(mana_bonus())\n quit()\n }\n }\n}\n")
|
|
if shq(`( cd {proj3} && {envp} {root}/bin/x get > {work}/get3.out 2>&1 )`) and file_exists(`{proj3}/ludic_modules/example.test/manapack/lib/{tgt}/libmanapack.dylib`) {
|
|
ok("x get fetches + links a prebuilt (kind prebuilt) package for the target")
|
|
} else { bad2("prebuilt x get failed", capture_line(`tail -1 {work}/get3.out`)) }
|
|
# compile the ECS consumer from the repo root (so runtime/native resolves), then
|
|
# link the module dylib via `x link-flags` (a real external build's link step).
|
|
run(`LUDIC_HOME={root} {root}/bin/ludicc --headless {proj3}/game.ludic --emit-llvm -o {proj3}/game.ll 2>{work}/gc.err`)
|
|
let flags = capture_line(`( cd {proj3} && LUDIC_STORE={store} {root}/bin/x link-flags )`)
|
|
if shq(`{cc()} -O2 -Wno-override-module {proj3}/game.ll {flags} -o {proj3}/game 2>{work}/gl.err`) {
|
|
let got = capture_line(`printf 'aaaaaa' | {proj3}/game`)
|
|
if got == "3 42" {
|
|
ok("a consumer links the prebuilt module: its system mutates the world + its function is callable (\"3 42\")")
|
|
} else { bad2("prebuilt consumer output mismatch", `got [{got}] want [3 42]`) }
|
|
} else { bad2("prebuilt consumer link failed", capture_line(`tail -1 {work}/gl.err`)) }
|
|
|
|
return report()
|
|
}
|