mirror of
https://chromium.googlesource.com/crosvm/crosvm
synced 2025-02-05 18:20:34 +00:00
3f63661afc
Instead of instanciating crosvm directly, we can start the binary as a sub-process. This includes parsing of crosvm options in the tests, and makes the test cases closer to real-world usage. To make make this possible, we need to make sure that the crosvm binary is uploaded to the VM before running the test, which is done by the sync_so script, which is baked into the builder container. We prevent future container re-builds for just maintaining the script, I have removed them from the container, and call the scripts from the local source directly. The test runner is also updated to ensure all package binaries are built (currently only tests are built). BUG=b:182841358 TEST=./test_all passes Change-Id: I7dfd21abcb2b90fe125eb43f85572fbf645b888a Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/2744280 Tested-by: Dennis Kempin <denniskempin@google.com> Commit-Queue: Dennis Kempin <denniskempin@google.com> Reviewed-by: Daniel Verkamp <dverkamp@chromium.org>
335 lines
11 KiB
Rust
335 lines
11 KiB
Rust
// Copyright 2020 The Chromium OS Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
use std::ffi::CString;
|
|
use std::io::{self, BufRead, BufReader, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
use std::sync::mpsc::sync_channel;
|
|
use std::sync::Once;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
use std::{env, process::Child};
|
|
use std::{fs::File, process::Stdio};
|
|
|
|
use anyhow::{anyhow, Result};
|
|
use base::syslog;
|
|
use tempfile::TempDir;
|
|
|
|
const PREBUILT_URL: &str = "https://storage.googleapis.com/chromeos-localmirror/distfiles";
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
const ARCH: &str = "x86_64";
|
|
#[cfg(target_arch = "arm")]
|
|
const ARCH: &str = "arm";
|
|
#[cfg(target_arch = "aarch64")]
|
|
const ARCH: &str = "aarch64";
|
|
|
|
/// Timeout for communicating with the VM. If we do not hear back, panic so we
|
|
/// do not block the tests.
|
|
const VM_COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(10);
|
|
|
|
fn prebuilt_version() -> &'static str {
|
|
include_str!("../guest_under_test/PREBUILT_VERSION").trim()
|
|
}
|
|
|
|
fn kernel_prebuilt_url() -> String {
|
|
format!(
|
|
"{}/crosvm-testing-bzimage-{}-{}",
|
|
PREBUILT_URL,
|
|
ARCH,
|
|
prebuilt_version()
|
|
)
|
|
}
|
|
|
|
fn rootfs_prebuilt_url() -> String {
|
|
format!(
|
|
"{}/crosvm-testing-rootfs-{}-{}",
|
|
PREBUILT_URL,
|
|
ARCH,
|
|
prebuilt_version()
|
|
)
|
|
}
|
|
|
|
/// The kernel bzImage is stored next to the test executable, unless overridden by
|
|
/// CROSVM_CARGO_TEST_KERNEL_BINARY
|
|
fn kernel_path() -> PathBuf {
|
|
match env::var("CROSVM_CARGO_TEST_KERNEL_BINARY") {
|
|
Ok(value) => PathBuf::from(value),
|
|
Err(_) => env::current_exe()
|
|
.unwrap()
|
|
.parent()
|
|
.unwrap()
|
|
.join("bzImage"),
|
|
}
|
|
}
|
|
|
|
/// The rootfs image is stored next to the test executable, unless overridden by
|
|
/// CROSVM_CARGO_TEST_ROOTFS_IMAGE
|
|
fn rootfs_path() -> PathBuf {
|
|
match env::var("CROSVM_CARGO_TEST_ROOTFS_IMAGE") {
|
|
Ok(value) => PathBuf::from(value),
|
|
Err(_) => env::current_exe().unwrap().parent().unwrap().join("rootfs"),
|
|
}
|
|
}
|
|
|
|
/// The crosvm binary is expected to be alongside to the integration tests
|
|
/// binary. Alternatively in the parent directory (cargo will put the
|
|
/// test binary in target/debug/deps/ but the crosvm binary in target/debug).
|
|
fn find_crosvm_binary() -> PathBuf {
|
|
let exe_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
|
let first = exe_dir.join("crosvm");
|
|
if first.exists() {
|
|
return first;
|
|
}
|
|
let second = exe_dir.parent().unwrap().join("crosvm");
|
|
if second.exists() {
|
|
return second;
|
|
}
|
|
panic!("Cannot find ./crosvm or ../crosvm alongside test binary.");
|
|
}
|
|
|
|
/// Safe wrapper for libc::mkfifo
|
|
fn mkfifo(path: &Path) -> io::Result<()> {
|
|
let cpath = CString::new(path.to_str().unwrap()).unwrap();
|
|
let result = unsafe { libc::mkfifo(cpath.as_ptr(), 0o777) };
|
|
if result == 0 {
|
|
Ok(())
|
|
} else {
|
|
Err(io::Error::last_os_error())
|
|
}
|
|
}
|
|
|
|
/// Run the provided closure, but panic if it does not complete until the timeout has passed.
|
|
/// We should panic here, as we cannot gracefully stop the closure from running.
|
|
fn panic_on_timeout<F, U>(closure: F, timeout: Duration) -> U
|
|
where
|
|
F: FnOnce() -> U + Send + 'static,
|
|
U: Send + 'static,
|
|
{
|
|
let (tx, rx) = sync_channel::<()>(1);
|
|
let handle = thread::spawn(move || {
|
|
let result = closure();
|
|
tx.send(()).unwrap();
|
|
result
|
|
});
|
|
rx.recv_timeout(timeout)
|
|
.expect("Operation timed out or closure paniced.");
|
|
handle.join().unwrap()
|
|
}
|
|
|
|
fn download_file(url: &str, destination: &Path) -> Result<()> {
|
|
let status = Command::new("curl")
|
|
.arg("--fail")
|
|
.arg("--location")
|
|
.args(&["--output", destination.to_str().unwrap()])
|
|
.arg(url)
|
|
.status();
|
|
match status {
|
|
Ok(exit_code) => {
|
|
if !exit_code.success() {
|
|
Err(anyhow!("Cannot download {}", url))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
Err(error) => Err(anyhow!(error)),
|
|
}
|
|
}
|
|
|
|
fn crosvm_command(command: &str, args: &[&str]) -> Result<()> {
|
|
println!("$ crosvm {} {:?}", command, &args.join(" "));
|
|
let status = Command::new(find_crosvm_binary())
|
|
.arg(command)
|
|
.args(args)
|
|
.status()?;
|
|
|
|
if !status.success() {
|
|
Err(anyhow!("Command failed with exit code {}", status))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Test fixture to spin up a VM running a guest that can be communicated with.
|
|
///
|
|
/// After creation, commands can be sent via exec_in_guest. The VM is stopped
|
|
/// when this instance is dropped.
|
|
pub struct TestVm {
|
|
/// Maintain ownership of test_dir until the vm is destroyed.
|
|
#[allow(dead_code)]
|
|
test_dir: TempDir,
|
|
from_guest_reader: BufReader<File>,
|
|
to_guest: File,
|
|
control_socket_path: PathBuf,
|
|
process: Child,
|
|
debug: bool,
|
|
}
|
|
|
|
impl TestVm {
|
|
/// Magic line sent by the delegate binary when the guest is ready.
|
|
const MAGIC_LINE: &'static str = "\x05Ready";
|
|
|
|
/// Downloads prebuilts if needed.
|
|
fn initialize_once() {
|
|
syslog::init().unwrap();
|
|
|
|
// It's possible the prebuilts downloaded by crosvm-9999.ebuild differ
|
|
// from the version that crosvm was compiled for.
|
|
if let Ok(value) = env::var("CROSVM_CARGO_TEST_PREBUILT_VERSION") {
|
|
if value != prebuilt_version() {
|
|
panic!(
|
|
"Environment provided prebuilts are version {}, but crosvm was compiled \
|
|
for prebuilt version {}. Did you update PREBUILT_VERSION everywhere?",
|
|
value,
|
|
prebuilt_version()
|
|
);
|
|
}
|
|
}
|
|
|
|
let kernel_path = kernel_path();
|
|
if env::var("CROSVM_CARGO_TEST_KERNEL_BINARY").is_err() {
|
|
if !kernel_path.exists() {
|
|
println!("Downloading kernel prebuilt:");
|
|
download_file(&kernel_prebuilt_url(), &kernel_path).unwrap();
|
|
}
|
|
}
|
|
assert!(kernel_path.exists(), "{:?} does not exist", kernel_path);
|
|
|
|
let rootfs_path = rootfs_path();
|
|
if env::var("CROSVM_CARGO_TEST_ROOTFS_IMAGE").is_err() {
|
|
if !rootfs_path.exists() {
|
|
println!("Downloading rootfs prebuilt:");
|
|
download_file(&rootfs_prebuilt_url(), &rootfs_path).unwrap();
|
|
}
|
|
}
|
|
assert!(rootfs_path.exists(), "{:?} does not exist", rootfs_path);
|
|
}
|
|
|
|
// Adds 2 serial devices:
|
|
// - ttyS0: Console device which prints kernel log / debug output of the
|
|
// delegate binary.
|
|
// - ttyS1: Serial device attached to the named pipes.
|
|
fn configure_serial_devices(
|
|
command: &mut Command,
|
|
from_guest_pipe: &Path,
|
|
to_guest_pipe: &Path,
|
|
) {
|
|
command.args(&["--serial", "type=syslog"]);
|
|
|
|
// Setup channel for communication with the delegate.
|
|
let serial_params = format!(
|
|
"type=file,path={},input={},num=2",
|
|
from_guest_pipe.display(),
|
|
to_guest_pipe.display()
|
|
);
|
|
command.args(&["--serial", &serial_params]);
|
|
}
|
|
|
|
/// Configures the VM kernel and rootfs to load from the guest_under_test assets.
|
|
fn configure_kernel(command: &mut Command) {
|
|
command
|
|
.args(&["--root", rootfs_path().to_str().unwrap()])
|
|
.args(&["--params", "init=/bin/delegate"])
|
|
.arg(kernel_path());
|
|
}
|
|
|
|
/// Instanciate a new crosvm instance. The first call will trigger the download of prebuilt
|
|
/// files if necessary.
|
|
pub fn new(additional_arguments: &[&str], debug: bool) -> Result<TestVm> {
|
|
static PREP_ONCE: Once = Once::new();
|
|
PREP_ONCE.call_once(|| TestVm::initialize_once());
|
|
|
|
// Create two named pipes to communicate with the guest.
|
|
let test_dir = TempDir::new()?;
|
|
let from_guest_pipe = test_dir.path().join("from_guest");
|
|
let to_guest_pipe = test_dir.path().join("to_guest");
|
|
mkfifo(&from_guest_pipe)?;
|
|
mkfifo(&to_guest_pipe)?;
|
|
|
|
let control_socket_path = test_dir.path().join("control");
|
|
|
|
let mut command = Command::new(find_crosvm_binary());
|
|
command.args(&["run", "--disable-sandbox"]);
|
|
TestVm::configure_serial_devices(&mut command, &from_guest_pipe, &to_guest_pipe);
|
|
command.args(&["--socket", &control_socket_path.to_str().unwrap()]);
|
|
command.args(additional_arguments);
|
|
|
|
TestVm::configure_kernel(&mut command);
|
|
|
|
println!("$ {:?}", command);
|
|
if !debug {
|
|
command.stdout(Stdio::null());
|
|
command.stderr(Stdio::null());
|
|
}
|
|
let process = command.spawn()?;
|
|
|
|
// Open pipes. Panic if we cannot connect after a timeout.
|
|
let (to_guest, from_guest) = panic_on_timeout(
|
|
move || (File::create(to_guest_pipe), File::open(from_guest_pipe)),
|
|
VM_COMMUNICATION_TIMEOUT,
|
|
);
|
|
|
|
// Wait for magic line to be received, indicating the delegate is ready.
|
|
let mut from_guest_reader = BufReader::new(from_guest?);
|
|
let mut magic_line = String::new();
|
|
from_guest_reader.read_line(&mut magic_line)?;
|
|
assert_eq!(magic_line.trim(), TestVm::MAGIC_LINE);
|
|
|
|
Ok(TestVm {
|
|
test_dir,
|
|
from_guest_reader,
|
|
to_guest: to_guest?,
|
|
control_socket_path,
|
|
process,
|
|
debug,
|
|
})
|
|
}
|
|
|
|
/// Executes the shell command `command` and returns the programs stdout.
|
|
pub fn exec_in_guest(&mut self, command: &str) -> Result<String> {
|
|
// Write command to serial port.
|
|
writeln!(&mut self.to_guest, "{}", command)?;
|
|
|
|
// We will receive an echo of what we have written on the pipe.
|
|
let mut echo = String::new();
|
|
self.from_guest_reader.read_line(&mut echo)?;
|
|
assert_eq!(echo.trim(), command);
|
|
|
|
// Return all remaining lines until we receive the MAGIC_LINE
|
|
let mut output = String::new();
|
|
loop {
|
|
let mut line = String::new();
|
|
self.from_guest_reader.read_line(&mut line)?;
|
|
if line.trim() == TestVm::MAGIC_LINE {
|
|
break;
|
|
}
|
|
output.push_str(&line);
|
|
}
|
|
let trimmed = output.trim();
|
|
if self.debug {
|
|
println!("<- {:?}", trimmed);
|
|
}
|
|
Ok(trimmed.to_string())
|
|
}
|
|
|
|
pub fn stop(&self) -> Result<()> {
|
|
crosvm_command("stop", &[self.control_socket_path.to_str().unwrap()])
|
|
}
|
|
|
|
pub fn suspend(&self) -> Result<()> {
|
|
crosvm_command("suspend", &[self.control_socket_path.to_str().unwrap()])
|
|
}
|
|
|
|
pub fn resume(&self) -> Result<()> {
|
|
crosvm_command("resume", &[self.control_socket_path.to_str().unwrap()])
|
|
}
|
|
}
|
|
|
|
impl Drop for TestVm {
|
|
fn drop(&mut self) {
|
|
self.stop().unwrap();
|
|
self.process.wait().unwrap();
|
|
}
|
|
}
|