Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7f25654
Add agent skill for running tests
halilozercan Mar 31, 2026
b522e9a
Translated AllowlistActivity to kotlin
Mar 9, 2026
4419697
Update ExoPlayer to 1.10.0 and remove OptIn(UnstableApi) in SoundFiel…
davejmurphy Mar 27, 2026
c183a56
Add option to pass in hardware buffers when simulating image
Apr 7, 2026
4f86826
Create new expressive scrollfield component and add relevant samples …
kendrickumstattd Apr 6, 2026
400b8e9
Merge "Add agent skill for running tests" into androidx-main
halilozercan Apr 10, 2026
ab4d9de
Temporarily disable failing prefetch tests
peterbn Apr 10, 2026
245fadd
Merge "Temporarily disable failing prefetch tests" into androidx-main
Apr 10, 2026
b9baed7
Generate routing logic in generated AppFunctionService
Apr 10, 2026
73a407b
Merge "Translated AllowlistActivity to kotlin" into androidx-main
Apr 10, 2026
cd38a64
Merge "Generate routing logic in generated AppFunctionService" into a…
Apr 10, 2026
54f9748
Fixed enableEdgeToEdge not working in WebkitDemoAppHelpers
Apr 9, 2026
0ba1cac
Merge "Create new expressive scrollfield component and add relevant s…
kendrickumstattd Apr 10, 2026
32e92c1
Merge "Fixed enableEdgeToEdge not working in WebkitDemoAppHelpers" in…
Apr 10, 2026
4a3e9c2
compose:remote: Introduced a new macrobenchmark module and its matchi…
Mar 18, 2026
5260f84
Update various library versions to newer alpha releases.
Apr 10, 2026
b8ea1c7
Merge "Add option to pass in hardware buffers when simulating image" …
Apr 10, 2026
119aa6a
Merge "Update various library versions to newer alpha releases." into…
Apr 10, 2026
6aa992d
Merge "Update ExoPlayer to 1.10.0 and remove OptIn(UnstableApi) in So…
Apr 10, 2026
a0da266
Merge "compose:remote: Introduced a new macrobenchmark module and its…
Apr 10, 2026
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
124 changes: 124 additions & 0 deletions .agents/skills/run_tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
name: run_tests
description: A skill for identifying and running tests (Unit, Instrumentation, FTL) in the AndroidX repository.
---

# Run Tests Skill

This skill provides comprehensive instructions for executing tests within the AndroidX repository. It covers module discovery, unit testing, instrumentation testing on connected devices, and remote testing via Firebase Test Lab (FTL).

## 1. How to Find and Run a Specific Test

If you have a specific failing test (e.g., `BasicTextFieldTest#longText_doesNotCrash_singleLine`) and want to execute it:

1. **Find the Test File**: Use code search, `find_declaration`, or `find_files` (e.g., search for `BasicTextFieldTest.kt`).
2. **Identify the Module**: The file path determines the Gradle project (e.g., `compose/foundation/foundation/src/...` belongs to `:compose:foundation:foundation`).
3. **Identify Test Type**:
* If the test is in a `test`, `androidHostTest`, or `jvmTest` folder, it is a Unit Test.
* If the test is in `androidTest` or `androidDeviceTest`, it is an Instrumentation Test.
4. **Identify Module Type**: Check if the module is KMP or standard Android (see details below) by inspecting `build.gradle` for `androidXMultiplatform {` or running `./gradlew <module>:tasks | grep "test"`.
5. **Formulate the Task**: Select the proper Gradle task (e.g., `testAndroidHostTest` vs `test`, `connectedAndroidDeviceTest` vs `connectedAndroidTest`).
6. **Filter by Class/Method**:
* **Class**: Append filtering arguments. E.g., for instrumentation: `-Pandroid.testInstrumentationRunnerArguments.class=androidx...BasicTextFieldTest`. For unit tests: `--tests "androidx...BasicTextFieldTest"`.
* **Method**: For instrumentation, append `#<method_name>` to the class name (e.g., `...BasicTextFieldTest#longText_doesNotCrash_singleLine`). For unit tests, append `.<method_name>` to the class name in `--tests` (e.g., `--tests "...BasicTextFieldTest.longText_doesNotCrash_singleLine"`).

