@mikrojs/native 0.21.1-next.20260927212749 → 0.22.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/CMakeLists.txt +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/include/mikrojs/mem.h +0 -1
- package/include/mikrojs/mikrojs.h +6 -6
- package/include/mikrojs/platform.h +13 -0
- package/include/mikrojs/private.h +60 -19
- package/include/mikrojs/utils.h +13 -8
- package/package.json +4 -4
- 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/modules.json +1 -1
- package/runtime/sys/types.ts +5 -2
- package/src/mem.cpp +2 -6
- package/src/mik_http_client.cpp +49 -32
- package/src/mik_inspect.cpp +101 -86
- package/src/mik_observable.cpp +26 -6
- package/src/mik_ota_client.cpp +3 -0
- package/src/mik_ota_js_hooks.cpp +3 -0
- package/src/mik_repl.cpp +40 -25
- package/src/mik_result.cpp +51 -43
- package/src/mik_sys.cpp +32 -1
- package/src/mik_test.cpp +1334 -0
- package/src/mik_text_encoding.cpp +77 -87
- package/src/mik_udp.cpp +11 -1
- package/src/mikrojs.cpp +55 -10
- package/src/modules.cpp +1 -0
- package/src/platform_posix.cpp +36 -0
- package/src/timers.cpp +3 -3
- package/src/utils.cpp +14 -9
- package/runtime/test/test.ts +0 -686
- package/src/eval_bytecode.cpp +0 -43
package/runtime/test/test.ts
DELETED
|
@@ -1,686 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
import {gc, memoryUsage, MonotonicTimestamp} from 'mikro/sys'
|
|
3
|
-
import {pendingCount as pendingHttpCount} from 'native:mikro/http'
|
|
4
|
-
import {activeTimers} from 'native:mikro/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
|
-
/** JS heap (bytes) the file still holds above baseline, summed over suites.
|
|
33
|
-
* Retention accumulates, so a per-suite figure is added rather than
|
|
34
|
-
* replacing the last one: a leak in suite 1 is still a leak once suite 5
|
|
35
|
-
* has run. */
|
|
36
|
-
let heapRetained = 0
|
|
37
|
-
/** Baseline the running suite is measured against. Set to the previous
|
|
38
|
-
* suite's closing heap, or recaptured after a beforeAll so that suite's
|
|
39
|
-
* warmup is excluded. */
|
|
40
|
-
let suiteBaseline = 0
|
|
41
|
-
/** Free system heap (bytes) at the start of the run, or 0 on the host (no
|
|
42
|
-
* system heap). Never recaptured: sysUsed is a peak, and warmup (module
|
|
43
|
-
* loads, TLS, wifi) is memory the file genuinely needed at once, so
|
|
44
|
-
* excluding it would understate how close the run came to OOM. Retention is
|
|
45
|
-
* the figure that wants warmup excluded, and it has its own baseline. */
|
|
46
|
-
let sysFreeStart = 0
|
|
47
|
-
/** Lowest free system heap (bytes) sampled (post-gc) this run, or 0 on the
|
|
48
|
-
* host. The file's closest sampled approach to OOM. The module re-inits
|
|
49
|
-
* for each test file (fresh runtime per file), so this is a true per-file
|
|
50
|
-
* figure, not a process-lifetime watermark shared across files. */
|
|
51
|
-
let sysFreeFloor = 0
|
|
52
|
-
/** Free system heap at the running suite's start, for its own peak. */
|
|
53
|
-
let suiteFreeStart = 0
|
|
54
|
-
/** Lowest free system heap sampled during the running suite. */
|
|
55
|
-
let suiteFreeFloor = 0
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Recapture the suite baseline. Called by the harness after each suite's
|
|
59
|
-
* beforeAll resolves so warmup allocations (module loads, fetch/TLS
|
|
60
|
-
* lazy-init, wifi connection) don't count toward heapDelta. A microtask
|
|
61
|
-
* yield before the gc lets the beforeAll async frame's locals become
|
|
62
|
-
* collectible — otherwise the baseline would be inflated by vars that
|
|
63
|
-
* were still pinned by the suspended closure when beforeAll resolved,
|
|
64
|
-
* and the suite would close below its baseline (negative delta).
|
|
65
|
-
*/
|
|
66
|
-
async function captureSuiteBaseline(): Promise<void> {
|
|
67
|
-
await Promise.resolve()
|
|
68
|
-
gc()
|
|
69
|
-
const {heapUsed} = memoryUsage()
|
|
70
|
-
suiteBaseline = heapUsed
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Close out the running suite: fold its retention into the file total and
|
|
75
|
-
* start the next suite from where this one ended. Returns the suite's own
|
|
76
|
-
* figures for the suite_end event, which is what a reader needs to find the
|
|
77
|
-
* suite behind a file-level regression.
|
|
78
|
-
*/
|
|
79
|
-
function closeSuite(): {retained: number; sysUsed: number} {
|
|
80
|
-
gc()
|
|
81
|
-
const {heapUsed, systemFree} = memoryUsage()
|
|
82
|
-
const retained = heapUsed - suiteBaseline
|
|
83
|
-
heapRetained += retained
|
|
84
|
-
suiteBaseline = heapUsed
|
|
85
|
-
if (systemFree > 0 && systemFree < suiteFreeFloor) suiteFreeFloor = systemFree
|
|
86
|
-
const sysUsed = suiteFreeStart > suiteFreeFloor ? suiteFreeStart - suiteFreeFloor : 0
|
|
87
|
-
return {retained, sysUsed}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function newSuite(name: string, flags: {skip?: boolean; only?: boolean; todo?: boolean}): Suite {
|
|
91
|
-
return {
|
|
92
|
-
name,
|
|
93
|
-
tests: [],
|
|
94
|
-
skip: flags.skip ?? false,
|
|
95
|
-
only: flags.only ?? false,
|
|
96
|
-
todo: flags.todo ?? false,
|
|
97
|
-
beforeEach: [],
|
|
98
|
-
afterEach: [],
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function runSuiteFn(suite: Suite, fn: () => void): void {
|
|
103
|
-
suites.push(suite)
|
|
104
|
-
const prev = currentSuite
|
|
105
|
-
currentSuite = suite
|
|
106
|
-
fn()
|
|
107
|
-
currentSuite = prev
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// --- name interpolation for .each ---
|
|
111
|
-
|
|
112
|
-
function interpolateName(template: string, value: unknown, index: number): string {
|
|
113
|
-
let name = template.replaceAll('%#', String(index))
|
|
114
|
-
name = name.replaceAll('%s', String(value))
|
|
115
|
-
if (typeof value === 'object' && value !== null) {
|
|
116
|
-
name = name.replaceAll('%o', JSON.stringify(value))
|
|
117
|
-
}
|
|
118
|
-
return name
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// --- describe ---
|
|
122
|
-
|
|
123
|
-
type EachDescribeFn = <T>(
|
|
124
|
-
cases: T[],
|
|
125
|
-
) => (name: string, fn: (value: T, index: number) => void) => void
|
|
126
|
-
|
|
127
|
-
function _describe(name: string, fn: () => void): void {
|
|
128
|
-
runSuiteFn(newSuite(name, {}), fn)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
_describe.skip = function (name: string, fn: () => void): void {
|
|
132
|
-
runSuiteFn(newSuite(name, {skip: true}), fn)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
_describe.only = function (name: string, fn: () => void): void {
|
|
136
|
-
runSuiteFn(newSuite(name, {only: true}), fn)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
_describe.todo = function (name: string, fn: () => void): void {
|
|
140
|
-
runSuiteFn(newSuite(name, {todo: true}), fn)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
_describe.fixme = function (name: string, fn: () => void): void {
|
|
144
|
-
runSuiteFn(newSuite(name, {skip: true}), fn)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
_describe.skipIf = function (condition: unknown) {
|
|
148
|
-
return condition ? _describe.skip : _describe
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
_describe.runIf = function (condition: unknown) {
|
|
152
|
-
return condition ? _describe : _describe.skip
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function describeEach(
|
|
156
|
-
variant: (name: string, fn: () => void) => void,
|
|
157
|
-
): <T>(cases: T[]) => (name: string, fn: (value: T, index: number) => void) => void {
|
|
158
|
-
return function <T>(cases: T[]) {
|
|
159
|
-
return function (nameTemplate: string, fn: (value: T, index: number) => void): void {
|
|
160
|
-
cases.forEach((value, index) => {
|
|
161
|
-
variant(interpolateName(nameTemplate, value, index), () => fn(value, index))
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
_describe.each = describeEach(_describe) as EachDescribeFn
|
|
168
|
-
;(_describe.skip as any).each = describeEach(_describe.skip) as EachDescribeFn
|
|
169
|
-
;(_describe.only as any).each = describeEach(_describe.only) as EachDescribeFn
|
|
170
|
-
;(_describe.todo as any).each = describeEach(_describe.todo) as EachDescribeFn
|
|
171
|
-
;(_describe.fixme as any).each = describeEach(_describe.fixme) as EachDescribeFn
|
|
172
|
-
|
|
173
|
-
export const describe = _describe as typeof _describe & {
|
|
174
|
-
skip: typeof _describe.skip & {each: EachDescribeFn}
|
|
175
|
-
only: typeof _describe.only & {each: EachDescribeFn}
|
|
176
|
-
todo: typeof _describe.todo & {each: EachDescribeFn}
|
|
177
|
-
fixme: typeof _describe.fixme & {each: EachDescribeFn}
|
|
178
|
-
skipIf: (condition: unknown) => typeof _describe & {each: EachDescribeFn}
|
|
179
|
-
runIf: (condition: unknown) => typeof _describe & {each: EachDescribeFn}
|
|
180
|
-
each: EachDescribeFn
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// --- test ---
|
|
184
|
-
|
|
185
|
-
type EachTestFn = <T>(
|
|
186
|
-
cases: T[],
|
|
187
|
-
) => (name: string, fn: (value: T, index: number) => void | Promise<void>) => void
|
|
188
|
-
|
|
189
|
-
function pushTest(
|
|
190
|
-
where: string,
|
|
191
|
-
name: string,
|
|
192
|
-
fn: TestFn,
|
|
193
|
-
flags: {skip?: boolean; only?: boolean; todo?: boolean},
|
|
194
|
-
options?: TestOptions,
|
|
195
|
-
): void {
|
|
196
|
-
if (!currentSuite) throw new Error(`${where}() must be inside describe()`)
|
|
197
|
-
currentSuite.tests.push({
|
|
198
|
-
name,
|
|
199
|
-
fn,
|
|
200
|
-
skip: flags.skip ?? false,
|
|
201
|
-
only: flags.only ?? false,
|
|
202
|
-
todo: flags.todo ?? false,
|
|
203
|
-
timeout: options?.timeout,
|
|
204
|
-
})
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function _test(name: string, fn: TestFn, options?: TestOptions): void {
|
|
208
|
-
pushTest('test', name, fn, {}, options)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
_test.skip = function (name: string, fn: TestFn, options?: TestOptions): void {
|
|
212
|
-
pushTest('test.skip', name, fn, {skip: true}, options)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
_test.only = function (name: string, fn: TestFn, options?: TestOptions): void {
|
|
216
|
-
pushTest('test.only', name, fn, {only: true}, options)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
_test.fixme = function (name: string, fn: TestFn, options?: TestOptions): void {
|
|
220
|
-
pushTest('test.fixme', name, fn, {skip: true}, options)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
_test.skipIf = function (condition: unknown) {
|
|
224
|
-
return condition ? _test.skip : _test
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
_test.runIf = function (condition: unknown) {
|
|
228
|
-
return condition ? _test : _test.skip
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
_test.todo = function (name: string): void {
|
|
232
|
-
pushTest('test.todo', name, () => {}, {todo: true})
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function testEach(
|
|
236
|
-
variant: (name: string, fn: TestFn) => void,
|
|
237
|
-
): <T>(
|
|
238
|
-
cases: T[],
|
|
239
|
-
) => (name: string, fn: (value: T, index: number) => void | Promise<void>) => void {
|
|
240
|
-
return function <T>(cases: T[]) {
|
|
241
|
-
return function (
|
|
242
|
-
nameTemplate: string,
|
|
243
|
-
fn: (value: T, index: number) => void | Promise<void>,
|
|
244
|
-
): void {
|
|
245
|
-
cases.forEach((value, index) => {
|
|
246
|
-
variant(interpolateName(nameTemplate, value, index), () => fn(value, index))
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
_test.each = testEach(_test) as EachTestFn
|
|
253
|
-
;(_test.skip as any).each = testEach(_test.skip) as EachTestFn
|
|
254
|
-
;(_test.only as any).each = testEach(_test.only) as EachTestFn
|
|
255
|
-
;(_test.fixme as any).each = testEach(_test.fixme) as EachTestFn
|
|
256
|
-
|
|
257
|
-
export const test = _test as typeof _test & {
|
|
258
|
-
skip: typeof _test.skip & {each: EachTestFn}
|
|
259
|
-
only: typeof _test.only & {each: EachTestFn}
|
|
260
|
-
fixme: typeof _test.fixme & {each: EachTestFn}
|
|
261
|
-
skipIf: (condition: unknown) => typeof _test & {each: EachTestFn}
|
|
262
|
-
runIf: (condition: unknown) => typeof _test & {each: EachTestFn}
|
|
263
|
-
todo: (name: string) => void
|
|
264
|
-
each: EachTestFn
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// --- hooks ---
|
|
268
|
-
|
|
269
|
-
export function beforeAll(fn: TestFn): void {
|
|
270
|
-
if (!currentSuite) throw new Error('beforeAll() must be inside describe()')
|
|
271
|
-
currentSuite.beforeAll = fn
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function afterAll(fn: TestFn): void {
|
|
275
|
-
if (!currentSuite) throw new Error('afterAll() must be inside describe()')
|
|
276
|
-
currentSuite.afterAll = fn
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function beforeEach(fn: TestFn): void {
|
|
280
|
-
if (!currentSuite) throw new Error('beforeEach() must be inside describe()')
|
|
281
|
-
currentSuite.beforeEach.push(fn)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export function afterEach(fn: TestFn): void {
|
|
285
|
-
if (!currentSuite) throw new Error('afterEach() must be inside describe()')
|
|
286
|
-
currentSuite.afterEach.push(fn)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// --- assertions ---
|
|
290
|
-
|
|
291
|
-
function fmt(v: unknown): string {
|
|
292
|
-
if (v === undefined) return 'undefined'
|
|
293
|
-
if (v === null) return 'null'
|
|
294
|
-
if (typeof v === 'string') return JSON.stringify(v)
|
|
295
|
-
if (v instanceof Uint8Array) return `Uint8Array[${v.join(', ')}]`
|
|
296
|
-
try {
|
|
297
|
-
return JSON.stringify(v)
|
|
298
|
-
} catch {
|
|
299
|
-
return String(v)
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
class AssertError extends Error {
|
|
304
|
-
constructor(message: string) {
|
|
305
|
-
super(message)
|
|
306
|
-
this.name = 'AssertError'
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function fail(message: string | undefined, detail: string): never {
|
|
311
|
-
throw new AssertError(message ? `${message}: ${detail}` : detail)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function assertOk<T, E>(r: Result<T, E>, message?: string): asserts r is OkResult<T> {
|
|
315
|
-
if (!r.ok) {
|
|
316
|
-
fail(message, `expected ok result, got error: ${formatThrown(r.error)}`)
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function assertErr<T, E>(r: Result<T, E>, message?: string): asserts r is ErrResult<E> {
|
|
321
|
-
if (r.ok) {
|
|
322
|
-
fail(message, `expected error result, got ok: ${fmt(r.value)}`)
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export const assert: Assert = {
|
|
327
|
-
equal(actual: unknown, expected: unknown, message?: string): void {
|
|
328
|
-
if (!Object.is(actual, expected)) {
|
|
329
|
-
fail(message, `expected ${fmt(expected)}, got ${fmt(actual)}`)
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
|
|
333
|
-
notEqual(actual: unknown, unexpected: unknown, message?: string): void {
|
|
334
|
-
if (Object.is(actual, unexpected)) {
|
|
335
|
-
fail(message, `expected value to differ from ${fmt(unexpected)}`)
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
truthy(value: unknown, message?: string): void {
|
|
340
|
-
if (!value) {
|
|
341
|
-
fail(message, `expected truthy, got ${fmt(value)}`)
|
|
342
|
-
}
|
|
343
|
-
},
|
|
344
|
-
|
|
345
|
-
deepEqual(actual: unknown, expected: unknown, message?: string): void {
|
|
346
|
-
const a = JSON.stringify(actual)
|
|
347
|
-
const b = JSON.stringify(expected)
|
|
348
|
-
if (a !== b) {
|
|
349
|
-
fail(message, `expected ${b}, got ${a}`)
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
throws(fn: () => void, message?: string): Error {
|
|
354
|
-
try {
|
|
355
|
-
fn()
|
|
356
|
-
} catch (e) {
|
|
357
|
-
return e instanceof Error ? e : new Error(String(e))
|
|
358
|
-
}
|
|
359
|
-
fail(message, 'expected function to throw')
|
|
360
|
-
},
|
|
361
|
-
|
|
362
|
-
async rejects(fn: () => Promise<unknown>, message?: string): Promise<Error> {
|
|
363
|
-
try {
|
|
364
|
-
await fn()
|
|
365
|
-
} catch (e) {
|
|
366
|
-
return e instanceof Error ? e : new Error(String(e))
|
|
367
|
-
}
|
|
368
|
-
fail(message, 'expected promise to reject')
|
|
369
|
-
},
|
|
370
|
-
|
|
371
|
-
type(value: unknown, expected: string, message?: string): void {
|
|
372
|
-
if (typeof value !== expected) {
|
|
373
|
-
fail(message, `expected typeof ${expected}, got ${typeof value}`)
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
|
|
377
|
-
instance(value: unknown, ctor: new (...args: any[]) => any, message?: string): void {
|
|
378
|
-
if (!(value instanceof ctor)) {
|
|
379
|
-
fail(message, `expected instanceof ${ctor.name}, got ${fmt(value)}`)
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
|
|
383
|
-
ok: assertOk,
|
|
384
|
-
|
|
385
|
-
err: assertErr,
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// --- runner ---
|
|
389
|
-
|
|
390
|
-
const DEFAULT_TEST_TIMEOUT = 10_000
|
|
391
|
-
|
|
392
|
-
type TestEvent =
|
|
393
|
-
| {e: 1; s: string; n: number}
|
|
394
|
-
| {e: 2; s: string; t: string; d: number}
|
|
395
|
-
| {e: 3; s: string; t: string; d: number; m: string}
|
|
396
|
-
| {e: 4; s: string; t: string}
|
|
397
|
-
| {e: 5; s: string; hr?: number; su?: number}
|
|
398
|
-
| {
|
|
399
|
-
e: 6
|
|
400
|
-
p: number
|
|
401
|
-
f: number
|
|
402
|
-
k: number
|
|
403
|
-
o: number
|
|
404
|
-
d: number
|
|
405
|
-
hb?: number
|
|
406
|
-
ha?: number
|
|
407
|
-
hr?: number
|
|
408
|
-
su?: number
|
|
409
|
-
sf?: number
|
|
410
|
-
tb?: number
|
|
411
|
-
ta?: number
|
|
412
|
-
pb?: number
|
|
413
|
-
pa?: number
|
|
414
|
-
}
|
|
415
|
-
| {e: 7; s: string; m: string}
|
|
416
|
-
| {e: 8; u: number; t: number; f?: number; mf?: number}
|
|
417
|
-
| {e: 9; s: string; t: string}
|
|
418
|
-
|
|
419
|
-
const emit: (event: TestEvent) => void =
|
|
420
|
-
typeof (globalThis as any).__testEmit === 'function'
|
|
421
|
-
? (event) => (globalThis as any).__testEmit(JSON.stringify(event))
|
|
422
|
-
: (event) => console.log('__TEST__' + JSON.stringify(event))
|
|
423
|
-
|
|
424
|
-
function elapsedMs(start: MonotonicTimestamp): number {
|
|
425
|
-
return Math.round(MonotonicTimestamp.now().since(start))
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/** Format a thrown value for test output. Handles Error instances,
|
|
429
|
-
* plain objects with `name`/`message` (like our Result error shapes
|
|
430
|
-
* `{name: 'Network', message: '...'}`), and falls back to String(). */
|
|
431
|
-
function formatThrown(e: unknown): string {
|
|
432
|
-
if (e instanceof Error) return e.message
|
|
433
|
-
if (e && typeof e === 'object') {
|
|
434
|
-
const obj = e as {name?: unknown; message?: unknown}
|
|
435
|
-
const name = typeof obj.name === 'string' ? obj.name : undefined
|
|
436
|
-
const message = typeof obj.message === 'string' ? obj.message : undefined
|
|
437
|
-
if (name && message) return `${name}: ${message}`
|
|
438
|
-
if (message) return message
|
|
439
|
-
if (name) return name
|
|
440
|
-
}
|
|
441
|
-
return String(e)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function emitHeap(): void {
|
|
445
|
-
gc()
|
|
446
|
-
const mem = memoryUsage()
|
|
447
|
-
const evt: TestEvent = {e: 8, u: mem.heapUsed, t: mem.heapTotal}
|
|
448
|
-
if (mem.systemFree > 0) {
|
|
449
|
-
evt.f = mem.systemFree
|
|
450
|
-
if (sysFreeFloor === 0 || mem.systemFree < sysFreeFloor) sysFreeFloor = mem.systemFree
|
|
451
|
-
if (suiteFreeFloor === 0 || mem.systemFree < suiteFreeFloor) suiteFreeFloor = mem.systemFree
|
|
452
|
-
}
|
|
453
|
-
if (mem.systemMinFree > 0) evt.mf = mem.systemMinFree
|
|
454
|
-
emit(evt)
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
async function run(): Promise<void> {
|
|
458
|
-
let passed = 0
|
|
459
|
-
let failed = 0
|
|
460
|
-
let skipped = 0
|
|
461
|
-
let todo = 0
|
|
462
|
-
const startTime = MonotonicTimestamp.now()
|
|
463
|
-
|
|
464
|
-
// Leak-detection baseline: captured after gc(), with all suites already
|
|
465
|
-
// registered from module eval, so persistent harness allocations are
|
|
466
|
-
// included in the baseline rather than counted as growth. The harness
|
|
467
|
-
// recaptures after each suite's beforeAll resolves (see below), so
|
|
468
|
-
// warmup costs (module loads, TLS lazy-init, wifi connection) get
|
|
469
|
-
// folded into the baseline automatically.
|
|
470
|
-
gc()
|
|
471
|
-
// Destructure to primitives so the live memoryUsage() object isn't held
|
|
472
|
-
// while the baseline is captured (it would otherwise be counted in it).
|
|
473
|
-
const {heapUsed: startHeap, systemFree: startFree} = memoryUsage()
|
|
474
|
-
heapRetained = 0
|
|
475
|
-
suiteBaseline = startHeap
|
|
476
|
-
if (startFree > 0) {
|
|
477
|
-
sysFreeStart = startFree
|
|
478
|
-
sysFreeFloor = startFree
|
|
479
|
-
}
|
|
480
|
-
const timersBefore = activeTimers()
|
|
481
|
-
const pendingBefore = pendingHttpCount()
|
|
482
|
-
|
|
483
|
-
// If any suite or test is marked .only anywhere, filter everything else.
|
|
484
|
-
const hasOnly = suites.some((s) => s.only || s.tests.some((t) => t.only))
|
|
485
|
-
|
|
486
|
-
for (const suite of suites) {
|
|
487
|
-
// Open this suite's own peak window on the post-gc sample emitHeap takes.
|
|
488
|
-
suiteFreeFloor = 0
|
|
489
|
-
emitHeap()
|
|
490
|
-
suiteFreeStart = suiteFreeFloor
|
|
491
|
-
emit({e: 1, s: suite.name, n: suite.tests.length})
|
|
492
|
-
|
|
493
|
-
if (suite.skip) {
|
|
494
|
-
for (const t of suite.tests) {
|
|
495
|
-
if (t.todo) {
|
|
496
|
-
emit({e: 9, s: suite.name, t: t.name})
|
|
497
|
-
todo++
|
|
498
|
-
} else {
|
|
499
|
-
emit({e: 4, s: suite.name, t: t.name})
|
|
500
|
-
skipped++
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
emit({e: 5, s: suite.name})
|
|
504
|
-
continue
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (suite.todo) {
|
|
508
|
-
for (const t of suite.tests) {
|
|
509
|
-
emit({e: 9, s: suite.name, t: t.name})
|
|
510
|
-
todo++
|
|
511
|
-
}
|
|
512
|
-
emit({e: 5, s: suite.name})
|
|
513
|
-
continue
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Only-mode: skip entire suites that are not .only and contain no .only tests.
|
|
517
|
-
const suiteHasOnlyTest = suite.tests.some((t) => t.only)
|
|
518
|
-
if (hasOnly && !suite.only && !suiteHasOnlyTest) {
|
|
519
|
-
for (const t of suite.tests) {
|
|
520
|
-
if (t.todo) {
|
|
521
|
-
emit({e: 9, s: suite.name, t: t.name})
|
|
522
|
-
todo++
|
|
523
|
-
} else {
|
|
524
|
-
emit({e: 4, s: suite.name, t: t.name})
|
|
525
|
-
skipped++
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
emit({e: 5, s: suite.name})
|
|
529
|
-
continue
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Within a participating suite, if any test is .only, skip the rest.
|
|
533
|
-
const onlyInSuite = hasOnly && suiteHasOnlyTest
|
|
534
|
-
|
|
535
|
-
if (suite.beforeAll) {
|
|
536
|
-
try {
|
|
537
|
-
await suite.beforeAll()
|
|
538
|
-
} catch (e) {
|
|
539
|
-
const msg = formatThrown(e)
|
|
540
|
-
emit({e: 7, s: suite.name, m: msg})
|
|
541
|
-
// Fail the suite's tests rather than skip them: a broken beforeAll is
|
|
542
|
-
// a broken run, and skipping made it indistinguishable from a
|
|
543
|
-
// deliberate gate. The e:7 above carries the real error; the per-test
|
|
544
|
-
// message just points at it.
|
|
545
|
-
for (const t of suite.tests) {
|
|
546
|
-
if (t.todo) {
|
|
547
|
-
emit({e: 9, s: suite.name, t: t.name})
|
|
548
|
-
todo++
|
|
549
|
-
} else {
|
|
550
|
-
emit({e: 3, s: suite.name, t: t.name, d: 0, m: 'beforeAll failed'})
|
|
551
|
-
failed++
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
emit({e: 5, s: suite.name})
|
|
555
|
-
continue
|
|
556
|
-
}
|
|
557
|
-
// Recapture the baseline now that beforeAll has fully resolved and
|
|
558
|
-
// its closure frame is eligible for collection. Only this suite is
|
|
559
|
-
// measured against it: earlier suites were already closed out into
|
|
560
|
-
// heapRetained, so their allocations survive the recapture.
|
|
561
|
-
await captureSuiteBaseline()
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
for (const t of suite.tests) {
|
|
565
|
-
if (t.todo) {
|
|
566
|
-
emit({e: 9, s: suite.name, t: t.name})
|
|
567
|
-
todo++
|
|
568
|
-
continue
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (t.skip) {
|
|
572
|
-
emit({e: 4, s: suite.name, t: t.name})
|
|
573
|
-
skipped++
|
|
574
|
-
continue
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (onlyInSuite && !t.only) {
|
|
578
|
-
emit({e: 4, s: suite.name, t: t.name})
|
|
579
|
-
skipped++
|
|
580
|
-
continue
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
emitHeap()
|
|
584
|
-
const testTimeout = t.timeout ?? DEFAULT_TEST_TIMEOUT
|
|
585
|
-
const testStart = MonotonicTimestamp.now()
|
|
586
|
-
try {
|
|
587
|
-
await new Promise<void>((resolve, reject) => {
|
|
588
|
-
const timer = setTimeout(
|
|
589
|
-
() => reject(new Error(`timeout (${testTimeout}ms)`)),
|
|
590
|
-
testTimeout,
|
|
591
|
-
)
|
|
592
|
-
;(async () => {
|
|
593
|
-
for (const hook of suite.beforeEach) await hook()
|
|
594
|
-
await t.fn()
|
|
595
|
-
})().then(
|
|
596
|
-
() => {
|
|
597
|
-
clearTimeout(timer)
|
|
598
|
-
resolve()
|
|
599
|
-
},
|
|
600
|
-
(e) => {
|
|
601
|
-
clearTimeout(timer)
|
|
602
|
-
reject(e)
|
|
603
|
-
},
|
|
604
|
-
)
|
|
605
|
-
})
|
|
606
|
-
emit({e: 2, s: suite.name, t: t.name, d: elapsedMs(testStart)})
|
|
607
|
-
passed++
|
|
608
|
-
} catch (e) {
|
|
609
|
-
const msg = formatThrown(e)
|
|
610
|
-
emit({e: 3, s: suite.name, t: t.name, d: elapsedMs(testStart), m: msg})
|
|
611
|
-
failed++
|
|
612
|
-
}
|
|
613
|
-
for (const hook of suite.afterEach) {
|
|
614
|
-
try {
|
|
615
|
-
await hook()
|
|
616
|
-
} catch {
|
|
617
|
-
// afterEach errors are not fatal
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (suite.afterAll) {
|
|
623
|
-
try {
|
|
624
|
-
await suite.afterAll()
|
|
625
|
-
} catch {
|
|
626
|
-
// afterAll errors are not fatal
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const closed = closeSuite()
|
|
631
|
-
const endEvt: TestEvent = {e: 5, s: suite.name, hr: closed.retained}
|
|
632
|
-
if (closed.sysUsed > 0) endEvt.su = closed.sysUsed
|
|
633
|
-
emit(endEvt)
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
gc()
|
|
637
|
-
// Destructure to primitives (see baseline capture above) so the live
|
|
638
|
-
// memoryUsage() object isn't held while we build the run_done event.
|
|
639
|
-
const {heapUsed: heapAfter, systemFree: endFree} = memoryUsage()
|
|
640
|
-
if (endFree > 0 && (sysFreeFloor === 0 || endFree < sysFreeFloor)) {
|
|
641
|
-
sysFreeFloor = endFree
|
|
642
|
-
}
|
|
643
|
-
// Fold in whatever ran outside a suite's own accounting: a file with no
|
|
644
|
-
// suites at all, and the skip/todo bookkeeping between them.
|
|
645
|
-
heapRetained += heapAfter - suiteBaseline
|
|
646
|
-
const timersAfter = activeTimers()
|
|
647
|
-
const pendingAfter = pendingHttpCount()
|
|
648
|
-
|
|
649
|
-
// Per-file system-heap figures. sysFreeFloor is the lowest free heap we
|
|
650
|
-
// sampled (post-gc) this run; sysUsed is how far free heap fell from the
|
|
651
|
-
// run's start to that low. They sum back to the starting free, so "peak"
|
|
652
|
-
// and "min free" read as one story. Measured from the start of the run and
|
|
653
|
-
// never rebaselined: unlike retention, a peak wants warmup counted, since
|
|
654
|
-
// memory a beforeAll takes is memory the file needed at once. Samples are
|
|
655
|
-
// taken between tests, so a transient peak inside a single test can dip
|
|
656
|
-
// below what sysFreeFloor saw.
|
|
657
|
-
const sysUsed = sysFreeStart > sysFreeFloor ? sysFreeStart - sysFreeFloor : 0
|
|
658
|
-
|
|
659
|
-
const doneEvt: TestEvent = {
|
|
660
|
-
e: 6,
|
|
661
|
-
p: passed,
|
|
662
|
-
f: failed,
|
|
663
|
-
k: skipped,
|
|
664
|
-
o: todo,
|
|
665
|
-
d: elapsedMs(startTime),
|
|
666
|
-
hb: startHeap,
|
|
667
|
-
ha: heapAfter,
|
|
668
|
-
hr: heapRetained,
|
|
669
|
-
tb: timersBefore,
|
|
670
|
-
ta: timersAfter,
|
|
671
|
-
pb: pendingBefore,
|
|
672
|
-
pa: pendingAfter,
|
|
673
|
-
}
|
|
674
|
-
if (sysFreeStart > 0) {
|
|
675
|
-
doneEvt.su = sysUsed
|
|
676
|
-
doneEvt.sf = sysFreeFloor
|
|
677
|
-
}
|
|
678
|
-
emit(doneEvt)
|
|
679
|
-
|
|
680
|
-
// Signal the supervisor (if any) that this file is complete so it can
|
|
681
|
-
// swap in the next runtime. No-op in non-test-harness contexts.
|
|
682
|
-
;(globalThis as any).__testFileDone?.()
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Auto-run after module evaluation completes
|
|
686
|
-
setTimeout(() => void run(), 0)
|