diff --git a/docs/src/content/docs/commands/BITFIELD.md b/docs/src/content/docs/commands/BITFIELD.md new file mode 100644 index 000000000..1318f9617 --- /dev/null +++ b/docs/src/content/docs/commands/BITFIELD.md @@ -0,0 +1,157 @@ +--- +title: BITFIELD +description: The BITFIELD command in DiceDB performs complex bitwise operations on string values at specified offsets, treating them as an array of integers. It allows manipulation of individual bits or groups of bits, supporting commands like SET, GET, and INCRBY to update or retrieve bitfield values. +--- + +## Syntax + +```bash +BITFIELD key [GET type offset | [OVERFLOW ] + + [GET type offset | [OVERFLOW ] + + ...]] +``` + +## Parameters + +| Parameter | Description |Type |Required| +|--------------------|----------------------------------------------------------------------------------------------------------------------------|------|--------| +| `key` | The name of the key containing the bitfield. |String|Yes | +| `GET type offset` | Retrieves bits starting at the specified offset with the specified type. Type defines the signed/unsigned integer format. |String|Optional| +| `SET type offset value`| Sets bits at the specified offset to the given value using the specified integer type. |String|Optional| +| `INCRBY type offset increment`| Increments bits at the specified offset by the increment value and wraps around on overflow based on type. |String|Optional| +| `OVERFLOW WRAP\|SAT\|FAIL` | Defines overflow behavior. |String|Optional| + +## Return values + +| Condition | Return Value | +|----------------------------------------------------------------|-------------------------------------------------------| +| Command is successful | Array of results corresponding to each subcommand | +| Syntax or specified constraints are invalid | error | + +## Behaviour +The BITFIELD command in DiceDB allows for direct bitwise manipulation within a binary string stored in a single key. It works by treating the string as an array of integers and performs operations on specific bits or groups of bits at specified offsets: +- SET: Sets the value of bits at a specific offset. +- GET: Retrieves the value of bits at a specific offset. +- INCRBY: Increments the value at a specific offset by a given amount, useful for counters. +- OVERFLOW: Defines the overflow behavior (WRAP, SAT, FAIL) for INCRBY, determining how to handle values that exceed the bitfield’s limits. + +#### Overflow Options: +- WRAP: Cycles values back to zero on overflow (default behavior). +- SAT: Saturates to the maximum or minimum value for the bit width. +- FAIL: Prevents overflow by causing INCRBY to fail if it would exceed the limits. +
+
+- Supports unsigned (u) with bit widths between 1 and 63 and signed (i) integers with bit widths between 1 and 64. +- The offset specifies where the bitfield starts within the key’s binary string. +- If an offset is out of range (far beyond the current size), DiceDB will automatically expand the binary string to accommodate it, which can impact memory usage. + +## Errors + +1. `Syntax Error`: + - Error Message: `(error) ERR syntax error` + - Occurs if the commands syntax is incorrect. + +2. `Invalid bitfield type`: + - Error Message: `(error) ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is` + - Occurs when attempting to use the command on a wrong type. + +3. `Non-integer value`: + - Error Message: `(error) ERR value is not an integer or out of range` + - Occurs when attempting to use the command on a value that contains a non-integer value. + +4. `Invalid OVERLOW type`: + - Error Message: `(error) ERR Invalid OVERFLOW type specified` + - Occurs when attempting to use a wrong OVERFLOW type. + +5. `Wrong type of value or key`: + - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + - Occurs when attempting to use the command on a key that contains a non-string value. + +6. `Invalid bIT offset`: + - Error Message: `(error) ERR bit offset is not an integer or out of range` + - Occurs when attempting to use the command with an invalid bit offset. + + +## Example Usage + +### Basic Usage: + +```bash +127.0.0.1:7379> BITFIELD mykey INCRBY i5 100 1 GET u4 0 +1) "1" +2) "0" +``` +### Overflow control: +```bash +127.0.0.1:7379> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +1) "1" +2) "1" +127.0.0.1:7379> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +1) "2" +2) "2" +127.0.0.1:7379> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +1) "3" +2) "3" +127.0.0.1:7379> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +1) "0" +2) "3" +``` +### Example of OVERFLOW FAIL returning nil: +```bash +127.0.0.1:7379> BITFIELD mykey OVERFLOW FAIL incrby u2 102 1 +(nil) +``` + +### Invalid usage: + +```bash +127.0.0.1:7379> BITFIELD +(error) ERR wrong number of arguments for 'bitfield' command +``` + +```bash +127.0.0.1:7379> SADD bits a b c +(integer) 3 +127.0.0.1:7379> BITFIELD bits +(error) ERR WRONGTYPE Operation against a key holding the wrong kind of value +``` + +```bash +127.0.0.1:7379> BITFIELD bits SET u8 0 255 INCRBY u8 0 100 GET u8 +(error) ERR syntax error +``` + +```bash +127.0.0.1:7379> bitfield bits set a8 0 255 incrby u8 0 100 get u8 +(error) ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is +``` + +```bash +127.0.0.1:7379> bitfield bits set u8 a 255 incrby u8 0 100 get u8 +(error) ERR bit offset is not an integer or out of range +``` + +```bash +127.0.0.1:7379> bitfield bits set u8 0 255 incrby u8 0 100 overflow wraap +(error) ERR Invalid OVERFLOW type specified +``` + +```bash +127.0.0.1:7379> bitfield bits set u8 0 incrby u8 0 100 get u8 288 +(error) ERR value is not an integer or out of range +``` + + +## Notes +Where an integer encoding is expected, it can be composed by prefixing with i for signed integers and u for unsigned integers with the number of bits of our integer encoding. So for example u8 is an unsigned integer of 8 bits and i16 is a signed integer of 16 bits. + +The supported encodings are up to 64 bits for signed integers, and up to 63 bits for unsigned integers. This limitation with unsigned integers is due to the fact that currently RESP is unable to return 64 bit unsigned integers as replies. + +## Subcommands +- **subcommand**: Optional. Available subcommands include: + - `GET` `` `` : Returns the specified bit field. + - `SET` `` `` `` : Set the specified bit field and - returns its old value. + - `INCRBY` `` `` `` : Increments or decrements (if a negative increment is given) the specified bit field and returns the new value. + - `OVERFLOW` [ `WRAP` | `SAT` | `FAIL` ] diff --git a/docs/src/content/docs/commands/BITFIELD_RO.md b/docs/src/content/docs/commands/BITFIELD_RO.md new file mode 100644 index 000000000..fc5a1dc0e --- /dev/null +++ b/docs/src/content/docs/commands/BITFIELD_RO.md @@ -0,0 +1,41 @@ +--- +title: BITFIELD_RO +description: Read-only variant of the BITFIELD command. It is like the original BITFIELD but only accepts GET subcommand. +--- + +## Syntax + +```bash +BITFIELD_RO key [GET type offset [GET type offset ...]] +``` + +## Parameters + +| Parameter | Description |Type |Required| +|-----------|-------------------------------------------------------------------------------------------------------------------------------------|------|--------| +| `key` | The name of the key containing the bitfield. |String|Yes | +| `GET type offset` | Retrieves bits starting at the specified offset with the specified type. Type defines the signed/unsigned integer format. |String|Optional| + + + +## Return values + +| Condition | Return Value | +|----------------------------------------------------------------|-------------------------------------------------------| +| Command is successful | Array of results corresponding to each `GET` command | +| Syntax or specified constraints are invalid | error | + +## Behaviour + +- Read-only variant of the BITFIELD command. It is like the original BITFIELD but only accepts GET subcommand. +- See original BITFIELD for more details. + +## Example Usage +### Basic Usage + +```bash +127.0.0.1:7379> SET hello "Hello World" +OK +127.0.0.1:7379> BITFIELD_RO hello GET i8 16 +1) "108" +``` diff --git a/docs/src/content/docs/commands/BITOP.md b/docs/src/content/docs/commands/BITOP.md index 239efeb3e..cae554c3d 100644 --- a/docs/src/content/docs/commands/BITOP.md +++ b/docs/src/content/docs/commands/BITOP.md @@ -12,14 +12,15 @@ BITOP operation destkey key [key ...] ``` ## Parameters +| Parameter | Description | Type | Required | +|-------------------|--------------------------------------------------------------------------------|--------------|----------| +| `AND` | Perform a bitwise AND operation. | Operation | Yes | +| `OR` | Perform a bitwise OR operation. | Operation | Yes | +| `XOR` | Perform a bitwise XOR operation. | Operation | Yes | +| `NOT` | Perform a bitwise NOT operation (only one key is allowed for this operation). | Operation | Yes | +| `destkey` | The key where the result of the bitwise operation will be stored. | String | Yes | +| `key [key ...]` | One or more keys containing the strings to be used in the bitwise operation. For the `NOT` operation, only one key is allowed.| String | Yes | -- `operation`: The bitwise operation to perform. It can be one of the following: - - `AND`: Perform a bitwise AND operation. - - `OR`: Perform a bitwise OR operation. - - `XOR`: Perform a bitwise XOR operation. - - `NOT`: Perform a bitwise NOT operation (only one key is allowed for this operation). -- `destkey`: The key where the result of the bitwise operation will be stored. -- `key [key ...]`: One or more keys containing the strings to be used in the bitwise operation. For the `NOT` operation, only one key is allowed. ## Return Value @@ -61,10 +62,14 @@ The `BITOP` command can raise errors in the following cases: ### Example 1: Bitwise AND Operation ```bash -SET key1 "foo" -SET key2 "bar" -BITOP AND result key1 key2 -GET result +127.0.0.1:7379> SET key1 "foo" +OK +127.0.0.1:7379> SET key2 "bar" +OK +127.0.0.1:7379> BITOP AND result key1 key2 +(integer) 3 +127.0.0.1:7379> GET result +"bab" ``` `Explanation`: @@ -77,10 +82,14 @@ GET result ### Example 2: Bitwise OR Operation ```bash -SET key1 "foo" -SET key2 "bar" -BITOP OR result key1 key2 -GET result +127.0.0.1:7379> SET key1 "foo" +OK +127.0.0.1:7379> SET key2 "bar" +OK +127.0.0.1:7379> BITOP OR result key1 key2 +(integer) 3 +127.0.0.1:7379> GET result +"fo\x7f" ``` `Explanation`: @@ -93,10 +102,14 @@ GET result ### Example 3: Bitwise XOR Operation ```bash -SET key1 "foo" -SET key2 "bar" -BITOP XOR result key1 key2 -GET result +127.0.0.1:7379> SET key1 "foo" +OK +127.0.0.1:7379> SET key2 "bar" +OK +127.0.0.1:7379> BITOP XOR result key1 key2 +(integer) 3 +127.0.0.1:7379> GET result +"\x04\x0e\x1d" ``` `Explanation`: @@ -109,9 +122,12 @@ GET result ### Example 4: Bitwise NOT Operation ```bash -SET key1 "foo" -BITOP NOT result key1 -GET result +127.0.0.1:7379> SET key1 "foo" +OK +127.0.0.1:7379> BITOP NOT result key1 +(integer) 3 +127.0.0.1:7379> GET result +"\x99\x90\x90" ``` `Explanation`: diff --git a/docs/src/content/docs/commands/BITPOS.md b/docs/src/content/docs/commands/BITPOS.md index 9fb738997..4aa0e4a28 100644 --- a/docs/src/content/docs/commands/BITPOS.md +++ b/docs/src/content/docs/commands/BITPOS.md @@ -13,15 +13,20 @@ BITPOS key bit [start] [end] ## Parameters -- `key`: The key of the string in which to search for the bit. -- `bit`: The bit value to search for, which can be either 0 or 1. -- `start`: (Optional) The starting byte position to begin the search. If not specified, the search starts from the beginning of the string. -- `end`: (Optional) The ending byte position to end the search. If not specified, the search continues until the end of the string. +| Parameter | Description | Type | Required | +|-----------|--------------------------------------------------------------------------------------------------------------------------------------|----------|----------| +| `key` | The key of the string in which to search for the bit. | String | Yes | +| `bit` | The value to be set for the key. | Bit | Yes | +| `start` | (Optional) The starting byte position to begin the search. If not specified, the search starts from the beginning of the string. | Integer | No | +| `end` | (Optional) The ending byte position to end the search. If not specified, the search continues until the end of the string. | Integer | No | ## Return Value -- `Integer`: The position of the first bit set to the specified value (0 or 1). The position is returned as a zero-based integer. -- `-1`: If the specified bit is not found within the specified range. +| Condition | Return Value | +|----------------------------------------------------------------|-------------------------| +| Command is successful | `Integer` | +| If the specified bit is not found within the specified range. | `-1` | +| Syntax or specified constraints are invalid | error | ## Behaviour @@ -50,14 +55,10 @@ The `BITPOS` command can raise errors in the following cases: Find the position of the first bit set to 1 in the string stored at key `mykey`: ```bash -SET mykey "foobar" -BITPOS mykey 1 -``` - -`Output`: - -```bash -6 +127.0.0.1:7379> SET mykey "foobar" +OK +127.0.0.1:7379> BITPOS mykey 1 +(integer) 1 ``` ### Specifying a Range @@ -65,29 +66,20 @@ BITPOS mykey 1 Find the position of the first bit set to 0 in the string stored at key `mykey`, starting from byte position 2 and ending at byte position 4: ```bash -SET mykey "foobar" -BITPOS mykey 0 2 4 +127.0.0.1:7379> SET mykey "foobar" +OK +127.0.0.1:7379> BITPOS mykey 0 2 4 +(integer) 16 ``` - -`Output`: - -```bash -16 -``` - ### Bit Not Found If the specified bit is not found within the specified range, the command returns -1: ```bash -SET mykey "foobar" -BITPOS mykey 1 2 4 -``` - -`Output`: - -```bash --1 +127.0.0.1:7379> SET mykey "foobar" +OK +127.0.0.1:7379> BITPOS mykey 1 2 4 +(integer) -1 ``` ### Error Cases @@ -97,13 +89,9 @@ BITPOS mykey 1 2 4 Attempting to use `BITPOS` on a key that holds a non-string value: ```bash -LPUSH mylist "item" -BITPOS mylist 1 -``` - -`Output`: - -```bash +127.0.0.1:7379> LPUSH mylist "item" +(integer) 1 +127.0.0.1:7379> BITPOS mylist 1 (error) WRONGTYPE Operation against a key holding the wrong kind of value ``` @@ -112,13 +100,9 @@ BITPOS mylist 1 Using a bit value other than 0 or 1: ```bash -SET mykey "foobar" -BITPOS mykey 2 -``` - -`Output`: - -```bash +127.0.0.1:7379> SET mykey "foobar" +OK +127.0.0.1:7379> BITPOS mykey 2 (error) ERR bit is not an integer or out of range ``` @@ -127,12 +111,8 @@ BITPOS mykey 2 Using non-integer values for the `start` or `end` parameters: ```bash -SET mykey "foobar" -BITPOS mykey 1 "a" "b" -``` - -`Output`: - -```bash +127.0.0.1:7379> SET mykey "foobar" +OK +127.0.0.1:7379> BITPOS mykey 1 "a" "b" (error) ERR value is not an integer or out of range ``` diff --git a/docs/src/content/docs/commands/SETBIT.md b/docs/src/content/docs/commands/SETBIT.md index f4cb272ef..eabbada64 100644 --- a/docs/src/content/docs/commands/SETBIT.md +++ b/docs/src/content/docs/commands/SETBIT.md @@ -12,10 +12,11 @@ SETBIT key offset value ``` ## Parameters - -- `key`: The key of the string where the bit is to be set or cleared. If the key does not exist, a new string value is created. -- `offset`: The position of the bit to be set or cleared. The offset is a zero-based integer, meaning the first bit is at position 0. -- `value`: The value to set the bit to. This must be either 0 or 1. +| Parameter | Description | Type | Required | +|-----------|--------------------------------------------------------------------------------------------------------------------------|---------|----------| +| `key` | The key of the string where the bit is to be set or cleared. If the key does not exist, a new string value is created. | String | Yes | +| `offset` | The position of the bit to be set or cleared. The offset is a zero-based integer, meaning the first bit is at position 0.| Integer | Yes | +| `value` | The value to set the bit to. This must be either 0 or 1. | Bit | Yes | ## Return Value @@ -28,17 +29,22 @@ The command returns the original bit value stored at the specified offset before - The command only affects the bit at the specified offset and leaves all other bits unchanged. ## Error Handling +1. `Bit is not an integer or out of range`: + - Error Message: `(error) ERR bit is not an integer or out of range` + - This error is raised if the `offset` is not a valid integer or if it is negative. + - This error is also raised if the `value` is not 0 or 1. -- `ERR bit is not an integer or out of range`: This error is raised if the `offset` is not a valid integer or if it is negative. -- `ERR bit is not an integer or out of range`: This error is also raised if the `value` is not 0 or 1. -- `WRONGTYPE Operation against a key holding the wrong kind of value`: This error is raised if the key exists but does not hold a string value. +2. `WRONGTYPE Operation against a key holding the wrong kind of value`: + - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + - This error is raised if the key exists but does not hold a bit string value. ## Example Usage ### Setting a Bit ```bash -SETBIT mykey 7 1 +127.0.0.1:7379> SETBIT mykey 7 1 +(integer) 0 ``` This command sets the bit at offset 7 in the string value stored at `mykey` to 1. If `mykey` does not exist, it is created and padded with zero bits up to the 7th bit. @@ -46,7 +52,8 @@ This command sets the bit at offset 7 in the string value stored at `mykey` to 1 ### Clearing a Bit ```bash -SETBIT mykey 7 0 +127.0.0.1:7379> SETBIT mykey 7 0 +(integer) 1 ``` This command clears the bit at offset 7 in the string value stored at `mykey` to 0. @@ -54,7 +61,8 @@ This command clears the bit at offset 7 in the string value stored at `mykey` to ### Checking the Original Bit Value ```bash -SETBIT mykey 7 1 +127.0.0.1:7379> SETBIT mykey 7 1 +(integer) 0 ``` If the bit at offset 7 was previously 0, this command will return 0 and then set the bit to 1. @@ -62,7 +70,8 @@ If the bit at offset 7 was previously 0, this command will return 0 and then set ### Extending the String ```bash -SETBIT mykey 100 1 +127.0.0.1:7379> SETBIT mykey 100 1 +(integer) 0 ``` If the string stored at `mykey` is shorter than 101 bits, it will be extended, and all new bits will be set to 0 except for the bit at offset 100, which will be set to 1. @@ -72,7 +81,8 @@ If the string stored at `mykey` is shorter than 101 bits, it will be extended, a ### Invalid Offset ```bash -SETBIT mykey -1 1 +127.0.0.1:7379> SETBIT mykey -1 1 +(error) ERR bit is not an integer or out of range ``` This command will raise an error: `ERR bit is not an integer or out of range` because the offset is negative. @@ -80,7 +90,8 @@ This command will raise an error: `ERR bit is not an integer or out of range` be ### Invalid Value ```bash -SETBIT mykey 7 2 +127.0.0.1:7379> SETBIT mykey 7 2 +(error) ERR bit is not an integer or out of range ``` This command will raise an error: `ERR bit is not an integer or out of range` because the value is not 0 or 1. @@ -88,8 +99,10 @@ This command will raise an error: `ERR bit is not an integer or out of range` be ### Wrong Type ```bash -SET mykey "Hello" -SETBIT mykey 7 1 +127.0.0.1:7379> SET mykey "Hello" +OK +127.0.0.1:7379> SETBIT mykey 7 1 +(error) WRONGTYPE Operation against a key holding the wrong kind of value ``` If `mykey` holds a string value that is not a bit string, the `SETBIT` command will raise an error: `WRONGTYPE Operation against a key holding the wrong kind of value`. diff --git a/integration_tests/commands/async/bit_operation_test.go b/integration_tests/commands/async/bit_operation_test.go deleted file mode 100644 index 959745f58..000000000 --- a/integration_tests/commands/async/bit_operation_test.go +++ /dev/null @@ -1,564 +0,0 @@ -package async - -import ( - "fmt" - "math" - "net" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBitOp(t *testing.T) { - conn := getLocalConnection() - testcases := []struct { - InCmds []string - Out []interface{} - }{ - { - InCmds: []string{"SETBIT unitTestKeyA 1 1", "SETBIT unitTestKeyA 3 1", "SETBIT unitTestKeyA 5 1", "SETBIT unitTestKeyA 7 1", "SETBIT unitTestKeyA 8 1"}, - Out: []interface{}{int64(0), int64(0), int64(0), int64(0), int64(0)}, - }, - { - InCmds: []string{"SETBIT unitTestKeyB 2 1", "SETBIT unitTestKeyB 4 1", "SETBIT unitTestKeyB 7 1"}, - Out: []interface{}{int64(0), int64(0), int64(0)}, - }, - { - InCmds: []string{"SET foo bar", "SETBIT foo 2 1", "SETBIT foo 4 1", "SETBIT foo 7 1", "GET foo"}, - Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"}, - }, - { - InCmds: []string{"SET mykey12 1343", "SETBIT mykey12 2 1", "SETBIT mykey12 4 1", "SETBIT mykey12 7 1", "GET mykey12"}, - Out: []interface{}{"OK", int64(1), int64(0), int64(1), int64(9343)}, - }, - { - InCmds: []string{"SET foo12 bar", "SETBIT foo12 2 1", "SETBIT foo12 4 1", "SETBIT foo12 7 1", "GET foo12"}, - Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"}, - }, - { - InCmds: []string{"BITOP NOT unitTestKeyNOT unitTestKeyA "}, - Out: []interface{}{int64(2)}, - }, - { - InCmds: []string{"GETBIT unitTestKeyNOT 1", "GETBIT unitTestKeyNOT 2", "GETBIT unitTestKeyNOT 7", "GETBIT unitTestKeyNOT 8", "GETBIT unitTestKeyNOT 9"}, - Out: []interface{}{int64(0), int64(1), int64(0), int64(0), int64(1)}, - }, - { - InCmds: []string{"BITOP OR unitTestKeyOR unitTestKeyB unitTestKeyA"}, - Out: []interface{}{int64(2)}, - }, - { - InCmds: []string{"GETBIT unitTestKeyOR 1", "GETBIT unitTestKeyOR 2", "GETBIT unitTestKeyOR 3", "GETBIT unitTestKeyOR 7", "GETBIT unitTestKeyOR 8", "GETBIT unitTestKeyOR 9", "GETBIT unitTestKeyOR 12"}, - Out: []interface{}{int64(1), int64(1), int64(1), int64(1), int64(1), int64(0), int64(0)}, - }, - { - InCmds: []string{"BITOP AND unitTestKeyAND unitTestKeyB unitTestKeyA"}, - Out: []interface{}{int64(2)}, - }, - { - InCmds: []string{"GETBIT unitTestKeyAND 1", "GETBIT unitTestKeyAND 2", "GETBIT unitTestKeyAND 7", "GETBIT unitTestKeyAND 8", "GETBIT unitTestKeyAND 9"}, - Out: []interface{}{int64(0), int64(0), int64(1), int64(0), int64(0)}, - }, - { - InCmds: []string{"BITOP XOR unitTestKeyXOR unitTestKeyB unitTestKeyA"}, - Out: []interface{}{int64(2)}, - }, - { - InCmds: []string{"GETBIT unitTestKeyXOR 1", "GETBIT unitTestKeyXOR 2", "GETBIT unitTestKeyXOR 3", "GETBIT unitTestKeyXOR 7", "GETBIT unitTestKeyXOR 8"}, - Out: []interface{}{int64(1), int64(1), int64(1), int64(0), int64(1)}, - }, - } - - for _, tcase := range testcases { - for i := 0; i < len(tcase.InCmds); i++ { - cmd := tcase.InCmds[i] - out := tcase.Out[i] - assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd) - } - } -} - -func TestBitCount(t *testing.T) { - conn := getLocalConnection() - testcases := []struct { - InCmds []string - Out []interface{} - }{ - { - InCmds: []string{"SETBIT mykey 7 1"}, - Out: []interface{}{int64(0)}, - }, - { - InCmds: []string{"SETBIT mykey 7 1"}, - Out: []interface{}{int64(1)}, - }, - { - InCmds: []string{"SETBIT mykey 122 1"}, - Out: []interface{}{int64(0)}, - }, - { - InCmds: []string{"GETBIT mykey 122"}, - Out: []interface{}{int64(1)}, - }, - { - InCmds: []string{"SETBIT mykey 122 0"}, - Out: []interface{}{int64(1)}, - }, - { - InCmds: []string{"GETBIT mykey 122"}, - Out: []interface{}{int64(0)}, - }, - { - InCmds: []string{"GETBIT mykey 1223232"}, - Out: []interface{}{int64(0)}, - }, - { - InCmds: []string{"GETBIT mykey 7"}, - Out: []interface{}{int64(1)}, - }, - { - InCmds: []string{"GETBIT mykey 8"}, - Out: []interface{}{int64(0)}, - }, - { - InCmds: []string{"BITCOUNT mykey 3 7 BIT"}, - Out: []interface{}{int64(1)}, - }, - { - InCmds: []string{"BITCOUNT mykey 3 7"}, - Out: []interface{}{int64(0)}, - }, - { - InCmds: []string{"BITCOUNT mykey 0 0"}, - Out: []interface{}{int64(1)}, - }, - { - InCmds: []string{"BITCOUNT"}, - Out: []interface{}{"ERR wrong number of arguments for 'bitcount' command"}, - }, - { - InCmds: []string{"BITCOUNT mykey"}, - Out: []interface{}{int64(1)}, - }, - { - InCmds: []string{"BITCOUNT mykey 0"}, - Out: []interface{}{"ERR syntax error"}, - }, - } - - for _, tcase := range testcases { - for i := 0; i < len(tcase.InCmds); i++ { - cmd := tcase.InCmds[i] - out := tcase.Out[i] - assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd) - } - } -} - -func TestBitPos(t *testing.T) { - conn := getLocalConnection() - testcases := []struct { - name string - val interface{} - inCmd string - out interface{} - setCmdSETBIT bool - }{ - { - name: "String interval BIT 0,-1 ", - val: "\\x00\\xff\\x00", - inCmd: "BITPOS testkey 0 0 -1 bit", - out: int64(0), - }, - { - name: "String interval BIT 8,-1", - val: "\\x00\\xff\\x00", - inCmd: "BITPOS testkey 0 8 -1 bit", - out: int64(8), - }, - { - name: "String interval BIT 16,-1", - val: "\\x00\\xff\\x00", - inCmd: "BITPOS testkey 0 16 -1 bit", - out: int64(16), - }, - { - name: "String interval BIT 16,200", - val: "\\x00\\xff\\x00", - inCmd: "BITPOS testkey 0 16 200 bit", - out: int64(16), - }, - { - name: "String interval BIT 8,8", - val: "\\x00\\xff\\x00", - inCmd: "BITPOS testkey 0 8 8 bit", - out: int64(8), - }, - { - name: "FindsFirstZeroBit", - val: "\xff\xf0\x00", - inCmd: "BITPOS testkey 0", - out: int64(12), - }, - { - name: "FindsFirstOneBit", - val: "\x00\x0f\xff", - inCmd: "BITPOS testkey 1", - out: int64(12), - }, - { - name: "NoOneBitFound", - val: "\x00\x00\x00", - inCmd: "BITPOS testkey 1", - out: int64(-1), - }, - { - name: "NoZeroBitFound", - val: "\xff\xff\xff", - inCmd: "BITPOS testkey 0", - out: int64(24), - }, - { - name: "NoZeroBitFoundWithRangeStartPos", - val: "\xff\xff\xff", - inCmd: "BITPOS testkey 0 2", - out: int64(24), - }, - { - name: "NoZeroBitFoundWithOOBRangeStartPos", - val: "\xff\xff\xff", - inCmd: "BITPOS testkey 0 4", - out: int64(-1), - }, - { - name: "NoZeroBitFoundWithRange", - val: "\xff\xff\xff", - inCmd: "BITPOS testkey 0 2 2", - out: int64(-1), - }, - { - name: "NoZeroBitFoundWithRangeAndRangeType", - val: "\xff\xff\xff", - inCmd: "BITPOS testkey 0 2 2 BIT", - out: int64(-1), - }, - { - name: "FindsFirstZeroBitInRange", - val: "\xff\xf0\xff", - inCmd: "BITPOS testkey 0 1 2", - out: int64(12), - }, - { - name: "FindsFirstOneBitInRange", - val: "\x00\x00\xf0", - inCmd: "BITPOS testkey 1 2 3", - out: int64(16), - }, - { - name: "StartGreaterThanEnd", - val: "\xff\xf0\x00", - inCmd: "BITPOS testkey 0 3 2", - out: int64(-1), - }, - { - name: "FindsFirstOneBitWithNegativeStart", - val: "\x00\x00\xf0", - inCmd: "BITPOS testkey 1 -2 -1", - out: int64(16), - }, - { - name: "FindsFirstZeroBitWithNegativeEnd", - val: "\xff\xf0\xff", - inCmd: "BITPOS testkey 0 1 -1", - out: int64(12), - }, - { - name: "FindsFirstZeroBitInByteRange", - val: "\xff\x00\xff", - inCmd: "BITPOS testkey 0 1 2 BYTE", - out: int64(8), - }, - { - name: "FindsFirstOneBitInBitRange", - val: "\x00\x01\x00", - inCmd: "BITPOS testkey 1 0 16 BIT", - out: int64(15), - }, - { - name: "NoBitFoundInByteRange", - val: "\xff\xff\xff", - inCmd: "BITPOS testkey 0 0 2 BYTE", - out: int64(-1), - }, - { - name: "NoBitFoundInBitRange", - val: "\x00\x00\x00", - inCmd: "BITPOS testkey 1 0 23 BIT", - out: int64(-1), - }, - { - name: "EmptyStringReturnsMinusOneForZeroBit", - val: "\"\"", - inCmd: "BITPOS testkey 0", - out: int64(-1), - }, - { - name: "EmptyStringReturnsMinusOneForOneBit", - val: "\"\"", - inCmd: "BITPOS testkey 1", - out: int64(-1), - }, - { - name: "SingleByteString", - val: "\x80", - inCmd: "BITPOS testkey 1", - out: int64(0), - }, - { - name: "RangeExceedsStringLength", - val: "\x00\xff", - inCmd: "BITPOS testkey 1 0 20 BIT", - out: int64(8), - }, - { - name: "InvalidBitArgument", - inCmd: "BITPOS testkey 2", - out: "ERR the bit argument must be 1 or 0", - }, - { - name: "NonIntegerStartParameter", - inCmd: "BITPOS testkey 0 start", - out: "ERR value is not an integer or out of range", - }, - { - name: "NonIntegerEndParameter", - inCmd: "BITPOS testkey 0 1 end", - out: "ERR value is not an integer or out of range", - }, - { - name: "InvalidRangeType", - inCmd: "BITPOS testkey 0 1 2 BYTEs", - out: "ERR syntax error", - }, - { - name: "InsufficientArguments", - inCmd: "BITPOS testkey", - out: "ERR wrong number of arguments for 'bitpos' command", - }, - { - name: "NonExistentKeyForZeroBit", - inCmd: "BITPOS nonexistentkey 0", - out: int64(0), - }, - { - name: "NonExistentKeyForOneBit", - inCmd: "BITPOS nonexistentkey 1", - out: int64(-1), - }, - { - name: "IntegerValue", - val: 65280, // 0xFF00 in decimal - inCmd: "BITPOS testkey 0", - out: int64(0), - }, - { - name: "LargeIntegerValue", - val: 16777215, // 0xFFFFFF in decimal - inCmd: "BITPOS testkey 1", - out: int64(2), - }, - { - name: "SmallIntegerValue", - val: 1, // 0x01 in decimal - inCmd: "BITPOS testkey 0", - out: int64(0), - }, - { - name: "ZeroIntegerValue", - val: 0, - inCmd: "BITPOS testkey 1", - out: int64(2), - }, - { - name: "BitRangeStartGreaterThanBitLength", - val: "\xff\xff\xff", - inCmd: "BITPOS testkey 0 25 30 BIT", - out: int64(-1), - }, - { - name: "BitRangeEndExceedsBitLength", - val: "\xff\xff\xff", - inCmd: "BITPOS testkey 0 0 30 BIT", - out: int64(-1), - }, - { - name: "NegativeStartInBitRange", - val: "\x00\xff\xff", - inCmd: "BITPOS testkey 1 -16 -1 BIT", - out: int64(8), - }, - { - name: "LargeNegativeStart", - val: "\x00\xff\xff", - inCmd: "BITPOS testkey 1 -100 -1", - out: int64(8), - }, - { - name: "LargePositiveEnd", - val: "\x00\xff\xff", - inCmd: "BITPOS testkey 1 0 100", - out: int64(8), - }, - { - name: "StartAndEndEqualInByteRange", - val: "\x0f\xff\xff", - inCmd: "BITPOS testkey 0 1 1 BYTE", - out: int64(-1), - }, - { - name: "StartAndEndEqualInBitRange", - val: "\x0f\xff\xff", - inCmd: "BITPOS testkey 1 1 1 BIT", - out: int64(-1), - }, - { - name: "FindFirstZeroBitInNegativeRange", - val: "\xff\x00\xff", - inCmd: "BITPOS testkey 0 -2 -1", - out: int64(8), - }, - { - name: "FindFirstOneBitInNegativeRangeBIT", - val: "\x00\x00\x80", - inCmd: "BITPOS testkey 1 -8 -1 BIT", - out: int64(16), - }, - { - name: "MaxIntegerValue", - val: math.MaxInt64, - inCmd: "BITPOS testkey 0", - out: int64(0), - }, - { - name: "MinIntegerValue", - val: math.MinInt64, - inCmd: "BITPOS testkey 1", - out: int64(2), - }, - { - name: "SingleBitStringZero", - val: "\x00", - inCmd: "BITPOS testkey 1", - out: int64(-1), - }, - { - name: "SingleBitStringOne", - val: "\x01", - inCmd: "BITPOS testkey 0", - out: int64(0), - }, - { - name: "AllBitsSetExceptLast", - val: "\xff\xff\xfe", - inCmd: "BITPOS testkey 0", - out: int64(23), - }, - { - name: "OnlyLastBitSet", - val: "\x00\x00\x01", - inCmd: "BITPOS testkey 1", - out: int64(23), - }, - { - name: "AlternatingBitsLongString", - val: "\xaa\xaa\xaa\xaa\xaa", - inCmd: "BITPOS testkey 0", - out: int64(1), - }, - { - name: "VeryLargeByteString", - val: strings.Repeat("\xff", 1000) + "\x00", - inCmd: "BITPOS testkey 0", - out: int64(8000), - }, - { - name: "FindZeroBitOnSetBitKey", - val: "8 1", - inCmd: "BITPOS testkeysb 1", - out: int64(8), - setCmdSETBIT: true, - }, - { - name: "FindOneBitOnSetBitKey", - val: "1 1", - inCmd: "BITPOS testkeysb 1", - out: int64(1), - setCmdSETBIT: true, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - var setCmd string - if tc.setCmdSETBIT { - setCmd = fmt.Sprintf("SETBIT testkeysb %s", tc.val.(string)) - } else { - switch v := tc.val.(type) { - case string: - setCmd = fmt.Sprintf("SET testkey %s", v) - case int: - setCmd = fmt.Sprintf("SET testkey %d", v) - default: - // For test cases where we don't set a value (e.g., error cases) - setCmd = "" - } - } - - if setCmd != "" { - FireCommand(conn, setCmd) - } - - result := FireCommand(conn, tc.inCmd) - assert.Equal(t, tc.out, result, "Mismatch for cmd %s\n", tc.inCmd) - }) - } -} - -func generateSetBitCommand(connection net.Conn, bitPosition int) int64 { - command := fmt.Sprintf("SETBIT unitTestKeyA %d 1", bitPosition) - responseValue := FireCommand(connection, command) - if responseValue == nil { - return -1 - } - return responseValue.(int64) -} - -func BenchmarkSetBitCommand(b *testing.B) { - connection := getLocalConnection() - for n := 0; n < 1000; n++ { - setBitCommand := generateSetBitCommand(connection, n) - if setBitCommand < 0 { - b.Fail() - } - } -} - -func generateGetBitCommand(connection net.Conn, bitPosition int) int64 { - command := fmt.Sprintf("GETBIT unitTestKeyA %d", bitPosition) - responseValue := FireCommand(connection, command) - if responseValue == nil { - return -1 - } - return responseValue.(int64) -} - -func BenchmarkGetBitCommand(b *testing.B) { - connection := getLocalConnection() - for n := 0; n < 1000; n++ { - getBitCommand := generateGetBitCommand(connection, n) - if getBitCommand < 0 { - b.Fail() - } - } -} diff --git a/integration_tests/commands/async/bit_ops_string_int_test.go b/integration_tests/commands/async/bit_test.go similarity index 76% rename from integration_tests/commands/async/bit_ops_string_int_test.go rename to integration_tests/commands/async/bit_test.go index 13379fdfc..68f565c88 100644 --- a/integration_tests/commands/async/bit_ops_string_int_test.go +++ b/integration_tests/commands/async/bit_test.go @@ -8,6 +8,75 @@ import ( "github.com/stretchr/testify/assert" ) +func TestBitOp(t *testing.T) { + conn := getLocalConnection() + testcases := []struct { + InCmds []string + Out []interface{} + }{ + { + InCmds: []string{"SETBIT unitTestKeyA 1 1", "SETBIT unitTestKeyA 3 1", "SETBIT unitTestKeyA 5 1", "SETBIT unitTestKeyA 7 1", "SETBIT unitTestKeyA 8 1"}, + Out: []interface{}{int64(0), int64(0), int64(0), int64(0), int64(0)}, + }, + { + InCmds: []string{"SETBIT unitTestKeyB 2 1", "SETBIT unitTestKeyB 4 1", "SETBIT unitTestKeyB 7 1"}, + Out: []interface{}{int64(0), int64(0), int64(0)}, + }, + { + InCmds: []string{"SET foo bar", "SETBIT foo 2 1", "SETBIT foo 4 1", "SETBIT foo 7 1", "GET foo"}, + Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"}, + }, + { + InCmds: []string{"SET mykey12 1343", "SETBIT mykey12 2 1", "SETBIT mykey12 4 1", "SETBIT mykey12 7 1", "GET mykey12"}, + Out: []interface{}{"OK", int64(1), int64(0), int64(1), int64(9343)}, + }, + { + InCmds: []string{"SET foo12 bar", "SETBIT foo12 2 1", "SETBIT foo12 4 1", "SETBIT foo12 7 1", "GET foo12"}, + Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"}, + }, + { + InCmds: []string{"BITOP NOT unitTestKeyNOT unitTestKeyA "}, + Out: []interface{}{int64(2)}, + }, + { + InCmds: []string{"GETBIT unitTestKeyNOT 1", "GETBIT unitTestKeyNOT 2", "GETBIT unitTestKeyNOT 7", "GETBIT unitTestKeyNOT 8", "GETBIT unitTestKeyNOT 9"}, + Out: []interface{}{int64(0), int64(1), int64(0), int64(0), int64(1)}, + }, + { + InCmds: []string{"BITOP OR unitTestKeyOR unitTestKeyB unitTestKeyA"}, + Out: []interface{}{int64(2)}, + }, + { + InCmds: []string{"GETBIT unitTestKeyOR 1", "GETBIT unitTestKeyOR 2", "GETBIT unitTestKeyOR 3", "GETBIT unitTestKeyOR 7", "GETBIT unitTestKeyOR 8", "GETBIT unitTestKeyOR 9", "GETBIT unitTestKeyOR 12"}, + Out: []interface{}{int64(1), int64(1), int64(1), int64(1), int64(1), int64(0), int64(0)}, + }, + { + InCmds: []string{"BITOP AND unitTestKeyAND unitTestKeyB unitTestKeyA"}, + Out: []interface{}{int64(2)}, + }, + { + InCmds: []string{"GETBIT unitTestKeyAND 1", "GETBIT unitTestKeyAND 2", "GETBIT unitTestKeyAND 7", "GETBIT unitTestKeyAND 8", "GETBIT unitTestKeyAND 9"}, + Out: []interface{}{int64(0), int64(0), int64(1), int64(0), int64(0)}, + }, + { + InCmds: []string{"BITOP XOR unitTestKeyXOR unitTestKeyB unitTestKeyA"}, + Out: []interface{}{int64(2)}, + }, + { + InCmds: []string{"GETBIT unitTestKeyXOR 1", "GETBIT unitTestKeyXOR 2", "GETBIT unitTestKeyXOR 3", "GETBIT unitTestKeyXOR 7", "GETBIT unitTestKeyXOR 8"}, + Out: []interface{}{int64(1), int64(1), int64(1), int64(0), int64(1)}, + }, + } + + for _, tcase := range testcases { + for i := 0; i < len(tcase.InCmds); i++ { + cmd := tcase.InCmds[i] + out := tcase.Out[i] + assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd) + } + } +} + func TestBitOpsString(t *testing.T) { // test code diff --git a/integration_tests/commands/async/bitfield_test.go b/integration_tests/commands/async/bitfield_test.go deleted file mode 100644 index 4664e8ebe..000000000 --- a/integration_tests/commands/async/bitfield_test.go +++ /dev/null @@ -1,377 +0,0 @@ -package async - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestBitfield(t *testing.T) { - conn := getLocalConnection() - defer conn.Close() - - FireCommand(conn, "FLUSHDB") - defer FireCommand(conn, "FLUSHDB") // clean up after all test cases - syntaxErrMsg := "ERR syntax error" - bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is." - integerErrMsg := "ERR value is not an integer or out of range" - overflowErrMsg := "ERR Invalid OVERFLOW type specified" - - testCases := []struct { - Name string - Commands []string - Expected []interface{} - Delay []time.Duration - CleanUp []string - }{ - { - Name: "BITFIELD Arity Check", - Commands: []string{"bitfield"}, - Expected: []interface{}{"ERR wrong number of arguments for 'bitfield' command"}, - Delay: []time.Duration{0}, - CleanUp: []string{}, - }, - { - Name: "BITFIELD on unsupported type of SET", - Commands: []string{"SADD bits a b c", "bitfield bits"}, - Expected: []interface{}{int64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"}, - Delay: []time.Duration{0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD on unsupported type of JSON", - Commands: []string{"json.set bits $ 1", "bitfield bits"}, - Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, - Delay: []time.Duration{0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD on unsupported type of HSET", - Commands: []string{"HSET bits a 1", "bitfield bits"}, - Expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, - Delay: []time.Duration{0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD with syntax errors", - Commands: []string{ - "bitfield bits set u8 0 255 incrby u8 0 100 get u8", - "bitfield bits set a8 0 255 incrby u8 0 100 get u8", - "bitfield bits set u8 a 255 incrby u8 0 100 get u8", - "bitfield bits set u8 0 255 incrby u8 0 100 overflow wraap", - "bitfield bits set u8 0 incrby u8 0 100 get u8 288", - }, - Expected: []interface{}{ - syntaxErrMsg, - bitFieldTypeErrMsg, - "ERR bit offset is not an integer or out of range", - overflowErrMsg, - integerErrMsg, - }, - Delay: []time.Duration{0, 0, 0, 0, 0}, - CleanUp: []string{"Del bits"}, - }, - { - Name: "BITFIELD signed SET and GET basics", - Commands: []string{"bitfield bits set i8 0 -100", "bitfield bits set i8 0 101", "bitfield bits get i8 0"}, - Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(-100)}, []interface{}{int64(101)}}, - Delay: []time.Duration{0, 0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD unsigned SET and GET basics", - Commands: []string{"bitfield bits set u8 0 255", "bitfield bits set u8 0 100", "bitfield bits get u8 0"}, - Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(255)}, []interface{}{int64(100)}}, - Delay: []time.Duration{0, 0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD signed SET and GET together", - Commands: []string{"bitfield bits set i8 0 255 set i8 0 100 get i8 0"}, - Expected: []interface{}{[]interface{}{int64(0), int64(-1), int64(100)}}, - Delay: []time.Duration{0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD unsigned with SET, GET and INCRBY arguments", - Commands: []string{"bitfield bits set u8 0 255 incrby u8 0 100 get u8 0"}, - Expected: []interface{}{[]interface{}{int64(0), int64(99), int64(99)}}, - Delay: []time.Duration{0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD with only key as argument", - Commands: []string{"bitfield bits"}, - Expected: []interface{}{[]interface{}{}}, - Delay: []time.Duration{0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD # form", - Commands: []string{ - "bitfield bits set u8 #0 65", - "bitfield bits set u8 #1 66", - "bitfield bits set u8 #2 67", - "get bits", - }, - Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(0)}, "ABC"}, - Delay: []time.Duration{0, 0, 0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD basic INCRBY form", - Commands: []string{ - "bitfield bits set u8 #0 10", - "bitfield bits incrby u8 #0 100", - "bitfield bits incrby u8 #0 100", - }, - Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110)}, []interface{}{int64(210)}}, - Delay: []time.Duration{0, 0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD chaining of multiple commands", - Commands: []string{ - "bitfield bits set u8 #0 10", - "bitfield bits incrby u8 #0 100 incrby u8 #0 100", - }, - Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110), int64(210)}}, - Delay: []time.Duration{0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD unsigned overflow wrap", - Commands: []string{ - "bitfield bits set u8 #0 100", - "bitfield bits overflow wrap incrby u8 #0 257", - "bitfield bits get u8 #0", - "bitfield bits overflow wrap incrby u8 #0 255", - "bitfield bits get u8 #0", - }, - Expected: []interface{}{ - []interface{}{int64(0)}, - []interface{}{int64(101)}, - []interface{}{int64(101)}, - []interface{}{int64(100)}, - []interface{}{int64(100)}, - }, - Delay: []time.Duration{0, 0, 0, 0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD unsigned overflow sat", - Commands: []string{ - "bitfield bits set u8 #0 100", - "bitfield bits overflow sat incrby u8 #0 257", - "bitfield bits get u8 #0", - "bitfield bits overflow sat incrby u8 #0 -255", - "bitfield bits get u8 #0", - }, - Expected: []interface{}{ - []interface{}{int64(0)}, - []interface{}{int64(255)}, - []interface{}{int64(255)}, - []interface{}{int64(0)}, - []interface{}{int64(0)}, - }, - Delay: []time.Duration{0, 0, 0, 0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD signed overflow wrap", - Commands: []string{ - "bitfield bits set i8 #0 100", - "bitfield bits overflow wrap incrby i8 #0 257", - "bitfield bits get i8 #0", - "bitfield bits overflow wrap incrby i8 #0 255", - "bitfield bits get i8 #0", - }, - Expected: []interface{}{ - []interface{}{int64(0)}, - []interface{}{int64(101)}, - []interface{}{int64(101)}, - []interface{}{int64(100)}, - []interface{}{int64(100)}, - }, - Delay: []time.Duration{0, 0, 0, 0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD signed overflow sat", - Commands: []string{ - "bitfield bits set u8 #0 100", - "bitfield bits overflow sat incrby i8 #0 257", - "bitfield bits get i8 #0", - "bitfield bits overflow sat incrby i8 #0 -255", - "bitfield bits get i8 #0", - }, - Expected: []interface{}{ - []interface{}{int64(0)}, - []interface{}{int64(127)}, - []interface{}{int64(127)}, - []interface{}{int64(-128)}, - []interface{}{int64(-128)}, - }, - Delay: []time.Duration{0, 0, 0, 0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD regression 1", - Commands: []string{"set bits 1", "bitfield bits get u1 0"}, - Expected: []interface{}{"OK", []interface{}{int64(0)}}, - Delay: []time.Duration{0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD regression 2", - Commands: []string{ - "bitfield mystring set i8 0 10", - "bitfield mystring set i8 64 10", - "bitfield mystring incrby i8 10 99900", - }, - Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(60)}}, - Delay: []time.Duration{0, 0, 0}, - CleanUp: []string{"DEL mystring"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - - for i := 0; i < len(tc.Commands); i++ { - if tc.Delay[i] > 0 { - time.Sleep(tc.Delay[i]) - } - result := FireCommand(conn, tc.Commands[i]) - expected := tc.Expected[i] - assert.Equal(t, expected, result) - } - - for _, cmd := range tc.CleanUp { - FireCommand(conn, cmd) - } - }) - } -} - -func TestBitfieldRO(t *testing.T) { - conn := getLocalConnection() - defer conn.Close() - - FireCommand(conn, "FLUSHDB") - defer FireCommand(conn, "FLUSHDB") - - syntaxErrMsg := "ERR syntax error" - bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is." - unsupportedCmdErrMsg := "ERR BITFIELD_RO only supports the GET subcommand" - - testCases := []struct { - Name string - Commands []string - Expected []interface{} - Delay []time.Duration - CleanUp []string - }{ - { - Name: "BITFIELD_RO Arity Check", - Commands: []string{"bitfield_ro"}, - Expected: []interface{}{"ERR wrong number of arguments for 'bitfield_ro' command"}, - Delay: []time.Duration{0}, - CleanUp: []string{}, - }, - { - Name: "BITFIELD_RO on unsupported type of SET", - Commands: []string{"SADD bits a b c", "bitfield_ro bits"}, - Expected: []interface{}{int64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"}, - Delay: []time.Duration{0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD_RO on unsupported type of JSON", - Commands: []string{"json.set bits $ 1", "bitfield_ro bits"}, - Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, - Delay: []time.Duration{0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD_RO on unsupported type of HSET", - Commands: []string{"HSET bits a 1", "bitfield_ro bits"}, - Expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, - Delay: []time.Duration{0, 0}, - CleanUp: []string{"DEL bits"}, - }, - { - Name: "BITFIELD_RO with unsupported commands", - Commands: []string{ - "bitfield_ro bits set u8 0 255", - "bitfield_ro bits incrby u8 0 100", - }, - Expected: []interface{}{ - unsupportedCmdErrMsg, - unsupportedCmdErrMsg, - }, - Delay: []time.Duration{0, 0}, - CleanUp: []string{"Del bits"}, - }, - { - Name: "BITFIELD_RO with syntax error", - Commands: []string{ - "set bits 1", - "bitfield_ro bits get u8", - "bitfield_ro bits get", - "bitfield_ro bits get somethingrandom", - }, - Expected: []interface{}{ - "OK", - syntaxErrMsg, - syntaxErrMsg, - syntaxErrMsg, - }, - Delay: []time.Duration{0, 0, 0, 0}, - CleanUp: []string{"Del bits"}, - }, - { - Name: "BITFIELD_RO with invalid bitfield type", - Commands: []string{ - "set bits 1", - "bitfield_ro bits get a8 0", - "bitfield_ro bits get s8 0", - "bitfield_ro bits get somethingrandom 0", - }, - Expected: []interface{}{ - "OK", - bitFieldTypeErrMsg, - bitFieldTypeErrMsg, - bitFieldTypeErrMsg, - }, - Delay: []time.Duration{0, 0, 0, 0}, - CleanUp: []string{"Del bits"}, - }, - { - Name: "BITFIELD_RO with only key as argument", - Commands: []string{"bitfield_ro bits"}, - Expected: []interface{}{[]interface{}{}}, - Delay: []time.Duration{0}, - CleanUp: []string{"DEL bits"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - - for i := 0; i < len(tc.Commands); i++ { - if tc.Delay[i] > 0 { - time.Sleep(tc.Delay[i]) - } - result := FireCommand(conn, tc.Commands[i]) - expected := tc.Expected[i] - assert.Equal(t, expected, result) - } - - for _, cmd := range tc.CleanUp { - FireCommand(conn, cmd) - } - }) - } -} diff --git a/integration_tests/commands/http/bit_test.go b/integration_tests/commands/http/bit_test.go index 783ec4249..a26456504 100644 --- a/integration_tests/commands/http/bit_test.go +++ b/integration_tests/commands/http/bit_test.go @@ -1,11 +1,570 @@ package http +// The following commands are a part of this test class: +// SETBIT, GETBIT, BITCOUNT, BITOP, BITPOS, BITFIELD, BITFIELD_RO + import ( "fmt" - "github.com/stretchr/testify/assert" + "math" + "strings" "testing" + "time" + + "github.com/stretchr/testify/assert" ) +// TODO: BITOP has not been migrated yet. Once done, we can uncomment the tests - please check accuracy and validate for expected values. + +// func TestBitOp(t *testing.T) { +// exec := NewHTTPCommandExecutor() + +// testcases := []struct { +// InCmds []HTTPCommand +// Out []interface{} +// }{ +// { +// InCmds: []HTTPCommand{ +// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{1, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{3, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{5, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{7, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{8, 1}}}, +// }, +// Out: []interface{}{float64(0), float64(0), float64(0), float64(0), float64(0)}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyB", "values": []interface{}{2, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyB", "values": []interface{}{4, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyB", "values": []interface{}{7, 1}}}, +// }, +// Out: []interface{}{float64(0), float64(0), float64(0)}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "bar"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{2, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{4, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{7, 1}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "foo"}}, +// }, +// Out: []interface{}{"OK", float64(1), float64(0), float64(0), "kar"}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "mykey12", "value": "1343"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey12", "values": []interface{}{2, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey12", "values": []interface{}{4, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey12", "values": []interface{}{7, 1}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "mykey12"}}, +// }, +// Out: []interface{}{"OK", float64(1), float64(0), float64(1), float64(9343)}, +// }, +// { +// InCmds: []HTTPCommand{{Command: "SET", Body: map[string]interface{}{"key": "foo12", "value": "bar"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo12", "values": []interface{}{2, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo12", "values": []interface{}{4, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo12", "values": []interface{}{7, 1}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "foo12"}}, +// }, +// Out: []interface{}{"OK", float64(1), float64(0), float64(0), "kar"}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "unitTestKeyNOT", "unitTestKeyA"}}}, +// }, +// Out: []interface{}{float64(2)}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{1}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{2}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{7}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{8}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{9}}}, +// }, +// Out: []interface{}{float64(0), float64(1), float64(0), float64(0), float64(1)}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "unitTestKeyOR", "unitTestKeyB", "unitTestKeyA"}}}, +// }, +// Out: []interface{}{float64(2)}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{1}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{2}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{3}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{7}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{8}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{9}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{12}}}, +// }, +// Out: []interface{}{float64(1), float64(1), float64(1), float64(1), float64(1), float64(0), float64(0)}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"AND", "unitTestKeyAND", "unitTestKeyB", "unitTestKeyA"}}}, +// }, +// Out: []interface{}{float64(2)}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{1}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{2}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{7}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{8}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{9}}}, +// }, +// Out: []interface{}{float64(0), float64(0), float64(1), float64(0), float64(0)}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "unitTestKeyXOR", "unitTestKeyB", "unitTestKeyA"}}}, +// }, +// Out: []interface{}{float64(2)}, +// }, +// { +// InCmds: []HTTPCommand{ +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{1}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{2}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{3}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{7}}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{8}}}, +// }, +// Out: []interface{}{float64(1), float64(1), float64(1), float64(0), float64(1)}, +// }, +// } + +// for _, tcase := range testcases { +// for i := 0; i < len(tcase.InCmds); i++ { +// cmd := tcase.InCmds[i] +// out := tcase.Out[i] +// res, _ := exec.FireCommand(cmd) +// assert.Equal(t, out, res, "Value mismatch for cmd %s\n.", cmd) +// } +// } +// } + +// func TestBitOpsString(t *testing.T) { + +// exec := NewHTTPCommandExecutor() + +// // foobar in bits is 01100110 01101111 01101111 01100010 01100001 01110010 +// fooBarBits := "011001100110111101101111011000100110000101110010" +// // randomly get 8 bits for testing +// testOffsets := make([]int, 8) + +// for i := 0; i < 8; i++ { +// testOffsets[i] = rand.Intn(len(fooBarBits)) +// } + +// getBitTestCommands := make([]HTTPCommand, 8+1) +// getBitTestExpected := make([]interface{}, 8+1) + +// getBitTestCommands[0] = HTTPCommand{Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}} +// getBitTestExpected[0] = "OK" + +// for i := 1; i < 8+1; i++ { +// getBitTestCommands[i] = HTTPCommand{Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "value": fmt.Sprintf("%d", testOffsets[i-1])}} +// getBitTestExpected[i] = float64(fooBarBits[testOffsets[i-1]] - '0') +// } + +// testCases := []struct { +// name string +// cmds []HTTPCommand +// expected []interface{} +// assertType []string +// }{ +// { +// name: "Getbit of a key containing a string", +// cmds: getBitTestCommands, +// expected: getBitTestExpected, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Getbit of a key containing an integer", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "10"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "0"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "1"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "2"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "3"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "4"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "5"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "6"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "7"}}, +// }, +// expected: []interface{}{"OK", float64(0), float64(0), float64(1), float64(1), float64(0), float64(0), float64(0), float64(1)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Getbit of a key containing an integer 2nd byte", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "10"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "8"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "9"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "10"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "11"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "12"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "13"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "14"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "15"}}, +// }, +// expected: []interface{}{"OK", float64(0), float64(0), float64(1), float64(1), float64(0), float64(0), float64(0), float64(0)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Getbit of a key with an offset greater than the length of the string in bits", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "100"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "48"}}, +// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "47"}}, +// }, +// expected: []interface{}{"OK", float64(0), float64(0), float64(0)}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitcount of a key containing a string", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{0, -1}}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo"}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{0, 0}}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1}}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1, "BYTE"}}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{5, 30, "BIT"}}}, +// }, +// expected: []interface{}{"OK", float64(26), float64(26), float64(4), float64(6), float64(6), float64(17)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitcount of a key containing an integer", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "10"}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{0, -1}}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo"}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{0, 0}}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1}}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1, "BYTE"}}}, +// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{5, 30, "BIT"}}}, +// }, +// expected: []interface{}{"OK", float64(5), float64(5), float64(3), float64(2), float64(2), float64(3)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Setbit of a key containing a string", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{7, 1}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "foo"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{49, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{50, 1}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "foo"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{49, 0}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "foo"}}, +// }, +// expected: []interface{}{"OK", float64(0), "goobar", float64(0), float64(0), "goobar`", float64(1), "goobar "}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Setbit of a key must not change the expiry of the key if expiry is set", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}}, +// {Command: "EXPIRE", Body: map[string]interface{}{"key": "foo", "values": []interface{}{100}}}, +// {Command: "TTL", Body: map[string]interface{}{"key": "foo"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{7, 1}}}, +// {Command: "TTL", Body: map[string]interface{}{"key": "foo"}}, +// }, +// expected: []interface{}{"OK", float64(1), float64(100), float64(0), float64(100)}, +// assertType: []string{"equal", "equal", "less", "equal", "less"}, +// }, +// { +// name: "Setbit of a key must not add expiry to the key if expiry is not set", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}}, +// {Command: "TTL", Body: map[string]interface{}{"key": "foo"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{7, 1}}}, +// {Command: "TTL", Body: map[string]interface{}{"key": "foo"}}, +// }, +// expected: []interface{}{"OK", float64(-1), float64(0), float64(-1)}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitop not of a key containing a string", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "baz", "foo"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "baz"}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "bazz", "baz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}}, +// }, +// expected: []interface{}{"OK", float64(6), "\\x99\\x90\\x90\\x9d\\x9e\\x8d", float64(6), "foobar"}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitop not of a key containing an integer", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": 10}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "baz", "foo"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "baz"}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "bazz", "baz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}}, +// }, +// expected: []interface{}{"OK", float64(2), "\\xce\\xcf", float64(2), float64(10)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Get a string created with setbit", +// cmds: []HTTPCommand{ +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{3, 1}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "foo"}}, +// }, +// expected: []interface{}{float64(0), float64(0), "P"}, +// assertType: []string{"equal", "equal", "equal"}, +// }, +// { +// name: "Bitop and of keys containing a string and get the destkey", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}}, +// {Command: "SET", Body: map[string]interface{}{"key": "baz", "value": "abcdef"}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"AND", "bazz", "foo", "baz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}}, +// }, +// expected: []interface{}{"OK", "OK", float64(6), "`bc`ab"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP AND of keys containing integers and get the destkey", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": 10}}, +// {Command: "SET", Body: map[string]interface{}{"key": "baz", "value": 5}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"AND", "bazz", "foo", "baz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}}, +// }, +// expected: []interface{}{"OK", "OK", float64(2), "1\x00"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitop or of keys containing a string, a bytearray and get the destkey", +// cmds: []HTTPCommand{ +// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 1}}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"AND", "bazzz", "foo", "baz", "bazz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}}, +// }, +// expected: []interface{}{"OK", float64(0), float64(6), "\x00\x00\x00\x00\x00\x00"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP OR of keys containing strings and get the destkey", +// cmds: []HTTPCommand{ +// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "bazz", "foo", "baz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}}, +// }, +// expected: []interface{}{"OK", float64(6), "goofev"}, +// assertType: []string{"equal", "equal", "equal"}, +// }, +// { +// name: "BITOP OR of keys containing integers and get the destkey", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": 10}}, +// {Command: "SET", Body: map[string]interface{}{"key": "baz", "value": 5}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "bazz", "foo", "baz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}}, +// }, +// expected: []interface{}{"OK", "OK", float64(2), "50"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP OR of keys containing strings and a bytearray and get the destkey", +// cmds: []HTTPCommand{ +// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 1}}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "bazzz", "foo", "baz", "bazz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 0}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{49, 1}}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "bazzz", "foo", "baz", "bazz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}}, +// }, +// expected: []interface{}{"OK", float64(0), float64(6), "g\xefofev", float64(1), float64(0), float64(7), "goofev@"}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP XOR of keys containing strings and get the destkey", +// cmds: []HTTPCommand{ +// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazz", "foo", "baz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}}, +// }, +// expected: []interface{}{"OK", float64(6), "\x07\x0d\x0c\x06\x04\x14"}, +// assertType: []string{"equal", "equal", "equal"}, +// }, +// { +// name: "BITOP XOR of keys containing strings and a bytearray and get the destkey", +// cmds: []HTTPCommand{ +// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 1}}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazzz", "foo", "baz", "bazz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 0}}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{49, 1}}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazzz", "foo", "baz", "bazz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}}, +// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{49, 0}}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazzz", "foo", "baz", "bazz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}}, +// }, +// expected: []interface{}{"OK", float64(0), float64(6), "\x07\x8d\x0c\x06\x04\x14", float64(1), float64(0), float64(7), "\x07\r\x0c\x06\x04\x14@", float64(1), float64(7), "\x07\r\x0c\x06\x04\x14\x00"}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP XOR of keys containing integers and get the destkey", +// cmds: []HTTPCommand{ +// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": 10}}, +// {Command: "SET", Body: map[string]interface{}{"key": "baz", "value": 5}}, +// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazz", "foo", "baz"}}}, +// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}}, +// }, +// expected: []interface{}{"OK", "OK", float64(2), "\x040"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Delete the key before running the test +// exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"key": "foo"}}) +// exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"key": "baz"}}) +// exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"key": "bazz"}}) +// exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"key": "bazzz"}}) +// for i := 0; i < len(tc.cmds); i++ { +// res, _ := exec.FireCommand(tc.cmds[i]) + +// switch tc.assertType[i] { +// case "equal": +// assert.Equal(t, tc.expected[i], res) +// case "less": +// assert.True(t, res.(float64) <= tc.expected[i].(float64), "CMD: %s Expected %d to be less than or equal to %d", tc.cmds[i], res, tc.expected[i]) +// } +// } +// }) +// } +// } + +func TestBitCount(t *testing.T) { + exec := NewHTTPCommandExecutor() + testcases := []struct { + InCmds []HTTPCommand + Out []interface{} + }{ + { + InCmds: []HTTPCommand{ + {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{7, 1}}}, + }, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{7, 1}}}, + }, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{122, 1}}}, + }, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{122}}}, + }, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{122, 0}}}, + }, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{122}}}, + }, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "value": 1223232}}, + }, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{7}}}, + }, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{8}}}, + }, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{3, 7, "BIT"}}}, + }, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{3, 7}}}, + }, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{0, 0}}}, + }, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "BITCOUNT"}, + }, + Out: []interface{}{"ERR wrong number of arguments for 'bitcount' command"}, + }, + { + InCmds: []HTTPCommand{ + {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey"}}, + }, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []HTTPCommand{ + {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{0}}}, + }, + Out: []interface{}{"ERR syntax error"}, + }, + } + + for _, tcase := range testcases { + for i := 0; i < len(tcase.InCmds); i++ { + cmd := tcase.InCmds[i] + out := tcase.Out[i] + res, _ := exec.FireCommand(cmd) + assert.Equal(t, out, res, "Value mismatch for cmd %s\n.", cmd) + } + } +} + func TestBitPos(t *testing.T) { exec := NewHTTPCommandExecutor() @@ -18,6 +577,36 @@ func TestBitPos(t *testing.T) { out interface{} setCmdSETBIT bool }{ + { + name: "String interval BIT 0,-1 ", + val: "\\x00\\xff\\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 0, -1, "bit"}}}, + out: float64(0), + }, + { + name: "String interval BIT 8,-1", + val: "\\x00\\xff\\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 8, -1, "bit"}}}, + out: float64(8), + }, + { + name: "String interval BIT 16,-1", + val: "\\x00\\xff\\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 16, -1, "bit"}}}, + out: float64(16), + }, + { + name: "String interval BIT 16,200", + val: "\\x00\\xff\\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 16, 200, "bit"}}}, + out: float64(16), + }, + { + name: "String interval BIT 8,8", + val: "\\x00\\xff\\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 8, 8, "bit"}}}, + out: float64(8), + }, { name: "FindsFirstZeroBit", val: []byte("\xff\xf0\x00"), @@ -30,6 +619,12 @@ func TestBitPos(t *testing.T) { inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}}, out: float64(12), }, + { + name: "NoOneBitFound", + val: "\x00\x00\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}}, + out: float64(-1), + }, { name: "NoZeroBitFound", val: []byte("\xff\xff\xff"), @@ -60,6 +655,259 @@ func TestBitPos(t *testing.T) { inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 2, 2, "BIT"}}}, out: float64(-1), }, + { + name: "FindsFirstZeroBitInRange", + val: []byte("\xff\xf0\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, 2}}}, + out: float64(12), + }, + { + name: "FindsFirstOneBitInRange", + val: "\x00\x00\xf0", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 2, 3}}}, + out: float64(16), + }, + { + name: "StartGreaterThanEnd", + val: "\xff\xf0\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 3, 2}}}, + out: float64(-1), + }, + { + name: "FindsFirstOneBitWithNegativeStart", + val: []byte("\x00\x00\xf0"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, -2, -1}}}, + out: float64(16), + }, + { + name: "FindsFirstZeroBitWithNegativeEnd", + val: []byte("\xff\xf0\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, -1}}}, + out: float64(12), + }, + { + name: "FindsFirstZeroBitInByteRange", + val: []byte("\xff\x00\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, 2, "BYTE"}}}, + out: float64(8), + }, + { + name: "FindsFirstOneBitInBitRange", + val: "\x00\x01\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 0, 16, "BIT"}}}, + out: float64(15), + }, + { + name: "NoBitFoundInByteRange", + val: []byte("\xff\xff\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 0, 2, "BYTE"}}}, + out: float64(-1), + }, + { + name: "NoBitFoundInBitRange", + val: "\x00\x00\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 0, 23, "BIT"}}}, + out: float64(-1), + }, + { + name: "EmptyStringReturnsMinusOneForZeroBit", + val: []byte(""), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}}, + out: float64(-1), + }, + { + name: "EmptyStringReturnsMinusOneForOneBit", + val: []byte(""), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}}, + out: float64(-1), + }, + { + name: "SingleByteString", + val: "\x80", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}}, + out: float64(0), + }, + { + name: "RangeExceedsStringLength", + val: "\x00\xff", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 0, 20, "BIT"}}}, + out: float64(8), + }, + { + name: "InvalidBitArgument", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 2}}, + out: "ERR the bit argument must be 1 or 0", + }, + { + name: "NonIntegerStartParameter", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, "start"}}}, + out: "ERR value is not an integer or out of range", + }, + { + name: "NonIntegerEndParameter", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, "end"}}}, + out: "ERR value is not an integer or out of range", + }, + { + name: "InvalidRangeType", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, 2, "BYTEs"}}}, + out: "ERR syntax error", + }, + { + name: "InsufficientArguments", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey"}}, + out: "ERR wrong number of arguments for 'bitpos' command", + }, + { + name: "NonExistentKeyForZeroBit", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "nonexistentkey", "value": 0}}, + out: float64(0), + }, + { + name: "NonExistentKeyForOneBit", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "nonexistentkey", "value": 1}}, + out: float64(-1), + }, + { + name: "IntegerValue", + val: 65280, // 0xFF00 in decimal + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}}, + out: float64(0), + }, + { + name: "LargeIntegerValue", + val: 16777215, // 0xFFFFFF in decimal + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}}, + out: float64(2), + }, + { + name: "SmallIntegerValue", + val: 1, // 0x01 in decimal + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}}, + out: float64(0), + }, + { + name: "ZeroIntegerValue", + val: 0, + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}}, + out: float64(2), + }, + { + name: "BitRangeStartGreaterThanBitLength", + val: []byte("\xff\xff\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 25, 30, "BIT"}}}, + out: float64(-1), + }, + { + name: "BitRangeEndExceedsBitLength", + val: []byte("\xff\xff\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 0, 30, "BIT"}}}, + out: float64(-1), + }, + { + name: "NegativeStartInBitRange", + val: []byte("\x00\xff\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, -16, -1, "BIT"}}}, + out: float64(8), + }, + { + name: "LargeNegativeStart", + val: []byte("\x00\xff\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, -100, -1}}}, + out: float64(8), + }, + { + name: "LargePositiveEnd", + val: "\x00\xff\xff", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 0, 100}}}, + out: float64(8), + }, + { + name: "StartAndEndEqualInByteRange", + val: []byte("\x0f\xff\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, 1, "BYTE"}}}, + out: float64(-1), + }, + { + name: "StartAndEndEqualInBitRange", + val: "\x0f\xff\xff", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 1, 1, "BIT"}}}, + out: float64(-1), + }, + { + name: "FindFirstZeroBitInNegativeRange", + val: []byte("\xff\x00\xff"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, -2, -1}}}, + out: float64(8), + }, + { + name: "FindFirstOneBitInNegativeRangeBIT", + val: []byte("\x00\x00\x80"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, -8, -1, "BIT"}}}, + out: float64(16), + }, + { + name: "MaxIntegerValue", + val: math.MaxInt64, + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}}, + out: float64(0), + }, + { + name: "MinIntegerValue", + val: math.MinInt64, + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}}, + out: float64(2), + }, + { + name: "SingleBitStringZero", + val: "\x00", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}}, + out: float64(-1), + }, + { + name: "SingleBitStringOne", + val: "\x01", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}}, + out: float64(0), + }, + { + name: "AllBitsSetExceptLast", + val: []byte("\xff\xff\xfe"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}}, + out: float64(23), + }, + { + name: "OnlyLastBitSet", + val: "\x00\x00\x01", + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}}, + out: float64(23), + }, + { + name: "AlternatingBitsLongString", + val: []byte("\xaa\xaa\xaa\xaa\xaa"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}}, + out: float64(1), + }, + { + name: "VeryLargeByteString", + val: []byte(strings.Repeat("\xff", 1000) + "\x00"), + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}}, + out: float64(8000), + }, + { + name: "FindZeroBitOnSetBitKey", + val: []interface{}{8, 1}, + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkeysb", "value": 1}}, + out: float64(8), + setCmdSETBIT: true, + }, + { + name: "FindOneBitOnSetBitKey", + val: []interface{}{1, 1}, + inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkeysb", "value": 1}}, + out: float64(1), + setCmdSETBIT: true, + }, } for _, tc := range testcases { @@ -68,7 +916,7 @@ func TestBitPos(t *testing.T) { if tc.setCmdSETBIT { setCmd = HTTPCommand{ Command: "SETBIT", - Body: map[string]interface{}{"key": "testkeysb", "value": fmt.Sprintf("%s", tc.val.(string))}, + Body: map[string]interface{}{"key": "testkeysb", "values": tc.val}, } } else { switch v := tc.val.(type) { @@ -80,7 +928,7 @@ func TestBitPos(t *testing.T) { case string: setCmd = HTTPCommand{ Command: "SET", - Body: map[string]interface{}{"key": "testkey", "value": fmt.Sprintf("%s", v)}, + Body: map[string]interface{}{"key": "testkey", "value": v}, } case int: setCmd = HTTPCommand{ @@ -102,3 +950,378 @@ func TestBitPos(t *testing.T) { }) } } + +func TestBitfield(t *testing.T) { + exec := NewHTTPCommandExecutor() + + exec.FireCommand(HTTPCommand{Command: "FLUSHDB"}) + defer exec.FireCommand(HTTPCommand{Command: "FLUSHDB"}) // clean up after all test cases + syntaxErrMsg := "ERR syntax error" + bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is" + integerErrMsg := "ERR value is not an integer or out of range" + overflowErrMsg := "ERR Invalid OVERFLOW type specified" + + testCases := []struct { + Name string + Commands []HTTPCommand + Expected []interface{} + Delay []time.Duration + CleanUp []HTTPCommand + }{ + { + Name: "BITFIELD Arity Check", + Commands: []HTTPCommand{{Command: "BITFIELD"}}, + Expected: []interface{}{"ERR wrong number of arguments for 'bitfield' command"}, + Delay: []time.Duration{0}, + CleanUp: []HTTPCommand{}, + }, + { + Name: "BITFIELD on unsupported type of SET", + Commands: []HTTPCommand{{Command: "SADD", Body: map[string]interface{}{"key": "bits", "values": []string{"a", "b", "c"}}}, {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits"}}}, + Expected: []interface{}{float64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD on unsupported type of JSON", + Commands: []HTTPCommand{{Command: "json.set", Body: map[string]interface{}{"key": "bits", "path": "$", "value": "1"}}, {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits"}}}, + Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD on unsupported type of HSET", + Commands: []HTTPCommand{{Command: "HSET", Body: map[string]interface{}{"key": "bits", "field": "a", "value": "1"}}, {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits"}}}, + Expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD with syntax errors", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255, "incrby", "u8", 0, 100, "get", "u8"}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "a8", 0, 255, "incrby", "u8", 0, 100, "get", "u8"}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "a", 255, "incrby", "u8", 0, 100}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255, "incrby", "u8", 0, 100, "overflow", "wraap"}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, "incrby", "u8", 0, 100, "get", "u8", 288}}}, + }, + Expected: []interface{}{ + syntaxErrMsg, + bitFieldTypeErrMsg, + "ERR bit offset is not an integer or out of range", + overflowErrMsg, + integerErrMsg, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD signed SET and GET basics", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "i8", 0, -100}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "i8", 0, 101}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", 0}}}, + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(-100)}, []interface{}{float64(101)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD unsigned SET and GET basics", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 100}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", 0}}}, + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(255)}, []interface{}{float64(100)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD signed SET and GET together", + Commands: []HTTPCommand{{Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "i8", 0, 255, "set", "i8", 0, 100, "get", "i8", 0}}}}, + Expected: []interface{}{[]interface{}{float64(0), float64(-1), float64(100)}}, + Delay: []time.Duration{0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD unsigned with SET, GET and INCRBY arguments", + Commands: []HTTPCommand{{Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255, "incrby", "u8", 0, 100, "get", "u8", 0}}}}, + Expected: []interface{}{[]interface{}{float64(0), float64(99), float64(99)}}, + Delay: []time.Duration{0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD with only key as argument", + Commands: []HTTPCommand{{Command: "BITFIELD", Body: map[string]interface{}{"key": "bits"}}}, + Expected: []interface{}{[]interface{}{}}, + Delay: []time.Duration{0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD # form", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 65}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#1", 66}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#2", 67}}}, + {Command: "GET", Body: map[string]interface{}{"key": "bits"}}, + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(0)}, []interface{}{float64(0)}, "ABC"}, + Delay: []time.Duration{0, 0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD basic INCRBY form", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 10}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"incrby", "u8", "#0", 100}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"incrby", "u8", "#0", 100}}}, + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(110)}, []interface{}{float64(210)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD chaining of multiple commands", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 10}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"incrby", "u8", "#0", 100, "incrby", "u8", "#0", 100}}}, + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(110), float64(210)}}, + Delay: []time.Duration{0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD unsigned overflow wrap", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 100}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "wrap", "incrby", "u8", "#0", 257}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", "#0"}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "wrap", "incrby", "u8", "#0", 255}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", "#0"}}}, + }, + Expected: []interface{}{ + []interface{}{float64(0)}, + []interface{}{float64(101)}, + []interface{}{float64(101)}, + []interface{}{float64(100)}, + []interface{}{float64(100)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD unsigned overflow sat", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 100}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "sat", "incrby", "u8", "#0", 257}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", "#0"}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "sat", "incrby", "u8", "#0", -255}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", "#0"}}}, + }, + Expected: []interface{}{ + []interface{}{float64(0)}, + []interface{}{float64(255)}, + []interface{}{float64(255)}, + []interface{}{float64(0)}, + []interface{}{float64(0)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD signed overflow wrap", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "i8", "#0", 100}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "wrap", "incrby", "i8", "#0", 257}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", "#0"}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "wrap", "incrby", "i8", "#0", 255}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", "#0"}}}, + }, + Expected: []interface{}{ + []interface{}{float64(0)}, + []interface{}{float64(101)}, + []interface{}{float64(101)}, + []interface{}{float64(100)}, + []interface{}{float64(100)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD signed overflow sat", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 100}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "sat", "incrby", "i8", "#0", 257}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", "#0"}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "sat", "incrby", "i8", "#0", -255}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", "#0"}}}, + }, + Expected: []interface{}{ + []interface{}{float64(0)}, + []interface{}{float64(127)}, + []interface{}{float64(127)}, + []interface{}{float64(-128)}, + []interface{}{float64(-128)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD regression 1", + Commands: []HTTPCommand{{Command: "SET", Body: map[string]interface{}{"key": "bits", "value": "1"}}, {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u1", 0}}}}, + Expected: []interface{}{"OK", []interface{}{float64(0)}}, + Delay: []time.Duration{0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD regression 2", + Commands: []HTTPCommand{ + {Command: "BITFIELD", Body: map[string]interface{}{"key": "mystring", "values": []interface{}{"set", "i8", 0, 10}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "mystring", "values": []interface{}{"set", "i8", 64, 10}}}, + {Command: "BITFIELD", Body: map[string]interface{}{"key": "mystring", "values": []interface{}{"incrby", "i8", 10, 99900}}}, + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(0)}, []interface{}{float64(60)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "mystring"}}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + + for i := 0; i < len(tc.Commands); i++ { + if tc.Delay[i] > 0 { + time.Sleep(tc.Delay[i]) + } + result, _ := exec.FireCommand(tc.Commands[i]) + expected := tc.Expected[i] + assert.Equal(t, expected, result) + } + + for _, cmd := range tc.CleanUp { + exec.FireCommand(cmd) + } + }) + } +} + +func TestBitfieldRO(t *testing.T) { + exec := NewHTTPCommandExecutor() + + exec.FireCommand(HTTPCommand{Command: "FLUSHDB"}) + defer exec.FireCommand(HTTPCommand{Command: "FLUSHDB"}) + + syntaxErrMsg := "ERR syntax error" + bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is" + unsupportedCmdErrMsg := "ERR BITFIELD_RO only supports the GET subcommand" + + testCases := []struct { + Name string + Commands []HTTPCommand + Expected []interface{} + Delay []time.Duration + CleanUp []HTTPCommand + }{ + { + Name: "BITFIELD_RO Arity Check", + Commands: []HTTPCommand{{Command: "BITFIELD_RO"}}, + Expected: []interface{}{"ERR wrong number of arguments for 'bitfield_ro' command"}, + Delay: []time.Duration{0}, + CleanUp: []HTTPCommand{}, + }, + { + Name: "BITFIELD_RO on unsupported type of SET", + Commands: []HTTPCommand{{Command: "SADD", Body: map[string]interface{}{"key": "bits", "values": []string{"a", "b", "c"}}}, {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits"}}}, + Expected: []interface{}{float64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD_RO on unsupported type of JSON", + Commands: []HTTPCommand{{Command: "JSON.SET", Body: map[string]interface{}{"key": "bits", "path": "$", "value": "1"}}, {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits"}}}, + Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD_RO on unsupported type of HSET", + Commands: []HTTPCommand{{Command: "HSET", Body: map[string]interface{}{"key": "bits", "field": "a", "value": "1"}}, {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits"}}}, + Expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD_RO with unsupported commands", + Commands: []HTTPCommand{ + {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255}}}, + {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"incrby", "u8", 0, 100}}}, + }, + Expected: []interface{}{ + unsupportedCmdErrMsg, + unsupportedCmdErrMsg, + }, + Delay: []time.Duration{0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD_RO with syntax error", + Commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "bits", "value": "1"}}, + {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8"}}}, + {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get"}}}, + {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "somethingrandom"}}}, + }, + Expected: []interface{}{ + "OK", + syntaxErrMsg, + syntaxErrMsg, + syntaxErrMsg, + }, + Delay: []time.Duration{0, 0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD_RO with invalid bitfield type", + Commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "bits", "value": "1"}}, + {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "a8", 0}}}, + {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "s8", 0}}}, + {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "somethingrandom", 0}}}, + }, + Expected: []interface{}{ + "OK", + bitFieldTypeErrMsg, + bitFieldTypeErrMsg, + bitFieldTypeErrMsg, + }, + Delay: []time.Duration{0, 0, 0, 0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + { + Name: "BITFIELD_RO with only key as argument", + Commands: []HTTPCommand{{Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits"}}}, + Expected: []interface{}{[]interface{}{}}, + Delay: []time.Duration{0}, + CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + + for i := 0; i < len(tc.Commands); i++ { + if tc.Delay[i] > 0 { + time.Sleep(tc.Delay[i]) + } + result, _ := exec.FireCommand(tc.Commands[i]) + expected := tc.Expected[i] + assert.Equal(t, expected, result) + } + + for _, cmd := range tc.CleanUp { + _, _ = exec.FireCommand(cmd) + } + }) + } +} diff --git a/integration_tests/commands/resp/bit_test.go b/integration_tests/commands/resp/bit_test.go new file mode 100644 index 000000000..f6033914a --- /dev/null +++ b/integration_tests/commands/resp/bit_test.go @@ -0,0 +1,1079 @@ +package resp + +// The following commands are a part of this test class: +// SETBIT, GETBIT, BITCOUNT, BITOP, BITPOS, BITFIELD, BITFIELD_RO + +import ( + "fmt" + "math" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// TODO: BITOP has not been migrated yet. Once done, we can uncomment the tests - please check accuracy and validate for expected values. + +// func TestBitOp(t *testing.T) { +// conn := getLocalConnection() +// defer conn.Close() +// testcases := []struct { +// InCmds []string +// Out []interface{} +// }{ +// { +// InCmds: []string{"SETBIT unitTestKeyA 1 1", "SETBIT unitTestKeyA 3 1", "SETBIT unitTestKeyA 5 1", "SETBIT unitTestKeyA 7 1", "SETBIT unitTestKeyA 8 1"}, +// Out: []interface{}{int64(0), int64(0), int64(0), int64(0), int64(0)}, +// }, +// { +// InCmds: []string{"SETBIT unitTestKeyB 2 1", "SETBIT unitTestKeyB 4 1", "SETBIT unitTestKeyB 7 1"}, +// Out: []interface{}{int64(0), int64(0), int64(0)}, +// }, +// { +// InCmds: []string{"SET foo bar", "SETBIT foo 2 1", "SETBIT foo 4 1", "SETBIT foo 7 1", "GET foo"}, +// Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"}, +// }, +// { +// InCmds: []string{"SET mykey12 1343", "SETBIT mykey12 2 1", "SETBIT mykey12 4 1", "SETBIT mykey12 7 1", "GET mykey12"}, +// Out: []interface{}{"OK", int64(1), int64(0), int64(1), int64(9343)}, +// }, +// { +// InCmds: []string{"SET foo12 bar", "SETBIT foo12 2 1", "SETBIT foo12 4 1", "SETBIT foo12 7 1", "GET foo12"}, +// Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"}, +// }, +// { +// InCmds: []string{"BITOP NOT unitTestKeyNOT unitTestKeyA "}, +// Out: []interface{}{int64(2)}, +// }, +// { +// InCmds: []string{"GETBIT unitTestKeyNOT 1", "GETBIT unitTestKeyNOT 2", "GETBIT unitTestKeyNOT 7", "GETBIT unitTestKeyNOT 8", "GETBIT unitTestKeyNOT 9"}, +// Out: []interface{}{int64(0), int64(1), int64(0), int64(0), int64(1)}, +// }, +// { +// InCmds: []string{"BITOP OR unitTestKeyOR unitTestKeyB unitTestKeyA"}, +// Out: []interface{}{int64(2)}, +// }, +// { +// InCmds: []string{"GETBIT unitTestKeyOR 1", "GETBIT unitTestKeyOR 2", "GETBIT unitTestKeyOR 3", "GETBIT unitTestKeyOR 7", "GETBIT unitTestKeyOR 8", "GETBIT unitTestKeyOR 9", "GETBIT unitTestKeyOR 12"}, +// Out: []interface{}{int64(1), int64(1), int64(1), int64(1), int64(1), int64(0), int64(0)}, +// }, +// { +// InCmds: []string{"BITOP AND unitTestKeyAND unitTestKeyB unitTestKeyA"}, +// Out: []interface{}{int64(2)}, +// }, +// { +// InCmds: []string{"GETBIT unitTestKeyAND 1", "GETBIT unitTestKeyAND 2", "GETBIT unitTestKeyAND 7", "GETBIT unitTestKeyAND 8", "GETBIT unitTestKeyAND 9"}, +// Out: []interface{}{int64(0), int64(0), int64(1), int64(0), int64(0)}, +// }, +// { +// InCmds: []string{"BITOP XOR unitTestKeyXOR unitTestKeyB unitTestKeyA"}, +// Out: []interface{}{int64(2)}, +// }, +// { +// InCmds: []string{"GETBIT unitTestKeyXOR 1", "GETBIT unitTestKeyXOR 2", "GETBIT unitTestKeyXOR 3", "GETBIT unitTestKeyXOR 7", "GETBIT unitTestKeyXOR 8"}, +// Out: []interface{}{int64(1), int64(1), int64(1), int64(0), int64(1)}, +// }, +// } + +// for _, tcase := range testcases { +// for i := 0; i < len(tcase.InCmds); i++ { +// cmd := tcase.InCmds[i] +// out := tcase.Out[i] +// assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd) +// } +// } +// } + +// func TestBitOpsString(t *testing.T) { + +// conn := getLocalConnection() +// defer conn.Close() +// // foobar in bits is 01100110 01101111 01101111 01100010 01100001 01110010 +// fooBarBits := "011001100110111101101111011000100110000101110010" +// // randomly get 8 bits for testing +// testOffsets := make([]int, 8) + +// for i := 0; i < 8; i++ { +// testOffsets[i] = rand.Intn(len(fooBarBits)) +// } + +// getBitTestCommands := make([]string, 8+1) +// getBitTestExpected := make([]interface{}, 8+1) + +// getBitTestCommands[0] = "SET foo foobar" +// getBitTestExpected[0] = "OK" + +// for i := 1; i < 8+1; i++ { +// getBitTestCommands[i] = fmt.Sprintf("GETBIT foo %d", testOffsets[i-1]) +// getBitTestExpected[i] = int64(fooBarBits[testOffsets[i-1]] - '0') +// } + +// testCases := []struct { +// name string +// cmds []string +// expected []interface{} +// assertType []string +// }{ +// { +// name: "Getbit of a key containing a string", +// cmds: getBitTestCommands, +// expected: getBitTestExpected, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Getbit of a key containing an integer", +// cmds: []string{"SET foo 10", "GETBIT foo 0", "GETBIT foo 1", "GETBIT foo 2", "GETBIT foo 3", "GETBIT foo 4", "GETBIT foo 5", "GETBIT foo 6", "GETBIT foo 7"}, +// expected: []interface{}{"OK", int64(0), int64(0), int64(1), int64(1), int64(0), int64(0), int64(0), int64(1)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, { +// name: "Getbit of a key containing an integer 2nd byte", +// cmds: []string{"SET foo 10", "GETBIT foo 8", "GETBIT foo 9", "GETBIT foo 10", "GETBIT foo 11", "GETBIT foo 12", "GETBIT foo 13", "GETBIT foo 14", "GETBIT foo 15"}, +// expected: []interface{}{"OK", int64(0), int64(0), int64(1), int64(1), int64(0), int64(0), int64(0), int64(0)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Getbit of a key with an offset greater than the length of the string in bits", +// cmds: []string{"SET foo foobar", "GETBIT foo 100", "GETBIT foo 48", "GETBIT foo 47"}, +// expected: []interface{}{"OK", int64(0), int64(0), int64(0)}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitcount of a key containing a string", +// cmds: []string{"SET foo foobar", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"}, +// expected: []interface{}{"OK", int64(26), int64(26), int64(4), int64(6), int64(6), int64(17)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitcount of a key containing an integer", +// cmds: []string{"SET foo 10", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"}, +// expected: []interface{}{"OK", int64(5), int64(5), int64(3), int64(2), int64(2), int64(3)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Setbit of a key containing a string", +// cmds: []string{"SET foo foobar", "setbit foo 7 1", "get foo", "setbit foo 49 1", "setbit foo 50 1", "get foo", "setbit foo 49 0", "get foo"}, +// expected: []interface{}{"OK", int64(0), "goobar", int64(0), int64(0), "goobar`", int64(1), "goobar "}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Setbit of a key must not change the expiry of the key if expiry is set", +// cmds: []string{"SET foo foobar", "EXPIRE foo 100", "TTL foo", "SETBIT foo 7 1", "TTL foo"}, +// expected: []interface{}{"OK", int64(1), int64(100), int64(0), int64(100)}, +// assertType: []string{"equal", "equal", "less", "equal", "less"}, +// }, +// { +// name: "Setbit of a key must not add expiry to the key if expiry is not set", +// cmds: []string{"SET foo foobar", "TTL foo", "SETBIT foo 7 1", "TTL foo"}, +// expected: []interface{}{"OK", int64(-1), int64(0), int64(-1)}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitop not of a key containing a string", +// cmds: []string{"SET foo foobar", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"}, +// expected: []interface{}{"OK", int64(6), "\x99\x90\x90\x9d\x9e\x8d", int64(6), "foobar"}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitop not of a key containing an integer", +// cmds: []string{"SET foo 10", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"}, +// expected: []interface{}{"OK", int64(2), "\xce\xcf", int64(2), int64(10)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Get a string created with setbit", +// cmds: []string{"SETBIT foo 1 1", "SETBIT foo 3 1", "GET foo"}, +// expected: []interface{}{int64(0), int64(0), "P"}, +// assertType: []string{"equal", "equal", "equal"}, +// }, +// { +// name: "Bitop and of keys containing a string and get the destkey", +// cmds: []string{"SET foo foobar", "SET baz abcdef", "BITOP AND bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", "OK", int64(6), "`bc`ab"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP AND of keys containing integers and get the destkey", +// cmds: []string{"SET foo 10", "SET baz 5", "BITOP AND bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", "OK", int64(2), "1\x00"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitop or of keys containing a string, a bytearray and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP and bazzz foo baz bazz", "GET bazzz"}, +// expected: []interface{}{"OK", int64(0), int64(6), "\x00\x00\x00\x00\x00\x00"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP OR of keys containing strings and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "BITOP OR bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", int64(6), "goofev"}, +// assertType: []string{"equal", "equal", "equal"}, +// }, +// { +// name: "BITOP OR of keys containing integers and get the destkey", +// cmds: []string{"SET foo 10", "SET baz 5", "BITOP OR bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", "OK", int64(2), "50"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP OR of keys containing strings and a bytearray and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP OR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP OR bazzz foo baz bazz", "GET bazzz"}, +// expected: []interface{}{"OK", int64(0), int64(6), "g\xefofev", int64(1), int64(0), int64(7), "goofev@"}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP XOR of keys containing strings and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "BITOP XOR bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", int64(6), "\x07\x0d\x0c\x06\x04\x14"}, +// assertType: []string{"equal", "equal", "equal"}, +// }, +// { +// name: "BITOP XOR of keys containing strings and a bytearray and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "Setbit bazz 49 0", "BITOP XOR bazzz foo baz bazz", "GET bazzz"}, +// expected: []interface{}{"OK", int64(0), int64(6), "\x07\x8d\x0c\x06\x04\x14", int64(1), int64(0), int64(7), "\x07\r\x0c\x06\x04\x14@", int64(1), int64(7), "\x07\r\x0c\x06\x04\x14\x00"}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP XOR of keys containing integers and get the destkey", +// cmds: []string{"SET foo 10", "SET baz 5", "BITOP XOR bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", "OK", int64(2), "\x040"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Delete the key before running the test +// FireCommand(conn, "DEL foo") +// FireCommand(conn, "DEL baz") +// FireCommand(conn, "DEL bazz") +// FireCommand(conn, "DEL bazzz") +// for i := 0; i < len(tc.cmds); i++ { +// res := FireCommand(conn, tc.cmds[i]) + +// switch tc.assertType[i] { +// case "equal": +// assert.Equal(t, tc.expected[i], res) +// case "less": +// assert.True(t, res.(int64) <= tc.expected[i].(int64), "CMD: %s Expected %d to be less than or equal to %d", tc.cmds[i], res, tc.expected[i]) +// } +// } +// }) +// } +// } + +func TestBitCount(t *testing.T) { + conn := getLocalConnection() + testcases := []struct { + InCmds []string + Out []interface{} + }{ + { + InCmds: []string{"SETBIT mykey 7 1"}, + Out: []interface{}{int64(0)}, + }, + { + InCmds: []string{"SETBIT mykey 7 1"}, + Out: []interface{}{int64(1)}, + }, + { + InCmds: []string{"SETBIT mykey 122 1"}, + Out: []interface{}{int64(0)}, + }, + { + InCmds: []string{"GETBIT mykey 122"}, + Out: []interface{}{int64(1)}, + }, + { + InCmds: []string{"SETBIT mykey 122 0"}, + Out: []interface{}{int64(1)}, + }, + { + InCmds: []string{"GETBIT mykey 122"}, + Out: []interface{}{int64(0)}, + }, + { + InCmds: []string{"GETBIT mykey 1223232"}, + Out: []interface{}{int64(0)}, + }, + { + InCmds: []string{"GETBIT mykey 7"}, + Out: []interface{}{int64(1)}, + }, + { + InCmds: []string{"GETBIT mykey 8"}, + Out: []interface{}{int64(0)}, + }, + { + InCmds: []string{"BITCOUNT mykey 3 7 BIT"}, + Out: []interface{}{int64(1)}, + }, + { + InCmds: []string{"BITCOUNT mykey 3 7"}, + Out: []interface{}{int64(0)}, + }, + { + InCmds: []string{"BITCOUNT mykey 0 0"}, + Out: []interface{}{int64(1)}, + }, + { + InCmds: []string{"BITCOUNT"}, + Out: []interface{}{"ERR wrong number of arguments for 'bitcount' command"}, + }, + { + InCmds: []string{"BITCOUNT mykey"}, + Out: []interface{}{int64(1)}, + }, + { + InCmds: []string{"BITCOUNT mykey 0"}, + Out: []interface{}{"ERR syntax error"}, + }, + } + + for _, tcase := range testcases { + for i := 0; i < len(tcase.InCmds); i++ { + cmd := tcase.InCmds[i] + out := tcase.Out[i] + assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd) + } + } +} + +func TestBitPos(t *testing.T) { + conn := getLocalConnection() + testcases := []struct { + name string + val interface{} + inCmd string + out interface{} + setCmdSETBIT bool + }{ + { + name: "String interval BIT 0,-1 ", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 0 -1 bit", + out: int64(0), + }, + { + name: "String interval BIT 8,-1", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 8 -1 bit", + out: int64(8), + }, + { + name: "String interval BIT 16,-1", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 16 -1 bit", + out: int64(16), + }, + { + name: "String interval BIT 16,200", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 16 200 bit", + out: int64(16), + }, + { + name: "String interval BIT 8,8", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 8 8 bit", + out: int64(8), + }, + { + name: "FindsFirstZeroBit", + val: "\xff\xf0\x00", + inCmd: "BITPOS testkey 0", + out: int64(12), + }, + { + name: "FindsFirstOneBit", + val: "\x00\x0f\xff", + inCmd: "BITPOS testkey 1", + out: int64(12), + }, + { + name: "NoOneBitFound", + val: "\x00\x00\x00", + inCmd: "BITPOS testkey 1", + out: int64(-1), + }, + { + name: "NoZeroBitFound", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0", + out: int64(24), + }, + { + name: "NoZeroBitFoundWithRangeStartPos", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 2", + out: int64(24), + }, + { + name: "NoZeroBitFoundWithOOBRangeStartPos", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 4", + out: int64(-1), + }, + { + name: "NoZeroBitFoundWithRange", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 2 2", + out: int64(-1), + }, + { + name: "NoZeroBitFoundWithRangeAndRangeType", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 2 2 BIT", + out: int64(-1), + }, + { + name: "FindsFirstZeroBitInRange", + val: "\xff\xf0\xff", + inCmd: "BITPOS testkey 0 1 2", + out: int64(12), + }, + { + name: "FindsFirstOneBitInRange", + val: "\x00\x00\xf0", + inCmd: "BITPOS testkey 1 2 3", + out: int64(16), + }, + { + name: "StartGreaterThanEnd", + val: "\xff\xf0\x00", + inCmd: "BITPOS testkey 0 3 2", + out: int64(-1), + }, + { + name: "FindsFirstOneBitWithNegativeStart", + val: "\x00\x00\xf0", + inCmd: "BITPOS testkey 1 -2 -1", + out: int64(16), + }, + { + name: "FindsFirstZeroBitWithNegativeEnd", + val: "\xff\xf0\xff", + inCmd: "BITPOS testkey 0 1 -1", + out: int64(12), + }, + { + name: "FindsFirstZeroBitInByteRange", + val: "\xff\x00\xff", + inCmd: "BITPOS testkey 0 1 2 BYTE", + out: int64(8), + }, + { + name: "FindsFirstOneBitInBitRange", + val: "\x00\x01\x00", + inCmd: "BITPOS testkey 1 0 16 BIT", + out: int64(15), + }, + { + name: "NoBitFoundInByteRange", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 0 2 BYTE", + out: int64(-1), + }, + { + name: "NoBitFoundInBitRange", + val: "\x00\x00\x00", + inCmd: "BITPOS testkey 1 0 23 BIT", + out: int64(-1), + }, + { + name: "EmptyStringReturnsMinusOneForZeroBit", + val: "\"\"", + inCmd: "BITPOS testkey 0", + out: int64(-1), + }, + { + name: "EmptyStringReturnsMinusOneForOneBit", + val: "\"\"", + inCmd: "BITPOS testkey 1", + out: int64(-1), + }, + { + name: "SingleByteString", + val: "\x80", + inCmd: "BITPOS testkey 1", + out: int64(0), + }, + { + name: "RangeExceedsStringLength", + val: "\x00\xff", + inCmd: "BITPOS testkey 1 0 20 BIT", + out: int64(8), + }, + { + name: "InvalidBitArgument", + inCmd: "BITPOS testkey 2", + out: "ERR the bit argument must be 1 or 0", + }, + { + name: "NonIntegerStartParameter", + inCmd: "BITPOS testkey 0 start", + out: "ERR value is not an integer or out of range", + }, + { + name: "NonIntegerEndParameter", + inCmd: "BITPOS testkey 0 1 end", + out: "ERR value is not an integer or out of range", + }, + { + name: "InvalidRangeType", + inCmd: "BITPOS testkey 0 1 2 BYTEs", + out: "ERR syntax error", + }, + { + name: "InsufficientArguments", + inCmd: "BITPOS testkey", + out: "ERR wrong number of arguments for 'bitpos' command", + }, + { + name: "NonExistentKeyForZeroBit", + inCmd: "BITPOS nonexistentkey 0", + out: int64(0), + }, + { + name: "NonExistentKeyForOneBit", + inCmd: "BITPOS nonexistentkey 1", + out: int64(-1), + }, + { + name: "IntegerValue", + val: 65280, // 0xFF00 in decimal + inCmd: "BITPOS testkey 0", + out: int64(0), + }, + { + name: "LargeIntegerValue", + val: 16777215, // 0xFFFFFF in decimal + inCmd: "BITPOS testkey 1", + out: int64(2), + }, + { + name: "SmallIntegerValue", + val: 1, // 0x01 in decimal + inCmd: "BITPOS testkey 0", + out: int64(0), + }, + { + name: "ZeroIntegerValue", + val: 0, + inCmd: "BITPOS testkey 1", + out: int64(2), + }, + { + name: "BitRangeStartGreaterThanBitLength", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 25 30 BIT", + out: int64(-1), + }, + { + name: "BitRangeEndExceedsBitLength", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 0 30 BIT", + out: int64(-1), + }, + { + name: "NegativeStartInBitRange", + val: "\x00\xff\xff", + inCmd: "BITPOS testkey 1 -16 -1 BIT", + out: int64(8), + }, + { + name: "LargeNegativeStart", + val: "\x00\xff\xff", + inCmd: "BITPOS testkey 1 -100 -1", + out: int64(8), + }, + { + name: "LargePositiveEnd", + val: "\x00\xff\xff", + inCmd: "BITPOS testkey 1 0 100", + out: int64(8), + }, + { + name: "StartAndEndEqualInByteRange", + val: "\x0f\xff\xff", + inCmd: "BITPOS testkey 0 1 1 BYTE", + out: int64(-1), + }, + { + name: "StartAndEndEqualInBitRange", + val: "\x0f\xff\xff", + inCmd: "BITPOS testkey 1 1 1 BIT", + out: int64(-1), + }, + { + name: "FindFirstZeroBitInNegativeRange", + val: "\xff\x00\xff", + inCmd: "BITPOS testkey 0 -2 -1", + out: int64(8), + }, + { + name: "FindFirstOneBitInNegativeRangeBIT", + val: "\x00\x00\x80", + inCmd: "BITPOS testkey 1 -8 -1 BIT", + out: int64(16), + }, + { + name: "MaxIntegerValue", + val: math.MaxInt64, + inCmd: "BITPOS testkey 0", + out: int64(0), + }, + { + name: "MinIntegerValue", + val: math.MinInt64, + inCmd: "BITPOS testkey 1", + out: int64(2), + }, + { + name: "SingleBitStringZero", + val: "\x00", + inCmd: "BITPOS testkey 1", + out: int64(-1), + }, + { + name: "SingleBitStringOne", + val: "\x01", + inCmd: "BITPOS testkey 0", + out: int64(0), + }, + { + name: "AllBitsSetExceptLast", + val: "\xff\xff\xfe", + inCmd: "BITPOS testkey 0", + out: int64(23), + }, + { + name: "OnlyLastBitSet", + val: "\x00\x00\x01", + inCmd: "BITPOS testkey 1", + out: int64(23), + }, + { + name: "AlternatingBitsLongString", + val: "\xaa\xaa\xaa\xaa\xaa", + inCmd: "BITPOS testkey 0", + out: int64(1), + }, + { + name: "VeryLargeByteString", + val: strings.Repeat("\xff", 1000) + "\x00", + inCmd: "BITPOS testkey 0", + out: int64(8000), + }, + { + name: "FindZeroBitOnSetBitKey", + val: "8 1", + inCmd: "BITPOS testkeysb 1", + out: int64(8), + setCmdSETBIT: true, + }, + { + name: "FindOneBitOnSetBitKey", + val: "1 1", + inCmd: "BITPOS testkeysb 1", + out: int64(1), + setCmdSETBIT: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var setCmd string + if tc.setCmdSETBIT { + setCmd = fmt.Sprintf("SETBIT testkeysb %s", tc.val.(string)) + } else { + switch v := tc.val.(type) { + case string: + setCmd = fmt.Sprintf("SET testkey %s", v) + case int: + setCmd = fmt.Sprintf("SET testkey %d", v) + default: + // For test cases where we don't set a value (e.g., error cases) + setCmd = "" + } + } + + if setCmd != "" { + FireCommand(conn, setCmd) + } + + result := FireCommand(conn, tc.inCmd) + assert.Equal(t, tc.out, result, "Mismatch for cmd %s\n", tc.inCmd) + }) + } +} + +func TestBitfield(t *testing.T) { + conn := getLocalConnection() + defer conn.Close() + + FireCommand(conn, "FLUSHDB") + defer FireCommand(conn, "FLUSHDB") // clean up after all test cases + syntaxErrMsg := "ERR syntax error" + bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is" + integerErrMsg := "ERR value is not an integer or out of range" + overflowErrMsg := "ERR Invalid OVERFLOW type specified" + + testCases := []struct { + Name string + Commands []string + Expected []interface{} + Delay []time.Duration + CleanUp []string + }{ + { + Name: "BITFIELD Arity Check", + Commands: []string{"bitfield"}, + Expected: []interface{}{"ERR wrong number of arguments for 'bitfield' command"}, + Delay: []time.Duration{0}, + CleanUp: []string{}, + }, + { + Name: "BITFIELD on unsupported type of SET", + Commands: []string{"SADD bits a b c", "bitfield bits"}, + Expected: []interface{}{int64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD on unsupported type of JSON", + Commands: []string{"json.set bits $ 1", "bitfield bits"}, + Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD on unsupported type of HSET", + Commands: []string{"HSET bits a 1", "bitfield bits"}, + Expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD with syntax errors", + Commands: []string{ + "bitfield bits set u8 0 255 incrby u8 0 100 get u8", + "bitfield bits set a8 0 255 incrby u8 0 100 get u8", + "bitfield bits set u8 a 255 incrby u8 0 100 get u8", + "bitfield bits set u8 0 255 incrby u8 0 100 overflow wraap", + "bitfield bits set u8 0 incrby u8 0 100 get u8 288", + }, + Expected: []interface{}{ + syntaxErrMsg, + bitFieldTypeErrMsg, + "ERR bit offset is not an integer or out of range", + overflowErrMsg, + integerErrMsg, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"Del bits"}, + }, + { + Name: "BITFIELD signed SET and GET basics", + Commands: []string{"bitfield bits set i8 0 -100", "bitfield bits set i8 0 101", "bitfield bits get i8 0"}, + Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(-100)}, []interface{}{int64(101)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD unsigned SET and GET basics", + Commands: []string{"bitfield bits set u8 0 255", "bitfield bits set u8 0 100", "bitfield bits get u8 0"}, + Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(255)}, []interface{}{int64(100)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD signed SET and GET together", + Commands: []string{"bitfield bits set i8 0 255 set i8 0 100 get i8 0"}, + Expected: []interface{}{[]interface{}{int64(0), int64(-1), int64(100)}}, + Delay: []time.Duration{0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD unsigned with SET, GET and INCRBY arguments", + Commands: []string{"bitfield bits set u8 0 255 incrby u8 0 100 get u8 0"}, + Expected: []interface{}{[]interface{}{int64(0), int64(99), int64(99)}}, + Delay: []time.Duration{0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD with only key as argument", + Commands: []string{"bitfield bits"}, + Expected: []interface{}{[]interface{}{}}, + Delay: []time.Duration{0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD # form", + Commands: []string{ + "bitfield bits set u8 #0 65", + "bitfield bits set u8 #1 66", + "bitfield bits set u8 #2 67", + "get bits", + }, + Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(0)}, "ABC"}, + Delay: []time.Duration{0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD basic INCRBY form", + Commands: []string{ + "bitfield bits set u8 #0 10", + "bitfield bits incrby u8 #0 100", + "bitfield bits incrby u8 #0 100", + }, + Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110)}, []interface{}{int64(210)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD chaining of multiple commands", + Commands: []string{ + "bitfield bits set u8 #0 10", + "bitfield bits incrby u8 #0 100 incrby u8 #0 100", + }, + Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110), int64(210)}}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD unsigned overflow wrap", + Commands: []string{ + "bitfield bits set u8 #0 100", + "bitfield bits overflow wrap incrby u8 #0 257", + "bitfield bits get u8 #0", + "bitfield bits overflow wrap incrby u8 #0 255", + "bitfield bits get u8 #0", + }, + Expected: []interface{}{ + []interface{}{int64(0)}, + []interface{}{int64(101)}, + []interface{}{int64(101)}, + []interface{}{int64(100)}, + []interface{}{int64(100)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD unsigned overflow sat", + Commands: []string{ + "bitfield bits set u8 #0 100", + "bitfield bits overflow sat incrby u8 #0 257", + "bitfield bits get u8 #0", + "bitfield bits overflow sat incrby u8 #0 -255", + "bitfield bits get u8 #0", + }, + Expected: []interface{}{ + []interface{}{int64(0)}, + []interface{}{int64(255)}, + []interface{}{int64(255)}, + []interface{}{int64(0)}, + []interface{}{int64(0)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD signed overflow wrap", + Commands: []string{ + "bitfield bits set i8 #0 100", + "bitfield bits overflow wrap incrby i8 #0 257", + "bitfield bits get i8 #0", + "bitfield bits overflow wrap incrby i8 #0 255", + "bitfield bits get i8 #0", + }, + Expected: []interface{}{ + []interface{}{int64(0)}, + []interface{}{int64(101)}, + []interface{}{int64(101)}, + []interface{}{int64(100)}, + []interface{}{int64(100)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD signed overflow sat", + Commands: []string{ + "bitfield bits set u8 #0 100", + "bitfield bits overflow sat incrby i8 #0 257", + "bitfield bits get i8 #0", + "bitfield bits overflow sat incrby i8 #0 -255", + "bitfield bits get i8 #0", + }, + Expected: []interface{}{ + []interface{}{int64(0)}, + []interface{}{int64(127)}, + []interface{}{int64(127)}, + []interface{}{int64(-128)}, + []interface{}{int64(-128)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD regression 1", + Commands: []string{"set bits 1", "bitfield bits get u1 0"}, + Expected: []interface{}{"OK", []interface{}{int64(0)}}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD regression 2", + Commands: []string{ + "bitfield mystring set i8 0 10", + "bitfield mystring set i8 64 10", + "bitfield mystring incrby i8 10 99900", + }, + Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(60)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []string{"DEL mystring"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + + for i := 0; i < len(tc.Commands); i++ { + if tc.Delay[i] > 0 { + time.Sleep(tc.Delay[i]) + } + result := FireCommand(conn, tc.Commands[i]) + expected := tc.Expected[i] + assert.Equal(t, expected, result) + } + + for _, cmd := range tc.CleanUp { + FireCommand(conn, cmd) + } + }) + } +} + +func TestBitfieldRO(t *testing.T) { + conn := getLocalConnection() + defer conn.Close() + + FireCommand(conn, "FLUSHDB") + defer FireCommand(conn, "FLUSHDB") + + syntaxErrMsg := "ERR syntax error" + bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is" + unsupportedCmdErrMsg := "ERR BITFIELD_RO only supports the GET subcommand" + + testCases := []struct { + Name string + Commands []string + Expected []interface{} + Delay []time.Duration + CleanUp []string + }{ + { + Name: "BITFIELD_RO Arity Check", + Commands: []string{"bitfield_ro"}, + Expected: []interface{}{"ERR wrong number of arguments for 'bitfield_ro' command"}, + Delay: []time.Duration{0}, + CleanUp: []string{}, + }, + { + Name: "BITFIELD_RO on unsupported type of SET", + Commands: []string{"SADD bits a b c", "bitfield_ro bits"}, + Expected: []interface{}{int64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD_RO on unsupported type of JSON", + Commands: []string{"json.set bits $ 1", "bitfield_ro bits"}, + Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD_RO on unsupported type of HSET", + Commands: []string{"HSET bits a 1", "bitfield_ro bits"}, + Expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD_RO with unsupported commands", + Commands: []string{ + "bitfield_ro bits set u8 0 255", + "bitfield_ro bits incrby u8 0 100", + }, + Expected: []interface{}{ + unsupportedCmdErrMsg, + unsupportedCmdErrMsg, + }, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"Del bits"}, + }, + { + Name: "BITFIELD_RO with syntax error", + Commands: []string{ + "set bits 1", + "bitfield_ro bits get u8", + "bitfield_ro bits get", + "bitfield_ro bits get somethingrandom", + }, + Expected: []interface{}{ + "OK", + syntaxErrMsg, + syntaxErrMsg, + syntaxErrMsg, + }, + Delay: []time.Duration{0, 0, 0, 0}, + CleanUp: []string{"Del bits"}, + }, + { + Name: "BITFIELD_RO with invalid bitfield type", + Commands: []string{ + "set bits 1", + "bitfield_ro bits get a8 0", + "bitfield_ro bits get s8 0", + "bitfield_ro bits get somethingrandom 0", + }, + Expected: []interface{}{ + "OK", + bitFieldTypeErrMsg, + bitFieldTypeErrMsg, + bitFieldTypeErrMsg, + }, + Delay: []time.Duration{0, 0, 0, 0}, + CleanUp: []string{"Del bits"}, + }, + { + Name: "BITFIELD_RO with only key as argument", + Commands: []string{"bitfield_ro bits"}, + Expected: []interface{}{[]interface{}{}}, + Delay: []time.Duration{0}, + CleanUp: []string{"DEL bits"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + + for i := 0; i < len(tc.Commands); i++ { + if tc.Delay[i] > 0 { + time.Sleep(tc.Delay[i]) + } + result := FireCommand(conn, tc.Commands[i]) + expected := tc.Expected[i] + assert.Equal(t, expected, result) + } + + for _, cmd := range tc.CleanUp { + FireCommand(conn, cmd) + } + }) + } +} diff --git a/integration_tests/commands/websocket/bit_test.go b/integration_tests/commands/websocket/bit_test.go new file mode 100644 index 000000000..687b339d5 --- /dev/null +++ b/integration_tests/commands/websocket/bit_test.go @@ -0,0 +1,1090 @@ +package websocket + +import ( + "fmt" + "math" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// TODO: BITOP has not been migrated yet. Once done, we can uncomment the tests - please check accuracy and validate for expected values. + +// func TestBitOp(t *testing.T) { +// exec := NewWebsocketCommandExecutor() +// conn := exec.ConnectToServer() +// defer conn.Close() +// testcases := []struct { +// InCmds []string +// Out []interface{} +// }{ +// { +// InCmds: []string{"SETBIT unitTestKeyA 1 1", "SETBIT unitTestKeyA 3 1", "SETBIT unitTestKeyA 5 1", "SETBIT unitTestKeyA 7 1", "SETBIT unitTestKeyA 8 1"}, +// Out: []interface{}{float64(0), float64(0), float64(0), float64(0), float64(0)}, +// }, +// { +// InCmds: []string{"SETBIT unitTestKeyB 2 1", "SETBIT unitTestKeyB 4 1", "SETBIT unitTestKeyB 7 1"}, +// Out: []interface{}{float64(0), float64(0), float64(0)}, +// }, +// { +// InCmds: []string{"SET foo bar", "SETBIT foo 2 1", "SETBIT foo 4 1", "SETBIT foo 7 1", "GET foo"}, +// Out: []interface{}{"OK", float64(1), float64(0), float64(0), "kar"}, +// }, +// { +// InCmds: []string{"SET mykey12 1343", "SETBIT mykey12 2 1", "SETBIT mykey12 4 1", "SETBIT mykey12 7 1", "GET mykey12"}, +// Out: []interface{}{"OK", float64(1), float64(0), float64(1), float64(9343)}, +// }, +// { +// InCmds: []string{"SET foo12 bar", "SETBIT foo12 2 1", "SETBIT foo12 4 1", "SETBIT foo12 7 1", "GET foo12"}, +// Out: []interface{}{"OK", float64(1), float64(0), float64(0), "kar"}, +// }, +// { +// InCmds: []string{"BITOP NOT unitTestKeyNOT unitTestKeyA "}, +// Out: []interface{}{float64(2)}, +// }, +// { +// InCmds: []string{"GETBIT unitTestKeyNOT 1", "GETBIT unitTestKeyNOT 2", "GETBIT unitTestKeyNOT 7", "GETBIT unitTestKeyNOT 8", "GETBIT unitTestKeyNOT 9"}, +// Out: []interface{}{float64(0), float64(1), float64(0), float64(0), float64(1)}, +// }, +// { +// InCmds: []string{"BITOP OR unitTestKeyOR unitTestKeyB unitTestKeyA"}, +// Out: []interface{}{float64(2)}, +// }, +// { +// InCmds: []string{"GETBIT unitTestKeyOR 1", "GETBIT unitTestKeyOR 2", "GETBIT unitTestKeyOR 3", "GETBIT unitTestKeyOR 7", "GETBIT unitTestKeyOR 8", "GETBIT unitTestKeyOR 9", "GETBIT unitTestKeyOR 12"}, +// Out: []interface{}{float64(1), float64(1), float64(1), float64(1), float64(1), float64(0), float64(0)}, +// }, +// { +// InCmds: []string{"BITOP AND unitTestKeyAND unitTestKeyB unitTestKeyA"}, +// Out: []interface{}{float64(2)}, +// }, +// { +// InCmds: []string{"GETBIT unitTestKeyAND 1", "GETBIT unitTestKeyAND 2", "GETBIT unitTestKeyAND 7", "GETBIT unitTestKeyAND 8", "GETBIT unitTestKeyAND 9"}, +// Out: []interface{}{float64(0), float64(0), float64(1), float64(0), float64(0)}, +// }, +// { +// InCmds: []string{"BITOP XOR unitTestKeyXOR unitTestKeyB unitTestKeyA"}, +// Out: []interface{}{float64(2)}, +// }, +// { +// InCmds: []string{"GETBIT unitTestKeyXOR 1", "GETBIT unitTestKeyXOR 2", "GETBIT unitTestKeyXOR 3", "GETBIT unitTestKeyXOR 7", "GETBIT unitTestKeyXOR 8"}, +// Out: []interface{}{float64(1), float64(1), float64(1), float64(0), float64(1)}, +// }, +// } + +// for _, tcase := range testcases { +// for i := 0; i < len(tcase.InCmds); i++ { +// cmd := tcase.InCmds[i] +// out := tcase.Out[i] +// result, err := exec.FireCommandAndReadResponse(conn, cmd) +// assert.Nil(t, err) +// assert.Equal(t, out, result, "Value mismatch for cmd %s\n.", cmd) +// } +// } +// } + +// func TestBitOpsString(t *testing.T) { + +// exec := NewWebsocketCommandExecutor() +// conn := exec.ConnectToServer() +// defer conn.Close() +// // foobar in bits is 01100110 01101111 01101111 01100010 01100001 01110010 +// fooBarBits := "011001100110111101101111011000100110000101110010" +// // randomly get 8 bits for testing +// testOffsets := make([]int, 8) + +// for i := 0; i < 8; i++ { +// testOffsets[i] = rand.Intn(len(fooBarBits)) +// } + +// getBitTestCommands := make([]string, 8+1) +// getBitTestExpected := make([]interface{}, 8+1) + +// getBitTestCommands[0] = "SET foo foobar" +// getBitTestExpected[0] = "OK" + +// for i := 1; i < 8+1; i++ { +// getBitTestCommands[i] = fmt.Sprintf("GETBIT foo %d", testOffsets[i-1]) +// getBitTestExpected[i] = float64(fooBarBits[testOffsets[i-1]] - '0') +// } + +// testCases := []struct { +// name string +// cmds []string +// expected []interface{} +// assertType []string +// }{ +// { +// name: "Getbit of a key containing a string", +// cmds: getBitTestCommands, +// expected: getBitTestExpected, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Getbit of a key containing an integer", +// cmds: []string{"SET foo 10", "GETBIT foo 0", "GETBIT foo 1", "GETBIT foo 2", "GETBIT foo 3", "GETBIT foo 4", "GETBIT foo 5", "GETBIT foo 6", "GETBIT foo 7"}, +// expected: []interface{}{"OK", float64(0), float64(0), float64(1), float64(1), float64(0), float64(0), float64(0), float64(1)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, { +// name: "Getbit of a key containing an integer 2nd byte", +// cmds: []string{"SET foo 10", "GETBIT foo 8", "GETBIT foo 9", "GETBIT foo 10", "GETBIT foo 11", "GETBIT foo 12", "GETBIT foo 13", "GETBIT foo 14", "GETBIT foo 15"}, +// expected: []interface{}{"OK", float64(0), float64(0), float64(1), float64(1), float64(0), float64(0), float64(0), float64(0)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Getbit of a key with an offset greater than the length of the string in bits", +// cmds: []string{"SET foo foobar", "GETBIT foo 100", "GETBIT foo 48", "GETBIT foo 47"}, +// expected: []interface{}{"OK", float64(0), float64(0), float64(0)}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitcount of a key containing a string", +// cmds: []string{"SET foo foobar", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"}, +// expected: []interface{}{"OK", float64(26), float64(26), float64(4), float64(6), float64(6), float64(17)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitcount of a key containing an integer", +// cmds: []string{"SET foo 10", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"}, +// expected: []interface{}{"OK", float64(5), float64(5), float64(3), float64(2), float64(2), float64(3)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Setbit of a key containing a string", +// cmds: []string{"SET foo foobar", "setbit foo 7 1", "get foo", "setbit foo 49 1", "setbit foo 50 1", "get foo", "setbit foo 49 0", "get foo"}, +// expected: []interface{}{"OK", float64(0), "goobar", float64(0), float64(0), "goobar`", float64(1), "goobar "}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Setbit of a key must not change the expiry of the key if expiry is set", +// cmds: []string{"SET foo foobar", "EXPIRE foo 100", "TTL foo", "SETBIT foo 7 1", "TTL foo"}, +// expected: []interface{}{"OK", float64(1), float64(100), float64(0), float64(100)}, +// assertType: []string{"equal", "equal", "less", "equal", "less"}, +// }, +// { +// name: "Setbit of a key must not add expiry to the key if expiry is not set", +// cmds: []string{"SET foo foobar", "TTL foo", "SETBIT foo 7 1", "TTL foo"}, +// expected: []interface{}{"OK", float64(-1), float64(0), float64(-1)}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitop not of a key containing a string", +// cmds: []string{"SET foo foobar", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"}, +// expected: []interface{}{"OK", float64(6), "\\x99\\x90\\x90\\x9d\\x9e\\x8d", float64(6), "foobar"}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitop not of a key containing an integer", +// cmds: []string{"SET foo 10", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"}, +// expected: []interface{}{"OK", float64(2), "\\xce\\xcf", float64(2), float64(10)}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Get a string created with setbit", +// cmds: []string{"SETBIT foo 1 1", "SETBIT foo 3 1", "GET foo"}, +// expected: []interface{}{float64(0), float64(0), "P"}, +// assertType: []string{"equal", "equal", "equal"}, +// }, +// { +// name: "Bitop and of keys containing a string and get the destkey", +// cmds: []string{"SET foo foobar", "SET baz abcdef", "BITOP AND bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", "OK", float64(6), "`bc`ab"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP AND of keys containing integers and get the destkey", +// cmds: []string{"SET foo 10", "SET baz 5", "BITOP AND bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", "OK", float64(2), "1\x00"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "Bitop or of keys containing a string, a bytearray and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP and bazzz foo baz bazz", "GET bazzz"}, +// expected: []interface{}{"OK", float64(0), float64(6), "\x00\x00\x00\x00\x00\x00"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP OR of keys containing strings and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "BITOP OR bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", float64(6), "goofev"}, +// assertType: []string{"equal", "equal", "equal"}, +// }, +// { +// name: "BITOP OR of keys containing integers and get the destkey", +// cmds: []string{"SET foo 10", "SET baz 5", "BITOP OR bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", "OK", float64(2), "50"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP OR of keys containing strings and a bytearray and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP OR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP OR bazzz foo baz bazz", "GET bazzz"}, +// expected: []interface{}{"OK", float64(0), float64(6), "g\xefofev", float64(1), float64(0), float64(7), "goofev@"}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP XOR of keys containing strings and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "BITOP XOR bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", float64(6), "\x07\x0d\x0c\x06\x04\x14"}, +// assertType: []string{"equal", "equal", "equal"}, +// }, +// { +// name: "BITOP XOR of keys containing strings and a bytearray and get the destkey", +// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "Setbit bazz 49 0", "bitop xor bazzz foo baz bazz", "get bazzz"}, +// expected: []interface{}{"OK", float64(0), float64(6), "\x07\x8d\x0c\x06\x04\x14", float64(1), float64(0), float64(7), "\x07\r\x0c\x06\x04\x14@", float64(1), float64(7), "\x07\r\x0c\x06\x04\x14\x00"}, +// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"}, +// }, +// { +// name: "BITOP XOR of keys containing integers and get the destkey", +// cmds: []string{"SET foo 10", "SET baz 5", "BITOP XOR bazz foo baz", "GET bazz"}, +// expected: []interface{}{"OK", "OK", float64(2), "\x040"}, +// assertType: []string{"equal", "equal", "equal", "equal"}, +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Delete the key before running the test +// _, _ = exec.FireCommandAndReadResponse(conn, "DEL foo") +// _, _ = exec.FireCommandAndReadResponse(conn, "DEL baz") +// _, _ = exec.FireCommandAndReadResponse(conn, "DEL bazz") +// _, _ = exec.FireCommandAndReadResponse(conn, "DEL bazzz") +// for i := 0; i < len(tc.cmds); i++ { +// res, err := exec.FireCommandAndReadResponse(conn, tc.cmds[i]) +// assert.Nil(t, err) + +// switch tc.assertType[i] { +// case "equal": +// assert.Equal(t, tc.expected[i], res) +// case "less": +// assert.True(t, res.(float64) <= tc.expected[i].(float64), "CMD: %s Expected %d to be less than or equal to %d", tc.cmds[i], res, tc.expected[i]) +// } +// } +// }) +// } +// } + +func TestBitCount(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + testcases := []struct { + InCmds []string + Out []interface{} + }{ + { + InCmds: []string{"SETBIT mykey 7 1"}, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []string{"SETBIT mykey 7 1"}, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []string{"SETBIT mykey 122 1"}, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []string{"GETBIT mykey 122"}, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []string{"SETBIT mykey 122 0"}, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []string{"GETBIT mykey 122"}, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []string{"GETBIT mykey 1223232"}, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []string{"GETBIT mykey 7"}, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []string{"GETBIT mykey 8"}, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []string{"BITCOUNT mykey 3 7 BIT"}, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []string{"BITCOUNT mykey 3 7"}, + Out: []interface{}{float64(0)}, + }, + { + InCmds: []string{"BITCOUNT mykey 0 0"}, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []string{"BITCOUNT"}, + Out: []interface{}{"ERR wrong number of arguments for 'bitcount' command"}, + }, + { + InCmds: []string{"BITCOUNT mykey"}, + Out: []interface{}{float64(1)}, + }, + { + InCmds: []string{"BITCOUNT mykey 0"}, + Out: []interface{}{"ERR syntax error"}, + }, + } + + for _, tcase := range testcases { + for i := 0; i < len(tcase.InCmds); i++ { + cmd := tcase.InCmds[i] + out := tcase.Out[i] + res, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, out, res, "Value mismatch for cmd %s\n.", cmd) + } + } +} + +func TestBitPos(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + testcases := []struct { + name string + val interface{} + inCmd string + out interface{} + setCmdSETBIT bool + }{ + { + name: "String interval BIT 0,-1 ", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 0 -1 bit", + out: float64(0), + }, + { + name: "String interval BIT 8,-1", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 8 -1 bit", + out: float64(8), + }, + { + name: "String interval BIT 16,-1", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 16 -1 bit", + out: float64(16), + }, + { + name: "String interval BIT 16,200", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 16 200 bit", + out: float64(16), + }, + { + name: "String interval BIT 8,8", + val: "\\x00\\xff\\x00", + inCmd: "BITPOS testkey 0 8 8 bit", + out: float64(8), + }, + { + name: "FindsFirstZeroBit", + val: "\xff\xf0\x00", + inCmd: "BITPOS testkey 0", + out: float64(12), + }, + { + name: "FindsFirstOneBit", + val: "\x00\x0f\xff", + inCmd: "BITPOS testkey 1", + out: float64(12), + }, + { + name: "NoOneBitFound", + val: "\x00\x00\x00", + inCmd: "BITPOS testkey 1", + out: float64(-1), + }, + { + name: "NoZeroBitFound", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0", + out: float64(24), + }, + { + name: "NoZeroBitFoundWithRangeStartPos", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 2", + out: float64(24), + }, + { + name: "NoZeroBitFoundWithOOBRangeStartPos", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 4", + out: float64(-1), + }, + { + name: "NoZeroBitFoundWithRange", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 2 2", + out: float64(-1), + }, + { + name: "NoZeroBitFoundWithRangeAndRangeType", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 2 2 BIT", + out: float64(-1), + }, + { + name: "FindsFirstZeroBitInRange", + val: "\xff\xf0\xff", + inCmd: "BITPOS testkey 0 1 2", + out: float64(12), + }, + { + name: "FindsFirstOneBitInRange", + val: "\x00\x00\xf0", + inCmd: "BITPOS testkey 1 2 3", + out: float64(16), + }, + { + name: "StartGreaterThanEnd", + val: "\xff\xf0\x00", + inCmd: "BITPOS testkey 0 3 2", + out: float64(-1), + }, + { + name: "FindsFirstOneBitWithNegativeStart", + val: "\x00\x00\xf0", + inCmd: "BITPOS testkey 1 -2 -1", + out: float64(16), + }, + { + name: "FindsFirstZeroBitWithNegativeEnd", + val: "\xff\xf0\xff", + inCmd: "BITPOS testkey 0 1 -1", + out: float64(12), + }, + { + name: "FindsFirstZeroBitInByteRange", + val: "\xff\x00\xff", + inCmd: "BITPOS testkey 0 1 2 BYTE", + out: float64(8), + }, + { + name: "FindsFirstOneBitInBitRange", + val: "\x00\x01\x00", + inCmd: "BITPOS testkey 1 0 16 BIT", + out: float64(15), + }, + { + name: "NoBitFoundInByteRange", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 0 2 BYTE", + out: float64(-1), + }, + { + name: "NoBitFoundInBitRange", + val: "\x00\x00\x00", + inCmd: "BITPOS testkey 1 0 23 BIT", + out: float64(-1), + }, + { + name: "EmptyStringReturnsMinusOneForZeroBit", + val: "\"\"", + inCmd: "BITPOS testkey 0", + out: float64(-1), + }, + { + name: "EmptyStringReturnsMinusOneForOneBit", + val: "\"\"", + inCmd: "BITPOS testkey 1", + out: float64(-1), + }, + { + name: "SingleByteString", + val: "\x80", + inCmd: "BITPOS testkey 1", + out: float64(0), + }, + { + name: "RangeExceedsStringLength", + val: "\x00\xff", + inCmd: "BITPOS testkey 1 0 20 BIT", + out: float64(8), + }, + { + name: "InvalidBitArgument", + inCmd: "BITPOS testkey 2", + out: "ERR the bit argument must be 1 or 0", + }, + { + name: "NonIntegerStartParameter", + inCmd: "BITPOS testkey 0 start", + out: "ERR value is not an integer or out of range", + }, + { + name: "NonIntegerEndParameter", + inCmd: "BITPOS testkey 0 1 end", + out: "ERR value is not an integer or out of range", + }, + { + name: "InvalidRangeType", + inCmd: "BITPOS testkey 0 1 2 BYTEs", + out: "ERR syntax error", + }, + { + name: "InsufficientArguments", + inCmd: "BITPOS testkey", + out: "ERR wrong number of arguments for 'bitpos' command", + }, + { + name: "NonExistentKeyForZeroBit", + inCmd: "BITPOS nonexistentkey 0", + out: float64(0), + }, + { + name: "NonExistentKeyForOneBit", + inCmd: "BITPOS nonexistentkey 1", + out: float64(-1), + }, + { + name: "IntegerValue", + val: 65280, // 0xFF00 in decimal + inCmd: "BITPOS testkey 0", + out: float64(0), + }, + { + name: "LargeIntegerValue", + val: 16777215, // 0xFFFFFF in decimal + inCmd: "BITPOS testkey 1", + out: float64(2), + }, + { + name: "SmallIntegerValue", + val: 1, // 0x01 in decimal + inCmd: "BITPOS testkey 0", + out: float64(0), + }, + { + name: "ZeroIntegerValue", + val: 0, + inCmd: "BITPOS testkey 1", + out: float64(2), + }, + { + name: "BitRangeStartGreaterThanBitLength", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 25 30 BIT", + out: float64(-1), + }, + { + name: "BitRangeEndExceedsBitLength", + val: "\xff\xff\xff", + inCmd: "BITPOS testkey 0 0 30 BIT", + out: float64(-1), + }, + { + name: "NegativeStartInBitRange", + val: "\x00\xff\xff", + inCmd: "BITPOS testkey 1 -16 -1 BIT", + out: float64(8), + }, + { + name: "LargeNegativeStart", + val: "\x00\xff\xff", + inCmd: "BITPOS testkey 1 -100 -1", + out: float64(8), + }, + { + name: "LargePositiveEnd", + val: "\x00\xff\xff", + inCmd: "BITPOS testkey 1 0 100", + out: float64(8), + }, + { + name: "StartAndEndEqualInByteRange", + val: "\x0f\xff\xff", + inCmd: "BITPOS testkey 0 1 1 BYTE", + out: float64(-1), + }, + { + name: "StartAndEndEqualInBitRange", + val: "\x0f\xff\xff", + inCmd: "BITPOS testkey 1 1 1 BIT", + out: float64(-1), + }, + { + name: "FindFirstZeroBitInNegativeRange", + val: "\xff\x00\xff", + inCmd: "BITPOS testkey 0 -2 -1", + out: float64(8), + }, + { + name: "FindFirstOneBitInNegativeRangeBIT", + val: "\x00\x00\x80", + inCmd: "BITPOS testkey 1 -8 -1 BIT", + out: float64(16), + }, + { + name: "MaxIntegerValue", + val: math.MaxInt64, + inCmd: "BITPOS testkey 0", + out: float64(0), + }, + { + name: "MinIntegerValue", + val: math.MinInt64, + inCmd: "BITPOS testkey 1", + out: float64(2), + }, + { + name: "SingleBitStringZero", + val: "\x00", + inCmd: "BITPOS testkey 1", + out: float64(-1), + }, + { + name: "SingleBitStringOne", + val: "\x01", + inCmd: "BITPOS testkey 0", + out: float64(0), + }, + { + name: "AllBitsSetExceptLast", + val: "\xff\xff\xfe", + inCmd: "BITPOS testkey 0", + out: float64(23), + }, + { + name: "OnlyLastBitSet", + val: "\x00\x00\x01", + inCmd: "BITPOS testkey 1", + out: float64(23), + }, + { + name: "AlternatingBitsLongString", + val: "\xaa\xaa\xaa\xaa\xaa", + inCmd: "BITPOS testkey 0", + out: float64(1), + }, + { + name: "VeryLargeByteString", + val: strings.Repeat("\xff", 1000) + "\x00", + inCmd: "BITPOS testkey 0", + out: float64(8000), + }, + { + name: "FindZeroBitOnSetBitKey", + val: "8 1", + inCmd: "BITPOS testkeysb 1", + out: float64(8), + setCmdSETBIT: true, + }, + { + name: "FindOneBitOnSetBitKey", + val: "1 1", + inCmd: "BITPOS testkeysb 1", + out: float64(1), + setCmdSETBIT: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var setCmd string + if tc.setCmdSETBIT { + setCmd = fmt.Sprintf("SETBIT testkeysb %s", tc.val.(string)) + } else { + switch v := tc.val.(type) { + case string: + setCmd = fmt.Sprintf("SET testkey %s", v) + case int: + setCmd = fmt.Sprintf("SET testkey %d", v) + default: + // For test cases where we don't set a value (e.g., error cases) + setCmd = "" + } + } + + if setCmd != "" { + _, _ = exec.FireCommandAndReadResponse(conn, setCmd) + } + + result, err := exec.FireCommandAndReadResponse(conn, tc.inCmd) + assert.Nil(t, err) + assert.Equal(t, tc.out, result, "Mismatch for cmd %s\n", tc.inCmd) + }) + } +} + +func TestBitfield(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + defer conn.Close() + + _, _ = exec.FireCommandAndReadResponse(conn, "FLUSHDB") + defer exec.FireCommand(conn, "FLUSHDB") // clean up after all test cases + syntaxErrMsg := "ERR syntax error" + bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is" + integerErrMsg := "ERR value is not an integer or out of range" + overflowErrMsg := "ERR Invalid OVERFLOW type specified" + + testCases := []struct { + Name string + Commands []string + Expected []interface{} + Delay []time.Duration + CleanUp []string + }{ + { + Name: "BITFIELD Arity Check", + Commands: []string{"bitfield"}, + Expected: []interface{}{"ERR wrong number of arguments for 'bitfield' command"}, + Delay: []time.Duration{0}, + CleanUp: []string{}, + }, + { + Name: "BITFIELD on unsupported type of SET", + Commands: []string{"SADD bits a b c", "bitfield bits"}, + Expected: []interface{}{float64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD on unsupported type of JSON", + Commands: []string{"json.set bits $ 1", "bitfield bits"}, + Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD on unsupported type of HSET", + Commands: []string{"HSET bits a 1", "bitfield bits"}, + Expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD with syntax errors", + Commands: []string{ + "bitfield bits set u8 0 255 incrby u8 0 100 get u8", + "bitfield bits set a8 0 255 incrby u8 0 100 get u8", + "bitfield bits set u8 a 255 incrby u8 0 100 get u8", + "bitfield bits set u8 0 255 incrby u8 0 100 overflow wraap", + "bitfield bits set u8 0 incrby u8 0 100 get u8 288", + }, + Expected: []interface{}{ + syntaxErrMsg, + bitFieldTypeErrMsg, + "ERR bit offset is not an integer or out of range", + overflowErrMsg, + integerErrMsg, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"Del bits"}, + }, + { + Name: "BITFIELD signed SET and GET basics", + Commands: []string{"bitfield bits set i8 0 -100", "bitfield bits set i8 0 101", "bitfield bits get i8 0"}, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(-100)}, []interface{}{float64(101)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD unsigned SET and GET basics", + Commands: []string{"bitfield bits set u8 0 255", "bitfield bits set u8 0 100", "bitfield bits get u8 0"}, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(255)}, []interface{}{float64(100)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD signed SET and GET together", + Commands: []string{"bitfield bits set i8 0 255 set i8 0 100 get i8 0"}, + Expected: []interface{}{[]interface{}{float64(0), float64(-1), float64(100)}}, + Delay: []time.Duration{0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD unsigned with SET, GET and INCRBY arguments", + Commands: []string{"bitfield bits set u8 0 255 incrby u8 0 100 get u8 0"}, + Expected: []interface{}{[]interface{}{float64(0), float64(99), float64(99)}}, + Delay: []time.Duration{0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD with only key as argument", + Commands: []string{"bitfield bits"}, + Expected: []interface{}{[]interface{}{}}, + Delay: []time.Duration{0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD # form", + Commands: []string{ + "bitfield bits set u8 #0 65", + "bitfield bits set u8 #1 66", + "bitfield bits set u8 #2 67", + "get bits", + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(0)}, []interface{}{float64(0)}, "ABC"}, + Delay: []time.Duration{0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD basic INCRBY form", + Commands: []string{ + "bitfield bits set u8 #0 10", + "bitfield bits incrby u8 #0 100", + "bitfield bits incrby u8 #0 100", + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(110)}, []interface{}{float64(210)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD chaining of multiple commands", + Commands: []string{ + "bitfield bits set u8 #0 10", + "bitfield bits incrby u8 #0 100 incrby u8 #0 100", + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(110), float64(210)}}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD unsigned overflow wrap", + Commands: []string{ + "bitfield bits set u8 #0 100", + "bitfield bits overflow wrap incrby u8 #0 257", + "bitfield bits get u8 #0", + "bitfield bits overflow wrap incrby u8 #0 255", + "bitfield bits get u8 #0", + }, + Expected: []interface{}{ + []interface{}{float64(0)}, + []interface{}{float64(101)}, + []interface{}{float64(101)}, + []interface{}{float64(100)}, + []interface{}{float64(100)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD unsigned overflow sat", + Commands: []string{ + "bitfield bits set u8 #0 100", + "bitfield bits overflow sat incrby u8 #0 257", + "bitfield bits get u8 #0", + "bitfield bits overflow sat incrby u8 #0 -255", + "bitfield bits get u8 #0", + }, + Expected: []interface{}{ + []interface{}{float64(0)}, + []interface{}{float64(255)}, + []interface{}{float64(255)}, + []interface{}{float64(0)}, + []interface{}{float64(0)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD signed overflow wrap", + Commands: []string{ + "bitfield bits set i8 #0 100", + "bitfield bits overflow wrap incrby i8 #0 257", + "bitfield bits get i8 #0", + "bitfield bits overflow wrap incrby i8 #0 255", + "bitfield bits get i8 #0", + }, + Expected: []interface{}{ + []interface{}{float64(0)}, + []interface{}{float64(101)}, + []interface{}{float64(101)}, + []interface{}{float64(100)}, + []interface{}{float64(100)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD signed overflow sat", + Commands: []string{ + "bitfield bits set u8 #0 100", + "bitfield bits overflow sat incrby i8 #0 257", + "bitfield bits get i8 #0", + "bitfield bits overflow sat incrby i8 #0 -255", + "bitfield bits get i8 #0", + }, + Expected: []interface{}{ + []interface{}{float64(0)}, + []interface{}{float64(127)}, + []interface{}{float64(127)}, + []interface{}{float64(-128)}, + []interface{}{float64(-128)}, + }, + Delay: []time.Duration{0, 0, 0, 0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD regression 1", + Commands: []string{"set bits 1", "bitfield bits get u1 0"}, + Expected: []interface{}{"OK", []interface{}{float64(0)}}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD regression 2", + Commands: []string{ + "bitfield mystring set i8 0 10", + "bitfield mystring set i8 64 10", + "bitfield mystring incrby i8 10 99900", + }, + Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(0)}, []interface{}{float64(60)}}, + Delay: []time.Duration{0, 0, 0}, + CleanUp: []string{"DEL mystring"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + + for i := 0; i < len(tc.Commands); i++ { + if tc.Delay[i] > 0 { + time.Sleep(tc.Delay[i]) + } + result, err := exec.FireCommandAndReadResponse(conn, tc.Commands[i]) + assert.Nil(t, err) + expected := tc.Expected[i] + assert.Equal(t, expected, result) + } + + for _, cmd := range tc.CleanUp { + _, _ = exec.FireCommandAndReadResponse(conn, cmd) + } + }) + } +} + +func TestBitfieldRO(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + defer conn.Close() + + _, _ = exec.FireCommandAndReadResponse(conn, "FLUSHDB") + defer exec.FireCommand(conn, "FLUSHDB") + + syntaxErrMsg := "ERR syntax error" + bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is" + unsupportedCmdErrMsg := "ERR BITFIELD_RO only supports the GET subcommand" + + testCases := []struct { + Name string + Commands []string + Expected []interface{} + Delay []time.Duration + CleanUp []string + }{ + { + Name: "BITFIELD_RO Arity Check", + Commands: []string{"bitfield_ro"}, + Expected: []interface{}{"ERR wrong number of arguments for 'bitfield_ro' command"}, + Delay: []time.Duration{0}, + CleanUp: []string{}, + }, + { + Name: "BITFIELD_RO on unsupported type of SET", + Commands: []string{"SADD bits a b c", "bitfield_ro bits"}, + Expected: []interface{}{float64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD_RO on unsupported type of JSON", + Commands: []string{"json.set bits $ 1", "bitfield_ro bits"}, + Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD_RO on unsupported type of HSET", + Commands: []string{"HSET bits a 1", "bitfield_ro bits"}, + Expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"DEL bits"}, + }, + { + Name: "BITFIELD_RO with unsupported commands", + Commands: []string{ + "bitfield_ro bits set u8 0 255", + "bitfield_ro bits incrby u8 0 100", + }, + Expected: []interface{}{ + unsupportedCmdErrMsg, + unsupportedCmdErrMsg, + }, + Delay: []time.Duration{0, 0}, + CleanUp: []string{"Del bits"}, + }, + { + Name: "BITFIELD_RO with syntax error", + Commands: []string{ + "set bits 1", + "bitfield_ro bits get u8", + "bitfield_ro bits get", + "bitfield_ro bits get somethingrandom", + }, + Expected: []interface{}{ + "OK", + syntaxErrMsg, + syntaxErrMsg, + syntaxErrMsg, + }, + Delay: []time.Duration{0, 0, 0, 0}, + CleanUp: []string{"Del bits"}, + }, + { + Name: "BITFIELD_RO with invalid bitfield type", + Commands: []string{ + "set bits 1", + "bitfield_ro bits get a8 0", + "bitfield_ro bits get s8 0", + "bitfield_ro bits get somethingrandom 0", + }, + Expected: []interface{}{ + "OK", + bitFieldTypeErrMsg, + bitFieldTypeErrMsg, + bitFieldTypeErrMsg, + }, + Delay: []time.Duration{0, 0, 0, 0}, + CleanUp: []string{"Del bits"}, + }, + { + Name: "BITFIELD_RO with only key as argument", + Commands: []string{"bitfield_ro bits"}, + Expected: []interface{}{[]interface{}{}}, + Delay: []time.Duration{0}, + CleanUp: []string{"DEL bits"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + + for i := 0; i < len(tc.Commands); i++ { + if tc.Delay[i] > 0 { + time.Sleep(tc.Delay[i]) + } + result, err := exec.FireCommandAndReadResponse(conn, tc.Commands[i]) + assert.Nil(t, err) + expected := tc.Expected[i] + assert.Equal(t, expected, result) + } + + for _, cmd := range tc.CleanUp { + _, _ = exec.FireCommandAndReadResponse(conn, cmd) + } + }) + } +} diff --git a/internal/eval/bitpos.go b/internal/eval/bitpos.go index 43913d559..701a5ce74 100644 --- a/internal/eval/bitpos.go +++ b/internal/eval/bitpos.go @@ -10,9 +10,12 @@ import ( dstore "github.com/dicedb/dice/internal/store" ) -func evalBITPOS(args []string, store *dstore.Store) []byte { +func evalBITPOS(args []string, store *dstore.Store) *EvalResponse { if len(args) < 2 || len(args) > 5 { - return diceerrors.NewErrArity("BITPOS") + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("BITPOS"), + } } key := args[0] @@ -20,36 +23,54 @@ func evalBITPOS(args []string, store *dstore.Store) []byte { bitToFind, err := parseBitToFind(args[1]) if err != nil { - return diceerrors.NewErrWithMessage(err.Error()) + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral(err.Error()), + } } if obj == nil { if bitToFind == 0 { - return clientio.Encode(0, true) + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } } - return clientio.Encode(-1, true) + return &EvalResponse{ + Result: clientio.IntegerNegativeOne, + Error: nil, + } } byteSlice, err := getValueAsByteSlice(obj) if err != nil { - return diceerrors.NewErrWithMessage(err.Error()) + return &EvalResponse{ + Result: nil, + Error: err, + } } start, end, rangeType, endRangeProvided, err := parseOptionalParams(args[2:], len(byteSlice)) if err != nil { - return diceerrors.NewErrWithMessage(err.Error()) + return &EvalResponse{ + Result: nil, + Error: err, + } } result := getBitPos(byteSlice, bitToFind, start, end, rangeType, endRangeProvided) - return clientio.Encode(result, true) + return &EvalResponse{ + Result: result, + Error: nil, + } } func parseBitToFind(arg string) (byte, error) { bitToFindInt, err := strconv.Atoi(arg) if err != nil { - return 0, errors.New("value is not an integer or out of range") + return 0, diceerrors.ErrIntegerOutOfRange } if bitToFindInt != 0 && bitToFindInt != 1 { @@ -66,14 +87,14 @@ func parseOptionalParams(args []string, byteLen int) (start, end int, rangeType if len(args) > 0 { start, err = strconv.Atoi(args[0]) if err != nil { - return 0, 0, "", false, errors.New("value is not an integer or out of range") + return 0, 0, "", false, diceerrors.ErrIntegerOutOfRange } } if len(args) > 1 { end, err = strconv.Atoi(args[1]) if err != nil { - return 0, 0, "", false, errors.New("value is not an integer or out of range") + return 0, 0, "", false, diceerrors.ErrIntegerOutOfRange } endRangeProvided = true } @@ -81,7 +102,7 @@ func parseOptionalParams(args []string, byteLen int) (start, end int, rangeType if len(args) > 2 { rangeType = strings.ToUpper(args[2]) if rangeType != BYTE && rangeType != BIT { - return 0, 0, "", false, errors.New("syntax error") + return 0, 0, "", false, diceerrors.ErrSyntax } } return start, end, rangeType, endRangeProvided, err @@ -152,7 +173,6 @@ func getBitPosWithBitRange(byteSlice []byte, bitToFind byte, start, end int) int return i } } - // Bit not found in the range return -1 } diff --git a/internal/eval/commands.go b/internal/eval/commands.go index b3001fdf3..71fd7eaf7 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -537,20 +537,23 @@ var ( Arity: 1, } setBitCmdMeta = DiceCmdMeta{ - Name: "SETBIT", - Info: "SETBIT sets or clears the bit at offset in the string value stored at key", - Eval: evalSETBIT, + Name: "SETBIT", + Info: "SETBIT sets or clears the bit at offset in the string value stored at key", + IsMigrated: true, + NewEval: evalSETBIT, } getBitCmdMeta = DiceCmdMeta{ - Name: "GETBIT", - Info: "GETBIT returns the bit value at offset in the string value stored at key", - Eval: evalGETBIT, + Name: "GETBIT", + Info: "GETBIT returns the bit value at offset in the string value stored at key", + IsMigrated: true, + NewEval: evalGETBIT, } bitCountCmdMeta = DiceCmdMeta{ - Name: "BITCOUNT", - Info: "BITCOUNT counts the number of set bits in the string value stored at key", - Eval: evalBITCOUNT, - Arity: -1, + Name: "BITCOUNT", + Info: "BITCOUNT counts the number of set bits in the string value stored at key", + Arity: -1, + IsMigrated: true, + NewEval: evalBITCOUNT, } bitOpCmdMeta = DiceCmdMeta{ Name: "BITOP", @@ -912,8 +915,9 @@ var ( RESP encoded -1 in case the bit argument is 1 and the string is empty or composed of just zero bytes. RESP encoded -1 if we look for set bits and the string is empty or composed of just zero bytes, -1 is returned. RESP encoded -1 if a clear bit isn't found in the specified range.`, - Eval: evalBITPOS, - Arity: -2, + IsMigrated: true, + NewEval: evalBITPOS, + Arity: -2, } saddCmdMeta = DiceCmdMeta{ Name: "SADD", @@ -1214,17 +1218,19 @@ var ( There is another subcommand that only changes the behavior of successive INCRBY and SET subcommands calls by setting the overflow behavior: OVERFLOW [WRAP|SAT|FAIL]`, - Arity: -1, - KeySpecs: KeySpecs{BeginIndex: 1}, - Eval: evalBITFIELD, + Arity: -1, + KeySpecs: KeySpecs{BeginIndex: 1}, + IsMigrated: true, + NewEval: evalBITFIELD, } bitfieldroCmdMeta = DiceCmdMeta{ Name: "BITFIELD_RO", Info: `It is read-only variant of the BITFIELD command. It is like the original BITFIELD but only accepts GET subcommand.`, - Arity: -1, - KeySpecs: KeySpecs{BeginIndex: 1}, - Eval: evalBITFIELDRO, + Arity: -1, + KeySpecs: KeySpecs{BeginIndex: 1}, + IsMigrated: true, + NewEval: evalBITFIELDRO, } hincrbyFloatCmdMeta = DiceCmdMeta{ Name: "HINCRBYFLOAT", diff --git a/internal/eval/eval.go b/internal/eval/eval.go index ee6480472..8bf1fb8c5 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -6,7 +6,6 @@ import ( "fmt" "log/slog" "math" - "math/bits" "regexp" "sort" "strconv" @@ -1185,268 +1184,6 @@ func EvalQUNWATCH(args []string, httpOp bool, client *comm.Client) []byte { return clientio.RespOK } -// SETBIT key offset value -func evalSETBIT(args []string, store *dstore.Store) []byte { - var err error - - if len(args) != 3 { - return diceerrors.NewErrArity("SETBIT") - } - - key := args[0] - offset, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - return diceerrors.NewErrWithMessage("bit offset is not an integer or out of range") - } - - value, err := strconv.ParseBool(args[2]) - if err != nil { - return diceerrors.NewErrWithMessage("bit is not an integer or out of range") - } - - obj := store.Get(key) - requiredByteArraySize := offset>>3 + 1 - - if obj == nil { - obj = store.NewObj(NewByteArray(int(requiredByteArraySize)), -1, object.ObjTypeByteArray, object.ObjEncodingByteArray) - store.Put(args[0], obj) - } - - if object.AssertType(obj.TypeEncoding, object.ObjTypeByteArray) == nil || - object.AssertType(obj.TypeEncoding, object.ObjTypeString) == nil || - object.AssertType(obj.TypeEncoding, object.ObjTypeInt) == nil { - var byteArray *ByteArray - oType, oEnc := object.ExtractTypeEncoding(obj) - - switch oType { - case object.ObjTypeByteArray: - byteArray = obj.Value.(*ByteArray) - case object.ObjTypeString, object.ObjTypeInt: - byteArray, err = NewByteArrayFromObj(obj) - if err != nil { - return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr) - } - default: - return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr) - } - - // Perform the resizing check - byteArrayLength := byteArray.Length - - // check whether resize required or not - if requiredByteArraySize > byteArrayLength { - // resize as per the offset - byteArray = byteArray.IncreaseSize(int(requiredByteArraySize)) - } - - resp := byteArray.GetBit(int(offset)) - byteArray.SetBit(int(offset), value) - - // We are returning newObject here so it is thread-safe - // Old will be removed by GC - newObj, err := ByteSliceToObj(store, obj, byteArray.data, oType, oEnc) - if err != nil { - return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr) - } - - exp, ok := dstore.GetExpiry(obj, store) - var exDurationMs int64 = -1 - if ok { - exDurationMs = int64(exp - uint64(utils.GetCurrentTime().UnixMilli())) - } - // newObj has bydefault expiry time -1 , we need to set it - if exDurationMs > 0 { - store.SetExpiry(newObj, exDurationMs) - } - - store.Put(key, newObj) - if resp { - return clientio.Encode(1, true) - } - return clientio.Encode(0, true) - } - return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr) -} - -// GETBIT key offset -func evalGETBIT(args []string, store *dstore.Store) []byte { - var err error - - if len(args) != 2 { - return diceerrors.NewErrArity("GETBIT") - } - - key := args[0] - offset, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - return diceerrors.NewErrWithMessage("bit offset is not an integer or out of range") - } - - obj := store.Get(key) - if obj == nil { - return clientio.Encode(0, true) - } - - requiredByteArraySize := offset>>3 + 1 - switch oType, _ := object.ExtractTypeEncoding(obj); oType { - case object.ObjTypeSet: - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - case object.ObjTypeByteArray: - byteArray := obj.Value.(*ByteArray) - byteArrayLength := byteArray.Length - - // check whether offset, length exists or not - if requiredByteArraySize > byteArrayLength { - return clientio.Encode(0, true) - } - value := byteArray.GetBit(int(offset)) - if value { - return clientio.Encode(1, true) - } - return clientio.Encode(0, true) - case object.ObjTypeString, object.ObjTypeInt: - byteArray, err := NewByteArrayFromObj(obj) - if err != nil { - return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr) - } - if requiredByteArraySize > byteArray.Length { - return clientio.Encode(0, true) - } - value := byteArray.GetBit(int(offset)) - if value { - return clientio.Encode(1, true) - } - return clientio.Encode(0, true) - default: - return clientio.Encode(0, true) - } -} - -func evalBITCOUNT(args []string, store *dstore.Store) []byte { - var err error - - // if no key is provided, return error - if len(args) == 0 { - return diceerrors.NewErrArity("BITCOUNT") - } - - // if more than 4 arguments are provided, return error - if len(args) > 4 { - return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) - } - - // fetching value of the key - key := args[0] - obj := store.Get(key) - if obj == nil { - return clientio.Encode(0, false) - } - - var value []byte - var valueLength int64 - - switch { - case object.AssertType(obj.TypeEncoding, object.ObjTypeByteArray) == nil: - byteArray := obj.Value.(*ByteArray) - value = byteArray.data - valueLength = byteArray.Length - case object.AssertType(obj.TypeEncoding, object.ObjTypeString) == nil: - value = []byte(obj.Value.(string)) - valueLength = int64(len(value)) - case object.AssertType(obj.TypeEncoding, object.ObjTypeInt) == nil: - value = []byte(strconv.FormatInt(obj.Value.(int64), 10)) - valueLength = int64(len(value)) - default: - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - } - - // defining constants of the function - start, end := int64(0), valueLength-1 - unit := BYTE - - // checking which arguments are present and validating arguments - if len(args) > 1 { - start, err = strconv.ParseInt(args[1], 10, 64) - if err != nil { - return diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr) - } - if len(args) <= 2 { - return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) - } - end, err = strconv.ParseInt(args[2], 10, 64) - if err != nil { - return diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr) - } - } - if len(args) > 3 { - unit = strings.ToUpper(args[3]) - } - - switch unit { - case BYTE: - if start < 0 { - start += valueLength - } - if end < 0 { - end += valueLength - } - if start > end || start >= valueLength { - return clientio.Encode(0, true) - } - end = min(end, valueLength-1) - bitCount := 0 - for i := start; i <= end; i++ { - bitCount += bits.OnesCount8(value[i]) - } - return clientio.Encode(bitCount, true) - case BIT: - if start < 0 { - start += valueLength * 8 - } - if end < 0 { - end += valueLength * 8 - } - if start > end { - return clientio.Encode(0, true) - } - startByte, endByte := start/8, min(end/8, valueLength-1) - startBitOffset, endBitOffset := start%8, end%8 - - if endByte == valueLength-1 { - endBitOffset = 7 - } - - if startByte >= valueLength { - return clientio.Encode(0, true) - } - - bitCount := 0 - - // Use bit masks to count the bits instead of a loop - if startByte == endByte { - mask := byte(0xFF >> startBitOffset) - mask &= byte(0xFF << (7 - endBitOffset)) - bitCount = bits.OnesCount8(value[startByte] & mask) - } else { - // Handle first byte - firstByteMask := byte(0xFF >> startBitOffset) - bitCount += bits.OnesCount8(value[startByte] & firstByteMask) - - // Handle all the middle ones - for i := startByte + 1; i < endByte; i++ { - bitCount += bits.OnesCount8(value[i]) - } - - // Handle last byte - lastByteMask := byte(0xFF << (7 - endBitOffset)) - bitCount += bits.OnesCount8(value[endByte] & lastByteMask) - } - return clientio.Encode(bitCount, true) - default: - return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) - } -} - // BITOP destkey key [key ...] func evalBITOP(args []string, store *dstore.Store) []byte { operation, destKey := args[0], args[1] @@ -2689,70 +2426,6 @@ func executeBitfieldOps(value *ByteArray, ops []utils.BitFieldOp) []interface{} } return result } - -// Generic method for both BITFIELD and BITFIELD_RO. -// isReadOnly method is true for BITFIELD_RO command. -func bitfieldEvalGeneric(args []string, store *dstore.Store, isReadOnly bool) []byte { - var ops []utils.BitFieldOp - ops, err2 := utils.ParseBitfieldOps(args, isReadOnly) - - if err2 != nil { - return err2 - } - - key := args[0] - obj := store.Get(key) - if obj == nil { - obj = store.NewObj(NewByteArray(1), -1, object.ObjTypeByteArray, object.ObjEncodingByteArray) - store.Put(args[0], obj) - } - var value *ByteArray - var err error - - switch oType, _ := object.ExtractTypeEncoding(obj); oType { - case object.ObjTypeByteArray: - value = obj.Value.(*ByteArray) - case object.ObjTypeString, object.ObjTypeInt: - value, err = NewByteArrayFromObj(obj) - if err != nil { - return diceerrors.NewErrWithMessage("value is not a valid byte array") - } - default: - return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr) - } - - result := executeBitfieldOps(value, ops) - return clientio.Encode(result, false) -} - -// evalBITFIELD evaluates BITFIELD operations on a key store string, int or bytearray types -// it returns an array of results depending on the subcommands -// it allows mutation using SET and INCRBY commands -// returns arity error, offset type error, overflow type error, encoding type error, integer error, syntax error -// GET -- Returns the specified bit field. -// SET -- Set the specified bit field -// and returns its old value. -// INCRBY -- Increments or decrements -// (if a negative increment is given) the specified bit field and returns the new value. -// There is another subcommand that only changes the behavior of successive -// INCRBY and SET subcommands calls by setting the overflow behavior: -// OVERFLOW [WRAP|SAT|FAIL]` -func evalBITFIELD(args []string, store *dstore.Store) []byte { - if len(args) < 1 { - return diceerrors.NewErrArity("BITFIELD") - } - - return bitfieldEvalGeneric(args, store, false) -} - -// Read-only variant of the BITFIELD command. It is like the original BITFIELD but only accepts GET subcommand and can safely be used in read-only replicas. -func evalBITFIELDRO(args []string, store *dstore.Store) []byte { - if len(args) < 1 { - return diceerrors.NewErrArity("BITFIELD_RO") - } - - return bitfieldEvalGeneric(args, store, true) -} func evalGEOADD(args []string, store *dstore.Store) []byte { if len(args) < 4 { return diceerrors.NewErrArity("GEOADD") diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index 1a832e846..7ff45c31c 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -6077,6 +6077,130 @@ func BenchmarkEvalINCRBYFLOAT(b *testing.B) { } } +// TODO: BITOP has not been migrated yet. Once done, we can uncomment the tests - please check accuracy and validate for expected values. + +// func testEvalBITOP(t *testing.T, store *dstore.Store) { +// tests := map[string]evalTestCase{ +// "BITOP NOT (empty string)": { +// setup: func() { +// store.Put("s{t}", store.NewObj(&ByteArray{data: []byte("")}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) +// }, +// input: []string{"NOT", "dest{t}", "s{t}"}, +// migratedOutput: EvalResponse{Result: clientio.IntegerZero, Error: nil}, +// newValidator: func(output interface{}) { +// expectedResult := []byte{} +// assert.Equal(t, expectedResult, store.Get("dest{t}").Value.(*ByteArray).data) +// }, +// }, +// "BITOP NOT (known string)": { +// setup: func() { +// store.Put("s{t}", store.NewObj(&ByteArray{data: []byte{0xaa, 0x00, 0xff, 0x55}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) +// }, +// input: []string{"NOT", "dest{t}", "s{t}"}, +// migratedOutput: EvalResponse{Result: 4, Error: nil}, +// newValidator: func(output interface{}) { +// expectedResult := []byte{0x55, 0xff, 0x00, 0xaa} +// assert.Equal(t, expectedResult, store.Get("dest{t}").Value.(*ByteArray).data) +// }, +// }, +// "BITOP where dest and target are the same key": { +// setup: func() { +// store.Put("s", store.NewObj(&ByteArray{data: []byte{0xaa, 0x00, 0xff, 0x55}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) +// }, +// input: []string{"NOT", "s", "s"}, +// migratedOutput: EvalResponse{Result: 4, Error: nil}, +// newValidator: func(output interface{}) { +// expectedResult := []byte{0x55, 0xff, 0x00, 0xaa} +// assert.Equal(t, expectedResult, store.Get("s").Value.(*ByteArray).data) +// }, +// }, +// "BITOP AND|OR|XOR don't change the string with single input key": { +// setup: func() { +// store.Put("a{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) +// }, +// input: []string{"AND", "res1{t}", "a{t}"}, +// migratedOutput: EvalResponse{Result: 3, Error: nil}, +// newValidator: func(output interface{}) { +// expectedResult := []byte{0x01, 0x02, 0xff} +// assert.Equal(t, expectedResult, store.Get("res1{t}").Value.(*ByteArray).data) +// }, +// }, +// "BITOP missing key is considered a stream of zero": { +// setup: func() { +// store.Put("a{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) +// }, +// input: []string{"AND", "res1{t}", "no-such-key{t}", "a{t}"}, +// migratedOutput: EvalResponse{Result: 3, Error: nil}, +// newValidator: func(output interface{}) { +// expectedResult := []byte{0x00, 0x00, 0x00} +// assert.Equal(t, expectedResult, store.Get("res1{t}").Value.(*ByteArray).data) +// }, +// }, +// "BITOP shorter keys are zero-padded to the key with max length": { +// setup: func() { +// store.Put("a{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) +// store.Put("b{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray)) +// }, +// input: []string{"AND", "res1{t}", "a{t}", "b{t}"}, +// migratedOutput: EvalResponse{Result: 4, Error: nil}, +// newValidator: func(output interface{}) { +// expectedResult := []byte{0x01, 0x02, 0xff, 0x00} +// assert.Equal(t, expectedResult, store.Get("res1{t}").Value.(*ByteArray).data) +// }, +// }, +// "BITOP with non string source key": { +// setup: func() { +// store.Put("a{t}", store.NewObj("1", maxExDuration, object.ObjTypeString, object.ObjEncodingRaw)) +// store.Put("b{t}", store.NewObj("2", maxExDuration, object.ObjTypeString, object.ObjEncodingRaw)) +// store.Put("c{t}", store.NewObj([]byte("foo"), maxExDuration, object.ObjTypeByteList, object.ObjEncodingRaw)) +// }, +// input: []string{"XOR", "dest{t}", "a{t}", "b{t}", "c{t}", "d{t}"}, +// migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrWrongTypeOperation}, +// }, +// "BITOP with empty string after non empty string": { +// setup: func() { +// store.Put("a{t}", store.NewObj(&ByteArray{data: []byte("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")}, -1, object.ObjTypeByteArray, object.ObjEncodingByteArray)) +// }, +// input: []string{"OR", "x{t}", "a{t}", "b{t}"}, +// migratedOutput: EvalResponse{Result: 32, Error: nil}, +// }, +// } + +// //runEvalTests(t, tests, evalBITOP, store) +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { + +// if tt.setup != nil { +// tt.setup() +// } +// response := evalBITOP(tt.input, store) + +// if tt.newValidator != nil { +// if tt.migratedOutput.Error != nil { +// tt.newValidator(tt.migratedOutput.Error) +// } else { +// tt.newValidator(response.Result) +// } +// } else { +// // Handle comparison for byte slices +// if b, ok := response.Result.([]byte); ok && tt.migratedOutput.Result != nil { +// if expectedBytes, ok := tt.migratedOutput.Result.([]byte); ok { +// assert.True(t, bytes.Equal(b, expectedBytes), "expected and actual byte slices should be equal") +// } +// } else { +// assert.Equal(t, tt.migratedOutput.Result, response.Result) +// } + +// if tt.migratedOutput.Error != nil { +// assert.EqualError(t, response.Error, tt.migratedOutput.Error.Error()) +// } else { +// assert.NoError(t, response.Error) +// } +// } +// }) +// } +// } + func testEvalBITOP(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ "BITOP NOT (empty string)": { @@ -7461,51 +7585,61 @@ func testEvalZCARD(t *testing.T, store *dstore.Store) { func testEvalBitField(t *testing.T, store *dstore.Store) { testCases := map[string]evalTestCase{ "BITFIELD signed SET": { - input: []string{"bits", "set", "i8", "0", "-100"}, - output: clientio.Encode([]int64{0}, false), + input: []string{"bits", "set", "i8", "0", "-100"}, + migratedOutput: EvalResponse{ + Result: []interface{}{int64(0)}, + Error: nil, + }, }, "BITFIELD GET": { setup: func() { args := []string{"bits", "set", "u8", "0", "255"} evalBITFIELD(args, store) }, - input: []string{"bits", "get", "u8", "0"}, - output: clientio.Encode([]int64{255}, false), + input: []string{"bits", "get", "u8", "0"}, + migratedOutput: EvalResponse{ + Result: []interface{}{int64(255)}, + Error: nil, + }, }, "BITFIELD INCRBY": { setup: func() { args := []string{"bits", "set", "u8", "0", "255"} evalBITFIELD(args, store) }, - input: []string{"bits", "incrby", "u8", "0", "100"}, - output: clientio.Encode([]int64{99}, false), + input: []string{"bits", "incrby", "u8", "0", "100"}, + migratedOutput: EvalResponse{ + Result: []interface{}{int64(99)}, + Error: nil, + }, }, "BITFIELD Arity": { - input: []string{}, - output: diceerrors.NewErrArity("BITFIELD"), + input: []string{}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrWrongArgumentCount("BITFIELD")}, }, "BITFIELD invalid combination of commands in a single operation": { - input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"}, - output: []byte("-ERR syntax error\r\n"), + input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrSyntax}, }, "BITFIELD invalid bitfield type": { - input: []string{"bits", "SET", "a8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"}, - output: []byte("-ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is.\r\n"), + input: []string{"bits", "SET", "a8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is")}, }, "BITFIELD invalid bit offset": { - input: []string{"bits", "SET", "u8", "a", "255", "INCRBY", "u8", "0", "100", "GET", "u8"}, - output: []byte("-ERR bit offset is not an integer or out of range\r\n"), + input: []string{"bits", "SET", "u8", "a", "255", "INCRBY", "u8", "0", "100", "GET", "u8"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("bit offset is not an integer or out of range")}, }, "BITFIELD invalid overflow type": { - input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "OVERFLOW", "wraap"}, - output: []byte("-ERR Invalid OVERFLOW type specified\r\n"), + input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "OVERFLOW", "wraap"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("Invalid OVERFLOW type specified")}, }, "BITFIELD missing arguments in SET": { - input: []string{"bits", "SET", "u8", "0", "INCRBY", "u8", "0", "100", "GET", "u8", "288"}, - output: []byte("-ERR value is not an integer or out of range\r\n"), + input: []string{"bits", "SET", "u8", "0", "INCRBY", "u8", "0", "100", "GET", "u8", "288"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrIntegerOutOfRange}, }, } - runEvalTests(t, testCases, evalBITFIELD, store) + + runMigratedEvalTests(t, testCases, evalBITFIELD, store) } func testEvalHINCRBYFLOAT(t *testing.T, store *dstore.Store) { @@ -7745,23 +7879,27 @@ func testEvalDUMP(t *testing.T, store *dstore.Store) { func testEvalBitFieldRO(t *testing.T, store *dstore.Store) { testCases := map[string]evalTestCase{ "BITFIELD_RO Arity": { - input: []string{}, - output: diceerrors.NewErrArity("BITFIELD_RO"), + input: []string{}, + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("BITFIELD_RO"), + }, }, "BITFIELD_RO syntax error": { - input: []string{"bits", "GET", "u8"}, - output: []byte("-ERR syntax error\r\n"), + input: []string{"bits", "GET", "u8"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrSyntax}, }, "BITFIELD_RO invalid bitfield type": { - input: []string{"bits", "GET", "a8", "0", "255"}, - output: []byte("-ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is.\r\n"), + input: []string{"bits", "GET", "a8", "0", "255"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is")}, }, "BITFIELD_RO unsupported commands": { - input: []string{"bits", "set", "u8", "0", "255"}, - output: []byte("-ERR BITFIELD_RO only supports the GET subcommand\r\n"), + input: []string{"bits", "set", "u8", "0", "255"}, + migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("BITFIELD_RO only supports the GET subcommand")}, }, } - runEvalTests(t, testCases, evalBITFIELDRO, store) + + runMigratedEvalTests(t, testCases, evalBITFIELDRO, store) } func testEvalGEOADD(t *testing.T, store *dstore.Store) { diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 79d63182a..440d974b2 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "math" + "math/bits" "sort" "strconv" "strings" @@ -3992,3 +3993,451 @@ func evalLINSERT(args []string, store *dstore.Store) *EvalResponse { } return makeEvalResult(res) } + +// SETBIT key offset value +func evalSETBIT(args []string, store *dstore.Store) *EvalResponse { + var err error + + if len(args) != 3 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("SETBIT"), + } + } + + key := args[0] + offset, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("bit offset is not an integer or out of range"), + } + } + + value, err := strconv.ParseBool(args[2]) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("bit is not an integer or out of range"), + } + } + + obj := store.Get(key) + requiredByteArraySize := offset>>3 + 1 + + if obj == nil { + obj = store.NewObj(NewByteArray(int(requiredByteArraySize)), -1, object.ObjTypeByteArray, object.ObjEncodingByteArray) + store.Put(args[0], obj) + } + + if object.AssertType(obj.TypeEncoding, object.ObjTypeByteArray) == nil || + object.AssertType(obj.TypeEncoding, object.ObjTypeString) == nil || + object.AssertType(obj.TypeEncoding, object.ObjTypeInt) == nil { + var byteArray *ByteArray + oType, oEnc := object.ExtractTypeEncoding(obj) + + switch oType { + case object.ObjTypeByteArray: + byteArray = obj.Value.(*ByteArray) + case object.ObjTypeString, object.ObjTypeInt: + byteArray, err = NewByteArrayFromObj(obj) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + default: + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + // Perform the resizing check + byteArrayLength := byteArray.Length + + // check whether resize required or not + if requiredByteArraySize > byteArrayLength { + // resize as per the offset + byteArray = byteArray.IncreaseSize(int(requiredByteArraySize)) + } + + resp := byteArray.GetBit(int(offset)) + byteArray.SetBit(int(offset), value) + + // We are returning newObject here so it is thread-safe + // Old will be removed by GC + newObj, err := ByteSliceToObj(store, obj, byteArray.data, oType, oEnc) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + exp, ok := dstore.GetExpiry(obj, store) + var exDurationMs int64 = -1 + if ok { + exDurationMs = int64(exp - uint64(utils.GetCurrentTime().UnixMilli())) + } + // newObj has bydefault expiry time -1 , we need to set it + if exDurationMs > 0 { + store.SetExpiry(newObj, exDurationMs) + } + + store.Put(key, newObj) + if resp { + return &EvalResponse{ + Result: clientio.IntegerOne, + Error: nil, + } + } + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + } + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } +} + +// GETBIT key offset +func evalGETBIT(args []string, store *dstore.Store) *EvalResponse { + var err error + + if len(args) != 2 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("GETBIT"), + } + } + + key := args[0] + offset, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + + obj := store.Get(key) + if obj == nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + requiredByteArraySize := offset>>3 + 1 + switch oType, _ := object.ExtractTypeEncoding(obj); oType { + case object.ObjTypeSet: + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + case object.ObjTypeByteArray: + byteArray := obj.Value.(*ByteArray) + byteArrayLength := byteArray.Length + + // check whether offset, length exists or not + if requiredByteArraySize > byteArrayLength { + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + } + value := byteArray.GetBit(int(offset)) + if value { + return &EvalResponse{ + Result: clientio.IntegerOne, + Error: nil, + } + } + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + + case object.ObjTypeString, object.ObjTypeInt: + byteArray, err := NewByteArrayFromObj(obj) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + if requiredByteArraySize > byteArray.Length { + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + } + value := byteArray.GetBit(int(offset)) + if value { + return &EvalResponse{ + Result: clientio.IntegerOne, + Error: nil, + } + } + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + + default: + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + } +} + +func evalBITCOUNT(args []string, store *dstore.Store) *EvalResponse { + var err error + + // if no key is provided, return error + if len(args) == 0 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("BITCOUNT"), + } + } + + // if more than 4 arguments are provided, return error + if len(args) > 4 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrSyntax, + } + } + + // fetching value of the key + key := args[0] + obj := store.Get(key) + if obj == nil { + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + } + + var value []byte + var valueLength int64 + + switch { + case object.AssertType(obj.TypeEncoding, object.ObjTypeByteArray) == nil: + byteArray := obj.Value.(*ByteArray) + value = byteArray.data + valueLength = byteArray.Length + case object.AssertType(obj.TypeEncoding, object.ObjTypeString) == nil: + value = []byte(obj.Value.(string)) + valueLength = int64(len(value)) + case object.AssertType(obj.TypeEncoding, object.ObjTypeInt) == nil: + value = []byte(strconv.FormatInt(obj.Value.(int64), 10)) + valueLength = int64(len(value)) + default: + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + // defining constants of the function + start, end := int64(0), valueLength-1 + unit := BYTE + + // checking which arguments are present and validating arguments + if len(args) > 1 { + start, err = strconv.ParseInt(args[1], 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + if len(args) <= 2 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrSyntax, + } + } + end, err = strconv.ParseInt(args[2], 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + } + if len(args) > 3 { + unit = strings.ToUpper(args[3]) + } + + switch unit { + case BYTE: + if start < 0 { + start += valueLength + } + if end < 0 { + end += valueLength + } + if start > end || start >= valueLength { + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + } + end = min(end, valueLength-1) + bitCount := 0 + for i := start; i <= end; i++ { + bitCount += bits.OnesCount8(value[i]) + } + return &EvalResponse{ + Result: bitCount, + Error: nil, + } + case BIT: + if start < 0 { + start += valueLength * 8 + } + if end < 0 { + end += valueLength * 8 + } + if start > end { + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + } + startByte, endByte := start/8, min(end/8, valueLength-1) + startBitOffset, endBitOffset := start%8, end%8 + + if endByte == valueLength-1 { + endBitOffset = 7 + } + + if startByte >= valueLength { + return &EvalResponse{ + Result: clientio.IntegerZero, + Error: nil, + } + } + + bitCount := 0 + + // Use bit masks to count the bits instead of a loop + if startByte == endByte { + mask := byte(0xFF >> startBitOffset) + mask &= byte(0xFF << (7 - endBitOffset)) + bitCount = bits.OnesCount8(value[startByte] & mask) + } else { + // Handle first byte + firstByteMask := byte(0xFF >> startBitOffset) + bitCount += bits.OnesCount8(value[startByte] & firstByteMask) + + // Handle all the middle ones + for i := startByte + 1; i < endByte; i++ { + bitCount += bits.OnesCount8(value[i]) + } + + // Handle last byte + lastByteMask := byte(0xFF << (7 - endBitOffset)) + bitCount += bits.OnesCount8(value[endByte] & lastByteMask) + } + return &EvalResponse{ + Result: bitCount, + Error: nil, + } + default: + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrSyntax, + } + } +} + +// Generic method for both BITFIELD and BITFIELD_RO. +// isReadOnly method is true for BITFIELD_RO command. +func bitfieldEvalGeneric(args []string, store *dstore.Store, isReadOnly bool) *EvalResponse { + var ops []utils.BitFieldOp + ops, err2 := utils.ParseBitfieldOps(args, isReadOnly) + + if err2 != nil { + return &EvalResponse{ + Result: nil, + Error: err2, + } + } + + key := args[0] + obj := store.Get(key) + if obj == nil { + obj = store.NewObj(NewByteArray(1), -1, object.ObjTypeByteArray, object.ObjEncodingByteArray) + store.Put(args[0], obj) + } + var value *ByteArray + var err error + + switch oType, _ := object.ExtractTypeEncoding(obj); oType { + case object.ObjTypeByteArray: + value = obj.Value.(*ByteArray) + case object.ObjTypeString, object.ObjTypeInt: + value, err = NewByteArrayFromObj(obj) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("value is not a valid byte array"), + } + } + default: + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + result := executeBitfieldOps(value, ops) + return &EvalResponse{ + Result: result, + Error: nil, + } +} + +// evalBITFIELD evaluates BITFIELD operations on a key store string, int or bytearray types +// it returns an array of results depending on the subcommands +// it allows mutation using SET and INCRBY commands +// returns arity error, offset type error, overflow type error, encoding type error, integer error, syntax error +// GET -- Returns the specified bit field. +// SET -- Set the specified bit field +// and returns its old value. +// INCRBY -- Increments or decrements +// (if a negative increment is given) the specified bit field and returns the new value. +// There is another subcommand that only changes the behavior of successive +// INCRBY and SET subcommands calls by setting the overflow behavior: +// OVERFLOW [WRAP|SAT|FAIL]` +func evalBITFIELD(args []string, store *dstore.Store) *EvalResponse { + if len(args) < 1 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("BITFIELD"), + } + } + + return bitfieldEvalGeneric(args, store, false) +} + +// Read-only variant of the BITFIELD command. It is like the original BITFIELD but only accepts GET subcommand and can safely be used in read-only replicas. +func evalBITFIELDRO(args []string, store *dstore.Store) *EvalResponse { + if len(args) < 1 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("BITFIELD_RO"), + } + } + + return bitfieldEvalGeneric(args, store, true) +} diff --git a/internal/server/cmd_meta.go b/internal/server/cmd_meta.go index c5f9bd4b7..41f5d7bae 100644 --- a/internal/server/cmd_meta.go +++ b/internal/server/cmd_meta.go @@ -171,6 +171,30 @@ var ( Cmd: "PTTL", CmdType: SingleShard, } + setbitCmdMeta = CmdsMeta{ + Cmd: "SETBIT", + CmdType: SingleShard, + } + getbitCmdMeta = CmdsMeta{ + Cmd: "GETBIT", + CmdType: SingleShard, + } + bitcountCmdMeta = CmdsMeta{ + Cmd: "BITCOUNT", + CmdType: SingleShard, + } + bitfieldCmdMeta = CmdsMeta{ + Cmd: "BITFIELD", + CmdType: SingleShard, + } + bitposCmdMeta = CmdsMeta{ + Cmd: "BITPOS", + CmdType: SingleShard, + } + bitfieldroCmdMeta = CmdsMeta{ + Cmd: "BITFIELD_RO", + CmdType: SingleShard, + } jsonclearCmdMeta = CmdsMeta{ Cmd: "JSON.CLEAR", @@ -425,5 +449,12 @@ func init() { WorkerCmdsMeta["HDEL"] = hdelCmdMeta WorkerCmdsMeta["HMSET"] = hmsetCmdMeta WorkerCmdsMeta["HMGET"] = hmgetCmdMeta + WorkerCmdsMeta["SETBIT"] = setbitCmdMeta + WorkerCmdsMeta["GETBIT"] = getbitCmdMeta + WorkerCmdsMeta["BITCOUNT"] = bitcountCmdMeta + WorkerCmdsMeta["BITFIELD"] = bitfieldCmdMeta + WorkerCmdsMeta["BITPOS"] = bitposCmdMeta + WorkerCmdsMeta["BITFIELD_RO"] = bitfieldroCmdMeta + // Additional commands (multishard, custom) can be added here as needed. } diff --git a/internal/server/utils/bitfield.go b/internal/server/utils/bitfield.go index 29544a4c6..bf5f389fc 100644 --- a/internal/server/utils/bitfield.go +++ b/internal/server/utils/bitfield.go @@ -25,26 +25,26 @@ func parseBitfieldEncodingAndOffset(args []string) (eType, eVal, offset interfac eType = SIGNED eVal, err = strconv.ParseInt(encodingRaw[1:], 10, 64) if err != nil { - err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is") return eType, eVal, offset, err } if eVal.(int64) <= 0 || eVal.(int64) > 64 { - err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is") return eType, eVal, offset, err } case 'u': eType = UNSIGNED eVal, err = strconv.ParseInt(encodingRaw[1:], 10, 64) if err != nil { - err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is") return eType, eVal, offset, err } if eVal.(int64) <= 0 || eVal.(int64) >= 64 { - err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is") return eType, eVal, offset, err } default: - err = diceerrors.NewErr(diceerrors.InvalidBitfieldType) + err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is") return eType, eVal, offset, err } @@ -52,21 +52,21 @@ func parseBitfieldEncodingAndOffset(args []string) (eType, eVal, offset interfac case '#': offset, err = strconv.ParseInt(offsetRaw[1:], 10, 64) if err != nil { - err = diceerrors.NewErr(diceerrors.BitfieldOffsetErr) + err = diceerrors.ErrGeneral("bit offset is not an integer or out of range") return eType, eVal, offset, err } offset = offset.(int64) * eVal.(int64) default: offset, err = strconv.ParseInt(offsetRaw, 10, 64) if err != nil { - err = diceerrors.NewErr(diceerrors.BitfieldOffsetErr) + err = diceerrors.ErrGeneral("bit offset is not an integer or out of range") return eType, eVal, offset, err } } return eType, eVal, offset, err } -func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byte) { +func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err error) { var overflowType string for i := 1; i < len(args); { @@ -74,11 +74,11 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt switch strings.ToUpper(args[i]) { case GET: if len(args) <= i+2 { - return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + return nil, diceerrors.ErrSyntax } eType, eVal, offset, err := parseBitfieldEncodingAndOffset(args[i+1 : i+3]) if err != nil { - return nil, diceerrors.NewErrWithFormattedMessage(err.Error()) + return nil, err } ops = append(ops, BitFieldOp{ Kind: GET, @@ -91,15 +91,15 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt isReadOnlyCommand = true case SET: if len(args) <= i+3 { - return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + return nil, diceerrors.ErrSyntax } eType, eVal, offset, err := parseBitfieldEncodingAndOffset(args[i+1 : i+3]) if err != nil { - return nil, diceerrors.NewErrWithFormattedMessage(err.Error()) + return nil, err } value, err1 := strconv.ParseInt(args[i+3], 10, 64) if err1 != nil { - return nil, diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr) + return nil, diceerrors.ErrIntegerOutOfRange } ops = append(ops, BitFieldOp{ Kind: SET, @@ -111,15 +111,15 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt i += 4 case INCRBY: if len(args) <= i+3 { - return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + return nil, diceerrors.ErrSyntax } eType, eVal, offset, err := parseBitfieldEncodingAndOffset(args[i+1 : i+3]) if err != nil { - return nil, diceerrors.NewErrWithFormattedMessage(err.Error()) + return nil, err } value, err1 := strconv.ParseInt(args[i+3], 10, 64) if err1 != nil { - return nil, diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr) + return nil, diceerrors.ErrIntegerOutOfRange } ops = append(ops, BitFieldOp{ Kind: INCRBY, @@ -131,13 +131,13 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt i += 4 case OVERFLOW: if len(args) <= i+1 { - return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + return nil, diceerrors.ErrSyntax } switch strings.ToUpper(args[i+1]) { case WRAP, FAIL, SAT: overflowType = strings.ToUpper(args[i+1]) default: - return nil, diceerrors.NewErrWithFormattedMessage(diceerrors.OverflowTypeErr) + return nil, diceerrors.ErrGeneral("Invalid OVERFLOW type specified") } ops = append(ops, BitFieldOp{ Kind: OVERFLOW, @@ -148,11 +148,11 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt }) i += 2 default: - return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr) + return nil, diceerrors.ErrSyntax } if readOnly && !isReadOnlyCommand { - return nil, diceerrors.NewErrWithMessage("BITFIELD_RO only supports the GET subcommand") + return nil, diceerrors.ErrGeneral("BITFIELD_RO only supports the GET subcommand") } } diff --git a/internal/worker/cmd_meta.go b/internal/worker/cmd_meta.go index ca9cc021d..9720c4d61 100644 --- a/internal/worker/cmd_meta.go +++ b/internal/worker/cmd_meta.go @@ -130,6 +130,12 @@ const ( CmdHDel = "HDEL" CmdHMSet = "HMSET" CmdHMGet = "HMGET" + CmdSetBit = "SETBIT" + CmdGetBit = "GETBIT" + CmdBitCount = "BITCOUNT" + CmdBitField = "BITFIELD" + CmdBitPos = "BITPOS" + CmdBitFieldRO = "BITFIELD_RO" ) type CmdMeta struct { @@ -267,6 +273,24 @@ var CommandsMeta = map[string]CmdMeta{ CmdHRandField: { CmdType: SingleShard, }, + CmdSetBit: { + CmdType: SingleShard, + }, + CmdGetBit: { + CmdType: SingleShard, + }, + CmdBitCount: { + CmdType: SingleShard, + }, + CmdBitField: { + CmdType: SingleShard, + }, + CmdBitPos: { + CmdType: SingleShard, + }, + CmdBitFieldRO: { + CmdType: SingleShard, + }, // Multi-shard commands. CmdRename: {