use assert_cmd::Command;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use sdkt_xdr::{encode_ledger_key, LedgerKeyParams};
use serde_json::Value;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
use stellar_xdr::{
ContractDataDurability, ContractDataEntry, ContractExecutable, ContractId, ExtensionPoint,
Hash, LedgerEntry, LedgerEntryData, LedgerEntryExt, Limited, Limits, ScAddress,
ScContractInstance, ScVal, WriteXdr,
};
use tempfile::TempDir;
fn sdkt() -> Command {
Command::cargo_bin("sdkt").expect("sdkt binary built")
}
fn write_fixture(dir: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
let p = dir.path().join(name);
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(content.as_bytes()).unwrap();
p
}
fn assert_valid_json(s: &str) -> Value {
serde_json::from_str::<Value>(s).unwrap_or_else(|e| {
panic!("expected valid JSON, got error: {e}\noutput:\n{s}");
})
}
static WASM_OLD: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/us_old.wasm");
static WASM_NEW: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/us_new.wasm");
const RPC_CONTRACT_ID: &str = "CAE3U7JKESRWZHPEQ72DVNGOQ6WPA7HSPQZL5YV46NPCE4TMUPAGYMEC";
const RPC_CONTRACT_ID_HEX: &str =
"09ba7d2a24a36c9de487f43ab4ce87acf07cf27c32bee2bcf35e22726ca3c06c";
const RPC_WASM_HASH_HEX: &str = "60cddae67f202c19ee7b000c894fd12aa8b44de09ab652f5e188bc0c63a6cf02";
fn read_rpc_request(sock: &mut TcpStream) -> String {
let mut data = Vec::new();
let mut buf = [0u8; 4096];
loop {
let n = sock.read(&mut buf).unwrap_or(0);
if n == 0 {
break;
}
data.extend_from_slice(&buf[..n]);
let text = String::from_utf8_lossy(&data);
if let Some(end) = text.find("\r\n\r\n") {
let len = text[..end]
.lines()
.filter_map(|line| line.split_once(':'))
.find(|(key, _)| key.trim().eq_ignore_ascii_case("content-length"))
.and_then(|(_, value)| value.trim().parse::<usize>().ok())
.unwrap_or(0);
if data.len() >= end + 4 + len {
break;
}
}
}
String::from_utf8_lossy(&data).into_owned()
}
fn encode_ledger_entry(entry: &LedgerEntry) -> String {
let mut bytes = Vec::new();
entry
.write_xdr(&mut Limited::new(&mut bytes, Limits::none()))
.unwrap();
STANDARD.encode(bytes)
}
fn mock_contract_data_xdr() -> String {
let mut wasm_hash = [0u8; 32];
hex::decode_to_slice(RPC_WASM_HASH_HEX, &mut wasm_hash).unwrap();
encode_ledger_entry(&LedgerEntry {
last_modified_ledger_seq: 1,
data: LedgerEntryData::ContractData(ContractDataEntry {
ext: ExtensionPoint::V0,
contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
val: ScVal::ContractInstance(ScContractInstance {
executable: ContractExecutable::Wasm(Hash(wasm_hash)),
storage: None,
}),
}),
ext: LedgerEntryExt::V0,
})
}
fn mock_storage_inspect_rpc() -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let contract_data_key = encode_ledger_key(&LedgerKeyParams::ContractData(
RPC_CONTRACT_ID_HEX.to_string(),
))
.unwrap();
let contract_data_xdr = mock_contract_data_xdr();
thread::spawn(move || {
for conn in listener.incoming() {
let Ok(mut sock) = conn else { break };
let request = read_rpc_request(&mut sock);
let body = request
.split_once("\r\n\r\n")
.map(|(_, body)| body)
.unwrap_or("");
let request_json: Value = serde_json::from_str(body).unwrap_or_default();
let method = request_json["method"].as_str().unwrap_or("");
let response = match method {
"getLatestLedger" => {
r#"{"jsonrpc":"2.0","id":1,"result":{"id":"mock","protocolVersion":22,"sequence":10000}}"#.to_string()
}
"getLedgerEntries" => {
let requested: Vec<&str> = request_json["params"]["keys"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect();
let entries = if requested.contains(&contract_data_key.as_str()) {
serde_json::json!([{
"key": contract_data_key,
"xdr": contract_data_xdr,
"lastModifiedLedgerSeq": 1,
"liveUntilLedgerSeq": 12345
}])
} else {
serde_json::json!([])
};
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"result": { "entries": entries, "latestLedger": 10000 }
})
.to_string()
}
_ => r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"method not found"}}"#.to_string(),
};
let http_response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
response.len(),
response
);
let _ = sock.write_all(http_response.as_bytes());
}
});
url
}
fn run_storage_check(with_abi: bool) -> Value {
let rpc_url = mock_storage_inspect_rpc();
let mut cmd = sdkt();
cmd.args([
"storage",
"check",
RPC_CONTRACT_ID,
"--format",
"json",
"--rpc-url",
])
.arg(rpc_url);
if with_abi {
cmd.arg("--abi").arg(WASM_NEW);
}
let output = cmd.assert().success().get_output().stdout.clone();
assert_valid_json(&String::from_utf8_lossy(&output))
}
fn run_contract_inspect(with_abi: bool) -> Value {
let rpc_url = mock_storage_inspect_rpc();
let mut cmd = sdkt();
cmd.args(["inspect", RPC_CONTRACT_ID, "--format", "json", "--rpc-url"])
.arg(rpc_url);
if with_abi {
cmd.arg("--abi").arg(WASM_NEW);
}
let output = cmd.assert().success().get_output().stdout.clone();
assert_valid_json(&String::from_utf8_lossy(&output))
}
fn assert_fields_unchanged(without_abi: &Value, with_abi: &Value, fields: &[&str]) {
for field in fields {
assert_eq!(
without_abi.get(field),
with_abi.get(field),
"existing field `{field}` changed when --abi was supplied"
);
}
}
mod storage_check_json_schema {
use super::*;
#[test]
fn abi_adds_metadata_without_changing_ttl_entries() {
let without_abi = run_storage_check(false);
let with_abi = run_storage_check(true);
let existing_fields = ["contract_id", "entries"];
assert_fields_unchanged(&without_abi, &with_abi, &existing_fields);
assert_eq!(
without_abi.as_object().unwrap().len(),
existing_fields.len()
);
assert_eq!(
with_abi.as_object().unwrap().len(),
existing_fields.len() + 1
);
let entries = with_abi["entries"]
.as_array()
.expect("entries remains an array with --abi");
assert!(!entries.is_empty());
for field in [
"key",
"current_ttl",
"expiration_time",
"days_remaining",
"extension_cost_stroops",
] {
assert!(
entries[0].get(field).is_some(),
"missing entry field `{field}`"
);
}
assert!(with_abi["abi"]["functions"].is_array());
assert!(with_abi["abi"]["events"].is_array());
assert!(with_abi["abi"]["custom_types"].is_array());
}
}
mod inspect_json_schema {
use super::*;
#[test]
fn abi_adds_full_spec_without_changing_inspection_fields() {
let without_abi = run_contract_inspect(false);
let with_abi = run_contract_inspect(true);
let existing_fields = [
"contract_id",
"wasm_hash",
"wasm_size",
"abi",
"storage_summary",
"ttl_info",
"storage_keys",
];
assert_fields_unchanged(&without_abi, &with_abi, &existing_fields);
assert_eq!(
without_abi.as_object().unwrap().len(),
existing_fields.len()
);
assert_eq!(
with_abi.as_object().unwrap().len(),
existing_fields.len() + 1
);
assert!(
with_abi["storage_keys"].is_array(),
"storage_keys remains an array with --abi"
);
let abi_spec = with_abi["abi_spec"]
.as_object()
.expect("abi_spec contains the full supplied ContractSpec");
for field in ["env_meta", "functions", "custom_types", "events"] {
assert!(
abi_spec.contains_key(field),
"missing ABI spec field `{field}`"
);
}
}
}
mod wasm_inspect {
use super::*;
#[test]
fn json_output_has_required_top_level_fields() {
let out = sdkt()
.args(["wasm", "inspect", WASM_NEW, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
assert!(v.get("file").is_some(), "missing `file` field");
assert!(v.get("metadata").is_some(), "missing `metadata` field");
assert!(v.get("spec").is_some(), "missing `spec` field");
}
#[test]
fn json_metadata_contract_meta_is_an_additive_key_value_array() {
let out = sdkt()
.args(["wasm", "inspect", WASM_NEW, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let meta = v.get("metadata").expect("metadata field");
let entries = meta
.get("contract_meta")
.and_then(|m| m.as_array())
.expect("metadata.contract_meta should be an array");
assert!(
!entries.is_empty(),
"us_new.wasm should expose contractmetav0 entries"
);
for entry in entries {
let key = entry.get("key").and_then(|k| k.as_str());
let value = entry.get("value").and_then(|val| val.as_str());
assert!(key.is_some(), "each entry needs a string `key`: {entry}");
assert!(
value.is_some(),
"each entry needs a string `value`: {entry}"
);
}
let keys: Vec<&str> = entries
.iter()
.filter_map(|e| e.get("key").and_then(|k| k.as_str()))
.collect();
for expected in ["rsver", "rssdkver", "cliver"] {
assert!(
keys.contains(&expected),
"expected contract_meta key `{expected}`, got {keys:?}"
);
}
}
#[test]
fn json_metadata_without_contract_meta_is_an_empty_array() {
let out = sdkt()
.args(["wasm", "inspect", WASM_OLD, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let entries = v
.get("metadata")
.and_then(|m| m.get("contract_meta"))
.and_then(|m| m.as_array())
.expect("metadata.contract_meta should be present and an array");
assert!(
entries.is_empty(),
"us_old.wasm has no contractmetav0, expected [], got {entries:?}"
);
}
#[test]
fn json_metadata_has_stable_fields() {
let out = sdkt()
.args(["wasm", "inspect", WASM_NEW, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let meta = v.get("metadata").expect("metadata field");
let hash = meta
.get("hash")
.and_then(|h| h.as_str())
.expect("metadata.hash");
assert!(
hash.chars().all(|c| c.is_ascii_hexdigit()),
"hash should be hex: {hash}"
);
let size = meta
.get("size_bytes")
.and_then(|s| s.as_u64())
.expect("metadata.size_bytes");
assert!(size > 0, "size_bytes must be > 0");
assert!(
meta.get("exports").is_some_and(|e| e.is_array()),
"metadata.exports should be an array"
);
}
#[test]
fn json_metadata_exports_and_imports_have_stable_lowercase_kind_strings() {
let out = sdkt()
.args(["wasm", "inspect", WASM_NEW, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let meta = v.get("metadata").expect("metadata field");
let exports = meta
.get("exports")
.and_then(|e| e.as_array())
.expect("metadata.exports array");
assert!(!exports.is_empty(), "us_new.wasm should have exports");
for exp in exports {
let kind = exp
.get("kind")
.and_then(|k| k.as_str())
.expect("export kind string");
assert!(
matches!(
kind,
"func" | "table" | "memory" | "global" | "tag" | "func_exact"
),
"export kind should be stable lowercase, got: {kind}"
);
assert!(
!kind.contains('(') && !kind.chars().next().is_some_and(|c| c.is_uppercase()),
"export kind must not leak debug or uppercase: {kind}"
);
}
let imports = meta
.get("imports")
.and_then(|i| i.as_array())
.expect("metadata.imports array");
assert!(!imports.is_empty(), "us_new.wasm should have imports");
for imp in imports {
let kind = imp
.get("kind")
.and_then(|k| k.as_str())
.expect("import kind string");
assert!(
matches!(
kind,
"func" | "table" | "memory" | "global" | "tag" | "func_exact"
),
"import kind should be stable lowercase without index payload, got: {kind}"
);
assert!(
!kind.contains('(') && !kind.chars().next().is_some_and(|c| c.is_uppercase()),
"import kind must not leak debug format like Func(0): {kind}"
);
}
}
#[test]
fn json_spec_has_function_array() {
let out = sdkt()
.args(["wasm", "inspect", WASM_NEW, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let spec = v.get("spec").expect("spec field");
assert!(
spec.get("functions").is_some_and(|f| f.is_array()),
"spec.functions should be an array"
);
}
#[test]
fn json_file_field_is_basename_not_absolute() {
let out = sdkt()
.args(["wasm", "inspect", WASM_NEW, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let file = v.get("file").and_then(|f| f.as_str()).expect("file field");
assert!(
!file.starts_with('/'),
"file should be basename (no absolute path), got: {file}"
);
}
#[test]
fn json_error_output_is_consistent() {
let out = sdkt()
.args(["wasm", "inspect", "/no/such/file.wasm", "--format", "json"])
.assert()
.failure()
.get_output()
.stderr
.clone();
let err_str = String::from_utf8_lossy(&out);
assert!(
err_str.contains("Error") || err_str.contains("No such file"),
"error output should mention the failure: {err_str}"
);
}
}
mod diff_upgrade_safety {
use super::*;
#[test]
fn json_output_has_compatible_boolean() {
let out = sdkt()
.args([
"diff",
"--old-wasm",
WASM_OLD,
"--new-wasm",
WASM_NEW,
"--upgrade-safety",
"--format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let compatible = v
.get("compatible")
.and_then(|c| c.as_bool())
.expect("compatible field");
assert!(!compatible, "fixture pair should be incompatible");
}
#[test]
fn json_breaking_changes_is_array_of_objects() {
let out = sdkt()
.args([
"diff",
"--old-wasm",
WASM_OLD,
"--new-wasm",
WASM_NEW,
"--upgrade-safety",
"--format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let breaking = v
.get("breaking_changes")
.and_then(|b| b.as_array())
.expect("breaking_changes field");
assert!(
!breaking.is_empty(),
"fixtures should have breaking changes"
);
for change in breaking {
assert!(
change.get("kind").is_some_and(|k| k.is_string()),
"each breaking change should have a string `kind`"
);
}
}
#[test]
fn json_non_breaking_changes_is_array_of_objects() {
let out = sdkt()
.args([
"diff",
"--old-wasm",
WASM_OLD,
"--new-wasm",
WASM_NEW,
"--upgrade-safety",
"--format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let non_breaking = v
.get("non_breaking_changes")
.and_then(|b| b.as_array())
.expect("non_breaking_changes field");
assert!(
!non_breaking.is_empty(),
"fixtures should have non-breaking changes"
);
for change in non_breaking {
assert!(
change.get("kind").is_some_and(|k| k.is_string()),
"each non-breaking change should have a string `kind`"
);
}
}
#[test]
fn json_error_when_old_wasm_missing() {
let out = sdkt()
.args([
"diff",
"--old-wasm",
"/no/old.wasm",
"--new-wasm",
WASM_NEW,
"--upgrade-safety",
"--format",
"json",
])
.assert()
.failure()
.get_output()
.stderr
.clone();
let err_str = String::from_utf8_lossy(&out);
assert!(
err_str.contains("Failed to read") || err_str.contains("No such file"),
"error should mention missing file: {err_str}"
);
}
}
mod audit {
use super::*;
#[test]
fn json_output_has_findings_and_summary() {
let dir = TempDir::new().unwrap();
let path = write_fixture(&dir, "bad.rs", "pub fn initialize(admin: Address) { }\n");
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
assert!(v.get("findings").is_some(), "missing `findings` field");
assert!(v.get("summary").is_some(), "missing `summary` field");
}
#[test]
fn json_findings_have_required_fields() {
let dir = TempDir::new().unwrap();
let path = write_fixture(&dir, "bad.rs", "pub fn initialize(admin: Address) { }\n");
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let findings = v
.get("findings")
.and_then(|f| f.as_array())
.expect("findings field");
assert!(!findings.is_empty(), "fixture should trigger findings");
for finding in findings {
assert!(
finding.get("rule_id").is_some_and(|r| r.is_string()),
"finding.rule_id should be a string"
);
assert!(
finding.get("severity").is_some_and(|s| s.is_string()),
"finding.severity should be a string"
);
assert!(
finding.get("message").is_some_and(|m| m.is_string()),
"finding.message should be a string"
);
}
}
#[test]
fn json_summary_has_numeric_counts() {
let dir = TempDir::new().unwrap();
let path = write_fixture(&dir, "bad.rs", "pub fn initialize(admin: Address) { }\n");
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let summary = v.get("summary").expect("summary field");
for field in &["critical", "warning", "info", "total"] {
assert!(
summary.get(field).and_then(|v| v.as_u64()).is_some(),
"summary.{field} should be a number"
);
}
}
#[test]
fn json_clean_source_has_empty_findings() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"ok.rs",
"pub fn balance_of(who: Address) -> u32 { require_auth(); 0 }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let findings = v
.get("findings")
.and_then(|f| f.as_array())
.expect("findings field");
assert!(findings.is_empty(), "clean source should have no findings");
}
#[test]
fn json_error_when_source_missing() {
let out = sdkt()
.args(["audit", "/no/such/contract.rs", "--format", "json"])
.assert()
.failure()
.get_output()
.stderr
.clone();
let err_str = String::from_utf8_lossy(&out);
assert!(
err_str.contains("Failed to read") || err_str.contains("No such file"),
"error should mention missing file: {err_str}"
);
}
}
mod package_validate {
use super::*;
#[test]
fn json_invalid_manifest_has_valid_false_and_error() {
let dir = TempDir::new().unwrap();
let out = sdkt()
.current_dir(dir.path())
.args(["package", "validate", "--format", "json"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let valid = v
.get("valid")
.and_then(|v| v.as_bool())
.expect("valid field");
assert!(!valid, "missing manifest should be invalid");
assert!(
v.get("error").is_some_and(|e| e.is_string()),
"error field should be a string when invalid"
);
}
#[test]
fn json_error_message_is_nonempty() {
let dir = TempDir::new().unwrap();
let out = sdkt()
.current_dir(dir.path())
.args(["package", "validate", "--format", "json"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let error = v
.get("error")
.and_then(|e| e.as_str())
.expect("error field");
assert!(!error.is_empty(), "error message should not be empty");
}
}
mod decode {
use super::*;
#[test]
fn json_error_on_invalid_xdr() {
let out = sdkt()
.args([
"decode",
"AAAA",
"--type",
"TransactionEnvelope",
"--format",
"json",
])
.assert()
.failure()
.get_output()
.stderr
.clone();
let err_str = String::from_utf8_lossy(&out);
assert!(
err_str.contains("XdrParse") || err_str.contains("Error"),
"error should mention parse failure: {err_str}"
);
}
#[test]
fn json_output_is_valid_for_ledger_key_type() {
}
}
mod cross_cutting {
use super::*;
#[test]
fn all_json_output_is_parseable() {
let out = sdkt()
.args(["wasm", "inspect", WASM_NEW, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
assert_valid_json(&String::from_utf8_lossy(&out));
let out = sdkt()
.args([
"diff",
"--old-wasm",
WASM_OLD,
"--new-wasm",
WASM_NEW,
"--upgrade-safety",
"--format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
assert_valid_json(&String::from_utf8_lossy(&out));
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"ok.rs",
"pub fn balance_of(who: Address) -> u32 { require_auth(); 0 }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
assert_valid_json(&String::from_utf8_lossy(&out));
let out = sdkt()
.args(["storage", "estimate", WASM_OLD, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
assert_valid_json(&String::from_utf8_lossy(&out));
}
}
mod storage_estimate {
use super::*;
#[test]
fn json_output_has_required_top_level_fields() {
let out = sdkt()
.args(["storage", "estimate", WASM_OLD, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
assert!(v.get("wasm_path").is_some(), "missing `wasm_path`");
assert!(v.get("ledgers").is_some(), "missing `ledgers`");
assert!(
v.get("cost_per_entry_stroops").is_some(),
"missing `cost_per_entry_stroops`"
);
assert!(
v.get("cost_per_entry_xlm").is_some(),
"missing `cost_per_entry_xlm`"
);
assert!(v.get("classes").is_some(), "missing `classes`");
assert!(v.get("total").is_some(), "missing `total`");
assert!(v.get("spec_metrics").is_some(), "missing `spec_metrics`");
assert!(
v.get("approximation_ceiling").is_some(),
"missing `approximation_ceiling`"
);
}
#[test]
fn json_classes_breakdown_schema_and_types() {
let out = sdkt()
.args(["storage", "estimate", WASM_OLD, "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
let classes = v.get("classes").expect("classes object");
for class_name in ["instance", "persistent", "temporary"] {
let class_obj = classes.get(class_name).unwrap_or_else(|| {
panic!("missing `{class_name}` class object");
});
assert!(
class_obj.get("entry_count").is_some_and(|c| c.is_u64()),
"{class_name}: entry_count should be u64"
);
assert!(
class_obj.get("cost_stroops").is_some_and(|c| c.is_u64()),
"{class_name}: cost_stroops should be u64"
);
assert!(
class_obj.get("cost_xlm").is_some_and(|c| c.is_string()),
"{class_name}: cost_xlm should be string"
);
assert!(
class_obj.get("derivation").is_some_and(|c| c.is_string()),
"{class_name}: derivation should be string"
);
assert!(
class_obj.get("notes").is_some_and(|c| c.is_string()),
"{class_name}: notes should be string"
);
}
assert_eq!(classes["instance"]["entry_count"], 1);
assert_eq!(classes["persistent"]["entry_count"], 0);
assert_eq!(classes["temporary"]["entry_count"], 0);
let total = v.get("total").expect("total object");
assert_eq!(total["baseline_entries"], 1);
assert_eq!(total["cost_stroops"], 1728000);
assert_eq!(total["cost_xlm"], "0.1728");
let metrics = v.get("spec_metrics").expect("spec_metrics object");
assert_eq!(metrics["functions_count"], 2);
assert_eq!(metrics["custom_types_count"], 1);
assert_eq!(metrics["events_count"], 1);
}
#[test]
fn json_custom_ledgers_scales_cost() {
let out = sdkt()
.args([
"storage",
"estimate",
WASM_NEW,
"--ledgers",
"100",
"--format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v = assert_valid_json(&String::from_utf8_lossy(&out));
assert_eq!(v["ledgers"], 100);
assert_eq!(v["cost_per_entry_stroops"], 10000);
assert_eq!(v["cost_per_entry_xlm"], "0.001");
let classes = &v["classes"];
assert_eq!(classes["instance"]["entry_count"], 1);
assert_eq!(classes["instance"]["cost_stroops"], 10000);
assert_eq!(classes["instance"]["cost_xlm"], "0.001");
assert_eq!(classes["persistent"]["entry_count"], 0);
assert_eq!(classes["persistent"]["cost_stroops"], 0);
assert_eq!(classes["persistent"]["cost_xlm"], "0");
let total = &v["total"];
assert_eq!(total["baseline_entries"], 1);
assert_eq!(total["cost_stroops"], 10000);
assert_eq!(total["cost_xlm"], "0.001");
}
}