UNPKG

9.15 kBPlain TextView Raw
1import assert from "assert";
2import cp from "child_process";
3import { CloseFn as FgCloseFn, proxy as fgChildProxy } from "demurgos-foreground-child";
4import findUp from "find-up";
5import fs from "fs";
6import { fromSysPath, toSysPath } from "furi";
7import sysPath from "path";
8import Exclude from "test-exclude";
9import urlMod from "url";
10import vinylFs from "vinyl-fs";
11import yargs from "yargs";
12import { asyncDonePromise } from "./async-done-promise";
13import { CoverageFilter, fromGlob } from "./filter";
14import { GetText, getText as defaultGetText, GetTextSync, getTextSyncFromSourceStore } from "./get-text";
15import { createReporter, reportStream, reportVinyl } from "./report";
16import { Reporter, StreamReporter, VinylReporter } from "./reporter";
17import { DEFAULT_REGISTRY } from "./reporter-registry";
18import { RichProcessCov, spawnInspected } from "./spawn-inspected";
19import { processCovsToIstanbul } from "./to-istanbul";
20import { VERSION } from "./version";
21
22const DEFAULT_GLOBS: ReadonlyArray<string> = [
23 ...(<string[]> Exclude.defaultExclude.map((pattern: string) => `!${pattern}`)),
24 "**/*",
25];
26
27interface Watermarks {
28 lines: [number, number];
29 functions: [number, number];
30 branches: [number, number];
31 statements: [number, number];
32}
33
34export interface FileConfig {
35 reporters?: ReadonlyArray<string>;
36 globs?: ReadonlyArray<string>;
37 coverageDir?: string;
38 waterMarks?: Watermarks;
39}
40
41export interface CliConfig {
42 reporters?: ReadonlyArray<string>;
43 globs?: ReadonlyArray<string>;
44 coverageDir?: string;
45 command: ReadonlyArray<string>;
46}
47
48export interface ResolvedConfig {
49 reporters: ReadonlyArray<string>;
50 globs: ReadonlyArray<string>;
51 coverageDir: string;
52 waterMarks: Watermarks;
53 command: ReadonlyArray<string>;
54}
55
56export interface MessageAction {
57 action: "message";
58 message: string;
59 error?: Error;
60}
61
62export interface RunAction {
63 action: "run";
64 config: ResolvedConfig;
65}
66
67export type CliAction = MessageAction | RunAction;
68
69export type ParseArgsResult = MessageAction | {action: "run"; config: CliConfig};
70
71const DEFAULT_WATERMARKS: Watermarks = Object.freeze({
72 lines: [80, 95] as [number, number],
73 functions: [80, 95] as [number, number],
74 branches: [80, 95] as [number, number],
75 statements: [80, 95] as [number, number],
76});
77
78// TODO: Fix yargs type definition
79const ARG_PARSER: yargs.Argv = yargs() as any;
80
81ARG_PARSER
82 .scriptName("c88")
83 .version(VERSION)
84 .usage("$0 [opts] [script] [opts]")
85 .locale("en")
86 .option("reporter", {
87 alias: "r",
88 describe: "coverage reporter(s) to use",
89 default: "text",
90 })
91 .option("match", {
92 alias: "m",
93 default: DEFAULT_GLOBS,
94 // tslint:disable-next-line:max-line-length
95 describe: "a list of specific files and directories that should be matched, glob patterns are supported.",
96 })
97 .option("coverage-directory", {
98 default: "coverage",
99 describe: "directory to output coverage JSON and reports",
100 })
101 .pkgConf("c88")
102 .demandCommand(1)
103 .epilog("visit https://git.io/vHysA for list of available reporters");
104
105// tslint:disable:whitespace
106
107/**
108 * Executes the c88 CLI
109 *
110 * @param args CLI arguments
111 * @param cwd Current working directory
112 * @param proc Current process
113 */
114export async function execCli(args: string[], cwd: string, proc: NodeJS.Process): Promise<number> {
115 const action: CliAction = await getAction(args, cwd);
116
117 switch (action.action) {
118 case "message":
119 process.stderr.write(Buffer.from(action.message));
120 return action.error === undefined ? 0 : 1;
121 case "run":
122 return execRunAction(action, cwd, proc);
123 default:
124 throw new Error(`AssertionError: Unexpected \`action\`: ${(action as any).action}`);
125 }
126}
127
128function resolveConfig(fileConfig: FileConfig, cliConfig: CliConfig): ResolvedConfig {
129 return {
130 command: cliConfig.command,
131 reporters: cliConfig.reporters !== undefined ? cliConfig.reporters : ["text"],
132 globs: cliConfig.globs !== undefined ? cliConfig.globs : DEFAULT_GLOBS,
133 waterMarks: fileConfig.waterMarks !== undefined ? fileConfig.waterMarks : DEFAULT_WATERMARKS,
134 coverageDir: cliConfig.coverageDir !== undefined ? cliConfig.coverageDir : "coverage",
135 };
136}
137
138async function execRunAction({config}: RunAction, cwd: string, proc: NodeJS.Process): Promise<number> {
139 const file: string = config.command[0];
140 const args: string[] = config.command.slice(1);
141 const filter: CoverageFilter = fromGlob({patterns: config.globs, base: fromSysPath(cwd)});
142
143 const subProcessExit: DeferredPromise<number> = deferPromise();
144
145 async function onRootProcess(inspectedProc: cp.ChildProcess): Promise<void> {
146 const closeFn: FgCloseFn = await fgChildProxy(proc, inspectedProc);
147 if (closeFn.signal !== null) {
148 subProcessExit.reject(new Error(`Process killed by signal: ${closeFn.signal}`));
149 } else {
150 subProcessExit.resolve(closeFn.code!);
151 }
152 }
153
154 let processCovs: RichProcessCov[];
155 try {
156 processCovs = await spawnInspected(file, args, {filter, onRootProcess});
157 } catch (err) {
158 proc.stderr.write(Buffer.from(`${err.toString()}\n`));
159 return 1;
160 }
161 const exitCode: number = await subProcessExit.promise;
162
163 try {
164 const reporter: Reporter = createReporter(DEFAULT_REGISTRY, config.reporters, {waterMarks: config.waterMarks});
165 const resolvedCoverageDir: string = sysPath.join(cwd, config.coverageDir);
166 const coverageDir: urlMod.URL = fromSysPath(resolvedCoverageDir);
167 await report(reporter, processCovs, proc.stdout, coverageDir);
168 return exitCode;
169 } catch (err) {
170 proc.stderr.write(Buffer.from(err.toString() + "\n"));
171 return Math.max(1, exitCode);
172 }
173}
174
175export async function report(
176 reporter: Reporter,
177 processCovs: ReadonlyArray<RichProcessCov>,
178 outStream: NodeJS.WritableStream,
179 outDir: Readonly<urlMod.URL>,
180 getText: GetText = defaultGetText,
181): Promise<void> {
182 const {coverageMap, sources} = await processCovsToIstanbul(processCovs, getText);
183 const getSourcesSync: GetTextSync = getTextSyncFromSourceStore(sources);
184
185 const tasks: Promise<void>[] = [];
186 if (reporter.reportStream !== undefined) {
187 const stream: NodeJS.ReadableStream = reportStream(reporter as StreamReporter, coverageMap, getSourcesSync);
188 tasks.push(pipeData(stream, outStream));
189 }
190 if (reporter.reportVinyl !== undefined) {
191 const stream: NodeJS.ReadableStream = reportVinyl(reporter as VinylReporter, coverageMap, getSourcesSync)
192 .pipe(vinylFs.dest(toSysPath(outDir.href)));
193 tasks.push(asyncDonePromise(() => stream));
194 }
195
196 await Promise.all(tasks);
197}
198
199export async function getAction(args: string[], cwd: string): Promise<CliAction> {
200 const parsed: ParseArgsResult = parseArgs(args);
201 if (parsed.action !== "run") {
202 return parsed;
203 }
204 const fileConfig: FileConfig = await readConfigFile(cwd);
205 return {
206 action: "run",
207 config: resolveConfig(fileConfig, parsed.config),
208 };
209}
210
211export function parseArgs(args: string[]): ParseArgsResult {
212 // The yargs pure API is kinda strange to use (apart from requiring a callback):
213 // The error can either be defined, `undefined` or `null`.
214 // If it is defined or `null`, then `output` should be a non-empty string
215 // intended to be written to stderr. `parsed` is defined but it should be
216 // ignored in this case.
217 // If `err` is `undefined`, then `output` is an empty string and `parsed`
218 // contains the succesfully parsed args.
219 // tslint:disable:variable-name
220 let _err: Error | undefined | null;
221 let _parsed: any;
222 let _output: string;
223 let isParsed: boolean = false;
224 ARG_PARSER.parse(args, (err: Error | undefined | null, parsed: any, output: string): void => {
225 _err = err;
226 _parsed = parsed;
227 _output = output;
228 isParsed = true;
229 });
230 assert(isParsed);
231 const err: Error | undefined | null = _err!;
232 const parsed: any = _parsed!;
233 const output: string = _output!;
234 if (err === null) {
235 // Successfully parsed
236 return {
237 action: "run",
238 config: {
239 command: parsed._,
240 reporters: Array.isArray(parsed.reporter) ? parsed.reporter : [parsed.reporter],
241 globs: parsed.match,
242 },
243 };
244 } else {
245 return {action: "message", message: output, error: err};
246 }
247}
248
249async function readConfigFile(_cwd: string): Promise<FileConfig> {
250 const configPath: string | undefined = findUp.sync([".c88rc", ".c88rc.json"]);
251 if (configPath === undefined) {
252 return Object.create(null);
253 }
254 return JSON.parse(fs.readFileSync(configPath, "UTF-8"));
255}
256
257interface DeferredPromise<T> {
258 promise: Promise<T>;
259
260 resolve(value: T): void;
261
262 reject(reason: any): void;
263}
264
265function deferPromise<T>(): DeferredPromise<T> {
266 let resolve: (value: T) => void;
267 let reject: (reason: any) => void;
268 const promise: Promise<T> = new Promise<T>((res, rej) => {
269 resolve = res;
270 reject = rej;
271 });
272 return {resolve: resolve!, reject: reject!, promise};
273}
274
275function pipeData(src: NodeJS.ReadableStream, dest: NodeJS.WritableStream): Promise<void> {
276 return new Promise<void>((resolve, reject) => {
277 src.on("data", chunk => dest.write(chunk));
278 src.on("error", reject);
279 src.on("end", resolve);
280 });
281}
282
\No newline at end of file