Add new tests and cleanup old ones.

squash test cleanup


squash tests
This commit is contained in:
Kevin Gorham 2021-04-09 22:29:29 -04:00
parent 4a90e6bf24
commit dffb6f257f
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
27 changed files with 600 additions and 539 deletions

View File

@ -244,6 +244,8 @@ dependencies {
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation "com.squareup.okhttp3:okhttp:3.8.0"
androidTestImplementation 'ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0'
// sample mnemonic plugin
androidTestImplementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.1'

View File

@ -1,110 +1,98 @@
package cash.z.ecc.android.sdk
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.ZcashSdk
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class InitializerTest {
lateinit var initializer: Initializer
@After
fun cleanUp() {
// don't leave databases sitting around after this test is run
if (::initializer.isInitialized) initializer.erase()
}
@Test
fun testInit() {
val height = 980000
initializer = Initializer(context) { config ->
config.importedWalletBirthday(height)
config.setViewingKeys(
"zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
"zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7"
)
config.alias = "VkInitTest1"
}
assertEquals(height, initializer.birthday.height)
initializer.erase()
}
@Test
fun testErase() {
val alias = "VkInitTest2"
initializer = Initializer(context) { config ->
config.importedWalletBirthday(1_419_900)
config.setViewingKeys(
"zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
"zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7"
)
config.alias = alias
}
assertTrue("Failed to erase initializer", Initializer.erase(context, alias))
assertFalse("Expected false when erasing nothing.", Initializer.erase(context))
}
@Test(expected = InitializerException.MissingDefaultBirthdayException::class)
fun testMissingBirthday() {
val config = Initializer.Config { config ->
config.setViewingKeys("vk1")
}
config.validate()
}
@Test(expected = InitializerException.InvalidBirthdayHeightException::class)
fun testOutOfBoundsBirthday() {
val config = Initializer.Config { config ->
config.setViewingKeys("vk1")
config.setBirthdayHeight(ZcashSdk.SAPLING_ACTIVATION_HEIGHT - 1)
}
config.validate()
}
@Test
fun testImportedWalletUsesSaplingActivation() {
initializer = Initializer(context) { config ->
config.setViewingKeys("vk1")
config.importWallet(ByteArray(32))
}
assertEquals("Incorrect height used for import.", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)
}
@Test
fun testDefaultToOldestHeight_true() {
initializer = Initializer(context) { config ->
config.setViewingKeys("vk1")
config.setBirthdayHeight(null, true)
}
assertEquals("Height should equal sapling activation height when defaultToOldestHeight is true", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)
}
@Test
fun testDefaultToOldestHeight_false() {
val initialHeight = 750_000
initializer = Initializer(context) { config ->
config.setViewingKeys("vk1")
config.setBirthdayHeight(initialHeight, false)
}
val h = initializer.birthday.height
assertNotEquals("Height should not equal sapling activation height when defaultToOldestHeight is false", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, h)
assertTrue("expected $h to be higher", h >= initialHeight)
}
companion object {
private val context = InstrumentationRegistry.getInstrumentation().context
init {
Twig.plant(TroubleshootingTwig())
}
}
// lateinit var initializer: Initializer
//
// @After
// fun cleanUp() {
// // don't leave databases sitting around after this test is run
// if (::initializer.isInitialized) initializer.erase()
// }
//
// @Test
// fun testInit() {
// val height = 980000
//
// initializer = Initializer(context) { config ->
// config.importedWalletBirthday(height)
// config.setViewingKeys(
// "zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
// "zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7"
// )
// config.alias = "VkInitTest1"
// }
// assertEquals(height, initializer.birthday.height)
// initializer.erase()
// }
//
// @Test
// fun testErase() {
// val alias = "VkInitTest2"
// initializer = Initializer(context) { config ->
// config.importedWalletBirthday(1_419_900)
// config.setViewingKeys(
// "zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
// "zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7"
// )
// config.alias = alias
// }
//
// assertTrue("Failed to erase initializer", Initializer.erase(context, alias))
// assertFalse("Expected false when erasing nothing.", Initializer.erase(context))
// }
//
// @Test(expected = InitializerException.MissingDefaultBirthdayException::class)
// fun testMissingBirthday() {
// val config = Initializer.Config { config ->
// config.setViewingKeys("vk1")
// }
// config.validate()
// }
//
// @Test(expected = InitializerException.InvalidBirthdayHeightException::class)
// fun testOutOfBoundsBirthday() {
// val config = Initializer.Config { config ->
// config.setViewingKeys("vk1")
// config.setBirthdayHeight(ZcashSdk.SAPLING_ACTIVATION_HEIGHT - 1)
// }
// config.validate()
// }
//
// @Test
// fun testImportedWalletUsesSaplingActivation() {
// initializer = Initializer(context) { config ->
// config.setViewingKeys("vk1")
// config.importWallet(ByteArray(32))
// }
// assertEquals("Incorrect height used for import.", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)
// }
//
// @Test
// fun testDefaultToOldestHeight_true() {
// initializer = Initializer(context) { config ->
// config.setViewingKeys("vk1")
// config.setBirthdayHeight(null, true)
// }
// assertEquals("Height should equal sapling activation height when defaultToOldestHeight is true", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)
// }
//
// @Test
// fun testDefaultToOldestHeight_false() {
// val initialHeight = 750_000
// initializer = Initializer(context) { config ->
// config.setViewingKeys("vk1")
// config.setBirthdayHeight(initialHeight, false)
// }
// val h = initializer.birthday.height
// assertNotEquals("Height should not equal sapling activation height when defaultToOldestHeight is false", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, h)
// assertTrue("expected $h to be higher", h >= initialHeight)
// }
//
// companion object {
// private val context = InstrumentationRegistry.getInstrumentation().context
// init {
// Twig.plant(TroubleshootingTwig())
// }
// }
}

View File

@ -0,0 +1,29 @@
package cash.z.ecc.android.sdk.annotation
enum class TestPurpose {
/**
* These tests are explicitly designed to preserve behavior that we do not want to lose after
* major upgrades or refactors. It is acceptable for these test to run long and require
* additional infrastructure.
*/
REGRESSION,
/**
* These tests are designed to be run against new pull requests and generally before any changes
* are committed. It is not ideal for these tests to run long.
*/
COMMIT,
/**
* These tests require a running instance of [darksidewalletd](https://github.com/zcash/lightwalletd/blob/master/docs/darksidewalletd.md).
*/
DARKSIDE,
}
/**
* Signals that this test is explicitly intended to be maintained and run regularly in order to
* achieve the given purpose. Eventually, we will run all such tests nightly.
*/
@Target(AnnotationTarget.CLASS)
annotation class MaintainedTest(vararg val purpose: TestPurpose)

View File

@ -3,6 +3,8 @@ package cash.z.ecc.android.sdk.ext
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
import cash.z.ecc.android.sdk.util.SimpleMnemonics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@ -12,14 +14,18 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import org.junit.After
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import ru.gildor.coroutines.okhttp.await
import java.util.concurrent.TimeoutException
fun Initializer.Config.seedPhrase(seedPhrase: String) {
setSeed(SimpleMnemonics().toSeed(seedPhrase.toCharArray()))
fun Initializer.Config.seedPhrase(seedPhrase: String, network: ZcashNetwork) {
setSeed(SimpleMnemonics().toSeed(seedPhrase.toCharArray()), network)
}
open class ScopedTest(val defaultTimeout: Long = 2000L) {
@ -93,6 +99,39 @@ open class ScopedTest(val defaultTimeout: Long = 2000L) {
}
}
open class DarksideTest(name: String = javaClass.simpleName) : ScopedTest() {
val sithLord = DarksideTestCoordinator(host = host, port = port)
val validator = sithLord.validator
fun runOnce(block: () -> Unit) {
if (!ranOnce) {
sithLord.enterTheDarkside()
sithLord.synchronizer.start(classScope)
block()
ranOnce = true
}
}
companion object {
// set the host for all tests. Someday, this will need to be set by CI
// so have it read from the environment first and give that precidence
var host = "192.168.1.134"
val port: Int = 9067
private var ranOnce = false
}
}
object BlockExplorer {
suspend fun fetchLatestHeight(): Int {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://api.blockchair.com/zcash/blocks?limit=1")
.build()
val result = client.newCall(request).await()
val body = result.body()?.string()
return JSONObject(body).getJSONArray("data").getJSONObject(0).getInt("id")
}
}
object Transactions {
val outbound = arrayOf(
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt",

View File

@ -0,0 +1,133 @@
package cash.z.ecc.android.sdk.integration
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.ext.BlockExplorer
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
* This test is intended to run to make sure that basic things are functional and pinpoint what is
* not working. It was originally developed after a major refactor to find what broke.
*/
@MaintainedTest(TestPurpose.COMMIT)
@RunWith(Parameterized::class)
class SanityTest(
private val wallet: TestWallet,
private val extfvk: String,
private val extpub: String,
private val birthday: Int,
) {
val networkName = wallet.networkName
val name = "$networkName wallet"
@Test
fun testNotPlaintext() {
val message =
"is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false"
assertFalse("$name $message", wallet.service.connectionInfo.usePlaintext)
}
@Test
fun testFilePaths() {
assertEquals(
"$name has invalid DataDB file",
"/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_${networkName}_Data.db",
wallet.initializer.rustBackend.pathDataDb
)
assertEquals(
"$name has invalid CacheDB file",
"/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_${networkName}_Cache.db",
wallet.initializer.rustBackend.pathCacheDb
)
assertEquals(
"$name has invalid CacheDB params dir",
"/data/user/0/cash.z.ecc.android.sdk.test/cache/params",
wallet.initializer.rustBackend.pathParamsDir
)
}
@Test
fun testBirthday() {
assertEquals(
"$name has invalid birthday height",
birthday,
wallet.initializer.birthday.height
)
}
@Test
fun testViewingKeys() {
assertEquals(
"$name has invalid extfvk",
extfvk,
wallet.initializer.viewingKeys[0].extfvk
)
assertEquals(
"$name has invalid extpub",
extpub,
wallet.initializer.viewingKeys[0].extpub
)
}
@Test
fun testServerConnection() {
assertEquals(
"$name has an invalid server connection",
"$networkName.lightwalletd.com:9067?usePlaintext=false",
wallet.connectionInfo
)
}
@Test
fun testLatestHeight() = runBlocking {
if (wallet.networkName == "mainnet") {
val expectedHeight = BlockExplorer.fetchLatestHeight()
// fetch height directly because the synchronizer hasn't started, yet
val downloaderHeight = wallet.service.getLatestBlockHeight()
val info = wallet.connectionInfo
assertTrue(
"$info\n ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight",
expectedHeight - 10 < downloaderHeight
)
}
}
@Test
fun testSingleBlockDownload() = runBlocking {
// fetch block directly because the synchronizer hasn't started, yet
val height = 1_000_000
val block = wallet.service.getBlockRange(height..height)[0]
assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height.toInt() == height)
}
companion object {
@JvmStatic
@Parameterized.Parameters
fun wallets() = listOf(
// Testnet wallet
arrayOf(
TestWallet(TestWallet.Backups.SAMPLE_WALLET),
"zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063",
"0234965f30c8611253d035f44e68d4e2ce82150e8665c95f41ccbaf916b16c69d8",
1320000
),
// Mainnet wallet
arrayOf(
TestWallet(TestWallet.Backups.SAMPLE_WALLET, ZcashNetwork.Mainnet),
"zxviews1q0hxkupsqqqqpqzsffgrk2smjuccedua7zswf5e3rgtv3ga9nhvhjug670egshd6me53r5n083s2m9mf4va4z7t39ltd3wr7hawnjcw09eu85q0ammsg0tsgx24p4ma0uvr4p8ltx5laum2slh2whc23ctwlnxme9w4dw92kalwk5u4wyem8dynknvvqvs68ktvm8qh7nx9zg22xfc77acv8hk3qqll9k3x4v2fa26puu2939ea7hy4hh60ywma69xtqhcy4037ne8g2sg8sq",
"031c6355641237643317e2d338f5e8734c57e8aa8ce960ee22283cf2d76bef73be",
1195000
)
)
}
}

View File

@ -0,0 +1,56 @@
package cash.z.ecc.android.sdk.integration
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Test
/**
* This test is intended to run to make sure that basic things are functional and pinpoint what is
* not working. It was originally developed after a major refactor to find what broke.
*/
@MaintainedTest(TestPurpose.COMMIT)
@MediumTest
class SmokeTest {
@Test
fun testNotPlaintext() {
val service =
wallet.synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService
Assert.assertFalse(
"Wallet is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false",
service.connectionInfo.usePlaintext
)
}
@Test
fun testFilePaths() {
Assert.assertEquals("Invalid DataDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Data.db", wallet.initializer.rustBackend.pathDataDb)
Assert.assertEquals("Invalid CacheDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Cache.db", wallet.initializer.rustBackend.pathCacheDb)
Assert.assertEquals("Invalid CacheDB params dir", "/data/user/0/cash.z.ecc.android.sdk.test/cache/params", wallet.initializer.rustBackend.pathParamsDir)
}
@Test
fun testBirthday() {
Assert.assertEquals("Invalid birthday height", 1_320_000, wallet.initializer.birthday.height)
}
@Test
fun testViewingKeys() {
Assert.assertEquals("Invalid extfvk", "zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063", wallet.initializer.viewingKeys[0].extfvk)
Assert.assertEquals("Invalid extpub", "0234965f30c8611253d035f44e68d4e2ce82150e8665c95f41ccbaf916b16c69d8", wallet.initializer.viewingKeys[0].extpub)
}
@Test
fun testSync() = runBlocking<Unit> {
wallet.sync(120_000L)
}
companion object {
val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET)
}
}

View File

@ -114,7 +114,7 @@ class TestnetIntegrationTest : ScopedTest() {
private val context = InstrumentationRegistry.getInstrumentation().context
private val initializer = Initializer(context) { config ->
config.setNetwork(ZcashNetwork.Testnet, host)
config.importWallet(seed, birthdayHeight)
config.importWallet(seed, birthdayHeight, ZcashNetwork.Testnet)
}
private lateinit var synchronizer: Synchronizer

View File

@ -1,4 +1,4 @@
// package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.integration.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.ext.ScopedTest
// import cash.z.ecc.android.sdk.ext.twigTask

View File

@ -1,4 +1,4 @@
package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.integration.darkside
// import cash.z.ecc.android.sdk.SdkSynchronizer
// import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess

View File

@ -1,4 +1,4 @@
// package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.integration.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.ext.ScopedTest
// import cash.z.ecc.android.sdk.ext.twig

View File

@ -1,4 +1,4 @@
// package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.integration.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.ext.ScopedTest
// import cash.z.ecc.android.sdk.util.DarksideTestCoordinator

View File

@ -0,0 +1,24 @@
package cash.z.ecc.android.sdk.integration.darkside
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose.DARKSIDE
import cash.z.ecc.android.sdk.annotation.TestPurpose.REGRESSION
import cash.z.ecc.android.sdk.ext.DarksideTest
import org.junit.Before
import org.junit.Test
/**
* Integration test to run in order to catch any regressions in transparent behavior.
*/
@MaintainedTest(DARKSIDE, REGRESSION)
class TransparentIntegrationTest : DarksideTest() {
@Before
fun setup() = runOnce {
sithLord.await()
}
@Test
fun sanityTest() {
validator.validateTxCount(5)
}
}

View File

@ -1,4 +1,4 @@
package cash.z.ecc.android.sdk.integration.reorgs
package cash.z.ecc.android.sdk.integration.darkside.reorgs
import cash.z.ecc.android.sdk.ext.ScopedTest
import cash.z.ecc.android.sdk.ext.twig

View File

@ -1,4 +1,4 @@
// package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.integration.darkside.reorgs // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.ext.ScopedTest
// import cash.z.ecc.android.sdk.util.DarksideTestCoordinator

View File

@ -1,4 +1,4 @@
// package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.integration.darkside.reorgs // package cash.z.ecc.android.sdk.integration
//
// import androidx.test.platform.app.InstrumentationRegistry
// import cash.z.ecc.android.sdk.Initializer

View File

@ -1,4 +1,4 @@
package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.integration.darkside.reorgs
import cash.z.ecc.android.sdk.ext.ScopedTest
import cash.z.ecc.android.sdk.util.DarksideTestCoordinator

View File

@ -1,4 +1,4 @@
package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.integration.darkside.reorgs
import cash.z.ecc.android.sdk.ext.ScopedTest
import cash.z.ecc.android.sdk.ext.twig

View File

@ -1,4 +1,4 @@
package cash.z.ecc.android.sdk.integration.reorgs
package cash.z.ecc.android.sdk.integration.darkside.reorgs
import cash.z.ecc.android.sdk.ext.ScopedTest
import cash.z.ecc.android.sdk.ext.toHex

View File

@ -1,15 +1,18 @@
package cash.z.ecc.android.sdk.integration.service
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.block.CompactBlockStore
import cash.z.ecc.android.sdk.exception.LightWalletException.ChangeServerException.ChainInfoNotMatching
import cash.z.ecc.android.sdk.exception.LightWalletException.ChangeServerException.StatusException
import cash.z.ecc.android.sdk.ext.ScopedTest
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -24,7 +27,9 @@ import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.Spy
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(AndroidJUnit4::class)
@SmallTest
class ChangeServiceTest : ScopedTest() {
val network = ZcashNetwork.Mainnet
@ -43,7 +48,7 @@ class ChangeServiceTest : ScopedTest() {
fun setup() {
initMocks()
downloader = CompactBlockDownloader(service, mockBlockStore)
otherService = LightWalletGrpcService(context, "lightwalletd.electriccoin.co", 9067)
otherService = LightWalletGrpcService(context, "lightwalletd.electriccoin.co")
}
@After
@ -58,7 +63,7 @@ class ChangeServiceTest : ScopedTest() {
@Test
fun testSanityCheck() {
val result = service.getLatestBlockHeight()
assertTrue(result > ZcashSdk.SAPLING_ACTIVATION_HEIGHT)
assertTrue(result > network.saplingActivationHeight)
}
@Test
@ -68,16 +73,20 @@ class ChangeServiceTest : ScopedTest() {
assertEquals(1_001, result)
}
/**
* Repeatedly connect to servers and download a range of blocks. Switch part way through and
* verify that the servers change over, even while actively downloading.
*/
@Test
fun testSwitchWhileActive() = runBlocking {
val start = 900_000
val count = 5
val vendors = mutableListOf<String>()
var oldVendor = downloader.getServerInfo().vendor
val differentiators = mutableListOf<String>()
var initialValue = downloader.getServerInfo().buildUser
val job = testScope.launch {
repeat(count) {
vendors.add(downloader.getServerInfo().vendor)
twig("downloading from ${vendors.last()}")
differentiators.add(downloader.getServerInfo().buildUser)
twig("downloading from ${differentiators.last()}")
downloader.downloadBlockRange(start..(start + 100 * it))
delay(10L)
}
@ -87,8 +96,8 @@ class ChangeServiceTest : ScopedTest() {
downloader.changeService(otherService)
}
job.join()
assertTrue(vendors.count { it == oldVendor } < vendors.size)
assertEquals(count, vendors.size)
assertTrue(differentiators.count { it == initialValue } < differentiators.size)
assertEquals(count, differentiators.size)
}
@Test
@ -116,7 +125,7 @@ class ChangeServiceTest : ScopedTest() {
caughtException is ChainInfoNotMatching
)
(caughtException as ChainInfoNotMatching).propertyNames.let { props ->
arrayOf("consensusBranchId", "saplingActivationHeight", "chainName").forEach {
arrayOf("saplingActivationHeight", "chainName").forEach {
assertTrue(
"$it should be a non-matching property but properties were [$props]", props.contains(it, true)
)

View File

@ -1,19 +1,24 @@
package cash.z.ecc.android.sdk.jni
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.test.BuildConfig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(AndroidJUnit4::class)
@SmallTest
class TransparentTest {
lateinit var expected: Expected
@ -21,13 +26,14 @@ class TransparentTest {
@Before
fun setup() {
if (BuildConfig.FLAVOR == "zcashtestnet") {
expected = ExpectedTestnet
network = ZcashNetwork.Testnet
} else {
expected = ExpectedMainnet
network = ZcashNetwork.Mainnet
}
// TODO: parameterize this for both networks
// if (BuildConfig.FLAVOR == "zcashtestnet") {
expected = ExpectedTestnet
network = ZcashNetwork.Testnet
// } else {
// expected = ExpectedMainnet
// network = ZcashNetwork.Mainnet
// }
}
@Test
@ -54,16 +60,6 @@ class TransparentTest {
assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPublicKey(uvk.extpub, network = network))
}
// @Test
// fun deriveTransparentAddressFromSecretKeyTest2() {
// while(false) {
// MnemonicCode(COUNT_24).let { phrase ->
// val addr = DerivationTool.deriveShieldedAddress(phrase.toSeed())
// twig("$addr${String(phrase.chars)}\t")
// }
// }
// }
companion object {
const val PHRASE = "deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"
val MNEMONIC = MnemonicCode(PHRASE)

View File

@ -10,6 +10,7 @@ import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork.Testnet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.catch
@ -28,9 +29,6 @@ class ShieldFundsSample {
val SEED_PHRASE = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame" // \"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread\"//\"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"//"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"
// simple flag to turn off actually spending funds
val IS_DRY_RUN = true
/**
* This test will construct a t2z transaction. It is safe to run this repeatedly, because
* nothing is submitted to the network (because the keys don't match the address so the encoding
@ -51,6 +49,9 @@ class ShieldFundsSample {
// when startHeight is null, it will use the latest checkpoint
class SimpleWallet(seedPhrase: String, startHeight: Int? = null) {
// simple flag to turn off actually spending funds
val IS_DRY_RUN = true
val walletScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(3, this.javaClass.simpleName)
)
@ -62,7 +63,7 @@ class ShieldFundsSample {
// t1b9Y6PESSGavavgge3ruTtX9X83817V29s
private val transparentAddress = DerivationTool.deriveTransparentAddress(seed, Testnet)
private val host = "lightwalletd.testnet.electriccoin.co"
private val config = Initializer.Config {
it.setSeed(seed, Testnet)
it.setBirthdayHeight(startHeight, false)

View File

@ -1,6 +1,9 @@
package cash.z.ecc.android.sdk.transaction
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCancelled
@ -29,7 +32,9 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(AndroidJUnit4::class)
@SmallTest
class PersistentTransactionManagerTest : ScopedTest() {
@Mock lateinit var mockEncoder: TransactionEncoder

View File

@ -79,7 +79,7 @@ class BalancePrinterUtil {
}.collect { seed ->
// TODO: clear the dataDb but leave the cacheDb
val initializer = Initializer(context) { config ->
config.importWallet(seed, birthdayHeight)
config.importWallet(seed, birthdayHeight, network)
config.setNetwork(network)
config.alias = alias
}

View File

@ -4,6 +4,8 @@ import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ScopedTest
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.seedPhrase
import cash.z.ecc.android.sdk.ext.twig
@ -14,8 +16,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Assert
@ -35,8 +39,7 @@ class DarksideTestCoordinator(val host: String = "127.0.0.1", val testName: Stri
// dependencies: public
val validator = DarksideTestValidator()
val chainMaker = DarksideChainMaker()
// var initializer = Initializer(context, Initializer.Builder(host, port, testName))
lateinit var synchronizer: SdkSynchronizer
lateinit var synchronizer: Synchronizer
val spendingKey: String get() = DerivationTool.deriveSpendingKeys(SimpleMnemonics().toSeed(seedPhrase.toCharArray()), network)[0]
@ -80,9 +83,10 @@ class DarksideTestCoordinator(val host: String = "127.0.0.1", val testName: Stri
config.setBirthdayHeight(birthdayHeight)
config.alias = testName
}
synchronizer = Synchronizer(initializer) as SdkSynchronizer
synchronizer = Synchronizer(initializer)
val channel = (synchronizer as SdkSynchronizer).channel
darkside = DarksideApi(channel)
darkside.reset()
}
// fun triggerSmallReorg() {
@ -226,7 +230,7 @@ class DarksideTestCoordinator(val host: String = "127.0.0.1", val testName: Stri
}
}
suspend fun validateBalance(available: Long = -1, total: Long = -1, accountIndex: Int = 0) {
val balance = synchronizer.processor.getBalanceInfo(accountIndex)
val balance = (synchronizer as SdkSynchronizer).processor.getBalanceInfo(accountIndex)
if (available > 0) {
assertEquals("invalid available balance", available, balance.availableZatoshi)
}

View File

@ -0,0 +1,159 @@
package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.isPending
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import java.util.concurrent.TimeoutException
/**
* A simple wallet that connects to testnet for integration testing. The intention is that it is
* easy to drive and nice to use.
*/
class TestWallet(
seedPhrase: String,
alias: String = "TestWallet",
network: ZcashNetwork = ZcashNetwork.Testnet,
host: String? = null,
startHeight: Int? = null
) {
constructor(
backup: Backups,
network: ZcashNetwork = ZcashNetwork.Testnet,
alias: String = "TestWallet"
) : this(
backup.seedPhrase,
network = network,
startHeight = backup.testnetBirthday,
alias = alias
)
val hostToUse = host ?: network.defaultHost
val walletScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(3, this.javaClass.simpleName)
)
private val context = InstrumentationRegistry.getInstrumentation().context
private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed()
private val shieldedSpendingKey = DerivationTool.deriveSpendingKeys(seed, network = network)[0]
private val transparentSecretKey = DerivationTool.deriveTransparentSecretKey(seed, network = network)
val initializer = Initializer(context) { config ->
config.importWallet(seed, startHeight, network, hostToUse, alias = alias)
}
val synchronizer: SdkSynchronizer = Synchronizer(initializer) as SdkSynchronizer
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.latestBalance.availableZatoshi
val shieldedAddress = DerivationTool.deriveShieldedAddress(seed, network = network)
val transparentAddress = DerivationTool.deriveTransparentAddress(seed, network = network)
val birthdayHeight get() = synchronizer.latestBirthdayHeight
val networkName get() = synchronizer.network.networkName
val connectionInfo get() = service.connectionInfo.toString()
suspend fun transparentBalance(): WalletBalance {
synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight)
return synchronizer.getTransparentBalance(transparentAddress)
}
suspend fun sync(timeout: Long = -1): TestWallet {
val killSwitch = walletScope.launch {
if (timeout > 0) {
delay(timeout)
throw TimeoutException("Failed to sync wallet within ${timeout}ms")
}
}
if (!synchronizer.isStarted) {
twig("Starting sync")
synchronizer.start(walletScope)
} else {
twig("Awaiting next SYNCED status")
}
// block until synced
synchronizer.status.first { it == Synchronizer.Status.SYNCED }
killSwitch.cancel()
twig("Synced!")
return this
}
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Long = 500L): TestWallet {
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo)
.takeWhile { it.isPending() }
.collect {
twig("Updated transaction: $it")
}
return this
}
suspend fun rewindToHeight(height: Int): TestWallet {
synchronizer.rewindToHeight(height, false)
return this
}
suspend fun shieldFunds(): TestWallet {
twig("checking $transparentAddress for transactions!")
synchronizer.refreshUtxos(transparentAddress, 935000).let { count ->
twig("FOUND $count new UTXOs")
}
synchronizer.getTransparentBalance(transparentAddress).let { walletBalance ->
twig("FOUND utxo balance of total: ${walletBalance.totalZatoshi} available: ${walletBalance.availableZatoshi}")
if (walletBalance.availableZatoshi > 0L) {
synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
.onCompletion { twig("done shielding funds") }
.catch { twig("Failed with $it") }
.collect()
}
}
return this
}
suspend fun join(timeout: Long? = null): TestWallet {
// block until stopped
twig("Staying alive until synchronizer is stopped!")
if (timeout != null) {
twig("Scheduling a stop in ${timeout}ms")
walletScope.launch {
delay(timeout)
synchronizer.stop()
}
}
synchronizer.status.first { it == Synchronizer.Status.STOPPED }
twig("Stopped!")
return this
}
companion object {
init {
Twig.enabled(true)
}
}
enum class Backups(val seedPhrase: String, val testnetBirthday: Int) {
DEFAULT("column rhythm acoustic gym cost fit keen maze fence seed mail medal shrimp tell relief clip cannon foster soldier shallow refuse lunar parrot banana", 1_355_928),
SAMPLE_WALLET("input frown warm senior anxiety abuse yard prefer churn reject people glimpse govern glory crumble swallow verb laptop switch trophy inform friend permit purpose", 1_330_190),
ALICE("quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten", 1_330_190),
BOB("canvas wine sugar acquire garment spy tongue odor hole cage year habit bullet make label human unit option top calm neutral try vocal arena", 1_330_190),
;
}
}

View File

@ -1,159 +0,0 @@
package cash.z.ecc.android.sdk.block
import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.ZcashSdk.SAPLING_ACTIVATION_HEIGHT
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.transaction.TransactionRepository
import com.nhaarman.mockitokotlin2.atLeastOnce
import com.nhaarman.mockitokotlin2.stub
import com.nhaarman.mockitokotlin2.verify
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.quality.Strictness
@ExtendWith(MockitoExtension::class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CompactBlockProcessorTest {
// Mocks/Spys
@Mock lateinit var rustBackend: RustBackend
lateinit var processor: CompactBlockProcessor
// Test variables
private var latestBlockHeight: Int = 500_000
private var lastDownloadedHeight: Int = SAPLING_ACTIVATION_HEIGHT
private var lastScannedHeight: Int = SAPLING_ACTIVATION_HEIGHT
private var errorBlock: Int = -1
@BeforeEach
fun setUp(
@Mock lightwalletService: LightWalletService,
@Mock compactBlockStore: CompactBlockStore,
@Mock repository: TransactionRepository
) {
Twig.plant(TroubleshootingTwig())
lightwalletService.stub {
onBlocking {
getBlockRange(any())
}.thenAnswer { invocation ->
val range = invocation.arguments[0] as IntRange
range.map { CompactBlockEntity(it, ByteArray(0)) }
}
}
lightwalletService.stub {
onBlocking {
getLatestBlockHeight()
}.thenAnswer { latestBlockHeight }
}
compactBlockStore.stub {
onBlocking {
write(any())
}.thenAnswer { invocation ->
val lastBlockHeight = (invocation.arguments[0] as List<CompactBlockEntity>).last().height
lastDownloadedHeight = lastBlockHeight
Unit
}
}
compactBlockStore.stub {
onBlocking {
getLatestHeight()
}.thenAnswer { lastDownloadedHeight }
}
compactBlockStore.stub {
onBlocking {
rewindTo(any())
}.thenAnswer { invocation ->
lastDownloadedHeight = invocation.arguments[0] as Int
Unit
}
}
repository.stub {
onBlocking {
lastScannedHeight()
}.thenAnswer { lastScannedHeight }
}
val downloader = spy(CompactBlockDownloader(lightwalletService, compactBlockStore))
processor = spy(CompactBlockProcessor(downloader, repository, rustBackend))
whenever(rustBackend.validateCombinedChain()).thenAnswer {
errorBlock
}
whenever(rustBackend.scanBlocks()).thenAnswer {
true
}
}
@Test
@Timeout(5)
fun `check for OBOE when downloading`() = runBlocking {
// if the last block downloaded was 350_000, then we already have that block and should start with 350_001
lastDownloadedHeight = 550_000
processBlocksThen {
verify(processor).downloadNewBlocks(350_001..latestBlockHeight)
}
}
@Test
@Timeout(5)
fun `chain error rewinds by expected amount`() = runBlocking {
// if the highest block whose prevHash doesn't match happens at block 300_010
errorBlock = 500_010
// then we should rewind the default (10) blocks
val expectedBlock = errorBlock - 10
processBlocksThen {
twig("FINISHED PROCESSING!")
verify(processor.downloader, atLeastOnce()).rewindToHeight(expectedBlock)
verify(rustBackend, atLeastOnce()).rewindToHeight(expectedBlock)
assertNotNull(processor)
}
}
@Test
// @Timeout(5)
fun `chain error downloads expected number of blocks`() = runBlocking {
// if the highest block whose prevHash doesn't match happens at block 300_010
// and our rewind distance is the default (10), then we want to download exactly ten blocks
errorBlock = 500_010
// plus 1 because the range is inclusive
val expectedRange = (errorBlock - 10 + 1)..latestBlockHeight
processBlocksThen {
verify(processor, atLeastOnce()).downloadNewBlocks(expectedRange)
}
}
// TODO: fix the fact that flows cause this not to work as originally coded. With a channel, we can stop observing once we reach 100. A flow makes that more difficult. The SDK behavior is still the same but testing that behavior is a little tricky without some refactors.
private suspend fun processBlocksThen(block: suspend () -> Unit) = runBlocking {
val scope = this
launch {
processor.start()
}
processor.progress.collect { i ->
if (i >= 100) {
block()
processor.stop()
}
twig("processed $i")
}
twig("Done processing!")
}
}

View File

@ -1,225 +0,0 @@
package cash.z.ecc.android.sdk.transaction
// import cash.z.ecc.android.sdk.dao.ClearedTransaction
// import kotlinx.coroutines.*
// import org.junit.jupiter.api.AfterEach
// import org.junit.jupiter.api.BeforeEach
// import org.junit.jupiter.api.Test
//
// import org.junit.jupiter.api.Assertions.*
// import kotlin.random.Random
// import kotlin.random.nextLong
// import kotlin.system.measureTimeMillis
internal class MockSynchronizerTest {
// private val transactionInterval = 200L
// private val activeTransactionInterval = 200L
// private val synchronizer = MockSynchronizer(transactionInterval, activeTransactionInterval)
// private val fastSynchronizer = MockSynchronizer(2L, 2L)
// private val allTransactionChannel = synchronizer.allTransactions()
// private val validAddress = "ztestsapling1yu2zy9aane2pje2qvm4qmn4k6q57y2d9ecs5vz0guthxx3m2aq57qm6hkx0520m9u9635xh6ttd"
//
// @BeforeEach
// fun setUp() {
// synchronizer.start(CoroutineScope(Dispatchers.IO))
// }
//
// @AfterEach
// fun tearDown() {
// synchronizer.stop()
// }
//
// @Test
// fun allTransactions() = runBlocking {
// var total = 0
// val duration = measureTimeMillis {
// repeat(10) {
// val transactions = allTransactionChannel.receive()
// total += transactions.size
// println("received ${transactions.size} transactions")
// }
// }
// assertTrue(total > 0)
// assertTrue(duration > transactionInterval)
// }
//
// @Test
// fun `never calling send yields zero sent transactions`() = runBlocking {
// val fastChannel = fastSynchronizer.start(fastSynchronizer).allTransactions()
// var transactions = fastChannel.receive()
// repeat(10_000) {
// transactions = fastChannel.receive()
// }
// assertTrue(transactions.size > 0, "no transactions created at all")
// assertTrue(transactions.none { it.isSend })
// }
//
// @Test
// fun `send - each call to send generates exactly one sent transaction`() = runBlocking {
// val fastChannel = fastSynchronizer.start(fastSynchronizer).allTransactions()
// var transactions = fastChannel.receive()
// repeat(10_000) {
// if (it.rem(2_000) == 0) {
// fastSynchronizer.sendToAddress(10, validAddress); println("yep")
// }
// transactions = fastChannel.receive()
// }
// assertEquals(5, transactions.count { it.isSend })
// }
//
// @Test
// fun `send - triggers an active transaction`() = runBlocking {
// synchronizer.sendToAddress(10, validAddress)
// delay(500L)
// assertNotNull(synchronizer.activeTransactions().receiveOrNull())
// synchronizer.stop()
// }
//
// @Test
// fun `send - results in success`() = runBlocking {
// synchronizer.sendToAddress(10, validAddress)
// delay(500L)
// val result = synchronizer.activeTransactions().receive()
// assertTrue(result.isNotEmpty(), "result was empty")
// assertTrue(TransactionState.AwaitingConfirmations(0).order <= result.values.first().order)
// assertTrue((result.keys.first() as ActiveSendTransaction).transactionId.get() != -1L, "transactionId missing")
// assertTrue((result.keys.first() as ActiveSendTransaction).height.get() != -1, "height missing")
// synchronizer.stop()
// }
//
// @Test
// fun `send - results in mined transaction`() = runBlocking {
// synchronizer.sendToAddress(10, validAddress)
// delay(500L)
// val result = synchronizer.activeTransactions().receive()
// assertTrue(result.isNotEmpty(), "result was empty")
// assertTrue(TransactionState.AwaitingConfirmations(0).order <= result.values.first().order)
// synchronizer.stop()
// }
//
// @Test
// fun `send - a bad address fails`() = runBlocking {
// synchronizer.sendToAddress(10, "fail")
// delay(500L)
// val result = synchronizer.activeTransactions().receive()
// assertTrue(result.isNotEmpty(), "result was empty")
// assertTrue(0 > result.values.first().order)
// synchronizer.stop()
// }
//
// @Test
// fun `send - a short address fails`() = runBlocking {
// // one character too short
// val toAddress = "ztestsapling1yu2zy9aane2pje2qvm4qmn4k6q57y2d9ecs5vz0guthxx3m2aq57qm6hkx0520m9u9635xh6tt"
// assertTrue(toAddress.length < 88, "sample address wasn't short enough (${toAddress.length})")
//
// synchronizer.sendToAddress(10, toAddress)
// delay(500L)
// val result = synchronizer.activeTransactions().receive()
// assertTrue(result.isNotEmpty(), "result was empty")
// assertTrue(0 > result.values.first().order,
// "result should have been a failure but was ${result.values.first()::class.simpleName}")
// }
//
// @Test
// fun `send - a non-z prefix address fails`() = runBlocking {
// // one character too short
// val toAddress = "atestsapling1yu2zy9aane2pje2qvm4qmn4k6q57y2d9ecs5vz0guthxx3m2aq57qm6hkx0520m9u9635xh6ttd"
// assertTrue(toAddress.length == 88,
// "sample address was not the proper length (${toAddress.length}")
// assertFalse(toAddress.startsWith('z'),
// "sample address should not start with z")
//
// synchronizer.sendToAddress(10, toAddress)
// delay(500L)
// val result = synchronizer.activeTransactions().receive()
// assertTrue(result.isNotEmpty(), "result was empty")
// assertTrue(0 > result.values.first().order,
// "result should have been a failure but was ${result.values.first()::class.simpleName}")
// }
//
// @Test
// fun `balance matches transactions without sends`() = runBlocking {
// val balances = fastSynchronizer.start(fastSynchronizer).balances()
// var transactions = listOf<ClearedTransaction>()
// while (transactions.count() < 10) {
// transactions = fastSynchronizer.allTransactions().receive()
// println("got ${transactions.count()} transaction(s)")
// }
// assertEquals(transactions.fold(0L) { acc, tx -> acc + tx.value }, balances.receive())
// }
//
// @Test
// fun `balance matches transactions with sends`() = runBlocking {
// var transactions = listOf<ClearedTransaction>()
// val balances = fastSynchronizer.start(fastSynchronizer).balances()
// val transactionChannel = fastSynchronizer.allTransactions()
// while (transactions.count() < 10) {
// fastSynchronizer.sendToAddress(Random.nextLong(1L..10_000_000_000), validAddress)
// transactions = transactionChannel.receive()
// println("got ${transactions.count()} transaction(s)")
// }
// val transactionsSnapshot = transactionChannel.receive()
// val balanceSnapshot = balances.receive()
//
// val positiveValue = transactionsSnapshot.fold(0L) { acc, tx -> acc + (if (tx.isSend) 0 else tx.value) }
// val negativeValue = transactionsSnapshot.fold(0L) { acc, tx -> acc + (if (!tx.isSend) 0 else tx.value) }
// assertEquals(positiveValue - negativeValue, balanceSnapshot, "incorrect balance. negative balance: $negativeValue positive balance: $positiveValue")
// }
//
// @Test
// fun `progress hits 100`() = runBlocking {
// var channel = synchronizer.progress()
// var now = System.currentTimeMillis()
// var delta = 0L
// val expectedUpperBounds = transactionInterval * 10
// while (channel.receive() < 100) {
// delta = now - System.currentTimeMillis()
// if (delta > expectedUpperBounds) break
// }
// assertTrue(delta < expectedUpperBounds, "progress did not hit 100 within the expected time of $expectedUpperBounds")
// }
//
// @Test
// fun `is out of sync about 10% of the time`() = runBlocking {
// var count = 0
// repeat(100_000) {
// if (synchronizer.isStale()) count++
// }
// assertTrue(count < 11_000, "a count of $count is too frequent")
// assertTrue(count > 9_000, "a count of $count is too infrequent")
// }
//
// @Test
// fun isFirstRun() {
// }
//
// @Test
// fun cancelSend() = runBlocking {
// val activeTransactions = synchronizer.activeTransactions()
//
// // verify that send creates one transaction
// launch {
// synchronizer.sendToAddress(10, validAddress)
// }
// println("done sending to address")
// delay(300L)
// var actives = activeTransactions.receiveOrNull()
// assertEquals(1, actives?.size)
// assertTrue((actives?.values?.first()?.order ?: 0) > -1, "expected positive order but was ${actives?.values?.first()?.order}")
// val transaction = actives?.keys?.first() as? ActiveSendTransaction
// assertNotNull(transaction)
//
// // and then verify that cancel changes its status
// synchronizer.cancelSend(transaction!!)
// delay(100L) // look for ignored state change
// actives = activeTransactions.receiveOrNull()
// assertNotNull(actives, "cancel changed nothing in 100ms")
// assertEquals(1, actives!!.size, "unexpected number of active transactions ${actives.size}")
// val finalState = actives!!.values.first()
// assertNotNull(finalState as? TransactionState.Cancelled, "transaction was ${finalState::class.simpleName} instead of cancelled for ${actives.keys.first()}")
// println("donso")
// synchronizer.stop()
// }
}