Skip to content

Commit 2f4f4f8

Browse files
committed
feat: add code coverage support using native-simulator
- Add ctor dependency and coverage_flush module to native-simulator template - Add coverage targets to workspace Makefile (coverage, coverage-html, coverage-lcov) - Add coverage documentation to README.md with clear prerequisites - Add coverage test step to CI workflow (ubuntu-latest only, not ARM) - Update test templates to use deploy_cell_by_name() for native-simulator support Note: Only the 'contract' template supports native-simulator feature and coverage. Other templates (atomics-contract, stack-reorder-contract, contract-without-simulator, standalone-contract) do not support coverage.
1 parent 6a78488 commit 2f4f4f8

10 files changed

Lines changed: 216 additions & 17 deletions

File tree

.github/workflows/rust.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ jobs:
4545
run: cd test-workspace && rustup target add riscv64imac-unknown-none-elf && rustup component add clippy
4646
- name: Run all checks
4747
run: cd test-workspace && make build test check clippy
48+
- name: Generate native simulators for coverage
49+
if: matrix.os == 'ubuntu-latest'
50+
run: cd test-workspace &&
51+
make generate-native-simulator CRATE=c1 TEMPLATE_TYPE=--path TEMPLATE_REPO=..
52+
- name: Install llvm-tools for coverage
53+
if: matrix.os == 'ubuntu-latest'
54+
run: cd test-workspace && make coverage-install
55+
- name: Run coverage tests
56+
if: matrix.os == 'ubuntu-latest'
57+
run: cd test-workspace && make coverage
4858
- name: Reproducible build runs
4959
run: cd test-workspace && ./scripts/reproducible_build_docker --update && ./scripts/reproducible_build_docker --no-clean
5060
- name: Generate standalone contract

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,47 @@ make generate-native-simulator CRATE=example_crate
255255

256256
- The `CRATE` parameter must refer to a subproject.
257257
- Missing subprojects will cause the command to fail.
258+
259+
### Code Coverage
260+
261+
The templates include built-in support for code coverage reporting using LLVM's coverage tools.
262+
263+
**Requirements:**
264+
- Only contracts created with the `contract` template support coverage (pure Rust contracts with `native-simulator` feature)
265+
- Other templates (`atomics-contract`, `stack-reorder-contract`, `contract-without-simulator`, `standalone-contract`) do not support coverage
266+
- Native simulators must be generated for each contract you want coverage for
267+
- Only works on x86_64 Linux (not ARM)
268+
269+
**Setup:**
270+
271+
1. Install llvm-tools:
272+
```bash
273+
make coverage-install
274+
```
275+
276+
2. Generate native simulators for your contracts:
277+
```bash
278+
make generate-native-simulator CRATE=<contract_name>
279+
```
280+
281+
**Generate Reports:**
282+
```bash
283+
make coverage # Text report to console
284+
make coverage-html # HTML report in target/coverage/html/
285+
make coverage-lcov # LCOV format for CI integration
286+
```
287+
288+
**Example:**
289+
```bash
290+
# Create a contract using the default 'contract' template
291+
make generate CRATE=my-contract
292+
293+
# Generate native simulator for it
294+
make generate-native-simulator CRATE=my-contract
295+
296+
# Install tools and run coverage
297+
make coverage-install
298+
make coverage
299+
```
300+
301+
Coverage works by running tests with the `native-simulator` feature, which executes contract code natively on x86_64 instead of in CKB-VM. This allows LLVM's coverage instrumentation to track which lines of code are executed.