## 2. Module Discovery

Before running tests, you must identify the Gradle project name (module) associated with the code you are testing.

* **Mapping a path to a project**: Most directories in this repository are Gradle projects. Use the directory structure as a guide (e.g., `appcompat/appcompat` corresponds to `:appcompat:appcompat`).
* **Verification**: Run `./gradlew projects` to see a full list of projects. Because the output is very long, consider chaining a `grep` command to filter the output if you are looking for a specific module (e.g., `./gradlew projects | grep appcompat`).
* **Module Type**: Be aware if the module is a standard Android library (like `appcompat`) or a Kotlin Multiplatform (KMP) library (like `compose:foundation:foundation`). Task names differ significantly. To determine if a module is KMP, you can run `./gradlew <project-name>:tasks | grep "test"`: if you see tasks like `testAndroidHostTest` or `jvmTest`, it is a KMP module. If you see standard `test` and `connectedAndroidTest` tasks, it is a standard Android module. Alternatively, inspect its `build.gradle` file for `androidXMultiplatform {`.

## 3. Unit Testing (JVM)

Unit tests run on the host machine and are the fastest way to verify logic.

* **Standard Android Modules**:
```bash
./gradlew <project-name>:test
```
* **KMP Modules**:
```bash
./gradlew <project-name>:testAndroidHostTest
./gradlew <project-name>:jvmStubsTest
```
* **Filtering**:
Use the `--tests` filter. You can also append `.<method_name>` for specific methods.
```bash
./gradlew <project-name>:test --tests "androidx.example.MyTest"
./gradlew <project-name>:testAndroidHostTest --tests "androidx.example.MyTest"
./gradlew <project-name>:test --tests "androidx.example.MyTest.myMethod"
```

## 4. Instrumentation Testing (Connected Devices)

These tests run on a physical device or emulator connected via ADB.

* **Standard Android Modules**:
```bash
./gradlew <project-name>:connectedAndroidTest
```
* **KMP Modules**:
```bash
./gradlew <project-name>:connectedAndroidDeviceTest
```
* **Filtering**:
Use the `android.testInstrumentationRunnerArguments.class` property. For specific methods, append `#<method_name>`.
```bash
./gradlew <project-name>:connectedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=androidx.example.MyTest
./gradlew <project-name>:connectedAndroidDeviceTest \
-Pandroid.testInstrumentationRunnerArguments.class=androidx.example.MyTest#myMethod
```

## 5. Remote Testing (Firebase Test Lab)

AndroidX provides specialized tasks for running **instrumentation tests** on Firebase Test Lab (FTL). FTL is not used for local unit tests. The task suffix changes based on module type.

* **Standard Android Modules**: `ftl<device><api>releaseAndroidTest`
* **KMP Modules**: `ftl<device><api>androidDeviceTest`
* **Listing Available Combinations**:
To see the full list of available FTL tasks for a specific project, you can run:
```bash
./gradlew <project-name>:tasks --all | grep ftl
```
* **Common Device/API combinations**:
- `mediumphoneapi36`
- `mediumphoneapi35`
- `mediumphoneapi34`
- `mediumphoneapi30`
- `nexus5api23`
* **Example (KMP)**:
```bash
./gradlew <project-name>:ftlmediumphoneapi35androidDeviceTest --className="androidx.example.MyTest"
```

### 5.1. Reproducing Flakes on FTL

To verify a flaky test, you can run it multiple times on Firebase Test Lab using the `ftlOnApis` task variant.

