chromium/content/public/android/javatests/src/org/chromium/content/browser/scheduler/NativePostTaskTest.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.content.browser.scheduler;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;

import androidx.test.filters.MediumTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.PowerMonitor;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.AsyncTask.Status;
import org.chromium.base.task.BackgroundOnlyAsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskRunner;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.task.SchedulerTestHelpers;
import org.chromium.base.test.task.ThreadPoolTestHelpers;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.RequiresRestart;
import org.chromium.content.app.ContentMain;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Test class for {@link PostTask}.
 *
 * <p>Due to layering concerns we can't test native backed task posting in base, so we do it here
 * instead.
 */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
public class NativePostTaskTest {
    private static class BlockedTask extends BackgroundOnlyAsyncTask<Integer> {
        private Object mStartLock = new Object();
        private AtomicInteger mValue = new AtomicInteger(0);
        private AtomicBoolean mStarted = new AtomicBoolean(false);
        private Thread mBackgroundThread;

        @Override
        protected Integer doInBackground() {
            synchronized (mStartLock) {
                mBackgroundThread = Thread.currentThread();
                mStarted.set(true);
                mStartLock.notify();
            }
            while (mValue.get() == 0) {
                // Busy wait because interrupting waiting on a lock or sleeping will clear the
                // interrupt.
            }
            return mValue.get();
        }

        public void setValue(int value) {
            mValue.set(value);
        }

        public void blockUntilDoInBackgroundStarts() throws Exception {
            synchronized (mStartLock) {
                while (!mStarted.get()) {
                    mStartLock.wait();
                }
            }
        }

        public Thread getBackgroundThread() {
            return mBackgroundThread;
        }
    }

    private static boolean sNativeLoaded;
    private boolean mFenceCreated;

    @After
    public void tearDown() {
        if (mFenceCreated) {
            ThreadPoolTestHelpers.disableThreadPoolExecutionForTesting();
        }
    }

    @Test
    @MediumTest
    public void testNativePostTask() throws Exception {
        startNativeScheduler();

        // This should not timeout.
        final Object lock = new Object();
        final AtomicBoolean taskExecuted = new AtomicBoolean();
        PostTask.postTask(
                TaskTraits.USER_BLOCKING,
                new Runnable() {
                    @Override
                    public void run() {
                        synchronized (lock) {
                            taskExecuted.set(true);
                            lock.notify();
                        }
                    }
                });

        synchronized (lock) {
            while (!taskExecuted.get()) {
                lock.wait();
            }
        }
    }

    @Test
    @MediumTest
    @RequiresRestart
    public void testNativePostDelayedTask() throws Exception {
        final Object lock = new Object();
        final AtomicBoolean taskExecuted = new AtomicBoolean();
        PostTask.postDelayedTask(
                TaskTraits.USER_BLOCKING,
                () -> {
                    synchronized (lock) {
                        taskExecuted.set(true);
                        lock.notify();
                    }
                },
                1);

        // We verify that the task didn't get scheduled before the native scheduler is initialised
        Assert.assertFalse(taskExecuted.get());
        startNativeScheduler();

        // The task should now be scheduled at some point after the delay, so the test shouldn't
        // time out.
        synchronized (lock) {
            while (!taskExecuted.get()) {
                lock.wait();
            }
        }
    }

    @Test
    @MediumTest
    public void testCreateTaskRunner() {
        startNativeScheduler();
        TaskRunner taskQueue = PostTask.createTaskRunner(TaskTraits.USER_BLOCKING);
        // This should not time out.
        SchedulerTestHelpers.postDelayedTaskAndBlockUntilRun(taskQueue, 1);
    }

    private void testRunningTasksInSequence(TaskRunner taskQueue) {
        List<Integer> orderListImmediate = new ArrayList<>();
        List<Integer> orderListDelayed = new ArrayList<>();

        SchedulerTestHelpers.postThreeTasksInOrder(taskQueue, orderListImmediate);
        SchedulerTestHelpers.postTaskAndBlockUntilRun(taskQueue);

        assertThat(orderListImmediate, contains(1, 2, 3));

        SchedulerTestHelpers.postThreeDelayedTasksInOrder(taskQueue, orderListDelayed);
        SchedulerTestHelpers.postDelayedTaskAndBlockUntilRun(taskQueue, 1);

        assertThat(orderListDelayed, contains(1, 2, 3));
    }

    @Test
    @MediumTest
    public void testCreateSequencedTaskRunner() {
        startNativeScheduler();
        TaskRunner taskQueue = PostTask.createSequencedTaskRunner(TaskTraits.USER_BLOCKING);
        testRunningTasksInSequence(taskQueue);
    }

