Skip to content

Commit

Permalink
checking order of prefixes
Browse files Browse the repository at this point in the history
  • Loading branch information
ProducerMatt committed Jun 25, 2024
1 parent 9251f84 commit 8942720
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 1 deletion.
1 change: 1 addition & 0 deletions .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}
]
}
}
Expand Down
172 changes: 172 additions & 0 deletions bench/prefix_conflicts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
alias Stampede, as: S
require Stampede.MsgReceived
require Aja

defmodule T do
require Plugin
require Aja

def check_prefixes_for_conflicts(prefixes) when is_list(prefixes) do
all_but_first = Enum.drop(prefixes, 1)
all_but_last = Enum.drop(prefixes, -1)

Enum.find_value(all_but_first, :no_conflict, fn
prefix_in_danger ->
Enum.find_value(all_but_last, false, fn
^prefix_in_danger ->
# we've caught up to ourselves
false

prefix_that_interrupts ->
{false_or_prefix, mangled} = S.split_prefix(prefix_in_danger, prefix_that_interrupts)

if false_or_prefix do
{:conflict, prefix_in_danger, prefix_that_interrupts, mangled}
else
false
end
end)
end)
end

@spec do_check_prefixes_for_conflicts(nonempty_list(binary())) ::
:no_conflict
| {:conflict, mangled_prefix :: binary(), prefix_responsible :: binary(),
how_it_was_mangled :: binary()}
def do_check_prefixes_for_conflicts([first | rest]) do
do_check_prefixes_for_conflicts(first, rest)
end

def do_check_prefixes_for_conflicts(_final_prefix, []),
do: :no_conflict

def do_check_prefixes_for_conflicts(prefix_that_interrupts, latter_prefixes) do
Enum.find_value(latter_prefixes, fn
prefix_in_danger ->
{false_or_prefix, mangled} = S.split_prefix(prefix_in_danger, prefix_that_interrupts)

if false_or_prefix do
{:conflict, prefix_in_danger, prefix_that_interrupts, mangled}
else
nil
end
end)
|> case do
nil ->
[h | t] = latter_prefixes
do_check_prefixes_for_conflicts(h, t)

otherwise ->
otherwise
end
end

def check_prefixes_for_conflicts_vec(prefixes) do
all_but_first = Aja.Vector.drop(prefixes, 1)
all_but_last = Aja.Vector.drop(prefixes, -1)

Aja.Enum.find_value(all_but_first, :no_conflict, fn
prefix_in_danger ->
Aja.Enum.find_value(all_but_last, false, fn
^prefix_in_danger ->
# we've caught up to ourselves
false

prefix_that_interrupts ->
{false_or_prefix, mangled} = S.split_prefix(prefix_in_danger, prefix_that_interrupts)

if false_or_prefix do
{:conflict, prefix_in_danger, prefix_that_interrupts, mangled}
else
false
end
end)
end)
end

@spec do_check_prefixes_for_conflicts_vec_2(%Aja.Vector{}) ::
:no_conflict
| {:conflict, mangled_prefix :: binary(), prefix_responsible :: binary(),
how_it_was_mangled :: binary()}
def do_check_prefixes_for_conflicts_vec_2(vector) do
first = Aja.Vector.first(vector)
rest = Aja.Vector.drop(vector, 1)
do_check_prefixes_for_conflicts_vec_2(first, rest)
end

def do_check_prefixes_for_conflicts_vec_2(v, prefix_that_interrupts_i) when Aja.vec_size(v),
do: :no_conflict

def do_check_prefixes_for_conflicts_vec_2(prefix_that_interrupts, latter_prefixes) do
Aja.Enum.find_value(latter_prefixes, fn
prefix_in_danger ->
{false_or_prefix, mangled} = S.split_prefix(prefix_in_danger, prefix_that_interrupts)

if false_or_prefix do
{:conflict, prefix_in_danger, prefix_that_interrupts, mangled}
else
nil
end
end)
|> case do
nil ->
h = Aja.Vector.first(latter_prefixes)
t = Aja.Vector.drop(latter_prefixes, 1)
do_check_prefixes_for_conflicts(h, t)

otherwise ->
otherwise
end
end

def check_prefixes_for_conflicts_nitpick([_h | []]), do: :no_conflict

def check_prefixes_for_conflicts_nitpick([prefix_that_interrupts | latter_prefixes]) do
Enum.find_value(latter_prefixes, fn
prefix_in_danger ->
{false_or_prefix, mangled} = S.split_prefix(prefix_in_danger, prefix_that_interrupts)

if false_or_prefix do
{:conflict, prefix_in_danger, prefix_that_interrupts, mangled}
else
nil
end
end)
|> case do
nil ->
check_prefixes_for_conflicts_nitpick(latter_prefixes)

otherwise ->
otherwise
end
end

def make_input(pref, number) do
for n <- 0..(number - 1) do
Enum.at(pref, Integer.mod(n, length(pref))) <> String.duplicate("x", 16)
end
end
end

bl = ["ac", "bc", "cc", "aca", "ad", "bd", "cc"]
blv = bl |> Aja.Vector.new()

suites = %{
"manual" => fn -> T.do_check_prefixes_for_conflicts(bl) end,
"manual nitpick" => fn -> T.check_prefixes_for_conflicts_nitpick(bl) end
# # slow
# "find_value" => fn -> T.check_prefixes_for_conflicts(bl) end,
# # Even slower
# "vectorized find_value" => fn -> T.check_prefixes_for_conflicts_vec(blv) end,
# "vectorized manual" => fn -> T.do_check_prefixes_for_conflicts_vec_2(blv) end,
}

