Implement basic types for CRDT data structure (#10)

This commit implements primitives for creating CRDT datatypes such as
TimeTicket, ActorID, CRDTElement, and so on.

We found a problem with the JS SDK using lamport as Uint64 and this
commit fixed it with Int64.
This commit is contained in:
원형식 2022-09-27 18:08:33 +09:00 committed by GitHub
parent f51c46f2f3
commit 86446ee7ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 936 additions and 32 deletions

View File

@ -0,0 +1,51 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
enum Converter {
/**
* parses the given bytes into value.
*/
static func valueFrom(valueType: ValueType, data: Data) throws -> PrimitiveValue {
switch valueType {
case .null:
return .null
case .boolean:
return .boolean(data[0] == 1)
case .integer:
let result = data.withUnsafeBytes { $0.load(as: Int32.self) }
return .integer(result)
case .double:
let result = data.withUnsafeBytes { $0.load(as: Double.self) }
return .double(result)
case .string:
return .string(String(decoding: data, as: UTF8.self))
case .long:
let result = data.withUnsafeBytes { $0.load(as: Int64.self) }
return .long(result)
case .bytes:
return .bytes(data)
case .date:
let milliseconds = data.withUnsafeBytes { $0.load(as: Double.self) }
return .date(Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000))
default:
throw YorkieError.unimplemented(message: String(describing: valueType))
}
}
}

View File

@ -1,7 +1,7 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
@ -14,4 +14,13 @@
* limitations under the License.
*/
// Use a Swift typealias to remap the type names of Protobuf locally
// Swift Protobuf Guide: https://github.com/apple/swift-protobuf/blob/main/Documentation/API.md#generated-struct-name
import Foundation
typealias ValueType = Yorkie_V1_ValueType
typealias YorkieServiceNIOClient = Yorkie_V1_YorkieServiceNIOClient
typealias ActivateClientRequest = Yorkie_V1_ActivateClientRequest
typealias DeactivateClientRequest = Yorkie_V1_DeactivateClientRequest
typealias YorkieServiceAsyncClient = Yorkie_V1_YorkieServiceAsyncClient

View File

@ -21,6 +21,7 @@ enum LogLevel: String {
case info = "INFO"
case warn = "WARN"
case error = "ERROR"
case fatal = "FATAL"
}
enum Logger {
@ -40,6 +41,10 @@ enum Logger {
self.log(level: .error, message, error: error, filename: filename, function: function, line: line)
}
static func fatal(_ message: String, error: Error? = nil, filename: String = #file, function: String = #function, line: Int = #line) {
self.log(level: .fatal, message, error: error, filename: filename, function: function, line: line)
}
static func log(level: LogLevel, _ message: String, error: Error? = nil, filename: String = #file, function: String = #function, line: Int = #line) {
let file = URL(fileURLWithPath: filename).lastPathComponent
let log = message + (error?.localizedDescription ?? "")

View File

@ -0,0 +1,161 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/**
* `CRDTElement` represents element type containing logical clock.
*
* @internal
*/
class CRDTElement {
private var createdAt: TimeTicket
private var movedAt: TimeTicket?
private var removedAt: TimeTicket?
init(createdAt: TimeTicket) {
self.createdAt = createdAt
}
/**
* `getCreatedAt` returns the creation time of this element.
*/
func getCreatedAt() -> TimeTicket {
return self.createdAt
}
/**
* `getID` returns the creation time of this element.
*/
func getID() -> TimeTicket {
return self.createdAt
}
/**
* `getMovedAt` returns the move time of this element.
*/
func getMovedAt() -> TimeTicket? {
return self.movedAt
}
/**
* `getRemovedAt` returns the removal time of this element.
*/
func getRemovedAt() -> TimeTicket? {
return self.removedAt
}
/**
* `setMovedAt` sets the move time of this element.
*/
@discardableResult
func setMovedAt(_ movedAt: TimeTicket?) -> Bool {
guard let currentMoveAt = self.movedAt else {
self.movedAt = movedAt
return true
}
if let movedAt = movedAt, movedAt.after(currentMoveAt) {
self.movedAt = movedAt
return true
}
return false
}
/**
* `setRemovedAt` sets the remove time of this element.
*/
func setRemovedAt(_ removedAt: TimeTicket?) {
self.removedAt = removedAt
}
/**
* `remove` removes this element.
*/
func remove(_ removedAt: TimeTicket?) -> Bool {
guard let removedAt = removedAt, removedAt.after(self.createdAt) else {
return false
}
if self.removedAt == nil {
self.removedAt = removedAt
return true
}
if let currentRemovedAt = self.removedAt, removedAt.after(currentRemovedAt) {
self.removedAt = removedAt
return true
}
return false
}
/**
* `isRemoved` check if this element was removed.
*/
func isRemoved() -> Bool {
return self.removedAt != nil
}
func toJSON() -> String {
fatalError("Must be implemented.")
}
func toSortedJSON() -> String {
fatalError("Must be implemented.")
}
func deepcopy() -> CRDTElement {
fatalError("Must be implemented.")
}
}
/**
*
* `CRDTContainer` represents CRDTArray or CRDtObject.
* @internal
*/
class CRDTContainer: CRDTElement {
func keyOf(createdAt: TimeTicket) -> String? {
fatalError("Must be implemented.")
}
func purge(element: CRDTElement) {
fatalError("Must be implemented.")
}
func delete(createdAt: TimeTicket, executedAt: TimeTicket) -> CRDTElement {
fatalError("Must be implemented.")
}
func getDescendants(callback: (_ elem: CRDTElement, _ parent: CRDTContainer) -> Bool) {
fatalError("Must be implemented.")
}
}
/**
* `CRDTTextElement` represents CRDTText or CRDTRichText.
*/
class CRDTTextElement: CRDTElement {
func getRemovedNodesLen() -> Int {
fatalError("Must be implemented.")
}
func purgeTextNodesWithGarbage(ticket: TimeTicket) -> Int {
fatalError("Must be implemented.")
}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
enum PrimitiveValue {
case null
case boolean(Bool)
case integer(Int32)
case long(Int64)
case double(Double)
case string(String)
case bytes(Data)
case date(Date)
}
/**
* `Primitive` represents primitive data type including logical clock.
* It has a type and a value.
*/
class Primitive: CRDTElement {
let value: PrimitiveValue
init(value: PrimitiveValue, createdAt: TimeTicket) {
self.value = value
super.init(createdAt: createdAt)
}
/**
* `toJSON` returns the JSON encoding of the value.
*
* TODOs: We need to consider the case where the value is
* a byte array and a date.
*/
var toJSON: String {
switch self.value {
case .null:
return "null"
case .boolean(let value):
return "\(value)"
case .integer(let value):
return "\(value)"
case .double(let value):
return "\(value)"
case .string(let value):
return "\(value.escaped())"
case .long(let value):
return "\(value)"
case .bytes(let value):
return "\(value)"
case .date(let value):
return "\(value.timeIntervalSince1970 * 1000)"
}
}
/**
* `toSortedJSON` returns the sorted JSON encoding of the value.
*/
var toSortedJSON: String {
return self.toJSON
}
/**
* `deepcopy` copies itself deeply.
*/
var deepcopy: Primitive {
let primitive = Primitive(value: self.value, createdAt: self.getCreatedAt())
primitive.setMovedAt(self.getMovedAt())
return primitive
}
/**
* `getPrimitiveType` returns the primitive type of the value.
*/
static func type(of value: Any?) -> PrimitiveValue? {
guard let value = value else {
return .null
}
switch value {
case let casted as Bool:
return .boolean(casted)
case let casted as Int32:
return .integer(casted)
case let casted as Int64:
return .long(casted)
case let casted as Double:
return .double(casted)
case let casted as String:
return .string(casted)
case let casted as Data:
return .bytes(casted)
case let casted as Date:
return .date(casted)
default:
return nil
}
}
/**
* `isSupport` check if the given value is supported type.
*/
static func isSupport(value: Any) -> Bool {
return Primitive.type(of: value) != nil
}
/**
* `isNumericType` checks numeric type by JSONPrimitive
*/
var isNumericType: Bool {
switch self.value {
case .integer, .long, .double:
return true
default:
return false
}
}
/**
* `toBytes` creates an array representing the value.
*/
func toBytes() throws -> Data {
switch self.value {
case .null:
return Data()
case .boolean(let value):
var valueInInt = Int(exactly: NSNumber(value: value))
return Data(bytes: &valueInInt, count: MemoryLayout.size(ofValue: valueInInt))
case .integer(let value):
return withUnsafeBytes(of: value) { Data($0) }
case .double(let value):
return withUnsafeBytes(of: value) { Data($0) }
case .string(let value):
return value.data(using: .utf8) ?? Data()
case .long(let value):
return withUnsafeBytes(of: value) { Data($0) }
case .bytes(let value):
return value
case .date(let value):
let milliseconds = value.timeIntervalSince1970 * 1000
return withUnsafeBytes(of: milliseconds) { Data($0) }
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/**
* `EscapeString` escapes string.
*/
extension String {
static let escapeSequences = [
(original: "\\", escaped: "\\\\"),
(original: "\"", escaped: "\\\""),
(original: "'", escaped: "\\'"),
(original: "\n", escaped: "\\n"),
(original: "\r", escaped: "\\r"),
(original: "\t", escaped: "\\t"),
(original: "\u{2028}", escaped: "\\u{2028}"),
(original: "\u{2029}", escaped: "\\u{2029}")
]
func escaped() -> String {
return String.escapeSequences.reduce(self) { string, seq in
string.replacingOccurrences(of: seq.original, with: seq.escaped)
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2020 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/**
* `ActorID` is used to identify who is making changes to the document.
* It is hexadecimal string and should be generated by unique value.
*
*/
typealias ActorID = String
enum ActorIds {
/**
* `InitialActorID` is the initial value of ActorID.
*/
static let initialActorID = "000000000000000000000000"
/**
* `MaxActorID` is the maximum value of ActorID.
*/
static let maxActorID = "FFFFFFFFFFFFFFFFFFFFFFFF"
}

View File

@ -0,0 +1,141 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/**
* `TimeTicket` is a timestamp of the logical clock. Ticket is immutable.
* It is created by `ChangeID`.
*/
class TimeTicket {
private enum InitialValue {
static let initialDelimiter: UInt32 = 0
static let maxDelemiter: UInt32 = .max
static let maxLamport: Int64 = .max
}
static let initialTimeTicket = TimeTicket(lamport: 0, delimiter: InitialValue.initialDelimiter, actorID: ActorIds.initialActorID)
static let maxTimeTicket = TimeTicket(lamport: InitialValue.maxLamport, delimiter: InitialValue.maxDelemiter, actorID: ActorIds.maxActorID)
private var lamport: Int64
private var delimiter: UInt32
private var actorID: ActorID?
/** @hideconstructor */
init(lamport: Int64, delimiter: UInt32, actorID: ActorID?) {
self.lamport = lamport
self.delimiter = delimiter
self.actorID = actorID
}
/**
* `of` creates an instance of Ticket.
*/
public static func of(lamport: Int64, delimiter: UInt32, actorID: ActorID?) -> TimeTicket {
return TimeTicket(lamport: lamport, delimiter: delimiter, actorID: actorID)
}
/**
* `toIDString` returns the lamport string for this Ticket.
*/
func toIDString() -> String {
guard let actorID = self.actorID else {
return "\(self.lamport):nil:\(self.delimiter)"
}
return "\(self.lamport):\(actorID):\(self.delimiter)"
}
/**
* `getStructureAsString` returns a string containing the meta data of the ticket
* for debugging purpose.
*/
func getStructureAsString() -> String {
guard let actorID = self.actorID else {
return "\(self.lamport):nil:\(self.delimiter)"
}
return "\(self.lamport):\(actorID):\(self.delimiter)"
}
/**
* `setActor` creates a new instance of Ticket with the given actorID.
*/
func setActor(actorID: ActorID) -> TimeTicket {
return TimeTicket(lamport: self.lamport, delimiter: self.delimiter, actorID: actorID)
}
/**
* `getLamportAsString` returns the lamport string.
*/
func getLamportAsString() -> String {
return "\(self.lamport)"
}
/**
* `getDelimiter` returns delimiter.
*/
func getDelimiter() -> UInt32 {
return self.delimiter
}
/**
* `getActorID` returns actorID.
*/
func getActorID() -> String? {
return self.actorID
}
/**
* `after` returns whether the given ticket was created later.
*/
func after(_ other: TimeTicket) -> Bool {
return self.compare(other) == .orderedDescending
}
/**
* `equals` returns whether the given ticket was created.
*/
func equals(other: TimeTicket) -> Bool {
return self.compare(other) == .orderedSame
}
/**
* `compare` returns an integer comparing two Ticket.
* The result will be 0 if id==other, -1 if `id < other`, and +1 if `id > other`.
* If the receiver or argument is nil, it would panic at runtime.
*/
func compare(_ other: TimeTicket) -> ComparisonResult {
if self.lamport > other.lamport {
return .orderedDescending
} else if self.lamport < other.lamport {
return .orderedAscending
}
if let actorID = actorID, let otherActorID = other.actorID {
let compare = actorID.localizedCompare(otherActorID)
if compare != .orderedSame {
return compare
}
}
if self.delimiter > other.delimiter {
return .orderedDescending
} else if other.delimiter > self.delimiter {
return .orderedAscending
}
return .orderedSame
}
}

View File

@ -1,7 +1,7 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
@ -15,3 +15,8 @@
*/
import Foundation
enum YorkieError: Error {
case unexpected(message: String)
case unimplemented(message: String)
}

View File

@ -33,8 +33,7 @@ class SplayNode<V> {
}
func getLength() -> Int {
assertionFailure("Must be implemented.")
return 0
fatalError("Must be implemented.")
}
/**
@ -248,6 +247,7 @@ class SplayTree<V> {
/**
* `insertAfter` inserts the node after the given previous node.
*/
@discardableResult
func insertAfter(_ target: SplayNode<V>?, newNode: SplayNode<V>) -> SplayNode<V> {
guard let target = target else {
self.root = newNode
@ -404,10 +404,10 @@ class SplayTree<V> {
}
/**
* `getAnnotatedString` returns a string containing the meta data of the Node
* `getStructureAsString` returns a string containing the meta data of the Node
* for debugging purpose.
*/
func getAnnotatedString() -> String {
func getStructureAsString() -> String {
var metaString: [SplayNode<V>] = []
self.traverseInorder(self.root!, stack: &metaString)
return metaString

View File

@ -35,8 +35,8 @@ class GRPCTests: XCTestCase {
return
}
let client = Yorkie_V1_YorkieServiceNIOClient(channel: channel)
var activateRequest = Yorkie_V1_ActivateClientRequest()
let client = YorkieServiceNIOClient(channel: channel)
var activateRequest = ActivateClientRequest()
activateRequest.clientKey = testClientKey
let activateResponse = try? client.activateClient(activateRequest, callOptions: nil).response.wait()
guard let activateResponse = activateResponse else {
@ -46,7 +46,7 @@ class GRPCTests: XCTestCase {
XCTAssertEqual(activateResponse.clientKey, testClientKey)
var deactivateRequest = Yorkie_V1_DeactivateClientRequest()
var deactivateRequest = DeactivateClientRequest()
deactivateRequest.clientID = activateResponse.clientID
guard let deactivatedResponse = try? client.deactivateClient(deactivateRequest).response.wait() else {
XCTFail("The response of deactivate is nil.")
@ -71,8 +71,8 @@ class GRPCTests: XCTestCase {
return
}
let client = Yorkie_V1_YorkieServiceAsyncClient(channel: channel)
var activateRequest = Yorkie_V1_ActivateClientRequest()
let client = YorkieServiceAsyncClient(channel: channel)
var activateRequest = ActivateClientRequest()
activateRequest.clientKey = testClientKey
let activateResponse = try? await client.activateClient(activateRequest, callOptions: nil)
guard let activateResponse = activateResponse else {
@ -82,7 +82,7 @@ class GRPCTests: XCTestCase {
XCTAssertEqual(activateResponse.clientKey, testClientKey)
var deactivateRequest = Yorkie_V1_DeactivateClientRequest()
var deactivateRequest = DeactivateClientRequest()
deactivateRequest.clientID = activateResponse.clientID
guard let deactivatedResponse = try? await client.deactivateClient(deactivateRequest) else {
XCTFail("The response of deactivate is nil.")

View File

@ -0,0 +1,89 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import XCTest
@testable import Yorkie
class CRDTElementTests: XCTestCase {
func test_can_set_smaller_movedAt() {
let small = TimeTicket.initialTimeTicket
let big = TimeTicket.maxTimeTicket
let target = CRDTElement(createdAt: big)
let movedResult = target.setMovedAt(small)
XCTAssertEqual(movedResult, true)
XCTAssertEqual(target.getMovedAt()?.compare(small), .orderedSame)
}
func test_can_set_bigger_movedAt_when_movedAt_is_nil() {
let small = TimeTicket.initialTimeTicket
let big = TimeTicket.maxTimeTicket
let target = CRDTElement(createdAt: small)
let movedResult = target.setMovedAt(big)
XCTAssertEqual(movedResult, true)
XCTAssertEqual(target.getMovedAt()?.compare(big), .orderedSame)
}
func test_can_not_set_bigger_movedAt_when_movedAt_is_non_nil() {
let small = TimeTicket.initialTimeTicket
let big = TimeTicket.maxTimeTicket
let target = CRDTElement(createdAt: small)
target.setMovedAt(big)
let timeTicket = TimeTicket(lamport: 10, delimiter: 10, actorID: ActorIds.initialActorID)
let movedResult = target.setMovedAt(timeTicket)
XCTAssertEqual(movedResult, false)
XCTAssertEqual(target.getMovedAt()?.compare(small), .orderedDescending)
}
func test_can_not_remove_when_nil() {
let target = CRDTElement(createdAt: TimeTicket.initialTimeTicket)
XCTAssertEqual(target.remove(nil), false)
}
func test_can_not_remove_when_removeAt_is_before_createdAt() {
let target = CRDTElement(createdAt: TimeTicket.maxTimeTicket)
XCTAssertEqual(target.remove(TimeTicket.initialTimeTicket), false)
}
func test_can_remove_when_current_removeAt_is_nil() {
let target = CRDTElement(createdAt: TimeTicket.initialTimeTicket)
XCTAssertEqual(target.remove(TimeTicket.maxTimeTicket), true)
}
func test_can_remove_when_current_removeAt_is_not_nil_and_samll() {
let target = CRDTElement(createdAt: TimeTicket.initialTimeTicket)
target.setRemovedAt(TimeTicket.initialTimeTicket)
XCTAssertEqual(target.remove(TimeTicket.maxTimeTicket), true)
}
func test_can_not_remove_when_current_removeAt_is_not_nil_and_big() {
let target = CRDTElement(createdAt: TimeTicket.initialTimeTicket)
target.setRemovedAt(TimeTicket.maxTimeTicket)
let timeTicket = TimeTicket(lamport: 10, delimiter: 10, actorID: ActorIds.initialActorID)
XCTAssertEqual(target.remove(timeTicket), false)
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import XCTest
@testable import Yorkie
class PrimitiveTests: XCTestCase {
func test_value_is_null() throws {
let primitiveValue = Primitive(value: .null, createdAt: TimeTicket.initialTimeTicket)
let valueFromData = try Converter.valueFrom(valueType: .null, data: primitiveValue.toBytes())
switch valueFromData {
case .null:
()
default:
XCTFail("Type error.")
}
}
func test_value_is_bool() throws {
let primitiveValue = Primitive(value: .boolean(true), createdAt: TimeTicket.initialTimeTicket)
let valueFromData = try Converter.valueFrom(valueType: .boolean, data: primitiveValue.toBytes())
switch valueFromData {
case .boolean(let value):
XCTAssertEqual(value, true)
default:
XCTFail("Type error.")
}
}
func test_value_is_integer() throws {
let primitiveValue = Primitive(value: .integer(12345), createdAt: TimeTicket.initialTimeTicket)
let valueFromData = try Converter.valueFrom(valueType: .integer, data: primitiveValue.toBytes())
switch valueFromData {
case .integer(let value):
XCTAssertEqual(value, 12345)
default:
XCTFail("Type error.")
}
}
func test_value_is_long() throws {
let primitiveValue = Primitive(value: .long(1_234_567_890), createdAt: TimeTicket.initialTimeTicket)
let valueFromData = try Converter.valueFrom(valueType: .long, data: primitiveValue.toBytes())
switch valueFromData {
case .long(let value):
XCTAssertEqual(value, 1_234_567_890)
default:
XCTFail("Type error.")
}
}
func test_value_is_double() throws {
let primitiveValue = Primitive(value: .double(-123_456_789), createdAt: TimeTicket.initialTimeTicket)
let valueFromData = try Converter.valueFrom(valueType: .double, data: primitiveValue.toBytes())
switch valueFromData {
case .double(let value):
XCTAssertEqual(value, -123_456_789)
default:
XCTFail("Type error.")
}
}
func test_value_is_string() throws {
let primitiveValue = Primitive(value: .string("ABCDEFG"), createdAt: TimeTicket.initialTimeTicket)
let valueFromData = try Converter.valueFrom(valueType: .string, data: primitiveValue.toBytes())
switch valueFromData {
case .string(let value):
XCTAssertEqual(value, "ABCDEFG")
default:
XCTFail("Type error.")
}
}
func test_value_is_bytes() throws {
let testData = "abcdefg".data(using: .utf8)!
let primitiveValue = Primitive(value: .bytes(testData), createdAt: TimeTicket.initialTimeTicket)
let valueFromData = try Converter.valueFrom(valueType: .bytes, data: primitiveValue.toBytes())
switch valueFromData {
case .bytes(let value):
XCTAssertEqual(String(decoding: value, as: UTF8.self), "abcdefg")
default:
XCTFail("Type error.")
}
}
func test_value_is_date() throws {
let testDate = Date()
print(testDate.timeIntervalSince1970)
let primitiveValue = Primitive(value: .date(testDate), createdAt: TimeTicket.initialTimeTicket)
let valueFromData = try Converter.valueFrom(valueType: .date, data: primitiveValue.toBytes())
switch valueFromData {
case .date(let value):
XCTAssertEqual(value.timeIntervalSince1970, testDate.timeIntervalSince1970)
default:
XCTFail("Type error.")
}
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2022 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import XCTest
@testable import Yorkie
class TimeTicketTests: XCTestCase {
func test_compare_with_a_big_thing() {
let small = TimeTicket.initialTimeTicket
let big = TimeTicket.maxTimeTicket
XCTAssertEqual(small.compare(big), .orderedAscending)
}
func test_compare_with_a_small_thing() {
let big = TimeTicket.maxTimeTicket
let small = TimeTicket.initialTimeTicket
XCTAssertEqual(big.compare(small), .orderedDescending)
}
func test_compare_with_a_same_thing() {
let big = TimeTicket.maxTimeTicket
let big2 = TimeTicket.maxTimeTicket
XCTAssertEqual(big.compare(big2), .orderedSame)
}
}

View File

@ -40,16 +40,16 @@ class SplayTreeTests: XCTestCase {
let tree = SplayTree<String>()
let nodeA = tree.insert(StringNode.create("A2"))
XCTAssertEqual(tree.getAnnotatedString(), "[2,2]A2")
XCTAssertEqual(tree.getStructureAsString(), "[2,2]A2")
XCTAssertEqual(tree.getRoot()?.value, "A2")
let nodeB = tree.insert(StringNode.create("B23"))
XCTAssertEqual(tree.getAnnotatedString(), "[2,2]A2[5,3]B23")
XCTAssertEqual(tree.getStructureAsString(), "[2,2]A2[5,3]B23")
XCTAssertEqual(tree.getRoot()?.value, "B23")
let nodeC = tree.insert(StringNode.create("C234"))
XCTAssertEqual(tree.getAnnotatedString(), "[2,2]A2[5,3]B23[9,4]C234")
XCTAssertEqual(tree.getStructureAsString(), "[2,2]A2[5,3]B23[9,4]C234")
XCTAssertEqual(tree.getRoot()?.value, "C234")
let nodeD = tree.insert(StringNode.create("D2345"))
XCTAssertEqual(tree.getAnnotatedString(), "[2,2]A2[5,3]B23[9,4]C234[14,5]D2345")
XCTAssertEqual(tree.getStructureAsString(), "[2,2]A2[5,3]B23[9,4]C234[14,5]D2345")
XCTAssertEqual(tree.getRoot()?.value, "D2345")
XCTAssertEqual(tree.indexOf(nodeA), 0)
@ -66,16 +66,16 @@ class SplayTreeTests: XCTestCase {
let tree = SplayTree<String>()
let nodeH = tree.insert(StringNode.create("H"))
XCTAssertEqual(tree.getAnnotatedString(), "[1,1]H")
XCTAssertEqual(tree.getStructureAsString(), "[1,1]H")
let nodeE = tree.insert(StringNode.create("E"))
XCTAssertEqual(tree.getAnnotatedString(), "[1,1]H[2,1]E")
XCTAssertEqual(tree.getStructureAsString(), "[1,1]H[2,1]E")
let nodeL = tree.insert(StringNode.create("LL"))
XCTAssertEqual(tree.getAnnotatedString(), "[1,1]H[2,1]E[4,2]LL")
XCTAssertEqual(tree.getStructureAsString(), "[1,1]H[2,1]E[4,2]LL")
let nodeO = tree.insert(StringNode.create("O"))
XCTAssertEqual(tree.getAnnotatedString(), "[1,1]H[2,1]E[4,2]LL[5,1]O")
XCTAssertEqual(tree.getStructureAsString(), "[1,1]H[2,1]E[4,2]LL[5,1]O")
tree.delete(nodeE)
XCTAssertEqual(tree.getAnnotatedString(), "[4,1]H[3,2]LL[1,1]O")
XCTAssertEqual(tree.getStructureAsString(), "[4,1]H[3,2]LL[1,1]O")
XCTAssertEqual(tree.indexOf(nodeH), 0)
XCTAssertEqual(tree.indexOf(nodeE), -1)
@ -113,12 +113,12 @@ class SplayTreeTests: XCTestCase {
func test_can_delete_range_between_the_given_2_boundary_nodes_first() {
let testTree = self.sampleTree
// check the filtering of rangeDelete
XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[24,2]HH[25,1]I", testTree.tree.getAnnotatedString())
XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[24,2]HH[25,1]I", testTree.tree.getStructureAsString())
self.removeNodes(testTree.nodes, from: 7, to: 8)
XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[24,0]HH[25,0]I", testTree.tree.getAnnotatedString())
XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[24,0]HH[25,0]I", testTree.tree.getStructureAsString())
testTree.tree.deleteRange(leftBoundary: testTree.nodes[6])
XCTAssertEqual(testTree.tree.indexOf(testTree.nodes[6]), 19)
XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[0,0]HH[0,0]I", testTree.tree.getAnnotatedString())
XCTAssertEqual("[1,1]A[3,2]BB[6,3]CCC[10,4]DDDD[15,5]EEEEE[19,4]FFFF[22,3]GGG[0,0]HH[0,0]I", testTree.tree.getStructureAsString())
XCTAssertTrue(testTree.nodes[6] === testTree.tree.getRoot())
XCTAssertEqual(testTree.nodes[6].getWeight(), 22)
XCTAssertEqual(self.sumOfWeight(testTree.nodes, from: 7, to: 8), 0)

View File

@ -14,12 +14,21 @@
CE3EC95228D195E4009471BC /* RedBlackTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC95028D195E0009471BC /* RedBlackTreeTests.swift */; };
CE3EC95F28D2AAA1009471BC /* SplayTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC95D28D2AA9C009471BC /* SplayTreeTests.swift */; };
CE3EC96128D2D626009471BC /* SplayTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC96028D2D626009471BC /* SplayTree.swift */; };
CE3EC96728D30E74009471BC /* CRDTElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC96628D30E74009471BC /* CRDTElement.swift */; };
CE3EC96928D30FEF009471BC /* ActorId.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC96828D30FEE009471BC /* ActorId.swift */; };
CE3EC96E28D3FFF0009471BC /* TimeTicketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC96C28D3FFED009471BC /* TimeTicketTests.swift */; };
CE3EC97328D40498009471BC /* CRDTElementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC97128D4042D009471BC /* CRDTElementTests.swift */; };
CE3EC97528D41903009471BC /* Primitive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EC97428D41903009471BC /* Primitive.swift */; };
CE6071E528C5D7EE00A8783E /* CONTRIBUTING.md in Resources */ = {isa = PBXBuildFile; fileRef = CE6071E428C5D7EE00A8783E /* CONTRIBUTING.md */; };
CE7B996E28E142A300D56198 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B996D28E142A300D56198 /* Errors.swift */; };
CE7B997028E1453E00D56198 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B996F28E1453E00D56198 /* Strings.swift */; };
CE7B997428E1766F00D56198 /* Converter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B997328E1766F00D56198 /* Converter.swift */; };
CE7B997628E1773A00D56198 /* GRPCTypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B997528E1773A00D56198 /* GRPCTypeAlias.swift */; };
CE7B997828E178EF00D56198 /* PrimitiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B997128E1750000D56198 /* PrimitiveTests.swift */; };
CE8C22EF28C9E85900432DE5 /* Change.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22EE28C9E85900432DE5 /* Change.swift */; };
CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F028C9E86A00432DE5 /* Root.swift */; };
CE8C22F328C9E87800432DE5 /* Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F228C9E87800432DE5 /* Object.swift */; };
CE8C22F528C9E88500432DE5 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F428C9E88500432DE5 /* Operation.swift */; };
CE8C22F728C9E89100432DE5 /* Ticket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F628C9E89100432DE5 /* Ticket.swift */; };
CE8C22F728C9E89100432DE5 /* TimeTicket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F628C9E89100432DE5 /* TimeTicket.swift */; };
CE8C22F928C9E8CA00432DE5 /* Heap.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F828C9E8CA00432DE5 /* Heap.swift */; };
CE8C230528C9F1BD00432DE5 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C230428C9F1BD00432DE5 /* Client.swift */; };
CE8C230728D1514900432DE5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C230628D1514900432DE5 /* Logger.swift */; };
@ -53,12 +62,21 @@
CE3EC95028D195E0009471BC /* RedBlackTreeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedBlackTreeTests.swift; sourceTree = "<group>"; };
CE3EC95D28D2AA9C009471BC /* SplayTreeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplayTreeTests.swift; sourceTree = "<group>"; };
CE3EC96028D2D626009471BC /* SplayTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplayTree.swift; sourceTree = "<group>"; };
CE3EC96628D30E74009471BC /* CRDTElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRDTElement.swift; sourceTree = "<group>"; };
CE3EC96828D30FEE009471BC /* ActorId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorId.swift; sourceTree = "<group>"; };
CE3EC96C28D3FFED009471BC /* TimeTicketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTicketTests.swift; sourceTree = "<group>"; };
CE3EC97128D4042D009471BC /* CRDTElementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRDTElementTests.swift; sourceTree = "<group>"; };
CE3EC97428D41903009471BC /* Primitive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Primitive.swift; sourceTree = "<group>"; };
CE6071E428C5D7EE00A8783E /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
CE7B996D28E142A300D56198 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = "<group>"; };
CE7B996F28E1453E00D56198 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
CE7B997128E1750000D56198 /* PrimitiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimitiveTests.swift; sourceTree = "<group>"; };
CE7B997328E1766F00D56198 /* Converter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Converter.swift; sourceTree = "<group>"; };
CE7B997528E1773A00D56198 /* GRPCTypeAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRPCTypeAlias.swift; sourceTree = "<group>"; };
CE8C22EE28C9E85900432DE5 /* Change.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Change.swift; sourceTree = "<group>"; };
CE8C22F028C9E86A00432DE5 /* Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Root.swift; sourceTree = "<group>"; };
CE8C22F228C9E87800432DE5 /* Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Object.swift; sourceTree = "<group>"; };
CE8C22F428C9E88500432DE5 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
CE8C22F628C9E89100432DE5 /* Ticket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ticket.swift; sourceTree = "<group>"; };
CE8C22F628C9E89100432DE5 /* TimeTicket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTicket.swift; sourceTree = "<group>"; };
CE8C22F828C9E8CA00432DE5 /* Heap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Heap.swift; sourceTree = "<group>"; };
CE8C230428C9F1BD00432DE5 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
CE8C230628D1514900432DE5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Logger.swift; path = Sources/Core/Logger.swift; sourceTree = SOURCE_ROOT; };
@ -128,6 +146,7 @@
96DA808F28C5B7B400E2C1DA /* Tests */ = {
isa = PBXGroup;
children = (
CE3EC97028D40421009471BC /* Document */,
CE6071EB28C5ECA900A8783E /* API */,
CE8C230828D15F5200432DE5 /* Core */,
CE3EC94A28D1885F009471BC /* Util */,
@ -155,10 +174,38 @@
path = Util;
sourceTree = "<group>";
};
CE3EC96B28D3FFDB009471BC /* Time */ = {
isa = PBXGroup;
children = (
CE3EC96C28D3FFED009471BC /* TimeTicketTests.swift */,
);
path = Time;
sourceTree = "<group>";
};
CE3EC96F28D40415009471BC /* CRDT */ = {
isa = PBXGroup;
children = (
CE3EC97128D4042D009471BC /* CRDTElementTests.swift */,
CE7B997128E1750000D56198 /* PrimitiveTests.swift */,
);
path = CRDT;
sourceTree = "<group>";
};
CE3EC97028D40421009471BC /* Document */ = {
isa = PBXGroup;
children = (
CE3EC96F28D40415009471BC /* CRDT */,
CE3EC96B28D3FFDB009471BC /* Time */,
);
path = Document;
sourceTree = "<group>";
};
CE6071E828C5EC2500A8783E /* API */ = {
isa = PBXGroup;
children = (
CEEB17CF28C8493F004988DD /* V1 */,
CE7B997328E1766F00D56198 /* Converter.swift */,
CE7B997528E1773A00D56198 /* GRPCTypeAlias.swift */,
);
path = API;
sourceTree = "<group>";
@ -204,6 +251,7 @@
CE8C22F828C9E8CA00432DE5 /* Heap.swift */,
CE3EC94E28D1922E009471BC /* RedBlackTree.swift */,
CE3EC96028D2D626009471BC /* SplayTree.swift */,
CE7B996D28E142A300D56198 /* Errors.swift */,
);
path = Util;
sourceTree = "<group>";
@ -219,7 +267,8 @@
CE8C22E728C9E55800432DE5 /* CRDT */ = {
isa = PBXGroup;
children = (
CE8C22F028C9E86A00432DE5 /* Root.swift */,
CE3EC96628D30E74009471BC /* CRDTElement.swift */,
CE3EC97428D41903009471BC /* Primitive.swift */,
);
path = CRDT;
sourceTree = "<group>";
@ -228,6 +277,7 @@
isa = PBXGroup;
children = (
CE8C22F228C9E87800432DE5 /* Object.swift */,
CE7B996F28E1453E00D56198 /* Strings.swift */,
);
path = Json;
sourceTree = "<group>";
@ -243,7 +293,8 @@
CE8C22EA28C9E56B00432DE5 /* Time */ = {
isa = PBXGroup;
children = (
CE8C22F628C9E89100432DE5 /* Ticket.swift */,
CE8C22F628C9E89100432DE5 /* TimeTicket.swift */,
CE3EC96828D30FEE009471BC /* ActorId.swift */,
);
path = Time;
sourceTree = "<group>";
@ -451,18 +502,24 @@
buildActionMask = 2147483647;
files = (
CE3EC96128D2D626009471BC /* SplayTree.swift in Sources */,
CE3EC96928D30FEF009471BC /* ActorId.swift in Sources */,
CE8C230728D1514900432DE5 /* Logger.swift in Sources */,
CE3EC97528D41903009471BC /* Primitive.swift in Sources */,
CEEB17E428C84D26004988DD /* resources.pb.swift in Sources */,
CE8C22F928C9E8CA00432DE5 /* Heap.swift in Sources */,
CE8C22EF28C9E85900432DE5 /* Change.swift in Sources */,
CEEB17E328C84D26004988DD /* yorkie.pb.swift in Sources */,
CE3EC94F28D1922E009471BC /* RedBlackTree.swift in Sources */,
CE7B997028E1453E00D56198 /* Strings.swift in Sources */,
CE8C230528C9F1BD00432DE5 /* Client.swift in Sources */,
CE8C22F528C9E88500432DE5 /* Operation.swift in Sources */,
CE8C22F728C9E89100432DE5 /* Ticket.swift in Sources */,
CE7B997428E1766F00D56198 /* Converter.swift in Sources */,
CE8C22F728C9E89100432DE5 /* TimeTicket.swift in Sources */,
CE7B997628E1773A00D56198 /* GRPCTypeAlias.swift in Sources */,
CE7B996E28E142A300D56198 /* Errors.swift in Sources */,
CE8C22F328C9E87800432DE5 /* Object.swift in Sources */,
CE3EC96728D30E74009471BC /* CRDTElement.swift in Sources */,
CEEB17E528C84D26004988DD /* yorkie.grpc.swift in Sources */,
CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -473,6 +530,9 @@
CECCCB8428C96CD600544204 /* XCTestCase+Extension.swift in Sources */,
CE3EC95F28D2AAA1009471BC /* SplayTreeTests.swift in Sources */,
CE8C230B28D15FF200432DE5 /* ClientTests.swift in Sources */,
CE7B997828E178EF00D56198 /* PrimitiveTests.swift in Sources */,
CE3EC97328D40498009471BC /* CRDTElementTests.swift in Sources */,
CE3EC96E28D3FFF0009471BC /* TimeTicketTests.swift in Sources */,
96DA809128C5B7B400E2C1DA /* GRPCTests.swift in Sources */,
CE3EC94D28D189EF009471BC /* HeapTests.swift in Sources */,
CE3EC95228D195E4009471BC /* RedBlackTreeTests.swift in Sources */,