blob: b0b0ec86934bd7e12afbc634fb7f697436560511 [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/ledger/cloud/cpp/fidl.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/ledger/bin/fidl/include/types.h"
#include "src/ledger/bin/tests/cloud_provider/types.h"
#include "src/ledger/bin/tests/cloud_provider/validation_test.h"
#include "src/ledger/lib/convert/convert.h"
#include "src/ledger/lib/encoding/encoding.h"
#include "src/ledger/lib/logging/logging.h"
#include "src/ledger/lib/socket/strings.h"
#include "src/ledger/lib/vmo/sized_vmo.h"
#include "src/ledger/lib/vmo/strings.h"
using ::testing::AllOf;
using ::testing::AnyOf;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Property;
#define TABLE_FIELD_MATCHES(type, field, predicate) \
AllOf(Property("has_" #field, &type::has_##field, Eq(true)), \
Property(#field, &type::field, predicate))
namespace cloud_provider {
namespace {
// Verifies that the given array of commits contains a commit of the given id
// and data.
::testing::AssertionResult CheckThatCommitsContain(
const std::vector<cloud_provider::Commit>& commits, convert::ExtendedStringView id,
convert::ExtendedStringView data) {
for (auto& entry : commits) {
if (!entry.has_id() || convert::ExtendedStringView(entry.id()) != id) {
continue;
}
if (!entry.has_data() || convert::ExtendedStringView(entry.data()) != data) {
return ::testing::AssertionFailure()
<< "The commit of the expected id: 0x" << convert::ToHex(id) << " (" << id << ") "
<< " was found but its data doesn't match - expected: 0x" << convert::ToHex(data)
<< " but found: " << convert::ToHex(entry.data());
}
return ::testing::AssertionSuccess();
}
return ::testing::AssertionFailure()
<< "The commit of the id: " << convert::ToHex(id) << " (" << id << ") "
<< " is missing.";
}
class PageCloudTest : public ValidationTest, public PageCloudWatcher {
public:
PageCloudTest() = default;
~PageCloudTest() override = default;
protected:
::testing::AssertionResult GetPageCloud(std::vector<uint8_t> app_id, std::vector<uint8_t> page_id,
PageCloudSyncPtr* page_cloud) {
*page_cloud = PageCloudSyncPtr();
Status status = Status::INTERNAL_ERROR;
if (cloud_provider_->GetPageCloud(std::move(app_id), std::move(page_id),
page_cloud->NewRequest(), &status) != ZX_OK) {
return ::testing::AssertionFailure()
<< "Failed to retrieve the page cloud due to channel error.";
}
if (status != Status::OK) {
return ::testing::AssertionFailure() << "Failed to retrieve the page cloud, received status: "
<< fidl::ToUnderlying(status);
}
return ::testing::AssertionSuccess();
}
::testing::AssertionResult GetLatestPositionToken(
PageCloudSyncPtr* page_cloud, std::unique_ptr<cloud_provider::PositionToken>* token) {
Status status = Status::INTERNAL_ERROR;
std::unique_ptr<cloud_provider::CommitPack> commit_pack;
if ((*page_cloud)->GetCommits(nullptr, &status, &commit_pack, token) != ZX_OK) {
return ::testing::AssertionFailure()
<< "Failed to retrieve the position token due to channel error.";
}
if (status != Status::OK) {
return ::testing::AssertionFailure()
<< "Failed to retrieve the position token, received status: "
<< fidl::ToUnderlying(status);
}
return ::testing::AssertionSuccess();
}
::testing::AssertionResult GetAndDecodeDiff(PageCloudSyncPtr* page_cloud,
std::vector<uint8_t> commit_id,
std::vector<std::vector<uint8_t>> possible_bases,
Diff* diff) {
Status status;
std::unique_ptr<DiffPack> diff_pack;
if ((*page_cloud)->GetDiff(commit_id, std::move(possible_bases), &status, &diff_pack) !=
ZX_OK) {
return ::testing::AssertionFailure() << "Failed to retrieve the diff due to a channel error.";
}
if (status != Status::OK) {
return ::testing::AssertionFailure()
<< "Failed to retrieve the diff, received status: " << fidl::ToUnderlying(status);
}
if (!diff_pack) {
return ::testing::AssertionFailure() << "Received an empty diff pack.";
}
if (!ledger::DecodeFromBuffer(diff_pack->buffer, diff)) {
return ::testing::AssertionFailure() << "Received invalid data in diff pack.";
}
return ::testing::AssertionSuccess();
}
::testing::AssertionResult AddCommits(
PageCloudSyncPtr* page_cloud,
std::vector<std::pair<std::string, std::string>> commit_ids_and_data) {
Commits commits;
for (auto& [commit_id, commit_data] : commit_ids_and_data) {
Commit commit;
*commit.mutable_id() = convert::ToArray(commit_id);
*commit.mutable_data() = convert::ToArray(commit_data);
commits.commits.push_back(std::move(commit));
}
CommitPack commit_pack;
if (!ledger::EncodeToBuffer(&commits, &commit_pack.buffer)) {
return ::testing::AssertionFailure() << "Failed to encode commit pack";
}
Status status = Status::INTERNAL_ERROR;
if ((*page_cloud)->AddCommits(std::move(commit_pack), &status) != ZX_OK) {
return ::testing::AssertionFailure() << "Failed to add commits due to a channel error.";
}
if (status != Status::OK) {
return ::testing::AssertionFailure()
<< "Failed to add commits, received status: " << fidl::ToUnderlying(status);
}
return ::testing::AssertionSuccess();
}
int on_new_commits_calls_ = 0;
std::vector<cloud_provider::Commit> on_new_commits_commits_;
cloud_provider::PositionToken on_new_commits_position_token_;
OnNewCommitsCallback on_new_commits_commits_callback_;
cloud_provider::Status on_error_status_ = cloud_provider::Status::OK;
private:
// PageCloudWatcher:
void OnNewCommits(CommitPack commits, cloud_provider::PositionToken position_token,
OnNewCommitsCallback callback) override {
cloud_provider::Commits commit_container;
ASSERT_TRUE(ledger::DecodeFromBuffer(commits.buffer, &commit_container));
on_new_commits_calls_++;
std::move(commit_container.commits.begin(), commit_container.commits.end(),
std::back_inserter(on_new_commits_commits_));
on_new_commits_position_token_ = std::move(position_token);
on_new_commits_commits_callback_ = std::move(callback);
}
void OnNewObject(std::vector<uint8_t> /*id*/, fuchsia::mem::Buffer /*data*/,
OnNewObjectCallback /*callback*/) override {
// We don't have any implementations yet that support this API.
// TODO(ppi): add tests for the OnNewObject notifications.
LEDGER_NOTIMPLEMENTED();
}
void OnError(cloud_provider::Status status) override { on_error_status_ = status; }
};
TEST_F(PageCloudTest, GetPageCloud) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
}
TEST_F(PageCloudTest, GetNoCommits) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
std::unique_ptr<cloud_provider::CommitPack> commit_pack;
std::unique_ptr<PositionToken> token;
Status status = Status::INTERNAL_ERROR;
ASSERT_EQ(page_cloud->GetCommits(nullptr, &status, &commit_pack, &token), ZX_OK);
EXPECT_EQ(status, Status::OK);
ASSERT_TRUE(commit_pack);
Commits entries;
ASSERT_TRUE(ledger::DecodeFromBuffer(commit_pack->buffer, &entries));
EXPECT_THAT(entries.commits, IsEmpty());
EXPECT_FALSE(token);
}
TEST_F(PageCloudTest, AddAndGetCommits) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
ASSERT_TRUE(AddCommits(&page_cloud, {{"id0", "data0"}, {"id1", "data1"}}));
Status status;
std::unique_ptr<CommitPack> result;
std::unique_ptr<PositionToken> token;
ASSERT_EQ(page_cloud->GetCommits(nullptr, &status, &result, &token), ZX_OK);
EXPECT_EQ(status, Status::OK);
ASSERT_TRUE(result);
Commits commits;
ASSERT_TRUE(ledger::DecodeFromBuffer(result->buffer, &commits));
EXPECT_EQ(commits.commits.size(), 2u);
EXPECT_TRUE(CheckThatCommitsContain(commits.commits, "id0", "data0"));
EXPECT_TRUE(CheckThatCommitsContain(commits.commits, "id1", "data1"));
EXPECT_TRUE(token);
}
TEST_F(PageCloudTest, GetCommitsByPositionToken) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
// Add two commits.
ASSERT_TRUE(AddCommits(&page_cloud, {{"id0", "data0"}, {"id1", "data1"}}));
// Retrieve the position token of the newest of the two (`id1`).
std::unique_ptr<cloud_provider::PositionToken> token;
ASSERT_TRUE(GetLatestPositionToken(&page_cloud, &token));
EXPECT_TRUE(token);
// Add one more commit.
ASSERT_TRUE(AddCommits(&page_cloud, {{"id2", "data2"}}));
// Retrieve the commits again with the position token of `id1`.
Status status;
std::unique_ptr<CommitPack> result;
ASSERT_EQ(page_cloud->GetCommits(std::move(token), &status, &result, &token), ZX_OK);
EXPECT_EQ(status, Status::OK);
ASSERT_TRUE(result);
Commits commits;
ASSERT_TRUE(ledger::DecodeFromBuffer(result->buffer, &commits));
// As per the API contract, the response must include `id2` and everything
// newer than it. It may or may not include `id0` and `id1`.
EXPECT_TRUE(CheckThatCommitsContain(commits.commits, "id2", "data2"));
}
TEST_F(PageCloudTest, AddAndGetObjects) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
ledger::SizedVmo data;
ASSERT_TRUE(ledger::VmoFromString("bazinga!", &data));
Status status = Status::INTERNAL_ERROR;
// Generate a random ID - the current cloud provider implementations don't
// erase storage objects upon .Erase(), and we want to avoid interference from
// previous test runs.
// TODO(ppi): use a fixed ID here once the cloud provider implementations
// support erasing objects.
const std::string id = convert::ToString(GetUniqueRandomId());
ASSERT_EQ(page_cloud->AddObject(convert::ToArray(id), std::move(data).ToTransport(), {}, &status),
ZX_OK);
EXPECT_EQ(status, Status::OK);
::fuchsia::mem::BufferPtr buffer_ptr;
ASSERT_EQ(page_cloud->GetObject(convert::ToArray(id), &status, &buffer_ptr), ZX_OK);
EXPECT_EQ(status, Status::OK);
std::string read_data;
ASSERT_TRUE(ledger::StringFromVmo(*buffer_ptr, &read_data));
EXPECT_EQ(read_data, "bazinga!");
}
TEST_F(PageCloudTest, AddSameObjectTwice) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
ledger::SizedVmo data;
ASSERT_TRUE(ledger::VmoFromString("bazinga!", &data));
Status status = Status::INTERNAL_ERROR;
const std::string id = "some id";
ASSERT_EQ(page_cloud->AddObject(convert::ToArray(id), std::move(data).ToTransport(), {}, &status),
ZX_OK);
EXPECT_EQ(status, Status::OK);
// Adding the same object again must succeed as per cloud provider contract.
ledger::SizedVmo more_data;
ASSERT_TRUE(ledger::VmoFromString("bazinga!", &more_data));
ASSERT_EQ(
page_cloud->AddObject(convert::ToArray(id), std::move(more_data).ToTransport(), {}, &status),
ZX_OK);
EXPECT_EQ(status, Status::OK);
}
TEST_F(PageCloudTest, WatchAndReceiveCommits) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
Status status = Status::INTERNAL_ERROR;
fidl::Binding<PageCloudWatcher> binding(this);
PageCloudWatcherPtr watcher;
binding.Bind(watcher.NewRequest());
ASSERT_EQ(page_cloud->SetWatcher(nullptr, std::move(watcher), &status), ZX_OK);
EXPECT_EQ(status, Status::OK);
// Add two commits.
ASSERT_TRUE(AddCommits(&page_cloud, {{"id0", "data0"}, {"id1", "data1"}}));
// The two commits could be delivered in one or two notifications. If they are
// delivered over two notifications, the second one can only be delivered
// after the client confirms having processed the first one by calling the
// notification callback.
while (on_new_commits_commits_.size() < 2u) {
ASSERT_EQ(binding.WaitForMessage(), ZX_OK);
on_new_commits_commits_callback_();
}
EXPECT_TRUE(CheckThatCommitsContain(on_new_commits_commits_, "id0", "data0"));
EXPECT_TRUE(CheckThatCommitsContain(on_new_commits_commits_, "id1", "data1"));
}
// Verifies that the pre-existing commits are also delivered when a watcher is
// registered.
TEST_F(PageCloudTest, WatchWithBacklog) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
Status status = Status::INTERNAL_ERROR;
// Add two commits.
ASSERT_TRUE(AddCommits(&page_cloud, {{"id0", "data0"}, {"id1", "data1"}}));
fidl::Binding<PageCloudWatcher> binding(this);
PageCloudWatcherPtr watcher;
binding.Bind(watcher.NewRequest());
ASSERT_EQ(page_cloud->SetWatcher(nullptr, std::move(watcher), &status), ZX_OK);
EXPECT_EQ(status, Status::OK);
while (on_new_commits_commits_.size() < 2u) {
ASSERT_EQ(binding.WaitForMessage(), ZX_OK);
on_new_commits_commits_callback_();
}
EXPECT_TRUE(CheckThatCommitsContain(on_new_commits_commits_, "id0", "data0"));
EXPECT_TRUE(CheckThatCommitsContain(on_new_commits_commits_, "id1", "data1"));
}
TEST_F(PageCloudTest, WatchWithPositionToken) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
// Add two commits.
ASSERT_TRUE(AddCommits(&page_cloud, {{"id0", "data0"}, {"id1", "data1"}}));
// Retrieve the position token of the newest of the two (`id1`).
std::unique_ptr<cloud_provider::PositionToken> token;
ASSERT_TRUE(GetLatestPositionToken(&page_cloud, &token));
EXPECT_TRUE(token);
// Set the watcher with the position token of `id1`.
fidl::Binding<PageCloudWatcher> binding(this);
PageCloudWatcherPtr watcher;
Status status;
binding.Bind(watcher.NewRequest());
ASSERT_EQ(page_cloud->SetWatcher(std::move(token), std::move(watcher), &status), ZX_OK);
EXPECT_EQ(status, Status::OK);
// Add one more commit.
ASSERT_TRUE(AddCommits(&page_cloud, {{"id2", "data2"}}));
while (!CheckThatCommitsContain(on_new_commits_commits_, "id2", "data2")) {
ASSERT_EQ(binding.WaitForMessage(), ZX_OK);
on_new_commits_commits_callback_();
}
// Add one more commit.
ASSERT_TRUE(AddCommits(&page_cloud, {{"id3", "data3"}}));
while (!CheckThatCommitsContain(on_new_commits_commits_, "id3", "data3")) {
ASSERT_EQ(binding.WaitForMessage(), ZX_OK);
on_new_commits_commits_callback_();
}
}
TEST_F(PageCloudTest, WatchWithPositionTokenBatch) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
// Add two commits.
ASSERT_TRUE(AddCommits(&page_cloud, {{"id0", "data0"}, {"id1", "data1"}}));
// Retrieve the position token of the newest of the two (`id1`).
std::unique_ptr<cloud_provider::PositionToken> token;
ASSERT_TRUE(GetLatestPositionToken(&page_cloud, &token));
EXPECT_TRUE(token);
// Set the watcher with the position token of `id1`.
fidl::Binding<PageCloudWatcher> binding(this);
PageCloudWatcherPtr watcher;
Status status;
binding.Bind(watcher.NewRequest());
ASSERT_EQ(page_cloud->SetWatcher(std::move(token), std::move(watcher), &status), ZX_OK);
EXPECT_EQ(status, Status::OK);
// Add two commits at once.
ASSERT_TRUE(AddCommits(&page_cloud, {{"id2", "data2"}, {"id3", "data3"}}));
while (!CheckThatCommitsContain(on_new_commits_commits_, "id2", "data2")) {
ASSERT_EQ(binding.WaitForMessage(), ZX_OK);
on_new_commits_commits_callback_();
}
// The two commits must be delivered at the same time.
EXPECT_TRUE(CheckThatCommitsContain(on_new_commits_commits_, "id3", "data3"));
}
TEST_F(PageCloudTest, Diff_GetDiffFromEmpty) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
// Add one commit, with a diff from the empty commit.
Diff diff;
diff.mutable_base_state()->set_empty_page({});
diff.mutable_changes()->push_back(std::move(DiffEntry()
.set_entry_id(convert::ToArray("entryA"))
.set_operation(Operation::INSERTION)
.set_data(convert::ToArray("entryA_data"))));
Commit commit;
*commit.mutable_id() = convert::ToArray("id0");
*commit.mutable_data() = convert::ToArray("data0");
*commit.mutable_diff() = std::move(diff);
std::vector<Commit> entries;
entries.push_back(std::move(commit));
CommitPack commit_pack;
Commits commits{std::move(entries)};
ASSERT_TRUE(ledger::EncodeToBuffer(&commits, &commit_pack.buffer));
Status status = Status::INTERNAL_ERROR;
ASSERT_EQ(page_cloud->AddCommits(std::move(commit_pack), &status), ZX_OK);
EXPECT_EQ(status, Status::OK);
// The cloud can only give a diff from the empty page.
ASSERT_TRUE(GetAndDecodeDiff(&page_cloud, convert::ToArray("id0"), {}, &diff));
ASSERT_TRUE(diff.has_base_state());
EXPECT_TRUE(diff.base_state().is_empty_page());
ASSERT_TRUE(diff.has_changes());
ASSERT_EQ(diff.changes().size(), 1u);
EXPECT_THAT(diff.changes()[0],
AllOf(TABLE_FIELD_MATCHES(DiffEntry, entry_id, convert::ToArray("entryA")),
TABLE_FIELD_MATCHES(DiffEntry, operation, Operation::INSERTION),
TABLE_FIELD_MATCHES(DiffEntry, data, convert::ToArray("entryA_data"))));
}
TEST_F(PageCloudTest, Diff_GetMultipleDiff) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
// Add one commit, with a diff from the empty commit.
Diff diff;
diff.mutable_base_state()->set_empty_page({});
diff.mutable_changes()->push_back(std::move(DiffEntry()
.set_entry_id(convert::ToArray("entryA"))
.set_operation(Operation::INSERTION)
.set_data(convert::ToArray("entryA_data"))));
Commit commit;
*commit.mutable_id() = convert::ToArray("id0");
*commit.mutable_data() = convert::ToArray("data0");
*commit.mutable_diff() = std::move(diff);
std::vector<Commit> entries;
entries.push_back(std::move(commit));
// Add a second commit deleting the entry inserted in the last commit.
diff = Diff{};
diff.mutable_base_state()->set_at_commit(convert::ToArray("id0"));
diff.mutable_changes()->push_back(std::move(DiffEntry()
.set_entry_id(convert::ToArray("entryA"))
.set_operation(Operation::DELETION)
.set_data(convert::ToArray("entryA_data2"))));
commit = {};
*commit.mutable_id() = convert::ToArray("id1");
*commit.mutable_data() = convert::ToArray("data1");
*commit.mutable_diff() = std::move(diff);
entries.push_back(std::move(commit));
// Upload both commits.
CommitPack commit_pack;
Commits commits{std::move(entries)};
ASSERT_TRUE(ledger::EncodeToBuffer(&commits, &commit_pack.buffer));
Status status = Status::INTERNAL_ERROR;
ASSERT_EQ(page_cloud->AddCommits(std::move(commit_pack), &status), ZX_OK);
EXPECT_EQ(status, Status::OK);
// Read the second commit. The cloud gives a diff from the empty page.
ASSERT_TRUE(GetAndDecodeDiff(&page_cloud, convert::ToArray("id1"), {}, &diff));
// The cloud can only give a diff from the empty page.
ASSERT_TRUE(diff.has_base_state());
EXPECT_TRUE(diff.base_state().is_empty_page());
// The diff is either empty, or the addition then the deletion.
ASSERT_TRUE(diff.has_changes());
auto entry_matcher = AllOf(TABLE_FIELD_MATCHES(DiffEntry, entry_id, convert::ToArray("entryA")),
TABLE_FIELD_MATCHES(DiffEntry, data,
AnyOf(convert::ToArray("entryA_data"),
convert::ToArray("entryA_data2"))));
auto two_changes_matcher = ElementsAre(
AllOf(entry_matcher, TABLE_FIELD_MATCHES(DiffEntry, operation, Operation::INSERTION)),
AllOf(entry_matcher, TABLE_FIELD_MATCHES(DiffEntry, operation, Operation::DELETION)));
EXPECT_THAT(diff, TABLE_FIELD_MATCHES(Diff, changes, AnyOf(IsEmpty(), two_changes_matcher)));
}
TEST_F(PageCloudTest, DiffCompat_GetNoDiff) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
// Add one commit without diff.
Commit commit;
*commit.mutable_id() = convert::ToArray("id0");
*commit.mutable_data() = convert::ToArray("data0");
std::vector<Commit> entries;
entries.push_back(std::move(commit));
CommitPack commit_pack;
Commits commits{std::move(entries)};
ASSERT_TRUE(ledger::EncodeToBuffer(&commits, &commit_pack.buffer));
Status status = Status::INTERNAL_ERROR;
ASSERT_EQ(page_cloud->AddCommits(std::move(commit_pack), &status), ZX_OK);
EXPECT_EQ(status, Status::OK);
// Request a diff for `id0'.
Diff diff;
ASSERT_TRUE(GetAndDecodeDiff(&page_cloud, convert::ToArray("id0"), {}, &diff));
// The cloud can only give a diff from the commit itself.
ASSERT_TRUE(diff.has_base_state());
EXPECT_TRUE(diff.base_state().is_at_commit());
EXPECT_EQ(diff.base_state().at_commit(), convert::ToArray("id0"));
// The diff is empty.
ASSERT_TRUE(diff.has_changes());
EXPECT_EQ(diff.changes().size(), 0u);
}
TEST_F(PageCloudTest, DiffCompat_GetDiffFromNoDiff) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
// Add one commit without diff.
Commit commit;
*commit.mutable_id() = convert::ToArray("id0");
*commit.mutable_data() = convert::ToArray("data0");
std::vector<Commit> entries;
entries.push_back(std::move(commit));
// Add one commit with diff based on this commit.
Diff diff;
diff.mutable_base_state()->set_at_commit(convert::ToArray("id0"));
diff.mutable_changes()->push_back(std::move(DiffEntry()
.set_entry_id(convert::ToArray("entryA"))
.set_operation(Operation::DELETION)
.set_data(convert::ToArray("entryA_data"))));
commit = {};
*commit.mutable_id() = convert::ToArray("id1");
*commit.mutable_data() = convert::ToArray("data1");
*commit.mutable_diff() = std::move(diff);
entries.push_back(std::move(commit));
// Upload both commits.
CommitPack commit_pack;
Commits commits{std::move(entries)};
ASSERT_TRUE(ledger::EncodeToBuffer(&commits, &commit_pack.buffer));
Status status = Status::INTERNAL_ERROR;
ASSERT_EQ(page_cloud->AddCommits(std::move(commit_pack), &status), ZX_OK);
EXPECT_EQ(status, Status::OK);
// Ask for a diff for `id1' with an empty base list.
ASSERT_TRUE(GetAndDecodeDiff(&page_cloud, convert::ToArray("id1"), {}, &diff));
// The cloud can only give a diff from `id0'.
ASSERT_TRUE(diff.has_base_state());
EXPECT_TRUE(diff.base_state().is_at_commit());
EXPECT_EQ(diff.base_state().at_commit(), convert::ToArray("id0"));
// The diff must contain only the deletion.
ASSERT_TRUE(diff.has_changes());
ASSERT_EQ(diff.changes().size(), 1u);
EXPECT_THAT(diff.changes()[0],
AllOf(TABLE_FIELD_MATCHES(DiffEntry, entry_id, convert::ToArray("entryA")),
TABLE_FIELD_MATCHES(DiffEntry, operation, Operation::DELETION),
TABLE_FIELD_MATCHES(DiffEntry, data, convert::ToArray("entryA_data"))));
}
TEST_F(PageCloudTest, Diff_GetDiffIntermediateCommit) {
PageCloudSyncPtr page_cloud;
ASSERT_TRUE(GetPageCloud(convert::ToArray("app_id"), GetUniqueRandomId(), &page_cloud));
// Add one commit, with a diff from the empty commit.
Diff diff;
diff.mutable_base_state()->set_empty_page({});
diff.mutable_changes()->push_back(std::move(DiffEntry()
.set_entry_id(convert::ToArray("entryA"))
.set_operation(Operation::INSERTION)
.set_data(convert::ToArray("entryA_data"))));
Commit commit;
*commit.mutable_id() = convert::ToArray("id0");
*commit.mutable_data() = convert::ToArray("data0");
*commit.mutable_diff() = std::move(diff);
std::vector<Commit> entries;
entries.push_back(std::move(commit));
// Add a second commit deleting the entry inserted in the last commit.
diff = Diff{};
diff.mutable_base_state()->set_at_commit(convert::ToArray("id0"));
diff.mutable_changes()->push_back(std::move(DiffEntry()
.set_entry_id(convert::ToArray("entryA"))
.set_operation(Operation::DELETION)
.set_data(convert::ToArray("entryA_data2"))));
commit = {};
*commit.mutable_id() = convert::ToArray("id1");
*commit.mutable_data() = convert::ToArray("data1");
*commit.mutable_diff() = std::move(diff);
entries.push_back(std::move(commit));
// Upload both commits.
CommitPack commit_pack;
Commits commits{std::move(entries)};
ASSERT_TRUE(ledger::EncodeToBuffer(&commits, &commit_pack.buffer));
Status status = Status::INTERNAL_ERROR;
ASSERT_EQ(page_cloud->AddCommits(std::move(commit_pack), &status), ZX_OK);
EXPECT_EQ(status, Status::OK);
// Read the first commit.
ASSERT_TRUE(
GetAndDecodeDiff(&page_cloud, convert::ToArray("id0"), {convert::ToArray("id1")}, &diff));
// The cloud may either give a diff from the empty page or from `id1'.
ASSERT_TRUE(diff.has_base_state());
ASSERT_TRUE(diff.has_changes());
ASSERT_EQ(diff.changes().size(), 1u);
auto& change = diff.changes()[0];
EXPECT_THAT(change, AllOf(TABLE_FIELD_MATCHES(DiffEntry, entry_id, convert::ToArray("entryA")),
TABLE_FIELD_MATCHES(DiffEntry, data,
AnyOf(convert::ToArray("entryA_data"),
convert::ToArray("entryA_data2")))));
ASSERT_TRUE(change.has_operation());
EXPECT_TRUE((diff.base_state().is_empty_page() && change.operation() == Operation::INSERTION) ||
(diff.base_state().at_commit() == convert::ToArray("id1") &&
change.operation() == Operation::INSERTION));
}
} // namespace
} // namespace cloud_provider