Skip to content

[Do-not-merge]: Remove the boost test runner#38

Draft
rustaceanrob wants to merge 3 commits into
2140-dev:masterfrom
rustaceanrob:boost-removal
Draft

[Do-not-merge]: Remove the boost test runner#38
rustaceanrob wants to merge 3 commits into
2140-dev:masterfrom
rustaceanrob:boost-removal

Conversation

@rustaceanrob
Copy link
Copy Markdown
Member

@rustaceanrob rustaceanrob commented Jun 2, 2026

Replace all boost macros (third commit)
import argparse
import re
import sys
from pathlib import Path


# ---------------------------------------------------------------------------
# Parser primitives
# ---------------------------------------------------------------------------

_DIGIT_SEP_PREV = set("0123456789abcdefABCDEF'")


def _is_char_literal_open(text: str, i: int) -> bool:
    """A `'` at position i is a char-literal opener unless the previous
    character is a digit, hex letter, or another digit separator (C++14
    digit-separator usage like 1'000'000 or 0xFF'FF)."""
    return i == 0 or text[i - 1] not in _DIGIT_SEP_PREV


def find_matching_paren(text: str, open_idx: int) -> int | None:
    """Return index of ')' that matches '(' at open_idx, respecting brackets,
    braces, and string/char literals."""
    assert text[open_idx] == "("
    depth = 0
    i = open_idx
    in_str = False
    in_char = False
    while i < len(text):
        c = text[i]
        if in_str:
            if c == "\\":
                i += 2
                continue
            if c == '"':
                in_str = False
        elif in_char:
            if c == "\\":
                i += 2
                continue
            if c == "'":
                in_char = False
        elif c == '"':
            in_str = True
        elif c == "'" and _is_char_literal_open(text, i):
            in_char = True
        elif c in "([{":
            depth += 1
        elif c in ")]}":
            depth -= 1
            if depth == 0:
                return i
        i += 1
    return None


def _is_binary_context(text: str, i: int) -> bool:
    """At position i (start of `|`, `&`, or `^`), is the previous non-whitespace
    character one that makes this a binary operator (not unary `&`)?"""
    j = i - 1
    while j >= 0 and text[j] in " \t\n":
        j -= 1
    if j < 0:
        return False
    return text[j].isalnum() or text[j] in "_)]}\"'"


def needs_parens(expr: str) -> bool:
    """True if expr contains a top-level operator that would interact badly
    with the framework's Decomposer (which captures via `<=`, applies one
    comparison via CapturedExpression's overloads, then yields a Result that
    has no operators).

    Detected: `&&`, `||`, `==`, `!=`, `<=`, `>=`, `|`, `&`, `^`. Bare `<` /
    `>` are intentionally skipped because they are commonly template
    brackets (`vector<int>`, `static_cast<T>`) which this scanner can't
    distinguish from comparison without a real C++ parser; if a real
    comparison with bare `<`/`>` ends up in an operand, the resulting build
    error is easy to spot and fix at the call site.

    Compound assignments (|=, &=, ^=) and unary `&` (address-of) are skipped."""
    depth = 0
    i = 0
    in_str = False
    in_char = False
    n = len(expr)
    while i < n:
        c = expr[i]
        if in_str:
            if c == "\\":
                i += 2
                continue
            if c == '"':
                in_str = False
            i += 1
            continue
        if in_char:
            if c == "\\":
                i += 2
                continue
            if c == "'":
                in_char = False
            i += 1
            continue
        if c == '"':
            in_str = True
            i += 1
            continue
        if c == "'" and _is_char_literal_open(expr, i):
            in_char = True
            i += 1
            continue
        if c in "([{":
            depth += 1
            i += 1
            continue
        if c in ")]}":
            depth -= 1
            i += 1
            continue
        if depth == 0:
            two = expr[i : i + 2] if i + 1 < n else ""
            if two in ("&&", "||", "==", "!=", "<=", ">="):
                return True
            if c in "|&^" and two[1:] != "=" and _is_binary_context(expr, i):
                return True
        i += 1
    return False


# Backwards-compat alias for has_top_level_bitwise's old name.
has_top_level_bitwise = needs_parens


def wrap_if_bitwise(expr: str) -> str:
    expr = expr.strip()
    return f"({expr})" if needs_parens(expr) else expr


