@mikrojs/firmware 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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/bin/idf.py +7 -0
  4. package/chips.json +3 -0
  5. package/cmake.js +9 -0
  6. package/components/mikrojs/CMakeLists.txt +187 -0
  7. package/components/mikrojs/Kconfig +55 -0
  8. package/components/mikrojs/idf_component.yml +6 -0
  9. package/components/mikrojs/include/mem.h +3 -0
  10. package/components/mikrojs/include/mik_color.h +3 -0
  11. package/components/mikrojs/include/mik_http_internal.h +77 -0
  12. package/components/mikrojs/include/mikrojs.h +5 -0
  13. package/components/mikrojs/include/mikrojs_esp32.h +65 -0
  14. package/components/mikrojs/include/private.h +10 -0
  15. package/components/mikrojs/include/utils.h +3 -0
  16. package/components/mikrojs/mik_ble.cpp +1588 -0
  17. package/components/mikrojs/mik_ble_c_shim.c +61 -0
  18. package/components/mikrojs/mik_ble_c_shim.h +37 -0
  19. package/components/mikrojs/mik_config.cpp +167 -0
  20. package/components/mikrojs/mik_deploy.cpp +584 -0
  21. package/components/mikrojs/mik_http.cpp +916 -0
  22. package/components/mikrojs/mik_i2c.cpp +364 -0
  23. package/components/mikrojs/mik_main.cpp +542 -0
  24. package/components/mikrojs/mik_neopixel.cpp +437 -0
  25. package/components/mikrojs/mik_nvs_kv.cpp +219 -0
  26. package/components/mikrojs/mik_pin.cpp +195 -0
  27. package/components/mikrojs/mik_pwm.cpp +525 -0
  28. package/components/mikrojs/mik_recovery.cpp +86 -0
  29. package/components/mikrojs/mik_rtc.cpp +305 -0
  30. package/components/mikrojs/mik_serial_io.cpp +362 -0
  31. package/components/mikrojs/mik_sleep.cpp +226 -0
  32. package/components/mikrojs/mik_sntp.cpp +275 -0
  33. package/components/mikrojs/mik_spi.cpp +330 -0
  34. package/components/mikrojs/mik_uart.cpp +497 -0
  35. package/components/mikrojs/mik_wifi.cpp +1434 -0
  36. package/components/mikrojs/platform_esp32.cpp +192 -0
  37. package/components/mikrojs/test/CMakeLists.txt +32 -0
  38. package/components/mikrojs/test/abort_test.cpp +254 -0
  39. package/components/mikrojs/test/ble_test.cpp +714 -0
  40. package/components/mikrojs/test/fs_js_test.cpp +458 -0
  41. package/components/mikrojs/test/fs_pub_test.cpp +312 -0
  42. package/components/mikrojs/test/http_test.cpp +475 -0
  43. package/components/mikrojs/test/i2c_test.cpp +138 -0
  44. package/components/mikrojs/test/modules_extended_test.cpp +137 -0
  45. package/components/mikrojs/test/modules_test.cpp +131 -0
  46. package/components/mikrojs/test/pins_test.cpp +47 -0
  47. package/components/mikrojs/test/pwm_test.cpp +166 -0
  48. package/components/mikrojs/test/repl_protocol_test.cpp +405 -0
  49. package/components/mikrojs/test/rtc_test.cpp +331 -0
  50. package/components/mikrojs/test/runtime_test.cpp +89 -0
  51. package/components/mikrojs/test/sleep_test.cpp +222 -0
  52. package/components/mikrojs/test/sntp_test.cpp +249 -0
  53. package/components/mikrojs/test/stdio_test.cpp +449 -0
  54. package/components/mikrojs/test/sys_test.cpp +165 -0
  55. package/components/mikrojs/test/text_encoding_test.cpp +224 -0
  56. package/components/mikrojs/test/timers_js_test.cpp +244 -0
  57. package/components/mikrojs/test/timers_test.cpp +79 -0
  58. package/components/mikrojs/test/wifi_test.cpp +599 -0
  59. package/default-app/main/CMakeLists.txt +3 -0
  60. package/default-app/main/main.cpp +5 -0
  61. package/discover.js +77 -0
  62. package/index.d.ts +7 -0
  63. package/index.js +20 -0
  64. package/package.json +61 -0
  65. package/partitions.csv +5 -0
  66. package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
  67. package/prebuilds/esp32/flasher_args.json +24 -0
  68. package/prebuilds/esp32/mikrojs.bin +0 -0
  69. package/prebuilds/esp32/partition_table/partition-table.bin +0 -0
  70. package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
  71. package/prebuilds/esp32c3/flasher_args.json +24 -0
  72. package/prebuilds/esp32c3/mikrojs.bin +0 -0
  73. package/prebuilds/esp32c3/partition_table/partition-table.bin +0 -0
  74. package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
  75. package/prebuilds/esp32c6/flasher_args.json +24 -0
  76. package/prebuilds/esp32c6/mikrojs.bin +0 -0
  77. package/prebuilds/esp32c6/partition_table/partition-table.bin +0 -0
  78. package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
  79. package/prebuilds/esp32s3/flasher_args.json +24 -0
  80. package/prebuilds/esp32s3/mikrojs.bin +0 -0
  81. package/prebuilds/esp32s3/partition_table/partition-table.bin +0 -0
  82. package/project.cmake +101 -0
  83. package/resolve.js +54 -0
  84. package/sdkconfig.defaults +127 -0
  85. package/sdkconfig.defaults.esp32 +8 -0
  86. package/sdkconfig.defaults.esp32c3 +15 -0
  87. package/sdkconfig.defaults.esp32c6 +26 -0
  88. package/sdkconfig.defaults.esp32s3 +22 -0
