Coverage Report

Created: 2026-08-25 01:10

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
}