├── .github ├── dependabot.yml └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── Makefile ├── README.md ├── cmd └── tester │ └── main.go ├── go.mod ├── go.sum ├── internal ├── anti_cheat.go ├── assets │ └── empty_rdb_hex.md ├── cli.go ├── instrumented_resp_connection │ └── instrumented_resp_connection.go ├── log_friendly_error.go ├── new_redis_client.go ├── rdb_file_creator.go ├── rdb_file_creator_test.go ├── redis_executable │ └── redis_executable.go ├── resp │ ├── connection │ │ └── connection.go │ ├── decoder │ │ ├── decode.go │ │ ├── decode_array.go │ │ ├── decode_bulk_string_or_nil.go │ │ ├── decode_error.go │ │ ├── decode_error_tests.yml │ │ ├── decode_full_resync_rdb_file.go │ │ ├── decode_integer.go │ │ ├── decode_simple_string.go │ │ ├── decode_test.go │ │ ├── errors.go │ │ └── utils.go │ ├── encoder │ │ └── encode.go │ └── value │ │ └── value.go ├── resp_assertions │ ├── assertion.go │ ├── command_assertion.go │ ├── error_assertion.go │ ├── integer_assertion.go │ ├── nil_assertion.go │ ├── noop_assertion.go │ ├── only_command_assertion.go │ ├── ordered_array_assertion.go │ ├── ordered_string_array_assertion.go │ ├── regex_string_assertion.go │ ├── string_assertion.go │ ├── unordered_string_array_assertion.go │ └── xread_response_assertion.go ├── stages_test.go ├── staticcheck.conf ├── stdio_mocker.go ├── test_bind.go ├── test_cases │ ├── bind_test_case.go │ ├── get_ack_test_case.go │ ├── multi_command_test_case.go │ ├── receive_command_test_case.go │ ├── receive_replication_handshake_test_case.go │ ├── receive_value_test_case.go │ ├── send_command_test_case.go │ ├── send_replication_handshake_test_case.go │ ├── transaction_test_case.go │ └── wait_test_case.go ├── test_echo.go ├── test_expiry.go ├── test_get_set.go ├── test_helpers │ ├── course_definition.yml │ ├── fixtures │ │ ├── bind │ │ │ ├── failure │ │ │ ├── success │ │ │ └── timeout │ │ ├── expiry │ │ │ └── pass │ │ ├── ping-pong │ │ │ ├── eof │ │ │ ├── slow_response │ │ │ ├── without_crlf │ │ │ └── without_read_multiple_pongs │ │ ├── rdb-read-value-with-expiry │ │ │ └── pass │ │ ├── repl-wait │ │ │ └── pass │ │ ├── streams │ │ │ └── pass │ │ └── transactions │ │ │ └── pass │ ├── pass_all │ │ ├── codecrafters.yml │ │ └── spawn_redis_server.sh │ └── scenarios │ │ ├── bind │ │ ├── failure │ │ │ ├── codecrafters.yml │ │ │ └── spawn_redis_server.sh │ │ ├── success │ │ │ ├── codecrafters.yml │ │ │ └── spawn_redis_server.sh │ │ └── timeout │ │ │ ├── codecrafters.yml │ │ │ └── spawn_redis_server.sh │ │ └── ping-pong │ │ ├── eof │ │ ├── codecrafters.yml │ │ └── spawn_redis_server.sh │ │ ├── slow_response │ │ ├── codecrafters.yml │ │ └── spawn_redis_server.sh │ │ ├── without_crlf │ │ ├── codecrafters.yml │ │ └── spawn_redis_server.sh │ │ └── without_read_multiple_pongs │ │ ├── app │ │ └── main.go │ │ ├── codecrafters.yml │ │ └── spawn_redis_server.sh ├── test_ping_pong.go ├── test_rdb_config.go ├── test_rdb_read_key.go ├── test_rdb_read_multiple_keys.go ├── test_rdb_read_multiple_string_values.go ├── test_rdb_read_string_value.go ├── test_rdb_read_value_with_expiry.go ├── test_repl_bind.go ├── test_repl_id.go ├── test_repl_info.go ├── test_repl_info_replica.go ├── test_repl_master_cmd_prop.go ├── test_repl_master_psync.go ├── test_repl_master_psync_rdb.go ├── test_repl_master_replconf.go ├── test_repl_multiple_replicas.go ├── test_repl_replica_cmd_processing.go ├── test_repl_replica_getack_nonzero.go ├── test_repl_replica_getack_zero.go ├── test_repl_replica_ping.go ├── test_repl_replica_psync.go ├── test_repl_replica_replconf.go ├── test_repl_wait.go ├── test_repl_wait_zero_offset.go ├── test_repl_wait_zero_replicas.go ├── test_streams_type.go ├── test_streams_xadd.go ├── test_streams_xadd_full_autoid.go ├── test_streams_xadd_partial_autoid.go ├── test_streams_xadd_validate_id.go ├── test_streams_xrange.go ├── test_streams_xrange_max_id.go ├── test_streams_xrange_min_id.go ├── test_streams_xread.go ├── test_streams_xread_block.go ├── test_streams_xread_block_max_id.go ├── test_streams_xread_block_no_timeout.go ├── test_streams_xread_multiple.go ├── test_txn_discard.go ├── test_txn_empty.go ├── test_txn_exec.go ├── test_txn_incr1.go ├── test_txn_incr2.go ├── test_txn_incr3.go ├── test_txn_multi.go ├── test_txn_multi_tx.go ├── test_txn_queue.go ├── test_txn_tx.go ├── test_txn_tx_failure.go ├── tester_definition.go ├── tester_definition_test.go └── util.go ├── main.goreleaser.yml ├── run_failure.sh └── test.sh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | publish_tester: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Unshallow 18 | run: git fetch --prune --unshallow 19 | - name: Set up Go 20 | uses: actions/setup-go@v1 21 | with: 22 | go-version: 1.21.x 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v5 25 | with: 26 | version: v1.21.2 27 | args: release -f main.goreleaser.yml --rm-dist 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | - name: Set up Go 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: 1.21.x 15 | 16 | # Required for tests 17 | - name: Install Redis 18 | run: |- 19 | curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg 20 | echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list 21 | sudo apt-get update 22 | sudo apt-get install redis 23 | 24 | - name: "Stop redis service" 25 | run: sudo service redis-server stop 26 | 27 | - uses: actions/setup-python@v2 28 | with: 29 | python-version: "3.x" # Version range or exact version of a Python version to use, using SemVer's version range syntax 30 | 31 | - run: make test 32 | 33 | lint: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v2 38 | 39 | - name: Set up Go 40 | uses: actions/setup-go@v1 41 | with: 42 | go-version: 1.21.x 43 | 44 | - uses: dominikh/staticcheck-action@v1.3.0 45 | with: 46 | version: "2023.1" 47 | install-go: false 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | 3 | # Tests generate this 4 | **.rdb 5 | 6 | vendor/ 7 | .history/ 8 | .idea/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Challenge Tester 2 | 3 | This is a program that validates your progress on the Redis challenge. 4 | 5 | # Requirements for binary 6 | 7 | - Following environment variables: 8 | - `CODECRAFTERS_SUBMISSION_DIR` - root of the user's code submission 9 | - `CODECRAFTERS_TEST_CASES_JSON` - test cases in JSON format 10 | 11 | # User code requirements 12 | 13 | - A binary named `spawn_redis_server.sh` that spins up the Redis server. 14 | - A file named `codecrafters.yml`, with the following values: 15 | - `debug` 16 | -------------------------------------------------------------------------------- /cmd/tester/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal" 8 | ) 9 | 10 | func main() { 11 | os.Exit(internal.RunCLI(envMap())) 12 | } 13 | 14 | func envMap() map[string]string { 15 | result := make(map[string]string) 16 | for _, keyVal := range os.Environ() { 17 | split := strings.SplitN(keyVal, "=", 2) 18 | key, val := split[0], split[1] 19 | result[key] = val 20 | } 21 | 22 | return result 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codecrafters-io/redis-tester 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/codecrafters-io/tester-utils v0.2.38 7 | github.com/go-redis/redis v6.15.9+incompatible 8 | github.com/hdt3213/rdb v1.0.18 9 | github.com/stretchr/testify v1.9.0 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/fatih/color v1.17.0 // indirect 16 | github.com/mattn/go-colorable v0.1.13 // indirect 17 | github.com/mattn/go-isatty v0.0.20 // indirect 18 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 19 | github.com/onsi/ginkgo v1.11.0 // indirect 20 | github.com/onsi/gomega v1.8.1 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | golang.org/x/sys v0.25.0 // indirect 23 | gopkg.in/yaml.v2 v2.4.0 // indirect 24 | ) 25 | 26 | // Use this to test locally 27 | // replace github.com/codecrafters-io/tester-utils v0.2.12 => /Users/rohitpaulk/experiments/codecrafters/tester-utils 28 | -------------------------------------------------------------------------------- /internal/anti_cheat.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 7 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | "github.com/codecrafters-io/tester-utils/test_case_harness" 11 | ) 12 | 13 | func antiCheatTest(stageHarness *test_case_harness.TestCaseHarness) error { 14 | logger := stageHarness.Logger 15 | 16 | b := redis_executable.NewRedisExecutable(stageHarness) 17 | // If we can't run the executable, it must be an internal error. 18 | if err := b.Run(); err != nil { 19 | logger.Criticalf("CodeCrafters internal error. Error instantiating executable: %v", err) 20 | logger.Criticalf("Try again? Please contact us at hello@codecrafters.io if this persists.") 21 | return fmt.Errorf("anti-cheat (ac1) failed") 22 | } 23 | 24 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "replica") 25 | // If we are unable to connect to the redis server, it is okay to skip anti-cheat in that case, their server must not be working. 26 | if err != nil { 27 | return nil 28 | } 29 | defer client.Close() 30 | 31 | // All the answers for MEMORY DOCTOR include the string "sam" in them. 32 | commandTestCase := test_cases.SendCommandTestCase{ 33 | Command: "MEMORY", 34 | Args: []string{"DOCTOR"}, 35 | Assertion: resp_assertions.NewRegexStringAssertion("[sS]am"), 36 | ShouldSkipUnreadDataCheck: true, 37 | } 38 | err = commandTestCase.Run(client, logger) 39 | 40 | if err == nil { 41 | logger.Criticalf("anti-cheat (ac1) failed.") 42 | logger.Criticalf("Please contact us at hello@codecrafters.io if you think this is a mistake.") 43 | return fmt.Errorf("anti-cheat (ac1) failed") 44 | } else { 45 | return nil 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/assets/empty_rdb_hex.md: -------------------------------------------------------------------------------- 1 | Here's the contents of an empty RDB file in base64: 2 | 3 | ``` 4 | UkVESVMwMDEx+glyZWRpcy12ZXIFNy4yLjD6CnJlZGlzLWJpdHPAQPoFY3RpbWXCbQi8ZfoIdXNlZC1tZW3CsMQQAPoIYW9mLWJhc2XAAP/wbjv+wP9aog== 5 | ``` 6 | 7 | Here's the same contents in hex: 8 | 9 | ``` 10 | 524544495330303131fa0972656469732d76657205372e322e30fa0a72656469732d62697473c040fa056374696d65c26d08bc65fa08757365642d6d656dc2b0c41000fa08616f662d62617365c000fff06e3bfec0ff5aa2 11 | ``` 12 | 13 | Here's a more readable [hexdump](https://opensource.com/article/19/8/dig-binary-files-hexdump) representation of the same file: 14 | 15 | ``` 16 | 52 45 44 49 53 30 30 31 31 fa 09 72 65 64 69 73 |REDIS0011..redis| 17 | 2d 76 65 72 05 37 2e 32 2e 30 fa 0a 72 65 64 69 |-ver.7.2.0..redi| 18 | 73 2d 62 69 74 73 c0 40 fa 05 63 74 69 6d 65 c2 |s-bits.@..ctime.| 19 | 6d 08 bc 65 fa 08 75 73 65 64 2d 6d 65 6d c2 b0 |m..e..used-mem..| 20 | c4 10 00 fa 08 61 6f 66 2d 62 61 73 65 c0 00 ff |.....aof-base...| 21 | f0 6e 3b fe c0 ff 5a a2 |.n;...Z.| 22 | ``` 23 | -------------------------------------------------------------------------------- /internal/cli.go: -------------------------------------------------------------------------------- 1 | // Package internal exposes RunCLI, which is the entry point for the tester CLI. 2 | package internal 3 | 4 | import ( 5 | testerutils "github.com/codecrafters-io/tester-utils" 6 | ) 7 | 8 | func RunCLI(env map[string]string) int { 9 | return testerutils.RunCLI(env, testerDefinition) 10 | } 11 | -------------------------------------------------------------------------------- /internal/instrumented_resp_connection/instrumented_resp_connection.go: -------------------------------------------------------------------------------- 1 | package instrumented_resp_connection 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | resp_connection "github.com/codecrafters-io/redis-tester/internal/resp/connection" 8 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 9 | "github.com/codecrafters-io/tester-utils/logger" 10 | ) 11 | 12 | func defaultCallbacks(logger *logger.Logger, logPrefix string) resp_connection.RespConnectionCallbacks { 13 | return resp_connection.RespConnectionCallbacks{ 14 | BeforeSendCommand: func(reusedConnection bool, command string, args ...string) { 15 | var commandPrefix string 16 | if reusedConnection { 17 | commandPrefix = ">" 18 | } else { 19 | commandPrefix = "$ redis-cli" 20 | } 21 | 22 | if len(args) > 0 { 23 | logger.Infof("%s%s %s %s", logPrefix, commandPrefix, command, strings.Join(args, " ")) 24 | } else { 25 | logger.Infof("%s%s %s", logPrefix, commandPrefix, command) 26 | } 27 | }, 28 | BeforeSendValue: func(value resp_value.Value) { 29 | logger.Infof("%sSent %s", logPrefix, value.FormattedString()) 30 | }, 31 | BeforeSendBytes: func(bytes []byte) { 32 | logger.Debugf("%sSent bytes: %q", logPrefix, string(bytes)) 33 | }, 34 | AfterBytesReceived: func(bytes []byte) { 35 | logger.Debugf("%sReceived bytes: %q", logPrefix, string(bytes)) 36 | }, 37 | AfterReadValue: func(value resp_value.Value) { 38 | valueTypeLowerCase := strings.ReplaceAll(strings.ToLower(value.Type), "_", " ") 39 | if valueTypeLowerCase == "nil" { 40 | valueTypeLowerCase = "null bulk string" 41 | } 42 | logger.Debugf("%sReceived RESP %s: %s", logPrefix, valueTypeLowerCase, value.FormattedString()) 43 | 44 | }, 45 | } 46 | } 47 | 48 | func NewFromAddr(logger *logger.Logger, addr string, clientIdentifier string) (*resp_connection.RespConnection, error) { 49 | logPrefix := "" 50 | if clientIdentifier != "" { 51 | logPrefix = clientIdentifier + ": " 52 | } 53 | return resp_connection.NewRespConnectionFromAddr( 54 | addr, defaultCallbacks(logger, logPrefix), 55 | ) 56 | } 57 | 58 | func NewFromConn(logger *logger.Logger, conn net.Conn, clientIdentifier string) (*resp_connection.RespConnection, error) { 59 | logPrefix := "" 60 | if clientIdentifier != "" { 61 | logPrefix = clientIdentifier + ": " 62 | } 63 | 64 | return resp_connection.NewRespConnectionFromConn( 65 | conn, defaultCallbacks(logger, logPrefix), 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /internal/log_friendly_error.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/codecrafters-io/tester-utils/logger" 7 | ) 8 | 9 | func logFriendlyError(logger *logger.Logger, err error) { 10 | if err.Error() == "EOF" { 11 | logger.Infof("Hint: EOF is short for 'end of file'. This usually means that your program either:") 12 | logger.Infof(" (a) didn't send a complete response, or") 13 | logger.Infof(" (b) closed the connection early") 14 | } 15 | 16 | if strings.Contains(err.Error(), "connection reset by peer") { 17 | logger.Infof("Hint: 'connection reset by peer' usually means that your program closed the connection before sending a complete response.") 18 | } 19 | 20 | if strings.Contains(err.Error(), "reply is empty") { 21 | logger.Infof("Hint: 'reply is empty' usually means that your program sent an additional `\\n` in the response.") 22 | logger.Infof(" A common reason for this is using methods like `Println` that append a newline charater.") 23 | } 24 | } 25 | 26 | func logFriendlyBindError(logger *logger.Logger, err error) { 27 | if strings.Contains(err.Error(), "bind: address already in use") { 28 | logger.Errorf("This failure most likely means that your server didn't use the SO_REUSEADDR socket option while starting the server in the previous stage. SO_REUSEADDR is required to reuse previous sockets which were bound on the same address. Try setting the SO_REUSEADDR flag when creating your TCP server.") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/new_redis_client.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/go-redis/redis" 8 | ) 9 | 10 | func NewRedisClient(addr string) *redis.Client { 11 | return redis.NewClient(&redis.Options{ 12 | Addr: addr, 13 | DialTimeout: 5 * time.Second, 14 | Dialer: func() (net.Conn, error) { 15 | attempts := 0 16 | 17 | for { 18 | var err error 19 | var conn net.Conn 20 | 21 | conn, err = net.Dial("tcp", addr) 22 | 23 | if err == nil { 24 | return conn, nil 25 | } 26 | 27 | // Already a timeout 28 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() { 29 | return nil, err 30 | } 31 | 32 | // 50 * 100ms = 5s 33 | if attempts > 50 { 34 | return nil, err 35 | } 36 | 37 | attempts += 1 38 | time.Sleep(100 * time.Millisecond) 39 | } 40 | }, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /internal/rdb_file_creator.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/codecrafters-io/tester-utils/logger" 9 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 10 | "github.com/codecrafters-io/tester-utils/test_case_harness" 11 | 12 | "github.com/hdt3213/rdb/encoder" 13 | ) 14 | 15 | type KeyValuePair struct { 16 | key string 17 | value string 18 | expiryTS int64 // Unix timestamp in milliseconds 19 | } 20 | 21 | type RDBFileCreator struct { 22 | Dir string 23 | Filename string 24 | 25 | StageHarness *test_case_harness.TestCaseHarness 26 | } 27 | 28 | func NewRDBFileCreator() (*RDBFileCreator, error) { 29 | tmpDir, err := os.MkdirTemp("", "rdbfiles") 30 | if err != nil { 31 | return &RDBFileCreator{}, err 32 | } 33 | 34 | // On MacOS, the tmpDir is a symlink to a directory in /var/folders/... 35 | realDir, err := filepath.EvalSymlinks(tmpDir) 36 | if err != nil { 37 | return &RDBFileCreator{}, fmt.Errorf("could not resolve symlink: %v", err) 38 | } 39 | 40 | return &RDBFileCreator{ 41 | Dir: realDir, 42 | Filename: fmt.Sprintf("%s.rdb", testerutils_random.RandomWord()), 43 | }, nil 44 | } 45 | 46 | func (r *RDBFileCreator) Cleanup() { 47 | os.RemoveAll(r.Dir) 48 | } 49 | 50 | func (r *RDBFileCreator) Write(keyValuePairs []KeyValuePair) error { 51 | rdbFile, err := os.Create(filepath.Join(r.Dir, r.Filename)) 52 | if err != nil { 53 | return err 54 | } 55 | defer rdbFile.Close() 56 | 57 | enc := encoder.NewEncoder(rdbFile) 58 | 59 | if err := enc.WriteHeader(); err != nil { 60 | return err 61 | } 62 | 63 | auxMap := map[string]string{ 64 | "redis-ver": "7.2.0", 65 | "redis-bits": "64", 66 | } 67 | 68 | for k, v := range auxMap { 69 | if err := enc.WriteAux(k, v); err != nil { 70 | return err 71 | } 72 | } 73 | 74 | keysWithTTLCount := 0 75 | 76 | for _, keyValuePair := range keyValuePairs { 77 | if keyValuePair.expiryTS > 0 { 78 | keysWithTTLCount++ 79 | } 80 | } 81 | 82 | if err := enc.WriteDBHeader(0, uint64(len(keyValuePairs)), uint64(keysWithTTLCount)); err != nil { 83 | return err 84 | } 85 | 86 | for _, keyValuePair := range keyValuePairs { 87 | if keyValuePair.expiryTS > 0 { 88 | if err := enc.WriteStringObject(keyValuePair.key, []byte(keyValuePair.value), encoder.WithTTL(uint64(keyValuePair.expiryTS))); err != nil { 89 | return err 90 | } 91 | } else { 92 | if err := enc.WriteStringObject(keyValuePair.key, []byte(keyValuePair.value)); err != nil { 93 | return err 94 | } 95 | } 96 | } 97 | 98 | if err = enc.WriteEnd(); err != nil { 99 | return err 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (r *RDBFileCreator) Contents() ([]byte, error) { 106 | contents, err := os.ReadFile(filepath.Join(r.Dir, r.Filename)) 107 | if err != nil { 108 | return nil, err 109 | } 110 | return contents, nil 111 | } 112 | 113 | func (r *RDBFileCreator) PrintContentHexdump(logger *logger.Logger) error { 114 | contents, err := r.Contents() 115 | if err != nil { 116 | return err 117 | } 118 | logger.Debugf("Hexdump of RDB file contents: \n%v\n", GetFormattedHexdump(contents)) 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /internal/rdb_file_creator_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestRDBFileCreator(t *testing.T) { 14 | RDBFileCreator, err := NewRDBFileCreator() 15 | if err != nil { 16 | t.Fatalf("CodeCrafters Tester Error: %s", err) 17 | } 18 | 19 | randomKeyAndValue := testerutils_random.RandomWords(2) 20 | randomKey, randomValue := randomKeyAndValue[0], randomKeyAndValue[1] 21 | 22 | if err := RDBFileCreator.Write([]KeyValuePair{{key: randomKey, value: randomValue}}); err != nil { 23 | t.Fatalf("CodeCrafters Tester Error: %s", err) 24 | } 25 | 26 | fh, _ := os.Open(filepath.Join(RDBFileCreator.Dir, RDBFileCreator.Filename)) 27 | defer fh.Close() 28 | data, err := io.ReadAll(fh) 29 | if err != nil { 30 | t.Fatalf("CodeCrafters Tester Error: %s", err) 31 | } 32 | 33 | versionData := string(data[:9]) 34 | magicString := versionData[0:5] 35 | version := versionData[5:9] 36 | assert.Equal(t, "REDIS", magicString) 37 | assert.Equal(t, "0011", version) 38 | } 39 | -------------------------------------------------------------------------------- /internal/redis_executable/redis_executable.go: -------------------------------------------------------------------------------- 1 | package redis_executable 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strings" 7 | 8 | executable "github.com/codecrafters-io/tester-utils/executable" 9 | logger "github.com/codecrafters-io/tester-utils/logger" 10 | "github.com/codecrafters-io/tester-utils/test_case_harness" 11 | ) 12 | 13 | type RedisExecutable struct { 14 | executable *executable.Executable 15 | logger *logger.Logger 16 | args []string 17 | } 18 | 19 | func NewRedisExecutable(stageHarness *test_case_harness.TestCaseHarness) *RedisExecutable { 20 | b := &RedisExecutable{ 21 | executable: stageHarness.NewExecutable(), 22 | logger: stageHarness.Logger, 23 | } 24 | 25 | stageHarness.RegisterTeardownFunc(func() { b.Kill() }) 26 | 27 | return b 28 | } 29 | 30 | func (b *RedisExecutable) Run(args ...string) error { 31 | b.args = args 32 | if b.args == nil || len(b.args) == 0 { 33 | b.logger.Infof("$ ./%s", path.Base(b.executable.Path)) 34 | } else { 35 | var log string 36 | log += fmt.Sprintf("$ ./%s", path.Base(b.executable.Path)) 37 | for _, arg := range b.args { 38 | if strings.Contains(arg, " ") { 39 | log += " \"" + arg + "\"" 40 | } else { 41 | log += " " + arg 42 | } 43 | } 44 | b.logger.Infof(log) 45 | } 46 | 47 | if err := b.executable.Start(b.args...); err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (b *RedisExecutable) HasExited() bool { 55 | return b.executable.HasExited() 56 | } 57 | 58 | func (b *RedisExecutable) Kill() error { 59 | b.logger.Debugf("Terminating program") 60 | if err := b.executable.Kill(); err != nil { 61 | b.logger.Debugf("Error terminating program: '%v'", err) 62 | return err 63 | } 64 | 65 | b.logger.Debugf("Program terminated successfully") 66 | return nil // When does this happen? 67 | } 68 | -------------------------------------------------------------------------------- /internal/resp/decoder/decode.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 9 | ) 10 | 11 | func Decode(data []byte) (value resp_value.Value, readBytesCount int, err error) { 12 | reader := bytes.NewReader(data) 13 | 14 | value, err = doDecodeValue(reader) 15 | if err != nil { 16 | return resp_value.Value{}, 0, err 17 | } 18 | 19 | return value, len(data) - reader.Len(), nil 20 | } 21 | 22 | func doDecodeValue(reader *bytes.Reader) (resp_value.Value, error) { 23 | firstByte, err := reader.ReadByte() 24 | if err == io.EOF { 25 | return resp_value.Value{}, IncompleteInputError{ 26 | Reader: reader, 27 | Message: "Expected start of a new RESP2 value (either +, -, :, $ or *)", 28 | } 29 | } 30 | 31 | switch firstByte { 32 | case '+': 33 | return decodeSimpleString(reader) 34 | case '-': 35 | return decodeError(reader) 36 | case ':': 37 | return decodeInteger(reader) 38 | case '$': 39 | return decodeBulkStringOrNil(reader) 40 | case '*': 41 | return decodeArray(reader) 42 | default: 43 | reader.UnreadByte() // Ensure the error points to the correct byte 44 | 45 | return resp_value.Value{}, InvalidInputError{ 46 | Reader: reader, 47 | Message: fmt.Sprintf("%q is not a valid start of a RESP2 value (expected +, -, :, $ or *)", string(firstByte)), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/resp/decoder/decode_array.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | 9 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 10 | ) 11 | 12 | func decodeArray(reader *bytes.Reader) (resp_value.Value, error) { 13 | offsetBeforeLength := getReaderOffset(reader) 14 | 15 | lengthBytes, err := readUntilCRLF(reader) 16 | if err == io.EOF { 17 | return resp_value.Value{}, IncompleteInputError{ 18 | Reader: reader, 19 | Message: `Expected \r\n after array length`, 20 | } 21 | } 22 | 23 | length, err := strconv.Atoi(string(lengthBytes)) 24 | if err != nil { 25 | // Ensure error points to the correct byte 26 | reader.Seek(int64(offsetBeforeLength), io.SeekStart) 27 | 28 | return resp_value.Value{}, InvalidInputError{ 29 | Reader: reader, 30 | Message: fmt.Sprintf("Invalid array length: %q, expected a number", string(lengthBytes)), 31 | } 32 | } 33 | 34 | if length == -1 { 35 | return resp_value.NewNilValue(), nil 36 | } 37 | 38 | if length < -1 { 39 | // Ensure error points to the correct byte 40 | reader.Seek(int64(offsetBeforeLength), io.SeekStart) 41 | 42 | return resp_value.Value{}, InvalidInputError{ 43 | Reader: reader, 44 | Message: fmt.Sprintf("Invalid array length: %d, expected 0 or a positive integer", length), 45 | } 46 | } 47 | 48 | values := make([]resp_value.Value, length) 49 | for i := 0; i < length; i++ { 50 | value, err := doDecodeValue(reader) 51 | if err != nil { 52 | return resp_value.Value{}, err 53 | } 54 | 55 | values[i] = value 56 | } 57 | 58 | return resp_value.NewArrayValue(values), nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/resp/decoder/decode_bulk_string_or_nil.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | 9 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 10 | ) 11 | 12 | func decodeBulkStringOrNil(reader *bytes.Reader) (resp_value.Value, error) { 13 | offsetBeforeLength := getReaderOffset(reader) 14 | 15 | lengthBytes, err := readUntilCRLF(reader) 16 | if err == io.EOF { 17 | return resp_value.Value{}, IncompleteInputError{ 18 | Reader: reader, 19 | Message: `Expected \r\n after bulk string length`, 20 | } 21 | } 22 | 23 | length, err := strconv.Atoi(string(lengthBytes)) 24 | if err != nil { 25 | // Ensure error points to the correct byte 26 | reader.Seek(int64(offsetBeforeLength), io.SeekStart) 27 | 28 | return resp_value.Value{}, InvalidInputError{ 29 | Reader: reader, 30 | Message: fmt.Sprintf("Invalid bulk string length: %q, expected a number", string(lengthBytes)), 31 | } 32 | } 33 | 34 | if length == -1 { 35 | return resp_value.NewNilValue(), nil 36 | } 37 | 38 | if length < 1 { 39 | // Ensure error points to the correct byte 40 | reader.Seek(int64(offsetBeforeLength), io.SeekStart) 41 | 42 | return resp_value.Value{}, InvalidInputError{ 43 | Reader: reader, 44 | Message: fmt.Sprintf("Invalid bulk string length: %d, expected a positive integer", length), 45 | } 46 | } 47 | 48 | bytes := bytes.NewBuffer([]byte{}) 49 | for i := 0; i < length; i++ { 50 | b, err := reader.ReadByte() 51 | 52 | if err == io.EOF { 53 | return resp_value.Value{}, IncompleteInputError{ 54 | Reader: reader, 55 | Message: fmt.Sprintf("Expected %d bytes of data in bulk string, got %d", length, i), 56 | } 57 | } 58 | 59 | bytes.WriteByte(b) 60 | } 61 | 62 | // Read the \r\n at the end of the bulk string 63 | errorMessage := fmt.Sprintf(`Expected \r\n after %d bytes of data in bulk string`, length) 64 | if err := readCRLF(reader, errorMessage); err != nil { 65 | return resp_value.Value{}, err 66 | } 67 | 68 | return resp_value.NewBulkStringValue(bytes.String()), nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/resp/decoder/decode_error.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 8 | ) 9 | 10 | func decodeError(reader *bytes.Reader) (resp_value.Value, error) { 11 | bytes, err := readUntilCRLF(reader) 12 | if err == io.EOF { 13 | return resp_value.Value{}, IncompleteInputError{ 14 | Reader: reader, 15 | Message: `Expected \r\n at the end of a simple error`, 16 | } 17 | } 18 | 19 | return resp_value.NewErrorValue(string(bytes)), nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/resp/decoder/decode_error_tests.yml: -------------------------------------------------------------------------------- 1 | # Invalid RESP 2 | 3 | - input: "" 4 | error: | 5 | Received: "" (no content received) 6 | ^ error 7 | Error: Expected start of a new RESP2 value (either +, -, :, $ or *) 8 | 9 | - input: "K" 10 | error: | 11 | Received: "K" 12 | ^ error 13 | Error: "K" is not a valid start of a RESP2 value (expected +, -, :, $ or *) 14 | 15 | # Simple Strings 16 | 17 | - input: "+OK" 18 | error: | 19 | Received: "+OK" 20 | ^ error 21 | Error: Expected \r\n at the end of a simple string 22 | 23 | - input: "+OK\r" 24 | error: | 25 | Received: "+OK\r" 26 | ^ error 27 | Error: Expected \r\n at the end of a simple string 28 | 29 | - input: "+OK\n" 30 | error: | 31 | Received: "+OK\n" 32 | ^ error 33 | Error: Expected \r\n at the end of a simple string 34 | 35 | # Bulk Strings 36 | 37 | - input: "$3" 38 | error: | 39 | Received: "$3" 40 | ^ error 41 | Error: Expected \r\n after bulk string length 42 | 43 | - input: "$3\r" 44 | error: | 45 | Received: "$3\r" 46 | ^ error 47 | Error: Expected \r\n after bulk string length 48 | 49 | - input: "$abc\r\n" 50 | error: | 51 | Received: "$abc\r\n" 52 | ^ error 53 | Error: Invalid bulk string length: "abc", expected a number 54 | 55 | - input: "$-5\r\nhello\r\n" 56 | error: | 57 | Received: "$-5\r\nhello\r\n" 58 | ^ error 59 | Error: Invalid bulk string length: -5, expected a positive integer 60 | 61 | - input: "$3\r\n" 62 | error: | 63 | Received: "$3\r\n" 64 | ^ error 65 | Error: Expected 3 bytes of data in bulk string, got 0 66 | 67 | - input: "$2\r\nabc\r\n" 68 | error: | 69 | Received: "$2\r\nabc\r\n" 70 | ^ error 71 | Error: Expected \r\n after 2 bytes of data in bulk string 72 | 73 | - input: "$3\r\nabc" 74 | error: | 75 | Received: "$3\r\nabc" 76 | ^ error 77 | Error: Expected \r\n after 3 bytes of data in bulk string 78 | 79 | # Integers 80 | 81 | - input: ":17" 82 | error: | 83 | Received: ":17" 84 | ^ error 85 | Error: Expected \r\n at the end of an integer 86 | 87 | - input: ":17\r" 88 | error: | 89 | Received: ":17\r" 90 | ^ error 91 | Error: Expected \r\n at the end of an integer 92 | 93 | - input: ":17\n" 94 | error: | 95 | Received: ":17\n" 96 | ^ error 97 | Error: Expected \r\n at the end of an integer 98 | 99 | - input: ":foo\r\n" 100 | error: | 101 | Received: ":foo\r\n" 102 | ^ error 103 | Error: Invalid integer: "foo", expected a number 104 | 105 | # Arrays 106 | 107 | # - input: "*abc" 108 | # error: | 109 | # Received: "*abc" 110 | # ^ error 111 | # Error: Expected \r\n after array length 112 | -------------------------------------------------------------------------------- /internal/resp/decoder/decode_full_resync_rdb_file.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | ) 9 | 10 | func DecodeFullResyncRDBFile(data []byte) (rdbFileContents []byte, readBytesCount int, err error) { 11 | reader := bytes.NewReader(data) 12 | 13 | rdbFileContents, err = doDecodeFullResyncRDBFile(reader) 14 | if err != nil { 15 | return nil, 0, err 16 | } 17 | 18 | return rdbFileContents, len(data) - reader.Len(), nil 19 | } 20 | 21 | func doDecodeFullResyncRDBFile(reader *bytes.Reader) ([]byte, error) { 22 | firstByte, err := reader.ReadByte() 23 | if err == io.EOF { 24 | return nil, IncompleteInputError{ 25 | Reader: reader, 26 | Message: "Expected first byte of RDB file message to be $", 27 | } 28 | } 29 | 30 | if firstByte != '$' { 31 | reader.UnreadByte() // Ensure the error points to the correct byte 32 | 33 | return nil, InvalidInputError{ 34 | Reader: reader, 35 | Message: fmt.Sprintf("Expected first byte of RDB file message to be $, got %q", string(firstByte)), 36 | } 37 | } 38 | 39 | offsetBeforeLength := getReaderOffset(reader) 40 | lengthBytes, err := readUntilCRLF(reader) 41 | if err == io.EOF { 42 | return nil, IncompleteInputError{ 43 | Reader: reader, 44 | Message: `Expected \r\n after RDB file length`, 45 | } 46 | } 47 | 48 | length, err := strconv.Atoi(string(lengthBytes)) 49 | if err != nil { 50 | // Ensure error points to the correct byte 51 | reader.Seek(int64(offsetBeforeLength), io.SeekStart) 52 | 53 | return nil, InvalidInputError{ 54 | Reader: reader, 55 | Message: fmt.Sprintf("Invalid RDB file length: %q, expected a number", string(lengthBytes)), 56 | } 57 | } 58 | 59 | if length < 1 { 60 | // Ensure error points to the correct byte 61 | reader.Seek(int64(offsetBeforeLength), io.SeekStart) 62 | 63 | return nil, InvalidInputError{ 64 | Reader: reader, 65 | Message: fmt.Sprintf("Invalid RDB file length: %d, expected a positive integer", length), 66 | } 67 | } 68 | 69 | bytes := bytes.NewBuffer([]byte{}) 70 | for i := 0; i < length; i++ { 71 | b, err := reader.ReadByte() 72 | 73 | if err == io.EOF { 74 | return nil, IncompleteInputError{ 75 | Reader: reader, 76 | Message: fmt.Sprintf("Expected %d bytes of data in RDB file message, got %d", length, i), 77 | } 78 | } 79 | 80 | bytes.WriteByte(b) 81 | } 82 | 83 | return bytes.Bytes(), nil 84 | 85 | } 86 | -------------------------------------------------------------------------------- /internal/resp/decoder/decode_integer.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | 9 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 10 | ) 11 | 12 | func decodeInteger(reader *bytes.Reader) (resp_value.Value, error) { 13 | offsetBeforeInteger := getReaderOffset(reader) 14 | 15 | bytes, err := readUntilCRLF(reader) 16 | if err == io.EOF { 17 | return resp_value.Value{}, IncompleteInputError{ 18 | Reader: reader, 19 | Message: `Expected \r\n at the end of an integer`, 20 | } 21 | } 22 | 23 | integer, err := strconv.Atoi(string(bytes)) 24 | if err != nil { 25 | // Ensure error points to the correct byte 26 | reader.Seek(int64(offsetBeforeInteger), io.SeekStart) 27 | 28 | return resp_value.Value{}, InvalidInputError{ 29 | Reader: reader, 30 | Message: fmt.Sprintf("Invalid integer: %q, expected a number", string(bytes)), 31 | } 32 | } 33 | 34 | return resp_value.NewIntegerValue(integer), nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/resp/decoder/decode_simple_string.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 8 | ) 9 | 10 | func decodeSimpleString(reader *bytes.Reader) (resp_value.Value, error) { 11 | bytes, err := readUntilCRLF(reader) 12 | if err == io.EOF { 13 | return resp_value.Value{}, IncompleteInputError{ 14 | Reader: reader, 15 | Message: `Expected \r\n at the end of a simple string`, 16 | } 17 | } 18 | 19 | return resp_value.NewSimpleStringValue(string(bytes)), nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/resp/decoder/decode_test.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | func TestDecodeSimpleStringSuccess(t *testing.T) { 14 | value, nBytes, err := Decode([]byte("+OK\r\n")) 15 | assert.Nil(t, err) 16 | assert.Equal(t, 5, nBytes) 17 | assert.Equal(t, resp_value.SIMPLE_STRING, value.Type) 18 | assert.Equal(t, "OK", value.String()) 19 | } 20 | 21 | func TestDecodeWithExtraDataSuccess(t *testing.T) { 22 | value, nBytes, err := Decode([]byte("+OK\r\nextra")) 23 | assert.Nil(t, err) 24 | assert.Equal(t, resp_value.SIMPLE_STRING, value.Type) 25 | assert.Equal(t, "OK", value.String()) 26 | assert.Equal(t, 5, nBytes) 27 | } 28 | 29 | type DecodeErrorTestCase struct { 30 | Input string `yaml:"input"` 31 | Error string `yaml:"error"` 32 | } 33 | 34 | func TestDecodeErrors(t *testing.T) { 35 | testCases := []DecodeErrorTestCase{} 36 | 37 | yamlContents, err := os.ReadFile("decode_error_tests.yml") 38 | assert.Nil(t, err) 39 | 40 | err = yaml.Unmarshal(yamlContents, &testCases) 41 | assert.Nil(t, err) 42 | 43 | for _, testCase := range testCases { 44 | t.Run(testCase.Input, func(t *testing.T) { 45 | _, _, err := Decode([]byte(testCase.Input)) 46 | assert.NotNil(t, err) 47 | assert.Equal(t, strings.TrimSpace(testCase.Error), err.Error()) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/resp/decoder/errors.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | 8 | inspectable_byte_string "github.com/codecrafters-io/tester-utils/inspectable_byte_string" 9 | ) 10 | 11 | type IncompleteInputError struct { 12 | Reader *bytes.Reader 13 | Message string 14 | } 15 | 16 | type InvalidInputError struct { 17 | Reader *bytes.Reader 18 | Message string 19 | } 20 | 21 | func (e IncompleteInputError) Error() string { 22 | return formatDetailedError(e.Reader, e.Message) 23 | } 24 | 25 | func (e InvalidInputError) Error() string { 26 | return formatDetailedError(e.Reader, e.Message) 27 | } 28 | 29 | func formatDetailedError(reader *bytes.Reader, message string) string { 30 | lines := []string{} 31 | 32 | offset := getReaderOffset(reader) 33 | receivedBytes := readBytesFromReader(reader) 34 | receivedByteString := inspectable_byte_string.NewInspectableByteString(receivedBytes) 35 | 36 | suffix := "" 37 | 38 | if len(receivedBytes) == 0 { 39 | suffix = " (no content received)" 40 | } 41 | 42 | lines = append(lines, receivedByteString.FormatWithHighlightedOffset(offset, "error", "Received: ", suffix)) 43 | lines = append(lines, fmt.Sprintf("Error: %s", message)) 44 | 45 | return strings.Join(lines, "\n") 46 | } 47 | 48 | func getReaderOffset(reader *bytes.Reader) int { 49 | return int(reader.Size()) - reader.Len() 50 | } 51 | 52 | func readBytesFromReader(reader *bytes.Reader) []byte { 53 | previousOffset := getReaderOffset(reader) 54 | defer reader.Seek(int64(previousOffset), 0) 55 | 56 | reader.Seek(0, 0) 57 | bytes := make([]byte, reader.Len()) 58 | 59 | if reader.Len() == 0 { 60 | return bytes 61 | } 62 | 63 | n, err := reader.Read(bytes) 64 | if err != nil { 65 | panic(fmt.Sprintf("Error reading from reader: %s", err)) // This should never happen 66 | } 67 | if n != len(bytes) { 68 | panic(fmt.Sprintf("Expected to read %d bytes, but only read %d", len(bytes), n)) // This should never happen 69 | } 70 | 71 | return bytes 72 | } 73 | -------------------------------------------------------------------------------- /internal/resp/decoder/utils.go: -------------------------------------------------------------------------------- 1 | package resp_decoder 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | func readUntilCRLF(r *bytes.Reader) ([]byte, error) { 9 | return readUntil(r, []byte("\r\n")) 10 | } 11 | 12 | func readCRLF(reader *bytes.Reader, errorMessage string) (err error) { 13 | offsetBeforeCRLF := getReaderOffset(reader) 14 | 15 | b, err := reader.ReadByte() 16 | if err == io.EOF { 17 | return IncompleteInputError{ 18 | Reader: reader, 19 | Message: errorMessage, 20 | } 21 | } 22 | 23 | if b != '\r' { 24 | reader.Seek(int64(offsetBeforeCRLF), io.SeekStart) 25 | 26 | return InvalidInputError{ 27 | Reader: reader, 28 | Message: errorMessage, 29 | } 30 | } 31 | 32 | b, err = reader.ReadByte() 33 | if err == io.EOF { 34 | return IncompleteInputError{ 35 | Reader: reader, 36 | Message: errorMessage, 37 | } 38 | } 39 | 40 | if b != '\n' { 41 | reader.Seek(int64(offsetBeforeCRLF), io.SeekStart) 42 | 43 | return InvalidInputError{ 44 | Reader: reader, 45 | Message: errorMessage, 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func readUntil(r *bytes.Reader, delim []byte) ([]byte, error) { 53 | var result []byte 54 | 55 | for { 56 | b, err := r.ReadByte() 57 | if err != nil { 58 | if err != io.EOF { 59 | panic("expected error to always be io.EOF") 60 | } 61 | 62 | return result, io.EOF 63 | } 64 | 65 | result = append(result, b) 66 | 67 | if len(result) >= len(delim) && bytes.Equal(result[len(result)-len(delim):], delim) { 68 | return result[:len(result)-len(delim)], nil 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/resp/encoder/encode.go: -------------------------------------------------------------------------------- 1 | package resp_encoder 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 8 | ) 9 | 10 | func Encode(v resp_value.Value) []byte { 11 | switch v.Type { 12 | case resp_value.INTEGER: 13 | return encodeInteger(v) 14 | case resp_value.SIMPLE_STRING: 15 | return encodeSimpleString(v) 16 | case resp_value.BULK_STRING: 17 | return encodeBulkString(v) 18 | case resp_value.ERROR: 19 | return encodeError(v) 20 | case resp_value.ARRAY: 21 | return encodeArray(v) 22 | default: 23 | panic(fmt.Sprintf("unsupported type: %v", v.Type)) 24 | } 25 | } 26 | 27 | func EncodeFullResyncRDBFile(fileContents []byte) []byte { 28 | return []byte(fmt.Sprintf("$%d\r\n%s", len(fileContents), fileContents)) 29 | } 30 | 31 | func encodeInteger(v resp_value.Value) []byte { 32 | int_value, err := strconv.Atoi(v.String()) 33 | if err != nil { 34 | panic(err) // We only expect valid values to be passed in 35 | } 36 | 37 | return []byte(fmt.Sprintf(":%d\r\n", int_value)) 38 | } 39 | 40 | func encodeSimpleString(v resp_value.Value) []byte { 41 | return []byte(fmt.Sprintf("+%s\r\n", v.String())) 42 | } 43 | 44 | func encodeBulkString(v resp_value.Value) []byte { 45 | return []byte(fmt.Sprintf("$%d\r\n%s\r\n", len(v.Bytes()), v.Bytes())) 46 | } 47 | 48 | func encodeError(v resp_value.Value) []byte { 49 | return []byte(fmt.Sprintf("-%s\r\n", v.String())) 50 | } 51 | 52 | func encodeArray(v resp_value.Value) []byte { 53 | res := []byte{} 54 | 55 | for _, elem := range v.Array() { 56 | res = append(res, Encode(elem)...) 57 | } 58 | 59 | return []byte(fmt.Sprintf("*%d\r\n%s", len(v.Array()), res)) 60 | } 61 | -------------------------------------------------------------------------------- /internal/resp/value/value.go: -------------------------------------------------------------------------------- 1 | package resp_value 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | SIMPLE_STRING string = "SIMPLE_STRING" 10 | INTEGER string = "INTEGER" 11 | BULK_STRING string = "BULK_STRING" 12 | ARRAY string = "ARRAY" 13 | ERROR string = "ERROR" 14 | NIL string = "NIL" 15 | ) 16 | 17 | type Value struct { 18 | Type string 19 | 20 | // Each type might use a different field to store data 21 | bytes []byte 22 | integer int 23 | array []Value 24 | } 25 | 26 | func NewSimpleStringValue(s string) Value { 27 | return Value{ 28 | Type: SIMPLE_STRING, 29 | bytes: []byte(s), 30 | } 31 | } 32 | 33 | func NewBulkStringValue(s string) Value { 34 | return Value{ 35 | Type: BULK_STRING, 36 | bytes: []byte(s), 37 | } 38 | } 39 | 40 | func NewIntegerValue(i int) Value { 41 | return Value{ 42 | Type: INTEGER, 43 | integer: i, 44 | } 45 | } 46 | 47 | func NewErrorValue(err string) Value { 48 | return Value{ 49 | Type: ERROR, 50 | bytes: []byte(err), 51 | } 52 | } 53 | 54 | func NewStringArrayValue(strings []string) Value { 55 | values := make([]Value, len(strings)) 56 | 57 | for i, s := range strings { 58 | values[i] = NewBulkStringValue(s) 59 | } 60 | 61 | return NewArrayValue(values) 62 | } 63 | 64 | func NewArrayValue(arr []Value) Value { 65 | return Value{ 66 | Type: ARRAY, 67 | array: arr, 68 | } 69 | } 70 | 71 | func NewNilValue() Value { 72 | return Value{ 73 | Type: NIL, 74 | } 75 | } 76 | 77 | func (v *Value) Bytes() []byte { 78 | return v.bytes 79 | } 80 | 81 | func (v *Value) Array() []Value { 82 | if v.Type == ARRAY { 83 | return v.array 84 | } 85 | 86 | return []Value{} 87 | } 88 | 89 | func (v *Value) String() string { 90 | return string(v.bytes) 91 | } 92 | 93 | func (v *Value) Integer() int { 94 | return v.integer 95 | } 96 | 97 | func (v *Value) Error() string { 98 | if v.Type == ERROR { 99 | return string(v.String()) 100 | } 101 | return "" 102 | } 103 | 104 | func (v *Value) FormattedString() string { 105 | switch v.Type { 106 | case SIMPLE_STRING: 107 | return fmt.Sprintf("%q", v.String()) 108 | case INTEGER: 109 | return fmt.Sprintf("%d", v.Integer()) 110 | case BULK_STRING: 111 | return fmt.Sprintf("%q", v.String()) 112 | case ARRAY: 113 | formattedStrings := make([]string, len(v.Array())) 114 | 115 | for i, value := range v.Array() { 116 | formattedStrings[i] = value.FormattedString() 117 | } 118 | 119 | return fmt.Sprintf("[%v]", strings.Join(formattedStrings, ", ")) 120 | case ERROR: 121 | return fmt.Sprintf("%q", "ERR: "+v.String()) 122 | case NIL: 123 | return "\"$-1\\r\\n\"" 124 | } 125 | 126 | return "" 127 | } 128 | 129 | func (v Value) ToSerializable() interface{} { 130 | switch v.Type { 131 | case BULK_STRING: 132 | return v.String() 133 | case ARRAY: 134 | arr := v.Array() 135 | result := make([]interface{}, len(arr)) 136 | for i, elem := range arr { 137 | result[i] = elem.ToSerializable() 138 | } 139 | return result 140 | default: 141 | return v.String() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /internal/resp_assertions/assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 5 | ) 6 | 7 | type RESPAssertion interface { 8 | Run(value resp_value.Value) error 9 | } 10 | -------------------------------------------------------------------------------- /internal/resp_assertions/command_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 8 | ) 9 | 10 | type CommandAssertion struct { 11 | ExpectedCommand string 12 | ExpectedArgs []string 13 | } 14 | 15 | func NewCommandAssertion(expectedCommand string, expectedArgs ...string) RESPAssertion { 16 | return CommandAssertion{ 17 | ExpectedCommand: expectedCommand, 18 | ExpectedArgs: expectedArgs, 19 | } 20 | } 21 | 22 | func (a CommandAssertion) Run(value resp_value.Value) error { 23 | if value.Type != resp_value.ARRAY { 24 | return fmt.Errorf("Expected array type, got %s", value.Type) 25 | } 26 | 27 | elements := value.Array() 28 | 29 | if len(elements) < 1 { 30 | return fmt.Errorf("Expected array with at least 1 element, got %d elements", len(elements)) 31 | } 32 | 33 | if elements[0].Type != resp_value.SIMPLE_STRING && elements[0].Type != resp_value.BULK_STRING { 34 | return fmt.Errorf("Expected first array element to be a string, got %s", elements[0].Type) 35 | } 36 | 37 | command := elements[0].String() 38 | 39 | if !strings.EqualFold(command, a.ExpectedCommand) { 40 | return fmt.Errorf("Expected command to be %q, got %q", strings.ToLower(a.ExpectedCommand), strings.ToLower(command)) 41 | } 42 | 43 | if len(elements) != len(a.ExpectedArgs)+1 { 44 | return fmt.Errorf("Expected command to have %d arguments, got %d", len(a.ExpectedArgs), len(elements)-1) 45 | } 46 | 47 | for i, expectedArg := range a.ExpectedArgs { 48 | actualArg := elements[i+1] 49 | if actualArg.Type != resp_value.SIMPLE_STRING && actualArg.Type != resp_value.BULK_STRING { 50 | return fmt.Errorf("Expected argument %d to be a string, got %s", i+1, actualArg.Type) 51 | } 52 | 53 | if actualArg.String() != expectedArg { 54 | return fmt.Errorf("Expected argument #%d to be %q, got %q", i+1, expectedArg, actualArg.String()) 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/resp_assertions/error_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "fmt" 5 | 6 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 7 | ) 8 | 9 | type ErrorAssertion struct { 10 | ExpectedValue string 11 | } 12 | 13 | func NewErrorAssertion(expectedValue string) RESPAssertion { 14 | return ErrorAssertion{ExpectedValue: expectedValue} 15 | } 16 | 17 | func (a ErrorAssertion) Run(value resp_value.Value) error { 18 | if value.Type != resp_value.ERROR { 19 | return fmt.Errorf("Expected error, got %s", value.Type) 20 | } 21 | 22 | if value.Error() != a.ExpectedValue { 23 | return fmt.Errorf("Expected %q, got %q", a.ExpectedValue, value.Error()) 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/resp_assertions/integer_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "fmt" 5 | 6 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 7 | ) 8 | 9 | type IntegerAssertion struct { 10 | ExpectedValue int 11 | } 12 | 13 | func NewIntegerAssertion(expectedValue int) RESPAssertion { 14 | return IntegerAssertion{ExpectedValue: expectedValue} 15 | } 16 | 17 | func (a IntegerAssertion) Run(value resp_value.Value) error { 18 | if value.Type != resp_value.INTEGER { 19 | return fmt.Errorf("Expected integer, got %s", value.Type) 20 | } 21 | 22 | if value.Integer() != a.ExpectedValue { 23 | return fmt.Errorf("Expected %d, got %d", a.ExpectedValue, value.Integer()) 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/resp_assertions/nil_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "fmt" 5 | 6 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 7 | ) 8 | 9 | type NilAssertion struct{} 10 | 11 | func NewNilAssertion() RESPAssertion { 12 | return NilAssertion{} 13 | } 14 | 15 | func (a NilAssertion) Run(value resp_value.Value) error { 16 | if value.Type != resp_value.NIL { 17 | return fmt.Errorf(`Expected null bulk string ("$-1\r\n"), got %s`, value.Type) 18 | } 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/resp_assertions/noop_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 4 | 5 | type NoopAssertion struct{} 6 | 7 | func NewNoopAssertion() RESPAssertion { 8 | return NoopAssertion{} 9 | } 10 | 11 | func (a NoopAssertion) Run(value resp_value.Value) error { 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /internal/resp_assertions/only_command_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 8 | ) 9 | 10 | type OnlyCommandAssertion struct { 11 | ExpectedCommand string 12 | } 13 | 14 | func NewOnlyCommandAssertion(expectedCommand string) RESPAssertion { 15 | return OnlyCommandAssertion{ 16 | ExpectedCommand: expectedCommand, 17 | } 18 | } 19 | 20 | func (a OnlyCommandAssertion) Run(value resp_value.Value) error { 21 | if value.Type != resp_value.ARRAY { 22 | return fmt.Errorf("Expected array type, got %s", value.Type) 23 | } 24 | 25 | elements := value.Array() 26 | 27 | if len(elements) < 1 { 28 | return fmt.Errorf("Expected array with at least 1 element, got %d elements", len(elements)) 29 | } 30 | 31 | if elements[0].Type != resp_value.SIMPLE_STRING && elements[0].Type != resp_value.BULK_STRING { 32 | return fmt.Errorf("Expected first array element to be a string, got %s", elements[0].Type) 33 | } 34 | 35 | command := elements[0].String() 36 | 37 | if !strings.EqualFold(command, a.ExpectedCommand) { 38 | return fmt.Errorf("Expected command to be %q, got %q", strings.ToLower(a.ExpectedCommand), strings.ToLower(command)) 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/resp_assertions/ordered_array_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "fmt" 5 | 6 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 7 | ) 8 | 9 | // OrderedArrayAssertion : Order of the actual and expected values matters. 10 | // All RESP values are accepted as elements in this array. 11 | // We don't alter the ordering. 12 | // For each element in the array, we run the corresponding assertion. 13 | type OrderedArrayAssertion struct { 14 | ExpectedValue []RESPAssertion 15 | } 16 | 17 | func NewOrderedArrayAssertion(expectedValue []RESPAssertion) RESPAssertion { 18 | return OrderedArrayAssertion{ExpectedValue: expectedValue} 19 | } 20 | 21 | func (a OrderedArrayAssertion) Run(value resp_value.Value) error { 22 | if value.Type != resp_value.ARRAY { 23 | return fmt.Errorf("Expected an array, got %s", value.Type) 24 | } 25 | 26 | if len(value.Array()) != len(a.ExpectedValue) { 27 | return fmt.Errorf("Expected %d elements in array, got %d (%s)", len(a.ExpectedValue), len(value.Array()), value.FormattedString()) 28 | } 29 | 30 | for i, assertion := range a.ExpectedValue { 31 | actualElement := value.Array()[i] 32 | 33 | if err := assertion.Run(actualElement); err != nil { 34 | return err 35 | } 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/resp_assertions/ordered_string_array_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "fmt" 5 | 6 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 7 | ) 8 | 9 | // OrderedStringArrayAssertion : Order of the actual and expected values matters. 10 | // We don't alter the ordering. 11 | type OrderedStringArrayAssertion struct { 12 | ExpectedValue []string 13 | } 14 | 15 | func NewOrderedStringArrayAssertion(expectedValue []string) RESPAssertion { 16 | return OrderedStringArrayAssertion{ExpectedValue: expectedValue} 17 | } 18 | 19 | func (a OrderedStringArrayAssertion) Run(value resp_value.Value) error { 20 | if value.Type != resp_value.ARRAY { 21 | return fmt.Errorf("Expected an array, got %s", value.Type) 22 | } 23 | 24 | if len(value.Array()) != len(a.ExpectedValue) { 25 | return fmt.Errorf("Expected %d elements in array, got %d (%s)", len(a.ExpectedValue), len(value.Array()), value.FormattedString()) 26 | } 27 | 28 | for i, expectedValue := range a.ExpectedValue { 29 | actualElement := value.Array()[i] 30 | 31 | if actualElement.Type != resp_value.BULK_STRING && actualElement.Type != resp_value.SIMPLE_STRING { 32 | return fmt.Errorf("Expected element #%d to be a string, got %s", i+1, actualElement.Type) 33 | } 34 | 35 | if actualElement.String() != expectedValue { 36 | return fmt.Errorf("Expected element #%d to be %q, got %q", i+1, expectedValue, actualElement.String()) 37 | } 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/resp_assertions/regex_string_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 8 | ) 9 | 10 | type RegexStringAssertion struct { 11 | ExpectedPattern *regexp.Regexp 12 | } 13 | 14 | func NewRegexStringAssertion(expectedPattern string) RESPAssertion { 15 | pattern, err := regexp.Compile(expectedPattern) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | return RegexStringAssertion{ExpectedPattern: pattern} 21 | } 22 | 23 | func (a RegexStringAssertion) Run(value resp_value.Value) error { 24 | if value.Type != resp_value.SIMPLE_STRING && value.Type != resp_value.BULK_STRING { 25 | return fmt.Errorf("Expected simple string or bulk string, got %s", value.Type) 26 | } 27 | 28 | if !a.ExpectedPattern.MatchString(value.String()) { 29 | return fmt.Errorf("Expected %q to match the pattern %q.", value.String(), a.ExpectedPattern.String()) 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/resp_assertions/string_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "fmt" 5 | 6 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 7 | ) 8 | 9 | type StringAssertion struct { 10 | ExpectedValue string 11 | } 12 | 13 | func NewStringAssertion(expectedValue string) RESPAssertion { 14 | return StringAssertion{ExpectedValue: expectedValue} 15 | } 16 | 17 | func (a StringAssertion) Run(value resp_value.Value) error { 18 | if value.Type != resp_value.SIMPLE_STRING && value.Type != resp_value.BULK_STRING { 19 | return fmt.Errorf("Expected simple string or bulk string, got %s", value.Type) 20 | } 21 | 22 | if value.String() != a.ExpectedValue { 23 | return fmt.Errorf("Expected %q, got %q", a.ExpectedValue, value.String()) 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/resp_assertions/unordered_string_array_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | 8 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 9 | ) 10 | 11 | // UnorderedStringArrayAssertion : Order of the actual and expected values doesn't matter. 12 | // We sort the expected and actual values before comparing them. 13 | type UnorderedStringArrayAssertion struct { 14 | ExpectedValue []string 15 | } 16 | 17 | func NewUnorderedStringArrayAssertion(expectedValue []string) RESPAssertion { 18 | return UnorderedStringArrayAssertion{ExpectedValue: expectedValue} 19 | } 20 | 21 | func (a UnorderedStringArrayAssertion) Run(value resp_value.Value) error { 22 | if value.Type != resp_value.ARRAY { 23 | return fmt.Errorf("Expected an array, got %s", value.Type) 24 | } 25 | 26 | if len(value.Array()) != len(a.ExpectedValue) { 27 | return fmt.Errorf("Expected %d elements in array, got %d (%s)", len(a.ExpectedValue), len(value.Array()), value.FormattedString()) 28 | } 29 | 30 | actualElementStringArray := make([]string, len(value.Array())) 31 | for i := range value.Array() { 32 | actualElement := value.Array()[i] 33 | if actualElement.Type != resp_value.BULK_STRING && actualElement.Type != resp_value.SIMPLE_STRING { 34 | return fmt.Errorf("Expected element #%d to be a string, got %s", i+1, actualElement.Type) 35 | } 36 | actualElementStringArray[i] = value.Array()[i].String() 37 | } 38 | 39 | expectedValueArrayForPrinting, _ := json.Marshal(a.ExpectedValue) 40 | expectedValueStringArray := make([]string, len(a.ExpectedValue)) 41 | copy(expectedValueStringArray, a.ExpectedValue) 42 | sort.Strings(actualElementStringArray) 43 | sort.Strings(expectedValueStringArray) 44 | 45 | for i, expectedValue := range expectedValueStringArray { 46 | actualElement := actualElementStringArray[i] 47 | 48 | if actualElement != expectedValue { 49 | return fmt.Errorf("Expected: %v (in any order), got %v", string(expectedValueArrayForPrinting), value.FormattedString()) 50 | } 51 | } 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/resp_assertions/xread_response_assertion.go: -------------------------------------------------------------------------------- 1 | package resp_assertions 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 8 | ) 9 | 10 | type StreamEntry struct { 11 | Id string 12 | FieldValuePairs [][]string 13 | } 14 | 15 | type StreamResponse struct { 16 | Key string 17 | Entries []StreamEntry 18 | } 19 | 20 | type XReadResponseAssertion struct { 21 | ExpectedStreamResponses []StreamResponse 22 | } 23 | 24 | func NewXReadResponseAssertion(expectedValue []StreamResponse) RESPAssertion { 25 | return XReadResponseAssertion{ExpectedStreamResponses: expectedValue} 26 | } 27 | 28 | func (a XReadResponseAssertion) Run(value resp_value.Value) error { 29 | if value.Type != resp_value.ARRAY { 30 | return fmt.Errorf("Expected array, got %s", value.Type) 31 | } 32 | 33 | expected := a.buildExpected().ToSerializable() 34 | actual := value.ToSerializable() 35 | 36 | expectedJSON, err := json.MarshalIndent(expected, "", " ") 37 | if err != nil { 38 | return fmt.Errorf("failed to marshal expected value: %w", err) 39 | } 40 | actualJSON, err := json.MarshalIndent(actual, "", " ") 41 | if err != nil { 42 | return fmt.Errorf("failed to marshal actual value: %w", err) 43 | } 44 | 45 | if string(expectedJSON) != string(actualJSON) { 46 | return fmt.Errorf("XREAD response mismatch:\nExpected:\n%s\nGot:\n%s", expectedJSON, actualJSON) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (a XReadResponseAssertion) buildExpected() resp_value.Value { 53 | streams := make([]resp_value.Value, len(a.ExpectedStreamResponses)) 54 | for i, stream := range a.ExpectedStreamResponses { 55 | streams[i] = stream.toRESPValue() 56 | } 57 | return resp_value.NewArrayValue(streams) 58 | } 59 | 60 | func (s StreamResponse) toRESPValue() resp_value.Value { 61 | entries := make([]resp_value.Value, len(s.Entries)) 62 | for i, entry := range s.Entries { 63 | entries[i] = entry.toRESPValue() 64 | } 65 | return resp_value.NewArrayValue([]resp_value.Value{ 66 | resp_value.NewBulkStringValue(s.Key), 67 | resp_value.NewArrayValue(entries), 68 | }) 69 | } 70 | 71 | func (e StreamEntry) toRESPValue() resp_value.Value { 72 | var fieldValues []resp_value.Value 73 | for _, pair := range e.FieldValuePairs { 74 | for _, v := range pair { 75 | fieldValues = append(fieldValues, resp_value.NewBulkStringValue(v)) 76 | } 77 | } 78 | return resp_value.NewArrayValue([]resp_value.Value{ 79 | resp_value.NewBulkStringValue(e.Id), 80 | resp_value.NewArrayValue(fieldValues), 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /internal/staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = [ 2 | "all", 3 | "-ST1005", # We're okay with capitalized errors 4 | "-ST1000", # We're okay with no package comments 5 | "-ST1003", # We're okay with bad package names (we've got too many already) 6 | ] -------------------------------------------------------------------------------- /internal/stdio_mocker.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type IOMocker struct { 8 | originalStdout *os.File 9 | originalStderr *os.File 10 | originalStdin *os.File 11 | mockedStdout *os.File 12 | mockedStderr *os.File 13 | mockedStdin *os.File 14 | } 15 | 16 | func NewStdIOMocker() *IOMocker { 17 | return &IOMocker{} 18 | } 19 | 20 | func (m *IOMocker) Start() { 21 | m.originalStdout = os.Stdout 22 | m.originalStdin = os.Stdin 23 | m.originalStdin = os.Stdin 24 | 25 | m.Reset() 26 | } 27 | 28 | func (m *IOMocker) Reset() { 29 | m.mockedStdout, _ = os.CreateTemp("", "") 30 | m.mockedStdin, _ = os.CreateTemp("", "") 31 | m.mockedStderr, _ = os.CreateTemp("", "") 32 | 33 | os.Stdout = m.mockedStdout 34 | os.Stdin = m.mockedStdin 35 | os.Stderr = m.mockedStderr 36 | } 37 | 38 | func (m *IOMocker) ReadStdout() []byte { 39 | bytes, err := os.ReadFile(m.mockedStdout.Name()) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | return bytes 45 | } 46 | 47 | func (m *IOMocker) ReadStderr() []byte { 48 | bytes, err := os.ReadFile(m.mockedStderr.Name()) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | return bytes 54 | } 55 | 56 | func (m *IOMocker) End() { 57 | os.Stdout = m.originalStdout 58 | os.Stdin = m.originalStdin 59 | os.Stderr = m.originalStderr 60 | } 61 | -------------------------------------------------------------------------------- /internal/test_bind.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 5 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 6 | "github.com/codecrafters-io/tester-utils/test_case_harness" 7 | ) 8 | 9 | func testBindToPort(stageHarness *test_case_harness.TestCaseHarness) error { 10 | b := redis_executable.NewRedisExecutable(stageHarness) 11 | if err := b.Run(); err != nil { 12 | return err 13 | } 14 | 15 | logger := stageHarness.Logger 16 | 17 | bindTestCase := test_cases.BindTestCase{ 18 | Port: 6379, 19 | Retries: 15, 20 | } 21 | 22 | return bindTestCase.Run(b, logger) 23 | } 24 | -------------------------------------------------------------------------------- /internal/test_cases/bind_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 10 | "github.com/codecrafters-io/tester-utils/logger" 11 | ) 12 | 13 | type BindTestCase struct { 14 | Port int 15 | Retries int 16 | } 17 | 18 | func (t BindTestCase) Run(executable *redis_executable.RedisExecutable, logger *logger.Logger) error { 19 | logger.Infof("Connecting to port %d...", t.Port) 20 | 21 | retries := 0 22 | var err error 23 | address := "localhost:" + strconv.Itoa(t.Port) 24 | for { 25 | _, err = net.Dial("tcp", address) 26 | if err != nil && retries > t.Retries { 27 | logger.Infof("All retries failed.") 28 | return err 29 | } 30 | 31 | if err != nil { 32 | if executable.HasExited() { 33 | // We don't need to mention that the user's program exited or is expected to be a long-running process as 34 | // this could be confusing in early stages where the user is expected to only handle a single request from 35 | // a single client. 36 | // 37 | // Let's just exit early and not retry if this happens. 38 | return fmt.Errorf("Failed to connect to port %d.", t.Port) 39 | } 40 | 41 | // Don't print errors in the first second 42 | if retries > 2 { 43 | logger.Infof("Failed to connect to port %d, retrying in 1s", t.Port) 44 | } 45 | 46 | retries += 1 47 | time.Sleep(1000 * time.Millisecond) 48 | } else { 49 | break 50 | } 51 | } 52 | logger.Debugln("Connection successful") 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/test_cases/get_ack_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | "strconv" 5 | 6 | resp_connection "github.com/codecrafters-io/redis-tester/internal/resp/connection" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | "github.com/codecrafters-io/tester-utils/logger" 9 | ) 10 | 11 | type GetAckTestCase struct{} 12 | 13 | func (t GetAckTestCase) Run(client *resp_connection.RespConnection, logger *logger.Logger, offset int) error { 14 | commandTest := SendCommandTestCase{ 15 | Command: "REPLCONF", 16 | Args: []string{"GETACK", "*"}, 17 | Assertion: resp_assertions.NewCommandAssertion("REPLCONF", "ACK", strconv.Itoa(offset)), 18 | } 19 | 20 | return commandTest.Run(client, logger) 21 | } 22 | -------------------------------------------------------------------------------- /internal/test_cases/multi_command_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | "fmt" 5 | 6 | resp_client "github.com/codecrafters-io/redis-tester/internal/resp/connection" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | "github.com/codecrafters-io/tester-utils/logger" 9 | ) 10 | 11 | // MultiCommandTestCase is a concise & easier way to define & run multiple SendCommandTestCase 12 | type MultiCommandTestCase struct { 13 | Commands [][]string 14 | Assertions []resp_assertions.RESPAssertion 15 | } 16 | 17 | func (t *MultiCommandTestCase) RunAll(client *resp_client.RespConnection, logger *logger.Logger) error { 18 | if len(t.Assertions) != len(t.Commands) { 19 | return fmt.Errorf("CodeCrafters internal error. Number of commands and assertions should be equal in MultiCommandTestCase") 20 | } 21 | 22 | for i, command := range t.Commands { 23 | setCommandTestCase := SendCommandTestCase{ 24 | Command: command[0], 25 | Args: command[1:], 26 | Assertion: t.Assertions[i], 27 | } 28 | 29 | if err := setCommandTestCase.Run(client, logger); err != nil { 30 | return err 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/test_cases/receive_command_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | "fmt" 5 | 6 | resp_connection "github.com/codecrafters-io/redis-tester/internal/resp/connection" 7 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/tester-utils/logger" 10 | ) 11 | 12 | type ReceiveCommandTestCase struct { 13 | Response resp_value.Value 14 | Assertion resp_assertions.RESPAssertion 15 | ShouldSkipUnreadDataCheck bool 16 | ReceivedValue resp_value.Value 17 | } 18 | 19 | func (t *ReceiveCommandTestCase) Run(conn *resp_connection.RespConnection, logger *logger.Logger) error { 20 | value, err := conn.ReadValue() 21 | if err != nil { 22 | return err 23 | } 24 | 25 | t.ReceivedValue = value 26 | 27 | if err = t.Assertion.Run(value); err != nil { 28 | return err 29 | } 30 | 31 | logger.Successf("Received %s", value.FormattedString()) 32 | 33 | if !t.ShouldSkipUnreadDataCheck { 34 | conn.ReadIntoBuffer() // Let's make sure there's no extra data 35 | 36 | if conn.UnreadBuffer.Len() > 0 { 37 | return fmt.Errorf("Found extra data: %q", conn.UnreadBuffer.String()) 38 | } 39 | } 40 | 41 | if err := conn.SendValue(t.Response); err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/test_cases/receive_replication_handshake_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strings" 7 | 8 | resp_connection "github.com/codecrafters-io/redis-tester/internal/resp/connection" 9 | resp_encoder "github.com/codecrafters-io/redis-tester/internal/resp/encoder" 10 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 11 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 12 | "github.com/codecrafters-io/tester-utils/logger" 13 | ) 14 | 15 | // ReceiveReplicationHandshakeTestCase is a test case where we connect to a master 16 | // as a replica and perform either all or a subset of the replication handshake. 17 | // 18 | // RunAll will run all the steps in the replication handshake. Alternatively, you 19 | // can run each step individually. 20 | type ReceiveReplicationHandshakeTestCase struct{} 21 | 22 | func (t ReceiveReplicationHandshakeTestCase) RunAll(client *resp_connection.RespConnection, logger *logger.Logger) error { 23 | if err := t.RunPingStep(client, logger); err != nil { 24 | return err 25 | } 26 | 27 | if err := t.RunReplconfStep1(client, logger); err != nil { 28 | return err 29 | } 30 | 31 | if err := t.RunReplconfStep2(client, logger); err != nil { 32 | return err 33 | } 34 | 35 | if err := t.RunPsyncStep(client, logger); err != nil { 36 | return err 37 | } 38 | 39 | if err := t.RunSendRDBStep(client, logger); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (t ReceiveReplicationHandshakeTestCase) RunPingStep(client *resp_connection.RespConnection, logger *logger.Logger) error { 47 | logger.Infof("master: Waiting for replica to initiate handshake with %q command", "PING") 48 | 49 | commandTest := ReceiveCommandTestCase{ 50 | Assertion: resp_assertions.NewCommandAssertion("PING"), 51 | Response: resp_value.NewSimpleStringValue("PONG"), 52 | } 53 | 54 | return commandTest.Run(client, logger) 55 | } 56 | 57 | func (t ReceiveReplicationHandshakeTestCase) RunReplconfStep1(client *resp_connection.RespConnection, logger *logger.Logger) error { 58 | logger.Infof("master: Waiting for replica to send %q command", "REPLCONF listening-port 6380") 59 | 60 | commandTest := ReceiveCommandTestCase{ 61 | Assertion: resp_assertions.NewCommandAssertion("REPLCONF", "listening-port", "6380"), 62 | Response: resp_value.NewSimpleStringValue("OK"), 63 | ShouldSkipUnreadDataCheck: true, 64 | } 65 | 66 | return commandTest.Run(client, logger) 67 | } 68 | 69 | func (t ReceiveReplicationHandshakeTestCase) RunReplconfStep2(client *resp_connection.RespConnection, logger *logger.Logger) error { 70 | logger.Infof("master: Waiting for replica to send %q command", "REPLCONF capa") 71 | 72 | commandTest := ReceiveCommandTestCase{ 73 | Assertion: resp_assertions.NewOnlyCommandAssertion("REPLCONF"), 74 | Response: resp_value.NewSimpleStringValue("OK"), 75 | } 76 | 77 | err := commandTest.Run(client, logger) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | receivedValue := commandTest.ReceivedValue 83 | 84 | elements := receivedValue.Array() 85 | 86 | if len(elements) < 3 { 87 | return fmt.Errorf("Expected array with at least 3 element, got %d elements", len(elements)) 88 | } 89 | 90 | firstCapaArg := elements[1].String() 91 | 92 | if elements[1].Type != resp_value.SIMPLE_STRING && elements[1].Type != resp_value.BULK_STRING { 93 | return fmt.Errorf("Expected first replconf argument to be a string, got %s", elements[1].Type) 94 | } 95 | 96 | if !strings.EqualFold(firstCapaArg, "capa") { 97 | return fmt.Errorf("Expected first replconf argument to be %q, got %q", "capa", strings.ToLower(firstCapaArg)) 98 | } 99 | 100 | if len(elements) == 5 { 101 | if elements[3].Type != resp_value.SIMPLE_STRING && elements[3].Type != resp_value.BULK_STRING { 102 | return fmt.Errorf("Expected third replconf argument to be a string, got %s", elements[3].Type) 103 | } 104 | 105 | secondCapaArg := elements[3].String() 106 | 107 | if !strings.EqualFold(secondCapaArg, "capa") { 108 | return fmt.Errorf("Expected third replconf argument to be %q, got %q", "capa", strings.ToLower(secondCapaArg)) 109 | } 110 | } 111 | 112 | return nil 113 | } 114 | 115 | func (t ReceiveReplicationHandshakeTestCase) RunPsyncStep(client *resp_connection.RespConnection, logger *logger.Logger) error { 116 | logger.Infof("master: Waiting for replica to send %q command", "PSYNC") 117 | 118 | id := "75cd7bc10c49047e0d163660f3b90625b1af31dc" 119 | commandTest := ReceiveCommandTestCase{ 120 | Assertion: resp_assertions.NewCommandAssertion("PSYNC", "?", "-1"), 121 | Response: resp_value.NewSimpleStringValue(fmt.Sprintf("FULLRESYNC %v 0", id)), 122 | } 123 | 124 | return commandTest.Run(client, logger) 125 | } 126 | 127 | func (t ReceiveReplicationHandshakeTestCase) RunSendRDBStep(client *resp_connection.RespConnection, logger *logger.Logger) error { 128 | logger.Debugln("Sending RDB file...") 129 | 130 | hexStr := "524544495330303131fa0972656469732d76657205372e322e30fa0a72656469732d62697473c040fa056374696d65c26d08bc65fa08757365642d6d656dc2b0c41000fa08616f662d62617365c000fff06e3bfec0ff5aa2" 131 | bytes, err := hex.DecodeString(hexStr) 132 | if err != nil { 133 | panic(fmt.Sprintf("Encountered %s while decoding hex string", err.Error())) 134 | } 135 | 136 | encodedValue := resp_encoder.EncodeFullResyncRDBFile(bytes) 137 | client.SendBytes(encodedValue) 138 | 139 | logger.Successf("Sent RDB file.") 140 | return nil 141 | } 142 | -------------------------------------------------------------------------------- /internal/test_cases/receive_value_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | "fmt" 5 | 6 | resp_client "github.com/codecrafters-io/redis-tester/internal/resp/connection" 7 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | logger "github.com/codecrafters-io/tester-utils/logger" 10 | ) 11 | 12 | type ReceiveValueTestCase struct { 13 | Assertion resp_assertions.RESPAssertion 14 | ShouldSkipUnreadDataCheck bool 15 | 16 | // This is set after the test is run 17 | ActualValue resp_value.Value 18 | } 19 | 20 | func (t *ReceiveValueTestCase) Run(client *resp_client.RespConnection, logger *logger.Logger) error { 21 | value, err := client.ReadValue() 22 | if err != nil { 23 | return err 24 | } 25 | 26 | t.ActualValue = value 27 | 28 | if err = t.Assertion.Run(value); err != nil { 29 | return err 30 | } 31 | 32 | if !t.ShouldSkipUnreadDataCheck { 33 | client.ReadIntoBuffer() // Let's make sure there's no extra data 34 | 35 | if client.UnreadBuffer.Len() > 0 { 36 | return fmt.Errorf("Found extra data: %q", client.UnreadBuffer.String()) 37 | } 38 | } 39 | 40 | logger.Successf("Received %s", value.FormattedString()) 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/test_cases/send_command_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | resp_client "github.com/codecrafters-io/redis-tester/internal/resp/connection" 10 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 11 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 12 | "github.com/codecrafters-io/tester-utils/logger" 13 | ) 14 | 15 | type SendCommandTestCase struct { 16 | Command string 17 | Args []string 18 | Assertion resp_assertions.RESPAssertion 19 | ShouldSkipUnreadDataCheck bool 20 | Retries int 21 | ShouldRetryFunc func(resp_value.Value) bool 22 | 23 | // ReceivedResponse is set after the test case is run 24 | ReceivedResponse resp_value.Value 25 | 26 | readMutex sync.Mutex 27 | } 28 | 29 | func (t *SendCommandTestCase) Run(client *resp_client.RespConnection, logger *logger.Logger) error { 30 | var value resp_value.Value 31 | var err error 32 | 33 | for attempt := 0; attempt <= t.Retries; attempt++ { 34 | if attempt > 0 { 35 | logger.Infof("Retrying... (%d/%d attempts)", attempt, t.Retries) 36 | } 37 | 38 | command := strings.ToUpper(t.Command) 39 | 40 | if err = client.SendCommand(command, t.Args...); err != nil { 41 | return err 42 | } 43 | 44 | t.readMutex.Lock() 45 | defer t.readMutex.Unlock() 46 | 47 | value, err = client.ReadValue() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if t.Retries == 0 { 53 | break 54 | } 55 | 56 | if t.ShouldRetryFunc == nil { 57 | panic(fmt.Sprintf("Received SendCommand with retries: %d but no ShouldRetryFunc.", t.Retries)) 58 | } else { 59 | if t.ShouldRetryFunc(value) { 60 | // If ShouldRetryFunc returns true, we sleep and retry. 61 | time.Sleep(500 * time.Millisecond) 62 | } else { 63 | break 64 | } 65 | } 66 | } 67 | 68 | t.ReceivedResponse = value 69 | 70 | if err = t.Assertion.Run(value); err != nil { 71 | return err 72 | } 73 | 74 | if !t.ShouldSkipUnreadDataCheck { 75 | client.ReadIntoBuffer() // Let's make sure there's no extra data 76 | 77 | if client.UnreadBuffer.Len() > 0 { 78 | return fmt.Errorf("Found extra data: %q", client.UnreadBuffer.String()) 79 | } 80 | } 81 | 82 | logger.Successf("Received %s", value.FormattedString()) 83 | return nil 84 | } 85 | 86 | func (t *SendCommandTestCase) PauseReadingResponse() { 87 | t.readMutex.Lock() 88 | } 89 | 90 | func (t *SendCommandTestCase) ResumeReadingResponse() { 91 | t.readMutex.Unlock() 92 | } 93 | -------------------------------------------------------------------------------- /internal/test_cases/send_replication_handshake_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | resp_client "github.com/codecrafters-io/redis-tester/internal/resp/connection" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/tester-utils/logger" 10 | rdb_parser "github.com/hdt3213/rdb/parser" 11 | ) 12 | 13 | // SendReplicationHandshakeTestCase is a test case where we connect to a master 14 | // as a replica and perform either all or a subset of the replication handshake. 15 | // 16 | // RunAll will run all the steps in the replication handshake. Alternatively, you 17 | // can run each step individually. 18 | type SendReplicationHandshakeTestCase struct{} 19 | 20 | func (t SendReplicationHandshakeTestCase) RunAll(client *resp_client.RespConnection, logger *logger.Logger, listeningPort int) error { 21 | if err := t.RunPingStep(client, logger); err != nil { 22 | return err 23 | } 24 | 25 | if err := t.RunReplconfStep(client, logger, listeningPort); err != nil { 26 | return err 27 | } 28 | 29 | if err := t.RunPsyncStep(client, logger); err != nil { 30 | return err 31 | } 32 | 33 | if err := t.RunReceiveRDBStep(client, logger); err != nil { 34 | return err 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (t SendReplicationHandshakeTestCase) RunPingStep(client *resp_client.RespConnection, logger *logger.Logger) error { 41 | commandTest := SendCommandTestCase{ 42 | Command: "PING", 43 | Args: []string{}, 44 | Assertion: resp_assertions.NewStringAssertion("PONG"), 45 | } 46 | 47 | return commandTest.Run(client, logger) 48 | } 49 | 50 | func (t SendReplicationHandshakeTestCase) RunReplconfStep(client *resp_client.RespConnection, logger *logger.Logger, listeningPort int) error { 51 | commandTest := SendCommandTestCase{ 52 | Command: "REPLCONF", 53 | Args: []string{"listening-port", fmt.Sprintf("%d", listeningPort)}, 54 | Assertion: resp_assertions.NewStringAssertion("OK"), 55 | } 56 | 57 | if err := commandTest.Run(client, logger); err != nil { 58 | return err 59 | } 60 | 61 | commandTest = SendCommandTestCase{ 62 | Command: "REPLCONF", 63 | Args: []string{"capa", "psync2"}, 64 | Assertion: resp_assertions.NewStringAssertion("OK"), 65 | } 66 | 67 | return commandTest.Run(client, logger) 68 | } 69 | 70 | func (t SendReplicationHandshakeTestCase) RunPsyncStep(client *resp_client.RespConnection, logger *logger.Logger) error { 71 | commandTest := SendCommandTestCase{ 72 | Command: "PSYNC", 73 | Args: []string{"?", "-1"}, 74 | Assertion: resp_assertions.NewRegexStringAssertion("FULLRESYNC \\w+ 0"), 75 | ShouldSkipUnreadDataCheck: true, // We're expecting the RDB file to be sent next 76 | } 77 | 78 | return commandTest.Run(client, logger) 79 | } 80 | 81 | func (t SendReplicationHandshakeTestCase) RunReceiveRDBStep(client *resp_client.RespConnection, logger *logger.Logger) error { 82 | logger.Debugln("Reading RDB file...") 83 | 84 | rdbFileBytes, err := client.ReadFullResyncRDBFile() 85 | if err != nil { 86 | return err 87 | } 88 | 89 | // We don't care about the contents of the RDB file, we just want to make sure the file was valid 90 | processRedisObject := func(_ rdb_parser.RedisObject) bool { 91 | return true 92 | } 93 | 94 | decoder := rdb_parser.NewDecoder(bytes.NewReader(rdbFileBytes)) 95 | if err = decoder.Parse(processRedisObject); err != nil { 96 | return fmt.Errorf("Invalid RDB file: %v", err) 97 | } 98 | 99 | client.ReadIntoBuffer() // Let's make sure there's no extra data 100 | 101 | if client.UnreadBuffer.Len() > 0 { 102 | return fmt.Errorf("Found extra data: %q", client.UnreadBuffer.String()) 103 | } 104 | 105 | logger.Successf("Received RDB file") 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /internal/test_cases/transaction_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | resp_client "github.com/codecrafters-io/redis-tester/internal/resp/connection" 5 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 6 | "github.com/codecrafters-io/tester-utils/logger" 7 | ) 8 | 9 | // TransactionTestCase is a test case where we initiate a transaction by sending "MULTI" command 10 | // Send a series of commands to the server expected back "QUEUED" for each command 11 | // Finally send "EXEC" command and expect the response to be the same as ExpectedResponseArray 12 | // 13 | // RunAll will run all the steps in the Transaction execution. Alternatively, you 14 | // can run each step individually. 15 | type TransactionTestCase struct { 16 | // All the CommandQueue will be sent in order to client 17 | // And a string "QUEUED" will be expected 18 | CommandQueue [][]string 19 | 20 | // After queueing all the commands, 21 | // if "EXEC" is sent (based on which function is called) 22 | // The elements in the response array are asserted based on the 23 | // assertions in the ExpectedResponseArray 24 | ExpectedResponseArray []resp_assertions.RESPAssertion 25 | } 26 | 27 | func (t TransactionTestCase) RunAll(client *resp_client.RespConnection, logger *logger.Logger) error { 28 | if err := t.RunMulti(client, logger); err != nil { 29 | return err 30 | } 31 | 32 | if err := t.RunQueueAll(client, logger); err != nil { 33 | return err 34 | } 35 | 36 | if err := t.RunExec(client, logger); err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (t TransactionTestCase) RunWithoutExec(client *resp_client.RespConnection, logger *logger.Logger) error { 44 | if err := t.RunMulti(client, logger); err != nil { 45 | return err 46 | } 47 | 48 | if err := t.RunQueueAll(client, logger); err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (t TransactionTestCase) RunMulti(client *resp_client.RespConnection, logger *logger.Logger) error { 56 | commandTest := SendCommandTestCase{ 57 | Command: "MULTI", 58 | Args: []string{}, 59 | Assertion: resp_assertions.NewStringAssertion("OK"), 60 | } 61 | 62 | return commandTest.Run(client, logger) 63 | } 64 | 65 | func (t TransactionTestCase) RunQueueAll(client *resp_client.RespConnection, logger *logger.Logger) error { 66 | for i, v := range t.CommandQueue { 67 | logger.Debugf("Sending command: #%d/#%d", i+1, len(t.CommandQueue)) 68 | commandTest := SendCommandTestCase{ 69 | Command: v[0], 70 | Args: v[1:], 71 | Assertion: resp_assertions.NewStringAssertion("QUEUED"), 72 | } 73 | if err := commandTest.Run(client, logger); err != nil { 74 | return err 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func (t TransactionTestCase) RunExec(client *resp_client.RespConnection, logger *logger.Logger) error { 82 | setCommandTestCase := SendCommandTestCase{ 83 | Command: "EXEC", 84 | Args: []string{}, 85 | Assertion: resp_assertions.NewOrderedArrayAssertion(t.ExpectedResponseArray), 86 | } 87 | 88 | return setCommandTestCase.Run(client, logger) 89 | } 90 | -------------------------------------------------------------------------------- /internal/test_cases/wait_test_case.go: -------------------------------------------------------------------------------- 1 | package test_cases 2 | 3 | import ( 4 | "strconv" 5 | 6 | resp_connection "github.com/codecrafters-io/redis-tester/internal/resp/connection" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | "github.com/codecrafters-io/tester-utils/logger" 9 | ) 10 | 11 | type WaitTestCase struct { 12 | Replicas int 13 | TimeoutInMilliseconds int 14 | ExpectedMessage int 15 | } 16 | 17 | func (t WaitTestCase) Run(client *resp_connection.RespConnection, logger *logger.Logger) error { 18 | commandTest := SendCommandTestCase{ 19 | Command: "WAIT", 20 | Args: []string{strconv.Itoa(t.Replicas), strconv.Itoa(t.TimeoutInMilliseconds)}, 21 | Assertion: resp_assertions.NewIntegerAssertion(t.ExpectedMessage), 22 | } 23 | 24 | return commandTest.Run(client, logger) 25 | } 26 | -------------------------------------------------------------------------------- /internal/test_echo.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 7 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 8 | "github.com/codecrafters-io/tester-utils/random" 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | // Tests 'ECHO' 13 | func testEcho(stageHarness *test_case_harness.TestCaseHarness) error { 14 | b := redis_executable.NewRedisExecutable(stageHarness) 15 | if err := b.Run(); err != nil { 16 | return err 17 | } 18 | 19 | logger := stageHarness.Logger 20 | 21 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "") 22 | if err != nil { 23 | return err 24 | } 25 | 26 | randomWord := random.RandomWord() 27 | 28 | commandTestCase := test_cases.SendCommandTestCase{ 29 | Command: "echo", 30 | Args: []string{randomWord}, 31 | Assertion: resp_assertions.NewStringAssertion(randomWord), 32 | } 33 | 34 | if err := commandTestCase.Run(client, logger); err != nil { 35 | logFriendlyError(logger, err) 36 | return err 37 | } 38 | 39 | client.Close() 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/test_expiry.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 7 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | "github.com/codecrafters-io/tester-utils/random" 11 | "github.com/codecrafters-io/tester-utils/test_case_harness" 12 | ) 13 | 14 | // Tests Expiry 15 | func testExpiry(stageHarness *test_case_harness.TestCaseHarness) error { 16 | b := redis_executable.NewRedisExecutable(stageHarness) 17 | if err := b.Run(); err != nil { 18 | return err 19 | } 20 | 21 | logger := stageHarness.Logger 22 | 23 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "") 24 | if err != nil { 25 | logFriendlyError(logger, err) 26 | return err 27 | } 28 | 29 | randomWords := random.RandomWords(2) 30 | randomKey := randomWords[0] 31 | randomValue := randomWords[1] 32 | 33 | setCommandTestCase := test_cases.SendCommandTestCase{ 34 | Command: "set", 35 | Args: []string{randomKey, randomValue, "px", "100"}, 36 | Assertion: resp_assertions.NewStringAssertion("OK"), 37 | } 38 | 39 | if err := setCommandTestCase.Run(client, logger); err != nil { 40 | logFriendlyError(logger, err) 41 | return err 42 | } 43 | 44 | logger.Successf("Received OK at %s", time.Now().Format("15:04:05.000")) 45 | logger.Infof("Fetching key %q at %s (should not be expired)", randomKey, time.Now().Format("15:04:05.000")) 46 | 47 | getCommandTestCase := test_cases.SendCommandTestCase{ 48 | Command: "get", 49 | Args: []string{randomKey}, 50 | Assertion: resp_assertions.NewStringAssertion(randomValue), 51 | } 52 | 53 | if err := getCommandTestCase.Run(client, logger); err != nil { 54 | logFriendlyError(logger, err) 55 | return err 56 | } 57 | 58 | logger.Debugf("Sleeping for 101ms") 59 | time.Sleep(101 * time.Millisecond) 60 | 61 | logger.Infof("Fetching key %q at %s (should be expired)", randomKey, time.Now().Format("15:04:05.000")) 62 | 63 | getCommandTestCase = test_cases.SendCommandTestCase{ 64 | Command: "get", 65 | Args: []string{randomKey}, 66 | Assertion: resp_assertions.NewNilAssertion(), 67 | } 68 | 69 | if err := getCommandTestCase.Run(client, logger); err != nil { 70 | logFriendlyError(logger, err) 71 | return err 72 | } 73 | 74 | client.Close() 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/test_get_set.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 7 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 8 | "github.com/codecrafters-io/tester-utils/random" 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | // Tests 'GET, SET' 13 | func testGetSet(stageHarness *test_case_harness.TestCaseHarness) error { 14 | b := redis_executable.NewRedisExecutable(stageHarness) 15 | if err := b.Run(); err != nil { 16 | return err 17 | } 18 | 19 | logger := stageHarness.Logger 20 | 21 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "") 22 | if err != nil { 23 | logFriendlyError(logger, err) 24 | return err 25 | } 26 | 27 | randomWords := random.RandomWords(2) 28 | 29 | randomKey := randomWords[0] 30 | randomValue := randomWords[1] 31 | 32 | logger.Debugf("Setting key %s to %s", randomKey, randomValue) 33 | setCommandTestCase := test_cases.SendCommandTestCase{ 34 | Command: "set", 35 | Args: []string{randomKey, randomValue}, 36 | Assertion: resp_assertions.NewStringAssertion("OK"), 37 | } 38 | 39 | if err := setCommandTestCase.Run(client, logger); err != nil { 40 | logFriendlyError(logger, err) 41 | return err 42 | } 43 | 44 | logger.Debugf("Getting key %s", randomKey) 45 | 46 | getCommandTestCase := test_cases.SendCommandTestCase{ 47 | Command: "get", 48 | Args: []string{randomKey}, 49 | Assertion: resp_assertions.NewStringAssertion(randomValue), 50 | } 51 | 52 | if err := getCommandTestCase.Run(client, logger); err != nil { 53 | logFriendlyError(logger, err) 54 | return err 55 | } 56 | 57 | client.Close() 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/test_helpers/fixtures/bind/failure: -------------------------------------------------------------------------------- 1 | [stage-1] Running tests for Stage #1: jm1 2 | [stage-1] $ ./spawn_redis_server.sh 3 | [stage-1] Connecting to port 6379... 4 | [your_program] hey, not going to bind! 5 | [stage-1] Failed to connect to port 6379. 6 | [stage-1] Test failed (try setting 'debug: true' in your codecrafters.yml to see more details) 7 | -------------------------------------------------------------------------------- /internal/test_helpers/fixtures/bind/success: -------------------------------------------------------------------------------- 1 | Debug = true 2 | 3 | [stage-1] Running tests for Stage #1: jm1 4 | [stage-1] $ ./spawn_redis_server.sh 5 | [stage-1] Connecting to port 6379... 6 | [your_program] hey, binding to 6379 7 | [stage-1] Connection successful 8 | [stage-1] Test passed. 9 | [stage-1] Terminating program 10 | [stage-1] Program terminated successfully 11 | -------------------------------------------------------------------------------- /internal/test_helpers/fixtures/bind/timeout: -------------------------------------------------------------------------------- 1 | [stage-1] Running tests for Stage #1: jm1 2 | [stage-1] $ ./spawn_redis_server.sh 3 | [stage-1] Connecting to port 6379... 4 | [your_program] hey, not going to bind! 5 | [stage-1] Failed to connect to port 6379, retrying in 1s 6 | [stage-1] Failed to connect to port 6379, retrying in 1s 7 | [stage-1] Failed to connect to port 6379, retrying in 1s 8 | [stage-1] Failed to connect to port 6379, retrying in 1s 9 | [stage-1] Failed to connect to port 6379, retrying in 1s 10 | [stage-1] Failed to connect to port 6379, retrying in 1s 11 | [stage-1] Failed to connect to port 6379, retrying in 1s 12 | [stage-1] Failed to connect to port 6379, retrying in 1s 13 | [stage-1] Failed to connect to port 6379, retrying in 1s 14 | [stage-1] Failed to connect to port 6379, retrying in 1s 15 | [stage-1] Failed to connect to port 6379, retrying in 1s 16 | [stage-1] Failed to connect to port 6379, retrying in 1s 17 | [stage-1] timed out, test exceeded 15 seconds 18 | [stage-1] Test failed (try setting 'debug: true' in your codecrafters.yml to see more details) 19 | -------------------------------------------------------------------------------- /internal/test_helpers/fixtures/ping-pong/eof: -------------------------------------------------------------------------------- 1 | Debug = true 2 | 3 | [stage-2] Running tests for Stage #2: rg2 4 | [stage-2] $ ./spawn_redis_server.sh 5 | [your_program] hey, binding to 6379 6 | [stage-2] Connection established, sending ping command... 7 | [stage-2] $ redis-cli PING 8 | [stage-2] Sent bytes: "*1\r\n$4\r\nPING\r\n" 9 | [stage-2] Received: "" (no content received) 10 | [stage-2]  ^ error 11 | [stage-2] Error: Expected start of a new RESP2 value (either +, -, :, $ or *) 12 | [stage-2] Test failed 13 | [stage-2] Terminating program 14 | [stage-2] Program terminated successfully 15 | -------------------------------------------------------------------------------- /internal/test_helpers/fixtures/ping-pong/slow_response: -------------------------------------------------------------------------------- 1 | [stage-2] Running tests for Stage #2: rg2 2 | [stage-2] $ ./spawn_redis_server.sh 3 | [your_program] hey, binding to 6379 4 | [stage-2] $ redis-cli PING 5 | [stage-2] Received "PONG" 6 | [stage-2] Test passed. 7 | 8 | [stage-1] Running tests for Stage #1: jm1 9 | [stage-1] $ ./spawn_redis_server.sh 10 | [stage-1] Connecting to port 6379... 11 | [your_program] hey, binding to 6379 12 | [stage-1] Test passed. 13 | -------------------------------------------------------------------------------- /internal/test_helpers/fixtures/ping-pong/without_crlf: -------------------------------------------------------------------------------- 1 | [stage-2] Running tests for Stage #2: rg2 2 | [stage-2] $ ./spawn_redis_server.sh 3 | [your_program] hey, binding to 6379 4 | [stage-2] $ redis-cli PING 5 | [stage-2] Received: "+PONG" 6 | [stage-2]  ^ error 7 | [stage-2] Error: Expected \r\n at the end of a simple string 8 | [stage-2] Test failed (try setting 'debug: true' in your codecrafters.yml to see more details) 9 | -------------------------------------------------------------------------------- /internal/test_helpers/fixtures/ping-pong/without_read_multiple_pongs: -------------------------------------------------------------------------------- 1 | [stage-2] Running tests for Stage #2: rg2 2 | [stage-2] $ ./spawn_redis_server.sh 3 | [your_program] Logs from your program will appear here! 4 | [stage-2] $ redis-cli PING 5 | [stage-2] Found extra data: "+PONG\r\n+PONG\r\n" 6 | [stage-2] Test failed (try setting 'debug: true' in your codecrafters.yml to see more details) 7 | -------------------------------------------------------------------------------- /internal/test_helpers/pass_all/codecrafters.yml: -------------------------------------------------------------------------------- 1 | # This is the current stage that you're on. 2 | # 3 | # Whenever you want to advance to the next stage, 4 | # bump this to the next number. 5 | current_stage: 7 6 | 7 | # Set this to true if you want debug logs. 8 | # 9 | # These can be VERY verbose, so we suggest turning them off 10 | # unless you really need them. 11 | debug: true 12 | -------------------------------------------------------------------------------- /internal/test_helpers/pass_all/spawn_redis_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | find "." -type f -name "*.rdb" -exec rm {} + 3 | exec redis-server --loglevel nothing $@ 4 | -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/bind/failure/codecrafters.yml: -------------------------------------------------------------------------------- 1 | # Set this to true if you want debug logs. 2 | # 3 | # These can be VERY verbose, so we suggest turning them off 4 | # unless you really need them. 5 | debug: false -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/bind/failure/spawn_redis_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "hey, not going to bind!" -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/bind/success/codecrafters.yml: -------------------------------------------------------------------------------- 1 | # Set this to true if you want debug logs. 2 | # 3 | # These can be VERY verbose, so we suggest turning them off 4 | # unless you really need them. 5 | debug: true -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/bind/success/spawn_redis_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "hey, binding to 6379" 3 | exec nc -l 6379 -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/bind/timeout/codecrafters.yml: -------------------------------------------------------------------------------- 1 | # Set this to true if you want debug logs. 2 | # 3 | # These can be VERY verbose, so we suggest turning them off 4 | # unless you really need them. 5 | debug: false -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/bind/timeout/spawn_redis_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "hey, not going to bind!" 3 | sleep 20 4 | -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/ping-pong/eof/codecrafters.yml: -------------------------------------------------------------------------------- 1 | # Set this to true if you want debug logs. 2 | # 3 | # These can be VERY verbose, so we suggest turning them off 4 | # unless you really need them. 5 | debug: true -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/ping-pong/eof/spawn_redis_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -u 2 | import socket 3 | import time 4 | print("hey, binding to 6379") 5 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 6 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 7 | 8 | sock.bind(('', 6379)) 9 | 10 | sock.listen(1) 11 | 12 | conn, cli_addr = sock.accept() 13 | time.sleep(0.5) 14 | conn.close() 15 | -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/ping-pong/slow_response/codecrafters.yml: -------------------------------------------------------------------------------- 1 | # Set this to true if you want debug logs. 2 | # 3 | # These can be VERY verbose, so we suggest turning them off 4 | # unless you really need them. 5 | debug: false -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/ping-pong/slow_response/spawn_redis_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -u 2 | import socket 3 | import time 4 | print("hey, binding to 6379") 5 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 6 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 7 | 8 | sock.bind(('', 6379)) 9 | 10 | sock.listen(1) 11 | 12 | conn, cli_addr = sock.accept() 13 | conn.send(b"+PONG") 14 | time.sleep(0.1) # Add sleep to ensure we're reading with an appropriate timeout 15 | conn.send(b"\r\n") 16 | -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/ping-pong/without_crlf/codecrafters.yml: -------------------------------------------------------------------------------- 1 | # Set this to true if you want debug logs. 2 | # 3 | # These can be VERY verbose, so we suggest turning them off 4 | # unless you really need them. 5 | debug: false 6 | -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/ping-pong/without_crlf/spawn_redis_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -u 2 | import socket 3 | import time 4 | print("hey, binding to 6379") 5 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 6 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 7 | 8 | sock.bind(('', 6379)) 9 | 10 | sock.listen(1) 11 | 12 | conn, cli_addr = sock.accept() 13 | conn.send(b"+PONG") 14 | -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/ping-pong/without_read_multiple_pongs/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | // You can use print statements as follows for debugging, they'll be visible when running tests. 11 | fmt.Println("Logs from your program will appear here!") 12 | l, err := net.Listen("tcp", "0.0.0.0:6379") 13 | if err != nil { 14 | fmt.Println("Failed to bind to port 6379") 15 | os.Exit(1) 16 | } 17 | connection, err := l.Accept() 18 | if err != nil { 19 | fmt.Println("Error accepting connection: ", err.Error()) 20 | os.Exit(1) 21 | } 22 | 23 | // Simulate case where user doesn't read commands one by one 24 | resp := []byte("+PONG\r\n+PONG\r\n+PONG\r\n") 25 | _, err = connection.Write(resp) 26 | if err != nil { 27 | fmt.Println("Error writing resp: ", err.Error()) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/ping-pong/without_read_multiple_pongs/codecrafters.yml: -------------------------------------------------------------------------------- 1 | # Set this to true if you want debug logs. 2 | # 3 | # These can be VERY verbose, so we suggest turning them off 4 | # unless you really need them. 5 | debug: false 6 | -------------------------------------------------------------------------------- /internal/test_helpers/scenarios/ping-pong/without_read_multiple_pongs/spawn_redis_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # DON'T EDIT THIS! 4 | # 5 | # CodeCrafters uses this file to test your code. Don't make any changes here! 6 | # 7 | # DON'T EDIT THIS! 8 | set -e 9 | tmpFile=$(mktemp) 10 | 11 | (cd $(dirname "$0") && 12 | go build -o "$tmpFile" app/main.go) 13 | 14 | exec "$tmpFile" 15 | -------------------------------------------------------------------------------- /internal/test_ping_pong.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | resp_connection "github.com/codecrafters-io/redis-tester/internal/resp/connection" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 9 | "github.com/codecrafters-io/tester-utils/logger" 10 | "github.com/codecrafters-io/tester-utils/test_case_harness" 11 | ) 12 | 13 | func testPingPongOnce(stageHarness *test_case_harness.TestCaseHarness) error { 14 | b := redis_executable.NewRedisExecutable(stageHarness) 15 | if err := b.Run(); err != nil { 16 | return err 17 | } 18 | 19 | logger := stageHarness.Logger 20 | 21 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "") 22 | if err != nil { 23 | return err 24 | } 25 | 26 | logger.Debugln("Connection established, sending ping command...") 27 | 28 | commandTestCase := test_cases.SendCommandTestCase{ 29 | Command: "ping", 30 | Args: []string{}, 31 | Assertion: resp_assertions.NewStringAssertion("PONG"), 32 | } 33 | 34 | if err := commandTestCase.Run(client, logger); err != nil { 35 | logFriendlyError(logger, err) 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func testPingPongMultiple(stageHarness *test_case_harness.TestCaseHarness) error { 43 | b := redis_executable.NewRedisExecutable(stageHarness) 44 | if err := b.Run(); err != nil { 45 | return err 46 | } 47 | 48 | logger := stageHarness.Logger 49 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client-1") 50 | if err != nil { 51 | return err 52 | } 53 | 54 | for i := 1; i <= 3; i++ { 55 | if err := runPing(logger, client); err != nil { 56 | return err 57 | } 58 | } 59 | 60 | logger.Debugf("Success, closing connection...") 61 | client.Close() 62 | 63 | return nil 64 | } 65 | 66 | func testPingPongConcurrent(stageHarness *test_case_harness.TestCaseHarness) error { 67 | b := redis_executable.NewRedisExecutable(stageHarness) 68 | if err := b.Run(); err != nil { 69 | return err 70 | } 71 | 72 | logger := stageHarness.Logger 73 | client1, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client-1") 74 | if err != nil { 75 | return err 76 | } 77 | 78 | if err := runPing(logger, client1); err != nil { 79 | return err 80 | } 81 | 82 | client2, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client-2") 83 | if err != nil { 84 | return err 85 | } 86 | 87 | if err := runPing(logger, client2); err != nil { 88 | return err 89 | } 90 | 91 | if err := runPing(logger, client1); err != nil { 92 | return err 93 | } 94 | if err := runPing(logger, client1); err != nil { 95 | return err 96 | } 97 | if err := runPing(logger, client2); err != nil { 98 | return err 99 | } 100 | 101 | logger.Debugf("client-%d: Success, closing connection...", 1) 102 | client1.Close() 103 | 104 | client3, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client-3") 105 | if err != nil { 106 | return err 107 | } 108 | 109 | if err := runPing(logger, client3); err != nil { 110 | return err 111 | } 112 | 113 | logger.Debugf("client-%d: Success, closing connection...", 2) 114 | client2.Close() 115 | logger.Debugf("client-%d: Success, closing connection...", 3) 116 | client3.Close() 117 | 118 | return nil 119 | } 120 | 121 | func runPing(logger *logger.Logger, client *resp_connection.RespConnection) error { 122 | commandTestCase := test_cases.SendCommandTestCase{ 123 | Command: "ping", 124 | Args: []string{}, 125 | Assertion: resp_assertions.NewStringAssertion("PONG"), 126 | } 127 | 128 | if err := commandTestCase.Run(client, logger); err != nil { 129 | logFriendlyError(logger, err) 130 | return err 131 | } 132 | 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /internal/test_rdb_config.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 9 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 10 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 11 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 12 | 13 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 14 | "github.com/codecrafters-io/tester-utils/test_case_harness" 15 | ) 16 | 17 | func testRdbConfig(stageHarness *test_case_harness.TestCaseHarness) error { 18 | tmpDir, err := os.MkdirTemp("", "rdbfiles") 19 | if err != nil { 20 | return err 21 | } 22 | 23 | // On MacOS, the tmpDir is a symlink to a directory in /var/folders/... 24 | realPath, err := filepath.EvalSymlinks(tmpDir) 25 | if err != nil { 26 | return fmt.Errorf("CodeCrafters tester error: could not resolve symlink: %v", err) 27 | } 28 | tmpDir = realPath 29 | 30 | b := redis_executable.NewRedisExecutable(stageHarness) 31 | stageHarness.RegisterTeardownFunc(func() { os.RemoveAll(tmpDir) }) 32 | if err := b.Run("--dir", tmpDir, 33 | "--dbfilename", fmt.Sprintf("%s.rdb", testerutils_random.RandomWord())); err != nil { 34 | return err 35 | } 36 | 37 | logger := stageHarness.Logger 38 | 39 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 40 | if err != nil { 41 | return err 42 | } 43 | defer client.Close() 44 | 45 | commandTestCase := test_cases.SendCommandTestCase{ 46 | Command: "CONFIG", 47 | Args: []string{"GET", "dir"}, 48 | Assertion: resp_assertions.NewOrderedStringArrayAssertion([]string{"dir", tmpDir}), 49 | ShouldSkipUnreadDataCheck: false, 50 | } 51 | 52 | if err := commandTestCase.Run(client, logger); err != nil { 53 | logFriendlyError(logger, err) 54 | return err 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/test_rdb_read_key.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 7 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | 11 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testRdbReadKey(stageHarness *test_case_harness.TestCaseHarness) error { 16 | RDBFileCreator, err := NewRDBFileCreator() 17 | if err != nil { 18 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 19 | } 20 | 21 | randomKeyAndValue := testerutils_random.RandomWords(2) 22 | randomKey, randomValue := randomKeyAndValue[0], randomKeyAndValue[1] 23 | 24 | if err := RDBFileCreator.Write([]KeyValuePair{{key: randomKey, value: randomValue}}); err != nil { 25 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 26 | } 27 | 28 | logger := stageHarness.Logger 29 | logger.Infof("Created RDB file with a single key: [%q]", randomKey) 30 | if err := RDBFileCreator.PrintContentHexdump(logger); err != nil { 31 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 32 | } 33 | 34 | b := redis_executable.NewRedisExecutable(stageHarness) 35 | stageHarness.RegisterTeardownFunc(func() { RDBFileCreator.Cleanup() }) 36 | if err := b.Run("--dir", RDBFileCreator.Dir, 37 | "--dbfilename", RDBFileCreator.Filename); err != nil { 38 | return err 39 | } 40 | 41 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 42 | if err != nil { 43 | return err 44 | } 45 | defer client.Close() 46 | 47 | commandTestCase := test_cases.SendCommandTestCase{ 48 | Command: "KEYS", 49 | Args: []string{"*"}, 50 | Assertion: resp_assertions.NewCommandAssertion(randomKey), 51 | ShouldSkipUnreadDataCheck: false, 52 | } 53 | 54 | return commandTestCase.Run(client, logger) 55 | } 56 | -------------------------------------------------------------------------------- /internal/test_rdb_read_multiple_keys.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 7 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | 11 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testRdbReadMultipleKeys(stageHarness *test_case_harness.TestCaseHarness) error { 16 | RDBFileCreator, err := NewRDBFileCreator() 17 | if err != nil { 18 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 19 | } 20 | 21 | keyCount := testerutils_random.RandomInt(3, 6) 22 | keys := testerutils_random.RandomWords(keyCount) 23 | values := testerutils_random.RandomWords(keyCount) 24 | 25 | keyValuePairs := make([]KeyValuePair, keyCount) 26 | for i := 0; i < keyCount; i++ { 27 | keyValuePairs[i] = KeyValuePair{key: keys[i], value: values[i]} 28 | } 29 | 30 | if err := RDBFileCreator.Write(keyValuePairs); err != nil { 31 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 32 | } 33 | 34 | logger := stageHarness.Logger 35 | logger.Infof("Created RDB file with %d keys: %v", keyCount, FormatKeys(keys)) 36 | if err := RDBFileCreator.PrintContentHexdump(logger); err != nil { 37 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 38 | } 39 | 40 | b := redis_executable.NewRedisExecutable(stageHarness) 41 | stageHarness.RegisterTeardownFunc(func() { RDBFileCreator.Cleanup() }) 42 | if err := b.Run("--dir", RDBFileCreator.Dir, 43 | "--dbfilename", RDBFileCreator.Filename); err != nil { 44 | return err 45 | } 46 | 47 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 48 | if err != nil { 49 | return err 50 | } 51 | defer client.Close() 52 | 53 | commandTestCase := test_cases.SendCommandTestCase{ 54 | Command: "KEYS", 55 | Args: []string{"*"}, 56 | Assertion: resp_assertions.NewUnorderedStringArrayAssertion(keys), 57 | ShouldSkipUnreadDataCheck: false, 58 | } 59 | 60 | return commandTestCase.Run(client, logger) 61 | } 62 | -------------------------------------------------------------------------------- /internal/test_rdb_read_multiple_string_values.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 7 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | 11 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testRdbReadMultipleStringValues(stageHarness *test_case_harness.TestCaseHarness) error { 16 | RDBFileCreator, err := NewRDBFileCreator() 17 | if err != nil { 18 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 19 | } 20 | 21 | keyCount := testerutils_random.RandomInt(3, 6) 22 | keys := testerutils_random.RandomWords(keyCount) 23 | values := testerutils_random.RandomWords(keyCount) 24 | 25 | keyValuePairs := make([]KeyValuePair, keyCount) 26 | for i := 0; i < keyCount; i++ { 27 | keyValuePairs[i] = KeyValuePair{key: keys[i], value: values[i]} 28 | } 29 | 30 | keyValueMap := make(map[string]string) 31 | for i := 0; i < keyCount; i++ { 32 | key := keys[i] 33 | value := values[i] 34 | keyValueMap[key] = value 35 | } 36 | 37 | if err := RDBFileCreator.Write(keyValuePairs); err != nil { 38 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 39 | } 40 | 41 | logger := stageHarness.Logger 42 | logger.Infof("Created RDB file with %d key-value pairs: %s", len(keys), FormatKeyValuePairs(keys, values)) 43 | if err := RDBFileCreator.PrintContentHexdump(logger); err != nil { 44 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 45 | } 46 | 47 | b := redis_executable.NewRedisExecutable(stageHarness) 48 | stageHarness.RegisterTeardownFunc(func() { RDBFileCreator.Cleanup() }) 49 | if err := b.Run("--dir", RDBFileCreator.Dir, 50 | "--dbfilename", RDBFileCreator.Filename); err != nil { 51 | return err 52 | } 53 | 54 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 55 | if err != nil { 56 | return err 57 | } 58 | defer client.Close() 59 | 60 | for i, key := range keys { 61 | expectedValue := keyValueMap[key] 62 | 63 | commandTestCase := test_cases.SendCommandTestCase{ 64 | Command: "GET", 65 | Args: []string{key}, 66 | Assertion: resp_assertions.NewStringAssertion(expectedValue), 67 | ShouldSkipUnreadDataCheck: i < len(keys)-1, 68 | } 69 | 70 | if err := commandTestCase.Run(client, logger); err != nil { 71 | return err 72 | } 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/test_rdb_read_string_value.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 7 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | 11 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testRdbReadStringValue(stageHarness *test_case_harness.TestCaseHarness) error { 16 | RDBFileCreator, err := NewRDBFileCreator() 17 | if err != nil { 18 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 19 | } 20 | 21 | randomKeyAndValue := testerutils_random.RandomWords(2) 22 | randomKey, randomValue := randomKeyAndValue[0], randomKeyAndValue[1] 23 | 24 | if err := RDBFileCreator.Write([]KeyValuePair{{key: randomKey, value: randomValue}}); err != nil { 25 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 26 | } 27 | 28 | logger := stageHarness.Logger 29 | logger.Infof("Created RDB file with a single key-value pair: {%q: %q}", randomKey, randomValue) 30 | if err := RDBFileCreator.PrintContentHexdump(logger); err != nil { 31 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 32 | } 33 | 34 | b := redis_executable.NewRedisExecutable(stageHarness) 35 | stageHarness.RegisterTeardownFunc(func() { RDBFileCreator.Cleanup() }) 36 | if err := b.Run("--dir", RDBFileCreator.Dir, 37 | "--dbfilename", RDBFileCreator.Filename); err != nil { 38 | return err 39 | } 40 | 41 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 42 | if err != nil { 43 | return err 44 | } 45 | defer client.Close() 46 | 47 | commandTestCase := test_cases.SendCommandTestCase{ 48 | Command: "GET", 49 | Args: []string{randomKey}, 50 | Assertion: resp_assertions.NewStringAssertion(randomValue), 51 | ShouldSkipUnreadDataCheck: false, 52 | } 53 | 54 | return commandTestCase.Run(client, logger) 55 | } 56 | -------------------------------------------------------------------------------- /internal/test_rdb_read_value_with_expiry.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 10 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 11 | 12 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 13 | "github.com/codecrafters-io/tester-utils/test_case_harness" 14 | ) 15 | 16 | func testRdbReadValueWithExpiry(stageHarness *test_case_harness.TestCaseHarness) error { 17 | RDBFileCreator, err := NewRDBFileCreator() 18 | if err != nil { 19 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 20 | } 21 | 22 | keyCount := testerutils_random.RandomInt(3, 6) 23 | keys := testerutils_random.RandomWords(keyCount) 24 | values := testerutils_random.RandomWords(keyCount) 25 | expiringKeyIndex := testerutils_random.RandomInt(0, keyCount-1) 26 | 27 | keyValueMap := make(map[string]string) 28 | keyValuePairs := make([]KeyValuePair, keyCount) 29 | for i := 0; i < keyCount; i++ { 30 | if expiringKeyIndex == i { 31 | keyValuePairs[i] = KeyValuePair{ 32 | key: keys[i], 33 | value: values[i], 34 | expiryTS: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(), 35 | } 36 | } else { 37 | keyValuePairs[i] = KeyValuePair{ 38 | key: keys[i], 39 | value: values[i], 40 | expiryTS: time.Date(2032, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(), 41 | } 42 | } 43 | key, value := keys[i], values[i] 44 | keyValueMap[key] = value 45 | } 46 | 47 | if err := RDBFileCreator.Write(keyValuePairs); err != nil { 48 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 49 | } 50 | 51 | logger := stageHarness.Logger 52 | logger.Infof("Created RDB file with %d key-value pairs: %s", len(keys), FormatKeyValuePairs(keys, values)) 53 | if err := RDBFileCreator.PrintContentHexdump(logger); err != nil { 54 | return fmt.Errorf("CodeCrafters Tester Error: %s", err) 55 | } 56 | 57 | b := redis_executable.NewRedisExecutable(stageHarness) 58 | stageHarness.RegisterTeardownFunc(func() { RDBFileCreator.Cleanup() }) 59 | if err := b.Run("--dir", RDBFileCreator.Dir, 60 | "--dbfilename", RDBFileCreator.Filename); err != nil { 61 | return err 62 | } 63 | 64 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 65 | if err != nil { 66 | return err 67 | } 68 | defer client.Close() 69 | 70 | for keyIndex, key := range keys { 71 | if keyIndex == expiringKeyIndex { 72 | commandTestCase := test_cases.SendCommandTestCase{ 73 | Command: "GET", 74 | Args: []string{key}, 75 | Assertion: resp_assertions.NewNilAssertion(), 76 | ShouldSkipUnreadDataCheck: false, 77 | } 78 | if err := commandTestCase.Run(client, logger); err != nil { 79 | return err 80 | } 81 | } else { 82 | commandTestCase := test_cases.SendCommandTestCase{ 83 | Command: "GET", 84 | Args: []string{key}, 85 | Assertion: resp_assertions.NewStringAssertion(keyValueMap[key]), 86 | ShouldSkipUnreadDataCheck: false, 87 | } 88 | if err := commandTestCase.Run(client, logger); err != nil { 89 | return err 90 | } 91 | } 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/test_repl_bind.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 7 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 8 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | func testReplBindToCustomPort(stageHarness *test_case_harness.TestCaseHarness) error { 13 | port := testerutils_random.RandomInt(6380, 6390) 14 | 15 | b := redis_executable.NewRedisExecutable(stageHarness) 16 | if err := b.Run("--port", strconv.Itoa(port)); err != nil { 17 | return err 18 | } 19 | 20 | logger := stageHarness.Logger 21 | 22 | bindTestCase := test_cases.BindTestCase{ 23 | Port: port, 24 | Retries: 15, 25 | } 26 | 27 | return bindTestCase.Run(b, logger) 28 | } 29 | -------------------------------------------------------------------------------- /internal/test_repl_id.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 10 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 11 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testReplReplicationID(stageHarness *test_case_harness.TestCaseHarness) error { 16 | deleteRDBfile() 17 | 18 | b := redis_executable.NewRedisExecutable(stageHarness) 19 | if err := b.Run(); err != nil { 20 | return err 21 | } 22 | 23 | logger := stageHarness.Logger 24 | 25 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 26 | if err != nil { 27 | logFriendlyError(logger, err) 28 | return err 29 | } 30 | defer client.Close() 31 | 32 | commandTestCase := &test_cases.SendCommandTestCase{ 33 | Command: "INFO", 34 | Args: []string{"replication"}, 35 | Assertion: resp_assertions.NewNoopAssertion(), 36 | ShouldSkipUnreadDataCheck: true, 37 | } 38 | 39 | if err := commandTestCase.Run(client, logger); err != nil { 40 | return err 41 | } 42 | 43 | responseValue := commandTestCase.ReceivedResponse 44 | 45 | if responseValue.Type != resp_value.BULK_STRING && responseValue.Type != resp_value.SIMPLE_STRING { 46 | return fmt.Errorf("Expected simple string or bulk string, got %s", responseValue.Type) 47 | } 48 | 49 | var patternMatchError error 50 | 51 | if regexp.MustCompile("master_replid:([a-zA-Z0-9]+)").Match([]byte(responseValue.String())) { 52 | logger.Successf("Found master_replid:xxx in response.") 53 | } else { 54 | patternMatchError = fmt.Errorf("Expected master_replid:xxx to be present in response. Got: %q", responseValue.String()) 55 | } 56 | 57 | if regexp.MustCompile("master_repl_offset:0").Match([]byte(responseValue.String())) { 58 | logger.Successf("Found master_reploffset:0 in response.") 59 | } else { 60 | patternMatchError = fmt.Errorf("Expected master_repl_offset:0 to be present in response. Got: %q", responseValue.String()) 61 | } 62 | 63 | return patternMatchError 64 | } 65 | -------------------------------------------------------------------------------- /internal/test_repl_info.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 10 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 11 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testReplInfo(stageHarness *test_case_harness.TestCaseHarness) error { 16 | b := redis_executable.NewRedisExecutable(stageHarness) 17 | if err := b.Run(); err != nil { 18 | return err 19 | } 20 | 21 | logger := stageHarness.Logger 22 | 23 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 24 | if err != nil { 25 | logFriendlyError(logger, err) 26 | return err 27 | } 28 | defer client.Close() 29 | 30 | commandTestCase := test_cases.SendCommandTestCase{ 31 | Command: "INFO", 32 | Args: []string{"replication"}, 33 | Assertion: resp_assertions.NewNoopAssertion(), 34 | ShouldSkipUnreadDataCheck: true, 35 | } 36 | 37 | if err := commandTestCase.Run(client, logger); err != nil { 38 | return err 39 | } 40 | 41 | responseValue := commandTestCase.ReceivedResponse 42 | 43 | if responseValue.Type != resp_value.BULK_STRING && responseValue.Type != resp_value.SIMPLE_STRING { 44 | return fmt.Errorf("Expected simple string or bulk string, got %s", responseValue.Type) 45 | } 46 | 47 | var patternMatchError error 48 | 49 | if !regexp.MustCompile("role:").Match([]byte(responseValue.String())) { 50 | patternMatchError = fmt.Errorf("Expected role to be present in response. Got: %q", responseValue.String()) 51 | } 52 | 53 | if regexp.MustCompile("role:master").Match([]byte(responseValue.String())) { 54 | logger.Successf("Found role:master in response.") 55 | } else { 56 | patternMatchError = fmt.Errorf("Expected role to be master in response. Got: %q", responseValue.String()) 57 | } 58 | 59 | return patternMatchError 60 | } 61 | -------------------------------------------------------------------------------- /internal/test_repl_info_replica.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "regexp" 7 | 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 10 | 11 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 12 | 13 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 14 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 15 | loggerutils "github.com/codecrafters-io/tester-utils/logger" 16 | "github.com/codecrafters-io/tester-utils/test_case_harness" 17 | ) 18 | 19 | func testReplInfoReplica(stageHarness *test_case_harness.TestCaseHarness) error { 20 | logger := stageHarness.Logger 21 | 22 | listener, err := net.Listen("tcp", ":6379") 23 | if err != nil { 24 | logFriendlyBindError(logger, err) 25 | return fmt.Errorf("Error starting TCP server: %v", err) 26 | } 27 | defer listener.Close() 28 | 29 | logger.Infof("Master is running on port 6379") 30 | 31 | replica := redis_executable.NewRedisExecutable(stageHarness) 32 | if err := replica.Run("--port", "6380", 33 | "--replicaof", "localhost 6379"); err != nil { 34 | return err 35 | } 36 | 37 | go func(l net.Listener) error { 38 | // Connecting to master in this stage is optional. 39 | conn, err := listener.Accept() 40 | if err != nil { 41 | logger.Debugf("Error accepting: %s", err.Error()) 42 | return err 43 | } 44 | defer conn.Close() 45 | 46 | quietLogger := loggerutils.GetQuietLogger("") 47 | master, err := instrumented_resp_connection.NewFromConn(quietLogger, conn, "master") 48 | if err != nil { 49 | logFriendlyError(quietLogger, err) 50 | return err 51 | } 52 | receiveReplicationHandshakeTestCase := test_cases.ReceiveReplicationHandshakeTestCase{} 53 | 54 | _ = receiveReplicationHandshakeTestCase.RunAll(master, quietLogger) 55 | 56 | return nil 57 | }(listener) 58 | 59 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6380", "client") 60 | if err != nil { 61 | logFriendlyError(logger, err) 62 | return err 63 | } 64 | defer client.Close() 65 | 66 | commandTestCase := test_cases.SendCommandTestCase{ 67 | Command: "INFO", 68 | Args: []string{"replication"}, 69 | Assertion: resp_assertions.NewNoopAssertion(), 70 | ShouldSkipUnreadDataCheck: true, 71 | } 72 | 73 | if err := commandTestCase.Run(client, logger); err != nil { 74 | return err 75 | } 76 | 77 | responseValue := commandTestCase.ReceivedResponse 78 | 79 | if responseValue.Type != resp_value.BULK_STRING && responseValue.Type != resp_value.SIMPLE_STRING { 80 | return fmt.Errorf("Expected simple string or bulk string, got %s", responseValue.Type) 81 | } 82 | 83 | var patternMatchError error 84 | 85 | if !regexp.MustCompile("role:").Match([]byte(responseValue.String())) { 86 | patternMatchError = fmt.Errorf("Expected role to be present in response. Got: %q", responseValue.String()) 87 | } 88 | 89 | if regexp.MustCompile("role:slave").Match([]byte(responseValue.String())) { 90 | logger.Successf("Found role:slave in response.") 91 | } else { 92 | patternMatchError = fmt.Errorf("Expected role to be slave in response. Got: %q", responseValue.String()) 93 | } 94 | 95 | return patternMatchError 96 | } 97 | -------------------------------------------------------------------------------- /internal/test_repl_master_cmd_prop.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | func testReplMasterCmdProp(stageHarness *test_case_harness.TestCaseHarness) error { 13 | deleteRDBfile() 14 | 15 | logger := stageHarness.Logger 16 | defer logger.ResetSecondaryPrefix() 17 | 18 | // Run the user's code as a master 19 | masterBinary := redis_executable.NewRedisExecutable(stageHarness) 20 | if err := masterBinary.Run("--port", "6379"); err != nil { 21 | return err 22 | } 23 | 24 | logger.UpdateSecondaryPrefix("handshake") 25 | 26 | // We use one client to send commands to the master 27 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 28 | if err != nil { 29 | logFriendlyError(logger, err) 30 | return err 31 | } 32 | defer client.Close() 33 | 34 | // We use another client to assert whether sent commands are replicated from the master (user's code) 35 | replicaClient, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "replica") 36 | if err != nil { 37 | logFriendlyError(logger, err) 38 | return err 39 | } 40 | defer replicaClient.Close() 41 | 42 | sendHandshakeTestCase := test_cases.SendReplicationHandshakeTestCase{} 43 | if err := sendHandshakeTestCase.RunAll(replicaClient, logger, 6380); err != nil { 44 | return err 45 | } 46 | 47 | logger.UpdateSecondaryPrefix("test") 48 | 49 | kvMap := map[int][]string{ 50 | 1: {"foo", "123"}, 51 | 2: {"bar", "456"}, 52 | 3: {"baz", "789"}, 53 | } 54 | 55 | // We send SET commands to the master in order (user's code) 56 | for i := 1; i <= len(kvMap); i++ { 57 | key, value := kvMap[i][0], kvMap[i][1] 58 | 59 | setCommandTestCase := test_cases.SendCommandTestCase{ 60 | Command: "SET", 61 | Args: []string{key, value}, 62 | Assertion: resp_assertions.NewStringAssertion("OK"), 63 | } 64 | 65 | if err := setCommandTestCase.Run(client, logger); err != nil { 66 | return err 67 | } 68 | } 69 | 70 | logger.Successf("Sent 3 SET commands to master successfully.") 71 | 72 | // We then assert that as a replica we receive the SET commands in order 73 | for i := 1; i <= len(kvMap); i++ { 74 | logger.Infof("replica: Expecting \"SET %s %s\" to be propagated", kvMap[i][0], kvMap[i][1]) 75 | 76 | receiveCommandTestCase := &test_cases.ReceiveValueTestCase{ 77 | Assertion: resp_assertions.NewCommandAssertion("SET", kvMap[i][0], kvMap[i][1]), 78 | ShouldSkipUnreadDataCheck: i < len(kvMap), // Except in the last case, we're expecting more SET commands to be present 79 | } 80 | 81 | if err := receiveCommandTestCase.Run(replicaClient, logger); err != nil { 82 | // Redis sends a SELECT command, but we don't expect it from users. 83 | // If the first command is a SELECT command, we'll re-run the test case to test the next command instead 84 | if i == 1 && IsSelectCommand(receiveCommandTestCase.ActualValue) { 85 | if err := receiveCommandTestCase.Run(replicaClient, logger); err != nil { 86 | return err 87 | } 88 | } else { 89 | return err 90 | } 91 | } 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/test_repl_master_psync.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 7 | 8 | "github.com/codecrafters-io/tester-utils/test_case_harness" 9 | ) 10 | 11 | func testReplMasterPsync(stageHarness *test_case_harness.TestCaseHarness) error { 12 | master := redis_executable.NewRedisExecutable(stageHarness) 13 | if err := master.Run("--port", "6379"); err != nil { 14 | return err 15 | } 16 | 17 | logger := stageHarness.Logger 18 | 19 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 20 | if err != nil { 21 | logFriendlyError(logger, err) 22 | return err 23 | } 24 | defer client.Close() 25 | 26 | sendHandshakeTestCase := test_cases.SendReplicationHandshakeTestCase{} 27 | 28 | if err := sendHandshakeTestCase.RunPingStep(client, logger); err != nil { 29 | return err 30 | } 31 | 32 | if err := sendHandshakeTestCase.RunReplconfStep(client, logger, 6380); err != nil { 33 | return err 34 | } 35 | 36 | return sendHandshakeTestCase.RunPsyncStep(client, logger) 37 | } 38 | -------------------------------------------------------------------------------- /internal/test_repl_master_psync_rdb.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 7 | 8 | "github.com/codecrafters-io/tester-utils/test_case_harness" 9 | ) 10 | 11 | func testReplMasterPsyncRdb(stageHarness *test_case_harness.TestCaseHarness) error { 12 | deleteRDBfile() 13 | master := redis_executable.NewRedisExecutable(stageHarness) 14 | if err := master.Run("--port", "6379"); err != nil { 15 | return err 16 | } 17 | 18 | logger := stageHarness.Logger 19 | 20 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 21 | if err != nil { 22 | logFriendlyError(logger, err) 23 | return err 24 | } 25 | defer client.Close() 26 | 27 | sendHandshakeTestCase := test_cases.SendReplicationHandshakeTestCase{} 28 | 29 | return sendHandshakeTestCase.RunAll(client, logger, 6380) 30 | } 31 | -------------------------------------------------------------------------------- /internal/test_repl_master_replconf.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 7 | 8 | "github.com/codecrafters-io/tester-utils/test_case_harness" 9 | ) 10 | 11 | func testReplMasterReplconf(stageHarness *test_case_harness.TestCaseHarness) error { 12 | master := redis_executable.NewRedisExecutable(stageHarness) 13 | if err := master.Run("--port", "6379"); err != nil { 14 | return err 15 | } 16 | 17 | logger := stageHarness.Logger 18 | 19 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 20 | if err != nil { 21 | logFriendlyError(logger, err) 22 | return err 23 | } 24 | defer client.Close() 25 | 26 | sendHandshakeTestCase := test_cases.SendReplicationHandshakeTestCase{} 27 | 28 | if err := sendHandshakeTestCase.RunPingStep(client, logger); err != nil { 29 | return err 30 | } 31 | 32 | return sendHandshakeTestCase.RunReplconfStep(client, logger, 6380) 33 | } 34 | -------------------------------------------------------------------------------- /internal/test_repl_multiple_replicas.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 7 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 8 | 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | func testReplMultipleReplicas(stageHarness *test_case_harness.TestCaseHarness) error { 13 | deleteRDBfile() 14 | 15 | logger := stageHarness.Logger 16 | defer logger.ResetSecondaryPrefix() 17 | 18 | master := redis_executable.NewRedisExecutable(stageHarness) 19 | if err := master.Run("--port", "6379"); err != nil { 20 | return err 21 | } 22 | 23 | logger.UpdateSecondaryPrefix("handshake") 24 | 25 | // We use one client to send commands to the master 26 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 27 | if err != nil { 28 | logFriendlyError(logger, err) 29 | return err 30 | } 31 | defer client.Close() 32 | 33 | replicaCount := 3 34 | // We use multiple replicas to assert whether sent commands are replicated from the master (user's code) 35 | replicas, err := SpawnReplicas(replicaCount, stageHarness, logger, "localhost:6379") 36 | if err != nil { 37 | return err 38 | } 39 | for _, replica := range replicas { 40 | defer replica.Close() 41 | } 42 | 43 | logger.UpdateSecondaryPrefix("test") 44 | 45 | kvMap := map[int][]string{ 46 | 1: {"foo", "123"}, 47 | 2: {"bar", "456"}, 48 | 3: {"baz", "789"}, 49 | } 50 | 51 | // We send SET commands to the master in order (user's code) 52 | for i := 1; i <= len(kvMap); i++ { 53 | key, value := kvMap[i][0], kvMap[i][1] 54 | setCommandTestCase := test_cases.SendCommandTestCase{ 55 | Command: "SET", 56 | Args: []string{key, value}, 57 | Assertion: resp_assertions.NewStringAssertion("OK"), 58 | } 59 | if err := setCommandTestCase.Run(client, logger); err != nil { 60 | return err 61 | } 62 | } 63 | 64 | // We then assert that across all the replicas we receive the SET commands in order 65 | for j, replica := range replicas { 66 | logger.Infof("Testing Replica: %d/%d", j+1, replicaCount) 67 | for i := 1; i <= len(kvMap); i++ { 68 | logger.Infof("replica-%d: Expecting \"SET %s %s\" to be propagated", j+1, kvMap[i][0], kvMap[i][1]) 69 | 70 | receiveValueTestCase := &test_cases.ReceiveValueTestCase{ 71 | Assertion: resp_assertions.NewCommandAssertion("SET", kvMap[i][0], kvMap[i][1]), 72 | ShouldSkipUnreadDataCheck: i < len(kvMap), // Except in the last case, we're expecting more SET commands to be present 73 | } 74 | 75 | if err := receiveValueTestCase.Run(replica, logger); err != nil { 76 | // Redis sends a SELECT command, but we don't expect it from users. 77 | // If the first command is a SELECT command, we'll re-run the test case to test the next command instead 78 | if i == 1 && IsSelectCommand(receiveValueTestCase.ActualValue) { 79 | if err := receiveValueTestCase.Run(replica, logger); err != nil { 80 | return err 81 | } 82 | } else { 83 | return err 84 | } 85 | } 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /internal/test_repl_replica_cmd_processing.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 10 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 11 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 12 | 13 | "github.com/codecrafters-io/tester-utils/test_case_harness" 14 | ) 15 | 16 | func testReplCmdProcessing(stageHarness *test_case_harness.TestCaseHarness) error { 17 | deleteRDBfile() 18 | 19 | logger := stageHarness.Logger 20 | defer logger.ResetSecondaryPrefix() 21 | 22 | listener, err := net.Listen("tcp", ":6379") 23 | if err != nil { 24 | logFriendlyBindError(logger, err) 25 | return fmt.Errorf("Error starting TCP server: %v", err) 26 | } 27 | defer listener.Close() 28 | 29 | logger.Infof("Master is running on port 6379") 30 | 31 | replica := redis_executable.NewRedisExecutable(stageHarness) 32 | if err := replica.Run("--port", "6380", 33 | "--replicaof", "localhost 6379"); err != nil { 34 | return err 35 | } 36 | 37 | logger.UpdateSecondaryPrefix("handshake") 38 | 39 | conn, err := listener.Accept() 40 | if err != nil { 41 | fmt.Println("Error accepting: ", err.Error()) 42 | return err 43 | } 44 | defer conn.Close() 45 | 46 | master, err := instrumented_resp_connection.NewFromConn(logger, conn, "master") 47 | if err != nil { 48 | logFriendlyError(logger, err) 49 | return err 50 | } 51 | 52 | receiveReplicationHandshakeTestCase := test_cases.ReceiveReplicationHandshakeTestCase{} 53 | 54 | if err := receiveReplicationHandshakeTestCase.RunAll(master, logger); err != nil { 55 | return err 56 | } 57 | 58 | logger.UpdateSecondaryPrefix("propagation") 59 | 60 | replicaClient, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6380", "client") 61 | if err != nil { 62 | logFriendlyError(logger, err) 63 | return err 64 | } 65 | defer replicaClient.Close() 66 | 67 | kvMap := map[int][]string{ 68 | 1: {"foo", "123"}, 69 | 2: {"bar", "456"}, 70 | 3: {"baz", "789"}, 71 | } 72 | 73 | for i := 1; i <= len(kvMap); i++ { // We need order of commands preserved 74 | key, value := kvMap[i][0], kvMap[i][1] 75 | // We are propagating commands to Replica as Master, don't expect any response back. 76 | if err := master.SendCommand("SET", key, value); err != nil { 77 | return err 78 | } 79 | } 80 | 81 | logger.UpdateSecondaryPrefix("test") 82 | 83 | for i := 1; i <= len(kvMap); i++ { 84 | key, value := kvMap[i][0], kvMap[i][1] 85 | logger.Infof("Getting key %s", key) 86 | getCommandTestCase := test_cases.SendCommandTestCase{ 87 | Command: "GET", 88 | Args: []string{key}, 89 | Assertion: resp_assertions.NewStringAssertion(value), 90 | Retries: 5, 91 | ShouldRetryFunc: func(value resp_value.Value) bool { 92 | return resp_assertions.NewNilAssertion().Run(value) == nil 93 | }, 94 | } 95 | 96 | if err := getCommandTestCase.Run(replicaClient, logger); err != nil { 97 | return err 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/test_repl_replica_getack_nonzero.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | 11 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testReplGetaAckNonZero(stageHarness *test_case_harness.TestCaseHarness) error { 16 | deleteRDBfile() 17 | 18 | logger := stageHarness.Logger 19 | defer logger.ResetSecondaryPrefix() 20 | 21 | listener, err := net.Listen("tcp", ":6379") 22 | if err != nil { 23 | logFriendlyBindError(logger, err) 24 | return fmt.Errorf("Error starting TCP server: %v", err) 25 | } 26 | defer listener.Close() 27 | 28 | logger.Infof("Master is running on port 6379") 29 | 30 | replica := redis_executable.NewRedisExecutable(stageHarness) 31 | if err := replica.Run("--port", "6380", 32 | "--replicaof", "localhost 6379"); err != nil { 33 | return err 34 | } 35 | 36 | conn, err := listener.Accept() 37 | if err != nil { 38 | fmt.Println("Error accepting: ", err.Error()) 39 | return err 40 | } 41 | defer conn.Close() 42 | 43 | master, err := instrumented_resp_connection.NewFromConn(logger, conn, "master") 44 | if err != nil { 45 | logFriendlyError(logger, err) 46 | return err 47 | } 48 | 49 | logger.UpdateSecondaryPrefix("handshake") 50 | 51 | receiveReplicationHandshakeTestCase := test_cases.ReceiveReplicationHandshakeTestCase{} 52 | 53 | if err := receiveReplicationHandshakeTestCase.RunAll(master, logger); err != nil { 54 | return err 55 | } 56 | 57 | // The bytes received and sent during the handshake don't count towards offset. 58 | // After finishing the handshake we reset the counters. 59 | master.ResetByteCounters() 60 | logger.UpdateSecondaryPrefix("test") 61 | 62 | getAckTestCase := test_cases.GetAckTestCase{} 63 | if err := getAckTestCase.Run(master, logger, master.SentBytes); err != nil { 64 | return err 65 | } 66 | 67 | logger.UpdateSecondaryPrefix("propagation") 68 | if err := master.SendCommand("PING"); err != nil { 69 | return err 70 | } 71 | 72 | logger.UpdateSecondaryPrefix("test") 73 | if err := getAckTestCase.Run(master, logger, master.SentBytes); err != nil { 74 | return err 75 | } 76 | 77 | logger.UpdateSecondaryPrefix("propagation") 78 | key := testerutils_random.RandomWord() 79 | value := testerutils_random.RandomWord() 80 | if err := master.SendCommand("SET", []string{key, value}...); err != nil { 81 | return err 82 | } 83 | 84 | key = testerutils_random.RandomWord() 85 | value = testerutils_random.RandomWord() 86 | if err := master.SendCommand("SET", []string{key, value}...); err != nil { 87 | return err 88 | } 89 | 90 | logger.UpdateSecondaryPrefix("test") 91 | return getAckTestCase.Run(master, logger, master.SentBytes) 92 | } 93 | -------------------------------------------------------------------------------- /internal/test_repl_replica_getack_zero.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | 11 | "github.com/codecrafters-io/tester-utils/test_case_harness" 12 | ) 13 | 14 | func testReplGetaAckZero(stageHarness *test_case_harness.TestCaseHarness) error { 15 | deleteRDBfile() 16 | 17 | logger := stageHarness.Logger 18 | defer logger.ResetSecondaryPrefix() 19 | 20 | listener, err := net.Listen("tcp", ":6379") 21 | if err != nil { 22 | logFriendlyBindError(logger, err) 23 | return fmt.Errorf("Error starting TCP server: %v", err) 24 | } 25 | defer listener.Close() 26 | 27 | logger.Infof("Master is running on port 6379") 28 | 29 | replica := redis_executable.NewRedisExecutable(stageHarness) 30 | if err := replica.Run("--port", "6380", 31 | "--replicaof", "localhost 6379"); err != nil { 32 | return err 33 | } 34 | 35 | conn, err := listener.Accept() 36 | if err != nil { 37 | fmt.Println("Error accepting: ", err.Error()) 38 | return err 39 | } 40 | defer conn.Close() 41 | 42 | master, err := instrumented_resp_connection.NewFromConn(logger, conn, "master") 43 | if err != nil { 44 | logFriendlyError(logger, err) 45 | return err 46 | } 47 | 48 | logger.UpdateSecondaryPrefix("handshake") 49 | 50 | receiveReplicationHandshakeTestCase := test_cases.ReceiveReplicationHandshakeTestCase{} 51 | 52 | if err := receiveReplicationHandshakeTestCase.RunAll(master, logger); err != nil { 53 | return err 54 | } 55 | 56 | logger.UpdateSecondaryPrefix("test") 57 | 58 | getAckTestCase := test_cases.GetAckTestCase{} 59 | 60 | return getAckTestCase.Run(master, logger, 0) 61 | } 62 | -------------------------------------------------------------------------------- /internal/test_repl_replica_ping.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 8 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 9 | 10 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 11 | "github.com/codecrafters-io/tester-utils/test_case_harness" 12 | ) 13 | 14 | func testReplReplicaSendsPing(stageHarness *test_case_harness.TestCaseHarness) error { 15 | logger := stageHarness.Logger 16 | 17 | listener, err := net.Listen("tcp", ":6379") 18 | if err != nil { 19 | logFriendlyBindError(logger, err) 20 | return fmt.Errorf("Error starting TCP server: %v", err) 21 | } 22 | defer listener.Close() 23 | 24 | logger.Infof("Master is running on port 6379.") 25 | 26 | replica := redis_executable.NewRedisExecutable(stageHarness) 27 | if err := replica.Run("--port", "6380", 28 | "--replicaof", "localhost 6379"); err != nil { 29 | return err 30 | } 31 | 32 | conn, err := listener.Accept() 33 | if err != nil { 34 | fmt.Println("Error accepting: ", err.Error()) 35 | return err 36 | } 37 | defer conn.Close() 38 | 39 | master, err := instrumented_resp_connection.NewFromConn(logger, conn, "master") 40 | if err != nil { 41 | logFriendlyError(logger, err) 42 | return err 43 | } 44 | 45 | receiveReplicationHandshakeTestCase := test_cases.ReceiveReplicationHandshakeTestCase{} 46 | 47 | return receiveReplicationHandshakeTestCase.RunPingStep(master, logger) 48 | } 49 | -------------------------------------------------------------------------------- /internal/test_repl_replica_psync.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | 11 | "github.com/codecrafters-io/tester-utils/test_case_harness" 12 | ) 13 | 14 | func testReplReplicaSendsPsync(stageHarness *test_case_harness.TestCaseHarness) error { 15 | deleteRDBfile() 16 | 17 | logger := stageHarness.Logger 18 | 19 | listener, err := net.Listen("tcp", ":6379") 20 | if err != nil { 21 | logFriendlyBindError(logger, err) 22 | return fmt.Errorf("Error starting TCP server: %v", err) 23 | } 24 | defer listener.Close() 25 | 26 | logger.Infof("Master is running on port 6379") 27 | 28 | replica := redis_executable.NewRedisExecutable(stageHarness) 29 | if err := replica.Run("--port", "6380", 30 | "--replicaof", "localhost 6379"); err != nil { 31 | return err 32 | } 33 | 34 | conn, err := listener.Accept() 35 | if err != nil { 36 | fmt.Println("Error accepting: ", err.Error()) 37 | return err 38 | } 39 | defer conn.Close() 40 | 41 | master, err := instrumented_resp_connection.NewFromConn(logger, conn, "master") 42 | if err != nil { 43 | logFriendlyError(logger, err) 44 | return err 45 | } 46 | 47 | receiveReplicationHandshakeTestCase := test_cases.ReceiveReplicationHandshakeTestCase{} 48 | 49 | if err := receiveReplicationHandshakeTestCase.RunPingStep(master, logger); err != nil { 50 | return err 51 | } 52 | 53 | if err := receiveReplicationHandshakeTestCase.RunReplconfStep1(master, logger); err != nil { 54 | return err 55 | } 56 | 57 | if err := receiveReplicationHandshakeTestCase.RunReplconfStep2(master, logger); err != nil { 58 | return err 59 | } 60 | 61 | return receiveReplicationHandshakeTestCase.RunPsyncStep(master, logger) 62 | } 63 | -------------------------------------------------------------------------------- /internal/test_repl_replica_replconf.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | 11 | "github.com/codecrafters-io/tester-utils/test_case_harness" 12 | ) 13 | 14 | func testReplReplicaSendsReplconf(stageHarness *test_case_harness.TestCaseHarness) error { 15 | logger := stageHarness.Logger 16 | 17 | listener, err := net.Listen("tcp", ":6379") 18 | if err != nil { 19 | logFriendlyBindError(logger, err) 20 | return fmt.Errorf("Error starting TCP server: %v", err) 21 | } 22 | defer listener.Close() 23 | 24 | logger.Infof("Master is running on port 6379") 25 | 26 | replica := redis_executable.NewRedisExecutable(stageHarness) 27 | if err := replica.Run("--port", "6380", 28 | "--replicaof", "localhost 6379"); err != nil { 29 | return err 30 | } 31 | 32 | conn, err := listener.Accept() 33 | if err != nil { 34 | fmt.Println("Error accepting: ", err.Error()) 35 | return err 36 | } 37 | defer conn.Close() 38 | 39 | master, err := instrumented_resp_connection.NewFromConn(logger, conn, "master") 40 | if err != nil { 41 | logFriendlyError(logger, err) 42 | return err 43 | } 44 | 45 | receiveReplicationHandshakeTestCase := test_cases.ReceiveReplicationHandshakeTestCase{} 46 | 47 | if err := receiveReplicationHandshakeTestCase.RunPingStep(master, logger); err != nil { 48 | return err 49 | } 50 | 51 | if err := receiveReplicationHandshakeTestCase.RunReplconfStep1(master, logger); err != nil { 52 | return err 53 | } 54 | 55 | return receiveReplicationHandshakeTestCase.RunReplconfStep2(master, logger) 56 | } 57 | -------------------------------------------------------------------------------- /internal/test_repl_wait_zero_offset.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 7 | 8 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | func testWaitZeroOffset(stageHarness *test_case_harness.TestCaseHarness) error { 13 | deleteRDBfile() 14 | 15 | master := redis_executable.NewRedisExecutable(stageHarness) 16 | if err := master.Run("--port", "6379"); err != nil { 17 | return err 18 | } 19 | 20 | logger := stageHarness.Logger 21 | defer logger.ResetSecondaryPrefix() 22 | 23 | replicaCount := testerutils_random.RandomInt(3, 9) 24 | logger.Infof("Proceeding to create %v replicas.", replicaCount) 25 | 26 | replicas, err := SpawnReplicas(replicaCount, stageHarness, logger, "localhost:6379") 27 | if err != nil { 28 | return err 29 | } 30 | for _, replica := range replicas { 31 | defer replica.Close() 32 | } 33 | 34 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 35 | if err != nil { 36 | logFriendlyError(logger, err) 37 | return err 38 | } 39 | defer client.Close() 40 | 41 | logger.UpdateSecondaryPrefix("test") 42 | 43 | diff := ((replicaCount + 3) - 3) / 3 44 | safeDiff := max(1, diff) // If diff is 0, it will get stuck in an infinite loop 45 | for actual := 3; actual < replicaCount+3; actual += safeDiff { 46 | waitTestCase := test_cases.WaitTestCase{ 47 | Replicas: actual, 48 | TimeoutInMilliseconds: 500, 49 | ExpectedMessage: replicaCount, 50 | } 51 | if err := waitTestCase.Run(client, logger); err != nil { 52 | return err 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/test_repl_wait_zero_replicas.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 7 | 8 | "github.com/codecrafters-io/tester-utils/test_case_harness" 9 | ) 10 | 11 | func testWaitZeroReplicas(stageHarness *test_case_harness.TestCaseHarness) error { 12 | deleteRDBfile() 13 | 14 | master := redis_executable.NewRedisExecutable(stageHarness) 15 | if err := master.Run("--port", "6379"); err != nil { 16 | return err 17 | } 18 | 19 | logger := stageHarness.Logger 20 | 21 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 22 | if err != nil { 23 | logFriendlyError(logger, err) 24 | return err 25 | } 26 | defer client.Close() 27 | 28 | waitTestCase := test_cases.WaitTestCase{ 29 | Replicas: 0, 30 | TimeoutInMilliseconds: 60000, 31 | ExpectedMessage: 0, 32 | } 33 | 34 | return waitTestCase.Run(client, logger) 35 | } 36 | -------------------------------------------------------------------------------- /internal/test_streams_type.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 7 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | 11 | "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testStreamsType(stageHarness *test_case_harness.TestCaseHarness) error { 16 | b := redis_executable.NewRedisExecutable(stageHarness) 17 | if err := b.Run(); err != nil { 18 | return err 19 | } 20 | 21 | logger := stageHarness.Logger 22 | 23 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 24 | if err != nil { 25 | logFriendlyError(logger, err) 26 | return err 27 | } 28 | defer client.Close() 29 | 30 | randomKey := random.RandomWord() 31 | randomValue := random.RandomWord() 32 | 33 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 34 | Commands: [][]string{ 35 | {"SET", randomKey, randomValue}, 36 | {"TYPE", randomKey}, 37 | {"TYPE", fmt.Sprintf("missing_key_%s", randomValue)}, 38 | }, 39 | Assertions: []resp_assertions.RESPAssertion{ 40 | resp_assertions.NewStringAssertion("OK"), 41 | resp_assertions.NewStringAssertion("string"), 42 | resp_assertions.NewStringAssertion("none"), 43 | }, 44 | } 45 | 46 | return multiCommandTestCase.RunAll(client, logger) 47 | } 48 | -------------------------------------------------------------------------------- /internal/test_streams_xadd.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 10 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 11 | 12 | "github.com/codecrafters-io/tester-utils/logger" 13 | "github.com/codecrafters-io/tester-utils/random" 14 | "github.com/codecrafters-io/tester-utils/test_case_harness" 15 | "github.com/go-redis/redis" 16 | ) 17 | 18 | type XADDTest struct { 19 | streamKey string 20 | id string 21 | values map[string]interface{} 22 | expectedResponse string 23 | expectedError string 24 | } 25 | 26 | func (t *XADDTest) Run(client *redis.Client, logger *logger.Logger) error { 27 | var values []string 28 | 29 | for key, value := range t.values { 30 | values = append(values, key, fmt.Sprintf("%v", value)) 31 | } 32 | 33 | logger.Infof("$ redis-cli xadd %v %v %v", t.streamKey, t.id, strings.Join(values, " ")) 34 | 35 | resp, err := client.XAdd(&redis.XAddArgs{ 36 | Stream: t.streamKey, 37 | ID: t.id, 38 | Values: t.values, 39 | }).Result() 40 | 41 | if err != nil && t.expectedError == "" { 42 | logFriendlyError(logger, err) 43 | return err 44 | } 45 | 46 | if err != nil && t.expectedError != "" { 47 | if err.Error() != t.expectedError { 48 | return fmt.Errorf("Expected %q, got %q", t.expectedError, err.Error()) 49 | } 50 | 51 | logger.Successf("Received error: %q", err.Error()) 52 | return nil 53 | } 54 | 55 | if resp != t.expectedResponse && t.expectedError != "" { 56 | logger.Infof("Received response: %q", resp) 57 | return fmt.Errorf("Expected an error as the response, got %q", resp) 58 | } else if resp != t.expectedResponse { 59 | logger.Infof("Received response: %q", resp) 60 | return fmt.Errorf("Expected %q, got %q", t.expectedResponse, resp) 61 | } else { 62 | logger.Successf("Received response: %q", resp) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func testStreamsXadd(stageHarness *test_case_harness.TestCaseHarness) error { 69 | b := redis_executable.NewRedisExecutable(stageHarness) 70 | if err := b.Run(); err != nil { 71 | return err 72 | } 73 | 74 | logger := stageHarness.Logger 75 | 76 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 77 | if err != nil { 78 | logFriendlyError(logger, err) 79 | return err 80 | } 81 | defer client.Close() 82 | 83 | randomKey := random.RandomWord() 84 | 85 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 86 | Commands: [][]string{ 87 | {"XADD", randomKey, "0-1", "foo", "bar"}, 88 | {"TYPE", randomKey}, 89 | }, 90 | Assertions: []resp_assertions.RESPAssertion{ 91 | resp_assertions.NewStringAssertion("0-1"), 92 | resp_assertions.NewStringAssertion("stream"), 93 | }, 94 | } 95 | 96 | return multiCommandTestCase.RunAll(client, logger) 97 | } 98 | -------------------------------------------------------------------------------- /internal/test_streams_xadd_full_autoid.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 10 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 11 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 12 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 13 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 14 | 15 | "github.com/codecrafters-io/tester-utils/random" 16 | "github.com/codecrafters-io/tester-utils/test_case_harness" 17 | ) 18 | 19 | func testStreamsXaddFullAutoid(stageHarness *test_case_harness.TestCaseHarness) error { 20 | b := redis_executable.NewRedisExecutable(stageHarness) 21 | if err := b.Run(); err != nil { 22 | return err 23 | } 24 | 25 | logger := stageHarness.Logger 26 | 27 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 28 | if err != nil { 29 | logFriendlyError(logger, err) 30 | return err 31 | } 32 | defer client.Close() 33 | 34 | randomKey := random.RandomWord() 35 | 36 | commandTestCase := &test_cases.SendCommandTestCase{ 37 | Command: "XADD", 38 | Args: []string{randomKey, "*", "foo", "bar"}, 39 | Assertion: resp_assertions.NewNoopAssertion(), 40 | ShouldSkipUnreadDataCheck: true, 41 | } 42 | 43 | if err := commandTestCase.Run(client, logger); err != nil { 44 | return err 45 | } 46 | 47 | responseValue := commandTestCase.ReceivedResponse 48 | 49 | if responseValue.Type != resp_value.BULK_STRING { 50 | return fmt.Errorf("Expected bulk string, got %s", responseValue.Type) 51 | } 52 | 53 | parts := strings.Split(responseValue.String(), "-") 54 | 55 | if len(parts) != 2 { 56 | return fmt.Errorf("Expected a string in the form \"-\", got %q", responseValue) 57 | } 58 | 59 | timeStr, sequenceNumber := parts[0], parts[1] 60 | timeInt64, _ := strconv.ParseInt(timeStr, 10, 64) 61 | now := time.Now().Unix() * 1000 62 | oneSecondAgo := now - 1000 63 | oneSecondLater := now + 1000 64 | 65 | if len(timeStr) != 13 { 66 | return fmt.Errorf("Expected the first part of the ID to be a unix timestamp (%d characters), got %d characters", len(strconv.FormatInt(now, 10)), len(timeStr)) 67 | } else if timeInt64 <= oneSecondAgo || timeInt64 >= oneSecondLater { 68 | return fmt.Errorf("Expected the first part of the ID to be a valid unix timestamp, got %q", timeStr) 69 | } else { 70 | logger.Successf("The first part of the ID is a valid unix milliseconds timestamp") 71 | } 72 | 73 | if sequenceNumber != "0" { 74 | return fmt.Errorf("Expected the second part of the ID to be a sequence number with a value of \"0\", got %q", sequenceNumber) 75 | } else { 76 | logger.Successf("The second part of the ID is a valid sequence number") 77 | } 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/test_streams_xadd_partial_autoid.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 7 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 8 | "github.com/codecrafters-io/tester-utils/random" 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | func testStreamsXaddPartialAutoid(stageHarness *test_case_harness.TestCaseHarness) error { 13 | b := redis_executable.NewRedisExecutable(stageHarness) 14 | if err := b.Run(); err != nil { 15 | return err 16 | } 17 | 18 | logger := stageHarness.Logger 19 | 20 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 21 | if err != nil { 22 | logFriendlyError(logger, err) 23 | return err 24 | } 25 | defer client.Close() 26 | 27 | randomKey := random.RandomWord() 28 | randomValues := random.RandomWords(3) 29 | 30 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 31 | Commands: [][]string{ 32 | {"XADD", randomKey, "0-*", randomValues[0], randomValues[1]}, 33 | {"XADD", randomKey, "1-*", randomValues[0], randomValues[1]}, 34 | {"XADD", randomKey, "1-*", randomValues[1], randomValues[2]}, 35 | }, 36 | Assertions: []resp_assertions.RESPAssertion{ 37 | resp_assertions.NewStringAssertion("0-1"), 38 | resp_assertions.NewStringAssertion("1-0"), 39 | resp_assertions.NewStringAssertion("1-1"), 40 | }, 41 | } 42 | 43 | return multiCommandTestCase.RunAll(client, logger) 44 | } 45 | -------------------------------------------------------------------------------- /internal/test_streams_xadd_validate_id.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 5 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 6 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 7 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 8 | "github.com/codecrafters-io/tester-utils/random" 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | func testStreamsXaddValidateID(stageHarness *test_case_harness.TestCaseHarness) error { 13 | b := redis_executable.NewRedisExecutable(stageHarness) 14 | if err := b.Run(); err != nil { 15 | return err 16 | } 17 | 18 | logger := stageHarness.Logger 19 | 20 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 21 | if err != nil { 22 | logFriendlyError(logger, err) 23 | return err 24 | } 25 | defer client.Close() 26 | 27 | randomKey := random.RandomWord() 28 | randomValues := random.RandomWords(10) 29 | 30 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 31 | Commands: [][]string{ 32 | {"XADD", randomKey, "1-1", randomValues[0], randomValues[1]}, 33 | {"XADD", randomKey, "1-2", randomValues[2], randomValues[3]}, 34 | {"XADD", randomKey, "1-2", randomValues[4], randomValues[5]}, 35 | {"XADD", randomKey, "0-3", randomValues[6], randomValues[7]}, 36 | {"XADD", randomKey, "0-0", randomValues[8], randomValues[9]}, 37 | }, 38 | Assertions: []resp_assertions.RESPAssertion{ 39 | resp_assertions.NewStringAssertion("1-1"), 40 | resp_assertions.NewStringAssertion("1-2"), 41 | resp_assertions.NewErrorAssertion("ERR The ID specified in XADD is equal or smaller than the target stream top item"), 42 | resp_assertions.NewErrorAssertion("ERR The ID specified in XADD is equal or smaller than the target stream top item"), 43 | resp_assertions.NewErrorAssertion("ERR The ID specified in XADD must be greater than 0-0"), 44 | }, 45 | } 46 | 47 | return multiCommandTestCase.RunAll(client, logger) 48 | } 49 | -------------------------------------------------------------------------------- /internal/test_streams_xrange.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 10 | 11 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | "github.com/go-redis/redis" 14 | ) 15 | 16 | func testStreamsXrange(stageHarness *test_case_harness.TestCaseHarness) error { 17 | b := redis_executable.NewRedisExecutable(stageHarness) 18 | if err := b.Run(); err != nil { 19 | return err 20 | } 21 | 22 | logger := stageHarness.Logger 23 | client := NewRedisClient("localhost:6379") 24 | 25 | randomKey := testerutils_random.RandomWord() 26 | 27 | max := 5 28 | min := 3 29 | randomNumber := testerutils_random.RandomInt(min, max) 30 | expectedResp := []redis.XMessage{} 31 | 32 | for i := 1; i <= randomNumber; i++ { 33 | id := "0-" + strconv.Itoa(i) 34 | 35 | xaddTest := &XADDTest{ 36 | streamKey: randomKey, 37 | id: id, 38 | values: map[string]interface{}{"foo": "bar"}, 39 | expectedResponse: id, 40 | } 41 | 42 | err := xaddTest.Run(client, logger) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | 48 | expectedResp = append(expectedResp, redis.XMessage{ 49 | ID: id, 50 | Values: map[string]interface{}{ 51 | "foo": "bar", 52 | }, 53 | }) 54 | } 55 | 56 | maxID := "0-" + strconv.Itoa(randomNumber) 57 | expectedResp = expectedResp[1:] 58 | 59 | logger.Infof("$ redis-cli xrange %v 0-2 %v", randomKey, maxID) 60 | resp, err := client.XRange(randomKey, "0-2", maxID).Result() 61 | 62 | if err != nil { 63 | logFriendlyError(logger, err) 64 | return err 65 | } 66 | 67 | expectedRespJSON, err := json.MarshalIndent(expectedResp, "", " ") 68 | 69 | if err != nil { 70 | logFriendlyError(logger, err) 71 | return err 72 | } 73 | 74 | respJSON, err := json.MarshalIndent(resp, "", " ") 75 | 76 | if err != nil { 77 | logFriendlyError(logger, err) 78 | return err 79 | } 80 | 81 | if !reflect.DeepEqual(resp, expectedResp) { 82 | logger.Infof("Received response: \"%v\"", string(respJSON)) 83 | return fmt.Errorf("Expected %v, got %v", string(expectedRespJSON), string(respJSON)) 84 | } else { 85 | logger.Successf("Received response: \"%v\"", string(respJSON)) 86 | } 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/test_streams_xrange_max_id.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 10 | 11 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | "github.com/go-redis/redis" 14 | ) 15 | 16 | func testStreamsXrangeMaxID(stageHarness *test_case_harness.TestCaseHarness) error { 17 | b := redis_executable.NewRedisExecutable(stageHarness) 18 | if err := b.Run(); err != nil { 19 | return err 20 | } 21 | 22 | logger := stageHarness.Logger 23 | client := NewRedisClient("localhost:6379") 24 | 25 | randomKey := testerutils_random.RandomWord() 26 | 27 | max := 5 28 | min := 3 29 | randomNumber := testerutils_random.RandomInt(min, max) 30 | expectedResp := []redis.XMessage{} 31 | 32 | for i := 1; i <= randomNumber; i++ { 33 | id := "0-" + strconv.Itoa(i) 34 | 35 | xaddTest := &XADDTest{ 36 | streamKey: randomKey, 37 | id: id, 38 | values: map[string]interface{}{"foo": "bar"}, 39 | expectedResponse: id, 40 | } 41 | 42 | err := xaddTest.Run(client, logger) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | for i := 2; i <= randomNumber; i++ { 50 | id := "0-" + strconv.Itoa(i) 51 | 52 | expectedResp = append(expectedResp, redis.XMessage{ 53 | ID: id, 54 | Values: map[string]interface{}{ 55 | "foo": "bar", 56 | }, 57 | }) 58 | } 59 | 60 | logger.Infof("$ redis-cli xrange %v 0-2 +", randomKey) 61 | resp, err := client.XRange(randomKey, "0-2", "+").Result() 62 | 63 | if err != nil { 64 | logFriendlyError(logger, err) 65 | return err 66 | } 67 | 68 | expectedRespJSON, err := json.MarshalIndent(expectedResp, "", " ") 69 | 70 | if err != nil { 71 | logFriendlyError(logger, err) 72 | return err 73 | } 74 | 75 | respJSON, err := json.MarshalIndent(resp, "", " ") 76 | 77 | if err != nil { 78 | logFriendlyError(logger, err) 79 | return err 80 | } 81 | 82 | if !reflect.DeepEqual(resp, expectedResp) { 83 | logger.Infof("Received response: \"%v\"", string(respJSON)) 84 | return fmt.Errorf("Expected %v, got %v", string(expectedRespJSON), string(respJSON)) 85 | } else { 86 | logger.Successf("Received response: \"%v\"", string(respJSON)) 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /internal/test_streams_xrange_min_id.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 10 | 11 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | "github.com/go-redis/redis" 14 | ) 15 | 16 | func testStreamsXrangeMinID(stageHarness *test_case_harness.TestCaseHarness) error { 17 | b := redis_executable.NewRedisExecutable(stageHarness) 18 | if err := b.Run(); err != nil { 19 | return err 20 | } 21 | 22 | logger := stageHarness.Logger 23 | client := NewRedisClient("localhost:6379") 24 | 25 | randomKey := testerutils_random.RandomWord() 26 | 27 | max := 5 28 | min := 3 29 | randomNumber := testerutils_random.RandomInt(min, max) 30 | expectedResp := []redis.XMessage{} 31 | 32 | for i := 1; i <= randomNumber; i++ { 33 | id := "0-" + strconv.Itoa(i) 34 | 35 | xaddTest := &XADDTest{ 36 | streamKey: randomKey, 37 | id: id, 38 | values: map[string]interface{}{"foo": "bar"}, 39 | expectedResponse: id, 40 | } 41 | 42 | err := xaddTest.Run(client, logger) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | maxID := "0-" + strconv.Itoa(randomNumber-1) 50 | 51 | for i := 1; i <= randomNumber-1; i++ { 52 | id := "0-" + strconv.Itoa(i) 53 | 54 | expectedResp = append(expectedResp, redis.XMessage{ 55 | ID: id, 56 | Values: map[string]interface{}{ 57 | "foo": "bar", 58 | }, 59 | }) 60 | } 61 | 62 | logger.Infof("$ redis-cli xrange %v - %v", randomKey, maxID) 63 | resp, err := client.XRange(randomKey, "-", maxID).Result() 64 | 65 | if err != nil { 66 | logFriendlyError(logger, err) 67 | return err 68 | } 69 | 70 | expectedRespJSON, err := json.MarshalIndent(expectedResp, "", " ") 71 | 72 | if err != nil { 73 | logFriendlyError(logger, err) 74 | return err 75 | } 76 | 77 | respJSON, err := json.MarshalIndent(resp, "", " ") 78 | 79 | if err != nil { 80 | logFriendlyError(logger, err) 81 | return err 82 | } 83 | 84 | if !reflect.DeepEqual(resp, expectedResp) { 85 | logger.Infof("Received response: \"%v\"", string(respJSON)) 86 | return fmt.Errorf("Expected %v, got %v", string(expectedRespJSON), string(respJSON)) 87 | } else { 88 | logger.Successf("Received response: \"%v\"", string(respJSON)) 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/test_streams_xread.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 12 | 13 | "github.com/codecrafters-io/tester-utils/logger" 14 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 15 | "github.com/codecrafters-io/tester-utils/test_case_harness" 16 | "github.com/go-redis/redis" 17 | ) 18 | 19 | type XREADTest struct { 20 | streams []string 21 | block *time.Duration 22 | expectedResponse []redis.XStream 23 | } 24 | 25 | func (t *XREADTest) Run(client *redis.Client, logger *logger.Logger) error { 26 | var resp []redis.XStream 27 | var err error 28 | 29 | if t.block == nil { 30 | logger.Infof("$ redis-cli xread streams %v", strings.Join(t.streams, " ")) 31 | 32 | resp, err = client.XRead(&redis.XReadArgs{ 33 | Streams: t.streams, 34 | Block: -1 * time.Millisecond, // Zero value for Block in XReadArgs struct is 0. Need a negative value to indicate no block. 35 | }).Result() 36 | } else { 37 | logger.Infof("$ redis-cli xread block %d streams %v", t.block.Milliseconds(), strings.Join(t.streams, " ")) 38 | 39 | resp, err = client.XRead(&redis.XReadArgs{ 40 | Streams: t.streams, 41 | Block: *t.block, 42 | }).Result() 43 | } 44 | 45 | if err != nil { 46 | logFriendlyError(logger, err) 47 | return err 48 | } 49 | 50 | expectedRespJSON, err := json.MarshalIndent(t.expectedResponse, "", " ") 51 | 52 | if err != nil { 53 | logFriendlyError(logger, err) 54 | return err 55 | } 56 | 57 | respJSON, err := json.MarshalIndent(resp, "", " ") 58 | 59 | if err != nil { 60 | logFriendlyError(logger, err) 61 | return err 62 | } 63 | 64 | if !reflect.DeepEqual(resp, t.expectedResponse) { 65 | logger.Infof("Received response: \"%v\"", string(respJSON)) 66 | return fmt.Errorf("Expected %v, got %v", string(expectedRespJSON), string(respJSON)) 67 | } else { 68 | logger.Successf("Received response: \"%v\"", string(respJSON)) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func testStreamsXread(stageHarness *test_case_harness.TestCaseHarness) error { 75 | b := redis_executable.NewRedisExecutable(stageHarness) 76 | if err := b.Run(); err != nil { 77 | return err 78 | } 79 | 80 | logger := stageHarness.Logger 81 | client := NewRedisClient("localhost:6379") 82 | 83 | randomKey := testerutils_random.RandomWord() 84 | randomInt := testerutils_random.RandomInt(1, 100) 85 | 86 | xaddTest := &XADDTest{ 87 | streamKey: randomKey, 88 | id: "0-1", 89 | values: map[string]interface{}{"temperature": randomInt}, 90 | expectedResponse: "0-1", 91 | } 92 | 93 | err := xaddTest.Run(client, logger) 94 | 95 | if err != nil { 96 | return err 97 | } 98 | 99 | expectedResp := []redis.XStream{ 100 | { 101 | Stream: randomKey, 102 | Messages: []redis.XMessage{ 103 | { 104 | ID: "0-1", 105 | Values: map[string]interface{}{"temperature": strconv.Itoa(randomInt)}, 106 | }, 107 | }, 108 | }, 109 | } 110 | 111 | xreadTest := &XREADTest{ 112 | streams: []string{randomKey, "0-0"}, 113 | expectedResponse: expectedResp, 114 | } 115 | 116 | err = xreadTest.Run(client, logger) 117 | 118 | if err != nil { 119 | return err 120 | } 121 | 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /internal/test_streams_xread_block.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 12 | 13 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 14 | "github.com/codecrafters-io/tester-utils/test_case_harness" 15 | "github.com/go-redis/redis" 16 | ) 17 | 18 | func testStreamsXreadBlock(stageHarness *test_case_harness.TestCaseHarness) error { 19 | b := redis_executable.NewRedisExecutable(stageHarness) 20 | if err := b.Run(); err != nil { 21 | return err 22 | } 23 | 24 | logger := stageHarness.Logger 25 | client := NewRedisClient("localhost:6379") 26 | 27 | randomKey := testerutils_random.RandomWord() 28 | randomInt := testerutils_random.RandomInt(1, 100) 29 | 30 | xaddTest := &XADDTest{ 31 | streamKey: randomKey, 32 | id: "0-1", 33 | values: map[string]interface{}{"temperature": randomInt}, 34 | expectedResponse: "0-1", 35 | } 36 | 37 | err := xaddTest.Run(client, logger) 38 | 39 | if err != nil { 40 | return err 41 | } 42 | 43 | var resp []redis.XStream 44 | 45 | done := make(chan bool) 46 | 47 | go func() error { 48 | logger.Infof("$ redis-cli xread block %d streams %v", 1000, strings.Join([]string{randomKey, "0-1"}, " ")) 49 | 50 | resp, err = client.XRead(&redis.XReadArgs{ 51 | Streams: []string{randomKey, "0-1"}, 52 | Block: 1000 * time.Millisecond, 53 | }).Result() 54 | 55 | if err != nil { 56 | logFriendlyError(logger, err) 57 | return err 58 | } 59 | 60 | done <- true 61 | return nil 62 | }() 63 | 64 | time.Sleep(500 * time.Millisecond) 65 | 66 | xaddTest = &XADDTest{ 67 | streamKey: randomKey, 68 | id: "0-2", 69 | values: map[string]interface{}{"temperature": randomInt}, 70 | expectedResponse: "0-2", 71 | } 72 | 73 | err = xaddTest.Run(client, logger) 74 | 75 | if err != nil { 76 | return err 77 | } 78 | 79 | <-done 80 | 81 | expectedResp := []redis.XStream{ 82 | { 83 | Stream: randomKey, 84 | Messages: []redis.XMessage{ 85 | { 86 | ID: "0-2", 87 | Values: map[string]interface{}{"temperature": strconv.Itoa(randomInt)}, 88 | }, 89 | }, 90 | }, 91 | } 92 | 93 | expectedRespJSON, err := json.MarshalIndent(expectedResp, "", " ") 94 | 95 | if err != nil { 96 | logFriendlyError(logger, err) 97 | return err 98 | } 99 | 100 | respJSON, err := json.MarshalIndent(resp, "", " ") 101 | 102 | if err != nil { 103 | logFriendlyError(logger, err) 104 | return err 105 | } 106 | 107 | if !reflect.DeepEqual(resp, expectedResp) { 108 | logger.Infof("Received response: \"%v\"", string(respJSON)) 109 | return fmt.Errorf("Expected %v, got %v", string(expectedRespJSON), string(respJSON)) 110 | } else { 111 | logger.Successf("Received response: \"%v\"", string(respJSON)) 112 | } 113 | 114 | logger.Infof("$ redis-cli xread block %d streams %v", 1000, strings.Join([]string{randomKey, "0-2"}, " ")) 115 | 116 | resp, err = client.XRead(&redis.XReadArgs{ 117 | Streams: []string{randomKey, "0-2"}, 118 | Block: 1000 * time.Millisecond, 119 | }).Result() 120 | 121 | if err != redis.Nil { 122 | if err == nil { 123 | logger.Debugf("Hint: Read about null bulk strings in the Redis protocol docs") 124 | return fmt.Errorf("Expected null string, got %q", resp) 125 | } 126 | 127 | logFriendlyError(logger, err) 128 | return err 129 | } else { 130 | logger.Successf("Received nil response") 131 | } 132 | 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /internal/test_streams_xread_block_max_id.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 9 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 10 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 11 | 12 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 13 | "github.com/codecrafters-io/tester-utils/test_case_harness" 14 | ) 15 | 16 | func testStreamsXreadBlockMaxID(stageHarness *test_case_harness.TestCaseHarness) error { 17 | b := redis_executable.NewRedisExecutable(stageHarness) 18 | if err := b.Run(); err != nil { 19 | return err 20 | } 21 | 22 | logger := stageHarness.Logger 23 | 24 | client1, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "redis-cli") 25 | if err != nil { 26 | logFriendlyError(logger, err) 27 | return err 28 | } 29 | defer client1.Close() 30 | 31 | randomKey := testerutils_random.RandomWord() 32 | randomInt := testerutils_random.RandomInt(1, 100) 33 | 34 | xaddCommandTestCase := &test_cases.SendCommandTestCase{ 35 | Command: "XADD", 36 | Args: []string{randomKey, "0-1", "temperature", strconv.Itoa(randomInt)}, 37 | Assertion: resp_assertions.NewStringAssertion("0-1"), 38 | ShouldSkipUnreadDataCheck: true, 39 | } 40 | 41 | if err := xaddCommandTestCase.Run(client1, logger); err != nil { 42 | return err 43 | } 44 | 45 | xReadDone := make(chan bool, 1) 46 | randomInt = testerutils_random.RandomInt(1, 100) 47 | 48 | xReadAssertion := resp_assertions.NewXReadResponseAssertion([]resp_assertions.StreamResponse{{ 49 | Key: randomKey, 50 | Entries: []resp_assertions.StreamEntry{{ 51 | Id: "0-2", 52 | FieldValuePairs: [][]string{{"temperature", strconv.Itoa(randomInt)}}, 53 | }}, 54 | }}) 55 | xReadTestCase := &test_cases.SendCommandTestCase{ 56 | Command: "XREAD", 57 | Args: []string{"block", "0", "streams", randomKey, "$"}, 58 | Assertion: xReadAssertion, 59 | ShouldSkipUnreadDataCheck: true, 60 | } 61 | xReadTestCase.PauseReadingResponse() 62 | 63 | go func() error { 64 | if err := xReadTestCase.Run(client1, logger); err != nil { 65 | return err 66 | } 67 | xReadDone <- true 68 | return nil 69 | }() 70 | 71 | time.Sleep(1000 * time.Millisecond) 72 | 73 | client2, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "other-redis-cli") 74 | if err != nil { 75 | logFriendlyError(logger, err) 76 | return err 77 | } 78 | defer client2.Close() 79 | 80 | xaddCommandTestCase = &test_cases.SendCommandTestCase{ 81 | Command: "XADD", 82 | Args: []string{randomKey, "0-2", "temperature", strconv.Itoa(randomInt)}, 83 | Assertion: resp_assertions.NewStringAssertion("0-2"), 84 | ShouldSkipUnreadDataCheck: true, 85 | } 86 | 87 | if err := xaddCommandTestCase.Run(client2, logger); err != nil { 88 | return err 89 | } 90 | 91 | // Ensure the responses of XAdd and XRead are fixed in the fixture 92 | xReadTestCase.ResumeReadingResponse() 93 | <-xReadDone 94 | 95 | xreadCommandTestCase := &test_cases.SendCommandTestCase{ 96 | Command: "XREAD", 97 | Args: []string{"block", "1000", "streams", randomKey, "$"}, 98 | Assertion: resp_assertions.NewNilAssertion(), 99 | ShouldSkipUnreadDataCheck: false, 100 | } 101 | 102 | if err := xreadCommandTestCase.Run(client1, logger); err != nil { 103 | return err 104 | } 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /internal/test_streams_xread_block_no_timeout.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 12 | 13 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 14 | "github.com/codecrafters-io/tester-utils/test_case_harness" 15 | "github.com/go-redis/redis" 16 | ) 17 | 18 | func testStreamsXreadBlockNoTimeout(stageHarness *test_case_harness.TestCaseHarness) error { 19 | b := redis_executable.NewRedisExecutable(stageHarness) 20 | if err := b.Run(); err != nil { 21 | return err 22 | } 23 | 24 | logger := stageHarness.Logger 25 | client := NewRedisClient("localhost:6379") 26 | 27 | randomKey := testerutils_random.RandomWord() 28 | randomInt := testerutils_random.RandomInt(1, 100) 29 | 30 | xaddTest := &XADDTest{ 31 | streamKey: randomKey, 32 | id: "0-1", 33 | values: map[string]interface{}{"temperature": randomInt}, 34 | expectedResponse: "0-1", 35 | } 36 | 37 | err := xaddTest.Run(client, logger) 38 | 39 | if err != nil { 40 | return err 41 | } 42 | 43 | respChan := make(chan *[]redis.XStream, 1) 44 | 45 | go func() error { 46 | logger.Infof("$ redis-cli xread block %d streams %v", 0, strings.Join([]string{randomKey, "0-1"}, " ")) 47 | 48 | resp, err := client.XRead(&redis.XReadArgs{ 49 | Streams: []string{randomKey, "0-1"}, 50 | Block: 0, 51 | }).Result() 52 | 53 | if err != nil { 54 | logger.Errorf("Error: %q", err) 55 | return err 56 | } 57 | 58 | respChan <- &resp 59 | 60 | return nil 61 | }() 62 | 63 | time.Sleep(1000 * time.Millisecond) 64 | 65 | xaddTest = &XADDTest{ 66 | streamKey: randomKey, 67 | id: "0-2", 68 | values: map[string]interface{}{"temperature": randomInt}, 69 | expectedResponse: "0-2", 70 | } 71 | 72 | err = xaddTest.Run(client, logger) 73 | 74 | if err != nil { 75 | return err 76 | } 77 | 78 | resp := <-respChan 79 | 80 | expectedResp := &[]redis.XStream{ 81 | { 82 | Stream: randomKey, 83 | Messages: []redis.XMessage{ 84 | { 85 | ID: "0-2", 86 | Values: map[string]interface{}{"temperature": strconv.Itoa(randomInt)}, 87 | }, 88 | }, 89 | }, 90 | } 91 | 92 | expectedRespJSON, err := json.MarshalIndent(expectedResp, "", " ") 93 | 94 | if err != nil { 95 | logFriendlyError(logger, err) 96 | return err 97 | } 98 | 99 | respJSON, err := json.MarshalIndent(resp, "", " ") 100 | 101 | if err != nil { 102 | logFriendlyError(logger, err) 103 | return err 104 | } 105 | 106 | if !reflect.DeepEqual(resp, expectedResp) { 107 | logger.Infof("Received response: \"%v\"", string(respJSON)) 108 | return fmt.Errorf("Expected %v, got %v", string(expectedRespJSON), string(respJSON)) 109 | } else { 110 | logger.Successf("Received response: \"%v\"", string(respJSON)) 111 | } 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /internal/test_streams_xread_multiple.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 5 | "strconv" 6 | 7 | testerutils_random "github.com/codecrafters-io/tester-utils/random" 8 | "github.com/codecrafters-io/tester-utils/test_case_harness" 9 | "github.com/go-redis/redis" 10 | ) 11 | 12 | func testStreamsXreadMultiple(stageHarness *test_case_harness.TestCaseHarness) error { 13 | b := redis_executable.NewRedisExecutable(stageHarness) 14 | if err := b.Run(); err != nil { 15 | return err 16 | } 17 | 18 | logger := stageHarness.Logger 19 | client := NewRedisClient("localhost:6379") 20 | 21 | randomKey := testerutils_random.RandomWord() 22 | var otherRandomKey string 23 | 24 | for { 25 | otherRandomKey = testerutils_random.RandomWord() 26 | if otherRandomKey != randomKey { 27 | break 28 | } 29 | } 30 | 31 | randomInt := testerutils_random.RandomInt(1, 100) 32 | otherRandomInt := testerutils_random.RandomInt(1, 100) 33 | 34 | xaddTest := &XADDTest{ 35 | streamKey: randomKey, 36 | id: "0-1", 37 | values: map[string]interface{}{"temperature": randomInt}, 38 | expectedResponse: "0-1", 39 | } 40 | 41 | err := xaddTest.Run(client, logger) 42 | 43 | if err != nil { 44 | return err 45 | } 46 | 47 | xaddTest = &XADDTest{ 48 | streamKey: otherRandomKey, 49 | id: "0-2", 50 | values: map[string]interface{}{"humidity": otherRandomInt}, 51 | expectedResponse: "0-2", 52 | } 53 | 54 | err = xaddTest.Run(client, logger) 55 | 56 | if err != nil { 57 | return err 58 | } 59 | 60 | expectedResp := []redis.XStream{ 61 | { 62 | Stream: randomKey, 63 | Messages: []redis.XMessage{ 64 | { 65 | ID: "0-1", 66 | Values: map[string]interface{}{"temperature": strconv.Itoa(randomInt)}, 67 | }, 68 | }, 69 | }, 70 | { 71 | Stream: otherRandomKey, 72 | Messages: []redis.XMessage{ 73 | { 74 | ID: "0-2", 75 | Values: map[string]interface{}{"humidity": strconv.Itoa(otherRandomInt)}, 76 | }, 77 | }, 78 | }, 79 | } 80 | 81 | xreadTest := &XREADTest{ 82 | streams: []string{randomKey, otherRandomKey, "0-0", "0-1"}, 83 | expectedResponse: expectedResp, 84 | } 85 | 86 | err = xreadTest.Run(client, logger) 87 | 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/test_txn_discard.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 10 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 11 | "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testTxDiscard(stageHarness *test_case_harness.TestCaseHarness) error { 16 | b := redis_executable.NewRedisExecutable(stageHarness) 17 | if err := b.Run(); err != nil { 18 | return err 19 | } 20 | 21 | logger := stageHarness.Logger 22 | 23 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 24 | if err != nil { 25 | logFriendlyError(logger, err) 26 | return err 27 | } 28 | defer client.Close() 29 | 30 | uniqueKeys := random.RandomWords(2) 31 | key1, key2 := uniqueKeys[0], uniqueKeys[1] 32 | randomInt1, randomInt2 := random.RandomInt(1, 100), random.RandomInt(1, 100) 33 | 34 | commandTestCase := test_cases.SendCommandTestCase{ 35 | Command: "SET", 36 | Args: []string{key2, fmt.Sprint(randomInt2)}, 37 | Assertion: resp_assertions.NewStringAssertion("OK"), 38 | } 39 | 40 | if err := commandTestCase.Run(client, logger); err != nil { 41 | return err 42 | } 43 | 44 | transactionTestCase := test_cases.TransactionTestCase{ 45 | CommandQueue: [][]string{ 46 | {"SET", key1, fmt.Sprint(randomInt1)}, 47 | {"INCR", key1}, 48 | }, 49 | } 50 | 51 | if err := transactionTestCase.RunWithoutExec(client, logger); err != nil { 52 | return err 53 | } 54 | 55 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 56 | Commands: [][]string{ 57 | {"DISCARD"}, 58 | {"GET", key1}, 59 | {"GET", key2}, 60 | {"DISCARD"}, 61 | }, 62 | Assertions: []resp_assertions.RESPAssertion{ 63 | resp_assertions.NewStringAssertion("OK"), 64 | resp_assertions.NewNilAssertion(), 65 | resp_assertions.NewStringAssertion(fmt.Sprint(randomInt2)), 66 | resp_assertions.NewErrorAssertion("ERR DISCARD without MULTI"), 67 | }, 68 | } 69 | 70 | if err := multiCommandTestCase.RunAll(client, logger); err != nil { 71 | return err 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/test_txn_empty.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 5 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | func testTxEmpty(stageHarness *test_case_harness.TestCaseHarness) error { 13 | b := redis_executable.NewRedisExecutable(stageHarness) 14 | if err := b.Run(); err != nil { 15 | return err 16 | } 17 | 18 | logger := stageHarness.Logger 19 | 20 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 21 | if err != nil { 22 | logFriendlyError(logger, err) 23 | return err 24 | } 25 | defer client.Close() 26 | 27 | emptyTransactionTestCase := test_cases.TransactionTestCase{} 28 | 29 | if err := emptyTransactionTestCase.RunAll(client, logger); err != nil { 30 | return err 31 | } 32 | 33 | bareExecCommandTestCase := test_cases.SendCommandTestCase{ 34 | Command: "EXEC", 35 | Args: []string{}, 36 | Assertion: resp_assertions.NewErrorAssertion("ERR EXEC without MULTI"), 37 | } 38 | 39 | return bareExecCommandTestCase.Run(client, logger) 40 | } 41 | -------------------------------------------------------------------------------- /internal/test_txn_exec.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 5 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 9 | "github.com/codecrafters-io/tester-utils/test_case_harness" 10 | ) 11 | 12 | func testTxExec(stageHarness *test_case_harness.TestCaseHarness) error { 13 | b := redis_executable.NewRedisExecutable(stageHarness) 14 | if err := b.Run(); err != nil { 15 | return err 16 | } 17 | 18 | logger := stageHarness.Logger 19 | 20 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 21 | if err != nil { 22 | logFriendlyError(logger, err) 23 | return err 24 | } 25 | defer client.Close() 26 | 27 | bareExecCommandTestCase := test_cases.SendCommandTestCase{ 28 | Command: "EXEC", 29 | Args: []string{}, 30 | Assertion: resp_assertions.NewErrorAssertion("ERR EXEC without MULTI"), 31 | } 32 | 33 | return bareExecCommandTestCase.Run(client, logger) 34 | } 35 | -------------------------------------------------------------------------------- /internal/test_txn_incr1.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 10 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 11 | "github.com/codecrafters-io/tester-utils/random" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | ) 14 | 15 | func testTxIncr1(stageHarness *test_case_harness.TestCaseHarness) error { 16 | b := redis_executable.NewRedisExecutable(stageHarness) 17 | if err := b.Run(); err != nil { 18 | return err 19 | } 20 | 21 | logger := stageHarness.Logger 22 | 23 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 24 | if err != nil { 25 | logFriendlyError(logger, err) 26 | return err 27 | } 28 | defer client.Close() 29 | 30 | randomValue := random.RandomInt(1, 100) 31 | randomKey := random.RandomWord() 32 | 33 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 34 | Commands: [][]string{ 35 | {"SET", randomKey, fmt.Sprint(randomValue)}, 36 | {"INCR", randomKey}, 37 | }, 38 | Assertions: []resp_assertions.RESPAssertion{ 39 | resp_assertions.NewStringAssertion("OK"), 40 | resp_assertions.NewIntegerAssertion(randomValue + 1), 41 | }, 42 | } 43 | 44 | return multiCommandTestCase.RunAll(client, logger) 45 | } 46 | -------------------------------------------------------------------------------- /internal/test_txn_incr2.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 5 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 9 | "github.com/codecrafters-io/tester-utils/random" 10 | "github.com/codecrafters-io/tester-utils/test_case_harness" 11 | ) 12 | 13 | func testTxIncr2(stageHarness *test_case_harness.TestCaseHarness) error { 14 | b := redis_executable.NewRedisExecutable(stageHarness) 15 | if err := b.Run(); err != nil { 16 | return err 17 | } 18 | 19 | logger := stageHarness.Logger 20 | 21 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 22 | if err != nil { 23 | logFriendlyError(logger, err) 24 | return err 25 | } 26 | defer client.Close() 27 | 28 | randomKey := random.RandomWord() 29 | 30 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 31 | Commands: [][]string{ 32 | {"INCR", randomKey}, 33 | {"INCR", randomKey}, 34 | {"GET", randomKey}, 35 | }, 36 | Assertions: []resp_assertions.RESPAssertion{ 37 | resp_assertions.NewIntegerAssertion(1), 38 | resp_assertions.NewIntegerAssertion(2), 39 | resp_assertions.NewStringAssertion("2"), 40 | }, 41 | } 42 | 43 | return multiCommandTestCase.RunAll(client, logger) 44 | } 45 | -------------------------------------------------------------------------------- /internal/test_txn_incr3.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 5 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 6 | 7 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 8 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 9 | "github.com/codecrafters-io/tester-utils/random" 10 | "github.com/codecrafters-io/tester-utils/test_case_harness" 11 | ) 12 | 13 | func testTxIncr3(stageHarness *test_case_harness.TestCaseHarness) error { 14 | b := redis_executable.NewRedisExecutable(stageHarness) 15 | if err := b.Run(); err != nil { 16 | return err 17 | } 18 | 19 | logger := stageHarness.Logger 20 | 21 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 22 | if err != nil { 23 | logFriendlyError(logger, err) 24 | return err 25 | } 26 | defer client.Close() 27 | 28 | uniqueKeys := random.RandomWords(2) 29 | randomKey, randomValue := uniqueKeys[0], uniqueKeys[1] 30 | 31 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 32 | Commands: [][]string{ 33 | {"SET", randomKey, randomValue}, 34 | {"INCR", randomKey}, 35 | }, 36 | Assertions: []resp_assertions.RESPAssertion{ 37 | resp_assertions.NewStringAssertion("OK"), 38 | resp_assertions.NewErrorAssertion("ERR value is not an integer or out of range"), 39 | }, 40 | } 41 | 42 | return multiCommandTestCase.RunAll(client, logger) 43 | } 44 | -------------------------------------------------------------------------------- /internal/test_txn_multi.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 7 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 8 | "github.com/codecrafters-io/tester-utils/test_case_harness" 9 | ) 10 | 11 | func testTxMulti(stageHarness *test_case_harness.TestCaseHarness) error { 12 | b := redis_executable.NewRedisExecutable(stageHarness) 13 | if err := b.Run(); err != nil { 14 | return err 15 | } 16 | 17 | logger := stageHarness.Logger 18 | 19 | client, err := instrumented_resp_connection.NewFromAddr(logger, "localhost:6379", "client") 20 | if err != nil { 21 | logFriendlyError(logger, err) 22 | return err 23 | } 24 | defer client.Close() 25 | 26 | transactionTestCase := test_cases.TransactionTestCase{} 27 | 28 | return transactionTestCase.RunMulti(client, logger) 29 | } 30 | -------------------------------------------------------------------------------- /internal/test_txn_multi_tx.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | "github.com/codecrafters-io/tester-utils/random" 11 | "github.com/codecrafters-io/tester-utils/test_case_harness" 12 | ) 13 | 14 | func testTxMultiTx(stageHarness *test_case_harness.TestCaseHarness) error { 15 | b := redis_executable.NewRedisExecutable(stageHarness) 16 | if err := b.Run(); err != nil { 17 | return err 18 | } 19 | 20 | logger := stageHarness.Logger 21 | 22 | clients, err := SpawnClients(3, "localhost:6379", stageHarness, logger) 23 | if err != nil { 24 | return err 25 | } 26 | for _, client := range clients { 27 | defer client.Close() 28 | } 29 | 30 | uniqueKeys := random.RandomWords(2) 31 | key1, key2 := uniqueKeys[0], uniqueKeys[1] 32 | randomIntegerValue := random.RandomInt(1, 100) 33 | 34 | for i, client := range clients { 35 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 36 | Commands: [][]string{ 37 | {"SET", key2, fmt.Sprint(randomIntegerValue)}, 38 | {"INCR", key1}, 39 | }, 40 | Assertions: []resp_assertions.RESPAssertion{ 41 | resp_assertions.NewStringAssertion("OK"), 42 | resp_assertions.NewIntegerAssertion(i + 1), 43 | }, 44 | } 45 | 46 | if err := multiCommandTestCase.RunAll(client, logger); err != nil { 47 | return err 48 | } 49 | } 50 | 51 | for _, client := range clients { 52 | transactionTestCase := test_cases.TransactionTestCase{ 53 | CommandQueue: [][]string{ 54 | {"INCR", key1}, 55 | {"INCR", key2}, 56 | }, 57 | } 58 | if err := transactionTestCase.RunWithoutExec(client, logger); err != nil { 59 | return err 60 | } 61 | } 62 | 63 | for i, client := range clients { 64 | transactionTestCase := test_cases.TransactionTestCase{ 65 | // Before a single transaction is queued, 66 | // We run 3x INCR key1, and set key2 to randomIntegerValue 67 | // Inside each transaction, we run 1x INCR key1, key2 68 | // So it increases by 1 for each transaction 69 | // `i` here is 0-indexed, so we add 1 to the expected value 70 | ExpectedResponseArray: []resp_assertions.RESPAssertion{resp_assertions.NewIntegerAssertion(3 + (1 + i)), resp_assertions.NewIntegerAssertion(randomIntegerValue + (1 + i))}, 71 | } 72 | if err := transactionTestCase.RunExec(client, logger); err != nil { 73 | return err 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/test_txn_queue.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | "github.com/codecrafters-io/tester-utils/random" 11 | "github.com/codecrafters-io/tester-utils/test_case_harness" 12 | ) 13 | 14 | func testTxQueue(stageHarness *test_case_harness.TestCaseHarness) error { 15 | b := redis_executable.NewRedisExecutable(stageHarness) 16 | if err := b.Run(); err != nil { 17 | return err 18 | } 19 | 20 | logger := stageHarness.Logger 21 | 22 | clients, err := SpawnClients(2, "localhost:6379", stageHarness, logger) 23 | if err != nil { 24 | return err 25 | } 26 | for _, client := range clients { 27 | defer client.Close() 28 | } 29 | 30 | key := random.RandomWord() 31 | randomIntegerValue := random.RandomInt(1, 100) 32 | 33 | transactionTestCase := test_cases.TransactionTestCase{ 34 | CommandQueue: [][]string{ 35 | {"SET", key, fmt.Sprint(randomIntegerValue)}, 36 | {"INCR", key}, 37 | }, 38 | } 39 | 40 | if err := transactionTestCase.RunWithoutExec(clients[0], logger); err != nil { 41 | return err 42 | } 43 | 44 | commandTestCase := test_cases.SendCommandTestCase{ 45 | Command: "GET", 46 | Args: []string{key}, 47 | Assertion: resp_assertions.NewNilAssertion(), 48 | } 49 | 50 | return commandTestCase.Run(clients[1], logger) 51 | } 52 | -------------------------------------------------------------------------------- /internal/test_txn_tx.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 7 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 8 | 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | "github.com/codecrafters-io/tester-utils/random" 11 | "github.com/codecrafters-io/tester-utils/test_case_harness" 12 | ) 13 | 14 | func testTxSuccess(stageHarness *test_case_harness.TestCaseHarness) error { 15 | b := redis_executable.NewRedisExecutable(stageHarness) 16 | if err := b.Run(); err != nil { 17 | return err 18 | } 19 | 20 | logger := stageHarness.Logger 21 | 22 | clients, err := SpawnClients(2, "localhost:6379", stageHarness, logger) 23 | if err != nil { 24 | return err 25 | } 26 | for _, client := range clients { 27 | defer client.Close() 28 | } 29 | 30 | uniqueKeys := random.RandomWords(2) 31 | key1, key2 := uniqueKeys[0], uniqueKeys[1] 32 | randomIntegerValue := random.RandomInt(1, 100) 33 | 34 | transactionTestCase := test_cases.TransactionTestCase{ 35 | CommandQueue: [][]string{ 36 | {"SET", key1, fmt.Sprint(randomIntegerValue)}, 37 | {"INCR", key1}, 38 | {"INCR", key2}, 39 | {"GET", key2}, 40 | }, 41 | ExpectedResponseArray: []resp_assertions.RESPAssertion{resp_assertions.NewStringAssertion("OK"), resp_assertions.NewIntegerAssertion(randomIntegerValue + 1), resp_assertions.NewIntegerAssertion(1), resp_assertions.NewStringAssertion("1")}, 42 | } 43 | 44 | if err := transactionTestCase.RunAll(clients[0], logger); err != nil { 45 | return err 46 | } 47 | 48 | commandTestCase := test_cases.SendCommandTestCase{ 49 | Command: "GET", 50 | Args: []string{key1}, 51 | Assertion: resp_assertions.NewStringAssertion(fmt.Sprint(randomIntegerValue + 1)), 52 | } 53 | 54 | return commandTestCase.Run(clients[1], logger) 55 | } 56 | -------------------------------------------------------------------------------- /internal/test_txn_tx_failure.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codecrafters-io/redis-tester/internal/redis_executable" 7 | 8 | "github.com/codecrafters-io/redis-tester/internal/resp_assertions" 9 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 10 | "github.com/codecrafters-io/tester-utils/random" 11 | "github.com/codecrafters-io/tester-utils/test_case_harness" 12 | ) 13 | 14 | func testTxErr(stageHarness *test_case_harness.TestCaseHarness) error { 15 | b := redis_executable.NewRedisExecutable(stageHarness) 16 | if err := b.Run(); err != nil { 17 | return err 18 | } 19 | 20 | logger := stageHarness.Logger 21 | 22 | clients, err := SpawnClients(2, "localhost:6379", stageHarness, logger) 23 | if err != nil { 24 | return err 25 | } 26 | for _, client := range clients { 27 | defer client.Close() 28 | } 29 | 30 | uniqueKeys := random.RandomWords(3) 31 | key1, key2 := uniqueKeys[0], uniqueKeys[1] 32 | randomStringValue := uniqueKeys[2] 33 | randomIntegerValue := random.RandomInt(1, 100) 34 | 35 | multiCommandTestCase := test_cases.MultiCommandTestCase{ 36 | Commands: [][]string{ 37 | {"SET", key1, randomStringValue}, 38 | {"SET", key2, fmt.Sprint(randomIntegerValue)}, 39 | }, 40 | Assertions: []resp_assertions.RESPAssertion{ 41 | resp_assertions.NewStringAssertion("OK"), 42 | resp_assertions.NewStringAssertion("OK"), 43 | }, 44 | } 45 | 46 | if err := multiCommandTestCase.RunAll(clients[0], logger); err != nil { 47 | return err 48 | } 49 | 50 | transactionTestCase := test_cases.TransactionTestCase{ 51 | CommandQueue: [][]string{ 52 | {"INCR", key1}, 53 | {"INCR", key2}, 54 | }, 55 | ExpectedResponseArray: []resp_assertions.RESPAssertion{ 56 | resp_assertions.NewErrorAssertion("ERR value is not an integer or out of range"), 57 | resp_assertions.NewIntegerAssertion(randomIntegerValue + 1), 58 | }, 59 | } 60 | 61 | if err := transactionTestCase.RunAll(clients[0], logger); err != nil { 62 | return err 63 | } 64 | 65 | multiCommandTestCase = test_cases.MultiCommandTestCase{ 66 | Commands: [][]string{ 67 | {"GET", key2}, 68 | {"GET", key1}, 69 | }, 70 | Assertions: []resp_assertions.RESPAssertion{ 71 | resp_assertions.NewStringAssertion(fmt.Sprint(randomIntegerValue + 1)), 72 | resp_assertions.NewStringAssertion(randomStringValue), 73 | }, 74 | } 75 | 76 | return multiCommandTestCase.RunAll(clients[1], logger) 77 | } 78 | -------------------------------------------------------------------------------- /internal/tester_definition.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/codecrafters-io/tester-utils/tester_definition" 7 | ) 8 | 9 | var testerDefinition = tester_definition.TesterDefinition{ 10 | AntiCheatTestCases: []tester_definition.TestCase{ 11 | { 12 | Slug: "anti-cheat-1", 13 | TestFunc: antiCheatTest, 14 | }, 15 | }, 16 | ExecutableFileName: "your_program.sh", 17 | LegacyExecutableFileName: "spawn_redis_server.sh", 18 | TestCases: []tester_definition.TestCase{ 19 | { 20 | Slug: "jm1", 21 | TestFunc: testBindToPort, 22 | Timeout: 15 * time.Second, 23 | }, 24 | { 25 | Slug: "rg2", 26 | TestFunc: testPingPongOnce, 27 | }, 28 | { 29 | Slug: "wy1", 30 | TestFunc: testPingPongMultiple, 31 | }, 32 | { 33 | Slug: "zu2", 34 | TestFunc: testPingPongConcurrent, 35 | }, 36 | { 37 | Slug: "qq0", 38 | TestFunc: testEcho, 39 | }, 40 | { 41 | Slug: "la7", 42 | TestFunc: testGetSet, 43 | }, 44 | { 45 | Slug: "yz1", 46 | TestFunc: testExpiry, 47 | }, 48 | { 49 | Slug: "zg5", 50 | TestFunc: testRdbConfig, 51 | }, 52 | { 53 | Slug: "jz6", 54 | TestFunc: testRdbReadKey, 55 | }, 56 | { 57 | Slug: "gc6", 58 | TestFunc: testRdbReadStringValue, 59 | }, 60 | { 61 | Slug: "jw4", 62 | TestFunc: testRdbReadMultipleKeys, 63 | }, 64 | { 65 | Slug: "dq3", 66 | TestFunc: testRdbReadMultipleStringValues, 67 | }, 68 | { 69 | Slug: "sm4", 70 | TestFunc: testRdbReadValueWithExpiry, 71 | }, 72 | { 73 | Slug: "bw1", 74 | TestFunc: testReplBindToCustomPort, 75 | }, 76 | { 77 | Slug: "ye5", 78 | TestFunc: testReplInfo, 79 | }, 80 | { 81 | Slug: "hc6", 82 | TestFunc: testReplInfoReplica, 83 | }, 84 | { 85 | Slug: "xc1", 86 | TestFunc: testReplReplicationID, 87 | }, 88 | { 89 | Slug: "gl7", 90 | TestFunc: testReplReplicaSendsPing, 91 | }, 92 | { 93 | Slug: "eh4", 94 | TestFunc: testReplReplicaSendsReplconf, 95 | }, 96 | { 97 | Slug: "ju6", 98 | TestFunc: testReplReplicaSendsPsync, 99 | }, 100 | { 101 | Slug: "fj0", 102 | TestFunc: testReplMasterReplconf, 103 | }, 104 | { 105 | Slug: "vm3", 106 | TestFunc: testReplMasterPsync, 107 | }, 108 | { 109 | Slug: "cf8", 110 | TestFunc: testReplMasterPsyncRdb, 111 | }, 112 | { 113 | Slug: "zn8", 114 | TestFunc: testReplMasterCmdProp, 115 | }, 116 | { 117 | Slug: "hd5", 118 | TestFunc: testReplMultipleReplicas, 119 | }, 120 | { 121 | Slug: "yg4", 122 | TestFunc: testReplCmdProcessing, 123 | }, 124 | { 125 | Slug: "xv6", 126 | TestFunc: testReplGetaAckZero, 127 | }, 128 | { 129 | Slug: "yd3", 130 | TestFunc: testReplGetaAckNonZero, 131 | }, 132 | { 133 | Slug: "my8", 134 | TestFunc: testWaitZeroReplicas, 135 | }, 136 | { 137 | Slug: "tu8", 138 | TestFunc: testWaitZeroOffset, 139 | }, 140 | { 141 | Slug: "na2", 142 | TestFunc: testWait, 143 | }, 144 | { 145 | Slug: "cc3", 146 | TestFunc: testStreamsType, 147 | }, 148 | { 149 | Slug: "cf6", 150 | TestFunc: testStreamsXadd, 151 | }, 152 | { 153 | Slug: "hq8", 154 | TestFunc: testStreamsXaddValidateID, 155 | }, 156 | { 157 | Slug: "yh3", 158 | TestFunc: testStreamsXaddPartialAutoid, 159 | }, 160 | { 161 | Slug: "xu6", 162 | TestFunc: testStreamsXaddFullAutoid, 163 | }, 164 | { 165 | Slug: "zx1", 166 | TestFunc: testStreamsXrange, 167 | }, 168 | { 169 | Slug: "yp1", 170 | TestFunc: testStreamsXrangeMinID, 171 | }, 172 | { 173 | Slug: "fs1", 174 | TestFunc: testStreamsXrangeMaxID, 175 | }, 176 | { 177 | Slug: "um0", 178 | TestFunc: testStreamsXread, 179 | }, 180 | { 181 | Slug: "ru9", 182 | TestFunc: testStreamsXreadMultiple, 183 | }, 184 | { 185 | Slug: "bs1", 186 | TestFunc: testStreamsXreadBlock, 187 | }, 188 | { 189 | Slug: "hw1", 190 | TestFunc: testStreamsXreadBlockNoTimeout, 191 | }, 192 | { 193 | Slug: "xu1", 194 | TestFunc: testStreamsXreadBlockMaxID, 195 | }, 196 | { 197 | Slug: "si4", 198 | TestFunc: testTxIncr1, 199 | }, 200 | { 201 | Slug: "lz8", 202 | TestFunc: testTxIncr2, 203 | }, 204 | { 205 | Slug: "mk1", 206 | TestFunc: testTxIncr3, 207 | }, 208 | { 209 | Slug: "pn0", 210 | TestFunc: testTxMulti, 211 | }, 212 | { 213 | Slug: "lo4", 214 | TestFunc: testTxExec, 215 | }, 216 | { 217 | Slug: "we1", 218 | TestFunc: testTxEmpty, 219 | }, 220 | { 221 | Slug: "rs9", 222 | TestFunc: testTxQueue, 223 | }, 224 | { 225 | Slug: "fy6", 226 | TestFunc: testTxSuccess, 227 | }, 228 | { 229 | Slug: "rl9", 230 | TestFunc: testTxDiscard, 231 | }, 232 | { 233 | Slug: "sg9", 234 | TestFunc: testTxErr, 235 | }, 236 | { 237 | Slug: "jf8", 238 | TestFunc: testTxMultiTx, 239 | }, 240 | }, 241 | } 242 | -------------------------------------------------------------------------------- /internal/tester_definition_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | tester_utils_testing "github.com/codecrafters-io/tester-utils/testing" 7 | ) 8 | 9 | func TestStagesMatchYAML(t *testing.T) { 10 | tester_utils_testing.ValidateTesterDefinitionAgainstYAML(t, testerDefinition, "test_helpers/course_definition.yml") 11 | } 12 | -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection" 9 | resp_connection "github.com/codecrafters-io/redis-tester/internal/resp/connection" 10 | "github.com/codecrafters-io/redis-tester/internal/test_cases" 11 | "github.com/codecrafters-io/tester-utils/logger" 12 | "github.com/codecrafters-io/tester-utils/test_case_harness" 13 | 14 | resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value" 15 | ) 16 | 17 | func deleteRDBfile() { 18 | fileName := "dump.rdb" 19 | _, err := os.Stat(fileName) 20 | if os.IsNotExist(err) { 21 | return 22 | } 23 | _ = os.Remove(fileName) 24 | } 25 | 26 | func IsSelectCommand(value resp_value.Value) bool { 27 | return value.Type == resp_value.ARRAY && 28 | len(value.Array()) > 0 && 29 | value.Array()[0].Type == resp_value.BULK_STRING && 30 | strings.ToLower(value.Array()[0].String()) == "select" 31 | } 32 | 33 | func SpawnReplicas(replicaCount int, stageHarness *test_case_harness.TestCaseHarness, logger *logger.Logger, addr string) ([]*resp_connection.RespConnection, error) { 34 | var replicas []*resp_connection.RespConnection 35 | sendHandshakeTestCase := test_cases.SendReplicationHandshakeTestCase{} 36 | 37 | listeningPort := 6380 38 | for j := 0; j < replicaCount; j++ { 39 | logger.Debugf("Creating replica: %v", j+1) 40 | replica, err := instrumented_resp_connection.NewFromAddr(logger, addr, fmt.Sprintf("replica-%v", j+1)) 41 | if err != nil { 42 | logFriendlyError(logger, err) 43 | return nil, err 44 | } 45 | 46 | logger.UpdateSecondaryPrefix("handshake") 47 | 48 | if err := sendHandshakeTestCase.RunAll(replica, logger, listeningPort); err != nil { 49 | return nil, err 50 | } 51 | 52 | logger.ResetSecondaryPrefix() 53 | 54 | listeningPort += 1 55 | // The bytes received and sent during the handshake don't count towards offset. 56 | // After finishing the handshake we reset the counters. 57 | replica.ResetByteCounters() 58 | 59 | replicas = append(replicas, replica) 60 | } 61 | return replicas, nil 62 | } 63 | 64 | // SpawnClients creates `clientCount` clients connected to the given address. 65 | // The clients are created using the `instrumented_resp_connection.NewFromAddr` function. 66 | // Clients are supposed to be closed after use. 67 | func SpawnClients(clientCount int, addr string, stageHarness *test_case_harness.TestCaseHarness, logger *logger.Logger) ([]*resp_connection.RespConnection, error) { 68 | var clients []*resp_connection.RespConnection 69 | 70 | for i := 0; i < clientCount; i++ { 71 | client, err := instrumented_resp_connection.NewFromAddr(logger, addr, fmt.Sprintf("client-%d", i+1)) 72 | if err != nil { 73 | logFriendlyError(logger, err) 74 | return nil, err 75 | } 76 | clients = append(clients, client) 77 | } 78 | return clients, nil 79 | } 80 | 81 | func GetFormattedHexdump(data []byte) string { 82 | // This is used for logs 83 | // Contains headers + vertical & horizontal separators + offset 84 | // We use a different format for the error logs 85 | var formattedHexdump strings.Builder 86 | var asciiChars strings.Builder 87 | 88 | formattedHexdump.WriteString("Idx | Hex | ASCII\n") 89 | formattedHexdump.WriteString("-----+-------------------------------------------------+-----------------\n") 90 | 91 | for i, b := range data { 92 | if i%16 == 0 && i != 0 { 93 | formattedHexdump.WriteString("| " + asciiChars.String() + "\n") 94 | asciiChars.Reset() 95 | } 96 | if i%16 == 0 { 97 | formattedHexdump.WriteString(fmt.Sprintf("%04x | ", i)) 98 | } 99 | formattedHexdump.WriteString(fmt.Sprintf("%02x ", b)) 100 | 101 | // Add ASCII representation 102 | if b >= 32 && b <= 126 { 103 | asciiChars.WriteByte(b) 104 | } else { 105 | asciiChars.WriteByte('.') 106 | } 107 | } 108 | 109 | // Pad the last line if necessary 110 | if len(data)%16 != 0 { 111 | padding := 16 - (len(data) % 16) 112 | for i := 0; i < padding; i++ { 113 | formattedHexdump.WriteString(" ") 114 | } 115 | } 116 | 117 | // Add the final ASCII representation 118 | formattedHexdump.WriteString("| " + asciiChars.String()) 119 | 120 | return formattedHexdump.String() 121 | } 122 | 123 | // FormatKeys formats a list of keys as a string, with each key quoted. 124 | // Used for logging RDB contents. 125 | func FormatKeys(keys []string) string { 126 | return fmt.Sprintf("[%s]", strings.Join(quotedStrings(keys), ", ")) 127 | } 128 | 129 | func quotedStrings(ss []string) []string { 130 | quoted := make([]string, len(ss)) 131 | for i, s := range ss { 132 | quoted[i] = fmt.Sprintf("%q", s) 133 | } 134 | return quoted 135 | } 136 | 137 | // FormatKeyValuePairs formats a list of key-value pairs as a string, with each key and value quoted. 138 | // Used for logging RDB contents. 139 | func FormatKeyValuePairs(keys []string, values []string) string { 140 | if len(keys) != len(values) { 141 | return "{}" 142 | } 143 | 144 | pairs := make([]string, len(keys)) 145 | for i := range keys { 146 | pairs[i] = fmt.Sprintf("%q: %q", keys[i], values[i]) 147 | } 148 | 149 | return fmt.Sprintf("{%s}", strings.Join(pairs, ", ")) 150 | } 151 | -------------------------------------------------------------------------------- /main.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: ./cmd/tester/main.go 3 | binary: tester 4 | env: 5 | - CGO_ENABLED=0 6 | goarch: [amd64, arm64] 7 | goos: [linux, darwin] 8 | 9 | archives: 10 | - name_template: "{{ .Tag }}_{{ .Os }}_{{ .Arch }}" 11 | format: tar.gz 12 | files: 13 | - test.sh 14 | -------------------------------------------------------------------------------- /run_failure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd $HOME/experiments/learnings/redis_challenge/implementation 3 | exit 1 4 | # bundle exec ruby main.rb 5 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec "${TESTER_DIR}/tester" 3 | --------------------------------------------------------------------------------