@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.
@@ -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
+ })