Écrire un exécuteur de test IRemoteTest fractionné

Lorsque vous écrivez un outil d'exécution de tests, il est important de penser à l'évolutivité. Demandez-vous : "Si mon outil d'exécution de tests devait exécuter 200 000 cas de test, combien de temps cela prendrait-il ?"

Le sharding est l'une des réponses disponibles dans Trade Federation. Il nécessite de diviser tous les tests dont le programme d'exécution a besoin en plusieurs segments pouvant être parallélisés.

Cette page explique comment rendre votre exécuteur fragmentable pour Tradefed.

Interface à implémenter

L'interface la plus importante à implémenter pour être considérée comme fragmentable par TF est IShardableTest, qui contient deux méthodes: split(int numShard) et split().

Si votre partitionnement dépend du nombre de partitions demandées, vous devez implémenter split(int numShard). Sinon, implémentez split().

Lorsqu'une commande de test TF est exécutée avec les paramètres de fractionnement --shard-count et --shard-index, TF itère sur tous les IRemoteTest pour rechercher ceux qui implémentent IShardableTest. Si elle est trouvée, elle appelle split pour obtenir un nouvel objet IRemoteTest afin d'exécuter un sous-ensemble de cas de test pour un fragment spécifique.

Que dois-je savoir sur l'implémentation de la division ?

  • Votre exécuteur peut effectuer un fractionnement uniquement dans certaines conditions. Dans ce cas, renvoyez null lorsque vous n'avez pas effectué de fractionnement.
  • Essayez de diviser autant que possible: divisez votre exécuteur en unités d'exécution qui ont du sens. Cela dépend vraiment de votre coureur. Par exemple, HostTest est partitionné au niveau de la classe. Chaque classe de test est placée dans un segment distinct.
  • Si cela est pertinent, ajoutez des options pour contrôler un peu le fractionnement. Par exemple, AndroidJUnitTest possède un ajur-max-shard pour spécifier le nombre maximal de fragments qu'il peut diviser, quel que soit le nombre demandé.

Exemple d'implémentation détaillé

Voici un exemple d'extrait de code implémentant IShardableTest que vous pouvez consulter. Le code complet est disponible sur (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;
    }
    ...
}

Cet exemple crée simplement une instance de lui-même et lui attribue des paramètres de fragment. Toutefois, la logique de fractionnement peut être totalement différente d'un test à l'autre. Tant qu'elle est déterministe et génère des sous-ensembles exhaustifs, tout va bien.

Independence

Les fragments doivent être indépendants. Les deux fragments créés par votre implémentation de split dans votre exécuteur ne doivent pas dépendre les uns des autres ni partager de ressources.

Le fractionnement des fragments doit être déterministe. Cela est également obligatoire. Compte tenu des mêmes conditions, votre méthode split doit toujours renvoyer exactement la même liste de fragments dans le même ordre.

REMARQUE: Étant donné que chaque fragment peut s'exécuter sur différentes instances TF, il est essentiel de s'assurer que la logique split génère des sous-ensembles mutuellement exclusifs et collectivement exhaustifs de manière déterministe.

Diviser un test en local

Pour fractionner un test sur un TF local, vous pouvez simplement ajouter l'option --shard-count à la ligne de commande.

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

TF génère ensuite automatiquement des commandes pour chaque fragment et les exécute.

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)

Agrégation des résultats des tests

Étant donné que TF n'effectue aucune agrégation des résultats des tests pour les invocations fractionnées, vous devez vous assurer que votre service de création de rapports le prend en charge.