/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include #include #include "ErrorList.h" #include "gtest/gtest.h" #include "mozilla/MozPromise.h" #include "mozilla/SpinEventLoopUntil.h" #include "nsISupportsImpl.h" #include "nsThreadUtils.h" using namespace mozilla; // Simple function to be able to distinguish threads in output size_t tid() { return std::hash{}(std::this_thread::get_id()); } // Invoking something on a background thread, but getting the completion on the // main thread. TEST(MozPromiseExamples, InvokeAsync) { bool done = false; InvokeAsync( GetCurrentSerialEventTarget(), __func__, []() { printf("[%zu] Doing some work on a background thread...\n", tid()); std::this_thread::sleep_for(std::chrono::milliseconds(100)); printf("[%zu] Done...\n", tid()); // Simulate various outcomes: srand(getpid()); switch (rand() % 4) { case 0: return GenericPromise::CreateAndResolve(true, __func__); case 1: return GenericPromise::CreateAndResolve(false, __func__); case 2: return GenericPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__); default: return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } }) ->Then( GetMainThreadSerialEventTarget(), __func__, [&done](GenericPromise::ResolveOrRejectValue&& aResult) { if (aResult.IsReject()) { printf("[%zu] Back on the main thread, the task failed: 0x%x\n", tid(), (unsigned int)aResult.RejectValue()); done = true; return; } printf("[%zu] back on the main thread, sucess, return value: %s\n", tid(), aResult.ResolveValue() ? "true" : "false"); done = true; }); // Process all events and check that `done` was effectively set to true. This // is just for the purpose of this test. MOZ_ALWAYS_TRUE(SpinEventLoopUntil( "xpcom:TEST(MozPromiseExamples, OneOff)"_ns, [&done]() { return done; })); EXPECT_TRUE(done); } class Something final { public: explicit Something(uint32_t aMilliseconds = 100) : mMilliseconds(aMilliseconds) {} RefPtr DoIt() { // Do no dispatch the async task twice if still underway. if (mPromise) { return mPromise; } mPromise = mHolder.Ensure(__func__); // Kick off some work to another thread... std::thread([self = RefPtr{this}, this] { printf("[%zu] Working...\n", tid()); std::this_thread::sleep_for(std::chrono::milliseconds(mMilliseconds)); printf("[%zu] Resolving from background thread\n", tid()); self->mHolder.Resolve(true, __func__); }).detach(); return mPromise; } private: ~Something() = default; const uint32_t mMilliseconds; NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Something) RefPtr mPromise; MozPromiseHolder mHolder{}; }; // Waiting for something asynchronous to complete, from outside the instance TEST(MozPromiseExamples, OneOff) { RefPtr thing(new Something); bool done = false; thing->DoIt()->Then( GetCurrentSerialEventTarget(), __func__, [&done, thing](bool aResult) { printf("[%zu] Success: %s\n", tid(), aResult ? "true" : "false"); done = true; }, [&done](nsresult aError) { printf("[%zu] Failure: 0x%x\n", tid(), (unsigned)aError); done = true; }); // Process all events and check that `done` was effectively set to true. This // is just for the purpose of this test. MOZ_ALWAYS_TRUE(SpinEventLoopUntil( "xpcom:TEST(MozPromiseExamples, OneOff)"_ns, [&done]() { return done; })); } class SomethingCancelable final { public: RefPtr DoIt() { if (mPromise) { return mPromise; } mPromise = mHolder.Ensure(__func__); // Kick off some work to another thread... std::thread([self = RefPtr{this}] { printf("[%zu] Working...\n", tid()); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // This is printed: despite being canceled, the thread runs normally and // resolves its promise. printf("[%zu] Resolving from background thread\n", tid()); self->mHolder.Resolve(true, __func__); }).detach(); return mPromise; } private: ~SomethingCancelable() = default; NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SomethingCancelable) MozPromiseHolder mHolder{}; RefPtr mPromise; MozPromiseRequestHolder mRequest; }; // Kick of an asynchronous job, and cancel it TEST(MozPromiseExamples, OneOffCancelable) { RefPtr thing(new SomethingCancelable); // Start a job that takes 100ms MozPromiseRequestHolder holder; thing->DoIt() ->Then(GetCurrentSerialEventTarget(), __func__, [&holder] { holder.Complete(); // This is never printed: in this example we disconnect the // request before completion. printf("[%zu] Async work finished", tid()); }) ->Track(holder); // But cancel it after just 10ms std::this_thread::sleep_for(std::chrono::milliseconds(10)); holder.Disconnect(); } // Waiting for multiple asynchronous tasks to complete, from outside the // instance TEST(MozPromiseExamples, MultipleWaits) { nsTArray> things; uint32_t count = 10; while (count--) { things.AppendElement(new Something(count * 10)); } bool done = false; nsTArray> promises; for (auto& thing : things) { promises.AppendElement(thing->DoIt()); } GenericPromise::All(GetCurrentSerialEventTarget(), promises) ->Then( GetCurrentSerialEventTarget(), __func__, [&done](nsTArray&& aResults) { nsCString results; for (auto b : aResults) { results.AppendPrintf("%s, ", b ? "true" : "false"); } printf("[%zu] All succeeded: %s\n", tid(), results.get()); done = true; }, [&done](nsresult aError) { printf("[%zu] One failed: 0x%x\n", tid(), (unsigned)aError); done = true; }); // Process all events and check that `done` was effectively set to true. This // is just for the purpose of this test. MOZ_ALWAYS_TRUE(SpinEventLoopUntil( "xpcom:TEST(MozPromiseExamples, OneOff)"_ns, [&done]() { return done; })); } RefPtr SyncOperation(uint32_t aConstraint) { printf("[%zu] SyncOperation(%" PRIu32 ")\n", tid(), aConstraint); if (aConstraint > 5) { return GenericPromise::CreateAndReject(NS_ERROR_UNEXPECTED, __func__); } return GenericPromise::CreateAndResolve(true, __func__); } // This test uses various MozPromise facilities and prints a message to the // console, to show how the scheduling works. TEST(MozPromiseExamples, SyncReturn) { bool done = false; // Dispatch a runnable to the current even loop, for the sole purpose of // understanding ordering. NS_DispatchToCurrentThread(NS_NewRunnableFunction("Initial runnable", [] { printf("[%zu] Dispatched before sync promise operation\n", tid()); })); // SyncOperation synchronously returns a resolved promise. However, `Then` // works by dispatching so the printf will happen after InitialRunnable. SyncOperation(3)->Then( GetCurrentSerialEventTarget(), __func__, [](bool aResult) { printf("[%zu] Sync promise value: %s\n", tid(), aResult ? "true" : "false"); }, [](nsresult aError) { printf("[%zu] Error: 0x%x\n", tid(), (unsigned)aError); }); // Now call the same method, but invoke it async on the current event queue. // The resolve will also be in its own event loop task. It follows that this // will be printed after the "Final Runnable" below. // MozPromise can be put in tail dispatch mode,or sync mode, and in those // cases, the ordering will be different. InvokeAsync(GetCurrentSerialEventTarget(), __func__, []() { return SyncOperation(4); }) ->Then( GetCurrentSerialEventTarget(), __func__, [&done](bool aResult) { printf("[%zu] Sync promise value (InvokeAsync): %s\n", tid(), aResult ? "true" : "false"); done = true; }, [](nsresult aError) { printf("[%zu] Error (InvokeAsync): 0x%x\n", tid(), (unsigned)aError); }); // Dispatch a runnable to the current even loop, for the sole purpose of // understanding ordering. NS_DispatchToCurrentThread(NS_NewRunnableFunction("Final runnable", [] { printf("[%zu] Dispatched after sync promise operation\n", tid()); })); // The output will be as such (omitting the thread ids): // [...] SyncOperation(3) // [...] Dispatched before sync promise operation // [...] Sync promise value: true // [...] SyncOperation(4) // [...] Dispatched after sync promise operation // [...] Sync promise value (InvokeAsync): true // Process all events and check that `done` was effectively set to true. This // is just for the purpose of this test. MOZ_ALWAYS_TRUE(SpinEventLoopUntil( "xpcom:TEST(MozPromiseExamples, OneOff)"_ns, [&done]() { return done; })); } using IntPromise = MozPromise; using UintPromise = MozPromise; class SomethingSync { public: RefPtr DoSomethingSync() { return GenericPromise::CreateAndResolve(true, "Returning true"); } }; TEST(MozPromiseExamples, Chaining) { bool done = false; SomethingSync s; // Do something that returns a bool, then chain it to a promise that returns // an int, then to a promise that returns an unsigned. s.DoSomethingSync() ->Then(GetCurrentSerialEventTarget(), __func__, [](GenericPromise::ResolveOrRejectValue&& aValue) { if (aValue.IsResolve()) { // Depending on the value of the bool, find the proper signed // integer value. return IntPromise::CreateAndResolve( aValue.ResolveValue() ? 3 : 5, "Example IntPromise Resolver"); } return IntPromise::CreateAndReject( aValue.RejectValue(), "Example IntPromise Rejecter"); }) ->Then(GetCurrentSerialEventTarget(), __func__, [&done](IntPromise::ResolveOrRejectValue&& aValue) { if (aValue.IsResolve()) { // Depending on the value of the signed integer, find the // proper unsigned integer value. done = true; return UintPromise::CreateAndResolve( static_cast(aValue.ResolveValue()), "Example UintPromise Resolver"); } return UintPromise::CreateAndReject( aValue.RejectValue(), "Example UintPromise Rejecter"); }); // Process all events and check that `done` was effectively set to true. This // is just for the purpose of this test. MOZ_ALWAYS_TRUE( SpinEventLoopUntil("xpcom:TEST(MozPromiseExamples, Chaining)"_ns, [&done]() { return done; })); } // - converting an async legacy callback interface to a modern MozPromise // version with MozPromiseHolder.