Skip to content

Commit fc1ec1a

Browse files
committed
Add contract-with-tests template
1 parent 4200482 commit fc1ec1a

File tree

7 files changed

+265
-0
lines changed

7 files changed

+265
-0
lines changed

cargo-generate.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[template]
22
sub_templates = [
33
"contract",
4+
"contract-with-tests",
45
"workspace",
56
]

contract-with-tests/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "{{project-name}}"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
ckb-std = "0.15.0"
8+
9+
[dev-dependencies]
10+
ckb-testtool = "0.10.1"
11+
serde_json = "1.0"

contract-with-tests/Makefile

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# We cannot use $(shell pwd), which will return unix path format on Windows,
2+
# making it hard to use.
3+
cur_dir = $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
4+
5+
TOP := $(cur_dir)
6+
# RUSTFLAGS that are likely to be tweaked by developers. For example,
7+
# while we enable debug logs by default here, some might want to strip them
8+
# for minimal code size / consumed cycles.
9+
CUSTOM_RUSTFLAGS := --cfg debug_assertions
10+
# RUSTFLAGS that are less likely to be tweaked by developers. Most likely
11+
# one would want to keep the default values here.
12+
FULL_RUSTFLAGS := -C target-feature=+zba,+zbb,+zbc,+zbs $(CUSTOM_RUSTFLAGS)
13+
# Additional cargo args to append here. For example, one can use
14+
# make test CARGO_ARGS="-- --nocapture" so as to inspect data emitted to
15+
# stdout in unit tests
16+
CARGO_ARGS :=
17+
MODE := release
18+
# Tweak this to change the clang version to use for building C code. By default
19+
# we use a bash script with somes heuristics to find clang in current system.
20+
CLANG := $(shell $(TOP)/scripts/find_clang)
21+
# When this is set to some value, the generated binaries will be copied over
22+
BUILD_DIR := build/$(MODE)
23+
# Generated binaries to copy. By convention, a Rust crate's directory name will
24+
# likely match the crate name, which is also the name of the final binary.
25+
# However if this is not the case, you can tweak this variable. As the name hints,
26+
# more than one binary is supported here.
27+
BINARIES := $(notdir $(shell pwd))
28+
29+
ifeq (release,$(MODE))
30+
MODE_ARGS := --release
31+
endif
32+
33+
default: build test
34+
35+
build:
36+
RUSTFLAGS="$(FULL_RUSTFLAGS)" TARGET_CC="$(CLANG)" \
37+
cargo build --target=riscv64imac-unknown-none-elf $(MODE_ARGS) $(CARGO_ARGS)
38+
mkdir -p $(BUILD_DIR)
39+
@set -eu; \
40+
if [ "x$(BUILD_DIR)" != "x" ]; then \
41+
for binary in $(BINARIES); do \
42+
echo "Copying binary $$binary to build directory"; \
43+
cp $(TOP)/target/riscv64imac-unknown-none-elf/$(MODE)/$$binary $(TOP)/$(BUILD_DIR); \
44+
done \
45+
fi
46+
47+
# test, check, clippy and fmt here are provided for completeness,
48+
# there is nothing wrong invoking cargo directly instead of make.
49+
test:
50+
cargo test $(CARGO_ARGS)
51+
52+
check:
53+
cargo check $(CARGO_ARGS)
54+
55+
clippy:
56+
cargo clippy $(CARGO_ARGS)
57+
58+
fmt:
59+
cargo fmt $(CARGO_ARGS)
60+
61+
# Arbitrary cargo command is supported here. For example:
62+
#
63+
# make cargo CARGO_CMD=expand CARGO_ARGS="--ugly"
64+
#
65+
# Invokes:
66+
# cargo expand --ugly
67+
CARGO_CMD :=
68+
cargo:
69+
cargo $(CARGO_CMD) $(CARGO_ARGS)
70+
71+
clean:
72+
rm -rf build
73+
cargo clean
74+
75+
prepare:
76+
rustup target add riscv64imac-unknown-none-elf
77+
78+
.PHONY: build test check clippy fmt cargo clean prepare
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
#
3+
# An utility script used to find a binary of clang 16+
4+
5+
if [[ -n "${CLANG}" ]]; then
6+
echo "${CLANG}"
7+
exit 0
8+
fi
9+
10+
CANDIDATES=("clang" "clang-16" "clang-17")
11+
12+
BREW_PREFIX=$(brew --prefix 2> /dev/null)
13+
if [[ -n "${BREW_PREFIX}" ]]; then
14+
CANDIDATES+=(
15+
"${BREW_PREFIX}/opt/llvm/bin/clang"
16+
"${BREW_PREFIX}/opt/llvm@16/bin/clang"
17+
"${BREW_PREFIX}/opt/llvm@17/bin/clang"
18+
)
19+
fi
20+
21+
for candidate in ${CANDIDATES[@]}; do
22+
OUTPUT=$($candidate -dumpversion 2> /dev/null | cut -d'.' -f 1)
23+
24+
if [[ $((OUTPUT)) -ge 16 ]]; then
25+
echo "$candidate"
26+
exit 0
27+
fi
28+
done
29+
30+
>&2 echo "Cannot find clang of version 16+!"
31+
exit 1

