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 Stdio::null()
175 } else {
176 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 Stdio::null()
200 } else {
201 std::io::stderr().into()
203 });
204 command
205}
206
207#[derive(Parser, Debug, Clone)]
209pub struct SnapshotConverterCommand {
210 #[clap(long)]
212 db_directory: PathBuf,
213
214 #[clap(long)]
218 cardano_node_version: String,
219
220 #[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 #[clap(long)]
230 utxo_hd_flavor: UTxOHDFlavor,
231
232 #[clap(long)]
234 commit: bool,
235
236 #[clap(long, env = "GITHUB_TOKEN")]
238 github_token: Option<String>,
239}
240
241impl SnapshotConverterCommand {
242 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 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 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}