this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add VersionMap fuzz tests

garrison b5b536cb d344adb8

+193 -2
+64
lib/test_version_map.ex
··· 1 + defmodule Hobbes.TestVersionMap do 2 + @moduledoc """ 3 + This module contains a simple (and very inefficient) implementation of the VersionMap API 4 + to be used for fuzz/simulation testing and model checking. 5 + """ 6 + 7 + alias Hobbes.TestVersionMap 8 + 9 + import Hobbes.Utils 10 + 11 + @type t :: %__MODULE__{ 12 + ranges: list, 13 + } 14 + @enforce_keys [ 15 + :ranges, 16 + ] 17 + defstruct @enforce_keys 18 + 19 + @spec new :: t 20 + def new do 21 + %TestVersionMap{ 22 + ranges: [], 23 + } 24 + end 25 + 26 + @spec add_writes(t, non_neg_integer, list) :: t 27 + def add_writes(%TestVersionMap{} = vm, version, writes) when is_list(writes) do 28 + Enum.reduce(writes, vm.ranges, fn key_or_range, acc -> 29 + range = case key_or_range do 30 + {_sk, _ek} = tup -> tup 31 + key -> {key, next_key(key)} 32 + end 33 + [{range, version} | acc] 34 + end) 35 + |> then(fn ranges -> 36 + %{vm | ranges: ranges} 37 + end) 38 + end 39 + 40 + @spec written_after?(t, non_neg_integer, {binary, binary} | binary) :: boolean 41 + def written_after?(%TestVersionMap{} = vm, version, {_start_key, _end_key} = range) do 42 + Enum.any?(vm.ranges, fn {r, ver} -> 43 + ver > version and intersects?(range, r) 44 + end) 45 + end 46 + 47 + def written_after?(%TestVersionMap{} = vm, version, key) when is_binary(key) do 48 + written_after?(vm, version, {key, next_key(key)}) 49 + end 50 + 51 + defp intersects?({sk1, ek1}, {sk2, ek2}) do 52 + ek1 > sk2 and ek2 > sk1 53 + end 54 + 55 + @spec clear_old(t, non_neg_integer) :: t 56 + def clear_old(%TestVersionMap{} = vm, up_to_version) do 57 + Enum.reject(vm.ranges, fn {_range, ver} -> 58 + ver <= up_to_version 59 + end) 60 + |> then(fn ranges -> 61 + %{vm | ranges: ranges} 62 + end) 63 + end 64 + end
+1 -1
lib/version_map.ex
··· 31 31 @spec add_writes(VersionMap.t, non_neg_integer, [binary]) :: :ok 32 32 def add_writes(table, version, keys) when is_integer(version) and is_list(keys) do 33 33 Enum.each(keys, fn 34 - {_sk, _ek} -> :noop 34 + {sk, ek} -> add_range(table, version, sk, ek) 35 35 k -> add_range(table, version, k, next_key(k)) 36 36 end) 37 37 :ok
+128 -1
test/version_map_test.exs
··· 1 1 defmodule Hobbes.VersionMapTest do 2 2 use ExUnit.Case, async: true 3 3 4 - alias Hobbes.VersionMap 4 + alias Hobbes.{VersionMap, TestVersionMap} 5 + 6 + defmodule Fuzzer do 7 + import Hobbes.Utils 8 + 9 + @enforce_keys [ 10 + :version_map, 11 + :test_version_map, 12 + 13 + :version, 14 + :cleared_version, 15 + 16 + :seed, 17 + ] 18 + defstruct @enforce_keys 19 + 20 + def new(seed) do 21 + :rand.seed(:exsss, seed) 22 + 23 + %Fuzzer{ 24 + version_map: VersionMap.new(), 25 + test_version_map: TestVersionMap.new(), 26 + 27 + version: 1, 28 + cleared_version: 0, 29 + 30 + seed: seed, 31 + } 32 + end 33 + 34 + def run(%Fuzzer{} = fuzzer, op_count) when is_integer(op_count) do 35 + Enum.reduce(1..op_count, fuzzer, fn i, fuzzer -> 36 + try do 37 + perform(random_op(), fuzzer) 38 + rescue 39 + e in [ExUnit.AssertionError] -> 40 + e = Map.update!(e, :message, &(&1 <> " (at op #{i}, seed=#{inspect(fuzzer.seed)})")) 41 + reraise e, __STACKTRACE__ 42 + e -> 43 + require Logger 44 + Logger.error("Error #{inspect(e)} at op=#{i}, seed=#{inspect(fuzzer.seed)}") 45 + reraise e, __STACKTRACE__ 46 + end 47 + end) 48 + end 49 + 50 + @ops [:add_writes, :check_read] 51 + defp random_op do 52 + case Enum.random(1..100) do 53 + 1 -> :clear_old 54 + _ -> Enum.random(@ops) 55 + end 56 + end 57 + 58 + defp inc_version(%Fuzzer{} = fuzzer) do 59 + %{fuzzer | version: fuzzer.version + Enum.random(1..100)} 60 + end 61 + 62 + defp perform(:clear_old, %Fuzzer{} = fuzzer) do 63 + up_to_version = Enum.random(fuzzer.cleared_version..fuzzer.version) 64 + 65 + :ok = VersionMap.clear_old(fuzzer.version_map, up_to_version) 66 + test_version_map = TestVersionMap.clear_old(fuzzer.test_version_map, up_to_version) 67 + 68 + %{fuzzer | test_version_map: test_version_map} 69 + end 70 + 71 + defp perform(:add_writes, %Fuzzer{} = fuzzer) do 72 + fuzzer = inc_version(fuzzer) 73 + 74 + count = Enum.random(1..10) 75 + # TODO: test single keys? 76 + writes = Enum.map(1..count, fn _i -> random_range() end) 77 + version = fuzzer.version 78 + 79 + :ok = VersionMap.add_writes(fuzzer.version_map, version, writes) 80 + test_version_map = TestVersionMap.add_writes(fuzzer.test_version_map, version, writes) 81 + 82 + %{fuzzer | test_version_map: test_version_map} 83 + end 84 + 85 + defp perform(:check_read, %Fuzzer{} = fuzzer) do 86 + range = random_range() 87 + read_version = Enum.random(fuzzer.cleared_version..fuzzer.version) 88 + 89 + vm_result = VersionMap.written_after?(fuzzer.version_map, read_version, range) 90 + tvm_result = TestVersionMap.written_after?(fuzzer.test_version_map, read_version, range) 91 + 92 + assert vm_result == tvm_result 93 + 94 + fuzzer 95 + end 96 + 97 + defp random_range do 98 + k1 = random_key() 99 + k2 = random_key() 100 + cond do 101 + k1 < k2 -> {k1, k2} 102 + k1 > k2 -> {k2, k1} 103 + k1 == k2 -> {k1, next_key(k1)} 104 + end 105 + end 106 + 107 + defp random_key do 108 + case Enum.random(1..10) do 109 + 1 -> "" 110 + 2 -> "\xFF" 111 + _ -> Enum.random(1..100) |> Integer.to_string() |> String.pad_leading(3, "0") 112 + end 113 + end 114 + end 115 + 116 + describe "fuzz" do 117 + @tag :fuzz_vm 118 + test "VersionMap" do 119 + Fuzzer.new(100) 120 + |> Fuzzer.run(10_000) 121 + end 122 + 123 + @tag :fuzz_vm_slow 124 + @tag :disable 125 + test "VersionMap (slow)" do 126 + for i <- 1..300 do 127 + Fuzzer.new(100 + i) 128 + |> Fuzzer.run(10_000) 129 + end 130 + end 131 + end 5 132 6 133 describe "VersionMap" do 7 134 @describetag :version_map