From 8ab5b3b57abe9de0d03bc6b655cd4b85f636e14c Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Mon, 6 Jun 2022 13:14:46 +0200 Subject: [PATCH] Start & stop network follower from SessionView --- Example/example-app/Package.swift | 5 + .../example-app/Sources/AppFeature/App.swift | 4 +- .../SessionFeature/SessionFeature.swift | 72 ++++++++++- .../Sources/SessionFeature/SessionView.swift | 47 ++++++- .../SessionFeatureTests.swift | 118 +++++++++++++++++- 5 files changed, 235 insertions(+), 11 deletions(-) diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift index 6277f619..55d54626 100644 --- a/Example/example-app/Package.swift +++ b/Example/example-app/Package.swift @@ -135,10 +135,15 @@ let package = Package( .target( name: "SessionFeature", dependencies: [ + .target(name: "ErrorFeature"), .product( name: "ComposableArchitecture", package: "swift-composable-architecture" ), + .product( + name: "ComposablePresentation", + package: "swift-composable-presentation" + ), .product( name: "ElixxirDAppsSDK", package: "elixxir-dapps-sdk-swift" diff --git a/Example/example-app/Sources/AppFeature/App.swift b/Example/example-app/Sources/AppFeature/App.swift index ed136fb1..341cb46c 100644 --- a/Example/example-app/Sources/AppFeature/App.swift +++ b/Example/example-app/Sources/AppFeature/App.swift @@ -42,7 +42,9 @@ extension AppEnvironment { error: ErrorEnvironment() ), session: SessionEnvironment( - getClient: { clientSubject.value } + getClient: { clientSubject.value }, + bgScheduler: bgScheduler, + mainScheduler: mainScheduler ) ) } diff --git a/Example/example-app/Sources/SessionFeature/SessionFeature.swift b/Example/example-app/Sources/SessionFeature/SessionFeature.swift index 2e0d9c96..b07a14cc 100644 --- a/Example/example-app/Sources/SessionFeature/SessionFeature.swift +++ b/Example/example-app/Sources/SessionFeature/SessionFeature.swift @@ -1,34 +1,98 @@ +import Combine import ComposableArchitecture import ElixxirDAppsSDK +import ErrorFeature public struct SessionState: Equatable { public init( - id: UUID + id: UUID, + networkFollowerStatus: NetworkFollowerStatus? = nil, + error: ErrorState? = nil ) { self.id = id + self.networkFollowerStatus = networkFollowerStatus + self.error = error } public var id: UUID + public var networkFollowerStatus: NetworkFollowerStatus? + public var error: ErrorState? } public enum SessionAction: Equatable { case viewDidLoad + case updateNetworkFollowerStatus + case didUpdateNetworkFollowerStatus(NetworkFollowerStatus?) + case runNetworkFollower(Bool) + case networkFollowerDidFail(NSError) + case error(ErrorAction) + case didDismissError } public struct SessionEnvironment { public init( - getClient: @escaping () -> Client? + getClient: @escaping () -> Client?, + bgScheduler: AnySchedulerOf<DispatchQueue>, + mainScheduler: AnySchedulerOf<DispatchQueue> ) { self.getClient = getClient + self.bgScheduler = bgScheduler + self.mainScheduler = mainScheduler } public var getClient: () -> Client? + public var bgScheduler: AnySchedulerOf<DispatchQueue> + public var mainScheduler: AnySchedulerOf<DispatchQueue> } public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironment> { state, action, env in switch action { case .viewDidLoad: + return .merge([ + .init(value: .updateNetworkFollowerStatus), + ]) + + case .updateNetworkFollowerStatus: + return Effect.future { fulfill in + let status = env.getClient()?.networkFollower.status() + fulfill(.success(.didUpdateNetworkFollowerStatus(status))) + } + .subscribe(on: env.bgScheduler) + .receive(on: env.mainScheduler) + .eraseToEffect() + + case .didUpdateNetworkFollowerStatus(let status): + state.networkFollowerStatus = status + return .none + + case .runNetworkFollower(let start): + state.networkFollowerStatus = start ? .starting : .stopping + return Effect.run { subscriber in + do { + if start { + try env.getClient()?.networkFollower.start(timeoutMS: 30_000) + } else { + try env.getClient()?.networkFollower.stop() + } + } catch { + subscriber.send(.networkFollowerDidFail(error as NSError)) + } + let status = env.getClient()?.networkFollower.status() + subscriber.send(.didUpdateNetworkFollowerStatus(status)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgScheduler) + .receive(on: env.mainScheduler) + .eraseToEffect() + + case .networkFollowerDidFail(let error): + state.error = ErrorState(error: error) + return .none + + case .didDismissError: + state.error = nil return .none } } @@ -36,7 +100,9 @@ public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironm #if DEBUG extension SessionEnvironment { public static let failing = SessionEnvironment( - getClient: { .failing } + getClient: { .failing }, + bgScheduler: .failing, + mainScheduler: .failing ) } #endif diff --git a/Example/example-app/Sources/SessionFeature/SessionView.swift b/Example/example-app/Sources/SessionFeature/SessionView.swift index cbf8934e..74b4d130 100644 --- a/Example/example-app/Sources/SessionFeature/SessionView.swift +++ b/Example/example-app/Sources/SessionFeature/SessionView.swift @@ -1,4 +1,7 @@ import ComposableArchitecture +import ComposablePresentation +import ElixxirDAppsSDK +import ErrorFeature import SwiftUI public struct SessionView: View { @@ -9,16 +12,50 @@ public struct SessionView: View { let store: Store<SessionState, SessionAction> struct ViewState: Equatable { - init(state: SessionState) {} + let networkFollowerStatus: NetworkFollowerStatus? + + init(state: SessionState) { + networkFollowerStatus = state.networkFollowerStatus + } } public var body: some View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in - Text("SessionView") - .navigationTitle("Session") - .task { - viewStore.send(.viewDidLoad) + Form { + Section { + NetworkFollowerStatusView(status: viewStore.networkFollowerStatus) + + Button { + viewStore.send(.runNetworkFollower(true)) + } label: { + Text("Start") + } + .disabled(viewStore.networkFollowerStatus != .stopped) + + Button { + viewStore.send(.runNetworkFollower(false)) + } label: { + Text("Stop") + } + .disabled(viewStore.networkFollowerStatus != .running) + } header: { + Text("Network follower") } + } + .navigationTitle("Session") + .task { + viewStore.send(.viewDidLoad) + } + .sheet( + store.scope( + state: \.error, + action: SessionAction.error + ), + onDismiss: { + viewStore.send(.didDismissError) + }, + content: ErrorView.init(store:) + ) } } } diff --git a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift index 1e13e0d1..092573ed 100644 --- a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift +++ b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift @@ -1,15 +1,129 @@ import ComposableArchitecture +import ElixxirDAppsSDK +import ErrorFeature import XCTest @testable import SessionFeature final class SessionFeatureTests: XCTestCase { - func testViewDidLoad() throws { + func testViewDidLoad() { + var networkFollowerStatus: NetworkFollowerStatus! + let bgScheduler = DispatchQueue.test + let mainScheduler = DispatchQueue.test + + var env = SessionEnvironment.failing + env.getClient = { + var client = Client.failing + client.networkFollower.status.status = { networkFollowerStatus } + return client + } + env.bgScheduler = bgScheduler.eraseToAnyScheduler() + env.mainScheduler = mainScheduler.eraseToAnyScheduler() + let store = TestStore( initialState: SessionState(id: UUID()), reducer: sessionReducer, - environment: .failing + environment: env ) store.send(.viewDidLoad) + + store.receive(.updateNetworkFollowerStatus) + + networkFollowerStatus = .stopped + bgScheduler.advance() + mainScheduler.advance() + + store.receive(.didUpdateNetworkFollowerStatus(.stopped)) { + $0.networkFollowerStatus = .stopped + } + } + + func testStartStopNetworkFollower() { + var networkFollowerStatus: NetworkFollowerStatus! + var didStartNetworkFollowerWithTimeout = [Int]() + var didStopNetworkFollower = 0 + var networkFollowerStartError: NSError? + let bgScheduler = DispatchQueue.test + let mainScheduler = DispatchQueue.test + + var env = SessionEnvironment.failing + env.getClient = { + var client = Client.failing + client.networkFollower.status.status = { + networkFollowerStatus + } + client.networkFollower.start.start = { + didStartNetworkFollowerWithTimeout.append($0) + if let error = networkFollowerStartError { + throw error + } + } + client.networkFollower.stop.stop = { + didStopNetworkFollower += 1 + } + return client + } + env.bgScheduler = bgScheduler.eraseToAnyScheduler() + env.mainScheduler = mainScheduler.eraseToAnyScheduler() + + let store = TestStore( + initialState: SessionState(id: UUID()), + reducer: sessionReducer, + environment: env + ) + + store.send(.runNetworkFollower(true)) { + $0.networkFollowerStatus = .starting + } + + networkFollowerStatus = .running + bgScheduler.advance() + mainScheduler.advance() + + XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000]) + XCTAssertEqual(didStopNetworkFollower, 0) + + store.receive(.didUpdateNetworkFollowerStatus(.running)) { + $0.networkFollowerStatus = .running + } + + store.send(.runNetworkFollower(false)) { + $0.networkFollowerStatus = .stopping + } + + networkFollowerStatus = .stopped + bgScheduler.advance() + mainScheduler.advance() + + XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000]) + XCTAssertEqual(didStopNetworkFollower, 1) + + store.receive(.didUpdateNetworkFollowerStatus(.stopped)) { + $0.networkFollowerStatus = .stopped + } + + store.send(.runNetworkFollower(true)) { + $0.networkFollowerStatus = .starting + } + + networkFollowerStartError = NSError(domain: "test", code: 1234) + networkFollowerStatus = .stopped + bgScheduler.advance() + mainScheduler.advance() + + XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000, 30_000]) + XCTAssertEqual(didStopNetworkFollower, 1) + + store.receive(.networkFollowerDidFail(networkFollowerStartError!)) { + $0.error = ErrorState(error: networkFollowerStartError!) + } + + store.receive(.didUpdateNetworkFollowerStatus(.stopped)) { + $0.networkFollowerStatus = .stopped + } + + store.send(.didDismissError) { + $0.error = nil + } } } -- GitLab