@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { runConformanceTests } from "./src-DRIMnUPk.js";
|
|
2
|
+
|
|
3
|
+
//#region src/test-runner.ts
|
|
4
|
+
const baseUrl = process.env.CONFORMANCE_TEST_URL;
|
|
5
|
+
if (!baseUrl) throw new Error("CONFORMANCE_TEST_URL environment variable is required. Use the CLI: npx @durable-streams/server-conformance-tests --run <url>");
|
|
6
|
+
runConformanceTests({ baseUrl });
|
|
7
|
+
|
|
8
|
+
//#endregion
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@durable-streams/server-conformance-tests",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Conformance test suite for Durable Streams server implementations",
|
|
5
|
+
"author": "Durable Stream contributors",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"durable-streams-server-conformance": "./dist/cli.js",
|
|
12
|
+
"durable-streams-server-conformance-dev": "./bin/conformance-dev.mjs"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsdown",
|
|
22
|
+
"dev": "tsdown --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@durable-streams/client": "workspace:*",
|
|
27
|
+
"fast-check": "^4.4.0",
|
|
28
|
+
"vitest": "^3.2.4"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"tsdown": "^0.9.0",
|
|
32
|
+
"tsx": "^4.19.2",
|
|
33
|
+
"typescript": "^5.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src",
|
|
38
|
+
"bin"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI for running Durable Streams conformance tests
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx @durable-streams/server-conformance-tests --run http://localhost:4473
|
|
8
|
+
* npx @durable-streams/server-conformance-tests --watch src http://localhost:4473
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from "node:child_process"
|
|
12
|
+
import { existsSync, watch } from "node:fs"
|
|
13
|
+
import { dirname, join, resolve } from "node:path"
|
|
14
|
+
import { fileURLToPath } from "node:url"
|
|
15
|
+
import type { ChildProcess } from "node:child_process"
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
|
|
19
|
+
interface ParsedArgs {
|
|
20
|
+
mode: `run` | `watch`
|
|
21
|
+
watchPaths: Array<string>
|
|
22
|
+
baseUrl: string
|
|
23
|
+
help: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function printUsage() {
|
|
27
|
+
console.log(`
|
|
28
|
+
Durable Streams Conformance Test Runner
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
npx @durable-streams/server-conformance-tests --run <url>
|
|
32
|
+
npx @durable-streams/server-conformance-tests --watch <path> [path...] <url>
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--run Run tests once and exit (for CI)
|
|
36
|
+
--watch <paths> Watch source paths and rerun tests on changes (for development)
|
|
37
|
+
--help, -h Show this help message
|
|
38
|
+
|
|
39
|
+
Arguments:
|
|
40
|
+
<url> Base URL of the Durable Streams server to test against
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
# Run tests once in CI
|
|
44
|
+
npx @durable-streams/server-conformance-tests --run http://localhost:4473
|
|
45
|
+
|
|
46
|
+
# Watch src directory and rerun tests on changes
|
|
47
|
+
npx @durable-streams/server-conformance-tests --watch src http://localhost:4473
|
|
48
|
+
|
|
49
|
+
# Watch multiple directories
|
|
50
|
+
npx @durable-streams/server-conformance-tests --watch src lib http://localhost:4473
|
|
51
|
+
`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(args: Array<string>): ParsedArgs {
|
|
55
|
+
const result: ParsedArgs = {
|
|
56
|
+
mode: `run`,
|
|
57
|
+
watchPaths: [],
|
|
58
|
+
baseUrl: ``,
|
|
59
|
+
help: false,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let i = 0
|
|
63
|
+
while (i < args.length) {
|
|
64
|
+
const arg = args[i]!
|
|
65
|
+
|
|
66
|
+
if (arg === `--help` || arg === `-h`) {
|
|
67
|
+
result.help = true
|
|
68
|
+
return result
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (arg === `--run`) {
|
|
72
|
+
result.mode = `run`
|
|
73
|
+
i++
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (arg === `--watch`) {
|
|
78
|
+
result.mode = `watch`
|
|
79
|
+
i++
|
|
80
|
+
// Collect all paths until we hit another flag or the last argument (url)
|
|
81
|
+
while (i < args.length - 1) {
|
|
82
|
+
const next = args[i]!
|
|
83
|
+
if (next.startsWith(`--`) || next.startsWith(`-`)) {
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
result.watchPaths.push(next)
|
|
87
|
+
i++
|
|
88
|
+
}
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Last non-flag argument is the URL
|
|
93
|
+
if (!arg.startsWith(`-`)) {
|
|
94
|
+
result.baseUrl = arg
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
i++
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateArgs(args: ParsedArgs): string | null {
|
|
104
|
+
if (!args.baseUrl) {
|
|
105
|
+
return `Error: Base URL is required`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
new URL(args.baseUrl)
|
|
110
|
+
} catch {
|
|
111
|
+
return `Error: Invalid URL "${args.baseUrl}"`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (args.mode === `watch` && args.watchPaths.length === 0) {
|
|
115
|
+
return `Error: --watch requires at least one path to watch`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Get the path to the test runner file
|
|
122
|
+
function getTestRunnerPath(): string {
|
|
123
|
+
const runnerInDist = join(__dirname, `test-runner.js`)
|
|
124
|
+
const runnerInSrc = join(__dirname, `test-runner.ts`)
|
|
125
|
+
|
|
126
|
+
// In production (dist), use the compiled JS
|
|
127
|
+
// In development (with tsx), use TS directly
|
|
128
|
+
if (existsSync(runnerInDist)) {
|
|
129
|
+
return runnerInDist
|
|
130
|
+
}
|
|
131
|
+
return runnerInSrc
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function runTests(baseUrl: string): Promise<number> {
|
|
135
|
+
return new Promise((resolvePromise) => {
|
|
136
|
+
const runnerPath = getTestRunnerPath()
|
|
137
|
+
|
|
138
|
+
// Find vitest binary
|
|
139
|
+
const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`)
|
|
140
|
+
const vitestBinAlt = join(
|
|
141
|
+
__dirname,
|
|
142
|
+
`..`,
|
|
143
|
+
`..`,
|
|
144
|
+
`..`,
|
|
145
|
+
`node_modules`,
|
|
146
|
+
`.bin`,
|
|
147
|
+
`vitest`
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
let vitestPath = `vitest`
|
|
151
|
+
if (existsSync(vitestBin)) {
|
|
152
|
+
vitestPath = vitestBin
|
|
153
|
+
} else if (existsSync(vitestBinAlt)) {
|
|
154
|
+
vitestPath = vitestBinAlt
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const args = [
|
|
158
|
+
`run`,
|
|
159
|
+
runnerPath,
|
|
160
|
+
`--no-coverage`,
|
|
161
|
+
`--reporter=default`,
|
|
162
|
+
`--passWithNoTests=false`,
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
const child = spawn(vitestPath, args, {
|
|
166
|
+
stdio: `inherit`,
|
|
167
|
+
env: {
|
|
168
|
+
...process.env,
|
|
169
|
+
CONFORMANCE_TEST_URL: baseUrl,
|
|
170
|
+
FORCE_COLOR: `1`,
|
|
171
|
+
},
|
|
172
|
+
shell: true,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
child.on(`close`, (code) => {
|
|
176
|
+
resolvePromise(code ?? 1)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
child.on(`error`, (err) => {
|
|
180
|
+
console.error(`Failed to run tests: ${err.message}`)
|
|
181
|
+
resolvePromise(1)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function runOnce(baseUrl: string): Promise<void> {
|
|
187
|
+
console.log(`Running conformance tests against ${baseUrl}\n`)
|
|
188
|
+
const exitCode = await runTests(baseUrl)
|
|
189
|
+
process.exit(exitCode)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function runWatch(
|
|
193
|
+
baseUrl: string,
|
|
194
|
+
watchPaths: Array<string>
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
let runningProcess: ChildProcess | null = null
|
|
197
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
198
|
+
const DEBOUNCE_MS = 300
|
|
199
|
+
|
|
200
|
+
const spawnTests = (): ChildProcess => {
|
|
201
|
+
const runnerPath = getTestRunnerPath()
|
|
202
|
+
|
|
203
|
+
// Find vitest binary
|
|
204
|
+
const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`)
|
|
205
|
+
const vitestBinAlt = join(
|
|
206
|
+
__dirname,
|
|
207
|
+
`..`,
|
|
208
|
+
`..`,
|
|
209
|
+
`..`,
|
|
210
|
+
`node_modules`,
|
|
211
|
+
`.bin`,
|
|
212
|
+
`vitest`
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
let vitestPath = `vitest`
|
|
216
|
+
if (existsSync(vitestBin)) {
|
|
217
|
+
vitestPath = vitestBin
|
|
218
|
+
} else if (existsSync(vitestBinAlt)) {
|
|
219
|
+
vitestPath = vitestBinAlt
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const args = [
|
|
223
|
+
`run`,
|
|
224
|
+
runnerPath,
|
|
225
|
+
`--no-coverage`,
|
|
226
|
+
`--reporter=default`,
|
|
227
|
+
`--passWithNoTests=false`,
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
return spawn(vitestPath, args, {
|
|
231
|
+
stdio: `inherit`,
|
|
232
|
+
env: {
|
|
233
|
+
...process.env,
|
|
234
|
+
CONFORMANCE_TEST_URL: baseUrl,
|
|
235
|
+
FORCE_COLOR: `1`,
|
|
236
|
+
},
|
|
237
|
+
shell: true,
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const runTestsDebounced = () => {
|
|
242
|
+
if (debounceTimer) {
|
|
243
|
+
clearTimeout(debounceTimer)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
debounceTimer = setTimeout(() => {
|
|
247
|
+
// Kill any running test process
|
|
248
|
+
if (runningProcess) {
|
|
249
|
+
runningProcess.kill(`SIGTERM`)
|
|
250
|
+
runningProcess = null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.clear()
|
|
254
|
+
console.log(`Running conformance tests against ${baseUrl}\n`)
|
|
255
|
+
|
|
256
|
+
runningProcess = spawnTests()
|
|
257
|
+
|
|
258
|
+
runningProcess.on(`close`, (code) => {
|
|
259
|
+
if (code === 0) {
|
|
260
|
+
console.log(`\nAll tests passed`)
|
|
261
|
+
} else {
|
|
262
|
+
console.log(`\nTests failed (exit code: ${code})`)
|
|
263
|
+
}
|
|
264
|
+
console.log(`\nWatching for changes in: ${watchPaths.join(`, `)}`)
|
|
265
|
+
console.log(`Press Ctrl+C to exit\n`)
|
|
266
|
+
runningProcess = null
|
|
267
|
+
})
|
|
268
|
+
}, DEBOUNCE_MS)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Set up file watchers
|
|
272
|
+
const watchers: Array<ReturnType<typeof watch>> = []
|
|
273
|
+
|
|
274
|
+
for (const watchPath of watchPaths) {
|
|
275
|
+
const absPath = resolve(process.cwd(), watchPath)
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const watcher = watch(
|
|
279
|
+
absPath,
|
|
280
|
+
{ recursive: true },
|
|
281
|
+
(eventType, filename) => {
|
|
282
|
+
if (filename && !filename.includes(`node_modules`)) {
|
|
283
|
+
console.log(`\nChange detected: ${filename}`)
|
|
284
|
+
runTestsDebounced()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
watchers.push(watcher)
|
|
290
|
+
console.log(`Watching: ${absPath}`)
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(
|
|
293
|
+
`Warning: Could not watch "${watchPath}": ${(err as Error).message}`
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (watchers.length === 0) {
|
|
299
|
+
console.error(`Error: No valid paths to watch`)
|
|
300
|
+
process.exit(1)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Handle cleanup
|
|
304
|
+
process.on(`SIGINT`, () => {
|
|
305
|
+
console.log(`\n\nStopping watch mode...`)
|
|
306
|
+
watchers.forEach((w) => w.close())
|
|
307
|
+
if (runningProcess) {
|
|
308
|
+
runningProcess.kill(`SIGTERM`)
|
|
309
|
+
}
|
|
310
|
+
process.exit(0)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// Run tests initially
|
|
314
|
+
runTestsDebounced()
|
|
315
|
+
|
|
316
|
+
// Keep the process running
|
|
317
|
+
await new Promise(() => {})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function main() {
|
|
321
|
+
const args = parseArgs(process.argv.slice(2))
|
|
322
|
+
|
|
323
|
+
if (args.help) {
|
|
324
|
+
printUsage()
|
|
325
|
+
process.exit(0)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const error = validateArgs(args)
|
|
329
|
+
if (error) {
|
|
330
|
+
console.error(error)
|
|
331
|
+
console.error(`\nRun with --help for usage information`)
|
|
332
|
+
process.exit(1)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (args.mode === `watch`) {
|
|
336
|
+
await runWatch(args.baseUrl, args.watchPaths)
|
|
337
|
+
} else {
|
|
338
|
+
await runOnce(args.baseUrl)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
main().catch((err) => {
|
|
343
|
+
console.error(`Fatal error: ${err.message}`)
|
|
344
|
+
process.exit(1)
|
|
345
|
+
})
|