fix: unify single and multifile testing

This commit is contained in:
Oliver Eyton-Williams
2020-07-13 18:59:40 +02:00
committed by Mrugesh Mohapatra
parent d7dc1acb4a
commit 0f3f27287d
4 changed files with 87 additions and 75 deletions

View File

@ -72,13 +72,15 @@ function getSchemaForLang(lang) {
crossDomain: Joi.bool() crossDomain: Joi.bool()
}) })
), ),
solutions: Joi.array().items(Joi.string().optional()), solutions: Joi.array().items(
solutionFiles: Joi.object().keys({ Joi.object().keys({
indexcss: fileJoi, indexcss: fileJoi,
indexhtml: fileJoi, indexhtml: fileJoi,
indexjs: fileJoi, indexjs: fileJoi,
indexjsx: fileJoi indexjsx: fileJoi,
}), indexpy: fileJoi
})
),
superBlock: Joi.string(), superBlock: Joi.string(),
superOrder: Joi.number(), superOrder: Joi.number(),
suborder: Joi.number(), suborder: Joi.number(),

View File

@ -360,31 +360,39 @@ function populateTestsForLang({ lang, challenges, meta }) {
assert(fails, 'Test suit does not fail on the initial contents'); assert(fails, 'Test suit does not fail on the initial contents');
}); });
let { solutions = [], solutionFiles = {} } = challenge; let { solutions = [] } = challenge;
const noSolution = new RegExp('// solution required'); // if there's an empty string as solution, this is likely a mistake
solutions = solutions.filter( // TODO: what does this look like now?
solution => !!solution && !noSolution.test(solution)
);
if (isEmpty(solutions)) {
// if there are no solutions in the challenge, it's assumed the next // if there are no solutions in the challenge, it's assumed the next
// challenge's seed will be a solution to the current challenge. // challenge's seed will be a solution to the current challenge.
// This is expected to happen in the project based curriculum. // This is expected to happen in the project based curriculum.
if (isEmpty(solutions)) {
if (!isEmpty(solutionFiles)) {
solutions = [solutionFiles];
// TODO: there needs to be a way of telling that a challenge uses
// multiple files when it doesn't have anything in solutionFiles!
} else {
const nextChallenge = challenges[id + 1]; const nextChallenge = challenges[id + 1];
// TODO: check this actually works...
if (nextChallenge) { if (nextChallenge) {
solutions = [nextChallenge.files[0].contents]; solutions = [nextChallenge.files];
} } else {
throw Error('solution omitted');
} }
} }
if (solutions.length === 0 && isEmpty(solutionFiles)) { // TODO: the no-solution filtering is a little convoluted:
const noSolution = new RegExp('// solution required');
const solutionsAsArrays = solutions.map(toSortedArray);
const filteredSolutions = solutionsAsArrays.filter(solution => {
return !isEmpty(
solution.filter(file => !noSolution.test(file.contents))
);
});
// console.log('filteredSolutions', filteredSolutions);
if (isEmpty(filteredSolutions)) {
it('Check tests. No solutions'); it('Check tests. No solutions');
return; return;
} }
@ -415,20 +423,9 @@ async function createTestRunner(challenge, solution, buildChallenge) {
// we should avoid modifying challenge, as it gets reused: // we should avoid modifying challenge, as it gets reused:
const files = cloneDeep(challenge.files); const files = cloneDeep(challenge.files);
// TODO: there must be a better way of handling both single and multi-file
// solutions
if (typeof solution === 'object' && !isEmpty(solution)) {
Object.keys(solution).forEach(key => { Object.keys(solution).forEach(key => {
files[key].contents = solution[key].contents; files[key].contents = solution[key].contents;
}); });
} else if (solution) {
// fallback for single solution
const sortedFiles = toSortedArray(files);
files[sortedFiles[0].key].contents = solution;
} else {
throw Error('Tried to create test runner without a solution.');
}
const { build, sources, loadEnzyme } = await buildChallenge({ const { build, sources, loadEnzyme } = await buildChallenge({
files, files,

View File

@ -19,22 +19,23 @@ function defaultFile(lang) {
tail: '' tail: ''
}; };
} }
function createCodeGetter(key, regEx, seeds) { function createCodeGetter(codeKey, regEx, seeds) {
return container => { return container => {
const { const {
properties: { id } properties: { id }
} = container; } = container;
const lang = id.match(regEx)[1]; const lang = id.match(regEx)[1];
const key = `index${lang}`;
const code = select('code', container).children[0].value; const code = select('code', container).children[0].value;
if (lang in seeds) { if (key in seeds) {
seeds[lang] = { seeds[key] = {
...seeds[lang], ...seeds[key],
[key]: code [codeKey]: code
}; };
} else { } else {
seeds[lang] = { seeds[key] = {
...defaultFile(lang), ...defaultFile(lang),
[key]: code [codeKey]: code
}; };
} }
}; };
@ -89,13 +90,13 @@ function createPlugin() {
file.data = { file.data = {
...file.data, ...file.data,
files: Object.keys(seeds).map(lang => seeds[lang]) files: seeds
}; };
// TODO: make this readable. // TODO: make this readable.
if (file.data.files) { Object.keys(seeds).forEach(key => {
file.data.files.forEach(fileData => { const fileData = seeds[key];
const editRegionMarkers = findRegionMarkers(fileData); const editRegionMarkers = findRegionMarkers(fileData);
if (editRegionMarkers) { if (editRegionMarkers) {
fileData.contents = removeLines( fileData.contents = removeLines(
@ -111,8 +112,6 @@ function createPlugin() {
fileData.editableRegionBoundaries = []; fileData.editableRegionBoundaries = [];
} }
}); });
}
// TODO: TESTS! // TODO: TESTS!
} }
} }
@ -122,3 +121,4 @@ function createPlugin() {
exports.challengeSeedToData = createPlugin; exports.challengeSeedToData = createPlugin;
exports.createCodeGetter = createCodeGetter; exports.createCodeGetter = createCodeGetter;
exports.defaultFile = defaultFile;

View File

@ -2,23 +2,27 @@ const visit = require('unist-util-visit');
const { selectAll } = require('hast-util-select'); const { selectAll } = require('hast-util-select');
const { sectionFilter } = require('./utils'); const { sectionFilter } = require('./utils');
const { createCodeGetter } = require('./challengeSeed-to-data'); const { createCodeGetter, defaultFile } = require('./challengeSeed-to-data');
const { isEmpty } = require('lodash');
const solutionRE = /(.+)-solution$/; const solutionRE = /(.+)-solution$/;
function indexByKey(obj) {
return { [obj.key]: { ...obj } };
}
function createPlugin() { function createPlugin() {
return function transformer(tree, file) { return function transformer(tree, file) {
function visitor(node) { function visitor(node) {
if (sectionFilter(node, 'solution')) { if (sectionFilter(node, 'solution')) {
// fallback for single-file challenges // fallback for single-file challenges
const solutions = selectAll('code', node).map( const rawSolutions = selectAll('code', node).map(element => ({
element => element.children[0].value lang: element.properties.className[0].split('-')[1],
); contents: element.children[0].value
file.data = { }));
...file.data,
solutions
};
const solutionFiles = {}; const solutionFiles = {};
const codeDivs = selectAll('div', node); const codeDivs = selectAll('div', node);
const solutionContainers = codeDivs.filter(({ properties: { id } }) => const solutionContainers = codeDivs.filter(({ properties: { id } }) =>
solutionRE.test(id) solutionRE.test(id)
@ -27,11 +31,20 @@ function createPlugin() {
createCodeGetter('contents', solutionRE, solutionFiles) createCodeGetter('contents', solutionRE, solutionFiles)
); );
const solutionsAsFiles = rawSolutions
.map(({ lang, contents }) => ({
...defaultFile(lang),
contents
}))
.map(indexByKey);
const solutions = isEmpty(solutionFiles)
? solutionsAsFiles
: [solutionFiles];
file.data = { file.data = {
...file.data, ...file.data,
solutionFiles: Object.keys(solutionFiles).map( solutions
lang => solutionFiles[lang]
)
}; };
} }
} }