chore: refactor and simplify testing (#39050)

This commit is contained in:
Oliver Eyton-Williams
2020-06-13 11:27:15 +02:00
committed by GitHub
parent 5934984064
commit b4926052f4
14 changed files with 122 additions and 305 deletions

View File

@ -1,206 +0,0 @@
// originally based off of https://github.com/gulpjs/vinyl
import invariant from 'invariant';
import { Observable } from 'rx';
import castToObservable from '../../server/utils/cast-to-observable';
// createFileStream(
// files: [...PolyVinyl]
// ) => Observable[...Observable[...PolyVinyl]]
export function createFileStream(files = []) {
return Observable.of(Observable.from(files));
}
// Observable::pipe(
// project(
// file: PolyVinyl
// ) => PolyVinyl|Observable[PolyVinyl]|Promise[PolyVinyl]
// ) => Observable[...Observable[...PolyVinyl]]
export function pipe(project) {
const source = this;
return source.map(files =>
files.flatMap(file => castToObservable(project(file)))
);
}
// interface PolyVinyl {
// source: String,
// contents: String,
// name: String,
// ext: String,
// path: String,
// key: String,
// head: String,
// tail: String,
// history: [...String],
// error: Null|Object|Error
// }
// createPoly({
// name: String,
// ext: String,
// contents: String,
// history?: [...String],
// }) => PolyVinyl, throws
export function createPoly({ name, ext, contents, history, ...rest } = {}) {
invariant(typeof name === 'string', 'name must be a string but got %s', name);
invariant(typeof ext === 'string', 'ext must be a string, but was %s', ext);
invariant(
typeof contents === 'string',
'contents must be a string but got %s',
contents
);
return {
...rest,
history: Array.isArray(history) ? history : [name + ext],
name,
ext,
path: name + '.' + ext,
key: name + ext,
contents,
error: null
};
}
// isPoly(poly: Any) => Boolean
export function isPoly(poly) {
return (
poly &&
typeof poly.contents === 'string' &&
typeof poly.name === 'string' &&
typeof poly.ext === 'string' &&
Array.isArray(poly.history)
);
}
// checkPoly(poly: Any) => Void, throws
export function checkPoly(poly) {
invariant(
isPoly(poly),
'function should receive a PolyVinyl, but got %s',
poly
);
}
// isEmpty(poly: PolyVinyl) => Boolean, throws
export function isEmpty(poly) {
checkPoly(poly);
return !!poly.contents;
}
// setContent(contents: String, poly: PolyVinyl) => PolyVinyl
// setContent will loose source if set
export function setContent(contents, poly) {
checkPoly(poly);
return {
...poly,
contents,
source: null
};
}
// setExt(ext: String, poly: PolyVinyl) => PolyVinyl
export function setExt(ext, poly) {
checkPoly(poly);
const newPoly = {
...poly,
ext,
path: poly.name + '.' + ext,
key: poly.name + ext
};
newPoly.history = [...poly.history, newPoly.path];
return newPoly;
}
// setName(name: String, poly: PolyVinyl) => PolyVinyl
export function setName(name, poly) {
checkPoly(poly);
const newPoly = {
...poly,
name,
path: name + '.' + poly.ext,
key: name + poly.ext
};
newPoly.history = [...poly.history, newPoly.path];
return newPoly;
}
// setError(error: Object, poly: PolyVinyl) => PolyVinyl
export function setError(error, poly) {
invariant(
typeof error === 'object',
'error must be an object or null, but got %',
error
);
checkPoly(poly);
return {
...poly,
error
};
}
// clearHeadTail(poly: PolyVinyl) => PolyVinyl
export function clearHeadTail(poly) {
checkPoly(poly);
return {
...poly,
head: '',
tail: ''
};
}
// appendToTail (tail: String, poly: PolyVinyl) => PolyVinyl
export function appendToTail(tail, poly) {
checkPoly(poly);
return {
...poly,
tail: poly.tail.concat(tail)
};
}
// compileHeadTail(padding: String, poly: PolyVinyl) => PolyVinyl
export function compileHeadTail(padding = '', poly) {
return clearHeadTail(
transformContents(
() => [poly.head, poly.contents, poly.tail].join(padding),
poly
)
);
}
// transformContents(
// wrap: (contents: String) => String,
// poly: PolyVinyl
// ) => PolyVinyl
// transformContents will keep a copy of the original
// code in the `source` property. If the original polyvinyl
// already contains a source, this version will continue as
// the source property
export function transformContents(wrap, poly) {
const newPoly = setContent(wrap(poly.contents), poly);
// if no source exist, set the original contents as source
newPoly.source = poly.source || poly.contents;
return newPoly;
}
// transformHeadTailAndContents(
// wrap: (source: String) => String,
// poly: PolyVinyl
// ) => PolyVinyl
export function transformHeadTailAndContents(wrap, poly) {
return {
...transformContents(wrap, poly),
head: wrap(poly.head),
tail: wrap(poly.tail)
};
}
export function testContents(predicate, poly) {
return !!predicate(poly.contents);
}
export function updateFileFromSpec(spec, poly) {
return setContent(poly.contents, createPoly(spec));
}

View File

@ -1,5 +1,5 @@
import validator from 'express-validator';
import { isPoly } from '../../common/utils/polyvinyl';
import { isPoly } from '../../../utils/polyvinyl';
const isObject = val => !!val && typeof val === 'object';

View File

@ -3,7 +3,7 @@ const env = require('../config/env');
const { createFilePath } = require('gatsby-source-filesystem');
const { dasherize } = require('../utils/slugs');
const { blockNameify } = require('./utils/blockNameify');
const { blockNameify } = require('../utils/block-nameify');
const {
createChallengePages,
createBlockIntroPages,

View File

@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap';
import { Spacer } from '../helpers';
import { blockNameify } from '../../../utils/blockNameify';
import { blockNameify } from '../../../../utils/block-nameify';
import Heart from '../../assets/icons/Heart';
import Cup from '../../assets/icons/Cup';
import MinimalDonateForm from './MinimalDonateForm';

View File

@ -8,7 +8,7 @@ import { Link } from 'gatsby';
import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import { completedChallengesSelector, executeGA } from '../../../redux';
import Caret from '../../../assets/icons/Caret';
import { blockNameify } from '../../../../utils/blockNameify';
import { blockNameify } from '../../../../../utils/block-nameify';
import GreenPass from '../../../assets/icons/GreenPass';
import GreenNotCompleted from '../../../assets/icons/GreenNotCompleted';
import IntroInformation from '../../../assets/icons/IntroInformation';

View File

@ -8,7 +8,11 @@ import {
template as _template
} from 'lodash';
import { compileHeadTail, setExt, transformContents } from '../utils/polyvinyl';
import {
compileHeadTail,
setExt,
transformContents
} from '../../../../../utils/polyvinyl';
const htmlCatch = '\n<!--fcc-->\n';
const jsCatch = '\n;/*fcc*/\n';

View File

@ -12,7 +12,7 @@ import {
import protect from '@freecodecamp/loop-protect';
import * as vinyl from '../utils/polyvinyl.js';
import * as vinyl from '../../../../../utils/polyvinyl.js';
import createWorker from '../utils/worker-executor';
// the config files are created during the build, but not before linting

View File

@ -14,7 +14,7 @@ import {
import { types as appTypes } from '../../../redux';
import { setContent, isPoly } from '../utils/polyvinyl';
import { setContent, isPoly } from '../../../../../utils/polyvinyl';
import { createFlashMessage } from '../../../components/Flash/redux';

View File

@ -2,7 +2,7 @@ import { createAction, handleActions } from 'redux-actions';
import { createTypes } from '../../../../utils/stateManagement';
import { createPoly } from '../utils/polyvinyl';
import { createPoly } from '../../../../../utils/polyvinyl';
import challengeModalEpic from './challenge-modal-epic';
import completionEpic from './completion-epic';
import codeLockEpic from './code-lock-epic';

View File

@ -7,7 +7,7 @@ const {
} = require('../../curriculum/getChallenges');
const { dasherize, nameify } = require('../../utils/slugs');
const { locale } = require('../config/env.json');
const { blockNameify } = require('./blockNameify');
const { blockNameify } = require('../../utils/block-nameify');
const arrToString = arr =>
Array.isArray(arr) ? arr.join('\n') : _.toString(arr);

View File

@ -6,8 +6,6 @@ const fs = require('fs');
const { dasherize } = require('../utils/slugs');
const { challengeSchemaValidator } = require('./schema/challengeSchema');
const challengesDir = path.resolve(__dirname, './challenges');
const metaDir = path.resolve(challengesDir, '_meta');
exports.challengesDir = challengesDir;
@ -38,13 +36,13 @@ exports.getChallengesForLang = function getChallengesForLang(lang) {
readDirP({ root: getChallengesDirForLang(lang) })
.on('data', file => {
running++;
buildCurriculum(file, curriculum, lang).then(done);
buildCurriculum(file, curriculum).then(done);
})
.on('end', done);
});
};
async function buildCurriculum(file, curriculum, lang) {
async function buildCurriculum(file, curriculum) {
const { name, depth, path: filePath, fullPath, stat } = file;
if (depth === 1 && stat.isDirectory()) {
// extract the superBlock info
@ -80,12 +78,12 @@ async function buildCurriculum(file, curriculum, lang) {
}
const { meta } = challengeBlock;
const challenge = await createChallenge(fullPath, meta, lang);
const challenge = await createChallenge(fullPath, meta);
challengeBlock.challenges = [...challengeBlock.challenges, challenge];
}
async function createChallenge(fullPath, maybeMeta, lang) {
async function createChallenge(fullPath, maybeMeta) {
let meta;
if (maybeMeta) {
meta = maybeMeta;
@ -98,11 +96,6 @@ async function createChallenge(fullPath, maybeMeta, lang) {
}
const { name: superBlock } = superBlockInfoFromFullPath(fullPath);
const challenge = await parseMarkdown(fullPath);
const result = challengeSchemaValidator(lang)(challenge);
if (result.error) {
console.log(result.value);
throw new Error(result.error);
}
const challengeOrder = findIndex(
meta.challengeOrder,
([id]) => id === challenge.id

View File

@ -53,9 +53,7 @@ const {
buildJSChallenge
} = require('../../client/src/templates/Challenges/utils/build');
const {
createPoly
} = require('../../client/src/templates/Challenges/utils/polyvinyl');
const { createPoly } = require('../../utils/polyvinyl');
const testEvaluator = require('../../client/config/test-evaluator').filename;
@ -323,24 +321,24 @@ function populateTestsForLang({ lang, challenges }, meta) {
});
let { files = [] } = challenge;
let createTestRunner;
if (challengeType === challengeTypes.backend) {
it('Check tests is not implemented.');
return;
} else if (
challengeType === challengeTypes.js ||
challengeType === challengeTypes.bonfire
) {
createTestRunner = createTestRunnerForJSChallenge;
} else if (files.length === 1) {
createTestRunner = createTestRunnerForDOMChallenge;
} else {
}
if (files.length > 1) {
it('Check tests.', () => {
throw new Error('Seed file should be only the one.');
});
return;
}
const buildChallenge =
challengeType === challengeTypes.js ||
challengeType === challengeTypes.bonfire
? buildJSChallenge
: buildDOMChallenge;
files = files.map(createPoly);
it('Test suite must fail on the initial contents', async function() {
this.timeout(5000 * tests.length + 1000);
@ -353,7 +351,7 @@ function populateTestsForLang({ lang, challenges }, meta) {
testRunner = await createTestRunner(
{ ...challenge, files },
'',
page
buildChallenge
);
} catch {
fails = true;
@ -390,7 +388,7 @@ function populateTestsForLang({ lang, challenges }, meta) {
const testRunner = await createTestRunner(
{ ...challenge, files },
solution,
page
buildChallenge
);
for (const test of tests) {
await testRunner(test);
@ -404,41 +402,29 @@ function populateTestsForLang({ lang, challenges }, meta) {
});
}
async function createTestRunnerForDOMChallenge(
async function createTestRunner(
{ required = [], template, files },
solution,
context
buildChallenge
) {
if (solution) {
files[0].contents = solution;
}
const { build, sources, loadEnzyme } = await buildDOMChallenge({
const { build, sources, loadEnzyme } = await buildChallenge({
files,
required,
template
});
await context.reload();
await context.setContent(build);
await context.evaluate(
async (sources, loadEnzyme) => {
const code = sources && 'index' in sources ? sources['index'] : '';
const getUserInput = fileName => sources[fileName];
await document.__initTestFrame({ code, getUserInput, loadEnzyme });
},
sources,
loadEnzyme
);
const evaluator = await (buildChallenge === buildDOMChallenge
? getContextEvaluator(build, sources, code, loadEnzyme)
: getWorkerEvaluator(build, sources, code));
return async ({ text, testString }) => {
try {
const { pass, err } = await Promise.race([
new Promise((_, reject) => setTimeout(() => reject('timeout'), 5000)),
await context.evaluate(async testString => {
return await document.__runTest(testString);
}, testString)
]);
const { pass, err } = await evaluator.evaluate(testString, 5000);
if (!pass) {
throw new AssertionError(err.message);
}
@ -448,30 +434,45 @@ async function createTestRunnerForDOMChallenge(
};
}
async function createTestRunnerForJSChallenge({ files }, solution) {
if (solution) {
files[0].contents = solution;
}
async function getContextEvaluator(build, sources, code, loadEnzyme) {
await initializeTestRunner(build, sources, code, loadEnzyme);
const { build, sources } = await buildJSChallenge({ files });
const code = sources && 'index' in sources ? sources['index'] : '';
const testWorker = createWorker(testEvaluator, { terminateWorker: true });
return async ({ text, testString }) => {
try {
const { pass, err } = await testWorker.execute(
{ testString, build, code, sources },
5000
).done;
if (!pass) {
throw new AssertionError(err.message);
}
} catch (err) {
reThrow(err, text);
}
return {
evaluate: async (testString, timeout) =>
Promise.race([
new Promise((_, reject) =>
setTimeout(() => reject('timeout'), timeout)
),
await page.evaluate(async testString => {
return await document.__runTest(testString);
}, testString)
])
};
}
async function getWorkerEvaluator(build, sources, code) {
const testWorker = createWorker(testEvaluator, { terminateWorker: true });
return {
evaluate: async (testString, timeout) =>
await testWorker.execute({ testString, build, code, sources }, timeout)
.done
};
}
async function initializeTestRunner(build, sources, code, loadEnzyme) {
await page.reload();
await page.setContent(build);
await page.evaluate(
async (code, sources, loadEnzyme) => {
const getUserInput = fileName => sources[fileName];
await document.__initTestFrame({ code, getUserInput, loadEnzyme });
},
code,
sources,
loadEnzyme
);
}
function reThrow(err, text) {
if (typeof err === 'string') {
throw new AssertionError(

View File

@ -1,14 +1,17 @@
// originally based off of https://github.com/gulpjs/vinyl
import invariant from 'invariant';
import { of, from, isObservable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
const invariant = require('invariant');
const { of, from, isObservable } = require('rxjs');
const { map, switchMap } = require('rxjs/operators');
export const isPromise = value =>
function isPromise(value) {
return (
value &&
typeof value.subscribe !== 'function' &&
typeof value.then === 'function';
typeof value.then === 'function'
);
}
export function castToObservable(maybe) {
function castToObservable(maybe) {
if (isObservable(maybe)) {
return maybe;
}
@ -21,7 +24,7 @@ export function castToObservable(maybe) {
// createFileStream(
// files: [...PolyVinyl]
// ) => Observable[...Observable[...PolyVinyl]]
export function createFileStream(files = []) {
function createFileStream(files = []) {
return of(from(files));
}
@ -30,7 +33,7 @@ export function createFileStream(files = []) {
// file: PolyVinyl
// ) => PolyVinyl|Observable[PolyVinyl]|Promise[PolyVinyl]
// ) => Observable[...Observable[...PolyVinyl]]
export function pipe(project) {
function pipe(project) {
const source = this;
return source.pipe(
map(files => {
@ -58,7 +61,7 @@ export function pipe(project) {
// contents: String,
// history?: [...String],
// }) => PolyVinyl, throws
export function createPoly({ name, ext, contents, history, ...rest } = {}) {
function createPoly({ name, ext, contents, history, ...rest } = {}) {
invariant(typeof name === 'string', 'name must be a string but got %s', name);
invariant(typeof ext === 'string', 'ext must be a string, but was %s', ext);
@ -82,7 +85,7 @@ export function createPoly({ name, ext, contents, history, ...rest } = {}) {
}
// isPoly(poly: Any) => Boolean
export function isPoly(poly) {
function isPoly(poly) {
return (
poly &&
typeof poly.contents === 'string' &&
@ -93,7 +96,7 @@ export function isPoly(poly) {
}
// checkPoly(poly: Any) => Void, throws
export function checkPoly(poly) {
function checkPoly(poly) {
invariant(
isPoly(poly),
'function should receive a PolyVinyl, but got %s',
@ -102,14 +105,14 @@ export function checkPoly(poly) {
}
// isEmpty(poly: PolyVinyl) => Boolean, throws
export function isEmpty(poly) {
function isEmpty(poly) {
checkPoly(poly);
return !!poly.contents;
}
// setContent(contents: String, poly: PolyVinyl) => PolyVinyl
// setContent will loose source if set
export function setContent(contents, poly) {
function setContent(contents, poly) {
checkPoly(poly);
return {
...poly,
@ -119,7 +122,7 @@ export function setContent(contents, poly) {
}
// setExt(ext: String, poly: PolyVinyl) => PolyVinyl
export function setExt(ext, poly) {
function setExt(ext, poly) {
checkPoly(poly);
const newPoly = {
...poly,
@ -132,7 +135,7 @@ export function setExt(ext, poly) {
}
// setName(name: String, poly: PolyVinyl) => PolyVinyl
export function setName(name, poly) {
function setName(name, poly) {
checkPoly(poly);
const newPoly = {
...poly,
@ -145,7 +148,7 @@ export function setName(name, poly) {
}
// setError(error: Object, poly: PolyVinyl) => PolyVinyl
export function setError(error, poly) {
function setError(error, poly) {
invariant(
typeof error === 'object',
'error must be an object or null, but got %',
@ -159,7 +162,7 @@ export function setError(error, poly) {
}
// clearHeadTail(poly: PolyVinyl) => PolyVinyl
export function clearHeadTail(poly) {
function clearHeadTail(poly) {
checkPoly(poly);
return {
...poly,
@ -169,7 +172,7 @@ export function clearHeadTail(poly) {
}
// appendToTail (tail: String, poly: PolyVinyl) => PolyVinyl
export function appendToTail(tail, poly) {
function appendToTail(tail, poly) {
checkPoly(poly);
return {
...poly,
@ -178,7 +181,7 @@ export function appendToTail(tail, poly) {
}
// compileHeadTail(padding: String, poly: PolyVinyl) => PolyVinyl
export function compileHeadTail(padding = '', poly) {
function compileHeadTail(padding = '', poly) {
return clearHeadTail(
transformContents(
() => [poly.head, poly.contents, poly.tail].join(padding),
@ -195,7 +198,7 @@ export function compileHeadTail(padding = '', poly) {
// code in the `source` property. If the original polyvinyl
// already contains a source, this version will continue as
// the source property
export function transformContents(wrap, poly) {
function transformContents(wrap, poly) {
const newPoly = setContent(wrap(poly.contents), poly);
// if no source exist, set the original contents as source
newPoly.source = poly.source || poly.contents;
@ -206,7 +209,7 @@ export function transformContents(wrap, poly) {
// wrap: (source: String) => String,
// poly: PolyVinyl
// ) => PolyVinyl
export function transformHeadTailAndContents(wrap, poly) {
function transformHeadTailAndContents(wrap, poly) {
return {
...transformContents(wrap, poly),
head: wrap(poly.head),
@ -214,10 +217,32 @@ export function transformHeadTailAndContents(wrap, poly) {
};
}
export function testContents(predicate, poly) {
function testContents(predicate, poly) {
return !!predicate(poly.contents);
}
export function updateFileFromSpec(spec, poly) {
function updateFileFromSpec(spec, poly) {
return setContent(poly.contents, createPoly(spec));
}
module.exports = {
isPromise,
castToObservable,
createFileStream,
pipe,
createPoly,
isPoly,
checkPoly,
isEmpty,
setContent,
setExt,
setName,
setError,
clearHeadTail,
appendToTail,
compileHeadTail,
transformContents,
transformHeadTailAndContents,
testContents,
updateFileFromSpec
};