1. **Parameterize the Test**: To run a test N times, temporarily modify the test class to be parameterized:
* Add `@RunWith(Parameterized::class)` to the class.
* Update the constructor to accept a repetition parameter: `class MyTest(private val repetition: Int)`.
* Add the companion object for data generation:
```kotlin
import org.junit.runners.Parameterized

companion object {
private const val RUNS = 100 // Adjust as needed
@JvmStatic
@Parameterized.Parameters
fun data(): Array<Int> = Array(RUNS) { 0 }
}
```
* *Note*: If there are many tests in the class but you only want to focus on one, comment out the `@Test` annotation on the irrelevant ones. DO NOT REPLACE ANY LINES OF CODE OR VARIABLES NAMES in the test class aside from making it parameterized. Test class name should remain the same.
2. **Run Locally (Optional)**: Verify it runs a few times locally before deploying to FTL. E.g., for KMP:
```bash
./gradlew <project-name>:connectedAndroidDeviceTest -Pandroid.testInstrumentationRunnerArguments.class=androidx.example.MyTest
```
3. **Run on FTL**: Use the `ftlOnApis` task. Specify the API level(s) with `--api` and add a longer timeout `--testTimeout=1h` (if the API level is not provided, you must ask the user for it).
```bash
# Example for KMP (append 'releaseAndroidTest' instead of 'androidDeviceTest' for standard Android)
./gradlew <project-name>:ftlOnApisandroidDeviceTest --testTimeout=1h --api 28 --api 30 --className=androidx.example.MyTest
```
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ object IntrospectionHelper {
ClassName(APP_FUNCTIONS_METADATA_PACKAGE_NAME, "CompileTimeAppFunctionMetadata")
val APP_FUNCTION_FUNCTION_NOT_FOUND_EXCEPTION_CLASS =
ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionFunctionNotFoundException")
val APP_FUNCTION_CANCELLED_EXCEPTION_CLASS =
ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionCancelledException")
val APP_FUNCTION_APP_UNKNOWN_EXCEPTION_CLASS =
ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionAppUnknownException")
val APP_FUNCTION_EXCEPTION_CLASS = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionException")
val APP_FUNCTION_SCHEMA_METADATA_CLASS =
ClassName(APP_FUNCTIONS_METADATA_PACKAGE_NAME, "AppFunctionSchemaMetadata")
val APP_FUNCTION_PARAMETER_METADATA_CLASS =
Expand Down Expand Up @@ -161,6 +166,10 @@ object IntrospectionHelper {
val APP_FUNCTION_RESPONSE_METADATA_CLASS =
ClassName(APP_FUNCTIONS_METADATA_PACKAGE_NAME, "AppFunctionResponseMetadata")

val DEPENDENCIES_CLASS = ClassName(APP_FUNCTIONS_INTERNAL_PACKAGE_NAME, "Dependencies")
val CANCELLATION_EXCEPTION_CLASS = ClassName("kotlinx.coroutines", "CancellationException")
val EXCEPTION_CLASS = ClassName("kotlin", "Exception")

object ConfigurableAppFunctionFactoryClass {
val CLASS_NAME =
ClassName(APP_FUNCTIONS_SERVICE_INTERNAL_PACKAGE_NAME, "ConfigurableAppFunctionFactory")
Expand All @@ -184,12 +193,27 @@ object IntrospectionHelper {
}
}

object AppFunctionExecutionDispatcherClass {
val CLASS_NAME =
ClassName(APP_FUNCTIONS_SERVICE_INTERNAL_PACKAGE_NAME, "AppFunctionExecutionDispatcher")

object ExecuteAppFunctionMethod {
const val METHOD_NAME = "executeAppFunction"
}
}

object ExecuteAppFunctionRequestClass {
val CLASS_NAME = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "ExecuteAppFunctionRequest")
}

object ExecuteAppFunctionResponseClass {
val CLASS_NAME = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "ExecuteAppFunctionResponse")
val SUCCESS_CLASS_NAME = CLASS_NAME.nestedClass("Success")
}

