mirror of
https://github.com/denoland/deno.git
synced 2024-12-29 02:29:06 -05:00
a472b6732d
Original: 17a214bbd5
245 lines
6.6 KiB
TypeScript
Executable file
245 lines
6.6 KiB
TypeScript
Executable file
#!/usr/bin/env -S deno -A
|
|
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
|
import { parse } from "../flags/mod.ts";
|
|
import {
|
|
WalkInfo,
|
|
expandGlobSync,
|
|
glob,
|
|
ExpandGlobOptions
|
|
} from "../fs/mod.ts";
|
|
import { isWindows } from "../fs/path/constants.ts";
|
|
import { isAbsolute, join } from "../fs/path/mod.ts";
|
|
import { RunTestsOptions, runTests } from "./mod.ts";
|
|
const { DenoError, ErrorKind, args, cwd, exit } = Deno;
|
|
|
|
const DIR_GLOBS = [join("**", "?(*_)test.{js,ts}")];
|
|
|
|
function showHelp(): void {
|
|
console.log(`Deno test runner
|
|
|
|
USAGE:
|
|
deno -A https://deno.land/std/testing/runner.ts [OPTIONS] [MODULES...]
|
|
|
|
OPTIONS:
|
|
-q, --quiet Don't show output from test cases
|
|
-f, --failfast Stop running tests on first error
|
|
-e, --exclude <MODULES...> List of comma-separated modules to exclude
|
|
--allow-none Exit with status 0 even when no test modules are
|
|
found
|
|
|
|
ARGS:
|
|
[MODULES...] List of test modules to run.
|
|
A directory <dir> will expand to:
|
|
${DIR_GLOBS.map((s: string): string => `${join("<dir>", s)}`)
|
|
.join(`
|
|
`)}
|
|
Defaults to "." when none are provided.
|
|
|
|
Note that modules can refer to file paths or URLs. File paths support glob
|
|
expansion.
|
|
|
|
Examples:
|
|
deno test src/**/*_test.ts
|
|
deno test tests`);
|
|
}
|
|
|
|
function isRemoteUrl(url: string): boolean {
|
|
return /^https?:\/\//.test(url);
|
|
}
|
|
|
|
function partition(
|
|
arr: string[],
|
|
callback: (el: string) => boolean
|
|
): [string[], string[]] {
|
|
return arr.reduce(
|
|
(paritioned: [string[], string[]], el: string): [string[], string[]] => {
|
|
paritioned[callback(el) ? 1 : 0].push(el);
|
|
return paritioned;
|
|
},
|
|
[[], []]
|
|
);
|
|
}
|
|
|
|
function filePathToUrl(path: string): string {
|
|
return `file://${isWindows ? "/" : ""}${path.replace(/\\/g, "/")}`;
|
|
}
|
|
|
|
function expandDirectory(dir: string, options: ExpandGlobOptions): WalkInfo[] {
|
|
return DIR_GLOBS.flatMap((s: string): WalkInfo[] => [
|
|
...expandGlobSync(s, { ...options, root: dir })
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Given a list of globs or URLs to include and exclude and a root directory
|
|
* from which to expand relative globs, return a list of URLs
|
|
* (file: or remote) that should be imported for the test runner.
|
|
*/
|
|
export async function findTestModules(
|
|
includeModules: string[],
|
|
excludeModules: string[],
|
|
root: string = cwd()
|
|
): Promise<string[]> {
|
|
const [includePaths, includeUrls] = partition(includeModules, isRemoteUrl);
|
|
const [excludePaths, excludeUrls] = partition(excludeModules, isRemoteUrl);
|
|
|
|
const expandGlobOpts = {
|
|
root,
|
|
extended: true,
|
|
globstar: true,
|
|
filepath: true
|
|
};
|
|
|
|
// TODO: We use the `g` flag here to support path prefixes when specifying
|
|
// excludes. Replace with a solution that does this more correctly.
|
|
const excludePathPatterns = excludePaths.map(
|
|
(s: string): RegExp =>
|
|
glob(isAbsolute(s) ? s : join(root, s), { ...expandGlobOpts, flags: "g" })
|
|
);
|
|
const excludeUrlPatterns = excludeUrls.map(
|
|
(url: string): RegExp => RegExp(url)
|
|
);
|
|
const notExcludedPath = ({ filename }: WalkInfo): boolean =>
|
|
!excludePathPatterns.some((p: RegExp): boolean => !!filename.match(p));
|
|
const notExcludedUrl = (url: string): boolean =>
|
|
!excludeUrlPatterns.some((p: RegExp): boolean => !!url.match(p));
|
|
|
|
const matchedPaths = includePaths
|
|
.flatMap((s: string): WalkInfo[] => [...expandGlobSync(s, expandGlobOpts)])
|
|
.filter(notExcludedPath)
|
|
.flatMap(({ filename, info }): string[] =>
|
|
info.isDirectory()
|
|
? expandDirectory(filename, { ...expandGlobOpts, includeDirs: false })
|
|
.filter(notExcludedPath)
|
|
.map(({ filename }): string => filename)
|
|
: [filename]
|
|
);
|
|
|
|
const matchedUrls = includeUrls.filter(notExcludedUrl);
|
|
|
|
return [...matchedPaths.map(filePathToUrl), ...matchedUrls];
|
|
}
|
|
|
|
export interface RunTestModulesOptions extends RunTestsOptions {
|
|
include?: string[];
|
|
exclude?: string[];
|
|
allowNone?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Import the specified test modules and run their tests as a suite.
|
|
*
|
|
* Test modules are specified as an array of strings and can include local files
|
|
* or URLs.
|
|
*
|
|
* File matching and excluding support glob syntax - arguments recognized as
|
|
* globs will be expanded using `glob()` from the `fs` module.
|
|
*
|
|
* Example:
|
|
*
|
|
* runTestModules({ include: ["**\/*_test.ts", "**\/test.ts"] });
|
|
*
|
|
* Any matched directory `<dir>` will expand to:
|
|
* <dir>/**\/?(*_)test.{js,ts}
|
|
*
|
|
* So the above example is captured naturally by:
|
|
*
|
|
* runTestModules({ include: ["."] });
|
|
*
|
|
* Which is the default used for:
|
|
*
|
|
* runTestModules();
|
|
*/
|
|
// TODO: Change return type to `Promise<void>` once, `runTests` is updated
|
|
// to return boolean instead of exiting.
|
|
export async function runTestModules({
|
|
include = ["."],
|
|
exclude = [],
|
|
allowNone = false,
|
|
parallel = false,
|
|
exitOnFail = false,
|
|
only = /[^\s]/,
|
|
skip = /^\s*$/,
|
|
disableLog = false
|
|
}: RunTestModulesOptions = {}): Promise<void> {
|
|
const testModuleUrls = await findTestModules(include, exclude);
|
|
|
|
if (testModuleUrls.length == 0) {
|
|
const noneFoundMessage = "No matching test modules found.";
|
|
if (!allowNone) {
|
|
throw new DenoError(ErrorKind.NotFound, noneFoundMessage);
|
|
} else if (!disableLog) {
|
|
console.log(noneFoundMessage);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!disableLog) {
|
|
console.log(`Found ${testModuleUrls.length} matching test modules.`);
|
|
}
|
|
|
|
for (const url of testModuleUrls) {
|
|
await import(url);
|
|
}
|
|
|
|
await runTests({
|
|
parallel,
|
|
exitOnFail,
|
|
only,
|
|
skip,
|
|
disableLog
|
|
});
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const parsedArgs = parse(args.slice(1), {
|
|
boolean: ["allow-none", "failfast", "help", "quiet"],
|
|
string: ["exclude"],
|
|
alias: {
|
|
exclude: ["e"],
|
|
failfast: ["f"],
|
|
help: ["h"],
|
|
quiet: ["q"]
|
|
},
|
|
default: {
|
|
"allow-none": false,
|
|
failfast: false,
|
|
help: false,
|
|
quiet: false
|
|
}
|
|
});
|
|
if (parsedArgs.help) {
|
|
return showHelp();
|
|
}
|
|
|
|
const include =
|
|
parsedArgs._.length > 0
|
|
? (parsedArgs._ as string[]).flatMap((fileGlob: string): string[] =>
|
|
fileGlob.split(",")
|
|
)
|
|
: ["."];
|
|
const exclude =
|
|
parsedArgs.exclude != null ? (parsedArgs.exclude as string).split(",") : [];
|
|
const allowNone = parsedArgs["allow-none"];
|
|
const exitOnFail = parsedArgs.failfast;
|
|
const disableLog = parsedArgs.quiet;
|
|
|
|
try {
|
|
await runTestModules({
|
|
include,
|
|
exclude,
|
|
allowNone,
|
|
exitOnFail,
|
|
disableLog
|
|
});
|
|
} catch (error) {
|
|
if (!disableLog) {
|
|
console.error(error.message);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
main();
|
|
}
|