Merge pull request #1878 from vector-im/feature/bma/moreTests2

Add more unit tests
This commit is contained in:
Benoit Marty 2023-11-24 17:17:52 +01:00 committed by GitHub
commit a2c1476793
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 615 additions and 226 deletions

View file

@ -225,6 +225,7 @@ dependencies {
kapt(libs.dagger.compiler)
testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.x.intent
import android.content.Context
import android.content.Intent
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.deeplink.DeepLinkCreator
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.x.MainActivity
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class IntentProviderImplTest {
@Test
fun `test getViewRoomIntent with Session`() {
val sut = createIntentProviderImpl()
val result = sut.getViewRoomIntent(
sessionId = A_SESSION_ID,
roomId = null,
threadId = null,
)
result.commonAssertions()
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org")
}
@Test
fun `test getViewRoomIntent with Session and Room`() {
val sut = createIntentProviderImpl()
val result = sut.getViewRoomIntent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
)
result.commonAssertions()
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
}
@Test
fun `test getViewRoomIntent with Session, Room and Thread`() {
val sut = createIntentProviderImpl()
val result = sut.getViewRoomIntent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
)
result.commonAssertions()
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
}
@Test
fun `test getInviteListIntent`() {
val sut = createIntentProviderImpl()
val result = sut.getInviteListIntent(
sessionId = A_SESSION_ID,
)
result.commonAssertions()
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/invites")
}
private fun createIntentProviderImpl(): IntentProviderImpl {
return IntentProviderImpl(
context = RuntimeEnvironment.getApplication() as Context,
deepLinkCreator = DeepLinkCreator(),
)
}
private fun Intent.commonAssertions() {
assertThat(action).isEqualTo(Intent.ACTION_VIEW)
assertThat(component?.className).isEqualTo(MainActivity::class.java.name)
}
}

View file

@ -58,12 +58,14 @@ dependencies {
implementation(projects.services.analytics.api)
testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.login.impl)
testImplementation(projects.tests.testutils)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl)

View file

@ -0,0 +1,173 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.appnav.intent
import android.app.Activity
import android.content.Intent
import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.impl.oidc.DefaultOidcIntentResolver
import io.element.android.features.login.impl.oidc.OidcUrlParser
import io.element.android.libraries.deeplink.DeepLinkCreator
import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class IntentResolverTest {
@Test
fun `test resolve navigation intent root`() {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = DeepLinkCreator().room(
sessionId = A_SESSION_ID,
roomId = null,
threadId = null,
)
.toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Navigation(
deeplinkData = DeeplinkData.Root(
sessionId = A_SESSION_ID,
)
)
)
}
@Test
fun `test resolve navigation intent room`() {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = DeepLinkCreator().room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
)
.toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Navigation(
deeplinkData = DeeplinkData.Room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
)
)
)
}
@Test
fun `test resolve navigation intent thread`() {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = DeepLinkCreator().room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
)
.toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Navigation(
deeplinkData = DeeplinkData.Room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
)
)
)
}
@Test
fun `test resolve oidc go back`() {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Oidc(
oidcAction = OidcAction.GoBack
)
)
}
@Test
fun `test resolve oidc success`() {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Oidc(
oidcAction = OidcAction.Success(
url = "io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
)
)
)
}
@Test
fun `test resolve oidc invalid`() {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/callback/invalid".toUri()
}
assertThrows(IllegalStateException::class.java) {
sut.resolve(intent)
}
}
@Test
fun `test resolve invalid`() {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/invalid".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isNull()
}
private fun createIntentResolver(): IntentResolver {
return IntentResolver(
deeplinkParser = DeeplinkParser(),
oidcIntentResolver = DefaultOidcIntentResolver(
oidcUrlParser = OidcUrlParser()
),
)
}
}

View file

@ -22,27 +22,19 @@ interface BugReporter {
/**
* Send a bug report.
*
* @param reportType The report type (bug, suggestion, feedback)
* @param withDevicesLogs true to include the device log
* @param withCrashLogs true to include the crash logs
* @param withKeyRequestHistory true to include the crash logs
* @param withScreenshot true to include the screenshot
* @param theBugDescription the bug description
* @param serverVersion version of the server
* @param canContact true if the user opt in to be contacted directly
* @param customFields fields which will be sent with the report
* @param listener the listener
*/
suspend fun sendBugReport(
reportType: ReportType,
withDevicesLogs: Boolean,
withCrashLogs: Boolean,
withKeyRequestHistory: Boolean,
withScreenshot: Boolean,
theBugDescription: String,
serverVersion: String,
canContact: Boolean = false,
customFields: Map<String, String>? = null,
listener: BugReporterListener?
)

View file

@ -42,5 +42,5 @@ interface BugReporterListener {
/**
* The bug report upload succeeded.
*/
fun onUploadSucceed(reportUrl: String?)
fun onUploadSucceed()
}

View file

@ -23,6 +23,12 @@ plugins {
android {
namespace = "io.element.android.features.rageshake.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
@ -50,12 +56,16 @@ dependencies {
ksp(libs.showkase.processor)
testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.mockk)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test)
testImplementation(libs.network.mockwebserver)
}

View file

@ -25,12 +25,11 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.rageshake.api.crash.CrashDataStore
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.api.reporter.ReportType
import io.element.android.features.rageshake.api.crash.CrashDataStore
import io.element.android.features.rageshake.impl.logs.VectorFileLogger
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
import io.element.android.features.rageshake.impl.logs.VectorFileLogger
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope
@ -64,7 +63,7 @@ class BugReportPresenter @Inject constructor(
sendingAction.value = Async.Loading()
}
override fun onUploadSucceed(reportUrl: String?) {
override fun onUploadSucceed() {
sendingProgress.floatValue = 0f
sendingAction.value = Async.Success(Unit)
}
@ -135,15 +134,11 @@ class BugReportPresenter @Inject constructor(
listener: BugReporterListener,
) = launch {
bugReporter.sendBugReport(
reportType = ReportType.BUG_REPORT,
withDevicesLogs = formState.sendLogs,
withCrashLogs = hasCrashLogs && formState.sendLogs,
withKeyRequestHistory = false,
withScreenshot = formState.sendScreenshot,
theBugDescription = formState.description,
serverVersion = "",
canContact = formState.canContact,
customFields = emptyMap(),
listener = listener
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,13 +14,10 @@
* limitations under the License.
*/
package io.element.android.features.rageshake.api.reporter
package io.element.android.features.rageshake.impl.reporter
enum class ReportType {
BUG_REPORT,
SUGGESTION,
SPACE_BETA_FEEDBACK,
THREADS_BETA_FEEDBACK,
AUTO_UISI,
AUTO_UISI_SENDER,
import okhttp3.HttpUrl
fun interface BugReporterUrlProvider {
fun provide(): HttpUrl
}

View file

@ -25,14 +25,12 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.rageshake.api.crash.CrashDataStore
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.api.reporter.ReportType
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
import io.element.android.features.rageshake.impl.R
import io.element.android.libraries.androidutils.file.compressFile
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.extensions.toOnOff
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope
@ -77,26 +75,12 @@ class DefaultBugReporter @Inject constructor(
private val userAgentProvider: UserAgentProvider,
private val sessionStore: SessionStore,
private val buildMeta: BuildMeta,
/*
private val versionProvider: VersionProvider,
private val vectorPreferences: VectorPreferences,
private val vectorFileLogger: VectorFileLogger,
private val systemLocaleProvider: SystemLocaleProvider,
private val matrix: Matrix,
private val processInfo: ProcessInfo,
private val sdkIntProvider: BuildVersionSdkIntProvider,
private val vectorLocale: VectorLocaleProvider,
*/
private val bugReporterUrlProvider: BugReporterUrlProvider,
) : BugReporter {
var inMultiWindowMode = false
companion object {
// filenames
private const val LOG_CAT_ERROR_FILENAME = "logcatError.log"
private const val LOG_CAT_FILENAME = "logcat.log"
private const val LOG_DIRECTORY_NAME = "logs"
// private const val KEY_REQUESTS_FILENAME = "keyRequests.log"
private const val BUFFER_SIZE = 1024 * 1024 * 50
}
@ -105,58 +89,20 @@ class DefaultBugReporter @Inject constructor(
// boolean to cancel the bug report
private val isCancelled = false
/*
val adapter = MatrixJsonParser.getMoshi()
.adapter<JsonDict>(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java))
*/
private val logcatCommandError = arrayOf(
"logcat", // /< Run 'logcat' command
"-d", // /< Dump the log rather than continue outputting it
"-v", // formatting
"threadtime", // include timestamps
"AndroidRuntime:E " + // /< Pick all AndroidRuntime errors (such as uncaught exceptions)"communicatorjni:V " + ///< All communicatorjni logging
"libcommunicator:V " + // /< All libcommunicator logging
"DEBUG:V " + // /< All DEBUG logging - which includes native land crashes (seg faults, etc)
"*:S" // /< Everything else silent, so don't pick it..
)
private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*")
/**
* Send a bug report.
*
* @param reportType The report type (bug, suggestion, feedback)
* @param withDevicesLogs true to include the device log
* @param withCrashLogs true to include the crash logs
* @param withKeyRequestHistory true to include the crash logs
* @param withScreenshot true to include the screenshot
* @param theBugDescription the bug description
* @param serverVersion version of the server
* @param canContact true if the user opt in to be contacted directly
* @param customFields fields which will be sent with the report
* @param listener the listener
*/
override suspend fun sendBugReport(
reportType: ReportType,
withDevicesLogs: Boolean,
withCrashLogs: Boolean,
withKeyRequestHistory: Boolean,
withScreenshot: Boolean,
theBugDescription: String,
serverVersion: String,
canContact: Boolean,
customFields: Map<String, String>?,
listener: BugReporterListener?
) {
// enumerate files to delete
val bugReportFiles: MutableList<File> = ArrayList()
try {
var serverError: String? = null
var reportURL: String? = null
withContext(coroutineDispatchers.io) {
var bugDescription = theBugDescription
val crashCallStack = crashDataStore.crashInfo().first()
@ -181,7 +127,7 @@ class DefaultBugReporter @Inject constructor(
}
if (!isCancelled && (withCrashLogs || withDevicesLogs)) {
val gzippedLogcat = saveLogCat(false)
val gzippedLogcat = saveLogCat()
if (null != gzippedLogcat) {
if (gzippedFiles.size == 0) {
@ -192,68 +138,21 @@ class DefaultBugReporter @Inject constructor(
}
}
/*
activeSessionHolder.getSafeActiveSession()
?.takeIf { !mIsCancelled && withKeyRequestHistory }
?.cryptoService()
?.getGossipingEvents()
?.let { GossipingEventsSerializer().serialize(it) }
?.toByteArray()
?.let { rawByteArray ->
File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME)
.also {
it.outputStream()
.use { os -> os.write(rawByteArray) }
}
}
?.let { compressFile(it) }
?.let { gzippedFiles.add(it) }
*/
val sessionData = sessionStore.getLatestSession()
val deviceId = sessionData?.deviceId ?: "undefined"
val userId = sessionData?.userId ?: "undefined"
var olmVersion = "undefined"
if (!isCancelled) {
val text = when (reportType) {
ReportType.BUG_REPORT -> bugDescription
ReportType.SUGGESTION -> "[Suggestion] $bugDescription"
ReportType.SPACE_BETA_FEEDBACK -> "[spaces-feedback] $bugDescription"
ReportType.THREADS_BETA_FEEDBACK -> "[threads-feedback] $bugDescription"
ReportType.AUTO_UISI_SENDER,
ReportType.AUTO_UISI -> bugDescription
}
// build the multi part request
val builder = BugReporterMultipartBody.Builder()
.addFormDataPart("text", text)
.addFormDataPart("app", rageShakeAppNameForReport(reportType))
.addFormDataPart("text", bugDescription)
.addFormDataPart("app", context.getString(R.string.bug_report_app_name))
.addFormDataPart("user_agent", userAgentProvider.provide())
.addFormDataPart("user_id", userId)
.addFormDataPart("can_contact", canContact.toString())
.addFormDataPart("device_id", deviceId)
// .addFormDataPart("version", versionProvider.getVersion(longFormat = true))
// .addFormDataPart("branch_name", buildMeta.gitBranchName)
// .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion())
.addFormDataPart("olm_version", olmVersion)
.addFormDataPart("device", Build.MODEL.trim())
// .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff())
.addFormDataPart("multi_window", inMultiWindowMode.toOnOff())
// .addFormDataPart(
// "os", Build.VERSION.RELEASE + " (API " + sdkIntProvider.get() + ") " +
// Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME
// )
.addFormDataPart("locale", Locale.getDefault().toString())
// .addFormDataPart("app_language", vectorLocale.applicationLocale.toString())
// .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString())
// .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
.addFormDataPart("server_version", serverVersion)
.apply {
customFields?.forEach { (name, value) ->
addFormDataPart(name, value)
}
}
// add the gzipped files, don't cancel the whole upload if only some file failed to upload
var uploadedSomeLogs = false
@ -293,31 +192,6 @@ class DefaultBugReporter @Inject constructor(
// add some github labels
builder.addFormDataPart("label", buildMeta.versionName)
// builder.addFormDataPart("label", buildMeta.flavorDescription)
// builder.addFormDataPart("label", buildMeta.gitBranchName)
// Possible values for BuildConfig.BUILD_TYPE: "debug", "nightly", "release".
// builder.addFormDataPart("label", BuildConfig.BUILD_TYPE)
when (reportType) {
ReportType.BUG_REPORT -> {
/* nop */
}
ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]")
ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback")
ReportType.THREADS_BETA_FEEDBACK -> builder.addFormDataPart("label", "threads-feedback")
ReportType.AUTO_UISI -> {
builder.addFormDataPart("label", "Z-UISI")
builder.addFormDataPart("label", "android")
builder.addFormDataPart("label", "uisi-recipient")
}
ReportType.AUTO_UISI_SENDER -> {
builder.addFormDataPart("label", "Z-UISI")
builder.addFormDataPart("label", "android")
builder.addFormDataPart("label", "uisi-sender")
}
}
if (crashCallStack.isNotEmpty() && withCrashLogs) {
builder.addFormDataPart("label", "crash")
}
@ -350,7 +224,7 @@ class DefaultBugReporter @Inject constructor(
// build the request
val request = Request.Builder()
.url(context.getString(R.string.bug_report_url))
.url(bugReporterUrlProvider.provide())
.post(requestBody)
.build()
@ -379,7 +253,6 @@ class DefaultBugReporter @Inject constructor(
} else {
try {
val inputStream = response.body!!.byteStream()
serverError = inputStream.use {
buildString {
var ch = it.read()
@ -389,7 +262,6 @@ class DefaultBugReporter @Inject constructor(
}
}
}
// check if the error message
serverError?.let {
try {
@ -401,7 +273,6 @@ class DefaultBugReporter @Inject constructor(
Timber.e(e, "doInBackground ; Json conversion failed")
}
}
// should never happen
if (null == serverError) {
serverError = "Failed with error $responseCode"
@ -412,25 +283,17 @@ class DefaultBugReporter @Inject constructor(
Timber.e(e, "## sendBugReport() : failed to parse error")
}
}
} else {
/*
reportURL = response?.body?.string()?.let { stringBody ->
adapter.fromJson(stringBody)?.get("report_url")?.toString()
}
*/
}
}
}
withContext(coroutineDispatchers.main) {
bugReportCall = null
if (null != listener) {
try {
if (isCancelled) {
listener.onUploadCancelled()
} else if (null == serverError) {
listener.onUploadSucceed(reportURL)
listener.onUploadSucceed()
} else {
listener.onUploadFailed(serverError)
}
@ -449,47 +312,6 @@ class DefaultBugReporter @Inject constructor(
}
}
/**
* Send a bug report either with email or with Vector.
*/
/* TODO Remove
fun openBugReportScreen(activity: FragmentActivity, reportType: ReportType = ReportType.BUG_REPORT) {
screenshot = takeScreenshot(activity)
logDbInfo()
logProcessInfo()
logOtherInfo()
activity.startActivity(BugReportActivity.intent(activity, reportType))
}
*/
// private fun logOtherInfo() {
// Timber.i("SyncThread state: " + activeSessionHolder.getSafeActiveSession()?.syncService()?.getSyncState())
// }
// private fun logDbInfo() {
// val dbInfo = matrix.debugService().getDbUsageInfo()
// Timber.i(dbInfo)
// }
// private fun logProcessInfo() {
// val pInfo = processInfo.getInfo()
// Timber.i(pInfo)
// }
private fun rageShakeAppNameForReport(reportType: ReportType): String {
// As per https://github.com/matrix-org/rageshake
// app: Identifier for the application (eg 'riot-web').
// Should correspond to a mapping configured in the configuration file for github issue reporting to work.
// (see R.string.bug_report_url for configured RS server)
return context.getString(
when (reportType) {
ReportType.AUTO_UISI_SENDER,
ReportType.AUTO_UISI -> R.string.bug_report_auto_uisi_app_name
else -> R.string.bug_report_app_name
}
)
}
override fun logDirectory(): File {
return File(context.cacheDir, LOG_DIRECTORY_NAME)
}
@ -545,11 +367,10 @@ class DefaultBugReporter @Inject constructor(
/**
* Save the logcat.
*
* @param isErrorLogcat true to save the error logcat
* @return the file if the operation succeeds
*/
private fun saveLogCat(isErrorLogcat: Boolean): File? {
val logCatErrFile = File(context.cacheDir.absolutePath, if (isErrorLogcat) LOG_CAT_ERROR_FILENAME else LOG_CAT_FILENAME)
private fun saveLogCat(): File? {
val logCatErrFile = File(context.cacheDir.absolutePath, LOG_CAT_FILENAME)
if (logCatErrFile.exists()) {
logCatErrFile.safeDelete()
@ -557,7 +378,7 @@ class DefaultBugReporter @Inject constructor(
try {
logCatErrFile.writer().use {
getLogCatError(it, isErrorLogcat)
getLogCatError(it)
}
return compressFile(logCatErrFile)
@ -578,13 +399,12 @@ class DefaultBugReporter @Inject constructor(
* Retrieves the logs.
*
* @param streamWriter the stream writer
* @param isErrorLogCat true to save the error logs
*/
private fun getLogCatError(streamWriter: OutputStreamWriter, isErrorLogCat: Boolean) {
private fun getLogCatError(streamWriter: OutputStreamWriter) {
val logcatProc: Process
try {
logcatProc = Runtime.getRuntime().exec(if (isErrorLogCat) logcatCommandError else logcatCommandDebug)
logcatProc = Runtime.getRuntime().exec(logcatCommandDebug)
} catch (e1: IOException) {
return
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.rageshake.impl.reporter
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.rageshake.impl.R
import io.element.android.libraries.di.AppScope
import io.element.android.services.toolbox.api.strings.StringProvider
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultBugReporterUrlProvider @Inject constructor(
private val stringProvider: StringProvider
) : BugReporterUrlProvider {
override fun provide(): HttpUrl {
return stringProvider.getString(R.string.bug_report_url).toHttpUrl()
}
}

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@ -19,7 +18,12 @@
<!-- Rageshake configuration -->
<string name="bug_report_url" translatable="false">https://riot.im/bugreports/submit</string>
<!--
As per https://github.com/matrix-org/rageshake
bug_report_app_name: Identifier for the application (eg 'riot-web').
Should correspond to a mapping configured in the configuration file for github issue reporting to work.
-->
<string name="bug_report_app_name" translatable="false">element-x-android</string>
<string name="bug_report_auto_uisi_app_name" translatable="false">element-auto-uisi</string>
</resources>

View file

@ -18,22 +18,17 @@ package io.element.android.features.rageshake.impl.bugreport
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.api.reporter.ReportType
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import kotlinx.coroutines.delay
import java.io.File
class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter {
override suspend fun sendBugReport(
reportType: ReportType,
withDevicesLogs: Boolean,
withCrashLogs: Boolean,
withKeyRequestHistory: Boolean,
withScreenshot: Boolean,
theBugDescription: String,
serverVersion: String,
canContact: Boolean,
customFields: Map<String, String>?,
listener: BugReporterListener?,
) {
delay(100)
@ -54,7 +49,7 @@ class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Succes
}
listener?.onProgress(100)
delay(100)
listener?.onUploadSucceed(null)
listener?.onUploadSucceed()
}
override fun cleanLogDirectoryIfNeeded() {

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.rageshake.impl.crash
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class VectorUncaughtExceptionHandlerTest {
@Test
fun `activate should change the default handler`() {
val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
sut.activate()
assertThat(Thread.getDefaultUncaughtExceptionHandler()).isInstanceOf(VectorUncaughtExceptionHandler::class.java)
}
@Test
fun `uncaught exception`() = runTest {
val crashDataStore = PreferencesCrashDataStore(RuntimeEnvironment.getApplication())
assertThat(crashDataStore.appHasCrashed().first()).isFalse()
assertThat(crashDataStore.crashInfo().first()).isEmpty()
val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
sut.uncaughtException(Thread(), AN_EXCEPTION)
assertThat(crashDataStore.appHasCrashed().first()).isTrue()
val crashInfo = crashDataStore.crashInfo().first()
assertThat(crashInfo).isNotEmpty()
assertThat(crashInfo).contains("Memory statuses")
crashDataStore.resetAppHasCrashed()
assertThat(crashDataStore.appHasCrashed().first()).isFalse()
}
}

View file

@ -0,0 +1,156 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.rageshake.impl.reporter
import com.google.common.truth.Truth.assertThat
import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class DefaultBugReporterTest {
@Test
fun `test sendBugReport success`() = runTest {
val server = MockWebServer()
server.enqueue(
MockResponse()
.setResponseCode(200)
)
server.start()
val sut = createDefaultBugReporter(server)
var onUploadCancelledCalled = false
var onUploadFailedCalled = false
var progressValues = mutableListOf<Int>()
var onUploadSucceedCalled = false
sut.sendBugReport(
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
theBugDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
override fun onUploadCancelled() {
onUploadCancelledCalled = true
}
override fun onUploadFailed(reason: String?) {
onUploadFailedCalled = true
}
override fun onProgress(progress: Int) {
progressValues.add(progress)
}
override fun onUploadSucceed() {
onUploadSucceedCalled = true
}
},
)
val request = server.takeRequest()
assertThat(request.path).isEqualTo("/")
assertThat(request.method).isEqualTo("POST")
server.shutdown()
assertThat(onUploadCancelledCalled).isFalse()
assertThat(onUploadFailedCalled).isFalse()
assertThat(progressValues.size).isEqualTo(10)
assertThat(onUploadSucceedCalled).isTrue()
}
@Test
fun `test sendBugReport error`() = runTest {
val server = MockWebServer()
server.enqueue(
MockResponse()
.setResponseCode(400)
.setBody("""{"error": "An error body"}""")
)
server.start()
val sut = createDefaultBugReporter(server)
var onUploadCancelledCalled = false
var onUploadFailedCalled = false
var onUploadFailedReason: String? = null
var progressValues = mutableListOf<Int>()
var onUploadSucceedCalled = false
sut.sendBugReport(
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
theBugDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
override fun onUploadCancelled() {
onUploadCancelledCalled = true
}
override fun onUploadFailed(reason: String?) {
onUploadFailedCalled = true
onUploadFailedReason = reason
}
override fun onProgress(progress: Int) {
progressValues.add(progress)
}
override fun onUploadSucceed() {
onUploadSucceedCalled = true
}
},
)
val request = server.takeRequest()
assertThat(request.path).isEqualTo("/")
assertThat(request.method).isEqualTo("POST")
server.shutdown()
assertThat(onUploadCancelledCalled).isFalse()
assertThat(onUploadFailedCalled).isTrue()
assertThat(onUploadFailedReason).isEqualTo("An error body")
assertThat(progressValues.size).isEqualTo(10)
assertThat(onUploadSucceedCalled).isFalse()
}
private fun TestScope.createDefaultBugReporter(
server: MockWebServer
): DefaultBugReporter {
val buildMeta = aBuildMeta()
return DefaultBugReporter(
context = RuntimeEnvironment.getApplication(),
screenshotHolder = FakeScreenshotHolder(),
crashDataStore = FakeCrashDataStore(),
coroutineScope = this,
systemClock = FakeSystemClock(),
coroutineDispatchers = testCoroutineDispatchers(),
okHttpClient = { OkHttpClient.Builder().build() },
userAgentProvider = DefaultUserAgentProvider(buildMeta),
sessionStore = InMemorySessionStore(),
buildMeta = buildMeta,
bugReporterUrlProvider = { server.url("/") }
)
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.rageshake.impl.reporter
import com.google.common.truth.Truth.assertThat
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.Test
class DefaultBugReporterUrlProviderTest {
@Test
fun `test DefaultBugReporterUrlProvider`() {
val sut = DefaultBugReporterUrlProvider(FakeStringProvider("https://example.org"))
val result = sut.provide()
assertThat(result).isEqualTo("https://example.org".toHttpUrl())
}
}

View file

@ -113,6 +113,7 @@ network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.12.0"
network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" }
network_okhttp_okhttp = { module = "com.squareup.okhttp3:okhttp" }
network_okhttp = { module = "com.squareup.okhttp3:okhttp" }
network_mockwebserver = { module = "com.squareup.okhttp3:mockwebserver" }
network_retrofit = "com.squareup.retrofit2:retrofit:2.9.0"
network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.services.toolbox.test.strings
import io.element.android.services.toolbox.api.strings.StringProvider
class FakeStringProvider(
private val defaultResult: String = "A string"
) : StringProvider {
override fun getString(resId: Int): String {
return defaultResult
}
override fun getString(resId: Int, vararg formatArgs: Any?): String {
return defaultResult
}
override fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any?): String {
return defaultResult
}
}