Skip to content

Commit 26d8de8

Browse files
committed
test(oracle): trace dynamic import resolution
1 parent 744038d commit 26d8de8

15 files changed

Lines changed: 869 additions & 0 deletions

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ tests/test262-implemented-leaves-global-*.tsv whitespace=-blank-at-eol
5959
tests/test262-destructuring-assignment-global-*.tsv whitespace=-blank-at-eol
6060
tests/test262-module-static-core-*.tsv whitespace=-blank-at-eol
6161
tests/test262-module-loader-linker-a-*.tsv whitespace=-blank-at-eol
62+
63+
# Upstream source context in test-only patches may intentionally retain spaces.
64+
dev-support/quickjs/*.patch whitespace=-blank-at-eol

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ jobs:
7676
./scripts/test-quickjs-fixtures.sh --validate
7777
./scripts/test-module-import-attributes-oracle.sh --validate
7878
./scripts/test-module-json-oracle.sh --validate
79+
./scripts/test-quickjs-dynamic-import-trace.sh --validate
7980
./scripts/check-test262-host-boundary.sh
8081
./scripts/test-test262.sh --spec dev-support/test262/current.conf --check
8182
node scripts/current-test262-metrics.mjs --check-docs
@@ -147,6 +148,8 @@ jobs:
147148
oracle="$oracle_source/qjs"
148149
QJS_ORACLE_ARCHIVE_FIXTURE="$QJS_ORACLE_CACHE/quickjs-2026-06-04.tar.xz" \
149150
./scripts/test-build-quickjs-oracle-cache.sh
151+
QJS_ORACLE_ARCHIVE="$QJS_ORACLE_CACHE/quickjs-2026-06-04.tar.xz" \
152+
./scripts/test-quickjs-dynamic-import-trace.sh
150153
suite=$(./scripts/prepare-test262.sh)
151154
node scripts/audit-negative-diagnostics.mjs \
152155
--contracts dev-support/test262/negative-diagnostics.tsv \

dev-support/quickjs/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Dynamic-import trace oracle
2+
3+
`dynamic-import-trace-2026-06-04.patch` is a test-only instrumentation patch
4+
for the authenticated QuickJS 2026-06-04 archive. It is not product code and
5+
must never be applied to `target/oracle/quickjs-2026-06-04`.
6+
7+
`scripts/build-quickjs-dynamic-import-trace.sh` verifies the archive, patch,
8+
and patched-source fingerprints, extracts into a fresh temporary directory,
9+
applies the patch with zero fuzz, builds `run-test262`, and prints the temporary
10+
trace source directory. The same isolated extraction also produces
11+
`run-test262.stock` for behavior A/B checks. Set
12+
`QJS_DYNAMIC_IMPORT_TRACE_BUILD_DIR` to choose the temporary build root;
13+
callers should remove the build root when done.
14+
15+
The patched runner is fail-closed. It enables tracing only when
16+
`QJS_OXIDE_DYNAMIC_IMPORT_TRACE=1` is present at process startup and descriptor
17+
3 is already a regular file. It verifies and duplicates descriptor 3 before
18+
opening config or test inputs, requires an explicit `-T 1`, refuses
19+
`$262.agent`, and exits with status 74 if a trace write fails. These constraints
20+
make the deliberately simple multi-write encoder deterministic:
21+
22+
```sh
23+
build=$(scripts/build-quickjs-dynamic-import-trace.sh)
24+
QJS_OXIDE_DYNAMIC_IMPORT_TRACE=1 \
25+
"$build/run-test262" -T 1 -N --module test.js 3>trace.tsv
26+
```
27+
28+
Each record begins with `QJODI1` and contains only ASCII. Arbitrary byte strings
29+
use `<byte-length>:<lowercase-hex>`; `-` represents null. Every record includes
30+
the root test identity (`JSRuntime.rt_info` or the loader opaque) before its
31+
event-specific fields. Record types are:
32+
33+
- `N`: root, base name, requested name, normalized name
34+
- `L`: root, loader request, effective filesystem path, outcome, saved load
35+
`errno`
36+
- `T`: root, compiled module name, parser-derived `has_tla` bit
37+
38+
`scripts/parse-quickjs-dynamic-import-trace.mjs` is the strict parser. The
39+
regression test covers computed and bare requests, missing-file `errno`, and
40+
top-level await in blocks/templates versus await inside a nested function. It
41+
also compares the stock and instrumented runners' exit status, stdout, and
42+
stderr to detect instrumentation-induced behavior drift. As a separate opt-in
43+
gate, the test passes an otherwise-valid descriptor 3 without the environment
44+
variable and requires the trace to remain empty.
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
--- a/quickjs.c
2+
+++ b/quickjs.c
3+
@@ -32,6 +32,10 @@
4+
#include <time.h>
5+
#include <fenv.h>
6+
#include <math.h>
7+
+#include <errno.h>
8+
+#include <fcntl.h>
9+
+#include <sys/stat.h>
10+
+#include <unistd.h>
11+
#if defined(__APPLE__)
12+
#include <malloc/malloc.h>
13+
#elif defined(__linux__) || defined(__GLIBC__)
14+
@@ -46,7 +50,156 @@
15+
#include "libregexp.h"
16+
#include "libunicode.h"
17+
#include "dtoa.h"
18+
+
19+
+/* quickjs-oxide test-only dynamic-import trace protocol (QJODI1).
20+
+ *
21+
+ * The runner explicitly enables tracing before opening inputs, after verifying
22+
+ * that descriptor 3 is a regular file. We duplicate it once so later fopen()
23+
+ * calls cannot redirect trace output. All caller-controlled strings are
24+
+ * encoded as byte-length-prefixed hexadecimal, and trace I/O preserves errno.
25+
+ * A write failure terminates this test-only oracle instead of yielding partial
26+
+ * evidence that could be mistaken for a complete trace.
27+
+ */
28+
+#define QJO_TRACE_FD 3
29+
+
30+
+static int qjo_trace_fd = -1;
31+
+static BOOL qjo_trace_enabled;
32+
+
33+
+int qjs_oxide_trace_init(void)
34+
+{
35+
+ const char *enabled = getenv("QJS_OXIDE_DYNAMIC_IMPORT_TRACE");
36+
+ struct stat st;
37+
+ int saved_errno = errno;
38+
+ int flags;
39+
+
40+
+ if (!enabled) {
41+
+ errno = saved_errno;
42+
+ return 0;
43+
+ }
44+
+ if (strcmp(enabled, "1") || fstat(QJO_TRACE_FD, &st) < 0 ||
45+
+ !S_ISREG(st.st_mode))
46+
+ return -1;
47+
+ qjo_trace_fd = dup(QJO_TRACE_FD);
48+
+ if (qjo_trace_fd < 0)
49+
+ return -1;
50+
+ flags = fcntl(qjo_trace_fd, F_GETFD);
51+
+ if (flags < 0 || fcntl(qjo_trace_fd, F_SETFD, flags | FD_CLOEXEC) < 0) {
52+
+ close(qjo_trace_fd);
53+
+ qjo_trace_fd = -1;
54+
+ return -1;
55+
+ }
56+
+ qjo_trace_enabled = TRUE;
57+
+ errno = saved_errno;
58+
+ return 1;
59+
+}
60+
+
61+
+BOOL qjs_oxide_trace_is_enabled(void)
62+
+{
63+
+ return qjo_trace_enabled;
64+
+}
65+
+
66+
+static void qjo_trace_write(const char *buf, size_t len)
67+
+{
68+
+ while (len != 0) {
69+
+ ssize_t ret = write(qjo_trace_fd, buf, len);
70+
+ if (ret > 0) {
71+
+ buf += ret;
72+
+ len -= ret;
73+
+ } else if (ret < 0 && errno == EINTR) {
74+
+ continue;
75+
+ } else {
76+
+ _exit(74);
77+
+ }
78+
+ }
79+
+}
80+
+
81+
+static void qjo_trace_cstr(const char *str)
82+
+{
83+
+ static const char hex[] = "0123456789abcdef";
84+
+ char len_buf[32], encoded[256];
85+
+ size_t len, offset;
86+
+ int len_len;
87+
+
88+
+ if (!str) {
89+
+ qjo_trace_write("-", 1);
90+
+ return;
91+
+ }
92+
+ len = strlen(str);
93+
+ len_len = snprintf(len_buf, sizeof(len_buf), "%zu:", len);
94+
+ if (len_len > 0)
95+
+ qjo_trace_write(len_buf, len_len);
96+
+ for (offset = 0; offset < len;) {
97+
+ size_t i, count = len - offset;
98+
+ if (count > sizeof(encoded) / 2)
99+
+ count = sizeof(encoded) / 2;
100+
+ for (i = 0; i < count; i++) {
101+
+ unsigned char c = (unsigned char)str[offset + i];
102+
+ encoded[i * 2] = hex[c >> 4];
103+
+ encoded[i * 2 + 1] = hex[c & 15];
104+
+ }
105+
+ qjo_trace_write(encoded, count * 2);
106+
+ offset += count;
107+
+ }
108+
+}
109+
110+
+static void qjo_trace_normalize(const char *root_name, const char *base_name,
111+
+ const char *request, const char *normalized)
112+
+{
113+
+ int saved_errno = errno;
114+
+
115+
+ if (qjo_trace_enabled) {
116+
+ qjo_trace_write("QJODI1\tN\t", sizeof("QJODI1\tN\t") - 1);
117+
+ qjo_trace_cstr(root_name);
118+
+ qjo_trace_write("\t", 1);
119+
+ qjo_trace_cstr(base_name);
120+
+ qjo_trace_write("\t", 1);
121+
+ qjo_trace_cstr(request);
122+
+ qjo_trace_write("\t", 1);
123+
+ qjo_trace_cstr(normalized);
124+
+ qjo_trace_write("\n", 1);
125+
+ }
126+
+ errno = saved_errno;
127+
+}
128+
+
129+
+void qjs_oxide_trace_loader(const char *root_name, const char *request,
130+
+ const char *effective_path, const char *outcome,
131+
+ int load_errno)
132+
+{
133+
+ char errno_buf[32];
134+
+ int saved_errno = errno;
135+
+ int errno_len;
136+
+
137+
+ if (qjo_trace_enabled) {
138+
+ qjo_trace_write("QJODI1\tL\t", sizeof("QJODI1\tL\t") - 1);
139+
+ qjo_trace_cstr(root_name);
140+
+ qjo_trace_write("\t", 1);
141+
+ qjo_trace_cstr(request);
142+
+ qjo_trace_write("\t", 1);
143+
+ qjo_trace_cstr(effective_path);
144+
+ qjo_trace_write("\t", 1);
145+
+ qjo_trace_cstr(outcome);
146+
+ errno_len = snprintf(errno_buf, sizeof(errno_buf), "\t%d\n", load_errno);
147+
+ if (errno_len > 0)
148+
+ qjo_trace_write(errno_buf, errno_len);
149+
+ }
150+
+ errno = saved_errno;
151+
+}
152+
+
153+
+static void qjo_trace_tla(const char *root_name, const char *module_name,
154+
+ BOOL has_tla)
155+
+{
156+
+ int saved_errno = errno;
157+
+
158+
+ if (qjo_trace_enabled) {
159+
+ qjo_trace_write("QJODI1\tT\t", sizeof("QJODI1\tT\t") - 1);
160+
+ qjo_trace_cstr(root_name);
161+
+ qjo_trace_write("\t", 1);
162+
+ qjo_trace_cstr(module_name);
163+
+ qjo_trace_write(has_tla ? "\t1\n" : "\t0\n", 3);
164+
+ }
165+
+ errno = saved_errno;
166+
+}
167+
+
168+
#define OPTIMIZE 1
169+
#define SHORT_OPCODES 1
170+
#if defined(__EMSCRIPTEN__)
171+
@@ -29908,6 +30061,7 @@
172+
cname = rt->module_normalize_func(ctx, base_cname, cname1,
173+
rt->module_loader_opaque);
174+
}
175+
+ qjo_trace_normalize(ctx->rt->rt_info, base_cname, cname1, cname);
176+
if (!cname)
177+
return NULL;
178+
179+
@@ -37156,8 +37310,10 @@
180+
goto fail1;
181+
}
182+
183+
- if (m != NULL)
184+
+ if (m != NULL) {
185+
m->has_tla = fd->has_await;
186+
+ qjo_trace_tla(ctx->rt->rt_info, filename, m->has_tla);
187+
+ }
188+
189+
/* create the function object and all the enclosed functions */
190+
fun_obj = js_create_function(ctx, fd);
191+
--- a/run-test262.c
192+
+++ b/run-test262.c
193+
@@ -46,6 +46,13 @@
194+
195+
#define CMD_NAME "run-test262"
196+
197+
+extern int qjs_oxide_trace_init(void);
198+
+extern BOOL qjs_oxide_trace_is_enabled(void);
199+
+extern void qjs_oxide_trace_loader(const char *root_name,
200+
+ const char *request,
201+
+ const char *effective_path,
202+
+ const char *outcome, int load_errno);
203+
+
204+
typedef struct namelist_t {
205+
char **array;
206+
int count;
207+
@@ -663,6 +670,8 @@
208+
Test262Agent *agent;
209+
pthread_attr_t attr;
210+
211+
+ if (qjs_oxide_trace_is_enabled())
212+
+ fatal(1, "QJODI1 trace does not support $262.agent");
213+
if (JS_GetContextOpaque(ctx) != NULL)
214+
return JS_ThrowTypeError(ctx, "cannot be called inside an agent");
215+
216+
@@ -970,9 +979,12 @@
217+
uint8_t *buf;
218+
JSModuleDef *m;
219+
char *filename, *slash, path[1024];
220+
+ const char *requested_name;
221+
+ int load_errno;
222+
223+
// interpret import("bar.js") from path/to/foo.js as
224+
// import("path/to/bar.js") but leave import("./bar.js") untouched
225+
+ requested_name = module_name;
226+
filename = opaque;
227+
if (!strchr(module_name, '/')) {
228+
slash = strrchr(filename, '/');
229+
@@ -984,7 +996,11 @@
230+
}
231+
232+
buf = js_load_file(ctx, &buf_len, module_name);
233+
+ load_errno = errno;
234+
if (!buf) {
235+
+ qjs_oxide_trace_loader(filename, requested_name, module_name,
236+
+ "read_error", load_errno);
237+
+ errno = load_errno;
238+
JS_ThrowReferenceError(ctx, "could not load module filename '%s'",
239+
module_name);
240+
return NULL;
241+
@@ -995,10 +1011,15 @@
242+
JSValue val;
243+
val = JS_ParseJSON(ctx, (char *)buf, buf_len, module_name);
244+
js_free(ctx, buf);
245+
- if (JS_IsException(val))
246+
+ if (JS_IsException(val)) {
247+
+ qjs_oxide_trace_loader(filename, requested_name, module_name,
248+
+ "json_error", 0);
249+
return NULL;
250+
+ }
251+
m = JS_NewCModule(ctx, module_name, json_module_init_test);
252+
if (!m) {
253+
+ qjs_oxide_trace_loader(filename, requested_name, module_name,
254+
+ "module_error", 0);
255+
JS_FreeValue(ctx, val);
256+
return NULL;
257+
}
258+
@@ -1011,12 +1032,16 @@
259+
func_val = JS_Eval(ctx, (char *)buf, buf_len, module_name,
260+
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
261+
js_free(ctx, buf);
262+
- if (JS_IsException(func_val))
263+
+ if (JS_IsException(func_val)) {
264+
+ qjs_oxide_trace_loader(filename, requested_name, module_name,
265+
+ "compile_error", 0);
266+
return NULL;
267+
+ }
268+
/* the module is already referenced, so we must free it */
269+
m = JS_VALUE_GET_PTR(func_val);
270+
JS_FreeValue(ctx, func_val);
271+
}
272+
+ qjs_oxide_trace_loader(filename, requested_name, module_name, "ok", 0);
273+
return m;
274+
}
275+
276+
@@ -2240,8 +2265,11 @@
277+
BOOL is_module = FALSE;
278+
BOOL can_block = TRUE;
279+
BOOL count_skipped_features = FALSE;
280+
+ BOOL trace_single_thread_requested = FALSE;
281+
clock_t clocks;
282+
283+
+ if (qjs_oxide_trace_init() < 0)
284+
+ fatal(1, "QJODI1 trace requires fd 3 to be a regular file and the opt-in value to be 1");
285+
init_thread_local_storage(tls);
286+
pthread_mutex_init(&stats_mutex, NULL);
287+
288+
@@ -2308,6 +2336,7 @@
289+
slow_test_threshold = atoi(get_opt_arg(arg, argv[optind++]));
290+
} else if (str_equal(arg, "-T")) {
291+
nthreads = atoi(get_opt_arg(arg, argv[optind++]));
292+
+ trace_single_thread_requested = (nthreads == 1);
293+
} else if (str_equal(arg, "-N")) {
294+
is_test262_harness = TRUE;
295+
} else if (str_equal(arg, "--module")) {
296+
@@ -2325,6 +2354,9 @@
297+
if (optind >= argc && !test_list.count)
298+
help();
299+
300+
+ if (qjs_oxide_trace_is_enabled() && !trace_single_thread_requested)
301+
+ fatal(1, "QJODI1 trace requires explicit -T 1");
302+
+
303+
if (is_test262_harness) {
304+
return run_test262_harness_test(tls, argv[optind], is_module, can_block);
305+
}

0 commit comments

Comments
 (0)