/*
* Copyright (C) 2013 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;
import static com.android.SdkConstants.DOT_ANDROID_PACKAGE;
import static com.android.SdkConstants.FD_RES;
import static com.android.SdkConstants.FD_RES_RAW;
import static com.android.builder.core.BuilderConstants.ANDROID_WEAR_MICRO_APK;
import static com.android.builder.model.AndroidProject.FD_INTERMEDIATES;
import static com.android.builder.model.AndroidProject.FD_OUTPUTS;
import static com.android.builder.model.AndroidProject.PROPERTY_SIGNING_KEY_ALIAS;
import static com.android.builder.model.AndroidProject.PROPERTY_SIGNING_KEY_PASSWORD;
import static com.android.builder.model.AndroidProject.PROPERTY_SIGNING_STORE_FILE;
import static com.android.builder.model.AndroidProject.PROPERTY_SIGNING_STORE_PASSWORD;
import static java.io.File.separator;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.internal.CommandLineRunner;
import com.android.ide.common.internal.LoggedErrorException;
import com.android.utils.StdLogger;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarInputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import javax.imageio.ImageIO;
/**
* Some manual tests for building projects.
*
* This requires an SDK, found through the ANDROID_HOME environment variable or present in the
* Android Source tree under out/host/<platform>/sdk/... (result of 'make sdk')
*/
public class ManualBuildTest extends BuildTest {
private static final int RED = 0xFFFF0000;
private static final int GREEN = 0xFF00FF00;
private static final int BLUE = 0xFF0000FF;
protected File manualDir;
protected File regularDir;
@Override
protected void setUp() throws Exception {
super.setUp();
manualDir = new File(testDir, FOLDER_TEST_MANUAL);
regularDir = new File(testDir, FOLDER_TEST_REGULAR);
}
public void testOverlay1Content() throws Exception {
File project = buildProject(FOLDER_TEST_REGULAR, "overlay1", BasePlugin.GRADLE_TEST_VERSION);
File drawableOutput = new File(project, "build/" + FD_INTERMEDIATES + "/res/debug/drawable");
checkImageColor(drawableOutput, "no_overlay.png", GREEN);
checkImageColor(drawableOutput, "type_overlay.png", GREEN);
}
public void testOverlay2Content() throws Exception {
File project = buildProject(FOLDER_TEST_REGULAR, "overlay2", BasePlugin.GRADLE_TEST_VERSION);
File drawableOutput = new File(project, "build/" + FD_INTERMEDIATES + "/res/one/debug/drawable");
checkImageColor(drawableOutput, "no_overlay.png", GREEN);
checkImageColor(drawableOutput, "type_overlay.png", GREEN);
checkImageColor(drawableOutput, "flavor_overlay.png", GREEN);
checkImageColor(drawableOutput, "type_flavor_overlay.png", GREEN);
checkImageColor(drawableOutput, "variant_type_flavor_overlay.png", GREEN);
}
public void testOverlay3Content() throws Exception {
File project = buildProject(FOLDER_TEST_REGULAR, "overlay3", BasePlugin.GRADLE_TEST_VERSION);
File drawableOutput = new File(project, "build/" + FD_INTERMEDIATES + "/res/freebeta/debug/drawable");
checkImageColor(drawableOutput, "no_overlay.png", GREEN);
checkImageColor(drawableOutput, "debug_overlay.png", GREEN);
checkImageColor(drawableOutput, "beta_overlay.png", GREEN);
checkImageColor(drawableOutput, "free_overlay.png", GREEN);
checkImageColor(drawableOutput, "free_beta_overlay.png", GREEN);
checkImageColor(drawableOutput, "free_beta_debug_overlay.png", GREEN);
checkImageColor(drawableOutput, "free_normal_overlay.png", RED);
drawableOutput = new File(project, "build/" + FD_INTERMEDIATES + "/res/freenormal/debug/drawable");
checkImageColor(drawableOutput, "no_overlay.png", GREEN);
checkImageColor(drawableOutput, "debug_overlay.png", GREEN);
checkImageColor(drawableOutput, "beta_overlay.png", RED);
checkImageColor(drawableOutput, "free_overlay.png", GREEN);
checkImageColor(drawableOutput, "free_beta_overlay.png", RED);
checkImageColor(drawableOutput, "free_beta_debug_overlay.png", RED);
checkImageColor(drawableOutput, "free_normal_overlay.png", GREEN);
drawableOutput = new File(project, "build/" + FD_INTERMEDIATES + "/res/paidbeta/debug/drawable");
checkImageColor(drawableOutput, "no_overlay.png", GREEN);
checkImageColor(drawableOutput, "debug_overlay.png", GREEN);
checkImageColor(drawableOutput, "beta_overlay.png", GREEN);
checkImageColor(drawableOutput, "free_overlay.png", RED);
checkImageColor(drawableOutput, "free_beta_overlay.png", RED);
checkImageColor(drawableOutput, "free_beta_debug_overlay.png", RED);
checkImageColor(drawableOutput, "free_normal_overlay.png", RED);
}
public void testRepo() {
File repo = new File(manualDir, "repo");
try {
runTasksOn(
new File(repo, "util"),
BasePlugin.GRADLE_TEST_VERSION,
"clean", "uploadArchives");
runTasksOn(
new File(repo, "baseLibrary"),
BasePlugin.GRADLE_TEST_VERSION,
"clean", "uploadArchives");
runTasksOn(
new File(repo, "library"),
BasePlugin.GRADLE_TEST_VERSION,
"clean", "uploadArchives");
runTasksOn(
new File(repo, "app"),
BasePlugin.GRADLE_TEST_VERSION,
"clean", "assemble");
} finally {
// clean up the test repository.
File testRepo = new File(repo, "testrepo");
deleteFolder(testRepo);
}
}
public void testLibsManifestMerging() throws Exception {
File project = new File(regularDir, "libsTest");
File fileOutput = new File(project, "libapp/build/" + FD_INTERMEDIATES + "/bundles/release/AndroidManifest.xml");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "build");
assertTrue(fileOutput.exists());
}
// test whether a library project has its fields obfuscated
public void testLibMinify() throws Exception {
File project = new File(regularDir, "libMinify");
File fileOutput = new File(project, "build/" + FD_OUTPUTS + "/mapping/release");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "build");
checkFile(fileOutput, "mapping.txt", new String[]{"int obfuscatedInt -> a"});
}
// test whether proguard.txt has been correctly merged
public void testLibProguardConsumerFile() throws Exception {
File project = new File(regularDir, "libProguardConsumerFiles");
File debugFileOutput = new File(project, "build/" + FD_INTERMEDIATES + "/bundles/debug");
File releaseFileOutput = new File(project, "build/" + FD_INTERMEDIATES + "/bundles/release");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "build");
checkFile(debugFileOutput, "proguard.txt", new String[]{"A"});
checkFile(releaseFileOutput, "proguard.txt", new String[]{"A", "B", "C"});
}
public void testShrinkResources() throws Exception {
File project = new File(manualDir, "shrink");
File output = new File(project, "build/" + FD_OUTPUTS);
File intermediates = new File(project, "build/" + FD_INTERMEDIATES);
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "assembleRelease", "assembleDebug", "assembleProguardNoShrink");
// The release target has shrinking enabled.
// The proguardNoShrink target has proguard but no shrinking enabled.
// The debug target has neither proguard nor shrinking enabled.
File apkRelease = new File(output, "apk" + separator + "shrink-release-unsigned.apk");
File apkDebug = new File(output, "apk" + separator + "shrink-debug.apk");
File apkProguardOnly = new File(output, "apk" + separator + "shrink-proguardNoShrink-unsigned.apk");
assertTrue(apkDebug + " is not a file", apkDebug.isFile());
assertTrue(apkRelease + " is not a file", apkRelease.isFile());
assertTrue(apkProguardOnly + " is not a file", apkProguardOnly.isFile());
File compressed = new File(intermediates, "res" + separator + "resources-release-stripped.ap_");
File uncompressed = new File(intermediates, "res" + separator + "resources-release.ap_");
assertTrue(compressed + " is not a file", compressed.isFile());
assertTrue(uncompressed + " is not a file", uncompressed.isFile());
// Check that there is no shrinking in the other two targets:
assertTrue(new File(intermediates,
"res" + separator + "resources-debug.ap_").exists());
assertFalse(new File(intermediates,
"res" + separator + "resources-debug-stripped.ap_").exists());
assertTrue(new File(intermediates,
"res" + separator + "resources-proguardNoShrink.ap_").exists());
assertFalse(new File(intermediates,
"res" + separator + "resources-proguardNoShrink-stripped.ap_").exists());
String expectedUnstrippedApk = ""
+ "AndroidManifest.xml\n"
+ "classes.dex\n"
+ "resources.arsc\n"
+ "res/layout/unused1.xml\n"
+ "res/layout/unused2.xml\n"
+ "res/drawable/unused9.xml\n"
+ "res/drawable/unused10.xml\n"
+ "res/drawable/unused11.xml\n"
+ "res/menu/unused12.xml\n"
+ "res/layout/unused13.xml\n"
+ "res/layout/used1.xml\n"
+ "res/layout/used2.xml\n"
+ "res/layout/used3.xml\n"
+ "res/layout/used4.xml\n"
+ "res/layout/used5.xml\n"
+ "res/layout/used6.xml\n"
+ "res/layout/used7.xml\n"
+ "res/layout/used8.xml\n"
+ "res/drawable/used9.xml\n"
+ "res/drawable/used10.xml\n"
+ "res/drawable/used11.xml\n"
+ "res/drawable/used12.xml\n"
+ "res/menu/used13.xml\n"
+ "res/layout/used14.xml";
String expectedStrippedApkContents = ""
+ "AndroidManifest.xml\n"
+ "classes.dex\n"
+ "resources.arsc\n"
+ "res/layout/used1.xml\n"
+ "res/layout/used2.xml\n"
+ "res/layout/used3.xml\n"
+ "res/layout/used4.xml\n"
+ "res/layout/used5.xml\n"
+ "res/layout/used6.xml\n"
+ "res/layout/used7.xml\n"
+ "res/layout/used8.xml\n"
+ "res/drawable/used9.xml\n"
+ "res/drawable/used10.xml\n"
+ "res/drawable/used11.xml\n"
+ "res/drawable/used12.xml\n"
+ "res/menu/used13.xml\n"
+ "res/layout/used14.xml";
// Should not have any unused resources in the compressed list
assertFalse(expectedStrippedApkContents, expectedStrippedApkContents.contains("unused"));
// Should have *all* the used resources, currently 1-14
for (int i = 1; i <= 14; i++) {
assertTrue("Missing used"+i + " in " + expectedStrippedApkContents,
expectedStrippedApkContents.contains("/used" + i + "."));
}
// Check that the uncompressed resources (.ap_) for the release target have everything
// we expect
String expectedUncompressed = expectedUnstrippedApk.replace("classes.dex\n", "");
assertEquals(expectedUncompressed, dumpZipContents(uncompressed).trim());
// The debug target should have everything there in the APK
assertEquals(expectedUnstrippedApk, dumpZipContents(apkDebug));
assertEquals(expectedUnstrippedApk, dumpZipContents(apkProguardOnly));
// Check the compressed .ap_:
String actualCompressed = dumpZipContents(compressed);
String expectedCompressed = expectedStrippedApkContents.replace("classes.dex\n", "");
assertEquals(expectedCompressed, actualCompressed);
assertFalse(expectedCompressed, expectedCompressed.contains("unused"));
assertEquals(expectedStrippedApkContents, dumpZipContents(apkRelease));
}
private static List<String> getZipPaths(File zipFile) throws IOException {
List<String> lines = Lists.newArrayList();
FileInputStream fis = new FileInputStream(zipFile);
try {
ZipInputStream zis = new ZipInputStream(fis);
try {
ZipEntry entry = zis.getNextEntry();
while (entry != null) {
lines.add(entry.getName());
entry = zis.getNextEntry();
}
} finally {
zis.close();
}
} finally {
fis.close();
}
return lines;
}
private static String dumpZipContents(File zipFile) throws IOException {
List<String> lines = getZipPaths(zipFile);
// Remove META-INF statements
ListIterator<String> iterator = lines.listIterator();
while (iterator.hasNext()) {
if (iterator.next().startsWith("META-INF/")) {
iterator.remove();
}
}
// Sort by base name (and numeric sort such that unused10 comes after unused9)
final Pattern pattern = Pattern.compile("(.*[^\\d])(\\d+)\\..+");
Collections.sort(lines, new Comparator<String>() {
@Override
public int compare(String line1, String line2) {
String name1 = line1.substring(line1.lastIndexOf('/') + 1);
String name2 = line2.substring(line2.lastIndexOf('/') + 1);
int delta = name1.compareTo(name2);
if (delta != 0) {
// Try to do numeric sort
Matcher match1 = pattern.matcher(name1);
if (match1.matches()) {
Matcher match2 = pattern.matcher(name2);
//noinspection ConstantConditions
if (match2.matches() && match1.group(1).equals(match2.group(1))) {
//noinspection ConstantConditions
int num1 = Integer.parseInt(match1.group(2));
//noinspection ConstantConditions
int num2 = Integer.parseInt(match2.group(2));
if (num1 != num2) {
return num1 - num2;
}
}
}
return delta;
}
return line1.compareTo(line2);
}
});
return Joiner.on('\n').join(lines);
}
public void testAnnotations() throws Exception {
File project = new File(regularDir, "extractAnnotations");
File debugFileOutput = new File(project, "build/" + FD_INTERMEDIATES + "/annotations/debug");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "assembleDebug");
File file = new File(debugFileOutput, "annotations.zip");
Map<String, String> map = Maps.newHashMap();
//noinspection SpellCheckingInspection
map.put("com/android/tests/extractannotations/annotations.xml", ""
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<root>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest ExtractTest(int, java.lang.String) 0\">\n"
+ " <annotation name=\"android.support.annotation.IdRes\" />\n"
+ " </item>\n"
// This item should be removed when I start supporting @hide
+ " <item name=\"com.android.tests.extractannotations.ExtractTest int getHiddenMethod()\">\n"
+ " <annotation name=\"android.support.annotation.IdRes\" />\n"
+ " </item>\n"
// This item should be removed when I start supporting @hide
+ " <item name=\"com.android.tests.extractannotations.ExtractTest int getPrivate()\">\n"
+ " <annotation name=\"android.support.annotation.IdRes\" />\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest int getVisibility()\">\n"
+ " <annotation name=\"android.support.annotation.IntDef\">\n"
+ " <val name=\"value\" val=\"{com.android.tests.extractannotations.ExtractTest.VISIBLE, com.android.tests.extractannotations.ExtractTest.INVISIBLE, com.android.tests.extractannotations.ExtractTest.GONE, 5, 17, com.android.tests.extractannotations.Constants.CONSTANT_1}\" />\n"
+ " </annotation>\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest int resourceTypeMethod(int, int)\">\n"
+ " <annotation name=\"android.support.annotation.StringRes\" />\n"
+ " <annotation name=\"android.support.annotation.IdRes\" />\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest int resourceTypeMethod(int, int) 0\">\n"
+ " <annotation name=\"android.support.annotation.DrawableRes\" />\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest int resourceTypeMethod(int, int) 1\">\n"
+ " <annotation name=\"android.support.annotation.IdRes\" />\n"
+ " <annotation name=\"android.support.annotation.ColorRes\" />\n"
+ " </item>\n"
// This item should be removed when I start supporting @hide
+ " <item name=\"com.android.tests.extractannotations.ExtractTest java.lang.Object getPackagePrivate()\">\n"
+ " <annotation name=\"android.support.annotation.IdRes\" />\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest java.lang.String getStringMode(int)\">\n"
+ " <annotation name=\"android.support.annotation.StringDef\">\n"
+ " <val name=\"value\" val=\"{com.android.tests.extractannotations.ExtractTest.STRING_1, com.android.tests.extractannotations.ExtractTest.STRING_2, "literalValue", "concatenated"}\" />\n"
+ " </annotation>\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest java.lang.String getStringMode(int) 0\">\n"
+ " <annotation name=\"android.support.annotation.IntDef\">\n"
+ " <val name=\"value\" val=\"{com.android.tests.extractannotations.ExtractTest.VISIBLE, com.android.tests.extractannotations.ExtractTest.INVISIBLE, com.android.tests.extractannotations.ExtractTest.GONE, 5, 17, com.android.tests.extractannotations.Constants.CONSTANT_1}\" />\n"
+ " </annotation>\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest void checkForeignTypeDef(int) 0\">\n"
+ " <annotation name=\"android.support.annotation.IntDef\">\n"
+ " <val name=\"value\" val=\"{com.android.tests.extractannotations.Constants.CONSTANT_1, com.android.tests.extractannotations.Constants.CONSTANT_2}\" />\n"
+ " <val name=\"flag\" val=\"true\" />\n"
+ " </annotation>\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest void resourceTypeMethodWithTypeArgs(java.util.Map<java.lang.String,? extends java.lang.Number>, T, int) 0\">\n"
+ " <annotation name=\"android.support.annotation.StringRes\" />\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest void resourceTypeMethodWithTypeArgs(java.util.Map<java.lang.String,? extends java.lang.Number>, T, int) 1\">\n"
+ " <annotation name=\"android.support.annotation.DrawableRes\" />\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest void resourceTypeMethodWithTypeArgs(java.util.Map<java.lang.String,? extends java.lang.Number>, T, int) 2\">\n"
+ " <annotation name=\"android.support.annotation.IdRes\" />\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest void testMask(int) 0\">\n"
+ " <annotation name=\"android.support.annotation.IntDef\">\n"
+ " <val name=\"value\" val=\"{0, com.android.tests.extractannotations.Constants.FLAG_VALUE_1, com.android.tests.extractannotations.Constants.FLAG_VALUE_2}\" />\n"
+ " <val name=\"flag\" val=\"true\" />\n"
+ " </annotation>\n"
+ " </item>\n"
+ " <item name=\"com.android.tests.extractannotations.ExtractTest void testNonMask(int) 0\">\n"
+ " <annotation name=\"android.support.annotation.IntDef\">\n"
+ " <val name=\"value\" val=\"{0, com.android.tests.extractannotations.Constants.CONSTANT_1, com.android.tests.extractannotations.Constants.CONSTANT_3}\" />\n"
+ " </annotation>\n"
+ " </item>\n"
// This should be hidden when we start filtering out hidden classes on @hide!
+ " <item name=\"com.android.tests.extractannotations.ExtractTest.HiddenClass int getHiddenMember()\">\n"
+ " <annotation name=\"android.support.annotation.IdRes\" />\n"
+ " </item>\n"
+ "</root>");
checkJar(file, map);
// check the resulting .aar file to ensure annotations.zip inclusion.
File archiveFile = new File(project, "build/outputs/aar/extractAnnotations-debug.aar");
assertTrue(archiveFile.isFile());
ZipFile archive = null;
try {
archive = new ZipFile(archiveFile);
ZipEntry entry = archive.getEntry("annotations.zip");
assertNotNull(entry);
} finally {
if (archive != null) {
archive.close();
}
}
}
public void testRsEnabledAnnotations() throws IOException {
File project = new File(regularDir, "extractRsEnabledAnnotations");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "assembleDebug");
// check the resulting .aar file to ensure annotations.zip inclusion.
File archiveFile = new File(project, "build/outputs/aar/extractRsEnabledAnnotations-debug.aar");
assertTrue(archiveFile.isFile());
ZipFile archive = null;
try {
archive = new ZipFile(archiveFile);
ZipEntry entry = archive.getEntry("annotations.zip");
assertNotNull(entry);
} finally {
if (archive != null) {
archive.close();
}
}
}
public void testSimpleManifestMerger() throws IOException {
File project = new File(manualDir, "simpleManifestMergingTask");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "manifestMerger");
}
public void test3rdPartyTests() throws Exception {
// custom because we want to run deviceCheck even without devices, since we use
// a fake DeviceProvider that doesn't use a device, but only record the calls made
// to the DeviceProvider and the DeviceConnector.
runTasksOn(
new File(manualDir, "3rdPartyTests"),
BasePlugin.GRADLE_TEST_VERSION,
"clean", "deviceCheck");
}
public void testEmbedded() throws Exception {
File project = new File(regularDir, "embedded");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", ":main:assembleRelease");
File mainApk = new File(project, "main/build/" + FD_OUTPUTS + "/apk/main-release-unsigned.apk");
checkJar(mainApk, Collections.<String, String>singletonMap(
FD_RES + '/' + FD_RES_RAW + '/' + ANDROID_WEAR_MICRO_APK + DOT_ANDROID_PACKAGE,
null));
}
public void testUserProvidedTestAndroidManifest() throws Exception {
File project = new File(regularDir, "androidManifestInTest");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "assembleDebugTest");
File testApk = new File(project, "build/" + FD_OUTPUTS + "/apk/androidManifestInTest-debug-test-unaligned.apk");
File aapt = new File(sdkDir, "build-tools/19.1.0/aapt");
assertTrue("Test requires build-tools 19.1.0", aapt.isFile());
String[] command = new String[4];
command[0] = aapt.getPath();
command[1] = "l";
command[2] = "-a";
command[3] = testApk.getPath();
CommandLineRunner commandLineRunner = new CommandLineRunner(new StdLogger(StdLogger.Level.ERROR));
final List<String> aaptOutput = Lists.newArrayList();
commandLineRunner.runCmdLine(command, new CommandLineRunner.CommandLineOutput() {
@Override
public void out(@Nullable String line) {
if (line != null) {
aaptOutput.add(line);
}
}
@Override
public void err(@Nullable String line) {
super.err(line);
}
}, null /*env vars*/);
System.out.println("Beginning dump");
boolean foundPermission = false;
boolean foundMetadata = false;
for (String line : aaptOutput) {
if (line.contains("foo.permission-group.COST_MONEY")) {
foundPermission = true;
}
if (line.contains("meta-data")) {
foundMetadata = true;
}
}
if (!foundPermission) {
fail("Could not find user-specified permission group.");
}
if (!foundMetadata) {
fail("Could not find meta-data under instrumentation ");
}
}
public void testDensitySplits() throws Exception {
File project = new File(regularDir, "densitySplit");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "assembleDebug");
Map<String, VersionData> expected = Maps.newHashMapWithExpectedSize(5);
expected.put("universal", VersionData.of(112, "version 112"));
expected.put("mdpi", VersionData.of(212, "version 212"));
expected.put("hdpi", VersionData.of(312, "version 312"));
expected.put("xhdpi", VersionData.of(412, "version 412"));
expected.put("xxhdpi", VersionData.of(512, "version 512"));
checkVersion(project, null, expected, "densitySplit");
}
public void testAbiSplits() throws Exception {
File project = new File(regularDir, "ndkJniLib");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "app:assembleDebug");
Map<String, VersionData> expected = Maps.newHashMapWithExpectedSize(8);
expected.put("gingerbread-universal", VersionData.of(1000123));
expected.put("gingerbread-armeabi-v7a", VersionData.of(1100123));
expected.put("gingerbread-mips", VersionData.of(1200123));
expected.put("gingerbread-x86", VersionData.of(1300123));
expected.put("icecreamSandwich-universal", VersionData.of(2000123));
expected.put("icecreamSandwich-armeabi-v7a", VersionData.of(2100123));
expected.put("icecreamSandwich-mips", VersionData.of(2200123));
expected.put("icecreamSandwich-x86", VersionData.of(2300123));
checkVersion(project, "app/", expected, "app");
}
private static final class VersionData {
static VersionData of(int code, String name) {
VersionData versionData = new VersionData();
versionData.code = code;
versionData.name = name;
return versionData;
}
static VersionData of(int code) {
return of(code, null);
}
@Nullable
Integer code;
@Nullable
String name;
}
private void checkVersion(
@NonNull File project,
@Nullable String outRoot,
@NonNull Map<String, VersionData> expected,
@NonNull String baseName)
throws IOException, InterruptedException, LoggedErrorException {
File aapt = new File(sdkDir, "build-tools/20.0.0/aapt");
assertTrue("Test requires build-tools 20.0.0", aapt.isFile());
String[] command = new String[4];
command[0] = aapt.getPath();
command[1] = "dump";
command[2] = "badging";
CommandLineRunner commandLineRunner = new CommandLineRunner(new StdLogger(StdLogger.Level.ERROR));
for (Map.Entry<String, VersionData> entry : expected.entrySet()) {
String path = "build/" + FD_OUTPUTS + "/apk/" + baseName + "-" + entry.getKey() + "-debug.apk";
if (outRoot != null) {
path = outRoot + path;
}
File apk = new File(project, path);
command[3] = apk.getPath();
final List<String> aaptOutput = Lists.newArrayList();
commandLineRunner.runCmdLine(command, new CommandLineRunner.CommandLineOutput() {
@Override
public void out(@Nullable String line) {
if (line != null) {
aaptOutput.add(line);
}
}
@Override
public void err(@Nullable String line) {
super.err(line);
}
}, null /*env vars*/);
Pattern p = Pattern.compile("^package: name='(.+)' versionCode='([0-9]*)' versionName='(.*)'$");
String versionCode = null;
String versionName = null;
for (String line : aaptOutput) {
Matcher m = p.matcher(line);
if (m.matches()) {
versionCode = m.group(2);
versionName = m.group(3);
break;
}
}
assertNotNull("Unable to determine version code", versionCode);
assertNotNull("Unable to determine version name", versionName);
VersionData versionData = entry.getValue();
if (versionData.code != null) {
assertEquals("Unexpected version code for split: " + entry.getKey(),
versionData.code.intValue(), Integer.parseInt(versionCode));
}
if (versionData.name != null) {
assertEquals("Unexpected version code for split: " + entry.getKey(),
versionData.name, versionName);
}
}
}
public void testBasicWithSigningOverride() throws Exception {
File project = new File(regularDir, "basic");
// add prop args for signing override.
List<String> args = Lists.newArrayListWithExpectedSize(4);
args.add("-P" + PROPERTY_SIGNING_STORE_FILE + "=" + new File(project, "debug.keystore").getPath());
args.add("-P" + PROPERTY_SIGNING_STORE_PASSWORD + "=android");
args.add("-P" + PROPERTY_SIGNING_KEY_ALIAS + "=AndroidDebugKey");
args.add("-P" + PROPERTY_SIGNING_KEY_PASSWORD + "=android");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
args,
Collections.<String, String>emptyMap(),
"clean", ":assembleRelease");
// check that the output exist. Since the filename is tried to signing/zipaligning
// this gives us a fairly good idea about signing already.
File releaseApk = new File(project, "build/" + FD_OUTPUTS + "/apk/basic-release.apk");
assertTrue(releaseApk.isFile());
// now check for signing file inside the archive.
checkJar(releaseApk, Collections.<String,
String>singletonMap("META-INF/CERT.RSA", null));
}
public void testMaxSdkVersion() throws Exception {
File project = new File(regularDir, "maxSdkVersion");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "assembleDebug");
checkMaxSdkVersion(
new File(project, "build/" + FD_OUTPUTS + "/apk/maxSdkVersion-f1-debug.apk"), "21");
checkMaxSdkVersion(
new File(project, "build/" + FD_OUTPUTS + "/apk/maxSdkVersion-f2-debug.apk"), "19");
}
public void testVariantConfigurationDependencies() throws Exception {
File project = new File(manualDir, "dependenciesWithVariants");
runTasksOn(
project,
BasePlugin.GRADLE_TEST_VERSION,
"clean", "assembleDebug", "assembleTest");
}
private void checkMaxSdkVersion(File testApk, String version)
throws InterruptedException, LoggedErrorException, IOException {
File aapt = new File(sdkDir, "build-tools/19.1.0/aapt");
assertTrue("Test requires build-tools 19.1.0", aapt.isFile());
String[] command = new String[4];
command[0] = aapt.getPath();
command[1] = "dump";
command[2] = "badging";
command[3] = testApk.getPath();
CommandLineRunner commandLineRunner = new CommandLineRunner(new StdLogger(StdLogger.Level.ERROR));
final List<String> aaptOutput = Lists.newArrayList();
commandLineRunner.runCmdLine(command, new CommandLineRunner.CommandLineOutput() {
@Override
public void out(@Nullable String line) {
if (line != null) {
aaptOutput.add(line);
}
}
@Override
public void err(@Nullable String line) {
super.err(line);
}
}, null /*env vars*/);
System.out.println("Beginning dump");
for (String line : aaptOutput) {
if (line.equals("maxSdkVersion:'" + version + "'")) {
return;
}
}
fail("Could not find uses-sdk:maxSdkVersion set to " + version + " in apk dump");
}
private static void checkImageColor(File folder, String fileName, int expectedColor)
throws IOException {
File f = new File(folder, fileName);
assertTrue("File '" + f.getAbsolutePath() + "' does not exist.", f.isFile());
BufferedImage image = ImageIO.read(f);
int rgb = image.getRGB(0, 0);
assertEquals(String.format("Expected: 0x%08X, actual: 0x%08X for file %s",
expectedColor, rgb, f),
expectedColor, rgb);
}
private static void checkFile(File folder, String fileName, String[] expectedContents)
throws IOException {
File f = new File(folder, fileName);
assertTrue("File '" + f.getAbsolutePath() + "' does not exist.", f.isFile());
String contents = Files.toString(f, Charsets.UTF_8);
for (String expectedContent : expectedContents) {
assertTrue("File '" + f.getAbsolutePath() + "' does not contain: " + expectedContent,
contents.contains(expectedContent));
}
}
private static void checkJar(File jar, Map<String, String> pathToContents)
throws IOException {
assertTrue("File '" + jar.getPath() + "' does not exist.", jar.isFile());
JarInputStream zis = null;
FileInputStream fis;
Set<String> notFound = Sets.newHashSet();
notFound.addAll(pathToContents.keySet());
fis = new FileInputStream(jar);
try {
zis = new JarInputStream(fis);
ZipEntry entry = zis.getNextEntry();
while (entry != null) {
String name = entry.getName();
String expected = pathToContents.get(name);
if (expected != null) {
notFound.remove(name);
if (!entry.isDirectory()) {
byte[] bytes = ByteStreams.toByteArray(zis);
if (bytes != null) {
String contents = new String(bytes, Charsets.UTF_8).trim();
assertEquals("Contents in " + name + " did not match",
expected, contents);
}
}
} else if (pathToContents.keySet().contains(name)) {
notFound.remove(name);
}
entry = zis.getNextEntry();
}
} finally {
fis.close();
if (zis != null) {
zis.close();
}
}
assertTrue("Did not find the following paths in the " + jar.getPath() + " file: " +
notFound, notFound.isEmpty());
}
}