--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
+// vim: ts=8 sw=2 sts=2 expandtab
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025 IBM
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ */
+
+#include "ECOmapJournal.h"
+
+#include <utility>
+
+ECOmapJournalEntry::ECOmapJournalEntry(
+ const eversion_t version, const bool clear_omap, std::optional<ceph::buffer::list> omap_header,
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> omap_updates)
+ : version(version), clear_omap(clear_omap),
+ omap_header(std::move(omap_header)), omap_updates(std::move(omap_updates)) {}
+
+void ECOmapValue::update_value(const eversion_t new_version, std::optional<ceph::buffer::list> new_value) {
+ this->version = new_version;
+ this->value = std::move(new_value);
+}
+
+void ECOmapRemovedRanges::add_range(const std::string& start, const std::optional<std::string>& end) {
+ std::string new_start = start;
+ std::optional<std::string> new_end = end;
+ auto it = ranges.begin();
+ bool inserted = false;
+ while (it != ranges.end()) {
+ // Current range is to the left of new range
+ if (it->second && *it->second < new_start) {
+ ++it;
+ continue;
+ }
+ // Current range is to the right of new range
+ if (new_end && *new_end < it->first) {
+ ranges.insert(it, {new_start, new_end});
+ inserted = true;
+ break;
+ }
+ // Ranges overlap, merge them
+ if (it->first < new_start) {
+ new_start = it->first;
+ }
+ if (!it->second) {
+ new_end = std::nullopt;
+ } else if (new_end && *it->second > *new_end) {
+ new_end = it->second;
+ }
+ it = ranges.erase(it);
+ }
+ if (!inserted) {
+ ranges.emplace_back(new_start, new_end);
+ }
+}
+
+void ECOmapRemovedRanges::clear_omap() {
+ ranges.clear();
+ ranges.emplace_back("", std::nullopt);
+}
+
+void ECOmapHeader::update_header(const eversion_t new_version,
+ std::optional<ceph::buffer::list> new_header) {
+ this->version = new_version;
+ this->header = std::move(new_header);
+}
+
+
+void ECOmapJournal::add_entry(const hobject_t &hoid, const ECOmapJournalEntry &entry) {
+ ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
+ << " version=" << entry.version
+ << " clear_omap=" << entry.clear_omap
+ << " header_size=" << (entry.omap_header ? entry.omap_header->length() : 0)
+ << dendl;
+ entries[hoid].push_back(entry);
+}
+
+bool ECOmapJournal::remove_entry(const hobject_t &hoid, const ECOmapJournalEntry &entry) {
+ // Attempt to remove entry from unprocessed entries
+ if (const auto it_map = entries.find(hoid);
+ it_map != entries.end()) {
+ auto &entry_list = it_map->second;
+ for (auto it = entry_list.begin(); it != entry_list.end(); ++it) {
+ if (it->version == entry.version) {
+ ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
+ << " version=" << entry.version << " found_unprocessed=true" << dendl;
+ entry_list.erase(it);
+ if (const auto header_it = header_map.find(hoid);
+ header_it != header_map.end() &&
+ header_it->second.version == entry.version) {
+ header_map.erase(header_it);
+ }
+ return true;
+ }
+ }
+ }
+ ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
+ << " version=" << entry.version << " found_unprocessed=false" << dendl;
+
+ // Attempt to remove entry from processed entries
+ return remove_processed_entry(hoid, entry);
+}
+
+bool ECOmapJournal::remove_entry_by_version(const hobject_t &hoid, const eversion_t version) {
+ // Attempt to remove entry from unprocessed entries
+ if (const auto it_map = entries.find(hoid);
+ it_map != entries.end()) {
+ auto &entry_list = it_map->second;
+ for (auto it = entry_list.begin(); it != entry_list.end(); ++it) {
+ if (it->version == version) {
+ entry_list.erase(it);
+ if (const auto header_it = header_map.find(hoid);
+ header_it != header_map.end() &&
+ header_it->second.version == version) {
+ header_map.erase(header_it);
+ }
+ return true;
+ }
+ }
+ }
+
+ // Attempt to remove entry from processed entries
+ return remove_processed_entry_by_version(hoid, version);
+}
+
+void ECOmapJournal::clear(const hobject_t &hoid) {
+ entries.erase(hoid);
+ key_map.erase(hoid);
+ removed_ranges_map.erase(hoid);
+ header_map.erase(hoid);
+ object_state_map.erase(hoid);
+}
+
+void ECOmapJournal::clear_all() {
+ entries.clear();
+ key_map.clear();
+ removed_ranges_map.clear();
+ header_map.clear();
+ object_state_map.clear();
+}
+
+std::size_t ECOmapJournal::entries_size(const hobject_t &hoid) const {
+ if (const auto entries_it = entries.find(hoid);
+ entries_it != entries.end()) {
+ return entries_it->second.size();
+ }
+ return 0u;
+}
+
+bool ECOmapJournal::has_unprocessed_entries(const hobject_t &hoid) const {
+ return entries.contains(hoid);
+}
+
+bool ECOmapJournal::has_omap_updates(const hobject_t &hoid) const {
+ // Check unprocessed entries
+ if (const auto entries_it = entries.find(hoid);
+ entries_it != entries.end() && !entries_it->second.empty()) {
+ return true;
+ }
+
+ // Check processed key map
+ if (const auto key_it = key_map.find(hoid);
+ key_it != key_map.end() && !key_it->second.empty()) {
+ return true;
+ }
+
+ // Check removed ranges
+ if (const auto ranges_it = removed_ranges_map.find(hoid);
+ ranges_it != removed_ranges_map.end() && !ranges_it->second.empty()) {
+ return true;
+ }
+
+ // Check header updates
+ if (header_map.find(hoid) != header_map.end()) {
+ return true;
+ }
+
+ return false;
+}
+
+// Function to get specific object's entries, if not present, creates an empty list
+std::list<ECOmapJournalEntry>& ECOmapJournal::get_entries(const hobject_t &hoid) {
+ return entries[hoid];
+}
+
+std::list<ECOmapJournalEntry> ECOmapJournal::snapshot_entries(const hobject_t &hoid) const {
+ if (const auto it = entries.find(hoid);
+ it != entries.end()) {
+ return it->second;
+ }
+ return {};
+}
+
+ECOmapJournal::const_iterator ECOmapJournal::begin_entries(const hobject_t &hoid) const {
+ return entries.at(hoid).begin();
+}
+
+ECOmapJournal::const_iterator ECOmapJournal::end_entries(const hobject_t &hoid) const {
+ return entries.at(hoid).end();
+}
+
+std::optional<ceph::buffer::list> ECOmapJournal::get_updated_header(const hobject_t &hoid) {
+ process_entries(hoid);
+ if (!header_map.contains(hoid)) {
+ return std::nullopt;
+ }
+ return header_map[hoid].header;
+}
+
+std::tuple<ECOmapJournal::UpdateMapType, ECOmapJournal::RangeMapType>
+ECOmapJournal::get_value_updates(const hobject_t &hoid) {
+ process_entries(hoid);
+ return {get_key_map(hoid), get_removed_ranges(hoid)};
+}
+
+void ECOmapJournal::process_entries(const hobject_t &hoid) {
+ auto entry_list = get_entries(hoid);
+ ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
+ << " processing " << entry_list.size() << " entries" << dendl;
+ if (!has_unprocessed_entries(hoid)) {
+ return;
+ }
+
+ for (auto entry_iter = begin_entries(hoid);
+ entry_iter != end_entries(hoid); ++entry_iter) {
+ ECOmapRemovedRanges removed_ranges(entry_iter->version);
+
+ // Clear omap if specified
+ if (entry_iter->clear_omap) {
+ // Mark all keys as removed
+ for (auto [_, value] : key_map[hoid]) {
+ value.update_value(entry_iter->version, std::nullopt);
+ }
+
+ // Mark entire range as removed
+ removed_ranges.clear_omap();
+
+ if (!entry_iter->omap_header) {
+ // Set the header to an empty bufferlist
+ bufferlist bl;
+ if (!header_map.contains(hoid)) {
+ header_map[hoid] = ECOmapHeader(entry_iter->version, bl);
+ } else {
+ header_map[hoid].update_header(entry_iter->version, bl);
+ }
+ }
+ }
+
+ // Update header if present
+ if (entry_iter->omap_header) {
+ if (!header_map.contains(hoid)) {
+ header_map[hoid] = ECOmapHeader(entry_iter->version, entry_iter->omap_header);
+ } else {
+ header_map[hoid].update_header(entry_iter->version, entry_iter->omap_header);
+ }
+ }
+
+ // Process key updates
+ auto &obj_map = key_map[hoid];
+ for (const auto & [type, update] : entry_iter->omap_updates) {
+ auto iter = update.cbegin();
+ switch (type) {
+ case OmapUpdateType::Insert: {
+ std::map<std::string, ceph::buffer::list> vals;
+ decode(vals, iter);
+ // Insert key value pairs into update_map
+ for (auto it = vals.begin(); it != vals.end(); ++it) {
+ const auto &key = it->first;
+ const auto &val = it->second;
+
+ // Check if key already exists in key map
+ auto entry_it = obj_map.find(key);
+ if (entry_it != obj_map.end()) {
+ // Update existing value
+ entry_it->second.update_value(entry_iter->version, val);
+ } else {
+ // Insert new value
+ obj_map.emplace(key, ECOmapValue(entry_iter->version, val));
+ }
+ }
+ break;
+ }
+ case OmapUpdateType::Remove: {
+ std::set<std::string> keys;
+ decode(keys, iter);
+ // Mark keys in key_map as removed
+ for (const auto &key : keys) {
+ // Check if key already exists in key map
+ if (auto entry_it = obj_map.find(key);
+ entry_it != obj_map.end()) {
+ // Update existing value to null
+ entry_it->second.update_value(entry_iter->version, std::nullopt);
+ } else {
+ // Insert new null value
+ obj_map.emplace(key, ECOmapValue(entry_iter->version, std::nullopt));
+ }
+ }
+ break;
+ }
+ case OmapUpdateType::RemoveRange: {
+ std::string key_begin, key_end;
+ decode(key_begin, iter);
+ decode(key_end, iter);
+
+ // Add removed range
+ std::string start = key_begin;
+ std::optional<std::string> end = key_end;
+ removed_ranges.add_range(start, end);
+
+ // Mark keys in key_map as removed that fall within the removed range
+ auto map_it = obj_map.lower_bound(key_begin);
+ while (map_it != obj_map.end()) {
+ if (map_it->first >= key_end) {
+ break;
+ }
+ map_it->second.update_value(entry_iter->version, std::nullopt);
+ ++map_it;
+ }
+ break;
+ }
+ default:
+ ceph_abort_msg("Unknown OmapUpdateType");
+ }
+ }
+ if (!removed_ranges.ranges.empty()) {
+ removed_ranges_map[hoid].emplace_back(removed_ranges);
+ }
+ }
+ entries.erase(hoid);
+}
+
+bool ECOmapJournal::remove_processed_entry(const hobject_t &hoid, const ECOmapJournalEntry &entry) {
+ // Remove the header if version matches
+ if (const auto header_it = header_map.find(hoid);
+ header_it != header_map.end() && header_it->second.version == entry.version) {
+ header_map.erase(header_it);
+ }
+
+ // Remove key updates if version matches
+ auto &obj_map = key_map[hoid];
+ for (const auto & [type, update] : entry.omap_updates) {
+ auto iter = update.cbegin();
+ switch (type) {
+ case OmapUpdateType::Insert: {
+ std::map<std::string, ceph::buffer::list> vals;
+ decode(vals, iter);
+ for (auto val_it = vals.begin(); val_it != vals.end(); ++val_it) {
+ const auto &key = val_it->first;
+ auto key_it = obj_map.find(key);
+ if (key_it != obj_map.end() &&
+ key_it->second.version == entry.version) {
+ obj_map.erase(key);
+ }
+ }
+ break;
+ }
+ case OmapUpdateType::Remove: {
+ std::set<std::string> keys;
+ decode(keys, iter);
+ for (const auto &key : keys) {
+ if (auto it = obj_map.find(key);
+ it != obj_map.end() &&
+ it->second.version == entry.version) {
+ obj_map.erase(key);
+ }
+ }
+ break;
+ }
+ case OmapUpdateType::RemoveRange: {
+ std::string key_begin, key_end;
+ decode(key_begin, iter);
+ decode(key_end, iter);
+ auto map_it = obj_map.lower_bound(key_begin);
+ while (map_it != obj_map.end()) {
+ if (map_it->first >= key_end) {
+ break;
+ }
+ if (map_it->second.version == entry.version) {
+ map_it = obj_map.erase(map_it);
+ } else {
+ ++map_it;
+ }
+ }
+ break;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ // Remove removed ranges if version matches
+ if (const auto removed_ranges_it = removed_ranges_map.find(hoid);
+ removed_ranges_it != removed_ranges_map.end()) {
+ auto &removed_ranges_list = removed_ranges_it->second;
+ for (auto rr_it = removed_ranges_list.begin(); rr_it != removed_ranges_list.end(); ++rr_it) {
+ if (rr_it->version == entry.version) {
+ removed_ranges_list.erase(rr_it);
+ break;
+ }
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+bool ECOmapJournal::remove_processed_entry_by_version(const hobject_t &hoid, const eversion_t version) {
+ // Remove the header if version matches
+ if (const auto header_it = header_map.find(hoid);
+ header_it != header_map.end() && header_it->second.version == version) {
+ header_map.erase(header_it);
+ }
+
+ // Remove key updates if version matches
+ auto key_map_it = key_map.find(hoid);
+ if (key_map_it != key_map.end()) {
+ for (auto it = key_map_it->second.begin(); it != key_map_it->second.end(); ) {
+ if (it->second.version == version) {
+ it = key_map_it->second.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ }
+
+ // Remove removed ranges if version matches
+ auto removed_ranges_it = removed_ranges_map.find(hoid);
+ if (removed_ranges_it != removed_ranges_map.end()) {
+ auto &removed_ranges_list = removed_ranges_it->second;
+ for (auto rr_it = removed_ranges_list.begin(); rr_it != removed_ranges_list.end(); ++rr_it) {
+ if (rr_it->version == version) {
+ removed_ranges_list.erase(rr_it);
+ break;
+ }
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+ECOmapJournal::UpdateMapType ECOmapJournal::get_key_map(const hobject_t &hoid) const {
+ if (const auto it = key_map.find(hoid); it != key_map.end()) {
+ return it->second;
+ }
+ return {};
+}
+
+ECOmapJournal::RangeMapType ECOmapJournal::get_removed_ranges(const hobject_t &hoid) const {
+ // Merge all removed ranges for the object
+ RangeMapType merged_ranges;
+ if (const auto it = removed_ranges_map.find(hoid);
+ it != removed_ranges_map.end()) {
+ for (const auto &rr : it->second) {
+ for (const auto & [range_first, range_second] : rr.ranges) {
+ // Add range to merged_ranges, merging overlapping ranges
+ std::string start = range_first;
+ std::optional<std::string> end = range_second;
+
+ // Find the range that starts after the current start
+ auto map_it = merged_ranges.upper_bound(start);
+ if (map_it != merged_ranges.begin()) {
+ // Merge range to the left, if they overlap
+ if (const auto prev = std::prev(map_it);
+ !prev->second || *prev->second >= start) {
+ start = prev->first;
+ if (!end) {
+ // end is already open ended so cannot be extended
+ } else if (!prev->second) {
+ end = std::nullopt;
+ } else if (*prev->second > *end) {
+ end = *prev->second;
+ }
+ merged_ranges.erase(prev);
+ }
+ }
+ // Merge ranges to the right, if they overlap
+ while (map_it != merged_ranges.end() &&
+ (!end || map_it->first <= *end)) {
+ if (!end) {
+ // end is already open ended so cannot be extended
+ } else if (!map_it->second) {
+ end = std::nullopt;
+ } else if (*map_it->second > *end) {
+ end = map_it->second;
+ }
+ map_it = merged_ranges.erase(map_it);
+ }
+ merged_ranges.emplace_hint(map_it, start, end);
+ }
+ }
+ }
+ return merged_ranges;
+}
+
+void ECOmapJournal::append_delete(
+ const hobject_t &hoid,
+ const version_t version,
+ const bool lost_delete) {
+ entries.erase(hoid);
+ key_map.erase(hoid);
+ removed_ranges_map.erase(hoid);
+ header_map.erase(hoid);
+
+ auto [it, inserted] = object_state_map.try_emplace(hoid, std::map<version_t, bool>{});
+ it->second.insert({version, lost_delete});
+
+ size_t total_versions = it->second.size();
+
+ ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
+ << " version=" << version
+ << " whiteout=" << lost_delete
+ << " total_versions=" << total_versions
+ << dendl;
+}
+
+void ECOmapJournal::append_create(const hobject_t &hoid) {
+ entries.erase(hoid);
+ key_map.erase(hoid);
+ removed_ranges_map.erase(hoid);
+ header_map.erase(hoid);
+}
+
+void ECOmapJournal::append_whiteout(const hobject_t &hoid) {
+ entries.erase(hoid);
+ key_map.erase(hoid);
+ removed_ranges_map.erase(hoid);
+ header_map.erase(hoid);
+}
+
+void ECOmapJournal::trim_delete(const hobject_t &hoid, const version_t version) {
+ // Capture whiteout value before erasing
+ bool whiteout = false;
+ if (const auto it = object_state_map.find(hoid); it != object_state_map.end()) {
+ if (const auto it2 = it->second.find(version); it2 != it->second.end()) {
+ whiteout = it2->second;
+ }
+ }
+
+ if (const auto it = object_state_map.find(hoid); it != object_state_map.end()) {
+ std::map<version_t,bool>& versions = it->second;
+ if (const auto it2 = versions.find(version); it2 != versions.end()) {
+ versions.erase(it2);
+ }
+ if (versions.empty()) {
+ object_state_map.erase(it);
+ }
+ }
+
+ // Get the total number of versions for this object after the operation
+ size_t total_versions = 0;
+ if (const auto it = object_state_map.find(hoid); it != object_state_map.end()) {
+ total_versions = it->second.size();
+ }
+
+ ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
+ << " version=" << version
+ << " whiteout=" << whiteout
+ << " total_versions=" << total_versions
+ << dendl;
+}
+
+std::pair<gen_t, bool> ECOmapJournal::get_generation(const hobject_t &hoid) const {
+ if (const auto it = object_state_map.find(hoid); it != object_state_map.end()) {
+ if (const auto& versions = it->second; !versions.empty()) {
+ const auto& [gen, lost] = *versions.begin();
+ return {gen, lost};
+ }
+ }
+ return {static_cast<gen_t>(ghobject_t::NO_GEN), false};
+}
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
+// vim: ts=8 sw=2 sts=2 expandtab
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2026 IBM
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+ */
+
+#include <gtest/gtest.h>
+#include "test/unit.cc"
+
+#include "osd/ECOmapJournal.h"
+#include "common/dout.h"
+
+class MockDoutPrefixProvider : public DoutPrefixProvider {
+public:
+ std::ostream& gen_prefix(std::ostream& out) const override {
+ return out << "test: ";
+ }
+ CephContext *get_cct() const override {
+ return g_ceph_context;
+ }
+ unsigned get_subsys() const override {
+ return ceph_subsys_osd;
+ }
+};
+
+TEST(ecomapjournalentry, attributes_retained)
+{
+ eversion_t version(1, 1);
+ bool clear_omap = true;
+ ceph::buffer::list omap_val_bl, omap_header_bl, omap_map_bl, keys_to_remove_bl;
+ const std::string omap_header = "this is the header";
+ encode(omap_header, omap_header_bl);
+ const std::string omap_key_1 = "omap_key_1_palomino";
+ const std::string omap_key_2 = "omap_key_2_chestnut";
+ const std::string omap_key_3 = "omap_key_3_bay";
+ const std::string omap_value = "omap_value_1_horse";
+ encode(omap_value, omap_val_bl);
+ std::map<std::string, bufferlist> omap_map = {
+ {omap_key_1.c_str(), omap_val_bl},
+ {omap_key_2.c_str(), omap_val_bl},
+ {omap_key_3.c_str(), omap_val_bl}
+ };
+ encode(omap_map, omap_map_bl);
+ std::set<std::string> keys_to_remove = {omap_key_2};
+ encode(keys_to_remove, keys_to_remove_bl);
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> omap_updates = {
+ {OmapUpdateType::Insert, omap_map_bl},
+ {OmapUpdateType::Remove, keys_to_remove_bl}
+ };
+
+ ECOmapJournalEntry entry(version, clear_omap, omap_header_bl, omap_updates);
+
+ // Attributes should be retained correctly
+ ASSERT_TRUE(entry.version == version);
+ ASSERT_TRUE(entry.clear_omap == clear_omap);
+ ASSERT_TRUE(*entry.omap_header == omap_header_bl);
+ ASSERT_TRUE(entry.omap_updates == omap_updates);
+}
+
+TEST(ecomapjournal, new_journal_starts_empty)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // A new journal should start empty
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+}
+
+TEST(ecomapjournal, add_entry)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key2", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+
+ // The journal should contain the added entry
+ ASSERT_EQ(1u, journal.entries_size(test_hoid));
+ ASSERT_TRUE(journal.begin_entries(test_hoid)->version == entry1.version);
+}
+
+TEST(ecomapjournal, remove_entry)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key3", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ ECOmapJournalEntry entry3(eversion_t(1, 3), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+ journal.add_entry(test_hoid, entry3);
+ bool res = journal.remove_entry(test_hoid, entry1);
+ ASSERT_TRUE(res);
+
+ // The journal should have 2 entries in it after removal
+ ASSERT_EQ(2u, journal.entries_size(test_hoid));
+ ASSERT_TRUE(journal.begin_entries(test_hoid)->version == entry2.version);
+ ASSERT_TRUE((++journal.begin_entries(test_hoid))->version == entry3.version);
+}
+
+TEST(ecomapjournal, remove_entry_by_version)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key4", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ ECOmapJournalEntry entry3(eversion_t(1, 3), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+ journal.add_entry(test_hoid, entry3);
+ bool res = journal.remove_entry_by_version(test_hoid, entry2.version);
+ ASSERT_TRUE(res);
+
+ // The journal should have 2 entries in it after removal
+ ASSERT_EQ(2u, journal.entries_size(test_hoid));
+ ASSERT_TRUE(journal.begin_entries(test_hoid)->version == entry1.version);
+ ASSERT_TRUE((++journal.begin_entries(test_hoid))->version == entry3.version);
+}
+
+TEST(ecomapjournal, clear_one_journal)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key5", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+ journal.clear(test_hoid);
+
+ // The journal should be empty after clearing
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+}
+
+TEST(ecomapjournal, clear_all_journals)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid1("test_key6", CEPH_NOSNAP, 1, 0, "test_namespace");
+ const hobject_t test_hoid2("test_key7", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ journal.add_entry(test_hoid1, entry1);
+ journal.add_entry(test_hoid2, entry2);
+ journal.clear_all();
+
+ // Both journals should be empty after clearing all
+ ASSERT_EQ(0u, journal.entries_size(test_hoid1));
+ ASSERT_EQ(0u, journal.entries_size(test_hoid2));
+}
+
+TEST(ecomapjournal, remove_bad_entry)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key6", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+
+ // Attempting to remove an entry not in the journal should fail
+ bool res = journal.remove_entry(test_hoid, entry2);
+ ASSERT_FALSE(res);
+
+ // The journal should still have 1 entry in it after failed removal
+ ASSERT_EQ(1u, journal.entries_size(test_hoid));
+ ASSERT_TRUE(journal.begin_entries(test_hoid)->version == entry1.version);
+}
+
+TEST(ecomapjournal, remove_bad_entry_by_version)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key7", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+
+ // Attempting to remove an entry not in the journal should fail
+ bool res = journal.remove_entry_by_version(test_hoid, entry2.version);
+ ASSERT_FALSE(res);
+
+ // The journal should still have 1 entry in it after failed removal
+ ASSERT_EQ(1u, journal.entries_size(test_hoid));
+ ASSERT_TRUE(journal.begin_entries(test_hoid)->version == entry1.version);
+}
+
+TEST(ecomapjournal, get_value_updates_no_updates)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key7", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // The journal should return empty updates for an object with no entries
+ auto [update_map, removed_ranges] = journal.get_value_updates(test_hoid);
+ ASSERT_TRUE(update_map.empty());
+ ASSERT_TRUE(removed_ranges.empty());
+}
+
+TEST(ecomapjournal, get_value_updates_multiple_updates)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key7", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ std::map<std::string, ceph::buffer::list> key_value;
+ ceph::buffer::list val_bl;
+ encode("my_value", val_bl);
+ key_value["key_1"] = val_bl;
+ ceph::buffer::list map_bl;
+ encode(key_value, map_bl);
+
+ std::string range_begin = "key_1";
+ std::string range_end = "key_2";
+
+ ceph::buffer::list range_bl;
+ encode(range_begin, range_bl);
+ encode(range_end, range_bl);
+
+ ECOmapJournalEntry entry1(
+ eversion_t(1, 1), false, std::nullopt,
+ {std::pair(OmapUpdateType::RemoveRange, range_bl)}
+ );
+ ECOmapJournalEntry entry2(
+ eversion_t(1, 2), true, std::nullopt,
+ {std::pair(OmapUpdateType::Insert, map_bl)}
+ );
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+
+ // The journal should return the combined updates for the object
+ auto [update_map, removed_ranges] = journal.get_value_updates(test_hoid);
+
+ ASSERT_TRUE(!update_map.empty());
+ ASSERT_TRUE(!removed_ranges.empty());
+
+ // Removed ranges should contain the clear from entry2
+ ASSERT_TRUE(removed_ranges.contains(""));
+ ASSERT_TRUE(!removed_ranges[""].has_value());
+
+ // Update map should contain key1 inserted in entry2
+ auto it = update_map.find("key_1");
+ ASSERT_TRUE(it != update_map.end());
+ ASSERT_TRUE(it->second.value.has_value());
+}
+
+TEST(ecomapjournal, get_updated_header_no_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key7", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // The journal should return no updated header for an object with no entries
+ std::optional<ceph::buffer::list> header = journal.get_updated_header(test_hoid);
+ ASSERT_TRUE(!header.has_value());
+}
+
+TEST(ecomapjournal, get_updated_header_single_entry)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key7", CEPH_NOSNAP, 1, 0, "test_namespace");
+ const std::string omap_header_str = "this is the header";
+ ceph::buffer::list omap_header_bl;
+ encode(omap_header_str, omap_header_bl);
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, omap_header_bl, {});
+ journal.add_entry(test_hoid, entry1);
+
+ // The journal should return the updated header for the object
+ std::optional<ceph::buffer::list> header = journal.get_updated_header(test_hoid);
+ ASSERT_TRUE(header.has_value());
+ ASSERT_TRUE(*header == omap_header_bl);
+}
+
+TEST(ecomapjournal, get_updated_header_multiple_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_key7", CEPH_NOSNAP, 1, 0, "test_namespace");
+ const std::string omap_header_str_1 = "this is the header";
+ const std::string omap_header_str_2 = "this is the new header";
+ ceph::buffer::list omap_header_bl_1, omap_header_bl_2;
+ encode(omap_header_str_1, omap_header_bl_1);
+ encode(omap_header_str_2, omap_header_bl_2);
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, omap_header_bl_1, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, omap_header_bl_2, {});
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+
+ // The journal should return the second updated header for the object
+ std::optional<ceph::buffer::list> header = journal.get_updated_header(test_hoid);
+ ASSERT_TRUE(header.has_value());
+ ASSERT_TRUE(*header == omap_header_bl_2);
+}
+
+std::string make_key(int i) {
+ std::stringstream ss;
+ ss << "key_" << std::setw(3) << std::setfill('0') << i;
+ return ss.str();
+}
+
+// Encodes a map of keys for OmapUpdateType::Insert
+ceph::buffer::list encode_map(const std::map<std::string, ceph::buffer::list>& keys) {
+ ceph::buffer::list bl;
+ encode(keys, bl);
+ return bl;
+}
+
+// Encodes start/end strings for OmapUpdateType::RemoveRange
+ceph::buffer::list encode_range(const std::string& start, const std::string& end) {
+ ceph::buffer::list bl;
+ encode(start, bl);
+ encode(end, bl);
+ return bl;
+}
+
+// Create an insert entry
+ECOmapJournalEntry create_insert_entry(const eversion_t v, const int start_key, const int end_key) {
+ std::map<std::string, ceph::buffer::list> key_map;
+ for (int i = start_key; i <= end_key; ++i) {
+ ceph::buffer::list val_bl;
+ val_bl.append("val");
+ key_map[make_key(i)] = val_bl;
+ }
+
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> updates;
+ updates.push_back({OmapUpdateType::Insert, encode_map(key_map)});
+
+ return ECOmapJournalEntry(v, false, std::nullopt, updates);
+}
+
+TEST(ecomapjournal, RemoveOneRange) {
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid("obj_rm_range", CEPH_NOSNAP, 1, 0, "nspace");
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 1), 1, 100));
+
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> rm_updates;
+ rm_updates.push_back({
+ OmapUpdateType::RemoveRange,
+ encode_range(make_key(20), make_key(41))
+ });
+ journal.add_entry(hoid, ECOmapJournalEntry(eversion_t(1, 2), false, std::nullopt, rm_updates));
+
+ auto [updates, ranges] = journal.get_value_updates(hoid);
+ ASSERT_EQ(1u, ranges.size());
+ auto it = ranges.begin();
+ EXPECT_EQ(make_key(20), it->first);
+ ASSERT_TRUE(it->second.has_value());
+ EXPECT_EQ(make_key(41), *it->second);
+}
+
+TEST(ecomapjournal, RemoveTwoRanges) {
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid("obj_rm_two_ranges", CEPH_NOSNAP, 1, 0, "nspace");
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 1), 1, 100));
+
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> rm_updates;
+ rm_updates.push_back({OmapUpdateType::RemoveRange, encode_range(make_key(20), make_key(41))});
+ rm_updates.push_back({OmapUpdateType::RemoveRange, encode_range(make_key(60), make_key(81))});
+ journal.add_entry(hoid, ECOmapJournalEntry(eversion_t(1, 2), false, std::nullopt, rm_updates));
+
+ auto [updates, ranges] = journal.get_value_updates(hoid);
+ ASSERT_EQ(2u, ranges.size());
+ auto it = ranges.begin();
+ EXPECT_EQ(make_key(20), it->first);
+ EXPECT_EQ(make_key(41), *it->second);
+ ++it;
+ EXPECT_EQ(make_key(60), it->first);
+ EXPECT_EQ(make_key(81), *it->second);
+}
+
+TEST(ecomapjournal, OverlappingRanges) {
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid("obj_overlap", CEPH_NOSNAP, 1, 0, "nspace");
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 1), 1, 100));
+
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> rm_updates;
+ rm_updates.push_back({OmapUpdateType::RemoveRange, encode_range(make_key(10), make_key(21))});
+ rm_updates.push_back({OmapUpdateType::RemoveRange, encode_range(make_key(15), make_key(31))});
+ journal.add_entry(hoid, ECOmapJournalEntry(eversion_t(1, 2), false, std::nullopt, rm_updates));
+
+ auto [updates, ranges] = journal.get_value_updates(hoid);
+ ASSERT_EQ(1u, ranges.size());
+ EXPECT_EQ(make_key(10), ranges.begin()->first);
+ EXPECT_EQ(make_key(31), *ranges.begin()->second);
+}
+
+TEST(ecomapjournal, RemoveWithNullStart) {
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid("obj_null_start", CEPH_NOSNAP, 1, 0, "nspace");
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 1), 1, 100));
+
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> rm_updates;
+ rm_updates.push_back({OmapUpdateType::RemoveRange, encode_range("", make_key(21))});
+ journal.add_entry(hoid, ECOmapJournalEntry(eversion_t(1, 2), false, std::nullopt, rm_updates));
+
+ auto [updates, ranges] = journal.get_value_updates(hoid);
+ ASSERT_EQ(1u, ranges.size());
+ EXPECT_EQ("", ranges.begin()->first);
+ EXPECT_EQ(make_key(21), *ranges.begin()->second);
+}
+
+TEST(ecomapjournal, RemoveRangeThenInsert) {
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid("obj_rm_ins", CEPH_NOSNAP, 1, 0, "nspace");
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 1), 1, 100));
+
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> ops;
+ ops.push_back({OmapUpdateType::RemoveRange, encode_range(make_key(30), make_key(61))});
+ journal.add_entry(hoid, ECOmapJournalEntry(eversion_t(1, 2), false, std::nullopt, ops));
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 3), 45, 45));
+
+ auto [updates, ranges] = journal.get_value_updates(hoid);
+ ASSERT_EQ(1u, ranges.size());
+ EXPECT_EQ(make_key(30), ranges.begin()->first);
+ EXPECT_EQ(make_key(61), *ranges.begin()->second);
+ ASSERT_TRUE(updates.count(make_key(45)));
+ EXPECT_TRUE(updates.at(make_key(45)).value.has_value());
+}
+
+TEST(ecomapjournal, RemoveInsertRemove) {
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid("obj_rm_ins_rm", CEPH_NOSNAP, 1, 0, "nspace");
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 1), 1, 100));
+
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> ops1;
+ ops1.push_back({OmapUpdateType::RemoveRange, encode_range(make_key(30), make_key(61))});
+ journal.add_entry(hoid, ECOmapJournalEntry(eversion_t(1, 2), false, std::nullopt, ops1));
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 3), 45, 45));
+
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> ops2;
+ ops2.push_back({OmapUpdateType::RemoveRange, encode_range(make_key(30), make_key(61))});
+ journal.add_entry(hoid, ECOmapJournalEntry(eversion_t(1, 4), false, std::nullopt, ops2));
+
+ auto [updates, ranges] = journal.get_value_updates(hoid);
+ ASSERT_EQ(1u, ranges.size());
+ EXPECT_EQ(make_key(30), ranges.begin()->first);
+ EXPECT_EQ(make_key(61), *ranges.begin()->second);
+ ASSERT_TRUE(updates.count(make_key(45)));
+ EXPECT_FALSE(updates.at(make_key(45)).value.has_value());
+}
+
+TEST(ecomapjournal, AdjacentRanges) {
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid("obj_adjacent", CEPH_NOSNAP, 1, 0, "nspace");
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 1), 1, 100));
+
+ std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> rm_updates;
+ rm_updates.push_back({OmapUpdateType::RemoveRange, encode_range(make_key(10), make_key(20))});
+ rm_updates.push_back({OmapUpdateType::RemoveRange, encode_range(make_key(20), make_key(30))});
+ journal.add_entry(hoid, ECOmapJournalEntry(eversion_t(1, 2), false, std::nullopt, rm_updates));
+
+ auto [updates, ranges] = journal.get_value_updates(hoid);
+ EXPECT_EQ(make_key(10), ranges.begin()->first);
+ EXPECT_EQ(make_key(30), *ranges.begin()->second);
+}
+
+TEST(ecomapjournal, ClearOmap) {
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid("obj_clear", CEPH_NOSNAP, 1, 0, "nspace");
+
+ ceph::buffer::list header_bl;
+ header_bl.append("header_v1");
+ auto entry_ins = create_insert_entry(eversion_t(1, 1), 1, 100);
+ entry_ins.omap_header = header_bl;
+ journal.add_entry(hoid, entry_ins);
+
+ ECOmapJournalEntry entry_clear(eversion_t(1, 2), true, std::nullopt, {});
+ journal.add_entry(hoid, entry_clear);
+
+ journal.add_entry(hoid, create_insert_entry(eversion_t(1, 3), 50, 50));
+
+ auto [updates, ranges] = journal.get_value_updates(hoid);
+ auto header = journal.get_updated_header(hoid);
+ ASSERT_TRUE(updates.contains(make_key(50)));
+ EXPECT_TRUE(updates.at(make_key(50)).value.has_value());
+ ASSERT_TRUE(header.has_value());
+ EXPECT_EQ(0u, header->length());
+ ASSERT_EQ(1u, ranges.size());
+ EXPECT_TRUE(ranges.contains(""));
+ EXPECT_TRUE(ranges.at("") == std::nullopt);
+}
+
+TEST(ecomapjournal, append_delete_clears_maps)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_delete", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Add some journal entries
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+
+ ASSERT_EQ(2u, journal.entries_size(test_hoid));
+
+ // Delete the object
+ journal.append_delete(test_hoid, 5, false);
+
+ // All entries should be cleared
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+}
+
+TEST(ecomapjournal, append_delete_adds_to_object_state_map)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_delete2", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Delete the object at version 5
+ journal.append_delete(test_hoid, 5, false);
+
+ // Generation should be the delete version
+ auto [gen, lost] = journal.get_generation(test_hoid);
+ ASSERT_EQ(5u, gen);
+ ASSERT_FALSE(lost);
+}
+
+TEST(ecomapjournal, append_delete_multiple_versions)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_delete3", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Delete the object at multiple versions
+ journal.append_delete(test_hoid, 5, false);
+ journal.append_delete(test_hoid, 8, false);
+ journal.append_delete(test_hoid, 3, false);
+
+ // Generation should be the lowest delete version
+ auto [gen, lost] = journal.get_generation(test_hoid);
+ ASSERT_EQ(3u, gen);
+ ASSERT_FALSE(lost);
+}
+
+TEST(ecomapjournal, append_create_clears_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_create", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Add some journal entries
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+
+ ASSERT_EQ(2u, journal.entries_size(test_hoid));
+
+ // Create the object
+ journal.append_create(test_hoid);
+
+ // All entries should be cleared
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+}
+
+TEST(ecomapjournal, trim_delete_removes_version)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_trim_delete", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Delete the object at multiple versions
+ journal.append_delete(test_hoid, 5, false);
+ journal.append_delete(test_hoid, 8, false);
+ journal.append_delete(test_hoid, 3, false);
+
+ auto [gen1, lost1] = journal.get_generation(test_hoid);
+ ASSERT_EQ(3u, gen1);
+ ASSERT_FALSE(lost1);
+
+ // Trim the lowest version
+ journal.trim_delete(test_hoid, 3);
+
+ // Generation should now be the next lowest version
+ auto [gen2, lost2] = journal.get_generation(test_hoid);
+ ASSERT_EQ(5u, gen2);
+ ASSERT_FALSE(lost2);
+}
+
+TEST(ecomapjournal, trim_delete_last_version)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_trim_last", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Delete the object at one version
+ journal.append_delete(test_hoid, 5, false);
+
+ // Trim the only version
+ journal.trim_delete(test_hoid, 5);
+
+ // Generation should be NO_GEN
+ auto [gen, lost] = journal.get_generation(test_hoid);
+ ASSERT_EQ(UINT64_MAX, gen);
+ ASSERT_FALSE(lost);
+}
+
+TEST(ecomapjournal, get_generation_no_deletes)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_no_delete", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Generation should be NO_GEN
+ auto [gen, lost] = journal.get_generation(test_hoid);
+ ASSERT_EQ(UINT64_MAX, gen);
+ ASSERT_FALSE(lost);
+}
+
+TEST(ecomapjournal, append_delete_with_lost_flag)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_lost", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Delete with lost_delete = true
+ journal.append_delete(test_hoid, 5, true);
+ auto [gen1, lost1] = journal.get_generation(test_hoid);
+ ASSERT_EQ(5u, gen1);
+ ASSERT_TRUE(lost1);
+
+ // Delete with lost_delete = false
+ journal.append_delete(test_hoid, 8, false);
+ auto [gen2, lost2] = journal.get_generation(test_hoid);
+ ASSERT_EQ(5u, gen2);
+ ASSERT_TRUE(lost2);
+}
+
+TEST(ecomapjournal, delete_then_create_workflow)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_delete_create", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Add entries
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 1), 1, 10));
+ ASSERT_EQ(1u, journal.entries_size(test_hoid));
+
+ // Delete object
+ journal.append_delete(test_hoid, 5, false);
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+
+ // Add more entries after delete
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 6), 20, 30));
+ ASSERT_EQ(1u, journal.entries_size(test_hoid));
+
+ // Create object (simulating recreation)
+ journal.append_create(test_hoid);
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+
+ // Object should still have the delete version tracked
+ auto [gen, lost] = journal.get_generation(test_hoid);
+ ASSERT_EQ(5u, gen);
+ ASSERT_FALSE(lost);
+}
+
+TEST(ecomapjournal, multiple_objects_state_tracking)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid1("obj1", CEPH_NOSNAP, 1, 0, "ns");
+ const hobject_t hoid2("obj2", CEPH_NOSNAP, 1, 0, "ns");
+ const hobject_t hoid3("obj3", CEPH_NOSNAP, 1, 0, "ns");
+
+ // Delete different objects at different versions
+ journal.append_delete(hoid1, 10, false);
+ journal.append_delete(hoid2, 5, false);
+ journal.append_delete(hoid3, 15, false);
+
+ auto [gen1, lost1] = journal.get_generation(hoid1);
+ ASSERT_EQ(10u, gen1);
+ ASSERT_FALSE(lost1);
+ auto [gen2, lost2] = journal.get_generation(hoid2);
+ ASSERT_EQ(5u, gen2);
+ ASSERT_FALSE(lost2);
+ auto [gen3, lost3] = journal.get_generation(hoid3);
+ ASSERT_EQ(15u, gen3);
+ ASSERT_FALSE(lost3);
+
+ // Trim one object's delete
+ journal.trim_delete(hoid2, 5);
+
+ // Only hoid2 should have NO_GEN
+ auto [gen1b, lost1b] = journal.get_generation(hoid1);
+ ASSERT_EQ(10u, gen1b);
+ ASSERT_FALSE(lost1b);
+ auto [gen2b, lost2b] = journal.get_generation(hoid2);
+ ASSERT_EQ(UINT64_MAX, gen2b);
+ ASSERT_FALSE(lost2b);
+ auto [gen3b, lost3b] = journal.get_generation(hoid3);
+ ASSERT_EQ(15u, gen3b);
+ ASSERT_FALSE(lost3b);
+}
+
+TEST(ecomapjournal, append_delete_clears_processed_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_processed", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Add entries and process them
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 1), 1, 10));
+ auto [updates, ranges] = journal.get_value_updates(test_hoid);
+
+ // Verify entries were processed (entries_size should be 0 after processing)
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+ ASSERT_FALSE(updates.empty());
+
+ // Delete the object - should clear processed entries too
+ journal.append_delete(test_hoid, 5, false);
+
+ // Get updates again - should be empty
+ auto [updates2, ranges2] = journal.get_value_updates(test_hoid);
+ ASSERT_TRUE(updates2.empty());
+ ASSERT_TRUE(ranges2.empty());
+}
+
+TEST(ecomapjournal, generation_with_whiteout)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_whiteout", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Simulate whiteout by deleting
+ journal.append_delete(test_hoid, 10, false);
+
+ auto [gen1, lost1] = journal.get_generation(test_hoid);
+ ASSERT_EQ(10u, gen1);
+ ASSERT_FALSE(lost1);
+
+ // Add another delete (could represent another whiteout)
+ journal.append_delete(test_hoid, 12, false);
+
+ // Generation should still be the lowest
+ auto [gen2, lost2] = journal.get_generation(test_hoid);
+ ASSERT_EQ(10u, gen2);
+ ASSERT_FALSE(lost2);
+}
+
+TEST(ecomapjournal, append_whiteout_clears_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_whiteout_entries", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Add some journal entries
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+
+ ASSERT_EQ(2u, journal.entries_size(test_hoid));
+
+ // Whiteout the object
+ journal.append_whiteout(test_hoid);
+
+ // All entries should be cleared
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+}
+
+TEST(ecomapjournal, append_whiteout_clears_processed_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_whiteout_processed", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Add entries and process them
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 1), 1, 10));
+ auto [updates, ranges] = journal.get_value_updates(test_hoid);
+
+ // Verify entries were processed
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+ ASSERT_FALSE(updates.empty());
+
+ // Whiteout the object - should clear processed entries too
+ journal.append_whiteout(test_hoid);
+
+ // Get updates again - should be empty
+ auto [updates2, ranges2] = journal.get_value_updates(test_hoid);
+ ASSERT_TRUE(updates2.empty());
+ ASSERT_TRUE(ranges2.empty());
+}
+
+TEST(ecomapjournal, append_whiteout_clears_header)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_whiteout_header", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ const std::string omap_header_str = "test header";
+ ceph::buffer::list omap_header_bl;
+ encode(omap_header_str, omap_header_bl);
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, omap_header_bl, {});
+ journal.add_entry(test_hoid, entry1);
+
+ // Verify header exists
+ std::optional<ceph::buffer::list> header = journal.get_updated_header(test_hoid);
+ ASSERT_TRUE(header.has_value());
+
+ // Whiteout the object
+ journal.append_whiteout(test_hoid);
+
+ // Header should be cleared
+ std::optional<ceph::buffer::list> header2 = journal.get_updated_header(test_hoid);
+ ASSERT_FALSE(header2.has_value());
+}
+
+TEST(ecomapjournal, append_whiteout_on_nonexistent_object)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_whiteout_nonexist", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Whiteout on non-existent object should not crash
+ journal.append_whiteout(test_hoid);
+
+ // Should still be empty
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+}
+
+TEST(ecomapjournal, append_whiteout_followed_by_new_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_whiteout_new", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Add entries
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 1), 1, 10));
+ ASSERT_EQ(1u, journal.entries_size(test_hoid));
+
+ // Whiteout
+ journal.append_whiteout(test_hoid);
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+
+ // Add new entries after whiteout
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 2), 20, 30));
+ ASSERT_EQ(1u, journal.entries_size(test_hoid));
+
+ // New entries should be present
+ auto [updates, ranges] = journal.get_value_updates(test_hoid);
+ ASSERT_FALSE(updates.empty());
+ ASSERT_TRUE(updates.contains(make_key(20)));
+}
+
+TEST(ecomapjournal, append_whiteout_does_not_affect_generation)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_whiteout_gen", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Delete object to set generation
+ journal.append_delete(test_hoid, 5, false);
+ auto [gen1, lost1] = journal.get_generation(test_hoid);
+ ASSERT_EQ(5u, gen1);
+
+ // Add entries
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 6), 1, 10));
+
+ // Whiteout should clear entries but not generation
+ journal.append_whiteout(test_hoid);
+
+ auto [gen2, lost2] = journal.get_generation(test_hoid);
+ ASSERT_EQ(5u, gen2);
+ ASSERT_FALSE(lost2);
+}
+
+TEST(ecomapjournal, whiteout_create_delete_workflow)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_whiteout_workflow", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Add entries
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 1), 1, 10));
+ ASSERT_EQ(1u, journal.entries_size(test_hoid));
+
+ // Whiteout
+ journal.append_whiteout(test_hoid);
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+
+ // Create
+ journal.append_create(test_hoid);
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+
+ // Add more entries
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 2), 20, 30));
+ ASSERT_EQ(1u, journal.entries_size(test_hoid));
+
+ // Delete
+ journal.append_delete(test_hoid, 10, false);
+ ASSERT_EQ(0u, journal.entries_size(test_hoid));
+
+ auto [gen, lost] = journal.get_generation(test_hoid);
+ ASSERT_EQ(10u, gen);
+}
+
+TEST(ecomapvalue, update_value_with_data)
+{
+ eversion_t v1(1, 1);
+ ceph::buffer::list bl1;
+ bl1.append("value1");
+
+ ECOmapValue value(v1, bl1);
+ ASSERT_EQ(v1, value.version);
+ ASSERT_TRUE(value.value.has_value());
+
+ eversion_t v2(1, 2);
+ ceph::buffer::list bl2;
+ bl2.append("value2");
+
+ value.update_value(v2, bl2);
+ ASSERT_EQ(v2, value.version);
+ ASSERT_TRUE(value.value.has_value());
+ ASSERT_EQ(bl2, *value.value);
+}
+
+TEST(ecomapvalue, update_value_to_nullopt)
+{
+ eversion_t v1(1, 1);
+ ceph::buffer::list bl1;
+ bl1.append("value1");
+
+ ECOmapValue value(v1, bl1);
+ ASSERT_TRUE(value.value.has_value());
+
+ eversion_t v2(1, 2);
+ value.update_value(v2, std::nullopt);
+
+ ASSERT_EQ(v2, value.version);
+ ASSERT_FALSE(value.value.has_value());
+}
+
+TEST(ecomapvalue, multiple_updates)
+{
+ eversion_t v1(1, 1);
+ ECOmapValue value(v1, std::nullopt);
+
+ for (int i = 2; i <= 5; ++i) {
+ eversion_t v(1, i);
+ ceph::buffer::list bl;
+ bl.append(std::to_string(i));
+ value.update_value(v, bl);
+ ASSERT_EQ(v, value.version);
+ }
+
+ ASSERT_EQ(eversion_t(1, 5), value.version);
+}
+
+TEST(ecomapremovedranges, add_range_non_overlapping)
+{
+ ECOmapRemovedRanges ranges(eversion_t(1, 1));
+
+ ranges.add_range("key_010", "key_020");
+ ranges.add_range("key_030", "key_040");
+
+ ASSERT_EQ(2u, ranges.ranges.size());
+}
+
+TEST(ecomapremovedranges, add_range_overlapping)
+{
+ ECOmapRemovedRanges ranges(eversion_t(1, 1));
+
+ ranges.add_range("key_010", "key_030");
+ ranges.add_range("key_020", "key_040");
+
+ // Should merge into single range
+ ASSERT_EQ(1u, ranges.ranges.size());
+ EXPECT_EQ("key_010", ranges.ranges.front().first);
+ EXPECT_EQ("key_040", *ranges.ranges.front().second);
+}
+
+TEST(ecomapremovedranges, add_range_adjacent)
+{
+ ECOmapRemovedRanges ranges(eversion_t(1, 1));
+
+ ranges.add_range("key_010", "key_020");
+ ranges.add_range("key_020", "key_030");
+
+ // Should merge into single range
+ ASSERT_EQ(1u, ranges.ranges.size());
+ EXPECT_EQ("key_010", ranges.ranges.front().first);
+ EXPECT_EQ("key_030", *ranges.ranges.front().second);
+}
+
+TEST(ecomapremovedranges, add_range_with_nullopt_end)
+{
+ ECOmapRemovedRanges ranges(eversion_t(1, 1));
+
+ ranges.add_range("key_010", "key_020");
+ ranges.add_range("key_015", std::nullopt);
+
+ // Should merge and extend to end
+ ASSERT_EQ(1u, ranges.ranges.size());
+ EXPECT_EQ("key_010", ranges.ranges.front().first);
+ EXPECT_FALSE(ranges.ranges.front().second.has_value());
+}
+
+TEST(ecomapremovedranges, clear_omap_test)
+{
+ ECOmapRemovedRanges ranges(eversion_t(1, 1));
+
+ ranges.add_range("key_010", "key_020");
+ ranges.add_range("key_030", "key_040");
+ ASSERT_EQ(2u, ranges.ranges.size());
+
+ ranges.clear_omap();
+
+ // Should replace all ranges with single range from "" to nullopt
+ ASSERT_EQ(1u, ranges.ranges.size());
+ EXPECT_EQ("", ranges.ranges.front().first);
+ EXPECT_FALSE(ranges.ranges.front().second.has_value());
+}
+
+TEST(ecomapheader, update_header_with_data)
+{
+ eversion_t v1(1, 1);
+ ceph::buffer::list bl1;
+ bl1.append("header1");
+
+ ECOmapHeader header(v1, bl1);
+ ASSERT_EQ(v1, header.version);
+ ASSERT_TRUE(header.header.has_value());
+
+ eversion_t v2(1, 2);
+ ceph::buffer::list bl2;
+ bl2.append("header2");
+
+ header.update_header(v2, bl2);
+ ASSERT_EQ(v2, header.version);
+ ASSERT_TRUE(header.header.has_value());
+ ASSERT_EQ(bl2, *header.header);
+}
+
+TEST(ecomapheader, update_header_to_nullopt)
+{
+ eversion_t v1(1, 1);
+ ceph::buffer::list bl1;
+ bl1.append("header1");
+
+ ECOmapHeader header(v1, bl1);
+ ASSERT_TRUE(header.header.has_value());
+
+ eversion_t v2(1, 2);
+ header.update_header(v2, std::nullopt);
+
+ ASSERT_EQ(v2, header.version);
+ ASSERT_FALSE(header.header.has_value());
+}
+
+TEST(ecomapheader, default_constructor)
+{
+ ECOmapHeader header;
+ ASSERT_EQ(eversion_t(), header.version);
+ ASSERT_FALSE(header.header.has_value());
+}
+
+TEST(ecomapjournal, remove_entry_on_empty_journal)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_empty", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry(eversion_t(1, 1), false, std::nullopt, {});
+ bool res = journal.remove_entry(test_hoid, entry);
+
+ ASSERT_FALSE(res);
+}
+
+TEST(ecomapjournal, remove_entry_by_version_on_empty_journal)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_empty2", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ bool res = journal.remove_entry_by_version(test_hoid, eversion_t(1, 1));
+
+ ASSERT_FALSE(res);
+}
+
+TEST(ecomapjournal, get_value_updates_after_clear)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_clear_updates", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ journal.add_entry(test_hoid, create_insert_entry(eversion_t(1, 1), 1, 10));
+ journal.clear(test_hoid);
+
+ auto [updates, ranges] = journal.get_value_updates(test_hoid);
+ ASSERT_TRUE(updates.empty());
+ ASSERT_TRUE(ranges.empty());
+}
+
+TEST(ecomapjournal, get_updated_header_after_clear)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_clear_header", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ceph::buffer::list header_bl;
+ header_bl.append("header");
+ ECOmapJournalEntry entry(eversion_t(1, 1), false, header_bl, {});
+ journal.add_entry(test_hoid, entry);
+
+ journal.clear(test_hoid);
+
+ std::optional<ceph::buffer::list> header = journal.get_updated_header(test_hoid);
+ ASSERT_FALSE(header.has_value());
+}
+
+TEST(ecomapjournal, entries_with_empty_omap_updates)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_empty_updates", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry(eversion_t(1, 1), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry);
+
+ auto [updates, ranges] = journal.get_value_updates(test_hoid);
+ ASSERT_TRUE(updates.empty());
+ ASSERT_TRUE(ranges.empty());
+}
+
+TEST(ecomapjournal, trim_delete_nonexistent_version)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_trim_nonexist", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ journal.append_delete(test_hoid, 5, false);
+
+ // Trim non-existent version should not crash
+ journal.trim_delete(test_hoid, 10);
+
+ auto [gen, lost] = journal.get_generation(test_hoid);
+ ASSERT_EQ(5u, gen);
+}
+
+TEST(ecomapjournal, get_generation_after_clear_all)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_gen_clear_all", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ journal.append_delete(test_hoid, 5, false);
+ journal.clear_all();
+
+ auto [gen, lost] = journal.get_generation(test_hoid);
+ ASSERT_EQ(UINT64_MAX, gen);
+ ASSERT_FALSE(lost);
+}
+
+TEST(ecomapjournal, multiple_objects_interleaved_operations)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t hoid1("obj1", CEPH_NOSNAP, 1, 0, "ns");
+ const hobject_t hoid2("obj2", CEPH_NOSNAP, 1, 0, "ns");
+ const hobject_t hoid3("obj3", CEPH_NOSNAP, 1, 0, "ns");
+
+ // Interleave operations on multiple objects
+ journal.add_entry(hoid1, create_insert_entry(eversion_t(1, 1), 1, 10));
+ journal.add_entry(hoid2, create_insert_entry(eversion_t(1, 1), 20, 30));
+ journal.append_delete(hoid3, 5, false);
+ journal.add_entry(hoid1, create_insert_entry(eversion_t(1, 2), 11, 20));
+ journal.append_whiteout(hoid2);
+
+ // Verify each object's state
+ ASSERT_EQ(2u, journal.entries_size(hoid1));
+ ASSERT_EQ(0u, journal.entries_size(hoid2));
+ ASSERT_EQ(0u, journal.entries_size(hoid3));
+
+ auto [gen3, lost3] = journal.get_generation(hoid3);
+ ASSERT_EQ(5u, gen3);
+}
+
+TEST(ecomapjournal, iterator_over_empty_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_iter_empty", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Check that hoid doesn't have unprocessed entries in the journal
+ ASSERT_FALSE(journal.has_unprocessed_entries(test_hoid));
+}
+
+TEST(ecomapjournal, iterator_over_single_entry)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_iter_single", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry(eversion_t(1, 1), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry);
+
+ auto begin = journal.begin_entries(test_hoid);
+ auto end = journal.end_entries(test_hoid);
+
+ ASSERT_NE(begin, end);
+ ASSERT_EQ(entry.version, begin->version);
+ ASSERT_EQ(end, ++begin);
+}
+
+TEST(ecomapjournal, iterator_over_multiple_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_iter_multiple", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+ ECOmapJournalEntry entry3(eversion_t(1, 3), false, std::nullopt, {});
+
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+ journal.add_entry(test_hoid, entry3);
+
+ int count = 0;
+ for (auto it = journal.begin_entries(test_hoid); it != journal.end_entries(test_hoid); ++it) {
+ count++;
+ }
+
+ ASSERT_EQ(3, count);
+}
+
+TEST(ecomapjournal, has_omap_updates_empty_journal)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_has_omap_1", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Empty journal should return false
+ ASSERT_FALSE(journal.has_omap_updates(test_hoid));
+}
+
+TEST(ecomapjournal, has_omap_updates_with_unprocessed_entries)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_has_omap_2", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+
+ // Journal with unprocessed entries should return true
+ ASSERT_TRUE(journal.has_omap_updates(test_hoid));
+}
+
+TEST(ecomapjournal, has_omap_updates_with_processed_key_map)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_has_omap_3", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Create an entry with key-value updates
+ std::map<std::string, ceph::buffer::list> key_value;
+ ceph::buffer::list val_bl;
+ encode("test_value", val_bl);
+ key_value["test_key"] = val_bl;
+ ceph::buffer::list map_bl;
+ encode(key_value, map_bl);
+
+ ECOmapJournalEntry entry1(
+ eversion_t(1, 1), false, std::nullopt,
+ {std::pair(OmapUpdateType::Insert, map_bl)}
+ );
+ journal.add_entry(test_hoid, entry1);
+
+ // Process entries by calling get_value_updates
+ auto [updates, ranges] = journal.get_value_updates(test_hoid);
+
+ // Journal with processed key map should return true
+ ASSERT_TRUE(journal.has_omap_updates(test_hoid));
+ ASSERT_FALSE(updates.empty());
+}
+
+TEST(ecomapjournal, has_omap_updates_with_removed_ranges)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_has_omap_4", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Create an entry with range removal
+ std::string range_begin = "key_a";
+ std::string range_end = "key_z";
+ ceph::buffer::list range_bl;
+ encode(range_begin, range_bl);
+ encode(range_end, range_bl);
+
+ ECOmapJournalEntry entry1(
+ eversion_t(1, 1), false, std::nullopt,
+ {std::pair(OmapUpdateType::RemoveRange, range_bl)}
+ );
+ journal.add_entry(test_hoid, entry1);
+
+ // Process entries by calling get_value_updates
+ auto [updates, ranges] = journal.get_value_updates(test_hoid);
+
+ // Journal with removed ranges should return true
+ ASSERT_TRUE(journal.has_omap_updates(test_hoid));
+ ASSERT_FALSE(ranges.empty());
+}
+
+TEST(ecomapjournal, has_omap_updates_with_header)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_has_omap_5", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ // Create an entry with header update
+ ceph::buffer::list header_bl;
+ encode("test_header", header_bl);
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, header_bl, {});
+ journal.add_entry(test_hoid, entry1);
+
+ // Process entries by calling get_updated_header
+ std::optional<ceph::buffer::list> header = journal.get_updated_header(test_hoid);
+
+ // Journal with header update should return true
+ ASSERT_TRUE(journal.has_omap_updates(test_hoid));
+ ASSERT_TRUE(header.has_value());
+}
+
+TEST(ecomapjournal, has_omap_updates_after_clear)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_has_omap_6", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ journal.add_entry(test_hoid, entry1);
+
+ // Should have updates before clear
+ ASSERT_TRUE(journal.has_omap_updates(test_hoid));
+
+ journal.clear(test_hoid);
+
+ // Should not have updates after clear
+ ASSERT_FALSE(journal.has_omap_updates(test_hoid));
+}
+
+TEST(ecomapjournal, has_omap_updates_different_objects)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid1("test_has_omap_7a", CEPH_NOSNAP, 1, 0, "test_namespace");
+ const hobject_t test_hoid2("test_has_omap_7b", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ journal.add_entry(test_hoid1, entry1);
+
+ // Object with entry should return true
+ ASSERT_TRUE(journal.has_omap_updates(test_hoid1));
+
+ // Different object without entry should return false
+ ASSERT_FALSE(journal.has_omap_updates(test_hoid2));
+}
+
+TEST(ecomapjournal, has_omap_updates_after_remove_entry)
+{
+ MockDoutPrefixProvider dpp;
+ ECOmapJournal journal(dpp);
+ const hobject_t test_hoid("test_has_omap_8", CEPH_NOSNAP, 1, 0, "test_namespace");
+
+ ECOmapJournalEntry entry1(eversion_t(1, 1), false, std::nullopt, {});
+ ECOmapJournalEntry entry2(eversion_t(1, 2), false, std::nullopt, {});
+
+ journal.add_entry(test_hoid, entry1);
+ journal.add_entry(test_hoid, entry2);
+
+ ASSERT_TRUE(journal.has_omap_updates(test_hoid));
+
+ // Remove one entry
+ journal.remove_entry(test_hoid, entry1);
+
+ // Should still have updates (entry2 remains)
+ ASSERT_TRUE(journal.has_omap_updates(test_hoid));
+
+ // Remove last entry
+ journal.remove_entry(test_hoid, entry2);
+
+ // Should not have updates after removing all entries
+ ASSERT_FALSE(journal.has_omap_updates(test_hoid));
+}
\ No newline at end of file