Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code coverage support for tests (scrypto coverage command) #1640

Merged
merged 3 commits into from Nov 10, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/template/.gitignore
@@ -1 +1,2 @@
/target
/coverage
2 changes: 2 additions & 0 deletions radix-engine-common/Cargo.toml
Expand Up @@ -51,6 +51,8 @@ radix_engine_fuzzing = ["arbitrary", "serde", "bnum/arbitrary", "bnum/serde", "s
resource_tracker=[]
full_math_benches = [ "dep:rug", "dep:ethnum"]

coverage = []

# Ref: https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
[lib]
doctest = false
Expand Down
6 changes: 6 additions & 0 deletions radix-engine-common/src/constants/transaction_execution.rs
Expand Up @@ -26,10 +26,16 @@ pub const MAX_TRACK_SUBSTATE_TOTAL_BYTES: usize = 64 * 1024 * 1024;
pub const MAX_SUBSTATE_KEY_SIZE: usize = 1024;

/// The maximum substate read and write size.
#[cfg(not(feature = "coverage"))]
pub const MAX_SUBSTATE_VALUE_SIZE: usize = 2 * 1024 * 1024;
#[cfg(feature = "coverage")]
pub const MAX_SUBSTATE_VALUE_SIZE: usize = 64 * 1024 * 1024;

/// The maximum invoke payload size.
#[cfg(not(feature = "coverage"))]
pub const MAX_INVOKE_PAYLOAD_SIZE: usize = 1 * 1024 * 1024;
#[cfg(feature = "coverage")]
pub const MAX_INVOKE_PAYLOAD_SIZE: usize = 32 * 1024 * 1024;

/// The proposer's share of tips
pub const TIPS_PROPOSER_SHARE_PERCENTAGE: u8 = 100;
Expand Down
3 changes: 3 additions & 0 deletions radix-engine-common/src/constants/wasm.rs
@@ -1,5 +1,8 @@
/// The maximum memory size (per call frame): 64 * 64KiB = 4MiB
#[cfg(not(feature = "coverage"))]
pub const MAX_MEMORY_SIZE_IN_PAGES: u32 = 64;
#[cfg(feature = "coverage")]
pub const MAX_MEMORY_SIZE_IN_PAGES: u32 = 512;

/// The maximum initial table size
pub const MAX_INITIAL_TABLE_SIZE: u32 = 1024;
Expand Down
3 changes: 3 additions & 0 deletions radix-engine/Cargo.toml
Expand Up @@ -95,6 +95,9 @@ radix_engine_tests = []

full_wasm_benchmarks = []

# This flag disables package size limit, memory size limit and fee limit
coverage = [ "radix-engine-common/coverage" ]

# Ref: https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
[lib]
doctest = false
Expand Down
14 changes: 14 additions & 0 deletions radix-engine/src/transaction/transaction_executor.rs
Expand Up @@ -59,6 +59,7 @@ pub struct CostingParameters {
}

impl Default for CostingParameters {
#[cfg(not(feature = "coverage"))]
fn default() -> Self {
Self {
execution_cost_unit_price: EXECUTION_COST_UNIT_PRICE_IN_XRD.try_into().unwrap(),
Expand All @@ -71,6 +72,19 @@ impl Default for CostingParameters {
archive_storage_price: ARCHIVE_STORAGE_PRICE_IN_XRD.try_into().unwrap(),
}
}
#[cfg(feature = "coverage")]
fn default() -> Self {
Self {
execution_cost_unit_price: Decimal::zero(),
execution_cost_unit_limit: u32::MAX,
execution_cost_unit_loan: u32::MAX,
finalization_cost_unit_price: Decimal::zero(),
finalization_cost_unit_limit: u32::MAX,
usd_price: USD_PRICE_IN_XRD.try_into().unwrap(),
state_storage_price: Decimal::zero(),
archive_storage_price: Decimal::zero(),
}
Copy link
Member

Choose a reason for hiding this comment

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

Due to the difference in configuration, some tests may fail in coverage mode but not in regular test mode. I guess it's not a big deal for most cases. We will need to make sure the --no-fail-fast flag is provided when running coverage.

}
}

impl CostingParameters {
Expand Down
29 changes: 29 additions & 0 deletions radix-engine/src/utils/coverage.rs
@@ -0,0 +1,29 @@
use crate::types::*;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;

#[cfg(feature = "coverage")]
pub fn save_coverage_data(blueprint_name: &String, coverage_data: &Vec<u8>) {
if let Some(dir) = env::var_os("COVERAGE_DIRECTORY") {
let mut file_path = Path::new(&dir).to_path_buf();
file_path.push(blueprint_name);

// Check if the blueprint directory exists, if not create it
if !file_path.exists() {
// error is ignored because when multiple tests are running it may fail
fs::create_dir(&file_path).ok();
}

// Write .profraw binary data
let file_name = hash(&coverage_data);
let file_name: String = file_name.0[..16]
.iter()
.map(|byte| format!("{:02x}", byte))
.collect();
file_path.push(format!("{}.profraw", file_name));
let mut file = File::create(file_path).unwrap();
file.write_all(&coverage_data).unwrap();
}
}
4 changes: 4 additions & 0 deletions radix-engine/src/utils/mod.rs
@@ -1,8 +1,12 @@
#[cfg(feature = "coverage")]
mod coverage;
mod macros;
mod native_blueprint_call_validator;
mod package_extractor;
mod panics;

#[cfg(feature = "coverage")]
pub use coverage::*;
pub use macros::*;
pub use native_blueprint_call_validator::*;
pub use package_extractor::*;
Expand Down
21 changes: 12 additions & 9 deletions radix-engine/src/vm/wasm/prepare.rs
Expand Up @@ -983,15 +983,18 @@ impl WasmModule {
mut self,
rules: &R,
) -> Result<Self, PrepareError> {
let backend = gas_metering::host_function::Injector::new(
MODULE_ENV_NAME,
COSTING_CONSUME_WASM_EXECUTION_UNITS_FUNCTION_NAME,
);
gas_metering::inject(&mut self.module, backend, rules).map_err(|err| {
PrepareError::RejectedByInstructionMetering {
reason: err.to_string(),
}
})?;
#[cfg(not(feature = "coverage"))]
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, did you run into any issue with gas metering ingestion for the coverage-enabled wasm code?

Copy link
Contributor

@bbarwik bbarwik Nov 10, 2023

Choose a reason for hiding this comment

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

Yes. When transaction fails for example due to out of gas then dump_coverage cannot be called because it will fail due to out of gas. When this is commented, the function dump_coverage is not using any build-in native functions so it's going to work even in case of transaction failure.
The other option is to disable gas/native functions only when dump_coverage is called.

Copy link
Member

Choose a reason for hiding this comment

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

That makes sense. I was hoping it wasn't an error with the instrumentation itself, otherwise we have a problem. All is good.

{
let backend = gas_metering::host_function::Injector::new(
MODULE_ENV_NAME,
COSTING_CONSUME_WASM_EXECUTION_UNITS_FUNCTION_NAME,
);
gas_metering::inject(&mut self.module, backend, rules).map_err(|err| {
PrepareError::RejectedByInstructionMetering {
reason: err.to_string(),
}
})?;
}

Ok(self)
}
Expand Down
39 changes: 34 additions & 5 deletions radix-engine/src/vm/wasm/wasmer.rs
@@ -1,5 +1,7 @@
use crate::errors::InvokeError;
use crate::types::*;
#[cfg(feature = "coverage")]
use crate::utils::save_coverage_data;
use crate::vm::wasm::constants::*;
use crate::vm::wasm::errors::*;
use crate::vm::wasm::traits::*;
Expand Down Expand Up @@ -835,13 +837,40 @@ impl WasmInstance for WasmerInstance {
.map_err(|e| {
let err: InvokeError<WasmRuntimeError> = e.into();
err
})?;
});

let result = match return_data {
Ok(data) => {
if let Some(v) = data.as_ref().get(0).and_then(|x| x.i64()) {
read_slice(&self.instance, Slice::transmute_i64(v))
.map_err(InvokeError::SelfError)
} else {
Err(InvokeError::SelfError(WasmRuntimeError::InvalidWasmPointer))
}
}
Err(err) => Err(err),
};

if let Some(v) = return_data.as_ref().get(0).and_then(|x| x.i64()) {
read_slice(&self.instance, Slice::transmute_i64(v)).map_err(InvokeError::SelfError)
} else {
Err(InvokeError::SelfError(WasmRuntimeError::InvalidWasmPointer))
#[cfg(feature = "coverage")]
if let Ok(dump_coverage) = self.instance.exports.get_function("dump_coverage") {
if let Ok(blueprint_buffer) = runtime.actor_get_blueprint_name() {
let blueprint_name =
String::from_utf8(runtime.buffer_consume(blueprint_buffer.id()).unwrap())
.unwrap();

let mut ret = dump_coverage
.call(&[])
.unwrap()
.as_ref()
.get(0)
.and_then(|x| x.i64())
.unwrap();
let coverage_data = read_slice(&self.instance, Slice::transmute_i64(ret)).unwrap();
save_coverage_data(&blueprint_name, &coverage_data);
}
}

result
}
}

Expand Down
48 changes: 38 additions & 10 deletions radix-engine/src/vm/wasm/wasmi.rs
Expand Up @@ -11,6 +11,8 @@ use wasmi::*;

use crate::errors::InvokeError;
use crate::types::*;
#[cfg(feature = "coverage")]
use crate::utils::save_coverage_data;
use crate::vm::wasm::constants::*;
use crate::vm::wasm::errors::*;
use crate::vm::wasm::traits::*;
Expand Down Expand Up @@ -1537,21 +1539,47 @@ impl WasmInstance for WasmiInstance {
.collect();
let mut ret = [Value::I64(0)];

let _result = func
let call_result = func
.call(self.store.as_context_mut(), &input, &mut ret)
.map_err(|e| {
let err: InvokeError<WasmRuntimeError> = e.into();
err
})?;

match i64::try_from(ret[0]) {
Ok(ret) => read_slice(
self.store.as_context_mut(),
self.memory,
Slice::transmute_i64(ret),
),
_ => Err(InvokeError::SelfError(WasmRuntimeError::InvalidWasmPointer)),
});

let result = match call_result {
Ok(_) => match i64::try_from(ret[0]) {
Ok(ret) => read_slice(
self.store.as_context_mut(),
self.memory,
Slice::transmute_i64(ret),
),
_ => Err(InvokeError::SelfError(WasmRuntimeError::InvalidWasmPointer)),
},
Err(err) => Err(err),
};

#[cfg(feature = "coverage")]
if let Ok(dump_coverage) = self.get_export_func("dump_coverage") {
if let Ok(blueprint_buffer) = runtime.actor_get_blueprint_name() {
let blueprint_name =
String::from_utf8(runtime.buffer_consume(blueprint_buffer.id()).unwrap())
.unwrap();

let mut ret = [Value::I64(0)];
dump_coverage
.call(self.store.as_context_mut(), &[], &mut ret)
.unwrap();
let coverage_data = read_slice(
self.store.as_context_mut(),
self.memory,
Slice::transmute_i64(i64::try_from(ret[0]).unwrap()),
)
.unwrap();
save_coverage_data(&blueprint_name, &coverage_data);
}
}

result
}
}

