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}
26
27impl StubPostBundleManager {
28    pub fn add_random_stub_post_bundle(&self, id: &Id, granularity: DurationMillis, epoch_offset_str: &str, num_posts: u8) -> anyhow::Result<EncodedPostBundleV1> {
29        let bucket_location = BucketLocation::new(BucketType::User, *id, granularity, TimeMillis::from_epoch_offset_str(epoch_offset_str)?)?;
30
31        let mut post_bundle = EncodedPostBundleV1::stub();
32        post_bundle.header.location_id = bucket_location.location_id;
33        post_bundle.header.num_posts = num_posts;
34        for _ in 0..num_posts {
35            post_bundle.header.encoded_post_ids.push(Id::random());
36            post_bundle.header.encoded_post_lengths.push(0);
37        }
38        let result = self.post_bundles.write().insert(bucket_location.location_id, post_bundle.clone());
39        if result.is_some() {
40            warn!("Replaced stub post bundle for location_id: {}", bucket_location.location_id);
41        }
42
43        Ok(post_bundle)
44    }
45
46    pub fn add_stub_post_bundle(&self, id: &Id, granularity: DurationMillis, epoch_offset_str: &str, post_bundle: &EncodedPostBundleV1) -> anyhow::Result<()> {
47        let bucket_location = BucketLocation::new(BucketType::User, *id, granularity, TimeMillis::from_epoch_offset_str(epoch_offset_str)?)?;
48
49        let result = self.post_bundles.write().insert(bucket_location.location_id, post_bundle.clone());
50        if result.is_some() {
51            warn!("Replaced stub post bundle for location_id: {}", bucket_location.location_id);
52        }
53
54        Ok(())
55    }
56}
57
58impl Default for StubPostBundleManager {
59    fn default() -> Self {
60        Self { post_bundles: RwLock::new(HashMap::new()) }
61    }
62}
63
64#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
65#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
66impl PostBundleManager for StubPostBundleManager {
67    async fn get_post_bundle(&self, bucket_location: &BucketLocation, time_millis: TimeMillis) -> anyhow::Result<EncodedPostBundleV1> {
68        let post_bundles = self.post_bundles.read();
69        let post_bundle = post_bundles.get(&bucket_location.location_id);
70        let post_bundle = match post_bundle {
71            Some(post_bundle) => {
72                let mut post_bundle = post_bundle.clone();
73                post_bundle.header.time_millis = time_millis;
74                post_bundle.header.overflowed = post_bundle.header.num_posts > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS;
75                post_bundle.header.sealed = post_bundle.header.overflowed || time_millis > bucket_location.bucket_time_millis + bucket_location.duration + config::ENCODED_POST_BUNDLE_V1_ELAPSED_THRESHOLD_MILLIS;
76                trace!("Returning stub postbundle: {}", post_bundle);
77                post_bundle
78            }
79            None => {
80                let sealed = time_millis > bucket_location.bucket_time_millis + bucket_location.duration + config::ENCODED_POST_BUNDLE_V1_ELAPSED_THRESHOLD_MILLIS;
81                let mut post_bundle = EncodedPostBundleV1::stub();
82                post_bundle.header.time_millis = time_millis;
83                post_bundle.header.location_id = bucket_location.location_id;
84                post_bundle.header.sealed = sealed;
85                trace!("Returning phantom postbundle: {}", post_bundle);
86                post_bundle
87            }
88        };
89
90        Ok(post_bundle)
91    }
92}
93
94impl EncodedPostBundleV1 {
95    pub fn stub() -> Self {
96        let header = EncodedPostBundleHeaderV1 {
97            time_millis: TimeMillis::zero(),
98            location_id: Id::zero(),
99            overflowed: false,
100            sealed: false,
101            num_posts: 0,
102            encoded_post_ids: vec![],
103            encoded_post_lengths: vec![],
104            encoded_post_healed: HashSet::new(),
105            peer: Peer::zero(),
106            signature: Signature::zero(),
107        };
108
109        EncodedPostBundleV1 { header, encoded_posts_bytes: Bytes::new() }
110    }
111}