[#895] Add Sync Session ID Synchronizer State (#906)

Closes #895

Add Sync Session ID to `SynchronizerState`.

A SyncSession is an attempt to sync the blockchain within the lifetime of a Synchronizer.
A Synchronizer can sync many times, when synced it will refresh every ~20 seconds +- random padding.
each sync attempt will have a different UUID even if it's from the same instance of SDKSynchronizer.

**How are SyncSessions are delimited? With `SyncStatus` changes.**

changes from [`.unprepared`|`.error`|`.disconnected`|`.stopped`] to `.syncing` assign a new sessionID to the synchronizer.

Any other transitions won't.

`areTwoStatusesDifferent ` was refactored to a helper function of `SyncStatus`

**How are IDs generated?**

ID generation is not mandated but delegated to a protocol `SyncSessionIDGenerator`.
Tests inject their own deterministic generator to avoid test flakiness.

Default implementation of SyncSessionIDGenerator is

````Swift
struct UniqueSyncSessionIDGenerator {}

extension UniqueSyncSessionIDGenerator: SyncSessionIDGenerator {
    func nextID() -> UUID {
        UUID()
    }
}
````

**SyncSession Pseudo-Atomicity and thread safety**

SyncSession is a type alias of a GenericActor holding a UUID

````
typealias SyncSession = GenericActor<UUID>

extension SyncSession {
    /// updates the current sync session to a new value with the given generator
    /// - Parameters generator: a `SyncSessionIDGenerator`
    /// - returns: the `UUID` of the newly updated value.
    @discardableResult
    func newSession(with generator: SyncSessionIDGenerator) async -> UUID {
        return await self.update(generator.nextID())
    }
}
````

Closes #895

SessionTicker struct to control session transitions.

switching to `.unprepared` now makes syncID to be `.nullID`
This commit is contained in:
Francisco Gindre 2023-04-07 09:02:05 -03:00 committed by GitHub
parent e3bc06b694
commit 6b7fbdd908
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 493 additions and 32 deletions

View File

@ -1108,8 +1108,8 @@ actor CompactBlockProcessor {
timeInterval: interval,
repeats: true,
block: { [weak self] _ in
Task { [self] in
guard let self = self else { return }
Task { [weak self] in
guard let self else { return }
if await self.shouldStart {
self.logger.debug(
"""

View File

@ -64,15 +64,27 @@ public enum ConnectionState {
/// the connection has been closed
case shutdown
}
/// Reports the state of a synchronizer.
public struct SynchronizerState: Equatable {
/// Unique Identifier for the current sync attempt
/// - Note: Although on it's lifetime a synchronizer will attempt to sync between random fractions of a minute (when idle),
/// each sync attempt will be considered a new sync session. This is to maintain a consistent UUID cadence
/// given how application lifecycle varies between OS Versions, platforms, etc.
/// SyncSessionIDs are provided to users
public var syncSessionID: UUID
/// shielded balance known to this synchronizer given the data that has processed locally
public var shieldedBalance: WalletBalance
/// transparent balance known to this synchronizer given the data that has processed locally
public var transparentBalance: WalletBalance
/// status of the whole sync process
public var syncStatus: SyncStatus
/// height of the latest scanned block known to this synchronizer.
public var latestScannedHeight: BlockHeight
/// Represents a synchronizer that has made zero progress hasn't done a sync attempt
public static var zero: SynchronizerState {
SynchronizerState(
syncSessionID: .nullID,
shieldedBalance: .zero,
transparentBalance: .zero,
syncStatus: .unprepared,
@ -423,3 +435,10 @@ extension SyncStatus {
}
}
}
extension UUID {
/// UUID 00000000-0000-0000-0000-000000000000
static var nullID: UUID {
UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
}
}

View File

@ -43,6 +43,9 @@ public class SDKSynchronizer: Synchronizer {
private var transactionRepository: TransactionRepository
private var utxoRepository: UnspentTransactionOutputRepository
private var syncSessionIDGenerator: SyncSessionIDGenerator
private var syncSession: SyncSession
private var syncSessionTicker: SessionTicker
private var syncStartDate: Date?
/// Creates an SDKSynchronizer instance
@ -61,7 +64,9 @@ public class SDKSynchronizer: Synchronizer {
logger: initializer.logger,
walletBirthdayProvider: { initializer.walletBirthday }
),
metrics: metrics
metrics: metrics,
syncSessionIDGenerator: UniqueSyncSessionIDGenerator(),
syncSessionTicker: .live
)
}
@ -72,7 +77,9 @@ public class SDKSynchronizer: Synchronizer {
transactionRepository: TransactionRepository,
utxoRepository: UnspentTransactionOutputRepository,
blockProcessor: CompactBlockProcessor,
metrics: SDKMetrics
metrics: SDKMetrics,
syncSessionIDGenerator: SyncSessionIDGenerator,
syncSessionTicker: SessionTicker
) {
self.connectionState = .idle
self.underlyingStatus = GenericActor(status)
@ -84,7 +91,10 @@ public class SDKSynchronizer: Synchronizer {
self.network = initializer.network
self.metrics = metrics
self.logger = initializer.logger
self.syncSessionIDGenerator = syncSessionIDGenerator
self.syncSession = SyncSession(.nullID)
self.syncSessionTicker = syncSessionTicker
initializer.lightWalletService.connectionStateChange = { [weak self] oldState, newState in
self?.connectivityStateChanged(oldState: oldState, newState: newState)
}
@ -572,7 +582,8 @@ public class SDKSynchronizer: Synchronizer {
// MARK: notify state
private func snapshotState(status: SyncStatus) async -> SynchronizerState {
SynchronizerState(
await SynchronizerState(
syncSessionID: syncSession.value,
shieldedBalance: WalletBalance(
verified: (try? await getShieldedVerifiedBalance()) ?? .zero,
total: (try? await getShieldedBalance()) ?? .zero
@ -592,12 +603,23 @@ public class SDKSynchronizer: Synchronizer {
// When new snapshot is created balance is checked. And when balance is checked and data DB doesn't exist then rust initialise new database.
// So it's necessary to not create new snapshot after status is switched to `unprepared` otherwise data DB exists after wipe
if newStatus == .unprepared {
newState = SynchronizerState.zero
var nextState = SynchronizerState.zero
let nextSessionID = await self.syncSession.update(.nullID)
nextState.syncSessionID = nextSessionID
newState = nextState
} else {
if areTwoStatusesDifferent(firstStatus: oldStatus, secondStatus: newStatus) {
if oldStatus.isDifferent(from: newStatus) {
if SessionTicker.live.isNewSyncSession(oldStatus, newStatus) {
await self.syncSession.newSession(with: self.syncSessionIDGenerator)
}
newState = await snapshotState(status: newStatus)
} else {
newState = SynchronizerState(
newState = await SynchronizerState(
syncSessionID: syncSession.value,
shieldedBalance: latestState.shieldedBalance,
transparentBalance: latestState.transparentBalance,
syncStatus: newStatus,
@ -610,20 +632,6 @@ public class SDKSynchronizer: Synchronizer {
updateStateStream(with: latestState)
}
private func areTwoStatusesDifferent(firstStatus: SyncStatus, secondStatus: SyncStatus) -> Bool {
switch (firstStatus, secondStatus) {
case (.unprepared, .unprepared): return false
case (.syncing, .syncing): return false
case (.enhancing, .enhancing): return false
case (.fetching, .fetching): return false
case (.synced, .synced): return false
case (.stopped, .stopped): return false
case (.disconnected, .disconnected): return false
case (.error, .error): return false
default: return true
}
}
private func updateStateStream(with newState: SynchronizerState) {
streamsUpdateQueue.async { [weak self] in
self?.stateSubject.send(newState)
@ -739,3 +747,44 @@ extension SDKSynchronizer {
}
}
}
extension SyncStatus {
func isDifferent(from otherStatus: SyncStatus) -> Bool {
switch (self, otherStatus) {
case (.unprepared, .unprepared): return false
case (.syncing, .syncing): return false
case (.enhancing, .enhancing): return false
case (.fetching, .fetching): return false
case (.synced, .synced): return false
case (.stopped, .stopped): return false
case (.disconnected, .disconnected): return false
case (.error, .error): return false
default: return true
}
}
}
struct SessionTicker {
/// Helper function to determine whether we are in front of a SyncSession change for a given syncStatus
/// transition we consider that every sync attempt is a new sync session and should have it's unique UUID reported.
var isNewSyncSession: (SyncStatus, SyncStatus) -> Bool
}
extension SessionTicker {
static let live = SessionTicker { oldStatus, newStatus in
// if the state hasn't changed to a different syncStatus member
guard oldStatus.isDifferent(from: newStatus) else { return false }
switch (oldStatus, newStatus) {
case (.unprepared, .syncing):
return true
case (.error, .syncing),
(.disconnected, .syncing),
(.stopped, .syncing),
(.synced, .syncing):
return true
default:
return false
}
}
}

View File

@ -0,0 +1,33 @@
//
// File.swift
//
//
// Created by Francisco Gindre on 3/31/23.
//
import Foundation
protocol SyncSessionIDGenerator {
func nextID() -> UUID
}
struct UniqueSyncSessionIDGenerator {}
extension UniqueSyncSessionIDGenerator: SyncSessionIDGenerator {
func nextID() -> UUID {
UUID()
}
}
typealias SyncSession = GenericActor<UUID>
extension SyncSession {
/// updates the current sync session to a new value with the given generator
/// - Parameters generator: a `SyncSessionIDGenerator`
/// - returns: the `UUID` of the newly updated value.
@discardableResult
func newSession(with generator: SyncSessionIDGenerator) async -> UUID {
return await self.update(generator.nextID())
}
}

View File

@ -10,7 +10,7 @@ import Combine
@testable import TestUtils
@testable import ZcashLightClientKit
class SychronizerDarksideTests: XCTestCase {
class SynchronizerDarksideTests: XCTestCase {
let sendAmount: Int64 = 1000
let defaultLatestHeight: BlockHeight = 663175
let branchID = "2bb40e60"
@ -26,11 +26,11 @@ class SychronizerDarksideTests: XCTestCase {
var reorgExpectation = XCTestExpectation(description: "reorg")
var foundTransactions: [ZcashTransaction.Overview] = []
var cancellables: [AnyCancellable] = []
var idGenerator: MockSyncSessionIDGenerator!
override func setUp() async throws {
try await super.setUp()
self.coordinator = try await TestCoordinator(walletBirthday: birthday, network: network)
idGenerator = MockSyncSessionIDGenerator(ids: [.deadbeef])
self.coordinator = try await TestCoordinator(walletBirthday: birthday, network: network, syncSessionIDGenerator: idGenerator)
try self.coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
}
@ -78,6 +78,8 @@ class SychronizerDarksideTests: XCTestCase {
}
func testFoundManyTransactions() async throws {
self.idGenerator.ids = [.deadbeef, .beefbeef, .beefdead]
coordinator.synchronizer.eventStream
.map { event in
guard case let .foundTransactions(transactions, _) = event else { return nil }
@ -142,6 +144,8 @@ class SychronizerDarksideTests: XCTestCase {
}
func testLastStates() async throws {
self.idGenerator.ids = [.deadbeef]
var cancellables: [AnyCancellable] = []
var states: [SynchronizerState] = []
@ -169,32 +173,39 @@ class SychronizerDarksideTests: XCTestCase {
wait(for: [preTxExpectation], timeout: 5)
let uuids = self.idGenerator.ids
let expectedStates: [SynchronizerState] = [
SynchronizerState(
syncSessionID: .nullID,
shieldedBalance: .zero,
transparentBalance: .zero,
syncStatus: .disconnected,
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: .zero,
transparentBalance: .zero,
syncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)),
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: .zero,
transparentBalance: .zero,
syncStatus: .syncing(BlockProgress(startHeight: 663150, targetHeight: 663189, progressHeight: 663189)),
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .enhancing(EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0)),
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .enhancing(
@ -223,6 +234,7 @@ class SychronizerDarksideTests: XCTestCase {
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .enhancing(
@ -251,12 +263,14 @@ class SychronizerDarksideTests: XCTestCase {
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .fetching,
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .synced,
@ -267,6 +281,202 @@ class SychronizerDarksideTests: XCTestCase {
XCTAssertEqual(states, expectedStates)
}
func testSyncSessionUpdates() async throws {
var cancellables: [AnyCancellable] = []
self.idGenerator.ids = [.deadbeef, .beefbeef]
var states: [SynchronizerState] = []
try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName)
let receivedTxHeight: BlockHeight = 663188
try coordinator.applyStaged(blockheight: receivedTxHeight + 1)
sleep(2)
let preTxExpectation = XCTestExpectation(description: "pre receive")
coordinator.synchronizer.stateStream
.sink { state in
states.append(state)
}
.store(in: &cancellables)
try await coordinator.sync(
completion: { _ in
preTxExpectation.fulfill()
},
error: self.handleError
)
wait(for: [preTxExpectation], timeout: 5)
let uuids = idGenerator.ids
let expectedStates: [SynchronizerState] = [
SynchronizerState(
syncSessionID: .nullID,
shieldedBalance: .zero,
transparentBalance: .zero,
syncStatus: .disconnected,
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: .zero,
transparentBalance: .zero,
syncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)),
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: .zero,
transparentBalance: .zero,
syncStatus: .syncing(BlockProgress(startHeight: 663150, targetHeight: 663189, progressHeight: 663189)),
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .enhancing(EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0)),
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .enhancing(
EnhancementProgress(
totalTransactions: 2,
enhancedTransactions: 1,
lastFoundTransaction: ZcashTransaction.Overview(
blockTime: 1.0,
expiryHeight: 663206,
fee: Zatoshi(0),
id: 2,
index: 1,
isWalletInternal: true,
hasChange: false,
memoCount: 1,
minedHeight: 663188,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000)
),
range: 663150...663189
)
),
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .enhancing(
EnhancementProgress(
totalTransactions: 2,
enhancedTransactions: 2,
lastFoundTransaction: ZcashTransaction.Overview(
blockTime: 1.0,
expiryHeight: 663192,
fee: Zatoshi(0),
id: 1,
index: 1,
isWalletInternal: true,
hasChange: false,
memoCount: 1,
minedHeight: 663174,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000)
),
range: 663150...663189
)
),
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .fetching,
latestScannedHeight: 663150
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .synced,
latestScannedHeight: 663189
)
]
XCTAssertEqual(states, expectedStates)
try coordinator.service.applyStaged(nextLatestHeight: 663_200)
sleep(1)
states.removeAll()
let secondSyncExpectation = XCTestExpectation(description: "second sync")
try await coordinator.sync(
completion: { _ in
secondSyncExpectation.fulfill()
},
error: self.handleError
)
wait(for: [secondSyncExpectation], timeout: 5)
let secondBatchOfExpectedStates: [SynchronizerState] = [
SynchronizerState(
syncSessionID: uuids[1],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)),
latestScannedHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[1],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .syncing(BlockProgress(startHeight: 663190, targetHeight: 663200, progressHeight: 663200)),
latestScannedHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[1],
shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .enhancing(EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0)),
latestScannedHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[1],
shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .fetching,
latestScannedHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[1],
shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
syncStatus: .synced,
latestScannedHeight: 663200
)
]
XCTAssertEqual(states, secondBatchOfExpectedStates)
}
func testSyncAfterWipeWorks() async throws {
try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName)
let receivedTxHeight: BlockHeight = 663188
@ -340,3 +550,9 @@ extension Zatoshi: CustomDebugStringConvertible {
"Zatoshi(\(self.amount))"
}
}
extension UUID {
static let deadbeef = UUID(uuidString: "DEADBEEF-BEEF-FAFA-BEEF-FAFAFAFAFAFA")!
static let beefbeef = UUID(uuidString: "BEEFBEEF-BEEF-DEAD-BEEF-BEEFEBEEFEBE")!
static let beefdead = UUID(uuidString: "BEEFDEAD-BEEF-FAFA-DEAD-EAEAEAEAEAEA")!
}

View File

@ -40,6 +40,7 @@ class ClosureSynchronizerOfflineTests: XCTestCase {
func testStateStreamEmitsAsExpected() {
let state = SynchronizerState(
syncSessionID: .nullID,
shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)),
transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)),
syncStatus: .fetching,
@ -70,6 +71,7 @@ class ClosureSynchronizerOfflineTests: XCTestCase {
func testLatestStateIsAsExpected() {
let state = SynchronizerState(
syncSessionID: .nullID,
shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)),
transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)),
syncStatus: .fetching,

View File

@ -38,6 +38,7 @@ class CombineSynchronizerOfflineTests: XCTestCase {
func testStateStreamEmitsAsExpected() {
let state = SynchronizerState(
syncSessionID: .nullID,
shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)),
transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)),
syncStatus: .fetching,
@ -68,6 +69,7 @@ class CombineSynchronizerOfflineTests: XCTestCase {
func testLatestStateIsAsExpected() {
let state = SynchronizerState(
syncSessionID: .nullID,
shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)),
transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)),
syncStatus: .fetching,

View File

@ -319,4 +319,88 @@ class SynchronizerOfflineTests: XCTestCase {
wait(for: [expectation], timeout: 1)
}
func testIsNewSessionOnUnpreparedToValidTransition() {
XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(.nullProgress)))
}
func testIsNotNewSessionOnUnpreparedToStateThatWontSync() {
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .error(SynchronizerError.criticalError)))
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .disconnected))
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .unprepared))
}
func testIsNotNewSessionOnUnpreparedToInvalidOrUnexpectedTransitions() {
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .synced))
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .fetching))
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .enhancing(.zero)))
}
func testIsNotNewSyncSessionOnSameSession() {
XCTAssertFalse(
SessionTicker.live.isNewSyncSession(
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 3)
),
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
)
)
)
}
func testIsNewSyncSessionWhenRestartingFromError() {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.error(SynchronizerError.generalError(message: "some error")),
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
)
)
)
}
func testIsNewSyncSessionWhenStartingFromSynced() {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.synced,
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
)
)
)
}
func testIsNewSyncSessionWhenStartingFromDisconnected() {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.disconnected,
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
)
)
)
}
func testIsNewSyncSessionWhenStartingFromStopped() {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.stopped,
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
)
)
)
}
func testSyncStatusesDontDifferWhenOuterStatusIsTheSame() {
XCTAssertFalse(SyncStatus.error(SynchronizerError.criticalError).isDifferent(from: .error(.invalidAccount)))
XCTAssertFalse(SyncStatus.disconnected.isDifferent(from: .disconnected))
XCTAssertFalse(SyncStatus.fetching.isDifferent(from: .fetching))
XCTAssertFalse(SyncStatus.stopped.isDifferent(from: .stopped))
XCTAssertFalse(SyncStatus.synced.isDifferent(from: .synced))
XCTAssertFalse(SyncStatus.syncing(.nullProgress).isDifferent(from: .syncing(.nullProgress)))
XCTAssertFalse(SyncStatus.unprepared.isDifferent(from: .unprepared))
}
}

View File

@ -0,0 +1,22 @@
//
// File.swift
//
//
// Created by Francisco Gindre on 3/31/23.
//
import Foundation
@testable import ZcashLightClientKit
/// This generator will consume the list of UUID passed and fail if empty.
class MockSyncSessionIDGenerator: SyncSessionIDGenerator {
var ids: [UUID]
init(ids: [UUID]) {
self.ids = ids
}
func nextID() -> UUID {
ids.removeFirst()
}
}

View File

@ -0,0 +1,31 @@
//
// File.swift
//
//
// Created by Francisco Gindre on 3/31/23.
//
import Foundation
@testable import ZcashLightClientKit
extension SDKSynchronizer {
convenience init(initializer: Initializer, sessionGenerator: SyncSessionIDGenerator, sessionTicker: SessionTicker) {
let metrics = SDKMetrics()
self.init(
status: .unprepared,
initializer: initializer,
transactionManager: OutboundTransactionManagerBuilder.build(initializer: initializer),
transactionRepository: initializer.transactionRepository,
utxoRepository: UTXORepositoryBuilder.build(initializer: initializer),
blockProcessor: CompactBlockProcessor(
initializer: initializer,
metrics: metrics,
logger: initializer.logger,
walletBirthdayProvider: { initializer.walletBirthday }
),
metrics: metrics,
syncSessionIDGenerator: sessionGenerator,
syncSessionTicker: sessionTicker
)
}
}

View File

@ -58,7 +58,8 @@ class TestCoordinator {
walletBirthday: BlockHeight,
network: ZcashNetwork,
callPrepareInConstructor: Bool = true,
endpoint: LightWalletEndpoint = TestCoordinator.defaultEndpoint
endpoint: LightWalletEndpoint = TestCoordinator.defaultEndpoint,
syncSessionIDGenerator: SyncSessionIDGenerator = UniqueSyncSessionIDGenerator()
) async throws {
let derivationTool = DerivationTool(networkType: network.networkType)
@ -76,7 +77,8 @@ class TestCoordinator {
walletBirthday: walletBirthday,
network: network,
callPrepareInConstructor: callPrepareInConstructor,
endpoint: endpoint
endpoint: endpoint,
syncSessionIDGenerator: syncSessionIDGenerator
)
}
@ -87,7 +89,8 @@ class TestCoordinator {
walletBirthday: BlockHeight,
network: ZcashNetwork,
callPrepareInConstructor: Bool = true,
endpoint: LightWalletEndpoint = TestCoordinator.defaultEndpoint
endpoint: LightWalletEndpoint = TestCoordinator.defaultEndpoint,
syncSessionIDGenerator: SyncSessionIDGenerator
) async throws {
await InternalSyncProgress(alias: alias, storage: UserDefaults.standard, logger: logger).rewind(to: 0)
@ -114,7 +117,7 @@ class TestCoordinator {
logLevel: .debug
)
let synchronizer = SDKSynchronizer(initializer: initializer)
let synchronizer = SDKSynchronizer(initializer: initializer, sessionGenerator: syncSessionIDGenerator, sessionTicker: .live)
self.synchronizer = synchronizer
subscribeToState(synchronizer: self.synchronizer)