[#1176] Cover Spend before Sync with tests

- WIP

[#1176] Cover Spend before Sync with tests

- next batch of updates

[#1176] Cover Spend before Sync with tests

- last batch of fixes and new tests

[#1176] Cover Spend before Sync with tests

- package.resolved updated

[#1176] Cover Spend before Sync with tests (#1212)

- added tests for brand new actions related Spend before Sync
- RewindActionTests
- UpdateChainTipActionTests
- UpdateSubtreeRootsActionTests
- ProcessSuggestedScanRangesActionTests
This commit is contained in:
Lukas Korba 2023-08-18 12:14:36 +02:00
parent 80eb3b1c8d
commit 95536638e5
27 changed files with 1504 additions and 332 deletions

View File

@ -158,7 +158,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "1411d9a839a62523997dae113150b2beccd6b3fc"
"revision" : "6a53c9e32520b46f8c70597e27b335105fabfc21"
}
}
],

View File

@ -113,7 +113,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "1411d9a839a62523997dae113150b2beccd6b3fc"
"revision" : "6a53c9e32520b46f8c70597e27b335105fabfc21"
}
}
],

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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()
}

View File

@ -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()

View File

@ -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)"
)
}
}

View File

@ -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)")
}
}
}

View File

@ -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)")
}
}
}

View File

@ -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."
)
}
}

View File

@ -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(),

View File

@ -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
}

View File

@ -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)")
}

View File

@ -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)")
}

View File

@ -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
)
}
}

View File

@ -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)
}
}

View File

@ -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
)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
)
}
}

View File

@ -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)

View File

@ -469,6 +469,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,

View File

@ -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
}
}

View File

@ -12,6 +12,7 @@
@testable import ZcashLightClientKit
extension ActionContext { }
extension BlockDownloader { }
extension BlockDownloaderService { }
extension BlockEnhancer { }

View File

@ -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 {