Files
RediStack/Tests/RediStackIntegrationTests/Commands/StringCommandsTests.swift
T
Nathan Harris 3c6713038d Refactor RedisCommand into a general use object
Motivation:

RediStack today represents a command as a temporary object for the purpose of writing to the channel.

While it is useful to have an object for that purpose, commands handled in this way require immediate execution
and aren't available for other purposes.

Commands can serve a better purpose as a lightweight object to support delayed execution,
so that pipeling as described in issue #63 could be possible.

Modifications:

- Add: `get` overloads for JSON codable interaction on `RedisClient`
- Add: New `RedisZRangeResultOption` type for better interactions with zrange operations that can optionally return scores
- Add: New `RedisHashFieldKey` for type-safety when working with Hash field keys much like `RedisKey`
- Change: A few API types from enums to structs for library evolution
- Change: `RedisCommandHandler` to operate on a tuple of `RESPValue, EventLoopPromise<RESPValue>` rather than `RedisCommand`
- Change: `RedisCommand` to be a generic struct with the keyword, arguments, and a transform closure to defer execution
- Change: Almost all `RedisClient` command extensions to be factory methods on `RedisCommand` instead
- Change: Many response types to be optional to avoid developers having to do `isNull` checks on their own
- Change: `RedisClient.send(command:arguments:)` to be generic with `send(_:)` as the signature
- Rename: RedisClient extensions for scan methods to be more discoverable and legible as `scanHashField`, etc.

Result:

It should be easier to support a clean pipelining API with deferred command execution,
with extensions being easier for 2nd party developers, and the maintenance overhead of all of the command extensions
should be a little lighter when it comes to changing HOW commands are sent such as adding a context parameter
2020-12-02 08:41:59 +00:00