def split_top_level_args(args: str) -> list[str]:
    """Split macro arg-list on top-level commas."""
    parts: list[str] = []
    depth = 0
    last = 0
    i = 0
    in_str = False
    in_char = False
    while i < len(args):
        c = args[i]
        if in_str:
            if c == "\\":
                i += 2
                continue
            if c == '"':
                in_str = False
        elif in_char:
            if c == "\\":
                i += 2
                continue
            if c == "'":
                in_char = False
        elif c == '"':
            in_str = True
        elif c == "'" and _is_char_literal_open(args, i):
            in_char = True
        elif c in "([{":
            depth += 1
        elif c in ")]}":
            depth -= 1
        elif c == "," and depth == 0:
            parts.append(args[last:i])
            last = i + 1
        i += 1
    parts.append(args[last:])
    return parts


# ---------------------------------------------------------------------------
# Macro tables
# ---------------------------------------------------------------------------

SIMPLE_RENAMES = {
    "BOOST_CHECK": "CHECK",
    "BOOST_REQUIRE": "REQUIRE",
    "BOOST_TEST": "CHECK",
    "BOOST_TEST_REQUIRE": "REQUIRE",
    "BOOST_CHECK_NO_THROW": "CHECK_NOTHROW",
    "BOOST_REQUIRE_NO_THROW": "REQUIRE_NOTHROW",
    "BOOST_CHECK_THROW": "CHECK_THROWS_AS",
    "BOOST_CHECK_EXCEPTION": "CHECK_EXCEPTION",
    "BOOST_TEST_MESSAGE": "TEST_MESSAGE",
    "BOOST_WARN_MESSAGE": "WARN_MESSAGE",
}

MESSAGE_RENAMES = {
    "BOOST_CHECK_MESSAGE": "CHECK",
    "BOOST_REQUIRE_MESSAGE": "REQUIRE",
}

COMPARISON_REWRITES = {
    "BOOST_CHECK_EQUAL":   ("CHECK", "=="),
    "BOOST_REQUIRE_EQUAL": ("REQUIRE", "=="),
    "BOOST_CHECK_NE":      ("CHECK", "!="),
    "BOOST_CHECK_LT":      ("CHECK", "<"),
    "BOOST_CHECK_LE":      ("CHECK", "<="),
    "BOOST_CHECK_GT":      ("CHECK", ">"),
    "BOOST_CHECK_GE":      ("CHECK", ">="),
    "BOOST_REQUIRE_NE":    ("REQUIRE", "!="),
    "BOOST_REQUIRE_LT":    ("REQUIRE", "<"),
    "BOOST_REQUIRE_LE":    ("REQUIRE", "<="),
    "BOOST_REQUIRE_GT":    ("REQUIRE", ">"),
    "BOOST_REQUIRE_GE":    ("REQUIRE", ">="),
}

COMMENTED_OUT = (
    "BOOST_TEST_INFO",
    "BOOST_TEST_INFO_SCOPE",
)

REPORTED_ONLY = (
    "BOOST_CHECK_CLOSE",
)

STRUCTURAL_MACROS = (
    "BOOST_AUTO_TEST_CASE",
    "BOOST_FIXTURE_TEST_CASE",
    "BOOST_AUTO_TEST_SUITE",
    "BOOST_FIXTURE_TEST_SUITE",
    "BOOST_AUTO_TEST_SUITE_END",
    "BOOST_CHECK_EQUAL_COLLECTIONS",
    "BOOST_ERROR",
)

ALL_KNOWN = (
    list(SIMPLE_RENAMES)
    + list(MESSAGE_RENAMES)
    + list(COMPARISON_REWRITES)
    + list(COMMENTED_OUT)
    + list(REPORTED_ONLY)
    + list(STRUCTURAL_MACROS)
)
# Longer names first so the alternation doesn't match a prefix.
KNOWN_SORTED = sorted(set(ALL_KNOWN), key=len, reverse=True)
MACRO_RE = re.compile(r"\b(" + "|".join(re.escape(m) for m in KNOWN_SORTED) + r")\s*\(")


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def quote_name(name_arg: str) -> str:
    return f'"{name_arg.strip()}"'


