feat(curriculum): control loop-protect (#51542)

pull/51524/head^2
Oliver Eyton-Williams 2023-09-19 17:51:43 +02:00 committed by GitHub
parent e6867fe7ba
commit e149b09087
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 62 additions and 23 deletions

View File

@ -78,6 +78,8 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
certification
challengeType
dashedName
disableLoopProtectTests
disableLoopProtectPreview
fields {
slug
blockHashSlug

View File

@ -312,6 +312,8 @@ export type ChallengeMeta = {
title?: string;
challengeType?: number;
helpCategory: string;
disableLoopProtectTests: boolean;
disableLoopProtectPreview: boolean;
};
export type PortfolioProjectData = {

View File

@ -159,12 +159,18 @@ const babelTransformer = loopProtectOptions => {
function getBabelOptions(
presets,
{ preview, protect } = { preview: false, protect: true }
{ preview, disableLoopProtectTests, disableLoopProtectPreview } = {
preview: false,
disableLoopProtectTests: false,
disableLoopProtectPreview: false
}
) {
// we always protect the preview, since it evaluates as the user types and
// they may briefly have infinite looping code accidentally
if (preview) return { ...presets, plugins: ['loopProtection'] };
if (protect) return { ...presets, plugins: ['testLoopProtection'] };
if (preview && !disableLoopProtectPreview)
return { ...presets, plugins: ['loopProtection'] };
if (!disableLoopProtectTests)
return { ...presets, plugins: ['testLoopProtection'] };
return presets;
}

View File

@ -30,7 +30,6 @@ import {
challengeHasPreview,
getTestRunner,
isJavaScriptChallenge,
isLoopProtected,
updatePreview,
updateProjectPreview
} from '../utils/build';
@ -107,10 +106,10 @@ function* executeChallengeSaga({ payload }) {
const challengeData = yield select(challengeDataSelector);
const challengeMeta = yield select(challengeMetaSelector);
const protect = isLoopProtected(challengeMeta);
const buildData = yield buildChallengeData(challengeData, {
preview: false,
protect,
disableLoopProtectTests: challengeMeta.disableLoopProtectTests,
disableLoopProtectPreview: challengeMeta.disableLoopProtectPreview,
usesTestRunner: true
});
const document = yield getContext('document');
@ -230,10 +229,10 @@ function* previewChallengeSaga({ flushLogs = true } = {}) {
if (canBuildChallenge(challengeData)) {
const challengeMeta = yield select(challengeMetaSelector);
const protect = isLoopProtected(challengeMeta);
const buildData = yield buildChallengeData(challengeData, {
preview: true,
protect
disableLoopProtectTests: challengeMeta.disableLoopProtectTests,
disableLoopProtectPreview: challengeMeta.disableLoopProtectPreview
});
// evaluate the user code in the preview frame or in the worker
if (challengeHasPreview(challengeData)) {

View File

@ -43,8 +43,9 @@ interface BuildChallengeData extends Context {
interface BuildOptions {
preview: boolean;
protect: boolean;
usesTestRunner: boolean;
disableLoopProtectTests: boolean;
disableLoopProtectPreview: boolean;
usesTestRunner?: boolean;
}
const { filename: testEvaluator } = testEvaluatorData;
@ -211,14 +212,15 @@ type BuildResult = {
// out of it.
export function buildDOMChallenge(
{ challengeFiles, required = [], template = '' }: BuildChallengeData,
{ usesTestRunner } = { usesTestRunner: false }
options?: BuildOptions
): Promise<BuildResult> | undefined {
const loadEnzyme = challengeFiles?.some(
challengeFile => challengeFile.ext === 'jsx'
);
const pipeLine = composeFunctions(...getTransformers());
const pipeLine = composeFunctions(...getTransformers(options));
const finalFiles = challengeFiles?.map(pipeLine);
const usesTestRunner = options?.usesTestRunner ?? false;
if (finalFiles) {
return Promise.all(finalFiles)
@ -385,7 +387,3 @@ export function isJavaScriptChallenge({
challengeType === challengeTypes.jsProject
);
}
export function isLoopProtected(challengeMeta: ChallengeMeta): boolean {
return challengeMeta.superBlock !== 'coding-interview-prep';
}

View File

@ -93,6 +93,8 @@ exports.createChallengePages = function (createPage) {
return function ({ node: { challenge } }, index, allChallengeEdges) {
const {
dashedName,
disableLoopProtectTests,
disableLoopProtectPreview,
certification,
superBlock,
block,
@ -113,6 +115,8 @@ exports.createChallengePages = function (createPage) {
blockHashSlug,
dashedName,
certification,
disableLoopProtectTests,
disableLoopProtectPreview,
superBlock,
block,
isFirstStep: getIsFirstStepInBlock(index, allChallengeEdges),

View File

@ -8,6 +8,7 @@
"template": "",
"required": [],
"superBlock": "project-euler",
"disableLoopProtectTests": true,
"challengeOrder": [
{
"id": "5900f36e1000cf542c50fe80",
@ -410,4 +411,4 @@
"title": "Problem 100: Arranged probability"
}
]
}
}

View File

@ -8,6 +8,7 @@
"template": "",
"required": [],
"superBlock": "project-euler",
"disableLoopProtectTests": true,
"challengeOrder": [
{
"id": "5900f3d21000cf542c50fee4",
@ -410,4 +411,4 @@
"title": "Problem 200: Find the 200th prime-proof sqube containing the contiguous sub-string \"200\""
}
]
}
}

View File

@ -8,6 +8,7 @@
"template": "",
"required": [],
"superBlock": "project-euler",
"disableLoopProtectTests": true,
"challengeOrder": [
{
"id": "5900f4361000cf542c50ff48",
@ -410,4 +411,4 @@
"title": "Problem 300: Protein folding"
}
]
}
}

View File

@ -8,6 +8,7 @@
"template": "",
"required": [],
"superBlock": "project-euler",
"disableLoopProtectTests": true,
"challengeOrder": [
{
"id": "5900f4991000cf542c50ffab",
@ -410,4 +411,4 @@
"title": "Problem 400: Fibonacci tree game"
}
]
}
}

View File

@ -8,6 +8,7 @@
"template": "",
"required": [],
"superBlock": "project-euler",
"disableLoopProtectTests": true,
"challengeOrder": [
{
"id": "5900f4fd1000cf542c51000f",
@ -330,4 +331,4 @@
"title": "Problem 480: The Last Question"
}
]
}
}

View File

@ -8,6 +8,7 @@
"template": "",
"required": [],
"superBlock": "coding-interview-prep",
"disableLoopProtectTests": true,
"challengeOrder": [
{
"id": "594810f028c0303b75339acb",
@ -650,4 +651,4 @@
"title": "Zig-zag matrix"
}
]
}
}

View File

@ -326,6 +326,8 @@ ${getFullPath('english', filePath)}
showUpcomingChanges: process.env.SHOW_UPCOMING_CHANGES === 'true'
});
challenge.usesMultifileEditor = !!meta.usesMultifileEditor;
challenge.disableLoopProtectTests = !!meta.disableLoopProtectTests;
challenge.disableLoopProtectPreview = !!meta.disableLoopProtectPreview;
}
function fixChallengeProperties(challenge) {

View File

@ -42,6 +42,8 @@ const schema = Joi.object()
then: Joi.string().allow(''),
otherwise: Joi.string().required()
}),
disableLoopProtectTests: Joi.boolean().required(),
disableLoopProtectPreview: Joi.boolean().required(),
challengeFiles: Joi.array().items(fileJoi),
guideUrl: Joi.string().uri({ scheme: 'https' }),
hasEditableBoundaries: Joi.boolean(),

View File

@ -566,3 +566,19 @@ pnpm run update-challenge-order
```
This will take you through an interactive process to select the order of the challenges.
## Troubleshooting
### Infinite Loop Detected
If you see the following error in the console while previewing a challenge:
```text
Potential infinite loop detected on line <number>...
```
This means that the loop-protect plugin has found a long-running loop or recursive function. If your challenge needs to do that (e.g. it contains an event loop that is supposed to run indefinitely), then you can prevent the plugin from being used in the preview. To do so, add `disableLoopProtectPreview: true` to the block's `meta.json` file.
If your tests are computationally intensive, then you may see this error when they run. If this happens then you can add `disableLoopProtectTests: true` to the block's `meta.json` file.
It's not typically necessary to have both set to true, so only set them as needed.

View File

@ -26,7 +26,9 @@ const blockSchema = Joi.object({}).keys({
id: Joi.string(),
title: Joi.string()
})
)
),
disableLoopProtectTests: Joi.boolean(),
disableLoopProtectPreview: Joi.boolean()
})
});