feat: handle multi-file solutions

This commit is contained in:
Oliver Eyton-Williams
2020-06-12 14:47:58 +02:00
committed by Mrugesh Mohapatra
parent 54630cbfca
commit 301212e194
3 changed files with 66 additions and 43 deletions

View File

@ -185,24 +185,30 @@ Trying to parse ${fullPath}`);
return prepareChallenge(challenge); return prepareChallenge(challenge);
} }
// TODO: tests and more descriptive name.
function filesToObject(files) {
return reduce(
files,
(map, file) => {
map[file.key] = {
...file,
head: arrToString(file.head),
contents: arrToString(file.contents),
tail: arrToString(file.tail)
};
return map;
},
{}
);
}
// gets the challenge ready for sourcing into Gatsby // gets the challenge ready for sourcing into Gatsby
function prepareChallenge(challenge) { function prepareChallenge(challenge) {
challenge.name = nameify(challenge.title); challenge.name = nameify(challenge.title);
if (challenge.files) { if (challenge.files) {
challenge.files = reduce( challenge.files = filesToObject(challenge.files);
challenge.files,
(map, file) => {
map[file.key] = {
...file,
head: arrToString(file.head),
contents: arrToString(file.contents),
tail: arrToString(file.tail)
};
return map;
},
{}
);
// TODO: This should be something that can be folded into the above reduce // TODO: This should be something that can be folded into the above reduce
// EDIT: maybe not, now that we're doing the same for solutionFiles.
challenge.files = Object.keys(challenge.files) challenge.files = Object.keys(challenge.files)
.filter(key => challenge.files[key]) .filter(key => challenge.files[key])
.map(key => challenge.files[key]) .map(key => challenge.files[key])
@ -217,6 +223,10 @@ function prepareChallenge(challenge) {
{} {}
); );
} }
if (challenge.solutionFiles) {
challenge.solutionFiles = filesToObject(challenge.solutionFiles);
}
challenge.block = dasherize(challenge.block); challenge.block = dasherize(challenge.block);
challenge.superBlock = blockNameify(challenge.superBlock); challenge.superBlock = blockNameify(challenge.superBlock);
return challenge; return challenge;

View File

@ -73,7 +73,12 @@ function getSchemaForLang(lang) {
}) })
), ),
solutions: Joi.array().items(Joi.string().optional()), solutions: Joi.array().items(Joi.string().optional()),
solutionFiles: Joi.array().items(fileJoi), solutionFiles: Joi.object().keys({
indexcss: fileJoi,
indexhtml: fileJoi,
indexjs: fileJoi,
indexjsx: fileJoi
}),
superBlock: Joi.string(), superBlock: Joi.string(),
superOrder: Joi.number(), superOrder: Joi.number(),
suborder: Joi.number(), suborder: Joi.number(),

View File

@ -23,7 +23,7 @@ const {
const { assert, AssertionError } = require('chai'); const { assert, AssertionError } = require('chai');
const Mocha = require('mocha'); const Mocha = require('mocha');
const { flatten, isEmpty } = require('lodash'); const { flatten, isEmpty, cloneDeep } = require('lodash');
const jsdom = require('jsdom'); const jsdom = require('jsdom');
@ -330,9 +330,6 @@ function populateTestsForLang({ lang, challenges, meta }) {
? buildJSChallenge ? buildJSChallenge
: buildDOMChallenge; : buildDOMChallenge;
// TODO: create more sophisticated validation now we allow for more
// than one seed/solution file.
it('Test suite must fail on the initial contents', async function() { it('Test suite must fail on the initial contents', async function() {
this.timeout(5000 * tests.length + 1000); this.timeout(5000 * tests.length + 1000);
// suppress errors in the console. // suppress errors in the console.
@ -363,22 +360,31 @@ 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 = [] } = challenge; let { solutions = [], solutionFiles = {} } = challenge;
// if there are no solutions in the challenge, it's assumed the next
// challenge's seed will be a solution to the current challenge.
// This is expected to happen in the project based curriculum.
if (isEmpty(solutions)) {
const nextChallenge = challenges[id + 1];
if (nextChallenge) {
solutions = [nextChallenge.files[0].contents];
}
}
const noSolution = new RegExp('// solution required'); const noSolution = new RegExp('// solution required');
solutions = solutions.filter( solutions = solutions.filter(
solution => !!solution && !noSolution.test(solution) solution => !!solution && !noSolution.test(solution)
); );
if (solutions.length === 0) { // if there are no solutions in the challenge, it's assumed the next
// challenge's seed will be a solution to the current challenge.
// 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];
if (nextChallenge) {
solutions = [nextChallenge.files[0].contents];
}
}
}
if (solutions.length === 0 && isEmpty(solutionFiles)) {
it('Check tests. No solutions'); it('Check tests. No solutions');
return; return;
} }
@ -404,22 +410,24 @@ function populateTestsForLang({ lang, challenges, meta }) {
}); });
} }
// TODO: solutions will need to be multi-file, too, with a fallback when there async function createTestRunner(challenge, solution, buildChallenge) {
// is only one file. const { required = [], template } = challenge;
// we cannot simply use the solution instead of files, because the are not // we should avoid modifying challenge, as it gets reused:
// just the seed(s), they contain the head and tail code. The best approach const files = cloneDeep(challenge.files);
// is probably to separate out the head and tail from the files. Then the
// files can be entirely replaced by the solution. // 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 => {
files[key].contents = solution[key].contents;
});
} else if (solution) {
// fallback for single solution
const sortedFiles = sortFiles(files);
async function createTestRunner(
{ required = [], template, files },
solution,
buildChallenge
) {
// fallback for single solution
const sortedFiles = sortFiles(files);
if (solution) {
files[sortedFiles[0].key].contents = solution; 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({