From fb085d0d90bc80638f7554322189f15c9453c72b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 21:14:19 +0200 Subject: [PATCH] test: cover Excache.StoreOpsRegressionTest --- test/excache/store_ops_regression_test.exs | 158 +++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test/excache/store_ops_regression_test.exs diff --git a/test/excache/store_ops_regression_test.exs b/test/excache/store_ops_regression_test.exs new file mode 100644 index 0000000..1b6afec --- /dev/null +++ b/test/excache/store_ops_regression_test.exs @@ -0,0 +1,158 @@ +defmodule Excache.StoreOpsRegressionTest do + use Excache.Test.ServerCase, async: false + + alias Excache.Store + + describe "SMOVE must not drop the member when the destination has the wrong type" do + test "the member stays in the source and the caller is told about the type clash" do + source = "regr:smove:src" + dest = "regr:smove:dst-string" + + assert {:ok, 1} = Store.sadd(source, ["alpha"]) + assert :ok = Store.set(dest, "not-a-set", []) + + assert {:error, :wrongtype} = Store.smove(source, dest, "alpha") + assert {:ok, ["alpha"]} = Store.smembers(source) + assert {:ok, "not-a-set"} = Store.get(dest) + end + + test "the destination type is checked before the source is touched" do + source = "regr:smove:absent-src" + dest = "regr:smove:dst-list" + + assert {:ok, 1} = Store.rpush(dest, ["element"]) + + assert {:error, :wrongtype} = Store.smove(source, dest, "alpha") + assert {:ok, ["element"]} = Store.lrange(dest, 0, -1) + end + + test "a wrong type on the source is reported and the destination is left alone" do + source = "regr:smove:src-string" + dest = "regr:smove:dst-set" + + assert :ok = Store.set(source, "not-a-set", []) + assert {:ok, 1} = Store.sadd(dest, ["beta"]) + + assert {:error, :wrongtype} = Store.smove(source, dest, "alpha") + assert {:ok, ["beta"]} = Store.smembers(dest) + end + + test "a healthy move across two shards still transfers the member exactly once" do + source = "regr:smove:move-src" + dest = keyInOtherShard("regr:smove:move-dst-", source) + + assert {:ok, 2} = Store.sadd(source, ["alpha", "gamma"]) + assert {:ok, 1} = Store.sadd(dest, ["beta"]) + + assert {:ok, 1} = Store.smove(source, dest, "alpha") + assert {:ok, ["gamma"]} = Store.smembers(source) + assert {:ok, members} = Store.smembers(dest) + assert Enum.sort(members) == ["alpha", "beta"] + end + + test "moving a member the source does not hold reports zero and adds nothing" do + source = "regr:smove:miss-src" + dest = "regr:smove:miss-dst" + + assert {:ok, 1} = Store.sadd(source, ["alpha"]) + assert {:ok, 1} = Store.sadd(dest, ["beta"]) + + assert {:ok, 0} = Store.smove(source, dest, "nothing-like-this") + assert {:ok, ["alpha"]} = Store.smembers(source) + assert {:ok, ["beta"]} = Store.smembers(dest) + end + + test "SMOVE over RESP reports WRONGTYPE and keeps the member", %{conn: conn} do + assert {1, conn} = Client.command(conn, ["SADD", "regr:smove:resp-src", "alpha"]) + assert {{:simple, "OK"}, conn} = Client.command(conn, ["SET", "regr:smove:resp-dst", "s"]) + + assert {{:error, "WRONGTYPE" <> _}, conn} = + Client.command(conn, [ + "SMOVE", + "regr:smove:resp-src", + "regr:smove:resp-dst", + "alpha" + ]) + + assert {["alpha"], _conn} = Client.command(conn, ["SMEMBERS", "regr:smove:resp-src"]) + end + end + + describe "RPOPLPUSH must not drop the element when the destination has the wrong type" do + test "the element stays at the tail of the source and the caller sees the error" do + source = "regr:rpoplpush:src" + dest = "regr:rpoplpush:dst-string" + + assert {:ok, 2} = Store.rpush(source, ["head", "tail"]) + assert :ok = Store.set(dest, "not-a-list", []) + + assert {:error, :wrongtype} = Store.rpoplpush(source, dest) + assert {:ok, ["head", "tail"]} = Store.lrange(source, 0, -1) + assert {:ok, "not-a-list"} = Store.get(dest) + end + + test "the destination type is checked before the source is popped" do + source = "regr:rpoplpush:absent-src" + dest = "regr:rpoplpush:dst-set" + + assert {:ok, 1} = Store.sadd(dest, ["beta"]) + + assert {:error, :wrongtype} = Store.rpoplpush(source, dest) + assert {:ok, ["beta"]} = Store.smembers(dest) + end + + test "a healthy rotation across two shards moves the tail to the head" do + source = "regr:rpoplpush:rot-src" + dest = keyInOtherShard("regr:rpoplpush:rot-dst-", source) + + assert {:ok, 2} = Store.rpush(source, ["head", "tail"]) + assert {:ok, 1} = Store.rpush(dest, ["existing"]) + + assert {:ok, "tail"} = Store.rpoplpush(source, dest) + assert {:ok, ["head"]} = Store.lrange(source, 0, -1) + assert {:ok, ["tail", "existing"]} = Store.lrange(dest, 0, -1) + end + + test "an empty source yields nil and never creates the destination" do + source = "regr:rpoplpush:empty-src" + dest = "regr:rpoplpush:empty-dst" + + assert {:ok, nil} = Store.rpoplpush(source, dest) + assert 0 == Store.exists([dest]) + end + + test "RPOPLPUSH over RESP reports WRONGTYPE and keeps the element", %{conn: conn} do + assert {1, conn} = Client.command(conn, ["RPUSH", "regr:rpoplpush:resp-src", "tail"]) + + assert {{:simple, "OK"}, conn} = + Client.command(conn, ["SET", "regr:rpoplpush:resp-dst", "s"]) + + assert {{:error, "WRONGTYPE" <> _}, conn} = + Client.command(conn, [ + "RPOPLPUSH", + "regr:rpoplpush:resp-src", + "regr:rpoplpush:resp-dst" + ]) + + assert {["tail"], _conn} = + Client.command(conn, ["LRANGE", "regr:rpoplpush:resp-src", "0", "-1"]) + end + end + + describe "LPOS with a nil count must not crash the shard" do + test "a nil count means the first match and the shard survives the call" do + key = "regr:lpos:nil-count" + assert {:ok, 4} = Store.rpush(key, ["a", "b", "c", "b"]) + shardBefore = shardPid(key) + + assert {:ok, 1} = Store.lpos(key, "b", 1, nil, 0) + + assert shardPid(key) == shardBefore + assert Process.alive?(shardBefore) + assert {:ok, ["a", "b", "c", "b"]} = Store.lrange(key, 0, -1) + end + + test "a nil count must not take the shard down and wipe its other keys" do + key = "regr:lpos:blast-radius" + neighbour = keyInSameShard("regr:lpos:neighbour-", key) +