Skip to content

Commit d0adc11

Browse files
committed
fix: make interactions-no-vp work in cargo xtest
1 parent 2bdc26e commit d0adc11

File tree

5 files changed

+158
-52
lines changed

5 files changed

+158
-52
lines changed

crates/vite_task_bin/src/main.rs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,15 @@ impl Drop for RawModeGuard {
112112

113113
fn run_interact() -> anyhow::Result<ExitStatus> {
114114
let stdin_is_tty = std::io::stdin().is_terminal();
115-
let mut raw_mode = RawModeGuard::new(stdin_is_tty)?;
115+
let enable_raw_mode = if cfg!(windows) { true } else { stdin_is_tty };
116+
let mut raw_mode = RawModeGuard::new(enable_raw_mode)?;
116117

117118
let mut stdin = std::io::stdin();
118119
let mut stdout = std::io::stdout();
119120
let mut text_buffer = Vec::<u8>::new();
121+
let mut ansi_escape_pending = false;
122+
let mut ansi_csi_pending = false;
123+
let mut windows_extended_key_pending = false;
120124

121125
write_line(&mut stdout, b"START")?;
122126
write_milestone(&mut stdout, "ready")?;
@@ -129,19 +133,50 @@ fn run_interact() -> anyhow::Result<ExitStatus> {
129133
}
130134

131135
let byte = byte[0];
132-
if byte == 0x1b {
133-
let mut seq = [0u8; 2];
134-
if stdin.read_exact(&mut seq).is_err() {
135-
break;
136+
if ansi_escape_pending {
137+
ansi_escape_pending = false;
138+
139+
if byte == b'[' || byte == b'O' {
140+
ansi_csi_pending = true;
141+
continue;
142+
}
143+
}
144+
145+
if ansi_csi_pending {
146+
ansi_csi_pending = false;
147+
148+
if byte == b'A' {
149+
write_milestone(&mut stdout, "after-up")?;
150+
continue;
151+
}
152+
153+
if byte == b'B' {
154+
write_milestone(&mut stdout, "after-down")?;
155+
continue;
136156
}
157+
}
158+
159+
if windows_extended_key_pending {
160+
windows_extended_key_pending = false;
137161

138-
if seq == [b'[', b'A'] {
139-
write_line(&mut stdout, b"KEY:UP")?;
162+
if byte == 72 {
140163
write_milestone(&mut stdout, "after-up")?;
141-
} else if seq == [b'[', b'B'] {
142-
write_line(&mut stdout, b"KEY:DOWN")?;
164+
continue;
165+
}
166+
167+
if byte == 80 {
143168
write_milestone(&mut stdout, "after-down")?;
169+
continue;
144170
}
171+
}
172+
173+
if byte == 0x1b {
174+
ansi_escape_pending = true;
175+
continue;
176+
}
177+
178+
if byte == 0x00 || byte == 0xe0 {
179+
windows_extended_key_pending = true;
145180
continue;
146181
}
147182

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[[e2e]]
22
name = "interactions without vp"
33
steps = [
4-
{ command = "vp interact", interactions = [{ "expect-milestone" = "ready" }, { "write" = "hello" }, { "write-line" = "hello" }, { "expect-milestone" = "after-line" }, { "write-key" = "up" }, { "expect-milestone" = "after-up" }, { "write-key" = "down" }, { "expect-milestone" = "after-down" }, { "write-key" = "enter" }, { "expect-milestone" = "after-enter" }] },
4+
{ command = "vp interact", interactions = [{ "expect-milestone" = "ready" }, { "write" = "hello" }, { "write-line" = "hello" }, { "expect-milestone" = "after-line" }, { "write-key" = "up" }, { "write-key" = "down" }, { "write" = "x" }, { "write-key" = "enter" }, { "expect-milestone" = "after-line" }, { "write-key" = "enter" }, { "expect-milestone" = "after-enter" }] },
55
{ command = "node -e \"console.log('PIPE_STDIN_IS_TTY:' + String(Boolean(process.stdin.isTTY)))\"", pty = false },
66
]

crates/vite_task_bin/tests/e2e_snapshots/fixtures/interactions-no-vp/snapshots/interactions without vp.snap

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
22
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
33
expression: e2e_outputs
4-
input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/interactions-no-vp
54
---
65
> vp interact
76
@ expect-milestone: ready
@@ -22,22 +21,10 @@ CHAR:l
2221
CHAR:o
2322
LINE:hellohello
2423
@ write-key: up
25-
@ expect-milestone: after-up
26-
START
27-
CHAR:h
28-
CHAR:e
29-
CHAR:l
30-
CHAR:l
31-
CHAR:o
32-
CHAR:h
33-
CHAR:e
34-
CHAR:l
35-
CHAR:l
36-
CHAR:o
37-
LINE:hellohello
38-
KEY:UP
3924
@ write-key: down
40-
@ expect-milestone: after-down
25+
@ write: x
26+
@ write-key: enter
27+
@ expect-milestone: after-line
4128
START
4229
CHAR:h
4330
CHAR:e
@@ -50,8 +37,8 @@ CHAR:l
5037
CHAR:l
5138
CHAR:o
5239
LINE:hellohello
53-
KEY:UP
54-
KEY:DOWN
40+
CHAR:x
41+
LINE:x
5542
@ write-key: enter
5643
@ expect-milestone: after-enter
5744
START
@@ -66,8 +53,8 @@ CHAR:l
6653
CHAR:l
6754
CHAR:o
6855
LINE:hellohello
69-
KEY:UP
70-
KEY:DOWN
56+
CHAR:x
57+
LINE:x
7158
KEY:ENTER
7259
DONE
7360
START
@@ -82,8 +69,8 @@ CHAR:l
8269
CHAR:l
8370
CHAR:o
8471
LINE:hellohello
85-
KEY:UP
86-
KEY:DOWN
72+
CHAR:x
73+
LINE:x
8774
KEY:ENTER
8875
DONE
8976
> node -e "console.log('PIPE_STDIN_IS_TTY:' + String(Boolean(process.stdin.isTTY)))"

crates/vite_task_bin/tests/e2e_snapshots/main.rs

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use std::{
99
time::{Duration, Instant},
1010
};
1111

12-
use copy_dir::copy_dir;
1312
use pty_terminal::{geo::ScreenSize, terminal::CommandBuilder};
1413
use pty_terminal_test::TestTerminal;
1514
use redact::redact_e2e_output;
@@ -23,6 +22,9 @@ const STEP_TIMEOUT: Duration = Duration::from_secs(20);
2322
/// Screen size for the PTY terminal. Large enough to avoid line wrapping.
2423
const SCREEN_SIZE: ScreenSize = ScreenSize { rows: 500, cols: 500 };
2524

25+
const COMPILE_TIME_CARGO_BIN_EXE_VP: &str = env!("CARGO_BIN_EXE_vp");
26+
const COMPILE_TIME_CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");
27+
2628
/// Get the shell executable for running e2e test steps.
2729
/// On Unix, uses /bin/sh.
2830
/// On Windows, uses BASH env var or falls back to Git Bash.
@@ -53,6 +55,65 @@ fn get_shell_exe() -> std::path::PathBuf {
5355
}
5456
}
5557

58+
#[expect(
59+
clippy::disallowed_types,
60+
reason = "Path types required for runtime path remapping between compile and runtime roots"
61+
)]
62+
fn runtime_manifest_dir() -> std::path::PathBuf {
63+
std::env::var_os("CARGO_MANIFEST_DIR").map_or_else(
64+
|| std::path::PathBuf::from(COMPILE_TIME_CARGO_MANIFEST_DIR),
65+
std::path::PathBuf::from,
66+
)
67+
}
68+
69+
#[expect(
70+
clippy::disallowed_types,
71+
reason = "Path types required for runtime path remapping between compile and runtime roots"
72+
)]
73+
fn relative_path_from(path: &std::path::Path, base: &std::path::Path) -> std::path::PathBuf {
74+
use std::path::{Component, PathBuf};
75+
76+
let path_components = path.components().collect::<Vec<_>>();
77+
let base_components = base.components().collect::<Vec<_>>();
78+
79+
let common_prefix_len = path_components
80+
.iter()
81+
.zip(base_components.iter())
82+
.take_while(|(path_comp, base_comp)| path_comp == base_comp)
83+
.count();
84+
85+
let mut relative_path = PathBuf::new();
86+
87+
for base_comp in &base_components[common_prefix_len..] {
88+
match base_comp {
89+
Component::Normal(_) | Component::CurDir | Component::ParentDir => {
90+
relative_path.push("..");
91+
}
92+
Component::RootDir | Component::Prefix(_) => {}
93+
}
94+
}
95+
96+
for path_comp in &path_components[common_prefix_len..] {
97+
relative_path.push(path_comp.as_os_str());
98+
}
99+
100+
relative_path
101+
}
102+
103+
#[expect(
104+
clippy::disallowed_types,
105+
reason = "Path types required for runtime path remapping between compile and runtime roots"
106+
)]
107+
fn resolve_runtime_vp_path() -> AbsolutePathBuf {
108+
let compile_time_vp = std::path::Path::new(COMPILE_TIME_CARGO_BIN_EXE_VP);
109+
let compile_time_manifest = std::path::Path::new(COMPILE_TIME_CARGO_MANIFEST_DIR);
110+
let vp_relative_path = relative_path_from(compile_time_vp, compile_time_manifest);
111+
112+
let runtime_manifest = runtime_manifest_dir();
113+
let runtime_vp = runtime_manifest.join(vp_relative_path);
114+
AbsolutePathBuf::new(runtime_vp).unwrap()
115+
}
116+
56117
const fn default_true() -> bool {
57118
true
58119
}
@@ -207,6 +268,29 @@ enum TerminationState {
207268
TimedOut,
208269
}
209270

