[#284] Refactor darkside tests to separate module

Simplifies running the SDK test suite versus darkside tests (which require special environment setup).

Note that the darkside tests are still broken.  This is not a regression, as they were broken before.  This is an intermediate step towards fixing those tests.
This commit is contained in:
Carter Jernigan 2021-09-26 08:14:11 -04:00
parent 0b3c0cf4e4
commit abac552ab5
32 changed files with 656 additions and 169 deletions

View File

@ -0,0 +1,53 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name=":darkside-test-lib:connectedAndroidTest" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="zcash-android-sdk.darkside-test-lib" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="2147483645" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="api-9130115880275692386-873230" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,33 @@
plugins {
id("com.android.library")
id("zcash.android-build-conventions")
id("kotlin-android")
id("kotlin-kapt")
}
android {
defaultConfig {
//targetSdk = 30 //Integer.parseInt(project.property("targetSdkVersion"))
multiDexEnabled = true
}
// Need to figure out how to move this into the build-conventions
kotlinOptions {
jvmTarget = libs.versions.java.get()
allWarningsAsErrors = project.property("IS_TREAT_WARNINGS_AS_ERRORS").toString().toBoolean()
}
}
dependencies {
implementation(projects.sdkLib)
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.multidex)
implementation(libs.bundles.grpc)
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.zcashwalletplgn)
androidTestImplementation(libs.bip39)
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cash.z.ecc.android.sdk.darkside">
<!-- For code coverage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:name="androidx.multidex.MultiDexApplication" />
</manifest>

View File

@ -1,8 +1,8 @@
package cash.z.ecc.android.sdk.integration.darkside // package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.ext.ScopedTest
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.ext.twigTask
// import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import kotlinx.coroutines.runBlocking
// import org.junit.BeforeClass
// import org.junit.Test
@ -73,7 +73,7 @@ package cash.z.ecc.android.sdk.integration.darkside // package cash.z.ecc.androi
//
//
// companion object {
// private val sithLord = DarksideTestCoordinator("192.168.1.134")
// private val sithLord = DarksideTestCoordinator()
// private val secondAddress = "zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx"
// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"
//

View File

@ -1,10 +1,10 @@
package cash.z.ecc.android.sdk.integration.darkside
package cash.z.ecc.android.sdk.darkside
// import cash.z.ecc.android.sdk.SdkSynchronizer
// import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
// import cash.z.ecc.android.sdk.ext.ScopedTest
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.ext.twig
// import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import kotlinx.coroutines.Job
// import kotlinx.coroutines.delay
// import kotlinx.coroutines.flow.launchIn
@ -60,7 +60,7 @@ package cash.z.ecc.android.sdk.integration.darkside
//
// companion object {
// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
// private val sithLord = DarksideTestCoordinator("192.168.1.134")
// private val sithLord = DarksideTestCoordinator()
// private val secondAddress = "zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx"
// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"
//

View File

@ -1,10 +1,10 @@
package cash.z.ecc.android.sdk.integration.darkside // package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.ext.ScopedTest
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.ext.twig
// import cash.z.ecc.android.sdk.ext.twigTask
// import cash.z.ecc.android.sdk.service.LightWalletGrpcService
// import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import cash.z.ecc.android.sdk.util.SimpleMnemonics
// import cash.z.wallet.sdk.rpc.CompactFormats
// import cash.z.wallet.sdk.rpc.Service
@ -153,7 +153,7 @@ package cash.z.ecc.android.sdk.integration.darkside // package cash.z.ecc.androi
// }
//
// companion object {
// private val sithLord = DarksideTestCoordinator("192.168.1.134", "MultiRecipientInRust")
// private val sithLord = DarksideTestCoordinator(, "MultiRecipientInRust")
//
// private val randomPhrases = listOf(
// "profit save black expose rude feature early rocket alter borrow finish october few duty flush kick spell bean burden enforce bitter theme silent uphold",

View File

@ -1,7 +1,7 @@
package cash.z.ecc.android.sdk.integration.darkside // package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.ext.ScopedTest
// import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import org.junit.Before
// import org.junit.BeforeClass
// import org.junit.Test

View File

