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