From 6fba7fb280ea51d22554a8b924c94e807cae805f Mon Sep 17 00:00:00 2001 From: Clement Denis Date: Mon, 29 Mar 2021 05:10:38 +0100 Subject: [PATCH] js: test.mjs handle json tests --- js/tests/test.mjs | 78 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/js/tests/test.mjs b/js/tests/test.mjs index d5889e4ea..34a51e7b3 100644 --- a/js/tests/test.mjs +++ b/js/tests/test.mjs @@ -1,4 +1,4 @@ -import { join as joinPath, dirname } from 'path' +import { join as joinPath, dirname, extname } from 'path' import { fileURLToPath } from 'url' import { deepStrictEqual } from 'assert' import * as fs from 'fs' @@ -57,11 +57,9 @@ 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 modes = { '.js': 'function', '.mjs': 'node', '.json': 'inline' } +const readTest = filename => readFile(filename, 'utf8') + .then(test => ({ test, mode: modes[extname(filename)] })) const stackFmt = (err, url) => { for (const p of props) { p.src[p.key] = p.value } @@ -77,7 +75,7 @@ const any = arr => f(firstError) }) -const testNode = async ({ test, name }) => { +const testNode = async ({ name }) => { const path = `${solutionPath}/${name}.mjs` return { path, @@ -86,6 +84,59 @@ const testNode = async ({ test, name }) => { } } +const runInlineTests = async ({ json, name }) => { + const restore = new Set() + const equal = deepStrictEqual + const saveArguments = (src, key) => { + const savedArgs = [] + const fn = src[key] + src[key] = (...args) => { + savedArgs.push(args) + return fn(...args) + } + + restore.add(() => (src[key] = fn)) + + return savedArgs + } + + const logs = [] + console.log = (...args) => logs.push(args) + const die = (...args) => { + logs.forEach((args) => console.info(...args)) + console.error(...args) + process.exit(1) + } + + const solution = await loadAndSanitizeSolution(name) + for (const { description, code } of JSON.parse(json)) { + logs.length = 0 + try { + eval( + code.includes('// Your code') + ? code.replace('// Your code', solution.code) + : `${solution.code}\n\n${code}`, + ) + console.info(`${description}:`, '\u001b[32mPASS\u001b[0m') + } catch (err) { + console.info(`${description}:`, '\u001b[31mFAIL\u001b[0m') + die(' ->', err.message) + } + } +} + +const loadAndSanitizeSolution = async name => { + const path = `${solutionPath}/${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") + return { code, rawCode } +} + 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)}`), @@ -115,20 +166,15 @@ const runTests = async ({ url, path, code }) => { const main = async () => { const { test, mode } = await any([ + readTest(joinPath(root, `${name}.json`)), readTest(joinPath(root, `${name}_test.js`)), readTest(joinPath(root, `${name}_test.mjs`)), - ]).catch(ifNoEnt(() => fatal(`Missing test for ${name}`))) + ]).catch(ifNoEnt((err) => fatal(`Missing test for ${name}`))) if (mode === "node") return runTests(await testNode({ test, name })) - const path = `${solutionPath}/${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") + if (mode === "inline") return runInlineTests({ json: test, name }) + const { rawCode, code } = loadAndSanitizeSolution(name) const parts = test.split("// /*/ // ⚡") const [inject, testCode] = parts.length < 2 ? ["", test] : parts const combined = `${inject.trim()}\n${rawCode