/* * Copyright (C) 2015 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.common.truth; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; import com.android.build.gradle.integration.common.utils.ApkHelper; import com.android.build.gradle.integration.common.utils.SdkHelper; import com.android.builder.core.ApkInfoParser; import com.android.ide.common.process.DefaultProcessExecutor; import com.android.ide.common.process.ProcessException; import com.android.ide.common.process.ProcessExecutor; import com.android.ide.common.process.ProcessInfoBuilder; import com.android.utils.StdLogger; import com.google.common.truth.FailureStrategy; import com.google.common.truth.IterableSubject; import com.google.common.truth.SubjectFactory; import com.google.common.truth.Truth; import org.junit.Assert; import java.io.File; import java.io.IOException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Truth support for apk files. */ public class ApkSubject extends AbstractAndroidSubject<ApkSubject> { private static final Pattern PATTERN_CLASS_DESC = Pattern.compile( "^Class descriptor\\W*:\\W*'(L.+;)'$"); private static final Pattern PATTERN_MAX_SDK_VERSION = Pattern.compile( "^maxSdkVersion\\W*:\\W*'(.+)'$"); static class Factory extends SubjectFactory<ApkSubject, File> { @NonNull public static Factory get() { return new Factory(); } private Factory() {} @Override public ApkSubject getSubject( @NonNull FailureStrategy failureStrategy, @NonNull File subject) { return new ApkSubject(failureStrategy, subject); } } public ApkSubject( @NonNull FailureStrategy failureStrategy, @NonNull File subject) { super(failureStrategy, subject); } @NonNull public IterableSubject<? extends IterableSubject<?, String, List<String>>, String, List<String>> locales() throws ProcessException { File apk = getSubject(); List<String> locales = ApkHelper.getLocales(apk); if (locales == null) { Assert.fail(String.format("locales not found in badging output for %s", apk)); } return Truth.assertThat(locales); } @SuppressWarnings("NonBooleanMethodNameMayNotStartWithQuestion") public void hasVersionCode(int versionCode) throws ProcessException { File apk = getSubject(); ApkInfoParser.ApkInfo apkInfo = getApkInfo(apk); Integer actualVersionCode = apkInfo.getVersionCode(); if (actualVersionCode == null) { failWithRawMessage("Unable to query %s for versionCode", getDisplaySubject()); } if (!apkInfo.getVersionCode().equals(versionCode)) { failWithBadResults("has versionCode", versionCode, "is", actualVersionCode); } } @SuppressWarnings("NonBooleanMethodNameMayNotStartWithQuestion") public void hasVersionName(@NonNull String versionName) throws ProcessException { File apk = getSubject(); ApkInfoParser.ApkInfo apkInfo = getApkInfo(apk); String actualVersionName = apkInfo.getVersionName(); if (actualVersionName == null) { failWithRawMessage("Unable to query %s for versionName", getDisplaySubject()); } if (!apkInfo.getVersionName().equals(versionName)) { failWithBadResults("has versionName", versionName, "is", actualVersionName); } } @SuppressWarnings("NonBooleanMethodNameMayNotStartWithQuestion") public void hasMaxSdkVersion(int maxSdkVersion) throws ProcessException { List<String> output = ApkHelper.getApkBadging(getSubject()); checkMaxSdkVersion(output, maxSdkVersion); } @Override protected String getDisplaySubject() { String name = (internalCustomName() == null) ? "" : "\"" + internalCustomName() + "\" "; return name + "<" + getSubject().getName() + ">"; } /** * Returns true if the provided class is present in the file. * @param expectedClassName the class name in the format Lpkg1/pk2/Name; */ @Override protected boolean checkForClass( @NonNull String expectedClassName) throws ProcessException, IOException { // get the dexdump exec File dexDump = SdkHelper.getDexDump(); ProcessExecutor executor = new DefaultProcessExecutor( new StdLogger(StdLogger.Level.ERROR)); ProcessInfoBuilder builder = new ProcessInfoBuilder(); builder.setExecutable(dexDump); builder.addArgs(getSubject().getAbsolutePath()); List<String> output = ApkHelper.runAndGetOutput(builder.createProcess(), executor); for (String line : output) { Matcher m = PATTERN_CLASS_DESC.matcher(line.trim()); if (m.matches()) { String className = m.group(1); if (expectedClassName.equals(className)) { return true; } } } return false; } @NonNull private static ApkInfoParser.ApkInfo getApkInfo(@NonNull File apk) throws ProcessException { ProcessExecutor processExecutor = new DefaultProcessExecutor( new StdLogger(StdLogger.Level.ERROR)); ApkInfoParser parser = new ApkInfoParser(SdkHelper.getAapt(), processExecutor); return parser.parseApk(apk); } @VisibleForTesting void checkMaxSdkVersion(@NonNull List<String> output, int maxSdkVersion) { for (String line : output) { Matcher m = PATTERN_MAX_SDK_VERSION.matcher(line.trim()); if (m.matches()) { String actual = m.group(1); try { Integer i = Integer.parseInt(actual); if (!i.equals(maxSdkVersion)) { failWithBadResults("has maxSdkVersion", maxSdkVersion, "is", i); } return; } catch (NumberFormatException e) { failureStrategy.fail( String.format( "maxSdkVersion in badging for %s is not a number: %s", getDisplaySubject(), actual), e); } } } failWithRawMessage("maxSdkVersion not found in badging output for %s", getDisplaySubject()); } }