250 lines
8.6 KiB
TypeScript
250 lines
8.6 KiB
TypeScript
import { expect } from "bun:test"
|
|
import { toValue, fromValue, type Value, toBytecode, run, type Bytecode } from "#reef"
|
|
import { isEqual } from "../src/value"
|
|
|
|
declare module "bun:test" {
|
|
interface Matchers<T> {
|
|
/**
|
|
* Run bytecode and assert that the result equals a JavaScript value after conversion via toValue()
|
|
* @example expect(bytecode).toEqualValue(42)
|
|
* @example expect("PUSH 5\nPUSH 3\nADD").toEqualValue(8)
|
|
* @example expect([["PUSH", 42]]).toEqualValue(42)
|
|
*/
|
|
toEqualValue(expected: any): Promise<void>
|
|
|
|
/**
|
|
* Run bytecode and assert that the result is null
|
|
* @example expect(bytecode).toBeNull()
|
|
* @example expect("PUSH null").toBeNull()
|
|
*/
|
|
toBeNull(): Promise<void>
|
|
|
|
/**
|
|
* Run bytecode and assert that the result is a boolean with the expected value
|
|
* @example expect(bytecode).toBeBoolean(true)
|
|
* @example expect("PUSH true").toBeBoolean(true)
|
|
*/
|
|
toBeBoolean(expected: boolean): Promise<void>
|
|
|
|
/**
|
|
* Run bytecode and assert that the result is a number with the expected value
|
|
* @example expect(bytecode).toBeNumber(42)
|
|
* @example expect("PUSH 42").toBeNumber(42)
|
|
*/
|
|
toBeNumber(expected: number): Promise<void>
|
|
|
|
/**
|
|
* Run bytecode and assert that the result is a string with the expected value
|
|
* @example expect(bytecode).toBeString("hello")
|
|
* @example expect("PUSH \"hello\"").toBeString("hello")
|
|
*/
|
|
toBeString(expected: string): Promise<void>
|
|
|
|
/**
|
|
* Run bytecode and assert that the result is an array with the expected values
|
|
* @example expect(bytecode).toBeArray([1, 2, 3])
|
|
*/
|
|
toBeArray(expected: any[]): Promise<void>
|
|
|
|
/**
|
|
* Run bytecode and assert that the result is a dict with the expected key-value pairs
|
|
* @example expect(bytecode).toBeDict({ x: 10, y: 20 })
|
|
*/
|
|
toBeDict(expected: Record<string, any>): Promise<void>
|
|
|
|
/**
|
|
* Run bytecode and assert that the result is a function (Reef or native)
|
|
* @example expect(bytecode).toBeFunction()
|
|
*/
|
|
toBeFunction(): Promise<void>
|
|
|
|
/**
|
|
* Run bytecode and assert that the result is truthy according to ReefVM semantics
|
|
* (only null and false are falsy)
|
|
* @example expect(bytecode).toBeTruthy()
|
|
*/
|
|
toBeTruthy(): Promise<void>
|
|
|
|
/**
|
|
* Run bytecode and assert that the result is falsy according to ReefVM semantics
|
|
* (only null and false are falsy)
|
|
* @example expect(bytecode).toBeFalsy()
|
|
*/
|
|
toBeFalsy(): Promise<void>
|
|
}
|
|
}
|
|
|
|
expect.extend({
|
|
async toEqualValue(received: string | any[] | Bytecode, expected: any) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
const expectedValue = toValue(expected)
|
|
const pass = isEqual(result, expectedValue)
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to equal ${formatValue(expectedValue)}, but it did`
|
|
: `Expected value to equal ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
|
|
async toBeNull(received: string | any[] | Bytecode) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
const pass = result.type === "null"
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to be null, but it was`
|
|
: `Expected value to be null, but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
|
|
async toBeBoolean(received: string | any[] | Bytecode, expected: boolean) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
const pass = result.type === "boolean" && result.value === expected
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to be boolean ${expected}, but it was`
|
|
: `Expected value to be boolean ${expected}, but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
|
|
async toBeNumber(received: string | any[] | Bytecode, expected: number) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
const pass = result.type === "number" && result.value === expected
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to be number ${expected}, but it was`
|
|
: `Expected value to be number ${expected}, but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
|
|
async toBeString(received: string | any[] | Bytecode, expected: string) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
const pass = result.type === "string" && result.value === expected
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to be string "${expected}", but it was`
|
|
: `Expected value to be string "${expected}", but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
|
|
async toBeArray(received: string | any[] | Bytecode, expected: any[]) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
const expectedValue = toValue(expected)
|
|
const pass = result.type === "array" && isEqual(result, expectedValue)
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to be array ${formatValue(expectedValue)}, but it was`
|
|
: `Expected value to be array ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
|
|
async toBeDict(received: string | any[] | Bytecode, expected: Record<string, any>) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
const expectedValue = toValue(expected)
|
|
const pass = result.type === "dict" && isEqual(result, expectedValue)
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to be dict ${formatValue(expectedValue)}, but it was`
|
|
: `Expected value to be dict ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
|
|
async toBeFunction(received: string | any[] | Bytecode) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
const pass = result.type === "function" || result.type === "native"
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to be a function, but it was`
|
|
: `Expected value to be a function, but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
|
|
async toBeTruthy(received: string | any[] | Bytecode) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
// ReefVM semantics: only null and false are falsy
|
|
const pass = !(result.type === "null" || (result.type === "boolean" && !result.value))
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to be truthy, but it was: ${formatValue(result)}`
|
|
: `Expected value to be truthy, but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
|
|
async toBeFalsy(received: string | any[] | Bytecode) {
|
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received
|
|
const result = await run(bytecode)
|
|
// ReefVM semantics: only null and false are falsy
|
|
const pass = result.type === "null" || (result.type === "boolean" && !result.value)
|
|
|
|
return {
|
|
pass,
|
|
message: () =>
|
|
pass
|
|
? `Expected value NOT to be falsy, but it was: ${formatValue(result)}`
|
|
: `Expected value to be falsy, but received ${formatValue(result)}`,
|
|
}
|
|
},
|
|
})
|
|
|
|
function formatValue(value: Value): string {
|
|
switch (value.type) {
|
|
case "null":
|
|
return "null"
|
|
case "boolean":
|
|
case "number":
|
|
return String(value.value)
|
|
case "string":
|
|
return `"${value.value}"`
|
|
case "array":
|
|
return `[${value.value.map(formatValue).join(", ")}]`
|
|
case "dict": {
|
|
const entries = Array.from(value.value.entries())
|
|
.map(([k, v]) => `${k}: ${formatValue(v)}`)
|
|
.join(", ")
|
|
return `{${entries}}`
|
|
}
|
|
case "regex":
|
|
return String(value.value)
|
|
case "function":
|
|
case "native":
|
|
return "<function>"
|
|
default:
|
|
return String(value)
|
|
}
|
|
}
|