| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.android_webview.test; |
| |
| import static org.chromium.android_webview.test.OnlyRunIn.ProcessMode.SINGLE_PROCESS; |
| |
| import android.os.ParcelFileDescriptor; |
| import android.support.test.filters.MediumTest; |
| |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import org.chromium.android_webview.VariationsUtils; |
| import org.chromium.android_webview.services.ServiceInit; |
| import org.chromium.android_webview.services.VariationsSeedHolder; |
| import org.chromium.android_webview.test.util.VariationsTestUtils; |
| import org.chromium.base.test.util.CallbackHelper; |
| import org.chromium.components.variations.firstrun.VariationsSeedFetcher.SeedInfo; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| /** |
| * Test VariationsSeedHolder. |
| */ |
| @RunWith(AwJUnit4ClassRunner.class) |
| @OnlyRunIn(SINGLE_PROCESS) |
| public class VariationsSeedHolderTest { |
| private class TestHolder extends VariationsSeedHolder { |
| private final CallbackHelper mWriteFinished; // notified after each writeSeedIfNewer |
| private final CallbackHelper mUpdateFinished; // notified after each updateSeed |
| |
| public TestHolder() { |
| mWriteFinished = new CallbackHelper(); |
| mUpdateFinished = new CallbackHelper(); |
| } |
| |
| public TestHolder(CallbackHelper writeFinished) { |
| mWriteFinished = writeFinished; |
| mUpdateFinished = new CallbackHelper(); |
| } |
| |
| // Don't schedule the seed download job. |
| @Override |
| public void scheduleFetchIfNeeded() {} |
| |
| @Override |
| public void onWriteFinished() { |
| mWriteFinished.notifyCalled(); |
| } |
| |
| public void writeSeedIfNewerBlocking(File destination, long date) |
| throws IOException, TimeoutException, InterruptedException { |
| ParcelFileDescriptor fd = null; |
| try { |
| fd = ParcelFileDescriptor.open(destination, ParcelFileDescriptor.MODE_WRITE_ONLY); |
| int calls = mWriteFinished.getCallCount(); |
| writeSeedIfNewer(fd, date); |
| mWriteFinished.waitForCallback(calls); |
| } finally { |
| if (fd != null) fd.close(); |
| } |
| } |
| |
| public void updateSeedBlocking(SeedInfo newSeed) |
| throws TimeoutException, InterruptedException { |
| int calls = mUpdateFinished.getCallCount(); |
| updateSeed(newSeed, /*onFinished=*/() -> mUpdateFinished.notifyCalled()); |
| mUpdateFinished.waitForCallback(calls); |
| } |
| } |
| |
| @Before |
| public void setUp() throws IOException { |
| ServiceInit.setPrivateDataDirectorySuffix(); |
| VariationsTestUtils.deleteSeeds(); |
| } |
| |
| @After |
| public void tearDown() throws IOException { |
| VariationsTestUtils.deleteSeeds(); |
| } |
| |
| // Request that the seed holder write its current seed to a file when the holder has no seed. No |
| // write should happen. |
| @Test |
| @MediumTest |
| public void testWriteNoSeed() throws IOException, TimeoutException, InterruptedException { |
| TestHolder holder = new TestHolder(); |
| File file = null; |
| try { |
| file = File.createTempFile("seed", null, null); |
| holder.writeSeedIfNewerBlocking(file, Long.MIN_VALUE); |
| Assert.assertEquals(0L, file.length()); |
| } finally { |
| if (file != null) file.delete(); |
| } |
| } |
| |
| // Test updating the holder with the mock seed, and then request the holder writes that seed to |
| // an empty file. The written seed should match the mock seed. |
| @Test |
| @MediumTest |
| public void testUpdateAndWriteToEmptySeed() |
| throws IOException, TimeoutException, InterruptedException { |
| try { |
| TestHolder holder = new TestHolder(); |
| holder.updateSeedBlocking(VariationsTestUtils.createMockSeed()); |
| File file = null; |
| try { |
| file = File.createTempFile("seed", null, null); |
| holder.writeSeedIfNewerBlocking(file, Long.MIN_VALUE); |
| SeedInfo readSeed = VariationsUtils.readSeedFile(file); |
| VariationsTestUtils.assertSeedsEqual( |
| VariationsTestUtils.createMockSeed(), readSeed); |
| } finally { |
| if (file != null) file.delete(); |
| } |
| } finally { |
| VariationsTestUtils.deleteSeeds(); // Remove the holder's saved seed. |
| } |
| } |
| |
| // Test updating the holder with the mock seed, and then request the holder writes that seed to |
| // a file. Pretend the file already contains a seed, but it's out of date, so writing should |
| // proceed. The written seed should match the mock seed. |
| @Test |
| @MediumTest |
| public void testUpdateAndWriteToStaleSeed() |
| throws IOException, TimeoutException, InterruptedException, ParseException { |
| try { |
| SeedInfo mockSeed = VariationsTestUtils.createMockSeed(); |
| long mockDateMinusOneDay = mockSeed.parseDate().getTime() - TimeUnit.DAYS.toMillis(1); |
| TestHolder holder = new TestHolder(); |
| holder.updateSeedBlocking(mockSeed); |
| File file = null; |
| try { |
| file = File.createTempFile("seed", null, null); |
| holder.writeSeedIfNewerBlocking(file, mockDateMinusOneDay); |
| SeedInfo readSeed = VariationsUtils.readSeedFile(file); |
| VariationsTestUtils.assertSeedsEqual(mockSeed, readSeed); |
| } finally { |
| if (file != null) file.delete(); |
| } |
| } finally { |
| VariationsTestUtils.deleteSeeds(); // Remove the holder's saved seed. |
| } |
| } |
| |
| // Test updating the holder with the mock seed, and then request the holder writes that seed to |
| // a file. Pretend the file already contains an up-to-date seed, so no write should happen. |
| @Test |
| @MediumTest |
| public void testUpdateAndWriteToFreshSeed() |
| throws IOException, TimeoutException, InterruptedException, ParseException { |
| try { |
| SeedInfo mockSeed = VariationsTestUtils.createMockSeed(); |
| long mockDate = mockSeed.parseDate().getTime(); |
| TestHolder holder = new TestHolder(); |
| holder.updateSeedBlocking(mockSeed); |
| File file = null; |
| try { |
| file = File.createTempFile("seed", null, null); |
| holder.writeSeedIfNewerBlocking(file, mockDate); |
| Assert.assertEquals(0L, file.length()); |
| } finally { |
| if (file != null) file.delete(); |
| } |
| } finally { |
| VariationsTestUtils.deleteSeeds(); // Remove the holder's saved seed. |
| } |
| } |
| |
| @Test |
| @MediumTest |
| public void testConcurrentUpdatesAndWrites() |
| throws IOException, FileNotFoundException, InterruptedException, TimeoutException { |
| ArrayList<File> files = new ArrayList<>(); |
| try { |
| ArrayList<ParcelFileDescriptor> fds = new ArrayList<>(); |
| try { |
| // Create a series of mock seeds (5 chosen arbitrarily) which will be "downloaded" |
| // to the holder via updateSeed. Differentiate the seeds by filling each seedData |
| // field with the index of the seed. |
| SeedInfo[] mockSeeds = new SeedInfo[5]; |
| for (int i = 0; i < mockSeeds.length; i++) { |
| mockSeeds[i] = VariationsTestUtils.createMockSeed(); |
| mockSeeds[i].seedData = new byte[100]; |
| Arrays.fill(mockSeeds[i].seedData, (byte) i); |
| } |
| |
| // Used to track the completion of every updateSeed and writeSeedIfNewer call. |
| CallbackHelper callbackHelper = new CallbackHelper(); |
| int callbacksExpected = 0; |
| |
| // TestHolder will notify callbackHelper whenever a writeSeedIfNewer request |
| // completes. |
| TestHolder holder = new TestHolder(callbackHelper); |
| |
| // "Download" each mock seed to the holder. |
| for (int i = 0; i < mockSeeds.length; i++) { |
| callbacksExpected++; |
| holder.updateSeed( |
| mockSeeds[i], /*onFinished=*/() -> callbackHelper.notifyCalled()); |
| |
| // Between each "download", schedule a few (3 chosen arbitrarily) requests for |
| // the seed, creating a new file to receive each request. |
| for (int write = 0; write < 3; write++) { |
| File file = File.createTempFile("seed", null, null); |
| files.add(file); |
| |
| ParcelFileDescriptor fd = ParcelFileDescriptor.open( |
| file, ParcelFileDescriptor.MODE_WRITE_ONLY); |
| fds.add(fd); |
| |
| callbacksExpected++; |
| holder.writeSeedIfNewer(fd, Long.MIN_VALUE); |
| } |
| } |
| |
| // Wait for all updateSeed and writeSeedIfNewer calls to finish. |
| callbackHelper.waitForCallback(0, callbacksExpected); |
| |
| // Read each requested seed and ensure it's either empty (if the request was |
| // scheduled before any "downloads") or it matches one of the mock seeds. For the |
| // requests that got a mock seed, there's no guarantee as to which seed they got. |
| for (int i = 0; i < files.size(); i++) { |
| if (files.get(i).length() == 0) continue; |
| |
| SeedInfo readSeed = VariationsUtils.readSeedFile(files.get(i)); |
| Assert.assertNotNull("Failed reading seed index " + i, readSeed); |
| |
| boolean match = false; |
| for (SeedInfo mockSeed : mockSeeds) { |
| if (Arrays.equals(readSeed.seedData, mockSeed.seedData)) { |
| match = true; |
| break; |
| } |
| } |
| Assert.assertTrue("Seed data " + Arrays.toString(readSeed.seedData) |
| + " read from seed index " + i |
| + " does not match any written data", |
| match); |
| } |
| } finally { |
| for (ParcelFileDescriptor fd : fds) { |
| fd.close(); |
| } |
| } |
| } finally { |
| for (File file : files) { |
| if (!file.delete()) { |
| throw new IOException("Failed to delete " + file); |
| } |
| } |
| VariationsTestUtils.deleteSeeds(); // Remove the holder's saved seed. |
| } |
| } |
| } |