Skip to content
This repository has been archived by the owner on Feb 11, 2025. It is now read-only.

#[drink::test] macro #70

Merged
merged 20 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions .github/workflows/rust-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ jobs:
targets: wasm32-unknown-unknown
components: clippy rustfmt

- name: Add rust-src
shell: bash
run: rustup component add rust-src

- name: Run format checks
uses: actions-rs/cargo@v1
with:
Expand All @@ -50,12 +54,6 @@ jobs:
with:
command: test

- name: Install cargo-contract
shell: bash
run: |
rustup component add rust-src
cargo install cargo-contract --version 3.2.0

- name: Run tests for examples
shell: bash
run: make test_examples
33 changes: 30 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 9 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"

members = [
"drink",
"drink/test-macro",
"drink-cli",
]

Expand All @@ -20,24 +21,27 @@ homepage = "https://github.com/Cardinal-Cryptography/drink"
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/Cardinal-Cryptography/drink"
version = "0.5.2"
version = "0.5.3"

[workspace.dependencies]
anyhow = { version = "1.0.71" }
cargo_metadata = { version = "0.18.1" }
clap = { version = "4.3.4" }
contract-build = { version = "3.0.1" }
contract-metadata = { version = "3.2.0" }
contract-transcode = { version = "3.2.0" }
crossterm = { version = "0.26.0" }
parity-scale-codec = { version = "3.4" }
parity-scale-codec-derive = { version = "3.4" }
proc-macro2 = { version = "1" }
quote = { version = "1" }
ratatui = { version = "0.21.0" }
scale-info = { version = "2.5.0" }
serde_json = { version = "1.0" }
syn = { version = "2" }
thiserror = { version = "1.0.40" }
wat = { version = "1.0.71" }

serde_json = { version = "1.0.106" }

# Substrate dependencies

frame-metadata = { version = "16.0.0" }
Expand All @@ -54,4 +58,5 @@ sp-runtime-interface = { version = "19.0.0" }

# Local dependencies

drink = { version = "0.5.2", path = "drink" }
drink = { version = "0.5.3", path = "drink" }
drink-test-macro = { version = "0.5.3", path = "drink/test-macro" }
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ lint: ## Run the linter
test_examples: ## Run tests for the examples
@for dir in $(EXAMPLES_PATHS); do \
echo "Processing $$dir" ; \
cargo contract build --quiet --manifest-path $$dir/Cargo.toml --release ; \
cargo test --quiet --manifest-path $$dir/Cargo.toml --release; \
done

Expand Down
2 changes: 2 additions & 0 deletions drink/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ scale-info = { workspace = true }
thiserror = { workspace = true }
wat = { workspace = true }

drink-test-macro = { workspace = true }

