test: measure panel shell startup path
This commit is contained in:
parent
7593202492
commit
caf18dbaab
0
.yoi/tickets/00001KVDQH839/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVDQH839/artifacts/.gitkeep
Normal file
13
.yoi/tickets/00001KVDQH839/artifacts/relations.json
Normal file
13
.yoi/tickets/00001KVDQH839/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVDQH839",
|
||||
"kind": "related",
|
||||
"target": "00001KVDETSN6",
|
||||
"note": "Adds shell-enter launch-path coverage on top of dashboard content-ready metric.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-18T16:03:59Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
57
.yoi/tickets/00001KVDQH839/item.md
Normal file
57
.yoi/tickets/00001KVDQH839/item.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
title: 'Panel E2E に shell Enter 起動経路の dashboard readiness 計測を追加する'
|
||||
state: 'done'
|
||||
created_at: '2026-06-18T16:02:56Z'
|
||||
updated_at: '2026-06-18T16:03:59Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['panel', 'e2e', 'startup-latency', 'shell-launch', 'dashboard-content-ready']
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
既存の Panel dashboard readiness E2E は direct `Command::spawn(yoi panel ...)` から dashboard content ready までを測っていた。ユーザー目線の「Enter した瞬間から実コンテンツが表示されるまで」に近づけるため、isolated fixture は維持しつつ、shell 上で command line を投入して Enter された起動経路を部分的に模した E2E を追加する。
|
||||
|
||||
この Ticket は live workspace の遅延改善そのものではなく、E2E の起動経路を実利用に近づける追加計測である。
|
||||
|
||||
## Requirements
|
||||
|
||||
- PTY 上で `/bin/sh` を起動し、`exec <yoi> panel ...` を command line として送って Enter 相当から測定する。
|
||||
- 測定開始点は command line を PTY に送る直前とする。
|
||||
- dashboard readiness は既存の `dashboard_content_ready` snapshot matcher を使う。
|
||||
- first frame だけ、単一 row だけでは通さない。
|
||||
- Isolated fixture / isolated HOME / XDG dirs / runtime dirs は維持する。
|
||||
- `YOI_POD_RUNTIME_COMMAND` は tested binary を明示して渡す。
|
||||
- Direct spawn E2E は残し、shell-enter path は追加 coverage とする。
|
||||
|
||||
## Implementation summary
|
||||
|
||||
- `PanelHarness::spawn_via_shell_enter` を追加した。
|
||||
- `/bin/sh` を PTY 上で起動。
|
||||
- `exec '<binary>' '<args>'...` を送信。
|
||||
- command line送信直前の `Instant` を返す。
|
||||
- artifacts `run.json` に `launch_mode: shell_enter_exec` を記録。
|
||||
- shell quote helper を追加した。
|
||||
- `panel_dashboard_content_ready_from_shell_enter_path` E2E を追加した。
|
||||
- Enter相当から first frame / dashboard content ready を測定。
|
||||
- expected dashboard snapshot / source breakdown を検証。
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p yoi-e2e --features e2e --test panel panel_dashboard_content_ready_from_shell_enter_path -- --nocapture`
|
||||
- observed: dashboard content ready 約 `220ms`, first frame 約 `20ms` in isolated fixture。
|
||||
- `cargo test -p yoi-e2e --features e2e --test panel`
|
||||
- `cargo check -p yoi-e2e -p yoi -p tui --features tui/e2e-test`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Live workspace startup latency の改善。
|
||||
- Interactive shell の command lookup / user typing latency の完全再現。
|
||||
- User's actual shell rc/profile を読むこと。
|
||||
- Panel architecture / lifecycle の変更。
|
||||
|
||||
## Related work
|
||||
|
||||
- `00001KVDETSN6` — Panel startup latency をユーザー目線の dashboard content ready 基準で計測・改善する。
|
||||
7
.yoi/tickets/00001KVDQH839/thread.md
Normal file
7
.yoi/tickets/00001KVDQH839/thread.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-18T16:02:56Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
|
@ -606,6 +606,7 @@ impl PanelHarness {
|
|||
"xdg_config_home": config.xdg_config_home,
|
||||
"xdg_runtime_dir": config.xdg_runtime_dir,
|
||||
"fixture_root": config.fixture_root,
|
||||
"launch_mode": "direct_exec",
|
||||
"runtime_policy": {
|
||||
"host_runtime_inherited": false,
|
||||
"host_xdg_runtime_dir_present": std::env::var_os("XDG_RUNTIME_DIR").is_some(),
|
||||
|
|
@ -686,6 +687,122 @@ impl PanelHarness {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn spawn_via_shell_enter(config: PanelHarnessConfig) -> Result<(Self, Instant)> {
|
||||
if !config.binary.exists() {
|
||||
return Err(HarnessError::MissingBinary(config.binary));
|
||||
}
|
||||
fs::create_dir_all(&config.artifacts_dir)?;
|
||||
let artifacts = PanelArtifacts {
|
||||
dir: config.artifacts_dir.clone(),
|
||||
events_jsonl: config.artifacts_dir.join("events.jsonl"),
|
||||
input_log: config.artifacts_dir.join("input.log"),
|
||||
output_log: config.artifacts_dir.join("pty-output.log"),
|
||||
run_json: config.artifacts_dir.join("run.json"),
|
||||
};
|
||||
fs::write(&artifacts.events_jsonl, "")?;
|
||||
fs::write(&artifacts.input_log, "")?;
|
||||
fs::write(&artifacts.output_log, "")?;
|
||||
let env_policy =
|
||||
tui_env_policy(config.hold_background_task.is_some(), config.rewind_fixture);
|
||||
fs::write(
|
||||
&artifacts.run_json,
|
||||
serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"binary": config.binary,
|
||||
"args": &config.command_args,
|
||||
"workspace": config.workspace,
|
||||
"home": config.home,
|
||||
"xdg_data_home": config.xdg_data_home,
|
||||
"xdg_state_home": config.xdg_state_home,
|
||||
"xdg_config_home": config.xdg_config_home,
|
||||
"xdg_runtime_dir": config.xdg_runtime_dir,
|
||||
"fixture_root": config.fixture_root,
|
||||
"launch_mode": "shell_enter_exec",
|
||||
"runtime_policy": {
|
||||
"host_runtime_inherited": false,
|
||||
"host_xdg_runtime_dir_present": std::env::var_os("XDG_RUNTIME_DIR").is_some(),
|
||||
"tested_yoi_runtime_source": "fixture XDG_RUNTIME_DIR"
|
||||
},
|
||||
"terminal_size": {
|
||||
"columns": config.terminal_size.0,
|
||||
"rows": config.terminal_size.1,
|
||||
},
|
||||
"hold_background_task": config.hold_background_task,
|
||||
"rewind_fixture": config.rewind_fixture,
|
||||
"tested_yoi_env_policy": &env_policy,
|
||||
}))?,
|
||||
)?;
|
||||
|
||||
let (master, slave) = open_pty(config.terminal_size)?;
|
||||
let slave_for_stdin = slave.try_clone()?;
|
||||
let slave_for_stdout = slave.try_clone()?;
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command
|
||||
.current_dir(&config.workspace)
|
||||
.env_clear()
|
||||
.env("YOI_TUI_TEST_EVENTS", &artifacts.events_jsonl)
|
||||
.env("YOI_POD_RUNTIME_COMMAND", &config.binary)
|
||||
.env("HOME", &config.home)
|
||||
.env("XDG_DATA_HOME", &config.xdg_data_home)
|
||||
.env("XDG_STATE_HOME", &config.xdg_state_home)
|
||||
.env("XDG_CONFIG_HOME", &config.xdg_config_home)
|
||||
.env("XDG_RUNTIME_DIR", &config.xdg_runtime_dir)
|
||||
.env("TERM", "xterm-256color")
|
||||
.stdin(Stdio::from(slave_for_stdin))
|
||||
.stdout(Stdio::from(slave_for_stdout))
|
||||
.stderr(Stdio::from(slave));
|
||||
if let Some(task) = &config.hold_background_task {
|
||||
command.env("YOI_TUI_TEST_HOLD_BACKGROUND_TASK", task);
|
||||
}
|
||||
if config.rewind_fixture {
|
||||
command.env("YOI_TUI_TEST_REWIND_FIXTURE", "1");
|
||||
}
|
||||
let child = command.spawn()?;
|
||||
|
||||
let output = Arc::new(Mutex::new(Vec::new()));
|
||||
let output_for_thread = Arc::clone(&output);
|
||||
let mut reader_file = master.try_clone()?;
|
||||
let output_log = artifacts.output_log.clone();
|
||||
let reader = thread::spawn(move || {
|
||||
let mut sink = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(output_log)
|
||||
.ok();
|
||||
let mut buf = [0_u8; 4096];
|
||||
loop {
|
||||
match reader_file.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
if let Some(sink) = sink.as_mut() {
|
||||
let _ = sink.write_all(&buf[..n]);
|
||||
}
|
||||
if let Ok(mut output) = output_for_thread.lock() {
|
||||
output.extend_from_slice(&buf[..n]);
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut harness = Self {
|
||||
child,
|
||||
master,
|
||||
reader: Some(reader),
|
||||
output,
|
||||
last_event_offset: 0,
|
||||
artifacts,
|
||||
};
|
||||
let command_line = shell_exec_command(&config.binary, &config.command_args);
|
||||
let started = Instant::now();
|
||||
harness.write_input(
|
||||
"shell Enter yoi panel",
|
||||
format!("{command_line}\n").as_bytes(),
|
||||
)?;
|
||||
Ok((harness, started))
|
||||
}
|
||||
|
||||
pub fn wait_for<F>(
|
||||
&mut self,
|
||||
what: impl Into<String>,
|
||||
|
|
@ -1665,6 +1782,30 @@ fn command_display(program: &Path, args: &[String]) -> String {
|
|||
.join(" ")
|
||||
}
|
||||
|
||||
fn shell_exec_command(program: &Path, args: &[String]) -> String {
|
||||
std::iter::once("exec".to_string())
|
||||
.chain(std::iter::once(shell_quote(&program.to_string_lossy())))
|
||||
.chain(args.iter().map(|arg| shell_quote(arg)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn shell_quote(value: &str) -> String {
|
||||
if value.is_empty() {
|
||||
return "''".to_string();
|
||||
}
|
||||
let mut quoted = String::from("'");
|
||||
for ch in value.chars() {
|
||||
if ch == '\'' {
|
||||
quoted.push_str("'\\''");
|
||||
} else {
|
||||
quoted.push(ch);
|
||||
}
|
||||
}
|
||||
quoted.push('\'');
|
||||
quoted
|
||||
}
|
||||
|
||||
fn open_pty(size: (u16, u16)) -> Result<(File, File)> {
|
||||
let mut master = 0;
|
||||
let mut slave = 0;
|
||||
|
|
|
|||
|
|
@ -372,6 +372,58 @@ fn panel_dashboard_content_ready_has_startup_budget() -> yoi_e2e::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_dashboard_content_ready_from_shell_enter_path() -> yoi_e2e::Result<()> {
|
||||
let binary = yoi_binary()?;
|
||||
let fixture = FixtureWorkspace::new(&binary)?;
|
||||
assert_fixture_paths_are_isolated(&fixture);
|
||||
let expected_content = fixture.expected_dashboard_content();
|
||||
|
||||
let (mut panel, started) = PanelHarness::spawn_via_shell_enter(fixture.panel_config(binary))?;
|
||||
let first_visible_remaining = FIRST_VISIBLE_RENDER_BUDGET
|
||||
.checked_sub(started.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_millis(0));
|
||||
panel.wait_for_first_visible_frame(first_visible_remaining)?;
|
||||
let first_visible_elapsed = started.elapsed();
|
||||
|
||||
let content_ready_remaining = DASHBOARD_CONTENT_READY_BUDGET
|
||||
.checked_sub(started.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_millis(0));
|
||||
let content_ready =
|
||||
panel.wait_for_dashboard_content_ready(&expected_content, content_ready_remaining)?;
|
||||
assert_eq!(
|
||||
content_ready.snapshot_for_expected(&expected_content),
|
||||
expected_content.snapshot(),
|
||||
"shell-enter dashboard content ready must match expected Ticket/action/overlay/header snapshot; artifacts at {}",
|
||||
panel.artifacts().dir.display()
|
||||
);
|
||||
let content_ready_elapsed = started.elapsed();
|
||||
eprintln!(
|
||||
"panel shell-enter dashboard content ready: {content_ready_elapsed:?} (budget {DASHBOARD_CONTENT_READY_BUDGET:?}; first frame {first_visible_elapsed:?}); artifacts at {}",
|
||||
panel.artifacts().dir.display()
|
||||
);
|
||||
assert!(
|
||||
content_ready_elapsed <= DASHBOARD_CONTENT_READY_BUDGET,
|
||||
"shell-enter dashboard content ready took {content_ready_elapsed:?}, budget {DASHBOARD_CONTENT_READY_BUDGET:?}; artifacts at {}",
|
||||
panel.artifacts().dir.display()
|
||||
);
|
||||
|
||||
let source_breakdown = panel.expect_dashboard_source_breakdown()?;
|
||||
assert!(
|
||||
source_breakdown.has_source("workspace_panel.build.total"),
|
||||
"shell-enter dashboard source breakdown should include total panel build source; got {:?}; artifacts at {}",
|
||||
source_breakdown,
|
||||
panel.artifacts().dir.display()
|
||||
);
|
||||
|
||||
panel.press(KeyPress::CtrlC)?;
|
||||
let status = panel.expect_exit_within(PanelHarness::default_exit_wait())?;
|
||||
assert!(status.success(), "panel should exit cleanly with Ctrl+C");
|
||||
drop(panel);
|
||||
assert_fixture_cleanup(fixture.cleanup()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result<()> {
|
||||
let binary = yoi_binary()?;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user