@ -1,17 +1,16 @@
package cash.z.ecc.android.sdk.integration.darkside
package cash.z.ecc.android.sdk.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 androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTest
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
/**
* Integration test to run in order to catch any regressions in transparent behavior.
*/
@MaintainedTest(DARKSIDE, REGRESSION)
@RunWith(AndroidJUnit4::class)
class TransparentIntegrationTest : DarksideTest() {
@Before
fun setup() = runOnce {

View File

@ -1,11 +1,15 @@
package cash.z.ecc.android.sdk.integration.darkside.reorgs
package cash.z.ecc.android.sdk.darkside.reorgs
import cash.z.ecc.android.sdk.ext.ScopedTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class InboundTxTests : ScopedTest() {
@Test
@ -78,7 +82,7 @@ class InboundTxTests : ScopedTest() {
private const val firstBlock = 663150
private const val targetTxBlock = 663188
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator("192.168.1.134")
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
private val chainMaker = sithLord.chainMaker
@ -92,7 +96,8 @@ class InboundTxTests : ScopedTest() {
.stageEmptyBlocks(firstBlock + 1, 100)
.applyTipHeight(targetTxBlock - 1)
sithLord.startSync(classScope).await()
sithLord.synchronizer.start(classScope)
sithLord.await()
}
}
}

View File

@ -1,7 +1,7 @@
package cash.z.ecc.android.sdk.integration.darkside.reorgs // package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.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
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import org.junit.Assert.assertFalse
// import org.junit.Assert.assertTrue
// import org.junit.BeforeClass

View File

@ -1,13 +1,13 @@
package cash.z.ecc.android.sdk.integration.darkside.reorgs // package cash.z.ecc.android.sdk.integration
package cash.z.ecc.android.sdk.darkside.reorgs // package cash.z.ecc.android.sdk.integration
//
// 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.ext.ScopedTest
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.ext.import
// import cash.z.ecc.android.sdk.ext.twig
// import cash.z.ecc.android.sdk.util.DarksideApi
// import cash.z.ecc.android.sdk.darkside.test.DarksideApi
// import io.grpc.StatusRuntimeException
// import kotlinx.coroutines.delay
// import kotlinx.coroutines.flow.filter
@ -109,7 +109,6 @@ package cash.z.ecc.android.sdk.integration.darkside.reorgs // package cash.z.ecc
// get() = (synchronizer as SdkSynchronizer).processor.downloader.lightwalletService
//
// companion object {
// private const val host = "192.168.1.134"
// private const val port = 9067
// private const val birthdayHeight = 663150
// private const val targetHeight = 663200

View File

@ -1,11 +1,15 @@
package cash.z.ecc.android.sdk.integration.darkside.reorgs
package cash.z.ecc.android.sdk.darkside.reorgs
import cash.z.ecc.android.sdk.ext.ScopedTest
import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReorgSetupTest : ScopedTest() {
private val birthdayHeight = 663150
@ -30,7 +34,7 @@ class ReorgSetupTest : ScopedTest() {
companion object {
private val sithLord = DarksideTestCoordinator("192.168.1.134")
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
@BeforeClass

View File

@ -1,13 +1,16 @@
package cash.z.ecc.android.sdk.integration.darkside.reorgs
package cash.z.ecc.android.sdk.darkside.reorgs
import cash.z.ecc.android.sdk.ext.ScopedTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReorgSmallTest : ScopedTest() {
private val targetHeight = 663250
@ -41,7 +44,7 @@ class ReorgSmallTest : ScopedTest() {
companion object {
private val sithLord = DarksideTestCoordinator("192.168.1.134")
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
private var hadReorg = false

View File

@ -1,13 +1,16 @@
package cash.z.ecc.android.sdk.integration.darkside.reorgs
package cash.z.ecc.android.sdk.darkside.reorgs
import cash.z.ecc.android.sdk.ext.ScopedTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.ext.toHex
import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
import cash.z.ecc.android.sdk.util.SimpleMnemonics
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.darkside.test.SimpleMnemonics
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SetupTest : ScopedTest() {
// @Test

View File

@ -1,11 +1,14 @@
package cash.z.ecc.android.sdk.integration.darkside.reproduce
package cash.z.ecc.android.sdk.darkside.reproduce
import cash.z.ecc.android.sdk.ext.DarksideTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTest
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReproduceZ2TFailureTest : DarksideTest() {
@Before
fun setup() {

View File

@ -1,4 +1,4 @@
package cash.z.ecc.android.sdk.util
package cash.z.ecc.android.sdk.darkside.test
import android.content.Context
import cash.z.ecc.android.sdk.R

View File

@ -0,0 +1,18 @@
package cash.z.ecc.android.sdk.darkside.test
open class DarksideTest(name: String = javaClass.simpleName) : ScopedTest() {
val sithLord = DarksideTestCoordinator()
val validator = sithLord.validator
fun runOnce(block: () -> Unit) {
if (!ranOnce) {
sithLord.enterTheDarkside()
sithLord.synchronizer.start(classScope)
block()
ranOnce = true
}
}
companion object {
private var ranOnce = false
}
}

View File

@ -1,10 +1,8 @@
package cash.z.ecc.android.sdk.util
package cash.z.ecc.android.sdk.darkside.test
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.ext.ScopedTest
import cash.z.ecc.android.sdk.ext.seedPhrase
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork
import io.grpc.StatusRuntimeException
@ -24,10 +22,10 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
alias: String = "DarksideTestCoordinator",
seedPhrase: String = DEFAULT_SEED_PHRASE,
startHeight: Int = DEFAULT_START_HEIGHT,
host: String = "127.0.0.1",
host: String = COMPUTER_LOCALHOST,
network: ZcashNetwork = ZcashNetwork.Mainnet,
port: Int = network.defaultPort,
) : this(TestWallet(seedPhrase, alias, network, host, port, startHeight))
) : this(TestWallet(seedPhrase, alias, network, host, startHeight = startHeight, port = port))
private val targetHeight = 663250
private val context = InstrumentationRegistry.getInstrumentation().context
@ -89,10 +87,6 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
// darkside.setBlocksUrl(largeReorg)
// }
fun startSync(scope: CoroutineScope): DarksideTestCoordinator = apply {
synchronizer.start(scope)
}
// redo this as a call to wallet but add delay time to wallet join() function
/**
* Waits for, at most, the given amount of time for the synchronizer to download and scan blocks
@ -296,6 +290,12 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
}
companion object {
/**
* This is a special localhost value on the Android emulator, which allows it to contact
* the localhost of the computer running the emulator.
*/
const val COMPUTER_LOCALHOST = "10.0.2.2"
// Block URLS
private const val beforeReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"

View File

@ -0,0 +1,48 @@
package cash.z.ecc.android.sdk.darkside.test
import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import org.junit.Before
/**
* Subclass this to validate the environment for running Darkside tests.
*/
open class DarksideTestPrerequisites {
@Before
fun verifyEmulator() {
require(isProbablyEmulator(ApplicationProvider.getApplicationContext())) {
"Darkside tests are configured to only run on the Android Emulator. Please see https://github.com/zcash/zcash-android-wallet-sdk/blob/master/docs/tests/Darkside.md"
}
}
companion object {
private fun isProbablyEmulator(context: Context): Boolean {
if (isDebuggable(context)) {
// This is imperfect and could break in the future
if (null == Build.DEVICE
|| "generic" == Build.DEVICE //$NON-NLS
|| ("generic_x86" == Build.DEVICE) //$NON-NLS
) {
return true
}
}
return false
}
/**
* @return Whether the application running is debuggable. This is determined from the
* ApplicationInfo object (`BuildInfo` is useless for libraries.)
*/
private fun isDebuggable(context: Context): Boolean {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
// Normally shouldn't be null, but could be with a MockContext
return packageInfo.applicationInfo?.let {
0 != (it.flags and ApplicationInfo.FLAG_DEBUGGABLE)
} ?: false
}
}
}

View File

@ -0,0 +1,91 @@
package cash.z.ecc.android.sdk.darkside.test
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import java.util.concurrent.TimeoutException
open class ScopedTest(val defaultTimeout: Long = 2000L) : DarksideTestPrerequisites() {
protected lateinit var testScope: CoroutineScope
// if an androidTest doesn't need a context, then maybe it should be a unit test instead?!
val context: Context = InstrumentationRegistry.getInstrumentation().context
@Before
fun start() {
twig("===================== TEST STARTED ==================================")
testScope = CoroutineScope(
Job(classScope.coroutineContext[Job]!!) + newFixedThreadPoolContext(
5,
this.javaClass.simpleName
)
)
}
@After
fun end() = runBlocking<Unit> {
twig("======================= TEST CANCELLING =============================")
testScope.cancel()
testScope.coroutineContext[Job]?.join()
twig("======================= TEST ENDED ==================================")
}
fun timeout(duration: Long, block: suspend () -> Unit) = timeoutWith(testScope, duration, block)
companion object {
@JvmStatic
lateinit var classScope: CoroutineScope
init {
Twig.plant(TroubleshootingTwig())
twig("================================================================ INIT")
}
@BeforeClass
@JvmStatic
fun createScope() {
twig("======================= CLASS STARTED ===============================")
classScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(2, this.javaClass.simpleName)
)
}
@AfterClass
@JvmStatic
fun destroyScope() = runBlocking<Unit> {
twig("======================= CLASS CANCELLING ============================")
classScope.cancel()
classScope.coroutineContext[Job]?.join()
twig("======================= CLASS ENDED =================================")
}
@JvmStatic
fun timeoutWith(scope: CoroutineScope, duration: Long, block: suspend () -> Unit) {
scope.launch {
delay(duration)
val message = "ERROR: Test timed out after ${duration}ms"
twig(message)
throw TimeoutException(message)
}.let { selfDestruction ->
scope.launch {
block()
selfDestruction.cancel()
}
}
}
}
}

View File

@ -0,0 +1,20 @@
package cash.z.ecc.android.sdk.darkside.test
import cash.z.android.plugin.MnemonicPlugin
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.bip39.toSeed
import java.util.Locale
class SimpleMnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
}

View File

@ -0,0 +1,163 @@
package cash.z.ecc.android.sdk.darkside.test
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(
val seedPhrase: String,
val alias: String = "TestWallet",
val network: ZcashNetwork = ZcashNetwork.Testnet,
val host: String = network.defaultHost,
startHeight: Int? = null,
val port: Int = network.defaultPort,
) {
constructor(
backup: Backups,
network: ZcashNetwork = ZcashNetwork.Testnet,
alias: String = "TestWallet"
) : this(
backup.seedPhrase,
network = network,
startHeight = if (network == ZcashNetwork.Mainnet) backup.mainnetBirthday else backup.testnetBirthday,
alias = alias
)
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, host, alias = alias)
}
val synchronizer: SdkSynchronizer = Synchronizer(initializer) as SdkSynchronizer
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.saplingBalances.value.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, fromAccountIndex: Int = 0): TestWallet {
Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() }
.collect {
twig("Updated transaction: $it")
}
Twig.clip("$alias sending")
return this
}
suspend fun rewindToHeight(height: Int): TestWallet {
synchronizer.rewindToNearestHeight(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, val mainnetBirthday: Int) {
// TODO: get the proper birthday values for these wallets
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, 1_000_000),
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, 1_000_000),
DEV_WALLET("still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread", 1_000_000, 991645),
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, 1_000_000),
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, 1_000_000),
;
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cash.z.ecc.android.sdk.darkside">
<application android:name="androidx.multidex.MultiDexApplication" />
</manifest>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="lightwalletd_allow_very_insecure_connections">true</bool>
</resources>

31
docs/tests/Darkside.md Normal file
View File

@ -0,0 +1,31 @@
# Running Darksidewalletd tests
Some tests are executed against a fake version of the Zcash network, by running a localhost lightwalletd server in a special mode called "darkside". This is different from the Zcash test network, which is a publicly accessible and deployed network that acts more like a staging network before changes are pushed to the production network.
The module [darkside-test-lib](../../darkside-test-lib) contains a test suite that requires manually launching a localhost lightwalletd instance in darkside mode.
To run these tests
1. clone [lightwalletd](https://github.com/zcash/lightwalletd.git)
`git clone https://github.com/zcash/lightwalletd.git`
1. Install Go.
1. If you're using homebrew
```` zsh
brew install go
````
1. Inside the `lightwalletd` checkout, compile lightwalletd
```` zsh
make
````
1. Inside the `lightwalletd` checkout, run the program in _darkside_ mode
```` zsh
./lightwalletd --log-file /dev/stdout --darkside-very-insecure --darkside-timeout 1000 --gen-cert-very-insecure --data-dir . --no-tls-very-insecure
````
1. Launch an Android emulator. Darkside tests are configured to only run on an Android emulator, as this makes it easy to automate finding the localhost server running on the same computer that's also running the emulator.
1. Run the Android test suite
1. From the command line
```` zsh
./gradlew :darkside-test-lib:connectedAndroidTest
````
1. From Android Studio
1. Choose the run configuration `:darkside-test-lib:connectedAndroidTest`

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cash.z.ecc.android.sdk">
<!-- For code coverage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:name="androidx.multidex.MultiDexApplication" />
</manifest>

View File

@ -1,125 +1,17 @@
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
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
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, network: ZcashNetwork) {
setSeed(SimpleMnemonics().toSeed(seedPhrase.toCharArray()), network)
}
open class ScopedTest(val defaultTimeout: Long = 2000L) {
protected lateinit var testScope: CoroutineScope
// if an androidTest doesn't need a context, then maybe it should be a unit test instead?!
val context: Context = InstrumentationRegistry.getInstrumentation().context
@Before
fun start() {
twig("===================== TEST STARTED ==================================")
testScope = CoroutineScope(
Job(classScope.coroutineContext[Job]!!) + newFixedThreadPoolContext(
5,
this.javaClass.simpleName
)
)
}
@After
fun end() = runBlocking<Unit> {
twig("======================= TEST CANCELLING =============================")
testScope.cancel()
testScope.coroutineContext[Job]?.join()
twig("======================= TEST ENDED ==================================")
}
fun timeout(duration: Long, block: suspend () -> Unit) = timeoutWith(testScope, duration, block)
companion object {
@JvmStatic
lateinit var classScope: CoroutineScope
init {
Twig.plant(TroubleshootingTwig())
twig("================================================================ INIT")
}
@BeforeClass
@JvmStatic
fun createScope() {
twig("======================= CLASS STARTED ===============================")
classScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(2, this.javaClass.simpleName)
)
}
@AfterClass
@JvmStatic
fun destroyScope() = runBlocking<Unit> {
twig("======================= CLASS CANCELLING ============================")
classScope.cancel()
classScope.coroutineContext[Job]?.join()
twig("======================= CLASS ENDED =================================")
}
@JvmStatic
fun timeoutWith(scope: CoroutineScope, duration: Long, block: suspend () -> Unit) {
scope.launch {
delay(duration)
val message = "ERROR: Test timed out after ${duration}ms"
twig(message)
throw TimeoutException(message)
}.let { selfDestruction ->
scope.launch {
block()
selfDestruction.cancel()
}
}
}
}
}
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()

View File

@ -6,13 +6,13 @@ import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ScopedTest
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 cash.z.ecc.android.sdk.ext.onFirst
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.test.ScopedTest
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.ZcashNetwork

View File

@ -8,10 +8,10 @@ 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.twig
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.test.ScopedTest
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

View File

@ -0,0 +1,91 @@
package cash.z.ecc.android.sdk.test
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import java.util.concurrent.TimeoutException
open class ScopedTest(val defaultTimeout: Long = 2000L) {
protected lateinit var testScope: CoroutineScope
// if an androidTest doesn't need a context, then maybe it should be a unit test instead?!
val context: Context = InstrumentationRegistry.getInstrumentation().context
@Before
fun start() {
twig("===================== TEST STARTED ==================================")
testScope = CoroutineScope(
Job(classScope.coroutineContext[Job]!!) + newFixedThreadPoolContext(
5,
this.javaClass.simpleName
)
)
}
@After
fun end() = runBlocking<Unit> {
twig("======================= TEST CANCELLING =============================")
testScope.cancel()
testScope.coroutineContext[Job]?.join()
twig("======================= TEST ENDED ==================================")
}
fun timeout(duration: Long, block: suspend () -> Unit) = timeoutWith(testScope, duration, block)
companion object {
@JvmStatic
lateinit var classScope: CoroutineScope
init {
Twig.plant(TroubleshootingTwig())
twig("================================================================ INIT")
}
@BeforeClass
@JvmStatic
fun createScope() {
twig("======================= CLASS STARTED ===============================")
classScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(2, this.javaClass.simpleName)
)
}
@AfterClass
@JvmStatic
fun destroyScope() = runBlocking<Unit> {
twig("======================= CLASS CANCELLING ============================")
classScope.cancel()
classScope.coroutineContext[Job]?.join()
twig("======================= CLASS ENDED =================================")
}
@JvmStatic
fun timeoutWith(scope: CoroutineScope, duration: Long, block: suspend () -> Unit) {
scope.launch {
delay(duration)
val message = "ERROR: Test timed out after ${duration}ms"
twig(message)
throw TimeoutException(message)
}.let { selfDestruction ->
scope.launch {
block()
selfDestruction.cancel()
}
}
}
}
}

View File

@ -7,11 +7,11 @@ 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
import cash.z.ecc.android.sdk.ext.ScopedTest
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.service.LightWalletService
import cash.z.ecc.android.sdk.test.ScopedTest
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.stub
import kotlinx.coroutines.cancel

View File

@ -141,7 +141,6 @@ dependencyResolutionManagement {
listOf(
"androidx-espresso-core",
"androidx-espresso-intents",
"androidx-espresso-contrib",
"androidx-test-junit",
"androidx-test-core"
)
@ -173,5 +172,6 @@ rootProject.name = "zcash-android-sdk"
includeBuild("build-conventions")
include("darkside-test-lib")
include("sdk-lib")
include("demo-app")