blob: 16d7d054c39ac9c07b0f3b0dc79bb8dc0d368c31 [file] [log] [blame]
// 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 <lib/async/cpp/operation.h>
#include <lib/async/cpp/future.h>
#include <lib/async/cpp/task.h>
#include <lib/async/default.h>
#include <lib/fxl/memory/weak_ptr.h>
#include <lib/gtest/test_loop_fixture.h>
#include "gtest/gtest.h"
namespace modular {
namespace {
class TestContainer : public OperationContainer {
public:
TestContainer() : weak_ptr_factory_(this) {}
fxl::WeakPtr<OperationContainer> GetWeakPtr() override {
return weak_ptr_factory_.GetWeakPtr();
}
int hold_count{0};
int drop_count{0};
int cont_count{0};
OperationBase* last_held = nullptr;
OperationBase* last_dropped = nullptr;
void Hold(OperationBase* o) override {
++hold_count;
last_held = o;
}
void Drop(OperationBase* o) override {
++drop_count;
last_dropped = o;
}
void Cont() override { ++cont_count; }
void ScheduleTask(fit::pending_task task) override {}
void PretendToDie() { weak_ptr_factory_.InvalidateWeakPtrs(); }
using OperationContainer::InvalidateWeakPtrs; // Promote for testing.
using OperationContainer::Schedule; // Promote for testing.
private:
fxl::WeakPtrFactory<OperationContainer> weak_ptr_factory_;
};
template <typename... Args>
class TestOperation : public Operation<Args...> {
public:
using ResultCall = std::function<void(Args...)>;
TestOperation(std::function<void()> task, ResultCall done)
: Operation<Args...>("Test Operation", std::move(done)), task_(task) {}
void SayDone(Args... args) { this->Done(args...); }
private:
void Run() override { task_(); }
std::function<void()> task_;
};
class OperationTest : public gtest::TestLoopFixture {};
// Test the lifecycle of a single Operation:
// 1) Creating a new operation and adding it to a container
// causes it to be scheduled (posted to the async task queue).
// 2) When the operation states it is done, the container is told
// to schedule the next task after the result callback is called.
TEST_F(OperationTest, Lifecycle) {
TestContainer container;
bool op_ran = false;
bool op_done = false;
auto op =
std::make_unique<TestOperation<>>([&op_ran] { op_ran = true; },
[&op_done, &container] {
op_done = true;
// When our op is done, we
// expect that the
// OperationContainer that owns
// it has already been told to
// drop it, but we do not expect
// Cont() to have been called.
// That happens after the done
// callback is executed.
EXPECT_EQ(1, container.drop_count);
EXPECT_EQ(0, container.cont_count);
});
// Add() calls Hold() on our OperationContainer implementation. Hold() is
// supposed to manage the memory. Ours doesn't.
// The task doesn't run until OperationContainer::Schedule is called with the
// operation.
// TODO(thatguy): TestContainer does not manage memory yet, so passing a
// naked pointer backed by a unique_ptr<> is safe.
container.Add(op.get());
EXPECT_EQ(1, container.hold_count);
EXPECT_EQ(op.get(), container.last_held);
EXPECT_EQ(0, container.drop_count);
EXPECT_EQ(0, container.cont_count);
EXPECT_FALSE(op_ran);
EXPECT_FALSE(op_done);
// Add() does not enqueue the operation to be run, at least not in our
// implementation of OperationContainer.
RunLoopUntilIdle();
EXPECT_FALSE(op_ran);
EXPECT_FALSE(op_done);
EXPECT_EQ(1, container.hold_count);
EXPECT_EQ(0, container.drop_count);
EXPECT_EQ(0, container.cont_count);
// OperationContainer impls opt to call Schedule() when they are ready to
// have an operation enqueued.
container.Schedule(op.get());
// So when we advance the async loop, we should see that it started running.
RunLoopUntilIdle();
EXPECT_TRUE(op_ran);
EXPECT_FALSE(op_done);
EXPECT_EQ(1, container.hold_count);
EXPECT_EQ(0, container.drop_count);
EXPECT_EQ(0, container.cont_count);
// When the operation reports being done, we expect it to be dropped, our
// done callback run, and the OperationContainer told to continue. These
// happen in order. The order is verified in our done callback.
op->SayDone();
EXPECT_TRUE(op_ran);
EXPECT_TRUE(op_done);
EXPECT_EQ(1, container.hold_count);
EXPECT_EQ(1, container.drop_count);
EXPECT_EQ(op.get(), container.last_dropped);
EXPECT_EQ(1, container.cont_count);
}
TEST_F(OperationTest, Lifecycle_ContainerGoesAway) {
// In this test, we make the Operation think that its container's memory has
// been cleaned up in its done callback. This manifests itself as the
// Operation not invoking Cont() on the container.
TestContainer container;
bool op_ran = false;
auto op = std::make_unique<TestOperation<>>(
[&op_ran]() { op_ran = true; },
[&container]() { container.PretendToDie(); });
container.Add(op.get());
container.Schedule(op.get());
RunLoopUntilIdle();
// When the operation reports being done, we expect it to be dropped, our
// done callback run, and the OperationContainer told to continue. These
// happen in order. The order is verified in our done callback.
op->SayDone();
EXPECT_TRUE(op_ran);
EXPECT_EQ(1, container.hold_count);
EXPECT_EQ(1, container.drop_count);
EXPECT_EQ(op.get(), container.last_dropped);
EXPECT_EQ(0, container.cont_count);
}
TEST_F(OperationTest, ResultsAreReceived) {
// Show that when an operation calls Done(), its arguments are
// passed to the done callback.
TestContainer container;
auto op = std::make_unique<TestOperation<int>>(
[] {}, [](int result) { EXPECT_EQ(42, result); });
container.Add(op.get());
container.Schedule(op.get());
RunLoopUntilIdle();
op->SayDone(42);
}
class TestFlowTokenOperation : public Operation<int> {
public:
TestFlowTokenOperation(ResultCall done)
: Operation("Test FlowToken Operation", std::move(done)) {}
// |call_before_flow_dies| is invoked before the FlowToken goes out of scope.
void SayDone(
int result, std::function<void()> call_before_flow_dies = [] {}) {
// When |flow| goes out of scope, it will call Done() for us.
FlowToken flow{this, &result_};
// Post the continuation of the operation to an async loop so that we
// exercise the refcounting of FlowTokens.
async::PostTask(async_get_default_dispatcher(),
[this, flow, result, call_before_flow_dies] {
result_ = result;
call_before_flow_dies();
});
}
private:
void Run() override {}
int result_;
};
TEST_F(OperationTest, Lifecycle_FlowToken) {
// FlowTokens simply call Done() for you with whatever results you've pointed
// it at. NOTE(thatguy): I haven't figured out a good way of testing the
// refcount goodness of FlowTokens. That said, everything else in the world
// will crash if they fail.
TestContainer container;
bool done_called{false};
int result{0};
auto op =
std::make_unique<TestFlowTokenOperation>([&result, &done_called](int r) {
done_called = true;
result = r;
});
container.Add(op.get());
container.Schedule(op.get());
RunLoopUntilIdle();
op->SayDone(42);
// TestFlowTokenOperation posts to the async loop in SayDone(). We shouldn't
// see Done() called until we run the loop and the FlowToken on the capture
// list of the callback goes out of scope.
EXPECT_FALSE(done_called);
RunLoopUntilIdle();
EXPECT_EQ(42, result);
}
TEST_F(OperationTest, Lifecycle_FlowToken_OperationGoesAway) {
// Similar to Lifecycle_FlowToken, but FlowTokens have the behavior that if
// their "owning" operation dies before they go out of scope (because, ie,
// some callback is sitting in someone else's memory longer than the
// operation lives), they don't call Done() on that operation.
TestContainer container;
bool done_called = false;
auto op = std::make_unique<TestFlowTokenOperation>(
[&done_called](int result) { done_called = true; });
container.Add(op.get());
container.Schedule(op.get());
RunLoopUntilIdle();
op->SayDone(42,
[&container, &op]() { container.InvalidateWeakPtrs(op.get()); });
RunLoopUntilIdle();
EXPECT_FALSE(done_called);
}
TEST_F(OperationTest, OperationQueue) {
// Here we test a specific implementation of OperationContainer
// (OperationQueue), which should only allow one operation to "run" at a
// given time.
OperationQueue container;
// OperationQueue, unlike TestContainer, does own the Operations.
bool op1_ran = false;
bool op1_done = false;
auto* op1 = new TestOperation<>([&op1_ran]() { op1_ran = true; },
[&op1_done]() { op1_done = true; });
bool op3_ran = false;
bool op3_done = false;
auto* op3 = new TestOperation<>([&op3_ran]() { op3_ran = true; },
[&op3_done]() { op3_done = true; });
// We'll queue |op1|, then a fit::promise ("op2") and another Operation,
// |op3|.
container.Add(op1);
bool op2_ran = false;
bool op2_done = false;
fit::suspended_task suspended_op2;
container.ScheduleTask(
fit::make_promise([&](fit::context& c) -> fit::result<> {
if (op2_ran == true) {
op2_done = true;
return fit::ok();
}
op2_ran = true;
suspended_op2 = c.suspend_task();
return fit::pending();
}));
container.Add(op3);
// Nothing has run yet because we haven't run the async loop.
EXPECT_FALSE(op1_ran);
EXPECT_FALSE(op1_done);
EXPECT_FALSE(op2_ran);
EXPECT_FALSE(op2_done);
EXPECT_FALSE(op3_ran);
EXPECT_FALSE(op3_done);
// Running the loop we expect op1 to have run, but not completed.
RunLoopUntilIdle();
EXPECT_TRUE(op1_ran);
EXPECT_FALSE(op1_done);
EXPECT_FALSE(op2_ran);
EXPECT_FALSE(op2_done);
EXPECT_FALSE(op3_ran);
EXPECT_FALSE(op3_done);
// But even if we run more, we do not expect any other ops to run.
RunLoopUntilIdle();
EXPECT_TRUE(op1_ran);
EXPECT_FALSE(op1_done);
EXPECT_FALSE(op2_ran);
EXPECT_FALSE(op2_done);
EXPECT_FALSE(op3_ran);
EXPECT_FALSE(op3_done);
// If op1 says it's Done(), we expect op2 to run.
op1->SayDone();
RunLoopUntilIdle();
EXPECT_TRUE(op1_done);
EXPECT_TRUE(op2_ran);
EXPECT_FALSE(op2_done);
EXPECT_FALSE(op3_ran);
EXPECT_FALSE(op3_done);
// Running the loop again should do nothing, as op2 is still pending (until
// we resume it manuall).
RunLoopUntilIdle();
EXPECT_TRUE(suspended_op2);
EXPECT_FALSE(op2_done);
EXPECT_FALSE(op3_ran);
EXPECT_FALSE(op3_done);
// Resume op2, and run the loop again. We expect op3 to start, but not
// complete.
suspended_op2.resume_task();
RunLoopUntilIdle();
EXPECT_TRUE(op2_done);
EXPECT_TRUE(op3_ran);
EXPECT_FALSE(op3_done);
}
TEST_F(OperationTest, OperationCollection) {
// OperationCollection starts all operations immediately and lets them run in
// parallel.
OperationCollection container;
// OperationQueue, unlike TestContainer, does manage its memory.
bool op1_ran = false;
bool op1_done = false;
auto* op1 = new TestOperation<>([&op1_ran]() { op1_ran = true; },
[&op1_done]() { op1_done = true; });
bool op2_ran = false;
bool op2_done = false;
auto* op2 = new TestOperation<>([&op2_ran]() { op2_ran = true; },
[&op2_done]() { op2_done = true; });
container.Add(op1);
container.Add(op2);
bool op3_ran = false;
container.ScheduleTask(fit::make_promise([&] { op3_ran = true; }));
// Nothing has run yet because we haven't run the async loop.
EXPECT_FALSE(op1_ran);
EXPECT_FALSE(op1_done);
EXPECT_FALSE(op2_ran);
EXPECT_FALSE(op2_done);
EXPECT_FALSE(op3_ran);
// Running the loop we expect all ops to have run. TestOperations won't have
// completed because they require us to call SayDone(). The fit::promise,
// however, will have completed (it doesn't suspend).
RunLoopUntilIdle();
EXPECT_TRUE(op1_ran);
EXPECT_FALSE(op1_done);
EXPECT_TRUE(op2_ran);
EXPECT_FALSE(op2_done);
EXPECT_TRUE(op2_ran);
}
class TestOperationNotNullPtr : public Operation<> {
public:
TestOperationNotNullPtr(const FlowToken& parent_flow_token)
: Operation("Test Operation on container is not nullptr", [] {}),
parent_flow_token_(parent_flow_token) {}
private:
void Run() override { FlowToken flow{this}; }
FlowToken parent_flow_token_;
};
class TestQueueNotNullPtr : public Operation<> {
public:
TestQueueNotNullPtr(ResultCall done)
: Operation("TestQueueNotNullPtr", std::move(done)) {}
private:
void Run() override {
// When |flow| goes out of scope, it will call Done() for us.
FlowToken flow{this};
operation_queue_.Add(new TestOperationNotNullPtr(flow));
}
OperationQueue operation_queue_;
};
class TestCollectionNotNullPtr : public Operation<> {
public:
TestCollectionNotNullPtr(ResultCall done)
: Operation("TestCollectionNotNullPtr", std::move(done)) {}
private:
void Run() override {
// When |flow| goes out of scope, it will call Done() for us.
FlowToken flow{this};
operation_collection_.Add(new TestOperationNotNullPtr(flow));
}
OperationCollection operation_collection_;
};
// See comments on OperationQueue::Drop.
TEST_F(OperationTest, TestQueueNotNullPtr) {
OperationQueue container;
bool done_called{false};
auto op = new TestQueueNotNullPtr([&done_called] { done_called = true; });
container.Add(op);
// Nothing has run yet because we haven't run the async loop.
EXPECT_FALSE(done_called);
// Running the loop we expect done_called to be set to true
RunLoopUntilIdle();
EXPECT_TRUE(done_called);
}
// See comments on OperationCollection::Drop.
TEST_F(OperationTest, TestCollectionNotNullPtr) {
OperationQueue container;
bool done_called{false};
auto op =
new TestCollectionNotNullPtr([&done_called] { done_called = true; });
container.Add(op);
// Nothing has run yet because we haven't run the async loop.
EXPECT_FALSE(done_called);
// Running the loop we expect done_called to be set to true
RunLoopUntilIdle();
EXPECT_TRUE(done_called);
}
TEST_F(OperationTest, WrapFutureAsOperation_WithResult) {
// Show that when we wrap a Future<> as an operation on a queue, it runs.
bool op_did_start{};
bool op_did_finish{};
auto on_run = Future<>::Create(__PRETTY_FUNCTION__);
auto done = on_run->Map([&]() -> int {
EXPECT_FALSE(op_did_finish);
op_did_start = true;
return 10;
});
OperationCollection container;
container.Add(WrapFutureAsOperation(__PRETTY_FUNCTION__, on_run, done,
std::function<void(int)>([&](int result) {
EXPECT_EQ(10, result);
op_did_finish = true;
})));
RunLoopUntilIdle();
EXPECT_TRUE(op_did_start);
EXPECT_TRUE(op_did_finish);
}
TEST_F(OperationTest, WrapFutureAsOperation_WithoutResult) {
// Show that when we wrap a Future<> as an operation on a queue, it runs.
bool op_did_start{};
bool op_did_finish{};
auto on_run = Future<>::Create(__PRETTY_FUNCTION__);
auto done = on_run->Then([&] {
EXPECT_FALSE(op_did_finish);
op_did_start = true;
});
OperationCollection container;
container.Add(WrapFutureAsOperation(
__PRETTY_FUNCTION__, on_run, done,
std::function<void()>([&] { op_did_finish = true; })));
RunLoopUntilIdle();
EXPECT_TRUE(op_did_start);
EXPECT_TRUE(op_did_finish);
}
} // namespace
} // namespace modular