test(zset): add skip list structure tests
This commit is contained in:
1 parent
72d7a93fed
commit
589ce95170
1 file changed
+149
@@ -0,0 +1,149 @@
|
||||
defmodule Excache.SkipListTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Excache.SkipList
|
||||
|
||||
test "empty skip list has zero size and no ranks" do
|
||||
list = SkipList.new()
|
||||
assert SkipList.size(list) == 0
|
||||
assert SkipList.to_list(list) == []
|
||||
assert SkipList.rank(list, "ghost") == :error
|
||||
assert SkipList.score(list, "ghost") == :error
|
||||
end
|
||||
|
||||
test "insert keeps ascending score order" do
|
||||
list =
|
||||
SkipList.new()
|
||||
|> SkipList.insert("c", 3)
|
||||
|> SkipList.insert("a", 1)
|
||||
|> SkipList.insert("b", 2)
|
||||
|
||||
assert SkipList.to_list(list) == [{1, "a"}, {2, "b"}, {3, "c"}]
|
||||
assert SkipList.size(list) == 3
|
||||
end
|
||||
|
||||
test "equal scores break ties by member" do
|
||||
list =
|
||||
SkipList.new()
|
||||
|> SkipList.insert("zebra", 5)
|
||||
|> SkipList.insert("apple", 5)
|
||||
|> SkipList.insert("mango", 5)
|
||||
|
||||
assert SkipList.to_list(list) == [{5, "apple"}, {5, "mango"}, {5, "zebra"}]
|
||||
end
|
||||
|
||||
test "insert on an existing member reorders it instead of duplicating" do
|
||||
list =
|
||||
SkipList.new()
|
||||
|> SkipList.insert("a", 1)
|
||||
|> SkipList.insert("b", 2)
|
||||
|> SkipList.insert("a", 10)
|
||||
|
||||
assert SkipList.size(list) == 2
|
||||
assert SkipList.to_list(list) == [{2, "b"}, {10, "a"}]
|
||||
assert SkipList.score(list, "a") == {:ok, 10}
|
||||
end
|
||||
|
||||
test "delete removes a member and closes the gap" do
|
||||
list =
|
||||
SkipList.new()
|
||||
|> SkipList.insert("a", 1)
|
||||
|> SkipList.insert("b", 2)
|
||||
|> SkipList.insert("c", 3)
|
||||
|> SkipList.delete("b")
|
||||
|
||||
assert SkipList.size(list) == 2
|
||||
assert SkipList.to_list(list) == [{1, "a"}, {3, "c"}]
|
||||
assert SkipList.member?(list, "b") == false
|
||||
end
|
||||
|
||||
test "delete on a missing member is a no-op" do
|
||||
list = SkipList.new() |> SkipList.insert("a", 1)
|
||||
unchanged = SkipList.delete(list, "nope")
|
||||
assert SkipList.to_list(unchanged) == [{1, "a"}]
|
||||
assert SkipList.size(unchanged) == 1
|
||||
end
|
||||
|
||||
test "rank matches zero-indexed position in sorted order" do
|
||||
list =
|
||||
SkipList.new()
|
||||
|> SkipList.insert("a", 10)
|
||||
|> SkipList.insert("b", 20)
|
||||
|> SkipList.insert("c", 30)
|
||||
|> SkipList.insert("d", 40)
|
||||
|
||||
assert SkipList.rank(list, "a") == {:ok, 0}
|
||||
assert SkipList.rank(list, "b") == {:ok, 1}
|
||||
assert SkipList.rank(list, "c") == {:ok, 2}
|
||||
assert SkipList.rank(list, "d") == {:ok, 3}
|
||||
assert SkipList.rank(list, "missing") == :error
|
||||
end
|
||||
|
||||
test "range_by_rank matches a plain list slice" do
|
||||
list =
|
||||
Enum.reduce(0..9, SkipList.new(), fn i, acc ->
|
||||
SkipList.insert(acc, "m#{i}", i)
|
||||
end)
|
||||
|
||||
full = SkipList.to_list(list)
|
||||
assert SkipList.range_by_rank(list, 0, 3) == Enum.slice(full, 0, 3)
|
||||
assert SkipList.range_by_rank(list, 3, 4) == Enum.slice(full, 3, 4)
|
||||
assert SkipList.range_by_rank(list, 8, 10) == Enum.slice(full, 8, 10)
|
||||
assert SkipList.range_by_rank(list, 20, 5) == []
|
||||
end
|
||||
|
||||
test "range_by_score respects inclusive and exclusive bounds" do
|
||||
list =
|
||||
Enum.reduce([{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}, {"e", 5}], SkipList.new(), fn {m, s},
|
||||
acc ->
|
||||
SkipList.insert(acc, m, s)
|
||||
end)
|
||||
|
||||
assert SkipList.range_by_score(list, {2, true}, {4, true}) == [{2, "b"}, {3, "c"}, {4, "d"}]
|
||||
assert SkipList.range_by_score(list, {2, false}, {4, false}) == [{3, "c"}]
|
||||
assert SkipList.range_by_score(list, :neg_infinity, :pos_infinity) == SkipList.to_list(list)
|
||||
assert SkipList.range_by_score(list, {10, true}, :pos_infinity) == []
|
||||
end
|
||||
|
||||
test "random insert/delete sequence matches Enum.sort ground truth" do
|
||||
seed = :erlang.phash2(make_ref())
|
||||
:rand.seed(:exsplus, {seed, seed, seed})
|
||||
|
||||
members = for i <- 1..200, do: "member-#{i}"
|
||||
|
||||
{list, ground_truth} =
|
||||
Enum.reduce(members, {SkipList.new(), %{}}, fn member, {acc_list, acc_truth} ->
|
||||
score = :rand.uniform(50)
|
||||
{SkipList.insert(acc_list, member, score), Map.put(acc_truth, member, score)}
|
||||
end)
|
||||
|
||||
expected =
|
||||
ground_truth
|
||||
|> Enum.map(fn {member, score} -> {score, member} end)
|
||||
|> Enum.sort()
|
||||
|
||||
assert SkipList.to_list(list) == expected
|
||||
assert SkipList.size(list) == map_size(ground_truth)
|
||||
|
||||
Enum.each(ground_truth, fn {member, score} ->
|
||||
assert SkipList.score(list, member) == {:ok, score}
|
||||
end)
|
||||
|
||||
{deleted, remaining_truth} = Enum.split(members, 40)
|
||||
|
||||
reduced =
|
||||
Enum.reduce(deleted, list, fn member, acc -> SkipList.delete(acc, member) end)
|
||||
|
||||
remaining_expected =
|
||||
remaining_truth
|
||||
|> Enum.map(fn member -> {Map.fetch!(ground_truth, member), member} end)
|
||||
|> Enum.sort()
|
||||
|
||||
assert SkipList.to_list(reduced) == remaining_expected
|
||||
assert SkipList.size(reduced) == length(remaining_truth)
|
||||
|
||||
Enum.each(deleted, fn member ->
|
||||
assert SkipList.member?(reduced, member) == false
|
||||
end)
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user