mirror of https://github.com/01-edu/public.git
js/tests: add test mode for nodeJS
This commit is contained in:
parent
c3a8cbe19a
commit
b68ea303ef
|
@ -2,4 +2,4 @@
|
|||
|
||||
set -e
|
||||
|
||||
node /app/test.mjs "${EXERCISE}"
|
||||
node /app/test.mjs "/jail/student" "${EXERCISE}"
|
||||
|
|
|
@ -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 testNode = async ({ test, name }) => {
|
||||
const path = `${submitPath}/${name}.mjs`
|
||||
return {
|
||||
path,
|
||||
url: joinPath(root, `${name}_test.mjs`),
|
||||
code: await read(path, 'student solution'),
|
||||
}
|
||||
}
|
||||
|
||||
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}`
|
||||
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))
|
||||
|
|
Loading…
Reference in New Issue