blob: 40f8a8d33bc316be7cf0d4fc4e247fb8ed2417c1 [file] [log] [blame]
/*
* Copyright 2024-present Alibaba Inc.
*
* 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 "paimon/memory/bytes.h"
#include <memory>
#include <utility>
#include "gtest/gtest.h"
#include "paimon/memory/memory_pool.h"
namespace paimon::test {
TEST(BytesTest, TestSimple) {
auto pool = paimon::GetMemoryPool();
Bytes bytes("abcde", pool.get());
Bytes moved_bytes("efgh", pool.get());
ASSERT_EQ(9, pool->CurrentUsage());
moved_bytes = std::move(bytes);
ASSERT_EQ(5, pool->CurrentUsage());
ASSERT_EQ("abcde", std::string(moved_bytes.data(), moved_bytes.size()));
}
TEST(BytesTest, TestCopyOf) {
auto pool = paimon::GetMemoryPool();
// pool allocate 5 bytes + sizeof(Bytes)
auto bytes = Bytes::AllocateBytes("abcde", pool.get());
ASSERT_EQ("abcde", std::string(bytes->data(), bytes->size()));
// pool allocate 100 bytes + sizeof(Bytes)
auto cp_bytes = Bytes::CopyOf(*bytes, 100, pool.get());
ASSERT_EQ("abcde", std::string(cp_bytes->data(), 5));
ASSERT_EQ(5 + 100 + sizeof(Bytes) * 2, pool->CurrentUsage());
}
TEST(BytesTest, TestAllocateBytesAndMove) {
auto pool = paimon::GetMemoryPool();
// pool allocate 5 + sizeof(Bytes)
PAIMON_UNIQUE_PTR<Bytes> bytes = Bytes::AllocateBytes("abcde", pool.get());
ASSERT_EQ("abcde", std::string(bytes->data(), bytes->size()));
ASSERT_EQ(5 + sizeof(Bytes), pool->CurrentUsage());
// pool allocate 4 + sizeof(Bytes)
PAIMON_UNIQUE_PTR<Bytes> moved_bytes = Bytes::AllocateBytes("efgh", pool.get());
ASSERT_EQ("efgh", std::string(moved_bytes->data(), moved_bytes->size()));
ASSERT_EQ(9 + 2 * sizeof(Bytes), pool->CurrentUsage());
// pool deallocate sizeof(Bytes) + 4
moved_bytes = std::move(bytes);
ASSERT_FALSE(bytes);
ASSERT_EQ("abcde", std::string(moved_bytes->data(), moved_bytes->size()));
ASSERT_EQ(5 + sizeof(Bytes), pool->CurrentUsage());
// pool allocate sizeof(Bytes) and deallocate sizeof(Bytes)
std::shared_ptr<Bytes> shared_bytes = std::move(moved_bytes);
ASSERT_FALSE(moved_bytes);
ASSERT_EQ("abcde", std::string(shared_bytes->data(), shared_bytes->size()));
ASSERT_EQ(5 + sizeof(Bytes), pool->CurrentUsage());
}
TEST(BytesTest, TestCompare) {
auto pool = paimon::GetMemoryPool();
PAIMON_UNIQUE_PTR<Bytes> bytes1 = Bytes::AllocateBytes("abcde", pool.get());
PAIMON_UNIQUE_PTR<Bytes> bytes2 = Bytes::AllocateBytes("abcdf", pool.get());
ASSERT_EQ(*bytes1, *bytes1);
ASSERT_EQ(*bytes2, *bytes2);
ASSERT_EQ(*bytes1, *bytes1);
ASSERT_EQ(*bytes2, *bytes2);
ASSERT_FALSE(*bytes1 == *bytes2);
ASSERT_LT(*bytes1, *bytes2);
ASSERT_FALSE(*bytes1 < *bytes1);
}
// Test to verify that move assignment correctly handles memory and prevents double-free.
// Before the fix, the old implementation used memcpy + destructor which caused:
// 1. The target's original memory was freed in destructor
// 2. After memcpy, both source and target pointed to same memory
// 3. When source was "reset" via placement new, it became empty
// 4. But if move assignment was called again on the same target, the memcpy'd
// pointer would be freed again (double-free) or memory accounting would be wrong.
TEST(BytesTest, TestMoveAssignmentNoDoubleFree) {
auto pool = paimon::GetMemoryPool();
// Create three Bytes objects on stack
Bytes a("aaaa", pool.get()); // 4 bytes
Bytes b("bb", pool.get()); // 2 bytes
Bytes c("cccccc", pool.get()); // 6 bytes
ASSERT_EQ(12, pool->CurrentUsage()); // 4 + 2 + 6 = 12
// First move: b = std::move(a)
// Should free b's original memory (2 bytes), transfer a's memory to b
b = std::move(a);
ASSERT_EQ(10, pool->CurrentUsage()); // 4 + 6 = 10 (b's 2 bytes freed)
ASSERT_EQ("aaaa", std::string(b.data(), b.size()));
// Moved-from object is expected to be empty by Bytes' contract.
ASSERT_EQ(nullptr, a.data()); // NOLINT(bugprone-use-after-move, clang-analyzer-cplusplus.Move)
ASSERT_EQ(0, a.size()); // NOLINT(bugprone-use-after-move, clang-analyzer-cplusplus.Move)
// Second move: b = std::move(c)
// Should free b's current memory (4 bytes from a), transfer c's memory to b
// This is where the old implementation would cause issues:
// - Old code would call destructor on b, freeing the 4 bytes
// - Then memcpy c into b, making b point to c's 6-byte buffer
// - Memory accounting would be wrong because Free was called on wrong data
b = std::move(c);
ASSERT_EQ(6, pool->CurrentUsage()); // Only c's 6 bytes remain (now owned by b)
ASSERT_EQ("cccccc", std::string(b.data(), b.size()));
// Moved-from object is expected to be empty by Bytes' contract.
ASSERT_EQ(nullptr, c.data()); // NOLINT(bugprone-use-after-move, clang-analyzer-cplusplus.Move)
ASSERT_EQ(0, c.size()); // NOLINT(bugprone-use-after-move, clang-analyzer-cplusplus.Move)
// Self-assignment should be safe. Use an alias to avoid -Wself-move.
Bytes* self = &b;
b = std::move(*self);
ASSERT_EQ(6, pool->CurrentUsage());
ASSERT_EQ("cccccc", std::string(b.data(), b.size()));
}
// Test move assignment with heap-allocated Bytes to verify no double-free
// when combining unique_ptr semantics with move assignment
TEST(BytesTest, TestMoveAssignmentHeapAllocated) {
auto pool = paimon::GetMemoryPool();
auto bytes1 = Bytes::AllocateBytes("hello", pool.get()); // 5 bytes + sizeof(Bytes)
auto bytes2 = Bytes::AllocateBytes("world!", pool.get()); // 6 bytes + sizeof(Bytes)
size_t expected = 5 + 6 + 2 * sizeof(Bytes);
ASSERT_EQ(expected, pool->CurrentUsage());
// Move the content of bytes1 into bytes2's Bytes object
// This should free "world!" (6 bytes) and transfer "hello" ownership
*bytes2 = std::move(*bytes1);
expected = 5 + 2 * sizeof(Bytes); // "world!" freed, "hello" transferred
ASSERT_EQ(expected, pool->CurrentUsage());
ASSERT_EQ("hello", std::string(bytes2->data(), bytes2->size()));
ASSERT_EQ(nullptr, bytes1->data());
// Reset bytes2, which should free "hello"
bytes2.reset();
expected = sizeof(Bytes); // Only bytes1's empty Bytes struct remains
ASSERT_EQ(expected, pool->CurrentUsage());
}
} // namespace paimon::test