@durable-streams/client-conformance-tests 0.1.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/README.md +451 -0
- package/dist/adapters/typescript-adapter.d.ts +1 -0
- package/dist/adapters/typescript-adapter.js +586 -0
- package/dist/benchmark-runner-C_Yghc8f.js +1333 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +265 -0
- package/dist/index.d.ts +508 -0
- package/dist/index.js +4 -0
- package/dist/protocol-DyEvTHPF.d.ts +472 -0
- package/dist/protocol-qb83AeUH.js +120 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +3 -0
- package/package.json +53 -0
- package/src/adapters/typescript-adapter.ts +848 -0
- package/src/benchmark-runner.ts +860 -0
- package/src/benchmark-scenarios.ts +311 -0
- package/src/cli.ts +294 -0
- package/src/index.ts +50 -0
- package/src/protocol.ts +656 -0
- package/src/runner.ts +1191 -0
- package/src/test-cases.ts +475 -0
- package/test-cases/consumer/cache-headers.yaml +150 -0
- package/test-cases/consumer/error-handling.yaml +108 -0
- package/test-cases/consumer/message-ordering.yaml +209 -0
- package/test-cases/consumer/offset-handling.yaml +209 -0
- package/test-cases/consumer/offset-resumption.yaml +197 -0
- package/test-cases/consumer/read-catchup.yaml +173 -0
- package/test-cases/consumer/read-longpoll.yaml +132 -0
- package/test-cases/consumer/read-sse.yaml +145 -0
- package/test-cases/consumer/retry-resilience.yaml +160 -0
- package/test-cases/consumer/streaming-equivalence.yaml +226 -0
- package/test-cases/lifecycle/dynamic-headers.yaml +147 -0
- package/test-cases/lifecycle/headers-params.yaml +117 -0
- package/test-cases/lifecycle/stream-lifecycle.yaml +148 -0
- package/test-cases/producer/append-data.yaml +142 -0
- package/test-cases/producer/batching.yaml +112 -0
- package/test-cases/producer/create-stream.yaml +87 -0
- package/test-cases/producer/error-handling.yaml +90 -0
- package/test-cases/producer/sequence-ordering.yaml +148 -0
package/README.md
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# @durable-streams/client-conformance-tests
|
|
2
|
+
|
|
3
|
+
Conformance test suite for Durable Streams client implementations (producer and consumer).
|
|
4
|
+
|
|
5
|
+
This package provides a comprehensive test suite to verify that a client correctly implements the [Durable Streams protocol](../../PROTOCOL.md) across any programming language.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
The conformance suite uses a **language-agnostic architecture** inspired by [ConnectRPC Conformance](https://github.com/connectrpc/conformance) and [AWS Smithy Protocol Tests](https://smithy.io/2.0/additional-specs/http-protocol-compliance-tests.html):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
13
|
+
│ Test Runner (Node.js) │
|
|
14
|
+
│ - Reads test cases from YAML │
|
|
15
|
+
│ - Manages reference server lifecycle │
|
|
16
|
+
│ - Orchestrates client adapter process │
|
|
17
|
+
│ - Compares results against expectations │
|
|
18
|
+
└────────────────────────┬────────────────────────────────────────┘
|
|
19
|
+
│ stdin/stdout (JSON lines)
|
|
20
|
+
▼
|
|
21
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
22
|
+
│ Client Adapter (any language) │
|
|
23
|
+
│ - Reads test commands from stdin │
|
|
24
|
+
│ - Uses native SDK to execute operations │
|
|
25
|
+
│ - Reports results to stdout │
|
|
26
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
27
|
+
│ HTTP
|
|
28
|
+
▼
|
|
29
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
30
|
+
│ Reference Server (TypeScript) │
|
|
31
|
+
│ - Full protocol compliance │
|
|
32
|
+
│ - Validates client behavior │
|
|
33
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @durable-streams/client-conformance-tests
|
|
40
|
+
# or
|
|
41
|
+
pnpm add @durable-streams/client-conformance-tests
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## CLI Usage
|
|
45
|
+
|
|
46
|
+
### Test the TypeScript Client
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx @durable-streams/client-conformance-tests --run ts
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Test a Custom Client Adapter
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Python client
|
|
56
|
+
npx @durable-streams/client-conformance-tests --run ./my-python-adapter.py
|
|
57
|
+
|
|
58
|
+
# Go client
|
|
59
|
+
npx @durable-streams/client-conformance-tests --run ./my-go-adapter
|
|
60
|
+
|
|
61
|
+
# Any executable
|
|
62
|
+
npx @durable-streams/client-conformance-tests --run /path/to/adapter
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### CLI Options
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Usage:
|
|
69
|
+
npx @durable-streams/client-conformance-tests --run <adapter> [options]
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
--suite <name> Run only specific suite(s): producer, consumer, lifecycle
|
|
73
|
+
--tag <name> Run only tests with specific tag(s)
|
|
74
|
+
--verbose Show detailed output for each operation
|
|
75
|
+
--fail-fast Stop on first test failure
|
|
76
|
+
--timeout <ms> Timeout for each test in milliseconds (default: 30000)
|
|
77
|
+
--port <port> Port for reference server (default: random)
|
|
78
|
+
--help, -h Show help message
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Examples
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Test only producer functionality
|
|
85
|
+
npx @durable-streams/client-conformance-tests --run ts --suite producer
|
|
86
|
+
|
|
87
|
+
# Test only consumer functionality
|
|
88
|
+
npx @durable-streams/client-conformance-tests --run ./python-client --suite consumer
|
|
89
|
+
|
|
90
|
+
# Test core functionality with verbose output
|
|
91
|
+
npx @durable-streams/client-conformance-tests --run ts --tag core --verbose
|
|
92
|
+
|
|
93
|
+
# Stop on first failure
|
|
94
|
+
npx @durable-streams/client-conformance-tests --run ts --fail-fast
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Programmatic Usage
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { runConformanceTests } from "@durable-streams/client-conformance-tests"
|
|
101
|
+
|
|
102
|
+
const summary = await runConformanceTests({
|
|
103
|
+
clientAdapter: "ts", // or path to your adapter
|
|
104
|
+
suites: ["producer", "consumer"],
|
|
105
|
+
verbose: true,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
console.log(`Passed: ${summary.passed}/${summary.total}`)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Implementing a Client Adapter
|
|
112
|
+
|
|
113
|
+
A client adapter is an executable that communicates with the test runner via stdin/stdout using a JSON-line protocol.
|
|
114
|
+
|
|
115
|
+
### Protocol Overview
|
|
116
|
+
|
|
117
|
+
1. Test runner starts your adapter as a subprocess
|
|
118
|
+
2. Runner sends JSON commands to stdin (one per line)
|
|
119
|
+
3. Adapter executes commands using your client SDK
|
|
120
|
+
4. Adapter sends JSON results to stdout (one per line)
|
|
121
|
+
|
|
122
|
+
### Commands and Results
|
|
123
|
+
|
|
124
|
+
#### Init Command (first command, always sent)
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
// Command (stdin)
|
|
128
|
+
{"type":"init","serverUrl":"http://localhost:3000"}
|
|
129
|
+
|
|
130
|
+
// Result (stdout)
|
|
131
|
+
{"type":"init","success":true,"clientName":"my-client","clientVersion":"1.0.0","features":{"batching":true,"sse":true,"longPoll":true}}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### Create Command
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
// Command
|
|
138
|
+
{"type":"create","path":"/my-stream","contentType":"text/plain"}
|
|
139
|
+
|
|
140
|
+
// Success Result
|
|
141
|
+
{"type":"create","success":true,"status":201,"offset":"0"}
|
|
142
|
+
|
|
143
|
+
// Error Result
|
|
144
|
+
{"type":"error","success":false,"commandType":"create","status":409,"errorCode":"CONFLICT","message":"Stream already exists"}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Append Command
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
// Command
|
|
151
|
+
{"type":"append","path":"/my-stream","data":"Hello, World!","seq":1}
|
|
152
|
+
|
|
153
|
+
// Success Result
|
|
154
|
+
{"type":"append","success":true,"status":200,"offset":"13"}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Read Command
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
// Command
|
|
161
|
+
{"type":"read","path":"/my-stream","offset":"0","live":"long-poll","timeoutMs":5000}
|
|
162
|
+
|
|
163
|
+
// Success Result
|
|
164
|
+
{"type":"read","success":true,"status":200,"chunks":[{"data":"Hello, World!","offset":"13"}],"offset":"13","upToDate":true}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Head Command
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
// Command
|
|
171
|
+
{"type":"head","path":"/my-stream"}
|
|
172
|
+
|
|
173
|
+
// Success Result
|
|
174
|
+
{"type":"head","success":true,"status":200,"offset":"13","contentType":"text/plain"}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Delete Command
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
// Command
|
|
181
|
+
{"type":"delete","path":"/my-stream"}
|
|
182
|
+
|
|
183
|
+
// Success Result
|
|
184
|
+
{"type":"delete","success":true,"status":200}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Shutdown Command
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
// Command
|
|
191
|
+
{"type":"shutdown"}
|
|
192
|
+
|
|
193
|
+
// Result
|
|
194
|
+
{"type":"shutdown","success":true}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Error Codes
|
|
198
|
+
|
|
199
|
+
Use these standard error codes in error results:
|
|
200
|
+
|
|
201
|
+
- `NETWORK_ERROR` - Network connection failed
|
|
202
|
+
- `TIMEOUT` - Operation timed out
|
|
203
|
+
- `CONFLICT` - Stream already exists (409)
|
|
204
|
+
- `NOT_FOUND` - Stream not found (404)
|
|
205
|
+
- `SEQUENCE_CONFLICT` - Sequence number conflict (409)
|
|
206
|
+
- `INVALID_OFFSET` - Invalid offset format
|
|
207
|
+
- `UNEXPECTED_STATUS` - Unexpected HTTP status
|
|
208
|
+
- `PARSE_ERROR` - Failed to parse response
|
|
209
|
+
- `INTERNAL_ERROR` - Client internal error
|
|
210
|
+
- `NOT_SUPPORTED` - Operation not supported
|
|
211
|
+
|
|
212
|
+
### Example: Python Adapter
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
#!/usr/bin/env python3
|
|
216
|
+
import sys
|
|
217
|
+
import json
|
|
218
|
+
from durable_streams import DurableStream, DurableStreamError
|
|
219
|
+
|
|
220
|
+
def main():
|
|
221
|
+
server_url = ""
|
|
222
|
+
|
|
223
|
+
for line in sys.stdin:
|
|
224
|
+
if not line.strip():
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
command = json.loads(line)
|
|
228
|
+
result = handle_command(command, server_url)
|
|
229
|
+
|
|
230
|
+
if command["type"] == "init":
|
|
231
|
+
server_url = command["serverUrl"]
|
|
232
|
+
|
|
233
|
+
print(json.dumps(result), flush=True)
|
|
234
|
+
|
|
235
|
+
if command["type"] == "shutdown":
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
def handle_command(cmd, server_url):
|
|
239
|
+
try:
|
|
240
|
+
if cmd["type"] == "init":
|
|
241
|
+
return {
|
|
242
|
+
"type": "init",
|
|
243
|
+
"success": True,
|
|
244
|
+
"clientName": "durable-streams-python",
|
|
245
|
+
"clientVersion": "0.1.0",
|
|
246
|
+
"features": {"batching": False, "sse": True, "longPoll": True}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
elif cmd["type"] == "create":
|
|
250
|
+
url = f"{server_url}{cmd['path']}"
|
|
251
|
+
stream = DurableStream.create(url, content_type=cmd.get("contentType"))
|
|
252
|
+
return {"type": "create", "success": True, "status": 201}
|
|
253
|
+
|
|
254
|
+
elif cmd["type"] == "append":
|
|
255
|
+
url = f"{server_url}{cmd['path']}"
|
|
256
|
+
stream = DurableStream(url)
|
|
257
|
+
stream.append(cmd["data"], seq=cmd.get("seq"))
|
|
258
|
+
return {"type": "append", "success": True, "status": 200}
|
|
259
|
+
|
|
260
|
+
elif cmd["type"] == "read":
|
|
261
|
+
url = f"{server_url}{cmd['path']}"
|
|
262
|
+
# ... implement read logic
|
|
263
|
+
return {"type": "read", "success": True, "status": 200, "chunks": [], "upToDate": True}
|
|
264
|
+
|
|
265
|
+
elif cmd["type"] == "head":
|
|
266
|
+
url = f"{server_url}{cmd['path']}"
|
|
267
|
+
result = DurableStream.head(url)
|
|
268
|
+
return {"type": "head", "success": True, "status": 200, "offset": result.offset}
|
|
269
|
+
|
|
270
|
+
elif cmd["type"] == "delete":
|
|
271
|
+
url = f"{server_url}{cmd['path']}"
|
|
272
|
+
DurableStream.delete(url)
|
|
273
|
+
return {"type": "delete", "success": True, "status": 200}
|
|
274
|
+
|
|
275
|
+
elif cmd["type"] == "shutdown":
|
|
276
|
+
return {"type": "shutdown", "success": True}
|
|
277
|
+
|
|
278
|
+
except DurableStreamError as e:
|
|
279
|
+
return {
|
|
280
|
+
"type": "error",
|
|
281
|
+
"success": False,
|
|
282
|
+
"commandType": cmd["type"],
|
|
283
|
+
"errorCode": map_error_code(e),
|
|
284
|
+
"message": str(e)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
def map_error_code(error):
|
|
288
|
+
# Map your client's error types to standard codes
|
|
289
|
+
if error.status == 404:
|
|
290
|
+
return "NOT_FOUND"
|
|
291
|
+
elif error.status == 409:
|
|
292
|
+
return "CONFLICT"
|
|
293
|
+
return "INTERNAL_ERROR"
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Example: Go Adapter
|
|
300
|
+
|
|
301
|
+
```go
|
|
302
|
+
package main
|
|
303
|
+
|
|
304
|
+
import (
|
|
305
|
+
"bufio"
|
|
306
|
+
"encoding/json"
|
|
307
|
+
"fmt"
|
|
308
|
+
"os"
|
|
309
|
+
|
|
310
|
+
durable "github.com/durable-streams/go-client"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
type Command struct {
|
|
314
|
+
Type string `json:"type"`
|
|
315
|
+
ServerURL string `json:"serverUrl,omitempty"`
|
|
316
|
+
Path string `json:"path,omitempty"`
|
|
317
|
+
Data string `json:"data,omitempty"`
|
|
318
|
+
// ... other fields
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
type Result struct {
|
|
322
|
+
Type string `json:"type"`
|
|
323
|
+
Success bool `json:"success"`
|
|
324
|
+
Status int `json:"status,omitempty"`
|
|
325
|
+
// ... other fields
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func main() {
|
|
329
|
+
scanner := bufio.NewScanner(os.Stdin)
|
|
330
|
+
var serverURL string
|
|
331
|
+
|
|
332
|
+
for scanner.Scan() {
|
|
333
|
+
line := scanner.Text()
|
|
334
|
+
if line == "" {
|
|
335
|
+
continue
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
var cmd Command
|
|
339
|
+
json.Unmarshal([]byte(line), &cmd)
|
|
340
|
+
|
|
341
|
+
result := handleCommand(cmd, serverURL)
|
|
342
|
+
|
|
343
|
+
if cmd.Type == "init" {
|
|
344
|
+
serverURL = cmd.ServerURL
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
output, _ := json.Marshal(result)
|
|
348
|
+
fmt.Println(string(output))
|
|
349
|
+
|
|
350
|
+
if cmd.Type == "shutdown" {
|
|
351
|
+
break
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
func handleCommand(cmd Command, serverURL string) Result {
|
|
357
|
+
switch cmd.Type {
|
|
358
|
+
case "init":
|
|
359
|
+
return Result{
|
|
360
|
+
Type: "init",
|
|
361
|
+
Success: true,
|
|
362
|
+
// ... client info
|
|
363
|
+
}
|
|
364
|
+
case "create":
|
|
365
|
+
// Use your Go client SDK
|
|
366
|
+
return Result{Type: "create", Success: true, Status: 201}
|
|
367
|
+
// ... handle other commands
|
|
368
|
+
}
|
|
369
|
+
return Result{Type: "error", Success: false}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Test Coverage
|
|
374
|
+
|
|
375
|
+
The conformance test suite covers:
|
|
376
|
+
|
|
377
|
+
### Producer Tests
|
|
378
|
+
|
|
379
|
+
- **Stream Creation** - Create, idempotency, content types, TTL
|
|
380
|
+
- **Append Operations** - String/binary data, unicode, large payloads
|
|
381
|
+
- **Sequence Ordering** - Monotonic sequences, conflict detection
|
|
382
|
+
- **Batching** - Concurrent appends, order preservation
|
|
383
|
+
- **Error Handling** - 404s, 409s, network errors
|
|
384
|
+
|
|
385
|
+
### Consumer Tests
|
|
386
|
+
|
|
387
|
+
- **Catch-up Reads** - Empty/full streams, offset resumption
|
|
388
|
+
- **Long-Poll** - Waiting for data, timeouts
|
|
389
|
+
- **SSE Mode** - Event streaming, reconnection
|
|
390
|
+
- **Offset Handling** - Monotonicity, byte-exactness
|
|
391
|
+
- **Error Handling** - Invalid offsets, deleted streams
|
|
392
|
+
|
|
393
|
+
### Lifecycle Tests
|
|
394
|
+
|
|
395
|
+
- **Full Lifecycle** - Create, append, read, delete
|
|
396
|
+
- **Headers/Params** - Custom headers, auth tokens
|
|
397
|
+
- **Metadata** - HEAD requests, content types
|
|
398
|
+
|
|
399
|
+
## Adding New Test Cases
|
|
400
|
+
|
|
401
|
+
Test cases are defined in YAML files in the `test-cases/` directory:
|
|
402
|
+
|
|
403
|
+
```yaml
|
|
404
|
+
id: my-new-tests
|
|
405
|
+
name: My New Tests
|
|
406
|
+
description: Tests for new functionality
|
|
407
|
+
category: producer # or consumer, lifecycle
|
|
408
|
+
tags:
|
|
409
|
+
- core
|
|
410
|
+
- custom
|
|
411
|
+
|
|
412
|
+
tests:
|
|
413
|
+
- id: my-test
|
|
414
|
+
name: My test case
|
|
415
|
+
description: What this test verifies
|
|
416
|
+
setup:
|
|
417
|
+
- action: create
|
|
418
|
+
as: streamPath
|
|
419
|
+
operations:
|
|
420
|
+
- action: append
|
|
421
|
+
path: ${streamPath}
|
|
422
|
+
data: "test data"
|
|
423
|
+
expect:
|
|
424
|
+
status: 200
|
|
425
|
+
- action: read
|
|
426
|
+
path: ${streamPath}
|
|
427
|
+
expect:
|
|
428
|
+
data: "test data"
|
|
429
|
+
upToDate: true
|
|
430
|
+
cleanup:
|
|
431
|
+
- action: delete
|
|
432
|
+
path: ${streamPath}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
## Protocol Types
|
|
436
|
+
|
|
437
|
+
For TypeScript/JavaScript adapters, you can import the protocol types:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import {
|
|
441
|
+
type TestCommand,
|
|
442
|
+
type TestResult,
|
|
443
|
+
parseCommand,
|
|
444
|
+
serializeResult,
|
|
445
|
+
ErrorCodes,
|
|
446
|
+
} from "@durable-streams/client-conformance-tests/protocol"
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## License
|
|
450
|
+
|
|
451
|
+
Apache 2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|