Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8b8218c0 authored by Fan Zhang's avatar Fan Zhang
Browse files

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
parent 3d9fd0cd
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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;
+130 −0
Original line number Diff line number Diff line
/*
 * 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<Class<?>> getClassesForPackage(String packageName)
            throws ClassNotFoundException {
        final List<Class<?>> classes = new ArrayList<>();

        try {
            final Enumeration<URL> 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<Class<?>> 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<Class<?>> classes) throws ClassNotFoundException, IOException {
        final JarFile jarFile = connection.getJarFile();
        final Enumeration<JarEntry> 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;
            }
        }
    }
}
+49 −0
Original line number Diff line number Diff line
/*
 * 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<Class<?>> mClasses;

    @Before
    public void setUp() throws Exception {
        mClasses = new ClassScanner().getClassesForPackage(CodeInspector.PACKAGE_NAME);
    }

    @Test
    public void runCodeInspections() {
        new InstrumentableFragmentCodeInspector(mClasses).run();
    }
}
+39 −0
Original line number Diff line number Diff line
/*
 * 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<Class<?>> mClasses;

    public CodeInspector(List<Class<?>> classes) {
        mClasses = classes;
    }

    /**
     * Code inspection runner method.
     */
    public abstract void run();
}
+108 −0
Original line number Diff line number Diff line
/*
 * 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<String> 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<Class<?>> classes) {
        super(classes);
    }

    @Override
    public void run() {
        final Set<String> 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();
    }
}