diff --git a/test/excache/introspection_test.exs b/test/excache/introspection_test.exs index 070d773..e6d41ee 100644 --- a/test/excache/introspection_test.exs +++ b/test/excache/introspection_test.exs @@ -467,3 +467,160 @@ defmodule Excache.IntrospectionTest do Client.command(conn, ["LINSERT", "a", "BEFORE", "b"]) end + test "GETKEYS walks plain ranges" do + assert getkeys(["GET", "one"]) == ["one"] + assert getkeys(["MSET", "a", "1", "b", "2", "c", "3"]) == ["a", "b", "c"] + assert getkeys(["DEL", "a", "b", "c"]) == ["a", "b", "c"] + assert getkeys(["BLPOP", "left", "right", "0"]) == ["left", "right"] + assert getkeys(["BITOP", "AND", "dest", "src1", "src2"]) == ["dest", "src1", "src2"] + assert getkeys(["SMOVE", "from", "to", "member"]) == ["from", "to"] + assert getkeys(["EXPIRE", "solo", "10"]) == ["solo"] + end + + test "GETKEYS resolves the commands whose keys move" do + assert getkeys(["XREAD", "COUNT", "2", "STREAMS", "s1", "s2", "0", "0"]) == ["s1", "s2"] + + assert getkeys([ + "XREADGROUP", + "GROUP", + "g", + "c", + "STREAMS", + "stream", + ">" + ]) == ["stream"] + + assert getkeys(["SINTERCARD", "2", "a", "b", "LIMIT", "1"]) == ["a", "b"] + assert getkeys(["BLMPOP", "0", "2", "a", "b", "LEFT"]) == ["a", "b"] + assert getkeys(["OBJECT", "ENCODING", "thing"]) == ["thing"] + assert getkeys(["MEMORY", "USAGE", "thing"]) == ["thing"] + assert getkeys(["XINFO", "STREAM", "events"]) == ["events"] + assert getkeys(["XGROUP", "CREATE", "events", "group", "$"]) == ["events"] + end + + test "GETKEYS refuses what it cannot answer" do + assert getkeys(["PING"]) == {:error, "ERR The command has no key arguments"} + assert getkeys(["MEMORY", "DOCTOR"]) == {:error, "ERR The command has no key arguments"} + assert getkeys(["NOSUCHCOMMAND", "a"]) == {:error, "ERR Invalid command specified"} + + assert getkeys(["GET"]) == + {:error, "ERR Invalid number of arguments specified for command"} + + assert getkeys(["XREAD", "COUNT", "2", "s1", "0"]) == + {:error, "ERR Invalid arguments specified for command"} + + assert getkeys(["SINTERCARD", "3", "a", "b"]) == + {:error, "ERR Invalid arguments specified for command"} + end + + test "DOCS answers with the group and arity taken from the same table" do + assert {[{:array, entries}], _state} = + Introspection.dispatch( + "COMMAND", + ["DOCS", "zadd", "nosuchcommand"], + commandState() + ) + + assert entries == [ + {:bulk, "zadd"}, + {:array, [{:bulk, "group"}, {:bulk, "zset"}, {:bulk, "arity"}, {:integer, -4}]} + ] + + assert {[{:array, everything}], _state} = + Introspection.dispatch("COMMAND", ["DOCS"], commandState()) + + assert length(everything) == 2 * length(Enum.uniq(Command.known_commands())) + end + + test "an unknown subcommand is named in the error" do + assert {[{:error, message}], _state} = + Introspection.dispatch("COMMAND", ["NOPE"], commandState()) + + assert message =~ "Unknown subcommand or wrong number of arguments for 'NOPE'" + end + + test "the extended dispatcher already routes COMMAND to this module" do + assert {[{:integer, count}], _state} = + Excache.Command.Extended.dispatch("COMMAND", ["COUNT"], commandState()) + + assert count == Catalog.count() + end + end + + defp commandState, + do: Map.put(Transaction.initial_state(), :subscriptions, MapSet.new()) + + defp commandInfo(name) do + assert {[{:array, [{:array, entry}]}], _state} = + Introspection.dispatch("COMMAND", ["INFO", name], commandState()) + + [ + {:bulk, lowered}, + {:integer, arity}, + {:array, flags}, + {:integer, first}, + {:integer, last}, + {:integer, step} + ] = entry + + {lowered, arity, Enum.map(flags, fn {:simple, flag} -> flag end), first, last, step} + end + + defp getkeys(argv) do + case Introspection.dispatch("COMMAND", ["GETKEYS" | argv], commandState()) do + {[{:array, keys}], _state} -> Enum.map(keys, fn {:bulk, key} -> key end) + {[error], _state} -> error + end + end + + defp clientField(conn, name) do + {text, _conn} = Client.command(conn, ["INFO", "clients"]) + String.to_integer(fields(text)[name]) + end + + defp sections(text) do + for "# " <> title <- String.split(text, "\r\n"), do: title + end + + defp fields(text) do + text + |> String.split("\r\n") + |> Enum.reject(fn line -> line == "" or String.starts_with?(line, "#") end) + |> Map.new(fn line -> + [key, value] = String.split(line, ":", parts: 2) + {key, value} + end) + end + + defp listLines(text) do + text + |> String.split("\n") + |> Enum.reject(&(&1 == "")) + |> Map.new(fn line -> + parsed = + Map.new(String.split(line, " "), fn pair -> + [key, value] = String.split(pair, "=", parts: 2) + {key, value} + end) + + {String.to_integer(parsed["id"]), parsed} + end) + end + + defp blockingTask(port, args) do + Task.async(fn -> + client = Client.connect(port) + {reply, _client} = Client.command(client, args) + Client.close(client) + reply + end) + end + + defp awaitUntil(check, attempts \\ 200) do + cond do + check.() -> :ok + attempts == 0 -> flunk("condition never held") + true -> Process.sleep(10) && awaitUntil(check, attempts - 1) + end + end +end