Skip to main content

mithril_build_script/
fake_aggregator.rs

1use std::collections::BTreeMap;
2use std::collections::BTreeSet;
3use std::fmt::Write as _;
4use std::fs;
5use std::fs::File;
6use std::path::Path;
7
8use serde_json;
9
10pub type ArtifactId = String;
11pub type FileContent = String;
12
13/// In memory representation of a folder containing data imported using the `scripts/import.sh` script
14/// of the fake aggregator.
15#[derive(Debug, Default)]
16pub struct FakeAggregatorData {
17    status: FileContent,
18
19    epoch_settings: FileContent,
20
21    certificates_list: FileContent,
22    individual_certificates: BTreeMap<ArtifactId, FileContent>,
23
24    snapshots_list: FileContent,
25    individual_snapshots: BTreeMap<ArtifactId, FileContent>,
26
27    mithril_stake_distributions_list: FileContent,
28    individual_mithril_stake_distributions: BTreeMap<ArtifactId, FileContent>,
29
30    cardano_transaction_snapshots_list: FileContent,
31    individual_cardano_transaction_snapshots: BTreeMap<ArtifactId, FileContent>,
32    cardano_transaction_proofs: BTreeMap<ArtifactId, FileContent>,
33
34    cardano_blocks_transactions_snapshots_list: FileContent,
35    individual_cardano_blocks_transactions_snapshots: BTreeMap<ArtifactId, FileContent>,
36    cardano_block_proofs: BTreeMap<ArtifactId, FileContent>,
37    cardano_transaction_v2_proofs: BTreeMap<ArtifactId, FileContent>,
38
39    cardano_stake_distributions_list: FileContent,
40    individual_cardano_stake_distributions: BTreeMap<ArtifactId, FileContent>,
41
42    cardano_database_snapshots_list: FileContent,
43    individual_cardano_database_snapshots: BTreeMap<ArtifactId, FileContent>,
44}
45
46impl FakeAggregatorData {
47    pub fn load_from_folder(folder: &Path) -> Self {
48        let mut data = FakeAggregatorData::default();
49
50        for entry in list_json_files_in_folder(folder) {
51            let filename = entry.file_name().to_string_lossy().to_string();
52            let file_content = fs::read_to_string(entry.path()).unwrap_or_else(|_| {
53                panic!(
54                    "Could not read file content, file_path: {}",
55                    entry.path().display()
56                )
57            });
58
59            match filename.as_str() {
60                "status.json" => {
61                    data.status = file_content;
62                }
63                "epoch-settings.json" => {
64                    data.epoch_settings = file_content;
65                }
66                "mithril-stake-distributions-list.json" => {
67                    data.mithril_stake_distributions_list = file_content;
68                }
69                "snapshots-list.json" => {
70                    data.snapshots_list = file_content;
71                }
72                "cardano-stake-distributions-list.json" => {
73                    data.cardano_stake_distributions_list = file_content;
74                }
75                "cardano-databases-list.json" => {
76                    data.cardano_database_snapshots_list = file_content;
77                }
78                "certificates-list.json" => {
79                    data.certificates_list = file_content;
80                }
81                "ctx-snapshots-list.json" => {
82                    data.cardano_transaction_snapshots_list = file_content;
83                }
84                "cardano-blocks-txs-snapshots-list.json" => {
85                    data.cardano_blocks_transactions_snapshots_list = file_content;
86                }
87                "mithril-stake-distributions.json" => {
88                    data.individual_mithril_stake_distributions =
89                        Self::read_artifacts_json_file(&entry.path());
90                }
91                "snapshots.json" => {
92                    data.individual_snapshots = Self::read_artifacts_json_file(&entry.path());
93                }
94                "cardano-stake-distributions.json" => {
95                    data.individual_cardano_stake_distributions =
96                        Self::read_artifacts_json_file(&entry.path());
97                }
98                "cardano-databases.json" => {
99                    data.individual_cardano_database_snapshots =
100                        Self::read_artifacts_json_file(&entry.path());
101                }
102                "certificates.json" => {
103                    data.individual_certificates = Self::read_artifacts_json_file(&entry.path());
104                }
105                "ctx-snapshots.json" => {
106                    data.individual_cardano_transaction_snapshots =
107                        Self::read_artifacts_json_file(&entry.path());
108                }
109                "cardano-blocks-txs-snapshots.json" => {
110                    data.individual_cardano_blocks_transactions_snapshots =
111                        Self::read_artifacts_json_file(&entry.path());
112                }
113                "ctx-proofs.json" => {
114                    data.cardano_transaction_proofs = Self::read_artifacts_json_file(&entry.path());
115                }
116                "ctx-proofs-v2.json" => {
117                    data.cardano_transaction_v2_proofs =
118                        Self::read_artifacts_json_file(&entry.path());
119                }
120                "cblk-proofs.json" => {
121                    data.cardano_block_proofs = Self::read_artifacts_json_file(&entry.path());
122                }
123                // unknown file
124                _ => {}
125            }
126        }
127
128        data
129    }
130
131    pub fn generate_code_for_ids(self) -> String {
132        let cardano_stake_distributions_per_epoch =
133            extract_item_by_epoch(&self.individual_cardano_stake_distributions, "/epoch");
134        let cardano_database_snapshots_per_epoch =
135            extract_item_list_per_epoch(&self.cardano_database_snapshots_list, "/beacon/epoch");
136
137        Self::assemble_code(
138            &[
139                generate_ids_array(
140                    "snapshot_digests",
141                    BTreeSet::from_iter(self.individual_snapshots.keys().cloned()),
142                ),
143                generate_ids_array(
144                    "mithril_stake_distribution_hashes",
145                    BTreeSet::from_iter(
146                        self.individual_mithril_stake_distributions.keys().cloned(),
147                    ),
148                ),
149                generate_ids_array(
150                    "cardano_stake_distribution_hashes",
151                    BTreeSet::from_iter(
152                        self.individual_cardano_stake_distributions.keys().cloned(),
153                    ),
154                ),
155                generate_epoch_array(
156                    "cardano_stake_distribution_epochs",
157                    BTreeSet::from_iter(cardano_stake_distributions_per_epoch.keys().cloned()),
158                ),
159                generate_ids_array(
160                    "cardano_database_snapshot_hashes",
161                    BTreeSet::from_iter(self.individual_cardano_database_snapshots.keys().cloned()),
162                ),
163                generate_epoch_array(
164                    "cardano_database_snapshot_epochs",
165                    BTreeSet::from_iter(cardano_database_snapshots_per_epoch.keys().cloned()),
166                ),
167                generate_ids_array(
168                    "certificate_hashes",
169                    BTreeSet::from_iter(self.individual_certificates.keys().cloned()),
170                ),
171                generate_ids_array(
172                    "cardano_transaction_snapshot_hashes",
173                    BTreeSet::from_iter(
174                        self.individual_cardano_transaction_snapshots.keys().cloned(),
175                    ),
176                ),
177                generate_ids_array(
178                    "cardano_blocks_transactions_snapshot_hashes",
179                    BTreeSet::from_iter(
180                        self.individual_cardano_blocks_transactions_snapshots.keys().cloned(),
181                    ),
182                ),
183                generate_ids_array(
184                    "proof_transaction_hashes",
185                    BTreeSet::from_iter(self.cardano_transaction_proofs.keys().cloned()),
186                ),
187                generate_ids_array(
188                    "proof_v2_transaction_hashes",
189                    BTreeSet::from_iter(self.cardano_transaction_v2_proofs.keys().cloned()),
190                ),
191                generate_ids_array(
192                    "proof_v2_block_hashes",
193                    BTreeSet::from_iter(self.cardano_block_proofs.keys().cloned()),
194                ),
195            ],
196            false,
197        )
198    }
199
200    pub fn generate_code_for_all_data(self) -> String {
201        let cardano_stake_distributions_per_epoch =
202            extract_item_by_epoch(&self.individual_cardano_stake_distributions, "/epoch");
203        let cardano_database_snapshots_per_epoch =
204            extract_item_list_per_epoch(&self.cardano_database_snapshots_list, "/beacon/epoch");
205
206        Self::assemble_code(
207            &[
208                generate_list_getter("status", self.status),
209                generate_list_getter("epoch_settings", self.epoch_settings),
210                generate_ids_array(
211                    "snapshot_digests",
212                    BTreeSet::from_iter(self.individual_snapshots.keys().cloned()),
213                ),
214                generate_artifact_getter("snapshots", self.individual_snapshots),
215                generate_list_getter("snapshot_list", self.snapshots_list),
216                generate_ids_array(
217                    "mithril_stake_distribution_hashes",
218                    BTreeSet::from_iter(
219                        self.individual_mithril_stake_distributions.keys().cloned(),
220                    ),
221                ),
222                generate_artifact_getter(
223                    "mithril_stake_distributions",
224                    self.individual_mithril_stake_distributions,
225                ),
226                generate_list_getter(
227                    "mithril_stake_distribution_list",
228                    self.mithril_stake_distributions_list,
229                ),
230                generate_ids_array(
231                    "cardano_stake_distribution_hashes",
232                    BTreeSet::from_iter(
233                        self.individual_cardano_stake_distributions.keys().cloned(),
234                    ),
235                ),
236                generate_epoch_array(
237                    "cardano_stake_distribution_epochs",
238                    BTreeSet::from_iter(cardano_stake_distributions_per_epoch.keys().cloned()),
239                ),
240                generate_artifact_per_epoch_getter(
241                    "cardano_stake_distributions_per_epoch",
242                    extract_item_by_epoch(&self.individual_cardano_stake_distributions, "/epoch"),
243                ),
244                generate_artifact_getter(
245                    "cardano_stake_distributions",
246                    self.individual_cardano_stake_distributions,
247                ),
248                generate_list_getter(
249                    "cardano_stake_distribution_list",
250                    self.cardano_stake_distributions_list,
251                ),
252                generate_ids_array(
253                    "certificate_hashes",
254                    BTreeSet::from_iter(self.individual_certificates.keys().cloned()),
255                ),
256                generate_ids_array(
257                    "cardano_database_snapshot_hashes",
258                    BTreeSet::from_iter(self.individual_cardano_database_snapshots.keys().cloned()),
259                ),
260                generate_epoch_array(
261                    "cardano_database_snapshot_epochs",
262                    BTreeSet::from_iter(cardano_database_snapshots_per_epoch.keys().cloned()),
263                ),
264                generate_artifact_getter(
265                    "cardano_database_snapshots",
266                    self.individual_cardano_database_snapshots,
267                ),
268                generate_list_getter(
269                    "cardano_database_snapshot_list",
270                    self.cardano_database_snapshots_list,
271                ),
272                generate_artifact_per_epoch_getter(
273                    "cardano_database_snapshot_list_per_epoch",
274                    cardano_database_snapshots_per_epoch,
275                ),
276                generate_artifact_getter("certificates", self.individual_certificates),
277                generate_list_getter("certificate_list", self.certificates_list),
278                generate_ids_array(
279                    "cardano_transaction_snapshot_hashes",
280                    BTreeSet::from_iter(
281                        self.individual_cardano_transaction_snapshots.keys().cloned(),
282                    ),
283                ),
284                generate_artifact_getter(
285                    "cardano_transaction_snapshots",
286                    self.individual_cardano_transaction_snapshots,
287                ),
288                generate_list_getter(
289                    "cardano_transaction_snapshots_list",
290                    self.cardano_transaction_snapshots_list,
291                ),
292                generate_ids_array(
293                    "proof_transaction_hashes",
294                    BTreeSet::from_iter(self.cardano_transaction_proofs.keys().cloned()),
295                ),
296                generate_artifact_getter(
297                    "cardano_transaction_proofs",
298                    self.cardano_transaction_proofs,
299                ),
300                generate_ids_array(
301                    "cardano_blocks_transactions_snapshot_hashes",
302                    BTreeSet::from_iter(
303                        self.individual_cardano_blocks_transactions_snapshots.keys().cloned(),
304                    ),
305                ),
306                generate_artifact_getter(
307                    "cardano_blocks_transactions_snapshots",
308                    self.individual_cardano_blocks_transactions_snapshots,
309                ),
310                generate_list_getter(
311                    "cardano_blocks_transactions_snapshots_list",
312                    self.cardano_blocks_transactions_snapshots_list,
313                ),
314                generate_ids_array(
315                    "proof_v2_block_hashes",
316                    BTreeSet::from_iter(self.cardano_block_proofs.keys().cloned()),
317                ),
318                generate_artifact_getter("cardano_block_proofs", self.cardano_block_proofs),
319                generate_ids_array(
320                    "proof_v2_transaction_hashes",
321                    BTreeSet::from_iter(self.cardano_transaction_v2_proofs.keys().cloned()),
322                ),
323                generate_artifact_getter(
324                    "cardano_transaction_proofs_v2",
325                    self.cardano_transaction_v2_proofs,
326                ),
327            ],
328            true,
329        )
330    }
331
332    fn assemble_code(functions_code: &[String], include_use_btree_map: bool) -> String {
333        format!(
334            "{}{}
335",
336            if include_use_btree_map {
337                "use std::collections::BTreeMap;
338
339"
340            } else {
341                ""
342            },
343            functions_code.join(
344                "
345
346"
347            )
348        )
349    }
350
351    fn read_artifacts_json_file(json_file: &Path) -> BTreeMap<ArtifactId, FileContent> {
352        let file = File::open(json_file).unwrap();
353        let parsed_json: serde_json::Value = serde_json::from_reader(&file).unwrap();
354
355        let json_object = parsed_json.as_object().unwrap();
356        let res: Result<Vec<_>, _> = json_object
357            .iter()
358            .map(|(key, value)| extract_artifact_id_and_content(key, value))
359            .collect();
360
361        BTreeMap::from_iter(res.unwrap())
362    }
363}
364
365fn extract_artifact_id_and_content(
366    key: &String,
367    value: &serde_json::Value,
368) -> Result<(ArtifactId, FileContent), String> {
369    let json_content = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?;
370    Ok((key.to_owned(), json_content))
371}
372
373/// Takes a map of json string indexed by hashes and re-indexes them using their epoch
374///
375/// Each item in the map must contain an epoch value at the specified JSON pointer location.
376pub fn extract_item_by_epoch(
377    items_per_hash: &BTreeMap<String, String>,
378    json_pointer_for_epoch: &str,
379) -> BTreeMap<u64, String> {
380    let mut res = BTreeMap::new();
381
382    for (key, value) in items_per_hash {
383        let parsed_json: serde_json::Value = serde_json::from_str(value)
384            .unwrap_or_else(|_| panic!("Could not parse JSON entity '{key}'"));
385        let epoch = parsed_json
386            .pointer(json_pointer_for_epoch)
387            .unwrap_or_else(|| panic!("missing `{json_pointer_for_epoch}` for JSON entity '{key}'"))
388            .as_u64()
389            .unwrap_or_else(|| {
390                panic!("`{json_pointer_for_epoch}` is not a number for JSON entity '{key}'")
391            });
392        res.insert(epoch, value.clone());
393    }
394
395    res
396}
397
398/// Takes a JSON string containing a list of items and extracts them into a map keyed by epoch.
399///
400/// Each item in the list must contain an epoch value at the specified JSON pointer location.
401pub fn extract_item_list_per_epoch(
402    source: &str,
403    json_pointer_for_epoch: &str,
404) -> BTreeMap<u64, String> {
405    let parsed_json: Vec<serde_json::Value> =
406        serde_json::from_str(source).expect("Failed to parse JSON list");
407    let mut list_per_epoch = BTreeMap::<u64, Vec<serde_json::Value>>::new();
408
409    for item in parsed_json {
410        let epoch = item
411            .pointer(json_pointer_for_epoch)
412            .unwrap_or_else(|| panic!("missing `{json_pointer_for_epoch}` for a json value"))
413            .as_u64()
414            .unwrap_or_else(|| panic!("`{json_pointer_for_epoch}` is not a number"));
415        list_per_epoch.entry(epoch).or_default().push(item);
416    }
417
418    list_per_epoch
419        .into_iter()
420        .map(|(k, v)| (k, serde_json::to_string(&v).unwrap()))
421        .collect()
422}
423
424pub fn list_json_files_in_folder(folder: &Path) -> impl Iterator<Item = fs::DirEntry> + '_ {
425    crate::list_files_in_folder(folder)
426        .filter(|e| e.file_name().to_string_lossy().ends_with(".json"))
427}
428
429// pub(crate) fn $fun_name()() -> BTreeMap<String, String>
430pub fn generate_artifact_getter(
431    fun_name: &str,
432    source_jsons: BTreeMap<ArtifactId, FileContent>,
433) -> String {
434    let mut artifacts_list = String::new();
435
436    for (artifact_id, file_content) in source_jsons {
437        write!(
438            artifacts_list,
439            r###"
440        (
441            "{artifact_id}",
442            r#"{file_content}"#
443        ),"###
444        )
445        .unwrap();
446    }
447
448    format!(
449        r###"pub(crate) fn {fun_name}() -> BTreeMap<String, String> {{
450    [{artifacts_list}
451    ]
452    .into_iter()
453    .map(|(k, v)| (k.to_owned(), v.to_owned()))
454    .collect()
455}}"###
456    )
457}
458
459// pub(crate) fn $fun_name()() -> BTreeMap<u64, String>
460pub fn generate_artifact_per_epoch_getter(
461    fun_name: &str,
462    source_jsons: BTreeMap<u64, FileContent>,
463) -> String {
464    let mut artifacts_list = String::new();
465
466    for (artifact_id, file_content) in source_jsons {
467        write!(
468            artifacts_list,
469            r###"
470        (
471            {artifact_id},
472            r#"{file_content}"#
473        ),"###
474        )
475        .unwrap();
476    }
477
478    format!(
479        r###"pub(crate) fn {fun_name}() -> BTreeMap<u64, String> {{
480    [{artifacts_list}
481    ]
482    .into_iter()
483    .map(|(k, v)| (k.to_owned(), v.to_owned()))
484    .collect()
485}}"###
486    )
487}
488
489/// pub(crate) fn $fun_name() -> &'static str
490pub fn generate_list_getter(fun_name: &str, source_json: FileContent) -> String {
491    format!(
492        r###"pub(crate) fn {fun_name}() -> &'static str {{
493    r#"{source_json}"#
494}}"###
495    )
496}
497
498/// pub(crate) fn $array_name() -> [&'a str; $ids.len]
499pub fn generate_ids_array(array_name: &str, ids: BTreeSet<ArtifactId>) -> String {
500    let mut ids_list = String::new();
501
502    for id in &ids {
503        write!(
504            ids_list,
505            r#"
506        "{id}","#
507        )
508        .unwrap();
509    }
510
511    format!(
512        r###"pub(crate) const fn {}<'a>() -> [&'a str; {}] {{
513    [{}
514    ]
515}}"###,
516        array_name,
517        ids.len(),
518        ids_list,
519    )
520}
521
522/// pub(crate) fn $array_name() -> [u64; $epoch.len]
523pub fn generate_epoch_array(array_name: &str, epoch: BTreeSet<u64>) -> String {
524    let mut ids_list = String::new();
525
526    for id in &epoch {
527        write!(
528            ids_list,
529            r#"
530        {id},"#
531        )
532        .unwrap();
533    }
534
535    format!(
536        r###"pub(crate) const fn {}() -> [u64; {}] {{
537    [{}
538    ]
539}}"###,
540        array_name,
541        epoch.len(),
542        ids_list,
543    )
544}
545
546#[cfg(test)]
547mod tests {
548    use crate::get_temp_dir;
549
550    use super::*;
551
552    #[test]
553    fn generate_ids_array_with_empty_data() {
554        assert_eq!(
555            generate_ids_array("snapshots_digests", BTreeSet::new()),
556            "pub(crate) const fn snapshots_digests<'a>() -> [&'a str; 0] {
557    [
558    ]
559}"
560        );
561    }
562
563    #[test]
564    fn generate_ids_array_with_non_empty_data() {
565        assert_eq!(
566            generate_ids_array(
567                "snapshots_digests",
568                BTreeSet::from_iter(["abc".to_string(), "def".to_string(), "hij".to_string()])
569            ),
570            r#"pub(crate) const fn snapshots_digests<'a>() -> [&'a str; 3] {
571    [
572        "abc",
573        "def",
574        "hij",
575    ]
576}"#
577        );
578    }
579
580    #[test]
581    fn assemble_code_with_btree_use() {
582        assert_eq!(
583            "use std::collections::BTreeMap;
584
585fn a() {}
586
587fn b() {}
588",
589            FakeAggregatorData::assemble_code(
590                &["fn a() {}".to_string(), "fn b() {}".to_string()],
591                true
592            )
593        )
594    }
595
596    #[test]
597    fn assemble_code_without_btree_use() {
598        assert_eq!(
599            "fn a() {}
600
601fn b() {}
602",
603            FakeAggregatorData::assemble_code(
604                &["fn a() {}".to_string(), "fn b() {}".to_string()],
605                false
606            )
607        )
608    }
609
610    #[test]
611    fn parse_artifacts_json_into_btree_of_key_and_pretty_sub_json() {
612        let dir = get_temp_dir("read_artifacts_json_file");
613        let file = dir.join("test.json");
614        fs::write(
615            &file,
616            r#"{
617    "hash1": { "name": "artifact1" },
618    "hash2": { "name": "artifact2" }
619}"#,
620        )
621        .unwrap();
622
623        let id_per_json = FakeAggregatorData::read_artifacts_json_file(&file);
624
625        let expected = BTreeMap::from([
626            (
627                "hash1".to_string(),
628                r#"{
629  "name": "artifact1"
630}"#
631                .to_string(),
632            ),
633            (
634                "hash2".to_string(),
635                r#"{
636  "name": "artifact2"
637}"#
638                .to_string(),
639            ),
640        ]);
641        assert_eq!(expected, id_per_json);
642    }
643
644    #[test]
645    fn test_extract_item_by_epoch_by_epoch_with_valid_data() {
646        let items_per_hash = BTreeMap::from([
647            (
648                "hash1".to_string(),
649                r#"{"bar":4,"epoch":3,"foo":"...","hash":"2"}"#.to_string(),
650            ),
651            (
652                "hash2".to_string(),
653                r#"{"bar":7,"epoch":2,"foo":"...","hash":"1"}"#.to_string(),
654            ),
655        ]);
656
657        // note: values are not re-serialized, so they are kept as is
658        let item_per_epoch = extract_item_by_epoch(&items_per_hash, "/epoch");
659        assert_eq!(
660            BTreeMap::from([
661                (3, items_per_hash.get("hash1").unwrap().to_string()),
662                (2, items_per_hash.get("hash2").unwrap().to_string())
663            ]),
664            item_per_epoch
665        )
666    }
667
668    #[test]
669    #[should_panic(expected = "Could not parse JSON entity 'csd-123'")]
670    fn test_extract_item_by_epoch_by_epoch_with_invalid_json() {
671        let mut items_per_hash = BTreeMap::new();
672        items_per_hash.insert(
673            "csd-123".to_string(),
674            r#""hash": "csd-123", "epoch": "123"#.to_string(),
675        );
676
677        extract_item_by_epoch(&items_per_hash, "/epoch");
678    }
679
680    #[test]
681    #[should_panic(expected = "missing `/epoch` for JSON entity 'csd-123'")]
682    fn test_extract_item_by_epoch_with_missing_epoch() {
683        let mut items_per_hash = BTreeMap::new();
684        items_per_hash.insert("csd-123".to_string(), r#"{"hash": "csd-123"}"#.to_string());
685
686        extract_item_by_epoch(&items_per_hash, "/epoch");
687    }
688
689    #[test]
690    fn test_extract_item_by_epoch_with_empty_map() {
691        let items_per_hash = BTreeMap::new();
692
693        let epochs = extract_item_by_epoch(&items_per_hash, "/epoch");
694
695        assert!(epochs.is_empty());
696    }
697
698    #[test]
699    fn test_extract_item_list_per_epoch_for_epoch() {
700        let list_per_epoch_json = r#"[
701                { "beacon": { "epoch": 1, "bar": 4 }, "hash":"3","foo":"..." },
702                { "beacon": { "epoch": 2}, "hash":"2","foo":"..." },
703                { "beacon": { "epoch": 1}, "hash":"1","foo":"..." }
704            ]"#;
705
706        // note: values are re-serialized, so serde_json reorders the keys
707        let map_per_epoch = extract_item_list_per_epoch(list_per_epoch_json, "/beacon/epoch");
708        assert_eq!(
709            BTreeMap::from([
710                (
711                    1,
712                    r#"[{"beacon":{"bar":4,"epoch":1},"foo":"...","hash":"3"},{"beacon":{"epoch":1},"foo":"...","hash":"1"}]"#
713                        .to_string()
714                ),
715                (2, r#"[{"beacon":{"epoch":2},"foo":"...","hash":"2"}]"#.to_string()),
716            ]),
717            map_per_epoch
718        )
719    }
720
721    #[test]
722    #[should_panic(expected = "Failed to parse JSON list")]
723    fn test_extract_item_list_per_epoch_with_invalid_json() {
724        // invalid because of the trailing comma
725        let list_per_epoch_json =
726            r#"[ { "beacon": { "epoch": 1, "bar": 4 }, "hash":"3","foo":"..." }, ]"#;
727
728        extract_item_list_per_epoch(list_per_epoch_json, "/epoch");
729    }
730
731    #[test]
732    #[should_panic(expected = "missing `/epoch` for a json value")]
733    fn test_extract_item_list_per_epoch_with_missing_epoch() {
734        let list_per_epoch_json = r#"[ { "beacon": { "bar": 4 }, "hash":"3","foo":"..." } ]"#;
735
736        extract_item_list_per_epoch(list_per_epoch_json, "/epoch");
737    }
738
739    #[test]
740    fn test_extract_item_list_per_epoch_with_list() {
741        let list_per_epoch_json = "[]";
742
743        let epochs = extract_item_list_per_epoch(list_per_epoch_json, "/epoch");
744
745        assert!(epochs.is_empty());
746    }
747}