Browse Source

Add test for `BugReportPresenter`

kittykat-patch-1
Benoit Marty 2 years ago
parent
commit
31eb86ebe4
  1. 5
      features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenter.kt
  2. 4
      features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt
  3. 9
      features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/DefaultBugReporter.kt
  4. 8
      features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/DefaultScreenshotHolder.kt
  5. 3
      features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt
  6. 247
      features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenterTest.kt
  7. 70
      features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeBugReporter.kt
  8. 30
      features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeScreenshotHolder.kt

5
features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenter.kt

@ -23,7 +23,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.core.net.toUri
import io.element.android.features.rageshake.crash.CrashDataStore import io.element.android.features.rageshake.crash.CrashDataStore
import io.element.android.features.rageshake.logs.VectorFileLogger import io.element.android.features.rageshake.logs.VectorFileLogger
import io.element.android.features.rageshake.reporter.BugReporter import io.element.android.features.rageshake.reporter.BugReporter
@ -73,7 +72,7 @@ class BugReportPresenter @Inject constructor(
override fun present(): BugReportState { override fun present(): BugReportState {
val screenshotUri = rememberSaveable { val screenshotUri = rememberSaveable {
mutableStateOf( mutableStateOf(
screenshotHolder.getFile()?.toUri()?.toString() screenshotHolder.getFileUri()
) )
} }
val crashInfo: String by crashDataStore val crashInfo: String by crashDataStore
@ -150,6 +149,6 @@ class BugReportPresenter @Inject constructor(
private fun CoroutineScope.resetAll() = launch { private fun CoroutineScope.resetAll() = launch {
screenshotHolder.reset() screenshotHolder.reset()
crashDataStore.reset() crashDataStore.reset()
VectorFileLogger.getFromTimber().reset() VectorFileLogger.getFromTimber()?.reset()
} }
} }

4
features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt

@ -43,8 +43,8 @@ class VectorFileLogger(
) : Timber.Tree() { ) : Timber.Tree() {
companion object { companion object {
fun getFromTimber(): VectorFileLogger { fun getFromTimber(): VectorFileLogger? {
return Timber.forest().filterIsInstance<VectorFileLogger>().first() return Timber.forest().filterIsInstance<VectorFileLogger>().firstOrNull()
} }
private const val SIZE_20MB = 20 * 1024 * 1024 private const val SIZE_20MB = 20 * 1024 * 1024

9
features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/DefaultBugReporter.kt

@ -18,6 +18,8 @@ package io.element.android.features.rageshake.reporter
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.core.net.toFile
import androidx.core.net.toUri
import io.element.android.features.rageshake.R import io.element.android.features.rageshake.R
import io.element.android.features.rageshake.crash.CrashDataStore import io.element.android.features.rageshake.crash.CrashDataStore
import io.element.android.features.rageshake.logs.VectorFileLogger import io.element.android.features.rageshake.logs.VectorFileLogger
@ -153,7 +155,7 @@ class DefaultBugReporter @Inject constructor(
val gzippedFiles = ArrayList<File>() val gzippedFiles = ArrayList<File>()
val vectorFileLogger = VectorFileLogger.getFromTimber() val vectorFileLogger = VectorFileLogger.getFromTimber()
if (withDevicesLogs) { if (withDevicesLogs && vectorFileLogger != null) {
val files = vectorFileLogger.getLogFiles() val files = vectorFileLogger.getLogFiles()
files.mapNotNullTo(gzippedFiles) { f -> files.mapNotNullTo(gzippedFiles) { f ->
if (!mIsCancelled) { if (!mIsCancelled) {
@ -254,7 +256,10 @@ class DefaultBugReporter @Inject constructor(
mBugReportFiles.addAll(gzippedFiles) mBugReportFiles.addAll(gzippedFiles)
if (withScreenshot) { if (withScreenshot) {
screenshotHolder.getFile()?.let { screenshotFile -> screenshotHolder.getFileUri()
?.toUri()
?.toFile()
?.let { screenshotFile ->
try { try {
builder.addFormDataPart( builder.addFormDataPart(
"file", "file",

8
features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/DefaultScreenshotHolder.kt

@ -18,6 +18,7 @@ package io.element.android.features.rageshake.screenshot
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.bitmap.writeBitmap import io.element.android.libraries.androidutils.bitmap.writeBitmap
import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.androidutils.file.safeDelete
@ -38,7 +39,12 @@ class DefaultScreenshotHolder @Inject constructor(
file.writeBitmap(data, Bitmap.CompressFormat.PNG, 85) file.writeBitmap(data, Bitmap.CompressFormat.PNG, 85)
} }
override fun getFile() = file.takeIf { it.exists() && it.length() > 0 } override fun getFileUri(): String? {
return file
.takeIf { it.exists() && it.length() > 0 }
?.toUri()
?.toString()
}
override fun reset() { override fun reset() {
file.safeDelete() file.safeDelete()

3
features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt

@ -17,10 +17,9 @@
package io.element.android.features.rageshake.screenshot package io.element.android.features.rageshake.screenshot
import android.graphics.Bitmap import android.graphics.Bitmap
import java.io.File
interface ScreenshotHolder { interface ScreenshotHolder {
fun writeBitmap(data: Bitmap) fun writeBitmap(data: Bitmap)
fun getFile(): File? fun getFileUri(): String?
fun reset() fun reset()
} }

247
features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenterTest.kt

@ -0,0 +1,247 @@
/*
* 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.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.rageshake.bugreport
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.rageshake.crash.ui.A_CRASH_DATA
import io.element.android.features.rageshake.crash.ui.FakeCrashDataStore
import io.element.android.libraries.architecture.Async
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
const val A_SHORT_DESCRIPTION = "bug!"
const val A_LONG_DESCRIPTION = "I have seen a bug!"
class BugReportPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.hasCrashLogs).isFalse()
assertThat(initialState.formState).isEqualTo(BugReportFormState.Default)
assertThat(initialState.sending).isEqualTo(Async.Uninitialized)
assertThat(initialState.screenshotUri).isNull()
assertThat(initialState.sendingProgress).isEqualTo(0f)
assertThat(initialState.submitEnabled).isFalse()
}
}
@Test
fun `present - set description`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_SHORT_DESCRIPTION))
assertThat(awaitItem().submitEnabled).isFalse()
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION))
assertThat(awaitItem().submitEnabled).isTrue()
}
}
@Test
fun `present - can contact`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetCanContact(true))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(canContact = true))
initialState.eventSink.invoke(BugReportEvents.SetCanContact(false))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(canContact = false))
}
}
@Test
fun `present - send crash logs`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Since this is true by default, start by disabling
initialState.eventSink.invoke(BugReportEvents.SetSendCrashLog(false))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendCrashLogs = false))
initialState.eventSink.invoke(BugReportEvents.SetSendCrashLog(true))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendCrashLogs = true))
}
}
@Test
fun `present - send logs`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Since this is true by default, start by disabling
initialState.eventSink.invoke(BugReportEvents.SetSendLog(false))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendLogs = false))
initialState.eventSink.invoke(BugReportEvents.SetSendLog(true))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendLogs = true))
}
}
@Test
fun `present - send screenshot`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetSendScreenshot(true))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendScreenshot = true))
initialState.eventSink.invoke(BugReportEvents.SetSendScreenshot(false))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendScreenshot = false))
}
}
@Test
fun `present - reset all`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.hasCrashLogs).isTrue()
assertThat(initialState.screenshotUri).isEqualTo(A_SCREENSHOT_URI)
initialState.eventSink.invoke(BugReportEvents.ResetAll)
val resetState = awaitItem()
assertThat(resetState.hasCrashLogs).isFalse()
// TODO Make it live assertThat(resetState.screenshotUri).isNull()
}
}
@Test
fun `present - send success`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(mode = FakeBugReporterMode.Success),
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SendBugReport)
skipItems(1)
val progressState = awaitItem()
assertThat(progressState.sending).isEqualTo(Async.Loading(null))
assertThat(progressState.sendingProgress).isEqualTo(0f)
assertThat(awaitItem().sendingProgress).isEqualTo(0.5f)
assertThat(awaitItem().sendingProgress).isEqualTo(1f)
skipItems(1)
assertThat(awaitItem().sending).isEqualTo(Async.Success(Unit))
}
}
@Test
fun `present - send failure`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(mode = FakeBugReporterMode.Failure),
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SendBugReport)
skipItems(1)
val progressState = awaitItem()
assertThat(progressState.sending).isEqualTo(Async.Loading(null))
assertThat(progressState.sendingProgress).isEqualTo(0f)
assertThat(awaitItem().sendingProgress).isEqualTo(0.5f)
// Failure
assertThat(awaitItem().sendingProgress).isEqualTo(0f)
assertThat((awaitItem().sending as Async.Failure).error.message).isEqualTo(A_REASON)
}
}
@Test
fun `present - send cancel`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(mode = FakeBugReporterMode.Cancel),
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SendBugReport)
skipItems(1)
val progressState = awaitItem()
assertThat(progressState.sending).isEqualTo(Async.Loading(null))
assertThat(progressState.sendingProgress).isEqualTo(0f)
assertThat(awaitItem().sendingProgress).isEqualTo(0.5f)
// Cancelled
assertThat(awaitItem().sendingProgress).isEqualTo(0f)
assertThat(awaitItem().sending).isEqualTo(Async.Uninitialized)
}
}
}

70
features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeBugReporter.kt

@ -0,0 +1,70 @@
/*
* 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.bugreport
import io.element.android.features.rageshake.reporter.BugReporter
import io.element.android.features.rageshake.reporter.BugReporterListener
import io.element.android.features.rageshake.reporter.ReportType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
const val A_REASON = "There has been a failure"
class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter {
override fun sendBugReport(
coroutineScope: CoroutineScope,
reportType: ReportType,
withDevicesLogs: Boolean,
withCrashLogs: Boolean,
withKeyRequestHistory: Boolean,
withScreenshot: Boolean,
theBugDescription: String,
serverVersion: String,
canContact: Boolean,
customFields: Map<String, String>?,
listener: BugReporterListener?,
) {
coroutineScope.launch {
delay(100)
listener?.onProgress(0)
delay(100)
listener?.onProgress(50)
delay(100)
when (mode) {
FakeBugReporterMode.Success -> Unit
FakeBugReporterMode.Failure -> {
listener?.onUploadFailed(A_REASON)
return@launch
}
FakeBugReporterMode.Cancel -> {
listener?.onUploadCancelled()
return@launch
}
}
listener?.onProgress(100)
delay(100)
listener?.onUploadSucceed(null)
}
}
}
enum class FakeBugReporterMode {
Success,
Failure,
Cancel
}

30
features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeScreenshotHolder.kt

@ -0,0 +1,30 @@
/*
* 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.bugreport
import android.graphics.Bitmap
import io.element.android.features.rageshake.screenshot.ScreenshotHolder
const val A_SCREENSHOT_URI = "file://content/uri"
class FakeScreenshotHolder(private val screenshotUri: String? = null) : ScreenshotHolder {
override fun writeBitmap(data: Bitmap) = Unit
override fun getFileUri() = screenshotUri
override fun reset() = Unit
}
Loading…
Cancel
Save