blob: 04ba95776a564ed023df1f9eef3c012d277de6ba [file]
/**
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 "LmdbStream.h"
#include "lmdb.h"
#include "unit/Catch.h"
#include "unit/TestBase.h"
namespace org::apache::nifi::minifi::test {
class LmdbStreamTest : TestController {
public:
LmdbStreamTest() : db_path_(createTempDirectory().string()) {
if (const int rc = mdb_env_create(&lmdb_env_); rc != MDB_SUCCESS) {
throw std::runtime_error("Failed to create LMDB environment: " + std::string(mdb_strerror(rc)));
}
if (const int rc = mdb_env_set_mapsize(lmdb_env_, 100ULL * 1024 * 1024); rc != MDB_SUCCESS) {
throw std::runtime_error("Failed to set LMDB map size: " + std::string(mdb_strerror(rc)));
}
if (const int rc = mdb_env_open(lmdb_env_, db_path_.c_str(), MDB_NOTLS, 0664); rc != MDB_SUCCESS) {
throw std::runtime_error("Failed to open LMDB environment " + db_path_ + ": " + std::string(mdb_strerror(rc)));
}
MDB_txn* init_txn = nullptr;
mdb_txn_begin(lmdb_env_, nullptr, 0, &init_txn);
if (const auto rc = mdb_dbi_open(init_txn, nullptr, 0, &lmdb_handle_); rc != MDB_SUCCESS) {
mdb_txn_abort(init_txn);
mdb_env_close(lmdb_env_);
throw std::runtime_error("Failed to open LMDB database: " + std::string(mdb_strerror(rc)));
}
mdb_txn_commit(init_txn);
}
LmdbStreamTest(const LmdbStreamTest&) = delete;
LmdbStreamTest& operator=(const LmdbStreamTest&) = delete;
LmdbStreamTest(LmdbStreamTest&&) = delete;
LmdbStreamTest& operator=(LmdbStreamTest&&) = delete;
~LmdbStreamTest() override {
mdb_dbi_close(lmdb_env_, lmdb_handle_);
mdb_env_close(lmdb_env_);
}
std::optional<std::string> readValue(const std::string& key) {
MDB_val db_key{key.size(), const_cast<char*>(key.data())};
MDB_val db_value{};
MDB_txn* txn = nullptr;
mdb_txn_begin(lmdb_env_, nullptr, MDB_RDONLY, &txn);
auto guard = gsl::finally([txn] { mdb_txn_abort(txn); });
const auto result = mdb_get(txn, lmdb_handle_, &db_key, &db_value);
if (result == MDB_SUCCESS) { return std::string(static_cast<char*>(db_value.mv_data), db_value.mv_size); }
return std::nullopt;
}
protected:
std::string db_path_;
MDB_env* lmdb_env_{nullptr};
MDB_dbi lmdb_handle_{};
};
namespace {
size_t writeString(io::LmdbStream& stream, const std::string& content) {
return stream.write(reinterpret_cast<const uint8_t*>(content.data()), content.size());
}
std::string bytesToString(const std::vector<std::byte>& buf, size_t len) {
return {reinterpret_cast<const char*>(buf.data()), len};
}
} // namespace
TEST_CASE_METHOD(LmdbStreamTest, "Simple write tests") {
std::string content;
SECTION("Non-empty value") {
content = "banana";
}
SECTION("Empty value") {
content = "";
}
{
io::LmdbStream stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(stream, content)));
}
auto val = readValue(db_path_);
REQUIRE(val.has_value());
CHECK(val->size() == content.size());
CHECK(*val == content);
}
TEST_CASE_METHOD(LmdbStreamTest, "Multiple write test") {
std::string content = "banana";
std::string expected_content;
{
io::LmdbStream stream(db_path_, lmdb_env_, &lmdb_handle_, true);
for (int i = 0; i < 5; ++i) {
REQUIRE_FALSE(minifi::io::isError(writeString(stream, content)));
expected_content += content;
}
}
auto val = readValue(db_path_);
REQUIRE(val.has_value());
CHECK(val->size() == expected_content.size());
CHECK(*val == expected_content);
}
TEST_CASE_METHOD(LmdbStreamTest, "Simple read tests") {
std::string content;
SECTION("Non-empty value") {
content = "banana";
}
SECTION("Empty value") {
content = "";
}
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, content)));
write_stream.close();
io::LmdbStream read_stream(db_path_, lmdb_env_, &lmdb_handle_, false);
std::vector<std::byte> buffer(content.size());
REQUIRE_FALSE(minifi::io::isError(read_stream.read(buffer)));
REQUIRE(bytesToString(buffer, buffer.size()) == content);
}
TEST_CASE_METHOD(LmdbStreamTest, "Read in chunks") {
std::string content = "banana";
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, content)));
write_stream.close();
io::LmdbStream read_stream(db_path_, lmdb_env_, &lmdb_handle_, false);
std::vector<std::byte> buffer(2);
REQUIRE_FALSE(minifi::io::isError(read_stream.read(buffer)));
REQUIRE(bytesToString(buffer, buffer.size()) == "ba");
REQUIRE_FALSE(minifi::io::isError(read_stream.read(buffer)));
REQUIRE(bytesToString(buffer, buffer.size()) == "na");
std::vector<std::byte> buffer2(5);
const auto read_result = read_stream.read(buffer2);
REQUIRE_FALSE(minifi::io::isError(read_result));
REQUIRE(bytesToString(buffer2, read_result) == "na");
}
TEST_CASE_METHOD(LmdbStreamTest, "Reading a nonexistent key returns STREAM_ERROR") {
io::LmdbStream read_stream(db_path_, lmdb_env_, &lmdb_handle_, false);
std::vector<std::byte> buffer(8);
REQUIRE(minifi::io::isError(read_stream.read(buffer)));
}
TEST_CASE_METHOD(LmdbStreamTest, "Reading after EOF returns zero") {
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, "abc")));
write_stream.close();
io::LmdbStream read_stream(db_path_, lmdb_env_, &lmdb_handle_, false);
std::vector<std::byte> buffer(3);
REQUIRE(read_stream.read(buffer) == 3);
REQUIRE(read_stream.read(buffer) == 0);
}
TEST_CASE_METHOD(LmdbStreamTest, "Reading into an empty buffer returns zero") {
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, "abc")));
write_stream.close();
io::LmdbStream read_stream(db_path_, lmdb_env_, &lmdb_handle_, false);
std::vector<std::byte> empty_buffer;
REQUIRE(read_stream.read(empty_buffer) == 0);
}
TEST_CASE_METHOD(LmdbStreamTest, "seek and tell control read offset") {
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, "banana")));
write_stream.close();
io::LmdbStream read_stream(db_path_, lmdb_env_, &lmdb_handle_, false);
read_stream.seek(2);
REQUIRE(read_stream.tell() == 2);
std::vector<std::byte> buffer(3);
REQUIRE(read_stream.read(buffer) == 3);
REQUIRE(bytesToString(buffer, buffer.size()) == "nan");
REQUIRE(read_stream.tell() == 5);
}
TEST_CASE_METHOD(LmdbStreamTest, "size reflects buffered writes before commit") {
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE(write_stream.size() == 0);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, "hello")));
REQUIRE(write_stream.size() == 5);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, "!")));
REQUIRE(write_stream.size() == 6);
}
TEST_CASE_METHOD(LmdbStreamTest, "Writing to a read-only stream returns STREAM_ERROR") {
io::LmdbStream read_stream(db_path_, lmdb_env_, &lmdb_handle_, false);
REQUIRE(minifi::io::isError(writeString(read_stream, "anything")));
}
TEST_CASE_METHOD(LmdbStreamTest, "Writing nullptr with a non-zero length returns STREAM_ERROR") {
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE(minifi::io::isError(write_stream.write(static_cast<const uint8_t*>(nullptr), 4)));
}
TEST_CASE_METHOD(LmdbStreamTest, "Writing zero bytes is a no-op without error") {
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
const std::array<uint8_t, 1> dummy{};
REQUIRE(write_stream.write(dummy.data(), 0) == 0);
REQUIRE(write_stream.size() == 0);
}
TEST_CASE_METHOD(LmdbStreamTest, "commit on a read-only stream returns false") {
io::LmdbStream read_stream(db_path_, lmdb_env_, &lmdb_handle_, false);
REQUIRE_FALSE(read_stream.commit());
}
TEST_CASE_METHOD(LmdbStreamTest, "Repeated commit is a no-op after the first") {
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, "banana")));
REQUIRE(write_stream.commit());
REQUIRE_FALSE(write_stream.commit());
}
TEST_CASE_METHOD(LmdbStreamTest, "Destructor commits buffered writes") {
{
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, "destroyed-write")));
// Stream goes out of scope here; the destructor must commit
}
auto val = readValue(db_path_);
REQUIRE(val.has_value());
REQUIRE(*val == "destroyed-write");
}
TEST_CASE_METHOD(LmdbStreamTest, "Reopening an existing key in write mode appends to existing value") {
{
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, "first-")));
}
io::LmdbStream reopened_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE(reopened_stream.size() == 6);
REQUIRE_FALSE(minifi::io::isError(writeString(reopened_stream, "second")));
REQUIRE(reopened_stream.commit());
auto val = readValue(db_path_);
REQUIRE(val.has_value());
REQUIRE(*val == "first-second");
}
TEST_CASE_METHOD(LmdbStreamTest, "A non-append write overwrites a pre-existing value instead of concatenating") {
{
io::LmdbStream write_stream(db_path_, lmdb_env_, &lmdb_handle_, true);
REQUIRE_FALSE(minifi::io::isError(writeString(write_stream, "old conent")));
}
io::LmdbStream overwrite_stream(db_path_, lmdb_env_, &lmdb_handle_, true, false);
REQUIRE(overwrite_stream.size() == 0);
REQUIRE_FALSE(minifi::io::isError(writeString(overwrite_stream, "new content")));
REQUIRE(overwrite_stream.commit());
auto val = readValue(db_path_);
REQUIRE(val.has_value());
REQUIRE(*val == "new content");
}
} // namespace org::apache::nifi::minifi::test