use svmscope::{Check, Cmp, Fixture, Mutation, Replay, Scenario};
const FIXTURE: &str = include_str!("fixtures/counter_increment.json");
const COUNTER_PDA: &str = "FLFUsWEUjARKvDrFf9WtF7k1tCxHqtCpfJU2krL6z7vY";
const REVERT_FIXTURE: &str = include_str!("fixtures/vesting_precliff_revert.json");
fn replay() -> Replay {
Replay::from_fixture(&Fixture::from_json(FIXTURE).expect("fixture parses"))
.expect("fixture loads")
}
#[test]
fn frozen_replay_matches_the_onchain_outcome() {
let out = replay()
.verify("faithful", &[], &[Check::matches_onchain()])
.unwrap();
assert!(out.pass, "{out:?}");
}
#[test]
fn named_field_asserts_resolve_offline_via_the_captured_idl() {
let out = replay()
.verify(
"count increments",
&[],
&[Check::account(COUNTER_PDA)
.field("count", Cmp::eq(2))
.field_delta("count", Cmp::eq(1))
.build()],
)
.unwrap();
assert!(out.pass, "{out:?}");
}
#[test]
fn mutations_and_the_check_dsl_compose() {
let out = replay()
.verify(
"patched counter reaches 100",
&[Mutation::patch(
COUNTER_PDA,
8,
99u64.to_le_bytes().to_vec(),
)],
&[
Check::success(),
Check::log_contains("incremented by 1 to : 100"),
Check::account(COUNTER_PDA)
.field("count", Cmp::eq(100))
.build(),
Check::compute_units(Cmp::le(200_000)),
],
)
.unwrap();
assert!(out.pass, "{out:?}");
}
#[test]
fn named_field_mutations_mirror_named_field_asserts() {
let out = replay()
.verify(
"field mutation reaches 100",
&[Mutation::field(COUNTER_PDA, "count", 99)],
&[
Check::success(),
Check::account(COUNTER_PDA)
.field("count", Cmp::eq(100))
.build(),
],
)
.unwrap();
assert!(out.pass, "{out:?}");
}
#[test]
fn field_mutation_errors_are_hard_and_specific() {
let err = replay()
.verify(
"typo'd field",
&[Mutation::field(COUNTER_PDA, "countt", 1)],
&[Check::success()],
)
.unwrap_err();
assert!(
matches!(&err, svmscope::Error::UnknownField { available, .. } if available.iter().any(|f| f == "count")),
"{err}"
);
let err = replay()
.verify(
"negative into u64",
&[Mutation::field(COUNTER_PDA, "count", -1)],
&[Check::success()],
)
.unwrap_err();
assert!(
matches!(err, svmscope::Error::FieldValueOutOfRange { .. }),
"{err}"
);
}
#[test]
fn time_travel_warps_the_clock() {
let mut replay = replay();
let before = replay.describe_clock();
replay.advance_seconds(30 * 86_400);
let after = replay.describe_clock();
assert_ne!(before, after);
assert!(replay.run().unwrap().result.success);
}
#[test]
fn a_typoed_mutation_address_is_a_hard_error_not_a_passing_revert() {
let typo = "7NypoTypoTypoTypoTypoTypoTypoTypoTypoTypoTyp";
let err = replay()
.run_suite(&[Scenario::new("drain")
.mutate(Mutation::lamports(typo, 0))
.check(Check::revert())])
.unwrap_err();
assert!(
matches!(
err,
svmscope::Error::InvalidAddress(_) | svmscope::Error::MutationTargetMissing(_)
),
"{err}"
);
}
#[test]
fn matches_onchain_alone_holds_for_a_reverting_recorded_outcome() {
let replay = Replay::from_fixture(&Fixture::from_json(REVERT_FIXTURE).unwrap()).unwrap();
let alone = replay
.verify("matches on-chain", &[], &[Check::matches_onchain()])
.unwrap();
assert!(
alone.pass,
"matches_onchain alone should pass on a revert: {alone:?}"
);
let paired = replay
.verify(
"revert + match",
&[],
&[Check::revert(), Check::matches_onchain()],
)
.unwrap();
assert!(paired.pass, "{paired:?}");
assert!(replay.recorded().is_some_and(|r| !r.success));
}
#[test]
fn a_typoed_assert_address_fails_the_check_instead_of_silently_passing() {
let absent = "So11111111111111111111111111111111111111112";
let out = replay()
.verify(
"typo'd assert address",
&[],
&[
Check::account(absent).lamports(Cmp::eq(0)).build(),
Check::account(absent).token_delta(Cmp::eq(0)).build(),
],
)
.unwrap();
assert!(!out.pass, "{out:?}");
assert!(
out.asserts.iter().all(|a| !a.pass),
"both asserts must fail, not vacuously pass: {out:?}"
);
}
#[test]
fn unknown_fields_error_with_the_available_names() {
let out = replay()
.verify(
"bad field name",
&[],
&[Check::account(COUNTER_PDA)
.field("countt", Cmp::eq(1))
.build()],
)
.unwrap();
assert!(!out.pass);
assert!(out.asserts[0].description.contains("count"), "{out:?}");
}
#[test]
fn delta_asserts_measure_from_the_mutated_start_not_the_original_load() {
let mutation = Mutation::patch(COUNTER_PDA, 8, 99u64.to_le_bytes().to_vec());
let real = replay()
.verify(
"delta is the increment",
std::slice::from_ref(&mutation),
&[Check::account(COUNTER_PDA)
.field_delta("count", Cmp::eq(1))
.build()],
)
.unwrap();
assert!(real.pass, "{real:?}");
let wrong = replay()
.verify(
"delta must not include the mutation",
std::slice::from_ref(&mutation),
&[Check::account(COUNTER_PDA)
.field_delta("count", Cmp::eq(99))
.build()],
)
.unwrap();
assert!(
!wrong.pass,
"a +99 delta credits the mutation to the program: {wrong:?}"
);
}