diff options
17 files changed, 354 insertions, 269 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt b/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt index 5c916882..6b33e1cd 100644 --- a/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt +++ b/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt @@ -31,7 +31,10 @@ import android.service.chooser.ChooserTarget import com.android.intentresolver.contentpreview.PayloadToggleInteractor.ShareouselUpdate import com.android.intentresolver.v2.ui.viewmodel.readAlternateIntents import com.android.intentresolver.v2.ui.viewmodel.readChooserActions +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.Valid import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.log import com.android.intentresolver.v2.validation.types.array import com.android.intentresolver.v2.validation.types.value import com.android.intentresolver.v2.validation.validateFrom @@ -61,11 +64,10 @@ class SelectionChangeCallback( } ) ?.let { bundle -> - readCallbackResponse(bundle).let { validation -> - if (validation.isSuccess()) { - validation.value - } else { - validation.reportToLogcat(TAG) + return when (val result = readCallbackResponse(bundle)) { + is Valid -> result.value + is Invalid -> { + result.errors.forEach { it.log(TAG) } null } } diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java index 98e82b00..52d5f2de 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java @@ -115,6 +115,10 @@ import com.android.intentresolver.v2.profiles.ResolverMultiProfilePagerAdapter; import com.android.intentresolver.v2.ui.ActionTitle; import com.android.intentresolver.v2.ui.model.ActivityModel; import com.android.intentresolver.v2.ui.model.ResolverRequest; +import com.android.intentresolver.v2.validation.Finding; +import com.android.intentresolver.v2.validation.FindingsKt; +import com.android.intentresolver.v2.validation.Invalid; +import com.android.intentresolver.v2.validation.Valid; import com.android.intentresolver.v2.validation.ValidationResult; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; @@ -135,6 +139,7 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import javax.inject.Inject; @@ -243,11 +248,16 @@ public class ResolverActivity extends Hilt_ResolverActivity implements } ValidationResult<ResolverRequest> result = readResolverRequest(mActivityModel); - if (!result.isSuccess()) { - result.reportToLogcat(TAG); + if (result instanceof Invalid) { + ((Invalid) result).getErrors().forEach(new Consumer<Finding>() { + @Override + public void accept(Finding finding) { + FindingsKt.log(finding, TAG); + } + }); finish(); } - mResolverRequest = result.getOrThrow(); + mResolverRequest = ((Valid<ResolverRequest>) result).getValue(); mLogic = createActivityLogic(); mResolvingHome = mResolverRequest.isResolvingHome(); mTargetDataLoader = new DefaultTargetDataLoader( diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt index cd1a16e3..424f36cd 100644 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt @@ -22,7 +22,10 @@ import com.android.intentresolver.inject.ChooserServiceFlags import com.android.intentresolver.v2.ui.model.ActivityModel import com.android.intentresolver.v2.ui.model.ActivityModel.Companion.ACTIVITY_MODEL_KEY import com.android.intentresolver.v2.ui.model.ChooserRequest +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.Valid import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.log import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -45,12 +48,17 @@ constructor( private val status: ValidationResult<ChooserRequest> = readChooserRequest(mActivityModel, flags) - val chooserRequest: ChooserRequest by lazy { status.getOrThrow() } + val chooserRequest: ChooserRequest by lazy { + when(status) { + is Valid -> status.value + is Invalid -> error(status.errors) + } + } fun init(): Boolean { Log.i(TAG, "viewModel init") - if (!status.isSuccess()) { - status.reportToLogcat(TAG) + if (status is Invalid) { + status.errors.forEach { finding -> finding.log(TAG) } return false } Log.i(TAG, "request = $chooserRequest") diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt index 9a3cc9c7..bdf2f00a 100644 --- a/java/src/com/android/intentresolver/v2/validation/Findings.kt +++ b/java/src/com/android/intentresolver/v2/validation/Findings.kt @@ -34,9 +34,13 @@ val Finding.logcatPriority get() = when (importance) { CRITICAL -> Log.ERROR - else -> Log.WARN + WARNING -> Log.WARN } +fun Finding.log(tag: String) { + Log.println(logcatPriority, tag, message) +} + private fun formatMessage(key: String? = null, msg: String) = buildString { key?.also { append("['$key']: ") } append(msg) @@ -52,18 +56,21 @@ data class IgnoredValue( get() = formatMessage(key, "Ignored. $reason") } -data class RequiredValueMissing( +data class NoValue( val key: String, + override val importance: Importance, val allowedType: KClass<*>, ) : Finding { - override val importance = CRITICAL - override val message: String get() = formatMessage( key, - "expected value of ${allowedType.simpleName}, " + "but no value was present" + if (importance == CRITICAL) { + "expected value of ${allowedType.simpleName}, " + "but no value was present" + } else { + "no ${allowedType.simpleName} value present" + } ) } diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt index 46939602..6072ec9f 100644 --- a/java/src/com/android/intentresolver/v2/validation/Validation.kt +++ b/java/src/com/android/intentresolver/v2/validation/Validation.kt @@ -90,7 +90,7 @@ fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): Va is InvalidResultError -> Invalid(validation.findings) // Some other exception was thrown from [validate], - else -> Invalid(findings = listOf(UncaughtException(it))) + else -> Invalid(error = UncaughtException(it)) } } ) @@ -107,8 +107,8 @@ private class ValidationImpl(val source: (String) -> Any?) : Validation { override fun <T> ignored(property: Validator<T>, reason: String) { val result = property.validate(source, WARNING) - if (result.value != null) { - // Note: Any findings about the value (result.findings) are ignored. + if (result is Valid) { + // Note: Any warnings about the value itself (result.findings) are ignored. findings += IgnoredValue(property.key, reason) } } @@ -117,8 +117,16 @@ private class ValidationImpl(val source: (String) -> Any?) : Validation { return runCatching { property.validate(source, importance) } .fold( onSuccess = { result -> - findings += result.findings - result.value + return when (result) { + is Valid -> { + findings += result.warnings + result.value + } + is Invalid -> { + findings += result.errors + null + } + } }, onFailure = { findings += UncaughtException(it, property.key) diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt index 856a521e..f5c467dc 100644 --- a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt +++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt @@ -15,25 +15,12 @@ */ package com.android.intentresolver.v2.validation -import android.util.Log +sealed interface ValidationResult<T> -sealed interface ValidationResult<T> { - val value: T? - val findings: List<Finding> - - fun isSuccess() = value != null - - fun getOrThrow(): T = - checkNotNull(value) { "The result was invalid: " + findings.joinToString(separator = "\n") } - - fun reportToLogcat(tag: String) { - findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) } - } +data class Valid<T>(val value: T, val warnings: List<Finding> = emptyList()) : ValidationResult<T> { + constructor(value: T, warning: Finding) : this(value, listOf(warning)) } -data class Valid<T>(override val value: T?, override val findings: List<Finding> = emptyList()) : - ValidationResult<T> - -data class Invalid<T>(override val findings: List<Finding>) : ValidationResult<T> { - override val value: T? = null +data class Invalid<T>(val errors: List<Finding> = emptyList()) : ValidationResult<T> { + constructor(error: Finding) : this(listOf(error)) } diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt index 3cefeb15..050bd895 100644 --- a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt +++ b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt @@ -18,7 +18,8 @@ package com.android.intentresolver.v2.validation.types import android.content.Intent import android.net.Uri import com.android.intentresolver.v2.validation.Importance -import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue import com.android.intentresolver.v2.validation.Valid import com.android.intentresolver.v2.validation.ValidationResult import com.android.intentresolver.v2.validation.Validator @@ -40,12 +41,14 @@ class IntentOrUri(override val key: String) : Validator<Intent> { is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME)) // No value present. - null -> createResult(importance, RequiredValueMissing(key, Intent::class)) + null -> when (importance) { + Importance.WARNING -> Invalid() // No warnings if optional, but missing + Importance.CRITICAL -> Invalid(NoValue(key, importance, Intent::class)) + } // Some other type. else -> { - return createResult( - importance, + return Invalid( ValueIsWrongType( key, importance, diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt index c6c4abba..78adfd36 100644 --- a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt +++ b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt @@ -15,8 +15,10 @@ */ package com.android.intentresolver.v2.validation.types +import android.content.Intent import com.android.intentresolver.v2.validation.Importance -import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue import com.android.intentresolver.v2.validation.Valid import com.android.intentresolver.v2.validation.ValidationResult import com.android.intentresolver.v2.validation.Validator @@ -37,8 +39,10 @@ class ParceledArray<T : Any>( return when (val value: Any? = source(key)) { // No value present. - null -> createResult(importance, RequiredValueMissing(key, elementType)) - + null -> when (importance) { + Importance.WARNING -> Invalid() // No warnings if optional, but missing + Importance.CRITICAL -> Invalid(NoValue(key, importance, elementType)) + } // A parcel does not transfer the element type information for parcelable // arrays. This leads to a restored type of Array<Parcelable>, which is // incompatible with Array<T : Parcelable>. @@ -54,8 +58,7 @@ class ParceledArray<T : Any>( // At least one incorrect element type found. else -> - createResult( - importance, + Invalid( WrongElementType( key, importance, @@ -69,8 +72,7 @@ class ParceledArray<T : Any>( // The value is not an Array at all. else -> - createResult( - importance, + Invalid( ValueIsWrongType( key, importance, diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt index 3287b84b..0105541d 100644 --- a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt +++ b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt @@ -16,7 +16,8 @@ package com.android.intentresolver.v2.validation.types import com.android.intentresolver.v2.validation.Importance -import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue import com.android.intentresolver.v2.validation.Valid import com.android.intentresolver.v2.validation.ValidationResult import com.android.intentresolver.v2.validation.Validator @@ -36,19 +37,21 @@ class SimpleValue<T : Any>( expected.isInstance(value) -> return Valid(expected.cast(value)) // No value is present. - value == null -> createResult(importance, RequiredValueMissing(key, expected)) + value == null -> when (importance) { + Importance.WARNING -> Invalid() // No warnings if optional, but missing + Importance.CRITICAL -> Invalid(NoValue(key, importance, expected)) + } // The value is some other type. else -> - createResult( - importance, + Invalid(listOf( ValueIsWrongType( key, importance, actualType = value::class, allowedTypes = listOf(expected) ) - ) + )) } } } diff --git a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt index 4e6e5dff..70993b4d 100644 --- a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt +++ b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt @@ -15,13 +15,6 @@ */ package com.android.intentresolver.v2.validation.types -import com.android.intentresolver.v2.validation.Finding -import com.android.intentresolver.v2.validation.Importance -import com.android.intentresolver.v2.validation.Importance.CRITICAL -import com.android.intentresolver.v2.validation.Importance.WARNING -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.validation.ValidationResult import com.android.intentresolver.v2.validation.Validator inline fun <reified T : Any> value(key: String): Validator<T> { @@ -31,15 +24,3 @@ inline fun <reified T : Any> value(key: String): Validator<T> { inline fun <reified T : Any> array(key: String): Validator<List<T>> { return ParceledArray(key, T::class) } - -/** - * Convenience function to wrap a finding in an appropriate result type. - * - * An error [finding] is suppressed when [importance] == [WARNING] - */ -internal fun <T> createResult(importance: Importance, finding: Finding): ValidationResult<T> { - return when (importance) { - WARNING -> Valid(null, listOf(finding).filter { it.importance == WARNING }) - CRITICAL -> Invalid(listOf(finding)) - } -} diff --git a/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt deleted file mode 100644 index 1ff0ce8e..00000000 --- a/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.android.intentresolver.v2.validation - -import com.google.common.truth.FailureMetadata -import com.google.common.truth.IterableSubject -import com.google.common.truth.Subject -import com.google.common.truth.Truth.assertAbout - -class ValidationResultSubject(metadata: FailureMetadata, private val actual: ValidationResult<*>?) : - Subject(metadata, actual) { - - fun isSuccess() = check("isSuccess()").that(actual?.isSuccess()).isTrue() - fun isFailure() = check("isSuccess()").that(actual?.isSuccess()).isFalse() - - fun value(): Subject = check("value").that(actual?.value) - - fun findings(): IterableSubject = check("findings").that(actual?.findings) - - companion object { - fun assertThat(input: ValidationResult<*>): ValidationResultSubject = - assertAbout(::ValidationResultSubject).that(input) - } -} diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt index d2ddf680..d3b9f559 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt @@ -21,6 +21,8 @@ import android.content.Intent.ACTION_SEND import android.content.Intent.ACTION_SEND_MULTIPLE import android.content.Intent.ACTION_VIEW import android.content.Intent.EXTRA_ALTERNATE_INTENTS +import android.content.Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI +import android.content.Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION import android.content.Intent.EXTRA_INTENT import android.content.Intent.EXTRA_REFERRER import android.net.Uri @@ -31,19 +33,13 @@ import com.android.intentresolver.ContentTypeHint import com.android.intentresolver.inject.FakeChooserServiceFlags import com.android.intentresolver.v2.ui.model.ActivityModel import com.android.intentresolver.v2.ui.model.ChooserRequest -import com.android.intentresolver.v2.validation.RequiredValueMissing -import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue +import com.android.intentresolver.v2.validation.Valid import com.google.common.truth.Truth.assertThat import org.junit.Test -// TODO: replace with the new API constant, Intent#EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI -private const val EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI = - "android.intent.extra.CHOOSER_ADDITIONAL_CONTENT_URI" - -// TODO: replace with the new API constant, Intent#EXTRA_CHOOSER_FOCUSED_ITEM_POSITION -private const val EXTRA_CHOOSER_FOCUSED_ITEM_POSITION = - "android.intent.extra.CHOOSER_FOCUSED_ITEM_POSITION" - private fun createActivityModel( targetIntent: Intent?, referrer: Uri? = null, @@ -70,26 +66,30 @@ class ChooserRequestTest { @Test fun missingIntent() { - val launch = createActivityModel(targetIntent = null) - val result = readChooserRequest(launch, fakeChooserServiceFlags) + val model = createActivityModel(targetIntent = null) + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<ChooserRequest> - assertThat(result).value().isNull() - assertThat(result) - .findings() - .containsExactly(RequiredValueMissing(EXTRA_INTENT, Intent::class)) + assertThat(result.errors) + .containsExactly(NoValue(EXTRA_INTENT, Importance.CRITICAL, Intent::class)) } @Test fun referrerFillIn() { val referrer = Uri.parse("android-app://example.com") - val launch = createActivityModel(targetIntent = Intent(ACTION_SEND), referrer) - launch.intent.putExtras(bundleOf(EXTRA_REFERRER to referrer)) + val model = createActivityModel(targetIntent = Intent(ACTION_SEND), referrer) + model.intent.putExtras(bundleOf(EXTRA_REFERRER to referrer)) - val result = readChooserRequest(launch, fakeChooserServiceFlags) + val result = readChooserRequest(model, fakeChooserServiceFlags) - val fillIn = result.value?.getReferrerFillInIntent() - assertThat(fillIn?.hasExtra(EXTRA_REFERRER)).isTrue() - assertThat(fillIn?.getParcelableExtra(EXTRA_REFERRER, Uri::class.java)).isEqualTo(referrer) + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> + + val fillIn = result.value.getReferrerFillInIntent() + assertThat(fillIn.hasExtra(EXTRA_REFERRER)).isTrue() + assertThat(fillIn.getParcelableExtra(EXTRA_REFERRER, Uri::class.java)).isEqualTo(referrer) } @Test @@ -97,45 +97,59 @@ class ChooserRequestTest { val referrer = Uri.parse("http://example.com") val intent = Intent().putExtras(bundleOf(EXTRA_INTENT to Intent(ACTION_SEND))) - val launch = createActivityModel(targetIntent = intent, referrer = referrer) + val model = createActivityModel(targetIntent = intent, referrer = referrer) + + val result = readChooserRequest(model, fakeChooserServiceFlags) - val result = readChooserRequest(launch, fakeChooserServiceFlags) + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> - assertThat(result.value?.referrerPackage).isNull() + assertThat(result.value.referrerPackage).isNull() } @Test fun referrerPackage_fromAppReferrer() { val referrer = Uri.parse("android-app://example.com") - val launch = createActivityModel(targetIntent = Intent(ACTION_SEND), referrer) + val model = createActivityModel(targetIntent = Intent(ACTION_SEND), referrer) + + model.intent.putExtras(bundleOf(EXTRA_REFERRER to referrer)) - launch.intent.putExtras(bundleOf(EXTRA_REFERRER to referrer)) + val result = readChooserRequest(model, fakeChooserServiceFlags) - val result = readChooserRequest(launch, fakeChooserServiceFlags) + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> - assertThat(result.value?.referrerPackage).isEqualTo(referrer.authority) + assertThat(result.value.referrerPackage).isEqualTo(referrer.authority) } @Test fun payloadIntents_includesTargetThenAdditional() { val intent1 = Intent(ACTION_SEND) val intent2 = Intent(ACTION_SEND_MULTIPLE) - val launch = createActivityModel(targetIntent = intent1, additionalIntents = listOf(intent2)) - val result = readChooserRequest(launch, fakeChooserServiceFlags) + val model = createActivityModel( + targetIntent = intent1, + additionalIntents = listOf(intent2) + ) + + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> - assertThat(result.value?.payloadIntents).containsExactly(intent1, intent2) + assertThat(result.value.payloadIntents).containsExactly(intent1, intent2) } @Test fun testRequest_withOnlyRequiredValues() { val intent = Intent().putExtras(bundleOf(EXTRA_INTENT to Intent(ACTION_SEND))) - val launch = createActivityModel(targetIntent = intent) - val result = readChooserRequest(launch, fakeChooserServiceFlags) + val model = createActivityModel(targetIntent = intent) - assertThat(result).value().isNotNull() - val value: ChooserRequest = result.getOrThrow() - assertThat(value.launchedFromPackage).isEqualTo(launch.launchedFromPackage) - assertThat(result).findings().isEmpty() + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> + + assertThat(result.value.launchedFromPackage).isEqualTo(model.launchedFromPackage) } @Test @@ -143,18 +157,19 @@ class ChooserRequestTest { fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true) val uri = Uri.parse("content://org.pkg/path") val position = 10 - val launch = + val model = createActivityModel(targetIntent = Intent(ACTION_SEND)).apply { intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, uri) intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, position) } - val result = readChooserRequest(launch, fakeChooserServiceFlags) - assertThat(result).value().isNotNull() - val value: ChooserRequest = result.getOrThrow() - assertThat(value.additionalContentUri).isEqualTo(uri) - assertThat(value.focusedItemPosition).isEqualTo(position) - assertThat(result).findings().isEmpty() + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> + + assertThat(result.value.additionalContentUri).isEqualTo(uri) + assertThat(result.value.focusedItemPosition).isEqualTo(position) } @Test @@ -162,46 +177,51 @@ class ChooserRequestTest { fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, false) val uri = Uri.parse("content://org.pkg/path") val position = 10 - val launch = + val model = createActivityModel(targetIntent = Intent(ACTION_SEND)).apply { intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, uri) intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, position) } - val result = readChooserRequest(launch, fakeChooserServiceFlags) + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> - assertThat(result).value().isNotNull() - val value: ChooserRequest = result.getOrThrow() - assertThat(value.additionalContentUri).isNull() - assertThat(value.focusedItemPosition).isEqualTo(0) - assertThat(result).findings().isEmpty() + assertThat(result.value.additionalContentUri).isNull() + assertThat(result.value.focusedItemPosition).isEqualTo(0) + assertThat(result.warnings).isEmpty() } @Test fun testRequest_actionSendWithInvalidAdditionalContentUri() { fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true) - val launch = + val model = createActivityModel(targetIntent = Intent(ACTION_SEND)).apply { - intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, "content://org.pkg/path") - intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, "1") + intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, "__invalid__") + intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, "__invalid__") } - val result = readChooserRequest(launch, fakeChooserServiceFlags) - assertThat(result).value().isNotNull() - val value: ChooserRequest = result.getOrThrow() - assertThat(value.additionalContentUri).isNull() - assertThat(value.focusedItemPosition).isEqualTo(0) + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> + + assertThat(result.value.additionalContentUri).isNull() + assertThat(result.value.focusedItemPosition).isEqualTo(0) } @Test fun testRequest_actionSendWithoutAdditionalContentUri() { fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true) - val launch = createActivityModel(targetIntent = Intent(ACTION_SEND)) - val result = readChooserRequest(launch, fakeChooserServiceFlags) + val model = createActivityModel(targetIntent = Intent(ACTION_SEND)) + + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> - assertThat(result).value().isNotNull() - val value: ChooserRequest = result.getOrThrow() - assertThat(value.additionalContentUri).isNull() - assertThat(value.focusedItemPosition).isEqualTo(0) + assertThat(result.value.additionalContentUri).isNull() + assertThat(result.value.focusedItemPosition).isEqualTo(0) } @Test @@ -209,52 +229,53 @@ class ChooserRequestTest { fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true) val uri = Uri.parse("content://org.pkg/path") val position = 10 - val launch = - createActivityModel(targetIntent = Intent(ACTION_VIEW)).apply { + val model = createActivityModel(targetIntent = Intent(ACTION_VIEW)).apply { intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, uri) intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, position) } - val result = readChooserRequest(launch, fakeChooserServiceFlags) - assertThat(result).value().isNotNull() - val value: ChooserRequest = result.getOrThrow() - assertThat(value.additionalContentUri).isNull() - assertThat(value.focusedItemPosition).isEqualTo(0) - assertThat(result).findings().isEmpty() + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> + + assertThat(result.value.additionalContentUri).isNull() + assertThat(result.value.focusedItemPosition).isEqualTo(0) + assertThat(result.warnings).isEmpty() } @Test fun testAlbumType() { fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_ALBUM_TEXT, true) - val launch = createActivityModel(Intent(ACTION_SEND)) - launch.intent.putExtra( + val model = createActivityModel(Intent(ACTION_SEND)) + model.intent.putExtra( Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT, Intent.CHOOSER_CONTENT_TYPE_ALBUM ) - val result = readChooserRequest(launch, fakeChooserServiceFlags) + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> - val value: ChooserRequest = result.getOrThrow() - assertThat(value.contentTypeHint).isEqualTo(ContentTypeHint.ALBUM) - assertThat(result).findings().isEmpty() + assertThat(result.value.contentTypeHint).isEqualTo(ContentTypeHint.ALBUM) + assertThat(result.warnings).isEmpty() } @Test fun metadataText_whenFlagFalse_isNull() { - // Arrange fakeChooserServiceFlags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, false) val metadataText: CharSequence = "Test metadata text" - val launch = - createActivityModel(targetIntent = Intent()).apply { + val model = createActivityModel(targetIntent = Intent()).apply { intent.putExtra(Intent.EXTRA_METADATA_TEXT, metadataText) } - // Act - val result = readChooserRequest(launch, fakeChooserServiceFlags) + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> - // Assert - assertThat(result).value().isNotNull() - assertThat(result.value?.metadataText).isNull() + assertThat(result.value.metadataText).isNull() } @Test @@ -262,16 +283,15 @@ class ChooserRequestTest { // Arrange fakeChooserServiceFlags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, true) val metadataText: CharSequence = "Test metadata text" - val launch = - createActivityModel(targetIntent = Intent()).apply { + val model = createActivityModel(targetIntent = Intent()).apply { intent.putExtra(Intent.EXTRA_METADATA_TEXT, metadataText) } - // Act - val result = readChooserRequest(launch, fakeChooserServiceFlags) + val result = readChooserRequest(model, fakeChooserServiceFlags) + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ChooserRequest> - // Assert - assertThat(result).value().isNotNull() - assertThat(result.value?.metadataText).isEqualTo(metadataText) + assertThat(result.value.metadataText).isEqualTo(metadataText) } } diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt index cc9b9a77..6f1ed853 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt @@ -24,9 +24,11 @@ import androidx.core.os.bundleOf import com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK import com.android.intentresolver.v2.shared.model.Profile.Type.WORK import com.android.intentresolver.v2.ui.model.ActivityModel +import com.android.intentresolver.v2.ui.model.ChooserRequest import com.android.intentresolver.v2.ui.model.ResolverRequest +import com.android.intentresolver.v2.validation.Invalid import com.android.intentresolver.v2.validation.UncaughtException -import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.android.intentresolver.v2.validation.Valid import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Test @@ -51,13 +53,15 @@ class ResolverRequestTest { val activity = createActivityModel(intent) val result = readResolverRequest(activity) - assertThat(result).isSuccess() - assertThat(result).findings().isEmpty() - val value: ResolverRequest = result.getOrThrow() - assertThat(value.intent.filterEquals(activity.intent)).isTrue() - assertThat(value.callingUser).isNull() - assertThat(value.selectedProfile).isNull() + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ResolverRequest> + + assertThat(result.warnings).isEmpty() + + assertThat(result.value.intent.filterEquals(activity.intent)).isTrue() + assertThat(result.value.callingUser).isNull() + assertThat(result.value.selectedProfile).isNull() } @Test @@ -72,9 +76,11 @@ class ResolverRequestTest { val result = readResolverRequest(activity) - assertThat(result).isFailure() + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<ResolverRequest> + assertWithMessage("the first finding") - .that(result.findings.firstOrNull()) + .that(result.errors.firstOrNull()) .isInstanceOf(UncaughtException::class.java) } @@ -89,9 +95,12 @@ class ResolverRequestTest { val result = readResolverRequest(activity) + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ResolverRequest> + // Assert that payloadIntents does NOT include EXTRA_ALTERNATE_INTENTS // that is only supported for Chooser and should be not be added here. - assertThat(result.value?.payloadIntents).containsExactly(intent1) + assertThat(result.value.payloadIntents).containsExactly(intent1) } @Test @@ -109,12 +118,12 @@ class ResolverRequestTest { val result = readResolverRequest(activity) - assertThat(result).value().isNotNull() - val value: ResolverRequest = result.getOrThrow() + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<ResolverRequest> - assertThat(value.intent.filterEquals(activity.intent)).isTrue() - assertThat(value.isAudioCaptureDevice).isTrue() - assertThat(value.callingUser).isEqualTo(UserHandle.of(123)) - assertThat(value.selectedProfile).isEqualTo(WORK) + assertThat(result.value.intent.filterEquals(activity.intent)).isTrue() + assertThat(result.value.isAudioCaptureDevice).isTrue() + assertThat(result.value.callingUser).isEqualTo(UserHandle.of(123)) + assertThat(result.value.selectedProfile).isEqualTo(WORK) } } diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt index 43fb448c..dbaa7c4e 100644 --- a/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt @@ -1,6 +1,5 @@ package com.android.intentresolver.v2.validation -import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat import com.android.intentresolver.v2.validation.types.value import com.google.common.truth.Truth.assertThat import org.junit.Assert.fail @@ -16,8 +15,12 @@ class ValidationTest { val required: Int = required(value<Int>("key")) "return value: $required" } - assertThat(result).value().isEqualTo("return value: 1") - assertThat(result).findings().isEmpty() + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<String> + + assertThat(result.value).isEqualTo("return value: 1") + assertThat(result.warnings).isEmpty() } /** Test reporting of absent required values. */ @@ -29,9 +32,12 @@ class ValidationTest { fail("'required' should have thrown an exception") "return value" } - assertThat(result).isFailure() - assertThat(result).findings().containsExactly( - RequiredValueMissing("key", Int::class)) + + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<String> + + assertThat(result.errors).containsExactly( + NoValue("key", Importance.CRITICAL, Int::class)) } /** Test optional values are ignored when absent. */ @@ -42,20 +48,28 @@ class ValidationTest { val optional: Int? = optional(value<Int>("key")) "return value: $optional" } - assertThat(result).value().isEqualTo("return value: 1") - assertThat(result).findings().isEmpty() + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<String> + + assertThat(result.value).isEqualTo("return value: 1") + assertThat(result.warnings).isEmpty() } /** Test optional values are ignored when absent. */ @Test fun optional_valueAbsent() { - val result: ValidationResult<String?> = + val result: ValidationResult<String> = validateFrom({ null }) { val optional: String? = optional(value<String>("key")) "return value: $optional" } - assertThat(result).isSuccess() - assertThat(result).findings().isEmpty() + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<String> + + assertThat(result.value).isEqualTo("return value: null") + assertThat(result.warnings).isEmpty() } /** Test reporting of ignored values. */ @@ -66,9 +80,12 @@ class ValidationTest { ignored(value<Int>("key"), "no longer supported") "result value" } - assertThat(result).value().isEqualTo("result value") - assertThat(result) - .findings() + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<String> + + assertThat(result.value).isEqualTo("result value") + assertThat(result.warnings) .containsExactly(IgnoredValue("key", "no longer supported")) } @@ -80,8 +97,11 @@ class ValidationTest { ignored(value<Int>("key"), "ignored when option foo is set") "result value" } - assertThat(result).value().isEqualTo("result value") - assertThat(result).findings().isEmpty() + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<String> + + assertThat(result.value).isEqualTo("result value") + assertThat(result.warnings).isEmpty() } /** Test handling of exceptions in the validation function. */ @@ -91,9 +111,12 @@ class ValidationTest { validateFrom({ null }) { error("something") } - assertThat(result).isFailure() - val findingTypes = result.findings.map { it::class } - assertThat(findingTypes.first()).isEqualTo(UncaughtException::class) + + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<String> + + val errorType = result.errors.map { it::class }.first() + assertThat(errorType).isEqualTo(UncaughtException::class) } } diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt index ad230488..03429f4c 100644 --- a/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt @@ -7,8 +7,9 @@ import androidx.core.net.toUri import androidx.test.ext.truth.content.IntentSubject.assertThat import com.android.intentresolver.v2.validation.Importance.CRITICAL import com.android.intentresolver.v2.validation.Importance.WARNING -import com.android.intentresolver.v2.validation.RequiredValueMissing -import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue +import com.android.intentresolver.v2.validation.Valid import com.android.intentresolver.v2.validation.ValueIsWrongType import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -22,7 +23,9 @@ class IntentOrUriTest { val values = mapOf("key" to Intent("GO")) val result = keyValidator.validate(values::get, CRITICAL) - assertThat(result).findings().isEmpty() + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<Intent> assertThat(result.value).hasAction("GO") } @@ -33,7 +36,9 @@ class IntentOrUriTest { val values = mapOf("key" to Intent("GO").toUri(URI_INTENT_SCHEME).toUri()) val result = keyValidator.validate(values::get, CRITICAL) - assertThat(result).findings().isEmpty() + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<Intent> assertThat(result.value).hasAction("GO") } @@ -44,8 +49,11 @@ class IntentOrUriTest { val result = keyValidator.validate({ null }, CRITICAL) - assertThat(result).value().isNull() - assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class)) + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<Intent> + + assertThat(result.errors) + .containsExactly(NoValue("key", CRITICAL, Intent::class)) } /** Check validation passes when value is null and importance is [WARNING] (optional). */ @@ -55,8 +63,9 @@ class IntentOrUriTest { val result = keyValidator.validate(source = { null }, WARNING) - assertThat(result).findings().isEmpty() - assertThat(result.value).isNull() + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<List<Intent>> + assertThat(result.errors).isEmpty() } /** @@ -69,9 +78,10 @@ class IntentOrUriTest { val result = keyValidator.validate(values::get, CRITICAL) - assertThat(result).value().isNull() - assertThat(result) - .findings() + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<Intent> + + assertThat(result.errors) .containsExactly( ValueIsWrongType( "key", @@ -92,9 +102,10 @@ class IntentOrUriTest { val result = keyValidator.validate(values::get, WARNING) - assertThat(result).value().isNull() - assertThat(result) - .findings() + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<Intent> + + assertThat(result.errors) .containsExactly( ValueIsWrongType( "key", diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt index d4dca01b..637873ea 100644 --- a/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt @@ -4,8 +4,9 @@ import android.content.Intent import android.graphics.Point import com.android.intentresolver.v2.validation.Importance.CRITICAL import com.android.intentresolver.v2.validation.Importance.WARNING -import com.android.intentresolver.v2.validation.RequiredValueMissing -import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue +import com.android.intentresolver.v2.validation.Valid import com.android.intentresolver.v2.validation.ValueIsWrongType import com.android.intentresolver.v2.validation.WrongElementType import com.google.common.truth.Truth.assertThat @@ -21,7 +22,8 @@ class ParceledArrayTest { val result = keyValidator.validate(values::get, CRITICAL) - assertThat(result).findings().isEmpty() + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<List<String>> assertThat(result.value).containsExactly("String") } @@ -33,9 +35,10 @@ class ParceledArrayTest { val result = keyValidator.validate(values::get, CRITICAL) - assertThat(result).value().isNull() - assertThat(result) - .findings() + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<List<Intent>> + + assertThat(result.errors) .containsExactly( // TODO: report with a new class `WrongElementType` to improve clarity WrongElementType( @@ -55,8 +58,10 @@ class ParceledArrayTest { val result = keyValidator.validate(source = { null }, CRITICAL) - assertThat(result).value().isNull() - assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class)) + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<List<Intent>> + + assertThat(result.errors).containsExactly(NoValue("key", CRITICAL, Intent::class)) } /** Check validation passes when value is null and importance is [WARNING] (optional). */ @@ -66,8 +71,10 @@ class ParceledArrayTest { val result = keyValidator.validate(source = { null }, WARNING) - assertThat(result).findings().isEmpty() - assertThat(result.value).isNull() + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<List<Intent>> + + assertThat(result.errors).isEmpty() } /** Check correct failure result when the array value itself is the wrong type. */ @@ -78,9 +85,10 @@ class ParceledArrayTest { val result = keyValidator.validate(values::get, CRITICAL) - assertThat(result).value().isNull() - assertThat(result) - .findings() + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<List<Intent>> + + assertThat(result.errors) .containsExactly( ValueIsWrongType( "key", diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt index 13bb4b33..93d76d46 100644 --- a/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt @@ -1,10 +1,13 @@ package com.android.intentresolver.v2.validation.types import com.android.intentresolver.v2.validation.Importance.CRITICAL -import com.android.intentresolver.v2.validation.RequiredValueMissing -import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.android.intentresolver.v2.validation.Importance.WARNING +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.NoValue +import com.android.intentresolver.v2.validation.Valid import com.android.intentresolver.v2.validation.ValueIsWrongType import org.junit.Test +import com.google.common.truth.Truth.assertThat class SimpleValueTest { @@ -15,8 +18,11 @@ class SimpleValueTest { val values = mapOf("key" to Math.PI) val result = keyValidator.validate(values::get, CRITICAL) - assertThat(result).findings().isEmpty() - assertThat(result).value().isEqualTo(Math.PI) + + + assertThat(result).isInstanceOf(Valid::class.java) + result as Valid<Double> + assertThat(result.value).isEqualTo(Math.PI) } /** Test for validation success when the value is present and the correct type. */ @@ -26,17 +32,17 @@ class SimpleValueTest { val values = mapOf("key" to "Apple Pie") val result = keyValidator.validate(values::get, CRITICAL) - assertThat(result).value().isNull() - assertThat(result) - .findings() - .containsExactly( - ValueIsWrongType( - "key", - importance = CRITICAL, - actualType = String::class, - allowedTypes = listOf(Double::class) - ) + + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<Double> + assertThat(result.errors).containsExactly( + ValueIsWrongType( + "key", + importance = CRITICAL, + actualType = String::class, + allowedTypes = listOf(Double::class) ) + ) } /** Test the failure result when the value is missing. */ @@ -46,7 +52,26 @@ class SimpleValueTest { val result = keyValidator.validate(source = { null }, CRITICAL) - assertThat(result).value().isNull() - assertThat(result).findings().containsExactly(RequiredValueMissing("key", Double::class)) + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<Double> + + assertThat(result.errors).containsExactly(NoValue("key", CRITICAL, Double::class)) + } + + + /** Test the failure result when the value is missing. */ + @Test + fun optional() { + val keyValidator = SimpleValue("key", expected = Double::class) + + val result = keyValidator.validate(source = { null }, WARNING) + + assertThat(result).isInstanceOf(Invalid::class.java) + result as Invalid<Double> + + // Note: As single optional validation result, the return must be Invalid + // when there is no value to return, but no errors will be reported because + // an optional value cannot be "missing". + assertThat(result.errors).isEmpty() } } |