Ctrl K
ComunidadGuías…

tests/test_cpp_conditional_initializers.py

Rama v8 · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Bounded static branch views recover C++ expression directives without cpp."""
from copy import deepcopy
import json
import subprocess
from pathlib import Path

import pytest

from graphify import extract

pytest.importorskip("tree_sitter_cpp")


def _extract(tmp_path, source):
    path = tmp_path / "conditional.cpp"
    path.write_bytes(source.encode("utf-8") if isinstance(source, str) else source)
    return path, extract.extract_cpp(path)


def _coverage(result):
    return result["nodes"][0]["metadata"]["cpp_conditional_recovery"]


def test_initializer_recovery_preserves_bytes_and_locations(tmp_path, monkeypatch):
    source = ("// \u00e9 byte offsets\r\nnamespace Demo {\r\nint values[] = {\r\n1,\r\n"
              "#ifndef NDEBUG\r\n2,\r\n#endif\r\n3\r\n};\r\nvoid After() {}\r\n}\r\n").encode()
    generic = extract._extract_generic
    parsed = []

    def capture(path, config, **kwargs):
        parsed.append(kwargs["source_override"])
        return generic(path, config, **kwargs)

    monkeypatch.setattr(extract, "_extract_generic", capture)
    _, result = _extract(tmp_path, source)
    assert not result.get("parse_errors")
    assert next(n for n in result["nodes"] if n["label"] == "After()")["source_location"] == "L10"
    recovery = _coverage(result)
    assert recovery["status"] == "recovered"
    assert recovery["variant_count"] == 2
    assert recovery["recovered_from"]["first_error_line"]
    assert recovery["conditions_evaluated"] is False
    assert recovery["combinations_enumerated"] is False
    group = recovery["groups"][0]
    assert group["line"] == 5
    branch = group["branches"][0]
    assert source[branch["start_byte"]:branch["end_byte"]] == b"\r\n2,\r\n"
    assert branch["start_line"] == branch["end_line"] == 6
    assert len(parsed) == 3  # Original plus the present and absent branch views.
    for view in parsed:
        assert len(view) == len(source)
        assert [i for i, c in enumerate(view) if c in b"\r\n"] == [
            i for i, c in enumerate(source) if c in b"\r\n"]
        assert all(before == after or after == ord(" ") for before, after in zip(source, view))


def test_alternatives_are_parsed_separately_and_keep_call_provenance(tmp_path):
    _, result = _extract(tmp_path, """int First(); int Second(); void Run() {
int values[] = {
#if FEATURE
First()
#else
Second()
#endif
};
}
void After() {}
""")
    assert not result.get("parse_errors")
    calls = {c["callee"]: c for c in result["raw_calls"]}
    assert {"First", "Second"} <= calls.keys()
    assert calls["First"]["source_location"] == "L4"
    assert calls["Second"]["source_location"] == "L6"
    assert calls["First"]["metadata"]["cpp_conditions"] == [{"group_line": 3, "branch": 0}]
    assert calls["Second"]["metadata"]["cpp_conditions"] == [{"group_line": 3, "branch": 1}]
    assert _coverage(result)["variant_count"] == 2
    # Flattening these alternatives would produce invalid `First() Second()`.


def test_valid_cpp_keeps_existing_call_pair_deduplication(tmp_path):
    _, result = _extract(tmp_path, "void First() {}\nvoid Run() {\nFirst();\nFirst();\n}\n")
    calls = [e for e in result["edges"] if e["relation"] == "calls"]
    assert len(calls) == 1
    assert calls[0]["source_location"] == "L3"


@pytest.mark.parametrize("group_count", [3, 8])
def test_repeated_conditional_calls_preserve_each_site_without_adding_symbols(tmp_path, group_count):
    lines = ["int First() { return 1; }", "int Second() { return 2; }", "void Run() {"]
    expected_calls = set()
    for index in range(group_count):
        group_line = len(lines) + 2
        lines.extend([
            f"int values_{index}[] = {{", f"#if FEATURE_{index}", "First()",
            "#else", "Second()", "#endif", "};",
        ])
        expected_calls.update({
            ("First()", f"L{group_line + 1}", group_line, 0),
            ("Second()", f"L{group_line + 3}", group_line, 1),
        })
    lines.extend(["}", "void After() {}"])
    path = tmp_path / "conditional.cpp"
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")
    results = [extract.extract([path], root=tmp_path, cache_root=tmp_path, parallel=False)
               for _ in range(2)]
    assert results[0] == results[1]
    for result in results:
        assert sorted(n["label"] for n in result["nodes"]) == [
            "After()", "First()", "Run()", "Second()", "conditional.cpp",
        ]
        nodes = {n["id"]: n for n in result["nodes"]}
        assert len(nodes) == 5
        recovery = next(n for n in nodes.values() if n["label"] == "conditional.cpp")["metadata"]["cpp_conditional_recovery"]
        assert recovery["status"] == "recovered"
        assert recovery["variant_count"] == group_count + 1
        calls = [e for e in result["edges"] if e["relation"] == "calls"]
        assert len(calls) == 2 * group_count
        assert {nodes[e["source"]]["label"] for e in calls} == {"Run()"}
        assert {(nodes[e["target"]]["label"], e["source_location"],
                 e["metadata"]["cpp_conditions"][0]["group_line"],
                 e["metadata"]["cpp_conditions"][0]["branch"]) for e in calls} == expected_calls
        assert all(len(e["metadata"]["cpp_conditions"]) == 1 for e in calls)


def test_nested_conditionals_cover_each_branch_with_ancestor_provenance(tmp_path):
    _, result = _extract(tmp_path, """int One(); int Two(); int Three(); void Run() {
int values[] = {
#if OUTER
#if INNER
One(),
#else
Two(),
#endif
#else
Three(),
#endif
0};
}
""")
    assert not result.get("parse_errors")
    calls = {c["callee"]: c for c in result["raw_calls"]}
    assert {"One", "Two", "Three"} <= calls.keys()
    assert calls["Two"]["metadata"]["cpp_conditions"] == [
        {"group_line": 3, "branch": 0}, {"group_line": 4, "branch": 1}]
    assert calls["Three"]["metadata"]["cpp_conditions"] == [{"group_line": 3, "branch": 1}]
    assert _coverage(result)["variant_count"] == 3


def test_argument_conditionals_recover_calls_without_flattening(tmp_path):
    _, result = _extract(tmp_path, """int First(); int Second(); void Consume(int);