def collapse_iter_pair(begin_expr: str, end_expr: str) -> str:
    """X.begin()/X.end() -> X. X->begin()/X->end() -> *X. Else subrange()."""
    b = begin_expr.strip()
    e = end_expr.strip()
    m_b = re.match(r"^(.+)\.begin\s*\(\s*\)$", b)
    m_e = re.match(r"^(.+)\.end\s*\(\s*\)$", e)
    if m_b and m_e and m_b.group(1).strip() == m_e.group(1).strip():
        return m_b.group(1).strip()
    m_b = re.match(r"^(.+)->begin\s*\(\s*\)$", b)
    m_e = re.match(r"^(.+)->end\s*\(\s*\)$", e)
    if m_b and m_e and m_b.group(1).strip() == m_e.group(1).strip():
        return f"*({m_b.group(1).strip()})"
    return f"std::ranges::subrange({b}, {e})"


# ---------------------------------------------------------------------------
# Main rewrite
# ---------------------------------------------------------------------------

def rewrite_includes_and_module(text: str) -> str:
    # Any boost/test include -> framework include.
    text = re.sub(
        r"^#include\s*<boost/test/[^>]+>\s*$",
        "#include <test/util/framework.hpp>",
        text,
        flags=re.MULTILINE,
    )
    # BOOST_TEST_MODULE in standalone test files -> framework main hook.
    text = re.sub(
        r"^#define\s+BOOST_TEST_MODULE\b.*$",
        "#define BITCOIN_TEST_MAIN",
        text,
        flags=re.MULTILINE,
    )
    # Collapse adjacent duplicate framework includes (a boost file may have
    # included two headers from boost/test/).
    text = re.sub(
        r"(#include <test/util/framework\.hpp>\n)(?:[ \t]*#include <test/util/framework\.hpp>\n)+",
        r"\1",
        text,
    )
    return text


def file_namespaces(text: str) -> set[str]:
    """Return the set of namespace names declared anywhere in the file. Used
    to decide whether to wrap a test suite body in a matching namespace —
    Boost.Test opens `namespace <suite>` implicitly, but the framework does
    not, so a fixture/helper defined in `namespace foo` won't be visible
    inside the suite body without an explicit re-open."""
    return set(re.findall(r"^[ \t]*namespace[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]*\{", text, re.MULTILINE))


def rewrite_macros(text: str):
    """Walk macro calls in order, applying conversions while tracking the
    current fixture-suite stack. Returns (new_text, reports)."""
    out: list[str] = []
    cursor = 0
    # Each entry: (fixture-or-None, opened-namespace-or-None)
    suite_stack: list[tuple[str | None, str | None]] = []
    declared_namespaces = file_namespaces(text)
    reports: dict[str, list[int]] = {}

    for m in MACRO_RE.finditer(text):
        macro = m.group(1)
        call_start = m.start()
        paren_open = m.end() - 1
        paren_close = find_matching_paren(text, paren_open)
        if paren_close is None:
            continue
        args_text = text[paren_open + 1 : paren_close]
        line = text.count("\n", 0, call_start) + 1

        replacement: str | None = None

        if macro in SIMPLE_RENAMES:
            target = SIMPLE_RENAMES[macro]
            # CHECK/REQUIRE go through Decomposer; a stray top-level bitwise
            # op would be applied to a Result. Wrap the whole expression.
            if target in ("CHECK", "REQUIRE") and has_top_level_bitwise(args_text):
                replacement = f"{target}(({args_text}))"
            else:
                replacement = f"{target}({args_text})"

        elif macro in MESSAGE_RENAMES:
            args = split_top_level_args(args_text)
            if len(args) >= 2:
                expr = wrap_if_bitwise(args[0])
                msg = ",".join(args[1:]).strip()
                replacement = f"{MESSAGE_RENAMES[macro]}({expr}, {msg})"

        elif macro in COMPARISON_REWRITES:
            new_name, op = COMPARISON_REWRITES[macro]
            args = split_top_level_args(args_text)
            if len(args) == 2:
                a = wrap_if_bitwise(args[0])
                b = wrap_if_bitwise(args[1])
                replacement = f"{new_name}({a} {op} {b})"

        elif macro == "BOOST_CHECK_EQUAL_COLLECTIONS":
            args = split_top_level_args(args_text)
            if len(args) == 4:
                lhs = collapse_iter_pair(args[0], args[1])
                rhs = collapse_iter_pair(args[2], args[3])
                replacement = f"CHECK_EQUAL_RANGES({lhs}, {rhs})"

        elif macro == "BOOST_ERROR":
            replacement = f"CHECK(false, {args_text.strip()})"

        elif macro == "BOOST_AUTO_TEST_CASE":
            args = split_top_level_args(args_text)
            name = quote_name(args[0])
            fixture = suite_stack[-1][0] if suite_stack else None
            if fixture:
                replacement = f"FIXTURE_TEST_CASE({name}, {fixture})"
            else:
                replacement = f"TEST_CASE({name})"

        elif macro == "BOOST_FIXTURE_TEST_CASE":
            args = split_top_level_args(args_text)
            if len(args) >= 2:
                name = quote_name(args[0])
                fixture = args[1].strip()
                replacement = f"FIXTURE_TEST_CASE({name}, {fixture})"

        elif macro == "BOOST_AUTO_TEST_SUITE":
            args = split_top_level_args(args_text)
            suite_name = args[0].strip()
            ns = suite_name if suite_name in declared_namespaces else None
            suite_stack.append((None, ns))
            prefix = f"namespace {ns} {{\n" if ns else ""
            replacement = f'{prefix}TEST_SUITE_BEGIN("{suite_name}")'

        elif macro == "BOOST_FIXTURE_TEST_SUITE":
            args = split_top_level_args(args_text)
            if len(args) >= 2:
                suite_name = args[0].strip()
                fixture = args[1].strip()
                ns = suite_name if suite_name in declared_namespaces else None
                suite_stack.append((fixture, ns))
                prefix = f"namespace {ns} {{\n" if ns else ""
                replacement = f'{prefix}TEST_SUITE_BEGIN("{suite_name}")'

        elif macro == "BOOST_AUTO_TEST_SUITE_END":
            ns = suite_stack.pop()[1] if suite_stack else None
            suffix = f"\n}} // namespace {ns}" if ns else ""
            replacement = f"TEST_SUITE_END(){suffix}"

        elif macro in COMMENTED_OUT:
            replacement = f"/* {macro}({args_text}) */"

        elif macro in REPORTED_ONLY:
            reports.setdefault(macro, []).append(line)

        if replacement is None:
            continue

        out.append(text[cursor:call_start])
        out.append(replacement)
        cursor = paren_close + 1

    out.append(text[cursor:])
    return "".join(out), reports


