Skip to content

Commit c8b2e38

Browse files
mihailtclaude
andcommitted
test(search): the memory repro names many as not measured when ripgrep exited between samples
The repro judges its many scenario by ripgrep's peak memory, sampled while ripgrep runs. A ripgrep that started and exited between two samples was never seen: its peak read 0, sampling had not failed, and the run said NOT REPRODUCED with many counted as bounded, without having measured ripgrep. At REPRO_MB=1 that happened 3 runs of 3 on Windows ("ripgrep peak 0 MB"), and on macOS at the default sizes, where ripgrep stops the file at 200 matches within milliseconds. As a skipped check is, many is now named as not measured ("ripgrep exited between samples"), is never counted as bounded, and doesn't fail the run by itself: "NOT REPRODUCED: ... (line); not measured: many (ripgrep exited between samples)", exit 0, or "SKIPPED: not measured: many (...)" when nothing else ran. The report line says "ripgrep peak not measured: ripgrep exited between samples". A real sampling failure is still NOT MEASURED, exit 2. REPRO_SAMPLE_MS sets the sampling interval (default 100). test-search-memory-unsampled.js runs the repro's many scenario with a one-minute interval, so the server is sampled before the search and its ripgrep is always missed. With the earlier repro its many line says "ripgrep peak 0 MB" and the run "NOT REPRODUCED: ... (many)"; here many is named as not measured and the run exits 0, Windows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 5e35aa7 commit c8b2e38

2 files changed

Lines changed: 73 additions & 7 deletions

File tree

‎test/repro/test-search-memory.js‎

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,16 @@
3939
// Run: node test/repro/run-repro.js test-search-memory.js
4040
// (REPRO_MB scales the fixtures, default 64; REPRO_SCENARIOS=many,line picks some;
4141
// REPRO_V8_LIMIT=1 adds the 540 MB line: set REPRO_TIMEOUT_MS=2400000 and
42-
// REPRO_SEARCH_LIMIT_MS=2200000 for the base)
42+
// REPRO_SEARCH_LIMIT_MS=2200000 for the base; REPRO_SAMPLE_MS sets the
43+
// sampling interval, default 100)
4344
// Exit code: 1 if the problem shows: ripgrep holds more than 128 MB for 200
4445
// results, the server grows by more than all the context text, a 48 MB line
4546
// takes more than 5 s or grows the server by more than 8 times its size, or the
4647
// server exits. 2 (NOT MEASURED) if a scenario judged by memory has no valid
4748
// measurement: its memory could not be sampled, or (many, context) its search
48-
// failed or did not complete.
49+
// failed or did not complete. A many search whose ripgrep exited between two
50+
// samples (sampling worked but never saw it) is named as not measured, as a
51+
// skipped check is: it isn't counted as bounded and doesn't fail the run.
4952
import { constants } from 'buffer';
5053
import fs from 'fs';
5154
import os from 'os';
@@ -67,6 +70,8 @@ const MAX_RESULTS = 200;
6770
const SHOWN_CHARS = 100;
6871
/** A search still running after this long is stopped and reported */
6972
const SEARCH_LIMIT_MS = Number(process.env.REPRO_SEARCH_LIMIT_MS || 170_000);
73+
/** How often the server and its ripgrep are sampled */
74+
const SAMPLE_MS = Number(process.env.REPRO_SAMPLE_MS || 100);
7075

7176
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7277

@@ -158,7 +163,7 @@ async function runScenario(name) {
158163
await client.connect(transport, { timeout: 30_000 });
159164

160165
const serverPid = transport.pid;
161-
const memory = watchPeakMemory(serverPid);
166+
const memory = watchPeakMemory(serverPid, SAMPLE_MS);
162167
let started = Date.now();
163168
try {
164169
await sleep(1000);
@@ -216,12 +221,15 @@ const names = (process.env.REPRO_SCENARIOS?.split(',') ?? ['many', 'context', 'l
216221
const findings = [];
217222
/** Scenarios that give no valid measurement to judge by */
218223
const unmeasured = [];
224+
/** Scenarios sampling worked for but never saw ripgrep in (it exited between samples) */
225+
const notSampled = [];
219226
for (const name of names) {
220227
const o = await runScenario(name);
221228
const growth = o.serverPeak - o.serverBefore;
222229
console.log(`${name}: ${o.fixture}; maxResults ${MAX_RESULTS}, answer shows ${SHOWN_CHARS} characters per entry`);
223230
console.log(` ${o.complete ? 'completed' : 'NOT completed'} in ${o.seconds?.toFixed(1)} s: ${o.matches} matches, ${o.results} results, answer ${o.answerChars} characters`);
224-
console.log(` ripgrep peak ${formatMB(o.ripgrepPeak)}; server peak ${formatMB(o.serverPeak)} (${formatMB(o.serverBefore)} before the search, + ${formatMB(growth)})`);
231+
const ripgrepMemory = o.ripgrepPeak ? formatMB(o.ripgrepPeak) : o.samplingFailure ? 'not measured' : 'not measured: ripgrep exited between samples';
232+
console.log(` ripgrep peak ${ripgrepMemory}; server peak ${formatMB(o.serverPeak)} (${formatMB(o.serverBefore)} before the search, + ${formatMB(growth)})`);
225233
if (o.samplingFailure) console.log(` memory not measured: ${o.samplingFailure}`);
226234
if (o.error) console.log(` error: ${o.error}`);
227235
if (o.serverExited) console.log(` the server exited${o.serverLog ? `: ${o.serverLog}` : ''}`);
@@ -245,13 +253,28 @@ for (const name of names) {
245253
if ((name === 'many' || name === 'context' || (name === 'line' && !lineFinding)) && !o.serverExited && o.samplingFailure) {
246254
unmeasured.push(`${name}: memory not measured (${o.samplingFailure})`);
247255
}
256+
// many is judged by ripgrep's peak. A ripgrep that started and exited between
257+
// two samples was never seen, so its peak reads 0, which measures nothing
258+
if (name === 'many' && sampled && searched && !o.serverExited && !o.ripgrepPeak) notSampled.push(name);
248259
if (!searched && !o.serverExited) {
249260
unmeasured.push(`${name}: the search ${o.error ? `failed (${o.error})` : `did not complete within ${SEARCH_LIMIT_MS / 1000} s`}`);
250261
}
251262
}
252263

253-
if (unmeasured.length > 0) console.log(`NOT MEASURED: ${unmeasured.join('; ')}`);
254-
if (findings.length > 0) console.log(`REPRODUCED: ${findings.join('; ')}`);
255-
else if (unmeasured.length === 0) console.log(`NOT REPRODUCED: a ${MAX_RESULTS}-result search stayed bounded in ripgrep and the server (${names.join(', ')})`);
264+
// A scenario whose ripgrep exited between samples is named, as a skipped check
265+
// is, and never counted as bounded
266+
const skippedNote = notSampled.length > 0
267+
? `not measured: ${notSampled.map((name) => `${name} (ripgrep exited between samples)`).join(', ')}` : '';
268+
const withSkipped = (line) => (skippedNote ? `${line}; ${skippedNote}` : line);
269+
const bounded = names.filter((name) => !notSampled.includes(name));
270+
if (unmeasured.length > 0) console.log(withSkipped(`NOT MEASURED: ${unmeasured.join('; ')}`));
271+
if (findings.length > 0) {
272+
const line = `REPRODUCED: ${findings.join('; ')}`;
273+
console.log(unmeasured.length > 0 ? line : withSkipped(line));
274+
} else if (unmeasured.length === 0) {
275+
console.log(bounded.length > 0
276+
? withSkipped(`NOT REPRODUCED: a ${MAX_RESULTS}-result search stayed bounded in ripgrep and the server (${bounded.join(', ')})`)
277+
: `SKIPPED: ${skippedNote}`);
278+
}
256279
// A request to a server that exited can leave its timeout timer running
257280
exitProcess(findings.length > 0 ? 1 : unmeasured.length > 0 ? 2 : 0);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* The search-memory repro judges its `many` scenario by ripgrep's peak memory,
3+
* sampled while ripgrep runs. A ripgrep that starts and exits between two
4+
* samples is never seen, and its peak reads 0, which measures nothing. As a
5+
* skipped check is, `many` is then named as not measured, never counted as
6+
* bounded, and doesn't fail the run by itself. Here the sampling interval is a
7+
* minute: the server is sampled once before the search, and the search's
8+
* ripgrep is certain to be missed.
9+
*/
10+
import assert from 'assert';
11+
import path from 'path';
12+
import { fileURLToPath } from 'url';
13+
import { runNode } from './helpers/run-node.js';
14+
import { runIfMain } from './helpers/run-if-main.js';
15+
16+
const REPRO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'repro', 'test-search-memory.js');
17+
const VERDICT = /^(REPRODUCED|NOT REPRODUCED|NOT MEASURED|SKIPPED):/;
18+
19+
export default async function runTests() {
20+
const result = await runNode([REPRO], {
21+
env: { ...process.env, REPRO_MB: '1', REPRO_SCENARIOS: 'many', REPRO_SAMPLE_MS: '60000' },
22+
timeoutMs: 120_000,
23+
});
24+
const lines = `${result.stdout}${result.stderr}`.split(/\r?\n/);
25+
const verdicts = lines.filter((line) => VERDICT.test(line));
26+
const report = lines.filter((line) => /ripgrep peak|memory not measured|error/i.test(line) || VERDICT.test(line)).join(' | ');
27+
try {
28+
assert.strictEqual(result.status, 0, `a many search whose ripgrep exited between samples should not fail the run by itself, got exit ${result.status}: ${report}`);
29+
assert(lines.some((line) => line.includes('ripgrep peak not measured: ripgrep exited between samples')),
30+
`the many line should say ripgrep was not measured: ${report}`);
31+
assert(verdicts.length === 1 && verdicts[0].includes('not measured: many (ripgrep exited between samples)'),
32+
`the verdict line should name many as not measured: ${report}`);
33+
assert(!verdicts.some((line) => /^(NOT REPRODUCED|REPRODUCED|NOT MEASURED):/.test(line)),
34+
`the verdict should neither claim many stayed bounded nor count it as a finding or a sampling failure: ${report}`);
35+
console.log(`✓ a many search whose ripgrep exited between samples is named as not measured, not bounded, and the run exits 0 ("${verdicts[0]}")`);
36+
return true;
37+
} catch (error) {
38+
console.log(`✗ ${error.message}`);
39+
return false;
40+
}
41+
}
42+
43+
runIfMain(import.meta.url, runTests);

0 commit comments

Comments
 (0)