chromium/android_webview/javatests/src/org/chromium/android_webview/test/VariationsSeedLoaderTest.java

// Copyright 2018 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.android_webview.test;

import static org.chromium.android_webview.test.OnlyRunIn.ProcessMode.EITHER_PROCESS;

import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;

import androidx.test.filters.MediumTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;

import org.chromium.android_webview.common.AwSwitches;
import org.chromium.android_webview.common.variations.VariationsServiceMetricsHelper;
import org.chromium.android_webview.common.variations.VariationsUtils;
import org.chromium.android_webview.test.services.MockVariationsSeedServer;
import org.chromium.android_webview.test.util.VariationsTestUtils;
import org.chromium.android_webview.variations.VariationsSeedLoader;
import org.chromium.base.ContextUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.HistogramWatcher;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/** Test VariationsSeedLoader. */
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
@OnlyRunIn(EITHER_PROCESS) // These tests don't use the renderer process
public class VariationsSeedLoaderTest extends AwParameterizedTest {
    private static final long CURRENT_TIME_MILLIS = 1234567890;
    private static final long EXPIRED_TIMESTAMP = 0;
    private static final long TIMEOUT_MILLIS = 10000;

    // Needed for tests that test histograms, which rely on native code.
    @Rule public AwActivityTestRule mActivityTestRule;

    /**
     * Helper class to interact with {@link TestLoader}. This can be used to retrieve whether
     * TestLoader requested a seed.
     */
    public static class TestLoaderResult extends CallbackHelper {
        private volatile boolean mBackgroundWorkFinished;
        private volatile boolean mForegroundWorkFinished;
        private volatile boolean mSeedRequested;

        public boolean wasSeedRequested() {
            assert getCallCount() > 0;
            return mSeedRequested;
        }

        public void markSeedRequested() {
            mSeedRequested = true;
        }

        public void onBackgroundWorkFinished() {
            mBackgroundWorkFinished = true;
            maybeNotifyCalled();
        }

        public void onForegroundWorkFinished() {
            mForegroundWorkFinished = true;
            maybeNotifyCalled();
        }

        private void maybeNotifyCalled() {
            if (mBackgroundWorkFinished && mForegroundWorkFinished) {
                notifyCalled();
            }
        }
    }

    /**
     * A {@link VariationsSeedLoader} which is suitable for integration tests. This overrides the
     * default timeout to be suitable for integration tests, allowing the test to call
     * startVariationsInit() immediately before finishVariationsInit(). This also overrides the
     * service Intent to match the test environment.
     */
    public static class TestLoader extends VariationsSeedLoader {
        private TestLoaderResult mResult;

        public TestLoader(TestLoaderResult result) {
            mResult = result;
        }

        // Bind to the MockVariationsSeedServer built in to the instrumentation test app, rather
        // than the real server in the WebView provider.
        @Override
        protected Intent getServerIntent() {
            return new Intent(ContextUtils.getApplicationContext(), MockVariationsSeedServer.class);
        }

        @Override
        protected boolean requestSeedFromService(long oldSeedDate) {
            boolean result = super.requestSeedFromService(oldSeedDate);
            mResult.markSeedRequested();
            return result;
        }

        @Override
        protected void onBackgroundWorkFinished() {
            mResult.onBackgroundWorkFinished();
        }

        @Override
        protected long getSeedLoadTimeoutMillis() {
            return TIMEOUT_MILLIS;
        }

        @Override
        protected long getCurrentTimeMillis() {
            return CURRENT_TIME_MILLIS;
        }
    }

    private Handler mMainHandler;

    public VariationsSeedLoaderTest(AwSettingsMutation param) {
        this.mActivityTestRule = new AwActivityTestRule(param.getMutation());
    }

    // Create a TestLoader, run it on the UI thread, and block until it's finished. The return value
    // indicates whether the loader decided to request a new seed.
    private boolean runTestLoaderBlocking() throws TimeoutException {
        final TestLoaderResult result = new TestLoaderResult();
        Runnable run =
                () -> {
                    TestLoader loader = new TestLoader(result);
                    loader.startVariationsInit();
                    loader.finishVariationsInit();
                    result.onForegroundWorkFinished();
                };

        CallbackHelper onRequestReceived = MockVariationsSeedServer.getRequestHelper();
        int requestsReceived = onRequestReceived.getCallCount();
        Assert.assertTrue("Failed to post seed loader Runnable", mMainHandler.post(run));
        result.waitForCallback("Timed out waiting for loader to finish.", 0);
        if (result.wasSeedRequested()) {
            onRequestReceived.waitForCallback(
                    "Seed requested, but timed out waiting for request"
                            + " to arrive in MockVariationsSeedServer",
                    requestsReceived);
            return true;
        }
        return false;
    }

    @Before
    public void setUp() throws IOException {
        mMainHandler = new Handler(Looper.getMainLooper());
        VariationsTestUtils.deleteSeeds();
    }

    @After
    public void tearDown() throws IOException {
        VariationsTestUtils.deleteSeeds();
    }