271+
#[expect(
272+
clippy::disallowed_types,
273+
reason = "Path required for recursive fixture copy in test harness"
274+
)]
275+
fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
276+
std::fs::create_dir_all(dst)?;
277+
278+
for entry in std::fs::read_dir(src)? {
279+
let entry = entry?;
280+
let source_path = entry.path();
281+
let target_path = dst.join(entry.file_name());
282+
let file_type = entry.file_type()?;
283+
284+
if file_type.is_dir() {
285+
copy_dir_recursive(&source_path, &target_path)?;
286+
} else if file_type.is_file() {
287+
std::fs::copy(&source_path, &target_path)?;
288+
}
289+
}
290+
291+
Ok(())
292+
}
293+
210294
fn kill_process(child: &mut std::process::Child) {
211295
let _ = child.kill();
212296
}
@@ -222,7 +306,7 @@ fn kill_process(child: &mut std::process::Child) {
222306
fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture_name: &str) {
223307
// Copy the case directory to a temporary directory to avoid discovering workspace outside of the test case.
224308
let stage_path = tmpdir.join(fixture_name);
225-
copy_dir(fixture_path, &stage_path).unwrap();
309+
copy_dir_recursive(fixture_path, stage_path.as_path()).unwrap();
226310

227311
let (workspace_root, _cwd) = find_workspace_root(&stage_path).unwrap();
228312

@@ -238,13 +322,9 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
238322
Err(err) => panic!("Failed to read cases.toml for fixture {fixture_name}: {err}"),
239323
};
240324

