From a16ea84dd0436f5f9175bc2ecd4719e6b5c2fa55 Mon Sep 17 00:00:00 2001 From: Matej Hrica Date: Wed, 22 Jan 2025 18:14:47 +0100 Subject: [PATCH] Intruduce an E2E testing framework Add a tests directory containing an E2E testing framework. The tests are E2E in the sense that they use the public API of libkrun to start a VM and run a test program in the guest. Note that currently this is very limited, because there are no other userspace executables or libraries (not even libc) in the guest apart from `guest-agent`. `guest-agent` is a statically linked Rust executable, that executes the guest part of each test. In the future this can be extended to run tests with specific images which would allow the use of system libraries by the test program. This also introduces 2 tests: test_vm_config - asserts the VM is constructed with the correct number of CPUs and amount of memory. This also serves as an example how the tests can be parameterized. test_vsock_guest_connect - This tests the guest connecting to a vsock port created by the `krun_add_vsock_port` API. Signed-off-by: Matej Hrica --- tests/Cargo.lock | 578 ++++++++++++++++++ tests/Cargo.toml | 3 + tests/README.md | 9 + tests/guest-agent/Cargo.toml | 7 + tests/guest-agent/src/main.rs | 22 + tests/macros/Cargo.toml | 10 + tests/macros/src/lib.rs | 28 + tests/run.sh | 15 + tests/runner/Cargo.toml | 11 + tests/runner/src/main.rs | 132 ++++ tests/test_cases/Cargo.toml | 17 + tests/test_cases/src/common.rs | 52 ++ tests/test_cases/src/krun.rs | 25 + tests/test_cases/src/lib.rs | 117 ++++ tests/test_cases/src/test_vm_config.rs | 78 +++ .../src/test_vsock_guest_connect.rs | 112 ++++ 16 files changed, 1216 insertions(+) create mode 100644 tests/Cargo.lock create mode 100644 tests/Cargo.toml create mode 100644 tests/README.md create mode 100644 tests/guest-agent/Cargo.toml create mode 100644 tests/guest-agent/src/main.rs create mode 100644 tests/macros/Cargo.toml create mode 100644 tests/macros/src/lib.rs create mode 100755 tests/run.sh create mode 100644 tests/runner/Cargo.toml create mode 100644 tests/runner/src/main.rs create mode 100644 tests/test_cases/Cargo.toml create mode 100644 tests/test_cases/src/common.rs create mode 100644 tests/test_cases/src/krun.rs create mode 100644 tests/test_cases/src/lib.rs create mode 100644 tests/test_cases/src/test_vm_config.rs create mode 100644 tests/test_cases/src/test_vsock_guest_connect.rs diff --git a/tests/Cargo.lock b/tests/Cargo.lock new file mode 100644 index 0000000..dcf2c47 --- /dev/null +++ b/tests/Cargo.lock @@ -0,0 +1,578 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", +] + +[[package]] +name = "clap" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "guest-agent" +version = "0.0.0" +dependencies = [ + "anyhow", + "test_cases", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "krun-sys" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68484a58251533786a3c2aca3361664af68ac6240bf25c29f463f990b2e8238d" +dependencies = [ + "bindgen", + "pkg-config", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "macros" +version = "0.0.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "runner" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "macros", + "nix", + "tempdir", + "test_cases", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand", + "remove_dir_all", +] + +[[package]] +name = "test_cases" +version = "0.0.0" +dependencies = [ + "anyhow", + "krun-sys", + "macros", + "nix", + "tempdir", +] + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/tests/Cargo.toml b/tests/Cargo.toml new file mode 100644 index 0000000..4cc809b --- /dev/null +++ b/tests/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["runner", "guest-agent", "macros", "test_cases"] +resolver = "2" \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..bc61b9d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,9 @@ +# End-to-end tests +The testing framework here allows you to write code to configure libkrun (using the public API) and run some specific code in the guest. + +## Running the tests: +The tests can be ran using `make test` (from the main libkrun directory). +You can also run `./run.sh` inside the `test` directory. When using the `./run.sh` script you probably want specify the `PKG_CONFIG_PATH` enviroment variable, otherwise you will be testing the system wide installation of libkrun. + +## Adding tests +To add a test you need to add a new rust module in the `test_cases` directory, implement the required host and guest side methods (see existing tests) and register the test in the `test_cases/src/lib.rs` to be ran. \ No newline at end of file diff --git a/tests/guest-agent/Cargo.toml b/tests/guest-agent/Cargo.toml new file mode 100644 index 0000000..47617a0 --- /dev/null +++ b/tests/guest-agent/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "guest-agent" +edition = "2021" + +[dependencies] +test_cases = { path = "../test_cases", features = ["guest"] } +anyhow = "1.0.95" diff --git a/tests/guest-agent/src/main.rs b/tests/guest-agent/src/main.rs new file mode 100644 index 0000000..1f9b796 --- /dev/null +++ b/tests/guest-agent/src/main.rs @@ -0,0 +1,22 @@ +use anyhow::Context; +use std::env::args; +use test_cases::{test_cases, TestCase}; + +fn run_guest_agent(test_name: &str) -> anyhow::Result<()> { + let tests = test_cases(); + let test_case = tests + .into_iter() + .find(|t| t.name() == test_name) + .context("No such test!")?; + let TestCase { test, name: _ } = test_case; + test.in_guest(); + Ok(()) +} + +fn main() -> anyhow::Result<()> { + let mut cli_args = args(); + let _exec_name = cli_args.next(); + let test_name = cli_args.next().context("Missing test name argument")?; + run_guest_agent(&test_name)?; + Ok(()) +} diff --git a/tests/macros/Cargo.toml b/tests/macros/Cargo.toml new file mode 100644 index 0000000..e3afd34 --- /dev/null +++ b/tests/macros/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "macros" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = "2.0.96" +quote = "1.0.38" \ No newline at end of file diff --git a/tests/macros/src/lib.rs b/tests/macros/src/lib.rs new file mode 100644 index 0000000..67f1dab --- /dev/null +++ b/tests/macros/src/lib.rs @@ -0,0 +1,28 @@ +extern crate proc_macro; +extern crate quote; +extern crate syn; + +use proc_macro::TokenStream; +use quote::quote; + +#[proc_macro_attribute] +pub fn guest(_args: TokenStream, input: TokenStream) -> TokenStream { + let mut prefix: TokenStream = quote! { + #[cfg(feature = "guest")] + } + .into(); + + prefix.extend(input); + prefix.into() +} + +#[proc_macro_attribute] +pub fn host(_args: TokenStream, input: TokenStream) -> TokenStream { + let mut prefix: TokenStream = quote! { + #[cfg(feature = "host")] + } + .into(); + + prefix.extend(input); + prefix.into() +} diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..2b7062c --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,15 @@ +#/bin/sh + +# This script has to be run with the working directory being "test" +# This runs the tests on the libkrun instance found by pkg-config. +# Specify PKG_CONFIG_PATH env variable to test a non-system installation of libkurn. + +set -e + +# Run the unit tests first (this tests the testing framework itself not libkrun) +cargo test -p test_cases --features guest + +GUEST_TARGET_ARCH="$(uname -m)-unknown-linux-musl" + +cargo build --target=$GUEST_TARGET_ARCH -p guest-agent +KRUN_TEST_GUEST_AGENT_PATH="target/$GUEST_TARGET_ARCH/debug/guest-agent" cargo run -p runner "$@" diff --git a/tests/runner/Cargo.toml b/tests/runner/Cargo.toml new file mode 100644 index 0000000..3b85708 --- /dev/null +++ b/tests/runner/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "runner" +edition = "2021" + +[dependencies] +test_cases = { path = "../test_cases", features = ["host"] } +anyhow = "1.0.95" +nix = { version = "0.29.0", features = ["resource"] } +macros = { path = "../macros" } +clap = { version = "4.5.27", features = ["derive"] } +tempdir = "0.3.7" diff --git a/tests/runner/src/main.rs b/tests/runner/src/main.rs new file mode 100644 index 0000000..449b5d5 --- /dev/null +++ b/tests/runner/src/main.rs @@ -0,0 +1,132 @@ +use anyhow::Context; +use clap::Parser; +use nix::sys::resource::{getrlimit, setrlimit, Resource}; +use std::panic::catch_unwind; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::{env, mem}; +use tempdir::TempDir; +use test_cases::{test_cases, Test, TestCase, TestSetup}; + +fn get_test(name: &str) -> anyhow::Result> { + let tests = test_cases(); + tests + .into_iter() + .find(|t| t.name() == name) + .with_context(|| format!("No such test: {name}")) + .map(|t| t.test) +} + +fn start_vm(test_setup: TestSetup) -> anyhow::Result<()> { + // Raise soft fd limit up to the hard limit + let (_soft_limit, hard_limit) = + getrlimit(Resource::RLIMIT_NOFILE).context("getrlimit RLIMIT_NOFILE")?; + setrlimit(Resource::RLIMIT_NOFILE, hard_limit, hard_limit) + .context("setrlimit RLIMIT_NOFILE")?; + + let test = get_test(&test_setup.test_case)?; + test.start_vm(test_setup.clone()) + .with_context(|| format!("testcase: {test_setup:?}"))?; + Ok(()) +} + +fn run_single_test(test_case: &str) -> anyhow::Result { + let executable = env::current_exe().context("Failed to detect current executable")?; + let tmp_dir = + TempDir::new(&format!("krun-test-{test_case}")).context("Failed to create tmp dir")?; + + let child = Command::new(&executable) + .arg("start-vm") + .arg("--test-case") + .arg(test_case) + .arg("--tmp-dir") + .arg(tmp_dir.path()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("Failed to start subprocess for test")?; + + let _ = get_test(test_case)?; + let result = catch_unwind(|| { + let test = get_test(test_case).unwrap(); + test.check(child); + }); + + match result { + Ok(()) => { + println!("[{test_case}]: OK"); + Ok(true) + } + Err(_e) => { + println!("[{test_case}]: FAIL (dir {:?} kept)", tmp_dir.path()); + mem::forget(tmp_dir); + Ok(false) + } + } +} + +fn run_tests(test_case: &str) -> anyhow::Result<()> { + let mut num_tests = 1; + let mut num_ok: usize = 0; + + if test_case == "all" { + let test_cases = test_cases(); + num_tests = test_cases.len(); + + for TestCase { name, test: _ } in test_cases { + num_ok += run_single_test(name).context(name)? as usize; + } + } else { + num_ok += run_single_test(test_case).context(test_case.to_string())? as usize; + } + + let num_failures = num_tests - num_ok; + if num_failures > 0 { + println!("\nFAIL (PASSED {num_ok}/{num_tests})"); + anyhow::bail!("") + } else { + println!("\nOK (PASSED {num_ok}/{num_tests})"); + } + + Ok(()) +} + +#[derive(clap::Subcommand, Clone, Debug)] +enum CliCommand { + Test { + /// Specify which test to run or "all" + #[arg(long, default_value = "all")] + test_case: String, + }, + StartVm { + #[arg(long)] + test_case: String, + #[arg(long)] + tmp_dir: PathBuf, + }, +} + +impl Default for CliCommand { + fn default() -> Self { + Self::Test { + test_case: "all".to_string(), + } + } +} + +#[derive(clap::Parser)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + let command = cli.command.unwrap_or_default(); + + match command { + CliCommand::StartVm { test_case, tmp_dir } => start_vm(TestSetup { test_case, tmp_dir }), + CliCommand::Test { test_case } => run_tests(&test_case), + } +} diff --git a/tests/test_cases/Cargo.toml b/tests/test_cases/Cargo.toml new file mode 100644 index 0000000..bc57fb0 --- /dev/null +++ b/tests/test_cases/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "test_cases" +edition = "2021" + +[features] +host = ["krun-sys"] +guest = [] + +[lib] +name = "test_cases" + +[dependencies] +krun-sys = { version = "1.10.1", optional = true } +macros = { path = "../macros" } +nix = { version = "0.29.0", features = ["socket"] } +anyhow = "1.0.95" +tempdir = "0.3.7" \ No newline at end of file diff --git a/tests/test_cases/src/common.rs b/tests/test_cases/src/common.rs new file mode 100644 index 0000000..6a3ee24 --- /dev/null +++ b/tests/test_cases/src/common.rs @@ -0,0 +1,52 @@ +//! Common utilities used by multiple test + +use anyhow::Context; +use std::ffi::CString; +use std::fs; +use std::fs::create_dir; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::ptr::null; + +use crate::{krun_call, TestSetup}; +use krun_sys::*; + +fn copy_guest_agent(dir: &Path) -> anyhow::Result<()> { + let path = std::env::var_os("KRUN_TEST_GUEST_AGENT_PATH") + .context("KRUN_TEST_GUEST_AGENT_PATH env variable not set")?; + + let output_path = dir.join("guest-agent"); + fs::copy(path, output_path).context("Failed to copy executable into vm")?; + Ok(()) +} + +/// Common part of most test. This setups an empty root filesystem, copies the guest agent there +/// and runs the guest agent in the VM. +/// Note that some tests might want to use a different root file system (perhaps a qcow image), +/// in which case the test can implement the equivalent functionality itself, or better if there +/// are more test doing that, add another utility method in this file. +/// +/// The returned object is used for deleting the temporary files. +pub fn setup_fs_and_enter(ctx: u32, test_setup: TestSetup) -> anyhow::Result<()> { + let root_dir = test_setup.tmp_dir.join("root"); + create_dir(&root_dir).context("Failed to create root directory")?; + + let path_str = CString::new(root_dir.as_os_str().as_bytes()).context("CString::new")?; + copy_guest_agent(&root_dir)?; + unsafe { + krun_call!(krun_set_root(ctx, path_str.as_ptr()))?; + krun_call!(krun_set_workdir(ctx, c"/".as_ptr()))?; + let test_case_cstr = CString::new(test_setup.test_case).context("CString::new")?; + let argv = [test_case_cstr.as_ptr(), null()]; + //let envp = [c"RUST_BACKTRACE=1".as_ptr(), null()]; + let envp = [null()]; + krun_call!(krun_set_exec( + ctx, + c"/guest-agent".as_ptr(), + argv.as_ptr(), + envp.as_ptr(), + ))?; + krun_call!(krun_start_enter(ctx))?; + } + unreachable!() +} diff --git a/tests/test_cases/src/krun.rs b/tests/test_cases/src/krun.rs new file mode 100644 index 0000000..f975215 --- /dev/null +++ b/tests/test_cases/src/krun.rs @@ -0,0 +1,25 @@ +#[macro_export] +macro_rules! krun_call { + ($func_call:expr) => {{ + let result = $func_call; + if result < 0 { + let err = std::io::Error::from_raw_os_error(-result); + Err(anyhow::anyhow!("`{}`: {}", stringify!($func_call), err)) + } else { + Ok::<(), anyhow::Error>(()) + } + }}; +} + +#[macro_export] +macro_rules! krun_call_u32 { + ($func_call:expr) => {{ + let result = $func_call; + if result < 0 { + let err = std::io::Error::from_raw_os_error(-result); + Err(anyhow::anyhow!("`{}`: {}", stringify!($func_call), err)) + } else { + Ok::(result as u32) + } + }}; +} diff --git a/tests/test_cases/src/lib.rs b/tests/test_cases/src/lib.rs new file mode 100644 index 0000000..1b4d402 --- /dev/null +++ b/tests/test_cases/src/lib.rs @@ -0,0 +1,117 @@ +mod test_vm_config; +use test_vm_config::TestVmConfig; + +mod test_vsock_guest_connect; +use test_vsock_guest_connect::TestVsockGuestConnect; + +pub fn test_cases() -> Vec { + // Register your test here: + vec![ + TestCase::new( + "configure-vm-1cpu-256MiB", + Box::new(TestVmConfig { + num_cpus: 1, + ram_mib: 256, + }), + ), + TestCase::new( + "configure-vm-2cpu-1GiB", + Box::new(TestVmConfig { + num_cpus: 2, + ram_mib: 1024, + }), + ), + TestCase::new("vsock-guest-connect", Box::new(TestVsockGuestConnect)), + ] +} + +//////////////////// +// Implementation details: +////////////////// +use macros::{guest, host}; +#[host] +use std::path::PathBuf; +#[host] +use std::process::Child; + +#[cfg(all(feature = "guest", feature = "host"))] +compile_error!("Cannot enable both guest and host in the same binary!"); + +#[cfg(feature = "host")] +mod common; + +#[cfg(feature = "host")] +mod krun; + +#[host] +#[derive(Clone, Debug)] +pub struct TestSetup { + pub test_case: String, + // A tmp directory for misc. artifacts used be the test (e.g. sockets) + pub tmp_dir: PathBuf, +} + +#[host] +pub trait Test { + /// Start the VM + fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()>; + + /// Checks the output of the (host) process which started the VM + fn check(self: Box, child: Child) { + let output = child.wait_with_output().unwrap(); + let err = String::from_utf8(output.stderr).unwrap(); + if !err.is_empty() { + eprintln!("{}", err); + } + + assert_eq!(String::from_utf8(output.stdout).unwrap(), "OK\n"); + } +} + +#[guest] +pub trait Test { + /// This will be executed in the guest, you can panic! if the test failed! + fn in_guest(self: Box) {} +} + +pub struct TestCase { + pub name: &'static str, + pub test: Box, +} + +impl TestCase { + // Your test can be parametrized, so you can add the same test multiple times constructed with + // different parameters with and specify a different name here. + pub fn new(name: &'static str, test: Box) -> Self { + Self { name, test } + } + + #[allow(dead_code)] + pub fn name(&self) -> &'static str { + self.name + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + #[test] + fn all_testcases_have_unique_names() { + let test_cases = test_cases(); + let mut names: HashSet<&str> = HashSet::new(); + + for test_case in test_cases { + let name = test_case.name(); + let was_inserted = names.insert(name); + if !was_inserted { + panic!("test_cases() contains multiple items named `{name}`") + } + + if name == "all" { + panic!("test_cases() contains test named {name}, but the name is reseved") + } + } + } +} diff --git a/tests/test_cases/src/test_vm_config.rs b/tests/test_cases/src/test_vm_config.rs new file mode 100644 index 0000000..ed891e8 --- /dev/null +++ b/tests/test_cases/src/test_vm_config.rs @@ -0,0 +1,78 @@ +use macros::{guest, host}; + +pub struct TestVmConfig { + pub(crate) num_cpus: u8, + pub(crate) ram_mib: u32, +} + +#[host] +mod host { + use super::*; + + use crate::common::setup_fs_and_enter; + use crate::{krun_call, krun_call_u32}; + use crate::{Test, TestSetup}; + use krun_sys::*; + + impl Test for TestVmConfig { + fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { + unsafe { + let ctx = krun_call_u32!(krun_create_ctx())?; + krun_call!(krun_set_vm_config(ctx, self.num_cpus, self.ram_mib))?; + setup_fs_and_enter(ctx, test_setup)?; + } + Ok(()) + } + } +} + +#[guest] +mod guest { + use super::*; + use crate::Test; + use std::fs; + use std::fs::File; + use std::io::{BufRead, BufReader}; + use std::str::FromStr; + + fn detect_num_cpus() -> u32 { + let cpus = fs::read_to_string("/sys/devices/system/cpu/online").unwrap(); + let mut parts = cpus.split("-"); + let low = u32::from_str(parts.next().unwrap().trim()).unwrap(); + if let Some(high) = parts.next() { + let high = u32::from_str(high.trim()).unwrap(); + high - low + 1 + } else { + low + 1 + } + } + + fn detect_ram_size_mib() -> u32 { + let file = BufReader::new(File::open("/proc/meminfo").unwrap()); + + for line in file.lines() { + let line = line.expect("Could not read line"); + if line.starts_with("MemTotal:") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + let size_kb: u32 = parts[1].trim().parse().unwrap(); + let size_mib = size_kb / 1024; + return size_mib; + } + } + } + panic!("MemTotal field not found"); + } + + impl Test for TestVmConfig { + fn in_guest(self: Box) { + assert_eq!(detect_num_cpus(), self.num_cpus as u32); + + let ram_available = detect_ram_size_mib(); + // Check if ram is within 15% of specified + assert!(self.ram_mib >= (ram_available as f64 * 0.85) as u32); + assert!(self.ram_mib <= (ram_available as f64 * 1.15) as u32); + println!("OK"); + } + } +} diff --git a/tests/test_cases/src/test_vsock_guest_connect.rs b/tests/test_cases/src/test_vsock_guest_connect.rs new file mode 100644 index 0000000..9b66c6a --- /dev/null +++ b/tests/test_cases/src/test_vsock_guest_connect.rs @@ -0,0 +1,112 @@ +use macros::{guest, host}; +use std::io::{ErrorKind, Read}; +use std::os::unix::net::UnixStream; +use std::time::Duration; + +pub struct TestVsockGuestConnect; + +fn stream_expect_msg(stream: &mut UnixStream, expected: &[u8]) { + let mut buf = vec![0; expected.len()]; + stream.read_exact(&mut buf[..]).unwrap(); + assert_eq!(&buf[..], expected); +} + +fn stream_expect_wouldblock(stream: &mut UnixStream) { + stream.set_nonblocking(true).unwrap(); + let err = stream.read(&mut [0u8; 1]).unwrap_err(); + stream.set_nonblocking(false).unwrap(); + assert_eq!(err.kind(), ErrorKind::WouldBlock); +} + +fn stream_set_timeouts(stream: &mut UnixStream) { + stream + .set_read_timeout(Some(Duration::from_secs(3))) + .unwrap(); + stream + .set_write_timeout(Some(Duration::from_secs(3))) + .unwrap(); +} + +const VSOCK_PORT: u32 = 1234; + +#[host] +mod host { + use super::*; + + use crate::common::setup_fs_and_enter; + use crate::{krun_call, krun_call_u32}; + use crate::{Test, TestSetup}; + use krun_sys::*; + use std::ffi::CString; + use std::io::Write; + use std::os::unix::net::UnixListener; + use std::os::unix::prelude::OsStrExt; + use std::{mem, thread}; + + fn server(listener: UnixListener) { + let (mut stream, _addr) = listener.accept().unwrap(); + stream_set_timeouts(&mut stream); + stream.write_all(b"ping!").unwrap(); + stream_expect_msg(&mut stream, b"pong!"); + stream_expect_wouldblock(&mut stream); + stream.write_all(b"bye!").unwrap(); + // Leak the socket fd, to make sure it is not closed early when we exit the thread + mem::forget(stream); + } + + impl Test for TestVsockGuestConnect { + fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { + let sock_path = test_setup.tmp_dir.join("test.sock"); + let sock_path_cstr = CString::new(sock_path.as_os_str().as_bytes())?; + + let listener = UnixListener::bind(&sock_path).unwrap(); + + thread::spawn(move || server(listener)); + unsafe { + let ctx = krun_call_u32!(krun_create_ctx())?; + krun_call!(krun_add_vsock_port( + ctx, + VSOCK_PORT, + sock_path_cstr.as_ptr() + ))?; + krun_call!(krun_set_vm_config(ctx, 1, 1024))?; + setup_fs_and_enter(ctx, test_setup)?; + } + Ok(()) + } + } +} + +#[guest] +mod guest { + use super::*; + use crate::Test; + + use nix::libc::VMADDR_CID_HOST; + use nix::sys::socket::{connect, socket, AddressFamily, SockFlag, SockType, VsockAddr}; + use std::io::Write; + use std::os::fd::AsRawFd; + + impl Test for TestVsockGuestConnect { + fn in_guest(self: Box) { + let sock = socket( + AddressFamily::Vsock, + SockType::Stream, + SockFlag::empty(), + None, + ) + .unwrap(); + let addr = VsockAddr::new(VMADDR_CID_HOST, VSOCK_PORT); + connect(sock.as_raw_fd(), &addr).unwrap(); + let mut stream = UnixStream::from(sock); + stream_set_timeouts(&mut stream); + + stream_expect_msg(&mut stream, b"ping!"); + stream_expect_wouldblock(&mut stream); + stream.write_all(b"pong!").unwrap(); + stream_expect_msg(&mut stream, b"bye!"); + + println!("OK"); + } + } +} -- 2.51.2