e2e_tests: Allow TestVM to run with custom artifacts

TestVM can now be configured to run on specific local or remote
kernel, initrd and rootfs images. This allow us to broaden e2e test
coverages to more diverse use cases.

TESTED=Relevant test case passed CQ and locally

BUG=b:257303497
BUG=b:181105093

Change-Id: I1270a189105af89259371f54b7df227b3efae380
Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/4451468
Reviewed-by: Dennis Kempin <denniskempin@google.com>
Commit-Queue: Zihan Chen <zihanchen@google.com>
This commit is contained in:
Zihan Chen 2023-04-19 15:36:03 -07:00 committed by crosvm LUCI
parent 1a0a8c26e2
commit 8f5225c042
6 changed files with 309 additions and 105 deletions

68
Cargo.lock generated
View file

@ -1054,12 +1054,14 @@ dependencies = [
"arch",
"base",
"cfg-if",
"crc32fast",
"libc",
"log",
"prebuilts",
"rand",
"shlex",
"tempfile",
"url",
]
[[package]]
@ -1083,6 +1085,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fuse"
version = "0.1.0"
@ -1320,6 +1331,16 @@ dependencies = [
"windows",
]
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.1"
@ -1904,6 +1925,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "perfetto"
version = "0.1.0"
@ -2541,6 +2568,21 @@ dependencies = [
"winapi",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "toml"
version = "0.5.9"
@ -2579,18 +2621,44 @@ dependencies = [
"winapi",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-xid"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
[[package]]
name = "url"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "usb_sys"
version = "0.1.0"

View file

@ -8,6 +8,7 @@ edition = "2021"
anyhow = "*"
arch = { path = "../../arch" }
base = "*"
crc32fast = "1.3"
cfg-if = "*"
libc = "0.2.65"
rand = "0.8"
@ -15,6 +16,7 @@ tempfile = "3"
prebuilts = { path = "../../prebuilts" }
log = "*"
shlex = "*"
url = "2.3"
[features]
direct = []

View file

@ -26,8 +26,7 @@ use tempfile::TempDir;
use crate::utils::find_crosvm_binary;
use crate::utils::run_with_timeout;
use crate::vm::kernel_path;
use crate::vm::rootfs_path;
use crate::vm::local_path_from_url;
use crate::vm::Config;
const FROM_GUEST_PIPE: &str = "from_guest";
@ -90,10 +89,12 @@ impl TestVmSys {
// - ttyS1: Serial device attached to the named pipes.
fn configure_serial_devices(
command: &mut Command,
stdout_hardware_type: &str,
from_guest_pipe: &Path,
to_guest_pipe: &Path,
) {
command.args(["--serial", "type=stdout,hardware=virtio-console,console"]);
let stdout_serial_option = format!("type=stdout,hardware={},console", stdout_hardware_type);
command.args(["--serial", &stdout_serial_option]);
// Setup channel for communication with the delegate.
let serial_params = format!(
@ -105,10 +106,10 @@ impl TestVmSys {
}
/// Configures the VM rootfs to load from the guest_under_test assets.
fn configure_rootfs(command: &mut Command, o_direct: bool) {
fn configure_rootfs(command: &mut Command, o_direct: bool, path: &Path) {
let rootfs_and_option = format!(
"{}{},ro,root",
rootfs_path().to_str().unwrap(),
path.as_os_str().to_str().unwrap(),
if o_direct { ",direct=true" } else { "" }
);
command
@ -203,14 +204,24 @@ impl TestVmSys {
pub fn append_config_args(command: &mut Command, test_dir: &Path, cfg: &Config) -> Result<()> {
TestVmSys::configure_serial_devices(
command,
&cfg.console_hardware,
&test_dir.join(FROM_GUEST_PIPE),
&test_dir.join(TO_GUEST_PIPE),
);
command.args(["--socket", test_dir.join(CONTROL_PIPE).to_str().unwrap()]);
TestVmSys::configure_rootfs(command, cfg.o_direct);
// Set kernel as the last argument.
command.arg(kernel_path());
if let Some(rootfs_url) = &cfg.rootfs_url {
TestVmSys::configure_rootfs(command, cfg.o_direct, &local_path_from_url(rootfs_url));
};
// Set initrd if being requested
if let Some(initrd_url) = &cfg.initrd_url {
command.arg("--initrd");
command.arg(local_path_from_url(initrd_url));
}
// Set kernel as the last argument.
command.arg(local_path_from_url(&cfg.kernel_url));
Ok(())
}
@ -219,42 +230,65 @@ impl TestVmSys {
let config_file_path = test_dir.join(VM_JSON_CONFIG_FILE);
let mut config_file = File::create(&config_file_path)?;
writeln!(config_file, "{{")?;
writeln!(
config_file,
r#""kernel": "{}""#,
local_path_from_url(&cfg.kernel_url).display()
)?;
if let Some(initrd_url) = &cfg.initrd_url {
writeln!(
config_file,
r#"",initrd": "{}""#,
local_path_from_url(initrd_url)
.to_str()
.context("invalid initrd path")?
)?;
};
writeln!(
config_file,
r#"
{{
"kernel": "{}",
"socket": "{}",
"params": [ "init=/bin/delegate" ],
"serial": [
{{
"type": "stdout"
}},
{{
"type": "file",
"path": "{}",
"input": "{}",
"num": 2
}}
],
"block": [
{{
"path": "{}",
"ro": true,
"root": true,
"direct": {}
}}
]
}}
"#,
kernel_path().display(),
,"socket": "{}",
"params": [ "init=/bin/delegate" ],
"serial": [
{{
"type": "stdout"
}},
{{
"type": "file",
"path": "{}",
"input": "{}",
"num": 2
}}
]
"#,
test_dir.join(CONTROL_PIPE).display(),
test_dir.join(FROM_GUEST_PIPE).display(),
test_dir.join(TO_GUEST_PIPE).display(),
rootfs_path().to_str().unwrap(),
cfg.o_direct,
)?;
if let Some(rootfs_url) = &cfg.rootfs_url {
writeln!(
config_file,
r#"
,"block": [
{{
"path": "{}",
"ro": true,
"root": true,
"direct": {}
}}
]
"#,
local_path_from_url(rootfs_url)
.to_str()
.context("invalid rootfs path")?,
cfg.o_direct,
)?;
};
writeln!(config_file, "}}")?;
Ok(config_file_path)
}

View file

@ -18,14 +18,14 @@ use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use base::named_pipes;
use base::PipeConnection;
use rand::Rng;
use crate::utils::find_crosvm_binary;
use crate::vm::kernel_path;
use crate::vm::rootfs_path;
use crate::vm::local_path_from_url;
use crate::vm::Config;
const GUEST_EARLYCON: &str = "guest_earlycon.log";
@ -141,7 +141,12 @@ impl TestVmSys {
// - 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, logs_dir: &Path) {
fn configure_serial_devices(
command: &mut Command,
stdout_hardware_type: &str,
from_guest_pipe: &Path,
logs_dir: &Path,
) {
let earlycon_path = Path::new(logs_dir).join(GUEST_EARLYCON);
let earlycon_str = earlycon_path.to_str().unwrap();
@ -154,7 +159,9 @@ impl TestVmSys {
let console_str = console_path.to_str().unwrap();
command.args([
r"--serial",
&format!("hardware=virtio-console,num=1,type=file,path={console_str},console=true"),
&format!(
"hardware={stdout_hardware_type},num=1,type=file,path={console_str},console=true"
),
]);
// Setup channel for communication with the delegate.
@ -166,9 +173,11 @@ impl TestVmSys {
}
/// Configures the VM rootfs to load from the guest_under_test assets.
fn configure_rootfs(command: &mut Command, _o_direct: bool) {
let rootfs_and_option =
format!("{},ro,root,sparse=false", rootfs_path().to_str().unwrap(),);
fn configure_rootfs(command: &mut Command, _o_direct: bool, path: &Path) {
let rootfs_and_option = format!(
"{},ro,root,sparse=false",
path.as_os_str().to_str().unwrap(),
);
command.args(["--root", &rootfs_and_option]).args([
"--params",
"init=/bin/delegate noxsaves noxsave nopat nopti tsc=reliable",
@ -232,12 +241,22 @@ impl TestVmSys {
) -> Result<()> {
TestVmSys::configure_serial_devices(
command,
&cfg.console_hardware,
&serial_args.from_guest_pipe,
&serial_args.logs_dir,
);
TestVmSys::configure_rootfs(command, cfg.o_direct);
if let Some(rootfs_url) = &cfg.rootfs_url {
TestVmSys::configure_rootfs(command, cfg.o_direct, &local_path_from_url(rootfs_url));
};
// Set initrd if being requested
if let Some(initrd_url) = &cfg.initrd_url {
command.arg("--initrd");
command.arg(local_path_from_url(initrd_url));
}
// Set kernel as the last argument.
command.arg(kernel_path());
command.arg(local_path_from_url(&cfg.kernel_url));
Ok(())
}
@ -246,15 +265,16 @@ impl TestVmSys {
fn generate_json_config_file(
from_guest_pipe: &Path,
logs_path: &Path,
_cfg: &Config,
cfg: &Config,
) -> Result<PathBuf> {
let config_file_path = logs_path.join(VM_JSON_CONFIG_FILE);
let mut config_file = File::create(&config_file_path)?;
writeln!(config_file, "{{")?;
writeln!(
config_file,
r#"
{{
"params": [ "init=/bin/delegate noxsaves noxsave nopat nopti tsc=reliable" ],
"serial": [
{{
@ -277,36 +297,62 @@ impl TestVmSys {
"type": "namedpipe",
"path": "{}",
}},
],
"root": [
]
}}
"#,
logs_path.join(GUEST_EARLYCON).display(),
logs_path.join(GUEST_CONSOLE).display(),
from_guest_pipe.display()
)?;
if let Some(rootfs_url) = &cfg.rootfs_url {
writeln!(
config_file,
r#"
,"root": [
{{
"path": "{}",
"ro": true,
"root": true,
"sparse": false
}}
],
"logs-directory": "{}",
"kernel-log-file": "{},
"hypervisor": "{}"
{},
{}
}}
"#,
logs_path.join(GUEST_EARLYCON).display(),
logs_path.join(GUEST_CONSOLE).display(),
from_guest_pipe.display(),
rootfs_path().to_str().unwrap(),
]
"#,
local_path_from_url(rootfs_url)
.to_str()
.context("invalid rootfs path")?,
)?;
};
if let Some(initrd_url) = &cfg.initrd_url {
writeln!(
config_file,
r#"",initrd": "{}""#,
local_path_from_url(initrd_url)
.to_str()
.context("invalid initrd path")?
)?;
};
writeln!(
config_file,
r#"
,"logs-directory": "{}",
"kernel-log-file": "{},
"hypervisor": "{}"
{},
{}"#,
logs_path.display(),
logs_path.join(HYPERVISOR_LOG).display(),
get_hypervisor(),
kernel_path().display(),
local_path_from_url(&cfg.kernel_url).display(),
&get_irqchip(&get_hypervisor()).map_or("".to_owned(), |irqchip| format!(
r#","irqchip": "{}""#,
irqchip
))
)?;
writeln!(config_file, "}}")?;
Ok(config_file_path)
}

View file

@ -16,8 +16,10 @@ use anyhow::Context;
use anyhow::Result;
use base::syslog;
use base::test_utils::check_can_sudo;
use crc32fast::hash;
use log::Level;
use prebuilts::download_file;
use url::Url;
use crate::sys::SerialArgs;
use crate::sys::TestVmSys;
@ -47,48 +49,38 @@ fn prebuilt_version() -> &'static str {
include_str!("../../guest_under_test/PREBUILT_VERSION").trim()
}
fn kernel_prebuilt_url() -> String {
format!(
fn kernel_prebuilt_url_string() -> Url {
Url::parse(&format!(
"{}/guest-bzimage-{}-{}",
PREBUILT_URL,
ARCH,
prebuilt_version()
)
))
.unwrap()
}
fn rootfs_prebuilt_url() -> String {
format!(
fn rootfs_prebuilt_url_string() -> Url {
Url::parse(&format!(
"{}/guest-rootfs-{}-{}",
PREBUILT_URL,
ARCH,
prebuilt_version()
)
))
.unwrap()
}
/// The kernel bzImage is stored next to the test executable, unless overridden by
/// CROSVM_CARGO_TEST_KERNEL_BINARY
pub(super) 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(format!("bzImage-{}", prebuilt_version())),
pub(super) fn local_path_from_url(url: &Url) -> PathBuf {
if url.scheme() == "file" {
return url.to_file_path().unwrap();
}
}
/// The rootfs image is stored next to the test executable, unless overridden by
/// CROSVM_CARGO_TEST_ROOTFS_IMAGE
pub(super) 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(format!("rootfs-{}", prebuilt_version())),
if url.scheme() != "http" && url.scheme() != "https" {
panic!("Only file, http, https URLs are supported for artifacts")
}
env::current_exe().unwrap().parent().unwrap().join(format!(
"e2e_prebuilt-{:x}-{:x}",
hash(url.as_str().as_bytes()),
hash(url.path().as_bytes())
))
}
/// Represents a command running in the guest. See `TestVm::exec_in_guest_async()`
@ -176,6 +168,18 @@ pub struct Config {
/// Wrapper command line for executing `TestVM`
pub(super) wrapper_cmd: Option<String>,
/// Url to kernel image
pub(super) kernel_url: Url,
/// Url to initrd image
pub(super) initrd_url: Option<Url>,
/// Url to rootfs image
pub(super) rootfs_url: Option<Url>,
/// Console hardware type
pub(super) console_hardware: String,
}
impl Default for Config {
@ -186,6 +190,10 @@ impl Default for Config {
o_direct: Default::default(),
log_file: None,
wrapper_cmd: None,
kernel_url: kernel_prebuilt_url_string(),
initrd_url: None,
rootfs_url: Some(rootfs_prebuilt_url_string()),
console_hardware: "virtio-console".to_owned(),
}
}
}
@ -197,7 +205,6 @@ impl Config {
}
/// Uses extra arguments for `crosvm run`.
#[allow(dead_code)]
pub fn extra_args(mut self, args: Vec<String>) -> Self {
let mut args = args;
self.extra_args.append(&mut args);
@ -221,8 +228,36 @@ impl Config {
env::var("CROSVM_CARGO_TEST_E2E_WRAPPER_CMD").map_or((), |x| cfg.wrapper_cmd = Some(x));
env::var("CROSVM_CARGO_TEST_LOG_FILE").map_or((), |x| cfg.log_file = Some(x));
env::var("CROSVM_CARGO_TEST_LOG_LEVEL_DEBUG").map_or((), |_| cfg.log_level = Level::Debug);
env::var("CROSVM_CARGO_TEST_KERNEL_IMAGE")
.map_or((), |x| cfg.kernel_url = Url::from_file_path(x).unwrap());
env::var("CROSVM_CARGO_TEST_INITRD_IMAGE").map_or((), |x| {
cfg.initrd_url = Some(Url::from_file_path(x).unwrap())
});
env::var("CROSVM_CARGO_TEST_ROOTFS_IMAGE").map_or((), |x| {
cfg.rootfs_url = Some(Url::from_file_path(x).unwrap())
});
cfg
}
pub fn with_kernel(mut self, url: &str) -> Self {
self.kernel_url = Url::parse(url).unwrap();
self
}
pub fn with_initrd(mut self, url: &str) -> Self {
self.initrd_url = Some(Url::parse(url).unwrap());
self
}
pub fn with_rootfs(mut self, url: &str) -> Self {
self.rootfs_url = Some(Url::parse(url).unwrap());
self
}
pub fn with_stdout_hardware(mut self, hw_type: &str) -> Self {
self.console_hardware = hw_type.to_owned();
self
}
}
static PREP_ONCE: Once = Once::new();
@ -264,24 +299,31 @@ impl TestVm {
);
}
}
}
let kernel_path = kernel_path();
if env::var("CROSVM_CARGO_TEST_KERNEL_BINARY").is_err() {
if !kernel_path.exists() {
download_file(&kernel_prebuilt_url(), &kernel_path).unwrap();
}
fn initiailize_artifacts(cfg: &Config) {
let kernel_path = local_path_from_url(&cfg.kernel_url);
if !kernel_path.exists() && cfg.kernel_url.scheme() != "file" {
download_file(cfg.kernel_url.as_str(), &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() {
download_file(&rootfs_prebuilt_url(), &rootfs_path).unwrap();
if let Some(initrd_url) = &cfg.initrd_url {
let initrd_path = local_path_from_url(initrd_url);
if !initrd_path.exists() && initrd_url.scheme() != "file" {
download_file(initrd_url.as_str(), &initrd_path).unwrap();
}
assert!(initrd_path.exists(), "{:?} does not exist", initrd_path);
}
assert!(rootfs_path.exists(), "{:?} does not exist", rootfs_path);
TestVmSys::check_rootfs_file(&rootfs_path);
if let Some(rootfs_url) = &cfg.rootfs_url {
let rootfs_path = local_path_from_url(rootfs_url);
if !rootfs_path.exists() && rootfs_url.scheme() != "file" {
download_file(rootfs_url.as_str(), &rootfs_path).unwrap();
}
assert!(rootfs_path.exists(), "{:?} does not exist", rootfs_path);
TestVmSys::check_rootfs_file(&rootfs_path);
}
}
/// Instanciate a new crosvm instance. The first call will trigger the download of prebuilt
@ -294,6 +336,9 @@ impl TestVm {
F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,
{
PREP_ONCE.call_once(TestVm::initialize_once);
TestVm::initiailize_artifacts(&cfg);
let mut vm = TestVm {
sys: TestVmSys::new_generic(f, cfg, sudo).with_context(|| "Could not start crosvm")?,
ready: false,

View file

@ -65,7 +65,12 @@ def parse_seccomp_trace_events_to_pid_table(
bench = cmd("cargo test").with_color_flag()
def main(target_name: str, log_seccomp: bool = False, log_seccomp_output_dir: str = ""):
def main(
target_name: str,
log_seccomp: bool = False,
log_seccomp_output_dir: str = "",
nocapture: bool = False,
):
"""Run an end-to-end benchmark target.
target-name -- name of target
@ -98,8 +103,12 @@ def main(target_name: str, log_seccomp: bool = False, log_seccomp_output_dir: st
),
}
)
bench("--release", "--bench", target_name).with_envs(build_env).fg()
if nocapture:
bench("--release", "--bench", target_name, "--", "--nocapture").with_envs(
build_env
).fg()
else:
bench("--release", "--bench", target_name).with_envs(build_env).fg()
if log_seccomp:
with open(pathlib.Path(tempdir) / "crosvm.log", "r") as f: