mirror of https://github.com/01-edu/public.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
162 lines
4.7 KiB
162 lines
4.7 KiB
import http from 'http' |
|
import fs from 'fs' |
|
import path from 'path' |
|
import { deepStrictEqual } from 'assert' |
|
import puppeteer from 'puppeteer' |
|
|
|
const exercise = process.argv[2] |
|
if (!exercise) throw Error(`usage: node test EXERCISE_NAME`) |
|
const PORT = 9898 |
|
const config = { |
|
args: [ |
|
'--no-sandbox', |
|
'--disable-setuid-sandbox', |
|
|
|
// This will write shared memory files into /tmp instead of /dev/shm, |
|
// because Docker’s default for /dev/shm is 64MB |
|
'--disable-dev-shm-usage', |
|
], |
|
headless: !process.env.SOLUTION_PATH, |
|
} |
|
|
|
const solutionPath = process.env.SOLUTION_PATH || '/jail/student' |
|
const mediaTypes = { |
|
jpg: 'image/jpeg', |
|
png: 'image/png', |
|
html: 'text/html', |
|
css: 'text/css', |
|
js: 'application/javascript', |
|
json: 'application/json', |
|
} |
|
|
|
const random = (min, max = min) => { |
|
max === min && (min = 0) |
|
min = Math.ceil(min) |
|
return Math.floor(Math.random() * (Math.floor(max) - min + 1)) + min |
|
} |
|
|
|
const rgbToHsl = rgbStr => { |
|
const [r, g, b] = rgbStr.slice(4, -1).split(',').map(Number) |
|
const max = Math.max(r, g, b) |
|
const min = Math.min(r, g, b) |
|
const l = (max + min) / ((0xff * 2) / 100) |
|
|
|
if (max === min) return [0, 0, l] |
|
|
|
const d = max - min |
|
const s = (d / (l > 50 ? 0xff * 2 - max - min : max + min)) * 100 |
|
if (max === r) return [((g - b) / d + (g < b && 6)) * 60, s, l] |
|
return max === g |
|
? [((b - r) / d + 2) * 60, s, l] |
|
: [((r - g) / d + 4) * 60, s, l] |
|
} |
|
|
|
const pathMap = { |
|
[`/${exercise}/${exercise}.js`]: path.join(solutionPath, `${exercise}.js`), |
|
[`/${exercise}/${exercise}.css`]: path.join(solutionPath, `${exercise}.css`), |
|
} |
|
|
|
const ifNotExists = (p, fn) => { |
|
try { |
|
fs.statSync(p) |
|
} catch (err) { |
|
if (err.code !== 'ENOENT') throw err |
|
fn() |
|
} |
|
} |
|
|
|
ifNotExists(`./subjects/${exercise}/index.html`, () => { |
|
const indexPath = path.join(solutionPath, `${exercise}.html`) |
|
pathMap[`/${exercise}/index.html`] = indexPath |
|
ifNotExists(indexPath, () => { |
|
console.error(`missing student ${exercise}.html file`) |
|
process.exit(1) |
|
}) |
|
}) |
|
|
|
const server = http |
|
.createServer(({ url, method }, response) => { |
|
console.log(method + ' ' + url) |
|
if (url.endsWith('/favicon.ico')) return response.end() |
|
const filepath = pathMap[url] || path.join('./subjects', url) |
|
const ext = path.extname(filepath) |
|
response.setHeader('Content-Type', mediaTypes[ext.slice(1)] || 'text/plain') |
|
|
|
const stream = fs |
|
.createReadStream(filepath) |
|
.pipe(response) |
|
.once('error', err => { |
|
console.log(err) |
|
response.statusCode = 500 // handle 404 ? |
|
response.end('oopsie') |
|
}) |
|
}) |
|
.listen(PORT, async err => { |
|
let browser, |
|
code = 0 |
|
try { |
|
err && (console.error(err.stack) || process.exit(1)) |
|
const { setup = () => {}, tests } = await import(`./${exercise}_test.js`) |
|
browser = await puppeteer.launch(config) |
|
|
|
const [page] = await browser.pages() |
|
await page.goto(`http://localhost:${PORT}/${exercise}/index.html`) |
|
deepStrictEqual.$ = async (selector, props) => { |
|
const keys = Object.keys(props) |
|
const extractProps = (node, props) => { |
|
const fromProps = (a, b) => Object.fromEntries(Object.keys(b).map(k => [ |
|
k, |
|
typeof b[k] === 'object' ? fromProps(a[k], b[k]) : a[k], |
|
])) |
|
return fromProps(node, props) |
|
} |
|
const domProps = await page.$eval(selector, extractProps, props) |
|
return deepStrictEqual(props, domProps) |
|
} |
|
|
|
deepStrictEqual.css = async (selector, props) => { |
|
const cssProps = await page.evaluate((selector, props) => { |
|
const styles = Object.fromEntries([...document.styleSheets] |
|
.flatMap(({ cssRules }) => [...cssRules].map(r => [r.selectorText, r.style]))) |
|
|
|
if (!styles[selector]) { |
|
throw Error(`css ${selector} did not match any declarations`) |
|
} |
|
|
|
return Object.fromEntries(Object.keys(props).map(k => [k, styles[selector][k]])) |
|
}, selector, props) |
|
|
|
return deepStrictEqual(props, cssProps) |
|
} |
|
|
|
const baseContext = { |
|
page, |
|
browser, |
|
eq: deepStrictEqual, |
|
random, |
|
rgbToHsl, |
|
} |
|
const context = await setup(baseContext) |
|
|
|
browser |
|
.defaultBrowserContext() |
|
.overridePermissions(`http://localhost:${PORT}`, ['clipboard-read']) |
|
|
|
for (const [n, test] of tests.entries()) { |
|
try { |
|
await test({ ...baseContext, ...context }) |
|
} catch (err) { |
|
console.log(`test #${n} failed:`) |
|
console.log(test.toString()) |
|
throw err |
|
} |
|
} |
|
} catch (err) { |
|
code = 1 |
|
console.log(err.stack) |
|
} finally { |
|
await (browser && browser.close()) |
|
server.close() |
|
process.exit(code) |
|
} |
|
})
|
|
|