blob: ff895d572184f6b5898177eecf6f44a7bbf6e25b [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 <fuchsia/modular/storymodel/cpp/fidl.h>
#include <lib/async_promise/executor.h>
#include <lib/fit/bridge.h>
#include <lib/fit/function.h>
#include <lib/fit/single_threaded_executor.h>
#include "gtest/gtest.h"
#include "peridot/bin/sessionmgr/story/model/apply_mutations.h"
#include "peridot/bin/sessionmgr/story/model/ledger_story_model_storage.h"
#include "peridot/bin/sessionmgr/story/model/testing/mutation_matchers.h"
#include "peridot/lib/ledger_client/ledger_client.h"
#include "peridot/lib/ledger_client/page_id.h"
#include "peridot/lib/testing/test_with_ledger.h"
using fuchsia::modular::StoryState;
using fuchsia::modular::StoryVisibilityState;
using fuchsia::modular::storymodel::StoryModel;
using fuchsia::modular::storymodel::StoryModelMutation;
namespace modular {
namespace {
// TODO: there is no good candidate for testing conflict resolution in the
// StoryModel as of yet. What would be good is, e.g.: setting a value on a
// ModuleModel while simultaneously deleting the entire entry.
class LedgerStoryModelStorageTest : public testing::TestWithLedger {
public:
async::Executor executor;
LedgerStoryModelStorageTest() : executor(dispatcher()) {}
// Creates a new LedgerStoryModelStorage instance and returns:
//
// 1) A unique_ptr to the new instance.
// 2) A ptr to a vector of lists of StoryModelMutations observed from that
// instance.
// 3) A ptr to a StoryModel updated with the observed commands.
std::tuple<std::unique_ptr<StoryModelStorage>,
std::vector<std::vector<StoryModelMutation>>*, StoryModel*>
Create(std::string page_id, std::string device_id) {
auto storage = std::make_unique<LedgerStoryModelStorage>(
ledger_client(), MakePageId(page_id), device_id);
auto observed_commands =
observed_mutations_.emplace(observed_mutations_.end());
auto observed_model = observed_models_.emplace(observed_models_.end());
storage->SetObserveCallback([=](std::vector<StoryModelMutation> commands) {
*observed_model = ApplyMutations(*observed_model, commands);
observed_commands->push_back(std::move(commands));
});
return std::make_tuple(std::move(storage), &*observed_commands,
&*observed_model);
}
// This is broken out into its own function because we use C++ structured
// bindings to capture the result of Create() above. These cannot be
// implicitly captured in lambdas without more verbose syntax. This function
// converts the binding into a real variable which is possible to capture.
void RunLoopUntilNumMutationsObserved(
std::vector<std::vector<StoryModelMutation>>* observed_mutations,
uint32_t n) {
RunLoopUntil([&] { return observed_mutations->size() >= n; });
}
private:
// A list (per StoryModelStorage instance) of the commands issued to each call
// to StoryModelStorage.Observe().
std::list<std::vector<std::vector<StoryModelMutation>>> observed_mutations_;
std::list<StoryModel> observed_models_;
};
// Store some device-local values (runtime state, visibility state), and
// observe the values coming back to us.
TEST_F(LedgerStoryModelStorageTest, DeviceLocal_RoundTrip) {
auto [storage, observed_mutations, observed_model] =
Create("page1", "device1");
std::vector<StoryModelMutation> commands(2);
commands[0].set_set_runtime_state(StoryState::RUNNING);
commands[1].set_set_visibility_state(StoryVisibilityState::IMMERSIVE);
fit::result<> result;
executor.schedule_task(
storage->Execute(std::move(commands)).then([&](fit::result<>& r) {
result = std::move(r);
}));
RunLoopUntil([&] { return !!result; });
EXPECT_TRUE(result.is_ok());
// We expect to see these values resulting in a notification from the ledger
// eventually.
RunLoopUntilNumMutationsObserved(observed_mutations, 1);
EXPECT_EQ(1lu, observed_mutations->size());
EXPECT_THAT(observed_mutations->at(0),
::testing::ElementsAre(
IsSetRuntimeStateMutation(StoryState::RUNNING),
IsSetVisibilityMutation(StoryVisibilityState::IMMERSIVE)));
// Now change only StoryState. We should see the result of our previous
// change to StoryVisibilityState preserved.
commands.resize(1);
commands[0].set_set_runtime_state(StoryState::STOPPED);
result = fit::result<>();
executor.schedule_task(
storage->Execute(std::move(commands)).then([&](fit::result<>& r) {
result = std::move(r);
}));
RunLoopUntil([&] { return !!result; });
EXPECT_TRUE(result.is_ok());
RunLoopUntilNumMutationsObserved(observed_mutations, 2);
EXPECT_EQ(2lu, observed_mutations->size());
EXPECT_THAT(observed_mutations->at(1),
::testing::ElementsAre(
IsSetRuntimeStateMutation(StoryState::STOPPED),
IsSetVisibilityMutation(StoryVisibilityState::IMMERSIVE)));
}
// Show that when we store values for two different device IDs in the same
// Ledger page, they do not cause any conflicts.
TEST_F(LedgerStoryModelStorageTest, DeviceLocal_DeviceIsolation) {
auto [storage1, observed_mutations1, observed_model1] =
Create("page1", "device1");
auto [storage2, observed_mutations2, observed_model2] =
Create("page1", "device2");
// Set runtime state to RUNNING on device1, and set visibility state to
// IMMERSIVE on device2.
{
std::vector<StoryModelMutation> commands(1);
commands[0].set_set_runtime_state(StoryState::RUNNING);
executor.schedule_task(storage1->Execute(std::move(commands)));
}
{
std::vector<StoryModelMutation> commands(1);
commands[0].set_set_visibility_state(StoryVisibilityState::IMMERSIVE);
executor.schedule_task(storage2->Execute(std::move(commands)));
}
RunLoopUntilNumMutationsObserved(observed_mutations1, 1);
RunLoopUntilNumMutationsObserved(observed_mutations2, 1);
EXPECT_TRUE(observed_model1->has_runtime_state());
EXPECT_FALSE(observed_model1->has_visibility_state());
EXPECT_TRUE(observed_model2->has_visibility_state());
EXPECT_FALSE(observed_model2->has_runtime_state());
}
// Create two update tasks but schedule them out of order. We expect them to
// run in order.
TEST_F(LedgerStoryModelStorageTest, UpdatesAreSequential) {
auto [storage, observed_mutations, observed_model] = Create("page", "device");
std::vector<StoryModelMutation> commands(1);
commands[0].set_set_runtime_state(StoryState::RUNNING);
auto promise1 = storage->Execute(std::move(commands));
commands.resize(1);
commands[0].set_set_runtime_state(StoryState::STOPPING);
auto promise2 = storage->Execute(std::move(commands));
executor.schedule_task(std::move(promise2));
RunLoopUntilIdle(); // For good measure.
executor.schedule_task(std::move(promise1));
RunLoopUntilNumMutationsObserved(observed_mutations, 2);
EXPECT_EQ(StoryState::STOPPING, *observed_model->runtime_state());
}
// When Load() is called, read what is stored in the Ledger back out and
// expect to see commands that represent that state through the storage
// observer.
TEST_F(LedgerStoryModelStorageTest, Load) {
StoryModel expected_model;
{
auto [storage, observed_mutations, observed_model] =
Create("page", "device");
std::vector<StoryModelMutation> commands(2);
commands[0].set_set_runtime_state(StoryState::RUNNING);
commands[1].set_set_visibility_state(StoryVisibilityState::IMMERSIVE);
// TODO(thatguy): As we add more StoryModelMutations, add more lines here.
executor.schedule_task(storage->Execute(std::move(commands)));
bool done{false};
executor.schedule_task(storage->Flush().and_then([&] { done = true; }));
RunLoopUntil([&] { return done; });
expected_model = std::move(*observed_model);
}
auto [storage, observed_mutations, observed_model] = Create("page", "device");
bool done{false};
executor.schedule_task(
storage->Load().then([&](fit::result<>&) { done = true; }));
RunLoopUntil([&] { return done; });
EXPECT_EQ(expected_model, *observed_model);
}
} // namespace
} // namespace modular