// 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.suggestions.mostvisited;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import androidx.core.util.AtomicFile;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.suggestions.SiteSuggestion;
import org.chromium.chrome.browser.suggestions.tile.Tile;
import org.chromium.chrome.browser.suggestions.tile.TileSectionType;
import org.chromium.chrome.browser.suggestions.tile.TileSource;
import org.chromium.chrome.browser.suggestions.tile.TileTitleSource;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.url.GURL;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
/** Instrumentation tests for {@link MostVisitedSitesMetadataUtils}. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class MostVisitedSitesMetadataUtilsTest {
@Rule public ChromeTabbedActivityTestRule mTestSetupRule = new ChromeTabbedActivityTestRule();
private MostVisitedSitesMetadataUtils mMostVisitedSitesMetadataUtils;
@Before
public void setUp() {
mTestSetupRule.startMainActivityOnBlankPage();
mMostVisitedSitesMetadataUtils = MostVisitedSitesMetadataUtils.getInstance();
}
@Test
@SmallTest
public void testSaveRestoreConsistency() throws InterruptedException, IOException {
List<Tile> expectedSuggestionTiles = createFakeSiteSuggestionTiles1();
// Get old file and ensure to delete it.
File oldFile = MostVisitedSitesMetadataUtils.getOrCreateTopSitesDirectory();
assertTrue(oldFile.delete() && !oldFile.exists());
// Save suggestion lists to file.
final CountDownLatch latch = new CountDownLatch(1);
MostVisitedSitesMetadataUtils.saveSuggestionListsToFile(
expectedSuggestionTiles, latch::countDown);
// Wait util the file has been saved.
latch.await();
// Restore list from file after saving finished.
List<Tile> sitesAfterRestore = MostVisitedSitesMetadataUtils.restoreFileToSuggestionLists();
// Ensure that the new list equals to old list.
assertEquals(expectedSuggestionTiles.size(), sitesAfterRestore.size());
for (int i = 0; i < expectedSuggestionTiles.size(); i++) {
assertEquals(
expectedSuggestionTiles.get(i).getData(), sitesAfterRestore.get(i).getData());
}
}
@Test(expected = IOException.class)
@SmallTest
public void testRestoreException() throws IOException {
// Get old file and ensure to delete it.
File oldFile = MostVisitedSitesMetadataUtils.getOrCreateTopSitesDirectory();
assertTrue(oldFile.delete() || !oldFile.exists());
// Call restore function and ensure it throws an IOException.
MostVisitedSitesMetadataUtils.restoreFileToSuggestionLists();
}
/**
* Test when current task is not finished, all coming tasks will be set as the pending task.
* Besides, the latest task will override the old one.
*/
@Test
@SmallTest
public void testCurrentNotNull() {
mMostVisitedSitesMetadataUtils.setCurrentTaskForTesting(() -> {});
Runnable task1 =
() ->
mMostVisitedSitesMetadataUtils.saveSuggestionListsToFile(
createFakeSiteSuggestionTiles1());
List<Tile> task2Tiles = createFakeSiteSuggestionTiles2();
Runnable task2 = () -> mMostVisitedSitesMetadataUtils.saveSuggestionListsToFile(task2Tiles);
// If current task is not null, all saving tasks should be set as pending task.
ThreadUtils.runOnUiThreadBlocking(
() -> {
task1.run();
task2.run();
});
// newTopSites1 should be skipped and newTopSites2 should be the pending task.
assertEquals(
task2Tiles.size(),
mMostVisitedSitesMetadataUtils.getPendingTaskTilesNumForTesting());
}
/**
* Test when current task is finished, the pending task should be set as current task and run.
*/
@Test
@SmallTest
public void testTasksContinuity() {
AtomicBoolean isPendingRun = new AtomicBoolean(false);
// Set and run current task.
assertNull(mMostVisitedSitesMetadataUtils.getCurrentTaskForTesting());
ThreadUtils.runOnUiThreadBlocking(
() ->
mMostVisitedSitesMetadataUtils.saveSuggestionListsToFile(
createFakeSiteSuggestionTiles1()));
// When current task is not finished, set pending task.
assertNotNull(mMostVisitedSitesMetadataUtils.getCurrentTaskForTesting());
mMostVisitedSitesMetadataUtils.setPendingTaskForTesting(() -> isPendingRun.set(true));
// isPendingRun should eventually become true.
CriteriaHelper.pollInstrumentationThread(isPendingRun::get);
}
/**
* Test that if the allowlist icon path (a deprecated field) is set on an old site suggestion
* that the site suggestion is being deserialized correctly.
*/
@Test
@SmallTest
public void testTileWithAllowlistIconPath() throws IOException {
// Create a site suggestion that has the allowlist icon path set.
SiteSuggestion expectedSiteSuggestion = createFakeSiteSuggestionTiles2().get(0).getData();
ByteArrayOutputStream output = new ByteArrayOutputStream();
DataOutputStream stream = new DataOutputStream(output);
stream.writeInt(/* topSitesCount= */ 1);
stream.writeInt(/* cacheVersion= */ 1);
stream.writeInt(/* index= */ 0);
stream.writeUTF(expectedSiteSuggestion.title);
stream.writeUTF(expectedSiteSuggestion.url.serialize());
// Hardcode this since the field is deprecated and can't be set in another way.
stream.writeUTF("allowlistIcon");
stream.writeInt(expectedSiteSuggestion.titleSource);
stream.writeInt(expectedSiteSuggestion.source);
stream.writeInt(expectedSiteSuggestion.sectionType);
stream.close();
byte[] listData = output.toByteArray();
// Save the site suggestion.
File topSitesDirectory = MostVisitedSitesMetadataUtils.getOrCreateTopSitesDirectory();
File topSitesFile = new File(topSitesDirectory, "top_sites");
AtomicFile file = new AtomicFile(topSitesFile);
FileOutputStream fileStream = null;
fileStream = file.startWrite();
fileStream.write(listData, 0, listData.length);
file.finishWrite(fileStream);
// Restore list from file after saving finished.
List<Tile> sitesAfterRestore = MostVisitedSitesMetadataUtils.restoreFileToSuggestionLists();
// Ensure that the new suggestion equals to old suggestion.
assertEquals(1, sitesAfterRestore.size());
assertEquals(sitesAfterRestore.get(0).getData(), expectedSiteSuggestion);
}
private static List<Tile> createFakeSiteSuggestionTiles1() {
List<Tile> suggestionTiles = new ArrayList<>();
SiteSuggestion data =
new SiteSuggestion(
"0 TOP_SITES",
new GURL("https://www.foo.com"),
TileTitleSource.TITLE_TAG,
TileSource.TOP_SITES,
TileSectionType.PERSONALIZED);
suggestionTiles.add(new Tile(data, 0));
data =
new SiteSuggestion(
"1 ALLOWLIST",
new GURL("https://www.bar.com"),
TileTitleSource.UNKNOWN,
TileSource.ALLOWLIST,
TileSectionType.PERSONALIZED);
suggestionTiles.add(new Tile(data, 1));
return suggestionTiles;
}
private static List<Tile> createFakeSiteSuggestionTiles2() {
List<Tile> suggestionTiles = new ArrayList<>();
SiteSuggestion data =
new SiteSuggestion(
"0 TOP_SITES",
new GURL("https://www.baz.com"),
TileTitleSource.TITLE_TAG,
TileSource.TOP_SITES,
TileSectionType.PERSONALIZED);
suggestionTiles.add(new Tile(data, 0));
return suggestionTiles;
}
}