Skip to content

Commit

Permalink
Add minimal leak tests for Android
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasberglund committed Oct 10, 2024
1 parent 897f462 commit a193531
Show file tree
Hide file tree
Showing 27 changed files with 684 additions and 17 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/android-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ jobs:
VALID_TEST_ACCOUNT_NUMBER: ${{ secrets.ANDROID_PROD_TEST_ACCOUNT }}
INVALID_TEST_ACCOUNT_NUMBER: '0000000000000000'
ENABLE_HIGHLY_RATE_LIMITED_TESTS: ${{ github.event_name == 'schedule' && 'true' || 'false' }}
ENABLE_ACCESS_TO_LOCAL_API_TESTS: true
REPORT_DIR: ${{ steps.prepare-report-dir.outputs.report_dir }}
run: ./android/scripts/run-instrumented-tests-repeat.sh ${{ matrix.test-repeat }}

Expand All @@ -553,8 +554,9 @@ jobs:
clearPackageData=true,\
runnerBuilder=de.mannodermaus.junit5.AndroidJUnit5Builder,\
invalid_test_account_number=0000000000000000,\
enable_highly_rate_limited_tests=${{ github.event_name == 'schedule' && 'true' || 'false' }},\
partner_auth=${{ secrets.STAGEMOLE_PARTNER_AUTH }}"
ENABLE_HIGHLY_RATE_LIMITED_TESTS=${{ github.event_name == 'schedule' && 'true' || 'false' }},\
partner_auth=${{ secrets.STAGEMOLE_PARTNER_AUTH }},\
ENABLE_ACCESS_TO_LOCAL_API_TESTS=false"
strategy:
fail-fast: false
matrix:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import net.mullvad.mullvadvpn.compose.component.ExpandChevron
import net.mullvad.mullvadvpn.compose.component.MullvadCheckbox
import net.mullvad.mullvadvpn.compose.preview.RelayItemCheckableCellPreviewParameterProvider
import net.mullvad.mullvadvpn.compose.test.EXPAND_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LOCATION_CELL_TEST_TAG
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
Expand All @@ -55,6 +58,7 @@ private fun PreviewCheckableRelayLocationCell(
expanded = false,
depth = 0,
onExpand = {},
modifier = Modifier.testTag(LOCATION_CELL_TEST_TAG),
)
}
}
Expand Down Expand Up @@ -163,6 +167,7 @@ fun RelayItemCell(
color = MaterialTheme.colorScheme.onSurface,
isExpanded = isExpanded,
onClick = { onToggleExpand(!isExpanded) },
modifier = Modifier.testTag(EXPAND_BUTTON_TEST_TAG),
)
}
}
Expand Down Expand Up @@ -217,6 +222,7 @@ private fun Name(modifier: Modifier = Modifier, relay: RelayItem) {

@Composable
private fun RowScope.ExpandButton(
modifier: Modifier,
color: Color,
isExpanded: Boolean,
onClick: (expand: Boolean) -> Unit,
Expand All @@ -229,7 +235,8 @@ private fun RowScope.ExpandButton(
color = color,
isExpanded = isExpanded,
modifier =
Modifier.fillMaxHeight()
modifier
.fillMaxHeight()
.clickable { onClick(!isExpanded) }
.padding(horizontal = Dimens.largePadding)
.align(Alignment.CenterVertically),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.compose.test.SWITCH_TEST_TAG
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisabled
Expand Down Expand Up @@ -55,7 +57,7 @@ fun MullvadSwitch(
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier,
modifier = modifier.testTag(SWITCH_TEST_TAG),
thumbContent = thumbContent,
enabled = enabled,
colors = colors,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import net.mullvad.mullvadvpn.compose.screen.BottomSheetState.ShowLocationBottom
import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.LOCATION_CELL_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG
Expand Down Expand Up @@ -431,6 +432,7 @@ fun LazyItemScope.RelayLocationItem(
onToggleExpand = { onExpand(it) },
isExpanded = relayItem.expanded,
depth = relayItem.depth,
modifier = Modifier.testTag(LOCATION_CELL_TEST_TAG),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@ const val LAZY_LIST_WIREGUARD_CUSTOM_PORT_NUMBER_TEST_TAG =
const val CUSTOM_PORT_DIALOG_INPUT_TEST_TAG = "custom_port_dialog_input_test_tag"
const val LAZY_LIST_WIREGUARD_OBFUSCATION_TITLE_TEST_TAG =
"lazy_list_wireguard_obfuscation_title_test_tag"
const val SWITCH_TEST_TAG = "switch_test_tag"

// SelectLocationScreen, ConnectScreen, CustomListLocationsScreen
const val CIRCULAR_PROGRESS_INDICATOR = "circular_progress_indicator"
const val EXPAND_BUTTON_TEST_TAG = "expand_button_test_tag"
const val LOCATION_CELL_TEST_TAG = "location_cell_test_tag"

// ConnectScreen
const val SCROLLABLE_COLUMN_TEST_TAG = "scrollable_column_test_tag"
const val SELECT_LOCATION_BUTTON_TEST_TAG = "select_location_button_test_tag"
const val CONNECT_BUTTON_TEST_TAG = "connect_button_test_tag"
const val RECONNECT_BUTTON_TEST_TAG = "reconnect_button_test_tag"
const val CONNECT_CARD_HEADER_TEST_TAG = "connect_card_header_test_tag"
const val LOCATION_INFO_TEST_TAG = "location_info_test_tag"
const val LOCATION_INFO_CONNECTION_IN_TEST_TAG = "location_info_connection_in_test_tag"
const val LOCATION_INFO_CONNECTION_OUT_TEST_TAG = "location_info_connection_out_test_tag"

// ConnectScreen - Notification banner
Expand Down
14 changes: 14 additions & 0 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ grpc-protobuf = "4.28.1"
koin = "4.0.0"
koin-compose = "4.0.0"

# Ktor
ktor = "3.0.0-beta-2"

# Kotlin
# Bump kotlin and kotlin-ksp together, find matching release here:
# https://github.com/google/ksp/releases
kotlin = "2.0.20"
kotlin-ksp = "2.0.20-1.0.25"
kotlinx = "1.9.0"
kotlinx-serialization = "2.0.20"

# Protobuf
protobuf = "0.9.4"
Expand Down Expand Up @@ -131,6 +135,13 @@ kotlin-native-prebuilt = { module = "org.jetbrains.kotlin:kotlin-native-prebuilt
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

# Ktor
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }

# MockK
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
Expand Down Expand Up @@ -160,6 +171,9 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" }

# Kotlinx
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinx-serialization" }

# Protobuf
protobuf-core = { id = "com.google.protobuf", version.ref = "protobuf" }
protobuf-protoc = { id = "com.google.protobuf:protoc", version.ref = "grpc-protobuf" }
Expand Down
6 changes: 5 additions & 1 deletion android/scripts/run-instrumented-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ PARTNER_AUTH="${PARTNER_AUTH:-}"
VALID_TEST_ACCOUNT_NUMBER="${VALID_TEST_ACCOUNT_NUMBER:-}"
INVALID_TEST_ACCOUNT_NUMBER="${INVALID_TEST_ACCOUNT_NUMBER:-}"
ENABLE_HIGHLY_RATE_LIMITED_TESTS="${ENABLE_HIGHLY_RATE_LIMITED_TESTS:-false}"
ENABLE_ACCESS_TO_LOCAL_API_TESTS="${ENABLE_ACCESS_TO_LOCAL_API_TESTS:-false}"
REPORT_DIR="${REPORT_DIR:-}"

while [[ "$#" -gt 0 ]]; do
Expand Down Expand Up @@ -131,7 +132,8 @@ case "$TEST_TYPE" in
echo "Error: The variable PARTNER_AUTH or VALID_TEST_ACCOUNT_NUMBER must be set."
exit 1
fi
OPTIONAL_TEST_ARGUMENTS+=" -e enable_highly_rate_limited_tests $ENABLE_HIGHLY_RATE_LIMITED_TESTS"
OPTIONAL_TEST_ARGUMENTS+=" -e ENABLE_HIGHLY_RATE_LIMITED_TESTS $ENABLE_HIGHLY_RATE_LIMITED_TESTS"
OPTIONAL_TEST_ARGUMENTS+=" -e ENABLE_ACCESS_TO_LOCAL_API_TESTS $ENABLE_ACCESS_TO_LOCAL_API_TESTS"
USE_ORCHESTRATOR="true"
PACKAGE_NAME="net.mullvad.mullvadvpn"
if [[ "$INFRA_FLAVOR" =~ ^(devmole|stagemole)$ ]]; then
Expand All @@ -152,12 +154,14 @@ INSTRUMENTATION_LOG_FILE_PATH="$REPORT_DIR/instrumentation-log.txt"
LOGCAT_FILE_PATH="$REPORT_DIR/logcat.txt"
LOCAL_SCREENSHOT_PATH="$REPORT_DIR/screenshots"
DEVICE_SCREENSHOT_PATH="/sdcard/Pictures/mullvad-$TEST_TYPE"
DEVICE_TEST_ATTACHMENTS_PATH="/sdcard/Download/test-attachments"

echo ""
echo "### Ensure clean report structure ###"
rm -rf "${REPORT_DIR:?}/*"
adb logcat --clear
adb shell rm -rf "$DEVICE_SCREENSHOT_PATH"
adb shell rm -rf "$DEVICE_TEST_ATTACHMENTS_PATH"
echo ""

if [[ "${USE_ORCHESTRATOR-}" == "true" ]]; then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ class AppInteractor(
.text
}

fun extractInIpv4Address(): String {
device.findObjectWithTimeout(By.res("location_info_test_tag")).click()
val inString =
device
.findObjectWithTimeout(
By.res("location_info_connection_in_test_tag"),
VERY_LONG_TIMEOUT,
)
.text

val extractedIpAddress = inString.split(" ")[1].split(":")[0]
return extractedIpAddress
}

fun clickSettingsCog() {
device.findObjectWithTimeout(By.res("top_bar_settings_button")).click()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package net.mullvad.mullvadvpn.test.common.misc

import android.os.Environment
import co.touchlab.kermit.Logger
import java.io.File
import java.io.IOException
import org.junit.jupiter.api.fail

object Attachment {
private const val DIRECTORY_NAME = "test-attachments"
private val testAttachmentsDirectory =
File(
Environment.getExternalStorageDirectory(),
"${Environment.DIRECTORY_DOWNLOADS}/$DIRECTORY_NAME",
)

fun saveAttachment(fileName: String, data: ByteArray) {
createAttachmentsDirectoryIfNotExists()

val file = File(testAttachmentsDirectory, fileName)
try {
file.writeBytes(data)
Logger.v("Saved attachment ${file.absolutePath}")
} catch (e: IOException) {
fail("Failed to save attachment $fileName: ${e.message}")
}
}

private fun createAttachmentsDirectoryIfNotExists() {
if (!testAttachmentsDirectory.exists() && !testAttachmentsDirectory.mkdirs()) {
fail("Failed to create directory ${testAttachmentsDirectory.absolutePath}")
}
}
}
2 changes: 2 additions & 0 deletions android/test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ adb shell 'CLASSPATH=$(pm path androidx.test.services) app_process / \
androidx.test.orchestrator/.AndroidTestOrchestrator'
```

If you want to run tests that make use of APIs hosted at Mullvad HQ you need to set `ENABLE_ACCESS_TO_LOCAL_API_TESTS=true` in `e2e.properties` or pass it as a command line argument when launching tests.

### Firebase Test Lab
Firebase Test Lab can be used to run the tests on vast collection of physical and virtual devices.

Expand Down
18 changes: 17 additions & 1 deletion android/test/e2e/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.gradle.configurationcache.extensions.capitalized
plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlinx.serialization)

id(Dependencies.junit5AndroidPluginId) version Versions.junit5Plugin
}
Expand All @@ -30,6 +31,10 @@ android {
Properties().apply {
load(project.file("e2e.properties").inputStream())
addRequiredPropertyAsBuildConfigField("API_VERSION")
addRequiredPropertyAsBuildConfigField("ENABLE_HIGHLY_RATE_LIMITED_TESTS")
addRequiredPropertyAsBuildConfigField("ENABLE_ACCESS_TO_LOCAL_API_TESTS")
addRequiredPropertyAsBuildConfigField("TRAFFIC_GENERATION_IP_ADDRESS")
addRequiredPropertyAsBuildConfigField("PACKET_CAPTURE_API_HOST")
}

fun MutableMap<String, String>.addOptionalPropertyAsArgument(name: String) {
Expand All @@ -47,7 +52,6 @@ android {
put("clearPackageData", "true")
addOptionalPropertyAsArgument("valid_test_account_number")
addOptionalPropertyAsArgument("invalid_test_account_number")
addOptionalPropertyAsArgument("enable_highly_rate_limited_tests")
}
}

Expand Down Expand Up @@ -106,6 +110,13 @@ android {
buildFeatures { buildConfig = true }
}

junitPlatform {
instrumentationTests {
version.set(Versions.junit5Android)
includeExtensions.set(true)
}
}

androidComponents {
beforeVariants { variantBuilder ->
variantBuilder.enable =
Expand Down Expand Up @@ -143,6 +154,11 @@ dependencies {
implementation(Dependencies.junit5AndroidTestExtensions)
implementation(Dependencies.junit5AndroidTestRunner)
implementation(libs.kotlin.stdlib)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.jodatime)

androidTestUtil(libs.androidx.test.orchestrator)

Expand Down
4 changes: 4 additions & 0 deletions android/test/e2e/e2e.properties
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
API_VERSION=v1
ENABLE_HIGHLY_RATE_LIMITED_TESTS=false
ENABLE_ACCESS_TO_LOCAL_API_TESTS=true
TRAFFIC_GENERATION_IP_ADDRESS=45.83.223.209
PACKET_CAPTURE_API_HOST=192.168.105.1
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,10 @@ abstract class EndToEndTest(private val infra: String) {

app = AppInteractor(device, targetContext, "net.mullvad.mullvadvpn$targetPackageNameSuffix")
}

companion object {
const val DEFAULT_COUNTRY = "Sweden"
const val DEFAULT_CITY = "Gothenburg"
const val DEFAULT_RELAY = "se-got-wg-001"
}
}
Loading

0 comments on commit a193531

Please sign in to comment.