@cifn/runner 0.0.1
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/index.d.mts +223 -0
- package/dist/index.js +1117 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1091 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.d.mts +2 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +48 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +41 -0
- package/src/artifacts-cache.test.ts +557 -0
- package/src/docker-executor.ts +76 -0
- package/src/executor/run-step.ts +34 -0
- package/src/index.ts +23 -0
- package/src/reporting/logfn-client.ts +37 -0
- package/src/reporting/redact.ts +12 -0
- package/src/runner.test.ts +957 -0
- package/src/runner.ts +626 -0
- package/src/secrets-steps.test.ts +603 -0
- package/src/server.ts +54 -0
- package/src/steps/artifact-download.ts +55 -0
- package/src/steps/artifact-upload.ts +89 -0
- package/src/steps/cache-restore.ts +61 -0
- package/src/steps/cache-save.ts +88 -0
- package/src/steps/checkout.ts +63 -0
- package/src/steps/hostfn-deploy.ts +52 -0
- package/src/steps/testfn-run.ts +179 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface ArtifactUploadOptions {
|
|
5
|
+
name: string;
|
|
6
|
+
path: string;
|
|
7
|
+
workspace: string;
|
|
8
|
+
runId: string;
|
|
9
|
+
fileFnClient: {
|
|
10
|
+
upload(namespace: string, key: string, data: Buffer): Promise<string>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ArtifactUploadResult {
|
|
15
|
+
success: boolean;
|
|
16
|
+
fileId?: string;
|
|
17
|
+
lines: string[];
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectFiles(dirPath: string): string[] {
|
|
22
|
+
const files: string[] = [];
|
|
23
|
+
if (!existsSync(dirPath)) return files;
|
|
24
|
+
const stat = statSync(dirPath);
|
|
25
|
+
if (stat.isFile()) return [dirPath];
|
|
26
|
+
if (!stat.isDirectory()) return files;
|
|
27
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
28
|
+
const fullPath = join(dirPath, entry.name);
|
|
29
|
+
if (entry.isFile()) {
|
|
30
|
+
files.push(fullPath);
|
|
31
|
+
} else if (entry.isDirectory()) {
|
|
32
|
+
files.push(...collectFiles(fullPath));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function executeArtifactUpload(options: ArtifactUploadOptions): Promise<ArtifactUploadResult> {
|
|
39
|
+
const { name, path: artifactPath, workspace, runId, fileFnClient } = options;
|
|
40
|
+
const lines: string[] = [];
|
|
41
|
+
|
|
42
|
+
// Validate artifact name length (max 256 chars)
|
|
43
|
+
if (name.length > 256) {
|
|
44
|
+
const msg = `Artifact name exceeds maximum length of 256 characters (actual: ${name.length})`;
|
|
45
|
+
lines.push(msg);
|
|
46
|
+
return { success: false, lines, error: msg };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fullPath = join(workspace, artifactPath);
|
|
50
|
+
|
|
51
|
+
if (!existsSync(fullPath)) {
|
|
52
|
+
const msg = `Artifact path not found: ${artifactPath}`;
|
|
53
|
+
lines.push(msg);
|
|
54
|
+
return { success: false, lines, error: msg };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
lines.push(`Uploading artifact "${name}" from ${artifactPath}`);
|
|
59
|
+
|
|
60
|
+
const files = collectFiles(fullPath);
|
|
61
|
+
const buffers: Buffer[] = [];
|
|
62
|
+
const manifest: Array<{ relativePath: string; offset: number; size: number }> = [];
|
|
63
|
+
|
|
64
|
+
let offset = 0;
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
const data = readFileSync(file);
|
|
67
|
+
const rel = relative(fullPath, file) || relative(workspace, file);
|
|
68
|
+
manifest.push({ relativePath: rel, offset, size: data.length });
|
|
69
|
+
buffers.push(data);
|
|
70
|
+
offset += data.length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const manifestBuf = Buffer.from(JSON.stringify(manifest));
|
|
74
|
+
const headerBuf = Buffer.alloc(4);
|
|
75
|
+
headerBuf.writeUInt32BE(manifestBuf.length, 0);
|
|
76
|
+
|
|
77
|
+
const combined = Buffer.concat([headerBuf, manifestBuf, ...buffers]);
|
|
78
|
+
|
|
79
|
+
const namespace = `artifact:${runId}`;
|
|
80
|
+
const fileId = await fileFnClient.upload(namespace, name, combined);
|
|
81
|
+
|
|
82
|
+
lines.push(`Uploaded ${files.length} file(s), artifact fileId: ${fileId}`);
|
|
83
|
+
return { success: true, fileId, lines };
|
|
84
|
+
} catch (err: unknown) {
|
|
85
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
86
|
+
lines.push(`Upload failed: ${msg}`);
|
|
87
|
+
return { success: false, lines, error: msg };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface CacheRestoreOptions {
|
|
5
|
+
key: string;
|
|
6
|
+
workspace: string;
|
|
7
|
+
fileFnClient: {
|
|
8
|
+
downloadByKey(namespace: string, key: string): Promise<Buffer | null>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CacheRestoreResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
hit: boolean;
|
|
15
|
+
lines: string[];
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function executeCacheRestore(options: CacheRestoreOptions): Promise<CacheRestoreResult> {
|
|
20
|
+
const { key, workspace, fileFnClient } = options;
|
|
21
|
+
const lines: string[] = [];
|
|
22
|
+
|
|
23
|
+
// Validate cache key length (max 1KB)
|
|
24
|
+
const keyByteLength = Buffer.byteLength(key, 'utf8');
|
|
25
|
+
if (keyByteLength > 1024) {
|
|
26
|
+
const msg = `Cache key exceeds maximum length of 1KB (actual: ${keyByteLength} bytes)`;
|
|
27
|
+
lines.push(msg);
|
|
28
|
+
return { success: false, hit: false, lines, error: msg };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
lines.push(`Restoring cache with key "${key}"`);
|
|
33
|
+
|
|
34
|
+
const data = await fileFnClient.downloadByKey('cache', key);
|
|
35
|
+
|
|
36
|
+
if (!data) {
|
|
37
|
+
lines.push(`Cache miss for key "${key}"`);
|
|
38
|
+
return { success: true, hit: false, lines };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const manifestLen = data.readUInt32BE(0);
|
|
42
|
+
const manifestJson = data.subarray(4, 4 + manifestLen).toString('utf-8');
|
|
43
|
+
const manifest: Array<{ relativePath: string; offset: number; size: number }> = JSON.parse(manifestJson);
|
|
44
|
+
|
|
45
|
+
const dataStart = 4 + manifestLen;
|
|
46
|
+
|
|
47
|
+
for (const entry of manifest) {
|
|
48
|
+
const fileBuf = data.subarray(dataStart + entry.offset, dataStart + entry.offset + entry.size);
|
|
49
|
+
const outPath = join(workspace, entry.relativePath);
|
|
50
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
51
|
+
writeFileSync(outPath, fileBuf);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
lines.push(`Cache hit: restored ${manifest.length} file(s)`);
|
|
55
|
+
return { success: true, hit: true, lines };
|
|
56
|
+
} catch (err: unknown) {
|
|
57
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
58
|
+
lines.push(`Cache restore failed: ${msg}`);
|
|
59
|
+
return { success: false, hit: false, lines, error: msg };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface CacheSaveOptions {
|
|
5
|
+
key: string;
|
|
6
|
+
paths: string[];
|
|
7
|
+
workspace: string;
|
|
8
|
+
fileFnClient: {
|
|
9
|
+
upload(namespace: string, key: string, data: Buffer): Promise<string>;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CacheSaveResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
lines: string[];
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectFiles(dirPath: string, basePath: string): Array<{ relativePath: string; data: Buffer }> {
|
|
20
|
+
const results: Array<{ relativePath: string; data: Buffer }> = [];
|
|
21
|
+
if (!existsSync(dirPath)) return results;
|
|
22
|
+
const stat = statSync(dirPath);
|
|
23
|
+
if (stat.isFile()) {
|
|
24
|
+
results.push({ relativePath: relative(basePath, dirPath), data: readFileSync(dirPath) });
|
|
25
|
+
return results;
|
|
26
|
+
}
|
|
27
|
+
if (!stat.isDirectory()) return results;
|
|
28
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
29
|
+
const fullPath = join(dirPath, entry.name);
|
|
30
|
+
if (entry.isFile()) {
|
|
31
|
+
results.push({ relativePath: relative(basePath, fullPath), data: readFileSync(fullPath) });
|
|
32
|
+
} else if (entry.isDirectory()) {
|
|
33
|
+
results.push(...collectFiles(fullPath, basePath));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function executeCacheSave(options: CacheSaveOptions): Promise<CacheSaveResult> {
|
|
40
|
+
const { key, paths, workspace, fileFnClient } = options;
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
|
|
43
|
+
// Validate cache key length (max 1KB)
|
|
44
|
+
const keyByteLength = Buffer.byteLength(key, 'utf8');
|
|
45
|
+
if (keyByteLength > 1024) {
|
|
46
|
+
const msg = `Cache key exceeds maximum length of 1KB (actual: ${keyByteLength} bytes)`;
|
|
47
|
+
lines.push(msg);
|
|
48
|
+
return { success: false, lines, error: msg };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
lines.push(`Saving cache with key "${key}"`);
|
|
53
|
+
|
|
54
|
+
const allFiles: Array<{ relativePath: string; data: Buffer }> = [];
|
|
55
|
+
for (const p of paths) {
|
|
56
|
+
const fullPath = join(workspace, p);
|
|
57
|
+
const files = collectFiles(fullPath, workspace);
|
|
58
|
+
allFiles.push(...files);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (allFiles.length === 0) {
|
|
62
|
+
lines.push('No files found to cache');
|
|
63
|
+
return { success: true, lines };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const manifest: Array<{ relativePath: string; offset: number; size: number }> = [];
|
|
67
|
+
const buffers: Buffer[] = [];
|
|
68
|
+
let offset = 0;
|
|
69
|
+
for (const f of allFiles) {
|
|
70
|
+
manifest.push({ relativePath: f.relativePath, offset, size: f.data.length });
|
|
71
|
+
buffers.push(f.data);
|
|
72
|
+
offset += f.data.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const manifestBuf = Buffer.from(JSON.stringify(manifest));
|
|
76
|
+
const headerBuf = Buffer.alloc(4);
|
|
77
|
+
headerBuf.writeUInt32BE(manifestBuf.length, 0);
|
|
78
|
+
const combined = Buffer.concat([headerBuf, manifestBuf, ...buffers]);
|
|
79
|
+
|
|
80
|
+
await fileFnClient.upload('cache', key, combined);
|
|
81
|
+
lines.push(`Cached ${allFiles.length} file(s) under key "${key}"`);
|
|
82
|
+
return { success: true, lines };
|
|
83
|
+
} catch (err: unknown) {
|
|
84
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
85
|
+
lines.push(`Cache save failed: ${msg}`);
|
|
86
|
+
return { success: false, lines, error: msg };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export interface CheckoutOptions {
|
|
4
|
+
repo: string;
|
|
5
|
+
ref: string;
|
|
6
|
+
workspace: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CheckoutResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
lines: string[];
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function executeCheckout(options: CheckoutOptions): CheckoutResult {
|
|
17
|
+
const { repo, ref, workspace, token } = options;
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
const secretsToRedact: string[] = [];
|
|
20
|
+
|
|
21
|
+
let cloneUrl = repo;
|
|
22
|
+
if (token && cloneUrl.startsWith('https://')) {
|
|
23
|
+
const url = new URL(cloneUrl);
|
|
24
|
+
url.username = 'x-access-token';
|
|
25
|
+
url.password = token;
|
|
26
|
+
cloneUrl = url.toString();
|
|
27
|
+
secretsToRedact.push(token);
|
|
28
|
+
secretsToRedact.push(cloneUrl);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const redactLine = (line: string): string => {
|
|
32
|
+
let redacted = line;
|
|
33
|
+
for (const secret of secretsToRedact) {
|
|
34
|
+
if (secret.length > 0) {
|
|
35
|
+
redacted = redacted.split(secret).join('***');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return redacted;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
lines.push(`Cloning ${repo} at ref ${ref}`);
|
|
43
|
+
|
|
44
|
+
const cloneCmd = `git clone --depth 1 --branch ${ref} ${cloneUrl} .`;
|
|
45
|
+
const output = execSync(cloneCmd, {
|
|
46
|
+
cwd: workspace,
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
|
+
timeout: 120_000,
|
|
50
|
+
});
|
|
51
|
+
if (output) {
|
|
52
|
+
lines.push(...output.split('\n').filter(l => l !== '').map(redactLine));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lines.push(`Checkout complete: ${ref}`);
|
|
56
|
+
return { success: true, lines };
|
|
57
|
+
} catch (err: unknown) {
|
|
58
|
+
const error = err as { stderr?: string; message?: string };
|
|
59
|
+
const errMsg = typeof error.stderr === 'string' ? redactLine(error.stderr) : (error.message ? redactLine(error.message) : 'Unknown error');
|
|
60
|
+
lines.push(`Checkout failed: ${errMsg}`);
|
|
61
|
+
return { success: false, lines, error: errMsg };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export interface HostFnDeployOptions {
|
|
4
|
+
environment: string;
|
|
5
|
+
ci?: boolean;
|
|
6
|
+
local?: boolean;
|
|
7
|
+
workspace: string;
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface HostFnDeployResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
exitCode: number;
|
|
14
|
+
lines: string[];
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function executeHostFnDeploy(options: HostFnDeployOptions): HostFnDeployResult {
|
|
19
|
+
const { environment, ci = true, local = false, workspace, env } = options;
|
|
20
|
+
const lines: string[] = [];
|
|
21
|
+
|
|
22
|
+
let command = `hostfn deploy ${environment}`;
|
|
23
|
+
if (local) command += ' --local';
|
|
24
|
+
if (ci) command += ' --ci';
|
|
25
|
+
|
|
26
|
+
lines.push(`Deploying: ${command}`);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const output = execSync(command, {
|
|
30
|
+
cwd: workspace,
|
|
31
|
+
encoding: 'utf-8',
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
|
+
timeout: 600_000,
|
|
34
|
+
shell: '/bin/sh',
|
|
35
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
36
|
+
});
|
|
37
|
+
if (output) {
|
|
38
|
+
lines.push(...output.split('\n').filter(l => l !== ''));
|
|
39
|
+
}
|
|
40
|
+
lines.push('Deploy succeeded');
|
|
41
|
+
return { success: true, exitCode: 0, lines };
|
|
42
|
+
} catch (err: unknown) {
|
|
43
|
+
const error = err as { status?: number; stdout?: string; stderr?: string };
|
|
44
|
+
const stdout = typeof error.stdout === 'string' ? error.stdout : '';
|
|
45
|
+
const stderr = typeof error.stderr === 'string' ? error.stderr : '';
|
|
46
|
+
const exitCode = typeof error.status === 'number' ? error.status : 1;
|
|
47
|
+
if (stdout) lines.push(...stdout.split('\n').filter(l => l !== ''));
|
|
48
|
+
if (stderr) lines.push(...stderr.split('\n').filter(l => l !== ''));
|
|
49
|
+
lines.push(`Deploy failed with exit code ${exitCode}`);
|
|
50
|
+
return { success: false, exitCode, lines, error: `Deploy failed with exit code ${exitCode}` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { TestRunner, JsonReporter, type TestFnConfig } from '@testfn/core';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
export interface TestFnRunOptions {
|
|
6
|
+
framework?: string;
|
|
7
|
+
testPattern?: string;
|
|
8
|
+
reporter?: string;
|
|
9
|
+
outputPath?: string;
|
|
10
|
+
workspace: string;
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
parallel?: number | 'auto';
|
|
13
|
+
timeout?: number;
|
|
14
|
+
retries?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TestFnRunResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
exitCode: number;
|
|
20
|
+
lines: string[];
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function executeTestFnRun(options: TestFnRunOptions): TestFnRunResult {
|
|
25
|
+
const {
|
|
26
|
+
framework = 'vitest',
|
|
27
|
+
testPattern,
|
|
28
|
+
reporter,
|
|
29
|
+
outputPath = './testfn-results.json',
|
|
30
|
+
workspace,
|
|
31
|
+
env,
|
|
32
|
+
parallel,
|
|
33
|
+
timeout,
|
|
34
|
+
retries
|
|
35
|
+
} = options;
|
|
36
|
+
const lines: string[] = [];
|
|
37
|
+
|
|
38
|
+
lines.push(`Running tests with testfn SDK (framework: ${framework})`);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (!['vitest', 'playwright', 'jest'].includes(framework)) {
|
|
42
|
+
throw new Error(`Unsupported framework: ${framework}. Supported: vitest, playwright, jest`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const config: TestFnConfig = {
|
|
46
|
+
framework: framework as 'vitest' | 'playwright' | 'jest',
|
|
47
|
+
testPattern: testPattern || './tests/**/*.{test,spec}.{ts,js}',
|
|
48
|
+
parallel,
|
|
49
|
+
timeout,
|
|
50
|
+
retries,
|
|
51
|
+
env,
|
|
52
|
+
reporters: reporter === 'json' ? [new JsonReporter(join(workspace, outputPath))] : undefined,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const originalCwd = process.cwd();
|
|
56
|
+
try {
|
|
57
|
+
process.chdir(workspace);
|
|
58
|
+
|
|
59
|
+
const runner = new TestRunner(config);
|
|
60
|
+
const results = runner.run();
|
|
61
|
+
|
|
62
|
+
process.chdir(originalCwd);
|
|
63
|
+
|
|
64
|
+
if (results && typeof results === 'object' && 'then' in results) {
|
|
65
|
+
throw new Error('executeTestFnRun must be called from async context or use sync adapter');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lines.push(`Tests completed: ${(results as any).summary?.total || 0} total`);
|
|
69
|
+
lines.push(`Passed: ${(results as any).summary?.passed || 0}, Failed: ${(results as any).summary?.failed || 0}`);
|
|
70
|
+
|
|
71
|
+
if (reporter === 'json' && outputPath) {
|
|
72
|
+
const fullPath = join(workspace, outputPath);
|
|
73
|
+
if (existsSync(fullPath)) {
|
|
74
|
+
lines.push(`JSON report written to ${outputPath}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const hasFailed = (results as any).summary?.failed > 0;
|
|
79
|
+
if (hasFailed) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
exitCode: 1,
|
|
83
|
+
lines,
|
|
84
|
+
error: 'Tests failed'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { success: true, exitCode: 0, lines };
|
|
89
|
+
} catch (innerErr) {
|
|
90
|
+
process.chdir(originalCwd);
|
|
91
|
+
throw innerErr;
|
|
92
|
+
}
|
|
93
|
+
} catch (err: unknown) {
|
|
94
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
95
|
+
lines.push(`Test execution failed: ${errorMessage}`);
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
exitCode: 1,
|
|
99
|
+
lines,
|
|
100
|
+
error: errorMessage
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function executeTestFnRunAsync(options: TestFnRunOptions): Promise<TestFnRunResult> {
|
|
106
|
+
const {
|
|
107
|
+
framework = 'vitest',
|
|
108
|
+
testPattern,
|
|
109
|
+
reporter,
|
|
110
|
+
outputPath = './testfn-results.json',
|
|
111
|
+
workspace,
|
|
112
|
+
env,
|
|
113
|
+
parallel,
|
|
114
|
+
timeout,
|
|
115
|
+
retries
|
|
116
|
+
} = options;
|
|
117
|
+
const lines: string[] = [];
|
|
118
|
+
|
|
119
|
+
lines.push(`Running tests with testfn SDK (framework: ${framework})`);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (!['vitest', 'playwright', 'jest'].includes(framework)) {
|
|
123
|
+
throw new Error(`Unsupported framework: ${framework}. Supported: vitest, playwright, jest`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const config: TestFnConfig = {
|
|
127
|
+
framework: framework as 'vitest' | 'playwright' | 'jest',
|
|
128
|
+
testPattern: testPattern || './tests/**/*.{test,spec}.{ts,js}',
|
|
129
|
+
parallel,
|
|
130
|
+
timeout,
|
|
131
|
+
retries,
|
|
132
|
+
env,
|
|
133
|
+
reporters: reporter === 'json' ? [new JsonReporter(join(workspace, outputPath))] : undefined,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const originalCwd = process.cwd();
|
|
137
|
+
try {
|
|
138
|
+
process.chdir(workspace);
|
|
139
|
+
|
|
140
|
+
const runner = new TestRunner(config);
|
|
141
|
+
const results = await runner.run();
|
|
142
|
+
|
|
143
|
+
process.chdir(originalCwd);
|
|
144
|
+
|
|
145
|
+
lines.push(`Tests completed: ${results.summary.total} total`);
|
|
146
|
+
lines.push(`Passed: ${results.summary.passed}, Failed: ${results.summary.failed}`);
|
|
147
|
+
|
|
148
|
+
if (reporter === 'json' && outputPath) {
|
|
149
|
+
const fullPath = join(workspace, outputPath);
|
|
150
|
+
if (existsSync(fullPath)) {
|
|
151
|
+
lines.push(`JSON report written to ${outputPath}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (results.summary.failed > 0) {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
exitCode: 1,
|
|
159
|
+
lines,
|
|
160
|
+
error: 'Tests failed'
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { success: true, exitCode: 0, lines };
|
|
165
|
+
} catch (innerErr) {
|
|
166
|
+
process.chdir(originalCwd);
|
|
167
|
+
throw innerErr;
|
|
168
|
+
}
|
|
169
|
+
} catch (err: unknown) {
|
|
170
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
171
|
+
lines.push(`Test execution failed: ${errorMessage}`);
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
exitCode: 1,
|
|
175
|
+
lines,
|
|
176
|
+
error: errorMessage
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2020"],
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"outDir": "dist",
|
|
14
|
+
"resolveJsonModule": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
18
|
+
}
|
package/tsup.config.ts
ADDED