From 0d27af871957376a06a121a62d90d2d32bdc1a8d Mon Sep 17 00:00:00 2001 From: Benjamin Naecker Date: Fri, 1 Dec 2023 21:35:04 +0000 Subject: [PATCH] Check that simulated and real Crucible Pantry APIs are the same Adds regression test for https://github.com/oxidecomputer/omicron/issues/4599 --- Cargo.lock | 131 +++++++++++++++--- Cargo.toml | 1 + sled-agent/Cargo.toml | 1 + sled-agent/src/sim/http_entrypoints_pantry.rs | 98 +++++++++++++ 4 files changed, 215 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06c1c2b5b75..454b5adde13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,13 +54,15 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if 1.0.0", + "getrandom 0.2.10", "once_cell", "version_check", + "zerocopy 0.7.28", ] [[package]] @@ -786,9 +788,9 @@ dependencies = [ [[package]] name = "cargo_metadata" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb9ac64500cc83ce4b9f8dafa78186aa008c8dea77a09b94cd307fd0cd5022a8" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", "cargo-platform", @@ -847,6 +849,16 @@ dependencies = [ "nom", ] +[[package]] +name = "cfg-expr" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +dependencies = [ + "smallvec 1.11.2", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -2618,6 +2630,39 @@ dependencies = [ "subtle", ] +[[package]] +name = "guppy" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114a100a9aa9f4c468a7b9e96626cdab267bb652660d8408e8f6d56d4c310edd" +dependencies = [ + "ahash", + "camino", + "cargo_metadata", + "cfg-if 1.0.0", + "debug-ignore", + "fixedbitset", + "guppy-workspace-hack", + "indexmap 2.1.0", + "itertools 0.12.0", + "nested", + "once_cell", + "pathdiff", + "petgraph", + "semver 1.0.20", + "serde", + "serde_json", + "smallvec 1.11.2", + "static_assertions", + "target-spec", +] + +[[package]] +name = "guppy-workspace-hack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92620684d99f750bae383ecb3be3748142d6095760afd5cbcf2261e9a279d780" + [[package]] name = "h2" version = "0.3.21" @@ -3918,6 +3963,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nested" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b420f638f07fe83056b55ea190bb815f609ec5a35e7017884a10f78839c9e" + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -4191,7 +4242,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ - "smallvec 1.11.0", + "smallvec 1.11.2", ] [[package]] @@ -4277,7 +4328,7 @@ dependencies = [ "num-traits", "rand 0.8.5", "serde", - "smallvec 1.11.0", + "smallvec 1.11.2", "zeroize", ] @@ -4823,6 +4874,7 @@ dependencies = [ "futures", "gateway-client", "glob", + "guppy", "http", "hyper", "hyper-staticfile", @@ -5485,7 +5537,7 @@ dependencies = [ "instant", "libc", "redox_syscall 0.2.16", - "smallvec 1.11.0", + "smallvec 1.11.2", "winapi", ] @@ -5498,7 +5550,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall 0.3.5", - "smallvec 1.11.0", + "smallvec 1.11.2", "windows-targets 0.48.5", ] @@ -5574,6 +5626,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498a099351efa4becc6a19c72aa9270598e8fd274ca47052e37455241c88b696" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +dependencies = [ + "camino", +] + [[package]] name = "pbkdf2" version = "0.11.0" @@ -7155,9 +7216,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] @@ -7193,9 +7254,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", @@ -7715,9 +7776,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "smawk" @@ -8172,6 +8233,24 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" + +[[package]] +name = "target-spec" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48b81540ee78bd9de9f7dca2378f264cf1f4193da6e2d09b54c0d595131a48f1" +dependencies = [ + "cfg-expr", + "guppy-workspace-hack", + "target-lexicon", + "unicode-ident", +] + [[package]] name = "tempdir" version = "0.3.7" @@ -8759,7 +8838,7 @@ dependencies = [ "ipnet", "lazy_static", "rand 0.8.5", - "smallvec 1.11.0", + "smallvec 1.11.2", "thiserror", "tinyvec", "tokio", @@ -8780,7 +8859,7 @@ dependencies = [ "lru-cache", "parking_lot 0.12.1", "resolv-conf", - "smallvec 1.11.0", + "smallvec 1.11.2", "thiserror", "tokio", "tracing", @@ -9872,6 +9951,15 @@ dependencies = [ "zerocopy-derive 0.6.4", ] +[[package]] +name = "zerocopy" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e" +dependencies = [ + "zerocopy-derive 0.7.28", +] + [[package]] name = "zerocopy-derive" version = "0.2.0" @@ -9894,6 +9982,17 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "zerocopy-derive" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 6d479093991..01e0a861ab7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -202,6 +202,7 @@ gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway- gateway-sp-comms = { git = "https://github.com/oxidecomputer/management-gateway-service", rev = "2739c18e80697aa6bc235c935176d14b4d757ee9" } gateway-test-utils = { path = "gateway-test-utils" } glob = "0.3.1" +guppy = "0.17.4" headers = "0.3.9" heck = "0.4" hex = "0.4.3" diff --git a/sled-agent/Cargo.toml b/sled-agent/Cargo.toml index 61e61709e16..4746b1c41bb 100644 --- a/sled-agent/Cargo.toml +++ b/sled-agent/Cargo.toml @@ -81,6 +81,7 @@ opte-ioctl.workspace = true [dev-dependencies] assert_matches.workspace = true expectorate.workspace = true +guppy.workspace = true http.workspace = true hyper.workspace = true omicron-test-utils.workspace = true diff --git a/sled-agent/src/sim/http_entrypoints_pantry.rs b/sled-agent/src/sim/http_entrypoints_pantry.rs index 8430dc0731d..8f572b46a04 100644 --- a/sled-agent/src/sim/http_entrypoints_pantry.rs +++ b/sled-agent/src/sim/http_entrypoints_pantry.rs @@ -280,3 +280,101 @@ async fn detach( Ok(HttpResponseDeleted()) } + +#[cfg(test)] +mod tests { + use guppy::graph::ExternalSource; + use guppy::graph::GitReq; + use guppy::graph::PackageGraph; + use guppy::MetadataCommand; + use serde_json::Value; + use std::path::Path; + + fn load_real_api_as_json() -> serde_json::Value { + let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("Cargo.toml"); + let mut cmd = MetadataCommand::new(); + cmd.manifest_path(&manifest_path); + let graph = PackageGraph::from_command(&mut cmd).unwrap(); + let package = graph + .packages() + .find(|pkg| pkg.name() == "crucible-pantry-client") + .unwrap(); + let ExternalSource::Git { req, .. } = + package.source().parse_external().unwrap() + else { + panic!("This should be a Git dependency"); + }; + let part = match req { + GitReq::Branch(inner) => inner, + GitReq::Rev(inner) => inner, + GitReq::Tag(inner) => inner, + GitReq::Default => "main", + _ => unreachable!(), + }; + let raw_url = format!( + "https://raw.githubusercontent.com/oxidecomputer/crucible/{part}/openapi/crucible-pantry.json", + ); + let raw_json = + reqwest::blocking::get(&raw_url).unwrap().text().unwrap(); + serde_json::from_str(&raw_json).unwrap() + } + + // Regression test for https://github.com/oxidecomputer/omicron/issues/4599. + #[test] + fn test_simulated_api_matches_real() { + let real_api = load_real_api_as_json(); + let Value::String(ref title) = real_api["info"]["title"] else { + unreachable!(); + }; + let Value::String(ref version) = real_api["info"]["version"] else { + unreachable!(); + }; + let sim_api = super::api().openapi(title, version).json().unwrap(); + + // We'll assert that anything which apppears in the simulated API must + // appear exactly as-is in the real API. I.e., the simulated is a subset + // (possibly non-strict) of the real API. + compare_json_values(&sim_api, &real_api, String::new()); + } + + fn compare_json_values(lhs: &Value, rhs: &Value, path: String) { + match lhs { + Value::Array(values) => { + let Value::Array(rhs_values) = &rhs else { + panic!( + "Expected an array in the real API JSON at \ + path \"{path}\", found {rhs:?}", + ); + }; + assert_eq!(values.len(), rhs_values.len()); + for (i, (left, right)) in + values.iter().zip(rhs_values.iter()).enumerate() + { + let new_path = format!("{path}[{i}]"); + compare_json_values(left, right, new_path); + } + } + Value::Object(map) => { + let Value::Object(rhs_map) = &rhs else { + panic!( + "Expected a map in the real API JSON at \ + path \"{path}\", found {rhs:?}", + ); + }; + for (key, value) in map.iter() { + let new_path = format!("{path}/{key}"); + let rhs_value = rhs_map.get(key).unwrap_or_else(|| { + panic!("Real API JSON missing key: \"{new_path}\"") + }); + compare_json_values(value, rhs_value, new_path); + } + } + _ => { + assert_eq!(lhs, rhs, "Mismatched keys at JSON path \"{path}\"") + } + } + } +}