summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--java/src/com/android/intentresolver/v2/validation/Findings.kt113
-rw-r--r--java/src/com/android/intentresolver/v2/validation/Validation.kt129
-rw-r--r--java/src/com/android/intentresolver/v2/validation/ValidationResult.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt59
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt83
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt54
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/Validators.kt45
-rw-r--r--tests/shared/Android.bp1
-rw-r--r--tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt22
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt99
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt107
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt93
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt52
13 files changed, 896 insertions, 0 deletions
diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt
new file mode 100644
index 00000000..9a3cc9c7
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/Findings.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.v2.validation
+
+import android.util.Log
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+import kotlin.reflect.KClass
+
+sealed interface Finding {
+ val importance: Importance
+ val message: String
+}
+
+enum class Importance {
+ CRITICAL,
+ WARNING,
+}
+
+val Finding.logcatPriority
+ get() =
+ when (importance) {
+ CRITICAL -> Log.ERROR
+ else -> Log.WARN
+ }
+
+private fun formatMessage(key: String? = null, msg: String) = buildString {
+ key?.also { append("['$key']: ") }
+ append(msg)
+}
+
+data class IgnoredValue(
+ val key: String,
+ val reason: String,
+) : Finding {
+ override val importance = WARNING
+
+ override val message: String
+ get() = formatMessage(key, "Ignored. $reason")
+}
+
+data class RequiredValueMissing(
+ val key: String,
+ 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"
+ )
+}
+
+data class WrongElementType(
+ val key: String,
+ override val importance: Importance,
+ val container: KClass<*>,
+ val actualType: KClass<*>,
+ val expectedType: KClass<*>
+) : Finding {
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "${container.simpleName} expected with elements of " +
+ "${expectedType.simpleName} " +
+ "but found ${actualType.simpleName} values instead"
+ )
+}
+
+data class ValueIsWrongType(
+ val key: String,
+ override val importance: Importance,
+ val actualType: KClass<*>,
+ val allowedTypes: List<KClass<*>>,
+) : Finding {
+
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "expected value of ${allowedTypes.map(KClass<*>::simpleName)} " +
+ "but was ${actualType.simpleName}"
+ )
+}
+
+data class UncaughtException(val thrown: Throwable, val key: String? = null) : Finding {
+ override val importance: Importance
+ get() = CRITICAL
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "An unhandled exception was caught during validation: " +
+ thrown.stackTraceToString()
+ )
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt
new file mode 100644
index 00000000..46939602
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/Validation.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.v2.validation
+
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+
+/**
+ * Provides a mechanism for validating a result from a set of properties.
+ *
+ * The results of validation are provided as [findings].
+ */
+interface Validation {
+ val findings: List<Finding>
+
+ /**
+ * Require a valid property.
+ *
+ * If [property] is not valid, this [Validation] will be immediately completed as [Invalid].
+ *
+ * @param property the required property
+ * @return a valid **T**
+ */
+ @Throws(InvalidResultError::class) fun <T> required(property: Validator<T>): T
+
+ /**
+ * Request an optional value for a property.
+ *
+ * If [property] is not valid, this [Validation] will be immediately completed as [Invalid].
+ *
+ * @param property the required property
+ * @return a valid **T**
+ */
+ fun <T> optional(property: Validator<T>): T?
+
+ /**
+ * Report a property as __ignored__.
+ *
+ * The presence of any value will report a warning citing [reason].
+ */
+ fun <T> ignored(property: Validator<T>, reason: String)
+}
+
+/** Performs validation for a specific key -> value pair. */
+interface Validator<T> {
+ val key: String
+
+ /**
+ * Performs validation on a specific value from [source].
+ *
+ * @param source a source for reading the property value. Values are intentionally untyped
+ * (Any?) to avoid upstream code from making type assertions through type inference. Types are
+ * asserted later using a [Validator].
+ * @param importance the importance of any findings
+ */
+ fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T>
+}
+
+internal class InvalidResultError internal constructor() : Error()
+
+/**
+ * Perform a number of validations on the source, assembling and returning a Result.
+ *
+ * When an exception is thrown by [validate], it is caught here. In response, a failed
+ * [ValidationResult] is returned containing a [CRITICAL] [Finding] for the exception.
+ *
+ * @param validate perform validations and return a [ValidationResult]
+ */
+fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): ValidationResult<T> {
+ val validation = ValidationImpl(source)
+ return runCatching { validate(validation) }
+ .fold(
+ onSuccess = { result -> Valid(result, validation.findings) },
+ onFailure = {
+ when (it) {
+ // A validator has interrupted validation. Return the findings.
+ is InvalidResultError -> Invalid(validation.findings)
+
+ // Some other exception was thrown from [validate],
+ else -> Invalid(findings = listOf(UncaughtException(it)))
+ }
+ }
+ )
+}
+
+private class ValidationImpl(val source: (String) -> Any?) : Validation {
+ override val findings = mutableListOf<Finding>()
+
+ override fun <T> optional(property: Validator<T>): T? = validate(property, WARNING)
+
+ override fun <T> required(property: Validator<T>): T {
+ return validate(property, CRITICAL) ?: throw InvalidResultError()
+ }
+
+ 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.
+ findings += IgnoredValue(property.key, reason)
+ }
+ }
+
+ private fun <T> validate(property: Validator<T>, importance: Importance): T? {
+ return runCatching { property.validate(source, importance) }
+ .fold(
+ onSuccess = { result ->
+ findings += result.findings
+ result.value
+ },
+ onFailure = {
+ findings += UncaughtException(it, property.key)
+ null
+ }
+ )
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
new file mode 100644
index 00000000..092cabe8
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.intentresolver.v2.validation
+
+import android.util.Log
+
+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 <T> reportToLogcat(tag: String) {
+ findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) }
+ }
+}
+
+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
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt
new file mode 100644
index 00000000..3cefeb15
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 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.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+
+class IntentOrUri(override val key: String) : Validator<Intent> {
+
+ override fun validate(
+ source: (String) -> Any?,
+ importance: Importance
+ ): ValidationResult<Intent> {
+
+ return when (val value = source(key)) {
+ // An intent, return it.
+ is Intent -> Valid(value)
+
+ // A Uri was supplied.
+ // Unfortunately, converting Uri -> Intent requires a toString().
+ is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME))
+
+ // No value present.
+ null -> createResult(importance, RequiredValueMissing(key, Intent::class))
+
+ // Some other type.
+ else -> {
+ return createResult(
+ importance,
+ ValueIsWrongType(
+ key,
+ importance,
+ actualType = value::class,
+ allowedTypes = listOf(Intent::class, Uri::class)
+ )
+ )
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt
new file mode 100644
index 00000000..c6c4abba
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 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.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+import com.android.intentresolver.v2.validation.WrongElementType
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+
+class ParceledArray<T : Any>(
+ override val key: String,
+ private val elementType: KClass<T>,
+) : Validator<List<T>> {
+
+ override fun validate(
+ source: (String) -> Any?,
+ importance: Importance
+ ): ValidationResult<List<T>> {
+
+ return when (val value: Any? = source(key)) {
+ // No value present.
+ null -> createResult(importance, RequiredValueMissing(key, 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>.
+
+ // To handle this safely, treat as Array<*>, assert contents of the expected
+ // parcelable type, and return as a list.
+
+ is Array<*> -> {
+ val invalid = value.filterNotNull().firstOrNull { !elementType.isInstance(it) }
+ when (invalid) {
+ // No invalid elements, result is ok.
+ null -> Valid(value.map { elementType.cast(it) })
+
+ // At least one incorrect element type found.
+ else ->
+ createResult(
+ importance,
+ WrongElementType(
+ key,
+ importance,
+ actualType = invalid::class,
+ container = Array::class,
+ expectedType = elementType
+ )
+ )
+ }
+ }
+
+ // The value is not an Array at all.
+ else ->
+ createResult(
+ importance,
+ ValueIsWrongType(
+ key,
+ importance,
+ actualType = value::class,
+ allowedTypes = listOf(elementType)
+ )
+ )
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt
new file mode 100644
index 00000000..3287b84b
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 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.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+
+class SimpleValue<T : Any>(
+ override val key: String,
+ private val expected: KClass<T>,
+) : Validator<T> {
+
+ override fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> {
+ val value: Any? = source(key)
+ return when {
+ // The value is present and of the expected type.
+ expected.isInstance(value) -> return Valid(expected.cast(value))
+
+ // No value is present.
+ value == null -> createResult(importance, RequiredValueMissing(key, expected))
+
+ // The value is some other type.
+ else ->
+ createResult(
+ importance,
+ 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
new file mode 100644
index 00000000..4e6e5dff
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 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> {
+ return SimpleValue(key, T::class)
+}
+
+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/Android.bp b/tests/shared/Android.bp
index dbd68b12..55188ee3 100644
--- a/tests/shared/Android.bp
+++ b/tests/shared/Android.bp
@@ -32,5 +32,6 @@ java_library {
"hamcrest",
"IntentResolver-core",
"mockito-target-minus-junit4",
+ "truth"
],
}
diff --git a/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt
new file mode 100644
index 00000000..1ff0ce8e
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt
@@ -0,0 +1,22 @@
+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/validation/ValidationTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt
new file mode 100644
index 00000000..43fb448c
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt
@@ -0,0 +1,99 @@
+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
+import org.junit.Test
+
+class ValidationTest {
+
+ /** Test required values. */
+ @Test
+ fun required_valuePresent() {
+ val result: ValidationResult<String> =
+ validateFrom({ 1 }) {
+ val required: Int = required(value<Int>("key"))
+ "return value: $required"
+ }
+ assertThat(result).value().isEqualTo("return value: 1")
+ assertThat(result).findings().isEmpty()
+ }
+
+ /** Test reporting of absent required values. */
+ @Test
+ fun required_valueAbsent() {
+ val result: ValidationResult<String> =
+ validateFrom({ null }) {
+ required(value<Int>("key"))
+ fail("'required' should have thrown an exception")
+ "return value"
+ }
+ assertThat(result).isFailure()
+ assertThat(result).findings().containsExactly(
+ RequiredValueMissing("key", Int::class))
+ }
+
+ /** Test optional values are ignored when absent. */
+ @Test
+ fun optional_valuePresent() {
+ val result: ValidationResult<String> =
+ validateFrom({ 1 }) {
+ val optional: Int? = optional(value<Int>("key"))
+ "return value: $optional"
+ }
+ assertThat(result).value().isEqualTo("return value: 1")
+ assertThat(result).findings().isEmpty()
+ }
+
+ /** Test optional values are ignored when absent. */
+ @Test
+ fun optional_valueAbsent() {
+ val result: ValidationResult<String?> =
+ validateFrom({ null }) {
+ val optional: String? = optional(value<String>("key"))
+ "return value: $optional"
+ }
+ assertThat(result).isSuccess()
+ assertThat(result).findings().isEmpty()
+ }
+
+ /** Test reporting of ignored values. */
+ @Test
+ fun ignored_valuePresent() {
+ val result: ValidationResult<String> =
+ validateFrom(mapOf("key" to 1)::get) {
+ ignored(value<Int>("key"), "no longer supported")
+ "result value"
+ }
+ assertThat(result).value().isEqualTo("result value")
+ assertThat(result)
+ .findings()
+ .containsExactly(IgnoredValue("key", "no longer supported"))
+ }
+
+ /** Test reporting of ignored values. */
+ @Test
+ fun ignored_valueAbsent() {
+ val result: ValidationResult<String> =
+ validateFrom({ null }) {
+ ignored(value<Int>("key"), "ignored when option foo is set")
+ "result value"
+ }
+ assertThat(result).value().isEqualTo("result value")
+ assertThat(result).findings().isEmpty()
+ }
+
+ /** Test handling of exceptions in the validation function. */
+ @Test
+ fun thrown_exception() {
+ val result: ValidationResult<String> =
+ validateFrom({ null }) {
+ error("something")
+ }
+ assertThat(result).isFailure()
+ val findingTypes = result.findings.map { it::class }
+ assertThat(findingTypes.first()).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
new file mode 100644
index 00000000..ad230488
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt
@@ -0,0 +1,107 @@
+package com.android.intentresolver.v2.validation.types
+
+import android.content.Intent
+import android.content.Intent.URI_INTENT_SCHEME
+import android.net.Uri
+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.ValueIsWrongType
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class IntentOrUriTest {
+
+ /** Test for validation success when the value is an Intent. */
+ @Test
+ fun intent() {
+ val keyValidator = IntentOrUri("key")
+ val values = mapOf("key" to Intent("GO"))
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+ assertThat(result).findings().isEmpty()
+ assertThat(result.value).hasAction("GO")
+ }
+
+ /** Test for validation success when the value is a Uri. */
+ @Test
+ fun uri() {
+ val keyValidator = IntentOrUri("key")
+ 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.value).hasAction("GO")
+ }
+
+ /** Test the failure result when the value is missing. */
+ @Test
+ fun missing() {
+ val keyValidator = IntentOrUri("key")
+
+ val result = keyValidator.validate({ null }, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class))
+ }
+
+ /** Check validation passes when value is null and importance is [WARNING] (optional). */
+ @Test
+ fun optional() {
+ val keyValidator = ParceledArray("key", Intent::class)
+
+ val result = keyValidator.validate(source = { null }, WARNING)
+
+ assertThat(result).findings().isEmpty()
+ assertThat(result.value).isNull()
+ }
+
+ /**
+ * Test for failure result when the value is neither Intent nor Uri, with importance CRITICAL.
+ */
+ @Test
+ fun wrongType_required() {
+ val keyValidator = IntentOrUri("key")
+ val values = mapOf("key" to 1)
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result)
+ .findings()
+ .containsExactly(
+ ValueIsWrongType(
+ "key",
+ importance = CRITICAL,
+ actualType = Int::class,
+ allowedTypes = listOf(Intent::class, Uri::class)
+ )
+ )
+ }
+
+ /**
+ * Test for warnings when the value is neither Intent nor Uri, with importance WARNING.
+ */
+ @Test
+ fun wrongType_optional() {
+ val keyValidator = IntentOrUri("key")
+ val values = mapOf("key" to 1)
+
+ val result = keyValidator.validate(values::get, WARNING)
+
+ assertThat(result).value().isNull()
+ assertThat(result)
+ .findings()
+ .containsExactly(
+ ValueIsWrongType(
+ "key",
+ importance = WARNING,
+ actualType = Int::class,
+ allowedTypes = listOf(Intent::class, Uri::class)
+ )
+ )
+ }
+}
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
new file mode 100644
index 00000000..d4dca01b
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt
@@ -0,0 +1,93 @@
+package com.android.intentresolver.v2.validation.types
+
+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.ValueIsWrongType
+import com.android.intentresolver.v2.validation.WrongElementType
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class ParceledArrayTest {
+
+ /** Check that a array is handled correctly when valid. */
+ @Test
+ fun valid() {
+ val keyValidator = ParceledArray("key", elementType = String::class)
+ val values = mapOf("key" to arrayOf("String"))
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+
+ assertThat(result).findings().isEmpty()
+ assertThat(result.value).containsExactly("String")
+ }
+
+ /** Check correct failure result when an array has the wrong element type. */
+ @Test
+ fun wrongElementType() {
+ val keyValidator = ParceledArray("key", elementType = Intent::class)
+ val values = mapOf("key" to arrayOf(Point()))
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result)
+ .findings()
+ .containsExactly(
+ // TODO: report with a new class `WrongElementType` to improve clarity
+ WrongElementType(
+ "key",
+ importance = CRITICAL,
+ container = Array::class,
+ actualType = Point::class,
+ expectedType = Intent::class
+ )
+ )
+ }
+
+ /** Check correct failure result when an array value is missing. */
+ @Test
+ fun missing() {
+ val keyValidator = ParceledArray("key", Intent::class)
+
+ val result = keyValidator.validate(source = { null }, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class))
+ }
+
+ /** Check validation passes when value is null and importance is [WARNING] (optional). */
+ @Test
+ fun optional() {
+ val keyValidator = ParceledArray("key", Intent::class)
+
+ val result = keyValidator.validate(source = { null }, WARNING)
+
+ assertThat(result).findings().isEmpty()
+ assertThat(result.value).isNull()
+ }
+
+ /** Check correct failure result when the array value itself is the wrong type. */
+ @Test
+ fun wrongType() {
+ val keyValidator = ParceledArray("key", Intent::class)
+ val values = mapOf("key" to 1)
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result)
+ .findings()
+ .containsExactly(
+ ValueIsWrongType(
+ "key",
+ importance = CRITICAL,
+ actualType = Int::class,
+ allowedTypes = listOf(Intent::class)
+ )
+ )
+ }
+}
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
new file mode 100644
index 00000000..13bb4b33
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt
@@ -0,0 +1,52 @@
+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.ValueIsWrongType
+import org.junit.Test
+
+class SimpleValueTest {
+
+ /** Test for validation success when the value is present and the correct type. */
+ @Test
+ fun present() {
+ val keyValidator = SimpleValue("key", expected = Double::class)
+ val values = mapOf("key" to Math.PI)
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+ assertThat(result).findings().isEmpty()
+ assertThat(result).value().isEqualTo(Math.PI)
+ }
+
+ /** Test for validation success when the value is present and the correct type. */
+ @Test
+ fun wrongType() {
+ val keyValidator = SimpleValue("key", expected = Double::class)
+ 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)
+ )
+ )
+ }
+
+ /** Test the failure result when the value is missing. */
+ @Test
+ fun missing() {
+ val keyValidator = SimpleValue("key", expected = Double::class)
+
+ val result = keyValidator.validate(source = { null }, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result).findings().containsExactly(RequiredValueMissing("key", Double::class))
+ }
+}