mirror of
https://github.com/denoland/deno.git
synced 2024-11-23 15:16:54 -05:00
parent
af18093498
commit
a472b6732d
25 changed files with 444 additions and 166 deletions
|
@ -4,5 +4,5 @@ parameters:
|
||||||
steps:
|
steps:
|
||||||
- bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-write --allow-read --allow-env ./format.ts --check
|
- bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-write --allow-read --allow-env ./format.ts --check
|
||||||
- bash: export START_TIME=$(date +%s)
|
- bash: export START_TIME=$(date +%s)
|
||||||
- bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-net --allow-write --allow-read --allow-env --config=tsconfig.test.json ./testing/runner.ts --exclude node_modules
|
- bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-net --allow-write --allow-read --allow-env --config=tsconfig.test.json ./testing/runner.ts --exclude node_modules,**/testdata
|
||||||
- bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-read .ci/check_source_file_changes.ts $START_TIME
|
- bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-read .ci/check_source_file_changes.ts $START_TIME
|
67
fs/glob.ts
67
fs/glob.ts
|
@ -1,4 +1,7 @@
|
||||||
import { globrex } from "./globrex.ts";
|
import { globrex } from "./globrex.ts";
|
||||||
|
import { isAbsolute, join } from "./path/mod.ts";
|
||||||
|
import { WalkInfo, walk, walkSync } from "./walk.ts";
|
||||||
|
const { cwd } = Deno;
|
||||||
|
|
||||||
export interface GlobOptions {
|
export interface GlobOptions {
|
||||||
// Allow ExtGlob features
|
// Allow ExtGlob features
|
||||||
|
@ -41,7 +44,11 @@ export interface GlobOptions {
|
||||||
* @returns A RegExp for the glob pattern
|
* @returns A RegExp for the glob pattern
|
||||||
*/
|
*/
|
||||||
export function glob(glob: string, options: GlobOptions = {}): RegExp {
|
export function glob(glob: string, options: GlobOptions = {}): RegExp {
|
||||||
return globrex(glob, options).regex;
|
const result = globrex(glob, options);
|
||||||
|
if (options.filepath) {
|
||||||
|
return result.path!.regex;
|
||||||
|
}
|
||||||
|
return result.regex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test whether the given string is a glob */
|
/** Test whether the given string is a glob */
|
||||||
|
@ -76,3 +83,61 @@ export function isGlob(str: string): boolean {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExpandGlobOptions extends GlobOptions {
|
||||||
|
root?: string;
|
||||||
|
includeDirs?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand the glob string from the specified `root` directory and yield each
|
||||||
|
* result as a `WalkInfo` object.
|
||||||
|
*/
|
||||||
|
// TODO: Use a proper glob expansion algorithm.
|
||||||
|
// This is a very incomplete solution. The whole directory tree from `root` is
|
||||||
|
// walked and parent paths are not supported.
|
||||||
|
export async function* expandGlob(
|
||||||
|
globString: string,
|
||||||
|
{
|
||||||
|
root = cwd(),
|
||||||
|
includeDirs = true,
|
||||||
|
extended = false,
|
||||||
|
globstar = false,
|
||||||
|
strict = false,
|
||||||
|
filepath = true,
|
||||||
|
flags = ""
|
||||||
|
}: ExpandGlobOptions = {}
|
||||||
|
): AsyncIterableIterator<WalkInfo> {
|
||||||
|
const absoluteGlob = isAbsolute(globString)
|
||||||
|
? globString
|
||||||
|
: join(root, globString);
|
||||||
|
const globOptions = { extended, globstar, strict, filepath, flags };
|
||||||
|
yield* walk(root, {
|
||||||
|
match: [glob(absoluteGlob, globOptions)],
|
||||||
|
includeDirs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Synchronous version of `expandGlob()`. */
|
||||||
|
// TODO: As `expandGlob()`.
|
||||||
|
export function* expandGlobSync(
|
||||||
|
globString: string,
|
||||||
|
{
|
||||||
|
root = cwd(),
|
||||||
|
includeDirs = true,
|
||||||
|
extended = false,
|
||||||
|
globstar = false,
|
||||||
|
strict = false,
|
||||||
|
filepath = true,
|
||||||
|
flags = ""
|
||||||
|
}: ExpandGlobOptions = {}
|
||||||
|
): IterableIterator<WalkInfo> {
|
||||||
|
const absoluteGlob = isAbsolute(globString)
|
||||||
|
? globString
|
||||||
|
: join(root, globString);
|
||||||
|
const globOptions = { extended, globstar, strict, filepath, flags };
|
||||||
|
yield* walkSync(root, {
|
||||||
|
match: [glob(absoluteGlob, globOptions)],
|
||||||
|
includeDirs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
const { mkdir } = Deno;
|
const { cwd, mkdir } = Deno;
|
||||||
type FileInfo = Deno.FileInfo;
|
|
||||||
import { test, runIfMain } from "../testing/mod.ts";
|
import { test, runIfMain } from "../testing/mod.ts";
|
||||||
import { assert, assertEquals } from "../testing/asserts.ts";
|
import { assert, assertEquals } from "../testing/asserts.ts";
|
||||||
import { glob, isGlob } from "./glob.ts";
|
import { isWindows } from "./path/constants.ts";
|
||||||
import { join } from "./path.ts";
|
import {
|
||||||
|
ExpandGlobOptions,
|
||||||
|
expandGlob,
|
||||||
|
glob,
|
||||||
|
isGlob,
|
||||||
|
expandGlobSync
|
||||||
|
} from "./glob.ts";
|
||||||
|
import { join, normalize, relative } from "./path.ts";
|
||||||
|
import { WalkInfo } from "./walk.ts";
|
||||||
import { testWalk } from "./walk_test.ts";
|
import { testWalk } from "./walk_test.ts";
|
||||||
import { touch, walkArray } from "./walk_test.ts";
|
import { touch, walkArray } from "./walk_test.ts";
|
||||||
|
|
||||||
|
@ -131,7 +138,6 @@ testWalk(
|
||||||
const arr = await walkArray(".", {
|
const arr = await walkArray(".", {
|
||||||
match: [glob("x.*", { flags: "g", globstar: true })]
|
match: [glob("x.*", { flags: "g", globstar: true })]
|
||||||
});
|
});
|
||||||
console.log(arr);
|
|
||||||
assertEquals(arr.length, 2);
|
assertEquals(arr.length, 2);
|
||||||
assertEquals(arr[0], "x.js");
|
assertEquals(arr[0], "x.js");
|
||||||
assertEquals(arr[1], "x.ts");
|
assertEquals(arr[1], "x.ts");
|
||||||
|
@ -253,4 +259,76 @@ test({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function expandGlobArray(
|
||||||
|
globString: string,
|
||||||
|
options: ExpandGlobOptions
|
||||||
|
): Promise<string[]> {
|
||||||
|
const infos: WalkInfo[] = [];
|
||||||
|
for await (const info of expandGlob(globString, options)) {
|
||||||
|
infos.push(info);
|
||||||
|
}
|
||||||
|
infos.sort();
|
||||||
|
const infosSync = [...expandGlobSync(globString, options)];
|
||||||
|
infosSync.sort();
|
||||||
|
assertEquals(infos, infosSync);
|
||||||
|
const root = normalize(options.root || cwd());
|
||||||
|
const paths = infos.map(({ filename }): string => filename);
|
||||||
|
for (const path of paths) {
|
||||||
|
assert(path.startsWith(root));
|
||||||
|
}
|
||||||
|
const relativePaths = paths.map((path: string): string =>
|
||||||
|
relative(root, path)
|
||||||
|
);
|
||||||
|
relativePaths.sort();
|
||||||
|
return relativePaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlToFilePath(url: URL): string {
|
||||||
|
// Since `new URL('file:///C:/a').pathname` is `/C:/a`, remove leading slash.
|
||||||
|
return url.pathname.slice(url.protocol == "file:" && isWindows ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const EG_OPTIONS = {
|
||||||
|
root: urlToFilePath(new URL(join("testdata", "glob"), import.meta.url)),
|
||||||
|
includeDirs: true,
|
||||||
|
extended: false,
|
||||||
|
globstar: false,
|
||||||
|
strict: false,
|
||||||
|
filepath: false,
|
||||||
|
flags: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
test(async function expandGlobExt(): Promise<void> {
|
||||||
|
const options = { ...EG_OPTIONS, extended: true };
|
||||||
|
assertEquals(await expandGlobArray("abc?(def|ghi)", options), [
|
||||||
|
"abc",
|
||||||
|
"abcdef"
|
||||||
|
]);
|
||||||
|
assertEquals(await expandGlobArray("abc*(def|ghi)", options), [
|
||||||
|
"abc",
|
||||||
|
"abcdef",
|
||||||
|
"abcdefghi"
|
||||||
|
]);
|
||||||
|
assertEquals(await expandGlobArray("abc+(def|ghi)", options), [
|
||||||
|
"abcdef",
|
||||||
|
"abcdefghi"
|
||||||
|
]);
|
||||||
|
assertEquals(await expandGlobArray("abc@(def|ghi)", options), ["abcdef"]);
|
||||||
|
assertEquals(await expandGlobArray("abc{def,ghi}", options), ["abcdef"]);
|
||||||
|
assertEquals(await expandGlobArray("abc!(def|ghi)", options), ["abc"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(async function expandGlobGlobstar(): Promise<void> {
|
||||||
|
const options = { ...EG_OPTIONS, globstar: true };
|
||||||
|
assertEquals(await expandGlobArray(join("**", "abc"), options), [
|
||||||
|
"abc",
|
||||||
|
join("subdir", "abc")
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(async function expandGlobIncludeDirs(): Promise<void> {
|
||||||
|
const options = { ...EG_OPTIONS, includeDirs: false };
|
||||||
|
assertEquals(await expandGlobArray("subdir", options), []);
|
||||||
|
});
|
||||||
|
|
||||||
runIfMain(import.meta);
|
runIfMain(import.meta);
|
||||||
|
|
|
@ -5,17 +5,18 @@
|
||||||
import { GlobOptions } from "./glob.ts";
|
import { GlobOptions } from "./glob.ts";
|
||||||
|
|
||||||
const isWin = Deno.build.os === "win";
|
const isWin = Deno.build.os === "win";
|
||||||
const SEP = isWin ? `\\\\+` : `\\/`;
|
const SEP = isWin ? `(\\\\+|\\/)` : `\\/`;
|
||||||
const SEP_ESC = isWin ? `\\\\` : `/`;
|
const SEP_ESC = isWin ? `\\\\` : `/`;
|
||||||
const GLOBSTAR = `((?:[^/]*(?:/|$))*)`;
|
const SEP_RAW = isWin ? `\\` : `/`;
|
||||||
const WILDCARD = `([^/]*)`;
|
const GLOBSTAR = `((?:[^${SEP_ESC}/]*(?:${SEP_ESC}|\/|$))*)`;
|
||||||
const GLOBSTAR_SEGMENT = `((?:[^${SEP_ESC}]*(?:${SEP_ESC}|$))*)`;
|
const WILDCARD = `([^${SEP_ESC}/]*)`;
|
||||||
const WILDCARD_SEGMENT = `([^${SEP_ESC}]*)`;
|
const GLOBSTAR_SEGMENT = `((?:[^${SEP_ESC}/]*(?:${SEP_ESC}|\/|$))*)`;
|
||||||
|
const WILDCARD_SEGMENT = `([^${SEP_ESC}/]*)`;
|
||||||
|
|
||||||
export interface GlobrexResult {
|
export interface GlobrexResult {
|
||||||
regex: RegExp;
|
regex: RegExp;
|
||||||
path?: {
|
path?: {
|
||||||
regex: string | RegExp;
|
regex: RegExp;
|
||||||
segments: RegExp[];
|
segments: RegExp[];
|
||||||
globstar?: RegExp;
|
globstar?: RegExp;
|
||||||
};
|
};
|
||||||
|
@ -44,11 +45,8 @@ export function globrex(
|
||||||
): GlobrexResult {
|
): GlobrexResult {
|
||||||
let regex = "";
|
let regex = "";
|
||||||
let segment = "";
|
let segment = "";
|
||||||
let path: {
|
let pathRegexStr = "";
|
||||||
regex: string | RegExp;
|
const pathSegments = [];
|
||||||
segments: RegExp[];
|
|
||||||
globstar?: RegExp;
|
|
||||||
} = { regex: "", segments: [] };
|
|
||||||
|
|
||||||
// If we are doing extended matching, this boolean is true when we are inside
|
// If we are doing extended matching, this boolean is true when we are inside
|
||||||
// a group (eg {*.html,*.js}), and false otherwise.
|
// a group (eg {*.html,*.js}), and false otherwise.
|
||||||
|
@ -72,13 +70,13 @@ export function globrex(
|
||||||
const { split, last, only } = options;
|
const { split, last, only } = options;
|
||||||
if (only !== "path") regex += str;
|
if (only !== "path") regex += str;
|
||||||
if (filepath && only !== "regex") {
|
if (filepath && only !== "regex") {
|
||||||
path.regex += str === "\\/" ? SEP : str;
|
pathRegexStr += str.match(new RegExp(`^${SEP}$`)) ? SEP : str;
|
||||||
if (split) {
|
if (split) {
|
||||||
if (last) segment += str;
|
if (last) segment += str;
|
||||||
if (segment !== "") {
|
if (segment !== "") {
|
||||||
// change it 'includes'
|
// change it 'includes'
|
||||||
if (!flags.includes("g")) segment = `^${segment}$`;
|
if (!flags.includes("g")) segment = `^${segment}$`;
|
||||||
path.segments.push(new RegExp(segment, flags));
|
pathSegments.push(new RegExp(segment, flags));
|
||||||
}
|
}
|
||||||
segment = "";
|
segment = "";
|
||||||
} else {
|
} else {
|
||||||
|
@ -267,9 +265,9 @@ export function globrex(
|
||||||
let isGlobstar =
|
let isGlobstar =
|
||||||
starCount > 1 && // multiple "*"'s
|
starCount > 1 && // multiple "*"'s
|
||||||
// from the start of the segment
|
// from the start of the segment
|
||||||
(prevChar === "/" || prevChar === undefined) &&
|
[SEP_RAW, "/", undefined].includes(prevChar) &&
|
||||||
// to the end of the segment
|
// to the end of the segment
|
||||||
(nextChar === "/" || nextChar === undefined);
|
[SEP_RAW, "/", undefined].includes(nextChar);
|
||||||
if (isGlobstar) {
|
if (isGlobstar) {
|
||||||
// it's a globstar, so match zero or more path segments
|
// it's a globstar, so match zero or more path segments
|
||||||
add(GLOBSTAR, { only: "regex" });
|
add(GLOBSTAR, { only: "regex" });
|
||||||
|
@ -292,20 +290,22 @@ export function globrex(
|
||||||
if (!flags.includes("g")) {
|
if (!flags.includes("g")) {
|
||||||
regex = `^${regex}$`;
|
regex = `^${regex}$`;
|
||||||
segment = `^${segment}$`;
|
segment = `^${segment}$`;
|
||||||
if (filepath) path.regex = `^${path.regex}$`;
|
if (filepath) pathRegexStr = `^${pathRegexStr}$`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: GlobrexResult = { regex: new RegExp(regex, flags) };
|
const result: GlobrexResult = { regex: new RegExp(regex, flags) };
|
||||||
|
|
||||||
// Push the last segment
|
// Push the last segment
|
||||||
if (filepath) {
|
if (filepath) {
|
||||||
path.segments.push(new RegExp(segment, flags));
|
pathSegments.push(new RegExp(segment, flags));
|
||||||
path.regex = new RegExp(path.regex.toString(), flags);
|
result.path = {
|
||||||
path.globstar = new RegExp(
|
regex: new RegExp(pathRegexStr, flags),
|
||||||
|
segments: pathSegments,
|
||||||
|
globstar: new RegExp(
|
||||||
!flags.includes("g") ? `^${GLOBSTAR_SEGMENT}$` : GLOBSTAR_SEGMENT,
|
!flags.includes("g") ? `^${GLOBSTAR_SEGMENT}$` : GLOBSTAR_SEGMENT,
|
||||||
flags
|
flags
|
||||||
);
|
)
|
||||||
result.path = path;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -50,3 +50,4 @@ export const CHAR_9 = 57; /* 9 */
|
||||||
|
|
||||||
export const isWindows = build.os === "win";
|
export const isWindows = build.os === "win";
|
||||||
export const EOL = isWindows ? "\r\n" : "\n";
|
export const EOL = isWindows ? "\r\n" : "\n";
|
||||||
|
export const SEP = isWindows ? "\\" : "/";
|
||||||
|
|
0
fs/testdata/glob/abc
vendored
Normal file
0
fs/testdata/glob/abc
vendored
Normal file
0
fs/testdata/glob/abcdef
vendored
Normal file
0
fs/testdata/glob/abcdef
vendored
Normal file
0
fs/testdata/glob/abcdefghi
vendored
Normal file
0
fs/testdata/glob/abcdefghi
vendored
Normal file
0
fs/testdata/glob/subdir/abc
vendored
Normal file
0
fs/testdata/glob/subdir/abc
vendored
Normal file
|
@ -348,7 +348,7 @@ async function runTestsSerial(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Defines options for controlling execution details of a test suite. */
|
/** Defines options for controlling execution details of a test suite. */
|
||||||
export interface RunOptions {
|
export interface RunTestsOptions {
|
||||||
parallel?: boolean;
|
parallel?: boolean;
|
||||||
exitOnFail?: boolean;
|
exitOnFail?: boolean;
|
||||||
only?: RegExp;
|
only?: RegExp;
|
||||||
|
@ -368,7 +368,7 @@ export async function runTests({
|
||||||
only = /[^\s]/,
|
only = /[^\s]/,
|
||||||
skip = /^\s*$/,
|
skip = /^\s*$/,
|
||||||
disableLog = false
|
disableLog = false
|
||||||
}: RunOptions = {}): Promise<void> {
|
}: RunTestsOptions = {}): Promise<void> {
|
||||||
const tests: TestDefinition[] = candidates.filter(
|
const tests: TestDefinition[] = candidates.filter(
|
||||||
({ name }): boolean => only.test(name) && !skip.test(name)
|
({ name }): boolean => only.test(name) && !skip.test(name)
|
||||||
);
|
);
|
||||||
|
@ -415,7 +415,7 @@ export async function runTests({
|
||||||
*/
|
*/
|
||||||
export async function runIfMain(
|
export async function runIfMain(
|
||||||
meta: ImportMeta,
|
meta: ImportMeta,
|
||||||
opts?: RunOptions
|
opts?: RunTestsOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (meta.main) {
|
if (meta.main) {
|
||||||
return runTests(opts);
|
return runTests(opts);
|
||||||
|
|
|
@ -1,44 +1,46 @@
|
||||||
#!/usr/bin/env -S deno -A
|
#!/usr/bin/env -S deno -A
|
||||||
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
||||||
import { parse } from "../flags/mod.ts";
|
import { parse } from "../flags/mod.ts";
|
||||||
import { glob, isGlob, walk } from "../fs/mod.ts";
|
import {
|
||||||
import { runTests } from "./mod.ts";
|
WalkInfo,
|
||||||
const { args, cwd } = Deno;
|
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 DEFAULT_GLOBS = [
|
const DIR_GLOBS = [join("**", "?(*_)test.{js,ts}")];
|
||||||
"**/*_test.ts",
|
|
||||||
"**/*_test.js",
|
|
||||||
"**/test.ts",
|
|
||||||
"**/test.js"
|
|
||||||
];
|
|
||||||
|
|
||||||
/* eslint-disable max-len */
|
|
||||||
function showHelp(): void {
|
function showHelp(): void {
|
||||||
console.log(`Deno test runner
|
console.log(`Deno test runner
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
deno -A https://deno.land/std/testing/runner.ts [OPTIONS] [FILES...]
|
deno -A https://deno.land/std/testing/runner.ts [OPTIONS] [MODULES...]
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
-q, --quiet Don't show output from test cases
|
-q, --quiet Don't show output from test cases
|
||||||
-f, --failfast Stop test suite on first error
|
-f, --failfast Stop running tests on first error
|
||||||
-e, --exclude <FILES...> List of file names to exclude from run. If this options is
|
-e, --exclude <MODULES...> List of comma-separated modules to exclude
|
||||||
used files to match must be specified after "--".
|
--allow-none Exit with status 0 even when no test modules are
|
||||||
|
found
|
||||||
|
|
||||||
ARGS:
|
ARGS:
|
||||||
[FILES...] List of file names to run. Defaults to: ${DEFAULT_GLOBS.join(
|
[MODULES...] List of test modules to run.
|
||||||
","
|
A directory <dir> will expand to:
|
||||||
)}
|
${DIR_GLOBS.map((s: string): string => `${join("<dir>", s)}`)
|
||||||
`);
|
.join(`
|
||||||
}
|
`)}
|
||||||
/* eslint-enable max-len */
|
Defaults to "." when none are provided.
|
||||||
|
|
||||||
function filePathToRegExp(str: string): RegExp {
|
Note that modules can refer to file paths or URLs. File paths support glob
|
||||||
if (isGlob(str)) {
|
expansion.
|
||||||
return glob(str, { flags: "g" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return RegExp(str, "g");
|
Examples:
|
||||||
|
deno test src/**/*_test.ts
|
||||||
|
deno test tests`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRemoteUrl(url: string): boolean {
|
function isRemoteUrl(url: string): boolean {
|
||||||
|
@ -58,114 +60,186 @@ function partition(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function filePathToUrl(path: string): string {
|
||||||
* Given list of globs or URLs to include and exclude and root directory return
|
return `file://${isWindows ? "/" : ""}${path.replace(/\\/g, "/")}`;
|
||||||
* list of file URLs that should be imported for test runner.
|
|
||||||
*/
|
|
||||||
export async function getMatchingUrls(
|
|
||||||
matchPaths: string[],
|
|
||||||
excludePaths: string[],
|
|
||||||
root: string
|
|
||||||
): Promise<string[]> {
|
|
||||||
const [includeLocal, includeRemote] = partition(matchPaths, isRemoteUrl);
|
|
||||||
const [excludeLocal, excludeRemote] = partition(excludePaths, isRemoteUrl);
|
|
||||||
|
|
||||||
const localFileIterator = walk(root, {
|
|
||||||
match: includeLocal.map((f: string): RegExp => filePathToRegExp(f)),
|
|
||||||
skip: excludeLocal.map((f: string): RegExp => filePathToRegExp(f))
|
|
||||||
});
|
|
||||||
|
|
||||||
let matchingLocalUrls: string[] = [];
|
|
||||||
for await (const { filename } of localFileIterator) {
|
|
||||||
matchingLocalUrls.push(`file://${filename}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const excludeRemotePatterns = excludeRemote.map(
|
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)
|
(url: string): RegExp => RegExp(url)
|
||||||
);
|
);
|
||||||
const matchingRemoteUrls = includeRemote.filter(
|
const notExcludedPath = ({ filename }: WalkInfo): boolean =>
|
||||||
(candidateUrl: string): boolean => {
|
!excludePathPatterns.some((p: RegExp): boolean => !!filename.match(p));
|
||||||
return !excludeRemotePatterns.some((pattern: RegExp): boolean => {
|
const notExcludedUrl = (url: string): boolean =>
|
||||||
const r = pattern.test(candidateUrl);
|
!excludeUrlPatterns.some((p: RegExp): boolean => !!url.match(p));
|
||||||
pattern.lastIndex = 0;
|
|
||||||
return r;
|
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]
|
||||||
);
|
);
|
||||||
|
|
||||||
return matchingLocalUrls.concat(matchingRemoteUrls);
|
const matchedUrls = includeUrls.filter(notExcludedUrl);
|
||||||
|
|
||||||
|
return [...matchedPaths.map(filePathToUrl), ...matchedUrls];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RunTestModulesOptions extends RunTestsOptions {
|
||||||
|
include?: string[];
|
||||||
|
exclude?: string[];
|
||||||
|
allowNone?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function runs matching test files in `root` directory.
|
* Import the specified test modules and run their tests as a suite.
|
||||||
*
|
*
|
||||||
* File matching and excluding supports glob syntax, ie. if encountered arg is
|
* Test modules are specified as an array of strings and can include local files
|
||||||
* a glob it will be expanded using `glob` method from `fs` module.
|
* or URLs.
|
||||||
*
|
*
|
||||||
* Note that your shell may expand globs for you:
|
* File matching and excluding support glob syntax - arguments recognized as
|
||||||
* $ deno -A ./runner.ts **\/*_test.ts **\/test.ts
|
* globs will be expanded using `glob()` from the `fs` module.
|
||||||
*
|
*
|
||||||
* Expanding using `fs.glob`:
|
* Example:
|
||||||
* $ deno -A ./runner.ts \*\*\/\*_test.ts \*\*\/test.ts
|
|
||||||
*
|
*
|
||||||
* `**\/*_test.ts` and `**\/test.ts"` are arguments that will be parsed and
|
* runTestModules({ include: ["**\/*_test.ts", "**\/test.ts"] });
|
||||||
* expanded as: [glob("**\/*_test.ts"), glob("**\/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
|
// TODO: Change return type to `Promise<void>` once, `runTests` is updated
|
||||||
// to return boolean instead of exiting
|
// to return boolean instead of exiting.
|
||||||
export async function main(root: string = cwd()): Promise<void> {
|
export async function runTestModules({
|
||||||
const parsedArgs = parse(args.slice(1), {
|
include = ["."],
|
||||||
boolean: ["quiet", "failfast", "help"],
|
exclude = [],
|
||||||
string: ["exclude"],
|
allowNone = false,
|
||||||
alias: {
|
parallel = false,
|
||||||
help: ["h"],
|
exitOnFail = false,
|
||||||
quiet: ["q"],
|
only = /[^\s]/,
|
||||||
failfast: ["f"],
|
skip = /^\s*$/,
|
||||||
exclude: ["e"]
|
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);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (parsedArgs.help) {
|
|
||||||
return showHelp();
|
|
||||||
}
|
|
||||||
|
|
||||||
let includeFiles: string[];
|
|
||||||
let excludeFiles: string[];
|
|
||||||
|
|
||||||
if (parsedArgs._.length) {
|
|
||||||
includeFiles = (parsedArgs._ as string[])
|
|
||||||
.map((fileGlob: string): string[] => {
|
|
||||||
return fileGlob.split(",");
|
|
||||||
})
|
|
||||||
.flat();
|
|
||||||
} else {
|
|
||||||
includeFiles = DEFAULT_GLOBS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedArgs.exclude) {
|
|
||||||
excludeFiles = (parsedArgs.exclude as string).split(",");
|
|
||||||
} else {
|
|
||||||
excludeFiles = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundTestUrls = await getMatchingUrls(includeFiles, excludeFiles, root);
|
|
||||||
|
|
||||||
if (foundTestUrls.length === 0) {
|
|
||||||
console.error("No matching test files found.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Found ${foundTestUrls.length} matching test files.`);
|
if (!disableLog) {
|
||||||
|
console.log(`Found ${testModuleUrls.length} matching test modules.`);
|
||||||
|
}
|
||||||
|
|
||||||
for (const url of foundTestUrls) {
|
for (const url of testModuleUrls) {
|
||||||
await import(url);
|
await import(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
await runTests({
|
await runTests({
|
||||||
exitOnFail: !!parsedArgs.failfast,
|
parallel,
|
||||||
disableLog: !!parsedArgs.quiet
|
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) {
|
if (import.meta.main) {
|
||||||
main();
|
main();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,78 @@
|
||||||
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
||||||
import { test } from "./mod.ts";
|
import { test } from "./mod.ts";
|
||||||
|
import { findTestModules } from "./runner.ts";
|
||||||
|
import { isWindows } from "../fs/path/constants.ts";
|
||||||
import { assertEquals } from "../testing/asserts.ts";
|
import { assertEquals } from "../testing/asserts.ts";
|
||||||
import { getMatchingUrls } from "./runner.ts";
|
|
||||||
import { join } from "../fs/path/mod.ts";
|
|
||||||
|
|
||||||
/**
|
function urlToFilePath(url: URL): string {
|
||||||
* IMPORTANT: This file assumes it is run from root of repository.
|
// Since `new URL('file:///C:/a').pathname` is `/C:/a`, remove leading slash.
|
||||||
*/
|
return url.pathname.slice(url.protocol == "file:" && isWindows ? 1 : 0);
|
||||||
const cwd = Deno.cwd();
|
}
|
||||||
const TEST_ROOT_PATH = join(cwd, "fmt");
|
|
||||||
|
|
||||||
test(async function getMatchingUrlsRemote(): Promise<void> {
|
const TEST_DATA_URL = new URL("testdata", import.meta.url);
|
||||||
const matches = [
|
const TEST_DATA_PATH = urlToFilePath(TEST_DATA_URL);
|
||||||
"https://deno.land/std/fmt/colors_test.ts",
|
|
||||||
"http://deno.land/std/fmt/printf_test.ts"
|
test(async function findTestModulesDir1(): Promise<void> {
|
||||||
|
const urls = await findTestModules(["."], [], TEST_DATA_PATH);
|
||||||
|
assertEquals(urls.sort(), [
|
||||||
|
`${TEST_DATA_URL}/bar_test.js`,
|
||||||
|
`${TEST_DATA_URL}/foo_test.ts`,
|
||||||
|
`${TEST_DATA_URL}/subdir/bar_test.js`,
|
||||||
|
`${TEST_DATA_URL}/subdir/foo_test.ts`,
|
||||||
|
`${TEST_DATA_URL}/subdir/test.js`,
|
||||||
|
`${TEST_DATA_URL}/subdir/test.ts`,
|
||||||
|
`${TEST_DATA_URL}/test.js`,
|
||||||
|
`${TEST_DATA_URL}/test.ts`
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(async function findTestModulesDir2(): Promise<void> {
|
||||||
|
const urls = await findTestModules(["subdir"], [], TEST_DATA_PATH);
|
||||||
|
assertEquals(urls.sort(), [
|
||||||
|
`${TEST_DATA_URL}/subdir/bar_test.js`,
|
||||||
|
`${TEST_DATA_URL}/subdir/foo_test.ts`,
|
||||||
|
`${TEST_DATA_URL}/subdir/test.js`,
|
||||||
|
`${TEST_DATA_URL}/subdir/test.ts`
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(async function findTestModulesGlob(): Promise<void> {
|
||||||
|
const urls = await findTestModules(["**/*_test.{js,ts}"], [], TEST_DATA_PATH);
|
||||||
|
assertEquals(urls.sort(), [
|
||||||
|
`${TEST_DATA_URL}/bar_test.js`,
|
||||||
|
`${TEST_DATA_URL}/foo_test.ts`,
|
||||||
|
`${TEST_DATA_URL}/subdir/bar_test.js`,
|
||||||
|
`${TEST_DATA_URL}/subdir/foo_test.ts`
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(async function findTestModulesExcludeDir(): Promise<void> {
|
||||||
|
const urls = await findTestModules(["."], ["subdir"], TEST_DATA_PATH);
|
||||||
|
assertEquals(urls.sort(), [
|
||||||
|
`${TEST_DATA_URL}/bar_test.js`,
|
||||||
|
`${TEST_DATA_URL}/foo_test.ts`,
|
||||||
|
`${TEST_DATA_URL}/test.js`,
|
||||||
|
`${TEST_DATA_URL}/test.ts`
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(async function findTestModulesExcludeGlob(): Promise<void> {
|
||||||
|
const urls = await findTestModules(["."], ["**/foo*"], TEST_DATA_PATH);
|
||||||
|
assertEquals(urls.sort(), [
|
||||||
|
`${TEST_DATA_URL}/bar_test.js`,
|
||||||
|
`${TEST_DATA_URL}/subdir/bar_test.js`,
|
||||||
|
`${TEST_DATA_URL}/subdir/test.js`,
|
||||||
|
`${TEST_DATA_URL}/subdir/test.ts`,
|
||||||
|
`${TEST_DATA_URL}/test.js`,
|
||||||
|
`${TEST_DATA_URL}/test.ts`
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(async function findTestModulesRemote(): Promise<void> {
|
||||||
|
const urls = [
|
||||||
|
"https://example.com/colors_test.ts",
|
||||||
|
"http://example.com/printf_test.ts"
|
||||||
];
|
];
|
||||||
|
const matches = await findTestModules(urls, []);
|
||||||
const urls = await getMatchingUrls(matches, [], TEST_ROOT_PATH);
|
assertEquals(matches, urls);
|
||||||
assertEquals(urls, matches);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(async function getMatchingUrlsLocal(): Promise<void> {
|
|
||||||
const urls = await getMatchingUrls(
|
|
||||||
["fmt/*_test.ts"],
|
|
||||||
["colors*"],
|
|
||||||
TEST_ROOT_PATH
|
|
||||||
);
|
|
||||||
assertEquals(urls.length, 1);
|
|
||||||
});
|
});
|
||||||
|
|
1
testing/testdata/bar.js
vendored
Normal file
1
testing/testdata/bar.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/bar_test.js
vendored
Normal file
1
testing/testdata/bar_test.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/foo.ts
vendored
Normal file
1
testing/testdata/foo.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/foo_test.ts
vendored
Normal file
1
testing/testdata/foo_test.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/subdir/bar.js
vendored
Normal file
1
testing/testdata/subdir/bar.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/subdir/bar_test.js
vendored
Normal file
1
testing/testdata/subdir/bar_test.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/subdir/foo.ts
vendored
Normal file
1
testing/testdata/subdir/foo.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/subdir/foo_test.ts
vendored
Normal file
1
testing/testdata/subdir/foo_test.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/subdir/test.js
vendored
Normal file
1
testing/testdata/subdir/test.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/subdir/test.ts
vendored
Normal file
1
testing/testdata/subdir/test.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/test.js
vendored
Normal file
1
testing/testdata/test.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
1
testing/testdata/test.ts
vendored
Normal file
1
testing/testdata/test.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
|
@ -33,7 +33,7 @@ test(async function xevalCliReplvar(): Promise<void> {
|
||||||
await p.stdin!.write(encode("hello"));
|
await p.stdin!.write(encode("hello"));
|
||||||
await p.stdin!.close();
|
await p.stdin!.close();
|
||||||
assertEquals(await p.status(), { code: 0, success: true });
|
assertEquals(await p.status(), { code: 0, success: true });
|
||||||
assertEquals(decode(await p.output()), "hello\n");
|
assertEquals(decode(await p.output()).trimEnd(), "hello");
|
||||||
});
|
});
|
||||||
|
|
||||||
test(async function xevalCliSyntaxError(): Promise<void> {
|
test(async function xevalCliSyntaxError(): Promise<void> {
|
||||||
|
|
Loading…
Reference in a new issue