Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1861195

Browse files
authoredJun 13, 2020
Merge pull request #34 from coderoad/feature/validate
Feature/validate
2 parents 46b64db + 2fc1113 commit 1861195

File tree

6 files changed

+263
-42
lines changed

6 files changed

+263
-42
lines changed
 

‎package-lock.json

Lines changed: 41 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"dependencies": {
5050
"ajv": "^6.12.2",
5151
"esm": "^3.2.25",
52+
"fs-extra": "^9.0.1",
5253
"js-yaml": "^3.14.0",
5354
"kleur": "^3.0.3",
5455
"lodash": "^4.17.15",
@@ -58,6 +59,7 @@
5859
"devDependencies": {
5960
"@babel/preset-typescript": "^7.10.1",
6061
"@types/ajv": "^1.0.0",
62+
"@types/fs-extra": "^9.0.1",
6163
"@types/inquirer": "^6.5.0",
6264
"@types/jest": "^25.2.3",
6365
"@types/js-yaml": "^3.12.4",

‎src/utils/exec.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as T from "../../typings/tutorial";
2+
import { exec as cpExec } from "child_process";
3+
import * as path from "path";
4+
import { promisify } from "util";
5+
6+
const asyncExec = promisify(cpExec);
7+
8+
export function createExec(cwd: string) {
9+
return async function exec(
10+
command: string
11+
): Promise<{ stdout: string | null; stderr: string }> {
12+
try {
13+
const result = await asyncExec(command, { cwd });
14+
return result;
15+
} catch (e) {
16+
return { stdout: null, stderr: e.message };
17+
}
18+
};
19+
}
20+
21+
export function createCherryPick(cwd: string) {
22+
return async function cherryPick(commits: string[]): Promise<void> {
23+
for (const commit of commits) {
24+
try {
25+
const { stdout } = await createExec(cwd)(
26+
`git cherry-pick -X theirs ${commit}`
27+
);
28+
if (!stdout) {
29+
console.warn(`No cherry-pick output for ${commit}`);
30+
}
31+
} catch (e) {
32+
console.warn(`Cherry-pick failed for ${commit}`);
33+
}
34+
}
35+
};
36+
}
37+
38+
export function createCommandRunner(cwd: string) {
39+
return async function runCommands(
40+
commands: string[],
41+
dir?: string
42+
): Promise<boolean> {
43+
let errors = [];
44+
for (const command of commands) {
45+
try {
46+
console.log(`--> ${command}`);
47+
let cwdDir = cwd;
48+
if (dir) {
49+
cwdDir = path.join(cwd, dir);
50+
}
51+
const { stdout, stderr } = await createExec(cwdDir)(command);
52+
53+
console.warn(stderr);
54+
} catch (e) {
55+
console.error(`Command failed: "${command}"`);
56+
console.warn(e.message);
57+
errors.push(e.message);
58+
}
59+
}
60+
return !!errors.length;
61+
};
62+
}
63+
64+
// function isAbsolute(p: string) {
65+
// return path.normalize(p + "/") === path.normalize(path.resolve(p) + "/");
66+
// }
67+
68+
export function createTestRunner(cwd: string, config: T.TestRunnerConfig) {
69+
const { command, args, directory } = config;
70+
71+
// const commandIsAbsolute = isAbsolute(command);
72+
73+
let wd = cwd;
74+
if (directory) {
75+
wd = path.join(cwd, directory);
76+
}
77+
78+
const commandWithArgs = `${command} ${args.tap}`;
79+
80+
return async function runTest(): Promise<{
81+
stdout: string | null;
82+
stderr: string | null;
83+
}> {
84+
try {
85+
// console.log(await createExec(wd)("ls -a node_modules/.bin"));
86+
return await createExec(wd)(commandWithArgs);
87+
} catch (e) {
88+
return Promise.resolve({ stdout: null, stderr: e.message });
89+
}
90+
};
91+
}

‎src/validate.ts

Lines changed: 128 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import * as path from "path";
2-
import * as fs from "fs";
3-
import util from "util";
2+
import * as fs from "fs-extra";
43
import * as yamlParser from "js-yaml";
54
import { getArg } from "./utils/args";
65
import gitP, { SimpleGit } from "simple-git/promise";
6+
import {
7+
createCommandRunner,
8+
createCherryPick,
9+
createTestRunner,
10+
} from "./utils/exec";
711
import { getCommits, CommitLogObject } from "./utils/commits";
812

9-
const mkdir = util.promisify(fs.mkdir);
10-
const exists = util.promisify(fs.exists);
11-
const rmdir = util.promisify(fs.rmdir);
12-
const read = util.promisify(fs.readFile);
13-
1413
async function validate(args: string[]) {
1514
// dir - default .
1615
const dir = !args.length || args[0].match(/^-/) ? "." : args[0];
@@ -21,55 +20,146 @@ async function validate(args: string[]) {
2120
yaml: getArg(args, { name: "yaml", alias: "y" }) || "coderoad.yaml",
2221
};
2322

24-
const _yaml = await read(path.join(localDir, options.yaml), "utf8");
23+
const _yaml = await fs.readFile(path.join(localDir, options.yaml), "utf8");
2524

2625
// parse yaml config
27-
let config;
26+
let skeleton;
2827
try {
29-
config = yamlParser.load(_yaml);
30-
// TODO: validate yaml
31-
if (!config || !config.length) {
28+
skeleton = yamlParser.load(_yaml);
29+
30+
if (!skeleton) {
3231
throw new Error("Invalid yaml file contents");
3332
}
3433
} catch (e) {
3534
console.error("Error parsing yaml");
3635
console.error(e.message);
3736
}
3837

39-
const codeBranch: string = config.config.repo.branch;
38+
const codeBranch: string = skeleton.config.repo.branch;
39+
40+
// validate commits
41+
const commits: CommitLogObject = await getCommits({ localDir, codeBranch });
42+
43+
// setup tmp dir
44+
const tmpDir = path.join(localDir, ".tmp");
4045

41-
// VALIDATE SKELETON WITH COMMITS
42-
const commits = getCommits({ localDir, codeBranch });
46+
try {
47+
if (!(await fs.pathExists(tmpDir))) {
48+
await fs.emptyDir(tmpDir);
49+
}
50+
const tempGit: SimpleGit = gitP(tmpDir);
4351

44-
// parse tutorial skeleton for order and commands
52+
await tempGit.init();
53+
await tempGit.addRemote("origin", skeleton.config.repo.uri);
54+
await tempGit.fetch("origin", skeleton.config.repo.branch);
55+
// no js cherry pick implementation
56+
const cherryPick = createCherryPick(tmpDir);
57+
const runCommands = createCommandRunner(tmpDir);
58+
const runTest = createTestRunner(tmpDir, skeleton.config.testRunner);
4559

46-
// on error, warn missing level/step
60+
// setup
61+
console.info("* Setup");
62+
if (commits.INIT) {
63+
// load commits
64+
console.info("-- Loading commits...");
65+
await cherryPick(commits.INIT);
4766

48-
// VALIDATE COMMIT ORDER
49-
// list all commits in order
50-
// validate that a level number doesn't come before another level
51-
// validate that a step falls within a level
52-
// validate that steps are in order
67+
// run commands
68+
if (skeleton.config?.testRunner?.setup?.commands) {
69+
console.info("-- Running commands...");
70+
71+
await runCommands(
72+
skeleton.config?.testRunner?.setup?.commands,
73+
// add optional setup directory
74+
skeleton.config?.testRunner?.directory
75+
);
76+
}
77+
}
5378

54-
// on error, show level/step out of order
79+
for (const level of skeleton.levels) {
80+
console.info(`* ${level.id}`);
81+
if (level?.setup) {
82+
// load commits
83+
if (commits[`${level.id}`]) {
84+
console.log(`-- Loading commits...`);
85+
await cherryPick(commits[level.id]);
86+
}
87+
// run commands
88+
if (level.setup?.commands) {
89+
console.log(`-- Running commands...`);
90+
await runCommands(level.setup.commands);
91+
}
92+
}
93+
// steps
94+
if (level.steps) {
95+
for (const step of level.steps) {
96+
console.info(`** ${step.id}`);
97+
// load commits
98+
const stepSetupCommits = commits[`${step.id}Q`];
99+
if (stepSetupCommits) {
100+
console.info(`--- Loading setup commits...`);
101+
await cherryPick(stepSetupCommits);
102+
}
103+
// run commands
104+
if (step.setup.commands) {
105+
console.info(`--- Running setup commands...`);
106+
await runCommands(step.setup.commands);
107+
}
55108

56-
// VALIDATE TUTORIAL TESTS
57-
// load INIT commit(s)
58-
// run test runner setup command(s)
59-
// loop over commits:
60-
// - load level commit
61-
// - run level setup command(s)
62-
// - load step setup commit(s)
63-
// - run step setup command(s)
64-
// - if next solution:
65-
// - run test - expect fail
66-
// - if solution
67-
// - run test - expect pass
109+
const stepSolutionCommits = commits[`${step.id}A`];
110+
const hasSolution = step.solution || stepSolutionCommits;
68111

69-
// log level/step
70-
// on error, show level/step & error message
112+
// ignore running tests on steps with no solution
113+
if (hasSolution) {
114+
// run test
115+
console.info("--- Running setup test...");
116+
// expect fail
117+
const { stdout, stderr } = await runTest();
118+
if (stdout) {
119+
console.error(
120+
`--- Expected ${step.id} setup tests to fail, but passed`
121+
);
122+
// log tests
123+
console.log(stdout);
124+
}
125+
}
71126

72-
// CLEANUP
127+
if (stepSolutionCommits) {
128+
console.info(`--- Loading solution commits...`);
129+
await cherryPick(stepSolutionCommits);
130+
}
131+
132+
// run commands
133+
if (step?.solution?.commands) {
134+
console.info(`--- Running solution commands...`);
135+
await runCommands(step.solution.commands);
136+
}
137+
138+
if (hasSolution) {
139+
// run test
140+
console.info("--- Running solution test...");
141+
// expect pass
142+
const { stdout, stderr } = await runTest();
143+
if (stderr) {
144+
console.error(
145+
`--- Expected ${step.id} solution tests to pass, but failed`
146+
);
147+
// log tests
148+
console.log(stderr);
149+
}
150+
}
151+
}
152+
}
153+
}
154+
155+
console.info(`\n✔ Success!`);
156+
} catch (e) {
157+
console.error("\n✘ Fail!");
158+
console.error(e.message);
159+
} finally {
160+
// cleanup
161+
await fs.emptyDir(tmpDir);
162+
}
73163
}
74164

75165
export default validate;
File renamed without changes.

‎typings/tutorial.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export interface TestRunnerArgs {
6262

6363
export interface TestRunnerConfig {
6464
command: string;
65-
args?: TestRunnerArgs;
65+
args: TestRunnerArgs;
6666
directory?: string;
6767
setup?: StepActions;
6868
}

0 commit comments

Comments
 (0)
Please sign in to comment.