def find_remaining_boost(text: str, exclude: set[str]) -> dict[str, list[int]]:
    """Scan for any BOOST_* identifier still present, excluding known-allowed
    ones (e.g. macros we deliberately left untouched)."""
    remaining: dict[str, list[int]] = {}
    for m in re.finditer(r"\bBOOST_[A-Z_]+", text):
        name = m.group(0)
        if name in exclude:
            continue
        line = text.count("\n", 0, m.start()) + 1
        remaining.setdefault(name, []).append(line)
    return remaining


def transform(text: str) -> tuple[str, dict[str, list[int]]]:
    text = rewrite_includes_and_module(text)
    text, reports = rewrite_macros(text)
    leftover = find_remaining_boost(text, exclude=set(REPORTED_ONLY))
    for macro, lines in leftover.items():
        reports.setdefault(macro, []).extend(lines)
    return text, reports


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def iter_source_files(paths: list[Path]):
    for p in paths:
        if p.is_file():
            if p.suffix in (".cpp", ".h", ".hpp", ".cc"):
                yield p
            continue
        for f in p.rglob("*"):
            if f.is_file() and f.suffix in (".cpp", ".h", ".hpp", ".cc"):
                yield f


def main() -> int:
    ap = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
    )
    ap.add_argument("paths", nargs="+", type=Path)
    ap.add_argument("--check", action="store_true", help="Exit 1 if any file would change")
    ap.add_argument("--quiet", action="store_true")
    args = ap.parse_args()

    total = 0
    changed = 0
    all_reports: dict[Path, dict[str, list[int]]] = {}

    for path in iter_source_files(args.paths):
        total += 1
        original = path.read_text()
        new_text, reports = transform(original)
        if new_text != original:
            changed += 1
            if not args.check:
                path.write_text(new_text)
            if not args.quiet:
                verb = "would rewrite" if args.check else "rewrote"
                print(f"{verb} {path}")
        if reports:
            all_reports[path] = reports

    if all_reports and not args.quiet:
        print("\nUnconverted BOOST_* identifiers (manual review):")
        for path in sorted(all_reports):
            for macro in sorted(all_reports[path]):
                for ln in all_reports[path][macro]:
                    print(f"  {path}:{ln}: {macro}")

    if not args.quiet:
        verb = "would rewrite" if args.check else "rewrote"
        print(f"\n{verb} {changed}/{total} file(s)")

    return 1 if args.check and changed > 0 else 0


