char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
25.5 KiB585 linesraw
1mod common;
2
3use std::fs;
4use std::process::{Command, Stdio};
5
6fn run(
7    source: &str,
8    flags: &[&str],
9    fixtures: &[(&str, &str)],
10) -> (tempfile::TempDir, std::process::Output) {
11    let dir = tempfile::tempdir().unwrap();
12    fs::write(dir.path().join("main.js"), source).unwrap();
13    fs::write(dir.path().join("data"), b"abcdef").unwrap();
14    for (name, contents) in fixtures {
15        let path = dir.path().join(name);
16        fs::create_dir_all(path.parent().unwrap()).unwrap();
17        fs::write(path, contents).unwrap();
18    }
19    let child = Command::new(env!("CARGO_BIN_EXE_avril"))
20        .arg("run")
21        .args(flags)
22        .arg("main.js")
23        .current_dir(dir.path())
24        .stdin(Stdio::null())
25        .stdout(Stdio::piped())
26        .stderr(Stdio::piped())
27        .spawn()
28        .unwrap();
29    let output = common::wait(child);
30    assert!(
31        output.status.success(),
32        "{}",
33        String::from_utf8_lossy(&output.stderr)
34    );
35    (dir, output)
36}
37
38#[test]
39fn mixed_operations_share_handles_and_cursor_without_running_js() {
40    let (dir, output) = run(
41        r#"
42        import { open as openAsync, FileHandle } from 'avril:std';
43        import { open, create, SeekMode, stat, FileHandle as SyncHandle } from 'avril:std/sync';
44        import { readTextFile } from 'avril:fs/sync';
45        if (FileHandle !== SyncHandle) throw Error('Separate handle families');
46        const file = open('data', {read: true, write: true});
47        if (!(file instanceof FileHandle)) throw Error('Wrong handle type');
48        const a = new Uint8Array(2), b = new Uint8Array(2);
49        let ran = false;
50        queueMicrotask(() => ran = true);
51        const first = file.read(a);
52        if (file.readSync(b) !== 2 || new TextDecoder().decode(b) !== 'cd') throw Error('Read order');
53        if (ran) throw Error('Sync read ran JavaScript');
54        const seek = file.seek(0, SeekMode.Start);
55        if (file.writeSync(new Uint8Array([88])) !== 1) throw Error('Write');
56        if (file.seekSync(-1, SeekMode.End) !== 5 || file.readSync(b) !== 1 || b[0] !== 102) throw Error('Seek');
57        if (file.readSync(b) !== null || file.readSync(new Uint8Array()) !== 0) throw Error('EOF');
58        if (await first !== 2 || new TextDecoder().decode(a) !== 'ab' || await seek !== 0) throw Error('Async completion');
59        file.truncateSync(3); file.syncDataSync(); file.syncSync();
60        if (file.statSync().size !== 3 || stat(new URL('./data', import.meta.url)).size !== 3) throw Error('Stat');
61        file.close();
62        const reopened = await openAsync('data');
63        if (reopened.readSync(b) !== 2 || b[0] !== 88) throw Error('Async-opened handle');
64        reopened[Symbol.dispose]();
65        const out = create('out');
66        const writes = [];
67        for (let i = 0; i < 100; i++) writes.push(out.write(new Uint8Array([65 + i % 26])));
68        out.seekSync(0, 0);
69        const all = new Uint8Array(100);
70        if (out.readSync(all) !== 100 || all.some((v, i) => v !== 65 + i % 26)) throw Error('Queued writes');
71        if ((await Promise.all(writes)).some(n => n !== 1)) throw Error('Write completions');
72        out.close();
73        if (readTextFile('data') !== 'Xbc') throw Error('Persisted contents');
74        console.log('mixed IO');
75    "#,
76        &[],
77        &[],
78    );
79    assert_eq!(output.stdout, b"mixed IO\n");
80    assert_eq!(fs::read(dir.path().join("data")).unwrap(), b"Xbc");
81}
82
83#[test]
84fn synchronous_paths_and_workflows_preserve_contents() {
85    let (dir, _) = run(
86        r#"
87        import { mkdir, readDir, copyFile, rename, remove, stat, lstat, realPath, truncate } from 'avril:std/sync';
88        import { mkdirs, copy, move, exists, readFile, readTextFile, writeFile, writeTextFile } from 'avril:fs/sync';
89        mkdir('tree'); mkdirs('tree/deep/nested'); mkdirs('tree/deep/nested');
90        const bytes = new Uint8Array(200003);
91        for (let i = 0; i < bytes.length; i++) bytes[i] = i % 251;
92        writeFile('tree/deep/nested/bytes', bytes);
93        const actual = readFile(new URL('./tree/deep/nested/bytes', import.meta.url));
94        if (actual.length !== bytes.length || actual.some((v, i) => v !== bytes[i])) throw Error('Partial transfer');
95        writeTextFile('tree/text', 'héllo 🌷'); writeTextFile('tree/text', '!', {append: true});
96        writeFile('empty', new Uint8Array());
97        if (readFile('empty').length !== 0 || readTextFile('tree/text') !== 'héllo 🌷!') throw Error('Text/empty');
98        const entries = [...readDir('tree')].sort((a, b) => a.name.localeCompare(b.name));
99        if (entries.length !== 2 || entries[0].name !== 'deep' || !entries[0].isDirectory || !entries[1].isFile) throw Error('Directory entries');
100        copy('tree', 'copies/tree');
101        move('copies/tree', 'moved');
102        if (exists('copies/tree') || !exists('moved/deep/nested/bytes')) throw Error('Move');
103        copyFile('moved/text', 'copied'); rename('copied', 'renamed'); truncate('renamed', 2);
104        if (stat('renamed').size !== 2 || !lstat('renamed').isFile) throw Error('Path metadata');
105        if (!realPath('renamed').endsWith('renamed') || exists('missing')) throw Error('Resolution');
106        remove('tree', {recursive: true}); remove('empty');
107        if (exists('tree')) throw Error('Remove');
108    "#,
109        &[],
110        &[],
111    );
112    assert_eq!(fs::read(dir.path().join("renamed")).unwrap(), b"h\xc3");
113    assert_eq!(
114        fs::read_to_string(dir.path().join("moved/text")).unwrap(),
115        "héllo 🌷!"
116    );
117    assert_eq!(
118        fs::metadata(dir.path().join("moved/deep/nested/bytes"))
119            .unwrap()
120            .len(),
121        200003
122    );
123}
124
125#[test]
126fn synchronous_temp_paths_match_async_behavior() {
127    run(
128        r#"
129        import { makeTempDir, makeTempFile, exists } from 'avril:fs/sync';
130        import { stat, remove, errors } from 'avril:std/sync';
131        const plain = makeTempDir();
132        if (!/^[0-9a-f]{8,16}$/.test(plain.split('/').pop())) throw Error('Hex name');
133        const named = makeTempDir({ prefix: 'sync-', suffix: '.d' });
134        if (!named.split('/').pop().startsWith('sync-') || !named.endsWith('.d')) throw Error('Affixes');
135        const file = makeTempFile({ dir: plain });
136        if (!file.startsWith(plain + '/') || stat(file).size !== 0 || !stat(file).isFile) throw Error('Temp file');
137        const seen = new Set();
138        for (let i = 0; i < 10; i++) { const candidate = makeTempFile({ dir: plain }); if (seen.has(candidate)) throw Error('Collision'); seen.add(candidate); }
139        try { makeTempDir({ suffix: 'a|b' }); throw Error('Accepted characters'); } catch (e) { if (!(e instanceof errors.InvalidInput)) throw e; }
140        try { makeTempFile({ dir: plain + '/missing' }); throw Error('Accepted parents'); } catch (e) { if (!(e instanceof errors.NotFound)) throw e; }
141        const dirInfo = stat(named);
142        if (!dirInfo.isDirectory) throw Error('Directory kind');
143        if (dirInfo.mode !== null && (dirInfo.mode & 0o777) !== 0o700) throw Error('Directory mode');
144        remove(plain, { recursive: true }); remove(named, { recursive: true });
145        if (exists(plain) || exists(named)) throw Error('Cleanup');
146    "#,
147        &[],
148        &[],
149    );
150}
151
152#[test]
153fn failures_throw_synchronously_and_handles_keep_their_brand_and_lifecycle() {
154    run(
155        r#"
156        import { open, create, stat, mkdir, remove, errors, FileHandle } from 'avril:std/sync';
157        import { readFile, writeFile, writeTextFile, mkdirs, copy, exists } from 'avril:fs/sync';
158        function fails(call, C) {
159            try { call(); } catch (e) { if (e instanceof C) return; throw e; }
160            throw Error('Expected synchronous ' + C.name);
161        }
162        fails(() => open('missing'), errors.NotFound);
163        fails(() => open(), TypeError);
164        fails(() => open('data', {}), errors.InvalidInput);
165        fails(() => open('data', {read: true, truncate: true}), errors.InvalidInput);
166        fails(() => open('data', {read: 'yes'}), TypeError);
167        fails(() => open('data', {write: true, mode: 8192}), RangeError);
168        fails(() => open(new URL('https://example.test')), errors.InvalidInput);
169        fails(() => open('.'), errors.NotSupported);
170        fails(() => stat(''), errors.NotFound);
171        fails(() => mkdir('directory', {mode: -1}), RangeError);
172        fails(() => mkdirs('directory', {mode: -1}), RangeError);
173        fails(() => remove('data', {recursive: 1}), TypeError);
174        fails(() => copy('data', 'copy', {dereference: 1}), TypeError);
175        fails(() => writeTextFile('new', 123), TypeError);
176        fails(() => writeFile('new', new Uint8Array(), {create: false}), errors.NotFound);
177        fails(() => writeFile('data', new Uint8Array(), {createNew: true}), errors.AlreadyExists);
178        fails(() => readFile(), TypeError);
179        if (exists('new') || exists('directory') || exists('copy')) throw Error('Validation had side effects');
180        const file = open('data');
181        fails(() => file.writeSync(new Uint8Array([1])), errors.BadResource);
182        fails(() => file.readSync(new DataView(new ArrayBuffer(1))), TypeError);
183        fails(() => file.seekSync(2 ** 53, 0), RangeError);
184        fails(() => FileHandle.prototype.readSync.call({}, new Uint8Array(1)), TypeError);
185        fails(() => structuredClone(file), DOMException);
186        const detached = new Uint8Array(1);
187        structuredClone(detached.buffer, {transfer: [detached.buffer]});
188        fails(() => file.readSync(detached), TypeError);
189        const read = FileHandle.prototype.readSync, close = FileHandle.prototype.close;
190        Object.setPrototypeOf(file, null);
191        if (read.call(file, new Uint8Array(1)) !== 1) throw Error('Private brand');
192        close.call(file);
193        fails(() => read.call(file, new Uint8Array(1)), errors.BadResource);
194        const out = create('out');
195        const shared = new Uint8Array(new SharedArrayBuffer(3)); shared.set([1, 2, 3]);
196        if (out.writeSync(shared) !== 3) throw Error('Shared buffer');
197        out[Symbol.dispose](); out[Symbol.dispose]();
198        fails(() => out.statSync(), errors.BadResource);
199        fails(() => out.close(), errors.BadResource);
200    "#,
201        &[],
202        &[],
203    );
204}
205
206#[test]
207fn synchronous_io_obeys_scoped_and_live_capabilities() {
208    let (dir, _) = run(
209        r#"
210        import { pledge } from 'avril:std';
211        import { open, stat, readDir, errors } from 'avril:std/sync';
212        import { readTextFile, writeTextFile, copy, exists } from 'avril:fs/sync';
213        function denied(call) {
214            try { call(); } catch (e) { if (e instanceof errors.PermissionDenied) return; throw e; }
215            throw Error('Retained authority');
216        }
217        if (readTextFile('data') !== 'abcdef') throw Error('Read grant');
218        writeTextFile('out', 'allowed');
219        denied(() => writeTextFile('forbidden', 'no'));
220        denied(() => readTextFile('allowed/secret'));
221        denied(() => exists('allowed/secret'));
222        const entries = [...readDir('allowed')];
223        if (entries.length !== 1 || entries[0].name !== 'public') throw Error('Listing leaked denied child');
224        denied(() => copy('allowed', 'target'));
225        const file = open('data', {read: true, write: true});
226        pledge({read: false, write: false});
227        for (const call of [
228            () => file.readSync(new Uint8Array(1)), () => file.writeSync(new Uint8Array([1])),
229            () => file.statSync(), () => file.truncateSync(), () => file.syncSync(),
230            () => stat('data'), () => readTextFile('data'), () => writeTextFile('out', 'no'),
231        ]) denied(call);
232        file.close();
233    "#,
234        &[
235            "-0",
236            "--allow-read=data",
237            "--allow-write=data",
238            "--allow-write=out",
239            "--allow-read=allowed/",
240            "--deny-read=allowed/secret",
241            "--allow-write=target/",
242        ],
243        &[("allowed/public", "yes"), ("allowed/secret", "no")],
244    );
245    assert_eq!(fs::read(dir.path().join("data")).unwrap(), b"abcdef");
246    assert_eq!(fs::read(dir.path().join("out")).unwrap(), b"allowed");
247    assert!(!dir.path().join("forbidden").exists());
248    assert!(!dir.path().join("target").exists());
249}
250
251#[test]
252fn streams_and_async_callbacks_can_use_sync_handles() {
253    let (dir, _) = run(
254        r#"
255        import { open, create } from 'avril:std/sync';
256        import { readTextFile, writeTextFile } from 'avril:fs/sync';
257        const source = open('data'), target = create('streamed');
258        await source.readable.pipeTo(target.writable);
259        await new Promise(resolve => setTimeout(() => {
260            writeTextFile('callback', readTextFile('streamed')); resolve();
261        }, 0));
262    "#,
263        &[],
264        &[],
265    );
266    assert_eq!(fs::read(dir.path().join("callback")).unwrap(), b"abcdef");
267}
268
269#[test]
270fn workers_can_use_sync_io_from_their_event_loop() {
271    let (dir, _) = run(
272        r#"
273        const worker = new Worker(new URL('./worker.js', import.meta.url), {type: 'module'});
274        const answer = new Promise((resolve, reject) => {
275            worker.onmessage = ({data}) => resolve(data);
276            worker.onerror = reject;
277        });
278        worker.postMessage('go');
279        if (await answer !== 'abcdef!') throw Error('Worker IO');
280        worker.terminate();
281    "#,
282        &[],
283        &[(
284            "worker.js",
285            r#"
286        import { readTextFile, writeTextFile } from 'avril:fs/sync';
287        onmessage = () => {
288            writeTextFile('worker-out', readTextFile('data') + '!');
289            postMessage(readTextFile('worker-out'));
290        };
291    "#,
292        )],
293    );
294    assert_eq!(fs::read(dir.path().join("worker-out")).unwrap(), b"abcdef!");
295}
296
297#[test]
298fn sync_modules_keep_helpers_private_and_capture_intrinsics() {
299    run(
300        r#"
301        import * as std from 'avril:std/sync';
302        import * as fs from 'avril:fs/sync';
303        import { pledge } from 'avril:std';
304        if ('path' in std || 'openArgs' in std || 'writeOptions' in fs) throw Error('Private exports');
305        for (const name of ['ext:file/sync.js', 'ext:file/fs_sync.js']) {
306            try { await import(name); throw Error('Private import succeeded'); }
307            catch (e) { if (!e.message.includes('private')) throw e; }
308        }
309        const log = console.log;
310        const fail = () => { throw Error('Mutable intrinsic'); };
311        std.FileHandle.prototype.readSync = fail;
312        std.FileHandle.prototype.writeSync = fail;
313        Uint8Array.prototype.set = fail;
314        TextEncoder.prototype.encode = fail;
315        TextDecoder.prototype.decode = fail;
316        globalThis.Uint8Array = fail;
317        Object.prototype.toJSON = fail;
318        fs.writeTextFile('out', fs.readTextFile('data'));
319        if (fs.readTextFile('out') !== 'abcdef') throw Error('Contents');
320        pledge({read: false, write: false, jsExec: false});
321        if ((await import('avril:fs/sync')).readFile !== fs.readFile) throw Error('Lost builtin');
322        log('intrinsics');
323    "#,
324        &[],
325        &[],
326    );
327}
328
329#[cfg(unix)]
330#[test]
331fn sync_posix_metadata_links_and_permissions_match_async_handles() {
332    run(
333        r#"
334        import { open, stat, lstat, chmod, chown, lchown, utime, lutime, link, symlink, readLink, realPath, errors } from 'avril:std/sync';
335        import { copy, readTextFile, mkdirs, exists } from 'avril:fs/sync';
336        const file = open('data', {read: true, write: true});
337        const initial = file.statSync();
338        file.fchmodSync(0o640); file.fchownSync(initial.uid, initial.gid);
339        file.futimeSync(new Date(1234567890123), 1234567890.125);
340        let info = stat('data');
341        if ((info.mode & 0o777) !== 0o640 || +info.mtime !== 1234567890125 || +info.atime !== 1234567890123) throw Error('Handle metadata');
342        chmod('data', 0o600); chown('data', initial.uid, null); utime('data', 1234567000, new Date(1234567890456));
343        if ((stat('data').mode & 0o777) !== 0o600 || +stat('data').mtime !== 1234567890456) throw Error('Path metadata');
344        link('data', 'hard'); symlink('data', 'soft');
345        if (!lstat('soft').isSymlink || !stat('soft').isFile || readLink('soft') !== 'data' || realPath('soft') !== realPath('data')) throw Error('Links');
346        lchown('soft', initial.uid, initial.gid); lutime('soft', 1234500000, 1234500000);
347        if (+lstat('soft').mtime !== 1234500000000 || +stat('data').mtime !== 1234567890456) throw Error('Link times');
348        copy('soft', 'preserved'); copy('soft', 'followed', {dereference: true});
349        if (!lstat('preserved').isSymlink || lstat('followed').isSymlink || readTextFile('followed') !== 'abcdef') throw Error('Copy links');
350        mkdirs('tree'); symlink('.', 'tree/cycle');
351        try { copy('tree', 'cycle-copy', {dereference: true}); throw Error('Accepted cycle'); }
352        catch (e) { if (!(e instanceof errors.InvalidInput)) throw e; }
353        file.flockSync(); file.funlockSync(); file.flockSync(false); await file.funlock(); file.close();
354        if (!exists('hard')) throw Error('Hard link');
355    "#,
356        &[],
357        &[],
358    );
359}
360
361#[cfg(unix)]
362#[test]
363fn synchronous_calls_do_not_follow_ungranted_symlink_targets() {
364    let (dir, _) = run(
365        r#"
366        import { pledge } from 'avril:std';
367        import { mkdir, symlink, open, stat, truncate, errors } from 'avril:std/sync';
368        import { readFile, writeTextFile, copy, exists } from 'avril:fs/sync';
369        mkdir('allowed'); symlink('../data', 'allowed/alias');
370        pledge({read: {allow: ['allowed/']}, write: {allow: ['allowed/', 'out']}});
371        for (const call of [
372            () => open('allowed/alias'), () => stat('allowed/alias'), () => truncate('allowed/alias'),
373            () => readFile('allowed/alias'), () => exists('allowed/alias'), () => writeTextFile('allowed/alias', 'no'),
374            () => copy('allowed/alias', 'out', {dereference: true}),
375        ]) {
376            try { call(); throw Error('Followed denied target'); }
377            catch (e) { if (!(e instanceof errors.PermissionDenied)) throw e; }
378        }
379    "#,
380        &[],
381        &[],
382    );
383    assert_eq!(fs::read(dir.path().join("data")).unwrap(), b"abcdef");
384    assert!(!dir.path().join("out").exists());
385}
386
387#[cfg(unix)]
388#[test]
389fn terminating_a_worker_interrupts_a_synchronous_lock_wait() {
390    run(
391        r#"
392        import { open, create } from 'avril:std/sync';
393        const file = open('data', {read: true, write: true}), lifetime = create('lifetime');
394        file.flockSync();
395        const worker = new Worker(new URL('./worker.js', import.meta.url), {type: 'module'});
396        const ready = new Promise((resolve, reject) => {
397            worker.onmessage = resolve; worker.onerror = reject;
398        });
399        worker.postMessage('go');
400        await ready;
401        await new Promise(resolve => setTimeout(resolve, 30));
402        worker.terminate();
403        lifetime.flockSync(); lifetime.funlockSync(); lifetime.close();
404        file.funlockSync(); file.close();
405    "#,
406        &[],
407        &[(
408            "worker.js",
409            r#"
410        import { open } from 'avril:std/sync';
411        onmessage = () => {
412            const file = open('data', {read: true, write: true});
413            const lifetime = open('lifetime', {read: true, write: true});
414            lifetime.flockSync();
415            postMessage('waiting');
416            file.flockSync();
417            throw Error('Acquired locked file');
418        };
419    "#,
420        )],
421    );
422}
423
424#[cfg(unix)]
425#[test]
426fn synchronous_lock_waits_for_another_isolate_without_pumping_js() {
427    run(
428        r#"
429        import { open } from 'avril:std/sync';
430        const worker = new Worker(new URL('./worker.js', import.meta.url), {type: 'module'});
431        await new Promise((resolve, reject) => { worker.onmessage = resolve; worker.onerror = reject; });
432        const file = open('data', {read: true, write: true});
433        let callback = false;
434        queueMicrotask(() => callback = true);
435        worker.postMessage('release');
436        file.flockSync();
437        if (callback) throw Error('Lock wait pumped JS');
438        file.funlockSync(); file.close(); worker.terminate();
439    "#,
440        &[],
441        &[(
442            "worker.js",
443            r#"
444        import { open } from 'avril:std/sync';
445        const file = open('data', {read: true, write: true});
446        file.flockSync();
447        onmessage = () => setTimeout(() => { file.funlockSync(); file.close(); }, 50);
448        postMessage('locked');
449    "#,
450        )],
451    );
452}
453
454#[test]
455fn repl_and_test_callbacks_support_sync_io() {
456    use std::io::Write;
457
458    let dir = tempfile::tempdir().unwrap();
459    fs::write(dir.path().join("data"), "hello").unwrap();
460    fs::write(
461        dir.path().join("io_test.ts"),
462        r#"
463        import { test } from 'avril:test';
464        import { open, type FileInfo } from 'avril:std/sync';
465        import { readTextFile, writeTextFile } from 'avril:fs/sync';
466        test('sync IO after await', async () => {
467            await new Promise(resolve => setTimeout(resolve, 0));
468            const file = open('data');
469            const info: FileInfo = file.statSync();
470            if (info.size !== 5) throw Error('Metadata');
471            file.close();
472            writeTextFile('test-out', readTextFile('data'));
473        });
474    "#,
475    )
476    .unwrap();
477    let child = Command::new(env!("CARGO_BIN_EXE_avril"))
478        .args(["test", "io_test.ts"])
479        .current_dir(dir.path())
480        .stdout(Stdio::piped())
481        .stderr(Stdio::piped())
482        .spawn()
483        .unwrap();
484    let output = common::wait(child);
485    assert!(
486        output.status.success(),
487        "{}",
488        String::from_utf8_lossy(&output.stderr)
489    );
490    assert_eq!(fs::read(dir.path().join("test-out")).unwrap(), b"hello");
491
492    let mut child = Command::new(env!("CARGO_BIN_EXE_avril"))
493        .arg("repl")
494        .current_dir(dir.path())
495        .stdin(Stdio::piped())
496        .stdout(Stdio::piped())
497        .stderr(Stdio::piped())
498        .spawn()
499        .unwrap();
500    child.stdin.take().unwrap().write_all(b"import { readTextFile, writeTextFile } from 'avril:fs/sync'\nawait new Promise(resolve => setTimeout(() => { writeTextFile('repl-out', readTextFile('data')); resolve('done'); }, 0))\n.exit\n").unwrap();
501    let output = common::wait(child);
502    assert!(
503        output.status.success(),
504        "{}",
505        String::from_utf8_lossy(&output.stderr)
506    );
507    assert_eq!(fs::read(dir.path().join("repl-out")).unwrap(), b"hello");
508}
509
510#[cfg(unix)]
511#[test]
512fn sync_opened_handles_can_be_lent_to_subprocesses() {
513    let (dir, _) = run(
514        r#"
515        import { runSync } from 'avril:std';
516        import { open, create } from 'avril:std/sync';
517        const input = open('data'), output = create('out');
518        input.seekSync(2, 0);
519        runSync('/bin/sh', ['-c', 'cat'], {stdin: {handle: input}, stdout: {handle: output}});
520        if (output.statSync().size !== 4) throw Error('Borrowed handle');
521        input.close(); output.close();
522    "#,
523        &[],
524        &[],
525    );
526    assert_eq!(fs::read(dir.path().join("out")).unwrap(), b"cdef");
527}
528
529#[test]
530fn sync_writev_shares_the_queue_orders_with_async_and_validates() {
531    let (dir, output) = run(
532        r#"
533        import { open, create, SeekMode, errors, FileHandle } from 'avril:std/sync';
534        import { open as openAsync } from 'avril:std';
535        function fails(call, C) {
536            try { call(); } catch (e) { if (e instanceof C) return; throw e; }
537            throw Error('Expected synchronous ' + C.name);
538        }
539        const file = create('out');
540        const pending = file.write(new Uint8Array([1,2,3]));
541        if (file.writevSync([new Uint8Array([4]), new Uint8Array(0), new Uint8Array([5,6])]) !== 3) throw Error('Sync count');
542        if (await pending !== 3) throw Error('Async count');
543        if (file.writevSync([]) !== 0) throw Error('Empty');
544        if (file.seekSync(0, SeekMode.Start) !== 0) throw Error('Seek');
545        const back = new Uint8Array(6);
546        if (file.readSync(back) !== 6 || back.join() !== '1,2,3,4,5,6') throw Error('Order ' + back.join());
547        const wide = create('wide'), segs = [new Uint8Array(0)];
548        for (let i = 0; i < 20; i++) { const view = new Uint8Array(5000); view.fill(i + 1); segs.push(view); }
549        segs.push(new Uint8Array(0));
550        if (wide.writevSync(segs) !== 100000) throw Error('Wide');
551        wide.close();
552        const view = new Uint8Array([7,8]);
553        if (file.writevSync([view.subarray(1)]) !== 1) throw Error('Subarray');
554        view.fill(0);
555        const shared = new Uint8Array(new SharedArrayBuffer(1)); shared[0] = 9;
556        if (file.writevSync([shared]) !== 1) throw Error('Shared');
557        fails(() => file.writevSync('bytes'), TypeError);
558        fails(() => file.writevSync([new Uint8Array([1]), null]), TypeError);
559        fails(() => file.writevSync(new Uint8Array([1])), TypeError);
560        fails(() => FileHandle.prototype.writevSync.call({}, [new Uint8Array([1])]), TypeError);
561        const detached = new Uint8Array([1]);
562        structuredClone(detached.buffer, {transfer: [detached.buffer]});
563        fails(() => file.writevSync([detached]), TypeError);
564        file.close();
565        fails(() => file.writevSync([new Uint8Array([1])]), errors.BadResource);
566        const check = await openAsync('out');
567        const all = new Uint8Array(8);
568        let offset = 0, n;
569        while (offset < 8 && (n = await check.read(all.subarray(offset))) !== null) offset += n;
570        if (offset !== 8 || all.join() !== '1,2,3,4,5,6,8,9') throw Error('Bytes ' + all.join());
571        check.close();
572        console.log('sync-vectors');
573    "#,
574        &[],
575        &[],
576    );
577    assert_eq!(output.stdout, b"sync-vectors\n");
578    let wide = fs::read(dir.path().join("wide")).unwrap();
579    let expected: Vec<u8> = (1..=20).flat_map(|value| vec![value; 5000]).collect();
580    assert_eq!(wide, expected);
581    assert_eq!(
582        fs::read(dir.path().join("out")).unwrap(),
583        [1, 2, 3, 4, 5, 6, 8, 9]
584    );
585}