Files
RediStack/Tests/RediStackTests/RESPTranslatorTests.swift
Nathan Harris 479c024d4b Change RESPValue.init(bulk:) initializers to accept a wider range of values
Motivation:

While working to add more test coverage with `RESPTranslator`, it was made apparent that a `.bulkString(.none)` is impossible to create directly with the `RESPValue` initializers, even though it is a reasonable possibility.

Additionally, forcing all integer types to have to be stored in an `Int` is unnecessarily restrictive.

Modifications:

- Change `RESPValue.init(bulk:)` initializers to accept `Optional` instances
- Change `RESPValue.init(bulk:)` for `Int` initializer to be generic on `FixedWidthInteger`

Result:

Converting types to and from `RESPValue` should be more bi-directional and seamless.
2019-07-29 05:05:39 +00:00

517 lines
18 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the RediStack open source project
//
// Copyright (c) 2019 RediStack project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of RediStack project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import struct Foundation.Data
import NIO
@testable import RediStack
import XCTest
final class RESPTranslatorTests: XCTestCase {
private let allocator = ByteBufferAllocator()
private let parser = RESPTranslator()
}
// MARK: WRITING
extension RESPTranslatorTests {
func testWriting_simpleStrings() {
XCTAssertTrue(writingTestPass(input: .simpleString("Test1".byteBuffer), expected: "+Test1\r\n"))
XCTAssertTrue(writingTestPass(input: .simpleString("®in§³¾".byteBuffer), expected: "+®in§³¾\r\n"))
}
func testWriting_bulkStrings() {
let bytes: [UInt8] = [0x01, 0x02, 0x0a, 0x1b, 0xaa]
var buffer = allocator.buffer(capacity: 5)
buffer.writeBytes(bytes)
XCTAssertTrue(writingTestPass(input: .bulkString(buffer), expected: "$5\r\n".bytes + bytes + "\r\n".bytes))
XCTAssertTrue(writingTestPass(input: .init(bulk: "®in§³¾"), expected: "$10\r\n®in§³¾\r\n"))
XCTAssertTrue(writingTestPass(input: .init(bulk: ""), expected: "$0\r\n\r\n"))
XCTAssertTrue(writingTestPass(input: .init(bulk: Optional<Int>.none), expected: "$0\r\n\r\n"))
}
func testWriting_integers() {
XCTAssertTrue(writingTestPass(input: .integer(Int.min), expected: ":\(Int.min)\r\n"))
XCTAssertTrue(writingTestPass(input: .integer(0), expected: ":0\r\n"))
}
func testWriting_arrays() {
XCTAssertTrue(writingTestPass(input: .array([]), expected: "*0\r\n"))
XCTAssertTrue(writingTestPass(
input: .array([ .integer(3), .simpleString("foo".byteBuffer) ]),
expected: "*2\r\n:3\r\n+foo\r\n"
))
let bytes: [UInt8] = [ 0x0a, 0x1a, 0x1b, 0xff ]
var buffer = allocator.buffer(capacity: 4)
buffer.writeBytes(bytes)
XCTAssertTrue(writingTestPass(
input: .array([ .array([ .integer(10), .bulkString(buffer) ]) ]),
expected: "*1\r\n*2\r\n:10\r\n$4\r\n".bytes + bytes + "\r\n".bytes
))
}
func testWriting_errors() {
let error = RedisError(reason: "Manual error")
XCTAssertTrue(writingTestPass(input: .error(error), expected: "-\(error.message)\r\n"))
}
func testWriting_null() {
XCTAssertTrue(writingTestPass(input: .null, expected: "$-1\r\n"))
}
func testWriting_foundationData() {
let name = #function
let data = Data(name.bytes).convertedToRESPValue()
XCTAssertTrue(writingTestPass(input: data, expected: "$\(name.count)\r\n\(name)\r\n"))
}
private func writingTestPass(input: RESPValue, expected: [UInt8]) -> Bool {
var buffer = allocator.buffer(capacity: expected.count)
buffer.writeRESPValue(input)
let result = buffer.getBytes(at: 0, length: buffer.readableBytes)
return result == expected
}
private func writingTestPass(input: RESPValue, expected: String) -> Bool {
var buffer = allocator.buffer(capacity: expected.count)
buffer.writeRESPValue(input)
let result = buffer.getString(at: 0, length: buffer.readableBytes)
return result == expected
}
}
// MARK: READING
fileprivate extension ByteBuffer {
mutating func mimicTokenParse() {
self.moveReaderIndex(forwardBy: 1)
}
}
// MARK: Parse
extension RESPTranslatorTests {
func testParsing_invalidToken() {
var buffer = self.allocator.buffer(capacity: 128)
buffer.writeString("!!!!")
XCTAssertThrowsError(try self.parser.parseBytes(from: &buffer)) { error in
XCTAssertEqual(error as? RESPTranslator.ParsingError, .invalidToken)
}
}
func testParsing_invalidSymbols() {
let testRESP = "&3\r\n"
var buffer = allocator.buffer(capacity: testRESP.count)
buffer.writeString(testRESP)
XCTAssertThrowsError(try parser.parseBytes(from: &buffer))
XCTAssertEqual(buffer.readerIndex, 0)
}
func testParsing_simpleString() throws {
let value = try parseTest(inputRESP: "+OK\r\n")
XCTAssertEqual(value?.string, "OK")
}
func testParsing_simpleString_chunked() throws {
let values = try parseTest(withChunks: ["+OK\r", "\n+&t®in§³¾\r", "\n"])
XCTAssertEqual(values.count, 2)
XCTAssertEqual(values[0].string, "OK")
XCTAssertEqual(values[1].string, "&t®in§³¾")
}
func testParsing_integer() throws {
let value = try parseTest(inputRESP: ":300\r\n")
XCTAssertEqual(value?.int, 300)
}
func testParsing_integer_chunked() throws {
let values = try parseTest(withChunks: [":300\r", "\n:\(Int.min)\r", "\n:", "\(Int.max)", "\r", "\n"])
XCTAssertEqual(values.count, 3)
XCTAssertEqual(values[0].int, 300)
XCTAssertEqual(values[1].int, Int.min)
XCTAssertEqual(values[2].int, Int.max)
}
func testParsing_bulkStrings() throws {
XCTAssertEqual(try parseTest(inputRESP: "$-1\r\n")?.isNull, true)
XCTAssertEqual(try parseTest(inputRESP: "$0\r\n\r\n")?.string, "")
XCTAssertEqual(try parseTest(inputRESP: "$1\r\n!\r\n")?.string, "!")
XCTAssertEqual(
try parseTest(input: "$1\r\n".bytes + [0xa3] + "\r\n".bytes)?.bytes,
[0xa3]
)
XCTAssertEqual(
try parseTest(input: "$1\r\n".bytes + [0xba] + "\r\n".bytes)?.bytes,
[0xba]
)
}
func testParsing_bulkStrings_chunked() throws {
let t1 = try parseTest(withChunks: ["$3\r", "\naaa\r\n$", "4\r\nnio!\r\n"])
XCTAssertEqual(t1.count, 2)
XCTAssertTrue(t1[0].bytes?.count == 3)
XCTAssertTrue(t1[1].bytes?.count == 4)
let chunks: [[UInt8]] = [
"$3\r".bytes,
"\n".bytes + [0xAA, 0xA3, 0xFF] + "\r\n$".bytes,
"4\r\n".bytes + [0xbb, 0x3a, 0xba, 0xFF] + "\r\n".bytes
]
let t2 = try parseTest(withChunks: chunks)
XCTAssertTrue(t2[0].bytes?.count == 3)
XCTAssertTrue(t2[1].bytes?.count == 4)
}
func testParsing_arrays() throws {
XCTAssertEqual(try parseTest(inputRESP: "*1\r\n+!\r\n")?.array?.count, 1)
XCTAssertEqual(try parseTest(inputRESP: "*2\r\n*1\r\n:1\r\n:3\r\n")?.array?.count, 2)
XCTAssertEqual(try parseTest(input: "*0\r\n".bytes)?.array?.count, 0)
XCTAssertNil(try parseTest(input: "*-1\r\n".bytes)?.array)
}
func testParsing_arrays_chunked() throws {
let t1 = try parseTest(withChunks: ["*2\r", "\n+a\r\n+c\r\n*", "0\r\n"])
XCTAssertEqual(t1.count, 2)
XCTAssertTrue(t1[0].array?.count == 2)
XCTAssertEqual(t1[0].array?[0].string, "a")
XCTAssertEqual(t1[0].array?[1].string, "c")
XCTAssertTrue(t1[1].array?.count == 0)
let t2 = try parseTest(withChunks: [
"*-1\r".bytes,
"\n".bytes,
"*1\r".bytes,
"\n\r\n".bytes
])
XCTAssertEqual(t2.count, 2)
XCTAssertTrue(t2[0].isNull)
XCTAssertTrue(t2[1].array?.count == 1)
XCTAssertEqual(t2[1].array?[0].string, "£")
}
func testParsing_error() throws {
let expectedContent = "ERR unknown command 'foobar'"
let testString = "-\(expectedContent)\r\n"
var buffer = allocator.buffer(capacity: expectedContent.count)
buffer.writeString(testString)
guard let value = try parser.parseBytes(from: &buffer) else { return XCTFail("Failed to parse error") }
XCTAssertEqual(value.error?.message.contains(expectedContent), true)
}
private func parseTest(inputRESP: String) throws -> RESPValue? {
return try parseTest(input: inputRESP.bytes)
}
private func parseTest(input: [UInt8]) throws -> RESPValue? {
var buffer = allocator.buffer(capacity: input.count)
buffer.writeBytes(input)
let result = try parser.parseBytes(from: &buffer)
assert(buffer.readerIndex == buffer.writerIndex)
return result
}
private func parseTest(withChunks messageChunks: [String]) throws -> [RESPValue] {
return try parseTest(withChunks: messageChunks.map({ $0.bytes }))
}
private func parseTest(withChunks messageChunks: [[UInt8]]) throws -> [RESPValue] {
var buffer = allocator.buffer(capacity: messageChunks.joined().count)
var results = [RESPValue]()
for chunk in messageChunks {
buffer.writeBytes(chunk)
guard let result = try parser.parseBytes(from: &buffer) else { continue }
results.append(result)
}
assert(buffer.readerIndex == buffer.writerIndex)
return results
}
}
// MARK: Simple Strings
extension RESPTranslatorTests {
func testParsing_simpleString_missingNewline() {
XCTAssertNil(simpleStringParseTest("+OK"))
XCTAssertNil(simpleStringParseTest("+OK\r"))
}
func testParsing_simpleString_withNoContent() {
XCTAssertEqual(simpleStringParseTest("+\r\n"), "")
}
func testParsing_simpleString_withContent() {
XCTAssertEqual(simpleStringParseTest("+ \r\n"), " ")
XCTAssertEqual(simpleStringParseTest("+OK\r\n"), "OK")
XCTAssertEqual(simpleStringParseTest("+OK\r\n+OTHER STRING\r\n"), "OK")
XCTAssertEqual(simpleStringParseTest("+&t®in§³¾\r\n"), "&t®in§³¾")
}
func testParsing_simpleString_handlesRecursion() {
let testString = "+OK\r\n+OTHER STRING\r\n"
var buffer = allocator.buffer(capacity: testString.count)
buffer.writeString(testString)
buffer.mimicTokenParse()
var first = parser.parseSimpleString(from: &buffer)!
XCTAssertEqual(buffer.readerIndex, 5) // position of the 2nd '+'
XCTAssertEqual(first.readBytes(length: first.readableBytes), "OK".bytes)
buffer.mimicTokenParse()
var second = parser.parseSimpleString(from: &buffer)!
XCTAssertEqual(buffer.readerIndex, 20)
XCTAssertEqual(second.readBytes(length: second.readableBytes), "OTHER STRING".bytes)
}
private func simpleStringParseTest(_ inputRESP: String) -> String? {
var buffer = allocator.buffer(capacity: inputRESP.count)
buffer.writeString(inputRESP)
buffer.mimicTokenParse()
guard let stringBuffer = parser.parseSimpleString(from: &buffer) else { return nil }
return RESPValue.simpleString(stringBuffer).string
}
}
// MARK: Integers
extension RESPTranslatorTests {
func testParsing_integer_withAllBytes() {
XCTAssertEqual(integerParseTest(":100\r\n"), 100)
XCTAssertEqual(integerParseTest(":-100\r\n"), -100)
XCTAssertEqual(integerParseTest(":\(Int.min)\r\n"), Int.min)
XCTAssertEqual(integerParseTest(":\(Int.max)\r\n"), Int.max)
}
func testParsing_integer_missingBytes() {
XCTAssertNil(integerParseTest(":\r"))
XCTAssertNil(integerParseTest(":"))
XCTAssertNil(integerParseTest(":\n"))
XCTAssertNil(integerParseTest(":\r\n"))
}
func testParsing_integer_recursively() {
let testString = ":1\r\n:300\r\n"
var buffer = allocator.buffer(capacity: testString.count)
buffer.writeString(testString)
buffer.mimicTokenParse()
let first = parser.parseInteger(from: &buffer)
XCTAssertEqual(buffer.readerIndex, 4) // position of the 2nd ':'
XCTAssertEqual(first, 1)
buffer.mimicTokenParse()
let second = parser.parseInteger(from: &buffer)
XCTAssertEqual(buffer.readerIndex, 10)
XCTAssertEqual(second, 300)
}
private func integerParseTest(_ inputRESP: String) -> Int? {
var buffer = allocator.buffer(capacity: inputRESP.count)
buffer.writeString(inputRESP)
buffer.mimicTokenParse()
return parser.parseInteger(from: &buffer)
}
}
// MARK: Bulk Strings
extension RESPTranslatorTests {
func testParsing_bulkString_sizeMismatch() {
var buffer = self.allocator.buffer(capacity: 128)
buffer.writeString("$2\r\ntoo long\r\n")
buffer.mimicTokenParse()
XCTAssertThrowsError(try self.parser.parseBulkString(from: &buffer)) { error in
XCTAssertEqual(error as? RESPTranslator.ParsingError, .bulkStringSizeMismatch)
}
}
func testParsing_bulkString_invalidNegativeSize() {
var buffer = self.allocator.buffer(capacity: 128)
buffer.writeString("$-4\r\nwhat\r\n")
buffer.mimicTokenParse()
XCTAssertThrowsError(try self.parser.parseBulkString(from: &buffer)) { error in
XCTAssertEqual(error as? RESPTranslator.ParsingError, .invalidBulkStringSize)
}
}
func testParsing_bulkString_SizeIsNaN() {
var buffer = self.allocator.buffer(capacity: 128)
buffer.writeString("$FOO\r\nwhat\r\n")
buffer.mimicTokenParse()
XCTAssertThrowsError(try self.parser.parseBulkString(from: &buffer)) { error in
XCTAssertEqual(error as? RESPTranslator.ParsingError, .invalidBulkStringSize)
}
}
func testParsing_bulkString_missingEndings() {
for message in ["$6", "$6\r\n", "$6\r\nabcdef", "$0\r\n"] {
XCTAssertNil(bulkStringParseTest(inputRESP: message))
}
}
func testParsing_bulkString_withNoSize() {
let result = bulkStringParseTest(inputRESP: "$0\r\n\r\n")
XCTAssertEqual(result?.bytes?.count, 0)
}
func testParsing_bulkString_withContent() {
let result = bulkStringParseTest(inputRESP: "$1\r\n:\r\n")?.bytes
XCTAssertEqual(result?.count, 1)
XCTAssertEqual(result?[0], .colon)
}
func testParsing_bulkString_null() {
let result = bulkStringParseTest(inputRESP: "$-1\r\n")
XCTAssertEqual(result?.isNull, true)
}
func testParsing_bulkString_rawBytes() {
let bytes: [UInt8] = [0x00, 0x01, 0x02, 0x03, 0x0A, 0xFF]
let allBytes = "$\(bytes.count)\r\n".bytes + bytes + "\r\n".bytes
let result = bulkStringParseTest(input: allBytes)
XCTAssertEqual(result?.bytes?.count, bytes.count)
}
func testParsing_bulkString_recursively() {
let testString = "$0\r\n\r\n$5\r\nredis\r\n"
var buffer = allocator.buffer(capacity: testString.count)
buffer.writeString(testString)
buffer.mimicTokenParse()
let first = try? parser.parseBulkString(from: &buffer)
XCTAssertEqual(buffer.readerIndex, 6) // position of the 2nd '$'
buffer.mimicTokenParse()
let second = try? parser.parseBulkString(from: &buffer)
XCTAssertEqual(buffer.readerIndex, 17)
XCTAssertEqual(first?.string, "")
XCTAssertEqual(second?.string, "redis")
}
private func bulkStringParseTest(inputRESP: String) -> RESPValue? {
return bulkStringParseTest(input: inputRESP.bytes)
}
private func bulkStringParseTest(input: [UInt8]) -> RESPValue? {
var buffer = allocator.buffer(capacity: input.count)
buffer.writeBytes(input)
buffer.mimicTokenParse()
guard let result = try? parser.parseBulkString(from: &buffer) else { return nil }
return result
}
}
// MARK: Arrays
extension RESPTranslatorTests {
func testParsing_array_whenNull() throws {
let result = try arrayParseTest(inputRESP: "*-1\r\n")
XCTAssertEqual(result?.isNull, true)
}
func testParsing_array_whenEmpty() throws {
let result = try arrayParseTest(inputRESP: "*0\r\n")
XCTAssertEqual(result?.array?.count, 0)
}
func testParsing_array_withMixedTypes() throws {
let result = try arrayParseTest(inputRESP: "*3\r\n:3\r\n+OK\r\n$1\r\na\r\n")?.array
XCTAssertEqual(result?.count, 3)
XCTAssertEqual(result?[0].int, 3)
XCTAssertEqual(result?[1].string, "OK")
XCTAssertEqual(result?[2].bytes?.count, 1)
}
func testParsing_array_withNullElements() throws {
let result = try arrayParseTest(inputRESP: "*3\r\n:3\r\n$-1\r\n:30\r\n")?.array
XCTAssertEqual(result?.count, 3)
XCTAssertEqual(result?[0].int, 3)
XCTAssertEqual(result?[1].isNull, true)
XCTAssertEqual(result?[2].int, 30)
}
func testParsing_array_nested() throws {
let result = try arrayParseTest(inputRESP: "*2\r\n:3\r\n*2\r\n:30\r\n:15\r\n")?.array
XCTAssertEqual(result?.count, 2)
XCTAssertEqual(result?[0].int, 3)
let nested = result?[1].array
XCTAssertEqual(nested?.count, 2)
XCTAssertEqual(nested?[0].int, 30)
XCTAssertEqual(nested?[1].int, 15)
}
func testParsing_array_recursively() throws {
let testString = "*1\r\n:3\r\n*2\r\n+OK\r\n$3\r\nabc\r\n"
var buffer = allocator.buffer(capacity: testString.count)
buffer.writeString(testString)
buffer.mimicTokenParse()
let first = try parser.parseArray(from: &buffer)
XCTAssertEqual(buffer.readerIndex, 8) // position of the 2nd '$'
buffer.mimicTokenParse()
let second = try parser.parseArray(from: &buffer)
XCTAssertEqual(buffer.readerIndex, 26)
guard
let array1 = first?.array,
let array2 = second?.array
else { return XCTFail("Failed to parse both values") }
XCTAssertEqual(array1.count, 1)
XCTAssertEqual(array1[0].int, 3)
XCTAssertEqual(array2.count, 2)
XCTAssertEqual(array2[0].string, "OK")
XCTAssertEqual(array2[1].string, "abc")
}
private func arrayParseTest(inputRESP: String) throws -> RESPValue? {
return try arrayParseTest(input: inputRESP.bytes)
}
private func arrayParseTest(input: [UInt8]) throws -> RESPValue? {
var buffer = allocator.buffer(capacity: input.count)
buffer.writeBytes(input)
buffer.mimicTokenParse()
guard let result = try parser.parseArray(from: &buffer) else { return nil }
return result
}
}