blob: 461b55ffe8e8e975ac0f46b2b2157292a206675e [file] [log] [blame]
/*
* 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.build.gradle.integration.application
import com.android.SdkConstants
import com.android.build.gradle.integration.common.fixture.GradleTestProject
import com.android.build.gradle.integration.common.fixture.app.MinimalSubProject
import com.android.build.gradle.integration.common.fixture.app.MultiModuleTestProject
import com.android.build.gradle.integration.common.runner.FilterableParameterized
import com.android.build.gradle.integration.common.truth.ModelContainerSubject
import com.android.build.gradle.integration.common.truth.TruthHelper.assertThat
import com.android.build.gradle.integration.common.utils.TestFileUtils
import com.android.build.gradle.integration.common.utils.getOutputByName
import com.android.build.gradle.internal.scope.CodeShrinker
import com.android.build.gradle.options.OptionalBooleanOption
import com.android.builder.model.AppBundleProjectBuildOutput
import com.android.builder.model.AppBundleVariantBuildOutput
import com.android.builder.model.SyncIssue
import com.android.testutils.TestInputsGenerator
import com.android.testutils.apk.Dex
import com.android.testutils.apk.Zip
import com.android.testutils.truth.DexSubject.assertThat
import com.android.testutils.truth.FileSubject
import com.android.testutils.truth.FileSubject.assertThat
import com.android.utils.FileUtils
import com.android.utils.Pair
import com.google.common.truth.Truth
import org.junit.Assume
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.nio.file.Files
import kotlin.test.fail
/**
* Tests using Proguard/R8 to shrink and obfuscate code in a project with features.
*
* Project roughly structured as follows (see implementation below for exact structure) :
*
* <pre>
* ---> library2 ------>
* otherFeature1 ---> library3 library1
* ---> baseModule ---->
* otherFeature2
*
* More explicitly,
* instantApp depends on otherFeature1, otherFeature2, baseModule (not pictured)
* app depends on otherFeature1, otherFeature2, baseModule (not pictured)
* otherFeature1 depends on library2, library3, baseModule
* otherFeature2 depends on baseModule
* baseModule depends on library1
* library2 depends on library1
* </pre>
*/
@RunWith(FilterableParameterized::class)
class MinifyFeaturesTest(
val codeShrinker: CodeShrinker,
val multiApkMode: MultiApkMode) {
enum class MultiApkMode {
DYNAMIC_APP, INSTANT_APP
}
companion object {
@JvmStatic
@Parameterized.Parameters(name = "codeShrinker {0}, {1}")
fun getConfigurations(): Collection<Array<Enum<*>>> =
listOf(
arrayOf(CodeShrinker.PROGUARD, MultiApkMode.DYNAMIC_APP),
arrayOf(CodeShrinker.PROGUARD, MultiApkMode.INSTANT_APP),
arrayOf(CodeShrinker.R8, MultiApkMode.DYNAMIC_APP),
arrayOf(CodeShrinker.R8, MultiApkMode.INSTANT_APP)
)
}
private val otherFeature2GradlePath = ":otherFeature2"
private val lib1 =
MinimalSubProject.lib("com.example.lib1")
.appendToBuild("""
android {
buildTypes {
minified.initWith(buildTypes.debug)
minified {
consumerProguardFiles "proguard-rules.pro"
}
}
}
""")
.withFile("src/main/resources/lib1_java_res.txt", "lib1")
.withFile(
"src/main/java/com/example/lib1/Lib1Class.java",
"""package com.example.lib1;
import java.io.InputStream;
public class Lib1Class {
public String getJavaRes() {
InputStream inputStream =
Lib1Class.class
.getClassLoader()
.getResourceAsStream("lib1_java_res.txt");
if (inputStream == null) {
return "can't find lib1_java_res";
}
byte[] line = new byte[1024];
try {
inputStream.read(line);
return new String(line, "UTF-8").trim();
} catch (Exception ignore) {
}
return "something went wrong";
}
}""")
.withFile(
"src/main/java/com/example/lib1/EmptyClassToKeep.java",
"""package com.example.lib1;
public class EmptyClassToKeep {
}""")
.withFile(
"src/main/java/com/example/lib1/EmptyClassToRemove.java",
"""package com.example.lib1;
public class EmptyClassToRemove {
}""")
.withFile(
"proguard-rules.pro",
"""-keep public class com.example.lib1.EmptyClassToKeep""")
private val lib2 =
MinimalSubProject.lib("com.example.lib2")
.appendToBuild("""
android {
buildTypes {
minified.initWith(buildTypes.debug)
minified {
consumerProguardFiles "proguard-rules.pro"
}
}
}
""")
// include foo_view.xml and FooView.java below to generate aapt proguard rules to be
// merged in the base.
.withFile(
"src/main/res/layout/foo_view.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<view
xmlns:android="http://schemas.android.com/apk/res/android"
class="com.example.lib2.FooView"
android:id="@+id/foo_view" />"""
)
.withFile(
"src/main/java/com/example/lib2/FooView.java",
"""package com.example.lib2;
import android.content.Context;
import android.view.View;
public class FooView extends View {
public FooView(Context context) {
super(context);
}
}""")
.withFile("src/main/resources/lib2_java_res.txt", "lib2")
.withFile(
"src/main/java/com/example/lib2/Lib2Class.java",
"""package com.example.lib2;
import java.io.InputStream;
public class Lib2Class {
public String getJavaRes() {
InputStream inputStream =
Lib2Class.class
.getClassLoader()
.getResourceAsStream("lib2_java_res.txt");
if (inputStream == null) {
return "can't find lib2_java_res";
}
byte[] line = new byte[1024];
try {
inputStream.read(line);
return new String(line, "UTF-8").trim();
} catch (Exception ignore) {
}
return "something went wrong";
}
}""")
.withFile(
"src/main/java/com/example/lib2/EmptyClassToKeep.java",
"""package com.example.lib2;
public class EmptyClassToKeep {
}""")
.withFile(
"src/main/java/com/example/lib2/EmptyClassToRemove.java",
"""package com.example.lib2;
public class EmptyClassToRemove {
}""")
.withFile(
"proguard-rules.pro",
"""-keep public class com.example.lib2.EmptyClassToKeep""")
private val lib3 =
MinimalSubProject.lib("com.example.lib3")
.appendToBuild("android { buildTypes { minified { initWith(buildTypes.debug) }}}")
private val baseModule =
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP ->
MinimalSubProject.app("com.example.baseModule")
.appendToBuild(
"""
android {
dynamicFeatures = [':foo:otherFeature1', '$otherFeature2GradlePath']
buildTypes {
minified.initWith(buildTypes.debug)
minified {
minifyEnabled true
useProguard ${codeShrinker == CodeShrinker.PROGUARD}
proguardFiles getDefaultProguardFile('proguard-android.txt'),
"proguard-rules.pro"
}
}
}
"""
)
MultiApkMode.INSTANT_APP ->
MinimalSubProject.feature("com.example.baseModule")
.appendToBuild(
"""
android {
baseFeature true
buildTypes {
minified.initWith(buildTypes.debug)
minified {
minifyEnabled true
useProguard ${codeShrinker == CodeShrinker.PROGUARD}
proguardFiles getDefaultProguardFile('proguard-android.txt')
consumerProguardFiles "proguard-rules.pro"
}
}
}
"""
)
}.let {
it
.withFile(
"src/main/AndroidManifest.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.baseModule">
<application android:label="app_name">
<activity android:name=".Main"
android:label="app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>"""
)
.withFile(
"src/main/res/layout/base_main.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Base"
android:id="@+id/text" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text=""
android:id="@+id/extraText" />
</LinearLayout>"""
)
.withFile(
"src/main/res/values/string.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="otherFeature1">otherFeature1</string>
<string name="otherFeature2">otherFeature2</string>
</resources>"""
)
.withFile("src/main/resources/base_java_res.txt", "base")
.withFile(
"src/main/java/com/example/baseModule/Main.java",
"""package com.example.baseModule;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import java.lang.Exception;
import java.lang.RuntimeException;
import com.example.lib1.Lib1Class;
public class Main extends Activity {
private int foo = 1234;
private final StringProvider stringProvider = new StringProvider();
private final Lib1Class lib1Class = new Lib1Class();
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.base_main);
TextView tv = (TextView) findViewById(R.id.extraText);
tv.setText(
""
+ getLib1Class().getJavaRes()
+ " "
+ getStringProvider().getString(foo));
}
public StringProvider getStringProvider() {
return stringProvider;
}
public Lib1Class getLib1Class() {
return lib1Class;
}
public void handleOnClick(android.view.View view) {
// This method should be kept by the default ProGuard rules.
}
}"""
)
.withFile(
"src/main/java/com/example/baseModule/StringProvider.java",
"""package com.example.baseModule;
public class StringProvider {
public String getString(int foo) {
return Integer.toString(foo);
}
}"""
)
.withFile(
"src/main/java/com/example/baseModule/EmptyClassToKeep.java",
"""package com.example.baseModule;
public class EmptyClassToKeep {
}"""
)
.withFile(
"src/main/java/com/example/baseModule/EmptyClassToRemove.java",
"""package com.example.baseModule;
public class EmptyClassToRemove {
}"""
)
.withFile(
"proguard-rules.pro",
"""-keep public class com.example.baseModule.EmptyClassToKeep"""
)
}
private val otherFeature1 =
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP ->
MinimalSubProject.dynamicFeature("com.example.otherFeature1")
MultiApkMode.INSTANT_APP -> MinimalSubProject.feature("com.example.otherFeature1")
}.let { minimalSubProject ->
val proguardFilesDsl =
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> "proguardFiles"
MultiApkMode.INSTANT_APP -> "consumerProguardFiles"
}
minimalSubProject
.appendToBuild(
"""
android {
buildTypes {
minified.initWith(buildTypes.debug)
minified {
$proguardFilesDsl "proguard-rules.pro"
}
}
}
"""
)
.withFile(
"src/main/AndroidManifest.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.otherFeature1">
<dist:module dist:onDemand="true"
dist:title="@string/otherFeature1">
<dist:fusing dist:include="true"/>
</dist:module>
<application android:label="app_name">
<activity android:name=".Main"
android:label="app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>"""
)
.withFile(
"src/main/res/layout/other_main_1.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Other Feature 1"
android:id="@+id/text" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text=""
android:id="@+id/extraText" />
</LinearLayout>"""
)
.withFile("src/main/resources/other_java_res_1.txt", "other")
.withFile(
"src/main/java/com/example/otherFeature1/Main.java",
"""package com.example.otherFeature1;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import java.lang.Exception;
import java.lang.RuntimeException;
import com.example.baseModule.StringProvider;
import com.example.lib2.Lib2Class;
public class Main extends Activity {
private int foo = 1234;
private final StringProvider stringProvider = new StringProvider();
private final Lib2Class lib2Class = new Lib2Class();
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.other_main_1);
TextView tv = (TextView) findViewById(R.id.extraText);
tv.setText(
""
+ getLib2Class().getJavaRes()
+ " "
+ getStringProvider().getString(foo));
}
public StringProvider getStringProvider() {
return stringProvider;
}
public Lib2Class getLib2Class() {
return lib2Class;
}
public void handleOnClick(android.view.View view) {
// This method should be kept by the default ProGuard rules.
}
}"""
)
.withFile(
"src/main/java/com/example/otherFeature1/EmptyClassToKeep.java",
"""package com.example.otherFeature1;
public class EmptyClassToKeep {
}"""
)
.withFile(
"src/main/java/com/example/otherFeature1/EmptyClassToRemove.java",
"""package com.example.otherFeature1;
public class EmptyClassToRemove {
}"""
)
.withFile(
"proguard-rules.pro",
"""-keep public class com.example.otherFeature1.EmptyClassToKeep"""
)
}
private val otherFeature2 =
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP ->
MinimalSubProject.dynamicFeature("com.example.otherFeature2")
MultiApkMode.INSTANT_APP -> MinimalSubProject.feature("com.example.otherFeature2")
}.let {
it
.appendToBuild("""
android {
buildTypes {
minified.initWith(buildTypes.debug)
}
}
""")
.withFile(
"src/main/AndroidManifest.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.otherFeature2">
<dist:module dist:onDemand="true"
dist:title="@string/otherFeature2">
<dist:fusing dist:include="true"/>
</dist:module>
<application android:label="app_name">
<activity android:name=".Main"
android:label="app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>"""
)
.withFile(
"src/main/res/layout/other_main_2.xml",
"""<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Other Feature 2"
android:id="@+id/text" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text=""
android:id="@+id/extraText" />
</LinearLayout>"""
)
.withFile("src/main/resources/other_java_res_2.txt", "other")
.withFile(
"src/main/java/com/example/otherFeature2/Main.java",
"""package com.example.otherFeature2;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import java.lang.Exception;
import java.lang.RuntimeException;
import com.example.baseModule.StringProvider;
public class Main extends Activity {
private int foo = 1234;
private final StringProvider stringProvider = new StringProvider();
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.other_main_2);
TextView tv = (TextView) findViewById(R.id.extraText);
tv.setText(getStringProvider().getString(foo));
}
public StringProvider getStringProvider() {
return stringProvider;
}
public void handleOnClick(android.view.View view) {
// This method should be kept by the default ProGuard rules.
}
}"""
)
}
private val app =
MinimalSubProject.app("com.example.app")
.appendToBuild("""
android {
buildTypes {
minified.initWith(buildTypes.debug)
minified {
minifyEnabled true
useProguard ${codeShrinker == CodeShrinker.PROGUARD}
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
}
}
""")
private val instantApp = MinimalSubProject.instantApp()
.appendToBuild("""
android {
buildTypes {
minified
}
}
""".trimIndent())
private val testApp =
MultiModuleTestProject.builder()
.subproject(":lib1", lib1)
.subproject(":lib2", lib2)
.subproject(":lib3", lib3)
.subproject(":baseModule", baseModule)
.subproject(":foo:otherFeature1", otherFeature1)
.subproject(otherFeature2GradlePath, otherFeature2)
.dependency(otherFeature1, lib2)
// otherFeature1 depends on lib3 to test having multiple library module dependencies.
.dependency(otherFeature1, lib3)
.dependency(otherFeature1, baseModule)
.dependency(otherFeature2, baseModule)
.dependency("api", lib2, lib1)
.dependency(baseModule, lib1)
.let {
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> it
MultiApkMode.INSTANT_APP ->
it
.subproject(":app", app)
.subproject(":instantApp", instantApp)
.dependency(app, baseModule)
.dependency(app, otherFeature1)
.dependency(app, otherFeature2)
.dependency(instantApp, baseModule)
.dependency(instantApp, otherFeature1)
.dependency(instantApp, otherFeature2)
.dependency("application", baseModule, app)
.dependency("feature", baseModule, otherFeature1)
.dependency("feature", baseModule, otherFeature2)
}
}
.build()
@get:Rule
val project = GradleTestProject.builder().fromTestApp(testApp).create()
@get:Rule
val temporaryFolder = TemporaryFolder()
@Test
fun testApksAreMinified() {
val apkType = object : GradleTestProject.ApkType {
override fun getBuildType() = "minified"
override fun getTestName(): String? = null
override fun isSigned() = true
}
val executor = project.executor()
.with(OptionalBooleanOption.ENABLE_R8, codeShrinker == CodeShrinker.R8)
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> executor.run("assembleMinified")
MultiApkMode.INSTANT_APP -> executor.run("app:assembleMinified", "instantApp:assembleMinified")
}
// check aapt_rules.txt merging
val aaptProguardFile =
FileUtils.join(
project.getSubproject("baseModule").intermediatesDir,
"aapt_proguard_file",
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> "minified"
MultiApkMode.INSTANT_APP -> "minifiedFeature"
},
SdkConstants.FN_AAPT_RULES)
assertThat(aaptProguardFile).exists()
assertThat(aaptProguardFile)
.doesNotContain("-keep class com.example.lib2.FooView")
val mergedAaptProguardFile =
FileUtils.join(
project.getSubproject("baseModule").intermediatesDir,
"merged_aapt_proguard_file",
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> "minified"
MultiApkMode.INSTANT_APP -> "minifiedFeature"
},
SdkConstants.FN_MERGED_AAPT_RULES)
assertThat(mergedAaptProguardFile).exists()
assertThat(mergedAaptProguardFile)
.contains("-keep class com.example.lib2.FooView")
project.getSubproject("baseModule")
.let {
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> it.getApk(apkType)
MultiApkMode.INSTANT_APP -> it.getFeatureApk(apkType)
}
}.use { apk ->
assertThat(apk.file.toFile()).exists()
assertThat(apk).containsClass("Lcom/example/baseModule/Main;")
assertThat(apk).containsClass("Lcom/example/baseModule/a;")
assertThat(apk).containsClass("Lcom/example/baseModule/EmptyClassToKeep;")
assertThat(apk).containsClass("Lcom/example/lib1/EmptyClassToKeep;")
assertThat(apk).containsClass("Lcom/example/lib1/a;")
assertThat(apk).containsJavaResource("base_java_res.txt")
assertThat(apk).containsJavaResource("other_java_res_1.txt")
assertThat(apk).containsJavaResource("other_java_res_2.txt")
assertThat(apk).containsJavaResource("lib1_java_res.txt")
assertThat(apk).containsJavaResource("lib2_java_res.txt")
assertThat(apk).doesNotContainClass("Lcom/example/baseFeature/EmptyClassToRemove;")
assertThat(apk).doesNotContainClass("Lcom/example/lib1/EmptyClassToRemove;")
assertThat(apk).doesNotContainClass("Lcom/example/lib2/EmptyClassKeep;")
assertThat(apk).doesNotContainClass("Lcom/example/lib2/Lib2Class;")
assertThat(apk).doesNotContainClass("Lcom/example/lib2/a;")
assertThat(apk).doesNotContainClass("Lcom/example/otherFeature1/Main;")
assertThat(apk).doesNotContainClass("Lcom/example/otherFeature2/Main;")
}
project.getSubproject(":foo:otherFeature1")
.let {
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> it.getApk(apkType)
MultiApkMode.INSTANT_APP -> it.getFeatureApk(apkType)
}
}.use { apk ->
assertThat(apk.file.toFile()).exists()
assertThat(apk).containsClass("Lcom/example/otherFeature1/Main;")
assertThat(apk).containsClass(
"Lcom/example/otherFeature1/EmptyClassToKeep;"
)
assertThat(apk).containsClass("Lcom/example/lib2/EmptyClassToKeep;")
assertThat(apk).containsClass("Lcom/example/lib2/FooView;")
assertThat(apk).containsClass("Lcom/example/lib2/a;")
assertThat(apk).doesNotContainJavaResource("other_java_res_1.txt")
assertThat(apk).doesNotContainClass(
"Lcom/example/otherFeature1/EmptyClassToRemove;"
)
assertThat(apk).doesNotContainClass("Lcom/example/lib2/EmptyClassToRemove;")
assertThat(apk).doesNotContainClass("Lcom/example/lib1/EmptyClassToKeep;")
assertThat(apk).doesNotContainClass("Lcom/example/lib1/Lib1Class;")
assertThat(apk).doesNotContainClass("Lcom/example/lib1/a;")
assertThat(apk).doesNotContainClass("Lcom/example/baseModule/Main;")
assertThat(apk).doesNotContainClass("Lcom/example/otherFeature2/Main;")
}
project.getSubproject(otherFeature2GradlePath)
.let {
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> it.getApk(apkType)
MultiApkMode.INSTANT_APP -> it.getFeatureApk(apkType)
}
}.use { apk ->
assertThat(apk.file.toFile()).exists()
assertThat(apk).containsClass("Lcom/example/otherFeature2/Main;")
assertThat(apk).doesNotContainJavaResource("other_java_res_2.txt")
assertThat(apk).doesNotContainClass("Lcom/example/lib1/EmptyClassToKeep;")
assertThat(apk).doesNotContainClass("Lcom/example/lib2/EmptyClassToKeep;")
assertThat(apk).doesNotContainClass("Lcom/example/baseModule/Main;")
assertThat(apk).doesNotContainClass("Lcom/example/otherFeature1/Main;")
}
if (multiApkMode == MultiApkMode.INSTANT_APP) {
project.getSubproject("app").getApk(apkType).use { apk ->
assertThat(apk.file.toFile()).exists()
assertThat(apk).containsClass("Lcom/example/baseModule/Main;")
assertThat(apk).containsClass("Lcom/example/baseModule/EmptyClassToKeep;")
assertThat(apk).containsClass("Lcom/example/lib1/EmptyClassToKeep;")
assertThat(apk).containsClass("Lcom/example/otherFeature1/Main;")
assertThat(apk).containsClass("Lcom/example/otherFeature1/EmptyClassToKeep;")
assertThat(apk).containsClass("Lcom/example/lib2/EmptyClassToKeep;")
assertThat(apk).containsClass("Lcom/example/lib2/FooView;")
assertThat(apk).containsClass("Lcom/example/otherFeature2/Main;")
assertThat(apk).doesNotContainClass("Lcom/example/baseModule/EmptyClassToRemove;")
assertThat(apk).doesNotContainClass("Lcom/example/lib2/EmptyClassToRemove;")
assertThat(apk).doesNotContainClass("Lcom/example/lib1/EmptyClassToRemove;")
}
}
}
@Test
fun testBundleIsMinified() {
Assume.assumeTrue(multiApkMode == MultiApkMode.DYNAMIC_APP)
val executor = project.executor()
.with(OptionalBooleanOption.ENABLE_R8, codeShrinker == CodeShrinker.R8)
executor.run("bundleMinified")
val bundleFile = getApkFolderOutput("minified", ":baseModule").bundleFile
FileSubject.assertThat(bundleFile).exists()
Zip(bundleFile).use {
// check that java resources are packaged as expected
val expectedJavaRes = listOf(
"/base/root/base_java_res.txt",
"/base/root/lib1_java_res.txt",
"/base/root/lib2_java_res.txt",
"/base/root/other_java_res_1.txt",
"/base/root/other_java_res_2.txt"
)
Truth.assertThat(it.entries.map { it.toString() }).containsAllIn(expectedJavaRes)
// check base dex
val baseDex = Dex(it.getEntry("base/dex/classes.dex")!!)
assertThat(baseDex).containsClasses(
"Lcom/example/baseModule/Main;",
"Lcom/example/baseModule/a;",
"Lcom/example/baseModule/EmptyClassToKeep;",
"Lcom/example/lib1/EmptyClassToKeep;",
"Lcom/example/lib1/a;"
)
assertThat(baseDex).doesNotContainClasses(
"Lcom/example/baseFeature/EmptyClassToRemove;",
"Lcom/example/lib1/EmptyClassToRemove;",
"Lcom/example/lib2/EmptyClassKeep;",
"Lcom/example/lib2/Lib2Class;",
"Lcom/example/lib2/a;",
"Lcom/example/otherFeature1/Main;",
"Lcom/example/otherFeature2/Main;"
)
// check otherFeature1 dex
val otherFeature1Dex = Dex(it.getEntry("otherFeature1/dex/classes.dex")!!)
assertThat(otherFeature1Dex).containsClasses(
"Lcom/example/otherFeature1/Main;",
"Lcom/example/otherFeature1/EmptyClassToKeep;",
"Lcom/example/lib2/EmptyClassToKeep;",
"Lcom/example/lib2/FooView;",
"Lcom/example/lib2/a;"
)
assertThat(otherFeature1Dex).doesNotContainClasses(
"Lcom/example/otherFeature1/EmptyClassToRemove;",
"Lcom/example/lib2/EmptyClassToRemove;",
"Lcom/example/lib1/EmptyClassToKeep;",
"Lcom/example/lib1/Lib1Class;",
"Lcom/example/lib1/a;",
"Lcom/example/baseModule/Main;",
"Lcom/example/otherFeature2/Main;"
)
// check otherFeature2 dex
val otherFeature2Dex = Dex(it.getEntry("otherFeature2/dex/classes.dex")!!)
assertThat(otherFeature2Dex).containsClasses(
"Lcom/example/otherFeature2/Main;"
)
assertThat(otherFeature2Dex).doesNotContainClasses(
"Lcom/example/lib1/EmptyClassToKeep;",
"Lcom/example/lib2/EmptyClassToKeep;",
"Lcom/example/baseModule/Main;",
"Lcom/example/otherFeature1/Main;"
)
}
}
@Test
fun testMinifyEnabledSyncError() {
Assume.assumeTrue(codeShrinker == CodeShrinker.R8)
project.getSubproject(":foo:otherFeature1")
.buildFile
.appendText("android.buildTypes.minified.minifyEnabled true")
val container = project.model().ignoreSyncIssues().fetchAndroidProjects()
ModelContainerSubject.assertThat(container).rootBuild().project(":foo:otherFeature1")
.hasSingleError(SyncIssue.TYPE_GENERIC)
.that()
.hasMessageThatContains("cannot set minifyEnabled to true.")
}
@Test
fun testDefaultProguardFilesSyncError() {
Assume.assumeTrue(codeShrinker == CodeShrinker.R8)
project.getSubproject(otherFeature2GradlePath)
.buildFile
.appendText(
"""
android {
buildTypes {
minified {
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
}
}
"""
)
val container = project.model().ignoreSyncIssues().fetchAndroidProjects()
ModelContainerSubject.assertThat(container).rootBuild().project(otherFeature2GradlePath)
.hasSingleError(SyncIssue.TYPE_GENERIC)
.that()
.hasMessageThatContains("should not be specified in this module.")
}
// Tests new shrinker rules filtering done by FilterShrinkerRulesTransform to select only rules
// targeted to specific R8 or Proguard versions.
@Test
fun appTestExtractedJarKeepRules() {
val executor = project.executor()
.with(OptionalBooleanOption.ENABLE_R8, codeShrinker == CodeShrinker.R8)
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> executor.run("assembleMinified")
MultiApkMode.INSTANT_APP -> executor.run("app:assembleMinified", "instantApp:assembleMinified")
}
val classContent = "package example;\n" + "public class ToBeKept { }"
val toBeKept = project.getSubproject("baseModule").mainSrcDir.toPath().resolve("example/ToBeKept.java")
Files.createDirectories(toBeKept.parent)
Files.write(toBeKept, classContent.toByteArray())
val classContent2 = "package example;\n" + "public class ToBeRemoved { }"
val toBeRemoved = project.getSubproject("baseModule").mainSrcDir.toPath().resolve("example/ToBeRemoved.java")
Files.createDirectories(toBeRemoved.parent)
Files.write(toBeRemoved, classContent2.toByteArray())
val jarFile = temporaryFolder.newFile("libkeeprules.jar")
val keepRule = "-keep class example.ToBeKept"
val keepRuleToBeIgnored = "-keep class example.ToBeRemoved"
TestInputsGenerator.writeJarWithTextEntries(
jarFile.toPath(),
Pair.of("META-INF/com.android.tools/r8/rules.pro", keepRule),
Pair.of("META-INF/com.android.tools/proguard/rules.pro", keepRule),
Pair.of("META-INF/proguard/rules.pro", keepRuleToBeIgnored)
)
TestFileUtils.appendToFile(
project.getSubproject(":foo:otherFeature1").buildFile,
""
+ "dependencies {\n"
+ " implementation files ('" + jarFile.absolutePath + "')\n"
+ "}"
)
project.executor()
.with(OptionalBooleanOption.ENABLE_R8, codeShrinker == CodeShrinker.R8)
.run("assembleMinified")
val apkType = GradleTestProject.ApkType.of("minified", true)
project.getSubproject("baseModule")
.let {
when (multiApkMode) {
MultiApkMode.DYNAMIC_APP -> it.getApk(apkType)
MultiApkMode.INSTANT_APP -> it.getFeatureApk(apkType)
}
}.use { minified ->
assertThat(minified).containsClass("Lexample/ToBeKept;")
assertThat(minified).doesNotContainClass("Lexample/ToBeRemoved;")
}
}
/** Regression test for https://issuetracker.google.com/79090176 */
@Test
fun testMinifyEnabledToggling() {
Assume.assumeTrue(multiApkMode == MultiApkMode.DYNAMIC_APP)
val executor = project.executor()
.with(OptionalBooleanOption.ENABLE_R8, codeShrinker == CodeShrinker.R8)
// first run with minifyEnabled true
executor.run("assembleMinified")
// then run with minifyEnabled false
TestFileUtils.searchAndReplace(
project.getSubproject(":baseModule").buildFile,
"minifyEnabled true",
"minifyEnabled false"
)
executor.run("assembleMinified")
}
private fun getApkFolderOutput(
variantName: String,
baseGradlePath: String
): AppBundleVariantBuildOutput {
val outputModels = project.model().fetchContainer(AppBundleProjectBuildOutput::class.java)
val outputAppModel =
outputModels.rootBuildModelMap[baseGradlePath]
?: fail("Failed to get output model for $baseGradlePath module")
return outputAppModel.getOutputByName(variantName)
}
}