Escreva um executor de teste IRemoteTest fragmentado

Ao escrever um executor de teste, é importante pensar na escalabilidade. Pergunte a si mesmo: "se meu executor de teste tivesse que executar 200 mil casos de teste", quanto tempo levaria?

Sharding é uma das respostas disponíveis na Trade Federation. Requer a divisão de todos os testes que o executor precisa em vários pedaços que podem ser paralelizados.

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

Interface para implementar

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

Se o seu sharding vai depender do número de shards solicitados, você deve implementar split(int numShard) . Caso contrário, implemente split() .

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

O que devo saber sobre a implementação dividida?

  • Seu runner pode fragmentar apenas sob algumas condições; nesse caso, retorne null quando você não fragmentou.
  • Tente dividir o máximo que fizer sentido: divida seu corredor em unidades de execução que façam sentido para ele. Realmente depende do seu corredor. Por exemplo: HostTest é fragmentado no nível de 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 possui um ajur-max-shard para especificar o número máximo de fragmentos que ele pode dividir, independentemente do número solicitado.

Implementação de exemplo detalhado

Aqui está um exemplo de trecho de código implementando IShardableTest que você pode consultar. 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 de si mesmo e define parâmetros de fragmento para ela. Porém, a lógica de divisão pode ser totalmente diferente de teste para teste; e desde que seja determinístico e produza subconjuntos coletivamente exaustivos, está tudo bem.

Independência

Os fragmentos precisam ser independentes! Dois fragmentos criados pela implementação de split em seu executor não devem ter dependências entre si nem compartilhar recursos.

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

NOTA: Como cada fragmento pode ser executado em diferentes instâncias do TF, é fundamental garantir que a lógica split produza subconjuntos que sejam mutuamente exclusivos e coletivamente exaustivos de maneira determinística.

Fragmentar um teste localmente

Para fragmentar um teste em um TF local, você pode simplesmente adicionar a opção --shard-count à linha de comando.

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

Então o TF gerará comandos automaticamente para cada fragmento e os executará.

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 de resultados de teste

Como o TF não faz nenhuma agregação de resultados de teste para invocações fragmentadas, você precisa ter certeza de que seu serviço de relatórios oferece suporte a isso.