From 8b8218c01b015f3acd504f0fa5ead9ca8616a239 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Mon, 28 Nov 2016 15:36:56 -0800 Subject: [PATCH] Add test to ensure all future fragments implements logging. The idea is: if a class is Fragment, it must also implements Instrumentable. To make the test possible, I added a structure to load all classes in current classloader, and filter to only the ones we care about. Then insepct each class definition using reflection. Bug: 32952614 Test: make RunSettingsRoboTests Change-Id: Ifa5e27c41d5ad0e84b6e9e9df81c96e8be2878c5 --- .../settings/SettingsDialogFragmentTest.java | 1 + .../core/codeinspection/ClassScanner.java | 130 ++++++++++++++++++ .../codeinspection/CodeInspectionTest.java | 49 +++++++ .../core/codeinspection/CodeInspector.java | 39 ++++++ .../InstrumentableFragmentCodeInspector.java | 108 +++++++++++++++ 5 files changed, 327 insertions(+) create mode 100644 tests/robotests/src/com/android/settings/core/codeinspection/ClassScanner.java create mode 100644 tests/robotests/src/com/android/settings/core/codeinspection/CodeInspectionTest.java create mode 100644 tests/robotests/src/com/android/settings/core/codeinspection/CodeInspector.java create mode 100644 tests/robotests/src/com/android/settings/core/instrumentation/InstrumentableFragmentCodeInspector.java diff --git a/tests/robotests/src/com/android/settings/SettingsDialogFragmentTest.java b/tests/robotests/src/com/android/settings/SettingsDialogFragmentTest.java index 86b613cb195..9bf168dc340 100644 --- a/tests/robotests/src/com/android/settings/SettingsDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/SettingsDialogFragmentTest.java @@ -17,6 +17,7 @@ package com.android.settings; import android.app.Dialog; import android.app.Fragment; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tests/robotests/src/com/android/settings/core/codeinspection/ClassScanner.java b/tests/robotests/src/com/android/settings/core/codeinspection/ClassScanner.java new file mode 100644 index 00000000000..09af870fdfd --- /dev/null +++ b/tests/robotests/src/com/android/settings/core/codeinspection/ClassScanner.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2016 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.settings.core.codeinspection; + +import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Scans and builds all classes in current classloader. + */ +public class ClassScanner { + + private static final String CLASS_SUFFIX = ".class"; + + public List> getClassesForPackage(String packageName) + throws ClassNotFoundException { + final List> classes = new ArrayList<>(); + + try { + final Enumeration resources = Thread.currentThread().getContextClassLoader() + .getResources(packageName.replace('.', '/')); + if (!resources.hasMoreElements()) { + return classes; + } + URL url = resources.nextElement(); + while (url != null) { + final URLConnection connection = url.openConnection(); + + if (connection instanceof JarURLConnection) { + loadClassFromJar((JarURLConnection) connection, packageName, + classes); + } else { + loadClassFromDirectory(new File(URLDecoder.decode(url.getPath(), "UTF-8")), + packageName, classes); + } + if (resources.hasMoreElements()) { + url = resources.nextElement(); + } else { + break; + } + } + } catch (final IOException e) { + throw new ClassNotFoundException("Error when parsing " + packageName, e); + } + return classes; + } + + private void loadClassFromDirectory(File directory, String packageName, List> classes) + throws ClassNotFoundException { + if (directory.exists() && directory.isDirectory()) { + final String[] files = directory.list(); + + for (final String file : files) { + if (file.endsWith(CLASS_SUFFIX)) { + try { + classes.add(Class.forName( + packageName + '.' + file.substring(0, file.length() - 6), + false /* init */, + Thread.currentThread().getContextClassLoader())); + } catch (NoClassDefFoundError e) { + // do nothing. this class hasn't been found by the + // loader, and we don't care. + } + } else { + final File tmpDirectory = new File(directory, file); + if (tmpDirectory.isDirectory()) { + loadClassFromDirectory(tmpDirectory, packageName + "." + file, classes); + } + } + } + } + } + + private void loadClassFromJar(JarURLConnection connection, String packageName, + List> classes) throws ClassNotFoundException, IOException { + final JarFile jarFile = connection.getJarFile(); + final Enumeration entries = jarFile.entries(); + String name; + if (!entries.hasMoreElements()) { + return; + } + JarEntry jarEntry = entries.nextElement(); + while (jarEntry != null) { + name = jarEntry.getName(); + + if (name.contains(CLASS_SUFFIX)) { + name = name.substring(0, name.length() - CLASS_SUFFIX.length()).replace('/', '.'); + + if (name.startsWith(packageName)) { + try { + classes.add(Class.forName(name, + false /* init */, + Thread.currentThread().getContextClassLoader())); + } catch (NoClassDefFoundError e) { + // do nothing. this class hasn't been found by the + // loader, and we don't care. + } + } + } + if (entries.hasMoreElements()) { + jarEntry = entries.nextElement(); + } else { + break; + } + } + } +} diff --git a/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspectionTest.java b/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspectionTest.java new file mode 100644 index 00000000000..88d817150c1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspectionTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 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.settings.core.codeinspection; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.core.instrumentation.InstrumentableFragmentCodeInspector; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import java.util.List; + +/** + * Test suite that scans all class in app package, and perform different types of code inspection + * for conformance. + */ +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class CodeInspectionTest { + + private List> mClasses; + + @Before + public void setUp() throws Exception { + mClasses = new ClassScanner().getClassesForPackage(CodeInspector.PACKAGE_NAME); + } + + @Test + public void runCodeInspections() { + new InstrumentableFragmentCodeInspector(mClasses).run(); + } +} diff --git a/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspector.java b/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspector.java new file mode 100644 index 00000000000..80a2c110af0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/core/codeinspection/CodeInspector.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 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.settings.core.codeinspection; + +import java.util.List; + +/** + * Inspector takes a list of class objects and perform static code analysis in its {@link #run()} + * method. + */ +public abstract class CodeInspector { + + public static final String PACKAGE_NAME = "com.android.settings"; + + protected final List> mClasses; + + public CodeInspector(List> classes) { + mClasses = classes; + } + + /** + * Code inspection runner method. + */ + public abstract void run(); +} diff --git a/tests/robotests/src/com/android/settings/core/instrumentation/InstrumentableFragmentCodeInspector.java b/tests/robotests/src/com/android/settings/core/instrumentation/InstrumentableFragmentCodeInspector.java new file mode 100644 index 00000000000..5e8cfdc95d1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/core/instrumentation/InstrumentableFragmentCodeInspector.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 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.settings.core.instrumentation; + +import android.app.Fragment; +import android.util.ArraySet; + +import com.android.settings.ChooseLockPassword; +import com.android.settings.ChooseLockPattern; +import com.android.settings.CredentialCheckResultTracker; +import com.android.settings.CustomDialogPreference; +import com.android.settings.CustomEditTextPreference; +import com.android.settings.CustomListPreference; +import com.android.settings.RestrictedListPreference; +import com.android.settings.applications.AppOpsCategory; +import com.android.settings.core.codeinspection.CodeInspector; +import com.android.settings.core.lifecycle.ObservableDialogFragment; +import com.android.settings.deletionhelper.ActivationWarningFragment; +import com.android.settings.inputmethod.UserDictionaryLocalePicker; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static com.google.common.truth.Truth.assertWithMessage; + +/** + * {@link CodeInspector} that verifies all fragments implements Instrumentable. + */ +public class InstrumentableFragmentCodeInspector extends CodeInspector { + + private static final String TEST_CLASS_SUFFIX = "Test"; + + private static final List whitelist; + + static { + whitelist = new ArrayList<>(); + whitelist.add( + CustomEditTextPreference.CustomPreferenceDialogFragment.class.getName()); + whitelist.add( + CustomListPreference.CustomListPreferenceDialogFragment.class.getName()); + whitelist.add( + RestrictedListPreference.RestrictedListPreferenceDialogFragment.class.getName()); + whitelist.add(ChooseLockPassword.SaveAndFinishWorker.class.getName()); + whitelist.add(ChooseLockPattern.SaveAndFinishWorker.class.getName()); + whitelist.add(ActivationWarningFragment.class.getName()); + whitelist.add(ObservableDialogFragment.class.getName()); + whitelist.add(CustomDialogPreference.CustomPreferenceDialogFragment.class.getName()); + whitelist.add(AppOpsCategory.class.getName()); + whitelist.add(UserDictionaryLocalePicker.class.getName()); + whitelist.add(CredentialCheckResultTracker.class.getName()); + } + + public InstrumentableFragmentCodeInspector(List> classes) { + super(classes); + } + + @Override + public void run() { + final Set broken = new ArraySet<>(); + + for (Class clazz : mClasses) { + // Skip abstract classes. + if (Modifier.isAbstract(clazz.getModifiers())) { + continue; + } + final String packageName = clazz.getPackage().getName(); + // Skip classes that are not in Settings. + if (!packageName.contains(PACKAGE_NAME + ".")) { + continue; + } + final String className = clazz.getName(); + // Skip classes from tests. + if (className.endsWith(TEST_CLASS_SUFFIX)) { + continue; + } + // If it's a fragment, it must also be instrumentable. + if (Fragment.class.isAssignableFrom(clazz) + && !Instrumentable.class.isAssignableFrom(clazz) + && !whitelist.contains(className)) { + broken.add(className); + } + } + final StringBuilder sb = new StringBuilder( + "All fragment should implement Instrumentable, but the following are not:\n"); + for (String c : broken) { + sb.append(c).append("\n"); + } + assertWithMessage(sb.toString()) + .that(broken.isEmpty()) + .isTrue(); + } +}