1 // Copyright 2022 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 use std::env;
6 use std::io::Write;
7 use std::path::Path;
8 use std::path::PathBuf;
9 use std::process::Command;
10 use std::sync::Once;
11 use std::time::Duration;
12
13 use anyhow::anyhow;
14 use anyhow::bail;
15 use anyhow::Context;
16 use anyhow::Result;
17 use base::syslog;
18 use base::test_utils::check_can_sudo;
19 use crc32fast::hash;
20 use delegate::wire_format::DelegateMessage;
21 use delegate::wire_format::ExitStatus;
22 use delegate::wire_format::GuestToHostMessage;
23 use delegate::wire_format::HostToGuestMessage;
24 use delegate::wire_format::ProgramExit;
25 use log::info;
26 use log::Level;
27 use prebuilts::download_file;
28 use readclock::ClockValues;
29 use url::Url;
30
31 use crate::sys::SerialArgs;
32 use crate::sys::TestVmSys;
33 use crate::utils::run_with_timeout;
34
35 const PREBUILT_URL: &str = "https://storage.googleapis.com/crosvm/integration_tests";
36
37 #[cfg(target_arch = "x86_64")]
38 const ARCH: &str = "x86_64";
39 #[cfg(target_arch = "arm")]
40 const ARCH: &str = "arm";
41 #[cfg(target_arch = "aarch64")]
42 const ARCH: &str = "aarch64";
43 #[cfg(target_arch = "riscv64")]
44 const ARCH: &str = "riscv64";
45
46 /// Timeout when waiting for pipes that are expected to be ready.
47 const COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(5);
48
49 /// Timeout for the VM to boot and the delegate to report that it's ready.
50 const BOOT_TIMEOUT: Duration = Duration::from_secs(60);
51
52 /// Default timeout when waiting for guest commands to execute
53 const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(10);
54
prebuilt_version() -> &'static str55 fn prebuilt_version() -> &'static str {
56 include_str!("../../guest_under_test/PREBUILT_VERSION").trim()
57 }
58
kernel_prebuilt_url_string() -> Url59 fn kernel_prebuilt_url_string() -> Url {
60 Url::parse(&format!(
61 "{}/guest-bzimage-{}-{}",
62 PREBUILT_URL,
63 ARCH,
64 prebuilt_version()
65 ))
66 .unwrap()
67 }
68
rootfs_prebuilt_url_string() -> Url69 fn rootfs_prebuilt_url_string() -> Url {
70 Url::parse(&format!(
71 "{}/guest-rootfs-{}-{}",
72 PREBUILT_URL,
73 ARCH,
74 prebuilt_version()
75 ))
76 .unwrap()
77 }
78
local_path_from_url(url: &Url) -> PathBuf79 pub(super) fn local_path_from_url(url: &Url) -> PathBuf {
80 if url.scheme() == "file" {
81 return url.to_file_path().unwrap();
82 }
83 if url.scheme() != "http" && url.scheme() != "https" {
84 panic!("Only file, http, https URLs are supported for artifacts")
85 }
86 env::current_exe().unwrap().parent().unwrap().join(format!(
87 "e2e_prebuilt-{:x}-{:x}",
88 hash(url.as_str().as_bytes()),
89 hash(url.path().as_bytes())
90 ))
91 }
92
93 /// Represents a command running in the guest. See `TestVm::exec_in_guest_async()`
94 #[must_use]
95 pub struct GuestProcess {
96 command: String,
97 timeout: Duration,
98 }
99
100 impl GuestProcess {
with_timeout(self, duration: Duration) -> Self101 pub fn with_timeout(self, duration: Duration) -> Self {
102 Self {
103 timeout: duration,
104 ..self
105 }
106 }
107
108 /// Waits for the process to finish execution and return ExitStatus.
109 /// Will fail on a non-zero exit code.
wait_ok(self, vm: &mut TestVm) -> Result<ProgramExit>110 pub fn wait_ok(self, vm: &mut TestVm) -> Result<ProgramExit> {
111 let command = self.command.clone();
112 let result = self.wait_result(vm)?;
113
114 match &result.exit_status {
115 ExitStatus::Code(0) => Ok(result),
116 ExitStatus::Code(code) => {
117 bail!("Command `{}` terminated with exit code {}", command, code)
118 }
119 ExitStatus::Signal(code) => bail!("Command `{}` stopped with signal {}", command, code),
120 ExitStatus::None => bail!("Command `{}` stopped for unknown reason", command),
121 }
122 }
123
124 /// Same as `wait_ok` but will return a ExitStatus instead of failing on a non-zero exit code,
125 /// will only fail when cannot receive output from guest.
wait_result(self, vm: &mut TestVm) -> Result<ProgramExit>126 pub fn wait_result(self, vm: &mut TestVm) -> Result<ProgramExit> {
127 let message = vm.read_message_from_guest(self.timeout).with_context(|| {
128 format!(
129 "Command `{}`: Failed to read response from guest",
130 self.command
131 )
132 })?;
133 // VM is ready when receiving any message (as for current protocol)
134 match message {
135 GuestToHostMessage::ProgramExit(exit_info) => Ok(exit_info),
136 _ => bail!("Receive other message when anticipating ProgramExit"),
137 }
138 }
139 }
140
141 /// Configuration to start `TestVm`.
142 pub struct Config {
143 /// Extra arguments for the `run` subcommand.
144 pub(super) extra_args: Vec<String>,
145
146 /// Use `O_DIRECT` for the rootfs.
147 pub(super) o_direct: bool,
148
149 /// Log level of `TestVm`
150 pub(super) log_level: Level,
151
152 /// File to save crosvm log to
153 pub(super) log_file: Option<String>,
154
155 /// Wrapper command line for executing `TestVM`
156 pub(super) wrapper_cmd: Option<String>,
157
158 /// Url to kernel image
159 pub(super) kernel_url: Url,
160
161 /// Url to initrd image
162 pub(super) initrd_url: Option<Url>,
163
164 /// Url to rootfs image
165 pub(super) rootfs_url: Option<Url>,
166
167 /// If rootfs image is writable
168 pub(super) rootfs_rw: bool,
169
170 /// If rootfs image is zstd compressed
171 pub(super) rootfs_compressed: bool,
172
173 /// Console hardware type
174 pub(super) console_hardware: String,
175 }
176
177 impl Default for Config {
default() -> Self178 fn default() -> Self {
179 Self {
180 log_level: Level::Info,
181 extra_args: Default::default(),
182 o_direct: Default::default(),
183 log_file: None,
184 wrapper_cmd: None,
185 kernel_url: kernel_prebuilt_url_string(),
186 initrd_url: None,
187 rootfs_url: Some(rootfs_prebuilt_url_string()),
188 rootfs_rw: false,
189 rootfs_compressed: false,
190 console_hardware: "virtio-console".to_owned(),
191 }
192 }
193 }
194
195 impl Config {
196 /// Creates a new `run` command with `extra_args`.
new() -> Self197 pub fn new() -> Self {
198 Self::from_env()
199 }
200
201 /// Uses extra arguments for `crosvm run`.
extra_args(mut self, args: Vec<String>) -> Self202 pub fn extra_args(mut self, args: Vec<String>) -> Self {
203 let mut args = args;
204 self.extra_args.append(&mut args);
205 self
206 }
207
208 /// Uses `O_DIRECT` for the rootfs.
o_direct(mut self) -> Self209 pub fn o_direct(mut self) -> Self {
210 self.o_direct = true;
211 self
212 }
213
214 /// Uses `disable-sandbox` argument for `crosvm run`.
disable_sandbox(mut self) -> Self215 pub fn disable_sandbox(mut self) -> Self {
216 self.extra_args.push("--disable-sandbox".to_string());
217 self
218 }
219
from_env() -> Self220 pub fn from_env() -> Self {
221 let mut cfg: Config = Default::default();
222 if let Ok(wrapper_cmd) = env::var("CROSVM_CARGO_TEST_E2E_WRAPPER_CMD") {
223 cfg.wrapper_cmd = Some(wrapper_cmd);
224 }
225 if let Ok(log_file) = env::var("CROSVM_CARGO_TEST_LOG_FILE") {
226 cfg.log_file = Some(log_file);
227 }
228 if env::var("CROSVM_CARGO_TEST_LOG_LEVEL_DEBUG").is_ok() {
229 cfg.log_level = Level::Debug;
230 }
231 if let Ok(kernel_url) = env::var("CROSVM_CARGO_TEST_KERNEL_IMAGE") {
232 info!("Using overrided kernel from env CROSVM_CARGO_TEST_KERNEL_IMAGE={kernel_url}");
233 cfg.kernel_url = Url::from_file_path(kernel_url).unwrap();
234 }
235 if let Ok(initrd_url) = env::var("CROSVM_CARGO_TEST_INITRD_IMAGE") {
236 info!("Using overrided kernel from env CROSVM_CARGO_TEST_INITRD_IMAGE={initrd_url}");
237 cfg.initrd_url = Some(Url::from_file_path(initrd_url).unwrap());
238 }
239 if let Ok(rootfs_url) = env::var("CROSVM_CARGO_TEST_ROOTFS_IMAGE") {
240 info!("Using overrided kernel from env CROSVM_CARGO_TEST_ROOTFS_IMAGE={rootfs_url}");
241 cfg.rootfs_url = Some(Url::from_file_path(rootfs_url).unwrap());
242 }
243 cfg
244 }
245
with_kernel(mut self, url: &str) -> Self246 pub fn with_kernel(mut self, url: &str) -> Self {
247 self.kernel_url = Url::parse(url).unwrap();
248 self
249 }
250
with_initrd(mut self, url: &str) -> Self251 pub fn with_initrd(mut self, url: &str) -> Self {
252 self.initrd_url = Some(Url::parse(url).unwrap());
253 self
254 }
255
with_rootfs(mut self, url: &str) -> Self256 pub fn with_rootfs(mut self, url: &str) -> Self {
257 self.rootfs_url = Some(Url::parse(url).unwrap());
258 self
259 }
260
rootfs_is_rw(mut self) -> Self261 pub fn rootfs_is_rw(mut self) -> Self {
262 self.rootfs_rw = true;
263 self
264 }
265
rootfs_is_compressed(mut self) -> Self266 pub fn rootfs_is_compressed(mut self) -> Self {
267 self.rootfs_compressed = true;
268 self
269 }
270
with_stdout_hardware(mut self, hw_type: &str) -> Self271 pub fn with_stdout_hardware(mut self, hw_type: &str) -> Self {
272 self.console_hardware = hw_type.into();
273 self
274 }
275
with_vhost_user(mut self, device_type: &str, socket_path: &Path) -> Self276 pub fn with_vhost_user(mut self, device_type: &str, socket_path: &Path) -> Self {
277 self.extra_args.push("--vhost-user".to_string());
278 self.extra_args.push(format!(
279 "{},socket={}",
280 device_type,
281 socket_path.to_str().unwrap()
282 ));
283 self
284 }
285 }
286
287 static PREP_ONCE: Once = Once::new();
288
289 /// Test fixture to spin up a VM running a guest that can be communicated with.
290 ///
291 /// After creation, commands can be sent via exec_in_guest. The VM is stopped
292 /// when this instance is dropped.
293 pub struct TestVm {
294 // Platform-dependent bits
295 sys: TestVmSys,
296 // The guest is ready to receive a command.
297 ready: bool,
298 // True if commands should be ran with `sudo`.
299 sudo: bool,
300 }
301
302 impl TestVm {
303 /// Downloads prebuilts if needed.
initialize_once()304 fn initialize_once() {
305 if let Err(e) = syslog::init() {
306 panic!("failed to initiailize syslog: {}", e);
307 }
308
309 // It's possible the prebuilts downloaded by crosvm-9999.ebuild differ
310 // from the version that crosvm was compiled for.
311 info!("Prebuilt version to be used: {}", prebuilt_version());
312 if let Ok(value) = env::var("CROSVM_CARGO_TEST_PREBUILT_VERSION") {
313 if value != prebuilt_version() {
314 panic!(
315 "Environment provided prebuilts are version {}, but crosvm was compiled \
316 for prebuilt version {}. Did you update PREBUILT_VERSION everywhere?",
317 value,
318 prebuilt_version()
319 );
320 }
321 }
322 }
323
initiailize_artifacts(cfg: &Config)324 fn initiailize_artifacts(cfg: &Config) {
325 let kernel_path = local_path_from_url(&cfg.kernel_url);
326 if !kernel_path.exists() && cfg.kernel_url.scheme() != "file" {
327 download_file(cfg.kernel_url.as_str(), &kernel_path).unwrap();
328 }
329 assert!(kernel_path.exists(), "{:?} does not exist", kernel_path);
330
331 if let Some(initrd_url) = &cfg.initrd_url {
332 let initrd_path = local_path_from_url(initrd_url);
333 if !initrd_path.exists() && initrd_url.scheme() != "file" {
334 download_file(initrd_url.as_str(), &initrd_path).unwrap();
335 }
336 assert!(initrd_path.exists(), "{:?} does not exist", initrd_path);
337 }
338
339 if let Some(rootfs_url) = &cfg.rootfs_url {
340 let rootfs_download_path = local_path_from_url(rootfs_url);
341 if !rootfs_download_path.exists() && rootfs_url.scheme() != "file" {
342 download_file(rootfs_url.as_str(), &rootfs_download_path).unwrap();
343 }
344 assert!(
345 rootfs_download_path.exists(),
346 "{:?} does not exist",
347 rootfs_download_path
348 );
349
350 if cfg.rootfs_compressed {
351 let rootfs_raw_path = rootfs_download_path.with_extension("raw");
352 Command::new("zstd")
353 .arg("-d")
354 .arg(&rootfs_download_path)
355 .arg("-o")
356 .arg(&rootfs_raw_path)
357 .arg("-f")
358 .output()
359 .expect("Failed to decompress rootfs");
360 TestVmSys::check_rootfs_file(&rootfs_raw_path);
361 } else {
362 TestVmSys::check_rootfs_file(&rootfs_download_path);
363 }
364 }
365 }
366
367 /// Instanciate a new crosvm instance. The first call will trigger the download of prebuilt
368 /// files if necessary.
369 ///
370 /// This generic method takes a `FnOnce` argument which is in charge of completing the `Command`
371 /// with all the relevant options needed to boot the VM.
new_generic<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVm> where F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,372 pub fn new_generic<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVm>
373 where
374 F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,
375 {
376 PREP_ONCE.call_once(TestVm::initialize_once);
377
378 TestVm::initiailize_artifacts(&cfg);
379
380 let mut vm = TestVm {
381 sys: TestVmSys::new_generic(f, cfg, sudo).with_context(|| "Could not start crosvm")?,
382 ready: false,
383 sudo,
384 };
385 vm.wait_for_guest_ready(BOOT_TIMEOUT)
386 .with_context(|| "Guest did not become ready after boot")?;
387 Ok(vm)
388 }
389
new_generic_restore<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVm> where F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,390 pub fn new_generic_restore<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVm>
391 where
392 F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,
393 {
394 PREP_ONCE.call_once(TestVm::initialize_once);
395 let mut vm = TestVm {
396 sys: TestVmSys::new_generic(f, cfg, sudo).with_context(|| "Could not start crosvm")?,
397 ready: false,
398 sudo,
399 };
400 vm.ready = true;
401 // TODO(b/280607404): A cold restored VM cannot respond to cmds from `exec_in_guest_async`.
402 Ok(vm)
403 }
404
new(cfg: Config) -> Result<TestVm>405 pub fn new(cfg: Config) -> Result<TestVm> {
406 TestVm::new_generic(TestVmSys::append_config_args, cfg, false)
407 }
408
409 /// Create `TestVm` from a snapshot, using `--restore` but NOT `--suspended`.
new_restore(cfg: Config) -> Result<TestVm>410 pub fn new_restore(cfg: Config) -> Result<TestVm> {
411 let mut vm = TestVm::new_generic_restore(TestVmSys::append_config_args, cfg, false)?;
412 // Send a resume request to wait for the restore to finish.
413 // We don't want to return from this function until the restore is complete, otherwise it
414 // will be difficult to differentiate between a slow restore and a slow response from the
415 // guest.
416 let vm = run_with_timeout(
417 move || {
418 vm.resume_full().expect("failed to resume after VM restore");
419 vm
420 },
421 Duration::from_secs(60),
422 )
423 .expect("VM restore timeout");
424
425 Ok(vm)
426 }
427
428 /// Create `TestVm` from a snapshot, using `--restore` AND `--suspended`.
new_restore_suspended(cfg: Config) -> Result<TestVm>429 pub fn new_restore_suspended(cfg: Config) -> Result<TestVm> {
430 TestVm::new_generic_restore(TestVmSys::append_config_args, cfg, false)
431 }
432
new_sudo(cfg: Config) -> Result<TestVm>433 pub fn new_sudo(cfg: Config) -> Result<TestVm> {
434 check_can_sudo();
435
436 TestVm::new_generic(TestVmSys::append_config_args, cfg, true)
437 }
438
439 /// Instanciate a new crosvm instance using a configuration file. The first call will trigger
440 /// the download of prebuilt files if necessary.
new_with_config_file(cfg: Config) -> Result<TestVm>441 pub fn new_with_config_file(cfg: Config) -> Result<TestVm> {
442 TestVm::new_generic(TestVmSys::append_config_file_arg, cfg, false)
443 }
444
445 /// Executes the provided command in the guest.
446 /// Returns command output as Ok(ProgramExit), or an Error if the program did not exit with 0.
exec_in_guest(&mut self, command: &str) -> Result<ProgramExit>447 pub fn exec_in_guest(&mut self, command: &str) -> Result<ProgramExit> {
448 self.exec_in_guest_async(command)?.wait_ok(self)
449 }
450
451 /// Same as `exec_in_guest` but will return Ok(ProgramExit) instead of failing on a
452 /// non-zero exit code.
exec_in_guest_unchecked(&mut self, command: &str) -> Result<ProgramExit>453 pub fn exec_in_guest_unchecked(&mut self, command: &str) -> Result<ProgramExit> {
454 self.exec_in_guest_async(command)?.wait_result(self)
455 }
456
457 /// Executes the provided command in the guest asynchronously.
458 /// The command will be run in the guest, but output will not be read until
459 /// GuestProcess::wait_ok() or GuestProcess::wait_result() is called.
exec_in_guest_async(&mut self, command: &str) -> Result<GuestProcess>460 pub fn exec_in_guest_async(&mut self, command: &str) -> Result<GuestProcess> {
461 assert!(self.ready);
462 self.ready = false;
463
464 // Send command to guest
465 self.write_message_to_guest(
466 &HostToGuestMessage::RunCommand {
467 command: command.to_owned(),
468 },
469 COMMUNICATION_TIMEOUT,
470 )
471 .with_context(|| format!("Command `{}`: Failed to write to guest pipe", command))?;
472
473 Ok(GuestProcess {
474 command: command.to_owned(),
475 timeout: DEFAULT_COMMAND_TIMEOUT,
476 })
477 }
478
479 // Waits for the guest to be ready to receive commands
wait_for_guest_ready(&mut self, timeout: Duration) -> Result<()>480 fn wait_for_guest_ready(&mut self, timeout: Duration) -> Result<()> {
481 assert!(!self.ready);
482 let message: GuestToHostMessage = self.read_message_from_guest(timeout)?;
483 match message {
484 GuestToHostMessage::Ready => {
485 self.ready = true;
486 Ok(())
487 }
488 _ => Err(anyhow!("Recevied unexpected data from delegate")),
489 }
490 }
491
492 /// Reads one line via the `from_guest` pipe from the guest delegate.
read_message_from_guest(&mut self, timeout: Duration) -> Result<GuestToHostMessage>493 fn read_message_from_guest(&mut self, timeout: Duration) -> Result<GuestToHostMessage> {
494 let reader = self.sys.from_guest_reader.clone();
495
496 let result = run_with_timeout(
497 move || loop {
498 let message = { reader.lock().unwrap().next() };
499
500 if let Some(message_result) = message {
501 if let Ok(msg) = message_result {
502 match msg {
503 DelegateMessage::GuestToHost(guest_to_host) => {
504 return Ok(guest_to_host);
505 }
506 // Guest will send an echo of the message sent from host, ignore it
507 DelegateMessage::HostToGuest(_) => {
508 continue;
509 }
510 }
511 } else {
512 bail!(format!(
513 "Failed to receive message from guest: {:?}",
514 message_result.unwrap_err()
515 ))
516 };
517 };
518 },
519 timeout,
520 );
521 match result {
522 Ok(x) => {
523 self.ready = true;
524 x
525 }
526 Err(x) => Err(x),
527 }
528 }
529
530 /// Send one line via the `to_guest` pipe to the guest delegate.
write_message_to_guest( &mut self, data: &HostToGuestMessage, timeout: Duration, ) -> Result<()>531 fn write_message_to_guest(
532 &mut self,
533 data: &HostToGuestMessage,
534 timeout: Duration,
535 ) -> Result<()> {
536 let writer = self.sys.to_guest.clone();
537 let data_str = serde_json::to_string_pretty(&DelegateMessage::HostToGuest(data.clone()))?;
538 run_with_timeout(
539 move || -> Result<()> {
540 println!("-> {}", &data_str);
541 {
542 writeln!(writer.lock().unwrap(), "{}", &data_str)?;
543 }
544 Ok(())
545 },
546 timeout,
547 )?
548 }
549
550 /// Hotplug a tap device.
hotplug_tap(&mut self, tap_name: &str) -> Result<()>551 pub fn hotplug_tap(&mut self, tap_name: &str) -> Result<()> {
552 self.sys
553 .crosvm_command(
554 "virtio-net",
555 vec!["add".to_owned(), tap_name.to_owned()],
556 self.sudo,
557 )
558 .map(|_| ())
559 }
560
561 /// Remove hotplugged device on bus.
remove_pci_device(&mut self, bus_num: u8) -> Result<()>562 pub fn remove_pci_device(&mut self, bus_num: u8) -> Result<()> {
563 self.sys
564 .crosvm_command(
565 "virtio-net",
566 vec!["remove".to_owned(), bus_num.to_string()],
567 self.sudo,
568 )
569 .map(|_| ())
570 }
571
stop(&mut self) -> Result<()>572 pub fn stop(&mut self) -> Result<()> {
573 self.sys
574 .crosvm_command("stop", vec![], self.sudo)
575 .map(|_| ())
576 }
577
suspend(&mut self) -> Result<()>578 pub fn suspend(&mut self) -> Result<()> {
579 self.sys
580 .crosvm_command("suspend", vec![], self.sudo)
581 .map(|_| ())
582 }
583
suspend_full(&mut self) -> Result<()>584 pub fn suspend_full(&mut self) -> Result<()> {
585 self.sys
586 .crosvm_command("suspend", vec!["--full".to_string()], self.sudo)
587 .map(|_| ())
588 }
589
resume(&mut self) -> Result<()>590 pub fn resume(&mut self) -> Result<()> {
591 self.sys
592 .crosvm_command("resume", vec![], self.sudo)
593 .map(|_| ())
594 }
595
resume_full(&mut self) -> Result<()>596 pub fn resume_full(&mut self) -> Result<()> {
597 self.sys
598 .crosvm_command("resume", vec!["--full".to_string()], self.sudo)
599 .map(|_| ())
600 }
601
disk(&mut self, args: Vec<String>) -> Result<()>602 pub fn disk(&mut self, args: Vec<String>) -> Result<()> {
603 self.sys.crosvm_command("disk", args, self.sudo).map(|_| ())
604 }
605
snapshot(&mut self, filename: &std::path::Path) -> Result<()>606 pub fn snapshot(&mut self, filename: &std::path::Path) -> Result<()> {
607 self.sys
608 .crosvm_command(
609 "snapshot",
610 vec!["take".to_string(), String::from(filename.to_str().unwrap())],
611 self.sudo,
612 )
613 .map(|_| ())
614 }
615
616 // No argument is passed in restore as we will always restore snapshot.bkp for testing.
restore(&mut self, filename: &std::path::Path) -> Result<()>617 pub fn restore(&mut self, filename: &std::path::Path) -> Result<()> {
618 self.sys
619 .crosvm_command(
620 "snapshot",
621 vec![
622 "restore".to_string(),
623 String::from(filename.to_str().unwrap()),
624 ],
625 self.sudo,
626 )
627 .map(|_| ())
628 }
629
swap_command(&mut self, command: &str) -> Result<Vec<u8>>630 pub fn swap_command(&mut self, command: &str) -> Result<Vec<u8>> {
631 self.sys
632 .crosvm_command("swap", vec![command.to_string()], self.sudo)
633 }
634
guest_clock_values(&mut self) -> Result<ClockValues>635 pub fn guest_clock_values(&mut self) -> Result<ClockValues> {
636 let output = self
637 .exec_in_guest("readclock")
638 .context("Failed to execute readclock binary")?;
639 serde_json::from_str(&output.stdout).context("Failed to parse result")
640 }
641 }
642
643 impl Drop for TestVm {
drop(&mut self)644 fn drop(&mut self) {
645 self.stop().unwrap();
646 let status = self.sys.process.take().unwrap().wait().unwrap();
647 if !status.success() {
648 panic!("VM exited illegally: {}", status);
649 }
650 }
651 }
652