contract-with-tests/src/main.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#![cfg_attr(target_arch = "riscv64", no_std)]
2+
#![cfg_attr(not(test), no_main)]
3+
4+
#[cfg(test)]
5+
extern crate alloc;
6+
7+
#[cfg(test)]
8+
mod tests;
9+
10+
#[cfg(not(test))]
11+
use ckb_std::default_alloc;
12+
#[cfg(not(test))]
13+
ckb_std::entry!(program_entry);
14+
#[cfg(not(test))]
15+
default_alloc!();
16+
17+
pub fn program_entry() -> i8 {
18+
ckb_std::debug!("This is a sample contract!");
19+
20+
0
21+
}

contract-with-tests/src/tests/mod.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#![allow(dead_code)]
2+
3+
use ckb_testtool::{
4+
ckb_error::Error,
5+
ckb_types::{
6+
bytes::Bytes,
7+
core::{Cycle, TransactionView},
8+
},
9+
context::Context,
10+
};
11+
use std::env;
12+
use std::fs;
13+
use std::path::PathBuf;
14+
use std::str::FromStr;
15+
16+
#[cfg(test)]
17+
mod tests;
18+
19+
// The exact same Loader code from capsule's template, except that
20+
// now we use MODE as the environment variable
21+
const TEST_ENV_VAR: &str = "MODE";
22+
23+
pub enum TestEnv {
24+
Debug,
25+
Release,
26+
}
27+
28+
impl FromStr for TestEnv {
29+
type Err = &'static str;
30+
31+
fn from_str(s: &str) -> Result<Self, Self::Err> {
32+
match s.to_lowercase().as_str() {
33+
"debug" => Ok(TestEnv::Debug),
34+
"release" => Ok(TestEnv::Release),
35+
_ => Err("no match"),
36+
}
37+
}
38+
}
39+
40+
pub struct Loader(PathBuf);
41+
42+
impl Default for Loader {
43+
fn default() -> Self {
44+
let test_env = match env::var(TEST_ENV_VAR) {
45+
Ok(val) => val.parse().expect("test env"),
46+
Err(_) => TestEnv::Release,
47+
};
48+
Self::with_test_env(test_env)
49+
}
50+
}
51+
52+
impl Loader {
53+
fn with_test_env(env: TestEnv) -> Self {
54+
let load_prefix = match env {
55+
TestEnv::Debug => "debug",
56+
TestEnv::Release => "release",
57+
};
58+
let mut base_path = match env::var("TOP") {
59+
Ok(val) => {
60+
let mut base_path: PathBuf = val.into();
61+
base_path.push("build");
62+
base_path
63+
}
64+
Err(_) => {
65+
let mut base_path = PathBuf::new();
66+
// cargo may use a different cwd when running tests, for example:
67+
// when running debug in vscode, it will use workspace root as cwd by default,
68+
// when running test by `cargo test`, it will use tests directory as cwd,
69+
// so we need a fallback path
70+
base_path.push("build");
71+
if !base_path.exists() {
72+
base_path.pop();
73+
base_path.push("..");
74+
base_path.push("build");
75+
}
76+
base_path
77+
}
78+
};
79+
80+
base_path.push(load_prefix);
81+
Loader(base_path)
82+
}
83+
84+
pub fn load_binary(&self, name: &str) -> Bytes {
85+
let mut path = self.0.clone();
86+
path.push(name);
87+
let result = fs::read(&path);
88+
if result.is_err() {
89+
panic!("Binary {:?} is missing!", path);
90+
}
91+
result.unwrap().into()
92+
}
93+
}
94+
95+
// This helper method runs Context::verify_tx, but in case error happens,
96+
// it also dumps current transaction to failed_txs folder.
97+
pub fn verify_and_dump_failed_tx(
98+
context: &Context,
99+
tx: &TransactionView,
100+
max_cycles: u64,
101+
) -> Result<Cycle, Error> {
102+
let result = context.verify_tx(tx, max_cycles);
103+
if result.is_err() {
104+
let mut path = env::current_dir().expect("current dir");
105+
path.push("failed_txs");
106+
std::fs::create_dir_all(&path).expect("create failed_txs dir");
107+
let mock_tx = context.dump_tx(tx).expect("dump failed tx");
108+
let json = serde_json::to_string_pretty(&mock_tx).expect("json");
109+
path.push(format!("0x{:x}.json", tx.hash()));
110+
println!("Failed tx written to {:?}", path);
111+
std::fs::write(path, json).expect("write");
112+
}
113+
result
114+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Include your tests here
2+
// See https://github.com/xxuejie/ckb-native-build-sample/blob/main/tests/src/tests.rs for examples
3+
4+
use super::Loader;
5+
6+
#[test]
7+
fn test_binary_exists() {
8+
Loader::default().load_binary("{{project-name}}");
9+
}

0 commit comments

Comments
 (0)