[#1044] Implement EnhanceAction

- EnhanceAction tests focused on 2 different methods:
- decideWhatToDoNext covered separately, decisions where the state machine goes next
- run tests for different cases
- new mocks generated for enhacer
- some typos fixed

[#1044] Implement EnhanceAction (#1107)

- empty assert messages fixed
This commit is contained in:
Lukas Korba 2023-05-19 16:35:05 +02:00
parent d1dd251423
commit 6734af4bde
7 changed files with 425 additions and 10 deletions

View File

@ -45,8 +45,8 @@ extension EnhanceAction: Action {
// This action is executed on each downloaded and scanned batch (typically each 100 blocks). But we want to run enhancement each 1000 blocks.
// This action can use `InternalSyncProgress` and last scanned height to compute when it should do work.
// if latestScannedHeight == context.scanRanges.downloadAndScanRange?.upperBound then set state `enhance`. Everything is scanned.
// If latestScannedHeight < context.scanRanges.downloadAndScanRange?.upperBound thne set state to `download` because there are blocks to
// if latestScannedHeight == context.scanRanges.downloadAndScanRange?.upperBound then set state `clearCache`. Everything is scanned.
// If latestScannedHeight < context.scanRanges.downloadAndScanRange?.upperBound then set state to `download` because there are blocks to
// download and scan.
let lastScannedHeight = try await transactionRepository.lastScannedHeight()

View File

@ -52,7 +52,7 @@ public struct EnhancementProgress: Equatable {
}
protocol BlockEnhancer {
func enhance(at range: CompactBlockRange, didEnhance: (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]?
func enhance(at range: CompactBlockRange, didEnhance: @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]?
}
struct BlockEnhancerImpl {
@ -82,7 +82,7 @@ struct BlockEnhancerImpl {
}
extension BlockEnhancerImpl: BlockEnhancer {
func enhance(at range: CompactBlockRange, didEnhance: (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? {
func enhance(at range: CompactBlockRange, didEnhance: @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? {
try Task.checkCancellation()
logger.debug("Started Enhancing range: \(range)")

View File

@ -47,6 +47,7 @@ protocol InternalSyncProgressStorage {
func bool(forKey defaultName: String) -> Bool
func integer(forKey defaultName: String) -> Int
func set(_ value: Int, forKey defaultName: String)
// sourcery: mockedName="setBool"
func set(_ value: Bool, forKey defaultName: String)
@discardableResult
func synchronize() -> Bool

View File

@ -0,0 +1,285 @@
//
// EnhanceActionTests.swift
//
//
// Created by Lukáš Korba on 19.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class EnhanceActionTests: ZcashTestCase {
var underlyingDownloadAndScanRange: CompactBlockRange?
var underlyingEnhanceRange: CompactBlockRange?
override func setUp() {
super.setUp()
underlyingDownloadAndScanRange = nil
underlyingEnhanceRange = nil
}
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
XCTAssertTrue(
nextState == .clearCache,
"testEnhanceAction_decideWhatToDoNext_NoDownloadAndScanRange is expected to be .clearCache but received \(nextState)"
)
}
func testEnhanceAction_decideWhatToDoNext_NothingToDownloadAndScanLeft() async throws {
let enhanceAction = setupAction()
underlyingDownloadAndScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 2000)
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .clearCache,
"testEnhanceAction_decideWhatToDoNext_NothingToDownloadAndScanLeft is expected to be .clearCache but received \(nextState)"
)
}
func testEnhanceAction_decideWhatToDoNext_DownloadExpected() async throws {
let enhanceAction = setupAction()
underlyingDownloadAndScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 1500)
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .download,
"testEnhanceAction_decideWhatToDoNext_DownloadExpected is expected to be .download but received \(nextState)"
)
}
func testEnhanceAction_NoEnhanceRange() async throws {
let blockEnhancerMock = BlockEnhancerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1
let enhanceAction = setupAction(
blockEnhancerMock,
transactionRepositoryMock,
internalSyncProgressStorageMock
)
let syncContext = await setupActionContext()
do {
let _ = try await enhanceAction.run(with: syncContext) { _ in }
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertFalse(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is not expected to be called.")
XCTAssertFalse(internalSyncProgressStorageMock.integerForKeyCalled, "internalSyncProgress.load() is not expected to be called.")
} catch {
XCTFail("testEnhanceAction_NoEnhanceRange is not expected to fail. \(error)")
}
}
func testEnhanceAction_1000BlocksConditionNotFulfilled() async throws {
let blockEnhancerMock = BlockEnhancerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1
internalSyncProgressStorageMock.integerForKeyReturnValue = 1
let enhanceAction = setupAction(
blockEnhancerMock,
transactionRepositoryMock,
internalSyncProgressStorageMock
)
underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
do {
let _ = try await enhanceAction.run(with: syncContext) { _ in }
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertTrue(internalSyncProgressStorageMock.integerForKeyCalled, "internalSyncProgress.load() is expected to be called.")
XCTAssertFalse(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is not expected to be called.")
} catch {
XCTFail("testEnhanceAction_1000BlocksConditionNotFulfilled is not expected to fail. \(error)")
}
}
func testEnhanceAction_EnhancementOfBlocksCalled_FoundTransactions() async throws {
let blockEnhancerMock = BlockEnhancerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1500
internalSyncProgressStorageMock.integerForKeyReturnValue = 1
let transaction = ZcashTransaction.Overview(
accountId: 0,
blockTime: 1.0,
expiryHeight: 663206,
fee: Zatoshi(0),
id: 2,
index: 1,
hasChange: false,
memoCount: 1,
minedHeight: 663188,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000),
isExpiredUmined: false
)
blockEnhancerMock.enhanceAtDidEnhanceClosure = { _, didEnhance in
await didEnhance(EnhancementProgress.zero)
return [transaction]
}
let enhanceAction = setupAction(
blockEnhancerMock,
transactionRepositoryMock,
internalSyncProgressStorageMock
)
underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
do {
let _ = try await enhanceAction.run(with: syncContext) { event in
guard case let .foundTransactions(transactions, _) = event else {
XCTFail("Event is expected to be .foundTransactions but received \(event)")
return
}
XCTAssertTrue(transactions.count == 1)
guard let receivedTransaction = transactions.first else {
XCTFail("Transaction.first is expected to pass.")
return
}
XCTAssertEqual(receivedTransaction.expiryHeight, transaction.expiryHeight, "ReceivedTransaction differs from mocked one.")
}
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertTrue(internalSyncProgressStorageMock.integerForKeyCalled, "internalSyncProgress.load() is expected to be called.")
XCTAssertTrue(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is expected to be called.")
} catch {
XCTFail("testEnhanceAction_EnhancementOfBlocksCalled_FoundTransactions is not expected to fail. \(error)")
}
}
func testEnhanceAction_EnhancementOfBlocksCalled_minedTransaction() async throws {
let blockEnhancerMock = BlockEnhancerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1500
internalSyncProgressStorageMock.integerForKeyReturnValue = 1
let transaction = ZcashTransaction.Overview(
accountId: 0,
blockTime: 1.0,
expiryHeight: 663206,
fee: Zatoshi(0),
id: 2,
index: 1,
hasChange: false,
memoCount: 1,
minedHeight: 663188,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000),
isExpiredUmined: false
)
blockEnhancerMock.enhanceAtDidEnhanceClosure = { _, didEnhance in
await didEnhance(EnhancementProgress(
totalTransactions: 0,
enhancedTransactions: 0,
lastFoundTransaction: transaction,
range: 0...0,
newlyMined: true)
)
return nil
}
let enhanceAction = setupAction(
blockEnhancerMock,
transactionRepositoryMock,
internalSyncProgressStorageMock
)
underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
do {
let _ = try await enhanceAction.run(with: syncContext) { event in
guard case .minedTransaction(let minedTransaction) = event else {
XCTFail("Event is expected to be .minedTransaction but received \(event)")
return
}
XCTAssertEqual(minedTransaction.expiryHeight, transaction.expiryHeight, "MinedTransaction differs from mocked one.")
}
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertTrue(internalSyncProgressStorageMock.integerForKeyCalled, "internalSyncProgress.load() is expected to be called.")
XCTAssertTrue(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is expected to be called.")
} catch {
XCTFail("testEnhanceAction_EnhancementOfBlocksCalled_minedTransaction is not expected to fail. \(error)")
}
}
func setupActionContext() async -> ActionContext {
let syncContext: ActionContext = .init(state: .enhance)
let syncRanges = SyncRanges(
latestBlockHeight: 0,
downloadedButUnscannedRange: nil,
downloadAndScanRange: underlyingDownloadAndScanRange,
enhanceRange: underlyingEnhanceRange,
fetchUTXORange: nil,
latestScannedHeight: nil,
latestDownloadedBlockHeight: nil
)
await syncContext.update(syncRanges: syncRanges)
await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000)))
return syncContext
}
func setupAction(
_ blockEnhancerMock: BlockEnhancerMock = BlockEnhancerMock(),
_ transactionRepositoryMock: TransactionRepositoryMock = TransactionRepositoryMock(),
_ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(),
_ loggerMock: LoggerMock = LoggerMock()
) -> EnhanceAction {
mockContainer.register(type: InternalSyncProgress.self, isSingleton: true) { di in
InternalSyncProgress(alias: .default, storage: internalSyncProgressStorageMock, logger: loggerMock)
}
mockContainer.mock(type: BlockEnhancer.self, isSingleton: true) { _ in blockEnhancerMock }
mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock }
mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock }
let config: CompactBlockProcessor.Configuration = .standard(
for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0
)
return EnhanceAction(
container: mockContainer,
config: config
)
}
}

View File

@ -55,9 +55,9 @@ final class ScanActionTests: ZcashTestCase {
do {
_ = try await scanAction.run(with: syncContext) { _ in }
XCTAssertFalse(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertFalse(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.")
XCTAssertFalse(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is expected to be called.")
XCTAssertFalse(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is not expected to be called.")
XCTAssertFalse(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is not expected to be called.")
XCTAssertFalse(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is not expected to be called.")
} catch {
XCTFail("testScanAction_NextAction is not expected to fail. \(error)")
}
@ -76,8 +76,8 @@ final class ScanActionTests: ZcashTestCase {
do {
_ = try await scanAction.run(with: syncContext) { _ in }
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertFalse(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.")
XCTAssertFalse(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is expected to be called.")
XCTAssertFalse(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is not expected to be called.")
XCTAssertFalse(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is not expected to be called.")
} catch {
XCTFail("testScanAction_NextAction is not expected to fail. \(error)")
}

View File

@ -12,8 +12,10 @@
@testable import ZcashLightClientKit
extension BlockEnhancer { }
extension BlockScanner { }
extension BlockValidator { }
extension InternalSyncProgressStorage { }
extension LightWalletdInfo { }
extension LightWalletService { }
extension Logger { }
@ -22,5 +24,4 @@ extension Synchronizer { }
extension TransactionRepository { }
extension UTXOFetcher { }
extension ZcashRustBackendWelding { }
// sourcery:end:

View File

@ -7,6 +7,38 @@ import Foundation
// MARK: - AutoMockable protocols
class BlockEnhancerMock: BlockEnhancer {
init(
) {
}
// MARK: - enhance
var enhanceAtDidEnhanceThrowableError: Error?
var enhanceAtDidEnhanceCallsCount = 0
var enhanceAtDidEnhanceCalled: Bool {
return enhanceAtDidEnhanceCallsCount > 0
}
var enhanceAtDidEnhanceReceivedArguments: (range: CompactBlockRange, didEnhance: (EnhancementProgress) async -> Void)?
var enhanceAtDidEnhanceReturnValue: [ZcashTransaction.Overview]?
var enhanceAtDidEnhanceClosure: ((CompactBlockRange, @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]?)?
func enhance(at range: CompactBlockRange, didEnhance: @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? {
if let error = enhanceAtDidEnhanceThrowableError {
throw error
}
enhanceAtDidEnhanceCallsCount += 1
enhanceAtDidEnhanceReceivedArguments = (range: range, didEnhance: didEnhance)
if let closure = enhanceAtDidEnhanceClosure {
return try await closure(range, didEnhance)
} else {
return enhanceAtDidEnhanceReturnValue
}
}
}
class BlockScannerMock: BlockScanner {
@ -63,6 +95,102 @@ class BlockValidatorMock: BlockValidator {
try await validateClosure!()
}
}
class InternalSyncProgressStorageMock: InternalSyncProgressStorage {
init(
) {
}
// MARK: - bool
var boolForKeyCallsCount = 0
var boolForKeyCalled: Bool {
return boolForKeyCallsCount > 0
}
var boolForKeyReceivedDefaultName: String?
var boolForKeyReturnValue: Bool!
var boolForKeyClosure: ((String) -> Bool)?
func bool(forKey defaultName: String) -> Bool {
boolForKeyCallsCount += 1
boolForKeyReceivedDefaultName = defaultName
if let closure = boolForKeyClosure {
return closure(defaultName)
} else {
return boolForKeyReturnValue
}
}
// MARK: - integer
var integerForKeyCallsCount = 0
var integerForKeyCalled: Bool {
return integerForKeyCallsCount > 0
}
var integerForKeyReceivedDefaultName: String?
var integerForKeyReturnValue: Int!
var integerForKeyClosure: ((String) -> Int)?
func integer(forKey defaultName: String) -> Int {
integerForKeyCallsCount += 1
integerForKeyReceivedDefaultName = defaultName
if let closure = integerForKeyClosure {
return closure(defaultName)
} else {
return integerForKeyReturnValue
}
}
// MARK: - set
var setForKeyCallsCount = 0
var setForKeyCalled: Bool {
return setForKeyCallsCount > 0
}
var setForKeyReceivedArguments: (value: Int, defaultName: String)?
var setForKeyClosure: ((Int, String) -> Void)?
func set(_ value: Int, forKey defaultName: String) {
setForKeyCallsCount += 1
setForKeyReceivedArguments = (value: value, defaultName: defaultName)
setForKeyClosure!(value, defaultName)
}
// MARK: - set
var setBoolCallsCount = 0
var setBoolCalled: Bool {
return setBoolCallsCount > 0
}
var setBoolReceivedArguments: (value: Bool, defaultName: String)?
var setBoolClosure: ((Bool, String) -> Void)?
func set(_ value: Bool, forKey defaultName: String) {
setBoolCallsCount += 1
setBoolReceivedArguments = (value: value, defaultName: defaultName)
setBoolClosure!(value, defaultName)
}
// MARK: - synchronize
var synchronizeCallsCount = 0
var synchronizeCalled: Bool {
return synchronizeCallsCount > 0
}
var synchronizeReturnValue: Bool!
var synchronizeClosure: (() -> Bool)?
func synchronize() -> Bool {
synchronizeCallsCount += 1
if let closure = synchronizeClosure {
return closure()
} else {
return synchronizeReturnValue
}
}
}
class LightWalletServiceMock: LightWalletService {