Add regexx: a single-file C regex interpreter with Python re semantics
concept.md is the full design document: the objective (Python re parity plus binary/ASCII/UTF-8 modes, gigabyte-scale input, single C file), the automata-theory argument for why unrestricted backreferences/lookaround are incompatible with strict single-pass constant memory, the resulting two-engine architecture, the exact Python-mirroring naming convention, and a full comparison against POSIX regex.h for C-background readers. regexx.c/regexx.h are the v1 implementation: parser, compiler to a Pike/backtracking-style bytecode, and a single recursive backtracking engine covering the pattern syntax and operations listed in README.md, validated against CPython's own re module output (tests/), clean under AddressSanitizer/UBSan, and stress-tested (50MB simple-quantifier match, graceful failure rather than a crash on complex repeats over large input, clean rejection of every intentionally unsupported construct). Also included: examples/rxgrep.c (a small grep-like program exercising all three data modes and the substitution API), the Makefile, the MIT LICENSE, and docs/API.md, an exhaustive reference for every type, flag, and function's exact return-value and memory-ownership convention, checked against the current source and against a real CPython interpreter rather than against memory. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EjuMk8kY9SDus1wWe2K9xY
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
"""Test cases for regexx, checked against CPython's own `re` module
|
||||
(concept.md Section 11). Each case records a pattern/subject pair and
|
||||
an operation; gen.py computes the expected result with Python's `re`
|
||||
and emits a C assertion that checks regexx against it.
|
||||
"""
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def add(op, pattern, subject, mode="utf8", flags=(), **kw):
|
||||
CASES.append(dict(op=op, pattern=pattern, subject=subject, mode=mode, flags=tuple(flags), **kw))
|
||||
|
||||
|
||||
# ---- literals, concatenation, anchors --------------------------------
|
||||
add("search", r"abc", "xxabcxx")
|
||||
add("fullmatch", r"abc", "abc")
|
||||
add("fullmatch", r"abc", "xabc")
|
||||
add("match", r"abc", "abcdef")
|
||||
add("match", r"abc", "xabcdef")
|
||||
add("search", r"^abc$", "abc")
|
||||
add("search", r"^abc$", "xabc")
|
||||
add("search", r"^abc", "abc\nabc", flags=["MULTILINE"])
|
||||
add("finditer", r"^abc", "abc\nabc", flags=["MULTILINE"])
|
||||
add("search", r"abc$", "xxabc\nabc", flags=["MULTILINE"])
|
||||
add("search", r"\Aabc", "abc")
|
||||
add("search", r"abc\Z", "xxabc")
|
||||
add("search", r"abc\Z", "xxabc\n")
|
||||
|
||||
# ---- character classes -------------------------------------------------
|
||||
add("search", r"[a-z]+", "ABCdefGHI")
|
||||
add("search", r"[^a-z]+", "abcDEFghi")
|
||||
add("search", r"[a-z]+", "ABC", flags=["IGNORECASE"])
|
||||
add("finditer", r"[abc]", "xaybzc")
|
||||
add("search", r"[]a]", "]a") # ']' literal as first class member
|
||||
add("search", r"[a\-z]", "-")
|
||||
|
||||
# ---- shorthand classes ---------------------------------------------------
|
||||
add("finditer", r"\d+", "ab123cd45")
|
||||
add("finditer", r"\D+", "ab123cd45")
|
||||
add("search", r"\s+", "a b")
|
||||
add("search", r"\S+", " ab ")
|
||||
add("finditer", r"\w+", "hi there, friend!")
|
||||
add("finditer", r"\W+", "hi there, friend!")
|
||||
add("search", r"[\d\S]", " ")
|
||||
add("search", r"[\d\S]", "5")
|
||||
|
||||
# ---- word boundaries ------------------------------------------------------
|
||||
add("finditer", r"\bcat\b", "cat catalog cat")
|
||||
add("finditer", r"\Bcat\B", "concatenate")
|
||||
|
||||
# ---- quantifiers ------------------------------------------------------------
|
||||
add("search", r"a*", "aaab")
|
||||
add("search", r"a+", "baaab")
|
||||
add("search", r"a?b", "b")
|
||||
add("search", r"a{2,4}", "aaaaa")
|
||||
add("search", r"a{2,4}?", "aaaaa")
|
||||
add("search", r"a{3}", "aa")
|
||||
add("search", r"a.*b", "axxxbxxxb")
|
||||
add("search", r"a.*?b", "axxxbxxxb")
|
||||
add("finditer", r"a*", "baaab") # exercises empty-match advancement
|
||||
|
||||
# ---- alternation and groups -------------------------------------------------
|
||||
add("search", r"(foo|bar)baz", "barbaz")
|
||||
add("search", r"(?:foo|bar)baz", "foobaz")
|
||||
add("finditer", r"(a)(b)?", "ab a")
|
||||
add("search", r"(?P<year>\d{4})-(?P<month>\d{2})", "2024-09")
|
||||
|
||||
# ---- backreferences ---------------------------------------------------------
|
||||
add("search", r"(\w+) \1", "hello hello")
|
||||
add("search", r"(\w+) \1", "hello world")
|
||||
add("search", r"(?P<w>\w+) (?P=w)", "abc abc")
|
||||
add("search", r"(\w)\1", "aa")
|
||||
add("search", r"(\w)\1", "ab")
|
||||
|
||||
# ---- lookaround --------------------------------------------------------------
|
||||
add("search", r"foo(?=bar)", "foobar")
|
||||
add("search", r"foo(?=bar)", "foobaz")
|
||||
add("search", r"foo(?!bar)", "foobaz")
|
||||
add("search", r"foo(?!bar)", "foobar")
|
||||
add("search", r"(?<=foo)bar", "foobar")
|
||||
add("search", r"(?<=foo)bar", "xxxbar")
|
||||
add("search", r"(?<!foo)bar", "xxxbar")
|
||||
add("search", r"(?<!foo)bar", "foobar")
|
||||
add("finditer", r"(?<=\d)(?=(\d{3})+(?!\d))", "1234567") # thousands-separator style
|
||||
|
||||
# ---- atomic groups / possessive quantifiers -----------------------------------
|
||||
add("search", r"a(?>bc|b)c", "abc")
|
||||
add("search", r"a(bc|b)c", "abc")
|
||||
add("search", r"a*+a", "aaaa")
|
||||
add("search", r"a*a", "aaaa")
|
||||
|
||||
# ---- DOTALL / VERBOSE ----------------------------------------------------------
|
||||
add("search", r"a.b", "a\nb")
|
||||
add("search", r"a.b", "a\nb", flags=["DOTALL"])
|
||||
add("search", r"""
|
||||
\d+ # the integer part
|
||||
\. # the dot
|
||||
\d+ # the fractional part
|
||||
""", "pi is 3.14 roughly", flags=["VERBOSE"])
|
||||
|
||||
# ---- escapes ---------------------------------------------------------------------
|
||||
add("search", r"a\tb", "a\tb")
|
||||
add("search", r"\x41\x42", "AB")
|
||||
add("search", r"é", "café")
|
||||
|
||||
# ---- UTF-8 mode ------------------------------------------------------------------
|
||||
add("search", r"\w+", "café au lait")
|
||||
add("finditer", r"\w+", "café 中文 word")
|
||||
add("search", r"[à-ÿ]+", "éèê")
|
||||
|
||||
# ---- sub / subn -------------------------------------------------------------------
|
||||
add("sub", r"a", "banana", repl="o", count=0)
|
||||
add("sub", r"a", "banana", repl="o", count=2)
|
||||
add("sub", r"(\w+)@(\w+)", "user@host", repl=r"\2@\1")
|
||||
add("sub", r"(?P<user>\w+)@(?P<host>\w+)", "user@host", repl=r"\g<host>@\g<user>")
|
||||
add("sub", r"\s+", "a b c", repl=" ")
|
||||
|
||||
# ---- split ------------------------------------------------------------------------
|
||||
add("split", r"[,;]\s*", "a, b;c , d")
|
||||
add("split", r"(,)", "a,b,c")
|
||||
add("split", r"\s*", "abc")
|
||||
add("split", r",", "a,b,c", maxsplit=1)
|
||||
|
||||
# ---- ASCII vs UTF8 mode differences for \w ----------------------------------------
|
||||
add("search", r"\w+", "café", mode="ascii")
|
||||
|
||||
# ---- BINARY mode --------------------------------------------------------------------
|
||||
add("search", r"a.c", "a\x00c", mode="binary")
|
||||
add("finditer", r"\d+", "12ab34", mode="binary")
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate tests/generated_tests.c from tests/cases.py, using CPython's
|
||||
own `re` module as ground truth (concept.md Section 11)."""
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from cases import CASES # noqa: E402
|
||||
|
||||
FLAGMAP = {
|
||||
"IGNORECASE": re.IGNORECASE,
|
||||
"MULTILINE": re.MULTILINE,
|
||||
"DOTALL": re.DOTALL,
|
||||
"VERBOSE": re.VERBOSE,
|
||||
"LOCALE": re.LOCALE,
|
||||
"ASCII": re.ASCII,
|
||||
}
|
||||
|
||||
|
||||
def c_str(b):
|
||||
if isinstance(b, str):
|
||||
b = b.encode("utf-8")
|
||||
out = ['"']
|
||||
for byte in b:
|
||||
c = chr(byte)
|
||||
if c == '"':
|
||||
out.append('\\"')
|
||||
elif c == "\\":
|
||||
out.append("\\\\")
|
||||
elif 32 <= byte < 127:
|
||||
out.append(c)
|
||||
else:
|
||||
out.append("\\%03o" % byte)
|
||||
out.append('"')
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def py_flags(case):
|
||||
f = 0
|
||||
if case["mode"] == "ascii":
|
||||
f |= re.ASCII
|
||||
for name in case["flags"]:
|
||||
f |= FLAGMAP[name]
|
||||
return f
|
||||
|
||||
|
||||
def c_flags(case):
|
||||
names = []
|
||||
if case["mode"] == "ascii":
|
||||
names.append("ASCII")
|
||||
elif case["mode"] == "utf8":
|
||||
names.append("UTF8")
|
||||
elif case["mode"] == "binary":
|
||||
names.append("BINARY")
|
||||
for name in case["flags"]:
|
||||
names.append(name)
|
||||
return "|".join(names) if names else "0"
|
||||
|
||||
|
||||
def encode(case, s):
|
||||
if case["mode"] == "binary":
|
||||
return s.encode("utf-8")
|
||||
return s
|
||||
|
||||
|
||||
def blen(case, s):
|
||||
"""Byte length of the encoded subject, as the C API always wants a
|
||||
byte count (UTF8 mode still stores a byte buffer; only Match_start
|
||||
reports code points), never a Python character count."""
|
||||
e = encode(case, s)
|
||||
return len(e) if isinstance(e, bytes) else len(e.encode("utf-8"))
|
||||
|
||||
|
||||
def desc(case, i):
|
||||
return "#%d %s /%s/ on %r" % (i, case["op"], case["pattern"], case["subject"])
|
||||
|
||||
|
||||
def gen_search_family(case, i, out):
|
||||
kind = {"search": 0, "match": 1, "fullmatch": 2}[case["op"]]
|
||||
flags = py_flags(case)
|
||||
subj = encode(case, case["subject"])
|
||||
pat = encode(case, case["pattern"])
|
||||
compiled = re.compile(pat, flags)
|
||||
fn = {"search": compiled.search, "match": compiled.match, "fullmatch": compiled.fullmatch}[case["op"]]
|
||||
m = fn(subj)
|
||||
d = desc(case, i)
|
||||
if m is None:
|
||||
out.append(' tc_search_family(%s, %s, %s, %s, %d, %d, 0, 0, NULL);' % (
|
||||
c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]), kind))
|
||||
return
|
||||
spans = [m.start(0), m.end(0)]
|
||||
for g in range(1, compiled.groups + 1):
|
||||
try:
|
||||
spans += [m.start(g), m.end(g)]
|
||||
except IndexError:
|
||||
spans += [-1, -1]
|
||||
n_spans = compiled.groups + 1
|
||||
arr = ",".join(str(v) for v in spans)
|
||||
out.append(' { static const long long sp[] = {%s}; tc_search_family(%s, %s, %s, %s, %d, %d, 1, %d, sp); }' % (
|
||||
arr, c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]), kind, n_spans))
|
||||
|
||||
|
||||
def gen_finditer(case, i, out):
|
||||
flags = py_flags(case)
|
||||
subj = encode(case, case["subject"])
|
||||
pat = encode(case, case["pattern"])
|
||||
ms = list(re.finditer(pat, subj, flags))
|
||||
spans = []
|
||||
for m in ms:
|
||||
spans += [m.start(0), m.end(0)]
|
||||
d = desc(case, i)
|
||||
if not spans:
|
||||
out.append(' tc_finditer(%s, %s, %s, %s, %d, 0, NULL);' % (
|
||||
c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"])))
|
||||
return
|
||||
arr = ",".join(str(v) for v in spans)
|
||||
out.append(' { static const long long sp[] = {%s}; tc_finditer(%s, %s, %s, %s, %d, %d, sp); }' % (
|
||||
arr, c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]), len(ms)))
|
||||
|
||||
|
||||
def gen_sub(case, i, out):
|
||||
flags = py_flags(case)
|
||||
subj = encode(case, case["subject"])
|
||||
pat = encode(case, case["pattern"])
|
||||
repl = encode(case, case["repl"])
|
||||
count = case.get("count", 0)
|
||||
result, n = re.subn(pat, repl, subj, count=count, flags=flags)
|
||||
d = desc(case, i)
|
||||
result_str = result.decode("utf-8") if isinstance(result, bytes) else result
|
||||
out.append(' tc_sub(%s, %s, %s, %s, %d, %s, %d, %s, %d);' % (
|
||||
c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]),
|
||||
c_str(case["repl"]), count, c_str(result_str), n))
|
||||
|
||||
|
||||
def gen_split(case, i, out):
|
||||
flags = py_flags(case)
|
||||
subj = encode(case, case["subject"])
|
||||
pat = encode(case, case["pattern"])
|
||||
maxsplit = case.get("maxsplit", 0)
|
||||
items = re.split(pat, subj, maxsplit=maxsplit, flags=flags)
|
||||
d = desc(case, i)
|
||||
parts = []
|
||||
for it in items:
|
||||
if it is None:
|
||||
parts.append("NULL")
|
||||
else:
|
||||
s = it.decode("utf-8") if isinstance(it, bytes) else it
|
||||
parts.append(c_str(s))
|
||||
arr = ",".join(parts) if parts else "0"
|
||||
out.append(' { static const char *const items[] = {%s}; tc_split(%s, %s, %s, %s, %d, %d, %d, items); }' % (
|
||||
arr, c_str(d), c_str(case["pattern"]), c_flags(case), c_str(case["subject"]), blen(case, case["subject"]),
|
||||
maxsplit, len(items)))
|
||||
|
||||
|
||||
def main():
|
||||
out = []
|
||||
out.append('/* GENERATED by tests/gen.py from tests/cases.py. Do not edit by hand. */')
|
||||
out.append('#include "harness.h"')
|
||||
out.append('void run_generated_tests(void) {')
|
||||
for i, case in enumerate(CASES):
|
||||
op = case["op"]
|
||||
if op in ("search", "match", "fullmatch"):
|
||||
gen_search_family(case, i, out)
|
||||
elif op == "finditer":
|
||||
gen_finditer(case, i, out)
|
||||
elif op == "sub":
|
||||
gen_sub(case, i, out)
|
||||
elif op == "split":
|
||||
gen_split(case, i, out)
|
||||
else:
|
||||
raise ValueError("unknown op %r" % op)
|
||||
out.append('}')
|
||||
dest = os.path.join(os.path.dirname(__file__), "generated_tests.c")
|
||||
with open(dest, "w") as f:
|
||||
f.write("\n".join(out) + "\n")
|
||||
print("wrote %s (%d cases)" % (dest, len(CASES)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
#include "harness.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int tc_pass = 0, tc_fail = 0;
|
||||
|
||||
void tc_search_family(const char *desc, const char *pat, int flags,
|
||||
const char *subj, size_t subjlen, int kind,
|
||||
int exp_matched, int n_spans, const long long *exp_spans) {
|
||||
PatternError err; memset(&err, 0, sizeof err);
|
||||
Pattern *p = re_compile(pat, strlen(pat), flags, &err);
|
||||
if (!p) {
|
||||
printf("FAIL [%s]: compile error: %s\n", desc, err.msg ? err.msg : "?");
|
||||
tc_fail++;
|
||||
return;
|
||||
}
|
||||
Input *in = Input_from_buffer((const uint8_t *)subj, subjlen);
|
||||
Match m; memset(&m, 0, sizeof m);
|
||||
int r;
|
||||
if (kind == 0) r = Pattern_search(p, in, 0, -1, &m);
|
||||
else if (kind == 1) r = Pattern_match(p, in, 0, -1, &m);
|
||||
else r = Pattern_fullmatch(p, in, 0, -1, &m);
|
||||
|
||||
if (r < 0) {
|
||||
printf("FAIL [%s]: internal error (depth exceeded or bad UTF-8)\n", desc);
|
||||
tc_fail++;
|
||||
} else if ((r == 1) != (exp_matched != 0)) {
|
||||
printf("FAIL [%s]: matched=%d expected=%d\n", desc, r, exp_matched);
|
||||
tc_fail++;
|
||||
} else if (r == 1) {
|
||||
int ok = 1;
|
||||
for (int g = 0; g < n_spans; g++) {
|
||||
long long es = exp_spans[2 * g], ee = exp_spans[2 * g + 1];
|
||||
long long as = Match_start(&m, g), ae = Match_end(&m, g);
|
||||
if (as != es || ae != ee) {
|
||||
printf("FAIL [%s]: group %d span (%lld,%lld) expected (%lld,%lld)\n", desc, g, as, ae, es, ee);
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
if (ok) tc_pass++; else tc_fail++;
|
||||
Match_free(&m);
|
||||
} else {
|
||||
tc_pass++;
|
||||
}
|
||||
Input_free(in);
|
||||
Pattern_free(p);
|
||||
}
|
||||
|
||||
typedef struct { long long *spans; int n, cap; } SpanList;
|
||||
static void span_cb(void *ctx, const Match *m_const) {
|
||||
SpanList *sl = ctx;
|
||||
Match *m = (Match *)m_const;
|
||||
if (sl->n == sl->cap) { sl->cap = sl->cap ? sl->cap * 2 : 8; sl->spans = realloc(sl->spans, (size_t)sl->cap * 2 * sizeof(long long)); }
|
||||
sl->spans[2 * sl->n] = Match_start(m, 0);
|
||||
sl->spans[2 * sl->n + 1] = Match_end(m, 0);
|
||||
sl->n++;
|
||||
}
|
||||
|
||||
void tc_finditer(const char *desc, const char *pat, int flags,
|
||||
const char *subj, size_t subjlen,
|
||||
int exp_n_matches, const long long *exp_spans) {
|
||||
PatternError err; memset(&err, 0, sizeof err);
|
||||
Pattern *p = re_compile(pat, strlen(pat), flags, &err);
|
||||
if (!p) { printf("FAIL [%s]: compile error: %s\n", desc, err.msg ? err.msg : "?"); tc_fail++; return; }
|
||||
Input *in = Input_from_buffer((const uint8_t *)subj, subjlen);
|
||||
SpanList sl; memset(&sl, 0, sizeof sl);
|
||||
int total = Pattern_finditer(p, in, 0, -1, span_cb, &sl);
|
||||
if (total < 0) { printf("FAIL [%s]: internal error\n", desc); tc_fail++; }
|
||||
else if (sl.n != exp_n_matches) { printf("FAIL [%s]: %d matches, expected %d\n", desc, sl.n, exp_n_matches); tc_fail++; }
|
||||
else {
|
||||
int ok = 1;
|
||||
for (int i = 0; i < sl.n; i++)
|
||||
if (sl.spans[2 * i] != exp_spans[2 * i] || sl.spans[2 * i + 1] != exp_spans[2 * i + 1]) {
|
||||
printf("FAIL [%s]: match %d span (%lld,%lld) expected (%lld,%lld)\n", desc, i,
|
||||
sl.spans[2 * i], sl.spans[2 * i + 1], exp_spans[2 * i], exp_spans[2 * i + 1]);
|
||||
ok = 0;
|
||||
}
|
||||
if (ok) tc_pass++; else tc_fail++;
|
||||
}
|
||||
free(sl.spans);
|
||||
Input_free(in);
|
||||
Pattern_free(p);
|
||||
}
|
||||
|
||||
void tc_sub(const char *desc, const char *pat, int flags,
|
||||
const char *subj, size_t subjlen, const char *repl, int count,
|
||||
const char *exp_result, int exp_n) {
|
||||
PatternError err; memset(&err, 0, sizeof err);
|
||||
Pattern *p = re_compile(pat, strlen(pat), flags, &err);
|
||||
if (!p) { printf("FAIL [%s]: compile error: %s\n", desc, err.msg ? err.msg : "?"); tc_fail++; return; }
|
||||
Input *in = Input_from_buffer((const uint8_t *)subj, subjlen);
|
||||
char *out = NULL; size_t outlen = 0; int n = 0;
|
||||
int r = Pattern_subn(p, in, repl, NULL, NULL, count, &out, &outlen, &n);
|
||||
if (r < 0) { printf("FAIL [%s]: internal error\n", desc); tc_fail++; }
|
||||
else {
|
||||
size_t explen = strlen(exp_result);
|
||||
if (outlen != explen || memcmp(out, exp_result, explen) != 0 || n != exp_n) {
|
||||
printf("FAIL [%s]: sub result '%.*s' (n=%d) expected '%s' (n=%d)\n", desc, (int)outlen, out, n, exp_result, exp_n);
|
||||
tc_fail++;
|
||||
} else tc_pass++;
|
||||
}
|
||||
free(out);
|
||||
Input_free(in);
|
||||
Pattern_free(p);
|
||||
}
|
||||
|
||||
typedef struct { int present; const char *ptr; size_t len; } SplitItem;
|
||||
typedef struct { SplitItem *items; int n, cap; } SplitList;
|
||||
static void split_cb(void *ctx, const Match *m_const) {
|
||||
SplitList *sl = ctx;
|
||||
Match *m = (Match *)m_const;
|
||||
if (sl->n == sl->cap) { sl->cap = sl->cap ? sl->cap * 2 : 8; sl->items = realloc(sl->items, (size_t)sl->cap * sizeof(SplitItem)); }
|
||||
const char *out; size_t outlen;
|
||||
int r = Match_group(m, NULL, 0, &out, &outlen);
|
||||
sl->items[sl->n].present = (r == 1);
|
||||
sl->items[sl->n].ptr = out; sl->items[sl->n].len = outlen;
|
||||
sl->n++;
|
||||
}
|
||||
|
||||
void tc_split(const char *desc, const char *pat, int flags,
|
||||
const char *subj, size_t subjlen, int maxsplit,
|
||||
int n_items, const char *const *texts) {
|
||||
PatternError err; memset(&err, 0, sizeof err);
|
||||
Pattern *p = re_compile(pat, strlen(pat), flags, &err);
|
||||
if (!p) { printf("FAIL [%s]: compile error: %s\n", desc, err.msg ? err.msg : "?"); tc_fail++; return; }
|
||||
Input *in = Input_from_buffer((const uint8_t *)subj, subjlen);
|
||||
SplitList sl; memset(&sl, 0, sizeof sl);
|
||||
int r = Pattern_split(p, in, maxsplit, split_cb, &sl);
|
||||
if (r < 0) { printf("FAIL [%s]: internal error\n", desc); tc_fail++; }
|
||||
else if (sl.n != n_items) { printf("FAIL [%s]: %d items, expected %d\n", desc, sl.n, n_items); tc_fail++; }
|
||||
else {
|
||||
int ok = 1;
|
||||
for (int i = 0; i < sl.n; i++) {
|
||||
int exp_none = (texts[i] == NULL);
|
||||
if (exp_none && sl.items[i].present) { printf("FAIL [%s]: item %d expected None\n", desc, i); ok = 0; }
|
||||
else if (!exp_none) {
|
||||
size_t explen = strlen(texts[i]);
|
||||
if (!sl.items[i].present || sl.items[i].len != explen || memcmp(sl.items[i].ptr, texts[i], explen) != 0) {
|
||||
printf("FAIL [%s]: item %d = '%.*s' expected '%s'\n", desc, i,
|
||||
sl.items[i].present ? (int)sl.items[i].len : 4,
|
||||
sl.items[i].present ? sl.items[i].ptr : "NONE", texts[i]);
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ok) tc_pass++; else tc_fail++;
|
||||
}
|
||||
free(sl.items);
|
||||
Input_free(in);
|
||||
Pattern_free(p);
|
||||
}
|
||||
|
||||
void harness_report(void) {
|
||||
printf("\n%d passed, %d failed\n", tc_pass, tc_fail);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* tests/harness.h - shared by hand-written and generated test cases.
|
||||
* Ground truth for generated cases comes from CPython's own `re`
|
||||
* module (concept.md Section 11); see gen.py. */
|
||||
#ifndef REGEXX_TEST_HARNESS_H
|
||||
#define REGEXX_TEST_HARNESS_H
|
||||
#include "../regexx.h"
|
||||
#include <stddef.h>
|
||||
|
||||
extern int tc_pass, tc_fail;
|
||||
|
||||
/* search/match/fullmatch: exp_spans holds 2*n_spans int64_t values,
|
||||
* (start,end) for group 0, then group 1, 2, ... in order; -1,-1 means
|
||||
* the group did not participate (or, for group 0 alone, that
|
||||
* exp_matched is 0 and no spans are checked). */
|
||||
void tc_search_family(const char *desc, const char *pat, int flags,
|
||||
const char *subj, size_t subjlen, int kind,
|
||||
int exp_matched, int n_spans, const long long *exp_spans);
|
||||
|
||||
/* finditer: exp_spans holds 2*n_matches int64_t values, (start,end) of
|
||||
* the whole match for each match found, in order. */
|
||||
void tc_finditer(const char *desc, const char *pat, int flags,
|
||||
const char *subj, size_t subjlen,
|
||||
int exp_n_matches, const long long *exp_spans);
|
||||
|
||||
void tc_sub(const char *desc, const char *pat, int flags,
|
||||
const char *subj, size_t subjlen, const char *repl, int count,
|
||||
const char *exp_result, int exp_n);
|
||||
|
||||
/* split: texts[i] == NULL means that list element is None in Python. */
|
||||
void tc_split(const char *desc, const char *pat, int flags,
|
||||
const char *subj, size_t subjlen, int maxsplit,
|
||||
int n_items, const char *const *texts);
|
||||
|
||||
void harness_report(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#include "harness.h"
|
||||
#include <stdio.h>
|
||||
|
||||
extern void run_generated_tests(void);
|
||||
|
||||
int main(void) {
|
||||
run_generated_tests();
|
||||
harness_report();
|
||||
return tc_fail ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user