233 lines
9.2 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the RediStack open source project
//
// Copyright (c) 2019-2020 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
//
//===----------------------------------------------------------------------===//
@testable import RediStack
import RediStackTestUtils
import XCTest
final class StringCommandsTests: RediStackIntegrationTestCase {
private static let testKey = "SortedSetCommandsTests"
func test_get() throws {
try connection.set(#function, to: "value").wait()
let r1: String? = try connection.get(#function, as: String.self).wait()
XCTAssertEqual(r1, "value")
try connection.set(#function, to: 30).wait()
let r2 = try connection.get(#function, as: Int.self).wait()
XCTAssertEqual(r2, 30)
_ = try connection.delete(#function).wait()
let r3: RESPValue? = try connection.get(#function).wait()
XCTAssertNil(r3)
}
func test_mget() throws {
let keys = ["one", "two"].map(RedisKey.init(_:))
try keys.forEach { _ = try connection.set($0, to: $0).wait() }
let values = try connection.send(.mget(keys + ["empty"])).wait()
XCTAssertEqual(values.count, 3)
XCTAssertEqual(values[0]?.string, "one")
XCTAssertEqual(values[1]?.string, "two")
XCTAssertNil(values[2])
XCTAssertEqual(try connection.send(.mget("empty", #function)).wait().count, 2)
}
func test_set() throws {
XCTAssertNoThrow(try connection.set(#function, to: "value").wait())
let val = try connection.get(#function, as: String.self).wait()
XCTAssertEqual(val, "value")
}
func test_set_condition() throws {
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .keyExists).wait(), .conditionNotMet)
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .keyDoesNotExist).wait(), .ok)
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .keyDoesNotExist).wait(), .conditionNotMet)
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .keyExists).wait(), .ok)
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .none).wait(), .ok)
}
func test_set_expiration() throws {
let expireInSecondsKey = RedisKey("\(#function)-seconds")
let expireInSecondsResult = connection.set(
expireInSecondsKey,
to: "value",
onCondition: .none,
expiration: .seconds(42)
)
XCTAssertEqual(try expireInSecondsResult.wait(), .ok)
let ttl = try connection.send(.ttl(expireInSecondsKey)).wait()
switch ttl {
case .keyDoesNotExist, .unlimited:
XCTFail("Unexpected TTL for key \(expireInSecondsKey)")
case .limited(let lifetime):
XCTAssertGreaterThan(lifetime.timeAmount, .nanoseconds(0))
XCTAssertLessThanOrEqual(lifetime.timeAmount, .seconds(42))
}
let expireInMillisecondsKey = RedisKey("\(#function)-milliseconds")
let expireInMillisecondsResult = connection.set(
expireInMillisecondsKey,
to: "value",
onCondition: .none,
expiration: .milliseconds(42_000)
)
XCTAssertEqual(try expireInMillisecondsResult.wait(), .ok)
let pttl = try connection.send(.ttl(expireInMillisecondsKey)).wait()
switch pttl {
case .keyDoesNotExist, .unlimited:
XCTFail("Unexpected TTL for key \(expireInMillisecondsKey)")
case .limited(let lifetime):
XCTAssertGreaterThan(lifetime.timeAmount, .nanoseconds(0))
XCTAssertLessThanOrEqual(lifetime.timeAmount, .milliseconds(42_000))
}
}
func test_set_condition_and_expiration() throws {
let setFailedResult = connection.set(#function, to: "value", onCondition: .keyExists, expiration: .seconds(42))
XCTAssertEqual(try setFailedResult.wait(), .conditionNotMet)
let setResult = connection.set(#function, to: "value", onCondition: .keyDoesNotExist, expiration: .seconds(42))
XCTAssertEqual(try setResult.wait(), .ok)
let ttl = try connection.send(.ttl(#function)).wait()
switch ttl {
case .keyDoesNotExist, .unlimited:
XCTFail("Unexpected TTL for key \(#function)")
case .limited(let lifetime):
XCTAssertGreaterThan(lifetime.timeAmount, .nanoseconds(0))
XCTAssertLessThanOrEqual(lifetime.timeAmount, .seconds(42))
}
}
func test_setnx() throws {
XCTAssertTrue(try connection.send(.setnx(#function, to: "value")).wait())
XCTAssertFalse(try connection.send(.setnx(#function, to: "value")).wait())
}
func test_setex() throws {
XCTAssertNoThrow(try connection.send(.setex(#function, to: "value", expirationInSeconds: 42)).wait())
let ttl = try connection.send(.ttl(#function)).wait()
switch ttl {
case .keyDoesNotExist, .unlimited:
XCTFail("Unexpected TTL for \(#function)")
case .limited(let lifetime):
XCTAssertGreaterThan(lifetime.timeAmount, .nanoseconds(0))
XCTAssertLessThanOrEqual(lifetime.timeAmount, .seconds(42))
}
}
func test_psetex() throws {
XCTAssertNoThrow(try connection.send(.psetex(#function, to: "value", expirationInMilliseconds: 42_000)).wait())
let ttl = try connection.send(.pttl(#function)).wait()
switch ttl {
case .keyDoesNotExist, .unlimited:
XCTFail("Unexpected TTL for \(#function)")
case .limited(let lifetime):
XCTAssertGreaterThan(lifetime.timeAmount, .nanoseconds(0))
XCTAssertLessThanOrEqual(lifetime.timeAmount, .milliseconds(42_000))
}
}
func test_append() throws {
let result = "value appended"
XCTAssertNoThrow(try connection.send(.append("value", to: #function)).wait())
let length = try connection.send(.append(" appended", to: #function)).wait()
XCTAssertEqual(length, result.count)
let val = try connection.get(#function, as: String.self).wait()
XCTAssertEqual(val, result)
}
func test_mset() throws {
let data: [RedisKey: Int] = [
"first": 1,
"second": 2
]
XCTAssertNoThrow(try connection.send(.mset(data)).wait())
let values = try connection.send(.mget(["first", "second"])).wait().compactMap { $0?.string }
XCTAssertEqual(values.count, 2)
XCTAssertEqual(values[0], "1")
XCTAssertEqual(values[1], "2")
XCTAssertNoThrow(try connection.send(.mset(["first": 10])).wait())
let val = try connection.get("first", as: String.self).wait()
XCTAssertEqual(val, "10")
}
func test_msetnx() throws {
let data: [RedisKey: Int] = [
"first": 1,
"second": 2
]
var success = try connection.send(.msetnx(data)).wait()
XCTAssertEqual(success, true)
success = try connection.send(.msetnx(["first": 10, "second": 20])).wait()
XCTAssertEqual(success, false)
let values = try connection.send(.mget(["first", "second"])).wait().compactMap { $0?.string }
XCTAssertEqual(values[0], "1")
XCTAssertEqual(values[1], "2")
}
func test_increment() throws {
var result = try connection.send(.incr(#function)).wait()
XCTAssertEqual(result, 1)
result = try connection.send(.incr(#function)).wait()
XCTAssertEqual(result, 2)
}
func test_incrementBy() throws {
var result = try connection.send(.incrby(#function, by: 10)).wait()
XCTAssertEqual(result, 10)
result = try connection.send(.incrby(#function, by: -3)).wait()
XCTAssertEqual(result, 7)
result = try connection.send(.incrby(#function, by: 0)).wait()
XCTAssertEqual(result, 7)
}
func test_incrementByFloat() throws {
var float = try connection.send(.incrbyfloat(#function, by: Float(3.0))).wait()
XCTAssertEqual(float, 3.0)
float = try connection.send(.incrbyfloat(#function, by: Float(-10.135901))).wait()
XCTAssertEqual(float, -7.135901)
var double = try connection.send(.incrbyfloat(#function, by: Double(10.2839))).wait()
XCTAssertEqual(double, 3.147999)
double = try connection.send(.incrbyfloat(#function, by: Double(15.2938))).wait()
XCTAssertEqual(double, 18.441799)
}
func test_decrement() throws {
var result = try connection.send(.decr(#function)).wait()
XCTAssertEqual(result, -1)
result = try connection.send(.decr(#function)).wait()
XCTAssertEqual(result, -2)
}
func test_decrementBy() throws {
var result = try connection.send(.decrby(#function, by: -10)).wait()
XCTAssertEqual(result, 10)
result = try connection.send(.decrby(#function, by: 3)).wait()
XCTAssertEqual(result, 7)
result = try connection.send(.decrby(#function, by: 0)).wait()
XCTAssertEqual(result, 7)
}
}