Scrivere un test runner IRemoteTest partizionato

Quando si scrive un test runner, è importante pensare alla scalabilità. Chiediti: "se il mio test runner dovesse eseguire 200.000 casi di test", quanto tempo ci vorrebbe?

Lo Sharding è una delle risposte disponibili in Trade Federation. Richiede la suddivisione di tutti i test di cui il corridore ha bisogno in più blocchi che possono essere parallelizzati.

Questa pagina descrive come rendere il tuo runner partizionabile per Tradefed.

Interfaccia da implementare

La singola interfaccia più importante da implementare per essere considerata partizionabile da TF è IShardableTest , che contiene due metodi: split(int numShard) e split() .

Se il tuo partizionamento dipenderà dal numero di frammenti richiesti, dovresti implementare split(int numShard) . Altrimenti, implementa split() .

Quando un comando di test TF viene eseguito con i parametri di sharding --shard-count e --shard-index , TF scorre tutti gli IRemoteTest per cercare quelli che implementano IShardableTest . Se trovato, chiamerà split per ottenere un nuovo oggetto IRemoteTest per eseguire un sottoinsieme di casi di test per uno shard specifico.

Cosa dovrei sapere sull'implementazione divisa?

  • Il tuo corridore può frammentarsi solo ad alcune condizioni; in tal caso restituisci null quando non hai effettuato lo sharding.
  • Prova a dividere quanto ha senso: dividi il tuo corridore in unità di esecuzione che abbiano senso per lui. Dipende davvero dal tuo corridore. Ad esempio: HostTest viene suddiviso a livello di classe, ogni classe di test viene inserita in uno shard separato.
  • Se ha senso, aggiungi alcune opzioni per controllare un po' lo sharding. Ad esempio: AndroidJUnitTest ha un ajur-max-shard per specificare il numero massimo di frammenti in cui può essere suddiviso, indipendentemente dal numero richiesto.

Implementazione di esempio dettagliata

Ecco un frammento di codice di esempio che implementa IShardableTest a cui puoi fare riferimento. Il codice completo è disponibile all'indirizzo (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;
    }
    ...
}

Questo esempio crea semplicemente una nuova istanza di se stesso e imposta su di essa i parametri di shard. Tuttavia, la logica di suddivisione può essere totalmente diversa da test a test; e finché è deterministico e produce sottoinsiemi collettivamente esaustivi, va bene.

Indipendenza

I frammenti devono essere indipendenti! Due frammenti creati dall'implementazione di split nel runner non dovrebbero avere dipendenze reciproche o condividere risorse.

La suddivisione dei frammenti deve essere deterministica! Anche questo è obbligatorio, date le stesse condizioni, il tuo metodo split dovrebbe sempre restituire esattamente lo stesso elenco di frammenti nello stesso ordine.

NOTA: poiché ogni frammento può essere eseguito su istanze TF diverse, è fondamentale garantire che la logica split produca sottoinsiemi mutuamente esclusivi e collettivamente esaustivi in ​​modo deterministico.

Shard un test localmente

Per partizionare un test su un TF locale, puoi semplicemente aggiungere l'opzione --shard-count alla riga di comando.

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

Quindi TF genererà automaticamente i comandi per ciascun frammento e li eseguirà.

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)

Aggregazione dei risultati dei test

Poiché TF non esegue alcuna aggregazione dei risultati dei test per le invocazioni partizionate, è necessario assicurarsi che il servizio di reporting lo supporti.