/** * Copyright (c) 2016, The CyanogenMod 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 org.cyanogenmod.tests.versioning.unit; import android.content.Context; import android.os.Binder; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; import android.util.Pair; import org.cyanogenmod.tests.CyanogenModTestApplication; import org.cyanogenmod.tests.versioning.unit.apiv2.ApiV2PriorReleaseInterfaces; import org.cyanogenmod.tests.versioning.unit.apiv4.ApiV4PriorReleaseInterfaces; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnitRunner; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * These tests validate the enumerated binder transaction call ids per each * release api level against the current framework. * * This is to validate that no interface contracts are broken in terms of binder * call method mapping between releases. * * After too much searching on the internet, I found that no one was bored enough to * spend time on this awesomely boring concept. But I am. * * Also this is a fun endeavour into parameterized unit testing, and by fun, I mean * horrible and full of drinking. * * If you need to blame anyone for this concept, look no further than danesh@cyngn.com */ @RunWith(Parameterized.class) @LargeTest public class BinderTransactionTest extends AndroidTestCase { private static final String TAG = BinderTransactionTest.class.getSimpleName(); private static final String STUB_SUFFIX = "$Stub"; private static final String CYANOGENMOD_NAMESPACE = "cyanogenmod"; private static final String TRANSACTION_PREFIX = "TRANSACTION_"; private static final int NOT_FROM_PRIOR_RELEASE = -1; private String mField; private int mExpectedValue; private int mActualValue; private static Context sContext; private static ArrayList mKnownSdkClasses; private static Map> mApiMethodsAndValues = new HashMap>(); @Before public void setUp() throws Exception { super.setUp(); } @Test public void testClassLoaderGivesSDKClasses() { /** * Verify we can retrieve our sdk classes from this package */ assertNotNull(mKnownSdkClasses); assertTrue(mKnownSdkClasses.size() > 0); } private static void doSetup() { mKnownSdkClasses = MagicalDexHelper.getLoadedClasses( CyanogenModTestApplication.getStaticApplicationContext(), CYANOGENMOD_NAMESPACE); sContext = CyanogenModTestApplication.getStaticApplicationContext(); addInterfaces(ApiV2PriorReleaseInterfaces.getInterfaces()); addInterfaces(ApiV4PriorReleaseInterfaces.getInterfaces()); } private static void addInterfaces(Map> mapToAdd) { for (String key : mapToAdd.keySet()) { if (mApiMethodsAndValues.get(key) != null) { Map internalMap = mApiMethodsAndValues.get(key); internalMap.putAll(mapToAdd.get(key)); } else { Map internalMap = mapToAdd.get(key); mApiMethodsAndValues.put(key, internalMap); } } } @Parameterized.Parameters public static Collection data() { doSetup(); //Ughhh, lets pretend this never happened ArrayList> targetClassAndFields = new ArrayList>(); ArrayList actualValues = new ArrayList(); for (String sClazz : mKnownSdkClasses) { if (sClazz.endsWith(STUB_SUFFIX)) { try { Class clazz = MagicalDexHelper.loadClassForNameSpace(CyanogenModTestApplication .getStaticApplicationContext(), sClazz); Field[] fields = clazz.getDeclaredFields(); Pattern pattern = Pattern.compile("\\.([\\w]+)\\$"); Matcher matcher = pattern.matcher(clazz.getName()); String className = null; if (matcher.find()) { className = matcher.group(1).substring(0, matcher.group(1).length()); } for (Field field : fields) { if (field.getName().startsWith(TRANSACTION_PREFIX)) { field.setAccessible(true); String fieldName = field.getName().substring( TRANSACTION_PREFIX.length()); fieldName = fieldName.split("_")[0]; Pair classAndField = new Pair( className, fieldName); Log.d(TAG, "Adding: " + classAndField.first + " with field " + classAndField.second); targetClassAndFields.add(classAndField); try { actualValues.add(field.getInt(clazz)); } catch (IllegalAccessException e) { throw new AssertionError("Unable to access " + field.getName()); } } } } catch (IOException e) { throw new AssertionError("Unable to load class " + sClazz); } } } Object[][] values = new Object[targetClassAndFields.size()][3]; for (int i = 0; i < targetClassAndFields.size(); i++) { Pair targetClassAndField = targetClassAndFields.get(i); values[i][0] = targetClassAndField.second; values[i][1] = lookupValueForField(targetClassAndField.first, targetClassAndField.second); values[i][2] = actualValues.get(i); } return Arrays.asList(values); } //Look up the target fields value from a prior release private static Object lookupValueForField(String clazz, String fieldName) { Log.d(TAG, "Looking up: " + clazz + " with field " + fieldName); Map internalMap = mApiMethodsAndValues.get(clazz); if (internalMap == null || !internalMap.containsKey(fieldName)) { Log.d(TAG, "Internal map for " + clazz + " is null or doesn't contain entry"); return NOT_FROM_PRIOR_RELEASE; } return internalMap.get(fieldName); } public BinderTransactionTest(String targetField, Integer expectedValue, Integer actualValue) { mField = targetField; mExpectedValue = expectedValue; mActualValue = actualValue; } @Test public void testBinderTransactionValidation() { Log.d(TAG, "Testing: " + mField + " with expected value of " + mExpectedValue + " and actual value of " + mActualValue); if (mExpectedValue == NOT_FROM_PRIOR_RELEASE) { //This is a new interface, no need to test against return; } try { assertEquals(mExpectedValue, mActualValue); } catch (AssertionError e) { throw new AssertionError("For the field " + mField + " expected value " + mExpectedValue + " but got " + mActualValue); } } }