@durable-streams/client-conformance-tests 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/typescript-adapter.cjs +72 -22
- package/dist/adapters/typescript-adapter.js +72 -22
- package/dist/{benchmark-runner-CrE6JkbX.js → benchmark-runner-81waaCzs.js} +89 -9
- package/dist/{benchmark-runner-Db4he452.cjs → benchmark-runner-DliEfq9k.cjs} +93 -8
- package/dist/cli.cjs +41 -5
- package/dist/cli.js +41 -5
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +50 -3
- package/dist/index.d.ts +50 -3
- package/dist/index.js +2 -2
- package/dist/{protocol-qb83AeUH.js → protocol-1p0soayz.js} +2 -1
- package/dist/{protocol-D37G3c4e.d.cts → protocol-BxZTqJmO.d.cts} +67 -5
- package/dist/{protocol-XeAOKBD-.cjs → protocol-IioVPNaP.cjs} +2 -1
- package/dist/{protocol-Mcbiq3nQ.d.ts → protocol-JuFzdV5x.d.ts} +67 -5
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +1 -1
- package/package.json +8 -3
- package/src/adapters/typescript-adapter.ts +110 -32
- package/src/benchmark-runner.ts +75 -1
- package/src/benchmark-scenarios.ts +4 -4
- package/src/cli.ts +46 -5
- package/src/protocol.ts +75 -2
- package/src/runner.ts +72 -1
- package/src/test-cases.ts +55 -0
- package/test-cases/consumer/error-context.yaml +67 -0
- package/test-cases/consumer/json-parsing-errors.yaml +115 -0
- package/test-cases/consumer/read-auto.yaml +155 -0
- package/test-cases/consumer/read-sse.yaml +24 -0
- package/test-cases/consumer/retry-resilience.yaml +28 -0
- package/test-cases/consumer/sse-parsing-errors.yaml +121 -0
- package/test-cases/producer/error-context.yaml +72 -0
- package/test-cases/producer/idempotent-json-batching.yaml +40 -0
- package/test-cases/validation/input-validation.yaml +192 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
const require_chunk = require('../chunk-BCwAaXi7.cjs');
|
|
4
|
-
const require_protocol = require('../protocol-
|
|
4
|
+
const require_protocol = require('../protocol-IioVPNaP.cjs');
|
|
5
5
|
const node_readline = require_chunk.__toESM(require("node:readline"));
|
|
6
6
|
const __durable_streams_client = require_chunk.__toESM(require("@durable-streams/client"));
|
|
7
7
|
|
|
@@ -79,8 +79,10 @@ async function handleCommand(command) {
|
|
|
79
79
|
batching: true,
|
|
80
80
|
sse: true,
|
|
81
81
|
longPoll: true,
|
|
82
|
+
auto: true,
|
|
82
83
|
streaming: true,
|
|
83
|
-
dynamicHeaders: true
|
|
84
|
+
dynamicHeaders: true,
|
|
85
|
+
strictZeroValidation: true
|
|
84
86
|
}
|
|
85
87
|
};
|
|
86
88
|
}
|
|
@@ -168,6 +170,7 @@ async function handleCommand(command) {
|
|
|
168
170
|
let live;
|
|
169
171
|
if (command.live === `long-poll`) live = `long-poll`;
|
|
170
172
|
else if (command.live === `sse`) live = `sse`;
|
|
173
|
+
else if (command.live === true) live = true;
|
|
171
174
|
else live = false;
|
|
172
175
|
const abortController = new AbortController();
|
|
173
176
|
const timeoutMs = command.timeoutMs ?? 5e3;
|
|
@@ -202,21 +205,30 @@ async function handleCommand(command) {
|
|
|
202
205
|
let finalOffset = command.offset ?? response.offset;
|
|
203
206
|
let upToDate = response.upToDate;
|
|
204
207
|
const maxChunks = command.maxChunks ?? 100;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
const contentType = streamContentTypes.get(command.path);
|
|
209
|
+
const isJson = contentType?.includes(`application/json`) ?? false;
|
|
210
|
+
if (!live) {
|
|
211
|
+
if (isJson) {
|
|
212
|
+
const items = await response.json();
|
|
213
|
+
if (items.length > 0) chunks.push({
|
|
214
|
+
data: JSON.stringify(items),
|
|
215
|
+
offset: response.offset
|
|
216
|
+
});
|
|
217
|
+
} else {
|
|
218
|
+
const data = await response.body();
|
|
219
|
+
if (data.length > 0) chunks.push({
|
|
220
|
+
data: new TextDecoder().decode(data),
|
|
221
|
+
offset: response.offset
|
|
222
|
+
});
|
|
223
|
+
}
|
|
211
224
|
finalOffset = response.offset;
|
|
212
225
|
upToDate = response.upToDate;
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
226
|
+
} else {
|
|
215
227
|
const decoder = new TextDecoder();
|
|
216
228
|
const startTime = Date.now();
|
|
217
229
|
let chunkCount = 0;
|
|
218
230
|
let done = false;
|
|
219
|
-
await new Promise((resolve) => {
|
|
231
|
+
await new Promise((resolve, reject) => {
|
|
220
232
|
const subscriptionTimeoutId = setTimeout(() => {
|
|
221
233
|
done = true;
|
|
222
234
|
abortController.abort();
|
|
@@ -262,11 +274,11 @@ async function handleCommand(command) {
|
|
|
262
274
|
finalOffset = response.offset;
|
|
263
275
|
resolve();
|
|
264
276
|
}
|
|
265
|
-
}).catch(() => {
|
|
277
|
+
}).catch((err) => {
|
|
266
278
|
if (!done) {
|
|
267
279
|
done = true;
|
|
268
280
|
clearTimeout(subscriptionTimeoutId);
|
|
269
|
-
|
|
281
|
+
reject(err);
|
|
270
282
|
}
|
|
271
283
|
});
|
|
272
284
|
});
|
|
@@ -364,11 +376,8 @@ async function handleCommand(command) {
|
|
|
364
376
|
maxInFlight: 1,
|
|
365
377
|
lingerMs: 0
|
|
366
378
|
});
|
|
367
|
-
const normalizedContentType = contentType.split(`;`)[0]?.trim().toLowerCase();
|
|
368
|
-
const isJson = normalizedContentType === `application/json`;
|
|
369
|
-
const data = isJson ? JSON.parse(command.data) : command.data;
|
|
370
379
|
try {
|
|
371
|
-
producer.append(data);
|
|
380
|
+
producer.append(command.data);
|
|
372
381
|
await producer.flush();
|
|
373
382
|
await producer.close();
|
|
374
383
|
return {
|
|
@@ -399,11 +408,8 @@ async function handleCommand(command) {
|
|
|
399
408
|
lingerMs: testingConcurrency ? 0 : 1e3,
|
|
400
409
|
maxBatchBytes: testingConcurrency ? 1 : 1024 * 1024
|
|
401
410
|
});
|
|
402
|
-
const normalizedContentType = contentType.split(`;`)[0]?.trim().toLowerCase();
|
|
403
|
-
const isJson = normalizedContentType === `application/json`;
|
|
404
|
-
const items = isJson ? command.items.map((item) => JSON.parse(item)) : command.items;
|
|
405
411
|
try {
|
|
406
|
-
for (const item of items) producer.append(item);
|
|
412
|
+
for (const item of command.items) producer.append(item);
|
|
407
413
|
await producer.flush();
|
|
408
414
|
await producer.close();
|
|
409
415
|
return {
|
|
@@ -418,6 +424,43 @@ async function handleCommand(command) {
|
|
|
418
424
|
} catch (err) {
|
|
419
425
|
return errorResult(`idempotent-append-batch`, err);
|
|
420
426
|
}
|
|
427
|
+
case `validate`: {
|
|
428
|
+
const { target } = command;
|
|
429
|
+
try {
|
|
430
|
+
switch (target.target) {
|
|
431
|
+
case `retry-options`: return {
|
|
432
|
+
type: `validate`,
|
|
433
|
+
success: true
|
|
434
|
+
};
|
|
435
|
+
case `idempotent-producer`: {
|
|
436
|
+
const ds = new __durable_streams_client.DurableStream({ url: `${serverUrl}/test-validate` });
|
|
437
|
+
new __durable_streams_client.IdempotentProducer(ds, target.producerId ?? `test-producer`, {
|
|
438
|
+
epoch: target.epoch,
|
|
439
|
+
maxBatchBytes: target.maxBatchBytes
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
type: `validate`,
|
|
443
|
+
success: true
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
default: return {
|
|
447
|
+
type: `error`,
|
|
448
|
+
success: false,
|
|
449
|
+
commandType: `validate`,
|
|
450
|
+
errorCode: require_protocol.ErrorCodes.NOT_SUPPORTED,
|
|
451
|
+
message: `Unknown validation target: ${target.target}`
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
} catch (err) {
|
|
455
|
+
return {
|
|
456
|
+
type: `error`,
|
|
457
|
+
success: false,
|
|
458
|
+
commandType: `validate`,
|
|
459
|
+
errorCode: require_protocol.ErrorCodes.INVALID_ARGUMENT,
|
|
460
|
+
message: err instanceof Error ? err.message : String(err)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
421
464
|
default: return {
|
|
422
465
|
type: `error`,
|
|
423
466
|
success: false,
|
|
@@ -443,7 +486,7 @@ function errorResult(commandType, err) {
|
|
|
443
486
|
} else if (err.code === `BAD_REQUEST`) {
|
|
444
487
|
errorCode = require_protocol.ErrorCodes.INVALID_OFFSET;
|
|
445
488
|
status = 400;
|
|
446
|
-
}
|
|
489
|
+
} else if (err.code === `PARSE_ERROR`) errorCode = require_protocol.ErrorCodes.PARSE_ERROR;
|
|
447
490
|
return {
|
|
448
491
|
type: `error`,
|
|
449
492
|
success: false,
|
|
@@ -479,6 +522,13 @@ function errorResult(commandType, err) {
|
|
|
479
522
|
errorCode: require_protocol.ErrorCodes.NETWORK_ERROR,
|
|
480
523
|
message: err.message
|
|
481
524
|
};
|
|
525
|
+
if (err instanceof SyntaxError || err.name === `SyntaxError` || err.message.includes(`JSON`) || err.message.includes(`parse`) || err.message.includes(`SSE`) || err.message.includes(`control event`)) return {
|
|
526
|
+
type: `error`,
|
|
527
|
+
success: false,
|
|
528
|
+
commandType,
|
|
529
|
+
errorCode: require_protocol.ErrorCodes.PARSE_ERROR,
|
|
530
|
+
message: err.message
|
|
531
|
+
};
|
|
482
532
|
return {
|
|
483
533
|
type: `error`,
|
|
484
534
|
success: false,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ErrorCodes, decodeBase64, parseCommand, serializeResult } from "../protocol-
|
|
2
|
+
import { ErrorCodes, decodeBase64, parseCommand, serializeResult } from "../protocol-1p0soayz.js";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { DurableStream, DurableStreamError, FetchError, IdempotentProducer, stream } from "@durable-streams/client";
|
|
5
5
|
|
|
@@ -77,8 +77,10 @@ async function handleCommand(command) {
|
|
|
77
77
|
batching: true,
|
|
78
78
|
sse: true,
|
|
79
79
|
longPoll: true,
|
|
80
|
+
auto: true,
|
|
80
81
|
streaming: true,
|
|
81
|
-
dynamicHeaders: true
|
|
82
|
+
dynamicHeaders: true,
|
|
83
|
+
strictZeroValidation: true
|
|
82
84
|
}
|
|
83
85
|
};
|
|
84
86
|
}
|
|
@@ -166,6 +168,7 @@ async function handleCommand(command) {
|
|
|
166
168
|
let live;
|
|
167
169
|
if (command.live === `long-poll`) live = `long-poll`;
|
|
168
170
|
else if (command.live === `sse`) live = `sse`;
|
|
171
|
+
else if (command.live === true) live = true;
|
|
169
172
|
else live = false;
|
|
170
173
|
const abortController = new AbortController();
|
|
171
174
|
const timeoutMs = command.timeoutMs ?? 5e3;
|
|
@@ -200,21 +203,30 @@ async function handleCommand(command) {
|
|
|
200
203
|
let finalOffset = command.offset ?? response.offset;
|
|
201
204
|
let upToDate = response.upToDate;
|
|
202
205
|
const maxChunks = command.maxChunks ?? 100;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
const contentType = streamContentTypes.get(command.path);
|
|
207
|
+
const isJson = contentType?.includes(`application/json`) ?? false;
|
|
208
|
+
if (!live) {
|
|
209
|
+
if (isJson) {
|
|
210
|
+
const items = await response.json();
|
|
211
|
+
if (items.length > 0) chunks.push({
|
|
212
|
+
data: JSON.stringify(items),
|
|
213
|
+
offset: response.offset
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
const data = await response.body();
|
|
217
|
+
if (data.length > 0) chunks.push({
|
|
218
|
+
data: new TextDecoder().decode(data),
|
|
219
|
+
offset: response.offset
|
|
220
|
+
});
|
|
221
|
+
}
|
|
209
222
|
finalOffset = response.offset;
|
|
210
223
|
upToDate = response.upToDate;
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
224
|
+
} else {
|
|
213
225
|
const decoder = new TextDecoder();
|
|
214
226
|
const startTime = Date.now();
|
|
215
227
|
let chunkCount = 0;
|
|
216
228
|
let done = false;
|
|
217
|
-
await new Promise((resolve) => {
|
|
229
|
+
await new Promise((resolve, reject) => {
|
|
218
230
|
const subscriptionTimeoutId = setTimeout(() => {
|
|
219
231
|
done = true;
|
|
220
232
|
abortController.abort();
|
|
@@ -260,11 +272,11 @@ async function handleCommand(command) {
|
|
|
260
272
|
finalOffset = response.offset;
|
|
261
273
|
resolve();
|
|
262
274
|
}
|
|
263
|
-
}).catch(() => {
|
|
275
|
+
}).catch((err) => {
|
|
264
276
|
if (!done) {
|
|
265
277
|
done = true;
|
|
266
278
|
clearTimeout(subscriptionTimeoutId);
|
|
267
|
-
|
|
279
|
+
reject(err);
|
|
268
280
|
}
|
|
269
281
|
});
|
|
270
282
|
});
|
|
@@ -362,11 +374,8 @@ async function handleCommand(command) {
|
|
|
362
374
|
maxInFlight: 1,
|
|
363
375
|
lingerMs: 0
|
|
364
376
|
});
|
|
365
|
-
const normalizedContentType = contentType.split(`;`)[0]?.trim().toLowerCase();
|
|
366
|
-
const isJson = normalizedContentType === `application/json`;
|
|
367
|
-
const data = isJson ? JSON.parse(command.data) : command.data;
|
|
368
377
|
try {
|
|
369
|
-
producer.append(data);
|
|
378
|
+
producer.append(command.data);
|
|
370
379
|
await producer.flush();
|
|
371
380
|
await producer.close();
|
|
372
381
|
return {
|
|
@@ -397,11 +406,8 @@ async function handleCommand(command) {
|
|
|
397
406
|
lingerMs: testingConcurrency ? 0 : 1e3,
|
|
398
407
|
maxBatchBytes: testingConcurrency ? 1 : 1024 * 1024
|
|
399
408
|
});
|
|
400
|
-
const normalizedContentType = contentType.split(`;`)[0]?.trim().toLowerCase();
|
|
401
|
-
const isJson = normalizedContentType === `application/json`;
|
|
402
|
-
const items = isJson ? command.items.map((item) => JSON.parse(item)) : command.items;
|
|
403
409
|
try {
|
|
404
|
-
for (const item of items) producer.append(item);
|
|
410
|
+
for (const item of command.items) producer.append(item);
|
|
405
411
|
await producer.flush();
|
|
406
412
|
await producer.close();
|
|
407
413
|
return {
|
|
@@ -416,6 +422,43 @@ async function handleCommand(command) {
|
|
|
416
422
|
} catch (err) {
|
|
417
423
|
return errorResult(`idempotent-append-batch`, err);
|
|
418
424
|
}
|
|
425
|
+
case `validate`: {
|
|
426
|
+
const { target } = command;
|
|
427
|
+
try {
|
|
428
|
+
switch (target.target) {
|
|
429
|
+
case `retry-options`: return {
|
|
430
|
+
type: `validate`,
|
|
431
|
+
success: true
|
|
432
|
+
};
|
|
433
|
+
case `idempotent-producer`: {
|
|
434
|
+
const ds = new DurableStream({ url: `${serverUrl}/test-validate` });
|
|
435
|
+
new IdempotentProducer(ds, target.producerId ?? `test-producer`, {
|
|
436
|
+
epoch: target.epoch,
|
|
437
|
+
maxBatchBytes: target.maxBatchBytes
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
type: `validate`,
|
|
441
|
+
success: true
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
default: return {
|
|
445
|
+
type: `error`,
|
|
446
|
+
success: false,
|
|
447
|
+
commandType: `validate`,
|
|
448
|
+
errorCode: ErrorCodes.NOT_SUPPORTED,
|
|
449
|
+
message: `Unknown validation target: ${target.target}`
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
return {
|
|
454
|
+
type: `error`,
|
|
455
|
+
success: false,
|
|
456
|
+
commandType: `validate`,
|
|
457
|
+
errorCode: ErrorCodes.INVALID_ARGUMENT,
|
|
458
|
+
message: err instanceof Error ? err.message : String(err)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
419
462
|
default: return {
|
|
420
463
|
type: `error`,
|
|
421
464
|
success: false,
|
|
@@ -441,7 +484,7 @@ function errorResult(commandType, err) {
|
|
|
441
484
|
} else if (err.code === `BAD_REQUEST`) {
|
|
442
485
|
errorCode = ErrorCodes.INVALID_OFFSET;
|
|
443
486
|
status = 400;
|
|
444
|
-
}
|
|
487
|
+
} else if (err.code === `PARSE_ERROR`) errorCode = ErrorCodes.PARSE_ERROR;
|
|
445
488
|
return {
|
|
446
489
|
type: `error`,
|
|
447
490
|
success: false,
|
|
@@ -477,6 +520,13 @@ function errorResult(commandType, err) {
|
|
|
477
520
|
errorCode: ErrorCodes.NETWORK_ERROR,
|
|
478
521
|
message: err.message
|
|
479
522
|
};
|
|
523
|
+
if (err instanceof SyntaxError || err.name === `SyntaxError` || err.message.includes(`JSON`) || err.message.includes(`parse`) || err.message.includes(`SSE`) || err.message.includes(`control event`)) return {
|
|
524
|
+
type: `error`,
|
|
525
|
+
success: false,
|
|
526
|
+
commandType,
|
|
527
|
+
errorCode: ErrorCodes.PARSE_ERROR,
|
|
528
|
+
message: err.message
|
|
529
|
+
};
|
|
480
530
|
return {
|
|
481
531
|
type: `error`,
|
|
482
532
|
success: false,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { calculateStats, formatStats, parseResult, serializeCommand } from "./protocol-
|
|
1
|
+
import { calculateStats, formatStats, parseResult, serializeCommand } from "./protocol-1p0soayz.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { DurableStreamTestServer } from "@durable-streams/server";
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import { join } from "node:path";
|
|
8
9
|
import YAML from "yaml";
|
|
10
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
9
11
|
import { DurableStream } from "@durable-streams/client";
|
|
10
12
|
|
|
11
13
|
//#region src/test-cases.ts
|
|
@@ -176,7 +178,7 @@ async function executeOperation(op, ctx) {
|
|
|
176
178
|
}
|
|
177
179
|
case `append`: {
|
|
178
180
|
const path$1 = resolveVariables(op.path, variables);
|
|
179
|
-
const data = op.data ? resolveVariables(op.data, variables) : ``;
|
|
181
|
+
const data = op.json !== void 0 ? JSON.stringify(op.json) : op.data ? resolveVariables(op.data, variables) : ``;
|
|
180
182
|
const result = await client.send({
|
|
181
183
|
type: `append`,
|
|
182
184
|
path: path$1,
|
|
@@ -411,7 +413,8 @@ async function executeOperation(op, ctx) {
|
|
|
411
413
|
probability: op.probability,
|
|
412
414
|
method: op.method,
|
|
413
415
|
corruptBody: op.corruptBody,
|
|
414
|
-
jitterMs: op.jitterMs
|
|
416
|
+
jitterMs: op.jitterMs,
|
|
417
|
+
injectSseEvent: op.injectSseEvent
|
|
415
418
|
})
|
|
416
419
|
});
|
|
417
420
|
const faultTypes = [];
|
|
@@ -422,6 +425,7 @@ async function executeOperation(op, ctx) {
|
|
|
422
425
|
if (op.truncateBodyBytes != null) faultTypes.push(`truncate=${op.truncateBodyBytes}b`);
|
|
423
426
|
if (op.corruptBody) faultTypes.push(`corrupt`);
|
|
424
427
|
if (op.probability != null) faultTypes.push(`p=${op.probability}`);
|
|
428
|
+
if (op.injectSseEvent) faultTypes.push(`sse:${op.injectSseEvent.eventType}`);
|
|
425
429
|
const faultDesc = faultTypes.join(`,`) || `unknown`;
|
|
426
430
|
if (verbose) console.log(` inject-error ${path$1} [${faultDesc}]x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`);
|
|
427
431
|
if (!response.ok) return { error: `Failed to inject fault: ${response.status}` };
|
|
@@ -461,6 +465,17 @@ async function executeOperation(op, ctx) {
|
|
|
461
465
|
if (verbose) console.log(` clear-dynamic: ${result.success ? `ok` : `failed`}`);
|
|
462
466
|
return { result };
|
|
463
467
|
}
|
|
468
|
+
case `validate`: {
|
|
469
|
+
const result = await client.send({
|
|
470
|
+
type: `validate`,
|
|
471
|
+
target: op.target
|
|
472
|
+
}, commandTimeout);
|
|
473
|
+
if (verbose) {
|
|
474
|
+
const targetType = op.target.target;
|
|
475
|
+
console.log(` validate ${targetType}: ${result.success ? `ok` : `failed`}`);
|
|
476
|
+
}
|
|
477
|
+
return { result };
|
|
478
|
+
}
|
|
464
479
|
default: return { error: `Unknown operation: ${op.action}` };
|
|
465
480
|
}
|
|
466
481
|
}
|
|
@@ -485,6 +500,13 @@ function validateExpectation(result, expect) {
|
|
|
485
500
|
if (result.success) return `Expected error ${expect.errorCode}, but operation succeeded`;
|
|
486
501
|
if (isErrorResult(result) && result.errorCode !== expect.errorCode) return `Expected error code ${expect.errorCode}, got ${result.errorCode}`;
|
|
487
502
|
}
|
|
503
|
+
if (expect.messageContains !== void 0) {
|
|
504
|
+
if (result.success) return `Expected error with message containing ${JSON.stringify(expect.messageContains)}, but operation succeeded`;
|
|
505
|
+
if (isErrorResult(result)) {
|
|
506
|
+
const missing = expect.messageContains.filter((s) => !result.message.toLowerCase().includes(s.toLowerCase()));
|
|
507
|
+
if (missing.length > 0) return `Expected error message to contain [${expect.messageContains.join(`, `)}], missing: [${missing.join(`, `)}]. Actual message: "${result.message}"`;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
488
510
|
if (expect.data !== void 0 && isReadResult(result)) {
|
|
489
511
|
const actualData = result.chunks.map((c) => c.data).join(``);
|
|
490
512
|
if (actualData !== expect.data) return `Expected data "${expect.data}", got "${actualData}"`;
|
|
@@ -557,6 +579,13 @@ function validateExpectation(result, expect) {
|
|
|
557
579
|
if (expect.producerReceivedSeq !== void 0 && isAppendResult(result)) {
|
|
558
580
|
if (result.producerReceivedSeq !== expect.producerReceivedSeq) return `Expected producerReceivedSeq=${expect.producerReceivedSeq}, got ${result.producerReceivedSeq}`;
|
|
559
581
|
}
|
|
582
|
+
if (expect.valid !== void 0) {
|
|
583
|
+
if (expect.valid === true && !result.success) return `Expected validation to pass, but it failed`;
|
|
584
|
+
if (expect.valid === false && result.success) return `Expected validation to fail, but it passed`;
|
|
585
|
+
}
|
|
586
|
+
if (expect.errorContains !== void 0 && isErrorResult(result)) {
|
|
587
|
+
if (!result.message.includes(expect.errorContains)) return `Expected error message to contain "${expect.errorContains}", got "${result.message}"`;
|
|
588
|
+
}
|
|
560
589
|
return null;
|
|
561
590
|
}
|
|
562
591
|
/**
|
|
@@ -568,9 +597,16 @@ function featureToProperty(feature) {
|
|
|
568
597
|
sse: `sse`,
|
|
569
598
|
"long-poll": `longPoll`,
|
|
570
599
|
longPoll: `longPoll`,
|
|
600
|
+
auto: `auto`,
|
|
571
601
|
streaming: `streaming`,
|
|
572
602
|
dynamicHeaders: `dynamicHeaders`,
|
|
573
|
-
"dynamic-headers": `dynamicHeaders
|
|
603
|
+
"dynamic-headers": `dynamicHeaders`,
|
|
604
|
+
retryOptions: `retryOptions`,
|
|
605
|
+
"retry-options": `retryOptions`,
|
|
606
|
+
batchItems: `batchItems`,
|
|
607
|
+
"batch-items": `batchItems`,
|
|
608
|
+
strictZeroValidation: `strictZeroValidation`,
|
|
609
|
+
"strict-zero-validation": `strictZeroValidation`
|
|
574
610
|
};
|
|
575
611
|
return map[feature];
|
|
576
612
|
}
|
|
@@ -868,7 +904,7 @@ const smallMessageThroughputScenario = {
|
|
|
868
904
|
createOperation: (ctx) => ({
|
|
869
905
|
op: `throughput_append`,
|
|
870
906
|
path: `${ctx.basePath}/throughput-small`,
|
|
871
|
-
count:
|
|
907
|
+
count: 1e5,
|
|
872
908
|
size: 100,
|
|
873
909
|
concurrency: 200
|
|
874
910
|
})
|
|
@@ -911,8 +947,8 @@ const readThroughputScenario = {
|
|
|
911
947
|
expectedCount: ctx.setupData.expectedCount
|
|
912
948
|
}),
|
|
913
949
|
setup: (ctx) => {
|
|
914
|
-
ctx.setupData.expectedCount =
|
|
915
|
-
return Promise.resolve({ data: { expectedCount:
|
|
950
|
+
ctx.setupData.expectedCount = 1e5;
|
|
951
|
+
return Promise.resolve({ data: { expectedCount: 1e5 } });
|
|
916
952
|
}
|
|
917
953
|
};
|
|
918
954
|
const sseLatencyScenario = {
|
|
@@ -1104,7 +1140,7 @@ async function runScenario(scenario, client, serverUrl, clientFeatures, verbose,
|
|
|
1104
1140
|
n: i,
|
|
1105
1141
|
data: `message-${i}-padding-for-size`
|
|
1106
1142
|
});
|
|
1107
|
-
await Promise.all(messages.map((msg) => ds.append(msg)));
|
|
1143
|
+
await Promise.all(messages.map((msg) => ds.append(JSON.stringify(msg))));
|
|
1108
1144
|
}
|
|
1109
1145
|
}
|
|
1110
1146
|
if (verbose) log(` Warmup: ${scenario.config.warmupIterations} iterations...`);
|
|
@@ -1422,6 +1458,50 @@ async function runBenchmarks(options) {
|
|
|
1422
1458
|
}
|
|
1423
1459
|
return summary;
|
|
1424
1460
|
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Reads benchmark JSON files from a directory and generates a combined markdown report.
|
|
1463
|
+
* Each subdirectory should contain a benchmark-results.json file.
|
|
1464
|
+
*/
|
|
1465
|
+
async function aggregateBenchmarkResults(resultsDir) {
|
|
1466
|
+
const entries = await readdir(resultsDir, { withFileTypes: true });
|
|
1467
|
+
const summaries = [];
|
|
1468
|
+
for (const entry of entries) {
|
|
1469
|
+
if (!entry.isDirectory()) continue;
|
|
1470
|
+
const jsonPath = join(resultsDir, entry.name, `benchmark-results.json`);
|
|
1471
|
+
try {
|
|
1472
|
+
const content = await readFile(jsonPath, `utf-8`);
|
|
1473
|
+
const summary = JSON.parse(content);
|
|
1474
|
+
summary.results = summary.results.map((r) => ({
|
|
1475
|
+
...r,
|
|
1476
|
+
scenario: r.scenario
|
|
1477
|
+
}));
|
|
1478
|
+
summaries.push(summary);
|
|
1479
|
+
} catch {
|
|
1480
|
+
console.error(`Skipping ${entry.name}: no valid benchmark-results.json`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (summaries.length === 0) return `# Client Benchmark Results\n\nNo benchmark results found.\n`;
|
|
1484
|
+
summaries.sort((a, b) => a.adapter.localeCompare(b.adapter));
|
|
1485
|
+
const lines = [];
|
|
1486
|
+
lines.push(`# Client Benchmark Results`);
|
|
1487
|
+
lines.push(``);
|
|
1488
|
+
const totalPassed = summaries.reduce((sum, s) => sum + s.passed, 0);
|
|
1489
|
+
const totalFailed = summaries.reduce((sum, s) => sum + s.failed, 0);
|
|
1490
|
+
const totalSkipped = summaries.reduce((sum, s) => sum + s.skipped, 0);
|
|
1491
|
+
lines.push(`| Client | Passed | Failed | Skipped | Status |`);
|
|
1492
|
+
lines.push(`|--------|--------|--------|---------|--------|`);
|
|
1493
|
+
for (const summary of summaries) {
|
|
1494
|
+
const status = summary.failed === 0 ? `✓` : `✗`;
|
|
1495
|
+
lines.push(`| ${summary.adapter} | ${summary.passed} | ${summary.failed} | ${summary.skipped} | ${status} |`);
|
|
1496
|
+
}
|
|
1497
|
+
lines.push(`| **Total** | **${totalPassed}** | **${totalFailed}** | **${totalSkipped}** | ${totalFailed === 0 ? `✓` : `✗`} |`);
|
|
1498
|
+
lines.push(``);
|
|
1499
|
+
for (const summary of summaries) {
|
|
1500
|
+
lines.push(generateMarkdownReport(summary));
|
|
1501
|
+
lines.push(``);
|
|
1502
|
+
}
|
|
1503
|
+
return lines.join(`\n`);
|
|
1504
|
+
}
|
|
1425
1505
|
|
|
1426
1506
|
//#endregion
|
|
1427
|
-
export { allScenarios, countTests, filterByCategory, getScenarioById, getScenariosByCategory, loadEmbeddedTestSuites, loadTestSuites, runBenchmarks, runConformanceTests, scenariosByCategory };
|
|
1507
|
+
export { aggregateBenchmarkResults, allScenarios, countTests, filterByCategory, getScenarioById, getScenariosByCategory, loadEmbeddedTestSuites, loadTestSuites, runBenchmarks, runConformanceTests, scenariosByCategory };
|