Skip to content

Commit

Permalink
Merge pull request #153 from codecrafters-io/CC-1299-txn
Browse files Browse the repository at this point in the history
CC-1299: Implement Transactions extension tests
  • Loading branch information
ryan-gang committed Jun 14, 2024
2 parents b4e9649 + b1bebb4 commit d8ba70b
Show file tree
Hide file tree
Showing 25 changed files with 3,365 additions and 219 deletions.
38 changes: 22 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,6 @@ build:
test:
go test -count=1 -p 1 -v ./internal/...

test_with_redis: build
CODECRAFTERS_SUBMISSION_DIR=./internal/test_helpers/pass_all \
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"repl-wait\",\"tester_log_prefix\":\"stage-118\",\"title\":\"Stage #118: WAIT Command\"}]" \
dist/main.out


test_tmp: build
cd /Users/ryang/Developer/byox/build-your-own-redis && \
CODECRAFTERS_SUBMISSION_DIR=/Users/ryang/Developer/byox/build-your-own-redis \
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"zg5\",\"tester_log_prefix\":\"stage-201\",\"title\":\"Stage #1: RDB Config\"}, {\"slug\":\"jz6\",\"tester_log_prefix\":\"stage-202\",\"title\":\"Stage #2: RDB Read Key\"}, {\"slug\":\"gc6\",\"tester_log_prefix\":\"stage-203\",\"title\":\"Stage #3: RDB String Value\"}, {\"slug\":\"jw4\",\"tester_log_prefix\":\"stage-204\",\"title\":\"Stage #4: RDB Read Multiple Keys\"}, {\"slug\":\"dq3\",\"tester_log_prefix\":\"stage-205\",\"title\":\"Stage #5: RDB Read Multiple String Values\"}, {\"slug\":\"sm4\",\"tester_log_prefix\":\"stage-206\",\"title\":\"Stage #6: RDB Read Value With Expiry\"}]" \
$(shell pwd)/dist/main.out

copy_course_file:
gh api repos/codecrafters-io/build-your-own-redis/contents/course-definition.yml \
| jq -r .content \
Expand All @@ -41,9 +29,20 @@ record_fixtures:
update_tester_utils:
go get -u github.com/codecrafters-io/tester-utils

test_all_with_redis: build
test_dev: build
cd /Users/ryang/Developer/byox/build-your-own-redis && \
CODECRAFTERS_SUBMISSION_DIR=/Users/ryang/Developer/byox/build-your-own-redis \
CODECRAFTERS_TEST_CASES_JSON="[]" \
$(shell pwd)/dist/main.out

