Criar um executor de testes IRemoteTest fragmentado

Ao programar um executor de testes, é importante pensar na escalonabilidade. Pergunte a si mesmo: "Se o executor de testes tivesse que executar 200 mil casos de teste", quanto tempo isso levaria?

O fragmentação é uma das respostas disponíveis na Trade Federation. É necessário dividir todos os testes necessários para o executor em vários blocos que podem ser executados em paralelo.

Esta página descreve como tornar o runner fragmentável para Tradefed.

Interface a ser implementada

A interface mais importante a ser implementada para ser considerada particionável pelo TF é IShardableTest, que contém dois métodos: split(int numShard) e split().

Se o sharding depender do número de fragmentos solicitados, implemente split(int numShard). Caso contrário, implemente split().

Quando um comando de teste do TF é executado com parâmetros de fragmentação --shard-count e --shard-index, o TF itera por todos os IRemoteTest para procurar aqueles que implementam IShardableTest. Se encontrado, ele vai chamar split para receber um novo objeto IRemoteTest e executar um subconjunto de casos de teste para um fragmento específico.

O que preciso saber sobre a implementação da divisão?

  • O executor pode dividir em fragmentos apenas em algumas condições. Nesse caso, retorne null quando não tiver feito a divisão.
  • Tente dividir o máximo possível: divida o executor em unidades de execução que façam sentido. Depende do seu corredor. Por exemplo: HostTest é fragmentado no nível da classe, e cada classe de teste é colocada em um fragmento separado.
  • Se fizer sentido, adicione algumas opções para controlar o fragmentação. Por exemplo, AndroidJUnitTest tem um ajur-max-shard para especificar o número máximo de fragmentos em que ele pode ser dividido, independentemente do número solicitado.

Exemplo detalhado de implementação

Confira um exemplo de snippet de código que implementa IShardableTest que você pode usar como referência. O código completo está disponível em (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;
    }
    ...
}

Este exemplo simplesmente cria uma nova instância dele mesmo e define os parâmetros do fragmento. No entanto, a lógica de divisão pode ser totalmente diferente de um teste para outro. Se ela for determinística e gerar subconjuntos exaustivos coletivamente, não há problema.

Independência

Os fragmentos precisam ser independentes. Dois fragmentos criados pela implementação de split no executor não podem ter dependências uns com os outros nem compartilhar recursos.

A divisão de fragmentos precisa ser determinística. Isso também é obrigatório, considerando as mesmas condições, seu método split sempre retornará a mesma lista de fragmentos na mesma ordem.

OBSERVAÇÃO: como cada fragmento pode ser executado em diferentes instâncias do TF, é fundamental garantir que a lógica split gere subconjuntos mutuamente exclusivos e coletivamente exaustivos de maneira determinística.

Fragmentar um teste localmente

Para dividir um teste em um TF local, basta adicionar a opção --shard-count à linha de comando.

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

Então, o TF vai gerar comandos automaticamente para cada fragmento e executá-los.

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)

Agregação do resultado do teste

Como o TF não faz nenhuma agregação de resultados de teste para invocações divididas, é necessário verificar se o serviço de relatórios oferece suporte a isso.