fix(client): disable build on error
parent
cafbe33cc7
commit
98f979f3b4
|
@ -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);
|
||||
|
|
|
@ -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 }) => ({
|
||||
|
|
|
@ -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 }
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue