Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
test_runner: simplify raw stdout frame parsing loop
#processRawBuffer used nested loops and a mimicsFrame flag, which made
the resync path hard to follow. Rewrite it as a single loop that
inspects the head of the buffer once per pass and handles one case:
leading stdout, an incomplete frame, a frame that only mimics the
header, or a real frame. The existing tests cover each case.

Signed-off-by: Muhammad Faizan Uddin <faizan.uddin94@gmail.com>
  • Loading branch information
faizanu94 committed Sep 26, 2026
commit d249668979910f2a55747615fb480101e874d958
147 changes: 67 additions & 80 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ const {
kTestTimeoutFailure,
Test,
} = require('internal/test_runner/test');
const { FastBuffer } = require('internal/buffer');

const {
createRandomSeed,
convertStringToRegExp,
Expand Down Expand Up @@ -465,99 +463,88 @@ class FileTest extends Test {
}
}
#processRawBuffer() {
// This method is called when it is known that there is at least one message
let bufferHead = this.#rawBuffer[0];

while (bufferHead) {
// Emit any leading bytes that are not the start of a frame as stdout,
// until a v8 header sits at the front of the buffer.
let headerIndex = bufferHead.indexOf(v8Header);
let nonSerialized = new FastBuffer();

while (bufferHead && headerIndex !== 0) {
const nonSerializedData = headerIndex === -1 ?
bufferHead :
bufferHead.slice(0, headerIndex);
nonSerialized = Buffer.concat([nonSerialized, nonSerializedData]);
this.#rawBufferSize -= TypedArrayPrototypeGetLength(nonSerializedData);
// This method is called when it is known that there is at least one message.
// Each pass looks at the head of the buffer and handles exactly one of four
// cases, then re-checks the new head. Recovery from stray stdout that only
// mimics a frame happens inline here, so no separate resync pass is needed.
while (this.#rawBuffer.length > 0) {
const bufferHead = this.#rawBuffer[0];
const headerIndex = bufferHead.indexOf(v8Header);

// 1. The head does not start with a frame header. Emit the bytes before
// the next header, or the whole head when there is none, as stdout and
// advance to the next header.
if (headerIndex !== 0) {
const nonSerialized = headerIndex === -1 ?
bufferHead : TypedArrayPrototypeSubarray(bufferHead, 0, headerIndex);
this.addToReport({
__proto__: null,
type: 'test:stdout',
data: { __proto__: null, file: this.name, message: nonSerialized.toString('utf-8') },
});
this.#rawBufferSize -= TypedArrayPrototypeGetLength(nonSerialized);
if (headerIndex === -1) {
ArrayPrototypeShift(this.#rawBuffer);
} else {
this.#rawBuffer[0] = TypedArrayPrototypeSubarray(bufferHead, headerIndex);
}
bufferHead = this.#rawBuffer[0];
headerIndex = bufferHead?.indexOf(v8Header);
continue;
}

// 2. The head starts with a header but the whole frame has not arrived
// yet. Stop and wait for more data.
if (TypedArrayPrototypeGetLength(bufferHead) < kSerializedSizeHeader) {
break;
}
// We call `readUInt32BE` manually here, because this is faster than first
// converting it to a buffer and using `readUInt32BE` on that.
const fullMessageSize = ((
bufferHead[kV8HeaderLength] << 24 |
bufferHead[kV8HeaderLength + 1] << 16 |
bufferHead[kV8HeaderLength + 2] << 8 |
bufferHead[kV8HeaderLength + 3]
) >>> 0) + kSerializedSizeHeader;
if (this.#rawBufferSize < fullMessageSize) {
break;
}

if (TypedArrayPrototypeGetLength(nonSerialized) > 0) {
const concatenatedBuffer = this.#rawBuffer.length === 1 ?
bufferHead : Buffer.concat(this.#rawBuffer, this.#rawBufferSize);

// 3. The head only mimics a frame. A real frame repeats the v8 header at
// the start of its payload, right before the serialized value, so a
// payload too short for that header, or one that does not start with
// it, is stray stdout. Emit one byte and let the next pass resync on
// the following header. A genuine frame that fails to deserialize is
// left to throw, so real report-protocol regressions are not hidden.
if (fullMessageSize - kSerializedSizeHeader < kV8HeaderLength ||
concatenatedBuffer.indexOf(v8Header, kSerializedSizeHeader) !== kSerializedSizeHeader) {
this.addToReport({
__proto__: null,
type: 'test:stdout',
data: { __proto__: null, file: this.name, message: nonSerialized.toString('utf-8') },
data: { __proto__: null, file: this.name, message: StringFromCharCode(bufferHead[0]) },
});
}

let mimicsFrame = false;
while (bufferHead?.length >= kSerializedSizeHeader) {
// We call `readUInt32BE` manually here, because this is faster than first converting
// it to a buffer and using `readUInt32BE` on that.
const fullMessageSize = ((
bufferHead[kV8HeaderLength] << 24 |
bufferHead[kV8HeaderLength + 1] << 16 |
bufferHead[kV8HeaderLength + 2] << 8 |
bufferHead[kV8HeaderLength + 3]
) >>> 0) + kSerializedSizeHeader;

if (this.#rawBufferSize < fullMessageSize) break;

const concatenatedBuffer = this.#rawBuffer.length === 1 ?
this.#rawBuffer[0] : Buffer.concat(this.#rawBuffer, this.#rawBufferSize);

// A real frame repeats the v8 header at the start of its payload, right
// before the serialized value. If that inner header is missing, these
// bytes only mimic a frame, so stop deserializing and fall through to
// the resync below. A genuine frame that fails to deserialize is left
// to throw, so real report-protocol regressions are not hidden.
if (fullMessageSize - kSerializedSizeHeader < kV8HeaderLength ||
concatenatedBuffer.indexOf(v8Header, kSerializedSizeHeader) !== kSerializedSizeHeader) {
mimicsFrame = true;
break;
if (TypedArrayPrototypeGetLength(bufferHead) === 1) {
ArrayPrototypeShift(this.#rawBuffer);
} else {
this.#rawBuffer[0] = TypedArrayPrototypeSubarray(bufferHead, 1);
}

const deserializer = new DefaultDeserializer(
TypedArrayPrototypeSubarray(concatenatedBuffer, kSerializedSizeHeader, fullMessageSize),
);
deserializer.readHeader();
const item = deserializer.readValue();

bufferHead = TypedArrayPrototypeSubarray(concatenatedBuffer, fullMessageSize);
this.#rawBufferSize = TypedArrayPrototypeGetLength(bufferHead);
this.#rawBuffer = this.#rawBufferSize !== 0 ? [bufferHead] : [];

this.addToReport(item);
this.#rawBufferSize--;
continue;
}

if (!mimicsFrame) {
// The head is either exhausted or an incomplete frame we must wait for.
break;
}
// 4. A real frame. Deserialize it and continue from the remaining bytes.
const deserializer = new DefaultDeserializer(
TypedArrayPrototypeSubarray(concatenatedBuffer, kSerializedSizeHeader, fullMessageSize),
);
deserializer.readHeader();
const item = deserializer.readValue();

// The head only mimics a frame. Emit its first byte as stdout and restart
// the header search, so a real frame that follows is reported right away
// instead of waiting for #drainRawBuffer at shutdown. This mirrors the no
// progress path in #drainRawBuffer, so the stdout bytes come out the same.
this.addToReport({
__proto__: null,
type: 'test:stdout',
data: { __proto__: null, file: this.name, message: StringFromCharCode(bufferHead[0]) },
});
if (TypedArrayPrototypeGetLength(bufferHead) === 1) {
ArrayPrototypeShift(this.#rawBuffer);
} else {
this.#rawBuffer[0] = TypedArrayPrototypeSubarray(bufferHead, 1);
}
this.#rawBufferSize--;
bufferHead = this.#rawBuffer[0];
const remaining = TypedArrayPrototypeSubarray(concatenatedBuffer, fullMessageSize);
this.#rawBufferSize = TypedArrayPrototypeGetLength(remaining);
this.#rawBuffer = this.#rawBufferSize !== 0 ? [remaining] : [];

this.addToReport(item);
}
}
}
Expand Down
Loading