real-regex/tests/engine/test_exact_literal_batch.cpp
Line | Count | Source |
1 | | // The exact-literal route batched, which is a REOPENED REFUSAL: the filler was written, measured and |
2 | | // refused once (see pike.hpp's fill_exact_literal_spans for the record and for what reopened it). The |
3 | | // first attempt was never wrong -- exhaustive-compat was byte-identical and a both-ways differential |
4 | | // agreed on every span -- so these tests are not what settled it then and are not what settles it now. |
5 | | // They exist so that the routing change cannot go wrong silently while the performance question is being |
6 | | // re-decided. |
7 | | // |
8 | | // The seam is class_fastpath_disabled, which takes BATCHING out (it gates every route's eligibility in |
9 | | // decide_batching) without touching the exact-literal route itself in run(): that gate has no such |
10 | | // check. So the two sides here are batched-walk against per-match-walk on the same route, which is |
11 | | // exactly the comparison a batch filler needs. |
12 | | #include <string> |
13 | | #include <vector> |
14 | | |
15 | | #include <sciforge/test/framework.hpp> |
16 | | #include "real/automata/lazy_dfa.hpp" // class_fastpath_disabled |
17 | | #include "real/real.hpp" |
18 | | |
19 | | namespace { |
20 | | |
21 | | using spans = std::vector<std::pair<std::size_t, std::size_t>>; |
22 | | |
23 | | spans walk(const real::regex& rx, |
24 | | std::string_view t) |
25 | 53 | { |
26 | 53 | spans v; |
27 | 4.34k | for (const auto& m : rx.find_iter(t)) { |
28 | 4.34k | v.emplace_back(m.start(0), m.end(0)); |
29 | 4.34k | } |
30 | 53 | return v; |
31 | 53 | } |
32 | | |
33 | | //! \brief The spans of a batched walk, and of the same walk with batching disabled. |
34 | | std::pair<spans, spans> both_ways(std::string_view pattern, |
35 | | std::string_view subject) |
36 | 25 | { |
37 | 25 | const real::regex rx {pattern}; |
38 | 25 | const spans batched {walk(rx, subject)}; |
39 | 25 | real::detail::class_fastpath_disabled() = true; |
40 | 25 | const real::regex per_match_rx {pattern}; |
41 | 25 | const spans per_match {walk(per_match_rx, subject)}; |
42 | 25 | real::detail::class_fastpath_disabled() = false; |
43 | 25 | return {batched, per_match}; |
44 | 25 | } |
45 | | |
46 | | std::string repeat_to(std::string_view unit, |
47 | | std::size_t len) |
48 | 3 | { |
49 | 3 | std::string s; |
50 | 423 | while (s.size() < len) { |
51 | 420 | s += unit; |
52 | 420 | } |
53 | 3 | return s; |
54 | 3 | } |
55 | | } // namespace |
56 | | |
57 | | TEST(exact_literal_batch_agrees_with_the_per_match_walk) |
58 | 1 | { |
59 | 1 | const std::string subject {repeat_to("the quick brown fox jumps over the lazy dog charlie ", 8192)}; |
60 | 5 | for (const char* pat : {"charlie", "dog", "the", "quick brown", "zz"}) { |
61 | 5 | const auto [batched, per_match] {both_ways(pat, subject)}; |
62 | 5 | EXPECT(batched == per_match); |
63 | 5 | } |
64 | 1 | } |
65 | | |
66 | | TEST(exact_literal_batch_handles_the_boundaries_of_the_buffer) |
67 | 1 | { |
68 | | // batch_cap is 4, so a subject with 1, 2, 3, 4, 5 and 9 occurrences crosses the refill boundary in |
69 | | // every position: exactly at it, one short, and one past. |
70 | 1 | for (const std::size_t count : {std::size_t {1}, std::size_t {2}, std::size_t {3}, |
71 | 6 | std::size_t {4}, std::size_t {5}, std::size_t {9}}) { |
72 | 6 | std::string subject {"lead "}; |
73 | 30 | for (std::size_t i = 0; i < count; ++i24 ) { |
74 | 24 | subject += "charlie gap "; |
75 | 24 | } |
76 | 6 | subject += "tail"; |
77 | 6 | const auto [batched, per_match] {both_ways("charlie", subject)}; |
78 | 6 | EXPECT(batched == per_match); |
79 | 6 | EXPECT(batched.size() == count); |
80 | 6 | } |
81 | 1 | } |
82 | | |
83 | | TEST(exact_literal_batch_handles_adjacent_and_overlapping_occurrences) |
84 | 1 | { |
85 | | // Back-to-back occurrences, and a literal whose own bytes overlap ("aa" in "aaaa" must yield the |
86 | | // non-overlapping [0,2) and [2,4), not three matches). |
87 | 1 | const auto [b1, p1] {both_ways("charlie", "charliecharliecharliecharlie")}; |
88 | 1 | EXPECT(b1 == p1); |
89 | 1 | EXPECT(b1.size() == 4); |
90 | 1 | const auto [b2, p2] {both_ways("aa", "aaaaaaaaa")}; |
91 | 1 | EXPECT(b2 == p2); |
92 | 1 | EXPECT(b2.size() == 4); // 9 bytes, non-overlapping pairs |
93 | 1 | const auto [b3, p3] {both_ways("aba", "abababa")}; |
94 | 1 | EXPECT(b3 == p3); |
95 | 1 | EXPECT(b3.size() == 2); // [0,3) and [4,7) -- the middle overlap is not a match |
96 | 1 | } |
97 | | |
98 | | TEST(exact_literal_batch_handles_the_subject_edges) |
99 | 1 | { |
100 | 1 | const auto [b1, p1] {both_ways("charlie", "charlie")}; |
101 | 1 | EXPECT(b1 == p1); |
102 | 1 | EXPECT(b1.size() == 1); |
103 | 1 | const auto [b2, p2] {both_ways("charlie", "xcharlie")}; |
104 | 1 | EXPECT(b2 == p2); |
105 | 1 | const auto [b3, p3] {both_ways("charlie", "charliex")}; |
106 | 1 | EXPECT(b3 == p3); |
107 | 1 | const auto [b4, p4] {both_ways("charlie", "")}; |
108 | 1 | EXPECT(b4 == p4); |
109 | 1 | EXPECT(b4.empty()); |
110 | 1 | const auto [b5, p5] {both_ways("charlie", "charli")}; |
111 | 1 | EXPECT(b5 == p5); |
112 | 1 | EXPECT(b5.empty()); |
113 | 1 | } |
114 | | |
115 | | TEST(exact_literal_batch_agrees_across_every_enumerating_surface) |
116 | 1 | { |
117 | 1 | const std::string subject {repeat_to("alpha charlie beta charlie gamma ", 4096)}; |
118 | 1 | const char* pat {"charlie"}; |
119 | | |
120 | 1 | const real::regex rx {pat}; |
121 | 1 | const std::size_t n_count {rx.count_matches(subject)}; |
122 | 1 | const std::size_t n_all {rx.find_all(subject).size()}; |
123 | 1 | const std::size_t n_iter {walk(rx, subject).size()}; |
124 | 1 | const std::string replaced {rx.replace(subject, "#")}; |
125 | 1 | const std::size_t n_split {rx.split(subject).size()}; |
126 | | |
127 | 1 | real::detail::class_fastpath_disabled() = true; |
128 | 1 | const real::regex core_rx {pat}; |
129 | 1 | const std::size_t c_count {core_rx.count_matches(subject)}; |
130 | 1 | const std::size_t c_all {core_rx.find_all(subject).size()}; |
131 | 1 | const std::size_t c_iter {walk(core_rx, subject).size()}; |
132 | 1 | const std::string c_replaced {core_rx.replace(subject, "#")}; |
133 | 1 | const std::size_t c_split {core_rx.split(subject).size()}; |
134 | 1 | real::detail::class_fastpath_disabled() = false; |
135 | | |
136 | 1 | EXPECT(n_count == c_count); |
137 | 1 | EXPECT(n_all == c_all); |
138 | 1 | EXPECT(n_iter == c_iter); |
139 | 1 | EXPECT(replaced == c_replaced); |
140 | 1 | EXPECT(n_split == c_split); |
141 | 1 | EXPECT(n_count == n_iter); |
142 | 1 | EXPECT(n_all == n_iter); |
143 | 1 | EXPECT(n_count > 100); |
144 | 1 | } |
145 | | |
146 | | TEST(exact_literal_batch_declines_the_shapes_it_must) |
147 | 1 | { |
148 | | // Each of these keeps the literal fast path in run() but must NOT be batched: the `literal_one_search` |
149 | | // hint is false for a capture, for any assertion, for an anchor and for a one-byte literal, and the |
150 | | // batched span path applies none of the machinery those need (an assertion can make a given occurrence |
151 | | // fail, which is why the per-match route retries the next one). A one-byte literal has its own measured |
152 | | // reason to stay off the batched class route, recorded at run_literal_one_search. |
153 | 1 | const std::string subject {repeat_to("charlie 12 charlie_ x charlie ", 4096)}; |
154 | 1 | for (const char* pat : {"(charlie)", |
155 | 1 | "\\bcharlie\\b", |
156 | 1 | "^charlie", |
157 | 1 | "charlie$", |
158 | 1 | "c", |
159 | 6 | "\\Bcharlie"}) { |
160 | 6 | const auto [batched, per_match] {both_ways(pat, subject)}; |
161 | 6 | EXPECT(batched == per_match); |
162 | 6 | } |
163 | 1 | } |
164 | | |
165 | | TEST(exact_literal_batch_keeps_a_trailing_assertion_correct) |
166 | 1 | { |
167 | | // The shape the differential fuzzer found for the per-match route (`\B2` on "220"): an occurrence at |
168 | | // which the assertion fails must be skipped, not reported. It declines the batch, and this pins that |
169 | | // declining did not disturb it. |
170 | 1 | const real::regex rx {"\\B2"}; |
171 | 1 | const auto got {walk(rx, "220")}; |
172 | 1 | EXPECT(got.size() == 1); |
173 | 1 | EXPECT(got[0].first == 1); |
174 | 1 | EXPECT(got[0].second == 2); |
175 | 1 | } |