| /* |
| * Copyright (C) 2018 Igalia, S.L. |
| * Copyright (C) 2018 Metrological Group B.V. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| |
| #include "Utilities.h" |
| #include <WebCore/AbortableTaskQueue.h> |
| #include <wtf/Threading.h> |
| |
| using namespace WebCore; |
| |
| namespace TestWebKitAPI { |
| |
| TEST(AbortableTaskQueue, AsyncTasks) |
| { |
| AbortableTaskQueue taskQueue; |
| bool testFinished { false }; |
| int currentStep { 0 }; |
| WTF::initializeMainThread(); |
| |
| auto backgroundThreadFunction = [&]() { |
| EXPECT_FALSE(isMainThread()); |
| taskQueue.enqueueTask([&]() { |
| EXPECT_TRUE(isMainThread()); |
| currentStep++; |
| EXPECT_EQ(1, currentStep); |
| }); |
| taskQueue.enqueueTask([&]() { |
| EXPECT_TRUE(isMainThread()); |
| currentStep++; |
| EXPECT_EQ(2, currentStep); |
| testFinished = true; |
| }); |
| }; |
| RunLoop::current().dispatch([backgroundThreadFunction = WTFMove(backgroundThreadFunction)]() mutable { |
| WTF::Thread::create("atq-background", WTFMove(backgroundThreadFunction))->detach(); |
| }); |
| |
| Util::run(&testFinished); |
| } |
| |
| struct FancyResponse { |
| WTF_MAKE_NONCOPYABLE(FancyResponse); |
| public: |
| FancyResponse(int fancyInt, bool* destructedFlagPointer = nullptr) |
| : fancyInt(fancyInt) |
| , destructedFlagPointer(destructedFlagPointer) |
| { } |
| |
| FancyResponse(FancyResponse&& original) |
| : fancyInt(original.fancyInt) |
| , destructedFlagPointer(original.destructedFlagPointer) |
| { |
| original.destructedFlagPointer = nullptr; |
| } |
| |
| ~FancyResponse() |
| { |
| if (destructedFlagPointer) { |
| RELEASE_ASSERT(!*destructedFlagPointer); |
| *destructedFlagPointer = true; |
| } |
| } |
| |
| FancyResponse& operator=(FancyResponse&& a) |
| { |
| this->fancyInt = a.fancyInt; |
| return *this; |
| } |
| |
| int fancyInt; |
| bool* destructedFlagPointer; |
| }; |
| |
| TEST(AbortableTaskQueue, SyncTasks) |
| { |
| AbortableTaskQueue taskQueue; |
| bool testFinished { false }; |
| bool destructedResponseFlag { false }; |
| int currentStep { 0 }; |
| WTF::initializeMainThread(); |
| |
| auto backgroundThreadFunction = [&]() { |
| EXPECT_FALSE(isMainThread()); |
| std::optional<FancyResponse> response = taskQueue.enqueueTaskAndWait<FancyResponse>([&]() -> FancyResponse { |
| EXPECT_TRUE(isMainThread()); |
| currentStep++; |
| EXPECT_EQ(1, currentStep); |
| FancyResponse returnValue(100, &destructedResponseFlag); |
| return returnValue; |
| }); |
| currentStep++; |
| EXPECT_EQ(2, currentStep); |
| EXPECT_TRUE(response); |
| EXPECT_FALSE(destructedResponseFlag); |
| EXPECT_EQ(100, response->fancyInt); |
| response = std::nullopt; |
| EXPECT_TRUE(destructedResponseFlag); |
| RunLoop::main().dispatch([&]() { |
| testFinished = true; |
| }); |
| }; |
| RunLoop::current().dispatch([backgroundThreadFunction = WTFMove(backgroundThreadFunction)]() mutable { |
| WTF::Thread::create("atq-background", WTFMove(backgroundThreadFunction))->detach(); |
| }); |
| |
| Util::run(&testFinished); |
| } |
| |
| template <typename ThreadEnum> |
| class DeterministicScheduler { |
| WTF_MAKE_NONCOPYABLE(DeterministicScheduler); |
| public: |
| DeterministicScheduler(ThreadEnum firstThread) |
| : m_currentThread(firstThread) |
| { } |
| |
| class ThreadContext { |
| WTF_MAKE_NONCOPYABLE(ThreadContext); |
| public: |
| ThreadContext(DeterministicScheduler& scheduler, ThreadEnum thisThread) |
| : m_scheduler(scheduler), m_thisThread(thisThread) |
| { } |
| |
| void waitMyTurn() |
| { |
| Locker locker { m_scheduler.m_mutex }; |
| m_scheduler.m_currentThreadChanged.wait(m_scheduler.m_mutex, [this]() { |
| return m_scheduler.m_currentThread == m_thisThread; |
| }); |
| } |
| |
| void yieldToThread(ThreadEnum nextThread) |
| { |
| Locker locker { m_scheduler.m_mutex }; |
| m_scheduler.m_currentThread = nextThread; |
| m_scheduler.m_currentThreadChanged.notifyAll(); |
| } |
| |
| private: |
| DeterministicScheduler& m_scheduler; |
| ThreadEnum m_thisThread; |
| }; |
| |
| |
| private: |
| ThreadEnum m_currentThread; |
| Condition m_currentThreadChanged; |
| Lock m_mutex; |
| }; |
| |
| enum class TestThread { |
| Main, |
| Background |
| }; |
| |
| TEST(AbortableTaskQueue, Abort) |
| { |
| DeterministicScheduler<TestThread> scheduler(TestThread::Background); |
| |
| AbortableTaskQueue taskQueue; |
| bool testFinished { false }; |
| WTF::initializeMainThread(); |
| |
| auto backgroundThreadFunction = [&]() { |
| EXPECT_FALSE(isMainThread()); |
| DeterministicScheduler<TestThread>::ThreadContext backgroundThreadContext(scheduler, TestThread::Background); |
| |
| taskQueue.enqueueTask([]() { |
| // This task should not have been able to run under the scheduling of this test. |
| EXPECT_TRUE(false); |
| }); |
| backgroundThreadContext.yieldToThread(TestThread::Main); |
| backgroundThreadContext.waitMyTurn(); |
| |
| // Main thread has called startAborting(). |
| |
| taskQueue.enqueueTask([]() { |
| // This task should not have been able to run under the scheduling of this test. |
| EXPECT_TRUE(false); |
| }); |
| // This call must return immediately because we are aborting. |
| std::optional<FancyResponse> response = taskQueue.enqueueTaskAndWait<FancyResponse>([]() -> FancyResponse { |
| // This task should not have been able to run under the scheduling of this test. |
| EXPECT_TRUE(false); |
| return FancyResponse(100); |
| }); |
| EXPECT_FALSE(response); |
| backgroundThreadContext.yieldToThread(TestThread::Main); |
| backgroundThreadContext.waitMyTurn(); |
| |
| // Main thread has called finishAborting(). |
| |
| taskQueue.enqueueTask([&]() { |
| testFinished = true; |
| }); |
| }; |
| RunLoop::current().dispatch([&, backgroundThreadFunction = WTFMove(backgroundThreadFunction)]() mutable { |
| EXPECT_TRUE(isMainThread()); |
| DeterministicScheduler<TestThread>::ThreadContext mainThreadContext(scheduler, TestThread::Main); |
| WTF::Thread::create("atq-background", WTFMove(backgroundThreadFunction))->detach(); |
| |
| mainThreadContext.waitMyTurn(); |
| |
| taskQueue.startAborting(); |
| mainThreadContext.yieldToThread(TestThread::Background); |
| mainThreadContext.waitMyTurn(); |
| |
| taskQueue.finishAborting(); |
| mainThreadContext.yieldToThread(TestThread::Background); |
| }); |
| |
| Util::run(&testFinished); |
| } |
| |
| TEST(AbortableTaskQueue, AbortBeforeSyncTaskRun) |
| { |
| AbortableTaskQueue taskQueue; |
| bool testFinished { false }; |
| WTF::initializeMainThread(); |
| |
| auto backgroundThreadFunction = [&]() { |
| EXPECT_FALSE(isMainThread()); |
| |
| std::optional<FancyResponse> response = taskQueue.enqueueTaskAndWait<FancyResponse>([]() -> FancyResponse { |
| // This task should not have been able to run under the scheduling of this test. |
| EXPECT_TRUE(false); |
| return FancyResponse(100); |
| }); |
| |
| // Main thread has called startAborting(). |
| EXPECT_FALSE(response); |
| |
| RunLoop::main().dispatch([&]() { |
| testFinished = true; |
| }); |
| }; |
| RunLoop::current().dispatch([&, backgroundThreadFunction = WTFMove(backgroundThreadFunction)]() mutable { |
| EXPECT_TRUE(isMainThread()); |
| WTF::Thread::create("atq-background", WTFMove(backgroundThreadFunction))->detach(); |
| |
| // Give the background thread a bit of time to get blocked waiting for a response. |
| WTF::sleep(100_ms); |
| |
| taskQueue.startAborting(); |
| }); |
| |
| Util::run(&testFinished); |
| } |
| |
| TEST(AbortableTaskQueue, AbortedBySyncTaskHandler) |
| { |
| AbortableTaskQueue taskQueue; |
| bool testFinished { false }; |
| int currentStep { 0 }; |
| bool destructedResponseFlag { false }; |
| WTF::initializeMainThread(); |
| |
| auto backgroundThreadFunction = [&]() { |
| EXPECT_FALSE(isMainThread()); |
| currentStep++; |
| EXPECT_EQ(1, currentStep); |
| |
| std::optional<FancyResponse> response = taskQueue.enqueueTaskAndWait<FancyResponse>([&]() -> FancyResponse { |
| currentStep++; |
| EXPECT_EQ(2, currentStep); |
| taskQueue.startAborting(); |
| // This task should not have been able to run under the scheduling of this test. |
| return FancyResponse(100, &destructedResponseFlag); |
| }); |
| |
| currentStep++; |
| EXPECT_EQ(3, currentStep); |
| |
| // Main thread has called startAborting(). |
| EXPECT_FALSE(response); |
| |
| // The response object has not been leaked. |
| EXPECT_TRUE(destructedResponseFlag); |
| |
| RunLoop::main().dispatch([&]() { |
| testFinished = true; |
| }); |
| }; |
| RunLoop::current().dispatch([&, backgroundThreadFunction = WTFMove(backgroundThreadFunction)]() mutable { |
| EXPECT_TRUE(isMainThread()); |
| WTF::Thread::create("atq-background", WTFMove(backgroundThreadFunction))->detach(); |
| }); |
| |
| Util::run(&testFinished); |
| } |
| |
| } // namespace TestWebKitAPI |