chore: Add more qlist tests (#4217)

* chore: Add more qlist tests

Also fix a typo bug in NodeAllowMerge.

Signed-off-by: Roman Gershman <roman@dragonflydb.io>

* chore: fix build

---------

Signed-off-by: Roman Gershman <roman@dragonflydb.io>
This commit is contained in:
Roman Gershman 2024-11-28 16:54:13 +02:00 committed by GitHub
parent dc04b196d5
commit c9654d4050
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 371 additions and 58 deletions

View file

@ -65,7 +65,7 @@ static_assert(sizeof(QList) == 32);
enum IterDir : uint8_t { FWD = 1, REV = 0 };
/* This is for test suite development purposes only, 0 means disabled. */
static size_t packed_threshold = 0;
size_t packed_threshold = 0;
/* Optimization levels for size-based filling.
* Note that the largest possible limit is 64k, so even if each record takes
@ -123,7 +123,9 @@ bool NodeAllowMerge(const quicklistNode* a, const quicklistNode* b, const int fi
/* approximate merged listpack size (- 7 to remove one listpack
* header/trailer, see LP_HDR_SIZE and LP_EOF) */
unsigned int merge_sz = a->sz + b->sz - 7;
return quicklistNodeExceedsLimit(fill, merge_sz, a->count + b->count);
// Allow merge if new node will not exceed the limit.
return !quicklistNodeExceedsLimit(fill, merge_sz, a->count + b->count);
}
quicklistNode* CreateNode() {
@ -140,6 +142,7 @@ quicklistNode* CreateNode() {
}
uint8_t* LP_Insert(uint8_t* lp, string_view elem, uint8_t* pos, int lp_where) {
DCHECK(pos);
return lpInsertString(lp, uint_ptr(elem), elem.size(), pos, lp_where, NULL);
}
@ -290,6 +293,10 @@ quicklistNode* SplitNode(quicklistNode* node, int offset, bool after) {
} // namespace
void QList::SetPackedThreshold(unsigned threshold) {
packed_threshold = threshold;
}
QList::QList() : fill_(-2), compress_(0), bookmark_count_(0) {
}
@ -529,28 +536,15 @@ void QList::InsertNode(quicklistNode* old_node, quicklistNode* new_node, InsertO
}
void QList::Insert(Iterator it, std::string_view elem, InsertOpt insert_opt) {
DCHECK(it.current_);
DCHECK(it.zi_);
int full = 0, at_tail = 0, at_head = 0, avail_next = 0, avail_prev = 0;
quicklistNode* node = it.current_;
quicklistNode* new_node = NULL;
size_t sz = elem.size();
bool after = insert_opt == AFTER;
if (!node) {
/* we have no reference node, so let's create only node in the list */
DCHECK_EQ(count_, 0u);
DCHECK_EQ(len_, 0u);
if (ABSL_PREDICT_FALSE(IsLargeElement(sz, fill_))) {
InsertPlainNode(tail_, elem, insert_opt);
return;
}
new_node = CreateNode(QUICKLIST_NODE_CONTAINER_PACKED, elem);
InsertNode(NULL, new_node, insert_opt);
count_++;
return;
}
/* Populate accounting flags for easier boolean checks later */
if (!NodeAllowInsert(node, fill_, sz)) {
full = 1;
@ -831,10 +825,10 @@ quicklistNode* QList::ListpackMerge(quicklistNode* a, quicklistNode* b) {
DelNode(nokeep);
quicklistCompress(keep);
return keep;
} else {
/* else, the merge returned NULL and nothing changed. */
return NULL;
}
/* else, the merge returned NULL and nothing changed. */
return NULL;
}
void QList::DelNode(quicklistNode* node) {
@ -909,13 +903,13 @@ auto QList::GetIterator(Where where) const -> Iterator {
auto QList::GetIterator(long idx) const -> Iterator {
quicklistNode* n;
unsigned long long accum = 0;
unsigned long long index;
int forward = idx < 0 ? 0 : 1; /* < 0 -> reverse, 0+ -> forward */
index = forward ? idx : (-idx) - 1;
uint64_t index = forward ? idx : (-idx) - 1;
if (index >= count_)
return {};
DCHECK(head_);
/* Seek in the other direction if that way is shorter. */
int seek_forward = forward;
unsigned long long seek_index = index;

View file

@ -46,6 +46,10 @@ class QList {
bool operator==(std::string_view sv) const;
friend bool operator==(std::string_view sv, const Entry& entry) {
return entry == sv;
}
std::string to_string() const {
if (std::holds_alternative<int64_t>(value_)) {
return std::to_string(std::get<int64_t>(value_));
@ -100,6 +104,8 @@ class QList {
// Returns true if pivot found and elem inserted, false otherwise.
bool Insert(std::string_view pivot, std::string_view elem, InsertOpt opt);
void Insert(Iterator it, std::string_view elem, InsertOpt opt);
// Returns true if item was replaced, false if index is out of range.
bool Replace(long index, std::string_view elem);
@ -141,6 +147,12 @@ class QList {
return tail_;
}
void set_fill(int fill) {
fill_ = fill;
}
static void SetPackedThreshold(unsigned threshold);
private:
bool AllowCompression() const {
return compress_ != 0;
@ -153,7 +165,6 @@ class QList {
bool PushTail(std::string_view value);
void InsertPlainNode(quicklistNode* old_node, std::string_view, InsertOpt insert_opt);
void InsertNode(quicklistNode* old_node, quicklistNode* new_node, InsertOpt insert_opt);
void Insert(Iterator it, std::string_view elem, InsertOpt opt);
void Replace(Iterator it, std::string_view elem);
void Compress(quicklistNode* node);

View file

@ -4,6 +4,7 @@
#include "core/qlist.h"
#include <absl/strings/match.h>
#include <absl/strings/str_cat.h>
#include <absl/strings/str_format.h>
#include <gmock/gmock.h>
@ -76,6 +77,7 @@ static int ql_verify(const QList& ql, uint32_t nc, uint32_t count, uint32_t head
while (node) {
node_size += node->count;
node = node->next;
CHECK(node != ql.Head());
}
if (node_size != ql.Size()) {
@ -101,13 +103,13 @@ static int ql_verify(const QList& ql, uint32_t nc, uint32_t count, uint32_t head
return 0;
}
if (ql.Head() && head_count != ql.Head()->count && head_count != lpLength(ql.Head()->entry)) {
if (head_count != ql.Head()->count && head_count != lpLength(ql.Head()->entry)) {
LOG(ERROR) << absl::StrFormat("head count wrong: expected %u got cached %u vs. actual %lu",
head_count, ql.Head()->count, lpLength(ql.Head()->entry));
errors++;
}
if (ql.Tail() && tail_count != ql.Tail()->count && tail_count != lpLength(ql.Tail()->entry)) {
if (tail_count != ql.Tail()->count && tail_count != lpLength(ql.Tail()->entry)) {
LOG(ERROR) << "tail count wrong: expected " << tail_count << "got cached " << ql.Tail()->count
<< " vs. actual " << lpLength(ql.Tail()->entry);
errors++;
@ -247,6 +249,37 @@ TEST_F(QListTest, PushPlain) {
EXPECT_THAT(items, ElementsAre(val));
}
TEST_F(QListTest, GetNum) {
ql_.Push("1251977", QList::HEAD);
QList::Iterator it = ql_.GetIterator(QList::HEAD);
ASSERT_TRUE(it.Next());
EXPECT_EQ(1251977, it.Get().ival());
}
TEST_F(QListTest, CompressionPlain) {
char buf[256];
QList::SetPackedThreshold(1);
ql_ = QList(-2, 1);
for (int i = 0; i < 500; i++) {
/* Set to 256 to allow the node to be triggered to compress,
* if it is less than 48(nocompress), the test will be successful. */
snprintf(buf, sizeof(buf), "hello%d", i);
ql_.Push(string_view{buf, sizeof(buf)}, QList::HEAD);
}
QList::SetPackedThreshold(0);
QList::Iterator it = ql_.GetIterator(QList::TAIL);
int i = 0;
while (it.Next()) {
string_view sv = it.Get().view();
ASSERT_EQ(sizeof(buf), sv.size());
ASSERT_TRUE(absl::StartsWith(sv, StrCat("hello", i)));
i++;
}
EXPECT_EQ(500, i);
}
using FillCompress = tuple<int, unsigned>;
class PrintToFillCompress {
@ -289,6 +322,25 @@ TEST_P(OptionsTest, Numbers) {
EXPECT_EQ("xxxxxxxxxxxxxxxxxxxx", it.Get().view());
}
TEST_P(OptionsTest, NumbersIndex) {
auto [fill, compress] = GetParam();
ql_ = QList(fill, compress);
long long nums[5000];
for (int i = 0; i < 760; i++) {
nums[i] = -5157318210846258176 + i;
ql_.Push(absl::StrCat(nums[i]), QList::TAIL);
}
unsigned i = 437;
QList::Iterator it = ql_.GetIterator(i);
while (it.Next()) {
ASSERT_EQ(nums[i], it.Get().ival());
i++;
}
ASSERT_EQ(760, i);
}
TEST_P(OptionsTest, DelRangeA) {
auto [fill, compress] = GetParam();
ql_ = QList(fill, compress);
@ -297,15 +349,17 @@ TEST_P(OptionsTest, DelRangeA) {
nums[i] = -5157318210846258176 + i;
ql_.Push(absl::StrCat(nums[i]), QList::TAIL);
}
if (fill == 32)
ql_verify(ql_, 2, 33, 32, 1);
if (fill == 32) {
ASSERT_EQ(0, ql_verify(ql_, 2, 33, 32, 1));
}
/* ltrim 3 3 (keep [3,3] inclusive = 1 remaining) */
ql_.Erase(0, 3);
ql_.Erase(-29, 4000); /* make sure not loop forever */
if (fill == 32)
ql_verify(ql_, 1, 1, 1, 1);
if (fill == 32) {
ASSERT_EQ(0, ql_verify(ql_, 1, 1, 1, 1));
}
auto it = ql_.GetIterator(0);
ASSERT_TRUE(it.Next());
EXPECT_EQ(-5157318210846258173, it.Get().ival());
@ -320,14 +374,15 @@ TEST_P(OptionsTest, DelRangeB) {
nums[i] = i;
ql_.Push(absl::StrCat(nums[i]), QList::TAIL);
}
if (fill == 32)
ql_verify(ql_, 2, 33, 32, 1);
if (fill == 32) {
ASSERT_EQ(0, ql_verify(ql_, 2, 33, 32, 1));
}
/* ltrim 5 16 (keep [5,16] inclusive = 12 remaining) */
ql_.Erase(0, 5);
ql_.Erase(-16, 16);
if (fill == 32)
ql_verify(ql_, 1, 12, 12, 12);
if (fill == 32) {
ASSERT_EQ(0, ql_verify(ql_, 1, 12, 12, 12));
}
auto it = ql_.GetIterator(0);
ASSERT_TRUE(it.Next());
@ -357,14 +412,16 @@ TEST_P(OptionsTest, DelRangeC) {
nums[i] = -5157318210846258176 + i;
ql_.Push(absl::StrCat(nums[i]), QList::TAIL);
}
if (fill == 32)
ql_verify(ql_, 2, 33, 32, 1);
if (fill == 32) {
ASSERT_EQ(0, ql_verify(ql_, 2, 33, 32, 1));
}
/* ltrim 3 3 (keep [3,3] inclusive = 1 remaining) */
ql_.Erase(0, 3);
ql_.Erase(-29, 4000); /* make sure not loop forever */
if (fill == 32)
ql_verify(ql_, 1, 1, 1, 1);
if (fill == 32) {
ASSERT_EQ(0, ql_verify(ql_, 1, 1, 1, 1));
}
auto it = ql_.GetIterator(0);
ASSERT_TRUE(it.Next());
ASSERT_EQ(-5157318210846258173, it.Get().ival());
@ -378,8 +435,9 @@ TEST_P(OptionsTest, DelRangeD) {
nums[i] = -5157318210846258176 + i;
ql_.Push(absl::StrCat(nums[i]), QList::TAIL);
}
if (fill == 32)
ql_verify(ql_, 2, 33, 32, 1);
if (fill == 32) {
ASSERT_EQ(0, ql_verify(ql_, 2, 33, 32, 1));
}
ql_.Erase(-12, 3);
ASSERT_EQ(30, ql_.Size());
@ -392,9 +450,9 @@ TEST_P(OptionsTest, DelRangeNode) {
for (int i = 0; i < 32; i++)
ql_.Push(StrCat("hello", i), QList::HEAD);
ql_verify(ql_, 1, 32, 32, 32);
ASSERT_EQ(0, ql_verify(ql_, 1, 32, 32, 32));
ql_.Erase(0, 32);
ql_verify(ql_, 0, 0, 0, 0);
ASSERT_EQ(0, ql_verify(ql_, 0, 0, 0, 0));
}
TEST_P(OptionsTest, DelRangeNodeOverflow) {
@ -403,9 +461,9 @@ TEST_P(OptionsTest, DelRangeNodeOverflow) {
for (int i = 0; i < 32; i++)
ql_.Push(StrCat("hello", i), QList::HEAD);
ql_verify(ql_, 1, 32, 32, 32);
ASSERT_EQ(0, ql_verify(ql_, 1, 32, 32, 32));
ql_.Erase(0, 128);
ql_verify(ql_, 0, 0, 0, 0);
ASSERT_EQ(0, ql_verify(ql_, 0, 0, 0, 0));
}
TEST_P(OptionsTest, DelRangeMiddle100of500) {
@ -415,9 +473,9 @@ TEST_P(OptionsTest, DelRangeMiddle100of500) {
for (int i = 0; i < 500; i++)
ql_.Push(StrCat("hello", i + 1), QList::TAIL);
ql_verify(ql_, 16, 500, 32, 20);
ASSERT_EQ(0, ql_verify(ql_, 16, 500, 32, 20));
ql_.Erase(200, 100);
ql_verify(ql_, 14, 400, 32, 20);
ASSERT_EQ(0, ql_verify(ql_, 14, 400, 32, 20));
}
TEST_P(OptionsTest, DelLessFillAcrossNodes) {
@ -426,9 +484,9 @@ TEST_P(OptionsTest, DelLessFillAcrossNodes) {
for (int i = 0; i < 500; i++)
ql_.Push(StrCat("hello", i + 1), QList::TAIL);
ql_verify(ql_, 16, 500, 32, 20);
ASSERT_EQ(0, ql_verify(ql_, 16, 500, 32, 20));
ql_.Erase(60, 10);
ql_verify(ql_, 16, 490, 32, 20);
ASSERT_EQ(0, ql_verify(ql_, 16, 490, 32, 20));
}
TEST_P(OptionsTest, DelNegOne) {
@ -436,9 +494,9 @@ TEST_P(OptionsTest, DelNegOne) {
ql_ = QList(32, compress);
for (int i = 0; i < 500; i++)
ql_.Push(StrCat("hello", i + 1), QList::TAIL);
ql_verify(ql_, 16, 500, 32, 20);
ASSERT_EQ(0, ql_verify(ql_, 16, 500, 32, 20));
ql_.Erase(-1, 1);
ql_verify(ql_, 16, 499, 32, 19);
ASSERT_EQ(0, ql_verify(ql_, 16, 499, 32, 19));
}
TEST_P(OptionsTest, DelNegOneOverflow) {
@ -447,10 +505,10 @@ TEST_P(OptionsTest, DelNegOneOverflow) {
for (int i = 0; i < 500; i++)
ql_.Push(StrCat("hello", i + 1), QList::TAIL);
ql_verify(ql_, 16, 500, 32, 20);
ASSERT_EQ(0, ql_verify(ql_, 16, 500, 32, 20));
ql_.Erase(-1, 128);
ql_verify(ql_, 16, 499, 32, 19);
ASSERT_EQ(0, ql_verify(ql_, 16, 499, 32, 19));
}
TEST_P(OptionsTest, DelNeg100From500) {
@ -459,7 +517,7 @@ TEST_P(OptionsTest, DelNeg100From500) {
for (int i = 0; i < 500; i++)
ql_.Push(StrCat("hello", i + 1), QList::TAIL);
ql_.Erase(-100, 100);
ql_verify(ql_, 13, 400, 32, 16);
ASSERT_EQ(0, ql_verify(ql_, 13, 400, 32, 16));
}
TEST_P(OptionsTest, DelMin10_5_from50) {
@ -468,9 +526,9 @@ TEST_P(OptionsTest, DelMin10_5_from50) {
for (int i = 0; i < 50; i++)
ql_.Push(StrCat("hello", i + 1), QList::TAIL);
ql_verify(ql_, 2, 50, 32, 18);
ASSERT_EQ(0, ql_verify(ql_, 2, 50, 32, 18));
ql_.Erase(-10, 5);
ql_verify(ql_, 2, 45, 32, 13);
ASSERT_EQ(0, ql_verify(ql_, 2, 45, 32, 13));
}
TEST_P(OptionsTest, DelElems) {
@ -513,4 +571,254 @@ TEST_P(OptionsTest, DelElems) {
EXPECT_THAT(ToItems(), ElementsAreArray(resultB));
}
TEST_P(OptionsTest, IterateReverse) {
auto [_, compress] = GetParam();
ql_ = QList(32, compress);
for (int i = 0; i < 500; i++)
ql_.Push(StrCat("hello", i), QList::HEAD);
QList::Iterator it = ql_.GetIterator(QList::TAIL);
int i = 0;
while (it.Next()) {
ASSERT_EQ(StrCat("hello", i), it.Get());
i++;
}
ASSERT_EQ(500, i);
ASSERT_EQ(0, ql_verify(ql_, 16, 500, 20, 32));
}
TEST_P(OptionsTest, Iterate500) {
auto [_, compress] = GetParam();
ql_ = QList(32, compress);
for (int i = 0; i < 500; i++)
ql_.Push(StrCat("hello", i), QList::HEAD);
QList::Iterator it = ql_.GetIterator(QList::HEAD);
int i = 499, count = 0;
while (it.Next()) {
QList::Entry entry = it.Get();
ASSERT_EQ(StrCat("hello", i), entry);
i--;
count++;
}
EXPECT_EQ(500, count);
ASSERT_EQ(0, ql_verify(ql_, 16, 500, 20, 32));
it = ql_.GetIterator(QList::TAIL);
i = 0;
while (it.Next()) {
ASSERT_EQ(StrCat("hello", i), it.Get());
i++;
}
EXPECT_EQ(500, i);
}
TEST_P(OptionsTest, IterateAfterOne) {
auto [_, compress] = GetParam();
ql_ = QList(-2, compress);
ql_.Push("hello", QList::HEAD);
QList::Iterator it = ql_.GetIterator(0);
ASSERT_TRUE(it.Next());
ql_.Insert(it, "abc", QList::AFTER);
ASSERT_EQ(0, ql_verify(ql_, 1, 2, 2, 2));
/* verify results */
it = ql_.GetIterator(0);
ASSERT_TRUE(it.Next());
ASSERT_EQ("hello", it.Get());
it = ql_.GetIterator(1);
ASSERT_TRUE(it.Next());
ASSERT_EQ("abc", it.Get());
}
TEST_P(OptionsTest, IterateDelete) {
auto [fill, compress] = GetParam();
ql_ = QList(fill, compress);
ql_.Push("abc", QList::TAIL);
ql_.Push("def", QList::TAIL);
ql_.Push("hij", QList::TAIL);
ql_.Push("jkl", QList::TAIL);
ql_.Push("oop", QList::TAIL);
QList::Iterator it = ql_.GetIterator(QList::HEAD);
int i = 0;
while (it.Next()) {
if (it.Get() == "hij") {
it = ql_.Erase(it);
}
i++;
}
ASSERT_EQ(5, i);
ASSERT_THAT(ToItems(), ElementsAre("abc", "def", "jkl", "oop"));
}
TEST_P(OptionsTest, InsertBeforeOne) {
auto [_, compress] = GetParam();
ql_ = QList(-2, compress);
ql_.Push("hello", QList::HEAD);
QList::Iterator it = ql_.GetIterator(0);
ASSERT_TRUE(it.Next());
ql_.Insert(it, "abc", QList::BEFORE);
ql_verify(ql_, 1, 2, 2, 2);
/* verify results */
it = ql_.GetIterator(0);
ASSERT_TRUE(it.Next());
ASSERT_EQ("abc", it.Get());
it = ql_.GetIterator(1);
ASSERT_TRUE(it.Next());
ASSERT_EQ("hello", it.Get());
}
TEST_P(OptionsTest, InsertWithHeadFull) {
auto [_, compress] = GetParam();
ql_ = QList(4, compress);
for (int i = 0; i < 10; i++)
ql_.Push(StrCat("hello", i), QList::TAIL);
ql_.set_fill(-1);
QList::Iterator it = ql_.GetIterator(-10);
ASSERT_TRUE(it.Next());
char buf[4096] = {0};
ql_.Insert(it, string_view{buf, sizeof(buf)}, QList::BEFORE);
ql_verify(ql_, 4, 11, 1, 2);
}
TEST_P(OptionsTest, InsertWithTailFull) {
auto [_, compress] = GetParam();
ql_ = QList(4, compress);
for (int i = 0; i < 10; i++)
ql_.Push(StrCat("hello", i), QList::HEAD);
ql_.set_fill(-1);
QList::Iterator it = ql_.GetIterator(-1);
ASSERT_TRUE(it.Next());
char buf[4096] = {0};
ql_.Insert(it, string_view{buf, sizeof(buf)}, QList::AFTER);
ql_verify(ql_, 4, 11, 2, 1);
}
TEST_P(OptionsTest, InsertOnceWhileIterating) {
auto [fill, compress] = GetParam();
ql_ = QList(fill, compress);
ql_.Push("abc", QList::TAIL);
ql_.set_fill(1);
ql_.Push("def", QList::TAIL);
ql_.set_fill(fill);
ql_.Push("bob", QList::TAIL);
ql_.Push("foo", QList::TAIL);
ql_.Push("zoo", QList::TAIL);
/* insert "bar" before "bob" while iterating over list. */
QList::Iterator it = ql_.GetIterator(QList::HEAD);
while (it.Next()) {
if (it.Get() == "bob") {
ql_.Insert(it, "bar", QList::BEFORE);
break; /* didn't we fix insert-while-iterating? */
}
}
EXPECT_THAT(ToItems(), ElementsAre("abc", "def", "bar", "bob", "foo", "zoo"));
}
TEST_P(OptionsTest, InsertBefore250NewInMiddleOf500Elements) {
auto [fill, compress] = GetParam();
ql_ = QList(fill, compress);
for (int i = 0; i < 500; i++) {
string val = StrCat("hello", i);
val.resize(32);
ql_.Push(val, QList::TAIL);
}
for (int i = 0; i < 250; i++) {
QList::Iterator it = ql_.GetIterator(250);
ASSERT_TRUE(it.Next());
ql_.Insert(it, StrCat("abc", i), QList::BEFORE);
}
if (fill == 32) {
ASSERT_EQ(0, ql_verify(ql_, 25, 750, 32, 20));
}
}
TEST_P(OptionsTest, InsertAfter250NewInMiddleOf500Elements) {
auto [fill, compress] = GetParam();
ql_ = QList(fill, compress);
for (int i = 0; i < 500; i++)
ql_.Push(StrCat("hello", i), QList::HEAD);
for (int i = 0; i < 250; i++) {
QList::Iterator it = ql_.GetIterator(250);
ASSERT_TRUE(it.Next());
ql_.Insert(it, StrCat("abc", i), QList::AFTER);
}
ASSERT_EQ(750, ql_.Size());
if (fill == 32) {
ASSERT_EQ(0, ql_verify(ql_, 26, 750, 20, 32));
}
}
TEST_P(OptionsTest, NextPlain) {
auto [_, compress] = GetParam();
ql_ = QList(-2, compress);
QList::SetPackedThreshold(3);
const char* strings[] = {"hello1", "hello2", "h3", "h4", "hello5"};
for (int i = 0; i < 5; ++i)
ql_.Push(strings[i], QList::HEAD);
QList::Iterator it = ql_.GetIterator(QList::TAIL);
int j = 0;
while (it.Next()) {
ASSERT_EQ(strings[j], it.Get());
j++;
}
}
TEST_P(OptionsTest, IndexFrom500) {
auto [fill, compress] = GetParam();
ql_ = QList(fill, compress);
for (int i = 0; i < 500; i++)
ql_.Push(StrCat("hello", i + 1), QList::TAIL);
QList::Iterator it = ql_.GetIterator(1);
ASSERT_TRUE(it.Next());
ASSERT_EQ("hello2", it.Get());
it = ql_.GetIterator(200);
ASSERT_TRUE(it.Next());
ASSERT_EQ("hello201", it.Get());
it = ql_.GetIterator(-1);
ASSERT_TRUE(it.Next());
ASSERT_EQ("hello500", it.Get());
it = ql_.GetIterator(-2);
ASSERT_TRUE(it.Next());
ASSERT_EQ("hello499", it.Get());
it = ql_.GetIterator(-100);
ASSERT_TRUE(it.Next());
ASSERT_EQ("hello401", it.Get());
it = ql_.GetIterator(500);
ASSERT_FALSE(it.Next());
}
} // namespace dfly