    private void performSequencedTestSchedulerMigration(
            TaskRunner taskQueue, List<Integer> orderListImmediate, List<Integer> orderListDelayed)
            throws Exception {
        SchedulerTestHelpers.postThreeTasksInOrder(taskQueue, orderListImmediate);
        SchedulerTestHelpers.postThreeDelayedTasksInOrder(taskQueue, orderListDelayed);

        postRepeatingTaskAndStartNativeSchedulerThenWaitForTaskToRun(
                taskQueue,
                new Runnable() {
                    @Override
                    public void run() {
                        orderListImmediate.add(4);
                    }
                });
        // We wait until all the delayed tasks have been scheduled.
        SchedulerTestHelpers.postDelayedTaskAndBlockUntilRun(taskQueue, 1);
    }

    @Test
    @MediumTest
    @DisabledTest(message = "https://crbug.com/938316")
    public void testCreateTaskRunnerMigrationToNative() throws Exception {
        final Object lock = new Object();
        final AtomicBoolean taskExecuted = new AtomicBoolean();
        TaskRunner taskQueue = PostTask.createTaskRunner(TaskTraits.USER_BLOCKING);

        postRepeatingTaskAndStartNativeSchedulerThenWaitForTaskToRun(
                taskQueue,
                new Runnable() {
                    @Override
                    public void run() {
                        synchronized (lock) {
                            taskExecuted.set(true);
                            lock.notify();
                        }
                    }
                });

        // The task should run at some point after the migration, so the test shouldn't
        // time out.
        synchronized (lock) {
            while (!taskExecuted.get()) {
                lock.wait();
            }
        }
    }

    @Test
    @MediumTest
    @RequiresRestart
    public void testCreateSequencedTaskRunnerMigrationToNative() throws Exception {
        List<Integer> orderListImmediate = new ArrayList<>();
        List<Integer> orderListDelayed = new ArrayList<>();
        TaskRunner taskQueue = PostTask.createSequencedTaskRunner(TaskTraits.USER_BLOCKING);
        performSequencedTestSchedulerMigration(taskQueue, orderListImmediate, orderListDelayed);

        assertThat(orderListImmediate, contains(1, 2, 3, 4));
        assertThat(orderListDelayed, contains(1, 2, 3));
    }

    private void postRepeatingTaskAndStartNativeSchedulerThenWaitForTaskToRun(
            TaskRunner taskQueue, Runnable taskToRunAfterNativeSchedulerLoaded) throws Exception {
        final Object lock = new Object();
        final AtomicBoolean taskRun = new AtomicBoolean();
        final AtomicBoolean nativeSchedulerStarted = new AtomicBoolean();

        // Post a task that reposts itself until nativeSchedulerStarted is set to true.  This tests
        // that tasks posted before the native library is loaded still run afterwards.
        taskQueue.postTask(
                new Runnable() {
                    @Override
                    public void run() {
                        if (nativeSchedulerStarted.compareAndSet(true, true)) {
                            taskToRunAfterNativeSchedulerLoaded.run();
                            synchronized (lock) {
                                taskRun.set(true);
                                lock.notify();
                            }
                        } else {
                            taskQueue.postTask(this);
                        }
                    }
                });

        startNativeScheduler();
        nativeSchedulerStarted.set(true);

        synchronized (lock) {
            while (!taskRun.get()) {
                lock.wait();
            }
        }
    }

    @Test
    @MediumTest
    public void testNativeAsyncTask() throws Exception {
        startNativeScheduler();

        BlockedTask task = new BlockedTask();

        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

        task.blockUntilDoInBackgroundStarts();
        Assert.assertEquals(Status.RUNNING, task.getStatus());

        final int value = 5;
        task.setValue(value);

        Assert.assertEquals(value, task.get().intValue());

        Assert.assertFalse(task.isCancelled());
        Assert.assertEquals(Status.FINISHED, task.getStatus());

        Assert.assertFalse(task.getBackgroundThread().isInterrupted());
    }

    @Test
    @MediumTest
    public void testNativeAsyncTaskInterruptIsCleared() throws Exception {
        startNativeScheduler();

        BlockedTask task = new BlockedTask();

        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

        task.blockUntilDoInBackgroundStarts();
        Assert.assertEquals(Status.RUNNING, task.getStatus());

        Assert.assertTrue(task.cancel(/* mayInterruptIfRunning= */ true));

        // get() will raise an exception although the task is started.
        try {
            task.get();
            Assert.fail();
        } catch (CancellationException e) {
            // expected
        }

        // Set a value to unblock the task.
        task.setValue(3);

        // Wait for the AsyncTask to finish.
        while (task.getStatus() != Status.FINISHED) {
            Thread.sleep(50);
        }

        // Sleep a bit longer for the FutureTask to finish.
        Thread.sleep(500);

        Assert.assertTrue(task.isCancelled());
        Assert.assertEquals(Status.FINISHED, task.getStatus());

        Assert.assertFalse(task.getBackgroundThread().isInterrupted());
    }

    private void startNativeScheduler() {
        if (!sNativeLoaded) {
            NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
            PowerMonitor.createForTests();
            ContentMain.start(/* startMinimalBrowser= */ false);
            sNativeLoaded = true;
        }
        mFenceCreated = true;
        ThreadPoolTestHelpers.enableThreadPoolExecutionForTesting();
    }
}