| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use assert_matches::assert_matches; |
| use component_events::events::*; |
| use component_events::matcher::*; |
| use diagnostics_reader::{ArchiveReader, Inspect, RetryConfig, ToSelectorArguments}; |
| use fidl_fuchsia_component::BinderMarker; |
| use fidl_fuchsia_samplertestcontroller::SamplerTestControllerProxy; |
| use fidl_test_persistence_factory::ControllerProxy; |
| use fuchsia_async as fasync; |
| use fuchsia_component_test::RealmInstance; |
| use futures::{FutureExt, select}; |
| use log::warn; |
| use pretty_assertions::StrComparison; |
| |
| use serde_json::Value; |
| use std::fs::File; |
| use std::io::ErrorKind; |
| use zx::{MonotonicDuration, MonotonicInstant}; |
| |
| mod mock_fidl; |
| mod mock_filesystems; |
| mod test_topology; |
| |
| /// Name of the service defined in all test_data configs. |
| const SERVICE: &str = "test-service"; |
| |
| /// Name of the tag defined in all test_data configs. |
| const TAG: &str = "test-component-metric"; |
| |
| // When to give up on polling for a change and fail the test. |
| // |
| // For development it may be convenient to set this to 5. For production, slow |
| // virtual devices may cause test flakes even with surprisingly long timeouts. |
| const GIVE_UP_POLLING_SECS: i64 = 120; |
| |
| const METADATA_KEY: &str = "metadata"; |
| const TIMESTAMP_METADATA_KEY: &str = "timestamp"; |
| |
| // Each persisted tag contains a "@timestamps" object with four timestamps that need to be zeroed. |
| const PAYLOAD_KEY: &str = "payload"; |
| const ROOT_KEY: &str = "root"; |
| const PERSIST_KEY: &str = "persist"; |
| const TIMESTAMP_STRUCT_KEY: &str = "@timestamps"; |
| const PUBLISHED_TIME_KEY: &str = "published"; |
| const TIMESTAMP_STRUCT_ENTRIES: [&str; 2] = ["last_sample_boot", "last_sample_utc"]; |
| /// If the "samples" Inspect source is publishing Inspect data, the stringified JSON |
| /// version of that data should include this string. Waiting for it to appear avoids a race |
| /// condition. |
| const KEY_FROM_INSPECT_SOURCE: &str = "samples"; |
| |
| enum Published { |
| /// No Inspect data is available. |
| Waiting, |
| /// Inspect data is available but it's empty. |
| Empty, |
| /// Inspect data is available and populated with the default state of |
| /// single_counter_test_component. |
| Default, |
| /// Custom Inspect data of single_counter_test_component has been set with a |
| /// specific integer. |
| Int(i64), |
| /// Inspect data is available and equal to the specified JSON content. |
| /// `persist_size` needs to be calculated manually when using this. |
| Content(String), |
| /// Publishing failed due to the size of the persisted data exceeded the |
| /// configured maximum. |
| SizeError(i64), |
| } |
| |
| struct TestRealm { |
| options: TestRealmOptions, |
| instance: RealmInstance, |
| inspect: SamplerTestControllerProxy, |
| controller: ControllerProxy, |
| } |
| |
| /// Inspect data should be persisted through the current boot then published on |
| /// the next boot. |
| #[fuchsia::test] |
| async fn persist_simple() { |
| let realm = |
| TestRealm::new(TestRealmOptions::new(include_str!("test_data/config/single_tag.persist"))) |
| .await; |
| |
| // Persistence should fetch Inspect state immediately after starting. |
| realm.set_inspect(Some(19i64)).await; |
| wait_for_inspect_source(&realm.instance).await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| |
| // Persistence should publish the Inspect state. |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Int(19)).await; |
| } |
| |
| /// A component overwriting their Inspect data with new values MUST reflect in |
| /// persisted data. |
| #[fuchsia::test] |
| async fn overwrite_with_some() { |
| let realm = |
| TestRealm::new(TestRealmOptions::new(include_str!("test_data/config/single_tag.persist"))) |
| .await; |
| |
| // Persistence should fetch Inspect state immediately after starting. |
| realm.set_inspect(Some(19i64)).await; |
| wait_for_inspect_source(&realm.instance).await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| |
| // Wait at least one sample period. |
| zx::MonotonicDuration::from_seconds(2).sleep(); |
| |
| // Persistence should overwrite persisted data with the new state after the |
| // sample period. |
| realm.set_inspect(Some(20i64)).await; |
| zx::MonotonicDuration::from_seconds(1).sleep(); |
| |
| // Persistence should publish the Inspect state. |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Int(20)).await; |
| } |
| |
| /// A component un-publishing its Inspect data MUST NOT remove previously |
| /// persisted data. If a component crashes, its persisted Inspect state needs to |
| /// remain for debugging. |
| #[fuchsia::test] |
| async fn overwrite_with_none() { |
| let realm = |
| TestRealm::new(TestRealmOptions::new(include_str!("test_data/config/single_tag.persist"))) |
| .await; |
| |
| // Persistence should fetch Inspect state immediately after starting. |
| realm.set_inspect(Some(19i64)).await; |
| wait_for_inspect_source(&realm.instance).await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| |
| // Wait at least one sample period. |
| zx::MonotonicDuration::from_seconds(2).sleep(); |
| |
| // Persistence should not remove existing persisted data if not found. |
| realm.set_inspect(None).await; |
| zx::MonotonicDuration::from_seconds(2).sleep(); |
| |
| // Persistence should publish the Inspect state. |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Int(19)).await; |
| } |
| |
| /// Tags are updated independently from one another. Updates to one should not |
| /// affect others, unless they contain overlapping selectors. |
| #[fuchsia::test] |
| async fn two_tags() { |
| let realm = |
| TestRealm::new(TestRealmOptions::new(include_str!("test_data/config/two_tags.persist"))) |
| .await; |
| |
| // Persistence should fetch Inspect state immediately after starting. |
| realm.set_inspect(Some(100i64)).await; |
| realm.increment_integer_counter(1).await; |
| wait_for_inspect_source(&realm.instance).await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| |
| // Wait at least one sample period. integer_1 should be updated to 11. |
| zx::MonotonicDuration::from_seconds(2).sleep(); |
| realm.increment_integer_counter(2).await; |
| realm.increment_integer_counter(2).await; |
| |
| // Wait at least another sample period. integer_2 should be updated to be 12. |
| zx::MonotonicDuration::from_seconds(2).sleep(); |
| |
| // Persistence should publish the Inspect state. |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| |
| let realm_name = realm.instance.root.child_name(); |
| realm |
| .verify_diagnostics_persistence_publication(Published::Content(format!( |
| r#" |
| "{SERVICE}": {{ |
| "test-component-metric-a": {{ |
| "@errors": [], |
| "@persist_size": 43, |
| "@timestamps": {{ |
| "last_sample_boot": 0, |
| "last_sample_utc": 0 |
| }}, |
| "realm_builder:{realm_name}/single_counter": {{ |
| "samples": {{ |
| "optional": 100, |
| "integer_1": 11 |
| }} |
| }} |
| }}, |
| "test-component-metric-b": {{ |
| "@errors": [], |
| "@persist_size": 43, |
| "@timestamps": {{ |
| "last_sample_boot": 0, |
| "last_sample_utc": 0 |
| }}, |
| "realm_builder:{realm_name}/single_counter": {{ |
| "samples": {{ |
| "optional": 100, |
| "integer_2": 22 |
| }} |
| }} |
| }} |
| }} |
| "# |
| ))) |
| .await; |
| } |
| |
| /// Configuring `min_seconds_between_fetch` should limit Persistence from |
| /// persisting new Inspect state too often. |
| #[fuchsia::test] |
| async fn waits_sample_period() { |
| let realm = |
| TestRealm::new(TestRealmOptions::new(include_str!("test_data/config/never_fetch.persist"))) |
| .await; |
| |
| // Persistence should fetch Inspect state immediately after starting. |
| realm.set_inspect(Some(19i64)).await; |
| wait_for_inspect_source(&realm.instance).await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| |
| // Persistence should NOT fetch the new state immediately; it should still |
| // be waiting for `min_seconds_between_fetch` to elapse. |
| realm.set_inspect(Some(20i64)).await; |
| realm.verify_diagnostics_persistence_publication(Published::Waiting).await; |
| |
| // Persistence should publish the older Inspect state. |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Int(19)).await; |
| } |
| |
| /// Persisted data shouldn't be published until Persistence is killed and |
| /// restarted, and the update has completed. |
| #[fuchsia::test] |
| async fn waits_to_publish_data() { |
| let realm = |
| TestRealm::new(TestRealmOptions::new(include_str!("test_data/config/single_tag.persist"))) |
| .await; |
| |
| // Persistence should fetch Inspect state immediately after starting. |
| realm.set_inspect(Some(19i64)).await; |
| wait_for_inspect_source(&realm.instance).await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| zx::MonotonicDuration::from_seconds(2).sleep(); // Wait at least 1 sample period. |
| realm.verify_diagnostics_persistence_publication(Published::Waiting).await; |
| |
| // Persistence should not publish without the update check. |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| zx::MonotonicDuration::from_seconds(2).sleep(); // Wait at least 1 sample period. |
| realm.verify_diagnostics_persistence_publication(Published::Waiting).await; |
| |
| // A successful update check will trigger publishing of persisted data. |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Int(19)).await; |
| |
| // After another restart, data for tags without "persist_across_boot" |
| // shouldn't be published before nor after update is completed. |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Default).await; |
| } |
| |
| /// When `skip_update_check` is enabled, Persistence shouldn't wait for a |
| /// successful update check before publishing. |
| #[fuchsia::test] |
| async fn skip_update_check() { |
| let realm = TestRealm::new( |
| TestRealmOptions::new(include_str!("test_data/config/single_tag.persist")) |
| .skip_update_check(), |
| ) |
| .await; |
| |
| realm.set_inspect(Some(19i64)).await; |
| wait_for_inspect_source(&realm.instance).await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.verify_diagnostics_persistence_publication(Published::Int(19)).await; |
| } |
| |
| /// Persisting data larger than max_bytes should abort the operation, instead |
| /// inserting an error into the persisted data. |
| #[fuchsia::test] |
| async fn too_big() { |
| let realm = |
| TestRealm::new(TestRealmOptions::new(include_str!("test_data/config/too_big.persist"))) |
| .await; |
| |
| realm.set_inspect(Some(9i64)).await; |
| wait_for_inspect_source(&realm.instance).await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| zx::MonotonicDuration::from_seconds(2).sleep(); // Wait at least 1 sample period. |
| |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::SizeError(9i64)).await; |
| } |
| |
| /// Tags with persist_across_boot should remain after restart. |
| #[fuchsia::test] |
| async fn persist_across_boot() { |
| let realm = TestRealm::new(TestRealmOptions::new(include_str!( |
| "test_data/config/persist_across_boot.persist" |
| ))) |
| .await; |
| |
| // Set the Inspect field to a custom value. This value won't publish till |
| // the next reboot. |
| realm.set_inspect(Some(8i64)).await; |
| wait_for_inspect_source(&realm.instance).await; |
| realm.start_persistence().await; |
| realm.wait_for_disk_write(); |
| realm.verify_diagnostics_persistence_publication(Published::Waiting).await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Empty).await; |
| |
| // After the first reboot, we should see the custom value. It should also |
| // persist this value to the current persisted data. |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Int(8)).await; |
| |
| // The next boot should continue to have the custom value since this tag is |
| // marked with "persist_across_reboot". |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Int(8)).await; |
| |
| // Once more for good measure. |
| let realm = realm.restart().await; |
| realm.start_persistence().await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Int(8)).await; |
| } |
| |
| /// Verify Persistence starts and never stops. |
| #[fuchsia::test] |
| async fn never_idles() { |
| let realm = |
| TestRealm::new(TestRealmOptions::new(include_str!("test_data/config/single_tag.persist"))) |
| .await; |
| let mut event_stream = EventStream::open().await.unwrap(); |
| realm.start_persistence().await; |
| |
| // Listen for the start event. |
| let moniker = format!(".*{}.*persistence$", realm.instance.root.child_name()); |
| EventMatcher::ok() |
| .moniker_regex(moniker.clone()) |
| .wait::<Started>(&mut event_stream) |
| .await |
| .unwrap(); |
| |
| // Allow Persistence to publish to Inspect. |
| realm.wait_for_disk_write(); |
| realm.verify_diagnostics_persistence_publication(Published::Waiting).await; |
| realm.set_update_completed().await; |
| realm.verify_diagnostics_persistence_publication(Published::Empty).await; |
| |
| // Listen for the stop event. There shouldn't be one. |
| let mut stop_event = Box::pin( |
| EventMatcher::ok().moniker_regex(moniker.clone()).wait::<Stopped>(&mut event_stream).fuse(), |
| ); |
| select! { |
| event = &mut stop_event => panic!("Unexpected stop event {event:?}"), |
| _ = fasync::Timer::new(MonotonicDuration::from_seconds(5)).fuse() => {}, |
| }; |
| } |
| |
| /// The Inspect source may not publish Inspect (via take_and_serve_directory_handle()) until |
| /// some time after the FIDL call that woke it up has returned. This function verifies that |
| /// the Inspect source is actually publishing data to avoid a race condition. |
| async fn wait_for_inspect_source(realm: &RealmInstance) { |
| let accessor_proxy = realm |
| .root |
| .connect_to_protocol_at_exposed_dir() |
| .expect("Failed to connect to ArchiveAccessor"); |
| let mut inspect_fetcher = ArchiveReader::inspect(); |
| inspect_fetcher |
| .with_archive(accessor_proxy) |
| .retry(RetryConfig::never()) |
| .add_selector("realm_builder*/single_counter:root"); |
| let start_time = MonotonicInstant::get(); |
| |
| loop { |
| assert!( |
| start_time + MonotonicDuration::from_seconds(GIVE_UP_POLLING_SECS) |
| > MonotonicInstant::get() |
| ); |
| let published_inspect = |
| inspect_fetcher.snapshot_raw::<serde_json::Value>().await.unwrap().to_string(); |
| if published_inspect.contains(KEY_FROM_INSPECT_SOURCE) { |
| return; |
| } |
| fasync::Timer::new(MonotonicDuration::from_millis(100)).await; |
| } |
| } |
| |
| pub(crate) struct TestRealmOptions { |
| name: String, |
| config: &'static str, |
| filesystem: mock_filesystems::TestFs, |
| skip_update_check: bool, |
| } |
| |
| impl TestRealmOptions { |
| fn new(config: &'static str) -> Self { |
| // Generate a unique name for each test realm to prevent conflicts |
| // between test runs. |
| let name = { |
| let id: u64 = rand::random(); |
| format!("auto-{id:x}") |
| }; |
| Self { name, config, filesystem: mock_filesystems::TestFs::new(), skip_update_check: false } |
| } |
| fn skip_update_check(self) -> Self { |
| Self { skip_update_check: true, ..self } |
| } |
| } |
| |
| impl TestRealm { |
| async fn new(options: TestRealmOptions) -> Self { |
| let instance = test_topology::create(&options).await; |
| // `inspect` is the source of Inspect data that Persistence will read and persist. |
| let inspect = instance.root.connect_to_protocol_at_exposed_dir().unwrap(); |
| // `controller` is the connection to send control signals to the test's update-checker mock. |
| let controller = instance.root.connect_to_protocol_at_exposed_dir().unwrap(); |
| TestRealm { options, instance, inspect, controller } |
| } |
| |
| async fn start_persistence(&self) { |
| // Start up the Persistence component by sending a request to one of its |
| // served FIDLs. |
| let _persistence_binder = self |
| .instance |
| .root |
| .connect_to_named_protocol_at_exposed_dir::<BinderMarker>( |
| "fuchsia.component.PersistenceBinder", |
| ) |
| .unwrap(); |
| |
| // These test are single threaded and will deadlock while loading |
| // Persistence configs unless sleeps are added to allow |
| // config-data-server to serve requests. This could also be fixed by |
| // setting threads=2 in the fuchsia_test macro. |
| if self.options.skip_update_check { |
| let inspector = self.archive_reader_with_selector(format!( |
| "realm_builder\\:{}/persistence:root/fuchsia.inspect.Health:status", |
| self.instance.root.child_name() |
| )); |
| |
| loop { |
| fasync::Timer::new(MonotonicDuration::from_millis(100)).await; |
| let snapshot = inspector.snapshot().await.unwrap(); |
| if let Some(hierarchy) = snapshot.into_iter().next().and_then(|d| d.payload) |
| && let Some(status) = hierarchy |
| .get_property_by_path(&["fuchsia.inspect.Health", "status"]) |
| .and_then(|p| p.string()) |
| && status == "OK" |
| { |
| break; |
| } |
| } |
| } else { |
| self.verify_diagnostics_persistence_publication(Published::Waiting).await; |
| } |
| } |
| |
| // Wait for Persistence to finish writing current data to disk. |
| fn wait_for_disk_write(&self) { |
| let current_data_path = format!("{}/current.json", self.options.filesystem.cache()); |
| loop { |
| match File::open(¤t_data_path) { |
| Ok(_) => break, |
| Err(e) |
| if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::ResourceBusy => |
| { |
| MonotonicDuration::from_millis(100).sleep(); |
| } |
| Err(e) => panic!("Unexpected error {e}"), |
| } |
| } |
| } |
| |
| async fn set_update_completed(&self) { |
| self.controller.set_update_completed().await.expect("This should never fail"); |
| } |
| |
| /// Set the `optional` value to a given number, or remove it from the Inspect tree. |
| async fn set_inspect(&self, value: Option<i64>) { |
| match value { |
| Some(value) => { |
| self.inspect.set_optional(value).await.expect("set_optional should work") |
| } |
| None => self.inspect.remove_optional().await.expect("remove_optional should work"), |
| }; |
| } |
| |
| /// Increment an "integer_*" counter in single-counter-test-component by ID. |
| /// |
| /// The test component contains 3 integer properties: |
| /// - integer_1 |
| /// - integer_2 |
| /// - integer_3 |
| async fn increment_integer_counter(&self, counter: u16) { |
| // Integer properties are included into the map at a +1 offset. |
| self.inspect.increment_int(counter + 1).await.expect("Incrementing counter failed") |
| } |
| |
| /// Tear down the realm to make sure everything is gone before you restart it. |
| /// Then create and return a new realm. |
| async fn restart(self) -> TestRealm { |
| let Self { options, instance, inspect: _inspect, controller: _controller } = self; |
| instance.destroy().await.expect("destroy should work"); |
| TestRealm::new(options).await |
| } |
| |
| /// Create an ArchiveReader scoped to this test realm. |
| fn archive_reader_with_selector(&self, selector: impl ToSelectorArguments) -> ArchiveReader<Inspect> { |
| let accessor_proxy = self |
| .instance |
| .root |
| .connect_to_protocol_at_exposed_dir() |
| .expect("Failed to connect to ArchiveAccessor"); |
| let mut reader = ArchiveReader::inspect(); |
| reader.with_archive(accessor_proxy).retry(RetryConfig::never()).add_selector(selector); |
| reader |
| } |
| |
| /// Verify that the expected data is published by Persistence in its Inspect hierarchy. |
| async fn verify_diagnostics_persistence_publication(&self, published: Published) { |
| let inspect_fetcher = self.archive_reader_with_selector(format!( |
| "realm_builder\\:{}/persistence:root", |
| self.instance.root.child_name() |
| )); |
| |
| loop { |
| fasync::Timer::new(MonotonicDuration::from_millis(100)).await; |
| let published_inspect = |
| inspect_fetcher.snapshot_raw::<serde_json::Value>().await.unwrap(); |
| let published_inspect = serde_json::to_string_pretty(&published_inspect).unwrap(); |
| if matches!(published, Published::Waiting) && published_inspect.contains("STARTING_UP") |
| { |
| break; |
| } else if published_inspect.contains(PUBLISHED_TIME_KEY) { |
| assert!(json_strings_match( |
| &clean_component_url(unbrittle_too_big_message(zero_and_test_timestamps( |
| &published_inspect |
| ))), |
| &expected_diagnostics_persistence_inspect( |
| self.instance.root.child_name(), |
| &self.options, |
| published |
| ), |
| "persistence publication" |
| )); |
| break; |
| } |
| } |
| } |
| } |
| |
| /// Given a mut map from a JSON object that's presumably sourced from Inspect, if it contains a |
| /// timestamp record entry, this function validates fields exist and zeros them. |
| fn clean_and_test_timestamps(map: &mut serde_json::Map<String, Value>) { |
| if let Some(Value::Object(map)) = map.get_mut(TIMESTAMP_STRUCT_KEY) { |
| for key in TIMESTAMP_STRUCT_ENTRIES.iter() { |
| assert_matches!( |
| map.insert(key.to_string(), serde_json::json!(0)), |
| Some(Value::Number(_)) |
| ); |
| } |
| } |
| } |
| |
| /// The number of bytes reported in the "too big" case may vary. It should be a 2-digit |
| /// number. Replace with underscores. |
| fn unbrittle_too_big_message(contents: String) -> String { |
| let matcher = regex::Regex::new(r"Data too big: \d{2} > max length 10").unwrap(); |
| matcher.replace_all(&contents, "Data too big: __ > max length 10").to_string() |
| } |
| |
| /// Remove index in component_url. |
| fn clean_component_url(contents: String) -> String { |
| let matcher = regex::Regex::new(r"realm-builder://\d+/persistence").unwrap(); |
| matcher.replace_all(&contents, "realm-builder/persistence").to_string() |
| } |
| |
| fn json_strings_match(observed: &str, expected: &str, context: &str) -> bool { |
| let mut observed_json: Value = serde_json::from_str(observed) |
| .unwrap_or_else(|e| panic!("Error parsing observed json in {context}: {e:?}")); |
| |
| // Remove health nodes if they exist. |
| if let Some(v) = observed_json.as_array_mut() { |
| for hierarchy in v.iter_mut() { |
| if let Some(Some(root)) = |
| hierarchy.pointer_mut("/payload/root").map(|r| r.as_object_mut()) |
| { |
| root.remove("fuchsia.inspect.Health"); |
| } |
| } |
| } |
| |
| let expected_json: Value = serde_json::from_str(expected).unwrap_or_else(|e| { |
| panic!("Error parsing expected json in {context}: {e:?}, data: {expected}") |
| }); |
| |
| if observed_json != expected_json { |
| let observed = serde_json::to_string_pretty(&observed_json).unwrap(); |
| let expected = serde_json::to_string_pretty(&expected_json).unwrap(); |
| warn!("Observed != expected \n{}", StrComparison::new(&observed, &expected)); |
| } |
| observed_json == expected_json |
| } |
| |
| fn zero_and_test_timestamps(contents: &str) -> String { |
| fn for_all_entries<F>(map: &mut serde_json::Map<String, Value>, func: F) |
| where |
| F: Fn(&mut serde_json::Map<String, Value>), |
| { |
| for (_key, value) in map.iter_mut() { |
| if let Value::Object(inner_map) = value { |
| func(inner_map); |
| } |
| } |
| } |
| |
| let result_json: Value = serde_json::from_str(contents).expect("parsing json failed."); |
| let mut string_result_array = result_json |
| .as_array() |
| .expect("result json is an array of objs.") |
| .iter() |
| .filter_map(|val| { |
| let mut val = val.clone(); |
| |
| val.as_object_mut().map(|obj: &mut serde_json::Map<String, serde_json::Value>| { |
| let metadata_obj = obj.get_mut(METADATA_KEY).unwrap().as_object_mut().unwrap(); |
| metadata_obj.insert(TIMESTAMP_METADATA_KEY.to_string(), serde_json::json!(0)); |
| let payload_obj = obj.get_mut(PAYLOAD_KEY).unwrap(); |
| if let Value::Object(map) = payload_obj |
| && let Some(Value::Object(map)) = map.get_mut(ROOT_KEY) |
| { |
| if map.contains_key(PUBLISHED_TIME_KEY) { |
| map.insert(PUBLISHED_TIME_KEY.to_string(), serde_json::json!(0)); |
| } |
| if let Some(Value::Object(persist_contents)) = map.get_mut(PERSIST_KEY) { |
| for_all_entries(persist_contents, |service_contents| { |
| for_all_entries(service_contents, clean_and_test_timestamps); |
| }); |
| } |
| } |
| serde_json::to_string_pretty(&serde_json::to_value(obj).unwrap()) |
| .expect("All entries in the array are valid.") |
| }) |
| }) |
| .collect::<Vec<String>>(); |
| |
| string_result_array.sort(); |
| |
| format!("[{}]", string_result_array.join(",")) |
| } |
| |
| fn expected_diagnostics_persistence_inspect( |
| realm_name: &str, |
| options: &TestRealmOptions, |
| published: Published, |
| ) -> String { |
| let content = match published { |
| Published::Waiting | Published::Empty | Published::Content(_) => "".to_string(), |
| Published::Default => r#" |
| { |
| "samples": { |
| "integer_1": 10 |
| } |
| } |
| "# |
| .to_string(), |
| Published::SizeError(number) => format!( |
| r#" |
| {{ |
| "samples": {{ |
| "optional": {number}, |
| "integer_1": 10 |
| }} |
| }} |
| "# |
| ), |
| Published::Int(number) => format!( |
| r#" |
| {{ |
| "samples": {{ |
| "optional": {number}, |
| "integer_1": 10 |
| }} |
| }} |
| "# |
| ), |
| }; |
| |
| let persist_size = if content.is_empty() { |
| 0 |
| } else { |
| let value = serde_json::from_str::<serde_json::Value>(&content).unwrap(); |
| let content = serde_json::to_string(&value).unwrap(); |
| content.len() |
| }; |
| |
| let skip_update_check = options.skip_update_check; |
| |
| let config = format!( |
| r#" |
| "config": {{ |
| "skip_update_check": {skip_update_check} |
| }} |
| "# |
| ); |
| |
| let variant = match published { |
| Published::Waiting => format!( |
| r#" |
| {config} |
| "# |
| ), |
| Published::Empty => format!( |
| r#" |
| {config}, |
| "persist": {{}}, |
| "published": 0 |
| "# |
| ), |
| Published::Content(content) => format!( |
| r#" |
| {config}, |
| "persist": {{ |
| {content} |
| }}, |
| "published": 0 |
| "# |
| ), |
| Published::Default | Published::Int(_) => format!( |
| r#" |
| {config}, |
| "persist": {{ |
| "{SERVICE}": {{ |
| "{TAG}": {{ |
| "@errors": [], |
| "@persist_size": {persist_size}, |
| "@timestamps": {{ |
| "last_sample_boot": 0, |
| "last_sample_utc": 0 |
| }}, |
| "realm_builder:{realm_name}/single_counter": {content} |
| }} |
| }} |
| }}, |
| "published": 0 |
| "# |
| ), |
| // unbrittle_too_big_message() will replace a 2-digit number after |
| // "big: " with "__" |
| Published::SizeError(_) => format!( |
| r#" |
| {config}, |
| "persist": {{ |
| "{SERVICE}": {{ |
| "{TAG}": {{ |
| "@errors": [ |
| "Data too big: __ > max length 10" |
| ], |
| "@persist_size": {persist_size}, |
| "@timestamps": {{ |
| "last_sample_boot": 0, |
| "last_sample_utc": 0 |
| }} |
| }} |
| }} |
| }}, |
| "published": 0 |
| "# |
| ), |
| }; |
| |
| format!( |
| r#" |
| [ |
| {{ |
| "data_source": "Inspect", |
| "metadata": {{ |
| "component_url": "realm-builder/persistence", |
| "name": "root", |
| "timestamp": 0 |
| }}, |
| "moniker": "realm_builder:{realm_name}/persistence", |
| "payload": {{ |
| "root": {{ |
| {variant} |
| }} |
| }}, |
| "version": 1 |
| }} |
| ] |
| "# |
| ) |
| } |