diff --git a/.gitignore b/.gitignore index f06235c..9c62828 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules +coverage dist diff --git a/package.json b/package.json index fa85480..8007f38 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "scripts": { "build": "esbuild-dev scripts/build.ts", - "test": "c8 --include=src esbuild-dev --no-warnings --loader test/index.ts" + "test": "c8 --include=src --reporter=html esbuild-dev --no-warnings --loader test/index.ts" }, "devDependencies": { "@hyrious/dts": "^0.2.1", diff --git a/src/ruby.ts b/src/ruby.ts index 3a09fad..149b5d2 100644 --- a/src/ruby.ts +++ b/src/ruby.ts @@ -34,7 +34,10 @@ export class RubyClass extends RubyBaseObject { } export class RubyModule extends RubyBaseObject { - constructor(public name: string, public old?: boolean) { + constructor( + public name: string, + public old?: boolean, + ) { super(); } } @@ -46,12 +49,9 @@ export class RubyHash { this.entries = entries || []; if (default_ !== undefined) this.default = default_; } - /** Returns a new object that is a ruby Hash with `compare_by_identity` enabled. */ - compareByIdentity(): RubyObject { - var obj = new RubyObject(Symbol.for("Hash")); - obj.wrapped = this; - return obj; - } + // compareByIdentity() { + // throw new Error("Not implemented"); + // } } export class RubyRange extends RubyObject { diff --git a/test/dump.ts b/test/dump.ts index b573a58..78754d5 100644 --- a/test/dump.ts +++ b/test/dump.ts @@ -4,7 +4,7 @@ import { describe, rb_load } from "./helper"; function dumps( value: unknown, - opts: { pre?: string; post?: string } & marshal.DumpOptions = {} + opts: { pre?: string; post?: string } & marshal.DumpOptions = {}, ): Promise { return rb_load(marshal.dump(value, opts), opts.pre, opts.post); } @@ -20,14 +20,39 @@ describe("dump", test => { }); test("number", async () => { + assert.is(await dumps(42), "42"); + assert.is(await dumps(-42), "-42"); assert.is(await dumps(114514), "114514"); assert.is(await dumps(-1919810), "-1919810"); assert.is(await dumps(1145141919810), "1145141919810"); + assert.is(await dumps(-11451419198), "-11451419198"); assert.is(await dumps(123.456), "123.456"); + assert.is(await dumps(new marshal.RubyInteger(114514)), "114514"); + assert.is(await dumps(new marshal.RubyInteger(1145141919810)), "1145141919810"); + assert.is(await dumps(new marshal.RubyFloat(-0)), "-0.0"); + assert.is(await dumps(1 / 0), "Infinity"); + assert.is(await dumps(-1 / 0), "-Infinity"); + assert.is(await dumps(NaN), "NaN"); }); test("string", async () => { assert.is(await dumps("hello"), '"hello"'); + assert.is(await dumps(new TextEncoder().encode("hello")), '"hello"'); + }); + + test("regexp", async () => { + assert.is(await dumps(/hello/), "/hello/"); + }); + + test("hash", async () => { + assert.is(await dumps(new marshal.RubyHash([["a", 1]])), '{"a"=>1}'); + assert.is(await dumps(new Map([["a", 1]])), '{"a"=>1}'); + + let a = new marshal.RubyHash([ + ["x", 1], + ["x", 1], + ]); + assert.is(await dumps(a), '{"x"=>1}'); }); test("circular", async () => { @@ -62,17 +87,75 @@ describe("dump", test => { assert.is(await dumps(obj, { hashStringKeysToSymbol: true }), "{:a=>1, :b=>2}"); }); + test("error on undefined", async () => { + try { + await dumps({ a: void 0 }); + assert.unreachable("should throw error"); + } catch (e) { + assert.instance(e, TypeError); + assert.match(e.message, /can't dump/); + } + }); + + test("user class", async () => { + let a = new marshal.RubyObject(Symbol.for("MyHash")); + a[marshal.S_EXTENDS] = [Symbol.for("MyHash")]; + a.wrapped = {}; // a Hash + assert.is(await dumps(a, { pre: "class MyHash < Hash; end", post: "print a.class" }), "MyHash"); + }); + + test("user defined", async () => { + let a = new marshal.RubyObject(Symbol.for("UserDefined")); + a.userDefined = Uint8Array.of(42); + const pre = ` + class UserDefined + attr_accessor :a + def self._load(data) + obj = allocate + obj.a = data + obj + end + end + `; + const post = `print a.a`; + assert.is(await dumps(a, { pre, post }), "*"); + }); + + test("user marshal", async () => { + let a = new marshal.RubyObject(Symbol.for("A")); + a.userMarshal = []; // an Array + const pre = "class A; def marshal_load(obj) print obj.inspect end end"; + assert.is(await dumps(a, { pre, post: "" }), "[]"); + }); + test("known", async () => { class A {} + let a = new A(); try { - await dumps(new A()); + a[marshal.S_EXTENDS] = [Symbol.for("A")]; + await dumps(a); assert.unreachable("should throw error"); } catch (e) { assert.instance(e, TypeError); assert.match(e.message, /can't dump/); } let pre = "class A end"; - assert.match(await dumps(new A(), { pre, known: { A } }), /^#$/); - assert.match(await dumps(new A(), { pre, unknown: a => a?.constructor?.name }), /^#$/); + assert.match(await dumps(a, { pre, known: { A } }), /^#$/); + assert.match(await dumps(a, { pre, unknown: a => a?.constructor?.name }), /^#$/); + }); + + test("struct", async () => { + let a = new marshal.RubyStruct(Symbol.for("A")); + a.members = { [Symbol.for("a")]: 1 }; + assert.is(await dumps(a, { pre: "A = Struct.new :a" }), "#"); + }); + + test("class and module", async () => { + assert.is(await dumps(new marshal.RubyClass("A"), { pre: "class A end" }), "A"); + assert.is(await dumps(new marshal.RubyModule("A"), { pre: "module A end" }), "A"); + }); + + test("range", async () => { + assert.is(await dumps(new marshal.RubyRange(1, 10, true)), "1...10"); }); }); diff --git a/test/helper.ts b/test/helper.ts index dfdf2a9..60c94d3 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -4,10 +4,16 @@ import os from "os"; import path from "path"; import { suite, Test } from "uvu"; +const tests: Test[] = []; + export function describe(title: string, callback: (test: Test) => void) { const test = suite(title); callback(test); - test.run(); + tests.push(test); +} + +export function runTests() { + tests.forEach(t => t.run()); } /** @@ -20,8 +26,8 @@ export async function rb_eval(code: string): Promise { await fs.promises.writeFile(file, code); const output = await new Promise((resolve, reject) => cp.exec(`ruby ${JSON.stringify(file)}`, (err, stdout, stderr) => - err ? reject(err) : stderr ? reject(new Error(stderr)) : resolve(stdout) - ) + err ? reject(err) : stderr ? reject(new Error(stderr)) : resolve(stdout), + ), ); await fs.promises.unlink(file); return output; diff --git a/test/index.ts b/test/index.ts index f3317e9..cd7d26a 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,7 +1,8 @@ import { Suite } from "uvu/parse"; import { readdirSync } from "fs"; -import { build } from "esbuild"; -import { join } from "path"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { runTests } from "./helper"; const ignored = ["index.ts", "helper.ts"]; const suites: Suite[] = []; @@ -10,23 +11,16 @@ const pattern = (() => { return p ? new RegExp(p, "i") : /\.ts$/; })(); -readdirSync(__dirname).forEach(name => { +const dir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url)); + +readdirSync(dir).forEach(name => { if (ignored.includes(name)) return; - if (pattern.test(name)) suites.push({ name, file: join(__dirname, name) }); + if (pattern.test(name)) suites.push({ name, file: join(dir, name) }); }); suites.sort((a, b) => a.name.localeCompare(b.name)); -const outfile = "./node_modules/.cache/test.mjs"; -await build({ - stdin: { - contents: suites.map(e => `import ${JSON.stringify("./" + e.name)}`).join("\n"), - resolveDir: __dirname, - }, - bundle: true, - format: "esm", - platform: "node", - external: ["uvu/*"], - outfile, -}).catch(() => process.exit(1)); +for (const e of suites) { + await import("./" + e.name); +} -await import(join(process.cwd(), outfile)); +runTests(); diff --git a/test/load.ts b/test/load.ts index e529aa6..9c3d8b0 100644 --- a/test/load.ts +++ b/test/load.ts @@ -7,6 +7,23 @@ function loads(code: string, options?: marshal.LoadOptions): Promise { } describe("load", test => { + test("error on short data", async () => { + try { + await marshal.load(Uint8Array.of()); + assert.unreachable("should throw error"); + } catch (e) { + assert.instance(e, TypeError); + assert.match(e.message, /too short/); + } + try { + await marshal.load(Uint8Array.of(0x4, 0x9, 0, 0)); + assert.unreachable("should throw error"); + } catch (e) { + assert.instance(e, TypeError); + assert.match(e.message, /can't be read/); + } + }); + test("trivial value", async () => { assert.is(await loads(`nil`), null); assert.is(await loads(`true`), true); @@ -47,6 +64,10 @@ describe("load", test => { assert.is(await loads(`:symbol`), Symbol.for("symbol")); }); + test("regexp", async () => { + assert.equal(await loads(`/hello/`), /hello/); + }); + test("hash", async () => { let hash = await loads(`a = Hash.new(false); a[:a] = true; a`); assert.is(hash[marshal.S_DEFAULT], false); @@ -59,7 +80,7 @@ describe("load", test => { assert.equal(obj1[marshal.S_EXTENDS], [Symbol.for("M")]); let obj2: marshal.RubyObject = await loads( - `module M end; class A end; a = A.new; a.singleton_class.prepend M; a` + `module M end; class A end; a = A.new; a.singleton_class.prepend M; a`, ); assert.is(obj2.class, Symbol.for("A")); assert.equal(obj2[marshal.S_EXTENDS], [Symbol.for("M"), Symbol.for("A")]); @@ -78,7 +99,7 @@ describe("load", test => { test("struct", async () => { let struct = marshal.load( - await rb_str`"\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a"` + await rb_str`"\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a"`, ) as marshal.RubyStruct; assert.instance(struct, marshal.RubyStruct); assert.is(struct.class, Symbol.for("Struct::Useful"));