Skip to content

imgui_test_engine: items inside begin_child unreachable via path strings; set_ref(window_info(...).window) hard-aborts #468

@boazmohar

Description

@boazmohar

Two related path-resolution issues in imgui_test_engine glue when items are nested inside child windows

Environment

  • imgui_bundle 1.92.601 (wheel, Python 3.13, Windows 11)
  • Backend: hello_imgui.run with app_window_params.hidden = True and use_imgui_test_engine = True (real GLFW backend, hidden window — same headless setup the test engine demos use; same behavior reproduced under default visible-window run)
  • gh --version for context: 2.89.0

I ran into two related issues while writing imgui_test_engine tests for a Python app whose layout uses imgui.begin_child(...) inside an imgui.columns(...) legacy block. The user-facing items (tree nodes, selectables) inside the child are not addressable from any path string I tried, and the wiki-recommended set_ref(window_info(...).window) overload aborts at the C++ layer.

Finding 1: items inside begin_child unreachable via every probed path string

ctx.item_exists returns False for items inside begin_child regardless of the path form, while items at the parent-window scope resolve normally.

Minimal repro (full script below):

imgui.begin("Window", True)
imgui.button("Close##sv_close")  # parent-window item
imgui.begin_child("foo_child", imgui.ImVec2(0.0, 0.0))
for i in range(20):
    imgui.tree_node(f"Animal {i}###sv_animal_BM{i}")  # child-window items
imgui.end_child()
imgui.end()

Test script (after ctx.set_ref("Window") + 8 yields):

Path probed ctx.item_exists
Close##sv_close True
//Window/Close##sv_close True
###sv_animal_BM6 False
Window/###sv_animal_BM6 False
Window/foo_child/###sv_animal_BM6 False
foo_child/###sv_animal_BM6 False
//foo_child/###sv_animal_BM6 False
//Window/foo_child/###sv_animal_BM6 False

Also: ctx.window_info("foo_child") returns a non-null window object but reports id == 0. (The wiki note at the binding's test_engine.pyi line 1473 says child-window paths are "internally mangled" — but no path I formed found the items.)

Finding 2: ctx.set_ref(ctx.window_info(name).window) hard-aborts at C++

The wiki-recommended next step (binding pyi line 1473):

SetRef(WindowInfo("Parent/Child")->Window) --> set ref to child window.

Translated literally:

info = ctx.window_info("foo_child")
ctx.set_ref(info.window)        # <-- hard-aborts at the C++ layer

The Python process exits with Fatal Python error: Aborted immediately on the set_ref(...) call (no Python exception, no engine log entry, no status from the test runner). No stack frames in the user code — the abort is from native assert() somewhere inside ImGuiTestContext::SetRef(ImGuiWindow*).

Workaround attempted: ctx.set_ref("foo_child") (bare child str_id) — runs without abort, but subsequent ctx.item_exists("###sv_animal_BM6") still returns False (Finding 1 again).

Why this matters

I'm building a section-viewer in a Python app with ~970 sections across 39 animals. The user needs native imgui.begin_child for the animal-list scroll (manual-scroll mechanism doesn't deliver real wheel input from real hardware reliably). With this binding limitation, our entire test suite (~28 call sites that navigate tree_nodeselectable to pick a section) cannot exercise the live render path. We worked around it with a hybrid render-mode flag (live GUI = begin_child, test backend = parent-scope render via a module flag toggled by the test runner), but that means our test suite no longer exercises the actual production rendering of the browser.

A fix to either (a) make path strings resolve into child windows, or (b) make set_ref(window_info(...).window) not abort, would let us drop the hybrid path and test the production render directly.

Full minimal repro script

"""Minimal repro: items inside ``begin_child`` unreachable via path
strings; ``set_ref(window_info(...).window)`` aborts at C++.

Run: pixi run -e delta python this_file.py  (or any imgui_bundle env).
"""

from __future__ import annotations

from imgui_bundle import hello_imgui, imgui


def gui() -> None:
    imgui.set_next_window_size(imgui.ImVec2(420.0, 360.0), imgui.Cond_.first_use_ever.value)
    imgui.begin("Window", True)
    if imgui.button("Close##sv_close"):
        pass
    imgui.text("---")
    visible = imgui.begin_child("foo_child", imgui.ImVec2(0.0, 0.0))
    if visible:
        for i in range(20):
            imgui.tree_node(f"Animal {i}###sv_animal_BM{i}")
    imgui.end_child()
    imgui.end()


