Files
freeCodeCamp/tools/client-plugins/browser-scripts/test-evaluator.ts
Oliver Eyton-Williams 69d6ee32bf feat: python in the browser (#50913)
Co-authored-by: Beau Carnes <1513130+beaucarnes@users.noreply.github.com>
2023-07-28 07:36:25 +02:00

185 lines
5.5 KiB
TypeScript

import chai from 'chai';
import { toString as __toString } from 'lodash-es';
import * as helpers from '@freecodecamp/curriculum-helpers';
import { format as __format } from './utils/format';
const ctx: Worker & typeof globalThis = self as unknown as Worker &
typeof globalThis;
const __utils = (() => {
const MAX_LOGS_SIZE = 64 * 1024;
let logs: string[] = [];
function flushLogs() {
if (logs.length) {
ctx.postMessage({
type: 'LOG',
data: logs.join('\n')
});
logs = [];
}
}
function pushLogs(logs: string[], args: string[]) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
logs.push(args.map(arg => __format(arg)).join(' '));
if (logs.join('\n').length > MAX_LOGS_SIZE) {
flushLogs();
}
}
const oldLog = ctx.console.log.bind(ctx.console);
function proxyLog(...args: string[]) {
pushLogs(logs, args);
return oldLog(...args);
}
const oldInfo = ctx.console.info.bind(ctx.console);
function proxyInfo(...args: string[]) {
pushLogs(logs, args);
return oldInfo(...args);
}
const oldWarn = ctx.console.warn.bind(ctx.console);
function proxyWarn(...args: string[]) {
pushLogs(logs, args);
return oldWarn(...args);
}
const oldError = ctx.console.error.bind(ctx.console);
function proxyError(...args: string[]) {
pushLogs(logs, args);
return oldError(...args);
}
function log(...msgs: Error[]) {
if (msgs && msgs[0] && !(msgs[0] instanceof chai.AssertionError)) {
// discards the stack trace via toString as it only useful to debug the
// site, not a specific challenge.
console.log(...msgs.map(msg => msg.toString()));
}
}
const toggleProxyLogger = (on: unknown) => {
ctx.console.log = on ? proxyLog : oldLog;
ctx.console.info = on ? proxyInfo : oldInfo;
ctx.console.warn = on ? proxyWarn : oldWarn;
ctx.console.error = on ? proxyError : oldError;
};
return {
log,
toggleProxyLogger,
flushLogs
};
})();
// We freeze these two to prevent learners from getting the tester into a weird
// state.
Object.freeze(self);
Object.freeze(__utils);
interface TestEvaluatorEvent extends MessageEvent {
data: {
code: {
contents: string;
editableContents: string;
original: { [id: string]: string };
};
removeComments: boolean;
firstTest: unknown;
testString: string;
build: string;
sources: {
[fileName: string]: unknown;
};
};
}
/* Run the test if there is one. If not just evaluate the user code */
ctx.onmessage = async (e: TestEvaluatorEvent) => {
/* eslint-disable @typescript-eslint/no-unused-vars */
let code = (e.data?.code?.contents || '').slice();
code = e.data?.removeComments ? helpers.removeJSComments(code) : code;
let editableContents = (e.data?.code?.editableContents || '').slice();
editableContents = e.data?.removeComments
? helpers.removeJSComments(editableContents)
: editableContents;
const assert = chai.assert;
const __helpers = helpers;
// Similarly to self and __utils, if the learner tries to modify these, weird
// behavior may result. Freezing them means they get an error instead.
Object.freeze(assert);
Object.freeze(__helpers);
// Fake Deep Equal dependency
const DeepEqual = (a: unknown, b: unknown) =>
JSON.stringify(a) === JSON.stringify(b);
// Build errors should be reported, but only once:
__utils.toggleProxyLogger(e.data.firstTest);
/* eslint-enable @typescript-eslint/no-unused-vars */
try {
let testResult;
// This can be reassigned by the eval inside the try block, so it should be declared as a let
// eslint-disable-next-line prefer-const
let __userCodeWasExecuted = false;
/* eslint-disable no-eval */
try {
// Logging is proxyed after the build to catch console.log messages
// generated during testing.
testResult = (await eval(`${
e.data?.removeComments
? helpers.removeJSComments(e.data.build)
: e.data.build
}
__utils.flushLogs();
__userCodeWasExecuted = true;
__utils.toggleProxyLogger(true);
${e.data.testString}`)) as unknown;
} catch (err) {
if (__userCodeWasExecuted) {
// rethrow error, since test failed.
throw err;
}
// log build errors unless they're related to import/export/require (there
// are challenges that use them and they should not trigger warnings)
if (
(err as Error).name !== 'ReferenceError' ||
((err as Error).message !== 'require is not defined' &&
(err as Error).message !== 'exports is not defined')
) {
__utils.log(err as Error);
}
// the tests may not require working code, so they are evaluated even if
// the user code does not get executed.
testResult = eval(e.data.testString) as unknown;
}
/* eslint-enable no-eval */
if (typeof testResult === 'function') {
await testResult((fileName: string) =>
__toString(e.data.sources[fileName])
);
}
__utils.flushLogs();
ctx.postMessage({ pass: true });
} catch (err) {
// Errors from testing go to the browser console only.
__utils.toggleProxyLogger(false);
// Report execution errors in case user code has errors that are only
// uncovered during testing.
__utils.log(err as Error);
// Now that all logs have been created we can flush them.
__utils.flushLogs();
ctx.postMessage({
err: {
message: (err as Error).message,
stack: (err as Error).stack
}
});
}
};
ctx.postMessage({ type: 'contentLoaded' });