fix(client): disable build on error

pull/35255/head
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 {
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);

View File

@ -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 }) => ({

View File

@ -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 }
};