-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
phd: check exit status of commands, have tests expect success/failure #797
base: master
Are you sure you want to change the base?
Changes from 1 commit
3091070
7f022e1
2b5fa0a
6c725de
507929c
35b1ac9
3c5f16c
a8312eb
324bb52
db4e1eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -142,6 +142,51 @@ enum VmState { | |
Ensured { serial: SerialConsole }, | ||
} | ||
|
||
/// Both the output and status of a command. | ||
pub struct ShellOutput { | ||
status: u16, | ||
output: String, | ||
} | ||
|
||
impl ShellOutput { | ||
/// Consume this [`ShellOutput`], returning the command's output as text | ||
/// if the command completed successfully, or an error if it did not. | ||
pub fn expect_ok(self) -> Result<String> { | ||
if self.status == 0 { | ||
Ok(self.output) | ||
} else { | ||
Err(anyhow!("command exited with non-zero status: {}", self.status)) | ||
} | ||
} | ||
|
||
/// Consume this [`ShellOutput`], returning the command's output as text | ||
/// if the command exited with non-zero status, or an error if it exited | ||
/// with status zero. | ||
pub fn expect_err(self) -> Result<String> { | ||
if self.status != 0 { | ||
Ok(self.output) | ||
} else { | ||
Err(anyhow!("command unexpectedly succeeded")) | ||
} | ||
} | ||
|
||
pub fn ignore_status(self) -> String { | ||
iximeow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.output | ||
} | ||
|
||
pub fn status(&self) -> u16 { | ||
self.status | ||
} | ||
|
||
/// Get the textual output of the command. You probably should use | ||
/// [`ShellOutput::expect_ok`] or [`ShellOutput::expect_err`] to ensure | ||
/// the command was processed as expected before asserting on its | ||
/// output. | ||
pub fn output(&self) -> &str { | ||
self.output.as_str() | ||
} | ||
} | ||
|
||
/// A virtual machine running in a Propolis server. Test cases create these VMs | ||
/// using the `factory::VmFactory` embedded in their test contexts. | ||
/// | ||
|
@@ -868,7 +913,21 @@ impl TestVm { | |
/// waits for another shell prompt to appear using | ||
/// [`Self::wait_for_serial_output`] and returns any text that was buffered | ||
/// to the serial console after the command was sent. | ||
pub async fn run_shell_command(&self, cmd: &str) -> Result<String> { | ||
/// | ||
/// After running the shell command, sends `echo $?` to query and return the | ||
/// command's return status as well. | ||
// TO REVIEWERS: it would be really nice to write this as a function | ||
// returning a `struct ShellCommandExecutor` that impls | ||
// `Future<Output=String>` where the underlying ShellOutput is automatically | ||
// `.expect_ok()`'d. In such a case it would be possible for the struct to | ||
// have an `.expect_err()` that replaces teh default `.expect_ok()` | ||
// behavior, so that the likely case doesn't need any change in PHD tests. | ||
// | ||
// unfortunately I don't know how to plumb the futures for that, since we'd | ||
// have to close over `&self`, so doing any Boxing to hold an | ||
// `async move {}` immediately causes issues. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my thinking is that it'd be nice if struct ShellCommandExecutor {
fut: Pin<Box<impl Future<Output=Result<ShellOutput>> + Send + Sync>>
}
impl ShellCommandExecutor {
fn expect_err(self) -> ShellCommandExecutorExpectErr { /* build the thing */ }
}
impl Future for ShellCommandExecutor {
fn poll(&self, cx: &mut Context) -> Self::Output {
match self.fut.poll(cx) {
Poll::Ready(v) => Poll::Ready(v.expect_ok()),
Poll::Pending => Poll::Pending
}
}
} but i couldn't figure out how to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels like it's what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok! now that i got the more Future-ful approach together i think this all is much cleaner (and incidentally resolves your other comments). it's kinda sickrad that this only comes with like 10 changes in tests now, since it's going along with the general assumption that commands run in guests should be successful. |
||
#[must_use] | ||
pub async fn run_shell_command(&self, cmd: &str) -> Result<ShellOutput> { | ||
// Allow the guest OS to transform the input command into a | ||
// guest-specific command sequence. This accounts for the guest's shell | ||
// type (which affects e.g. affects how it displays multi-line commands) | ||
|
@@ -881,7 +940,18 @@ impl TestVm { | |
// before actually issuing the final '\n' that issues the command. | ||
// This ensures that the buffer contents returned by this call contain | ||
// only the command's output. | ||
let out = self | ||
let output = self | ||
.wait_for_serial_output( | ||
self.guest_os.get_shell_prompt(), | ||
Duration::from_secs(300), | ||
) | ||
.await?; | ||
|
||
let status_command_sequence = | ||
self.guest_os.shell_command_sequence("echo $?"); | ||
self.run_command_sequence(status_command_sequence).await?; | ||
|
||
let status = self | ||
.wait_for_serial_output( | ||
self.guest_os.get_shell_prompt(), | ||
Duration::from_secs(300), | ||
|
@@ -891,7 +961,10 @@ impl TestVm { | |
// Trim any leading newlines inserted when the command was issued and | ||
// any trailing whitespace that isn't actually part of the command | ||
// output. Any other embedded whitespace is the caller's problem. | ||
Ok(out.trim().to_string()) | ||
let output = output.trim().to_string(); | ||
let status = status.trim().parse::<u16>()?; | ||
|
||
Ok(ShellOutput { status, output }) | ||
} | ||
|
||
pub async fn graceful_reboot(&self) -> Result<()> { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,7 @@ async fn guest_reboot_test(ctx: &Framework) { | |
vm.launch().await?; | ||
vm.wait_to_boot().await?; | ||
|
||
// XXX: use graceful_reboot() now. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (got a followup patch for this) |
||
// Don't use `run_shell_command` because the guest won't echo another prompt | ||
// after this. | ||
vm.send_serial_str("reboot\n").await?; | ||
|
@@ -64,10 +65,11 @@ async fn shutdown_persistence_test(ctx: &Framework) { | |
|
||
// Verify that the test file doesn't exist yet, then touch it, flush it, and | ||
// shut down the VM. | ||
let lsout = vm.run_shell_command("ls foo.bar 2> /dev/null").await?; | ||
let lsout = | ||
vm.run_shell_command("ls foo.bar 2> /dev/null").await?.expect_err()?; | ||
assert_eq!(lsout, ""); | ||
vm.run_shell_command("touch ./foo.bar").await?; | ||
vm.run_shell_command("sync ./foo.bar").await?; | ||
vm.run_shell_command("touch ./foo.bar").await?.expect_ok()?; | ||
vm.run_shell_command("sync ./foo.bar").await?.expect_ok()?; | ||
vm.stop().await?; | ||
vm.wait_for_state(InstanceState::Destroyed, Duration::from_secs(60)) | ||
.await?; | ||
|
@@ -82,6 +84,7 @@ async fn shutdown_persistence_test(ctx: &Framework) { | |
vm.wait_to_boot().await?; | ||
|
||
// The touched file from the previous VM should be present in the new one. | ||
let lsout = vm.run_shell_command("ls foo.bar 2> /dev/null").await?; | ||
let lsout = | ||
vm.run_shell_command("ls foo.bar 2> /dev/null").await?.expect_ok()?; | ||
assert_eq!(lsout, "foo.bar"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's a bit weird to me that
expect_ok()
returns aResult
instead of panicking --- i would generally expect (haha) a function namedexpect_foo
to panic if the expectation is violated. Perhaps these would make more sense as just.ok()
/.err()
? I dunno. Maybe this is clearer to others...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hum yeah that seems pretty reasonable. this naming probably made a little more sense in the first draft where i was hoping this would configure a future to expect the result is ok rather than this
goofy-Result-to-Result
transformation. i'll see if i can get something nice withIntoFuture
and if not i'lls/expect_/g