Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
rope_test.cc
Go to the documentation of this file.
1
2/*
3 Aleph_w
4
5 Data structures & Algorithms
6 https://github.com/lrleon/Aleph-w
7
8 This file is part of Aleph-w library
9
10 Copyright (c) 2002-2026 Leandro Rabindranath Leon
11
12 Permission is hereby granted, free of charge, to any person obtaining a copy
13 of this software and associated documentation files (the "Software"), to deal
14 in the Software without restriction, including without limitation the rights
15 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 copies of the Software, and to permit persons to whom the Software is
17 furnished to do so, subject to the following conditions:
18
19 The above copyright notice and this permission notice shall be included in all
20 copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 SOFTWARE.
29*/
30
36#include <gtest/gtest.h>
37
38#include <tpl_rope.H>
39
40#include <atomic>
41#include <chrono>
42#include <cstddef>
43#include <cstdlib>
44#include <new>
45#include <random>
46#include <stdexcept>
47#include <string>
48#include <string_view>
49
50// `Rope<Char>::View` is `std::basic_string_view<Char>`, which the standard
51// library requires `Char` to be a trivial, standard-layout type for (see
52// `<string_view>`'s own static_assert) -- so, unlike e.g. `RadixTree<T>`'s
53// mapped value `T`, `Rope`'s `Char` can never be a type whose copy
54// constructor throws: a trivial copy constructor cannot run user code at
55// all. The only realistic exception source `Rope`'s methods have is
56// allocation failure (`std::bad_alloc`, already documented on every
57// `@throws`), so exception-safety here is tested via allocation-failure
58// injection instead of a throwing element type. This reuses the same
59// global operator new/delete override pattern already established in
60// `Tests/prefix_tree_test.cc` for the same purpose.
61#if defined(__SANITIZE_THREAD__)
62# define ALEPH_ROPE_TEST_UNDER_TSAN 1
63#elif defined(__has_feature)
64# if __has_feature(thread_sanitizer)
65# define ALEPH_ROPE_TEST_UNDER_TSAN 1
66# endif
67#endif
68#ifndef ALEPH_ROPE_TEST_UNDER_TSAN
69# define ALEPH_ROPE_TEST_UNDER_TSAN 0
70#endif
71
72namespace
73{
74 std::atomic<int> allocation_countdown{-1};
75 std::atomic<bool> track_allocations{false};
76 std::atomic<int> tracked_balance{0};
77
78#if !ALEPH_ROPE_TEST_UNDER_TSAN
79 bool should_fail_allocation() noexcept
80 {
81 int remaining = allocation_countdown.load(std::memory_order_relaxed);
82 while (remaining >= 0)
83 {
84 if (remaining == 0)
85 return true;
86 if (allocation_countdown.compare_exchange_weak(
87 remaining, remaining - 1, std::memory_order_relaxed))
88 return false;
89 }
90 return false;
91 }
92
93 void * allocate_or_throw(std::size_t size)
94 {
96 throw std::bad_alloc();
97
98 if (size == 0)
99 size = 1;
100
101 void * ptr = std::malloc(size);
102 if (ptr == nullptr)
103 throw std::bad_alloc();
104
105 if (track_allocations.load(std::memory_order_relaxed))
106 tracked_balance.fetch_add(1, std::memory_order_relaxed);
107
108 return ptr;
109 }
110
111 void release_allocation(void * ptr) noexcept
112 {
113 if (ptr == nullptr)
114 return;
115
116 if (track_allocations.load(std::memory_order_relaxed))
117 tracked_balance.fetch_sub(1, std::memory_order_relaxed);
118
119 std::free(ptr);
120 }
121#endif // !ALEPH_ROPE_TEST_UNDER_TSAN
122
129 class AllocationFailureScope
130 {
131 public:
132 explicit AllocationFailureScope(const int successful_allocations_before_failure)
133 {
134 tracked_balance.store(0, std::memory_order_relaxed);
135 track_allocations.store(true, std::memory_order_relaxed);
137 std::memory_order_relaxed);
138 }
139
141 {
142 allocation_countdown.store(-1, std::memory_order_relaxed);
143 track_allocations.store(false, std::memory_order_relaxed);
144 }
145
146 [[nodiscard]] int balance() const noexcept
147 {
148 return tracked_balance.load(std::memory_order_relaxed);
149 }
150 };
151} // namespace
152
153#if !ALEPH_ROPE_TEST_UNDER_TSAN
154
155void * operator new(std::size_t size)
156{
157 return allocate_or_throw(size);
158}
159
160void * operator new[](std::size_t size)
161{
162 return allocate_or_throw(size);
163}
164
165void * operator new(std::size_t size, const std::nothrow_t &) noexcept
166{
167 try
168 {
169 return allocate_or_throw(size);
170 }
171 catch (...)
172 {
173 return nullptr;
174 }
175}
176
177void * operator new[](std::size_t size, const std::nothrow_t &) noexcept
178{
179 try
180 {
181 return allocate_or_throw(size);
182 }
183 catch (...)
184 {
185 return nullptr;
186 }
187}
188
189void operator delete(void * ptr) noexcept
190{
192}
193
194void operator delete[](void * ptr) noexcept
195{
197}
198
199void operator delete(void * ptr, std::size_t) noexcept
200{
202}
203
204void operator delete[](void * ptr, std::size_t) noexcept
205{
207}
208
209void operator delete(void * ptr, const std::nothrow_t &) noexcept
210{
212}
213
214void operator delete[](void * ptr, const std::nothrow_t &) noexcept
215{
217}
218
219#endif // !ALEPH_ROPE_TEST_UNDER_TSAN
220
221using namespace Aleph;
222
223namespace
224{
225 // A tiny leaf size to force frequent splitting/rebalancing in tests
226 // without needing enormous input strings.
227 using TinyRope = Rope<char, 4>;
228} // namespace
229
231{
233 EXPECT_TRUE(r.is_empty());
234 EXPECT_EQ(r.size(), 0u);
235 EXPECT_EQ(r.to_string(), "");
236}
237
239{
241 EXPECT_TRUE(Rope<char>{std::string_view("x")}.verify());
242 EXPECT_TRUE(Rope<char>{std::string_view("hello world")}.verify());
243}
244
246{
247 Rope<char> r{std::string_view("hello world")};
248 EXPECT_FALSE(r.is_empty());
249 EXPECT_EQ(r.size(), 11u);
250 EXPECT_EQ(r.to_string(), "hello world");
251}
252
254{
255 Rope<char> r{std::string_view("")};
256 EXPECT_TRUE(r.is_empty());
257 EXPECT_EQ(r.size(), 0u);
258}
259
261{
262 Rope<char> r{std::string_view("abcdef")};
263 EXPECT_EQ(r.at(0), 'a');
264 EXPECT_EQ(r.at(5), 'f');
265 EXPECT_EQ(r.at(3), 'd');
266}
267
269{
270 Rope<char> r{std::string_view("abc")};
271 EXPECT_THROW(r.at(3), std::out_of_range);
272 EXPECT_THROW(r.at(100), std::out_of_range);
273
274 Rope<char> empty;
275 EXPECT_THROW(empty.at(0), std::out_of_range);
276}
277
279{
280 Rope<char> a{std::string_view("hello ")};
281 Rope<char> b{std::string_view("world")};
282 Rope<char> c = a.concat(b);
283
284 EXPECT_EQ(c.size(), 11u);
285 EXPECT_EQ(c.to_string(), "hello world");
286 // Originals are untouched (immutability).
287 EXPECT_EQ(a.to_string(), "hello ");
288 EXPECT_EQ(b.to_string(), "world");
289 EXPECT_TRUE(c.verify());
290}
291
293{
294 Rope<char> a{std::string_view("abc")};
295 Rope<char> empty;
296
297 EXPECT_EQ(a.concat(empty).to_string(), "abc");
298 EXPECT_EQ(empty.concat(a).to_string(), "abc");
300}
301
303{
304 Rope<char> r{std::string_view("hello world")};
305 EXPECT_EQ(r.substr(0, 5).to_string(), "hello");
306 EXPECT_EQ(r.substr(6, 5).to_string(), "world");
307 EXPECT_EQ(r.substr(0, 11).to_string(), "hello world");
308 EXPECT_EQ(r.substr(3, 0).to_string(), "");
309 EXPECT_EQ(r.substr(11, 0).to_string(), ""); // one-past-the-end, zero length
310}
311
313{
314 Rope<char> r{std::string_view("abc")};
315 EXPECT_THROW(r.substr(0, 4), std::out_of_range);
316 EXPECT_THROW(r.substr(4, 0), std::out_of_range);
317 EXPECT_THROW(r.substr(2, 5), std::out_of_range);
318}
319
321{
322 // Use a multi-leaf (internal-rooted) rope: with a single-leaf rope this
323 // test would pass even if `slice()` failed to take the whole-node
324 // sharing fast path for internal nodes (content would still come out
325 // right either way), which is exactly what happened before that fast
326 // path covered internal nodes too, not just leaves.
327 const std::string_view text = "the quick brown fox jumps over the lazy dog";
328 TinyRope r{text};
329 TinyRope whole = r.substr(0, r.size());
330 EXPECT_EQ(whole.to_string(), text);
331 EXPECT_TRUE(r.verify());
332 EXPECT_TRUE(whole.verify());
333}
334
336{
337 Rope<char> r{std::string_view("hello world")};
338 Rope<char> ins{std::string_view("brave new ")};
339 Rope<char> result = r.insert(6, ins);
340
341 EXPECT_EQ(result.to_string(), "hello brave new world");
342 EXPECT_EQ(r.to_string(), "hello world"); // original untouched
343 EXPECT_TRUE(result.verify());
344}
345
347{
348 Rope<char> r{std::string_view("world")};
349 EXPECT_EQ(r.insert(0, Rope<char>{std::string_view("hello ")}).to_string(),
350 "hello world");
351 EXPECT_EQ(r.insert(5, Rope<char>{std::string_view("!")}).to_string(),
352 "world!");
353}
354
356{
357 Rope<char> r{std::string_view("abc")};
358 EXPECT_THROW(r.insert(4, Rope<char>{std::string_view("x")}), std::out_of_range);
359}
360
362{
363 Rope<char> r{std::string_view("hello brave new world")};
364 Rope<char> result = r.erase(6, 10); // remove "brave new "
365
366 EXPECT_EQ(result.to_string(), "hello world");
367 EXPECT_EQ(r.to_string(), "hello brave new world"); // original untouched
368 EXPECT_TRUE(result.verify());
369}
370
372{
373 Rope<char> r{std::string_view("abc")};
374 EXPECT_TRUE(r.erase(0, 3).is_empty());
375}
376
378{
379 Rope<char> r{std::string_view("abc")};
380 EXPECT_EQ(r.erase(1, 0).to_string(), "abc");
381}
382
384{
385#if ALEPH_ROPE_TEST_UNDER_TSAN
386 GTEST_SKIP() << "AllocationFailureScope needs a custom global operator "
387 "new/delete, which conflicts with TSan's own at link time.";
388#else
389 const Rope<char> r{std::string_view("abcdef")};
390 const Rope<char> empty;
391
392 Rope<char> inserted;
393 int insert_balance = 0;
394 {
395 AllocationFailureScope fail_immediately(0);
396 EXPECT_NO_THROW(inserted = r.insert(3, empty));
398 }
400 EXPECT_EQ(inserted.to_string(), "abcdef");
401
403 int erase_balance = 0;
404 {
405 AllocationFailureScope fail_immediately(0);
406 EXPECT_NO_THROW(erased = r.erase(3, 0));
408 }
410 EXPECT_EQ(erased.to_string(), "abcdef");
411#endif // !ALEPH_ROPE_TEST_UNDER_TSAN
412}
413
415{
416 Rope<char> r{std::string_view("abc")};
417 EXPECT_THROW(r.erase(2, 5), std::out_of_range);
418 EXPECT_THROW(r.erase(4, 0), std::out_of_range);
419}
420
422{
423 Rope<char> r{std::string_view("abcdef")};
424 auto arr = r.flatten();
425 ASSERT_EQ(arr.size(), 6u);
426 const char expected[] = {'a', 'b', 'c', 'd', 'e', 'f'};
427 for (size_t i = 0; i < 6; ++i)
428 EXPECT_EQ(arr[i], expected[i]);
429}
430
432{
433 Rope<char> a{std::string_view("hello")};
434 Rope<char> b{std::string_view("hello")};
435 Rope<char> c{std::string_view("world")};
436 Rope<char> a_copy = a; // shares the same tree
437
438 EXPECT_TRUE(a == b); // same content, different trees
439 EXPECT_TRUE(a == a_copy); // same content, same tree (fast path)
440 EXPECT_FALSE(a == c);
441 EXPECT_FALSE(Rope<char>() == a);
443}
444
446{
447 // Same content, but built two different ways so the two trees have
448 // different shapes (misaligned leaf boundaries): the lock-step
449 // leaf-cursor comparison in operator== must handle a leaf pair that
450 // only partially overlaps, not just leaf-for-leaf-aligned trees like
451 // EqualityComparesContentNotIdentity above.
452 TinyRope whole{std::string_view("abcdefghijkl")}; // one build_from_view split
453 TinyRope pieced = TinyRope{std::string_view("ab")}
454 .concat(TinyRope{std::string_view("cde")})
455 .concat(TinyRope{std::string_view("fghij")})
456 .concat(TinyRope{std::string_view("kl")});
457 ASSERT_EQ(whole.to_string(), "abcdefghijkl");
458 ASSERT_EQ(pieced.to_string(), "abcdefghijkl");
461
462 // A one-character difference near the end must still be caught even
463 // though most of the content (and many leaf boundaries) matches.
464 TinyRope pieced_diff = TinyRope{std::string_view("ab")}
465 .concat(TinyRope{std::string_view("cde")})
466 .concat(TinyRope{std::string_view("fghij")})
467 .concat(TinyRope{std::string_view("kX")});
469
470 // Structural sharing partially aligned: a rope built by concatenating a
471 // shared sub-rope with different tails should still compare correctly
472 // whether or not the shared part lines up on leaf boundaries with the
473 // other side.
474 TinyRope shared_prefix{std::string_view("abcdef")};
475 TinyRope left = shared_prefix.concat(TinyRope{std::string_view("ghijkl")});
476 TinyRope right = shared_prefix.concat(TinyRope{std::string_view("ghijkl")});
477 EXPECT_TRUE(left == right);
478 EXPECT_TRUE(left.verify());
479 EXPECT_TRUE(right.verify());
480}
481
483{
484 Rope<char> a{std::string_view("hello")};
485 Rope<char> b = a;
486 a = a.concat(Rope<char>{std::string_view(" world")});
487
488 EXPECT_EQ(a.to_string(), "hello world");
489 EXPECT_EQ(b.to_string(), "hello"); // b is unaffected by reassigning a
490}
491
492// --- Leaf-splitting / small-LeafSize scenarios ------------------------------
493
495{
496 TinyRope r{std::string_view("the quick brown fox jumps over the lazy dog")};
497 EXPECT_EQ(r.size(), 43u);
498 EXPECT_EQ(r.to_string(), "the quick brown fox jumps over the lazy dog");
499 EXPECT_TRUE(r.verify());
500
501 auto sub = r.substr(4, 5); // "quick"
502 EXPECT_EQ(sub.to_string(), "quick");
503 EXPECT_TRUE(sub.verify());
504
505 auto ins = r.insert(10, TinyRope{std::string_view("very ")});
506 EXPECT_EQ(ins.to_string(),
507 "the quick very brown fox jumps over the lazy dog");
508 EXPECT_TRUE(ins.verify());
509
510 auto er = r.erase(4, 6); // remove "quick "
511 EXPECT_EQ(er.to_string(), "the brown fox jumps over the lazy dog");
512 EXPECT_TRUE(er.verify());
513}
514
516{
517 // Pathological case for a naive (unbalanced) rope: N concats of a
518 // single character each, worst case for depth. Confirms the explicit
519 // rebalancing keeps every operation correct even if not asymptotically
520 // optimal (see tpl_rope.H's "Rebalancing" note).
521 TinyRope r;
522 std::string expected;
523 for (int i = 0; i < 500; ++i)
524 {
525 const char c = static_cast<char>('a' + (i % 26));
526 r = r.concat(TinyRope{std::string_view(&c, 1)});
527 expected.push_back(c);
528 }
529
530 ASSERT_EQ(r.size(), expected.size());
531 EXPECT_EQ(r.to_string(), expected);
532 EXPECT_TRUE(r.verify());
533 for (size_t i = 0; i < expected.size(); ++i)
534 ASSERT_EQ(r.at(i), expected[i]) << "mismatch at index " << i;
535}
536
538{
539 // Mirror of RepeatedSingleCharacterConcatStaysCorrect, but prepending
540 // instead of appending: exercises the leaf-absorption fast path on the
541 // *left* side (try_absorb_left) instead of the right, repeatedly
542 // crossing the LeafSize boundary and falling back to a normal concat.
543 TinyRope r;
544 std::string expected;
545 for (int i = 0; i < 500; ++i)
546 {
547 const char c = static_cast<char>('a' + (i % 26));
548 r = TinyRope{std::string_view(&c, 1)}.concat(r);
549 expected.insert(expected.begin(), c);
550 }
551
552 ASSERT_EQ(r.size(), expected.size());
553 EXPECT_EQ(r.to_string(), expected);
554 EXPECT_TRUE(r.verify());
555 for (size_t i = 0; i < expected.size(); ++i)
556 ASSERT_EQ(r.at(i), expected[i]) << "mismatch at index " << i;
557}
558
560{
561 // LeafSize=4: "abcd|efgh|ijkl" if split naively; exercise pos/len that
562 // land exactly on those (implementation-internal) boundaries as well as
563 // straddling them.
564 TinyRope r{std::string_view("abcdefghijkl")};
565 EXPECT_EQ(r.substr(0, 4).to_string(), "abcd");
566 EXPECT_EQ(r.substr(4, 4).to_string(), "efgh");
567 EXPECT_EQ(r.substr(2, 4).to_string(), "cdef"); // straddles a boundary
568 EXPECT_EQ(r.substr(0, 12).to_string(), "abcdefghijkl");
569 EXPECT_TRUE(r.verify());
570}
571
572// --- Randomized parity against std::string ----------------------------------
573
575{
576 std::mt19937 rng(0xC0FFEEu);
577 std::uniform_int_distribution<int> op_dist(0, 3); // concat/insert/erase/substr-roundtrip
578 std::uniform_int_distribution<int> char_dist('a', 'z');
579
580 using SmallLeafRope = Rope<char, 8>; // small leaf: forces frequent splits
581
583 std::string reference;
584
585 const auto random_string = [&](const int len)
586 {
587 std::string s;
588 for (int i = 0; i < len; ++i)
589 s.push_back(static_cast<char>(char_dist(rng)));
590 return s;
591 };
592
593 std::uniform_int_distribution<int> len_dist(1, 6);
594
595 for (int iter = 0; iter < 2000; ++iter)
596 {
597 const int op = reference.empty() ? 0 : op_dist(rng);
598
599 if (op == 0)
600 {
601 // concat a random chunk at the end.
602 const std::string chunk = random_string(len_dist(rng));
603 subject = subject.concat(SmallLeafRope{std::string_view(chunk)});
604 reference += chunk;
605 }
606 else if (op == 1)
607 {
608 // insert a random chunk at a random position.
609 std::uniform_int_distribution<size_t> pos_dist(0, reference.size());
610 const size_t pos = pos_dist(rng);
611 const std::string chunk = random_string(len_dist(rng));
612 subject = subject.insert(pos, SmallLeafRope{std::string_view(chunk)});
613 reference.insert(pos, chunk);
614 }
615 else if (op == 2)
616 {
617 // erase a random range.
618 std::uniform_int_distribution<size_t> pos_dist(0, reference.size());
619 const size_t pos = pos_dist(rng);
620 std::uniform_int_distribution<size_t> len_d(0, reference.size() - pos);
621 const size_t len = len_d(rng);
622 subject = subject.erase(pos, len);
623 reference.erase(pos, len);
624 }
625 else
626 {
627 // substr round-trip: extract, then rebuild reference the same
628 // way, verifying substr() against std::string::substr().
629 std::uniform_int_distribution<size_t> pos_dist(0, reference.size());
630 const size_t pos = pos_dist(rng);
631 std::uniform_int_distribution<size_t> len_d(0, reference.size() - pos);
632 const size_t len = len_d(rng);
633 ASSERT_EQ(subject.substr(pos, len).to_string(), reference.substr(pos, len))
634 << "substr disagreement at iter " << iter;
635 }
636
637 ASSERT_EQ(subject.size(), reference.size()) << "size disagreement at iter " << iter;
638 ASSERT_EQ(subject.to_string(), reference) << "content disagreement at iter " << iter;
639 ASSERT_TRUE(subject.verify()) << "invariant violation at iter " << iter;
640 }
641}
642
643// --- Leaf-absorption fast-path regression --------------------------------
644
646{
647 // Timing-based assertions are inherently flaky under CI conditions
648 // (debug builds, sanitizers, shared/throttled runners), so -- matching
649 // the ENABLE_PERF_TESTS convention already used across this repo (see
650 // e.g. Tests/math_nt_test.cc, Tests/ntt_test.cc) -- this is opt-in
651 // rather than run unconditionally.
652 if (not std::getenv("ENABLE_PERF_TESTS"))
653 GTEST_SKIP() << "Skipping Rope performance regression (set "
654 "ENABLE_PERF_TESTS=1 to enable)";
655
656 // Correctness alone doesn't prove the leaf-absorption fast path
657 // (try_absorb_right/try_absorb_left in concat_nodes) is actually being
658 // taken: RepeatedSingleCharacterConcatStaysCorrect and
659 // RepeatedSinglePrependStaysCorrect above would still pass, just much
660 // more slowly, even if that fast path were silently deleted, since the
661 // general concat+rebalance path is still correct on its own (see
662 // tpl_rope.H's "Leaf absorption" note for the measured ~550x effect).
663 // This test instead asserts a generous wall-clock time budget for a
664 // pattern that is specifically pathological *without* the fast path
665 // (many single-character concats), so a regression that silently
666 // removes or breaks absorption is expected to blow well past it.
668 constexpr int N = 20000;
669
670 const auto start = std::chrono::steady_clock::now();
672 for (int i = 0; i < N; ++i)
673 {
674 const char c = static_cast<char>('a' + (i % 26));
675 r = r.concat(SmallLeafRope{std::string_view(&c, 1)});
676 }
677 const auto elapsed = std::chrono::steady_clock::now() - start;
678
679 EXPECT_EQ(r.size(), static_cast<size_t>(N));
680 EXPECT_TRUE(r.verify());
681 // With absorption active this runs in ~10-20ms; measured ~1.7s for the
682 // same N with absorption forcibly disabled during development of this
683 // test (confirming it actually catches a regression). 400ms leaves
684 // roughly 20x slack above the healthy case and roughly 4x margin below
685 // the broken one -- generous enough for a loaded CI machine without
686 // losing the ability to fail loudly on a real regression. Overridable
687 // via ROPE_ABSORPTION_MAX_MS for machines where even that isn't enough
688 // (matches the ad-hoc TIMSORT_MAX_MS pattern in Tests/sort_utils.cc).
689 long max_ms = 400;
690 if (const char * env_ms = std::getenv("ROPE_ABSORPTION_MAX_MS"))
691 max_ms = std::atol(env_ms);
692 EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count(), max_ms)
693 << N << " single-character concats took too long -- the leaf-absorption "
694 "fast path may have regressed (see tpl_rope.H's \"Leaf absorption\" note).";
695}
696
697// --- Structural sharing (copy is O(1), independent of source size) -------
698
700{
701 // Opt-in only: see the matching comment on
702 // RepeatedSmallConcatStaysFastEnoughToProveAbsorptionFired above.
703 if (not std::getenv("ENABLE_PERF_TESTS"))
704 GTEST_SKIP() << "Skipping Rope performance regression (set "
705 "ENABLE_PERF_TESTS=1 to enable)";
706
707 // Structural-sharing proof by timing: content-only tests (like
708 // CopyIsIndependentOfLaterOperationsOnTheOriginalVariable above) prove
709 // a copy is *independent*, but not that it is *cheap* -- a rope whose
710 // copy constructor accidentally started deep-cloning the tree would
711 // still pass every content-based test, just far more slowly. Building
712 // one large rope once, then doing many independent copy+small-concat
713 // derivations from it, should cost roughly the same per derivation
714 // (shared_ptr refcount bump + a short leaf-absorption/wrap) regardless
715 // of how large the shared source tree is -- an O(size) copy would blow
716 // the budget below by orders of magnitude.
717 using R = Rope<char, 256>;
718 const std::string big_text(200000, 'x');
719 R big{std::string_view(big_text)};
720 ASSERT_TRUE(big.verify());
721
722 constexpr int N = 5000;
723 const auto start = std::chrono::steady_clock::now();
724 for (int i = 0; i < N; ++i)
725 {
726 R copy = big; // O(1): shares the tree
727 R derived = copy.concat(R{std::string_view("!")}); // small, cheap addition
728 ASSERT_EQ(derived.size(), big.size() + 1);
729 }
730 const auto elapsed = std::chrono::steady_clock::now() - start;
731
732 // Overridable via ROPE_SHARING_MAX_MS for slow/loaded machines (see the
733 // matching override on the absorption regression test above).
734 long max_ms = 500;
735 if (const char * env_ms = std::getenv("ROPE_SHARING_MAX_MS"))
736 max_ms = std::atol(env_ms);
737 EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count(), max_ms)
738 << N << " copy+small-concat derivations from a " << big_text.size()
739 << "-character rope took too long -- copy() or concat() may no longer be "
740 "O(1)/structurally sharing (see tpl_rope.H's \"Structure\" note).";
741}
742
743// --- Exception safety on allocation failure -------------------------------
744
746{
747#if ALEPH_ROPE_TEST_UNDER_TSAN
748 GTEST_SKIP() << "AllocationFailureScope needs a custom global operator "
749 "new/delete, which conflicts with TSan's own at link time.";
750#else
751 // Build an internal-rooted `left` whose rightmost leaf ("ef") has spare
752 // room: "abcd" (4 chars) is already a full leaf at LeafSize=4, so
753 // concat-ing "ef" can't absorb and falls back to make_internal,
754 // producing one internal node over two leaves.
755 const TinyRope left =
756 TinyRope{std::string_view("abcd")}.concat(TinyRope{std::string_view("ef")});
757 const TinyRope right{std::string_view("g")};
758 ASSERT_TRUE(left.verify());
759 ASSERT_EQ(left.to_string(), "abcdef");
760
761 // Absorbing "g" into `left` walks one level of the right spine: it
762 // allocates the new merged leaf "efg" (try_absorb_right's base case,
763 // via make_leaf -- one allocation for the Node, one for its
764 // heap-indirected leaf_data, see the Node struct's own comment on why
765 // leaf storage isn't embedded inline), then allocates one new internal
766 // node wrapping the *unchanged* "abcd" leaf and the new "efg" leaf
767 // (try_absorb_right's recursive case) -- three allocations total.
768 // Failing after 0, 1, and 2 successful allocations exercises every
769 // point along that chain: no work done yet, the leaf's Node built but
770 // not its data, and the whole leaf built but not the wrapper.
771 for (int allowed = 0; allowed <= 2; ++allowed)
772 {
773 bool threw = false;
774 int balance = 0;
775 {
776 AllocationFailureScope fail_after(allowed);
777 try
778 {
779 [[maybe_unused]] const TinyRope result = left.concat(right);
780 }
781 catch (const std::bad_alloc &)
782 {
783 threw = true;
784 }
785 balance = fail_after.balance();
786 }
787 EXPECT_TRUE(threw) << "allowed=" << allowed;
788 EXPECT_EQ(balance, 0) << "allocation leaked with allowed=" << allowed;
789
790 // Neither operand was mutated by the failed attempt: concat() only
791 // ever builds brand-new nodes and reaches existing ones via
792 // shared_ptr copies, so a mid-construction throw has nothing to
793 // unwind on either side.
794 EXPECT_TRUE(left.verify());
795 EXPECT_TRUE(right.verify());
796 EXPECT_EQ(left.to_string(), "abcdef");
797 EXPECT_EQ(right.to_string(), "g");
798 }
799
800 // With enough budget, the same operation succeeds normally afterwards --
801 // no leftover poisoned state from the earlier failed attempts.
802 const TinyRope combined = left.concat(right);
803 EXPECT_TRUE(combined.verify());
804 EXPECT_EQ(combined.to_string(), "abcdefg");
805#endif // !ALEPH_ROPE_TEST_UNDER_TSAN
806}
807
808// --- substr() on ranges spanning many leaves ------------------------------
809
811{
812 // Opt-in only: see the matching comment on
813 // RepeatedSmallConcatStaysFastEnoughToProveAbsorptionFired above. This
814 // test also builds multi-megabyte ropes, which is itself costly to run
815 // unconditionally in every default CI run; the underlying correctness
816 // property (substr() on multi-leaf ranges) is already covered by
817 // RandomizedEditScriptMatchesStdString and SubstrAtExactLeafBoundaries.
818 if (not std::getenv("ENABLE_PERF_TESTS"))
819 GTEST_SKIP() << "Skipping Rope performance regression (set "
820 "ENABLE_PERF_TESTS=1 to enable)";
821
822 // substr()'s straddle path (see tpl_rope.H's own @note on the method)
823 // can, in the worst case, cost up to O(range's leaf count) rather than
824 // a strict O(log size()) if the rejoin triggers a rebalance. Extracting
825 // a fixed-size range that spans many leaves (~40 at LeafSize=256)
826 // should still be correct and should not scale with the *total* rope
827 // size -- only with the extracted range itself.
828 using R = Rope<char, 256>;
829 constexpr size_t extract_len = 10000; // spans ~40 leaves
830
831 for (const int total : {100000, 1000000, 5000000})
832 {
833 std::string text(static_cast<size_t>(total), 'x');
834 std::mt19937 rng(42);
835 for (auto & c : text)
836 c = static_cast<char>('a' + (rng() % 26));
837 R r{std::string_view(text)};
838 ASSERT_TRUE(r.verify());
839
840 const size_t pos = static_cast<size_t>(total) / 2;
841 const auto start = std::chrono::steady_clock::now();
842 for (int i = 0; i < 200; ++i)
843 {
844 R sub = r.substr(pos, extract_len);
845 ASSERT_EQ(sub.size(), extract_len);
846 ASSERT_EQ(sub.to_string(), text.substr(pos, extract_len));
847 }
848 const double ms = std::chrono::duration<double, std::milli>(
849 std::chrono::steady_clock::now() - start)
850 .count();
851 // Loose bound: 200 extractions of a many-leaf range should never
852 // take anywhere close to a second, regardless of how large the
853 // source rope is. Overridable for slow/loaded machines.
854 long max_ms = 2000;
855 if (const char * env_ms = std::getenv("ROPE_SLICE_MAX_MS"))
856 max_ms = std::atol(env_ms);
857 EXPECT_LT(ms, static_cast<double>(max_ms))
858 << "200x substr(len=" << extract_len << ") from a " << total
859 << "-character rope took too long -- see tpl_rope.H substr()'s "
860 "@note on the straddle-rebalance worst case.";
861 }
862}
863
864// --- Length overflow via structural sharing -------------------------------
865
867{
868 // Structural sharing makes an astronomically large logical length
869 // reachable with very little real work: `r = r.concat(r)` doubles
870 // size() each call at O(1) real cost (both sides already share the same
871 // subtree), so this reaches SIZE_MAX in ~64 fast iterations -- unlike a
872 // naive "you'd need exabytes of real data" argument, which only holds
873 // without sharing. Left unchecked, the wrapped length can desync from
874 // the real depth and make a later maybe_rebalance() call collect_leaves
875 // on an exponentially-shared structure (2^depth root-to-leaf paths over
876 // O(depth) real nodes) instead of throwing promptly here.
877 Rope<char> r{std::string_view("x")};
878 size_t iterations = 0;
880 {
881 for (; iterations < 100; ++iterations)
882 r = r.concat(r);
883 },
884 std::overflow_error);
885 // Reached the limit well before the loop's own safety cap, and the last
886 // successful doubling left size() consistent with that many iterations.
887 //
888 // Deliberately NOT calling r.verify() here: verify_rec(), like
889 // collect_leaves(), does not deduplicate shared nodes either, so it
890 // would walk this self-shared tree's ~2^60 root-to-leaf paths instead
891 // of its ~60 distinct nodes -- the same exponential-blowup shape this
892 // test's own overflow guard exists to prevent one level up, just not
893 // (yet) guarded against inside verify()/collect_leaves() themselves.
894 EXPECT_LT(iterations, 100u);
895 EXPECT_EQ(r.size(), size_t{1} << iterations);
896}
static string random_string(std::mt19937 &rng, size_t len)
Immutable, structurally-shared rope over a sequence of Char.
Definition tpl_rope.H:176
Rope erase(const size_t pos, const size_t len) const
Return a new rope with [pos, pos+len) removed.
Definition tpl_rope.H:775
Rope insert(const size_t pos, const Rope &other) const
Return a new rope with other inserted at pos.
Definition tpl_rope.H:753
Array< Char > flatten() const
Return every character of this rope as an independent Array.
Definition tpl_rope.H:790
Rope concat(const Rope &other) const
Return a new rope that is *this followed by other.
Definition tpl_rope.H:708
std::basic_string< Char > to_string() const
Return every character of this rope as a std::basic_string.
Definition tpl_rope.H:813
size_t size() const noexcept
Return the number of characters in this rope.
Definition tpl_rope.H:661
bool verify() const noexcept
Check this rope's internal structural invariants.
Definition tpl_rope.H:927
Char at(const size_t pos) const
Return the character at pos.
Definition tpl_rope.H:683
Minimal std::expected-style result type for C++20.
#define TEST(name)
static mt19937 rng
#define N
Definition fib.C:294
size_t blossom_maximum_cardinality_matching(const GT &g, DynDlist< typename GT::Arc * > &matching, SA sa=SA())
Alias of compute_maximum_cardinality_general_matching().
Definition Blossom.H:466
void verify()
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
size_t size(Node *root) noexcept
std::string concat(const Args &...args)
Concatenate multiple arguments into a single std::string.
Itor2 copy(Itor1 sourceBeg, const Itor1 &sourceEnd, Itor2 destBeg)
Copy elements from one range to another.
Definition ahAlgo.H:584
Itor::difference_type count(const Itor &beg, const Itor &end, const T &value)
Count elements equal to a value.
Definition ahAlgo.H:127
gsl_rng * r
Immutable, structurally-shared rope (Aleph::Rope) for large character sequences.