diff --git a/test/excache/introspection_test.exs b/test/excache/introspection_test.exs new file mode 100644 index 0000000..63069d9 --- /dev/null +++ b/test/excache/introspection_test.exs @@ -0,0 +1,158 @@ +defmodule Excache.IntrospectionTest do + use Excache.Test.ServerCase, async: false + + alias Excache.Aof + alias Excache.Blocking + alias Excache.Command + alias Excache.Command.Introspection + alias Excache.Command.Introspection.Catalog + alias Excache.Command.Transaction + alias Excache.Config + alias Excache.Eviction.Runner + alias Excache.Memory + + setup do + Config.reset() + Runner.resetStats() + on_exit(&Config.reset/0) + :ok + end + + describe "INFO" do + test "answers with every default section in the documented order", %{conn: conn} do + assert {text, _conn} = Client.command(conn, ["INFO"]) + + assert sections(text) == [ + "Server", + "Clients", + "Memory", + "Persistence", + "Stats", + "Replication", + "Keyspace" + ] + + assert String.ends_with?(text, "\r\n\r\n") + end + + test "the server section reports the running system, not placeholders", %{conn: conn} do + assert {text, _conn} = Client.command(conn, ["INFO", "server"]) + assert sections(text) == ["Server"] + + values = fields(text) + + assert values["redis_mode"] == "standalone" + assert values["excache_version"] == to_string(Application.spec(:excache, :vsn)) + assert values["run_id"] =~ ~r/^[0-9a-f]{40}$/ + assert values["arch_bits"] == Integer.to_string(:erlang.system_info(:wordsize) * 8) + assert values["process_id"] == to_string(:os.getpid()) + assert values["elixir_version"] == System.version() + assert values["otp_release"] == to_string(:erlang.system_info(:otp_release)) + assert String.to_integer(values["uptime_in_seconds"]) >= 0 + end + + test "used_memory matches what the memory accountant reports", %{conn: conn} do + assert {{:simple, "OK"}, conn} = + Client.command(conn, ["SET", "solo", String.duplicate("payload", 40)]) + + assert {text, _conn} = Client.command(conn, ["INFO", "memory"]) + values = fields(text) + + assert sections(text) == ["Memory"] + assert String.to_integer(values["used_memory"]) == Memory.usedMemory() + assert values["maxmemory"] == "0" + assert values["maxmemory_policy"] == "noeviction" + assert values["used_memory_human"] =~ ~r/^[0-9.]+[BKMG]$/ + + dataset = String.to_integer(values["used_memory_dataset"]) + overhead = String.to_integer(values["used_memory_overhead"]) + assert dataset + overhead == String.to_integer(values["used_memory"]) + end + + test "the keyspace section counts live keys, volatile keys and the mean ttl", %{conn: conn} do + assert {text, conn} = Client.command(conn, ["INFO", "keyspace"]) + refute String.contains?(text, "db0") + + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "plain", "one"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "other", "two"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "timed", "three"]) + assert {1, conn} = Client.command(conn, ["EXPIRE", "timed", "120"]) + + assert {text, _conn} = Client.command(conn, ["INFO", "keyspace"]) + assert %{"db0" => summary} = fields(text) + + assert [keys, expires, avgTtl] = + Regex.run(~r/keys=(\d+),expires=(\d+),avg_ttl=(\d+)/, summary, + capture: :all_but_first + ) + + assert keys == "3" + assert expires == "1" + assert String.to_integer(avgTtl) > 118_000 + assert String.to_integer(avgTtl) <= 120_000 + end + + test "the persistence section reads the real append only file", %{conn: conn} do + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "durable", "value"]) + :ok = Aof.flush() + + assert {text, _conn} = Client.command(conn, ["INFO", "persistence"]) + values = fields(text) + + assert values["aof_enabled"] == "1" + assert values["aof_fsync_policy"] == "everysec" + assert values["loading"] == "0" + + size = String.to_integer(values["aof_current_size"]) + assert size > 0 + assert size == File.stat!(Aof.path()).size + end + + test "the stats section counts connections and eviction refusals", %{conn: conn, port: port} do + assert {text, conn} = Client.command(conn, ["INFO", "stats"]) + before = String.to_integer(fields(text)["total_connections_received"]) + + extra = Client.connect(port) + assert {{:simple, "PONG"}, _extra} = Client.command(extra, ["PING"]) + + assert {text, conn} = Client.command(conn, ["INFO", "stats"]) + values = fields(text) + + assert String.to_integer(values["total_connections_received"]) == before + 1 + assert String.to_integer(values["total_net_input_bytes"]) > 0 + assert values["evicted_keys"] == "0" + assert values["oom_rejections"] == "0" + + assert {{:simple, "OK"}, conn} = + Client.command(conn, ["SET", "filler", String.duplicate("x", 128)]) + + assert {{:simple, "OK"}, conn} = Client.command(conn, ["CONFIG", "SET", "maxmemory", "1"]) + assert {{:error, "OOM" <> _}, conn} = Client.command(conn, ["SET", "toobig", "value"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["CONFIG", "SET", "maxmemory", "0"]) + + assert {text, _conn} = Client.command(conn, ["INFO", "stats"]) + assert String.to_integer(fields(text)["oom_rejections"]) > 0 + + Client.close(extra) + end + + test "the replication section tells the truth about being a lone master", %{conn: conn} do + assert {text, _conn} = Client.command(conn, ["INFO", "replication"]) + values = fields(text) + + assert values["role"] == "master" + assert values["connected_slaves"] == "0" + assert values["master_failover_state"] == "no-failover" + assert values["repl_backlog_active"] == "0" + end + + test "INFO all and INFO everything add the cpu section", %{conn: conn} do + assert {plain, conn} = Client.command(conn, ["INFO"]) + refute "Cpu" in sections(plain) + + assert {all, conn} = Client.command(conn, ["INFO", "all"]) + assert "Cpu" in sections(all) + + assert {everything, conn} = Client.command(conn, ["INFO", "everything"]) + assert sections(everything) == sections(all) +