From 13d0053036785955fde75bc534c5b1f1d7ff6bb7 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 00:54:41 +0900 Subject: [PATCH] test: build e2e yoi binary provider --- .../artifacts/implementation-report.md | 21 +++ .yoi/tickets/00001KV0TJVN5/item.md | 2 +- .yoi/tickets/00001KV0TJVN5/thread.md | 29 +++ tests/e2e/src/lib.rs | 175 ++++++++++++++++-- tests/e2e/tests/panel.rs | 4 +- 5 files changed, 211 insertions(+), 20 deletions(-) create mode 100644 .yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md diff --git a/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md b/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md new file mode 100644 index 00000000..fc30991e --- /dev/null +++ b/.yoi/tickets/00001KV0TJVN5/artifacts/implementation-report.md @@ -0,0 +1,21 @@ +Implementation report for Ticket 00001KV0TJVN5 + +Files changed: +- `tests/e2e/src/lib.rs` + - Added a cached e2e binary provider using `OnceLock`. + - Preserves `YOI_E2E_BIN=` as the explicit override and skips the default cargo build provider in that path. + - Default path runs `${CARGO:-cargo} build -p yoi --features e2e-test --bin yoi` from the workspace root, then returns the direct `target/{profile}/yoi` binary path for PTY spawning. + - Writes `target/e2e-artifacts/binary-provider.json` and emits diagnostics with provider, build command, and binary path. + - Expanded command-failure diagnostics to include command args. +- `tests/e2e/tests/panel.rs` + - Updated panel tests to use the fallible cached binary provider. + +Validation: +- `cargo fmt --check` — passed. +- `git diff --check` — passed. +- `cargo check -p yoi-e2e --all-targets --features e2e` — passed. +- `unset YOI_E2E_BIN && cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; default provider built the current `yoi` binary and PTY-spawned `target/debug/yoi`. +- `YOI_E2E_BIN=/home/hare/Projects/yoi/.worktree/e2e-binary-provider/target/debug/yoi cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; override provider path used without invoking the default cargo-build provider. + +Remaining gaps: +- None known. diff --git a/.yoi/tickets/00001KV0TJVN5/item.md b/.yoi/tickets/00001KV0TJVN5/item.md index 5a84e797..a2fb4a8b 100644 --- a/.yoi/tickets/00001KV0TJVN5/item.md +++ b/.yoi/tickets/00001KV0TJVN5/item.md @@ -2,7 +2,7 @@ title: 'E2E harness が最新 yoi binary を自動 build して使うようにする' state: 'inprogress' created_at: '2026-06-13T15:46:07Z' -updated_at: '2026-06-13T15:47:00Z' +updated_at: '2026-06-13T15:54:18Z' assignee: null readiness: 'ready' queued_by: 'yoi ticket' diff --git a/.yoi/tickets/00001KV0TJVN5/thread.md b/.yoi/tickets/00001KV0TJVN5/thread.md index c793ea0b..f24a87fe 100644 --- a/.yoi/tickets/00001KV0TJVN5/thread.md +++ b/.yoi/tickets/00001KV0TJVN5/thread.md @@ -83,4 +83,33 @@ Escalate if: Ticket evidence、existing E2E harness code map、Orchestrator worktree clean state を確認した。ユーザーが明示的に修正を依頼しており、blocking relation はない。実装 side effect の前に inprogress acceptance を記録する。 +--- + + + +## Implementation report + +Implementation report for Ticket 00001KV0TJVN5 + +Files changed: +- `tests/e2e/src/lib.rs` + - Added a cached e2e binary provider using `OnceLock`. + - Preserves `YOI_E2E_BIN=` as the explicit override and skips the default cargo build provider in that path. + - Default path runs `${CARGO:-cargo} build -p yoi --features e2e-test --bin yoi` from the workspace root, then returns the direct `target/{profile}/yoi` binary path for PTY spawning. + - Writes `target/e2e-artifacts/binary-provider.json` and emits diagnostics with provider, build command, and binary path. + - Expanded command-failure diagnostics to include command args. +- `tests/e2e/tests/panel.rs` + - Updated panel tests to use the fallible cached binary provider. + +Validation: +- `cargo fmt --check` — passed. +- `git diff --check` — passed. +- `cargo check -p yoi-e2e --all-targets --features e2e` — passed. +- `unset YOI_E2E_BIN && cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; default provider built the current `yoi` binary and PTY-spawned `target/debug/yoi`. +- `YOI_E2E_BIN=/home/hare/Projects/yoi/.worktree/e2e-binary-provider/target/debug/yoi cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed; override provider path used without invoking the default cargo-build provider. + +Remaining gaps: +- None known. + + --- diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs index 5c26b6e0..4214d837 100644 --- a/tests/e2e/src/lib.rs +++ b/tests/e2e/src/lib.rs @@ -10,7 +10,7 @@ use std::os::fd::{AsRawFd, FromRawFd}; use std::path::{Path, PathBuf}; use std::process::{Child, Command, ExitStatus, Stdio}; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -23,12 +23,42 @@ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0); pub type Result = std::result::Result; +#[derive(Clone, Debug, Serialize)] +pub struct BinaryProviderInfo { + pub provider: String, + pub binary: PathBuf, + pub workspace_root: PathBuf, + pub cargo: Option, + pub build_args: Vec, + pub build_command: Option, + pub profile: String, +} + +impl BinaryProviderInfo { + fn log(&self) { + match &self.build_command { + Some(command) => eprintln!( + "yoi-e2e binary provider={} command={} binary={}", + self.provider, + command, + self.binary.display() + ), + None => eprintln!( + "yoi-e2e binary provider={} binary={}", + self.provider, + self.binary.display() + ), + } + } +} + #[derive(Debug)] pub enum HarnessError { Io(io::Error), Json(serde_json::Error), CommandFailed { program: PathBuf, + args: Vec, status: ExitStatus, stdout: String, stderr: String, @@ -51,13 +81,14 @@ impl std::fmt::Display for HarnessError { Self::Json(err) => write!(f, "json error: {err}"), Self::CommandFailed { program, + args, status, stdout, stderr, } => write!( f, "{} exited with {status}\nstdout:\n{stdout}\nstderr:\n{stderr}", - program.display() + command_display(program, args) ), Self::Timeout { what, artifacts } => write!( f, @@ -66,7 +97,7 @@ impl std::fmt::Display for HarnessError { ), Self::MissingBinary(path) => write!( f, - "missing yoi binary {}; run `cargo build -p yoi --features e2e-test` or set YOI_E2E_BIN", + "missing yoi binary {}; set YOI_E2E_BIN to an existing binary or inspect target/e2e-artifacts/binary-provider.json", path.display() ), Self::MouseCaptureNotEnabled { artifacts } => write!( @@ -496,13 +527,7 @@ pub struct FixtureWorkspace { impl FixtureWorkspace { pub fn new(binary: &Path) -> Result { - let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(Path::parent) - .ok_or_else(|| { - HarnessError::Protocol("could not resolve workspace root for artifacts".to_owned()) - })? - .to_path_buf(); + let workspace_root = workspace_root()?; let root = workspace_root .join("target") .join("e2e-artifacts") @@ -602,19 +627,134 @@ impl FixtureWorkspace { } } -pub fn yoi_binary() -> PathBuf { - if let Some(path) = std::env::var_os("YOI_E2E_BIN") { - return PathBuf::from(path); +pub fn yoi_binary() -> Result { + Ok(yoi_binary_info()?.binary) +} + +pub fn yoi_binary_info() -> Result { + static BINARY_INFO: OnceLock> = OnceLock::new(); + match BINARY_INFO.get_or_init(|| resolve_yoi_binary().map_err(|err| err.to_string())) { + Ok(info) => Ok(info.clone()), + Err(message) => Err(HarnessError::Protocol(message.clone())), } - let mut path = std::env::current_exe().expect("current executable path"); +} + +fn resolve_yoi_binary() -> Result { + if let Some(path) = std::env::var_os("YOI_E2E_BIN") { + let info = BinaryProviderInfo { + provider: "YOI_E2E_BIN".to_owned(), + binary: PathBuf::from(path), + workspace_root: workspace_root()?, + cargo: None, + build_args: Vec::new(), + build_command: None, + profile: test_profile(), + }; + info.log(); + write_binary_provider_artifact(&info)?; + return Ok(info); + } + + let workspace_root = workspace_root()?; + let cargo = PathBuf::from(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + let mut args = vec![ + "build".to_owned(), + "-p".to_owned(), + "yoi".to_owned(), + "--features".to_owned(), + "e2e-test".to_owned(), + "--bin".to_owned(), + "yoi".to_owned(), + ]; + if test_profile() == "release" { + args.push("--release".to_owned()); + } + + let command = command_display(&cargo, &args); + eprintln!("yoi-e2e binary provider=cargo-build command={command}"); + let output = Command::new(&cargo) + .args(&args) + .current_dir(&workspace_root) + .output()?; + if !output.status.success() { + return Err(HarnessError::CommandFailed { + program: cargo, + args, + status: output.status, + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }); + } + + let binary = current_target_profile_dir()?.join(binary_name()); + let info = BinaryProviderInfo { + provider: "cargo-build".to_owned(), + binary, + workspace_root, + cargo: Some(cargo), + build_args: args, + build_command: Some(command), + profile: test_profile(), + }; + info.log(); + write_binary_provider_artifact(&info)?; + if !info.binary.exists() { + return Err(HarnessError::MissingBinary(info.binary)); + } + Ok(info) +} + +fn workspace_root() -> Result { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .map(Path::to_path_buf) + .ok_or_else(|| HarnessError::Protocol("could not resolve workspace root".to_owned())) +} + +fn current_target_profile_dir() -> Result { + let mut path = std::env::current_exe()?; while let Some(name) = path.file_name().and_then(|name| name.to_str()) { if name == "debug" || name == "release" { - path.push("yoi"); - return path; + return Ok(path); } path.pop(); } - PathBuf::from("target/debug/yoi") + Ok(workspace_root()?.join("target").join(test_profile())) +} + +fn test_profile() -> String { + let Ok(mut path) = std::env::current_exe() else { + return "debug".to_owned(); + }; + while let Some(name) = path.file_name().and_then(|name| name.to_str()) { + if name == "debug" || name == "release" { + return name.to_owned(); + } + path.pop(); + } + "debug".to_owned() +} + +fn binary_name() -> String { + format!("yoi{}", std::env::consts::EXE_SUFFIX) +} + +fn write_binary_provider_artifact(info: &BinaryProviderInfo) -> Result<()> { + let dir = info.workspace_root.join("target").join("e2e-artifacts"); + fs::create_dir_all(&dir)?; + fs::write( + dir.join("binary-provider.json"), + serde_json::to_vec_pretty(info)?, + )?; + Ok(()) +} + +fn command_display(program: &Path, args: &[String]) -> String { + std::iter::once(program.display().to_string()) + .chain(args.iter().cloned()) + .collect::>() + .join(" ") } fn open_pty(size: (u16, u16)) -> Result<(File, File)> { @@ -704,6 +844,7 @@ fn run_yoi_capture( if !output.status.success() { return Err(HarnessError::CommandFailed { program: binary.to_path_buf(), + args: args.iter().map(|arg| (*arg).to_owned()).collect(), status: output.status, stdout: String::from_utf8_lossy(&output.stdout).into_owned(), stderr: String::from_utf8_lossy(&output.stderr).into_owned(), diff --git a/tests/e2e/tests/panel.rs b/tests/e2e/tests/panel.rs index 87a9638c..bf4d492a 100644 --- a/tests/e2e/tests/panel.rs +++ b/tests/e2e/tests/panel.rs @@ -4,7 +4,7 @@ use yoi_e2e::{FixtureWorkspace, KeyPress, PanelHarness, yoi_binary}; #[test] fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result<()> { - let binary = yoi_binary(); + let binary = yoi_binary()?; let fixture = FixtureWorkspace::new(&binary)?; let mut panel = PanelHarness::spawn(fixture.panel_config(binary))?; @@ -39,7 +39,7 @@ fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result #[test] fn panel_ctrl_c_exits_promptly_after_background_barrier() -> yoi_e2e::Result<()> { - let binary = yoi_binary(); + let binary = yoi_binary()?; let fixture = FixtureWorkspace::new(&binary)?; let mut panel = PanelHarness::spawn(fixture.panel_config_holding_background_task(binary, "reload"))?;