grading
Version:
Grading of student submissions, in particular programming tests.
547 lines • 20.4 kB
JavaScript
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
;