[fit][scope] Add a mechanism for scoping promises.

fit::scope is a mechanism for wrapping promises such that they will
automatically be abandoned if the scope is exited before they complete.
Can be very useful when handing out promises that capture references
to "this" or to local variables.

Test: fit-test
Change-Id: I31a37d67e745156d3ee255fa1ed1060379d32bab
diff --git a/system/ulib/fit/include/lib/fit/scope.h b/system/ulib/fit/include/lib/fit/scope.h
new file mode 100644
index 0000000..6dc0bfc
--- /dev/null
+++ b/system/ulib/fit/include/lib/fit/scope.h
@@ -0,0 +1,279 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_SCOPE_H_
+#define LIB_FIT_SCOPE_H_
+
+#include <assert.h>
+
+#include <atomic>
+#include <mutex>
+
+#include "promise.h"
+#include "thread_safety.h"
+
+namespace fit {
+
+// Provides a mechanism for binding promises to the lifetime of another object
+// such that they are destroyed before that object goes out of scope.  It is
+// particularly useful for ensuring that the lifetime of a promise does not
+// exceed the lifetime of any variables that it has captured by reference.
+//
+// A scope is thread-safe but non-reentrant: it must not be destroyed while
+// any of its associated promises are running.
+//
+// EXAMPLE
+//
+// Define a |fit::scope| as a member of the object to whose lifetime the
+// promises should be bound.
+//
+//     // We mark this class final because its destructor has side-effects
+//     // that rely on the order of destruction.  If this object were
+//     // subclassed there would be a possibility for promises bound to its
+//     // scope to inadvertently access the subclass's state while the object
+//     // was being destroyed.
+//     class accumulator final {
+//     public:
+//         accumulator() = default;
+//         ~accumulator() = default;
+//
+//         fit::promise<int> accumulate(int value);
+//
+//     private:
+//         int prior_total_ = 0;
+//
+//         // This member is last so that the scope is exited before all
+//         // other members of the object are destroyed.  Alternately, we
+//         // could enforce this ordering by explicitly invoking
+//         // |fit::scope::exit()| where appropriate.
+//         fit::scope scope_;
+//     };
+//
+// Use |fit::promise::wrap_with()| to wrap up promises that capture pointers
+// to the object.  In this example, the captured pointer is "this".
+//
+//     fit::promise<int> accumulator::accumulate(int value) {
+//         return fit::make_promise([this, value] {
+//             prior_total_ += value;
+//             return fit::ok(prior_total_);
+//         }).wrap_with(scope_); /* binding to scope happens here */
+//     }
+//
+class scope final {
+public:
+    // Creates a new scope.
+    scope();
+
+    // Exits the scope and destroys all of its wrapped promises.
+    // Asserts that no promises are currently running.
+    ~scope();
+
+    // Returns true if the scope has been exited.
+    //
+    // This method is thread-safe.
+    bool exited() const { return state_->exited(); }
+
+    // Exits the scope and destroys all of its wrapped promises.
+    // Assets that no promises are currently running.
+    //
+    // This method is thread-safe.
+    void exit() { return state_->exit(false /*scope_was_destroyed*/); }
+
+    // Returns a promise which wraps the specified |promise| and binds the
+    // promise to this scope.
+    //
+    // The specified promise will automatically be destroyed when its wrapper
+    // is destroyed or when the scope is exited.  If the scope has already
+    // exited then the wrapped promise will be immediately destroyed.
+    //
+    // When the returned promise is invoked before the scope is exited,
+    // the promise that it wraps will be invoked as usual.  However, when
+    // the returned promise is invoked after the scope is exited, it
+    // immediately returns a pending result (since the promise that it
+    // previously wrapped has already been destroyed).  By returning a
+    // pending result, the return promise effectively indicates to the
+    // executor that the task has been "abandoned" due to the scope being
+    // exited.
+    //
+    // This method is thread-safe.
+    template <typename Promise>
+    decltype(auto) wrap(Promise promise) {
+        assert(promise);
+        return fit::make_promise_with_continuation(
+            scoped_continuation<Promise>(
+                state_->link_promise(
+                    new promise_holder<Promise>(std::move(promise)))));
+    }
+
+    scope(const scope&) = delete;
+    scope(scope&&) = delete;
+    scope& operator=(const scope&) = delete;
+    scope& operator=(scope&&) = delete;
+
+private:
+    class state;
+    class promise_holder_base;
+
+    // Holds a reference to a promise that is owned by the state.
+    class promise_handle final {
+    public:
+        promise_handle() = default;
+
+    private:
+        // |state| and |promise_holder| belong to the state object.
+        // Invariant: If |promise_holder| is non-null then |state| is
+        // also non-null.
+        friend state;
+        promise_handle(state* state, promise_holder_base* promise_holder)
+            : state(state), promise_holder(promise_holder) {}
+
+        state* state = nullptr;
+        promise_holder_base* promise_holder = nullptr;
+    };
+
+    // Holds the shared state of the scope.
+    // This object is destroyed once the scope and all of its promises
+    // have been destroyed.
+    class state final {
+    public:
+        state();
+        ~state();
+
+        // The following methods are called from the |scope|.
+
+        bool exited() const;
+        void exit(bool scope_was_destroyed);
+
+        // The following methods are called from the |scoped_continuation|.
+
+        // Links a promise to the scope's lifecycle such that it will be
+        // destroyed when the scope is exited.  Returns a handle that may
+        // be used to access the promise later.
+        // The state takes ownership of the promise.
+        promise_handle link_promise(promise_holder_base* promise_holder);
+
+        // Unlinks a promise from the scope's lifecycle given its handle
+        // and causes the underlying promise to be destroyed if it hasn't
+        // already been destroyed due to the scope exiting.
+        // Does nothing if the handle was default-initialized.
+        static void unlink_and_drop_promise(promise_handle promise_handle);
+
+        // Acquires a promise given its handle.
+        // Returns nullptr if the handle was default-initialized or if
+        // the scope exited, meaning that the promise was not acquired.
+        // The promise must be released before it can be acquired again.
+        static promise_holder_base* try_acquire_promise(
+            promise_handle promise_handle);
+
+        // Releases a promise that was successfully acquired.
+        static void release_promise(promise_handle promise_handle);
+
+        state(const state&) = delete;
+        state(state&&) = delete;
+        state& operator=(const state&) = delete;
+        state& operator=(state&&) = delete;
+
+    private:
+        bool should_delete_self() const FIT_GUARDED(mutex_) {
+            return scope_was_destroyed_ && promise_handle_count_ == 0;
+        }
+
+        static constexpr uint64_t scope_exited = static_cast<uint64_t>(1u) << 63;
+
+        // Tracks of the number of promises currently running ("acquired").
+        // The top bit is set when the scope is exited, at which point no
+        // new promises can be acquired.  After exiting, the count can
+        // be incremented transiently but is immediately decremented again
+        // until all promise handles have been released.  Once no promise
+        // handles remain, the count will equal |scope_exited| and will not
+        // change again.
+        std::atomic_uint64_t acquired_promise_count_{0};
+
+        mutable std::mutex mutex_;
+        bool scope_was_destroyed_ FIT_GUARDED(mutex_) = false;
+        uint64_t promise_handle_count_ FIT_GUARDED(mutex_) = 0;
+        promise_holder_base* head_promise_holder_ FIT_GUARDED(mutex_) = nullptr;
+    };
+
+    // Base type for managing the lifetime of a promise of any type.
+    // It is owned by the state and retained indirectly by the continuation
+    // using a |promise_handle|.
+    class promise_holder_base {
+    public:
+        promise_holder_base() = default;
+        virtual ~promise_holder_base() = default;
+
+        promise_holder_base(const promise_holder_base&) = delete;
+        promise_holder_base(promise_holder_base&&) = delete;
+        promise_holder_base& operator=(const promise_holder_base&) = delete;
+        promise_holder_base& operator=(promise_holder_base&&) = delete;
+
+    private:
+        // |next| and |prev| belong to the state object.
+        friend class state;
+        promise_holder_base* next = nullptr;
+        promise_holder_base* prev = nullptr;
+    };
+
+    // Holder for a promise of a particular type.
+    template <typename Promise>
+    class promise_holder final : public promise_holder_base {
+    public:
+        explicit promise_holder(Promise promise)
+            : promise(std::move(promise)) {}
+        ~promise_holder() override = default;
+
+        Promise promise;
+    };
+
+    // Wraps a promise whose lifetime is managed by the scope.
+    template <typename Promise>
+    class scoped_continuation final {
+    public:
+        explicit scoped_continuation(promise_handle promise_handle)
+            : promise_handle_(promise_handle) {}
+
+        scoped_continuation(scoped_continuation&& other)
+            : promise_handle_(other.promise_handle_) {
+            other.promise_handle_ = promise_handle{};
+        }
+
+        ~scoped_continuation() {
+            state::unlink_and_drop_promise(promise_handle_);
+        }
+
+        typename Promise::result_type operator()(context& context) {
+            typename Promise::result_type result;
+            auto holder = static_cast<promise_holder<Promise>*>(
+                state::try_acquire_promise(promise_handle_));
+            if (holder) {
+                result = holder->promise(context);
+                state::release_promise(promise_handle_);
+            }
+            return result;
+        }
+
+        scoped_continuation& operator=(scoped_continuation&& other) {
+            if (this != &other) {
+                state::unlink_and_drop_promise(promise_handle_);
+                promise_handle_ = other.promise_handle_;
+                other.promise_handle_ = promise_handle{};
+            }
+            return *this;
+        }
+
+        scoped_continuation(const scoped_continuation&) = delete;
+        scoped_continuation& operator=(const scoped_continuation&) = delete;
+
+    private:
+        promise_handle promise_handle_;
+    };
+
+    // The scope's shared state.
+    state* const state_;
+};
+
+} // namespace fit
+
+#endif // LIB_FIT_SCOPE_H_
diff --git a/system/ulib/fit/rules.mk b/system/ulib/fit/rules.mk
index 80e0b82..692bbb7 100644
--- a/system/ulib/fit/rules.mk
+++ b/system/ulib/fit/rules.mk
@@ -7,6 +7,7 @@
 fit_srcs := \
     $(LOCAL_DIR)/promise.cpp \
     $(LOCAL_DIR)/scheduler.cpp \
