Skip to content

Commit e4f5c19

Browse files
authoredJun 24, 2020
Merge pull request #48 from coderoad/feature/markdown-as-master
Feature/markdown as master
2 parents b34375b + aa745f9 commit e4f5c19

18 files changed

+551
-316
lines changed
 

‎README.md

+10-14
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ The configuration file is created by matching the `level` and `step` ids between
4848

4949
Tutorial description.
5050

51-
## L1 This is a level with id = 1
51+
## 1. This is a level with id = 1
5252

5353
This level has two steps...
5454

55-
### L1S1 First step
55+
### 1.1 First step
5656

5757
The first step with id L1S1. The Step id should start with the level id.
5858

@@ -62,7 +62,7 @@ The first step with id L1S1. The Step id should start with the level id.
6262
- The second hint that will show
6363
- The third and final hint, as it is last in order
6464

65-
### L1S2 The second step
65+
### 1.2 The second step
6666

6767
The second step...
6868
```
@@ -72,10 +72,10 @@ The second step...
7272
```yaml
7373
---
7474
levels:
75-
- id: L1
75+
- id: "1"
7676
config: {}
7777
steps:
78-
- id: L1S1
78+
- id: "1.1"
7979
setup:
8080
files:
8181
- package.json
@@ -86,7 +86,7 @@ levels:
8686
- package.json
8787
commands:
8888
- npm install
89-
- id: L1S2
89+
- id: "1.2"
9090
setup:
9191
files:
9292
- src/server.js
@@ -104,23 +104,19 @@ commit 8e0e3a42ae565050181fdb68298114df21467a74 (HEAD -> v2, origin/v2)
104104
Author: creator <author@email.com>
105105
Date: Sun May 3 16:16:01 2020 -0700
106106

107-
L1S1Q setup step 1 for level 1
107+
1.1 setup for level 1, step 1
108108

109109
commit 9499611fc9b311040dcabaf2d98439fc0c356cc9
110110
Author: creator <author@email.com>
111111
Date: Sun May 3 16:13:37 2020 -0700
112112

113-
L1S2A checkout solution for level 1, step 2
113+
1.1S solution for level 1, step 1
114114

115115
commit c5c62041282579b495d3589b2eb1fdda2bcd7155
116116
Author: creator <author@email.com>
117117
Date: Sun May 3 16:11:42 2020 -0700
118118

