158 lines
5.5 KiB
Elixir
158 lines
5.5 KiB
Elixir
defmodule Towerops.Workers.PollingOffsetTest do
|
|
use ExUnit.Case, async: true
|
|
use ExUnitProperties
|
|
|
|
alias Towerops.Workers.PollingOffset
|
|
|
|
doctest PollingOffset
|
|
|
|
describe "calculate_offset/2" do
|
|
test "returns value within interval bounds" do
|
|
device_id = Ecto.UUID.generate()
|
|
offset = PollingOffset.calculate_offset(device_id, 300)
|
|
|
|
assert offset >= 0
|
|
assert offset < 300
|
|
end
|
|
|
|
test "returns value within bounds for different intervals" do
|
|
device_id = Ecto.UUID.generate()
|
|
|
|
# Test various intervals
|
|
for interval <- [60, 120, 300, 600, 3600] do
|
|
offset = PollingOffset.calculate_offset(device_id, interval)
|
|
assert offset >= 0, "offset should be >= 0 for interval #{interval}"
|
|
assert offset < interval, "offset should be < #{interval} for interval #{interval}"
|
|
end
|
|
end
|
|
|
|
test "is deterministic - same device_id always returns same offset" do
|
|
device_id = Ecto.UUID.generate()
|
|
|
|
offset1 = PollingOffset.calculate_offset(device_id, 300)
|
|
offset2 = PollingOffset.calculate_offset(device_id, 300)
|
|
offset3 = PollingOffset.calculate_offset(device_id, 300)
|
|
|
|
assert offset1 == offset2
|
|
assert offset2 == offset3
|
|
end
|
|
|
|
test "different devices get different offsets" do
|
|
id1 = Ecto.UUID.generate()
|
|
id2 = Ecto.UUID.generate()
|
|
id3 = Ecto.UUID.generate()
|
|
|
|
offset1 = PollingOffset.calculate_offset(id1, 300)
|
|
offset2 = PollingOffset.calculate_offset(id2, 300)
|
|
offset3 = PollingOffset.calculate_offset(id3, 300)
|
|
|
|
# Very unlikely all three would be equal with good hash distribution
|
|
# At least two should be different
|
|
unique_offsets = Enum.uniq([offset1, offset2, offset3])
|
|
assert length(unique_offsets) >= 2, "expected at least 2 different offsets from 3 devices"
|
|
end
|
|
|
|
test "offset changes with different intervals for same device" do
|
|
device_id = Ecto.UUID.generate()
|
|
|
|
offset_60 = PollingOffset.calculate_offset(device_id, 60)
|
|
offset_300 = PollingOffset.calculate_offset(device_id, 300)
|
|
offset_600 = PollingOffset.calculate_offset(device_id, 600)
|
|
|
|
# Verify all within their respective bounds
|
|
assert offset_60 < 60
|
|
assert offset_300 < 300
|
|
assert offset_600 < 600
|
|
end
|
|
|
|
test "handles minimum interval (1 second)" do
|
|
device_id = Ecto.UUID.generate()
|
|
offset = PollingOffset.calculate_offset(device_id, 1)
|
|
|
|
assert offset == 0, "with interval of 1, offset must be 0"
|
|
end
|
|
|
|
test "distributes devices across interval buckets" do
|
|
# Generate 100 device IDs
|
|
device_ids = for _ <- 1..100, do: Ecto.UUID.generate()
|
|
|
|
# Calculate offsets with 300 second interval
|
|
offsets = Enum.map(device_ids, &PollingOffset.calculate_offset(&1, 300))
|
|
|
|
# Divide 300 seconds into 10 buckets (0-29, 30-59, 60-89, etc.)
|
|
buckets = Enum.group_by(offsets, &div(&1, 30))
|
|
|
|
# With good distribution, each bucket should have roughly 10 devices (100/10)
|
|
# Allow for variance - hash functions have natural clustering
|
|
# Check that no single bucket has more than 30% of devices (pathological case)
|
|
for {bucket_num, devices_in_bucket} <- buckets do
|
|
count = length(devices_in_bucket)
|
|
|
|
assert count <= 30,
|
|
"bucket #{bucket_num} has #{count} devices (#{count}%), expected at most 30 (30%)"
|
|
end
|
|
|
|
# Verify we're actually using multiple buckets (good distribution)
|
|
# With 100 devices and 10 buckets, expect at least 7 buckets to be used
|
|
assert map_size(buckets) >= 7,
|
|
"expected at least 7 of 10 buckets to be used, got #{map_size(buckets)}"
|
|
end
|
|
|
|
test "distributes devices across full interval range" do
|
|
# Generate many device IDs
|
|
device_ids = for _ <- 1..1000, do: Ecto.UUID.generate()
|
|
|
|
# Calculate offsets
|
|
offsets = Enum.map(device_ids, &PollingOffset.calculate_offset(&1, 300))
|
|
|
|
# Verify we see offsets across the full range
|
|
min_offset = Enum.min(offsets)
|
|
max_offset = Enum.max(offsets)
|
|
|
|
# With 1000 devices, we should see offsets near both ends
|
|
assert min_offset < 30, "expected some offsets near 0, got min #{min_offset}"
|
|
assert max_offset > 270, "expected some offsets near 299, got max #{max_offset}"
|
|
end
|
|
|
|
test "handles binary UUID format" do
|
|
# Test with binary UUID (which is what Device.id actually is)
|
|
binary_uuid = Ecto.UUID.dump!(Ecto.UUID.generate())
|
|
offset = PollingOffset.calculate_offset(binary_uuid, 300)
|
|
|
|
assert offset >= 0
|
|
assert offset < 300
|
|
end
|
|
|
|
test "handles string UUID format" do
|
|
# Test with string UUID
|
|
string_uuid = Ecto.UUID.generate()
|
|
offset = PollingOffset.calculate_offset(string_uuid, 300)
|
|
|
|
assert offset >= 0
|
|
assert offset < 300
|
|
end
|
|
end
|
|
|
|
describe "property: calculate_offset/2" do
|
|
property "result is always within [0, interval) for any term and positive interval" do
|
|
check all(
|
|
term <- one_of([binary(), integer(), atom(:alphanumeric), list_of(integer())]),
|
|
interval <- integer(1..100_000)
|
|
) do
|
|
offset = PollingOffset.calculate_offset(term, interval)
|
|
assert offset >= 0
|
|
assert offset < interval
|
|
end
|
|
end
|
|
|
|
property "deterministic: same input yields same output" do
|
|
check all(
|
|
term <- binary(),
|
|
interval <- integer(1..10_000)
|
|
) do
|
|
assert PollingOffset.calculate_offset(term, interval) ==
|
|
PollingOffset.calculate_offset(term, interval)
|
|
end
|
|
end
|
|
end
|
|
end
|