Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CC-1299: Update RESP framework to accomodate Transaction tests elegantly #153

Merged
merged 41 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
39df651
feat: Add ordered array assertion for RESP values
ryan-gang Jun 12, 2024
fa84532
feat:
ryan-gang Jun 12, 2024
08ac41d
feat: Add transaction test cases for transaction execution
ryan-gang Jun 12, 2024
5f7be75
feat: Add testTxMulti function for executing multiple transactions
ryan-gang Jun 12, 2024
af0e0d9
feat: Add bytes field to Value struct for Ints and Nils
ryan-gang Jun 12, 2024
7087489
feat: Refactor testTxMulti function to use MultiCommandTestCase
ryan-gang Jun 12, 2024
9e28d76
feat: Add `ErrorAssertion` for comparing error values in tests
ryan-gang Jun 12, 2024
d82bbb6
feat: refactor testTxDiscard function to use MultiCommandTestCase
ryan-gang Jun 12, 2024
cf7e938
fix: revert tester definition update
ryan-gang Jun 12, 2024
67b14ec
docs: update module level docstring
ryan-gang Jun 12, 2024
fecf766
feat: add tests for stages 1 - 3
ryan-gang Jun 13, 2024
fb6e805
feat: Add testTxMulti function to internal package
ryan-gang Jun 13, 2024
11c4a5c
feat: Add testTxEmpty function to internal package
ryan-gang Jun 13, 2024
2c4393a
feat: Add testTxQueue function for testing transaction queue
ryan-gang Jun 13, 2024
2003c23
feat: Add testTxExec function
ryan-gang Jun 13, 2024
c1e381f
feat: update TransactionTestCase to even run with empty results array
ryan-gang Jun 13, 2024
a5de8c0
feat: Add multiple clients to testTxErr, testTxSuccess, and testTxDis…
ryan-gang Jun 13, 2024
983add3
feat: Add `spawnClients` function
ryan-gang Jun 13, 2024
035058f
feat: Update function name and add documentation for spawning clients
ryan-gang Jun 13, 2024
b0e7962
feat: update tester_definition with new test cases for transactions
ryan-gang Jun 13, 2024
0bd8bac
tests: add fixtures for transaction stages
ryan-gang Jun 13, 2024
28be476
refactor: Rename test stages from numbered stages to named stages
ryan-gang Jun 13, 2024
a3957c1
refactor: improve transaction test cases
ryan-gang Jun 13, 2024
f2c6762
feat: add placeholder stage descriptions
ryan-gang Jun 13, 2024
a764c12
feat: Add support for Transactions extension
ryan-gang Jun 13, 2024
7b04ff9
refactor: Update testTxMulti to use TransactionTestCase
ryan-gang Jun 14, 2024
715817c
feat: Refactor transaction test cases
ryan-gang Jun 14, 2024
571cdf7
feat: add randomness to test case for transaction failure
ryan-gang Jun 14, 2024
a37aa7b
feat: add randomness to test case for transaction success
ryan-gang Jun 14, 2024
af87ab5
feat: add random key generation for INCR test cases
ryan-gang Jun 14, 2024
4bf9120
fix: make sure all individual keys and values are always unique
ryan-gang Jun 14, 2024
63b9b01
feat: Add randomness to test cases for transaction discard
ryan-gang Jun 14, 2024
263a9cb
feat: add randomness to concurrent txn test
ryan-gang Jun 14, 2024
3f032d4
tests: update fixtures
ryan-gang Jun 14, 2024
6690867
Merge pull request #154 from codecrafters-io/CC-1299-txn-stages
ryan-gang Jun 14, 2024
634dcce
fix: update naming to match standards, update internal error codes
ryan-gang Jun 14, 2024
77a2189
refactor: refactor transaction test case to accept an array of assert…
ryan-gang Jun 14, 2024
a3b7777
tests: update fixtures
ryan-gang Jun 14, 2024
b665420
fix: use quoted values in error assertion failure logs
ryan-gang Jun 14, 2024
f89d1ff
fix: add test to make sure INCR stores string value and not int
ryan-gang Jun 14, 2024
b1bebb4
tests: update fixtures
ryan-gang Jun 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion internal/resp/value/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func NewIntegerValue(i int) Value {
return Value{
Type: INTEGER,
integer: i,
bytes: []byte(fmt.Sprintf("%d", i)),
ryan-gang marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -70,7 +71,8 @@ func NewArrayValue(arr []Value) Value {

func NewNilValue() Value {
return Value{
Type: NIL,
Type: NIL,
bytes: []byte("$-1\r\n"),
ryan-gang marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand All @@ -94,6 +96,10 @@ func (v *Value) Integer() int {
return v.integer
}

func (v *Value) Error() string {
return string(v.String())
}

func (v *Value) FormattedString() string {
switch v.Type {
case SIMPLE_STRING:
Expand Down
27 changes: 27 additions & 0 deletions internal/resp_assertions/error_assertion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package resp_assertions

import (
"fmt"

resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value"
)

type ErrorAssertion struct {
ExpectedValue string
}

func NewErrorAssertion(expectedValue string) RESPAssertion {
return ErrorAssertion{ExpectedValue: expectedValue}
}

func (a ErrorAssertion) Run(value resp_value.Value) error {
if value.Type != resp_value.ERROR {
return fmt.Errorf("Expected error, got %s", value.Type)
}

if value.Error() != a.ExpectedValue {
return fmt.Errorf("Expected %s, got %s", a.ExpectedValue, value.Error())
ryan-gang marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
}
50 changes: 50 additions & 0 deletions internal/resp_assertions/ordered_array_assertion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package resp_assertions

import (
"bytes"
"fmt"

resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value"
)

// OrderedArrayAssertion : Order of the actual and expected values matters.
// All RESP values are accepted as elements in this array.
// We don't alter the ordering.
type OrderedArrayAssertion struct {
ExpectedValue []resp_value.Value
}

func NewOrderedArrayAssertion(expectedValue []resp_value.Value) RESPAssertion {
return OrderedArrayAssertion{ExpectedValue: expectedValue}
}

func (a OrderedArrayAssertion) Run(value resp_value.Value) error {
if value.Type != resp_value.ARRAY {
return fmt.Errorf("Expected an array, got %s", value.Type)
}

if len(value.Array()) != len(a.ExpectedValue) {
return fmt.Errorf("Expected %d elements in array, got %d (%s)", len(a.ExpectedValue), len(value.Array()), value.FormattedString())
}

for i, expectedValue := range a.ExpectedValue {
ryan-gang marked this conversation as resolved.
Show resolved Hide resolved
actualElement := value.Array()[i]

if actualElement.Type != expectedValue.Type {
return fmt.Errorf("Expected element #%d to be a %s, got %s", i+1, expectedValue.Type, actualElement.Type)
}

if expectedValue.Bytes() == nil {
// This should never happen, but just in case
// This is the case for ArrayValues
return fmt.Errorf("CodeCrafters internal error. expectedValue Bytes of type: %s is nil", expectedValue.Type)
}

// ToDo: Equal or EqualFold ?
if !bytes.Equal(actualElement.Bytes(), expectedValue.Bytes()) {
return fmt.Errorf("Expected element #%d to be %s, got %s", i+1, expectedValue.FormattedString(), actualElement.FormattedString())
ryan-gang marked this conversation as resolved.
Show resolved Hide resolved
}
}

return nil
}
35 changes: 35 additions & 0 deletions internal/test_cases/multi_command_test_case.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package test_cases

import (
"fmt"

resp_client "github.com/codecrafters-io/redis-tester/internal/resp/connection"
"github.com/codecrafters-io/redis-tester/internal/resp_assertions"
"github.com/codecrafters-io/tester-utils/logger"
)

// MultiCommandTestCase is a concise & easier way to define & run multiple SendCommandTestCase
type MultiCommandTestCase struct {
Commands [][]string
Assertions []resp_assertions.RESPAssertion
}

func (t *MultiCommandTestCase) RunAll(client *resp_client.RespConnection, logger *logger.Logger) error {
if len(t.Assertions) != len(t.Commands) {
return fmt.Errorf("Number of commands and assertions should be equal")
ryan-gang marked this conversation as resolved.
Show resolved Hide resolved
}

for i, command := range t.Commands {
setCommandTestCase := SendCommandTestCase{
Command: command[0],
Args: command[1:],
Assertion: t.Assertions[i],
}

if err := setCommandTestCase.Run(client, logger); err != nil {
return err
}
}

return nil
}
79 changes: 79 additions & 0 deletions internal/test_cases/transaction_test_case.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package test_cases

import (
resp_client "github.com/codecrafters-io/redis-tester/internal/resp/connection"
resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value"
"github.com/codecrafters-io/redis-tester/internal/resp_assertions"
"github.com/codecrafters-io/tester-utils/logger"
)

// TransactionTestCase is a test case where we initiate a transaction by sending "MULTI" command
// Send a series of commands to the server expected back "QUEUED" for each command
// Finally send "EXEC" command and expect the response to be the same as ResultArray
//
// RunAll will run all the steps in the Transaction execution. Alternatively, you
// can run each step individually.
type TransactionTestCase struct {
// All the CommandQueue will be sent in order to client
// And a string "QUEUED" will be expected
CommandQueue [][]string

// After queueing all the commands,
// if ResultArray is not empty, "EXEC" is sent
// And the response is compared with this ResultArray
ResultArray []resp_value.Value
ryan-gang marked this conversation as resolved.
Show resolved Hide resolved
}

func (t TransactionTestCase) RunAll(client *resp_client.RespConnection, logger *logger.Logger) error {
if err := t.RunMulti(client, logger); err != nil {
return err
}

if err := t.RunQueueAll(client, logger); err != nil {
return err
}

if len(t.ResultArray) > 0 {
if err := t.RunExec(client, logger); err != nil {
return err
}
}

return nil
}

func (t TransactionTestCase) RunMulti(client *resp_client.RespConnection, logger *logger.Logger) error {
rohitpaulk marked this conversation as resolved.
Show resolved Hide resolved
commandTest := SendCommandTestCase{
Command: "MULTI",
Args: []string{},
Assertion: resp_assertions.NewStringAssertion("OK"),
}

return commandTest.Run(client, logger)
}

func (t TransactionTestCase) RunQueueAll(client *resp_client.RespConnection, logger *logger.Logger) error {
for i, v := range t.CommandQueue {
logger.Debugf("Sent #%d command", i)
ryan-gang marked this conversation as resolved.
Show resolved Hide resolved
commandTest := SendCommandTestCase{
Command: v[0],
Args: v[1:],
Assertion: resp_assertions.NewStringAssertion("QUEUED"),
}
if err := commandTest.Run(client, logger); err != nil {
return err
}
}

return nil
}

func (t TransactionTestCase) RunExec(client *resp_client.RespConnection, logger *logger.Logger) error {
setCommandTestCase := SendCommandTestCase{
Command: "EXEC",
Args: []string{},
Assertion: resp_assertions.NewOrderedArrayAssertion(t.ResultArray),
}

return setCommandTestCase.Run(client, logger)
}
53 changes: 53 additions & 0 deletions internal/test_txn_10.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package internal

import (
"github.com/codecrafters-io/redis-tester/internal/redis_executable"
resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value"

"github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection"
"github.com/codecrafters-io/redis-tester/internal/resp_assertions"
"github.com/codecrafters-io/redis-tester/internal/test_cases"
"github.com/codecrafters-io/tester-utils/test_case_harness"
)

func testTxErr(stageHarness *test_case_harness.TestCaseHarness) error {

Check failure on line 13 in internal/test_txn_10.go

View workflow job for this annotation

GitHub Actions / lint

func testTxErr is unused (U1000)
b := redis_executable.NewRedisExecutable(stageHarness)
if err := b.Run(); err != nil {
return err
}

logger := stageHarness.Logger

client, err := instrumented_resp_connection.NewFromAddr(stageHarness, "localhost:6379", "client1")
if err != nil {
logFriendlyError(logger, err)
return err
}
defer client.Close()

multiCommandTestCase := test_cases.MultiCommandTestCase{
Commands: [][]string{
{"SET", "foo", "abc"},
{"SET", "bar", "7"},
},
Assertions: []resp_assertions.RESPAssertion{
resp_assertions.NewStringAssertion("OK"),
resp_assertions.NewStringAssertion("OK"),
},
}

if err := multiCommandTestCase.RunAll(client, logger); err != nil {
return err
}

transactionTestCase := test_cases.TransactionTestCase{
CommandQueue: [][]string{
{"INCR", "foo"},
{"INCR", "bar"},
},
ResultArray: []resp_value.Value{
resp_value.NewErrorValue("ERR value is not an integer or out of range"), resp_value.NewIntegerValue(8)},
}

return transactionTestCase.RunAll(client, logger)
}
82 changes: 82 additions & 0 deletions internal/test_txn_11.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package internal

import (
"github.com/codecrafters-io/redis-tester/internal/redis_executable"
resp_connection "github.com/codecrafters-io/redis-tester/internal/resp/connection"
resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value"
"github.com/codecrafters-io/redis-tester/internal/resp_assertions"

"github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection"
"github.com/codecrafters-io/redis-tester/internal/test_cases"
"github.com/codecrafters-io/tester-utils/test_case_harness"
)

func testTxMulti(stageHarness *test_case_harness.TestCaseHarness) error {

Check failure on line 14 in internal/test_txn_11.go

View workflow job for this annotation

GitHub Actions / lint

func testTxMulti is unused (U1000)
b := redis_executable.NewRedisExecutable(stageHarness)
if err := b.Run(); err != nil {
return err
}

logger := stageHarness.Logger

var clients []*resp_connection.RespConnection

for i := 0; i < 3; i++ {
client, err := instrumented_resp_connection.NewFromAddr(stageHarness, "localhost:6379", "client1")
if err != nil {
logFriendlyError(logger, err)
return err
}
clients = append(clients, client)
defer client.Close()
}

for i, client := range clients {
multiCommandTestCase := test_cases.MultiCommandTestCase{
Commands: [][]string{
{"SET", "bar", "7"},
{"INCR", "foo"},
},
Assertions: []resp_assertions.RESPAssertion{
resp_assertions.NewStringAssertion("OK"),
resp_assertions.NewIntegerAssertion(i + 1),
},
}

if err := multiCommandTestCase.RunAll(client, logger); err != nil {
return err
}
}

for i, client := range clients {
transactionTestCase := test_cases.TransactionTestCase{
CommandQueue: [][]string{
{"INCR", "foo"},
{"INCR", "bar"},
},
ResultArray: []resp_value.Value{resp_value.NewIntegerValue(4 + i), resp_value.NewIntegerValue(8 + i)},
}
if err := transactionTestCase.RunMulti(client, logger); err != nil {
return err
}

if err := transactionTestCase.RunQueueAll(client, logger); err != nil {
return err
}
}

for i, client := range clients {
transactionTestCase := test_cases.TransactionTestCase{
CommandQueue: [][]string{
{"INCR", "foo"},
{"INCR", "bar"},
},
ResultArray: []resp_value.Value{resp_value.NewIntegerValue(4 + i), resp_value.NewIntegerValue(8 + i)},
}
if err := transactionTestCase.RunExec(client, logger); err != nil {
return err
}
}

return nil
}
38 changes: 38 additions & 0 deletions internal/test_txn_8.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package internal

import (
"github.com/codecrafters-io/redis-tester/internal/redis_executable"
resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value"

"github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection"
"github.com/codecrafters-io/redis-tester/internal/test_cases"
"github.com/codecrafters-io/tester-utils/test_case_harness"
)

func testTxSuccess(stageHarness *test_case_harness.TestCaseHarness) error {

Check failure on line 12 in internal/test_txn_8.go

View workflow job for this annotation

GitHub Actions / lint

func testTxSuccess is unused (U1000)
b := redis_executable.NewRedisExecutable(stageHarness)
if err := b.Run(); err != nil {
return err
}

logger := stageHarness.Logger

client, err := instrumented_resp_connection.NewFromAddr(stageHarness, "localhost:6379", "client1")
if err != nil {
logFriendlyError(logger, err)
return err
}
defer client.Close()

transactionTestCase := test_cases.TransactionTestCase{
CommandQueue: [][]string{
{"SET", "foo", "6"},
{"INCR", "foo"},
{"INCR", "bar"},
{"GET", "bar"},
},
ResultArray: []resp_value.Value{resp_value.NewSimpleStringValue("OK"), resp_value.NewIntegerValue(7), resp_value.NewIntegerValue(1), resp_value.NewBulkStringValue("1")},
}

return transactionTestCase.RunAll(client, logger)
}
Loading
Loading