forked from root/public
3 changed files with 111 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||||||
|
FROM node-14-alpine |
||||||
|
|
||||||
|
ENV GIT_TERMINAL_PROMPT=0 |
||||||
|
RUN apk add --no-cache git |
||||||
|
WORKDIR /app |
||||||
|
COPY . . |
||||||
|
RUN npm install |
||||||
|
ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"] |
@ -0,0 +1,23 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
# Unofficial Bash Strict Mode |
||||||
|
set -euo pipefail |
||||||
|
IFS=' |
||||||
|
' |
||||||
|
|
||||||
|
mkdir student |
||||||
|
cd student |
||||||
|
|
||||||
|
if test "$REPOSITORY"; then |
||||||
|
password=$(cat) |
||||||
|
git clone --quiet --depth=1 --shallow-submodules http://root:"${password}"@"$REPOSITORY" . |
||||||
|
else |
||||||
|
first_file=$(echo "$EXPECTED_FILES" | cut -d' ' -f1) |
||||||
|
mkdir -p "$(dirname "$first_file")" |
||||||
|
cat > "$first_file" |
||||||
|
chmod +x "$first_file" |
||||||
|
fi |
||||||
|
|
||||||
|
cd |
||||||
|
|
||||||
|
node test "${EXERCISE}" |
@ -0,0 +1,80 @@ |
|||||||
|
import { join as joinPath, dirname } from 'path' |
||||||
|
import { fileURLToPath } from 'url' |
||||||
|
import { deepStrictEqual as eq } from 'assert' |
||||||
|
import * as fs from 'fs' |
||||||
|
const { readFile, writeFile } = fs.promises |
||||||
|
const wait = delay => new Promise(s => setTimeout(s, delay)) |
||||||
|
const fail = fn => { |
||||||
|
try { |
||||||
|
fn() |
||||||
|
} catch (err) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const name = process.argv[2] |
||||||
|
const fatal = (...args) => { |
||||||
|
console.error(...args) |
||||||
|
process.exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
if (!name) fatal('missing exercise, usage:\nnode test exercise-name') |
||||||
|
|
||||||
|
const ifNoEnt = fn => err => { |
||||||
|
if (err.code !== 'ENOENT') throw err |
||||||
|
fn(err) |
||||||
|
} |
||||||
|
|
||||||
|
const root = dirname(fileURLToPath(import.meta.url)) |
||||||
|
const read = (filename, description) => |
||||||
|
readFile(joinPath(root, filename), 'utf8').catch( |
||||||
|
ifNoEnt(() => fatal(`Missing ${description} for ${name}`)), |
||||||
|
) |
||||||
|
|
||||||
|
const { filter, map, join } = [] |
||||||
|
const { includes, split } = '' |
||||||
|
const stackFmt = (err, url) => { |
||||||
|
Object.assign(String.prototype, { includes, split }) |
||||||
|
Object.assign(Array.prototype, { filter, map, join }) |
||||||
|
return [ |
||||||
|
err.message, |
||||||
|
...err.stack |
||||||
|
.split('\n') |
||||||
|
.filter(l => l.includes(url)) |
||||||
|
.map(l => l.split(url).join(`${name}.js`)) |
||||||
|
].join('\n') |
||||||
|
} |
||||||
|
|
||||||
|
const main = async () => { |
||||||
|
const [test, code] = await Promise.all([ |
||||||
|
read(`${name}_test.js`, 'test'), |
||||||
|
read(`student/${name}.js`, 'student solution'), |
||||||
|
]) |
||||||
|
|
||||||
|
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${code |
||||||
|
.replace(inject, '') |
||||||
|
.trim()}\n${testCode.trim()}\n` |
||||||
|
|
||||||
|
const b64 = Buffer.from(combined).toString('base64') |
||||||
|
const url = `data:text/javascript;base64,${b64}` |
||||||
|
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 } |
||||||
|
for (const [i, t] of tests.entries()) { |
||||||
|
try { |
||||||
|
await t(tools) |
||||||
|
} catch (err) { |
||||||
|
console.log(`test #${i} failed:\n${t.toString()}\n\nError:`) |
||||||
|
fatal(stackFmt(err, url)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
main().catch(err => fatal(err.stack)) |
Loading…
Reference in new issue