    // Test that Seed and AppSeed Freshness diff is correct and recorded
    @Test
    @MediumTest
    public void testRecordSeedDiff() throws Exception {
        // The first line is needed to set the seed freshness to zero
        // in order to calculate the diff correctly
        VariationsSeedLoader.cacheSeedFreshness(0);
        long seedFreshnessInMinutes = 100;
        long appSeedFreshnessInMinutes = 40;
        long diff = seedFreshnessInMinutes - appSeedFreshnessInMinutes;
        var histogramWatcherOne =
                HistogramWatcher.newSingleRecordWatcher(
                        VariationsSeedLoader.SEED_FRESHNESS_DIFF_HISTOGRAM_NAME, (int) diff);
        VariationsSeedLoader.cacheAppSeedFreshness(appSeedFreshnessInMinutes);
        VariationsSeedLoader.cacheSeedFreshness(seedFreshnessInMinutes);
        histogramWatcherOne.assertExpected();

        var histogramWatcherTwo =
                HistogramWatcher.newSingleRecordWatcher(
                        VariationsSeedLoader.SEED_FRESHNESS_DIFF_HISTOGRAM_NAME, (int) diff);
        VariationsSeedLoader.cacheSeedFreshness(seedFreshnessInMinutes);
        VariationsSeedLoader.cacheAppSeedFreshness(appSeedFreshnessInMinutes);
        histogramWatcherTwo.assertExpected();
    }

