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