Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 117 additions & 24 deletions crates/vite_task_bin/tests/e2e_snapshots/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ mod redact;
use std::{
env::{self, join_paths, split_paths},
ffi::OsStr,
io::Write,
path::{Path, PathBuf},
process::{Command, Stdio},
process::Stdio,
sync::Arc,
time::Duration,
};

use copy_dir::copy_dir;
use redact::redact_e2e_output;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
process::Command,
};
use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf};
use vite_str::Str;
use vite_workspace::find_workspace_root;

/// Timeout for each step in e2e tests
const STEP_TIMEOUT: Duration = Duration::from_secs(10);

/// Get the shell executable for running e2e test steps.
/// On Unix, uses /bin/sh.
/// On Windows, uses BASH env var or falls back to Git Bash.
Expand Down Expand Up @@ -77,7 +84,12 @@ struct SnapshotsFile {
pub e2e_cases: Vec<E2e>,
}

fn run_case(tmpdir: &AbsolutePath, fixture_path: &Path, filter: Option<&str>) {
fn run_case(
runtime: &tokio::runtime::Runtime,
tmpdir: &AbsolutePath,
fixture_path: &Path,
filter: Option<&str>,
) {
let fixture_name = fixture_path.file_name().unwrap().to_str().unwrap();
if fixture_name.starts_with(".") {
return; // skip hidden files like .DS_Store
Expand All @@ -96,10 +108,11 @@ fn run_case(tmpdir: &AbsolutePath, fixture_path: &Path, filter: Option<&str>) {
settings.set_prepend_module_to_snapshot(false);
settings.remove_snapshot_suffix();

settings.bind(|| run_case_inner(tmpdir, fixture_path, fixture_name));
// Use block_on inside bind to run async code with insta settings applied
settings.bind(|| runtime.block_on(run_case_inner(tmpdir, fixture_path, fixture_name)));
}

fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &Path, fixture_name: &str) {
async fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &Path, fixture_name: &str) {
// Copy the case directory to a temporary directory to avoid discovering workspace outside of the test case.
let stage_path = tmpdir.join(fixture_name);
copy_dir(fixture_path, &stage_path).unwrap();
Expand Down Expand Up @@ -175,30 +188,109 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &Path, fixture_name: &str
}
}

let output = if let Some(stdin_content) = step.stdin() {
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn().unwrap();
child.stdin.take().unwrap().write_all(stdin_content.as_bytes()).unwrap();
child.wait_with_output().unwrap()
} else {
cmd.output().unwrap()
// Spawn the child process
cmd.stdin(if step.stdin().is_some() { Stdio::piped() } else { Stdio::null() });
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());

let mut child = cmd.spawn().unwrap();

// Write stdin if provided, then close it
if let Some(stdin_content) = step.stdin() {
let mut stdin = child.stdin.take().unwrap();
stdin.write_all(stdin_content.as_bytes()).await.unwrap();
drop(stdin); // Close stdin to signal EOF
}

// Take stdout/stderr handles
let mut stdout_handle = child.stdout.take().unwrap();
let mut stderr_handle = child.stderr.take().unwrap();

// Buffers for accumulating output
let mut stdout_buf = Vec::new();
let mut stderr_buf = Vec::new();

// Read chunks concurrently with process wait, using select! with timeout
let mut stdout_done = false;
let mut stderr_done = false;

enum TerminationState {
Exited(std::process::ExitStatus),
TimedOut,
}
// Initial state is running
let mut termination_state: Option<TerminationState> = None;

let timeout = tokio::time::sleep(STEP_TIMEOUT);
tokio::pin!(timeout);

let termination_state = loop {
let mut stdout_chunk = [0u8; 8192];
let mut stderr_chunk = [0u8; 8192];

tokio::select! {
result = stdout_handle.read(&mut stdout_chunk), if !stdout_done => {
match result {
Ok(0) => stdout_done = true,
Ok(n) => stdout_buf.extend_from_slice(&stdout_chunk[..n]),
Err(_) => stdout_done = true,
}
}
result = stderr_handle.read(&mut stderr_chunk), if !stderr_done => {
match result {
Ok(0) => stderr_done = true,
Ok(n) => stderr_buf.extend_from_slice(&stderr_chunk[..n]),
Err(_) => stderr_done = true,
}
}
result = child.wait(), if termination_state.is_none() => {
termination_state = Some(TerminationState::Exited(result.unwrap()));
}
_ = &mut timeout, if termination_state.is_none() => {
// Timeout - kill the process
let _ = child.kill().await;
termination_state = Some(TerminationState::TimedOut);
}
}

// Exit conditions:
// 1. Process exited and all output drained
// 2. Timed out and all output drained (after kill, pipes close)
if let Some(termination_state) = &termination_state
&& stdout_done
&& stderr_done
{
break termination_state;
}
};

let exit_code = output.status.code().unwrap_or(-1);
if exit_code != 0 {
e2e_outputs.push_str(format!("[{}]", exit_code).as_str());
// Format output
match termination_state {
TerminationState::TimedOut => {
e2e_outputs.push_str("[timeout]");
}
TerminationState::Exited(status) => {
let exit_code = status.code().unwrap_or(-1);
if exit_code != 0 {
e2e_outputs.push_str(format!("[{}]", exit_code).as_str());
}
}
}

e2e_outputs.push_str("> ");
e2e_outputs.push_str(step.cmd());
e2e_outputs.push('\n');

let stdout = String::from_utf8(output.stdout).unwrap();
let stderr = String::from_utf8(output.stderr).unwrap();
let stdout = String::from_utf8_lossy(&stdout_buf).into_owned();
let stderr = String::from_utf8_lossy(&stderr_buf).into_owned();
e2e_outputs.push_str(&redact_e2e_output(stdout, e2e_stage_path_str));
e2e_outputs.push_str(&redact_e2e_output(stderr, e2e_stage_path_str));
e2e_outputs.push('\n');

// Skip remaining steps if timed out
if matches!(termination_state, TerminationState::TimedOut) {
break;
}
}
insta::assert_snapshot!(e2e.name.as_str(), e2e_outputs);
}
Expand All @@ -212,9 +304,10 @@ fn main() {

let tests_dir = std::env::current_dir().unwrap().join("tests");

insta::glob!(tests_dir, "e2e_snapshots/fixtures/*", |case_path| run_case(
&tmp_dir_path,
case_path,
filter.as_deref()
));
// Create tokio runtime for async operations
let runtime = tokio::runtime::Runtime::new().unwrap();

insta::glob!(tests_dir, "e2e_snapshots/fixtures/*", |case_path| {
run_case(&runtime, &tmp_dir_path, case_path, filter.as_deref())
});
}