From 23cceff08ed0188fc453d66b900abecf4f5b57d5 Mon Sep 17 00:00:00 2001 From: Sean Levin Date: Mon, 11 Oct 2021 11:30:17 -0700 Subject: [PATCH] migrate from internal repo --- .gitignore | 93 +++ BGSwift.podspec | 40 + BGSwift/Classes/BGAction.swift | 10 + BGSwift/Classes/BGBehavior.swift | 63 ++ BGSwift/Classes/BGEvent.swift | 14 + BGSwift/Classes/BGExtent.swift | 230 ++++++ BGSwift/Classes/BGGraph.swift | 474 ++++++++++++ BGSwift/Classes/BGResource.swift | 309 ++++++++ BGSwift/Classes/BGSideEffect.swift | 17 + BGSwift/Classes/Mutex.swift | 74 ++ BGSwift/Classes/PriorityQueue.swift | 69 ++ BGSwift/Classes/WeakSet.swift | 44 ++ BGSwift/Classes/WeakWrapper.swift | 27 + CONTRIBUTING.md | 36 + Code_of_Conduct.md | 10 +- Example/BGSwift.xcodeproj/project.pbxproj | 714 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/BGSwift-Example.xcscheme | 110 +++ Example/BGSwift/AppDelegate.swift | 43 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 +++ Example/BGSwift/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + Example/BGSwift/Base.lproj/Main.storyboard | 138 ++++ Example/BGSwift/Info.plist | 5 + Example/BGSwift/LoginExtent.swift | 114 +++ Example/BGSwift/ViewController.swift | 63 ++ Example/Tests/AssertionTests.swift | 54 ++ Example/Tests/BGExtentTests.swift | 93 +++ Example/Tests/BGGraphTests.swift | 99 +++ Example/Tests/BGMomentTests.swift | 212 ++++++ Example/Tests/BGStateTests.swift | 593 +++++++++++++++ Example/Tests/ConcurrencyTests.swift | 310 ++++++++ Example/Tests/DependenciesTests.swift | 77 ++ Example/Tests/DynamicsTests.swift | 465 ++++++++++++ Example/Tests/EventTests.swift | 164 ++++ Example/Tests/ImpulseTests.swift | 34 + Example/Tests/TestSupport.swift | 30 + LICENSE.txt | 202 +++++ Podfile | 23 + Podfile.lock | 29 + README.md | 38 +- bgswift.xcworkspace/contents.xcworkspacedata | 10 + .../xcshareddata/IDETemplateMacros.plist | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + 46 files changed, 5297 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100644 BGSwift.podspec create mode 100644 BGSwift/Classes/BGAction.swift create mode 100644 BGSwift/Classes/BGBehavior.swift create mode 100644 BGSwift/Classes/BGEvent.swift create mode 100644 BGSwift/Classes/BGExtent.swift create mode 100644 BGSwift/Classes/BGGraph.swift create mode 100644 BGSwift/Classes/BGResource.swift create mode 100644 BGSwift/Classes/BGSideEffect.swift create mode 100644 BGSwift/Classes/Mutex.swift create mode 100644 BGSwift/Classes/PriorityQueue.swift create mode 100644 BGSwift/Classes/WeakSet.swift create mode 100644 BGSwift/Classes/WeakWrapper.swift create mode 100644 CONTRIBUTING.md create mode 100644 Example/BGSwift.xcodeproj/project.pbxproj create mode 100644 Example/BGSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Example/BGSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example/BGSwift.xcodeproj/xcshareddata/xcschemes/BGSwift-Example.xcscheme create mode 100644 Example/BGSwift/AppDelegate.swift create mode 100644 Example/BGSwift/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Example/BGSwift/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/BGSwift/Assets.xcassets/Contents.json create mode 100644 Example/BGSwift/Base.lproj/LaunchScreen.storyboard create mode 100644 Example/BGSwift/Base.lproj/Main.storyboard create mode 100644 Example/BGSwift/Info.plist create mode 100644 Example/BGSwift/LoginExtent.swift create mode 100644 Example/BGSwift/ViewController.swift create mode 100644 Example/Tests/AssertionTests.swift create mode 100644 Example/Tests/BGExtentTests.swift create mode 100644 Example/Tests/BGGraphTests.swift create mode 100644 Example/Tests/BGMomentTests.swift create mode 100644 Example/Tests/BGStateTests.swift create mode 100644 Example/Tests/ConcurrencyTests.swift create mode 100644 Example/Tests/DependenciesTests.swift create mode 100644 Example/Tests/DynamicsTests.swift create mode 100644 Example/Tests/EventTests.swift create mode 100644 Example/Tests/ImpulseTests.swift create mode 100644 Example/Tests/TestSupport.swift create mode 100644 LICENSE.txt create mode 100644 Podfile create mode 100644 Podfile.lock create mode 100644 bgswift.xcworkspace/contents.xcworkspacedata create mode 100644 bgswift.xcworkspace/xcshareddata/IDETemplateMacros.plist create mode 100644 bgswift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03c39e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +# macOS +.DS_Store + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control + +Pods/ + +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ diff --git a/BGSwift.podspec b/BGSwift.podspec new file mode 100644 index 0000000..6dcc1fd --- /dev/null +++ b/BGSwift.podspec @@ -0,0 +1,40 @@ +Pod::Spec.new do |s| + s.name = 'BGSwift' + s.platform = :ios, '12.0' + s.version = '0.5.0' + s.summary = 'Behavior Graph is a software library that greatly enhances our ability to program user facing software and control systems.' + + s.description = <<-DESC +Behavior Graph is a software library that greatly enhances our ability to program user facing software and control systems. Programs of this type quickly scale up in complexity as features are added. Behavior Graph directly addresses this complexity by shifting more of the burden to the computer. It works by offering the programmer a new unit of code organization called a behavior. Behaviors are blocks of code enriched with additional information about their stateful relationships. Using this information, Behavior Graph enforces _safe use of mutable state_, arguably the primary source of complexity in this class of software. It does this by taking on the responsibility of control flow between behaviors, ensuring they are are _run at the correct time and in the correct order_. +DESC + + s.license = { :type => 'Apache-2.0', :file => 'LICENSE.txt' } + s.homepage = 'https://www.github.com/yahoo/BGSwift' + s.authors = { + 'James Lou' => 'jlou@yahooinc.com', + 'Sean Levin' => 'slevin@yahooinc.com' + } + s.source = { + :git => 'git@github.com:yahoo/BGSwift.git', + :tag => s.version.to_s + } + s.requires_arc = true + + s.default_subspecs = 'Core' + + s.subspec "Core" do |sp| + sp.resources = [] + + sp.source_files =[ + 'BGSwift/Classes/**/*.{h,m,swift}', + ] + + sp.public_header_files = [ + ] + + sp.frameworks = [ + 'Foundation', + ] + end +end + diff --git a/BGSwift/Classes/BGAction.swift b/BGSwift/Classes/BGAction.swift new file mode 100644 index 0000000..474ee44 --- /dev/null +++ b/BGSwift/Classes/BGAction.swift @@ -0,0 +1,10 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation + +struct BGAction { + let impulse: String? + let action: () -> Void +} diff --git a/BGSwift/Classes/BGBehavior.swift b/BGSwift/Classes/BGBehavior.swift new file mode 100644 index 0000000..3c1ba6a --- /dev/null +++ b/BGSwift/Classes/BGBehavior.swift @@ -0,0 +1,63 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation + +enum OrderingState: Int { + case ordered + case ordering + case unordered +} + +public class BGBehavior { + var orderingState = OrderingState.ordered + var order = UInt(0) + var enqueuedSequence = UInt(0) + var lastUpdateSequence = UInt(0) + var removedSequence = UInt(0) + + var propertyName: String? + + var supplies = WeakSet() + var demands = WeakSet() + + var modifiedDemands: [BGResource]? + var modifiedSupplies: [BGResource]? + + let runBlock: (BGExtent) -> Void + + weak var extent: BGExtent? + + + init(supplies: [BGResource], demands: [BGResource], body: @escaping (BGExtent) -> Void) { + modifiedSupplies = supplies + modifiedDemands = demands + + self.runBlock = body + } + + func run() { + guard let extent = self.extent else { + return + } + runBlock(extent) + } + + + public func setDemands(_ demands: [BGResource]) { + modifiedDemands = demands + extent?.graph?.updateDemands(behavior: self) + } + + public func setSupplies(_ supplies: [BGResource]) { + modifiedSupplies = supplies + extent?.graph?.updateSupplies(behavior: self) + } +} + +extension BGBehavior: CustomDebugStringConvertible { + public var debugDescription: String { + return "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(propertyName ?? "Unlabeled"))>" + } +} diff --git a/BGSwift/Classes/BGEvent.swift b/BGSwift/Classes/BGEvent.swift new file mode 100644 index 0000000..756d4f5 --- /dev/null +++ b/BGSwift/Classes/BGEvent.swift @@ -0,0 +1,14 @@ +// +// Copyright © 2021 Yahoo +// + + +import Foundation + +public struct BGEvent: Equatable { + public let sequence: UInt + public let timestamp: Date + public let impulse: String? + + public static let unknownPast = BGEvent(sequence: 0, timestamp: Date(timeIntervalSince1970: 0), impulse: nil) +} diff --git a/BGSwift/Classes/BGExtent.swift b/BGSwift/Classes/BGExtent.swift new file mode 100644 index 0000000..6c6475b --- /dev/null +++ b/BGSwift/Classes/BGExtent.swift @@ -0,0 +1,230 @@ +// +// Copyright © 2021 Yahoo +// + + +import Foundation + +open class BGExtent { + public weak var graph: BGGraph? + + let resources: [BGResource] + let behaviors: [BGBehavior] + + var _added: BGMoment + public var added: BGResource { _added } + + public init(builder: BGExtentBuilderGeneric) { + graph = builder.graph + _added = builder._added + resources = builder.resources + behaviors = builder.behaviors + + builder.resources.removeAll() + builder.behaviors.removeAll() + + self.resources.forEach { + $0.extent = self + } + + self.behaviors.forEach { + $0.extent = self + } + + let mirror = Mirror(reflecting: self) + mirror.children.forEach { child in + if let resource = child.value as? BGResource { + if resource.propertyName == nil { + resource.propertyName = "\(String(describing: Self.self)).\(child.label ?? "Anonymous_Resource")" + } + } else if let behavior = child.value as? BGBehavior { + if behavior.propertyName == nil { + behavior.propertyName = "\(String(describing: Self.self)).\(child.label ?? "Anonymous_Behavior")" + } + } + } + } + + deinit { + guard let graph = self.graph else { + return + } + let behaviors = self.behaviors + + graph.action(syncStrategy: .none) { [weak graph] in + guard let graph = graph else { + return + } + + behaviors.forEach(graph.removeBehavior) + } + } + + public func addToGraph() { + graph?.addExtent(self) + } + + public func addToGraphWithAction() { + graph?.action { + self.addToGraph() + } + } + + public func sideEffect(file: String = #fileID, line: Int = #line, function: String = #function, _ body: @escaping () -> Void) { + let impulse = BGGraph.impulseString(file: file, line: line, function: function) + sideEffect(impulse, body) + } + + public func sideEffect(_ label: String?, _ body: @escaping () -> Void) { + guard let graph = graph else { + // assert + return + } + graph.sideEffect(label, body: body) + } +} + +public class BGExtentBuilderGeneric { + var resources = [BGResource]() + var behaviors = [BGBehavior]() + let graph: BGGraph + let _added = BGMoment() + public var added: BGResource { _added } + + public init(graph: BGGraph) { + self.graph = graph + resources.append(_added) + } + + public func moment() -> BGMoment { + let moment = BGMoment() + resources.append(moment) + return moment + } + + public func typedMoment() -> BGTypedMoment { + let moment = BGTypedMoment() + resources.append(moment) + return moment + } + + public func state(_ value: T, comparison: @escaping (T, T) -> Bool) -> BGState { + let state = BGState(value, comparison: comparison) + resources.append(state) + return state + } + + @_disfavoredOverload + public func state(_ value: T, comparison: BGResource.ComparisonNone = .none) -> BGState { + return state(value) { _, _ in + false + } + } + + public func state(_ value: T, comparison: BGResource.ComparisonEqual = .equal) -> BGState { + return state(value) { lhs, rhs in + lhs == rhs + } + } + + public func state(_ value: T, comparison: BGResource.ComparisonIdentical = .identical) -> BGState { + return state(value) { lhs, rhs in + lhs === rhs + } + } + + @_disfavoredOverload + public func state(_ value: T, comparison: BGResource.ComparisonIdentical = .identical) -> BGState { + return state(value) { lhs, rhs in + lhs.bg_unwrapped === rhs.bg_unwrapped + } + } +} + +public class BGExtentBuilder: BGExtentBuilderGeneric { + + @discardableResult public func behavior(supplies staticSupplies: [BGResource] = [], + demands staticDemands: [BGResource] = [], + body: @escaping (Extent) -> Void) -> BGBehavior { + return behavior(supplies: staticSupplies, demands: staticDemands, dynamicSupplies: nil, dynamicDemands: nil, body: body) + } + + @discardableResult public func behavior(supplies staticSupplies: [BGResource] = [], + demands staticDemands: [BGResource] = [], + dynamicSupplies: DynamicResourceLink? = nil, + dynamicDemands: DynamicResourceLink? = nil, + body: @escaping (Extent) -> Void) -> BGBehavior { + let genericBody: (BGExtent) -> Void = { extent in + body(extent as! Extent) + } + + var extendedDemands = staticDemands + let dynamicSuppliesOrderingResource: BGResource? + let dynamicDemandsOrderingResource: BGResource? + + if let dynamicSupplies = dynamicSupplies, !dynamicSupplies.switches.isEmpty { + let orderingResource = moment() + orderingResource.propertyName = "DynamicSuppliesOrdering" + + dynamicSuppliesOrderingResource = orderingResource + extendedDemands.append(orderingResource) + } else { + dynamicSuppliesOrderingResource = nil + } + + if let dynamicDemands = dynamicDemands, !dynamicDemands.switches.isEmpty { + let orderingResource = moment() + orderingResource.propertyName = "DynamicDemandsOrdering" + + dynamicDemandsOrderingResource = orderingResource + extendedDemands.append(orderingResource) + } else { + dynamicDemandsOrderingResource = nil + } + + let behavior = BGBehavior(supplies: staticSupplies, + demands: extendedDemands, + body: genericBody) + behaviors.append(behavior) + + if let dynamicSupplies = dynamicSupplies, let orderingResource = dynamicSuppliesOrderingResource { + let resolver = dynamicSupplies.resolver + let implicitBehavior = BGBehavior(supplies: [orderingResource], demands: dynamicSupplies.switches) { [weak behavior] extent in + guard let behavior = behavior else { + return + } + var supplies = staticSupplies + supplies.append(contentsOf: resolver(extent as! Extent)) + behavior.setSupplies(supplies) + } + behaviors.append(implicitBehavior) + } + + if let dynamicDemands = dynamicDemands, let orderingResource = dynamicDemandsOrderingResource { + let resolver = dynamicDemands.resolver + let implicitBehavior = BGBehavior(supplies: [orderingResource], demands: dynamicDemands.switches) { [weak behavior] extent in + guard let behavior = behavior else { + return + } + var demands = extendedDemands + demands.append(contentsOf: resolver(extent as! Extent)) + behavior.setDemands(demands) + } + behaviors.append(implicitBehavior) + } + + return behavior + } +} + +public class DynamicResourceLink { + var switches: [BGResource] + var resolver: (Extent) -> ([BGResource]) + + public init(switches: [BGResource], _ resolver: @escaping (Extent) -> ([BGResource])) { + self.switches = switches + self.resolver = resolver + } +} + + diff --git a/BGSwift/Classes/BGGraph.swift b/BGSwift/Classes/BGGraph.swift new file mode 100644 index 0000000..f9354d5 --- /dev/null +++ b/BGSwift/Classes/BGGraph.swift @@ -0,0 +1,474 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation + +public class BGGraph { + public enum SynchronizationStrategy { + case sync + case async(queue: DispatchQueue? = nil) + } + + private let currentDate: () -> Date + private let behaviorQueue = PriorityQueue() + private var actionQueue = [BGAction]() + private var eventLoopState: EventLoopState? + private var sequence: UInt = 0 + private var deferredRelease = [Any]() + private var sideEffectQueue = [BGSideEffect]() + var updatedTransientResources = [TransientResource]() + + private let mutex = Mutex(recursive: true) + + private let defaultQueue: DispatchQueue + + var behaviorsWithModifiedSupplies = [BGBehavior]() + var behaviorsWithModifiedDemands = [BGBehavior]() + var updatedResources = [BGResource]() + private var untrackedBehaviors = [BGBehavior]() + private var needsOrdering = [BGBehavior]() + + var currentRunningBehavior: BGBehavior? + + public var currentEvent: BGEvent? { + get { eventLoopState?.event } + } + + var processingChanges: Bool { + eventLoopState?.processingChanges ?? false + } + + var processingAction: Bool { + eventLoopState?.processingAction ?? false + } + + private var _lastEvent: BGEvent + public var lastEvent: BGEvent { _lastEvent } + + public init(dateProvider: @escaping () -> Date = { Date() }) { + self._lastEvent = BGEvent.unknownPast + + defaultQueue = DispatchQueue(label: "BGGraph.default", qos: .userInteractive) + + self.currentDate = dateProvider + } + + public func action(file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: SynchronizationStrategy? = nil, body: @escaping (() -> Void)) { + let impulse = BGGraph.impulseString(file: file, line: line, function: function) + action(impulse: impulse, syncStrategy: syncStrategy, body: body) + } + + public func action(impulse: String?, syncStrategy: SynchronizationStrategy? = nil, body: @escaping (() -> Void)) { + switch syncStrategy { + case .sync: + mutex.balancedUnlock { + guard !processingAction else { + assertionFailure("Nested actions cannot be executed synchronously.") + return + } + guard !processingChanges else { + assertionFailure("Actions originating from behavior closures cannot be executed synchronously.") + return + } + + actionQueue.append(BGAction(impulse: impulse, action: body)) + eventLoop() + } + case .async(let queue): + (queue ?? defaultQueue).async { + self.action(impulse: impulse, syncStrategy: .sync, body: body) + } + case .none: + if mutex.tryLock() { + let action = BGAction(impulse: impulse, action: body) + actionQueue.append(action) + + // Run sync when the graph is idle or when called from a side-effect. + if !processingChanges { + eventLoop() + } + + mutex.unlock() + } else { + // Cannot acquire the lock, so dispatch async + action(impulse: impulse, syncStrategy: .async(queue: nil), body: body) + } + } + } + + public func sideEffect(_ label: String? = nil, body: @escaping () -> Void) { + guard let event = currentEvent else { + assertionFailure("Side effects must be created inside actions or behaviors.") + return + } + + let sideEffect = BGSideEffect(label: label, event: event, run: body) + self.sideEffectQueue.append(sideEffect) + } + + private func eventLoop() { + var finished = false + while !finished { + autoreleasepool { + if let eventLoopState = self.eventLoopState { + + if eventLoopState.processingChanges { + + if !untrackedBehaviors.isEmpty { + commitUntrackedBehaviors() + return + } + + if !behaviorsWithModifiedSupplies.isEmpty { + commitModifiedSupplies() + } + + if !behaviorsWithModifiedDemands.isEmpty { + commitModifiedDemands() + } + + if !needsOrdering.isEmpty { + orderBehaviors() + } + + if !updatedResources.isEmpty { + updatedResources.forEach { + for subsequent in $0.subsequents { + submitToQueue(subsequent) + } + } + updatedResources.removeAll() + } + + if !behaviorQueue.isEmpty { + + let behavior = behaviorQueue.pop() + + let currentSequence = eventLoopState.event.sequence + if behavior.removedSequence != currentSequence { + behavior.lastUpdateSequence = currentSequence + + if let extent = behavior.extent { + currentRunningBehavior = behavior + behavior.runBlock(extent) + currentRunningBehavior = nil + } + } + return + } + } + + eventLoopState.processingChanges = false + + if !sideEffectQueue.isEmpty { + executeSideEffect { + while !sideEffectQueue.isEmpty { + let sideEffect = sideEffectQueue.removeFirst() + sideEffect.run() + } + } + return + } + + if !updatedTransientResources.isEmpty { + while !updatedTransientResources.isEmpty { + updatedTransientResources.removeFirst().clearValue() + } + return + } + + if !deferredRelease.isEmpty { + deferredRelease.removeAll() + return + } + + self._lastEvent = eventLoopState.event + + self.eventLoopState = nil + } + + if let action = actionQueue.first { + actionQueue.removeFirst() + + let currentDate = self.currentDate() + sequence += 1 + let event = BGEvent(sequence: sequence, timestamp: currentDate, impulse: action.impulse) + + let eventLoopState = EventLoopState(event: event) + self.eventLoopState = eventLoopState + + action.action() + eventLoopState.processingAction = false + + // NOTE: We keep the action block around because it may capture capture and retain some external objects + // If it were to go away right after running then that might cause a dealloc to be called as it goes out of scope internal + // to the event loop and thus create a side effect during the update phase. + // So we keep it around until after all updates are processed. + deferredRelease.append(action) + + return + } + + finished = true + } + } + } + + private func commitUntrackedBehaviors() { + for behavior in untrackedBehaviors { + if behavior.modifiedSupplies != nil { + behaviorsWithModifiedSupplies.append(behavior) + } + + if behavior.modifiedDemands != nil { + behaviorsWithModifiedDemands.append(behavior) + } + } + untrackedBehaviors.removeAll() + } + + private func commitModifiedSupplies() { + for behavior in behaviorsWithModifiedSupplies { + if let modifiedSupplies = behavior.modifiedSupplies { + let removedSupplies = behavior.supplies.filter { + return !modifiedSupplies.contains($0) + } + let addedSupplies = modifiedSupplies.filter { + guard $0.supplier === behavior || $0.supplier == nil else { + assertionFailure("Resource is already supplied by a different behavior.") + return false + } + return !behavior.supplies.contains($0) + } + + for supply in removedSupplies { + supply.supplier = nil + behavior.supplies.remove(supply) + } + + if !addedSupplies.isEmpty { + for supply in addedSupplies { + supply.supplier = behavior + behavior.supplies.add(supply) + + for subsequent in supply.subsequents { + if subsequent.order <= behavior.order { + needsOrdering.append(subsequent) + } + } + } + } + + behavior.modifiedSupplies = nil + } + } + behaviorsWithModifiedSupplies.removeAll() + } + + private func commitModifiedDemands() { + for behavior in behaviorsWithModifiedDemands { + if let modifiedDemands = behavior.modifiedDemands { + let removedDemands = behavior.demands.filter { + return !modifiedDemands.contains($0) + } + let addedDemands = modifiedDemands.filter { + assert($0.extent?.graph === self, "Demanded resource has not been added to the graph: \($0.debugDescription)") + return !behavior.demands.contains($0) + } + + for demand in removedDemands { + demand.subsequents.remove(behavior) + behavior.demands.remove(demand) + } + + if !addedDemands.isEmpty { + var needsOrdering: Bool = false + var needsRunning: Bool = false + for demand in addedDemands { + demand.subsequents.add(behavior) + behavior.demands.add(demand) + + if demand.justUpdated() { + needsRunning = true + } + + if !needsOrdering, + let prior = demand.supplier, + prior.order >= behavior.order { + needsOrdering = true + } + } + + if needsOrdering { + self.needsOrdering.append(behavior) + } + if needsRunning { + self.submitToQueue(behavior) + } + } + + behavior.modifiedDemands = nil + } + } + behaviorsWithModifiedDemands.removeAll() + } + + private func orderBehaviors() { + var traversalQueue = needsOrdering + needsOrdering.removeAll() + + var needsOrdering = [BGBehavior]() + while !traversalQueue.isEmpty { + let behavior = traversalQueue.removeFirst() + if behavior.orderingState != .unordered { + behavior.orderingState = .unordered + needsOrdering.append(behavior) + + for supply in behavior.supplies { + traversalQueue.append(contentsOf: supply.subsequents) + } + } + } + + var needsReheap = false + needsOrdering.forEach { + sortDFS(behavior: $0, needsReheap: &needsReheap) + } + + if needsReheap { + behaviorQueue.setNeedsReheap() + } + } + + private func sortDFS(behavior: BGBehavior, needsReheap: inout Bool) { + guard behavior.orderingState != .ordering else { + // assert or fail? + assert(behavior.orderingState != .ordering, "Dependency cycle detected") + return + } + + if behavior.orderingState == .unordered { + behavior.orderingState = .ordering + + var order = UInt(1) + for demand in behavior.demands { + if let prior = demand.supplier { + if prior.orderingState != .ordered { + sortDFS(behavior: prior, needsReheap: &needsReheap) + } + order = max(order, prior.order + 1) + } + } + + behavior.orderingState = .ordered + if order != behavior.order { + behavior.order = order + needsReheap = true + } + } + } + + func submitToQueue(_ behavior: BGBehavior) { + // @SAL 8/26/2019-- I'm not sure how either of these would trigger, it seems they are both a result of a broken + // algorithm, not a misconfigured graph + // jlou 2/5/19 - These asserts are checking for graph implementation bugs, not for user error. + assert(eventLoopState?.processingChanges ?? false, "Should not be activating behaviors in current phase.") + assert(behavior.lastUpdateSequence != sequence, "Behavior already ran.") + + if behavior.enqueuedSequence < sequence { + behavior.enqueuedSequence = sequence + + behaviorQueue.push(behavior) + } + } + + func addExtent(_ extent: BGExtent) { + guard let eventLoopState = self.eventLoopState, eventLoopState.processingChanges else { + assertionFailure("Extents must be added during an event.") + return + } + guard extent._added.eventDirectAccess == BGEvent.unknownPast else { + assertionFailure("Extent can only be added once.") + return + } + + extent._added.update() + untrackedBehaviors.append(contentsOf: extent.behaviors) + } + + func updateDemands(behavior: BGBehavior) { + guard let eventLoopState = self.eventLoopState, eventLoopState.processingChanges else { + assertionFailure("Can only update demands during an event.") + return + } + behaviorsWithModifiedDemands.append(behavior) + } + + func updateSupplies(behavior: BGBehavior) { + guard let eventLoopState = self.eventLoopState, eventLoopState.processingChanges else { + assertionFailure("Can only update supplies during an event.") + return + } + behaviorsWithModifiedSupplies.append(behavior) + } + + func removeBehavior(_ behavior: BGBehavior) { + guard let eventLoopState = eventLoopState, eventLoopState.processingChanges else { + assertionFailure("Can only remove behaviors during an event.") + return + } + + for supply in behavior.supplies { + supply.supplier = nil + } + + for demand in behavior.demands { + demand.subsequents.remove(behavior) + } + behavior.demands.removeAll() + + behavior.removedSequence = eventLoopState.event.sequence + } + + func executeSideEffect(_ work: () -> Void) { + if Thread.current.isMainThread { + work() + } else { + mutex.unlock() + DispatchQueue.main.sync { + mutex.lock() + work() + mutex.unlock() + } + mutex.lock() + } + } + + static func impulseString(file: String, line: Int, function: String) -> String { + "\(function)@\(file):\(line)" + } +} + +fileprivate class EventLoopState { + let event: BGEvent + var processingAction: Bool = true + var processingChanges: Bool = true + init(event: BGEvent) { + self.event = event + } +} + +#if DEBUG +var assertionFailureImpl: ((@autoclosure () -> String, StaticString, UInt) -> Void)? = nil + +func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) { + if !condition() { + (assertionFailureImpl ?? Swift.assertionFailure)(message(), file, line) + } +} + +func assertionFailure(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) { + (assertionFailureImpl ?? Swift.assertionFailure)(message(), file, line) +} +#endif diff --git a/BGSwift/Classes/BGResource.swift b/BGSwift/Classes/BGResource.swift new file mode 100644 index 0000000..ab2e518 --- /dev/null +++ b/BGSwift/Classes/BGResource.swift @@ -0,0 +1,309 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation + +protocol TransientResource { + func clearValue() +} + +public protocol BGOptionalObject { + associatedtype Wrapped: AnyObject + var bg_unwrapped: Wrapped? { get } +} + +extension Optional: BGOptionalObject where Wrapped: AnyObject { + public var bg_unwrapped: Wrapped? { + return self + } +} + +public class BGResource { + public enum ComparisonNone { case none } + public enum ComparisonEqual { case equal } + public enum ComparisonIdentical { case identical } + + var subsequents = WeakSet() + weak var supplier: BGBehavior? + weak var extent: BGExtent? + var graph: BGGraph? { + get { extent?.graph } + } + + private var _event = BGEvent.unknownPast + public internal(set) var event: BGEvent { + get { + verifyDemands() + return _event + } + + set { _event = newValue } + } + + var eventDirectAccess: BGEvent { _event } + + var previousEvent = BGEvent.unknownPast + + public var traceEvent: BGEvent { + return _event.sequence == graph?.currentEvent?.sequence ? previousEvent : _event + } + + var propertyName: String? + + public func justUpdated() -> Bool { + guard let currentEvent = graph?.currentEvent else { + return false + } + + return currentEvent.sequence == event.sequence + } + + init(name: String? = nil) { + self.propertyName = name; + } + + public func hasUpdated() -> Bool { + return event.sequence > BGEvent.unknownPast.sequence + } + + var canUpdate: Bool { + guard let graph = self.graph else { + // If graph is nil, then weak extent has been deallocated and resource updates are no-ops. + return false + } + + guard let currentEvent = graph.currentEvent else { + assertionFailure("Can only update a resource during an event.") + return false + } + + guard let extent = extent else { + assertionFailure("Cannot update a resource that does not belong to an extent.") + return false + } + + if let behavior = supplier { + + guard graph.currentRunningBehavior === behavior else { + assertionFailure("Can only supplied resource during its supplying behavior's run.") + return false + } + } else { + if self !== extent._added { + guard graph.processingAction else { + assertionFailure("Can only update unsupplied resource during an action.") + return false + } + } + } + + guard eventDirectAccess.sequence < currentEvent.sequence else { + // assert or fail? + assertionFailure() + return false + } + + return true + } + + static var assertUndeclaredDemands: Bool { + ProcessInfo.processInfo.arguments.contains("-BGGraphVerifyDemands") + } + + func verifyDemands() { + // TODO: compile out checks with build flag + + guard BGResource.assertUndeclaredDemands else { + return + } + + if let currentBehavior = graph?.currentRunningBehavior, currentBehavior !== supplier { + assert(currentBehavior.demands.contains(self), "Accessed a resource in a behavior that was not declared as a demand.") + } + } +} + +extension BGResource: CustomDebugStringConvertible { + public var debugDescription: String { + return "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(propertyName ?? "Unlabeled"))>" + } +} + +extension BGResource: Hashable { + public static func == (lhs: BGResource, rhs: BGResource) -> Bool { + return lhs === rhs + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + +public class BGTypedResource: BGResource { + private var _value: Type + public internal(set) var value: Type { + get { + // TODO: compile out this check with build flag + if BGResource.assertUndeclaredDemands { + verifyDemands() + } + return _value + } + + set { _value = newValue } + } + + var valueDirectAccess: Type { _value } + + init(_ value: Type, name: String? = nil) { + _value = value + super.init(name: name) + } +} + +public class BGMoment: BGResource { + + public func update() { + guard canUpdate else { + return + } + + previousEvent = eventDirectAccess; + event = graph!.currentEvent! + + graph!.updatedResources.append(self) + } + + public func updateWithAction(file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { + graph?.action(file: file, line: line, function: function, syncStrategy: syncStrategy) { + self.update() + } + } + + public func updateWithAction(impulse: String, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { + graph?.action(impulse: impulse, syncStrategy: syncStrategy) { + self.update() + } + } + +} + +public class BGTypedMoment: BGTypedResource, TransientResource { + public typealias ReadableValueType = Type? + + init(name: String? = nil) { + super.init(nil) + } + + public func update(_ newValue: Type, withAction: Bool = false) { + guard canUpdate else { + return + } + + previousEvent = eventDirectAccess; + + value = newValue + event = graph!.currentEvent! + + graph!.updatedResources.append(self) + graph!.updatedTransientResources.append(self) + } + + public func updateWithAction(_ newValue: Type, file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { + graph?.action(file: file, line: line, function: function, syncStrategy: syncStrategy) { + self.update(newValue) + } + } + + public func updateWithAction(_ newValue: Type, impulse: String, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { + graph?.action(impulse: impulse, syncStrategy: syncStrategy) { + self.update(newValue) + } + } + + func clearValue() { + value = nil + } + + public var updatedValue: ReadableValueType { value } +} + +public class BGState: BGTypedResource { + public typealias ReadableValueType = Type + + private var comparison: ((Type, Type) -> Bool)? + private var previousValue: Type + + public var traceValue: Type { + get { eventDirectAccess.sequence == graph?.currentEvent?.sequence ? previousValue : valueDirectAccess } + } + + init(_ value: Type, name: String? = nil, comparison: ((Type, Type) -> Bool)?) { + self.comparison = comparison + self.previousValue = value + super.init(value, name: name) + } +} + +extension BGState { + func commitUpdate(_ newValue: Type) { + guard let graph = self.graph, let currentEvent = graph.currentEvent else { + return + } + + previousValue = valueDirectAccess; + previousEvent = eventDirectAccess; + + value = newValue + event = currentEvent + + graph.updatedResources.append(self) + } + + func valueEquals(_ other: Type) -> Bool { + if let comparison = comparison { + return comparison(valueDirectAccess, other) + } else { + return false + } + } + + public func update(_ newValue: Type) { + guard canUpdate else { return } + + if !valueEquals(newValue) { + commitUpdate(newValue) + } + } + + public func updateWithAction(_ newValue: Type, file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { + graph?.action(file: file, line: line, function: function, syncStrategy: syncStrategy) { + self.update(newValue) + } + } + + public func updateWithAction(_ newValue: Type, impulse: String, syncStrategy: BGGraph.SynchronizationStrategy? = nil) { + graph?.action(impulse: impulse, syncStrategy: syncStrategy) { + self.update(newValue) + } + } + + public func justUpdated(to: Type) -> Bool { + return justUpdated() && valueEquals(to) + } + + public func justUpdated(from: Type) -> Bool { + guard justUpdated(), let comparison = comparison else { + return false + } + return comparison(traceValue, from) + } + + public func justUpdated(to: Type, from: Type) -> Bool { + guard justUpdated(), let comparison = comparison else { + return false + } + return comparison(valueDirectAccess, to) && comparison(traceValue, from) + } +} diff --git a/BGSwift/Classes/BGSideEffect.swift b/BGSwift/Classes/BGSideEffect.swift new file mode 100644 index 0000000..82d0d6f --- /dev/null +++ b/BGSwift/Classes/BGSideEffect.swift @@ -0,0 +1,17 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation + +struct BGSideEffect { + let label: String? + let event: BGEvent + let run: () -> Void + + init(label: String? = nil, event: BGEvent, run: @escaping () -> Void) { + self.label = label + self.event = event + self.run = run + } +} diff --git a/BGSwift/Classes/Mutex.swift b/BGSwift/Classes/Mutex.swift new file mode 100644 index 0000000..443f2ee --- /dev/null +++ b/BGSwift/Classes/Mutex.swift @@ -0,0 +1,74 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation + +class Mutex { + let mutex: UnsafeMutablePointer + + init(recursive: Bool = true) { + let attributes = UnsafeMutablePointer.allocate(capacity: 1) + attributes.initialize(to: pthread_mutexattr_t()) + + guard pthread_mutexattr_init(attributes) == 0 else { + preconditionFailure() + } + + defer { + pthread_mutexattr_destroy(attributes) + attributes.deallocate() + } + + pthread_mutexattr_settype(attributes, recursive ? PTHREAD_MUTEX_RECURSIVE : PTHREAD_MUTEX_NORMAL) + + mutex = .allocate(capacity: 1) + mutex.initialize(to: pthread_mutex_t()) + + guard pthread_mutex_init(mutex, attributes) == 0 else { + preconditionFailure() + } + } + + deinit { + pthread_mutex_destroy(mutex) + mutex.deallocate() + } + + // jlou 2/18/21 - Avoid concurrent execution by surrounding code with mutex lock/unlock. I believe Swift + // compiler should be able to inline the non-escaping closure and avoid heap allocation for captured variables + // but I will need to test this. If not, we can pass variables into the closure with a generic input argument. + + @inline(__always) + func balancedUnlock(_ code: () throws -> Void) rethrows { + lock() + defer { + unlock() + } + try code() + } + + @inline(__always) + func balancedUnlock(_ code: () throws -> T) rethrows -> T { + lock() + defer { + unlock() + } + return try code() + } + + @inline(__always) + func lock() { + pthread_mutex_lock(mutex) + } + + @inline(__always) + func tryLock() -> Bool { + return pthread_mutex_trylock(mutex) == 0 + } + + @inline(__always) + func unlock() { + pthread_mutex_unlock(mutex) + } +} diff --git a/BGSwift/Classes/PriorityQueue.swift b/BGSwift/Classes/PriorityQueue.swift new file mode 100644 index 0000000..a7cd198 --- /dev/null +++ b/BGSwift/Classes/PriorityQueue.swift @@ -0,0 +1,69 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation + +class PriorityQueue { + private let heap: CFBinaryHeap + private var unheapedElements = [BGBehavior]() + + init() { + var callbacks = CFBinaryHeapCallBacks(version: 0, retain: nil, release: nil, copyDescription: nil) { ptr1, ptr2, context in + + let lhs = Unmanaged.fromOpaque(ptr1!).takeUnretainedValue().order + let rhs = Unmanaged.fromOpaque(ptr2!).takeUnretainedValue().order + + return ( + lhs < rhs ? .compareLessThan : + lhs > rhs ? .compareGreaterThan : + .compareEqualTo + ) + } + + heap = CFBinaryHeapCreate(kCFAllocatorDefault, 0, &callbacks, nil) + } + + var count: Int { + unheapedElements.count + CFBinaryHeapGetCount(heap) + } + + var isEmpty: Bool { + unheapedElements.isEmpty && CFBinaryHeapGetCount(heap) == 0 + } + + func setNeedsReheap() { + for _ in 0 ..< CFBinaryHeapGetCount(heap) { + let value = popHeap() + unheapedElements.append(value) + } + } + + private func reheapIfNeeded() { + unheapedElements.forEach { element in + let ptr = Unmanaged.passRetained(element).toOpaque() + CFBinaryHeapAddValue(heap, ptr) + } + unheapedElements.removeAll() + } + + func push(_ value: BGBehavior) { + unheapedElements.append(value) + } + + func pop() -> BGBehavior { + reheapIfNeeded() + + guard count > 0 else { + preconditionFailure() + } + + return popHeap() + } + + private func popHeap() -> BGBehavior { + let value = Unmanaged.fromOpaque(CFBinaryHeapGetMinimum(heap)).takeRetainedValue() + CFBinaryHeapRemoveMinimumValue(heap) + return value + } +} diff --git a/BGSwift/Classes/WeakSet.swift b/BGSwift/Classes/WeakSet.swift new file mode 100644 index 0000000..8011e89 --- /dev/null +++ b/BGSwift/Classes/WeakSet.swift @@ -0,0 +1,44 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation + +class WeakSetIterator { + private let enumerator: NSEnumerator + + init(_ enumerator: NSEnumerator) { + self.enumerator = enumerator + } +} + +extension WeakSetIterator: IteratorProtocol { + typealias Element = Value + + func next() -> Value? { enumerator.nextObject() as! Value? } +} + +class WeakSet { + private let hashTable = NSHashTable.weakObjects() + + var count: Int { hashTable.allObjects.count } + func add(_ value: Value) { hashTable.add(value) } + func remove(_ value: Value) { hashTable.remove(value) } + func removeAll() { hashTable.removeAllObjects() } +} + +extension WeakSet: Sequence { + typealias Iterator = WeakSetIterator + + func makeIterator() -> WeakSetIterator { + WeakSetIterator(hashTable.objectEnumerator()) + } + + func contains(_ element: WeakSet.Element) -> Bool { hashTable.contains(element) } +} + +extension WeakSet: CustomDebugStringConvertible { + var debugDescription: String { + hashTable.allObjects.debugDescription + } +} diff --git a/BGSwift/Classes/WeakWrapper.swift b/BGSwift/Classes/WeakWrapper.swift new file mode 100644 index 0000000..e5a3aab --- /dev/null +++ b/BGSwift/Classes/WeakWrapper.swift @@ -0,0 +1,27 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation + +struct WeakWrapper { + private let objectIdentifier: ObjectIdentifier + weak var unwrapped: Value? + init(_ value: Value) { + unwrapped = value + objectIdentifier = ObjectIdentifier(value) + } +} + +extension WeakWrapper: Hashable { + + func hash(into hasher: inout Hasher) { + hasher.combine(objectIdentifier) + } +} + +extension WeakWrapper: Equatable { + static func == (lhs: WeakWrapper, rhs: WeakWrapper) -> Bool { + return lhs.unwrapped === rhs.unwrapped + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b255bbf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# How to contribute +First, thanks for taking the time to contribute to our project! There are many ways you can help out. + +### Questions + +If you have a question that needs an answer, create an issue, and label it as a question. + +### Issues for bugs or feature requests + +If you encounter any bugs in the code, or want to request a new feature or enhancement, please create an issue to report it. Kindly add a label to indicate what type of issue it is. + +### Contribute Code +We welcome your pull requests for bug fixes. To implement something new, please create an issue first so we can discuss it together. + +***Creating a Pull Request*** +Please follow [best practices](https://github.com/trein/dev-best-practices/wiki/Git-Commit-Best-Practices) for creating git commits. In addition: + +- Make sure your code respects existing formatting conventions. In general, follow + the same coding style as the code that you are modifying. +- Bugfixes must include a unit test or integration test reproducing the issue. +- Try to keep pull requests short and submit separate ones for unrelated + features, but feel free to combine simple bugfixes/tests into one pull request. +- Keep the number of commits small and combine commits for related changes. +- Keep formatting changes in separate commits to make code reviews easier and + distinguish them from actual code changes. + +When your code is ready to be submitted, submit a pull request to begin the code review process. + +We only seek to accept code that you are authorized to contribute to the project. We have added a pull request template on our projects so that your contributions are made with the following confirmation: + +> I confirm that this contribution is made under the terms of the license found in the root directory of this repository's source tree and that I have the authority necessary to make this contribution on behalf of its copyright owner. + +## Code of Conduct + +We encourage inclusive and professional interactions on our project. We welcome everyone to open an issue, improve the documentation, report bug or submit a pull request. By participating in this project, you agree to abide by the [Code of Conduct](Code-Of-Conduct.md). If you feel there is a conduct issue related to this project, please raise it per the Code of Conduct process and we will address it. + diff --git a/Code_of_Conduct.md b/Code_of_Conduct.md index 0061f8e..61e3eed 100644 --- a/Code_of_Conduct.md +++ b/Code_of_Conduct.md @@ -1,9 +1,9 @@ -# Verizon Media Open Source Code of Conduct +# Yahoo Inc Open Source Code of Conduct ## Summary This Code of Conduct is our way to encourage good behavior and discourage bad behavior in our open source projects. We invite participation from many people to bring different perspectives to our projects. We will do our part to foster a welcoming and professional environment free of harassment. We expect participants to communicate professionally and thoughtfully during their involvement with this project. -Participants may lose their good standing by engaging in misconduct. For example: insulting, threatening, or conveying unwelcome sexual content. We ask participants who observe conduct issues to report the incident directly to the project's Response Team at opensource-conduct@verizonmedia.com. Verizon Media will assign a respondent to address the issue. We may remove harassers from this project. +Participants may lose their good standing by engaging in misconduct. For example: insulting, threatening, or conveying unwelcome sexual content. We ask participants who observe conduct issues to report the incident directly to the project's Response Team at opensource-conduct@yahooinc.com. Yahoo will assign a respondent to address the issue. We may remove harassers from this project. This code does not replace the terms of service or acceptable use policies of the websites used to support this project. We acknowledge that participants may be subject to additional conduct terms based on their employment which may govern their online expressions. @@ -35,16 +35,16 @@ Participants remain in good standing when they do not engage in misconduct or ha * **Don't disrupt.** Sustained disruptions in a discussion. ### Reporting Issues -If you experience or witness misconduct, or have any other concerns about the conduct of members of this project, please report it by contacting our Response Team at opensource-conduct@verizonmedia.com who will handle your report with discretion. Your report should include: +If you experience or witness misconduct, or have any other concerns about the conduct of members of this project, please report it by contacting our Response Team at opensource-conduct@yahooinc.com who will handle your report with discretion. Your report should include: * Your preferred contact information. We cannot process anonymous reports. * Names (real or usernames) of those involved in the incident. * Your account of what occurred, and if the incident is ongoing. Please provide links to or transcripts of the publicly available records (e.g. a mailing list archive or a public IRC logger), so that we can review it. * Any additional information that may be helpful to achieve resolution. -After filing a report, a representative will contact you directly to review the incident and ask additional questions. If a member of the Verizon Media Response Team is named in an incident report, that member will be recused from handling your incident. If the complaint originates from a member of the Response Team, it will be addressed by a different member of the Response Team. We will consider reports to be confidential for the purpose of protecting victims of abuse. +After filing a report, a representative will contact you directly to review the incident and ask additional questions. If a member of the Yahoo Response Team is named in an incident report, that member will be recused from handling your incident. If the complaint originates from a member of the Response Team, it will be addressed by a different member of the Response Team. We will consider reports to be confidential for the purpose of protecting victims of abuse. ### Scope -Verizon Media will assign a Response Team member with admin rights on the project and legal rights on the project copyright. The Response Team is empowered to restrict some privileges to the project as needed. Since this project is governed by an open source license, any participant may fork the code under the terms of the project license. The Response Team’s goal is to preserve the project if possible, and will restrict or remove participation from those who disrupt the project. +Yahoo will assign a Response Team member with admin rights on the project and legal rights on the project copyright. The Response Team is empowered to restrict some privileges to the project as needed. Since this project is governed by an open source license, any participant may fork the code under the terms of the project license. The Response Team’s goal is to preserve the project if possible, and will restrict or remove participation from those who disrupt the project. This code does not replace the terms of service or acceptable use policies that are provided by the websites used to support this community. Nor does this code apply to communications or actions that take place outside of the context of this community. Many participants in this project are also subject to codes of conduct based on their employment. This code is a social-contract that informs participants of our social expectations. It is not a terms of service or legal contract. diff --git a/Example/BGSwift.xcodeproj/project.pbxproj b/Example/BGSwift.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3023fcd --- /dev/null +++ b/Example/BGSwift.xcodeproj/project.pbxproj @@ -0,0 +1,714 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 5A0BB7A3270517E40043E632 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7A2270517E40043E632 /* AppDelegate.swift */; }; + 5A0BB7A7270517E40043E632 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7A6270517E40043E632 /* ViewController.swift */; }; + 5A0BB7AC270517E50043E632 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5A0BB7AB270517E50043E632 /* Assets.xcassets */; }; + 5A0BB7AF270517E50043E632 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5A0BB7AD270517E50043E632 /* LaunchScreen.storyboard */; }; + 5A0BB7D3270518E30043E632 /* LoginExtent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7D2270518E30043E632 /* LoginExtent.swift */; }; + 5A0BB7F527051DAB0043E632 /* ImpulseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7EE27051DAB0043E632 /* ImpulseTests.swift */; }; + 5A0BB7F627051DAB0043E632 /* DependenciesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7EF27051DAB0043E632 /* DependenciesTests.swift */; }; + 5A0BB7F727051DAB0043E632 /* DynamicsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7F027051DAB0043E632 /* DynamicsTests.swift */; }; + 5A0BB7F827051DAB0043E632 /* AssertionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7F127051DAB0043E632 /* AssertionTests.swift */; }; + 5A0BB7F927051DAB0043E632 /* TestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7F227051DAB0043E632 /* TestSupport.swift */; }; + 5A0BB7FA27051DAB0043E632 /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7F327051DAB0043E632 /* ConcurrencyTests.swift */; }; + 5A0BB7FB27051DAB0043E632 /* BGStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0BB7F427051DAB0043E632 /* BGStateTests.swift */; }; + 5A0BB7FF27051FA10043E632 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5A0BB7FC27051F6C0043E632 /* Main.storyboard */; }; + 733FCDF527067E28009CF264 /* BGMomentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733FCDF427067E28009CF264 /* BGMomentTests.swift */; }; + 739FF91A270F98F4000DACD1 /* BGExtentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739FF919270F98F4000DACD1 /* BGExtentTests.swift */; }; + 739FF91C270F9D00000DACD1 /* BGGraphTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739FF91B270F9D00000DACD1 /* BGGraphTests.swift */; }; + 739FF91F270FAF6A000DACD1 /* EventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739FF91E270FAF6A000DACD1 /* EventTests.swift */; }; + 9D92A68929EE0FE7B105AD22 /* Pods_BGSwift_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A12E8DD9E2CF2B6B45E7835 /* Pods_BGSwift_Example.framework */; }; + ED1C6795BC032A3B5AE714DF /* Pods_BGSwift_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A2F667ABE5A14147F7D89E8 /* Pods_BGSwift_Tests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 5A0BB7B6270517E60043E632 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5A0BB797270517E40043E632 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5A0BB79E270517E40043E632; + remoteInfo = BGSwift; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 08CD0F42D31C66D6249278B2 /* Pods-BGSwift-Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BGSwift-Tests.debug.xcconfig"; path = "Target Support Files/Pods-BGSwift-Tests/Pods-BGSwift-Tests.debug.xcconfig"; sourceTree = ""; }; + 201C0CF2C814828DFCD7F7A3 /* Pods-BGSwift-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BGSwift-Example.release.xcconfig"; path = "Target Support Files/Pods-BGSwift-Example/Pods-BGSwift-Example.release.xcconfig"; sourceTree = ""; }; + 544081C857CAAA5A85BA8AD0 /* Pods-BGSwift-Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BGSwift-Tests.release.xcconfig"; path = "Target Support Files/Pods-BGSwift-Tests/Pods-BGSwift-Tests.release.xcconfig"; sourceTree = ""; }; + 5A0BB79F270517E40043E632 /* BGSwift-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BGSwift-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5A0BB7A2270517E40043E632 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 5A0BB7A6270517E40043E632 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 5A0BB7AB270517E50043E632 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5A0BB7AE270517E50043E632 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 5A0BB7B0270517E50043E632 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5A0BB7B5270517E60043E632 /* BGSwift-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BGSwift-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5A0BB7D2270518E30043E632 /* LoginExtent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginExtent.swift; sourceTree = ""; }; + 5A0BB7EE27051DAB0043E632 /* ImpulseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImpulseTests.swift; sourceTree = ""; }; + 5A0BB7EF27051DAB0043E632 /* DependenciesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DependenciesTests.swift; sourceTree = ""; }; + 5A0BB7F027051DAB0043E632 /* DynamicsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicsTests.swift; sourceTree = ""; }; + 5A0BB7F127051DAB0043E632 /* AssertionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssertionTests.swift; sourceTree = ""; }; + 5A0BB7F227051DAB0043E632 /* TestSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSupport.swift; sourceTree = ""; }; + 5A0BB7F327051DAB0043E632 /* ConcurrencyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = ""; }; + 5A0BB7F427051DAB0043E632 /* BGStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BGStateTests.swift; sourceTree = ""; }; + 5A0BB7FD27051F6C0043E632 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 5AC349BA2705258600289AD2 /* BGBehavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BGBehavior.swift; sourceTree = ""; }; + 5AC349C9270526C200289AD2 /* BGEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGEvent.swift; sourceTree = ""; }; + 5AC349CB270526E000289AD2 /* BGExtent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGExtent.swift; sourceTree = ""; }; + 5AC349CD270526ED00289AD2 /* BGGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGGraph.swift; sourceTree = ""; }; + 5AC349CF270526F600289AD2 /* BGResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGResource.swift; sourceTree = ""; }; + 5AC349D12705270500289AD2 /* BGSideEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGSideEffect.swift; sourceTree = ""; }; + 5AC349D32705271000289AD2 /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; + 5AC349D52705271A00289AD2 /* PriorityQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriorityQueue.swift; sourceTree = ""; }; + 5AC349D72705272200289AD2 /* WeakSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakSet.swift; sourceTree = ""; }; + 5AC349D92705272A00289AD2 /* WeakWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakWrapper.swift; sourceTree = ""; }; + 5AC34A3F270533EF00289AD2 /* BGAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGAction.swift; sourceTree = ""; }; + 733FCDF427067E28009CF264 /* BGMomentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGMomentTests.swift; sourceTree = ""; }; + 739FF919270F98F4000DACD1 /* BGExtentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGExtentTests.swift; sourceTree = ""; }; + 739FF91B270F9D00000DACD1 /* BGGraphTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGGraphTests.swift; sourceTree = ""; }; + 739FF91E270FAF6A000DACD1 /* EventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTests.swift; sourceTree = ""; }; + 8A12E8DD9E2CF2B6B45E7835 /* Pods_BGSwift_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BGSwift_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8A2F667ABE5A14147F7D89E8 /* Pods_BGSwift_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BGSwift_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DBFB91C16E7BCE9F06B08EE1 /* Pods-BGSwift-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BGSwift-Example.debug.xcconfig"; path = "Target Support Files/Pods-BGSwift-Example/Pods-BGSwift-Example.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5A0BB79C270517E40043E632 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9D92A68929EE0FE7B105AD22 /* Pods_BGSwift_Example.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5A0BB7B2270517E60043E632 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ED1C6795BC032A3B5AE714DF /* Pods_BGSwift_Tests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 5A0BB796270517E40043E632 = { + isa = PBXGroup; + children = ( + 5AC349BC2705259F00289AD2 /* BGSwift */, + 5A0BB7A1270517E40043E632 /* Example */, + 5A0BB7B8270517E60043E632 /* Tests */, + 5A0BB7A0270517E40043E632 /* Products */, + 85A1E74D8ADCA4330AF98A21 /* Pods */, + FAC4FA9887D933384B90B993 /* Frameworks */, + ); + sourceTree = ""; + }; + 5A0BB7A0270517E40043E632 /* Products */ = { + isa = PBXGroup; + children = ( + 5A0BB79F270517E40043E632 /* BGSwift-Example.app */, + 5A0BB7B5270517E60043E632 /* BGSwift-Tests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 5A0BB7A1270517E40043E632 /* Example */ = { + isa = PBXGroup; + children = ( + 5A0BB7A2270517E40043E632 /* AppDelegate.swift */, + 5A0BB7A6270517E40043E632 /* ViewController.swift */, + 5A0BB7D2270518E30043E632 /* LoginExtent.swift */, + 5A0BB7AB270517E50043E632 /* Assets.xcassets */, + 5A0BB7FC27051F6C0043E632 /* Main.storyboard */, + 5A0BB7AD270517E50043E632 /* LaunchScreen.storyboard */, + 5A0BB7B0270517E50043E632 /* Info.plist */, + ); + name = Example; + path = BGSwift; + sourceTree = ""; + }; + 5A0BB7B8270517E60043E632 /* Tests */ = { + isa = PBXGroup; + children = ( + 739FF91B270F9D00000DACD1 /* BGGraphTests.swift */, + 5A0BB7F127051DAB0043E632 /* AssertionTests.swift */, + 5A0BB7F427051DAB0043E632 /* BGStateTests.swift */, + 733FCDF427067E28009CF264 /* BGMomentTests.swift */, + 5A0BB7F327051DAB0043E632 /* ConcurrencyTests.swift */, + 5A0BB7EF27051DAB0043E632 /* DependenciesTests.swift */, + 5A0BB7F027051DAB0043E632 /* DynamicsTests.swift */, + 5A0BB7EE27051DAB0043E632 /* ImpulseTests.swift */, + 739FF91E270FAF6A000DACD1 /* EventTests.swift */, + 739FF919270F98F4000DACD1 /* BGExtentTests.swift */, + 5A0BB7F227051DAB0043E632 /* TestSupport.swift */, + ); + path = Tests; + sourceTree = ""; + }; + 5AC349BC2705259F00289AD2 /* BGSwift */ = { + isa = PBXGroup; + children = ( + 5AC349BD270525C800289AD2 /* Classes */, + ); + name = BGSwift; + path = ../BGSwift; + sourceTree = ""; + }; + 5AC349BD270525C800289AD2 /* Classes */ = { + isa = PBXGroup; + children = ( + 5AC34A3F270533EF00289AD2 /* BGAction.swift */, + 5AC349BA2705258600289AD2 /* BGBehavior.swift */, + 5AC349C9270526C200289AD2 /* BGEvent.swift */, + 5AC349CB270526E000289AD2 /* BGExtent.swift */, + 5AC349CD270526ED00289AD2 /* BGGraph.swift */, + 5AC349CF270526F600289AD2 /* BGResource.swift */, + 5AC349D12705270500289AD2 /* BGSideEffect.swift */, + 5AC349D32705271000289AD2 /* Mutex.swift */, + 5AC349D52705271A00289AD2 /* PriorityQueue.swift */, + 5AC349D72705272200289AD2 /* WeakSet.swift */, + 5AC349D92705272A00289AD2 /* WeakWrapper.swift */, + ); + path = Classes; + sourceTree = ""; + }; + 85A1E74D8ADCA4330AF98A21 /* Pods */ = { + isa = PBXGroup; + children = ( + DBFB91C16E7BCE9F06B08EE1 /* Pods-BGSwift-Example.debug.xcconfig */, + 201C0CF2C814828DFCD7F7A3 /* Pods-BGSwift-Example.release.xcconfig */, + 08CD0F42D31C66D6249278B2 /* Pods-BGSwift-Tests.debug.xcconfig */, + 544081C857CAAA5A85BA8AD0 /* Pods-BGSwift-Tests.release.xcconfig */, + ); + name = Pods; + path = ../Pods; + sourceTree = ""; + }; + FAC4FA9887D933384B90B993 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8A12E8DD9E2CF2B6B45E7835 /* Pods_BGSwift_Example.framework */, + 8A2F667ABE5A14147F7D89E8 /* Pods_BGSwift_Tests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5A0BB79E270517E40043E632 /* BGSwift-Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5A0BB7C9270517E60043E632 /* Build configuration list for PBXNativeTarget "BGSwift-Example" */; + buildPhases = ( + 335D8D431130F92E70CF0CC9 /* [CP] Check Pods Manifest.lock */, + 5A0BB79B270517E40043E632 /* Sources */, + 5A0BB79C270517E40043E632 /* Frameworks */, + 5A0BB79D270517E40043E632 /* Resources */, + 8159968D97A1938F66B0E028 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "BGSwift-Example"; + productName = BGSwift; + productReference = 5A0BB79F270517E40043E632 /* BGSwift-Example.app */; + productType = "com.apple.product-type.application"; + }; + 5A0BB7B4270517E60043E632 /* BGSwift-Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5A0BB7CC270517E60043E632 /* Build configuration list for PBXNativeTarget "BGSwift-Tests" */; + buildPhases = ( + A28FA4B5C12DFC11D20326F4 /* [CP] Check Pods Manifest.lock */, + 5A0BB7B1270517E60043E632 /* Sources */, + 5A0BB7B2270517E60043E632 /* Frameworks */, + 5A0BB7B3270517E60043E632 /* Resources */, + 2AC6629A7FAC952167371F79 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 5A0BB7B7270517E60043E632 /* PBXTargetDependency */, + ); + name = "BGSwift-Tests"; + productName = BGSwiftTests; + productReference = 5A0BB7B5270517E60043E632 /* BGSwift-Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5A0BB797270517E40043E632 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1300; + LastUpgradeCheck = 1300; + TargetAttributes = { + 5A0BB79E270517E40043E632 = { + CreatedOnToolsVersion = 13.0; + }; + 5A0BB7B4270517E60043E632 = { + CreatedOnToolsVersion = 13.0; + LastSwiftMigration = 1300; + TestTargetID = 5A0BB79E270517E40043E632; + }; + }; + }; + buildConfigurationList = 5A0BB79A270517E40043E632 /* Build configuration list for PBXProject "BGSwift" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5A0BB796270517E40043E632; + productRefGroup = 5A0BB7A0270517E40043E632 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5A0BB79E270517E40043E632 /* BGSwift-Example */, + 5A0BB7B4270517E60043E632 /* BGSwift-Tests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5A0BB79D270517E40043E632 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5A0BB7FF27051FA10043E632 /* Main.storyboard in Resources */, + 5A0BB7AF270517E50043E632 /* LaunchScreen.storyboard in Resources */, + 5A0BB7AC270517E50043E632 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5A0BB7B3270517E60043E632 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2AC6629A7FAC952167371F79 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-BGSwift-Tests/Pods-BGSwift-Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-BGSwift-Tests/Pods-BGSwift-Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BGSwift-Tests/Pods-BGSwift-Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 335D8D431130F92E70CF0CC9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-BGSwift-Example-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8159968D97A1938F66B0E028 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-BGSwift-Example/Pods-BGSwift-Example-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-BGSwift-Example/Pods-BGSwift-Example-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BGSwift-Example/Pods-BGSwift-Example-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A28FA4B5C12DFC11D20326F4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-BGSwift-Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5A0BB79B270517E40043E632 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5A0BB7A7270517E40043E632 /* ViewController.swift in Sources */, + 5A0BB7D3270518E30043E632 /* LoginExtent.swift in Sources */, + 5A0BB7A3270517E40043E632 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5A0BB7B1270517E60043E632 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 739FF91A270F98F4000DACD1 /* BGExtentTests.swift in Sources */, + 5A0BB7F727051DAB0043E632 /* DynamicsTests.swift in Sources */, + 5A0BB7F627051DAB0043E632 /* DependenciesTests.swift in Sources */, + 739FF91F270FAF6A000DACD1 /* EventTests.swift in Sources */, + 733FCDF527067E28009CF264 /* BGMomentTests.swift in Sources */, + 5A0BB7F827051DAB0043E632 /* AssertionTests.swift in Sources */, + 739FF91C270F9D00000DACD1 /* BGGraphTests.swift in Sources */, + 5A0BB7FB27051DAB0043E632 /* BGStateTests.swift in Sources */, + 5A0BB7FA27051DAB0043E632 /* ConcurrencyTests.swift in Sources */, + 5A0BB7F927051DAB0043E632 /* TestSupport.swift in Sources */, + 5A0BB7F527051DAB0043E632 /* ImpulseTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 5A0BB7B7270517E60043E632 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5A0BB79E270517E40043E632 /* BGSwift-Example */; + targetProxy = 5A0BB7B6270517E60043E632 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 5A0BB7AD270517E50043E632 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5A0BB7AE270517E50043E632 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 5A0BB7FC27051F6C0043E632 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5A0BB7FD27051F6C0043E632 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 5A0BB7C7270517E60043E632 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 5A0BB7C8270517E60043E632 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5A0BB7CA270517E60043E632 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DBFB91C16E7BCE9F06B08EE1 /* Pods-BGSwift-Example.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2ARNJY38FW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = BGSwift/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.yahoo.demo.BGSwift-Example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Debug; + }; + 5A0BB7CB270517E60043E632 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 201C0CF2C814828DFCD7F7A3 /* Pods-BGSwift-Example.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2ARNJY38FW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = BGSwift/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.yahoo.demo.BGSwift-Example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Release; + }; + 5A0BB7CD270517E60043E632 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 08CD0F42D31C66D6249278B2 /* Pods-BGSwift-Tests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2ARNJY38FW; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yahoo.BGSwiftTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BGSwift-Example.app/BGSwift-Example"; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Debug; + }; + 5A0BB7CE270517E60043E632 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 544081C857CAAA5A85BA8AD0 /* Pods-BGSwift-Tests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2ARNJY38FW; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yahoo.BGSwiftTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BGSwift-Example.app/BGSwift-Example"; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5A0BB79A270517E40043E632 /* Build configuration list for PBXProject "BGSwift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5A0BB7C7270517E60043E632 /* Debug */, + 5A0BB7C8270517E60043E632 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5A0BB7C9270517E60043E632 /* Build configuration list for PBXNativeTarget "BGSwift-Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5A0BB7CA270517E60043E632 /* Debug */, + 5A0BB7CB270517E60043E632 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5A0BB7CC270517E60043E632 /* Build configuration list for PBXNativeTarget "BGSwift-Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5A0BB7CD270517E60043E632 /* Debug */, + 5A0BB7CE270517E60043E632 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5A0BB797270517E40043E632 /* Project object */; +} diff --git a/Example/BGSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/BGSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Example/BGSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/BGSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/BGSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/BGSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/BGSwift.xcodeproj/xcshareddata/xcschemes/BGSwift-Example.xcscheme b/Example/BGSwift.xcodeproj/xcshareddata/xcschemes/BGSwift-Example.xcscheme new file mode 100644 index 0000000..506f835 --- /dev/null +++ b/Example/BGSwift.xcodeproj/xcshareddata/xcschemes/BGSwift-Example.xcscheme @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/BGSwift/AppDelegate.swift b/Example/BGSwift/AppDelegate.swift new file mode 100644 index 0000000..41ac987 --- /dev/null +++ b/Example/BGSwift/AppDelegate.swift @@ -0,0 +1,43 @@ +// +// Copyright © 2021 Yahoo +// + + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/Example/BGSwift/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/BGSwift/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Example/BGSwift/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/BGSwift/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/BGSwift/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Example/BGSwift/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/BGSwift/Assets.xcassets/Contents.json b/Example/BGSwift/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/BGSwift/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/BGSwift/Base.lproj/LaunchScreen.storyboard b/Example/BGSwift/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Example/BGSwift/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/BGSwift/Base.lproj/Main.storyboard b/Example/BGSwift/Base.lproj/Main.storyboard new file mode 100644 index 0000000..88ddced --- /dev/null +++ b/Example/BGSwift/Base.lproj/Main.storyboard @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/BGSwift/Info.plist b/Example/BGSwift/Info.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Example/BGSwift/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Example/BGSwift/LoginExtent.swift b/Example/BGSwift/LoginExtent.swift new file mode 100644 index 0000000..9956489 --- /dev/null +++ b/Example/BGSwift/LoginExtent.swift @@ -0,0 +1,114 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation +import BGSwift + +class LoginExtent: BGExtent { + let email: BGState + let password: BGState + let loginClick: BGMoment + let emailValid: BGState + let passwordValid: BGState + let loginEnabled: BGState + let loggingIn: BGState + let loginComplete: BGTypedMoment + + weak var loginForm: ViewController? + var savedLoginBlock: ((Bool) -> Void)? + + init(graph: BGGraph) { + let bld: BGExtentBuilder = BGExtentBuilder(graph: graph) + email = bld.state("") + password = bld.state("") + loginClick = bld.moment() + emailValid = bld.state(false) + passwordValid = bld.state(false) + loginEnabled = bld.state(false) + loggingIn = bld.state(false) + loginComplete = bld.typedMoment() + + + bld.behavior(supplies: [emailValid], demands: [email, bld.added]) { extent in + extent.emailValid.update(LoginExtent.validEmailAddress(extent.email.value)) + extent.sideEffect { + extent.loginForm?.emailFeedback.text = extent.emailValid.value ? "✅" : "❌" + } + } + + + bld.behavior(supplies: [passwordValid], demands: [password, bld.added]) { extent in + extent.passwordValid.update(extent.password.value.count > 0) + extent.sideEffect { + extent.loginForm?.passwordFeedback.text = extent.passwordValid.value ? "✅" : "❌" + } + } + + + bld.behavior(supplies: [loginEnabled], demands: [emailValid, passwordValid, loggingIn, bld.added]) { extent in + let enabled = extent.emailValid.value && extent.passwordValid.value && !extent.loggingIn.value; + extent.loginEnabled.update(enabled) + extent.sideEffect { + extent.loginForm?.loginButton.isEnabled = extent.loginEnabled.value + } + } + + + bld.behavior(supplies: [loggingIn], demands: [loginClick, loginComplete, bld.added]) { extent in + if extent.loginClick.justUpdated() && extent.loginEnabled.traceValue { + extent.loggingIn.update(true) + } else if extent.loginComplete.justUpdated() && extent.loggingIn.value { + extent.loggingIn.update(false) + } + + if extent.loggingIn.justUpdated(to: true) { + extent.sideEffect { + extent.loginCall(email: extent.email.value, password: extent.password.value) { success in + + extent.graph?.action { + extent.loginComplete.update(success) + } + + } + } + } + } + + + bld.behavior(supplies: [], demands: [loggingIn, loginComplete, bld.added]) { extent in + extent.sideEffect { + var status = "" + if extent.loggingIn.value { + status = "Logging in..."; + } else { + if extent.loginComplete.justUpdated() && extent.loginComplete.value! { + status = "Login Success" + } else if extent.loginComplete.justUpdated() && !extent.loginComplete.value! { + status = "Login Failed" + } + } + extent.loginForm?.loginStatus.text = status; + } + } + + super.init(builder: bld) + + } + + static func validEmailAddress(_ email: String) -> Bool { + let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let pred = NSPredicate(format: "SELF matches %@", regex) + return pred.evaluate(with: email) + } + + func loginCall(email: String, password: String, complete: @escaping (Bool) -> Void) { + self.savedLoginBlock = complete + } + + func completeLogin(success: Bool) { + self.savedLoginBlock?(success) + self.savedLoginBlock = nil + } + +} diff --git a/Example/BGSwift/ViewController.swift b/Example/BGSwift/ViewController.swift new file mode 100644 index 0000000..fa98edb --- /dev/null +++ b/Example/BGSwift/ViewController.swift @@ -0,0 +1,63 @@ +// +// Copyright © 2021 Yahoo +// + +import UIKit +import BGSwift + +class ViewController: UIViewController { + + @IBOutlet var emailField: UITextField! + @IBOutlet var passwordField: UITextField! + @IBOutlet var loginButton: UIButton! + @IBOutlet var emailFeedback: UILabel! + @IBOutlet var passwordFeedback: UILabel! + @IBOutlet var loginStatus: UILabel! + @IBOutlet var loginSuccess: UIButton! + @IBOutlet var loginFail: UIButton! + + let graph: BGGraph + let loginExtent: LoginExtent + + required init?(coder: NSCoder) { + graph = BGGraph() + loginExtent = LoginExtent(graph: graph) + super.init(coder: coder) + loginExtent.loginForm = self + } + + override func viewDidLoad() { + super.viewDidLoad() + loginExtent.addToGraphWithAction() + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + } + + @IBAction func didUpdateEmailField(sender: UITextField) { + graph.action { + self.loginExtent.email.update(self.emailField.text ?? "") + } + } + + @IBAction func didUpdatePasswordField(sender: UITextField) { + graph.action { + self.loginExtent.password.update(self.passwordField.text ?? "") + } + } + + @IBAction func loginButtonClicked(sender: UIButton) { + graph.action { + self.loginExtent.loginClick.update() + } + } + + @IBAction func loginSucceeded(sender: UIButton) { + loginExtent.completeLogin(success: true) + } + + @IBAction func loginFailed(sender: UIButton) { + loginExtent.completeLogin(success: false) + } +} diff --git a/Example/Tests/AssertionTests.swift b/Example/Tests/AssertionTests.swift new file mode 100644 index 0000000..459177c --- /dev/null +++ b/Example/Tests/AssertionTests.swift @@ -0,0 +1,54 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation +import XCTest +import BGSwift + +class AssertionTests: XCTestCase { + + func testAssertsUndeclaredDemandWithSupplier() { + var assertionHit = false + + let g = BGGraph() + + let b = BGExtentBuilder(graph: g) + let r = b.moment() + + b.behavior(supplies: [r]) { _ in + // do nothing + } + + b.behavior(demands: [b.added]) { extent in + assertionHit = CheckAssertionHit { + _ = r.justUpdated() + } + } + + let e = BGExtent(builder: b) + e.addToGraphWithAction() + + XCTAssertTrue(assertionHit) + } + + func testAssertsUndeclaredDemandWithNoSupplier() { + var assertionHit = false + + let g = BGGraph() + + let b = BGExtentBuilder(graph: g) + let r = b.moment() + + b.behavior(demands: [b.added]) { extent in + assertionHit = CheckAssertionHit { + _ = r.justUpdated() + } + } + + let e = BGExtent(builder: b) + e.addToGraphWithAction() + + XCTAssertTrue(assertionHit) + } +} diff --git a/Example/Tests/BGExtentTests.swift b/Example/Tests/BGExtentTests.swift new file mode 100644 index 0000000..cb786e1 --- /dev/null +++ b/Example/Tests/BGExtentTests.swift @@ -0,0 +1,93 @@ +// +// Copyright © 2021 Yahoo +// + +import XCTest +@testable import BGSwift + +class BGExtentTests: XCTestCase { + + var g: BGGraph! + var b: BGExtentBuilder! + var e: BGExtent! + var rA: BGState! + var rB: BGState! + var rC: BGState! + + + override func setUpWithError() throws { + g = BGGraph() + b = BGExtentBuilder(graph: g) + rA = b.state(0) + rB = b.state(0) + rC = b.state(0) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testAddedResourceIsUpdatedOnAdding() { + // |> Given an extent + var run = false + var nonAddedRun = false + b.behavior(supplies: [], demands: [b.added]) { extent in + run = true + } + b.behavior(supplies: [], demands: []) { extent in + nonAddedRun = true + } + e = BGExtent(builder: b) + + // |> When it is added + e.addToGraphWithAction() + + // |> The added resource is updated + XCTAssertTrue(run) + XCTAssertFalse(nonAddedRun) + } + + func testCheckCannotAddExtentToGraphMultipleTimes() { + // NOTE: This is primarily to prevent user error. + // Perhaps it makes sense to remove/add extents in the future. + + // |> Given an extent added to a graph + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When it is added again + // |> Then there is an error + TestAssertionHit { + e.addToGraphWithAction() + } + } + + func testCheckExtentCanOnlyBeAddedDuringEvent() { + // |> Given an extent + e = BGExtent(builder: b) + + // |> When added outside an event + // |> Then there is an error + TestAssertionHit { + e.addToGraph() + } + } + + + class MyExtent: BGExtent { + let r1: BGMoment + + init(graph: BGGraph) { + let b = BGExtentBuilder(graph: graph) + r1 = b.moment() + super.init(builder: b) + } + } + + func testResourcePropertiesGetNames() { + let e2 = MyExtent(graph: self.g) + + XCTAssertEqual(e2.r1.propertyName, "MyExtent.r1") + } + +} diff --git a/Example/Tests/BGGraphTests.swift b/Example/Tests/BGGraphTests.swift new file mode 100644 index 0000000..9bc6bdd --- /dev/null +++ b/Example/Tests/BGGraphTests.swift @@ -0,0 +1,99 @@ +// +// Copyright © 2021 Yahoo +// + +import XCTest +import BGSwift + +class BGGraphTests: XCTestCase { + + var g: BGGraph! + var b: BGExtentBuilder! + var e: BGExtent! + var rA: BGState! + var rB: BGState! + var rC: BGState! + + override func setUpWithError() throws { + g = BGGraph() + b = BGExtentBuilder(graph: g) + rA = b.state(0) + rB = b.state(0) + rC = b.state(0) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testDependencyCyclesCaught() { + // |> Given a graph with dependency cycles + b.behavior(supplies: [rA], demands: [rB]) { extent in + // nothing + } + b.behavior(supplies: [rB], demands: [rA]) { extent in + // nothing + } + e = BGExtent(builder: b) + + // |> When it is added to the graph + // |> Then it will raise an error + TestAssertionHit { + e.addToGraphWithAction() + } + } + + func testResourceCanOnlyBeSuppliedByOneBehavior() { + // |> Given an extent with multiple behaviors that supply the same resource + b.behavior(supplies: [rA], demands: [], body:{ extent in + // nothing + }) + b.behavior(supplies: [rA], demands: []) { extent in + // nothing + } + e = BGExtent(builder: b) + + // |> When it is added + // |> Then it will raise an error + TestAssertionHit { + e.addToGraphWithAction() + } + } + + func testCannotAddDemandNotPartOfGraph() { + // |> Given a extent with a behavior that demands a resource not added to the graph + let b2 = BGExtentBuilder(graph: g) + b2.behavior(supplies: [], demands: [rA]) { extent in + // nothing + } + let e2 = BGExtent(builder: b2) + + // |> When it is added to the graph + // |> Then there should be an error + TestAssertionHit { + e2.addToGraphWithAction() + } + } + + func testLinksCanOnlyBeUpdatedDuringAnEvent() { + // |> Given a behavior in a graph + let bhv = b.behavior(supplies: [], demands: []) { extent in + // nothing + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When updating demands outside of event + // |> Then there is an error + TestAssertionHit { + bhv.setDemands([rA]) + } + + // |> And when updating supplies outside of event + // |> Then there is an error + TestAssertionHit { + bhv.setSupplies([rB]) + } + } + +} diff --git a/Example/Tests/BGMomentTests.swift b/Example/Tests/BGMomentTests.swift new file mode 100644 index 0000000..9c54f60 --- /dev/null +++ b/Example/Tests/BGMomentTests.swift @@ -0,0 +1,212 @@ +// +// Copyright © 2021 Yahoo +// + +import XCTest +import BGSwift + +class BGMomentTests: XCTestCase { + + var g: BGGraph! + var bld: BGExtentBuilder! + var ext: BGExtent! + + override func setUpWithError() throws { + g = BGGraph() + bld = BGExtentBuilder(graph: g) + } + + override func tearDownWithError() throws { + } + + func testMomentUpdates() throws { + // |> Given a moment in the graph + let mr1 = bld.moment() + var afterUpdate = false; + bld.behavior(demands:[mr1]) { extent in + if mr1.justUpdated() { + afterUpdate = true + } + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When it is read in the graph (and was not updated) + var beforeUpdate = false + var updateEvent: BGEvent? = nil + g.action { + beforeUpdate = mr1.justUpdated() + mr1.update() + updateEvent = self.g.currentEvent + } + + // |> Then it didn't justUpdate before updating + XCTAssertFalse(beforeUpdate) + + // |> And after + // |> Then it did + XCTAssertTrue(afterUpdate) + + // |> And outside an event + // |> It is not just Updated + XCTAssertFalse(mr1.justUpdated()) + + // |> And event stays the same from when it last happened + XCTAssertEqual(mr1.event, updateEvent) + } + + func testTypedMomentsHaveInformation() { + // Given a moment with data + let mr1: BGTypedMoment = bld.typedMoment() + var afterUpdate: Int? = nil + bld.behavior(supplies: [], demands: [mr1]) { extent in + if mr1.justUpdated() { + afterUpdate = mr1.value + } + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When it happens + mr1.updateWithAction(1) + + // |> Then the data is visible in subsequent behaviors + XCTAssertEqual(afterUpdate, 1) + } + + func testTypedMomentsAreTransient() { + // NOTE this default prevents retaining data that no longer is needed + + // |> Given a moment with data + let mr1: BGTypedMoment = bld.typedMoment() + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When current event is over + mr1.updateWithAction(1) + + // |> Then value nils out + XCTAssertNil(mr1.value) + } + + func testNonSuppliedMomentsCanUpdateBeforeAdding() { + // |> Given a moment + let mr1 = bld.moment() + var didRun = false; + bld.behavior(supplies: [], demands: [mr1]) { extent in + if mr1.justUpdated() { + didRun = true + } + } + ext = BGExtent(builder: bld) + + // |> When it is updated in the same event as adding to graph + g.action { + mr1.update() + self.ext.addToGraph() + } + + // |> Then it runs and demanding behavior is run + XCTAssertTrue(didRun) + } + + func testCheckUpdatingMomentNotInGraphIsANullOp() { + // NOTE: Extent can be deallocated and removed from graph while + // some pending resource update may exist. Doing so should just + // do nothing. + + // |> Given a moment resource not part of the graph + let mr1 = bld.moment() + + // |> When it is updated + var errorHappened = false + g.action { + errorHappened = CheckAssertionHit { + mr1.update() + } + } + + // |> Then nothing happens + XCTAssertFalse(errorHappened) + XCTAssertEqual(mr1.event, BGEvent.unknownPast) + } + + func testCheckMomentUpdatesOnlyHappenDuringEvent() { + // |> Given a moment in the graph + let mr1 = bld.moment() + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When updating outside of an event + // |> Then it should fail + TestAssertionHit { + mr1.update() + } + } + + func testCheckMomentOnlyUpdatedBySupplier() { + // |> Given a supplied moment + let mr1 = bld.moment() + let mr2 = bld.moment() + bld.behavior(supplies: [mr2], demands: [mr1]) { extent in + } + bld.behavior(supplies:[], demands: [mr1]) { extent in + if mr1.justUpdated() { + mr2.update() + } + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When it is updated by the wrong behavior + // |> Then it should throw + TestAssertionHit { + self.g.action { + mr1.update() + } + } + } + + func testCheckUnsuppliedMomentOnlyUpdatedInAction() { + // |> Given a supplied moment and unsupplied moment + let mr1 = bld.moment() + let mr2 = bld.moment() + var updateFailed = false + bld.behavior(supplies: [], demands: [mr1]) { extent in + updateFailed = CheckAssertionHit { + mr2.update() + } + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When the unsupplied moment is updated by a behavior + g.action { + mr1.update() + } + + // |> Then it should throw + XCTAssertTrue(updateFailed) + } + + func testCheckSuppliedMomentCannotBeUpdatedInAction() { + // |> Given a supplied moment + let mr1 = bld.moment() + bld.behavior(supplies: [mr1], demands: []) { extent in + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When we try updating that moment in an action + var updateFailed = false + self.g.action { + updateFailed = CheckAssertionHit { + mr1.update() + } + } + + // |> Then the updating chould throw + XCTAssertTrue(updateFailed) + } + +} diff --git a/Example/Tests/BGStateTests.swift b/Example/Tests/BGStateTests.swift new file mode 100644 index 0000000..c083fed --- /dev/null +++ b/Example/Tests/BGStateTests.swift @@ -0,0 +1,593 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation +import Quick +import Nimble +@testable import BGSwift + +class BGStateTests : QuickSpec { + override func spec() { + describe("BGState") { + var g: BGGraph! + var r_a, r_b: BGState! + var bld: BGExtentBuilder! + var ext: BGExtent! + + beforeEach { + g = BGGraph() + bld = BGExtentBuilder(graph: g) + r_a = bld.state(0) + r_b = bld.state(0) + } + + it("has initial state") { + // |> When we create a new state resource + // |> It has an initial value + expect(r_a.value) == 0 + } + + context("added to graph") { + beforeEach { + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + } + + it("updates") { + // |> When it is updated + r_a.updateWithAction(2) + + // |> Then it has new value and event + expect(r_a.value) == 2 + expect(r_a.event) == g.lastEvent + } + + } + + it("can handle null/nil values") { + // Motivation: nullable states are useful for modeling false/true with data + + // |> Given a nullable state + let r_n: BGState = bld.state(nil) + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When updated + r_n.updateWithAction(1) + + // |> Then it will have that new state + expect(r_n.value) == 1 + + // |> And when updated to nil + r_n.updateWithAction(nil) + + // |> Then it will have nil state + expect(r_n.value).to(beNil()) + } + + it("works as demand and supply") { + // |> Given state resources and behaviors + var ran = false; + bld.behavior(supplies: [r_b], demands: [r_a]) { extent in + r_b.update(r_a.value) + } + + bld.behavior(supplies:[], demands:[r_b]) { extent in + ran = true + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When event is started + r_a.updateWithAction(1) + + // |> Then subsequent behaviors are run + expect(r_b.value) == 1 + expect(ran) == true + } + + it("justUpdated checks work during event") { + var updatedA = false + var updatedB = false + var updatedToA = false + var updatedToWrongToA = false + var updatedToB = false + var updatedToFromA = false + var updatedToFromWrongToA = false + var updatedToFromWrongFromA = false + var updatedToFromB = false + var updatedFromA = false + var updatedFromWrongFromA = false + var updatedFromB = false + + // |> Given a behavior that tracks updated methods + bld.behavior(supplies:[], demands:[r_a, r_b]) { extent in + updatedA = r_a.justUpdated() + updatedB = r_b.justUpdated() + updatedToA = r_a.justUpdated(to: 1) + updatedToWrongToA = r_a.justUpdated(to: 2) + updatedToB = r_b.justUpdated(to: 1) + updatedToFromA = r_a.justUpdated(to: 1, from: 0) + updatedToFromWrongToA = r_a.justUpdated(to: 2, from: 0) + updatedToFromWrongFromA = r_a.justUpdated(to: 1, from: 2) + updatedToFromB = r_b.justUpdated(to: 1, from: 0) + updatedFromA = r_a.justUpdated(from: 0) + updatedFromWrongFromA = r_a.justUpdated(from: 2) + updatedFromB = r_b.justUpdated(from: 0) + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When r_a updates + r_a.updateWithAction(1) + + // |> Then updates are tracked inside behavior + expect(updatedA) == true + expect(r_a.justUpdated()) == false // false outside event + expect(updatedB) == false // not updated + expect(updatedToA) == true + expect(r_a.justUpdated(to: 1)) == false + expect(updatedToWrongToA) == false + expect(updatedToB) == false + expect(updatedToFromA) == true + expect(r_a.justUpdated(to: 1, from: 0)) == false + expect(updatedToFromWrongToA) == false + expect(updatedToFromWrongFromA) == false + expect(updatedToFromB) == false + expect(updatedFromA) == true + expect(r_a.justUpdated(from: 0)) == false + expect(updatedFromWrongFromA) == false + expect(updatedFromB) == false + } + + it("can access trace values/events") { + var beforeValue: Int? = nil + var beforeEvent: BGEvent? = nil + + // |> Given a behavior that accesses trace + bld.behavior(supplies:[], demands:[r_a]) { extent in + beforeValue = r_a.traceValue + beforeEvent = r_a.traceEvent + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When resource is updated + r_a.updateWithAction(1) + + // |> Trace captures original state during + expect(beforeValue) == 0 + expect(beforeEvent) == BGEvent.unknownPast + // and current state outside event + expect(r_a.traceValue) == 1 + expect(r_a.traceEvent) == g.lastEvent + } + + xit("internal state is transient after event") { + // need to test that state is cleared internally + // how to access private? + } + + it("can update resource during same event as adding") { + // @SAL this doesn't work yet, can't add and update in the same event + var didRun = false + bld.behavior(supplies:[], demands:[r_a]) { extent in + didRun = true + } + ext = BGExtent(builder: bld) + + g.action { + r_a.update(1) + ext.addToGraph() + } + + expect(r_a.value) == 1 + expect(didRun) == true + } + + describe("checks") { + + // @SAL we don't have a way of catching asserts yet + // so these are disabled + xit("part of graph before updating") { + expect { + r_a.updateWithAction(1) + }.to(raiseException()) + } + + // @SAL-- 10/1/2021 basic canUpdate checks are in the moment tests + // probably could move those to a BGResource tests class + it("ensures canUpdate checks are run") { + // NOTE: there are multiple canUpdate checks, this just ensures that + // code path is followed + bld.behavior(supplies: [], demands: [r_a]) { extent in + r_b.update(r_a.value) + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + TestAssertionHit { + r_a.updateWithAction(1) + } + } + } + } + } + +} + +fileprivate struct Struct {} +fileprivate class Object {} + +fileprivate class EquatableObject: Equatable { + var failEquality = false + + static func == (lhs: EquatableObject, rhs: EquatableObject) -> Bool { + !lhs.failEquality && !rhs.failEquality + } +} + +class BGStateUpdateTests: XCTestCase { + + let g = BGGraph() + var b: BGExtentBuilder! + + override func setUp() { + b = BGExtentBuilder(graph: g) + } + + override func tearDown() { + assertionFailureImpl = nil + } + + // MARK: Non-Equatable, Non-Object + + func testNonEquatableStruct_default() { + // default is .none + + let s = b.state(Struct()) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertTrue(s.justUpdated()) + } + } + + func testNonEquatableStruct_noCheck() { + let s = b.state(Struct(), comparison: .none) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertTrue(s.justUpdated()) + } + } + + // MARK: Equatable, Non-Object + + func testEquatable_default() { + // default is .equal + + let s = b.state("foo") + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update("foo") + XCTAssertFalse(s.justUpdated()) + } + + g.action { + s.update("bar") + XCTAssertTrue(s.justUpdated()) + } + } + + func testEquatable_noCheck() { + let s = b.state("foo", comparison: .none) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertTrue(s.justUpdated()) + } + } + + func testEquatable_equals() { + let s = b.state("foo", comparison: .equal) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + +// s.valueEquality("foo", equalityCheck: .equal) + s.update("foo") + XCTAssertFalse(s.justUpdated()) + } + + g.action { + s.update("bar") + XCTAssertTrue(s.justUpdated()) + } + } + + // MARK: Non-Equatable, Object + + func testObject_default() { + // default is .identical + + let s = b.state(Object()) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertFalse(s.justUpdated()) + } + + g.action { + s.update(Object()) + XCTAssertTrue(s.justUpdated()) + } + } + + func testObject_noCheck() { + let s = b.state(Object(), comparison: .none) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertTrue(s.justUpdated()) + } + } + + func testObject_indentical() { + let s = b.state(Object(), comparison: .identical) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertFalse(s.justUpdated()) + } + + g.action { + s.update(Object()) + XCTAssertTrue(s.justUpdated()) + } + } + + // MARK: Equatable, Object + + func testEquatableObject_default() { + // default is .equal + + let obj = EquatableObject() + + let s = b.state(obj) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(obj) + XCTAssertFalse(s.justUpdated()) + } + + obj.failEquality = true + g.action { + s.update(obj) + XCTAssertTrue(s.justUpdated()) + } + } + + func testEquatableObject_noCheck() { + let s = b.state(EquatableObject(), comparison: .none) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertTrue(s.justUpdated()) + } + } + + func testEquatableObject_equal() { + let obj = EquatableObject() + + let s = b.state(obj, comparison: .equal) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(obj) + XCTAssertFalse(s.justUpdated()) + } + + obj.failEquality = true + g.action { + s.update(obj) + XCTAssertTrue(s.justUpdated()) + } + } + + func testEquatableObject_indentical() { + let s = b.state(EquatableObject(), comparison: .identical) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertFalse(s.justUpdated()) + } + + g.action { + s.update(EquatableObject()) + XCTAssertTrue(s.justUpdated()) + } + } + + // MARK: Optional, Non-Equatable, Object + + func testOptionalObject_default() { + // default is .identical + + let s: BGState = b.state(Object()) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertFalse(s.justUpdated()) + } + + g.action { + s.update(Object()) + XCTAssertTrue(s.justUpdated()) + } + } + + func testOptionalObject_noCheck() { + let s: BGState = b.state(Object(), comparison: .none) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertTrue(s.justUpdated()) + } + } + + func testOptionalObject_indentical() { + let s: BGState = b.state(Object(), comparison: .identical) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertFalse(s.justUpdated()) + } + + g.action { + s.update(Object()) + XCTAssertTrue(s.justUpdated()) + } + } + + // MARK: Optional Equatable, Object + + func testOptionalEquatableObject_default() { + let obj = EquatableObject() + + let s: BGState = b.state(EquatableObject()) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(obj) + XCTAssertFalse(s.justUpdated()) + } + + obj.failEquality = true + + g.action { + s.update(obj) + XCTAssertTrue(s.justUpdated()) + } + } + + func testOptionalEquatableObject_noCheck() { + let s: BGState = b.state(EquatableObject(), comparison: .none) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(s.value) + XCTAssertTrue(s.justUpdated()) + } + } + + func testOptionalEquatableObject_equal() { + let obj = EquatableObject() + + let s: BGState = b.state(EquatableObject(), comparison: .equal) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(obj) + XCTAssertFalse(s.justUpdated()) + } + + obj.failEquality = true + + g.action { + s.update(obj) + XCTAssertTrue(s.justUpdated()) + } + } + + func testOptionalEquatableObject_indentical() { + let obj1 = EquatableObject() + obj1.failEquality = false + + let obj2 = EquatableObject() + obj2.failEquality = false + + let s: BGState = b.state(obj1, comparison: .identical) + + let extent = BGExtent(builder: b) + + g.action { + extent.addToGraph() + + s.update(obj1) + XCTAssertFalse(s.justUpdated()) + } + + g.action { + s.update(obj2) + XCTAssertTrue(s.justUpdated()) + } + } +} diff --git a/Example/Tests/ConcurrencyTests.swift b/Example/Tests/ConcurrencyTests.swift new file mode 100644 index 0000000..9d877d2 --- /dev/null +++ b/Example/Tests/ConcurrencyTests.swift @@ -0,0 +1,310 @@ +// +// Copyright © 2021 Yahoo +// + +import XCTest +@testable import BGSwift + +class ConcurrencyTests: XCTestCase { + + let SYNC_EXPECTATION_TIMEOUT = TimeInterval(0) + let ASYNC_EXPECTATION_TIMEOUT = TimeInterval(0.1) + + func runSyncOnMain(_ code: () throws -> Void) rethrows { + if Thread.isMainThread { + try code() + } else { + try DispatchQueue.main.sync(execute: code) + } + } + + func testSyncActionRunInSideEffectRunsSynchronously() { + let exp = XCTestExpectation() + + let g = BGGraph() + + DispatchQueue.global().async { + XCTAssertFalse(Thread.isMainThread) + + g.action(syncStrategy: .sync) { + g.sideEffect { + var actionRan = false + g.action(syncStrategy: .sync) { + actionRan = true + } + if actionRan { + exp.fulfill() + } + } + } + } + + wait(for: [exp], timeout: ASYNC_EXPECTATION_TIMEOUT) + } + + func testSyncStrategyUnspecifiedActionRunInSideEffectRunsSynchronously() { + let exp = XCTestExpectation() + + let g = BGGraph() + + DispatchQueue.global().async { + XCTAssertFalse(Thread.isMainThread) + + g.action(syncStrategy: .sync) { + g.sideEffect { + var actionRan = false + g.action(syncStrategy: nil) { + actionRan = true + } + if actionRan { + exp.fulfill() + } + } + } + } + + wait(for: [exp], timeout: ASYNC_EXPECTATION_TIMEOUT) + } + + func testSyncActionRunWhenOtherActionInProgressWaitsAndRunsSynchronously() { + let innerActionRan = XCTestExpectation() + + let g = BGGraph() + + let sleepInterval = TimeInterval(0.1) + + var waitedForInnerAction: Bool? = nil + + // Begin execution onto a background thread so that both actions' threads are equal priortity + DispatchQueue.global().async { + g.action(syncStrategy: .sync) { + // Begin second action on another thread + DispatchQueue.global().async { + var actionRan = false + + // Note that it is possible that the outer action completes before the inner + // action attempts to run. In that case, the test will succeed even + // if there was never any contention for the graph's mutex. Hopefully this + // won't be the case if we choose a sufficient sleep interval. + g.action(syncStrategy: .sync) { + actionRan = true + innerActionRan.fulfill() + } + + waitedForInnerAction = actionRan + } + + // Wait to give the other action's thread a chance to run + Thread.sleep(forTimeInterval: sleepInterval) + } + } + + wait(for: [innerActionRan], timeout: ASYNC_EXPECTATION_TIMEOUT + sleepInterval) + XCTAssertEqual(waitedForInnerAction, true) + } + + func testSyncStrategyUnspecifiedActionRunWhenOtherActionInProgressReturnsSynchronouslyAndSchedulesAsyncAction() { + let innerActionRan = XCTestExpectation() + + let g = BGGraph() + + var waitedForInnerAction: Bool? = nil + + // Begin execution onto a background thread so that both actions' threads are equal priortity + DispatchQueue.global().async { + g.action(syncStrategy: .sync) { + let semaphore = DispatchSemaphore(value: 0) + + // Begin second action on another thread + DispatchQueue.global().async { + var actionRan = false + g.action(syncStrategy: nil) { + actionRan = true + innerActionRan.fulfill() + } + waitedForInnerAction = actionRan + + semaphore.signal() + } + + // Wait until other action is scheduled + semaphore.wait() + } + } + + wait(for: [innerActionRan], timeout: ASYNC_EXPECTATION_TIMEOUT) + XCTAssertEqual(waitedForInnerAction, false) + } + + func testSyncStrategyUnspecifiedActionCalledInAnotherActionIsRunAsyncOnSameDispatchQueueLoop() { + let g = BGGraph() + + var actionOrder = [String]() + g.action(impulse: "outer", syncStrategy: .sync) { + g.action(impulse: "inner", syncStrategy: nil) { + actionOrder.append(g.currentEvent!.impulse!) + } + actionOrder.append(g.currentEvent!.impulse!) + } + XCTAssertEqual(actionOrder, ["outer", "inner"]) + } + + func testSyncStrategyUnspecifiedActionCalledInBehaviorIsRunAsyncOnSameDispatchQueueLoop() { + let g = BGGraph() + + var execOrder = [String]() + + let b = BGExtentBuilder(graph: g) + b.behavior(supplies: [], demands: [b.added]) { extent in + g.action(impulse: "inner") { + execOrder.append(g.currentEvent!.impulse!) + } + execOrder.append(g.currentEvent!.impulse!) + } + + let e = BGExtent(builder: b) + g.action(impulse: "outer") { + e.addToGraph() + } + XCTAssertEqual(execOrder, ["outer", "inner"]) + } + + func testUpdatingResourceOfDeallocedExtentIsANoOp() { + let g = BGGraph() + + let asyncActionFinished = XCTestExpectation() + + autoreleasepool { + let b = BGExtentBuilder(graph: g) + let r = b.state(false) + let e = BGExtent(builder: b) + + g.action { + e.addToGraph() + } + + g.action(syncStrategy: .async(queue: DispatchQueue.global(qos: .background))) { [weak e] in + r.update(true) + + XCTAssertNil(e) + XCTAssertFalse(r.value) + asyncActionFinished.fulfill() + } + + // e deallocs before the async action is executed + } + + wait(for: [asyncActionFinished], timeout: ASYNC_EXPECTATION_TIMEOUT) + } + + func testSyncActionOnMainThreadWhileBackgroundActionWithSideEffectIsRunningCompletesSynchronously() { + let g = BGGraph() + + var execOrder = [String]() + + let mutex = Mutex(recursive: false) + func appendExecOrder(_ str: String) { + mutex.balancedUnlock { + execOrder.append(str) + } + } + + let workTime: TimeInterval = 1 + + let backgroundActionRunning = XCTestExpectation() + let backgroundActionCompleted = XCTestExpectation() + + g.action(syncStrategy: .async(queue: DispatchQueue.global(qos: .background))) { + appendExecOrder("background-action-start") + backgroundActionRunning.fulfill() + + // do work + Thread.sleep(forTimeInterval: workTime) + appendExecOrder("background-work-complete") + + g.sideEffect { + appendExecOrder("se-background") + } + + backgroundActionCompleted.fulfill() + } + + wait(for: [backgroundActionRunning], timeout: ASYNC_EXPECTATION_TIMEOUT) + appendExecOrder("creating-main-action") + g.action(syncStrategy: .sync) { + appendExecOrder("main-action-start") + g.sideEffect { + appendExecOrder("se-main") + } + } + XCTAssertEqual(execOrder, ["background-action-start", "creating-main-action", "background-work-complete", "se-background", "main-action-start", "se-main"]) + } + + func testSyncNestedActionsDisallowed() { + let g = BGGraph() + TestAssertionHit { + g.action { + g.action(syncStrategy:.sync) { + } + } + } + } + + func testSyncActionsInBehaviorsDisallowed() { + // NOTE: an action inside a behavior can essentially be considered a side effect by default + // as long as we can delay that action until after the current event which is impossible + // with a sync actions + + // |> Given a behavior in the graph that creates an action outside of a side effect + let g = BGGraph() + let b = BGExtentBuilder(graph: g) + let r1 = b.moment() + b.behavior(supplies: [], demands: [r1]) { extent in + g.action(syncStrategy:.sync) { + // do something + } + // cant run because I may have code here that expected it to run + } + let e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When that behavior runs + // |> Then it should throw an error + TestAssertionHit { + r1.updateWithAction() + } + } + + func testSyncActionsInSideEffectsAreAllowed() { + // NOTE: This is currently allowed because its where one would expect actions to be created + // however, technically the synchronous action could cut ahead of some pending side effects from + // the existing event. So maybe that could be disallowed and forced into optional asynchrony or fixed? + + // |> Given a behavior in the graph that creates an action outside of a side effect + let g = BGGraph() + let b = BGExtentBuilder(graph: g) + let r1 = b.moment() + b.behavior(supplies: [], demands: [r1]) { extent in + extent.sideEffect { + g.action(syncStrategy:.sync) { + // do something + } + // cant run because I may have code here that expected it to run + } + extent.sideEffect { + // run something else + // technicallthe the action above is cutting ahead in line and changing state on us + } + } + let e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When that behavior runs + // |> Then is should be allowed? + let failed = CheckAssertionHit { + r1.updateWithAction() + } + XCTAssertFalse(failed) + } + +} diff --git a/Example/Tests/DependenciesTests.swift b/Example/Tests/DependenciesTests.swift new file mode 100644 index 0000000..9d0b2fb --- /dev/null +++ b/Example/Tests/DependenciesTests.swift @@ -0,0 +1,77 @@ +// +// Copyright © 2021 Yahoo +// + + +import Foundation +import Quick +import Nimble +@testable import BGSwift + +class DependenciesTest : QuickSpec { + override func spec() { + + var g: BGGraph! + var r_a, r_b, r_c: BGState! + var bld: BGExtentBuilder! + var ext: BGExtent! + + beforeEach { + g = BGGraph() + bld = BGExtentBuilder(graph: g) + r_a = bld.state(0) + r_b = bld.state(0) + r_c = bld.state(0) + } + + it("a activates b") { + // |> Given a behavior with supplies and demands + bld.behavior(supplies: [r_b], demands: [r_a]) { extent in + r_b.update(2 * r_a.value) + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction(); + + // |> When the demand is updated + r_a.updateWithAction(1) + + // |> Then the demanding behavior will run and update its supplied resource + expect(r_b.value).to(equal(2)) + expect(r_b.event).to(equal(r_a.event)) + } + + it("activates behaviors once per event") { + // |> Given a behavior that demands multiple resources + var called = 0 + bld.behavior(supplies: [r_c], demands: [r_a, r_b]) { extent in + called += 1 + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + // |> When both resources are updated in same event + g.action { + r_a.update(1) + r_b.update(2) + } + + // |> Then the demanding behavior is activated only once + expect(called) == 1 + } + + xit("filters out duplicates") { + /* + let b1 = bld.behavior(supplies: [r_b, r_b], demands: [r_a, r_a]) { extent in + + } + ext = BGExtent(builder: bld) + ext.addToGraphWithAction() + + expect(b1.demands!.size) == 1 + expect(b1.supplies!.size) == 1 + expect(r_a.subsequents!.size) == 1 + */ + } + + } +} diff --git a/Example/Tests/DynamicsTests.swift b/Example/Tests/DynamicsTests.swift new file mode 100644 index 0000000..99ea8d4 --- /dev/null +++ b/Example/Tests/DynamicsTests.swift @@ -0,0 +1,465 @@ +// +// Copyright © 2021 Yahoo +// + +import XCTest +@testable import BGSwift +//import BGSwift + + +class DynamicsTests: XCTestCase { + + var g: BGGraph! + var b: BGExtentBuilder! + var e: BGExtent! + var rA: BGState! + var rB: BGState! + var rC: BGState! + + override func setUp() { + g = BGGraph() + b = BGExtentBuilder(graph: g) + rA = b.state(0) + rB = b.state(0) + rC = b.state(0) + } + + override func tearDown() { + } + + func testBehaviorOrdersUpdateWhenDemandsAdded() throws { + enum BehaviorOrder { + case abc + case acb + } + + let b = BGExtentBuilder(graph: g) + + let r_a: BGTypedMoment = b.typedMoment() + let r_b = b.moment() + let r_c = b.moment() + + let r_x = b.moment() + let r_y = b.moment() + let r_z = b.moment() + + var sequence = 0 + var update_b = 0 + var update_c = 0 + var update_x = 0 + var update_y = 0 + var update_z = 0 + + b.behavior(supplies: [r_b], demands: [r_a]) { extent in + update_b = sequence + sequence += 1 + r_b.update() + } + + b.behavior(supplies: [r_c], demands: [r_b]) { extent in + update_c = sequence + sequence += 1 + r_c.update() + } + + b.behavior(supplies: [r_x], demands: [r_a], + dynamicDemands: .init(switches: [r_a], { extent in + if let value = r_a.value, value == true { + return [r_c] + } else { + return [] + } + })) { extent in + update_x = sequence + sequence += 1 + r_x.update() + } + + b.behavior(supplies: [r_y], demands: [r_x]) { extent in + update_y = sequence + sequence += 1 + r_y.update() + } + + b.behavior(supplies: [r_z], demands: [r_y]) { extent in + update_z = sequence + sequence += 1 + r_z.update() + } + + e = BGExtent(builder: b) + + g.action { + self.e.addToGraph() + r_a.update(false) + } + + XCTAssertLessThan(update_b, update_c) + XCTAssertLessThan(update_x, update_c) + XCTAssertLessThan(update_x, update_y) + XCTAssertLessThan(update_y, update_z) + + sequence = 0 + r_a.updateWithAction(true) + XCTAssertLessThan(update_b, update_c) + XCTAssertGreaterThan(update_x, update_c) + XCTAssertLessThan(update_x, update_y) + XCTAssertLessThan(update_y, update_z) + } + + func testBehaviorOrdersUpdateWhenSuppliesAdded() throws { + + let r_a = b.moment() + let r_b = b.moment() + + var sequence = 0 + var update_b1 = 0 + var update_b2 = 0 + + let b1 = b.behavior(demands: [r_a], + dynamicSupplies: .init(switches: [r_a], { extent in + return [r_b] + })) { extent in + r_b.update() + update_b1 = sequence + sequence += 1 + } + + let b2 = b.behavior(demands: [r_b]) { extent in + update_b2 = sequence + sequence += 1 + } + + e = BGExtent(builder: b) + + e.addToGraphWithAction() + XCTAssertGreaterThanOrEqual(b1.order, b2.order) + + sequence = 0 + r_a.updateWithAction() + XCTAssertLessThan(update_b1, update_b2) + XCTAssertLessThan(b1.order, b2.order) + } + + + func testCanAddAndUpdateSameEvent() { + // |> Given a new extent + let rX: BGState = b.state(0) + b.behavior(supplies: [rX], demands: [rA]) { extent in + if self.rA.justUpdated() { + rX.update(self.rA.value * 2) + } + } + e = BGExtent(builder: b) + + // |> When it is added to graph in same event as one of its resources is updated + // but technically updated before added + g.action { + self.rA.update(2) + self.e.addToGraph() + } + + // |> Then that updating should still activate the concerned behavior + XCTAssertEqual(rX.value, 4) + } + + func testCanAddExtentInsideBehavior() { + // |> Given a behavior that adds a new extent when something happens + let b2 = BGExtentBuilder(graph: g) + b2.behavior(supplies: [rC], demands: [rB]) { extent in + self.rC.update(self.rB.value + 1) + } + let e2 = BGExtent(builder: b2) + + b.behavior(supplies: [], demands: [rA]) { extent in + if (self.rA.justUpdated()) { + e2.addToGraph() + } + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When updating resource before adding extent with demanding behavior + g.action { + self.rB.update(1) + } + + // |> Then demanding behavior isn't run + XCTAssertEqual(rC.value, 0) + + // |> And when behavior that adds the extent also gets run in same action + g.action { + self.rB.update(2) + self.rA.update(1) + } + + // |> Then new extent is added and demanding behavior activated, and supplied resource is updated + XCTAssertEqual(rC.value, 3) + } + + func testActivatedBehaviorsCanReorder() { + var counter = 0; + var whenX = 0; + var whenY = 0; + + // |> Given two behaviors where x comes before y + let reordering = b.moment() + let x_out: BGState = b.state(0) + let x_bhv = b.behavior(supplies: [x_out], demands: [rA, reordering]) { extent in + whenX = counter + counter = counter + 1 + } + let y_out: BGState = b.state(0) + let y_bhv = b.behavior(supplies: [y_out], demands: [rA, reordering, x_out]) { extent in + whenY = counter + counter = counter + 1 + } + + // this behavior makes y come before x + b.behavior(supplies: [reordering], demands: [rA]) { extent in + if self.rA.justUpdated() { + x_bhv.setDemands([self.rA, reordering, y_out]) + y_bhv.setDemands([self.rA, reordering]) + } + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When event that activates both behaviors and the reordering behavior runs + rA.updateWithAction(2); + + // |> Then Y should get run before X + XCTAssertEqual(whenX, whenY + 1) + } + + func testSupplyingResourceAfterSubsequentHasBeenAdded() { + // NOTE: This test ensures that just adding a supplier is enough to force + // a resorting. + + // |> Given a behavior that demands an unsupplied resource has been added + let rZ = b.state(0) + let rY = b.state(0) + b.behavior(supplies: [rZ], demands: [rY]) { extent in + rZ.update(rY.value) + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When a new extent is added that supplies that resource + let b2 = BGExtentBuilder(graph: g) + let rX = b2.state(0) + b2.behavior(supplies: [rY], demands: [rX]) { extent in + rY.update(rX.value) + } + let e2 = BGExtent(builder: b2) + e2.addToGraphWithAction() + + // |> Then the original behavior should get sorted correctly on just a supplier change + rX.updateWithAction(1) + XCTAssertEqual(rZ.value, 1) + } + + func testChangingBehaviorToDemandAlreadyUpdatedResourceShouldRunBehavior() { + // |> Given we have a behavior that doesn't demand r_a + var run = false + let bhv1 = b.behavior(supplies: [], demands: []) { extent in + run = true + } + b.behavior(supplies: [], demands: [rA]) { extent in + bhv1.setDemands([self.rA]) + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When we update the behavior to demand r_a in the same event that r_a has already run + rA.updateWithAction(1) + + // |> Then our behavior will activate + XCTAssertTrue(run) + } + + func testUpdatingSuppliesWillReorderActivatedBehaviors() { + // NOTE: This tests that activated behaviors will get properly + // reordered when their supplies change + + // |> Given two unorderd behaviors + let rY = b.state(0) + let rX = b.state(0) + b.behavior(supplies: [rY], demands: [rA, rX]) { extent in + if rX.justUpdated() { + rY.update(self.rA.value) + } + } + // this behavior will get its supplies added + let bhv2 = b.behavior(supplies: [], demands: [rA]) { extent in + rX.update(self.rA.value) + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When they are activated by one resource and change supplies + g.action { + self.rA.update(3) + bhv2.setSupplies([rX]) + } + + // |> Then they should be ordered correctly + XCTAssertEqual(rY.value, 3) + } + + func testChangingSuppliesWillUnsupplyOldResources() { + // |> Given we supply a resource + let rX = b.moment() + let bhv1 = b.behavior(supplies: [rX], demands: []) { extent in + // no op + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + XCTAssertTrue(rX.supplier === bhv1) + + // |> When that behavior no longer supplies the resource + g.action { + bhv1.setSupplies([]) + } + + // |> Then that resource should be free to be supplied by another behavior + XCTAssertNil(rX.supplier) + } + + func testChangingDemandsWillUnsupplyOldDemands() { + // |> Given we have demands + let rX = b.moment() + var run = false + let bhv1 = b.behavior(supplies: [], demands: [rX]) { extent in + run = true + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When that behavior no longer supplies the resource + g.action { + bhv1.setDemands([]) + } + rX.updateWithAction() + + // |> Then that resource should be free to be supplied by another behavior + XCTAssertFalse(run) + } + + func testRemovedBehaviorsDontRun() { + // |> Given a foreign demand which gets removed + e = BGExtent(builder: b) + e.addToGraphWithAction() + + var run = false + autoreleasepool { + // add new foreign demand + let b2 = BGExtentBuilder(graph: g) + b2.behavior(supplies: [], demands: [self.rA]) { extent in + run = true + } + let e2 = BGExtent(builder: b2) + e2.addToGraphWithAction() + // leaving scope here will deinit and remove extent from graph + } + + // |> When previously demanded resource is updated + rA.updateWithAction(1) + + // |> Then removed demanding behavior is not run + XCTAssertFalse(run) + } + + func testRemovedResourcesAreRemovedFromForeignLinks() { + // |> Given we have a behavior that will link to foreign resources + let bhv1 = b.behavior(supplies: [], demands: []) { extent in + // nothing + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + autoreleasepool { + let b2 = BGExtentBuilder(graph: self.g) + let rX = b2.moment() + let rY = b2.moment() + let e2 = BGExtent(builder: b2) + self.g.action { + e2.addToGraph() + bhv1.setDemands([rX]) + bhv1.setSupplies([rY]) + } + XCTAssertEqual(bhv1.demands.count, 1) + XCTAssertEqual(bhv1.supplies.count, 1) + // rX and e2 are deallocated and removed when it exits scope + } + + // |> When those resources are removed + // |> Then the remaining behavior should have them removed as links + XCTAssertEqual(bhv1.demands.count, 0) + XCTAssertEqual(bhv1.supplies.count, 0) + } + + func testRemovedBehaviorsUnlinkFromForeignResources() { + // |> Given resources that will be linked to a foreign behavior + let rX = b.moment() + let rY = b.moment() + e = BGExtent(builder: b) + e.addToGraphWithAction() + autoreleasepool { + let b2 = BGExtentBuilder(graph: self.g) + b2.behavior(supplies: [rY], demands: [rX]) { extent in + // nothing + } + let e2 = BGExtent(builder: b2) + e2.addToGraphWithAction() + XCTAssertEqual(rX.subsequents.count, 1) + XCTAssertTrue(rY.supplier != nil) + // behavior is removed and dealloced + } + + XCTAssertEqual(rX.subsequents.count, 0) + XCTAssertTrue(rY.supplier == nil) + } + + func testSetDemandsTwiceWorks() { + // |> Given a behavior with no demands + var run = false + let bhv = b.behavior(supplies: [], demands: []) { extent in + run = true + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When that behavior gets its demands updated twice in same event + g.action { + bhv.setDemands([self.rA]) + bhv.setDemands([self.rB]) + } + + // |> Then the most recent update should hold + rA.updateWithAction(1) + XCTAssertFalse(run) + rB.updateWithAction(1) + XCTAssertTrue(run) + } + + func testSetSuppliesTwiceWorks() { + // |> Given a behavior with no supplies + let bhv = b.behavior(supplies: [], demands: [rA]) { extent in + self.rB.update(1) + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When that behavior gets its supplies updated twice in same event + g.action { + bhv.setSupplies([self.rA]) + bhv.setSupplies([self.rB]) + } + + // |> Then the most recent update should hold + let failed = CheckAssertionHit { + rA.updateWithAction(1) + } + XCTAssertFalse(failed) + } +} diff --git a/Example/Tests/EventTests.swift b/Example/Tests/EventTests.swift new file mode 100644 index 0000000..8ea167d --- /dev/null +++ b/Example/Tests/EventTests.swift @@ -0,0 +1,164 @@ +// +// Copyright © 2021 Yahoo +// + +import XCTest +import BGSwift + +class EventTests: XCTestCase { + + var g: BGGraph! + var b: BGExtentBuilder! + var e: BGExtent! + var rA: BGState! + var rB: BGState! + var rC: BGState! + + override func setUpWithError() throws { + g = BGGraph() + b = BGExtentBuilder(graph: g) + rA = b.state(0) + rB = b.state(0) + rC = b.state(0) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testSideEffectsHappenAfterAllBehaviors() { + // |> Given a behavior in the graph + var counter = 0; + var sideEffectCount: Int? = nil; + b.behavior(supplies: [], demands: [rA]) { extent in + counter = counter + 1 + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When a sideEffect is created + g.action { + self.rA.update(1) + self.g.sideEffect { + sideEffectCount = counter + } + } + + // |> Then it will be run after all behaviors + XCTAssertEqual(sideEffectCount, 1) + } + + func testSideEffectsHappenInOrderTheyAreCreated() { + // |> Given behaviors with side effects + var runs: [Int] = [] + b.behavior(supplies: [rB], demands: [rA]) { extent in + self.rB.update(1) + extent.sideEffect { + runs.append(2) + } + } + b.behavior(supplies: [], demands: [rB]) { extent in + extent.sideEffect { + runs.append(1) + } + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When those behaviors are run + rA.updateWithAction(1) + + // |> Then the sideEffects are run in the order they are created + XCTAssertEqual(runs[0], 2) + XCTAssertEqual(runs[1], 1) + } + + func testTransientValuesAreAvailableDuringEffects() { + // |> Given a behavior with side effects and a transient resource + var valueAvailable = false + var updatedAvailable = false + let rX: BGTypedMoment = b.typedMoment() + b.behavior(supplies: [rX], demands: [rA]) { extent in + rX.update(2) + extent.sideEffect { + valueAvailable = rX.value == 2 + updatedAvailable = rX.justUpdated() + } + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When that side effect is run + g.action { + self.rA.update(1) + } + + // |> Then the value and updated state of that transient resource will be available + XCTAssertTrue(valueAvailable) + XCTAssertTrue(updatedAvailable) + // and the value/updated state will not be available outside that event + XCTAssertNil(rX.value) + XCTAssertFalse(rX.justUpdated()) + } + + func testNestedActionsRunAfterSideEffects() { + // |> Given a behavior with a side effect that creates a new event + var counter = 0 + var effectCount: Int? + var actionCount: Int? + b.behavior(supplies: [], demands: [rA]) { extent in + self.e.sideEffect { + self.g.action { + actionCount = counter + counter = counter + 1 + } + } + self.e.sideEffect { + effectCount = counter + counter = counter + 1 + } + } + e = BGExtent(builder: b) + e.addToGraphWithAction() + + // |> When a nested chain of sideEffects is started + rA.updateWithAction(1) + + // |> Then side effects are still run in order they were created + XCTAssertEqual(effectCount, 0) + XCTAssertEqual(actionCount, 1) + } + + func testActionsAreSynchronousByDefault() { + // |> Given a graph + var counter = 0 + + // |> When an action runs by default + g.action { + counter = counter + 1 + } + + // |> It will be run synchronously + XCTAssertEqual(counter, 1) + } + + func testSideEffectsMustBeCreatedInsideEvent() { + // |> When a side effect is created outside of an event + // |> Then an error will be raised + TestAssertionHit { + self.g.sideEffect { + // nothing + } + } + } + + func testDateProviderGivesAlternateTime() { + let g2 = BGGraph { + return Date(timeIntervalSinceReferenceDate: 0) + } + g2.action { + XCTAssertEqual(g2.currentEvent?.timestamp, Date(timeIntervalSinceReferenceDate: 0)) + } + } +} + diff --git a/Example/Tests/ImpulseTests.swift b/Example/Tests/ImpulseTests.swift new file mode 100644 index 0000000..efe31cb --- /dev/null +++ b/Example/Tests/ImpulseTests.swift @@ -0,0 +1,34 @@ +// +// Copyright © 2021 Yahoo +// + +import XCTest +@testable import BGSwift + +class ImpulseTests: XCTestCase { + + let g = BGGraph() + + func testActionWithUnspecifiedImpulse() { + var impulse: String! + + let expectedLine = String(#line + 1) + g.action { [g] in + impulse = g.currentEvent!.impulse! + } + + XCTAssertTrue(impulse.contains(#fileID)) + XCTAssertTrue(impulse.contains(#function)) + XCTAssertTrue(impulse.contains(expectedLine)) + } + + func testActionWithImpulseString() { + var impulse: String! + + g.action(impulse: "foo") { [g] in + impulse = g.currentEvent!.impulse! + } + + XCTAssertEqual(impulse, "foo") + } +} diff --git a/Example/Tests/TestSupport.swift b/Example/Tests/TestSupport.swift new file mode 100644 index 0000000..0446ef6 --- /dev/null +++ b/Example/Tests/TestSupport.swift @@ -0,0 +1,30 @@ +// +// Copyright © 2021 Yahoo +// + +import Foundation +import XCTest +@testable import BGSwift + +func TestAssertionHit(_ code: () -> (), _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { + let assertionHit = CheckAssertionHit(code) + + if !assertionHit { + XCTFail(message(), file: file, line: line) + } +} + +func CheckAssertionHit(_ code: () -> ()) -> Bool { + var assertionHit = false + BGSwift.assertionFailureImpl = { _, _, _ in + assertionHit = true + } + + defer { + BGSwift.assertionFailureImpl = nil + } + + code() + + return assertionHit +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..325c8c6 --- /dev/null +++ b/Podfile @@ -0,0 +1,23 @@ +use_frameworks! + +platform :ios, '12.0' +project 'Example/BGSwift.xcodeproj' + +target 'BGSwift-Example' do + pod 'BGSwift', :path => '.' + + target 'BGSwift-Tests' do + inherit! :search_paths + + pod 'Quick', '~> 4.0' + pod 'Nimble', '~> 9.2' + end +end + +#post_install do |installer| +# installer.generated_projects.each do |project| +# project.build_configurations.each do |configuration| +# configuration.build_settings["ENABLE_TESTABILITY"] = "YES" +# end +# end +#end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..8fa0756 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,29 @@ +PODS: + - BGSwift (0.5.0): + - BGSwift/Core (= 0.5.0) + - BGSwift/Core (0.5.0) + - Nimble (9.2.1) + - Quick (4.0.0) + +DEPENDENCIES: + - BGSwift (from `.`) + - Nimble (~> 9.2) + - Quick (~> 4.0) + +SPEC REPOS: + trunk: + - Nimble + - Quick + +EXTERNAL SOURCES: + BGSwift: + :path: "." + +SPEC CHECKSUMS: + BGSwift: 874ad974413a8ee2f2537245ecd5f0f9a3e1bf43 + Nimble: e7e615c0335ee4bf5b0d786685451e62746117d5 + Quick: 6473349e43b9271a8d43839d9ba1c442ed1b7ac4 + +PODFILE CHECKSUM: f89ef0e6aae7f28f91489394e672d73b7409fae8 + +COCOAPODS: 1.11.2 diff --git a/README.md b/README.md index a46ae92..0c360c3 100644 --- a/README.md +++ b/README.md @@ -1 +1,37 @@ -# .github \ No newline at end of file +# BehaviorGraph + +**Behavior Graph** is a software library that greatly enhances our ability to program **user facing software** and **control systems**. Programs of this type quickly scale up in complexity as features are added. Behavior Graph directly addresses this complexity by shifting more of the burden to the computer. It works by offering the programmer a new unit of code organization called a **behavior**. Behaviors are blocks of code enriched with additional information about their stateful relationships. Using this information, Behavior Graph enforces _safe use of mutable state_, arguably the primary source of complexity in this class of software. It does this by taking on the responsibility of control flow between behaviors, ensuring they are are _run at the correct time and in the correct order_. + +## Is it any good? + +Yes + +## Documentation + +Coming Soon. See Objective-C Docs Here + +[Introduction to Behavior Graph](/bgdocs/objc/intro.html) + +[Behavior Graph Programming Guide](/bgdocs/objc/guide.html) + +## Example + +To run the example project, clone the repo, and run `pod install` from the Example directory first. + +## Requirements + +iOS or MacOS + +## Installation + +Work In Progress: + +Simply add the following line to your Podfile: + +```ruby +pod 'BGSwift', :git => '???' +``` + +## License + +BehaviorGraph is available under the Apache 2.0 license. See the LICENSE file for more info. diff --git a/bgswift.xcworkspace/contents.xcworkspacedata b/bgswift.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1b585ec --- /dev/null +++ b/bgswift.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/bgswift.xcworkspace/xcshareddata/IDETemplateMacros.plist b/bgswift.xcworkspace/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..fcd09f0 --- /dev/null +++ b/bgswift.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,10 @@ + + + + + FILEHEADER + +// Copyright © __YEAR__ Yahoo +// + + diff --git a/bgswift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/bgswift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/bgswift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + +