@durable-streams/server-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 +132 -0
- package/bin/conformance-dev.mjs +27 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +221 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +3 -0
- package/dist/src-DRIMnUPk.js +2326 -0
- package/dist/test-runner.d.ts +1 -0
- package/dist/test-runner.js +8 -0
- package/package.json +43 -0
- package/src/cli.ts +345 -0
- package/src/index.ts +3596 -0
- package/src/test-runner.ts +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# @durable-streams/server-conformance-tests
|
|
2
|
+
|
|
3
|
+
Protocol compliance test suite for Durable Streams server implementations.
|
|
4
|
+
|
|
5
|
+
This package provides a comprehensive test suite to verify that a server correctly implements the [Durable Streams protocol](../../PROTOCOL.md).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @durable-streams/server-conformance-tests
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @durable-streams/server-conformance-tests
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## CLI Usage
|
|
16
|
+
|
|
17
|
+
The easiest way to run conformance tests against your server:
|
|
18
|
+
|
|
19
|
+
### Run Once (CI)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @durable-streams/server-conformance-tests --run http://localhost:4437
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Watch Mode (Development)
|
|
26
|
+
|
|
27
|
+
Watch source files and automatically rerun tests when changes are detected:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx @durable-streams/server-conformance-tests --watch src http://localhost:4437
|
|
31
|
+
|
|
32
|
+
# Watch multiple directories
|
|
33
|
+
npx @durable-streams/server-conformance-tests --watch src lib http://localhost:4437
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### CLI Options
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Usage:
|
|
40
|
+
npx @durable-streams/server-conformance-tests --run <url>
|
|
41
|
+
npx @durable-streams/server-conformance-tests --watch <path> [path...] <url>
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--run Run tests once and exit (for CI)
|
|
45
|
+
--watch <paths> Watch source paths and rerun tests on changes (for development)
|
|
46
|
+
--help, -h Show help message
|
|
47
|
+
|
|
48
|
+
Arguments:
|
|
49
|
+
<url> Base URL of the Durable Streams server to test against
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Programmatic Usage
|
|
53
|
+
|
|
54
|
+
You can also run the conformance tests programmatically within your own test suite:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { runConformanceTests } from "@durable-streams/server-conformance-tests"
|
|
58
|
+
|
|
59
|
+
// In your test file (e.g., with vitest)
|
|
60
|
+
describe("My Server Implementation", () => {
|
|
61
|
+
const config = { baseUrl: "" }
|
|
62
|
+
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
// Start your server
|
|
65
|
+
const server = await startMyServer({ port: 0 })
|
|
66
|
+
config.baseUrl = server.url
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
afterAll(async () => {
|
|
70
|
+
await server.stop()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Run all conformance tests
|
|
74
|
+
runConformanceTests(config)
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## CI Integration
|
|
79
|
+
|
|
80
|
+
Add conformance tests to your CI pipeline:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
# GitHub Actions example
|
|
84
|
+
jobs:
|
|
85
|
+
conformance:
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
- uses: actions/setup-node@v4
|
|
90
|
+
with:
|
|
91
|
+
node-version: "20"
|
|
92
|
+
|
|
93
|
+
- name: Install dependencies
|
|
94
|
+
run: npm install
|
|
95
|
+
|
|
96
|
+
- name: Start server
|
|
97
|
+
run: npm run start:server &
|
|
98
|
+
|
|
99
|
+
- name: Wait for server
|
|
100
|
+
run: npx wait-on http://localhost:4437
|
|
101
|
+
|
|
102
|
+
- name: Run conformance tests
|
|
103
|
+
run: npx @durable-streams/server-conformance-tests --run http://localhost:4437
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Test Coverage
|
|
107
|
+
|
|
108
|
+
The conformance test suite covers:
|
|
109
|
+
|
|
110
|
+
- **Basic Stream Operations** - Create, delete, idempotent operations
|
|
111
|
+
- **Append Operations** - String data, chunking, sequence ordering
|
|
112
|
+
- **Read Operations** - Empty/full streams, offset reads
|
|
113
|
+
- **Long-Poll Operations** - Data waiting, immediate returns
|
|
114
|
+
- **HTTP Protocol** - Headers, status codes, content negotiation
|
|
115
|
+
- **TTL and Expiry** - TTL/Expires-At handling
|
|
116
|
+
- **Case-Insensitivity** - Content-type, header casing
|
|
117
|
+
- **Content-Type Validation** - Match enforcement
|
|
118
|
+
- **HEAD Metadata** - Metadata-only responses
|
|
119
|
+
- **Offset Validation** - Malformed offsets, resumable reads
|
|
120
|
+
- **Protocol Edge Cases** - Empty bodies, binary data, monotonic progression
|
|
121
|
+
- **Byte-Exactness** - Data integrity guarantees
|
|
122
|
+
- **Caching and ETag** - ETag and 304 responses
|
|
123
|
+
- **Chunking and Large Payloads** - Pagination, large files
|
|
124
|
+
- **Property-Based Fuzzing** - Random append/read sequences
|
|
125
|
+
- **Malformed Input Fuzzing** - Security-focused tests
|
|
126
|
+
- **Read-Your-Writes Consistency** - Immediate visibility after writes
|
|
127
|
+
- **SSE Mode** - Server-sent events streaming
|
|
128
|
+
- **JSON Mode** - JSON serialization and batching
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
Apache 2.0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/* eslint-disable stylistic/quotes */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Development wrapper that uses tsx to run the TypeScript source directly.
|
|
7
|
+
* This allows you to use `pnpm link --global` and see changes immediately
|
|
8
|
+
* without rebuilding.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from "node:child_process"
|
|
12
|
+
import { fileURLToPath } from "node:url"
|
|
13
|
+
import { dirname, join } from "node:path"
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
16
|
+
const __dirname = dirname(__filename)
|
|
17
|
+
|
|
18
|
+
const srcPath = join(__dirname, "..", "src", "cli.ts")
|
|
19
|
+
|
|
20
|
+
// Run tsx with the source file
|
|
21
|
+
const child = spawn("tsx", [srcPath, ...process.argv.slice(2)], {
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
child.on("exit", (code) => {
|
|
26
|
+
process.exit(code ?? 0)
|
|
27
|
+
})
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, watch } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
//#region src/cli.ts
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
function printUsage() {
|
|
10
|
+
console.log(`
|
|
11
|
+
Durable Streams Conformance Test Runner
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
npx @durable-streams/server-conformance-tests --run <url>
|
|
15
|
+
npx @durable-streams/server-conformance-tests --watch <path> [path...] <url>
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--run Run tests once and exit (for CI)
|
|
19
|
+
--watch <paths> Watch source paths and rerun tests on changes (for development)
|
|
20
|
+
--help, -h Show this help message
|
|
21
|
+
|
|
22
|
+
Arguments:
|
|
23
|
+
<url> Base URL of the Durable Streams server to test against
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
# Run tests once in CI
|
|
27
|
+
npx @durable-streams/server-conformance-tests --run http://localhost:4473
|
|
28
|
+
|
|
29
|
+
# Watch src directory and rerun tests on changes
|
|
30
|
+
npx @durable-streams/server-conformance-tests --watch src http://localhost:4473
|
|
31
|
+
|
|
32
|
+
# Watch multiple directories
|
|
33
|
+
npx @durable-streams/server-conformance-tests --watch src lib http://localhost:4473
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
function parseArgs(args) {
|
|
37
|
+
const result = {
|
|
38
|
+
mode: `run`,
|
|
39
|
+
watchPaths: [],
|
|
40
|
+
baseUrl: ``,
|
|
41
|
+
help: false
|
|
42
|
+
};
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < args.length) {
|
|
45
|
+
const arg = args[i];
|
|
46
|
+
if (arg === `--help` || arg === `-h`) {
|
|
47
|
+
result.help = true;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
if (arg === `--run`) {
|
|
51
|
+
result.mode = `run`;
|
|
52
|
+
i++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === `--watch`) {
|
|
56
|
+
result.mode = `watch`;
|
|
57
|
+
i++;
|
|
58
|
+
while (i < args.length - 1) {
|
|
59
|
+
const next = args[i];
|
|
60
|
+
if (next.startsWith(`--`) || next.startsWith(`-`)) break;
|
|
61
|
+
result.watchPaths.push(next);
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!arg.startsWith(`-`)) result.baseUrl = arg;
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function validateArgs(args) {
|
|
72
|
+
if (!args.baseUrl) return `Error: Base URL is required`;
|
|
73
|
+
try {
|
|
74
|
+
new URL(args.baseUrl);
|
|
75
|
+
} catch {
|
|
76
|
+
return `Error: Invalid URL "${args.baseUrl}"`;
|
|
77
|
+
}
|
|
78
|
+
if (args.mode === `watch` && args.watchPaths.length === 0) return `Error: --watch requires at least one path to watch`;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function getTestRunnerPath() {
|
|
82
|
+
const runnerInDist = join(__dirname, `test-runner.js`);
|
|
83
|
+
const runnerInSrc = join(__dirname, `test-runner.ts`);
|
|
84
|
+
if (existsSync(runnerInDist)) return runnerInDist;
|
|
85
|
+
return runnerInSrc;
|
|
86
|
+
}
|
|
87
|
+
function runTests(baseUrl) {
|
|
88
|
+
return new Promise((resolvePromise) => {
|
|
89
|
+
const runnerPath = getTestRunnerPath();
|
|
90
|
+
const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`);
|
|
91
|
+
const vitestBinAlt = join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
|
|
92
|
+
let vitestPath = `vitest`;
|
|
93
|
+
if (existsSync(vitestBin)) vitestPath = vitestBin;
|
|
94
|
+
else if (existsSync(vitestBinAlt)) vitestPath = vitestBinAlt;
|
|
95
|
+
const args = [
|
|
96
|
+
`run`,
|
|
97
|
+
runnerPath,
|
|
98
|
+
`--no-coverage`,
|
|
99
|
+
`--reporter=default`,
|
|
100
|
+
`--passWithNoTests=false`
|
|
101
|
+
];
|
|
102
|
+
const child = spawn(vitestPath, args, {
|
|
103
|
+
stdio: `inherit`,
|
|
104
|
+
env: {
|
|
105
|
+
...process.env,
|
|
106
|
+
CONFORMANCE_TEST_URL: baseUrl,
|
|
107
|
+
FORCE_COLOR: `1`
|
|
108
|
+
},
|
|
109
|
+
shell: true
|
|
110
|
+
});
|
|
111
|
+
child.on(`close`, (code) => {
|
|
112
|
+
resolvePromise(code ?? 1);
|
|
113
|
+
});
|
|
114
|
+
child.on(`error`, (err) => {
|
|
115
|
+
console.error(`Failed to run tests: ${err.message}`);
|
|
116
|
+
resolvePromise(1);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function runOnce(baseUrl) {
|
|
121
|
+
console.log(`Running conformance tests against ${baseUrl}\n`);
|
|
122
|
+
const exitCode = await runTests(baseUrl);
|
|
123
|
+
process.exit(exitCode);
|
|
124
|
+
}
|
|
125
|
+
async function runWatch(baseUrl, watchPaths) {
|
|
126
|
+
let runningProcess = null;
|
|
127
|
+
let debounceTimer = null;
|
|
128
|
+
const DEBOUNCE_MS = 300;
|
|
129
|
+
const spawnTests = () => {
|
|
130
|
+
const runnerPath = getTestRunnerPath();
|
|
131
|
+
const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`);
|
|
132
|
+
const vitestBinAlt = join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
|
|
133
|
+
let vitestPath = `vitest`;
|
|
134
|
+
if (existsSync(vitestBin)) vitestPath = vitestBin;
|
|
135
|
+
else if (existsSync(vitestBinAlt)) vitestPath = vitestBinAlt;
|
|
136
|
+
const args = [
|
|
137
|
+
`run`,
|
|
138
|
+
runnerPath,
|
|
139
|
+
`--no-coverage`,
|
|
140
|
+
`--reporter=default`,
|
|
141
|
+
`--passWithNoTests=false`
|
|
142
|
+
];
|
|
143
|
+
return spawn(vitestPath, args, {
|
|
144
|
+
stdio: `inherit`,
|
|
145
|
+
env: {
|
|
146
|
+
...process.env,
|
|
147
|
+
CONFORMANCE_TEST_URL: baseUrl,
|
|
148
|
+
FORCE_COLOR: `1`
|
|
149
|
+
},
|
|
150
|
+
shell: true
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const runTestsDebounced = () => {
|
|
154
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
155
|
+
debounceTimer = setTimeout(() => {
|
|
156
|
+
if (runningProcess) {
|
|
157
|
+
runningProcess.kill(`SIGTERM`);
|
|
158
|
+
runningProcess = null;
|
|
159
|
+
}
|
|
160
|
+
console.clear();
|
|
161
|
+
console.log(`Running conformance tests against ${baseUrl}\n`);
|
|
162
|
+
runningProcess = spawnTests();
|
|
163
|
+
runningProcess.on(`close`, (code) => {
|
|
164
|
+
if (code === 0) console.log(`\nAll tests passed`);
|
|
165
|
+
else console.log(`\nTests failed (exit code: ${code})`);
|
|
166
|
+
console.log(`\nWatching for changes in: ${watchPaths.join(`, `)}`);
|
|
167
|
+
console.log(`Press Ctrl+C to exit\n`);
|
|
168
|
+
runningProcess = null;
|
|
169
|
+
});
|
|
170
|
+
}, DEBOUNCE_MS);
|
|
171
|
+
};
|
|
172
|
+
const watchers = [];
|
|
173
|
+
for (const watchPath of watchPaths) {
|
|
174
|
+
const absPath = resolve(process.cwd(), watchPath);
|
|
175
|
+
try {
|
|
176
|
+
const watcher = watch(absPath, { recursive: true }, (eventType, filename) => {
|
|
177
|
+
if (filename && !filename.includes(`node_modules`)) {
|
|
178
|
+
console.log(`\nChange detected: ${filename}`);
|
|
179
|
+
runTestsDebounced();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
watchers.push(watcher);
|
|
183
|
+
console.log(`Watching: ${absPath}`);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(`Warning: Could not watch "${watchPath}": ${err.message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (watchers.length === 0) {
|
|
189
|
+
console.error(`Error: No valid paths to watch`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
process.on(`SIGINT`, () => {
|
|
193
|
+
console.log(`\n\nStopping watch mode...`);
|
|
194
|
+
watchers.forEach((w) => w.close());
|
|
195
|
+
if (runningProcess) runningProcess.kill(`SIGTERM`);
|
|
196
|
+
process.exit(0);
|
|
197
|
+
});
|
|
198
|
+
runTestsDebounced();
|
|
199
|
+
await new Promise(() => {});
|
|
200
|
+
}
|
|
201
|
+
async function main() {
|
|
202
|
+
const args = parseArgs(process.argv.slice(2));
|
|
203
|
+
if (args.help) {
|
|
204
|
+
printUsage();
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
const error = validateArgs(args);
|
|
208
|
+
if (error) {
|
|
209
|
+
console.error(error);
|
|
210
|
+
console.error(`\nRun with --help for usage information`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
if (args.mode === `watch`) await runWatch(args.baseUrl, args.watchPaths);
|
|
214
|
+
else await runOnce(args.baseUrl);
|
|
215
|
+
}
|
|
216
|
+
main().catch((err) => {
|
|
217
|
+
console.error(`Fatal error: ${err.message}`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
//#endregion
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
interface ConformanceTestOptions {
|
|
3
|
+
/** Base URL of the server to test */
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Run the full conformance test suite against a server
|
|
8
|
+
*/
|
|
9
|
+
declare function runConformanceTests(options: ConformanceTestOptions): void; //#endregion
|
|
10
|
+
export { ConformanceTestOptions, runConformanceTests };
|
package/dist/index.js
ADDED