Skip to content

Commit e404d9d

Browse files
chore: stop building entire curriculum when testing subset (freeCodeCamp#59599)
1 parent 5ef9868 commit e404d9d

2 files changed

Lines changed: 129 additions & 68 deletions

File tree

curriculum/get-challenges.js

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const util = require('util');
44
const yaml = require('js-yaml');
55
const { findIndex } = require('lodash');
66
const readDirP = require('readdirp');
7+
const stringSimilarity = require('string-similarity');
78

89
const { curriculum: curriculumLangs } =
910
require('../shared/config/i18n').availableLangs;
@@ -176,7 +177,10 @@ const walk = (root, target, options, cb) => {
176177
});
177178
};
178179

179-
exports.getChallengesForLang = async function getChallengesForLang(lang) {
180+
exports.getChallengesForLang = async function getChallengesForLang(
181+
lang,
182+
filters
183+
) {
180184
const invalidLang = !curriculumLangs.includes(lang);
181185
if (invalidLang)
182186
throw Error(`${lang} is not a accepted language.
@@ -192,11 +196,86 @@ Accepted languages are ${curriculumLangs.join(', ')}`);
192196
{ type: 'directories', depth: 0 },
193197
buildSuperBlocks
194198
);
195-
const cb = (file, curriculum) => buildChallenges(file, curriculum, lang);
199+
200+
const superBlocks = Object.keys(curriculum);
201+
const blocksWithParent = Object.entries(curriculum).flatMap(
202+
([key, superBlock]) => {
203+
const blocks = Object.entries(superBlock.blocks);
204+
return blocks.map(([block, blockData]) => ({
205+
block,
206+
blockData,
207+
superBlock: key
208+
}));
209+
}
210+
);
211+
212+
const blocks = blocksWithParent.map(({ block }) => block);
213+
214+
let filteredCurriculum = curriculum;
215+
const updatedFilters = { ...filters };
216+
if (filters?.superBlock) {
217+
const target = stringSimilarity.findBestMatch(
218+
filters.superBlock,
219+
superBlocks
220+
).bestMatch.target;
221+
222+
console.log('superBlock being tested:', target);
223+
224+
filteredCurriculum = {
225+
[target]: curriculum[target]
226+
};
227+
updatedFilters.superBlock = target;
228+
} else if (filters?.block) {
229+
const target = stringSimilarity.findBestMatch(filters.block, blocks)
230+
.bestMatch.target;
231+
232+
console.log('block being tested:', target);
233+
const targetBlock = blocksWithParent.find(({ block }) => block === target);
234+
235+
filteredCurriculum = {
236+
[targetBlock.superBlock]: {
237+
blocks: {
238+
[targetBlock.block]: targetBlock.blockData
239+
}
240+
}
241+
};
242+
updatedFilters.block = targetBlock.block;
243+
} else if (filters?.challengeId) {
244+
const blocksWithMeta = blocksWithParent.filter(
245+
({ blockData }) => blockData.meta
246+
);
247+
const container = blocksWithMeta.filter(({ block, blockData }) => {
248+
return blockData.meta.challengeOrder.some(
249+
({ id }) => id === filters.challengeId
250+
);
251+
});
252+
253+
if (container.length === 0) {
254+
throw new Error(`No block found with challengeId ${filters.challengeId}`);
255+
}
256+
if (container.length > 1) {
257+
throw new Error(
258+
`Multiple blocks found with challengeId ${filters.challengeId}`
259+
);
260+
}
261+
const targetBlock = container[0];
262+
filteredCurriculum = {
263+
[targetBlock.superBlock]: {
264+
blocks: {
265+
[targetBlock.block]: targetBlock.blockData
266+
}
267+
}
268+
};
269+
updatedFilters.block = targetBlock.block;
270+
updatedFilters.superBlock = targetBlock.superBlock;
271+
}
272+
273+
const cb = (file, curriculum) =>
274+
buildChallenges(file, curriculum, lang, updatedFilters);
196275
// fill the scaffold with the challenges
197276
return walk(
198277
root,
199-
curriculum,
278+
filteredCurriculum,
200279
{ type: 'files', fileFilter: ['*.md', '*.yml'] },
201280
cb
202281
);
@@ -250,11 +329,17 @@ async function buildSuperBlocks({ path, fullPath }, curriculum) {
250329
return walk(fullPath, curriculum, { depth: 1, type: 'directories' }, cb);
251330
}
252331

253-
async function buildChallenges({ path: filePath }, curriculum, lang) {
332+
async function buildChallenges({ path: filePath }, curriculum, lang, filters) {
254333
// path is relative to getChallengesDirForLang(lang)
255334
const block = getBlockNameFromPath(filePath);
335+
if (filters?.block && block !== filters.block) {
336+
return;
337+
}
256338
const superBlockDir = getBaseDir(filePath);
257339
const superBlock = getSuperBlockFromDir(superBlockDir);
340+
if (filters?.superBlock && superBlock !== filters.superBlock) {
341+
return;
342+
}
258343
let challengeBlock;
259344

260345
// TODO: this try block and process exit can all go once errors terminate the
@@ -286,6 +371,9 @@ async function buildChallenges({ path: filePath }, curriculum, lang) {
286371
? await parseCert(englishPath)
287372
: await createChallenge(filePath, meta);
288373

374+
// this builds the entire block, even if we only want one challenge, which is
375+
// inefficient, but finding the next challenge without building the whole
376+
// block is fiddly.
289377
challengeBlock.challenges = [...challengeBlock.challenges, challenge];
290378
}
291379

curriculum/test/test-challenges.js

Lines changed: 37 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,28 @@ const { sortChallenges } = require('./utils/sort-challenges');
6060

6161
const { flatten, isEmpty, cloneDeep } = lodash;
6262

63+
if (
64+
[
65+
process.env.FCC_BLOCK,
66+
process.env.FCC_CHALLENGE_ID,
67+
process.env.FCC_SUPERBLOCK
68+
].filter(Boolean).length > 1
69+
) {
70+
throw new Error(
71+
`Please use at most single input from: block, challenge id, superblock.`
72+
);
73+
}
74+
75+
const testFilter = {
76+
block: process.env.FCC_BLOCK ? process.env.FCC_BLOCK.trim() : undefined,
77+
challengeId: process.env.FCC_CHALLENGE_ID
78+
? process.env.FCC_CHALLENGE_ID.trim()
79+
: undefined,
80+
superBlock: process.env.FCC_SUPERBLOCK
81+
? process.env.FCC_SUPERBLOCK.trim()
82+
: undefined
83+
};
84+
6385
// rethrow unhandled rejections to make sure the tests exit with non-zero code
6486
process.on('unhandledRejection', err => handleRejection(err));
6587
// If an uncaught exception gets here, then mocha is in an unexpected state. All
@@ -122,18 +144,6 @@ setup()
122144
.catch(err => handleRejection(err));
123145

124146
async function setup() {
125-
if (
126-
[
127-
process.env.FCC_BLOCK,
128-
process.env.FCC_CHALLENGE_ID,
129-
process.env.FCC_SUPERBLOCK
130-
].filter(Boolean).length > 1
131-
) {
132-
throw new Error(
133-
`Please use at most single input from: block, challenge id, superblock.`
134-
);
135-
}
136-
137147
// liveServer starts synchronously
138148
liveServer.start({
139149
host: '127.0.0.1',
@@ -165,59 +175,21 @@ async function setup() {
165175

166176
const lang = testedLang();
167177

168-
let challenges = await getChallenges(lang);
178+
let challenges = await getChallenges(lang, testFilter);
169179

170180
// the next few statements create a list of all blocks and superblocks
171181
// as they appear in the list of challenges
172-
const blocks = challenges.map(({ block }) => block);
173182
const superBlocks = challenges.map(({ superBlock }) => superBlock);
174-
const targetBlockStrings = [...new Set(blocks.filter(el => Boolean(el)))];
175183
const targetSuperBlockStrings = [
176184
...new Set(superBlocks.filter(el => Boolean(el)))
177185
];
178186

179-
// the next few statements will filter challenges based on command variables
180-
if (process.env.FCC_SUPERBLOCK) {
181-
const filter = stringSimilarity.findBestMatch(
182-
process.env.FCC_SUPERBLOCK,
183-
targetSuperBlockStrings
184-
).bestMatch.target;
185-
186-
console.log(`\nsuperBlock being tested: ${filter}`);
187-
challenges = challenges.filter(
188-
challenge => challenge.superBlock === filter
189-
);
190-
191-
if (!challenges.length) {
192-
throw new Error(`No challenges found with superBlock "${filter}"`);
193-
}
194-
}
195-
196-
if (process.env.FCC_BLOCK) {
197-
const filter = stringSimilarity.findBestMatch(
198-
process.env.FCC_BLOCK,
199-
targetBlockStrings
200-
).bestMatch.target;
201-
202-
console.log(`\nblock being tested: ${filter}`);
203-
challenges = challenges.filter(challenge => challenge.block === filter);
204-
205-
if (!challenges.length) {
206-
throw new Error(`No challenges found with block "${filter}"`);
207-
}
208-
}
209-
210-
if (process.env.FCC_CHALLENGE_ID) {
211-
console.log(
212-
`\nChallenge Id being tested: ${process.env.FCC_CHALLENGE_ID.trim()}`
213-
);
187+
if (testFilter.challengeId) {
214188
const challengeIndex = challenges.findIndex(
215-
challenge => challenge.id === process.env.FCC_CHALLENGE_ID.trim()
189+
challenge => challenge.id === testFilter.challengeId
216190
);
217191
if (challengeIndex === -1) {
218-
throw new Error(
219-
`No challenge found with id "${process.env.FCC_CHALLENGE_ID}"`
220-
);
192+
throw new Error(`No challenge found with id "${testFilter.challengeId}"`);
221193
}
222194
const { solutions = [] } = challenges[challengeIndex];
223195
if (isEmpty(solutions)) {
@@ -269,16 +241,17 @@ function runTests(challengeData) {
269241
run();
270242
}
271243

272-
async function getChallenges(lang) {
273-
const challenges = await getChallengesForLang(lang).then(curriculum =>
274-
Object.keys(curriculum)
275-
.map(key => curriculum[key].blocks)
276-
.reduce((challengeArray, superBlock) => {
277-
const challengesForBlock = Object.keys(superBlock).map(
278-
key => superBlock[key].challenges
279-
);
280-
return [...challengeArray, ...flatten(challengesForBlock)];
281-
}, [])
244+
async function getChallenges(lang, filters) {
245+
const challenges = await getChallengesForLang(lang, filters).then(
246+
curriculum =>
247+
Object.keys(curriculum)
248+
.map(key => curriculum[key].blocks)
249+
.reduce((challengeArray, superBlock) => {
250+
const challengesForBlock = Object.keys(superBlock).map(
251+
key => superBlock[key].challenges
252+
);
253+
return [...challengeArray, ...flatten(challengesForBlock)];
254+
}, [])
282255
);
283256
// This matches the order Gatsby uses (via a GraphQL query). Ideally both
284257
// should be sourced and sorted using a single query, but we're not there yet.

0 commit comments

Comments
 (0)