blob: 5269c9706ee89326d824e11ebab19b1ee39a90f5 [file] [log] [blame] [edit]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "perfetto/ext/base/circular_queue.h"
#include <random>
#include "test/gtest_and_gmock.h"
namespace perfetto {
namespace base {
namespace {
using testing::ElementsAre;
TEST(CircularQueueTest, Int) {
CircularQueue<int> queue(/*initial_capacity=*/1);
ASSERT_EQ(queue.size(), 0u);
queue.emplace_back(101);
ASSERT_EQ(queue.size(), 1u);
queue.emplace_back(102);
queue.emplace_back(103);
queue.emplace_back(104);
ASSERT_EQ(queue.size(), 4u);
auto it = queue.begin();
for (int i = 101; i <= 104; i++) {
ASSERT_EQ(*it, i);
it++;
}
ASSERT_EQ(it, queue.end());
queue.erase_front(1);
ASSERT_EQ(queue.size(), 3u);
ASSERT_EQ(*queue.begin(), 102);
*(queue.begin() + 1) = 42;
ASSERT_EQ(*(queue.end() - 2), 42);
queue.erase_front(2);
ASSERT_EQ(queue.size(), 1u);
ASSERT_EQ(*queue.begin(), 104);
queue.pop_front();
ASSERT_EQ(queue.size(), 0u);
ASSERT_EQ(queue.begin(), queue.end());
const size_t kNumInts = 100000u;
{
std::minstd_rand0 rnd_engine(0);
for (size_t i = 0; i < kNumInts; i++) {
int n = static_cast<int>(rnd_engine());
queue.emplace_back(n);
}
}
ASSERT_EQ(queue.size(), kNumInts);
ASSERT_EQ(static_cast<size_t>(queue.end() - queue.begin()), kNumInts);
{
std::minstd_rand0 rnd_engine(0);
it = queue.begin();
for (size_t i = 0; i < kNumInts; ++i, ++it) {
ASSERT_LT(it, queue.end());
ASSERT_EQ(*it, static_cast<int>(rnd_engine()));
}
}
{
std::minstd_rand0 del_rnd(42);
std::minstd_rand0 rnd_engine(0);
while (!queue.empty()) {
ASSERT_EQ(*queue.begin(), static_cast<int>(rnd_engine()));
size_t num_del = (del_rnd() % 8) + 1;
queue.erase_front(num_del + 1); // +1 because of the read 2 lines above.
rnd_engine.discard(num_del);
}
}
}
TEST(CircularQueueTest, Sorting) {
CircularQueue<uint64_t> queue;
std::minstd_rand0 rnd_engine(0);
for (int i = 0; i < 100000; i++) {
queue.emplace_back(static_cast<uint64_t>(rnd_engine()));
if ((i % 100) == 0)
queue.erase_front(29);
}
ASSERT_FALSE(std::is_sorted(queue.begin(), queue.end()));
std::sort(queue.begin(), queue.end());
ASSERT_TRUE(std::is_sorted(queue.begin(), queue.end()));
}
TEST(CircularQueueTest, MoveOperators) {
CircularQueue<int> queue;
queue.emplace_back(1);
queue.emplace_back(2);
{
CircularQueue<int> moved(std::move(queue));
ASSERT_TRUE(queue.empty());
ASSERT_EQ(moved.size(), 2u);
moved.emplace_back(3);
moved.emplace_back(4);
ASSERT_EQ(moved.size(), 4u);
}
queue.emplace_back(10);
queue.emplace_back(11);
queue.emplace_back(12);
ASSERT_EQ(queue.size(), 3u);
ASSERT_EQ(queue.front(), 10);
ASSERT_EQ(queue.back(), 12);
{
CircularQueue<int> moved;
moved.emplace_back(42);
moved = std::move(queue);
ASSERT_TRUE(queue.empty());
ASSERT_EQ(moved.size(), 3u);
ASSERT_EQ(moved.size(), 3u);
ASSERT_EQ(moved.front(), 10);
ASSERT_EQ(moved.back(), 12);
}
}
TEST(CircularQueueTest, Iterators) {
for (size_t repeat = 1; repeat < 8; repeat++) {
size_t capacity = 8 * (1 << repeat);
CircularQueue<size_t> queue(capacity);
for (size_t i = 0; i < capacity - 2; i++)
queue.emplace_back(0u);
queue.erase_front(queue.size());
ASSERT_TRUE(queue.empty());
ASSERT_EQ(queue.capacity(), capacity);
// Now the queue is empty and the internal write iterator is abut to wrap.
// Add a bit more than half-capacity and check that the queue didn't resize.
for (size_t i = 0; i < capacity / 2 + 3; i++)
queue.emplace_back(i);
ASSERT_EQ(queue.capacity(), capacity);
// Check that all iterators are consistent.
auto begin = queue.begin();
auto end = queue.end();
auto mid = begin + std::distance(begin, end) / 2;
ASSERT_TRUE(std::is_sorted(begin, end));
ASSERT_TRUE(begin < end);
ASSERT_TRUE(begin <= begin);
ASSERT_TRUE(begin >= begin);
ASSERT_FALSE(begin < begin);
ASSERT_FALSE(begin > begin);
ASSERT_TRUE(begin + 1 > begin);
ASSERT_TRUE(begin + 1 >= begin);
ASSERT_FALSE(begin >= begin + 1);
ASSERT_TRUE(begin <= begin);
ASSERT_TRUE(begin <= begin + 1);
ASSERT_TRUE(end > mid);
ASSERT_TRUE(mid > begin);
ASSERT_TRUE(std::is_sorted(begin, mid));
ASSERT_TRUE(std::is_sorted(mid, end));
}
}
TEST(CircularQueueTest, ReverseIterators) {
CircularQueue<int> queue;
ASSERT_EQ(queue.rbegin(), queue.rend());
queue.emplace_back(1);
ASSERT_EQ(*queue.rbegin(), 1);
ASSERT_EQ(++queue.rbegin(), queue.rend());
queue.emplace_back(2);
queue.emplace_back(3);
auto it = queue.rbegin();
ASSERT_EQ(*(it++), 3);
ASSERT_EQ(*(it++), 2);
ASSERT_EQ(*(it++), 1);
ASSERT_EQ(it, queue.rend());
}
TEST(CircularQueueTest, InsertBefore) {
CircularQueue<int> queue;
queue.InsertBefore(queue.end(), 20);
ASSERT_THAT(queue, ElementsAre(20));
queue.InsertBefore(queue.begin(), 10);
ASSERT_THAT(queue, ElementsAre(10, 20));
queue.InsertBefore(queue.end(), 40);
ASSERT_THAT(queue, ElementsAre(10, 20, 40));
queue.InsertBefore(queue.Find(40), 30);
ASSERT_THAT(queue, ElementsAre(10, 20, 30, 40));
queue.InsertBefore(queue.begin(), 0);
ASSERT_THAT(queue, ElementsAre(0, 10, 20, 30, 40));
// Now test InsertAfter(reverse_iterator). There is a catch here,
// Insert on a reverse iterator actually inserts *after* the iterator.
// As bad as it sound, this is consistent with what other STL containers do.
// It's tied to the fact that std::reverse_iterator creates maps r == i-1.
// From STL docs:
// > For a reverse iterator r constructed from an iterator i, the relationship
// > &*r == &*(i - 1) is always true (as long as r is dereferenceable); thus a
// > reverse iterator constructed from a one-past-the-end iterator
// > dereferences to the last element in a sequence.
// ```
queue.InsertAfter(queue.rbegin(), 60);
ASSERT_THAT(queue, ElementsAre(0, 10, 20, 30, 40, 60));
for (auto it = queue.rbegin(); it != queue.rend(); ++it) {
if (*it == 40) {
queue.InsertAfter(it, 50);
break;
}
}
ASSERT_THAT(queue, ElementsAre(0, 10, 20, 30, 40, 50, 60));
// I know you don't believe me, so here's the proof.
std::vector<int> v{10, 20};
v.emplace(v.rbegin().base(), 40);
ASSERT_THAT(v, ElementsAre(10, 20, 40));
for (auto it = v.rbegin(); it != v.rend(); ++it) {
if (*it == 20) {
v.insert(it.base(), 30);
break;
}
}
ASSERT_THAT(v, ElementsAre(10, 20, 30, 40));
}
TEST(CircularQueueTest, InsertBeforeReverse) {
CircularQueue<int> queue;
std::array<int, 5> new_entries{4, 1, 5, 2, 3};
for (int n : new_entries) {
auto it = queue.rbegin();
while (it != queue.rend() && n < *it) {
++it;
}
queue.InsertAfter(it, n);
}
ASSERT_THAT(queue, ElementsAre(1, 2, 3, 4, 5));
}
// Test that InsertBefore works correctly when it triggers a capacity growth.
// This verifies that the iterator's pos_ (which is an abstract index) remains
// valid after the internal storage is reallocated during Grow().
TEST(CircularQueueTest, InsertBeforeWithGrow) {
// Use a small initial capacity so we can easily trigger growth.
CircularQueue<int> queue(/*initial_capacity=*/4);
// Fill the queue to capacity.
queue.emplace_back(10);
queue.emplace_back(20);
queue.emplace_back(30);
queue.emplace_back(40);
ASSERT_EQ(queue.size(), 4u);
ASSERT_EQ(queue.capacity(), 4u);
// Get the position where we want to insert (at element 30).
// Note: We can't use the iterator directly after InsertBefore because
// increment_generation() invalidates it. We use pos_ which is accessed
// directly by InsertBefore.
auto it = queue.begin() + 2;
ASSERT_EQ(*it, 30);
// This InsertBefore should trigger Grow() since we're at capacity.
// After Grow(), the iterator's pos_ (an abstract index) should still be
// valid because CircularQueue uses monotonic indices, not pointers.
queue.InsertBefore(it, 25);
// Verify the queue grew.
ASSERT_GT(queue.capacity(), 4u);
ASSERT_EQ(queue.size(), 5u);
// Verify all elements are in the correct order.
ASSERT_THAT(queue, ElementsAre(10, 20, 25, 30, 40));
}
// Test InsertBefore with growth when the queue has wrapped around internally.
TEST(CircularQueueTest, InsertBeforeWithGrowAfterWrap) {
CircularQueue<int> queue(/*initial_capacity=*/4);
// Fill and then pop to move the internal begin_ forward.
queue.emplace_back(1);
queue.emplace_back(2);
queue.pop_front();
queue.pop_front();
// Now internal begin_ is at position 2. Add elements to wrap around.
queue.emplace_back(10);
queue.emplace_back(20);
queue.emplace_back(30);
queue.emplace_back(40);
ASSERT_EQ(queue.size(), 4u);
ASSERT_EQ(queue.capacity(), 4u);
// Insert in the middle, triggering growth while wrapped.
auto it = queue.begin() + 2;
ASSERT_EQ(*it, 30);
queue.InsertBefore(it, 25);
ASSERT_GT(queue.capacity(), 4u);
ASSERT_EQ(queue.size(), 5u);
ASSERT_THAT(queue, ElementsAre(10, 20, 25, 30, 40));
}
TEST(CircularQueueTest, ObjectLifetime) {
class Checker {
public:
struct Stats {
int num_ctors = 0;
int num_dtors = 0;
int num_alive = 0;
};
Checker(Stats* stats, int n) : stats_(stats), n_(n) {
EXPECT_GE(n, 0);
stats_->num_ctors++;
stats_->num_alive++;
ptr_ = reinterpret_cast<uintptr_t>(this);
}
~Checker() {
EXPECT_EQ(ptr_, reinterpret_cast<uintptr_t>(this));
if (n_ >= 0)
stats_->num_alive--;
n_ = -1;
stats_->num_dtors++;
}
Checker(Checker&& other) noexcept {
n_ = other.n_;
other.n_ = -1;
stats_ = other.stats_;
ptr_ = reinterpret_cast<uintptr_t>(this);
}
Checker& operator=(Checker&& other) {
new (this) Checker(std::move(other));
return *this;
}
Checker(const Checker&) = delete;
Checker& operator=(const Checker&) = delete;
Stats* stats_ = nullptr;
uintptr_t ptr_ = 0;
int n_ = -1;
};
// Check that dtors are invoked on old elements when growing capacity.
Checker::Stats stats;
{
CircularQueue<Checker> queue(/*initial_capacity=*/2);
for (int i = 0; i < 2; i++)
queue.emplace_back(&stats, i);
ASSERT_EQ(stats.num_ctors, 2);
ASSERT_EQ(stats.num_dtors, 0);
// This further insertion will grow the queue, causing two std::move()s
// for the previous elements and one emplace.
queue.emplace_back(&stats, 2);
ASSERT_EQ(stats.num_ctors, 3);
// The two old elements that have std::move()d should be destroyed upon
// expansion.
ASSERT_EQ(stats.num_dtors, 2);
}
ASSERT_EQ(stats.num_dtors, 2 + 3);
stats = Checker::Stats();
{
CircularQueue<Checker> queue(/*initial_capacity=*/1);
for (int i = 0; i < 5; i++)
queue.emplace_back(&stats, i);
ASSERT_EQ(stats.num_ctors, 5);
Checker c5(&stats, 5);
queue.emplace_back(std::move(c5));
ASSERT_EQ(stats.num_alive, 5 + 1);
queue.erase_front(2);
ASSERT_EQ(stats.num_alive, 5 + 1 - 2);
for (int i = 0; i < 4; i++)
queue.emplace_back(&stats, 10 + i);
ASSERT_EQ(stats.num_alive, 5 + 1 - 2 + 4);
}
ASSERT_EQ(stats.num_ctors, 5 + 1 + 4);
ASSERT_EQ(stats.num_alive, 0);
stats = Checker::Stats();
{
CircularQueue<Checker> q1(1);
CircularQueue<Checker> q2(64);
for (int i = 0; i < 100; i++) {
q1.emplace_back(&stats, 1000 + i * 2);
q2.emplace_back(&stats, 1001 + i * 2);
}
ASSERT_EQ(stats.num_alive, 200);
for (int i = 0; i < 100; i += 2) {
auto it1 = q1.begin() + i;
auto it2 = q2.begin() + i;
std::swap(*it1, *it2);
}
auto comparer = [](const Checker& lhs, const Checker& rhs) {
return lhs.n_ < rhs.n_;
};
ASSERT_TRUE(std::is_sorted(q1.begin(), q1.end(), comparer));
ASSERT_TRUE(std::is_sorted(q2.begin(), q2.end(), comparer));
ASSERT_EQ(stats.num_alive, 200);
}
}
} // namespace
} // namespace base
} // namespace perfetto