Expand Down
1 change: 1 addition & 0 deletions scrypto-unit/Cargo.toml
Expand Up @@ -28,6 +28,7 @@ lru = ["radix-engine/lru", "radix-engine-queries/lru"]

rocksdb = ["radix-engine-stores/rocksdb"]
post_run_db_check = []
coverage = ["radix-engine/coverage"]

[lib]
doctest = false
Expand Down
97 changes: 70 additions & 27 deletions scrypto-unit/src/test_runner.rs
Expand Up @@ -79,31 +79,7 @@ impl Compile {
package_dir: P,
env_vars: sbor::rust::collections::BTreeMap<String, String>,
) -> (Vec<u8>, PackageDefinition) {
// Build
let status = Command::new("cargo")
.envs(env_vars)
.current_dir(package_dir.as_ref())
.args([
"build",
"--target",
"wasm32-unknown-unknown",
"--release",
"--features",
"scrypto/log-error,scrypto/log-warn,scrypto/log-info,scrypto/log-debug,scrypto/log-trace"
])
.status()
.unwrap_or_else(|error| {
panic!(
"Compiling \"{:?}\" failed with \"{:?}\"",
package_dir.as_ref(),
error
)
});
if !status.success() {
panic!("Failed to compile package: {:?}", package_dir.as_ref());
}

// Find wasm path
// Find wasm name
let mut cargo = package_dir.as_ref().to_owned();
cargo.push("Cargo.toml");
let wasm_name = if cargo.exists() {
Expand All @@ -122,12 +98,79 @@ impl Compile {
.to_owned()
.replace("-", "_")
};
let mut path = PathBuf::from_str(&get_cargo_target_directory(&cargo)).unwrap(); // Infallible;

let mut path = PathBuf::from_str(&get_cargo_target_directory(&cargo)).unwrap();
path.push("wasm32-unknown-unknown");
path.push("release");
path.push(wasm_name);
path.push(&wasm_name);
path.set_extension("wasm");

#[cfg(feature = "coverage")]
// Check if binary exists in coverage directory, if it doesn't only then build it
{
let mut coverage_path = PathBuf::from_str(&get_cargo_target_directory(&cargo)).unwrap();
coverage_path.pop();
coverage_path.push("coverage");
coverage_path.push("wasm32-unknown-unknown");
coverage_path.push("release");
coverage_path.push(wasm_name);
coverage_path.set_extension("wasm");
if coverage_path.is_file() {
let code = fs::read(&coverage_path).unwrap_or_else(|err| {
panic!(
"Failed to read built WASM from path {:?} - {:?}",
&path, err
)
});
coverage_path.set_extension("rpd");
let definition = fs::read(&coverage_path).unwrap_or_else(|err| {
panic!(
"Failed to read package definition from path {:?} - {:?}",
&coverage_path, err
)
});
let definition = manifest_decode(&definition).unwrap_or_else(|err| {
panic!(
"Failed to parse package definition from path {:?} - {:?}",
&coverage_path, err
)
});
return (code, definition);
}
};

// Build
let features = vec![
"scrypto/log-error",
"scrypto/log-warn",
"scrypto/log-info",
"scrypto/log-debug",
"scrypto/log-trace",
];

let status = Command::new("cargo")
.envs(env_vars)
.current_dir(package_dir.as_ref())
.args([
"build",
"--target",
"wasm32-unknown-unknown",
"--release",
"--features",
&features.join(","),
])
.status()
.unwrap_or_else(|error| {
panic!(
"Compiling \"{:?}\" failed with \"{:?}\"",
package_dir.as_ref(),
error
)
});
if !status.success() {
panic!("Failed to compile package: {:?}", package_dir.as_ref());
}

// Extract schema
let code = fs::read(&path).unwrap_or_else(|err| {
panic!(
Expand Down