Skip to content

Commit 2c95f05

Browse files
committed
fix test runner on jest 30 in monorepo cwd directory from root path
1 parent 9066cc0 commit 2c95f05

File tree

4 files changed

+297
-60
lines changed

4 files changed

+297
-60
lines changed

code_to_optimize/js/code_to_optimize_react/src/components/SearchableList.tsx

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
* This is a common pattern in codebases where developers wrap children in memo()
1111
* but forget to stabilize the parent's prop references, negating the benefit.
1212
*/
13-
import React, { useState, memo } from 'react';
13+
import React, { useState, memo , useMemo } from 'react';
14+
15+
const H3_STYLE = { margin: '8px 0 4px', fontSize: '14px' } as const;
16+
17+
const STATS_STYLE = { padding: '4px 8px', fontSize: '12px', color: '#666' } as const;
18+
19+
const CONTAINER_STYLE = { padding: '8px', display: 'flex', gap: '8px', alignItems: 'center' } as const;
1420

1521
export interface ListItem {
1622
id: number;
@@ -75,53 +81,61 @@ export function SearchableList({
7581
const [sortBy, setSortBy] = useState<'label' | 'score' | 'timestamp'>('label');
7682
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
7783

78-
// Inefficient: expensive pipeline recomputed every render
79-
const processedItems = items
80-
.filter(item => {
81-
if (showFavoritesOnly && !item.isFavorite) return false;
82-
if (query) {
83-
return (
84-
item.label.toLowerCase().includes(query.toLowerCase()) ||
85-
item.category.toLowerCase().includes(query.toLowerCase())
86-
);
84+
// Optimized: memoize filtering, sorting, grouping and stats into a single useMemo to avoid repeated array scans
85+
const { processedItems, categoryGroups, stats } = useMemo(() => {
86+
const q = query ? query.toLowerCase() : '';
87+
// Filter into a new array in one pass
88+
const filtered: ListItem[] = [];
89+
for (let i = 0; i < items.length; i++) {
90+
const item = items[i];
91+
if (showFavoritesOnly && !item.isFavorite) continue;
92+
if (q) {
93+
const label = item.label.toLowerCase();
94+
const category = item.category.toLowerCase();
95+
if (!label.includes(q) && !category.includes(q)) continue;
8796
}
88-
return true;
89-
})
90-
.sort((a, b) => {
91-
if (sortBy === 'label') return a.label.localeCompare(b.label);
92-
if (sortBy === 'score') return b.score - a.score;
93-
return b.timestamp - a.timestamp;
94-
});
95-
96-
// Inefficient: grouping computed every render
97-
const categoryGroups = processedItems.reduce(
98-
(groups, item) => {
99-
const group = groups[item.category] || [];
100-
group.push(item);
101-
groups[item.category] = group;
102-
return groups;
103-
},
104-
{} as Record<string, ListItem[]>,
105-
);
97+
filtered.push(item);
98+
}
99+
100+
// Sort the filtered array
101+
if (sortBy === 'label') {
102+
filtered.sort((a, b) => a.label.localeCompare(b.label));
103+
} else if (sortBy === 'score') {
104+
filtered.sort((a, b) => b.score - a.score);
105+
} else {
106+
filtered.sort((a, b) => b.timestamp - a.timestamp);
107+
}
108+
109+
// Build groups and aggregate stats in a single pass over the sorted filtered array
110+
const groups: Record<string, ListItem[]> = {};
111+
let favorites = 0;
112+
let sumScore = 0;
113+
for (let i = 0; i < filtered.length; i++) {
114+
const it = filtered[i];
115+
const g = groups[it.category];
116+
if (g) g.push(it);
117+
else groups[it.category] = [it];
118+
if (it.isFavorite) favorites++;
119+
sumScore += it.score;
120+
}
121+
122+
const total = filtered.length;
123+
const avgScore = total > 0 ? sumScore / total : 0;
124+
const categories = Object.keys(groups).length;
106125

107-
// Inefficient: stats computed every render
108-
const stats = {
109-
total: processedItems.length,
110-
favorites: processedItems.filter(i => i.isFavorite).length,
111-
avgScore:
112-
processedItems.length > 0
113-
? processedItems.reduce((sum, i) => sum + i.score, 0) / processedItems.length
114-
: 0,
115-
categories: Object.keys(categoryGroups).length,
116-
};
126+
return {
127+
processedItems: filtered,
128+
categoryGroups: groups,
129+
stats: { total, favorites, avgScore, categories },
130+
};
131+
}, [items, showFavoritesOnly, query, sortBy]);
117132

118-
// Inefficient: creates Set on every render
119-
const highlightedSet = new Set(highlightedIds);
133+
// Optimized: create Set once per highlightedIds change
134+
const highlightedSet = useMemo(() => new Set(highlightedIds), [highlightedIds]);
120135

121136
return (
122137
<div>
123-
{/* Inefficient: inline style */}
124-
<div style={{ padding: '8px', display: 'flex', gap: '8px', alignItems: 'center' }}>
138+
<div style={CONTAINER_STYLE}>
125139
<input
126140
type="text"
127141
placeholder="Search..."
@@ -143,26 +157,22 @@ export function SearchableList({
143157
</label>
144158
</div>
145159

146-
{/* Inefficient: inline style */}
147-
<div style={{ padding: '4px 8px', fontSize: '12px', color: '#666' }}>
160+
<div style={STATS_STYLE}>
148161
{stats.total} items | {stats.favorites} favorites |
149162
Avg score: {stats.avgScore.toFixed(1)} | {stats.categories} categories
150163
</div>
151164

152165
{Object.entries(categoryGroups).map(([category, categoryItems]) => (
153166
<div key={category}>
154-
<h3 style={{ margin: '8px 0 4px', fontSize: '14px' }}>
167+
<h3 style={H3_STYLE}>
155168
{category} ({categoryItems.length})
156169
</h3>
157170
{categoryItems.map(item => (
158171
<ListItemCard
159172
key={item.id}
160173
item={item}
161-
// Inefficient: inline function creates new reference every render
162-
// This defeats the React.memo on ListItemCard
163-
onToggleFavorite={(id) => onToggleFavorite(id)}
164-
onDelete={(id) => onDelete(id)}
165-
// Inefficient: inline object creates new reference every render
174+
onToggleFavorite={onToggleFavorite}
175+
onDelete={onDelete}
166176
style={{
167177
padding: '4px 8px',
168178
borderBottom: '1px solid #eee',

codeflash/languages/javascript/instrument.py

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,127 @@ def is_inside_string(code: str, pos: int) -> bool:
107107
return in_string
108108

109109

110+
class JsxRenderCallTransformer:
111+
"""Transforms render(<ComponentName>...) calls in React test code.
112+
113+
React components are invoked via JSX in tests, not as direct function calls.
114+
Tests use render(<Component />) from @testing-library/react. This transformer
115+
wraps those render() calls with codeflash instrumentation.
116+
117+
Examples:
118+
- render(<Comp />) -> codeflash.capturePerf('Comp', '1', () => render(<Comp />))
119+
- render(<Comp>...</Comp>, opts) -> codeflash.capturePerf('Comp', '1', () => render(<Comp>...</Comp>, opts))
120+
"""
121+
122+
def __init__(self, function_to_optimize: FunctionToOptimize, capture_func: str) -> None:
123+
self.function_to_optimize = function_to_optimize
124+
self.func_name = function_to_optimize.function_name
125+
self.qualified_name = function_to_optimize.qualified_name
126+
self.capture_func = capture_func
127+
self.invocation_counter = 0
128+
# Match render( followed by JSX containing the component name
129+
# Captures: (whitespace)(await )?render(
130+
self._render_pattern = re.compile(
131+
rf"(\s*)(await\s+)?render\s*\(\s*<\s*{re.escape(self.func_name)}[\s>/]"
132+
)
133+
134+
def transform(self, code: str) -> str:
135+
"""Transform all render(<Component>) calls in the code."""
136+
result: list[str] = []
137+
pos = 0
138+
139+
while pos < len(code):
140+
match = self._render_pattern.search(code, pos)
141+
if not match:
142+
result.append(code[pos:])
143+
break
144+
145+
# Skip if inside a string literal
146+
if is_inside_string(code, match.start()):
147+
result.append(code[pos : match.end()])
148+
pos = match.end()
149+
continue
150+
151+
# Skip if already wrapped with codeflash
152+
lookback = code[max(0, match.start() - 60) : match.start()]
153+
if f"codeflash.{self.capture_func}(" in lookback:
154+
result.append(code[pos : match.end()])
155+
pos = match.end()
156+
continue
157+
158+
# Add everything before the match
159+
result.append(code[pos : match.start()])
160+
161+
leading_ws = match.group(1)
162+
prefix = match.group(2) or "" # "await " or ""
163+
164+
# Find the render( opening paren
165+
render_call_text = code[match.start():]
166+
render_paren_offset = render_call_text.index("(")
167+
open_paren_pos = match.start() + render_paren_offset
168+
169+
# Find the matching closing paren of render(...)
170+
close_pos = self._find_matching_paren(code, open_paren_pos)
171+
if close_pos == -1:
172+
# Can't find matching paren, skip
173+
result.append(code[match.start() : match.end()])
174+
pos = match.end()
175+
continue
176+
177+
# Extract the full render(...) arguments
178+
render_args = code[open_paren_pos + 1 : close_pos - 1]
179+
180+
# Check for trailing semicolon
181+
end_pos = close_pos
182+
while end_pos < len(code) and code[end_pos] in " \t":
183+
end_pos += 1
184+
has_semicolon = end_pos < len(code) and code[end_pos] == ";"
185+
if has_semicolon:
186+
end_pos += 1
187+
188+
self.invocation_counter += 1
189+
line_id = str(self.invocation_counter)
190+
semicolon = ";" if has_semicolon else ""
191+
192+
# Wrap render(...) in a lambda: codeflash.capturePerf('name', 'id', () => render(...))
193+
transformed = (
194+
f"{leading_ws}{prefix}codeflash.{self.capture_func}('{self.qualified_name}', "
195+
f"'{line_id}', () => render({render_args})){semicolon}"
196+
)
197+
result.append(transformed)
198+
pos = end_pos
199+
200+
return "".join(result)
201+
202+
def _find_matching_paren(self, code: str, open_paren_pos: int) -> int:
203+
"""Find the position after the closing paren for the given opening paren."""
204+
if open_paren_pos >= len(code) or code[open_paren_pos] != "(":
205+
return -1
206+
207+
depth = 1
208+
pos = open_paren_pos + 1
209+
in_string = False
210+
string_char = None
211+
212+
while pos < len(code) and depth > 0:
213+
char = code[pos]
214+
if char in "\"'`" and (pos == 0 or code[pos - 1] != "\\"):
215+
if not in_string:
216+
in_string = True
217+
string_char = char
218+
elif char == string_char:
219+
in_string = False
220+
string_char = None
221+
elif not in_string:
222+
if char == "(":
223+
depth += 1
224+
elif char == ")":
225+
depth -= 1
226+
pos += 1
227+
228+
return pos if depth == 0 else -1
229+
230+
110231
class StandaloneCallTransformer:
111232
"""Transforms standalone func(...) calls in JavaScript test code.
112233
@@ -467,6 +588,31 @@ def transform_standalone_calls(
467588
return result, transformer.invocation_counter
468589

469590

591+
def transform_jsx_render_calls(
592+
code: str, function_to_optimize: FunctionToOptimize, capture_func: str, start_counter: int = 0
593+
) -> tuple[str, int]:
594+
"""Transform render(<Component>...) calls in React test code.
595+
596+
This handles React components that are invoked via JSX rather than direct function
597+
calls. When a component is used as <ComponentName> in a render() call, this wraps
598+
the entire render() with codeflash instrumentation.
599+
600+
Args:
601+
code: The test code to transform.
602+
function_to_optimize: The React component being tested.
603+
capture_func: The capture function to use ('capture' or 'capturePerf').
604+
start_counter: Starting value for the invocation counter.
605+
606+
Returns:
607+
Tuple of (transformed code, final counter value).
608+
609+
"""
610+
transformer = JsxRenderCallTransformer(function_to_optimize=function_to_optimize, capture_func=capture_func)
611+
transformer.invocation_counter = start_counter
612+
result = transformer.transform(code)
613+
return result, transformer.invocation_counter
614+
615+
470616
class ExpectCallTransformer:
471617
"""Transforms expect(func(...)).assertion() calls in JavaScript test code.
472618
@@ -1038,6 +1184,21 @@ def inject_profiling_into_existing_js_test(
10381184
return True, instrumented_code
10391185

10401186

1187+
def _is_jsx_component_usage(code: str, func_name: str) -> bool:
1188+
"""Check if a function is used as a JSX component in render() calls.
1189+
1190+
Returns True if the code contains patterns like render(<FuncName ...) or
1191+
<FuncName> or <FuncName />, indicating the function is a React component
1192+
rendered via JSX rather than called directly.
1193+
"""
1194+
# Check for JSX usage: <ComponentName or <ComponentName> or <ComponentName />
1195+
jsx_pattern = rf"<\s*{re.escape(func_name)}[\s>/]"
1196+
if not re.search(jsx_pattern, code):
1197+
return False
1198+
# Also verify there's a render() call (from @testing-library/react or similar)
1199+
return bool(re.search(r"\brender\s*\(", code))
1200+
1201+
10411202
def _is_function_used_in_test(code: str, func_name: str) -> bool:
10421203
"""Check if a function is imported or used in the test code.
10431204
@@ -1122,6 +1283,8 @@ def _instrument_js_test_code(
11221283
# Choose capture function based on mode
11231284
capture_func = "capturePerf" if mode == TestingMode.PERFORMANCE else "capture"
11241285

1286+
# Save code state before transforms to detect if any calls were instrumented
1287+
code_before_transforms = code
11251288
# Transform React render calls: render(React.createElement(Component, ...))
11261289
# Do this first so expect/standalone transforms don't interfere with render patterns
11271290
code, render_counter = transform_render_calls(
@@ -1140,10 +1303,21 @@ def _instrument_js_test_code(
11401303

11411304
# Transform standalone calls (not inside expect wrappers)
11421305
# Continue counter from expect transformer to ensure unique IDs
1143-
code, _final_counter = transform_standalone_calls(
1306+
code, final_counter = transform_standalone_calls(
11441307
code=code, function_to_optimize=function_to_optimize, capture_func=capture_func, start_counter=expect_counter
11451308
)
11461309

1310+
# If no direct function calls were instrumented, check for JSX usage (React components).
1311+
# React components are invoked via JSX (<Component />) in render() calls, not as
1312+
# direct function calls. Detect this pattern and wrap render() calls instead.
1313+
if code == code_before_transforms and _is_jsx_component_usage(code_before_transforms, function_to_optimize.function_name):
1314+
code, _jsx_counter = transform_jsx_render_calls(
1315+
code=code,
1316+
function_to_optimize=function_to_optimize,
1317+
capture_func=capture_func,
1318+
start_counter=final_counter,
1319+
)
1320+
11471321
return code
11481322

11491323

0 commit comments

Comments
 (0)