Skip to content
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: use stty to widen the effective terminal for Linux guests #818

Merged
merged 1 commit into from
Nov 28, 2024
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
1 change: 1 addition & 0 deletions phd-tests/framework/src/guest_os/alpine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ impl GuestOs for Alpine {
CommandSequenceEntry::write_str("root"),
CommandSequenceEntry::wait_for(self.get_shell_prompt()),
])
.extend(super::linux::stty_enable_long_lines(self))
}

fn get_shell_prompt(&self) -> &'static str {
Expand Down
1 change: 1 addition & 0 deletions phd-tests/framework/src/guest_os/debian11_nocloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ impl GuestOs for Debian11NoCloud {
CommandSequenceEntry::write_str("root"),
CommandSequenceEntry::wait_for(self.get_shell_prompt()),
])
.extend(super::linux::stty_enable_long_lines(self))
}

fn get_shell_prompt(&self) -> &'static str {
Expand Down
18 changes: 18 additions & 0 deletions phd-tests/framework/src/guest_os/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Helper functions for building guest OS adaptations for Linux OSes.

use super::{CommandSequence, CommandSequenceEntry, GuestOs};

/// Yields an `stty` command that tells the guest terminal to behave as though
/// it is 9,999 columns wide.
pub(super) fn stty_enable_long_lines<'a>(
guest_os: &impl GuestOs,
) -> CommandSequence<'a> {
CommandSequence(vec![
CommandSequenceEntry::write_str("stty -F `tty` cols 9999"),
CommandSequenceEntry::wait_for(guest_os.get_shell_prompt()),
])
}
8 changes: 8 additions & 0 deletions phd-tests/framework/src/guest_os/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};

mod alpine;
mod debian11_nocloud;
mod linux;
mod shell_commands;
mod ubuntu22_04;
mod windows;
Expand Down Expand Up @@ -60,6 +61,13 @@ impl<'a> CommandSequenceEntry<'a> {

pub(super) struct CommandSequence<'a>(pub Vec<CommandSequenceEntry<'a>>);

impl<'a> CommandSequence<'a> {
fn extend(mut self, other: CommandSequence<'a>) -> CommandSequence<'a> {
self.0.extend(other.0);
self
}
}

pub(super) trait GuestOs: Send + Sync {
/// Retrieves the command sequence used to wait for the OS to boot and log
/// into it.
Expand Down
1 change: 1 addition & 0 deletions phd-tests/framework/src/guest_os/ubuntu22_04.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ impl GuestOs for Ubuntu2204 {
CommandSequenceEntry::write_str("1!Passw0rd"),
CommandSequenceEntry::wait_for(self.get_shell_prompt()),
])
.extend(super::linux::stty_enable_long_lines(self))
}

fn get_shell_prompt(&self) -> &'static str {
Expand Down
11 changes: 11 additions & 0 deletions phd-tests/framework/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use camino::Utf8PathBuf;

use disk::DiskFactory;
use futures::{stream::FuturesUnordered, StreamExt};
use guest_os::GuestOsKind;
use port_allocator::PortAllocator;
use server_log_mode::ServerLogMode;
pub use test_vm::TestVm;
Expand Down Expand Up @@ -304,6 +305,16 @@ impl Framework {
&self.default_guest_os_artifact
}

/// Yields the guest OS adapter corresponding to the default guest OS
/// artifact.
pub async fn default_guest_os_kind(&self) -> anyhow::Result<GuestOsKind> {
Ok(self
.artifact_store
.get_guest_os_image(&self.default_guest_os_artifact)
.await?
.1)
}