if __name__ == "__main__":
    sys.exit(main())

@rustaceanrob rustaceanrob force-pushed the boost-removal branch 4 times, most recently from 896e74d to 742edb9 Compare June 3, 2026 12:35
@rustaceanrob rustaceanrob force-pushed the boost-removal branch 13 times, most recently from 3367419 to c807d70 Compare June 6, 2026 09:26
-BEGIN VERIFY SCRIPT-
set -eu

MACRO_RE='BOOST_CHECK|BOOST_REQUIRE|BOOST_CHECK_MESSAGE|BOOST_REQUIRE_MESSAGE|BOOST_CHECK_NO_THROW|BOOST_REQUIRE_NO_THROW'

FILES=$(git grep -lE "\b(${MACRO_RE})[[:space:]]*\(" -- \
    ':(glob)src/test/**/*.cpp' ':(glob)src/test/**/*.h' \
    ':(glob)src/test/*.cpp' ':(glob)src/test/*.h' \
    ':(glob)src/ipc/test/**/*.cpp' ':(glob)src/ipc/test/**/*.h' \
    ':(glob)src/ipc/test/*.cpp' ':(glob)src/ipc/test/*.h' 2>/dev/null || true)

if [ -z "$FILES" ]; then
    echo "no matching files"
    exit 0
fi

perl -i -0777 -pe '
use strict;
use warnings;