+    $(LOCAL_DIR)/scope.cpp \
     $(LOCAL_DIR)/sequencer.cpp \
     $(LOCAL_DIR)/single_threaded_executor.cpp \
 
diff --git a/system/ulib/fit/scope.cpp b/system/ulib/fit/scope.cpp
new file mode 100644
index 0000000..c1cbea7
--- /dev/null
+++ b/system/ulib/fit/scope.cpp
@@ -0,0 +1,159 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Can't compile this for Zircon userspace yet since libstdc++ isn't available.
+#ifndef FIT_NO_STD_FOR_ZIRCON_USERSPACE
+
+#include <lib/fit/scope.h>
+
+namespace fit {
+
+scope::scope()
+    : state_(new state()) {}
+
+scope::~scope() {
+    state_->exit(true /*scope_was_destroyed*/);
+}
+
+scope::state::state() = default;
+
+scope::state::~state() {
+    assert(acquired_promise_count_.load(std::memory_order_relaxed) ==
+           scope_exited);
+    assert(scope_was_destroyed_);
+    assert(promise_handle_count_ == 0);
+    assert(head_promise_holder_ == nullptr);
+}
+
+bool scope::state::exited() const {
+    return acquired_promise_count_.load(std::memory_order_relaxed) & scope_exited;
+}
+
+void scope::state::exit(bool scope_was_destroyed) {
+    promise_holder_base* release_head = nullptr;
+    bool delete_self = false;
+    {
+        std::lock_guard<std::mutex> lock(mutex_);
+        assert(!scope_was_destroyed_);
+        scope_was_destroyed_ = scope_was_destroyed;
+
+        // Atomically exit the scope.  We cannot do this safely if there are
+        // any running promises since they might still be accessing state which
+        // is guarded by the scope.  Worse, if a promise re-entrantly destroys
+        // the scope during its execution then as a side-effect the promise
+        // itself will be destroyed.  So assert!
+        uint64_t prior_count = acquired_promise_count_.exchange(
+            scope_exited, std::memory_order_relaxed);
+        if (!(prior_count & scope_exited)) {
+            // Cannot exit fit::scope while any of its promises are running!
+            assert(prior_count == 0);
+
+            // Take the promises so they can be deleted outside of the lock.
+            release_head = head_promise_holder_;
+            head_promise_holder_ = nullptr;
+        }
+
+        // If there are no more handles then we can delete the state now.
+        delete_self = should_delete_self();
+    }
+
+    // Delete aborted promises and self outside of the lock.
+    while (release_head) {
+        promise_holder_base* release_next = release_head->next;
+        delete release_head;
+        release_head = release_next;
+    }
+    if (delete_self) {
+        delete this;
+    }
+}
+
+scope::promise_handle scope::state::link_promise(
+    promise_holder_base* promise_holder) {
+    {
+        std::lock_guard<std::mutex> lock(mutex_);
+        assert(!scope_was_destroyed_); // otherwise how did we get here?
+
+        // If the scope hasn't been exited yet, link the promise and mint
+        // a new handle.  Otherwise we will abort the promise.
+        if (!exited()) {
+            if (head_promise_holder_) {
+                head_promise_holder_->prev = promise_holder;
+                promise_holder->next = head_promise_holder_;
+            }
+            head_promise_holder_ = promise_holder;
+            promise_handle_count_++;
+            return promise_handle(this, promise_holder);
+        }
+    }
+
+    // Delete aborted promise outside of the lock.
+    delete promise_holder;
+    return promise_handle{};
+}
+
+void scope::state::unlink_and_drop_promise(promise_handle promise_handle) {
+    if (!promise_handle.promise_holder) {
+        return; // invalid handle, nothing to do
+    }
+
+    {
+        std::lock_guard<std::mutex> lock(promise_handle.state->mutex_);
+
+        // If the scope hasn't been exited yet, unlink the promise and
+        // prepare to destroy it.  Otherwise, it's already been unlinked
+        // and destroyed so release the handle but don't touch the pointer!
+        assert(promise_handle.state->promise_handle_count_ > 0);
+        promise_handle.state->promise_handle_count_--;
+        if (!promise_handle.state->exited()) {
+            if (promise_handle.promise_holder->next) {
+                promise_handle.promise_holder->next->prev =
+                    promise_handle.promise_holder->prev;
+            }
+            if (promise_handle.promise_holder->prev) {
+                promise_handle.promise_holder->prev->next =
+                    promise_handle.promise_holder->next;
+            } else {
+                promise_handle.state->head_promise_holder_ =
+                    promise_handle.promise_holder->next;
+            }
+            // Fallthrough to delete the promise.
+        } else if (!promise_handle.state->should_delete_self()) {
+            return;
+        } else {
+            // Fallthrough to delete self.
+            promise_handle.promise_holder = nullptr;
+        }
+    }
+
+    // Delete the promise or scope outside of the lock.
+    if (promise_handle.promise_holder) {
+        delete promise_handle.promise_holder;
+    } else {
+        delete promise_handle.state;
+    }
+}
+
+scope::promise_holder_base* scope::state::try_acquire_promise(
+    promise_handle promise_handle) {
+    if (promise_handle.promise_holder) {
+        uint64_t prior_count = promise_handle.state->acquired_promise_count_.fetch_add(
+            1u, std::memory_order_relaxed);
+        if (!(prior_count & scope_exited)) {
+            return promise_handle.promise_holder;
+        }
+        promise_handle.state->acquired_promise_count_.fetch_sub(
+            1u, std::memory_order_relaxed);
+    }
+    return nullptr;
+}
+
+void scope::state::release_promise(promise_handle promise_handle) {
+    promise_handle.state->acquired_promise_count_.fetch_sub(
+        1u, std::memory_order_relaxed);
+}
+
+} // namespace fit
+
+#endif // FIT_NO_STD_FOR_ZIRCON_USERSPACE
diff --git a/system/utest/fit/rules.mk b/system/utest/fit/rules.mk
index 9443e4a..efaa2aa 100644
--- a/system/utest/fit/rules.mk
+++ b/system/utest/fit/rules.mk
@@ -21,6 +21,7 @@
     $(LOCAL_DIR)/promise_tests.cpp \
     $(LOCAL_DIR)/result_tests.cpp \
     $(LOCAL_DIR)/scheduler_tests.cpp \
