/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.tools.build.bundletool.commands; import static com.android.bundle.Targeting.Abi.AbiAlias.ARM64_V8A; import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI; import static com.android.bundle.Targeting.Abi.AbiAlias.ARMEABI_V7A; import static com.android.bundle.Targeting.Abi.AbiAlias.MIPS; import static com.android.bundle.Targeting.Abi.AbiAlias.MIPS64; import static com.android.bundle.Targeting.Abi.AbiAlias.X86; import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.HDPI; import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.LDPI; import static com.android.bundle.Targeting.ScreenDensity.DensityAlias.MDPI; import static com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias.ASTC; import static com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias.ETC2; import static com.android.tools.build.bundletool.commands.GetSizeCommand.SUPPORTED_DIMENSIONS; import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.ALL; import static com.android.tools.build.bundletool.model.utils.CsvFormatter.CRLF; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApkDescription; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createApksArchiveFile; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createAssetSliceSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createInstantApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createMasterApkDescription; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createSplitApkSet; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.createVariant; import static com.android.tools.build.bundletool.testing.ApksArchiveHelpers.standaloneVariant; import static com.android.tools.build.bundletool.testing.DeviceFactory.createDeviceSpecFile; import static com.android.tools.build.bundletool.testing.DeviceFactory.deviceWithSdk; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkSdkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkTextureTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeApkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantDensityTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredFlagException; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Commands.BuildApksResult; import com.android.bundle.Commands.DeliveryType; import com.android.bundle.Commands.Variant; import com.android.bundle.Config.Bundletool; import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.SdkVersion; import com.android.tools.build.bundletool.TestData; import com.android.tools.build.bundletool.commands.GetSizeCommand.GetSizeSubcommand; import com.android.tools.build.bundletool.flags.FlagParser; import com.android.tools.build.bundletool.flags.FlagParser.FlagParseException; import com.android.tools.build.bundletool.flags.ParsedFlags; import com.android.tools.build.bundletool.io.ZipBuilder; import com.android.tools.build.bundletool.io.ZipBuilder.EntryOption; import com.android.tools.build.bundletool.model.ConfigurationSizes; import com.android.tools.build.bundletool.model.GetSizeRequest.Dimension; import com.android.tools.build.bundletool.model.SizeConfiguration; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.model.exceptions.InvalidDeviceSpecException; import com.android.tools.build.bundletool.model.utils.GZipUtils; import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import com.google.protobuf.util.JsonFormat; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.PrintStream; import java.io.Reader; import java.nio.file.Path; import java.nio.file.Paths; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.FromDataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; @RunWith(Theories.class) public final class GetSizeCommandTest { private static final byte[] DUMMY_BYTES = new byte[100]; @Rule public final TemporaryFolder tmp = new TemporaryFolder(); private Path tmpDir; private long compressedApkSize; @Before public void setUp() throws Exception { tmpDir = tmp.getRoot().toPath(); compressedApkSize = GZipUtils.calculateGzipCompressedSize(ByteSource.wrap(DUMMY_BYTES)); } @Test public void missingDeviceSpecFlag_defaultDeviceSpec() throws Exception { Path apksArchiveFile = createApksArchiveFile(BuildApksResult.getDefaultInstance(), tmpDir.resolve("bundle.apks")); GetSizeCommand getSizeCommand = GetSizeCommand.fromFlags( new FlagParser().parse("get-size", "total", "--apks=" + apksArchiveFile)); assertThat(getSizeCommand.getDeviceSpec()).isEqualToDefaultInstance(); } @Test public void missingApksArchiveFlag_throws() { expectMissingRequiredFlagException( "apks", () -> GetSizeCommand.fromFlags(new FlagParser().parse())); } @Test public void nonExistentApksArchiveFile_throws() throws Exception { Path deviceSpecFile = createDeviceSpecFile(DeviceSpec.getDefaultInstance(), tmpDir.resolve("device.json")); ParsedFlags flags = new FlagParser() .parse("get-size", "total", "--device-spec=" + deviceSpecFile, "--apks=nonexistent"); Throwable exception = assertThrows(IllegalArgumentException.class, () -> GetSizeCommand.fromFlags(flags)); assertThat(exception).hasMessageThat().contains("File 'nonexistent' was not found"); } @DataPoints("deviceSpecs") public static final ImmutableSet<String> DEVICE_SPECS = ImmutableSet.of( "testdata/device/pixel2_spec.json", "testdata/device/invalid_spec_abi_empty.json"); @Test @Theory public void checkFlagsConstructionWithDeviceSpec( @FromDataPoints("deviceSpecs") String deviceSpecPath) throws Exception { DeviceSpec.Builder expectedDeviceSpecBuilder = DeviceSpec.newBuilder(); try (Reader reader = TestData.openReader(deviceSpecPath)) { JsonFormat.parser().merge(reader, expectedDeviceSpecBuilder); } DeviceSpec expectedDeviceSpec = expectedDeviceSpecBuilder.build(); BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); Path deviceSpecFile = copyToTempDir(deviceSpecPath); GetSizeCommand command = GetSizeCommand.fromFlags( new FlagParser() .parse( "get-size", "total", "--device-spec=" + deviceSpecFile, "--apks=" + apksArchiveFile)); assertThat(command.getDeviceSpec()).isEqualTo(expectedDeviceSpec); } @Test public void deviceSpecUnknownExtension_throws() throws Exception { DeviceSpec deviceSpec = deviceWithSdk(21); Path deviceSpecFile = createDeviceSpecFile(deviceSpec, tmpDir.resolve("bad_filename.dat")); BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ParsedFlags flags = new FlagParser() .parse( "get-size", "total", "--device-spec=" + deviceSpecFile, "--apks=" + apksArchiveFile); Throwable exception = assertThrows(InvalidDeviceSpecException.class, () -> GetSizeCommand.fromFlags(flags)); assertThat(exception).hasMessageThat().contains("Expected .json extension for the device spec"); } @Test public void testSupportedDimensions_onlySkipsAllDimension() { assertThat(Sets.difference(SUPPORTED_DIMENSIONS, ImmutableSet.copyOf(Dimension.values()))) .isEmpty(); assertThat(Sets.difference(ImmutableSet.copyOf(Dimension.values()), SUPPORTED_DIMENSIONS)) .containsExactly(ALL); } @Test public void wrongSubCommand_throws() throws Exception { BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ParsedFlags flags = new FlagParser().parse("get-size", "full", "--apks=" + apksArchiveFile); Throwable exception = assertThrows(InvalidCommandException.class, () -> GetSizeCommand.fromFlags(flags)); assertThat(exception).hasMessageThat().contains("Unrecognized get-size command target:"); assertThat(exception).hasMessageThat().contains("full"); } @Test public void deviceSpecWrongDimensions_throws() throws Exception { BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ParsedFlags flags = new FlagParser() .parse( "get-size", "total", "--apks=" + apksArchiveFile, "--dimensions=" + "ABI,SCREEN"); Throwable exception = assertThrows(FlagParseException.class, () -> GetSizeCommand.fromFlags(flags)); assertThat(exception).hasMessageThat().contains("Not a valid enum value"); assertThat(exception).hasMessageThat().contains("SCREEN"); } @Test public void deviceSpecAll_hasAllDimensions() throws Exception { BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); GetSizeCommand command = GetSizeCommand.fromFlags( new FlagParser() .parse("get-size", "total", "--apks=" + apksArchiveFile, "--dimensions=" + "ALL")); assertThat(command.getDimensions()).isSameInstanceAs(SUPPORTED_DIMENSIONS); } @Test public void builderAndFlagsConstruction_optionalDimensions_equivalent() throws Exception { BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); GetSizeCommand fromFlags = GetSizeCommand.fromFlags( new FlagParser() .parse( "get-size", "total", "--apks=" + apksArchiveFile, "--dimensions=" + "ABI,SCREEN_DENSITY")); GetSizeCommand fromBuilderApi = GetSizeCommand.builder() .setApksArchivePath(apksArchiveFile) .setDimensions(ImmutableSet.of(Dimension.ABI, Dimension.SCREEN_DENSITY)) .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .build(); assertThat(fromFlags).isEqualTo(fromBuilderApi); } @Test public void builderAndFlagsConstruction_equivalent() throws Exception { BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); GetSizeCommand fromFlags = GetSizeCommand.fromFlags( new FlagParser().parse("get-size", "total", "--apks=" + apksArchiveFile)); GetSizeCommand fromBuilderApi = GetSizeCommand.builder() .setApksArchivePath(apksArchiveFile) .setDeviceSpec(DeviceSpec.getDefaultInstance()) .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .build(); assertThat(fromFlags).isEqualTo(fromBuilderApi); } @Test public void builderAndFlagsConstruction_optionalDeviceSpec_inJavaViaApi_equivalent() throws Exception { DeviceSpec deviceSpec = deviceWithSdk(21); Path deviceSpecFile = createDeviceSpecFile(deviceSpec, tmpDir.resolve("device.json")); BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); GetSizeCommand fromFlags = GetSizeCommand.fromFlags( new FlagParser() .parse( "get-size", "total", "--apks=" + apksArchiveFile, // Optional values. "--device-spec=" + deviceSpecFile)); GetSizeCommand fromBuilderApi = GetSizeCommand.builder() .setApksArchivePath(apksArchiveFile) .setDeviceSpec(deviceSpec) .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .build(); assertThat(fromFlags).isEqualTo(fromBuilderApi); } @Test public void builderAndFlagsConstruction_optionalDeviceSpec_inJavaViaFiles_equivalent() throws Exception { DeviceSpec deviceSpec = deviceWithSdk(21); Path deviceSpecFile = createDeviceSpecFile(deviceSpec, tmpDir.resolve("device.json")); BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); GetSizeCommand fromFlags = GetSizeCommand.fromFlags( new FlagParser() .parse( "get-size", "total", "--apks=" + apksArchiveFile, // Optional values. "--device-spec=" + deviceSpecFile)); GetSizeCommand fromBuilderApi = GetSizeCommand.builder() .setApksArchivePath(apksArchiveFile) .setDeviceSpec(deviceSpecFile) .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .build(); assertThat(fromFlags).isEqualTo(fromBuilderApi); } @Test public void builderAndFlagsConstruction_optionalModules_equivalent() throws Exception { BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); GetSizeCommand fromFlags = GetSizeCommand.fromFlags( new FlagParser() .parse( "get-size", "total", "--apks=" + apksArchiveFile, // Optional values. "--modules=base")); GetSizeCommand fromBuilderApi = GetSizeCommand.builder() .setApksArchivePath(apksArchiveFile) .setModules(ImmutableSet.of("base")) .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .build(); assertThat(fromFlags).isEqualTo(fromBuilderApi); } @Test public void builderAndFlagsConstruction_optionalInstant_equivalent() throws Exception { BuildApksResult tableOfContentsProto = BuildApksResult.getDefaultInstance(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); GetSizeCommand fromFlags = GetSizeCommand.fromFlags( new FlagParser() .parse( "get-size", "total", "--apks=" + apksArchiveFile, // Optional values. "--instant")); GetSizeCommand fromBuilderApi = GetSizeCommand.builder() .setApksArchivePath(apksArchiveFile) .setInstant(true) .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .build(); assertThat(fromFlags).isEqualTo(fromBuilderApi); } @Test public void getSizeTotalInternal_singleSplitVariant() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkAbiTargeting(X86, ImmutableSet.of(X86_64)), ZipPath.create("base-x86.apk"), /* isMasterSplit= */ false), createApkDescription( apkAbiTargeting(X86_64, ImmutableSet.of(X86)), ZipPath.create("base-x86_64.apk"), /* isMasterSplit= */ false))); ZipBuilder archiveBuilder = new ZipBuilder(); archiveBuilder.addFileWithContent(ZipPath.create("base-master.apk"), DUMMY_BYTES); archiveBuilder.addFileWithContent( ZipPath.create("base-x86.apk"), DUMMY_BYTES, EntryOption.UNCOMPRESSED); // APK stored uncompressed in the APKs zip. archiveBuilder.addFileWithContent(ZipPath.create("base-x86_64.apk"), new byte[10000]); archiveBuilder.addFileWithProtoContent( ZipPath.create("toc.pb"), BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .build()); Path apksArchiveFile = archiveBuilder.writeTo(tmpDir.resolve("bundle.apks")); ConfigurationSizes configurationSizes = GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .build() .getSizeTotalInternal(); assertThat(configurationSizes.getMinSizeConfigurationMap().keySet()) .containsExactly(SizeConfiguration.getDefaultInstance()); assertThat( configurationSizes .getMinSizeConfigurationMap() .get(SizeConfiguration.getDefaultInstance())) .isEqualTo(2 * compressedApkSize); // base+x86 assertThat(configurationSizes.getMaxSizeConfigurationMap().keySet()) .containsExactly(SizeConfiguration.getDefaultInstance()); assertThat( configurationSizes .getMaxSizeConfigurationMap() .get(SizeConfiguration.getDefaultInstance())) .isGreaterThan(2 * compressedApkSize); // base+x86_64 } @Test public void getSizeTotalInternal_multipleStandaloneVariant() throws Exception { Variant preLLdpiVariant = standaloneVariant( mergeVariantTargeting( variantSdkTargeting(sdkVersionFrom(1), ImmutableSet.of(sdkVersionFrom(21))), variantDensityTargeting(LDPI, ImmutableSet.of(MDPI))), ApkTargeting.getDefaultInstance(), ZipPath.create("preL-ldpi.apk")); Variant preLMdpiVariant = standaloneVariant( mergeVariantTargeting( variantSdkTargeting(sdkVersionFrom(1), ImmutableSet.of(sdkVersionFrom(21))), variantDensityTargeting(MDPI, ImmutableSet.of(LDPI))), ApkTargeting.getDefaultInstance(), ZipPath.create("preL-mdpi.apk")); ZipBuilder archiveBuilder = new ZipBuilder(); archiveBuilder.addFileWithContent(ZipPath.create("preL-ldpi.apk"), new byte[1000]); archiveBuilder.addFileWithContent(ZipPath.create("preL-mdpi.apk"), new byte[10]); archiveBuilder.addFileWithProtoContent( ZipPath.create("toc.pb"), BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(preLLdpiVariant) .addVariant(preLMdpiVariant) .build()); Path apksArchiveFile = archiveBuilder.writeTo(tmpDir.resolve("bundle.apks")); ConfigurationSizes configurationSizes = GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .build() .getSizeTotalInternal(); assertThat(configurationSizes.getMinSizeConfigurationMap().keySet()) .containsExactly(SizeConfiguration.getDefaultInstance()); assertThat( configurationSizes .getMinSizeConfigurationMap() .get(SizeConfiguration.getDefaultInstance())) .isLessThan(compressedApkSize); assertThat(configurationSizes.getMaxSizeConfigurationMap().keySet()) .containsExactly(SizeConfiguration.getDefaultInstance()); assertThat( configurationSizes .getMaxSizeConfigurationMap() .get(SizeConfiguration.getDefaultInstance())) .isGreaterThan(compressedApkSize); } @Test public void getSizeTotalInternal_withNoDimensionsAndDeviceSpec() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")))); Variant preLVariant = standaloneVariant( mergeVariantTargeting( variantSdkTargeting(sdkVersionFrom(1), ImmutableSet.of(sdkVersionFrom(21))), variantAbiTargeting(X86, ImmutableSet.of(ARMEABI_V7A, ARM64_V8A))), ApkTargeting.getDefaultInstance(), ZipPath.create("preL.apk")); ZipBuilder archiveBuilder = new ZipBuilder(); archiveBuilder.addFileWithContent(ZipPath.create("base-master.apk"), DUMMY_BYTES); archiveBuilder.addFileWithContent(ZipPath.create("preL.apk"), new byte[10000]); archiveBuilder.addFileWithProtoContent( ZipPath.create("toc.pb"), BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .addVariant(preLVariant) .build()); Path apksArchiveFile = archiveBuilder.writeTo(tmpDir.resolve("bundle.apks")); ConfigurationSizes configurationSizes = GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setDeviceSpec(DeviceSpec.newBuilder().setSdkVersion(21).build()) .build() .getSizeTotalInternal(); assertThat(configurationSizes.getMinSizeConfigurationMap()) .containsExactly( SizeConfiguration.getDefaultInstance(), compressedApkSize); // only split apk assertThat(configurationSizes.getMaxSizeConfigurationMap()) .containsExactly( SizeConfiguration.getDefaultInstance(), compressedApkSize); // only split apk } @Test public void getSizeTotalInternal_withDimensionsAndDeviceSpec() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkLanguageTargeting("jp"), ZipPath.create("base-jp.apk"), /* isMasterSplit= */ false), createApkDescription( apkLanguageTargeting("fr"), ZipPath.create("base-fr.apk"), /* isMasterSplit= */ false), createApkDescription( apkDensityTargeting(HDPI, ImmutableSet.of(LDPI)), ZipPath.create("base-hdpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkDensityTargeting(LDPI, ImmutableSet.of(HDPI)), ZipPath.create("base-ldpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkAbiTargeting(ARM64_V8A, ImmutableSet.of(ARMEABI_V7A)), ZipPath.create("base-arm64_v8a.apk"), /* isMasterSplit= */ false), createApkDescription( apkAbiTargeting(ARMEABI_V7A, ImmutableSet.of(ARM64_V8A)), ZipPath.create("base-armeabi_v7a.apk"), /* isMasterSplit= */ false))); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ConfigurationSizes configurationSizes = GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setDeviceSpec( DeviceSpec.newBuilder() .setSdkVersion(21) .setScreenDensity(125) .addSupportedLocales("jp") .build()) .setDimensions( ImmutableSet.of( Dimension.SDK, Dimension.ABI, Dimension.LANGUAGE, Dimension.SCREEN_DENSITY)) .build() .getSizeTotalInternal(); assertThat(configurationSizes.getMinSizeConfigurationMap()) .containsExactly( SizeConfiguration.builder() .setAbi("armeabi-v7a") .setLocale("jp") .setScreenDensity("125") .setSdkVersion("21") .build(), 4 * compressedApkSize, SizeConfiguration.builder() .setAbi("arm64-v8a") .setLocale("jp") .setScreenDensity("125") .setSdkVersion("21") .build(), 4 * compressedApkSize); assertThat(configurationSizes.getMaxSizeConfigurationMap()) .containsExactly( SizeConfiguration.builder() .setAbi("armeabi-v7a") .setLocale("jp") .setScreenDensity("125") .setSdkVersion("21") .build(), 4 * compressedApkSize, SizeConfiguration.builder() .setAbi("arm64-v8a") .setLocale("jp") .setScreenDensity("125") .setSdkVersion("21") .build(), 4 * compressedApkSize); } @Test public void getSizeTotalInternal_multipleDimensions() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), ZipPath.create("base-ldpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), ZipPath.create("base-mdpi.apk"), /* isMasterSplit= */ false))); Variant preLVariant = standaloneVariant( mergeVariantTargeting( variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(sdkVersionFrom(21))), variantAbiTargeting(ARMEABI, ImmutableSet.of(X86))), ApkTargeting.getDefaultInstance(), ZipPath.create("preL.apk")); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .addVariant(preLVariant) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ConfigurationSizes configurationSizes = GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setDimensions( ImmutableSet.of( Dimension.SDK, Dimension.ABI, Dimension.LANGUAGE, Dimension.SCREEN_DENSITY)) .build() .getSizeTotalInternal(); assertThat(configurationSizes.getMinSizeConfigurationMap()) .containsExactly( SizeConfiguration.builder() .setSdkVersion("21-") .setScreenDensity("LDPI") .build(), 2 * compressedApkSize, SizeConfiguration.builder() .setSdkVersion("21-") .setScreenDensity("MDPI") .build(), 2 * compressedApkSize, SizeConfiguration.builder() .setSdkVersion("15-20") .setAbi("armeabi") .build(), compressedApkSize); assertThat(configurationSizes.getMaxSizeConfigurationMap()) .containsExactly( SizeConfiguration.builder() .setSdkVersion("21-") .setScreenDensity("LDPI") .build(), 2 * compressedApkSize, SizeConfiguration.builder() .setSdkVersion("21-") .setScreenDensity("MDPI") .build(), 2 * compressedApkSize, SizeConfiguration.builder() .setSdkVersion("15-20") .setAbi("armeabi") .build(), compressedApkSize); } @Test public void getSizeTotal_noDimensions() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), ZipPath.create("base-ldpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), ZipPath.create("base-mdpi.apk"), /* isMasterSplit= */ false)), createSplitApkSet( /* moduleName= */ "feature1", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-feature1.apk")))); Variant preLVariant = standaloneVariant( mergeVariantTargeting( variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(sdkVersionFrom(21)))), ApkTargeting.getDefaultInstance(), ZipPath.create("preL.apk")); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .addVariant(preLVariant) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .build() .getSizeTotal(new PrintStream(outputStream)); assertThat(new String(outputStream.toByteArray(), UTF_8)) .isEqualTo( "MIN,MAX" + CRLF + String.format("%d,%d", compressedApkSize, 3 * compressedApkSize) + CRLF); } @Test public void getSizeTotal_withSelectModules() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk"))), createSplitApkSet( /* moduleName= */ "feature1", DeliveryType.ON_DEMAND, /* moduleDependencies= */ ImmutableList.of(), createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-feature1.apk"))), createSplitApkSet( /* moduleName= */ "feature2", DeliveryType.ON_DEMAND, /* moduleDependencies= */ ImmutableList.of("feature3"), createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-feature2.apk"))), createSplitApkSet( /* moduleName= */ "feature3", DeliveryType.ON_DEMAND, /* moduleDependencies= */ ImmutableList.of(), createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-feature3.apk")))); Variant preLVariant = standaloneVariant( mergeVariantTargeting( variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(sdkVersionFrom(21)))), ApkTargeting.getDefaultInstance(), ZipPath.create("preL.apk")); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .addVariant(preLVariant) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setModules(ImmutableSet.of("base", "feature2")) .build() .getSizeTotal(new PrintStream(outputStream)); // base, feature2, feature 3 modules are selected and standalone variants are skipped. assertThat(new String(outputStream.toByteArray(), UTF_8)) .isEqualTo( "MIN,MAX" + CRLF + String.format("%d,%d", 3 * compressedApkSize, 3 * compressedApkSize) + CRLF); } @Test public void getSizeTotal_withInstant() throws Exception { Variant lInstantVariant = createVariant( variantSdkTargeting( sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), createInstantApkSet( "base", ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createInstantApkSet( "instant", ApkTargeting.getDefaultInstance(), ZipPath.create("instant-master.apk"))); Variant lSplitVariant = createVariant( variantSdkTargeting( sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), createSplitApkSet( "other", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("other-master.apk")))); Variant preLVariant = standaloneVariant( mergeVariantTargeting( variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(sdkVersionFrom(21)))), ApkTargeting.getDefaultInstance(), ZipPath.create("preL.apk")); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lInstantVariant) .addVariant(lSplitVariant) .addVariant(preLVariant) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setInstant(true) .build() .getSizeTotal(new PrintStream(outputStream)); // only instant split variant is selected assertThat(new String(outputStream.toByteArray(), UTF_8)) .isEqualTo( "MIN,MAX" + CRLF + String.format("%d,%d", 2 * compressedApkSize, 2 * compressedApkSize) + CRLF); } @Test public void getSizeTotal_multipleDimensions() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkAbiTargeting(MIPS64, ImmutableSet.of(MIPS)), ZipPath.create("base-mips64.apk"), /* isMasterSplit= */ false), createApkDescription( apkAbiTargeting(MIPS, ImmutableSet.of(MIPS64)), ZipPath.create("base-mips.apk"), /* isMasterSplit= */ false))); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setDimensions( ImmutableSet.of( Dimension.SDK, Dimension.ABI, Dimension.LANGUAGE, Dimension.SCREEN_DENSITY)) .build() .getSizeTotal(new PrintStream(outputStream)); ImmutableList<String> csvRows = ImmutableList.copyOf(new String(outputStream.toByteArray(), UTF_8).split(CRLF)); assertThat(csvRows) .containsExactly( "SDK,ABI,SCREEN_DENSITY,LANGUAGE,MIN,MAX", String.format("21-,mips64,,,%d,%d", 2 * compressedApkSize, 2 * compressedApkSize), String.format("21-,mips,,,%d,%d", 2 * compressedApkSize, 2 * compressedApkSize)); } @Test public void getSizeTotal_withDimensionsAndDeviceSpec() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkDensityTargeting(LDPI, ImmutableSet.of(MDPI)), ZipPath.create("base-ldpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkDensityTargeting(MDPI, ImmutableSet.of(LDPI)), ZipPath.create("base-mdpi.apk"), /* isMasterSplit= */ false), createApkDescription( apkAbiTargeting(X86, ImmutableSet.of(X86_64)), ZipPath.create("base-x86.apk"), /* isMasterSplit= */ false), createApkDescription( apkAbiTargeting(X86_64, ImmutableSet.of(X86)), ZipPath.create("base-x86_64.apk"), /* isMasterSplit= */ false))); Variant preLVariant = standaloneVariant( mergeVariantTargeting( variantSdkTargeting(sdkVersionFrom(15), ImmutableSet.of(sdkVersionFrom(21))), variantAbiTargeting(X86), variantDensityTargeting(LDPI)), ApkTargeting.getDefaultInstance(), ZipPath.create("preL.apk")); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .addVariant(preLVariant) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setDeviceSpec( DeviceSpec.newBuilder().setScreenDensity(124).addSupportedAbis("x86").build()) .setDimensions(ImmutableSet.of(Dimension.ABI, Dimension.SCREEN_DENSITY)) .build() .getSizeTotal(new PrintStream(outputStream)); assertThat(new String(outputStream.toByteArray(), UTF_8)) .isEqualTo( "ABI,SCREEN_DENSITY,MIN,MAX" + CRLF + String.format("x86,124,%d,%d", compressedApkSize, 3 * compressedApkSize) + CRLF); } @Test public void getSizeTotal_withAssetModules() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkAbiTargeting(X86, ImmutableSet.of(X86_64)), ZipPath.create("base-x86.apk"), /* isMasterSplit= */ false), createApkDescription( apkAbiTargeting(X86_64, ImmutableSet.of(X86)), ZipPath.create("base-x86_64.apk"), /* isMasterSplit= */ false))); AssetSliceSet assetModule = createAssetSliceSet( /* moduleName= */ "asset1", DeliveryType.INSTALL_TIME, createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("asset1-master.apk")), createApkDescription( apkTextureTargeting(ETC2, ImmutableSet.of(ASTC)), ZipPath.create("asset1-tcf_etc2.apk"), /* isMasterSplit= */ false), createApkDescription( apkTextureTargeting(ASTC, ImmutableSet.of(ETC2)), ZipPath.create("asset1-tcf_astc.apk"), /* isMasterSplit= */ false)); // Only install-time asset modules are counted towards the size. AssetSliceSet ignoredOnDemandAssetModule = createAssetSliceSet( /* moduleName= */ "asset2", DeliveryType.ON_DEMAND, createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("asset2-master.apk"))); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .addAssetSliceSet(assetModule) .addAssetSliceSet(ignoredOnDemandAssetModule) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setDimensions( ImmutableSet.of(Dimension.ABI, Dimension.TEXTURE_COMPRESSION_FORMAT, Dimension.SDK)) .build() .getSizeTotal(new PrintStream(outputStream)); assertThat(new String(outputStream.toByteArray(), UTF_8).split(CRLF)) .asList() .containsExactly( "SDK,ABI,TEXTURE_COMPRESSION_FORMAT,MIN,MAX", String.format( "%s,%s,%s,%d,%d", "21-", "x86_64", "astc", 4 * compressedApkSize, 4 * compressedApkSize), String.format( "%s,%s,%s,%d,%d", "21-", "x86_64", "etc2", 4 * compressedApkSize, 4 * compressedApkSize), String.format( "%s,%s,%s,%d,%d", "21-", "x86", "astc", 4 * compressedApkSize, 4 * compressedApkSize), String.format( "%s,%s,%s,%d,%d", "21-", "x86", "etc2", 4 * compressedApkSize, 4 * compressedApkSize)); } @Test public void getSizeTotal_withAssetModulesAndDeviceSpec() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), createApkDescription( apkAbiTargeting(X86, ImmutableSet.of(X86_64)), ZipPath.create("base-x86.apk"), /* isMasterSplit= */ false), createApkDescription( apkAbiTargeting(X86_64, ImmutableSet.of(X86)), ZipPath.create("base-x86_64.apk"), /* isMasterSplit= */ false))); AssetSliceSet assetModule = createAssetSliceSet( /* moduleName= */ "asset1", DeliveryType.INSTALL_TIME, createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("asset1-master.apk")), createApkDescription( apkTextureTargeting(ETC2, ImmutableSet.of(ASTC)), ZipPath.create("asset1-tcf_etc2.apk"), /* isMasterSplit= */ false), createApkDescription( apkTextureTargeting(ASTC, ImmutableSet.of(ETC2)), ZipPath.create("asset1-tcf_astc.apk"), /* isMasterSplit= */ false)); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .addAssetSliceSet(assetModule) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setDimensions( ImmutableSet.of(Dimension.ABI, Dimension.TEXTURE_COMPRESSION_FORMAT, Dimension.SDK)) .setDeviceSpec( DeviceSpec.newBuilder() .setSdkVersion(25) .addSupportedAbis("x86") .addGlExtensions("GL_KHR_texture_compression_astc_ldr") .build()) .build() .getSizeTotal(new PrintStream(outputStream)); assertThat(new String(outputStream.toByteArray(), UTF_8)) .isEqualTo( "SDK,ABI,TEXTURE_COMPRESSION_FORMAT,MIN,MAX" + CRLF + String.format( "%s,%s,%s,%d,%d", "25", "x86", "astc", 4 * compressedApkSize, 4 * compressedApkSize) + CRLF); } @Test public void getSizeTotal_withAssetModulesAndMultipleVariants() throws Exception { Variant lVariant = createVariant( lPlusVariantTargeting(), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")))); Variant nVariant = createVariant( variantSdkTargeting(24), createSplitApkSet( /* moduleName= */ "base", createMasterApkDescription( ApkTargeting.getDefaultInstance(), ZipPath.create("base-master_2.apk")))); AssetSliceSet assetModule = createAssetSliceSet( /* moduleName= */ "asset1", DeliveryType.INSTALL_TIME, createMasterApkDescription( apkSdkTargeting(sdkVersionFrom(21)), ZipPath.create("asset1-master.apk")), createApkDescription( mergeApkTargeting( apkTextureTargeting(ETC2, ImmutableSet.of(ASTC)), apkSdkTargeting(sdkVersionFrom(21))), ZipPath.create("asset1-tcf_etc2.apk"), /* isMasterSplit= */ false), createApkDescription( mergeApkTargeting( apkTextureTargeting(ASTC, ImmutableSet.of(ETC2)), apkSdkTargeting(sdkVersionFrom(21))), ZipPath.create("asset1-tcf_astc.apk"), /* isMasterSplit= */ false)); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( Bundletool.newBuilder() .setVersion(BundleToolVersion.getCurrentVersion().toString())) .addVariant(lVariant) .addVariant(nVariant) .addAssetSliceSet(assetModule) .build(); Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GetSizeCommand.builder() .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) .setApksArchivePath(apksArchiveFile) .setDimensions( ImmutableSet.of(Dimension.TEXTURE_COMPRESSION_FORMAT, Dimension.ABI, Dimension.SDK)) .build() .getSizeTotal(new PrintStream(outputStream)); assertThat(new String(outputStream.toByteArray(), UTF_8).split(CRLF)) .asList() .containsExactly( "SDK,ABI,TEXTURE_COMPRESSION_FORMAT,MIN,MAX", String.format( "%s,,%s,%d,%d", "21-", "etc2", 3 * compressedApkSize, 3 * compressedApkSize), String.format( "%s,,%s,%d,%d", "21-", "astc", 3 * compressedApkSize, 3 * compressedApkSize), String.format( "%s,,%s,%d,%d", "24-", "etc2", 3 * compressedApkSize, 3 * compressedApkSize), String.format( "%s,,%s,%d,%d", "24-", "astc", 3 * compressedApkSize, 3 * compressedApkSize)); } /** Copies the testdata resource into the temporary directory. */ private Path copyToTempDir(String testDataPath) throws Exception { Path testDataFilename = Paths.get(testDataPath).getFileName(); Path outputFile = tmp.newFolder().toPath().resolve(testDataFilename); try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile.toFile())) { ByteStreams.copy(TestData.openStream(testDataPath), fileOutputStream); } return outputFile; } }