シャードされた IRemoteTest テストランナーを作成する

テストランナーを作成するときは、拡張性を考慮することが重要です。たとえば、テストランナーが 20 万件のテストケースを実行する場合、どれほどの時間がかかるかを考えてみてください。

シャーディングは、拡張性の問題に対して Trade Federation(Tradefed または TF)で利用できる解決策の一つです。シャーディングでは、ランナーが必要とするすべてのテストを、並列化できる複数のチャンクに分割する必要があります。

このページでは、Tradefed でランナーをシャード可能にする方法を説明します。

実装するインターフェース

TF によってシャード可能と見なされるために実装すべき最も重要なインターフェースを 1 つ挙げるとすれば、それは IShardableTest です。これには split(int numShard)split() という 2 つのメソッドが含まれています。

シャーディングがリクエストされたシャードの数に依存する場合は、split(int numShard) を実装する必要があります。依存しない場合は、split() を実装します。

シャーディング パラメータ --shard-count および --shard-index を指定して TF テストコマンドを実行すると、TF はすべての IRemoteTest を反復して、IShardableTest を実装しているものを探します。見つかった場合は、split を呼び出して新しい IRemoteTest オブジェクトを取得し、特定のシャードに対してテストケースのサブセットを実行します。

分割実装に関する注意点

  • いくつかの条件でのみランナーをシャードする場合があります。その場合は、シャードしなかったときに null を返します。
  • できるだけ合理的に分割するようにします。つまり、理にかなった実行単位にランナーを分割します。分割方法はランナーによってそれぞれ異なります。たとえば、HostTest をクラスレベルでシャードして、各テストクラスを個別のシャードに配置します。
  • 合理的な範囲でいくつかのオプションを追加し、シャーディングをきめ細かく制御します。たとえば、AndroidJUnitTest には ajur-max-shard があり、リクエストされた数にかかわらず、分割できるシャードの最大数を指定できます。

実装の詳細な例

参照可能な IShardableTest を実装するコード スニペットの例を次に示します。完全なコードは以下の場所にあります。 (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;
    }
    ...
}

この例では、単にそれ自体の新しいインスタンスを作成して、シャード パラメータを設定しています。ただし、分割ロジックはテストごとにまったく別のものを使用できます。ロジックが決定論的で、全体としてすべてを網羅するサブセットを生成するものである限り、問題はありません。

独立性

シャードは独立している必要があります。ランナーの split の実装によって作成された 2 つのシャードは、互いに依存したりリソースを共有したりしてはなりません。

シャード分割は決定論的であることが必要です。同じ条件の下では、split メソッドが常に同じ順序でまったく同じシャードのリストを返すことも必要です。

注: 各シャードは異なる TF インスタンス上で実行できるため、split ロジックにより、決定論的な方法で全体としてすべてを網羅する相互排他的なサブセットが生成されることが重要です。

ローカルでテストをシャードする

ローカル TF でテストをシャードするには、コマンドラインに --shard-count オプションを追加するだけです。

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

そうすると、TF はシャードごとにコマンドを自動的に生成して実行します。

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)

テスト結果の集計

TF はシャードされた呼び出しのテスト結果を集計しないので、レポート作成サービスが集計をサポートしていることを確認する必要があります。