Coverage Report

Created: 2026-08-25 01:10

real-regex/tests/automata/test_dfa_which_matched.cpp
Line
Count
Source
1
// Stage-2 S2a: multi-accept unanchored which_matched vs N×search oracle.
2
#include <chrono>
3
#include <cstdint>
4
#include <string>
5
#include <string_view>
6
#include <vector>
7
8
#include <sciforge/test/framework.hpp>
9
#include "real/dfa.hpp"
10
#include "real/real.hpp"
11
12
namespace {
13
14
  std::vector<bool> nwalk_which(const std::vector<real::regex>& pats,
15
                                std::string_view                text)
16
7
  {
17
7
    std::vector<bool> hit;
18
7
    hit.reserve(pats.size());
19
56
    for (const auto& re : pats) {
20
56
      hit.push_back(static_cast<bool>(re.search(text)));
21
56
    }
22
7
    return hit;
23
7
  }
24
25
  std::vector<std::string> present_log()
26
3
  {
27
3
    return {
28
3
      R"([0-9]{4}-[0-9]{2}-[0-9]{2})",
29
3
      R"([0-9]{2}:[0-9]{2}:[0-9]{2})",
30
3
      R"(error|warn|info|debug|fatal)",
31
3
      R"([a-f0-9]{8})",
32
3
      R"([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)",
33
3
      R"(GET|POST|PUT|DELETE)",
34
3
      R"(user=[a-z]+)",
35
3
      R"(q=[0-9]+)",
36
3
    };
37
3
  }
38
} // namespace
39
40
TEST(which_matched_equals_nwalk_search)
41
1
{
42
1
  const auto               raw {present_log()};
43
1
  std::vector<real::regex> pats;
44
1
  pats.reserve(raw.size());
45
8
  for (const auto& p : raw) {
46
8
    pats.emplace_back(p);
47
8
  }
48
1
  const real::dfa d {std::span<const real::regex>(pats), real::dfa_mode::which_matched};
49
1
  EXPECT(d.is_unanchored());
50
1
  EXPECT_EQ(d.rule_count(), pats.size());
51
52
1
  const std::string text =
53
1
    "2026-06-13 12:04:55 error id=a3f9c1d8 GET /api/x from 10.0.2.15 user=bob q=42\n"
54
1
    "plain line\n";
55
1
  const auto got = d.which_matched(text);
56
1
  const auto ora = nwalk_which(pats, text);
57
1
  EXPECT_EQ(got.size(), ora.size());
58
9
  for (std::size_t i = 0; i < got.size(); 
++i8
) {
59
8
    EXPECT_EQ(static_cast<int>(got[i]), static_cast<int>(ora[i]));
60
8
  }
61
1
}
62
63
TEST(which_matched_both_on_overlap_prefix)
64
1
{
65
  // "ab" and "a" both match "ab" (which-matched); munch would pick one.
66
1
  std::vector<real::regex> pats;
67
1
  pats.emplace_back("ab");
68
1
  pats.emplace_back("a");
69
1
  const real::dfa d   {std::span<const real::regex>(pats), real::dfa_mode::which_matched};
70
1
  const auto      hit {d.which_matched("xx ab yy")};
71
1
  EXPECT(hit[0] && hit[1]);
72
1
}
73
74
TEST(which_matched_absent_false)
75
1
{
76
1
  std::vector<real::regex> pats;
77
1
  pats.emplace_back("needle");
78
1
  pats.emplace_back("zzz_absent");
79
1
  const real::dfa d   {std::span<const real::regex>(pats), real::dfa_mode::which_matched};
80
1
  const auto      hit {d.which_matched("a long haystack with a needle")};
81
1
  EXPECT(hit[0]);
82
1
  EXPECT(!hit[1]);
83
1
}
84
85
TEST(munch_mode_unchanged_default)
86
1
{
87
1
  std::vector<real::regex> pats;
88
1
  pats.emplace_back("ab");
89
1
  pats.emplace_back("a");
90
1
  const real::dfa d {std::span<const real::regex>(pats)}; // default munch
91
1
  EXPECT(!d.is_unanchored());
92
1
  const auto m      {d.match("ab")};
93
1
  EXPECT(m.has_value());
94
1
  EXPECT_EQ(m->rule_index, 0U); // longest "ab"
95
1
  EXPECT_EQ(m->length, 2U);
96
1
}
97
98
TEST(which_matched_state_count_bounded_log_patterns)
99
1
{
100
1
  auto raw = present_log();
101
57
  for (int i = 0; i < 56; 
++i56
) {
102
56
    raw.push_back("SEV" + std::to_string(i) + "|trace" + std::to_string(i));
103
56
  }
104
1
  std::vector<real::regex> pats;
105
1
  pats.reserve(raw.size());
106
64
  for (const auto& p : raw) {
107
64
    pats.emplace_back(p);
108
64
  }
109
1
  const real::dfa d {std::span<const real::regex>(pats), real::dfa_mode::which_matched};
110
  // Unanchored can be larger than munch proxy; must stay under production cap.
111
1
  EXPECT(d.state_count() < 65536U);
112
1
  EXPECT(d.state_count() > 0U);
113
1
}
114
115
// First-byte skip is a pure opt — avec == sans == N×search on sparse/dense/edge.
116
TEST(which_matched_first_byte_skip_equals_oracle)
117
1
{
118
1
  const auto               raw {present_log()};
119
1
  std::vector<real::regex> pats;
120
1
  pats.reserve(raw.size());
121
8
  for (const auto& p : raw) {
122
8
    pats.emplace_back(p);
123
8
  }
124
1
  const real::dfa d {std::span<const real::regex>(pats), real::dfa_mode::which_matched};
125
1
  EXPECT(d.has_first_byte_skip()); // all present_log rules have sound first_bytes
126
127
1
  const std::string dense =
128
1
    "2026-06-13 12:04:55 error id=a3f9c1d8 GET /api/x from 10.0.2.15 user=bob q=42\n"
129
1
    "plain line\n";
130
  // Sparse: long generic text, rare hits at the end (skip's happy path).
131
1
  std::string sparse;
132
1
  sparse.reserve(8000);
133
101
  for (int i = 0; i < 100; 
++i100
) {
134
100
    sparse += "the quick brown fox jumps over the lazy dog once more today\n";
135
100
  }
136
1
  sparse += "error id=deadbeef GET user=alice q=7\n";
137
138
  // Named buffer: dense+sparse must outlive the string_view (temporary would dangle → ASan UAF).
139
1
  const std::string                   dense_plus_sparse {dense + sparse};
140
1
  const std::vector<std::string_view> corpora           {
141
1
    dense, sparse, "", "zzz", "error", dense_plus_sparse,
142
1
  };
143
6
  for (const auto text : corpora) {
144
6
    const auto with_skip {d.which_matched(text, true)};
145
6
    const auto no_skip   {d.which_matched(text, false)};
146
6
    const auto ora       {nwalk_which(pats, text)};
147
6
    EXPECT_EQ(with_skip.size(), no_skip.size());
148
6
    EXPECT_EQ(with_skip.size(), ora.size());
149
54
    for (std::size_t i = 0; i < with_skip.size(); 
++i48
) {
150
48
      EXPECT_EQ(static_cast<int>(with_skip[i]), static_cast<int>(no_skip[i]));
151
48
      EXPECT_EQ(static_cast<int>(with_skip[i]), static_cast<int>(ora[i]));
152
48
    }
153
6
  }
154
1
}
155
156
TEST(which_matched_skip_disabled_when_nullable_rule)
157
1
{
158
  // Empty-match-possible rule ⇒ first_bytes_valid false on that rule ⇒ set skip off.
159
1
  std::vector<real::regex> pats;
160
1
  pats.emplace_back("needle");
161
1
  pats.emplace_back("a*"); // nullable
162
1
  const real::dfa d {std::span<const real::regex>(pats), real::dfa_mode::which_matched};
163
1
  EXPECT(!d.has_first_byte_skip());
164
1
  const auto hit    {d.which_matched("xx needle yy")};
165
1
  EXPECT(hit[0]);
166
1
}