mithril_client_cli/commands/tools/utxo_hd/
snapshot_converter.rs

1use std::{
2    env, fmt,
3    fs::{create_dir, read_dir, remove_dir_all, rename},
4    path::{Path, PathBuf},
5    process::{Command, Stdio},
6};
7
8use anyhow::{Context, anyhow};
9use chrono::Utc;
10use clap::{Parser, ValueEnum};
11
12use mithril_client::{
13    MithrilError, MithrilResult,
14    common::{CardanoNetwork, MagicId},
15};
16
17use crate::utils::{
18    ArchiveUnpacker, CardanoDbUtils, GitHubReleaseRetriever, HttpDownloader, LedgerFormat,
19    ProgressOutputType, ProgressPrinter, ReqwestGitHubApiClient, ReqwestHttpDownloader, copy_dir,
20    is_version_equal_or_upper, print_simple_warning, remove_dir_contents,
21};
22use crate::{CommandContext, utils::CARDANO_NODE_V10_6_2};
23
24const GITHUB_ORGANIZATION: &str = "IntersectMBO";
25const GITHUB_REPOSITORY: &str = "cardano-node";
26
27const LATEST_DISTRIBUTION_TAG: &str = "latest";
28const PRERELEASE_DISTRIBUTION_TAG: &str = "pre-release";
29
30const WORK_DIR: &str = "tmp";
31const CARDANO_DISTRIBUTION_DIR: &str = "cardano-node-distribution";
32const SNAPSHOTS_DIR: &str = "snapshots";
33
34const SNAPSHOT_CONVERTER_BIN_DIR: &str = "bin";
35const SNAPSHOT_CONVERTER_BIN_NAME_UNIX: &str = "snapshot-converter";
36const SNAPSHOT_CONVERTER_BIN_NAME_WINDOWS: &str = "snapshot-converter.exe";
37const SNAPSHOT_CONVERTER_CONFIG_DIR: &str = "share";
38const SNAPSHOT_CONVERTER_CONFIG_FILE: &str = "config.json";
39
40const LEDGER_DIR: &str = "ledger";
41const PROTOCOL_MAGIC_ID_FILE: &str = "protocolMagicId";
42
43const CONVERSION_FALLBACK_LIMIT: usize = 2;
44
45#[derive(Debug, Clone, ValueEnum, Eq, PartialEq)]
46enum UTxOHDFlavor {
47    #[clap(name = "Legacy")]
48    Legacy,
49    #[clap(name = "LMDB")]
50    Lmdb,
51}
52
53impl fmt::Display for UTxOHDFlavor {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::Legacy => write!(f, "Legacy"),
57            Self::Lmdb => write!(f, "LMDB"),
58        }
59    }
60}
61
62impl From<&UTxOHDFlavor> for LedgerFormat {
63    fn from(value: &UTxOHDFlavor) -> Self {
64        match value {
65            UTxOHDFlavor::Legacy => LedgerFormat::Legacy,
66            UTxOHDFlavor::Lmdb => LedgerFormat::Lmdb,
67        }
68    }
69}
70
71#[derive(Debug, Clone, ValueEnum, Eq, PartialEq)]
72enum CardanoNetworkCliArg {
73    Preview,
74    Preprod,
75    Mainnet,
76}
77
78impl fmt::Display for CardanoNetworkCliArg {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            Self::Preview => write!(f, "preview"),
82            Self::Preprod => write!(f, "preprod"),
83            Self::Mainnet => write!(f, "mainnet"),
84        }
85    }
86}
87
88impl TryFrom<CardanoNetwork> for CardanoNetworkCliArg {
89    type Error = MithrilError;
90
91    fn try_from(network: CardanoNetwork) -> Result<Self, Self::Error> {
92        match network {
93            CardanoNetwork::MainNet => Ok(Self::Mainnet),
94            CardanoNetwork::TestNet(magic_id) => match magic_id {
95                CardanoNetwork::PREVIEW_MAGIC_ID => Ok(Self::Preview),
96                CardanoNetwork::PREPROD_MAGIC_ID => Ok(Self::Preprod),
97                _ => Err(anyhow!(
98                    "Cardano network not supported for ledger state snapshot conversion: {network:?}",
99                )),
100            },
101        }
102    }
103}
104
105#[cfg_attr(test, mockall::automock)]
106trait SnapshotConverter {
107    fn convert(&self, input_path: &Path, output_path: &Path) -> MithrilResult<()>;
108}
109#[derive(Clone)]
110struct SnapshotConverterConfig {
111    pub converter_bin: PathBuf,
112    pub config_path: PathBuf,
113    pub utxo_hd_flavor: UTxOHDFlavor,
114    pub hide_output: bool,
115}
116
117enum SnapshotConverterBin {
118    From10_6(SnapshotConverterConfig),
119    UpTo10_5(SnapshotConverterConfig),
120}
121
122impl SnapshotConverter for SnapshotConverterBin {
123    fn convert(&self, input_path: &Path, output_path: &Path) -> MithrilResult<()> {
124        let mut command = match self {
125            SnapshotConverterBin::From10_6(cfg) => {
126                build_command_from_10_6(input_path, output_path, cfg.clone())
127            }
128
129            SnapshotConverterBin::UpTo10_5(cfg) => {
130                build_command_up_to_10_5(input_path, output_path, cfg.clone())
131            }
132        };
133
134        let status = command.status().with_context(|| {
135            format!(
136                "Failed to execute snapshot-converter binary at {}",
137                match self {
138                    SnapshotConverterBin::From10_6(cfg) | SnapshotConverterBin::UpTo10_5(cfg) =>
139                        cfg.converter_bin.display(),
140                }
141            )
142        })?;
143
144        if !status.success() {
145            return Err(anyhow!(
146                "Failure while running snapshot-converter binary, exited with status code: {:?}",
147                status.code().map_or(String::from("unknown"), |c| c.to_string())
148            ));
149        }
150
151        Ok(())
152    }
153}
154
155fn build_command_from_10_6(
156    input_path: &Path,
157    output_path: &Path,
158    cfg: SnapshotConverterConfig,
159) -> Command {
160    let mut command = Command::new(cfg.converter_bin);
161
162    command
163        .arg("--mem-in")
164        .arg(input_path)
165        .arg(format!(
166            "--{}-out",
167            cfg.utxo_hd_flavor.to_string().to_lowercase()
168        ))
169        .arg(output_path)
170        .arg("--config")
171        .arg(cfg.config_path)
172        .stdout(if cfg.hide_output {
173            // Hide output when JSON output is enabled (as they are not JSON formatted)
174            Stdio::null()
175        } else {
176            // redirect to stderr to keep stdout dedicated to the command result.
177            std::io::stderr().into()
178        });
179    command
180}
181
182fn build_command_up_to_10_5(
183    input_path: &Path,
184    output_path: &Path,
185    cfg: SnapshotConverterConfig,
186) -> Command {
187    let mut command = Command::new(cfg.converter_bin);
188
189    command
190        .arg("Mem")
191        .arg(input_path)
192        .arg(cfg.utxo_hd_flavor.to_string())
193        .arg(output_path)
194        .arg("cardano")
195        .arg("--config")
196        .arg(cfg.config_path.clone())
197        .stdout(if cfg.hide_output {
198            // Hide output when JSON output is enabled (as they are not JSON formatted)
199            Stdio::null()
200        } else {
201            // redirect to stderr to keep stdout dedicated to the command result.
202            std::io::stderr().into()
203        });
204    command
205}
206
207/// Clap command to convert a restored `InMemory` Mithril snapshot to another flavor.
208#[derive(Parser, Debug, Clone)]
209pub struct SnapshotConverterCommand {
210    /// Path to the Cardano node database directory.
211    #[clap(long)]
212    db_directory: PathBuf,
213
214    /// Cardano node version of the Mithril signed snapshot (`latest` and `pre-release` are also supported to download the latest or pre-release distribution).
215    ///
216    /// `latest` and `pre-release` are also supported to download the latest or pre-release distribution.
217    #[clap(long)]
218    cardano_node_version: String,
219
220    /// Cardano network.
221    #[clap(long)]
222    #[deprecated(
223        since = "0.12.12",
224        note = "optional: automatically detected from the protocolMagicId file"
225    )]
226    cardano_network: Option<CardanoNetworkCliArg>,
227
228    /// UTxO-HD flavor to convert the ledger snapshot to (`Legacy` or `LMDB`).
229    #[clap(long)]
230    utxo_hd_flavor: UTxOHDFlavor,
231
232    /// Replaces the current ledger state in the `db_directory`.
233    #[clap(long)]
234    commit: bool,
235
236    /// GitHub token for authenticated API calls.
237    #[clap(long, env = "GITHUB_TOKEN")]
238    github_token: Option<String>,
239}
240
241impl SnapshotConverterCommand {
242    /// Main command execution
243    pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> {
244        let progress_output_type = if context.is_json_output_enabled() {
245            ProgressOutputType::JsonReporter
246        } else {
247            ProgressOutputType::Tty
248        };
249
250        if is_version_at_least_10_6_2_or_latest(&self.cardano_node_version)
251            && self.utxo_hd_flavor == UTxOHDFlavor::Legacy
252        {
253            return Err(anyhow!(
254                "UTxO HD Flavor Legacy is not supported on Cardano node 10.6.2 or upper"
255            ));
256        }
257
258        let number_of_steps = if self.commit { 4 } else { 3 };
259        let progress_printer = ProgressPrinter::new(progress_output_type, number_of_steps);
260
261        let work_dir = self.db_directory.join(WORK_DIR);
262        create_dir(&work_dir).with_context(|| {
263            format!(
264                "Failed to create snapshot converter work directory: {}",
265                work_dir.display()
266            )
267        })?;
268        let distribution_dir = work_dir.join(CARDANO_DISTRIBUTION_DIR);
269
270        let result = {
271            create_dir(&distribution_dir).with_context(|| {
272                format!(
273                    "Failed to create distribution directory: {}",
274                    distribution_dir.display()
275                )
276            })?;
277            let archive_path = Self::download_cardano_node_distribution(
278                1,
279                &progress_printer,
280                ReqwestGitHubApiClient::new(self.github_token.clone())?,
281                ReqwestHttpDownloader::new()?,
282                &self.cardano_node_version,
283                &distribution_dir,
284            )
285            .await
286            .with_context(|| "Failed to download Cardano node distribution")?;
287
288            progress_printer.report_step(
289                2,
290                &format!(
291                    "Unpacking distribution from archive: {}",
292                    archive_path.display()
293                ),
294            )?;
295            ArchiveUnpacker::default()
296                .unpack(&archive_path, &distribution_dir)
297                .with_context(|| {
298                    format!(
299                        "Failed to unpack distribution to directory: {}",
300                        distribution_dir.display()
301                    )
302                })?;
303            progress_printer.print_message(&format!(
304                "Distribution unpacked successfully to: {}",
305                distribution_dir.display()
306            ))?;
307
308            #[allow(deprecated)]
309            let cardano_network = if let Some(network) = &self.cardano_network {
310                network.clone()
311            } else {
312                Self::detect_cardano_network(&self.db_directory).with_context(|| {
313                    format!(
314                        "Could not detect Cardano network from the database directory: {}",
315                        self.db_directory.display()
316                    )
317                })?
318            };
319            let converted_snapshot_path = Self::convert_ledger_state_snapshot(
320                3,
321                &progress_printer,
322                &work_dir,
323                &distribution_dir,
324                &cardano_network,
325                self,
326            )
327            .with_context(|| {
328                format!(
329                    "Failed to convert ledger snapshot to flavor: {}",
330                    self.utxo_hd_flavor
331                )
332            })?;
333
334            if self.commit {
335                Self::commit_converted_snapshot(
336                    4,
337                    &progress_printer,
338                    &self.db_directory,
339                    &converted_snapshot_path,
340                )
341                .with_context(
342                    || "Failed to overwrite the ledger state with the converted snapshot.",
343                )?;
344            }
345
346            Self::print_successful_result(
347                &self.db_directory,
348                &converted_snapshot_path,
349                &cardano_network,
350                &self.utxo_hd_flavor,
351                &self.cardano_node_version,
352                self.commit,
353                context.is_json_output_enabled(),
354            )?;
355
356            Ok(())
357        };
358
359        if let Err(e) = Self::cleanup(&work_dir, &distribution_dir, self.commit, result.is_ok()) {
360            progress_printer.print_message(&format!(
361                "Failed to clean up temporary directory '{distribution_dir}' after execution: {e:?}",
362                distribution_dir = distribution_dir.display(),
363            ))?;
364        }
365
366        result
367    }
368
369    async fn download_cardano_node_distribution(
370        step_number: u16,
371        progress_printer: &ProgressPrinter,
372        github_api_client: impl GitHubReleaseRetriever,
373        http_downloader: impl HttpDownloader,
374        tag: &str,
375        target_dir: &Path,
376    ) -> MithrilResult<PathBuf> {
377        progress_printer.report_step(
378            step_number,
379            &format!("Downloading Cardano node distribution for tag: '{tag}'..."),
380        )?;
381        let release = match tag {
382            LATEST_DISTRIBUTION_TAG => github_api_client
383                .get_latest_release(GITHUB_ORGANIZATION, GITHUB_REPOSITORY)
384                .await
385                .with_context(|| "Failed to get latest release")?,
386            PRERELEASE_DISTRIBUTION_TAG => github_api_client
387                .get_prerelease(GITHUB_ORGANIZATION, GITHUB_REPOSITORY)
388                .await
389                .with_context(|| "Failed to get pre-release")?,
390            _ => github_api_client
391                .get_release_by_tag(GITHUB_ORGANIZATION, GITHUB_REPOSITORY, tag)
392                .await
393                .with_context(|| format!("Failed to get release by tag: {tag}"))?,
394        };
395        let asset = release.get_asset_for_os(env::consts::OS)?.with_context(|| {
396            format!(
397                "Failed to find asset for current platform: {}",
398                env::consts::OS
399            )
400        })?;
401        let archive_path = http_downloader
402            .download_file(asset.browser_download_url.parse()?, target_dir, &asset.name)
403            .await?;
404
405        progress_printer.print_message(&format!(
406            "Distribution downloaded successfully. Archive location: {}",
407            archive_path.display()
408        ))?;
409
410        Ok(archive_path)
411    }
412
413    fn convert_ledger_state_snapshot(
414        step_number: u16,
415        progress_printer: &ProgressPrinter,
416        work_dir: &Path,
417        distribution_dir: &Path,
418        cardano_network: &CardanoNetworkCliArg,
419        command: &SnapshotConverterCommand,
420    ) -> MithrilResult<PathBuf> {
421        progress_printer.report_step(
422            step_number,
423            &format!(
424                "Converting ledger state snapshot to '{}' flavor",
425                command.utxo_hd_flavor
426            ),
427        )?;
428        let converter_bin =
429            Self::get_snapshot_converter_binary_path(distribution_dir, env::consts::OS)?;
430        let config_path =
431            Self::get_snapshot_converter_config_path(distribution_dir, cardano_network);
432        let snapshots =
433            Self::find_most_recent_snapshots(&command.db_directory, CONVERSION_FALLBACK_LIMIT)?;
434        let converter_bin_config = SnapshotConverterConfig {
435            converter_bin,
436            config_path,
437            utxo_hd_flavor: command.utxo_hd_flavor.clone(),
438            hide_output: matches!(
439                progress_printer.output_type(),
440                ProgressOutputType::JsonReporter | ProgressOutputType::Hidden
441            ),
442        };
443
444        let converter_bin = get_snapshot_converter_bin_by_version(
445            &command.cardano_node_version,
446            converter_bin_config,
447        );
448
449        Self::try_convert(
450            progress_printer,
451            work_dir,
452            &command.utxo_hd_flavor,
453            &snapshots,
454            Box::new(converter_bin),
455        )
456    }
457
458    fn print_successful_result(
459        db_dir: &Path,
460        converted_snapshot_path: &Path,
461        cardano_network: &CardanoNetworkCliArg,
462        utxo_hd_flavor: &UTxOHDFlavor,
463        cardano_node_version: &str,
464        commit: bool,
465        is_json_output_enabled: bool,
466    ) -> MithrilResult<()> {
467        let canonical_db_dir = &db_dir
468            .canonicalize()
469            .with_context(|| format!("Could not get canonical path of '{}'", db_dir.display()))?;
470        let message =
471            format!("Ledger state have been successfully converted to {utxo_hd_flavor} format");
472        let timestamp = Utc::now().to_rfc3339();
473
474        if commit {
475            let docker_cmd = CardanoDbUtils::get_docker_run_command(
476                canonical_db_dir,
477                &cardano_network.to_string(),
478                cardano_node_version,
479                utxo_hd_flavor.into(),
480            );
481
482            if matches!(&utxo_hd_flavor, UTxOHDFlavor::Legacy) {
483                print_simple_warning(
484                    "Legacy ledger format is only compatible with cardano-node up to `10.3.1`.",
485                    is_json_output_enabled,
486                );
487            }
488
489            if is_json_output_enabled {
490                println!(
491                    "{}",
492                    serde_json::json!({
493                        "message": message,
494                        "timestamp": timestamp,
495                        "run_docker_cmd": docker_cmd,
496                    })
497                );
498            } else {
499                println!(
500                    r###"{message}.
501
502If you are using the Cardano Docker image, you can start the Cardano node with:
503
504   {docker_cmd}
505"###,
506                );
507            }
508        } else if is_json_output_enabled {
509            println!(
510                "{}",
511                serde_json::json!({
512                    "message": message,
513                    "timestamp": timestamp,
514                    "converted_snapshot_path": converted_snapshot_path,
515                })
516            );
517        } else {
518            println!(
519                r###"{message}.
520
521Snapshot location: {}
522"###,
523                converted_snapshot_path.display()
524            );
525        }
526
527        Ok(())
528    }
529
530    fn try_convert(
531        progress_printer: &ProgressPrinter,
532        work_dir: &Path,
533        utxo_hd_flavor: &UTxOHDFlavor,
534        snapshots: &[PathBuf],
535        converter: Box<dyn SnapshotConverter>,
536    ) -> MithrilResult<PathBuf> {
537        let snapshots_dir = work_dir.join(SNAPSHOTS_DIR);
538
539        for (i, snapshot) in snapshots.iter().enumerate() {
540            let attempt = i + 1;
541            let snapshot_path_display = snapshot.display();
542            progress_printer.print_message(&format!(
543                "Converting '{snapshot_path_display}' (attempt #{attempt})",
544            ))?;
545
546            let input_path = copy_dir(snapshot, &snapshots_dir)?;
547            let output_path = Self::compute_converted_snapshot_output_path(
548                &snapshots_dir,
549                &input_path,
550                utxo_hd_flavor,
551            )?;
552
553            match converter.convert(&input_path, &output_path) {
554                Ok(()) => {
555                    progress_printer.print_message(&format!(
556                        "Successfully converted ledger state snapshot: '{snapshot_path_display}'",
557                    ))?;
558                    return Ok(output_path);
559                }
560                Err(e) => {
561                    progress_printer.print_message(&format!(
562                        "Failed to convert ledger state snapshot '{snapshot_path_display}': {e}",
563                    ))?;
564                    continue;
565                }
566            };
567        }
568
569        Err(anyhow!(
570            "Failed to convert any of the provided ledger state snapshots to the desired flavor: {}",
571            utxo_hd_flavor
572        ))
573    }
574
575    fn get_snapshot_converter_binary_path(
576        distribution_dir: &Path,
577        target_os: &str,
578    ) -> MithrilResult<PathBuf> {
579        let base_path = distribution_dir.join(SNAPSHOT_CONVERTER_BIN_DIR);
580        let binary_name = match target_os {
581            "linux" | "macos" => SNAPSHOT_CONVERTER_BIN_NAME_UNIX,
582            "windows" => SNAPSHOT_CONVERTER_BIN_NAME_WINDOWS,
583            _ => return Err(anyhow!("Unsupported platform: {}", target_os)),
584        };
585
586        Ok(base_path.join(binary_name))
587    }
588
589    fn get_snapshot_converter_config_path(
590        distribution_dir: &Path,
591        network: &CardanoNetworkCliArg,
592    ) -> PathBuf {
593        distribution_dir
594            .join(SNAPSHOT_CONVERTER_CONFIG_DIR)
595            .join(network.to_string())
596            .join(SNAPSHOT_CONVERTER_CONFIG_FILE)
597    }
598
599    /// Returns the list of valid ledger snapshot directories sorted in ascending order of slot number.
600    ///
601    /// Only directories with numeric names are considered valid snapshots.
602    fn get_sorted_snapshot_dirs(ledger_dir: &Path) -> MithrilResult<Vec<(u64, PathBuf)>> {
603        let entries = read_dir(ledger_dir).with_context(|| {
604            format!(
605                "Failed to read ledger state snapshots directory: {}",
606                ledger_dir.display()
607            )
608        })?;
609
610        let mut snapshots = entries
611            .filter_map(|entry| {
612                let path = entry.ok()?.path();
613                if !path.is_dir() {
614                    return None;
615                }
616                SnapshotConverterCommand::extract_slot_number(&path)
617                    .ok()
618                    .map(|slot| (slot, path))
619            })
620            .collect::<Vec<_>>();
621
622        snapshots.sort_by_key(|(slot, _)| *slot);
623
624        Ok(snapshots)
625    }
626
627    fn find_most_recent_snapshots(db_dir: &Path, count: usize) -> MithrilResult<Vec<PathBuf>> {
628        let ledger_dir = db_dir.join(LEDGER_DIR);
629        let snapshots = Self::get_sorted_snapshot_dirs(&ledger_dir)?
630            .into_iter()
631            .rev()
632            .take(count)
633            .map(|(_, path)| path)
634            .collect::<Vec<_>>();
635
636        if snapshots.is_empty() {
637            return Err(anyhow!(
638                "No valid ledger state snapshots found in directory: {}",
639                ledger_dir.display()
640            ));
641        }
642
643        Ok(snapshots)
644    }
645
646    fn compute_converted_snapshot_output_path(
647        snapshots_dir: &Path,
648        input_snapshot: &Path,
649        flavor: &UTxOHDFlavor,
650    ) -> MithrilResult<PathBuf> {
651        let slot_number = Self::extract_slot_number(input_snapshot).with_context(|| {
652            format!(
653                "Failed to extract slot number from: {}",
654                input_snapshot.display()
655            )
656        })?;
657        let converted_snapshot_path = snapshots_dir.join(format!(
658            "{}_{}",
659            slot_number,
660            flavor.to_string().to_lowercase()
661        ));
662
663        Ok(converted_snapshot_path)
664    }
665
666    fn extract_slot_number(path: &Path) -> MithrilResult<u64> {
667        let file_name = path
668            .file_name()
669            .with_context(|| format!("No filename in path: {}", path.display()))?;
670        let file_name_str = file_name
671            .to_str()
672            .with_context(|| format!("Invalid UTF-8 in path filename: {:?}", file_name))?;
673
674        file_name_str
675            .parse::<u64>()
676            .with_context(|| format!("Invalid slot number in path filename: {file_name_str}"))
677    }
678
679    /// Commits the converted snapshot by replacing the current ledger state snapshots in the database directory.
680    fn commit_converted_snapshot(
681        step_number: u16,
682        progress_printer: &ProgressPrinter,
683        db_dir: &Path,
684        converted_snapshot_path: &Path,
685    ) -> MithrilResult<()> {
686        let ledger_dir = db_dir.join(LEDGER_DIR);
687        progress_printer.report_step(
688            step_number,
689            &format!(
690                "Upgrading and replacing ledger state in {} with converted snapshot: {}",
691                ledger_dir.display(),
692                converted_snapshot_path.display()
693            ),
694        )?;
695
696        let filename = converted_snapshot_path
697            .file_name()
698            .with_context(|| "Missing filename in converted snapshot path")?
699            .to_string_lossy();
700        let (slot_number, _) = filename
701            .split_once('_')
702            .with_context(|| format!("Invalid converted snapshot name format: {}", filename))?;
703        remove_dir_contents(&ledger_dir).with_context(|| {
704            format!(
705                "Failed to remove contents of ledger directory: {}",
706                ledger_dir.display()
707            )
708        })?;
709        let destination = ledger_dir.join(slot_number);
710        rename(converted_snapshot_path, &destination).with_context(|| {
711            format!(
712                "Failed to move converted snapshot to ledger directory: {}",
713                destination.display()
714            )
715        })?;
716
717        Ok(())
718    }
719
720    fn cleanup(
721        work_dir: &Path,
722        distribution_dir: &Path,
723        commit: bool,
724        success: bool,
725    ) -> MithrilResult<()> {
726        match (success, commit) {
727            (true, true) => {
728                remove_dir_all(distribution_dir)?;
729                remove_dir_all(work_dir)?;
730            }
731            (true, false) => {
732                remove_dir_all(distribution_dir)?;
733            }
734            (false, _) => {
735                remove_dir_all(distribution_dir)?;
736                remove_dir_all(work_dir)?;
737            }
738        }
739
740        Ok(())
741    }
742
743    fn detect_cardano_network(db_dir: &Path) -> MithrilResult<CardanoNetworkCliArg> {
744        let magic_id_path = db_dir.join(PROTOCOL_MAGIC_ID_FILE);
745        let content = std::fs::read_to_string(&magic_id_path).with_context(|| {
746            format!(
747                "Failed to read protocolMagicId file: {}",
748                magic_id_path.display()
749            )
750        })?;
751        let id: MagicId = content
752            .trim()
753            .parse()
754            .with_context(|| format!("Invalid protocolMagicId value: '{}'", content.trim()))?;
755        let network = CardanoNetwork::from(id);
756
757        CardanoNetworkCliArg::try_from(network)
758    }
759}
760
761fn get_snapshot_converter_bin_by_version(
762    cardano_node_version: &str,
763    converter_bin_config: SnapshotConverterConfig,
764) -> SnapshotConverterBin {
765    if is_version_at_least_10_6_2_or_latest(cardano_node_version) {
766        SnapshotConverterBin::From10_6(converter_bin_config)
767    } else {
768        SnapshotConverterBin::UpTo10_5(converter_bin_config)
769    }
770}
771
772fn is_version_at_least_10_6_2_or_latest(version: &str) -> bool {
773    is_version_equal_or_upper(version, CARDANO_NODE_V10_6_2)
774}
775
776#[cfg(test)]
777mod tests {
778    use std::fs::File;
779
780    use mithril_common::temp_dir_create;
781
782    use super::*;
783
784    mod execute {
785        use std::path::PathBuf;
786
787        use slog::{Logger, o};
788
789        use crate::{
790            CommandContext, ConfigParameters,
791            commands::tools::{
792                SnapshotConverterCommand, utxo_hd::snapshot_converter::UTxOHDFlavor,
793            },
794        };
795
796        fn fake_command_context() -> CommandContext {
797            CommandContext::new(
798                ConfigParameters::default(),
799                true,
800                true,
801                Logger::root(slog::Discard, o!()),
802            )
803        }
804
805        fn dummy_snapshot_conveter_command() -> SnapshotConverterCommand {
806            #[allow(deprecated)]
807            SnapshotConverterCommand {
808                db_directory: PathBuf::new(),
809                cardano_node_version: "1.0.0".to_string(),
810                commit: false,
811                utxo_hd_flavor: UTxOHDFlavor::Legacy,
812                github_token: None,
813                cardano_network: None,
814            }
815        }
816
817        #[tokio::test]
818        async fn should_return_error_if_htx0_hd_flavor_is_legacy_and_cardano_node_version_10_6_2() {
819            let command = SnapshotConverterCommand {
820                cardano_node_version: "10.6.2".to_string(),
821                utxo_hd_flavor: UTxOHDFlavor::Legacy,
822                ..dummy_snapshot_conveter_command()
823            };
824
825            let result = SnapshotConverterCommand::execute(&command, fake_command_context()).await;
826
827            assert!(result.is_err());
828            assert_eq!(
829                result.unwrap_err().to_string(),
830                "UTxO HD Flavor Legacy is not supported on Cardano node 10.6.2 or upper"
831            );
832        }
833
834        #[tokio::test]
835        async fn should_return_error_if_htx0_hd_flavor_is_legacy_and_cardano_node_version_10_6_2_or_upper()
836         {
837            let command = SnapshotConverterCommand {
838                cardano_node_version: "10.7.7".to_string(),
839                utxo_hd_flavor: UTxOHDFlavor::Legacy,
840                ..dummy_snapshot_conveter_command()
841            };
842
843            let result = SnapshotConverterCommand::execute(&command, fake_command_context()).await;
844
845            assert!(result.is_err());
846            assert_eq!(
847                result.unwrap_err().to_string(),
848                "UTxO HD Flavor Legacy is not supported on Cardano node 10.6.2 or upper"
849            );
850        }
851
852        #[tokio::test]
853        async fn should_return_error_if_htx0_hd_flavor_is_legacy_and_cardano_node_version_latest() {
854            let command = SnapshotConverterCommand {
855                cardano_node_version: "latest".to_string(),
856                utxo_hd_flavor: UTxOHDFlavor::Legacy,
857                ..dummy_snapshot_conveter_command()
858            };
859
860            let result = SnapshotConverterCommand::execute(&command, fake_command_context()).await;
861
862            assert!(result.is_err());
863            assert_eq!(
864                result.unwrap_err().to_string(),
865                "UTxO HD Flavor Legacy is not supported on Cardano node 10.6.2 or upper"
866            );
867        }
868    }
869
870    mod download_cardano_node_distribution {
871        use mockall::predicate::eq;
872        use reqwest::Url;
873
874        use crate::utils::{GitHubRelease, MockGitHubReleaseRetriever, MockHttpDownloader};
875
876        use super::*;
877
878        #[tokio::test]
879        async fn downloads_latest_release_distribution() {
880            let temp_dir = temp_dir_create!();
881            let release = GitHubRelease::dummy_with_all_supported_assets();
882            let asset = release.get_asset_for_os(env::consts::OS).unwrap().unwrap();
883
884            let cloned_release = release.clone();
885            let mut github_api_client = MockGitHubReleaseRetriever::new();
886            github_api_client
887                .expect_get_latest_release()
888                .with(eq(GITHUB_ORGANIZATION), eq(GITHUB_REPOSITORY))
889                .returning(move |_, _| Ok(cloned_release.clone()));
890
891            let mut http_downloader = MockHttpDownloader::new();
892            http_downloader
893                .expect_download_file()
894                .with(
895                    eq(Url::parse(&asset.browser_download_url).unwrap()),
896                    eq(temp_dir.clone()),
897                    eq(asset.name.clone()),
898                )
899                .returning(|_, _, _| Ok(PathBuf::new()));
900
901            SnapshotConverterCommand::download_cardano_node_distribution(
902                1,
903                &ProgressPrinter::new(ProgressOutputType::Hidden, 1),
904                github_api_client,
905                http_downloader,
906                LATEST_DISTRIBUTION_TAG,
907                &temp_dir,
908            )
909            .await
910            .unwrap();
911        }
912
913        #[tokio::test]
914        async fn downloads_prerelease_distribution() {
915            let temp_dir = temp_dir_create!();
916            let release = GitHubRelease::dummy_with_all_supported_assets();
917            let asset = release.get_asset_for_os(env::consts::OS).unwrap().unwrap();
918
919            let cloned_release = release.clone();
920            let mut github_api_client = MockGitHubReleaseRetriever::new();
921            github_api_client
922                .expect_get_prerelease()
923                .with(eq(GITHUB_ORGANIZATION), eq(GITHUB_REPOSITORY))
924                .returning(move |_, _| Ok(cloned_release.clone()));
925
926            let mut http_downloader = MockHttpDownloader::new();
927            http_downloader
928                .expect_download_file()
929                .with(
930                    eq(Url::parse(&asset.browser_download_url).unwrap()),
931                    eq(temp_dir.clone()),
932                    eq(asset.name.clone()),
933                )
934                .returning(|_, _, _| Ok(PathBuf::new()));
935
936            SnapshotConverterCommand::download_cardano_node_distribution(
937                1,
938                &ProgressPrinter::new(ProgressOutputType::Hidden, 1),
939                github_api_client,
940                http_downloader,
941                PRERELEASE_DISTRIBUTION_TAG,
942                &temp_dir,
943            )
944            .await
945            .unwrap();
946        }
947
948        #[tokio::test]
949        async fn downloads_tagged_release_distribution() {
950            let cardano_node_version = "10.3.1";
951            let temp_dir = temp_dir_create!();
952            let release = GitHubRelease::dummy_with_all_supported_assets();
953            let asset = release.get_asset_for_os(env::consts::OS).unwrap().unwrap();
954
955            let cloned_release = release.clone();
956            let mut github_api_client = MockGitHubReleaseRetriever::new();
957            github_api_client
958                .expect_get_release_by_tag()
959                .with(
960                    eq(GITHUB_ORGANIZATION),
961                    eq(GITHUB_REPOSITORY),
962                    eq(cardano_node_version),
963                )
964                .returning(move |_, _, _| Ok(cloned_release.clone()));
965
966            let mut http_downloader = MockHttpDownloader::new();
967            http_downloader
968                .expect_download_file()
969                .with(
970                    eq(Url::parse(&asset.browser_download_url).unwrap()),
971                    eq(temp_dir.clone()),
972                    eq(asset.name.clone()),
973                )
974                .returning(|_, _, _| Ok(PathBuf::new()));
975
976            SnapshotConverterCommand::download_cardano_node_distribution(
977                1,
978                &ProgressPrinter::new(ProgressOutputType::Hidden, 1),
979                github_api_client,
980                http_downloader,
981                cardano_node_version,
982                &temp_dir,
983            )
984            .await
985            .unwrap();
986        }
987    }
988
989    mod get_snapshot_converter_binary_path {
990        use super::*;
991
992        #[test]
993        fn returns_correct_binary_path_for_linux() {
994            let distribution_dir = PathBuf::from("/path/to/distribution");
995
996            let binary_path = SnapshotConverterCommand::get_snapshot_converter_binary_path(
997                &distribution_dir,
998                "linux",
999            )
1000            .unwrap();
1001
1002            assert_eq!(
1003                binary_path,
1004                distribution_dir
1005                    .join(SNAPSHOT_CONVERTER_BIN_DIR)
1006                    .join(SNAPSHOT_CONVERTER_BIN_NAME_UNIX)
1007            );
1008        }
1009
1010        #[test]
1011        fn returns_correct_binary_path_for_macos() {
1012            let distribution_dir = PathBuf::from("/path/to/distribution");
1013
1014            let binary_path = SnapshotConverterCommand::get_snapshot_converter_binary_path(
1015                &distribution_dir,
1016                "macos",
1017            )
1018            .unwrap();
1019
1020            assert_eq!(
1021                binary_path,
1022                distribution_dir
1023                    .join(SNAPSHOT_CONVERTER_BIN_DIR)
1024                    .join(SNAPSHOT_CONVERTER_BIN_NAME_UNIX)
1025            );
1026        }
1027
1028        #[test]
1029        fn returns_correct_binary_path_for_windows() {
1030            let distribution_dir = PathBuf::from("/path/to/distribution");
1031
1032            let binary_path = SnapshotConverterCommand::get_snapshot_converter_binary_path(
1033                &distribution_dir,
1034                "windows",
1035            )
1036            .unwrap();
1037
1038            assert_eq!(
1039                binary_path,
1040                distribution_dir
1041                    .join(SNAPSHOT_CONVERTER_BIN_DIR)
1042                    .join(SNAPSHOT_CONVERTER_BIN_NAME_WINDOWS)
1043            );
1044        }
1045    }
1046
1047    mod get_snapshot_converter_config_path {
1048        use super::*;
1049
1050        #[test]
1051        fn returns_config_path_for_mainnet() {
1052            let distribution_dir = PathBuf::from("/path/to/distribution");
1053            let network = CardanoNetworkCliArg::Mainnet;
1054
1055            let config_path = SnapshotConverterCommand::get_snapshot_converter_config_path(
1056                &distribution_dir,
1057                &network,
1058            );
1059
1060            assert_eq!(
1061                config_path,
1062                distribution_dir
1063                    .join(SNAPSHOT_CONVERTER_CONFIG_DIR)
1064                    .join(network.to_string())
1065                    .join(SNAPSHOT_CONVERTER_CONFIG_FILE)
1066            );
1067        }
1068
1069        #[test]
1070        fn returns_config_path_for_preprod() {
1071            let distribution_dir = PathBuf::from("/path/to/distribution");
1072            let network = CardanoNetworkCliArg::Preprod;
1073
1074            let config_path = SnapshotConverterCommand::get_snapshot_converter_config_path(
1075                &distribution_dir,
1076                &network,
1077            );
1078
1079            assert_eq!(
1080                config_path,
1081                distribution_dir
1082                    .join(SNAPSHOT_CONVERTER_CONFIG_DIR)
1083                    .join(network.to_string())
1084                    .join(SNAPSHOT_CONVERTER_CONFIG_FILE)
1085            );
1086        }
1087
1088        #[test]
1089        fn returns_config_path_for_preview() {
1090            let distribution_dir = PathBuf::from("/path/to/distribution");
1091            let network = CardanoNetworkCliArg::Preview;
1092
1093            let config_path = SnapshotConverterCommand::get_snapshot_converter_config_path(
1094                &distribution_dir,
1095                &network,
1096            );
1097
1098            assert_eq!(
1099                config_path,
1100                distribution_dir
1101                    .join(SNAPSHOT_CONVERTER_CONFIG_DIR)
1102                    .join(network.to_string())
1103                    .join(SNAPSHOT_CONVERTER_CONFIG_FILE)
1104            );
1105        }
1106    }
1107
1108    mod extract_slot_number {
1109        use super::*;
1110
1111        #[test]
1112        fn parses_valid_numeric_path() {
1113            let path = PathBuf::from("/whatever").join("123456");
1114
1115            let slot = SnapshotConverterCommand::extract_slot_number(&path).unwrap();
1116
1117            assert_eq!(slot, 123456);
1118        }
1119
1120        #[test]
1121        fn fails_with_non_numeric_filename() {
1122            let path = PathBuf::from("/whatever").join("notanumber");
1123
1124            SnapshotConverterCommand::extract_slot_number(&path)
1125                .expect_err("Should fail with non-numeric filename");
1126        }
1127
1128        #[test]
1129        fn fails_if_no_filename() {
1130            let path = PathBuf::from("/");
1131
1132            SnapshotConverterCommand::extract_slot_number(&path)
1133                .expect_err("Should fail if path has no filename");
1134        }
1135    }
1136
1137    mod compute_converted_snapshot_output_path {
1138        use super::*;
1139
1140        #[test]
1141        fn compute_output_path_from_numeric_file_name() {
1142            let snapshots_dir = PathBuf::from("/snapshots");
1143            let input_snapshot = PathBuf::from("/whatever").join("123456");
1144
1145            {
1146                let snapshot_path =
1147                    SnapshotConverterCommand::compute_converted_snapshot_output_path(
1148                        &snapshots_dir,
1149                        &input_snapshot,
1150                        &UTxOHDFlavor::Lmdb,
1151                    )
1152                    .unwrap();
1153
1154                assert_eq!(snapshot_path, snapshots_dir.join("123456_lmdb"));
1155            }
1156
1157            {
1158                let snapshot_path =
1159                    SnapshotConverterCommand::compute_converted_snapshot_output_path(
1160                        &snapshots_dir,
1161                        &input_snapshot,
1162                        &UTxOHDFlavor::Legacy,
1163                    )
1164                    .unwrap();
1165
1166                assert_eq!(snapshot_path, snapshots_dir.join("123456_legacy"));
1167            }
1168        }
1169
1170        #[test]
1171        fn fails_with_invalid_slot_number() {
1172            let snapshots_dir = PathBuf::from("/snapshots");
1173            let input_snapshot = PathBuf::from("/whatever/notanumber");
1174
1175            SnapshotConverterCommand::compute_converted_snapshot_output_path(
1176                &snapshots_dir,
1177                &input_snapshot,
1178                &UTxOHDFlavor::Lmdb,
1179            )
1180            .expect_err("Should fail with invalid slot number");
1181        }
1182    }
1183
1184    mod commit_converted_snapshot {
1185        use super::*;
1186
1187        #[test]
1188        fn moves_converted_snapshot_to_ledger_directory() {
1189            let tmp_dir = temp_dir_create!();
1190            let ledger_dir = tmp_dir.join(LEDGER_DIR);
1191            create_dir(&ledger_dir).unwrap();
1192            let previous_snapshot = ledger_dir.join("123");
1193            File::create(&previous_snapshot).unwrap();
1194
1195            let converted_snapshot = tmp_dir.join("456_lmdb");
1196            File::create(&converted_snapshot).unwrap();
1197
1198            assert!(previous_snapshot.exists());
1199            SnapshotConverterCommand::commit_converted_snapshot(
1200                1,
1201                &ProgressPrinter::new(ProgressOutputType::Hidden, 1),
1202                &tmp_dir,
1203                &converted_snapshot,
1204            )
1205            .unwrap();
1206
1207            assert!(!previous_snapshot.exists());
1208            assert!(ledger_dir.join("456").exists());
1209        }
1210
1211        #[test]
1212        fn fails_if_converted_snapshot_has_invalid_filename() {
1213            let tmp_dir = temp_dir_create!();
1214            let ledger_dir = tmp_dir.join(LEDGER_DIR);
1215            create_dir(&ledger_dir).unwrap();
1216            let previous_snapshot = ledger_dir.join("123");
1217            File::create(&previous_snapshot).unwrap();
1218
1219            let converted_snapshot = tmp_dir.join("456");
1220            File::create(&converted_snapshot).unwrap();
1221
1222            SnapshotConverterCommand::commit_converted_snapshot(
1223                1,
1224                &ProgressPrinter::new(ProgressOutputType::Hidden, 1),
1225                &tmp_dir,
1226                &converted_snapshot,
1227            )
1228            .expect_err("Should fail if converted snapshot has invalid filename");
1229
1230            assert!(previous_snapshot.exists());
1231        }
1232    }
1233
1234    mod cleanup {
1235        use super::*;
1236
1237        #[test]
1238        fn removes_both_dirs_on_success_when_commit_is_true() {
1239            let tmp = temp_dir_create!();
1240            let work_dir = tmp.join("workdir_dir");
1241            let distribution_dir = tmp.join("distribution_dir");
1242            create_dir(&work_dir).unwrap();
1243            create_dir(&distribution_dir).unwrap();
1244
1245            SnapshotConverterCommand::cleanup(&work_dir, &distribution_dir, true, true).unwrap();
1246
1247            assert!(!distribution_dir.exists());
1248            assert!(!work_dir.exists());
1249        }
1250
1251        #[test]
1252        fn removes_only_distribution_on_success_when_commit_is_false() {
1253            let tmp = temp_dir_create!();
1254            let work_dir = tmp.join("workdir_dir");
1255            let distribution_dir = tmp.join("distribution_dir");
1256            create_dir(&work_dir).unwrap();
1257            create_dir(&distribution_dir).unwrap();
1258
1259            SnapshotConverterCommand::cleanup(&work_dir, &distribution_dir, false, true).unwrap();
1260
1261            assert!(!distribution_dir.exists());
1262            assert!(work_dir.exists());
1263        }
1264
1265        #[test]
1266        fn removes_both_dirs_on_success_when_commit_is_true_and_distribution_is_nested() {
1267            let tmp = temp_dir_create!();
1268            let work_dir = tmp.join("workdir_dir");
1269            let distribution_dir = work_dir.join("distribution_dir");
1270            create_dir(&work_dir).unwrap();
1271            create_dir(&distribution_dir).unwrap();
1272
1273            SnapshotConverterCommand::cleanup(&work_dir, &distribution_dir, true, true).unwrap();
1274
1275            assert!(!distribution_dir.exists());
1276            assert!(!work_dir.exists());
1277        }
1278
1279        #[test]
1280        fn removes_only_distribution_on_success_when_commit_is_false_and_distribution_is_nested() {
1281            let tmp = temp_dir_create!();
1282            let work_dir = tmp.join("workdir_dir");
1283            let distribution_dir = work_dir.join("distribution_dir");
1284            create_dir(&work_dir).unwrap();
1285            create_dir(&distribution_dir).unwrap();
1286
1287            SnapshotConverterCommand::cleanup(&work_dir, &distribution_dir, false, true).unwrap();
1288
1289            assert!(!distribution_dir.exists());
1290            assert!(work_dir.exists());
1291        }
1292
1293        #[test]
1294        fn removes_both_dirs_on_failure() {
1295            let tmp = temp_dir_create!();
1296            let work_dir = tmp.join("workdir_dir");
1297            let distribution_dir = tmp.join("distribution_dir");
1298            create_dir(&work_dir).unwrap();
1299            create_dir(&distribution_dir).unwrap();
1300
1301            SnapshotConverterCommand::cleanup(&work_dir, &distribution_dir, false, false).unwrap();
1302
1303            assert!(!distribution_dir.exists());
1304            assert!(!work_dir.exists());
1305        }
1306    }
1307
1308    mod detect_cardano_network {
1309        use super::*;
1310
1311        fn create_protocol_magic_id_file(db_dir: &Path, magic_id: MagicId) -> PathBuf {
1312            let file_path = db_dir.join(PROTOCOL_MAGIC_ID_FILE);
1313            std::fs::write(&file_path, magic_id.to_string()).unwrap();
1314
1315            file_path
1316        }
1317
1318        #[test]
1319        fn detects_mainnet() {
1320            let db_dir = temp_dir_create!();
1321            create_protocol_magic_id_file(&db_dir, CardanoNetwork::MAINNET_MAGIC_ID);
1322
1323            let network = SnapshotConverterCommand::detect_cardano_network(&db_dir).unwrap();
1324
1325            assert_eq!(network, CardanoNetworkCliArg::Mainnet);
1326        }
1327
1328        #[test]
1329        fn detects_preprod() {
1330            let db_dir = temp_dir_create!();
1331            create_protocol_magic_id_file(&db_dir, CardanoNetwork::PREPROD_MAGIC_ID);
1332
1333            let network = SnapshotConverterCommand::detect_cardano_network(&db_dir).unwrap();
1334
1335            assert_eq!(network, CardanoNetworkCliArg::Preprod);
1336        }
1337
1338        #[test]
1339        fn detects_preview() {
1340            let db_dir = temp_dir_create!();
1341            create_protocol_magic_id_file(&db_dir, CardanoNetwork::PREVIEW_MAGIC_ID);
1342
1343            let network = SnapshotConverterCommand::detect_cardano_network(&db_dir).unwrap();
1344
1345            assert_eq!(network, CardanoNetworkCliArg::Preview);
1346        }
1347
1348        #[test]
1349        fn fails_on_invalid_network() {
1350            let db_dir = temp_dir_create!();
1351            let invalid_magic_id = 999999;
1352            create_protocol_magic_id_file(&db_dir, invalid_magic_id);
1353
1354            SnapshotConverterCommand::detect_cardano_network(&db_dir)
1355                .expect_err("Should fail with invalid network magic ID");
1356        }
1357
1358        #[test]
1359        fn fails_when_protocol_magic_id_file_is_empty() {
1360            let db_dir = temp_dir_create!();
1361            File::create(db_dir.join(PROTOCOL_MAGIC_ID_FILE)).unwrap();
1362
1363            SnapshotConverterCommand::detect_cardano_network(&db_dir)
1364                .expect_err("Should fail when protocol magic ID file is empty");
1365        }
1366
1367        #[test]
1368        fn fails_when_protocol_magic_id_file_is_missing() {
1369            let db_dir = temp_dir_create!();
1370
1371            assert!(!db_dir.join(PROTOCOL_MAGIC_ID_FILE).exists());
1372
1373            SnapshotConverterCommand::detect_cardano_network(&db_dir)
1374                .expect_err("Should fail when protocol magic ID file is missing");
1375        }
1376    }
1377
1378    mod snapshots_indexing {
1379        use mithril_common::temp_dir_create;
1380
1381        use super::*;
1382
1383        #[test]
1384        fn returns_the_two_most_recent_snapshots() {
1385            let db_dir = temp_dir_create!();
1386            let ledger_dir = db_dir.join(LEDGER_DIR);
1387            create_dir(&ledger_dir).unwrap();
1388
1389            create_dir(ledger_dir.join("1500")).unwrap();
1390            create_dir(ledger_dir.join("500")).unwrap();
1391            create_dir(ledger_dir.join("1000")).unwrap();
1392
1393            let found = SnapshotConverterCommand::find_most_recent_snapshots(&db_dir, 2).unwrap();
1394
1395            assert_eq!(
1396                found,
1397                vec![ledger_dir.join("1500"), ledger_dir.join("1000")]
1398            );
1399        }
1400
1401        #[test]
1402        fn returns_list_with_one_entry_if_only_one_valid_snapshot() {
1403            let db_dir = temp_dir_create!();
1404            let ledger_dir = db_dir.join(LEDGER_DIR);
1405            create_dir(&ledger_dir).unwrap();
1406
1407            create_dir(ledger_dir.join("500")).unwrap();
1408
1409            let found = SnapshotConverterCommand::find_most_recent_snapshots(&db_dir, 2).unwrap();
1410
1411            assert_eq!(found, vec![ledger_dir.join("500")]);
1412        }
1413
1414        #[test]
1415        fn ignores_non_numeric_and_non_directory_entries() {
1416            let temp_dir = temp_dir_create!();
1417            let ledger_dir = temp_dir.join(LEDGER_DIR);
1418            create_dir(&ledger_dir).unwrap();
1419
1420            create_dir(ledger_dir.join("1000")).unwrap();
1421            File::create(ledger_dir.join("500")).unwrap();
1422            create_dir(ledger_dir.join("invalid")).unwrap();
1423
1424            let found = SnapshotConverterCommand::find_most_recent_snapshots(&temp_dir, 2).unwrap();
1425
1426            assert_eq!(found, vec![ledger_dir.join("1000")]);
1427        }
1428
1429        #[test]
1430        fn returns_all_available_snapshots_when_count_exceeds_available() {
1431            let db_dir = temp_dir_create!();
1432            let ledger_dir = db_dir.join(LEDGER_DIR);
1433            create_dir(&ledger_dir).unwrap();
1434
1435            create_dir(ledger_dir.join("1000")).unwrap();
1436            create_dir(ledger_dir.join("1500")).unwrap();
1437
1438            let found = SnapshotConverterCommand::find_most_recent_snapshots(&db_dir, 99).unwrap();
1439
1440            assert_eq!(
1441                found,
1442                vec![ledger_dir.join("1500"), ledger_dir.join("1000")]
1443            );
1444        }
1445
1446        #[test]
1447        fn returns_error_if_no_valid_snapshot_found() {
1448            let temp_dir = temp_dir_create!();
1449            let ledger_dir = temp_dir.join(LEDGER_DIR);
1450            create_dir(&ledger_dir).unwrap();
1451
1452            File::create(ledger_dir.join("invalid")).unwrap();
1453
1454            SnapshotConverterCommand::find_most_recent_snapshots(&temp_dir, 2)
1455                .expect_err("Should return error if no valid ledger snapshot directory found");
1456        }
1457
1458        #[test]
1459        fn get_sorted_snapshot_dirs_returns_sorted_valid_directories() {
1460            let temp_dir = temp_dir_create!();
1461            let ledger_dir = temp_dir.join(LEDGER_DIR);
1462            create_dir(&ledger_dir).unwrap();
1463
1464            create_dir(ledger_dir.join("1500")).unwrap();
1465            create_dir(ledger_dir.join("1000")).unwrap();
1466            create_dir(ledger_dir.join("2000")).unwrap();
1467            File::create(ledger_dir.join("500")).unwrap();
1468            create_dir(ledger_dir.join("notanumber")).unwrap();
1469
1470            let snapshots =
1471                SnapshotConverterCommand::get_sorted_snapshot_dirs(&ledger_dir).unwrap();
1472
1473            assert_eq!(
1474                snapshots,
1475                vec![
1476                    (1000, ledger_dir.join("1000")),
1477                    (1500, ledger_dir.join("1500")),
1478                    (2000, ledger_dir.join("2000")),
1479                ]
1480            );
1481        }
1482    }
1483
1484    mod snapshot_conversion_fallback {
1485        use mockall::predicate::{self, always};
1486
1487        use super::*;
1488
1489        fn create_dummy_snapshots(dir: &Path, count: usize) -> Vec<PathBuf> {
1490            (1..=count)
1491                .map(|i| {
1492                    let snapshot_path = dir.join(format!("{i}"));
1493                    create_dir(&snapshot_path).unwrap();
1494                    snapshot_path
1495                })
1496                .collect()
1497        }
1498
1499        fn contains_filename(expected: &Path) -> impl Fn(&Path) -> bool + use<> {
1500            let filename = expected.file_name().unwrap().to_string_lossy().to_string();
1501
1502            move |p: &Path| p.to_string_lossy().contains(&filename)
1503        }
1504
1505        #[test]
1506        fn conversion_succeed_and_is_called_once_if_first_conversion_succeeds() {
1507            let temp_dir = temp_dir_create!();
1508            let snapshots = create_dummy_snapshots(&temp_dir, 2);
1509
1510            let mut converter = MockSnapshotConverter::new();
1511            converter
1512                .expect_convert()
1513                .with(
1514                    predicate::function(contains_filename(&snapshots[0])),
1515                    always(),
1516                )
1517                .return_once(|_, _| Ok(()))
1518                .once();
1519
1520            SnapshotConverterCommand::try_convert(
1521                &ProgressPrinter::new(ProgressOutputType::Hidden, 1),
1522                Path::new(""),
1523                &UTxOHDFlavor::Lmdb,
1524                &snapshots,
1525                Box::new(converter),
1526            )
1527            .unwrap();
1528        }
1529
1530        #[test]
1531        fn conversion_falls_back_if_first_conversion_fails() {
1532            let temp_dir = temp_dir_create!();
1533            let snapshots = create_dummy_snapshots(&temp_dir, 2);
1534
1535            let mut converter = MockSnapshotConverter::new();
1536            converter
1537                .expect_convert()
1538                .with(
1539                    predicate::function(contains_filename(&snapshots[0])),
1540                    always(),
1541                )
1542                .return_once(|_, _| Err(anyhow!("Error during conversion")))
1543                .once();
1544            converter
1545                .expect_convert()
1546                .with(
1547                    predicate::function(contains_filename(&snapshots[1])),
1548                    always(),
1549                )
1550                .return_once(|_, _| Ok(()))
1551                .once();
1552
1553            SnapshotConverterCommand::try_convert(
1554                &ProgressPrinter::new(ProgressOutputType::Hidden, 1),
1555                Path::new(""),
1556                &UTxOHDFlavor::Lmdb,
1557                &snapshots,
1558                Box::new(converter),
1559            )
1560            .expect("Should succeed even if the first conversion fails");
1561        }
1562
1563        #[test]
1564        fn conversion_fails_if_all_attempts_fail() {
1565            let temp_dir = temp_dir_create!();
1566            let snapshots = create_dummy_snapshots(&temp_dir, 2);
1567            let mut converter = MockSnapshotConverter::new();
1568            converter
1569                .expect_convert()
1570                .returning(|_, _| Err(anyhow!("Conversion failed")))
1571                .times(2);
1572
1573            SnapshotConverterCommand::try_convert(
1574                &ProgressPrinter::new(ProgressOutputType::Hidden, 1),
1575                Path::new(""),
1576                &UTxOHDFlavor::Lmdb,
1577                &snapshots,
1578                Box::new(converter),
1579            )
1580            .expect_err("Should fail if all conversion attempts fail");
1581        }
1582    }
1583
1584    mod get_snapshot_converter_bin_by_version {
1585        use std::path::PathBuf;
1586
1587        use crate::commands::tools::utxo_hd::snapshot_converter::{
1588            SnapshotConverterBin, SnapshotConverterConfig, UTxOHDFlavor,
1589            get_snapshot_converter_bin_by_version,
1590        };
1591
1592        #[test]
1593        fn should_return_snapshot_converter_bin_new_with_cardano_version_10_6_2_or_upper() {
1594            let config = SnapshotConverterConfig {
1595                converter_bin: PathBuf::new(),
1596                config_path: PathBuf::new(),
1597                utxo_hd_flavor: UTxOHDFlavor::Lmdb,
1598                hide_output: true,
1599            };
1600
1601            let converter_bin = get_snapshot_converter_bin_by_version("10.6.2", config.clone());
1602            assert!(
1603                matches!(converter_bin, SnapshotConverterBin::From10_6(_)),
1604                "returned type is not SnapshotConverterBin::From10_6"
1605            );
1606
1607            let converter_bin = get_snapshot_converter_bin_by_version("10.7.0", config.clone());
1608            assert!(
1609                matches!(converter_bin, SnapshotConverterBin::From10_6(_)),
1610                "returned type is not SnapshotConverterBin::From10_6"
1611            );
1612        }
1613
1614        #[test]
1615        fn should_return_snapshot_converter_bin_new_with_cardano_version_latest() {
1616            let config = SnapshotConverterConfig {
1617                converter_bin: PathBuf::new(),
1618                config_path: PathBuf::new(),
1619                utxo_hd_flavor: UTxOHDFlavor::Lmdb,
1620                hide_output: true,
1621            };
1622
1623            let converter_bin = get_snapshot_converter_bin_by_version("latest", config.clone());
1624            assert!(
1625                matches!(converter_bin, SnapshotConverterBin::From10_6(_)),
1626                "returned type is not SnapshotConverterBin::From10_6"
1627            );
1628        }
1629
1630        #[test]
1631        fn should_return_snapshot_converter_bin_old_with_cardano_version_bellow_10_6_2() {
1632            let config = SnapshotConverterConfig {
1633                converter_bin: PathBuf::new(),
1634                config_path: PathBuf::new(),
1635                utxo_hd_flavor: UTxOHDFlavor::Lmdb,
1636                hide_output: true,
1637            };
1638
1639            let converter_bin = get_snapshot_converter_bin_by_version("10.6.1", config);
1640            assert!(
1641                matches!(converter_bin, SnapshotConverterBin::UpTo10_5(_)),
1642                "returned type is not SnapshotConverterBin::UpTo10_5"
1643            );
1644        }
1645    }
1646}