@mikrojs/native 0.21.1-next.20260928214050 → 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 -0
- package/include/mikrojs/platform.h +6 -0
- package/include/mikrojs/private.h +9 -2
- package/include/mikrojs/utils.h +12 -7
- 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/mik_repl.cpp +3 -2
- package/src/mik_sys.cpp +32 -1
- package/src/mik_test.cpp +1334 -0
- package/src/mik_udp.cpp +4 -1
- package/src/modules.cpp +1 -0
- package/src/utils.cpp +1 -2
- package/runtime/test/test.ts +0 -686
package/src/mik_test.cpp
ADDED
|
@@ -0,0 +1,1334 @@
|
|
|
1
|
+
/* mikro/test: the on-device test runner. Every hook and test settles through
|
|
2
|
+
* Promise.resolve(v).then(ok, err), so the C stack stays flat across a file. */
|
|
3
|
+
|
|
4
|
+
#include <cinttypes>
|
|
5
|
+
#include <cstdio>
|
|
6
|
+
#include <cstring>
|
|
7
|
+
#include <string>
|
|
8
|
+
#include <vector>
|
|
9
|
+
|
|
10
|
+
#include <mikrojs/platform.h>
|
|
11
|
+
#include <mikrojs/private.h>
|
|
12
|
+
#include <mikrojs/utils.h>
|
|
13
|
+
#include <quickjs.h>
|
|
14
|
+
|
|
15
|
+
namespace {
|
|
16
|
+
|
|
17
|
+
enum : int { F_SKIP = 1, F_ONLY = 2, F_TODO = 4, F_FIXME = 8 };
|
|
18
|
+
|
|
19
|
+
/* Steps a promise reaction (or the timeout timer) can advance the run to. */
|
|
20
|
+
enum Step : int {
|
|
21
|
+
STEP_RUN,
|
|
22
|
+
STEP_BEFORE_ALL_OK,
|
|
23
|
+
STEP_BEFORE_ALL_ERR,
|
|
24
|
+
STEP_BASELINE,
|
|
25
|
+
STEP_HOOK_OK,
|
|
26
|
+
STEP_TEST_OK,
|
|
27
|
+
STEP_TEST_ERR,
|
|
28
|
+
STEP_TIMEOUT,
|
|
29
|
+
STEP_AFTER_EACH,
|
|
30
|
+
STEP_AFTER_ALL,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
enum Phase : int { PH_SUITE_BEGIN, PH_TESTS, PH_SUITE_FINISH, PH_DONE };
|
|
34
|
+
|
|
35
|
+
constexpr int64_t DEFAULT_TEST_TIMEOUT_MS = 10000;
|
|
36
|
+
|
|
37
|
+
struct TestCase {
|
|
38
|
+
std::string name;
|
|
39
|
+
JSValue fn;
|
|
40
|
+
bool skip;
|
|
41
|
+
bool only;
|
|
42
|
+
bool todo;
|
|
43
|
+
int64_t timeout_ms;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
struct Suite {
|
|
47
|
+
std::string name;
|
|
48
|
+
std::vector<TestCase> tests;
|
|
49
|
+
bool skip;
|
|
50
|
+
bool only;
|
|
51
|
+
bool todo;
|
|
52
|
+
JSValue before_all;
|
|
53
|
+
JSValue after_all;
|
|
54
|
+
std::vector<JSValue> before_each;
|
|
55
|
+
std::vector<JSValue> after_each;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
struct Registry {
|
|
59
|
+
std::vector<Suite> suites;
|
|
60
|
+
int current = -1; /* suite index describe() is filling, -1 outside */
|
|
61
|
+
JSValue promise_ctor = JS_UNDEFINED;
|
|
62
|
+
|
|
63
|
+
/* Run state. Suites and tests are addressed by index: a JS call may
|
|
64
|
+
* append to `suites`, so references are never held across one. */
|
|
65
|
+
Phase phase = PH_SUITE_BEGIN;
|
|
66
|
+
size_t si = 0;
|
|
67
|
+
size_t ti = 0;
|
|
68
|
+
size_t hook_i = 0;
|
|
69
|
+
uint32_t seq = 0; /* bumped per started test; stale callbacks compare it */
|
|
70
|
+
bool test_settled = false;
|
|
71
|
+
/* True while the 0 ms start timer's callback is on the stack: that timer
|
|
72
|
+
* is still registered then and must not count as the file's. */
|
|
73
|
+
bool in_start = false;
|
|
74
|
+
uint32_t timeout_timer = 0;
|
|
75
|
+
int passed = 0;
|
|
76
|
+
int failed = 0;
|
|
77
|
+
int skipped = 0;
|
|
78
|
+
int todo = 0;
|
|
79
|
+
int64_t start_us = 0;
|
|
80
|
+
int64_t test_start_us = 0;
|
|
81
|
+
/* JS heap: the file's retention is summed over suites, each measured
|
|
82
|
+
* against its own baseline (the previous suite's closing heap, or the
|
|
83
|
+
* heap after its beforeAll so warmup is excluded). */
|
|
84
|
+
int64_t start_heap = 0;
|
|
85
|
+
int64_t suite_baseline = 0;
|
|
86
|
+
int64_t heap_retained = 0;
|
|
87
|
+
/* System heap (0 on hosts without one): the run's start and lowest
|
|
88
|
+
* post-gc sample give the file's peak; never rebaselined, since memory a
|
|
89
|
+
* beforeAll takes is memory the file needed at once. */
|
|
90
|
+
int64_t sys_free_start = 0;
|
|
91
|
+
int64_t sys_free_floor = 0;
|
|
92
|
+
int64_t suite_free_start = 0;
|
|
93
|
+
int64_t suite_free_floor = 0;
|
|
94
|
+
int32_t timers_before = 0;
|
|
95
|
+
int32_t pending_before = 0;
|
|
96
|
+
bool has_only = false;
|
|
97
|
+
bool only_in_suite = false;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
|
101
|
+
JSClassID registry_class_id = 0;
|
|
102
|
+
|
|
103
|
+
Registry* reg_of(JSValueConst obj) {
|
|
104
|
+
return static_cast<Registry*>(JS_GetOpaque(obj, registry_class_id));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
void registry_finalizer(JSRuntime* rt, JSValue val) {
|
|
108
|
+
auto* reg = reg_of(val);
|
|
109
|
+
if (!reg) return;
|
|
110
|
+
JS_FreeValueRT(rt, reg->promise_ctor);
|
|
111
|
+
for (auto& s : reg->suites) {
|
|
112
|
+
JS_FreeValueRT(rt, s.before_all);
|
|
113
|
+
JS_FreeValueRT(rt, s.after_all);
|
|
114
|
+
for (auto& h : s.before_each) JS_FreeValueRT(rt, h);
|
|
115
|
+
for (auto& h : s.after_each) JS_FreeValueRT(rt, h);
|
|
116
|
+
for (auto& t : s.tests) JS_FreeValueRT(rt, t.fn);
|
|
117
|
+
}
|
|
118
|
+
delete reg;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
void registry_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
|
|
122
|
+
auto* reg = reg_of(val);
|
|
123
|
+
if (!reg) return;
|
|
124
|
+
JS_MarkValue(rt, reg->promise_ctor, mark_func);
|
|
125
|
+
for (auto& s : reg->suites) {
|
|
126
|
+
JS_MarkValue(rt, s.before_all, mark_func);
|
|
127
|
+
JS_MarkValue(rt, s.after_all, mark_func);
|
|
128
|
+
for (auto& h : s.before_each) JS_MarkValue(rt, h, mark_func);
|
|
129
|
+
for (auto& h : s.after_each) JS_MarkValue(rt, h, mark_func);
|
|
130
|
+
for (auto& t : s.tests) JS_MarkValue(rt, t.fn, mark_func);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
JSClassDef registry_class_def = {
|
|
135
|
+
"TestRegistry", registry_finalizer, registry_gc_mark, nullptr, nullptr,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/* ── Small helpers ──────────────────────────────────────────────── */
|
|
139
|
+
|
|
140
|
+
void drop_exception(JSContext* ctx) {
|
|
141
|
+
JSValue e = JS_GetException(ctx);
|
|
142
|
+
JS_FreeValue(ctx, e);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* String(v). False (exception pending) when the conversion throws. */
|
|
146
|
+
bool to_string(JSContext* ctx, JSValueConst v, std::string& out) {
|
|
147
|
+
size_t len;
|
|
148
|
+
const char* s = JS_ToCStringLen(ctx, &len, v);
|
|
149
|
+
if (!s) return false;
|
|
150
|
+
out.assign(s, len);
|
|
151
|
+
JS_FreeCString(ctx, s);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* JSON.stringify(v): "undefined" for an undefined result, String(v) when
|
|
156
|
+
* stringify throws (a cyclic value). */
|
|
157
|
+
std::string json_or_string(JSContext* ctx, JSValueConst v) {
|
|
158
|
+
JSValue json = JS_JSONStringify(ctx, v, JS_UNDEFINED, JS_UNDEFINED);
|
|
159
|
+
std::string out;
|
|
160
|
+
if (JS_IsException(json)) {
|
|
161
|
+
drop_exception(ctx);
|
|
162
|
+
if (!to_string(ctx, v, out)) {
|
|
163
|
+
drop_exception(ctx);
|
|
164
|
+
out = "undefined";
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
if (JS_IsUndefined(json)) return "undefined";
|
|
169
|
+
if (!to_string(ctx, json, out)) drop_exception(ctx);
|
|
170
|
+
JS_FreeValue(ctx, json);
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const char* js_typeof(JSContext* ctx, JSValueConst v) {
|
|
175
|
+
if (JS_IsUndefined(v)) return "undefined";
|
|
176
|
+
if (JS_IsNull(v)) return "object";
|
|
177
|
+
if (JS_IsBool(v)) return "boolean";
|
|
178
|
+
if (JS_IsNumber(v)) return "number";
|
|
179
|
+
if (JS_IsBigInt(v)) return "bigint";
|
|
180
|
+
if (JS_IsString(v)) return "string";
|
|
181
|
+
if (JS_IsSymbol(v)) return "symbol";
|
|
182
|
+
if (JS_IsFunction(ctx, v)) return "function";
|
|
183
|
+
return "object";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* The runner's failure text for a thrown value: Error.message, "name:
|
|
187
|
+
* message" for Result-style error shapes, else String(e). */
|
|
188
|
+
std::string format_thrown(JSContext* ctx, JSValueConst e) {
|
|
189
|
+
const MIKResultAtoms& atoms = MIK_GetRuntime(ctx)->result_atoms;
|
|
190
|
+
std::string out;
|
|
191
|
+
if (JS_IsError(e)) {
|
|
192
|
+
JSValue msg = JS_GetProperty(ctx, e, atoms.message);
|
|
193
|
+
if (!to_string(ctx, msg, out)) drop_exception(ctx);
|
|
194
|
+
JS_FreeValue(ctx, msg);
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
if (JS_IsObject(e)) {
|
|
198
|
+
JSValue name_v = JS_GetProperty(ctx, e, atoms.name);
|
|
199
|
+
if (JS_IsException(name_v)) drop_exception(ctx);
|
|
200
|
+
JSValue msg_v = JS_GetProperty(ctx, e, atoms.message);
|
|
201
|
+
if (JS_IsException(msg_v)) drop_exception(ctx);
|
|
202
|
+
std::string name;
|
|
203
|
+
std::string msg;
|
|
204
|
+
bool has_name = JS_IsString(name_v) && to_string(ctx, name_v, name) && !name.empty();
|
|
205
|
+
bool has_msg = JS_IsString(msg_v) && to_string(ctx, msg_v, msg) && !msg.empty();
|
|
206
|
+
JS_FreeValue(ctx, name_v);
|
|
207
|
+
JS_FreeValue(ctx, msg_v);
|
|
208
|
+
if (has_name && has_msg) return name + ": " + msg;
|
|
209
|
+
if (has_msg) return msg;
|
|
210
|
+
if (has_name) return name;
|
|
211
|
+
}
|
|
212
|
+
if (!to_string(ctx, e, out)) drop_exception(ctx);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Text for an assertion message: JSON for most values, Uint8Array[..] for
|
|
217
|
+
* byte arrays (JSON would print them as an object). */
|
|
218
|
+
std::string fmt_value(JSContext* ctx, JSValueConst v) {
|
|
219
|
+
if (JS_IsUndefined(v)) return "undefined";
|
|
220
|
+
if (JS_IsNull(v)) return "null";
|
|
221
|
+
if (JS_IsString(v)) return json_or_string(ctx, v);
|
|
222
|
+
if (JS_GetTypedArrayType(v) == JS_TYPED_ARRAY_UINT8) {
|
|
223
|
+
JSValue sep = JS_NewString(ctx, ", ");
|
|
224
|
+
JSAtom join = JS_NewAtom(ctx, "join");
|
|
225
|
+
JSValue joined = JS_Invoke(ctx, v, join, 1, &sep);
|
|
226
|
+
JS_FreeAtom(ctx, join);
|
|
227
|
+
JS_FreeValue(ctx, sep);
|
|
228
|
+
std::string body;
|
|
229
|
+
if (JS_IsException(joined) || !to_string(ctx, joined, body)) drop_exception(ctx);
|
|
230
|
+
JS_FreeValue(ctx, joined);
|
|
231
|
+
return "Uint8Array[" + body + "]";
|
|
232
|
+
}
|
|
233
|
+
return json_or_string(ctx, v);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ── JSON event lines ───────────────────────────────────────────── */
|
|
237
|
+
|
|
238
|
+
void json_append_string(std::string& out, const std::string& s) {
|
|
239
|
+
out += '"';
|
|
240
|
+
for (unsigned char c : s) {
|
|
241
|
+
switch (c) {
|
|
242
|
+
case '"':
|
|
243
|
+
out += "\\\"";
|
|
244
|
+
break;
|
|
245
|
+
case '\\':
|
|
246
|
+
out += "\\\\";
|
|
247
|
+
break;
|
|
248
|
+
case '\b':
|
|
249
|
+
out += "\\b";
|
|
250
|
+
break;
|
|
251
|
+
case '\f':
|
|
252
|
+
out += "\\f";
|
|
253
|
+
break;
|
|
254
|
+
case '\n':
|
|
255
|
+
out += "\\n";
|
|
256
|
+
break;
|
|
257
|
+
case '\r':
|
|
258
|
+
out += "\\r";
|
|
259
|
+
break;
|
|
260
|
+
case '\t':
|
|
261
|
+
out += "\\t";
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
if (c < 0x20) {
|
|
265
|
+
char buf[8];
|
|
266
|
+
snprintf(buf, sizeof(buf), "\\u%04x", c);
|
|
267
|
+
out += buf;
|
|
268
|
+
} else {
|
|
269
|
+
out += static_cast<char>(c);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
out += '"';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
struct Event {
|
|
277
|
+
std::string buf;
|
|
278
|
+
|
|
279
|
+
explicit Event(int code) { buf = "{\"e\":" + std::to_string(code); }
|
|
280
|
+
|
|
281
|
+
Event& num(const char* key, int64_t v) {
|
|
282
|
+
buf += ",\"";
|
|
283
|
+
buf += key;
|
|
284
|
+
buf += "\":";
|
|
285
|
+
buf += std::to_string(v);
|
|
286
|
+
return *this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
Event& str(const char* key, const std::string& v) {
|
|
290
|
+
buf += ",\"";
|
|
291
|
+
buf += key;
|
|
292
|
+
buf += "\":";
|
|
293
|
+
json_append_string(buf, v);
|
|
294
|
+
return *this;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const std::string& done() {
|
|
298
|
+
buf += '}';
|
|
299
|
+
return buf;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
void emit(JSContext* ctx, Event& ev) {
|
|
304
|
+
const std::string& json = ev.done();
|
|
305
|
+
JSValue g = JS_GetGlobalObject(ctx);
|
|
306
|
+
JSValue fn = JS_GetPropertyStr(ctx, g, "__testEmit");
|
|
307
|
+
JSValue r;
|
|
308
|
+
if (JS_IsFunction(ctx, fn)) {
|
|
309
|
+
JSValue s = JS_NewStringLen(ctx, json.data(), json.size());
|
|
310
|
+
r = JS_Call(ctx, fn, JS_UNDEFINED, 1, &s);
|
|
311
|
+
JS_FreeValue(ctx, s);
|
|
312
|
+
} else {
|
|
313
|
+
JSValue console = JS_GetPropertyStr(ctx, g, "console");
|
|
314
|
+
JSValue log = JS_GetPropertyStr(ctx, console, "log");
|
|
315
|
+
std::string line = "__TEST__" + json;
|
|
316
|
+
JSValue s = JS_NewStringLen(ctx, line.data(), line.size());
|
|
317
|
+
r = JS_Call(ctx, log, console, 1, &s);
|
|
318
|
+
JS_FreeValue(ctx, s);
|
|
319
|
+
JS_FreeValue(ctx, log);
|
|
320
|
+
JS_FreeValue(ctx, console);
|
|
321
|
+
}
|
|
322
|
+
if (JS_IsException(r)) drop_exception(ctx);
|
|
323
|
+
JS_FreeValue(ctx, r);
|
|
324
|
+
JS_FreeValue(ctx, fn);
|
|
325
|
+
JS_FreeValue(ctx, g);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ── Registration ───────────────────────────────────────────────── */
|
|
329
|
+
|
|
330
|
+
const char* test_where(int flags) {
|
|
331
|
+
if (flags & F_FIXME) return "test.fixme";
|
|
332
|
+
if (flags & F_SKIP) return "test.skip";
|
|
333
|
+
if (flags & F_ONLY) return "test.only";
|
|
334
|
+
if (flags & F_TODO) return "test.todo";
|
|
335
|
+
return "test";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Register a suite and run its body with it as the current suite. */
|
|
339
|
+
JSValue run_suite_fn(JSContext* ctx, Registry* reg, int flags, std::string name, JSValueConst fn,
|
|
340
|
+
int argc, JSValueConst* argv) {
|
|
341
|
+
Suite s;
|
|
342
|
+
s.name = std::move(name);
|
|
343
|
+
s.skip = (flags & (F_SKIP | F_FIXME)) != 0;
|
|
344
|
+
s.only = (flags & F_ONLY) != 0;
|
|
345
|
+
s.todo = (flags & F_TODO) != 0;
|
|
346
|
+
s.before_all = JS_UNDEFINED;
|
|
347
|
+
s.after_all = JS_UNDEFINED;
|
|
348
|
+
reg->suites.push_back(std::move(s));
|
|
349
|
+
int prev = reg->current;
|
|
350
|
+
reg->current = static_cast<int>(reg->suites.size()) - 1;
|
|
351
|
+
JSValue r = JS_Call(ctx, fn, JS_UNDEFINED, argc, argv);
|
|
352
|
+
reg->current = prev;
|
|
353
|
+
if (JS_IsException(r)) return r;
|
|
354
|
+
JS_FreeValue(ctx, r);
|
|
355
|
+
return JS_UNDEFINED;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
JSValue push_test(JSContext* ctx, Registry* reg, int flags, std::string name, JSValueConst fn,
|
|
359
|
+
JSValueConst options) {
|
|
360
|
+
if (reg->current < 0) {
|
|
361
|
+
return JS_ThrowPlainError(ctx, "%s() must be inside describe()", test_where(flags));
|
|
362
|
+
}
|
|
363
|
+
TestCase t;
|
|
364
|
+
t.name = std::move(name);
|
|
365
|
+
t.fn = JS_DupValue(ctx, fn);
|
|
366
|
+
t.skip = (flags & (F_SKIP | F_FIXME)) != 0;
|
|
367
|
+
t.only = (flags & F_ONLY) != 0;
|
|
368
|
+
t.todo = (flags & F_TODO) != 0;
|
|
369
|
+
t.timeout_ms = DEFAULT_TEST_TIMEOUT_MS;
|
|
370
|
+
if (JS_IsObject(options)) {
|
|
371
|
+
JSValue timeout = JS_GetPropertyStr(ctx, options, "timeout");
|
|
372
|
+
if (JS_IsException(timeout)) {
|
|
373
|
+
JS_FreeValue(ctx, t.fn);
|
|
374
|
+
return JS_EXCEPTION;
|
|
375
|
+
}
|
|
376
|
+
/* Any number counts, negative included: a bad value should fail the
|
|
377
|
+
* test at once, not wait out the default. */
|
|
378
|
+
if (JS_IsNumber(timeout)) JS_ToInt64(ctx, &t.timeout_ms, timeout);
|
|
379
|
+
JS_FreeValue(ctx, timeout);
|
|
380
|
+
}
|
|
381
|
+
reg->suites[static_cast<size_t>(reg->current)].tests.push_back(std::move(t));
|
|
382
|
+
return JS_UNDEFINED;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* describe(name, fn) and its skip/only/todo/fixme variants (magic = flags). */
|
|
386
|
+
JSValue describe_cb(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic,
|
|
387
|
+
JSValue* func_data) {
|
|
388
|
+
(void)this_val;
|
|
389
|
+
std::string name;
|
|
390
|
+
if (!to_string(ctx, argc > 0 ? argv[0] : JS_UNDEFINED, name)) return JS_EXCEPTION;
|
|
391
|
+
return run_suite_fn(ctx, reg_of(func_data[0]), magic, std::move(name),
|
|
392
|
+
argc > 1 ? argv[1] : JS_UNDEFINED, 0, nullptr);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* test(name, fn, options?) and its variants; test.todo(name) takes no fn. */
|
|
396
|
+
JSValue test_cb(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic,
|
|
397
|
+
JSValue* func_data) {
|
|
398
|
+
(void)this_val;
|
|
399
|
+
Registry* reg = reg_of(func_data[0]);
|
|
400
|
+
if (reg->current < 0) {
|
|
401
|
+
return JS_ThrowPlainError(ctx, "%s() must be inside describe()", test_where(magic));
|
|
402
|
+
}
|
|
403
|
+
std::string name;
|
|
404
|
+
if (!to_string(ctx, argc > 0 ? argv[0] : JS_UNDEFINED, name)) return JS_EXCEPTION;
|
|
405
|
+
return push_test(ctx, reg, magic, std::move(name), argc > 1 ? argv[1] : JS_UNDEFINED,
|
|
406
|
+
argc > 2 ? argv[2] : JS_UNDEFINED);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* Name template for .each: %# is the index, %s String(value), and %o
|
|
410
|
+
* JSON.stringify(value) for object values. */
|
|
411
|
+
void replace_all(std::string& s, const char* from, const std::string& to) {
|
|
412
|
+
size_t n = strlen(from);
|
|
413
|
+
size_t at = 0;
|
|
414
|
+
while ((at = s.find(from, at)) != std::string::npos) {
|
|
415
|
+
s.replace(at, n, to);
|
|
416
|
+
at += to.size();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
bool interpolate_name(JSContext* ctx, const std::string& tmpl, JSValueConst value, uint32_t index,
|
|
421
|
+
std::string& out) {
|
|
422
|
+
out = tmpl;
|
|
423
|
+
replace_all(out, "%#", std::to_string(index));
|
|
424
|
+
std::string sv;
|
|
425
|
+
if (!to_string(ctx, value, sv)) return false;
|
|
426
|
+
replace_all(out, "%s", sv);
|
|
427
|
+
if (JS_IsObject(value) && !JS_IsFunction(ctx, value)) {
|
|
428
|
+
JSValue json = JS_JSONStringify(ctx, value, JS_UNDEFINED, JS_UNDEFINED);
|
|
429
|
+
if (JS_IsException(json)) return false;
|
|
430
|
+
std::string js = "undefined";
|
|
431
|
+
if (!JS_IsUndefined(json) && !to_string(ctx, json, js)) {
|
|
432
|
+
JS_FreeValue(ctx, json);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
JS_FreeValue(ctx, json);
|
|
436
|
+
replace_all(out, "%o", js);
|
|
437
|
+
}
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* The wrapper a test.each case registers: () => fn(value, index). */
|
|
442
|
+
JSValue each_wrapper_cb(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
|
|
443
|
+
int magic, JSValue* func_data) {
|
|
444
|
+
(void)this_val;
|
|
445
|
+
(void)argc;
|
|
446
|
+
(void)argv;
|
|
447
|
+
(void)magic;
|
|
448
|
+
JSValue args[2] = {func_data[1], func_data[2]};
|
|
449
|
+
return JS_Call(ctx, func_data[0], JS_UNDEFINED, 2, args);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* (describe|test).<variant>.each(cases)(name, fn). func_data: [registry, cases];
|
|
453
|
+
* magic: flags, plus EACH_TEST when registering tests rather than suites. */
|
|
454
|
+
constexpr int EACH_TEST = 1 << 8;
|
|
455
|
+
|
|
456
|
+
JSValue each_bound_cb(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic,
|
|
457
|
+
JSValue* func_data) {
|
|
458
|
+
(void)this_val;
|
|
459
|
+
Registry* reg = reg_of(func_data[0]);
|
|
460
|
+
JSValueConst cases = func_data[1];
|
|
461
|
+
int flags = magic & 0xff;
|
|
462
|
+
bool is_test = (magic & EACH_TEST) != 0;
|
|
463
|
+
std::string tmpl;
|
|
464
|
+
if (!to_string(ctx, argc > 0 ? argv[0] : JS_UNDEFINED, tmpl)) return JS_EXCEPTION;
|
|
465
|
+
JSValueConst fn = argc > 1 ? argv[1] : JS_UNDEFINED;
|
|
466
|
+
JSValue len_v = JS_GetPropertyStr(ctx, cases, "length");
|
|
467
|
+
uint32_t len = 0;
|
|
468
|
+
if (JS_IsException(len_v) || JS_ToUint32(ctx, &len, len_v) < 0) {
|
|
469
|
+
JS_FreeValue(ctx, len_v);
|
|
470
|
+
return JS_EXCEPTION;
|
|
471
|
+
}
|
|
472
|
+
JS_FreeValue(ctx, len_v);
|
|
473
|
+
for (uint32_t i = 0; i < len; i++) {
|
|
474
|
+
JSValue value = JS_GetPropertyUint32(ctx, cases, i);
|
|
475
|
+
if (JS_IsException(value)) return JS_EXCEPTION;
|
|
476
|
+
std::string name;
|
|
477
|
+
if (!interpolate_name(ctx, tmpl, value, i, name)) {
|
|
478
|
+
JS_FreeValue(ctx, value);
|
|
479
|
+
return JS_EXCEPTION;
|
|
480
|
+
}
|
|
481
|
+
JSValue r;
|
|
482
|
+
if (is_test) {
|
|
483
|
+
JSValue index = JS_NewUint32(ctx, i);
|
|
484
|
+
JSValue data[3] = {fn, value, index};
|
|
485
|
+
JSValue wrapper = JS_NewCFunctionData(ctx, each_wrapper_cb, 0, 0, 3, data);
|
|
486
|
+
JS_FreeValue(ctx, index);
|
|
487
|
+
r = push_test(ctx, reg, flags, std::move(name), wrapper, JS_UNDEFINED);
|
|
488
|
+
JS_FreeValue(ctx, wrapper);
|
|
489
|
+
} else {
|
|
490
|
+
JSValue index = JS_NewUint32(ctx, i);
|
|
491
|
+
JSValue args[2] = {value, index};
|
|
492
|
+
r = run_suite_fn(ctx, reg, flags, name, fn, 2, args);
|
|
493
|
+
JS_FreeValue(ctx, index);
|
|
494
|
+
}
|
|
495
|
+
JS_FreeValue(ctx, value);
|
|
496
|
+
if (JS_IsException(r)) return r;
|
|
497
|
+
}
|
|
498
|
+
return JS_UNDEFINED;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
JSValue each_cb(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic,
|
|
502
|
+
JSValue* func_data) {
|
|
503
|
+
(void)this_val;
|
|
504
|
+
JSValue data[2] = {func_data[0], argc > 0 ? argv[0] : JS_UNDEFINED};
|
|
505
|
+
return JS_NewCFunctionData(ctx, each_bound_cb, 2, magic, 2, data);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* skipIf(cond) / runIf(cond): func_data = [plain, skip]; magic 1 = runIf. */
|
|
509
|
+
JSValue pick_cb(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic,
|
|
510
|
+
JSValue* func_data) {
|
|
511
|
+
(void)this_val;
|
|
512
|
+
bool cond = argc > 0 && JS_ToBool(ctx, argv[0]) > 0;
|
|
513
|
+
bool skip = magic ? !cond : cond;
|
|
514
|
+
return JS_DupValue(ctx, func_data[skip ? 1 : 0]);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
enum HookKind : int { HOOK_BEFORE_ALL, HOOK_AFTER_ALL, HOOK_BEFORE_EACH, HOOK_AFTER_EACH };
|
|
518
|
+
const char* const hook_names[] = {"beforeAll", "afterAll", "beforeEach", "afterEach"};
|
|
519
|
+
|
|
520
|
+
JSValue hook_cb(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic,
|
|
521
|
+
JSValue* func_data) {
|
|
522
|
+
(void)this_val;
|
|
523
|
+
Registry* reg = reg_of(func_data[0]);
|
|
524
|
+
if (reg->current < 0) {
|
|
525
|
+
return JS_ThrowPlainError(ctx, "%s() must be inside describe()", hook_names[magic]);
|
|
526
|
+
}
|
|
527
|
+
Suite& s = reg->suites[static_cast<size_t>(reg->current)];
|
|
528
|
+
JSValue fn = JS_DupValue(ctx, argc > 0 ? argv[0] : JS_UNDEFINED);
|
|
529
|
+
switch (magic) {
|
|
530
|
+
case HOOK_BEFORE_ALL:
|
|
531
|
+
JS_FreeValue(ctx, s.before_all);
|
|
532
|
+
s.before_all = fn;
|
|
533
|
+
break;
|
|
534
|
+
case HOOK_AFTER_ALL:
|
|
535
|
+
JS_FreeValue(ctx, s.after_all);
|
|
536
|
+
s.after_all = fn;
|
|
537
|
+
break;
|
|
538
|
+
case HOOK_BEFORE_EACH:
|
|
539
|
+
s.before_each.push_back(fn);
|
|
540
|
+
break;
|
|
541
|
+
default:
|
|
542
|
+
s.after_each.push_back(fn);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
return JS_UNDEFINED;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* ── Assertions ─────────────────────────────────────────────────── */
|
|
549
|
+
|
|
550
|
+
/* Throw an AssertError: `message: detail` when a message was given. */
|
|
551
|
+
JSValue fail(JSContext* ctx, JSValueConst message, const std::string& detail) {
|
|
552
|
+
std::string text;
|
|
553
|
+
if (JS_ToBool(ctx, message) > 0) {
|
|
554
|
+
if (!to_string(ctx, message, text)) return JS_EXCEPTION;
|
|
555
|
+
text += ": ";
|
|
556
|
+
}
|
|
557
|
+
text += detail;
|
|
558
|
+
JSValue g = JS_GetGlobalObject(ctx);
|
|
559
|
+
JSValue error_ctor = JS_GetPropertyStr(ctx, g, "Error");
|
|
560
|
+
JS_FreeValue(ctx, g);
|
|
561
|
+
JSValue msg = JS_NewStringLen(ctx, text.data(), text.size());
|
|
562
|
+
JSValue err = JS_CallConstructor(ctx, error_ctor, 1, &msg);
|
|
563
|
+
JS_FreeValue(ctx, msg);
|
|
564
|
+
JS_FreeValue(ctx, error_ctor);
|
|
565
|
+
if (JS_IsException(err)) return err;
|
|
566
|
+
JS_SetPropertyStr(ctx, err, "name", JS_NewString(ctx, "AssertError"));
|
|
567
|
+
return JS_Throw(ctx, err);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
JSValueConst arg(int argc, JSValueConst* argv, int i) {
|
|
571
|
+
return i < argc ? argv[i] : JS_UNDEFINED;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* e instanceof Error ? e : new Error(String(e)). Consumes e. */
|
|
575
|
+
JSValue as_error(JSContext* ctx, JSValue e) {
|
|
576
|
+
if (JS_IsError(e)) return e;
|
|
577
|
+
std::string text;
|
|
578
|
+
if (!to_string(ctx, e, text)) drop_exception(ctx);
|
|
579
|
+
JS_FreeValue(ctx, e);
|
|
580
|
+
JSValue g = JS_GetGlobalObject(ctx);
|
|
581
|
+
JSValue error_ctor = JS_GetPropertyStr(ctx, g, "Error");
|
|
582
|
+
JS_FreeValue(ctx, g);
|
|
583
|
+
JSValue msg = JS_NewStringLen(ctx, text.data(), text.size());
|
|
584
|
+
JSValue err = JS_CallConstructor(ctx, error_ctor, 1, &msg);
|
|
585
|
+
JS_FreeValue(ctx, msg);
|
|
586
|
+
JS_FreeValue(ctx, error_ctor);
|
|
587
|
+
return err;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
JSValue assert_equal(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
591
|
+
(void)this_val;
|
|
592
|
+
JSValueConst actual = arg(argc, argv, 0);
|
|
593
|
+
JSValueConst expected = arg(argc, argv, 1);
|
|
594
|
+
if (JS_IsSameValue(ctx, actual, expected)) return JS_UNDEFINED;
|
|
595
|
+
return fail(ctx, arg(argc, argv, 2),
|
|
596
|
+
"expected " + fmt_value(ctx, expected) + ", got " + fmt_value(ctx, actual));
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
JSValue assert_not_equal(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
600
|
+
(void)this_val;
|
|
601
|
+
JSValueConst actual = arg(argc, argv, 0);
|
|
602
|
+
JSValueConst unexpected = arg(argc, argv, 1);
|
|
603
|
+
if (!JS_IsSameValue(ctx, actual, unexpected)) return JS_UNDEFINED;
|
|
604
|
+
return fail(ctx, arg(argc, argv, 2),
|
|
605
|
+
"expected value to differ from " + fmt_value(ctx, unexpected));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
JSValue assert_truthy(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
609
|
+
(void)this_val;
|
|
610
|
+
JSValueConst value = arg(argc, argv, 0);
|
|
611
|
+
if (JS_ToBool(ctx, value) > 0) return JS_UNDEFINED;
|
|
612
|
+
return fail(ctx, arg(argc, argv, 1), "expected truthy, got " + fmt_value(ctx, value));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/* Deep equality is JSON equality, as documented. */
|
|
616
|
+
JSValue assert_deep_equal(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
617
|
+
(void)this_val;
|
|
618
|
+
JSValue a = JS_JSONStringify(ctx, arg(argc, argv, 0), JS_UNDEFINED, JS_UNDEFINED);
|
|
619
|
+
if (JS_IsException(a)) return a;
|
|
620
|
+
JSValue b = JS_JSONStringify(ctx, arg(argc, argv, 1), JS_UNDEFINED, JS_UNDEFINED);
|
|
621
|
+
if (JS_IsException(b)) {
|
|
622
|
+
JS_FreeValue(ctx, a);
|
|
623
|
+
return b;
|
|
624
|
+
}
|
|
625
|
+
bool same = JS_IsUndefined(a) && JS_IsUndefined(b);
|
|
626
|
+
std::string sa = "undefined";
|
|
627
|
+
std::string sb = "undefined";
|
|
628
|
+
if (!same) {
|
|
629
|
+
if (!JS_IsUndefined(a) && !to_string(ctx, a, sa)) drop_exception(ctx);
|
|
630
|
+
if (!JS_IsUndefined(b) && !to_string(ctx, b, sb)) drop_exception(ctx);
|
|
631
|
+
same = !JS_IsUndefined(a) && !JS_IsUndefined(b) && sa == sb;
|
|
632
|
+
}
|
|
633
|
+
JS_FreeValue(ctx, a);
|
|
634
|
+
JS_FreeValue(ctx, b);
|
|
635
|
+
if (same) return JS_UNDEFINED;
|
|
636
|
+
return fail(ctx, arg(argc, argv, 2), "expected " + sb + ", got " + sa);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
JSValue assert_throws(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
640
|
+
(void)this_val;
|
|
641
|
+
JSValue r = JS_Call(ctx, arg(argc, argv, 0), JS_UNDEFINED, 0, nullptr);
|
|
642
|
+
if (JS_IsException(r)) return as_error(ctx, JS_GetException(ctx));
|
|
643
|
+
JS_FreeValue(ctx, r);
|
|
644
|
+
return fail(ctx, arg(argc, argv, 1), "expected function to throw");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
JSValue rejects_fulfilled(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
|
|
648
|
+
int magic, JSValue* func_data) {
|
|
649
|
+
(void)this_val;
|
|
650
|
+
(void)argc;
|
|
651
|
+
(void)argv;
|
|
652
|
+
(void)magic;
|
|
653
|
+
return fail(ctx, func_data[0], "expected promise to reject");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
JSValue rejects_rejected(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
657
|
+
(void)this_val;
|
|
658
|
+
return as_error(ctx, JS_DupValue(ctx, arg(argc, argv, 0)));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
JSValue promise_resolve(JSContext* ctx, JSValueConst promise_ctor, JSValue v);
|
|
662
|
+
|
|
663
|
+
/* rejects(fn, message?): resolves with the rejection as an Error, rejects
|
|
664
|
+
* with an AssertError when the promise fulfils. */
|
|
665
|
+
JSValue assert_rejects(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
666
|
+
(void)this_val;
|
|
667
|
+
JSValue g = JS_GetGlobalObject(ctx);
|
|
668
|
+
JSValue promise_ctor = JS_GetPropertyStr(ctx, g, "Promise");
|
|
669
|
+
JS_FreeValue(ctx, g);
|
|
670
|
+
JSValue r = JS_Call(ctx, arg(argc, argv, 0), JS_UNDEFINED, 0, nullptr);
|
|
671
|
+
if (JS_IsException(r)) {
|
|
672
|
+
/* A synchronous throw is a rejection too. */
|
|
673
|
+
JSValue err = as_error(ctx, JS_GetException(ctx));
|
|
674
|
+
JSValue p = promise_resolve(ctx, promise_ctor, err);
|
|
675
|
+
JS_FreeValue(ctx, promise_ctor);
|
|
676
|
+
return p;
|
|
677
|
+
}
|
|
678
|
+
JSValue p = promise_resolve(ctx, promise_ctor, r);
|
|
679
|
+
JS_FreeValue(ctx, promise_ctor);
|
|
680
|
+
if (JS_IsException(p)) return p;
|
|
681
|
+
JSValueConst message = arg(argc, argv, 1);
|
|
682
|
+
JSValue on_ok = JS_NewCFunctionData(ctx, rejects_fulfilled, 1, 0, 1, &message);
|
|
683
|
+
JSValue on_err = JS_NewCFunction(ctx, rejects_rejected, "rejected", 1);
|
|
684
|
+
JSValue handlers[2] = {on_ok, on_err};
|
|
685
|
+
JSAtom then = JS_NewAtom(ctx, "then");
|
|
686
|
+
JSValue derived = JS_Invoke(ctx, p, then, 2, handlers);
|
|
687
|
+
JS_FreeAtom(ctx, then);
|
|
688
|
+
JS_FreeValue(ctx, on_ok);
|
|
689
|
+
JS_FreeValue(ctx, on_err);
|
|
690
|
+
JS_FreeValue(ctx, p);
|
|
691
|
+
return derived;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
JSValue assert_type(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
695
|
+
(void)this_val;
|
|
696
|
+
const char* actual = js_typeof(ctx, arg(argc, argv, 0));
|
|
697
|
+
std::string expected;
|
|
698
|
+
if (!to_string(ctx, arg(argc, argv, 1), expected)) return JS_EXCEPTION;
|
|
699
|
+
if (expected == actual) return JS_UNDEFINED;
|
|
700
|
+
return fail(ctx, arg(argc, argv, 2), "expected typeof " + expected + ", got " + actual);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
JSValue assert_instance(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
704
|
+
(void)this_val;
|
|
705
|
+
JSValueConst value = arg(argc, argv, 0);
|
|
706
|
+
JSValueConst ctor = arg(argc, argv, 1);
|
|
707
|
+
int is = JS_IsInstanceOf(ctx, value, ctor);
|
|
708
|
+
if (is < 0) return JS_EXCEPTION;
|
|
709
|
+
if (is) return JS_UNDEFINED;
|
|
710
|
+
JSValue name_v = JS_GetPropertyStr(ctx, ctor, "name");
|
|
711
|
+
std::string name;
|
|
712
|
+
if (JS_IsException(name_v) || !to_string(ctx, name_v, name)) drop_exception(ctx);
|
|
713
|
+
JS_FreeValue(ctx, name_v);
|
|
714
|
+
return fail(ctx, arg(argc, argv, 2),
|
|
715
|
+
"expected instanceof " + name + ", got " + fmt_value(ctx, value));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/* ok(result) / err(result): magic 1 expects an error result. */
|
|
719
|
+
JSValue assert_result(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv,
|
|
720
|
+
int magic) {
|
|
721
|
+
(void)this_val;
|
|
722
|
+
JSValueConst r = arg(argc, argv, 0);
|
|
723
|
+
const MIKResultAtoms& atoms = MIK_GetRuntime(ctx)->result_atoms;
|
|
724
|
+
JSValue ok_v = JS_GetProperty(ctx, r, atoms.ok);
|
|
725
|
+
if (JS_IsException(ok_v)) return ok_v;
|
|
726
|
+
bool ok = JS_ToBool(ctx, ok_v) > 0;
|
|
727
|
+
JS_FreeValue(ctx, ok_v);
|
|
728
|
+
if (ok != (magic == 1)) return JS_UNDEFINED;
|
|
729
|
+
if (magic == 0) {
|
|
730
|
+
JSValue error = JS_GetProperty(ctx, r, atoms.error);
|
|
731
|
+
std::string detail = "expected ok result, got error: " + format_thrown(ctx, error);
|
|
732
|
+
JS_FreeValue(ctx, error);
|
|
733
|
+
return fail(ctx, arg(argc, argv, 1), detail);
|
|
734
|
+
}
|
|
735
|
+
JSValue value = JS_GetProperty(ctx, r, atoms.value);
|
|
736
|
+
std::string detail = "expected error result, got ok: " + fmt_value(ctx, value);
|
|
737
|
+
JS_FreeValue(ctx, value);
|
|
738
|
+
return fail(ctx, arg(argc, argv, 1), detail);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
JSValue make_assert(JSContext* ctx) {
|
|
742
|
+
JSValue a = JS_NewObject(ctx);
|
|
743
|
+
JS_SetPropertyStr(ctx, a, "equal", JS_NewCFunction(ctx, assert_equal, "equal", 3));
|
|
744
|
+
JS_SetPropertyStr(ctx, a, "notEqual", JS_NewCFunction(ctx, assert_not_equal, "notEqual", 3));
|
|
745
|
+
JS_SetPropertyStr(ctx, a, "truthy", JS_NewCFunction(ctx, assert_truthy, "truthy", 2));
|
|
746
|
+
JS_SetPropertyStr(ctx, a, "deepEqual", JS_NewCFunction(ctx, assert_deep_equal, "deepEqual", 3));
|
|
747
|
+
JS_SetPropertyStr(ctx, a, "throws", JS_NewCFunction(ctx, assert_throws, "throws", 2));
|
|
748
|
+
JS_SetPropertyStr(ctx, a, "rejects", JS_NewCFunction(ctx, assert_rejects, "rejects", 2));
|
|
749
|
+
JS_SetPropertyStr(ctx, a, "type", JS_NewCFunction(ctx, assert_type, "type", 3));
|
|
750
|
+
JS_SetPropertyStr(ctx, a, "instance", JS_NewCFunction(ctx, assert_instance, "instance", 3));
|
|
751
|
+
JS_SetPropertyStr(ctx, a, "ok",
|
|
752
|
+
JS_NewCFunctionMagic(ctx, assert_result, "ok", 2, JS_CFUNC_generic_magic, 0));
|
|
753
|
+
JS_SetPropertyStr(ctx, a, "err",
|
|
754
|
+
JS_NewCFunctionMagic(ctx, assert_result, "err", 2, JS_CFUNC_generic_magic, 1));
|
|
755
|
+
return a;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/* ── Runner ─────────────────────────────────────────────────────── */
|
|
759
|
+
|
|
760
|
+
/* Promise.resolve(v). Consumes v. */
|
|
761
|
+
JSValue promise_resolve(JSContext* ctx, JSValueConst promise_ctor, JSValue v) {
|
|
762
|
+
JSAtom resolve = JS_NewAtom(ctx, "resolve");
|
|
763
|
+
JSValue p = JS_Invoke(ctx, promise_ctor, resolve, 1, &v);
|
|
764
|
+
JS_FreeAtom(ctx, resolve);
|
|
765
|
+
JS_FreeValue(ctx, v);
|
|
766
|
+
return p;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
JSValue step_cb(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic,
|
|
770
|
+
JSValue* func_data);
|
|
771
|
+
|
|
772
|
+
/* Continue the run once `v` settles: Promise.resolve(v).then(ok, err), the
|
|
773
|
+
* handlers carrying the registry and the current test's sequence number.
|
|
774
|
+
* Consumes v. */
|
|
775
|
+
void settle(JSContext* ctx, JSValueConst reg_obj, Registry* reg, JSValue v, Step ok, Step err) {
|
|
776
|
+
JSValue p = promise_resolve(ctx, reg->promise_ctor, v);
|
|
777
|
+
if (JS_IsException(p)) {
|
|
778
|
+
mik_dump_error(ctx);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
JSValue seq = JS_NewUint32(ctx, reg->seq);
|
|
782
|
+
JSValue data[2] = {reg_obj, seq};
|
|
783
|
+
JSValue on_ok = JS_NewCFunctionData(ctx, step_cb, 1, ok, 2, data);
|
|
784
|
+
JSValue on_err = JS_NewCFunctionData(ctx, step_cb, 1, err, 2, data);
|
|
785
|
+
JS_FreeValue(ctx, seq);
|
|
786
|
+
JSValue handlers[2] = {on_ok, on_err};
|
|
787
|
+
JSAtom then = JS_NewAtom(ctx, "then");
|
|
788
|
+
JSValue derived = JS_Invoke(ctx, p, then, 2, handlers);
|
|
789
|
+
JS_FreeAtom(ctx, then);
|
|
790
|
+
if (JS_IsException(derived)) mik_dump_error(ctx);
|
|
791
|
+
JS_FreeValue(ctx, derived);
|
|
792
|
+
JS_FreeValue(ctx, on_ok);
|
|
793
|
+
JS_FreeValue(ctx, on_err);
|
|
794
|
+
JS_FreeValue(ctx, p);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/* Call fn() and settle on its result; a synchronous throw settles as a
|
|
798
|
+
* rejection so every step, failing or not, returns through the job queue. */
|
|
799
|
+
void settle_call(JSContext* ctx, JSValueConst reg_obj, Registry* reg, JSValueConst fn, Step ok,
|
|
800
|
+
Step err) {
|
|
801
|
+
JSValue r = JS_Call(ctx, fn, JS_UNDEFINED, 0, nullptr);
|
|
802
|
+
if (JS_IsException(r)) {
|
|
803
|
+
JSValue e = JS_GetException(ctx);
|
|
804
|
+
JSValue funcs[2];
|
|
805
|
+
r = JS_NewPromiseCapability(ctx, funcs);
|
|
806
|
+
if (JS_IsException(r)) {
|
|
807
|
+
JS_FreeValue(ctx, e);
|
|
808
|
+
mik_dump_error(ctx);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
JSValue rr = JS_Call(ctx, funcs[1], JS_UNDEFINED, 1, &e);
|
|
812
|
+
JS_FreeValue(ctx, rr);
|
|
813
|
+
JS_FreeValue(ctx, funcs[0]);
|
|
814
|
+
JS_FreeValue(ctx, funcs[1]);
|
|
815
|
+
JS_FreeValue(ctx, e);
|
|
816
|
+
}
|
|
817
|
+
settle(ctx, reg_obj, reg, r, ok, err);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
int64_t now_us() {
|
|
821
|
+
return MIK_GetPlatform()->get_rtc_us();
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
int64_t elapsed_ms(int64_t start) {
|
|
825
|
+
return (now_us() - start + 500) / 1000;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
struct Mem {
|
|
829
|
+
int64_t heap_used;
|
|
830
|
+
int64_t heap_total;
|
|
831
|
+
int64_t sys_free;
|
|
832
|
+
int64_t sys_min_free;
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
Mem gc_and_measure(JSContext* ctx) {
|
|
836
|
+
JSRuntime* rt = JS_GetRuntime(ctx);
|
|
837
|
+
JS_RunGC(rt);
|
|
838
|
+
JSMemoryUsage mem;
|
|
839
|
+
JS_ComputeMemoryUsage(rt, &mem);
|
|
840
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
841
|
+
return Mem{mem.malloc_size, mem.malloc_limit > 0 ? mem.malloc_limit : mem.malloc_size,
|
|
842
|
+
static_cast<int64_t>(platform->get_free_system_mem()),
|
|
843
|
+
static_cast<int64_t>(platform->get_min_free_system_mem())};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
void emit_heap(JSContext* ctx, Registry* reg) {
|
|
847
|
+
Mem m = gc_and_measure(ctx);
|
|
848
|
+
Event ev(8);
|
|
849
|
+
ev.num("u", m.heap_used).num("t", m.heap_total);
|
|
850
|
+
if (m.sys_free > 0) {
|
|
851
|
+
ev.num("f", m.sys_free);
|
|
852
|
+
if (reg->sys_free_floor == 0 || m.sys_free < reg->sys_free_floor) {
|
|
853
|
+
reg->sys_free_floor = m.sys_free;
|
|
854
|
+
}
|
|
855
|
+
if (reg->suite_free_floor == 0 || m.sys_free < reg->suite_free_floor) {
|
|
856
|
+
reg->suite_free_floor = m.sys_free;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (m.sys_min_free > 0) ev.num("mf", m.sys_min_free);
|
|
860
|
+
emit(ctx, ev);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
int32_t active_timers(JSContext* ctx, Registry* reg) {
|
|
864
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
865
|
+
auto n = static_cast<int32_t>(mik_rt->timers->entries.size());
|
|
866
|
+
return reg->in_start ? n - 1 : n;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/* pendingCount() from native:mikro/http. Only read when something already
|
|
870
|
+
* loaded that module: nothing can be pending in a runtime without it, and
|
|
871
|
+
* loading it here would charge every test file for the http stack. */
|
|
872
|
+
int32_t pending_http(JSContext* ctx) {
|
|
873
|
+
JSAtom name = JS_NewAtom(ctx, "native:mikro/http");
|
|
874
|
+
JSModuleDef* m = JS_FindLoadedModule(ctx, name);
|
|
875
|
+
JS_FreeAtom(ctx, name);
|
|
876
|
+
if (!m) return 0;
|
|
877
|
+
JSValue ns = JS_GetModuleNamespace(ctx, m);
|
|
878
|
+
if (JS_IsException(ns)) {
|
|
879
|
+
drop_exception(ctx);
|
|
880
|
+
return 0;
|
|
881
|
+
}
|
|
882
|
+
JSValue fn = JS_GetPropertyStr(ctx, ns, "pendingCount");
|
|
883
|
+
JS_FreeValue(ctx, ns);
|
|
884
|
+
int32_t n = 0;
|
|
885
|
+
if (JS_IsFunction(ctx, fn)) {
|
|
886
|
+
JSValue r = JS_Call(ctx, fn, JS_UNDEFINED, 0, nullptr);
|
|
887
|
+
if (JS_IsException(r) || JS_ToInt32(ctx, &n, r) < 0) {
|
|
888
|
+
drop_exception(ctx);
|
|
889
|
+
n = 0;
|
|
890
|
+
}
|
|
891
|
+
JS_FreeValue(ctx, r);
|
|
892
|
+
}
|
|
893
|
+
JS_FreeValue(ctx, fn);
|
|
894
|
+
return n;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/* The running suite / test. Re-fetch after every call that can re-enter JS:
|
|
898
|
+
* describe() from inside a hook, a test, a getter or an emit transport
|
|
899
|
+
* appends to `suites` and may reallocate it. */
|
|
900
|
+
Suite& cur_suite(Registry* reg) {
|
|
901
|
+
return reg->suites[reg->si];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
TestCase& cur_test(Registry* reg) {
|
|
905
|
+
return reg->suites[reg->si].tests[reg->ti];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
void clear_timeout(JSContext* ctx, Registry* reg) {
|
|
909
|
+
if (!reg->timeout_timer) return;
|
|
910
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
911
|
+
MIK_Timer_UnSchedule(mik_rt->timers, ctx, reg->timeout_timer);
|
|
912
|
+
reg->timeout_timer = 0;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/* e:9 for a todo test, e:4 for a skipped one. */
|
|
916
|
+
void emit_test_skip(JSContext* ctx, Registry* reg, const std::string& suite,
|
|
917
|
+
const std::string& test, bool todo) {
|
|
918
|
+
if (todo) {
|
|
919
|
+
Event ev(9);
|
|
920
|
+
ev.str("s", suite).str("t", test);
|
|
921
|
+
emit(ctx, ev);
|
|
922
|
+
reg->todo++;
|
|
923
|
+
} else {
|
|
924
|
+
Event ev(4);
|
|
925
|
+
ev.str("s", suite).str("t", test);
|
|
926
|
+
emit(ctx, ev);
|
|
927
|
+
reg->skipped++;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
void emit_suite_end_bare(JSContext* ctx, const std::string& name);
|
|
932
|
+
|
|
933
|
+
/* Skip (or, for a .todo suite, todo) every test of the running suite, then
|
|
934
|
+
* close it without a retention figure. Indexed: each emit may re-enter JS. */
|
|
935
|
+
void skip_suite(JSContext* ctx, Registry* reg, bool all_todo) {
|
|
936
|
+
std::string suite = cur_suite(reg).name;
|
|
937
|
+
for (size_t i = 0; i < cur_suite(reg).tests.size(); i++) {
|
|
938
|
+
const TestCase& t = cur_suite(reg).tests[i];
|
|
939
|
+
std::string name = t.name;
|
|
940
|
+
bool todo = all_todo || t.todo;
|
|
941
|
+
emit_test_skip(ctx, reg, suite, name, todo);
|
|
942
|
+
}
|
|
943
|
+
emit_suite_end_bare(ctx, suite);
|
|
944
|
+
reg->si++;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
void emit_suite_end_bare(JSContext* ctx, const std::string& name) {
|
|
948
|
+
Event ev(5);
|
|
949
|
+
ev.str("s", name);
|
|
950
|
+
emit(ctx, ev);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/* Fold the finished suite's retention into the file total and start the
|
|
954
|
+
* next suite from where this one ended. */
|
|
955
|
+
void close_suite(JSContext* ctx, Registry* reg) {
|
|
956
|
+
Mem m = gc_and_measure(ctx);
|
|
957
|
+
int64_t retained = m.heap_used - reg->suite_baseline;
|
|
958
|
+
reg->heap_retained += retained;
|
|
959
|
+
reg->suite_baseline = m.heap_used;
|
|
960
|
+
if (m.sys_free > 0 && m.sys_free < reg->suite_free_floor) reg->suite_free_floor = m.sys_free;
|
|
961
|
+
int64_t sys_used = reg->suite_free_start > reg->suite_free_floor
|
|
962
|
+
? reg->suite_free_start - reg->suite_free_floor
|
|
963
|
+
: 0;
|
|
964
|
+
Event ev(5);
|
|
965
|
+
ev.str("s", reg->suites[reg->si].name).num("hr", retained);
|
|
966
|
+
if (sys_used > 0) ev.num("su", sys_used);
|
|
967
|
+
emit(ctx, ev);
|
|
968
|
+
reg->si++;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
void run_finish(JSContext* ctx, Registry* reg) {
|
|
972
|
+
Mem m = gc_and_measure(ctx);
|
|
973
|
+
if (m.sys_free > 0 && (reg->sys_free_floor == 0 || m.sys_free < reg->sys_free_floor)) {
|
|
974
|
+
reg->sys_free_floor = m.sys_free;
|
|
975
|
+
}
|
|
976
|
+
/* Whatever ran outside a suite's own accounting: a file without
|
|
977
|
+
* suites, and the skip/todo bookkeeping between them. */
|
|
978
|
+
reg->heap_retained += m.heap_used - reg->suite_baseline;
|
|
979
|
+
int32_t timers_after = active_timers(ctx, reg);
|
|
980
|
+
int32_t pending_after = pending_http(ctx);
|
|
981
|
+
int64_t sys_used =
|
|
982
|
+
reg->sys_free_start > reg->sys_free_floor ? reg->sys_free_start - reg->sys_free_floor : 0;
|
|
983
|
+
Event ev(6);
|
|
984
|
+
ev.num("p", reg->passed)
|
|
985
|
+
.num("f", reg->failed)
|
|
986
|
+
.num("k", reg->skipped)
|
|
987
|
+
.num("o", reg->todo)
|
|
988
|
+
.num("d", elapsed_ms(reg->start_us))
|
|
989
|
+
.num("hb", reg->start_heap)
|
|
990
|
+
.num("ha", m.heap_used)
|
|
991
|
+
.num("hr", reg->heap_retained)
|
|
992
|
+
.num("tb", reg->timers_before)
|
|
993
|
+
.num("ta", timers_after)
|
|
994
|
+
.num("pb", reg->pending_before)
|
|
995
|
+
.num("pa", pending_after);
|
|
996
|
+
if (reg->sys_free_start > 0) ev.num("su", sys_used).num("sf", reg->sys_free_floor);
|
|
997
|
+
emit(ctx, ev);
|
|
998
|
+
|
|
999
|
+
/* Tell a supervisor this file is complete so it can swap in the next
|
|
1000
|
+
* runtime. Absent outside the test harness. */
|
|
1001
|
+
JSValue g = JS_GetGlobalObject(ctx);
|
|
1002
|
+
JSValue done = JS_GetPropertyStr(ctx, g, "__testFileDone");
|
|
1003
|
+
JS_FreeValue(ctx, g);
|
|
1004
|
+
if (JS_IsFunction(ctx, done)) {
|
|
1005
|
+
JSValue r = JS_Call(ctx, done, JS_UNDEFINED, 0, nullptr);
|
|
1006
|
+
if (JS_IsException(r)) mik_dump_error(ctx);
|
|
1007
|
+
JS_FreeValue(ctx, r);
|
|
1008
|
+
}
|
|
1009
|
+
JS_FreeValue(ctx, done);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
void test_step(JSContext* ctx, JSValueConst reg_obj, Registry* reg);
|
|
1013
|
+
void after_each_step(JSContext* ctx, JSValueConst reg_obj, Registry* reg);
|
|
1014
|
+
|
|
1015
|
+
/* Drive the run until it has to wait on a promise (or is done). */
|
|
1016
|
+
void advance(JSContext* ctx, JSValueConst reg_obj, Registry* reg) {
|
|
1017
|
+
for (;;) {
|
|
1018
|
+
switch (reg->phase) {
|
|
1019
|
+
case PH_SUITE_BEGIN: {
|
|
1020
|
+
if (reg->si >= reg->suites.size()) {
|
|
1021
|
+
reg->phase = PH_DONE;
|
|
1022
|
+
run_finish(ctx, reg);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
/* Open this suite's own peak window on the post-gc sample. */
|
|
1026
|
+
reg->suite_free_floor = 0;
|
|
1027
|
+
emit_heap(ctx, reg);
|
|
1028
|
+
reg->suite_free_start = reg->suite_free_floor;
|
|
1029
|
+
Event begin(1);
|
|
1030
|
+
begin.str("s", cur_suite(reg).name)
|
|
1031
|
+
.num("n", static_cast<int64_t>(cur_suite(reg).tests.size()));
|
|
1032
|
+
emit(ctx, begin);
|
|
1033
|
+
|
|
1034
|
+
bool suite_has_only = false;
|
|
1035
|
+
for (const auto& t : cur_suite(reg).tests) suite_has_only |= t.only;
|
|
1036
|
+
bool filtered = reg->has_only && !cur_suite(reg).only && !suite_has_only;
|
|
1037
|
+
if (cur_suite(reg).skip || cur_suite(reg).todo || filtered) {
|
|
1038
|
+
skip_suite(ctx, reg, cur_suite(reg).todo);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
/* Within a participating suite, .only tests exclude the rest. */
|
|
1042
|
+
reg->only_in_suite = reg->has_only && suite_has_only;
|
|
1043
|
+
reg->ti = 0;
|
|
1044
|
+
reg->phase = PH_TESTS;
|
|
1045
|
+
JSValueConst before_all = cur_suite(reg).before_all;
|
|
1046
|
+
if (JS_IsUndefined(before_all)) continue;
|
|
1047
|
+
settle_call(ctx, reg_obj, reg, before_all, STEP_BEFORE_ALL_OK,
|
|
1048
|
+
STEP_BEFORE_ALL_ERR);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
case PH_TESTS: {
|
|
1052
|
+
if (reg->ti >= cur_suite(reg).tests.size()) {
|
|
1053
|
+
reg->phase = PH_SUITE_FINISH;
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
const TestCase& t = cur_test(reg);
|
|
1057
|
+
if (t.todo || t.skip || (reg->only_in_suite && !t.only)) {
|
|
1058
|
+
std::string suite = cur_suite(reg).name;
|
|
1059
|
+
std::string name = t.name;
|
|
1060
|
+
bool todo = t.todo;
|
|
1061
|
+
emit_test_skip(ctx, reg, suite, name, todo);
|
|
1062
|
+
reg->ti++;
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
int64_t timeout_ms = t.timeout_ms;
|
|
1066
|
+
emit_heap(ctx, reg);
|
|
1067
|
+
reg->seq++;
|
|
1068
|
+
reg->test_settled = false;
|
|
1069
|
+
reg->hook_i = 0;
|
|
1070
|
+
reg->test_start_us = now_us();
|
|
1071
|
+
JSValue seq = JS_NewUint32(ctx, reg->seq);
|
|
1072
|
+
JSValue data[2] = {reg_obj, seq};
|
|
1073
|
+
JSValue on_timeout = JS_NewCFunctionData(ctx, step_cb, 0, STEP_TIMEOUT, 2, data);
|
|
1074
|
+
JS_FreeValue(ctx, seq);
|
|
1075
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
1076
|
+
reg->timeout_timer =
|
|
1077
|
+
MIK_Timer_Schedule(mik_rt->timers, ctx, on_timeout, 0, nullptr,
|
|
1078
|
+
timeout_ms * 1000, false, MIK_GetPlatform()->get_boot_us());
|
|
1079
|
+
JS_FreeValue(ctx, on_timeout);
|
|
1080
|
+
test_step(ctx, reg_obj, reg);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
case PH_SUITE_FINISH: {
|
|
1084
|
+
JSValueConst after_all = cur_suite(reg).after_all;
|
|
1085
|
+
if (!JS_IsUndefined(after_all)) {
|
|
1086
|
+
reg->phase = PH_SUITE_BEGIN;
|
|
1087
|
+
settle_call(ctx, reg_obj, reg, after_all, STEP_AFTER_ALL, STEP_AFTER_ALL);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
close_suite(ctx, reg);
|
|
1091
|
+
reg->phase = PH_SUITE_BEGIN;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
case PH_DONE:
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/* Run the next beforeEach hook, then the test itself. */
|
|
1101
|
+
void test_step(JSContext* ctx, JSValueConst reg_obj, Registry* reg) {
|
|
1102
|
+
if (reg->hook_i < cur_suite(reg).before_each.size()) {
|
|
1103
|
+
JSValueConst hook = cur_suite(reg).before_each[reg->hook_i];
|
|
1104
|
+
settle_call(ctx, reg_obj, reg, hook, STEP_HOOK_OK, STEP_TEST_ERR);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
JSValueConst fn = cur_test(reg).fn;
|
|
1108
|
+
settle_call(ctx, reg_obj, reg, fn, STEP_TEST_OK, STEP_TEST_ERR);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
void test_done(JSContext* ctx, JSValueConst reg_obj, Registry* reg, bool passed,
|
|
1112
|
+
const std::string& message) {
|
|
1113
|
+
reg->test_settled = true;
|
|
1114
|
+
clear_timeout(ctx, reg);
|
|
1115
|
+
Event ev(passed ? 2 : 3);
|
|
1116
|
+
ev.str("s", cur_suite(reg).name).str("t", cur_test(reg).name);
|
|
1117
|
+
ev.num("d", elapsed_ms(reg->test_start_us));
|
|
1118
|
+
if (!passed) ev.str("m", message);
|
|
1119
|
+
emit(ctx, ev);
|
|
1120
|
+
if (passed) {
|
|
1121
|
+
reg->passed++;
|
|
1122
|
+
} else {
|
|
1123
|
+
reg->failed++;
|
|
1124
|
+
}
|
|
1125
|
+
reg->hook_i = 0;
|
|
1126
|
+
after_each_step(ctx, reg_obj, reg);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/* afterEach hooks run whatever the outcome; their errors are not fatal. */
|
|
1130
|
+
void after_each_step(JSContext* ctx, JSValueConst reg_obj, Registry* reg) {
|
|
1131
|
+
if (reg->hook_i < cur_suite(reg).after_each.size()) {
|
|
1132
|
+
JSValueConst hook = cur_suite(reg).after_each[reg->hook_i];
|
|
1133
|
+
settle_call(ctx, reg_obj, reg, hook, STEP_AFTER_EACH, STEP_AFTER_EACH);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
reg->ti++;
|
|
1137
|
+
advance(ctx, reg_obj, reg);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
void run_start(JSContext* ctx, JSValueConst reg_obj, Registry* reg) {
|
|
1141
|
+
reg->start_us = now_us();
|
|
1142
|
+
reg->pending_before = pending_http(ctx);
|
|
1143
|
+
/* Leak-detection baseline: post-gc, with every suite registered, so the
|
|
1144
|
+
* runner's own allocations sit inside it rather than count as growth. */
|
|
1145
|
+
Mem m = gc_and_measure(ctx);
|
|
1146
|
+
reg->start_heap = m.heap_used;
|
|
1147
|
+
reg->heap_retained = 0;
|
|
1148
|
+
reg->suite_baseline = m.heap_used;
|
|
1149
|
+
if (m.sys_free > 0) {
|
|
1150
|
+
reg->sys_free_start = m.sys_free;
|
|
1151
|
+
reg->sys_free_floor = m.sys_free;
|
|
1152
|
+
}
|
|
1153
|
+
reg->timers_before = active_timers(ctx, reg);
|
|
1154
|
+
/* Any .only anywhere filters everything else. */
|
|
1155
|
+
for (const auto& s : reg->suites) {
|
|
1156
|
+
reg->has_only |= s.only;
|
|
1157
|
+
for (const auto& t : s.tests) reg->has_only |= t.only;
|
|
1158
|
+
}
|
|
1159
|
+
reg->phase = PH_SUITE_BEGIN;
|
|
1160
|
+
reg->si = 0;
|
|
1161
|
+
advance(ctx, reg_obj, reg);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
JSValue step_cb(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic,
|
|
1165
|
+
JSValue* func_data) {
|
|
1166
|
+
(void)this_val;
|
|
1167
|
+
JSValueConst reg_obj = func_data[0];
|
|
1168
|
+
Registry* reg = reg_of(reg_obj);
|
|
1169
|
+
uint32_t seq = 0;
|
|
1170
|
+
JS_ToUint32(ctx, &seq, func_data[1]);
|
|
1171
|
+
JSValueConst value = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
1172
|
+
switch (static_cast<Step>(magic)) {
|
|
1173
|
+
case STEP_RUN:
|
|
1174
|
+
/* A file with nothing to run finishes inside this callback. */
|
|
1175
|
+
reg->in_start = true;
|
|
1176
|
+
run_start(ctx, reg_obj, reg);
|
|
1177
|
+
reg->in_start = false;
|
|
1178
|
+
break;
|
|
1179
|
+
case STEP_BEFORE_ALL_OK:
|
|
1180
|
+
/* One more microtask before the baseline so the beforeAll frame's
|
|
1181
|
+
* locals are collectible; otherwise the suite would close below
|
|
1182
|
+
* its baseline. */
|
|
1183
|
+
settle(ctx, reg_obj, reg, JS_UNDEFINED, STEP_BASELINE, STEP_BASELINE);
|
|
1184
|
+
break;
|
|
1185
|
+
case STEP_BASELINE: {
|
|
1186
|
+
Mem m = gc_and_measure(ctx);
|
|
1187
|
+
reg->suite_baseline = m.heap_used;
|
|
1188
|
+
advance(ctx, reg_obj, reg);
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
case STEP_BEFORE_ALL_ERR: {
|
|
1192
|
+
/* A broken beforeAll fails the suite's tests rather than skipping
|
|
1193
|
+
* them: a skip reads as a deliberate gate. */
|
|
1194
|
+
std::string message = format_thrown(ctx, value); /* runs getters */
|
|
1195
|
+
std::string suite = cur_suite(reg).name;
|
|
1196
|
+
Event ev(7);
|
|
1197
|
+
ev.str("s", suite).str("m", message);
|
|
1198
|
+
emit(ctx, ev);
|
|
1199
|
+
for (size_t i = 0; i < cur_suite(reg).tests.size(); i++) {
|
|
1200
|
+
const TestCase& t = cur_suite(reg).tests[i];
|
|
1201
|
+
std::string name = t.name;
|
|
1202
|
+
if (t.todo) {
|
|
1203
|
+
Event todo(9);
|
|
1204
|
+
todo.str("s", suite).str("t", name);
|
|
1205
|
+
emit(ctx, todo);
|
|
1206
|
+
reg->todo++;
|
|
1207
|
+
} else {
|
|
1208
|
+
Event failed(3);
|
|
1209
|
+
failed.str("s", suite).str("t", name).num("d", 0).str("m", "beforeAll failed");
|
|
1210
|
+
emit(ctx, failed);
|
|
1211
|
+
reg->failed++;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
emit_suite_end_bare(ctx, suite);
|
|
1215
|
+
reg->si++;
|
|
1216
|
+
reg->phase = PH_SUITE_BEGIN;
|
|
1217
|
+
advance(ctx, reg_obj, reg);
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
case STEP_HOOK_OK:
|
|
1221
|
+
if (seq != reg->seq || reg->test_settled) break;
|
|
1222
|
+
reg->hook_i++;
|
|
1223
|
+
test_step(ctx, reg_obj, reg);
|
|
1224
|
+
break;
|
|
1225
|
+
case STEP_TEST_OK:
|
|
1226
|
+
if (seq != reg->seq || reg->test_settled) break;
|
|
1227
|
+
test_done(ctx, reg_obj, reg, true, "");
|
|
1228
|
+
break;
|
|
1229
|
+
case STEP_TEST_ERR:
|
|
1230
|
+
if (seq != reg->seq || reg->test_settled) break;
|
|
1231
|
+
test_done(ctx, reg_obj, reg, false, format_thrown(ctx, value));
|
|
1232
|
+
break;
|
|
1233
|
+
case STEP_TIMEOUT: {
|
|
1234
|
+
/* test_done unschedules this timer while it runs, so a file that
|
|
1235
|
+
* ends here does not count it as still active. */
|
|
1236
|
+
if (seq != reg->seq || reg->test_settled) break;
|
|
1237
|
+
int64_t ms = cur_test(reg).timeout_ms;
|
|
1238
|
+
test_done(ctx, reg_obj, reg, false, "timeout (" + std::to_string(ms) + "ms)");
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
case STEP_AFTER_EACH:
|
|
1242
|
+
if (seq != reg->seq) break;
|
|
1243
|
+
reg->hook_i++;
|
|
1244
|
+
after_each_step(ctx, reg_obj, reg);
|
|
1245
|
+
break;
|
|
1246
|
+
case STEP_AFTER_ALL:
|
|
1247
|
+
close_suite(ctx, reg);
|
|
1248
|
+
advance(ctx, reg_obj, reg);
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
return JS_UNDEFINED;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/* ── Module ─────────────────────────────────────────────────────── */
|
|
1255
|
+
|
|
1256
|
+
JSValue make_registrar(JSContext* ctx, JSCFunctionData* fn, int length, int flags,
|
|
1257
|
+
JSValueConst reg_obj, bool with_each, int each_flags) {
|
|
1258
|
+
JSValue f = JS_NewCFunctionData(ctx, fn, length, flags, 1, ®_obj);
|
|
1259
|
+
if (with_each) {
|
|
1260
|
+
JS_SetPropertyStr(ctx, f, "each", JS_NewCFunctionData(ctx, each_cb, 1, each_flags, 1, ®_obj));
|
|
1261
|
+
}
|
|
1262
|
+
return f;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/* describe / test with their variant trees. `each_base` is EACH_TEST for test. */
|
|
1266
|
+
JSValue make_family(JSContext* ctx, JSCFunctionData* fn, int length, JSValueConst reg_obj,
|
|
1267
|
+
int each_base, bool is_test) {
|
|
1268
|
+
JSValue plain = make_registrar(ctx, fn, length, 0, reg_obj, true, each_base);
|
|
1269
|
+
JSValue skip = make_registrar(ctx, fn, length, F_SKIP, reg_obj, true, each_base | F_SKIP);
|
|
1270
|
+
JSValue pair[2] = {plain, skip};
|
|
1271
|
+
JS_SetPropertyStr(ctx, plain, "skipIf", JS_NewCFunctionData(ctx, pick_cb, 1, 0, 2, pair));
|
|
1272
|
+
JS_SetPropertyStr(ctx, plain, "runIf", JS_NewCFunctionData(ctx, pick_cb, 1, 1, 2, pair));
|
|
1273
|
+
JS_SetPropertyStr(ctx, plain, "skip", skip);
|
|
1274
|
+
JS_SetPropertyStr(ctx, plain, "only",
|
|
1275
|
+
make_registrar(ctx, fn, length, F_ONLY, reg_obj, true, each_base | F_ONLY));
|
|
1276
|
+
JS_SetPropertyStr(ctx, plain, "fixme",
|
|
1277
|
+
make_registrar(ctx, fn, length, F_FIXME, reg_obj, true, each_base | F_FIXME));
|
|
1278
|
+
if (is_test) {
|
|
1279
|
+
/* test.todo(name): no function, no .each */
|
|
1280
|
+
JS_SetPropertyStr(ctx, plain, "todo", make_registrar(ctx, fn, 1, F_TODO, reg_obj, false, 0));
|
|
1281
|
+
} else {
|
|
1282
|
+
JS_SetPropertyStr(ctx, plain, "todo",
|
|
1283
|
+
make_registrar(ctx, fn, length, F_TODO, reg_obj, true, F_TODO));
|
|
1284
|
+
}
|
|
1285
|
+
return plain;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
int test_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
1289
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
1290
|
+
CHECK_NOT_NULL(mik_rt);
|
|
1291
|
+
JSRuntime* rt = JS_GetRuntime(ctx);
|
|
1292
|
+
MIK_NewClassID(rt, ®istry_class_id);
|
|
1293
|
+
JS_NewClass(rt, registry_class_id, ®istry_class_def);
|
|
1294
|
+
JSValue reg_obj = JS_NewObjectClass(ctx, registry_class_id);
|
|
1295
|
+
if (JS_IsException(reg_obj)) return -1;
|
|
1296
|
+
auto* reg = new Registry();
|
|
1297
|
+
JS_SetOpaque(reg_obj, reg);
|
|
1298
|
+
JSValue g = JS_GetGlobalObject(ctx);
|
|
1299
|
+
reg->promise_ctor = JS_GetPropertyStr(ctx, g, "Promise");
|
|
1300
|
+
JS_FreeValue(ctx, g);
|
|
1301
|
+
|
|
1302
|
+
JS_SetModuleExport(ctx, m, "describe", make_family(ctx, describe_cb, 2, reg_obj, 0, false));
|
|
1303
|
+
JS_SetModuleExport(ctx, m, "test", make_family(ctx, test_cb, 2, reg_obj, EACH_TEST, true));
|
|
1304
|
+
for (int k = HOOK_BEFORE_ALL; k <= HOOK_AFTER_EACH; k++) {
|
|
1305
|
+
JS_SetModuleExport(ctx, m, hook_names[k],
|
|
1306
|
+
JS_NewCFunctionData(ctx, hook_cb, 1, k, 1, ®_obj));
|
|
1307
|
+
}
|
|
1308
|
+
JS_SetModuleExport(ctx, m, "assert", make_assert(ctx));
|
|
1309
|
+
|
|
1310
|
+
/* Auto-run once the importing file's evaluation has completed. */
|
|
1311
|
+
JSValue seq = JS_NewUint32(ctx, 0);
|
|
1312
|
+
JSValue data[2] = {reg_obj, seq};
|
|
1313
|
+
JSValue run_fn = JS_NewCFunctionData(ctx, step_cb, 0, STEP_RUN, 2, data);
|
|
1314
|
+
JS_FreeValue(ctx, seq);
|
|
1315
|
+
MIK_Timer_Schedule(mik_rt->timers, ctx, run_fn, 0, nullptr, 0, false,
|
|
1316
|
+
MIK_GetPlatform()->get_boot_us());
|
|
1317
|
+
JS_FreeValue(ctx, run_fn);
|
|
1318
|
+
JS_FreeValue(ctx, reg_obj);
|
|
1319
|
+
return 0;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const char* const exports[] = {"describe", "test", "beforeAll", "afterAll",
|
|
1323
|
+
"beforeEach", "afterEach", "assert"};
|
|
1324
|
+
|
|
1325
|
+
} // namespace
|
|
1326
|
+
|
|
1327
|
+
/* mikro/test, resolved through the C-module table in modules.cpp on first
|
|
1328
|
+
* import. */
|
|
1329
|
+
JSModuleDef* mik__test_load(JSContext* ctx) {
|
|
1330
|
+
JSModuleDef* m = JS_NewCModule(ctx, "mikro/test", test_module_init);
|
|
1331
|
+
if (!m) return nullptr;
|
|
1332
|
+
for (const char* name : exports) JS_AddModuleExport(ctx, m, name);
|
|
1333
|
+
return m;
|
|
1334
|
+
}
|