diff --git a/src/tool/subcommands/api_cmd/test_snapshot.rs b/src/tool/subcommands/api_cmd/test_snapshot.rs index 0edf43b4aed..5c88445fc32 100644 --- a/src/tool/subcommands/api_cmd/test_snapshot.rs +++ b/src/tool/subcommands/api_cmd/test_snapshot.rs @@ -70,6 +70,49 @@ fn backfill_eth_mappings(db: &MemoryDB, index: Option) -> anyhow::Result< Ok(()) } +/// JSON fields filtered from both actual and expected responses +/// before strict raw-JSON snapshot comparison, scoped to the methods whose +/// responses actually contain them. +fn fields_to_filter(method: &str) -> &'static [&'static str] { + match method { + // Skip time taken and duration as they are non-deterministic. + "Filecoin.StateCall" | "Filecoin.StateReplay" | "Filecoin.StateCompute" => { + &["Duration", "tt"] + } + // Skip `accessList` as it is known-divergent from Lotus. + // See . + "Filecoin.EthGetBlockByHash" + | "Filecoin.EthGetBlockByNumber" + | "Filecoin.EthGetTransactionByHash" + | "Filecoin.EthGetTransactionByHashLimited" + | "Filecoin.EthGetTransactionByBlockHashAndIndex" + | "Filecoin.EthGetTransactionByBlockNumberAndIndex" => &["accessList"], + _ => &[], + } +} + +/// Recursively filters `fields` from a JSON value so the strict raw-JSON +/// snapshot comparison ignores them. +fn filter_out_fields(value: &mut serde_json::Value, fields: &[&str]) { + if fields.is_empty() { + return; + } + match value { + serde_json::Value::Object(map) => { + for field in fields { + map.remove(*field); + } + for val in map.values_mut() { + filter_out_fields(val, fields); + } + } + serde_json::Value::Array(items) => { + items.iter_mut().for_each(|v| filter_out_fields(v, fields)); + } + _ => {} + } +} + pub async fn run_test_from_snapshot(path: &Path) -> anyhow::Result<()> { let mut run = false; let snapshot_bytes = std::fs::read(path)?; @@ -135,22 +178,18 @@ pub async fn run_test_from_snapshot(path: &Path) -> anyhow::Result<()> { { let params = <$ty>::parse_params(params_raw.clone(), ParamStructure::Either) .context("failed to parse params")?; - let result = <$ty>::handle(ctx.clone(), params, &ext) + let mut result = <$ty>::handle(ctx.clone(), params, &ext) .await - .map(|r| { - let lotus_json = r.into_lotus_json(); - // Normalize through the same serde round-trip the golden is read with - // below, so a response Forest generated reproduces itself. - // A more strict approach would be to compare JSONs directly, tracked in https://github.com/ChainSafe/forest/issues/7313 - serde_json::to_value(&lotus_json) - .and_then(serde_json::from_value) - .unwrap_or(lotus_json) - }) - .map_err(|e| e.deref().to_string()); - let expected = match expected_response.clone() { - Ok(v) => serde_json::from_value(v).map_err(|e| e.to_string()), - Err(e) => Err(e), - }; + .map_err(|e| e.deref().to_string()) + .and_then(|r| r.into_lotus_json_value().map_err(|e| e.to_string())); + let mut expected = expected_response.clone(); + let fields = fields_to_filter(<$ty>::NAME); + if let Ok(v) = result.as_mut() { + filter_out_fields(v, fields); + } + if let Ok(v) = expected.as_mut() { + filter_out_fields(v, fields); + } pretty_assertions::assert_eq!(result, expected); run = true; } diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index 38fca390f2b..6ed1df97207 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -12,7 +12,7 @@ filecoin_chaingetfinalizedtipset_1776082510830037.rpcsnap.json.zst filecoin_chaingetgenesis_1736937286915866.rpcsnap.json.zst filecoin_chaingetmessage_1758734340836824.rpcsnap.json.zst filecoin_chaingetmessagesintipset_1758734696116136.rpcsnap.json.zst -filecoin_chaingetparentmessages_1736937305551928.rpcsnap.json.zst +filecoin_chaingetparentmessages_1783660760016875.rpcsnap.json.zst filecoin_chaingetparentreceipts_1736937305550289.rpcsnap.json.zst filecoin_chaingetpath_1736937942817384.rpcsnap.json.zst filecoin_chaingettipset_1736937942817675.rpcsnap.json.zst