Skip to content

Commit 845dbaf

Browse files
committed
[WIP] E2E test instrumentation
1 parent 7950915 commit 845dbaf

12 files changed

Lines changed: 5348 additions & 170 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,4 @@ WARP.MD
260260
.tessl/
261261
CLAUDE.md
262262
tessl.json
263+
*/node_modules/*
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
/**
2+
* Codeflash Jest Helper - Unified Test Instrumentation
3+
*
4+
* This module provides a unified approach to instrumenting JavaScript tests
5+
* for both behavior verification and performance measurement.
6+
*
7+
* Unlike Python which has separate instrumentation methods for generated
8+
* vs existing tests, this helper works identically for ALL JavaScript tests.
9+
*
10+
* Usage:
11+
* const codeflash = require('codeflash-jest-helper');
12+
*
13+
* // Wrap function calls to capture behavior
14+
* const result = codeflash.capture('functionName', targetFunction, arg1, arg2);
15+
*
16+
* Environment Variables:
17+
* CODEFLASH_OUTPUT_FILE - Path to write results (default: /tmp/codeflash_results.bin)
18+
* CODEFLASH_LOOP_INDEX - Current benchmark loop iteration (default: 0)
19+
* CODEFLASH_MODE - Testing mode: 'behavior' or 'performance' (default: 'behavior')
20+
*/
21+
22+
const fs = require('fs');
23+
const path = require('path');
24+
const { performance } = require('perf_hooks');
25+
26+
// Configuration from environment
27+
const OUTPUT_FILE = process.env.CODEFLASH_OUTPUT_FILE || '/tmp/codeflash_results.bin';
28+
const LOOP_INDEX = parseInt(process.env.CODEFLASH_LOOP_INDEX || '0', 10);
29+
const MODE = process.env.CODEFLASH_MODE || 'behavior';
30+
31+
// Current test context
32+
let currentTestName = null;
33+
let invocationCounter = 0;
34+
35+
// Results buffer
36+
const results = [];
37+
38+
/**
39+
* Safely serialize a value to JSON.
40+
* Handles circular references and special types.
41+
*
42+
* @param {any} value - Value to serialize
43+
* @returns {any} - Serializable representation
44+
*/
45+
function safeSerialize(value) {
46+
const seen = new WeakSet();
47+
48+
function serialize(val) {
49+
// Handle primitives
50+
if (val === null || val === undefined) return val;
51+
if (typeof val === 'number') {
52+
if (Number.isNaN(val)) return { __type: 'NaN' };
53+
if (!Number.isFinite(val)) return { __type: val > 0 ? 'Infinity' : '-Infinity' };
54+
return val;
55+
}
56+
if (typeof val === 'string' || typeof val === 'boolean') return val;
57+
if (typeof val === 'bigint') return { __type: 'BigInt', value: val.toString() };
58+
if (typeof val === 'symbol') return { __type: 'Symbol', description: val.description };
59+
if (typeof val === 'function') return { __type: 'Function', name: val.name || 'anonymous' };
60+
61+
// Handle special objects
62+
if (val instanceof Date) return { __type: 'Date', value: val.toISOString() };
63+
if (val instanceof RegExp) return { __type: 'RegExp', source: val.source, flags: val.flags };
64+
if (val instanceof Error) return { __type: 'Error', name: val.name, message: val.message };
65+
if (val instanceof Map) return { __type: 'Map', entries: Array.from(val.entries()).map(([k, v]) => [serialize(k), serialize(v)]) };
66+
if (val instanceof Set) return { __type: 'Set', values: Array.from(val).map(serialize) };
67+
if (ArrayBuffer.isView(val)) return { __type: val.constructor.name, data: Array.from(val) };
68+
if (val instanceof ArrayBuffer) return { __type: 'ArrayBuffer', byteLength: val.byteLength };
69+
if (val instanceof Promise) return { __type: 'Promise' };
70+
71+
// Handle arrays
72+
if (Array.isArray(val)) {
73+
if (seen.has(val)) return { __type: 'CircularReference' };
74+
seen.add(val);
75+
return val.map(serialize);
76+
}
77+
78+
// Handle objects
79+
if (typeof val === 'object') {
80+
if (seen.has(val)) return { __type: 'CircularReference' };
81+
seen.add(val);
82+
const result = {};
83+
for (const key of Object.keys(val)) {
84+
try {
85+
result[key] = serialize(val[key]);
86+
} catch (e) {
87+
result[key] = { __type: 'UnserializableProperty', error: e.message };
88+
}
89+
}
90+
return result;
91+
}
92+
93+
return { __type: 'Unknown', typeof: typeof val };
94+
}
95+
96+
try {
97+
return serialize(value);
98+
} catch (e) {
99+
return { __type: 'SerializationError', error: e.message };
100+
}
101+
}
102+
103+
/**
104+
* Record a test result.
105+
*
106+
* @param {string} funcName - Name of the function being tested
107+
* @param {Array} args - Arguments passed to the function
108+
* @param {any} returnValue - Return value from the function
109+
* @param {Error|null} error - Error thrown by the function (if any)
110+
* @param {number} durationNs - Execution time in nanoseconds
111+
*/
112+
function recordResult(funcName, args, returnValue, error, durationNs) {
113+
const result = {
114+
testName: currentTestName,
115+
funcName,
116+
args: safeSerialize(args),
117+
returnValue: safeSerialize(returnValue),
118+
error: error ? {
119+
name: error.name,
120+
message: error.message,
121+
stack: error.stack
122+
} : null,
123+
durationNs: Math.round(durationNs),
124+
invocationId: invocationCounter++,
125+
loopIndex: LOOP_INDEX,
126+
mode: MODE,
127+
timestamp: Date.now()
128+
};
129+
results.push(result);
130+
}
131+
132+
/**
133+
* Capture a function call with full behavior tracking.
134+
*
135+
* This is the main API for instrumenting function calls.
136+
* It captures inputs, outputs, errors, and timing for every call.
137+
*
138+
* @param {string} funcName - Name of the function being tested
139+
* @param {Function} fn - The function to call
140+
* @param {...any} args - Arguments to pass to the function
141+
* @returns {any} - The function's return value
142+
* @throws {Error} - Re-throws any error from the function
143+
*/
144+
function capture(funcName, fn, ...args) {
145+
const startTime = performance.now();
146+
let returnValue;
147+
let error = null;
148+
149+
try {
150+
returnValue = fn(...args);
151+
152+
// Handle promises (async functions)
153+
if (returnValue instanceof Promise) {
154+
return returnValue.then(
155+
(resolved) => {
156+
const endTime = performance.now();
157+
const durationNs = (endTime - startTime) * 1_000_000;
158+
recordResult(funcName, args, resolved, null, durationNs);
159+
return resolved;
160+
},
161+
(err) => {
162+
const endTime = performance.now();
163+
const durationNs = (endTime - startTime) * 1_000_000;
164+
recordResult(funcName, args, null, err, durationNs);
165+
throw err;
166+
}
167+
);
168+
}
169+
} catch (e) {
170+
error = e;
171+
}
172+
173+
const endTime = performance.now();
174+
const durationNs = (endTime - startTime) * 1_000_000;
175+
recordResult(funcName, args, returnValue, error, durationNs);
176+
177+
if (error) throw error;
178+
return returnValue;
179+
}
180+
181+
/**
182+
* Capture multiple invocations for benchmarking.
183+
*
184+
* @param {string} funcName - Name of the function being tested
185+
* @param {Function} fn - The function to call
186+
* @param {Array<Array>} argsList - List of argument arrays to test
187+
* @returns {Array} - Array of return values
188+
*/
189+
function captureMultiple(funcName, fn, argsList) {
190+
return argsList.map(args => capture(funcName, fn, ...args));
191+
}
192+
193+
/**
194+
* Write results to output file.
195+
* Called automatically via Jest afterAll hook.
196+
*/
197+
function writeResults() {
198+
if (results.length === 0) return;
199+
200+
try {
201+
const output = {
202+
version: '1.0.0',
203+
mode: MODE,
204+
loopIndex: LOOP_INDEX,
205+
timestamp: Date.now(),
206+
results
207+
};
208+
const buffer = Buffer.from(JSON.stringify(output, null, 2));
209+
fs.writeFileSync(OUTPUT_FILE, buffer);
210+
} catch (e) {
211+
console.error('[codeflash] Error writing results:', e.message);
212+
}
213+
}
214+
215+
/**
216+
* Clear all recorded results.
217+
* Useful for resetting between test files.
218+
*/
219+
function clearResults() {
220+
results.length = 0;
221+
invocationCounter = 0;
222+
}
223+
224+
/**
225+
* Get the current results buffer.
226+
* Useful for debugging or custom result handling.
227+
*
228+
* @returns {Array} - Current results buffer
229+
*/
230+
function getResults() {
231+
return results;
232+
}
233+
234+
/**
235+
* Set the current test name.
236+
* Called automatically via Jest beforeEach hook.
237+
*
238+
* @param {string} name - Test name
239+
*/
240+
function setTestName(name) {
241+
currentTestName = name;
242+
invocationCounter = 0;
243+
}
244+
245+
// Jest lifecycle hooks - these run automatically when this module is imported
246+
if (typeof beforeEach !== 'undefined') {
247+
beforeEach(() => {
248+
// Get current test name from Jest's expect state
249+
try {
250+
currentTestName = expect.getState().currentTestName || 'unknown';
251+
} catch (e) {
252+
currentTestName = 'unknown';
253+
}
254+
invocationCounter = 0;
255+
});
256+
}
257+
258+
if (typeof afterAll !== 'undefined') {
259+
afterAll(() => {
260+
writeResults();
261+
});
262+
}
263+
264+
// Export public API
265+
module.exports = {
266+
capture,
267+
captureMultiple,
268+
writeResults,
269+
clearResults,
270+
getResults,
271+
setTestName,
272+
safeSerialize,
273+
// Constants
274+
MODE,
275+
LOOP_INDEX,
276+
OUTPUT_FILE
277+
};

0 commit comments

Comments
 (0)