chromium/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/MultiInstanceMigrationTest.java

// Copyright 2016 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.tabmodel;

import android.content.Context;

import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import org.chromium.base.ContextUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.AdvancedMockContext;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.app.tabmodel.TabWindowManagerSingleton;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tabpersistence.TabStateDirectory;
import org.chromium.chrome.browser.tabpersistence.TabStateFileManager;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.browser.tabmodel.MockTabModelSelector;

import java.io.File;
import java.io.IOException;

/**
 * Test that migrating the old multi-instance tab state folder structure to the new one works.
 * Previously each instance had its own subdirectory for storing files. Now there is one shared
 * directory.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
public class MultiInstanceMigrationTest {
    @Mock private Profile mProfile;
    @Mock private Profile mIncognitoProfile;

    private Context mAppContext;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        Mockito.when(mIncognitoProfile.isOffTheRecord()).thenReturn(true);

        mAppContext =
                new AdvancedMockContext(
                        InstrumentationRegistry.getInstrumentation()
                                .getTargetContext()
                                .getApplicationContext());
        ContextUtils.initApplicationContextForTests(mAppContext);

        // Set the shared pref stating that the legacy file migration has occurred. The
        // multi-instance migration won't happen if the legacy path is taken.
        ChromeSharedPreferences.getInstance()
                .writeBoolean(ChromePreferenceKeys.TABMODEL_HAS_RUN_FILE_MIGRATION, true);
    }

    @After
    public void tearDown() {
        // Cleanup filesystem changes, shared preference changes and reset sMigrationTask
        // between tests - otherwise changes to these in one test will interfere with the other
        // tests.
        // TODO(crbug.com/40694404) Don't persist changes to SharedPreferences.
        ChromeSharedPreferences.getInstance()
                .writeBoolean(
                        ChromePreferenceKeys.TABMODEL_HAS_RUN_MULTI_INSTANCE_FILE_MIGRATION, false);
        FileUtils.recursivelyDeleteFile(
                TabStateDirectory.getOrCreateBaseStateDirectory(), FileUtils.DELETE_ALL);
        TabbedModeTabPersistencePolicy.resetMigrationTaskForTesting();
        TabWindowManagerSingleton.resetTabModelSelectorFactoryForTesting();
    }

    private void buildPersistentStoreAndWaitForMigration() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    MockTabModelSelector selector =
                            new MockTabModelSelector(mProfile, mIncognitoProfile, 0, 0, null);
                    TabbedModeTabPersistencePolicy persistencePolicy =
                            new TabbedModeTabPersistencePolicy(0, false, true);
                    TabPersistentStore store =
                            new TabPersistentStore(
                                    TabPersistentStore.CLIENT_TAG_REGULAR,
                                    persistencePolicy,
                                    selector,
                                    null,
                                    TabWindowManagerSingleton.getInstance());
                    store.waitForMigrationToFinish();
                });
    }

    /** Tests that normal migration of multi-instance metadata files works. */
    @Test
    @MediumTest
    @Feature({"TabPersistentStore"})
    public void testMigrateData() throws IOException {
        // Write old metadata files.
        File[] stateDirs = createOldStateDirs(TabWindowManager.MAX_SELECTORS_LEGACY, true);
        File metadataFile0 =
                new File(stateDirs[0], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);
        File metadataFile1 =
                new File(stateDirs[1], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);
        File metadataFile2 =
                new File(stateDirs[2], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);
        File customTabsStateFile =
                new File(stateDirs[3], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);

        Assert.assertTrue("Could not create metadata file 0", metadataFile0.createNewFile());
        Assert.assertTrue("Could not create metadata file 1", metadataFile1.createNewFile());
        Assert.assertTrue("Could not create metadata file 2", metadataFile2.createNewFile());
        Assert.assertTrue(
                "Could not create custom tabs metadata file", customTabsStateFile.createNewFile());

        // Create a couple of tabs for each tab state subdirectory.
        File tab0 = new File(stateDirs[0], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "0");
        File tab1 = new File(stateDirs[0], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "1");
        File tab2 = new File(stateDirs[1], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "2");
        File tab3 =
                new File(
                        stateDirs[1],
                        TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO + "3");
        File tab4 = new File(stateDirs[2], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "4");
        File tab5 =
                new File(
                        stateDirs[2],
                        TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO + "5");
        File tab6 = new File(stateDirs[3], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "6");

        Assert.assertTrue("Could not create tab 0 file", tab0.createNewFile());
        Assert.assertTrue("Could not create tab 1 file", tab1.createNewFile());
        Assert.assertTrue("Could not create tab 2 file", tab2.createNewFile());
        Assert.assertTrue("Could not create tab 3 file", tab3.createNewFile());
        Assert.assertTrue("Could not create tab 4 file", tab4.createNewFile());
        Assert.assertTrue("Could not create tab 5 file", tab5.createNewFile());
        Assert.assertTrue("Could not create tab 6 file", tab6.createNewFile());

        // Build the TabPersistentStore which will try to move the files.
        buildPersistentStoreAndWaitForMigration();

        // Make sure we don't hit the migration path again.
        Assert.assertTrue(
                ChromeSharedPreferences.getInstance()
                        .readBoolean(
                                ChromePreferenceKeys.TABMODEL_HAS_RUN_MULTI_INSTANCE_FILE_MIGRATION,
                                false));

        // Check that all metadata files moved.
        File newMetadataFile0 =
                new File(
                        stateDirs[0],
                        TabbedModeTabPersistencePolicy.getMetadataFileNameForIndex(0));
        File newMetadataFile1 =
                new File(
                        stateDirs[0],
                        TabbedModeTabPersistencePolicy.getMetadataFileNameForIndex(1));
        File newMetadataFile2 =
                new File(
                        stateDirs[0],
                        TabbedModeTabPersistencePolicy.getMetadataFileNameForIndex(2));
        File newCustomTabsStateFile =
                new File(
                        stateDirs[0],
                        TabbedModeTabPersistencePolicy.getMetadataFileNameForIndex(
                                TabModelSelectorImpl.CUSTOM_TABS_SELECTOR_INDEX));
        Assert.assertTrue("Could not find new metadata file 0", newMetadataFile0.exists());
        Assert.assertTrue("Could not find new metadata file 1", newMetadataFile1.exists());
        Assert.assertTrue("Could not find new metadata file 2", newMetadataFile2.exists());
        Assert.assertTrue(
                "Could not find new custom tabs metadata file", newCustomTabsStateFile.exists());
        Assert.assertFalse("Could still find old metadata file 0", metadataFile0.exists());
        Assert.assertFalse("Could still find old metadata file 1", metadataFile1.exists());
        Assert.assertFalse("Could still find old metadata file 2", metadataFile2.exists());
        Assert.assertFalse(
                "Could still find old custom tabs metadata file", customTabsStateFile.exists());

        // Check that tab 0 and 1 did not move.
        Assert.assertTrue("Could not find tab 0 file", tab0.exists());
        Assert.assertTrue("Could not find tab 1 file", tab1.exists());

        // Check that tabs 2-5 did move.
        File newTab2 =
                new File(stateDirs[0], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "2");
        File newTab3 =
                new File(
                        stateDirs[0],
                        TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO + "3");
        File newTab4 =
                new File(stateDirs[0], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "4");
        File newTab5 =
                new File(
                        stateDirs[0],
                        TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO + "5");
        Assert.assertTrue("Could not find new tab 2 file", newTab2.exists());
        Assert.assertTrue("Could not find new tab 3 file", newTab3.exists());
        Assert.assertTrue("Could not find new tab 4 file", newTab4.exists());
        Assert.assertTrue("Could not find new tab 5 file", newTab5.exists());
        Assert.assertFalse("Could still find old tab 2 file", tab2.exists());
        Assert.assertFalse("Could still find old tab 3 file", tab3.exists());
        Assert.assertFalse("Could still find old tab 4 file", tab4.exists());
        Assert.assertFalse("Could still find old tab 5 file", tab5.exists());

        // Check that the custom tab (tab 6) was deleted.
        File newTab6 =
                new File(stateDirs[0], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "6");
        Assert.assertFalse("Could still find old tab 6 file", tab6.exists());
        Assert.assertFalse("Found new tab 6 file. It should have been deleted.", newTab6.exists());

        // Check that old directories were deleted.
        Assert.assertFalse("Could still find old state dir 1", stateDirs[1].exists());
        Assert.assertFalse("Could still find old state dir 2", stateDirs[2].exists());
        Assert.assertFalse("Could still find old custom tabs state dir", stateDirs[3].exists());
    }

    /**
     * Tests that the metadata file migration skips unrelated files. Also tests that migration works
     * if the number of tab state subdirectories to migrate is less than {@code
     * TabWindowManagerSingleton.getMaxSimultaneousSelectors()}
     */
    @Test
    @MediumTest
    @Feature({"TabPersistentStore"})
    public void testMigrationLeavesOtherFilesAlone() throws IOException {
        // Write old metadata files and an extra file.
        File[] stateDirs = createOldStateDirs(2, false);
        File metadataFile0 =
                new File(stateDirs[0], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);
        File metadataFile1 =
                new File(stateDirs[1], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);
        File tab0 = new File(stateDirs[0], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "0");
        File tab1 = new File(stateDirs[1], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "1");
        File otherFile = new File(stateDirs[1], "other.file");

        Assert.assertTrue("Could not create metadata file 0", metadataFile0.createNewFile());
        Assert.assertTrue("Could not create metadata file 1", metadataFile1.createNewFile());
        Assert.assertTrue("Could not create tab 0 file", tab0.createNewFile());
        Assert.assertTrue("Could not create tab 1 file", tab1.createNewFile());
        Assert.assertTrue("Could not create other file", otherFile.createNewFile());

        // Build the TabPersistentStore which will try to move the files.
        buildPersistentStoreAndWaitForMigration();

        // Check that the other file wasn't moved.
        File newOtherFile = new File(stateDirs[0], "other.file");
        Assert.assertFalse("Could find new other file", newOtherFile.exists());
        Assert.assertTrue("Could not find original other file", otherFile.exists());

        // Check that the metadata files were renamed and/or moved.
        File newMetadataFile0 =
                new File(
                        stateDirs[0],
                        TabbedModeTabPersistencePolicy.getMetadataFileNameForIndex(0));
        File newMetadataFile1 =
                new File(
                        stateDirs[0],
                        TabbedModeTabPersistencePolicy.getMetadataFileNameForIndex(1));
        Assert.assertTrue("Could not find new metadata file 0", newMetadataFile0.exists());
        Assert.assertTrue("Could not find new metadata file 1", newMetadataFile1.exists());
        Assert.assertFalse("Could still find old metadata file 0", metadataFile0.exists());
        Assert.assertFalse("Could still find old metadata file 1", metadataFile1.exists());

        // Check that tab 0 did not move.
        Assert.assertTrue("Could not find tab 0 file", tab0.exists());

        // Check that tab 1 did move.
        File newTab1 =
                new File(stateDirs[0], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "1");
        Assert.assertTrue("Could not find tab 1 file", newTab1.exists());
        Assert.assertFalse("Could still find old tab 1 file", tab1.exists());
    }

    /**
     * Tests that migration of multi-instance metadata files works when tab files with the same name
     * exists in both directories.
     */
    @Test
    @MediumTest
    @Feature({"TabPersistentStore"})
    public void testMigrateDataDuplicateTabFiles() throws IOException {
        // Write old metadata files.
        File[] stateDirs = createOldStateDirs(2, false);
        File metadataFile0 =
                new File(stateDirs[0], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);
        File metadataFile1 =
                new File(stateDirs[1], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);

        Assert.assertTrue("Could not create metadata file 0", metadataFile0.createNewFile());
        Assert.assertTrue("Could not create metadata file 1", metadataFile1.createNewFile());

        // Create duplicate "tab0" files and ensure tab0Dir1 has been modified more recently so that
        // it overwrites tab0Dir0.
        File tab0Dir0 =
                new File(stateDirs[0], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "0");
        File tab0Dir1 =
                new File(stateDirs[1], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "0");
        Assert.assertTrue("Could not create tab 0-0 file", tab0Dir0.createNewFile());
        Assert.assertTrue("Could not create tab 0-1 file", tab0Dir1.createNewFile());
        long expectedTab0LastModifiedTime = tab0Dir0.lastModified() + 1000;
        if (!tab0Dir1.setLastModified(expectedTab0LastModifiedTime)) {
            Assert.fail("Failed to set last modified time.");
        }

        // Create duplicate "tab1" files and ensure tab1Dir0 has been modified more recently so that
        // it does not get overwritten.
        File tab1Dir0 =
                new File(stateDirs[0], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "1");
        File tab1Dir1 =
                new File(stateDirs[1], TabStateFileManager.SAVED_TAB_STATE_FILE_PREFIX + "1");
        Assert.assertTrue("Could not create tab 1-0 file", tab1Dir0.createNewFile());
        Assert.assertTrue("Could not create tab 1-1 file", tab1Dir1.createNewFile());
        long expectedTab1LastModifiedTime = tab1Dir1.lastModified() + 1000;
        if (!tab1Dir0.setLastModified(expectedTab1LastModifiedTime)) {
            Assert.fail("Failed to set last modified time.");
        }

        // Build the TabPersistentStore which will try to move the files.
        buildPersistentStoreAndWaitForMigration();

        // Check that "tab0" still exists and has the expected last modified time.
        Assert.assertTrue("Could not find tab 0 file", tab0Dir0.exists());
        Assert.assertFalse("Could still find old tab 0 file", tab0Dir1.exists());
        Assert.assertEquals(
                "tab 0 file not overwritten properly",
                expectedTab0LastModifiedTime,
                tab0Dir0.lastModified());

        // Check that "tab1" still exists and has the expected last modified time.
        Assert.assertTrue("Could not find tab 1 file", tab1Dir0.exists());
        Assert.assertFalse("Could still find old tab 1 file", tab1Dir1.exists());
        Assert.assertEquals(
                "tab 1 file unexpectedly overwritten",
                expectedTab1LastModifiedTime,
                tab1Dir0.lastModified());

        // Check that old directory was deleted.
        Assert.assertFalse("Could still find old state dir 1", stateDirs[1].exists());
    }

    /**
     * Tests that tab_state0 is not overwritten if it already exists when migration is attempted.
     */
    @Test
    @MediumTest
    @Feature({"TabPersistentStore"})
    public void testNewMetataFileExists() throws Exception {
        // Set up two old metadata files.
        int maxCount =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                TabWindowManagerSingleton.getInstance()
                                        .getMaxSimultaneousSelectors());
        File[] stateDirs = createOldStateDirs(maxCount, true);
        File metadataFile0 =
                new File(stateDirs[0], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);
        File metadataFile1 =
                new File(stateDirs[1], TabbedModeTabPersistencePolicy.LEGACY_SAVED_STATE_FILE);

        Assert.assertTrue("Could not create metadata file 0", metadataFile0.createNewFile());
        Assert.assertTrue("Could not create metadata file 1", metadataFile1.createNewFile());

        // Create a new metadata file.
        File newMetadataFile0 =
                new File(
                        stateDirs[0],
                        TabbedModeTabPersistencePolicy.getMetadataFileNameForIndex(0));
        Assert.assertTrue("Could not create new metadata file 0", newMetadataFile0.createNewFile());
        long expectedLastModifiedTime = newMetadataFile0.lastModified() - 1000000;
        if (!newMetadataFile0.setLastModified(expectedLastModifiedTime)) {
            Assert.fail("Failed to set last modified time.");
        }

        // Build the TabPersistentStore which will try to move the files.
        buildPersistentStoreAndWaitForMigration();

        // Check that new metadata file was not overwritten during migration.
        Assert.assertEquals(
                "State file 0 unexpectedly overwritten",
                expectedLastModifiedTime,
                newMetadataFile0.lastModified());

        // Check that migration of other files still occurred.
        File newMetadataFile1 =
                new File(
                        stateDirs[0],
                        TabbedModeTabPersistencePolicy.getMetadataFileNameForIndex(1));
        Assert.assertTrue("Could not find new metadata file 1", newMetadataFile1.exists());
    }

    /**
     * Creates tab state directories using the old pre-multi-instance migration file paths where
     * each tab model had its own state directory.
     *
     * @param numRegularDirsToCreate The number of regular tab state directories to create.
     * @param createCustomTabsDir Whether a custom tabs directory should be created.
     * @return An array containing the tab state directories. If createCustomTabsDir is true, a
     *     directory for custom tabs will be added to the end of the array.
     */
    private File[] createOldStateDirs(int numRegularDirsToCreate, boolean createCustomTabsDir) {
        int numDirsToCreate =
                createCustomTabsDir ? numRegularDirsToCreate + 1 : numRegularDirsToCreate;
        File[] stateDirs = new File[numDirsToCreate];
        for (int i = 0; i < numRegularDirsToCreate; i++) {
            stateDirs[i] =
                    new File(
                            TabStateDirectory.getOrCreateBaseStateDirectory(), Integer.toString(i));
            if (!stateDirs[i].exists()) {
                Assert.assertTrue("Could not create state dir " + i, stateDirs[i].mkdirs());
            }
        }

        if (createCustomTabsDir) {
            stateDirs[numDirsToCreate - 1] =
                    new File(
                            TabStateDirectory.getOrCreateBaseStateDirectory(),
                            Integer.toString(TabModelSelectorImpl.CUSTOM_TABS_SELECTOR_INDEX));
            if (!stateDirs[numDirsToCreate - 1].exists()) {
                Assert.assertTrue(
                        "Could not create custom tab state dir",
                        stateDirs[numDirsToCreate - 1].mkdirs());
            }
        }

        return stateDirs;
    }
}