void Run() { Consume(
#if FEATURE
First()
#else
Second()
#endif
); }
""")
    assert not result.get("parse_errors")
    assert {"Consume", "First", "Second"} <= {c["callee"] for c in result["raw_calls"]}
    assert _coverage(result)["status"] == "recovered"


def test_comments_strings_and_raw_strings_do_not_create_directives(tmp_path):
    source = '''const char* raw = u8R"tag(
#if RAW_STRING
#else
)tag";
/*
#ifndef COMMENT
#endif
*/
const char* escaped = "continued\\
#if STRING";
const char character = '#'; // #ifdef LINE_COMMENT
int values[] = {
#ifndef REAL
1,
#endif
0};
'''
    _, result = _extract(tmp_path, source)
    assert not result.get("parse_errors")
    groups = _coverage(result)["groups"]
    assert len(groups) == 1
    assert groups[0]["branches"][0]["directive"] == "#ifndef REAL"


@pytest.mark.parametrize("directives", [
    "#if FLAG\n1,\n",  # Missing endif.
    "#if FLAG\n1,\n#else\n2,\n#else\n3,\n#endif\n",
    "#if FLAG\n1,\n#else\n2,\n#elif OTHER\n3,\n#endif\n",
    "#ifdef FLAG EXTRA\n1,\n#endif\n",
    "#if FLAG\n1,\n#endif EXTRA\n",
    "#if )\n1,\n#endif\n",
    "#if FLAG &&\n1,\n#endif\n",
    "#if defined(\n1,\n#endif\n",
])
def test_malformed_conditionals_retain_original_parse_errors(tmp_path, directives):
    _, result = _extract(tmp_path, "int values[] = {\n" + directives + "0};\n")
    assert result.get("parse_errors")
    assert _coverage(result)["status"] == "unrecovered"
    assert _coverage(result)["recovered_from"] == result["parse_errors"]


def test_unrelated_syntax_errors_are_not_hidden(tmp_path):
    _, result = _extract(tmp_path, """int values[] = {
#if FLAG
1,
#endif
0};
void Broken( {
""")
    assert result.get("parse_errors")
    assert _coverage(result)["status"] == "unrecovered"
    assert _coverage(result)["reason"] == "a branch view still has parse errors"


def test_branch_view_limit_fails_before_trying_variants(tmp_path, monkeypatch):
    monkeypatch.setattr(extract, "_CPP_CONDITIONAL_VARIANT_LIMIT", 2)
    original_generic = extract._extract_generic
    calls = []

    def capture(*args, **kwargs):
        calls.append(1)
        return original_generic(*args, **kwargs)

    monkeypatch.setattr(extract, "_extract_generic", capture)
    _, result = _extract(tmp_path, """int values[] = {
#if FIRST
1,
#endif
#if SECOND
2,
#endif
0};
""")
    assert result.get("parse_errors")
    assert _coverage(result)["reason"] == "branch-view limit exceeded"
    assert len(calls) == 1


@pytest.mark.parametrize("source,reason,parse_count", [
    ("""int values[] {
#if FLAG
[]{ struct Local {}; return 1; }(),
#else
[]{ struct Local {}; return 2; }(),
#endif
0};
""", "branch views have conflicting symbol identities", 3),
    ("int values[] {\n" + "#if FLAG\n1,\n#endif\n" * 129 + "0};\n",
     "conditional-group limit exceeded", 1),
])
def test_rejected_recovery_preserves_the_entire_original_result(
    tmp_path, monkeypatch, source, reason, parse_count,
):
    generic = extract._extract_generic
    parsed = []

    def capture(*args, **kwargs):
        result = generic(*args, **kwargs)
        parsed.append(deepcopy(result))
        return result

    monkeypatch.setattr(extract, "_extract_generic", capture)
    _, result = _extract(tmp_path, source)
    coverage = _coverage(result)
    assert coverage["status"] == "unrecovered"
    assert coverage["reason"] == reason
    assert result["parse_errors"] == parsed[0]["parse_errors"]
    assert len(parsed) == parse_count
    expected = parsed[0]
    expected["nodes"][0].setdefault("metadata", {})["cpp_conditional_recovery"] = coverage
    assert result == expected


def test_include_paths_are_never_opened_and_no_preprocessor_runs(tmp_path, monkeypatch):
    secret = tmp_path / "secret.hpp"
    secret.write_text("HOST_SECRET_SENTINEL")
    path = tmp_path / "conditional.cpp"
    source = (f'#include "{secret.as_posix()}"\n#include "../secret.hpp"\n'
              'int values[] = {\n#if FLAG\n1,\n#endif\n0};\n')
    path.write_text(source)
    read_bytes = Path.read_bytes
    opened = []

    def guarded_read(p):
        opened.append(p)
        assert p == path
        return read_bytes(p)

    def no_process(*args, **kwargs):
        pytest.fail("C++ conditional recovery must not invoke a process")

    monkeypatch.setattr(Path, "read_bytes", guarded_read)
    monkeypatch.setattr(subprocess, "run", no_process)
    result = extract.extract_cpp(path)
    assert not result.get("parse_errors")
    assert opened and set(opened) == {path}
    assert "HOST_SECRET_SENTINEL" not in json.dumps(result)


def test_valid_nonexpression_conditionals_keep_existing_extraction(tmp_path):
    _, result = _extract(tmp_path, """#ifdef FLAG
void First() {}
#else
void Second() {}
#endif
""")
    assert not result.get("parse_errors")
    assert {"First()", "Second()"} <= {n["label"] for n in result["nodes"]}
    assert not result["nodes"][0].get("metadata", {}).get("cpp_conditional_recovery")


def test_digit_separators_do_not_open_character_literals(tmp_path):
    _, result = _extract(tmp_path, """int values[] = {1'000, 0xAB'CD, 0x1.A'Bp0,
#if FLAG
2,
#endif
3};
char later = 'x';
void After() {}
""")
    assert not result.get("parse_errors")
    assert _coverage(result)["groups"][0]["branches"][0]["directive"] == "#if FLAG"
    assert next(n for n in result["nodes"] if n["label"] == "After()")["source_location"] == "L7"


def test_spliced_line_comments_hide_directive_looking_lines(tmp_path):
    source = ("int values[] = {\n// continued comment \\\n#if PHANTOM\n1,\n"
              "// another continued comment \\\n#endif\n#ifndef REAL\n2,\n#endif\n0};\n")
    groups, problem = extract._cpp_conditional_groups(source.encode())
    assert problem is None
    assert len(groups) == 1
    assert groups[0]["branches"][0]["directive"] == "#ifndef REAL"
    _, result = _extract(tmp_path, source)
    assert not result.get("parse_errors")
    assert len(_coverage(result)["groups"]) == 1


def test_spliced_non_directive_line_is_not_recovered(tmp_path):
    source = "int values[] = { \\\n#if PHANTOM\n1};\n"
    groups, problem = extract._cpp_conditional_groups(source.encode())
    assert groups == [] and problem is None
    _, result = _extract(tmp_path, source)
    assert result.get("parse_errors")
    assert not result["nodes"][0].get("metadata", {}).get("cpp_conditional_recovery")


def test_multiline_directive_preserves_original_body_offsets(tmp_path):
    source = b"int values[] = {\r\n#if defined(ONE) && \\\r\n defined(TWO)\r\n2,\r\n#endif\r\n0};\r\n"
    _, result = _extract(tmp_path, source)
    assert not result.get("parse_errors")
    branch = _coverage(result)["groups"][0]["branches"][0]
    assert branch["directive"] == "#if defined(ONE) && \\\r\n defined(TWO)"
    assert source[branch["start_byte"]:branch["end_byte"]] == b"\r\n2,\r\n"
    assert branch["start_line"] == branch["end_line"] == 4


@pytest.mark.parametrize("outer", [False, True])
def test_split_member_names_without_precise_guard_provenance_fail_closed(tmp_path, outer):
    source = """struct Worker { int First(){return 1;} int Second(){return 2;} };
void Consume(int);
void Run(Worker& w) { Consume(w.
#if FLAG
First
#else
Second
#endif
()); }
"""
    if outer:
        source = "#ifdef OUTER\n" + source + "#endif\n"
    _, result = _extract(tmp_path, source)
    assert result.get("parse_errors")
    assert _coverage(result)["status"] == "unrecovered"
    assert _coverage(result)["reason"] == "branch-spanning records lack source guard provenance"


@pytest.mark.parametrize("warm", [False, True])
@pytest.mark.parametrize("expression", ["w.Tick<int>()", "Fetch()"])
def test_later_resolver_edges_keep_the_callsite_conditions(tmp_path, warm, expression):
    header = tmp_path / "worker.hpp"
    header.write_text("int Fetch() { return 1; }\n"
                      "struct Worker { template<class T> int Tick() { return 1; } };", encoding="utf-8")
    caller = tmp_path / "use.cpp"
    caller.write_text('''#include "worker.hpp"
void Run(Worker& w) { int values[] = {
#if FEATURE
''' + expression + ''',
#else
0,
#endif
}; }
''', encoding="utf-8")
    result = extract.extract([header, caller], root=tmp_path, cache_root=tmp_path, parallel=False)
    cold_calls = [e for e in result["edges"] if e["relation"] == "calls"]
    if warm:
        result = extract.extract([caller, header], root=tmp_path, cache_root=tmp_path, parallel=False)
    calls = [e for e in result["edges"] if e["relation"] == "calls"]
    assert calls == cold_calls
    assert len(calls) == 1
    assert calls[0]["metadata"]["cpp_conditions"] == [{"group_line": 3, "branch": 0}]
    assert calls[0]["source_file"] == "use.cpp"
    assert calls[0]["source_location"] == "L4"
    if not warm:
        fresh = extract.extract(
            [caller], root=tmp_path, cache_root=tmp_path, parallel=False,
            resolution_context_nodes=[n for n in result["nodes"] if n.get("source_file") != "use.cpp"],
            resolution_context_edges=[e for e in result["edges"] if e.get("source_file") != "use.cpp"],
        )
        assert [e for e in fresh["edges"] if e["relation"] == "calls"] == calls


@pytest.mark.parametrize("coverage", [
    {"status": "unrecovered", "groups": [{"line": 1, "branches": [{"start_line": 2, "end_line": 4}]}]},
    {"status": "recovered", "groups": None},
    {"status": "recovered", "groups": [{"line": 1, "branches": [None, {"start_line": "2", "end_line": 4}]}]},
])
def test_unrecovered_or_malformed_cached_ranges_cannot_add_conditions(coverage):
    nodes = [{"source_file": "use.cpp", "metadata": {"cpp_conditional_recovery": coverage}}]
    edge = {"source_file": "use.cpp", "source_location": "L3"}
    extract._stamp_cpp_condition_provenance(nodes, [edge])
    assert "metadata" not in edge