object ServiceInternalHelper {
const val UNSAFE_GET_PARAMETER_VALUE_METHOD = "unsafeGetParameterValue"
const val UNSAFE_BUILD_RETURN_VALUE_METHOD = "unsafeBuildReturnValue"
}

object AppFunctionInvokerClass {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ import androidx.annotation.VisibleForTesting
import androidx.appfunctions.compiler.AppFunctionCompiler
import androidx.appfunctions.compiler.core.AnnotatedAppFunctionEntryPoint
import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver
import androidx.appfunctions.compiler.core.IntrospectionHelper.APP_FUNCTION_FUNCTION_NOT_FOUND_EXCEPTION_CLASS
import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionExecutionDispatcherClass
import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionServiceClass
import androidx.appfunctions.compiler.core.IntrospectionHelper.ExecuteAppFunctionRequestClass
import androidx.appfunctions.compiler.core.IntrospectionHelper.ExecuteAppFunctionResponseClass
import androidx.appfunctions.compiler.core.ProcessingException
import androidx.appfunctions.compiler.core.logException
import androidx.appfunctions.compiler.core.toTypeName
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
Expand All @@ -34,10 +37,12 @@ import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.buildCodeBlock

/**
* The processor to generate the AppFunctionService subclass for an AppFunction entry point.
Expand Down Expand Up @@ -80,6 +85,7 @@ class AppFunctionEntryPointProcessor(
try {
entryPoint.validate()
generateAppFunctionService(entryPoint)
// TODO(b/463909015): Generate XML
} catch (e: ProcessingException) {
logger.logException(e)
}
Expand All @@ -98,19 +104,7 @@ class AppFunctionEntryPointProcessor(
TypeSpec.classBuilder(serviceName)
.superclass(originalClassName)
.addAnnotation(AppFunctionCompiler.GENERATED_ANNOTATION)
val executeFunctionBuilder =
FunSpec.builder(AppFunctionServiceClass.ExecuteFunctionMethod.METHOD_NAME)
.addModifiers(KModifier.OVERRIDE, KModifier.SUSPEND)
.addParameter(
AppFunctionServiceClass.ExecuteFunctionMethod.REQUEST_PARAM_NAME,
ExecuteAppFunctionRequestClass.CLASS_NAME,
)
.returns(ExecuteAppFunctionResponseClass.CLASS_NAME)
// TODO(b/463909015): Implement the routing logic.
.addStatement("TODO(%S)", "Not yet implemented")
.build()

serviceClassBuilder.addFunction(executeFunctionBuilder)
.addFunction(buildExecuteFunction(entryPoint))

val fileSpec =
FileSpec.builder(packageName, serviceName).addType(serviceClassBuilder.build()).build()
Expand All @@ -128,6 +122,50 @@ class AppFunctionEntryPointProcessor(
.use { fileSpec.writeTo(it) }
}

private fun buildExecuteFunction(entryPoint: AnnotatedAppFunctionEntryPoint): FunSpec {
return FunSpec.builder(AppFunctionServiceClass.ExecuteFunctionMethod.METHOD_NAME)
.addModifiers(KModifier.OVERRIDE, KModifier.SUSPEND)
.addParameter(
AppFunctionServiceClass.ExecuteFunctionMethod.REQUEST_PARAM_NAME,
ExecuteAppFunctionRequestClass.CLASS_NAME,
)
.returns(ExecuteAppFunctionResponseClass.CLASS_NAME)
.addCode(buildExecuteFunctionBody(entryPoint))
.build()
}

private fun buildExecuteFunctionBody(entryPoint: AnnotatedAppFunctionEntryPoint): CodeBlock {
return buildCodeBlock {
beginControlFlow(
"return %T.%L(request) { parameters ->",
AppFunctionExecutionDispatcherClass.CLASS_NAME,
AppFunctionExecutionDispatcherClass.ExecuteAppFunctionMethod.METHOD_NAME,
)
beginControlFlow("when (request.functionIdentifier)")
for (function in entryPoint.annotatedAppFunctions.appFunctionDeclarations) {
val identifier = entryPoint.annotatedAppFunctions.getAppFunctionIdentifier(function)
beginControlFlow("%S ->", identifier)
add("this.%N(\n", function.simpleName.asString())
indent()
for (param in function.parameters) {
val paramName = param.name!!.asString()
addStatement("parameters[%S] as %T,", paramName, param.type.toTypeName())
}
unindent()
addStatement(")")
endControlFlow()
}
beginControlFlow("else ->")
addStatement(
"throw %T(\n \"\${request.functionIdentifier} is not available\"\n)",
APP_FUNCTION_FUNCTION_NOT_FOUND_EXCEPTION_CLASS,
)
endControlFlow() // end else
endControlFlow() // end when
endControlFlow() // end executeAppFunction
}
}

@VisibleForTesting
class Provider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import androidx.appfunctions.service.AppFunction

@AppFunctionEntryPoint("MySimpleService", "app_functions_v2.xml")
abstract class SimpleEntryPoint: AppFunctionService() {
@AppFunction fun simpleFunction() {}
@AppFunction
internal fun simpleFunction(int: Int, string: String): String {
TODO("Not implemented")
}

override fun onCreate() {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
package com.testdata

import androidx.appfunctions.AppFunctionFunctionNotFoundException
import androidx.appfunctions.ExecuteAppFunctionRequest
import androidx.appfunctions.ExecuteAppFunctionResponse
import androidx.appfunctions.service.`internal`.AppFunctionExecutionDispatcher
import javax.`annotation`.processing.Generated
import kotlin.Int
import kotlin.String

@Generated("androidx.appfunctions.compiler.AppFunctionCompiler")
public class MySimpleService : SimpleEntryPoint() {
override suspend fun executeFunction(request: ExecuteAppFunctionRequest): ExecuteAppFunctionResponse {
TODO("Not yet implemented")
override suspend fun executeFunction(request: ExecuteAppFunctionRequest): ExecuteAppFunctionResponse = AppFunctionExecutionDispatcher.executeAppFunction(request) { parameters ->
when (request.functionIdentifier) {
"com.testdata.SimpleEntryPoint#simpleFunction" -> {
this.simpleFunction(
parameters["int"] as Int,
parameters["string"] as String,
)
}
else -> {
throw AppFunctionFunctionNotFoundException(
"${request.functionIdentifier} is not available"
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.os.Build
import androidx.annotation.RequiresApi
import androidx.appfunctions.core.AppFunctionMetadataTestHelper
import androidx.appfunctions.metadata.AppFunctionComponentsMetadata
import androidx.appfunctions.metadata.AppFunctionIntTypeMetadata
import androidx.appfunctions.metadata.AppFunctionLongTypeMetadata
import androidx.appfunctions.metadata.AppFunctionParameterMetadata
import androidx.appfunctions.metadata.AppFunctionResponseMetadata
Expand All @@ -37,6 +38,26 @@ class `$AggregatedAppFunctionInventory_Impl` : AggregatedAppFunctionInventory()
override val functionIdToMetadataMap: Map<String, CompileTimeAppFunctionMetadata>
get() =
mapOf(
AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_ENABLED_BY_DEFAULT to
CompileTimeAppFunctionMetadata(
id =
AppFunctionMetadataTestHelper.FunctionIds
.NO_SCHEMA_ENABLED_BY_DEFAULT,
isEnabledByDefault = true,
schema = null,
parameters =
listOf(
AppFunctionParameterMetadata(
name = "intParam",
isRequired = true,
dataType = AppFunctionIntTypeMetadata(isNullable = false),
)
),
response =
AppFunctionResponseMetadata(
valueType = AppFunctionUnitTypeMetadata(isNullable = false)
),
),
AppFunctionMetadataTestHelper.FunctionIds.NO_SCHEMA_EXECUTION_SUCCEED to
CompileTimeAppFunctionMetadata(
id =
Expand Down
Loading
Loading