+    $(LOCAL_DIR)/scope_tests.cpp \
     $(LOCAL_DIR)/sequencer_tests.cpp \
     $(LOCAL_DIR)/single_threaded_executor_tests.cpp \
     $(LOCAL_DIR)/suspended_task_tests.cpp \
diff --git a/system/utest/fit/scope_tests.cpp b/system/utest/fit/scope_tests.cpp
new file mode 100644
index 0000000..102decd
--- /dev/null
+++ b/system/utest/fit/scope_tests.cpp
@@ -0,0 +1,253 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <unistd.h>
+
+#include <thread>
+
+#include <lib/fit/bridge.h>
+#include <lib/fit/defer.h>
+#include <lib/fit/scope.h>
+#include <lib/fit/single_threaded_executor.h>
+#include <unittest/unittest.h>
+
+#include "unittest_utils.h"
+
+namespace {
+
+class fake_context : public fit::context {
+public:
+    fit::executor* executor() const override {
+        ASSERT_CRITICAL(false);
+    }
+    fit::suspended_task suspend_task() override {
+        ASSERT_CRITICAL(false);
+    }
+};
+
+// Asynchronously accumulates a sum.
+// This is an example of an object that offers promises that captures
+// the "this" pointer, thereby needing a scope to prevent dangling pointers
+// in case it is destroyed before the promises complete.
+class accumulator {
+public:
+    // Adds a value to the counter then returns it.
+    // Takes time proportional to the value being added.
+    fit::promise<uint32_t> add(uint32_t value) {
+        return fit::make_promise(
+                   [this, cycles = value](fit::context& context) mutable
+                   -> fit::result<uint32_t> {
+                       if (cycles == 0)
+                           return fit::ok(counter_);
+                       counter_++;
+                       cycles--;
+                       context.suspend_task().resume_task();
+                       return fit::pending();
+                   })
+            .wrap_with(scope_);
+    }
+
+    // Gets the current count, immediately.
+    uint32_t count() const { return counter_; }
+
+private:
+    fit::scope scope_;
+    uint32_t counter_ = 0;
+};
+
+bool scoping_tasks() {
+    BEGIN_TEST;
+
+    auto acc = std::make_unique<accumulator>();
+    fit::single_threaded_executor executor;
+    uint32_t sums[4] = {};
+
+    // Schedule some tasks which accumulate values asynchronously.
+    executor.schedule_task(acc->add(2).and_then(
+        [&](uint32_t value) { sums[0] = value; }));
+    executor.schedule_task(acc->add(1).and_then(
+        [&](uint32_t value) { sums[1] = value; }));
+    executor.schedule_task(acc->add(5).and_then(
+        [&](uint32_t value) { sums[2] = value; }));
+
+    // Schedule a task which accumulates and then destroys the accumulator
+    // so that the scope is exited.  Any remaining promises will be aborted.
+    uint32_t last_count = 0;
+    executor.schedule_task(acc->add(3).and_then(
+        [&](uint32_t value) {
+            sums[3] = value;
+            // Schedule destruction in another task to avoid re-entrance.
+            executor.schedule_task(fit::make_promise([&] {
+                last_count = acc->count();
+                acc.reset();
+            }));
+        }));
+
+    // Run the tasks.
+    executor.run();
+
+    // The counts reflect the fact that the scope is exited part-way through
+    // the cycle.  For example, the sums[2] task doesn't get to run since
+    // it only runs after 5 cycles and the scope is exited on the third.
+    EXPECT_EQ(11, last_count);
+    EXPECT_EQ(7, sums[0]);
+    EXPECT_EQ(5, sums[1]);
+    EXPECT_EQ(0, sums[2]);
+    EXPECT_EQ(10, sums[3]);
+
+    END_TEST;
+}
+
+bool exit_destroys_wrapped_promises() {
+    BEGIN_TEST;
+
+    fit::scope scope;
+    EXPECT_FALSE(scope.exited());
+
+    // Set up three wrapped promises.
+    bool destroyed[4] = {};
+    auto p0 = scope.wrap(fit::make_promise(
+        [d = fit::defer([&] { destroyed[0] = true; })] { return fit::ok(); }));
+    auto p1 = scope.wrap(fit::make_promise(
+        [d = fit::defer([&] { destroyed[1] = true; })] { return fit::ok(); }));
+    auto p2 = scope.wrap(fit::make_promise(
+        [d = fit::defer([&] { destroyed[2] = true; })] { return fit::ok(); }));
+    EXPECT_FALSE(destroyed[0]);
+    EXPECT_FALSE(destroyed[1]);
+    EXPECT_FALSE(destroyed[2]);
+
+    // Execute one of them to completion, causing it to be destroyed.
+    EXPECT_TRUE(fit::run_single_threaded(std::move(p1)).is_ok());
+    EXPECT_FALSE(destroyed[0]);
+    EXPECT_TRUE(destroyed[1]);
+    EXPECT_FALSE(destroyed[2]);
+
+    // Exit the scope, causing the wrapped promise to be destroyed
+    // while still leaving the wrapper alive (but aborted).
+    scope.exit();
+    EXPECT_TRUE(scope.exited());
+    EXPECT_TRUE(destroyed[0]);
+    EXPECT_TRUE(destroyed[1]);
+    EXPECT_TRUE(destroyed[2]);
+
+    // Wrapping another promise causes the wrapped promise to be immediately
+    // destroyed.
+    auto p3 = scope.wrap(fit::make_promise(
+        [d = fit::defer([&] { destroyed[3] = true; })] { return fit::ok(); }));
+    EXPECT_TRUE(destroyed[3]);
+
+    // Executing the wrapped promises returns pending.
+    EXPECT_TRUE(fit::run_single_threaded(std::move(p0)).is_pending());
+    EXPECT_TRUE(fit::run_single_threaded(std::move(p2)).is_pending());
+    EXPECT_TRUE(fit::run_single_threaded(std::move(p3)).is_pending());
+
+    // Exiting again has no effect.
+    scope.exit();
+    EXPECT_TRUE(scope.exited());
+
+    END_TEST;
+}
+
+bool double_wrap() {
+    BEGIN_TEST;
+
+    fit::scope scope;
+    fake_context context;
+
+    // Here we wrap a task that's already been wrapped to see what happens
+    // when the scope is exited.  This is interesting because it means that
+    // the destruction of one wrapped promise will cause the destruction of
+    // another wrapped promise and could uncover re-entrance issues.
+    uint32_t run_count = 0;
+    bool destroyed = false;
+    auto promise =
+        fit::make_promise(
+            [&, d = fit::defer([&] { destroyed = true; })](fit::context& context) {
+                run_count++;
+                return fit::pending();
+            })
+            .wrap_with(scope)
+            .wrap_with(scope); // wrap again!
+
+    // Run the promise once to show that we can.
+    EXPECT_EQ(fit::result_state::pending, promise(context).state());
+    EXPECT_EQ(1, run_count);
+    EXPECT_FALSE(destroyed);
+
+    // Now exit the scope, which should cause the promise to be destroyed.
+    scope.exit();
+    EXPECT_EQ(1, run_count);
+    EXPECT_TRUE(destroyed);
+
+    // Running the promise again should do nothing.
+    EXPECT_EQ(fit::result_state::pending, promise(context).state());
+    EXPECT_EQ(1, run_count);
+    EXPECT_TRUE(destroyed);
+
+    END_TEST;
+}
+
+bool thread_safety() {
+    BEGIN_TEST;
+
+    fit::scope scope;
+    fit::single_threaded_executor executor;
+    uint64_t run_count = 0;
+
+    // Schedule work from a few threads, just to show that we can.
+    // Part way through, exit the scope.
+    constexpr int num_threads = 4;
+    constexpr int num_tasks_per_thread = 100;
+    constexpr int exit_threshold = 75;
+    std::thread threads[num_threads];
+    for (int i = 0; i < num_threads; i++) {
+        fit::bridge bridge;
+        threads[i] =
+            std::thread([&, completer = std::move(bridge.completer())]() mutable {
+                for (int j = 0; j < num_tasks_per_thread; j++) {
+                    if (j == exit_threshold) {
+                        executor.schedule_task(fit::make_promise([&] {
+                            scope.exit();
+                        }));
+                    }
+
+                    executor.schedule_task(
+                        fit::make_promise([&] {
+                            run_count++;
+                        }).wrap_with(scope));
+                }
+                completer.complete_ok();
+            });
+        executor.schedule_task(bridge.consumer().promise());
+    }
+
+    // Run the tasks.
+    executor.run();
+    for (int i = 0; i < num_threads; i++)
+        threads[i].join();
+
+    // We expect some non-deterministic number of tasks to have run
+    // related to the exit threshold.
+    // We scheduled num_threads * num_tasks_per_thread tasks, but on each thread
+    // we exited the (common) scope after scheduling its first exit_threshold
+    // tasks.  Once one of those threads exits the scope, no more tasks
+    // (scheduled by any thread) will run within the scope, so the number of
+    // executed tasks cannot increase any further.  Therefore we know that at
+    // least exit_threshold tasks have run but we could have run as many as
+    // num_threads * exit_threshold in a perfect world where all of the threads
+    // called scope.exit() at the same time.
+    EXPECT_GE(run_count, exit_threshold);
+    EXPECT_LE(run_count, num_threads * exit_threshold);
+
+    END_TEST;
+}
+
+} // namespace
+
+BEGIN_TEST_CASE(scope_tests)
+RUN_TEST(scoping_tasks)
+RUN_TEST(exit_destroys_wrapped_promises)
+RUN_TEST(double_wrap)
+RUN_TEST(thread_safety)
+END_TEST_CASE(scope_tests)