[features]
default = [
# This is required for the runtime-interface to work properly in the std env.
Expand Down
1 change: 1 addition & 0 deletions drink/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use std::{
sync::{Arc, Mutex},
};

pub use drink_test_macro::test;
pub use errors::Error;
use frame_support::sp_runtime::{traits::One, BuildStorage};
pub use frame_support::{
Expand Down
21 changes: 21 additions & 0 deletions drink/test-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "drink-test-macro"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
description = "Procedural macro providing a `#[drink::test]` attribute for `drink`-based contract testing"

[lib]
proc-macro = true

[dependencies]
cargo_metadata = { workspace = true }
contract-build = { workspace = true }
contract-metadata = { workspace = true }
proc-macro2 = { workspace = true }
syn = { workspace = true, features = ["full"] }
quote = { workspace = true }
110 changes: 110 additions & 0 deletions drink/test-macro/src/contract_building.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::{
collections::HashSet,
path::PathBuf,
sync::{Mutex, OnceLock},
};

use cargo_metadata::{Metadata, MetadataCommand, Package};
use contract_build::{
BuildArtifacts, BuildMode, ExecuteArgs, Features, ManifestPath, Network, OptimizationPasses,
OutputType, Target, UnstableFlags, Verbosity,
};

/// Contract package differentiator.
const INK_AS_DEPENDENCY_FEATURE: &str = "ink-as-dependency";

/// Stores the manifest paths of all contracts that have already been built.
///
/// This prevents from building the same contract for every testcase separately.
static CONTRACTS_BUILT: OnceLock<Mutex<HashSet<PathBuf>>> = OnceLock::new();

/// Build the current package with `cargo contract build --release` (if it is a contract package),
/// as well as all its contract dependencies.
///
/// A package is considered as a contract package, if it has the `ink-as-dependency` feature.
///
/// A contract dependency, is a package defined in the `Cargo.toml` file with the
/// `ink-as-dependency` feature enabled.
pub fn build_contracts() {
let metadata = MetadataCommand::new()
.exec()
.expect("Error invoking `cargo metadata`");

for contract_crate in get_contract_crates(&metadata) {
build_contract_crate(contract_crate);
}
}

fn get_contract_crates(metadata: &Metadata) -> Vec<&Package> {
let pkg_lookup = |id| {
metadata
.packages
.iter()
.find(|package| package.id == id)
.unwrap_or_else(|| panic!("Error resolving package {id}"))
};

let dep_graph = metadata
.resolve
.as_ref()
.expect("Error resolving dependencies");

let contract_deps = dep_graph
.nodes
.iter()
.filter_map(|node| {
node.features
.contains(&INK_AS_DEPENDENCY_FEATURE.to_string())
.then(|| node.id.clone())
})
.map(pkg_lookup);

let root = dep_graph
.root
.as_ref()
.expect("Error resolving root package");
let root = pkg_lookup(root.clone());

root.features
.contains_key(INK_AS_DEPENDENCY_FEATURE)
.then_some(root)
.into_iter()
.chain(contract_deps)
.collect()
}

fn build_contract_crate(pkg: &Package) {
let manifest_path = get_manifest_path(pkg);

if !CONTRACTS_BUILT
.get_or_init(|| Mutex::new(HashSet::new()))
.lock()
.expect("Error locking mutex")
.insert(manifest_path.clone().into())
{
return;
}

let args = ExecuteArgs {
manifest_path,
verbosity: Verbosity::Default,
build_mode: BuildMode::Release,
features: Features::default(),
network: Network::Online,
build_artifact: BuildArtifacts::All,
unstable_flags: UnstableFlags::default(),
optimization_passes: Some(OptimizationPasses::default()),
keep_debug_symbols: false,
lint: false,
output_type: OutputType::HumanReadable,
skip_wasm_validation: false,
target: Target::Wasm,
};
Comment on lines +88 to +102
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering - some of these params are definable in Cargo.toml - especially for the root package - and here we're "overwriting" them. That's probably fine since we are "optimising for drink!" but just something to be aware of.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#72


contract_build::execute(args).expect("Error building contract");
}

fn get_manifest_path(package: &Package) -> ManifestPath {
ManifestPath::new(package.manifest_path.clone().into_std_path_buf())
.unwrap_or_else(|_| panic!("Error resolving manifest path for package {}", package.name))
}
64 changes: 64 additions & 0 deletions drink/test-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! Procedural macro providing a `#[drink::test]` attribute for `drink`-based contract testing.

#![warn(missing_docs)]

mod contract_building;

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::ItemFn;

use crate::contract_building::build_contracts;

type SynResult<T> = Result<T, syn::Error>;

/// Defines a drink!-based test.
///
/// # Requirements
///
/// - You mustn't import `drink::test` in the scope, where the macro is used. In other words, you
/// should always use the macro only with a qualified path `#[drink::test]`.
/// - Your crate cannot be part of a cargo workspace.
///
/// # Impact
///
/// This macro will take care of building all needed contracts for the test. The building process
/// will be executed during compile time.
///
/// Contracts to be built:
/// - current cargo package if contains a `ink-as-dependency` feature
/// - all dependencies declared in the `Cargo.toml` file with the `ink-as-dependency` feature
/// enabled
///
/// Note: Depending on a non-local contract is not tested yet.
///
/// # Example
///
/// ```rust, ignore
/// #[drink::test]
/// fn testcase() {
/// Session::<MinimalRuntime>::new()
/// .unwrap()
/// .deploy(bytes(), "new", NO_ARGS, vec![], None, &transcoder())
/// .unwrap();
/// }
/// ```
#[proc_macro_attribute]
pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
match test_internal(attr.into(), item.into()) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}

/// Auxiliary function to enter ?-based error propagation.
fn test_internal(_attr: TokenStream2, item: TokenStream2) -> SynResult<TokenStream2> {
let item_fn = syn::parse2::<ItemFn>(item)?;
build_contracts();

Ok(quote! {
#[test]
#item_fn
})
}
2 changes: 1 addition & 1 deletion examples/cross-contract-call-tracing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals
scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true }

[dev-dependencies]
drink = { path = "../../drink", features = ["session"] }
drink = { path = "../../drink" }

[lib]
path = "lib.rs"
Expand Down
2 changes: 1 addition & 1 deletion examples/cross-contract-call-tracing/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ mod tests {
}
}

#[test]
#[drink::test]
fn test() -> Result<(), Box<dyn Error>> {
let mut session = Session::<MinimalRuntime>::new()?;
session.override_debug_handle(TracingExt(Box::new(TestDebugger {})));
Expand Down
2 changes: 1 addition & 1 deletion examples/flipper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals
scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true }

[dev-dependencies]
drink = { path = "../../drink", features = ["session"] }
drink = { path = "../../drink" }

[lib]
path = "lib.rs"
Expand Down
Loading