From f6da3086fed29b5a52fd4cf5a2cbc329c6d156b1 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 | 113 +++++++++++++++--- Cargo.toml | 1 + sled-agent/Cargo.toml | 1 + sled-agent/src/sim/http_entrypoints_pantry.rs | 98 +++++++++++++++ workspace-hack/Cargo.toml | 12 +- 5 files changed, 205 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c379dcfbff..dfab530a28 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", + "getrandom 0.2.10", "once_cell", "version_check", + "zerocopy 0.7.26", ] [[package]] @@ -780,9 +782,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", @@ -841,6 +843,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 = "1.0.0" @@ -2658,6 +2670,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", + "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" @@ -3962,6 +4007,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" @@ -4235,7 +4286,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]] @@ -4321,7 +4372,7 @@ dependencies = [ "num-traits", "rand 0.8.5", "serde", - "smallvec 1.11.0", + "smallvec 1.11.2", "zeroize", ] @@ -4868,6 +4919,7 @@ dependencies = [ "futures", "gateway-client", "glob", + "guppy", "hex", "http", "hyper", @@ -4965,6 +5017,7 @@ dependencies = [ name = "omicron-workspace-hack" version = "0.1.0" dependencies = [ + "ahash", "anyhow", "base16ct", "bit-set", @@ -5070,6 +5123,7 @@ dependencies = [ "usdt", "uuid", "yasna", + "zerocopy 0.7.26", "zeroize", "zip", ] @@ -5540,7 +5594,7 @@ dependencies = [ "instant", "libc", "redox_syscall 0.2.16", - "smallvec 1.11.0", + "smallvec 1.11.2", "winapi", ] @@ -5553,7 +5607,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall 0.3.5", - "smallvec 1.11.0", + "smallvec 1.11.2", "windows-targets 0.48.5", ] @@ -5629,6 +5683,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" @@ -7231,9 +7294,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", ] @@ -7269,9 +7332,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", @@ -7766,9 +7829,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" @@ -8226,6 +8289,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" @@ -8813,7 +8894,7 @@ dependencies = [ "ipnet", "lazy_static", "rand 0.8.5", - "smallvec 1.11.0", + "smallvec 1.11.2", "thiserror", "tinyvec", "tokio", @@ -8834,7 +8915,7 @@ dependencies = [ "lru-cache", "parking_lot 0.12.1", "resolv-conf", - "smallvec 1.11.0", + "smallvec 1.11.2", "thiserror", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 5591dcebc9..7ea34b795f 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 3f7fd1c7f2..b734248f32 100644 --- a/sled-agent/Cargo.toml +++ b/sled-agent/Cargo.toml @@ -85,6 +85,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 8430dc0731..8f572b46a0 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}\"") + } + } + } +} diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 1d14b26a69..f462fd5b6d 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -14,6 +14,7 @@ publish = false ### BEGIN HAKARI SECTION [dependencies] +ahash = { version = "0.8.6" } anyhow = { version = "1.0.75", features = ["backtrace"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } bit-set = { version = "0.5.3" } @@ -86,8 +87,8 @@ reqwest = { version = "0.11.22", features = ["blocking", "json", "rustls-tls", " ring = { version = "0.17.7", features = ["std"] } schemars = { version = "0.8.13", features = ["bytes", "chrono", "uuid1"] } semver = { version = "1.0.20", features = ["serde"] } -serde = { version = "1.0.192", features = ["alloc", "derive", "rc"] } -serde_json = { version = "1.0.108", features = ["raw_value"] } +serde = { version = "1.0.193", features = ["alloc", "derive", "rc"] } +serde_json = { version = "1.0.108", features = ["raw_value", "unbounded_depth"] } sha2 = { version = "0.10.8", features = ["oid"] } similar = { version = "2.3.0", features = ["inline", "unicode"] } slog = { version = "2.7.0", features = ["dynamic-keys", "max_level_trace", "release_max_level_debug", "release_max_level_trace"] } @@ -111,10 +112,12 @@ unicode-normalization = { version = "0.1.22" } usdt = { version = "0.3.5" } uuid = { version = "1.6.1", features = ["serde", "v4"] } yasna = { version = "0.5.2", features = ["bit-vec", "num-bigint", "std", "time"] } +zerocopy = { version = "0.7.26", features = ["derive", "simd"] } zeroize = { version = "1.7.0", features = ["std", "zeroize_derive"] } zip = { version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } [build-dependencies] +ahash = { version = "0.8.6" } anyhow = { version = "1.0.75", features = ["backtrace"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } bit-set = { version = "0.5.3" } @@ -187,8 +190,8 @@ reqwest = { version = "0.11.22", features = ["blocking", "json", "rustls-tls", " ring = { version = "0.17.7", features = ["std"] } schemars = { version = "0.8.13", features = ["bytes", "chrono", "uuid1"] } semver = { version = "1.0.20", features = ["serde"] } -serde = { version = "1.0.192", features = ["alloc", "derive", "rc"] } -serde_json = { version = "1.0.108", features = ["raw_value"] } +serde = { version = "1.0.193", features = ["alloc", "derive", "rc"] } +serde_json = { version = "1.0.108", features = ["raw_value", "unbounded_depth"] } sha2 = { version = "0.10.8", features = ["oid"] } similar = { version = "2.3.0", features = ["inline", "unicode"] } slog = { version = "2.7.0", features = ["dynamic-keys", "max_level_trace", "release_max_level_debug", "release_max_level_trace"] } @@ -213,6 +216,7 @@ unicode-normalization = { version = "0.1.22" } usdt = { version = "0.3.5" } uuid = { version = "1.6.1", features = ["serde", "v4"] } yasna = { version = "0.5.2", features = ["bit-vec", "num-bigint", "std", "time"] } +zerocopy = { version = "0.7.26", features = ["derive", "simd"] } zeroize = { version = "1.7.0", features = ["std", "zeroize_derive"] } zip = { version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] }