fix: remove JS comments revisited (#41952)

* fix: restrict removeJSComments to test-evaluator

To prevent @babel from being included everywhere, this only calls
removeJSComments inside the test-evaluator

* test: add removeJSComments test
This commit is contained in:
Oliver Eyton-Williams
2021-04-30 21:30:06 +02:00
committed by GitHub
parent 341fe76f0f
commit 2eac236e39
6 changed files with 49 additions and 47 deletions

View File

@ -2,7 +2,9 @@ import chai from 'chai';
import '@babel/polyfill'; import '@babel/polyfill';
import { toString as __toString } from 'lodash-es'; import { toString as __toString } from 'lodash-es';
import { format as __format } from '../../utils/format'; import { format as __format } from '../../utils/format';
import curriculumHelpers from '../../utils/curriculum-helpers'; import curriculumHelpers, {
removeJSComments
} from '../../utils/curriculum-helpers';
const __utils = (() => { const __utils = (() => {
const MAX_LOGS_SIZE = 64 * 1024; const MAX_LOGS_SIZE = 64 * 1024;
@ -56,8 +58,12 @@ const __utils = (() => {
/* Run the test if there is one. If not just evaluate the user code */ /* Run the test if there is one. If not just evaluate the user code */
self.onmessage = async e => { self.onmessage = async e => {
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
const code = (e.data?.code?.contents || '').slice(); let code = (e.data?.code?.contents || '').slice();
const editableContents = (e.data?.code?.editableContents || '').slice(); code = e.data?.removeComments ? removeJSComments(code) : code;
let editableContents = (e.data?.code?.editableContents || '').slice();
editableContents = e.data?.removeComments
? removeJSComments(editableContents)
: editableContents;
const assert = chai.assert; const assert = chai.assert;
const __helpers = curriculumHelpers; const __helpers = curriculumHelpers;
@ -74,7 +80,9 @@ self.onmessage = async e => {
try { try {
// Logging is proxyed after the build to catch console.log messages // Logging is proxyed after the build to catch console.log messages
// generated during testing. // generated during testing.
testResult = eval(`${e.data.build} testResult = eval(`${
e.data?.removeComments ? removeJSComments(e.data.build) : e.data.build
}
__utils.flushLogs(); __utils.flushLogs();
__userCodeWasExecuted = true; __userCodeWasExecuted = true;
__utils.toggleProxyLogger(true); __utils.toggleProxyLogger(true);

View File

@ -172,7 +172,7 @@ class ShowClassic extends Component {
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
removeComments, removeComments: removeComments !== false,
challengeType, challengeType,
helpCategory helpCategory
}); });

View File

@ -87,14 +87,13 @@ export function* executeChallengeSaga({
const protect = isLoopProtected(challengeMeta); const protect = isLoopProtected(challengeMeta);
const buildData = yield buildChallengeData(challengeData, { const buildData = yield buildChallengeData(challengeData, {
preview: false, preview: false,
removeComments: challengeMeta.removeComments,
protect protect
}); });
const document = yield getContext('document'); const document = yield getContext('document');
const testRunner = yield call( const testRunner = yield call(
getTestRunner, getTestRunner,
buildData, buildData,
{ proxyLogger }, { proxyLogger, removeComments: challengeMeta.removeComments },
document document
); );
const testResults = yield executeTests(testRunner, tests); const testResults = yield executeTests(testRunner, tests);
@ -202,7 +201,6 @@ function* previewChallengeSaga({ flushLogs = true } = {}) {
const protect = isLoopProtected(challengeMeta); const protect = isLoopProtected(challengeMeta);
const buildData = yield buildChallengeData(challengeData, { const buildData = yield buildChallengeData(challengeData, {
preview: true, preview: true,
removeComments: challengeMeta.removeComments,
protect protect
}); });
// evaluate the user code in the preview frame or in the worker // evaluate the user code in the preview frame or in the worker
@ -210,7 +208,10 @@ function* previewChallengeSaga({ flushLogs = true } = {}) {
const document = yield getContext('document'); const document = yield getContext('document');
yield call(updatePreview, buildData, document, proxyLogger); yield call(updatePreview, buildData, document, proxyLogger);
} else if (isJavaScriptChallenge(challengeData)) { } else if (isJavaScriptChallenge(challengeData)) {
const runUserCode = getTestRunner(buildData, { proxyLogger }); const runUserCode = getTestRunner(buildData, {
proxyLogger,
removeComments: challengeMeta.removeComments
});
// without a testString the testRunner just evaluates the user's code // without a testString the testRunner just evaluates the user's code
yield call(runUserCode, null, previewTimeout); yield call(runUserCode, null, previewTimeout);
} }

View File

@ -13,7 +13,6 @@ import {
import frameRunnerData from '../../../../../config/client/frame-runner.json'; import frameRunnerData from '../../../../../config/client/frame-runner.json';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import testEvaluatorData from '../../../../../config/client/test-evaluator.json'; import testEvaluatorData from '../../../../../config/client/test-evaluator.json';
import { removeJSComments } from '../../../utils/curriculum-helpers';
const { filename: runner } = frameRunnerData; const { filename: runner } = frameRunnerData;
const { filename: testEvaluator } = testEvaluatorData; const { filename: testEvaluator } = testEvaluatorData;
@ -107,16 +106,16 @@ const testRunners = {
[challengeTypes.backend]: getDOMTestRunner, [challengeTypes.backend]: getDOMTestRunner,
[challengeTypes.pythonProject]: getDOMTestRunner [challengeTypes.pythonProject]: getDOMTestRunner
}; };
export function getTestRunner(buildData, { proxyLogger }, document) { export function getTestRunner(buildData, runnerConfig, document) {
const { challengeType } = buildData; const { challengeType } = buildData;
const testRunner = testRunners[challengeType]; const testRunner = testRunners[challengeType];
if (testRunner) { if (testRunner) {
return testRunner(buildData, proxyLogger, document); return testRunner(buildData, runnerConfig, document);
} }
throw new Error(`Cannot get test runner for challenge type ${challengeType}`); throw new Error(`Cannot get test runner for challenge type ${challengeType}`);
} }
function getJSTestRunner({ build, sources }, proxyLogger) { function getJSTestRunner({ build, sources }, { proxyLogger, removeComments }) {
const code = { const code = {
contents: sources.index, contents: sources.index,
editableContents: sources.editableContents editableContents: sources.editableContents
@ -126,12 +125,15 @@ function getJSTestRunner({ build, sources }, proxyLogger) {
return (testString, testTimeout, firstTest = true) => { return (testString, testTimeout, firstTest = true) => {
return testWorker return testWorker
.execute({ build, testString, code, sources, firstTest }, testTimeout) .execute(
{ build, testString, code, sources, firstTest, removeComments },
testTimeout
)
.on('LOG', proxyLogger).done; .on('LOG', proxyLogger).done;
}; };
} }
async function getDOMTestRunner(buildData, proxyLogger, document) { async function getDOMTestRunner(buildData, { proxyLogger }, document) {
await new Promise(resolve => await new Promise(resolve =>
createTestFramer(document, resolve, proxyLogger)(buildData) createTestFramer(document, resolve, proxyLogger)(buildData)
); );
@ -165,27 +167,16 @@ export function buildJSChallenge({ files }, options) {
.map(pipeLine); .map(pipeLine);
return Promise.all(finalFiles) return Promise.all(finalFiles)
.then(checkFilesErrors) .then(checkFilesErrors)
.then(files => { .then(files => ({
let build = files challengeType: challengeTypes.js,
build: files
.reduce( .reduce(
(body, file) => [...body, file.head, file.contents, file.tail], (body, file) => [...body, file.head, file.contents, file.tail],
[] []
) )
.join('\n'); .join('\n'),
let sources = buildSourceMap(files); sources: buildSourceMap(files)
if (options?.removeComments !== false) { }));
build = removeJSComments(build);
sources = {
...sources,
index: removeJSComments(sources.index)
};
}
return {
challengeType: challengeTypes.js,
build,
sources
};
});
} }
export function buildBackendChallenge({ url }) { export function buildBackendChallenge({ url }) {

View File

@ -52,6 +52,11 @@ describe('removeJSComments', () => {
); );
}); });
it('leaves malformed JS unchanged', () => {
const actual = '/ unclosed regex';
expect(removeJSComments(actual)).toBe(actual);
});
it('does not remove a url found in JS code', () => { it('does not remove a url found in JS code', () => {
expect(removeJSComments(jsCodeWithUrl)).toBe(jsCodeWithUrlUnchanged); expect(removeJSComments(jsCodeWithUrl)).toBe(jsCodeWithUrlUnchanged);
}); });

View File

@ -519,7 +519,7 @@ async function createTestRunner(
buildChallenge, buildChallenge,
solutionFromNext solutionFromNext
) { ) {
const { required = [], template } = challenge; const { required = [], template, removeComments } = challenge;
// we should avoid modifying challenge, as it gets reused: // we should avoid modifying challenge, as it gets reused:
const files = cloneDeep(challenge.files); const files = cloneDeep(challenge.files);
@ -528,16 +528,11 @@ async function createTestRunner(
files[key].editableContents = solution[key].editableContents; files[key].editableContents = solution[key].editableContents;
}); });
const { build, sources, loadEnzyme } = await buildChallenge( const { build, sources, loadEnzyme } = await buildChallenge({
{
files, files,
required, required,
template template
}, });
{
removeComments: challenge.removeComments
}
);
const code = { const code = {
contents: sources.index, contents: sources.index,
@ -546,7 +541,7 @@ async function createTestRunner(
const evaluator = await (buildChallenge === buildDOMChallenge const evaluator = await (buildChallenge === buildDOMChallenge
? getContextEvaluator(build, sources, code, loadEnzyme) ? getContextEvaluator(build, sources, code, loadEnzyme)
: getWorkerEvaluator(build, sources, code)); : getWorkerEvaluator(build, sources, code, removeComments));
return async ({ text, testString }) => { return async ({ text, testString }) => {
try { try {
@ -587,12 +582,14 @@ async function getContextEvaluator(build, sources, code, loadEnzyme) {
}; };
} }
async function getWorkerEvaluator(build, sources, code) { async function getWorkerEvaluator(build, sources, code, removeComments) {
const testWorker = createWorker(testEvaluator, { terminateWorker: true }); const testWorker = createWorker(testEvaluator, { terminateWorker: true });
return { return {
evaluate: async (testString, timeout) => evaluate: async (testString, timeout) =>
await testWorker.execute({ testString, build, code, sources }, timeout) await testWorker.execute(
.done { testString, build, code, sources, removeComments },
timeout
).done
}; };
} }