Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gracefully handle compose errors and record compose preview information for displaying #155

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,16 @@ class EmergeSnapshots : TestRule {
composePreviewSnapshotConfig = composePreviewSnapshotConfig,
)
}

internal fun saveError(
type: SnapshotType,
composePreviewSnapshotConfig: ComposePreviewSnapshotConfig? = null,
) {
SnapshotSaver.saveError(
displayName = null,
fqn = fqn,
type = type,
composePreviewSnapshotConfig = composePreviewSnapshotConfig,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
package com.emergetools.snapshots

import com.emergetools.snapshots.shared.ComposePreviewSnapshotConfig
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

enum class SnapshotType {
COMPOSABLE,
VIEW,
ACTIVITY,
}

@OptIn(ExperimentalSerializationApi::class)
@Serializable
internal data class SnapshotImageMetadata(
// Used as the primary key
val name: String,
@Deprecated("Use name instead")
val keyName: String,
// User defined name, or set to defaults by our backend
val displayName: String?,
// Filename of the outputted image
val filename: String,
// FQN of the test class
val fqn: String,
val type: SnapshotType,
// Compose-specific metadata, only set if type == COMPOSABLE
val composePreviewSnapshotConfig: ComposePreviewSnapshotConfig? = null,
)
@JsonClassDiscriminator("metadataType")
sealed class SnapshotMetadata {
abstract val name: String
abstract val displayName: String?
abstract val fqn: String
abstract val type: SnapshotType
abstract val composePreviewSnapshotConfig: ComposePreviewSnapshotConfig?

@Serializable
internal class SuccessMetadata(
// Used as the primary key
override val name: String,
// User defined name, or set to defaults by our backend
override val displayName: String?,
// Filename of the outputted image
val filename: String,
// FQN of the test class
override val fqn: String,
override val type: SnapshotType,
// Compose-specific metadata, only set if type == COMPOSABLE
override val composePreviewSnapshotConfig: ComposePreviewSnapshotConfig? = null,
) : SnapshotMetadata()

@Serializable
internal class ErrorMetadata(
// Used as the primary key
override val name: String,
// User defined name, or set to defaults by our backend
override val displayName: String?,
// FQN of the test class
override val fqn: String,
override val type: SnapshotType,
// Compose-specific metadata, only set if type == COMPOSABLE
override val composePreviewSnapshotConfig: ComposePreviewSnapshotConfig? = null,
) : SnapshotMetadata()
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ internal object SnapshotSaver {
private val args: Bundle
get() = InstrumentationRegistry.getArguments()

private val saveMetadata: Boolean
get() = args.getBoolean(ARG_KEY_SAVE_METADATA, false) ||
args.getString(ARG_KEY_SAVE_METADATA, "false").toBoolean()

fun save(
displayName: String?,
bitmap: Bitmap,
Expand All @@ -56,10 +60,7 @@ internal object SnapshotSaver {
keyName = keyName,
bitmap = bitmap
)
if (
args.getBoolean(ARG_KEY_SAVE_METADATA, false) ||
args.getString(ARG_KEY_SAVE_METADATA, "false").toBoolean()
) {
if (saveMetadata) {
saveMetadata(
snapshotsDir = snapshotsDir,
displayName = displayName,
Expand All @@ -71,6 +72,37 @@ internal object SnapshotSaver {
}
}

fun saveError(
displayName: String?,
fqn: String,
type: SnapshotType,
composePreviewSnapshotConfig: ComposePreviewSnapshotConfig? = null,
) {
val snapshotsDir = File(filesDir, SNAPSHOTS_DIR_NAME)
if (!snapshotsDir.exists() && !snapshotsDir.mkdirs()) {
error("Unable to create snapshots storage directory.")
}

// We need a stable key to use for the filename and comparison
// For composables, see [ComposePreviewSnapshotConfig.keyName]
// For non-composables, we use the normalized displayName
val keyName = keyName(
type = type,
displayName = displayName,
composePreviewSnapshotConfig = composePreviewSnapshotConfig,
)
if (saveMetadata) {
saveErrorMetadata(
snapshotsDir = snapshotsDir,
displayName = displayName,
keyName = keyName,
type = type,
fqn = fqn,
composePreviewSnapshotConfig = composePreviewSnapshotConfig,
)
}
}

private fun saveImage(
snapshotsDir: File,
keyName: String,
Expand All @@ -89,17 +121,40 @@ internal object SnapshotSaver {
type: SnapshotType,
composePreviewSnapshotConfig: ComposePreviewSnapshotConfig? = null,
) {
val metadata = SnapshotImageMetadata(
val metadata: SnapshotMetadata = SnapshotMetadata.SuccessMetadata(
name = keyName,
// TODO: Ryan remove in future
keyName = keyName,
displayName = displayName,
filename = "$keyName$PNG_EXTENSION",
fqn = fqn,
type = type,
composePreviewSnapshotConfig = composePreviewSnapshotConfig,
)

Log.d(TAG, "Saving error metadata for $keyName")
val jsonString = Json.encodeToString(metadata)

saveFile(snapshotsDir, "$keyName$JSON_EXTENSION") {
write(jsonString.toByteArray(Charset.defaultCharset()))
}
}

private fun saveErrorMetadata(
snapshotsDir: File,
keyName: String,
displayName: String?,
fqn: String,
type: SnapshotType,
composePreviewSnapshotConfig: ComposePreviewSnapshotConfig? = null,
) {
val metadata: SnapshotMetadata = SnapshotMetadata.ErrorMetadata(
name = keyName,
displayName = displayName,
fqn = fqn,
type = type,
composePreviewSnapshotConfig = composePreviewSnapshotConfig,
)

Log.d(TAG, "Saving error metadata for $keyName")
val jsonString = Json.encodeToString(metadata)

saveFile(snapshotsDir, "$keyName$JSON_EXTENSION") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.platform.app.InstrumentationRegistry
import com.emergetools.snapshots.EmergeSnapshots
import com.emergetools.snapshots.SnapshotType.COMPOSABLE
import com.emergetools.snapshots.shared.ComposePreviewSnapshotConfig
import com.emergetools.snapshots.shared.ComposeSnapshots
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -51,25 +52,30 @@ class EmergeComposeSnapshotReflectiveParameterizedInvoker(private val previewCon
val snapshotRule: EmergeSnapshots = EmergeSnapshots()

@Test
@Suppress("TooGenericExceptionCaught")
fun reflectiveComposableInvoker() {
composeRule.setContent {
val klass = Class.forName(previewConfig.fullyQualifiedClassName)
val composableMethod = klass.methods.find {
it.name == previewConfig.originalFqn.substringAfterLast(".")
}
try {
composeRule.setContent {
val klass = Class.forName(previewConfig.fullyQualifiedClassName)
val composableMethod = klass.methods.find {
it.name == previewConfig.originalFqn.substringAfterLast(".")
}

Log.d(TAG, "Invoking composable method: $composableMethod")
Log.d(TAG, "Invoking composable method: $composableMethod")

composableMethod?.let {
it.isAccessible = true
SnapshotVariantProvider(previewConfig) {
it.invoke(null, currentComposer, 0)
}
} ?: run {
// TODO: Ryan look to write error to file for better debugging
error("Unable to find composable method: ${previewConfig.originalFqn}")
composableMethod?.let {
it.isAccessible = true
SnapshotVariantProvider(previewConfig) {
it.invoke(null, currentComposer, 0)
}
} ?: error("Unable to find composable method: ${previewConfig.originalFqn}")
}
snapshotRule.take(composeRule, previewConfig)
} catch (e: Exception) {
Log.e(TAG, "Error invoking composable method", e)
snapshotRule.saveError(COMPOSABLE, previewConfig)
// Re-throw to fail the test
throw e
}
snapshotRule.take(composeRule, previewConfig)
}
}
Loading