mirror of
https://github.com/codeflash-ai/codeflash.git
synced 2026-05-04 18:25:17 +00:00
Merge pull request #2105 from codeflash-ai/feat/js-tracer-rebase-v2
feat(js): add JavaScript function tracer with Babel instrumentation
This commit is contained in:
commit
dc5090e2dd
13 changed files with 3228 additions and 401 deletions
244
codeflash/languages/javascript/replay_test.py
Normal file
244
codeflash/languages/javascript/replay_test.py
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
|
||||
@dataclass
|
||||
class JavaScriptFunctionModule:
|
||||
function_name: str
|
||||
file_name: Path
|
||||
module_name: str
|
||||
class_name: Optional[str] = None
|
||||
line_no: Optional[int] = None
|
||||
|
||||
|
||||
def get_next_arg_and_return(
|
||||
trace_file: str, function_name: str, file_name: str, class_name: Optional[str] = None, num_to_get: int = 25
|
||||
) -> Generator[Any]:
|
||||
db = sqlite3.connect(trace_file)
|
||||
cur = db.cursor()
|
||||
|
||||
try:
|
||||
cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = {row[0] for row in cur.fetchall()}
|
||||
|
||||
if "function_calls" in tables:
|
||||
if class_name:
|
||||
cursor = cur.execute(
|
||||
"SELECT args FROM function_calls WHERE function = ? AND filename = ? AND classname = ? AND type = 'call' ORDER BY time_ns ASC LIMIT ?",
|
||||
(function_name, file_name, class_name, num_to_get),
|
||||
)
|
||||
else:
|
||||
cursor = cur.execute(
|
||||
"SELECT args FROM function_calls WHERE function = ? AND filename = ? AND type = 'call' ORDER BY time_ns ASC LIMIT ?",
|
||||
(function_name, file_name, num_to_get),
|
||||
)
|
||||
|
||||
while (val := cursor.fetchone()) is not None:
|
||||
args_data = val[0]
|
||||
if isinstance(args_data, bytes):
|
||||
yield args_data
|
||||
else:
|
||||
yield args_data
|
||||
|
||||
elif "traces" in tables:
|
||||
if class_name:
|
||||
cursor = cur.execute(
|
||||
"SELECT args FROM traces WHERE function = ? AND file = ? ORDER BY id ASC LIMIT ?",
|
||||
(function_name, file_name, num_to_get),
|
||||
)
|
||||
else:
|
||||
cursor = cur.execute(
|
||||
"SELECT args FROM traces WHERE function = ? AND file = ? ORDER BY id ASC LIMIT ?",
|
||||
(function_name, file_name, num_to_get),
|
||||
)
|
||||
|
||||
while (val := cursor.fetchone()) is not None:
|
||||
yield val[0]
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def get_function_alias(module: str, function_name: str, class_name: Optional[str] = None) -> str:
|
||||
module_alias = re.sub(r"[^a-zA-Z0-9]", "_", module).strip("_")
|
||||
|
||||
if class_name:
|
||||
return f"{module_alias}_{class_name}_{function_name}"
|
||||
return f"{module_alias}_{function_name}"
|
||||
|
||||
|
||||
def create_javascript_replay_test(
|
||||
trace_file: str,
|
||||
functions: list[JavaScriptFunctionModule],
|
||||
max_run_count: int = 100,
|
||||
framework: str = "jest",
|
||||
project_root: Optional[Path] = None,
|
||||
) -> str:
|
||||
is_vitest = framework.lower() == "vitest"
|
||||
|
||||
imports = []
|
||||
|
||||
if is_vitest:
|
||||
imports.append("import { describe, test } from 'vitest';")
|
||||
|
||||
imports.append("const { getNextArg } = require('codeflash/replay');")
|
||||
imports.append("")
|
||||
|
||||
for func in functions:
|
||||
if func.function_name in ("__init__", "constructor"):
|
||||
continue
|
||||
|
||||
alias = get_function_alias(func.module_name, func.function_name, func.class_name)
|
||||
|
||||
if func.class_name:
|
||||
imports.append(f"const {{ {func.class_name}: {alias}_class }} = require('./{func.module_name}');")
|
||||
else:
|
||||
imports.append(f"const {{ {func.function_name}: {alias} }} = require('./{func.module_name}');")
|
||||
|
||||
imports.append("")
|
||||
|
||||
functions_to_test = [f.function_name for f in functions if f.function_name not in ("__init__", "constructor")]
|
||||
metadata = f"""const traceFilePath = '{trace_file}';
|
||||
const functions = {json.dumps(functions_to_test)};
|
||||
"""
|
||||
|
||||
test_cases = []
|
||||
|
||||
for func in functions:
|
||||
if func.function_name in ("__init__", "constructor"):
|
||||
continue
|
||||
|
||||
alias = get_function_alias(func.module_name, func.function_name, func.class_name)
|
||||
test_name = f"{func.class_name}.{func.function_name}" if func.class_name else func.function_name
|
||||
|
||||
if func.class_name:
|
||||
class_arg = f"'{func.class_name}'"
|
||||
test_body = textwrap.dedent(f"""
|
||||
describe('Replay: {test_name}', () => {{
|
||||
const traces = getNextArg(traceFilePath, '{func.function_name}', '{func.file_name.as_posix()}', {max_run_count}, {class_arg});
|
||||
|
||||
test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {{
|
||||
const instance = new {alias}_class();
|
||||
instance.{func.function_name}(...args);
|
||||
}});
|
||||
}});
|
||||
""")
|
||||
else:
|
||||
test_body = textwrap.dedent(f"""
|
||||
describe('Replay: {test_name}', () => {{
|
||||
const traces = getNextArg(traceFilePath, '{func.function_name}', '{func.file_name.as_posix()}', {max_run_count});
|
||||
|
||||
test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {{
|
||||
{alias}(...args);
|
||||
}});
|
||||
}});
|
||||
""")
|
||||
|
||||
test_cases.append(test_body)
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
"// Auto-generated replay test by Codeflash",
|
||||
"// Do not edit this file directly",
|
||||
"",
|
||||
*imports,
|
||||
metadata,
|
||||
*test_cases,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def get_traced_functions_from_db(trace_file: Path) -> list[JavaScriptFunctionModule]:
|
||||
if not trace_file.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(trace_file)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = {row[0] for row in cursor.fetchall()}
|
||||
|
||||
functions = []
|
||||
|
||||
if "function_calls" in tables:
|
||||
cursor.execute(
|
||||
"SELECT DISTINCT function, filename, classname, line_number FROM function_calls WHERE type = 'call'"
|
||||
)
|
||||
for row in cursor.fetchall():
|
||||
func_name = row[0]
|
||||
file_name = row[1]
|
||||
class_name = row[2]
|
||||
line_number = row[3]
|
||||
|
||||
module_path = file_name.replace("\\", "/").replace(".js", "").replace(".ts", "")
|
||||
module_path = module_path.removeprefix("./")
|
||||
|
||||
functions.append(
|
||||
JavaScriptFunctionModule(
|
||||
function_name=func_name,
|
||||
file_name=Path(file_name),
|
||||
module_name=module_path,
|
||||
class_name=class_name,
|
||||
line_no=line_number,
|
||||
)
|
||||
)
|
||||
|
||||
elif "traces" in tables:
|
||||
cursor.execute("SELECT DISTINCT function, file FROM traces")
|
||||
for row in cursor.fetchall():
|
||||
func_name = row[0]
|
||||
file_name = row[1]
|
||||
|
||||
module_path = file_name.replace("\\", "/").replace(".js", "").replace(".ts", "")
|
||||
module_path = module_path.removeprefix("./")
|
||||
|
||||
functions.append(
|
||||
JavaScriptFunctionModule(
|
||||
function_name=func_name, file_name=Path(file_name), module_name=module_path
|
||||
)
|
||||
)
|
||||
|
||||
conn.close()
|
||||
return functions
|
||||
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def create_replay_test_file(
|
||||
trace_file: Path,
|
||||
output_path: Path,
|
||||
framework: str = "jest",
|
||||
max_run_count: int = 100,
|
||||
project_root: Optional[Path] = None,
|
||||
) -> Optional[Path]:
|
||||
functions = get_traced_functions_from_db(trace_file)
|
||||
|
||||
if not functions:
|
||||
return None
|
||||
|
||||
content = create_javascript_replay_test(
|
||||
trace_file=str(trace_file),
|
||||
functions=functions,
|
||||
max_run_count=max_run_count,
|
||||
framework=framework,
|
||||
project_root=project_root,
|
||||
)
|
||||
|
||||
try:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(content, encoding="utf-8")
|
||||
return output_path
|
||||
except Exception:
|
||||
return None
|
||||
|
|
@ -1702,8 +1702,9 @@ class JavaScriptSupport:
|
|||
) -> str:
|
||||
"""Add behavior instrumentation to capture inputs/outputs.
|
||||
|
||||
For JavaScript, this wraps functions to capture their arguments
|
||||
and return values.
|
||||
For JavaScript, instrumentation is handled at runtime by the Babel tracer plugin
|
||||
(babel-tracer-plugin.js) via trace-runner.js. This method returns the source
|
||||
unchanged since no source-level transformation is needed.
|
||||
|
||||
Args:
|
||||
source: Source code to instrument.
|
||||
|
|
@ -1711,21 +1712,11 @@ class JavaScriptSupport:
|
|||
output_file: Optional output file for traces.
|
||||
|
||||
Returns:
|
||||
Instrumented source code.
|
||||
Source code unchanged (Babel handles instrumentation at runtime).
|
||||
|
||||
"""
|
||||
if not functions:
|
||||
return source
|
||||
|
||||
from codeflash.languages.javascript.tracer import JavaScriptTracer
|
||||
|
||||
# Use first function's file path if output_file not specified
|
||||
if output_file is None:
|
||||
file_path = functions[0].file_path
|
||||
output_file = file_path.parent / ".codeflash" / "traces.db"
|
||||
|
||||
tracer = JavaScriptTracer(output_file)
|
||||
return tracer.instrument_source(source, functions[0].file_path, list(functions))
|
||||
# JavaScript tracing is done at runtime via Babel plugin, not source transformation
|
||||
return source
|
||||
|
||||
def instrument_for_benchmarking(self, test_source: str, target_function: FunctionToOptimize) -> str:
|
||||
"""Add timing instrumentation to test code.
|
||||
|
|
|
|||
|
|
@ -1,35 +1,56 @@
|
|||
"""Function tracing instrumentation for JavaScript.
|
||||
|
||||
This module provides functionality to wrap JavaScript functions to capture their
|
||||
inputs, outputs, and execution behavior. This is used for generating replay tests
|
||||
and verifying optimization correctness.
|
||||
This module provides functionality to parse JavaScript function traces and generate
|
||||
replay tests. Tracing is performed via Babel AST transformation using the
|
||||
babel-tracer-plugin.js and trace-runner.js in the npm package.
|
||||
|
||||
The tracer uses Babel plugin for AST transformation which:
|
||||
- Works with both CommonJS and ESM
|
||||
- Handles async functions, arrow functions, methods correctly
|
||||
- Preserves source maps and formatting
|
||||
|
||||
Database Schema (matches Python tracer):
|
||||
- function_calls: Main trace data (type, function, classname, filename, line_number, time_ns, args)
|
||||
- metadata: Key-value metadata about the trace session
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sqlite3
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JavaScriptTracer:
|
||||
"""Instruments JavaScript code to capture function inputs and outputs.
|
||||
@dataclass
|
||||
class JavaScriptFunctionInfo:
|
||||
function_name: str
|
||||
file_name: str
|
||||
module_path: str
|
||||
class_name: Optional[str] = None
|
||||
line_number: Optional[int] = None
|
||||
|
||||
Similar to Python's tracing system, this wraps functions to record:
|
||||
- Input arguments
|
||||
- Return values
|
||||
- Exceptions thrown
|
||||
- Execution time
|
||||
|
||||
class JavaScriptTracer:
|
||||
"""Parses JavaScript function traces and generates replay tests.
|
||||
|
||||
Tracing is performed via Babel AST transformation (trace-runner.js).
|
||||
This class handles:
|
||||
- Parsing trace results from SQLite database
|
||||
- Extracting traced function information
|
||||
- Generating replay test files for Jest/Vitest
|
||||
"""
|
||||
|
||||
SCHEMA_VERSION = "1.0.0"
|
||||
|
||||
def __init__(self, output_db: Path) -> None:
|
||||
"""Initialize the tracer.
|
||||
|
||||
|
|
@ -38,322 +59,15 @@ class JavaScriptTracer:
|
|||
|
||||
"""
|
||||
self.output_db = output_db
|
||||
self.tracer_var = "__codeflash_tracer__"
|
||||
|
||||
def instrument_source(self, source: str, file_path: Path, functions: list[FunctionToOptimize]) -> str:
|
||||
"""Instrument JavaScript source code with function tracing.
|
||||
|
||||
Wraps specified functions to capture their inputs and outputs.
|
||||
|
||||
Args:
|
||||
source: Original JavaScript source code.
|
||||
file_path: Path to the source file.
|
||||
functions: List of functions to instrument.
|
||||
|
||||
Returns:
|
||||
Instrumented source code with tracing.
|
||||
|
||||
"""
|
||||
if not functions:
|
||||
return source
|
||||
|
||||
# Add tracer initialization at the top
|
||||
tracer_init = self._generate_tracer_init()
|
||||
|
||||
# Add instrumentation to each function
|
||||
lines = source.splitlines(keepends=True)
|
||||
|
||||
# Process functions in reverse order to preserve line numbers
|
||||
for func in sorted(functions, key=lambda f: f.starting_line, reverse=True):
|
||||
instrumented = self._instrument_function(func, lines, file_path)
|
||||
start_idx = func.starting_line - 1
|
||||
end_idx = func.ending_line
|
||||
lines = lines[:start_idx] + instrumented + lines[end_idx:]
|
||||
|
||||
instrumented_source = "".join(lines)
|
||||
|
||||
# Add tracer save at the end
|
||||
tracer_save = self._generate_tracer_save()
|
||||
|
||||
return tracer_init + "\n" + instrumented_source + "\n" + tracer_save
|
||||
|
||||
def _generate_tracer_init(self) -> str:
|
||||
"""Generate JavaScript code for tracer initialization."""
|
||||
return f"""
|
||||
// Codeflash function tracer initialization
|
||||
const {self.tracer_var} = {{
|
||||
traces: [],
|
||||
callId: 0,
|
||||
|
||||
serialize: function(value) {{
|
||||
try {{
|
||||
// Handle special cases
|
||||
if (value === undefined) return {{ __type__: 'undefined' }};
|
||||
if (value === null) return null;
|
||||
if (typeof value === 'function') return {{ __type__: 'function', name: value.name }};
|
||||
if (typeof value === 'symbol') return {{ __type__: 'symbol', value: value.toString() }};
|
||||
if (value instanceof Error) return {{
|
||||
__type__: 'error',
|
||||
name: value.name,
|
||||
message: value.message,
|
||||
stack: value.stack
|
||||
}};
|
||||
if (typeof value === 'bigint') return {{ __type__: 'bigint', value: value.toString() }};
|
||||
if (value instanceof Date) return {{ __type__: 'date', value: value.toISOString() }};
|
||||
if (value instanceof RegExp) return {{ __type__: 'regexp', value: value.toString() }};
|
||||
if (value instanceof Map) return {{
|
||||
__type__: 'map',
|
||||
value: Array.from(value.entries()).map(([k, v]) => [this.serialize(k), this.serialize(v)])
|
||||
}};
|
||||
if (value instanceof Set) return {{
|
||||
__type__: 'set',
|
||||
value: Array.from(value).map(v => this.serialize(v))
|
||||
}};
|
||||
|
||||
// Handle circular references with a simple check
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}} catch (e) {{
|
||||
return {{ __type__: 'unserializable', error: e.message }};
|
||||
}}
|
||||
}},
|
||||
|
||||
wrap: function(originalFunc, funcName, filePath) {{
|
||||
const self = this;
|
||||
|
||||
if (originalFunc.constructor.name === 'AsyncFunction') {{
|
||||
return async function(...args) {{
|
||||
const callId = self.callId++;
|
||||
const start = process.hrtime.bigint();
|
||||
let result, error;
|
||||
|
||||
try {{
|
||||
result = await originalFunc.apply(this, args);
|
||||
}} catch (e) {{
|
||||
error = e;
|
||||
}}
|
||||
|
||||
const end = process.hrtime.bigint();
|
||||
|
||||
self.traces.push({{
|
||||
call_id: callId,
|
||||
function: funcName,
|
||||
file: filePath,
|
||||
args: args.map(a => self.serialize(a)),
|
||||
result: error ? null : self.serialize(result),
|
||||
error: error ? self.serialize(error) : null,
|
||||
runtime_ns: (end - start).toString(),
|
||||
timestamp: Date.now()
|
||||
}});
|
||||
|
||||
if (error) throw error;
|
||||
return result;
|
||||
}};
|
||||
}}
|
||||
|
||||
return function(...args) {{
|
||||
const callId = self.callId++;
|
||||
const start = process.hrtime.bigint();
|
||||
let result, error;
|
||||
|
||||
try {{
|
||||
result = originalFunc.apply(this, args);
|
||||
}} catch (e) {{
|
||||
error = e;
|
||||
}}
|
||||
|
||||
const end = process.hrtime.bigint();
|
||||
|
||||
self.traces.push({{
|
||||
call_id: callId,
|
||||
function: funcName,
|
||||
file: filePath,
|
||||
args: args.map(a => self.serialize(a)),
|
||||
result: error ? null : self.serialize(result),
|
||||
error: error ? self.serialize(error) : null,
|
||||
runtime_ns: (end - start).toString(),
|
||||
timestamp: Date.now()
|
||||
}});
|
||||
|
||||
if (error) throw error;
|
||||
return result;
|
||||
}};
|
||||
}},
|
||||
|
||||
saveToDb: function() {{
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = '{self.output_db.as_posix()}';
|
||||
const dbDir = path.dirname(dbPath);
|
||||
|
||||
if (!fs.existsSync(dbDir)) {{
|
||||
fs.mkdirSync(dbDir, {{ recursive: true }});
|
||||
}}
|
||||
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
|
||||
db.serialize(() => {{
|
||||
// Create table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS traces (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
call_id INTEGER,
|
||||
function TEXT,
|
||||
file TEXT,
|
||||
args TEXT,
|
||||
result TEXT,
|
||||
error TEXT,
|
||||
runtime_ns TEXT,
|
||||
timestamp INTEGER
|
||||
)
|
||||
`);
|
||||
|
||||
// Insert traces
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO traces (call_id, function, file, args, result, error, runtime_ns, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (const trace of this.traces) {{
|
||||
stmt.run(
|
||||
trace.call_id,
|
||||
trace.function,
|
||||
trace.file,
|
||||
JSON.stringify(trace.args),
|
||||
JSON.stringify(trace.result),
|
||||
JSON.stringify(trace.error),
|
||||
trace.runtime_ns,
|
||||
trace.timestamp
|
||||
);
|
||||
}}
|
||||
|
||||
stmt.finalize();
|
||||
}});
|
||||
|
||||
db.close();
|
||||
}},
|
||||
|
||||
saveToJson: function() {{
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const jsonPath = '{self.output_db.with_suffix(".json").as_posix()}';
|
||||
const jsonDir = path.dirname(jsonPath);
|
||||
|
||||
if (!fs.existsSync(jsonDir)) {{
|
||||
fs.mkdirSync(jsonDir, {{ recursive: true }});
|
||||
}}
|
||||
|
||||
fs.writeFileSync(jsonPath, JSON.stringify(this.traces, null, 2));
|
||||
}}
|
||||
}};
|
||||
"""
|
||||
|
||||
def _generate_tracer_save(self) -> str:
|
||||
"""Generate JavaScript code to save tracer results."""
|
||||
return f"""
|
||||
// Save tracer results on process exit
|
||||
process.on('exit', () => {{
|
||||
try {{
|
||||
{self.tracer_var}.saveToJson();
|
||||
// Try SQLite, but don't fail if sqlite3 is not installed
|
||||
try {{
|
||||
{self.tracer_var}.saveToDb();
|
||||
}} catch (e) {{
|
||||
// SQLite not available, JSON is sufficient
|
||||
}}
|
||||
}} catch (e) {{
|
||||
console.error('Failed to save traces:', e);
|
||||
}}
|
||||
}});
|
||||
"""
|
||||
|
||||
def _instrument_function(self, func: FunctionToOptimize, lines: list[str], file_path: Path) -> list[str]:
|
||||
"""Instrument a single function with tracing.
|
||||
|
||||
Args:
|
||||
func: Function to instrument.
|
||||
lines: Source lines.
|
||||
file_path: Path to source file.
|
||||
|
||||
Returns:
|
||||
Instrumented function lines.
|
||||
|
||||
"""
|
||||
func_lines = lines[func.starting_line - 1 : func.ending_line]
|
||||
func_text = "".join(func_lines)
|
||||
|
||||
# Detect function pattern
|
||||
func_name = func.function_name
|
||||
is_arrow = "=>" in func_text.split("\n")[0]
|
||||
is_method = func.is_method
|
||||
is_async = func.is_async
|
||||
|
||||
# Generate wrapper code based on function type
|
||||
if is_arrow:
|
||||
# For arrow functions: const foo = (a, b) => { ... }
|
||||
# Replace with: const foo = __codeflash_tracer__.wrap((a, b) => { ... }, 'foo', 'file.js')
|
||||
return self._wrap_arrow_function(func_lines, func_name, file_path)
|
||||
if is_method:
|
||||
# For methods: methodName(a, b) { ... }
|
||||
# Wrap the method body
|
||||
return self._wrap_method(func_lines, func_name, file_path, is_async)
|
||||
# For regular functions: function foo(a, b) { ... }
|
||||
# Wrap the entire function
|
||||
return self._wrap_regular_function(func_lines, func_name, file_path, is_async)
|
||||
|
||||
def _wrap_arrow_function(self, func_lines: list[str], func_name: str, file_path: Path) -> list[str]:
|
||||
"""Wrap an arrow function with tracing."""
|
||||
# Find the assignment line
|
||||
first_line = func_lines[0]
|
||||
indent = len(first_line) - len(first_line.lstrip())
|
||||
indent_str = " " * indent
|
||||
|
||||
# Insert wrapper call
|
||||
func_text = "".join(func_lines).rstrip()
|
||||
|
||||
# Find the '=' and wrap everything after it
|
||||
if "=" in func_text:
|
||||
parts = func_text.split("=", 1)
|
||||
wrapped = f"{parts[0]}= {self.tracer_var}.wrap({parts[1]}, '{func_name}', '{file_path.as_posix()}');\n"
|
||||
return [wrapped]
|
||||
|
||||
return func_lines
|
||||
|
||||
def _wrap_method(self, func_lines: list[str], func_name: str, file_path: Path, is_async: bool) -> list[str]:
|
||||
"""Wrap a class method with tracing."""
|
||||
# For methods, we wrap by reassigning them after definition
|
||||
# This is complex, so for now we'll return unwrapped
|
||||
# TODO: Implement method wrapping
|
||||
logger.warning("Method wrapping not fully implemented for %s", func_name)
|
||||
return func_lines
|
||||
|
||||
def _wrap_regular_function(
|
||||
self, func_lines: list[str], func_name: str, file_path: Path, is_async: bool
|
||||
) -> list[str]:
|
||||
"""Wrap a regular function declaration with tracing."""
|
||||
# Replace: function foo(a, b) { ... }
|
||||
# With: const __original_foo = function foo(a, b) { ... }; const foo = __codeflash_tracer__.wrap(__original_foo, 'foo', 'file.js');
|
||||
|
||||
func_text = "".join(func_lines).rstrip()
|
||||
first_line = func_lines[0]
|
||||
indent = len(first_line) - len(first_line.lstrip())
|
||||
indent_str = " " * indent
|
||||
|
||||
wrapped = (
|
||||
f"{indent_str}const __original_{func_name}__ = {func_text};\n"
|
||||
f"{indent_str}const {func_name} = {self.tracer_var}.wrap(__original_{func_name}__, '{func_name}', '{file_path.as_posix()}');\n"
|
||||
)
|
||||
|
||||
return [wrapped]
|
||||
|
||||
@staticmethod
|
||||
def parse_results(trace_file: Path) -> list[dict[str, Any]]:
|
||||
"""Parse tracing results from output file.
|
||||
|
||||
Supports both the new function_calls schema and legacy traces schema.
|
||||
|
||||
Args:
|
||||
trace_file: Path to traces JSON file.
|
||||
trace_file: Path to traces file (SQLite or JSON).
|
||||
|
||||
Returns:
|
||||
List of trace records.
|
||||
|
|
@ -363,36 +77,59 @@ process.on('exit', () => {{
|
|||
|
||||
if json_file.exists():
|
||||
try:
|
||||
with json_file.open("r") as f:
|
||||
return json.load(f)
|
||||
with json_file.open("r", encoding="utf-8") as f:
|
||||
data: list[dict[str, Any]] = json.load(f)
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.exception("Failed to parse trace JSON: %s", e)
|
||||
return []
|
||||
|
||||
# Try SQLite database
|
||||
if not trace_file.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(trace_file)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM traces ORDER BY id")
|
||||
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = {row[0] for row in cursor.fetchall()}
|
||||
|
||||
traces = []
|
||||
for row in cursor.fetchall():
|
||||
traces.append(
|
||||
{
|
||||
"id": row[0],
|
||||
"call_id": row[1],
|
||||
"function": row[2],
|
||||
"file": row[3],
|
||||
"args": json.loads(row[4]),
|
||||
"result": json.loads(row[5]),
|
||||
"error": json.loads(row[6]) if row[6] != "null" else None,
|
||||
"runtime_ns": int(row[7]),
|
||||
"timestamp": row[8],
|
||||
}
|
||||
|
||||
if "function_calls" in tables:
|
||||
cursor.execute(
|
||||
"SELECT type, function, classname, filename, line_number, "
|
||||
"last_frame_address, time_ns, args FROM function_calls ORDER BY time_ns"
|
||||
)
|
||||
for row in cursor.fetchall():
|
||||
traces.append(
|
||||
{
|
||||
"type": row[0],
|
||||
"function": row[1],
|
||||
"classname": row[2],
|
||||
"filename": row[3],
|
||||
"line_number": row[4],
|
||||
"last_frame_address": row[5],
|
||||
"time_ns": row[6],
|
||||
"args": json.loads(row[7]) if row[7] else [],
|
||||
}
|
||||
)
|
||||
elif "traces" in tables:
|
||||
cursor.execute("SELECT * FROM traces ORDER BY id")
|
||||
for row in cursor.fetchall():
|
||||
traces.append(
|
||||
{
|
||||
"id": row[0],
|
||||
"call_id": row[1],
|
||||
"function": row[2],
|
||||
"file": row[3],
|
||||
"args": json.loads(row[4]) if row[4] else [],
|
||||
"result": json.loads(row[5]) if row[5] else None,
|
||||
"error": json.loads(row[6]) if row[6] and row[6] != "null" else None,
|
||||
"runtime_ns": int(row[7]) if row[7] else 0,
|
||||
"timestamp": row[8] if len(row) > 8 else None,
|
||||
}
|
||||
)
|
||||
|
||||
conn.close()
|
||||
return traces
|
||||
|
|
@ -400,3 +137,145 @@ process.on('exit', () => {{
|
|||
except Exception as e:
|
||||
logger.exception("Failed to parse trace database: %s", e)
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_traced_functions(trace_file: Path) -> list[JavaScriptFunctionInfo]:
|
||||
if not trace_file.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(trace_file)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = {row[0] for row in cursor.fetchall()}
|
||||
|
||||
functions = []
|
||||
|
||||
if "function_calls" in tables:
|
||||
cursor.execute(
|
||||
"SELECT DISTINCT function, filename, classname, line_number FROM function_calls WHERE type = 'call'"
|
||||
)
|
||||
for row in cursor.fetchall():
|
||||
func_name = row[0]
|
||||
file_name = row[1]
|
||||
class_name = row[2]
|
||||
line_number = row[3]
|
||||
|
||||
module_path = file_name.replace("\\", "/").replace(".js", "").replace(".ts", "")
|
||||
module_path = module_path.removeprefix("./")
|
||||
|
||||
functions.append(
|
||||
JavaScriptFunctionInfo(
|
||||
function_name=func_name,
|
||||
file_name=file_name,
|
||||
module_path=module_path,
|
||||
class_name=class_name,
|
||||
line_number=line_number,
|
||||
)
|
||||
)
|
||||
|
||||
conn.close()
|
||||
return functions
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Failed to get traced functions: %s", e)
|
||||
return []
|
||||
|
||||
def create_replay_test(
|
||||
self,
|
||||
trace_file: Path,
|
||||
output_path: Path,
|
||||
framework: str = "jest",
|
||||
max_run_count: int = 100,
|
||||
project_root: Optional[Path] = None,
|
||||
) -> Optional[str]:
|
||||
functions = self.get_traced_functions(trace_file)
|
||||
if not functions:
|
||||
logger.warning("No traced functions found in %s", trace_file)
|
||||
return None
|
||||
|
||||
is_vitest = framework.lower() == "vitest"
|
||||
|
||||
imports = []
|
||||
if is_vitest:
|
||||
imports.append("import { describe, test } from 'vitest';")
|
||||
|
||||
imports.append("const { getNextArg } = require('codeflash/replay');")
|
||||
imports.append("")
|
||||
|
||||
for func in functions:
|
||||
alias = self._get_function_alias(func.module_path, func.function_name, func.class_name)
|
||||
if func.class_name:
|
||||
imports.append(f"const {{ {func.class_name}: {alias}_class }} = require('./{func.module_path}');")
|
||||
else:
|
||||
imports.append(f"const {{ {func.function_name}: {alias} }} = require('./{func.module_path}');")
|
||||
|
||||
imports.append("")
|
||||
|
||||
trace_path = trace_file.as_posix()
|
||||
metadata = [
|
||||
f"const traceFilePath = '{trace_path}';",
|
||||
f"const functions = {json.dumps([f.function_name for f in functions])};",
|
||||
"",
|
||||
]
|
||||
|
||||
test_cases = []
|
||||
for func in functions:
|
||||
alias = self._get_function_alias(func.module_path, func.function_name, func.class_name)
|
||||
test_name = f"{func.class_name}.{func.function_name}" if func.class_name else func.function_name
|
||||
class_arg = f"'{func.class_name}'" if func.class_name else "null"
|
||||
|
||||
if func.class_name:
|
||||
test_cases.append(
|
||||
textwrap.dedent(f"""
|
||||
describe('Replay: {test_name}', () => {{
|
||||
const traces = getNextArg(traceFilePath, '{func.function_name}', '{func.file_name}', {max_run_count}, {class_arg});
|
||||
|
||||
test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {{
|
||||
const instance = new {alias}_class();
|
||||
instance.{func.function_name}(...args);
|
||||
}});
|
||||
}});
|
||||
""")
|
||||
)
|
||||
else:
|
||||
test_cases.append(
|
||||
textwrap.dedent(f"""
|
||||
describe('Replay: {test_name}', () => {{
|
||||
const traces = getNextArg(traceFilePath, '{func.function_name}', '{func.file_name}', {max_run_count});
|
||||
|
||||
test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {{
|
||||
{alias}(...args);
|
||||
}});
|
||||
}});
|
||||
""")
|
||||
)
|
||||
|
||||
content = "\n".join(
|
||||
[
|
||||
"// Auto-generated replay test by Codeflash",
|
||||
"// Do not edit this file directly",
|
||||
"",
|
||||
*imports,
|
||||
*metadata,
|
||||
*test_cases,
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(content, encoding="utf-8")
|
||||
logger.info("Generated replay test: %s", output_path)
|
||||
return str(output_path)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to write replay test: %s", e)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_function_alias(module_path: str, function_name: str, class_name: Optional[str] = None) -> str:
|
||||
module_alias = re.sub(r"[^a-zA-Z0-9]", "_", module_path).strip("_")
|
||||
|
||||
if class_name:
|
||||
return f"{module_alias}_{class_name}_{function_name}"
|
||||
return f"{module_alias}_{function_name}"
|
||||
|
|
|
|||
231
codeflash/languages/javascript/tracer_runner.py
Normal file
231
codeflash/languages/javascript/tracer_runner.py
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_node_executable() -> Optional[Path]:
|
||||
node_path = shutil.which("node")
|
||||
if node_path:
|
||||
return Path(node_path)
|
||||
|
||||
npx_path = shutil.which("npx")
|
||||
if npx_path:
|
||||
return Path(npx_path)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_trace_runner() -> Optional[Path]:
|
||||
cwd = Path.cwd()
|
||||
|
||||
local_path = cwd / "node_modules" / "codeflash" / "runtime" / "trace-runner.js"
|
||||
if local_path.exists():
|
||||
return local_path
|
||||
|
||||
try:
|
||||
result = subprocess.run(["npm", "root", "-g"], capture_output=True, text=True, check=True)
|
||||
global_modules = Path(result.stdout.strip())
|
||||
global_path = global_modules / "codeflash" / "runtime" / "trace-runner.js"
|
||||
if global_path.exists():
|
||||
return global_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
bundled_path = Path(__file__).parent.parent.parent.parent / "packages" / "codeflash" / "runtime" / "trace-runner.js"
|
||||
if bundled_path.exists():
|
||||
return bundled_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def run_javascript_tracer(args: Namespace, config: dict[str, Any], project_root: Path) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {"success": False, "trace_file": None, "replay_test_file": None, "error": None}
|
||||
|
||||
node_path = find_node_executable()
|
||||
if not node_path:
|
||||
result["error"] = "Node.js not found. Please install Node.js to use JavaScript tracing."
|
||||
logger.error(result["error"])
|
||||
return result
|
||||
|
||||
trace_runner_path = find_trace_runner()
|
||||
if not trace_runner_path:
|
||||
result["error"] = "trace-runner.js not found. Please install the codeflash npm package."
|
||||
logger.error(result["error"])
|
||||
return result
|
||||
|
||||
outfile = getattr(args, "outfile", None) or "codeflash.trace.sqlite"
|
||||
trace_file = Path(outfile).resolve()
|
||||
|
||||
env = os.environ.copy()
|
||||
env["CODEFLASH_TRACE_DB"] = str(trace_file)
|
||||
env["CODEFLASH_PROJECT_ROOT"] = str(project_root)
|
||||
|
||||
max_count = getattr(args, "max_function_count", 256)
|
||||
env["CODEFLASH_MAX_FUNCTION_COUNT"] = str(max_count)
|
||||
|
||||
timeout = getattr(args, "tracer_timeout", None)
|
||||
if timeout:
|
||||
env["CODEFLASH_TRACER_TIMEOUT"] = str(timeout)
|
||||
|
||||
only_functions = getattr(args, "only_functions", None)
|
||||
if only_functions:
|
||||
env["CODEFLASH_FUNCTIONS"] = json.dumps(only_functions)
|
||||
|
||||
cmd = [str(node_path), str(trace_runner_path)]
|
||||
|
||||
cmd.extend(["--trace-db", str(trace_file)])
|
||||
cmd.extend(["--project-root", str(project_root)])
|
||||
|
||||
if max_count:
|
||||
cmd.extend(["--max-function-count", str(max_count)])
|
||||
|
||||
if timeout:
|
||||
cmd.extend(["--timeout", str(timeout)])
|
||||
|
||||
if only_functions:
|
||||
cmd.extend(["--functions", json.dumps(only_functions)])
|
||||
|
||||
is_module = getattr(args, "module", False)
|
||||
script_args = []
|
||||
|
||||
if hasattr(args, "script_args"):
|
||||
script_args = args.script_args
|
||||
elif hasattr(args, "unknown_args"):
|
||||
script_args = args.unknown_args
|
||||
|
||||
if is_module and script_args and script_args[0] == "jest":
|
||||
cmd.append("--jest")
|
||||
cmd.append("--")
|
||||
cmd.extend(script_args[1:])
|
||||
elif is_module and script_args and script_args[0] == "vitest":
|
||||
cmd.append("--vitest")
|
||||
cmd.append("--")
|
||||
cmd.extend(script_args[1:])
|
||||
elif script_args:
|
||||
cmd.extend(script_args)
|
||||
|
||||
logger.info("Running JavaScript tracer: %s", " ".join(cmd))
|
||||
|
||||
try:
|
||||
process = subprocess.run(cmd, cwd=project_root, env=env, capture_output=False, check=False)
|
||||
|
||||
if process.returncode != 0:
|
||||
result["error"] = f"Tracing failed with exit code {process.returncode}"
|
||||
logger.error(result["error"])
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = f"Failed to run tracer: {e}"
|
||||
logger.exception(result["error"])
|
||||
return result
|
||||
|
||||
if not trace_file.exists():
|
||||
result["error"] = f"Trace file not created: {trace_file}"
|
||||
logger.error(result["error"])
|
||||
return result
|
||||
|
||||
result["success"] = True
|
||||
result["trace_file"] = str(trace_file)
|
||||
|
||||
trace_only = getattr(args, "trace_only", False)
|
||||
if not trace_only:
|
||||
replay_test_path = generate_replay_test(trace_file=trace_file, project_root=project_root, config=config)
|
||||
if replay_test_path:
|
||||
result["replay_test_file"] = str(replay_test_path)
|
||||
logger.info("Generated replay test: %s", replay_test_path)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_replay_test(
|
||||
trace_file: Path, project_root: Path, config: dict[str, Any], output_path: Optional[Path] = None
|
||||
) -> Optional[Path]:
|
||||
from codeflash.languages.javascript.replay_test import create_replay_test_file
|
||||
|
||||
framework = detect_test_framework(project_root, config)
|
||||
|
||||
if output_path is None:
|
||||
tests_root = config.get("tests_root", "tests")
|
||||
tests_dir = project_root / tests_root
|
||||
output_path = tests_dir / "codeflash_replay.test.js"
|
||||
|
||||
return create_replay_test_file(
|
||||
trace_file=trace_file,
|
||||
output_path=output_path,
|
||||
framework=framework,
|
||||
max_run_count=100,
|
||||
project_root=project_root,
|
||||
)
|
||||
|
||||
|
||||
def detect_test_framework(project_root: Path, config: dict[str, Any]) -> str:
|
||||
if "test_framework" in config:
|
||||
framework: str = config["test_framework"]
|
||||
return framework
|
||||
|
||||
vitest_configs = ["vitest.config.js", "vitest.config.ts", "vitest.config.mjs"]
|
||||
for conf in vitest_configs:
|
||||
if (project_root / conf).exists():
|
||||
return "vitest"
|
||||
|
||||
jest_configs = ["jest.config.js", "jest.config.ts", "jest.config.mjs", "jest.config.json"]
|
||||
for conf in jest_configs:
|
||||
if (project_root / conf).exists():
|
||||
return "jest"
|
||||
|
||||
package_json = project_root / "package.json"
|
||||
if package_json.exists():
|
||||
try:
|
||||
with package_json.open(encoding="utf-8") as f:
|
||||
pkg = json.load(f)
|
||||
test_script = pkg.get("scripts", {}).get("test", "")
|
||||
if "vitest" in test_script:
|
||||
return "vitest"
|
||||
if "jest" in test_script:
|
||||
return "jest"
|
||||
|
||||
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
||||
if "vitest" in deps:
|
||||
return "vitest"
|
||||
if "jest" in deps:
|
||||
return "jest"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "jest"
|
||||
|
||||
|
||||
def check_javascript_tracer_available() -> bool:
|
||||
if not find_node_executable():
|
||||
return False
|
||||
|
||||
if not find_trace_runner():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_tracer_requirements_message() -> str:
|
||||
missing = []
|
||||
|
||||
if not find_node_executable():
|
||||
missing.append("Node.js (v18+)")
|
||||
|
||||
if not find_trace_runner():
|
||||
missing.append("codeflash npm package (npm install codeflash)")
|
||||
|
||||
if not missing:
|
||||
return "All requirements met for JavaScript tracing."
|
||||
|
||||
return "Missing requirements for JavaScript tracing:\n- " + "\n- ".join(missing)
|
||||
|
|
@ -149,6 +149,11 @@ def main(args: Namespace | None = None) -> ArgumentParser:
|
|||
parser.add_argument(
|
||||
"--limit", type=int, default=None, help="Limit the number of test files to process (for -m pytest mode)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--language",
|
||||
help="Language to trace (python, javascript, typescript). Auto-detected if not specified.",
|
||||
default=None,
|
||||
)
|
||||
|
||||
if args is not None:
|
||||
parsed_args = args
|
||||
|
|
@ -182,6 +187,13 @@ def main(args: Namespace | None = None) -> ArgumentParser:
|
|||
outfile = parsed_args.outfile
|
||||
config, found_config_path = parse_config_file(parsed_args.codeflash_config)
|
||||
project_root = project_root_from_module_root(Path(config["module_root"]), found_config_path)
|
||||
|
||||
language = getattr(parsed_args, "language", None) or config.get("language", "python")
|
||||
if language in ("javascript", "typescript"):
|
||||
return run_javascript_tracer_main(
|
||||
parsed_args=parsed_args, config=config, project_root=project_root, unknown_args=unknown_args
|
||||
)
|
||||
|
||||
if len(unknown_args) > 0:
|
||||
args_dict = {
|
||||
"functions": parsed_args.only_functions,
|
||||
|
|
@ -332,6 +344,87 @@ def main(args: Namespace | None = None) -> ArgumentParser:
|
|||
return parser
|
||||
|
||||
|
||||
def run_javascript_tracer_main(
|
||||
parsed_args: Namespace, config: dict, project_root: Path, unknown_args: list[str]
|
||||
) -> ArgumentParser:
|
||||
from codeflash.languages.javascript.tracer_runner import (
|
||||
check_javascript_tracer_available,
|
||||
detect_test_framework,
|
||||
get_tracer_requirements_message,
|
||||
run_javascript_tracer,
|
||||
)
|
||||
|
||||
if not check_javascript_tracer_available():
|
||||
console.print("[bold red]Error:[/] JavaScript tracer requirements not met.")
|
||||
console.print(get_tracer_requirements_message())
|
||||
sys.exit(1)
|
||||
|
||||
trace_only = getattr(parsed_args, "trace_only", False)
|
||||
only_functions = getattr(parsed_args, "only_functions", None)
|
||||
max_function_count = getattr(parsed_args, "max_function_count", 256)
|
||||
timeout = getattr(parsed_args, "tracer_timeout", None)
|
||||
|
||||
framework = detect_test_framework(project_root, config)
|
||||
logger.info("JavaScript tracer: framework=%s, project_root=%s", framework, project_root)
|
||||
|
||||
trace_db_path = get_run_tmp_file(Path("js_trace.sqlite"))
|
||||
|
||||
script_or_test_args = unknown_args if unknown_args else []
|
||||
|
||||
replay_test_path = run_javascript_tracer(
|
||||
script_args=script_or_test_args,
|
||||
trace_db_path=trace_db_path,
|
||||
project_root=project_root,
|
||||
functions=only_functions,
|
||||
max_function_count=max_function_count,
|
||||
timeout=int(timeout) if timeout else 0,
|
||||
framework=framework,
|
||||
)
|
||||
|
||||
if replay_test_path and not trace_only:
|
||||
from codeflash.cli_cmds.cli import parse_args as cli_parse_args
|
||||
from codeflash.cli_cmds.cli import process_pyproject_config
|
||||
from codeflash.cli_cmds.console import paneled_text
|
||||
from codeflash.cli_cmds.console_constants import CODEFLASH_LOGO
|
||||
from codeflash.languages import Language, set_current_language
|
||||
from codeflash.optimization import optimizer
|
||||
from codeflash.telemetry import posthog_cf
|
||||
from codeflash.telemetry.sentry import init_sentry
|
||||
|
||||
language = getattr(parsed_args, "language", None) or config.get("language", "javascript")
|
||||
if language == "typescript":
|
||||
set_current_language(Language.TYPESCRIPT)
|
||||
else:
|
||||
set_current_language(Language.JAVASCRIPT)
|
||||
|
||||
sys.argv = ["codeflash", "--replay-test", str(replay_test_path)]
|
||||
args = cli_parse_args()
|
||||
paneled_text(
|
||||
CODEFLASH_LOGO,
|
||||
panel_args={"title": "https://codeflash.ai", "expand": False},
|
||||
text_args={"style": "bold gold3"},
|
||||
)
|
||||
|
||||
args = process_pyproject_config(args)
|
||||
args.previous_checkpoint_functions = None
|
||||
init_sentry(enabled=not args.disable_telemetry, exclude_errors=True)
|
||||
posthog_cf.initialize_posthog(enabled=not args.disable_telemetry)
|
||||
|
||||
args.effort = EffortLevel.HIGH.value
|
||||
optimizer.run_with_args(args)
|
||||
|
||||
# Clean up
|
||||
trace_db_path.unlink(missing_ok=True)
|
||||
if replay_test_path:
|
||||
Path(replay_test_path).unlink(missing_ok=True)
|
||||
elif replay_test_path:
|
||||
console.print(f"[bold green]Trace complete.[/] Replay test: {replay_test_path}")
|
||||
else:
|
||||
console.print("[bold yellow]Warning:[/] No functions were traced.")
|
||||
|
||||
return ArgumentParser()
|
||||
|
||||
|
||||
def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser:
|
||||
"""Run the Java two-stage tracer (JFR + argument capture) and optionally optimize."""
|
||||
from codeflash.cli_cmds.cli import parse_args, process_pyproject_config
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"types": "runtime/index.d.ts",
|
||||
"bin": {
|
||||
"codeflash": "./bin/codeflash.js",
|
||||
"codeflash-setup": "./bin/codeflash-setup.js"
|
||||
"codeflash-setup": "./bin/codeflash-setup.js",
|
||||
"codeflash-trace": "./runtime/trace-runner.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
|
@ -36,6 +37,18 @@
|
|||
"./jest-reporter": {
|
||||
"require": "./runtime/jest-reporter.js",
|
||||
"import": "./runtime/jest-reporter.js"
|
||||
},
|
||||
"./tracer": {
|
||||
"require": "./runtime/tracer.js",
|
||||
"import": "./runtime/tracer.js"
|
||||
},
|
||||
"./replay": {
|
||||
"require": "./runtime/replay.js",
|
||||
"import": "./runtime/replay.js"
|
||||
},
|
||||
"./babel-tracer-plugin": {
|
||||
"require": "./runtime/babel-tracer-plugin.js",
|
||||
"import": "./runtime/babel-tracer-plugin.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -92,5 +105,11 @@
|
|||
"dependencies": {
|
||||
"better-sqlite3": "^12.0.0",
|
||||
"@msgpack/msgpack": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/register": "^7.24.0",
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@babel/preset-typescript": "^7.24.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
434
packages/codeflash/runtime/babel-tracer-plugin.js
Normal file
434
packages/codeflash/runtime/babel-tracer-plugin.js
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
/**
|
||||
* Codeflash Babel Tracer Plugin
|
||||
*
|
||||
* A Babel plugin that instruments JavaScript/TypeScript functions for tracing.
|
||||
* This plugin wraps functions with tracing calls to capture:
|
||||
* - Function arguments
|
||||
* - Return values
|
||||
* - Execution time
|
||||
*
|
||||
* The plugin transforms:
|
||||
* function foo(a, b) { return a + b; }
|
||||
*
|
||||
* Into:
|
||||
* const __codeflash_tracer__ = require('codeflash/tracer');
|
||||
* function foo(a, b) {
|
||||
* return __codeflash_tracer__.wrap(function foo(a, b) { return a + b; }, 'foo', '/path/file.js', 1)
|
||||
* .apply(this, arguments);
|
||||
* }
|
||||
*
|
||||
* Supported function types:
|
||||
* - FunctionDeclaration: function foo() {}
|
||||
* - FunctionExpression: const foo = function() {}
|
||||
* - ArrowFunctionExpression: const foo = () => {}
|
||||
* - ClassMethod: class Foo { bar() {} }
|
||||
* - ObjectMethod: const obj = { foo() {} }
|
||||
*
|
||||
* Configuration (via plugin options or environment variables):
|
||||
* - functions: Array of function names to trace (traces all if not set)
|
||||
* - files: Array of file patterns to trace (traces all if not set)
|
||||
* - exclude: Array of patterns to exclude from tracing
|
||||
*
|
||||
* Usage with @babel/register:
|
||||
* require('@babel/register')({
|
||||
* plugins: [['codeflash/babel-tracer-plugin', { functions: ['myFunc'] }]],
|
||||
* });
|
||||
*
|
||||
* Environment Variables:
|
||||
* CODEFLASH_FUNCTIONS - JSON array of functions to trace
|
||||
* CODEFLASH_TRACE_FILES - JSON array of file patterns to trace
|
||||
* CODEFLASH_TRACE_EXCLUDE - JSON array of patterns to exclude
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// Parse environment variables for configuration
|
||||
function getEnvConfig() {
|
||||
const config = {
|
||||
functions: null,
|
||||
files: null,
|
||||
exclude: null,
|
||||
};
|
||||
|
||||
try {
|
||||
if (process.env.CODEFLASH_FUNCTIONS) {
|
||||
config.functions = JSON.parse(process.env.CODEFLASH_FUNCTIONS);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[codeflash-babel] Failed to parse CODEFLASH_FUNCTIONS:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.env.CODEFLASH_TRACE_FILES) {
|
||||
config.files = JSON.parse(process.env.CODEFLASH_TRACE_FILES);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[codeflash-babel] Failed to parse CODEFLASH_TRACE_FILES:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.env.CODEFLASH_TRACE_EXCLUDE) {
|
||||
config.exclude = JSON.parse(process.env.CODEFLASH_TRACE_EXCLUDE);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[codeflash-babel] Failed to parse CODEFLASH_TRACE_EXCLUDE:', e.message);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a function should be traced based on configuration.
|
||||
*
|
||||
* @param {string} funcName - Function name
|
||||
* @param {string} fileName - File path
|
||||
* @param {string|null} className - Class name (for methods)
|
||||
* @param {Object} config - Plugin configuration
|
||||
* @returns {boolean} - True if function should be traced
|
||||
*/
|
||||
function shouldTraceFunction(funcName, fileName, className, config) {
|
||||
// Check exclude patterns first
|
||||
if (config.exclude && config.exclude.length > 0) {
|
||||
for (const pattern of config.exclude) {
|
||||
if (typeof pattern === 'string') {
|
||||
if (funcName === pattern || fileName.includes(pattern)) {
|
||||
return false;
|
||||
}
|
||||
} else if (pattern instanceof RegExp) {
|
||||
if (pattern.test(funcName) || pattern.test(fileName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check file patterns
|
||||
if (config.files && config.files.length > 0) {
|
||||
const matchesFile = config.files.some(pattern => {
|
||||
if (typeof pattern === 'string') {
|
||||
return fileName.includes(pattern);
|
||||
}
|
||||
if (pattern instanceof RegExp) {
|
||||
return pattern.test(fileName);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!matchesFile) return false;
|
||||
}
|
||||
|
||||
// Check function names
|
||||
if (config.functions && config.functions.length > 0) {
|
||||
const matchesName = config.functions.some(f => {
|
||||
if (typeof f === 'string') {
|
||||
return f === funcName || f === `${className}.${funcName}`;
|
||||
}
|
||||
// Support object format: { function: 'name', file: 'path', class: 'className' }
|
||||
if (typeof f === 'object' && f !== null) {
|
||||
if (f.function && f.function !== funcName) return false;
|
||||
if (f.file && !fileName.includes(f.file)) return false;
|
||||
if (f.class && f.class !== className) return false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!matchesName) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path should be excluded from tracing (node_modules, etc.)
|
||||
*
|
||||
* @param {string} fileName - File path
|
||||
* @returns {boolean} - True if file should be excluded
|
||||
*/
|
||||
function isExcludedPath(fileName) {
|
||||
// Always exclude node_modules
|
||||
if (fileName.includes('node_modules')) return true;
|
||||
|
||||
// Exclude common test runner internals
|
||||
if (fileName.includes('jest-runner') || fileName.includes('jest-jasmine')) return true;
|
||||
if (fileName.includes('@vitest')) return true;
|
||||
|
||||
// Exclude this plugin itself
|
||||
if (fileName.includes('codeflash/runtime')) return true;
|
||||
if (fileName.includes('babel-tracer-plugin')) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the Babel plugin.
|
||||
*
|
||||
* @param {Object} babel - Babel object with types (t)
|
||||
* @returns {Object} - Babel plugin configuration
|
||||
*/
|
||||
module.exports = function codeflashTracerPlugin(babel) {
|
||||
const { types: t } = babel;
|
||||
|
||||
// Merge environment config with plugin options
|
||||
const envConfig = getEnvConfig();
|
||||
|
||||
return {
|
||||
name: 'codeflash-tracer',
|
||||
|
||||
visitor: {
|
||||
Program: {
|
||||
enter(programPath, state) {
|
||||
// Merge options from plugin config and environment
|
||||
state.codeflashConfig = {
|
||||
...envConfig,
|
||||
...(state.opts || {}),
|
||||
};
|
||||
|
||||
// Track whether we've added the tracer import
|
||||
state.tracerImportAdded = false;
|
||||
|
||||
// Get file info
|
||||
state.fileName = state.filename || state.file.opts.filename || 'unknown';
|
||||
|
||||
// Check if entire file should be excluded
|
||||
if (isExcludedPath(state.fileName)) {
|
||||
state.skipFile = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state.skipFile = false;
|
||||
},
|
||||
|
||||
exit(programPath, state) {
|
||||
// Add tracer import if we instrumented any functions
|
||||
if (state.tracerImportAdded) {
|
||||
const tracerRequire = t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.identifier('__codeflash_tracer__'),
|
||||
t.callExpression(
|
||||
t.identifier('require'),
|
||||
[t.stringLiteral('codeflash/tracer')]
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
// Add at the beginning of the program
|
||||
programPath.unshiftContainer('body', tracerRequire);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Handle: function foo() {}
|
||||
FunctionDeclaration(path, state) {
|
||||
if (state.skipFile) return;
|
||||
if (!path.node.id) return; // Skip anonymous functions
|
||||
|
||||
const funcName = path.node.id.name;
|
||||
const lineNumber = path.node.loc ? path.node.loc.start.line : 0;
|
||||
|
||||
if (!shouldTraceFunction(funcName, state.fileName, null, state.codeflashConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform the function body to wrap with tracing
|
||||
wrapFunctionBody(t, path, funcName, state.fileName, lineNumber, null);
|
||||
state.tracerImportAdded = true;
|
||||
},
|
||||
|
||||
// Handle: const foo = function() {} or const foo = () => {}
|
||||
VariableDeclarator(path, state) {
|
||||
if (state.skipFile) return;
|
||||
if (!t.isIdentifier(path.node.id)) return;
|
||||
if (!path.node.init) return;
|
||||
|
||||
const init = path.node.init;
|
||||
if (!t.isFunctionExpression(init) && !t.isArrowFunctionExpression(init)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const funcName = path.node.id.name;
|
||||
const lineNumber = path.node.loc ? path.node.loc.start.line : 0;
|
||||
|
||||
if (!shouldTraceFunction(funcName, state.fileName, null, state.codeflashConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrap the function expression with tracer.wrap()
|
||||
path.node.init = createWrapperCall(t, init, funcName, state.fileName, lineNumber, null);
|
||||
state.tracerImportAdded = true;
|
||||
},
|
||||
|
||||
// Handle: class Foo { bar() {} }
|
||||
ClassMethod(path, state) {
|
||||
if (state.skipFile) return;
|
||||
if (path.node.kind === 'constructor') return; // Skip constructors for now
|
||||
|
||||
const funcName = path.node.key.name || (path.node.key.value && String(path.node.key.value));
|
||||
if (!funcName) return;
|
||||
|
||||
// Get class name from parent
|
||||
const classPath = path.findParent(p => t.isClassDeclaration(p) || t.isClassExpression(p));
|
||||
const className = classPath && classPath.node.id ? classPath.node.id.name : null;
|
||||
|
||||
const lineNumber = path.node.loc ? path.node.loc.start.line : 0;
|
||||
|
||||
if (!shouldTraceFunction(funcName, state.fileName, className, state.codeflashConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrap the method body
|
||||
wrapMethodBody(t, path, funcName, state.fileName, lineNumber, className);
|
||||
state.tracerImportAdded = true;
|
||||
},
|
||||
|
||||
// Handle: const obj = { foo() {} }
|
||||
ObjectMethod(path, state) {
|
||||
if (state.skipFile) return;
|
||||
|
||||
const funcName = path.node.key.name || (path.node.key.value && String(path.node.key.value));
|
||||
if (!funcName) return;
|
||||
|
||||
const lineNumber = path.node.loc ? path.node.loc.start.line : 0;
|
||||
|
||||
if (!shouldTraceFunction(funcName, state.fileName, null, state.codeflashConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrap the method body
|
||||
wrapMethodBody(t, path, funcName, state.fileName, lineNumber, null);
|
||||
state.tracerImportAdded = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a __codeflash_tracer__.wrap() call expression.
|
||||
*
|
||||
* @param {Object} t - Babel types
|
||||
* @param {Object} funcNode - The function AST node
|
||||
* @param {string} funcName - Function name
|
||||
* @param {string} fileName - File path
|
||||
* @param {number} lineNumber - Line number
|
||||
* @param {string|null} className - Class name
|
||||
* @returns {Object} - Call expression AST node
|
||||
*/
|
||||
function createWrapperCall(t, funcNode, funcName, fileName, lineNumber, className) {
|
||||
const args = [
|
||||
funcNode,
|
||||
t.stringLiteral(funcName),
|
||||
t.stringLiteral(fileName),
|
||||
t.numericLiteral(lineNumber),
|
||||
];
|
||||
|
||||
if (className) {
|
||||
args.push(t.stringLiteral(className));
|
||||
} else {
|
||||
args.push(t.nullLiteral());
|
||||
}
|
||||
|
||||
return t.callExpression(
|
||||
t.memberExpression(
|
||||
t.identifier('__codeflash_tracer__'),
|
||||
t.identifier('wrap')
|
||||
),
|
||||
args
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a function declaration's body with tracing.
|
||||
* Transforms:
|
||||
* function foo(a, b) { return a + b; }
|
||||
* Into:
|
||||
* function foo(a, b) {
|
||||
* const __original__ = function(a, b) { return a + b; };
|
||||
* return __codeflash_tracer__.wrap(__original__, 'foo', 'file.js', 1, null).apply(this, arguments);
|
||||
* }
|
||||
*
|
||||
* @param {Object} t - Babel types
|
||||
* @param {Object} path - Babel path
|
||||
* @param {string} funcName - Function name
|
||||
* @param {string} fileName - File path
|
||||
* @param {number} lineNumber - Line number
|
||||
* @param {string|null} className - Class name
|
||||
*/
|
||||
function wrapFunctionBody(t, path, funcName, fileName, lineNumber, className) {
|
||||
const node = path.node;
|
||||
const isAsync = node.async;
|
||||
const isGenerator = node.generator;
|
||||
|
||||
// Create a copy of the original function as an expression
|
||||
const originalFunc = t.functionExpression(
|
||||
null, // anonymous
|
||||
node.params,
|
||||
node.body,
|
||||
isGenerator,
|
||||
isAsync
|
||||
);
|
||||
|
||||
// Create the wrapper call
|
||||
const wrapperCall = createWrapperCall(t, originalFunc, funcName, fileName, lineNumber, className);
|
||||
|
||||
// Create: return __codeflash_tracer__.wrap(...).apply(this, arguments)
|
||||
const applyCall = t.callExpression(
|
||||
t.memberExpression(wrapperCall, t.identifier('apply')),
|
||||
[t.thisExpression(), t.identifier('arguments')]
|
||||
);
|
||||
|
||||
const returnStatement = t.returnStatement(applyCall);
|
||||
|
||||
// Replace the function body
|
||||
node.body = t.blockStatement([returnStatement]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a method's body with tracing.
|
||||
* Similar to wrapFunctionBody but preserves method semantics.
|
||||
*
|
||||
* @param {Object} t - Babel types
|
||||
* @param {Object} path - Babel path
|
||||
* @param {string} funcName - Function name
|
||||
* @param {string} fileName - File path
|
||||
* @param {number} lineNumber - Line number
|
||||
* @param {string|null} className - Class name
|
||||
*/
|
||||
function wrapMethodBody(t, path, funcName, fileName, lineNumber, className) {
|
||||
const node = path.node;
|
||||
const isAsync = node.async;
|
||||
const isGenerator = node.generator;
|
||||
|
||||
// Create a copy of the original function as an expression
|
||||
const originalFunc = t.functionExpression(
|
||||
null, // anonymous
|
||||
node.params,
|
||||
node.body,
|
||||
isGenerator,
|
||||
isAsync
|
||||
);
|
||||
|
||||
// Create the wrapper call
|
||||
const wrapperCall = createWrapperCall(t, originalFunc, funcName, fileName, lineNumber, className);
|
||||
|
||||
// Create: return __codeflash_tracer__.wrap(...).apply(this, arguments)
|
||||
const applyCall = t.callExpression(
|
||||
t.memberExpression(wrapperCall, t.identifier('apply')),
|
||||
[t.thisExpression(), t.identifier('arguments')]
|
||||
);
|
||||
|
||||
let returnStatement;
|
||||
if (isAsync) {
|
||||
// For async methods, we need to await the result
|
||||
returnStatement = t.returnStatement(t.awaitExpression(applyCall));
|
||||
} else {
|
||||
returnStatement = t.returnStatement(applyCall);
|
||||
}
|
||||
|
||||
// Replace the function body
|
||||
node.body = t.blockStatement([returnStatement]);
|
||||
}
|
||||
|
||||
// Export helper functions for testing
|
||||
module.exports.shouldTraceFunction = shouldTraceFunction;
|
||||
module.exports.isExcludedPath = isExcludedPath;
|
||||
module.exports.getEnvConfig = getEnvConfig;
|
||||
|
|
@ -8,6 +8,8 @@
|
|||
* - capturePerf: Capture performance metrics (timing only)
|
||||
* - serialize/deserialize: Value serialization for storage
|
||||
* - comparator: Deep equality comparison
|
||||
* - tracer: Function tracing for replay test generation
|
||||
* - replay: Replay test utilities
|
||||
*
|
||||
* Usage (CommonJS):
|
||||
* const { capture, capturePerf } = require('codeflash');
|
||||
|
|
@ -30,6 +32,22 @@ const comparator = require('./comparator');
|
|||
// Result comparison (used by CLI)
|
||||
const compareResults = require('./compare-results');
|
||||
|
||||
// Function tracing (for replay test generation)
|
||||
let tracer = null;
|
||||
try {
|
||||
tracer = require('./tracer');
|
||||
} catch (e) {
|
||||
// Tracer may not be available if better-sqlite3 is not installed
|
||||
}
|
||||
|
||||
// Replay test utilities
|
||||
let replay = null;
|
||||
try {
|
||||
replay = require('./replay');
|
||||
} catch (e) {
|
||||
// Replay may not be available
|
||||
}
|
||||
|
||||
// Re-export all public APIs
|
||||
module.exports = {
|
||||
// === Main Instrumentation API ===
|
||||
|
|
@ -88,4 +106,24 @@ module.exports = {
|
|||
// === Feature Detection ===
|
||||
hasV8: serializer.hasV8,
|
||||
hasMsgpack: serializer.hasMsgpack,
|
||||
|
||||
// === Function Tracing (for replay test generation) ===
|
||||
tracer: tracer ? {
|
||||
init: tracer.init,
|
||||
wrap: tracer.wrap,
|
||||
createWrapper: tracer.createWrapper,
|
||||
disable: tracer.disable,
|
||||
enable: tracer.enable,
|
||||
getStats: tracer.getStats,
|
||||
} : null,
|
||||
|
||||
// === Replay Test Utilities ===
|
||||
replay: replay ? {
|
||||
getNextArg: replay.getNextArg,
|
||||
getTracesWithMetadata: replay.getTracesWithMetadata,
|
||||
getTracedFunctions: replay.getTracedFunctions,
|
||||
getTraceMetadata: replay.getTraceMetadata,
|
||||
generateReplayTest: replay.generateReplayTest,
|
||||
createReplayTestFromTrace: replay.createReplayTestFromTrace,
|
||||
} : null,
|
||||
};
|
||||
|
|
|
|||
454
packages/codeflash/runtime/replay.js
Normal file
454
packages/codeflash/runtime/replay.js
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
/**
|
||||
* Codeflash Replay Test Utilities
|
||||
*
|
||||
* This module provides utilities for generating and running replay tests
|
||||
* from traced function calls. Replay tests allow verifying that optimized
|
||||
* code produces the same results as the original code.
|
||||
*
|
||||
* Usage:
|
||||
* const { getNextArg, createReplayTest } = require('codeflash/replay');
|
||||
*
|
||||
* // In a test file:
|
||||
* describe('Replay tests', () => {
|
||||
* test.each(getNextArg(traceFile, 'myFunction', '/path/file.js', 25))
|
||||
* ('myFunction replay %#', (args) => {
|
||||
* myFunction(...args);
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* The module supports both Jest and Vitest test frameworks.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Load the codeflash serializer for argument deserialization
|
||||
const serializer = require('./serializer');
|
||||
|
||||
// ============================================================================
|
||||
// DATABASE ACCESS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open a SQLite database connection.
|
||||
*
|
||||
* @param {string} dbPath - Path to the SQLite database
|
||||
* @returns {Object|null} - Database connection or null if failed
|
||||
*/
|
||||
function openDatabase(dbPath) {
|
||||
try {
|
||||
const Database = require('better-sqlite3');
|
||||
return new Database(dbPath, { readonly: true });
|
||||
} catch (e) {
|
||||
console.error('[codeflash-replay] Failed to open database:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get traced function calls from the database.
|
||||
*
|
||||
* @param {string} traceFile - Path to the trace SQLite database
|
||||
* @param {string} functionName - Name of the function
|
||||
* @param {string} fileName - Path to the source file
|
||||
* @param {string|null} className - Class name (for methods)
|
||||
* @param {number} limit - Maximum number of traces to retrieve
|
||||
* @returns {Array} - Array of traced arguments
|
||||
*/
|
||||
function getNextArg(traceFile, functionName, fileName, limit = 25, className = null) {
|
||||
const db = openDatabase(traceFile);
|
||||
if (!db) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
let stmt;
|
||||
let rows;
|
||||
|
||||
if (className) {
|
||||
stmt = db.prepare(`
|
||||
SELECT args FROM function_calls
|
||||
WHERE function = ? AND filename = ? AND classname = ? AND type = 'call'
|
||||
ORDER BY time_ns ASC
|
||||
LIMIT ?
|
||||
`);
|
||||
rows = stmt.all(functionName, fileName, className, limit);
|
||||
} else {
|
||||
stmt = db.prepare(`
|
||||
SELECT args FROM function_calls
|
||||
WHERE function = ? AND filename = ? AND type = 'call'
|
||||
ORDER BY time_ns ASC
|
||||
LIMIT ?
|
||||
`);
|
||||
rows = stmt.all(functionName, fileName, limit);
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
// Deserialize arguments
|
||||
return rows.map((row, index) => {
|
||||
try {
|
||||
const args = serializer.deserialize(row.args);
|
||||
return args;
|
||||
} catch (e) {
|
||||
console.warn(`[codeflash-replay] Failed to deserialize args at index ${index}:`, e.message);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[codeflash-replay] Database query failed:', e.message);
|
||||
db.close();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get traced function calls with full metadata.
|
||||
*
|
||||
* @param {string} traceFile - Path to the trace SQLite database
|
||||
* @param {string} functionName - Name of the function
|
||||
* @param {string} fileName - Path to the source file
|
||||
* @param {string|null} className - Class name (for methods)
|
||||
* @param {number} limit - Maximum number of traces to retrieve
|
||||
* @returns {Array} - Array of trace objects with args and metadata
|
||||
*/
|
||||
function getTracesWithMetadata(traceFile, functionName, fileName, limit = 25, className = null) {
|
||||
const db = openDatabase(traceFile);
|
||||
if (!db) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
let stmt;
|
||||
let rows;
|
||||
|
||||
if (className) {
|
||||
stmt = db.prepare(`
|
||||
SELECT type, function, classname, filename, line_number, time_ns, args
|
||||
FROM function_calls
|
||||
WHERE function = ? AND filename = ? AND classname = ? AND type = 'call'
|
||||
ORDER BY time_ns ASC
|
||||
LIMIT ?
|
||||
`);
|
||||
rows = stmt.all(functionName, fileName, className, limit);
|
||||
} else {
|
||||
stmt = db.prepare(`
|
||||
SELECT type, function, classname, filename, line_number, time_ns, args
|
||||
FROM function_calls
|
||||
WHERE function = ? AND filename = ? AND type = 'call'
|
||||
ORDER BY time_ns ASC
|
||||
LIMIT ?
|
||||
`);
|
||||
rows = stmt.all(functionName, fileName, limit);
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
// Deserialize arguments and return with metadata
|
||||
return rows.map((row, index) => {
|
||||
let args;
|
||||
try {
|
||||
args = serializer.deserialize(row.args);
|
||||
} catch (e) {
|
||||
console.warn(`[codeflash-replay] Failed to deserialize args at index ${index}:`, e.message);
|
||||
args = [];
|
||||
}
|
||||
|
||||
return {
|
||||
args,
|
||||
function: row.function,
|
||||
className: row.classname,
|
||||
fileName: row.filename,
|
||||
lineNumber: row.line_number,
|
||||
timeNs: row.time_ns,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[codeflash-replay] Database query failed:', e.message);
|
||||
db.close();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all traced functions from the database.
|
||||
*
|
||||
* @param {string} traceFile - Path to the trace SQLite database
|
||||
* @returns {Array} - Array of { function, fileName, className, count } objects
|
||||
*/
|
||||
function getTracedFunctions(traceFile) {
|
||||
const db = openDatabase(traceFile);
|
||||
if (!db) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT function, filename, classname, COUNT(*) as count
|
||||
FROM function_calls
|
||||
WHERE type = 'call'
|
||||
GROUP BY function, filename, classname
|
||||
ORDER BY count DESC
|
||||
`);
|
||||
const rows = stmt.all();
|
||||
db.close();
|
||||
|
||||
return rows.map(row => ({
|
||||
function: row.function,
|
||||
fileName: row.filename,
|
||||
className: row.classname,
|
||||
count: row.count,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('[codeflash-replay] Failed to get traced functions:', e.message);
|
||||
db.close();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata from the trace database.
|
||||
*
|
||||
* @param {string} traceFile - Path to the trace SQLite database
|
||||
* @returns {Object} - Metadata key-value pairs
|
||||
*/
|
||||
function getTraceMetadata(traceFile) {
|
||||
const db = openDatabase(traceFile);
|
||||
if (!db) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const stmt = db.prepare('SELECT key, value FROM metadata');
|
||||
const rows = stmt.all();
|
||||
db.close();
|
||||
|
||||
const metadata = {};
|
||||
for (const row of rows) {
|
||||
metadata[row.key] = row.value;
|
||||
}
|
||||
return metadata;
|
||||
} catch (e) {
|
||||
console.error('[codeflash-replay] Failed to get metadata:', e.message);
|
||||
db.close();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST GENERATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Generate a Jest/Vitest replay test file.
|
||||
*
|
||||
* @param {string} traceFile - Path to the trace SQLite database
|
||||
* @param {Array} functions - Array of { function, fileName, className, modulePath } to test
|
||||
* @param {Object} options - Generation options
|
||||
* @returns {string} - Generated test file content
|
||||
*/
|
||||
function generateReplayTest(traceFile, functions, options = {}) {
|
||||
const {
|
||||
framework = 'jest', // 'jest' or 'vitest'
|
||||
maxRunCount = 100,
|
||||
outputPath = null,
|
||||
} = options;
|
||||
|
||||
const isVitest = framework === 'vitest';
|
||||
|
||||
// Build imports section
|
||||
const imports = [];
|
||||
|
||||
if (isVitest) {
|
||||
imports.push("import { describe, test } from 'vitest';");
|
||||
}
|
||||
|
||||
imports.push("const { getNextArg } = require('codeflash/replay');");
|
||||
imports.push('');
|
||||
|
||||
// Build function imports
|
||||
for (const func of functions) {
|
||||
const alias = getFunctionAlias(func.modulePath, func.function, func.className);
|
||||
|
||||
if (func.className) {
|
||||
// Import class for method testing
|
||||
imports.push(`const { ${func.className}: ${alias}_class } = require('${func.modulePath}');`);
|
||||
} else {
|
||||
// Import function directly
|
||||
imports.push(`const { ${func.function}: ${alias} } = require('${func.modulePath}');`);
|
||||
}
|
||||
}
|
||||
|
||||
imports.push('');
|
||||
|
||||
// Metadata
|
||||
const metadata = [
|
||||
`const traceFilePath = '${traceFile}';`,
|
||||
`const functions = ${JSON.stringify(functions.map(f => f.function))};`,
|
||||
'',
|
||||
];
|
||||
|
||||
// Build test cases
|
||||
const testCases = [];
|
||||
|
||||
for (const func of functions) {
|
||||
const alias = getFunctionAlias(func.modulePath, func.function, func.className);
|
||||
const testName = func.className
|
||||
? `${func.className}.${func.function}`
|
||||
: func.function;
|
||||
|
||||
if (func.className) {
|
||||
// Method test
|
||||
testCases.push(`
|
||||
describe('Replay: ${testName}', () => {
|
||||
const traces = getNextArg(traceFilePath, '${func.function}', '${func.fileName}', ${maxRunCount}, '${func.className}');
|
||||
|
||||
test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {
|
||||
// For instance methods, first arg is 'this' context
|
||||
const [thisArg, ...methodArgs] = args;
|
||||
const instance = thisArg || new ${alias}_class();
|
||||
instance.${func.function}(...methodArgs);
|
||||
});
|
||||
});
|
||||
`);
|
||||
} else {
|
||||
// Function test
|
||||
testCases.push(`
|
||||
describe('Replay: ${testName}', () => {
|
||||
const traces = getNextArg(traceFilePath, '${func.function}', '${func.fileName}', ${maxRunCount});
|
||||
|
||||
test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => {
|
||||
${alias}(...args);
|
||||
});
|
||||
});
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all parts
|
||||
const content = [
|
||||
'// Auto-generated replay test by Codeflash',
|
||||
'// Do not edit this file directly',
|
||||
'',
|
||||
...imports,
|
||||
...metadata,
|
||||
...testCases,
|
||||
].join('\n');
|
||||
|
||||
// Write to file if outputPath provided
|
||||
if (outputPath) {
|
||||
const dir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(outputPath, content);
|
||||
console.log(`[codeflash-replay] Generated test file: ${outputPath}`);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a function alias for imports to avoid naming conflicts.
|
||||
*
|
||||
* @param {string} modulePath - Module path
|
||||
* @param {string} functionName - Function name
|
||||
* @param {string|null} className - Class name
|
||||
* @returns {string} - Alias name
|
||||
*/
|
||||
function getFunctionAlias(modulePath, functionName, className = null) {
|
||||
// Normalize module path to valid identifier
|
||||
const moduleAlias = modulePath
|
||||
.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
|
||||
if (className) {
|
||||
return `${moduleAlias}_${className}_${functionName}`;
|
||||
}
|
||||
return `${moduleAlias}_${functionName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create replay tests from a trace file.
|
||||
* This is the main entry point for Python integration.
|
||||
*
|
||||
* @param {string} traceFile - Path to the trace SQLite database
|
||||
* @param {string} outputPath - Path to write the test file
|
||||
* @param {Object} options - Generation options
|
||||
* @returns {Object} - { success, outputPath, functions }
|
||||
*/
|
||||
function createReplayTestFromTrace(traceFile, outputPath, options = {}) {
|
||||
const {
|
||||
framework = 'jest',
|
||||
maxRunCount = 100,
|
||||
projectRoot = process.cwd(),
|
||||
} = options;
|
||||
|
||||
// Get all traced functions
|
||||
const tracedFunctions = getTracedFunctions(traceFile);
|
||||
|
||||
if (tracedFunctions.length === 0) {
|
||||
console.warn('[codeflash-replay] No traced functions found in database');
|
||||
return { success: false, outputPath: null, functions: [] };
|
||||
}
|
||||
|
||||
// Convert to the format expected by generateReplayTest
|
||||
const functions = tracedFunctions.map(tf => {
|
||||
// Calculate module path from file name
|
||||
let modulePath = tf.fileName;
|
||||
|
||||
// Make relative to project root
|
||||
if (path.isAbsolute(modulePath)) {
|
||||
modulePath = path.relative(projectRoot, modulePath);
|
||||
}
|
||||
|
||||
// Convert to module path (remove .js extension, use forward slashes)
|
||||
modulePath = './' + modulePath
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\.js$/, '')
|
||||
.replace(/\.ts$/, '');
|
||||
|
||||
return {
|
||||
function: tf.function,
|
||||
fileName: tf.fileName,
|
||||
className: tf.className,
|
||||
modulePath,
|
||||
};
|
||||
});
|
||||
|
||||
// Generate the test file
|
||||
const testContent = generateReplayTest(traceFile, functions, {
|
||||
framework,
|
||||
maxRunCount,
|
||||
outputPath,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
outputPath,
|
||||
functions: functions.map(f => f.function),
|
||||
content: testContent,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
module.exports = {
|
||||
// Core API
|
||||
getNextArg,
|
||||
getTracesWithMetadata,
|
||||
getTracedFunctions,
|
||||
getTraceMetadata,
|
||||
|
||||
// Test generation
|
||||
generateReplayTest,
|
||||
createReplayTestFromTrace,
|
||||
getFunctionAlias,
|
||||
|
||||
// Database utilities
|
||||
openDatabase,
|
||||
};
|
||||
381
packages/codeflash/runtime/trace-runner.js
Normal file
381
packages/codeflash/runtime/trace-runner.js
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Codeflash Trace Runner
|
||||
*
|
||||
* Entry point script that runs JavaScript/TypeScript code with function tracing enabled.
|
||||
* This script:
|
||||
* 1. Registers Babel with the tracer plugin for AST transformation
|
||||
* 2. Sets up environment variables for tracing configuration
|
||||
* 3. Runs the user's script, tests, or module
|
||||
*
|
||||
* Usage:
|
||||
* # Run a script with tracing
|
||||
* node trace-runner.js script.js
|
||||
*
|
||||
* # Run tests with tracing (Jest)
|
||||
* node trace-runner.js --jest -- --testPathPattern=mytest
|
||||
*
|
||||
* # Run tests with tracing (Vitest)
|
||||
* node trace-runner.js --vitest -- --run
|
||||
*
|
||||
* # Run with specific functions to trace
|
||||
* node trace-runner.js --functions='["myFunc","otherFunc"]' script.js
|
||||
*
|
||||
* Environment Variables (also settable via command line):
|
||||
* CODEFLASH_TRACE_DB - Path to SQLite database for storing traces
|
||||
* CODEFLASH_PROJECT_ROOT - Project root for relative path calculation
|
||||
* CODEFLASH_FUNCTIONS - JSON array of functions to trace
|
||||
* CODEFLASH_MAX_FUNCTION_COUNT - Maximum traces per function (default: 256)
|
||||
* CODEFLASH_TRACER_TIMEOUT - Timeout in seconds for tracing
|
||||
*
|
||||
* For ESM (ECMAScript modules), use the loader flag:
|
||||
* node --loader ./esm-loader.mjs trace-runner.js script.mjs
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// ============================================================================
|
||||
// ARGUMENT PARSING
|
||||
// ============================================================================
|
||||
|
||||
function parseArgs(args) {
|
||||
const config = {
|
||||
traceDb: process.env.CODEFLASH_TRACE_DB || path.join(process.cwd(), 'codeflash.trace.sqlite'),
|
||||
projectRoot: process.env.CODEFLASH_PROJECT_ROOT || process.cwd(),
|
||||
functions: process.env.CODEFLASH_FUNCTIONS || null,
|
||||
maxFunctionCount: process.env.CODEFLASH_MAX_FUNCTION_COUNT || '256',
|
||||
tracerTimeout: process.env.CODEFLASH_TRACER_TIMEOUT || null,
|
||||
traceFiles: process.env.CODEFLASH_TRACE_FILES || null,
|
||||
traceExclude: process.env.CODEFLASH_TRACE_EXCLUDE || null,
|
||||
jest: false,
|
||||
vitest: false,
|
||||
module: false,
|
||||
script: null,
|
||||
scriptArgs: [],
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
while (i < args.length) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '--trace-db') {
|
||||
config.traceDb = args[++i];
|
||||
} else if (arg.startsWith('--trace-db=')) {
|
||||
config.traceDb = arg.split('=')[1];
|
||||
} else if (arg === '--project-root') {
|
||||
config.projectRoot = args[++i];
|
||||
} else if (arg.startsWith('--project-root=')) {
|
||||
config.projectRoot = arg.split('=')[1];
|
||||
} else if (arg === '--functions') {
|
||||
config.functions = args[++i];
|
||||
} else if (arg.startsWith('--functions=')) {
|
||||
config.functions = arg.split('=')[1];
|
||||
} else if (arg === '--max-function-count') {
|
||||
config.maxFunctionCount = args[++i];
|
||||
} else if (arg.startsWith('--max-function-count=')) {
|
||||
config.maxFunctionCount = arg.split('=')[1];
|
||||
} else if (arg === '--timeout') {
|
||||
config.tracerTimeout = args[++i];
|
||||
} else if (arg.startsWith('--timeout=')) {
|
||||
config.tracerTimeout = arg.split('=')[1];
|
||||
} else if (arg === '--trace-files') {
|
||||
config.traceFiles = args[++i];
|
||||
} else if (arg.startsWith('--trace-files=')) {
|
||||
config.traceFiles = arg.split('=')[1];
|
||||
} else if (arg === '--trace-exclude') {
|
||||
config.traceExclude = args[++i];
|
||||
} else if (arg.startsWith('--trace-exclude=')) {
|
||||
config.traceExclude = arg.split('=')[1];
|
||||
} else if (arg === '--jest') {
|
||||
config.jest = true;
|
||||
} else if (arg === '--vitest') {
|
||||
config.vitest = true;
|
||||
} else if (arg === '-m' || arg === '--module') {
|
||||
config.module = true;
|
||||
} else if (arg === '--') {
|
||||
// Everything after -- is passed to the script/test runner
|
||||
config.scriptArgs = args.slice(i + 1);
|
||||
break;
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
} else if (!arg.startsWith('-')) {
|
||||
// First non-flag argument is the script
|
||||
config.script = arg;
|
||||
config.scriptArgs = args.slice(i + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
Codeflash Trace Runner - JavaScript Function Tracing
|
||||
|
||||
Usage:
|
||||
trace-runner [options] <script> [script-args...]
|
||||
trace-runner [options] --jest -- [jest-args...]
|
||||
trace-runner [options] --vitest -- [vitest-args...]
|
||||
|
||||
Options:
|
||||
--trace-db <path> Path to SQLite database for traces (default: ./codeflash.trace.sqlite)
|
||||
--project-root <path> Project root directory (default: cwd)
|
||||
--functions <json> JSON array of functions to trace (traces all if not set)
|
||||
--max-function-count <n> Maximum traces per function (default: 256)
|
||||
--timeout <seconds> Timeout for tracing
|
||||
--trace-files <json> JSON array of file patterns to trace
|
||||
--trace-exclude <json> JSON array of patterns to exclude from tracing
|
||||
--jest Run with Jest test framework
|
||||
--vitest Run with Vitest test framework
|
||||
-m, --module Run a module (like python -m)
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
# Trace a script
|
||||
trace-runner --functions='["processData"]' ./src/main.js
|
||||
|
||||
# Trace Jest tests
|
||||
trace-runner --jest --functions='["myFunc"]' -- --testPathPattern=mytest
|
||||
|
||||
# Trace Vitest tests
|
||||
trace-runner --vitest -- --run
|
||||
|
||||
Environment Variables:
|
||||
CODEFLASH_TRACE_DB Path to SQLite database
|
||||
CODEFLASH_PROJECT_ROOT Project root directory
|
||||
CODEFLASH_FUNCTIONS JSON array of functions to trace
|
||||
CODEFLASH_MAX_FUNCTION_COUNT Maximum traces per function
|
||||
CODEFLASH_TRACER_TIMEOUT Timeout in seconds
|
||||
`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BABEL REGISTRATION
|
||||
// ============================================================================
|
||||
|
||||
function registerBabel(config) {
|
||||
// Set environment variables before loading Babel
|
||||
process.env.CODEFLASH_TRACE_DB = config.traceDb;
|
||||
process.env.CODEFLASH_PROJECT_ROOT = config.projectRoot;
|
||||
process.env.CODEFLASH_MAX_FUNCTION_COUNT = config.maxFunctionCount;
|
||||
|
||||
if (config.functions) {
|
||||
process.env.CODEFLASH_FUNCTIONS = config.functions;
|
||||
}
|
||||
if (config.tracerTimeout) {
|
||||
process.env.CODEFLASH_TRACER_TIMEOUT = config.tracerTimeout;
|
||||
}
|
||||
if (config.traceFiles) {
|
||||
process.env.CODEFLASH_TRACE_FILES = config.traceFiles;
|
||||
}
|
||||
if (config.traceExclude) {
|
||||
process.env.CODEFLASH_TRACE_EXCLUDE = config.traceExclude;
|
||||
}
|
||||
|
||||
// Try to find @babel/register
|
||||
let babelRegister;
|
||||
try {
|
||||
babelRegister = require('@babel/register');
|
||||
} catch (e) {
|
||||
console.error('[codeflash] Error: @babel/register is required for tracing.');
|
||||
console.error('Install it with: npm install --save-dev @babel/register @babel/core');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get the path to our Babel plugin
|
||||
const pluginPath = path.join(__dirname, 'babel-tracer-plugin.js');
|
||||
|
||||
// Configure Babel
|
||||
const babelConfig = {
|
||||
// Use our tracer plugin
|
||||
plugins: [pluginPath],
|
||||
|
||||
// Compile only project files, not node_modules
|
||||
ignore: [/node_modules/],
|
||||
|
||||
// Only compile files in project root
|
||||
only: [config.projectRoot],
|
||||
|
||||
// Don't look for .babelrc files - we provide all config
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
|
||||
// Support TypeScript and modern JS
|
||||
presets: [],
|
||||
|
||||
// Enable source maps for better error messages
|
||||
sourceMaps: 'inline',
|
||||
|
||||
// Cache for faster repeated runs
|
||||
cache: true,
|
||||
|
||||
// File extensions to process
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'],
|
||||
};
|
||||
|
||||
// Try to add TypeScript support if available
|
||||
try {
|
||||
require.resolve('@babel/preset-typescript');
|
||||
babelConfig.presets.push('@babel/preset-typescript');
|
||||
} catch (e) {
|
||||
// TypeScript preset not available, skip
|
||||
}
|
||||
|
||||
// Try to add modern JS support
|
||||
try {
|
||||
require.resolve('@babel/preset-env');
|
||||
babelConfig.presets.push(['@babel/preset-env', { targets: { node: 'current' } }]);
|
||||
} catch (e) {
|
||||
// preset-env not available, skip
|
||||
}
|
||||
|
||||
// Register Babel
|
||||
babelRegister(babelConfig);
|
||||
|
||||
console.log(`[codeflash] Tracing enabled. Output: ${config.traceDb}`);
|
||||
if (config.functions) {
|
||||
console.log(`[codeflash] Tracing functions: ${config.functions}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SCRIPT EXECUTION
|
||||
// ============================================================================
|
||||
|
||||
function runScript(config) {
|
||||
if (!config.script) {
|
||||
console.error('[codeflash] Error: No script specified');
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Resolve script path
|
||||
const scriptPath = path.resolve(config.projectRoot, config.script);
|
||||
|
||||
if (!fs.existsSync(scriptPath)) {
|
||||
console.error(`[codeflash] Error: Script not found: ${scriptPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Update process.argv for the script
|
||||
process.argv = [process.argv[0], scriptPath, ...config.scriptArgs];
|
||||
|
||||
// Run the script
|
||||
require(scriptPath);
|
||||
}
|
||||
|
||||
function runJest(config) {
|
||||
// Find Jest
|
||||
let jestPath;
|
||||
try {
|
||||
jestPath = require.resolve('jest');
|
||||
} catch (e) {
|
||||
console.error('[codeflash] Error: Jest not found. Install it with: npm install --save-dev jest');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get Jest CLI path
|
||||
const jestCli = path.join(path.dirname(jestPath), 'cli');
|
||||
|
||||
// Update process.argv for Jest
|
||||
process.argv = [process.argv[0], 'jest', ...config.scriptArgs];
|
||||
|
||||
// Run Jest
|
||||
const jest = require(jestCli);
|
||||
jest.run();
|
||||
}
|
||||
|
||||
function runVitest(config) {
|
||||
// Vitest needs special handling as it's ESM-first
|
||||
// We'll spawn it as a subprocess with our loader
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const args = [
|
||||
'--experimental-vm-modules',
|
||||
require.resolve('vitest/vitest.mjs'),
|
||||
'run',
|
||||
...config.scriptArgs,
|
||||
];
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
CODEFLASH_TRACE_DB: config.traceDb,
|
||||
CODEFLASH_PROJECT_ROOT: config.projectRoot,
|
||||
CODEFLASH_MAX_FUNCTION_COUNT: config.maxFunctionCount,
|
||||
};
|
||||
|
||||
if (config.functions) {
|
||||
env.CODEFLASH_FUNCTIONS = config.functions;
|
||||
}
|
||||
if (config.tracerTimeout) {
|
||||
env.CODEFLASH_TRACER_TIMEOUT = config.tracerTimeout;
|
||||
}
|
||||
|
||||
console.log('[codeflash] Running Vitest with tracing...');
|
||||
console.log('[codeflash] Note: ESM tracing requires additional setup. See documentation.');
|
||||
|
||||
const child = spawn(process.execPath, args, {
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
cwd: config.projectRoot,
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
}
|
||||
|
||||
function runModule(config) {
|
||||
if (!config.script) {
|
||||
console.error('[codeflash] Error: No module specified');
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// For module mode, we resolve the module from the project root
|
||||
const modulePath = require.resolve(config.script, { paths: [config.projectRoot] });
|
||||
|
||||
// Update process.argv
|
||||
process.argv = [process.argv[0], modulePath, ...config.scriptArgs];
|
||||
|
||||
// Run the module
|
||||
require(modulePath);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN
|
||||
// ============================================================================
|
||||
|
||||
function main() {
|
||||
// Parse command line arguments (skip node and script name)
|
||||
const args = process.argv.slice(2);
|
||||
const config = parseArgs(args);
|
||||
|
||||
// Register Babel with tracer plugin
|
||||
registerBabel(config);
|
||||
|
||||
// Initialize the tracer
|
||||
const tracer = require('./tracer');
|
||||
tracer.init(config.traceDb, config.projectRoot);
|
||||
|
||||
// Run based on mode
|
||||
if (config.jest) {
|
||||
runJest(config);
|
||||
} else if (config.vitest) {
|
||||
runVitest(config);
|
||||
} else if (config.module) {
|
||||
runModule(config);
|
||||
} else {
|
||||
runScript(config);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
558
packages/codeflash/runtime/tracer.js
Normal file
558
packages/codeflash/runtime/tracer.js
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
/**
|
||||
* Codeflash JavaScript Function Tracer
|
||||
*
|
||||
* This module provides function tracing instrumentation that captures:
|
||||
* - Function inputs (arguments)
|
||||
* - Return values
|
||||
* - Exceptions thrown
|
||||
* - Execution time (nanosecond precision)
|
||||
*
|
||||
* Traces are stored in SQLite database for later replay test generation.
|
||||
* This mirrors the Python tracer functionality in codeflash/tracing/.
|
||||
*
|
||||
* Database Schema (matches Python tracer):
|
||||
* - function_calls: Main trace data (type, function, classname, filename, line_number, time_ns, args)
|
||||
* - metadata: Key-value metadata about the trace session
|
||||
* - pstats: Profiling statistics (optional)
|
||||
*
|
||||
* Usage:
|
||||
* const tracer = require('codeflash/tracer');
|
||||
* tracer.init('/path/to/output.sqlite', ['/path/to/project']);
|
||||
*
|
||||
* // Wrap a function for tracing
|
||||
* const tracedFunc = tracer.wrap(originalFunc, 'funcName', '/path/to/file.js', 10);
|
||||
*
|
||||
* // Or use the decorator pattern
|
||||
* tracer.trace('funcName', '/path/to/file.js', 10, () => {
|
||||
* // function body
|
||||
* });
|
||||
*
|
||||
* Environment Variables:
|
||||
* CODEFLASH_TRACE_DB - Path to SQLite database for storing traces
|
||||
* CODEFLASH_PROJECT_ROOT - Project root for relative path calculation
|
||||
* CODEFLASH_FUNCTIONS - JSON array of functions to trace (optional, traces all if not set)
|
||||
* CODEFLASH_MAX_FUNCTION_COUNT - Maximum traces per function (default: 256)
|
||||
* CODEFLASH_TRACER_TIMEOUT - Timeout in seconds for tracing (optional)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Load the codeflash serializer for robust value serialization
|
||||
const serializer = require('./serializer');
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
// Configuration from environment
|
||||
const TRACE_DB = process.env.CODEFLASH_TRACE_DB;
|
||||
const PROJECT_ROOT = process.env.CODEFLASH_PROJECT_ROOT || process.cwd();
|
||||
const MAX_FUNCTION_COUNT = parseInt(process.env.CODEFLASH_MAX_FUNCTION_COUNT || '256', 10);
|
||||
const TRACER_TIMEOUT = process.env.CODEFLASH_TRACER_TIMEOUT
|
||||
? parseFloat(process.env.CODEFLASH_TRACER_TIMEOUT) * 1000
|
||||
: null;
|
||||
|
||||
// Parse functions to trace from environment
|
||||
let FUNCTIONS_TO_TRACE = null;
|
||||
try {
|
||||
if (process.env.CODEFLASH_FUNCTIONS) {
|
||||
FUNCTIONS_TO_TRACE = JSON.parse(process.env.CODEFLASH_FUNCTIONS);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[codeflash-tracer] Failed to parse CODEFLASH_FUNCTIONS:', e.message);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
// SQLite database (lazy initialized)
|
||||
let db = null;
|
||||
let dbInitialized = false;
|
||||
|
||||
// Track function call counts for MAX_FUNCTION_COUNT limit
|
||||
const functionCallCounts = new Map();
|
||||
|
||||
// Track start time for timeout
|
||||
let tracingStartTime = null;
|
||||
|
||||
// Track if tracing is enabled
|
||||
let tracingEnabled = true;
|
||||
|
||||
// Address counter for unique call identification
|
||||
let lastFrameAddress = 0;
|
||||
|
||||
// Prepared statements (cached for performance)
|
||||
let insertCallStmt = null;
|
||||
let insertMetadataStmt = null;
|
||||
|
||||
// ============================================================================
|
||||
// DATABASE INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Initialize the SQLite database for storing traces.
|
||||
*
|
||||
* @param {string} dbPath - Path to the SQLite database file
|
||||
* @returns {boolean} - True if initialization succeeded
|
||||
*/
|
||||
function initDatabase(dbPath) {
|
||||
if (dbInitialized) return true;
|
||||
if (!dbPath) {
|
||||
console.error('[codeflash-tracer] No database path provided');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
// Ensure directory exists
|
||||
const dbDir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
db = new Database(dbPath);
|
||||
|
||||
// Create tables matching Python tracer schema
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS function_calls (
|
||||
type TEXT,
|
||||
function TEXT,
|
||||
classname TEXT,
|
||||
filename TEXT,
|
||||
line_number INTEGER,
|
||||
last_frame_address INTEGER,
|
||||
time_ns INTEGER,
|
||||
args BLOB
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS metadata (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pstats (
|
||||
filename TEXT,
|
||||
line_number INTEGER,
|
||||
function TEXT,
|
||||
class_name TEXT,
|
||||
call_count_nonrecursive INTEGER,
|
||||
num_callers INTEGER,
|
||||
total_time_ns INTEGER,
|
||||
cumulative_time_ns INTEGER,
|
||||
callers BLOB
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_function_calls_function
|
||||
ON function_calls(function, filename);
|
||||
CREATE INDEX IF NOT EXISTS idx_function_calls_time
|
||||
ON function_calls(time_ns);
|
||||
`);
|
||||
|
||||
// Prepare statements for performance
|
||||
insertCallStmt = db.prepare(`
|
||||
INSERT INTO function_calls (type, function, classname, filename, line_number, last_frame_address, time_ns, args)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
insertMetadataStmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)
|
||||
`);
|
||||
|
||||
// Record metadata
|
||||
insertMetadataStmt.run('tracer_version', '1.0.0');
|
||||
insertMetadataStmt.run('language', 'javascript');
|
||||
insertMetadataStmt.run('project_root', PROJECT_ROOT);
|
||||
insertMetadataStmt.run('node_version', process.version);
|
||||
insertMetadataStmt.run('start_time', new Date().toISOString());
|
||||
|
||||
dbInitialized = true;
|
||||
tracingStartTime = Date.now();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('[codeflash-tracer] Failed to initialize database:', e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database and finalize traces.
|
||||
*/
|
||||
function closeDatabase() {
|
||||
if (db) {
|
||||
try {
|
||||
// Record end time
|
||||
if (insertMetadataStmt) {
|
||||
insertMetadataStmt.run('end_time', new Date().toISOString());
|
||||
insertMetadataStmt.run('total_traces', getTotalTraceCount());
|
||||
}
|
||||
db.close();
|
||||
} catch (e) {
|
||||
console.error('[codeflash-tracer] Error closing database:', e.message);
|
||||
}
|
||||
db = null;
|
||||
dbInitialized = false;
|
||||
insertCallStmt = null;
|
||||
insertMetadataStmt = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TIMING UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get high-resolution time in nanoseconds.
|
||||
*
|
||||
* @returns {bigint} - Time in nanoseconds
|
||||
*/
|
||||
function getTimeNs() {
|
||||
return process.hrtime.bigint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate duration in nanoseconds.
|
||||
*
|
||||
* @param {bigint} start - Start time in nanoseconds
|
||||
* @param {bigint} end - End time in nanoseconds
|
||||
* @returns {number} - Duration in nanoseconds (as Number for SQLite compatibility)
|
||||
*/
|
||||
function getDurationNs(start, end) {
|
||||
return Number(end - start);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TRACING UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check if tracing is still enabled (not timed out or disabled).
|
||||
*
|
||||
* @returns {boolean} - True if tracing should continue
|
||||
*/
|
||||
function isTracingEnabled() {
|
||||
if (!tracingEnabled) return false;
|
||||
|
||||
if (TRACER_TIMEOUT && tracingStartTime) {
|
||||
const elapsed = Date.now() - tracingStartTime;
|
||||
if (elapsed >= TRACER_TIMEOUT) {
|
||||
console.log('[codeflash-tracer] Tracing timeout reached, stopping tracer');
|
||||
tracingEnabled = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a function should be traced based on configuration.
|
||||
*
|
||||
* @param {string} funcName - Function name
|
||||
* @param {string} fileName - File path
|
||||
* @param {string|null} className - Class name (for methods)
|
||||
* @returns {boolean} - True if function should be traced
|
||||
*/
|
||||
function shouldTraceFunction(funcName, fileName, className = null) {
|
||||
if (!isTracingEnabled()) return false;
|
||||
|
||||
// Check if we've exceeded the max call count for this function
|
||||
const key = `${fileName}:${className || ''}:${funcName}`;
|
||||
const count = functionCallCounts.get(key) || 0;
|
||||
if (count >= MAX_FUNCTION_COUNT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if function is in the filter list
|
||||
if (FUNCTIONS_TO_TRACE && FUNCTIONS_TO_TRACE.length > 0) {
|
||||
// Check by function name only, or by full qualified name
|
||||
const matchesName = FUNCTIONS_TO_TRACE.some(f => {
|
||||
if (typeof f === 'string') {
|
||||
return f === funcName || f === `${className}.${funcName}`;
|
||||
}
|
||||
// Support object format: { function: 'name', file: 'path', class: 'className' }
|
||||
if (typeof f === 'object') {
|
||||
if (f.function && f.function !== funcName) return false;
|
||||
if (f.file && !fileName.includes(f.file)) return false;
|
||||
if (f.class && f.class !== className) return false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!matchesName) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the call count for a function.
|
||||
*
|
||||
* @param {string} funcName - Function name
|
||||
* @param {string} fileName - File path
|
||||
* @param {string|null} className - Class name (for methods)
|
||||
*/
|
||||
function incrementCallCount(funcName, fileName, className = null) {
|
||||
const key = `${fileName}:${className || ''}:${funcName}`;
|
||||
const count = functionCallCounts.get(key) || 0;
|
||||
functionCallCounts.set(key, count + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total trace count across all functions.
|
||||
*
|
||||
* @returns {number} - Total number of traces
|
||||
*/
|
||||
function getTotalTraceCount() {
|
||||
let total = 0;
|
||||
for (const count of functionCallCounts.values()) {
|
||||
total += count;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely serialize arguments for storage.
|
||||
*
|
||||
* @param {Array} args - Arguments to serialize
|
||||
* @returns {Buffer} - Serialized arguments
|
||||
*/
|
||||
function serializeArgs(args) {
|
||||
try {
|
||||
return serializer.serialize(args);
|
||||
} catch (e) {
|
||||
console.warn('[codeflash-tracer] Serialization failed:', e.message);
|
||||
return Buffer.from(JSON.stringify({ __error__: 'SerializationError', message: e.message }));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CORE TRACING API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Record a function call trace.
|
||||
*
|
||||
* @param {string} type - Event type ('call' or 'return')
|
||||
* @param {string} funcName - Function name
|
||||
* @param {string} fileName - File path
|
||||
* @param {number} lineNumber - Line number
|
||||
* @param {string|null} className - Class name (for methods)
|
||||
* @param {bigint} timeNs - Timestamp in nanoseconds
|
||||
* @param {any} argsOrResult - Arguments (for 'call') or return value (for 'return')
|
||||
*/
|
||||
function recordTrace(type, funcName, fileName, lineNumber, className, timeNs, argsOrResult) {
|
||||
if (!dbInitialized) {
|
||||
if (!initDatabase(TRACE_DB)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const serializedData = serializeArgs(argsOrResult);
|
||||
const frameAddress = lastFrameAddress++;
|
||||
|
||||
insertCallStmt.run(
|
||||
type,
|
||||
funcName,
|
||||
className,
|
||||
fileName,
|
||||
lineNumber,
|
||||
frameAddress,
|
||||
Number(timeNs),
|
||||
serializedData
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('[codeflash-tracer] Failed to record trace:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a function with tracing instrumentation.
|
||||
*
|
||||
* @param {Function} fn - The function to wrap
|
||||
* @param {string} funcName - Function name
|
||||
* @param {string} fileName - File path
|
||||
* @param {number} lineNumber - Line number
|
||||
* @param {string|null} className - Class name (for methods)
|
||||
* @returns {Function} - Wrapped function
|
||||
*/
|
||||
function wrap(fn, funcName, fileName, lineNumber, className = null) {
|
||||
// Don't wrap if function shouldn't be traced
|
||||
if (typeof fn !== 'function') {
|
||||
return fn;
|
||||
}
|
||||
|
||||
// Check if it's an async function
|
||||
const isAsync = fn.constructor.name === 'AsyncFunction';
|
||||
|
||||
if (isAsync) {
|
||||
return async function codeflashTracedAsync(...args) {
|
||||
if (!shouldTraceFunction(funcName, fileName, className)) {
|
||||
return fn.apply(this, args);
|
||||
}
|
||||
|
||||
incrementCallCount(funcName, fileName, className);
|
||||
const startTime = getTimeNs();
|
||||
|
||||
// Record call
|
||||
recordTrace('call', funcName, fileName, lineNumber, className, startTime, args);
|
||||
|
||||
try {
|
||||
const result = await fn.apply(this, args);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return function codeflashTraced(...args) {
|
||||
if (!shouldTraceFunction(funcName, fileName, className)) {
|
||||
return fn.apply(this, args);
|
||||
}
|
||||
|
||||
incrementCallCount(funcName, fileName, className);
|
||||
const startTime = getTimeNs();
|
||||
|
||||
// Record call
|
||||
recordTrace('call', funcName, fileName, lineNumber, className, startTime, args);
|
||||
|
||||
try {
|
||||
const result = fn.apply(this, args);
|
||||
|
||||
// Handle promise returns from non-async functions
|
||||
if (result instanceof Promise) {
|
||||
return result.then(
|
||||
(resolved) => resolved,
|
||||
(error) => { throw error; }
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper function factory for use with Babel transformation.
|
||||
*
|
||||
* @param {string} funcName - Function name
|
||||
* @param {string} fileName - File path
|
||||
* @param {number} lineNumber - Line number
|
||||
* @param {string|null} className - Class name (for methods)
|
||||
* @returns {Function} - A function that takes the original function and returns a wrapped version
|
||||
*/
|
||||
function createWrapper(funcName, fileName, lineNumber, className = null) {
|
||||
return function(fn) {
|
||||
return wrap(fn, funcName, fileName, lineNumber, className);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the tracer.
|
||||
*
|
||||
* @param {string} dbPath - Path to SQLite database
|
||||
* @param {string} projectRoot - Project root path
|
||||
*/
|
||||
function init(dbPath, projectRoot) {
|
||||
if (projectRoot) {
|
||||
process.env.CODEFLASH_PROJECT_ROOT = projectRoot;
|
||||
}
|
||||
initDatabase(dbPath || TRACE_DB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable tracing.
|
||||
*/
|
||||
function disable() {
|
||||
tracingEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable tracing.
|
||||
*/
|
||||
function enable() {
|
||||
tracingEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracing statistics.
|
||||
*
|
||||
* @returns {Object} - Statistics object
|
||||
*/
|
||||
function getStats() {
|
||||
return {
|
||||
totalTraces: getTotalTraceCount(),
|
||||
functionCounts: Object.fromEntries(functionCallCounts),
|
||||
tracingEnabled,
|
||||
dbInitialized,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROCESS EXIT HANDLER
|
||||
// ============================================================================
|
||||
|
||||
// Ensure database is closed on process exit
|
||||
process.on('exit', () => {
|
||||
closeDatabase();
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
closeDatabase();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
closeDatabase();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Also handle uncaught exceptions
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[codeflash-tracer] Uncaught exception:', err);
|
||||
closeDatabase();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
module.exports = {
|
||||
// Core API
|
||||
init,
|
||||
wrap,
|
||||
createWrapper,
|
||||
recordTrace,
|
||||
|
||||
// Control
|
||||
disable,
|
||||
enable,
|
||||
isTracingEnabled,
|
||||
|
||||
// Database
|
||||
initDatabase,
|
||||
closeDatabase,
|
||||
|
||||
// Utilities
|
||||
shouldTraceFunction,
|
||||
getStats,
|
||||
serializeArgs,
|
||||
getTimeNs,
|
||||
getDurationNs,
|
||||
|
||||
// Configuration
|
||||
TRACE_DB,
|
||||
PROJECT_ROOT,
|
||||
MAX_FUNCTION_COUNT,
|
||||
FUNCTIONS_TO_TRACE,
|
||||
};
|
||||
|
|
@ -94,48 +94,6 @@ class TestJavaScriptTracer:
|
|||
tracer = JavaScriptTracer(output_db)
|
||||
|
||||
assert tracer.output_db == output_db
|
||||
assert tracer.tracer_var == "__codeflash_tracer__"
|
||||
|
||||
def test_tracer_generates_init_code(self):
|
||||
"""Test tracer generates initialization code."""
|
||||
output_db = Path("/tmp/test_traces.db")
|
||||
tracer = JavaScriptTracer(output_db)
|
||||
|
||||
init_code = tracer._generate_tracer_init()
|
||||
|
||||
assert tracer.tracer_var in init_code
|
||||
assert "serialize" in init_code
|
||||
assert "wrap" in init_code
|
||||
assert output_db.as_posix() in init_code
|
||||
|
||||
def test_tracer_instruments_simple_function(self):
|
||||
"""Test tracer can instrument a simple function."""
|
||||
source = """
|
||||
function multiply(x, y) {
|
||||
return x * y;
|
||||
}
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".js", mode="w", delete=False) as f:
|
||||
f.write(source)
|
||||
f.flush()
|
||||
file_path = Path(f.name)
|
||||
|
||||
func_info = FunctionInfo(
|
||||
function_name="multiply", file_path=file_path, starting_line=2, ending_line=4, language="javascript"
|
||||
)
|
||||
|
||||
output_db = Path("/tmp/test_traces.db")
|
||||
tracer = JavaScriptTracer(output_db)
|
||||
|
||||
instrumented = tracer.instrument_source(source, file_path, [func_info])
|
||||
|
||||
# Check that tracer initialization is added
|
||||
assert tracer.tracer_var in instrumented
|
||||
assert "wrap" in instrumented
|
||||
|
||||
# Clean up
|
||||
file_path.unlink()
|
||||
|
||||
def test_tracer_parse_results_empty(self):
|
||||
"""Test parsing results when file doesn't exist."""
|
||||
|
|
@ -149,7 +107,10 @@ class TestJavaScriptSupportInstrumentation:
|
|||
"""Integration tests for JavaScript support instrumentation methods."""
|
||||
|
||||
def test_javascript_support_instrument_for_behavior(self):
|
||||
"""Test JavaScriptSupport.instrument_for_behavior method."""
|
||||
"""Test JavaScriptSupport.instrument_for_behavior returns source unchanged.
|
||||
|
||||
JavaScript tracing is now handled at runtime via Babel plugin, not source transformation.
|
||||
"""
|
||||
from codeflash.languages import get_language_support
|
||||
|
||||
js_support = get_language_support(Language.JAVASCRIPT)
|
||||
|
|
@ -172,8 +133,7 @@ function greet(name) {
|
|||
output_file = file_path.parent / ".codeflash" / "traces.db"
|
||||
instrumented = js_support.instrument_for_behavior(source, [func_info], output_file=output_file)
|
||||
|
||||
assert "__codeflash_tracer__" in instrumented
|
||||
assert "wrap" in instrumented
|
||||
assert instrumented == source
|
||||
|
||||
# Clean up
|
||||
file_path.unlink()
|
||||
|
|
|
|||
545
tests/test_languages/test_javascript_tracer.py
Normal file
545
tests/test_languages/test_javascript_tracer.py
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
"""Tests for JavaScript function tracing.
|
||||
|
||||
Tests the JavaScript tracer implementation including:
|
||||
- Unit tests for Python-side trace parsing and replay test generation
|
||||
- End-to-end tests for the full tracing pipeline via trace-runner.js
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from codeflash.languages.javascript.replay_test import (
|
||||
JavaScriptFunctionModule,
|
||||
create_javascript_replay_test,
|
||||
get_function_alias,
|
||||
get_traced_functions_from_db,
|
||||
)
|
||||
from codeflash.languages.javascript.tracer import JavaScriptTracer
|
||||
|
||||
|
||||
def node_available() -> bool:
|
||||
return shutil.which("node") is not None
|
||||
|
||||
|
||||
def skip_if_node_not_available() -> None:
|
||||
if not node_available():
|
||||
pytest.skip("Node.js not available")
|
||||
|
||||
|
||||
class TestJavaScriptTracerParsing:
|
||||
|
||||
@pytest.fixture
|
||||
def trace_db_with_function_calls(self, tmp_path: Path) -> Path:
|
||||
db_path = (tmp_path / "trace.sqlite").resolve()
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE function_calls (
|
||||
type TEXT,
|
||||
function TEXT,
|
||||
classname TEXT,
|
||||
filename TEXT,
|
||||
line_number INTEGER,
|
||||
last_frame_address INTEGER,
|
||||
time_ns INTEGER,
|
||||
args BLOB
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE metadata (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("INSERT INTO metadata (key, value) VALUES ('language', 'javascript')")
|
||||
|
||||
test_args = json.dumps([1, 2, 3])
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO function_calls (type, function, classname, filename, line_number, last_frame_address, time_ns, args)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("call", "add", None, "/project/src/math.js", 10, 1, 1000000, test_args),
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO function_calls (type, function, classname, filename, line_number, last_frame_address, time_ns, args)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("call", "multiply", "Calculator", "/project/src/calc.js", 25, 2, 2000000, json.dumps([5, 10])),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return db_path
|
||||
|
||||
@pytest.fixture
|
||||
def trace_db_legacy_schema(self, tmp_path: Path) -> Path:
|
||||
db_path = (tmp_path / "legacy_trace.sqlite").resolve()
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE traces (
|
||||
id INTEGER PRIMARY KEY,
|
||||
call_id INTEGER,
|
||||
function TEXT,
|
||||
file TEXT,
|
||||
args TEXT,
|
||||
result TEXT,
|
||||
error TEXT,
|
||||
runtime_ns TEXT,
|
||||
timestamp INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO traces (call_id, function, file, args, result, error, runtime_ns, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(1, "legacyFunc", "/old/path.js", json.dumps(["arg1"]), json.dumps("result"), "null", "5000", 1234567890),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return db_path
|
||||
|
||||
def test_parse_results_function_calls_schema(self, trace_db_with_function_calls: Path) -> None:
|
||||
traces = JavaScriptTracer.parse_results(trace_db_with_function_calls)
|
||||
|
||||
assert len(traces) == 2
|
||||
|
||||
add_trace = next(t for t in traces if t["function"] == "add")
|
||||
assert add_trace["type"] == "call"
|
||||
assert add_trace["filename"] == "/project/src/math.js"
|
||||
assert add_trace["line_number"] == 10
|
||||
assert add_trace["args"] == [1, 2, 3]
|
||||
assert add_trace["classname"] is None
|
||||
|
||||
multiply_trace = next(t for t in traces if t["function"] == "multiply")
|
||||
assert multiply_trace["classname"] == "Calculator"
|
||||
assert multiply_trace["args"] == [5, 10]
|
||||
|
||||
def test_parse_results_legacy_schema(self, trace_db_legacy_schema: Path) -> None:
|
||||
traces = JavaScriptTracer.parse_results(trace_db_legacy_schema)
|
||||
|
||||
assert len(traces) == 1
|
||||
trace = traces[0]
|
||||
assert trace["function"] == "legacyFunc"
|
||||
assert trace["file"] == "/old/path.js"
|
||||
assert trace["args"] == ["arg1"]
|
||||
assert trace["result"] == "result"
|
||||
assert trace["runtime_ns"] == 5000
|
||||
|
||||
def test_parse_results_nonexistent_file(self, tmp_path: Path) -> None:
|
||||
traces = JavaScriptTracer.parse_results((tmp_path / "nonexistent.sqlite").resolve())
|
||||
assert traces == []
|
||||
|
||||
def test_parse_results_json_file(self, tmp_path: Path) -> None:
|
||||
json_path = (tmp_path / "trace.json").resolve()
|
||||
trace_data = [{"function": "jsonFunc", "args": [1, 2], "time_ns": 1000}]
|
||||
json_path.write_text(json.dumps(trace_data), encoding="utf-8")
|
||||
|
||||
sqlite_path = (tmp_path / "trace.sqlite").resolve()
|
||||
traces = JavaScriptTracer.parse_results(sqlite_path)
|
||||
|
||||
assert len(traces) == 1
|
||||
assert traces[0]["function"] == "jsonFunc"
|
||||
|
||||
def test_get_traced_functions(self, trace_db_with_function_calls: Path) -> None:
|
||||
functions = JavaScriptTracer.get_traced_functions(trace_db_with_function_calls)
|
||||
|
||||
assert len(functions) == 2
|
||||
|
||||
func_names = {f.function_name for f in functions}
|
||||
assert func_names == {"add", "multiply"}
|
||||
|
||||
add_func = next(f for f in functions if f.function_name == "add")
|
||||
assert add_func.file_name == "/project/src/math.js"
|
||||
assert add_func.class_name is None
|
||||
assert add_func.line_number == 10
|
||||
assert "math" in add_func.module_path
|
||||
|
||||
multiply_func = next(f for f in functions if f.function_name == "multiply")
|
||||
assert multiply_func.class_name == "Calculator"
|
||||
|
||||
|
||||
class TestJavaScriptReplayTestGeneration:
|
||||
|
||||
def test_get_function_alias_simple(self) -> None:
|
||||
alias = get_function_alias("src/utils", "processData")
|
||||
assert alias == "src_utils_processData"
|
||||
|
||||
def test_get_function_alias_with_class(self) -> None:
|
||||
alias = get_function_alias("src/calculator", "add", "Calculator")
|
||||
assert alias == "src_calculator_Calculator_add"
|
||||
|
||||
def test_get_function_alias_special_chars(self) -> None:
|
||||
alias = get_function_alias("@scope/package/lib", "func")
|
||||
assert "_" in alias
|
||||
assert "func" in alias
|
||||
|
||||
def test_create_javascript_replay_test_jest(self, tmp_path: Path) -> None:
|
||||
functions = [
|
||||
JavaScriptFunctionModule(
|
||||
function_name="add", file_name=(tmp_path / "math.js").resolve(), module_name="src/math"
|
||||
),
|
||||
JavaScriptFunctionModule(
|
||||
function_name="multiply",
|
||||
file_name=(tmp_path / "calc.js").resolve(),
|
||||
module_name="src/calc",
|
||||
class_name="Calculator",
|
||||
),
|
||||
]
|
||||
|
||||
content = create_javascript_replay_test(
|
||||
trace_file=str((tmp_path / "trace.sqlite").resolve()),
|
||||
functions=functions,
|
||||
max_run_count=50,
|
||||
framework="jest",
|
||||
)
|
||||
|
||||
assert "// Auto-generated replay test by Codeflash" in content
|
||||
assert "require('codeflash/replay')" in content
|
||||
assert "describe('Replay: add'" in content
|
||||
assert "describe('Replay: Calculator.multiply'" in content
|
||||
assert "test.each" in content
|
||||
assert "50" in content
|
||||
|
||||
def test_create_javascript_replay_test_vitest(self, tmp_path: Path) -> None:
|
||||
functions = [
|
||||
JavaScriptFunctionModule(
|
||||
function_name="process", file_name=(tmp_path / "data.js").resolve(), module_name="src/data"
|
||||
)
|
||||
]
|
||||
|
||||
content = create_javascript_replay_test(
|
||||
trace_file=str((tmp_path / "trace.sqlite").resolve()), functions=functions, framework="vitest"
|
||||
)
|
||||
|
||||
assert "import { describe, test } from 'vitest'" in content
|
||||
assert "describe('Replay: process'" in content
|
||||
|
||||
def test_create_javascript_replay_test_skips_constructors(self, tmp_path: Path) -> None:
|
||||
functions = [
|
||||
JavaScriptFunctionModule(
|
||||
function_name="constructor",
|
||||
file_name=(tmp_path / "class.js").resolve(),
|
||||
module_name="src/class",
|
||||
class_name="MyClass",
|
||||
),
|
||||
JavaScriptFunctionModule(
|
||||
function_name="__init__", file_name=(tmp_path / "class.js").resolve(), module_name="src/class"
|
||||
),
|
||||
JavaScriptFunctionModule(
|
||||
function_name="doWork", file_name=(tmp_path / "class.js").resolve(), module_name="src/class"
|
||||
),
|
||||
]
|
||||
|
||||
content = create_javascript_replay_test(
|
||||
trace_file=str((tmp_path / "trace.sqlite").resolve()), functions=functions
|
||||
)
|
||||
|
||||
assert "constructor" not in content.lower() or "Replay: constructor" not in content
|
||||
assert "__init__" not in content or "Replay: __init__" not in content
|
||||
assert "describe('Replay: doWork'" in content
|
||||
|
||||
|
||||
class TestJavaScriptTracerCreateReplayTest:
|
||||
|
||||
@pytest.fixture
|
||||
def trace_db(self, tmp_path: Path) -> Path:
|
||||
db_path = (tmp_path / "trace.sqlite").resolve()
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE function_calls (
|
||||
type TEXT,
|
||||
function TEXT,
|
||||
classname TEXT,
|
||||
filename TEXT,
|
||||
line_number INTEGER,
|
||||
last_frame_address INTEGER,
|
||||
time_ns INTEGER,
|
||||
args BLOB
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO function_calls (type, function, classname, filename, line_number, last_frame_address, time_ns, args)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("call", "fibonacci", None, "./src/math.js", 5, 1, 1000, json.dumps([10])),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return db_path
|
||||
|
||||
def test_create_replay_test_generates_file(self, trace_db: Path, tmp_path: Path) -> None:
|
||||
tracer = JavaScriptTracer(trace_db)
|
||||
output_path = (tmp_path / "tests" / "replay.test.js").resolve()
|
||||
|
||||
result = tracer.create_replay_test(trace_db, output_path)
|
||||
|
||||
assert result is not None
|
||||
assert output_path.exists()
|
||||
|
||||
content = output_path.read_text(encoding="utf-8")
|
||||
assert "fibonacci" in content
|
||||
assert "describe" in content
|
||||
|
||||
def test_create_replay_test_empty_db(self, tmp_path: Path) -> None:
|
||||
db_path = (tmp_path / "empty.sqlite").resolve()
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("""
|
||||
CREATE TABLE function_calls (
|
||||
type TEXT, function TEXT, classname TEXT, filename TEXT,
|
||||
line_number INTEGER, last_frame_address INTEGER, time_ns INTEGER, args BLOB
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
tracer = JavaScriptTracer(db_path)
|
||||
result = tracer.create_replay_test(db_path, (tmp_path / "test.js").resolve())
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt" or not node_available(), reason="Skipped on Windows or if Node.js not available")
|
||||
class TestJavaScriptTracerE2E:
|
||||
|
||||
@pytest.fixture
|
||||
def js_project(self, tmp_path: Path) -> Path:
|
||||
project_dir = (tmp_path / "js_project").resolve()
|
||||
project_dir.mkdir()
|
||||
|
||||
src_dir = project_dir / "src"
|
||||
src_dir.mkdir()
|
||||
|
||||
math_js = src_dir / "math.js"
|
||||
math_js.write_text(
|
||||
"""
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
function multiply(a, b) {
|
||||
return a * b;
|
||||
}
|
||||
|
||||
module.exports = { add, multiply };
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
main_js = project_dir / "main.js"
|
||||
main_js.write_text(
|
||||
"""
|
||||
const { add, multiply } = require('./src/math.js');
|
||||
|
||||
console.log('Running calculations...');
|
||||
console.log('add(2, 3) =', add(2, 3));
|
||||
console.log('add(10, 20) =', add(10, 20));
|
||||
console.log('multiply(4, 5) =', multiply(4, 5));
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
package_json = project_dir / "package.json"
|
||||
package_json.write_text(
|
||||
json.dumps({"name": "test-project", "version": "1.0.0", "main": "main.js"}), encoding="utf-8"
|
||||
)
|
||||
|
||||
return project_dir
|
||||
|
||||
@pytest.fixture
|
||||
def trace_runner_path(self) -> Path:
|
||||
runner_path = Path(__file__).parent.parent.parent / "packages" / "codeflash" / "runtime" / "trace-runner.js"
|
||||
if not runner_path.exists():
|
||||
pytest.skip("trace-runner.js not found")
|
||||
return runner_path
|
||||
|
||||
def test_trace_runner_help(self, trace_runner_path: Path) -> None:
|
||||
result = subprocess.run(
|
||||
["node", str(trace_runner_path), "--help"], capture_output=True, text=True, timeout=30, check=False
|
||||
)
|
||||
|
||||
assert "Usage:" in result.stdout or result.returncode == 0
|
||||
|
||||
def test_trace_javascript_file(self, js_project: Path, trace_runner_path: Path, tmp_path: Path) -> None:
|
||||
trace_db = (tmp_path / "trace.sqlite").resolve()
|
||||
|
||||
package_json = js_project / "package.json"
|
||||
package_json.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"name": "test-project",
|
||||
"version": "1.0.0",
|
||||
"main": "main.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/register": "^7.24.0",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
npm_install = subprocess.run(
|
||||
["npm", "install", "--silent"], capture_output=True, text=True, timeout=120, cwd=js_project, check=False
|
||||
)
|
||||
|
||||
if npm_install.returncode != 0:
|
||||
pytest.skip(f"npm install failed: {npm_install.stderr}")
|
||||
|
||||
codeflash_runtime = trace_runner_path.parent
|
||||
node_modules = js_project / "node_modules"
|
||||
codeflash_pkg = node_modules / "codeflash"
|
||||
if codeflash_pkg.exists():
|
||||
shutil.rmtree(codeflash_pkg)
|
||||
codeflash_pkg.mkdir()
|
||||
runtime_dst = codeflash_pkg / "runtime"
|
||||
|
||||
shutil.copytree(codeflash_runtime, runtime_dst)
|
||||
|
||||
(codeflash_pkg / "package.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"name": "codeflash",
|
||||
"version": "1.0.0",
|
||||
"main": "runtime/index.js",
|
||||
"exports": {
|
||||
".": {"require": "./runtime/index.js"},
|
||||
"./tracer": {"require": "./runtime/tracer.js"},
|
||||
"./replay": {"require": "./runtime/replay.js"},
|
||||
"./babel-tracer-plugin": {"require": "./runtime/babel-tracer-plugin.js"},
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
env = os.environ.copy()
|
||||
env["NODE_PATH"] = str(node_modules.resolve())
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(trace_runner_path),
|
||||
"--trace-db",
|
||||
str(trace_db),
|
||||
"--project-root",
|
||||
str(js_project),
|
||||
"--functions",
|
||||
json.dumps(["add", "multiply"]),
|
||||
str(js_project / "main.js"),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
cwd=js_project,
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"STDOUT: {result.stdout}")
|
||||
print(f"STDERR: {result.stderr}")
|
||||
|
||||
assert "add(2, 3) =" in result.stdout, f"Expected output not found. stderr: {result.stderr}"
|
||||
|
||||
assert trace_db.exists(), "Trace database was not created"
|
||||
|
||||
conn = sqlite3.connect(trace_db)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM function_calls WHERE type = 'call'")
|
||||
trace_count = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
assert trace_count >= 2, f"Expected at least 2 traced calls, got {trace_count}"
|
||||
|
||||
def test_tracer_runner_python_integration(self, js_project: Path, tmp_path: Path) -> None:
|
||||
from codeflash.languages.javascript.tracer_runner import (
|
||||
check_javascript_tracer_available,
|
||||
detect_test_framework,
|
||||
)
|
||||
|
||||
assert check_javascript_tracer_available() is True
|
||||
|
||||
framework = detect_test_framework(js_project, {})
|
||||
assert framework in ("jest", "vitest")
|
||||
|
||||
def test_detect_jest_framework(self, tmp_path: Path) -> None:
|
||||
from codeflash.languages.javascript.tracer_runner import detect_test_framework
|
||||
|
||||
(tmp_path / "jest.config.js").write_text("module.exports = {};", encoding="utf-8")
|
||||
framework = detect_test_framework(tmp_path, {})
|
||||
assert framework == "jest"
|
||||
|
||||
def test_detect_vitest_framework(self, tmp_path: Path) -> None:
|
||||
from codeflash.languages.javascript.tracer_runner import detect_test_framework
|
||||
|
||||
(tmp_path / "vitest.config.js").write_text("export default {};", encoding="utf-8")
|
||||
framework = detect_test_framework(tmp_path, {})
|
||||
assert framework == "vitest"
|
||||
|
||||
def test_detect_framework_from_package_json(self, tmp_path: Path) -> None:
|
||||
from codeflash.languages.javascript.tracer_runner import detect_test_framework
|
||||
|
||||
(tmp_path / "package.json").write_text(
|
||||
json.dumps({"scripts": {"test": "vitest run"}, "devDependencies": {"vitest": "^1.0.0"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
framework = detect_test_framework(tmp_path, {})
|
||||
assert framework == "vitest"
|
||||
|
||||
|
||||
class TestGetTracedFunctionsFromDb:
|
||||
|
||||
def test_get_traced_functions_from_db(self, tmp_path: Path) -> None:
|
||||
db_path = (tmp_path / "trace.sqlite").resolve()
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE function_calls (
|
||||
type TEXT, function TEXT, classname TEXT, filename TEXT,
|
||||
line_number INTEGER, last_frame_address INTEGER, time_ns INTEGER, args BLOB
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO function_calls VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
("call", "testFunc", None, "./src/test.js", 1, 1, 1000, "[]"),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
functions = get_traced_functions_from_db(db_path)
|
||||
|
||||
assert len(functions) == 1
|
||||
assert functions[0].function_name == "testFunc"
|
||||
assert functions[0].module_name == "src/test"
|
||||
|
||||
def test_get_traced_functions_nonexistent_file(self, tmp_path: Path) -> None:
|
||||
functions = get_traced_functions_from_db((tmp_path / "nonexistent.sqlite").resolve())
|
||||
assert functions == []
|
||||
Loading…
Reference in a new issue