Skip to content

Commit 84cb598

Browse files
ddoktorskiksew1Arcticae
authored
Add --coverage flag (#2369)
<!-- Reference any GitHub issues resolved by this PR --> Closes software-mansion/cairo-coverage#5 ## Introduced changes <!-- A brief description of the changes --> - Added a flag `--coverage` to `test` command that creates coverage report of all non-fuzz tests that passed, using the `cairo-coverage` binary ## Checklist <!-- Make sure all of these are complete --> - [X] Linked relevant issue - [X] Updated relevant documentation - [X] Added relevant tests - [X] Performed self-review of the code - [ ] Added changes to `CHANGELOG.md` --------- Co-authored-by: Karol Sewiło <[email protected]> Co-authored-by: Arcticae <[email protected]> Co-authored-by: Karol Sewilo <[email protected]> Co-authored-by: Tomasz Rejowski <[email protected]>
1 parent 02aa76a commit 84cb598

File tree

20 files changed

+207
-12
lines changed

20 files changed

+207
-12
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ jobs:
7272
run: |
7373
curl -L https://raw.githubusercontent.com/software-mansion/cairo-profiler/main/scripts/install.sh | sh
7474
75+
- name: Install cairo-coverage
76+
run: |
77+
curl -L https://raw.githubusercontent.com/software-mansion/cairo-coverage/main/scripts/install.sh | sh
78+
7579
- uses: actions/checkout@v4
7680
- uses: dtolnay/rust-toolchain@stable
7781
- uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Scarb.lock
1111
*/node_modules
1212
.spr.yml
1313
.snfoundry_cache/
14-
snfoundry_trace/
1514
.snfoundry_versioned_programs/
15+
snfoundry_trace/
16+
coverage/
1617
**/.forge_e2e_cache/

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
#### Added
1313

1414
- Derived `Debug` and `Clone` on `trace.cairo` items
15+
- `--coverage` flag to the `test` command. Saves trace data and then generates coverage report of test cases which pass and are not fuzz tests. You need [cairo-coverage](https://github.com/software-mansion/cairo-coverage) installed on your system.
1516

1617
## [0.29.0] - 2024-08-28
1718

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use anyhow::{Context, Result};
2+
use shared::command::CommandExt;
3+
use std::process::Stdio;
4+
use std::{env, fs, path::PathBuf, process::Command};
5+
6+
pub const COVERAGE_DIR: &str = "coverage";
7+
pub const OUTPUT_FILE_NAME: &str = "coverage.lcov";
8+
9+
pub fn run_coverage(saved_trace_data_paths: &[PathBuf]) -> Result<()> {
10+
let coverage = env::var("CAIRO_COVERAGE")
11+
.map(PathBuf::from)
12+
.ok()
13+
.unwrap_or_else(|| PathBuf::from("cairo-coverage"));
14+
15+
let dir_to_save_coverage = PathBuf::from(COVERAGE_DIR);
16+
fs::create_dir_all(&dir_to_save_coverage).context("Failed to create a coverage dir")?;
17+
let path_to_save_coverage = dir_to_save_coverage.join(OUTPUT_FILE_NAME);
18+
19+
let trace_files: Vec<&str> = saved_trace_data_paths
20+
.iter()
21+
.map(|trace_data_path| {
22+
trace_data_path
23+
.to_str()
24+
.expect("Failed to convert trace data path to string")
25+
})
26+
.collect();
27+
28+
Command::new(coverage)
29+
.arg("--output-path")
30+
.arg(&path_to_save_coverage)
31+
.args(trace_files)
32+
.stdout(Stdio::inherit())
33+
.stderr(Stdio::inherit())
34+
.output_checked()
35+
.with_context(|| {
36+
"cairo-coverage failed to generate coverage - inspect the errors above for more info"
37+
})?;
38+
39+
Ok(())
40+
}

crates/forge-runner/src/forge_config.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,21 @@ pub struct OutputConfig {
3333
pub struct ExecutionDataToSave {
3434
pub trace: bool,
3535
pub profile: bool,
36+
pub coverage: bool,
3637
}
3738

3839
impl ExecutionDataToSave {
3940
#[must_use]
40-
pub fn from_flags(save_trace_data: bool, build_profile: bool) -> Self {
41+
pub fn from_flags(save_trace_data: bool, build_profile: bool, coverage: bool) -> Self {
4142
Self {
4243
trace: save_trace_data,
4344
profile: build_profile,
45+
coverage,
4446
}
4547
}
4648
#[must_use]
4749
pub fn is_vm_trace_needed(&self) -> bool {
48-
self.trace || self.profile
50+
self.trace || self.profile || self.coverage
4951
}
5052
}
5153

crates/forge-runner/src/lib.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use crate::build_trace_data::test_sierra_program_path::VersionedProgramPath;
2+
use crate::coverage_api::run_coverage;
23
use crate::forge_config::{ExecutionDataToSave, ForgeConfig, TestRunnerConfig};
34
use crate::fuzzer::RandomFuzzer;
45
use crate::running::{run_fuzz_test, run_test};
56
use crate::test_case_summary::TestCaseSummary;
6-
use anyhow::Result;
7+
use anyhow::{anyhow, Result};
78
use build_trace_data::save_trace_data;
89
use cairo_lang_sierra::program::{ConcreteTypeLongId, Function, TypeDeclaration};
910
use camino::Utf8Path;
@@ -14,14 +15,17 @@ use package_tests::with_config_resolved::{
1415
TestCaseWithResolvedConfig, TestTargetWithResolvedConfig,
1516
};
1617
use profiler_api::run_profiler;
18+
use shared::print::print_as_warning;
1719
use std::collections::HashMap;
20+
use std::path::PathBuf;
1821
use std::sync::Arc;
1922
use test_case_summary::{AnyTestCaseSummary, Fuzzing};
2023
use tokio::sync::mpsc::{channel, Sender};
2124
use tokio::task::JoinHandle;
2225
use universal_sierra_compiler_api::AssembledProgramWithDebugInfo;
2326

2427
pub mod build_trace_data;
28+
pub mod coverage_api;
2529
pub mod expected_result;
2630
pub mod forge_config;
2731
pub mod package_tests;
@@ -57,7 +61,7 @@ pub trait TestCaseFilter {
5761
pub fn maybe_save_trace_and_profile(
5862
result: &AnyTestCaseSummary,
5963
execution_data_to_save: ExecutionDataToSave,
60-
) -> Result<()> {
64+
) -> Result<Option<PathBuf>> {
6165
if let AnyTestCaseSummary::Single(TestCaseSummary::Passed {
6266
name, trace_data, ..
6367
}) = result
@@ -67,6 +71,21 @@ pub fn maybe_save_trace_and_profile(
6771
if execution_data_to_save.profile {
6872
run_profiler(name, &trace_path)?;
6973
}
74+
return Ok(Some(trace_path));
75+
}
76+
}
77+
Ok(None)
78+
}
79+
80+
pub fn maybe_generate_coverage(
81+
execution_data_to_save: ExecutionDataToSave,
82+
saved_trace_data_paths: &[PathBuf],
83+
) -> Result<()> {
84+
if execution_data_to_save.coverage {
85+
if saved_trace_data_paths.is_empty() {
86+
print_as_warning(&anyhow!("No trace data to generate coverage from"));
87+
} else {
88+
run_coverage(saved_trace_data_paths)?;
7089
}
7190
}
7291
Ok(())

crates/forge/src/combine_configs.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub fn combine_configs(
1818
detailed_resources: bool,
1919
save_trace_data: bool,
2020
build_profile: bool,
21+
coverage: bool,
2122
max_n_steps: Option<u32>,
2223
contracts_data: ContractsData,
2324
cache_dir: Utf8PathBuf,
@@ -27,6 +28,7 @@ pub fn combine_configs(
2728
let execution_data_to_save = ExecutionDataToSave::from_flags(
2829
save_trace_data || forge_config_from_scarb.save_trace_data,
2930
build_profile || forge_config_from_scarb.build_profile,
31+
coverage || forge_config_from_scarb.coverage,
3032
);
3133

3234
ForgeConfig {
@@ -65,6 +67,7 @@ mod tests {
6567
false,
6668
false,
6769
false,
70+
false,
6871
None,
6972
Default::default(),
7073
Default::default(),
@@ -78,6 +81,7 @@ mod tests {
7881
false,
7982
false,
8083
false,
84+
false,
8185
None,
8286
Default::default(),
8387
Default::default(),
@@ -102,6 +106,7 @@ mod tests {
102106
false,
103107
false,
104108
false,
109+
false,
105110
None,
106111
Default::default(),
107112
Default::default(),
@@ -140,6 +145,7 @@ mod tests {
140145
detailed_resources: true,
141146
save_trace_data: true,
142147
build_profile: true,
148+
coverage: true,
143149
max_n_steps: Some(1_000_000),
144150
};
145151

@@ -150,6 +156,7 @@ mod tests {
150156
false,
151157
false,
152158
false,
159+
false,
153160
None,
154161
Default::default(),
155162
Default::default(),
@@ -174,6 +181,7 @@ mod tests {
174181
execution_data_to_save: ExecutionDataToSave {
175182
trace: true,
176183
profile: true,
184+
coverage: true,
177185
},
178186
versioned_programs_dir: Default::default(),
179187
}),
@@ -191,6 +199,7 @@ mod tests {
191199
detailed_resources: false,
192200
save_trace_data: false,
193201
build_profile: false,
202+
coverage: false,
194203
max_n_steps: Some(1234),
195204
};
196205
let config = combine_configs(
@@ -200,6 +209,7 @@ mod tests {
200209
true,
201210
true,
202211
true,
212+
true,
203213
Some(1_000_000),
204214
Default::default(),
205215
Default::default(),
@@ -225,6 +235,7 @@ mod tests {
225235
execution_data_to_save: ExecutionDataToSave {
226236
trace: true,
227237
profile: true,
238+
coverage: true,
228239
},
229240
versioned_programs_dir: Default::default(),
230241
}),

crates/forge/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,14 @@ pub struct TestArgs {
126126
#[arg(long)]
127127
save_trace_data: bool,
128128

129-
/// Build profiles of all test which have passed and are not fuzz tests using the cairo-profiler
129+
/// Build profiles of all tests which have passed and are not fuzz tests using the cairo-profiler
130130
#[arg(long)]
131131
build_profile: bool,
132132

133+
/// Generate a coverage report for the executed tests which have passed and are not fuzz tests using the cairo-coverage
134+
#[arg(long)]
135+
coverage: bool,
136+
133137
/// Number of maximum steps during a single test. For fuzz tests this value is applied to each subtest separately.
134138
#[arg(long)]
135139
max_n_steps: Option<u32>,

crates/forge/src/run_tests/package.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ impl RunForPackageArgs {
6565
args.detailed_resources,
6666
args.save_trace_data,
6767
args.build_profile,
68+
args.coverage,
6869
args.max_n_steps,
6970
contracts_data,
7071
cache_dir.clone(),

crates/forge/src/run_tests/test_target.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use anyhow::Result;
22
use cairo_lang_runner::RunnerError;
33
use forge_runner::{
44
forge_config::ForgeConfig,
5-
function_args, maybe_save_trace_and_profile, maybe_save_versioned_program,
5+
function_args, maybe_generate_coverage, maybe_save_trace_and_profile,
6+
maybe_save_versioned_program,
67
package_tests::with_config_resolved::TestTargetWithResolvedConfig,
78
printing::print_test_result,
89
run_for_test_case,
@@ -84,13 +85,21 @@ pub async fn run_for_test_target(
8485
}
8586

8687
let mut results = vec![];
88+
let mut saved_trace_data_paths = vec![];
8789
let mut interrupted = false;
8890

8991
while let Some(task) = tasks.next().await {
9092
let result = task??;
9193

9294
print_test_result(&result, forge_config.output_config.detailed_resources);
93-
maybe_save_trace_and_profile(&result, forge_config.output_config.execution_data_to_save)?;
95+
96+
let trace_path = maybe_save_trace_and_profile(
97+
&result,
98+
forge_config.output_config.execution_data_to_save,
99+
)?;
100+
if let Some(path) = trace_path {
101+
saved_trace_data_paths.push(path);
102+
}
94103

95104
if result.is_failed() && forge_config.test_runner_config.exit_first {
96105
interrupted = true;
@@ -100,6 +109,11 @@ pub async fn run_for_test_target(
100109
results.push(result);
101110
}
102111

112+
maybe_generate_coverage(
113+
forge_config.output_config.execution_data_to_save,
114+
&saved_trace_data_paths,
115+
)?;
116+
103117
let summary = TestTargetSummary {
104118
test_case_summaries: results,
105119
};

0 commit comments

Comments
 (0)