    // Test the case that:
    // VariationsUtils.getSeedFile() - doesn't exist
    // VariationsUtils.getNewSeedFile() - doesn't exist
    @Test
    @MediumTest
    public void testHaveNoSeed() throws Exception {
        try {
            boolean seedRequested = runTestLoaderBlocking();

            // Since there was no seed, another seed should be requested.
            Assert.assertTrue("No seed requested", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Test the case that:
    // VariationsUtils.getSeedFile() - exists, timestamp = now
    // VariationsUtils.getNewSeedFile() - doesn't exist
    @Test
    @MediumTest
    public void testHaveFreshSeed() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            Assert.assertTrue("Seed file already exists", oldFile.createNewFile());
            VariationsTestUtils.writeMockSeed(oldFile);

            boolean seedRequested = runTestLoaderBlocking();

            // Since there was a fresh seed, we should not request another seed.
            Assert.assertFalse(
                    "New seed was requested when it should not have been", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Test the case that:
    // VariationsUtils.getSeedFile() - exists, timestamp = epoch
    // VariationsUtils.getNewSeedFile() - doesn't exist
    @Test
    @MediumTest
    public void testHaveExpiredSeed() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            Assert.assertTrue("Seed file already exists", oldFile.createNewFile());
            VariationsTestUtils.writeMockSeed(oldFile);
            oldFile.setLastModified(0);

            boolean seedRequested = runTestLoaderBlocking();

            // Since the seed was expired, another seed should be requested.
            Assert.assertTrue("No seed requested", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Test the case that:
    // VariationsUtils.getSeedFile() - doesn't exist
    // VariationsUtils.getNewSeedFile() - exists, timestamp = now
    @Test
    @MediumTest
    public void testHaveFreshNewSeed() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            File newFile = VariationsUtils.getNewSeedFile();
            Assert.assertTrue("New seed file already exists", newFile.createNewFile());
            VariationsTestUtils.writeMockSeed(newFile);

            boolean seedRequested = runTestLoaderBlocking();

            // The "new" seed should have been renamed to the "old" seed.
            Assert.assertTrue("Old seed not found", oldFile.exists());
            Assert.assertFalse("New seed still exists", newFile.exists());

            // Since the "new" seed was fresh, we should not request another seed.
            Assert.assertFalse(
                    "New seed was requested when it should not have been", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Test the case that:
    // VariationsUtils.getSeedFile() - doesn't exist
    // VariationsUtils.getNewSeedFile() - exists, timestamp = epoch
    @Test
    @MediumTest
    public void testHaveExpiredNewSeed() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            File newFile = VariationsUtils.getNewSeedFile();
            Assert.assertTrue("Seed file already exists", newFile.createNewFile());
            VariationsTestUtils.writeMockSeed(newFile);
            newFile.setLastModified(0);

            boolean seedRequested = runTestLoaderBlocking();

            // The "new" seed should have been renamed to the "old" seed. Another empty "new" seed
            // should have been created as a destination for the request.
            Assert.assertTrue("Old seed not found", oldFile.exists());
            Assert.assertTrue("New seed not found", newFile.exists());
            Assert.assertTrue("New seed is not empty", newFile.length() == 0L);

            // Since the "new" seed was expired, another seed should be requested.
            Assert.assertTrue("No seed requested", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Test the case that:
    // VariationsUtils.getSeedFile() - doesn't exist
    // VariationsUtils.getNewSeedFile() - exists, empty
    @Test
    @MediumTest
    public void testHaveEmptyNewSeed() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            File newFile = VariationsUtils.getNewSeedFile();
            Assert.assertTrue("Seed file should not already exist", newFile.createNewFile());

            boolean seedRequested = runTestLoaderBlocking();

            // Neither file should have been touched.
            Assert.assertFalse("Old seed file should not exist", oldFile.exists());
            Assert.assertTrue("New seed file not found", newFile.exists());
            Assert.assertEquals("New seed file is not empty", 0L, newFile.length());

            // Since the "new" seed was empty/invalid, another seed should be requested.
            Assert.assertTrue("No seed requested", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Test the case that:
    // VariationsUtils.getSeedFile() - exists, timestamp = epoch
    // VariationsUtils.getNewSeedFile() - exists, timestamp = epoch + 1 day
    @Test
    @MediumTest
    public void testHaveBothExpiredSeeds() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            Assert.assertTrue("Old seed file already exists", oldFile.createNewFile());
            VariationsTestUtils.writeMockSeed(oldFile);
            oldFile.setLastModified(0);

            File newFile = VariationsUtils.getNewSeedFile();
            Assert.assertTrue("New seed file already exists", newFile.createNewFile());
            VariationsTestUtils.writeMockSeed(newFile);
            newFile.setLastModified(TimeUnit.DAYS.toMillis(1));

            boolean seedRequested = runTestLoaderBlocking();

            // The "new" seed should have been renamed to the "old" seed. Another empty "new" seed
            // should have been created as a destination for the request.
            Assert.assertTrue("Old seed not found", oldFile.exists());
            Assert.assertTrue("New seed not found", newFile.exists());
            Assert.assertTrue("New seed is not empty", newFile.length() == 0L);

            // Since the "new" seed was expired, another seed should be requested.
            Assert.assertTrue("No seed requested", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Test loading twice. The first load should trigger a request, but the second should not,
    // because requests should be rate-limited.
    // VariationsUtils.getSeedFile() - doesn't exist VariationsUtils.getNewSeedFile() - doesn't
    // exist
    @Test
    @MediumTest
    public void testDoubleLoad() throws Exception {
        try {
            boolean seedRequested = runTestLoaderBlocking();
            Assert.assertTrue("No seed requested", seedRequested);

            seedRequested = runTestLoaderBlocking();
            Assert.assertFalse(
                    "New seed was requested when it should not have been", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Tests that the finch-seed-expiration-age flag works.
    @Test
    @MediumTest
    @CommandLineFlags.Add(AwSwitches.FINCH_SEED_EXPIRATION_AGE + "=0")
    public void testFinchSeedExpirationAgeFlag() throws Exception {
        try {
            // Create a new seed file with a recent timestamp.
            File oldFile = VariationsUtils.getSeedFile();
            VariationsTestUtils.writeMockSeed(oldFile);
            oldFile.setLastModified(CURRENT_TIME_MILLIS);

            boolean seedRequested = runTestLoaderBlocking();

            Assert.assertTrue("Seed file should be requested", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Tests that the finch-seed-min-update-period flag overrides the seed request throttling.
    @Test
    @MediumTest
    @CommandLineFlags.Add(AwSwitches.FINCH_SEED_MIN_UPDATE_PERIOD + "=0")
    public void testFinchSeedMinUpdatePeriodFlag() throws Exception {
        try {
            // Update the last modified time of the stamp file to simulate having just requested a
            // new seed from the service.
            VariationsUtils.getStampFile().createNewFile();
            VariationsUtils.updateStampTime(CURRENT_TIME_MILLIS);

            boolean seedRequested = runTestLoaderBlocking();

            Assert.assertTrue("Seed file should be requested", seedRequested);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    // Tests that metrics passed from the service get recorded to histograms.
    @Test
    @MediumTest
    public void testRecordMetricsFromService() throws Exception {
        try {
            long nineMinutesMs = TimeUnit.MINUTES.toMillis(9);
            long twoWeeksMs = TimeUnit.DAYS.toMillis(14);
            long threeWeeksMs = TimeUnit.DAYS.toMillis(21);
            HistogramWatcher histogramExpectationInterval =
                    HistogramWatcher.newBuilder()
                            .expectIntRecordTimes(
                                    VariationsSeedLoader.DOWNLOAD_JOB_INTERVAL_HISTOGRAM_NAME,
                                    (int) TimeUnit.MILLISECONDS.toMinutes(threeWeeksMs),
                                    1)
                            .build();
            HistogramWatcher histogramExpectationQueueTime =
                    HistogramWatcher.newBuilder()
                            .expectIntRecordTimes(
                                    VariationsSeedLoader.DOWNLOAD_JOB_QUEUE_TIME_HISTOGRAM_NAME,
                                    (int) TimeUnit.MILLISECONDS.toMinutes(twoWeeksMs),
                                    1)
                            .build();

            VariationsServiceMetricsHelper metrics =
                    VariationsServiceMetricsHelper.fromBundle(new Bundle());
            metrics.setJobInterval(threeWeeksMs);
            metrics.setJobQueueTime(twoWeeksMs);
            MockVariationsSeedServer.setMetricsBundle(metrics.toBundle());

            runTestLoaderBlocking();
            histogramExpectationInterval.assertExpected();
            histogramExpectationQueueTime.assertExpected();
        } finally {
            MockVariationsSeedServer.setMetricsBundle(null);
        }
    }
}