towerops/test/towerops/workers/polling_offset_test.exs
2026-06-13 14:25:19 -05:00

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