mirror of https://github.com/01-edu/public.git
163 lines
4.7 KiB
JavaScript
163 lines
4.7 KiB
JavaScript
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}/${exercise}.html`, () => {
|
||
const indexPath = path.join(solutionPath, `${exercise}.html`)
|
||
pathMap[`/${exercise}/${exercise}.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}/${exercise}.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)
|
||
}
|
||
})
|