Files
leporello-js/test/utils.js

324 lines
8.8 KiB
JavaScript
Raw Normal View History

2023-07-06 18:34:03 +03:00
import {find_error_origin_node} from '../src/ast_utils.js'
2023-06-15 23:42:54 +03:00
import {parse, print_debug_node, load_modules} from '../src/parse_js.js'
import {active_frame, pp_calltree, version_number_symbol} from '../src/calltree.js'
2022-11-26 02:56:32 +08:00
import {COMMANDS} from '../src/cmd.js'
2022-09-10 02:48:13 +08:00
// external
import {create_app_window} from './run_utils.js'
// external
import {with_version_number} from '../src/runtime/runtime.js'
2022-12-19 20:46:40 +08:00
Object.assign(globalThis,
{
// for convenience, to type just `log` instead of `console.log`
2022-12-19 20:46:40 +08:00
log: console.log,
}
)
2022-09-10 02:48:13 +08:00
2023-06-15 23:55:06 +03:00
export const do_parse = code => parse(
code,
new Set(Object.getOwnPropertyNames(globalThis))
2023-06-15 23:55:06 +03:00
)
2022-09-10 02:48:13 +08:00
export const parse_modules = (entry, modules) =>
2023-07-02 22:43:26 +03:00
load_modules(
entry,
module_name => modules[module_name],
2024-03-02 13:43:31 +08:00
{},
2023-07-11 18:24:28 +03:00
new Set(Object.getOwnPropertyNames(globalThis.app_window))
2023-07-02 22:43:26 +03:00
)
2022-09-10 02:48:13 +08:00
export const assert_code_evals_to = (codestring, expected) => {
2023-02-08 04:09:53 +08:00
const s = test_initial_state(codestring)
2023-10-26 13:55:56 +08:00
if(!s.parse_result.ok) {
console.error('parse problems', s.parse_result.problems)
throw new Error('parse failed')
}
2023-02-08 04:09:53 +08:00
const frame = active_frame(s)
2023-05-16 00:04:53 +03:00
const result = frame.children.at(-1).result
2023-02-08 04:09:53 +08:00
assert_equal(result.ok, true)
assert_equal(result.value, expected)
2022-09-10 02:48:13 +08:00
return frame
}
export const assert_code_error = (codestring, error) => {
2023-02-08 04:09:53 +08:00
const state = test_initial_state(codestring)
const frame = active_frame(state)
2023-07-06 18:34:03 +03:00
assert_equal(frame.result.ok, false)
assert_equal(find_error_origin_node(frame).result.error, error)
2022-09-10 02:48:13 +08:00
}
2022-12-07 05:06:15 +08:00
export const assert_code_evals_to_async = async (codestring, expected) => {
const s = await test_initial_state_async(codestring)
const frame = active_frame(s)
2023-02-06 01:53:34 +08:00
const result = frame.children.at(-1).result
2022-12-07 05:06:15 +08:00
assert_equal(result.ok, true)
assert_equal(result.value, expected)
}
export const assert_code_error_async = async (codestring, error) => {
const s = await test_initial_state_async(codestring)
const frame = active_frame(s)
const result = frame.children[frame.children.length - 1].result
assert_equal(result.ok, false)
assert_equal(result.error, error)
}
function patch_app_window(app_window_patches) {
Object.entries(app_window_patches).forEach(([path, value]) => {
let obj = globalThis.app_window
const path_arr = path.split('.')
path_arr.forEach((el, i) => {
if(i == path_arr.length - 1) {
return
}
obj = obj[el]
})
const prop = path_arr.at(-1)
obj[prop] = value
})
}
export const run_code = (state, app_window_patches) => {
globalThis.app_window = create_app_window()
if(app_window_patches != null) {
patch_app_window(app_window_patches)
}
return COMMANDS.run_code(
state,
state.globals ?? new Set(Object.getOwnPropertyNames(globalThis.app_window))
)
}
2024-02-10 15:20:46 +08:00
export const test_initial_state = (code, cursor_pos, options = {}) => {
2023-11-17 12:44:12 +08:00
if(cursor_pos < 0) {
throw new Error('illegal cursor_pos')
}
if(typeof(options) != 'object') {
throw new Error('illegal state')
}
2024-02-10 15:20:46 +08:00
const {
current_module,
project_dir,
on_deferred_call,
app_window_patches,
2024-02-10 15:20:46 +08:00
} = options
const entrypoint = options.entrypoint ?? ''
return run_code(
2022-11-29 03:37:15 +08:00
COMMANDS.get_initial_state(
{
files: typeof(code) == 'object' ? code : { '' : code},
2024-02-10 15:20:46 +08:00
project_dir,
on_deferred_call,
},
{
2024-02-10 15:20:46 +08:00
entrypoint,
current_module: current_module ?? '',
2023-08-02 05:59:49 +03:00
},
cursor_pos
2023-06-15 23:55:06 +03:00
),
app_window_patches,
2022-10-25 02:29:59 +08:00
)
2022-09-10 02:48:13 +08:00
}
const wait_for_result = async state => {
assert_equal(state.eval_modules_state != null, true)
const result = await state.eval_modules_state.promise
2022-12-07 05:06:15 +08:00
return COMMANDS.eval_modules_finished(
state,
state,
2022-12-07 05:06:15 +08:00
result,
)
}
export const test_initial_state_async = async (code, ...args) => {
const s = test_initial_state(code, ...args)
return wait_for_result(s)
}
export const input = (s, code, index, options = {}) => {
if(typeof(options) != 'object') {
throw new Error('illegal state')
}
const {state, effects} = COMMANDS.input(s, code, index)
const nextstate = run_code(state, options.app_window_patches)
if(nextstate.rt_cxt?.io_trace_is_replay_aborted) {
const with_clear_trace = run_code(
COMMANDS.clear_io_trace(nextstate),
options.app_window_patches
)
return { state: with_clear_trace, effects }
} else {
return { state: nextstate, effects }
}
}
export const input_async = async (s, code, index, options) => {
const after_input = input(s, code, index, options).state
const state = await wait_for_result(after_input)
if(state.rt_cxt?.io_trace_is_replay_aborted) {
return wait_for_result(
run_code(COMMANDS.clear_io_trace(state), options.app_window_patches)
)
} else {
return state
}
2023-02-06 01:53:34 +08:00
}
2024-03-02 12:45:56 +08:00
export const test_deferred_calls_state = (code, index) => {
2022-12-02 04:31:16 +08:00
const {get_deferred_call, on_deferred_call} = (new Function(`
2022-12-15 21:15:52 +08:00
let args
2022-11-29 04:22:56 +08:00
return {
2022-12-02 04:31:16 +08:00
get_deferred_call() {
2022-12-15 21:15:52 +08:00
return args
2022-11-29 04:22:56 +08:00
},
2022-12-15 21:15:52 +08:00
on_deferred_call(..._args) {
args = _args
2022-11-29 04:22:56 +08:00
}
}
`))()
2024-03-02 12:45:56 +08:00
const state = test_initial_state(code, index, { on_deferred_call })
2022-11-29 04:22:56 +08:00
return {
state,
2022-12-02 04:31:16 +08:00
get_deferred_call,
on_deferred_call: state => COMMANDS.on_deferred_call(state, ...get_deferred_call())
2022-11-29 04:22:56 +08:00
}
}
2022-09-10 02:48:13 +08:00
export const stringify = val =>
JSON.stringify(val, (key, value) => {
2024-02-15 17:03:26 +08:00
if(value?.[Symbol.toStringTag] == 'Set'){
2022-09-10 02:48:13 +08:00
return [...value]
2024-02-15 17:03:26 +08:00
} else if(value?.[Symbol.toStringTag] == 'Map'){
return Object.fromEntries([...value.entries()])
2023-07-06 18:34:03 +03:00
} else if(value instanceof Error) {
return {message: value.message}
2022-09-10 02:48:13 +08:00
} else {
return value
}
}, 2)
export const assert_equal = (exp, actual) => {
if(typeof(exp) == 'object' && typeof(actual) == 'object'){
const exp_json = stringify(exp)
const act_json = stringify(actual)
if(exp_json != act_json){
throw new Error(`FAIL: ${exp_json} != ${act_json}`)
}
} else {
if(exp != actual){
throw new Error(`FAIL: ${exp} != ${actual}`)
}
}
}
export const print_debug_ct_node = node => {
const do_print = node => {
const {id, fn, ok, value, error, args, has_more_children} = node
const res = {id, fn: fn?.name, ok, value, error, args, has_more_children}
if(node.children == null) {
return res
} else {
const next_children = node.children.map(do_print)
return {...res, children: next_children}
}
}
return stringify(do_print(node))
}
export const assert_versioned_value = (state, versioned, expected) => {
const version_number = versioned[version_number_symbol] ?? versioned.version_number
if(version_number == null) {
throw new Error('illegal state')
}
return with_version_number(state.rt_cxt, version_number, () =>
assert_equal(versioned.value, expected)
)
}
export const assert_value_explorer = (state, expected) =>
assert_versioned_value(state, state.value_explorer.result, expected)
export const assert_selection = (state, expected) =>
assert_versioned_value(state, state.selection_state.node.result, expected)
2022-09-10 02:48:13 +08:00
export const test = (message, test, only = false) => {
return {
message,
test: Object.defineProperty(test, 'name', {value: message}),
only,
}
}
export const test_only = (message, t) => test(message, t, true)
// Create `run` function like this because we do not want its internals to be
// present in calltree
export const run = new Function('create_app_window', `
return function run(tests) {
// create app window for simple tests, that do not use 'test_initial_state'
globalThis.app_window = create_app_window()
// Runs test, return failure or null if not failed
const run_test = t => {
return Promise.resolve().then(t.test)
.then(() => null)
.catch(e => {
if(globalThis.process != null) {
// In node.js runner, fail fast
console.error('Failed: ' + t.message)
throw e
} else {
return e
}
})
}
// If not run in node, then dont apply filter
const filter = globalThis.process && globalThis.process.argv[2]
if(filter == null) {
const only = tests.find(t => t.only)
const tests_to_run = only == null ? tests : [only]
// Exec each test. After all tests are done, we rethrow first error if
// any. So we will mark root calltree node if one of tests failed
return tests_to_run.reduce(
(failureP, t) =>
Promise.resolve(failureP).then(failure =>
run_test(t).then(next_failure => failure ?? next_failure)
)
,
null
).then(failure => {
if(failure != null) {
throw failure
} else {
if(globalThis.process != null) {
console.log('Ok')
}
}
})
} else {
const test = tests.find(t => t.message.includes(filter))
if(test == null) {
throw new Error('test not found')
} else {
return run_test(test).then(() => {
if(globalThis.process != null) {
console.log('Ok')
}
})
}
}
}
`)(create_app_window)