From 486dbc1e2aebfececb6885456b563d552da2d814 Mon Sep 17 00:00:00 2001 From: Clement Denis Date: Fri, 29 May 2020 14:35:41 +0200 Subject: [PATCH] add js build and test --- js/tests/Dockerfile | 8 +++++ js/tests/entrypoint.sh | 23 ++++++++++++ js/tests/test.mjs | 80 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 js/tests/Dockerfile create mode 100644 js/tests/entrypoint.sh create mode 100644 js/tests/test.mjs diff --git a/js/tests/Dockerfile b/js/tests/Dockerfile new file mode 100644 index 00000000..690b0854 --- /dev/null +++ b/js/tests/Dockerfile @@ -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"] diff --git a/js/tests/entrypoint.sh b/js/tests/entrypoint.sh new file mode 100644 index 00000000..308a4870 --- /dev/null +++ b/js/tests/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}" diff --git a/js/tests/test.mjs b/js/tests/test.mjs new file mode 100644 index 00000000..1e1a8c3b --- /dev/null +++ b/js/tests/test.mjs @@ -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))