blob: 851f343e9065e5dae1109975379ed6dd327ec39e [file] [log] [blame]
/*
* Copyright 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 androidx.appactions.interaction.capabilities.core.impl
import android.util.SizeF
import androidx.appactions.interaction.capabilities.core.ActionExecutor
import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync
import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync.Companion.toActionExecutorAsync
import androidx.appactions.interaction.capabilities.core.ExecutionResult
import androidx.appactions.interaction.capabilities.core.HostProperties
import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.Entity
import androidx.appactions.interaction.capabilities.core.properties.StringValue
import androidx.appactions.interaction.capabilities.core.properties.ParamProperty
import androidx.appactions.interaction.capabilities.testing.internal.ArgumentUtils
import androidx.appactions.interaction.capabilities.testing.internal.FakeCallbackInternal
import androidx.appactions.interaction.capabilities.testing.internal.TestingUtils.CB_TIMEOUT
import androidx.appactions.interaction.capabilities.testing.internal.TestingUtils.BLOCKING_TIMEOUT
import androidx.appactions.interaction.capabilities.core.testing.spec.Arguments
import androidx.appactions.interaction.capabilities.core.testing.spec.Output
import androidx.appactions.interaction.capabilities.core.testing.spec.Property
import androidx.appactions.interaction.proto.FulfillmentResponse
import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput
import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput.OutputValue
import androidx.appactions.interaction.proto.ParamValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class SingleTurnCapabilityTest {
private val hostProperties =
HostProperties.Builder().setMaxHostSizeDp(SizeF(300f, 500f)).build()
private val fakeSessionId = "fakeSessionId"
@Test
fun oneShotCapability_successWithOutput() {
val actionExecutor =
ActionExecutor<Arguments, Output> {
ExecutionResult.Builder<Output>()
.setOutput(
Output.builder().setOptionalStringField("stringOutput").build(),
)
.build()
}
val capability =
SingleTurnCapabilityImpl(
id = "capabilityId",
actionSpec = ACTION_SPEC,
property =
Property.newBuilder()
.setRequiredEntityField(
ParamProperty.Builder<Entity>().build(),
)
.setOptionalStringField(
ParamProperty.Builder<StringValue>().setProhibited(true).build(),
)
.build(),
actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
)
val capabilitySession = capability.createSession(fakeSessionId, hostProperties)
assertThat(capabilitySession.sessionId).isEqualTo(fakeSessionId)
val callbackInternal = FakeCallbackInternal(CB_TIMEOUT)
capabilitySession.execute(
ArgumentUtils.buildArgs(
mapOf(
"optionalString" to
ParamValue.newBuilder().setIdentifier("string argument value").build(),
),
),
callbackInternal,
)
val response = callbackInternal.receiveResponse()
assertThat(response.fulfillmentResponse).isNotNull()
assertThat(response.fulfillmentResponse)
.isEqualTo(
FulfillmentResponse.newBuilder()
.setExecutionOutput(
StructuredOutput.newBuilder()
.addOutputValues(
OutputValue.newBuilder()
.setName("optionalStringOutput")
.addValues(
ParamValue.newBuilder()
.setStringValue("stringOutput")
.build(),
)
.build(),
)
.build(),
)
.build(),
)
}
@Test
fun oneShotCapability_failure() {
val actionExecutor = ActionExecutor<Arguments, Output> { throw IllegalStateException("") }
val capability =
SingleTurnCapabilityImpl(
id = "capabilityId",
actionSpec = ACTION_SPEC,
property =
Property.newBuilder()
.setRequiredEntityField(
ParamProperty.Builder<Entity>().build(),
)
.setOptionalStringField(
ParamProperty.Builder<StringValue>().setProhibited(true).build(),
)
.build(),
actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
)
val capabilitySession = capability.createSession(fakeSessionId, hostProperties)
val callbackInternal = FakeCallbackInternal(CB_TIMEOUT)
capabilitySession.execute(
ArgumentUtils.buildArgs(
mapOf(
"optionalString" to
ParamValue.newBuilder().setIdentifier("string argument value").build(),
),
),
callbackInternal,
)
val response = callbackInternal.receiveResponse()
assertThat(response.errorStatus).isNotNull()
assertThat(response.errorStatus).isEqualTo(ErrorStatusInternal.CANCELLED)
}
@Test
fun oneShotSession_uiHandle_withActionExecutor() {
val actionExecutor =
ActionExecutor<Arguments, Output> { ExecutionResult.Builder<Output>().build() }
val capability =
SingleTurnCapabilityImpl(
id = "capabilityId",
actionSpec = ACTION_SPEC,
property =
Property.newBuilder()
.setRequiredEntityField(
ParamProperty.Builder<Entity>().build(),
)
.build(),
actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
)
val session = capability.createSession(fakeSessionId, hostProperties)
assertThat(session.uiHandle).isSameInstanceAs(actionExecutor)
}
@Test
fun oneShotSession_uiHandle_withActionExecutorAsync() {
val actionExecutorAsync =
ActionExecutorAsync<Arguments, Output> {
Futures.immediateFuture(ExecutionResult.Builder<Output>().build())
}
val capability =
SingleTurnCapabilityImpl(
id = "capabilityId",
actionSpec = ACTION_SPEC,
property =
Property.newBuilder()
.setRequiredEntityField(
ParamProperty.Builder<Entity>().build(),
)
.build(),
actionExecutorAsync = actionExecutorAsync,
)
val session = capability.createSession(fakeSessionId, hostProperties)
assertThat(session.uiHandle).isSameInstanceAs(actionExecutorAsync)
}
@Ignore // b/277121577
@Test
fun multipleSessions_sequentialExecution(): Unit = runBlocking {
val executionResultChannel = Channel<ExecutionResult<Output>>()
val argumentChannel = Channel<Arguments>()
val actionExecutor = ActionExecutor<Arguments, Output> {
argumentChannel.send(it)
executionResultChannel.receive()
}
val capability = SingleTurnCapabilityImpl(
id = "capabilityId",
actionSpec = ACTION_SPEC,
property = Property.newBuilder().setRequiredEntityField(
ParamProperty.Builder<Entity>().build(),
).build(),
actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
)
val session1 = capability.createSession("session1", hostProperties)
val session2 = capability.createSession("session2", hostProperties)
val callbackInternal1 = FakeCallbackInternal(CB_TIMEOUT)
val callbackInternal2 = FakeCallbackInternal(CB_TIMEOUT)
session1.execute(
ArgumentUtils.buildArgs(
mapOf(
"optionalString" to
ParamValue.newBuilder().setIdentifier("string value 1").build(),
),
),
callbackInternal1,
)
session2.execute(
ArgumentUtils.buildArgs(
mapOf(
"optionalString" to
ParamValue.newBuilder().setIdentifier("string value 2").build(),
),
),
callbackInternal2,
)
// verify ActionExecutor receives 1st request.
assertThat(argumentChannel.receive()).isEqualTo(
Arguments.newBuilder().setOptionalStringField("string value 1").build(),
)
// verify the 2nd request cannot be received due to mutex.
assertThat(withTimeoutOrNull(BLOCKING_TIMEOUT) { argumentChannel.receive() }).isNull()
// unblock first request handling.
executionResultChannel.send(ExecutionResult.Builder<Output>().build())
assertThat(callbackInternal1.receiveResponse().fulfillmentResponse).isEqualTo(
FulfillmentResponse.getDefaultInstance(),
)
assertThat(argumentChannel.receive()).isEqualTo(
Arguments.newBuilder().setOptionalStringField("string value 2").build(),
)
executionResultChannel.send(ExecutionResult.Builder<Output>().build())
assertThat(callbackInternal2.receiveResponse().fulfillmentResponse).isEqualTo(
FulfillmentResponse.getDefaultInstance(),
)
}
companion object {
val ACTION_SPEC: ActionSpec<Property, Arguments, Output> =
ActionSpecBuilder.ofCapabilityNamed(
"actions.intent.TEST",
)
.setDescriptor(Property::class.java)
.setArguments(Arguments::class.java, Arguments::newBuilder)
.setOutput(Output::class.java)
.bindOptionalParameter(
"optionalString",
Property::optionalStringField,
Arguments.Builder::setOptionalStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
.bindOptionalOutput(
"optionalStringOutput",
Output::optionalStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER::toParamValue,
)
.build()
}
}