Skip to content

Instantly share code, notes, and snippets.

@Nezteb
Last active February 5, 2025 20:03
Show Gist options
  • Save Nezteb/d0e1c0d7ed1a7d2bf1a58782a8aea6a0 to your computer and use it in GitHub Desktop.
Save Nezteb/d0e1c0d7ed1a7d2bf1a58782a8aea6a0 to your computer and use it in GitHub Desktop.
Playing around with DST and ExUnit seed generation.
defmodule Simulator do
@moduledoc """
Handles parsing of different seed formats for simulation purposes.
Supports Git hashes, ISO8601 dates, and normal integer seeds.
Based on:
- https://docs.tigerbeetle.com/about/vopr/
- https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/HACKING.md#simulation-tests
- https://github.com/tigerbeetle/tigerbeetle/blob/main/src/testing/fuzz.zig#L72-L90
- https://github.com/whatyouhide/stream_data/blob/main/lib/ex_unit_properties.ex#L674-L689
- https://notes.eatonphil.com/2024-08-20-deterministic-simulation-testing.html
"""
@git_hash_length 40
@max_seed_value Bitwise.bsl(1, 64)
@type seed_error :: :invalid_seed
@type parse_result :: {:ok, non_neg_integer()} | {:error, seed_error}
@doc """
Parses a seed value from either:
- A Git hash (40 characters)
- An ISO8601 datetime string
- A decimal string
Returns a 64-bit integer seed value wrapped in a result tuple.
## Examples
iex> Simulator.parse_seed("1234567890abcdef1234567890abcdef12345678")
{:ok, 10424652189165442680}
iex> Simulator.parse_seed("2025-02-05T12:00:00Z")
{:ok, 1738756800000000}
iex> Simulator.parse_seed("12345")
{:ok, 12345}
iex> Simulator.parse_seed("invalid")
{:error, :invalid_seed}
"""
@spec parse_seed(binary()) :: parse_result()
def parse_seed(input) when is_binary(input) do
input
|> determine_input_type()
|> parse_input()
rescue
_ -> {:error, :invalid_seed}
end
defp determine_input_type(input) do
cond do
is_length_of_git_hash?(input) -> {:git_hash, input}
is_iso_datetime_string?(input) -> {:datetime, input}
true -> {:integer, input}
end
end
defp is_length_of_git_hash?(string), do: byte_size(string) == @git_hash_length
defp is_iso_datetime_string?(string) do
case DateTime.from_iso8601(string) do
{:ok, _datetime, _offset} -> true
_ -> false
end
end
defp parse_input({:git_hash, hash}) do
seed =
hash
|> String.downcase()
|> Base.decode16!(case: :lower)
|> :binary.decode_unsigned()
|> normalize_seed()
{:ok, seed}
end
defp parse_input({:datetime, dt_string}) do
{:ok, datetime, 0} = DateTime.from_iso8601(dt_string)
seed =
datetime
|> DateTime.to_unix(:microsecond)
|> normalize_seed()
{:ok, seed}
end
defp parse_input({:integer, int_string}) do
{num, ""} = Integer.parse(int_string)
{:ok, normalize_seed(num)}
end
defp normalize_seed(value) do
rem(value, @max_seed_value)
end
end
{:ok, git_sha_seed} = Simulator.parse_seed("1234567890abcdef1234567890abcdef12345678")
{:ok, date_time_seed} = Simulator.parse_seed("2025-02-05T12:00:00Z")
{:ok, regular_int_seed} = Simulator.parse_seed("12345")
{git_sha_seed, date_time_seed, regular_int_seed}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment