Skip to content

Commit 2ba033c

Browse files
authored
test(fspy): run node fs tests with Deno (#541)
## Motivation The shared Node-compatible filesystem tests only covered Node.js, leaving Deno compatibility unverified. Add Deno as a workspace test dependency and run the same ignored `node_fs` cases against both runtimes on supported targets. Deno does not distribute a musl runtime, so only its generated cases are excluded there while Node coverage remains enabled.
1 parent c844cf0 commit 2ba033c

6 files changed

Lines changed: 170 additions & 47 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fspy/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ tempfile = { workspace = true }
4343
anyhow = { workspace = true }
4444
csv-async = { workspace = true }
4545
ctor = { workspace = true }
46+
ntest = { workspace = true }
4647
subprocess_test = { workspace = true, features = ["fspy"] }
4748
test-log = { workspace = true }
4849
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "fs", "io-std"] }

crates/fspy/tests/node_fs.rs

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,161 @@
11
mod test_utils;
22

33
use std::{
4-
env::{current_dir, vars_os},
5-
ffi::OsStr,
4+
env::{current_dir, join_paths, split_paths, var_os, vars_os},
5+
ffi::{OsStr, OsString},
6+
iter,
7+
path::PathBuf,
68
};
79

810
use fspy::{AccessMode, PathAccessIterable};
9-
use test_log::test;
11+
use ntest::test_case;
1012
use test_utils::assert_contains;
1113

