From efb3ba76e4324c3caf2a960e3c48e23e4133df16 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Mon, 22 Aug 2022 19:23:08 +0100
Subject: [PATCH] Add MessengerWaitForNodes functor

---
 .../Functors/MessengerWaitForNodes.swift      |  57 +++++++++
 .../Messenger/Messenger.swift                 |   7 +-
 .../Messenger/MessengerEnvironment.swift      |   3 +
 .../Functors/MessengerWaitForNodesTests.swift | 108 ++++++++++++++++++
 4 files changed, 173 insertions(+), 2 deletions(-)
 create mode 100644 Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift
 create mode 100644 Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift

diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift
new file mode 100644
index 00000000..39048a37
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift
@@ -0,0 +1,57 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerWaitForNodes {
+  public typealias Progress = (Double) -> Void
+
+  public enum Error: Swift.Error {
+    case notLoaded
+    case timeout
+  }
+
+  public var run: (Double, Int, Int, @escaping Progress) throws -> Void
+
+  public func callAsFunction(
+    targetRatio: Double = 0.8,
+    sleepMS: Int = 1_000,
+    retries: Int = 10,
+    onProgress: @escaping Progress = { _ in }
+  ) throws {
+    try run(targetRatio, sleepMS, retries, onProgress)
+  }
+}
+
+extension MessengerWaitForNodes {
+  public static func live(_ env: MessengerEnvironment) -> MessengerWaitForNodes {
+    MessengerWaitForNodes { targetRatio, sleepMS, retries, onProgress in
+      guard let cMix = env.ctx.getCMix() else {
+        throw Error.notLoaded
+      }
+
+      func getProgress(_ report: NodeRegistrationReport) -> Double {
+        min(1, ((report.ratio / targetRatio) * 100).rounded() / 100)
+      }
+
+      var report = try cMix.getNodeRegistrationStatus()
+      var retries = retries
+      onProgress(getProgress(report))
+
+      while report.ratio < targetRatio && retries > 0 {
+        env.sleep(sleepMS)
+        retries -= 1
+        report = try cMix.getNodeRegistrationStatus()
+        onProgress(getProgress(report))
+      }
+
+      if report.ratio < targetRatio {
+        throw Error.timeout
+      }
+    }
+  }
+}
+
+extension MessengerWaitForNodes {
+  public static let unimplemented = MessengerWaitForNodes(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift
index 959aff7f..5a442ac9 100644
--- a/Sources/XXMessengerClient/Messenger/Messenger.swift
+++ b/Sources/XXMessengerClient/Messenger/Messenger.swift
@@ -16,6 +16,7 @@ public struct Messenger {
   public var isLoggedIn: MessengerIsLoggedIn
   public var logIn: MessengerLogIn
   public var waitForNetwork: MessengerWaitForNetwork
+  public var waitForNodes: MessengerWaitForNodes
 }
 
 extension Messenger {
@@ -35,7 +36,8 @@ extension Messenger {
       register: .live(env),
       isLoggedIn: .live(env),
       logIn: .live(env),
-      waitForNetwork: .live(env)
+      waitForNetwork: .live(env),
+      waitForNodes: .live(env)
     )
   }
 }
@@ -56,6 +58,7 @@ extension Messenger {
     register: .unimplemented,
     isLoggedIn: .unimplemented,
     logIn: .unimplemented,
-    waitForNetwork: .unimplemented
+    waitForNetwork: .unimplemented,
+    waitForNodes: .unimplemented
   )
 }
diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
index b3009ae0..eb7646f3 100644
--- a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
+++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
@@ -16,6 +16,7 @@ public struct MessengerEnvironment {
   public var newCMix: NewCMix
   public var newOrLoadUd: NewOrLoadUd
   public var passwordStorage: PasswordStorage
+  public var sleep: (Int) -> Void
   public var storageDir: String
   public var udAddress: String?
   public var udCert: Data?
@@ -44,6 +45,7 @@ extension MessengerEnvironment {
       newCMix: .live,
       newOrLoadUd: .live,
       passwordStorage: .keychain,
+      sleep: { Foundation.sleep(UInt32($0)) },
       storageDir: MessengerEnvironment.defaultStorageDir,
       udAddress: nil,
       udCert: nil,
@@ -67,6 +69,7 @@ extension MessengerEnvironment {
     newCMix: .unimplemented,
     newOrLoadUd: .unimplemented,
     passwordStorage: .unimplemented,
+    sleep: XCTUnimplemented("\(Self.self).sleep"),
     storageDir: "unimplemented",
     udAddress: nil,
     udCert: nil,
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift
new file mode 100644
index 00000000..3154758f
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift
@@ -0,0 +1,108 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerWaitForNodesTests: XCTestCase {
+  func testWaitWhenNotLoaded() {
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.getCMix = { nil }
+    let waitForNodes: MessengerWaitForNodes = .live(env)
+
+    XCTAssertThrowsError(try waitForNodes()) { error in
+      XCTAssertEqual(
+        error as? MessengerWaitForNodes.Error,
+        MessengerWaitForNodes.Error.notLoaded
+      )
+    }
+  }
+
+  func testWaitWhenHasTargetRatio() throws {
+    var didProgress: [Double] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.getCMix = {
+      var cMix: CMix = .unimplemented
+      cMix.getNodeRegistrationStatus.run = {
+        NodeRegistrationReport(registered: 8, total: 10)
+      }
+      return cMix
+    }
+    let waitForNodes: MessengerWaitForNodes = .live(env)
+
+    try waitForNodes(
+      targetRatio: 0.7,
+      sleepMS: 123,
+      retries: 3,
+      onProgress: { didProgress.append($0) }
+    )
+
+    XCTAssertNoDifference(didProgress, [1])
+  }
+
+  func testWaitForTargetRatio() throws {
+    var didSleep: [Int] = []
+    var didProgress: [Double] = []
+
+    var reports: [NodeRegistrationReport] = [
+      .init(registered: 0, total: 10),
+      .init(registered: 3, total: 10),
+      .init(registered: 8, total: 10),
+    ]
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.getCMix = {
+      var cMix: CMix = .unimplemented
+      cMix.getNodeRegistrationStatus.run = { reports.removeFirst() }
+      return cMix
+    }
+    env.sleep = { didSleep.append($0) }
+    let waitForNodes: MessengerWaitForNodes = .live(env)
+
+    try waitForNodes(
+      targetRatio: 0.7,
+      sleepMS: 123,
+      retries: 3,
+      onProgress: { didProgress.append($0) }
+    )
+
+    XCTAssertNoDifference(didSleep, [123, 123])
+    XCTAssertNoDifference(didProgress, [0, 0.43, 1])
+  }
+
+  func testWaitTimeout() {
+    var didSleep: [Int] = []
+    var didProgress: [Double] = []
+
+    var reports: [NodeRegistrationReport] = [
+      .init(registered: 0, total: 10),
+      .init(registered: 3, total: 10),
+      .init(registered: 5, total: 10),
+      .init(registered: 6, total: 10),
+    ]
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.getCMix = {
+      var cMix: CMix = .unimplemented
+      cMix.getNodeRegistrationStatus.run = { reports.removeFirst() }
+      return cMix
+    }
+    env.sleep = { didSleep.append($0) }
+    let waitForNodes: MessengerWaitForNodes = .live(env)
+
+    XCTAssertThrowsError(try waitForNodes(
+      targetRatio: 0.7,
+      sleepMS: 123,
+      retries: 3,
+      onProgress: { didProgress.append($0) }
+    )) { error in
+      XCTAssertEqual(
+        error as? MessengerWaitForNodes.Error,
+        MessengerWaitForNodes.Error.timeout
+      )
+    }
+
+    XCTAssertNoDifference(didSleep, [123, 123, 123])
+    XCTAssertNoDifference(didProgress, [0, 0.43, 0.71, 0.86])
+  }
+}
-- 
GitLab