xrpld
Loading...
Searching...
No Matches
tests/libxrpl/nodestore/NuDBFactory.cpp
1#include <xrpl/basics/ByteUtilities.h>
2#include <xrpl/basics/FileUtilities.h>
3#include <xrpl/beast/utility/Journal.h>
4#include <xrpl/config/BasicConfig.h>
5#include <xrpl/nodestore/DummyScheduler.h>
6#include <xrpl/nodestore/Manager.h>
7
8#include <gtest/gtest.h>
9#include <helpers/CaptureSink.h>
10#include <helpers/TestSink.h>
11#include <nodestore/TestBase.h>
12
13#include <array>
14#include <cstddef>
15#include <exception>
16#include <memory>
17#include <string>
18#include <utility>
19#include <vector>
20
21namespace xrpl::node_store {
22
23namespace {
24
25Section
26makeSection(std::string const& path, std::string const& blockSize = "")
27{
28 Section params;
29 params.set("type", "nudb");
30 params.set("path", path);
31 if (!blockSize.empty())
32 params.set("nudb_block_size", blockSize);
33 return params;
34}
35
36void
37runRoundTrip(Section const& params, std::size_t expectedBlocksize)
38{
39 DummyScheduler scheduler;
40 beast::Journal const journal(TestSink::instance());
41 auto backend = Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
42
43 ASSERT_TRUE(backend);
44 ASSERT_EQ(backend->getBlockSize(), expectedBlocksize);
45 backend->open();
46 ASSERT_TRUE(backend->isOpen());
47
48 auto const batch = createPredictableBatch(10, 12345);
49 storeBatch(*backend, batch);
50
51 auto const copy = fetchCopyOfBatch(*backend, batch);
52
53 backend->close();
54 EXPECT_EQ(batch, copy);
55}
56
57} // namespace
58
59TEST(NuDBFactory, default_block_size)
60{
61 TempDir const tempDir;
62 auto const params = makeSection(tempDir.path());
63 ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096));
64}
65
66TEST(NuDBFactory, valid_block_sizes)
67{
68 auto const kValidSizes = std::to_array<std::size_t>({4096, 8192, 16384, 32768});
69 for (auto const size : kValidSizes)
70 {
71 SCOPED_TRACE("size=" + std::to_string(size));
72 TempDir const tempDir;
73 auto const params = makeSection(tempDir.path(), std::to_string(size));
74 ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, size));
75 }
76
77 // empty value is ignored by config parser; default (4096) is used
78 {
79 TempDir const tempDir;
80 auto const params = makeSection(tempDir.path(), "");
81 ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096));
82 }
83}
84
85TEST(NuDBFactory, invalid_block_sizes)
86{
87 std::vector<std::string> const kInvalidSizes = {
88 "2048", // too small
89 "1024", // too small
90 "65536", // too large
91 "131072", // too large
92 "5000", // not power of 2
93 "6000", // not power of 2
94 "10000", // not power of 2
95 "0", // zero
96 "-1", // negative
97 "abc", // non-numeric
98 "4k", // invalid format
99 "4096.5"}; // decimal
100
101 for (auto const& size : kInvalidSizes)
102 {
103 SCOPED_TRACE("size='" + size + "'");
104 TempDir const tempDir;
105 auto const params = makeSection(tempDir.path(), size);
106 EXPECT_THROW(runRoundTrip(params, 4096), std::exception);
107 }
108
109 // whitespace handling — lexical_cast may or may not strip; treat as invalid
110 std::vector<std::string> const kWhitespaceSizes = {"4096 ", " 4096"};
111 for (auto const& size : kWhitespaceSizes)
112 {
113 SCOPED_TRACE("size='" + size + "'");
114 TempDir const tempDir;
115 auto const params = makeSection(tempDir.path(), size);
116 EXPECT_THROW(runRoundTrip(params, 4096), std::exception);
117 }
118}
119
120TEST(NuDBFactory, log_messages)
121{
122 // valid custom block size emits info log
123 {
124 TempDir const tempDir;
125 auto const params = makeSection(tempDir.path(), "8192");
127 beast::Journal const journal(sink);
128
129 DummyScheduler scheduler;
130 [[maybe_unused]] auto backend =
131 Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
132
133 EXPECT_TRUE(sink.messages().contains("Using custom NuDB block size: 8192"));
134 }
135
136 // invalid block size throws with informative message
137 {
138 TempDir const tempDir;
139 auto const params = makeSection(tempDir.path(), "5000");
141 beast::Journal const journal(sink);
142 DummyScheduler scheduler;
143 try
144 {
145 auto backend =
146 Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
147 FAIL() << "expected exception for invalid block size 5000";
148 }
149 catch (std::exception const& e)
150 {
151 std::string const what{e.what()};
152 EXPECT_TRUE(what.contains("Invalid nudb_block_size: 5000"));
153 EXPECT_TRUE(what.contains("Must be power of 2 between 4096 and 32768"));
154 }
155 }
156
157 // non-numeric value throws
158 {
159 TempDir const tempDir;
160 auto const params = makeSection(tempDir.path(), "invalid");
162 beast::Journal const journal(sink);
163 DummyScheduler scheduler;
164 try
165 {
166 auto backend =
167 Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
168 FAIL() << "expected exception for non-numeric block size";
169 }
170 catch (std::exception const& e)
171 {
172 std::string const what{e.what()};
173 EXPECT_TRUE(what.contains("Invalid nudb_block_size value: invalid"));
174 }
175 }
176}
177
178TEST(NuDBFactory, power_of_two_validation)
179{
181 {"4095", false}, // just below minimum
182 {"4096", true}, // minimum valid
183 {"4097", false}, // not power of 2
184 {"8192", true}, // valid power of 2
185 {"8193", false}, // not power of 2
186 {"16384", true}, // valid power of 2
187 {"32768", true}, // maximum valid
188 {"32769", false}, // just above maximum
189 {"65536", false}}; // power of 2 but too large
190
191 for (auto const& [size, shouldWork] : kCASES)
192 {
193 SCOPED_TRACE("size=" + size + " shouldWork=" + (shouldWork ? "true" : "false"));
194 TempDir const tempDir;
195 auto const params = makeSection(tempDir.path(), size);
197 beast::Journal const journal(sink);
198 DummyScheduler scheduler;
199 try
200 {
201 auto backend =
202 Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
203 EXPECT_TRUE(shouldWork);
204 }
205 catch (std::exception const& e)
206 {
207 // A throw is only expected for sizes that should NOT work; if a
208 // valid size throws, fail here instead of silently matching the
209 // message below (which would mask the regression).
210 EXPECT_FALSE(shouldWork);
211 std::string const what{e.what()};
212 EXPECT_TRUE(what.contains("Invalid nudb_block_size"));
213 }
214 }
215}
216
217TEST(NuDBFactory, both_constructor_variants)
218{
219 TempDir const tempDir;
220 auto const params = makeSection(tempDir.path(), "16384");
221 DummyScheduler scheduler;
222 beast::Journal const journal(TestSink::instance());
223
224 auto backend1 = Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
225 EXPECT_NE(backend1, nullptr);
226 ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 16384));
227
228 // Test second constructor (with nudb::context)
229 // Note: This would require access to nudb::context, which might not be
230 // easily testable without more complex setup. For now, we test that
231 // the factory can create backends with the first constructor.
232}
233
234TEST(NuDBFactory, configuration_parsing)
235{
236 // basic valid format emits success log
237 {
238 TempDir const tempDir;
239 auto const params = makeSection(tempDir.path(), "8192");
241 beast::Journal const journal(sink);
242 DummyScheduler scheduler;
243 [[maybe_unused]] auto backend =
244 Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
245 EXPECT_TRUE(sink.messages().contains("Using custom NuDB block size"));
246 }
247
248 // Test whitespace handling separately since lexical_cast behavior may vary
249 std::vector<std::string> const kWhitespaceFormats = {" 8192", "8192 "};
250 for (auto const& format : kWhitespaceFormats)
251 {
252 SCOPED_TRACE("format='" + format + "'");
253 TempDir const tempDir;
254 auto const params = makeSection(tempDir.path(), format);
256 beast::Journal const journal(sink);
257 DummyScheduler scheduler;
258 EXPECT_ANY_THROW(Manager::instance().makeBackend(params, megabytes(4), scheduler, journal));
259 }
260}
261
262TEST(NuDBFactory, data_persistence)
263{
264 std::vector<std::string> const kBlockSizes = {"4096", "8192", "16384", "32768"};
265 for (auto const& size : kBlockSizes)
266 {
267 SCOPED_TRACE("size=" + size);
268 TempDir const tempDir;
269 auto const params = makeSection(tempDir.path(), size);
270 DummyScheduler scheduler;
271 beast::Journal const journal(TestSink::instance());
272
273 // Create test data
274 auto const batch = createPredictableBatch(50, 54321);
275
276 // Store data
277 {
278 auto backend =
279 Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
280 backend->open();
281 storeBatch(*backend, batch);
282 backend->close();
283 }
284
285 // Retrieve data in new backend instance
286 {
287 auto backend =
288 Manager::instance().makeBackend(params, megabytes(4), scheduler, journal);
289 backend->open();
290 auto const copy = fetchCopyOfBatch(*backend, batch);
291 EXPECT_EQ(batch, copy);
292 backend->close();
293 }
294 }
295}
296
297} // namespace xrpl::node_store
A generic endpoint for log messages.
Definition Journal.h:44
RAII temporary directory.
std::string path() const
Get the native path for the temporary directory.
static TestSink & instance()
Definition TestSink.h:12
Simple NodeStore Scheduler that just performs the tasks synchronously.
virtual std::unique_ptr< Backend > makeBackend(Section const &parameters, std::size_t burstSize, Scheduler &scheduler, beast::Journal journal)=0
Create a backend.
static Manager & instance()
Returns the instance of the manager singleton.
std::string messages() const
Definition CaptureSink.h:41
T copy(T... args)
TEST(NodeStoreBasics, predictable_batches)
Definition Basics.cpp:14
Batch createPredictableBatch(std::size_t numObjects, std::uint64_t seed)
Definition TestBase.h:48
void storeBatch(Backend &backend, Batch const &batch)
Definition TestBase.h:85
Batch fetchCopyOfBatch(Backend &backend, Batch const &batch)
Definition TestBase.h:92
constexpr auto megabytes(T value) noexcept
T to_string(T... args)
T what(T... args)