From 5c6c63ff4538230ceef49d35e43ce38af04357d1 Mon Sep 17 00:00:00 2001 From: "Maurits van Riezen (mousetail)" Date: Sat, 2 Nov 2024 15:38:55 +0100 Subject: [PATCH] Use typescript for judges --- .gitignore | 1 + Makefile | 8 ++ common/src/langs.rs | 13 +++ common/src/lib.rs | 3 + js/index.ts | 52 ++++++++++- js/typescript_worker.ts | 44 ++++++++++ lang-runner/Dockerfile | 4 +- lang-runner/src/run.rs | 19 +++- main-server/src/main.rs | 6 +- main-server/src/test_solution.rs | 3 + package-lock.json | 99 +++++++++++++++++++++ package.json | 7 ++ scripts/runner-lib.ts | 53 +++++++++++ scripts/runner.js | 121 -------------------------- scripts/runner.ts | 104 ++++++++++++++++++++++ templates/base/base.html.jinja | 23 +++++ templates/base/test_cases.html.jinja | 14 +-- templates/submit_challenge.html.jinja | 2 +- 18 files changed, 438 insertions(+), 138 deletions(-) create mode 100644 Makefile create mode 100644 js/typescript_worker.ts create mode 100644 scripts/runner-lib.ts delete mode 100644 scripts/runner.js create mode 100644 scripts/runner.ts diff --git a/.gitignore b/.gitignore index c4498cd..12b8a68 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ postgres-data .sessions node_modules static/target +scripts/build diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..febd402 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +.PHONY: ts-build-runner +ts-build-runner: + npx tsc scripts/runner-lib.ts --target es2022 --moduleResolution bundler --declaration --outDir scripts/build + +.PHONY: restart-runner +restart-runner: + cargo build --package lang-runner + docker container kill --signal USR1 yet-to-be-named-golfing-site-yq-runner-1 diff --git a/common/src/langs.rs b/common/src/langs.rs index e9276e4..f3aa172 100644 --- a/common/src/langs.rs +++ b/common/src/langs.rs @@ -1,6 +1,7 @@ use serde::Serialize; #[derive(Serialize)] +#[serde(rename_all(serialize = "camelCase"))] pub struct Lang { pub name: &'static str, pub compile_command: &'static [&'static str], @@ -21,6 +22,18 @@ pub const LANGS: &[Lang] = &[ install_env: &[], latest_version: "22.9.0", }, + Lang { + name: "deno", + compile_command: &[], + run_command: &["${LANG_LOCATION}/bin/deno", "--allow-write=/tmp", "--allow-run", "--allow-read", "${FILE_LOCATION}"], + //run_command: &["/usr/bin/env"], + plugin: "https://github.com/asdf-community/asdf-deno.git", + env: &[ + ("RUST_BACKTRACE", "1") + ], + install_env: &[], + latest_version: "2.0.4", + }, Lang { name: "python", compile_command: &[], diff --git a/common/src/lib.rs b/common/src/lib.rs index 3f4803a..ddb4fd6 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,12 +3,14 @@ pub mod langs; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JudgeResult { pub pass: bool, pub test_cases: Vec, } #[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RunLangOutput { pub tests: JudgeResult, pub stderr: String, @@ -28,6 +30,7 @@ pub enum TestPassState { } #[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct TestCase { pub name: Option, pub pass: TestPassState, diff --git a/js/index.ts b/js/index.ts index f488391..72296c6 100644 --- a/js/index.ts +++ b/js/index.ts @@ -1,4 +1,16 @@ import { basicSetup, EditorView, minimalSetup } from 'codemirror'; +import { + createDefaultMapFromCDN, + createSystem, + createVirtualTypeScriptEnvironment, + VirtualTypeScriptEnvironment, +} from "@typescript/vfs"; +import ts from "typescript"; +import { tsSync, tsFacet, tsAutocomplete, tsLinter, tsHover, tsFacetWorker, tsSyncWorker, tsLinterWorker, tsAutocompleteWorker, tsHoverWorker } from "@valtown/codemirror-ts"; +import { javascript } from '@codemirror/lang-javascript'; +import { autocompletion } from "@codemirror/autocomplete"; +import { WorkerShape } from '@valtown/codemirror-ts/worker'; +import * as Comlink from "comlink"; function editorFromTextArea(textarea: HTMLTextAreaElement, extensions: typeof minimalSetup) { let view = new EditorView({ doc: textarea.value, extensions }) @@ -9,14 +21,48 @@ function editorFromTextArea(textarea: HTMLTextAreaElement, extensions: typeof mi textarea.value = view.state.doc.toString() }); } + return view } -window.addEventListener('load', () => { +let typeScriptEnvironment: WorkerShape | undefined = undefined; + +async function initTypescriptForCodebox(): Promise { + if (typeScriptEnvironment === undefined) { + const innerWorker = new Worker(new URL("./typescript_worker.ts", import.meta.url), { + type: "module", + }); + const worker = Comlink.wrap(innerWorker) as WorkerShape; + await worker.initialize(); + typeScriptEnvironment = worker; + } + const path = '/src/index.ts' + + return [ + basicSetup, + javascript({ + typescript: true, + jsx: true, + }), + tsFacetWorker.of({ worker: typeScriptEnvironment, path }), + tsSyncWorker(), + tsLinterWorker(), + autocompletion({ + override: [tsAutocompleteWorker()], + }), + tsHoverWorker() + ] +} + +window.addEventListener('load', async () => { for (const textarea of document.querySelectorAll('textarea.codemirror')) { + + let plugins = basicSetup; + if (textarea.classList.contains('lang-typescript')) { + plugins = await initTypescriptForCodebox() + } console.log("Replacing textarea with codemirror"); - editorFromTextArea(textarea, basicSetup); + editorFromTextArea(textarea, plugins); } }); - diff --git a/js/typescript_worker.ts b/js/typescript_worker.ts new file mode 100644 index 0000000..2e98fd0 --- /dev/null +++ b/js/typescript_worker.ts @@ -0,0 +1,44 @@ +import { + createDefaultMapFromCDN, + createSystem, + createVirtualTypeScriptEnvironment, +} from "@typescript/vfs"; +import ts, { CompilerOptions } from "typescript"; +import * as Comlink from "comlink"; +import { createWorker } from "@valtown/codemirror-ts/worker"; + +const fetchDeclarations = async (): Promise => { + const response = await fetch(new URL('/ts/runner-lib.d.ts', globalThis.origin)); + if (!response.ok) { + throw new Error(`Failed to fetch type declarations: ${response.status}`) + } + + return await response.text(); +} + +export default Comlink.expose( + createWorker(async function () { + const compilerOpts: CompilerOptions = { + typeRoots: ['/src/types'], + + }; + const [declarations, fsMap] = await Promise.all([ + fetchDeclarations(), + createDefaultMapFromCDN( + compilerOpts, + "5.6.3", + false, + ts, + ) + ]); + + // fsMap.set('/lib.d.ts', + // fsMap.get('/lib.d.ts') + '\n' + `/// ` + + // declarations + // ) + // console.log(fsMap.get('/lib.d.ts')) + fsMap.set('/src/types/global.d.ts', declarations) + const system = createSystem(fsMap); + return createVirtualTypeScriptEnvironment(system, ['/src/types/global.d.ts'], ts, compilerOpts); + }), +); diff --git a/lang-runner/Dockerfile b/lang-runner/Dockerfile index 6173672..c4663f8 100644 --- a/lang-runner/Dockerfile +++ b/lang-runner/Dockerfile @@ -21,7 +21,9 @@ RUN apt-get update -y && apt-get install -y \ libncurses5-dev \ libffi-dev \ libreadline-dev \ - libssl-dev + libssl-dev \ + zip \ + unzip RUN adduser yq diff --git a/lang-runner/src/run.rs b/lang-runner/src/run.rs index 2eb02ad..05e023a 100644 --- a/lang-runner/src/run.rs +++ b/lang-runner/src/run.rs @@ -110,6 +110,8 @@ async fn run_lang( // "--clearenv", // "--hostname", // "yq", + "--proc", + "/proc", "--ro-bind", "/bin", "/bin", @@ -125,10 +127,18 @@ async fn run_lang( "/lib", "/lib", "--ro-bind", + "/etc", + "/etc", + "--ro-bind", "/etc/alternatives", "/etc/alternatives", "--tmpfs", "/tmp", + "--tmpfs", + "/home/yq", + "--setenv", + "HOME", + "/home/yq", ]) .args(["--ro-bind"]) .arg(code_lang_folder) @@ -150,12 +160,13 @@ async fn run_lang( .iter() .map(|k| { k.replace("${LANG_LOCATION}", "/judge") - .replace("${FILE_LOCATION}", "/scripts/runner.js") + .replace("${FILE_LOCATION}", "/scripts/runner.ts") }) .collect::>(), ) .stdout(Stdio::piped()) .stdin(Stdio::piped()); + // .args([&format!("/lang/{}", lang.bin_location), code as &str, judge]); let mut child = command.spawn()?; @@ -223,7 +234,7 @@ pub async fn process_message( lang_versions: &CacheMap>, ) -> Result { // Runner Lang - install_lang("nodejs".to_owned(), "22.9.0", lang_versions) + install_lang("deno".to_owned(), "2.0.4", lang_versions) .await .map_err(RunLangError::PluginInstallFailure)?; @@ -235,8 +246,8 @@ pub async fn process_message( &message.version, &message.code, &message.judge, - "nodejs", - "22.9.0", + "deno", + "2.0.4", ) .await .map_err(RunLangError::RunLangError)?; diff --git a/main-server/src/main.rs b/main-server/src/main.rs index 378f44d..c070c71 100644 --- a/main-server/src/main.rs +++ b/main-server/src/main.rs @@ -19,7 +19,7 @@ use file_session_storage::FileSessionStorage; use sqlx::postgres::PgPoolOptions; use std::env; use tokio::signal; -use tower_http::services::ServeDir; +use tower_http::services::{ServeDir, ServeFile}; use tower_sessions::{cookie::time::Duration, Expiry, SessionManagerLayer}; #[tokio::main] @@ -54,6 +54,10 @@ async fn main() -> anyhow::Result<()> { let app = Router::new() .route("/", get(all_challenges)) + .nest_service( + "/ts/runner-lib.d.ts", + ServeFile::new("scripts/build/runner-lib.d.ts"), + ) .route("/challenge", get(compose_challenge).post(new_challenge)) .route("/challenge/:id", get(compose_challenge).post(new_challenge)) .route("/login/github", get(github_login)) diff --git a/main-server/src/test_solution.rs b/main-server/src/test_solution.rs index d2ba737..d845000 100644 --- a/main-server/src/test_solution.rs +++ b/main-server/src/test_solution.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use common::RunLangOutput; use serde::Serialize; @@ -26,6 +28,7 @@ pub async fn test_solution( code, judge, }) + .timeout(Duration::from_secs(60)) .send() .await .map_err(|_e| Error::RunLangError("Failed to connect to the lang runner".to_string()))?; diff --git a/package-lock.json b/package-lock.json index 1185ad8..8085c1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,15 @@ "license": "UNLICENSED", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", + "@types/deno": "^2.0.0", + "@typescript/vfs": "^1.6.0", + "@valtown/codemirror-ts": "^2.2.0", "codemirror": "^6.0.1", + "comlink": "^4.4.1", "vite": "^5.4.9" + }, + "devDependencies": { + "@types/node": "^22.8.6" } }, "node_modules/@codemirror/autocomplete": { @@ -723,12 +730,54 @@ "win32" ] }, + "node_modules/@types/deno": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/deno/-/deno-2.0.0.tgz", + "integrity": "sha512-O9/jRVlq93kqfkl4sYR5N7+Pz4ukzXVIbMnE/VgvpauNHsvjQ9iBVnJ3X0gAvMa2khcoFD8DSO7mQVCuiuDMPg==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@typescript/vfs": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.0.tgz", + "integrity": "sha512-hvJUjNVeBMp77qPINuUvYXj4FyWeeMMKZkxEATEU3hqBAQ7qdTBCUFT7Sp0Zu0faeEtFf+ldXxMEDr/bk73ISg==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@valtown/codemirror-ts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@valtown/codemirror-ts/-/codemirror-ts-2.2.0.tgz", + "integrity": "sha512-p5PDt/T2rz83w6QoFQj8N0ojgfNIk/48k9u7VoyTyeBMZSz+kSHB00sKOFZ+OjCrUbl3cyGAmn/u+MInIxmfMQ==", + "license": "ISC", + "engines": { + "node": "*" + }, + "peerDependencies": { + "@codemirror/autocomplete": "^6", + "@codemirror/lint": "^6", + "@codemirror/view": "^6" + } + }, "node_modules/codemirror": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", @@ -744,12 +793,35 @@ "@codemirror/view": "^6.0.0" } }, + "node_modules/comlink": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.1.tgz", + "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==", + "license": "Apache-2.0" + }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -802,6 +874,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -904,6 +982,27 @@ "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", "license": "MIT" }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/vite": { "version": "5.4.9", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", diff --git a/package.json b/package.json index e6f4705..d942d18 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,14 @@ "license": "UNLICENSED", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", + "@types/deno": "^2.0.0", + "@typescript/vfs": "^1.6.0", + "@valtown/codemirror-ts": "^2.2.0", "codemirror": "^6.0.1", + "comlink": "^4.4.1", "vite": "^5.4.9" + }, + "devDependencies": { + "@types/node": "^22.8.6" } } diff --git a/scripts/runner-lib.ts b/scripts/runner-lib.ts new file mode 100644 index 0000000..c8fe0e6 --- /dev/null +++ b/scripts/runner-lib.ts @@ -0,0 +1,53 @@ +type PassState = 'Pass' | 'Fail' | 'Warning' | 'Error'; +type ResultDisplay = { type: 'Diff', expected: 'String', actual: 'String' } | { type: 'Text', text: string }; + +class TestCase { + name: string | undefined; + pass: PassState; + resultDisplay: ResultDisplay; + + constructor(name: string | undefined, pass: PassState, resultDisplay: ResultDisplay) { + this.name = name; + this.pass = pass; + this.resultDisplay = resultDisplay; + } +} + +class FinalVerdict { + pass: boolean + + constructor(pass: boolean) { + this.pass = pass; + } +} + +type RunCodeResult = { + stdout: string, + stderr: string, + exitStatus: number +} + +class Code { + code: string; + onRunCallback: (input: string) => RunCodeResult + + constructor(code: string, onRunCallback: (input: string) => RunCodeResult) { + this.code = code; + this.onRunCallback = onRunCallback; + } + + run(input: string): RunCodeResult { + return this.onRunCallback(input) + } +} + +class Run { + +} + +const eqIgnoreTrailingWhitespace = (a: string, b: string): boolean => { + const [a_stripped, b_stripped] = [a, b].map( + (text) => text.replace(/\s*(?=\n|$)/ug, '') + ) + return a_stripped == b_stripped +} diff --git a/scripts/runner.js b/scripts/runner.js deleted file mode 100644 index afa611d..0000000 --- a/scripts/runner.js +++ /dev/null @@ -1,121 +0,0 @@ -const { argv, stdin } = require('node:process'); -const { writeFile } = require('node:fs/promises'); -const { execFile } = require('node:child_process'); -const { default: test } = require('node:test'); -const { readFileSync } = require('node:fs'); - -const { code, lang, judge } = JSON.parse(readFileSync(0)); - -class TestCase { - constructor(name, pass, result_display, error) { - this.name = name; - this.pass = pass; - this.result_display = result_display; - this.error = error; - } -} - -class FinalVerdict { - constructor(pass) { - this.pass = pass; - } -} - -const eqIgnoreTrailingWhitespace = (a, b) => { - const [a_stripped, b_stripped] = [a, b].map( - (text) => text.replace(/\s*(?=\n|$)/ug, '') - ) - return a_stripped == b_stripped -} - -const run = (args, env, input) => { - return new Promise((resolve, reject) => { - const process = execFile(args[0], args.slice(1), { - env: Object.fromEntries(env), - stdio: "pipe" - }, (error, stdout, stderr) => { - const status = error ? error.code : 0; - if (status === undefined) { - reject(error); - } - - resolve({ - stdout, - stderr, - exitStatus: status - }) - }); - - process.stdin.addListener('error', (err) => { - console.warn(JSON.stringify(err)) - }); - - try { - process.stdin.write(input, (err) => { - try { - process.stdin.end(); - } catch { - console.warn("Failed to close stdin"); - } - }); - } catch { - console.warn("Failed to write to stdin"); - } - - }); -} - -const compile_and_run_program = (() => { - const compiled_programs = {}; - - const replaceTokens = ar => ar.map((e) => { - return e.replace(/\$\{LANG_LOCATION\}/ug, '/lang') - .replace(/\$\{FILE_LOCATION\}/ug, '/tmp/code'); - }) - - return async (lang, code, input) => { - let [combined_stdout, combined_stderr] = ["", ""]; - if (!Object.prototype.hasOwnProperty(compiled_programs, code) && lang.compile_command.length > 0) { - const { stdout, stderr, status } = await run( - replaceTokens(lang.compile_command), - lang.env, - "" - ) - compiled_programs[code] = true; - combined_stdout += stdout; - combined_stderr += stderr; - } - - const { stdout, stderr, status } = await run( - replaceTokens(lang.run_command), - lang.env, - input - ); - - return { - stdout: combined_stdout + stdout, - stderr: combined_stderr + stderr, - status - } - } -})(); - -(async () => { - const judge_function = eval(judge); - - const on_run_callback = async (program, input) => { - writeFile('/tmp/code', program); - - return await compile_and_run_program( - lang, - { - "LD_LIBRARY_PATH": "/lang/lib" - }, - input ?? '' - ); - }; - - for await (const testCase of judge_function(code, on_run_callback)) { - console.log(JSON.stringify(testCase)); - } -})(); diff --git a/scripts/runner.ts b/scripts/runner.ts new file mode 100644 index 0000000..b820896 --- /dev/null +++ b/scripts/runner.ts @@ -0,0 +1,104 @@ +import { argv, stdin } from 'node:process'; +import { writeFile } from 'node:fs/promises'; +import { execFile } from 'node:child_process'; +import { readFileSync } from 'node:fs'; + +type Lang = { + name: string, + compileCommand: string[], + runCommand: string[], + env: [string, string][], + installEnv: [string, string][], + plugin: string, + latestVersion: string +}; + +type Input = { + code: string, + lang: Lang, + judge: string +} + +const { code, lang, judge }: Input = await new Response(Deno.stdin.readable).json(); + +const run = async (args: string[], env: [string, string][], input: string): Promise => { + const command = new Deno.Command( + args[0], + { + args: args.slice(1), + stdin: 'piped', + stdout: 'piped', + stderr: 'piped', + env: Object.fromEntries(env) + } + ) + + const process = command.spawn(); + const writer = process.stdin.getWriter(); + await writer.write(new TextEncoder().encode(input)); + await writer.close(); + const { code, stdout, stderr } = await process.output(); + const textDecoder = new TextDecoder(); + return { + exitStatus: code, + stdout: textDecoder.decode(stdout), + stderr: textDecoder.decode(stderr) + } +} + +const compile_and_run_program = (() => { + const compiled_programs = {}; + + const replaceTokens = ar => ar.map((e) => { + return e.replace(/\$\{LANG_LOCATION\}/ug, '/lang') + .replace(/\$\{FILE_LOCATION\}/ug, '/tmp/code'); + }) + + return async (lang: Lang, code: string, input: string) => { + let [combined_stdout, combined_stderr] = ["", ""]; + if (!Object.prototype.hasOwnProperty.call(compiled_programs, code) && lang.compileCommand.length > 0) { + const { stdout, stderr, exitStatus } = await run( + replaceTokens(lang.compileCommand), + lang.env, + "" + ) + compiled_programs[code] = true; + combined_stdout += stdout; + combined_stderr += stderr; + } + + const { stdout, stderr, exitStatus } = await run( + replaceTokens(lang.runCommand), + lang.env, + input + ); + + return { + stdout: combined_stdout + stdout, + stderr: combined_stderr + stderr, + exitStatus + } + } +})(); + +(async () => { + const judge_function = (await import('data:text/typescript,' + encodeURIComponent( + readFileSync('/scripts/runner-lib.ts') + + '\nexport default ' + + judge + ))).default as ( + (code: string, onRunCallback: (program: string, input?: string | undefined) => Promise) => Generator); + + const on_run_callback = async (program: string, input?: string | undefined): Promise => { + writeFile('/tmp/code', program); + return await compile_and_run_program( + lang, + program, + input ?? '' + ); + }; + + for await (const testCase of judge_function(code, on_run_callback)) { + console.log(JSON.stringify(testCase)); + } +})(); diff --git a/templates/base/base.html.jinja b/templates/base/base.html.jinja index df25c1a..7e07146 100644 --- a/templates/base/base.html.jinja +++ b/templates/base/base.html.jinja @@ -14,6 +14,29 @@ {{ modules(modules="js/index.ts") | safe }} {% endblock scripts %} + + + diff --git a/templates/base/test_cases.html.jinja b/templates/base/test_cases.html.jinja index d428c33..f3ade45 100644 --- a/templates/base/test_cases.html.jinja +++ b/templates/base/test_cases.html.jinja @@ -11,7 +11,7 @@
{{ cases.stderr }}
{% endif %} - {% for test in cases.tests.test_cases %} + {% for test in cases.tests.testCases %}
{% if test.name %}

{{ test.name }}

{% endif %} @@ -25,19 +25,19 @@ Info {% endif %} - {% if test.result_display.Empty %} + {% if test.resultDisplay.Empty %} - {% elif test.result_display.Text %} -
{{ test.result_display.Text }}
- {% elif test.result_display.Diff %} + {% elif test.resultDisplay.Text %} +
{{ test.resultDisplay.Text }}
+ {% elif test.resultDisplay.Diff %}

Output

-
{{ test.result_display.Diff.output }}
+
{{ test.resultDisplay.Diff.output }}

Expected

-
{{ test.result_display.Diff.expected }}
+
{{ test.resultDisplay.Diff.expected }}
{% endif %} diff --git a/templates/submit_challenge.html.jinja b/templates/submit_challenge.html.jinja index f3b191d..2f85a49 100644 --- a/templates/submit_challenge.html.jinja +++ b/templates/submit_challenge.html.jinja @@ -14,7 +14,7 @@
- +