Escribe un ejecutor de pruebas fragmentado de IRemoteTest

Cuando se escribe un ejecutor de pruebas, es importante considerar la escalabilidad. Pregúntate: "Si mi ejecutor de pruebas tuviera que ejecutar 200,000 casos de prueba", ¿cuánto tiempo tardaría?

El particionamiento es una de las respuestas disponibles en Trade Federation. Requiere dividir todas las pruebas que necesita el ejecutor en varios segmentos que se puedan paralelizar.

En esta página, se describe cómo hacer que tu ejecutor sea particionable para Tradefed.

Interfaz que se implementará

La interfaz más importante que se debe implementar para que TF la considere particionable es IShardableTest, que contiene dos métodos: split(int numShard) y split().

Si tu fragmentación dependerá de la cantidad de fragmentos solicitados, debes implementar split(int numShard). De lo contrario, implementa split().

Cuando se ejecuta un comando de prueba de TF con los parámetros de fragmentación --shard-count y --shard-index, TF itera por todos los IRemoteTest para buscar aquellos que implementan IShardableTest. Si se encuentra, llamará a split para obtener un nuevo objeto IRemoteTest que ejecute un subconjunto de casos de prueba para un fragmento específico.

¿Qué debo saber sobre la implementación de la división?

  • El ejecutor puede particionar solo en algunas condiciones. En ese caso, muestra null cuando no lo hagas.
  • Intenta dividirlo tanto como sea posible: divide el comparador en unidades de ejecución que tengan sentido para él. Realmente depende de tu ejecutor. Por ejemplo, HostTest se fragmenta a nivel de la clase, y cada clase de prueba se coloca en un fragmento independiente.
  • Si tiene sentido, agrega algunas opciones para controlar un poco el particionamiento. Por ejemplo, AndroidJUnitTest tiene un ajur-max-shard para especificar la cantidad máxima de fragmentos en los que se puede dividir, independientemente de la cantidad solicitada.

Ejemplo detallado de implementación

Este es un ejemplo de fragmento de código que implementa IShardableTest que puedes usar como referencia. El código completo está disponible en (https://android.googlesource.com/platform/tools/tradefederation/+/refs/heads/main/test_framework/com/android/tradefed/testtype/InstalledInstrumentationsTest.java).

/**
 * Runs all instrumentation found on current device.
 */
@OptionClass(alias = "installed-instrumentation")
public class InstalledInstrumentationsTest
        implements IDeviceTest, IResumableTest, IShardableTest {
    ...

    /** {@inheritDoc} */
    @Override
    public Collection<IRemoteTest> split(int shardCountHint) {
        if (shardCountHint > 1) {
            Collection<IRemoteTest> shards = new ArrayList<>(shardCountHint);
            for (int index = 0; index < shardCountHint; index++) {
                shards.add(getTestShard(shardCountHint, index));
            }
            return shards;
        }
        // Nothing to shard
        return null;
    }

    private IRemoteTest getTestShard(int shardCount, int shardIndex) {
        InstalledInstrumentationsTest shard = new InstalledInstrumentationsTest();
        try {
            OptionCopier.copyOptions(this, shard);
        } catch (ConfigurationException e) {
            CLog.e("failed to copy instrumentation options: %s", e.getMessage());
        }
        shard.mShardIndex = shardIndex;
        shard.mTotalShards = shardCount;
        return shard;
    }
    ...
}

En este ejemplo, simplemente se crea una instancia nueva de sí misma y se establecen parámetros de fragmento en ella. Sin embargo, la lógica de división puede ser totalmente diferente de una prueba a otra, y está bien siempre que sea determinista y genere subconjuntos colectivamente exhaustivos.

Independencia

Los fragmentos deben ser independientes. Los dos fragmentos creados por tu implementación de split en tu ejecutor no deben tener dependencias entre sí ni compartir recursos.

La división de fragmentos debe ser determinista. Esto también es obligatorio, ya que, dadas las mismas condiciones, tu método split siempre debe mostrar la misma lista de fragmentos en el mismo orden.

NOTA: Dado que cada fragmento puede ejecutarse en diferentes instancias de TF, es fundamental garantizar que la lógica de split produzca subconjuntos que son mutuamente excluyentes y colectivamente exhaustivos de manera determinista.

Fragmenta una prueba de forma local

Para fragmentar una prueba en un TF local, simplemente puedes agregar la opción --shard-count a la línea de comandos.

tf >run host --class com.android.tradefed.UnitTests --shard-count 3

Luego, TF creará automáticamente comandos para cada fragmento y los ejecutará.

tf >l i
Command Id  Exec Time  Device          State
3           0m:03      [null-device-2]  running stub on build 0 (shard 1 of 3)
3           0m:03      [null-device-1]  running stub on build 0 (shard 0 of 3)
3           0m:03      [null-device-3]  running stub on build 0 (shard 2 of 3)

Agregación de resultados de prueba

Dado que TF no realiza ninguna agregación de resultados de prueba para invocaciones fragmentadas, debes asegurarte de que tu servicio de informes lo admita.