Benchee.run(
suites,
time: 30,
memory_time: 5,
# profile_after: true,
# profile_after: :fprof
measure_function_call_overhead: true,
pre_check: true
)
66 changes: 65 additions & 1 deletion lib/site_config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule SiteConfig do
"""
use TypeCheck
use TypeCheck.Defstruct
require Logger
alias Stampede, as: S
require S

Expand Down Expand Up @@ -96,7 +97,7 @@ defmodule SiteConfig do
end

def schema(atom),
do: S.service_atom_to_name(atom) |> apply(:site_config_schema, [])
do: S.service_atom_to_name(atom).site_config_schema()

def fetch!(cfg, key) when is_map_key(cfg, key), do: Map.fetch!(cfg, key)

Expand Down Expand Up @@ -334,4 +335,67 @@ defmodule SiteConfig do
otherwise
end
end

def maybe_sort_prefixes(cfg, _schema) do
# check and warn for conflicting prefixes
cfg[:prefix]
|> case do
nil ->
cfg

singular when not is_list(singular) ->
cfg

ps when is_list(ps) ->
case check_prefixes_for_conflicts(ps) do
:no_conflict ->
cfg

{:conflict, mangled_prefix, prefix_responsible, how_it_was_mangled} ->
sorted = S.sort_rev_str_len(ps)

Logger.warning(fn ->
"""
Prefix "#{mangled_prefix}" was interrupted by prefix "#{prefix_responsible}". What this means:
- sent command: `#{mangled_prefix} hello`
- intended command: `hello`
- interpreted command: `#{how_it_was_mangled}`
This could be fixed by putting `#{prefix_responsible}` after `#{mangled_prefix}` in the list.
We sorted the list for you:
#{ps |> S.pp()} |> #{sorted |> S.pp()}
"""
end)

Keyword.put(cfg, :prefix, sorted)
end
end
end

@spec! check_prefixes_for_conflicts(nonempty_list(binary())) ::
:no_conflict
| {:conflict, mangled_prefix :: binary(), prefix_responsible :: binary(),
how_it_was_mangled :: binary()}
def check_prefixes_for_conflicts([_h | []]), do: :no_conflict

def check_prefixes_for_conflicts([prefix_that_interrupts | latter_prefixes]) do
Enum.find_value(latter_prefixes, fn
prefix_in_danger ->
{false_or_prefix, mangled} = S.split_prefix(prefix_in_danger, prefix_that_interrupts)

if false_or_prefix do
{:conflict, prefix_in_danger, prefix_that_interrupts, mangled}
else
nil
end
end)
|> case do
nil ->
check_prefixes_for_conflicts(latter_prefixes)

otherwise ->
otherwise
end
end
end
23 changes: 23 additions & 0 deletions lib/stampede.ex
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,29 @@ defmodule Stampede do
def enable_typechecking?(), do: false
end

def sort_rev_str_len(str_list) do
Enum.sort(str_list, fn s1, s2 ->
l1 = String.length(s1)
l2 = String.length(s2)

cond do
l1 > l2 ->
true

l1 < l2 ->
false

l1 == l2 ->
s1 <= s2
end
end)
end

def end_with_newline(unmodified_bin) do
String.trim_trailing(unmodified_bin)
|> Kernel.<>("\n")
end

defmodule Debugging do
@moduledoc false
use TypeCheck
Expand Down
63 changes: 63 additions & 0 deletions test/stampede_stateless_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,63 @@ defmodule StampedeStatelessTest do
assert_value S.split_prefix("s, ", bl) == {false, "s, "}
end

test "split_prefix conflict check" do
bl = ["a", "b", "c", "ab", "d"]
assert_value SiteConfig.check_prefixes_for_conflicts(bl) == {:conflict, "ab", "a", "b"}

bl = ["a", "b", "c", "d"]
assert_value SiteConfig.check_prefixes_for_conflicts(bl) == :no_conflict
end

test "cfg prefix conflict sorting" do
rev = ["a", "b", "c", "aa", "ab", "ba", "bc", "aaa", "aba", "bbc", "cac", "aaaa", "ddddd"]

{result, log} =
with_log(fn ->
SiteConfig.maybe_sort_prefixes([prefix: rev], nil)
end)

assert_value result[:prefix] == [
"ddddd",
"aaaa",
"aaa",
"aba",
"bbc",
"cac",
"aa",
"ab",
"ba",
"bc",
"a",
"b",
"c"
]

assert String.contains?(log, "sorted")
end

test "sort_by_str_len" do
rev = ["a", "b", "c", "aa", "ab", "ba", "bc", "aaa", "aba", "bbc", "cac", "aaaa", "ddddd"]

answer = [
"ddddd",
"aaaa",
"aaa",
"aba",
"bbc",
"cac",
"aa",
"ab",
"ba",
"bc",
"a",
"b",
"c"
]

assert S.sort_rev_str_len(rev) == answer
end

test "Plugin.is_bot_invoked?" do
cfg_defaults = %{bot_is_loud: false}

Expand Down Expand Up @@ -370,6 +427,12 @@ defmodule StampedeStatelessTest do
3. Item 3
"""
end

test "cut blank space and replace with newline" do
assert "a\n" == S.end_with_newline("a ")
assert "a b\n" == S.end_with_newline("a b ")
assert_value S.end_with_newline("a\n") == "a\n"
end
end

describe "Response picking and tracebacks" do
Expand Down

0 comments on commit 8942720

Please sign in to comment.