Skip to content

Commit 46553de

Browse files
wan9chicodex
andcommitted
test(fspy): run node fs tests with Deno
Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 77c69e4 commit 46553de

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: 94 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,159 @@
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+
#[test_case("node")]
57+
#[ignore = "requires node"]
58+
#[test_case("deno")]
2659
#[ignore = "requires node"]
27-
async fn read_sync() -> anyhow::Result<()> {
28-
let accesses = track_node_script("try { fs.readFileSync('hello') } catch {}", &[]).await?;
60+
fn read_sync(runtime: &str) -> anyhow::Result<()> {
61+
let accesses = track_script(runtime, "try { fs.readFileSync('hello') } catch {}", &[])?;
2962
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
3063
Ok(())
3164
}
3265

33-
#[test(tokio::test)]
66+
#[test_case("node")]
67+
#[ignore = "requires node"]
68+
#[test_case("deno")]
3469
#[ignore = "requires node"]
35-
async fn exist_sync() -> anyhow::Result<()> {
36-
let accesses = track_node_script("try { fs.existsSync('hello') } catch {}", &[]).await?;
70+
fn exist_sync(runtime: &str) -> anyhow::Result<()> {
71+
let accesses = track_script(runtime, "try { fs.existsSync('hello') } catch {}", &[])?;
3772
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
3873
Ok(())
3974
}
4075

41-
#[test(tokio::test)]
76+
#[test_case("node")]
4277
#[ignore = "requires node"]
43-
async fn stat_sync() -> anyhow::Result<()> {
44-
let accesses = track_node_script("try { fs.statSync('hello') } catch {}", &[]).await?;
78+
#[test_case("deno")]
79+
#[ignore = "requires node"]
80+
fn stat_sync(runtime: &str) -> anyhow::Result<()> {
81+
let accesses = track_script(runtime, "try { fs.statSync('hello') } catch {}", &[])?;
4582
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
4683
Ok(())
4784
}
4885

49-
#[test(tokio::test)]
86+
#[test_case("node")]
87+
#[ignore = "requires node"]
88+
#[test_case("deno")]
5089
#[ignore = "requires node"]
51-
async fn create_read_stream() -> anyhow::Result<()> {
52-
let accesses = track_node_script(
90+
fn create_read_stream(runtime: &str) -> anyhow::Result<()> {
91+
let accesses = track_script(
92+
runtime,
5393
"try { fs.createReadStream('hello').on('error', () => {}) } catch {}",
5494
&[],
55-
)
56-
.await?;
95+
)?;
5796
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
5897
Ok(())
5998
}
6099

61-
#[test(tokio::test)]
100+
#[test_case("node")]
101+
#[ignore = "requires node"]
102+
#[test_case("deno")]
62103
#[ignore = "requires node"]
63-
async fn create_write_stream() -> anyhow::Result<()> {
104+
fn create_write_stream(runtime: &str) -> anyhow::Result<()> {
64105
let tmpdir = tempfile::tempdir()?;
65106
let file_path = tmpdir.path().join("hello");
66-
let accesses = track_node_script(
67-
"try { fs.createWriteStream(process.argv[1]).on('error', () => {}) } catch {}",
107+
let accesses = track_script(
108+
runtime,
109+
"try { fs.createWriteStream(process.argv.at(-1)).on('error', () => {}) } catch {}",
68110
&[file_path.as_os_str()],
69-
)
70-
.await?;
111+
)?;
71112
assert_contains(&accesses, file_path.as_path(), AccessMode::WRITE);
72113
Ok(())
73114
}
74115

75-
#[test(tokio::test)]
116+
#[test_case("node")]
76117
#[ignore = "requires node"]
77-
async fn write_sync() -> anyhow::Result<()> {
118+
#[test_case("deno")]
119+
#[ignore = "requires node"]
120+
fn write_sync(runtime: &str) -> anyhow::Result<()> {
78121
let tmpdir = tempfile::tempdir()?;
79122
let file_path = tmpdir.path().join("hello");
80-
let accesses = track_node_script(
81-
"try { fs.writeFileSync(process.argv[1], '') } catch {}",
123+
let accesses = track_script(
124+
runtime,
125+
"try { fs.writeFileSync(process.argv.at(-1), '') } catch {}",
82126
&[file_path.as_os_str()],
83-
)
84-
.await?;
127+
)?;
85128
assert_contains(&accesses, &file_path, AccessMode::WRITE);
86129
Ok(())
87130
}
88131

89-
#[test(tokio::test)]
132+
#[test_case("node")]
133+
#[ignore = "requires node"]
134+
#[test_case("deno")]
90135
#[ignore = "requires node"]
91-
async fn read_dir_sync() -> anyhow::Result<()> {
92-
let accesses = track_node_script("try { fs.readdirSync('.') } catch {}", &[]).await?;
136+
fn read_dir_sync(runtime: &str) -> anyhow::Result<()> {
137+
let accesses = track_script(runtime, "try { fs.readdirSync('.') } catch {}", &[])?;
93138
assert_contains(&accesses, &current_dir().unwrap(), AccessMode::READ_DIR);
94139
Ok(())
95140
}
96141

97-
#[test(tokio::test)]
142+
#[test_case("node")]
143+
#[ignore = "requires node"]
144+
#[test_case("deno")]
98145
#[ignore = "requires node"]
99-
async fn subprocess() -> anyhow::Result<()> {
146+
fn subprocess(runtime: &str) -> anyhow::Result<()> {
100147
let cmd = if cfg!(windows) {
101148
r"'cmd', ['/c', 'type hello']"
102149
} else {
103150
r"'/bin/sh', ['-c', 'cat hello']"
104151
};
105-
let accesses = track_node_script(
152+
let accesses = track_script(
153+
runtime,
106154
&format!("try {{ child_process.spawnSync({cmd}, {{ stdio: 'ignore' }}) }} catch {{}}"),
107155
&[],
108-
)
109-
.await?;
156+
)?;
110157
assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::READ);
111158
Ok(())
112159
}

packages/tools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"dependencies": {
66
"@voidzero-dev/vite-task-client": "workspace:*",
77
"cross-env": "catalog:",
8+
"deno": "catalog:",
89
"oxfmt": "catalog:",
910
"oxlint": "catalog:",
1011
"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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ packages:
33
- packages/tools
44
- packages/vite-task-client
55

6+
allowBuilds:
7+
deno: true
8+
69
catalog:
710
'@tsconfig/strictest': ^2.0.8
811
'@types/node': 25.0.3
912
cross-env: ^10.1.0
13+
deno: 2.9.2
1014
husky: ^9.1.7
1115
lint-staged: ^17.0.0
1216
oxfmt: 0.57.0

0 commit comments

Comments
 (0)