test_base_with_redis: build
CODECRAFTERS_SUBMISSION_DIR=./internal/test_helpers/pass_all \
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"jm1\",\"tester_log_prefix\":\"stage-1\",\"title\":\"Stage #1: Bind to a port\"},{\"slug\":\"rg2\",\"tester_log_prefix\":\"stage-2\",\"title\":\"Stage #2: Respond to PING\"},{\"slug\":\"wy1\",\"tester_log_prefix\":\"stage-3\",\"title\":\"Stage #3: Respond to multiple PINGs\"},{\"slug\":\"zu2\",\"tester_log_prefix\":\"stage-4\",\"title\":\"Stage #4: Handle concurrent clients\"},{\"slug\":\"qq0\",\"tester_log_prefix\":\"stage-5\",\"title\":\"Stage #5: Implement the ECHO command\"},{\"slug\":\"la7\",\"tester_log_prefix\":\"stage-6\",\"title\":\"Stage #6: Implement the SET \u0026 GET commands\"},{\"slug\":\"yz1\",\"tester_log_prefix\":\"stage-7\",\"title\":\"Stage #7: Expiry\"},{\"slug\":\"bw1\",\"tester_log_prefix\":\"stage-101\",\"title\":\"Stage #101: Replication - Custom Port\"}, {\"slug\":\"ye5\",\"tester_log_prefix\":\"stage-102\",\"title\":\"Stage #102: Replication - Info on Master\"},{\"slug\":\"hc6\",\"tester_log_prefix\":\"stage-103\",\"title\":\"Stage #103: Replication - Info on Replica\"}, {\"slug\":\"xc1\",\"tester_log_prefix\":\"stage-104\",\"title\":\"Stage #104: Replication - Replication ID and Offset\"}, {\"slug\":\"gl7\",\"tester_log_prefix\":\"stage-105\",\"title\":\"Stage #105: Replication - Handshake 1\"},{\"slug\":\"eh4\",\"tester_log_prefix\":\"stage-106\",\"title\":\"Stage #106: Replication - Handshake 2\"},{\"slug\":\"ju6\",\"tester_log_prefix\":\"stage-107\",\"title\":\"Stage #107: Replication - Handshake 3\"},{\"slug\":\"fj0\",\"tester_log_prefix\":\"stage-108\",\"title\":\"Stage #108: Replication - REPLCONF\"},{\"slug\":\"vm3\",\"tester_log_prefix\":\"stage-109\",\"title\":\"Stage #109: Replication - PSYNC\"},{\"slug\":\"cf8\",\"tester_log_prefix\":\"stage-110\",\"title\":\"Stage #110: Replication - PSYNC w RDB file\"},{\"slug\":\"zn8\",\"tester_log_prefix\":\"stage-111\",\"title\":\"Stage #111: Command Propagation\"},{\"slug\":\"hd5\",\"tester_log_prefix\":\"stage-112\",\"title\":\"Stage #112: Command Propagation to multiple Replicas\"},{\"slug\":\"yg4\",\"tester_log_prefix\":\"stage-113\",\"title\":\"Stage #113: Command Processing\"},{\"slug\":\"xv6\",\"tester_log_prefix\":\"stage-114\",\"title\":\"Stage #114: GetAck with 0 offset\"},{\"slug\":\"yd3\",\"tester_log_prefix\":\"stage-115\",\"title\":\"Stage #115: GetAck with non-0 offset\"},{\"slug\":\"my8\",\"tester_log_prefix\":\"stage-116\",\"title\":\"Stage #116: WAIT with 0 replicas\"},{\"slug\":\"tu8\",\"tester_log_prefix\":\"stage-117\",\"title\":\"Stage #117: WAIT with 0 offset\"},{\"slug\":\"na2\",\"tester_log_prefix\":\"stage-118\",\"title\":\"Stage #118: WAIT Command\"}]" \
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"jm1\",\"tester_log_prefix\":\"stage-1\",\"title\":\"Stage #1: Bind to a port\"},{\"slug\":\"rg2\",\"tester_log_prefix\":\"stage-2\",\"title\":\"Stage #2: Respond to PING\"},{\"slug\":\"wy1\",\"tester_log_prefix\":\"stage-3\",\"title\":\"Stage #3: Respond to multiple PINGs\"},{\"slug\":\"zu2\",\"tester_log_prefix\":\"stage-4\",\"title\":\"Stage #4: Handle concurrent clients\"},{\"slug\":\"qq0\",\"tester_log_prefix\":\"stage-5\",\"title\":\"Stage #5: Implement the ECHO command\"},{\"slug\":\"la7\",\"tester_log_prefix\":\"stage-6\",\"title\":\"Stage #6: Implement the SET \u0026 GET commands\"},{\"slug\":\"yz1\",\"tester_log_prefix\":\"stage-7\",\"title\":\"Stage #7: Expiry\"}]" \
dist/main.out