@@ -0,0 +1,475 @@
1
+ #include <atomic>
2
+
3
+ #include "freertos/FreeRTOS.h"
4
+ #include "freertos/queue.h"
5
+ #include "freertos/semphr.h"
6
+ #include "mik_http_internal.h"
7
+ #include "mikrojs.h"
8
+ #include "private.h"
9
+ #include "quickjs.h"
10
+ #include "unity.h"
11
+ #include "utils.h"
12
+
13
+ /* Access HTTP state and consume function from mik_http.cpp */
14
+ extern int mik__http_slot;
15
+ extern void mik__http_consume(JSContext* ctx);
16
+
17
+ static inline MIKHttpState*& mik__http(MIKRuntime* r) {
18
+ return reinterpret_cast<MIKHttpState*&>(r->module_data[mik__http_slot]);
19
+ }
20
+
21
+ /* ── Test helpers ─────────────────────────────────────────────────── */
22
+
23
+ /*
24
+ * IMPORTANT: Unity's TEST_ASSERT_* macros use longjmp on failure, which
25
+ * skips any cleanup after the assertion. To avoid leaking the entire
26
+ * runtime on a test failure, we structure every test as:
27
+ *
28
+ * 1. setup() + test work
29
+ * 2. extract observed values into C locals (int, string copy, etc.)
30
+ * 3. teardown()
31
+ * 4. TEST_ASSERT_* against the extracted locals
32
+ *
33
+ * See fs_js_test.cpp for the same convention.
34
+ */
35
+
36
+ static MIKRuntime* rt;
37
+ static JSContext* ctx;
38
+
39
+ static void setup() {
40
+ rt = MIK_NewRuntime();
41
+ ctx = MIK_GetJSContext(rt);
42
+ }
43
+
44
+ static void ensure_http_initialized() {
45
+ const char* code = "import { request } from 'native:http';";
46
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test", code, strlen(code));
47
+ if (!JS_IsException(ret)) {
48
+ JS_FreeValue(ctx, ret);
49
+ mik__execute_jobs(ctx);
50
+ }
51
+ }
52
+
53
+ static void teardown() { MIK_FreeRuntime(rt); }
54
+
55
+ static JSValue eval_module(const char* code) {
56
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test", code, strlen(code));
57
+ if (!JS_IsException(ret)) {
58
+ JS_FreeValue(ctx, ret);
59
+ mik__execute_jobs(ctx);
60
+ }
61
+ return ret;
62
+ }
63
+
64
+ static void push_headers(uint32_t id, int status, MIKHttpHeader* headers, size_t header_count) {
65
+ MIKHttpMsg m = {};
66
+ m.id = id;
67
+ m.kind = MIK_HTTP_MSG_HEADERS;
68
+ m.status = status;
69
+ m.headers = headers;
70
+ m.header_count = header_count;
71
+ xQueueSend(mik__http(rt)->result_queue, &m, 0);
72
+ }
73
+
74
+ static void push_chunk(uint32_t id, const char* s) {
75
+ size_t n = strlen(s);
76
+ uint8_t* data = static_cast<uint8_t*>(malloc(n));
77
+ memcpy(data, s, n);
78
+ MIKHttpMsg m = {};
79
+ m.id = id;
80
+ m.kind = MIK_HTTP_MSG_CHUNK;
81
+ m.chunk_data = data;
82
+ m.chunk_len = n;
83
+ xSemaphoreTake(mik__http(rt)->inflight, 0);
84
+ xQueueSend(mik__http(rt)->result_queue, &m, 0);
85
+ }
86
+
87
+ static void push_end(uint32_t id) {
88
+ MIKHttpMsg m = {};
89
+ m.id = id;
90
+ m.kind = MIK_HTTP_MSG_END;
91
+ xQueueSend(mik__http(rt)->result_queue, &m, 0);
92
+ }
93
+
94
+ static void push_error(uint32_t id, const char* err_msg, bool cancelled = false) {
95
+ MIKHttpMsg m = {};
96
+ m.id = id;
97
+ m.kind = MIK_HTTP_MSG_ERROR;
98
+ m.is_cancelled = cancelled;
99
+ m.error_message = strdup(err_msg);
100
+ xQueueSend(mik__http(rt)->result_queue, &m, 0);
101
+ }
102
+
103
+ /* Copy a JS string property from globalThis into a caller-provided buffer.
104
+ * Used when we need to extract a value before teardown so TEST_ASSERT can
105
+ * run safely after cleanup. */
106
+ static void read_global_string(const char* key, char* out, size_t out_len) {
107
+ JSValue global = JS_GetGlobalObject(ctx);
108
+ JSValue v = JS_GetPropertyStr(ctx, global, key);
109
+ const char* s = JS_ToCString(ctx, v);
110
+ if (s) {
111
+ strncpy(out, s, out_len - 1);
112
+ out[out_len - 1] = '\0';
113
+ JS_FreeCString(ctx, s);
114
+ } else {
115
+ out[0] = '\0';
116
+ }
117
+ JS_FreeValue(ctx, v);
118
+ JS_FreeValue(ctx, global);
119
+ }
120
+
121
+ static int32_t read_global_int(const char* key) {
122
+ JSValue global = JS_GetGlobalObject(ctx);
123
+ JSValue v = JS_GetPropertyStr(ctx, global, key);
124
+ int32_t n = 0;
125
+ JS_ToInt32(ctx, &n, v);
126
+ JS_FreeValue(ctx, v);
127
+ JS_FreeValue(ctx, global);
128
+ return n;
129
+ }
130
+
131
+ static bool read_global_bool(const char* key) {
132
+ JSValue global = JS_GetGlobalObject(ctx);
133
+ JSValue v = JS_GetPropertyStr(ctx, global, key);
134
+ bool b = JS_ToBool(ctx, v);
135
+ JS_FreeValue(ctx, v);
136
+ JS_FreeValue(ctx, global);
137
+ return b;
138
+ }
139
+
140
+ /* ── Module structure tests ───────────────────────────────────────── */
141
+
142
+ TEST_CASE("native:http exports request, nextMessage, cancel", "[http]") {
143
+ setup();
144
+
145
+ JSValue ret = eval_module(R"(
146
+ import { request, nextMessage, cancel } from "native:http";
147
+ globalThis.__requestType = typeof request;
148
+ globalThis.__nextType = typeof nextMessage;
149
+ globalThis.__cancelType = typeof cancel;
150
+ )");
151
+ bool evalOk = !JS_IsException(ret);
152
+
153
+ char types[3][32] = {};
154
+ read_global_string("__requestType", types[0], sizeof(types[0]));
155
+ read_global_string("__nextType", types[1], sizeof(types[1]));
156
+ read_global_string("__cancelType", types[2], sizeof(types[2]));
157
+
158
+ teardown();
159
+
160
+ TEST_ASSERT_TRUE_MESSAGE(evalOk, "Module eval should not throw");
161
+ TEST_ASSERT_EQUAL_STRING("function", types[0]);
162
+ TEST_ASSERT_EQUAL_STRING("function", types[1]);
163
+ TEST_ASSERT_EQUAL_STRING("function", types[2]);
164
+ }
165
+
166
+ TEST_CASE("native:http cannot be imported from non-internal module", "[http]") {
167
+ setup();
168
+ const char* code = R"(
169
+ import { request } from "native:http";
170
+ )";
171
+ JSValue ret = MIK_EvalModuleContent(ctx, "/user/app.js", code, strlen(code));
172
+ bool wasException = JS_IsException(ret);
173
+ if (wasException) {
174
+ JSValue exc = JS_GetException(ctx);
175
+ JS_FreeValue(ctx, exc);
176
+ }
177
+ teardown();
178
+
179
+ TEST_ASSERT_TRUE_MESSAGE(wasException,
180
+ "Importing native:http from user module should fail");
181
+ }
182
+
183
+ TEST_CASE("http state is lazily initialized", "[http]") {
184
+ setup();
185
+ bool nullBeforeImport = (mik__http(rt) == nullptr);
186
+ ensure_http_initialized();
187
+ bool notNullAfterImport = (mik__http(rt) != nullptr);
188
+ bool queueOk = notNullAfterImport && (mik__http(rt)->result_queue != nullptr);
189
+ bool inflightOk = notNullAfterImport && (mik__http(rt)->inflight != nullptr);
190
+ size_t pendingCount = notNullAfterImport ? mik__http(rt)->pending_count : 999;
191
+ teardown();
192
+
193
+ TEST_ASSERT_TRUE_MESSAGE(nullBeforeImport, "HTTP state should not exist before import");
194
+ TEST_ASSERT_TRUE_MESSAGE(notNullAfterImport,
195
+ "HTTP state should be initialized after import");
196
+ TEST_ASSERT_TRUE(queueOk);
197
+ TEST_ASSERT_TRUE(inflightOk);
198
+ TEST_ASSERT_EQUAL_MESSAGE(0, pendingCount, "No pending requests initially");
199
+ }
200
+
201
+ TEST_CASE("http consume with no messages is a no-op", "[http]") {
202
+ setup();
203
+ ensure_http_initialized();
204
+ mik__http_consume(ctx);
205
+ size_t pendingCount = mik__http(rt)->pending_count;
206
+ teardown();
207
+
208
+ TEST_ASSERT_EQUAL(0, pendingCount);
209
+ }
210
+
211
+ /* ── Streaming protocol tests ─────────────────────────────────────── */
212
+
213
+ /* Seed a pending entry that isn't backed by a real background task.
214
+ * The `cancelled` flag is intentionally null so `mik__http_destroy`
215
+ * doesn't wait for a terminator message that will never arrive.
216
+ * Tests that exercise the cancel path allocate it themselves. */
217
+ static JSValue seed_pending(uint32_t* out_id) {
218
+ MIKHttpPending pending = {};
219
+ pending.id = mik__http(rt)->next_id++;
220
+ JSValue headers_promise = MIK_InitPromise(ctx, &pending.headers_promise);
221
+ mik__http(rt)->pending[mik__http(rt)->pending_count++] = pending;
222
+ *out_id = pending.id;
223
+ return headers_promise;
224
+ }
225
+
226
+ TEST_CASE("headers message resolves headers promise with status and pairs", "[http]") {
227
+ setup();
228
+ ensure_http_initialized();
229
+
230
+ uint32_t id;
231
+ JSValue headers_promise = seed_pending(&id);
232
+
233
+ JSValue global = JS_GetGlobalObject(ctx);
234
+ JS_SetPropertyStr(ctx, global, "__hp", JS_DupValue(ctx, headers_promise));
235
+ JS_FreeValue(ctx, headers_promise);
236
+ JS_FreeValue(ctx, global);
237
+
238
+ eval_module(R"(
239
+ globalThis.__status = -1;
240
+ globalThis.__headerKey = "";
241
+ globalThis.__hp.then((r) => {
242
+ globalThis.__status = r.value.status;
243
+ if (r.value.headers.length > 0) globalThis.__headerKey = r.value.headers[0][0];
244
+ });
245
+ )");
246
+
247
+ auto* hdrs = static_cast<MIKHttpHeader*>(malloc(sizeof(MIKHttpHeader)));
248
+ hdrs[0].key = strdup("Content-Type");
249
+ hdrs[0].value = strdup("text/plain");
250
+ push_headers(id, 200, hdrs, 1);
251
+ mik__http_consume(ctx);
252
+ mik__execute_jobs(ctx);
253
+
254
+ int32_t status = read_global_int("__status");
255
+ char headerKey[64];
256
+ read_global_string("__headerKey", headerKey, sizeof(headerKey));
257
+
258
+ push_end(id);
259
+ mik__http_consume(ctx);
260
+ mik__execute_jobs(ctx);
261
+ teardown();
262
+
263
+ TEST_ASSERT_EQUAL_INT32_MESSAGE(200, status, "headers promise delivers status");
264
+ TEST_ASSERT_EQUAL_STRING("Content-Type", headerKey);
265
+ }
266
+
267
+ TEST_CASE("nextMessage delivers buffered chunks in order", "[http]") {
268
+ setup();
269
+ ensure_http_initialized();
270
+
271
+ uint32_t id;
272
+ JSValue headers_promise = seed_pending(&id);
273
+ JS_FreeValue(ctx, headers_promise);
274
+
275
+ char code[512];
276
+ snprintf(code, sizeof(code),
277
+ "import { nextMessage } from 'native:http';"
278
+ "globalThis.__body = '';"
279
+ "(async () => {"
280
+ " for (;;) {"
281
+ " const m = await nextMessage(%u);"
282
+ " if (m.kind === 'chunk') globalThis.__body += new TextDecoder().decode(m.data);"
283
+ " else break;"
284
+ " }"
285
+ "})();",
286
+ id);
287
+ eval_module(code);
288
+
289
+ push_headers(id, 200, nullptr, 0);
290
+ push_chunk(id, "Hello, ");
291
+ push_chunk(id, "world");
292
+ push_end(id);
293
+ mik__http_consume(ctx);
294
+ mik__execute_jobs(ctx);
295
+
296
+ char body[64];
297
+ read_global_string("__body", body, sizeof(body));
298
+ size_t pendingCount = mik__http(rt)->pending_count;
299
+ teardown();
300
+
301
+ TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, world", body, "chunks concat in order");
302
+ TEST_ASSERT_EQUAL_MESSAGE(0, pendingCount, "pending cleared after END delivered");
303
+ }
304
+
305
+ TEST_CASE("error message surfaces kind/cancelled/message fields", "[http]") {
306
+ setup();
307
+ ensure_http_initialized();
308
+
309
+ uint32_t id;
310
+ JSValue headers_promise = seed_pending(&id);
311
+ JS_FreeValue(ctx, headers_promise);
312
+
313
+ char code[512];
314
+ snprintf(code, sizeof(code),
315
+ "import { nextMessage } from 'native:http';"
316
+ "globalThis.__kind = '';"
317
+ "globalThis.__cancelled = null;"
318
+ "globalThis.__message = '';"
319
+ "(async () => {"
320
+ " const m = await nextMessage(%u);"
321
+ " globalThis.__kind = m.kind;"
322
+ " if (m.kind === 'error') {"
323
+ " globalThis.__cancelled = m.cancelled;"
324
+ " globalThis.__message = m.message;"
325
+ " }"
326
+ "})();",
327
+ id);
328
+ eval_module(code);
329
+
330
+ push_headers(id, 200, nullptr, 0);
331
+ push_error(id, "connection reset", /*cancelled=*/false);
332
+ mik__http_consume(ctx);
333
+ mik__execute_jobs(ctx);
334
+
335
+ char kind[32];
336
+ char message[64];
337
+ read_global_string("__kind", kind, sizeof(kind));
338
+ read_global_string("__message", message, sizeof(message));
339
+ size_t pendingCount = mik__http(rt)->pending_count;
340
+ teardown();
341
+
342
+ TEST_ASSERT_EQUAL_STRING("error", kind);
343
+ TEST_ASSERT_EQUAL_STRING_MESSAGE("connection reset", message,
344
+ "error message carried through to JS");
345
+ TEST_ASSERT_EQUAL_MESSAGE(0, pendingCount, "pending cleared after error");
346
+ }
347
+
348
+ TEST_CASE("error before headers resolves headers promise with failure", "[http]") {
349
+ setup();
350
+ ensure_http_initialized();
351
+
352
+ uint32_t id;
353
+ JSValue headers_promise = seed_pending(&id);
354
+
355
+ JSValue global = JS_GetGlobalObject(ctx);
356
+ JS_SetPropertyStr(ctx, global, "__hp", JS_DupValue(ctx, headers_promise));
357
+ JS_FreeValue(ctx, headers_promise);
358
+ JS_FreeValue(ctx, global);
359
+
360
+ /* Native error is a {name, message} tag; the pre-headers error path
361
+ * emits "Network" (or "Aborted" when cancelled). */
362
+ eval_module(R"(
363
+ globalThis.__ok = "pending";
364
+ globalThis.__errorName = "";
365
+ globalThis.__hp.then((r) => {
366
+ globalThis.__ok = r.ok ? "true" : "false";
367
+ if (!r.ok) globalThis.__errorName = r.error.name;
368
+ });
369
+ )");
370
+
371
+ push_error(id, "connection refused", /*cancelled=*/false);
372
+ mik__http_consume(ctx);
373
+ mik__execute_jobs(ctx);
374
+
375
+ char ok[16];
376
+ char errorName[32];
377
+ read_global_string("__ok", ok, sizeof(ok));
378
+ read_global_string("__errorName", errorName, sizeof(errorName));
379
+ size_t pendingCount = mik__http(rt)->pending_count;
380
+ teardown();
381
+
382
+ TEST_ASSERT_EQUAL_STRING_MESSAGE(
383
+ "false", ok, "headers promise resolves with ok=false on pre-headers error");
384
+ TEST_ASSERT_EQUAL_STRING_MESSAGE("Network", errorName,
385
+ "error name is Network for non-cancelled failure");
386
+ TEST_ASSERT_EQUAL_MESSAGE(0, pendingCount, "pending dropped after pre-headers error");
387
+ }
388
+
389
+ TEST_CASE("error before headers also resolves pending nextMessage", "[http]") {
390
+ setup();
391
+ ensure_http_initialized();
392
+
393
+ uint32_t id;
394
+ JSValue headers_promise = seed_pending(&id);
395
+
396
+ JSValue global = JS_GetGlobalObject(ctx);
397
+ JS_SetPropertyStr(ctx, global, "__hp", JS_DupValue(ctx, headers_promise));
398
+ JS_FreeValue(ctx, headers_promise);
399
+ JS_FreeValue(ctx, global);
400
+
401
+ /* Caller awaits nextMessage before awaiting headers — unusual but
402
+ * defensible. Pre-headers error must resolve both, or the async
403
+ * nextMessage hangs and JSValues leak. */
404
+ char code[512];
405
+ snprintf(code, sizeof(code),
406
+ "import { nextMessage } from 'native:http';"
407
+ "globalThis.__headersOk = null;"
408
+ "globalThis.__nextKind = '';"
409
+ "globalThis.__hp.then((r) => { globalThis.__headersOk = r.ok; });"
410
+ "(async () => {"
411
+ " const m = await nextMessage(%u);"
412
+ " globalThis.__nextKind = m.kind;"
413
+ "})();",
414
+ id);
415
+ eval_module(code);
416
+
417
+ push_error(id, "DNS failure", /*cancelled=*/false);
418
+ mik__http_consume(ctx);
419
+ mik__execute_jobs(ctx);
420
+
421
+ bool headersOk = read_global_bool("__headersOk");
422
+ char nextKind[32];
423
+ read_global_string("__nextKind", nextKind, sizeof(nextKind));
424
+ size_t pendingCount = mik__http(rt)->pending_count;
425
+ teardown();
426
+
427
+ TEST_ASSERT_FALSE_MESSAGE(headersOk, "headers resolved with ok=false");
428
+ TEST_ASSERT_EQUAL_STRING_MESSAGE("error", nextKind, "nextMessage resolved with error kind");
429
+ TEST_ASSERT_EQUAL_MESSAGE(0, pendingCount, "pending dropped");
430
+ }
431
+
432
+ TEST_CASE("cancel sets the pending cancelled flag", "[http]") {
433
+ setup();
434
+ ensure_http_initialized();
435
+
436
+ uint32_t id;
437
+ JSValue headers_promise = seed_pending(&id);
438
+ JS_FreeValue(ctx, headers_promise);
439
+
440
+ MIKHttpPending* p = nullptr;
441
+ for (size_t i = 0; i < mik__http(rt)->pending_count; i++) {
442
+ if (mik__http(rt)->pending[i].id == id) p = &mik__http(rt)->pending[i];
443
+ }
444
+ bool foundPending = (p != nullptr);
445
+ /* Allocate the cancel flag that a real background task would own.
446
+ * Ownership transfers to the pending entry: mik__pending_drop (called
447
+ * from the ERROR-before-HEADERS path in mik__http_consume below) will
448
+ * free it. Do not delete it from the test. */
449
+ auto* flag = new std::atomic<bool>(false);
450
+ if (p) p->cancelled = flag;
451
+
452
+ /* JS calls cancel(id) and also awaits nextMessage so the pending entry
453
+ * drains cleanly once we simulate the task's ERROR response. */
454
+ char code[512];
455
+ snprintf(code, sizeof(code),
456
+ "import { cancel, nextMessage } from 'native:http';"
457
+ "cancel(%u);"
458
+ "(async () => { await nextMessage(%u); })();",
459
+ id, id);
460
+ eval_module(code);
461
+ bool flagAfterCancel = flag->load();
462
+
463
+ /* Simulate the task posting an error after seeing the cancelled flag.
464
+ * consume resolves via the pre-headers-error path and drops the pending. */
465
+ push_error(id, "cancelled", true);
466
+ mik__http_consume(ctx);
467
+ mik__execute_jobs(ctx);
468
+
469
+ size_t pendingCount = mik__http(rt)->pending_count;
470
+ teardown();
471
+
472
+ TEST_ASSERT_TRUE(foundPending);
473
+ TEST_ASSERT_TRUE_MESSAGE(flagAfterCancel, "cancel sets the shared flag");
474
+ TEST_ASSERT_EQUAL_MESSAGE(0, pendingCount, "pending dropped after cancellation settles");
475
+ }
@@ -0,0 +1,138 @@
1
+ #include "../mik_i2c.cpp"
2
+ #include "unity.h"
3
+
4
+ /* I2C test pins — use GPIO 6 (SDA) and GPIO 7 (SCL) on ESP32-C6.
5
+ * Tests that don't require a connected device will still work with
6
+ * floating pins; device-dependent tests are clearly marked. */
7
+ #define I2C_TEST_SDA 6
8
+ #define I2C_TEST_SCL 7
9
+ #define I2C_TEST_PORT 0
10
+
11
+ /* ── Bus lifecycle tests ─────────────────────────────────────────── */
12
+
13
+ TEST_CASE("I2C begin and end succeed", "[i2c]") {
14
+ i2c_master_bus_handle_t bus = nullptr;
15
+
16
+ i2c_master_bus_config_t bus_cfg = {};
17
+ bus_cfg.i2c_port = static_cast<i2c_port_num_t>(I2C_TEST_PORT);
18
+ bus_cfg.sda_io_num = static_cast<gpio_num_t>(I2C_TEST_SDA);
19
+ bus_cfg.scl_io_num = static_cast<gpio_num_t>(I2C_TEST_SCL);
20
+ bus_cfg.clk_source = I2C_CLK_SRC_DEFAULT;
21
+ bus_cfg.glitch_ignore_cnt = 7;
22
+ bus_cfg.flags.enable_internal_pullup = true;
23
+
24
+ esp_err_t err = i2c_new_master_bus(&bus_cfg, &bus);
25
+ TEST_ASSERT_EQUAL(ESP_OK, err);
26
+ TEST_ASSERT_NOT_NULL(bus);
27
+
28
+ err = i2c_del_master_bus(bus);
29
+ TEST_ASSERT_EQUAL(ESP_OK, err);
30
+ }
31
+
32
+ TEST_CASE("I2C begin twice is idempotent via state guard", "[i2c]") {
33
+ /* Simulate what the JS begin() does — second call is a no-op */
34
+ MIKI2CState s = {};
35
+ s.port = I2C_TEST_PORT;
36
+ s.sda = I2C_TEST_SDA;
37
+ s.scl = I2C_TEST_SCL;
38
+ s.freq = MIK_I2C_DEFAULT_FREQ;
39
+ s.timeout_ms = MIK_I2C_DEFAULT_TIMEOUT_MS;
40
+ s.begun = false;
41
+
42
+ /* First begin */
43
+ i2c_master_bus_config_t bus_cfg = {};
44
+ bus_cfg.i2c_port = static_cast<i2c_port_num_t>(s.port);
45
+ bus_cfg.sda_io_num = static_cast<gpio_num_t>(s.sda);
46
+ bus_cfg.scl_io_num = static_cast<gpio_num_t>(s.scl);
47
+ bus_cfg.clk_source = I2C_CLK_SRC_DEFAULT;
48
+ bus_cfg.glitch_ignore_cnt = 7;
49
+ bus_cfg.flags.enable_internal_pullup = true;
50
+
51
+ esp_err_t err = i2c_new_master_bus(&bus_cfg, &s.bus);
52
+ TEST_ASSERT_EQUAL(ESP_OK, err);
53
+ s.begun = true;
54
+
55
+ /* Second begin — should skip because s.begun is true */
56
+ TEST_ASSERT_TRUE(s.begun);
57
+
58
+ /* Cleanup */
59
+ i2c_del_master_bus(s.bus);
60
+ }
61
+
62
+ TEST_CASE("I2C probe single address does not crash", "[i2c]") {
63
+ i2c_master_bus_handle_t bus = nullptr;
64
+
65
+ i2c_master_bus_config_t bus_cfg = {};
66
+ bus_cfg.i2c_port = static_cast<i2c_port_num_t>(I2C_TEST_PORT);
67
+ bus_cfg.sda_io_num = static_cast<gpio_num_t>(I2C_TEST_SDA);
68
+ bus_cfg.scl_io_num = static_cast<gpio_num_t>(I2C_TEST_SCL);
69
+ bus_cfg.clk_source = I2C_CLK_SRC_DEFAULT;
70
+ bus_cfg.glitch_ignore_cnt = 7;
71
+ bus_cfg.flags.enable_internal_pullup = true;
72
+
73
+ esp_err_t err = i2c_new_master_bus(&bus_cfg, &bus);
74
+ TEST_ASSERT_EQUAL(ESP_OK, err);
75
+
76
+ /* Probe a single address — just verify it returns without crashing.
77
+ * Without pull-ups or a connected device, this will timeout (expected). */
78
+ esp_err_t probe_err = i2c_master_probe(bus, 0x44, 50);
79
+ /* Either ESP_OK (device found) or an error (timeout/nack) is fine */
80
+ TEST_ASSERT_TRUE(probe_err == ESP_OK || probe_err != ESP_OK);
81
+
82
+ i2c_del_master_bus(bus);
83
+ }
84
+
85
+ TEST_CASE("I2C add and remove device succeeds", "[i2c]") {
86
+ i2c_master_bus_handle_t bus = nullptr;
87
+
88
+ i2c_master_bus_config_t bus_cfg = {};
89
+ bus_cfg.i2c_port = static_cast<i2c_port_num_t>(I2C_TEST_PORT);
90
+ bus_cfg.sda_io_num = static_cast<gpio_num_t>(I2C_TEST_SDA);
91
+ bus_cfg.scl_io_num = static_cast<gpio_num_t>(I2C_TEST_SCL);
92
+ bus_cfg.clk_source = I2C_CLK_SRC_DEFAULT;
93
+ bus_cfg.glitch_ignore_cnt = 7;
94
+ bus_cfg.flags.enable_internal_pullup = true;
95
+
96
+ esp_err_t err = i2c_new_master_bus(&bus_cfg, &bus);
97
+ TEST_ASSERT_EQUAL(ESP_OK, err);
98
+
99
+ i2c_master_dev_handle_t dev = nullptr;
100
+ i2c_device_config_t dev_cfg = {};
101
+ dev_cfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
102
+ dev_cfg.device_address = 0x44; // common sensor address
103
+ dev_cfg.scl_speed_hz = 100000;
104
+
105
+ err = i2c_master_bus_add_device(bus, &dev_cfg, &dev);
106
+ TEST_ASSERT_EQUAL(ESP_OK, err);
107
+ TEST_ASSERT_NOT_NULL(dev);
108
+
109
+ err = i2c_master_bus_rm_device(dev);
110
+ TEST_ASSERT_EQUAL(ESP_OK, err);
111
+
112
+ i2c_del_master_bus(bus);
113
+ }
114
+
115
+ TEST_CASE("I2C pending write buffer works", "[i2c]") {
116
+ MIKI2CState s = {};
117
+ mik__i2c_clear_pending(&s);
118
+
119
+ TEST_ASSERT_FALSE(s.has_pending_write);
120
+ TEST_ASSERT_EQUAL(0, s.pending_write_len);
121
+
122
+ /* Simulate buffering a write */
123
+ uint8_t data[] = {0xFD};
124
+ memcpy(s.pending_write, data, sizeof(data));
125
+ s.pending_write_len = sizeof(data);
126
+ s.pending_write_addr = 0x44;
127
+ s.has_pending_write = true;
128
+
129
+ TEST_ASSERT_TRUE(s.has_pending_write);
130
+ TEST_ASSERT_EQUAL(1, s.pending_write_len);
131
+ TEST_ASSERT_EQUAL(0x44, s.pending_write_addr);
132
+ TEST_ASSERT_EQUAL(0xFD, s.pending_write[0]);
133
+
134
+ /* Clear */
135
+ mik__i2c_clear_pending(&s);
136
+ TEST_ASSERT_FALSE(s.has_pending_write);
137
+ TEST_ASSERT_EQUAL(0, s.pending_write_len);
138
+ }