샤딩된 IRemoteTest 테스트 실행기 작성

테스트 실행기를 작성할 때는 확장성을 고려하는 것이 중요합니다. 만약 테스트 실행기가 200,000건의 테스트 사례를 실행해야 했다면 얼마의 시간이 소요되었을까요?

샤딩은 Trade Federation에서 제공되는 해결책 중 하나이며, 실행기에 필요한 모든 테스트를 병렬화 가능한 여러 개의 청크로 분할하도록 요구합니다.

이 페이지에서는 Tradefed에서 실행기를 샤딩 가능하도록 만드는 방법을 설명합니다.

구현할 인터페이스

TF에서 샤딩 가능하다고 간주하도록 구현해야 하는 가장 중요한 인터페이스는 IShardableTest입니다. 여기에는 두 개의 메서드 split(int numShard)split()가 포함되어 있습니다.

샤딩이 요청된 조각 수에 따라 좌우된다면 split(int numShard)를 구현해야 합니다. 나머지 경우에는 split()를 구현합니다.

TF 테스트 명령어가 샤딩 매개변수인 --shard-count--shard-index로 실행되면 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 구현에 의해 생성된 두 개의 샤드는 서로에 관한 종속 항목을 지니거나 리소스를 공유해서는 안 됩니다.

샤드 분할은 확정적이어야 합니다! 이 역시 필수이지만 조건이 같다면 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는 샤딩된 호출에 관한 어떠한 테스트 결과 집계도 실행하지 않으므로 보고 서비스에서 이를 지원하는지 확인해야 합니다.