test_repl_with_redis: build
CODECRAFTERS_SUBMISSION_DIR=./internal/test_helpers/pass_all \
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"bw1\",\"tester_log_prefix\":\"stage-101\",\"title\":\"Stage #101: Replication - Custom Port\"}, {\"slug\":\"ye5\",\"tester_log_prefix\":\"stage-102\",\"title\":\"Stage #102: Replication - Info on Master\"},{\"slug\":\"hc6\",\"tester_log_prefix\":\"stage-103\",\"title\":\"Stage #103: Replication - Info on Replica\"}, {\"slug\":\"xc1\",\"tester_log_prefix\":\"stage-104\",\"title\":\"Stage #104: Replication - Replication ID and Offset\"}, {\"slug\":\"gl7\",\"tester_log_prefix\":\"stage-105\",\"title\":\"Stage #105: Replication - Handshake 1\"},{\"slug\":\"eh4\",\"tester_log_prefix\":\"stage-106\",\"title\":\"Stage #106: Replication - Handshake 2\"},{\"slug\":\"ju6\",\"tester_log_prefix\":\"stage-107\",\"title\":\"Stage #107: Replication - Handshake 3\"},{\"slug\":\"fj0\",\"tester_log_prefix\":\"stage-108\",\"title\":\"Stage #108: Replication - REPLCONF\"},{\"slug\":\"vm3\",\"tester_log_prefix\":\"stage-109\",\"title\":\"Stage #109: Replication - PSYNC\"},{\"slug\":\"cf8\",\"tester_log_prefix\":\"stage-110\",\"title\":\"Stage #110: Replication - PSYNC w RDB file\"},{\"slug\":\"zn8\",\"tester_log_prefix\":\"stage-111\",\"title\":\"Stage #111: Command Propagation\"},{\"slug\":\"hd5\",\"tester_log_prefix\":\"stage-112\",\"title\":\"Stage #112: Command Propagation to multiple Replicas\"},{\"slug\":\"yg4\",\"tester_log_prefix\":\"stage-113\",\"title\":\"Stage #113: Command Processing\"},{\"slug\":\"xv6\",\"tester_log_prefix\":\"stage-114\",\"title\":\"Stage #114: GetAck with 0 offset\"},{\"slug\":\"yd3\",\"tester_log_prefix\":\"stage-115\",\"title\":\"Stage #115: GetAck with non-0 offset\"},{\"slug\":\"my8\",\"tester_log_prefix\":\"stage-116\",\"title\":\"Stage #116: WAIT with 0 replicas\"},{\"slug\":\"tu8\",\"tester_log_prefix\":\"stage-117\",\"title\":\"Stage #117: WAIT with 0 offset\"},{\"slug\":\"na2\",\"tester_log_prefix\":\"stage-118\",\"title\":\"Stage #118: WAIT Command\"}]" \
dist/main.out

test_rdb_with_redis: build
Expand All @@ -56,7 +55,14 @@ test_streams_with_redis: build
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\": \"cc3\", \"tester_log_prefix\": \"stage-301\", \"title\": \"stage #01: StreamsType\"},{\"slug\": \"cf6\", \"tester_log_prefix\": \"stage-302\", \"title\": \"stage #02: StreamsXadd\"},{\"slug\": \"hq8\", \"tester_log_prefix\": \"stage-303\", \"title\": \"stage #03: StreamsXaddValidateID\"},{\"slug\": \"yh3\", \"tester_log_prefix\": \"stage-304\", \"title\": \"stage #04: StreamsXaddPartialAutoid\"},{\"slug\": \"xu6\", \"tester_log_prefix\": \"stage-305\", \"title\": \"stage #05: StreamsXaddFullAutoid\"},{\"slug\": \"zx1\", \"tester_log_prefix\": \"stage-306\", \"title\": \"stage #06: StreamsXrange\"},{\"slug\": \"yp1\", \"tester_log_prefix\": \"stage-307\", \"title\": \"stage #07: StreamsXrangeMinID\"},{\"slug\": \"fs1\", \"tester_log_prefix\": \"stage-308\", \"title\": \"stage #08: StreamsXrangeMaxID\"},{\"slug\": \"um0\", \"tester_log_prefix\": \"stage-309\", \"title\": \"stage #09: StreamsXread\"},{\"slug\": \"ru9\", \"tester_log_prefix\": \"stage-310\", \"title\": \"stage #10: StreamsXreadMultiple\"},{\"slug\": \"bs1\", \"tester_log_prefix\": \"stage-311\", \"title\": \"stage #11: StreamsXreadBlock\"},{\"slug\": \"hw1\", \"tester_log_prefix\": \"stage-312\", \"title\": \"stage #12: StreamsXreadBlockNoTimeout\"},{\"slug\": \"xu1\", \"tester_log_prefix\": \"stage-313\", \"title\": \"stage #13: StreamsXreadBlockMaxID\"}]" \
dist/main.out

