// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation import Quick @testable import SessionUtilitiesKit public class TestDependencies: Dependencies { private var singletonInstances: [String: Any] = [:] private var cacheInstances: [String: MutableCacheType] = [:] private var defaultsInstances: [String: (any UserDefaultsType)] = [:] private var featureInstances: [String: (any FeatureType)] = [:] // MARK: - Subscript Access override public subscript(singleton singleton: SingletonConfig) -> S { guard let value: S = (singletonInstances[singleton.identifier] as? S) else { let value: S = singleton.createInstance(self) singletonInstances[singleton.identifier] = value return value } return value } public subscript(singleton singleton: SingletonConfig) -> S? { get { return (singletonInstances[singleton.identifier] as? S) } set { singletonInstances[singleton.identifier] = newValue } } override public subscript(cache cache: CacheConfig) -> I { guard let value: M = (cacheInstances[cache.identifier] as? M) else { let value: M = cache.createInstance(self) let mutableInstance: MutableCacheType = cache.mutableInstance(value) cacheInstances[cache.identifier] = mutableInstance return cache.immutableInstance(value) } return cache.immutableInstance(value) } public subscript(cache cache: CacheConfig) -> M? { get { return (cacheInstances[cache.identifier] as? M) } set { cacheInstances[cache.identifier] = newValue.map { cache.mutableInstance($0) } } } override public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType { guard let value: UserDefaultsType = defaultsInstances[defaults.identifier] else { let value: UserDefaultsType = defaults.createInstance(self) defaultsInstances[defaults.identifier] = value return value } return value } override public subscript(feature feature: FeatureConfig) -> T { guard let value: Feature = (featureInstances[feature.identifier] as? Feature) else { let value: Feature = feature.createInstance(self) featureInstances[feature.identifier] = value return value.currentValue(using: self) } return value.currentValue(using: self) } public subscript(feature feature: FeatureConfig) -> T? { get { return (featureInstances[feature.identifier] as? T) } set { if featureInstances[feature.identifier] == nil { featureInstances[feature.identifier] = feature.createInstance(self) } set(feature: feature, to: newValue) } } public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType? { get { return defaultsInstances[defaults.identifier] } set { defaultsInstances[defaults.identifier] = newValue } } // MARK: - Timing and Async Handling @ThreadSafeObject private var cachedDateNow: Date? = nil override public var dateNow: Date { get { (cachedDateNow ?? Date()) } set { _cachedDateNow.set(to: newValue) } } @ThreadSafe private var cachedFixedTime: Int? = nil override public var fixedTime: Int { get { (cachedFixedTime ?? 0) } set { cachedFixedTime = newValue } } public var _forceSynchronous: Bool = false override public var forceSynchronous: Bool { get { _forceSynchronous } set { _forceSynchronous = newValue } } private var asyncExecutions: [Int: [() -> Void]] = [:] // MARK: - Initialization public init(initialState: ((TestDependencies) -> ())? = nil) { super.init(forTesting: true) initialState?(self) } // MARK: - Functions override public func async(at timestamp: TimeInterval, closure: @escaping () -> Void) { asyncExecutions.append(closure, toArrayOn: Int(ceil(timestamp))) } @discardableResult override public func mutate( cache: CacheConfig, _ mutation: (M) -> R ) -> R { let value: M = ((cacheInstances[cache.identifier] as? M) ?? cache.createInstance(self)) let mutableInstance: MutableCacheType = cache.mutableInstance(value) cacheInstances[cache.identifier] = mutableInstance return mutation(value) } @discardableResult override public func mutate( cache: CacheConfig, _ mutation: (M) throws -> R ) throws -> R { let value: M = ((cacheInstances[cache.identifier] as? M) ?? cache.createInstance(self)) let mutableInstance: MutableCacheType = cache.mutableInstance(value) cacheInstances[cache.identifier] = mutableInstance return try mutation(value) } public func stepForwardInTime() { let targetTime: Int = ((cachedFixedTime ?? 0) + 1) cachedFixedTime = targetTime if let currentDate: Date = cachedDateNow { _cachedDateNow.set(to: Date(timeIntervalSince1970: currentDate.timeIntervalSince1970 + 1)) } // Run and clear any executions which should run at the target time let targetKeys: [Int] = asyncExecutions.keys .filter { $0 <= targetTime } targetKeys.forEach { key in asyncExecutions[key]?.forEach { $0() } asyncExecutions[key] = nil } } // MARK: - Random Access Functions public override func randomElement(_ collection: T) -> T.Element? { return collection.first } /// `Set` is unsorted so we need a deterministic method to retrieve the same value each time public override func randomElement(_ elements: Set) -> T? { return Array(elements) .sorted { lhs, rhs -> Bool in lhs.hashValue < rhs.hashValue } .first } /// `Set` is unsorted so we need a deterministic method to retrieve the same value each time public override func popRandomElement(_ elements: inout Set) -> T? { let result: T? = Array(elements) .sorted { lhs, rhs -> Bool in lhs.hashValue < rhs.hashValue } .first return result.map { elements.remove($0) } } // MARK: - Instance replacing public override func set(singleton: SingletonConfig, to instance: S) { singletonInstances[singleton.identifier] = instance } public override func set(cache: CacheConfig, to instance: M) { cacheInstances[cache.identifier] = cache.mutableInstance(instance) } public override func remove(cache: CacheConfig) { cacheInstances[cache.identifier] = nil } } // MARK: - TestState Convenience internal extension TestState { init( wrappedValue: @escaping @autoclosure () -> T?, cache: CacheConfig, in dependenciesRetriever: @escaping @autoclosure () -> TestDependencies? ) where T: MutableCacheType { self.init(wrappedValue: { let dependencies: TestDependencies? = dependenciesRetriever() let value: T? = wrappedValue() (value as? DependenciesSettable)?.setDependencies(dependencies) dependencies?[cache: cache] = (value as! M) return value }()) } init( wrappedValue: @escaping @autoclosure () -> T?, singleton: SingletonConfig, in dependenciesRetriever: @escaping @autoclosure () -> TestDependencies? ) { self.init(wrappedValue: { let dependencies: TestDependencies? = dependenciesRetriever() let value: T? = wrappedValue() (value as? DependenciesSettable)?.setDependencies(dependencies) dependencies?[singleton: singleton] = (value as! S) return value }()) } init( wrappedValue: @escaping @autoclosure () -> T?, defaults: UserDefaultsConfig, in dependenciesRetriever: @escaping @autoclosure () -> TestDependencies? ) where T: UserDefaultsType { self.init(wrappedValue: { let dependencies: TestDependencies? = dependenciesRetriever() let value: T? = wrappedValue() (value as? DependenciesSettable)?.setDependencies(dependencies) dependencies?[defaults: defaults] = value return value }()) } }