plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
5617070
— Peter John
2026-11-02T14:02:19+05:30
T7: incidental files
plum-cli/tests/game_web_test.rs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use std::process::Command;
|
|
3
|
+
|
|
4
|
+
fn plumBin() -> std::path::PathBuf {
|
|
5
|
+
let mut path = std::env::current_exe().unwrap();
|
|
6
|
+
path.pop();
|
|
7
|
+
if path.ends_with("deps") {
|
|
8
|
+
path.pop();
|
|
9
|
+
}
|
|
10
|
+
path.push("plum");
|
|
11
|
+
path
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[test]
|
|
15
|
+
fn webBuildWritesTheDist() {
|
|
16
|
+
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
17
|
+
.parent()
|
|
18
|
+
.unwrap();
|
|
19
|
+
let scratch = std::env::temp_dir().join(format!("plum_game_web_{}", std::process::id()));
|
|
20
|
+
let assets = scratch.join("assets");
|
|
21
|
+
let dist = scratch.join("dist");
|
|
22
|
+
// Stand-in asset files: the build copies them without reading them.
|
|
23
|
+
let sprites = [
|
|
24
|
+
"GUI_a", "GUIMain", "Houses_a", "Tileset", "Trees_a", "Units_a",
|
|
25
|
+
];
|
|
26
|
+
std::fs::create_dir_all(assets.join("sprites")).unwrap();
|
|
27
|
+
std::fs::create_dir_all(assets.join("data/defines")).unwrap();
|
|
28
|
+
for s in sprites {
|
|
29
|
+
std::fs::write(assets.join(format!("sprites/{s}.rxx")), b"x").unwrap();
|
|
30
|
+
}
|
|
31
|
+
std::fs::write(assets.join("data/defines/mapelem.dat"), b"x").unwrap();
|
|
32
|
+
std::fs::write(assets.join("extra.map"), b"x").unwrap();
|
|
33
|
+
|
|
34
|
+
let out = Command::new(plumBin())
|
|
35
|
+
.current_dir(root)
|
|
36
|
+
.args([
|
|
37
|
+
"game",
|
|
38
|
+
"--web",
|
|
39
|
+
"plum-game/tests/fixtures/host_fixture.plum",
|
|
40
|
+
"-o",
|
|
41
|
+
])
|
|
42
|
+
.arg(&dist)
|
|
43
|
+
.arg("--assets")
|
|
44
|
+
.arg(&assets)
|
|
45
|
+
.args(["--preload", "extra.map"])
|
|
46
|
+
.output()
|
|
47
|
+
.expect("failed to run plum");
|
|
48
|
+
let all = format!(
|
|
49
|
+
"{}{}",
|
|
50
|
+
String::from_utf8_lossy(&out.stdout),
|
|
51
|
+
String::from_utf8_lossy(&out.stderr)
|
|
52
|
+
);
|
|
53
|
+
assert!(out.status.success(), "plum game --web failed:\n{all}");
|
|
54
|
+
|
|
55
|
+
for f in [
|
|
56
|
+
"index.html",
|
|
57
|
+
"mq_js_bundle.js",
|
|
58
|
+
"plum_bridge.js",
|
|
59
|
+
"plum-game.wasm",
|
|
60
|
+
"plum.wasm",
|
|
61
|
+
"preload.txt",
|
|
62
|
+
] {
|
|
63
|
+
assert!(dist.join(f).is_file(), "missing {f}");
|
|
64
|
+
}
|
|
65
|
+
let preload = std::fs::read_to_string(dist.join("preload.txt")).unwrap();
|
|
66
|
+
let lines: Vec<&str> = preload.lines().collect();
|
|
67
|
+
for s in sprites {
|
|
68
|
+
assert!(
|
|
69
|
+
lines.contains(&format!("sprites/{s}.rxx").as_str()),
|
|
70
|
+
"{s} not preloaded"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
assert!(lines.contains(&"extra.map"));
|
|
74
|
+
for l in &lines {
|
|
75
|
+
assert!(
|
|
76
|
+
dist.join("assets").join(l).is_file(),
|
|
77
|
+
"assets/{l} not copied"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
let _ = std::fs::remove_dir_all(&scratch);
|
|
81
|
+
}
|
plum-game/web/bridge_check.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Usage: node bridge_check.mjs <plum.wasm>
|
|
2
|
+
//
|
|
3
|
+
// Loads a compiled Plum game and checks that plum_bridge.js satisfies every
|
|
4
|
+
// import, that every pg_* export it calls exists in src/web.rs, and that the
|
|
5
|
+
// module runs init/update/draw against a stub host.
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import vm from "node:vm";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const wasmPath = process.argv[2];
|
|
14
|
+
if (!wasmPath) {
|
|
15
|
+
console.error("usage: node bridge_check.mjs <plum.wasm>");
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
vm.runInThisContext(readFileSync(join(here, "plum_bridge.js"), "utf8"));
|
|
20
|
+
const { makeImports, hostNames } = globalThis.PlumBridge;
|
|
21
|
+
|
|
22
|
+
// Every pg_* name the plugin calls is exported by the Rust host.
|
|
23
|
+
const webRs = readFileSync(join(here, "../src/web.rs"), "utf8");
|
|
24
|
+
const exported = new Set([...webRs.matchAll(/extern "C" fn (pg_\w+)/g)].map((m) => m[1]));
|
|
25
|
+
for (const n of hostNames) assert.ok(exported.has(n), `web.rs does not export ${n}`);
|
|
26
|
+
|
|
27
|
+
const module = await WebAssembly.compile(readFileSync(wasmPath));
|
|
28
|
+
const imports = WebAssembly.Module.imports(module).filter((i) => i.module === "plum");
|
|
29
|
+
const importObject = makeImports(module, stubHost(), { plum: null });
|
|
30
|
+
const missing = imports.filter((i) => typeof importObject.plum[i.name] !== "function");
|
|
31
|
+
assert.deepEqual(missing, [], "plum imports without a binding");
|
|
32
|
+
|
|
33
|
+
// Stub host: records calls; handles load as failures.
|
|
34
|
+
function stubHost() {
|
|
35
|
+
const memory = new WebAssembly.Memory({ initial: 4 });
|
|
36
|
+
let bump = 1024;
|
|
37
|
+
const calls = [];
|
|
38
|
+
const host = new Proxy(
|
|
39
|
+
{ memory, calls, loaded: [] },
|
|
40
|
+
{
|
|
41
|
+
get(t, name) {
|
|
42
|
+
if (name in t) return t[name];
|
|
43
|
+
if (name === "pg_alloc") return (n) => ((bump += Math.max(n, 1)), bump - Math.max(n, 1));
|
|
44
|
+
if (name === "pg_free") return () => {};
|
|
45
|
+
return (...a) => {
|
|
46
|
+
calls.push([name, ...a]);
|
|
47
|
+
if (/load$/.test(name)) {
|
|
48
|
+
// (ptr, len): the Str arrived intact in host memory.
|
|
49
|
+
t.loaded.push(new TextDecoder().decode(new Uint8Array(memory.buffer, a[0], a[1])));
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
if (/screen_(width|height)/.test(name)) return 800;
|
|
53
|
+
return 0;
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
return host;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const host = stubHost();
|
|
62
|
+
const ctx = { plum: null };
|
|
63
|
+
const instance = await WebAssembly.instantiate(module, makeImports(module, host, ctx));
|
|
64
|
+
ctx.plum = instance.exports;
|
|
65
|
+
for (const n of ["strLen", "strByte", "strNew", "strSetByte", "init", "update", "draw"]) {
|
|
66
|
+
assert.equal(typeof ctx.plum[n], "function", `plum.wasm has no ${n} export`);
|
|
67
|
+
}
|
|
68
|
+
const state = ctx.plum.init();
|
|
69
|
+
ctx.plum.update(state, 0.016);
|
|
70
|
+
ctx.plum.draw(state);
|
|
71
|
+
assert.ok(host.calls.length > 0, "draw made no host calls");
|
|
72
|
+
const loads = host.loaded;
|
|
73
|
+
assert.ok(loads.every((s) => /^[\w .\/-]+$/.test(s)), `garbled Str: ${JSON.stringify(loads)}`);
|
|
74
|
+
console.log(`ok: ${imports.length} plum imports bound, ${host.calls.length} host calls in one frame (loads: ${JSON.stringify(loads)})`);
|