test_dev: build
test_txn_with_redis: build
CODECRAFTERS_SUBMISSION_DIR=./internal/test_helpers/pass_all \
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"yg4\",\"tester_log_prefix\":\"stage-113\",\"title\":\"Stage #113: Command Processing\"}]" \
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"si4\",\"tester_log_prefix\":\"stage-401\",\"title\":\"Stage #401: INCR-1\"},{\"slug\":\"lz8\",\"tester_log_prefix\":\"stage-402\",\"title\":\"Stage #402: INCR-2\"}, {\"slug\":\"mk1\",\"tester_log_prefix\":\"stage-403\",\"title\":\"Stage #403: INCR-3\"}, {\"slug\":\"pn0\",\"tester_log_prefix\":\"stage-404\",\"title\":\"Stage #404: MULTI\"}, {\"slug\":\"lo4\",\"tester_log_prefix\":\"stage-405\",\"title\":\"Stage #405: EXEC\"}, {\"slug\":\"we1\",\"tester_log_prefix\":\"stage-406\",\"title\":\"Stage #406: Empty Transaction\"}, {\"slug\":\"rs9\",\"tester_log_prefix\":\"stage-407\",\"title\":\"Stage #407: Queueing Commands\"}, {\"slug\":\"fy6\",\"tester_log_prefix\":\"stage-408\",\"title\":\"Stage #408: Executing a transaction\"}, {\"slug\":\"rl9\",\"tester_log_prefix\":\"stage-409\",\"title\":\"Stage #409: Discarding a transaction\"}, {\"slug\":\"sg9\",\"tester_log_prefix\":\"stage-410\",\"title\":\"Stage #410: Executing a failed transaction\"}, {\"slug\":\"jf8\",\"tester_log_prefix\":\"stage-411\",\"title\":\"Stage #411: Executing concurrent transactions\"}]" \
dist/main.out

test_all_with_redis:
make test_base_with_redis || true
make test_repl_with_redis || true
make test_rdb_with_redis || true
make test_streams_with_redis || true
make test_txn_with_redis || true
7 changes: 7 additions & 0 deletions internal/resp/value/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ func (v *Value) Integer() int {
return v.integer
}

func (v *Value) Error() string {
if v.Type == ERROR {
return string(v.String())
}
return ""
}

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 %q, got %q", a.ExpectedValue, value.Error())
}

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

import (
"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.
// For each element in the array, we run the corresponding assertion.
type OrderedArrayAssertion struct {
ExpectedValue []RESPAssertion
}

func NewOrderedArrayAssertion(expectedValue []RESPAssertion) 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, assertion := range a.ExpectedValue {
actualElement := value.Array()[i]

if err := assertion.Run(actualElement); err != nil {
return err
}
}

return nil
}
7 changes: 7 additions & 0 deletions internal/stages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ func TestStages(t *testing.T) {
StdoutFixturePath: "./test_helpers/fixtures/streams/pass",
NormalizeOutputFunc: normalizeTesterOutput,
},
"transactions_pass": {
UntilStageSlug: "jf8",
CodePath: "./test_helpers/pass_all",
ExpectedExitCode: 0,
StdoutFixturePath: "./test_helpers/fixtures/transactions/pass",
NormalizeOutputFunc: normalizeTesterOutput,
},
}

tester_utils_testing.TestTesterOutput(t, testerDefinition, testCases)
Expand Down
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("CodeCrafters internal error. Number of commands and assertions should be equal in MultiCommandTestCase")
}

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
}
89 changes: 89 additions & 0 deletions internal/test_cases/transaction_test_case.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package test_cases

import (
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"
)

// 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 ExpectedResponseArray
//
// 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 "EXEC" is sent (based on which function is called)
// The elements in the response array are asserted based on the
// assertions in the ExpectedResponseArray
ExpectedResponseArray []resp_assertions.RESPAssertion
}

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 err := t.RunExec(client, logger); err != nil {
return err
}

return nil
}

func (t TransactionTestCase) RunWithoutExec(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
}

return nil
}

func (t TransactionTestCase) RunMulti(client *resp_client.RespConnection, logger *logger.Logger) error {
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("Sending command: #%d/#%d", i+1, len(t.CommandQueue))
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.ExpectedResponseArray),
}

return setCommandTestCase.Run(client, logger)
}
Loading

0 comments on commit d8ba70b

Please sign in to comment.