my $names = "BOOST_CHECK|BOOST_REQUIRE|BOOST_CHECK_MESSAGE|BOOST_REQUIRE_MESSAGE|BOOST_CHECK_NO_THROW|BOOST_REQUIRE_NO_THROW";
my $re = qr/\b($names)\s*\(/;
my @DIGIT_SEP_PREV = (0) x 256;
$DIGIT_SEP_PREV[ord($_)] = 1 for split //, "0123456789abcdefABCDEF" . chr(39);

sub is_char_open {
    my ($s, $i) = @_;
    return 1 if $i == 0;
    return $DIGIT_SEP_PREV[ord(substr($$s, $i-1, 1))] ? 0 : 1;
}

sub close_paren {
    my ($s, $open) = @_;
    my ($depth, $i, $in_str, $in_char) = (0, $open, 0, 0);
    my $n = length($$s);
    while ($i < $n) {
        my $c = substr($$s, $i, 1);
        if ($in_str) {
            if ($c eq "\\") { $i += 2; next; }
            $in_str = 0 if $c eq q{"};
        } elsif ($in_char) {
            if ($c eq "\\") { $i += 2; next; }
            $in_char = 0 if $c eq chr(39);
        } elsif ($c eq q{"}) { $in_str = 1; }
        elsif ($c eq chr(39) && is_char_open($s, $i)) { $in_char = 1; }
        elsif ($c =~ /[(\[{]/) { $depth++; }
        elsif ($c =~ /[)\]}]/) { $depth--; return $i if $depth == 0; }
        $i++;
    }
    return -1;
}

sub split_first_comma {
    my ($s) = @_;
    my ($depth, $i, $in_str, $in_char) = (0, 0, 0, 0);
    my $n = length($s);
    while ($i < $n) {
        my $c = substr($s, $i, 1);
        if ($in_str) {
            if ($c eq "\\") { $i += 2; next; }
            $in_str = 0 if $c eq q{"};
        } elsif ($in_char) {
            if ($c eq "\\") { $i += 2; next; }
            $in_char = 0 if $c eq chr(39);
        } elsif ($c eq q{"}) { $in_str = 1; }
        elsif ($c eq chr(39) && is_char_open(\$s, $i)) { $in_char = 1; }
        elsif ($c =~ /[(\[{]/) { $depth++; }
        elsif ($c =~ /[)\]}]/) { $depth--; }
        elsif ($c eq "," && $depth == 0) {
            return (substr($s, 0, $i), substr($s, $i));
        }
        $i++;
    }
    return ($s, "");
}

sub top_level_and_positions {
    my ($e) = @_;
    my @ops;
    my ($depth, $i, $in_str, $in_char) = (0, 0, 0, 0);
    my $n = length($e);
    while ($i < $n) {
        my $c = substr($e, $i, 1);
        if ($in_str) {
            if ($c eq "\\") { $i += 2; next; }
            $in_str = 0 if $c eq q{"};
            $i++; next;
        } elsif ($in_char) {
            if ($c eq "\\") { $i += 2; next; }
            $in_char = 0 if $c eq chr(39);
            $i++; next;
        }
        if ($c eq q{"}) { $in_str = 1; $i++; next; }
        if ($c eq chr(39) && is_char_open(\$e, $i)) { $in_char = 1; $i++; next; }
        if ($c =~ /[(\[{]/) { $depth++; $i++; next; }
        if ($c =~ /[)\]}]/) { $depth--; $i++; next; }
        if ($depth == 0 && $i + 1 < $n && substr($e, $i, 2) eq "&&") {
            push @ops, $i;
            $i += 2; next;
        }
        $i++;
    }
    return @ops;
}

my $text = $_;
my $out  = "";
my $cur  = 0;

while ($text =~ /$re/g) {
    my $macro    = $1;
    my $m_start  = $-[0];
    my $p_open   = $+[0] - 1;
    my $p_close  = close_paren(\$text, $p_open);
    next if $p_close < 0;

    my $args = substr($text, $p_open + 1, $p_close - $p_open - 1);
    my ($expr_raw, $message) = split_first_comma($args);
    my $expr = $expr_raw;
    $expr =~ s/^\s+//;
    $expr =~ s/\s+$//;

    my @ats = top_level_and_positions($expr);
    next unless @ats;

    # Compose the replacement with original indentation.
    my $line_start = rindex(substr($text, 0, $m_start), "\n") + 1;
    my $indent = substr($text, $line_start, $m_start - $line_start);
    $indent =~ s/[^\s].*//s;

    my @Parts;
    my $last = 0;
    for my $p (@ats) {
        my $piece = substr($expr, $last, $p - $last);
        $piece =~ s/^\s+//; $piece =~ s/\s+$//;
        push @Parts, $piece;
        $last = $p + 2;
    }
    my $tail = substr($expr, $last);
    $tail =~ s/^\s+//; $tail =~ s/\s+$//;
    push @Parts, $tail;

    my $sep = ";\n" . $indent;
    my $replacement = join($sep, map { "$macro($_$message)" } @Parts);

    $out .= substr($text, $cur, $m_start - $cur);
    $out .= $replacement;
    $cur = $p_close + 1;
    # Resume the global match where the new content ends.
    pos($text) = $cur;
}
$out .= substr($text, $cur);
$_ = $out;
' -- $FILES
-END VERIFY SCRIPT-
-BEGIN VERIFY SCRIPT-
set -eu

MACRO_RE='BOOST_CHECK|BOOST_REQUIRE|BOOST_CHECK_MESSAGE|BOOST_REQUIRE_MESSAGE|BOOST_CHECK_NO_THROW|BOOST_REQUIRE_NO_THROW'

FILES=$(git grep -lE "\b(${MACRO_RE})[[:space:]]*\(" -- \
    ':(glob)src/test/**/*.cpp' ':(glob)src/test/**/*.h' \
    ':(glob)src/test/*.cpp' ':(glob)src/test/*.h' \
    ':(glob)src/ipc/test/**/*.cpp' ':(glob)src/ipc/test/**/*.h' \
    ':(glob)src/ipc/test/*.cpp' ':(glob)src/ipc/test/*.h' 2>/dev/null || true)

if [ -z "$FILES" ]; then
    echo "no matching files"
    exit 0
fi

perl -i -0777 -pe '
use strict;
use warnings;

my $names = "BOOST_CHECK|BOOST_REQUIRE|BOOST_CHECK_MESSAGE|BOOST_REQUIRE_MESSAGE|BOOST_CHECK_NO_THROW|BOOST_REQUIRE_NO_THROW";
my $re = qr/\b($names)\s*\(/;
my @DIGIT_SEP_PREV = (0) x 256;
$DIGIT_SEP_PREV[ord($_)] = 1 for split //, "0123456789abcdefABCDEF" . chr(39);

sub is_char_open {
    my ($s, $i) = @_;
    return 1 if $i == 0;
    return $DIGIT_SEP_PREV[ord(substr($$s, $i-1, 1))] ? 0 : 1;
}

sub close_paren {
    my ($s, $open) = @_;
    my ($depth, $i, $in_str, $in_char) = (0, $open, 0, 0);
    my $n = length($$s);
    while ($i < $n) {
        my $c = substr($$s, $i, 1);
        if ($in_str) {
            if ($c eq "\\") { $i += 2; next; }
            $in_str = 0 if $c eq q{"};
        } elsif ($in_char) {
            if ($c eq "\\") { $i += 2; next; }
            $in_char = 0 if $c eq chr(39);
        } elsif ($c eq q{"}) { $in_str = 1; }
        elsif ($c eq chr(39) && is_char_open($s, $i)) { $in_char = 1; }
        elsif ($c =~ /[(\[{]/) { $depth++; }
        elsif ($c =~ /[)\]}]/) { $depth--; return $i if $depth == 0; }
        $i++;
    }
    return -1;
}

sub split_first_comma {
    my ($s) = @_;
    my ($depth, $i, $in_str, $in_char) = (0, 0, 0, 0);
    my $n = length($s);
    while ($i < $n) {
        my $c = substr($s, $i, 1);
        if ($in_str) {
            if ($c eq "\\") { $i += 2; next; }
            $in_str = 0 if $c eq q{"};
        } elsif ($in_char) {
            if ($c eq "\\") { $i += 2; next; }
            $in_char = 0 if $c eq chr(39);
        } elsif ($c eq q{"}) { $in_str = 1; }
        elsif ($c eq chr(39) && is_char_open(\$s, $i)) { $in_char = 1; }
        elsif ($c =~ /[(\[{]/) { $depth++; }
        elsif ($c =~ /[)\]}]/) { $depth--; }
        elsif ($c eq "," && $depth == 0) {
            return (substr($s, 0, $i), substr($s, $i));
        }
        $i++;
    }
    return ($s, "");
}

sub has_top_level_or {
    my ($e) = @_;
    my ($depth, $i, $in_str, $in_char) = (0, 0, 0, 0);
    my $n = length($e);
    while ($i < $n) {
        my $c = substr($e, $i, 1);
        if ($in_str) {
            if ($c eq "\\") { $i += 2; next; }
            $in_str = 0 if $c eq q{"};
            $i++; next;
        } elsif ($in_char) {
            if ($c eq "\\") { $i += 2; next; }
            $in_char = 0 if $c eq chr(39);
            $i++; next;
        }
        if ($c eq q{"}) { $in_str = 1; $i++; next; }
        if ($c eq chr(39) && is_char_open(\$e, $i)) { $in_char = 1; $i++; next; }
        if ($c =~ /[(\[{]/) { $depth++; $i++; next; }
        if ($c =~ /[)\]}]/) { $depth--; $i++; next; }
        if ($depth == 0 && $i + 1 < $n && substr($e, $i, 2) eq "||") {
            return 1;
        }
        $i++;
    }
    return 0;
}

my $text = $_;
my $out  = "";
my $cur  = 0;

while ($text =~ /$re/g) {
    my $macro   = $1;
    my $m_start = $-[0];
    my $p_open  = $+[0] - 1;
    my $p_close = close_paren(\$text, $p_open);
    next if $p_close < 0;

    my $args = substr($text, $p_open + 1, $p_close - $p_open - 1);
    my ($expr_raw, $message) = split_first_comma($args);
    my $expr = $expr_raw;
    $expr =~ s/^\s+//;
    $expr =~ s/\s+$//;

    next unless has_top_level_or($expr);

    $out .= substr($text, $cur, $m_start - $cur);
    $out .= "$macro(($expr)$message)";
    $cur = $p_close + 1;
    pos($text) = $cur;
}
$out .= substr($text, $cur);
$_ = $out;
' -- $FILES
-END VERIFY SCRIPT-
@rustaceanrob rustaceanrob force-pushed the boost-removal branch 4 times, most recently from b57087d to 270a034 Compare June 6, 2026 14:43
Adds src/test/util/framework.hpp as a lightweight Boost.Test replacement,
along with CMake and main.cpp changes needed to build against it.

Includes:
- `CHECK`, valid with any comparison operator, optional message
- `REQUIRE`, valid with any comparison operator, optional message
- `CHECK_EQUAL_RANGES`, better debugging for vectors
- `THROW_*`, macros for checking throwing conditions
- Info and warn messages

Includes a scripted diff to replace all BOOST macros with with new
counterparts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant