mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 21:14:28 -08:00
reload app_window on every code execution
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import {tests} from './test.js'
|
||||
|
||||
// external
|
||||
import {run} from './run_utils.js'
|
||||
import {run} from './utils.js'
|
||||
|
||||
await run(tests)
|
||||
|
||||
@@ -8,61 +8,47 @@
|
||||
*/
|
||||
globalThis.Response
|
||||
|
||||
export const run = tests => {
|
||||
// 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(globalThis.process != null) {
|
||||
globalThis.NodeVM = await import('node:vm')
|
||||
}
|
||||
|
||||
// If not run in node, then dont apply filter
|
||||
const filter = globalThis.process && globalThis.process.argv[2]
|
||||
let iframe
|
||||
|
||||
if(filter == null) {
|
||||
export function create_app_window() {
|
||||
if(globalThis.process != null) {
|
||||
// We are in node.js
|
||||
// `NodeVM` was preloaded earlier
|
||||
|
||||
const only = tests.find(t => t.only)
|
||||
const tests_to_run = only == null ? tests : [only]
|
||||
const context = globalThis.NodeVM.createContext({
|
||||
|
||||
// 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 => {
|
||||
process,
|
||||
|
||||
if(failure != null) {
|
||||
throw failure
|
||||
} else {
|
||||
if(globalThis.process != null) {
|
||||
console.log('Ok')
|
||||
}
|
||||
}
|
||||
// for some reason URL is not available inside VM
|
||||
URL,
|
||||
|
||||
console,
|
||||
setTimeout,
|
||||
// break fetch because we dont want it to be accidentally called in unit test
|
||||
fetch: () => {
|
||||
console.error('Error! fetch called')
|
||||
},
|
||||
})
|
||||
const get_global_object = globalThis.NodeVM.compileFunction(
|
||||
'return this',
|
||||
[], // args
|
||||
{parsingContext: context}
|
||||
)
|
||||
|
||||
return get_global_object()
|
||||
|
||||
} 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')
|
||||
}
|
||||
})
|
||||
// We are in browser
|
||||
if(iframe != null) {
|
||||
globalThis.document.body.removeChild(iframe)
|
||||
}
|
||||
iframe = globalThis.document.createElement('iframe')
|
||||
document.body.appendChild(iframe)
|
||||
return iframe.contentWindow
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
249
test/test.js
249
test/test.js
@@ -23,12 +23,12 @@ import {
|
||||
assert_code_error, assert_code_error_async,
|
||||
assert_versioned_value, assert_value_explorer, assert_selection,
|
||||
parse_modules,
|
||||
run_code,
|
||||
test_initial_state, test_initial_state_async,
|
||||
test_deferred_calls_state,
|
||||
print_debug_ct_node,
|
||||
command_input_async,
|
||||
patch_builtin,
|
||||
original_setTimeout,
|
||||
input,
|
||||
input_async,
|
||||
} from './utils.js'
|
||||
|
||||
export const tests = [
|
||||
@@ -1012,7 +1012,8 @@ export const tests = [
|
||||
const spoil_file = {...s, files: {...s.files, 'c': ',,,'}}
|
||||
|
||||
// change module ''
|
||||
const {state: s2} = COMMANDS.input(spoil_file, 'import {c} from "c"', 0)
|
||||
const {state: s2} = input(spoil_file, 'import {c} from "c"', 0)
|
||||
assert_equal(s2.dirty_files, new Set())
|
||||
|
||||
assert_equal(s2.parse_result.ok, true)
|
||||
}),
|
||||
@@ -1110,7 +1111,7 @@ export const tests = [
|
||||
|
||||
const index = edited.indexOf('foo_var')
|
||||
|
||||
const {state, effects} = COMMANDS.input(
|
||||
const {state, effects} = input(
|
||||
initial,
|
||||
edited,
|
||||
index
|
||||
@@ -1164,39 +1165,6 @@ export const tests = [
|
||||
)
|
||||
}),
|
||||
|
||||
test('module external cache', () => {
|
||||
const code = `
|
||||
// external
|
||||
import {foo_var} from 'foo.js'
|
||||
console.log(foo_var)
|
||||
`
|
||||
const initial = test_initial_state(code)
|
||||
|
||||
const next = COMMANDS.external_imports_loaded(initial, initial, {
|
||||
'foo.js': {
|
||||
ok: true,
|
||||
module: {
|
||||
'foo_var': 'foo_value'
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const edited = `
|
||||
// external
|
||||
import {foo_var} from 'foo.js'
|
||||
foo_var
|
||||
`
|
||||
|
||||
const {state} = COMMANDS.input(
|
||||
next,
|
||||
edited,
|
||||
edited.lastIndexOf('foo_var'),
|
||||
)
|
||||
|
||||
// If cache was not used then effects will be `load_external_imports`
|
||||
assert_equal(state.value_explorer.result.value, 'foo_value')
|
||||
}),
|
||||
|
||||
test('module external cache error bug', () => {
|
||||
const code = `
|
||||
// external
|
||||
@@ -1221,7 +1189,7 @@ export const tests = [
|
||||
`
|
||||
|
||||
// edit code
|
||||
const {state} = COMMANDS.input(
|
||||
const {state} = input(
|
||||
next,
|
||||
edited,
|
||||
edited.lastIndexOf('foo_var'),
|
||||
@@ -1259,7 +1227,7 @@ export const tests = [
|
||||
const edited = ``
|
||||
|
||||
// edit code
|
||||
const {state, effects} = COMMANDS.input(
|
||||
const {state, effects} = input(
|
||||
next,
|
||||
edited,
|
||||
0,
|
||||
@@ -2000,7 +1968,7 @@ const y = x()`
|
||||
|
||||
x(2);
|
||||
`
|
||||
const s3 = COMMANDS.input(s2, invalid, invalid.indexOf('return')).state
|
||||
const s3 = input(s2, invalid, invalid.indexOf('return')).state
|
||||
|
||||
const edited = `
|
||||
const x = foo => {
|
||||
@@ -2010,7 +1978,7 @@ const y = x()`
|
||||
x(2);
|
||||
`
|
||||
|
||||
const n = COMMANDS.input(s3, edited, edited.indexOf('return')).state
|
||||
const n = input(s3, edited, edited.indexOf('return')).state
|
||||
|
||||
const res = find_leaf(active_frame(n), edited.indexOf('*'))
|
||||
|
||||
@@ -2044,7 +2012,7 @@ const y = x()`
|
||||
[1,2,3].map(x)
|
||||
`
|
||||
|
||||
const e = COMMANDS.input(s4, edited, edited.indexOf('2')).state
|
||||
const e = input(s4, edited, edited.indexOf('2')).state
|
||||
|
||||
const active = active_frame(e)
|
||||
|
||||
@@ -2070,7 +2038,7 @@ const y = x()`
|
||||
}
|
||||
`
|
||||
|
||||
const {state: s2} = COMMANDS.input(s1, edited, edited.indexOf('1'))
|
||||
const {state: s2} = input(s1, edited, edited.indexOf('1'))
|
||||
const s3 = COMMANDS.move_cursor(s2, edited.indexOf('import'))
|
||||
assert_equal(s3.value_explorer.result.value.x, 1)
|
||||
}),
|
||||
@@ -2098,7 +2066,7 @@ const y = x()`
|
||||
x()
|
||||
`
|
||||
|
||||
const e = COMMANDS.input(s3, edited, edited.indexOf('123')).state
|
||||
const e = input(s3, edited, edited.indexOf('123')).state
|
||||
|
||||
assert_equal(e.active_calltree_node.toplevel, true)
|
||||
}),
|
||||
@@ -2111,7 +2079,7 @@ const y = x()`
|
||||
}),
|
||||
'x'
|
||||
)
|
||||
const e = COMMANDS.input(s1, 'export const x = 2', 0).state
|
||||
const e = input(s1, 'export const x = 2', 0).state
|
||||
assert_equal(e.current_calltree_node.module, '')
|
||||
assert_equal(e.active_calltree_node, null)
|
||||
}),
|
||||
@@ -2143,7 +2111,7 @@ const y = x()`
|
||||
`
|
||||
|
||||
const moved = COMMANDS.move_cursor(s3, code.indexOf('2'))
|
||||
const e = COMMANDS.input(moved, edited, edited.indexOf('3')).state
|
||||
const e = input(moved, edited, edited.indexOf('3')).state
|
||||
assert_equal(e.active_calltree_node, null)
|
||||
assert_equal(e.current_calltree_node.toplevel, true)
|
||||
}),
|
||||
@@ -2156,7 +2124,7 @@ const y = x()`
|
||||
x()
|
||||
`
|
||||
const i = test_initial_state(code)
|
||||
const edited = COMMANDS.input(i, code.replace('1', '100'), code.indexOf('1')).state
|
||||
const edited = input(i, code.replace('1', '100'), code.indexOf('1')).state
|
||||
const left = COMMANDS.calltree.arrow_left(edited)
|
||||
assert_equal(left.active_calltree_node.toplevel, true)
|
||||
}),
|
||||
@@ -3550,12 +3518,12 @@ const y = x()`
|
||||
`
|
||||
const {state: i, on_deferred_call} = test_deferred_calls_state(code)
|
||||
|
||||
const input = COMMANDS.input(i, code, 0).state
|
||||
const state = input(i, code, 0).state
|
||||
|
||||
// Make deferred call, calling fn from previous code
|
||||
i.modules[''].fn(1)
|
||||
|
||||
const result = on_deferred_call(input)
|
||||
const result = on_deferred_call(state)
|
||||
|
||||
// deferred calls must be null, because deferred calls from previous executions
|
||||
// must be discarded
|
||||
@@ -3911,7 +3879,7 @@ const y = x()`
|
||||
}
|
||||
await f()
|
||||
`
|
||||
const next = await command_input_async(i, code2, code2.indexOf('1'))
|
||||
const next = await input_async(i, code2, code2.indexOf('1'))
|
||||
assert_equal(next.active_calltree_node.fn.name, 'f')
|
||||
assert_equal(next.value_explorer.result.value, 1)
|
||||
}),
|
||||
@@ -3955,22 +3923,21 @@ const y = x()`
|
||||
`
|
||||
const {state: i, on_deferred_call} = test_deferred_calls_state(code)
|
||||
|
||||
await i.eval_modules_state.promise.__original_then(result => {
|
||||
const s = COMMANDS.eval_modules_finished(
|
||||
i,
|
||||
i,
|
||||
result,
|
||||
i.eval_modules_state.node,
|
||||
i.eval_modules_state.toplevel
|
||||
)
|
||||
const result = await i.eval_modules_state.promise
|
||||
|
||||
// Make deferred call
|
||||
s.modules[''].fn()
|
||||
const state = on_deferred_call(s)
|
||||
assert_equal(get_deferred_calls(state).length, 1)
|
||||
assert_equal(get_deferred_calls(state)[0].value, 1)
|
||||
})
|
||||
const s = COMMANDS.eval_modules_finished(
|
||||
i,
|
||||
i,
|
||||
result,
|
||||
i.eval_modules_state.node,
|
||||
i.eval_modules_state.toplevel
|
||||
)
|
||||
|
||||
// Make deferred call
|
||||
s.modules[''].fn()
|
||||
const state = on_deferred_call(s)
|
||||
assert_equal(get_deferred_calls(state).length, 1)
|
||||
assert_equal(get_deferred_calls(state)[0].value, 1)
|
||||
}),
|
||||
|
||||
test('async/await await argument bug', async () => {
|
||||
@@ -3991,101 +3958,94 @@ const y = x()`
|
||||
}),
|
||||
|
||||
test('record io', () => {
|
||||
// Patch Math.random to always return 1
|
||||
patch_builtin('random', () => 1)
|
||||
let app_window_patches
|
||||
|
||||
const initial = test_initial_state(`
|
||||
const x = Math.random()
|
||||
`)
|
||||
// Patch Math.random to always return 1
|
||||
app_window_patches = {'Math.random': () => 1}
|
||||
|
||||
const initial = test_initial_state(`const x = Math.random()`, undefined, {
|
||||
app_window_patches
|
||||
})
|
||||
|
||||
// Now call to Math.random is cached, break it to ensure it was not called
|
||||
// on next run
|
||||
patch_builtin('random', () => { throw 'fail' })
|
||||
app_window_patches = {'Math.random': () => { throw 'fail' }}
|
||||
|
||||
const next = COMMANDS.input(initial, `const x = Math.random()*2`, 0).state
|
||||
const next = input(initial, `const x = Math.random()*2`, 0, {app_window_patches}).state
|
||||
assert_equal(next.value_explorer.result.value, 2)
|
||||
assert_equal(next.rt_cxt.io_trace_index, 1)
|
||||
|
||||
// Patch Math.random to return 2.
|
||||
// TODO The first call to Math.random() is cached with value 1, and the
|
||||
// second shoud return 2
|
||||
patch_builtin('random', () => 2)
|
||||
const replay_failed = COMMANDS.input(
|
||||
app_window_patches = {'Math.random': () => 2}
|
||||
const replay_failed = input(
|
||||
initial,
|
||||
`const x = Math.random() + Math.random()`,
|
||||
0
|
||||
0,
|
||||
{app_window_patches}
|
||||
).state
|
||||
|
||||
// TODO must reuse first cached call?
|
||||
assert_equal(replay_failed.value_explorer.result.value, 4)
|
||||
|
||||
// Remove patch
|
||||
patch_builtin('random', null)
|
||||
}),
|
||||
|
||||
|
||||
test('record io trace discarded if args does not match', async () => {
|
||||
// Patch fetch
|
||||
patch_builtin('fetch', async () => 'first')
|
||||
let app_window_patches
|
||||
app_window_patches = {fetch: async () => 'first'}
|
||||
|
||||
const initial = await test_initial_state_async(`
|
||||
console.log(await fetch('url', {method: 'GET'}))
|
||||
`)
|
||||
`, undefined, {app_window_patches})
|
||||
assert_equal(initial.logs.logs[0].args[0], 'first')
|
||||
|
||||
// Patch fetch again
|
||||
patch_builtin('fetch', async () => 'second')
|
||||
app_window_patches = {fetch: async () => 'second'}
|
||||
|
||||
const cache_discarded = await command_input_async(initial, `
|
||||
const cache_discarded = await input_async(initial, `
|
||||
console.log(await fetch('url', {method: 'POST'}))
|
||||
`, 0)
|
||||
`, 0, {app_window_patches})
|
||||
assert_equal(cache_discarded.logs.logs[0].args[0], 'second')
|
||||
|
||||
// Remove patch
|
||||
patch_builtin('fetch', null)
|
||||
}),
|
||||
|
||||
test('record io fetch rejects', async () => {
|
||||
// Patch fetch
|
||||
patch_builtin('fetch', () => Promise.reject('fail'))
|
||||
let app_window_patches
|
||||
app_window_patches = {fetch: () => globalThis.app_window.Promise.reject('fail')}
|
||||
|
||||
const initial = await test_initial_state_async(`
|
||||
await fetch('url', {method: 'GET'})
|
||||
`)
|
||||
`, undefined, {app_window_patches})
|
||||
assert_equal(root_calltree_node(initial).error, 'fail')
|
||||
|
||||
// Patch fetch again
|
||||
patch_builtin('fetch', () => async () => 'result')
|
||||
app_window_patches = {fetch: () => async () => 'result'}
|
||||
|
||||
const with_cache = await command_input_async(initial, `
|
||||
const with_cache = await input_async(initial, `
|
||||
await fetch('url', {method: 'GET'})
|
||||
`, 0)
|
||||
`, 0, {app_window_patches})
|
||||
assert_equal(root_calltree_node(initial).error, 'fail')
|
||||
|
||||
// Remove patch
|
||||
patch_builtin('fetch', null)
|
||||
}),
|
||||
|
||||
test('record io preserve promise resolution order', async () => {
|
||||
// Generate fetch function which calls get resolved in reverse order
|
||||
const {fetch, resolve} = new Function(`
|
||||
const calls = []
|
||||
return {
|
||||
fetch(...args) {
|
||||
let resolver
|
||||
const promise = new Promise(r => resolver = r)
|
||||
calls.push({resolver, promise, args})
|
||||
return promise
|
||||
},
|
||||
const calls = []
|
||||
function fetch(...args) {
|
||||
let resolver
|
||||
const promise = new (globalThis.app_window.Promise)(r => {resolver = r})
|
||||
calls.push({resolver, promise, args})
|
||||
return promise
|
||||
}
|
||||
|
||||
resolve() {
|
||||
[...calls].reverse().forEach(call => call.resolver(...call.args))
|
||||
},
|
||||
}
|
||||
`)()
|
||||
function resolve() {
|
||||
[...calls].reverse().forEach(call => call.resolver(...call.args))
|
||||
}
|
||||
|
||||
let app_window_patches
|
||||
|
||||
// Patch fetch
|
||||
patch_builtin('fetch', fetch)
|
||||
app_window_patches = {fetch}
|
||||
|
||||
const code = `
|
||||
await Promise.all(
|
||||
@@ -4096,7 +4056,7 @@ const y = x()`
|
||||
)
|
||||
`
|
||||
|
||||
const initial_promise = test_initial_state_async(code)
|
||||
const initial_promise = test_initial_state_async(code, undefined, {app_window_patches})
|
||||
|
||||
resolve()
|
||||
|
||||
@@ -4106,56 +4066,54 @@ const y = x()`
|
||||
assert_equal(initial.logs.logs.map(l => l.args[0]), [3,2,1])
|
||||
|
||||
// Break fetch to ensure it is not get called anymore
|
||||
patch_builtin('fetch', () => {throw 'broken'})
|
||||
app_window_patches = {fetch: () => {throw 'broken'}}
|
||||
|
||||
const with_cache = await command_input_async(
|
||||
const with_cache = await input_async(
|
||||
initial,
|
||||
code,
|
||||
0
|
||||
0,
|
||||
{app_window_patches}
|
||||
)
|
||||
|
||||
// cached calls to fetch should be resolved in the same (reverse) order as
|
||||
// on the first run, so first call wins
|
||||
assert_equal(with_cache.logs.logs.map(l => l.args[0]), [3,2,1])
|
||||
|
||||
// Remove patch
|
||||
patch_builtin('fetch', null)
|
||||
}),
|
||||
|
||||
test('record io setTimeout', async () => {
|
||||
let app_window_patches
|
||||
// Patch fetch to return result in 10ms
|
||||
patch_builtin(
|
||||
'fetch',
|
||||
() => new Promise(resolve => original_setTimeout(resolve, 10))
|
||||
)
|
||||
app_window_patches = {
|
||||
fetch: () => new (globalThis.app_window.Promise)(resolve => setTimeout(resolve, 10))
|
||||
}
|
||||
|
||||
const code = `
|
||||
setTimeout(() => console.log('timeout'), 0)
|
||||
await fetch().then(() => console.log('fetch'))
|
||||
`
|
||||
|
||||
const i = await test_initial_state_async(code)
|
||||
const i = await test_initial_state_async(code, undefined, {app_window_patches})
|
||||
|
||||
// First executed setTimeout, then fetch
|
||||
assert_equal(i.logs.logs.map(l => l.args[0]), ['timeout', 'fetch'])
|
||||
|
||||
// Break fetch to ensure it would not be called
|
||||
patch_builtin('fetch', async () => {throw 'break'})
|
||||
app_window_patches = {
|
||||
fetch: async () => {throw 'break'}
|
||||
}
|
||||
|
||||
const with_cache = await command_input_async(i, code, 0)
|
||||
const with_cache = await input_async(i, code, 0, {app_window_patches})
|
||||
|
||||
// Cache must preserve resolution order
|
||||
assert_equal(with_cache.logs.logs.map(l => l.args[0]), ['timeout', 'fetch'])
|
||||
|
||||
patch_builtin('fetch', null)
|
||||
}),
|
||||
|
||||
test('record io clear io trace', async () => {
|
||||
const s1 = test_initial_state(`Math.random()`)
|
||||
const rnd = s1.value_explorer.result.value
|
||||
const s2 = COMMANDS.input(s1, `Math.random() + 1`, 0).state
|
||||
const s2 = input(s1, `Math.random() + 1`, 0).state
|
||||
assert_equal(s2.value_explorer.result.value, rnd + 1)
|
||||
const cleared = COMMANDS.clear_io_trace(s2)
|
||||
const cleared = input(COMMANDS.clear_io_trace(s2), `Math.random() + 1`).state
|
||||
assert_equal(
|
||||
cleared.value_explorer.result.value == rnd + 1,
|
||||
false
|
||||
@@ -4185,8 +4143,8 @@ const y = x()`
|
||||
const rnd = i.active_calltree_node.children[0].value
|
||||
|
||||
// Run two versions of code in parallel
|
||||
const next = COMMANDS.input(i, `await Promise.resolve()`, 0)
|
||||
const next2 = COMMANDS.input(i, `Math.random(1)`, 0).state
|
||||
const next = input(i, `await Promise.resolve()`, 0)
|
||||
const next2 = input(i, `Math.random(1)`, 0).state
|
||||
const next_rnd = i.active_calltree_node.children[0].value
|
||||
assert_equal(rnd, next_rnd)
|
||||
}),
|
||||
@@ -4211,10 +4169,10 @@ const y = x()`
|
||||
}),
|
||||
|
||||
test('record io hangs bug', async () => {
|
||||
patch_builtin(
|
||||
'fetch',
|
||||
() => new Promise(resolve => original_setTimeout(resolve, 0))
|
||||
)
|
||||
let app_window_patches
|
||||
app_window_patches = {
|
||||
fetch: () => new (globalThis.app_window.Promise)(resolve => setTimeout(resolve, 0))
|
||||
}
|
||||
|
||||
const code = `
|
||||
const p = fetch('')
|
||||
@@ -4222,22 +4180,20 @@ const y = x()`
|
||||
await p
|
||||
`
|
||||
|
||||
const i = await test_initial_state_async(code)
|
||||
const i = await test_initial_state_async(code, undefined, {app_window_patches})
|
||||
|
||||
assert_equal(i.io_trace.length, 3)
|
||||
|
||||
const next_code = `await fetch('')`
|
||||
|
||||
const state = await command_input_async(i, next_code, 0)
|
||||
const state = await input_async(i, next_code, 0, {app_window_patches})
|
||||
assert_equal(state.io_trace.length, 2)
|
||||
|
||||
patch_builtin('fetch', null)
|
||||
}),
|
||||
|
||||
test('record io logs recorded twice bug', () => {
|
||||
const code = `Math.random()`
|
||||
const i = test_initial_state(code)
|
||||
const second = COMMANDS.input(
|
||||
const second = input(
|
||||
i,
|
||||
`console.log(1); Math.random(); Math.random()`,
|
||||
0
|
||||
@@ -5401,4 +5357,19 @@ const y = x()`
|
||||
assert_value_explorer(i, expected)
|
||||
|
||||
}),
|
||||
|
||||
test('leporello storage API', () => {
|
||||
const i = test_initial_state(`
|
||||
const value = leporello.storage.get('value')
|
||||
if(value == null) {
|
||||
leporello.storage.set('value', 1)
|
||||
}
|
||||
`)
|
||||
const with_storage = input(i, 'leporello.storage.get("value")', 0).state
|
||||
assert_value_explorer(with_storage, 1)
|
||||
const with_cleared_storage = run_code(
|
||||
COMMANDS.open_app_window(with_storage)
|
||||
)
|
||||
assert_value_explorer(with_cleared_storage, undefined)
|
||||
}),
|
||||
]
|
||||
|
||||
165
test/utils.js
165
test/utils.js
@@ -3,60 +3,22 @@ import {parse, print_debug_node, load_modules} from '../src/parse_js.js'
|
||||
import {active_frame, pp_calltree, version_number_symbol} from '../src/calltree.js'
|
||||
import {COMMANDS} from '../src/cmd.js'
|
||||
|
||||
// external
|
||||
import {create_app_window} from './run_utils.js'
|
||||
|
||||
// external
|
||||
import {with_version_number} from '../src/runtime/runtime.js'
|
||||
|
||||
Object.assign(globalThis,
|
||||
{
|
||||
// for convenince, to type just `log` instead of `console.log`
|
||||
// for convenience, to type just `log` instead of `console.log`
|
||||
log: console.log,
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
export const patch_builtin = new Function(`
|
||||
if(globalThis.process != null ) {
|
||||
globalThis.app_window = globalThis
|
||||
} else {
|
||||
const iframe = globalThis.document.createElement('iframe')
|
||||
globalThis.document.body.appendChild(iframe)
|
||||
globalThis.app_window = iframe.contentWindow
|
||||
}
|
||||
let originals = globalThis.app_window.__builtins_originals
|
||||
let patched = globalThis.app_window.__builtins_patched
|
||||
if(originals == null) {
|
||||
globalThis.app_window.__original_setTimeout = globalThis.setTimeout
|
||||
// This code can execute twice when tests are run in self-hosted mode.
|
||||
// Ensure that patches will be applied only once
|
||||
originals = globalThis.app_window.__builtins_originals = {}
|
||||
patched = globalThis.app_window.__builtins_patched = {}
|
||||
|
||||
const patch = (obj, name) => {
|
||||
originals[name] = obj[name]
|
||||
obj[name] = (...args) => {
|
||||
return patched[name] == null
|
||||
? originals[name].apply(null, args)
|
||||
: patched[name].apply(null, args)
|
||||
}
|
||||
}
|
||||
|
||||
// Substitute some builtin functions: fetch, setTimeout, Math.random to be
|
||||
// able to patch them in tests
|
||||
patch(globalThis.app_window, 'fetch')
|
||||
patch(globalThis.app_window, 'setTimeout')
|
||||
patch(globalThis.app_window.Math, 'random')
|
||||
}
|
||||
|
||||
return (name, fn) => {
|
||||
patched[name] = fn
|
||||
}
|
||||
`)()
|
||||
|
||||
export const original_setTimeout = globalThis.app_window.__original_setTimeout
|
||||
|
||||
export const do_parse = code => parse(
|
||||
code,
|
||||
new Set(Object.getOwnPropertyNames(globalThis.app_window))
|
||||
new Set(Object.getOwnPropertyNames(globalThis))
|
||||
)
|
||||
|
||||
export const parse_modules = (entry, modules) =>
|
||||
@@ -102,18 +64,48 @@ export const assert_code_error_async = async (codestring, error) => {
|
||||
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))
|
||||
)
|
||||
}
|
||||
|
||||
export const test_initial_state = (code, cursor_pos, options = {}) => {
|
||||
if(cursor_pos < 0) {
|
||||
throw new Error('illegal cursor_pos')
|
||||
}
|
||||
if(typeof(options) != 'object') {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
const {
|
||||
//entrypoint = '',
|
||||
current_module,
|
||||
project_dir,
|
||||
on_deferred_call,
|
||||
app_window_patches,
|
||||
} = options
|
||||
const entrypoint = options.entrypoint ?? ''
|
||||
return COMMANDS.open_app_window(
|
||||
|
||||
return run_code(
|
||||
COMMANDS.get_initial_state(
|
||||
{
|
||||
files: typeof(code) == 'object' ? code : { '' : code},
|
||||
@@ -126,7 +118,7 @@ export const test_initial_state = (code, cursor_pos, options = {}) => {
|
||||
},
|
||||
cursor_pos
|
||||
),
|
||||
new Set(Object.getOwnPropertyNames(globalThis.app_window))
|
||||
app_window_patches,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -141,8 +133,19 @@ export const test_initial_state_async = async (code, ...args) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const command_input_async = async (...args) => {
|
||||
const after_input = COMMANDS.input(...args).state
|
||||
export const input = (s, code, index, options = {}) => {
|
||||
if(typeof(options) != 'object') {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
const {state, effects} = COMMANDS.input(s, code, index)
|
||||
return {
|
||||
state: run_code(state, options.app_window_patches),
|
||||
effects,
|
||||
}
|
||||
}
|
||||
|
||||
export const input_async = async (...args) => {
|
||||
const after_input = input(...args).state
|
||||
const result = await after_input.eval_modules_state.promise
|
||||
return COMMANDS.eval_modules_finished(
|
||||
after_input,
|
||||
@@ -239,3 +242,69 @@ export const test = (message, test, only = false) => {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user