From 9cbf275f819c28d903f04156597cb6e826f4ae5e Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 13 Jul 2026 11:10:13 +0800 Subject: [PATCH] test(fspy): run node fs tests with Deno Co-authored-by: GPT-5 Codex --- Cargo.lock | 1 + crates/fspy/Cargo.toml | 1 + crates/fspy/tests/node_fs.rs | 143 +++++++++++++++++++++++------------ packages/tools/package.json | 1 + pnpm-lock.yaml | 69 +++++++++++++++++ pnpm-workspace.yaml | 2 + 6 files changed, 170 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 549e2cb5a..5c09ae71e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1257,6 +1257,7 @@ dependencies = [ "materialized_artifact", "materialized_artifact_build", "nix 0.31.2", + "ntest", "ouroboros", "rustc-hash", "sha2 0.11.0", diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index f99d25241..236a0f7bd 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -43,6 +43,7 @@ tempfile = { workspace = true } anyhow = { workspace = true } csv-async = { workspace = true } ctor = { workspace = true } +ntest = { workspace = true } subprocess_test = { workspace = true, features = ["fspy"] } test-log = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "fs", "io-std"] } diff --git a/crates/fspy/tests/node_fs.rs b/crates/fspy/tests/node_fs.rs index 077495ac2..e902bdf10 100644 --- a/crates/fspy/tests/node_fs.rs +++ b/crates/fspy/tests/node_fs.rs @@ -1,112 +1,161 @@ mod test_utils; use std::{ - env::{current_dir, vars_os}, - ffi::OsStr, + env::{current_dir, join_paths, split_paths, var_os, vars_os}, + ffi::{OsStr, OsString}, + iter, + path::PathBuf, }; use fspy::{AccessMode, PathAccessIterable}; -use test_log::test; +use ntest::test_case; use test_utils::assert_contains; -async fn track_node_script(script: &str, args: &[&OsStr]) -> anyhow::Result { - let mut command = fspy::Command::new("node"); +fn resolve_runtime(runtime: &str) -> anyhow::Result<(PathBuf, OsString)> { + let manifest_dir = PathBuf::from(var_os("CARGO_MANIFEST_DIR").unwrap()); + let tools_bin = + manifest_dir.parent().unwrap().parent().unwrap().join("packages/tools/node_modules/.bin"); + let path = + join_paths(iter::once(tools_bin).chain(var_os("PATH").iter().flat_map(split_paths)))?; + let program = which::which_in(runtime, Some(&path), current_dir()?)?; + Ok((program, path)) +} + +fn track_script( + runtime: &str, + script: &str, + args: &[&OsStr], +) -> anyhow::Result { + let (program, path) = resolve_runtime(runtime)?; + + let mut command = fspy::Command::new(program); command - .arg("-e") - .envs(vars_os()) // https://github.com/jdx/mise/discussions/5968 - .arg(script) - .args(args); - let child = command.spawn(tokio_util::sync::CancellationToken::new()).await?; - let termination = child.wait_handle.await?; - assert!(termination.status.success()); - Ok(termination.path_accesses) + .envs(vars_os().filter(|(name, _)| !name.eq_ignore_ascii_case("PATH"))) + .env("PATH", path); // https://github.com/jdx/mise/discussions/5968 + let script = format!( + "const fs = require('node:fs'); \ + const child_process = require('node:child_process'); \ + {script}" + ); + if runtime == "deno" { + command.args(["eval", "--ext=cjs"]).arg(script).args(args); + } else { + command.arg("-e").arg(script).args(args); + } + + // `ntest::test_case` generates synchronous `#[test]` functions, so drive + // fspy's asynchronous process tracking from this shared helper. + tokio::runtime::Runtime::new()?.block_on(async { + let child = command.spawn(tokio_util::sync::CancellationToken::new()).await?; + let termination = child.wait_handle.await?; + assert!(termination.status.success()); + Ok(termination.path_accesses) + }) } -#[test(tokio::test)] +// Deno does not distribute a musl runtime, so only its generated cases are +// excluded there. Node remains covered by the same tests on musl. +#[test_case("node")] #[ignore = "requires node"] -async fn read_sync() -> anyhow::Result<()> { - let accesses = track_node_script("try { fs.readFileSync('hello') } catch {}", &[]).await?; +#[cfg_attr(not(target_env = "musl"), test_case("deno"))] +#[cfg_attr(not(target_env = "musl"), ignore = "requires node")] +fn read_sync(runtime: &str) -> anyhow::Result<()> { + let accesses = track_script(runtime, "try { fs.readFileSync('hello') } catch {}", &[])?; assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ); Ok(()) } -#[test(tokio::test)] +#[test_case("node")] #[ignore = "requires node"] -async fn exist_sync() -> anyhow::Result<()> { - let accesses = track_node_script("try { fs.existsSync('hello') } catch {}", &[]).await?; +#[cfg_attr(not(target_env = "musl"), test_case("deno"))] +#[cfg_attr(not(target_env = "musl"), ignore = "requires node")] +fn exist_sync(runtime: &str) -> anyhow::Result<()> { + let accesses = track_script(runtime, "try { fs.existsSync('hello') } catch {}", &[])?; assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ); Ok(()) } -#[test(tokio::test)] +#[test_case("node")] #[ignore = "requires node"] -async fn stat_sync() -> anyhow::Result<()> { - let accesses = track_node_script("try { fs.statSync('hello') } catch {}", &[]).await?; +#[cfg_attr(not(target_env = "musl"), test_case("deno"))] +#[cfg_attr(not(target_env = "musl"), ignore = "requires node")] +fn stat_sync(runtime: &str) -> anyhow::Result<()> { + let accesses = track_script(runtime, "try { fs.statSync('hello') } catch {}", &[])?; assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ); Ok(()) } -#[test(tokio::test)] +#[test_case("node")] #[ignore = "requires node"] -async fn create_read_stream() -> anyhow::Result<()> { - let accesses = track_node_script( +#[cfg_attr(not(target_env = "musl"), test_case("deno"))] +#[cfg_attr(not(target_env = "musl"), ignore = "requires node")] +fn create_read_stream(runtime: &str) -> anyhow::Result<()> { + let accesses = track_script( + runtime, "try { fs.createReadStream('hello').on('error', () => {}) } catch {}", &[], - ) - .await?; + )?; assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ); Ok(()) } -#[test(tokio::test)] +#[test_case("node")] #[ignore = "requires node"] -async fn create_write_stream() -> anyhow::Result<()> { +#[cfg_attr(not(target_env = "musl"), test_case("deno"))] +#[cfg_attr(not(target_env = "musl"), ignore = "requires node")] +fn create_write_stream(runtime: &str) -> anyhow::Result<()> { let tmpdir = tempfile::tempdir()?; let file_path = tmpdir.path().join("hello"); - let accesses = track_node_script( - "try { fs.createWriteStream(process.argv[1]).on('error', () => {}) } catch {}", + let accesses = track_script( + runtime, + "try { fs.createWriteStream(process.argv.at(-1)).on('error', () => {}) } catch {}", &[file_path.as_os_str()], - ) - .await?; + )?; assert_contains(&accesses, file_path.as_path(), AccessMode::WRITE); Ok(()) } -#[test(tokio::test)] +#[test_case("node")] #[ignore = "requires node"] -async fn write_sync() -> anyhow::Result<()> { +#[cfg_attr(not(target_env = "musl"), test_case("deno"))] +#[cfg_attr(not(target_env = "musl"), ignore = "requires node")] +fn write_sync(runtime: &str) -> anyhow::Result<()> { let tmpdir = tempfile::tempdir()?; let file_path = tmpdir.path().join("hello"); - let accesses = track_node_script( - "try { fs.writeFileSync(process.argv[1], '') } catch {}", + let accesses = track_script( + runtime, + "try { fs.writeFileSync(process.argv.at(-1), '') } catch {}", &[file_path.as_os_str()], - ) - .await?; + )?; assert_contains(&accesses, &file_path, AccessMode::WRITE); Ok(()) } -#[test(tokio::test)] +#[test_case("node")] #[ignore = "requires node"] -async fn read_dir_sync() -> anyhow::Result<()> { - let accesses = track_node_script("try { fs.readdirSync('.') } catch {}", &[]).await?; +#[cfg_attr(not(target_env = "musl"), test_case("deno"))] +#[cfg_attr(not(target_env = "musl"), ignore = "requires node")] +fn read_dir_sync(runtime: &str) -> anyhow::Result<()> { + let accesses = track_script(runtime, "try { fs.readdirSync('.') } catch {}", &[])?; assert_contains(&accesses, ¤t_dir().unwrap(), AccessMode::READ_DIR); Ok(()) } -#[test(tokio::test)] +#[test_case("node")] #[ignore = "requires node"] -async fn subprocess() -> anyhow::Result<()> { +#[cfg_attr(not(target_env = "musl"), test_case("deno"))] +#[cfg_attr(not(target_env = "musl"), ignore = "requires node")] +fn subprocess(runtime: &str) -> anyhow::Result<()> { let cmd = if cfg!(windows) { r"'cmd', ['/c', 'type hello']" } else { r"'/bin/sh', ['-c', 'cat hello']" }; - let accesses = track_node_script( + let accesses = track_script( + runtime, &format!("try {{ child_process.spawnSync({cmd}, {{ stdio: 'ignore' }}) }} catch {{}}"), &[], - ) - .await?; + )?; assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ); Ok(()) } diff --git a/packages/tools/package.json b/packages/tools/package.json index ac4a21141..0302643de 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -7,6 +7,7 @@ "@vitest/browser-playwright": "catalog:", "@voidzero-dev/vite-task-client": "workspace:*", "cross-env": "catalog:", + "deno": "catalog:", "oxfmt": "catalog:", "oxlint": "catalog:", "oxlint-tsgolint": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bb0aec85..a2430d900 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ catalogs: cross-env: specifier: ^10.1.0 version: 10.1.0 + deno: + specifier: 2.9.2 + version: 2.9.2 oxfmt: specifier: 0.57.0 version: 0.57.0 @@ -79,6 +82,9 @@ importers: cross-env: specifier: 'catalog:' version: 10.1.0 + deno: + specifier: 'catalog:' + version: 2.9.2 oxfmt: specifier: 'catalog:' version: 0.57.0(vite-plus@0.2.4(@types/node@25.0.3)(@vitest/browser-playwright@4.1.10)(typescript@6.0.3)(vite@8.1.3(@types/node@25.0.3))) @@ -117,6 +123,38 @@ packages: '@blazediff/core@1.9.1': resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + '@deno/darwin-arm64@2.9.2': + resolution: {integrity: sha512-UJswiF56uU1Slb0BSXpCloTFBpq226HP9ev9omhAPXmKmRMsDbQVGiflXr0M17tNf2T26/zzTOAzyJqs1hmjKQ==} + cpu: [arm64] + os: [darwin] + + '@deno/darwin-x64@2.9.2': + resolution: {integrity: sha512-fro8jjKASMY4yt8Guxp32QyRITm1cKfnEJCxjOZdGt3JVGuZG2TbHKa1YsT06k8ibIXWkOcZJ0rNVvQvNRybNw==} + cpu: [x64] + os: [darwin] + + '@deno/linux-arm64-glibc@2.9.2': + resolution: {integrity: sha512-auZ8yqm+ALQgHAlcQDAB6LNgIeDszUoXZsjrZZ4f4oIl/ZQpWPjeVaVBRqZuRYQcItspapG565wkW7khpG2NHg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@deno/linux-x64-glibc@2.9.2': + resolution: {integrity: sha512-bOmdYLSydi5E4m6V7/9OHVFnvmwbrPAYXunuMoZO3Tik7NOhwyv551HIbBmliADwXlj7/Na9B3elmA3+AZ7X9w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@deno/win32-arm64@2.9.2': + resolution: {integrity: sha512-qshNCBNy17HvXSX1jRS4+ek188x1LR88WPQ/LTuFruS1RCpjAcBaNeE7zHpJyWqzwZYcDZV8UGupEfxTVytTbw==} + cpu: [arm64] + os: [win32] + + '@deno/win32-x64@2.9.2': + resolution: {integrity: sha512-pkyCD46XwP410oDyVPsMpwhIC3/+DDmghNgeWlTal0LsM+RJpArNULL6QSHyL3WDxticwGJ7N4lFEwnAfMgqWg==} + cpu: [x64] + os: [win32] + '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} @@ -807,6 +845,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + deno@2.9.2: + resolution: {integrity: sha512-9+QTuIC400Zg/iVJ0OkwqbafMbhAFXaJjLaGGe9/dSk535yzBSorQPKCnz9Jnu3V1Erb3enRQzHKzRAKPOVrmw==} + hasBin: true + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1212,6 +1254,24 @@ snapshots: '@blazediff/core@1.9.1': {} + '@deno/darwin-arm64@2.9.2': + optional: true + + '@deno/darwin-x64@2.9.2': + optional: true + + '@deno/linux-arm64-glibc@2.9.2': + optional: true + + '@deno/linux-x64-glibc@2.9.2': + optional: true + + '@deno/win32-arm64@2.9.2': + optional: true + + '@deno/win32-x64@2.9.2': + optional: true + '@emnapi/core@1.11.1': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -1681,6 +1741,15 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + deno@2.9.2: + optionalDependencies: + '@deno/darwin-arm64': 2.9.2 + '@deno/darwin-x64': 2.9.2 + '@deno/linux-arm64-glibc': 2.9.2 + '@deno/linux-x64-glibc': 2.9.2 + '@deno/win32-arm64': 2.9.2 + '@deno/win32-x64': 2.9.2 + dequal@2.0.3: {} detect-libc@2.1.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4d59a0a9e..b30b4fd9d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ packages: allowBuilds: '@playwright/browser-chromium': true + deno: true catalog: '@tsconfig/strictest': ^2.0.8 @@ -12,6 +13,7 @@ catalog: '@playwright/browser-chromium': 1.52.0 '@vitest/browser-playwright': 4.1.10 cross-env: ^10.1.0 + deno: 2.9.2 husky: ^9.1.7 lint-staged: ^17.0.0 oxfmt: 0.57.0