diff --git a/Cargo.lock b/Cargo.lock index 7c94fce22d..10d81c2a0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5042,18 +5042,13 @@ dependencies = [ [[package]] name = "runtime-tests" -version = "0.1.0" +version = "2.2.0-pre0" dependencies = [ "anyhow", "env_logger", - "fslock", "log", - "nix 0.26.4", - "regex", "reqwest", - "temp-dir", - "test-components", - "toml 0.8.8", + "testing-framework", ] [[package]] @@ -5736,6 +5731,7 @@ dependencies = [ "terminal", "test-codegen-macro", "test-components", + "testing-framework", "tokio", "toml 0.6.0", "tracing", @@ -6589,6 +6585,21 @@ dependencies = [ "wit-component 0.19.0", ] +[[package]] +name = "testing-framework" +version = "0.1.0" +dependencies = [ + "anyhow", + "fslock", + "log", + "nix 0.26.4", + "regex", + "reqwest", + "temp-dir", + "test-components", + "toml 0.8.8", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index b537c5391d..180ef6856d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ sha2 = "0.10.1" which = "4.2.5" e2e-testing = { path = "crates/e2e-testing" } http-body-util = { workspace = true } +testing-framework = { path = "tests/testing-framework" } runtime-tests = { path = "tests/runtime-tests" } test-components = { path = "tests/test-components" } test-codegen-macro = { path = "crates/test-codegen-macro" } @@ -114,7 +115,13 @@ llm-metal = ["llm", "spin-trigger-http/llm-metal"] llm-cublas = ["llm", "spin-trigger-http/llm-cublas"] [workspace] -members = ["crates/*", "sdk/rust", "sdk/rust/macro", "tests/runtime-tests"] +members = [ + "crates/*", + "sdk/rust", + "sdk/rust/macro", + "tests/runtime-tests", + "tests/testing-framework", +] [workspace.dependencies] anyhow = "1.0.75" diff --git a/build.rs b/build.rs index 377a704d7d..69924f5e06 100644 --- a/build.rs +++ b/build.rs @@ -7,7 +7,6 @@ use std::{ use cargo_target_dep::build_target_dep; -const RUST_HTTP_INTEGRATION_TEST: &str = "tests/http/simple-spin-rust"; const RUST_HTTP_VAULT_VARIABLES_TEST: &str = "tests/http/vault-variables-test"; const TIMER_TRIGGER_INTEGRATION_TEST: &str = "examples/spin-timer/app-example"; const WASI_HTTP_INTEGRATION_TEST: &str = "examples/wasi-http-rust-streaming-outgoing-body"; @@ -86,7 +85,6 @@ error: the `wasm32-wasi` target is not installed ); build_wasm_test_program("timer_app_example.wasm", "examples/spin-timer/app-example"); - cargo_build(RUST_HTTP_INTEGRATION_TEST); cargo_build(RUST_HTTP_VAULT_VARIABLES_TEST); cargo_build(TIMER_TRIGGER_INTEGRATION_TEST); cargo_build(WASI_HTTP_INTEGRATION_TEST); diff --git a/crates/test-codegen-macro/src/lib.rs b/crates/test-codegen-macro/src/lib.rs index 37770213aa..62e81d03b1 100644 --- a/crates/test-codegen-macro/src/lib.rs +++ b/crates/test-codegen-macro/src/lib.rs @@ -4,10 +4,14 @@ use std::{env, path::PathBuf}; /// This macro generates the `#[test]` functions for the runtime tests. #[proc_macro] -pub fn codegen_tests(_input: TokenStream) -> TokenStream { +pub fn codegen_runtime_tests(_input: TokenStream) -> TokenStream { let mut tests = Vec::new(); let tests_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/runtime-tests/tests"); + let tests_path_string = tests_path + .to_str() + .expect("CARGO_MANIFEST_DIR is not valid utf8") + .to_owned(); for entry in std::fs::read_dir(tests_path).expect("failed to read tests directory") { let entry = entry.expect("error reading test directory entry"); let test = entry.path(); @@ -33,7 +37,7 @@ pub fn codegen_tests(_input: TokenStream) -> TokenStream { #[test] #feature_attribute fn #ident() { - run(#name) + run(::std::path::PathBuf::from(#tests_path_string).join(#name)) } }); } diff --git a/tests/http/simple-spin-rust/.cargo/config.toml b/tests/http/simple-spin-rust/.cargo/config.toml deleted file mode 100644 index 6b77899cb3..0000000000 --- a/tests/http/simple-spin-rust/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "wasm32-wasi" diff --git a/tests/http/simple-spin-rust/Cargo.lock b/tests/http/simple-spin-rust/Cargo.lock deleted file mode 100644 index 08580619e7..0000000000 --- a/tests/http/simple-spin-rust/Cargo.lock +++ /dev/null @@ -1,586 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "anyhow" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" - -[[package]] -name = "async-trait" -version = "0.1.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "bytes" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-executor" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "futures-sink" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" - -[[package]] -name = "futures-task" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" - -[[package]] -name = "futures-util" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "hashbrown" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "indexmap" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" -dependencies = [ - "equivalent", - "hashbrown", - "serde", -] - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - -[[package]] -name = "memchr" -version = "2.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "pin-project-lite" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "proc-macro2" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "routefinder" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f8f99b10dedd317514253dda1fa7c14e344aac96e1f78149a64879ce282aca" -dependencies = [ - "smartcow", - "smartstring", -] - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "semver" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" - -[[package]] -name = "serde" -version = "1.0.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "serde_json" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "simple-spin-rust" -version = "0.1.0" -dependencies = [ - "anyhow", - "http", - "spin-sdk", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" - -[[package]] -name = "smartcow" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" -dependencies = [ - "smartstring", -] - -[[package]] -name = "smartstring" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] - -[[package]] -name = "spdx" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b19b32ed6d899ab23174302ff105c1577e45a06b08d4fe0a9dd13ce804bbbf71" -dependencies = [ - "smallvec", -] - -[[package]] -name = "spin-macro" -version = "2.2.0-pre0" -dependencies = [ - "anyhow", - "bytes", - "http", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "spin-sdk" -version = "2.2.0-pre0" -dependencies = [ - "anyhow", - "async-trait", - "bytes", - "form_urlencoded", - "futures", - "http", - "once_cell", - "routefinder", - "serde", - "serde_json", - "spin-macro", - "thiserror", - "wit-bindgen", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasm-encoder" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca90ba1b5b0a70d3d49473c5579951f3bddc78d47b59256d2f9d4922b150aca" -dependencies = [ - "leb128", -] - -[[package]] -name = "wasm-metadata" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14abc161bfda5b519aa229758b68f2a52b45a12b993808665c857d1a9a00223c" -dependencies = [ - "anyhow", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.115.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e06c0641a4add879ba71ccb3a1e4278fd546f76f1eafb21d8f7b07733b547cd5" -dependencies = [ - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d92ce0ca6b6074059413a9581a637550c3a740581c854f9847ec293c8aed71" -dependencies = [ - "bitflags", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565b945ae074886071eccf9cdaf8ccd7b959c2b0d624095bea5fe62003e8b3e0" -dependencies = [ - "anyhow", - "wit-component", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5695ff4e41873ed9ce56d2787e6b5772bdad9e70e2c1d2d160621d1762257f4f" -dependencies = [ - "anyhow", - "heck", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91835ea4231da1fe7971679d505ba14be7826e192b6357f08465866ef482e08" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.38", - "wit-bindgen-core", - "wit-bindgen-rust", - "wit-component", -] - -[[package]] -name = "wit-component" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87488b57a08e2cbbd076b325acbe7f8666965af174d69d5929cd373bd54547f" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ace9943d89bbf3dbbc71b966da0e7302057b311f36a4ac3d65ddfef17b52cf" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", -] diff --git a/tests/http/simple-spin-rust/Cargo.toml b/tests/http/simple-spin-rust/Cargo.toml deleted file mode 100644 index 923c6d210c..0000000000 --- a/tests/http/simple-spin-rust/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "simple-spin-rust" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -anyhow = "1" -http = "0.2" -spin-sdk = { path = "../../../sdk/rust" } - -[workspace] - -# Metadata about this component. -[package.metadata.component] -name = "spinhelloworld" diff --git a/tests/http/simple-spin-rust/double-trouble.toml b/tests/http/simple-spin-rust/double-trouble.toml index b42f8181c5..64ef2a198f 100644 --- a/tests/http/simple-spin-rust/double-trouble.toml +++ b/tests/http/simple-spin-rust/double-trouble.toml @@ -16,7 +16,7 @@ component = "hello" # intentionally pointing to same component object = { default = "teapot" } [component.hello] -source = "target/wasm32-wasi/release/simple_spin_rust.wasm" +source = "%{source=integration-simple}" files = [{ source = "assets", destination = "/" }] [component.hello.variables] message = "I'm a {{object}}" diff --git a/tests/http/simple-spin-rust/spin.toml b/tests/http/simple-spin-rust/spin.toml index 6fd14fe29d..e069317e11 100644 --- a/tests/http/simple-spin-rust/spin.toml +++ b/tests/http/simple-spin-rust/spin.toml @@ -10,7 +10,7 @@ object = { default = "teapot" } [[component]] id = "hello" -source = "target/wasm32-wasi/release/simple_spin_rust.wasm" +source = "%{source=integration-simple}" files = [{ source = "assets", destination = "/" }] [component.trigger] route = "/hello/..." diff --git a/tests/integration.rs b/tests/integration.rs index 8bc7504ed1..a60308c8ce 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -4,7 +4,7 @@ mod integration_tests { use futures::{channel::oneshot, future, stream, FutureExt}; use http_body_util::BodyExt; use hyper::{body::Bytes, server::conn::http1, service::service_fn, Method, StatusCode}; - use reqwest::{Client, Response}; + use reqwest::Client; use sha2::{Digest, Sha256}; use spin_http::body; use spin_manifest::schema::v2; @@ -13,7 +13,7 @@ mod integration_tests { ffi::OsStr, iter, net::{Ipv4Addr, SocketAddrV4, TcpListener}, - path::Path, + path::{Path, PathBuf}, process::{self, Child, Command, Output}, sync::{Arc, Mutex}, time::Duration, @@ -30,51 +30,39 @@ mod integration_tests { const DEFAULT_MANIFEST_LOCATION: &str = "spin.toml"; fn spin_binary() -> String { - format!("{}/debug/spin", target_dir()) + env!("CARGO_BIN_EXE_spin").into() } - fn target_dir() -> String { - match std::env::var_os("CARGO_TARGET_DIR") { - Some(d) => d - .to_str() - .expect("CARGO_TARGET_DIR is not utf-8") - .to_owned(), - None => "./target".into(), - } - } - - #[tokio::test] - async fn test_simple_rust_local() -> Result<()> { - let s = SpinTestController::with_manifest( - &format!( + #[test] + fn test_simple_rust_local() -> Result<()> { + integration_test( + format!( "{}/{}", RUST_HTTP_INTEGRATION_TEST, DEFAULT_MANIFEST_LOCATION ), - &[], - &[], - ) - .await?; - - assert_status(&s, "/test/hello", 200).await?; - assert_status(&s, "/test/hello/wildcards/should/be/handled", 200).await?; - assert_status(&s, "/thisshouldfail", 404).await?; - assert_status(&s, "/test/hello/test-placement", 200).await?; + |spin| { + assert_spin_status(spin, "/test/hello", 200)?; + assert_spin_status(spin, "/test/hello/wildcards/should/be/handled", 200)?; + assert_spin_status(spin, "/thisshouldfail", 404)?; + assert_spin_status(spin, "/test/hello/test-placement", 200)?; + Ok(()) + }, + )?; Ok(()) } - #[tokio::test] - async fn test_duplicate_rust_local() -> Result<()> { - let s = SpinTestController::with_manifest( - &format!("{}/{}", RUST_HTTP_INTEGRATION_TEST, "double-trouble.toml"), - &[], - &[], - ) - .await?; - - assert_status(&s, "/route1", 200).await?; - assert_status(&s, "/route2", 200).await?; - assert_status(&s, "/thisshouldfail", 404).await?; + #[test] + fn test_duplicate_rust_local() -> Result<()> { + integration_test( + format!("{}/{}", RUST_HTTP_INTEGRATION_TEST, "double-trouble.toml"), + |spin| { + assert_spin_status(spin, "/route1", 200)?; + assert_spin_status(spin, "/route2", 200)?; + assert_spin_status(spin, "/thisshouldfail", 404)?; + Ok(()) + }, + )?; Ok(()) } @@ -252,12 +240,59 @@ mod integration_tests { } } + fn integration_test( + manifest_path: impl Into, + test: impl FnOnce(&mut testing_framework::Spin) -> testing_framework::TestResult + + 'static, + ) -> anyhow::Result<()> { + let manifest_path = manifest_path.into(); + let spin = testing_framework::TestEnvironmentConfig::spin( + spin_binary().into(), + move |env| { + // Copy manifest + let mut template = testing_framework::ManifestTemplate::from_file(manifest_path)?; + template.substitute(env)?; + env.write_file(DEFAULT_MANIFEST_LOCATION, template.contents())?; + + // Copy assets directory + let assets_path = format!("{}/assets", RUST_HTTP_INTEGRATION_TEST); + env.copy_into(assets_path, "assets")?; + Ok(()) + }, + testing_framework::ServicesConfig::none(), + ); + let mut env = testing_framework::TestEnvironment::up(spin)?; + Ok(env.test(test)?) + } + + fn assert_spin_status( + spin: &mut testing_framework::Spin, + uri: &str, + status: u16, + ) -> testing_framework::TestResult { + let r = spin.make_http_request(reqwest::Method::GET, uri)?; + if r.status() != status { + return Err(testing_framework::TestError::Failure(anyhow!( + "Expected status {} for {} but got {}", + status, + uri, + r.status() + ))); + } + Ok(()) + } + + #[cfg(feature = "config-provider-tests")] async fn assert_status( s: &SpinTestController, absolute_uri: &str, expected: u16, ) -> Result<()> { - let res = req(s, absolute_uri).await?; + let res = Client::new() + .get(format!("http://{}{}", s.url, absolute_uri)) + .send() + .await?; + let status = res.status(); let body = res.bytes().await?; assert_eq!(status, expected, "{}", String::from_utf8_lossy(&body)); @@ -265,13 +300,6 @@ mod integration_tests { Ok(()) } - async fn req(s: &SpinTestController, absolute_uri: &str) -> Result { - Ok(Client::new() - .get(format!("http://{}{}", s.url, absolute_uri)) - .send() - .await?) - } - /// Controller for running Spin. pub struct SpinTestController { pub url: String, diff --git a/tests/runtime-tests/Cargo.toml b/tests/runtime-tests/Cargo.toml index 5576ad2622..262ca4ae59 100644 --- a/tests/runtime-tests/Cargo.toml +++ b/tests/runtime-tests/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "runtime-tests" -version = "0.1.0" -edition = "2021" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +rust-version.workspace = true [dependencies] -anyhow = { workspace = true } +anyhow = "1.0" env_logger = "0.10.0" -fslock = "0.2.1" log = "0.4" -nix = "0.26.1" -regex = "1.10.2" reqwest = { workspace = true } -temp-dir = "0.1.11" -test-components = { path = "../test-components" } -toml = "0.8.6" +testing-framework = { path = "../testing-framework" } diff --git a/tests/runtime-tests/README.md b/tests/runtime-tests/README.md index da0fa7f83a..0f776e7446 100644 --- a/tests/runtime-tests/README.md +++ b/tests/runtime-tests/README.md @@ -1,13 +1,13 @@ -# Runtime Tests +# Runtime tests -The runtime tests ensure that Spin can properly run applications. +Runtime tests are a specific type of test for testing the runtime behavior of a Spin compliant runtime. For the purposes of these tests, an "application" is a collection of the following things: * A Spin compliant WebAssembly binary * A spin.toml manifest * Optional runtime-config.toml files -## What do runtime tests supposed test and not test? +## What are runtime tests supposed test and not test? Runtime tests are meant to test the runtime functionality of Spin. In other words, they ensure that a valid combination of Spin manifest and some number of Spin compliant WebAssembly binaries perform in expected ways or fail in expected ways. @@ -37,38 +37,8 @@ The test directory may additionally contain: The test runner will make a GET request against the `/` path. The component should either return a 200 if everything goes well or a 500 if there is an error. If an `error.txt` file is present, the Spin application must return a 500 with the body set to some error message that contains the contents of `error.txt`. -### Services - -Services allow for tests to be run against external sources. The service definitions can be found in the 'services' directory. Each test directory contains a 'services' file that configures the tests services. Each line of the services file should contain the name of a services file that needs to run. For example, the following 'services' file will run the `tcp-echo.py` service: - -```txt -tcp-echo -``` - -Each service is run under a file lock meaning that all other tests that require that service must wait until the current test using that service has finished. - -The following service types are supported: -* Python services (a python script ending in the .py file extension) -* Docker services (a docker file ending in the .Dockerfile extension) - -When looking to add a new service, always prefer the Python based service as it's generally much quicker and lighter weight to run a Python script than a Docker container. Only use Docker when the service you require is not possible to achieve in cross platform way as a Python script. - -### Signaling Service Readiness - -Services can signal that they are ready so that tests aren't run against them until they are ready: - -* Python: Python services signal they are ready by printing `READY` to stdout. -* Docker: Docker services signal readiness by exposing a Docker health check in the Dockerfile (e.g., `HEALTHCHECK --start-period=4s --interval=1s CMD /usr/bin/mysqladmin ping --silent`) - -### Exposing Ports - -Both Docker and Python based services can expose some logical port number that will be mapped to a random free port number at runtime. - -* Python: Python based services can do this by printing `PORT=($PORT1, $PORT2)` to stdout where the $PORT1 is the logical port the service exposes and $PORT2 is the random port actually being exposed (e.g., `PORT=(80, 59392)`) -* Docker: Docker services can do this by exposing the port in their Dockerfile (e.g., `EXPOSE 3306`) - ## When do tests pass? A test will pass in the following conditions: * The Spin web server returns a 200 -* The Spin web server returns a 500 with a body that contains the same text inside of the test's error.txt file. +* The Spin web server returns a 500 with a body that contains the same text inside of the test's error.txt file. \ No newline at end of file diff --git a/tests/runtime-tests/src/lib.rs b/tests/runtime-tests/src/lib.rs index 4112dcb653..4a9add1862 100644 --- a/tests/runtime-tests/src/lib.rs +++ b/tests/runtime-tests/src/lib.rs @@ -1,212 +1,204 @@ -pub(crate) mod io; -mod services; -pub mod spin; - -use std::{ - path::{Path, PathBuf}, - sync::OnceLock, -}; - use anyhow::Context; -use services::Services; - -/// A callback to create a runtime given a path to a temporary directory and a set of services -pub type RuntimeCreator = dyn Fn(&Path) -> anyhow::Result>; +use std::path::{Path, PathBuf}; +use testing_framework::{ + ManifestTemplate, OnTestError, ServicesConfig, Spin, TestEnvironment, TestEnvironmentConfig, + TestError, TestResult, +}; -/// Configuration for the test suite -pub struct Config { - /// The runtime under test - pub create_runtime: Box, - /// The path to the tests directory which contains all the runtime test definitions - pub tests_path: PathBuf, - /// What to do when an individual test fails +/// Configuration for a runtime test +pub struct RuntimeTestConfig { + pub test_path: PathBuf, + pub spin_binary: PathBuf, pub on_error: OnTestError, } -#[derive(Debug, Clone, Copy)] -/// What to do on a test error -pub enum OnTestError { - Panic, - Log, +/// A single runtime test +pub struct RuntimeTest { + test_path: PathBuf, + on_error: OnTestError, + env: TestEnvironment, } -/// Run the runtime tests suite. -/// -/// Error represents an error in bootstrapping the tests. What happens on individual test failures -/// is controlled by `config`. -pub fn run_all(config: Config) -> anyhow::Result<()> { - for test in std::fs::read_dir(&config.tests_path).with_context(|| { - format!( - "failed to read test directory '{}'", - config.tests_path.display() - ) - })? { - let test = test.context("I/O error reading entry from test directory")?; - if !test.file_type()?.is_dir() { - log::debug!( - "Ignoring non-sub-directory in test directory: {}", - test.path().display() - ); - continue; - } +impl RuntimeTest { + /// Run the runtime tests suite. + /// + /// Error represents an error in bootstrapping the tests. What happens on individual test failures + /// is controlled by `on_error`. + pub fn run_all( + tests_path: &Path, + spin_binary: PathBuf, + on_error: OnTestError, + ) -> anyhow::Result<()> { + for test in std::fs::read_dir(tests_path) + .with_context(|| format!("failed to read test directory '{}'", tests_path.display()))? + { + let test = test.context("I/O error reading entry from test directory")?; + if !test.file_type()?.is_dir() { + log::debug!( + "Ignoring non-sub-directory in test directory: {}", + test.path().display() + ); + continue; + } - bootstrap_and_run(&test.path(), &config)?; + let config = RuntimeTestConfig { + test_path: test.path(), + spin_binary: spin_binary.clone(), + on_error, + }; + RuntimeTest::bootstrap(config)?.run(); + } + Ok(()) } - Ok(()) -} - -/// Bootstrap and run a test at a path against the given config -pub fn bootstrap_and_run(test_path: &Path, config: &Config) -> anyhow::Result<()> { - log::info!("Testing: {}", test_path.display()); - let temp = temp_dir::TempDir::new() - .context("failed to produce a temporary directory to run the test in")?; - log::trace!("Temporary directory: {}", temp.path().display()); - let mut services = services::start_services(test_path)?; - copy_manifest(test_path, &temp, &mut services)?; - services.error().context("services have failed")?; - let runtime = &mut *(config.create_runtime)(temp.path())?; - services.error().context("services have failed")?; - run_test(runtime, test_path, config.on_error); - Ok(()) -} -pub trait Runtime { - fn test(&mut self) -> anyhow::Result; -} - -/// Run an individual test -fn run_test(runtime: &mut dyn Runtime, test_path: &Path, on_error: OnTestError) { - // macro which will look at `on_error` and do the right thing - macro_rules! error { - ($on_error:expr, $($arg:tt)*) => { - match $on_error { - OnTestError::Panic => panic!($($arg)*), - OnTestError::Log => { - eprintln!($($arg)*); - return; - } - } + pub fn bootstrap(config: RuntimeTestConfig) -> anyhow::Result { + log::info!("Testing: {}", config.test_path.display()); + let test_path_clone = config.test_path.to_owned(); + let spin_binary = config.spin_binary.clone(); + let preboot = move |env: &mut TestEnvironment| { + copy_manifest(&test_path_clone, env)?; + Ok(()) }; + let services_config = services_config(&config)?; + let env_config = TestEnvironmentConfig::spin(spin_binary, preboot, services_config); + let env = TestEnvironment::up(env_config)?; + Ok(Self { + test_path: config.test_path, + on_error: config.on_error, + env, + }) } - let response = match runtime.test() { - Ok(r) => r, - Err(e) => { - error!(on_error, "failed to run test: {e}") - } - }; - let error_file = test_path.join("error.txt"); - match response { - TestResult::Pass if !error_file.exists() => log::info!("Test passed!"), - TestResult::Pass => { - let expected = match std::fs::read_to_string(&error_file) { - Ok(e) => e, - Err(e) => error!(on_error, "failed to read error.txt file: {}", e), + + /// Run an individual test + pub fn run(&mut self) { + let on_error = self.on_error; + // macro which will look at `on_error` and do the right thing + macro_rules! error { + ($on_error:expr, $($arg:tt)*) => { + match $on_error { + OnTestError::Panic => panic!($($arg)*), + OnTestError::Log => { + eprintln!($($arg)*); + return; + } + } }; - error!( - on_error, - "Test passed but should have failed with error: {expected}" - ) } - TestResult::Fail(e, extra) if error_file.exists() => { - let expected = match std::fs::read_to_string(&error_file) { - Ok(e) => e, - Err(e) => error!(on_error, "failed to read error.txt file: {e}"), - }; - if e.contains(&expected) { - log::info!("Test passed!"); - } else { + let response = self.env.test(test); + let error_file = self.test_path.join("error.txt"); + match response { + Ok(()) if !error_file.exists() => log::info!("Test passed!"), + Ok(()) => { + let expected = match std::fs::read_to_string(&error_file) { + Ok(e) => e, + Err(e) => error!(on_error, "failed to read error.txt file: {}", e), + }; error!( on_error, - "Test errored but not in the expected way.\n\texpected: {expected}\n\tgot: {e}\n\n{extra}", + "Test passed but should have failed with error: {expected}" ) } + Err(TestError::Failure(RuntimeTestFailure { error, stderr })) + if error_file.exists() => + { + let expected = match std::fs::read_to_string(&error_file) { + Ok(e) => e, + Err(e) => error!(on_error, "failed to read error.txt file: {e}"), + }; + if error.contains(&expected) { + log::info!("Test passed!"); + } else { + error!( + on_error, + "Test errored but not in the expected way.\n\texpected: {expected}\n\tgot: {error}\n\nstderr:\n{stderr}", + ) + } + } + Err(TestError::Failure(RuntimeTestFailure { error, stderr })) => { + error!( + on_error, + "Test '{}' errored: {error}\nstderr:\n{stderr}", + self.test_path.display() + ); + } + Err(TestError::Fatal(extra)) => { + error!( + on_error, + "Test '{}' failed to run: {extra}", + self.test_path.display() + ); + } } - TestResult::Fail(e, extra) => { - error!( - on_error, - "Test '{}' errored: {e}\n{extra}", - test_path.display() - ); - } - TestResult::RuntimeError(extra) => { - error!( - on_error, - "Test '{}' failed fatally: {extra}", - test_path.display() - ); + if let OnTestError::Log = on_error { + println!("'{}' passed", self.test_path.display()) } } - if let OnTestError::Log = on_error { - println!("'{}' passed", test_path.display()) +} + +fn services_config(config: &RuntimeTestConfig) -> anyhow::Result { + let required_services = required_services(&config.test_path)?; + let services_config = ServicesConfig::new(required_services)?; + Ok(services_config) +} + +/// Get the services that a test requires. +fn required_services(test_path: &Path) -> anyhow::Result> { + let services_config_path = test_path.join("services"); + if !services_config_path.exists() { + return Ok(Vec::new()); } + let services_config_file = + std::fs::read_to_string(&services_config_path).context("could not read services file")?; + let iter = services_config_file.lines().filter_map(|s| { + let s = s.trim(); + (!s.is_empty()).then_some(s.to_owned()) + }); + Ok(iter.collect()) } -static TEMPLATE: OnceLock = OnceLock::new(); /// Copies the test dir's manifest file into the temporary directory /// /// Replaces template variables in the manifest file with components from the components path. -fn copy_manifest( - test_dir: &Path, - temp: &temp_dir::TempDir, - services: &mut Services, -) -> anyhow::Result<()> { +fn copy_manifest(test_dir: &Path, env: &mut TestEnvironment) -> anyhow::Result<()> { let manifest_path = test_dir.join("spin.toml"); - let mut manifest = std::fs::read_to_string(manifest_path).with_context(|| { + let mut manifest = ManifestTemplate::from_file(manifest_path).with_context(|| { format!( "no spin.toml manifest found in test directory {}", test_dir.display() ) })?; - let regex = TEMPLATE.get_or_init(|| regex::Regex::new(r"%\{(.*?)\}").unwrap()); - while let Some(captures) = regex.captures(&manifest) { - let (Some(full), Some(capture)) = (captures.get(0), captures.get(1)) else { - continue; - }; - let template = capture.as_str(); - let (template_key, template_value) = template.split_once('=').with_context(|| { - format!("invalid template '{template}'(template should be in the form $KEY=$VALUE)") - })?; - let replacement = match template_key.trim() { - "source" => { - let path = std::path::PathBuf::from( - test_components::path(template_value) - .with_context(|| format!("no such component '{template_value}'"))?, - ); - let wasm_name = path.file_name().unwrap().to_str().unwrap(); - std::fs::copy(&path, temp.path().join(wasm_name)).with_context(|| { - format!( - "failed to copy wasm file '{}' to temporary directory", - path.display() - ) - })?; - wasm_name.to_owned() - } - "port" => { - let guest_port = template_value - .parse() - .with_context(|| format!("failed to parse '{template_value}' as port"))?; - let port = services - .get_port(guest_port)? - .with_context(|| format!("no port {guest_port} exposed by any service"))?; - port.to_string() - } - _ => { - anyhow::bail!("unknown template key: {template_key}"); - } - }; - manifest.replace_range(full.range(), &replacement); - } - std::fs::write(temp.path().join("spin.toml"), manifest) + manifest.substitute(env)?; + env.write_file("spin.toml", manifest.contents()) .context("failed to copy spin.toml manifest to temporary directory")?; Ok(()) } -#[derive(Debug)] -pub enum TestResult { - /// The test passed - Pass, - /// Wasm errored (the wasm error, additional error info) - Fail(String, String), - /// Wasm failed to run (additional error info) - RuntimeError(String), +fn test(runtime: &mut Spin) -> TestResult { + let response = runtime.make_http_request(reqwest::Method::GET, "/")?; + if response.status() == 200 { + return Ok(()); + } + if response.status() != 500 { + return Err(anyhow::anyhow!("Runtime tests are expected to return either either a 200 or a 500, but it returned a {}", response.status()).into()); + } + let text = response + .text() + .context("could not get runtime test HTTP response")?; + if text.is_empty() { + let stderr = runtime.stderr(); + return Err(anyhow::anyhow!("Runtime tests are expected to return a response body, but the response body was empty.\nstderr:\n{stderr}").into()); + } + + Err(TestError::Failure(RuntimeTestFailure { + error: text, + stderr: runtime.stderr().to_owned(), + })) +} + +/// A runtime test failure +struct RuntimeTestFailure { + /// The error message returned by the runtime + error: String, + /// The runtime's stderr + stderr: String, } diff --git a/tests/runtime-tests/src/main.rs b/tests/runtime-tests/src/main.rs index 9f57a3e428..fa4e8b6047 100644 --- a/tests/runtime-tests/src/main.rs +++ b/tests/runtime-tests/src/main.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; -use runtime_tests::{run_all, spin::Spin, Config, OnTestError}; +use runtime_tests::RuntimeTest; +use testing_framework::OnTestError; fn main() -> anyhow::Result<()> { env_logger::init(); @@ -10,12 +11,6 @@ fn main() -> anyhow::Result<()> { .next() .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests")); - let config = Config { - create_runtime: Box::new(move |temp| { - Ok(Box::new(Spin::start(&spin_binary_path, temp)?) as _) - }), - tests_path, - on_error: OnTestError::Log, - }; - run_all(config) + let config = OnTestError::Log; + RuntimeTest::run_all(&tests_path, spin_binary_path, config) } diff --git a/tests/runtime-tests/src/services.rs b/tests/runtime-tests/src/services.rs deleted file mode 100644 index 80cf9c1240..0000000000 --- a/tests/runtime-tests/src/services.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::{collections::HashMap, path::Path}; - -mod docker; -mod python; - -use anyhow::{bail, Context}; - -use docker::DockerService; -use python::PythonService; - -pub fn start_services(test_path: &Path) -> anyhow::Result { - let services_config_path = test_path.join("services"); - if !services_config_path.exists() { - return Ok(Services { - services: Vec::new(), - }); - } - - let services_config_file = - std::fs::read_to_string(&services_config_path).context("could not read services file")?; - let required_services = services_config_file.lines().filter_map(|s| { - let s = s.trim(); - (!s.is_empty()).then_some(s) - }); - - // TODO: make this more robust so that it is not just assumed that the services definitions are - // located at ../../services relative to the test path - let service_definitions_path = test_path - .parent() - .unwrap() - .parent() - .unwrap() - .join("services"); - let service_definitions = service_definitions(&service_definitions_path)?; - let mut services = Vec::new(); - for required_service in required_services { - let service_definition_extension = service_definitions - .get(required_service) - .map(|e| e.as_str()); - let mut service: Box = match service_definition_extension { - Some("py") => Box::new(PythonService::start( - required_service, - &service_definitions_path, - )?), - Some("Dockerfile") => Box::new(DockerService::start( - required_service, - &service_definitions_path, - )?), - Some(extension) => { - bail!("service definitions with the '{extension}' extension are not supported") - } - None => bail!("no service definition found for '{required_service}'"), - }; - service.await_ready()?; - services.push(service); - } - - Ok(Services { services }) -} - -/// Get all of the service definitions returning a HashMap of the service name to the extension. -fn service_definitions(service_definitions_path: &Path) -> anyhow::Result> { - std::fs::read_dir(service_definitions_path)? - .map(|d| { - let d = d?; - if !d.file_type()?.is_file() { - bail!("directories are not allowed in the service definitions directory") - } - let file_name = d.file_name(); - let file_name = file_name.to_str().unwrap(); - let (file_name, file_extension) = file_name - .find('.') - .map(|i| (&file_name[..i], &file_name[i + 1..])) - .context("service definition did not have an extension")?; - Ok((file_name.to_owned(), file_extension.to_owned())) - }) - .filter(|r| !matches!( r , Ok((_, extension)) if extension == "lock")) - .collect() -} - -/// All the services that are running for a test. -pub struct Services { - services: Vec>, -} - -impl Services { - pub fn error(&mut self) -> anyhow::Result<()> { - for service in &mut self.services { - service.error()?; - } - Ok(()) - } - - /// Get the host port that a service exposes a guest port on. - pub(crate) fn get_port(&mut self, guest_port: u16) -> anyhow::Result> { - let mut result = None; - for service in &mut self.services { - let host_port = service.ports().unwrap().get(&guest_port); - match result { - None => result = host_port.copied(), - Some(_) => { - anyhow::bail!("more than one service exposes port {guest_port} to the host"); - } - } - } - Ok(result) - } -} - -impl<'a> IntoIterator for &'a Services { - type Item = &'a Box; - type IntoIter = std::slice::Iter<'a, Box>; - - fn into_iter(self) -> Self::IntoIter { - self.services.iter() - } -} - -/// An external service a test may depend on. -pub trait Service { - /// The name of the service - fn name(&self) -> &str; - - /// Block until the service is ready. - fn await_ready(&mut self) -> anyhow::Result<()>; - - /// Check if the service is in an error state. - fn error(&mut self) -> anyhow::Result<()>; - - /// Get a mapping of ports that the service exposes. - fn ports(&mut self) -> anyhow::Result<&HashMap>; -} diff --git a/tests/runtime.rs b/tests/runtime.rs index 09ad9dcc89..1d20b6db66 100644 --- a/tests/runtime.rs +++ b/tests/runtime.rs @@ -1,25 +1,19 @@ /// Run the tests found in `tests/runtime-tests` directory. mod runtime_tests { - use runtime_tests::{spin::Spin, Config}; use std::path::PathBuf; // The macro inspects the tests directory and // generates individual tests for each one. - test_codegen_macro::codegen_tests!(); + test_codegen_macro::codegen_runtime_tests!(); - fn run(name: &str) { - let spin_binary_path: PathBuf = env!("CARGO_BIN_EXE_spin").into(); - let tests_path = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/runtime-tests/tests"); - let config = Config { - create_runtime: Box::new(move |temp| { - Ok(Box::new(Spin::start(&spin_binary_path, temp)?) as _) - }), - tests_path, - on_error: runtime_tests::OnTestError::Panic, + fn run(test_path: PathBuf) { + let config = runtime_tests::RuntimeTestConfig { + test_path, + spin_binary: env!("CARGO_BIN_EXE_spin").into(), + on_error: testing_framework::OnTestError::Panic, }; - let path = config.tests_path.join(name); - runtime_tests::bootstrap_and_run(&path, &config) - .expect("failed to bootstrap runtime tests tests"); + runtime_tests::RuntimeTest::bootstrap(config) + .expect("failed to bootstrap runtime tests tests") + .run(); } } diff --git a/tests/test-components/components/Cargo.lock b/tests/test-components/components/Cargo.lock index 46c4309c0e..81dc9aa724 100644 --- a/tests/test-components/components/Cargo.lock +++ b/tests/test-components/components/Cargo.lock @@ -8,18 +8,47 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -29,6 +58,95 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -48,7 +166,18 @@ dependencies = [ name = "helper" version = "0.1.0" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.16.0", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", ] [[package]] @@ -78,6 +207,15 @@ dependencies = [ "serde", ] +[[package]] +name = "integration-simple" +version = "0.1.0" +dependencies = [ + "anyhow", + "http", + "spin-sdk", +] + [[package]] name = "itoa" version = "1.0.10" @@ -89,7 +227,7 @@ name = "key-value" version = "0.1.0" dependencies = [ "helper", - "wit-bindgen", + "wit-bindgen 0.16.0", ] [[package]] @@ -104,12 +242,24 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "outbound-mysql-test-component" version = "0.1.0" dependencies = [ "helper", - "wit-bindgen", + "wit-bindgen 0.16.0", ] [[package]] @@ -117,7 +267,7 @@ name = "outbound-postgres-test-component" version = "0.1.0" dependencies = [ "helper", - "wit-bindgen", + "wit-bindgen 0.16.0", ] [[package]] @@ -125,7 +275,7 @@ name = "outbound-redis-test-component" version = "0.1.0" dependencies = [ "helper", - "wit-bindgen", + "wit-bindgen 0.16.0", ] [[package]] @@ -134,24 +284,46 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] +[[package]] +name = "routefinder" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f8f99b10dedd317514253dda1fa7c14e344aac96e1f78149a64879ce282aca" +dependencies = [ + "smartcow", + "smartstring", +] + [[package]] name = "ryu" version = "1.0.16" @@ -181,7 +353,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -195,12 +367,41 @@ dependencies = [ "serde", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "smartcow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" +dependencies = [ + "smartstring", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + [[package]] name = "spdx" version = "0.10.2" @@ -210,19 +411,67 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spin-macro" +version = "2.2.0-pre0" +dependencies = [ + "anyhow", + "bytes", + "http", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "spin-sdk" +version = "2.2.0-pre0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "form_urlencoded", + "futures", + "http", + "once_cell", + "routefinder", + "serde", + "serde_json", + "spin-macro", + "thiserror", + "wit-bindgen 0.13.1", +] + [[package]] name = "sqlite-test-component" version = "0.1.0" dependencies = [ "helper", - "wit-bindgen", + "wit-bindgen 0.16.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] name = "syn" -version = "2.0.41" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -234,7 +483,27 @@ name = "tcp-sockets-test-component" version = "0.1.0" dependencies = [ "helper", - "wit-bindgen", + "wit-bindgen 0.16.0", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", ] [[package]] @@ -301,16 +570,31 @@ name = "variables" version = "0.1.0" dependencies = [ "helper", - "wit-bindgen", + "wit-bindgen 0.16.0", ] +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi-http-rc-2023-11-10" version = "0.1.0" dependencies = [ "helper", "url", - "wit-bindgen", + "wit-bindgen 0.16.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822b645bf4f2446b949776ffca47e2af60b167209ffb70814ef8779d299cd421" +dependencies = [ + "leb128", ] [[package]] @@ -334,8 +618,18 @@ dependencies = [ "serde_derive", "serde_json", "spdx", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.38.1", + "wasmparser 0.118.1", +] + +[[package]] +name = "wasmparser" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" +dependencies = [ + "indexmap", + "semver", ] [[package]] @@ -348,6 +642,16 @@ dependencies = [ "semver", ] +[[package]] +name = "wit-bindgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38726c54a5d7c03cac28a2a8de1006cfe40397ddf6def3f836189033a413bc08" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro 0.13.1", +] + [[package]] name = "wit-bindgen" version = "0.16.0" @@ -355,7 +659,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b76f1d099678b4f69402a421e888bbe71bf20320c2f3f3565d0e7484dbe5bc20" dependencies = [ "bitflags", - "wit-bindgen-rust-macro", + "wit-bindgen-rust-macro 0.16.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8bf1fddccaff31a1ad57432d8bfb7027a7e552969b6c68d6d8820dcf5c2371f" +dependencies = [ + "anyhow", + "wit-component 0.17.0", + "wit-parser 0.12.2", ] [[package]] @@ -365,8 +680,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75d55e1a488af2981fb0edac80d8d20a51ac36897a1bdef4abde33c29c1b6d0d" dependencies = [ "anyhow", - "wit-component", - "wit-parser", + "wit-component 0.18.2", + "wit-parser 0.13.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7200e565124801e01b7b5ddafc559e1da1b2e1bed5364d669cd1d96fb88722" +dependencies = [ + "anyhow", + "heck", + "wasm-metadata", + "wit-bindgen-core 0.13.1", + "wit-component 0.17.0", ] [[package]] @@ -378,8 +706,23 @@ dependencies = [ "anyhow", "heck", "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "wit-bindgen-core 0.16.0", + "wit-component 0.18.2", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae33920ad8119fe72cf59eb00f127c0b256a236b9de029a1a10397b1f38bdbd" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.48", + "wit-bindgen-core 0.13.1", + "wit-bindgen-rust 0.13.2", + "wit-component 0.17.0", ] [[package]] @@ -391,10 +734,29 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", - "wit-component", + "syn 2.0.48", + "wit-bindgen-core 0.16.0", + "wit-bindgen-rust 0.16.0", + "wit-component 0.18.2", +] + +[[package]] +name = "wit-component" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "480cc1a078b305c1b8510f7c455c76cbd008ee49935f3a6c5fd5e937d8d95b1e" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.36.2", + "wasm-metadata", + "wasmparser 0.116.1", + "wit-parser 0.12.2", ] [[package]] @@ -410,10 +772,27 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", + "wasm-encoder 0.38.1", "wasm-metadata", - "wasmparser", - "wit-parser", + "wasmparser 0.118.1", + "wit-parser 0.13.0", +] + +[[package]] +name = "wit-parser" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43771ee863a16ec4ecf9da0fc65c3bbd4a1235c8e3da5f094b562894843dfa76" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", ] [[package]] diff --git a/tests/test-components/components/integration-simple/Cargo.toml b/tests/test-components/components/integration-simple/Cargo.toml new file mode 100644 index 0000000000..fd049e62b8 --- /dev/null +++ b/tests/test-components/components/integration-simple/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "integration-simple" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +http = "0.2" +spin-sdk = { path = "../../../../sdk/rust" } diff --git a/tests/http/simple-spin-rust/src/lib.rs b/tests/test-components/components/integration-simple/src/lib.rs similarity index 100% rename from tests/http/simple-spin-rust/src/lib.rs rename to tests/test-components/components/integration-simple/src/lib.rs diff --git a/tests/runtime-tests/.gitignore b/tests/testing-framework/.gitignore similarity index 100% rename from tests/runtime-tests/.gitignore rename to tests/testing-framework/.gitignore diff --git a/tests/testing-framework/Cargo.toml b/tests/testing-framework/Cargo.toml new file mode 100644 index 0000000000..5b56c32dfc --- /dev/null +++ b/tests/testing-framework/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "testing-framework" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +fslock = "0.2.1" +log = "0.4" +nix = "0.26.1" +regex = "1.10.2" +reqwest = { workspace = true } +temp-dir = "0.1.11" +test-components = { path = "../test-components" } +toml = "0.8.6" diff --git a/tests/testing-framework/README.md b/tests/testing-framework/README.md new file mode 100644 index 0000000000..601ec83c4b --- /dev/null +++ b/tests/testing-framework/README.md @@ -0,0 +1,33 @@ +# Testing Framework + +The testing framework is a general framework for running tests against a Spin compliant runtime. The testing framework includes the ability to start up dependent services (e.g., Redis or MySQL). + +## Services + +Services allow for tests to be run against external sources. The service definitions can be found in the 'services' directory. Each test directory contains a 'services' file that configures the tests services. Each line of the services file should contain the name of a services file that needs to run. For example, the following 'services' file will run the `tcp-echo.py` service: + +```txt +tcp-echo +``` + +Each service is run under a file lock meaning that all other tests that require that service must wait until the current test using that service has finished. + +The following service types are supported: +* Python services (a python script ending in the .py file extension) +* Docker services (a docker file ending in the .Dockerfile extension) + +When looking to add a new service, always prefer the Python based service as it's generally much quicker and lighter weight to run a Python script than a Docker container. Only use Docker when the service you require is not possible to achieve in cross platform way as a Python script. + +### Signaling Service Readiness + +Services can signal that they are ready so that tests aren't run against them until they are ready: + +* Python: Python services signal they are ready by printing `READY` to stdout. +* Docker: Docker services signal readiness by exposing a Docker health check in the Dockerfile (e.g., `HEALTHCHECK --start-period=4s --interval=1s CMD /usr/bin/mysqladmin ping --silent`) + +### Exposing Ports + +Both Docker and Python based services can expose some logical port number that will be mapped to a random free port number at runtime. + +* Python: Python based services can do this by printing `PORT=($PORT1, $PORT2)` to stdout where the $PORT1 is the logical port the service exposes and $PORT2 is the random port actually being exposed (e.g., `PORT=(80, 59392)`) +* Docker: Docker services can do this by exposing the port in their Dockerfile (e.g., `EXPOSE 3306`) diff --git a/tests/runtime-tests/services/http-echo.py b/tests/testing-framework/services/http-echo.py similarity index 100% rename from tests/runtime-tests/services/http-echo.py rename to tests/testing-framework/services/http-echo.py diff --git a/tests/runtime-tests/services/mysql.Dockerfile b/tests/testing-framework/services/mysql.Dockerfile similarity index 100% rename from tests/runtime-tests/services/mysql.Dockerfile rename to tests/testing-framework/services/mysql.Dockerfile diff --git a/tests/runtime-tests/services/postgres.Dockerfile b/tests/testing-framework/services/postgres.Dockerfile similarity index 100% rename from tests/runtime-tests/services/postgres.Dockerfile rename to tests/testing-framework/services/postgres.Dockerfile diff --git a/tests/runtime-tests/services/redis.Dockerfile b/tests/testing-framework/services/redis.Dockerfile similarity index 100% rename from tests/runtime-tests/services/redis.Dockerfile rename to tests/testing-framework/services/redis.Dockerfile diff --git a/tests/runtime-tests/services/tcp-echo.py b/tests/testing-framework/services/tcp-echo.py similarity index 100% rename from tests/runtime-tests/services/tcp-echo.py rename to tests/testing-framework/services/tcp-echo.py diff --git a/tests/runtime-tests/src/io.rs b/tests/testing-framework/src/io.rs similarity index 100% rename from tests/runtime-tests/src/io.rs rename to tests/testing-framework/src/io.rs diff --git a/tests/testing-framework/src/lib.rs b/tests/testing-framework/src/lib.rs new file mode 100644 index 0000000000..53bde64847 --- /dev/null +++ b/tests/testing-framework/src/lib.rs @@ -0,0 +1,97 @@ +//! Testing infrastructure +//! +//! This crate has a few entry points depending on what you want to do: +//! * `RuntimeTest` - bootstraps and runs a single runtime test +//! * `TestEnvironment` - bootstraps a test environment which can be used by more than just runtime tests + +mod io; +mod manifest_template; +mod services; +mod spin; +mod test_environment; + +pub use manifest_template::ManifestTemplate; +pub use services::ServicesConfig; +pub use spin::Spin; +pub use test_environment::{TestEnvironment, TestEnvironmentConfig}; + +#[derive(Debug, Clone, Copy)] +/// What to do when a test errors +pub enum OnTestError { + /// Panic + Panic, + /// Log the error to stderr + Log, +} + +/// A runtime which can be tested +pub trait Runtime { + /// Return an error if one has occurred + fn error(&mut self) -> anyhow::Result<()>; +} + +/// A test which can be run against a runtime +pub trait Test { + /// The runtime the test is run against + type Runtime: Runtime; + /// The type of error the test can return when the test is in a failure state + /// + /// This type is used when the test is actually run but it fails as opposed to the + /// error state where the test cannot be run at all. + type Failure; + + /// Run the test against the runtime + fn test(self, runtime: &mut Self::Runtime) -> TestResult; +} + +impl Test for F +where + F: FnOnce(&mut Spin) -> TestResult + 'static, +{ + type Runtime = Spin; + type Failure = E; + + fn test(self, runtime: &mut Self::Runtime) -> TestResult { + self(runtime) + } +} + +/// The result of running a test. +/// +/// The result has three states: +/// * `Ok(())` - the test ran and passed +/// * `Err(TestError::Failure(_))` - the test ran and failed +/// * `Err(TestError::Fatal(_))` - the test did not run because of an error +pub type TestResult = Result<(), TestError>; + +/// An error in a test. +/// +/// This type is generic over the `Failure` type (i.e., the type that is returned when the test +/// is actually run and fails). +#[derive(Debug)] +pub enum TestError { + /// The test was run but failed. + Failure(E), + /// The test did not run because of an error. + Fatal(anyhow::Error), +} + +impl From for TestError { + fn from(e: anyhow::Error) -> Self { + TestError::Fatal(e) + } +} + +impl std::error::Error for TestError {} + +impl std::fmt::Display for TestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TestError::Failure(e) => { + write!(f, "{e}")?; + Ok(()) + } + TestError::Fatal(e) => write!(f, "Test failed to run: {}", e), + } + } +} diff --git a/tests/testing-framework/src/manifest_template.rs b/tests/testing-framework/src/manifest_template.rs new file mode 100644 index 0000000000..f1b4533a4b --- /dev/null +++ b/tests/testing-framework/src/manifest_template.rs @@ -0,0 +1,63 @@ +use anyhow::Context as _; +use std::{path::Path, sync::OnceLock}; + +use crate::TestEnvironment; + +/// A manifest template with template variables that can be substituted. +pub struct ManifestTemplate { + manifest: String, +} + +static TEMPLATE: OnceLock = OnceLock::new(); +impl ManifestTemplate { + /// Read a manifest template from a file. + pub fn from_file(path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + let manifest = std::fs::read_to_string(path) + .with_context(|| format!("could not read manifest template at '{}'", path.display()))?; + Ok(Self { manifest }) + } + + /// Substitute template variables in the manifest template. + pub fn substitute(&mut self, env: &mut TestEnvironment) -> Result<(), anyhow::Error> { + let regex = TEMPLATE.get_or_init(|| regex::Regex::new(r"%\{(.*?)\}").unwrap()); + while let Some(captures) = regex.captures(&self.manifest) { + let (Some(full), Some(capture)) = (captures.get(0), captures.get(1)) else { + continue; + }; + let template = capture.as_str(); + let (template_key, template_value) = template.split_once('=').with_context(|| { + format!("invalid template '{template}'(template should be in the form $KEY=$VALUE)") + })?; + let replacement = match template_key.trim() { + "source" => { + let component_binary = std::path::PathBuf::from( + test_components::path(template_value) + .with_context(|| format!("no such component '{template_value}'"))?, + ); + let wasm_name = component_binary.file_name().unwrap().to_str().unwrap(); + env.copy_into(&component_binary, wasm_name)?; + wasm_name.to_owned() + } + "port" => { + let guest_port = template_value + .parse() + .with_context(|| format!("failed to parse '{template_value}' as port"))?; + let port = env + .get_port(guest_port)? + .with_context(|| format!("no port {guest_port} exposed by any service"))?; + port.to_string() + } + _ => { + anyhow::bail!("unknown template key: {template_key}"); + } + }; + self.manifest.replace_range(full.range(), &replacement); + } + Ok(()) + } + + pub fn contents(&self) -> &str { + &self.manifest + } +} diff --git a/tests/testing-framework/src/services.rs b/tests/testing-framework/src/services.rs new file mode 100644 index 0000000000..5a87cc3d02 --- /dev/null +++ b/tests/testing-framework/src/services.rs @@ -0,0 +1,144 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +mod docker; +mod python; + +use anyhow::{bail, Context}; + +use docker::DockerService; +use python::PythonService; + +/// All the services that are running for a test. +#[derive(Default)] +pub struct Services { + services: Vec>, +} + +impl Services { + /// Start all the required services given a path to service definitions + pub fn start(config: &ServicesConfig) -> anyhow::Result { + let mut services = Vec::new(); + for required_service in &config.services { + let service_definition_extension = + config.definitions.get(required_service).map(|e| e.as_str()); + let mut service: Box = match service_definition_extension { + Some("py") => Box::new(PythonService::start( + required_service, + &config.definitions_path, + )?), + Some("Dockerfile") => Box::new(DockerService::start( + required_service, + &config.definitions_path, + )?), + Some(extension) => { + bail!("service definitions with the '{extension}' extension are not supported") + } + None => bail!("no service definition found for '{required_service}'"), + }; + service.ready()?; + services.push(service); + } + + Ok(Services { services }) + } + + pub fn healthy(&mut self) -> anyhow::Result<()> { + for service in &mut self.services { + service.ready()?; + } + Ok(()) + } + + /// Get the host port that a service exposes a guest port on. + pub(crate) fn get_port(&mut self, guest_port: u16) -> anyhow::Result> { + let mut result = None; + for service in &mut self.services { + let host_port = service.ports().unwrap().get(&guest_port); + match result { + None => result = host_port.copied(), + Some(_) => { + anyhow::bail!("more than one service exposes port {guest_port} to the host"); + } + } + } + Ok(result) + } +} + +impl<'a> IntoIterator for &'a Services { + type Item = &'a Box; + type IntoIter = std::slice::Iter<'a, Box>; + + fn into_iter(self) -> Self::IntoIter { + self.services.iter() + } +} + +pub struct ServicesConfig { + services: Vec, + definitions_path: PathBuf, + definitions: HashMap, +} + +impl ServicesConfig { + /// Create a new services config given a path to service definitions and a list of services to start. + pub fn new(services: Vec) -> anyhow::Result { + let definitions = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("services"); + let service_definitions = service_definitions(&definitions)?; + Ok(Self { + services, + definitions_path: definitions, + definitions: service_definitions, + }) + } + + /// Configure no services + pub fn none() -> Self { + Self { + services: Vec::new(), + definitions_path: PathBuf::new(), + definitions: HashMap::new(), + } + } +} + +/// Get all of the service definitions returning a HashMap of the service name to the service definition file extension. +fn service_definitions(service_definitions_path: &Path) -> anyhow::Result> { + std::fs::read_dir(service_definitions_path) + .with_context(|| { + format!( + "no service definitions found at '{}'", + service_definitions_path.display() + ) + })? + .map(|d| { + let d = d?; + if !d.file_type()?.is_file() { + bail!("directories are not allowed in the service definitions directory") + } + let file_name = d.file_name(); + let file_name = file_name.to_str().unwrap(); + let (file_name, file_extension) = file_name + .find('.') + .map(|i| (&file_name[..i], &file_name[i + 1..])) + .context("service definition did not have an extension")?; + Ok((file_name.to_owned(), file_extension.to_owned())) + }) + .filter(|r| !matches!( r , Ok((_, extension)) if extension == "lock")) + .collect() +} + +/// An external service a test may depend on. +pub trait Service { + /// The name of the service. + fn name(&self) -> &str; + + /// Block until the service is ready and error if service is in bad state. + fn ready(&mut self) -> anyhow::Result<()>; + + /// Get a mapping of ports that the service exposes. + fn ports(&mut self) -> anyhow::Result<&HashMap>; +} diff --git a/tests/runtime-tests/src/services/docker.rs b/tests/testing-framework/src/services/docker.rs similarity index 97% rename from tests/runtime-tests/src/services/docker.rs rename to tests/testing-framework/src/services/docker.rs index 6ec8d4210e..fac10043a2 100644 --- a/tests/runtime-tests/src/services/docker.rs +++ b/tests/testing-framework/src/services/docker.rs @@ -14,6 +14,7 @@ pub struct DockerService { // We declare lock after container so that the lock is dropped after the container is _lock: fslock::LockFile, ports: OnceCell>, + ready: bool, } impl DockerService { @@ -35,6 +36,7 @@ impl DockerService { container, _lock: lock, ports: OnceCell::new(), + ready: false, }) } } @@ -86,9 +88,9 @@ impl Service for DockerService { "docker" } - fn await_ready(&mut self) -> anyhow::Result<()> { + fn ready(&mut self) -> anyhow::Result<()> { // docker container inspect -f '{{.State.Health.Status}}' - loop { + while !self.ready { let output = Command::new("docker") .arg("container") .arg("inspect") @@ -104,14 +106,11 @@ impl Service for DockerService { } let output = String::from_utf8(output.stdout)?; match output.trim() { - "healthy" => return Ok(()), + "healthy" => self.ready = true, "unhealthy" => bail!("docker container is unhealthy"), _ => std::thread::sleep(std::time::Duration::from_millis(100)), } } - } - - fn error(&mut self) -> anyhow::Result<()> { anyhow::ensure!(!get_running_containers(&self.image_name)?.is_empty()); Ok(()) } diff --git a/tests/runtime-tests/src/services/python.rs b/tests/testing-framework/src/services/python.rs similarity index 94% rename from tests/runtime-tests/src/services/python.rs rename to tests/testing-framework/src/services/python.rs index 496fe5834e..baf4b87cc4 100644 --- a/tests/runtime-tests/src/services/python.rs +++ b/tests/testing-framework/src/services/python.rs @@ -14,6 +14,7 @@ pub struct PythonService { stdout: OutputStream, ports: OnceCell>, _lock: fslock::LockFile, + ready: bool, } impl PythonService { @@ -43,6 +44,7 @@ impl PythonService { child, ports: OnceCell::new(), _lock: lock, + ready: false, }) } } @@ -52,28 +54,25 @@ impl Service for PythonService { "python" } - fn await_ready(&mut self) -> anyhow::Result<()> { - loop { + fn ready(&mut self) -> anyhow::Result<()> { + while !self.ready { let stdout = self .stdout .output_as_str() .context("stdout is not valid utf8")?; if stdout.contains("READY") { - break; + self.ready = true; } } - Ok(()) - } - - fn error(&mut self) -> anyhow::Result<()> { let exit = self.child.try_wait()?; if exit.is_some() { return Err(std::io::Error::new( std::io::ErrorKind::Interrupted, - "process exited early", + "python service process exited early", ) .into()); } + Ok(()) } diff --git a/tests/runtime-tests/src/spin.rs b/tests/testing-framework/src/spin.rs similarity index 79% rename from tests/runtime-tests/src/spin.rs rename to tests/testing-framework/src/spin.rs index 6202a6b500..a94aec1862 100644 --- a/tests/runtime-tests/src/spin.rs +++ b/tests/testing-framework/src/spin.rs @@ -1,9 +1,10 @@ -use crate::{io::OutputStream, Runtime, TestResult}; +use crate::{io::OutputStream, Runtime}; use std::{ path::Path, process::{Command, Stdio}, }; +/// A wrapper around a running Spin instance pub struct Spin { process: std::process::Child, #[allow(dead_code)] @@ -65,12 +66,22 @@ impl Spin { ) } - fn make_http_request(&mut self) -> Result { + pub fn make_http_request( + &mut self, + method: reqwest::Method, + path: &str, + ) -> anyhow::Result { if let Some(status) = self.try_wait()? { anyhow::bail!("Spin exited early with status code {:?}", status.code()); } log::debug!("Connecting to HTTP server on port {}...", self.port); - let response = reqwest::blocking::get(format!("http://127.0.0.1:{}", self.port))?; + let request = reqwest::blocking::Request::new( + method, + format!("http://localhost:{}{}", self.port, path) + .parse() + .unwrap(), + ); + let response = reqwest::blocking::Client::new().execute(request)?; log::debug!("Awaiting response from server"); if let Some(status) = self.try_wait()? { anyhow::bail!("Spin exited early with status code {:?}", status.code()); @@ -78,6 +89,10 @@ impl Spin { Ok(response) } + pub fn stderr(&mut self) -> &str { + self.stderr.output_as_str().unwrap_or("") + } + fn try_wait(&mut self) -> std::io::Result> { self.process.try_wait() } @@ -90,27 +105,12 @@ impl Drop for Spin { } impl Runtime for Spin { - fn test(&mut self) -> anyhow::Result { - let response = self.make_http_request()?; - if response.status() == 200 { - return Ok(TestResult::Pass); - } - if response.status() != 500 { - anyhow::bail!("Response was neither 200 nor 500") - } - let text = response.text()?; - if text.is_empty() { - let stderr = self.stderr.output_as_str().unwrap_or(""); - return Ok(TestResult::RuntimeError(stderr.to_owned())); + fn error(&mut self) -> anyhow::Result<()> { + if self.try_wait()?.is_some() { + anyhow::bail!("Spin exited early: {}", self.stderr()); } - Ok(TestResult::Fail( - text, - self.stderr - .output_as_str() - .unwrap_or("") - .to_owned(), - )) + Ok(()) } } diff --git a/tests/testing-framework/src/test_environment.rs b/tests/testing-framework/src/test_environment.rs new file mode 100644 index 0000000000..a75cac9b9e --- /dev/null +++ b/tests/testing-framework/src/test_environment.rs @@ -0,0 +1,144 @@ +use std::path::{Path, PathBuf}; + +use crate::{ + services::{Services, ServicesConfig}, + spin::Spin, + Runtime, Test, TestResult, +}; +use anyhow::Context as _; + +/// A callback to create a runtime given a path to a temporary directory and a set of services +pub type RuntimeCreator = dyn FnOnce(&mut TestEnvironment) -> anyhow::Result; + +/// All the requirements to run a test +pub struct TestEnvironment { + temp: temp_dir::TempDir, + services: Services, + runtime: Option, +} + +impl TestEnvironment { + /// Spin up a test environment + pub fn up(config: TestEnvironmentConfig) -> anyhow::Result { + let temp = temp_dir::TempDir::new() + .context("failed to produce a temporary directory to run the test in")?; + log::trace!("Temporary directory: {}", temp.path().display()); + let mut services = + Services::start(&config.services_config).context("failed to start services")?; + services.healthy().context("services have failed")?; + let mut env = Self { + temp, + services, + runtime: None, + }; + let runtime = (config.create_runtime)(&mut env)?; + env.runtime = Some(runtime); + env.error().context("services have failed")?; + Ok(env) + } + + /// Run test against runtime + pub fn test>(&mut self, test: T) -> TestResult { + let runtime = self + .runtime + .as_mut() + .context("runtime was not initialized")?; + test.test(runtime) + } + + /// Whether an error has occurred + fn error(&mut self) -> anyhow::Result<()> { + self.services.healthy()?; + if let Some(runtime) = &mut self.runtime { + runtime.error()?; + } + Ok(()) + } +} + +impl TestEnvironment { + /// Copy a file into the test environment at the given relative path + pub fn copy_into(&self, from: impl AsRef, into: impl AsRef) -> anyhow::Result<()> { + fn copy_dir_all(from: &Path, into: &Path) -> anyhow::Result<()> { + std::fs::create_dir_all(into)?; + for entry in std::fs::read_dir(from)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_all(&entry.path(), &into.join(entry.file_name()))?; + } else { + std::fs::copy(entry.path(), into.join(entry.file_name()))?; + } + } + Ok(()) + } + let from = from.as_ref(); + let into = into.as_ref(); + if from.is_dir() { + copy_dir_all(from, &self.temp.path().join(into)).with_context(|| { + format!( + "failed to copy directory '{}' to temporary directory", + from.display() + ) + })?; + } else { + std::fs::copy(from, self.temp.path().join(into)).with_context(|| { + format!( + "failed to copy file '{}' to temporary directory", + from.display() + ) + })?; + } + Ok(()) + } + + /// Get the host port that is mapped to the given guest port + pub fn get_port(&mut self, guest_port: u16) -> anyhow::Result> { + self.services.get_port(guest_port) + } + + /// Write a file into the test environment at the given relative path + pub fn write_file( + &self, + to: impl AsRef, + contents: impl AsRef<[u8]>, + ) -> anyhow::Result<()> { + std::fs::write(self.temp.path().join(to), contents)?; + Ok(()) + } + + /// Get the path to test environment + pub(crate) fn path(&self) -> &Path { + self.temp.path() + } +} + +/// Configuration for a test environment +pub struct TestEnvironmentConfig { + /// A callback to create a runtime given a path to a temporary directory + pub create_runtime: Box>, + /// The services that the test requires + pub services_config: ServicesConfig, +} + +impl TestEnvironmentConfig { + /// Configure a test environment that uses a local Spin as a runtime + /// + /// * `spin_binary` - the path to the Spin binary + /// * `preboot` - a callback that happens after the services have started but before the runtime is + /// * `test` - a callback that runs the test against the runtime + /// * `services_config` - the services that the test requires + pub fn spin( + spin_binary: PathBuf, + preboot: impl FnOnce(&mut TestEnvironment) -> anyhow::Result<()> + 'static, + services_config: ServicesConfig, + ) -> Self { + Self { + services_config, + create_runtime: Box::new(move |env| { + preboot(env)?; + Spin::start(&spin_binary, env.path()) + }), + } + } +}