12-
async fn track_node_script(script: &str, args: &[&OsStr]) -> anyhow::Result<PathAccessIterable> {
13-
let mut command = fspy::Command::new("node");
14+
fn resolve_runtime(runtime: &str) -> anyhow::Result<(PathBuf, OsString)> {
15+
let manifest_dir = PathBuf::from(var_os("CARGO_MANIFEST_DIR").unwrap());
16+
let tools_bin =
17+
manifest_dir.parent().unwrap().parent().unwrap().join("packages/tools/node_modules/.bin");
18+
let path =
19+
join_paths(iter::once(tools_bin).chain(var_os("PATH").iter().flat_map(split_paths)))?;
20+
let program = which::which_in(runtime, Some(&path), current_dir()?)?;
21+
Ok((program, path))
22+
}
23+
24+
fn track_script(
25+
runtime: &str,
26+
script: &str,
27+
args: &[&OsStr],
28+
) -> anyhow::Result<PathAccessIterable> {
29+
let (program, path) = resolve_runtime(runtime)?;
30+
31+
let mut command = fspy::Command::new(program);
1432
command
15-
.arg("-e")
16-
.envs(vars_os()) // https://github.com/jdx/mise/discussions/5968
17-
.arg(script)
18-
.args(args);
19-
let child = command.spawn(tokio_util::sync::CancellationToken::new()).await?;
20-
let termination = child.wait_handle.await?;
21-
assert!(termination.status.success());
22-
Ok(termination.path_accesses)
33+
.envs(vars_os().filter(|(name, _)| !name.eq_ignore_ascii_case("PATH")))
34+
.env("PATH", path); // https://github.com/jdx/mise/discussions/5968
35+
let script = format!(
36+
"const fs = require('node:fs'); \
37+
const child_process = require('node:child_process'); \
38+
{script}"
39+
);
40+
if runtime == "deno" {
41+
command.args(["eval", "--ext=cjs"]).arg(script).args(args);
42+
} else {
43+
command.arg("-e").arg(script).args(args);
44+
}
45+
46+
// `ntest::test_case` generates synchronous `#[test]` functions, so drive
47+
// fspy's asynchronous process tracking from this shared helper.
48+
tokio::runtime::Runtime::new()?.block_on(async {
49+
let child = command.spawn(tokio_util::sync::CancellationToken::new()).await?;
50+
let termination = child.wait_handle.await?;
51+
assert!(termination.status.success());
52+
Ok(termination.path_accesses)
53+
})
2354
}
2455

25-
#[test(tokio::test)]
56+
// Deno does not distribute a musl runtime, so only its generated cases are
57+
// excluded there. Node remains covered by the same tests on musl.
58+
#[test_case("node")]
2659
#[ignore = "requires node"]
27-
async fn read_sync() -> anyhow::Result<()> {
28-
let accesses = track_node_script("try { fs.readFileSync('hello') } catch {}", &[]).await?;
60+
#[cfg_attr(not(target_env = "musl"), test_case("deno"))]
61+
#[cfg_attr(not(target_env = "musl"), ignore = "requires node")]
62+
fn read_sync(runtime: &str) -> anyhow::Result<()> {
63+
let accesses = track_script(runtime, "try { fs.readFileSync('hello') } catch {}", &[])?;
2964
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
3065
Ok(())
3166
}
3267

33-
#[test(tokio::test)]
68+
#[test_case("node")]
3469
#[ignore = "requires node"]
35-
async fn exist_sync() -> anyhow::Result<()> {
36-
let accesses = track_node_script("try { fs.existsSync('hello') } catch {}", &[]).await?;
70+
#[cfg_attr(not(target_env = "musl"), test_case("deno"))]
71+
#[cfg_attr(not(target_env = "musl"), ignore = "requires node")]
72+
fn exist_sync(runtime: &str) -> anyhow::Result<()> {
73+
let accesses = track_script(runtime, "try { fs.existsSync('hello') } catch {}", &[])?;
3774
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
3875
Ok(())
3976
}
4077

41-
#[test(tokio::test)]
78+
#[test_case("node")]
4279
#[ignore = "requires node"]
43-
async fn stat_sync() -> anyhow::Result<()> {
44-
let accesses = track_node_script("try { fs.statSync('hello') } catch {}", &[]).await?;
80+
#[cfg_attr(not(target_env = "musl"), test_case("deno"))]
81+
#[cfg_attr(not(target_env = "musl"), ignore = "requires node")]
82+
fn stat_sync(runtime: &str) -> anyhow::Result<()> {
83+
let accesses = track_script(runtime, "try { fs.statSync('hello') } catch {}", &[])?;
4584
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
4685
Ok(())
4786
}
4887

49-
#[test(tokio::test)]
88+
#[test_case("node")]
5089
#[ignore = "requires node"]
51-
async fn create_read_stream() -> anyhow::Result<()> {
52-
let accesses = track_node_script(
90+
#[cfg_attr(not(target_env = "musl"), test_case("deno"))]
91+
#[cfg_attr(not(target_env = "musl"), ignore = "requires node")]
92+
fn create_read_stream(runtime: &str) -> anyhow::Result<()> {
93+
let accesses = track_script(
94+
runtime,
5395
"try { fs.createReadStream('hello').on('error', () => {}) } catch {}",
5496
&[],
55-
)
56-
.await?;
97+
)?;
5798
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
5899
Ok(())
59100
}
60101

61-
#[test(tokio::test)]
102+
#[test_case("node")]
62103
#[ignore = "requires node"]
63-
async fn create_write_stream() -> anyhow::Result<()> {
104+
#[cfg_attr(not(target_env = "musl"), test_case("deno"))]
105+
#[cfg_attr(not(target_env = "musl"), ignore = "requires node")]
106+
fn create_write_stream(runtime: &str) -> anyhow::Result<()> {
64107
let tmpdir = tempfile::tempdir()?;
65108
let file_path = tmpdir.path().join("hello");
66-
let accesses = track_node_script(
67-
"try { fs.createWriteStream(process.argv[1]).on('error', () => {}) } catch {}",
109+
let accesses = track_script(
110+
runtime,
111+
"try { fs.createWriteStream(process.argv.at(-1)).on('error', () => {}) } catch {}",
68112
&[file_path.as_os_str()],
69-
)
70-
.await?;
113+
)?;
71114
assert_contains(&accesses, file_path.as_path(), AccessMode::WRITE);
72115
Ok(())
73116
}
74117

75-
#[test(tokio::test)]
118+
#[test_case("node")]
76119
#[ignore = "requires node"]
77-
async fn write_sync() -> anyhow::Result<()> {
120+
#[cfg_attr(not(target_env = "musl"), test_case("deno"))]
121+
#[cfg_attr(not(target_env = "musl"), ignore = "requires node")]
122+
fn write_sync(runtime: &str) -> anyhow::Result<()> {
78123
let tmpdir = tempfile::tempdir()?;
79124
let file_path = tmpdir.path().join("hello");
80-
let accesses = track_node_script(
81-
"try { fs.writeFileSync(process.argv[1], '') } catch {}",
125+
let accesses = track_script(
126+
runtime,
127+
"try { fs.writeFileSync(process.argv.at(-1), '') } catch {}",
82128
&[file_path.as_os_str()],
83-
)
84-
.await?;
129+
)?;
85130
assert_contains(&accesses, &file_path, AccessMode::WRITE);
86131
Ok(())
87132
}
88133

89-
#[test(tokio::test)]
134+
#[test_case("node")]
90135
#[ignore = "requires node"]
91-
async fn read_dir_sync() -> anyhow::Result<()> {
92-
let accesses = track_node_script("try { fs.readdirSync('.') } catch {}", &[]).await?;
136+
#[cfg_attr(not(target_env = "musl"), test_case("deno"))]
137+
#[cfg_attr(not(target_env = "musl"), ignore = "requires node")]
138+
fn read_dir_sync(runtime: &str) -> anyhow::Result<()> {
139+
let accesses = track_script(runtime, "try { fs.readdirSync('.') } catch {}", &[])?;
93140
assert_contains(&accesses, &current_dir().unwrap(), AccessMode::READ_DIR);
94141
Ok(())
95142
}
96143

97-
#[test(tokio::test)]
144+
#[test_case("node")]
98145
#[ignore = "requires node"]
99-
async fn subprocess() -> anyhow::Result<()> {
146+
#[cfg_attr(not(target_env = "musl"), test_case("deno"))]
147+
#[cfg_attr(not(target_env = "musl"), ignore = "requires node")]
148+
fn subprocess(runtime: &str) -> anyhow::Result<()> {
100149
let cmd = if cfg!(windows) {
101150
r"'cmd', ['/c', 'type hello']"
102151
} else {
103152
r"'/bin/sh', ['-c', 'cat hello']"
104153
};
105-
let accesses = track_node_script(
154+
let accesses = track_script(
155+
runtime,
106156
&format!("try {{ child_process.spawnSync({cmd}, {{ stdio: 'ignore' }}) }} catch {{}}"),
107157
&[],
108-
)
109-
.await?;
158+
)?;
110159
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
111160
Ok(())
112161
}

packages/tools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@vitest/browser-playwright": "catalog:",
88
"@voidzero-dev/vite-task-client": "workspace:*",
99
"cross-env": "catalog:",
10+
"deno": "catalog:",
1011
"oxfmt": "catalog:",
1112
"oxlint": "catalog:",
1213
"oxlint-tsgolint": "catalog:",

pnpm-lock.yaml

Lines changed: 69 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ packages:
55

66
allowBuilds:
77
'@playwright/browser-chromium': true
8+
deno: true
89

910
catalog:
1011
'@tsconfig/strictest': ^2.0.8
1112
'@types/node': 25.0.3
1213
'@playwright/browser-chromium': 1.52.0
1314
'@vitest/browser-playwright': 4.1.10
1415
cross-env: ^10.1.0
16+
deno: 2.9.2
1517
husky: ^9.1.7
1618
lint-staged: ^17.0.0
1719
oxfmt: 0.57.0

0 commit comments

Comments
 (0)