UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

547 lines 20.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.timestamp = exports.COLOR_RESET = exports.COLOR_END = exports.COLOR_START = exports.execShellAndRetrieveOutput = exports.execShell = exports.execShellExec = exports.absPath = exports.findDirsInPathToRoot = exports.findDirContainingFile = exports.findInFolder = exports.isEmptyDir = exports.readDir = exports.unzip = exports.zip = exports.createDir = exports.rmFile = exports.rmDir = exports.fileExists = exports.folderExists = exports.ensureFolderExists = exports.ensureFileExists = exports.fsUtilVerbose = exports.EXT_EXCEL = void 0; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const fs = __importStar(require("fs/promises")); const jszip_1 = __importDefault(require("jszip")); const path_1 = __importDefault(require("path")); const cliUtil_1 = require("./cli/cliUtil"); exports.EXT_EXCEL = '.xlsx'; let verbose = false; function fsUtilVerbose(flag) { verbose = flag; } exports.fsUtilVerbose = fsUtilVerbose; function verb(msg) { if (verbose) { console.log(msg); } } function throwFileNotFoundError(name, hint) { if (!name) { throw new Error(`File not found, no file name defined.${hint ? " " + hint : ""}`); } if (!name.startsWith(path_1.default.sep)) { throw new Error(`File '${name}' not found in ${process.cwd()}.${hint ? " " + hint : ""}`); } throw new Error(`File '${name}' not found.${hint ? " " + hint : ""}`); } async function ensureFileExists(name, hint) { if (!name) { throwFileNotFoundError(name, hint); } let stat; try { stat = await fs.stat(name); } catch (err) { throwFileNotFoundError(name, hint); } if (!stat.isFile()) { throw Error(`'${name}' is not a file.${hint ? " " + hint : ""}`); } verb(`${hint ? hint + " " : ""}'${name}' exists and is a file.`); } exports.ensureFileExists = ensureFileExists; async function ensureFolderExists(name, hint) { if (!name) { throwFileNotFoundError(name, hint); } let stat; try { stat = await fs.stat(name); } catch (err) { throwFileNotFoundError(name, hint); } if (!stat.isDirectory()) { throw Error(`'${name}' is not a folder.${hint ? " " + hint : ""}`); } verb(`${hint ? hint + " " : ""}'${name}' exists and is a folder.`); } exports.ensureFolderExists = ensureFolderExists; async function folderExists(name, hint) { if (!name) { throw Error(`Cannot check folder existence, no name provided.${hint ? " Check option '" + hint + "'." : ""}`); } try { const stat = await fs.stat(name); return stat.isDirectory(); } catch (err) { return false; } } exports.folderExists = folderExists; async function fileExists(name, hint) { if (!name) { throw Error(`Cannot check folder existence, no name provided.${hint ? " Check option '" + hint + "'." : ""}`); } try { const stat = await fs.stat(name, {}); return true; } catch (err) { return false; } } exports.fileExists = fileExists; async function rmDir(name) { try { if (await folderExists(name)) { verb(`Try to remove folder '${name}'.`); await fs.rm(name, { recursive: true, maxRetries: 0, force: true }); verb(`Removed folder '${name}'.`); } else { verb(`Folder '${name}' not removed since it does not exist.`); } } catch (err) { throw Error(`Error deleting folder '${name}': ` + err); } } exports.rmDir = rmDir; async function rmFile(name) { try { if (await fileExists(name)) { verb(`Remove file '${name}'.`); await fs.rm(name); } else { verb(`File '${name}' not removed since it does not exist.`); } } catch (err) { throw Error(`Error deleting file '${name}': ` + err); } } exports.rmFile = rmFile; async function createDir(name, recursive = true) { if (!await folderExists(name)) { verb(`Create folder '${name}'.`); doCreateDir(name, recursive); } } exports.createDir = createDir; async function doCreateDir(name, recursive = true) { try { await fs.mkdir(name, { recursive: recursive }); } catch (err) { throw Error(`Error creating folder '${name}': ` + err); } } async function zip(srcFileOrFolder, zipFile) { try { const zip = new jszip_1.default(); const stat = await fs.stat(srcFileOrFolder); if (stat.isFile()) { await addFile(zip, srcFileOrFolder); } else { await addFolder(zip, srcFileOrFolder); } return new Promise((resolve, reject) => { zip.generateNodeStream({ type: 'nodebuffer', streamFiles: true }) .pipe((0, fs_1.createWriteStream)(zipFile)) .on('finish', resolve // JSZip generates a readable stream with a "end" event, // but is piped here in a writable stream which emits a "finish" event. ) .on('error', reject); }); } catch (err) { throw Error(`Error creating '${zipFile}': ` + err); } } exports.zip = zip; async function addFile(zip, fileName) { const data = await fs.readFile(fileName); zip.file(path_1.default.basename(fileName), data); } async function addFolder(zip, folderName) { const baseName = path_1.default.basename(folderName); const folderZip = zip.folder(baseName); if (folderZip == null) { throw new Error(`Error creating folder '${baseName}' in Zip.`); } for (const child of await readDir(folderName, 'any', false)) { const fullChildName = path_1.default.join(folderName, child); const stat = await fs.stat(fullChildName); if (stat.isFile()) { await addFile(folderZip, fullChildName); } else { await addFolder(folderZip, fullChildName); } } } async function unzip(zipFile, targetDir) { try { verb(`Unzipping '${zipFile}' to '${targetDir}'.`); const zip = await jszip_1.default.loadAsync(await fs.readFile(zipFile)); const items = []; zip.forEach((relativePath, file) => items.push({ relativePath: relativePath, file: file })); for (const item of items) { const fullFileName = path_1.default.join(targetDir, item.relativePath); if (item.file.dir) { verb(`Extracting folder '${fullFileName}'.`); doCreateDir(fullFileName); } else { const fullDirName = path_1.default.dirname(fullFileName); verb(`Extracting file '${fullFileName}'.`); await doCreateDir(fullDirName); fs.writeFile(fullFileName, Buffer.from(await item.file.async('arraybuffer'))); } } } catch (err) { throw Error(`Error extracting '${zipFile}': ` + err); } } exports.unzip = unzip; async function listZip(zipFile, targetDir) { try { const zip = new jszip_1.default(); const content = await zip.loadAsync(await fs.readFile(zipFile)); for (const key of Object.keys(content.files)) { const item = content.files[key]; console.log(item.name); } } catch (err) { throw Error(`Error extracting '${zipFile}': ` + err); } } const IGNORED_FILES = ['__MACOS', '.DS_Store']; /** * * __MACOS is ignored */ async function readDir(dir, kind, recursive = false, hint) { await ensureFolderExists(dir, hint); const normalizedDir = path_1.default.join(dir); // remove "./", would be removed later on anyway causing trouble const subDirs = [normalizedDir]; const entries = []; while (subDirs.length > 0) { const subDir = subDirs.pop(); // we just checked the length; const dirEntries = await fs.readdir(subDir, { withFileTypes: true }); dirEntries.forEach(dirEntry => { if (IGNORED_FILES.indexOf(dirEntry.name) < 0) { const fullName = path_1.default.join(subDir, dirEntry.name); const relName = fullName.substring(normalizedDir.length + 1); switch (kind) { case 'dir': if (dirEntry.isDirectory()) entries.push(relName); break; case 'file': if (dirEntry.isFile()) entries.push(relName); break; case 'any': entries.push(relName); break; } if (recursive && dirEntry.isDirectory()) { subDirs.push(path_1.default.join(subDir, dirEntry.name)); } } }); } return entries; } exports.readDir = readDir; async function isEmptyDir(dir) { try { const files = await fs.readdir(dir); return files.length === 0; } catch (err) { throw Error(`Error checking if folder '${dir}' is empty: ` + err); } } exports.isEmptyDir = isEmptyDir; async function findInFolder(dir, extension, hint) { await ensureFolderExists(dir, hint); const normalizedDir = path_1.default.join(dir); // remove "./", would be removed later on anyway causing trouble const dirEntries = await fs.readdir(dir, { withFileTypes: true }); const res = []; dirEntries.forEach(dirEntry => { if (dirEntry.isFile() && dirEntry.name.endsWith(extension)) { const fullName = path_1.default.join(dir, dirEntry.name); res.push(path_1.default.join(normalizedDir, dirEntry.name)); } }); return res; } exports.findInFolder = findInFolder; async function findDirContainingFile(startDir, containingFile, ignoredDirs) { if (!containingFile || containingFile.length == 0) { throw new Error(`No project file provided, cannot find project in '${startDir}`); } const normalizedIgnoredDirs = ignoredDirs.map(d => path_1.default.normalize(d)); await ensureFolderExists(startDir); const subDirs = [startDir]; for (const subDir of subDirs) { const dirEntries = await fs.readdir(subDir, { withFileTypes: true }); for (const dirEntry of dirEntries) { if (IGNORED_FILES.indexOf(dirEntry.name) < 0) { if (dirEntry.name == containingFile) { return subDir; } if (dirEntry.isDirectory() && !dirEntry.name.startsWith('.') && !normalizedIgnoredDirs.includes(dirEntry.name) && !normalizedIgnoredDirs.includes(path_1.default.join(subDir, dirEntry.name))) { subDirs.push(path_1.default.join(subDir, dirEntry.name)); } } } } throw new Error(`Did not find "${containingFile}" in any sub-folder of "${startDir}".`); } exports.findDirContainingFile = findDirContainingFile; async function findDirsInPathToRoot(startDir, dirToFind) { if (!dirToFind || dirToFind.length == 0) { throw new Error(`No dir to file provided, cannot find it '${startDir}`); } const foundDirs = []; await ensureFolderExists(startDir); startDir = path_1.default.resolve(startDir); let currentDir = startDir; do { const dirEntries = await fs.readdir(startDir, { withFileTypes: true }); for (const dirEntry of dirEntries) { if (dirEntry.name == dirToFind && dirEntry.isDirectory()) { foundDirs.unshift(path_1.default.join(startDir, dirToFind)); break; } } currentDir = startDir; startDir = path_1.default.dirname(startDir); } while (startDir != currentDir); return foundDirs; } exports.findDirsInPathToRoot = findDirsInPathToRoot; function absPath(file) { let resolved = path_1.default.resolve(file); if (!resolved) { resolved = path_1.default.resolve(path_1.default.join('.', file)); } if (!resolved) { throw new Error(`Cannot resolve absolute path of ${file} in ${process.cwd}.`); } return resolved; } exports.absPath = absPath; async function execShellExec(cmd) { await new Promise((resolve, reject) => { (0, child_process_1.exec)(cmd, (error, stdout, stderr) => { if (error) { reject(error); } resolve(stdout ? stdout : stderr); }); }); } exports.execShellExec = execShellExec; /** * Executes a command via `process.spawn` and captures output while running. * The output may be formatted using the options: * - `indent`: if true, the output is indented * - `prefix`: if set, the prefix is added to every line */ async function execShell(cmd, args, colorOptions, options = { indent: true, prefix: '> ', cwd: undefined }) { return new Promise((resolve, reject) => { let spawnOptions = undefined; if (options.cwd && options.cwd.length > 0) { spawnOptions = { cwd: options.cwd }; } if (options.env) { if (!spawnOptions) { spawnOptions = { env: options.env }; } else { spawnOptions = { ...spawnOptions, env: options.env }; } } // Omit empty lines const colorCode = { code: undefined }; const consoleLog = (data) => { const str = formatShellOutput(data, colorOptions, options, colorCode, false); if (str != null) console.log(str); }; const consoleErr = (data) => { const str = formatShellOutput(data, colorOptions, options, colorCode, true); if (str != null) console.log(str); }; (0, cliUtil_1.debug)(`Exec: ${options.cwd ? options.cwd : process.cwd()}> ${cmd} ${args.join(' ')}`); const spawnedProcess = (0, child_process_1.spawn)(cmd, args, spawnOptions); spawnedProcess.stdout.on('data', consoleLog); spawnedProcess.stderr.on('data', consoleErr); spawnedProcess.on('exit', function (code) { // *** Process completed resolve(code); }); spawnedProcess.on('error', function (err) { // *** Process creation failed reject(err); }); }); } exports.execShell = execShell; /** * Executes a command via `process.spawn` and captures output while running. * The output may be formatted using the options: * - `indent`: if true, the output is indented * - `prefix`: if set, the prefix is added to every line */ async function execShellAndRetrieveOutput(cmd, args, options) { const output = []; const shellPromise = new Promise((resolve, reject) => { let spawnOptions = undefined; if (options.cwd && options.cwd.length > 0) { spawnOptions = { cwd: options.cwd }; } const consoleLog = (data) => { const str = formatShellOutput(data, { colored: false }, { indent: false, prefix: '' }, {}, false); if (!str) { return; } const lines = str.split(/\r?\n/); (0, cliUtil_1.debug)(lines.map(l => cmd + "> " + l).join('\n')); output.push(...lines); }; const consoleErr = (data) => { const str = formatShellOutput(data, { colored: false }, { indent: false, prefix: cmd }, {}, true); if (str != null) console.log(str); }; (0, cliUtil_1.debug)(`Exec: ${options.cwd ? options.cwd : process.cwd()}> ${cmd} ${args.join(' ')}`); const spawnedProcess = (0, child_process_1.spawn)(cmd, args, spawnOptions); spawnedProcess.stdout.on('data', consoleLog); spawnedProcess.stderr.on('data', consoleErr); spawnedProcess.on('exit', function (code) { // *** Process completed resolve(code); }); spawnedProcess.on('error', function (err) { // *** Process creation failed reject(err); }); }); await shellPromise; return output; } exports.execShellAndRetrieveOutput = execShellAndRetrieveOutput; exports.COLOR_START = "\x1b["; exports.COLOR_END = "m"; exports.COLOR_RESET = exports.COLOR_START + "0" + exports.COLOR_END; function formatShellOutput(data, colorOptions, options, colorCode, isError) { let str = data instanceof Buffer ? data.toString() : String(data); str = str.trimEnd(); const match = str.match(/<<color:\s*([0-9;]+)>>/); if (match) { colorCode.code = match[1]; return null; } if (str.startsWith('<<color:reset>>')) { colorCode.code = undefined; return null; } const lines = str.split(/\r?\n/); const coloredLines = []; let omitEmptyLines = false; if (lines.length > 0) { if (lines[0].startsWith(" ●")) { omitEmptyLines = true; } } for (const line of lines) { const trimmedLine = line.trim(); if (omitEmptyLines && trimmedLine.length == 0) { continue; } if (trimmedLine === "console.log") { continue; } let colorPrefix = ''; let colorPostFix = ''; if (colorOptions.colored) { let colorCodeSpecial = colorCode.code ? colorCode.code + ";" : ''; colorPostFix = exports.COLOR_RESET; if (line.match(/^\d+\./)) { colorPrefix = `${exports.COLOR_START}${colorOptions.colorSubsection}${exports.COLOR_END}`; colorCode.code = undefined; } else if (line.match(/Processing submission \d+/)) { colorPrefix = `${exports.COLOR_START}${colorOptions.colorSection}${exports.COLOR_END}`; colorCode.code = undefined; } else if (isError && colorOptions.colorError) { colorPrefix = `${exports.COLOR_START}${colorCodeSpecial}${colorOptions.colorError}${exports.COLOR_END}`; } else if (colorOptions.colorStandard) { colorPrefix = `${exports.COLOR_START}${colorCodeSpecial}${colorOptions.colorStandard}${exports.COLOR_END}`; } else if (colorCode.code) { colorPrefix = `${exports.COLOR_START}${colorCode.code}${exports.COLOR_END}`; } else { colorPostFix = ''; } } let coloredLine = line; if (options.prefix) { coloredLine = coloredLine.replace(/^/gm, options.prefix); } if (options.indent) { coloredLine = coloredLine.replace(/^/gm, ' '); } coloredLine = colorPrefix + coloredLine + colorPostFix; coloredLines.push(coloredLine); } return coloredLines.join('\n'); } function timestamp() { return new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); } exports.timestamp = timestamp; //# sourceMappingURL=fsUtil.js.map