/// Indicates whether the disk factory in this framework supports the
/// creation of Crucible disks. This can be used to skip tests that require
/// Crucible support.
Expand Down
2 changes: 1 addition & 1 deletion phd-tests/testcase_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub fn phd_skip(args: TokenStream) -> TokenStream {
let args = if args.is_empty() {
None
} else {
let lit = parse_macro_input!(args as syn::Lit);
let lit = parse_macro_input!(args as proc_macro2::TokenStream);
Some(lit)
};

Expand Down
36 changes: 3 additions & 33 deletions phd-tests/tests/src/boot_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use anyhow::{bail, Error};
use anyhow::bail;
use phd_framework::{
disk::{fat::FatFilesystem, DiskSource},
test_vm::{DiskBackend, DiskInterface},
Expand All @@ -20,37 +20,6 @@ use efi_utils::{
EDK2_FIRMWARE_VOL_GUID, EDK2_UI_APP_GUID,
};

pub(crate) async fn run_long_command(
vm: &phd_framework::TestVm,
cmd: &str,
) -> Result<String, Error> {
// Ok, this is a bit whacky: something about the line wrapping for long
// commands causes `run_shell_command` to hang instead of ever actually
// seeing a response prompt.
//
// I haven't gone and debugged that; instead, chunk the input command up
// into segments short enough to not wrap when input, put them all in a
// file, then run the file.
vm.run_shell_command("rm cmd").await?;
let mut offset = 0;
// Escape any internal `\`. This isn't comprehensive escaping (doesn't
// handle \n, for example)..
let cmd = cmd.replace("\\", "\\\\");
while offset < cmd.len() {
let lim = std::cmp::min(cmd.len() - offset, 50);
let chunk = &cmd[offset..][..lim];
offset += lim;

// Catch this before it causes weird issues in half-executed commands.
//
// Could escape these here, but right now that's not really necessary.
assert!(!chunk.contains("\n"));

vm.run_shell_command(&format!("echo -n \'{}\' >>cmd", chunk)).await?;
}
vm.run_shell_command(". cmd").await
}

// This test checks that with a specified boot order, the guest boots whichever
// disk we wanted to come first. This is simple enough, until you want to know
// "what you booted from"..
Expand Down Expand Up @@ -305,7 +274,8 @@ async fn guest_can_adjust_boot_order(ctx: &Framework) {

// Try adding a few new boot options, then add them to the boot order,
// reboot, and make sure they're all as we set them.
if !run_long_command(&vm, &format!("ls {}", efipath(&bootvar(0xffff))))
if !vm
.run_shell_command(&format!("ls {}", efipath(&bootvar(0xffff))))
.await?
.is_empty()
{
Expand Down
8 changes: 3 additions & 5 deletions phd-tests/tests/src/boot_order/efi_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ use std::fmt::Write;
use std::io::{Cursor, Read};
use tracing::{info, trace, warn};

use super::run_long_command;

// First, some GUIDs. These GUIDs come from EDK2, and OVMF reuses them. Notably
// these are the raw bytes of the GUID: textual values will have slightly
// different ordering of bytes.
Expand Down Expand Up @@ -326,7 +324,7 @@ pub(crate) async fn read_efivar(
efipath(varname)
);

let hex = run_long_command(vm, &cmd).await?;
let hex = vm.run_shell_command(&cmd).await?;

Ok(unhex(&hex))
}
Expand All @@ -345,7 +343,7 @@ pub(crate) async fn write_efivar(
efipath(varname)
);

let attr_read_bytes = run_long_command(vm, &attr_cmd).await?;
let attr_read_bytes = vm.run_shell_command(&attr_cmd).await?;
let attrs = if attr_read_bytes.ends_with(": No such file or directory") {
// Default attributes if the variable does not exist yet. We expect it
// to be non-volatile because we are writing it, we expect it to be
Expand Down Expand Up @@ -390,7 +388,7 @@ pub(crate) async fn write_efivar(
efipath(varname)
);

let res = run_long_command(vm, &cmd).await?;
let res = vm.run_shell_command(&cmd).await?;
// If something went sideways and the write failed with something like
// `invalid argument`...
if !res.is_empty() {
Expand Down
39 changes: 37 additions & 2 deletions phd-tests/tests/src/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! This module contains tests whose primary goal is to verify the correctness
//! of the PHD framework itself.
//! Tests that primarily exercise the PHD framework itself.

use phd_framework::guest_os::GuestOsKind;
use phd_testcase::*;

#[phd_testcase]
Expand All @@ -16,3 +16,38 @@ async fn multiline_serial_test(ctx: &Framework) {
let out = vm.run_shell_command("echo \\\nhello \\\nworld").await?;
assert_eq!(out, "hello world");
}

#[phd_testcase]
async fn long_line_serial_test(ctx: &Framework) {
let os = ctx.default_guest_os_kind().await?;
if matches!(
os,
GuestOsKind::WindowsServer2016 | GuestOsKind::WindowsServer2019
) {
phd_skip!(format!(
"long serial lines not supported for guest OS {os:?}"
));
}

let mut vm = ctx.spawn_default_vm("long_line_serial_test").await?;
vm.launch().await?;
vm.wait_to_boot().await?;

let long_str = "In my younger and more vulnerable years my father gave \
me some advice that I've been turning over in my mind ever since. \
\"Whenever you feel like sending a long serial console line,\" he told me, \
\"just remember that all the guest OSes in this world haven't had the tty \
settings you've had.\"";
Comment on lines +38 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amazing


let out = vm
.run_shell_command(&format!(
"echo '{}'",
// Fitzgerald didn't have to deal with nested Bash quotes, but this
// test does. Replace apostrophes in the input string with a
// string-terminating `'`, followed by an escaped single quote that
// serves as the apostrophe, followed by a string-opening `'`.
long_str.replace("'", "'\\''")
))
.await?;
assert_eq!(out, long_str);
}
Loading