ZcashLightClientKit/Tests/DarksideTests/SynchronizerTests.swift

348 lines
12 KiB
Swift

//
// SynchronizerTests.swift
// DarksideTests
//
// Created by Francisco Gindre on 9/16/22.
//
import Combine
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class SynchronizerTests: ZcashTestCase {
let sendAmount = Zatoshi(1000)
var birthday: BlockHeight = 663150
let defaultLatestHeight: BlockHeight = 663175
var coordinator: TestCoordinator!
var expectedReorgHeight: BlockHeight = 665188
var expectedRewindHeight: BlockHeight = 665188
var reorgExpectation = XCTestExpectation(description: "reorg")
let branchID = "2bb40e60"
let chainName = "main"
let network = DarksideWalletDNetwork()
var cancellables: [AnyCancellable] = []
var sdkSynchronizerSyncStatusHandler: SDKSynchronizerSyncStatusHandler! = SDKSynchronizerSyncStatusHandler()
override func setUp() async throws {
try await super.setUp()
// don't use an exact birthday, users never do.
self.coordinator = try await TestCoordinator(
container: mockContainer,
walletBirthday: birthday + 50,
network: network
)
try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
let eventClosure: CompactBlockProcessor.EventClosure = { [weak self] event in
switch event {
case .handledReorg: self?.handleReorg(event: event)
default: break
}
}
await self.coordinator.synchronizer.blockProcessor.updateEventClosure(identifier: "tests", closure: eventClosure)
}
override func tearDown() async throws {
try await super.tearDown()
let coordinator = self.coordinator!
self.coordinator = nil
sdkSynchronizerSyncStatusHandler = nil
cancellables = []
try await coordinator.stop()
try? FileManager.default.removeItem(at: coordinator.databases.fsCacheDbRoot)
try? FileManager.default.removeItem(at: coordinator.databases.dataDB)
}
func handleReorg(event: CompactBlockProcessor.Event) {
guard case let .handledReorg(reorgHeight, rewindHeight) = event else { return XCTFail("empty reorg notification") }
logger.debug("--- REORG DETECTED \(reorgHeight)--- RewindHeight: \(rewindHeight)", file: #file, function: #function, line: #line)
XCTAssertEqual(reorgHeight, expectedReorgHeight)
reorgExpectation.fulfill()
}
func testSynchronizerStops() async throws {
/*
1. create fake chain
*/
let fullSyncLength = 100_000
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: fullSyncLength)
try coordinator.applyStaged(blockheight: birthday + fullSyncLength)
sleep(10)
let syncStoppedExpectation = XCTestExpectation(description: "SynchronizerStopped Expectation")
sdkSynchronizerSyncStatusHandler.subscribe(
to: coordinator.synchronizer.stateStream,
expectations: [.stopped: syncStoppedExpectation]
)
/*
sync to latest height
*/
try await coordinator.sync(
completion: { _ in
XCTFail("Sync should have stopped")
},
error: self.handleError
)
try await Task.sleep(nanoseconds: 5_000_000_000)
self.coordinator.synchronizer.stop()
await fulfillment(of: [syncStoppedExpectation], timeout: 6)
let status = await coordinator.synchronizer.status
XCTAssertEqual(status, .stopped)
let state = await coordinator.synchronizer.blockProcessor.state
XCTAssertEqual(state, .stopped)
}
// MARK: Wipe tests
func testWipeCalledWhichSyncDoesntRun() async throws {
/*
create fake chain
*/
let fullSyncLength = 1000
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: fullSyncLength)
try coordinator.applyStaged(blockheight: birthday + fullSyncLength)
sleep(2)
let syncFinished = XCTestExpectation(description: "SynchronizerSyncFinished Expectation")
/*
sync to latest height
*/
try await coordinator.sync(
completion: { _ in
syncFinished.fulfill()
},
error: handleError
)
await fulfillment(of: [syncFinished], timeout: 3)
let wipeFinished = XCTestExpectation(description: "SynchronizerWipeFinished Expectation")
/*
Call wipe
*/
coordinator.synchronizer.wipe()
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
wipeFinished.fulfill()
case .failure(let error):
XCTFail("Wipe should finish successfully. \(error)")
}
},
receiveValue: {
XCTFail("No no value should be received from wipe.")
}
)
.store(in: &cancellables)
await fulfillment(of: [wipeFinished], timeout: 1)
/*
Check that wipe cleared everything that is expected
*/
await checkThatWipeWorked()
}
func testWipeCalledWhileSyncRuns() async throws {
/*
1. create fake chain
*/
let fullSyncLength = 50_000
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: fullSyncLength)
try coordinator.applyStaged(blockheight: birthday + fullSyncLength)
sleep(5)
/*
Start sync
*/
try await coordinator.sync(
completion: { _ in
XCTFail("Sync should have stopped")
},
error: self.handleError
)
try await Task.sleep(nanoseconds: 2_000_000_000)
// Just to be sure that blockProcessor is still syncing and that this test does what it should.
let blockProcessorState = await coordinator.synchronizer.blockProcessor.state
XCTAssertEqual(blockProcessorState, .syncing)
let wipeFinished = XCTestExpectation(description: "SynchronizerWipeFinished Expectation")
/*
Call wipe
*/
coordinator.synchronizer.wipe()
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
wipeFinished.fulfill()
case .failure(let error):
XCTFail("Wipe should finish successfully. \(error)")
}
},
receiveValue: {
XCTFail("No no value should be received from wipe.")
}
)
.store(in: &cancellables)
await fulfillment(of: [wipeFinished], timeout: 6)
/*
Check that wipe cleared everything that is expected
*/
await checkThatWipeWorked()
}
private func checkThatWipeWorked() async {
let storage = await self.coordinator.synchronizer.blockProcessor.storage as! FSCompactBlockRepository
let fm = FileManager.default
print(coordinator.synchronizer.initializer.dataDbURL.path)
XCTAssertFalse(fm.fileExists(atPath: coordinator.synchronizer.initializer.dataDbURL.path), "Data DB should be deleted.")
XCTAssertTrue(fm.fileExists(atPath: storage.blocksDirectory.path), "FS Cache directory should exist")
XCTAssertEqual(try fm.contentsOfDirectory(atPath: storage.blocksDirectory.path), [], "FS Cache directory should be empty")
let internalSyncProgress = InternalSyncProgress(
alias: .default,
storage: UserDefaults.standard,
logger: logger
)
let latestDownloadedBlockHeight = await internalSyncProgress.load(.latestDownloadedBlockHeight)
let latestEnhancedHeight = await internalSyncProgress.load(.latestEnhancedHeight)
let latestUTXOFetchedHeight = await internalSyncProgress.load(.latestUTXOFetchedHeight)
XCTAssertEqual(latestDownloadedBlockHeight, 0, "internalSyncProgress latestDownloadedBlockHeight should be 0")
XCTAssertEqual(latestEnhancedHeight, 0, "internalSyncProgress latestEnhancedHeight should be 0")
XCTAssertEqual(latestUTXOFetchedHeight, 0, "internalSyncProgress latestUTXOFetchedHeight should be 0")
let blockProcessorState = await coordinator.synchronizer.blockProcessor.state
XCTAssertEqual(blockProcessorState, .stopped, "CompactBlockProcessor state should be stopped")
let status = await coordinator.synchronizer.status
XCTAssertEqual(status, .unprepared, "SDKSynchronizer state should be unprepared")
}
func handleError(_ error: Error?) async {
_ = try? await coordinator.stop()
guard let testError = error else {
XCTFail("failed with nil error")
return
}
XCTFail("Failed with error: \(testError)")
}
// MARK: Rewind tests
func testRewindCalledWhileSyncRuns() async throws {
// 1 sync and get spendable funds
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: 663200)
let initialVerifiedBalance: Zatoshi = try await coordinator.synchronizer.getShieldedVerifiedBalance()
let initialTotalBalance: Zatoshi = try await coordinator.synchronizer.getShieldedBalance()
sleep(1)
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
do {
try await coordinator.sync(
completion: { _ in
firstSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [firstSyncExpectation], timeout: 12)
let verifiedBalance: Zatoshi = try await coordinator.synchronizer.getShieldedVerifiedBalance()
let totalBalance: Zatoshi = try await coordinator.synchronizer.getShieldedBalance()
// 2 check that there are no unconfirmed funds
XCTAssertTrue(verifiedBalance > network.constants.defaultFee(for: defaultLatestHeight))
XCTAssertEqual(verifiedBalance, totalBalance)
// Add more blocks to the chain so the long sync can start.
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: 10000)
try coordinator.applyStaged(blockheight: birthday + 10000)
sleep(2)
do {
// Start the long sync.
try await coordinator.sync(
completion: { _ in },
error: self.handleError
)
} catch {
await handleError(error)
}
// Wait 0.5 second and then start rewind while sync is in progress.
let waitExpectation = XCTestExpectation()
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
waitExpectation.fulfill()
}
await fulfillment(of: [waitExpectation], timeout: 1)
let rewindExpectation = XCTestExpectation(description: "RewindExpectation")
// rewind to birthday
coordinator.synchronizer.rewind(.birthday)
.sink(
receiveCompletion: { result in
switch result {
case .finished:
break
case let .failure(error):
XCTFail("Rewind failed with error: \(error)")
}
rewindExpectation.fulfill()
},
receiveValue: { _ in rewindExpectation.fulfill() }
)
.store(in: &cancellables)
await fulfillment(of: [rewindExpectation], timeout: 5)
// assert that after the new height is
let lastScannedHeight = try await coordinator.synchronizer.initializer.transactionRepository.lastScannedHeight()
XCTAssertEqual(lastScannedHeight, self.birthday)
// check that the balance is cleared
let expectedVerifiedBalance = try await coordinator.synchronizer.getShieldedVerifiedBalance()
let expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(initialVerifiedBalance, expectedVerifiedBalance)
XCTAssertEqual(initialTotalBalance, expectedBalance)
}
}