119-
L1S2Q setup level 1, step 2
119+
1.2 setup for level 1, step 2
120120
```
121121
122-
Note that the step `L1S2` has two commits, one with the suffix `Q` and another one with `A`. The suffixes mean `Question` and `Answer`, respectively, and refer to the unit tests and the commit that makes them pass.
123-
124-
Steps defined as questions are **required** as they are meant to set the task to be executed by the student. The answer is optional and should be used when a commit must be loaded to verify the student's solution.
125-
126-
If there are multiple commits for a level or step, they are captured in order.
122+
Note that the step `1.1` has two commits, one with the suffix `S`. The first commit refers to the required tests and setup, while the second optional commit contains the solution. If there are multiple commits for a level or step, they are captured in order.

‎package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coderoad/cli",
3-
"version": "0.3.1",
3+
"version": "0.4.0",
44
"description": "A CLI to build the configuration file for Coderoad Tutorials",
55
"keywords": [
66
"coderoad",

‎src/build.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ async function build(args: string[]) {
102102
try {
103103
const valid = validateSchema(skeletonSchema, skeleton);
104104
if (!valid) {
105-
console.error("Tutorial validation failed. See above to see what to fix");
105+
console.error("Skeleton validation failed. See above to see what to fix");
106106
return;
107107
}
108108
} catch (e) {

‎src/schema/meta.ts

+35
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,40 @@ export default {
9595
},
9696
additionalProperties: false,
9797
},
98+
setup_action_without_commits: {
99+
type: "object",
100+
description:
101+
"A collection of files/commands that run when a level/step or solution is loaded",
102+
properties: {
103+
files: {
104+
$ref: "#/definitions/file_array",
105+
},
106+
commands: {
107+
$ref: "#/definitions/command_array",
108+
},
109+
watchers: {
110+
type: "array",
111+
items: {
112+
$ref: "#/definitions/file_path",
113+
// uniqueItems: true,
114+
},
115+
description:
116+
"An array file paths that, when updated, will trigger the test runner to run",
117+
},
118+
filter: {
119+
type: "string",
120+
description:
121+
"A regex pattern that will be passed to the test runner to limit the number of tests running",
122+
examples: ["^TestSuiteName"],
123+
},
124+
subtasks: {
125+
type: "boolean",
126+
description:
127+
'A feature that shows subtasks: all active test names and the status of the tests (pass/fail). Use together with "filter"',
128+
examples: [true],
129+
},
130+
},
131+
additionalProperties: false,
132+
},
98133
},
99134
};

‎src/schema/skeleton.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ export default {
5353
examples: ["coderoad"],
5454
},
5555
setup: {
56-
$ref: "#/definitions/setup_action",
56+
$ref: "#/definitions/setup_action_without_commits",
5757
description:
58-
"Setup commits or commands used for setting up the test runner on tutorial launch",
58+
"Setup actions or commands used for setting up the test runner on tutorial launch",
5959
},
6060
},
6161
required: ["command", "args"],
@@ -135,9 +135,9 @@ export default {
135135
examples: ["L1", "L11"],
136136
},
137137
setup: {
138-
$ref: "#/definitions/setup_action",
138+
$ref: "#/definitions/setup_action_without_commits",
139139
description:
140-
"An optional point for loading commits, running commands or opening files",
140+
"An optional point for running actions, commands or opening files",
141141
},
142142
steps: {
143143
type: "array",
@@ -152,18 +152,18 @@ export default {
152152
setup: {
153153
allOf: [
154154
{
155-
$ref: "#/definitions/setup_action",
155+
$ref: "#/definitions/setup_action_without_commits",
156156
description:
157-
"A point for loading commits. It can also run commands and/or open files",
157+
"A point for running actions, commands and/or opening files",
158158
},
159159
],
160160
},
161161
solution: {
162162
allOf: [
163163
{
164-
$ref: "#/definitions/setup_action",
164+
$ref: "#/definitions/setup_action_without_commits",
165165
description:
166-
"The solution commits that can be loaded if the user gets stuck. It can also run commands and/or open files",
166+
"The solution can be loaded if the user gets stuck. It can run actions, commands and/or open files",
167167
},
168168
{
169169
required: [],

‎src/schema/tutorial.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ export default {
211211
examples: ["Have you tried doing X?"],
212212
},
213213
},
214-
required: ["content", "setup", "solution"],
214+
required: ["content", "setup"],
215215
},
216216
},
217217
},

‎src/utils/commits.ts

+53-38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from "fs";
22
import util from "util";
33
import * as path from "path";
4+
import { ListLogSummary } from "simple-git/typings/response";
45
import gitP, { SimpleGit } from "simple-git/promise";
56
import { validateCommitOrder } from "./validateCommits";
67

@@ -15,6 +16,54 @@ type GetCommitOptions = {
1516

1617
export type CommitLogObject = { [position: string]: string[] };
1718

19+
20+
21+
export function parseCommits(logs: ListLogSummary<any>): { [hash: string]: string[]} {
22+
// Filter relevant logs
23+
const commits: CommitLogObject = {};
24+
const positions: string[] = [];
25+
26+
for (const commit of logs.all) {
27+
const matches = commit.message.match(
28+
/^(?<init>INIT)|(L?(?<levelId>\d+)[S|\.]?(?<stepId>\d+)?(?<stepType>[Q|A|T|S])?)/
29+
);
30+
31+
if (matches && matches.length) {
32+
// Use an object of commit arrays to collect all commits
33+
const { groups } = matches
34+
let position
35+
if (groups.init) {
36+
position = 'INIT'
37+
} else if (groups.levelId && groups.stepId) {
38+
let stepType
39+
// @deprecated Q
40+
if (!groups.stepType || ['Q', 'T'].includes(groups.stepType)) {
41+
stepType = 'T' // test
42+
// @deprecated A
43+
} else if (!groups.stepType || ['A', 'S'].includes(groups.stepType)) {
44+
stepType = 'S' // solution
45+
}
46+
position = `${groups.levelId}.${groups.stepId}:${stepType}`
47+
} else if (groups.levelId) {
48+
position = groups.levelId
49+
} else {
50+
console.warn(`No matcher for commit "${commit.message}"`)
51+
}
52+
commits[position] = [...(commits[position] || []), commit.hash]
53+
positions.unshift(position);
54+
} else {
55+
const initMatches = commit.message.match(/^INIT/);
56+
if (initMatches && initMatches.length) {
57+
commits.INIT = [...(commits.INIT || []), commit.hash]
58+
positions.unshift("INIT");
59+
}
60+
}
61+
}
62+
// validate order
63+
validateCommitOrder(positions);
64+
return commits;
65+
}
66+
1867
export async function getCommits({
1968
localDir,
2069
codeBranch,
@@ -49,48 +98,16 @@ export async function getCommits({
4998
// track the original branch in case of failure
5099
const originalBranch = branches.current;
51100

52-
// Filter relevant logs
53-
const commits: CommitLogObject = {};
54-
55101
try {
56102
// Checkout the code branches
57103
await git.checkout(codeBranch);
58104

59105
// Load all logs
60106
const logs = await git.log();
61-
const positions: string[] = [];
62-
63-
for (const commit of logs.all) {
64-
const matches = commit.message.match(
65-
/^(?<stepId>(?<levelId>L\d+)(S\d+))(?<stepType>[QA])?/
66-
);
67-
68-
if (matches && matches.length) {
69-
// Use an object of commit arrays to collect all commits
70-
const position = matches[0];
71-
if (!commits[position]) {
72-
// does not exist, create the list
73-
commits[position] = [commit.hash];
74-
} else {
75-
// add to the list
76-
commits[position].push(commit.hash);
77-
}
78-
positions.unshift(position);
79-
} else {
80-
const initMatches = commit.message.match(/^INIT/);
81-
if (initMatches && initMatches.length) {
82-
if (!commits.INIT) {
83-
// does not exist, create the list
84-
commits.INIT = [commit.hash];
85-
} else {
86-
// add to the list
87-
commits.INIT.push(commit.hash);
88-
}
89-
positions.unshift("INIT");
90-
}
91-
}
92-
}
93-
validateCommitOrder(positions);
107+
108+
const commits = parseCommits(logs);
109+
110+
return commits;
94111
} catch (e) {
95112
console.error("Error with checkout or commit matching");
96113
throw new Error(e.message);
@@ -100,6 +117,4 @@ export async function getCommits({
100117
// cleanup the tmp directory
101118
await rmdir(tmpDir, { recursive: true });
102119
}
103-
104-
return commits;
105120
}

‎src/utils/parse.ts

+62-65
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ import * as T from "../../typings/tutorial";
44

55
type TutorialFrame = {
66
summary: T.TutorialSummary;
7-
levels: {
8-
[levelKey: string]: T.Level;
9-
};
10-
steps: { [stepKey: string]: Partial<T.Step> };
7+
levels: T.Level[];
118
};
129

1310
export function parseMdContent(md: string): TutorialFrame | never {
@@ -33,8 +30,7 @@ export function parseMdContent(md: string): TutorialFrame | never {
3330
title: "",
3431
description: "",
3532
},
36-
levels: {},
37-
steps: {},
33+
levels: [],
3834
};
3935

4036
// Capture summary
@@ -49,23 +45,20 @@ export function parseMdContent(md: string): TutorialFrame | never {
4945
mdContent.summary.description = summaryMatch.groups.tutorialDescription.trim();
5046
}
5147

52-
let current = { level: "0", step: "0" };
48+
let current = { level: -1, step: -1 };
5349
// Identify each part of the content
5450
parts.forEach((section: string) => {
5551
// match level
56-
const levelRegex = /^(#{2}\s(?<levelId>L\d+)\s(?<levelTitle>.*)[\n\r]*(>\s(?<levelSummary>.*))?[\n\r]+(?<levelContent>[^]*))/;
52+
const levelRegex = /^(#{2}\s(?<levelId>L?\d+\.?)\s(?<levelTitle>.*)[\n\r]*(>\s(?<levelSummary>.*))?[\n\r]+(?<levelContent>[^]*))/;
5753
const levelMatch: RegExpMatchArray | null = section.match(levelRegex);
54+
5855
if (levelMatch && levelMatch.groups) {
59-
const {
60-
levelId,
61-
levelTitle,
62-
levelSummary,
63-
levelContent,
64-
} = levelMatch.groups;
56+
current = { level: current.level + 1, step: -1 };
57+
const { levelTitle, levelSummary, levelContent } = levelMatch.groups;
6558

6659
// @ts-ignore
67-
mdContent.levels[levelId] = {
68-
id: levelId,
60+
mdContent.levels[current.level] = {
61+
id: (current.level + 1).toString(),
6962
title: levelTitle.trim(),
7063
summary:
7164
levelSummary && levelSummary.trim().length
@@ -75,23 +68,22 @@ export function parseMdContent(md: string): TutorialFrame | never {
7568
omission: "...",
7669
}),
7770
content: levelContent.trim(),
71+
steps: [],
7872
};
79-
current = { level: levelId, step: "0" };
8073
} else {
8174
// match step
82-
const stepRegex = /^(#{3}\s(?<stepId>(?<levelId>L\d+)S\d+)\s(?<stepTitle>.*)[\n\r]+(?<stepContent>[^]*))/;
75+
const stepRegex = /^(#{3}\s(?<stepTitle>.*)[\n\r]+(?<stepContent>[^]*))/;
8376
const stepMatch: RegExpMatchArray | null = section.match(stepRegex);
8477
if (stepMatch && stepMatch.groups) {
78+
current = { level: current.level, step: current.step + 1 };
8579
const { stepId, stepContent } = stepMatch.groups;
86-
87-
mdContent.steps[stepId] = {
88-
id: stepId,
80+
mdContent.levels[current.level].steps[current.step] = {
81+
id: `${current.level + 1}.${current.step + 1}`,
8982
content: stepContent.trim(),
9083
};
91-
current = { ...current, step: stepId };
9284
} else {
9385
// parse hints from stepContent
94-
const hintDetectRegex = /^(#{4}\sHINTS[\n\r]+(\*\s(?<hintContent>[^]*))[\n\r]+)+/;
86+
const hintDetectRegex = /^(#{4}\sHINTS[\n\r]+([\*|\-]\s(?<hintContent>[^]*))[\n\r]+)+/;
9587
const hintMatch = section.match(hintDetectRegex);
9688
if (!!hintMatch) {
9789
const hintItemRegex = /[\n\r]+\*\s/;
@@ -100,7 +92,7 @@ export function parseMdContent(md: string): TutorialFrame | never {
10092
.slice(1) // remove #### HINTS
10193
.map((h) => h.trim());
10294
if (hints.length) {
103-
mdContent.steps[current.step].hints = hints;
95+
mdContent.levels[current.level].steps[current.step].hints = hints;
10496
}
10597
}
10698
}
@@ -135,33 +127,30 @@ export function parse(params: ParseParams): any {
135127
};
136128
}
137129

138-
// merge content and tutorial
139-
if (params.skeleton.levels && params.skeleton.levels.length) {
140-
parsed.levels = params.skeleton.levels
141-
.map((level: T.Level, levelIndex: number) => {
142-
const levelContent = mdContent.levels[level.id];
130+
// merge content levels and tutorial
143131

144-
if (!levelContent) {
145-
return null;
146-
}
132+
parsed.levels = mdContent.levels.map(
133+
(mdLevel: T.Level, mdLevelIndex: number) => {
134+
// add level setup commits
135+
let level: T.Level = { ...mdLevel };
147136

148-
level = { ...level, ...levelContent };
149-
150-
// add level setup commits
151-
const levelSetupKey = level.id;
152-
if (params.commits[levelSetupKey]) {
153-
level.setup = {
154-
...(level.setup || {}),
155-
commits: params.commits[levelSetupKey],
156-
};
157-
}
137+
const configLevel = params.skeleton.levels[mdLevelIndex];
158138

139+
if (configLevel) {
159140
// add level step commits
160-
try {
161-
level.steps = (level.steps || []).map(
162-
(step: T.Step, stepIndex: number) => {
163-
const stepKey = `${levelSetupKey}S${stepIndex + 1}`;
164-
const stepSetupKey = `${stepKey}Q`;
141+
const { steps, ...configLevelProps } = configLevel;
142+
level = { ...configLevelProps, ...level };
143+
if (steps) {
144+
steps.forEach((step: T.Step, index: number) => {
145+
try {
146+
const mdStep = level.steps[index];
147+
148+
step = {
149+
...step,
150+
...mdStep,
151+
};
152+
153+
const stepSetupKey = `${step.id}:T`;
165154
if (params.commits[stepSetupKey]) {
166155
if (!step.setup) {
167156
step.setup = {
@@ -171,7 +160,7 @@ export function parse(params: ParseParams): any {
171160
step.setup.commits = params.commits[stepSetupKey];
172161
}
173162

174-
const stepSolutionKey = `${stepKey}A`;
163+
const stepSolutionKey = `${step.id}:S`;
175164
if (params.commits[stepSolutionKey]) {
176165
if (!step.solution) {
177166
step.solution = {
@@ -180,27 +169,35 @@ export function parse(params: ParseParams): any {
180169
}
181170
step.solution.commits = params.commits[stepSolutionKey];
182171
}
172+
} catch (error) {
173+
console.error("Error parsing level steps");
174+
console.warn(JSON.stringify(level.steps));
175+
console.error(error.message);
176+
}
177+
// update level step
178+
level.steps[index] = step;
179+
});
180+
}
181+
}
183182

184-
// add markdown
185-
const stepMarkdown: Partial<T.Step> = mdContent.steps[step.id];
186-
if (stepMarkdown) {
187-
step = { ...step, ...stepMarkdown };
188-
}
183+
if (params.commits[level.id]) {
184+
if (!level.setup) {
185+
level.setup = {};
186+
}
187+
level.setup.commits = params.commits[level.id];
188+
}
189189

190-
step.id = `${stepKey}`;
191-
return step;
192-
}
193-
);
194-
} catch (error) {
195-
console.log(JSON.stringify(level.steps));
196-
console.error("Error parsing level steps");
197-
console.error(error.message);
190+
// @deprecated L1 system
191+
if (params.commits[`L${level.id}`]) {
192+
if (!level.setup) {
193+
level.setup = {};
198194
}
195+
level.setup.commits = params.commits[`L${level.id}`];
196+
}
199197

200-
return level;
201-
})
202-
.filter((l: T.Level | null) => !!l);
203-
}
198+
return level;
199+
}
200+
);
204201

205202
return parsed;
206203
}

‎src/utils/validateCommits.ts

+35-9
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,61 @@
33
export function validateCommitOrder(positions: string[]): boolean {
44
// loop over positions
55
const errors: number[] = [];
6-
let previous = { level: 0, step: 0 };
7-
let current = { level: 0, step: 0 };
6+
let previous = { level: 0, step: 0, type: "" };
7+
let current = { level: 0, step: 0, type: "" };
88
positions.forEach((position: string, index: number) => {
99
if (position === "INIT") {
1010
if (previous.level !== 0 && previous.step !== 0) {
1111
errors.push(index);
1212
}
13-
current = { level: 0, step: 0 };
13+
current = { level: 0, step: 0, type: "" };
1414
return;
1515
} else {
16-
const levelMatch = position.match(/^L([0-9]+)Q?$/);
17-
const stepMatch = position.match(/^L([0-9]+)S([0-9]+)[Q|A]?$/);
16+
// @deprecate - remove L|Q
17+
const levelMatch = position.match(/^(?<level>[0-9]+)$/);
18+
// @deprecate - remove S|Q|A
19+
const stepMatch = position.match(
20+
/^(?<level>[0-9]+)\.(?<step>[0-9]+):(?<stepType>[T|S])$/
21+
);
1822
if (levelMatch) {
1923
// allows next level or step
20-
const [_, levelString] = levelMatch;
24+
const levelString = levelMatch?.groups?.level;
25+
if (!levelString) {
26+
console.warn(`No commit level match for ${position}`);
27+
return;
28+
}
2129
const level = Number(levelString);
22-
current = { level, step: 0 };
30+
current = { level, step: 0, type: "" };
2331
} else if (stepMatch) {
2432
// allows next level or step
25-
const [_, levelString, stepString] = stepMatch;
33+
if (!stepMatch?.groups?.level || !stepMatch?.groups.step) {
34+
console.warn(`No commit step match for ${position}`);
35+
return;
36+
}
37+
const { level: levelString, step: stepString } = stepMatch.groups;
38+
2639
const level = Number(levelString);
2740
const step = Number(stepString);
28-
current = { level, step };
41+
const type = stepMatch?.groups.stepType;
42+
43+
const sameStep = previous.level === level && previous.step === step;
44+
45+
if (
46+
// tests should come before the solution
47+
(sameStep && type === "T" && previous.type === "S") ||
48+
// step should have tests
49+
(!sameStep && type === "S")
50+
) {
51+
errors.push(index);
52+
}
53+
current = { level, step, type };
2954
} else {
3055
// error
3156
console.warn(`Invalid commit position: ${position}`);
3257
return;
3358
}
3459
if (
60+
// levels or steps are out of order
3561
current.level < previous.level ||
3662
(current.level === previous.level && current.step < previous.step)
3763
) {

‎src/utils/validateMarkdown.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,23 @@ const validations: Validation[] = [
2424
},
2525
},
2626
{
27-
message: "should have a level `##` with a format of `L[0-9]+`",
27+
message: "should have a level `##` with a format of `[0-9]+.`",
2828
validate: (t) => {
2929
const headers = t.match(/^#{2}\s(.+)$/gm) || [];
3030
for (const header of headers) {
31-
if (!header.match(/^#{2}\s(L\d+)\s(.+)$/)) {
31+
if (!header.match(/^#{2}\s(\d+\.)\s(.+)$/)) {
3232
return false;
3333
}
3434
}
3535
return true;
3636
},
3737
},
3838
{
39-
message: "should have a step `###` with a format of `L[0-9]+S[0-9]+`",
39+
message: "should have a step `###` with a format of `[0-9].[0-9]+`",
4040
validate: (t) => {
4141
const headers = t.match(/^#{3}\s(.+)$/gm) || [];
4242
for (const header of headers) {
43-
if (!header.match(/^#{3}\s(L\d+)S\d+/)) {
43+
if (!header.match(/^#{3}\s(\d+\.\d+)/)) {
4444
return false;
4545
}
4646
}
@@ -60,9 +60,9 @@ export function validateMarkdown(md: string): boolean {
6060
for (const v of validations) {
6161
if (!v.validate(text)) {
6262
valid = false;
63-
if (process.env.NODE_ENV !== "test") {
64-
console.warn(v.message);
65-
}
63+
// if (process.env.NODE_ENV !== "test") {
64+
console.warn(v.message);
65+
// }
6666
}
6767
}
6868

‎tests/commitOrder.test.ts

+56-42
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,61 @@
11
import { validateCommitOrder } from "../src/utils/validateCommits";
22

33
describe("commitOrder", () => {
4-
it("should return true if order is valid", () => {
5-
const positions = ["INIT", "L1", "L1S1", "L1S2", "L2", "L2S1"];
6-
const result = validateCommitOrder(positions);
7-
expect(result).toBe(true);
8-
});
9-
it("should return true if valid with duplicates", () => {
10-
const positions = [
11-
"INIT",
12-
"INIT",
13-
"L1",
14-
"L1",
15-
"L1S1",
16-
"L1S1",
17-
"L1S2",
18-
"L1S2",
19-
"L2",
20-
"L2",
21-
"L2S1",
22-
"L2S1",
23-
];
24-
const result = validateCommitOrder(positions);
25-
expect(result).toBe(true);
26-
});
27-
it("should return false if INIT is out of order", () => {
28-
const positions = ["INIT", "L1", "L1S1", "L1S2", "INIT", "L2", "L2S1"];
29-
const result = validateCommitOrder(positions);
30-
expect(result).toBe(false);
31-
});
32-
it("should return false if level after step is out of order", () => {
33-
const positions = ["INIT", "L1", "L1S1", "L1S2", "L2S1", "L2"];
34-
const result = validateCommitOrder(positions);
35-
expect(result).toBe(false);
36-
});
37-
it("should return false if level is out of order", () => {
38-
const positions = ["INIT", "L1", "L3", "L2"];
39-
const result = validateCommitOrder(positions);
40-
expect(result).toBe(false);
41-
});
42-
it("should return false if step is out of order", () => {
43-
const positions = ["INIT", "L1", "L1S1", "L1S3", "L1S2"];
44-
const result = validateCommitOrder(positions);
45-
expect(result).toBe(false);
4+
describe("#.# format", () => {
5+
it("should return true if order is valid", () => {
6+
const positions = ["INIT", "1", "1.1:T", "1.2:T", "2", "2.1:T"];
7+
const result = validateCommitOrder(positions);
8+
expect(result).toBe(true);
9+
});
10+
it("should return true if valid with duplicates", () => {
11+
const positions = [
12+
"INIT",
13+
"INIT",
14+
"1",
15+
"1",
16+
"1.1:T",
17+
"1.1:T",
18+
"1.1:S",
19+
"1.1:S",
20+
"1.2:T",
21+
"1.2:S",
22+
"2",
23+
"2",
24+
"2.1:T",
25+
"2.1:S",
26+
];
27+
const result = validateCommitOrder(positions);
28+
expect(result).toBe(true);
29+
});
30+
it("should return false if INIT is out of order", () => {
31+
const positions = ["INIT", "1", "1.1:T", "1.2:T", "INIT", "2", "2.1:T"];
32+
const result = validateCommitOrder(positions);
33+
expect(result).toBe(false);
34+
});
35+
it("should return false if level after step is out of order", () => {
36+
const positions = ["INIT", "1", "1.1:T", "1.2:T", "2.1:T", "2"];
37+
const result = validateCommitOrder(positions);
38+
expect(result).toBe(false);
39+
});
40+
it("should return false if level is out of order", () => {
41+
const positions = ["INIT", "1", "3", "2"];
42+
const result = validateCommitOrder(positions);
43+
expect(result).toBe(false);
44+
});
45+
it("should return false if step is out of order", () => {
46+
const positions = ["INIT", "1", "1.1:T", "1.3:T", "1.2:T"];
47+
const result = validateCommitOrder(positions);
48+
expect(result).toBe(false);
49+
});
50+
it("should return false if solution is before step", () => {
51+
const positions = ["INIT", "1", "1.1:S", "1.1:T", "1.2:T"];
52+
const result = validateCommitOrder(positions);
53+
expect(result).toBe(false);
54+
});
55+
it("should return false if solution but no test step", () => {
56+
const positions = ["INIT", "1", "1.1:S", "1.2:T"];
57+
const result = validateCommitOrder(positions);
58+
expect(result).toBe(false);
59+
});
4660
});
4761
});

‎tests/commitParse.test.ts

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { parseCommits } from "../src/utils/commits";
2+
3+
describe("commitParse", () => {
4+
it("should parse out #. commits", () => {
5+
const logs = {
6+
all: [
7+
{
8+
message: "INIT",
9+
hash: "1",
10+
},
11+
{
12+
message: "1. First Level",
13+
hash: "2",
14+
},
15+
{
16+
message: "1.1 First Step",
17+
hash: "3",
18+
},
19+
],
20+
total: 2,
21+
latest: {},
22+
};
23+
const commits = parseCommits(logs);
24+
expect(commits).toEqual({
25+
INIT: ["1"],
26+
"1": ["2"],
27+
"1.1:T": ["3"],
28+
});
29+
});
30+
// @deprecated - remove L#
31+
it("should parse out L# commits", () => {
32+
const logs = {
33+
all: [
34+
{
35+
message: "INIT",
36+
hash: "1",
37+
},
38+
{
39+
message: "L1 First Level",
40+
hash: "2",
41+
},
42+
{
43+
message: "L1S1 First Step",
44+
hash: "3",
45+
},
46+
],
47+
total: 2,
48+
latest: {},
49+
};
50+
const commits = parseCommits(logs);
51+
expect(commits).toEqual({
52+
INIT: ["1"],
53+
"1": ["2"],
54+
"1.1:T": ["3"],
55+
});
56+
});
57+
// @deprecated - remove with QA
58+
it("should parse out #.Q|A commits", () => {
59+
const logs = {
60+
all: [
61+
{
62+
message: "INIT",
63+
hash: "1",
64+
},
65+
{
66+
message: "1. First Level",
67+
hash: "2",
68+
},
69+
{
70+
message: "1.1Q First Step",
71+
hash: "3",
72+
},
73+
{
74+
message: "1.1A First Step Solution",
75+
hash: "4",
76+
},
77+
],
78+
total: 2,
79+
latest: {},
80+
};
81+
const commits = parseCommits(logs);
82+
expect(commits).toEqual({
83+
INIT: ["1"],
84+
"1": ["2"],
85+
"1.1:T": ["3"],
86+
"1.1:S": ["4"],
87+
});
88+
});
89+
it("should parse out #.T|S commits", () => {
90+
const logs = {
91+
all: [
92+
{
93+
message: "INIT",
94+
hash: "1",
95+
},
96+
{
97+
message: "1. First Level",
98+
hash: "2",
99+
},
100+
{
101+
message: "1.1T First Step",
102+
hash: "3",
103+
},
104+
{
105+
message: "1.1S First Step Solution",
106+
hash: "4",
107+
},
108+
],
109+
total: 2,
110+
latest: {},
111+
};
112+
const commits = parseCommits(logs);
113+
expect(commits).toEqual({
114+
INIT: ["1"],
115+
"1": ["2"],
116+
"1.1:T": ["3"],
117+
"1.1:S": ["4"],
118+
});
119+
});
120+
it("should parse out #._|S commits", () => {
121+
const logs = {
122+
all: [
123+
{
124+
message: "INIT",
125+
hash: "1",
126+
},
127+
{
128+
message: "1. First Level",
129+
hash: "2",
130+
},
131+
{
132+
message: "1.1 First Step",
133+
hash: "3",
134+
},
135+
{
136+
message: "1.1S First Step Solution",
137+
hash: "4",
138+
},
139+
],
140+
total: 2,
141+
latest: {},
142+
};
143+
const commits = parseCommits(logs);
144+
expect(commits).toEqual({
145+
INIT: ["1"],
146+
"1": ["2"],
147+
"1.1:T": ["3"],
148+
"1.1:S": ["4"],
149+
});
150+
});
151+
});

‎tests/markdown.test.ts

+11-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe("validate markdown", () => {
55
const md = `
66
Description.
77
8-
## L1 Put Level's title here
8+
## 1. Put Level's title here
99
1010
> Level's summary: a short description of the level's content in one line.
1111
@@ -19,7 +19,7 @@ Description.
1919
2020
# Another Title
2121
22-
## L1 Put Level's title here
22+
## 1. Put Level's title here
2323
2424
> Level's summary: a short description of the level's content in one line.
2525
@@ -29,7 +29,7 @@ Some text that describes the level`;
2929
Description.
3030
3131
32-
## L1 Put Level's title here
32+
## 1. Put Level's title here
3333
3434
> Level's summary: a short description of the level's content in one line.
3535
@@ -45,7 +45,7 @@ Some text that describes the level
4545
it("should return false if missing a summary description", () => {
4646
const md = `# A Title
4747
48-
## L1 Put Level's title here
48+
## 1. Put Level's title here
4949
5050
> Level's summary: a short description of the level's content in one line.
5151
@@ -79,24 +79,25 @@ A description
7979
8080
Some text that describes the level
8181
82-
### A Step
82+
### Missing step id
8383
8484
First step
8585
`;
86+
expect(validateMarkdown(md)).toBe(false);
8687
});
8788

8889
it("should return true for valid markdown", () => {
8990
const md = `# Title
9091
9192
Description.
9293
93-
## L1 Put Level's title here
94+
## 1. Put Level's title here
9495
9596
> Level's summary: a short description of the level's content in one line.
9697
9798
Some text that describes the level
9899
99-
### L1S1
100+
### 1.1
100101
101102
First Step`;
102103
expect(validateMarkdown(md)).toBe(true);
@@ -114,19 +115,19 @@ Should not be a problem
114115
\`\`\`
115116
116117
117-
## L1 Put Level's title here
118+
## 1. Put Level's title here
118119
119120
> Level's summary: a short description of the level's content in one line.
120121
121122
Some text that describes the level
122123
123124
\`\`\`
124-
## Another Level in markdown
125+
## 2. Another Level in markdown
125126
126127
Should not be an issue
127128
\`\`\`
128129
129-
### L1S1
130+
### 1.1
130131
131132
First Step`;
132133
expect(validateMarkdown(md)).toBe(true);

‎tests/parse.test.ts

+109-109
Large diffs are not rendered by default.

‎tests/skeleton.test.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const validJson = {
3030
{
3131
steps: [
3232
{
33-
id: "L1S1",
33+
id: "1.1",
3434
setup: {
3535
files: ["package.json"],
3636
},
@@ -39,7 +39,7 @@ const validJson = {
3939
},
4040
},
4141
{
42-
id: "L1S2",
42+
id: "1.2",
4343
setup: {
4444
commands: ["npm install"],
4545
},
@@ -48,7 +48,7 @@ const validJson = {
4848
},
4949
},
5050
{
51-
id: "L1S3",
51+
id: "1.3",
5252
setup: {
5353
files: ["package.json"],
5454
watchers: ["package.json", "node_modules/some-package"],
@@ -58,15 +58,15 @@ const validJson = {
5858
},
5959
},
6060
{
61-
id: "L1S4",
61+
id: "1.4",
6262
setup: {
6363
commands: [],
6464
filter: "^Example 2",
6565
subtasks: true,
6666
},
6767
},
6868
],
69-
id: "L1",
69+
id: "1",
7070
},
7171
],
7272
};
@@ -186,7 +186,7 @@ describe("validate skeleton", () => {
186186
const valid = validateSkeleton(json);
187187
expect(valid).toBe(false);
188188
});
189-
it("should fial if level is missing id", () => {
189+
it("should fail if level is missing id", () => {
190190
const level1 = { ...validJson.levels[0], id: undefined };
191191
const json = {
192192
...validJson,

‎tests/tutorial.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe("validate tutorial", () => {
3939
},
4040
levels: [
4141
{
42-
id: "L1",
42+
id: "1",
4343
title: "Level 1",
4444
summary: "The first level",
4545
content: "The first level",

‎typings/tutorial.d.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export type Level = {
2525
export type Step = {
2626
id: string;
2727
content: string;
28-
setup: StepActions;
29-
solution: Maybe<StepActions>;
28+
setup?: StepActions;
29+
solution?: Maybe<StepActions>;
3030
subtasks?: { [testName: string]: boolean };
3131
hints?: string[];
3232
};
@@ -48,7 +48,7 @@ export type TutorialSummary = {
4848

4949
export type StepActions = {
5050
commands?: string[];
51-
commits: string[];
51+
commits?: string[];
5252
files?: string[];
5353
watchers?: string[];
5454
filter?: string;

0 commit comments

Comments
 (0)
Please sign in to comment.