Skip to content

Commit 04d8b4a

Browse files
authored
[move] Add lcov test coverage emitter for Move code (#21723)
## Description This adds support generating LCOV information based on Move unit tests. It does this by adding a new flag to the `coverage` command. So to get LCOV test coverage data for Move unit tests you would do ``` sui move test --trace-execution sui move coverage lcov ``` This would generate an `lcov.info` file in your package root which you can then use for tools that use LCOV data (e.g., coverage gutters and the like). ## Test plan CI + added a test the existing tracing test to test that we can generate the lcov information.
1 parent effcc07 commit 04d8b4a

File tree

14 files changed

+777
-7
lines changed

14 files changed

+777
-7
lines changed

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

external-crates/move/Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

external-crates/move/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ z3tracer = "0.8.0"
141141
inline_colorization = "0.1.6"
142142
insta = "1.42.0"
143143
zstd = "0.13.2"
144+
lcov = "0.8.1"
144145

145146
bytecode-interpreter-crypto = { path = "crates/bytecode-interpreter-crypto" }
146147
enum-compat-util = { path = "crates/enum-compat-util" }

external-crates/move/crates/move-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ move-symbol-pool.workspace = true
4444
move-unit-test.workspace = true
4545
move-bytecode-viewer.workspace = true
4646
move-model-2.workspace = true
47+
move-trace-format.workspace = true
4748

4849
[dev-dependencies]
4950
datatest-stable.workspace = true

external-crates/move/crates/move-cli/src/base/coverage.rs

Lines changed: 143 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@ use super::reroot_path;
55
use clap::*;
66
use move_compiler::compiled_unit::NamedCompiledModule;
77
use move_coverage::{
8-
coverage_map::CoverageMap, format_csv_summary, format_human_summary,
9-
source_coverage::SourceCoverageBuilder, summary::summarize_inst_cov,
8+
coverage_map::CoverageMap, differential_coverage, format_csv_summary, format_human_summary,
9+
lcov, source_coverage::SourceCoverageBuilder, summary::summarize_inst_cov,
1010
};
1111
use move_disassembler::disassembler::Disassembler;
1212
use move_package::BuildConfig;
13-
use std::path::Path;
13+
use move_trace_format::format::MoveTraceReader;
14+
use std::{
15+
fs::File,
16+
path::{Path, PathBuf},
17+
};
18+
19+
const COVERAGE_FILE_NAME: &str = "lcov.info";
20+
const DIFFERENTIAL: &str = "diff";
1421

1522
#[derive(Parser)]
1623
pub enum CoverageSummaryOptions {
@@ -36,6 +43,19 @@ pub enum CoverageSummaryOptions {
3643
#[clap(long = "module")]
3744
module_name: String,
3845
},
46+
#[clap(name = "lcov")]
47+
Lcov {
48+
/// Compute differential coverage for the provided test name. Lines that are hit by this
49+
/// test only will show as covered, and lines that are hit by both this test and all other
50+
/// tests will show as "uncovered". Otherwise lines are not annotated with coverage
51+
/// information.
52+
#[clap(long = "differential-test")]
53+
differential: Option<String>,
54+
/// Compute coverage for the provided test name. Only this test will contribute to the
55+
/// coverage calculation.
56+
#[clap(long = "only-test", conflicts_with = "differential")]
57+
test: Option<String>,
58+
},
3959
}
4060

4161
/// Inspect test coverage for this package. A previous test run with the `--coverage` flag must
@@ -50,9 +70,16 @@ pub struct Coverage {
5070
impl Coverage {
5171
pub fn execute(self, path: Option<&Path>, config: BuildConfig) -> anyhow::Result<()> {
5272
let path = reroot_path(path)?;
53-
let coverage_map = CoverageMap::from_binary_file(path.join(".coverage_map.mvcov"))?;
73+
74+
// We treat lcov-format coverage differently because it requires traces to be present, and
75+
// we don't use the old trace format for it.
76+
if let CoverageSummaryOptions::Lcov { differential, test } = self.options {
77+
return Self::output_lcov_coverage(path, config, differential, test);
78+
}
79+
5480
let package = config.compile_package(&path, &mut Vec::new())?;
5581
let modules = package.root_modules().map(|unit| &unit.unit.module);
82+
let coverage_map = CoverageMap::from_binary_file(path.join(".coverage_map.mvcov"))?;
5683
match self.options {
5784
CoverageSummaryOptions::Source { module_name } => {
5885
let unit = package.get_module_by_name_from_root(&module_name)?;
@@ -95,7 +122,119 @@ impl Coverage {
95122
disassembler.add_coverage_map(coverage_map.to_unified_exec_map());
96123
println!("{}", disassembler.disassemble()?);
97124
}
125+
CoverageSummaryOptions::Lcov { .. } => {
126+
unreachable!()
127+
}
98128
}
99129
Ok(())
100130
}
131+
132+
pub fn output_lcov_coverage(
133+
path: PathBuf,
134+
mut config: BuildConfig,
135+
differential: Option<String>,
136+
test: Option<String>,
137+
) -> anyhow::Result<()> {
138+
// Make sure we always compile the package in test mode so we get correct source maps.
139+
config.test_mode = true;
140+
let package = config.compile_package(&path, &mut Vec::new())?;
141+
let units: Vec<_> = package
142+
.all_modules()
143+
.cloned()
144+
.map(|unit| (unit.unit, unit.source_path))
145+
.collect();
146+
let traces = path.join("traces");
147+
let sanitize_name = |s: &str| s.replace("::", "__");
148+
let trace_of_test = |test_name: &str| {
149+
let trace_substr_name = format!("{}.", sanitize_name(test_name));
150+
std::fs::read_dir(&traces)?
151+
.filter_map(|entry| {
152+
let entry = entry.unwrap();
153+
let path = entry.path();
154+
if path.is_file()
155+
&& path
156+
.file_name()
157+
.unwrap()
158+
.to_str()
159+
.unwrap()
160+
.contains(&trace_substr_name)
161+
{
162+
Some(path)
163+
} else {
164+
None
165+
}
166+
})
167+
.next()
168+
.ok_or_else(|| {
169+
anyhow::anyhow!(
170+
"No trace found for test {}. Please run with `--coverage` to generate traces.",
171+
test_name
172+
)
173+
})
174+
};
175+
176+
if let Some(test_name) = test {
177+
let mut coverage = lcov::PackageRecordKeeper::new(units, package.file_map.clone());
178+
let trace_path = trace_of_test(&test_name)?;
179+
let file = File::open(&trace_path)?;
180+
let move_trace_reader = MoveTraceReader::new(file)?;
181+
coverage.calculate_coverage(move_trace_reader);
182+
std::fs::write(
183+
&path.join(format!(
184+
"{}.{COVERAGE_FILE_NAME}",
185+
sanitize_name(&test_name)
186+
)),
187+
coverage.lcov_record_string(),
188+
)?;
189+
} else {
190+
let mut coverage =
191+
lcov::PackageRecordKeeper::new(units.clone(), package.file_map.clone());
192+
let differential_test_path = differential
193+
.as_ref()
194+
.map(|s| trace_of_test(s))
195+
.transpose()?;
196+
197+
for entry in std::fs::read_dir(&traces)? {
198+
let entry = entry?;
199+
let path = entry.path();
200+
if path.is_file()
201+
&& differential_test_path
202+
.as_ref()
203+
.is_none_or(|diff_path| diff_path != &path)
204+
{
205+
let file = File::open(&path)?;
206+
let move_trace_reader = MoveTraceReader::new(file)?;
207+
coverage.calculate_coverage(move_trace_reader);
208+
}
209+
}
210+
211+
if let Some(differential_test_name) = differential {
212+
let trace_path =
213+
differential_test_path.expect("Differential test path is already computed");
214+
let file = File::open(&trace_path)?;
215+
let move_trace_reader = MoveTraceReader::new(file)?;
216+
let mut test_coverage =
217+
lcov::PackageRecordKeeper::new(units, package.file_map.clone());
218+
test_coverage.calculate_coverage(move_trace_reader);
219+
220+
let differential_string =
221+
differential_coverage::differential_report(&coverage, &test_coverage)?;
222+
223+
std::fs::write(
224+
&path.join(format!(
225+
"{}.{DIFFERENTIAL}.{COVERAGE_FILE_NAME}",
226+
sanitize_name(&differential_test_name)
227+
)),
228+
differential_string,
229+
)?;
230+
} else {
231+
std::fs::write(
232+
&path.join(COVERAGE_FILE_NAME),
233+
coverage.lcov_record_string(),
234+
)?;
235+
}
236+
};
237+
238+
Ok(())
239+
}
101240
}

external-crates/move/crates/move-cli/src/sandbox/commands/test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,8 @@ pub fn run_one(
243243
let cmd_output = command.output()?;
244244

245245
writeln!(&mut output, "External Command `{}`:", external_cmd)?;
246-
output += std::str::from_utf8(&cmd_output.stdout)?;
247-
output += std::str::from_utf8(&cmd_output.stderr)?;
246+
output += std::str::from_utf8(cmd_output.stdout.trim_ascii_start())?;
247+
output += std::str::from_utf8(cmd_output.stderr.trim_ascii_start())?;
248248

249249
continue;
250250
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
traces
2+
*lcov.info

external-crates/move/crates/move-cli/tests/tracing_tests/tracing-unit-tests/args.exp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Running Move unit tests
66
[ PASS ] 0x1::calls::test_call_return_order
77
[ PASS ] 0x1::calls::test_complex_nested_calls
88
[ PASS ] 0x1::calls::test_return_order
9+
[ PASS ] 0x1::differential_test::f1
10+
[ PASS ] 0x1::differential_test::f21
911
[ PASS ] 0x1::errors::aborter
1012
[ PASS ] 0x1::errors::bad_cast
1113
[ PASS ] 0x1::errors::div_0
@@ -26,5 +28,16 @@ Running Move unit tests
2628
[ PASS ] 0x1::references::test_struct_borrow
2729
[ PASS ] 0x1::references::test_vector_mut_borrow
2830
[ PASS ] 0x1::references::test_vector_mut_borrow_pop
29-
Test result: OK. Total tests: 24; passed: 24; failed: 0
31+
Test result: OK. Total tests: 26; passed: 26; failed: 0
3032
External Command `diff -qr traces saved_traces`:
33+
Only in traces: 0x1__differential_test__f1.json.zst
34+
Only in traces: 0x1__differential_test__f21.json.zst
35+
Command `coverage lcov`:
36+
External Command `wc -l lcov.info`:
37+
6012 lcov.info
38+
Command `coverage lcov --only-test f21`:
39+
External Command `wc -l f21.lcov.info`:
40+
5828 f21.lcov.info
41+
Command `coverage lcov --differential-test f21`:
42+
External Command `wc -l f21.diff.lcov.info`:
43+
26 f21.diff.lcov.info
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,11 @@
11
test -t 1 --trace-execution
22
> diff -qr traces saved_traces
3+
4+
coverage lcov
5+
> wc -l lcov.info
6+
7+
coverage lcov --only-test f21
8+
> wc -l f21.lcov.info
9+
10+
coverage lcov --differential-test f21
11+
> wc -l f21.diff.lcov.info
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module 0x1::differential_test;
2+
3+
#[test]
4+
fun f1() {
5+
f(0);
6+
}
7+
8+
#[test]
9+
fun f21() {
10+
f(21);
11+
}
12+
13+
public fun f(x: u64): u64 {
14+
let x = x + 1;
15+
call(x);
16+
if (x > 10) {
17+
1
18+
} else {
19+
0
20+
}
21+
}
22+
23+
24+
public fun call(x: u64) {
25+
x + 1;
26+
}

0 commit comments

Comments
 (0)