taist 1.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/.taistrc.json +15 -0
- package/LICENSE +20 -0
- package/README.md +782 -0
- package/index.js +147 -0
- package/lib/ast-transformer.js +282 -0
- package/lib/execution-tracer.js +370 -0
- package/lib/loader-hooks.js +71 -0
- package/lib/loader.js +154 -0
- package/lib/output-formatter.js +151 -0
- package/lib/spawn-traced.js +204 -0
- package/lib/toon-formatter.js +351 -0
- package/lib/traced.js +223 -0
- package/lib/tracer-setup.js +115 -0
- package/lib/vitest-plugin.js +138 -0
- package/lib/vitest-runner.js +326 -0
- package/lib/watch-handler.js +300 -0
- package/package.json +71 -0
- package/taist.js +294 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Formatter - Multi-format test result formatter
|
|
3
|
+
* Supports TOON, JSON, and Compact output formats
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ToonFormatter } from './toon-formatter.js';
|
|
7
|
+
|
|
8
|
+
export class OutputFormatter {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.formatType = options.format || 'toon';
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.toonFormatter = new ToonFormatter(options);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format test results based on configured format
|
|
17
|
+
* @param {Object} results - Test results object
|
|
18
|
+
* @returns {string} - Formatted output
|
|
19
|
+
*/
|
|
20
|
+
format(results) {
|
|
21
|
+
switch (this.formatType.toLowerCase()) {
|
|
22
|
+
case 'toon':
|
|
23
|
+
return this.formatToon(results);
|
|
24
|
+
case 'json':
|
|
25
|
+
return this.formatJson(results);
|
|
26
|
+
case 'compact':
|
|
27
|
+
return this.formatCompact(results);
|
|
28
|
+
default:
|
|
29
|
+
throw new Error(`Unknown format: ${this.formatType}. Use: toon, json, or compact`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format as TOON
|
|
35
|
+
*/
|
|
36
|
+
formatToon(results) {
|
|
37
|
+
return this.toonFormatter.format(results);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format as JSON
|
|
42
|
+
*/
|
|
43
|
+
formatJson(results) {
|
|
44
|
+
const output = {
|
|
45
|
+
status: this.determineStatus(results),
|
|
46
|
+
stats: results.stats || {
|
|
47
|
+
total: 0,
|
|
48
|
+
passed: 0,
|
|
49
|
+
failed: 0,
|
|
50
|
+
skipped: 0
|
|
51
|
+
},
|
|
52
|
+
failures: (results.failures || []).map(f => this.formatFailureForJson(f)),
|
|
53
|
+
trace: results.trace || [],
|
|
54
|
+
coverage: results.coverage || null,
|
|
55
|
+
duration: results.duration || 0,
|
|
56
|
+
timestamp: new Date().toISOString()
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return JSON.stringify(output, null, this.options.pretty ? 2 : 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format as compact one-liner
|
|
64
|
+
*/
|
|
65
|
+
formatCompact(results) {
|
|
66
|
+
const stats = results.stats || {};
|
|
67
|
+
const passed = stats.passed || 0;
|
|
68
|
+
const total = stats.total || 0;
|
|
69
|
+
const failed = stats.failed || 0;
|
|
70
|
+
|
|
71
|
+
const status = failed === 0 ? '✓' : '✗';
|
|
72
|
+
const parts = [`${status} ${passed}/${total}`];
|
|
73
|
+
|
|
74
|
+
if (failed > 0) {
|
|
75
|
+
parts.push(`${failed} fail`);
|
|
76
|
+
if (results.failures && results.failures.length > 0) {
|
|
77
|
+
const firstError = results.failures[0];
|
|
78
|
+
const errorMsg = this.extractErrorMessage(firstError);
|
|
79
|
+
parts.push(`(${errorMsg})`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (results.coverage) {
|
|
84
|
+
parts.push(`cov:${Math.round(results.coverage.percent)}%`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (results.duration) {
|
|
88
|
+
parts.push(`${Math.round(results.duration)}ms`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return parts.join(' ');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Determine overall status
|
|
96
|
+
*/
|
|
97
|
+
determineStatus(results) {
|
|
98
|
+
const stats = results.stats || {};
|
|
99
|
+
if (stats.failed > 0) return 'fail';
|
|
100
|
+
if (stats.passed === 0 && stats.total === 0) return 'empty';
|
|
101
|
+
return 'pass';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Format failure for JSON output
|
|
106
|
+
*/
|
|
107
|
+
formatFailureForJson(failure) {
|
|
108
|
+
return {
|
|
109
|
+
test: failure.test || failure.name,
|
|
110
|
+
error: this.extractErrorMessage(failure),
|
|
111
|
+
location: failure.location || null,
|
|
112
|
+
diff: failure.diff || null,
|
|
113
|
+
stack: this.formatStackForJson(failure.stack),
|
|
114
|
+
path: failure.path || null
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract error message
|
|
120
|
+
*/
|
|
121
|
+
extractErrorMessage(failure) {
|
|
122
|
+
if (failure.error) {
|
|
123
|
+
if (typeof failure.error === 'string') return failure.error;
|
|
124
|
+
if (failure.error.message) return failure.error.message;
|
|
125
|
+
return String(failure.error);
|
|
126
|
+
}
|
|
127
|
+
return 'Unknown error';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Format stack trace for JSON
|
|
132
|
+
*/
|
|
133
|
+
formatStackForJson(stack) {
|
|
134
|
+
if (!stack) return null;
|
|
135
|
+
if (typeof stack === 'string') {
|
|
136
|
+
return stack.split('\n')
|
|
137
|
+
.filter(line => line.trim())
|
|
138
|
+
.slice(0, 5);
|
|
139
|
+
}
|
|
140
|
+
return stack;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set format type
|
|
145
|
+
*/
|
|
146
|
+
setFormat(format) {
|
|
147
|
+
this.formatType = format;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export default OutputFormatter;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taist Spawn Helper - Run child processes with execution tracing
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for spawning Node.js processes with instrumentation
|
|
5
|
+
* enabled, collecting traces, and integrating with test frameworks.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { spawnTraced, execTraced } from 'taist/spawn-traced';
|
|
9
|
+
*
|
|
10
|
+
* const { traces, stdout, stderr, exitCode } = await execTraced('node', ['my-script.js']);
|
|
11
|
+
* console.log(traces); // Array of execution traces
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn, execFile } from 'node:child_process';
|
|
15
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join, resolve, dirname } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
// Get the directory of this file to find the loader
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
const loaderPath = resolve(__dirname, 'loader.js');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a temporary file for trace collection
|
|
27
|
+
* @returns {{ path: string, cleanup: () => void }}
|
|
28
|
+
*/
|
|
29
|
+
function createTraceFile() {
|
|
30
|
+
const path = join(tmpdir(), `taist-trace-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
31
|
+
writeFileSync(path, '[]');
|
|
32
|
+
return {
|
|
33
|
+
path,
|
|
34
|
+
cleanup: () => {
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(path)) unlinkSync(path);
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore cleanup errors
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read traces from a trace file
|
|
46
|
+
* @param {string} path - Path to trace file
|
|
47
|
+
* @returns {Array} Array of trace entries
|
|
48
|
+
*/
|
|
49
|
+
function readTraces(path) {
|
|
50
|
+
try {
|
|
51
|
+
if (!existsSync(path)) return [];
|
|
52
|
+
const content = readFileSync(path, 'utf-8');
|
|
53
|
+
return JSON.parse(content);
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Execute a Node.js script with tracing enabled
|
|
61
|
+
* Returns a promise that resolves when the process exits
|
|
62
|
+
*
|
|
63
|
+
* @param {string} command - Command to run (usually 'node')
|
|
64
|
+
* @param {string[]} args - Arguments to pass
|
|
65
|
+
* @param {Object} options - Options
|
|
66
|
+
* @param {string} options.cwd - Working directory
|
|
67
|
+
* @param {Object} options.env - Additional environment variables
|
|
68
|
+
* @param {boolean} options.debug - Enable debug output
|
|
69
|
+
* @param {number} options.timeout - Timeout in milliseconds
|
|
70
|
+
* @returns {Promise<{ traces: Array, stdout: string, stderr: string, exitCode: number }>}
|
|
71
|
+
*/
|
|
72
|
+
export async function execTraced(command, args = [], options = {}) {
|
|
73
|
+
const traceFile = createTraceFile();
|
|
74
|
+
|
|
75
|
+
const env = {
|
|
76
|
+
...process.env,
|
|
77
|
+
...options.env,
|
|
78
|
+
TAIST_TRACE_FILE: traceFile.path,
|
|
79
|
+
NODE_OPTIONS: `--import ${loaderPath} ${process.env.NODE_OPTIONS || ''}`.trim()
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (options.debug) {
|
|
83
|
+
env.TAIST_DEBUG = '1';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return new Promise((resolvePromise, reject) => {
|
|
87
|
+
const child = spawn(command, args, {
|
|
88
|
+
cwd: options.cwd || process.cwd(),
|
|
89
|
+
env,
|
|
90
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
let stdout = '';
|
|
94
|
+
let stderr = '';
|
|
95
|
+
let timeoutId;
|
|
96
|
+
|
|
97
|
+
if (options.timeout) {
|
|
98
|
+
timeoutId = setTimeout(() => {
|
|
99
|
+
child.kill('SIGTERM');
|
|
100
|
+
reject(new Error(`Process timed out after ${options.timeout}ms`));
|
|
101
|
+
}, options.timeout);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
child.stdout.on('data', (data) => {
|
|
105
|
+
stdout += data.toString();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
child.stderr.on('data', (data) => {
|
|
109
|
+
stderr += data.toString();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
child.on('error', (error) => {
|
|
113
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
114
|
+
traceFile.cleanup();
|
|
115
|
+
reject(error);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
child.on('close', (exitCode) => {
|
|
119
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
120
|
+
|
|
121
|
+
// Give a small delay for trace file to be written
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
const traces = readTraces(traceFile.path);
|
|
124
|
+
traceFile.cleanup();
|
|
125
|
+
|
|
126
|
+
resolvePromise({
|
|
127
|
+
traces,
|
|
128
|
+
stdout,
|
|
129
|
+
stderr,
|
|
130
|
+
exitCode: exitCode ?? 0
|
|
131
|
+
});
|
|
132
|
+
}, 50);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Spawn a Node.js process with tracing enabled
|
|
139
|
+
* Returns the child process immediately (for streaming output)
|
|
140
|
+
*
|
|
141
|
+
* @param {string} command - Command to run
|
|
142
|
+
* @param {string[]} args - Arguments
|
|
143
|
+
* @param {Object} options - Spawn options
|
|
144
|
+
* @returns {{ child: ChildProcess, getTraces: () => Array, cleanup: () => void }}
|
|
145
|
+
*/
|
|
146
|
+
export function spawnTraced(command, args = [], options = {}) {
|
|
147
|
+
const traceFile = createTraceFile();
|
|
148
|
+
|
|
149
|
+
const env = {
|
|
150
|
+
...process.env,
|
|
151
|
+
...options.env,
|
|
152
|
+
TAIST_TRACE_FILE: traceFile.path,
|
|
153
|
+
NODE_OPTIONS: `--import ${loaderPath} ${process.env.NODE_OPTIONS || ''}`.trim()
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (options.debug) {
|
|
157
|
+
env.TAIST_DEBUG = '1';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const child = spawn(command, args, {
|
|
161
|
+
...options,
|
|
162
|
+
env,
|
|
163
|
+
stdio: options.stdio || 'pipe'
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
child,
|
|
168
|
+
getTraces: () => readTraces(traceFile.path),
|
|
169
|
+
cleanup: () => traceFile.cleanup()
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Run a Node.js script file with tracing
|
|
175
|
+
* Convenience wrapper for execTraced('node', [scriptPath, ...args])
|
|
176
|
+
*
|
|
177
|
+
* @param {string} scriptPath - Path to the script to run
|
|
178
|
+
* @param {string[]} args - Arguments to pass to the script
|
|
179
|
+
* @param {Object} options - Options (same as execTraced)
|
|
180
|
+
* @returns {Promise<{ traces: Array, stdout: string, stderr: string, exitCode: number }>}
|
|
181
|
+
*/
|
|
182
|
+
export async function runTraced(scriptPath, args = [], options = {}) {
|
|
183
|
+
return execTraced('node', [scriptPath, ...args], options);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create a test helper for integration testing
|
|
188
|
+
* Returns functions pre-configured for a specific working directory
|
|
189
|
+
*
|
|
190
|
+
* @param {string} cwd - Working directory for the tests
|
|
191
|
+
* @returns {{ run: Function, exec: Function, spawn: Function }}
|
|
192
|
+
*/
|
|
193
|
+
export function createTestHelper(cwd) {
|
|
194
|
+
return {
|
|
195
|
+
run: (scriptPath, args, options) =>
|
|
196
|
+
runTraced(scriptPath, args, { ...options, cwd }),
|
|
197
|
+
exec: (command, args, options) =>
|
|
198
|
+
execTraced(command, args, { ...options, cwd }),
|
|
199
|
+
spawn: (command, args, options) =>
|
|
200
|
+
spawnTraced(command, args, { ...options, cwd })
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export default { execTraced, spawnTraced, runTraced, createTestHelper };
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOON Formatter - Token-Optimized Output Notation
|
|
3
|
+
* Converts test results to a token-efficient format for AI consumption
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class ToonFormatter {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = {
|
|
9
|
+
abbreviate: options.abbreviate !== false,
|
|
10
|
+
maxTokens: options.maxTokens || 1000,
|
|
11
|
+
maxStringLength: options.maxStringLength || 50,
|
|
12
|
+
maxStackFrames: options.maxStackFrames || 2,
|
|
13
|
+
maxObjectKeys: options.maxObjectKeys || 3,
|
|
14
|
+
maxArrayItems: options.maxArrayItems || 2,
|
|
15
|
+
...options
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Abbreviation dictionary
|
|
19
|
+
this.abbrev = {
|
|
20
|
+
function: 'fn',
|
|
21
|
+
error: 'err',
|
|
22
|
+
expected: 'exp',
|
|
23
|
+
received: 'got',
|
|
24
|
+
actual: 'got',
|
|
25
|
+
undefined: 'undef',
|
|
26
|
+
null: 'nil',
|
|
27
|
+
test: 'tst',
|
|
28
|
+
testing: 'tst',
|
|
29
|
+
passed: 'pass',
|
|
30
|
+
failed: 'fail',
|
|
31
|
+
arguments: 'args',
|
|
32
|
+
return: 'ret',
|
|
33
|
+
result: 'ret',
|
|
34
|
+
message: 'msg',
|
|
35
|
+
location: 'loc',
|
|
36
|
+
line: 'ln',
|
|
37
|
+
column: 'col'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format test results in TOON format
|
|
43
|
+
* @param {Object} results - Test results object
|
|
44
|
+
* @returns {string} - Formatted TOON output
|
|
45
|
+
*/
|
|
46
|
+
format(results) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
|
|
49
|
+
// Header
|
|
50
|
+
lines.push(this.formatHeader(results));
|
|
51
|
+
|
|
52
|
+
// Failures
|
|
53
|
+
if (results.failures && results.failures.length > 0) {
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push('FAILURES:');
|
|
56
|
+
results.failures.forEach(failure => {
|
|
57
|
+
lines.push(...this.formatFailure(failure));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Trace
|
|
62
|
+
if (results.trace && results.trace.length > 0) {
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push('TRACE:');
|
|
65
|
+
results.trace.forEach(entry => {
|
|
66
|
+
lines.push(this.formatTraceEntry(entry));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Coverage
|
|
71
|
+
if (results.coverage) {
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push(this.formatCoverage(results.coverage));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format test result header
|
|
81
|
+
*/
|
|
82
|
+
formatHeader(results) {
|
|
83
|
+
const passed = results.stats?.passed || 0;
|
|
84
|
+
const total = results.stats?.total || 0;
|
|
85
|
+
return `===TESTS: ${passed}/${total}===`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Format a single test failure
|
|
90
|
+
*/
|
|
91
|
+
formatFailure(failure) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
|
|
94
|
+
// Test name
|
|
95
|
+
lines.push(`✗ ${this.truncate(failure.test || failure.name)}`);
|
|
96
|
+
|
|
97
|
+
// Location
|
|
98
|
+
if (failure.location) {
|
|
99
|
+
lines.push(` @${this.formatLocation(failure.location)}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Error message
|
|
103
|
+
if (failure.error) {
|
|
104
|
+
const msg = this.cleanErrorMessage(failure.error);
|
|
105
|
+
lines.push(` ${msg}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Expected vs Actual
|
|
109
|
+
if (failure.diff) {
|
|
110
|
+
if (failure.diff.expected !== undefined) {
|
|
111
|
+
lines.push(` exp: ${this.formatValue(failure.diff.expected)}`);
|
|
112
|
+
}
|
|
113
|
+
if (failure.diff.actual !== undefined) {
|
|
114
|
+
lines.push(` got: ${this.formatValue(failure.diff.actual)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Execution path
|
|
119
|
+
if (failure.path) {
|
|
120
|
+
lines.push(` path: ${this.formatPath(failure.path)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Stack trace (abbreviated)
|
|
124
|
+
if (failure.stack && this.options.maxStackFrames > 0) {
|
|
125
|
+
const stack = this.formatStack(failure.stack);
|
|
126
|
+
if (stack) {
|
|
127
|
+
lines.push(` ${stack}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Format trace entry
|
|
136
|
+
*/
|
|
137
|
+
formatTraceEntry(entry) {
|
|
138
|
+
const parts = [`fn:${entry.name}`];
|
|
139
|
+
|
|
140
|
+
if (entry.duration !== undefined) {
|
|
141
|
+
parts.push(`ms:${Math.round(entry.duration)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (entry.args && entry.args.length > 0) {
|
|
145
|
+
const args = entry.args
|
|
146
|
+
.slice(0, this.options.maxArrayItems)
|
|
147
|
+
.map(arg => this.formatValue(arg))
|
|
148
|
+
.join(',');
|
|
149
|
+
parts.push(`args:[${args}]`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (entry.result !== undefined) {
|
|
153
|
+
parts.push(`ret:${this.formatValue(entry.result)}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (entry.error) {
|
|
157
|
+
parts.push(`err:${this.cleanErrorMessage(entry.error)}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return ` ${parts.join(' ')}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format coverage information
|
|
165
|
+
*/
|
|
166
|
+
formatCoverage(coverage) {
|
|
167
|
+
const percent = Math.round(coverage.percent || 0);
|
|
168
|
+
const covered = coverage.covered || 0;
|
|
169
|
+
const total = coverage.total || 0;
|
|
170
|
+
return `COV: ${percent}% (${covered}/${total})`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Format a location reference
|
|
175
|
+
*/
|
|
176
|
+
formatLocation(location) {
|
|
177
|
+
if (typeof location === 'string') {
|
|
178
|
+
return this.abbreviatePath(location);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const file = this.abbreviatePath(location.file || '');
|
|
182
|
+
const line = location.line || '';
|
|
183
|
+
const col = location.column || '';
|
|
184
|
+
|
|
185
|
+
if (col) {
|
|
186
|
+
return `${file}:${line}:${col}`;
|
|
187
|
+
} else if (line) {
|
|
188
|
+
return `${file}:${line}`;
|
|
189
|
+
}
|
|
190
|
+
return file;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Format an execution path
|
|
195
|
+
*/
|
|
196
|
+
formatPath(path) {
|
|
197
|
+
if (Array.isArray(path)) {
|
|
198
|
+
return path
|
|
199
|
+
.map(step => {
|
|
200
|
+
if (typeof step === 'string') return step;
|
|
201
|
+
if (step.fn && step.result !== undefined) {
|
|
202
|
+
return `${step.fn}(...)→${this.formatValue(step.result)}`;
|
|
203
|
+
}
|
|
204
|
+
return step.fn || String(step);
|
|
205
|
+
})
|
|
206
|
+
.join('→');
|
|
207
|
+
}
|
|
208
|
+
return String(path);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Format a value for output
|
|
213
|
+
*/
|
|
214
|
+
formatValue(value) {
|
|
215
|
+
if (value === null) return 'nil';
|
|
216
|
+
if (value === undefined) return 'undef';
|
|
217
|
+
|
|
218
|
+
const type = typeof value;
|
|
219
|
+
|
|
220
|
+
if (type === 'string') {
|
|
221
|
+
return `"${this.truncate(value)}"`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (type === 'number' || type === 'boolean') {
|
|
225
|
+
return String(value);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (Array.isArray(value)) {
|
|
229
|
+
if (value.length === 0) return '[]';
|
|
230
|
+
const items = value
|
|
231
|
+
.slice(0, this.options.maxArrayItems)
|
|
232
|
+
.map(v => this.formatValue(v))
|
|
233
|
+
.join(',');
|
|
234
|
+
const more = value.length > this.options.maxArrayItems
|
|
235
|
+
? `...+${value.length - this.options.maxArrayItems}`
|
|
236
|
+
: '';
|
|
237
|
+
return `[${items}${more}]`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (type === 'object') {
|
|
241
|
+
const keys = Object.keys(value).slice(0, this.options.maxObjectKeys);
|
|
242
|
+
if (keys.length === 0) return '{}';
|
|
243
|
+
const more = Object.keys(value).length > this.options.maxObjectKeys
|
|
244
|
+
? '...'
|
|
245
|
+
: '';
|
|
246
|
+
return `{${keys.join(',')}${more}}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return String(value).slice(0, 20);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Format stack trace
|
|
254
|
+
*/
|
|
255
|
+
formatStack(stack) {
|
|
256
|
+
if (typeof stack === 'string') {
|
|
257
|
+
const lines = stack.split('\n')
|
|
258
|
+
.filter(line => line.trim() && !line.includes('node_modules'))
|
|
259
|
+
.slice(0, this.options.maxStackFrames);
|
|
260
|
+
|
|
261
|
+
return lines
|
|
262
|
+
.map(line => {
|
|
263
|
+
// Extract file:line:col from stack frame
|
|
264
|
+
const match = line.match(/\((.+):(\d+):(\d+)\)/) ||
|
|
265
|
+
line.match(/at (.+):(\d+):(\d+)/);
|
|
266
|
+
if (match) {
|
|
267
|
+
const [, file, line, col] = match;
|
|
268
|
+
return `@${this.abbreviatePath(file)}:${line}`;
|
|
269
|
+
}
|
|
270
|
+
return line.trim().slice(0, 50);
|
|
271
|
+
})
|
|
272
|
+
.join(' ');
|
|
273
|
+
}
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Clean error message
|
|
279
|
+
*/
|
|
280
|
+
cleanErrorMessage(error) {
|
|
281
|
+
if (typeof error === 'object' && error.message) {
|
|
282
|
+
error = error.message;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let msg = String(error);
|
|
286
|
+
|
|
287
|
+
// Remove ANSI codes
|
|
288
|
+
msg = msg.replace(/\u001b\[\d+m/g, '');
|
|
289
|
+
|
|
290
|
+
// Remove timestamps
|
|
291
|
+
msg = msg.replace(/\[\d{2}:\d{2}:\d{2}\]/g, '');
|
|
292
|
+
|
|
293
|
+
// Remove absolute paths
|
|
294
|
+
msg = msg.replace(/\/[^\s]+\//g, match => {
|
|
295
|
+
const parts = match.split('/');
|
|
296
|
+
return parts[parts.length - 1] || match;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Truncate
|
|
300
|
+
msg = this.truncate(msg);
|
|
301
|
+
|
|
302
|
+
return msg;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Abbreviate file path
|
|
307
|
+
*/
|
|
308
|
+
abbreviatePath(path) {
|
|
309
|
+
if (!path) return '';
|
|
310
|
+
|
|
311
|
+
// Remove common prefixes
|
|
312
|
+
path = path.replace(/^.*\/node_modules\//, 'npm/');
|
|
313
|
+
path = path.replace(/^.*\/src\//, 'src/');
|
|
314
|
+
path = path.replace(/^.*\/test\//, 'test/');
|
|
315
|
+
path = path.replace(/^.*\/lib\//, 'lib/');
|
|
316
|
+
|
|
317
|
+
// Get just filename if still too long
|
|
318
|
+
if (path.length > 30) {
|
|
319
|
+
const parts = path.split('/');
|
|
320
|
+
path = parts[parts.length - 1];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return path;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Truncate string
|
|
328
|
+
*/
|
|
329
|
+
truncate(str, maxLength = this.options.maxStringLength) {
|
|
330
|
+
if (!str) return '';
|
|
331
|
+
str = String(str);
|
|
332
|
+
if (str.length <= maxLength) return str;
|
|
333
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Apply abbreviations
|
|
338
|
+
*/
|
|
339
|
+
abbreviate(text) {
|
|
340
|
+
if (!this.options.abbreviate) return text;
|
|
341
|
+
|
|
342
|
+
let result = text;
|
|
343
|
+
for (const [full, abbr] of Object.entries(this.abbrev)) {
|
|
344
|
+
const regex = new RegExp(`\\b${full}\\b`, 'gi');
|
|
345
|
+
result = result.replace(regex, abbr);
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export default ToonFormatter;
|