@livestore/utils-dev 0.4.0-dev.2 → 0.4.0-dev.20
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/.tsbuildinfo.json +1 -1
- package/dist/node/DockerComposeService/DockerComposeService.d.ts +58 -0
- package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +1 -0
- package/dist/node/DockerComposeService/DockerComposeService.js +144 -0
- package/dist/node/DockerComposeService/DockerComposeService.js.map +1 -0
- package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +2 -0
- package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +1 -0
- package/dist/node/DockerComposeService/DockerComposeService.test.js +64 -0
- package/dist/node/DockerComposeService/DockerComposeService.test.js.map +1 -0
- package/dist/node/cmd-log.d.ts +21 -0
- package/dist/node/cmd-log.d.ts.map +1 -0
- package/dist/node/cmd-log.js +47 -0
- package/dist/node/cmd-log.js.map +1 -0
- package/dist/node/cmd.d.ts +35 -0
- package/dist/node/cmd.d.ts.map +1 -0
- package/dist/node/cmd.js +235 -0
- package/dist/node/cmd.js.map +1 -0
- package/dist/node/cmd.test.d.ts +2 -0
- package/dist/node/cmd.test.d.ts.map +1 -0
- package/dist/node/cmd.test.js +102 -0
- package/dist/node/cmd.test.js.map +1 -0
- package/dist/node/mod.d.ts +5 -26
- package/dist/node/mod.d.ts.map +1 -1
- package/dist/node/mod.js +52 -59
- package/dist/node/mod.js.map +1 -1
- package/dist/node/workspace.d.ts +22 -0
- package/dist/node/workspace.d.ts.map +1 -0
- package/dist/node/workspace.js +26 -0
- package/dist/node/workspace.js.map +1 -0
- package/dist/node-vitest/Vitest.d.ts +41 -7
- package/dist/node-vitest/Vitest.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.js +82 -5
- package/dist/node-vitest/Vitest.js.map +1 -1
- package/dist/node-vitest/Vitest.test.d.ts +2 -0
- package/dist/node-vitest/Vitest.test.d.ts.map +1 -0
- package/dist/node-vitest/Vitest.test.js +70 -0
- package/dist/node-vitest/Vitest.test.js.map +1 -0
- package/dist/wrangler/WranglerDevServer.d.ts +69 -0
- package/dist/wrangler/WranglerDevServer.d.ts.map +1 -0
- package/dist/wrangler/WranglerDevServer.js +103 -0
- package/dist/wrangler/WranglerDevServer.js.map +1 -0
- package/dist/wrangler/WranglerDevServer.test.d.ts +2 -0
- package/dist/wrangler/WranglerDevServer.test.d.ts.map +1 -0
- package/dist/wrangler/WranglerDevServer.test.js +77 -0
- package/dist/wrangler/WranglerDevServer.test.js.map +1 -0
- package/dist/wrangler/fixtures/cf-worker.d.ts +8 -0
- package/dist/wrangler/fixtures/cf-worker.d.ts.map +1 -0
- package/dist/wrangler/fixtures/cf-worker.js +11 -0
- package/dist/wrangler/fixtures/cf-worker.js.map +1 -0
- package/dist/wrangler/mod.d.ts +2 -0
- package/dist/wrangler/mod.d.ts.map +1 -0
- package/dist/wrangler/mod.js +2 -0
- package/dist/wrangler/mod.js.map +1 -0
- package/package.json +11 -10
- package/src/node/DockerComposeService/DockerComposeService.test.ts +91 -0
- package/src/node/DockerComposeService/DockerComposeService.ts +328 -0
- package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +4 -0
- package/src/node/cmd-log.ts +87 -0
- package/src/node/cmd.test.ts +130 -0
- package/src/node/cmd.ts +420 -0
- package/src/node/mod.ts +63 -116
- package/src/node/workspace.ts +45 -0
- package/src/node-vitest/Vitest.test.ts +112 -0
- package/src/node-vitest/Vitest.ts +193 -17
- package/src/wrangler/WranglerDevServer.test.ts +133 -0
- package/src/wrangler/WranglerDevServer.ts +220 -0
- package/src/wrangler/fixtures/cf-worker.ts +11 -0
- package/src/wrangler/fixtures/wrangler.toml +11 -0
- package/src/wrangler/mod.ts +6 -0
- package/dist/node-vitest/polyfill.d.ts +0 -2
- package/dist/node-vitest/polyfill.d.ts.map +0 -1
- package/dist/node-vitest/polyfill.js +0 -3
- package/dist/node-vitest/polyfill.js.map +0 -1
package/src/node/mod.ts
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks'
|
|
2
2
|
|
|
3
3
|
import * as OtelNodeSdk from '@effect/opentelemetry/NodeSdk'
|
|
4
|
-
import { IS_BUN, isNonEmptyString
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
7
|
-
Command,
|
|
8
|
-
Config,
|
|
9
|
-
Effect,
|
|
10
|
-
FiberRef,
|
|
11
|
-
identity,
|
|
12
|
-
Layer,
|
|
13
|
-
LogLevel,
|
|
14
|
-
OtelTracer,
|
|
15
|
-
Schema,
|
|
16
|
-
} from '@livestore/utils/effect'
|
|
4
|
+
import { IS_BUN, isNonEmptyString } from '@livestore/utils'
|
|
5
|
+
import type { Tracer } from '@livestore/utils/effect'
|
|
6
|
+
import { Config, Effect, FiberRef, Layer, LogLevel, OtelTracer } from '@livestore/utils/effect'
|
|
17
7
|
import { OtelLiveDummy } from '@livestore/utils/node'
|
|
18
8
|
import * as otel from '@opentelemetry/api'
|
|
19
9
|
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
|
|
@@ -23,8 +13,18 @@ import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
|
23
13
|
|
|
24
14
|
export { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
|
|
25
15
|
export { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
|
26
|
-
|
|
16
|
+
export * from './cmd.ts'
|
|
17
|
+
export {
|
|
18
|
+
type DockerComposeArgs,
|
|
19
|
+
DockerComposeError,
|
|
20
|
+
type DockerComposeOperations,
|
|
21
|
+
DockerComposeService,
|
|
22
|
+
type LogsOptions,
|
|
23
|
+
type StartOptions,
|
|
24
|
+
startDockerComposeServicesScoped,
|
|
25
|
+
} from './DockerComposeService/DockerComposeService.ts'
|
|
27
26
|
export * as FileLogger from './FileLogger.ts'
|
|
27
|
+
export * from './workspace.ts'
|
|
28
28
|
|
|
29
29
|
export const OtelLiveHttp = ({
|
|
30
30
|
serviceName,
|
|
@@ -105,10 +105,7 @@ export const OtelLiveHttp = ({
|
|
|
105
105
|
const tracer = yield* OtelTracer.OtelTracer
|
|
106
106
|
const currentSpan = yield* OtelTracer.currentOtelSpan
|
|
107
107
|
|
|
108
|
-
const nodeTiming =
|
|
109
|
-
|
|
110
|
-
// TODO get rid of this workaround for Bun once Bun properly supports performance.nodeTiming
|
|
111
|
-
const startTime = IS_BUN ? nodeTiming.startTime : performance.timeOrigin + nodeTiming.nodeStart
|
|
108
|
+
const { nodeTiming, endAbs, durationAttr } = computeBootstrapTiming()
|
|
112
109
|
|
|
113
110
|
const bootSpan = tracer.startSpan(
|
|
114
111
|
'node-bootstrap',
|
|
@@ -119,13 +116,13 @@ export const OtelLiveHttp = ({
|
|
|
119
116
|
'node.timing.environment': nodeTiming.environment,
|
|
120
117
|
'node.timing.bootstrapComplete': nodeTiming.bootstrapComplete,
|
|
121
118
|
'node.timing.loopStart': nodeTiming.loopStart,
|
|
122
|
-
'node.timing.duration':
|
|
119
|
+
'node.timing.duration': durationAttr,
|
|
123
120
|
},
|
|
124
121
|
},
|
|
125
122
|
otel.trace.setSpanContext(otel.context.active(), currentSpan.spanContext()),
|
|
126
123
|
)
|
|
127
124
|
|
|
128
|
-
bootSpan.end(
|
|
125
|
+
bootSpan.end(endAbs)
|
|
129
126
|
}).pipe(Effect.provide(layer), Effect.orDie)
|
|
130
127
|
}
|
|
131
128
|
|
|
@@ -170,99 +167,49 @@ export const getTracingBackendUrl = (span: otel.Span) =>
|
|
|
170
167
|
return `${grafanaEndpoint}/explore?${searchParams.toString()}`
|
|
171
168
|
})
|
|
172
169
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
env: options?.env ?? {},
|
|
220
|
-
stderr: options?.stderr ?? 'inherit',
|
|
221
|
-
}),
|
|
222
|
-
),
|
|
223
|
-
),
|
|
224
|
-
)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
export const cmdText: (
|
|
228
|
-
commandInput: string | (string | undefined)[],
|
|
229
|
-
options?: {
|
|
230
|
-
cwd?: string
|
|
231
|
-
stderr?: 'inherit' | 'pipe'
|
|
232
|
-
runInShell?: boolean
|
|
233
|
-
env?: Record<string, string | undefined>
|
|
234
|
-
},
|
|
235
|
-
) => Effect.Effect<string, PlatformError.PlatformError, CommandExecutor.CommandExecutor> = Effect.fn('cmdText')(
|
|
236
|
-
function* (commandInput, options) {
|
|
237
|
-
const cwd = options?.cwd ?? process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
|
|
238
|
-
const [command, ...args] = Array.isArray(commandInput)
|
|
239
|
-
? commandInput.filter(isNotUndefined)
|
|
240
|
-
: commandInput.split(' ')
|
|
241
|
-
const debugEnvStr = Object.entries(options?.env ?? {})
|
|
242
|
-
.map(([key, value]) => `${key}='${value}' `)
|
|
243
|
-
.join('')
|
|
244
|
-
|
|
245
|
-
const commandDebugStr = debugEnvStr + [command, ...args].join(' ')
|
|
246
|
-
const subshellStr = options?.runInShell ? ' (in subshell)' : ''
|
|
247
|
-
|
|
248
|
-
yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'${subshellStr}`)
|
|
249
|
-
yield* Effect.annotateCurrentSpan({ 'span.label': commandDebugStr, command, cwd })
|
|
250
|
-
|
|
251
|
-
return yield* Command.make(command!, ...args).pipe(
|
|
252
|
-
// inherit = Stream stderr to process.stderr, pipe = Stream stderr to process.stdout
|
|
253
|
-
Command.stderr(options?.stderr ?? 'inherit'),
|
|
254
|
-
Command.workingDirectory(cwd),
|
|
255
|
-
options?.runInShell ? Command.runInShell(true) : identity,
|
|
256
|
-
Command.env(options?.env ?? {}),
|
|
257
|
-
Command.string,
|
|
258
|
-
)
|
|
259
|
-
},
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
export class CmdError extends Schema.TaggedError<CmdError>()('CmdError', {
|
|
263
|
-
command: Schema.String,
|
|
264
|
-
args: Schema.Array(Schema.String),
|
|
265
|
-
cwd: Schema.String,
|
|
266
|
-
env: Schema.Record({ key: Schema.String, value: Schema.String.pipe(Schema.UndefinedOr) }),
|
|
267
|
-
stderr: Schema.Literal('inherit', 'pipe'),
|
|
268
|
-
}) {}
|
|
170
|
+
/**
|
|
171
|
+
* Compute absolute start/end timestamps for the Node.js bootstrap span in a
|
|
172
|
+
* way that works in both Node and Bun.
|
|
173
|
+
*
|
|
174
|
+
* Context: Bun's perf_hooks PerformanceNodeTiming currently throws when
|
|
175
|
+
* accessing standard PerformanceEntry getters like `startTime` and
|
|
176
|
+
* `duration`, and some fields differ in semantics (e.g. `nodeStart` appears
|
|
177
|
+
* as an epoch timestamp rather than an offset). See:
|
|
178
|
+
* https://github.com/oven-sh/bun/issues/23041
|
|
179
|
+
*
|
|
180
|
+
* We therefore avoid the problematic getters and derive absolute timestamps
|
|
181
|
+
* using fields that exist in both runtimes.
|
|
182
|
+
*
|
|
183
|
+
* TODO: Simplify to a single, non-branching computation once the Bun issue
|
|
184
|
+
* above is fixed and Bun matches Node's semantics for PerformanceNodeTiming.
|
|
185
|
+
*/
|
|
186
|
+
const computeBootstrapTiming = () => {
|
|
187
|
+
const nodeTiming = performance.nodeTiming
|
|
188
|
+
|
|
189
|
+
// Absolute start time in ms since epoch.
|
|
190
|
+
const startAbs = IS_BUN
|
|
191
|
+
? typeof nodeTiming.nodeStart === 'number'
|
|
192
|
+
? nodeTiming.nodeStart
|
|
193
|
+
: performance.timeOrigin
|
|
194
|
+
: performance.timeOrigin + nodeTiming.nodeStart
|
|
195
|
+
|
|
196
|
+
// Absolute end time.
|
|
197
|
+
const endAbs = IS_BUN
|
|
198
|
+
? (() => {
|
|
199
|
+
const { loopStart, bootstrapComplete } = nodeTiming
|
|
200
|
+
if (typeof loopStart === 'number' && loopStart > 0) return startAbs + loopStart
|
|
201
|
+
if (typeof bootstrapComplete === 'number' && bootstrapComplete >= startAbs) return bootstrapComplete
|
|
202
|
+
return startAbs + 1
|
|
203
|
+
})()
|
|
204
|
+
: startAbs + nodeTiming.duration
|
|
205
|
+
|
|
206
|
+
// Duration attribute value for the span.
|
|
207
|
+
const durationAttr = IS_BUN
|
|
208
|
+
? (() => {
|
|
209
|
+
const { loopStart } = nodeTiming
|
|
210
|
+
return typeof loopStart === 'number' && loopStart > 0 ? loopStart : 0
|
|
211
|
+
})()
|
|
212
|
+
: nodeTiming.duration
|
|
213
|
+
|
|
214
|
+
return { nodeTiming, startAbs, endAbs, durationAttr } as const
|
|
215
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
3
|
+
import { Context, Effect, Layer } from '@livestore/utils/effect'
|
|
4
|
+
|
|
5
|
+
export type WorkspaceInfo = string
|
|
6
|
+
|
|
7
|
+
/** Current working directory. */
|
|
8
|
+
export class CurrentWorkingDirectory extends Context.Tag('CurrentWorkingDirectory')<
|
|
9
|
+
CurrentWorkingDirectory,
|
|
10
|
+
WorkspaceInfo
|
|
11
|
+
>() {
|
|
12
|
+
/** Layer that captures the process cwd once. */
|
|
13
|
+
static live = Layer.effect(
|
|
14
|
+
CurrentWorkingDirectory,
|
|
15
|
+
Effect.sync(() => process.cwd()),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
/** Override CWD for tests or nested invocations. */
|
|
19
|
+
static fromPath = (cwd: string) => Layer.succeed(CurrentWorkingDirectory, cwd)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Livestore workspace root (env required). */
|
|
23
|
+
export class LivestoreWorkspace extends Context.Tag('LivestoreWorkspace')<LivestoreWorkspace, WorkspaceInfo>() {
|
|
24
|
+
/** Resolve from WORKSPACE_ROOT env. */
|
|
25
|
+
static live = Layer.effect(
|
|
26
|
+
LivestoreWorkspace,
|
|
27
|
+
Effect.sync(() => {
|
|
28
|
+
const root = process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
|
|
29
|
+
return root
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
/** Provide a fixed Livestore root. */
|
|
34
|
+
static fromPath = (root: string) => Layer.succeed(LivestoreWorkspace, root)
|
|
35
|
+
|
|
36
|
+
/** Derive a CurrentWorkingDirectory layer from the Livestore workspace root (with optional subpath) */
|
|
37
|
+
static toCwd = (/** Relative path to the Livestore workspace root */ subPath?: string) =>
|
|
38
|
+
Layer.effect(
|
|
39
|
+
CurrentWorkingDirectory,
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const root = yield* LivestoreWorkspace
|
|
42
|
+
return path.join(root, subPath ?? '')
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Effect, FastCheck } from '@livestore/utils/effect'
|
|
2
|
+
import * as Vitest from './Vitest.ts'
|
|
3
|
+
|
|
4
|
+
// Demonstrate enhanced asProp functionality with clear shrinking progress
|
|
5
|
+
// This showcases the fix for the "Run 26/6" bug and accurate shrinking detection
|
|
6
|
+
|
|
7
|
+
Vitest.describe('Vitest.asProp', () => {
|
|
8
|
+
const IntArbitrary = FastCheck.integer({ min: 1, max: 100 })
|
|
9
|
+
|
|
10
|
+
// Always-passing test - should only show initial phase
|
|
11
|
+
Vitest.asProp(
|
|
12
|
+
Vitest.scopedLive,
|
|
13
|
+
'always-pass test (shows only initial runs)',
|
|
14
|
+
[IntArbitrary],
|
|
15
|
+
(properties, _ctx, enhanced) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const [value] = properties
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return yield* Effect.fail(new Error('Value is undefined'))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(
|
|
23
|
+
`✅ ALWAYS-PASS [${enhanced._tag.toUpperCase()}]: ` +
|
|
24
|
+
(enhanced._tag === 'initial'
|
|
25
|
+
? `Run ${enhanced.runIndex + 1}/${enhanced.numRuns}`
|
|
26
|
+
: `Shrink #${enhanced.shrinkAttempt} (finding minimal counterexample)`) +
|
|
27
|
+
`, value=${value}, total=${enhanced.totalExecutions}`,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// This test always passes, so no shrinking will occur
|
|
31
|
+
return
|
|
32
|
+
}),
|
|
33
|
+
{ fastCheck: { numRuns: 4 } },
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// Failing test - should show initial + shrinking phases
|
|
37
|
+
let alreadyFailed = false
|
|
38
|
+
Vitest.asProp(
|
|
39
|
+
Vitest.scopedLive,
|
|
40
|
+
'failing test (shows initial runs + shrinking)',
|
|
41
|
+
[IntArbitrary],
|
|
42
|
+
(properties, ctx, enhanced) =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const [value] = properties
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return yield* Effect.fail(new Error('Value is undefined'))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const displayInfo =
|
|
50
|
+
enhanced._tag === 'initial'
|
|
51
|
+
? `Run ${enhanced.runIndex + 1}/${enhanced.numRuns}`
|
|
52
|
+
: `Shrink #${enhanced.shrinkAttempt} (finding minimal counterexample)`
|
|
53
|
+
|
|
54
|
+
const status = value > 80 ? '💥' : '✅'
|
|
55
|
+
console.log(
|
|
56
|
+
`${status} FAILING-TEST [${enhanced._tag.toUpperCase()}]: ${displayInfo}, value=${value}, total=${enhanced.totalExecutions}`,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// Fail when value is greater than 80 to trigger shrinking
|
|
60
|
+
if (value > 80) {
|
|
61
|
+
alreadyFailed = true
|
|
62
|
+
return yield* Effect.fail(new Error(`Value ${value} is too large (> 80)`))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (alreadyFailed && enhanced._tag === 'shrinking') {
|
|
66
|
+
ctx.skip("For the sake of this test, we don't want to fail but want to skip")
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return
|
|
71
|
+
}),
|
|
72
|
+
{ fastCheck: { numRuns: 3 } },
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// Test with endOnFailure: true - should not show shrinking
|
|
76
|
+
Vitest.asProp(
|
|
77
|
+
Vitest.scopedLive,
|
|
78
|
+
'failing test with endOnFailure (no shrinking)',
|
|
79
|
+
[IntArbitrary],
|
|
80
|
+
(properties, _ctx, enhanced) =>
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
const [value] = properties
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
return yield* Effect.fail(new Error('Value is undefined'))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(
|
|
88
|
+
`🚫 END-ON-FAILURE [${enhanced._tag.toUpperCase()}]: ` +
|
|
89
|
+
`Run ${enhanced.runIndex + 1}/${enhanced.numRuns}, value=${value}, total=${enhanced.totalExecutions}`,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// This will fail but shrinking is disabled
|
|
93
|
+
if (value > 50) {
|
|
94
|
+
return yield* Effect.fail(new Error(`Value ${value} is too large (> 50) - but no shrinking!`))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return
|
|
98
|
+
}),
|
|
99
|
+
{
|
|
100
|
+
fastCheck: {
|
|
101
|
+
numRuns: 5,
|
|
102
|
+
endOnFailure: true,
|
|
103
|
+
// Provide explicit samples so the second run crosses >50. Randomly drawing five
|
|
104
|
+
// integers has ~3% chance to stay ≤ 50, which would break the `fails: true`
|
|
105
|
+
// expectation even though shrinking remains disabled. The examples keep the
|
|
106
|
+
// demo readable while leaving the remaining runs to FastCheck.
|
|
107
|
+
examples: [[5], [66]],
|
|
108
|
+
},
|
|
109
|
+
fails: true,
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
})
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import * as inspector from 'node:inspector'
|
|
2
2
|
import type * as Vitest from '@effect/vitest'
|
|
3
3
|
import { IS_CI } from '@livestore/utils'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type Cause,
|
|
6
|
+
Duration,
|
|
7
|
+
Effect,
|
|
8
|
+
type FastCheck as FC,
|
|
9
|
+
identity,
|
|
10
|
+
Layer,
|
|
11
|
+
type OtelTracer,
|
|
12
|
+
Predicate,
|
|
13
|
+
type Schema,
|
|
14
|
+
type Scope,
|
|
15
|
+
} from '@livestore/utils/effect'
|
|
5
16
|
import { OtelLiveDummy } from '@livestore/utils/node'
|
|
6
17
|
import { OtelLiveHttp } from '../node/mod.ts'
|
|
7
18
|
|
|
@@ -9,20 +20,29 @@ export * from '@effect/vitest'
|
|
|
9
20
|
|
|
10
21
|
export const DEBUGGER_ACTIVE = Boolean(process.env.DEBUGGER_ACTIVE ?? inspector.url() !== undefined)
|
|
11
22
|
|
|
12
|
-
export const makeWithTestCtx =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
export const makeWithTestCtx: <ROut = never, E1 = never, RIn = never>(
|
|
24
|
+
ctxParams: WithTestCtxParams<ROut, E1, RIn>,
|
|
25
|
+
) => (testContext: Vitest.TestContext) => <A, E, R>(
|
|
26
|
+
self: Effect.Effect<A, E, R>,
|
|
27
|
+
) => Effect.Effect<
|
|
28
|
+
A,
|
|
29
|
+
E | E1 | Cause.TimeoutException,
|
|
30
|
+
// Exclude dependencies provided by `withTestCtx` from the layer dependencies
|
|
31
|
+
| Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
|
|
32
|
+
// Exclude dependencies provided by `withTestCtx` **and** dependencies produced
|
|
33
|
+
// by the layer from the effect dependencies
|
|
34
|
+
| Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
|
|
35
|
+
> = (ctxParams) => (testContext: Vitest.TestContext) => withTestCtx(testContext, ctxParams)
|
|
16
36
|
|
|
17
|
-
export type WithTestCtxParams<
|
|
37
|
+
export type WithTestCtxParams<ROut, E1, RIn> = {
|
|
18
38
|
suffix?: string
|
|
19
|
-
makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<
|
|
39
|
+
makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn | Scope.Scope>
|
|
20
40
|
timeout?: Duration.DurationInput
|
|
21
41
|
forceOtel?: boolean
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
export const withTestCtx =
|
|
25
|
-
<
|
|
45
|
+
<ROut = never, E1 = never, RIn = never>(
|
|
26
46
|
testContext: Vitest.TestContext,
|
|
27
47
|
{
|
|
28
48
|
suffix,
|
|
@@ -31,19 +51,31 @@ export const withTestCtx =
|
|
|
31
51
|
forceOtel = false,
|
|
32
52
|
}: {
|
|
33
53
|
suffix?: string
|
|
34
|
-
makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<
|
|
54
|
+
makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn>
|
|
35
55
|
timeout?: Duration.DurationInput
|
|
36
56
|
forceOtel?: boolean
|
|
37
57
|
} = {},
|
|
38
58
|
) =>
|
|
39
|
-
<A, E>(
|
|
40
|
-
self: Effect.Effect<A, E,
|
|
41
|
-
): Effect.Effect<
|
|
59
|
+
<A, E, R>(
|
|
60
|
+
self: Effect.Effect<A, E, R>,
|
|
61
|
+
): Effect.Effect<
|
|
62
|
+
A,
|
|
63
|
+
E | E1 | Cause.TimeoutException,
|
|
64
|
+
// Exclude dependencies provided internally from the provided layer's dependencies
|
|
65
|
+
| Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
|
|
66
|
+
// Exclude dependencies provided internally **and** dependencies produced by the
|
|
67
|
+
// provided layer from the effect dependencies
|
|
68
|
+
| Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
|
|
69
|
+
> => {
|
|
42
70
|
const spanName = `${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`
|
|
43
|
-
const layer = makeLayer?.(testContext)
|
|
71
|
+
const layer = makeLayer?.(testContext) ?? Layer.empty
|
|
44
72
|
|
|
45
73
|
const otelLayer =
|
|
46
|
-
DEBUGGER_ACTIVE || forceOtel
|
|
74
|
+
DEBUGGER_ACTIVE || forceOtel
|
|
75
|
+
? OtelLiveHttp({ rootSpanName: spanName, serviceName: 'vitest-runner', skipLogUrl: false })
|
|
76
|
+
: OtelLiveDummy
|
|
77
|
+
|
|
78
|
+
const combinedLayer = layer.pipe(Layer.provideMerge(otelLayer))
|
|
47
79
|
|
|
48
80
|
return self.pipe(
|
|
49
81
|
DEBUGGER_ACTIVE
|
|
@@ -53,10 +85,154 @@ export const withTestCtx =
|
|
|
53
85
|
label: `${spanName} approaching timeout (timeout: ${Duration.format(timeout)})`,
|
|
54
86
|
}),
|
|
55
87
|
DEBUGGER_ACTIVE ? identity : Effect.timeout(timeout),
|
|
56
|
-
Effect.provide(
|
|
57
|
-
Effect.provide(layer ?? Layer.empty),
|
|
88
|
+
Effect.provide(combinedLayer),
|
|
58
89
|
Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
|
|
59
|
-
Effect.withSpan(spanName),
|
|
60
90
|
Effect.annotateLogs({ suffix }),
|
|
61
91
|
) as any
|
|
62
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Shared properties for all enhanced test context phases
|
|
96
|
+
*/
|
|
97
|
+
export interface EnhancedTestContextBase {
|
|
98
|
+
numRuns: number
|
|
99
|
+
/** 0-based index */
|
|
100
|
+
runIndex: number
|
|
101
|
+
/** Total number of executions including initial runs and shrinking attempts */
|
|
102
|
+
totalExecutions: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Enhanced context for property-based tests that includes shrinking phase information
|
|
107
|
+
*/
|
|
108
|
+
export type EnhancedTestContext =
|
|
109
|
+
| (EnhancedTestContextBase & {
|
|
110
|
+
_tag: 'initial'
|
|
111
|
+
})
|
|
112
|
+
| (EnhancedTestContextBase & {
|
|
113
|
+
_tag: 'shrinking'
|
|
114
|
+
/** Number of shrinking attempts */
|
|
115
|
+
shrinkAttempt: number
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Normalizes propOptions to ensure @effect/vitest receives correct fastCheck structure
|
|
120
|
+
*/
|
|
121
|
+
const normalizePropOptions = <Arbs extends Vitest.Vitest.Arbitraries>(
|
|
122
|
+
propOptions:
|
|
123
|
+
| number
|
|
124
|
+
| (Vitest.TestOptions & {
|
|
125
|
+
fastCheck?: FC.Parameters<{
|
|
126
|
+
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]>
|
|
127
|
+
}>
|
|
128
|
+
}),
|
|
129
|
+
): Vitest.TestOptions & {
|
|
130
|
+
fastCheck?: FC.Parameters<{
|
|
131
|
+
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]>
|
|
132
|
+
}>
|
|
133
|
+
} => {
|
|
134
|
+
// If it's a number, treat as timeout and add our default fastCheck
|
|
135
|
+
if (!Predicate.isObject(propOptions)) {
|
|
136
|
+
return {
|
|
137
|
+
timeout: propOptions,
|
|
138
|
+
fastCheck: { numRuns: 100 },
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If no fastCheck property, add it with our default numRuns
|
|
143
|
+
if (!propOptions.fastCheck) {
|
|
144
|
+
return {
|
|
145
|
+
...propOptions,
|
|
146
|
+
fastCheck: { numRuns: 100 },
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// If fastCheck exists but no numRuns, add our default
|
|
151
|
+
if (propOptions.fastCheck && !propOptions.fastCheck.numRuns) {
|
|
152
|
+
return {
|
|
153
|
+
...propOptions,
|
|
154
|
+
fastCheck: {
|
|
155
|
+
...propOptions.fastCheck,
|
|
156
|
+
numRuns: 100,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If everything is properly structured, pass through
|
|
162
|
+
return propOptions
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Equivalent to Vitest.prop but provides enhanced context including shrinking progress visibility
|
|
167
|
+
*
|
|
168
|
+
* This function enhances the standard property-based testing by providing clear information about
|
|
169
|
+
* whether FastCheck is in the initial testing phase or the shrinking phase, solving the confusion
|
|
170
|
+
* where tests show "Run 26/6" when FastCheck's shrinking algorithm is active.
|
|
171
|
+
*
|
|
172
|
+
* TODO: allow for upper timelimit instead of / additional to `numRuns`
|
|
173
|
+
*
|
|
174
|
+
* TODO: Upstream to Effect
|
|
175
|
+
*/
|
|
176
|
+
export const asProp = <Arbs extends Vitest.Vitest.Arbitraries, A, E, R>(
|
|
177
|
+
api: Vitest.Vitest.Tester<R>,
|
|
178
|
+
name: string,
|
|
179
|
+
arbitraries: Arbs,
|
|
180
|
+
test: Vitest.Vitest.TestFunction<
|
|
181
|
+
A,
|
|
182
|
+
E,
|
|
183
|
+
R,
|
|
184
|
+
[
|
|
185
|
+
{ [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]> },
|
|
186
|
+
Vitest.TestContext,
|
|
187
|
+
EnhancedTestContext,
|
|
188
|
+
]
|
|
189
|
+
>,
|
|
190
|
+
propOptions:
|
|
191
|
+
| number
|
|
192
|
+
| (Vitest.TestOptions & {
|
|
193
|
+
fastCheck?: FC.Parameters<{
|
|
194
|
+
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]>
|
|
195
|
+
}>
|
|
196
|
+
}),
|
|
197
|
+
) => {
|
|
198
|
+
const normalizedPropOptions = normalizePropOptions(propOptions)
|
|
199
|
+
const numRuns = normalizedPropOptions.fastCheck?.numRuns ?? 100
|
|
200
|
+
let runIndex = 0
|
|
201
|
+
let shrinkAttempts = 0
|
|
202
|
+
let totalExecutions = 0
|
|
203
|
+
|
|
204
|
+
return api.prop(
|
|
205
|
+
name,
|
|
206
|
+
arbitraries,
|
|
207
|
+
(properties, ctx) => {
|
|
208
|
+
if (ctx.signal.aborted) {
|
|
209
|
+
return ctx.skip('Test aborted')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
totalExecutions++
|
|
213
|
+
const isInShrinkingPhase = runIndex >= numRuns
|
|
214
|
+
|
|
215
|
+
if (isInShrinkingPhase) {
|
|
216
|
+
shrinkAttempts++
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const enhancedContext: EnhancedTestContext = isInShrinkingPhase
|
|
220
|
+
? {
|
|
221
|
+
_tag: 'shrinking',
|
|
222
|
+
numRuns,
|
|
223
|
+
runIndex: runIndex++,
|
|
224
|
+
shrinkAttempt: shrinkAttempts,
|
|
225
|
+
totalExecutions,
|
|
226
|
+
}
|
|
227
|
+
: {
|
|
228
|
+
_tag: 'initial',
|
|
229
|
+
numRuns,
|
|
230
|
+
runIndex: runIndex++,
|
|
231
|
+
totalExecutions,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return test(properties, ctx, enhancedContext)
|
|
235
|
+
},
|
|
236
|
+
normalizedPropOptions,
|
|
237
|
+
)
|
|
238
|
+
}
|