From 98f979f3b47cd677a339141b8d841a49fe82760e Mon Sep 17 00:00:00 2001 From: Valeriy S Date: Fri, 8 Feb 2019 17:33:05 +0300 Subject: [PATCH] fix(client): disable build on error --- .../redux/execute-challenge-saga.js | 123 +++++------------- .../src/templates/Challenges/redux/index.js | 61 +++++++-- .../src/templates/Challenges/utils/build.js | 73 +++++++++-- 3 files changed, 153 insertions(+), 104 deletions(-) diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index 1992e18cc73..f7bb97558d2 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -10,56 +10,46 @@ import { import { delay, channel } from 'redux-saga'; import { - backendFormValuesSelector, - challengeFilesSelector, - challengeMetaSelector, + challengeDataSelector, challengeTestsSelector, initConsole, updateConsole, initLogs, updateLogs, logsToConsole, - updateTests + updateTests, + isBuildEnabledSelector, + disableBuildOnError } from './'; -import { - buildJSChallenge, - buildDOMChallenge, - buildBackendChallenge -} from '../utils/build'; +import { buildChallenge, getTestRunner } from '../utils/build'; import { challengeTypes } from '../../../../utils/challengeTypes'; -import createWorker from '../utils/worker-executor'; -import { - createMainFramer, - createTestFramer, - runTestInTestFrame -} from '../utils/frame.js'; +import { createMainFramer } from '../utils/frame.js'; export function* executeChallengeSaga() { + const isBuildEnabled = yield select(isBuildEnabledSelector); + if (!isBuildEnabled) { + return; + } + const consoleProxy = yield channel(); try { - const { js, bonfire, backend } = challengeTypes; - const { challengeType } = yield select(challengeMetaSelector); - yield put(initLogs()); yield put(initConsole('// running tests')); yield fork(logToConsole, consoleProxy); const proxyLogger = args => consoleProxy.put(args); - let testResults; - switch (challengeType) { - case js: - case bonfire: - testResults = yield executeJSChallengeSaga(proxyLogger); - break; - case backend: - testResults = yield executeBackendChallengeSaga(proxyLogger); - break; - default: - testResults = yield executeDOMChallengeSaga(proxyLogger); - } + const buildData = yield buildChallengeData(); + const document = yield getContext('document'); + const testRunner = yield call( + getTestRunner, + buildData, + proxyLogger, + document + ); + const testResults = yield executeTests(testRunner); yield put(updateTests(testResults)); yield put(updateConsole('// tests completed')); @@ -77,63 +67,16 @@ function* logToConsole(channel) { }); } -function* executeJSChallengeSaga(proxyLogger) { - const files = yield select(challengeFilesSelector); - const { build, sources } = yield call(buildJSChallenge, files); - const code = sources && 'index' in sources ? sources['index'] : ''; - - const testWorker = createWorker('test-evaluator'); - testWorker.on('LOG', proxyLogger); - +function* buildChallengeData() { + const challengeData = yield select(challengeDataSelector); try { - return yield call(executeTests, async(testString, testTimeout) => { - try { - return await testWorker.execute( - { build, testString, code, sources }, - testTimeout - ); - } finally { - testWorker.killWorker(); - } - }); - } finally { - testWorker.remove('LOG', proxyLogger); + return yield call(buildChallenge, challengeData); + } catch (e) { + yield put(disableBuildOnError(e)); + throw ['Build failed']; } } -function createTestFrame(document, ctx, proxyLogger) { - return new Promise(resolve => - createTestFramer(document, resolve, proxyLogger)(ctx) - ); -} - -function* executeDOMChallengeSaga(proxyLogger) { - const files = yield select(challengeFilesSelector); - const meta = yield select(challengeMetaSelector); - const document = yield getContext('document'); - const ctx = yield call(buildDOMChallenge, files, meta); - ctx.loadEnzyme = Object.keys(files).some(key => files[key].ext === 'jsx'); - yield call(createTestFrame, document, ctx, proxyLogger); - // wait for a code execution on a "ready" event in jQuery challenges - yield delay(100); - - return yield call(executeTests, (testString, testTimeout) => - runTestInTestFrame(document, testString, testTimeout) - ); -} - -// TODO: use a web worker -function* executeBackendChallengeSaga(proxyLogger) { - const formValues = yield select(backendFormValuesSelector); - const document = yield getContext('document'); - const ctx = yield call(buildBackendChallenge, formValues); - yield call(createTestFrame, document, ctx, proxyLogger); - - return yield call(executeTests, (testString, testTimeout) => - runTestInTestFrame(document, testString, testTimeout) - ); -} - function* executeTests(testRunner) { const tests = yield select(challengeTestsSelector); const testTimeout = 5000; @@ -168,16 +111,20 @@ function* executeTests(testRunner) { } function* updateMainSaga() { - yield delay(500); + const isBuildEnabled = yield select(isBuildEnabledSelector); + if (!isBuildEnabled) { + return; + } + + yield delay(700); try { + yield put(initConsole('')); const { html, modern } = challengeTypes; - const meta = yield select(challengeMetaSelector); - const { challengeType } = meta; + const { challengeType } = yield select(challengeDataSelector); if (challengeType !== html && challengeType !== modern) { return; } - const files = yield select(challengeFilesSelector); - const ctx = yield call(buildDOMChallenge, files, meta); + const ctx = yield buildChallengeData(); const document = yield getContext('document'); const frameMain = yield call(createMainFramer, document); yield call(frameMain, ctx); diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index 30ab22112a5..046a6ca7da2 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -14,6 +14,7 @@ import codeStorageEpic from './code-storage-epic'; import { createIdToNameMapSaga } from './id-to-name-map-saga'; import { createExecuteChallengeSaga } from './execute-challenge-saga'; import { createCurrentChallengeSaga } from './current-challenge-saga'; +import { challengeTypes } from '../../../../utils/challengeTypes'; export const ns = 'challenge'; export const backendNS = 'backendChallenge'; @@ -23,12 +24,14 @@ const initialState = { challengeIdToNameMap: {}, challengeMeta: { id: '', - nextChallengePath: '/' + nextChallengePath: '/', + introPath: '', + challengeType: -1 }, challengeTests: [], consoleOut: '', isCodeLocked: false, - isJSEnabled: true, + isBuildEnabled: true, modal: { completion: false, help: false, @@ -59,7 +62,7 @@ export const types = createTypes( 'lockCode', 'unlockCode', - 'disableJSOnError', + 'disableBuildOnError', 'storedCodeFound', 'noStoredCodeFound', @@ -136,7 +139,7 @@ export const logsToConsole = createAction(types.logsToConsole); export const lockCode = createAction(types.lockCode); export const unlockCode = createAction(types.unlockCode); -export const disableJSOnError = createAction(types.disableJSOnError); +export const disableBuildOnError = createAction(types.disableBuildOnError); export const storedCodeFound = createAction(types.storedCodeFound); export const noStoredCodeFound = createAction(types.noStoredCodeFound); @@ -165,13 +168,55 @@ export const isCompletionModalOpenSelector = state => export const isHelpModalOpenSelector = state => state[ns].modal.help; export const isVideoModalOpenSelector = state => state[ns].modal.video; export const isResetModalOpenSelector = state => state[ns].modal.reset; -export const isJSEnabledSelector = state => state[ns].isJSEnabled; +export const isBuildEnabledSelector = state => state[ns].isBuildEnabled; export const successMessageSelector = state => state[ns].successMessage; export const backendFormValuesSelector = state => state.form[backendNS]; export const projectFormValuesSelector = state => state[ns].projectFormValues || {}; +export const challengeDataSelector = state => { + const { challengeType } = challengeMetaSelector(state); + let challengeData = { challengeType }; + if ( + challengeType === challengeTypes.js || + challengeType === challengeTypes.bonfire + ) { + challengeData = { + ...challengeData, + files: challengeFilesSelector(state) + }; + } else if (challengeType === challengeTypes.backend) { + const { + solution: { value: url } + } = backendFormValuesSelector(state); + challengeData = { + ...challengeData, + url + }; + } else if ( + challengeType === challengeTypes.frontEndProject || + challengeType === challengeTypes.backendEndProject + ) { + challengeData = { + ...challengeData, + ...projectFormValuesSelector(state) + }; + } else if ( + challengeType === challengeTypes.html || + challengeType === challengeTypes.modern + ) { + const { required = [], template = '' } = challengeMetaSelector(state); + challengeData = { + ...challengeData, + files: challengeFilesSelector(state), + required, + template + }; + } + return challengeData; +}; + export const reducer = handleActions( { [types.fetchIdToNameMapComplete]: (state, { payload }) => ({ @@ -269,13 +314,13 @@ export const reducer = handleActions( }), [types.unlockCode]: state => ({ ...state, - isJSEnabled: true, + isBuildEnabled: true, isCodeLocked: false }), - [types.disableJSOnError]: (state, { payload }) => ({ + [types.disableBuildOnError]: (state, { payload }) => ({ ...state, consoleOut: state.consoleOut + ' \n' + payload, - isJSEnabled: false + isBuildEnabled: false }), [types.updateSuccessMessage]: (state, { payload }) => ({ diff --git a/client/src/templates/Challenges/utils/build.js b/client/src/templates/Challenges/utils/build.js index b74cf7aef4f..7faead1bad9 100644 --- a/client/src/templates/Challenges/utils/build.js +++ b/client/src/templates/Challenges/utils/build.js @@ -1,5 +1,8 @@ import { transformers } from '../rechallenge/transformers'; import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js'; +import { challengeTypes } from '../../../../utils/challengeTypes'; +import createWorker from './worker-executor'; +import { createTestFramer, runTestInTestFrame } from './frame'; const frameRunner = [ { @@ -49,9 +52,62 @@ function checkFilesErrors(files) { return files; } -export function buildDOMChallenge(files, meta = {}) { - const { required = [], template = '' } = meta; +const buildFunctions = { + [challengeTypes.js]: buildJSChallenge, + [challengeTypes.bonfire]: buildJSChallenge, + [challengeTypes.html]: buildDOMChallenge, + [challengeTypes.modern]: buildDOMChallenge, + [challengeTypes.backend]: buildBackendChallenge +}; + +export async function buildChallenge(challengeData) { + const { challengeType } = challengeData; + let build = buildFunctions[challengeType]; + if (build) { + return build(challengeData); + } + return null; +} + +const testRunners = { + [challengeTypes.js]: getJSTestRunner, + [challengeTypes.html]: getDOMTestRunner, + [challengeTypes.backend]: getDOMTestRunner +}; +export function getTestRunner(buildData, proxyLogger, document) { + return testRunners[buildData.challengeType](buildData, proxyLogger, document); +} + +function getJSTestRunner({ build, sources }, proxyLogger) { + const code = sources && 'index' in sources ? sources['index'] : ''; + + const testWorker = createWorker('test-evaluator'); + + return async(testString, testTimeout) => { + try { + testWorker.on('LOG', proxyLogger); + return await testWorker.execute( + { build, testString, code, sources }, + testTimeout + ); + } finally { + testWorker.killWorker(); + testWorker.remove('LOG', proxyLogger); + } + }; +} + +async function getDOMTestRunner(buildData, proxyLogger, document) { + await new Promise(resolve => + createTestFramer(document, resolve, proxyLogger)(buildData) + ); + return (testString, testTimeout) => + runTestInTestFrame(document, testString, testTimeout); +} + +export function buildDOMChallenge({ files, required = [], template = '' }) { const finalRequires = [...globalRequires, ...required, ...frameRunner]; + const loadEnzyme = Object.keys(files).some(key => files[key].ext === 'jsx'); const toHtml = [jsToHtml, cssToHtml]; const pipeLine = composeFunctions(...transformers, ...toHtml); const finalFiles = Object.keys(files) @@ -60,12 +116,14 @@ export function buildDOMChallenge(files, meta = {}) { return Promise.all(finalFiles) .then(checkFilesErrors) .then(files => ({ + challengeType: challengeTypes.html, build: concatHtml({ required: finalRequires, template, files }), - sources: buildSourceMap(files) + sources: buildSourceMap(files), + loadEnzyme })); } -export function buildJSChallenge(files) { +export function buildJSChallenge({ files }) { const pipeLine = composeFunctions(...transformers); const finalFiles = Object.keys(files) .map(key => files[key]) @@ -73,6 +131,7 @@ export function buildJSChallenge(files) { return Promise.all(finalFiles) .then(checkFilesErrors) .then(files => ({ + challengeType: challengeTypes.js, build: files .reduce( (body, file) => [...body, file.head, file.contents, file.tail], @@ -83,11 +142,9 @@ export function buildJSChallenge(files) { })); } -export function buildBackendChallenge(formValues) { - const { - solution: { value: url } - } = formValues; +export function buildBackendChallenge({ url }) { return { + challengeType: challengeTypes.backend, build: concatHtml({ required: frameRunner }), sources: { url } };