[#789] Benchmarking Demo-app
* [#789] Add Benchmark module to Demo-app * Code cleanup * Opti-in experimental coroutines api in tests * Add Testing documentation * Documentation update + ktlint * Check screen on and keyguard unlocked in each test * Introduce UiAutomator extensions * Enhance BenchmarkTrace events definition * Remove unnecessary mutex * Change blocks range * Increase sync blockchain timeout - To always fit in the Balances screen timeout for the selected blocks range * Remove unnecessary fun suspend modifiers * Macrobenchmark lib bump to 1.2.0-alpha8 * Remove duplicate label attr in app/manifest * File and link benchmark on CI task * Add proguard keep rules * Documentation update
This commit is contained in:
parent
d9a0e98dc0
commit
97c0628798
|
@ -4,7 +4,7 @@ This code review checklist is intended to serve as a starting point for the auth
|
|||
<!-- NOTE: Do not modify these when initially opening the pull request. This is a checklist template that you tick off AFTER the pull request is created. -->
|
||||
- [ ] Self-review: Did you review your own code in GitHub's web interface? _Code often looks different when reviewing the diff in a browser, making it easier to spot potential bugs._
|
||||
- [ ] Automated tests: Did you add appropriate automated tests for any code changes?
|
||||
- [ ] Manual tests: Did you update the [manual tests](../blob/main/docs/tests) as appropriate? _While we aim for automated testing of the application, some aspects require manual testing. If you had to manually test something during development of this pull request, write those steps down._
|
||||
- [ ] Manual tests: Did you update the [manual tests](../blob/main/docs/testing/manual_testing) as appropriate? _While we aim for automated testing of the application, some aspects require manual testing. If you had to manually test something during development of this pull request, write those steps down._
|
||||
- [ ] Documentation: Did you update documentation as appropriate? (e.g [README.md](../blob/main/README.md), etc.)
|
||||
- [ ] Run the app: Did you run the demo app and try the changes?
|
||||
- [ ] Rebase and squash: Did you pull in the latest changes from the main branch and squash your commits before assigning a reviewer? _Having your code up to date and squashed will make it easier for others to review. Use best judgement when squashing commits, as some changes (such as refactoring) might be easier to review as a separate commit._
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="demo-app-benchmark-test:connectedBenchmarkAndroidTest"
|
||||
type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
|
||||
<module name="zcash-android-sdk.demo-app-benchmark-test" />
|
||||
<option name="TESTING_TYPE" value="0" />
|
||||
<option name="METHOD_NAME" value="" />
|
||||
<option name="CLASS_NAME" value="" />
|
||||
<option name="PACKAGE_NAME" value="" />
|
||||
<option name="TEST_NAME_REGEX" value="" />
|
||||
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
|
||||
<option name="EXTRA_OPTIONS" value="" />
|
||||
<option name="RETENTION_ENABLED" value="No" />
|
||||
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
|
||||
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
|
||||
<option name="CLEAR_LOGCAT" value="false" />
|
||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
|
||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
|
||||
<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="Java/Kotlin Method Sample (legacy)" />
|
||||
<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>
|
|
@ -52,6 +52,32 @@ pluginManager.withPlugin("com.android.library") {
|
|||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("com.android.test") {
|
||||
project.the<com.android.build.gradle.TestExtension>().apply {
|
||||
configureBaseExtension()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = project.property("ANDROID_MIN_BENCHMARK_VERSION").toString().toInt()
|
||||
targetSdk = project.property("ANDROID_TARGET_SDK_VERSION").toString().toInt()
|
||||
|
||||
// The last two are for support of pseudolocales in debug builds.
|
||||
// If we add other localizations, they should be included in this list.
|
||||
// By explicitly setting supported locales, we strip out unused localizations from third party
|
||||
// libraries (e.g. play services)
|
||||
resourceConfigurations.addAll(listOf("en", "en-rUS", "en-rGB", "en-rAU", "en_XA", "ar_XB"))
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
|
||||
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||
}
|
||||
}
|
||||
testCoverage {
|
||||
jacocoVersion = project.property("JACOCO_VERSION").toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun com.android.build.gradle.BaseExtension.configureBaseExtension() {
|
||||
compileSdkVersion(project.property("ANDROID_COMPILE_SDK_VERSION").toString().toInt())
|
||||
|
@ -116,8 +142,7 @@ fun com.android.build.gradle.BaseExtension.configureBaseExtension() {
|
|||
kotlinOptions {
|
||||
jvmTarget = project.property("ANDROID_JVM_TARGET").toString()
|
||||
allWarningsAsErrors = project.property("ZCASH_IS_TREAT_WARNINGS_AS_ERRORS").toString().toBoolean()
|
||||
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" +
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + "-opt-in=kotlinx.coroutines.FlowPreview"
|
||||
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,14 @@ android {
|
|||
//targetSdk = 30 //Integer.parseInt(project.property("targetSdkVersion"))
|
||||
multiDexEnabled = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
create("benchmark") {
|
||||
// We provide the extra benchmark build type just for benchmarking purposes
|
||||
initWith(buildTypes.getByName("release"))
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
@ -14,7 +14,8 @@ 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"
|
||||
"Darkside tests are configured to only run on the Android Emulator. Please see https://github" +
|
||||
".com/zcash/zcash-android-wallet-sdk/blob/master/docs/testing/Darkside.md"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
plugins {
|
||||
id("com.android.test")
|
||||
kotlin("android")
|
||||
id("zcash-sdk.android-conventions")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "cash.z.ecc.android.sdk.demoapp.benchmark"
|
||||
targetProjectPath = ":${projects.demoApp.name}"
|
||||
experimentalProperties["android.experimental.self-instrumenting"] = true
|
||||
|
||||
defaultConfig {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
// To enable benchmarking for emulators, although only a physical device us gives real results
|
||||
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR"
|
||||
// To simplify module variants, we assume to run benchmarking against mainnet only
|
||||
missingDimensionStrategy("network", "zcashmainnet")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
create("release") {
|
||||
// To provide compatibility with other modules
|
||||
}
|
||||
create("benchmark") {
|
||||
// We provide the extra benchmark build type for benchmarking. We still need to support debug
|
||||
// variants to be compatible with debug variants in other modules, although benchmarking does not allow
|
||||
// not minified build variants - benchmarking with the debug build variants will fail.
|
||||
isMinifyEnabled = true
|
||||
isDebuggable = true
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.androidx.test)
|
||||
implementation(libs.androidx.test.macrobenchmark)
|
||||
implementation(libs.androidx.uiAutomator)
|
||||
|
||||
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
|
||||
implementation(libs.androidx.test.orchestrator) {
|
||||
artifact {
|
||||
type = "apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn androidx.test.services.storage.internal.InternalTestStorage
|
||||
-dontwarn com.google.errorprone.annotations.InlineMe
|
||||
-dontwarn com.google.errorprone.annotations.MustBeClosed
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,123 @@
|
|||
package cash.z.ecc.android.sdk.demoapp.benchmark
|
||||
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.ExperimentalMetricApi
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.StartupTimingMetric
|
||||
import androidx.benchmark.macro.TraceSectionMetric
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Until
|
||||
import cash.z.ecc.android.sdk.demoapp.test.UiTestPrerequisites
|
||||
import cash.z.ecc.android.sdk.demoapp.test.clickAndWaitFor
|
||||
import cash.z.ecc.android.sdk.demoapp.test.waitFor
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Purpose of this class is to provide a basic startup measurements, and captured system traces for investigating the
|
||||
* app's performance. It navigates to the device's home screen, and launches the default activity.
|
||||
*
|
||||
* Run this benchmark from Android Studio only against the Benchmark build type set in all modules.
|
||||
*
|
||||
* We ideally run this against a physical device with Android SDK level 29, at least, as profiling is provided by this
|
||||
* version and later on.
|
||||
*/
|
||||
|
||||
// TODO [#809]: Enable macrobenchmark on CI
|
||||
// TODO [#809]: https://github.com/zcash/zcash-android-wallet-sdk/issues/809
|
||||
class StartupBenchmark : UiTestPrerequisites() {
|
||||
|
||||
companion object {
|
||||
private const val APP_TARGET_PACKAGE_NAME = "cash.z.ecc.android.sdk.demoapp.mainnet" // NON-NLS
|
||||
|
||||
private const val ADDRESS_SCREEN_SECTION = "ADDRESS_SCREEN" // NON-NLS
|
||||
private const val UNIFIED_ADDRESS_SECTION = "UNIFIED_ADDRESS" // NON-NLS
|
||||
private const val SAPLING_ADDRESS_SECTION = "SAPLING_ADDRESS" // NON-NLS
|
||||
private const val TRANSPARENT_ADDRESS_SECTION = "TRANSPARENT_ADDRESS" // NON-NLS
|
||||
}
|
||||
|
||||
private val unifiedAddressPattern = "^[a-z0-9]{141}$".toPattern() // NON-NLS
|
||||
private val saplingAddressPattern = "^[a-z0-9]{78}$".toPattern() // NON-NLS
|
||||
private val transparentAddressPattern = "^[a-zA-Z0-9]{35}$".toPattern() // NON-NLS
|
||||
|
||||
@get:Rule
|
||||
val benchmarkRule = MacrobenchmarkRule()
|
||||
|
||||
/**
|
||||
* This test starts the Demo-app on Home screen and measures its metrics.
|
||||
*/
|
||||
@Test
|
||||
fun appStartup() = benchmarkRule.measureRepeated(
|
||||
packageName = APP_TARGET_PACKAGE_NAME,
|
||||
metrics = listOf(StartupTimingMetric()),
|
||||
iterations = 5,
|
||||
startupMode = StartupMode.COLD,
|
||||
setupBlock = {
|
||||
// Press home button before each run to ensure the starting activity isn't visible
|
||||
pressHome()
|
||||
}
|
||||
) {
|
||||
startActivityAndWait()
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced trace events startup test, which starts the Demo-app on the Home screen and then navigates to the
|
||||
* Get Address screen. Logic for providing addresses from SDK is measured here.
|
||||
*/
|
||||
@Test
|
||||
@OptIn(ExperimentalMetricApi::class)
|
||||
fun tracesSdkStartup() = benchmarkRule.measureRepeated(
|
||||
packageName = APP_TARGET_PACKAGE_NAME,
|
||||
metrics = listOf(
|
||||
TraceSectionMetric(sectionName = ADDRESS_SCREEN_SECTION, mode = TraceSectionMetric.Mode.First),
|
||||
TraceSectionMetric(sectionName = UNIFIED_ADDRESS_SECTION, mode = TraceSectionMetric.Mode.First),
|
||||
TraceSectionMetric(sectionName = SAPLING_ADDRESS_SECTION, mode = TraceSectionMetric.Mode.First),
|
||||
TraceSectionMetric(sectionName = TRANSPARENT_ADDRESS_SECTION, mode = TraceSectionMetric.Mode.First)
|
||||
),
|
||||
compilationMode = CompilationMode.Full(),
|
||||
startupMode = StartupMode.COLD,
|
||||
iterations = 5,
|
||||
measureBlock = {
|
||||
startActivityAndWait()
|
||||
gotoAddressScreen()
|
||||
waitForAddressScreen()
|
||||
closeAddressScreen()
|
||||
}
|
||||
)
|
||||
|
||||
private fun MacrobenchmarkScope.closeAddressScreen() {
|
||||
// To close the Address screen and disconnect from SDK Synchronizer
|
||||
device.pressBack()
|
||||
}
|
||||
|
||||
private fun MacrobenchmarkScope.waitForAddressScreen() {
|
||||
val timeoutSeconds = 5.seconds
|
||||
check(
|
||||
waitForAddressAppear(unifiedAddressPattern, timeoutSeconds) &&
|
||||
waitForAddressAppear(saplingAddressPattern, timeoutSeconds) &&
|
||||
waitForAddressAppear(transparentAddressPattern, timeoutSeconds)
|
||||
) {
|
||||
"Some of the addresses didn't appear before $timeoutSeconds seconds timeout."
|
||||
}
|
||||
}
|
||||
|
||||
private fun MacrobenchmarkScope.waitForAddressAppear(addressPattern: Pattern, timeout: Duration): Boolean {
|
||||
return device.waitFor(Until.hasObject(By.text(addressPattern)), timeout)
|
||||
}
|
||||
|
||||
// TODO [#808]: Add demo-ui-lib module (and reference the hardcoded texts here)
|
||||
// TODO [#808]: https://github.com/zcash/zcash-android-wallet-sdk/issues/808
|
||||
|
||||
private fun MacrobenchmarkScope.gotoAddressScreen() {
|
||||
// Open drawer menu
|
||||
device.findObject(By.desc("Open navigation drawer")) // NON-NLS
|
||||
.clickAndWaitFor(Until.newWindow(), 2.seconds)
|
||||
// Navigate to Addresses screen
|
||||
device.findObject(By.text("Get Address")).click() // NON-NLS
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package cash.z.ecc.android.sdk.demoapp.benchmark
|
||||
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.ExperimentalMetricApi
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.TraceSectionMetric
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Until
|
||||
import cash.z.ecc.android.sdk.demoapp.test.UiTestPrerequisites
|
||||
import cash.z.ecc.android.sdk.demoapp.test.clickAndWaitFor
|
||||
import cash.z.ecc.android.sdk.demoapp.test.waitFor
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* This benchmark class provides measurements and captured custom traces for investigating SDK syncing mechanisms
|
||||
* with restricted blockchain range. It always resets the SDK before the next sync iteration. It uses UIAutomator to
|
||||
* navigate to the Balances screen, where the whole download -> validate -> scan -> enhance process is performed and
|
||||
* thus measured by this test.
|
||||
*
|
||||
* Run this benchmark from Android Studio only against the Benchmark build type set in all modules.
|
||||
*
|
||||
* We ideally run this on a physical device with Android SDK level 29, at least, as profiling is provided by this
|
||||
* version and later on.
|
||||
*/
|
||||
|
||||
// TODO [#809]: Enable macrobenchmark on CI
|
||||
// TODO [#809]: https://github.com/zcash/zcash-android-wallet-sdk/issues/809
|
||||
class SyncBlockchainBenchmark : UiTestPrerequisites() {
|
||||
|
||||
companion object {
|
||||
private const val APP_TARGET_PACKAGE_NAME = "cash.z.ecc.android.sdk.demoapp.mainnet" // NON-NLS
|
||||
|
||||
private const val BALANCE_SCREEN_SECTION = "BALANCE_SCREEN" // NON-NLS
|
||||
private const val BLOCKCHAIN_SYNC_SECTION = "BLOCKCHAIN_SYNC" // NON-NLS
|
||||
private const val DOWNLOAD_SECTION = "DOWNLOAD" // NON-NLS
|
||||
private const val VALIDATION_SECTION = "VALIDATION" // NON-NLS
|
||||
private const val SCAN_SECTION = "SCAN" // NON-NLS
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val benchmarkRule = MacrobenchmarkRule()
|
||||
|
||||
/**
|
||||
* Advanced trace events test, which starts the Demo-app on the Home screen and then navigates to the Get Balance
|
||||
* screen. SDK sync phases with restricted blockchain range are measured during the overall sync mechanism here.
|
||||
*/
|
||||
@Test
|
||||
@OptIn(ExperimentalMetricApi::class)
|
||||
fun tracesSyncBlockchain() = benchmarkRule.measureRepeated(
|
||||
packageName = APP_TARGET_PACKAGE_NAME,
|
||||
metrics = listOf(
|
||||
TraceSectionMetric(sectionName = BALANCE_SCREEN_SECTION, mode = TraceSectionMetric.Mode.First),
|
||||
TraceSectionMetric(sectionName = BLOCKCHAIN_SYNC_SECTION, mode = TraceSectionMetric.Mode.First),
|
||||
TraceSectionMetric(sectionName = DOWNLOAD_SECTION, mode = TraceSectionMetric.Mode.First),
|
||||
TraceSectionMetric(sectionName = VALIDATION_SECTION, mode = TraceSectionMetric.Mode.First),
|
||||
TraceSectionMetric(sectionName = SCAN_SECTION, mode = TraceSectionMetric.Mode.First)
|
||||
),
|
||||
compilationMode = CompilationMode.Full(),
|
||||
startupMode = StartupMode.COLD,
|
||||
iterations = 3,
|
||||
measureBlock = {
|
||||
startActivityAndWait()
|
||||
resetSDK()
|
||||
gotoBalanceScreen()
|
||||
waitForBalanceScreen()
|
||||
closeBalanceScreen()
|
||||
}
|
||||
)
|
||||
|
||||
// TODO [#808]: Add demo-ui-lib module (and reference the hardcoded texts here)
|
||||
// TODO [#808]: https://github.com/zcash/zcash-android-wallet-sdk/issues/808
|
||||
|
||||
private fun MacrobenchmarkScope.resetSDK() {
|
||||
// Open toolbar overflow menu
|
||||
device.findObject(By.desc("More options")).clickAndWaitFor(Until.newWindow(), 2.seconds) // NON-NLS
|
||||
// Click on the reset sdk menu item
|
||||
device.findObject(By.text("Reset SDK")).click() // NON-NLS
|
||||
device.waitForIdle()
|
||||
}
|
||||
|
||||
private fun MacrobenchmarkScope.waitForBalanceScreen() {
|
||||
device.waitFor(Until.hasObject(By.text("Status: SYNCED")), 5.minutes) // NON-NLS
|
||||
}
|
||||
|
||||
private fun MacrobenchmarkScope.closeBalanceScreen() {
|
||||
// To close the Balance screen and disconnect from SDK Synchronizer
|
||||
device.pressBack()
|
||||
}
|
||||
|
||||
private fun MacrobenchmarkScope.gotoBalanceScreen() {
|
||||
// Open drawer menu
|
||||
device.findObject(By.desc("Open navigation drawer")).clickAndWaitFor(Until.newWindow(), 2.seconds) // NON-NLS
|
||||
// Navigate to Balances screen
|
||||
device.findObject(By.text("Get Balance")).click() // NON-NLS
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package cash.z.ecc.android.sdk.demoapp.test
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
|
||||
fun getAppContext(): Context = ApplicationProvider.getApplicationContext()
|
||||
|
||||
fun getStringResource(@StringRes resId: Int) = getAppContext().getString(resId)
|
||||
|
||||
fun getStringResourceWithArgs(@StringRes resId: Int, vararg formatArgs: String) =
|
||||
getAppContext().getString(resId, *formatArgs)
|
|
@ -0,0 +1,15 @@
|
|||
package cash.z.ecc.android.sdk.demoapp.test
|
||||
|
||||
import androidx.test.uiautomator.EventCondition
|
||||
import androidx.test.uiautomator.SearchCondition
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import androidx.test.uiautomator.UiObject2
|
||||
import kotlin.time.Duration
|
||||
|
||||
fun UiDevice.waitFor(condition: SearchCondition<Boolean>, timeout: Duration): Boolean {
|
||||
return wait(condition, timeout.inWholeMilliseconds)
|
||||
}
|
||||
|
||||
fun UiObject2.clickAndWaitFor(condition: EventCondition<Boolean>, timeout: Duration): Boolean {
|
||||
return clickAndWait(condition, timeout.inWholeMilliseconds)
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package cash.z.ecc.android.sdk.demoapp.test
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.os.PowerManager
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Before
|
||||
|
||||
/**
|
||||
* Subclass this in view unit and integration tests. This verifies that
|
||||
* prerequisites necessary for reliable UI tests are met, and it provides more useful error messages.
|
||||
*/
|
||||
open class UiTestPrerequisites {
|
||||
@Before
|
||||
fun verifyPrerequisites() {
|
||||
assertScreenIsOn()
|
||||
assertKeyguardIsUnlocked()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun assertScreenIsOn() {
|
||||
if (!isScreenOn()) {
|
||||
throw AssertionError("Screen must be on for Android UI tests to run") // $NON-NLS
|
||||
}
|
||||
}
|
||||
|
||||
private fun isScreenOn(): Boolean {
|
||||
val powerService = ApplicationProvider.getApplicationContext<Context>()
|
||||
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return powerService.isInteractive
|
||||
}
|
||||
|
||||
fun assertKeyguardIsUnlocked() {
|
||||
if (isKeyguardLocked()) {
|
||||
throw AssertionError("Device must be unlocked on for Android UI tests to run") // $NON-NLS
|
||||
}
|
||||
}
|
||||
|
||||
private fun isKeyguardLocked(): Boolean {
|
||||
val keyguardService = (
|
||||
ApplicationProvider.getApplicationContext<Context>()
|
||||
.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
)
|
||||
|
||||
return keyguardService.isKeyguardLocked
|
||||
}
|
||||
}
|
||||
}
|
|
@ -82,6 +82,12 @@ android {
|
|||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
create("benchmark") {
|
||||
// We provide the extra benchmark build type just for benchmarking purposes
|
||||
initWith(buildTypes.getByName("release"))
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
|
@ -103,6 +109,10 @@ dependencies {
|
|||
implementation(libs.androidx.multidex)
|
||||
implementation(libs.androidx.navigation.fragment)
|
||||
implementation(libs.androidx.navigation.ui)
|
||||
// Just to support profile installation and tracing events needed by benchmark tests
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
implementation(libs.androidx.tracing)
|
||||
|
||||
implementation(libs.material)
|
||||
androidTestImplementation(libs.bundles.androidx.test)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
|
@ -12,7 +12,6 @@
|
|||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
|
@ -21,6 +20,24 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Enable profiling by benchmark -->
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="29" />
|
||||
|
||||
<!-- To bypass "The DROP_SHADER_CACHE broadcast was not received." error
|
||||
see https://issuetracker.google.com/issues/258619948 -->
|
||||
<receiver
|
||||
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
|
@ -11,8 +11,14 @@ import android.widget.Toast
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.util.BenchmarkTrace
|
||||
import cash.z.ecc.android.sdk.demoapp.util.ProvideAddressBenchmarkTrace
|
||||
import cash.z.ecc.android.sdk.demoapp.util.SyncBlockchainBenchmarkTrace
|
||||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
|
||||
|
||||
|
@ -27,6 +33,8 @@ abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
|
|||
val sharedViewModel: SharedViewModel by activityViewModels()
|
||||
lateinit var binding: T
|
||||
|
||||
internal val traceScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -93,4 +101,13 @@ abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
|
|||
* interface so the base class cannot take care of this behavior without some help.
|
||||
*/
|
||||
abstract fun inflateBinding(layoutInflater: LayoutInflater): T
|
||||
|
||||
internal fun reportTraceEvent(event: BenchmarkTrace.Event?) {
|
||||
traceScope.launch {
|
||||
when (event) {
|
||||
is ProvideAddressBenchmarkTrace.Event -> ProvideAddressBenchmarkTrace.writeEvent(event)
|
||||
is SyncBlockchainBenchmarkTrace.Event -> SyncBlockchainBenchmarkTrace.writeEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.view.Menu
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.getSystemService
|
||||
|
@ -36,6 +37,7 @@ class MainActivity :
|
|||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
private lateinit var clipboard: ClipboardManager
|
||||
var fabListener: BaseDemoFragment<out ViewBinding>? = null
|
||||
private val sharedViewModel: SharedViewModel by viewModels()
|
||||
|
||||
/**
|
||||
* The service to use for all demos that interact directly with the service. Since gRPC channels
|
||||
|
@ -102,6 +104,11 @@ class MainActivity :
|
|||
startActivity(newBrowserIntent("https://faucet.zecpages.com/"))
|
||||
}
|
||||
true
|
||||
} else if (item.itemId == R.id.action_reset_sdk) {
|
||||
val navController = findNavController(R.id.nav_host_fragment)
|
||||
navController.navigate(R.id.nav_home)
|
||||
sharedViewModel.resetSDK()
|
||||
true
|
||||
} else {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
|
|
@ -2,13 +2,16 @@ package cash.z.ecc.android.sdk.demoapp
|
|||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
|
@ -42,6 +45,17 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
|
|||
}
|
||||
}
|
||||
|
||||
fun resetSDK() {
|
||||
viewModelScope.launch {
|
||||
with(getApplication<Application>()) {
|
||||
Synchronizer.erase(
|
||||
appContext = applicationContext,
|
||||
network = ZcashNetwork.fromResources(applicationContext)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidSeedPhrase(phrase: String?): Boolean {
|
||||
if (phrase.isNullOrEmpty()) {
|
||||
return false
|
||||
|
|
|
@ -9,6 +9,7 @@ import cash.z.ecc.android.sdk.Synchronizer
|
|||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.ProvideAddressBenchmarkTrace
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
|
@ -61,17 +62,23 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
|||
private fun displayAddress() {
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
|
||||
binding.unifiedAddress.apply {
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_START)
|
||||
val uaddress = synchronizer.getUnifiedAddress()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_END)
|
||||
text = uaddress
|
||||
setOnClickListener { copyToClipboard(uaddress) }
|
||||
}
|
||||
binding.saplingAddress.apply {
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_START)
|
||||
val sapling = synchronizer.getSaplingAddress()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_END)
|
||||
text = sapling
|
||||
setOnClickListener { copyToClipboard(sapling) }
|
||||
}
|
||||
binding.transparentAddress.apply {
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_START)
|
||||
val transparent = synchronizer.getTransparentAddress()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_END)
|
||||
text = transparent
|
||||
setOnClickListener { copyToClipboard(transparent) }
|
||||
}
|
||||
|
@ -84,6 +91,7 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.ADDRESS_SCREEN_START)
|
||||
setup()
|
||||
}
|
||||
|
||||
|
@ -92,6 +100,11 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
|||
displayAddress()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.ADDRESS_SCREEN_END)
|
||||
}
|
||||
|
||||
//
|
||||
// Base Fragment overrides
|
||||
//
|
||||
|
|
|
@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.demoapp.demos.getbalance
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
|
@ -9,12 +10,17 @@ import cash.z.ecc.android.bip39.toSeed
|
|||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.SyncBlockchainBenchmarkTrace
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.ecc.android.sdk.fixture.BlockRangeFixture
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
|
@ -38,9 +44,22 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_START)
|
||||
setHasOptionsMenu(true)
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
// We rather hide options menu actions while actively using the Synchronizer
|
||||
menu.setGroupVisible(R.id.main_menu_group, false)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_END)
|
||||
}
|
||||
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
val seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
@ -55,7 +74,11 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
birthday = if (BenchmarkingExt.isBenchmarking()) {
|
||||
BlockRangeFixture.new().start
|
||||
} else {
|
||||
sharedViewModel.birthdayHeight.value
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -116,7 +139,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
|
||||
binding.shield.apply {
|
||||
// TODO [#776]: Support variable fees
|
||||
// https://github.com/zcash/zcash-android-wallet-sdk/issues/776
|
||||
// TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776
|
||||
visibility = if ((transparentBalance?.available ?: Zatoshi(0)) > ZcashSdk.MINERS_FEE) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
|
@ -126,6 +149,37 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
}
|
||||
|
||||
private fun onStatus(status: Synchronizer.Status) {
|
||||
twig("Synchronizer status: $status")
|
||||
// report benchmark event
|
||||
val traceEvents = when (status) {
|
||||
Synchronizer.Status.DOWNLOADING -> {
|
||||
listOf(
|
||||
SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_START,
|
||||
SyncBlockchainBenchmarkTrace.Event.DOWNLOAD_START
|
||||
)
|
||||
}
|
||||
Synchronizer.Status.VALIDATING -> {
|
||||
listOf(
|
||||
SyncBlockchainBenchmarkTrace.Event.DOWNLOAD_END,
|
||||
SyncBlockchainBenchmarkTrace.Event.VALIDATION_START
|
||||
)
|
||||
}
|
||||
Synchronizer.Status.SCANNING -> {
|
||||
listOf(
|
||||
SyncBlockchainBenchmarkTrace.Event.VALIDATION_END,
|
||||
SyncBlockchainBenchmarkTrace.Event.SCAN_START
|
||||
)
|
||||
}
|
||||
Synchronizer.Status.SYNCED -> {
|
||||
listOf(
|
||||
SyncBlockchainBenchmarkTrace.Event.SCAN_END,
|
||||
SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_END
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
traceEvents?.forEach { reportTraceEvent(it) }
|
||||
|
||||
binding.textStatus.text = "Status: $status"
|
||||
onOrchardBalance(synchronizer.orchardBalances.value)
|
||||
onSaplingBalance(synchronizer.saplingBalances.value)
|
||||
|
|
|
@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -11,6 +12,7 @@ import cash.z.ecc.android.bip39.toSeed
|
|||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
|
@ -132,6 +134,11 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
// Android Lifecycle overrides
|
||||
//
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -147,6 +154,12 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
initTransactionUI()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
// We rather hide options menu actions while actively using the Synchronizer
|
||||
menu.setGroupVisible(R.id.main_menu_group, false)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// the lifecycleScope is used to dispose of the synchronizer when the fragment dies
|
||||
|
|
|
@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.demoapp.demos.send
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
|
@ -12,6 +13,7 @@ import cash.z.ecc.android.sdk.Synchronizer
|
|||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.DemoConstants
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
|
@ -233,6 +235,11 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
// Android Lifecycle overrides
|
||||
//
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -248,6 +255,12 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
initSendUi()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
// We rather hide options menu actions while actively using the Synchronizer
|
||||
menu.setGroupVisible(R.id.main_menu_group, false)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// the lifecycleScope is used to dispose of the synchronizer when the fragment dies
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
package cash.z.ecc.android.sdk.demoapp.util
|
||||
|
||||
import android.os.Looper
|
||||
import androidx.tracing.Trace
|
||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
|
||||
interface BenchmarkTrace {
|
||||
fun checkMainThread() {
|
||||
check(Looper.getMainLooper().thread === Thread.currentThread()) {
|
||||
"Should be called from the main thread, not ${Thread.currentThread()}."
|
||||
}
|
||||
}
|
||||
|
||||
interface Event {
|
||||
val section: String
|
||||
val cookie: Int
|
||||
}
|
||||
}
|
||||
|
||||
object SyncBlockchainBenchmarkTrace : BenchmarkTrace {
|
||||
fun writeEvent(event: BenchmarkTrace.Event?) {
|
||||
twig("New SyncBlockchain event: $event arrived.")
|
||||
if (!BenchmarkingExt.isBenchmarking()) {
|
||||
return
|
||||
}
|
||||
checkMainThread()
|
||||
when (event) {
|
||||
Event.BALANCE_SCREEN_START -> {
|
||||
Trace.beginAsyncSection(Event.BALANCE_SCREEN_START.section, Event.BALANCE_SCREEN_START.cookie)
|
||||
}
|
||||
Event.BALANCE_SCREEN_END -> {
|
||||
Trace.endAsyncSection(Event.BALANCE_SCREEN_END.section, Event.BALANCE_SCREEN_END.cookie)
|
||||
}
|
||||
Event.BLOCKCHAIN_SYNC_START -> {
|
||||
Trace.beginAsyncSection(Event.BLOCKCHAIN_SYNC_START.section, Event.BLOCKCHAIN_SYNC_START.cookie)
|
||||
}
|
||||
Event.BLOCKCHAIN_SYNC_END -> {
|
||||
Trace.endAsyncSection(Event.BLOCKCHAIN_SYNC_END.section, Event.BLOCKCHAIN_SYNC_END.cookie)
|
||||
}
|
||||
Event.DOWNLOAD_START -> {
|
||||
Trace.beginAsyncSection(Event.DOWNLOAD_START.section, Event.DOWNLOAD_START.cookie)
|
||||
}
|
||||
Event.DOWNLOAD_END -> {
|
||||
Trace.endAsyncSection(Event.DOWNLOAD_END.section, Event.DOWNLOAD_END.cookie)
|
||||
}
|
||||
Event.VALIDATION_START -> {
|
||||
Trace.beginAsyncSection(Event.VALIDATION_START.section, Event.VALIDATION_START.cookie)
|
||||
}
|
||||
Event.VALIDATION_END -> {
|
||||
Trace.endAsyncSection(Event.VALIDATION_END.section, Event.VALIDATION_END.cookie)
|
||||
}
|
||||
Event.SCAN_START -> {
|
||||
Trace.beginAsyncSection(Event.SCAN_START.section, Event.SCAN_START.cookie)
|
||||
}
|
||||
Event.SCAN_END -> {
|
||||
Trace.endAsyncSection(Event.SCAN_END.section, Event.SCAN_END.cookie)
|
||||
}
|
||||
else -> { /* nothing to write */ }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
enum class Event : BenchmarkTrace.Event {
|
||||
BALANCE_SCREEN_START {
|
||||
override val section: String = "BALANCE_SCREEN" // NON-NLS
|
||||
override val cookie: Int = 100
|
||||
},
|
||||
BALANCE_SCREEN_END {
|
||||
override val section: String = "BALANCE_SCREEN" // NON-NLS
|
||||
override val cookie: Int = 100
|
||||
},
|
||||
BLOCKCHAIN_SYNC_START {
|
||||
override val section: String = "BLOCKCHAIN_SYNC" // NON-NLS
|
||||
override val cookie: Int = 200
|
||||
},
|
||||
BLOCKCHAIN_SYNC_END {
|
||||
override val section: String = "BLOCKCHAIN_SYNC" // NON-NLS
|
||||
override val cookie: Int = 200
|
||||
},
|
||||
DOWNLOAD_START {
|
||||
override val section: String = "DOWNLOAD" // NON-NLS
|
||||
override val cookie: Int = 300
|
||||
},
|
||||
DOWNLOAD_END {
|
||||
override val section: String = "DOWNLOAD" // NON-NLS
|
||||
override val cookie: Int = 300
|
||||
},
|
||||
VALIDATION_START {
|
||||
override val section: String = "VALIDATION" // NON-NLS
|
||||
override val cookie: Int = 400
|
||||
},
|
||||
VALIDATION_END {
|
||||
override val section: String = "VALIDATION" // NON-NLS
|
||||
override val cookie: Int = 400
|
||||
},
|
||||
SCAN_START {
|
||||
override val section: String = "SCAN" // NON-NLS
|
||||
override val cookie: Int = 500
|
||||
},
|
||||
SCAN_END {
|
||||
override val section: String = "SCAN" // NON-NLS
|
||||
override val cookie: Int = 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ProvideAddressBenchmarkTrace : BenchmarkTrace {
|
||||
fun writeEvent(event: BenchmarkTrace.Event?) {
|
||||
twig("New ProvideAddress event: $event arrived.")
|
||||
if (!BenchmarkingExt.isBenchmarking()) {
|
||||
return
|
||||
}
|
||||
checkMainThread()
|
||||
when (event) {
|
||||
Event.ADDRESS_SCREEN_START -> {
|
||||
Trace.beginAsyncSection(Event.ADDRESS_SCREEN_START.section, Event.ADDRESS_SCREEN_START.cookie)
|
||||
}
|
||||
Event.ADDRESS_SCREEN_END -> {
|
||||
Trace.endAsyncSection(Event.ADDRESS_SCREEN_END.section, Event.ADDRESS_SCREEN_END.cookie)
|
||||
}
|
||||
Event.UNIFIED_ADDRESS_START -> {
|
||||
Trace.beginAsyncSection(Event.UNIFIED_ADDRESS_START.section, Event.UNIFIED_ADDRESS_START.cookie)
|
||||
}
|
||||
Event.UNIFIED_ADDRESS_END -> {
|
||||
Trace.endAsyncSection(Event.UNIFIED_ADDRESS_END.section, Event.UNIFIED_ADDRESS_END.cookie)
|
||||
}
|
||||
Event.SAPLING_ADDRESS_START -> {
|
||||
Trace.beginAsyncSection(Event.SAPLING_ADDRESS_START.section, Event.SAPLING_ADDRESS_START.cookie)
|
||||
}
|
||||
Event.SAPLING_ADDRESS_END -> {
|
||||
Trace.endAsyncSection(Event.SAPLING_ADDRESS_END.section, Event.SAPLING_ADDRESS_END.cookie)
|
||||
}
|
||||
Event.TRANSPARENT_ADDRESS_START -> {
|
||||
Trace.beginAsyncSection(
|
||||
Event.TRANSPARENT_ADDRESS_START.section,
|
||||
Event.TRANSPARENT_ADDRESS_START.cookie
|
||||
)
|
||||
}
|
||||
Event.TRANSPARENT_ADDRESS_END -> {
|
||||
Trace.endAsyncSection(Event.TRANSPARENT_ADDRESS_END.section, Event.TRANSPARENT_ADDRESS_END.cookie)
|
||||
}
|
||||
else -> { /* nothing to write */ }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
enum class Event : BenchmarkTrace.Event {
|
||||
ADDRESS_SCREEN_START {
|
||||
override val section: String = "ADDRESS_SCREEN" // NON-NLS
|
||||
override val cookie: Int = 100
|
||||
},
|
||||
ADDRESS_SCREEN_END {
|
||||
override val section: String = "ADDRESS_SCREEN" // NON-NLS
|
||||
override val cookie: Int = 100
|
||||
},
|
||||
UNIFIED_ADDRESS_START {
|
||||
override val section: String = "UNIFIED_ADDRESS" // NON-NLS
|
||||
override val cookie: Int = 200
|
||||
},
|
||||
UNIFIED_ADDRESS_END {
|
||||
override val section: String = "UNIFIED_ADDRESS" // NON-NLS
|
||||
override val cookie: Int = 200
|
||||
},
|
||||
SAPLING_ADDRESS_START {
|
||||
override val section: String = "SAPLING_ADDRESS" // NON-NLS
|
||||
override val cookie: Int = 300
|
||||
},
|
||||
SAPLING_ADDRESS_END {
|
||||
override val section: String = "SAPLING_ADDRESS" // NON-NLS
|
||||
override val cookie: Int = 300
|
||||
},
|
||||
TRANSPARENT_ADDRESS_START {
|
||||
override val section: String = "TRANSPARENT_ADDRESS" // NON-NLS
|
||||
override val cookie: Int = 400
|
||||
},
|
||||
TRANSPARENT_ADDRESS_END {
|
||||
override val section: String = "TRANSPARENT_ADDRESS" // NON-NLS
|
||||
override val cookie: Int = 400
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,14 +2,23 @@
|
|||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_settings"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_faucet"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_faucet"
|
||||
app:showAsAction="never" />
|
||||
<!-- Note: We hide entire group on SDK sync related screens -->
|
||||
<group
|
||||
android:id="@+id/main_menu_group">
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_settings"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_faucet"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_faucet"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_reset_sdk"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_reset_sdk"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
</menu>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<string name="nav_header_desc">Navigation header</string>
|
||||
<string name="action_settings">Change Seed Phrase</string>
|
||||
<string name="action_faucet">Testnet Faucet</string>
|
||||
<string name="action_reset_sdk">Reset SDK</string>
|
||||
|
||||
<!-- Drawer Menu -->
|
||||
<string name="menu_home">Home</string>
|
||||
|
|
|
@ -3,7 +3,7 @@ The SDK has a variety of public APIs that should be kept stable for SDK consumer
|
|||
|
||||
# Compile Compatibility
|
||||
1. Publish the SDK to mavenLocal
|
||||
1. Bump the SDK version in [gradle.properties](../../gradle.properties)
|
||||
1. Bump the SDK version in [gradle.properties](../gradle.properties)
|
||||
1. Navigate to the root of the SDK checkout
|
||||
1. Run the Gradle task `./gradlew publishToMavenLocal`
|
||||
1. Modify the wallet app to build against the new SDK
|
||||
|
@ -20,7 +20,7 @@ The SDK has a variety of public APIs that should be kept stable for SDK consumer
|
|||
1. Build the unmodified version of the wallet app
|
||||
1. Run the wallet app and create a new wallet
|
||||
1. Publish the SDK to mavenLocal
|
||||
1. Bump the SDK version in [gradle.properties](../../gradle.properties)
|
||||
1. Bump the SDK version in [gradle.properties](../gradle.properties)
|
||||
1. Navigate to the root of the SDK checkout
|
||||
1. Run the Gradle task `./gradlew publishToMavenLocal`
|
||||
1. Modify the wallet app to build against the new SDK
|
|
@ -0,0 +1,109 @@
|
|||
# Testing
|
||||
This documentation outlines our approach to testing. By running tests against our app consistently, we verify the
|
||||
SDK's correctness, functional behavior, and usability before releasing it publicly.
|
||||
|
||||
## Automated testing
|
||||
|
||||
- TBD
|
||||
<!-- TODO [#807]: Testing documentation update -->
|
||||
<!-- TODO [#807]: https://github.com/zcash/zcash-android-wallet-sdk/issues/807 -->
|
||||
|
||||
## Manual testing
|
||||
|
||||
We aim to automate as much as we possibly can. Still manual testing is really important for Quality Assurance.
|
||||
|
||||
Here you'll find our manual testing scripts. When developing a new feature you can add your own that provide the proper steps to properly test it.
|
||||
|
||||
## Gathering Code Coverage
|
||||
The app consists of different Gradle module types (e.g. Kotlin Multiplatform, Android). Generating coverage for these different module types requires different command line invocations.
|
||||
|
||||
### Kotlin Multiplatform
|
||||
Kotlin Multiplatform does not support coverage for all platforms. Most of our code lives under commonMain, with a JVM target. This effectively allows generation of coverage reports with Jacoco. Coverage is enabled by default when running `./gradlew check`.
|
||||
|
||||
### Android
|
||||
The Android Gradle plugin supports code coverage with Jacoco. This integration can sometimes be buggy. For that reason, coverage is disabled by default and can be enabled on a case-by-case basis, by passing `-PIS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED=true` as a command line argument for Gradle builds. For example: `./gradlew connectedCheck -PIS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED=true`.
|
||||
|
||||
When coverage is enabled, running instrumentation tests will automatically generate coverage reports stored under `$module/build/reports/coverage`.
|
||||
|
||||
## Benchmarking
|
||||
This section provides information about available benchmark tests integrated in this project as well as how to use them. Currently, we support macrobenchmark tests run locally as described in the Android [documentation](https://developer.android.com/topic/performance/benchmarking/benchmarking-overview).
|
||||
|
||||
We provide dedicated benchmark test module `demo-app-benchmark-test` for this. If you want to run these benchmark
|
||||
tests against our demo application, make sure you have a physical device connected with Android SDK level 29, at least.
|
||||
Select `benchmark` build variant for this module. Make sure that other modules are set to benchmark
|
||||
type too. The benchmark tests can be run with Android Studio run configuration
|
||||
`demo-app-benchmark-test:connectedBenchmarkAndroidTest`. Running the benchmark test this way automatically
|
||||
provides benchmarking results in Run panel. Or you can run the tests manually from the terminal with `./gradlew connectedBenchmarkAndroidTest` and analyze results with Android Studio's Profiler or [Perfetto](https://ui.perfetto.dev/) tool, as described in this Android [documentation](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview#access-trace).
|
||||
|
||||
**Note**: We've enabled benchmarking also for emulators, although it's always better to run the tests on a real physical device. Emulator benchmark improvements might not carry over to a real user's experience (or even regress real device performance).
|
||||
|
||||
### Referential benchmark tests results
|
||||
Every few months, or before a major SDK release, we run and compare benchmark test results to avoid making the SDK's mechanisms significantly slower.
|
||||
|
||||
**Note**: If possible, run the benchmark tests on a physical device with sufficient empty disk space, connected to the
|
||||
internet and charged or plugged-in to a charger. It's always better to restart the device before approaching to
|
||||
running the benchmark tests. Also, please, ensure you're running it on the latest main branch
|
||||
commits of that date. Generate tests results with the Android Studio run configuration
|
||||
`demo-app-benchmark-test:connectedBenchmarkAndroidTest` and gather results from the Run panel.
|
||||
|
||||
#### Dec 7, 2022:
|
||||
|
||||
- SDK version: `1.10.0-beta01`
|
||||
- Git branch: `789-benchmark-demo-app`
|
||||
- Device:
|
||||
- Pixel 6 - Android 13:
|
||||
```
|
||||
Starting 3 tests on Pixel 6 - 13
|
||||
|
||||
StartupBenchmark_appStartup
|
||||
timeToInitialDisplayMs min 388.8, median 410.9, max 423.0
|
||||
Traces: Iteration 0 1 2 3 4
|
||||
|
||||
StartupBenchmark_tracesSdkStartup
|
||||
ADDRESS_SCREENMs min 784.1, median 900.4, max 926.9
|
||||
SAPLING_ADDRESSMs min 2.4, median 4.3, max 5.9
|
||||
TRANSPARENT_ADDRESSMs min 1.7, median 2.6, max 6.0
|
||||
UNIFIED_ADDRESSMs min 1.5, median 2.2, max 2.8
|
||||
Traces: Iteration 0 1 2 3 4
|
||||
|
||||
SyncBlockchainBenchmark_tracesSyncBlockchain
|
||||
BALANCE_SCREENMs min 46,042.2, median 46,233.0, max 46,462.2
|
||||
BLOCKCHAIN_SYNCMs min 45,393.5, median 45,578.3, max 45,830.3
|
||||
DOWNLOADMs min 34,951.3, median 35,763.0, max 35,870.6
|
||||
SCANMs min 9,536.7, median 9,846.5, max 10,501.8
|
||||
VALIDATIONMs min 93.1, median 112.5, max 124.3
|
||||
Traces: Iteration 0 1 2
|
||||
|
||||
BUILD SUCCESSFUL in 4m 18s
|
||||
```
|
||||
- Pixel 3a - Android 12:
|
||||
```
|
||||
Starting 3 tests on Pixel 3a - 12
|
||||
|
||||
StartupBenchmark_appStartup
|
||||
timeToInitialDisplayMs min 545.3, median 565.3, max 607.2
|
||||
Traces: Iteration 0 1 2 3 4
|
||||
|
||||
StartupBenchmark_tracesSdkStartup
|
||||
ADDRESS_SCREENMs min 897.1, median 955.3, max 1,352.8
|
||||
SAPLING_ADDRESSMs min 3.9, median 6.1, max 8.3
|
||||
TRANSPARENT_ADDRESSMs min 2.0, median 4.2, max 5.9
|
||||
UNIFIED_ADDRESSMs min 2.4, median 2.4, max 5.2
|
||||
Traces: Iteration 0 1 2 3 4
|
||||
|
||||
SyncBlockchainBenchmark_tracesSyncBlockchain
|
||||
BALANCE_SCREENMs min 63,403.1, median 63,716.7, max 63,993.6
|
||||
BLOCKCHAIN_SYNCMs min 62,739.0, median 63,060.5, max 63,345.0
|
||||
DOWNLOADMs min 34,317.3, median 34,462.8, max 34,551.3
|
||||
SCANMs min 28,279.3, median 28,463.3, max 28,655.8
|
||||
VALIDATIONMs min 133.0, median 136.4, max 141.2
|
||||
Traces: Iteration 0 1 2
|
||||
|
||||
BUILD SUCCESSFUL in 6m 12s
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -65,9 +65,11 @@ IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY=false
|
|||
|
||||
# Versions
|
||||
ANDROID_MIN_SDK_VERSION=21
|
||||
ANDROID_MIN_BENCHMARK_VERSION=24
|
||||
ANDROID_TARGET_SDK_VERSION=33
|
||||
ANDROID_COMPILE_SDK_VERSION=33
|
||||
|
||||
# TODO[#317]: Update NDK to 24.0.7856742
|
||||
# TODO[#317]: https://github.com/zcash/zcash-android-wallet-sdk/issues/317
|
||||
# When changing this, be sure to update .github/actions/setup/action.yml
|
||||
ANDROID_NDK_VERSION=22.1.7171670
|
||||
|
@ -94,11 +96,14 @@ ANDROIDX_LIFECYCLE_VERSION=2.4.1
|
|||
ANDROIDX_MULTIDEX_VERSION=2.0.1
|
||||
ANDROIDX_NAVIGATION_VERSION=2.4.2
|
||||
ANDROIDX_PAGING_VERSION=2.1.2
|
||||
ANDROIDX_PROFILE_INSTALLER_VERSION=1.3.0-alpha02
|
||||
ANDROIDX_ROOM_VERSION=2.4.3
|
||||
ANDROIDX_TEST_JUNIT_VERSION=1.1.3
|
||||
ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.1.0-alpha1
|
||||
ANDROIDX_TEST_MACROBENCHMARK_VERSION=1.2.0-alpha08
|
||||
ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.4.2
|
||||
ANDROIDX_TEST_VERSION=1.4.0
|
||||
ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1
|
||||
ANDROIDX_TRACING_VERSION=1.2.0-alpha01
|
||||
ANDROIDX_UI_AUTOMATOR_VERSION=2.3.0-alpha01
|
||||
BIP39_VERSION=1.0.4
|
||||
COROUTINES_OKHTTP=1.0
|
||||
GOOGLE_MATERIAL_VERSION=1.6.1
|
||||
|
|
|
@ -147,6 +147,11 @@ android {
|
|||
)
|
||||
)
|
||||
}
|
||||
create("benchmark") {
|
||||
// We provide the extra benchmark build type just for benchmarking purposes
|
||||
initWith(buildTypes.getByName("release"))
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets.getByName("main") {
|
||||
|
|
|
@ -9,6 +9,7 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|||
import cash.z.ecc.android.sdk.test.getAppContext
|
||||
import cash.z.ecc.fixture.DatabaseNameFixture
|
||||
import cash.z.ecc.fixture.DatabasePathFixture
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
|
@ -37,6 +38,7 @@ class DatabaseCoordinatorTest {
|
|||
// to test mutex implementation and correct DatabaseCoordinator function call result.
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun mutex_test() = runTest {
|
||||
var testResult: File? = null
|
||||
|
||||
|
@ -77,6 +79,7 @@ class DatabaseCoordinatorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun database_cache_file_creation_test() = runTest {
|
||||
val directory = File(DatabasePathFixture.new())
|
||||
val fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_CACHE_NAME)
|
||||
|
@ -92,6 +95,7 @@ class DatabaseCoordinatorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun database_data_file_creation_test() = runTest {
|
||||
val directory = File(DatabasePathFixture.new())
|
||||
val fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_DATA_NAME)
|
||||
|
@ -107,6 +111,7 @@ class DatabaseCoordinatorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun database_transactions_file_creation_test() = runTest {
|
||||
val directory = File(DatabasePathFixture.new())
|
||||
val fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_PENDING_TRANSACTIONS_NAME)
|
||||
|
@ -122,6 +127,7 @@ class DatabaseCoordinatorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun database_files_move_test() = runTest {
|
||||
val parentFile = File(
|
||||
DatabasePathFixture.new(
|
||||
|
@ -205,6 +211,7 @@ class DatabaseCoordinatorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun delete_database_files_test() = runTest {
|
||||
val parentFile = File(
|
||||
DatabasePathFixture.new(
|
||||
|
|
|
@ -5,6 +5,7 @@ import cash.z.ecc.android.sdk.internal.ext.createNewFileSuspend
|
|||
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
|
||||
import cash.z.ecc.android.sdk.internal.ext.getSha1Hash
|
||||
import cash.z.ecc.android.sdk.test.getAppContext
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
|
@ -25,6 +26,7 @@ class FileExtTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun check_empty_file_sha1_result() = runTest {
|
||||
testFile.apply {
|
||||
createNewFileSuspend()
|
||||
|
@ -39,6 +41,7 @@ class FileExtTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun check_not_empty_file_sha1_result() = runTest {
|
||||
testFile.apply {
|
||||
createNewFileSuspend()
|
||||
|
|
|
@ -8,6 +8,7 @@ import cash.z.ecc.android.sdk.internal.ext.listFilesSuspend
|
|||
import cash.z.ecc.android.sdk.test.getAppContext
|
||||
import cash.z.ecc.fixture.SaplingParamToolFixture
|
||||
import cash.z.ecc.fixture.SaplingParamsFixture
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
|
@ -32,6 +33,7 @@ class SaplingParamToolBasicTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun init_sapling_param_tool_test() = runTest {
|
||||
val spendSaplingParams = SaplingParamsFixture.new()
|
||||
val outputSaplingParams = SaplingParamsFixture.new(
|
||||
|
@ -60,6 +62,7 @@ class SaplingParamToolBasicTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun init_and_get_params_destination_dir_test() = runTest {
|
||||
val destDir = SaplingParamTool.new(getAppContext()).properties.paramsDirectory
|
||||
|
||||
|
@ -73,6 +76,7 @@ class SaplingParamToolBasicTest {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun move_files_from_legacy_destination_test() = runTest {
|
||||
SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY.mkdirs()
|
||||
val spendFile = File(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY, SaplingParamsFixture.SPEND_FILE_NAME)
|
||||
|
@ -124,6 +128,7 @@ class SaplingParamToolBasicTest {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun ensure_params_exception_thrown_test() = runTest {
|
||||
val saplingParamTool = SaplingParamTool(
|
||||
SaplingParamToolFixture.new(
|
||||
|
|
|
@ -6,6 +6,7 @@ import cash.z.ecc.android.sdk.exception.TransactionEncoderException
|
|||
import cash.z.ecc.android.sdk.internal.ext.listFilesSuspend
|
||||
import cash.z.ecc.android.sdk.test.getAppContext
|
||||
import cash.z.ecc.fixture.SaplingParamsFixture
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
|
@ -146,6 +147,7 @@ class SaplingParamToolIntegrationTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun fetch_params_uninitialized_test() = runTest {
|
||||
val saplingParamTool = SaplingParamTool.new(getAppContext())
|
||||
|
||||
|
@ -160,6 +162,7 @@ class SaplingParamToolIntegrationTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun fetch_params_incorrect_hash_test() = runTest {
|
||||
val saplingParamTool = SaplingParamTool.new(getAppContext())
|
||||
|
||||
|
@ -178,6 +181,7 @@ class SaplingParamToolIntegrationTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun fetch_params_incorrect_max_file_size_test() = runTest {
|
||||
val saplingParamTool = SaplingParamTool.new(getAppContext())
|
||||
|
||||
|
@ -196,6 +200,7 @@ class SaplingParamToolIntegrationTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun fetch_param_manual_recover_test_from_fetch_params_exception() = runTest {
|
||||
val saplingParamTool = SaplingParamTool.new(getAppContext())
|
||||
|
||||
|
@ -225,6 +230,7 @@ class SaplingParamToolIntegrationTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun fetch_param_manual_recover_test_from_validate_params_exception() = runTest {
|
||||
val saplingParamTool = SaplingParamTool.new(getAppContext())
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.model
|
|||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
|
@ -10,6 +11,7 @@ import kotlin.test.assertEquals
|
|||
class UnifiedSpendingKeyTest {
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun factory_copies_bytes() = runTest {
|
||||
val spendingKey = WalletFixture.getUnifiedSpendingKey()
|
||||
val expected = spendingKey.copyBytes().copyOf()
|
||||
|
@ -23,6 +25,7 @@ class UnifiedSpendingKeyTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun get_copies_bytes() = runTest {
|
||||
val spendingKey = WalletFixture.getUnifiedSpendingKey()
|
||||
|
||||
|
@ -36,6 +39,7 @@ class UnifiedSpendingKeyTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun toString_does_not_leak() = runTest {
|
||||
assertEquals(
|
||||
"UnifiedSpendingKey(account=Account(value=0))",
|
||||
|
|
|
@ -3,12 +3,14 @@ package cash.z.ecc.android.sdk.tool
|
|||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
|
||||
class DerivationToolTest {
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun create_spending_key_does_not_mutate_passed_bytes() = runTest {
|
||||
val bytesOne = Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy()
|
||||
val bytesTwo = Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy()
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package cash.z.ecc.android.sdk.annotation
|
||||
|
||||
/**
|
||||
* Used in conjunction with the kotlin-allopen plugin to make any class with this annotation open for extension.
|
||||
* Typically, we apply this to classes that we want to mock in androidTests because unit tests don't have this problem,
|
||||
* it's only an issue with JUnit4 Instrumentation tests. This annotation is only leveraged in debug builds.
|
||||
*
|
||||
* Note: the counterpart to this annotation in the debug buildType applies the OpenClass annotation but here we do not.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class OpenForTesting
|
|
@ -68,7 +68,6 @@ import io.grpc.ManagedChannel
|
|||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -102,7 +101,6 @@ import kotlin.coroutines.EmptyCoroutineContext
|
|||
* @property processor saves the downloaded compact blocks to the cache and then scans those blocks for
|
||||
* data related to this wallet.
|
||||
*/
|
||||
@FlowPreview
|
||||
@Suppress("TooManyFunctions")
|
||||
class SdkSynchronizer internal constructor(
|
||||
private val storage: DerivedDataRepository,
|
||||
|
|
|
@ -19,6 +19,7 @@ import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.Mismatche
|
|||
import cash.z.ecc.android.sdk.exception.InitializeException
|
||||
import cash.z.ecc.android.sdk.exception.RustLayerException
|
||||
import cash.z.ecc.android.sdk.ext.BatchMetrics
|
||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.DOWNLOAD_BATCH_SIZE
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
|
||||
|
@ -27,6 +28,7 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk.POLL_INTERVAL
|
|||
import cash.z.ecc.android.sdk.ext.ZcashSdk.RETRIES
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.REWIND_DISTANCE
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.SCAN_BATCH_SIZE
|
||||
import cash.z.ecc.android.sdk.fixture.BlockRangeFixture
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
|
||||
import cash.z.ecc.android.sdk.internal.ext.retryUpTo
|
||||
|
@ -288,13 +290,26 @@ class CompactBlockProcessor internal constructor(
|
|||
setState(Scanned(currentInfo.lastScanRange))
|
||||
BlockProcessingResult.NoBlocksToProcess
|
||||
} else {
|
||||
downloadNewBlocks(currentInfo.lastDownloadRange)
|
||||
val error = validateAndScanNewBlocks(currentInfo.lastScanRange)
|
||||
if (error != BlockProcessingResult.Success) {
|
||||
error
|
||||
if (BenchmarkingExt.isBenchmarking()) {
|
||||
// We inject a benchmark test blocks range at this point to process only a restricted range of blocks
|
||||
// for a more reliable benchmark results.
|
||||
val benchmarkBlockRange = BlockRangeFixture.new()
|
||||
downloadNewBlocks(benchmarkBlockRange)
|
||||
val error = validateAndScanNewBlocks(benchmarkBlockRange)
|
||||
if (error != BlockProcessingResult.Success) {
|
||||
error
|
||||
} else {
|
||||
enhanceTransactionDetails(benchmarkBlockRange)
|
||||
}
|
||||
} else {
|
||||
currentInfo.lastScanRange?.let { enhanceTransactionDetails(it) }
|
||||
?: BlockProcessingResult.NoBlocksToProcess
|
||||
downloadNewBlocks(currentInfo.lastDownloadRange)
|
||||
val error = validateAndScanNewBlocks(currentInfo.lastScanRange)
|
||||
if (error != BlockProcessingResult.Success) {
|
||||
error
|
||||
} else {
|
||||
currentInfo.lastScanRange?.let { enhanceTransactionDetails(it) }
|
||||
?: BlockProcessingResult.NoBlocksToProcess
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package cash.z.ecc.android.sdk.ext
|
||||
|
||||
import cash.z.ecc.android.sdk.BuildConfig
|
||||
|
||||
object BenchmarkingExt {
|
||||
private const val TARGET_BUILD_TYPE = "benchmark" // NON-NLS
|
||||
|
||||
fun isBenchmarking(): Boolean = TARGET_BUILD_TYPE == BuildConfig.BUILD_TYPE
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package cash.z.ecc.android.sdk.fixture
|
||||
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
|
||||
object BlockRangeFixture {
|
||||
|
||||
// Be aware that changing these bounds values in a broader range may result in a timeout reached in
|
||||
// SyncBlockchainBenchmark. So if changing these, don't forget to align also the test timeout in
|
||||
// waitForBalanceScreen() appropriately.
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private val BLOCK_HEIGHT_LOWER_BOUND = BlockHeight.new(ZcashNetwork.Mainnet, 1730001L)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private val BLOCK_HEIGHT_UPPER_BOUND = BlockHeight.new(ZcashNetwork.Mainnet, 1730100L)
|
||||
|
||||
fun new(
|
||||
lowerBound: BlockHeight = BLOCK_HEIGHT_LOWER_BOUND,
|
||||
upperBound: BlockHeight = BLOCK_HEIGHT_UPPER_BOUND
|
||||
): ClosedRange<BlockHeight> {
|
||||
return lowerBound..upperBound
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal.ext.android
|
|||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
@ -23,6 +24,7 @@ abstract class ComputableFlow<T>(dispatcher: CoroutineDispatcher = Dispatchers.I
|
|||
computationScope.launch { computationFlow.emit(compute()) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun cancel() {
|
||||
computationScope.cancel()
|
||||
computationFlow.resetReplayCache()
|
||||
|
|
|
@ -2,6 +2,8 @@ package cash.z.ecc.android.sdk.internal.service
|
|||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.annotation.OpenForTesting
|
||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
||||
import cash.z.ecc.android.sdk.fixture.BlockRangeFixture
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
|
@ -50,10 +52,16 @@ class LightWalletGrpcService private constructor(
|
|||
}
|
||||
|
||||
override fun getLatestBlockHeight(): BlockHeight {
|
||||
return BlockHeight(
|
||||
requireChannel().createStub(singleRequestTimeout)
|
||||
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height
|
||||
)
|
||||
return if (BenchmarkingExt.isBenchmarking()) {
|
||||
// We inject a benchmark test blocks range at this point to process only a restricted range of blocks
|
||||
// for a more reliable benchmark results.
|
||||
BlockRangeFixture.new().endInclusive
|
||||
} else {
|
||||
BlockHeight(
|
||||
requireChannel().createStub(singleRequestTimeout)
|
||||
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getServerInfo(): Service.LightdInfo {
|
||||
|
|
|
@ -27,6 +27,7 @@ pluginManagement {
|
|||
|
||||
id("com.android.application") version (androidGradlePluginVersion) apply (false)
|
||||
id("com.android.library") version (androidGradlePluginVersion) apply (false)
|
||||
id("com.android.test") version (androidGradlePluginVersion) apply (false)
|
||||
id("com.github.ben-manes.versions") version (gradleVersionsPluginVersion) apply (false)
|
||||
id("com.google.devtools.ksp") version(kspVersion) apply (false)
|
||||
id("com.google.protobuf") version (protobufVersion) apply (false)
|
||||
|
@ -74,10 +75,13 @@ dependencyResolutionManagement {
|
|||
val androidxMultidexVersion = extra["ANDROIDX_MULTIDEX_VERSION"].toString()
|
||||
val androidxNavigationVersion = extra["ANDROIDX_NAVIGATION_VERSION"].toString()
|
||||
val androidxPagingVersion = extra["ANDROIDX_PAGING_VERSION"].toString()
|
||||
val androidxProfileInstallerVersion = extra["ANDROIDX_PROFILE_INSTALLER_VERSION"].toString()
|
||||
val androidxRoomVersion = extra["ANDROIDX_ROOM_VERSION"].toString()
|
||||
val androidxTestJunitVersion = extra["ANDROIDX_TEST_JUNIT_VERSION"].toString()
|
||||
val androidxTestOrchestratorVersion = extra["ANDROIDX_ESPRESSO_VERSION"].toString()
|
||||
val androidxTestMacrobenchmarkVersion = extra["ANDROIDX_TEST_MACROBENCHMARK_VERSION"].toString()
|
||||
val androidxTestOrchestratorVersion = extra["ANDROIDX_TEST_ORCHESTRATOR_VERSION"].toString()
|
||||
val androidxTestVersion = extra["ANDROIDX_TEST_VERSION"].toString()
|
||||
val androidxTracingVersion = extra["ANDROIDX_TRACING_VERSION"].toString()
|
||||
val androidxUiAutomatorVersion = extra["ANDROIDX_UI_AUTOMATOR_VERSION"].toString()
|
||||
val bip39Version = extra["BIP39_VERSION"].toString()
|
||||
val coroutinesOkhttpVersion = extra["COROUTINES_OKHTTP"].toString()
|
||||
|
@ -125,6 +129,7 @@ dependencyResolutionManagement {
|
|||
library("androidx-navigation-fragment", "androidx.navigation:navigation-fragment-ktx:$androidxNavigationVersion")
|
||||
library("androidx-navigation-ui", "androidx.navigation:navigation-ui-ktx:$androidxNavigationVersion")
|
||||
library("androidx-paging", "androidx.paging:paging-runtime-ktx:$androidxPagingVersion")
|
||||
library("androidx-profileinstaller", "androidx.profileinstaller:profileinstaller:$androidxProfileInstallerVersion")
|
||||
library("androidx-room-compiler", "androidx.room:room-compiler:$androidxRoomVersion")
|
||||
library("androidx-room-core", "androidx.room:room-ktx:$androidxRoomVersion")
|
||||
library("androidx-sqlite", "androidx.sqlite:sqlite-ktx:${androidxDatabaseVersion}")
|
||||
|
@ -150,9 +155,11 @@ dependencyResolutionManagement {
|
|||
library("androidx-espresso-intents", "androidx.test.espresso:espresso-intents:$androidxEspressoVersion")
|
||||
library("androidx-test-core", "androidx.test:core:$androidxTestVersion")
|
||||
library("androidx-test-junit", "androidx.test.ext:junit:$androidxTestJunitVersion")
|
||||
library("androidx-test-macrobenchmark", "androidx.benchmark:benchmark-macro-junit4:$androidxTestMacrobenchmarkVersion")
|
||||
library("androidx-test-runner", "androidx.test:runner:$androidxTestVersion")
|
||||
library("androidx-testOrchestrator", "androidx.test:orchestrator:$androidxTestOrchestratorVersion")
|
||||
library("androidx-uiAutomator", "androidx.test.uiautomator:uiautomator-v18:$androidxUiAutomatorVersion")
|
||||
library("androidx-test-orchestrator", "androidx.test:orchestrator:$androidxTestOrchestratorVersion")
|
||||
library("androidx-tracing", "androidx.tracing:tracing:$androidxTracingVersion")
|
||||
library("androidx-uiAutomator", "androidx.test.uiautomator:uiautomator:$androidxUiAutomatorVersion")
|
||||
library("coroutines-okhttp", "ru.gildor.coroutines:kotlin-coroutines-okhttp:$coroutinesOkhttpVersion")
|
||||
library("grpc-testing", "io.grpc:grpc-testing:$grpcVersion")
|
||||
library("junit-api", "org.junit.jupiter:junit-jupiter-api:$junitVersion")
|
||||
|
@ -203,4 +210,5 @@ includeBuild("build-conventions")
|
|||
|
||||
include("darkside-test-lib")
|
||||
include("sdk-lib")
|
||||
include("demo-app")
|
||||
include("demo-app")
|
||||
include("demo-app-benchmark-test")
|
Loading…
Reference in New Issue