fix(client): disable build on error

This commit is contained in:
Valeriy S
2019-02-08 17:33:05 +03:00
committed by Bouncey
parent cafbe33cc7
commit 98f979f3b4
3 changed files with 153 additions and 104 deletions

View File

@ -10,56 +10,46 @@ import {
import { delay, channel } from 'redux-saga'; import { delay, channel } from 'redux-saga';
import { import {
backendFormValuesSelector, challengeDataSelector,
challengeFilesSelector,
challengeMetaSelector,
challengeTestsSelector, challengeTestsSelector,
initConsole, initConsole,
updateConsole, updateConsole,
initLogs, initLogs,
updateLogs, updateLogs,
logsToConsole, logsToConsole,
updateTests updateTests,
isBuildEnabledSelector,
disableBuildOnError
} from './'; } from './';
import { import { buildChallenge, getTestRunner } from '../utils/build';
buildJSChallenge,
buildDOMChallenge,
buildBackendChallenge
} from '../utils/build';
import { challengeTypes } from '../../../../utils/challengeTypes'; import { challengeTypes } from '../../../../utils/challengeTypes';
import createWorker from '../utils/worker-executor'; import { createMainFramer } from '../utils/frame.js';
import {
createMainFramer,
createTestFramer,
runTestInTestFrame
} from '../utils/frame.js';
export function* executeChallengeSaga() { export function* executeChallengeSaga() {
const isBuildEnabled = yield select(isBuildEnabledSelector);
if (!isBuildEnabled) {
return;
}
const consoleProxy = yield channel(); const consoleProxy = yield channel();
try { try {
const { js, bonfire, backend } = challengeTypes;
const { challengeType } = yield select(challengeMetaSelector);
yield put(initLogs()); yield put(initLogs());
yield put(initConsole('// running tests')); yield put(initConsole('// running tests'));
yield fork(logToConsole, consoleProxy); yield fork(logToConsole, consoleProxy);
const proxyLogger = args => consoleProxy.put(args); const proxyLogger = args => consoleProxy.put(args);
let testResults; const buildData = yield buildChallengeData();
switch (challengeType) { const document = yield getContext('document');
case js: const testRunner = yield call(
case bonfire: getTestRunner,
testResults = yield executeJSChallengeSaga(proxyLogger); buildData,
break; proxyLogger,
case backend: document
testResults = yield executeBackendChallengeSaga(proxyLogger); );
break; const testResults = yield executeTests(testRunner);
default:
testResults = yield executeDOMChallengeSaga(proxyLogger);
}
yield put(updateTests(testResults)); yield put(updateTests(testResults));
yield put(updateConsole('// tests completed')); yield put(updateConsole('// tests completed'));
@ -77,63 +67,16 @@ function* logToConsole(channel) {
}); });
} }
function* executeJSChallengeSaga(proxyLogger) { function* buildChallengeData() {
const files = yield select(challengeFilesSelector); const challengeData = yield select(challengeDataSelector);
const { build, sources } = yield call(buildJSChallenge, files);
const code = sources && 'index' in sources ? sources['index'] : '';
const testWorker = createWorker('test-evaluator');
testWorker.on('LOG', proxyLogger);
try { try {
return yield call(executeTests, async(testString, testTimeout) => { return yield call(buildChallenge, challengeData);
try { } catch (e) {
return await testWorker.execute( yield put(disableBuildOnError(e));
{ build, testString, code, sources }, throw ['Build failed'];
testTimeout
);
} finally {
testWorker.killWorker();
}
});
} finally {
testWorker.remove('LOG', proxyLogger);
} }
} }
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) { function* executeTests(testRunner) {
const tests = yield select(challengeTestsSelector); const tests = yield select(challengeTestsSelector);
const testTimeout = 5000; const testTimeout = 5000;
@ -168,16 +111,20 @@ function* executeTests(testRunner) {
} }
function* updateMainSaga() { function* updateMainSaga() {
yield delay(500); const isBuildEnabled = yield select(isBuildEnabledSelector);
if (!isBuildEnabled) {
return;
}
yield delay(700);
try { try {
yield put(initConsole(''));
const { html, modern } = challengeTypes; const { html, modern } = challengeTypes;
const meta = yield select(challengeMetaSelector); const { challengeType } = yield select(challengeDataSelector);
const { challengeType } = meta;
if (challengeType !== html && challengeType !== modern) { if (challengeType !== html && challengeType !== modern) {
return; return;
} }
const files = yield select(challengeFilesSelector); const ctx = yield buildChallengeData();
const ctx = yield call(buildDOMChallenge, files, meta);
const document = yield getContext('document'); const document = yield getContext('document');
const frameMain = yield call(createMainFramer, document); const frameMain = yield call(createMainFramer, document);
yield call(frameMain, ctx); yield call(frameMain, ctx);

View File

@ -14,6 +14,7 @@ import codeStorageEpic from './code-storage-epic';
import { createIdToNameMapSaga } from './id-to-name-map-saga'; import { createIdToNameMapSaga } from './id-to-name-map-saga';
import { createExecuteChallengeSaga } from './execute-challenge-saga'; import { createExecuteChallengeSaga } from './execute-challenge-saga';
import { createCurrentChallengeSaga } from './current-challenge-saga'; import { createCurrentChallengeSaga } from './current-challenge-saga';
import { challengeTypes } from '../../../../utils/challengeTypes';
export const ns = 'challenge'; export const ns = 'challenge';
export const backendNS = 'backendChallenge'; export const backendNS = 'backendChallenge';
@ -23,12 +24,14 @@ const initialState = {
challengeIdToNameMap: {}, challengeIdToNameMap: {},
challengeMeta: { challengeMeta: {
id: '', id: '',
nextChallengePath: '/' nextChallengePath: '/',
introPath: '',
challengeType: -1
}, },
challengeTests: [], challengeTests: [],
consoleOut: '', consoleOut: '',
isCodeLocked: false, isCodeLocked: false,
isJSEnabled: true, isBuildEnabled: true,
modal: { modal: {
completion: false, completion: false,
help: false, help: false,
@ -59,7 +62,7 @@ export const types = createTypes(
'lockCode', 'lockCode',
'unlockCode', 'unlockCode',
'disableJSOnError', 'disableBuildOnError',
'storedCodeFound', 'storedCodeFound',
'noStoredCodeFound', 'noStoredCodeFound',
@ -136,7 +139,7 @@ export const logsToConsole = createAction(types.logsToConsole);
export const lockCode = createAction(types.lockCode); export const lockCode = createAction(types.lockCode);
export const unlockCode = createAction(types.unlockCode); 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 storedCodeFound = createAction(types.storedCodeFound);
export const noStoredCodeFound = createAction(types.noStoredCodeFound); export const noStoredCodeFound = createAction(types.noStoredCodeFound);
@ -165,13 +168,55 @@ export const isCompletionModalOpenSelector = state =>
export const isHelpModalOpenSelector = state => state[ns].modal.help; export const isHelpModalOpenSelector = state => state[ns].modal.help;
export const isVideoModalOpenSelector = state => state[ns].modal.video; export const isVideoModalOpenSelector = state => state[ns].modal.video;
export const isResetModalOpenSelector = state => state[ns].modal.reset; 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 successMessageSelector = state => state[ns].successMessage;
export const backendFormValuesSelector = state => state.form[backendNS]; export const backendFormValuesSelector = state => state.form[backendNS];
export const projectFormValuesSelector = state => export const projectFormValuesSelector = state =>
state[ns].projectFormValues || {}; 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( export const reducer = handleActions(
{ {
[types.fetchIdToNameMapComplete]: (state, { payload }) => ({ [types.fetchIdToNameMapComplete]: (state, { payload }) => ({
@ -269,13 +314,13 @@ export const reducer = handleActions(
}), }),
[types.unlockCode]: state => ({ [types.unlockCode]: state => ({
...state, ...state,
isJSEnabled: true, isBuildEnabled: true,
isCodeLocked: false isCodeLocked: false
}), }),
[types.disableJSOnError]: (state, { payload }) => ({ [types.disableBuildOnError]: (state, { payload }) => ({
...state, ...state,
consoleOut: state.consoleOut + ' \n' + payload, consoleOut: state.consoleOut + ' \n' + payload,
isJSEnabled: false isBuildEnabled: false
}), }),
[types.updateSuccessMessage]: (state, { payload }) => ({ [types.updateSuccessMessage]: (state, { payload }) => ({

View File

@ -1,5 +1,8 @@
import { transformers } from '../rechallenge/transformers'; import { transformers } from '../rechallenge/transformers';
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js'; 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 = [ const frameRunner = [
{ {
@ -49,9 +52,62 @@ function checkFilesErrors(files) {
return files; return files;
} }
export function buildDOMChallenge(files, meta = {}) { const buildFunctions = {
const { required = [], template = '' } = meta; [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 finalRequires = [...globalRequires, ...required, ...frameRunner];
const loadEnzyme = Object.keys(files).some(key => files[key].ext === 'jsx');
const toHtml = [jsToHtml, cssToHtml]; const toHtml = [jsToHtml, cssToHtml];
const pipeLine = composeFunctions(...transformers, ...toHtml); const pipeLine = composeFunctions(...transformers, ...toHtml);
const finalFiles = Object.keys(files) const finalFiles = Object.keys(files)
@ -60,12 +116,14 @@ export function buildDOMChallenge(files, meta = {}) {
return Promise.all(finalFiles) return Promise.all(finalFiles)
.then(checkFilesErrors) .then(checkFilesErrors)
.then(files => ({ .then(files => ({
challengeType: challengeTypes.html,
build: concatHtml({ required: finalRequires, template, files }), 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 pipeLine = composeFunctions(...transformers);
const finalFiles = Object.keys(files) const finalFiles = Object.keys(files)
.map(key => files[key]) .map(key => files[key])
@ -73,6 +131,7 @@ export function buildJSChallenge(files) {
return Promise.all(finalFiles) return Promise.all(finalFiles)
.then(checkFilesErrors) .then(checkFilesErrors)
.then(files => ({ .then(files => ({
challengeType: challengeTypes.js,
build: files build: files
.reduce( .reduce(
(body, file) => [...body, file.head, file.contents, file.tail], (body, file) => [...body, file.head, file.contents, file.tail],
@ -83,11 +142,9 @@ export function buildJSChallenge(files) {
})); }));
} }
export function buildBackendChallenge(formValues) { export function buildBackendChallenge({ url }) {
const {
solution: { value: url }
} = formValues;
return { return {
challengeType: challengeTypes.backend,
build: concatHtml({ required: frameRunner }), build: concatHtml({ required: frameRunner }),
sources: { url } sources: { url }
}; };