Repository: microsoft/garnet
File: test/Garnet.test/RespCustomCommandTests.cs (C#)

Code before:
                valueToMessWith.RemoveAt(valueToMessWith.Count - 1);
            }

            RawStringInput input = new RawStringInput(RespCommand.SET);
            input.header.cmd = RespCommand.SET;
            // if we send a SET we must explictly ask it to retain etag, and use conditional set
            input.header.SetWithEtagFlag();

            fixed (byte* valuePtr = valueToMessWith.ToArray())
            {
                ArgSlice valForKey1 = new ArgSlice(valuePtr, valueToMessWith.Count);
                input.parseState.InitializeWithArgument(valForKey1);
                // since we are setting with retain to etag, this change should be reflected in an etag update
                SpanByte sameKeyToUse = key.SpanByte;
                garnetApi.SET_Conditional(ref sameKeyToUse, ref input);
            }


            var keyToIncrment = GetNextArg(ref procInput, ref offset);

            // for a non SET command the etag should be invisible and be updated automatically
            garnetApi.Increment(keyToIncrment, out long _, 1);
        }
    }

    public class ListPopCommandProc : CustomTransactionProcedure
    {
        public override bool Prepare<TGarnetReadApi>(TGarnetReadApi api, ref CustomProcedureInput procInput)
        {
            int offset = 0;
            AddKey(GetNextArg(ref procInput, ref offset), LockType.Exclusive, true);
            return true;
        }

        public override void Main<TGarnetApi>(TGarnetApi api, ref CustomProcedureInput procInput,
            ref MemoryResult<byte> output)
        {
            int offset = 0;
            var key = GetNextArg(ref procInput, ref offset);
            var status = api.ListLeftPop(key, out ArgSlice value);
            WriteSimpleString(ref output, status == GarnetStatus.OK ? value.ToString() : "NOTFOUND");
        }
    }

    public class HGetCommandProc : CustomTransactionProcedure
    {
        public override bool Prepare<TGarnetReadApi>(TGarnetReadApi api, ref CustomProcedureInput procInput)
        {
            int offset = 0;
            AddKey(GetNextArg(ref procInput, ref offset), LockType.Shared, true);
            return true;

Review comment (suggestion):
I think that instead of creating new procedures for these two specific commands, you can add the api calls to `TestProcedureHash` and `TestProcedureLists`

Code after:
                valueToMessWith.RemoveAt(valueToMessWith.Count - 1);
            }

            RawStringInput input = new RawStringInput(RespCommand.SET);
            input.header.cmd = RespCommand.SET;
            // if we send a SET we must explictly ask it to retain etag, and use conditional set
            input.header.SetWithEtagFlag();

            fixed (byte* valuePtr = valueToMessWith.ToArray())
            {
                ArgSlice valForKey1 = new ArgSlice(valuePtr, valueToMessWith.Count);
                input.parseState.InitializeWithArgument(valForKey1);
                // since we are setting with retain to etag, this change should be reflected in an etag update
                SpanByte sameKeyToUse = key.SpanByte;
                garnetApi.SET_Conditional(ref sameKeyToUse, ref input);
            }


            var keyToIncrment = GetNextArg(ref procInput, ref offset);

            // for a non SET command the etag should be invisible and be updated automatically
            garnetApi.Increment(keyToIncrment, out long _, 1);
        }
    }

    [TestFixture]
    public class RespCustomCommandTests
    {
        GarnetServer server;
        private string _extTestDir1;
        private string _extTestDir2;

        [SetUp]
        public void Setup()
        {
            _extTestDir1 = Path.Combine(TestUtils.MethodTestDir, "test1");
            _extTestDir2 = Path.Combine(TestUtils.MethodTestDir, "test2");

            TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true);
            server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir,
                disablePubSub: true,
                extensionBinPaths: [_extTestDir1, _extTestDir2],
                extensionAllowUnsignedAssemblies: true);
            server.Start();
        }

        [TearDown]
        public void TearDown()
        {
            server.Dispose();
            TestUtils.DeleteDirectory(TestUtils.MethodTestDir);