diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3f149901..f8cef871 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -158,7 +158,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "c3b5f4ebee758b619aa81e844d791aa1fd35b918" + "revision" : "6a53c9e32520b46f8c70597e27b335105fabfc21" } } ], diff --git a/Package.resolved b/Package.resolved index 70f89c77..f964897f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -113,7 +113,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "c3b5f4ebee758b619aa81e844d791aa1fd35b918" + "revision" : "6a53c9e32520b46f8c70597e27b335105fabfc21" } } ], diff --git a/Package.swift b/Package.swift index 5080fc3e..57e97800 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.14.0"), .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1"), - .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "c3b5f4ebee758b619aa81e844d791aa1fd35b918") + .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "6a53c9e32520b46f8c70597e27b335105fabfc21") ], targets: [ .target( diff --git a/Sources/ZcashLightClientKit/Block/Actions/Action.swift b/Sources/ZcashLightClientKit/Block/Actions/Action.swift index 3ab0d7ee..b6f07782 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/Action.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/Action.swift @@ -7,7 +7,32 @@ import Foundation -actor ActionContext { +protocol ActionContext { + var state: CBPState { get async } + var prevState: CBPState? { get async } + var syncControlData: SyncControlData { get async } + var preferredSyncAlgorithm: SyncAlgorithm { get } + var supportedSyncAlgorithm: SyncAlgorithm? { get async } + var requestedRewindHeight: BlockHeight? { get async } + var totalProgressRange: CompactBlockRange { get async } + var processedHeight: BlockHeight { get async } + var lastChainTipUpdateTime: TimeInterval { get async } + var lastScannedHeight: BlockHeight? { get async } + var lastEnhancedHeight: BlockHeight? { get async } + + func update(state: CBPState) async + func update(syncControlData: SyncControlData) async + func update(totalProgressRange: CompactBlockRange) async + func update(processedHeight: BlockHeight) async + func update(lastChainTipUpdateTime: TimeInterval) async + func update(lastScannedHeight: BlockHeight) async + func update(lastDownloadedHeight: BlockHeight) async + func update(lastEnhancedHeight: BlockHeight?) async + func update(supportedSyncAlgorithm: SyncAlgorithm) async + func update(requestedRewindHeight: BlockHeight) async +} + +actor ActionContextImpl: ActionContext { var state: CBPState var prevState: CBPState? var syncControlData: SyncControlData diff --git a/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift index d43f8155..98a45caf 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift @@ -20,6 +20,7 @@ extension ClearCacheAction: Action { func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { try await storage.clear() + if await context.prevState == .idle { await context.update(state: .migrateLegacyCacheDB) } else { @@ -37,6 +38,7 @@ extension ClearCacheAction: Action { } } } + return context } diff --git a/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift b/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift index 2caface9..0ed1654f 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift @@ -50,6 +50,7 @@ extension DownloadAction: Action { let downloadLimit = await context.syncControlData.latestBlockHeight >= potentialDownloadLimit ? potentialDownloadLimit : batchRangeEnd logger.debug("Starting download with range: \(batchRange.lowerBound)...\(batchRange.upperBound)") + await downloader.update(latestDownloadedBlockHeight: batchRange.lowerBound, force: true) // SbS try await downloader.setSyncRange(lastScannedHeight...latestBlockHeight, batchSize: config.batchSize) await downloader.setDownloadLimit(downloadLimit) diff --git a/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift b/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift index d6d6460d..35bc2ac2 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift @@ -11,13 +11,11 @@ final class EnhanceAction { let blockEnhancer: BlockEnhancer let configProvider: CompactBlockProcessor.ConfigProvider let logger: Logger - let transactionRepository: TransactionRepository init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) { blockEnhancer = container.resolve(BlockEnhancer.self) self.configProvider = configProvider logger = container.resolve(Logger.self) - transactionRepository = container.resolve(TransactionRepository.self) } func decideWhatToDoNext(context: ActionContext, lastScannedHeight: BlockHeight) async -> ActionContext { diff --git a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift index 706e2ea0..5a36104c 100644 --- a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift +++ b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift @@ -193,7 +193,7 @@ actor CompactBlockProcessor { ) let configProvider = ConfigProvider(config: config) - context = ActionContext(state: .idle, preferredSyncAlgorithm: config.syncAlgorithm) + context = ActionContextImpl(state: .idle, preferredSyncAlgorithm: config.syncAlgorithm) actions = Self.makeActions(container: container, configProvider: configProvider) self.metrics = container.resolve(SDKMetrics.self) @@ -638,7 +638,7 @@ extension CompactBlockProcessor { private func resetContext() async { let lastEnhancedheight = await context.lastEnhancedHeight - context = ActionContext(state: .idle, preferredSyncAlgorithm: config.syncAlgorithm) + context = ActionContextImpl(state: .idle, preferredSyncAlgorithm: config.syncAlgorithm) await context.update(lastEnhancedHeight: lastEnhancedheight) await compactBlockProgress.reset() } diff --git a/Tests/NetworkTests/BlockStreamingTest.swift b/Tests/NetworkTests/BlockStreamingTest.swift index 72bcbbf5..33d0d731 100644 --- a/Tests/NetworkTests/BlockStreamingTest.swift +++ b/Tests/NetworkTests/BlockStreamingTest.swift @@ -140,7 +140,7 @@ class BlockStreamingTest: ZcashTestCase { latestScannedHeight: startHeight, firstUnenhancedHeight: nil ) - let context = ActionContext(state: .download) + let context = ActionContextMock() await context.update(syncControlData: syncControlData) let expectation = XCTestExpectation() @@ -175,7 +175,7 @@ class BlockStreamingTest: ZcashTestCase { latestScannedHeight: startHeight, firstUnenhancedHeight: nil ) - let context = ActionContext(state: .download) + let context = ActionContextMock() await context.update(syncControlData: syncControlData) let date = Date() diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ActionContextStateTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ActionContextStateTests.swift index 7aca605c..8e81e7ce 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/ActionContextStateTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ActionContextStateTests.swift @@ -11,7 +11,7 @@ import XCTest final class ActionContextStateTests: XCTestCase { func testPreviousState() async throws { - let syncContext: ActionContext = .init(state: .idle) + let syncContext = ActionContextImpl(state: .idle) await syncContext.update(state: .clearCache) @@ -32,4 +32,15 @@ final class ActionContextStateTests: XCTestCase { XCTFail("syncContext.prevState is not expected to be nil.") } } + + func testDefaultSyncAlgorith() async throws { + let syncContext = ActionContextImpl(state: .idle) + + let preferredSyncAlgorithm = await syncContext.preferredSyncAlgorithm + + XCTAssertTrue( + preferredSyncAlgorithm == .linear, + "ActionContext default preferredSyncAlgorithm is expected to be .linear but received \(preferredSyncAlgorithm)" + ) + } } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift index 17475c37..aee9ca50 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift @@ -12,6 +12,48 @@ import XCTest final class ClearAlreadyScannedBlocksActionTests: ZcashTestCase { func testClearAlreadyScannedBlocksAction_NextAction() async throws { let compactBlockRepositoryMock = CompactBlockRepositoryMock() + + let clearAlreadyScannedBlocksAction = setupAction(compactBlockRepositoryMock) + + do { + let context = ActionContextMock.default() + context.lastScannedHeight = -1 + + let nextContext = try await clearAlreadyScannedBlocksAction.run(with: context) { _ in } + + XCTAssertTrue(compactBlockRepositoryMock.clearUpToCalled, "storage.clear(upTo:) is expected to be called.") + + let acResult = nextContext.checkStateIs(.enhance) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testClearAlreadyScannedBlocksAction_NextAction is not expected to fail. \(error)") + } + } + + func testClearAlreadyScannedBlocksAction_LastScanHeightZcashError() async throws { + let clearAlreadyScannedBlocksAction = setupAction() + + do { + let context = ActionContextMock() + + _ = try await clearAlreadyScannedBlocksAction.run(with: context) { _ in } + + XCTFail("testClearAlreadyScannedBlocksAction_LastScanHeightZcashError should throw an error so fail here is unexpected.") + } catch ZcashError.compactBlockProcessorLastScannedHeight { + // it's expected to end up here because we test that error is a specific one and Swift automatically catched it up for us + } catch { + XCTFail( + """ + testClearAlreadyScannedBlocksAction_NextAction is expected to fail + with ZcashError.compactBlockProcessorLastScannedHeight but received \(error) + """ + ) + } + } + + private func setupAction( + _ compactBlockRepositoryMock: CompactBlockRepositoryMock = CompactBlockRepositoryMock() + ) -> ClearAlreadyScannedBlocksAction { let transactionRepositoryMock = TransactionRepositoryMock() compactBlockRepositoryMock.clearUpToClosure = { _ in } @@ -19,24 +61,9 @@ final class ClearAlreadyScannedBlocksActionTests: ZcashTestCase { mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock } mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock } - - let clearAlreadyScannedBlocksAction = ClearAlreadyScannedBlocksAction( + + return ClearAlreadyScannedBlocksAction( container: mockContainer ) - - do { - let context = ActionContext(state: .clearAlreadyScannedBlocks) - await context.update(lastScannedHeight: -1) - - let nextContext = try await clearAlreadyScannedBlocksAction.run(with: context) { _ in } - XCTAssertTrue(compactBlockRepositoryMock.clearUpToCalled, "storage.clear(upTo:) is expected to be called.") - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .enhance, - "nextContext after .clearAlreadyScannedBlocks is expected to be .enhance but received \(nextState)" - ) - } catch { - XCTFail("testClearAlreadyScannedBlocksAction_NextAction is not expected to fail. \(error)") - } } } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ClearCacheActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ClearCacheActionTests.swift index e7fc9f6f..b789cf30 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/ClearCacheActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ClearCacheActionTests.swift @@ -10,27 +10,119 @@ import XCTest @testable import ZcashLightClientKit final class ClearCacheActionTests: ZcashTestCase { - func testClearCacheAction_NextAction() async throws { + func testClearCacheAction_MigrationLegacyCacheDB() async throws { let compactBlockRepositoryMock = CompactBlockRepositoryMock() + let clearCacheAction = setupAction(compactBlockRepositoryMock) + + do { + let context = ActionContextMock.default() + context.prevState = .idle + + let nextContext = try await clearCacheAction.run(with: context) { _ in } + + XCTAssertTrue(compactBlockRepositoryMock.clearCalled, "storage.clear() is expected to be called.") + + let acResult = nextContext.checkStateIs(.migrateLegacyCacheDB) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testClearCacheAction_MigrationLegacyCacheDB is not expected to fail. \(error)") + } + } + + func testClearCacheAction_FinishedLinear() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + + let clearCacheAction = setupAction(compactBlockRepositoryMock) + + do { + let context = ActionContextMock.default() + context.underlyingPreferredSyncAlgorithm = .linear + + let nextContext = try await clearCacheAction.run(with: context) { _ in } + + XCTAssertTrue(compactBlockRepositoryMock.clearCalled, "storage.clear() is expected to be called.") + + let acResult = nextContext.checkStateIs(.finished) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testClearCacheAction_FinishedLinear is not expected to fail. \(error)") + } + } + + func testClearCacheAction_PreferredSbSNoSupportedSyncAlgorithm() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + + let clearCacheAction = setupAction(compactBlockRepositoryMock) + + do { + let context = ActionContextMock.default() + context.underlyingPreferredSyncAlgorithm = .spendBeforeSync + + _ = try await clearCacheAction.run(with: context) { _ in } + } catch ZcashError.compactBlockProcessorSupportedSyncAlgorithm { + XCTAssertTrue(compactBlockRepositoryMock.clearCalled, "storage.clear() is expected to be called.") + } catch { + XCTFail( + """ + testClearCacheAction_PredferedSbSNoSupportedSyncAlgorithm is expected to fail + with ZcashError.compactBlockProcessorSupportedSyncAlgorithm but received \(error) + """ + ) + } + } + + func testClearCacheAction_PreferredSbSSupportedSyncAlgorithmLinear() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + + let clearCacheAction = setupAction(compactBlockRepositoryMock) + + do { + let context = ActionContextMock.default() + context.underlyingPreferredSyncAlgorithm = .spendBeforeSync + context.supportedSyncAlgorithm = .linear + + let nextContext = try await clearCacheAction.run(with: context) { _ in } + + XCTAssertTrue(compactBlockRepositoryMock.clearCalled, "storage.clear() is expected to be called.") + + let acResult = nextContext.checkStateIs(.finished) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testClearCacheAction_FinishedLinear is not expected to fail. \(error)") + } + } + + func testClearCacheAction_PreferedSbSSupportedSyncAlgorithmSbS() async throws { + let compactBlockRepositoryMock = CompactBlockRepositoryMock() + + let clearCacheAction = setupAction(compactBlockRepositoryMock) + + do { + let context = ActionContextMock.default() + context.underlyingPreferredSyncAlgorithm = .spendBeforeSync + context.supportedSyncAlgorithm = .spendBeforeSync + + let nextContext = try await clearCacheAction.run(with: context) { _ in } + + XCTAssertTrue(compactBlockRepositoryMock.clearCalled, "storage.clear() is expected to be called.") + + let acResult = nextContext.checkStateIs(.processSuggestedScanRanges) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testClearCacheAction_FinishedLinear is not expected to fail. \(error)") + } + } + + private func setupAction( + _ compactBlockRepositoryMock: CompactBlockRepositoryMock = CompactBlockRepositoryMock() + ) -> ClearCacheAction { compactBlockRepositoryMock.clearClosure = { } mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock } - let clearCacheAction = ClearCacheAction( + return ClearCacheAction( container: mockContainer ) - - do { - let nextContext = try await clearCacheAction.run(with: .init(state: .clearCache)) { _ in } - XCTAssertTrue(compactBlockRepositoryMock.clearCalled, "storage.clear() is expected to be called.") - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .finished, - "nextContext after .clearCache is expected to be .finished but received \(nextState)" - ) - } catch { - XCTFail("testClearCacheAction_NextAction is not expected to fail. \(error)") - } } } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ComputeSyncControlDataActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ComputeSyncControlDataActionTests.swift index bb9bb47d..40620ec5 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/ComputeSyncControlDataActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ComputeSyncControlDataActionTests.swift @@ -30,32 +30,24 @@ final class ComputeSyncControlDataActionTests: ZcashTestCase { latestBlocksDataProviderMock, loggerMock ) + latestBlocksDataProviderMock.underlyingLatestBlockHeight = 123 + latestBlocksDataProviderMock.underlyingLatestScannedHeight = 123 let syncContext = await setupActionContext() do { let nextContext = try await computeSyncControlDataAction.run(with: syncContext) { _ in } - XCTAssertTrue( - latestBlocksDataProviderMock.updateScannedDataCalled, - "latestBlocksDataProvider.updateScannedData() is expected to be called." - ) - XCTAssertTrue( - latestBlocksDataProviderMock.updateBlockDataCalled, - "latestBlocksDataProvider.updateBlockData() is expected to be called." - ) - - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .finished, - "nextContext after .computeSyncControlData is expected to be .finished but received \(nextState)" - ) + checkLatestBlocksDataProvider(latestBlocksDataProviderMock) + checkActionContext(nextContext, expectedNextState: .finished) + + XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug() is expected to be called.") } catch { XCTFail("testComputeSyncControlDataAction_finishProcessingCase is not expected to fail. \(error)") } } - func testComputeSyncControlDataAction_fetchUTXOsCase() async throws { + func testComputeSyncControlDataAction_DownloadCase() async throws { let blockDownloaderServiceMock = BlockDownloaderServiceMock() let latestBlocksDataProviderMock = LatestBlocksDataProviderMock() let loggerMock = LoggerMock() @@ -65,45 +57,51 @@ final class ComputeSyncControlDataActionTests: ZcashTestCase { latestBlocksDataProviderMock, loggerMock ) - latestBlocksDataProviderMock.underlyingLatestBlockHeight = 10 + latestBlocksDataProviderMock.underlyingLatestBlockHeight = 1234 + latestBlocksDataProviderMock.underlyingLatestScannedHeight = 123 let syncContext = await setupActionContext() do { let nextContext = try await computeSyncControlDataAction.run(with: syncContext) { _ in } - XCTAssertTrue( - latestBlocksDataProviderMock.updateScannedDataCalled, - "latestBlocksDataProvider.updateScannedData() is expected to be called." - ) - XCTAssertTrue(latestBlocksDataProviderMock.updateBlockDataCalled, "latestBlocksDataProvider.updateBlockData() is expected to be called.") - XCTAssertFalse(loggerMock.infoFileFunctionLineCalled, "logger.info() is not expected to be called.") + checkLatestBlocksDataProvider(latestBlocksDataProviderMock) + checkActionContext(nextContext, expectedNextState: .download) - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .download, - "nextContext after .computeSyncControlData is expected to be .download but received \(nextState)" - ) + XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug() is expected to be called.") } catch { XCTFail("testComputeSyncControlDataAction_checksBeforeSyncCase is not expected to fail. \(error)") } } - private func setupSyncControlData() -> SyncControlData { - SyncControlData( - latestBlockHeight: 0, - latestScannedHeight: underlyingScanRange?.lowerBound, - firstUnenhancedHeight: nil - ) + private func setupActionContext() async -> ActionContextMock { + let syncContext = ActionContextMock() + + syncContext.updateLastScannedHeightClosure = { _ in } + syncContext.updateLastDownloadedHeightClosure = { _ in } + syncContext.updateSyncControlDataClosure = { _ in } + syncContext.updateTotalProgressRangeClosure = { _ in } + syncContext.updateStateClosure = { _ in } + syncContext.underlyingState = .idle + + return syncContext } - private func setupActionContext() async -> ActionContext { - let syncContext: ActionContext = .init(state: .computeSyncControlData) + private func setupDefaultMocksAndReturnAction( + _ blockDownloaderServiceMock: BlockDownloaderServiceMock = BlockDownloaderServiceMock(), + _ latestBlocksDataProviderMock: LatestBlocksDataProviderMock = LatestBlocksDataProviderMock(), + _ loggerMock: LoggerMock = LoggerMock() + ) -> ComputeSyncControlDataAction { + latestBlocksDataProviderMock.updateScannedDataClosure = { } + latestBlocksDataProviderMock.updateBlockDataClosure = { } + latestBlocksDataProviderMock.updateUnenhancedDataClosure = { } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } - await syncContext.update(syncControlData: setupSyncControlData()) - await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000))) - - return syncContext + return setupAction( + blockDownloaderServiceMock, + latestBlocksDataProviderMock, + loggerMock + ) } private func setupAction( @@ -125,23 +123,44 @@ final class ComputeSyncControlDataActionTests: ZcashTestCase { ) } - private func setupDefaultMocksAndReturnAction( - _ blockDownloaderServiceMock: BlockDownloaderServiceMock = BlockDownloaderServiceMock(), - _ latestBlocksDataProviderMock: LatestBlocksDataProviderMock = LatestBlocksDataProviderMock(), - _ loggerMock: LoggerMock = LoggerMock() - ) -> ComputeSyncControlDataAction { - blockDownloaderServiceMock.lastDownloadedBlockHeightReturnValue = 1 - latestBlocksDataProviderMock.underlyingLatestBlockHeight = 1 - latestBlocksDataProviderMock.underlyingLatestScannedHeight = 1 - latestBlocksDataProviderMock.updateScannedDataClosure = { } - latestBlocksDataProviderMock.updateBlockDataClosure = { } - latestBlocksDataProviderMock.updateUnenhancedDataClosure = { } - loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + private func checkLatestBlocksDataProvider(_ latestBlocksDataProviderMock: LatestBlocksDataProviderMock) { + XCTAssertTrue( + latestBlocksDataProviderMock.updateScannedDataCalled, + "latestBlocksDataProvider.updateScannedData() is expected to be called." + ) + XCTAssertTrue( + latestBlocksDataProviderMock.updateBlockDataCalled, + "latestBlocksDataProvider.updateBlockData() is expected to be called." + ) + XCTAssertTrue( + latestBlocksDataProviderMock.updateUnenhancedDataCalled, + "latestBlocksDataProvider.updateUnenhancedData() is expected to be called." + ) + } + + private func checkActionContext(_ actionContext: ActionContext, expectedNextState: CBPState) { + guard let nextContextMock = actionContext as? ActionContextMock else { + return XCTFail("Result of run(with:) is expected to be an ActionContextMock") + } - return setupAction( - blockDownloaderServiceMock, - latestBlocksDataProviderMock, - loggerMock + XCTAssertTrue(nextContextMock.updateStateCallsCount == 1) + XCTAssertTrue(nextContextMock.updateStateReceivedState == expectedNextState) + + XCTAssertTrue( + nextContextMock.updateLastScannedHeightCallsCount == 1, + "actionContext.update(lastScannedHeight:) is expected to be called." + ) + XCTAssertTrue( + nextContextMock.updateLastDownloadedHeightCallsCount == 1, + "actionContext.update(lastDownloadedHeight:) is expected to be called." + ) + XCTAssertTrue( + nextContextMock.updateSyncControlDataCallsCount == 1, + "actionContext.update(syncControlData:) is expected to be called." + ) + XCTAssertTrue( + nextContextMock.updateTotalProgressRangeCallsCount == 1, + "actionContext.update(totalProgressRange:) is expected to be called." ) } } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift index 777cd2ac..a18f6c7a 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift @@ -13,7 +13,7 @@ final class DownloadActionTests: ZcashTestCase { var underlyingDownloadRange: CompactBlockRange? var underlyingScanRange: CompactBlockRange? - func testDownloadAction_NextAction() async throws { + func testDownloadAction_FullPass() async throws { let blockDownloaderMock = BlockDownloaderMock() let transactionRepositoryMock = TransactionRepositoryMock() @@ -32,25 +32,69 @@ final class DownloadActionTests: ZcashTestCase { underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) - let syncContext = await setupActionContext() - await syncContext.update(lastScannedHeight: 1000) + let syncContext = ActionContextMock.default() + syncContext.lastScannedHeight = 1000 + syncContext.underlyingSyncControlData = SyncControlData( + latestBlockHeight: 2000, + latestScannedHeight: underlyingScanRange?.lowerBound, + firstUnenhancedHeight: nil + ) do { let nextContext = try await downloadAction.run(with: syncContext) { _ in } - XCTAssertTrue(blockDownloaderMock.setSyncRangeBatchSizeCalled, "downloader.setSyncRange() is expected to be called.") - XCTAssertTrue(blockDownloaderMock.setDownloadLimitCalled, "downloader.setDownloadLimit() is expected to be called.") - XCTAssertTrue(blockDownloaderMock.startDownloadMaxBlockBufferSizeCalled, "downloader.startDownload() is expected to be called.") XCTAssertTrue( - blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCalled, - "downloader.waitUntilRequestedBlocksAreDownloaded() is expected to be called." + blockDownloaderMock.setSyncRangeBatchSizeCallsCount == 1, + "downloader.setSyncRange() is expected to be called exatcly once." + ) + XCTAssertTrue(blockDownloaderMock.setDownloadLimitCallsCount == 1, "downloader.setDownloadLimit() is expected to be called exatcly once.") + XCTAssertTrue( + blockDownloaderMock.startDownloadMaxBlockBufferSizeCallsCount == 1, + "downloader.startDownload() is expected to be called exatcly once." + ) + XCTAssertTrue( + blockDownloaderMock.updateLatestDownloadedBlockHeightForceCallsCount == 1, + "downloader.update(latestDownloadedBlockHeight:) expected to be called exactly once." + ) + XCTAssertTrue( + blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCallsCount == 1, + "downloader.waitUntilRequestedBlocksAreDownloaded() is expected to be called exatcly once." ) - let nextState = await nextContext.state + let acResult = nextContext.checkStateIs(.scan) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testDownloadAction_NextAction is not expected to fail. \(error)") + } + } + + func testDownloadAction_LastScanHeightNil() async throws { + let blockDownloaderMock = BlockDownloaderMock() + + let downloadAction = setupAction(blockDownloaderMock) + + let syncContext = ActionContextMock.default() + + do { + let nextContext = try await downloadAction.run(with: syncContext) { _ in } + + XCTAssertTrue(blockDownloaderMock.setSyncRangeBatchSizeCallsCount == 0, "downloader.setSyncRange() is not expected to be called.") + XCTAssertTrue(blockDownloaderMock.setDownloadLimitCallsCount == 0, "downloader.setDownloadLimit() is not expected to be called.") XCTAssertTrue( - nextState == .scan, - "nextContext after .download is expected to be .scan but received \(nextState)" + blockDownloaderMock.startDownloadMaxBlockBufferSizeCallsCount == 0, + "downloader.startDownload() is not expected to be called." ) + XCTAssertTrue( + blockDownloaderMock.updateLatestDownloadedBlockHeightForceCallsCount == 0, + "downloader.update(latestDownloadedBlockHeight:) is not expected to be called." + ) + XCTAssertTrue( + blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCallsCount == 0, + "downloader.waitUntilRequestedBlocksAreDownloaded() is not expected to be called." + ) + + let acResult = nextContext.checkStateIs(.scan) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } catch { XCTFail("testDownloadAction_NextAction is not expected to fail. \(error)") } @@ -65,7 +109,13 @@ final class DownloadActionTests: ZcashTestCase { transactionRepositoryMock ) - let syncContext = await setupActionContext() + let syncContext = ActionContextMock.default() + syncContext.lastScannedHeight = 1000 + syncContext.underlyingSyncControlData = SyncControlData( + latestBlockHeight: 999, + latestScannedHeight: underlyingScanRange?.lowerBound, + firstUnenhancedHeight: nil + ) do { let nextContext = try await downloadAction.run(with: syncContext) { _ in } @@ -82,53 +132,13 @@ final class DownloadActionTests: ZcashTestCase { "downloader.waitUntilRequestedBlocksAreDownloaded() is not expected to be called." ) - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .scan, - "nextContext after .download is expected to be .scan but received \(nextState)" - ) + let acResult = nextContext.checkStateIs(.scan) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } catch { XCTFail("testDownloadAction_NoDownloadAndScanRange is not expected to fail. \(error)") } } - func testDownloadAction_NothingMoreToDownload() async throws { - let blockDownloaderMock = BlockDownloaderMock() - let transactionRepositoryMock = TransactionRepositoryMock() - - transactionRepositoryMock.lastScannedHeightReturnValue = 2001 - - let downloadAction = setupAction( - blockDownloaderMock, - transactionRepositoryMock - ) - - underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) - underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) - - let syncContext = await setupActionContext() - - do { - let nextContext = try await downloadAction.run(with: syncContext) { _ in } - - XCTAssertFalse(blockDownloaderMock.setSyncRangeBatchSizeCalled, "downloader.setSyncRange() is not expected to be called.") - XCTAssertFalse(blockDownloaderMock.setDownloadLimitCalled, "downloader.setDownloadLimit() is not expected to be called.") - XCTAssertFalse(blockDownloaderMock.startDownloadMaxBlockBufferSizeCalled, "downloader.startDownload() is not expected to be called.") - XCTAssertFalse( - blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCalled, - "downloader.waitUntilRequestedBlocksAreDownloaded() is not expected to be called." - ) - - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .scan, - "nextContext after .download is expected to be .scan but received \(nextState)" - ) - } catch { - XCTFail("testDownloadAction_NothingMoreToDownload is not expected to fail. \(error)") - } - } - func testDownloadAction_DownloadStops() async throws { let blockDownloaderMock = BlockDownloaderMock() @@ -142,21 +152,7 @@ final class DownloadActionTests: ZcashTestCase { XCTAssertTrue(blockDownloaderMock.stopDownloadCalled, "downloader.stopDownload() is expected to be called.") } - - private func setupActionContext() async -> ActionContext { - let syncContext: ActionContext = .init(state: .download) - - let syncControlData = SyncControlData( - latestBlockHeight: 2000, - latestScannedHeight: underlyingScanRange?.lowerBound, - firstUnenhancedHeight: nil - ) - - await syncContext.update(syncControlData: syncControlData) - - return syncContext - } - + private func setupAction( _ blockDownloaderMock: BlockDownloaderMock = BlockDownloaderMock(), _ transactionRepositoryMock: TransactionRepositoryMock = TransactionRepositoryMock(), diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift index 2463b6a5..e5f96c3b 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift @@ -25,14 +25,12 @@ final class EnhanceActionTests: ZcashTestCase { func testEnhanceAction_decideWhatToDoNext_NoDownloadAndScanRange() async throws { let enhanceAction = setupAction() - let syncContext = await setupActionContext() - let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 1) - let nextState = await nextContext.state + let syncContext = setupActionContext() - XCTAssertTrue( - nextState == .clearCache, - "testEnhanceAction_decideWhatToDoNext_NoDownloadAndScanRange is expected to be .clearCache but received \(nextState)" - ) + let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 1) + + let acResult = nextContext.checkStateIs(.clearCache) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } func testEnhanceAction_decideWhatToDoNext_NothingToDownloadAndScanLeft() async throws { @@ -40,14 +38,12 @@ final class EnhanceActionTests: ZcashTestCase { underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) - let syncContext = await setupActionContext() - let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 2000) - let nextState = await nextContext.state + let syncContext = setupActionContext() - XCTAssertTrue( - nextState == .clearCache, - "testEnhanceAction_decideWhatToDoNext_NothingToDownloadAndScanLeft is expected to be .clearCache but received \(nextState)" - ) + let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 2000) + + let acResult = nextContext.checkStateIs(.clearCache) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } func testEnhanceAction_decideWhatToDoNext_UpdateChainTipExpected() async throws { @@ -55,28 +51,62 @@ final class EnhanceActionTests: ZcashTestCase { underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) - let syncContext = await setupActionContext() - let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 1500) - let nextState = await nextContext.state + let syncContext = setupActionContext() - XCTAssertTrue( - nextState == .updateChainTip, - "testEnhanceAction_decideWhatToDoNext_DownloadExpected is expected to be .updateChainTip but received \(nextState)" - ) + let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 1500) + + let acResult = nextContext.checkStateIs(.updateChainTip) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } + func testEnhanceAction_LastScanHeightNil() async throws { + let blockEnhancerMock = BlockEnhancerMock() + + let enhanceAction = setupAction(blockEnhancerMock) + + let syncContext = setupActionContext() + + do { + _ = try await enhanceAction.run(with: syncContext) { _ in } + XCTFail("testEnhanceAction_LastScanHeightNil is expected to fail.") + } catch ZcashError.compactBlockProcessorLastScannedHeight { + XCTAssertFalse(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is not expected to be called.") + } catch { + XCTFail("testEnhanceAction_LastScanHeightNil is not expected to fail. \(error)") + } + } + + func testEnhanceAction_firstUnenhancedHeightNil() async throws { + let blockEnhancerMock = BlockEnhancerMock() + + let enhanceAction = setupAction(blockEnhancerMock) + + let syncContext = setupActionContext() + syncContext.lastScannedHeight = 1 + + do { + let nextContext = try await enhanceAction.run(with: syncContext) { _ in } + XCTAssertFalse(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is not expected to be called.") + + let acResult = nextContext.checkStateIs(.clearCache) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testEnhanceAction_NoEnhanceRange is not expected to fail. \(error)") + } + } + func testEnhanceAction_NoEnhanceRange() async throws { let blockEnhancerMock = BlockEnhancerMock() - let transactionRepositoryMock = TransactionRepositoryMock() - transactionRepositoryMock.lastScannedHeightReturnValue = 1 + let enhanceAction = setupAction(blockEnhancerMock) - let enhanceAction = setupAction( - blockEnhancerMock, - transactionRepositoryMock + let syncContext = setupActionContext() + syncContext.lastScannedHeight = 1 + syncContext.underlyingSyncControlData = SyncControlData( + latestBlockHeight: 2000, + latestScannedHeight: underlyingScanRange?.lowerBound, + firstUnenhancedHeight: 2000 ) - - let syncContext = await setupActionContext() do { _ = try await enhanceAction.run(with: syncContext) { _ in } @@ -88,18 +118,17 @@ final class EnhanceActionTests: ZcashTestCase { func testEnhanceAction_1000BlocksConditionNotFulfilled() async throws { let blockEnhancerMock = BlockEnhancerMock() - let transactionRepositoryMock = TransactionRepositoryMock() - transactionRepositoryMock.lastScannedHeightReturnValue = 1 + let enhanceAction = setupAction(blockEnhancerMock) - let enhanceAction = setupAction( - blockEnhancerMock, - transactionRepositoryMock + let syncContext = setupActionContext() + syncContext.lastScannedHeight = 1000 + syncContext.lastEnhancedHeight = 1000 + syncContext.underlyingSyncControlData = SyncControlData( + latestBlockHeight: 2000, + latestScannedHeight: 1000, + firstUnenhancedHeight: 1000 ) - - underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) - - let syncContext = await setupActionContext() do { _ = try await enhanceAction.run(with: syncContext) { _ in } @@ -111,10 +140,7 @@ final class EnhanceActionTests: ZcashTestCase { func testEnhanceAction_EnhancementOfBlocksCalled_FoundTransactions() async throws { let blockEnhancerMock = BlockEnhancerMock() - let transactionRepositoryMock = TransactionRepositoryMock() - - transactionRepositoryMock.lastScannedHeightReturnValue = 1500 - + let transaction = ZcashTransaction.Overview( accountId: 0, blockTime: 1.0, @@ -138,14 +164,17 @@ final class EnhanceActionTests: ZcashTestCase { return [transaction] } - let enhanceAction = setupAction( - blockEnhancerMock, - transactionRepositoryMock + let enhanceAction = setupAction(blockEnhancerMock) + + let syncContext = setupActionContext() + syncContext.lastScannedHeight = 2000 + syncContext.lastEnhancedHeight = 1500 + syncContext.underlyingSyncControlData = SyncControlData( + latestBlockHeight: 2000, + latestScannedHeight: 1, + firstUnenhancedHeight: 1000 ) - - underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) - - let syncContext = await setupActionContext() + syncContext.updateLastEnhancedHeightClosure = { _ in } do { _ = try await enhanceAction.run(with: syncContext) { event in @@ -168,9 +197,6 @@ final class EnhanceActionTests: ZcashTestCase { func testEnhanceAction_EnhancementOfBlocksCalled_minedTransaction() async throws { let blockEnhancerMock = BlockEnhancerMock() - let transactionRepositoryMock = TransactionRepositoryMock() - - transactionRepositoryMock.lastScannedHeightReturnValue = 1500 let transaction = ZcashTransaction.Overview( accountId: 0, @@ -203,14 +229,17 @@ final class EnhanceActionTests: ZcashTestCase { return nil } - let enhanceAction = setupAction( - blockEnhancerMock, - transactionRepositoryMock + let enhanceAction = setupAction(blockEnhancerMock) + + let syncContext = setupActionContext() + syncContext.lastScannedHeight = 2000 + syncContext.lastEnhancedHeight = 1500 + syncContext.underlyingSyncControlData = SyncControlData( + latestBlockHeight: 2000, + latestScannedHeight: 1, + firstUnenhancedHeight: 1000 ) - - underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) - - let syncContext = await setupActionContext() + syncContext.updateLastEnhancedHeightClosure = { _ in } do { _ = try await enhanceAction.run(with: syncContext) { event in @@ -229,9 +258,6 @@ final class EnhanceActionTests: ZcashTestCase { func testEnhanceAction_EnhancementOfBlocksCalled_usingSmallRange_minedTransaction() async throws { let blockEnhancerMock = BlockEnhancerMock() - let transactionRepositoryMock = TransactionRepositoryMock() - - transactionRepositoryMock.lastScannedHeightReturnValue = 2000 let transaction = ZcashTransaction.Overview( accountId: 0, @@ -264,15 +290,18 @@ final class EnhanceActionTests: ZcashTestCase { return nil } - let enhanceAction = setupAction( - blockEnhancerMock, - transactionRepositoryMock + let enhanceAction = setupAction(blockEnhancerMock) + + let syncContext = setupActionContext() + syncContext.lastScannedHeight = 2000 + syncContext.lastEnhancedHeight = 1500 + syncContext.underlyingSyncControlData = SyncControlData( + latestBlockHeight: 2000, + latestScannedHeight: 1, + firstUnenhancedHeight: 1000 ) - - underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1900, 2000)) - - let syncContext = await setupActionContext() - + syncContext.updateLastEnhancedHeightClosure = { _ in } + do { _ = try await enhanceAction.run(with: syncContext) { event in if case .progressPartialUpdate = event { return } @@ -288,18 +317,14 @@ final class EnhanceActionTests: ZcashTestCase { } } - private func setupActionContext() async -> ActionContext { - let syncContext: ActionContext = .init(state: .enhance) - - let syncControlData = SyncControlData( + private func setupActionContext() -> ActionContextMock { + let syncContext = ActionContextMock.default() + + syncContext.underlyingSyncControlData = SyncControlData( latestBlockHeight: 2000, latestScannedHeight: underlyingScanRange?.lowerBound, firstUnenhancedHeight: underlyingEnhanceRange?.lowerBound ) - - await syncContext.update(syncControlData: syncControlData) - await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000))) - await syncContext.update(lastScannedHeight: underlyingScanRange?.lowerBound ?? -1) return syncContext } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/FetchUTXOsActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/FetchUTXOsActionTests.swift index da2ff29d..c5b391ae 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/FetchUTXOsActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/FetchUTXOsActionTests.swift @@ -24,16 +24,13 @@ final class FetchUTXOsActionTests: ZcashTestCase { let fetchUTXOsAction = FetchUTXOsAction(container: mockContainer) - let syncContext: ActionContext = .init(state: .fetchUTXO) - - let syncControlData = SyncControlData( + let syncContext = ActionContextMock.default() + syncContext.underlyingSyncControlData = SyncControlData( latestBlockHeight: 0, latestScannedHeight: 0, firstUnenhancedHeight: nil ) - await syncContext.update(syncControlData: syncControlData) - do { let nextContext = try await fetchUTXOsAction.run(with: syncContext) { event in guard case .storedUTXOs(let result) = event else { @@ -45,11 +42,9 @@ final class FetchUTXOsActionTests: ZcashTestCase { } XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.") XCTAssertTrue(uTXOFetcherMock.fetchDidFetchCalled, "utxoFetcher.fetch() is expected to be called.") - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .handleSaplingParams, - "nextContext after .fetchUTXO is expected to be .handleSaplingParams but received \(nextState)" - ) + + let acResult = nextContext.checkStateIs(.handleSaplingParams) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } catch { XCTFail("testFetchUTXOsAction_NextAction is not expected to fail. \(error)") } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift index 935e1f39..c2fd8d50 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/MigrateLegacyCacheDBActionTests.swift @@ -34,7 +34,8 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { ) do { - let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + let context = ActionContextMock.default() + let nextContext = try await migrateLegacyCacheDBAction.run(with: context) { _ in } XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") XCTAssertFalse( @@ -44,11 +45,8 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { XCTAssertFalse(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is not expected to be called.") XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.") - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .validateServer, - "nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)" - ) + let acResult = nextContext.checkStateIs(.validateServer) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } catch { XCTFail("testMigrateLegacyCacheDBAction_noCacheDbURL is not expected to fail. \(error)") } @@ -68,7 +66,8 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { ) do { - _ = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + let context = ActionContextMock.default() + _ = try await migrateLegacyCacheDBAction.run(with: context) { _ in } XCTFail("testMigrateLegacyCacheDBAction_noFsBlockCacheRoot is expected to fail.") } catch ZcashError.compactBlockProcessorCacheDbMigrationFsCacheMigrationFailedSameURL { XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") @@ -104,7 +103,8 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { ) do { - let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + let context = ActionContextMock.default() + let nextContext = try await migrateLegacyCacheDBAction.run(with: context) { _ in } XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") XCTAssertFalse( @@ -114,11 +114,8 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { XCTAssertFalse(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is not expected to be called.") XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.") - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .validateServer, - "nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)" - ) + let acResult = nextContext.checkStateIs(.validateServer) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } catch { XCTFail("testMigrateLegacyCacheDBAction_aliasDoesntMatchDefault is not expected to fail. \(error)") } @@ -142,8 +139,9 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { ) do { - let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } - + let context = ActionContextMock.default() + let nextContext = try await migrateLegacyCacheDBAction.run(with: context) { _ in } + XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") XCTAssertFalse( transactionRepositoryMock.lastScannedHeightCalled, @@ -152,11 +150,8 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { XCTAssertTrue(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is expected to be called.") XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.") - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .validateServer, - "nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)" - ) + let acResult = nextContext.checkStateIs(.validateServer) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } catch { XCTFail("testMigrateLegacyCacheDBAction_isNotReadableFile is not expected to fail. \(error)") } @@ -181,7 +176,8 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { ) do { - _ = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } + let context = ActionContextMock.default() + _ = try await migrateLegacyCacheDBAction.run(with: context) { _ in } } catch ZcashError.compactBlockProcessorCacheDbMigrationFailedToDeleteLegacyDb { XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.") XCTAssertFalse( @@ -218,17 +214,15 @@ final class MigrateLegacyCacheDBActionTests: ZcashTestCase { ) do { - let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in } - + let context = ActionContextMock.default() + let nextContext = try await migrateLegacyCacheDBAction.run(with: context) { _ in } + XCTAssertTrue(compactBlockRepositoryMock.createCalled, "storage.create() is expected to be called.") XCTAssertTrue(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is expected to be called.") XCTAssertTrue(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is expected to be called.") - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .validateServer, - "nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)" - ) + let acResult = nextContext.checkStateIs(.validateServer) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } catch { XCTFail("testMigrateLegacyCacheDBAction_nextAction is not expected to fail. \(error)") } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ProcessSuggestedScanRangesActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ProcessSuggestedScanRangesActionTests.swift new file mode 100644 index 00000000..d0d23e56 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ProcessSuggestedScanRangesActionTests.swift @@ -0,0 +1,247 @@ +// +// ProcessSuggestedScanRangesActionTests.swift +// +// +// Created by Lukáš Korba on 25.08.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class ProcessSuggestedScanRangesActionTests: ZcashTestCase { + var underlyingChainName = "" + var underlyingNetworkType = NetworkType.testnet + var underlyingSaplingActivationHeight: BlockHeight? + var underlyingConsensusBranchID = "" + + override func setUp() { + super.setUp() + + underlyingChainName = "test" + underlyingNetworkType = .testnet + underlyingSaplingActivationHeight = nil + underlyingConsensusBranchID = "c2d6d0b4" + } + + func testProcessSuggestedScanRangesAction_EmptyScanRanges() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + + let tupple = setupAction(loggerMock) + await tupple.rustBackendMock.setSuggestScanRangesClosure( { [] } ) + + let processSuggestedScanRangesActionAction = tupple.action + + do { + let context = ActionContextMock.default() + + let nextContext = try await processSuggestedScanRangesActionAction.run(with: context) { _ in } + + XCTAssertFalse( + loggerMock.debugFileFunctionLineCalled, + "logger.debug() is not expected to be called." + ) + + let acResult = nextContext.checkStateIs(.finished) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testProcessSuggestedScanRangesAction_EmptyScanRanges is not expected to fail. \(error)") + } + } + + func testProcessSuggestedScanRangesAction_VerifyScanRangeSetTotalProgressRange() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + let tupple = setupAction(loggerMock) + await tupple.rustBackendMock.setSuggestScanRangesClosure( { [ + ScanRange(range: 0..<10, priority: .verify) + ] } ) + + let processSuggestedScanRangesActionAction = tupple.action + + do { + let context = ActionContextMock.default() + context.updateLastScannedHeightClosure = { _ in } + context.updateLastDownloadedHeightClosure = { _ in } + context.updateSyncControlDataClosure = { _ in } + context.underlyingTotalProgressRange = 0...0 + context.updateTotalProgressRangeClosure = { _ in } + context.updateRequestedRewindHeightClosure = { _ in } + + let nextContext = try await processSuggestedScanRangesActionAction.run(with: context) { _ in } + + XCTAssertTrue( + loggerMock.debugFileFunctionLineCalled, + "logger.debug() is not expected to be called." + ) + + if let nextContextMock = nextContext as? ActionContextMock { + XCTAssertTrue( + nextContextMock.updateRequestedRewindHeightCallsCount == 1, + "context.update(requestedRewindHeight:) is expected to be called exactly once." + ) + } else { + XCTFail("`nextContext` is not the ActionContextMock") + } + + XCTAssertTrue( + loggerMock.debugFileFunctionLineCalled, + "logger.debug() is not expected to be called." + ) + + if let infoArguments = loggerMock.infoFileFunctionLineReceivedArguments { + XCTAssertTrue(infoArguments.message.contains("Setting the total range for Spend before Sync to")) + } else { + XCTFail("`infoArguments` unavailable.") + } + + let acResult = nextContext.checkStateIs(.rewind) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testProcessSuggestedScanRangesAction_VerifyScanRangeSetTotalProgressRange is not expected to fail. \(error)") + } + } + + func testProcessSuggestedScanRangesAction_VerifyScanRangeTotalProgressRangeSkipped() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + let tupple = setupAction(loggerMock) + await tupple.rustBackendMock.setSuggestScanRangesClosure( { [ + ScanRange(range: 0..<10, priority: .verify) + ] } ) + + let processSuggestedScanRangesActionAction = tupple.action + + do { + let context = ActionContextMock.default() + context.updateLastScannedHeightClosure = { _ in } + context.updateLastDownloadedHeightClosure = { _ in } + context.updateSyncControlDataClosure = { _ in } + context.underlyingTotalProgressRange = 1...1 + context.updateRequestedRewindHeightClosure = { _ in } + + let nextContext = try await processSuggestedScanRangesActionAction.run(with: context) { _ in } + + XCTAssertTrue( + loggerMock.debugFileFunctionLineCalled, + "logger.debug() is not expected to be called." + ) + + if let nextContextMock = nextContext as? ActionContextMock { + XCTAssertTrue( + nextContextMock.updateRequestedRewindHeightCallsCount == 1, + "context.update(requestedRewindHeight:) is expected to be called exactly once." + ) + } else { + XCTFail("`nextContext` is not the ActionContextMock") + } + + if let infoArguments = loggerMock.infoFileFunctionLineReceivedArguments { + XCTAssertFalse(infoArguments.message.contains("Setting the total range for Spend before Sync to")) + } else { + XCTFail("`infoArguments` unavailable.") + } + + let acResult = nextContext.checkStateIs(.rewind) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testProcessSuggestedScanRangesAction_VerifyScanRangeTotalProgressRangeSkipped is not expected to fail. \(error)") + } + } + + func testProcessSuggestedScanRangesAction_ChainTipScanRange() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + let tupple = setupAction(loggerMock) + await tupple.rustBackendMock.setSuggestScanRangesClosure( { [ + ScanRange(range: 0..<10, priority: .chainTip) + ] } ) + + let processSuggestedScanRangesActionAction = tupple.action + + do { + let context = ActionContextMock.default() + context.updateLastScannedHeightClosure = { _ in } + context.updateLastDownloadedHeightClosure = { _ in } + context.updateSyncControlDataClosure = { _ in } + context.underlyingTotalProgressRange = 1...1 + context.updateRequestedRewindHeightClosure = { _ in } + + let nextContext = try await processSuggestedScanRangesActionAction.run(with: context) { _ in } + + if let nextContextMock = nextContext as? ActionContextMock { + XCTAssertFalse( + nextContextMock.updateRequestedRewindHeightCalled, + "context.update(requestedRewindHeight:) is not expected to be called" + ) + } else { + XCTFail("`nextContext` is not the ActionContextMock") + } + + XCTAssertTrue( + loggerMock.debugFileFunctionLineCalled, + "logger.debug() is not expected to be called." + ) + + if let infoArguments = loggerMock.infoFileFunctionLineReceivedArguments { + XCTAssertFalse(infoArguments.message.contains("Setting the total range for Spend before Sync to")) + } else { + XCTFail("`infoArguments` unavailable.") + } + + let acResult = nextContext.checkStateIs(.download) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testProcessSuggestedScanRangesAction_ChainTipScanRange is not expected to fail. \(error)") + } + } + + private func setupAction( + _ loggerMock: LoggerMock = LoggerMock() + ) -> ( + action: ProcessSuggestedScanRangesAction, + serviceMock: LightWalletServiceMock, + rustBackendMock: ZcashRustBackendWeldingMock + ) { + let config: CompactBlockProcessor.Configuration = .standard( + for: ZcashNetworkBuilder.network(for: underlyingNetworkType), walletBirthday: 0 + ) + + let rustBackendMock = ZcashRustBackendWeldingMock( + consensusBranchIdForHeightClosure: { height in + XCTAssertEqual(height, 2, "") + return -1026109260 + } + ) + + let lightWalletdInfoMock = LightWalletdInfoMock() + lightWalletdInfoMock.underlyingConsensusBranchID = underlyingConsensusBranchID + lightWalletdInfoMock.underlyingSaplingActivationHeight = UInt64(underlyingSaplingActivationHeight ?? config.saplingActivation) + lightWalletdInfoMock.underlyingBlockHeight = 2 + lightWalletdInfoMock.underlyingChainName = underlyingChainName + + let serviceMock = LightWalletServiceMock() + serviceMock.getInfoReturnValue = lightWalletdInfoMock + + mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in rustBackendMock } + mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in serviceMock } + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + + return ( + action:ProcessSuggestedScanRangesAction(container: mockContainer), + serviceMock: serviceMock, + rustBackendMock: rustBackendMock + ) + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/RewindActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/RewindActionTests.swift new file mode 100644 index 00000000..b8b31fe3 --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/RewindActionTests.swift @@ -0,0 +1,112 @@ +// +// RewindActionTests.swift +// +// +// Created by Lukáš Korba on 25.08.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class RewindActionTests: ZcashTestCase { + var underlyingChainName = "" + var underlyingNetworkType = NetworkType.testnet + var underlyingSaplingActivationHeight: BlockHeight? + var underlyingConsensusBranchID = "" + + override func setUp() { + super.setUp() + + underlyingChainName = "test" + underlyingNetworkType = .testnet + underlyingSaplingActivationHeight = nil + underlyingConsensusBranchID = "c2d6d0b4" + } + + func testRewindAction_requestedRewindHeightNil() async throws { + let blockDownloaderMock = BlockDownloaderMock() + + let rewindActionAction = await setupAction(blockDownloaderMock) + + do { + let context = ActionContextMock.default() + + let nextContext = try await rewindActionAction.run(with: context) { _ in } + + XCTAssertFalse( + blockDownloaderMock.rewindLatestDownloadedBlockHeightCalled, + "downloader.rewind(latestDownloadedBlockHeight:) is not expected to be called." + ) + + let acResult = nextContext.checkStateIs(.download) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testRewindAction_requestedRewindHeightNil is not expected to fail. \(error)") + } + } + + func testRewindAction_FullPass() async throws { + let blockDownloaderMock = BlockDownloaderMock() + let loggerMock = LoggerMock() + let blockDownloaderServiceMock = BlockDownloaderServiceMock() + + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + blockDownloaderMock.rewindLatestDownloadedBlockHeightClosure = { _ in } + blockDownloaderServiceMock.rewindToClosure = { _ in } + + let rewindActionAction = await setupAction( + blockDownloaderMock, + loggerMock, + blockDownloaderServiceMock + ) + + do { + let context = ActionContextMock.default() + context.requestedRewindHeight = 1 + + let nextContext = try await rewindActionAction.run(with: context) { _ in } + + XCTAssertTrue( + blockDownloaderMock.rewindLatestDownloadedBlockHeightCallsCount == 1, + "downloader.rewind(latestDownloadedBlockHeight:) is expected to be called." + ) + XCTAssertTrue( + loggerMock.debugFileFunctionLineCallsCount == 1, + "logger.debug() is expected to be called." + ) + XCTAssertTrue( + blockDownloaderServiceMock.rewindToCallsCount == 1, + "downloaderService.rewind(to:) is expected to be called." + ) + + let acResult = nextContext.checkStateIs(.download) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testRewindAction_FullPass is not expected to fail. \(error)") + } + } + + private func setupAction( + _ blockDownloaderMock: BlockDownloaderMock = BlockDownloaderMock(), + _ loggerMock: LoggerMock = LoggerMock(), + _ blockDownloaderServiceMock: BlockDownloaderServiceMock = BlockDownloaderServiceMock() + ) async -> RewindAction { + let rustBackendMock = ZcashRustBackendWeldingMock( + consensusBranchIdForHeightClosure: { height in + XCTAssertEqual(height, 2, "") + return -1026109260 + } + ) + + await rustBackendMock.setRewindToHeightHeightClosure( { _ in } ) + + mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in rustBackendMock } + mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in blockDownloaderServiceMock } + mockContainer.mock(type: BlockDownloader.self, isSingleton: true) { _ in blockDownloaderMock } + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + + return RewindAction(container: mockContainer) + } +} + diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift index 107b8ea1..c0bdfb14 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift @@ -10,29 +10,62 @@ import XCTest @testable import ZcashLightClientKit final class SaplingParamsActionTests: ZcashTestCase { - func testSaplingParamsAction_NextAction() async throws { + func testSaplingParamsAction_NextAction_linearSync() async throws { let loggerMock = LoggerMock() let saplingParametersHandlerMock = SaplingParametersHandlerMock() + let saplingParamsActionAction = setupAction(saplingParametersHandlerMock, loggerMock) + + do { + let context = ActionContextMock.default() + context.underlyingPreferredSyncAlgorithm = .linear + + let nextContext = try await saplingParamsActionAction.run(with: context) { _ in } + + XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.") + XCTAssertTrue(saplingParametersHandlerMock.handleIfNeededCalled, "saplingParametersHandler.handleIfNeeded() is expected to be called.") + + let acResult = nextContext.checkStateIs(.computeSyncControlData) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testSaplingParamsAction_NextAction is not expected to fail. \(error)") + } + } + + func testSaplingParamsAction_NextAction_SpendBeforeSync() async throws { + let loggerMock = LoggerMock() + let saplingParametersHandlerMock = SaplingParametersHandlerMock() + + let saplingParamsActionAction = setupAction(saplingParametersHandlerMock, loggerMock) + + do { + let context = ActionContextMock.default() + context.underlyingPreferredSyncAlgorithm = .spendBeforeSync + + let nextContext = try await saplingParamsActionAction.run(with: context) { _ in } + + XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.") + XCTAssertTrue(saplingParametersHandlerMock.handleIfNeededCalled, "saplingParametersHandler.handleIfNeeded() is expected to be called.") + + let acResult = nextContext.checkStateIs(.updateSubtreeRoots) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testSaplingParamsAction_NextAction is not expected to fail. \(error)") + } + } + + private func setupAction( + _ saplingParametersHandlerMock: SaplingParametersHandlerMock = SaplingParametersHandlerMock(), + _ loggerMock: LoggerMock = LoggerMock() + ) -> SaplingParamsAction { loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } saplingParametersHandlerMock.handleIfNeededClosure = { } mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } mockContainer.mock(type: SaplingParametersHandler.self, isSingleton: true) { _ in saplingParametersHandlerMock } - let saplingParamsActionAction = SaplingParamsAction(container: mockContainer) - - do { - let nextContext = try await saplingParamsActionAction.run(with: .init(state: .handleSaplingParams)) { _ in } - XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.") - XCTAssertTrue(saplingParametersHandlerMock.handleIfNeededCalled, "saplingParametersHandler.handleIfNeeded() is expected to be called.") - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .computeSyncControlData, - "nextContext after .handleSaplingParams is expected to be .computeSyncControlData but received \(nextState)" - ) - } catch { - XCTFail("testSaplingParamsAction_NextAction is not expected to fail. \(error)") - } + return SaplingParamsAction( + container: mockContainer + ) } } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift index c4459b0c..2e833ad4 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift @@ -20,10 +20,16 @@ final class ScanActionTests: ZcashTestCase { blockScannerMock.scanBlocksAtTotalProgressRangeDidScanClosure = { _, _, _ in 2 } let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock) - let syncContext = await setupActionContext() - - await syncContext.update(lastScannedHeight: 1500) - + + let syncContext = ActionContextMock.default() + syncContext.lastScannedHeight = 1500 + syncContext.underlyingTotalProgressRange = 1000...2000 + syncContext.underlyingSyncControlData = SyncControlData( + latestBlockHeight: 2000, + latestScannedHeight: 1000, + firstUnenhancedHeight: nil + ) + do { let nextContext = try await scanAction.run(with: syncContext) { event in guard case .progressPartialUpdate(.syncing(let progress)) = event else { @@ -36,11 +42,9 @@ final class ScanActionTests: ZcashTestCase { } XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.") XCTAssertTrue(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is expected to be called.") - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .clearAlreadyScannedBlocks, - "nextContext after .scan is expected to be .clearAlreadyScannedBlocks but received \(nextState)" - ) + + let acResult = nextContext.checkStateIs(.clearAlreadyScannedBlocks) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } catch { XCTFail("testScanAction_NextAction is not expected to fail. \(error)") } @@ -52,7 +56,7 @@ final class ScanActionTests: ZcashTestCase { let loggerMock = LoggerMock() let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock) - let syncContext: ActionContext = .init(state: .scan) + let syncContext = ActionContextMock.default() do { _ = try await scanAction.run(with: syncContext) { _ in } @@ -75,7 +79,7 @@ final class ScanActionTests: ZcashTestCase { transactionRepositoryMock.lastScannedHeightReturnValue = 2001 let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock) - let syncContext = await setupActionContext() + let syncContext = ActionContextMock.default() do { _ = try await scanAction.run(with: syncContext) { _ in } @@ -104,19 +108,4 @@ final class ScanActionTests: ZcashTestCase { configProvider: CompactBlockProcessor.ConfigProvider(config: config) ) } - - private func setupActionContext() async -> ActionContext { - let syncContext: ActionContext = .init(state: .scan) - - let syncControlData = SyncControlData( - latestBlockHeight: 2000, - latestScannedHeight: 1000, - firstUnenhancedHeight: nil - ) - - await syncContext.update(syncControlData: syncControlData) - await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000))) - - return syncContext - } } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/UpdateChainTipActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/UpdateChainTipActionTests.swift new file mode 100644 index 00000000..b11b6e4a --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/UpdateChainTipActionTests.swift @@ -0,0 +1,125 @@ +// +// UpdateChainTipActionTests.swift +// +// +// Created by Lukáš Korba on 25.08.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class UpdateChainTipActionTests: ZcashTestCase { + var underlyingChainName = "" + var underlyingNetworkType = NetworkType.testnet + var underlyingSaplingActivationHeight: BlockHeight? + var underlyingConsensusBranchID = "" + + override func setUp() { + super.setUp() + + underlyingChainName = "test" + underlyingNetworkType = .testnet + underlyingSaplingActivationHeight = nil + underlyingConsensusBranchID = "c2d6d0b4" + } + + func testUpdateChainTipAction_UpdateChainTipTimeTriggered() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + + let updateChainTipAction = await setupAction(loggerMock) + + do { + let context = ActionContextMock.default() + context.prevState = .idle + context.underlyingLastChainTipUpdateTime = 0.0 + context.updateLastChainTipUpdateTimeClosure = { _ in } + + let nextContext = try await updateChainTipAction.run(with: context) { _ in } + + let acResult = nextContext.checkStateIs(.processSuggestedScanRanges) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testUpdateChainTipAction_UpdateChainTipTimeTriggered is not expected to fail. \(error)") + } + } + + func testUpdateChainTipAction_UpdateChainTipPrevActionTriggered() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + + let updateChainTipAction = await setupAction(loggerMock) + + do { + let context = ActionContextMock.default() + context.prevState = .updateSubtreeRoots + context.underlyingLastChainTipUpdateTime = Date().timeIntervalSince1970 + context.updateLastChainTipUpdateTimeClosure = { _ in } + + let nextContext = try await updateChainTipAction.run(with: context) { _ in } + + let acResult = nextContext.checkStateIs(.processSuggestedScanRanges) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testUpdateChainTipAction_UpdateChainTipPrevActionTriggered is not expected to fail. \(error)") + } + } + + func testUpdateChainTipAction_UpdateChainTipSkipped() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + + let updateChainTipAction = await setupAction(loggerMock) + + do { + let context = ActionContextMock.default() + context.prevState = .enhance + context.underlyingLastChainTipUpdateTime = Date().timeIntervalSince1970 + context.updateLastChainTipUpdateTimeClosure = { _ in } + + let nextContext = try await updateChainTipAction.run(with: context) { _ in } + + let acResult = nextContext.checkStateIs(.download) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testUpdateChainTipAction_UpdateChainTipSkipped is not expected to fail. \(error)") + } + } + + private func setupAction( + _ loggerMock: LoggerMock = LoggerMock() + ) async -> UpdateChainTipAction { + let config: CompactBlockProcessor.Configuration = .standard( + for: ZcashNetworkBuilder.network(for: underlyingNetworkType), walletBirthday: 0 + ) + + let rustBackendMock = ZcashRustBackendWeldingMock( + consensusBranchIdForHeightClosure: { height in + XCTAssertEqual(height, 2, "") + return -1026109260 + } + ) + + await rustBackendMock.setUpdateChainTipHeightClosure( { _ in } ) + + let lightWalletdInfoMock = LightWalletdInfoMock() + lightWalletdInfoMock.underlyingConsensusBranchID = underlyingConsensusBranchID + lightWalletdInfoMock.underlyingSaplingActivationHeight = UInt64(underlyingSaplingActivationHeight ?? config.saplingActivation) + lightWalletdInfoMock.underlyingBlockHeight = 2 + lightWalletdInfoMock.underlyingChainName = underlyingChainName + + let serviceMock = LightWalletServiceMock() + serviceMock.getInfoReturnValue = lightWalletdInfoMock + serviceMock.latestBlockHeightReturnValue = 1 + + mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in rustBackendMock } + mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in serviceMock } + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + + return UpdateChainTipAction(container: mockContainer) + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/UpdateSubtreeRootsActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/UpdateSubtreeRootsActionTests.swift new file mode 100644 index 00000000..f1d4185b --- /dev/null +++ b/Tests/OfflineTests/CompactBlockProcessorActions/UpdateSubtreeRootsActionTests.swift @@ -0,0 +1,225 @@ +// +// UpdateSubtreeRootsActionTests.swift +// +// +// Created by Lukáš Korba on 25.08.2023. +// + +import XCTest +@testable import TestUtils +@testable import ZcashLightClientKit + +final class UpdateSubtreeRootsActionTests: ZcashTestCase { + var underlyingChainName = "" + var underlyingNetworkType = NetworkType.testnet + var underlyingSaplingActivationHeight: BlockHeight? + var underlyingConsensusBranchID = "" + + override func setUp() { + super.setUp() + + underlyingChainName = "test" + underlyingNetworkType = .testnet + underlyingSaplingActivationHeight = nil + underlyingConsensusBranchID = "c2d6d0b4" + } + + func testUpdateSubtreeRootsAction_getSubtreeRootsFailure() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + let tupple = setupAction(loggerMock) + let updateSubtreeRootsActionAction = tupple.action + tupple.serviceMock.getSubtreeRootsClosure = { _ in + AsyncThrowingStream { continuation in continuation.finish(throwing: ZcashError.serviceSubmitFailed(.invalidBlock)) } + } + + do { + let context = ActionContextMock.default() + context.updateSupportedSyncAlgorithmClosure = { _ in } + + let nextContext = try await updateSubtreeRootsActionAction.run(with: context) { _ in } + + if let nextContextMock = nextContext as? ActionContextMock { + if let supportedSyncAlgorithm = nextContextMock.updateSupportedSyncAlgorithmReceivedSupportedSyncAlgorithm { + XCTAssertTrue( + supportedSyncAlgorithm == .linear, + "supportedSyncAlgorithm is expected to be .linear but received \(supportedSyncAlgorithm)" + ) + } else { + XCTFail("`nextContextMock` supportedSyncAlgorithm not set") + } + } else { + XCTFail("`nextContext` is not the ActionContextMock") + } + + if let debugArguments = loggerMock.debugFileFunctionLineReceivedArguments { + XCTAssertTrue(debugArguments.message.contains("getSubtreeRoots failed with error")) + } else { + XCTFail("`debugArguments` unavailable.") + } + + let acResult = nextContext.checkStateIs(.computeSyncControlData) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testUpdateSubtreeRootsAction_getSubtreeRootsFailure is not expected to fail. \(error)") + } + } + + func testUpdateSubtreeRootsAction_getSubtreeRootsEmpty() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + let tupple = setupAction(loggerMock) + let updateSubtreeRootsActionAction = tupple.action + tupple.serviceMock.getSubtreeRootsClosure = { _ in + AsyncThrowingStream { continuation in continuation.finish() } + } + + do { + let context = ActionContextMock.default() + context.updateSupportedSyncAlgorithmClosure = { _ in } + + let nextContext = try await updateSubtreeRootsActionAction.run(with: context) { _ in } + + if let nextContextMock = nextContext as? ActionContextMock { + if let supportedSyncAlgorithm = nextContextMock.updateSupportedSyncAlgorithmReceivedSupportedSyncAlgorithm { + XCTAssertTrue( + supportedSyncAlgorithm == .linear, + "supportedSyncAlgorithm is expected to be .linear but received \(supportedSyncAlgorithm)" + ) + } else { + XCTFail("`nextContextMock` supportedSyncAlgorithm not set") + } + } else { + XCTFail("`nextContext` is not the ActionContextMock") + } + + XCTAssertFalse(loggerMock.debugFileFunctionLineCalled, "logger.debug() is not expected to be called.") + + let acResult = nextContext.checkStateIs(.computeSyncControlData) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testUpdateSubtreeRootsAction_getSubtreeRootsEmpty is not expected to fail. \(error)") + } + } + + func testUpdateSubtreeRootsAction_RootsAvailablePutRootsSuccess() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + let tupple = setupAction(loggerMock) + let updateSubtreeRootsActionAction = tupple.action + tupple.serviceMock.getSubtreeRootsClosure = { _ in + AsyncThrowingStream { continuation in + continuation.yield(SubtreeRoot()) + continuation.finish() + } + } + await tupple.rustBackendMock.setPutSaplingSubtreeRootsStartIndexRootsClosure( { _, _ in } ) + + do { + let context = ActionContextMock.default() + context.updateSupportedSyncAlgorithmClosure = { _ in } + + let nextContext = try await updateSubtreeRootsActionAction.run(with: context) { _ in } + + if let nextContextMock = nextContext as? ActionContextMock { + if let supportedSyncAlgorithm = nextContextMock.updateSupportedSyncAlgorithmReceivedSupportedSyncAlgorithm { + XCTAssertTrue( + supportedSyncAlgorithm == .spendBeforeSync, + "supportedSyncAlgorithm is expected to be .linear but received \(supportedSyncAlgorithm)" + ) + } else { + XCTFail("`nextContextMock` supportedSyncAlgorithm not set") + } + } else { + XCTFail("`nextContext` is not the ActionContextMock") + } + + XCTAssertFalse(loggerMock.debugFileFunctionLineCalled, "logger.debug() is not expected to be called.") + + let acResult = nextContext.checkStateIs(.updateChainTip) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") + } catch { + XCTFail("testUpdateSubtreeRootsAction_RootsAvailablePutRootsSuccess is not expected to fail. \(error)") + } + } + + func testUpdateSubtreeRootsAction_RootsAvailablePutRootsFailure() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + let tupple = setupAction(loggerMock) + let updateSubtreeRootsActionAction = tupple.action + tupple.serviceMock.getSubtreeRootsClosure = { _ in + AsyncThrowingStream { continuation in + continuation.yield(SubtreeRoot()) + continuation.finish() + } + } + await tupple.rustBackendMock.setPutSaplingSubtreeRootsStartIndexRootsThrowableError("putSaplingFailed") + + do { + let context = ActionContextMock.default() + context.updateSupportedSyncAlgorithmClosure = { _ in } + + _ = try await updateSubtreeRootsActionAction.run(with: context) { _ in } + + XCTFail("updateSubtreeRootsActionAction.run(with:) is excpected to fail but didn't.") + } catch ZcashError.compactBlockProcessorPutSaplingSubtreeRoots { + // this is expected result of this test + } catch { + XCTFail("testUpdateSubtreeRootsAction_RootsAvailablePutRootsFailure is not expected to fail. \(error)") + } + } + + private func setupAction( + _ loggerMock: LoggerMock = LoggerMock() + ) -> ( + action: UpdateSubtreeRootsAction, + serviceMock: LightWalletServiceMock, + rustBackendMock: ZcashRustBackendWeldingMock + ) { + let config: CompactBlockProcessor.Configuration = .standard( + for: ZcashNetworkBuilder.network(for: underlyingNetworkType), walletBirthday: 0 + ) + + let rustBackendMock = ZcashRustBackendWeldingMock( + consensusBranchIdForHeightClosure: { height in + XCTAssertEqual(height, 2, "") + return -1026109260 + } + ) + + let lightWalletdInfoMock = LightWalletdInfoMock() + lightWalletdInfoMock.underlyingConsensusBranchID = underlyingConsensusBranchID + lightWalletdInfoMock.underlyingSaplingActivationHeight = UInt64(underlyingSaplingActivationHeight ?? config.saplingActivation) + lightWalletdInfoMock.underlyingBlockHeight = 2 + lightWalletdInfoMock.underlyingChainName = underlyingChainName + + let serviceMock = LightWalletServiceMock() + serviceMock.getInfoReturnValue = lightWalletdInfoMock + + mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in rustBackendMock } + mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in serviceMock } + mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock } + + return ( + action: + UpdateSubtreeRootsAction( + container: mockContainer, + configProvider: CompactBlockProcessor.ConfigProvider(config: config)), + serviceMock: serviceMock, + rustBackendMock: rustBackendMock + ) + } +} diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ValidateServerActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ValidateServerActionTests.swift index 74f7002c..d904aa71 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/ValidateServerActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ValidateServerActionTests.swift @@ -28,12 +28,11 @@ final class ValidateServerActionTests: ZcashTestCase { let validateServerAction = setupAction() do { - let nextContext = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } - let nextState = await nextContext.state - XCTAssertTrue( - nextState == .fetchUTXO, - "nextContext after .validateServer is expected to be .fetchUTXO but received \(nextState)" - ) + let context = ActionContextMock.default() + let nextContext = try await validateServerAction.run(with: context) { _ in } + + let acResult = nextContext.checkStateIs(.fetchUTXO) + XCTAssertTrue(acResult == .true, "Check of state failed with '\(acResult)'") } catch { XCTFail("testValidateServerAction_NextAction is not expected to fail. \(error)") } @@ -45,7 +44,7 @@ final class ValidateServerActionTests: ZcashTestCase { let validateServerAction = setupAction() do { - _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + _ = try await validateServerAction.run(with: ActionContextMock()) { _ in } XCTFail("testValidateServerAction_ChainNameError is expected to fail.") } catch ZcashError.compactBlockProcessorChainName(let chainName) { XCTAssertEqual(chainName, "invalid") @@ -63,7 +62,7 @@ final class ValidateServerActionTests: ZcashTestCase { let validateServerAction = setupAction() do { - _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + _ = try await validateServerAction.run(with: ActionContextMock()) { _ in } XCTFail("testValidateServerAction_NetworkMatchError is expected to fail.") } catch let ZcashError.compactBlockProcessorNetworkMismatch(expected, found) { XCTAssertEqual(expected, .mainnet) @@ -82,7 +81,7 @@ final class ValidateServerActionTests: ZcashTestCase { let validateServerAction = setupAction() do { - _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + _ = try await validateServerAction.run(with: ActionContextMock()) { _ in } XCTFail("testValidateServerAction_SaplingActivationError is expected to fail.") } catch let ZcashError.compactBlockProcessorSaplingActivationMismatch(expected, found) { XCTAssertEqual(expected, 280_000) @@ -101,7 +100,7 @@ final class ValidateServerActionTests: ZcashTestCase { let validateServerAction = setupAction() do { - _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + _ = try await validateServerAction.run(with: ActionContextMock()) { _ in } XCTFail("testValidateServerAction_ConsensusBranchIDError_InvalidRemoteBranch is expected to fail.") } catch ZcashError.compactBlockProcessorConsensusBranchID { } catch { @@ -118,7 +117,7 @@ final class ValidateServerActionTests: ZcashTestCase { let validateServerAction = setupAction() do { - _ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in } + _ = try await validateServerAction.run(with: ActionContextMock()) { _ in } XCTFail("testValidateServerAction_ConsensusBranchIDError_ValidRemoteBranch is expected to fail.") } catch let ZcashError.compactBlockProcessorWrongConsensusBranchId(expected, found) { XCTAssertEqual(expected, -1026109260) diff --git a/Tests/OfflineTests/SynchronizerOfflineTests.swift b/Tests/OfflineTests/SynchronizerOfflineTests.swift index c7be3969..007fe4ac 100644 --- a/Tests/OfflineTests/SynchronizerOfflineTests.swift +++ b/Tests/OfflineTests/SynchronizerOfflineTests.swift @@ -475,6 +475,25 @@ class SynchronizerOfflineTests: ZcashTestCase { } } + func testLinearIsSetAsDefault() async throws { + let databases = TemporaryDbBuilder.build() + let initializer = Initializer( + cacheDbURL: nil, + fsBlockDbRoot: databases.fsCacheDbRoot, + generalStorageURL: testGeneralStorageDirectory, + dataDbURL: databases.dataDB, + endpoint: LightWalletEndpoint(address: "lightwalletd.electriccoin.co", port: 9067, secure: true), + network: ZcashNetworkBuilder.network(for: .mainnet), + spendParamsURL: try __spendParamsURL(), + outputParamsURL: try __outputParamsURL(), + saplingParamsSourceURL: SaplingParamsSourceURL.tests, + alias: .default, + loggingPolicy: .default(.debug) + ) + + XCTAssertTrue(initializer.syncAlgorithm == .linear, "Spend before Sync is a beta feature so linear syncing is set to default.") + } + func synchronizerState(for internalSyncStatus: InternalSyncStatus) -> SynchronizerState { SynchronizerState( syncSessionID: .nullID, diff --git a/Tests/TestUtils/ActionContext+tests.swift b/Tests/TestUtils/ActionContext+tests.swift new file mode 100644 index 00000000..86f746de --- /dev/null +++ b/Tests/TestUtils/ActionContext+tests.swift @@ -0,0 +1,50 @@ +// +// ActionContext+tests.swift +// +// +// Created by Lukáš Korba on 24.08.2023. +// + +import XCTest +@testable import ZcashLightClientKit + +enum ActionContextResult: Equatable { + case `true` + case isNotMock + case called(Int) + case nilState + case wrongState(CBPState) +} + +extension ActionContext { + func checkStateIs(_ expectedState: CBPState) -> ActionContextResult { + guard let nextContextMock = self as? ActionContextMock else { + return .isNotMock + } + + if nextContextMock.updateStateCallsCount != 1 { + return .called(nextContextMock.updateStateCallsCount) + } + + guard let updateStateReceivedState = nextContextMock.updateStateReceivedState else { + return .nilState + } + + if updateStateReceivedState != expectedState { + return .wrongState(updateStateReceivedState) + } + + return .true + } +} + +extension ActionContextMock { + static func `default`() -> ActionContextMock { + let context = ActionContextMock() + + context.underlyingState = .idle + context.updateStateClosure = { _ in } + + return context + } +} diff --git a/Tests/TestUtils/Sourcery/AutoMockable.swift b/Tests/TestUtils/Sourcery/AutoMockable.swift index fa55cf69..a763d5b9 100644 --- a/Tests/TestUtils/Sourcery/AutoMockable.swift +++ b/Tests/TestUtils/Sourcery/AutoMockable.swift @@ -12,6 +12,7 @@ @testable import ZcashLightClientKit +extension ActionContext { } extension BlockDownloader { } extension BlockDownloaderService { } extension BlockEnhancer { } diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index 7c414cda..29b8e19c 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -7,6 +7,193 @@ import Foundation // MARK: - AutoMockable protocols +class ActionContextMock: ActionContext { + + + init( + ) { + } + var state: CBPState { + get { return underlyingState } + } + var underlyingState: CBPState! + var prevState: CBPState? + var syncControlData: SyncControlData { + get { return underlyingSyncControlData } + } + var underlyingSyncControlData: SyncControlData! + var preferredSyncAlgorithm: SyncAlgorithm { + get { return underlyingPreferredSyncAlgorithm } + } + var underlyingPreferredSyncAlgorithm: SyncAlgorithm! + var supportedSyncAlgorithm: SyncAlgorithm? + var requestedRewindHeight: BlockHeight? + var totalProgressRange: CompactBlockRange { + get { return underlyingTotalProgressRange } + } + var underlyingTotalProgressRange: CompactBlockRange! + var processedHeight: BlockHeight { + get { return underlyingProcessedHeight } + } + var underlyingProcessedHeight: BlockHeight! + var lastChainTipUpdateTime: TimeInterval { + get { return underlyingLastChainTipUpdateTime } + } + var underlyingLastChainTipUpdateTime: TimeInterval! + var lastScannedHeight: BlockHeight? + var lastEnhancedHeight: BlockHeight? + + // MARK: - update + + var updateStateCallsCount = 0 + var updateStateCalled: Bool { + return updateStateCallsCount > 0 + } + var updateStateReceivedState: CBPState? + var updateStateClosure: ((CBPState) async -> Void)? + + func update(state: CBPState) async { + updateStateCallsCount += 1 + updateStateReceivedState = state + await updateStateClosure!(state) + } + + // MARK: - update + + var updateSyncControlDataCallsCount = 0 + var updateSyncControlDataCalled: Bool { + return updateSyncControlDataCallsCount > 0 + } + var updateSyncControlDataReceivedSyncControlData: SyncControlData? + var updateSyncControlDataClosure: ((SyncControlData) async -> Void)? + + func update(syncControlData: SyncControlData) async { + updateSyncControlDataCallsCount += 1 + updateSyncControlDataReceivedSyncControlData = syncControlData + await updateSyncControlDataClosure!(syncControlData) + } + + // MARK: - update + + var updateTotalProgressRangeCallsCount = 0 + var updateTotalProgressRangeCalled: Bool { + return updateTotalProgressRangeCallsCount > 0 + } + var updateTotalProgressRangeReceivedTotalProgressRange: CompactBlockRange? + var updateTotalProgressRangeClosure: ((CompactBlockRange) async -> Void)? + + func update(totalProgressRange: CompactBlockRange) async { + updateTotalProgressRangeCallsCount += 1 + updateTotalProgressRangeReceivedTotalProgressRange = totalProgressRange + await updateTotalProgressRangeClosure!(totalProgressRange) + } + + // MARK: - update + + var updateProcessedHeightCallsCount = 0 + var updateProcessedHeightCalled: Bool { + return updateProcessedHeightCallsCount > 0 + } + var updateProcessedHeightReceivedProcessedHeight: BlockHeight? + var updateProcessedHeightClosure: ((BlockHeight) async -> Void)? + + func update(processedHeight: BlockHeight) async { + updateProcessedHeightCallsCount += 1 + updateProcessedHeightReceivedProcessedHeight = processedHeight + await updateProcessedHeightClosure!(processedHeight) + } + + // MARK: - update + + var updateLastChainTipUpdateTimeCallsCount = 0 + var updateLastChainTipUpdateTimeCalled: Bool { + return updateLastChainTipUpdateTimeCallsCount > 0 + } + var updateLastChainTipUpdateTimeReceivedLastChainTipUpdateTime: TimeInterval? + var updateLastChainTipUpdateTimeClosure: ((TimeInterval) async -> Void)? + + func update(lastChainTipUpdateTime: TimeInterval) async { + updateLastChainTipUpdateTimeCallsCount += 1 + updateLastChainTipUpdateTimeReceivedLastChainTipUpdateTime = lastChainTipUpdateTime + await updateLastChainTipUpdateTimeClosure!(lastChainTipUpdateTime) + } + + // MARK: - update + + var updateLastScannedHeightCallsCount = 0 + var updateLastScannedHeightCalled: Bool { + return updateLastScannedHeightCallsCount > 0 + } + var updateLastScannedHeightReceivedLastScannedHeight: BlockHeight? + var updateLastScannedHeightClosure: ((BlockHeight) async -> Void)? + + func update(lastScannedHeight: BlockHeight) async { + updateLastScannedHeightCallsCount += 1 + updateLastScannedHeightReceivedLastScannedHeight = lastScannedHeight + await updateLastScannedHeightClosure!(lastScannedHeight) + } + + // MARK: - update + + var updateLastDownloadedHeightCallsCount = 0 + var updateLastDownloadedHeightCalled: Bool { + return updateLastDownloadedHeightCallsCount > 0 + } + var updateLastDownloadedHeightReceivedLastDownloadedHeight: BlockHeight? + var updateLastDownloadedHeightClosure: ((BlockHeight) async -> Void)? + + func update(lastDownloadedHeight: BlockHeight) async { + updateLastDownloadedHeightCallsCount += 1 + updateLastDownloadedHeightReceivedLastDownloadedHeight = lastDownloadedHeight + await updateLastDownloadedHeightClosure!(lastDownloadedHeight) + } + + // MARK: - update + + var updateLastEnhancedHeightCallsCount = 0 + var updateLastEnhancedHeightCalled: Bool { + return updateLastEnhancedHeightCallsCount > 0 + } + var updateLastEnhancedHeightReceivedLastEnhancedHeight: BlockHeight? + var updateLastEnhancedHeightClosure: ((BlockHeight?) async -> Void)? + + func update(lastEnhancedHeight: BlockHeight?) async { + updateLastEnhancedHeightCallsCount += 1 + updateLastEnhancedHeightReceivedLastEnhancedHeight = lastEnhancedHeight + await updateLastEnhancedHeightClosure!(lastEnhancedHeight) + } + + // MARK: - update + + var updateSupportedSyncAlgorithmCallsCount = 0 + var updateSupportedSyncAlgorithmCalled: Bool { + return updateSupportedSyncAlgorithmCallsCount > 0 + } + var updateSupportedSyncAlgorithmReceivedSupportedSyncAlgorithm: SyncAlgorithm? + var updateSupportedSyncAlgorithmClosure: ((SyncAlgorithm) async -> Void)? + + func update(supportedSyncAlgorithm: SyncAlgorithm) async { + updateSupportedSyncAlgorithmCallsCount += 1 + updateSupportedSyncAlgorithmReceivedSupportedSyncAlgorithm = supportedSyncAlgorithm + await updateSupportedSyncAlgorithmClosure!(supportedSyncAlgorithm) + } + + // MARK: - update + + var updateRequestedRewindHeightCallsCount = 0 + var updateRequestedRewindHeightCalled: Bool { + return updateRequestedRewindHeightCallsCount > 0 + } + var updateRequestedRewindHeightReceivedRequestedRewindHeight: BlockHeight? + var updateRequestedRewindHeightClosure: ((BlockHeight) async -> Void)? + + func update(requestedRewindHeight: BlockHeight) async { + updateRequestedRewindHeightCallsCount += 1 + updateRequestedRewindHeightReceivedRequestedRewindHeight = requestedRewindHeight + await updateRequestedRewindHeightClosure!(requestedRewindHeight) + } + +} class BlockDownloaderMock: BlockDownloader {