@mikrojs/native 0.0.7
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/CMakeLists.txt +198 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/cmake/mikrojs_bytecode.cmake +146 -0
- package/cmake.js +22 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/include/byteorder_apple.h +11 -0
- package/include/byteorder_windows.h +12 -0
- package/include/mikrojs/cbor_helpers.h +24 -0
- package/include/mikrojs/cutils_wrap.h +59 -0
- package/include/mikrojs/errors.h +144 -0
- package/include/mikrojs/mem.h +11 -0
- package/include/mikrojs/mik_color.h +32 -0
- package/include/mikrojs/mikrojs.h +331 -0
- package/include/mikrojs/platform.h +82 -0
- package/include/mikrojs/private.h +281 -0
- package/include/mikrojs/utils.h +125 -0
- package/package.json +100 -0
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/ble/ble.ts +231 -0
- package/runtime/ble/types.ts +194 -0
- package/runtime/ble/uuid.ts +89 -0
- package/runtime/ble/validators.ts +61 -0
- package/runtime/cbor/cbor.ts +1 -0
- package/runtime/cbor/types.ts +8 -0
- package/runtime/console/types.ts +50 -0
- package/runtime/env/env.ts +17 -0
- package/runtime/env/types.ts +12 -0
- package/runtime/format/types.ts +4 -0
- package/runtime/fs/fs.ts +93 -0
- package/runtime/fs/types.ts +92 -0
- package/runtime/globals.d.ts +87 -0
- package/runtime/http/helpers.ts +222 -0
- package/runtime/http/native.ts +151 -0
- package/runtime/http/request.ts +25 -0
- package/runtime/i2c/i2c.ts +35 -0
- package/runtime/i2c/types.ts +55 -0
- package/runtime/inspect/types.ts +10 -0
- package/runtime/internal.d.ts +456 -0
- package/runtime/kv/nvs.ts +17 -0
- package/runtime/kv/rtc.ts +17 -0
- package/runtime/kv/shared.ts +107 -0
- package/runtime/kv/types.ts +150 -0
- package/runtime/neopixel/neopixel.ts +38 -0
- package/runtime/neopixel/types.ts +27 -0
- package/runtime/pin/pin.ts +51 -0
- package/runtime/pin/types.ts +49 -0
- package/runtime/pwm/pwm.ts +32 -0
- package/runtime/pwm/types.ts +29 -0
- package/runtime/reader/reader.ts +167 -0
- package/runtime/reader/types.ts +34 -0
- package/runtime/result/native-result.node-shim.ts +44 -0
- package/runtime/result/result.ts +26 -0
- package/runtime/result/types.ts +60 -0
- package/runtime/schema/schema.ts +321 -0
- package/runtime/schema/types.ts +152 -0
- package/runtime/sleep/sleep.ts +14 -0
- package/runtime/sleep/types.ts +44 -0
- package/runtime/sntp/sntp.ts +54 -0
- package/runtime/sntp/types.ts +38 -0
- package/runtime/spi/spi.ts +31 -0
- package/runtime/spi/types.ts +42 -0
- package/runtime/stdio/stdio.ts +44 -0
- package/runtime/stdio/types.ts +22 -0
- package/runtime/stream/stream.ts +150 -0
- package/runtime/stream/types.ts +47 -0
- package/runtime/sys/sys.ts +90 -0
- package/runtime/sys/types.ts +131 -0
- package/runtime/test/test.ts +595 -0
- package/runtime/test/types.ts +97 -0
- package/runtime/uart/types.ts +75 -0
- package/runtime/uart/uart.ts +51 -0
- package/runtime/wifi/types.ts +156 -0
- package/runtime/wifi/wifi.ts +208 -0
- package/scripts/bundle-runtime.js +149 -0
- package/scripts/compare-minifiers.js +189 -0
- package/scripts/compile-bytecode.sh +38 -0
- package/scripts/copy-prebuild.js +20 -0
- package/scripts/generate-symbol-map.js +146 -0
- package/src/builtins.cpp +82 -0
- package/src/cutils_compat.c +38 -0
- package/src/eval_bytecode.cpp +42 -0
- package/src/fs.cpp +878 -0
- package/src/mem.cpp +63 -0
- package/src/mik_abort.cpp +160 -0
- package/src/mik_app_config.cpp +358 -0
- package/src/mik_cbor.cpp +334 -0
- package/src/mik_color.cpp +46 -0
- package/src/mik_console.cpp +422 -0
- package/src/mik_inspect.cpp +850 -0
- package/src/mik_repl.cpp +1122 -0
- package/src/mik_result.cpp +344 -0
- package/src/mik_stdio.cpp +147 -0
- package/src/mik_sys.cpp +239 -0
- package/src/mik_text_encoding.cpp +443 -0
- package/src/mikrojs.cpp +942 -0
- package/src/modules.cpp +944 -0
- package/src/platform_posix.cpp +134 -0
- package/src/timers.cpp +208 -0
- package/src/utils.cpp +173 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import {gc, memoryUsage, MonotonicTimestamp} from 'mikrojs/sys'
|
|
3
|
+
import {pendingCount as pendingHttpCount} from 'native:http'
|
|
4
|
+
import {activeTimers} from 'native:sys'
|
|
5
|
+
|
|
6
|
+
import type {ErrResult, OkResult, Result} from '../result/types.js'
|
|
7
|
+
import type {Assert, TestFn, TestOptions} from './types.js'
|
|
8
|
+
|
|
9
|
+
interface TestCase {
|
|
10
|
+
name: string
|
|
11
|
+
fn: TestFn
|
|
12
|
+
skip: boolean
|
|
13
|
+
only: boolean
|
|
14
|
+
todo: boolean
|
|
15
|
+
timeout?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Suite {
|
|
19
|
+
name: string
|
|
20
|
+
tests: TestCase[]
|
|
21
|
+
skip: boolean
|
|
22
|
+
only: boolean
|
|
23
|
+
todo: boolean
|
|
24
|
+
beforeAll?: TestFn
|
|
25
|
+
afterAll?: TestFn
|
|
26
|
+
beforeEach: TestFn[]
|
|
27
|
+
afterEach: TestFn[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const suites: Suite[] = []
|
|
31
|
+
let currentSuite: Suite | null = null
|
|
32
|
+
let heapBaseline = 0
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recapture the heap baseline. Called by the harness after each suite's
|
|
36
|
+
* beforeAll resolves so warmup allocations (module loads, fetch/TLS
|
|
37
|
+
* lazy-init, wifi connection) don't count toward heapDelta. A microtask
|
|
38
|
+
* yield before the gc lets the beforeAll async frame's locals become
|
|
39
|
+
* collectible — otherwise the baseline would be inflated by vars that
|
|
40
|
+
* were still pinned by the suspended closure when beforeAll resolved,
|
|
41
|
+
* and heapAfter would come in lower than baseline (negative delta).
|
|
42
|
+
*/
|
|
43
|
+
async function captureHeapBaseline(): Promise<void> {
|
|
44
|
+
await Promise.resolve()
|
|
45
|
+
gc()
|
|
46
|
+
heapBaseline = memoryUsage().heapUsed
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function newSuite(name: string, flags: {skip?: boolean; only?: boolean; todo?: boolean}): Suite {
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
tests: [],
|
|
53
|
+
skip: flags.skip ?? false,
|
|
54
|
+
only: flags.only ?? false,
|
|
55
|
+
todo: flags.todo ?? false,
|
|
56
|
+
beforeEach: [],
|
|
57
|
+
afterEach: [],
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runSuiteFn(suite: Suite, fn: () => void): void {
|
|
62
|
+
suites.push(suite)
|
|
63
|
+
const prev = currentSuite
|
|
64
|
+
currentSuite = suite
|
|
65
|
+
fn()
|
|
66
|
+
currentSuite = prev
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- name interpolation for .each ---
|
|
70
|
+
|
|
71
|
+
function interpolateName(template: string, value: unknown, index: number): string {
|
|
72
|
+
let name = template.replaceAll('%#', String(index))
|
|
73
|
+
name = name.replaceAll('%s', String(value))
|
|
74
|
+
if (typeof value === 'object' && value !== null) {
|
|
75
|
+
name = name.replaceAll('%o', JSON.stringify(value))
|
|
76
|
+
}
|
|
77
|
+
return name
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- describe ---
|
|
81
|
+
|
|
82
|
+
type EachDescribeFn = <T>(
|
|
83
|
+
cases: T[],
|
|
84
|
+
) => (name: string, fn: (value: T, index: number) => void) => void
|
|
85
|
+
|
|
86
|
+
function _describe(name: string, fn: () => void): void {
|
|
87
|
+
runSuiteFn(newSuite(name, {}), fn)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_describe.skip = function (name: string, fn: () => void): void {
|
|
91
|
+
runSuiteFn(newSuite(name, {skip: true}), fn)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_describe.only = function (name: string, fn: () => void): void {
|
|
95
|
+
runSuiteFn(newSuite(name, {only: true}), fn)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_describe.todo = function (name: string, fn: () => void): void {
|
|
99
|
+
runSuiteFn(newSuite(name, {todo: true}), fn)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_describe.fixme = function (name: string, fn: () => void): void {
|
|
103
|
+
runSuiteFn(newSuite(name, {skip: true}), fn)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_describe.skipIf = function (condition: unknown) {
|
|
107
|
+
return condition ? _describe.skip : _describe
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_describe.runIf = function (condition: unknown) {
|
|
111
|
+
return condition ? _describe : _describe.skip
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function describeEach(
|
|
115
|
+
variant: (name: string, fn: () => void) => void,
|
|
116
|
+
): <T>(cases: T[]) => (name: string, fn: (value: T, index: number) => void) => void {
|
|
117
|
+
return function <T>(cases: T[]) {
|
|
118
|
+
return function (nameTemplate: string, fn: (value: T, index: number) => void): void {
|
|
119
|
+
cases.forEach((value, index) => {
|
|
120
|
+
variant(interpolateName(nameTemplate, value, index), () => fn(value, index))
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_describe.each = describeEach(_describe) as EachDescribeFn
|
|
127
|
+
;(_describe.skip as any).each = describeEach(_describe.skip) as EachDescribeFn
|
|
128
|
+
;(_describe.only as any).each = describeEach(_describe.only) as EachDescribeFn
|
|
129
|
+
;(_describe.todo as any).each = describeEach(_describe.todo) as EachDescribeFn
|
|
130
|
+
;(_describe.fixme as any).each = describeEach(_describe.fixme) as EachDescribeFn
|
|
131
|
+
|
|
132
|
+
export const describe = _describe as typeof _describe & {
|
|
133
|
+
skip: typeof _describe.skip & {each: EachDescribeFn}
|
|
134
|
+
only: typeof _describe.only & {each: EachDescribeFn}
|
|
135
|
+
todo: typeof _describe.todo & {each: EachDescribeFn}
|
|
136
|
+
fixme: typeof _describe.fixme & {each: EachDescribeFn}
|
|
137
|
+
skipIf: (condition: unknown) => typeof _describe & {each: EachDescribeFn}
|
|
138
|
+
runIf: (condition: unknown) => typeof _describe & {each: EachDescribeFn}
|
|
139
|
+
each: EachDescribeFn
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- test ---
|
|
143
|
+
|
|
144
|
+
type EachTestFn = <T>(
|
|
145
|
+
cases: T[],
|
|
146
|
+
) => (name: string, fn: (value: T, index: number) => void | Promise<void>) => void
|
|
147
|
+
|
|
148
|
+
function pushTest(
|
|
149
|
+
where: string,
|
|
150
|
+
name: string,
|
|
151
|
+
fn: TestFn,
|
|
152
|
+
flags: {skip?: boolean; only?: boolean; todo?: boolean},
|
|
153
|
+
options?: TestOptions,
|
|
154
|
+
): void {
|
|
155
|
+
if (!currentSuite) throw new Error(`${where}() must be inside describe()`)
|
|
156
|
+
currentSuite.tests.push({
|
|
157
|
+
name,
|
|
158
|
+
fn,
|
|
159
|
+
skip: flags.skip ?? false,
|
|
160
|
+
only: flags.only ?? false,
|
|
161
|
+
todo: flags.todo ?? false,
|
|
162
|
+
timeout: options?.timeout,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _test(name: string, fn: TestFn, options?: TestOptions): void {
|
|
167
|
+
pushTest('test', name, fn, {}, options)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_test.skip = function (name: string, fn: TestFn, options?: TestOptions): void {
|
|
171
|
+
pushTest('test.skip', name, fn, {skip: true}, options)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_test.only = function (name: string, fn: TestFn, options?: TestOptions): void {
|
|
175
|
+
pushTest('test.only', name, fn, {only: true}, options)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_test.fixme = function (name: string, fn: TestFn, options?: TestOptions): void {
|
|
179
|
+
pushTest('test.fixme', name, fn, {skip: true}, options)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_test.skipIf = function (condition: unknown) {
|
|
183
|
+
return condition ? _test.skip : _test
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_test.runIf = function (condition: unknown) {
|
|
187
|
+
return condition ? _test : _test.skip
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_test.todo = function (name: string): void {
|
|
191
|
+
pushTest('test.todo', name, () => {}, {todo: true})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function testEach(
|
|
195
|
+
variant: (name: string, fn: TestFn) => void,
|
|
196
|
+
): <T>(
|
|
197
|
+
cases: T[],
|
|
198
|
+
) => (name: string, fn: (value: T, index: number) => void | Promise<void>) => void {
|
|
199
|
+
return function <T>(cases: T[]) {
|
|
200
|
+
return function (
|
|
201
|
+
nameTemplate: string,
|
|
202
|
+
fn: (value: T, index: number) => void | Promise<void>,
|
|
203
|
+
): void {
|
|
204
|
+
cases.forEach((value, index) => {
|
|
205
|
+
variant(interpolateName(nameTemplate, value, index), () => fn(value, index))
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_test.each = testEach(_test) as EachTestFn
|
|
212
|
+
;(_test.skip as any).each = testEach(_test.skip) as EachTestFn
|
|
213
|
+
;(_test.only as any).each = testEach(_test.only) as EachTestFn
|
|
214
|
+
;(_test.fixme as any).each = testEach(_test.fixme) as EachTestFn
|
|
215
|
+
|
|
216
|
+
export const test = _test as typeof _test & {
|
|
217
|
+
skip: typeof _test.skip & {each: EachTestFn}
|
|
218
|
+
only: typeof _test.only & {each: EachTestFn}
|
|
219
|
+
fixme: typeof _test.fixme & {each: EachTestFn}
|
|
220
|
+
skipIf: (condition: unknown) => typeof _test & {each: EachTestFn}
|
|
221
|
+
runIf: (condition: unknown) => typeof _test & {each: EachTestFn}
|
|
222
|
+
todo: (name: string) => void
|
|
223
|
+
each: EachTestFn
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// --- hooks ---
|
|
227
|
+
|
|
228
|
+
export function beforeAll(fn: TestFn): void {
|
|
229
|
+
if (!currentSuite) throw new Error('beforeAll() must be inside describe()')
|
|
230
|
+
currentSuite.beforeAll = fn
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function afterAll(fn: TestFn): void {
|
|
234
|
+
if (!currentSuite) throw new Error('afterAll() must be inside describe()')
|
|
235
|
+
currentSuite.afterAll = fn
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function beforeEach(fn: TestFn): void {
|
|
239
|
+
if (!currentSuite) throw new Error('beforeEach() must be inside describe()')
|
|
240
|
+
currentSuite.beforeEach.push(fn)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function afterEach(fn: TestFn): void {
|
|
244
|
+
if (!currentSuite) throw new Error('afterEach() must be inside describe()')
|
|
245
|
+
currentSuite.afterEach.push(fn)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// --- assertions ---
|
|
249
|
+
|
|
250
|
+
function fmt(v: unknown): string {
|
|
251
|
+
if (v === undefined) return 'undefined'
|
|
252
|
+
if (v === null) return 'null'
|
|
253
|
+
if (typeof v === 'string') return JSON.stringify(v)
|
|
254
|
+
if (v instanceof Uint8Array) return `Uint8Array[${v.join(', ')}]`
|
|
255
|
+
try {
|
|
256
|
+
return JSON.stringify(v)
|
|
257
|
+
} catch {
|
|
258
|
+
return String(v)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
class AssertError extends Error {
|
|
263
|
+
constructor(message: string) {
|
|
264
|
+
super(message)
|
|
265
|
+
this.name = 'AssertError'
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function fail(message: string | undefined, detail: string): never {
|
|
270
|
+
throw new AssertError(message ? `${message}: ${detail}` : detail)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function assertOk<T, E>(r: Result<T, E>, message?: string): asserts r is OkResult<T> {
|
|
274
|
+
if (!r.ok) {
|
|
275
|
+
fail(message, `expected ok result, got error: ${formatThrown(r.error)}`)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function assertErr<T, E>(r: Result<T, E>, message?: string): asserts r is ErrResult<E> {
|
|
280
|
+
if (r.ok) {
|
|
281
|
+
fail(message, `expected error result, got ok: ${fmt(r.value)}`)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export const assert: Assert = {
|
|
286
|
+
equal(actual: unknown, expected: unknown, message?: string): void {
|
|
287
|
+
if (!Object.is(actual, expected)) {
|
|
288
|
+
fail(message, `expected ${fmt(expected)}, got ${fmt(actual)}`)
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
notEqual(actual: unknown, unexpected: unknown, message?: string): void {
|
|
293
|
+
if (Object.is(actual, unexpected)) {
|
|
294
|
+
fail(message, `expected value to differ from ${fmt(unexpected)}`)
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
truthy(value: unknown, message?: string): void {
|
|
299
|
+
if (!value) {
|
|
300
|
+
fail(message, `expected truthy, got ${fmt(value)}`)
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
deepEqual(actual: unknown, expected: unknown, message?: string): void {
|
|
305
|
+
const a = JSON.stringify(actual)
|
|
306
|
+
const b = JSON.stringify(expected)
|
|
307
|
+
if (a !== b) {
|
|
308
|
+
fail(message, `expected ${b}, got ${a}`)
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
throws(fn: () => void, message?: string): Error {
|
|
313
|
+
try {
|
|
314
|
+
fn()
|
|
315
|
+
} catch (e) {
|
|
316
|
+
return e instanceof Error ? e : new Error(String(e))
|
|
317
|
+
}
|
|
318
|
+
fail(message, 'expected function to throw')
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
async rejects(fn: () => Promise<unknown>, message?: string): Promise<Error> {
|
|
322
|
+
try {
|
|
323
|
+
await fn()
|
|
324
|
+
} catch (e) {
|
|
325
|
+
return e instanceof Error ? e : new Error(String(e))
|
|
326
|
+
}
|
|
327
|
+
fail(message, 'expected promise to reject')
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
type(value: unknown, expected: string, message?: string): void {
|
|
331
|
+
if (typeof value !== expected) {
|
|
332
|
+
fail(message, `expected typeof ${expected}, got ${typeof value}`)
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
instance(value: unknown, ctor: new (...args: any[]) => any, message?: string): void {
|
|
337
|
+
if (!(value instanceof ctor)) {
|
|
338
|
+
fail(message, `expected instanceof ${ctor.name}, got ${fmt(value)}`)
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
ok: assertOk,
|
|
343
|
+
|
|
344
|
+
err: assertErr,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// --- runner ---
|
|
348
|
+
|
|
349
|
+
const DEFAULT_TEST_TIMEOUT = 10_000
|
|
350
|
+
|
|
351
|
+
type TestEvent =
|
|
352
|
+
| {e: 1; s: string; n: number}
|
|
353
|
+
| {e: 2; s: string; t: string; d: number}
|
|
354
|
+
| {e: 3; s: string; t: string; d: number; m: string}
|
|
355
|
+
| {e: 4; s: string; t: string}
|
|
356
|
+
| {e: 5; s: string}
|
|
357
|
+
| {
|
|
358
|
+
e: 6
|
|
359
|
+
p: number
|
|
360
|
+
f: number
|
|
361
|
+
k: number
|
|
362
|
+
o: number
|
|
363
|
+
d: number
|
|
364
|
+
hb?: number
|
|
365
|
+
ha?: number
|
|
366
|
+
tb?: number
|
|
367
|
+
ta?: number
|
|
368
|
+
pb?: number
|
|
369
|
+
pa?: number
|
|
370
|
+
}
|
|
371
|
+
| {e: 7; s: string; m: string}
|
|
372
|
+
| {e: 8; u: number; t: number; f?: number; mf?: number}
|
|
373
|
+
| {e: 9; s: string; t: string}
|
|
374
|
+
|
|
375
|
+
const emit: (event: TestEvent) => void =
|
|
376
|
+
typeof (globalThis as any).__testEmit === 'function'
|
|
377
|
+
? (event) => (globalThis as any).__testEmit(JSON.stringify(event))
|
|
378
|
+
: (event) => console.log('__TEST__' + JSON.stringify(event))
|
|
379
|
+
|
|
380
|
+
function elapsedMs(start: MonotonicTimestamp): number {
|
|
381
|
+
return Math.round(MonotonicTimestamp.now().since(start))
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Format a thrown value for test output. Handles Error instances,
|
|
385
|
+
* plain objects with `name`/`message` (like our Result error shapes
|
|
386
|
+
* `{name: 'Network', message: '...'}`), and falls back to String(). */
|
|
387
|
+
function formatThrown(e: unknown): string {
|
|
388
|
+
if (e instanceof Error) return e.message
|
|
389
|
+
if (e && typeof e === 'object') {
|
|
390
|
+
const obj = e as {name?: unknown; message?: unknown}
|
|
391
|
+
if (typeof obj.message === 'string') {
|
|
392
|
+
return typeof obj.name === 'string' ? `${obj.name}: ${obj.message}` : obj.message
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return String(e)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function emitHeap(): void {
|
|
399
|
+
gc()
|
|
400
|
+
const mem = memoryUsage()
|
|
401
|
+
const evt: TestEvent = {e: 8, u: mem.heapUsed, t: mem.heapTotal}
|
|
402
|
+
if (mem.systemFree > 0) evt.f = mem.systemFree
|
|
403
|
+
if (mem.systemMinFree > 0) evt.mf = mem.systemMinFree
|
|
404
|
+
emit(evt)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function run(): Promise<void> {
|
|
408
|
+
let passed = 0
|
|
409
|
+
let failed = 0
|
|
410
|
+
let skipped = 0
|
|
411
|
+
let todo = 0
|
|
412
|
+
const startTime = MonotonicTimestamp.now()
|
|
413
|
+
|
|
414
|
+
// Leak-detection baseline: captured after gc(), with all suites already
|
|
415
|
+
// registered from module eval, so persistent harness allocations are
|
|
416
|
+
// included in the baseline rather than counted as growth. The harness
|
|
417
|
+
// recaptures after each suite's beforeAll resolves (see below), so
|
|
418
|
+
// warmup costs (module loads, TLS lazy-init, wifi connection) get
|
|
419
|
+
// folded into the baseline automatically.
|
|
420
|
+
gc()
|
|
421
|
+
heapBaseline = memoryUsage().heapUsed
|
|
422
|
+
const timersBefore = activeTimers()
|
|
423
|
+
const pendingBefore = pendingHttpCount()
|
|
424
|
+
|
|
425
|
+
// If any suite or test is marked .only anywhere, filter everything else.
|
|
426
|
+
const hasOnly = suites.some((s) => s.only || s.tests.some((t) => t.only))
|
|
427
|
+
|
|
428
|
+
for (const suite of suites) {
|
|
429
|
+
emitHeap()
|
|
430
|
+
emit({e: 1, s: suite.name, n: suite.tests.length})
|
|
431
|
+
|
|
432
|
+
if (suite.skip) {
|
|
433
|
+
for (const t of suite.tests) {
|
|
434
|
+
if (t.todo) {
|
|
435
|
+
emit({e: 9, s: suite.name, t: t.name})
|
|
436
|
+
todo++
|
|
437
|
+
} else {
|
|
438
|
+
emit({e: 4, s: suite.name, t: t.name})
|
|
439
|
+
skipped++
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
emit({e: 5, s: suite.name})
|
|
443
|
+
continue
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (suite.todo) {
|
|
447
|
+
for (const t of suite.tests) {
|
|
448
|
+
emit({e: 9, s: suite.name, t: t.name})
|
|
449
|
+
todo++
|
|
450
|
+
}
|
|
451
|
+
emit({e: 5, s: suite.name})
|
|
452
|
+
continue
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Only-mode: skip entire suites that are not .only and contain no .only tests.
|
|
456
|
+
const suiteHasOnlyTest = suite.tests.some((t) => t.only)
|
|
457
|
+
if (hasOnly && !suite.only && !suiteHasOnlyTest) {
|
|
458
|
+
for (const t of suite.tests) {
|
|
459
|
+
if (t.todo) {
|
|
460
|
+
emit({e: 9, s: suite.name, t: t.name})
|
|
461
|
+
todo++
|
|
462
|
+
} else {
|
|
463
|
+
emit({e: 4, s: suite.name, t: t.name})
|
|
464
|
+
skipped++
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
emit({e: 5, s: suite.name})
|
|
468
|
+
continue
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Within a participating suite, if any test is .only, skip the rest.
|
|
472
|
+
const onlyInSuite = hasOnly && suiteHasOnlyTest
|
|
473
|
+
|
|
474
|
+
if (suite.beforeAll) {
|
|
475
|
+
try {
|
|
476
|
+
await suite.beforeAll()
|
|
477
|
+
} catch (e) {
|
|
478
|
+
const msg = formatThrown(e)
|
|
479
|
+
emit({e: 7, s: suite.name, m: msg})
|
|
480
|
+
for (const t of suite.tests) {
|
|
481
|
+
if (t.todo) {
|
|
482
|
+
emit({e: 9, s: suite.name, t: t.name})
|
|
483
|
+
todo++
|
|
484
|
+
} else {
|
|
485
|
+
emit({e: 4, s: suite.name, t: t.name})
|
|
486
|
+
skipped++
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
emit({e: 5, s: suite.name})
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
492
|
+
// Recapture the baseline now that beforeAll has fully resolved and
|
|
493
|
+
// its closure frame is eligible for collection. Multi-suite files
|
|
494
|
+
// let the last successful beforeAll set the baseline — fine because
|
|
495
|
+
// every earlier suite's test allocations have already been bounded
|
|
496
|
+
// by the previous (stricter) baseline.
|
|
497
|
+
await captureHeapBaseline()
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
for (const t of suite.tests) {
|
|
501
|
+
if (t.todo) {
|
|
502
|
+
emit({e: 9, s: suite.name, t: t.name})
|
|
503
|
+
todo++
|
|
504
|
+
continue
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (t.skip) {
|
|
508
|
+
emit({e: 4, s: suite.name, t: t.name})
|
|
509
|
+
skipped++
|
|
510
|
+
continue
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (onlyInSuite && !t.only) {
|
|
514
|
+
emit({e: 4, s: suite.name, t: t.name})
|
|
515
|
+
skipped++
|
|
516
|
+
continue
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
emitHeap()
|
|
520
|
+
const testTimeout = t.timeout ?? DEFAULT_TEST_TIMEOUT
|
|
521
|
+
const testStart = MonotonicTimestamp.now()
|
|
522
|
+
try {
|
|
523
|
+
await new Promise<void>((resolve, reject) => {
|
|
524
|
+
const timer = setTimeout(
|
|
525
|
+
() => reject(new Error(`timeout (${testTimeout}ms)`)),
|
|
526
|
+
testTimeout,
|
|
527
|
+
)
|
|
528
|
+
;(async () => {
|
|
529
|
+
for (const hook of suite.beforeEach) await hook()
|
|
530
|
+
await t.fn()
|
|
531
|
+
})().then(
|
|
532
|
+
() => {
|
|
533
|
+
clearTimeout(timer)
|
|
534
|
+
resolve()
|
|
535
|
+
},
|
|
536
|
+
(e) => {
|
|
537
|
+
clearTimeout(timer)
|
|
538
|
+
reject(e)
|
|
539
|
+
},
|
|
540
|
+
)
|
|
541
|
+
})
|
|
542
|
+
emit({e: 2, s: suite.name, t: t.name, d: elapsedMs(testStart)})
|
|
543
|
+
passed++
|
|
544
|
+
} catch (e) {
|
|
545
|
+
const msg = formatThrown(e)
|
|
546
|
+
emit({e: 3, s: suite.name, t: t.name, d: elapsedMs(testStart), m: msg})
|
|
547
|
+
failed++
|
|
548
|
+
}
|
|
549
|
+
for (const hook of suite.afterEach) {
|
|
550
|
+
try {
|
|
551
|
+
await hook()
|
|
552
|
+
} catch {
|
|
553
|
+
// afterEach errors are not fatal
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (suite.afterAll) {
|
|
559
|
+
try {
|
|
560
|
+
await suite.afterAll()
|
|
561
|
+
} catch {
|
|
562
|
+
// afterAll errors are not fatal
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
emit({e: 5, s: suite.name})
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
gc()
|
|
570
|
+
const heapAfter = memoryUsage().heapUsed
|
|
571
|
+
const timersAfter = activeTimers()
|
|
572
|
+
const pendingAfter = pendingHttpCount()
|
|
573
|
+
|
|
574
|
+
emit({
|
|
575
|
+
e: 6,
|
|
576
|
+
p: passed,
|
|
577
|
+
f: failed,
|
|
578
|
+
k: skipped,
|
|
579
|
+
o: todo,
|
|
580
|
+
d: elapsedMs(startTime),
|
|
581
|
+
hb: heapBaseline,
|
|
582
|
+
ha: heapAfter,
|
|
583
|
+
tb: timersBefore,
|
|
584
|
+
ta: timersAfter,
|
|
585
|
+
pb: pendingBefore,
|
|
586
|
+
pa: pendingAfter,
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
// Signal the supervisor (if any) that this file is complete so it can
|
|
590
|
+
// swap in the next runtime. No-op in non-test-harness contexts.
|
|
591
|
+
;(globalThis as any).__testFileDone?.()
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Auto-run after module evaluation completes
|
|
595
|
+
setTimeout(() => void run(), 0)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {ErrResult, OkResult, Result} from '../result/types.js'
|
|
2
|
+
|
|
3
|
+
export type TestFn = () => void | Promise<void>
|
|
4
|
+
|
|
5
|
+
export interface TestOptions {
|
|
6
|
+
timeout?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Assert {
|
|
10
|
+
/** Strict equality (Object.is) */
|
|
11
|
+
equal(actual: unknown, expected: unknown, message?: string): void
|
|
12
|
+
/** Strict inequality */
|
|
13
|
+
notEqual(actual: unknown, unexpected: unknown, message?: string): void
|
|
14
|
+
/** Truthy check */
|
|
15
|
+
truthy(value: unknown, message?: string): void
|
|
16
|
+
/** Deep equality via JSON.stringify comparison */
|
|
17
|
+
deepEqual(actual: unknown, expected: unknown, message?: string): void
|
|
18
|
+
/** Expect function to throw, returns the error */
|
|
19
|
+
throws(fn: () => void, message?: string): Error
|
|
20
|
+
/** Expect promise to reject, returns the error */
|
|
21
|
+
rejects(fn: () => Promise<unknown>, message?: string): Promise<Error>
|
|
22
|
+
/** typeof check */
|
|
23
|
+
type(value: unknown, expected: string, message?: string): void
|
|
24
|
+
/** instanceof check */
|
|
25
|
+
instance(value: unknown, ctor: new (...args: any[]) => any, message?: string): void
|
|
26
|
+
/** Assert a Result is ok. Narrows r to OkResult; read r.value afterwards. */
|
|
27
|
+
ok<T, E>(r: Result<T, E>, message?: string): asserts r is OkResult<T>
|
|
28
|
+
/** Assert a Result is an error. Narrows r to ErrResult; read r.error afterwards. */
|
|
29
|
+
err<T, E>(r: Result<T, E>, message?: string): asserts r is ErrResult<E>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export declare const assert: Assert
|
|
33
|
+
|
|
34
|
+
export declare function describe(name: string, fn: () => void): void
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- needed for describe.skip() syntax
|
|
36
|
+
export declare namespace describe {
|
|
37
|
+
function skip(name: string, fn: () => void): void
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- needed for describe.skip.each() syntax
|
|
39
|
+
namespace skip {
|
|
40
|
+
function each<T>(cases: T[]): (name: string, fn: (value: T, index: number) => void) => void
|
|
41
|
+
}
|
|
42
|
+
function only(name: string, fn: () => void): void
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- needed for describe.only.each() syntax
|
|
44
|
+
namespace only {
|
|
45
|
+
function each<T>(cases: T[]): (name: string, fn: (value: T, index: number) => void) => void
|
|
46
|
+
}
|
|
47
|
+
function todo(name: string, fn: () => void): void
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- needed for describe.todo.each() syntax
|
|
49
|
+
namespace todo {
|
|
50
|
+
function each<T>(cases: T[]): (name: string, fn: (value: T, index: number) => void) => void
|
|
51
|
+
}
|
|
52
|
+
function fixme(name: string, fn: () => void): void
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- needed for describe.fixme.each() syntax
|
|
54
|
+
namespace fixme {
|
|
55
|
+
function each<T>(cases: T[]): (name: string, fn: (value: T, index: number) => void) => void
|
|
56
|
+
}
|
|
57
|
+
function skipIf(condition: unknown): typeof describe
|
|
58
|
+
function runIf(condition: unknown): typeof describe
|
|
59
|
+
function each<T>(cases: T[]): (name: string, fn: (value: T, index: number) => void) => void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export declare function test(name: string, fn: TestFn, options?: TestOptions): void
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- needed for test.skip() syntax
|
|
64
|
+
export declare namespace test {
|
|
65
|
+
function skip(name: string, fn: TestFn, options?: TestOptions): void
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- needed for test.skip.each() syntax
|
|
67
|
+
namespace skip {
|
|
68
|
+
function each<T>(
|
|
69
|
+
cases: T[],
|
|
70
|
+
): (name: string, fn: (value: T, index: number) => void | Promise<void>) => void
|
|
71
|
+
}
|
|
72
|
+
function only(name: string, fn: TestFn, options?: TestOptions): void
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- needed for test.only.each() syntax
|
|
74
|
+
namespace only {
|
|
75
|
+
function each<T>(
|
|
76
|
+
cases: T[],
|
|
77
|
+
): (name: string, fn: (value: T, index: number) => void | Promise<void>) => void
|
|
78
|
+
}
|
|
79
|
+
function fixme(name: string, fn: TestFn, options?: TestOptions): void
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- needed for test.fixme.each() syntax
|
|
81
|
+
namespace fixme {
|
|
82
|
+
function each<T>(
|
|
83
|
+
cases: T[],
|
|
84
|
+
): (name: string, fn: (value: T, index: number) => void | Promise<void>) => void
|
|
85
|
+
}
|
|
86
|
+
function skipIf(condition: unknown): typeof test
|
|
87
|
+
function runIf(condition: unknown): typeof test
|
|
88
|
+
function todo(name: string): void
|
|
89
|
+
function each<T>(
|
|
90
|
+
cases: T[],
|
|
91
|
+
): (name: string, fn: (value: T, index: number) => void | Promise<void>) => void
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export declare function beforeAll(fn: TestFn): void
|
|
95
|
+
export declare function afterAll(fn: TestFn): void
|
|
96
|
+
export declare function beforeEach(fn: TestFn): void
|
|
97
|
+
export declare function afterEach(fn: TestFn): void
|