Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions cargo-budget-report/src/contract_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,199 @@ mod tests {
assert!(msg.contains("shared-types"));
assert!(msg.contains("cdylib"));
}

// ── runtime-symbol classification ─────────────────────────────────

#[test]
fn runtime_symbols_are_recognised_by_name() {
for name in [
"memory",
"__heap_base",
"__data_end",
"__stack_pointer",
"__rust_alloc",
"_start",
"_",
"__other_compiler_symbol",
] {
assert!(is_runtime_symbol(name), "expected runtime symbol: {name}");
}

for name in [
"swap",
"deposit",
"memory_usage",
"data_end",
"Memory",
"noop",
] {
assert!(!is_runtime_symbol(name), "expected contract export: {name}");
}
}

// ── export scanning ───────────────────────────────────────────────

#[test]
fn runtime_symbols_are_filtered_out_of_the_function_list() {
let wasm = wasm_with_exports(&["__data_end", "swap", "_start"], true);
match scan_wasm_exports(&wasm).unwrap() {
ExportScan::Functions(fns) => {
assert_eq!(
fns.into_iter().collect::<Vec<_>>(),
vec!["swap"],
"neither the toolchain symbols nor the `memory` export count"
);
}
other => panic!("expected Functions, got {other:?}"),
}
}

#[test]
fn function_exports_are_returned_in_a_stable_sorted_order() {
let names: Vec<String> = (0..10).map(|i| format!("f{i}")).collect();
let wasm = {
let borrowed: Vec<&str> = names.iter().map(String::as_str).collect();
wasm_with_exports(&borrowed, false)
};

match scan_wasm_exports(&wasm).unwrap() {
ExportScan::Functions(fns) => {
let collected: Vec<String> = fns.into_iter().collect();
assert_eq!(collected, names, "every export is kept, in sorted order");
}
other => panic!("expected Functions, got {other:?}"),
}
}

#[test]
fn contract_names_containing_underscores_are_kept() {
let wasm = wasm_with_exports(&["transfer_from", "burn_from_account"], false);
match scan_wasm_exports(&wasm).unwrap() {
ExportScan::Functions(fns) => {
assert_eq!(
fns.into_iter().collect::<Vec<_>>(),
vec!["burn_from_account", "transfer_from"],
"only a leading underscore marks a toolchain symbol"
);
}
other => panic!("expected Functions, got {other:?}"),
}
}

/// A minimal module with no export section at all — the shape a plain
/// `rlib` build leaves behind.
fn wasm_without_export_section() -> Vec<u8> {
let mut module = Vec::new();
module.extend_from_slice(b"\0asm");
module.extend_from_slice(&1u32.to_le_bytes());
section(&mut module, 1, &[0x01, 0x60, 0x00, 0x00]);
section(&mut module, 3, &[0x01, 0x00]);
section(&mut module, 10, &[0x01, 0x02, 0x00, 0x0b]);
module
}

#[test]
fn a_module_without_an_export_section_reports_no_function_exports() {
let wasm = wasm_without_export_section();
assert_eq!(
scan_wasm_exports(&wasm).unwrap(),
ExportScan::NoFunctionExports
);
assert!(
ExportScan::NoFunctionExports.diagnostic("pkg").is_some(),
"a module with no exports must still produce a diagnostic"
);
}

#[test]
fn invalid_wasm_is_reported_as_an_error_not_a_scan_result() {
assert!(
scan_wasm_exports(b"not a wasm module").is_err(),
"garbage bytes must not be classified"
);
assert!(
scan_wasm_exports(b"\0asm").is_err(),
"a truncated header is an error"
);
}

// ── diagnostics ───────────────────────────────────────────────────

#[test]
fn runtime_symbol_list_is_sorted_for_stable_messages() {
let wasm = wasm_with_exports(&["_start", "memory", "__data_end"], false);
match scan_wasm_exports(&wasm).unwrap() {
ExportScan::OnlyRuntimeSymbols { found } => {
assert_eq!(
found,
vec![
"__data_end".to_string(),
"_start".to_string(),
"memory".to_string()
],
"the list is sorted so the message is stable across builds"
);
}
other => panic!("expected OnlyRuntimeSymbols, got {other:?}"),
}
}

#[test]
fn runtime_symbols_diagnostic_lists_every_name() {
let scan = ExportScan::OnlyRuntimeSymbols {
found: vec!["__heap_base".into(), "_start".into()],
};
let message = scan.diagnostic("my-contract").unwrap();
assert!(message.contains("my-contract"), "got: {message}");
assert!(
message.contains("Exports found: __heap_base, _start"),
"got: {message}"
);
assert!(message.contains("#[contractimpl]"), "got: {message}");
}

#[test]
fn runtime_symbols_diagnostic_handles_an_empty_list() {
let scan = ExportScan::OnlyRuntimeSymbols { found: Vec::new() };
let message = scan.diagnostic("my-contract").unwrap();
assert!(message.contains("Package 'my-contract'"), "got: {message}");
assert!(message.contains("Exports found:"), "got: {message}");
}

#[test]
fn no_function_exports_diagnostic_names_the_missing_attributes() {
let message = ExportScan::NoFunctionExports
.diagnostic("my-contract")
.unwrap();
assert!(message.contains("my-contract"), "got: {message}");
assert!(message.contains("plain library"), "got: {message}");
assert!(message.contains("#[contract]"), "got: {message}");
assert!(message.contains("crate-type"), "got: {message}");
}

#[test]
fn cdylib_message_offers_the_rlib_alternative() {
let message = not_a_cdylib_message("shared-types");
assert!(message.contains("crate-type"), "got: {message}");
assert!(message.contains("rlib"), "got: {message}");
assert!(message.contains("Skipping"), "got: {message}");
}

#[test]
fn export_scan_clones_and_compares_by_value() {
let fns: BTreeSet<String> = ["swap".to_string()].into_iter().collect();
let scan = ExportScan::Functions(fns.clone());

assert_eq!(scan, ExportScan::Functions(fns));
assert_eq!(scan.clone(), scan);
assert_ne!(scan, ExportScan::NoFunctionExports);
assert_ne!(
ExportScan::NoFunctionExports,
ExportScan::OnlyRuntimeSymbols { found: Vec::new() }
);
assert!(
format!("{scan:?}").contains("Functions"),
"the scan result stays debuggable for callers that log it"
);
}
}
Loading
Loading