chromium/chrome/android/junit/src/org/chromium/chrome/browser/base/DexFixerTest.java

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.base;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.system.Os;
import android.system.StructStat;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowDexFile;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.build.BuildConfig;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;

import java.io.IOException;

/** Unit tests for {@link DexFixer}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        sdk = Build.VERSION_CODES.O_MR1,
        shadows = {DexFixerTest.ShadowOs.class})
public class DexFixerTest {
    @Implements(Os.class)
    public static class ShadowOs {
        static boolean sWorldReadable = true;

        @Implementation
        public static StructStat stat(String path) {
            if (path.endsWith(".odex")) {
                return new StructStat(
                        0, 0, sWorldReadable ? 0777 : 0700, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
            }
            return Shadow.directlyOn(Os.class, "stat", ClassParameter.from(String.class, path));
        }
    }

    @Mock private Runtime mMockRuntime;
    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

    @Before
    public void setUp() {
        ShadowOs.sWorldReadable = true;
    }

    @After
    public void tearDown() {
        DexFixer.setHasIsolatedSplits(false);
    }

    private void verifyDexOpt() {
        try {
            verify(mMockRuntime)
                    .exec(Mockito.matches("/system/bin/cmd package compile -r shared \\S+"));
        } catch (IOException e) {
            // Mocks don't actually throw...
        }
    }

    @Test
    public void testFixDexIfNecessary_notNeeded() {
        @DexFixerReason int reason = DexFixer.fixDexIfNecessary(mMockRuntime);
        assertThat(reason).isEqualTo(DexFixerReason.NOT_NEEDED);
        verifyNoMoreInteractions(mMockRuntime);
    }

    @Test
    public void testFixDexIfNecessary_notReadable() {
        ShadowOs.sWorldReadable = false;
        @DexFixerReason int reason = DexFixer.fixDexIfNecessary(mMockRuntime);
        assertThat(reason).isEqualTo(DexFixerReason.NOT_READABLE);
        verifyDexOpt();
    }

    @Test
    public void testFixDexIfNecessary_update() {
        DexFixer.setHasIsolatedSplits(true);
        @DexFixerReason int reason = DexFixer.fixDexIfNecessary(mMockRuntime);
        assertThat(reason).isEqualTo(DexFixerReason.O_MR1_AFTER_UPDATE);
        verifyDexOpt();

        reason = DexFixer.fixDexIfNecessary(mMockRuntime);
        assertThat(reason).isEqualTo(DexFixerReason.NOT_NEEDED);
        verifyNoMoreInteractions(mMockRuntime);
    }

    @Test
    public void testFixDexIfNecessary_corruptDex() {
        ApplicationInfo appInfo = ContextUtils.getApplicationContext().getApplicationInfo();
        appInfo.splitNames = new String[] {"a"};
        appInfo.splitSourceDirs = new String[] {"/a.apk"};
        DexFixer.setHasIsolatedSplits(true);
        ChromeSharedPreferences.getInstance()
                .writeLong(
                        ChromePreferenceKeys.ISOLATED_SPLITS_DEX_COMPILE_VERSION,
                        BuildConfig.VERSION_CODE);

        ShadowDexFile.setIsDexOptNeeded(true);
        @DexFixerReason int reason = DexFixer.fixDexIfNecessary(mMockRuntime);
        assertThat(reason).isEqualTo(DexFixerReason.O_MR1_CORRUPTED);
        verifyDexOpt();
    }

    @Test
    public void testFixDexIfNecessary_notReadableWithSplits() {
        ApplicationInfo appInfo = ContextUtils.getApplicationContext().getApplicationInfo();
        appInfo.splitNames = new String[] {"ignored.en"};
        DexFixer.setHasIsolatedSplits(true);
        ShadowOs.sWorldReadable = false;
        ChromeSharedPreferences.getInstance()
                .writeLong(
                        ChromePreferenceKeys.ISOLATED_SPLITS_DEX_COMPILE_VERSION,
                        BuildConfig.VERSION_CODE);

        @DexFixerReason int reason = DexFixer.fixDexIfNecessary(mMockRuntime);
        assertThat(reason).isEqualTo(DexFixerReason.NOT_READABLE);
        verifyDexOpt();
    }
}