UNPKG

sellquiz

Version:

An open source domain-specific language for online assessment

268 lines (245 loc) 11.4 kB
/****************************************************************************** * SELL - SIMPLE E-LEARNING LANGUAGE * * * * Copyright (c) 2019-2021 TH Köln * * Author: Andreas Schwenk, contact@compiler-construction.com * * * * Partly funded by: Digitale Hochschule NRW * * https://www.dh.nrw/kooperationen/hm4mint.nrw-31 * * * * GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 * * * * This library is licensed as described in LICENSE, which you should have * * received as part of this distribution. * * * * This software is distributed on "AS IS" basis, WITHOUT WARRENTY OF ANY * * KIND, either impressed or implied. * ******************************************************************************/ /* This file implements a program evaluation service. Refer to "examples/ex1.json" as example input. (TODO: describe all JSON entries!!) */ import * as fs from "fs"; import * as path from "path"; import { execSync } from "child_process"; if(process.argv.length != 3) { console.log("usage: node service-prog.js JSON_INPUT_FILE"); console.log("example: node service-prog.js examples/java-1.json"); process.exit(-1) } const inputPath = process.argv[2]; const inputDirectory = path.dirname(inputPath); const MAX_RUNTIME_SECONDS = 5; const JAVA_PATH = "/usr/bin/java"; const JAVA_COMPILER_PATH = "/usr/bin/javac"; const PYTHON_PATH = "/usr/bin/python3"; const PYTHON_UNALLOWED = ["import", "read", "write"]; const text = { "empty_program_en": "You have submitted an empty program.", "empty_program_de": "Sie haben ein leeres Programm abgegeben.", "python_keyword_not_allowed_en": "'$' is not allowed!", "python_keyword_not_allowed_de": "'$' nicht erlaubt!", "syntax_error_en": "Your code contains syntax errors. Hints:", "syntax_error_de": "Der Code enthält Syntaxfehler. Hinweise:", "python_error_en": "Your code contains errors. Hints:", "python_error_de": "Der Code enthält Fehler. Hinweise:", "semantic_errors_en": "Although the code can be compiled, it still contains errors in terms of content.", "semantic_errors_de": "Der Code lässt sich zwar kompilieren, enthält aber noch inhaltliche Fehler.", "semantic_errors_python_en": "Although the code can be executed, it still contains errors in terms of content.", "semantic_errors_python_de": "Der Code lässt sich zwar ausführen, enthält aber noch inhaltliche Fehler.", "look_on_datatypes_en": "Take a close look to see whether you have exactly adopted the prescribed data types and identifiers.", "look_on_datatypes_de": "Schauen Sie genau hin, ob Sie die vorgeschriebenen Datentypen und Bezeichner exakt übernommen haben.", "look_on_identifiers_en": "Take a close look to see whether you have exactly adopted the prescribed identifiers.", "look_on_identifiers_de": "Schauen Sie genau hin, ob Sie die vorgeschriebenen Bezeichner exakt übernommen haben.", "not_terminating_en": "Your program does not terminate!", "not_terminating_de": "Ihr Progamm terminiert nicht!" }; function runBashCommand(cmd, timeoutMilliseconds=100000000) { let res_status=0, res_stdout="", res_stderr=""; try { let options = { stdio: 'pipe', timeout: timeoutMilliseconds }; res_stdout = execSync(cmd, options).toString(); } catch(error) { res_status = error.status; res_stderr = error.stderr.toString(); res_stdout = error.stdout.toString(); } return [res_status, res_stderr, res_stdout]; } let inputJson = fs.readFileSync(inputPath); let input = JSON.parse(inputJson); function getText(desc) { return text[desc+"_"+input["language"]]; } const JAVA_TEMPLATE = `import java.util.*;public class Main { /*__METHODS__*/ public static boolean compare_arrays__double(double[] a, double[] b, double eps) { if(a.length != b.length) return false; for(int i=0; i<a.length; i++) { if(Math.abs(a[i]-b[i]) > eps) return false; } return true; } public static void main(String[] args) { /*__MAIN__*/ /*__ASSERTS__*/ }}`; const PYTHON_TEMPLATE = `#__MAIN__ import sys #__ASSERTS__ `; let cmd="", status="", stdout="", stderr="", java_src="", python_src=""; let output = {"status": "ok", "msg": ""}; // empty code? if(input["source"].trim().length == 0) { output["status"] = "error"; output["msg"] = getText("empty_program"); } // unallowed keywords? if(input["type"].startsWith("Python")) { let keyword = ""; for(let kw of PYTHON_UNALLOWED) { if(input["source"].includes(kw)) { keyword = kw; break; } } if(keyword.length > 0) { output["status"] = "error"; output["msg"] = getText("python_keyword_not_allowed").replace('$', keyword); } } // try to compile code block without asserts if(output["status"] === "ok") { switch(input["type"]) { case "JavaBlock": java_src = JAVA_TEMPLATE.replace("/*__MAIN__*/", input["source"] + "\n"); break; case "JavaMethod": java_src = JAVA_TEMPLATE.replace("/*__METHODS__*/", input["source"] + "\n" + input["hiddenMethods"].join("\n") + "\n"); break; case "Python": python_src = PYTHON_TEMPLATE.replace("#__MAIN__", input["source"] + "\n"); break; default: output["status"] = "devError"; output["msg"] = "unknown input type '" + input["type"] + "'!"; console.log(JSON.stringify(output, null, 4)); process.exit(-1); } //console.log(inputDirectory + "/Main.java"); //console.log(java_src); if(input["type"].startsWith("Java")) { fs.writeFileSync(inputDirectory + "/Main.java", java_src); cmd = JAVA_COMPILER_PATH + " " + inputDirectory + "/Main.java"; [status, stderr, stdout] = runBashCommand(cmd); if(status != 0) { output["status"] = "error"; output.msg += getText("syntax_error") + "\n"; output.msg += "----------------------------------------\n"; let errLines = stderr.replaceAll(inputDirectory+"/","").split("\n"); // adjust line numbers for(let i=0; i<errLines.length; i++) { if(errLines[i].startsWith("Main.java:")) { let lineNo = parseInt(errLines[i].substring(10)); if(input["type"].startsWith("JavaMethod")) lineNo -= 1; // refer to JAVA_TEMPLATE else lineNo -= 11; // refer to JAVA_TEMPLATE let j = 10; for(; j<errLines[i].length; j++) { if(errLines[i][j] == ':') break; } errLines[i] = "Line " + lineNo + errLines[i].substring(j); } } output.msg += errLines.join('\n'); output.msg += "----------------------------------------\n"; } } else if(input["type"].startsWith("Python")) { fs.writeFileSync(inputDirectory + "/main.py", python_src); cmd = PYTHON_PATH + " " + inputDirectory + "/main.py"; [status, stderr, stdout] = runBashCommand(cmd, MAX_RUNTIME_SECONDS*1000); if(status != 0) { output["status"] = "error"; if(status == 143) output.msg += getText("not_terminating"); else { output.msg += getText("python_error") + "\n"; output.msg += "----------------------------------------\n"; let errLines = stderr.replaceAll(inputDirectory+"/","").split("\n"); for(let i=0; i<errLines.length; i++) { if(errLines[i].includes("File \"main.py\", line")) { let tokens = errLines[i].split(" "); let lineNo = parseInt(tokens[tokens.length-1]) /*- 1*/; errLines[i] = " File \"main.py\", line " + lineNo + "\n"; } } output.msg += errLines.join('\n'); output.msg += "----------------------------------------\n"; } } } } // [Java] try to compile code block with asserts and run code if(input["type"].startsWith("Java")) { // try to compile code block with asserts if(output["status"] === "ok") { let asserts = ''; for(let a of input["asserts"]) asserts += "if(" + a + ") {} else System.exit(-1);\n"; java_src = java_src.replace("/*__ASSERTS__*/", asserts); //console.log(java_src); fs.writeFileSync(inputDirectory + "/Main.java", java_src); cmd = JAVA_COMPILER_PATH + " " + inputDirectory + "/Main.java"; [status, stderr, stdout] = runBashCommand(cmd); fs.writeFileSync(inputDirectory + "/log-compile-code.txt", stderr); if(status != 0) { //console.log(stderr) output["status"] = "error"; output.msg += getText("semantic_errors") + " " + getText("look_on_datatypes"); } } // run code (max 10 seconds!) if(output["status"] === "ok") { cmd = "cd " + inputDirectory + " && " + JAVA_PATH + " Main"; [status, stderr, stdout] = runBashCommand(cmd, MAX_RUNTIME_SECONDS*1000); fs.writeFileSync(inputDirectory + "/log-run-code.txt", stderr); if(status != 0) { //console.log(status); output["status"] = "error"; if(status == 143) output.msg += getText("not_terminating"); else output.msg += getText("semantic_errors"); } } } // [Python] run code block with asserts if(input["type"].startsWith("Python")) { if(output["status"] === "ok") { let asserts = ''; for(let a of input["asserts"]) asserts += "if not(" + a + "):\n\tsys.exit(-1)\n"; python_src = python_src.replace("#__ASSERTS__", asserts); //console.log(python_src); fs.writeFileSync(inputDirectory + "/main.py", python_src); cmd = PYTHON_PATH + " " + inputDirectory + "/main.py"; [status, stderr, stdout] = runBashCommand(cmd, MAX_RUNTIME_SECONDS*1000); fs.writeFileSync(inputDirectory + "/log-run-code-with-asserts.txt", stderr); if(status != 0) { //console.log(stderr) output["status"] = "error"; if(status == 143) output.msg += getText("not_terminating"); else output.msg += getText("semantic_errors_python") + " " + getText("look_on_identifiers"); } } } // print output console.log(JSON.stringify(output, null, 4));