@telorun/test 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/suite.d.ts +20 -0
- package/dist/suite.js +188 -0
- package/package.json +29 -0
- package/src/suite.ts +236 -0
package/dist/suite.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResourceContext, Runnable } from "@telorun/sdk";
|
|
2
|
+
import { Static } from "@sinclair/typebox";
|
|
3
|
+
export declare const args: {
|
|
4
|
+
filter: {
|
|
5
|
+
type: "string";
|
|
6
|
+
alias: string;
|
|
7
|
+
description: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare const schema: import("@sinclair/typebox").TObject<{
|
|
11
|
+
metadata: import("@sinclair/typebox").TObject<{
|
|
12
|
+
name: import("@sinclair/typebox").TString;
|
|
13
|
+
}>;
|
|
14
|
+
include: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
15
|
+
exclude: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
16
|
+
filter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
17
|
+
}>;
|
|
18
|
+
type SuiteManifest = Static<typeof schema>;
|
|
19
|
+
export declare function create(manifest: SuiteManifest, ctx: ResourceContext): Promise<Runnable>;
|
|
20
|
+
export {};
|
package/dist/suite.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Kernel } from "@telorun/kernel";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { Writable } from "stream";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
class BufferedWritable extends Writable {
|
|
8
|
+
chunks = [];
|
|
9
|
+
_write(chunk, _encoding, cb) {
|
|
10
|
+
this.chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
11
|
+
cb();
|
|
12
|
+
}
|
|
13
|
+
get content() {
|
|
14
|
+
return Buffer.concat(this.chunks).toString("utf8");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const args = {
|
|
18
|
+
filter: { type: "string", alias: "f", description: "Filter tests by name substring" },
|
|
19
|
+
};
|
|
20
|
+
export const schema = Type.Object({
|
|
21
|
+
metadata: Type.Object({
|
|
22
|
+
name: Type.String(),
|
|
23
|
+
}),
|
|
24
|
+
include: Type.Optional(Type.Array(Type.String())),
|
|
25
|
+
exclude: Type.Optional(Type.Array(Type.String())),
|
|
26
|
+
filter: Type.Optional(Type.String()),
|
|
27
|
+
});
|
|
28
|
+
function createColors(stream) {
|
|
29
|
+
const useColor = stream.isTTY ?? false;
|
|
30
|
+
const c = (code, text) => (useColor ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
31
|
+
return {
|
|
32
|
+
bold: (t) => c("1", t),
|
|
33
|
+
red: (t) => c("31", t),
|
|
34
|
+
green: (t) => c("32", t),
|
|
35
|
+
yellow: (t) => c("33", t),
|
|
36
|
+
dim: (t) => c("2", t),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function globToRegex(pattern) {
|
|
40
|
+
const re = pattern
|
|
41
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
42
|
+
.replace(/\*\*\//g, "(.+/)?")
|
|
43
|
+
.replace(/\*/g, "[^/]+");
|
|
44
|
+
return new RegExp(`^${re}$`);
|
|
45
|
+
}
|
|
46
|
+
function discoverTests(baseDir, include, exclude, filter) {
|
|
47
|
+
const entries = fs.readdirSync(baseDir, { recursive: true, encoding: "utf8" });
|
|
48
|
+
const includeRe = include.map(globToRegex);
|
|
49
|
+
const excludeRe = exclude.map(globToRegex);
|
|
50
|
+
const results = [];
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const normalized = entry.replace(/\\/g, "/");
|
|
53
|
+
if (normalized.includes("node_modules/"))
|
|
54
|
+
continue;
|
|
55
|
+
if (!includeRe.some((re) => re.test(normalized)))
|
|
56
|
+
continue;
|
|
57
|
+
if (excludeRe.some((re) => re.test(normalized)))
|
|
58
|
+
continue;
|
|
59
|
+
if (filter && !normalized.includes(filter))
|
|
60
|
+
continue;
|
|
61
|
+
results.push(path.resolve(baseDir, entry));
|
|
62
|
+
}
|
|
63
|
+
results.sort();
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
function labelFor(testPath, baseDir) {
|
|
67
|
+
return path.relative(baseDir, testPath);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Parse a .env file into a key-value map.
|
|
71
|
+
* Supports KEY=VALUE lines, ignores comments and blank lines.
|
|
72
|
+
*/
|
|
73
|
+
function parseEnvFile(content) {
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const line of content.split("\n")) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
78
|
+
continue;
|
|
79
|
+
const eq = trimmed.indexOf("=");
|
|
80
|
+
if (eq === -1)
|
|
81
|
+
continue;
|
|
82
|
+
const key = trimmed.slice(0, eq).trim();
|
|
83
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
84
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
85
|
+
value = value.slice(1, -1);
|
|
86
|
+
}
|
|
87
|
+
result[key] = value;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
function tryReadFile(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
return fs.readFileSync(filePath, "utf8");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Build an env object for a test manifest by layering .env and .env.local
|
|
101
|
+
* from the manifest's directory on top of the current process.env.
|
|
102
|
+
* Keys already present in process.env take precedence (same as CLI behaviour).
|
|
103
|
+
*/
|
|
104
|
+
function buildEnvForManifest(manifestPath) {
|
|
105
|
+
const dir = path.dirname(path.resolve(manifestPath));
|
|
106
|
+
const base = parseEnvFile(tryReadFile(path.join(dir, ".env")));
|
|
107
|
+
const local = parseEnvFile(tryReadFile(path.join(dir, ".env.local")));
|
|
108
|
+
return { ...base, ...local, ...process.env };
|
|
109
|
+
}
|
|
110
|
+
async function runOneTest(testPath, captureOutput, parentStdout, parentStderr) {
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
const stdout = captureOutput ? new BufferedWritable() : parentStdout;
|
|
113
|
+
const stderr = captureOutput ? new BufferedWritable() : parentStderr;
|
|
114
|
+
try {
|
|
115
|
+
const kernel = new Kernel({ env: buildEnvForManifest(testPath), stdout, stderr });
|
|
116
|
+
await kernel.loadFromConfig(testPath);
|
|
117
|
+
await kernel.start();
|
|
118
|
+
return {
|
|
119
|
+
path: testPath,
|
|
120
|
+
label: "",
|
|
121
|
+
passed: kernel.exitCode === 0,
|
|
122
|
+
durationMs: Date.now() - start,
|
|
123
|
+
output: captureOutput ? stdout.content + stderr.content : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return {
|
|
128
|
+
path: testPath,
|
|
129
|
+
label: "",
|
|
130
|
+
passed: false,
|
|
131
|
+
durationMs: Date.now() - start,
|
|
132
|
+
error: err instanceof Error ? err.message : String(err),
|
|
133
|
+
output: captureOutput ? stdout.content + stderr.content : undefined,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function create(manifest, ctx) {
|
|
138
|
+
const { bold, red, green, yellow, dim } = createColors(ctx.stderr);
|
|
139
|
+
return {
|
|
140
|
+
run: async () => {
|
|
141
|
+
const sourceUrl = ctx.moduleContext.source;
|
|
142
|
+
const baseDir = sourceUrl.startsWith("file://")
|
|
143
|
+
? path.dirname(fileURLToPath(sourceUrl))
|
|
144
|
+
: process.cwd();
|
|
145
|
+
const include = manifest.include ?? ["**/tests/*.yaml"];
|
|
146
|
+
const exclude = manifest.exclude ?? ["**/__fixtures__/**"];
|
|
147
|
+
const filter = ctx.args.filter || ctx.args._[0] || manifest.filter;
|
|
148
|
+
const tests = discoverTests(baseDir, include, exclude, filter);
|
|
149
|
+
if (tests.length === 0) {
|
|
150
|
+
ctx.stderr.write(bold(yellow(`Test.Suite.${manifest.metadata.name}: no tests found`)) + "\n");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const singleTest = tests.length === 1;
|
|
154
|
+
const results = [];
|
|
155
|
+
for (const testPath of tests) {
|
|
156
|
+
const label = labelFor(testPath, baseDir);
|
|
157
|
+
const result = await runOneTest(testPath, !singleTest, ctx.stdout, ctx.stderr);
|
|
158
|
+
result.label = label;
|
|
159
|
+
results.push(result);
|
|
160
|
+
if (result.passed) {
|
|
161
|
+
ctx.stdout.write(green("PASS") + " " + dim(label) + " " + dim(`(${result.durationMs}ms)`) + "\n");
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
ctx.stderr.write(red("FAIL") + " " + label + " " + dim(`(${result.durationMs}ms)`) + "\n");
|
|
165
|
+
if (result.output) {
|
|
166
|
+
ctx.stderr.write(result.output);
|
|
167
|
+
}
|
|
168
|
+
if (result.error) {
|
|
169
|
+
ctx.stderr.write(dim(` ${result.error}`) + "\n");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const passed = results.filter((r) => r.passed);
|
|
174
|
+
const failed = results.filter((r) => !r.passed);
|
|
175
|
+
if (!singleTest) {
|
|
176
|
+
ctx.stdout.write("\n" + bold("Test Suite Results") + "\n");
|
|
177
|
+
ctx.stdout.write(green(` Passed: ${passed.length}`) +
|
|
178
|
+
(failed.length > 0 ? " " + red(`Failed: ${failed.length}`) : "") +
|
|
179
|
+
" " +
|
|
180
|
+
dim(`Total: ${results.length}`) +
|
|
181
|
+
"\n");
|
|
182
|
+
}
|
|
183
|
+
if (failed.length > 0) {
|
|
184
|
+
ctx.requestExit(1);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@telorun/test",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
"./suite": {
|
|
7
|
+
"source": "./src/suite.ts",
|
|
8
|
+
"bun": "./src/suite.ts",
|
|
9
|
+
"import": "./dist/suite.js",
|
|
10
|
+
"default": "./dist/suite.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.lib.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src/**"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@sinclair/typebox": "^0.34.48",
|
|
22
|
+
"@telorun/kernel": "workspace:*",
|
|
23
|
+
"@telorun/sdk": "workspace:*"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20.0.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/suite.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Kernel } from "@telorun/kernel";
|
|
2
|
+
import type { ResourceContext, Runnable } from "@telorun/sdk";
|
|
3
|
+
import { Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { Writable } from "stream";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
class BufferedWritable extends Writable {
|
|
10
|
+
private chunks: Buffer[] = [];
|
|
11
|
+
|
|
12
|
+
_write(chunk: Buffer | string, _encoding: string, cb: () => void) {
|
|
13
|
+
this.chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
14
|
+
cb();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get content(): string {
|
|
18
|
+
return Buffer.concat(this.chunks).toString("utf8");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const args = {
|
|
23
|
+
filter: { type: "string" as const, alias: "f", description: "Filter tests by name substring" },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const schema = Type.Object({
|
|
27
|
+
metadata: Type.Object({
|
|
28
|
+
name: Type.String(),
|
|
29
|
+
}),
|
|
30
|
+
include: Type.Optional(
|
|
31
|
+
Type.Array(Type.String()),
|
|
32
|
+
),
|
|
33
|
+
exclude: Type.Optional(
|
|
34
|
+
Type.Array(Type.String()),
|
|
35
|
+
),
|
|
36
|
+
filter: Type.Optional(Type.String()),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
type SuiteManifest = Static<typeof schema>;
|
|
40
|
+
|
|
41
|
+
interface TestResult {
|
|
42
|
+
path: string;
|
|
43
|
+
label: string;
|
|
44
|
+
passed: boolean;
|
|
45
|
+
durationMs: number;
|
|
46
|
+
error?: string;
|
|
47
|
+
output?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createColors(stream: NodeJS.WritableStream) {
|
|
51
|
+
const useColor = (stream as any).isTTY ?? false;
|
|
52
|
+
const c = (code: string, text: string) => (useColor ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
53
|
+
return {
|
|
54
|
+
bold: (t: string) => c("1", t),
|
|
55
|
+
red: (t: string) => c("31", t),
|
|
56
|
+
green: (t: string) => c("32", t),
|
|
57
|
+
yellow: (t: string) => c("33", t),
|
|
58
|
+
dim: (t: string) => c("2", t),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function globToRegex(pattern: string): RegExp {
|
|
63
|
+
const re = pattern
|
|
64
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
65
|
+
.replace(/\*\*\//g, "(.+/)?")
|
|
66
|
+
.replace(/\*/g, "[^/]+");
|
|
67
|
+
return new RegExp(`^${re}$`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function discoverTests(
|
|
71
|
+
baseDir: string,
|
|
72
|
+
include: string[],
|
|
73
|
+
exclude: string[],
|
|
74
|
+
filter?: string,
|
|
75
|
+
): string[] {
|
|
76
|
+
const entries = fs.readdirSync(baseDir, { recursive: true, encoding: "utf8" });
|
|
77
|
+
const includeRe = include.map(globToRegex);
|
|
78
|
+
const excludeRe = exclude.map(globToRegex);
|
|
79
|
+
|
|
80
|
+
const results: string[] = [];
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const normalized = entry.replace(/\\/g, "/");
|
|
83
|
+
if (normalized.includes("node_modules/")) continue;
|
|
84
|
+
if (!includeRe.some((re) => re.test(normalized))) continue;
|
|
85
|
+
if (excludeRe.some((re) => re.test(normalized))) continue;
|
|
86
|
+
if (filter && !normalized.includes(filter)) continue;
|
|
87
|
+
results.push(path.resolve(baseDir, entry));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
results.sort();
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function labelFor(testPath: string, baseDir: string): string {
|
|
95
|
+
return path.relative(baseDir, testPath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse a .env file into a key-value map.
|
|
100
|
+
* Supports KEY=VALUE lines, ignores comments and blank lines.
|
|
101
|
+
*/
|
|
102
|
+
function parseEnvFile(content: string): Record<string, string> {
|
|
103
|
+
const result: Record<string, string> = {};
|
|
104
|
+
for (const line of content.split("\n")) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
107
|
+
const eq = trimmed.indexOf("=");
|
|
108
|
+
if (eq === -1) continue;
|
|
109
|
+
const key = trimmed.slice(0, eq).trim();
|
|
110
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
111
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
112
|
+
value = value.slice(1, -1);
|
|
113
|
+
}
|
|
114
|
+
result[key] = value;
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function tryReadFile(filePath: string): string {
|
|
120
|
+
try {
|
|
121
|
+
return fs.readFileSync(filePath, "utf8");
|
|
122
|
+
} catch {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build an env object for a test manifest by layering .env and .env.local
|
|
129
|
+
* from the manifest's directory on top of the current process.env.
|
|
130
|
+
* Keys already present in process.env take precedence (same as CLI behaviour).
|
|
131
|
+
*/
|
|
132
|
+
function buildEnvForManifest(manifestPath: string): Record<string, string | undefined> {
|
|
133
|
+
const dir = path.dirname(path.resolve(manifestPath));
|
|
134
|
+
const base = parseEnvFile(tryReadFile(path.join(dir, ".env")));
|
|
135
|
+
const local = parseEnvFile(tryReadFile(path.join(dir, ".env.local")));
|
|
136
|
+
return { ...base, ...local, ...process.env };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function runOneTest(
|
|
140
|
+
testPath: string,
|
|
141
|
+
captureOutput: boolean,
|
|
142
|
+
parentStdout: NodeJS.WritableStream,
|
|
143
|
+
parentStderr: NodeJS.WritableStream,
|
|
144
|
+
): Promise<TestResult> {
|
|
145
|
+
const start = Date.now();
|
|
146
|
+
const stdout = captureOutput ? new BufferedWritable() : parentStdout;
|
|
147
|
+
const stderr = captureOutput ? new BufferedWritable() : parentStderr;
|
|
148
|
+
try {
|
|
149
|
+
const kernel = new Kernel({ env: buildEnvForManifest(testPath), stdout, stderr });
|
|
150
|
+
await kernel.loadFromConfig(testPath);
|
|
151
|
+
await kernel.start();
|
|
152
|
+
return {
|
|
153
|
+
path: testPath,
|
|
154
|
+
label: "",
|
|
155
|
+
passed: kernel.exitCode === 0,
|
|
156
|
+
durationMs: Date.now() - start,
|
|
157
|
+
output: captureOutput ? (stdout as BufferedWritable).content + (stderr as BufferedWritable).content : undefined,
|
|
158
|
+
};
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return {
|
|
161
|
+
path: testPath,
|
|
162
|
+
label: "",
|
|
163
|
+
passed: false,
|
|
164
|
+
durationMs: Date.now() - start,
|
|
165
|
+
error: err instanceof Error ? err.message : String(err),
|
|
166
|
+
output: captureOutput ? (stdout as BufferedWritable).content + (stderr as BufferedWritable).content : undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function create(
|
|
172
|
+
manifest: SuiteManifest,
|
|
173
|
+
ctx: ResourceContext,
|
|
174
|
+
): Promise<Runnable> {
|
|
175
|
+
const { bold, red, green, yellow, dim } = createColors(ctx.stderr);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
run: async () => {
|
|
179
|
+
const sourceUrl = ctx.moduleContext.source;
|
|
180
|
+
const baseDir = sourceUrl.startsWith("file://")
|
|
181
|
+
? path.dirname(fileURLToPath(sourceUrl))
|
|
182
|
+
: process.cwd();
|
|
183
|
+
|
|
184
|
+
const include = manifest.include ?? ["**/tests/*.yaml"];
|
|
185
|
+
const exclude = manifest.exclude ?? ["**/__fixtures__/**"];
|
|
186
|
+
const filter = (ctx.args.filter as string) || (ctx.args._[0] as string) || manifest.filter;
|
|
187
|
+
|
|
188
|
+
const tests = discoverTests(baseDir, include, exclude, filter);
|
|
189
|
+
|
|
190
|
+
if (tests.length === 0) {
|
|
191
|
+
ctx.stderr.write(bold(yellow(`Test.Suite.${manifest.metadata.name}: no tests found`)) + "\n");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const singleTest = tests.length === 1;
|
|
196
|
+
const results: TestResult[] = [];
|
|
197
|
+
|
|
198
|
+
for (const testPath of tests) {
|
|
199
|
+
const label = labelFor(testPath, baseDir);
|
|
200
|
+
const result = await runOneTest(testPath, !singleTest, ctx.stdout, ctx.stderr);
|
|
201
|
+
result.label = label;
|
|
202
|
+
results.push(result);
|
|
203
|
+
|
|
204
|
+
if (result.passed) {
|
|
205
|
+
ctx.stdout.write(green("PASS") + " " + dim(label) + " " + dim(`(${result.durationMs}ms)`) + "\n");
|
|
206
|
+
} else {
|
|
207
|
+
ctx.stderr.write(red("FAIL") + " " + label + " " + dim(`(${result.durationMs}ms)`) + "\n");
|
|
208
|
+
if (result.output) {
|
|
209
|
+
ctx.stderr.write(result.output);
|
|
210
|
+
}
|
|
211
|
+
if (result.error) {
|
|
212
|
+
ctx.stderr.write(dim(` ${result.error}`) + "\n");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const passed = results.filter((r) => r.passed);
|
|
218
|
+
const failed = results.filter((r) => !r.passed);
|
|
219
|
+
|
|
220
|
+
if (!singleTest) {
|
|
221
|
+
ctx.stdout.write("\n" + bold("Test Suite Results") + "\n");
|
|
222
|
+
ctx.stdout.write(
|
|
223
|
+
green(` Passed: ${passed.length}`) +
|
|
224
|
+
(failed.length > 0 ? " " + red(`Failed: ${failed.length}`) : "") +
|
|
225
|
+
" " +
|
|
226
|
+
dim(`Total: ${results.length}`) +
|
|
227
|
+
"\n",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (failed.length > 0) {
|
|
232
|
+
ctx.requestExit(1);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|