Skip to content
Merged
Show file tree
Hide file tree
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
18 changes: 9 additions & 9 deletions crates/glass-android/src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! Reports `Check` statuses; never errors.

use glass_core::Deadline;
use glass_core::{Check, CheckStatus};
use glass_core::{BoundedRun, Check, CheckStatus};

use crate::a11y::{Attempt, adb_runner, attempt_deadline, dump_once};
use crate::adb::Adb;
Expand Down Expand Up @@ -254,14 +254,14 @@ fn first_line(s: &str) -> String {
fn list_avds(bin: &str, budget: std::time::Duration) -> AvdList {
let mut cmd = std::process::Command::new(bin);
cmd.arg("-list-avds");
match glass_core::run_bounded(&mut cmd, budget, "emulator:-list-avds") {
Ok(o) => AvdList::Listed(parse_list_avds(&String::from_utf8_lossy(&o.stdout))),
Err(e) => {
// A timeout is reported, not logged (the detail carries the budget); every other
// failure is logged the way the other doctor probes are.
if e.bound() == Some(glass_core::BoundKind::TimedOut) {
return AvdList::TimedOut(e.to_string());
}
match glass_core::run_bounded_classified(&mut cmd, budget, "emulator:-list-avds") {
BoundedRun::Answered(o) => {
AvdList::Listed(parse_list_avds(&String::from_utf8_lossy(&o.stdout)))
}
// A timeout is reported, not logged (the detail carries the budget); every other
// failure is logged the way the other doctor probes are.
BoundedRun::TimedOut(e) => AvdList::TimedOut(e.to_string()),
BoundedRun::NotStarted(e) | BoundedRun::Failed(e) => {
eprintln!("glass-android doctor: {e}");
AvdList::Unreadable(e.to_string())
}
Expand Down
84 changes: 84 additions & 0 deletions crates/glass-core/src/bounded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ pub const KILL_REAP: Duration = Duration::from_millis(500);
/// AVD, a `uiautomator dump` is ~12KB and the largest, `exec-out screencap`, is ~10MB.
const MAX_CAPTURE: usize = 64 * 1024 * 1024;

/// The execution facts from one bounded command, before a caller assigns domain meaning.
#[derive(Debug)]
pub enum BoundedRun {
/// The command ran to completion. A non-zero status is still an answer.
Answered(Output),
/// The command ran until its effective bound elapsed.
TimedOut(GlassError),
/// The command was not spawned because no time remained.
NotStarted(GlassError),
/// The command did not produce a complete answer for another reason.
Failed(GlassError),
}

/// Run one command under `budget` and retain its execution facts as structured values.
pub fn run_bounded_classified(cmd: &mut Command, budget: Duration, op: &str) -> BoundedRun {
match run_bounded(cmd, budget, op) {
Ok(output) => BoundedRun::Answered(output),
Err(error) => match error.bound() {
Some(BoundKind::TimedOut) => BoundedRun::TimedOut(error),
Some(BoundKind::NotStarted) => BoundedRun::NotStarted(error),
None => BoundedRun::Failed(error),
},
}
}

/// Run `cmd` to completion, or kill it and fail once `budget` elapses.
///
/// `op` names the operation in the error (`"adb:uiautomator dump"`), so a timeout says which call
Expand Down Expand Up @@ -487,6 +512,65 @@ mod tests {
assert_eq!(out.status.code(), Some(3));
}

#[test]
#[cfg(unix)]
fn a_classified_run_preserves_a_nonzero_answer() {
let run = run_bounded_classified(
Command::new("/bin/sh").args(["-c", "printf answered; exit 3"]),
Duration::from_secs(10),
"test:classified-answer",
);
let BoundedRun::Answered(out) = run else {
panic!("a completed command is an answer");
};
assert_eq!(out.status.code(), Some(3));
assert_eq!(String::from_utf8_lossy(&out.stdout), "answered");
}

#[test]
#[cfg(unix)]
fn a_classified_run_names_a_timeout_without_message_matching() {
let run = run_bounded_classified(
Command::new("/bin/sh").args(["-c", "sleep 30"]),
Duration::from_millis(100),
"test:classified-timeout",
);
let BoundedRun::TimedOut(err) = run else {
panic!("a command killed at its bound is timed out");
};
assert_eq!(err.bound(), Some(BoundKind::TimedOut));
assert!(err.to_string().contains("test:classified-timeout"), "{err}");
}

#[test]
#[cfg(unix)]
fn a_classified_run_says_when_it_never_started() {
let run = run_bounded_classified(
&mut Command::new("/bin/true"),
Duration::ZERO,
"test:classified-not-started",
);
let BoundedRun::NotStarted(err) = run else {
panic!("a zero-budget command is not started");
};
assert_eq!(err.bound(), Some(BoundKind::NotStarted));
}

#[test]
#[cfg(unix)]
fn a_classified_run_keeps_an_ordinary_failure_distinct() {
let run = run_bounded_classified(
&mut Command::new("/path/glass-test-command-does-not-exist"),
Duration::from_secs(10),
"test:classified-failure",
);
let BoundedRun::Failed(err) = run else {
panic!("a spawn refusal is an ordinary execution failure");
};
assert_eq!(err.bound(), None);
assert!(err.to_string().contains("failed to start"), "{err}");
}

#[test]
#[cfg(unix)]
fn stderr_is_captured_alongside_stdout() {
Expand Down
5 changes: 4 additions & 1 deletion crates/glass-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ pub mod toolpath;
pub use toolpath::tool_path;

pub mod bounded;
pub use bounded::{note_if_skipped, run_bounded, run_bounded_until, run_bounded_with_stdin};
pub use bounded::{
BoundedRun, note_if_skipped, run_bounded, run_bounded_classified, run_bounded_until,
run_bounded_with_stdin,
};

pub mod a11y_thread;
pub use a11y_thread::A11yThread;
Expand Down
19 changes: 10 additions & 9 deletions crates/glass-ios/src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};

use glass_core::{BoundKind, Check, CheckStatus, GlassError};
use glass_core::{BoundedRun, Check, CheckStatus, GlassError};
use glass_exec_unix::Resolved;

use crate::device::{Resolve, SimDevice, parse_devices, resolve};
Expand Down Expand Up @@ -280,20 +280,21 @@ enum Run {

/// One bounded one-shot probe; the cause is kept on the result and logged to stderr.
fn run_probe(cmd: &mut Command, op: &str) -> Run {
match glass_core::run_bounded(cmd, PROBE_BUDGET, op) {
Ok(o) => Run::Answered {
match glass_core::run_bounded_classified(cmd, PROBE_BUDGET, op) {
BoundedRun::Answered(o) => Run::Answered {
status_ok: o.status.success(),
stdout: String::from_utf8_lossy(&o.stdout).into_owned(),
stderr: String::from_utf8_lossy(&o.stderr).into_owned(),
},
Err(e) => {
BoundedRun::TimedOut(e) => {
let cause = e.to_string();
eprintln!("glass-ios doctor: {cause}");
if e.bound() == Some(BoundKind::TimedOut) {
Run::TimedOut(cause)
} else {
Run::Failed(cause)
}
Run::TimedOut(cause)
}
BoundedRun::NotStarted(e) | BoundedRun::Failed(e) => {
let cause = e.to_string();
eprintln!("glass-ios doctor: {cause}");
Run::Failed(cause)
}
}
}
Expand Down