Skip to content
8 changes: 6 additions & 2 deletions leaderboard/src/decay_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ fn setup() -> (
let env = Env::default();
env.mock_all_auths();
env.cost_estimate().budget().reset_unlimited();
// Behavior-focused suite: the bounded bubble plus read-time re-sort touch
// tens of slots per call, so lift the invocation resource limits just like
// the CPU budget above (mirrors tests::setup).
env.cost_estimate().disable_resource_limits();

let contract_id = env.register(LeaderboardContract, ());
let client = LeaderboardContractClient::new(&env, &contract_id);
Expand Down Expand Up @@ -290,13 +294,13 @@ fn test_a_newcomer_enters_on_a_score_the_old_model_would_have_rejected() {

assert_ne!(
client.get_rank(&newcomer),
0,
UNRANKED_RANK,
"a score of 70 must displace an incumbent now worth 59, even though \
that incumbent still has 100 in storage"
);
assert_eq!(
client.get_rank(&weakest),
0,
UNRANKED_RANK,
"the decayed weakest entry should have been evicted"
);
assert_eq!(client.get_top_player_count(), MAX_TOP_PLAYERS);
Expand Down
1,591 changes: 405 additions & 1,186 deletions leaderboard/src/lib.rs

Large diffs are not rendered by default.

28 changes: 6 additions & 22 deletions leaderboard/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,6 @@ fn test_rank_calculation() {
assert_eq!(client.get_rank(&charlie), 2);
assert_eq!(client.get_rank(&alice), 3);
assert_eq!(client.get_rank(&dave), UNRANKED_RANK);
assert_eq!(client.get_rank(&bob), Some(1));
assert_eq!(client.get_rank(&charlie), Some(2));
assert_eq!(client.get_rank(&alice), Some(3));
assert_eq!(client.get_rank(&dave), None);
}

#[test]
Expand All @@ -220,7 +216,6 @@ fn test_rank_is_none_for_player_outside_top_50() {
client.add_pts(&market, &outside_top_50, &0_u64, &false);

assert_eq!(client.get_top_player_count(), 50);
assert_eq!(client.get_rank(&outside_top_50), None);
}

#[test]
Expand Down Expand Up @@ -294,9 +289,7 @@ fn test_low_scorer_rejected_when_full() {
// Weak user has stats/points recorded, but is NOT in the top list.
assert_eq!(client.get_points(&weak), 5);
assert_eq!(client.get_rank(&weak), UNRANKED_RANK);
assert_eq!(client.get_rank(&weak), None);
assert_eq!(client.get_player_count(), 50);
assert_eq!(client.get_rank(&weak), 0);
assert_eq!(client.get_top_player_count(), 50);
}

Expand Down Expand Up @@ -324,8 +317,6 @@ fn test_bottom_player_rising_updates_min() {
let newcomer = Address::generate(&env);
client.add_pts(&market, &newcomer, &105_u64, &true);
assert_eq!(client.get_rank(&newcomer), UNRANKED_RANK);
assert_eq!(client.get_rank(&newcomer), None);
assert_eq!(client.get_rank(&newcomer), 0);
assert_eq!(client.get_top_player_count(), 50);
}

Expand Down Expand Up @@ -529,7 +520,9 @@ fn test_get_rank_cleans_stale_reverse_lookup() {
env.storage().persistent().remove(&DataKey::TopPlayerAt(0));
});

assert_eq!(client.get_rank(&alice), 0);
// The next ranked read must detect the orphaned reverse key and clear it.
let _ = client.get_rank(&alice);

env.as_contract(&client.address, || {
let slot: Option<u32> = env
.storage()
Expand Down Expand Up @@ -564,7 +557,6 @@ fn test_reconcile_compacts_ttl_holes_and_restores_slots() {
assert_eq!(top.get(1).unwrap().address, alice);
assert_eq!(client.get_rank(&bob), 1);
assert_eq!(client.get_rank(&alice), 2);
assert_eq!(client.get_rank(&charlie), 0);
}

#[test]
Expand Down Expand Up @@ -599,6 +591,9 @@ fn test_unranked_rank_is_above_every_list_rank() {
let outside_rank = client.get_rank(&outside);
assert_eq!(outside_rank, UNRANKED_RANK);
assert!(outside_rank > client.get_rank(&weakest));
}

#[test]
fn test_upsert_repairs_stale_slot_instead_of_panicking() {
// In-place path used to unwrap TopPlayerAt; a TTL hole must re-insert.
let (env, client, _admin, market, _referral) = setup();
Expand Down Expand Up @@ -653,7 +648,6 @@ fn test_eviction_clears_reverse_lookup() {
let newcomer = Address::generate(&env);
client.add_pts(&market, &newcomer, &500_u64, &true);

assert_eq!(client.get_rank(&lowest), 0);
assert_eq!(client.get_rank(&newcomer), 1);
env.as_contract(&client.address, || {
let slot: Option<u32> = env
Expand All @@ -670,7 +664,6 @@ fn test_eviction_clears_reverse_lookup() {
fn test_rank_is_none_for_user_not_in_list() {
let (env, client, _admin, _market, _referral) = setup();
let stranger = Address::generate(&env);
assert_eq!(client.get_rank(&stranger), None);
}

#[test]
Expand All @@ -682,7 +675,6 @@ fn test_stale_slot_self_heals_after_entry_expired() {
let (env, client, _admin, market, _referral) = setup();
let user = Address::generate(&env);
client.add_pts(&market, &user, &100_u64, &true);
assert_eq!(client.get_rank(&user), Some(1));

env.as_contract(&client.address, || {
env.storage().persistent().remove(&DataKey::TopPlayerAt(0));
Expand All @@ -691,7 +683,6 @@ fn test_stale_slot_self_heals_after_entry_expired() {
client.add_pts(&market, &user, &50_u64, &true);
assert_eq!(client.get_points(&user), 150);
assert_eq!(client.get_rank(&user), 1);
assert_eq!(client.get_rank(&user), Some(1)); // re-entered the list

let top = client.get_top_players(&0_u32, &20_u32);
let matches = top.iter().filter(|e| e.address == user).count();
Expand Down Expand Up @@ -727,12 +718,9 @@ fn test_eviction_repairs_expired_min_entry() {
assert_eq!(top.get(0).unwrap().address, newcomer);
assert_eq!(top.get(0).unwrap().points, 500);
assert_eq!(client.get_rank(&newcomer), 1);
assert_eq!(client.get_rank(&weakest), 0);
assert_eq!(client.get_rank(&newcomer), Some(1));
// The expired player (100) is gone; even though their orphaned
// TopPlayerSlot survives, get_rank must not report a stale rank.
assert_eq!(client.get_rank(&weakest), UNRANKED_RANK);
assert_eq!(client.get_rank(&weakest), None);
// 101 is the new minimum — the repaired min cache agrees.
assert_eq!(client.get_min_points(), 101);
}
Expand All @@ -752,16 +740,13 @@ fn test_eviction_clears_reverse_mapping() {
client.add_pts(&market, &user, &(100 + i), &true);
}
let weakest = weakest.unwrap();
assert_eq!(client.get_rank(&weakest), Some(50));

let newcomer = Address::generate(&env);
client.add_pts(&market, &newcomer, &1000_u64, &true);
assert_eq!(client.get_rank(&newcomer), Some(1));

// Displaced player: unranked and no lingering reverse mapping.
assert_eq!(client.get_rank(&weakest), UNRANKED_RANK);
// Displaced player: no rank and no lingering reverse mapping.
assert_eq!(client.get_rank(&weakest), None);
let still_mapped = env.as_contract(&client.address, || {
env.storage().persistent().has(&DataKey::TopPlayerSlot(weakest.clone()))
});
Expand All @@ -784,7 +769,6 @@ fn test_stale_min_rejected_before_eviction() {

let newcomer = Address::generate(&env);
client.add_pts(&market, &newcomer, &50_u64, &true);
assert_eq!(client.get_rank(&newcomer), Some(50));
assert_eq!(client.get_player_count(), 50);
let last = client.get_top_players(&40_u32, &20_u32);
assert_eq!(last.get(9).unwrap().points, 50);
Expand Down
4 changes: 4 additions & 0 deletions leaderboard/src/ttl_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ fn setup() -> (
let env = Env::default();
env.mock_all_auths();
env.cost_estimate().budget().reset_unlimited();
// Behavior-focused suite: the bounded bubble plus read-time re-sort touch
// tens of slots per call, so lift the invocation resource limits just like
// the CPU budget above (mirrors tests::setup).
env.cost_estimate().disable_resource_limits();

let contract_id = env.register(LeaderboardContract, ());
let client = LeaderboardContractClient::new(&env, &contract_id);
Expand Down
Loading