241-
// Navigate from CARGO_MANIFEST_DIR to packages/tools at the repo root
242-
#[expect(
243-
clippy::disallowed_types,
244-
reason = "Path required for CARGO_MANIFEST_DIR path manipulation via env! macro"
245-
)]
246-
let repo_root =
247-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent().unwrap();
325+
// Navigate from runtime CARGO_MANIFEST_DIR to packages/tools at the repo root.
326+
let repo_root = runtime_manifest_dir();
327+
let repo_root = repo_root.parent().unwrap().parent().unwrap();
248328
let test_bin_path = Arc::<OsStr>::from(
249329
repo_root.join("packages").join("tools").join("node_modules").join(".bin").into_os_string(),
250330
);
@@ -257,7 +337,7 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
257337
[
258338
// Include vp binary path to PATH so that e2e tests can run "vp ..." commands.
259339
{
260-
let vp_path = AbsolutePath::new(env!("CARGO_BIN_EXE_vp")).unwrap();
340+
let vp_path = resolve_runtime_vp_path();
261341
let vp_dir = vp_path.parent().unwrap();
262342
vp_dir.as_path().as_os_str().into()
263343
},
@@ -289,7 +369,7 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
289369

290370
let e2e_stage_path = tmpdir.join(vite_str::format!("{fixture_name}_e2e_stage_{e2e_count}"));
291371
e2e_count += 1;
292-
assert!(copy_dir(fixture_path, &e2e_stage_path).unwrap().is_empty());
372+
copy_dir_recursive(fixture_path, e2e_stage_path.as_path()).unwrap();
293373

294374
let e2e_stage_path_str = e2e_stage_path.as_path().to_str().unwrap();
295375

@@ -530,20 +610,20 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
530610
}
531611
}
532612

533-
#[expect(clippy::disallowed_types, reason = "Path required by insta::glob! macro callback")]
534-
#[expect(
535-
clippy::disallowed_methods,
536-
reason = "current_dir needed because insta::glob! requires std PathBuf"
537-
)]
538613
fn main() {
539614
let filter = std::env::args().nth(1);
540615

541616
let tmp_dir = tempfile::tempdir().unwrap();
542-
let tmp_dir_path = AbsolutePathBuf::new(tmp_dir.path().canonicalize().unwrap()).unwrap();
617+
let tmp_dir_path = AbsolutePathBuf::new(tmp_dir.path().to_path_buf()).unwrap();
543618

544-
let tests_dir = std::env::current_dir().unwrap().join("tests");
619+
let fixtures_dir = runtime_manifest_dir().join("tests").join("e2e_snapshots").join("fixtures");
620+
let mut fixture_paths = std::fs::read_dir(fixtures_dir)
621+
.unwrap()
622+
.map(|entry| entry.unwrap().path())
623+
.collect::<Vec<_>>();
624+
fixture_paths.sort();
545625

546-
insta::glob!(tests_dir, "e2e_snapshots/fixtures/*", |case_path| {
626+
for case_path in &fixture_paths {
547627
run_case(&tmp_dir_path, case_path, filter.as_deref());
548-
});
628+
}
549629
}

crates/vite_task_bin/tests/e2e_snapshots/redact.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ pub fn redact_e2e_output(mut output: String, workspace_root: &str) -> String {
6060
.unwrap();
6161
output = node_trace_warning_regex.replace_all(&output, "").into_owned();
6262

63+
// Remove nondeterministic mise warnings from shell startup in cross-platform runners.
64+
let mise_warning_regex = regex::Regex::new(r"(?m)^mise WARN\s+.*\n?").unwrap();
65+
output = mise_warning_regex.replace_all(&output, "").into_owned();
66+
6367
// Sort consecutive diagnostic blocks to handle non-deterministic tool output
6468
// (e.g., oxlint reports warnings in arbitrary order due to multi-threading).
6569
// Each block starts with " ! " and ends at the next empty line.

0 commit comments

Comments
 (0)