[#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:
Honza Rychnovsky 2022-12-13 14:25:09 +01:00 committed by GitHub
parent d9a0e98dc0
commit 97c0628798
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1056 additions and 40 deletions

View File

@ -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._

View File

@ -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>

View File

@ -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"
}
}
}

View File

@ -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 {

View File

@ -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"
}
}

View File

@ -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"
}
}
}
}

View File

@ -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

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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>

View File

@ -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)
}
}
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
//

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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

109
docs/testing/Testing.md Normal file
View File

@ -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
```

View File

@ -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

View File

@ -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") {

View File

@ -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(

View File

@ -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()

View File

@ -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(

View File

@ -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())

View File

@ -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))",

View File

@ -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()

View File

@ -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

View File

@ -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,

View File

@ -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
}
}
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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()

View File

@ -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 {

View File

@ -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")