Criar um executor de testes IRemoteTest fragmentado

Ao programar um executor de testes, é importante pensar na escalonabilidade. Perguntar "Se meu executor tivesse que executar 200 mil casos de teste" quanto tempo isso levaria?

A fragmentação é uma das respostas disponíveis na Trade Federation. Ele exige dividir todos os testes que o executor precisa em vários blocos que podem ser em paralelo.

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

Interface a ser implementada

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

Se a fragmentação depender do número de fragmentos solicitados, precisa implementar split(int numShard). Caso contrário, implemente split().

Quando um comando de teste do TF é executado com os parâmetros de fragmentação --shard-count e --shard-index, o TF faz a iteração de todos os IRemoteTest para procurar um implementar IShardableTest. Se encontrado, ele vai chamar split para receber um novo objeto IRemoteTest para executar um subconjunto de casos de teste para um determinado no fragmento.

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

  • O corredor pode fazer a fragmentação apenas sob algumas condições; Nesse caso, retorne null. sem fragmentação.
  • Tente dividir o que quiser: divida o executor em uma unidade de de uma execução que faça sentido para ele. Realmente depende do seu corredor. Para exemplo: HostTest (em inglês) é fragmentado no nível da classe, cada classe de teste é colocada em um fragmento separado.
  • Se fizer sentido, adicione algumas opções para controlar um pouco a fragmentação. Por exemplo: AndroidJUnitTest (link em inglês) tem um ajur-max-shard para especificar o número máximo de fragmentos que pode dividido, independentemente do número solicitado.

Exemplo detalhado de implementação

Confira um exemplo de snippet de código que implementa IShardableTest. de 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/localizedInstrumentationsTest.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 apenas cria uma nova instância de si mesma e define o fragmento parâmetros a ela. No entanto, a lógica de divisão pode ser totalmente diferente teste para testar; e contanto que seja determinista e produza coletivamente subconjuntos exaustivos, tudo bem.

Independência

Os fragmentos precisam ser independentes. Dois fragmentos criados por sua implementação do No executor, split não pode ter dependências uns com os outros nem compartilhar do Google Cloud.

A divisão de fragmentos precisa ser determinista! Isso também é obrigatório, considerando que mesmas condições, o método split precisa sempre retornar exatamente a mesma lista de os 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 produza subconjuntos mutuamente exclusivos coletivamente exaustivas de maneira determinista.

Fragmentar um teste localmente

Para fragmentar um teste em um TF local, basta adicionar a opção --shard-count ao na 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 resultado de teste para invocações fragmentadas, precisamos ter certeza de que seu serviço de relatórios oferece suporte a isso.