atomics-contract/.cargo-generate/tests.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
fn test_{{crate_name}}() {
55
// deploy contract
66
let mut context = Context::default();
7-
let contract_bin: Bytes = Loader::default().load_binary("{{project-name}}");
8-
let out_point = context.deploy_cell(contract_bin);
7+
let out_point = context.deploy_cell_by_name("{{project-name}}");
98

109
// prepare scripts
1110
let lock_script = context

contract-without-simulator/.cargo-generate/tests.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
fn test_{{crate_name}}() {
55
// deploy contract
66
let mut context = Context::default();
7-
let contract_bin: Bytes = Loader::default().load_binary("{{project-name}}");
8-
let out_point = context.deploy_cell(contract_bin);
7+
let out_point = context.deploy_cell_by_name("{{project-name}}");
98

109
// prepare scripts
1110
let lock_script = context

contract/.cargo-generate/tests.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
fn test_{{crate_name}}() {
55
// deploy contract
66
let mut context = Context::default();
7-
let contract_bin: Bytes = Loader::default().load_binary("{{project-name}}");
8-
let out_point = context.deploy_cell(contract_bin);
7+
let out_point = context.deploy_cell_by_name("{{project-name}}");
98

109
// prepare scripts
1110
let lock_script = context

native-simulator/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ name = "{{project-name}}"
33
version = "0.1.0"
44
edition = "2024"
55

6-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
76
[dependencies]
87
{{project-name | append: "@@SIMULATOR_PLACEHOLDER@@" | remove: "-sim@@SIMULATOR_PLACEHOLDER@@"}} = { path = "../../contracts/{{project-name | append: "@@SIMULATOR_PLACEHOLDER@@" | remove: "-sim@@SIMULATOR_PLACEHOLDER@@"}}", features = ["native-simulator"] }
98
ckb-std = { version = "1.0", features = ["native-simulator"] }
9+
ctor = "0.6"
1010

1111
[lib]
1212
crate-type = ["cdylib"]
13+
14+
[lints.rust]
15+
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] }

native-simulator/src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
11
ckb_std::entry_simulator!({{project-name | append: "@@SIMULATOR_PLACEHOLDER@@" | remove: "-sim@@SIMULATOR_PLACEHOLDER@@" | replace: "-", "_"}}::program_entry);
2+
3+
// Flush LLVM coverage data when the shared library is unloaded.
4+
// This is necessary because coverage data from dynamically loaded libraries
5+
// is not automatically written when using cargo-llvm-cov.
6+
#[cfg(coverage)]
7+
mod coverage_flush {
8+
unsafe extern "C" {
9+
fn __llvm_profile_write_file() -> i32;
10+
}
11+
12+
#[ctor::dtor]
13+
fn flush_coverage() {
14+
unsafe {
15+
__llvm_profile_write_file();
16+
}
17+
}
18+
}

stack-reorder-contract/.cargo-generate/tests.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
fn test_{{crate_name}}() {
55
// deploy contract
66
let mut context = Context::default();
7-
let contract_bin: Bytes = Loader::default().load_binary("{{project-name}}");
8-
let out_point = context.deploy_cell(contract_bin);
7+
let out_point = context.deploy_cell_by_name("{{project-name}}");
98

109
// prepare scripts
1110
let lock_script = context

workspace/Makefile

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,139 @@ CHECKSUM_FILE := build/checksums-$(MODE).txt
150150
checksum: build
151151
shasum -a 256 build/$(MODE)/* > $(CHECKSUM_FILE)
152152

153-
.PHONY: build test check clippy fmt cargo clean prepare checksum
153+
# ============================================================================
154+
# Coverage targets (using native-simulator mode)
155+
# ============================================================================
156+
#
157+
# Coverage works by:
158+
# 1. Building simulator .so files with LLVM coverage instrumentation
159+
# 2. Running tests with native-simulator feature (executes contracts natively)
160+
# 3. Simulator .so files call __llvm_profile_write_file() on unload to flush coverage
161+
# 4. Using llvm-cov to generate reports from the profraw files
162+
#
163+
# Requirements: rustup component add llvm-tools-preview
164+
#
165+
# Usage:
166+
# make coverage # Generate text coverage report
167+
# make coverage-html # Generate HTML coverage report
168+
# make coverage-lcov # Generate LCOV format for CI integration
169+
170+
LLVM_TOOLS_DIR := $(shell rustc --print sysroot)/lib/rustlib/$(shell rustc -vV | grep host | cut -d' ' -f2)/bin
171+
LLVM_PROFDATA := $(LLVM_TOOLS_DIR)/llvm-profdata
172+
LLVM_COV := $(LLVM_TOOLS_DIR)/llvm-cov
173+
COVERAGE_DIR := target/coverage
174+
PROFRAW_PATTERN := $(COVERAGE_DIR)/profraw
175+
176+
# Dynamically find all simulator .so files
177+
SIMULATOR_OBJECTS = $(wildcard build/debug/lib*_sim.so)
178+
SIMULATOR_OBJECT_ARGS = $(foreach obj,$(SIMULATOR_OBJECTS),-object $(obj))
179+
180+
# Build simulators with coverage instrumentation
181+
build-sim-cov:
182+
@echo "Building native simulators with coverage instrumentation..."
183+
@# First build the RISC-V contract binaries (these don't need coverage)
184+
$(MAKE) build MODE=debug CLEAN_BUILD_DIR_FIRST=true
185+
@# Clean simulator artifacts to force rebuild with coverage
186+
@rm -f target/debug/deps/lib*_sim*.rlib
187+
@rm -f target/debug/lib*_sim.so
188+
@# Rebuild simulators with LLVM coverage instrumentation
189+
@echo "Rebuilding simulators with LLVM coverage instrumentation..."
190+
@for sim in $(wildcard native-simulators/*); do \
191+
simname=$$(basename $$sim); \
192+
RUSTFLAGS="-C instrument-coverage --cfg=coverage" \
193+
cargo build -p $$simname; \
194+
done
195+
@# Copy the instrumented simulators to build/debug/
196+
@echo "Copying instrumented simulators to build/debug/..."
197+
@for sim in $(wildcard native-simulators/*); do \
198+
simname=$$(basename $$sim); \
199+
libname=$$(echo $$simname | sed 's/-/_/g'); \
200+
if [ -f "target/debug/lib$${libname}.so" ]; then \
201+
cp "target/debug/lib$${libname}.so" build/debug/; \
202+
fi; \
203+
done
204+
@echo "Done! Instrumented simulators are in build/debug/"
205+
206+
# Build simulators for coverage (debug mode for better coverage data)
207+
build-sim:
208+
@echo "Building native simulators..."
209+
$(MAKE) build MODE=debug CLEAN_BUILD_DIR_FIRST=true
210+
211+
# Internal target: run tests and collect coverage data
212+
coverage-run-tests:
213+
@echo "Running tests with coverage (native-simulator mode)..."
214+
@mkdir -p $(COVERAGE_DIR)
215+
@rm -f $(PROFRAW_PATTERN)-*.profraw
216+
@# Run all tests with native-simulator feature and coverage instrumentation
217+
LLVM_PROFILE_FILE="$(CURDIR)/$(PROFRAW_PATTERN)-%p-%8m.profraw" \
218+
RUSTFLAGS="-C instrument-coverage" \
219+
MODE=debug cargo test --features native-simulator --package tests \
220+
-- --test-threads=1 2>&1 || true
221+
@echo "Merging coverage data..."
222+
@if ls $(PROFRAW_PATTERN)-*.profraw 1> /dev/null 2>&1; then \
223+
$(LLVM_PROFDATA) merge -sparse $(PROFRAW_PATTERN)-*.profraw -o $(COVERAGE_DIR)/coverage.profdata; \
224+
rm -f $(PROFRAW_PATTERN)-*.profraw; \
225+
else \
226+
echo "Warning: No profraw files found. Tests may have failed or no coverage data was generated."; \
227+
fi
228+
@# Clean up stray default_*.profraw files created by child processes
229+
@rm -f $(CURDIR)/default_*.profraw
230+
231+
# Run tests with coverage and generate text report
232+
coverage: build-sim-cov coverage-run-tests
233+
@echo ""
234+
@echo "=== Coverage Report ==="
235+
@if [ -f "$(COVERAGE_DIR)/coverage.profdata" ]; then \
236+
$(LLVM_COV) report \
237+
$(SIMULATOR_OBJECT_ARGS) \
238+
-instr-profile=$(COVERAGE_DIR)/coverage.profdata \
239+
--ignore-filename-regex='\.cargo|rustc|rustlib'; \
240+
else \
241+
echo "Error: No coverage data found. Run 'make coverage-install' to install llvm-tools."; \
242+
fi
243+
244+
# Generate HTML coverage report
245+
coverage-html: build-sim-cov coverage-run-tests
246+
@echo "Generating HTML coverage report..."
247+
@mkdir -p $(COVERAGE_DIR)/html
248+
@if [ -f "$(COVERAGE_DIR)/coverage.profdata" ]; then \
249+
$(LLVM_COV) show \
250+
$(SIMULATOR_OBJECT_ARGS) \
251+
-instr-profile=$(COVERAGE_DIR)/coverage.profdata \
252+
--ignore-filename-regex='\.cargo|rustc|rustlib' \
253+
--format=html --output-dir=$(COVERAGE_DIR)/html \
254+
--show-line-counts-or-regions --show-instantiations; \
255+
echo "Coverage report generated at: $(COVERAGE_DIR)/html/index.html"; \
256+
else \
257+
echo "Error: No coverage data found."; \
258+
fi
259+
260+
# Generate LCOV format for CI integration
261+
coverage-lcov: build-sim-cov coverage-run-tests
262+
@echo "Generating LCOV coverage report..."
263+
@if [ -f "$(COVERAGE_DIR)/coverage.profdata" ]; then \
264+
$(LLVM_COV) export \
265+
$(SIMULATOR_OBJECT_ARGS) \
266+
-instr-profile=$(COVERAGE_DIR)/coverage.profdata \
267+
--ignore-filename-regex='\.cargo|rustc|rustlib' \
268+
--format=lcov > $(COVERAGE_DIR)/lcov.info; \
269+
echo "LCOV report generated at: $(COVERAGE_DIR)/lcov.info"; \
270+
else \
271+
echo "Error: No coverage data found."; \
272+
fi
273+
274+
# Install llvm-tools if not present
275+
coverage-install:
276+
@if ! rustup component list --installed | grep -q llvm-tools; then \
277+
echo "Installing llvm-tools-preview..."; \
278+
rustup component add llvm-tools-preview; \
279+
else \
280+
echo "llvm-tools-preview is already installed"; \
281+
fi
282+
283+
# Clean coverage artifacts
284+
coverage-clean:
285+
rm -rf $(COVERAGE_DIR)
286+
287+
.PHONY: build test check clippy fmt cargo clean prepare checksum \
288+
build-sim build-sim-cov coverage-run-tests coverage coverage-html coverage-lcov coverage-install coverage-clean

workspace/tests/src/tests.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
use crate::Loader;
2-
use ckb_testtool::ckb_types::{
3-
bytes::Bytes,
4-
core::TransactionBuilder,
5-
packed::*,
6-
prelude::*,
7-
};
1+
use ckb_testtool::ckb_types::{bytes::Bytes, core::TransactionBuilder, packed::*, prelude::*};
82
use ckb_testtool::context::Context;
93

104
// Include your tests here

0 commit comments

Comments
 (0)