From 5d2a9406e8cd667b1dd02052b27d6ab46e3a159e Mon Sep 17 00:00:00 2001 From: Peter White <23270067+peterwht@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:45:56 -0600 Subject: [PATCH] feat(contract-e2e): auto-source substrate-contracts-node with e2e tests (#254) * feat(contracts-e2e): auto-source contracts-node binary if not present * chore(contract-e2e): deprecate --features e2e-tests * fix: remove unnecessary async function * refactor(contracts-node): reduce duplicated code with binary sourcing (#255) * refactor(up-contract): use Binary struct from parachains -> up -> sourcing to auto-launch contracts-node * refactor(contracts_node): reduce duplicated code -- checkpoint, not working * refactor(contracts_node): use Binary and Source structs for substrate-contracts-node * chore: small changes * chore: small changes * chore: add e2e help, prevent cmd output, and cleanup * refactor(contracts_node): introduce helper to download contracts node if it does not exist * refactor(sourcing): move sourcing functionality to `pop-common` (#258) * refactor(sourcing): move sourcing to pop-common * refactor(sourcing-tests): move sourcing tests to pop-common * refactor(sourcing): better imports * refactor(sourcing): clean sourcing module with better component categorization. Other cleanups * fix: failing tests * fix: needed to download contracts_node for test * fix: add contracts feature, more tests need s c n downloaded * fix: manually create temp dir * fix: append pop_tmp folder in tests * refactor: e2e test improvements (#263) * fix: use async sleep in async context * refactor: remove unnecessary module * refactor: standardise sourcing ux * fix: temp dir issue * chore: component locations, minor improvements --------- Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> --- Cargo.lock | 5 + crates/pop-cli/src/commands/mod.rs | 2 +- crates/pop-cli/src/commands/test/contract.rs | 37 +- crates/pop-cli/src/commands/up/contract.rs | 23 +- crates/pop-cli/src/commands/up/parachain.rs | 3 +- crates/pop-cli/src/common/contracts.rs | 41 ++ crates/pop-cli/src/common/mod.rs | 2 + crates/pop-cli/src/main.rs | 1 + crates/pop-cli/tests/contract.rs | 11 +- crates/pop-common/Cargo.toml | 5 + crates/pop-common/src/errors.rs | 10 +- crates/pop-common/src/git.rs | 87 ++- crates/pop-common/src/lib.rs | 59 ++ crates/pop-common/src/sourcing/binary.rs | 486 ++++++++++++++ .../src/sourcing/mod.rs} | 42 +- crates/pop-contracts/Cargo.toml | 13 +- crates/pop-contracts/src/call.rs | 14 +- crates/pop-contracts/src/errors.rs | 3 + crates/pop-contracts/src/lib.rs | 9 +- .../{utils/contracts_node.rs => node/mod.rs} | 143 ++-- crates/pop-contracts/src/up.rs | 14 +- crates/pop-contracts/src/utils/mod.rs | 1 - crates/pop-parachains/src/errors.rs | 10 +- crates/pop-parachains/src/lib.rs | 4 +- crates/pop-parachains/src/up/chain_specs.rs | 21 +- crates/pop-parachains/src/up/mod.rs | 617 +----------------- crates/pop-parachains/src/up/parachains.rs | 10 +- crates/pop-parachains/src/up/relay.rs | 14 +- 28 files changed, 921 insertions(+), 766 deletions(-) create mode 100644 crates/pop-cli/src/common/contracts.rs create mode 100644 crates/pop-cli/src/common/mod.rs create mode 100644 crates/pop-common/src/sourcing/binary.rs rename crates/{pop-parachains/src/up/sourcing.rs => pop-common/src/sourcing/mod.rs} (97%) rename crates/pop-contracts/src/{utils/contracts_node.rs => node/mod.rs} (50%) diff --git a/Cargo.lock b/Cargo.lock index 56099827..39e11e77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4503,6 +4503,8 @@ version = "0.2.0" dependencies = [ "anyhow", "cargo_toml", + "duct", + "flate2", "git2", "git2_credentials", "mockito", @@ -4511,6 +4513,8 @@ dependencies = [ "serde", "serde_json", "strum 0.26.2", + "strum_macros 0.26.4", + "tar", "tempfile", "thiserror", "tokio", @@ -4524,6 +4528,7 @@ dependencies = [ "anyhow", "contract-build", "contract-extrinsics", + "dirs", "duct", "flate2", "heck 0.5.0", diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index 63fc6cc9..21d489fb 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -105,7 +105,7 @@ impl Command { }, #[cfg(feature = "contract")] Self::Test(args) => match args.command { - test::Command::Contract(cmd) => match cmd.execute() { + test::Command::Contract(cmd) => match cmd.execute().await { Ok(feature) => Ok(json!(feature)), Err(e) => Err(e), }, diff --git a/crates/pop-cli/src/commands/test/contract.rs b/crates/pop-cli/src/commands/test/contract.rs index d5f51443..97b67cb5 100644 --- a/crates/pop-cli/src/commands/test/contract.rs +++ b/crates/pop-cli/src/commands/test/contract.rs @@ -1,17 +1,23 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::style::style; +use crate::{common::contracts::check_contracts_node_and_prompt, style::style}; use clap::Args; -use cliclack::{clear_screen, intro, outro}; +use cliclack::{clear_screen, intro, log::warning, outro}; use pop_contracts::{test_e2e_smart_contract, test_smart_contract}; use std::path::PathBuf; +#[cfg(not(test))] +use {std::time::Duration, tokio::time::sleep}; #[derive(Args)] pub(crate) struct TestContractCommand { #[arg(short = 'p', long, help = "Path for the contract project [default: current directory]")] path: Option, - #[arg(short = 'f', long = "features", help = "Features for the contract project", value_parser=["e2e-tests"])] + /// [DEPRECATED] Run e2e tests + #[arg(short = 'f', long = "features", value_parser=["e2e-tests"])] features: Option, + /// Run end-to-end tests + #[arg(short = 'e', long = "e2e")] + e2e: bool, #[arg( short = 'n', long = "node", @@ -22,13 +28,36 @@ pub(crate) struct TestContractCommand { impl TestContractCommand { /// Executes the command. - pub(crate) fn execute(self) -> anyhow::Result<&'static str> { + pub(crate) async fn execute(mut self) -> anyhow::Result<&'static str> { clear_screen()?; + + let mut show_deprecated = false; if self.features.is_some() && self.features.clone().unwrap().contains("e2e-tests") { + show_deprecated = true; + self.e2e = true; + #[cfg(not(test))] + sleep(Duration::from_secs(3)).await; + } + + if self.e2e { intro(format!( "{}: Starting end-to-end tests", style(" Pop CLI ").black().on_magenta() ))?; + + if show_deprecated { + warning("NOTE: --features e2e-tests is deprecated. Use --e2e instead.")?; + } + + let maybe_node_path = check_contracts_node_and_prompt().await?; + if let Some(node_path) = maybe_node_path { + if node_path != PathBuf::new() { + self.node = Some(node_path); + } + } else { + warning("🚫 substrate-contracts-node is necessary to run e2e tests. Will try to run tests anyway...")?; + } + test_e2e_smart_contract(self.path.as_deref(), self.node.as_deref())?; outro("End-to-end testing complete")?; Ok("e2e") diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index bfcb3a57..26162637 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -2,6 +2,7 @@ use crate::{ cli::{traits::Cli as _, Cli}, + common::contracts::check_contracts_node_and_prompt, style::style, }; use clap::Args; @@ -124,10 +125,28 @@ impl UpContractCommand { // Update url to that of the launched node self.url = Url::parse(DEFAULT_URL).expect("default url is valid"); + let log = NamedTempFile::new()?; + + // default to standalone binary, if it exists. + let mut binary_path = PathBuf::from("substrate-contracts-node"); + + // uses the cache location + let maybe_node_path = check_contracts_node_and_prompt().await?; + if let Some(node_path) = maybe_node_path { + if node_path != PathBuf::new() { + binary_path = node_path; + } + } else { + Cli.outro_cancel( + "🚫 You need to specify an accessible endpoint to deploy the contract.", + )?; + return Ok(()); + } + let spinner = spinner(); spinner.start("Starting local node..."); - let log = NamedTempFile::new()?; - let process = run_contracts_node(crate::cache()?, Some(log.as_file())).await?; + + let process = run_contracts_node(binary_path, Some(log.as_file())).await?; let bar = Style::new().magenta().dim().apply_to(Emoji("│", "|")); spinner.stop(format!( "Local node started successfully:{}", diff --git a/crates/pop-cli/src/commands/up/parachain.rs b/crates/pop-cli/src/commands/up/parachain.rs index c91ae0db..a59d8284 100644 --- a/crates/pop-cli/src/commands/up/parachain.rs +++ b/crates/pop-cli/src/commands/up/parachain.rs @@ -8,7 +8,8 @@ use cliclack::{ }; use console::{Emoji, Style, Term}; use duct::cmd; -use pop_parachains::{Error, IndexSet, NetworkNode, Status, Zombienet}; +use pop_common::Status; +use pop_parachains::{Error, IndexSet, NetworkNode, Zombienet}; use std::{path::PathBuf, time::Duration}; use tokio::time::sleep; diff --git a/crates/pop-cli/src/common/contracts.rs b/crates/pop-cli/src/common/contracts.rs new file mode 100644 index 00000000..f0750563 --- /dev/null +++ b/crates/pop-cli/src/common/contracts.rs @@ -0,0 +1,41 @@ +use cliclack::{confirm, log::warning, spinner}; +use pop_contracts::{does_contracts_node_exist, download_contracts_node}; +use std::path::PathBuf; + +/// Helper function to check if the contracts node binary exists, and if not download it. +/// returns: +/// - Some("") if the standalone binary exists +/// - Some(binary_cache_location) if the binary exists in pop's cache +/// - None if the binary does not exist +pub async fn check_contracts_node_and_prompt() -> anyhow::Result> { + let mut node_path = None; + + // if the contracts node binary does not exist, prompt the user to download it + let maybe_contract_node_path = does_contracts_node_exist(crate::cache()?); + if maybe_contract_node_path == None { + warning("⚠️ The substrate-contracts-node binary is not found.")?; + if confirm("📦 Would you like to source it automatically now?") + .initial_value(true) + .interact()? + { + let spinner = spinner(); + spinner.start("📦 Sourcing substrate-contracts-node..."); + + let cache_path = crate::cache()?; + let binary = download_contracts_node(cache_path.clone()).await?; + + spinner.stop(format!( + "✅ substrate-contracts-node successfully sourced. Cached at: {}", + binary.path().to_str().unwrap() + )); + node_path = Some(binary.path()); + } + } else { + if let Some(contract_node_path) = maybe_contract_node_path { + // If the node_path is not empty (cached binary). Otherwise, the standalone binary will be used by cargo-contract. + node_path = Some(contract_node_path.0); + } + } + + Ok(node_path) +} diff --git a/crates/pop-cli/src/common/mod.rs b/crates/pop-cli/src/common/mod.rs new file mode 100644 index 00000000..0cd72aa9 --- /dev/null +++ b/crates/pop-cli/src/common/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "contract")] +pub mod contracts; diff --git a/crates/pop-cli/src/main.rs b/crates/pop-cli/src/main.rs index 73ddcd90..bccefae3 100644 --- a/crates/pop-cli/src/main.rs +++ b/crates/pop-cli/src/main.rs @@ -17,6 +17,7 @@ use { mod cli; #[cfg(any(feature = "parachain", feature = "contract"))] mod commands; +mod common; mod style; #[tokio::main] diff --git a/crates/pop-cli/tests/contract.rs b/crates/pop-cli/tests/contract.rs index f6696f9e..f017d9dc 100644 --- a/crates/pop-cli/tests/contract.rs +++ b/crates/pop-cli/tests/contract.rs @@ -4,10 +4,10 @@ use anyhow::Result; use assert_cmd::Command; use pop_common::templates::Template; use pop_contracts::{ - dry_run_gas_estimate_instantiate, instantiate_smart_contract, run_contracts_node, - set_up_deployment, Contract, UpOpts, + download_contracts_node, dry_run_gas_estimate_instantiate, instantiate_smart_contract, + run_contracts_node, set_up_deployment, Contract, UpOpts, }; -use std::{path::Path, process::Command as Cmd}; +use std::{env::temp_dir, path::Path, process::Command as Cmd}; use strum::VariantArray; use url::Url; @@ -43,8 +43,9 @@ async fn contract_lifecycle() -> Result<()> { assert!(temp_dir.join("test_contract/target/ink/test_contract.json").exists()); // Run the contracts node - let cache = temp_dir.join("cache"); - let process = run_contracts_node(cache, None).await?; + let node_path = download_contracts_node(temp_dir.to_path_buf().clone()).await?; + let process = run_contracts_node(node_path.path(), None).await?; + // Only upload the contract // pop up contract --upload-only Command::cargo_bin("pop") diff --git a/crates/pop-common/Cargo.toml b/crates/pop-common/Cargo.toml index 815b751b..35721ee2 100644 --- a/crates/pop-common/Cargo.toml +++ b/crates/pop-common/Cargo.toml @@ -10,6 +10,8 @@ repository.workspace = true [dependencies] anyhow.workspace = true cargo_toml.workspace = true +duct.workspace = true +flate2.workspace = true git2.workspace = true git2_credentials.workspace = true regex.workspace = true @@ -17,10 +19,13 @@ reqwest.workspace = true serde_json.workspace = true serde.workspace = true strum.workspace = true +tar.workspace = true +tempfile.workspace = true thiserror.workspace = true tokio.workspace = true url.workspace = true [dev-dependencies] mockito.workspace = true +strum_macros.workspace = true tempfile.workspace = true diff --git a/crates/pop-common/src/errors.rs b/crates/pop-common/src/errors.rs index 375b9b1a..02fb0c9b 100644 --- a/crates/pop-common/src/errors.rs +++ b/crates/pop-common/src/errors.rs @@ -1,10 +1,12 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::templates; +use crate::{sourcing, templates}; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { + #[error("Anyhow error: {0}")] + AnyhowError(#[from] anyhow::Error), #[error("Configuration error: {0}")] Config(String), #[error("a git error occurred: {0}")] @@ -17,6 +19,12 @@ pub enum Error { ManifestError(#[from] cargo_toml::Error), #[error("ParseError error: {0}")] ParseError(#[from] url::ParseError), + #[error("SourceError error: {0}")] + SourceError(#[from] sourcing::Error), #[error("TemplateError error: {0}")] TemplateError(#[from] templates::Error), + #[error("Unsupported command: {0}")] + UnsupportedCommand(String), + #[error("Unsupported platform: {arch} {os}")] + UnsupportedPlatform { arch: &'static str, os: &'static str }, } diff --git a/crates/pop-common/src/git.rs b/crates/pop-common/src/git.rs index b8977508..e4b674a4 100644 --- a/crates/pop-common/src/git.rs +++ b/crates/pop-common/src/git.rs @@ -3,7 +3,8 @@ use crate::{errors::Error, APP_USER_AGENT}; use anyhow::Result; use git2::{ - build::RepoBuilder, FetchOptions, IndexAddOption, RemoteCallbacks, Repository, ResetType, + build::RepoBuilder, FetchOptions, IndexAddOption, RemoteCallbacks, Repository as GitRepository, + ResetType, }; use git2_credentials::CredentialHandler; use regex::Regex; @@ -35,7 +36,7 @@ impl Git { Ok(()) } - fn ssh_clone(url: &Url, working_dir: &Path) -> Result { + fn ssh_clone(url: &Url, working_dir: &Path) -> Result { let ssh_url = GitHub::convert_to_ssh_url(url); // Prepare callback and fetch options. let mut fo = FetchOptions::new(); @@ -58,7 +59,7 @@ impl Git { target: &Path, tag_version: Option, ) -> Result> { - let repo = match Repository::clone(url, target) { + let repo = match GitRepository::clone(url, target) { Ok(repo) => repo, Err(_e) => Self::ssh_clone_and_degit( url::Url::parse(url).map_err(|err| Error::from(err))?, @@ -92,7 +93,7 @@ impl Git { } /// For users that have ssh configuration for cloning repositories. - fn ssh_clone_and_degit(url: Url, target: &Path) -> Result { + fn ssh_clone_and_degit(url: Url, target: &Path) -> Result { let ssh_url = GitHub::convert_to_ssh_url(&url); // Prepare callback and fetch options. let mut fo = FetchOptions::new(); @@ -118,7 +119,7 @@ impl Git { } /// Fetch the latest release from a repository - fn fetch_latest_tag(repo: &Repository) -> Option { + fn fetch_latest_tag(repo: &GitRepository) -> Option { let version_reg = Regex::new(r"v\d+\.\d+\.\d+").expect("Valid regex"); let tags = repo.tag_names(None).ok()?; // Start from latest tags @@ -139,7 +140,7 @@ impl Git { /// * `target` - location where the parachain will be created. /// * `message` - message for first commit. pub fn git_init(target: &Path, message: &str) -> Result<(), git2::Error> { - let repo = Repository::init(target)?; + let repo = GitRepository::init(target)?; let signature = repo.signature()?; let mut index = repo.index()?; @@ -278,6 +279,41 @@ pub struct Release { pub commit: Option, } +/// A descriptor of a remote repository. +#[derive(Debug, PartialEq)] +pub struct Repository { + /// The url of the repository. + pub url: Url, + /// If applicable, the branch or tag to be used. + pub reference: Option, + /// The name of a package within the repository. Defaults to the repository name. + pub package: String, +} + +impl Repository { + /// Parses a url in the form of https://github.com/org/repository?package#tag into its component parts. + /// + /// # Arguments + /// * `url` - The url to be parsed. + pub fn parse(url: &str) -> Result { + let url = Url::parse(url)?; + let package = url.query(); + let reference = url.fragment().map(|f| f.to_string()); + + let mut url = url.clone(); + url.set_query(None); + url.set_fragment(None); + + let package = match package { + Some(b) => b, + None => crate::GitHub::name(&url)?, + } + .to_string(); + + Ok(Self { url, reference, package }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -454,4 +490,43 @@ mod tests { "git@github.com:paritytech/frontier-parachain-template.git" ); } + + mod repository { + use super::Error; + use crate::git::Repository; + use url::Url; + + #[test] + fn parsing_full_url_works() { + assert_eq!( + Repository::parse("https://github.com/org/repository?package#tag").unwrap(), + Repository { + url: Url::parse("https://github.com/org/repository").unwrap(), + reference: Some("tag".into()), + package: "package".into(), + } + ); + } + + #[test] + fn parsing_simple_url_works() { + let url = "https://github.com/org/repository"; + assert_eq!( + Repository::parse(url).unwrap(), + Repository { + url: Url::parse(url).unwrap(), + reference: None, + package: "repository".into(), + } + ); + } + + #[test] + fn parsing_invalid_url_returns_error() { + assert!(matches!( + Repository::parse("github.com/org/repository"), + Err(Error::ParseError(..)) + )); + } + } } diff --git a/crates/pop-common/src/lib.rs b/crates/pop-common/src/lib.rs index e8edccc1..f4a08bf7 100644 --- a/crates/pop-common/src/lib.rs +++ b/crates/pop-common/src/lib.rs @@ -3,6 +3,7 @@ pub mod errors; pub mod git; pub mod helpers; pub mod manifest; +pub mod sourcing; pub mod templates; pub use build::Profile; @@ -11,3 +12,61 @@ pub use git::{Git, GitHub, Release}; pub use helpers::{get_project_name_from_path, replace_in_file}; static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + +/// Trait for observing status updates. +pub trait Status { + /// Update the observer with the provided `status`. + fn update(&self, status: &str); +} + +impl Status for () { + // no-op: status updates are ignored + fn update(&self, _: &str) {} +} + +/// Determines the target triple based on the current platform. +pub fn target() -> Result<&'static str, Error> { + use std::env::consts::*; + + if OS == "windows" { + return Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }); + } + + match ARCH { + "aarch64" => { + return match OS { + "macos" => Ok("aarch64-apple-darwin"), + _ => Ok("aarch64-unknown-linux-gnu"), + } + }, + "x86_64" | "x86" => { + return match OS { + "macos" => Ok("x86_64-apple-darwin"), + _ => Ok("x86_64-unknown-linux-gnu"), + } + }, + &_ => {}, + } + Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }) +} + +#[cfg(test)] +mod test { + use super::*; + use anyhow::Result; + + #[test] + fn target_works() -> Result<()> { + use std::{process::Command, str}; + let output = Command::new("rustc").arg("-vV").output()?; + let output = str::from_utf8(&output.stdout)?; + let target_expected = output + .lines() + .find(|l| l.starts_with("host: ")) + .map(|l| &l[6..]) + .unwrap() + .to_string(); + assert_eq!(target()?, target_expected); + Ok(()) + } +} diff --git a/crates/pop-common/src/sourcing/binary.rs b/crates/pop-common/src/sourcing/binary.rs new file mode 100644 index 00000000..705fae63 --- /dev/null +++ b/crates/pop-common/src/sourcing/binary.rs @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::{ + sourcing::{ + from_local_package, Error, GitHub::ReleaseArchive, GitHub::SourceCodeArchive, Source, + Source::Archive, Source::Git, Source::GitHub, + }, + Status, +}; +use std::path::{Path, PathBuf}; + +/// A binary used to launch a node. +#[derive(Debug, PartialEq)] +pub enum Binary { + /// A local binary. + Local { + /// The name of the binary. + name: String, + /// The path of the binary. + path: PathBuf, + /// If applicable, the path to a manifest used to build the binary if missing. + manifest: Option, + }, + /// A binary which needs to be sourced. + Source { + /// The name of the binary. + name: String, + /// The source of the binary. + #[allow(private_interfaces)] + source: Source, + /// The cache to be used to store the binary. + cache: PathBuf, + }, +} + +impl Binary { + /// Whether the binary exists. + pub fn exists(&self) -> bool { + self.path().exists() + } + + /// If applicable, the latest version available. + pub fn latest(&self) -> Option<&str> { + match self { + Self::Local { .. } => None, + Self::Source { source, .. } => { + if let GitHub(ReleaseArchive { latest, .. }) = source { + latest.as_deref() + } else { + None + } + }, + } + } + + /// Whether the binary is defined locally. + pub fn local(&self) -> bool { + matches!(self, Self::Local { .. }) + } + + /// The name of the binary. + pub fn name(&self) -> &str { + match self { + Self::Local { name, .. } => name, + Self::Source { name, .. } => name, + } + } + + /// The path of the binary. + pub fn path(&self) -> PathBuf { + match self { + Self::Local { path, .. } => path.to_path_buf(), + Self::Source { name, source, cache, .. } => { + // Determine whether a specific version is specified + let version = match source { + Git { reference, .. } => reference.as_ref(), + GitHub(source) => match source { + ReleaseArchive { tag, .. } => tag.as_ref(), + SourceCodeArchive { reference, .. } => reference.as_ref(), + }, + Archive { .. } | Source::Url { .. } => None, + }; + version.map_or_else(|| cache.join(name), |v| cache.join(format!("{name}-{v}"))) + }, + } + } + + /// Attempts to resolve a version of a binary based on whether one is specified, an existing version + /// can be found cached locally, or uses the latest version. + /// + /// # Arguments + /// * `name` - The name of the binary. + /// * `specified` - If available, a version explicitly specified. + /// * `available` - The available versions, used to check for those cached locally or the latest otherwise. + /// * `cache` - The location used for caching binaries. + pub fn resolve_version( + name: &str, + specified: Option<&str>, + available: &[impl AsRef], + cache: &Path, + ) -> Option { + match specified { + Some(version) => Some(version.to_string()), + None => available + .iter() + .map(|v| v.as_ref()) + // Default to latest version available locally + .filter_map(|version| { + let path = cache.join(format!("{name}-{version}")); + path.exists().then_some(Some(version.to_string())) + }) + .nth(0) + .unwrap_or( + // Default to latest version + available.get(0).and_then(|version| Some(version.as_ref().to_string())), + ), + } + } + + /// Sources the binary. + /// + /// # Arguments + /// * `release` - Whether any binaries needing to be built should be done so using the release profile. + /// * `status` - Used to observe status updates. + /// * `verbose` - Whether verbose output is required. + pub async fn source( + &self, + release: bool, + status: &impl Status, + verbose: bool, + ) -> Result<(), Error> { + match self { + Self::Local { name, path, manifest, .. } => match manifest { + None => { + return Err(Error::MissingBinary(format!( + "The {path:?} binary cannot be sourced automatically." + ))) + }, + Some(manifest) => { + from_local_package(manifest, name, release, status, verbose).await + }, + }, + Self::Source { source, cache, .. } => { + source.source(cache, release, status, verbose).await + }, + } + } + + /// Whether any locally cached version can be replaced with a newer version. + pub fn stale(&self) -> bool { + // Only binaries sourced from GitHub release archives can currently be determined as stale + let Self::Source { source: GitHub(ReleaseArchive { tag, latest, .. }), .. } = self else { + return false; + }; + latest.as_ref().map_or(false, |l| tag.as_ref() != Some(l)) + } + + /// Specifies that the latest available versions are to be used (where possible). + pub fn use_latest(&mut self) { + if let Self::Source { source: GitHub(ReleaseArchive { tag, latest, .. }), .. } = self { + if let Some(latest) = latest { + *tag = Some(latest.clone()) + } + }; + } + + /// If applicable, the version of the binary. + pub fn version(&self) -> Option<&str> { + match self { + Self::Local { .. } => None, + Self::Source { source, .. } => match source { + Git { reference, .. } => reference.as_ref(), + GitHub(source) => match source { + ReleaseArchive { tag, .. } => tag.as_ref(), + SourceCodeArchive { reference, .. } => reference.as_ref(), + }, + Archive { .. } | Source::Url { .. } => None, + }, + } + .map(|r| r.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{sourcing::tests::Output, target}; + use anyhow::Result; + use duct::cmd; + use std::fs::{create_dir_all, File}; + use tempfile::tempdir; + use url::Url; + + #[test] + fn local_binary_works() -> Result<()> { + let name = "polkadot"; + let temp_dir = tempdir()?; + let path = temp_dir.path().join(name); + File::create(&path)?; + + let binary = Binary::Local { name: name.to_string(), path: path.clone(), manifest: None }; + + assert!(binary.exists()); + assert_eq!(binary.latest(), None); + assert!(binary.local()); + assert_eq!(binary.name(), name); + assert_eq!(binary.path(), path); + assert!(!binary.stale()); + assert_eq!(binary.version(), None); + Ok(()) + } + + #[test] + fn local_package_works() -> Result<()> { + let name = "polkadot"; + let temp_dir = tempdir()?; + let path = temp_dir.path().join("target/release").join(name); + create_dir_all(&path.parent().unwrap())?; + File::create(&path)?; + let manifest = Some(temp_dir.path().join("Cargo.toml")); + + let binary = Binary::Local { name: name.to_string(), path: path.clone(), manifest }; + + assert!(binary.exists()); + assert_eq!(binary.latest(), None); + assert!(binary.local()); + assert_eq!(binary.name(), name); + assert_eq!(binary.path(), path); + assert!(!binary.stale()); + assert_eq!(binary.version(), None); + Ok(()) + } + + #[test] + fn resolve_version_works() -> Result<()> { + let name = "polkadot"; + let temp_dir = tempdir()?; + + let available = vec!["v1.13.0", "v1.12.0", "v1.11.0"]; + + // Specified + let specified = Some("v1.12.0"); + assert_eq!( + Binary::resolve_version(name, specified, &available, temp_dir.path()).unwrap(), + specified.unwrap() + ); + // Latest + assert_eq!( + Binary::resolve_version(name, None, &available, temp_dir.path()).unwrap(), + available[0] + ); + // Cached + File::create(temp_dir.path().join(format!("{name}-{}", available[1])))?; + assert_eq!( + Binary::resolve_version(name, None, &available, temp_dir.path()).unwrap(), + available[1] + ); + Ok(()) + } + + #[test] + fn sourced_from_archive_works() -> Result<()> { + let name = "polkadot"; + let url = "https://github.com/r0gue-io/polkadot/releases/latest/download/polkadot-aarch64-apple-darwin.tar.gz".to_string(); + let contents = vec![ + name.to_string(), + "polkadot-execute-worker".into(), + "polkadot-prepare-worker".into(), + ]; + let temp_dir = tempdir()?; + let path = temp_dir.path().join(name); + File::create(&path)?; + + let mut binary = Binary::Source { + name: name.to_string(), + source: Archive { url: url.to_string(), contents }, + cache: temp_dir.path().to_path_buf(), + }; + + assert!(binary.exists()); + assert_eq!(binary.latest(), None); + assert!(!binary.local()); + assert_eq!(binary.name(), name); + assert_eq!(binary.path(), path); + assert!(!binary.stale()); + assert_eq!(binary.version(), None); + binary.use_latest(); + assert_eq!(binary.version(), None); + Ok(()) + } + + #[test] + fn sourced_from_git_works() -> Result<()> { + let package = "hello_world"; + let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?; + let temp_dir = tempdir()?; + for reference in [None, Some("436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3".to_string())] { + let path = temp_dir.path().join( + reference + .as_ref() + .map_or(package.into(), |reference| format!("{package}-{reference}")), + ); + File::create(&path)?; + + let mut binary = Binary::Source { + name: package.to_string(), + source: Git { + url: url.clone(), + reference: reference.clone(), + manifest: None, + package: package.to_string(), + artifacts: vec![package.to_string()], + }, + cache: temp_dir.path().to_path_buf(), + }; + + assert!(binary.exists()); + assert_eq!(binary.latest(), None); + assert!(!binary.local()); + assert_eq!(binary.name(), package); + assert_eq!(binary.path(), path); + assert!(!binary.stale()); + assert_eq!(binary.version(), reference.as_ref().map(|r| r.as_str())); + binary.use_latest(); + assert_eq!(binary.version(), reference.as_ref().map(|r| r.as_str())); + } + + Ok(()) + } + + #[test] + fn sourced_from_github_release_archive_works() -> Result<()> { + let owner = "r0gue-io"; + let repository = "polkadot"; + let tag_format = "polkadot-{tag}"; + let name = "polkadot"; + let archive = format!("{name}-{}.tar.gz", target()?); + let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"]; + let temp_dir = tempdir()?; + for tag in [None, Some("v1.12.0".to_string())] { + let path = temp_dir + .path() + .join(tag.as_ref().map_or(name.to_string(), |t| format!("{name}-{t}"))); + File::create(&path)?; + for latest in [None, Some("v2.0.0".to_string())] { + let mut binary = Binary::Source { + name: name.to_string(), + source: GitHub(ReleaseArchive { + owner: owner.into(), + repository: repository.into(), + tag: tag.clone(), + tag_format: Some(tag_format.to_string()), + archive: archive.clone(), + contents: contents.into_iter().map(|b| (b, None)).collect(), + latest: latest.clone(), + }), + cache: temp_dir.path().to_path_buf(), + }; + + assert!(binary.exists()); + assert_eq!(binary.latest(), latest.as_ref().map(|l| l.as_str())); + assert!(!binary.local()); + assert_eq!(binary.name(), name); + assert_eq!(binary.path(), path); + assert_eq!(binary.stale(), latest.is_some()); + assert_eq!(binary.version(), tag.as_ref().map(|t| t.as_str())); + binary.use_latest(); + if latest.is_some() { + assert_eq!(binary.version(), latest.as_ref().map(|l| l.as_str())); + } + } + } + Ok(()) + } + + #[test] + fn sourced_from_github_source_code_archive_works() -> Result<()> { + let owner = "paritytech"; + let repository = "polkadot-sdk"; + let package = "polkadot"; + let manifest = "substrate/Cargo.toml"; + let temp_dir = tempdir()?; + for reference in [None, Some("72dba98250a6267c61772cd55f8caf193141050f".to_string())] { + let path = temp_dir + .path() + .join(reference.as_ref().map_or(package.to_string(), |t| format!("{package}-{t}"))); + File::create(&path)?; + let mut binary = Binary::Source { + name: package.to_string(), + source: GitHub(SourceCodeArchive { + owner: owner.to_string(), + repository: repository.to_string(), + reference: reference.clone(), + manifest: Some(PathBuf::from(manifest)), + package: package.to_string(), + artifacts: vec![package.to_string()], + }), + cache: temp_dir.path().to_path_buf(), + }; + + assert!(binary.exists()); + assert_eq!(binary.latest(), None); + assert!(!binary.local()); + assert_eq!(binary.name(), package); + assert_eq!(binary.path(), path); + assert_eq!(binary.stale(), false); + assert_eq!(binary.version(), reference.as_ref().map(|r| r.as_str())); + binary.use_latest(); + assert_eq!(binary.version(), reference.as_ref().map(|l| l.as_str())); + } + Ok(()) + } + + #[test] + fn sourced_from_url_works() -> Result<()> { + let name = "polkadot"; + let url = + "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc"; + let temp_dir = tempdir()?; + let path = temp_dir.path().join(name); + File::create(&path)?; + + let mut binary = Binary::Source { + name: name.to_string(), + source: Source::Url { url: url.to_string(), name: name.to_string() }, + cache: temp_dir.path().to_path_buf(), + }; + + assert!(binary.exists()); + assert_eq!(binary.latest(), None); + assert!(!binary.local()); + assert_eq!(binary.name(), name); + assert_eq!(binary.path(), path); + assert!(!binary.stale()); + assert_eq!(binary.version(), None); + binary.use_latest(); + assert_eq!(binary.version(), None); + Ok(()) + } + + #[tokio::test] + async fn sourcing_from_local_binary_not_supported() -> Result<()> { + let name = "polkadot".to_string(); + let temp_dir = tempdir()?; + let path = temp_dir.path().join(&name); + assert!(matches!( + Binary::Local { name, path: path.clone(), manifest: None }.source(true, &Output, true).await, + Err(Error::MissingBinary(error)) if error == format!("The {path:?} binary cannot be sourced automatically.") + )); + Ok(()) + } + + #[tokio::test] + async fn sourcing_from_local_package_works() -> Result<()> { + let temp_dir = tempdir()?; + let name = "hello_world"; + cmd("cargo", ["new", name, "--bin"]).dir(temp_dir.path()).run()?; + let path = temp_dir.path().join(name); + let manifest = Some(path.join("Cargo.toml")); + let path = path.join("target/release").join(name); + Binary::Local { name: name.to_string(), path: path.clone(), manifest } + .source(true, &Output, true) + .await?; + assert!(path.exists()); + Ok(()) + } + + #[tokio::test] + async fn sourcing_from_url_works() -> Result<()> { + let name = "polkadot"; + let url = + "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc"; + let temp_dir = tempdir()?; + let path = temp_dir.path().join(name); + + Binary::Source { + name: name.to_string(), + source: Source::Url { url: url.to_string(), name: name.to_string() }, + cache: temp_dir.path().to_path_buf(), + } + .source(true, &Output, true) + .await?; + assert!(path.exists()); + Ok(()) + } +} diff --git a/crates/pop-parachains/src/up/sourcing.rs b/crates/pop-common/src/sourcing/mod.rs similarity index 97% rename from crates/pop-parachains/src/up/sourcing.rs rename to crates/pop-common/src/sourcing/mod.rs index 426871b7..ea925ef4 100644 --- a/crates/pop-parachains/src/up/sourcing.rs +++ b/crates/pop-common/src/sourcing/mod.rs @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::{Error, Status, APP_USER_AGENT}; +mod binary; +pub use binary::*; + +use crate::{Git, Status, APP_USER_AGENT}; use duct::cmd; use flate2::read::GzDecoder; -use pop_common::Git; use reqwest::StatusCode; use std::time::Duration; use std::{ @@ -14,11 +16,28 @@ use std::{ }; use tar::Archive; use tempfile::{tempdir, tempfile}; +use thiserror::Error; use url::Url; +#[derive(Error, Debug)] +pub enum Error { + #[error("Anyhow error: {0}")] + AnyhowError(#[from] anyhow::Error), + #[error("Archive error: {0}")] + ArchiveError(String), + #[error("HTTP error: {0}")] + HttpError(#[from] reqwest::Error), + #[error("IO error: {0}")] + IO(#[from] std::io::Error), + #[error("Missing binary: {0}")] + MissingBinary(String), + #[error("ParseError error: {0}")] + ParseError(#[from] url::ParseError), +} + /// The source of a binary. #[derive(Clone, Debug, PartialEq)] -pub(crate) enum Source { +pub enum Source { /// An archive for download. #[allow(dead_code)] Archive { @@ -103,7 +122,7 @@ impl Source { /// A binary sourced from GitHub. #[derive(Clone, Debug, PartialEq)] -pub(crate) enum GitHub { +pub enum GitHub { /// An archive for download from a GitHub release. ReleaseArchive { /// The owner of the repository - i.e. https://github.com/{owner}/repository. @@ -481,7 +500,8 @@ async fn download(url: &str, dest: &Path) -> Result<(), Error> { #[cfg(test)] pub(super) mod tests { - use super::{super::target, GitHub::*, *}; + use super::{GitHub::*, Status, *}; + use crate::target; use tempfile::tempdir; #[tokio::test] @@ -805,13 +825,12 @@ pub(super) mod tests { } } -pub(crate) mod traits { - use crate::Error; - use pop_common::GitHub; +pub mod traits { + use crate::{sourcing::Error, GitHub}; use strum::EnumProperty; /// The source of a binary. - pub(crate) trait Source: EnumProperty { + pub trait Source: EnumProperty { /// The name of the binary. fn binary(&self) -> &'static str { self.get_str("Binary").expect("expected specification of `Binary` name") @@ -830,6 +849,7 @@ pub(crate) mod traits { } /// Determine the available releases from the source. + #[allow(async_fn_in_trait)] async fn releases(&self) -> Result, Error> { let repo = GitHub::parse(self.repository())?; let releases = match repo.releases().await { @@ -868,7 +888,7 @@ pub(crate) mod traits { } /// An attempted conversion into a Source. - pub(crate) trait TryInto { + pub trait TryInto { /// Attempt the conversion. /// /// # Arguments @@ -878,7 +898,7 @@ pub(crate) mod traits { &self, specifier: Option, latest: Option, - ) -> Result; + ) -> Result; } #[cfg(test)] diff --git a/crates/pop-contracts/Cargo.toml b/crates/pop-contracts/Cargo.toml index 91f85dfc..ddef53d3 100644 --- a/crates/pop-contracts/Cargo.toml +++ b/crates/pop-contracts/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "pop-contracts" description = "Library for generating, building, deploying, and calling ink! smart contracts." -version = "0.2.0" -license = "Apache-2.0" documentation = "https://docs.rs/pop-contracts/latest/pop_contracts" edition.workspace = true +license = "Apache-2.0" +name = "pop-contracts" readme = "README.md" repository.workspace = true +version = "0.2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -21,14 +21,14 @@ thiserror.workspace = true tokio.workspace = true url.workspace = true +heck.workspace = true ink_env.workspace = true sp-core.workspace = true sp-weights.workspace = true -subxt-signer.workspace = true -subxt.workspace = true strum.workspace = true strum_macros.workspace = true -heck.workspace = true +subxt-signer.workspace = true +subxt.workspace = true # cargo-contracts contract-build.workspace = true @@ -38,5 +38,6 @@ contract-extrinsics.workspace = true pop-common = { path = "../pop-common", version = "0.2.0" } [dev-dependencies] +dirs.workspace = true mockito.workspace = true tokio-test.workspace = true diff --git a/crates/pop-contracts/src/call.rs b/crates/pop-contracts/src/call.rs index 4a9f566a..615f2269 100644 --- a/crates/pop-contracts/src/call.rs +++ b/crates/pop-contracts/src/call.rs @@ -162,8 +162,9 @@ pub async fn call_smart_contract( mod tests { use super::*; use crate::{ - create_smart_contract, dry_run_gas_estimate_instantiate, errors::Error, - instantiate_smart_contract, run_contracts_node, set_up_deployment, Contract, UpOpts, + create_smart_contract, download_contracts_node, dry_run_gas_estimate_instantiate, + errors::Error, instantiate_smart_contract, run_contracts_node, set_up_deployment, Contract, + UpOpts, }; use anyhow::Result; use sp_core::Bytes; @@ -309,9 +310,11 @@ mod tests { const LOCALHOST_URL: &str = "ws://127.0.0.1:9944"; let temp_dir = generate_smart_contract_test_environment()?; mock_build_process(temp_dir.path().join("testing"))?; - // Run the contracts-node. - let cache = temp_dir.path().join("cache"); - let process = run_contracts_node(cache, None).await?; + + let cache = temp_dir.path().join(""); + + let node_path = download_contracts_node(cache.clone()).await?; + let process = run_contracts_node(node_path.path(), None).await?; // Instantiate a Smart Contract. let instantiate_exec = set_up_deployment(UpOpts { path: Some(temp_dir.path().join("testing")), @@ -369,6 +372,7 @@ mod tests { .args(["-s", "TERM", &process.id().to_string()]) .spawn()? .wait()?; + Ok(()) } } diff --git a/crates/pop-contracts/src/errors.rs b/crates/pop-contracts/src/errors.rs index 137e0f4c..29defc84 100644 --- a/crates/pop-contracts/src/errors.rs +++ b/crates/pop-contracts/src/errors.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 +use pop_common::sourcing::Error as SourcingError; use thiserror::Error; #[derive(Error, Debug)] @@ -48,4 +49,6 @@ pub enum Error { UnsupportedPlatform { os: &'static str }, #[error("{0}")] UploadContractError(String), + #[error("Sourcing error {0}")] + SourcingError(SourcingError), } diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index 3b38309f..4b701581 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -5,6 +5,7 @@ mod build; mod call; mod errors; mod new; +mod node; mod templates; mod test; mod up; @@ -15,13 +16,13 @@ pub use call::{ call_smart_contract, dry_run_call, dry_run_gas_estimate_call, set_up_call, CallOpts, }; pub use new::{create_smart_contract, is_valid_contract_name}; +pub use node::{ + does_contracts_node_exist, download_contracts_node, is_chain_alive, run_contracts_node, +}; pub use templates::{Contract, ContractType}; pub use test::{test_e2e_smart_contract, test_smart_contract}; pub use up::{ dry_run_gas_estimate_instantiate, dry_run_upload, instantiate_smart_contract, set_up_deployment, set_up_upload, upload_smart_contract, UpOpts, }; -pub use utils::{ - contracts_node::{is_chain_alive, run_contracts_node}, - signer::parse_hex_bytes, -}; +pub use utils::signer::parse_hex_bytes; diff --git a/crates/pop-contracts/src/utils/contracts_node.rs b/crates/pop-contracts/src/node/mod.rs similarity index 50% rename from crates/pop-contracts/src/utils/contracts_node.rs rename to crates/pop-contracts/src/node/mod.rs index 5b83b421..4ce4fb96 100644 --- a/crates/pop-contracts/src/utils/contracts_node.rs +++ b/crates/pop-contracts/src/node/mod.rs @@ -1,22 +1,19 @@ use crate::errors::Error; use contract_extrinsics::{RawParams, RpcRequest}; -use flate2::read::GzDecoder; -use pop_common::GitHub; +use duct::cmd; +use pop_common::sourcing::{ + Binary, {GitHub as GitHubSource, Source}, +}; use std::{ env::consts::OS, - fs::{self, File}, - io::{Seek, SeekFrom, Write}, + fs::File, path::PathBuf, process::{Child, Command, Stdio}, time::Duration, }; -use tar::Archive; -use tempfile::tempfile; use tokio::time::sleep; -const SUBSTRATE_CONTRACT_NODE: &str = "https://github.com/paritytech/substrate-contracts-node"; const BIN_NAME: &str = "substrate-contracts-node"; -const STABLE_VERSION: &str = "v0.41.0"; /// Checks if the specified node is alive and responsive. /// @@ -39,36 +36,66 @@ pub async fn is_chain_alive(url: url::Url) -> Result { } } +/// Checks if the `substrate-contracts-node` binary exists +/// or if the binary exists in pop's cache. +/// returns: +/// - Some("", ) if the standalone binary exists +/// - Some(binary_cache_location, "") if the binary exists in pop's cache +/// - None if the binary does not exist +pub fn does_contracts_node_exist(cache: PathBuf) -> Option<(PathBuf, String)> { + let cached_location = cache.join(BIN_NAME); + let standalone_output = cmd(BIN_NAME, vec!["--version"]).read(); + + if standalone_output.is_ok() { + Some((PathBuf::new(), standalone_output.unwrap())) + } else if cached_location.exists() { + Some((cached_location, "".to_string())) + } else { + None + } +} + +/// Downloads the latest version of the `substrate-contracts-node` binary +/// into the specified cache location. +pub async fn download_contracts_node(cache: PathBuf) -> Result { + let archive = archive_name_by_target()?; + let archive_bin_path = release_folder_by_target()?; + + let source = Source::GitHub(GitHubSource::ReleaseArchive { + owner: "paritytech".into(), + repository: "substrate-contracts-node".into(), + tag: None, + tag_format: None, + archive, + contents: vec![(archive_bin_path, Some(BIN_NAME.to_string()))], + latest: None, + }); + + let contracts_node = + Binary::Source { name: "substrate-contracts-node".into(), source, cache: cache.clone() }; + + // source the substrate-contracts-node binary + contracts_node + .source(false, &(), true) + .await + .map_err(|err| Error::SourcingError(err))?; + + Ok(contracts_node) +} + /// Runs the latest version of the `substrate-contracts-node` in the background. /// /// # Arguments /// -/// * `cache` - The path where the binary will be stored. +/// * `binary_path` - The path where the binary is stored. Can be the binary name itself if in PATH. /// * `output` - The optional log file for node output. /// -pub async fn run_contracts_node(cache: PathBuf, output: Option<&File>) -> Result { - let cached_file = cache.join(BIN_NAME); - if !cached_file.exists() { - let archive = archive_name_by_target()?; - - let latest_version = latest_contract_node_release().await?; - let releases_url = - format!("{SUBSTRATE_CONTRACT_NODE}/releases/download/{latest_version}/{archive}"); - // Download archive - let response = reqwest::get(releases_url.as_str()).await?.error_for_status()?; - let mut file = tempfile()?; - file.write_all(&response.bytes().await?)?; - file.seek(SeekFrom::Start(0))?; - // Extract contents - let tar = GzDecoder::new(file); - let mut archive = Archive::new(tar); - archive.unpack(cache.clone())?; - // Copy the file into the cache folder and remove the folder artifacts - let extracted_dir = cache.join(release_folder_by_target()?); - fs::copy(&extracted_dir.join(BIN_NAME), &cached_file)?; - fs::remove_dir_all(&extracted_dir.parent().unwrap_or(&cache.join("artifacts")))?; - } - let mut command = Command::new(cached_file.display().to_string().as_str()); +pub async fn run_contracts_node( + binary_path: PathBuf, + output: Option<&File>, +) -> Result { + let mut command = Command::new(binary_path); + if let Some(output) = output { command.stdout(Stdio::from(output.try_clone()?)); command.stderr(Stdio::from(output.try_clone()?)); @@ -81,24 +108,6 @@ pub async fn run_contracts_node(cache: PathBuf, output: Option<&File>) -> Result Ok(process) } -async fn latest_contract_node_release() -> Result { - let repo = GitHub::parse(SUBSTRATE_CONTRACT_NODE)?; - match repo.releases().await { - Ok(releases) => { - // Fetching latest releases - for release in releases { - if !release.prerelease { - return Ok(release.tag_name); - } - } - // It should never reach this point, but in case we download a default version of polkadot - Ok(STABLE_VERSION.to_string()) - }, - // If an error with GitHub API return the STABLE_VERSION - Err(_) => Ok(STABLE_VERSION.to_string()), - } -} - fn archive_name_by_target() -> Result { match OS { "macos" => Ok(format!("{}-mac-universal.tar.gz", BIN_NAME)), @@ -106,10 +115,11 @@ fn archive_name_by_target() -> Result { _ => Err(Error::UnsupportedPlatform { os: OS }), } } + fn release_folder_by_target() -> Result<&'static str, Error> { match OS { - "macos" => Ok("artifacts/substrate-contracts-node-mac"), - "linux" => Ok("artifacts/substrate-contracts-node-linux"), + "macos" => Ok("artifacts/substrate-contracts-node-mac/substrate-contracts-node"), + "linux" => Ok("artifacts/substrate-contracts-node-linux/substrate-contracts-node"), _ => Err(Error::UnsupportedPlatform { os: OS }), } } @@ -120,25 +130,6 @@ mod tests { use anyhow::{Error, Result}; use std::process::Command; - #[tokio::test] - async fn test_latest_polkadot_release() -> Result<()> { - let version = latest_contract_node_release().await?; - // Result will change all the time to the current version, check at least starts with v - assert!(version.starts_with("v")); - Ok(()) - } - #[tokio::test] - async fn release_folder_by_target_works() -> Result<()> { - let path = release_folder_by_target(); - if cfg!(target_os = "macos") { - assert_eq!(path?, "artifacts/substrate-contracts-node-mac"); - } else if cfg!(target_os = "linux") { - assert_eq!(path?, "artifacts/substrate-contracts-node-linux"); - } else { - assert!(path.is_err()) - } - Ok(()) - } #[tokio::test] async fn folder_path_by_target() -> Result<()> { let archive = archive_name_by_target(); @@ -164,10 +155,13 @@ mod tests { #[tokio::test] async fn run_contracts_node_works() -> Result<(), Error> { let local_url = url::Url::parse("ws://localhost:9944")?; - // Run the contracts node + let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); - let cache = temp_dir.path().join("cache"); - let process = run_contracts_node(cache.clone(), None).await?; + let cache = temp_dir.path().join(""); + + let node_path = download_contracts_node(cache.clone()).await?; + let process = run_contracts_node(node_path.path(), None).await?; + // Check if the node is alive assert!(is_chain_alive(local_url).await?); assert!(cache.join("substrate-contracts-node").exists()); @@ -177,6 +171,7 @@ mod tests { .args(["-s", "TERM", &process.id().to_string()]) .spawn()? .wait()?; + Ok(()) } } diff --git a/crates/pop-contracts/src/up.rs b/crates/pop-contracts/src/up.rs index ac7f7f48..e21d61a0 100644 --- a/crates/pop-contracts/src/up.rs +++ b/crates/pop-contracts/src/up.rs @@ -199,7 +199,10 @@ pub async fn upload_smart_contract( #[cfg(test)] mod tests { use super::*; - use crate::{create_smart_contract, errors::Error, run_contracts_node, templates::Contract}; + use crate::{ + create_smart_contract, download_contracts_node, errors::Error, run_contracts_node, + templates::Contract, + }; use anyhow::Result; use std::{env, fs, process::Command}; use url::Url; @@ -335,9 +338,11 @@ mod tests { const LOCALHOST_URL: &str = "ws://127.0.0.1:9944"; let temp_dir = generate_smart_contract_test_environment()?; mock_build_process(temp_dir.path().join("testing"))?; - // Run contracts-node - let cache = temp_dir.path().join("cache"); - let process = run_contracts_node(cache, None).await?; + + let cache = temp_dir.path().join(""); + + let node_path = download_contracts_node(cache.clone()).await?; + let process = run_contracts_node(node_path.path(), None).await?; let upload_exec = set_up_upload(UpOpts { path: Some(temp_dir.path().join("testing")), @@ -386,6 +391,7 @@ mod tests { .args(["-s", "TERM", &process.id().to_string()]) .spawn()? .wait()?; + Ok(()) } } diff --git a/crates/pop-contracts/src/utils/mod.rs b/crates/pop-contracts/src/utils/mod.rs index 9fc10518..357c6608 100644 --- a/crates/pop-contracts/src/utils/mod.rs +++ b/crates/pop-contracts/src/utils/mod.rs @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-3.0 -pub mod contracts_node; pub mod helpers; pub mod signer; diff --git a/crates/pop-parachains/src/errors.rs b/crates/pop-parachains/src/errors.rs index 04356429..1d6ff960 100644 --- a/crates/pop-parachains/src/errors.rs +++ b/crates/pop-parachains/src/errors.rs @@ -9,8 +9,6 @@ pub enum Error { Aborted, #[error("Anyhow error: {0}")] AnyhowError(#[from] anyhow::Error), - #[error("Archive error: {0}")] - ArchiveError(String), #[error("{0}")] CommonError(#[from] pop_common::Error), #[error("Configuration error: {0}")] @@ -19,8 +17,6 @@ pub enum Error { CurrentDirAccess, #[error("Failed to parse the endowment value")] EndowmentError, - #[error("HTTP error: {0}")] - HttpError(#[from] reqwest::Error), #[error("IO error: {0}")] IO(#[from] std::io::Error), #[error("Missing binary: {0}")] @@ -33,18 +29,14 @@ pub enum Error { OrchestratorError(#[from] OrchestratorError), #[error("Failed to create pallet directory")] PalletDirCreation, - #[error("ParseError error: {0}")] - ParseError(#[from] url::ParseError), #[error("Failed to execute rustfmt")] RustfmtError(std::io::Error), #[error("Template error: {0}")] - TemplateError(#[from] pop_common::templates::Error), + SourcingError(#[from] pop_common::sourcing::Error), #[error("Toml error: {0}")] TomlError(#[from] toml_edit::de::Error), #[error("Unsupported command: {0}")] UnsupportedCommand(String), #[error("Failed to locate the workspace")] WorkspaceLocate, - #[error("Unsupported platform: {arch} {os}")] - UnsupportedPlatform { arch: &'static str, os: &'static str }, } diff --git a/crates/pop-parachains/src/lib.rs b/crates/pop-parachains/src/lib.rs index a6cfe259..9874dabb 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -19,10 +19,8 @@ pub use indexmap::IndexSet; pub use new_pallet::{create_pallet_template, TemplatePalletConfig}; pub use new_parachain::instantiate_template_dir; pub use templates::{Config, Parachain, Provider}; -pub use up::{Binary, Status, Zombienet}; +pub use up::Zombienet; pub use utils::helpers::is_initial_endowment_valid; pub use utils::pallet_helpers::resolve_pallet_path; /// Information about the Node. External export from Zombienet-SDK. pub use zombienet_sdk::NetworkNode; - -static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); diff --git a/crates/pop-parachains/src/up/chain_specs.rs b/crates/pop-parachains/src/up/chain_specs.rs index 0b62dd8e..c5a90349 100644 --- a/crates/pop-parachains/src/up/chain_specs.rs +++ b/crates/pop-parachains/src/up/chain_specs.rs @@ -1,18 +1,15 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::{ - up::{ - sourcing::{ - self, - traits::{Source as _, *}, - GitHub::*, - Source, - }, - target, +pub use pop_common::{ + git::GitHub, + sourcing::{ + traits::{Source as _, *}, + Binary, + GitHub::*, + Source, }, - Binary, Error, + target, Error, }; -use pop_common::GitHub; use std::path::Path; use strum::{EnumProperty as _, VariantArray as _}; use strum_macros::{AsRefStr, EnumProperty, VariantArray}; @@ -85,7 +82,7 @@ impl Runtime { } } -impl sourcing::traits::Source for Runtime {} +impl pop_common::sourcing::traits::Source for Runtime {} pub(super) async fn chain_spec_generator( chain: &str, diff --git a/crates/pop-parachains/src/up/mod.rs b/crates/pop-parachains/src/up/mod.rs index b08d247c..be1e618c 100644 --- a/crates/pop-parachains/src/up/mod.rs +++ b/crates/pop-parachains/src/up/mod.rs @@ -3,8 +3,10 @@ use crate::errors::Error; use glob::glob; use indexmap::IndexMap; -use pop_common::git::GitHub; -use sourcing::{GitHub::*, Source, Source::*}; +pub use pop_common::{ + git::{GitHub, Repository}, + sourcing::{Binary, GitHub::*, Source, Source::*}, +}; use std::{ fmt::Debug, fs::write, @@ -14,14 +16,12 @@ use std::{ use symlink::{remove_symlink_file, symlink_file}; use tempfile::{Builder, NamedTempFile}; use toml_edit::{value, ArrayOfTables, DocumentMut, Formatted, Item, Table, Value}; -use url::Url; use zombienet_sdk::{Network, NetworkConfig, NetworkConfigExt}; use zombienet_support::fs::local::LocalFileSystem; mod chain_specs; mod parachains; mod relay; -mod sourcing; /// Configuration to launch a local network. pub struct Zombienet { @@ -579,224 +579,6 @@ impl Parachain { } } -/// A binary used to launch a node. -#[derive(Debug, PartialEq)] -pub enum Binary { - /// A local binary. - Local { - /// The name of the binary. - name: String, - /// The path of the binary. - path: PathBuf, - /// If applicable, the path to a manifest used to build the binary if missing. - manifest: Option, - }, - /// A binary which needs to be sourced. - Source { - /// The name of the binary. - name: String, - /// The source of the binary. - #[allow(private_interfaces)] - source: Source, - /// The cache to be used to store the binary. - cache: PathBuf, - }, -} - -impl Binary { - /// Whether the binary exists. - pub fn exists(&self) -> bool { - self.path().exists() - } - - /// If applicable, the latest version available. - pub fn latest(&self) -> Option<&str> { - match self { - Self::Local { .. } => None, - Self::Source { source, .. } => { - if let GitHub(ReleaseArchive { latest, .. }) = source { - latest.as_deref() - } else { - None - } - }, - } - } - - /// Whether the binary is defined locally. - pub fn local(&self) -> bool { - matches!(self, Self::Local { .. }) - } - - /// The name of the binary. - pub fn name(&self) -> &str { - match self { - Self::Local { name, .. } => name, - Self::Source { name, .. } => name, - } - } - - /// The path of the binary. - pub fn path(&self) -> PathBuf { - match self { - Self::Local { path, .. } => path.to_path_buf(), - Self::Source { name, source, cache, .. } => { - // Determine whether a specific version is specified - let version = match source { - Git { reference, .. } => reference.as_ref(), - GitHub(source) => match source { - ReleaseArchive { tag, .. } => tag.as_ref(), - SourceCodeArchive { reference, .. } => reference.as_ref(), - }, - Archive { .. } | Source::Url { .. } => None, - }; - version.map_or_else(|| cache.join(name), |v| cache.join(format!("{name}-{v}"))) - }, - } - } - - /// Attempts to resolve a version of a binary based on whether one is specified, an existing version - /// can be found cached locally, or uses the latest version. - /// - /// # Arguments - /// * `name` - The name of the binary. - /// * `specified` - If available, a version explicitly specified. - /// * `available` - The available versions, used to check for those cached locally or the latest otherwise. - /// * `cache` - The location used for caching binaries. - fn resolve_version( - name: &str, - specified: Option<&str>, - available: &[impl AsRef], - cache: &Path, - ) -> Option { - match specified { - Some(version) => Some(version.to_string()), - None => available - .iter() - .map(|v| v.as_ref()) - // Default to latest version available locally - .filter_map(|version| { - let path = cache.join(format!("{name}-{version}")); - path.exists().then_some(Some(version.to_string())) - }) - .nth(0) - .unwrap_or( - // Default to latest version - available.get(0).and_then(|version| Some(version.as_ref().to_string())), - ), - } - } - - /// Sources the binary. - /// - /// # Arguments - /// * `release` - Whether any binaries needing to be built should be done so using the release profile. - /// * `status` - Used to observe status updates. - /// * `verbose` - Whether verbose output is required. - pub async fn source( - &self, - release: bool, - status: &impl Status, - verbose: bool, - ) -> Result<(), Error> { - match self { - Self::Local { name, path, manifest, .. } => match manifest { - None => { - return Err(Error::MissingBinary(format!( - "The {path:?} binary cannot be sourced automatically." - ))) - }, - Some(manifest) => { - sourcing::from_local_package(manifest, name, release, status, verbose).await - }, - }, - Self::Source { source, cache, .. } => { - source.source(cache, release, status, verbose).await - }, - } - } - - /// Whether any locally cached version can be replaced with a newer version. - pub fn stale(&self) -> bool { - // Only binaries sourced from GitHub release archives can currently be determined as stale - let Self::Source { source: GitHub(ReleaseArchive { tag, latest, .. }), .. } = self else { - return false; - }; - latest.as_ref().map_or(false, |l| tag.as_ref() != Some(l)) - } - - /// Specifies that the latest available versions are to be used (where possible). - pub fn use_latest(&mut self) { - if let Self::Source { source: GitHub(ReleaseArchive { tag, latest, .. }), .. } = self { - if let Some(latest) = latest { - *tag = Some(latest.clone()) - } - }; - } - - /// If applicable, the version of the binary. - pub fn version(&self) -> Option<&str> { - match self { - Self::Local { .. } => None, - Self::Source { source, .. } => match source { - Git { reference, .. } => reference.as_ref(), - GitHub(source) => match source { - ReleaseArchive { tag, .. } => tag.as_ref(), - SourceCodeArchive { reference, .. } => reference.as_ref(), - }, - Archive { .. } | Source::Url { .. } => None, - }, - } - .map(|r| r.as_str()) - } -} - -/// A descriptor of a remote repository. -#[derive(Debug, PartialEq)] -struct Repository { - /// The url of the repository. - url: Url, - /// If applicable, the branch or tag to be used. - reference: Option, - /// The name of a package within the repository. Defaults to the repository name. - package: String, -} - -impl Repository { - /// Parses a url in the form of https://github.com/org/repository?package#tag into its component parts. - /// - /// # Arguments - /// * `url` - The url to be parsed. - fn parse(url: &str) -> Result { - let url = Url::parse(url)?; - let package = url.query(); - let reference = url.fragment().map(|f| f.to_string()); - - let mut url = url.clone(); - url.set_query(None); - url.set_fragment(None); - - let package = match package { - Some(b) => b, - None => GitHub::name(&url)?, - } - .to_string(); - - Ok(Self { url, reference, package }) - } -} - -/// Trait for observing status updates. -pub trait Status { - /// Update the observer with the provided `status`. - fn update(&self, status: &str); -} - -impl Status for () { - // no-op: status updates are ignored - fn update(&self, _: &str) {} -} - /// Attempts to resolve the package manifest from the specified path. /// /// # Arguments @@ -858,32 +640,6 @@ fn resolve_manifest(package: &str, path: &Path) -> Result, Error Ok(manifest.map(|p| p.join("Cargo.toml"))) } -/// Determines the target triple based on the current platform. -fn target() -> Result<&'static str, Error> { - use std::env::consts::*; - - if OS == "windows" { - return Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }); - } - - match ARCH { - "aarch64" => { - return match OS { - "macos" => Ok("aarch64-apple-darwin"), - _ => Ok("aarch64-unknown-linux-gnu"), - } - }, - "x86_64" | "x86" => { - return match OS { - "macos" => Ok("x86_64-apple-darwin"), - _ => Ok("x86_64-unknown-linux-gnu"), - } - }, - &_ => {}, - } - Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }) -} - #[cfg(test)] mod tests { use super::*; @@ -894,7 +650,14 @@ mod tests { mod zombienet { use super::*; - use sourcing::tests::Output; + use pop_common::Status; + + pub(crate) struct Output; + impl Status for Output { + fn update(&self, status: &str) { + println!("{status}") + } + } #[tokio::test] async fn new_with_relay_only_works() -> Result<()> { @@ -2051,7 +1814,7 @@ node_spawn_timeout = 300 mod parachain { use super::*; - use crate::up::sourcing::GitHub::SourceCodeArchive; + use pop_common::sourcing::GitHub::SourceCodeArchive; use std::path::PathBuf; #[test] @@ -2144,345 +1907,6 @@ node_spawn_timeout = 300 } } - mod binary { - use super::*; - use duct::cmd; - use sourcing::tests::Output; - use std::fs::create_dir_all; - - #[test] - fn local_binary_works() -> Result<()> { - let name = "polkadot"; - let temp_dir = tempdir()?; - let path = temp_dir.path().join(name); - File::create(&path)?; - - let binary = - Binary::Local { name: name.to_string(), path: path.clone(), manifest: None }; - - assert!(binary.exists()); - assert_eq!(binary.latest(), None); - assert!(binary.local()); - assert_eq!(binary.name(), name); - assert_eq!(binary.path(), path); - assert!(!binary.stale()); - assert_eq!(binary.version(), None); - Ok(()) - } - - #[test] - fn local_package_works() -> Result<()> { - let name = "polkadot"; - let temp_dir = tempdir()?; - let path = temp_dir.path().join("target/release").join(name); - create_dir_all(&path.parent().unwrap())?; - File::create(&path)?; - let manifest = Some(temp_dir.path().join("Cargo.toml")); - - let binary = Binary::Local { name: name.to_string(), path: path.clone(), manifest }; - - assert!(binary.exists()); - assert_eq!(binary.latest(), None); - assert!(binary.local()); - assert_eq!(binary.name(), name); - assert_eq!(binary.path(), path); - assert!(!binary.stale()); - assert_eq!(binary.version(), None); - Ok(()) - } - - #[test] - fn resolve_version_works() -> Result<()> { - let name = "polkadot"; - let temp_dir = tempdir()?; - - let available = vec!["v1.13.0", "v1.12.0", "v1.11.0"]; - - // Specified - let specified = Some("v1.12.0"); - assert_eq!( - Binary::resolve_version(name, specified, &available, temp_dir.path()).unwrap(), - specified.unwrap() - ); - // Latest - assert_eq!( - Binary::resolve_version(name, None, &available, temp_dir.path()).unwrap(), - available[0] - ); - // Cached - File::create(temp_dir.path().join(format!("{name}-{}", available[1])))?; - assert_eq!( - Binary::resolve_version(name, None, &available, temp_dir.path()).unwrap(), - available[1] - ); - Ok(()) - } - - #[test] - fn sourced_from_archive_works() -> Result<()> { - let name = "polkadot"; - let url = "https://github.com/r0gue-io/polkadot/releases/latest/download/polkadot-aarch64-apple-darwin.tar.gz".to_string(); - let contents = vec![ - name.to_string(), - "polkadot-execute-worker".into(), - "polkadot-prepare-worker".into(), - ]; - let temp_dir = tempdir()?; - let path = temp_dir.path().join(name); - File::create(&path)?; - - let mut binary = Binary::Source { - name: name.to_string(), - source: Archive { url: url.to_string(), contents }, - cache: temp_dir.path().to_path_buf(), - }; - - assert!(binary.exists()); - assert_eq!(binary.latest(), None); - assert!(!binary.local()); - assert_eq!(binary.name(), name); - assert_eq!(binary.path(), path); - assert!(!binary.stale()); - assert_eq!(binary.version(), None); - binary.use_latest(); - assert_eq!(binary.version(), None); - Ok(()) - } - - #[test] - fn sourced_from_git_works() -> Result<()> { - let package = "hello_world"; - let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?; - let temp_dir = tempdir()?; - for reference in [None, Some("436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3".to_string())] { - let path = temp_dir.path().join( - reference - .as_ref() - .map_or(package.into(), |reference| format!("{package}-{reference}")), - ); - File::create(&path)?; - - let mut binary = Binary::Source { - name: package.to_string(), - source: Git { - url: url.clone(), - reference: reference.clone(), - manifest: None, - package: package.to_string(), - artifacts: vec![package.to_string()], - }, - cache: temp_dir.path().to_path_buf(), - }; - - assert!(binary.exists()); - assert_eq!(binary.latest(), None); - assert!(!binary.local()); - assert_eq!(binary.name(), package); - assert_eq!(binary.path(), path); - assert!(!binary.stale()); - assert_eq!(binary.version(), reference.as_ref().map(|r| r.as_str())); - binary.use_latest(); - assert_eq!(binary.version(), reference.as_ref().map(|r| r.as_str())); - } - - Ok(()) - } - - #[test] - fn sourced_from_github_release_archive_works() -> Result<()> { - let owner = "r0gue-io"; - let repository = "polkadot"; - let tag_format = "polkadot-{tag}"; - let name = "polkadot"; - let archive = format!("{name}-{}.tar.gz", target()?); - let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"]; - let temp_dir = tempdir()?; - for tag in [None, Some("v1.12.0".to_string())] { - let path = temp_dir - .path() - .join(tag.as_ref().map_or(name.to_string(), |t| format!("{name}-{t}"))); - File::create(&path)?; - for latest in [None, Some("v2.0.0".to_string())] { - let mut binary = Binary::Source { - name: name.to_string(), - source: GitHub(ReleaseArchive { - owner: owner.into(), - repository: repository.into(), - tag: tag.clone(), - tag_format: Some(tag_format.to_string()), - archive: archive.clone(), - contents: contents.into_iter().map(|b| (b, None)).collect(), - latest: latest.clone(), - }), - cache: temp_dir.path().to_path_buf(), - }; - - assert!(binary.exists()); - assert_eq!(binary.latest(), latest.as_ref().map(|l| l.as_str())); - assert!(!binary.local()); - assert_eq!(binary.name(), name); - assert_eq!(binary.path(), path); - assert_eq!(binary.stale(), latest.is_some()); - assert_eq!(binary.version(), tag.as_ref().map(|t| t.as_str())); - binary.use_latest(); - if latest.is_some() { - assert_eq!(binary.version(), latest.as_ref().map(|l| l.as_str())); - } - } - } - Ok(()) - } - - #[test] - fn sourced_from_github_source_code_archive_works() -> Result<()> { - let owner = "paritytech"; - let repository = "polkadot-sdk"; - let package = "polkadot"; - let manifest = "substrate/Cargo.toml"; - let temp_dir = tempdir()?; - for reference in [None, Some("72dba98250a6267c61772cd55f8caf193141050f".to_string())] { - let path = temp_dir.path().join( - reference.as_ref().map_or(package.to_string(), |t| format!("{package}-{t}")), - ); - File::create(&path)?; - let mut binary = Binary::Source { - name: package.to_string(), - source: GitHub(SourceCodeArchive { - owner: owner.to_string(), - repository: repository.to_string(), - reference: reference.clone(), - manifest: Some(PathBuf::from(manifest)), - package: package.to_string(), - artifacts: vec![package.to_string()], - }), - cache: temp_dir.path().to_path_buf(), - }; - - assert!(binary.exists()); - assert_eq!(binary.latest(), None); - assert!(!binary.local()); - assert_eq!(binary.name(), package); - assert_eq!(binary.path(), path); - assert_eq!(binary.stale(), false); - assert_eq!(binary.version(), reference.as_ref().map(|r| r.as_str())); - binary.use_latest(); - assert_eq!(binary.version(), reference.as_ref().map(|l| l.as_str())); - } - Ok(()) - } - - #[test] - fn sourced_from_url_works() -> Result<()> { - let name = "polkadot"; - let url = - "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc"; - let temp_dir = tempdir()?; - let path = temp_dir.path().join(name); - File::create(&path)?; - - let mut binary = Binary::Source { - name: name.to_string(), - source: Source::Url { url: url.to_string(), name: name.to_string() }, - cache: temp_dir.path().to_path_buf(), - }; - - assert!(binary.exists()); - assert_eq!(binary.latest(), None); - assert!(!binary.local()); - assert_eq!(binary.name(), name); - assert_eq!(binary.path(), path); - assert!(!binary.stale()); - assert_eq!(binary.version(), None); - binary.use_latest(); - assert_eq!(binary.version(), None); - Ok(()) - } - - #[tokio::test] - async fn sourcing_from_local_binary_not_supported() -> Result<()> { - let name = "polkadot".to_string(); - let temp_dir = tempdir()?; - let path = temp_dir.path().join(&name); - assert!(matches!( - Binary::Local { name, path: path.clone(), manifest: None }.source(true, &Output, true).await, - Err(Error::MissingBinary(error)) if error == format!("The {path:?} binary cannot be sourced automatically.") - )); - Ok(()) - } - - #[tokio::test] - async fn sourcing_from_local_package_works() -> Result<()> { - let temp_dir = tempdir()?; - let name = "hello_world"; - cmd("cargo", ["new", name, "--bin"]).dir(temp_dir.path()).run()?; - let path = temp_dir.path().join(name); - let manifest = Some(path.join("Cargo.toml")); - let path = path.join("target/release").join(name); - Binary::Local { name: name.to_string(), path: path.clone(), manifest } - .source(true, &Output, true) - .await?; - assert!(path.exists()); - Ok(()) - } - - #[tokio::test] - async fn sourcing_from_url_works() -> Result<()> { - let name = "polkadot"; - let url = - "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc"; - let temp_dir = tempdir()?; - let path = temp_dir.path().join(name); - - Binary::Source { - name: name.to_string(), - source: Source::Url { url: url.to_string(), name: name.to_string() }, - cache: temp_dir.path().to_path_buf(), - } - .source(true, &Output, true) - .await?; - assert!(path.exists()); - Ok(()) - } - } - - mod repository { - use super::{Error, Repository}; - use url::Url; - - #[test] - fn parsing_full_url_works() { - assert_eq!( - Repository::parse("https://github.com/org/repository?package#tag").unwrap(), - Repository { - url: Url::parse("https://github.com/org/repository").unwrap(), - reference: Some("tag".into()), - package: "package".into(), - } - ); - } - - #[test] - fn parsing_simple_url_works() { - let url = "https://github.com/org/repository"; - assert_eq!( - Repository::parse(url).unwrap(), - Repository { - url: Url::parse(url).unwrap(), - reference: None, - package: "repository".into(), - } - ); - } - - #[test] - fn parsing_invalid_url_returns_error() { - assert!(matches!( - Repository::parse("github.com/org/repository"), - Err(Error::ParseError(..)) - )); - } - } - #[test] fn resolve_manifest_works() -> Result<()> { let current_dir = current_dir()?; @@ -2498,19 +1922,4 @@ node_spawn_timeout = 300 ); Ok(()) } - - #[test] - fn target_works() -> Result<()> { - use std::{process::Command, str}; - let output = Command::new("rustc").arg("-vV").output()?; - let output = str::from_utf8(&output.stdout)?; - let target = output - .lines() - .find(|l| l.starts_with("host: ")) - .map(|l| &l[6..]) - .unwrap() - .to_string(); - assert_eq!(super::target()?, target); - Ok(()) - } } diff --git a/crates/pop-parachains/src/up/parachains.rs b/crates/pop-parachains/src/up/parachains.rs index 053690c6..3bea2cba 100644 --- a/crates/pop-parachains/src/up/parachains.rs +++ b/crates/pop-parachains/src/up/parachains.rs @@ -1,16 +1,14 @@ // SPDX-License-Identifier: GPL-3.0 -use super::{ - chain_specs::chain_spec_generator, +use super::{chain_specs::chain_spec_generator, Binary}; +use pop_common::{ sourcing::{ - self, traits::{Source as _, *}, GitHub::ReleaseArchive, Source, }, - target, Binary, Error, + target, Error, GitHub, }; -use pop_common::GitHub; use std::path::Path; use strum::VariantArray as _; use strum_macros::{EnumProperty, VariantArray}; @@ -61,7 +59,7 @@ impl TryInto for Parachain { } } -impl sourcing::traits::Source for Parachain {} +impl pop_common::sourcing::traits::Source for Parachain {} /// Initialises the configuration required to launch a system parachain. /// diff --git a/crates/pop-parachains/src/up/relay.rs b/crates/pop-parachains/src/up/relay.rs index 05304363..4d50fdf4 100644 --- a/crates/pop-parachains/src/up/relay.rs +++ b/crates/pop-parachains/src/up/relay.rs @@ -1,16 +1,16 @@ // SPDX-License-Identifier: GPL-3.0 -use super::{ - chain_specs::chain_spec_generator, +use super::chain_specs::chain_spec_generator; +pub use pop_common::{ + git::GitHub, sourcing::{ - self, traits::{Source as _, *}, - GitHub::ReleaseArchive, + Binary, + GitHub::*, Source, }, - target, Binary, Error, + target, Error, }; -use pop_common::GitHub; use std::{iter::once, path::Path}; use strum::VariantArray as _; use strum_macros::{EnumProperty, VariantArray}; @@ -63,7 +63,7 @@ impl RelayChain { } } -impl sourcing::traits::Source for RelayChain {} +impl pop_common::sourcing::traits::Source for RelayChain {} /// Initialises the configuration required to launch the relay chain. ///