1use crate::{api, git::GITHUB_API_CLIENT, Git, Status};
4pub use binary::*;
5use duct::cmd;
6use flate2::read::GzDecoder;
7use reqwest::StatusCode;
8use std::{
9 error::Error as _,
10 fs::{copy, metadata, read_dir, rename, File},
11 io::{BufRead, Seek, SeekFrom, Write},
12 os::unix::fs::PermissionsExt,
13 path::{Path, PathBuf},
14 time::Duration,
15};
16use tar::Archive;
17use tempfile::{tempdir, tempfile};
18use thiserror::Error;
19use url::Url;
20
21mod binary;
22
23#[derive(Error, Debug)]
25pub enum Error {
26 #[error("Anyhow error: {0}")]
28 AnyhowError(#[from] anyhow::Error),
29 #[error("API error: {0}")]
31 ApiError(#[from] api::Error),
32 #[error("Archive error: {0}")]
34 ArchiveError(String),
35 #[error("HTTP error: {0} caused by {:?}", reqwest::Error::source(.0))]
37 HttpError(#[from] reqwest::Error),
38 #[error("IO error: {0}")]
40 IO(#[from] std::io::Error),
41 #[error("Missing binary: {0}")]
43 MissingBinary(String),
44 #[error("ParseError error: {0}")]
46 ParseError(#[from] url::ParseError),
47}
48
49#[derive(Clone, Debug, PartialEq)]
51pub enum Source {
52 #[allow(dead_code)]
54 Archive {
55 url: String,
57 contents: Vec<String>,
59 },
60 Git {
62 url: Url,
64 reference: Option<String>,
66 manifest: Option<PathBuf>,
68 package: String,
70 artifacts: Vec<String>,
72 },
73 GitHub(GitHub),
75 #[allow(dead_code)]
77 Url {
78 url: String,
80 name: String,
82 },
83}
84
85impl Source {
86 pub(super) async fn source(
96 &self,
97 cache: &Path,
98 release: bool,
99 status: &impl Status,
100 verbose: bool,
101 ) -> Result<(), Error> {
102 use Source::*;
103 match self {
104 Archive { url, contents } => {
105 let contents: Vec<_> =
106 contents.iter().map(|name| (name.as_str(), cache.join(name))).collect();
107 from_archive(url, &contents, status).await
108 },
109 Git { url, reference, manifest, package, artifacts } => {
110 let artifacts: Vec<_> = artifacts
111 .iter()
112 .map(|name| match reference {
113 Some(version) => (name.as_str(), cache.join(format!("{name}-{version}"))),
114 None => (name.as_str(), cache.join(name)),
115 })
116 .collect();
117 from_git(
118 url.as_str(),
119 reference.as_deref(),
120 manifest.as_ref(),
121 package,
122 &artifacts,
123 release,
124 status,
125 verbose,
126 )
127 .await
128 },
129 GitHub(source) => source.source(cache, release, status, verbose).await,
130 Url { url, name } => from_url(url, &cache.join(name), status).await,
131 }
132 }
133}
134
135#[derive(Clone, Debug, PartialEq)]
137pub enum GitHub {
138 ReleaseArchive {
140 owner: String,
142 repository: String,
144 tag: Option<String>,
146 tag_format: Option<String>,
148 archive: String,
150 contents: Vec<(&'static str, Option<String>)>,
153 latest: Option<String>,
155 },
156 SourceCodeArchive {
158 owner: String,
160 repository: String,
162 reference: Option<String>,
164 manifest: Option<PathBuf>,
166 package: String,
168 artifacts: Vec<String>,
170 },
171}
172
173impl GitHub {
174 async fn source(
184 &self,
185 cache: &Path,
186 release: bool,
187 status: &impl Status,
188 verbose: bool,
189 ) -> Result<(), Error> {
190 use GitHub::*;
191 match self {
192 ReleaseArchive { owner, repository, tag, tag_format, archive, contents, .. } => {
193 let base_url = format!("https://github.com/{owner}/{repository}/releases");
195 let url = match tag.as_ref() {
196 Some(tag) => {
197 let tag = tag_format.as_ref().map_or_else(
198 || tag.to_string(),
199 |tag_format| tag_format.replace("{tag}", tag),
200 );
201 format!("{base_url}/download/{tag}/{archive}")
202 },
203 None => format!("{base_url}/latest/download/{archive}"),
204 };
205 let contents: Vec<_> = contents
206 .iter()
207 .map(|(name, target)| match tag.as_ref() {
208 Some(tag) => (
209 *name,
210 cache.join(format!(
211 "{}-{tag}",
212 target.as_ref().map_or(*name, |t| t.as_str())
213 )),
214 ),
215 None => (*name, cache.join(target.as_ref().map_or(*name, |t| t.as_str()))),
216 })
217 .collect();
218 from_archive(&url, &contents, status).await
219 },
220 SourceCodeArchive { owner, repository, reference, manifest, package, artifacts } => {
221 let artifacts: Vec<_> = artifacts
222 .iter()
223 .map(|name| match reference {
224 Some(reference) =>
225 (name.as_str(), cache.join(format!("{name}-{reference}"))),
226 None => (name.as_str(), cache.join(name)),
227 })
228 .collect();
229 from_github_archive(
230 owner,
231 repository,
232 reference.as_ref().map(|r| r.as_str()),
233 manifest.as_ref(),
234 package,
235 &artifacts,
236 release,
237 status,
238 verbose,
239 )
240 .await
241 },
242 }
243 }
244}
245
246async fn from_archive(
253 url: &str,
254 contents: &[(&str, PathBuf)],
255 status: &impl Status,
256) -> Result<(), Error> {
257 status.update(&format!("Downloading from {url}..."));
259 let response = reqwest::get(url).await?.error_for_status()?;
260 let mut file = tempfile()?;
261 file.write_all(&response.bytes().await?)?;
262 file.seek(SeekFrom::Start(0))?;
263 status.update("Extracting from archive...");
265 let tar = GzDecoder::new(file);
266 let mut archive = Archive::new(tar);
267 let temp_dir = tempdir()?;
268 let working_dir = temp_dir.path();
269 archive.unpack(working_dir)?;
270 for (name, dest) in contents {
271 let src = working_dir.join(name);
272 if src.exists() {
273 if let Err(_e) = rename(&src, dest) {
274 copy(&src, dest)?;
276 std::fs::remove_file(&src)?;
277 }
278 } else {
279 return Err(Error::ArchiveError(format!(
280 "Expected file '{}' in archive, but it was not found.",
281 name
282 )));
283 }
284 }
285 status.update("Sourcing complete.");
286 Ok(())
287}
288
289#[allow(clippy::too_many_arguments)]
301async fn from_git(
302 url: &str,
303 reference: Option<&str>,
304 manifest: Option<impl AsRef<Path>>,
305 package: &str,
306 artifacts: &[(&str, impl AsRef<Path>)],
307 release: bool,
308 status: &impl Status,
309 verbose: bool,
310) -> Result<(), Error> {
311 let temp_dir = tempdir()?;
313 let working_dir = temp_dir.path();
314 status.update(&format!("Cloning {url}..."));
315 Git::clone(&Url::parse(url)?, working_dir, reference)?;
316 status.update("Starting build of binary...");
318 let manifest = manifest
319 .as_ref()
320 .map_or_else(|| working_dir.join("Cargo.toml"), |m| working_dir.join(m));
321 build(manifest, package, artifacts, release, status, verbose).await?;
322 status.update("Sourcing complete.");
323 Ok(())
324}
325
326#[allow(clippy::too_many_arguments)]
339async fn from_github_archive(
340 owner: &str,
341 repository: &str,
342 reference: Option<&str>,
343 manifest: Option<impl AsRef<Path>>,
344 package: &str,
345 artifacts: &[(&str, impl AsRef<Path>)],
346 release: bool,
347 status: &impl Status,
348 verbose: bool,
349) -> Result<(), Error> {
350 let response =
352 match reference {
353 Some(reference) => {
354 let urls = [
356 format!("https://github.com/{owner}/{repository}/archive/refs/heads/{reference}.tar.gz"),
357 format!("https://github.com/{owner}/{repository}/archive/refs/tags/{reference}.tar.gz"),
358 format!("https://github.com/{owner}/{repository}/archive/{reference}.tar.gz"),
359 ];
360 let mut response = None;
361 for url in urls {
362 status.update(&format!("Downloading from {url}..."));
363 response = Some(GITHUB_API_CLIENT.get(url).await);
364 if let Some(Err(api::Error::HttpError(e))) = &response {
365 if e.status() == Some(StatusCode::NOT_FOUND) {
366 tokio::time::sleep(Duration::from_secs(1)).await;
367 continue;
368 }
369 }
370 break;
371 }
372 response.expect("value set above")?
373 },
374 None => {
375 let url = format!("https://api.github.com/repos/{owner}/{repository}/tarball");
376 status.update(&format!("Downloading from {url}..."));
377 GITHUB_API_CLIENT.get(url).await?
378 },
379 };
380 let mut file = tempfile()?;
381 file.write_all(&response)?;
382 file.seek(SeekFrom::Start(0))?;
383 status.update("Extracting from archive...");
385 let tar = GzDecoder::new(file);
386 let mut archive = Archive::new(tar);
387 let temp_dir = tempdir()?;
388 let mut working_dir = temp_dir.path().into();
389 archive.unpack(&working_dir)?;
390 let entries: Vec<_> = read_dir(&working_dir)?.take(2).filter_map(|x| x.ok()).collect();
392 match entries.len() {
393 0 =>
394 return Err(Error::ArchiveError(
395 "The downloaded archive does not contain any entries.".into(),
396 )),
397 1 => working_dir = entries[0].path(), _ => {}, }
401 status.update("Starting build of binary...");
403 let manifest = manifest
404 .as_ref()
405 .map_or_else(|| working_dir.join("Cargo.toml"), |m| working_dir.join(m));
406 build(&manifest, package, artifacts, release, status, verbose).await?;
407 status.update("Sourcing complete.");
408 Ok(())
409}
410
411pub(crate) async fn from_local_package(
420 manifest: &Path,
421 package: &str,
422 release: bool,
423 status: &impl Status,
424 verbose: bool,
425) -> Result<(), Error> {
426 status.update("Starting build of binary...");
428 const EMPTY: [(&str, PathBuf); 0] = [];
429 build(manifest, package, &EMPTY, release, status, verbose).await?;
430 status.update("Sourcing complete.");
431 Ok(())
432}
433
434async fn from_url(url: &str, path: &Path, status: &impl Status) -> Result<(), Error> {
441 status.update(&format!("Downloading from {url}..."));
443 download(url, path).await?;
444 status.update("Sourcing complete.");
445 Ok(())
446}
447
448async fn build(
458 manifest: impl AsRef<Path>,
459 package: &str,
460 artifacts: &[(&str, impl AsRef<Path>)],
461 release: bool,
462 status: &impl Status,
463 verbose: bool,
464) -> Result<(), Error> {
465 let manifest_path = manifest.as_ref().to_str().expect("expected manifest path to be valid");
467 let mut args = vec!["build", "-p", package, "--manifest-path", manifest_path];
468 if release {
469 args.push("--release")
470 }
471 let command = cmd("cargo", args);
473 match verbose {
474 false => {
475 let reader = command.stderr_to_stdout().reader()?;
476 let output = std::io::BufReader::new(reader).lines();
477 for line in output {
478 status.update(&line?);
479 }
480 },
481 true => {
482 command.run()?;
483 },
484 }
485 let target = manifest
487 .as_ref()
488 .parent()
489 .expect("")
490 .join(format!("target/{}", if release { "release" } else { "debug" }));
491 for (name, dest) in artifacts {
492 copy(target.join(name), dest)?;
493 }
494 Ok(())
495}
496
497async fn download(url: &str, dest: &Path) -> Result<(), Error> {
503 let response = reqwest::get(url).await?.error_for_status()?;
505 let mut file = File::create(dest)?;
506 file.write_all(&response.bytes().await?)?;
507 set_executable_permission(dest)?;
509 Ok(())
510}
511
512pub fn set_executable_permission<P: AsRef<Path>>(path: P) -> Result<(), Error> {
517 let mut perms = metadata(&path)?.permissions();
518 perms.set_mode(0o755);
519 std::fs::set_permissions(path, perms)?;
520 Ok(())
521}
522
523#[cfg(test)]
524pub(super) mod tests {
525 use super::{GitHub::*, Status, *};
526 use crate::target;
527 use tempfile::tempdir;
528
529 #[tokio::test]
530 async fn sourcing_from_archive_works() -> anyhow::Result<()> {
531 let url = "https://github.com/r0gue-io/polkadot/releases/latest/download/polkadot-aarch64-apple-darwin.tar.gz".to_string();
532 let name = "polkadot".to_string();
533 let contents =
534 vec![name.clone(), "polkadot-execute-worker".into(), "polkadot-prepare-worker".into()];
535 let temp_dir = tempdir()?;
536
537 Source::Archive { url, contents: contents.clone() }
538 .source(temp_dir.path(), true, &Output, true)
539 .await?;
540 for item in contents {
541 assert!(temp_dir.path().join(item).exists());
542 }
543 Ok(())
544 }
545
546 #[tokio::test]
547 async fn sourcing_from_git_works() -> anyhow::Result<()> {
548 let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
549 let package = "hello_world".to_string();
550 let temp_dir = tempdir()?;
551
552 Source::Git {
553 url,
554 reference: None,
555 manifest: None,
556 package: package.clone(),
557 artifacts: vec![package.clone()],
558 }
559 .source(temp_dir.path(), true, &Output, true)
560 .await?;
561 assert!(temp_dir.path().join(package).exists());
562 Ok(())
563 }
564
565 #[tokio::test]
566 async fn sourcing_from_git_ref_works() -> anyhow::Result<()> {
567 let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
568 let initial_commit = "436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3".to_string();
569 let package = "hello_world".to_string();
570 let temp_dir = tempdir()?;
571
572 Source::Git {
573 url,
574 reference: Some(initial_commit.clone()),
575 manifest: None,
576 package: package.clone(),
577 artifacts: vec![package.clone()],
578 }
579 .source(temp_dir.path(), true, &Output, true)
580 .await?;
581 assert!(temp_dir.path().join(format!("{package}-{initial_commit}")).exists());
582 Ok(())
583 }
584
585 #[tokio::test]
586 async fn sourcing_from_github_release_archive_works() -> anyhow::Result<()> {
587 let owner = "r0gue-io".to_string();
588 let repository = "polkadot".to_string();
589 let tag = "v1.12.0";
590 let tag_format = Some("polkadot-{tag}".to_string());
591 let name = "polkadot".to_string();
592 let archive = format!("{name}-{}.tar.gz", target()?);
593 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
594 let temp_dir = tempdir()?;
595
596 Source::GitHub(ReleaseArchive {
597 owner,
598 repository,
599 tag: Some(tag.to_string()),
600 tag_format,
601 archive,
602 contents: contents.map(|n| (n, None)).to_vec(),
603 latest: None,
604 })
605 .source(temp_dir.path(), true, &Output, true)
606 .await?;
607 for item in contents {
608 assert!(temp_dir.path().join(format!("{item}-{tag}")).exists());
609 }
610 Ok(())
611 }
612
613 #[tokio::test]
614 async fn sourcing_from_github_release_archive_maps_contents() -> anyhow::Result<()> {
615 let owner = "r0gue-io".to_string();
616 let repository = "polkadot".to_string();
617 let tag = "v1.12.0";
618 let tag_format = Some("polkadot-{tag}".to_string());
619 let name = "polkadot".to_string();
620 let archive = format!("{name}-{}.tar.gz", target()?);
621 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
622 let temp_dir = tempdir()?;
623 let prefix = "test";
624
625 Source::GitHub(ReleaseArchive {
626 owner,
627 repository,
628 tag: Some(tag.to_string()),
629 tag_format,
630 archive,
631 contents: contents.map(|n| (n, Some(format!("{prefix}-{n}")))).to_vec(),
632 latest: None,
633 })
634 .source(temp_dir.path(), true, &Output, true)
635 .await?;
636 for item in contents {
637 assert!(temp_dir.path().join(format!("{prefix}-{item}-{tag}")).exists());
638 }
639 Ok(())
640 }
641
642 #[tokio::test]
643 async fn sourcing_from_latest_github_release_archive_works() -> anyhow::Result<()> {
644 let owner = "r0gue-io".to_string();
645 let repository = "polkadot".to_string();
646 let tag_format = Some("polkadot-{tag}".to_string());
647 let name = "polkadot".to_string();
648 let archive = format!("{name}-{}.tar.gz", target()?);
649 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
650 let temp_dir = tempdir()?;
651
652 Source::GitHub(ReleaseArchive {
653 owner,
654 repository,
655 tag: None,
656 tag_format,
657 archive,
658 contents: contents.map(|n| (n, None)).to_vec(),
659 latest: None,
660 })
661 .source(temp_dir.path(), true, &Output, true)
662 .await?;
663 for item in contents {
664 assert!(temp_dir.path().join(item).exists());
665 }
666 Ok(())
667 }
668
669 #[tokio::test]
670 async fn sourcing_from_github_source_code_archive_works() -> anyhow::Result<()> {
671 let owner = "paritytech".to_string();
672 let repository = "polkadot-sdk".to_string();
673 let package = "polkadot".to_string();
674 let temp_dir = tempdir()?;
675 let initial_commit = "72dba98250a6267c61772cd55f8caf193141050f";
676 let manifest = PathBuf::from("substrate/Cargo.toml");
677
678 Source::GitHub(SourceCodeArchive {
679 owner,
680 repository,
681 reference: Some(initial_commit.to_string()),
682 manifest: Some(manifest),
683 package: package.clone(),
684 artifacts: vec![package.clone()],
685 })
686 .source(temp_dir.path(), true, &Output, true)
687 .await?;
688 assert!(temp_dir.path().join(format!("{package}-{initial_commit}")).exists());
689 Ok(())
690 }
691
692 #[tokio::test]
693 async fn sourcing_from_latest_github_source_code_archive_works() -> anyhow::Result<()> {
694 let owner = "hpaluch".to_string();
695 let repository = "rust-hello-world".to_string();
696 let package = "hello_world".to_string();
697 let temp_dir = tempdir()?;
698
699 Source::GitHub(SourceCodeArchive {
700 owner,
701 repository,
702 reference: None,
703 manifest: None,
704 package: package.clone(),
705 artifacts: vec![package.clone()],
706 })
707 .source(temp_dir.path(), true, &Output, true)
708 .await?;
709 assert!(temp_dir.path().join(package).exists());
710 Ok(())
711 }
712
713 #[tokio::test]
714 async fn sourcing_from_url_works() -> anyhow::Result<()> {
715 let url =
716 "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc"
717 .to_string();
718 let name = "polkadot";
719 let temp_dir = tempdir()?;
720
721 Source::Url { url, name: name.into() }
722 .source(temp_dir.path(), false, &Output, true)
723 .await?;
724 assert!(temp_dir.path().join(&name).exists());
725 Ok(())
726 }
727
728 #[tokio::test]
729 async fn from_archive_works() -> anyhow::Result<()> {
730 let temp_dir = tempdir()?;
731 let url = "https://github.com/r0gue-io/polkadot/releases/latest/download/polkadot-aarch64-apple-darwin.tar.gz";
732 let contents: Vec<_> = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"]
733 .into_iter()
734 .map(|b| (b, temp_dir.path().join(b)))
735 .collect();
736
737 from_archive(url, &contents, &Output).await?;
738 for (_, file) in contents {
739 assert!(file.exists());
740 }
741 Ok(())
742 }
743
744 #[tokio::test]
745 async fn from_git_works() -> anyhow::Result<()> {
746 let url = "https://github.com/hpaluch/rust-hello-world";
747 let package = "hello_world";
748 let initial_commit = "436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3";
749 let temp_dir = tempdir()?;
750 let path = temp_dir.path().join(package);
751
752 from_git(
753 url,
754 Some(initial_commit),
755 None::<&Path>,
756 &package,
757 &[(&package, &path)],
758 true,
759 &Output,
760 false,
761 )
762 .await?;
763 assert!(path.exists());
764 Ok(())
765 }
766
767 #[tokio::test]
768 async fn from_github_archive_works() -> anyhow::Result<()> {
769 let owner = "paritytech";
770 let repository = "polkadot-sdk";
771 let package = "polkadot";
772 let temp_dir = tempdir()?;
773 let path = temp_dir.path().join(package);
774 let initial_commit = "72dba98250a6267c61772cd55f8caf193141050f";
775 let manifest = "substrate/Cargo.toml";
776
777 from_github_archive(
778 owner,
779 repository,
780 Some(initial_commit),
781 Some(manifest),
782 package,
783 &[(package, &path)],
784 true,
785 &Output,
786 true,
787 )
788 .await?;
789 assert!(path.exists());
790 Ok(())
791 }
792
793 #[tokio::test]
794 async fn from_latest_github_archive_works() -> anyhow::Result<()> {
795 let owner = "hpaluch";
796 let repository = "rust-hello-world";
797 let package = "hello_world";
798 let temp_dir = tempdir()?;
799 let path = temp_dir.path().join(package);
800
801 from_github_archive(
802 owner,
803 repository,
804 None,
805 None::<&Path>,
806 package,
807 &[(package, &path)],
808 true,
809 &Output,
810 true,
811 )
812 .await?;
813 assert!(path.exists());
814 Ok(())
815 }
816
817 #[tokio::test]
818 async fn from_local_package_works() -> anyhow::Result<()> {
819 let temp_dir = tempdir()?;
820 let name = "hello_world";
821 cmd("cargo", ["new", name, "--bin"]).dir(temp_dir.path()).run()?;
822 let manifest = temp_dir.path().join(name).join("Cargo.toml");
823
824 from_local_package(&manifest, name, false, &Output, true).await?;
825 assert!(manifest.parent().unwrap().join("target/debug").join(name).exists());
826 Ok(())
827 }
828
829 #[tokio::test]
830 async fn from_url_works() -> anyhow::Result<()> {
831 let url =
832 "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc";
833 let temp_dir = tempdir()?;
834 let path = temp_dir.path().join("polkadot");
835
836 from_url(url, &path, &Output).await?;
837 assert!(path.exists());
838 assert_ne!(metadata(path)?.permissions().mode() & 0o755, 0);
839 Ok(())
840 }
841
842 pub(crate) struct Output;
843 impl Status for Output {
844 fn update(&self, status: &str) {
845 println!("{status}")
846 }
847 }
848}
849
850pub mod traits {
852 use crate::{sourcing::Error, GitHub};
853 use strum::EnumProperty;
854
855 pub trait Source: EnumProperty {
857 fn binary(&self) -> &'static str {
859 self.get_str("Binary").expect("expected specification of `Binary` name")
860 }
861
862 fn fallback(&self) -> &str {
864 self.get_str("Fallback")
865 .expect("expected specification of `Fallback` release tag")
866 }
867
868 fn prerelease(&self) -> Option<bool> {
870 self.get_str("Prerelease")
871 .map(|v| v.parse().expect("expected parachain prerelease value to be true/false"))
872 }
873
874 #[allow(async_fn_in_trait)]
876 async fn releases(&self) -> Result<Vec<String>, Error> {
877 let repo = GitHub::parse(self.repository())?;
878 let releases = match repo.releases().await {
879 Ok(releases) => releases,
880 Err(_) => return Ok(vec![self.fallback().to_string()]),
881 };
882 let prerelease = self.prerelease();
883 let tag_format = self.tag_format();
884 Ok(releases
885 .iter()
886 .filter(|r| match prerelease {
887 None => !r.prerelease, Some(prerelease) => r.prerelease == prerelease,
889 })
890 .map(|r| {
891 if let Some(tag_format) = tag_format {
892 let tag_format = tag_format.replace("{tag}", "");
894 r.tag_name.replace(&tag_format, "")
895 } else {
896 r.tag_name.clone()
897 }
898 })
899 .collect())
900 }
901
902 fn repository(&self) -> &str {
904 self.get_str("Repository").expect("expected specification of `Repository` url")
905 }
906
907 fn tag_format(&self) -> Option<&str> {
909 self.get_str("TagFormat")
910 }
911 }
912
913 pub trait TryInto {
915 fn try_into(
921 &self,
922 specifier: Option<String>,
923 latest: Option<String>,
924 ) -> Result<super::Source, crate::Error>;
925 }
926
927 #[cfg(test)]
928 mod tests {
929 use super::Source;
930 use strum_macros::{EnumProperty, VariantArray};
931
932 #[derive(EnumProperty, VariantArray)]
933 pub(super) enum Chain {
934 #[strum(props(
935 Repository = "https://github.com/paritytech/polkadot-sdk",
936 Binary = "polkadot",
937 Prerelease = "false",
938 Fallback = "v1.12.0",
939 TagFormat = "polkadot-{tag}"
940 ))]
941 Polkadot,
942 #[strum(props(
943 Repository = "https://github.com/r0gue-io/fallback",
944 Fallback = "v1.0"
945 ))]
946 Fallback,
947 }
948
949 impl Source for Chain {}
950
951 #[test]
952 fn binary_works() {
953 assert_eq!("polkadot", Chain::Polkadot.binary())
954 }
955
956 #[test]
957 fn fallback_works() {
958 assert_eq!("v1.12.0", Chain::Polkadot.fallback())
959 }
960
961 #[test]
962 fn prerelease_works() {
963 assert!(!Chain::Polkadot.prerelease().unwrap())
964 }
965
966 #[tokio::test]
967 async fn releases_works() -> anyhow::Result<()> {
968 assert!(!Chain::Polkadot.releases().await?.is_empty());
969 Ok(())
970 }
971
972 #[tokio::test]
973 async fn releases_uses_fallback() -> anyhow::Result<()> {
974 let chain = Chain::Fallback;
975 assert_eq!(chain.fallback(), chain.releases().await?[0]);
976 Ok(())
977 }
978
979 #[test]
980 fn repository_works() {
981 assert_eq!("https://github.com/paritytech/polkadot-sdk", Chain::Polkadot.repository())
982 }
983
984 #[test]
985 fn tag_format_works() {
986 assert_eq!("polkadot-{tag}", Chain::Polkadot.tag_format().unwrap())
987 }
988 }
989}