mod common; use std::fs; use std::process::{Command, Output}; fn run_in(dir: &std::path::Path, args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir) .args(args) .output() .unwrap() } fn run_main(dir: &std::path::Path, source: &str, args: &[&str]) -> Output { fs::write(dir.join("main.js"), source).unwrap(); run_in(dir, args) } fn success(output: Output) -> String { assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout).unwrap() } #[test] fn posix_lexical_vectors() { let output = common::run( r#" import * as path from 'avril:path/posix'; const cases = [ ['join', [], '.'], ['join', ['', ''], '.'], ['join', ['/a', 'b', 'c'], '/a/b/c'], ['join', ['/a/', '/b/'], '/a/b/'], ['join', ['a', '', 'b'], 'a/b'], ['join', ['a', '..', 'b'], 'b'], ['join', ['/', '..', 'a'], '/a'], ['join', ['', '.', ''], '.'], ['join', ['/a', '..'], '/'], ['join', ['a//b', 'c'], 'a/b/c'], ['join', ['/a/./b/'], '/a/b/'], ['join', ['foo', 'bar', 'baz', '..', 'qux'], 'foo/bar/qux'], ['join', ['aé', 'b'], 'aé/b'], ['join', ['/a\\b', 'c'], '/a\\b/c'], ['join', ['..', '..', 'a'], '../../a'], ['join', ['/', '/'], '/'], ['join', ['///a', 'b'], '/a/b'], ['join', ['a/', '/'], 'a/'], ['join', ['.', '.', '.'], '.'], ['resolve', ['/a', 'b', 'c'], '/a/b/c'], ['resolve', ['/a', '/b', 'c'], '/b/c'], ['resolve', ['/'], '/'], ['resolve', ['/a', '..', '..'], '/'], ['resolve', ['/a/b/', '../c/'], '/a/c'], ['resolve', ['/foo/bar', './baz'], '/foo/bar/baz'], ['resolve', ['/foo/bar', '/tmp/file/'], '/tmp/file'], ['resolve', ['/a', 'b', '', 'c'], '/a/b/c'], ['normalize', [''], '.'], ['normalize', ['.'], '.'], ['normalize', ['/'], '/'], ['normalize', ['//'], '/'], ['normalize', ['///'], '/'], ['normalize', ['/a//b/./c/../d/'], '/a/b/d/'], ['normalize', ['a/b/../../c'], 'c'], ['normalize', ['../a'], '../a'], ['normalize', ['a/'], 'a/'], ['normalize', ['a//'], 'a/'], ['normalize', ['/a/'], '/a/'], ['normalize', ['./a'], 'a'], ['normalize', ['a/./'], 'a/'], ['normalize', ['...'], '...'], ['normalize', ['....'], '....'], ['normalize', ['/a\\b'], '/a\\b'], ['normalize', ['a\\b'], 'a\\b'], ['normalize', ['a/.../b'], 'a/.../b'], ['normalize', ['/...'], '/...'], ['normalize', ['a/././b'], 'a/b'], ['relative', ['/a/b/c', '/a/d/e'], '../../d/e'], ['relative', ['/a/b', '/a/b'], ''], ['relative', ['/', '/foo'], 'foo'], ['relative', ['/foo', '/'], '..'], ['relative', ['/foo/bar', '/foo/bar/baz'], 'baz'], ['relative', ['/foo/bar/baz', '/foo/bar'], '..'], ['relative', ['/a', '/a'], ''], ['relative', ['/a/b/c/d', '/a/x/y'], '../../../x/y'], ['relative', ['/foo', '/foobar'], '../foobar'], ['relative', ['/foobar', '/foo'], '../foo'], ['relative', ['/a/./b', '/a/b/c'], 'c'], ['dirname', [''], '.'], ['dirname', ['.'], '.'], ['dirname', ['/'], '/'], ['dirname', ['//'], '/'], ['dirname', ['///'], '/'], ['dirname', ['/a'], '/'], ['dirname', ['/a/'], '/'], ['dirname', ['/a/b'], '/a'], ['dirname', ['/a/b/'], '/a'], ['dirname', ['a'], '.'], ['dirname', ['a/'], '.'], ['dirname', ['a/b'], 'a'], ['dirname', ['a/b/'], 'a'], ['dirname', ['..'], '.'], ['dirname', ['../'], '.'], ['dirname', ['/a/b/c.txt'], '/a/b'], ['dirname', ['a\\b'], '.'], ['dirname', ['/a\\b'], '/'], ['basename', [''], ''], ['basename', ['/'], ''], ['basename', ['//'], ''], ['basename', ['a'], 'a'], ['basename', ['a/'], 'a'], ['basename', ['a//'], 'a'], ['basename', ['/a/b/c'], 'c'], ['basename', ['/a/b/c/'], 'c'], ['basename', ['a.txt'], 'a.txt'], ['basename', ['.txt'], '.txt'], ['basename', ['..'], '..'], ['basename', ['...'], '...'], ['basename', ['.'], '.'], ['basename', ['a/.txt'], '.txt'], ['basename', ['a/..txt'], '..txt'], ['basename', ['a/b.tar.gz', '.gz'], 'b.tar'], ['basename', ['a/b.tar.gz', '.tar.gz'], 'b'], ['basename', ['a.txt', '.txt'], 'a'], ['basename', ['a.txt', 'a.txt'], ''], ['basename', ['a', 'a'], ''], ['basename', ['a/', 'a'], 'a'], ['basename', ['/a', '/a'], ''], ['basename', ['a.txt', ''], 'a.txt'], ['basename', ['ab', 'b'], 'a'], ['basename', ['ab', 'ab'], ''], ['extname', [''], ''], ['extname', ['a'], ''], ['extname', ['a.'], '.'], ['extname', ['.a'], ''], ['extname', ['..a'], '.a'], ['extname', ['a..'], '.'], ['extname', ['...'], '.'], ['extname', ['..'], ''], ['extname', ['.'], ''], ['extname', ['a.txt'], '.txt'], ['extname', ['a.tar.gz'], '.gz'], ['extname', ['.txt'], ''], ['extname', ['a/.txt'], ''], ['extname', ['a/..'], ''], ['extname', ['a/b.'], '.'], ['extname', ['/a.b/c'], ''], ['extname', ['/a.b/c.d'], '.d'], ['extname', ['a/b/c'], ''], ['extname', ['a\\b.txt'], '.txt'], ['extname', ['a b.c d'], '.c d'], ['isAbsolute', [''], false], ['isAbsolute', ['/'], true], ['isAbsolute', ['//'], true], ['isAbsolute', ['a'], false], ['isAbsolute', ['/a'], true], ['isAbsolute', ['./a'], false], ['isAbsolute', ['../a'], false], ['isAbsolute', ['a/b'], false], ['isAbsolute', ['\\'], false], ['isAbsolute', ['C:\\a'], false], ['isAbsolute', ['/a\\b'], true], ]; for (let i = 0; i < cases.length; i++) { const [fn, args, expected] = cases[i]; const actual = path[fn](...args); if (actual !== expected) { throw new Error(`${fn}(${args.map((a) => JSON.stringify(a)).join(', ')}) = ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`); } } if (path.sep !== '/' || path.delimiter !== ':') throw new Error('separators'); if (path.toNamespacedPath('/a/b') !== '/a/b' || path.toNamespacedPath('a') !== 'a' || path.toNamespacedPath('') !== '' || path.toNamespacedPath(42) !== 42) { throw new Error('toNamespacedPath passthrough'); } const formats = [ [{ dir: '/a/b', base: 'c.txt' }, '/a/b/c.txt'], [{ root: '/', base: 'c' }, '/c'], [{ dir: '/a', name: 'b', ext: 'txt' }, '/a/b.txt'], [{ dir: '/a', name: 'b', ext: '.txt' }, '/a/b.txt'], [{ name: 'b', ext: '.txt' }, 'b.txt'], [{ root: '/a/', base: '' }, '/a/'], [{ dir: '', base: '' }, ''], [{ root: '', base: 'x', name: 'ignored', ext: '.y' }, 'x'], [{ dir: '/a', root: '/b', base: 'c' }, '/a/c'], [{ name: '', ext: '' }, ''], ]; for (const [object, expected] of formats) { if (path.format(object) !== expected) throw new Error(`format ${JSON.stringify(object)}`); } const parsed = [ ['', ['', '', '', '', '']], ['/', ['/', '/', '', '', '']], ['//', ['/', '/', '', '', '']], ['/a', ['/', '/', 'a', '', 'a']], ['/a/', ['/', '/', 'a', '', 'a']], ['/a/b', ['/', '/a', 'b', '', 'b']], ['/a/b/', ['/', '/a', 'b', '', 'b']], ['/a/b/c.txt', ['/', '/a/b', 'c.txt', '.txt', 'c']], ['a', ['', '', 'a', '', 'a']], ['a/', ['', '', 'a', '', 'a']], ['a.txt', ['', '', 'a.txt', '.txt', 'a']], ['.txt', ['', '', '.txt', '', '.txt']], ['..', ['', '', '..', '', '..']], ['...', ['', '', '...', '.', '..']], ['a.tar.gz', ['', '', 'a.tar.gz', '.gz', 'a.tar']], ['a/b.tar.gz', ['', 'a', 'b.tar.gz', '.gz', 'b.tar']], ['/.txt', ['/', '/', '.txt', '', '.txt']], ['a/.txt', ['', 'a', '.txt', '', '.txt']], ['/a/..', ['/', '/a', '..', '', '..']], ['a\\b', ['', '', 'a\\b', '', 'a\\b']], ['/a\\b', ['/', '/', 'a\\b', '', 'a\\b']], ]; for (const [input, [root, dir, base, ext, name]] of parsed) { const actual = path.parse(input); if (actual.root !== root || actual.dir !== dir || actual.base !== base || actual.ext !== ext || actual.name !== name) { throw new Error(`parse(${JSON.stringify(input)}) = ${JSON.stringify(actual)}`); } } console.log('posix vectors'); "#, &[], ); assert_eq!(output.stdout, b"posix vectors\n"); } #[test] fn win32_lexical_vectors() { let output = common::run( r#" import * as path from 'avril:path/win32'; const cases = [ ['join', [], '.'], ['join', ['.'], '.'], ['join', ['', ''], '.'], ['join', ['C:\\a', 'b', 'c'], 'C:\\a\\b\\c'], ['join', ['C:\\a\\', 'b\\'], 'C:\\a\\b\\'], ['join', ['a', '', 'b'], 'a\\b'], ['join', ['a', '..', 'b'], 'b'], ['join', ['C:\\', '..', 'a'], 'C:\\a'], ['join', ['', '.', ''], '.'], ['join', ['C:\\a', '..'], 'C:\\'], ['join', ['a//b', 'c'], 'a\\b\\c'], ['join', ['//server', 'share'], '\\\\server\\share\\'], ['join', ['//server/share', 'dir'], '\\\\server\\share\\dir'], ['join', ['C:/a', 'b'], 'C:\\a\\b'], ['join', ['a/b', 'c'], 'a\\b\\c'], ['join', ['\\\\server\\share\\', 'x'], '\\\\server\\share\\x'], ['join', ['C:', 'a'], 'C:\\a'], ['join', ['\\', 'a'], '\\a'], ['join', ['\\\\', 'a'], '\\a'], ['join', ['\\\\\\', 'a'], '\\a'], ['join', ['aé', 'b'], 'aé\\b'], ['join', ['..', '..', 'a'], '..\\..\\a'], ['join', ['/', '/'], '\\'], ['join', ['a/', '/'], 'a\\'], ['resolve', ['C:\\a', 'b', 'c'], 'C:\\a\\b\\c'], ['resolve', ['C:\\a', 'D:\\b', 'c'], 'D:\\b\\c'], ['resolve', ['\\\\s\\sh', 'a'], '\\\\s\\sh\\a'], ['resolve', ['\\a', 'b'], '\\a\\b'], ['resolve', ['/a', 'b'], '\\a\\b'], ['resolve', ['C:\\'], 'C:\\'], ['resolve', ['C:\\a', '..', '..'], 'C:\\'], ['resolve', ['C:\\a\\b\\', '..\\c\\'], 'C:\\a\\c'], ['resolve', ['C:/a', './b'], 'C:\\a\\b'], ['resolve', ['\\\\?\\C:\\a', 'b'], '\\\\?\\C:\\a\\b'], ['resolve', ['\\\\.\\C:\\a', 'b'], '\\\\.\\C:\\a\\b'], ['normalize', [''], '.'], ['normalize', ['.'], '.'], ['normalize', ['\\'], '\\'], ['normalize', ['//'], '\\'], ['normalize', ['\\\\'], '\\'], ['normalize', ['C:\\'], 'C:\\'], ['normalize', ['C:'], 'C:.'], ['normalize', ['C:a'], 'C:a'], ['normalize', ['C:\\a\\\\b\\.\\c\\..\\d\\'], 'C:\\a\\b\\d\\'], ['normalize', ['a\\b\\..\\..\\c'], 'c'], ['normalize', ['..\\a'], '..\\a'], ['normalize', ['a\\'], 'a\\'], ['normalize', ['a\\\\'], 'a\\'], ['normalize', ['\\a\\'], '\\a\\'], ['normalize', ['.\\a'], 'a'], ['normalize', ['a\\.\\'], 'a\\'], ['normalize', ['...'], '...'], ['normalize', ['....'], '....'], ['normalize', ['C:..\\..'], 'C:..\\..'], ['normalize', ['C:..\\a'], 'C:..\\a'], ['normalize', ['\\a\\b\\'], '\\a\\b\\'], ['normalize', ['a/.../b'], 'a\\...\\b'], ['normalize', ['C:a\\b'], 'C:a\\b'], ['normalize', ['\\\\server\\share'], '\\\\server\\share\\'], ['normalize', ['\\\\server\\share\\'], '\\\\server\\share\\'], ['normalize', ['\\\\server\\share\\a\\..\\b'], '\\\\server\\share\\b'], ['relative', ['C:\\a\\b', 'C:\\a\\b'], ''], ['relative', ['C:\\a\\b\\c', 'C:\\a\\d\\e'], '..\\..\\d\\e'], ['relative', ['C:\\', 'C:\\foo'], 'foo'], ['relative', ['C:\\foo', 'C:\\'], '..'], ['relative', ['C:\\foo\\bar', 'C:\\foo\\bar\\baz'], 'baz'], ['relative', ['C:\\foo\\bar\\baz', 'C:\\foo\\bar'], '..'], ['relative', ['c:\\a', 'C:\\A'], ''], ['relative', ['C:\\a', 'D:\\b'], 'D:\\b'], ['relative', ['\\\\s\\sh\\a', '\\\\s\\sh\\b'], '..\\b'], ['relative', ['\\\\s\\sh\\a', '\\\\o\\sh\\b'], '\\\\o\\sh\\b'], ['relative', ['C:\\a\\b', 'C:\\a\\b\\'], ''], ['relative', ['C:\\Foo\\Bar', 'c:\\foo\\bar\\baz'], 'baz'], ['relative', ['C:\\a', 'C:\\ab'], '..\\ab'], ['relative', ['C:\\ab', 'C:\\a'], '..\\a'], ['relative', ['\\a\\b', '\\a\\c'], '..\\c'], ['dirname', [''], '.'], ['dirname', ['.'], '.'], ['dirname', ['\\'], '\\'], ['dirname', ['\\\\'], '\\'], ['dirname', ['C:\\'], 'C:\\'], ['dirname', ['C:'], 'C:'], ['dirname', ['C:a'], 'C:'], ['dirname', ['C:\\a'], 'C:\\'], ['dirname', ['C:\\a\\'], 'C:\\'], ['dirname', ['C:\\a\\b'], 'C:\\a'], ['dirname', ['C:\\a\\b\\'], 'C:\\a'], ['dirname', ['a'], '.'], ['dirname', ['a\\'], '.'], ['dirname', ['a\\b'], 'a'], ['dirname', ['..'], '.'], ['dirname', ['..\\'], '.'], ['dirname', ['\\\\s\\sh'], '\\\\s\\sh'], ['dirname', ['\\\\s\\sh\\'], '\\\\s\\sh\\'], ['dirname', ['\\\\s\\sh\\a\\b'], '\\\\s\\sh\\a'], ['dirname', ['C:\\a/b'], 'C:\\a'], ['dirname', ['/a/b'], '/a'], ['dirname', ['\\\\?\\C:\\a\\b'], '\\\\?\\C:\\a'], ['basename', [''], ''], ['basename', ['\\'], ''], ['basename', ['\\\\'], ''], ['basename', ['a'], 'a'], ['basename', ['a\\'], 'a'], ['basename', ['a\\\\'], 'a'], ['basename', ['C:\\a\\b\\c'], 'c'], ['basename', ['C:\\a\\b\\c\\'], 'c'], ['basename', ['a.txt'], 'a.txt'], ['basename', ['.txt'], '.txt'], ['basename', ['..'], '..'], ['basename', ['...'], '...'], ['basename', ['.'], '.'], ['basename', ['a\\.txt'], '.txt'], ['basename', ['a\\..txt'], '..txt'], ['basename', ['C:\\'], ''], ['basename', ['C:'], ''], ['basename', ['C:a'], 'a'], ['basename', ['\\\\s\\sh\\a.txt'], 'a.txt'], ['basename', ['a\\b.tar.gz', '.gz'], 'b.tar'], ['basename', ['a.txt', '.txt'], 'a'], ['basename', ['a.txt', 'a.txt'], ''], ['basename', ['a', 'a'], ''], ['basename', ['a.txt', ''], 'a.txt'], ['basename', ['ab', 'b'], 'a'], ['basename', ['C:\\a', 'C:\\a'], ''], ['extname', [''], ''], ['extname', ['a'], ''], ['extname', ['a.'], '.'], ['extname', ['.a'], ''], ['extname', ['..a'], '.a'], ['extname', ['a..'], '.'], ['extname', ['...'], '.'], ['extname', ['..'], ''], ['extname', ['.'], ''], ['extname', ['a.txt'], '.txt'], ['extname', ['a.tar.gz'], '.gz'], ['extname', ['.txt'], ''], ['extname', ['C:\\a\\.txt'], ''], ['extname', ['C:a.txt'], '.txt'], ['extname', ['\\\\s\\sh\\a.b\\c'], ''], ['extname', ['a\\b.'], '.'], ['extname', ['a b.c d'], '.c d'], ['extname', ['C:\\'], ''], ['extname', ['C:'], ''], ['isAbsolute', [''], false], ['isAbsolute', ['\\'], true], ['isAbsolute', ['/'], true], ['isAbsolute', ['\\\\'], true], ['isAbsolute', ['C:\\'], true], ['isAbsolute', ['C:'], false], ['isAbsolute', ['C:a'], false], ['isAbsolute', ['C:\\a'], true], ['isAbsolute', ['C:/a'], true], ['isAbsolute', ['a'], false], ['isAbsolute', ['\\a'], true], ['isAbsolute', ['\\\\s\\sh'], true], ['isAbsolute', ['a\\b'], false], ['isAbsolute', ['C'], false], ['isAbsolute', ['C\\a'], false], ['isAbsolute', ['\\\\?\\C:\\a'], true], ]; for (let i = 0; i < cases.length; i++) { const [fn, args, expected] = cases[i]; const actual = path[fn](...args); if (actual !== expected) { throw new Error(`${fn}(${args.map((a) => JSON.stringify(a)).join(', ')}) = ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`); } } if (path.sep !== '\\' || path.delimiter !== ';') throw new Error('separators'); const formats = [ [{ dir: 'C:\\a\\b', base: 'c.txt' }, 'C:\\a\\b\\c.txt'], [{ root: 'C:\\', base: 'c' }, 'C:\\c'], [{ dir: 'C:\\a', name: 'b', ext: 'txt' }, 'C:\\a\\b.txt'], [{ root: '\\\\s\\sh\\', base: 'a' }, '\\\\s\\sh\\a'], [{ dir: '', base: '' }, ''], [{ dir: 'C:\\a', root: 'C:\\b', base: 'c' }, 'C:\\a\\c'], ]; for (const [object, expected] of formats) { if (path.format(object) !== expected) throw new Error(`format ${JSON.stringify(object)}`); } const parsed = [ ['', ['', '', '', '', '']], ['\\', ['\\', '\\', '', '', '']], ['C:\\', ['C:\\', 'C:\\', '', '', '']], ['C:', ['C:', 'C:', '', '', '']], ['C:a', ['C:', 'C:', 'a', '', 'a']], ['C:\\a', ['C:\\', 'C:\\', 'a', '', 'a']], ['C:\\a\\', ['C:\\', 'C:\\', 'a', '', 'a']], ['C:\\a\\b', ['C:\\', 'C:\\a', 'b', '', 'b']], ['C:\\a\\b\\', ['C:\\', 'C:\\a', 'b', '', 'b']], ['C:\\a\\b\\c.txt', ['C:\\', 'C:\\a\\b', 'c.txt', '.txt', 'c']], ['a', ['', '', 'a', '', 'a']], ['a\\', ['', '', 'a', '', 'a']], ['a.txt', ['', '', 'a.txt', '.txt', 'a']], ['.txt', ['', '', '.txt', '', '.txt']], ['..', ['', '', '..', '', '..']], ['...', ['', '', '...', '.', '..']], ['a.tar.gz', ['', '', 'a.tar.gz', '.gz', 'a.tar']], ['C:a.txt', ['C:', 'C:', 'a.txt', '.txt', 'a']], ['\\\\s\\sh', ['\\\\s\\sh', '\\\\s\\sh', '', '', '']], ['\\\\s\\sh\\', ['\\\\s\\sh\\', '\\\\s\\sh\\', '', '', '']], ['\\\\s\\sh\\a', ['\\\\s\\sh\\', '\\\\s\\sh\\', 'a', '', 'a']], ['\\\\s\\sh\\a\\b.txt', ['\\\\s\\sh\\', '\\\\s\\sh\\a', 'b.txt', '.txt', 'b']], ['C:\\a/b', ['C:\\', 'C:\\a', 'b', '', 'b']], ]; for (const [input, [root, dir, base, ext, name]] of parsed) { const actual = path.parse(input); if (actual.root !== root || actual.dir !== dir || actual.base !== base || actual.ext !== ext || actual.name !== name) { throw new Error(`parse(${JSON.stringify(input)}) = ${JSON.stringify(actual)}`); } } if (path.toNamespacedPath('C:\\a\\b') !== '\\\\?\\C:\\a\\b') throw new Error('device'); if (path.toNamespacedPath('\\\\s\\sh\\a') !== '\\\\?\\UNC\\s\\sh\\a') throw new Error('unc'); if (path.toNamespacedPath('\\\\?\\C:\\a') !== '\\\\?\\C:\\a') throw new Error('long'); if (path.toNamespacedPath('\\a\\b') !== '\\a\\b') throw new Error('rooted'); if (path.toNamespacedPath('') !== '') throw new Error('empty'); // Drive-relative resolution with a live working directory is covered // separately in win32_drive_relative_uses_cwd_drive_or_drive_root. console.log('win32 vectors'); "#, &[], ); assert_eq!(output.stdout, b"win32 vectors\n"); } #[test] fn win32_drive_relative_uses_cwd_drive_or_drive_root() { // Drive-relative inputs consult the working directory's drive without // ever reading per-drive environment entries: a cwd on the same drive is // reused, any other drive falls back to its root. Runs with ambient // authority so the cwd is readable; expectations adapt to the host. let output = common::run( r#" import * as win32 from 'avril:path/win32'; import { process, runtime } from 'avril:std'; const cwd = process.cwd(); const same = /^[A-Za-z]:[\\/]/.exec(cwd); // A drive the cwd cannot be on: the fallback must be that drive's root. const other = same && same[0][0].toLowerCase() === 'q' ? 'Z:' : 'Q:'; if (win32.resolve(`${other}foo`, 'bar') !== `${other}\\foo\\bar`) { throw new Error('drive root fallback'); } if (win32.resolve(other, 'b') !== `${other}\\b`) throw new Error('bare fallback'); if (same) { const drive = same[0].slice(0, 2); if (win32.resolve(`${drive}foo`) !== win32.join(cwd, 'foo')) { throw new Error('same drive reuses cwd'); } } else if (runtime.os !== 'windows') { // POSIX hosts never have a drive-lettered cwd, so C: always falls back. if (win32.resolve('C:foo', 'bar') !== 'C:\\foo\\bar') throw new Error('C root'); if (win32.resolve('C:', 'b') !== 'C:\\b') throw new Error('C bare root'); } console.log('drive relative'); "#, &[], ); assert_eq!(output.stdout, b"drive relative\n"); } #[test] fn native_default_aliases_selected_flavor_and_namespaces_stay_frozen() { let output = common::run( r#" import * as path from 'avril:path'; import * as explicitPosix from 'avril:path/posix'; import * as explicitWin32 from 'avril:path/win32'; import { runtime } from 'avril:std'; const isWindows = runtime.os === 'windows'; const selected = isWindows ? path.win32 : path.posix; if (path.sep !== (isWindows ? '\\' : '/')) throw new Error('native sep'); if (path.delimiter !== (isWindows ? ';' : ':')) throw new Error('native delimiter'); if (path.join !== selected.join || path.resolve !== selected.resolve || path.fromFileUrl !== selected.fromFileUrl || path.toFileUrl !== selected.toFileUrl || path.sep !== selected.sep || path.parse !== selected.parse) { throw new Error('native alias'); } if (path.posix.sep !== '/' || path.win32.sep !== '\\') throw new Error('nested sep'); if (path.win32.normalize('C:\\a\\.\\b') !== 'C:\\a\\b') throw new Error('nested win32'); if (explicitWin32.isAbsolute('C:\\a') !== true) throw new Error('subpath'); if (explicitPosix.join('/a', 'b') !== '/a/b') throw new Error('explicit posix'); // All entry modules share one frozen namespace object per flavor. if (path.posix !== explicitPosix.posix || path.win32 !== explicitWin32.win32 || explicitPosix.win32 !== explicitWin32.win32) { throw new Error('namespace identity'); } for (const ns of [path.posix, path.win32]) { if (!Object.isFrozen(ns)) throw new Error('unfrozen namespace'); } if (path.posix.posix !== undefined || path.posix.win32 !== undefined || path.win32.posix !== undefined || path.win32.win32 !== undefined) { throw new Error('recursive namespace fields'); } const expected = ['basename', 'delimiter', 'dirname', 'extname', 'format', 'fromFileUrl', 'isAbsolute', 'join', 'normalize', 'parse', 'posix', 'relative', 'resolve', 'sep', 'toFileUrl', 'toNamespacedPath', 'win32']; for (const module of [path, explicitPosix, explicitWin32]) { const keys = Object.keys(module).sort(); if (JSON.stringify(keys) !== JSON.stringify(expected)) { throw new Error(`exports: ${JSON.stringify(keys)}`); } } // Detached calls never consult a receiver; expectations use the native separator. const { join, normalize } = path; if (join('a', 'b') !== `a${path.sep}b`) throw new Error('detached join'); if (normalize('a/./b') !== `a${path.sep}b`) throw new Error('detached normalize'); const { parse, format } = explicitPosix; if (parse('/a/b').base !== 'b' || format({ dir: '/a', base: 'b' }) !== '/a/b') { throw new Error('detached parse/format'); } const winJoin = path.win32.join; if (winJoin('C:\\a', 'b') !== 'C:\\a\\b') throw new Error('detached win32'); console.log('native default'); "#, &[], ); assert_eq!(output.stdout, b"native default\n"); } #[test] fn unicode_strings_survive_lexical_operations_losslessly() { let output = common::run( r#" import * as path from 'avril:path/posix'; import * as win32 from 'avril:path/win32'; if (path.join('aé', 'b') !== 'aé/b') throw new Error('latin'); if (path.normalize('/日本語/./x/') !== '/日本語/x/') throw new Error('cjk'); if (path.basename('/😀.txt') !== '😀.txt' || path.extname('/😀.txt') !== '.txt') { throw new Error('emoji'); } if (path.parse('/😀.txt').name !== '😀') throw new Error('parse emoji'); // Lone surrogates are preserved as-is by purely lexical operations. if (path.join('\ud800', 'x') !== '\ud800/x') throw new Error('join surrogate'); if (path.normalize('\ud800') !== '\ud800') throw new Error('normalize surrogate'); if (path.basename('\ud800.txt') !== '\ud800.txt') throw new Error('basename surrogate'); if (win32.normalize('C:\\\ud800\\x') !== 'C:\\\ud800\\x') throw new Error('win32 surrogate'); if (path.extname('\udfff') !== '') throw new Error('trail surrogate'); console.log('unicode lossless'); "#, &[], ); assert_eq!(output.stdout, b"unicode lossless\n"); } #[test] fn invalid_arguments_throw_type_errors_without_coercion() { let output = common::run( r#" import * as path from 'avril:path'; import * as win32 from 'avril:path/win32'; const marker = {}; const throwing = { toString() { throw marker; } }; const cases = [ () => path.join(42), () => path.join(throwing), () => path.resolve({}), () => path.normalize(null), () => path.normalize(undefined), () => path.relative('/a'), () => path.relative('/a', 5), () => path.dirname([]), () => path.basename('a', null), () => path.basename('a', Symbol('s')), () => path.extname(Symbol('e')), () => path.isAbsolute(true), () => path.parse(123), () => path.format('x'), () => path.format(null), () => path.format([]), () => path.format(42), () => path.format(() => {}), () => path.fromFileUrl(42), () => path.fromFileUrl({}), () => path.fromFileUrl({ href: 'file:///a' }), () => path.toFileUrl(7), () => win32.toFileUrl(null), ]; for (const fn of cases) { try { fn(); throw new Error('accepted invalid input'); } catch (e) { if (e === marker) throw new Error('coerced throwing input'); if (!(e instanceof TypeError)) throw e; } } // Property reads on the format object keep guest exceptions intact. const failing = {}; Object.defineProperty(failing, 'dir', { get() { throw marker; } }); try { path.format(failing); throw new Error('swallowed getter'); } catch (e) { if (e !== marker) throw e; } // Missing arguments fail the same way as explicit undefined. for (const fn of [() => path.normalize(), () => path.parse(), () => path.dirname()]) { try { fn(); throw new Error('accepted missing input'); } catch (e) { if (!(e instanceof TypeError)) throw e; } } // Non-strings pass straight through toNamespacedPath without validation. const passthrough = {}; if (win32.toNamespacedPath(passthrough) !== passthrough) throw new Error('brand'); if (path.toNamespacedPath(42) !== 42) throw new Error('number'); console.log('invalid args'); "#, &[], ); assert_eq!(output.stdout, b"invalid args\n"); } #[test] fn cwd_is_used_only_when_needed_and_rechecked_after_pledge() { let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("main.js"), r#" import { normalize, relative, resolve, sep, toNamespacedPath } from 'avril:path'; import * as win32 from 'avril:path/win32'; import { runtime } from 'avril:std'; const isWindows = runtime.os === 'windows'; const root = isWindows ? 'C:\\abs' : '/abs'; const other = isWindows ? 'C:\\y' : '/y'; if (normalize('a/./b') !== 'a/b') throw new Error('pure'); if (resolve(root, 'x') !== `${root}${sep}x`) throw new Error('absolute resolve'); if (relative(isWindows ? 'C:\\x' : '/x', other) !== `..${sep}y`) { throw new Error('absolute relative'); } if (relative(root, root) !== '') throw new Error('early return'); if (win32.toNamespacedPath('C:\\a') !== '\\\\?\\C:\\a') throw new Error('namespaced'); // POSIX toNamespacedPath is a pure passthrough, so only the win32 // flavor (which resolves first) needs the directory here. if (isWindows) { try { toNamespacedPath('rel'); throw new Error('read cwd without a grant'); } catch (e) { if (e.name !== 'PermissionDenied') throw e; } } else if (toNamespacedPath('rel') !== 'rel') { throw new Error('passthrough'); } for (const fn of [() => resolve('rel'), () => relative('a', other), () => resolve(), () => win32.toNamespacedPath('rel')]) { try { fn(); throw new Error('read cwd without a grant'); } catch (e) { if (e.name !== 'PermissionDenied') throw e; } } console.log('no cwd without grants'); "#, ) .unwrap(); let output = run_in(dir.path(), &["-0", "main.js"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"no cwd without grants\n"); // With a read grant for the directory, relative resolution matches process.cwd(). fs::write( dir.path().join("main.js"), r#" import { join, relative, resolve, sep } from 'avril:path'; import { process } from 'avril:std'; const cwd = process.cwd(); if (resolve('a', 'b') !== join(cwd, 'a', 'b')) throw new Error('resolve cwd'); if (resolve() !== cwd) throw new Error('bare resolve'); if (relative('sub1', 'sub2') !== `..${sep}sub2`) throw new Error('relative cwd'); console.log('cwd matches'); "#, ) .unwrap(); let output = run_in(dir.path(), &["-0", "--allow-read=.", "main.js"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"cwd matches\n"); // Pledging away read authority is honored on the next call: no cached cwd. fs::write(dir.path().join("main.js"), r#" import { pledge, process, runtime } from 'avril:std'; import { join, normalize, resolve } from 'avril:path'; import * as win32 from 'avril:path/win32'; const cwd = process.cwd(); const abs = runtime.os === 'windows' ? 'C:\\still\\absolute' : '/still/absolute'; if (resolve('a') !== join(cwd, 'a')) throw new Error('before pledge'); pledge({ read: false }); // Native toNamespacedPath on POSIX hosts is a pure passthrough, so only // resolve and the resolving win32 flavor recheck the directory here. for (const fn of [() => resolve('b'), () => win32.toNamespacedPath('rel')]) { try { fn(); throw new Error('used cached cwd after pledge'); } catch (e) { if (e.name !== 'PermissionDenied') throw e; } } if (normalize('x/../y') !== 'y') throw new Error('pure after pledge'); if (resolve(abs) !== abs) throw new Error('absolute after pledge'); if (win32.toNamespacedPath('C:\\x') !== '\\\\?\\C:\\x') throw new Error('absolute namespaced'); console.log('pledge rechecked'); "#).unwrap(); let output = run_in(dir.path(), &["-0", "--allow-read=.", "main.js"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"pledge rechecked\n"); } #[test] fn file_urls_round_trip_and_reject_without_any_grants() { let dir = tempfile::tempdir().unwrap(); let output = run_main( dir.path(), r#" import * as path from 'avril:path'; import * as win32 from 'avril:path/win32'; const fromCharCode = String.fromCharCode; const roundTrip = (ns, input) => { const back = ns.fromFileUrl(ns.toFileUrl(input)); if (back !== input) throw new Error(`round trip ${JSON.stringify(input)} -> ${back}`); }; for (const input of ['/a b/c', '/a%b', '/a#b?c', '/日本語/😀', '/a\\b', '/a/b/', "/a'b", '/a+b', '/a\tb', '/a|b^c', '/a~b', '/a[b]c', '/a;b,c=d:e@f$g&h!i*j(k)l']) { roundTrip(path, input); } // Every C0 control except NUL, plus DEL, survives the round trip encoded. for (let code = 1; code < 32; code++) roundTrip(path, `/a${fromCharCode(code)}b`); roundTrip(path, `/a${fromCharCode(127)}b`); // UTF-8 width boundaries round-trip; lone surrogates never reach the URL. for (const point of [0x7f, 0x80, 0x7ff, 0x800, 0xffff, 0x10000, 0x10ffff]) { roundTrip(path, `/a${String.fromCodePoint(point)}b`); } for (const input of ['C:\\a b\\c', '\\\\server\\share\\a b', 'C:\\a\\', 'C:\\日本語\\x', 'C:\\a#b', 'C:\\a+b', 'C:\\a~b', '\\\\[::1]\\share']) { roundTrip(win32, input); } // Encoding matches Node's pathToFileURL across the ASCII range: only // alphanumerics, sub-delims, `@:`, and `;,=`, `-`, `.`, `_` stay literal. const kept = "!$&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; for (let code = 1; code < 128; code++) { if (code === 47) continue; const href = path.toFileUrl(`/a${fromCharCode(code)}b`).href; const literal = `file:///a${fromCharCode(code)}b`; if (kept.includes(fromCharCode(code)) !== (href === literal)) { throw new Error(`ascii ${code}: ${href}`); } } if (path.fromFileUrl('file://localhost/a') !== '/a') throw new Error('localhost'); if (win32.fromFileUrl('file://localhost/C:/a') !== 'C:\\a') throw new Error('win localhost'); if (win32.fromFileUrl('file://server/share/a') !== '\\\\server\\share\\a') { throw new Error('unc host'); } if (win32.fromFileUrl('file://server/share/') !== '\\\\server\\share\\') { throw new Error('unc trailing'); } if (win32.fromFileUrl('file://[::1]/share') !== '\\\\[::1]\\share') { throw new Error('ipv6'); } if (win32.fromFileUrl('file:///c:/a') !== 'c:\\a') throw new Error('lower drive'); // Non-ASCII servers normalize through IDNA, deterministically. if (win32.toFileUrl('\\\\münchen\\share').href !== 'file://xn--mnchen-3ya/share') { throw new Error('idna href'); } if (win32.fromFileUrl('file://münchen/share') !== '\\\\xn--mnchen-3ya\\share') { throw new Error('idna path'); } // DNS names fold ASCII case, so mixed-case servers still round-trip. if (win32.toFileUrl('\\\\SERVER\\share').href !== 'file://server/share') { throw new Error('server case'); } if (path.toFileUrl('/a/b/').href !== 'file:///a/b/') throw new Error('trailing slash'); if (win32.toFileUrl('C:\\a\\').href !== 'file:///C:/a/') throw new Error('win trailing'); // `.`/`..` clamp at the drive and share roots instead of crossing them. const clamped = [ ['C:\\..\\a', 'file:///C:/a', 'C:\\a'], ['C:\\', 'file:///C:/', 'C:\\'], ['C:\\a', 'file:///C:/a', 'C:\\a'], ['\\\\server\\share\\\\a', 'file://server/share/a', '\\\\server\\share\\a'], ['\\\\server\\share', 'file://server/share', '\\\\server\\share'], ['\\\\server\\share\\', 'file://server/share/', '\\\\server\\share\\'], ['\\\\server\\share\\..\\other', 'file://server/share/other', '\\\\server\\share\\other'], ['\\\\server\\share\\..', 'file://server/share', '\\\\server\\share'], ['\\\\server\\share\\a\\.\\b\\..\\c', 'file://server/share/a/c', '\\\\server\\share\\a\\c'], ['\\\\server\\share\\a', 'file://server/share/a', '\\\\server\\share\\a'], ]; for (const [input, href, back] of clamped) { const url = win32.toFileUrl(input); if (url.href !== href) throw new Error(`href ${JSON.stringify(input)} -> ${url.href}`); if (win32.fromFileUrl(url) !== back) throw new Error(`back ${JSON.stringify(input)}`); } // A URL instance and its href string convert identically. const url = path.toFileUrl('/a b'); if (!(url instanceof URL)) throw new Error('returns URL'); if (path.fromFileUrl(url) !== path.fromFileUrl(url.href)) throw new Error('instance'); const rejections = [ () => path.fromFileUrl('https://example.com/a'), () => path.fromFileUrl('/a/b'), () => path.fromFileUrl('file:///a?b=c'), () => path.fromFileUrl('file:///a#x'), () => path.fromFileUrl('file:///a%2Fb'), () => path.fromFileUrl('file:///a%2fb'), () => win32.fromFileUrl('file:///C:/a%5Cb'), () => win32.fromFileUrl('file:///C:/a%5cb'), () => path.fromFileUrl('file:///a%2'), () => path.fromFileUrl('file:///a%2G'), () => path.fromFileUrl('file:///a%'), () => path.fromFileUrl('file:///a%00b'), () => path.fromFileUrl('file:///a%ED%A0%80b'), () => path.fromFileUrl('file:///a%ED%BF%BFb'), () => path.fromFileUrl('file:///a%C0%AFb'), () => path.fromFileUrl('file:///a%E0%80%AFb'), () => path.fromFileUrl('file:///a%F0%80%80%AFb'), () => path.fromFileUrl('file:///a%E2%82b'), () => path.fromFileUrl('file:///a%F0%9F%98b'), () => path.fromFileUrl('file:///a%80b'), () => path.fromFileUrl('file:///a%BFb'), () => path.fromFileUrl('file:///a%F4%90%80%80b'), () => path.fromFileUrl('file:///a%FEb'), () => path.fromFileUrl('file:///a%FFb'), () => path.fromFileUrl('file://other/a'), () => path.fromFileUrl(42), () => path.fromFileUrl({ href: 'file:///a' }), () => win32.fromFileUrl('file:///no-drive/a'), () => win32.fromFileUrl('file:///'), () => win32.fromFileUrl('file:///C:'), () => win32.fromFileUrl('file:///C:foo'), () => win32.fromFileUrl('file://server/'), () => win32.fromFileUrl('file://server'), () => win32.fromFileUrl('file://server//'), () => path.fromFileUrl(`file:///${fromCharCode(0xd800)}`), () => path.fromFileUrl(`file:///a${fromCharCode(0)}b`), () => path.fromFileUrl('file:///a\tb'), () => path.fromFileUrl('file:///a\nb'), () => path.toFileUrl('a/b'), () => path.toFileUrl(''), () => win32.toFileUrl('a\\b'), () => win32.toFileUrl('C:foo'), () => win32.toFileUrl('C:'), () => win32.toFileUrl('\\foo'), () => win32.toFileUrl('/foo'), () => win32.toFileUrl('\\\\?\\C:\\a'), () => win32.toFileUrl('\\\\.\\C:\\a'), () => win32.toFileUrl('\\\\localhost\\share'), () => win32.toFileUrl('\\\\LOCALHOST\\share'), () => win32.toFileUrl('\\\\localhost\\\\share\\\\a'), () => win32.toFileUrl('\\\\localhost\\\\share\\\\a'), () => win32.toFileUrl('\\\\server\\\\.\\\\x'), () => win32.toFileUrl('\\\\server\\\\..\\\\x'), () => win32.toFileUrl('\\\\server'), () => win32.toFileUrl('\\\\server\\'), () => win32.toFileUrl('\\\\a\tb\\share'), () => win32.toFileUrl('\\\\a\nb\\share'), () => win32.toFileUrl('\\\\a b\\share'), () => win32.toFileUrl('\\\\a#b\\share'), () => win32.toFileUrl('\\\\a?b\\share'), () => win32.toFileUrl('\\\\a@b\\share'), () => win32.toFileUrl('\\\\a%b\\share'), () => win32.toFileUrl('\\\\[::1\\share'), () => path.toFileUrl('/a\0b'), () => path.toFileUrl('/a\ud800b'), () => win32.toFileUrl(3), ]; for (const fn of rejections) { try { fn(); throw new Error('accepted bad file URL input'); } catch (e) { if (!(e instanceof TypeError)) throw e; } } if (path.fromFileUrl('file:///a?') !== '/a') throw new Error('empty query'); if (path.fromFileUrl('file:///a#') !== '/a') throw new Error('empty fragment'); console.log('file urls'); "#, &["-0", "main.js"], ); let output = success(output); assert_eq!(output, "file urls\n"); } #[test] fn hardened_calls_survive_primordial_and_url_pollution() { let dir = tempfile::tempdir().unwrap(); let output = run_main( dir.path(), r#" import * as path from 'avril:path'; import * as win32 from 'avril:path/win32'; import { posix } from 'avril:path/posix'; const TypeErr = TypeError; const before = new URL('file:///a%20b/c'); const unc = new URL('file://server/share/a'); const poison = () => { throw new Error('Poisoned'); }; for (const key of ['slice', 'split', 'charCodeAt', 'indexOf', 'toLowerCase', 'includes', 'replace', 'repeat', 'lastIndexOf']) { String.prototype[key] = poison; } String.prototype[Symbol.split] = poison; String.fromCharCode = poison; for (const key of ['join', 'slice', 'push', 'pop']) Array.prototype[key] = poison; RegExp.prototype.exec = poison; globalThis.encodeURI = poison; globalThis.encodeURIComponent = poison; globalThis.decodeURIComponent = poison; const URLProto = URL.prototype; Object.defineProperty(URLProto, 'hostname', { value: 'poisoned' }); Object.defineProperty(URLProto, 'pathname', { value: 'poisoned' }); globalThis.URL = poison; if (path.join('/a', 'b', '../c') !== '/a/c') throw new Error('join'); if (path.normalize('a/./b//') !== 'a/b/') throw new Error('normalize'); if (path.resolve('/a', 'b') !== '/a/b') throw new Error('absolute resolve'); if (path.relative('/a/b', '/a/c') !== '../c') throw new Error('absolute relative'); if (path.parse('/a/b.txt').ext !== '.txt') throw new Error('parse'); if (path.format({ dir: '/a', base: 'b' }) !== '/a/b') throw new Error('format'); if (win32.join('C:\\a', '..\\b') !== 'C:\\b') throw new Error('win32 join'); if (win32.relative('C:\\a\\b\\', 'C:\\a\\c') !== '..\\c') throw new Error('win32 relative'); if (posix.dirname('/a/b') !== '/a') throw new Error('namespace'); if (path.toFileUrl('/a b').href !== 'file:///a%20b') throw new Error('toFileUrl'); if (path.fromFileUrl(before.href) !== '/a b/c') throw new Error('fromFileUrl string'); if (win32.fromFileUrl(unc) !== '\\\\server\\share\\a') throw new Error('unc instance'); try { path.fromFileUrl('file:///a%2Fb'); throw new Error('lost encoded separator check'); } catch (e) { if (!(e instanceof TypeErr)) throw e; } try { win32.fromFileUrl({ href: 'file:///C:/a' }); throw new Error('lost brand check'); } catch (e) { if (!(e instanceof TypeErr)) throw e; } try { path.resolve('needs-cwd'); throw new Error('lost permission error'); } catch (e) { if (e.name !== 'PermissionDenied') throw e; } try { path.join(1); throw new Error('lost validation'); } catch (e) { if (!(e instanceof TypeErr)) throw e; } console.log('hardened'); "#, &["-0", "main.js"], ); let output = success(output); assert_eq!(output, "hardened\n"); } #[test] fn workers_import_path_and_its_subpaths() { let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("worker.ts"), r#" import * as path from 'avril:path'; import { fromFileUrl as posixFromFileUrl, join, toFileUrl } from 'avril:path/posix'; import * as win32 from 'avril:path/win32'; import { runtime } from 'avril:std'; const sep = runtime.os === 'windows' ? '\\' : '/'; const result = [ path.sep, join('/w', 'x'), win32.normalize('C:\\a\\.\\b'), toFileUrl('/w x').href, posixFromFileUrl('file:///w%20x'), ]; postMessage(result); close(); "#, ) .unwrap(); let output = run_main( dir.path(), r#" const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }); const answer = await new Promise((resolve, reject) => { worker.onmessage = (e) => resolve(e.data); worker.onerror = reject; }); const { runtime } = await import('avril:std'); const sep = runtime.os === 'windows' ? '\\' : '/'; const expected = [sep, '/w/x', 'C:\\a\\b', 'file:///w%20x', '/w x']; if (JSON.stringify(answer) !== JSON.stringify(expected)) { throw new Error(`worker paths: ${JSON.stringify(answer)}`); } console.log('worker paths'); "#, &["main.js"], ); let output = success(output); assert_eq!(output, "worker paths\n"); } #[test] fn typescript_imports_path_with_types_and_rejects_unknown_exports() { let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("main.ts"), r#" import { join as nativeJoin, parse as nativeParse, posix, resolve as nativeResolve, sep, win32, type ParsedPath } from 'avril:path'; import * as posixns from 'avril:path/posix'; import * as win32ns from 'avril:path/win32'; import { runtime } from 'avril:std'; const parts: ParsedPath = posixns.parse('/a/b.txt'); const combined: string = posixns.join('a', posixns.basename('/x/y')) + posixns.sep + posixns.delimiter; const parent: string = posixns.dirname('/a/b'); const back: string = posixns.fromFileUrl(posixns.toFileUrl('/a b')); const flavored: string = win32ns.normalize('C:\\a') + posix.sep + win32.sep; const native = runtime.os === 'windows' ? win32 : posix; const aliased: boolean = nativeJoin === native.join && nativeResolve === native.resolve && nativeParse === native.parse && sep === native.sep; console.log([parts.ext, combined, parent, back, flavored, aliased].join('|')); "#, ) .unwrap(); let output = run_in(dir.path(), &["-0", "main.ts"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b".txt|a/y/:|/a|/a b|C:\\a/\\|true\n"); fs::write( dir.path().join("main.js"), "import { missing } from 'avril:path'; console.log(missing);", ) .unwrap(); let output = run_in(dir.path(), &["-0", "main.js"]); assert!(!output.status.success()); assert!( String::from_utf8_lossy(&output.stderr) .contains("does not provide an export named 'missing'") ); }