225 lines
6.5 KiB
Python
225 lines
6.5 KiB
Python
import os
|
|
import sys
|
|
import pytest
|
|
from panda3d import core
|
|
from direct.showbase.ShowBase import ShowBase
|
|
|
|
|
|
@pytest.fixture
|
|
def base():
|
|
base = ShowBase(windowType='none')
|
|
yield base
|
|
base.destroy()
|
|
|
|
|
|
@pytest.fixture
|
|
def tk_toplevel():
|
|
tk = pytest.importorskip('tkinter')
|
|
|
|
if sys.platform == 'darwin' and not core.ConfigVariableBool('want-tk', False):
|
|
pytest.skip('"want-tk" must be true to use tkinter with Panda3D on macOS')
|
|
try:
|
|
root = tk.Toplevel()
|
|
except tk.TclError as e:
|
|
pytest.skip(str(e))
|
|
yield root
|
|
root.destroy()
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def graphics_pipe():
|
|
from panda3d.core import GraphicsPipeSelection
|
|
|
|
pipe = GraphicsPipeSelection.get_global_ptr().make_default_pipe()
|
|
|
|
if pipe is None or not pipe.is_valid():
|
|
pytest.skip("GraphicsPipe is invalid")
|
|
|
|
yield pipe
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def graphics_engine():
|
|
from panda3d.core import GraphicsEngine
|
|
|
|
engine = GraphicsEngine.get_global_ptr()
|
|
yield engine
|
|
|
|
# This causes GraphicsEngine to also terminate the render threads.
|
|
engine.remove_all_windows()
|
|
|
|
|
|
@pytest.fixture
|
|
def window(graphics_pipe, graphics_engine):
|
|
from panda3d.core import GraphicsPipe, FrameBufferProperties, WindowProperties
|
|
|
|
fbprops = FrameBufferProperties.get_default()
|
|
winprops = WindowProperties.get_default()
|
|
|
|
win = graphics_engine.make_output(
|
|
graphics_pipe,
|
|
'window',
|
|
0,
|
|
fbprops,
|
|
winprops,
|
|
GraphicsPipe.BF_require_window
|
|
)
|
|
graphics_engine.open_windows()
|
|
|
|
if win is None:
|
|
pytest.skip("GraphicsPipe cannot make windows")
|
|
|
|
yield win
|
|
|
|
if win is not None:
|
|
graphics_engine.remove_window(win)
|
|
|
|
|
|
@pytest.fixture(scope='module')
|
|
def gsg(graphics_pipe, graphics_engine):
|
|
"Returns a windowless GSG that can be used for offscreen rendering."
|
|
from panda3d.core import GraphicsPipe, FrameBufferProperties, WindowProperties
|
|
|
|
fbprops = FrameBufferProperties()
|
|
fbprops.force_hardware = True
|
|
|
|
buffer = graphics_engine.make_output(
|
|
graphics_pipe,
|
|
'buffer',
|
|
0,
|
|
fbprops,
|
|
WindowProperties.size(32, 32),
|
|
GraphicsPipe.BF_refuse_window
|
|
)
|
|
graphics_engine.open_windows()
|
|
|
|
if buffer is None:
|
|
pytest.skip("GraphicsPipe cannot make offscreen buffers")
|
|
|
|
yield buffer.gsg
|
|
|
|
if buffer is not None:
|
|
graphics_engine.remove_window(buffer)
|
|
|
|
|
|
def pytest_configure(config):
|
|
"""Initialize the failure collector."""
|
|
config._github_summary_failures = []
|
|
config._github_summary_counts = {
|
|
"passed": 0,
|
|
"failed": 0,
|
|
"skipped": 0,
|
|
"xfailed": 0,
|
|
}
|
|
|
|
|
|
@pytest.hookimpl(hookwrapper=True)
|
|
def pytest_runtest_makereport(item, call):
|
|
"""Collect information about test outcomes."""
|
|
outcome = yield
|
|
report = outcome.get_result()
|
|
|
|
if report.when == "call":
|
|
counts = item.config._github_summary_counts
|
|
|
|
if report.passed:
|
|
if hasattr(report, "wasxfail"):
|
|
# xpass - passed but was expected to fail (treat as passed for now)
|
|
counts["passed"] += 1
|
|
else:
|
|
counts["passed"] += 1
|
|
elif report.failed:
|
|
counts["failed"] += 1
|
|
failure_info = {
|
|
"nodeid": report.nodeid,
|
|
"location": report.location,
|
|
"longrepr": str(report.longrepr) if report.longrepr else None,
|
|
"sections": report.sections,
|
|
"duration": report.duration,
|
|
}
|
|
item.config._github_summary_failures.append(failure_info)
|
|
elif report.skipped:
|
|
if hasattr(report, "wasxfail"):
|
|
counts["xfailed"] += 1
|
|
else:
|
|
counts["skipped"] += 1
|
|
|
|
elif report.when == "setup" and report.skipped:
|
|
# Handle skip during setup (e.g., skipif, skip markers)
|
|
item.config._github_summary_counts["skipped"] += 1
|
|
|
|
|
|
def pytest_sessionfinish(session, exitstatus):
|
|
"""Write GitHub step summary if GITHUB_STEP_SUMMARY is set."""
|
|
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
if not summary_path:
|
|
return
|
|
|
|
failures = session.config._github_summary_failures
|
|
counts = session.config._github_summary_counts
|
|
|
|
lines = []
|
|
|
|
# Build status parts for the summary line
|
|
status_parts = []
|
|
if counts["failed"]:
|
|
status_parts.append(f"**{counts['failed']}** failed")
|
|
if counts["passed"]:
|
|
status_parts.append(f"**{counts['passed']}** passed")
|
|
if counts["skipped"]:
|
|
status_parts.append(f"**{counts['skipped']}** skipped")
|
|
if counts["xfailed"]:
|
|
status_parts.append(f"**{counts['xfailed']}** xfailed")
|
|
|
|
total = sum(counts.values())
|
|
status_line = ", ".join(status_parts) + f" (**{total}** total)\n"
|
|
|
|
# Header with overall status
|
|
if counts["failed"] == 0:
|
|
lines.append(f"### :white_check_mark: All tests passed (Python {sys.version_info.major}.{sys.version_info.minor})\n\n")
|
|
else:
|
|
lines.append(f"### :x: Test failures (Python {sys.version_info.major}.{sys.version_info.minor})\n\n")
|
|
|
|
lines.append(status_line)
|
|
lines.append("\n")
|
|
|
|
# Each failure in a collapsible section
|
|
for failure in failures:
|
|
nodeid = failure["nodeid"]
|
|
duration = failure["duration"]
|
|
|
|
lines.append(f"<details>\n<summary><code>{_escape_html(nodeid)}</code> ({duration:.2f}s)</summary>\n")
|
|
|
|
# Traceback / longrepr
|
|
if failure["longrepr"]:
|
|
lines.append("\n#### Traceback\n")
|
|
lines.append("```python\n")
|
|
lines.append(failure["longrepr"])
|
|
if not failure["longrepr"].endswith("\n"):
|
|
lines.append("\n")
|
|
lines.append("```\n")
|
|
|
|
# Captured output sections (stdout, stderr, log, etc.)
|
|
for section_name, section_content in failure["sections"]:
|
|
if section_content.strip():
|
|
lines.append(f"\n#### {_escape_html(section_name)}\n")
|
|
lines.append("```\n")
|
|
lines.append(section_content)
|
|
if not section_content.endswith("\n"):
|
|
lines.append("\n")
|
|
lines.append("```\n")
|
|
|
|
lines.append("\n</details>\n\n")
|
|
|
|
with open(summary_path, "a", encoding="utf-8") as f:
|
|
f.writelines(lines)
|
|
|
|
|
|
def _escape_html(text: str) -> str:
|
|
"""Escape HTML special characters for safe rendering in markdown."""
|
|
return (
|
|
text.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
)
|