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

Commit fcbb6528 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add test to ensure all future fragments implements logging."

parents 737ae83a 8b8218c0
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();
    }
}