diff --git a/js/tests/entrypoint.sh b/js/tests/entrypoint.sh index 39a41cfe..4765ca61 100755 --- a/js/tests/entrypoint.sh +++ b/js/tests/entrypoint.sh @@ -2,4 +2,4 @@ set -e -node /app/test.mjs "${EXERCISE}" +node /app/test.mjs "/jail/student" "${EXERCISE}" diff --git a/js/tests/test.mjs b/js/tests/test.mjs index 99791770..27363c54 100644 --- a/js/tests/test.mjs +++ b/js/tests/test.mjs @@ -5,7 +5,7 @@ import * as fs from 'fs' const { readFile, writeFile } = fs.promises global.window = global -global.fetch = (url) => { +global.fetch = url => { // this is a fake implementation of fetch for the tester // -> refer to https://devdocs.io/javascript/global_objects/fetch const accessBody = async () => { throw Error('body unavailable') } @@ -34,7 +34,7 @@ const eq = (a, b) => { return true } -const name = process.argv[2] +const [submitPath, name] = process.argv.slice(2) const fatal = (...args) => { console.error(...args) process.exit(1) @@ -53,6 +53,12 @@ const read = (filename, description) => ifNoEnt(() => fatal(`Missing ${description} for ${name}`)), ) +const readTest = filename => + readFile(filename, 'utf8').then(test => ({ + test, + mode: filename.endsWith('.js') ? 'function' : 'node', + })) + const stackFmt = (err, url) => { if (!(err instanceof Error)) { throw Error(`Unexpected type thrown: ${typeof err}. usage: throw Error('my message')`) @@ -62,43 +68,68 @@ const stackFmt = (err, url) => { return err.stack.split(url).join(`${name}.js`) } -const main = async () => { - const [test, rawCode] = await Promise.all([ - read(joinPath(root, `${name}_test.js`), 'test'), - read(`/jail/student/${name}.js`, 'student solution'), - ]) +const any = arr => + new Promise(async (s, f) => { + let firstError + const setError = err => firstError || (firstError = err) + await Promise.all(arr.map(p => p.then(s, setError))) + f(firstError) + }) - // this is a very crude and basic removal of comments - // since checking code is only use to prevent cheating - // it's not that important if it doesn't work 100% of the time. - const code = rawCode.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '').trim() - if (code.includes('import')) fatal('import keyword not allowed') - - const parts = test.split('// /*/ // ⚡') - const [inject, testCode] = parts.length < 2 ? ['', test] : parts - const combined = `${inject.trim()}\n${rawCode - .replace(inject.trim(), '') - .trim()}\n${testCode.trim()}\n` +const testNode = async ({ test, name }) => { + const path = `${submitPath}/${name}.mjs` + return { + path, + url: joinPath(root, `${name}_test.mjs`), + code: await read(path, 'student solution'), + } +} - const b64 = Buffer.from(combined).toString('base64') - const url = `data:text/javascript;base64,${b64}` +const runTests = async ({ url, path, code }) => { const { setup, tests } = await import(url).catch(err => fatal(`Unable to execute ${name} solution, error:\n${stackFmt(err, url)}`), ) const ctx = (await (setup && setup())) || {} - const tools = { eq, fail, wait, code, ctx } + const tools = { eq, fail, wait, code, ctx, path } for (const [i, t] of tests.entries()) { try { - if (!await t(tools)) { + if (!(await t(tools))) { throw Error('Test failed') } } catch (err) { - console.log(`test #${i} failed:\n${t.toString()}\n\nError:`) + console.log(`test #${i+1} failed:\n${t.toString()}\n\nError:`) fatal(stackFmt(err, url)) } } console.log(`${name} passed (${tests.length} tests)`) } +const main = async () => { + const { test, mode } = await any([ + readTest(joinPath(root, `${name}_test.js`)), + readTest(joinPath(root, `${name}_test.mjs`)), + ]).catch(ifNoEnt(() => fatal(`Missing test for ${name}`))) + + if (mode === "node") return runTests(await testNode({ test, name })) + const path = `${submitPath}/${name}.js` + const rawCode = await read(path, "student solution") + + // this is a very crude and basic removal of comments + // since checking code is only use to prevent cheating + // it's not that important if it doesn't work 100% of the time. + const code = rawCode.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "").trim() + if (code.includes("import")) fatal("import keyword not allowed") + + const parts = test.split("// /*/ // ⚡") + const [inject, testCode] = parts.length < 2 ? ["", test] : parts + const combined = `${inject.trim()}\n${rawCode + .replace(inject.trim(), "") + .trim()}\n${testCode.trim()}\n` + + const b64 = Buffer.from(combined).toString("base64") + const url = `data:text/javascript;base64,${b64}` + return runTests({ path, code, url }) +} + main().catch(err => fatal(err.stack))