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