Escreva um executor de teste IRemoteTest fragmentado

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

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

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

Interface para implementar

A interface mais importante a ser implementada para ser considerada shardable 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 do TF é executado com parâmetros --shard-count e --shard-index , o TF percorre 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 de divisão?

  • Seu corredor pode fragmentar apenas em algumas condições; nesse caso, retorne null quando você não fragmentou.
  • Tente dividir o quanto fizer sentido: divida seu runner em unidades de execução que façam sentido para ele. Depende muito 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 o sharding. Por exemplo: AndroidJUnitTest tem um ajur-max-shard para especificar o número máximo de shards que ele pode dividir, independentemente do número solicitado.

Implementação de exemplo detalhada

Aqui está um trecho de código de exemplo implementando IShardableTest que você pode fazer referência. O código completo está disponível em (https://android.googlesource.com/platform/tools/tradefederation/+/refs/heads/master/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 estilhaço para ela. No entanto, a lógica de divisão pode ser totalmente diferente de teste para teste; e desde que seja determinista e produza subconjuntos coletivamente exaustivos, tudo bem.

Independência

Os fragmentos precisam ser independentes! Dois shards criados por sua implementação de split em seu runner não devem ter dependências entre si ou compartilhar recursos.

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

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

Como fragmentar um teste localmente

Para fragmentar 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

Em seguida, 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 certificar-se de que seu serviço de relatórios oferece suporte a ele.