test: build e2e yoi binary provider

This commit is contained in:
Keisuke Hirata 2026-06-14 00:54:41 +09:00
parent a4df975415
commit 13d0053036
No known key found for this signature in database
5 changed files with 211 additions and 20 deletions

View File

@ -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=<path>` 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.

View File

@ -2,7 +2,7 @@
title: 'E2E harness が最新 yoi binary を自動 build して使うようにする' title: 'E2E harness が最新 yoi binary を自動 build して使うようにする'
state: 'inprogress' state: 'inprogress'
created_at: '2026-06-13T15:46:07Z' created_at: '2026-06-13T15:46:07Z'
updated_at: '2026-06-13T15:47:00Z' updated_at: '2026-06-13T15:54:18Z'
assignee: null assignee: null
readiness: 'ready' readiness: 'ready'
queued_by: 'yoi ticket' queued_by: 'yoi ticket'

View File

@ -83,4 +83,33 @@ Escalate if:
Ticket evidence、existing E2E harness code map、Orchestrator worktree clean state を確認した。ユーザーが明示的に修正を依頼しており、blocking relation はない。実装 side effect の前に inprogress acceptance を記録する。 Ticket evidence、existing E2E harness code map、Orchestrator worktree clean state を確認した。ユーザーが明示的に修正を依頼しており、blocking relation はない。実装 side effect の前に inprogress acceptance を記録する。
---
<!-- event: implementation_report author: hare at: 2026-06-13T15:54:18Z -->
## 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=<path>` 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.
--- ---

View File

@ -10,7 +10,7 @@ use std::os::fd::{AsRawFd, FromRawFd};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio}; use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex, OnceLock};
use std::thread::{self, JoinHandle}; use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
@ -23,12 +23,42 @@ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
pub type Result<T> = std::result::Result<T, HarnessError>; pub type Result<T> = std::result::Result<T, HarnessError>;
#[derive(Clone, Debug, Serialize)]
pub struct BinaryProviderInfo {
pub provider: String,
pub binary: PathBuf,
pub workspace_root: PathBuf,
pub cargo: Option<PathBuf>,
pub build_args: Vec<String>,
pub build_command: Option<String>,
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)] #[derive(Debug)]
pub enum HarnessError { pub enum HarnessError {
Io(io::Error), Io(io::Error),
Json(serde_json::Error), Json(serde_json::Error),
CommandFailed { CommandFailed {
program: PathBuf, program: PathBuf,
args: Vec<String>,
status: ExitStatus, status: ExitStatus,
stdout: String, stdout: String,
stderr: String, stderr: String,
@ -51,13 +81,14 @@ impl std::fmt::Display for HarnessError {
Self::Json(err) => write!(f, "json error: {err}"), Self::Json(err) => write!(f, "json error: {err}"),
Self::CommandFailed { Self::CommandFailed {
program, program,
args,
status, status,
stdout, stdout,
stderr, stderr,
} => write!( } => write!(
f, f,
"{} exited with {status}\nstdout:\n{stdout}\nstderr:\n{stderr}", "{} exited with {status}\nstdout:\n{stdout}\nstderr:\n{stderr}",
program.display() command_display(program, args)
), ),
Self::Timeout { what, artifacts } => write!( Self::Timeout { what, artifacts } => write!(
f, f,
@ -66,7 +97,7 @@ impl std::fmt::Display for HarnessError {
), ),
Self::MissingBinary(path) => write!( Self::MissingBinary(path) => write!(
f, 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() path.display()
), ),
Self::MouseCaptureNotEnabled { artifacts } => write!( Self::MouseCaptureNotEnabled { artifacts } => write!(
@ -496,13 +527,7 @@ pub struct FixtureWorkspace {
impl FixtureWorkspace { impl FixtureWorkspace {
pub fn new(binary: &Path) -> Result<Self> { pub fn new(binary: &Path) -> Result<Self> {
let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let workspace_root = workspace_root()?;
.parent()
.and_then(Path::parent)
.ok_or_else(|| {
HarnessError::Protocol("could not resolve workspace root for artifacts".to_owned())
})?
.to_path_buf();
let root = workspace_root let root = workspace_root
.join("target") .join("target")
.join("e2e-artifacts") .join("e2e-artifacts")
@ -602,19 +627,134 @@ impl FixtureWorkspace {
} }
} }
pub fn yoi_binary() -> PathBuf { pub fn yoi_binary() -> Result<PathBuf> {
if let Some(path) = std::env::var_os("YOI_E2E_BIN") { Ok(yoi_binary_info()?.binary)
return PathBuf::from(path); }
pub fn yoi_binary_info() -> Result<BinaryProviderInfo> {
static BINARY_INFO: OnceLock<std::result::Result<BinaryProviderInfo, String>> = 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<BinaryProviderInfo> {
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> {
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<PathBuf> {
let mut path = std::env::current_exe()?;
while let Some(name) = path.file_name().and_then(|name| name.to_str()) { while let Some(name) = path.file_name().and_then(|name| name.to_str()) {
if name == "debug" || name == "release" { if name == "debug" || name == "release" {
path.push("yoi"); return Ok(path);
return path;
} }
path.pop(); 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::<Vec<_>>()
.join(" ")
} }
fn open_pty(size: (u16, u16)) -> Result<(File, File)> { fn open_pty(size: (u16, u16)) -> Result<(File, File)> {
@ -704,6 +844,7 @@ fn run_yoi_capture(
if !output.status.success() { if !output.status.success() {
return Err(HarnessError::CommandFailed { return Err(HarnessError::CommandFailed {
program: binary.to_path_buf(), program: binary.to_path_buf(),
args: args.iter().map(|arg| (*arg).to_owned()).collect(),
status: output.status, status: output.status,
stdout: String::from_utf8_lossy(&output.stdout).into_owned(), stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(), stderr: String::from_utf8_lossy(&output.stderr).into_owned(),

View File

@ -4,7 +4,7 @@ use yoi_e2e::{FixtureWorkspace, KeyPress, PanelHarness, yoi_binary};
#[test] #[test]
fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result<()> { 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 fixture = FixtureWorkspace::new(&binary)?;
let mut panel = PanelHarness::spawn(fixture.panel_config(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] #[test]
fn panel_ctrl_c_exits_promptly_after_background_barrier() -> yoi_e2e::Result<()> { 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 fixture = FixtureWorkspace::new(&binary)?;
let mut panel = let mut panel =
PanelHarness::spawn(fixture.panel_config_holding_background_task(binary, "reload"))?; PanelHarness::spawn(fixture.panel_config_holding_background_task(binary, "reload"))?;