├── Example
├── BGSwift
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── ViewController.swift
│ ├── AppDelegate.swift
│ └── LoginExtent.swift
├── BGSwift.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── BGSwift-Example.xcscheme
└── Tests
│ ├── TestSupport.swift
│ ├── ImpulseTests.swift
│ ├── AssertionTests.swift
│ ├── DependenciesTests.swift
│ ├── BGGraphTests.swift
│ ├── BGExtentTests.swift
│ ├── EventTests.swift
│ ├── BGMomentTests.swift
│ ├── ConcurrencyTests.swift
│ └── BGStateTests.swift
├── BGSwift
└── Classes
│ ├── BGAction.swift
│ ├── BGSideEffect.swift
│ ├── BGEvent.swift
│ ├── BGLink.swift
│ ├── Mutex.swift
│ ├── PriorityQueue.swift
│ ├── BGBehavior.swift
│ ├── BGResource.swift
│ ├── BGExtent.swift
│ └── BGGraph.swift
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── PULL_REQUEST_TEMPLATE.md
├── Package.swift
├── README.md
├── CONTRIBUTING.md
├── .gitignore
├── Code_of_Conduct.md
└── LICENSE.txt
/Example/BGSwift/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/BGSwift/Classes/BGAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 |
7 | struct BGAction {
8 | let impulse: String?
9 | let action: () -> Void
10 | }
11 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/BGSwift/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Example/BGSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/BGSwift/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 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.
4 |
--------------------------------------------------------------------------------
/Example/BGSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/BGSwift/Classes/BGSideEffect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 |
7 | struct BGSideEffect {
8 | let label: String?
9 | let event: BGEvent
10 | let run: () -> Void
11 |
12 | init(label: String? = nil, event: BGEvent, run: @escaping () -> Void) {
13 | self.label = label
14 | self.event = event
15 | self.run = run
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/BGSwift/Classes/BGEvent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 |
6 | import Foundation
7 |
8 | public struct BGEvent: Equatable {
9 | public let sequence: UInt
10 | public let timestamp: Date
11 | public let impulse: String?
12 |
13 | public static let unknownPast: BGEvent = .init(sequence: 0, timestamp: Date(timeIntervalSince1970: 0), impulse: nil)
14 |
15 | init(sequence: UInt, timestamp: Date, impulse: String?) {
16 | self.sequence = sequence
17 | self.timestamp = timestamp
18 | self.impulse = impulse
19 | }
20 |
21 | public func happenedSince(sequence: UInt) -> Bool {
22 | self.sequence > 0 && self.sequence >= sequence
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Example/Tests/TestSupport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 | import XCTest
7 | @testable import BGSwift
8 |
9 | func TestAssertionHit(graph: BGGraph, _ code: () -> (), _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
10 | let assertionHit = CheckAssertionHit(graph: graph, code)
11 |
12 | if !assertionHit {
13 | XCTFail(message(), file: file, line: line)
14 | }
15 | }
16 |
17 | func CheckAssertionHit(graph: BGGraph, _ code: () -> ()) -> Bool {
18 | var assertionHit = false
19 | graph.debugOnAssertionFailure = { _, _, _ in
20 | assertionHit = true
21 | }
22 |
23 | defer {
24 | graph.debugOnAssertionFailure = nil
25 | }
26 |
27 | code()
28 |
29 | return assertionHit
30 | }
31 |
--------------------------------------------------------------------------------
/Example/Tests/ImpulseTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import XCTest
6 | @testable import BGSwift
7 |
8 | class ImpulseTests: XCTestCase {
9 |
10 | let g = BGGraph()
11 |
12 | func testActionWithUnspecifiedImpulse() {
13 | var impulse: String!
14 |
15 | let expectedLine = String(#line + 1)
16 | g.action { [g] in
17 | impulse = g.currentEvent!.impulse!
18 | }
19 |
20 | XCTAssertTrue(impulse.contains(#fileID))
21 | XCTAssertTrue(impulse.contains(#function))
22 | XCTAssertTrue(impulse.contains(expectedLine))
23 | }
24 |
25 | func testActionWithImpulseString() {
26 | var impulse: String!
27 |
28 | g.action(impulse: "foo") { [g] in
29 | impulse = g.currentEvent!.impulse!
30 | }
31 |
32 | XCTAssertEqual(impulse, "foo")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "BGSwift",
8 | platforms: [
9 | .macOS(.v10_15), // These are where Combine becomes available which we will use in SwiftPM compatibility
10 | .iOS(.v13)
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, and make them visible to other packages.
14 | .library(
15 | name: "BGSwift",
16 | targets: ["BGSwift"]),
17 | ],
18 | dependencies: [
19 | // Dependencies declare other packages that this package depends on.
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
24 | .target(
25 | name: "BGSwift",
26 | path: "BGSwift/Classes"),
27 | .testTarget(
28 | name: "BGSwiftTests",
29 | dependencies: ["BGSwift"],
30 | path: "Example/Tests"),
31 | ],
32 | swiftLanguageVersions: [.v5]
33 | )
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BehaviorGraph
2 |
3 | **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_.
4 |
5 | ## Is it any good?
6 |
7 | Yes
8 |
9 | ## Documentation
10 |
11 | Coming Soon. See Objective-C Docs Here
12 |
13 | [Introduction to Behavior Graph](/bgdocs/objc/intro.html)
14 |
15 | [Behavior Graph Programming Guide](/bgdocs/objc/guide.html)
16 |
17 | ## Example
18 |
19 | To run the example project, clone the repo, and run `pod install` from the Example directory first.
20 |
21 | ## Requirements
22 |
23 | iOS or MacOS
24 |
25 | ## Installation
26 |
27 | Work In Progress:
28 |
29 | Simply add the following line to your Podfile:
30 |
31 | ```ruby
32 | pod 'BGSwift', :git => '???'
33 | ```
34 |
35 | ## License
36 |
37 | BehaviorGraph is available under the Apache 2.0 license. See the LICENSE file for more info.
38 |
--------------------------------------------------------------------------------
/Example/Tests/AssertionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 | import XCTest
7 | import BGSwift
8 |
9 | class AssertionTests: XCTestCase {
10 |
11 | func testAssertsUndeclaredDemandWithSupplier() {
12 | var assertionHit = false
13 |
14 | let g = BGGraph()
15 | g.checkUndeclaredDemands = true
16 |
17 | let b = BGExtentBuilder(graph: g)
18 | let r = b.moment()
19 |
20 | b.behavior().supplies([r]).runs { _ in
21 | // do nothing
22 | }
23 |
24 | b.behavior().demands([b.added]).runs { extent in
25 | assertionHit = CheckAssertionHit(graph: g) {
26 | _ = r.justUpdated()
27 | }
28 | }
29 |
30 | let e = BGExtent(builder: b)
31 | e.addToGraphWithAction()
32 |
33 | XCTAssertTrue(assertionHit)
34 | }
35 |
36 | func testAssertsUndeclaredDemandWithNoSupplier() {
37 | var assertionHit = false
38 |
39 | let g = BGGraph()
40 | g.checkUndeclaredDemands = true
41 |
42 | let b = BGExtentBuilder(graph: g)
43 | let r = b.moment()
44 |
45 | b.behavior().demands([b.added]).runs { extent in
46 | assertionHit = CheckAssertionHit(graph: g) {
47 | _ = r.justUpdated()
48 | }
49 | }
50 |
51 | let e = BGExtent(builder: b)
52 | e.addToGraphWithAction()
53 |
54 | XCTAssertTrue(assertionHit)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Example/BGSwift/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Example/BGSwift/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import UIKit
6 | import BGSwift
7 |
8 | class ViewController: UIViewController {
9 |
10 | @IBOutlet var emailField: UITextField!
11 | @IBOutlet var passwordField: UITextField!
12 | @IBOutlet var loginButton: UIButton!
13 | @IBOutlet var emailFeedback: UILabel!
14 | @IBOutlet var passwordFeedback: UILabel!
15 | @IBOutlet var loginStatus: UILabel!
16 | @IBOutlet var loginSuccess: UIButton!
17 | @IBOutlet var loginFail: UIButton!
18 |
19 | let graph: BGGraph
20 | let loginExtent: LoginExtent
21 |
22 | required init?(coder: NSCoder) {
23 | graph = BGGraph()
24 | loginExtent = LoginExtent(graph: graph)
25 | super.init(coder: coder)
26 | loginExtent.loginForm = self
27 | }
28 |
29 | override func viewDidLoad() {
30 | super.viewDidLoad()
31 | loginExtent.addToGraphWithAction()
32 | }
33 |
34 | override func didReceiveMemoryWarning() {
35 | super.didReceiveMemoryWarning()
36 | }
37 |
38 | @IBAction func didUpdateEmailField(sender: UITextField) {
39 | graph.action {
40 | self.loginExtent.email.update(self.emailField.text ?? "")
41 | }
42 | }
43 |
44 | @IBAction func didUpdatePasswordField(sender: UITextField) {
45 | graph.action {
46 | self.loginExtent.password.update(self.passwordField.text ?? "")
47 | }
48 | }
49 |
50 | @IBAction func loginButtonClicked(sender: UIButton) {
51 | graph.action {
52 | self.loginExtent.loginClick.update()
53 | }
54 | }
55 |
56 | @IBAction func loginSucceeded(sender: UIButton) {
57 | loginExtent.completeLogin(success: true)
58 | }
59 |
60 | @IBAction func loginFailed(sender: UIButton) {
61 | loginExtent.completeLogin(success: false)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/BGSwift/Classes/BGLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 |
7 | enum LinkType {
8 | case reactive
9 | case order
10 | }
11 |
12 | struct BGSubsequentLink: Equatable, Hashable {
13 | weak private(set) var behavior: BGBehavior?
14 | let type: LinkType
15 | let behaviorPtr: ObjectIdentifier
16 |
17 | init(behavior: BGBehavior, type: LinkType) {
18 | self.behavior = behavior
19 | self.type = type
20 | behaviorPtr = ObjectIdentifier(behavior)
21 | }
22 |
23 | // MARK: Equatable
24 |
25 | static func == (lhs: BGSubsequentLink, rhs: BGSubsequentLink) -> Bool {
26 | if lhs.type == rhs.type,
27 | let bl = lhs.behavior, let br = rhs.behavior,
28 | bl === br {
29 | return true
30 | }
31 | return false
32 | }
33 |
34 | // MARK: Hashable
35 |
36 | func hash(into hasher: inout Hasher) {
37 | hasher.combine(behaviorPtr)
38 | hasher.combine(type)
39 | }
40 | }
41 |
42 | struct BGDemandLink: BGDemandable, Equatable, Hashable {
43 | weak private(set) var resource: (any BGResourceInternal)?
44 | let type: LinkType
45 | let resourcePtr: ObjectIdentifier
46 |
47 | init(resource: any BGResourceInternal, type: LinkType) {
48 | self.resource = resource
49 | self.type = type
50 | resourcePtr = ObjectIdentifier(resource)
51 | }
52 |
53 | // MARK: Equatable
54 |
55 | static func == (lhs: BGDemandLink, rhs: BGDemandLink) -> Bool {
56 | if lhs.type == rhs.type,
57 | let rl = lhs.resource, let rr = rhs.resource,
58 | rl === rr {
59 | return true
60 | }
61 | return false
62 | }
63 |
64 | // MARK: Hashable
65 |
66 | func hash(into hasher: inout Hasher) {
67 | hasher.combine(resourcePtr)
68 | hasher.combine(type)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Example/BGSwift/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Example/BGSwift/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 |
6 | import UIKit
7 |
8 | @UIApplicationMain
9 | class AppDelegate: UIResponder, UIApplicationDelegate {
10 |
11 | var window: UIWindow?
12 |
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | // Override point for customization after application launch.
16 | return true
17 | }
18 |
19 | func applicationWillResignActive(_ application: UIApplication) {
20 | // 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.
21 | // 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.
22 | }
23 |
24 | func applicationDidEnterBackground(_ application: UIApplication) {
25 | // 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.
26 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
27 | }
28 |
29 | func applicationWillEnterForeground(_ application: UIApplication) {
30 | // 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.
31 | }
32 |
33 | func applicationDidBecomeActive(_ application: UIApplication) {
34 | // 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.
35 | }
36 |
37 | func applicationWillTerminate(_ application: UIApplication) {
38 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
39 | }
40 |
41 |
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/BGSwift/Classes/Mutex.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 |
7 | class Mutex {
8 | let mutex: UnsafeMutablePointer
9 |
10 | init(recursive: Bool = true) {
11 | let attributes = UnsafeMutablePointer.allocate(capacity: 1)
12 | attributes.initialize(to: pthread_mutexattr_t())
13 |
14 | guard pthread_mutexattr_init(attributes) == 0 else {
15 | preconditionFailure()
16 | }
17 |
18 | defer {
19 | pthread_mutexattr_destroy(attributes)
20 | attributes.deallocate()
21 | }
22 |
23 | pthread_mutexattr_settype(attributes, recursive ? PTHREAD_MUTEX_RECURSIVE : PTHREAD_MUTEX_NORMAL)
24 |
25 | mutex = .allocate(capacity: 1)
26 | mutex.initialize(to: pthread_mutex_t())
27 |
28 | guard pthread_mutex_init(mutex, attributes) == 0 else {
29 | preconditionFailure()
30 | }
31 | }
32 |
33 | deinit {
34 | pthread_mutex_destroy(mutex)
35 | mutex.deallocate()
36 | }
37 |
38 | // jlou 2/18/21 - Avoid concurrent execution by surrounding code with mutex lock/unlock. I believe Swift
39 | // compiler should be able to inline the non-escaping closure and avoid heap allocation for captured variables
40 | // but I will need to test this. If not, we can pass variables into the closure with a generic input argument.
41 |
42 | @inline(__always)
43 | func balancedUnlock(_ code: () throws -> Void) rethrows {
44 | lock()
45 | defer {
46 | unlock()
47 | }
48 | try code()
49 | }
50 |
51 | @inline(__always)
52 | func balancedUnlock(_ code: () throws -> T) rethrows -> T {
53 | lock()
54 | defer {
55 | unlock()
56 | }
57 | return try code()
58 | }
59 |
60 | @inline(__always)
61 | func lock() {
62 | pthread_mutex_lock(mutex)
63 | }
64 |
65 | @inline(__always)
66 | func tryLock() -> Bool {
67 | return pthread_mutex_trylock(mutex) == 0
68 | }
69 |
70 | @inline(__always)
71 | func unlock() {
72 | pthread_mutex_unlock(mutex)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 | First, thanks for taking the time to contribute to our project! There are many ways you can help out.
3 |
4 | ### Questions
5 |
6 | If you have a question that needs an answer, create an issue, and label it as a question.
7 |
8 | ### Issues for bugs or feature requests
9 |
10 | 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.
11 |
12 | ### Contribute Code
13 | We welcome your pull requests for bug fixes. To implement something new, please create an issue first so we can discuss it together.
14 |
15 | ***Creating a Pull Request***
16 | Please follow [best practices](https://github.com/trein/dev-best-practices/wiki/Git-Commit-Best-Practices) for creating git commits. In addition:
17 |
18 | - Make sure your code respects existing formatting conventions. In general, follow
19 | the same coding style as the code that you are modifying.
20 | - Bugfixes must include a unit test or integration test reproducing the issue.
21 | - Try to keep pull requests short and submit separate ones for unrelated
22 | features, but feel free to combine simple bugfixes/tests into one pull request.
23 | - Keep the number of commits small and combine commits for related changes.
24 | - Keep formatting changes in separate commits to make code reviews easier and
25 | distinguish them from actual code changes.
26 |
27 | When your code is ready to be submitted, submit a pull request to begin the code review process.
28 |
29 | 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:
30 |
31 | > 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.
32 |
33 | ## Code of Conduct
34 |
35 | 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.
36 |
37 |
--------------------------------------------------------------------------------
/BGSwift/Classes/PriorityQueue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 |
7 | class PriorityQueue {
8 | private let heap: CFBinaryHeap
9 | private var unheapedElements = [BGBehavior]()
10 |
11 | init() {
12 | var callbacks = CFBinaryHeapCallBacks(version: 0, retain: nil, release: nil, copyDescription: nil) { ptr1, ptr2, context in
13 |
14 | let lhs = Unmanaged.fromOpaque(ptr1!).takeUnretainedValue().order
15 | let rhs = Unmanaged.fromOpaque(ptr2!).takeUnretainedValue().order
16 |
17 | return (
18 | lhs < rhs ? .compareLessThan :
19 | lhs > rhs ? .compareGreaterThan :
20 | .compareEqualTo
21 | )
22 | }
23 |
24 | heap = CFBinaryHeapCreate(kCFAllocatorDefault, 0, &callbacks, nil)
25 | }
26 |
27 | var count: Int {
28 | unheapedElements.count + CFBinaryHeapGetCount(heap)
29 | }
30 |
31 | var isEmpty: Bool {
32 | unheapedElements.isEmpty && CFBinaryHeapGetCount(heap) == 0
33 | }
34 |
35 | func setNeedsReheap() {
36 | for _ in 0 ..< CFBinaryHeapGetCount(heap) {
37 | let value = popHeap()
38 | unheapedElements.append(value)
39 | }
40 | }
41 |
42 | private func reheapIfNeeded() {
43 | unheapedElements.forEach { element in
44 | let ptr = Unmanaged.passRetained(element).toOpaque()
45 | CFBinaryHeapAddValue(heap, ptr)
46 | }
47 | unheapedElements.removeAll()
48 | }
49 |
50 | func push(_ value: BGBehavior) {
51 | unheapedElements.append(value)
52 | }
53 |
54 | func pop() -> BGBehavior {
55 | reheapIfNeeded()
56 |
57 | guard count > 0 else {
58 | preconditionFailure()
59 | }
60 |
61 | return popHeap()
62 | }
63 |
64 | func peek() -> BGBehavior {
65 | reheapIfNeeded()
66 |
67 | guard count > 0 else {
68 | preconditionFailure()
69 | }
70 |
71 | return Unmanaged.fromOpaque(CFBinaryHeapGetMinimum(heap)).takeUnretainedValue()
72 | }
73 |
74 | private func popHeap() -> BGBehavior {
75 | let value = Unmanaged.fromOpaque(CFBinaryHeapGetMinimum(heap)).takeRetainedValue()
76 | CFBinaryHeapRemoveMinimumValue(heap)
77 | return value
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | # macOS
6 | .DS_Store
7 |
8 | ## User settings
9 | xcuserdata/
10 |
11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
12 | *.xcscmblueprint
13 | *.xccheckout
14 |
15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
16 | build/
17 | DerivedData/
18 | *.moved-aside
19 | *.pbxuser
20 | !default.pbxuser
21 | *.mode1v3
22 | !default.mode1v3
23 | *.mode2v3
24 | !default.mode2v3
25 | *.perspectivev3
26 | !default.perspectivev3
27 |
28 | ## Obj-C/Swift specific
29 | *.hmap
30 |
31 | ## App packaging
32 | *.ipa
33 | *.dSYM.zip
34 | *.dSYM
35 |
36 | ## Playgrounds
37 | timeline.xctimeline
38 | playground.xcworkspace
39 |
40 | # Swift Package Manager
41 | #
42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
43 | # Packages/
44 | # Package.pins
45 | # Package.resolved
46 | # *.xcodeproj
47 | #
48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
49 | # hence it is not needed unless you have added a package configuration file to your project
50 | # .swiftpm
51 |
52 | .build/
53 |
54 | # CocoaPods
55 | #
56 | # We recommend against adding the Pods directory to your .gitignore. However
57 | # you should judge for yourself, the pros and cons are mentioned at:
58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
59 |
60 | Pods/
61 |
62 | # Add this line if you want to avoid checking in source code from the Xcode workspace
63 | # *.xcworkspace
64 |
65 | # Carthage
66 | #
67 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
68 | # Carthage/Checkouts
69 |
70 | Carthage/Build/
71 |
72 | # Accio dependency management
73 | Dependencies/
74 | .accio/
75 |
76 | # fastlane
77 | #
78 | # It is recommended to not store the screenshots in the git repo.
79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
80 | # For more information about the recommended setup visit:
81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
82 |
83 | fastlane/report.xml
84 | fastlane/Preview.html
85 | fastlane/screenshots/**/*.png
86 | fastlane/test_output
87 |
88 | # Code Injection
89 | #
90 | # After new code Injection tools there's a generated folder /iOSInjectionProject
91 | # https://github.com/johnno1962/injectionforxcode
92 |
93 | iOSInjectionProject/
94 |
--------------------------------------------------------------------------------
/Example/Tests/DependenciesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 |
6 | import Foundation
7 | import XCTest
8 | @testable import BGSwift
9 |
10 | class DependenciesTest : XCTestCase {
11 |
12 | var g: BGGraph!
13 | var r_a, r_b, r_c: BGState!
14 | var bld: BGExtentBuilder!
15 | var ext: BGExtent!
16 |
17 | override func setUp() {
18 | super.setUp()
19 | g = BGGraph()
20 | bld = BGExtentBuilder(graph: g)
21 | r_a = bld.state(0)
22 | r_b = bld.state(0)
23 | r_c = bld.state(0)
24 | }
25 |
26 | func testAActivatesB() {
27 | // |> Given a behavior with supplies and demands
28 | bld.behavior().supplies([r_b]).demands([r_a]).runs { extent in
29 | self.r_b.update(2 * self.r_a.value)
30 | }
31 | ext = BGExtent(builder: bld)
32 | ext.addToGraphWithAction();
33 |
34 | // |> When the demand is updated
35 | r_a.updateWithAction(1)
36 |
37 | // |> Then the demanding behavior will run and update its supplied resource
38 | XCTAssertEqual(r_b.value, 2)
39 | XCTAssertEqual(r_b.event, r_a.event)
40 | }
41 |
42 | func testActivatesBehaviorsOncePerEvent() {
43 | // |> Given a behavior that demands multiple resources
44 | var called = 0
45 | bld.behavior().supplies([r_c]).demands([r_a, r_b]).runs { extent in
46 | called += 1
47 | }
48 | ext = BGExtent(builder: bld)
49 | ext.addToGraphWithAction()
50 |
51 | // |> When both resources are updated in same event
52 | self.g.action {
53 | self.r_a.update(1)
54 | self.r_b.update(2)
55 | }
56 |
57 | // |> Then the demanding behavior is activated only once
58 | XCTAssertEqual(called, 1)
59 | }
60 | }
61 |
62 | class DependenciesTest2: XCTestCase {
63 |
64 | func testBehaviorNotActivatedFromOrderingDemandUpdate() {
65 | let g = BGGraph()
66 | let b = BGExtentBuilder(graph: g)
67 | let r = b.moment()
68 |
69 | var reactiveLinkActivated = false
70 | var orderLinkActivated = false
71 |
72 | b.behavior().demands([r]).runs { extent in
73 | reactiveLinkActivated = true
74 | }
75 |
76 | b.behavior().demands([r.order]).runs { extent in
77 | orderLinkActivated = true
78 | }
79 |
80 | let ext = BGExtent(builder: b)
81 | g.action {
82 | ext.addToGraph()
83 | r.update()
84 | }
85 |
86 | XCTAssertTrue(reactiveLinkActivated)
87 | XCTAssertFalse(orderLinkActivated)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Example/Tests/BGGraphTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import XCTest
6 | import BGSwift
7 |
8 | class BGGraphTests: XCTestCase {
9 |
10 | var g: BGGraph!
11 | var b: BGExtentBuilder!
12 | var e: BGExtent!
13 | var rA: BGState!
14 | var rB: BGState!
15 | var rC: BGState!
16 |
17 | override func setUpWithError() throws {
18 | g = BGGraph()
19 | b = BGExtentBuilder(graph: g)
20 | rA = b.state(0)
21 | rB = b.state(0)
22 | rC = b.state(0)
23 | }
24 |
25 | override func tearDownWithError() throws {
26 | // Put teardown code here. This method is called after the invocation of each test method in the class.
27 | }
28 |
29 | func testDependencyCyclesCaught() {
30 | // |> Given a graph with dependency cycles
31 | b.behavior().supplies([rA]).demands([rB]).runs { extent in
32 | // nothing
33 | }
34 | b.behavior().supplies([rB]).demands([rA]).runs { extent in
35 | // nothing
36 | }
37 | e = BGExtent(builder: b)
38 |
39 | // |> When it is added to the graph
40 | // |> Then it will raise an error
41 | TestAssertionHit(graph: g) {
42 | e.addToGraphWithAction()
43 | }
44 | }
45 |
46 | func testResourceCanOnlyBeSuppliedByOneBehavior() {
47 | // |> Given an a resource that is supplied by a behavior
48 | b.behavior().supplies([rA]).runs { extent in
49 | // nothing
50 | }
51 |
52 | // |> When a behavior sets a static supply that is already supplied by another behavior
53 | // |> Then it will raise an error
54 | TestAssertionHit(graph: g) {
55 | b.behavior().supplies([rA]).runs { extent in
56 | // nothing
57 | }
58 | }
59 |
60 | let bhv = b.behavior().runs { extent in
61 | // nothing
62 | }
63 |
64 | e = BGExtent(builder: b)
65 | e.addToGraphWithAction()
66 |
67 | // |> When a behavior sets a dynamic supply that is already supplied by another behavior
68 | // |> Then it will raise an error
69 | TestAssertionHit(graph: g) {
70 | g.action {
71 | bhv.setDynamicSupplies([self.rA])
72 | }
73 | }
74 | }
75 |
76 | func testLinksCanOnlyBeUpdatedDuringAnEvent() {
77 | // |> Given a behavior in a graph
78 | let bhv = b.behavior().runs { extent in
79 | // nothing
80 | }
81 | e = BGExtent(builder: b)
82 | e.addToGraphWithAction()
83 |
84 | // |> When updating demands outside of event
85 | // |> Then there is an error
86 | TestAssertionHit(graph: g) {
87 | bhv.setDynamicDemands([rA])
88 | }
89 |
90 | // |> And when updating supplies outside of event
91 | // |> Then there is an error
92 | TestAssertionHit(graph: g) {
93 | bhv.setDynamicSupplies([rB])
94 | }
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/BGSwift/Classes/BGBehavior.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 |
7 | enum OrderingState: Int {
8 | case ordered
9 | case ordering
10 | case unordered
11 | }
12 |
13 | public class BGBehavior {
14 | var orderingState = OrderingState.ordered
15 | var order = UInt(0)
16 | var enqueuedSequence = UInt(0)
17 | var lastUpdateSequence = UInt(0)
18 | var removedSequence = UInt(0)
19 |
20 | var debugName: String?
21 |
22 | var staticSupplies = Set()
23 | var staticDemands = Set()
24 |
25 | var supplies = Set()
26 | var demands = Set()
27 |
28 | var uncommittedDynamicSupplies: [any BGResourceInternal]?
29 | var uncommittedDynamicDemands: [BGDemandable]?
30 |
31 | var uncommittedSupplies: Bool
32 | var uncommittedDemands: Bool
33 |
34 | let requiresMainThread: Bool
35 |
36 | let runBlock: (BGExtent) -> Void
37 |
38 | weak var owner: BGExtent?
39 | var graph: BGGraph? { owner?.graph }
40 |
41 |
42 | internal init(graph: BGGraph, supplies: [BGResource], demands: [BGDemandable], requiresMainThread: Bool, body: @escaping (BGExtent) -> Void) {
43 | self.requiresMainThread = requiresMainThread
44 | self.runBlock = body
45 |
46 | uncommittedSupplies = !supplies.isEmpty
47 | uncommittedDemands = !demands.isEmpty
48 |
49 | supplies.forEach {
50 | let resource = $0.asInternal
51 | guard resource.supplier == nil else {
52 | graph.assertionFailure("Resource is already supplied by a different behavior.")
53 | return
54 | }
55 | staticSupplies.insert(resource.weakReference)
56 | resource.supplier = self
57 | }
58 |
59 | demands.forEach { staticDemands.insert($0.link) }
60 | }
61 |
62 | func run() {
63 | guard let owner = self.owner else {
64 | return
65 | }
66 | runBlock(owner)
67 | }
68 |
69 | public func setDynamicSupplies(_ supplies: [BGResource]) {
70 | uncommittedDynamicSupplies = (supplies.map { $0.asInternal} )
71 | uncommittedSupplies = true
72 |
73 | if let owner = owner, owner.status == .added {
74 | owner.graph.updateSupplies(behavior: self)
75 | }
76 | }
77 |
78 | public func setDynamicSupplies(_ supplies: BGResource...) {
79 | setDynamicSupplies(supplies as [BGResource])
80 | }
81 |
82 | public func setDynamicDemands(_ demands: [BGDemandable]) {
83 | uncommittedDynamicDemands = demands
84 | uncommittedDemands = true
85 |
86 | if let owner = owner, owner.status == .added {
87 | owner.graph.updateDemands(behavior: self)
88 | }
89 | }
90 |
91 | public func setDynamicDemands(_ demands: BGDemandable...) {
92 | setDynamicDemands(demands)
93 | }
94 | }
95 |
96 | extension BGBehavior: CustomDebugStringConvertible {
97 | public var debugDescription: String {
98 | "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(debugName ?? "Unlabeled"))>"
99 | }
100 | }
101 |
102 | extension BGBehavior: Equatable {
103 | public static func == (lhs: BGBehavior, rhs: BGBehavior) -> Bool { lhs === rhs }
104 |
105 | }
106 |
107 | extension BGBehavior: Hashable {
108 | public func hash(into hasher: inout Hasher) {
109 | hasher.combine(ObjectIdentifier(self))
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Example/BGSwift.xcodeproj/xcshareddata/xcschemes/BGSwift-Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
35 |
36 |
37 |
38 |
40 |
46 |
47 |
48 |
49 |
50 |
60 |
62 |
68 |
69 |
70 |
71 |
74 |
75 |
76 |
77 |
81 |
82 |
83 |
84 |
90 |
92 |
98 |
99 |
100 |
101 |
103 |
104 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/Example/BGSwift/LoginExtent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 | import BGSwift
7 |
8 | class LoginExtent: BGExtent {
9 | let email: BGState
10 | let password: BGState
11 | let loginClick: BGMoment
12 | let emailValid: BGState
13 | let passwordValid: BGState
14 | let loginEnabled: BGState
15 | let loggingIn: BGState
16 | let loginComplete: BGTypedMoment
17 |
18 | weak var loginForm: ViewController?
19 | var savedLoginBlock: ((Bool) -> Void)?
20 |
21 | init(graph: BGGraph) {
22 | let bld: BGExtentBuilder = BGExtentBuilder(graph: graph)
23 | email = bld.state("")
24 | password = bld.state("")
25 | loginClick = bld.moment()
26 | emailValid = bld.state(false)
27 | passwordValid = bld.state(false)
28 | loginEnabled = bld.state(false)
29 | loggingIn = bld.state(false)
30 | loginComplete = bld.typedMoment()
31 |
32 |
33 | bld.behavior()
34 | .supplies([emailValid])
35 | .demands([email, bld.added])
36 | .runs { extent in
37 | extent.emailValid.update(LoginExtent.validEmailAddress(extent.email.value))
38 | extent.sideEffect {
39 | extent.loginForm?.emailFeedback.text = extent.emailValid.value ? "✅" : "❌"
40 | }
41 | }
42 |
43 | bld.behavior()
44 | .supplies([passwordValid])
45 | .demands([password, bld.added])
46 | .runs { extent in
47 | extent.passwordValid.update(extent.password.value.count > 0)
48 | extent.sideEffect {
49 | extent.loginForm?.passwordFeedback.text = extent.passwordValid.value ? "✅" : "❌"
50 | }
51 | }
52 |
53 | bld.behavior()
54 | .supplies([loginEnabled])
55 | .demands([emailValid, passwordValid, loggingIn, bld.added])
56 | .runs { extent in
57 | let enabled = extent.emailValid.value && extent.passwordValid.value && !extent.loggingIn.value;
58 | extent.loginEnabled.update(enabled)
59 | extent.sideEffect {
60 | extent.loginForm?.loginButton.isEnabled = extent.loginEnabled.value
61 | }
62 | }
63 |
64 | bld.behavior()
65 | .supplies([loggingIn])
66 | .demands([loginClick, loginComplete, bld.added])
67 | .runs { extent in
68 | if extent.loginClick.justUpdated() && extent.loginEnabled.traceValue {
69 | extent.loggingIn.update(true)
70 | } else if extent.loginComplete.justUpdated() && extent.loggingIn.value {
71 | extent.loggingIn.update(false)
72 | }
73 |
74 | if extent.loggingIn.justUpdated(to: true) {
75 | extent.sideEffect {
76 | extent.loginCall(email: extent.email.value, password: extent.password.value) { success in
77 |
78 | extent.graph.action {
79 | extent.loginComplete.update(success)
80 | }
81 |
82 | }
83 | }
84 | }
85 | }
86 |
87 | bld.behavior()
88 | .demands([loggingIn, loginComplete, bld.added])
89 | .runs { extent in
90 | extent.sideEffect {
91 | var status = ""
92 | if extent.loggingIn.value {
93 | status = "Logging in...";
94 | } else if let loginComplete = extent.loginComplete.updatedValue {
95 | if loginComplete {
96 | status = "Login Success"
97 | } else {
98 | status = "Login Failed"
99 | }
100 | }
101 | extent.loginForm?.loginStatus.text = status;
102 | }
103 | }
104 |
105 | super.init(builder: bld)
106 |
107 | }
108 |
109 | static func validEmailAddress(_ email: String) -> Bool {
110 | let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
111 | let pred = NSPredicate(format: "SELF matches %@", regex)
112 | return pred.evaluate(with: email)
113 | }
114 |
115 | func loginCall(email: String, password: String, complete: @escaping (Bool) -> Void) {
116 | self.savedLoginBlock = complete
117 | }
118 |
119 | func completeLogin(success: Bool) {
120 | self.savedLoginBlock?(success)
121 | self.savedLoginBlock = nil
122 | }
123 |
124 | }
125 |
--------------------------------------------------------------------------------
/Example/Tests/BGExtentTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import XCTest
6 | @testable import BGSwift
7 |
8 | class BGExtentTests: XCTestCase {
9 |
10 | var g: BGGraph!
11 | var b: BGExtentBuilder!
12 | var e: BGExtent!
13 | var rA: BGState!
14 | var rB: BGState!
15 | var rC: BGState!
16 |
17 |
18 | override func setUpWithError() throws {
19 | g = BGGraph()
20 | b = BGExtentBuilder(graph: g)
21 | rA = b.state(0)
22 | rB = b.state(0)
23 | rC = b.state(0)
24 | }
25 |
26 | override func tearDownWithError() throws {
27 | // Put teardown code here. This method is called after the invocation of each test method in the class.
28 | }
29 |
30 | func testAddedResourceIsUpdatedOnAdding() {
31 | // |> Given an extent
32 | var run = false
33 | var nonAddedRun = false
34 | b.behavior().demands([b.added]).runs { extent in
35 | run = true
36 | }
37 | b.behavior().demands([b.added]).runs { extent in
38 | run = true
39 | }
40 | b.behavior().runs { extent in
41 | nonAddedRun = true
42 | }
43 | e = BGExtent(builder: b)
44 |
45 | // |> When it is added
46 | e.addToGraphWithAction()
47 |
48 | // |> The added resource is updated
49 | XCTAssertTrue(run)
50 | XCTAssertFalse(nonAddedRun)
51 | }
52 |
53 | func testCanGetResourcesAndBehaviors() {
54 | b.behavior().runs { _ in }
55 | b.behavior().runs { _ in }
56 | e = BGExtent(builder: b)
57 |
58 | // |> When it is added
59 | e.addToGraphWithAction()
60 |
61 | // |> Then we can get the resources and behaviors
62 | XCTAssertEqual(4, e.allResources.count) // _added is a resource also
63 | XCTAssertEqual(2, e.allBehaviors.count)
64 | }
65 |
66 | func testCheckCannotAddExtentToGraphMultipleTimes() {
67 | // NOTE: This is primarily to prevent user error.
68 | // Perhaps it makes sense to remove/add extents in the future.
69 |
70 | // |> Given an extent added to a graph
71 | e = BGExtent(builder: b)
72 | e.addToGraphWithAction()
73 |
74 | // |> When it is added again
75 | // |> Then there is an error
76 | TestAssertionHit(graph: g) {
77 | e.addToGraphWithAction()
78 | }
79 | }
80 |
81 | func testCheckExtentCanOnlyBeAddedDuringEvent() {
82 | // |> Given an extent
83 | e = BGExtent(builder: b)
84 |
85 | // |> When added outside an event
86 | // |> Then there is an error
87 | TestAssertionHit(graph: g) {
88 | e.addToGraph()
89 | }
90 | }
91 |
92 | func testCheckExtentCanOnlyBeRemovedDuringEvent() {
93 | // |> Given an extent
94 | e = BGExtent(builder: b)
95 | e.addToGraphWithAction()
96 |
97 | // |> When added outside an event
98 | // |> Then there is an error
99 | TestAssertionHit(graph: g) {
100 | e.removeFromGraph()
101 | }
102 | }
103 |
104 | class MyExtent: BGExtent {
105 | let r1: BGMoment
106 |
107 | init(graph: BGGraph) {
108 | let b = BGExtentBuilder(graph: graph)
109 | r1 = b.moment()
110 | super.init(builder: b)
111 | }
112 | }
113 |
114 | func testResourcePropertiesGetNames() {
115 | let e2 = MyExtent(graph: self.g)
116 |
117 | XCTAssertEqual(e2.r1.debugName, "MyExtent.r1")
118 | }
119 |
120 | func testCanTrackWhenExtentsAreAdded() {
121 | // |> Given a graph with debugOnExtentAdded defined
122 | var addedExtents: [BGExtent] = []
123 | var addedResources: [any BGResource] = []
124 | g.debugOnExtentAdded = {
125 | addedExtents.append($0)
126 | addedResources.append(contentsOf: $0.allResources)
127 | }
128 |
129 | // |> When an extent is added
130 | let b1 = BGExtentBuilder(graph: g)
131 | let _ = b1.moment()
132 | let e1 = BGExtent(builder: b1)
133 |
134 | e1.addToGraphWithAction()
135 |
136 | // |> Then callback is called
137 | XCTAssertEqual(e1, addedExtents[0])
138 | XCTAssertEqual(1, addedExtents.count)
139 | XCTAssertEqual(2, addedResources.count) // _added is a default resource
140 |
141 | // |> And when callback is undefined
142 | g.debugOnExtentAdded = nil
143 |
144 | // and another extent is added
145 | let b2 = BGExtentBuilder(graph: g)
146 | let _ = b2.moment()
147 | let e2 = BGExtent(builder: b2)
148 |
149 | e2.addToGraphWithAction()
150 |
151 | // |> Then callback is not called
152 | XCTAssertEqual(1, addedExtents.count)
153 | XCTAssertEqual(2, addedResources.count)
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/Example/Tests/EventTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import XCTest
6 | import BGSwift
7 |
8 | class EventTests: XCTestCase {
9 |
10 | var g: BGGraph!
11 | var b: BGExtentBuilder!
12 | var e: BGExtent!
13 | var rA: BGState!
14 | var rB: BGState!
15 | var rC: BGState!
16 |
17 | override func setUpWithError() throws {
18 | g = BGGraph()
19 | b = BGExtentBuilder(graph: g)
20 | rA = b.state(0)
21 | rB = b.state(0)
22 | rC = b.state(0)
23 | }
24 |
25 | override func tearDownWithError() throws {
26 | // Put teardown code here. This method is called after the invocation of each test method in the class.
27 | }
28 |
29 | func testSideEffectsHappenAfterAllBehaviors() {
30 | // |> Given a behavior in the graph
31 | var counter = 0;
32 | var sideEffectCount: Int? = nil;
33 | b.behavior().demands([rA]).runs { extent in
34 | counter = counter + 1
35 | }
36 | e = BGExtent(builder: b)
37 | e.addToGraphWithAction()
38 |
39 | // |> When a sideEffect is created
40 | g.action {
41 | self.rA.update(1)
42 | self.g.sideEffect {
43 | sideEffectCount = counter
44 | }
45 | }
46 |
47 | // |> Then it will be run after all behaviors
48 | XCTAssertEqual(sideEffectCount, 1)
49 | }
50 |
51 | func testSideEffectsHappenInOrderTheyAreCreated() {
52 | // |> Given behaviors with side effects
53 | var runs: [Int] = []
54 | b.behavior().supplies([rB]).demands([rA]).runs { extent in
55 | self.rB.update(1)
56 | extent.sideEffect {
57 | runs.append(2)
58 | }
59 | }
60 | b.behavior().demands([rB]).runs { extent in
61 | extent.sideEffect {
62 | runs.append(1)
63 | }
64 | }
65 | e = BGExtent(builder: b)
66 | e.addToGraphWithAction()
67 |
68 | // |> When those behaviors are run
69 | rA.updateWithAction(1)
70 |
71 | // |> Then the sideEffects are run in the order they are created
72 | XCTAssertEqual(runs[0], 2)
73 | XCTAssertEqual(runs[1], 1)
74 | }
75 |
76 | func testTransientValuesAreAvailableDuringEffects() {
77 | // |> Given a behavior with side effects and a transient resource
78 | var valueAvailable = false
79 | var updatedAvailable = false
80 | let rX: BGTypedMoment = b.typedMoment()
81 | b.behavior().supplies([rX]).demands([rA]).runs { extent in
82 | rX.update(2)
83 | extent.sideEffect {
84 | valueAvailable = rX.updatedValue == 2
85 | updatedAvailable = rX.justUpdated()
86 | }
87 | }
88 | e = BGExtent(builder: b)
89 | e.addToGraphWithAction()
90 |
91 | // |> When that side effect is run
92 | g.action {
93 | self.rA.update(1)
94 | }
95 |
96 | // |> Then the value and updated state of that transient resource will be available
97 | XCTAssertTrue(valueAvailable)
98 | XCTAssertTrue(updatedAvailable)
99 | // and the value/updated state will not be available outside that event
100 | XCTAssertNil(rX.updatedValue)
101 | XCTAssertFalse(rX.justUpdated())
102 | }
103 |
104 | func testNestedActionsRunAfterSideEffects() {
105 | // |> Given a behavior with a side effect that creates a new event
106 | var counter = 0
107 | var effectCount: Int?
108 | var actionCount: Int?
109 | b.behavior().demands([rA]).runs { extent in
110 | self.e.sideEffect {
111 | self.g.action {
112 | actionCount = counter
113 | counter = counter + 1
114 | }
115 | }
116 | self.e.sideEffect {
117 | effectCount = counter
118 | counter = counter + 1
119 | }
120 | }
121 | e = BGExtent(builder: b)
122 | e.addToGraphWithAction()
123 |
124 | // |> When a nested chain of sideEffects is started
125 | rA.updateWithAction(1)
126 |
127 | // |> Then side effects are still run in order they were created
128 | XCTAssertEqual(effectCount, 0)
129 | XCTAssertEqual(actionCount, 1)
130 | }
131 |
132 | func testActionsAreSynchronousByDefault() {
133 | // |> Given a graph
134 | var counter = 0
135 |
136 | // |> When an action runs by default
137 | g.action {
138 | counter = counter + 1
139 | }
140 |
141 | // |> It will be run synchronously
142 | XCTAssertEqual(counter, 1)
143 | }
144 |
145 | func testSideEffectsMustBeCreatedInsideEvent() {
146 | // |> When a side effect is created outside of an event
147 | // |> Then an error will be raised
148 | TestAssertionHit(graph: g) {
149 | self.g.sideEffect {
150 | // nothing
151 | }
152 | }
153 | }
154 |
155 | func testDateProviderGivesAlternateTime() {
156 | g.dateProvider = {
157 | Date(timeIntervalSinceReferenceDate: 123)
158 | }
159 | var timeStamp: Date?
160 | g.action {
161 | timeStamp = self.g.currentEvent?.timestamp
162 | }
163 | XCTAssertEqual(timeStamp, Date(timeIntervalSinceReferenceDate: 123))
164 | }
165 | }
166 |
167 |
--------------------------------------------------------------------------------
/Example/Tests/BGMomentTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import XCTest
6 | import BGSwift
7 |
8 | class BGMomentTests: XCTestCase {
9 |
10 | var g: BGGraph!
11 | var bld: BGExtentBuilder!
12 | var ext: BGExtent!
13 |
14 | override func setUpWithError() throws {
15 | g = BGGraph()
16 | bld = BGExtentBuilder(graph: g)
17 | }
18 |
19 | override func tearDownWithError() throws {
20 | }
21 |
22 | func testMomentUpdates() throws {
23 | // |> Given a moment in the graph
24 | let mr1 = bld.moment()
25 | var afterUpdate = false;
26 | bld.behavior().demands([mr1]).runs { extent in
27 | if mr1.justUpdated() {
28 | afterUpdate = true
29 | }
30 | }
31 | ext = BGExtent(builder: bld)
32 | ext.addToGraphWithAction()
33 |
34 | // |> When it is read in the graph (and was not updated)
35 | var beforeUpdate = false
36 | var updateEvent: BGEvent? = nil
37 | g.action {
38 | beforeUpdate = mr1.justUpdated()
39 | mr1.update()
40 | updateEvent = self.g.currentEvent
41 | }
42 |
43 | // |> Then it didn't justUpdate before updating
44 | XCTAssertFalse(beforeUpdate)
45 |
46 | // |> And after
47 | // |> Then it did
48 | XCTAssertTrue(afterUpdate)
49 |
50 | // |> And outside an event
51 | // |> It is not just Updated
52 | XCTAssertFalse(mr1.justUpdated())
53 |
54 | // |> And event stays the same from when it last happened
55 | XCTAssertEqual(mr1.event, updateEvent)
56 | }
57 |
58 | func testTypedMomentsHaveInformation() {
59 | // Given a moment with data
60 | let mr1: BGTypedMoment = bld.typedMoment()
61 | var afterUpdate: Int? = nil
62 | bld.behavior().demands([mr1]).runs { extent in
63 | if mr1.justUpdated() {
64 | afterUpdate = mr1.updatedValue
65 | }
66 | }
67 | ext = BGExtent(builder: bld)
68 | ext.addToGraphWithAction()
69 |
70 | // |> When it happens
71 | mr1.updateWithAction(1)
72 |
73 | // |> Then the data is visible in subsequent behaviors
74 | XCTAssertEqual(afterUpdate, 1)
75 | }
76 |
77 | func testTypedMomentsAreTransient() {
78 | // NOTE this default prevents retaining data that no longer is needed
79 |
80 | class ClassType { }
81 |
82 | // |> Given a moment with data
83 | let mr1: BGTypedMoment = bld.typedMoment()
84 | ext = BGExtent(builder: bld)
85 | ext.addToGraphWithAction()
86 |
87 | // |> When current event is over
88 | weak var weakValue: ClassType?
89 | autoreleasepool {
90 | let value = ClassType()
91 | weakValue = value
92 | mr1.updateWithAction(value)
93 | }
94 |
95 | // |> Then value nils out and is not retained
96 | XCTAssertNil(mr1.updatedValue)
97 | XCTAssertNil(weakValue)
98 | }
99 |
100 | func testNonSuppliedMomentsCanUpdateBeforeAdding() {
101 | // |> Given a moment
102 | let mr1 = bld.moment()
103 | var didRun = false;
104 | bld.behavior().demands([mr1]).runs { extent in
105 | if mr1.justUpdated() {
106 | didRun = true
107 | }
108 | }
109 | ext = BGExtent(builder: bld)
110 |
111 | // |> When it is updated in the same event as adding to graph
112 | g.action {
113 | mr1.update()
114 | self.ext.addToGraph()
115 | }
116 |
117 | // |> Then it runs and demanding behavior is run
118 | XCTAssertTrue(didRun)
119 | }
120 |
121 | func testCheckUpdatingMomentNotInGraphIsANullOp() {
122 | // NOTE: Extent can be deallocated and removed from graph while
123 | // some pending resource update may exist. Doing so should just
124 | // do nothing.
125 |
126 | // |> Given a moment resource not part of the graph
127 | let mr1 = bld.moment()
128 |
129 | // |> When it is updated
130 | var errorHappened = false
131 | g.action {
132 | errorHappened = CheckAssertionHit(graph: self.g) {
133 | mr1.update()
134 | }
135 | }
136 |
137 | // |> Then nothing happens
138 | XCTAssertFalse(errorHappened)
139 | XCTAssertEqual(mr1.event, BGEvent.unknownPast)
140 | }
141 |
142 | func testCheckMomentUpdatesOnlyHappenDuringEvent() {
143 | // |> Given a moment in the graph
144 | let mr1 = bld.moment()
145 | ext = BGExtent(builder: bld)
146 | ext.addToGraphWithAction()
147 |
148 | // |> When updating outside of an event
149 | // |> Then it should fail
150 | TestAssertionHit(graph: g) {
151 | mr1.update()
152 | }
153 | }
154 |
155 | func testCheckMomentOnlyUpdatedBySupplier() {
156 | // |> Given a supplied moment
157 | let mr1 = bld.moment()
158 | let mr2 = bld.moment()
159 | bld.behavior().supplies([mr2]).demands([mr1]).runs { extent in
160 | }
161 | bld.behavior().demands([mr1]).runs { extent in
162 | if mr1.justUpdated() {
163 | mr2.update()
164 | }
165 | }
166 | ext = BGExtent(builder: bld)
167 | ext.addToGraphWithAction()
168 |
169 | // |> When it is updated by the wrong behavior
170 | // |> Then it should throw
171 | TestAssertionHit(graph: g) {
172 | self.g.action {
173 | mr1.update()
174 | }
175 | }
176 | }
177 |
178 | func testCheckUnsuppliedMomentOnlyUpdatedInAction() {
179 | // |> Given a supplied moment and unsupplied moment
180 | let mr1 = bld.moment()
181 | let mr2 = bld.moment()
182 | var updateFailed = false
183 | bld.behavior().demands([mr1]).runs { extent in
184 | updateFailed = CheckAssertionHit(graph: self.g) {
185 | mr2.update()
186 | }
187 | }
188 | ext = BGExtent(builder: bld)
189 | ext.addToGraphWithAction()
190 |
191 | // |> When the unsupplied moment is updated by a behavior
192 | g.action {
193 | mr1.update()
194 | }
195 |
196 | // |> Then it should throw
197 | XCTAssertTrue(updateFailed)
198 | }
199 |
200 | func testCheckSuppliedMomentCannotBeUpdatedInAction() {
201 | // |> Given a supplied moment
202 | let mr1 = bld.moment()
203 | bld.behavior().supplies([mr1]).demands([]).runs { extent in
204 | }
205 | ext = BGExtent(builder: bld)
206 | ext.addToGraphWithAction()
207 |
208 | // |> When we try updating that moment in an action
209 | var updateFailed = false
210 | self.g.action {
211 | updateFailed = CheckAssertionHit(graph: self.g) {
212 | mr1.update()
213 | }
214 | }
215 |
216 | // |> Then the updating chould throw
217 | XCTAssertTrue(updateFailed)
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/Code_of_Conduct.md:
--------------------------------------------------------------------------------
1 | # Yahoo Inc Open Source Code of Conduct
2 |
3 | ## Summary
4 | 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.
5 |
6 | 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.
7 |
8 | 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.
9 |
10 | ## Details
11 | This Code of Conduct makes our expectations of participants in this community explicit.
12 | * We forbid harassment and abusive speech within this community.
13 | * We request participants to report misconduct to the project’s Response Team.
14 | * We urge participants to refrain from using discussion forums to play out a fight.
15 |
16 | ### Expected Behaviors
17 | We expect participants in this community to conduct themselves professionally. Since our primary mode of communication is text on an online forum (e.g. issues, pull requests, comments, emails, or chats) devoid of vocal tone, gestures, or other context that is often vital to understanding, it is important that participants are attentive to their interaction style.
18 |
19 | * **Assume positive intent.** We ask community members to assume positive intent on the part of other people’s communications. We may disagree on details, but we expect all suggestions to be supportive of the community goals.
20 | * **Respect participants.** We expect occasional disagreements. Open Source projects are learning experiences. Ask, explore, challenge, and then _respectfully_ state if you agree or disagree. If your idea is rejected, be more persuasive not bitter.
21 | * **Welcoming to new members.** New members bring new perspectives. Some ask questions that have been addressed before. _Kindly_ point to existing discussions. Everyone is new to every project once.
22 | * **Be kind to beginners.** Beginners use open source projects to get experience. They might not be talented coders yet, and projects should not accept poor quality code. But we were all beginners once, and we need to engage kindly.
23 | * **Consider your impact on others.** Your work will be used by others, and you depend on the work of others. We expect community members to be considerate and establish a balance their self-interest with communal interest.
24 | * **Use words carefully.** We may not understand intent when you say something ironic. Often, people will misinterpret sarcasm in online communications. We ask community members to communicate plainly.
25 | * **Leave with class.** When you wish to resign from participating in this project for any reason, you are free to fork the code and create a competitive project. Open Source explicitly allows this. Your exit should not be dramatic or bitter.
26 |
27 | ### Unacceptable Behaviors
28 | Participants remain in good standing when they do not engage in misconduct or harassment (some examples follow). We do not list all forms of harassment, nor imply some forms of harassment are not worthy of action. Any participant who *feels* harassed or *observes* harassment, should report the incident to the Response Team.
29 | * **Don't be a bigot.** Calling out project members by their identity or background in a negative or insulting manner. This includes, but is not limited to, slurs or insinuations related to protected or suspect classes e.g. race, color, citizenship, national origin, political belief, religion, sexual orientation, gender identity and expression, age, size, culture, ethnicity, genetic features, language, profession, national minority status, mental or physical ability.
30 | * **Don't insult.** Insulting remarks about a person’s lifestyle practices.
31 | * **Don't dox.** Revealing private information about other participants without explicit permission.
32 | * **Don't intimidate.** Threats of violence or intimidation of any project member.
33 | * **Don't creep.** Unwanted sexual attention or content unsuited for the subject of this project.
34 | * **Don't inflame.** We ask that victim of harassment not address their grievances in the public forum, as this often intensifies the problem. Report it, and let us address it off-line.
35 | * **Don't disrupt.** Sustained disruptions in a discussion.
36 |
37 | ### Reporting Issues
38 | 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:
39 | * Your preferred contact information. We cannot process anonymous reports.
40 | * Names (real or usernames) of those involved in the incident.
41 | * 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.
42 | * Any additional information that may be helpful to achieve resolution.
43 |
44 | 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.
45 |
46 | ### Scope
47 | 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.
48 |
49 | 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.
50 |
51 | ## License and Acknowledgment.
52 | This text is shared under the [CC-BY-4.0 license](https://creativecommons.org/licenses/by/4.0/). This code is based on a study conducted by the [TODO Group](https://todogroup.org/) of many codes used in the open source community. If you have feedback about this code, contact our Response Team at the address listed above.
53 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/Example/Tests/ConcurrencyTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import XCTest
6 | @testable import BGSwift
7 |
8 | class ConcurrencyTests: XCTestCase {
9 |
10 | let SYNC_EXPECTATION_TIMEOUT = TimeInterval(0)
11 | let ASYNC_EXPECTATION_TIMEOUT = TimeInterval(0.1)
12 |
13 | func runSyncOnMain(_ code: () throws -> Void) rethrows {
14 | if Thread.isMainThread {
15 | try code()
16 | } else {
17 | try DispatchQueue.main.sync(execute: code)
18 | }
19 | }
20 |
21 | func testSyncActionRunInSideEffectRunsSynchronously() {
22 | let exp = XCTestExpectation()
23 |
24 | let g = BGGraph()
25 |
26 | DispatchQueue.global().async {
27 | XCTAssertFalse(Thread.isMainThread)
28 |
29 | g.action(syncStrategy: .sync) {
30 | g.sideEffect {
31 | var actionRan = false
32 | g.action(syncStrategy: .sync) {
33 | actionRan = true
34 | }
35 | if actionRan {
36 | exp.fulfill()
37 | }
38 | }
39 | }
40 | }
41 |
42 | wait(for: [exp], timeout: ASYNC_EXPECTATION_TIMEOUT)
43 | }
44 |
45 | func testSyncStrategyUnspecifiedActionRunInSideEffectRunsSynchronously() {
46 | let exp = XCTestExpectation()
47 |
48 | let g = BGGraph()
49 |
50 | DispatchQueue.global().async {
51 | XCTAssertFalse(Thread.isMainThread)
52 |
53 | g.action(syncStrategy: .sync) {
54 | g.sideEffect {
55 | var actionRan = false
56 | g.action(syncStrategy: nil) {
57 | actionRan = true
58 | }
59 | if actionRan {
60 | exp.fulfill()
61 | }
62 | }
63 | }
64 | }
65 |
66 | wait(for: [exp], timeout: ASYNC_EXPECTATION_TIMEOUT)
67 | }
68 |
69 | func testSyncActionRunWhenOtherActionInProgressWaitsAndRunsSynchronously() {
70 | //@SAL 9/6/2023 -- I'm having a hard time figuring out what scenario this test is trying to cover
71 | let innerActionRan = XCTestExpectation()
72 |
73 | let g = BGGraph()
74 |
75 | let sleepInterval = TimeInterval(0.1)
76 |
77 | var waitedForInnerAction: Bool? = nil
78 |
79 | // Begin execution onto a background thread so that both actions' threads are equal priortity
80 | DispatchQueue.global().async {
81 | g.action(syncStrategy: .sync) {
82 | // Begin second action on another thread
83 | DispatchQueue.global().async {
84 | var actionRan = false
85 |
86 | // Note that it is possible that the outer action completes before the inner
87 | // action attempts to run. In that case, the test will succeed even
88 | // if there was never any contention for the graph's mutex. Hopefully this
89 | // won't be the case if we choose a sufficient sleep interval.
90 | g.action(syncStrategy: .sync) {
91 | actionRan = true
92 | }
93 | waitedForInnerAction = actionRan
94 | // @SAL 9/6/2023 -- Moved this here because the action was running syncrhronously(as expected)
95 | // but as soon as innerActionRan was fulfilled, the test would end and the assertion
96 | // this may be because there are now always side effects (because deferred release clearing is a side effect)
97 | // So it now always synchronizes onto the main thread which gives it a chance for the wait below to be satisfied
98 | innerActionRan.fulfill()
99 | }
100 | }
101 | }
102 |
103 | wait(for: [innerActionRan], timeout: ASYNC_EXPECTATION_TIMEOUT + sleepInterval)
104 | XCTAssertEqual(waitedForInnerAction, true)
105 | }
106 |
107 | func testSyncStrategyUnspecifiedActionRunWhenOtherActionInProgressReturnsSynchronouslyAndSchedulesAsyncAction() {
108 | let innerActionRan = XCTestExpectation()
109 |
110 | let g = BGGraph()
111 |
112 | var waitedForInnerAction: Bool? = nil
113 |
114 | // Begin execution onto a background thread so that both actions' threads are equal priortity
115 | DispatchQueue.global().async {
116 | g.action(syncStrategy: .sync) {
117 | let semaphore = DispatchSemaphore(value: 0)
118 |
119 | // Begin second action on another thread
120 | DispatchQueue.global().async {
121 | var actionRan = false
122 | g.action(syncStrategy: nil) {
123 | actionRan = true
124 | innerActionRan.fulfill()
125 | }
126 | waitedForInnerAction = actionRan
127 |
128 | semaphore.signal()
129 | }
130 |
131 | // Wait until other action is scheduled
132 | semaphore.wait()
133 | }
134 | }
135 |
136 | wait(for: [innerActionRan], timeout: ASYNC_EXPECTATION_TIMEOUT)
137 | XCTAssertEqual(waitedForInnerAction, false)
138 | }
139 |
140 | func testSyncStrategyUnspecifiedActionCalledInAnotherActionIsRunAsyncOnSameDispatchQueueLoop() {
141 | let g = BGGraph()
142 |
143 | var actionOrder = [String]()
144 | g.action(impulse: "outer", syncStrategy: .sync) {
145 | g.action(impulse: "inner", syncStrategy: nil) {
146 | actionOrder.append(g.currentEvent!.impulse!)
147 | }
148 | actionOrder.append(g.currentEvent!.impulse!)
149 | }
150 | XCTAssertEqual(actionOrder, ["outer", "inner"])
151 | }
152 |
153 | func testSyncStrategyUnspecifiedActionCalledInBehaviorIsRunAsyncOnSameDispatchQueueLoop() {
154 | let g = BGGraph()
155 |
156 | var execOrder = [String]()
157 |
158 | let b = BGExtentBuilder(graph: g)
159 | b.behavior().demands([b.added]).runs { extent in
160 | g.action(impulse: "inner") {
161 | execOrder.append(g.currentEvent!.impulse!)
162 | }
163 | execOrder.append(g.currentEvent!.impulse!)
164 | }
165 |
166 | let e = BGExtent(builder: b)
167 | g.action(impulse: "outer") {
168 | e.addToGraph()
169 | }
170 | XCTAssertEqual(execOrder, ["outer", "inner"])
171 | }
172 |
173 | func testUpdatingResourceOfDeallocedExtentIsANoOp() {
174 | let g = BGGraph()
175 |
176 | let asyncActionFinished = XCTestExpectation()
177 |
178 | autoreleasepool {
179 | let b = BGExtentBuilder(graph: g)
180 | let r = b.state(false)
181 | let e = BGExtent(builder: b)
182 |
183 | g.action {
184 | e.addToGraph()
185 | }
186 |
187 | g.action(syncStrategy: .async(queue: DispatchQueue.global(qos: .background))) { [weak e] in
188 | r.update(true)
189 |
190 | XCTAssertNil(e)
191 | XCTAssertFalse(r.value)
192 | asyncActionFinished.fulfill()
193 | }
194 |
195 | // e deallocs before the async action is executed
196 | }
197 |
198 | wait(for: [asyncActionFinished], timeout: ASYNC_EXPECTATION_TIMEOUT)
199 | }
200 |
201 | func testSyncActionOnMainThreadWhileBackgroundActionWithSideEffectIsRunningCompletesSynchronously() {
202 | let g = BGGraph()
203 |
204 | var execOrder = [String]()
205 |
206 | let mutex = Mutex(recursive: false)
207 | func appendExecOrder(_ str: String) {
208 | mutex.balancedUnlock {
209 | execOrder.append(str)
210 | }
211 | }
212 |
213 | let workTime: TimeInterval = 1
214 |
215 | let backgroundActionRunning = XCTestExpectation()
216 | let backgroundActionCompleted = XCTestExpectation()
217 |
218 | g.action(syncStrategy: .async(queue: DispatchQueue.global(qos: .background))) {
219 | appendExecOrder("background-action-start")
220 | backgroundActionRunning.fulfill()
221 |
222 | // do work
223 | Thread.sleep(forTimeInterval: workTime)
224 | appendExecOrder("background-work-complete")
225 |
226 | g.sideEffect {
227 | appendExecOrder("se-background")
228 | }
229 |
230 | backgroundActionCompleted.fulfill()
231 | }
232 |
233 | wait(for: [backgroundActionRunning], timeout: ASYNC_EXPECTATION_TIMEOUT)
234 | appendExecOrder("creating-main-action")
235 | g.action(syncStrategy: .sync) {
236 | appendExecOrder("main-action-start")
237 | g.sideEffect {
238 | appendExecOrder("se-main")
239 | }
240 | }
241 | XCTAssertEqual(execOrder, ["background-action-start", "creating-main-action", "background-work-complete", "se-background", "main-action-start", "se-main"])
242 | }
243 |
244 | func testSyncNestedActionsDisallowed() {
245 | let g = BGGraph()
246 | TestAssertionHit(graph: g) {
247 | g.action {
248 | g.action(syncStrategy:.sync) {
249 | }
250 | }
251 | }
252 | }
253 |
254 | func testSyncActionsInBehaviorsDisallowed() {
255 | // NOTE: an action inside a behavior can essentially be considered a side effect by default
256 | // as long as we can delay that action until after the current event which is impossible
257 | // with a sync actions
258 |
259 | // |> Given a behavior in the graph that creates an action outside of a side effect
260 | let g = BGGraph()
261 | let b = BGExtentBuilder(graph: g)
262 | let r1 = b.moment()
263 | b.behavior().demands([r1]).runs { extent in
264 | g.action(syncStrategy:.sync) {
265 | // do something
266 | }
267 | // cant run because I may have code here that expected it to run
268 | }
269 | let e = BGExtent(builder: b)
270 | e.addToGraphWithAction()
271 |
272 | // |> When that behavior runs
273 | // |> Then it should throw an error
274 | TestAssertionHit(graph: g) {
275 | r1.updateWithAction()
276 | }
277 | }
278 |
279 | func testSyncActionsInSideEffectsAreAllowed() {
280 | // NOTE: This is currently allowed because its where one would expect actions to be created
281 | // however, technically the synchronous action could cut ahead of some pending side effects from
282 | // the existing event. So maybe that could be disallowed and forced into optional asynchrony or fixed?
283 |
284 | // |> Given a behavior in the graph that creates an action outside of a side effect
285 | let g = BGGraph()
286 | let b = BGExtentBuilder(graph: g)
287 | let r1 = b.moment()
288 | b.behavior().demands([r1]).runs { extent in
289 | extent.sideEffect {
290 | g.action(syncStrategy:.sync) {
291 | // do something
292 | }
293 | // cant run because I may have code here that expected it to run
294 | }
295 | extent.sideEffect {
296 | // run something else
297 | // technicallthe the action above is cutting ahead in line and changing state on us
298 | }
299 | }
300 | let e = BGExtent(builder: b)
301 | e.addToGraphWithAction()
302 |
303 | // |> When that behavior runs
304 | // |> Then is should be allowed?
305 | let failed = CheckAssertionHit(graph: g) {
306 | r1.updateWithAction()
307 | }
308 | XCTAssertFalse(failed)
309 | }
310 |
311 | func testBehaviorsRunOnMainThreadWhenConfigured() {
312 | let g = BGGraph()
313 | let b = BGExtentBuilder(graph: g)
314 | let r1 = b.moment()
315 |
316 | let backgroundWorkRan = XCTestExpectation()
317 |
318 | var behavior1RanOnMain: Bool?
319 | var behavior2RanOnMain: Bool?
320 |
321 | b.behavior()
322 | .demands(r1)
323 | .requiresMainThread()
324 | .runs { extent in
325 | behavior1RanOnMain = Thread.isMainThread
326 | }
327 |
328 | b.behavior()
329 | .demands(r1)
330 | .runs { extent in
331 | behavior2RanOnMain = Thread.isMainThread
332 | }
333 |
334 | let e = BGExtent(builder: b)
335 | e.addToGraphWithAction()
336 |
337 | DispatchQueue.global(qos: .default).async {
338 | g.action(syncStrategy: .sync) {
339 | r1.update()
340 | }
341 | backgroundWorkRan.fulfill()
342 | }
343 |
344 | wait(for: [backgroundWorkRan], timeout: ASYNC_EXPECTATION_TIMEOUT)
345 | XCTAssertEqual(behavior1RanOnMain, true)
346 | XCTAssertEqual(behavior2RanOnMain, false)
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/Example/BGSwift/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
66 |
74 |
82 |
90 |
97 |
98 |
99 |
100 |
101 |
102 |
109 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/BGSwift/Classes/BGResource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 |
7 | public protocol BGOptionalObject {
8 | associatedtype Wrapped: AnyObject
9 | var bg_unwrapped: Wrapped? { get }
10 | }
11 |
12 | extension Optional: BGOptionalObject where Wrapped: AnyObject {
13 | public var bg_unwrapped: Wrapped? {
14 | return self
15 | }
16 | }
17 |
18 | public protocol BGResource: AnyObject, BGDemandable, CustomDebugStringConvertible {
19 | var _event: BGEvent { get }
20 | var debugName: String? { get set }
21 | }
22 |
23 | protocol BGResourceInternal: AnyObject, BGResource {
24 | associatedtype ValueType
25 |
26 | var subsequents: Set { get set }
27 | var supplier: BGBehavior? { get set }
28 | var owner: BGExtent? { get set }
29 | var _prevEvent: BGEvent { get set }
30 | var _order: BGDemandable? { get set }
31 | var _event: BGEvent { get set }
32 |
33 | var _value: ValueType { get set }
34 | func shouldSkipUpdate(_ other: ValueType) -> Bool
35 |
36 | var deferClearingTransientValue: Bool { get }
37 | func clearTransientValue()
38 | }
39 |
40 | public extension BGResource {
41 | var event: BGEvent {
42 | asInternal.verifyDemands()
43 | return _event
44 | }
45 |
46 | var traceEvent: BGEvent {
47 | let asInternal = asInternal
48 | return _event.sequence == asInternal.graph?.currentEvent?.sequence ? asInternal._prevEvent : _event
49 | }
50 |
51 | var order: BGDemandable {
52 | let asInternal = asInternal
53 | if let order = asInternal._order {
54 | return order
55 | } else {
56 | let order = BGDemandLink(resource: asInternal, type: .order)
57 | asInternal._order = order
58 | return order
59 | }
60 | }
61 |
62 | func justUpdated() -> Bool {
63 | guard let currentEvent = asInternal.graph?.currentEvent else {
64 | return false
65 | }
66 |
67 | return currentEvent.sequence == event.sequence
68 | }
69 |
70 | func hasUpdated() -> Bool {
71 | event.sequence > BGEvent.unknownPast.sequence
72 | }
73 |
74 | func hasUpdatedSince(_ resource: BGResource?) -> Bool {
75 | event.happenedSince(sequence: resource?.event.sequence ?? 0)
76 | }
77 |
78 | func happenedSince(sequence: UInt) -> Bool {
79 | event.happenedSince(sequence: sequence)
80 | }
81 |
82 | var subsequentBehaviors: [BGBehavior] {
83 | asInternal.subsequents.compactMap { $0.behavior }
84 | }
85 | }
86 |
87 | internal extension BGResource {
88 | var asInternal: any BGResourceInternal { self as! (any BGResourceInternal) }
89 | }
90 |
91 | extension BGResourceInternal {
92 | var graph: BGGraph? { owner?.graph }
93 |
94 | func verifyDemands() {
95 | // TODO: compile out checks with build flag
96 | guard let graph = graph, graph.checkUndeclaredDemands, graph.currentThread == Thread.current else {
97 | return
98 | }
99 |
100 | if let currentBehavior = graph.currentRunningBehavior, currentBehavior !== supplier {
101 | graph.assert(currentBehavior.demands.first(where: { $0.resource === self }) != nil,
102 | "Accessed a resource in a behavior that was not declared as a demand.")
103 | }
104 | }
105 |
106 | var updateable: BGResourceUpdatable {
107 | guard let graph = self.graph else {
108 | // If graph is nil, then weak extent has been deallocated and resource updates are no-ops.
109 | return .notUpdatable
110 | }
111 |
112 | guard let currentEvent = graph.currentEvent else {
113 | graph.assertionFailure("Can only update a resource during an event.")
114 | return .notUpdatable
115 | }
116 |
117 | guard let owner = owner else {
118 | graph.assertionFailure("Cannot update a resource that does not belong to an extent.")
119 | return .notUpdatable
120 | }
121 |
122 | if let behavior = supplier {
123 |
124 | guard graph.currentRunningBehavior === behavior else {
125 | graph.assertionFailure("Cannot update a resource with a supplying behavior during an action.")
126 | return .notUpdatable
127 | }
128 | } else {
129 | if self !== owner._added {
130 | guard graph.processingAction else {
131 | graph.assertionFailure("Cannot update a resource with no supplying behaviour outside an action.")
132 | return .notUpdatable
133 | }
134 | }
135 | }
136 |
137 | guard _event.sequence < currentEvent.sequence else {
138 | // assert or fail?
139 | graph.assertionFailure("Can only update a resource once per event.")
140 | return .notUpdatable
141 | }
142 |
143 | return .updateable(graph: graph, currentEvent: currentEvent)
144 | }
145 |
146 | func _update(_ newValue: ValueType) {
147 | guard case .updateable(let graph, let event) = updateable else {
148 | return
149 | }
150 |
151 | if shouldSkipUpdate(newValue) {
152 | return
153 | }
154 |
155 | _prevEvent = _event;
156 |
157 | _value = newValue
158 | _event = event
159 |
160 | graph.updatedResources.append(self)
161 |
162 | if deferClearingTransientValue {
163 | graph.updatedTransientResources.append(self)
164 | } else {
165 | clearTransientValue()
166 | }
167 | }
168 |
169 | func _updateWithAction(_ newValue: ValueType, file: String, line: Int, function: String, syncStrategy: BGGraph.SynchronizationStrategy?) {
170 | graph?.action(file: file, line: line, function: function, syncStrategy: syncStrategy) {
171 | self._update(newValue)
172 | }
173 | }
174 |
175 | func _updateWithAction(_ newValue: ValueType, impulse: String, syncStrategy: BGGraph.SynchronizationStrategy?) {
176 | graph?.action(impulse: impulse, syncStrategy: syncStrategy) {
177 | self._update(newValue)
178 | }
179 | }
180 |
181 | var weakReference: WeakResource {
182 | WeakResource(self)
183 | }
184 |
185 | public func hash(into hasher: inout Hasher) {
186 | hasher.combine(ObjectIdentifier(self))
187 | }
188 | }
189 |
190 | struct WeakResource: Equatable, Hashable {
191 | weak var resource: (any BGResourceInternal)?
192 | let resourcePtr: ObjectIdentifier
193 |
194 | init(_ resource: any BGResourceInternal) {
195 | self.resource = resource
196 | resourcePtr = ObjectIdentifier(resource)
197 | }
198 |
199 | // MARK: Equatable
200 |
201 | static func == (lhs: WeakResource, rhs: WeakResource) -> Bool {
202 | guard let l = lhs.resource, let r = rhs.resource, l === r else {
203 | return false
204 | }
205 | return true
206 | }
207 |
208 | // MARK: Hashable
209 |
210 | func hash(into hasher: inout Hasher) {
211 | hasher.combine(resourcePtr)
212 | }
213 | }
214 |
215 | enum BGResourceUpdatable {
216 | case notUpdatable
217 | case updateable(graph: BGGraph, currentEvent: BGEvent)
218 | }
219 |
220 | public class BGMoment: BGResource, BGResourceInternal {
221 | typealias ValueType = Void
222 | var _value: ValueType = Void()
223 |
224 | public func update() {
225 | _update(Void())
226 | }
227 |
228 | public func updateWithAction(file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: BGGraph.SynchronizationStrategy? = nil) {
229 | _updateWithAction(Void(), file: file, line: line, function: function, syncStrategy: syncStrategy)
230 | }
231 |
232 | public func updateWithAction(impulse: String, syncStrategy: BGGraph.SynchronizationStrategy? = nil) {
233 | _updateWithAction(Void(), impulse: impulse, syncStrategy: syncStrategy)
234 | }
235 |
236 | // MARK: BGResource
237 |
238 | var _debugName: String?
239 | public var debugName: String? {
240 | get {
241 | if _debugName != nil {
242 | return _debugName
243 | } else {
244 | self.owner?.loadDebugNames()
245 | return _debugName
246 | }
247 | }
248 | set { _debugName = newValue }
249 | }
250 | public var _event: BGEvent = .unknownPast
251 |
252 | // MARK: BGResourceInternal
253 |
254 | var subsequents = Set()
255 | weak var supplier: BGBehavior?
256 | weak var owner: BGExtent?
257 | var _prevEvent: BGEvent = .unknownPast
258 | var _order: BGDemandable?
259 |
260 | func shouldSkipUpdate(_ other: Void) -> Bool { false }
261 | var deferClearingTransientValue: Bool { false }
262 | func clearTransientValue() {}
263 |
264 | // MARK: CustomDebugStringConvertible
265 |
266 | public var debugDescription: String {
267 | var updated = false
268 | if let currentEvent = graph?.currentEvent,
269 | currentEvent.sequence == _event.sequence {
270 | updated = true
271 | }
272 |
273 | return "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(debugName ?? "Unlabeled")), updated=\(updated)>"
274 | }
275 | }
276 |
277 | public class BGTypedMoment: BGResource, BGResourceInternal {
278 | public var updatedValue: Type? {
279 | verifyDemands()
280 | return _value
281 | }
282 |
283 | public func update(_ newValue: Type) {
284 | _update(newValue)
285 | }
286 |
287 | public func updateWithAction(_ newValue: Type, file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: BGGraph.SynchronizationStrategy? = nil) {
288 | _updateWithAction(newValue, file: file, line: line, function: function, syncStrategy: syncStrategy)
289 | }
290 |
291 | public func updateWithAction(_ newValue: Type, impulse: String, syncStrategy: BGGraph.SynchronizationStrategy? = nil) {
292 | _updateWithAction(newValue, impulse: impulse, syncStrategy: syncStrategy)
293 | }
294 |
295 | func shouldSkipUpdate(_ other: Type?) -> Bool { false }
296 | var deferClearingTransientValue: Bool { true }
297 |
298 | func clearTransientValue() {
299 | _value = nil
300 | }
301 |
302 | // MARK: BGResource
303 |
304 | var _debugName: String?
305 | public var debugName: String? {
306 | get {
307 | if _debugName != nil {
308 | return _debugName
309 | } else {
310 | self.owner?.loadDebugNames()
311 | return _debugName
312 | }
313 | }
314 | set { _debugName = newValue }
315 | }
316 | public var _event: BGEvent = .unknownPast
317 |
318 | // MARK: BGResourceInternal
319 |
320 | typealias ValueType = Type?
321 | var subsequents = Set()
322 | weak var supplier: BGBehavior?
323 | weak var owner: BGExtent?
324 | var _prevEvent: BGEvent = .unknownPast
325 | var _order: BGDemandable?
326 | public var _value: Type?
327 |
328 | // MARK: CustomDebugStringConvertible
329 |
330 | public var debugDescription: String {
331 | let updated: String
332 | if let value = _value {
333 | updated = " updatedValue=\(String(describing: value))"
334 | } else {
335 | updated = ""
336 | }
337 |
338 | return "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(debugName ?? "Unlabeled")\(updated)>"
339 | }
340 | }
341 |
342 | public enum BGStateComparison {
343 | public enum None { case none }
344 | public enum Equal { case equal }
345 | public enum Identical { case identical }
346 | }
347 |
348 | public class BGState: BGResource, BGResourceInternal {
349 | var _prevValue: Type?
350 | private var comparison: ((_ lhs: Type, _ rhs: Type) -> Bool)?
351 |
352 | internal init(_ value: Type, comparison: ((_ lhs: Type, _ rhs: Type) -> Bool)?) {
353 | self.comparison = comparison
354 | self._value = value
355 | self._prevValue = value
356 | }
357 |
358 | public var value: Type {
359 | verifyDemands()
360 | return _value
361 | }
362 |
363 | public var traceValue: Type {
364 | if let value = _prevValue {
365 | return value
366 | } else {
367 | return _value
368 | }
369 | }
370 |
371 | public var updatedValue: Type? {
372 | verifyDemands()
373 | return justUpdated() ? _value : nil
374 | }
375 |
376 | func valueEquals(_ other: Type) -> Bool {
377 | if let comparison = comparison {
378 | return comparison(_value, other)
379 | } else {
380 | return false
381 | }
382 | }
383 |
384 | func setInitialValue(_ value: Type) {
385 | graph?.assert(owner == nil || owner?.status == .inactive)
386 | _prevValue = value
387 | _value = value
388 | }
389 |
390 | public func update(_ newValue: Type, forced: Bool = false) {
391 | guard case .updateable(let graph, let event) = updateable else {
392 | return
393 | }
394 |
395 | if forced || !valueEquals(newValue) {
396 | _prevValue = _value;
397 | _prevEvent = _event;
398 |
399 | _value = newValue
400 | _event = event
401 |
402 | graph.updatedResources.append(self)
403 | graph.updatedTransientResources.append(self)
404 | }
405 | }
406 |
407 | public func updateWithAction(_ newValue: Type, file: String = #fileID, line: Int = #line, function: String = #function, syncStrategy: BGGraph.SynchronizationStrategy? = nil) {
408 | graph?.action(file: file, line: line, function: function, syncStrategy: syncStrategy) {
409 | self.update(newValue)
410 | }
411 | }
412 |
413 | public func updateWithAction(_ newValue: Type, impulse: String, syncStrategy: BGGraph.SynchronizationStrategy? = nil) {
414 | graph?.action(impulse: impulse, syncStrategy: syncStrategy) {
415 | self.update(newValue)
416 | }
417 | }
418 |
419 | public func justUpdated(to: Type) -> Bool {
420 | return justUpdated() && valueEquals(to)
421 | }
422 |
423 | public func justUpdated(from: Type) -> Bool {
424 | guard justUpdated(), let comparison = comparison else {
425 | return false
426 | }
427 | return comparison(traceValue, from)
428 | }
429 |
430 | public func justUpdated(to: Type, from: Type) -> Bool {
431 | guard justUpdated(), let comparison = comparison else {
432 | return false
433 | }
434 | return comparison(_value, to) && comparison(traceValue, from)
435 | }
436 |
437 | // MARK: BGResource
438 |
439 | var _debugName: String?
440 | public var debugName: String? {
441 | get {
442 | if _debugName != nil {
443 | return _debugName
444 | } else {
445 | self.owner?.loadDebugNames()
446 | return _debugName
447 | }
448 | }
449 | set { _debugName = newValue }
450 | }
451 | public var _event: BGEvent = .unknownPast
452 |
453 | // MARK: BGResourceInternal
454 |
455 | var subsequents = Set()
456 | weak var supplier: BGBehavior?
457 | weak var owner: BGExtent?
458 | var _prevEvent: BGEvent = .unknownPast
459 | var _order: BGDemandable?
460 | public var _value: Type
461 |
462 | func shouldSkipUpdate(_ other: Type) -> Bool {
463 | valueEquals(other)
464 | }
465 |
466 | var deferClearingTransientValue: Bool { _prevValue != nil }
467 |
468 | func clearTransientValue() {
469 | // Temporarily extend the lifetime of the value on the stack to avoid a crash
470 | // if clearing this value triggers dealloc code that in turn accesses
471 | // this value.
472 | withExtendedLifetime(_prevValue) {
473 | _prevValue = nil
474 | }
475 | }
476 |
477 | // MARK: CustomDebugStringConvertible
478 |
479 | public var debugDescription: String {
480 | let traceValue: String
481 | if let prevValue = _prevValue {
482 | traceValue = ", traceValue=\(String(describing: prevValue))"
483 | } else {
484 | traceValue = ""
485 | }
486 |
487 | return "<\(String(describing: Self.self)):\(String(format: "%018p", unsafeBitCast(self, to: Int64.self))) (\(debugName ?? "Unlabeled") value=\(String(describing: _value))\(traceValue)>"
488 | }
489 | }
490 |
491 | public protocol BGDemandable {}
492 |
493 | extension BGDemandable {
494 |
495 | var link: BGDemandLink {
496 | switch self {
497 | case let link as BGDemandLink:
498 | return link
499 | case let resource as any BGResourceInternal:
500 | return BGDemandLink(resource: resource, type: .reactive)
501 | default:
502 | preconditionFailure("Unknown `BGDemandable` type.")
503 | }
504 | }
505 | }
506 |
--------------------------------------------------------------------------------
/Example/Tests/BGStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 Yahoo
3 | //
4 |
5 | import Foundation
6 | import XCTest
7 | @testable import BGSwift
8 |
9 | class BGStateTests : XCTestCase {
10 |
11 | var g: BGGraph!
12 | var r_a, r_b: BGState!
13 | var bld: BGExtentBuilder!
14 | var ext: BGExtent!
15 |
16 | override func setUp() {
17 | g = BGGraph()
18 | bld = BGExtentBuilder(graph: g)
19 | r_a = bld.state(0)
20 | r_b = bld.state(0)
21 | }
22 |
23 | func testHasInitialState() {
24 | // |> When we create a new state resource
25 | // |> It has an initial value
26 | XCTAssertEqual(r_a.value, 0)
27 | }
28 |
29 | func testUpdatesWhenAddedToGraph() {
30 | ext = BGExtent(builder: bld)
31 | ext.addToGraphWithAction()
32 |
33 | // |> When it is updated
34 | r_a.updateWithAction(2)
35 |
36 | // |> Then it has new value and event
37 | XCTAssertEqual(r_a.value, 2)
38 | XCTAssertEqual(r_a.event, g.lastEvent)
39 | }
40 |
41 | func testCanHandleNullNilValues() {
42 | // Motivation: nullable states are useful for modeling false/true with data
43 |
44 | // |> Given a nullable state
45 | let r_n: BGState = bld.state(nil)
46 | ext = BGExtent(builder: bld)
47 | ext.addToGraphWithAction()
48 |
49 | // |> When updated
50 | r_n.updateWithAction(1)
51 |
52 | // |> Then it will have that new state
53 | XCTAssertEqual(r_n.value, 1)
54 |
55 | // |> And when updated to nil
56 | r_n.updateWithAction(nil)
57 |
58 | // |> Then it will have nil state
59 | XCTAssertNil(r_n.value)
60 | }
61 |
62 | func testWorksAsDemandAndSupply() {
63 | // |> Given state resources and behaviors
64 | var ran = false;
65 | bld.behavior().supplies([r_b]).demands([r_a]).runs { extent in
66 | self.r_b.update(self.r_a.value)
67 | }
68 |
69 | bld.behavior().demands([r_b]).runs { extent in
70 | ran = true
71 | }
72 | ext = BGExtent(builder: bld)
73 | ext.addToGraphWithAction()
74 |
75 | // |> When event is started
76 | r_a.updateWithAction(1)
77 |
78 | // |> Then subsequent behaviors are run
79 | XCTAssertEqual(r_b.value, 1)
80 | XCTAssertEqual(ran, true)
81 | }
82 |
83 | func testJustUpdatedChecksDuringEvent() {
84 | var updatedA = false
85 | var updatedB = false
86 | var updatedToA = false
87 | var updatedToWrongToA = false
88 | var updatedToB = false
89 | var updatedToFromA = false
90 | var updatedToFromWrongToA = false
91 | var updatedToFromWrongFromA = false
92 | var updatedToFromB = false
93 | var updatedFromA = false
94 | var updatedFromWrongFromA = false
95 | var updatedFromB = false
96 |
97 | // |> Given a behavior that tracks updated methods
98 | bld.behavior().demands([r_a, r_b]).runs { extent in
99 | updatedA = self.r_a.justUpdated()
100 | updatedB = self.r_b.justUpdated()
101 | updatedToA = self.r_a.justUpdated(to: 1)
102 | updatedToWrongToA = self.r_a.justUpdated(to: 2)
103 | updatedToB = self.r_b.justUpdated(to: 1)
104 | updatedToFromA = self.r_a.justUpdated(to: 1, from: 0)
105 | updatedToFromWrongToA = self.r_a.justUpdated(to: 2, from: 0)
106 | updatedToFromWrongFromA = self.r_a.justUpdated(to: 1, from: 2)
107 | updatedToFromB = self.r_b.justUpdated(to: 1, from: 0)
108 | updatedFromA = self.r_a.justUpdated(from: 0)
109 | updatedFromWrongFromA = self.r_a.justUpdated(from: 2)
110 | updatedFromB = self.r_b.justUpdated(from: 0)
111 | }
112 | ext = BGExtent(builder: bld)
113 | ext.addToGraphWithAction()
114 |
115 | // |> When r_a updates
116 | r_a.updateWithAction(1)
117 |
118 | // |> Then updates are tracked inside behavior
119 | XCTAssertEqual(updatedA, true)
120 | XCTAssertEqual(r_a.justUpdated(), false) // false outside event
121 | XCTAssertEqual(updatedB, false) // not updated
122 | XCTAssertEqual(updatedToA, true)
123 | XCTAssertEqual(r_a.justUpdated(to: 1), false)
124 | XCTAssertEqual(updatedToWrongToA, false)
125 | XCTAssertEqual(updatedToB, false)
126 | XCTAssertEqual(updatedToFromA, true)
127 | XCTAssertEqual(r_a.justUpdated(to: 1, from: 0), false)
128 | XCTAssertEqual(updatedToFromWrongToA, false)
129 | XCTAssertEqual(updatedToFromWrongFromA, false)
130 | XCTAssertEqual(updatedToFromB, false)
131 | XCTAssertEqual(updatedFromA, true)
132 | XCTAssertEqual(r_a.justUpdated(from: 0), false)
133 | XCTAssertEqual(updatedFromWrongFromA, false)
134 | XCTAssertEqual(updatedFromB, false)
135 | }
136 |
137 | func testCanAccessTraceValuesAndEvents() {
138 | var beforeValue: Int? = nil
139 | var beforeEvent: BGEvent? = nil
140 |
141 | // |> Given a behavior that accesses trace
142 | bld.behavior().demands([r_a]).runs { extent in
143 | beforeValue = self.r_a.traceValue
144 | beforeEvent = self.r_a.traceEvent
145 | }
146 | ext = BGExtent(builder: bld)
147 | ext.addToGraphWithAction()
148 |
149 | // |> When resource is updated
150 | r_a.updateWithAction(1)
151 |
152 | // |> Trace captures original state during
153 | XCTAssertEqual(beforeValue, 0)
154 | XCTAssertEqual(beforeEvent, BGEvent.unknownPast)
155 | // and current state outside event
156 | XCTAssertEqual(r_a.traceValue, 1)
157 | XCTAssertEqual(r_a.traceEvent, g.lastEvent)
158 | }
159 |
160 | func testCanUpdateResourceDuringSameEventAsAdding() {
161 | // @SAL this doesn't work yet, can't add and update in the same event
162 | var didRun = false
163 | bld.behavior().demands([r_a]).runs { extent in
164 | didRun = true
165 | }
166 | ext = BGExtent(builder: bld)
167 |
168 | self.g.action {
169 | self.r_a.update(1)
170 | self.ext.addToGraph()
171 | }
172 |
173 | XCTAssertEqual(r_a.value, 1)
174 | XCTAssertEqual(didRun, true)
175 | }
176 |
177 | // @SAL 8/16/2025 -- these quickspec tests werent running under xcodebuild, so this
178 | // wasn't failing. Moving to spm tests caused it to run and fail.
179 | // An ability to update a resource before extent is added to graph may be a desired
180 | // behavior, but it is not currently supported.
181 | // it("can update resource before extent is added to graph") {
182 | // //ext = BGExtent(builder: bld)
183 | //
184 | // r_a.update(1)
185 | //
186 | // expect(ext.status) == .inactive
187 | // expect(r_a.value) == 1
188 | // }
189 |
190 | func testCanUpdateStateBeforeAdding() {
191 | // |> Given a state resource that hasn't been added
192 | let r = bld.state(0)
193 | ext = BGExtent(builder: bld)
194 |
195 | // |> When we update
196 | r.setInitialValue(1)
197 | // |> Then it's value is the new initial value
198 | XCTAssertEqual(r.value, 1)
199 | XCTAssertEqual(r._prevValue, 1)
200 | }
201 |
202 | func testCanUpdateStateBeforeBuildingExtent() {
203 | // |> Given a state resource that hasn't been added
204 | let r = bld.state(0)
205 |
206 | // |> When we update
207 | r.setInitialValue(1)
208 | // |> Then it's value is the new initial value
209 | XCTAssertEqual(r.value, 1)
210 | XCTAssertEqual(r._prevValue, 1)
211 | }
212 |
213 | func testCannotSetInitialValueAfterAdding() {
214 | // |> Given a state that has been added
215 | let r = bld.state(0)
216 | ext = BGExtent(builder: bld)
217 | ext.addToGraphWithAction()
218 |
219 | // |> when we try to set initial value
220 | // |> then it will assert
221 | TestAssertionHit(graph: g) {
222 | r.setInitialValue(1)
223 | }
224 | }
225 |
226 | func testEnsuresCanUpdateChecksAreRun() {
227 | // NOTE: there are multiple canUpdate checks, this just ensures that
228 | // code path is followed
229 | bld.behavior().demands([r_a]).runs { extent in
230 | self.r_b.update(self.r_a.value)
231 | }
232 | ext = BGExtent(builder: bld)
233 | ext.addToGraphWithAction()
234 |
235 | TestAssertionHit(graph: g) {
236 | r_a.updateWithAction(1)
237 | }
238 | }
239 |
240 | }
241 |
242 | fileprivate struct Struct {}
243 | fileprivate class Object {}
244 |
245 | fileprivate class EquatableObject: Equatable {
246 | var failEquality = false
247 |
248 | static func == (lhs: EquatableObject, rhs: EquatableObject) -> Bool {
249 | !lhs.failEquality && !rhs.failEquality
250 | }
251 | }
252 |
253 | class BGStateUpdateTests: XCTestCase {
254 |
255 | let g = BGGraph()
256 | var b: BGExtentBuilder!
257 |
258 | override func setUp() {
259 | b = BGExtentBuilder(graph: g)
260 | }
261 |
262 | override func tearDown() {
263 | g.debugOnAssertionFailure = nil
264 | }
265 |
266 | // MARK: Non-Equatable, Non-Object
267 |
268 | func testNonEquatableStruct_default() {
269 | // default is .none
270 |
271 | let s = b.state(Struct())
272 |
273 | let extent = BGExtent(builder: b)
274 |
275 | self.g.action {
276 | extent.addToGraph()
277 |
278 | s.update(s.value)
279 | XCTAssertTrue(s.justUpdated())
280 | }
281 | }
282 |
283 | func testNonEquatableStruct_noCheck() {
284 | let s = b.state(Struct(), comparison: .none)
285 |
286 | let extent = BGExtent(builder: b)
287 |
288 | self.g.action {
289 | extent.addToGraph()
290 |
291 | s.update(s.value)
292 | XCTAssertTrue(s.justUpdated())
293 | }
294 | }
295 |
296 | // MARK: Equatable, Non-Object
297 |
298 | func testEquatable_default() {
299 | // default is .equal
300 |
301 | let s = b.state("foo")
302 |
303 | let extent = BGExtent(builder: b)
304 |
305 | self.g.action {
306 | extent.addToGraph()
307 |
308 | s.update("foo")
309 | XCTAssertFalse(s.justUpdated())
310 | }
311 |
312 | self.g.action {
313 | s.update("bar")
314 | XCTAssertTrue(s.justUpdated())
315 | }
316 | }
317 |
318 | func testEquatable_noCheck() {
319 | let s = b.state("foo", comparison: .none)
320 |
321 | let extent = BGExtent(builder: b)
322 |
323 | self.g.action {
324 | extent.addToGraph()
325 |
326 | s.update(s.value)
327 | XCTAssertTrue(s.justUpdated())
328 | }
329 | }
330 |
331 | func testEquatable_equals() {
332 | let s = b.state("foo", comparison: .equal)
333 |
334 | let extent = BGExtent(builder: b)
335 |
336 | self.g.action {
337 | extent.addToGraph()
338 |
339 | // s.valueEquality("foo", equalityCheck: .equal)
340 | s.update("foo")
341 | XCTAssertFalse(s.justUpdated())
342 | }
343 |
344 | self.g.action {
345 | s.update("bar")
346 | XCTAssertTrue(s.justUpdated())
347 | }
348 | }
349 |
350 | func testEquatable_forced() {
351 | let s = b.state("foo", comparison: .equal)
352 |
353 | let extent = BGExtent(builder: b)
354 |
355 | self.g.action {
356 | extent.addToGraph()
357 |
358 | s.update("foo")
359 | XCTAssertFalse(s.justUpdated())
360 | }
361 |
362 | self.g.action {
363 | s.update("foo", forced: true)
364 | XCTAssertTrue(s.justUpdated())
365 | }
366 | }
367 |
368 | // MARK: Non-Equatable, Object
369 |
370 | func testObject_default() {
371 | // default is .identical
372 |
373 | let s = b.state(Object())
374 |
375 | let extent = BGExtent(builder: b)
376 |
377 | self.g.action {
378 | extent.addToGraph()
379 |
380 | s.update(s.value)
381 | XCTAssertFalse(s.justUpdated())
382 | }
383 |
384 | self.g.action {
385 | s.update(Object())
386 | XCTAssertTrue(s.justUpdated())
387 | }
388 | }
389 |
390 | func testObject_noCheck() {
391 | let s = b.state(Object(), comparison: .none)
392 |
393 | let extent = BGExtent(builder: b)
394 |
395 | self.g.action {
396 | extent.addToGraph()
397 |
398 | s.update(s.value)
399 | XCTAssertTrue(s.justUpdated())
400 | }
401 | }
402 |
403 | func testObject_indentical() {
404 | let s = b.state(Object(), comparison: .identical)
405 |
406 | let extent = BGExtent(builder: b)
407 |
408 | self.g.action {
409 | extent.addToGraph()
410 |
411 | s.update(s.value)
412 | XCTAssertFalse(s.justUpdated())
413 | }
414 |
415 | self.g.action {
416 | s.update(Object())
417 | XCTAssertTrue(s.justUpdated())
418 | }
419 | }
420 |
421 | // MARK: Equatable, Object
422 |
423 | func testEquatableObject_default() {
424 | // default is .equal
425 |
426 | let obj = EquatableObject()
427 |
428 | let s = b.state(obj)
429 |
430 | let extent = BGExtent(builder: b)
431 |
432 | self.g.action {
433 | extent.addToGraph()
434 |
435 | s.update(obj)
436 | XCTAssertFalse(s.justUpdated())
437 | }
438 |
439 | obj.failEquality = true
440 | self.g.action {
441 | s.update(obj)
442 | XCTAssertTrue(s.justUpdated())
443 | }
444 | }
445 |
446 | func testEquatableObject_noCheck() {
447 | let s = b.state(EquatableObject(), comparison: .none)
448 |
449 | let extent = BGExtent(builder: b)
450 |
451 | self.g.action {
452 | extent.addToGraph()
453 |
454 | s.update(s.value)
455 | XCTAssertTrue(s.justUpdated())
456 | }
457 | }
458 |
459 | func testEquatableObject_equal() {
460 | let obj = EquatableObject()
461 |
462 | let s = b.state(obj, comparison: .equal)
463 |
464 | let extent = BGExtent(builder: b)
465 |
466 | self.g.action {
467 | extent.addToGraph()
468 |
469 | s.update(obj)
470 | XCTAssertFalse(s.justUpdated())
471 | }
472 |
473 | obj.failEquality = true
474 | self.g.action {
475 | s.update(obj)
476 | XCTAssertTrue(s.justUpdated())
477 | }
478 | }
479 |
480 | func testEquatableObject_indentical() {
481 | let s = b.state(EquatableObject(), comparison: .identical)
482 |
483 | let extent = BGExtent(builder: b)
484 |
485 | self.g.action {
486 | extent.addToGraph()
487 |
488 | s.update(s.value)
489 | XCTAssertFalse(s.justUpdated())
490 | }
491 |
492 | self.g.action {
493 | s.update(EquatableObject())
494 | XCTAssertTrue(s.justUpdated())
495 | }
496 | }
497 |
498 | func testEquatableObject_forced() {
499 | let obj = EquatableObject()
500 | obj.failEquality = false
501 |
502 | let s = b.state(obj, comparison: .equal)
503 |
504 | let extent = BGExtent(builder: b)
505 |
506 | self.g.action {
507 | extent.addToGraph()
508 |
509 | s.update(obj)
510 | XCTAssertFalse(s.justUpdated())
511 | }
512 |
513 | self.g.action {
514 | s.update(obj, forced: true)
515 | XCTAssertTrue(s.justUpdated())
516 | }
517 | }
518 |
519 | // MARK: Optional, Non-Equatable, Object
520 |
521 | func testOptionalObject_default() {
522 | // default is .identical
523 |
524 | let s: BGState