Skip to main content

hashiverse_lib/client/post_bundle/
stub_post_bundle_manager.rs

1//! # Test stub [`PostBundleManager`]
2//!
3//! An in-memory implementation of
4//! [`crate::client::post_bundle::post_bundle_manager::PostBundleManager`] for tests that
5//! need to exercise timeline logic without any network. Lookups return pre-configured
6//! bundles from a `HashMap`, and the stub can synthesise random bundles of a requested
7//! post count on demand. `sealed` / `overflowed` flags are set based on the bucket's
8//! position in time relative to the test clock so edge-case transitions (just-sealed,
9//! just-overflowed) can be reproduced deterministically.
10
11use crate::client::post_bundle::post_bundle_manager::PostBundleManager;
12use crate::protocol::peer::Peer;
13use crate::protocol::posting::encoded_post_bundle::{EncodedPostBundleHeaderV1, EncodedPostBundleV1};
14use crate::tools::buckets::{BucketLocation, BucketType};
15use crate::tools::config;
16use crate::tools::time::{DurationMillis, TimeMillis};
17use crate::tools::types::{Id, Signature};
18use log::{trace, warn};
19use parking_lot::RwLock;
20use std::collections::{HashMap, HashSet};
21use bytes::Bytes;
22
23pub struct StubPostBundleManager {
24    post_bundles: RwLock<HashMap<Id, EncodedPostBundleV1>>,
25    requested_location_ids: RwLock<Vec<Id>>,
26}
27
28impl StubPostBundleManager {
29    pub fn add_random_stub_post_bundle(&self, id: &Id, granularity: DurationMillis, epoch_offset_str: &str, num_posts: u8) -> anyhow::Result<EncodedPostBundleV1> {
30        let bucket_location = BucketLocation::new(BucketType::User, *id, granularity, TimeMillis::from_epoch_offset_str(epoch_offset_str)?)?;
31
32        let mut post_bundle = EncodedPostBundleV1::stub();
33        post_bundle.header.location_id = bucket_location.location_id;
34        post_bundle.header.num_posts = num_posts;
35        for _ in 0..num_posts {
36            post_bundle.header.encoded_post_ids.push(Id::random());
37            post_bundle.header.encoded_post_lengths.push(0);
38        }
39        let result = self.post_bundles.write().insert(bucket_location.location_id, post_bundle.clone());
40        if result.is_some() {
41            warn!("Replaced stub post bundle for location_id: {}", bucket_location.location_id);
42        }
43
44        Ok(post_bundle)
45    }
46
47    pub fn add_stub_post_bundle(&self, id: &Id, granularity: DurationMillis, epoch_offset_str: &str, post_bundle: &EncodedPostBundleV1) -> anyhow::Result<()> {
48        let bucket_location = BucketLocation::new(BucketType::User, *id, granularity, TimeMillis::from_epoch_offset_str(epoch_offset_str)?)?;
49
50        let result = self.post_bundles.write().insert(bucket_location.location_id, post_bundle.clone());
51        if result.is_some() {
52            warn!("Replaced stub post bundle for location_id: {}", bucket_location.location_id);
53        }
54
55        Ok(())
56    }
57
58    /// Every location_id passed to `get_post_bundle`, in request order.
59    pub fn requested_location_ids(&self) -> Vec<Id> {
60        self.requested_location_ids.read().clone()
61    }
62}
63
64impl Default for StubPostBundleManager {
65    fn default() -> Self {
66        Self { post_bundles: RwLock::new(HashMap::new()), requested_location_ids: RwLock::new(Vec::new()) }
67    }
68}
69
70#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
71#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
72impl PostBundleManager for StubPostBundleManager {
73    async fn get_post_bundle(&self, bucket_location: &BucketLocation, time_millis: TimeMillis) -> anyhow::Result<EncodedPostBundleV1> {
74        self.requested_location_ids.write().push(bucket_location.location_id);
75        let post_bundles = self.post_bundles.read();
76        let post_bundle = post_bundles.get(&bucket_location.location_id);
77        let post_bundle = match post_bundle {
78            Some(post_bundle) => {
79                let mut post_bundle = post_bundle.clone();
80                post_bundle.header.time_millis = time_millis;
81                post_bundle.header.overflowed = post_bundle.header.num_posts > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS;
82                post_bundle.header.sealed = bucket_location.is_sealed_at(time_millis, post_bundle.header.overflowed);
83                trace!("Returning stub postbundle: {}", post_bundle);
84                post_bundle
85            }
86            None => {
87                let sealed = bucket_location.is_sealed_at(time_millis, false);
88                let mut post_bundle = EncodedPostBundleV1::stub();
89                post_bundle.header.time_millis = time_millis;
90                post_bundle.header.location_id = bucket_location.location_id;
91                post_bundle.header.sealed = sealed;
92                trace!("Returning phantom postbundle: {}", post_bundle);
93                post_bundle
94            }
95        };
96
97        Ok(post_bundle)
98    }
99}
100
101impl EncodedPostBundleV1 {
102    pub fn stub() -> Self {
103        let header = EncodedPostBundleHeaderV1 {
104            time_millis: TimeMillis::zero(),
105            location_id: Id::zero(),
106            overflowed: false,
107            sealed: false,
108            num_posts: 0,
109            encoded_post_ids: vec![],
110            encoded_post_lengths: vec![],
111            encoded_post_healed: HashSet::new(),
112            peer: Peer::zero(),
113            signature: Signature::zero(),
114        };
115
116        EncodedPostBundleV1 { header, encoded_posts_bytes: Bytes::new() }
117    }
118}