Cómo escribir un ejecutor de pruebas IRemoteTest fragmentado

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

El sharding es una de las respuestas disponibles en Trade Federation. Requiere dividir todas las pruebas que necesita el ejecutor en varios fragmentos que se pueden paralelizar.

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

Interfaz para implementar

La interfaz más importante que se debe implementar para que TF considere que se puede fragmentar 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 los que implementan IShardableTest. Si se encuentra, llamará a split para obtener un nuevo objeto IRemoteTest y ejecutar un subconjunto de casos de prueba para un fragmento específico.

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

  • Es posible que tu ejecutor solo cree fragmentos en algunas condiciones. En ese caso, devuelve null cuando no hayas creado fragmentos.
  • Intenta dividirlo tanto como tenga sentido: divide tu ejecutor en unidades de ejecución que tengan sentido para él. Realmente depende de tu corredor. Por ejemplo, HostTest se fragmenta a nivel de la clase, y cada clase de prueba se coloca en un fragmento separado.
  • Si tiene sentido, agrega algunas opciones para controlar un poco la fragmentación. Por ejemplo, AndroidJUnitTest tiene un ajur-max-shard para especificar la cantidad máxima de fragmentos en los que se podría dividir, independientemente de la cantidad solicitada.

Ejemplo detallado de implementación

A continuación, se incluye un ejemplo de fragmento de código que implementa IShardableTest al que puedes hacer referencia. El código completo está disponible en (https://android.googlesource.com/platform/tools/tradefederation/+/refs/heads/android16-release/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í mismo 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, siempre que sea determinística y produzca subconjuntos colectivamente exhaustivos, está bien.

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 determinística. Esto también es obligatorio, ya que, en las mismas condiciones, tu método split siempre debe devolver la misma lista exacta 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 genere subconjuntos que sean mutuamente exclusivos y colectivamente exhaustivos de manera determinística.

Cómo fragmentar una prueba de forma local

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

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

Luego, TF generará 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 pruebas

Dado que TF no agrega ningún resultado de prueba para las invocaciones fragmentadas, debes asegurarte de que tu servicio de informes lo admita.