| 24 | let serverFinReceived = false; |
| 25 | |
| 26 | function assertQlogOutput(chunks, finReceived, side) { |
| 27 | ok(chunks.length > 0, `Expected ${side} qlog chunks, got ${chunks.length}`); |
| 28 | ok(finReceived, `Expected ${side} to receive fin`); |
| 29 | |
| 30 | for (const { data, fin } of chunks) { |
| 31 | strictEqual(typeof data, 'string', |
| 32 | `Each ${side} qlog chunk should be a string`); |
| 33 | strictEqual(typeof fin, 'boolean', |
| 34 | `Each ${side} fin flag should be a boolean`); |
| 35 | } |
| 36 | |
| 37 | // Only the last chunk should have fin=true. |
| 38 | for (let i = 0; i < chunks.length - 1; i++) { |
| 39 | strictEqual(chunks[i].fin, false, |
| 40 | `${side} chunk ${i} should not be fin`); |
| 41 | } |
| 42 | strictEqual(chunks[chunks.length - 1].fin, true, |
| 43 | `${side} last chunk should be fin`); |
| 44 | |
| 45 | // ngtcp2 emits qlog in JSON-SEQ format (RFC 7464): each record is |
| 46 | // prefixed with 0x1e (Record Separator) and terminated by a newline. |
| 47 | // Parse the individual records and verify the header has expected fields. |
| 48 | const joined = chunks.map((c) => c.data).join(''); |
| 49 | const records = joined.split('\x1e').filter((s) => s.trim().length > 0); |
| 50 | ok(records.length > 0, `${side} qlog should have at least one record`); |
| 51 | |
| 52 | // The first record is the qlog header with format metadata. |
| 53 | const header = JSON.parse(records[0]); |
| 54 | |
| 55 | ok(header.qlog_version !== undefined || header.qlog_format !== undefined, |
| 56 | `${side} qlog header should have qlog_version or qlog_format field`); |
| 57 | |
| 58 | for (let i = 1; i < records.length; i++) { |
| 59 | const record = JSON.parse(records[i]); |
| 60 | ok('name' in record); |
| 61 | ok('data' in record); |
| 62 | ok('time' in record); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | const serverEndpoint = await listen(mustCall(async (serverSession) => { |
| 67 | await serverSession.opened; |