_state: dict = {"test": None}


def register_tests() -> None:
    engine = hello_imgui.get_imgui_test_engine()
    te = imgui.test_engine
    io = te.get_io(engine)
    io.config_run_speed = te.TestRunSpeed.fast
    io.config_no_throttle = True
    io.config_log_to_tty = False
    io.config_capture_enabled = False
    io.config_watchdog_kill_test = 15.0
    io.config_watchdog_kill_app = 20.0

    test = te.register_test(engine, "spike", "child_path_resolve")
    _state["test"] = test

    def script(ctx) -> None:
        ctx.set_ref("Window")
        for _ in range(8):
            ctx.yield_()

        # Parent-window controls (these both work).
        print(
            f"[parent] Close##sv_close exists at parent ref:           "
            f"{ctx.item_exists('Close##sv_close')!r}"
        )
        print(
            f"[parent] //Window/Close##sv_close exists:                "
            f"{ctx.item_exists('//Window/Close##sv_close')!r}"
        )

        # Items inside begin_child (all 6 path variants tried).
        for path in [
            "###sv_animal_BM6",
            "Window/###sv_animal_BM6",
            "Window/foo_child/###sv_animal_BM6",
            "foo_child/###sv_animal_BM6",
            "//foo_child/###sv_animal_BM6",
            "//Window/foo_child/###sv_animal_BM6",
        ]:
            try:
                ok = ctx.item_exists(path)
            except Exception as exc:
                ok = f"err={exc!r}"
            print(f"[child]  {path:<55} exists: {ok!r}")

        info = ctx.window_info("foo_child")
        is_none = getattr(info, "window", None) is None
        wid = int(getattr(info, "id", 0))
        print(
            f"[child]  window_info('foo_child').window is_none={is_none}, id={wid}"
        )
        # The line below is the wiki-recommended pattern; it
        # HARD-ABORTS at the C++ layer on this binding. Commented
        # so the script can complete.
        # ctx.set_ref(info.window)

        try:
            ctx.engine.app_shall_exit = True
        except Exception:
            pass

    test.test_func = script
    te.queue_test(engine, test)


def post_new_frame() -> None:
    test = _state.get("test")
    if test is None:
        return
    te = imgui.test_engine
    status = test.output.status
    live = hello_imgui.get_runner_params()
    if status == te.TestStatus.success or status == te.TestStatus.error:
        live.app_shall_exit = True
    elif live.app_shall_exit:
        live.app_shall_exit = False


def main() -> None:
    rp = hello_imgui.RunnerParams()
    rp.app_window_params.window_title = "spike_child_path_resolve"
    rp.app_window_params.window_geometry.size = (640, 480)
    rp.app_window_params.hidden = True
    rp.use_imgui_test_engine = True
    rp.ini_disable = True
    rp.callbacks.show_gui = gui
    rp.callbacks.register_tests = register_tests
    rp.callbacks.post_new_frame = post_new_frame
    hello_imgui.run(rp)


if __name__ == "__main__":
    main()

Repro output (literal)

[parent] Close##sv_close exists at parent ref:           True
[parent] //Window/Close##sv_close exists:                True
[child]  ###sv_animal_BM6                                        exists: False
[child]  Window/###sv_animal_BM6                                 exists: False
[child]  Window/foo_child/###sv_animal_BM6                       exists: False
[child]  foo_child/###sv_animal_BM6                              exists: False
[child]  //foo_child/###sv_animal_BM6                            exists: False
[child]  //Window/foo_child/###sv_animal_BM6                     exists: False
[child]  window_info('foo_child').window is_none=False, id=0

Uncomment the ctx.set_ref(info.window) line near the bottom of the script to reproduce Finding 2 (Python exits with Fatal Python error: Aborted immediately on that call; no engine log, no Python stack frame in user code).

What I'm hoping for

Either (a) make ctx.item_exists("Window/foo_child/###sv_animal_BM6") (or some documented form) resolve, or (b) make ctx.set_ref(window_info("foo_child").window) not abort. Happy to test a candidate fix on the same setup if a maintainer wants me to.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions