Files
CoreStore/CoreStoreTests/DynamicModelTests.swift
2019-02-08 18:42:19 +09:00

358 lines
14 KiB
Swift

//
// DynamicModelTests.swift
// CoreStore
//
// Copyright © 2018 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import XCTest
@testable
import CoreStore
#if os(macOS)
typealias Color = NSColor
#else
typealias Color = UIColor
#endif
class Animal: CoreStoreObject {
let species = Value.Required<String>("species", initial: "Swift")
let master = Relationship.ToOne<Person>("master")
let color = Transformable.Optional<Color>("color")
}
class Dog: Animal {
let nickname = Value.Optional<String>("nickname")
let age = Value.Required<Int>("age", initial: 1)
let friends = Relationship.ToManyOrdered<Dog>("friends")
let friendedBy = Relationship.ToManyUnordered<Dog>("friendedBy", inverse: { $0.friends })
}
class Person: CoreStoreObject {
let title = Value.Required<String>(
"title",
initial: "Mr.",
customSetter: Person.setTitle
)
let name = Value.Required<String>(
"name",
initial: "",
customSetter: Person.setName
)
let displayName = Value.Optional<String>(
"displayName",
isTransient: true,
customGetter: Person.getDisplayName(_:),
affectedByKeyPaths: Person.keyPathsAffectingDisplayName()
)
let spouse = Relationship.ToOne<Person>("spouse")
let pets = Relationship.ToManyUnordered<Animal>("pets", inverse: { $0.master })
private let _spouse = Relationship.ToOne<Person>("_spouseInverse", inverse: { $0.spouse })
private static func setTitle(_ partialObject: PartialObject<Person>, _ newValue: String) {
partialObject.setPrimitiveValue(newValue, for: { $0.title })
partialObject.setPrimitiveValue(nil, for: { $0.displayName })
}
private static func setName(_ partialObject: PartialObject<Person>, _ newValue: String) {
partialObject.setPrimitiveValue(newValue, for: { $0.name })
partialObject.setPrimitiveValue(nil, for: { $0.displayName })
}
static func getDisplayName(_ partialObject: PartialObject<Person>) -> String? {
if let displayName = partialObject.primitiveValue(for: { $0.displayName }) {
return displayName
}
let title = partialObject.value(for: { $0.title })
let name = partialObject.value(for: { $0.name })
let displayName = "\(title) \(name)"
partialObject.setPrimitiveValue(displayName, for: { $0.displayName })
return displayName
}
static func keyPathsAffectingDisplayName() -> Set<String> {
return [
String(keyPath: \Person.title),
String(keyPath: \Person.name)
]
}
}
// MARK: - DynamicModelTests
class DynamicModelTests: BaseTestDataTestCase {
@objc
dynamic func test_ThatDynamicModels_CanBeDeclaredCorrectly() {
let dataStack = DataStack(
CoreStoreSchema(
modelVersion: "V1",
entities: [
Entity<Animal>("Animal"),
Entity<Dog>("Dog"),
Entity<Person>("Person")
],
versionLock: [
"Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
"Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7],
"Person": [0x2831cf046084d96d, 0xbe19b13ace54641, 0x635a082728b0f6f0, 0x3d4ef2dd4b74a87c]
]
)
)
self.prepareStack(dataStack, configurations: [nil]) { (stack) in
let k1 = String(keyPath: \Animal.species)
XCTAssertEqual(k1, "species")
#if swift(<5.0)
let k2 = String(keyPath: \Dog.species)
XCTAssertEqual(k2, "species")
#endif
let k3 = String(keyPath: \Dog.nickname)
XCTAssertEqual(k3, "nickname")
let updateDone = self.expectation(description: "update-done")
let fetchDone = self.expectation(description: "fetch-done")
let willSetPriorObserverDone = self.expectation(description: "willSet-observe-prior-done")
let willSetNotPriorObserverDone = self.expectation(description: "willSet-observe-notPrior-done")
let didSetObserverDone = self.expectation(description: "didSet-observe-done")
stack.perform(
asynchronous: { (transaction) in
let animal = transaction.create(Into<Animal>())
XCTAssertEqual(animal.species.value, "Swift")
XCTAssertTrue(type(of: animal.species.value) == String.self)
animal.species .= "Sparrow"
XCTAssertEqual(animal.species.value, "Sparrow")
animal.color .= .yellow
XCTAssertEqual(animal.color.value, Color.yellow)
let dog = transaction.create(Into<Dog>())
XCTAssertEqual(dog.species.value, "Swift")
XCTAssertEqual(dog.nickname.value, nil)
XCTAssertEqual(dog.age.value, 1)
let didSetObserver = dog.species.observe(options: [.new, .old]) { (object, change) in
XCTAssertEqual(object, dog)
XCTAssertEqual(change.kind, .setting)
XCTAssertEqual(change.newValue, "Dog")
XCTAssertEqual(change.oldValue, "Swift")
XCTAssertFalse(change.isPrior)
XCTAssertEqual(object.species.value, "Dog")
didSetObserverDone.fulfill()
}
let willSetObserver = dog.species.observe(options: [.new, .old, .prior]) { (object, change) in
XCTAssertEqual(object, dog)
XCTAssertEqual(change.kind, .setting)
XCTAssertEqual(change.oldValue, "Swift")
if change.isPrior {
XCTAssertNil(change.newValue)
XCTAssertEqual(object.species.value, "Swift")
willSetPriorObserverDone.fulfill()
}
else {
XCTAssertEqual(change.newValue, "Dog")
XCTAssertEqual(object.species.value, "Dog")
willSetNotPriorObserverDone.fulfill()
}
}
dog.species .= "Dog"
XCTAssertEqual(dog.species.value, "Dog")
didSetObserver.invalidate()
willSetObserver.invalidate()
dog.nickname .= "Spot"
XCTAssertEqual(dog.nickname.value, "Spot")
let person = transaction.create(Into<Person>())
XCTAssertTrue(person.pets.value.isEmpty)
XCTAssertEqual(
cs_dynamicType(of: person.rawObject!).keyPathsForValuesAffectingValue(forKey: "displayName"),
["title", "name"]
)
person.name .= "Joe"
XCTAssertEqual(person.rawObject!.value(forKey: "name") as! String?, "Joe")
XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "Mr. Joe")
person.rawObject!.setValue("AAAA", forKey: "displayName")
XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "AAAA")
person.name .= "John"
XCTAssertEqual(person.name.value, "John")
XCTAssertEqual(person.displayName.value, "Mr. John") // Custom getter
person.title .= "Sir"
XCTAssertEqual(person.displayName.value, "Sir John")
person.pets.value.insert(dog)
XCTAssertEqual(person.pets.count, 1)
XCTAssertEqual(person.pets.value.first, dog)
XCTAssertEqual(person.pets.value.first?.master.value, person)
XCTAssertEqual(dog.master.value, person)
XCTAssertEqual(dog.master.value?.pets.value.first, dog)
},
success: { _ in
updateDone.fulfill()
},
failure: { _ in
XCTFail()
}
)
stack.perform(
asynchronous: { (transaction) in
let p1 = Where<Animal>({ $0.species == "Sparrow" })
XCTAssertEqual(p1.predicate, NSPredicate(format: "%K == %@", "species", "Sparrow"))
let bird = try transaction.fetchOne(From<Animal>(), p1)
XCTAssertNotNil(bird)
XCTAssertEqual(bird!.species.value, "Sparrow")
let p2 = Where<Dog>({ $0.nickname == "Spot" })
XCTAssertEqual(p2.predicate, NSPredicate(format: "%K == %@", "nickname", "Spot"))
let dog = try transaction.fetchOne(From<Dog>().where(\.nickname == "Spot"))
XCTAssertNotNil(dog)
XCTAssertEqual(dog!.nickname.value, "Spot")
XCTAssertEqual(dog!.species.value, "Dog")
let person = try transaction.fetchOne(From<Person>())
XCTAssertNotNil(person)
XCTAssertEqual(person!.pets.value.first, dog)
let p3 = Where<Dog>({ $0.age == 10 })
XCTAssertEqual(p3.predicate, NSPredicate(format: "%K == %d", "age", 10))
let totalAge = try transaction.queryValue(From<Dog>().select(Int.self, .sum(\Dog.age)))
XCTAssertEqual(totalAge, 1)
_ = try transaction.fetchAll(
From<Dog>()
.where(\Animal.species == "Dog" && \Dog.age == 10)
)
_ = try transaction.fetchAll(
From<Dog>()
.where(\Dog.age == 10 && \Animal.species == "Dog")
.orderBy(.ascending({ $0.species }))
)
_ = try transaction.fetchAll(
From<Dog>(),
Where<Dog>({ $0.age > 10 && $0.age <= 15 })
)
_ = try transaction.fetchAll(
From<Dog>(),
Where<Dog>({ $0.species == "Dog" && $0.age == 10 })
)
_ = try transaction.fetchAll(
From<Dog>(),
Where<Dog>({ $0.age == 10 && $0.species == "Dog" })
)
_ = try transaction.fetchAll(
From<Dog>(),
Where<Dog>({ $0.age > 10 && $0.age <= 15 })
)
_ = try transaction.fetchAll(
From<Dog>(),
(\Dog.age > 10 && \Dog.age <= 15)
)
},
success: { _ in
fetchDone.fulfill()
},
failure: { _ in
XCTFail()
}
)
self.waitAndCheckExpectations()
}
}
@objc
dynamic func test_ThatDynamicModelKeyPaths_CanBeCreated() {
XCTAssertEqual(String(keyPath: \Animal.species), "species")
#if swift(<5.0)
XCTAssertEqual(String(keyPath: \Dog.species), "species")
#endif
}
@nonobjc
func prepareStack(_ dataStack: DataStack, configurations: [ModelConfiguration] = [nil], _ closure: (_ dataStack: DataStack) -> Void) {
do {
try configurations.forEach { (configuration) in
try dataStack.addStorageAndWait(
SQLiteStore(
fileURL: SQLiteStore.defaultRootDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathComponent("\(type(of: self))_\((configuration ?? "-null-")).sqlite"),
configuration: configuration,
localStorageOptions: .recreateStoreOnModelMismatch
)
)
}
}
catch let error as NSError {
XCTFail(error.coreStoreDumpString)
}
closure(dataStack)
}
}