From 301212e194250051413ff846e3c0ea8cc9f37138 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Fri, 12 Jun 2020 14:47:58 +0200 Subject: [PATCH] feat: handle multi-file solutions --- curriculum/getChallenges.js | 36 +++++++++------ curriculum/schema/challengeSchema.js | 7 ++- curriculum/test/test-challenges.js | 66 ++++++++++++++++------------ 3 files changed, 66 insertions(+), 43 deletions(-) diff --git a/curriculum/getChallenges.js b/curriculum/getChallenges.js index d563445834..1e944be889 100644 --- a/curriculum/getChallenges.js +++ b/curriculum/getChallenges.js @@ -185,24 +185,30 @@ Trying to parse ${fullPath}`); 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 function prepareChallenge(challenge) { challenge.name = nameify(challenge.title); if (challenge.files) { - challenge.files = reduce( - challenge.files, - (map, file) => { - map[file.key] = { - ...file, - head: arrToString(file.head), - contents: arrToString(file.contents), - tail: arrToString(file.tail) - }; - return map; - }, - {} - ); + challenge.files = filesToObject(challenge.files); // 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) .filter(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.superBlock = blockNameify(challenge.superBlock); return challenge; diff --git a/curriculum/schema/challengeSchema.js b/curriculum/schema/challengeSchema.js index dcc4404289..de6ad85427 100644 --- a/curriculum/schema/challengeSchema.js +++ b/curriculum/schema/challengeSchema.js @@ -73,7 +73,12 @@ function getSchemaForLang(lang) { }) ), 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(), superOrder: Joi.number(), suborder: Joi.number(), diff --git a/curriculum/test/test-challenges.js b/curriculum/test/test-challenges.js index f8ab80c7ea..4b3a5e865f 100644 --- a/curriculum/test/test-challenges.js +++ b/curriculum/test/test-challenges.js @@ -23,7 +23,7 @@ const { const { assert, AssertionError } = require('chai'); const Mocha = require('mocha'); -const { flatten, isEmpty } = require('lodash'); +const { flatten, isEmpty, cloneDeep } = require('lodash'); const jsdom = require('jsdom'); @@ -330,9 +330,6 @@ function populateTestsForLang({ lang, challenges, meta }) { ? buildJSChallenge : 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() { this.timeout(5000 * tests.length + 1000); // 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'); }); - let { solutions = [] } = 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]; - } - } + let { solutions = [], solutionFiles = {} } = challenge; + const noSolution = new RegExp('// solution required'); solutions = solutions.filter( 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'); return; } @@ -404,22 +410,24 @@ function populateTestsForLang({ lang, challenges, meta }) { }); } -// TODO: solutions will need to be multi-file, too, with a fallback when there -// is only one file. -// we cannot simply use the solution instead of files, because the are not -// just the seed(s), they contain the head and tail code. The best approach -// is probably to separate out the head and tail from the files. Then the -// files can be entirely replaced by the solution. +async function createTestRunner(challenge, solution, buildChallenge) { + const { required = [], template } = challenge; + // we should avoid modifying challenge, as it gets reused: + 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 => { + 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; + } else { + throw Error('Tried to create test runner without a solution.'); } const { build, sources, loadEnzyme } = await buildChallenge({