Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FlashXTest/__meta__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Metadata for FlashXTest package"""

__pkgname__ = "FlashXTest"
__version__ = "2025.02"
__version__ = "2026.06.b"
__authors__ = "The Flash-X Team"
__license__ = "MIT License"
36 changes: 18 additions & 18 deletions FlashXTest/backend/Webview/modern_static/build_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
"""

import html
import os
import re
from pathlib import Path
from typing import List, Dict
from urllib.parse import quote as url_quote

from .html_utils import page_header, page_header_nobody, page_footer


def generate_build_page_frameset(site_name: str, inv_name: str, build_name: str) -> str:
def generate_build_page_frameset(site_name: str, inv_name: str, build_name: str, css_content: str) -> str:
"""Return a frameset HTML page."""
title = f"{site_name} – {inv_name} – {build_name}"
lines: List[str] = page_header_nobody(title, css_href="../../../style.css")
lines: List[str] = page_header_nobody(title, css_content)
lines.append('<frameset cols="40%,*">')
lines.append(' <frame src="leftframe.html" name="leftframe">')
lines.append(' <frame src="rightframe.html" name="rightframe">')
Expand All @@ -23,13 +25,13 @@ def generate_build_page_frameset(site_name: str, inv_name: str, build_name: str)


def generate_left_frame_html(
site_name: str, inv_name: str, build_name: str, build_dir: Path
site_name: str, inv_name: str, build_name: str, build_dir: Path, build_output_dir: Path, css_content: str
) -> str:
"""Return HTML for the left frame listing structured sections for a build."""
title = f"{site_name} – {inv_name} – {build_name}"
lines: List[str] = page_header(
title,
css_href="../../../style.css",
css_content,
base_target="rightframe",
body_class="left-frame",
)
Expand All @@ -39,14 +41,17 @@ def generate_left_frame_html(
+ f"{html.escape(inv_name)}</a></h2>"
)

def _rel_href(file: Path) -> str:
return url_quote(os.path.relpath(file, build_output_dir), safe="/")

# invocation files
lines.append("<ul>")
ti = build_dir / "test.info"
if ti.is_file():
lines.append(f' <li><a href="{ti.resolve().as_uri()}">test.info</a></li>')
lines.append(f' <li><a href="{_rel_href(ti)}">test.info</a></li>')
df = build_dir / "deleted_files"
if df.is_file():
lines.append(f' <li><a href="{df.resolve().as_uri()}">deleted_files</a></li>')
lines.append(f' <li><a href="{_rel_href(df)}">deleted_files</a></li>')
lines.append("</ul>")

# Setup
Expand All @@ -58,7 +63,7 @@ def generate_left_frame_html(
call_text = sc.read_text().strip()
if so.is_file():
lines.append(
f' <li><a href="{so.resolve().as_uri()}">{html.escape(call_text)}</a></li>'
f' <li><a href="{_rel_href(so)}">{html.escape(call_text)}</a></li>'
)
else:
lines.append(f" <li>{html.escape(call_text)}</li>")
Expand All @@ -73,15 +78,11 @@ def generate_left_frame_html(
lines.append("<h3>Compilation</h3>")
lines.append("<ul>")
if gc.is_file():
lines.append(f' <li><a href="{gc.resolve().as_uri()}">gmake_call</a></li>')
lines.append(f' <li><a href="{_rel_href(gc)}">gmake_call</a></li>')
if go.is_file():
lines.append(
f' <li><a href="{go.resolve().as_uri()}">gmake_output</a></li>'
)
lines.append(f' <li><a href="{_rel_href(go)}">gmake_output</a></li>')
if ge.is_file():
lines.append(
f' <li><a href="{ge.resolve().as_uri()}">gmake_error</a></li>'
)
lines.append(f' <li><a href="{_rel_href(ge)}">gmake_error</a></li>')
lines.append("</ul>")
if ct.is_file():
lines.append(
Expand Down Expand Up @@ -138,8 +139,7 @@ def generate_left_frame_html(
lines.append("</pre>")

def _process_li_line(file: Path) -> str:
"""A Helper function to build string for a list item."""
return f' <li><a href="{file.resolve().as_uri()}">{html.escape(file.name)}</a></li>'
return f' <li><a href="{_rel_href(file)}">{html.escape(file.name)}</a></li>'

lines.append("<ul>")
# checkpoint files
Expand Down Expand Up @@ -211,12 +211,12 @@ def _process_li_line(file: Path) -> str:
return "\n".join(lines)


def generate_right_frame_html(site_name: str, inv_name: str, build_name: str) -> str:
def generate_right_frame_html(site_name: str, inv_name: str, build_name: str, css_content: str) -> str:
"""Return HTML for the right frame initial content."""
title = f"{site_name} – {inv_name} – {build_name}"
lines: List[str] = page_header(
title,
css_href="../../../style.css",
css_content,
body_class="right-frame",
)
lines.append("<p>Select a file from the left to view its contents.</p>")
Expand Down
33 changes: 16 additions & 17 deletions FlashXTest/backend/Webview/modern_static/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,31 +68,30 @@ def generate_flashx_testview(

output_dir.mkdir(parents=True, exist_ok=False)

# Read CSS once; embed inline in every page so files are self-contained
pkg_dir = Path(__file__).parent
style_src = pkg_dir / "style.css"
css_content = style_src.read_text(encoding="utf-8") if style_src.is_file() else ""

# Copy JS assets (still needed as external files)
js_src = pkg_dir / "js"
if js_src.is_dir():
dst_js = output_dir / "js"
shutil.copytree(js_src, dst_js, dirs_exist_ok=True)

# Write overview index.html
index_html = generate_html(board, sites, invocations_sorted, inv_dir_lookup)
index_html = generate_html(board, sites, invocations_sorted, inv_dir_lookup, css_content)
(output_dir / "index.html").write_text(index_html, encoding="utf-8")

# Write site-combined invocation pages
inv_index_dir = output_dir / "invocations"
inv_index_dir.mkdir(parents=True, exist_ok=True)
for inv_name in invocations_sorted:
combined = generate_combined_invocation_page(
inv_name, inv_dir_lookup.get(inv_name, {}), sites
inv_name, inv_dir_lookup.get(inv_name, {}), sites, css_content
)
(inv_index_dir / f"{inv_name}.html").write_text(combined, encoding="utf-8")

# Copy shared CSS and JS assets
pkg_dir = Path(__file__).parent
# style.css
style_src = pkg_dir / "style.css"
if style_src.is_file():
shutil.copy(style_src, output_dir / "style.css")
# js assets
js_src = pkg_dir / "js"
if js_src.is_dir():
dst_js = output_dir / "js"
shutil.copytree(js_src, dst_js, dirs_exist_ok=True)

# Generate build pages (frameset, left/right)
for site_path in site_paths:
site_output_dir = output_dir / site_path.name
Expand All @@ -105,17 +104,17 @@ def generate_flashx_testview(
build_output_dir.mkdir(parents=True, exist_ok=True)
# Frameset
(build_output_dir / "frameset.html").write_text(
generate_build_page_frameset(site_path.name, inv_dir.name, b.name),
generate_build_page_frameset(site_path.name, inv_dir.name, b.name, css_content),
encoding="utf-8",
)
# Left frame
(build_output_dir / "leftframe.html").write_text(
generate_left_frame_html(site_path.name, inv_dir.name, b.name, b),
generate_left_frame_html(site_path.name, inv_dir.name, b.name, b, build_output_dir, css_content),
encoding="utf-8",
)
# Right frame
(build_output_dir / "rightframe.html").write_text(
generate_right_frame_html(site_path.name, inv_dir.name, b.name),
generate_right_frame_html(site_path.name, inv_dir.name, b.name, css_content),
encoding="utf-8",
)

Expand Down
19 changes: 11 additions & 8 deletions FlashXTest/backend/Webview/modern_static/html_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,27 @@ def zebra_row_class(idx: int) -> str:
return "row-alt" if idx % 2 else "row"


def _css_tag(css_content: str) -> str:
return f" <style>\n{css_content}\n </style>"


def page_header(
title: str,
css_href: str,
css_content: str,
base_target: Optional[str] = None,
body_class: Optional[str] = None,
) -> List[str]:
"""Return common HTML header lines including DOCTYPE, head, and opening body.
Optionally set <base> and a CSS class on <body>."""
"""Return common HTML header lines including DOCTYPE, head, and opening body."""
lines: List[str] = [
"<!DOCTYPE html>",
'<html lang="en">',
"<head>",
' <meta charset="utf-8">',
' <meta name="viewport" content="width=device-width, initial-scale=1">',
f" <title>{html.escape(title)}</title>",
f' <link rel="stylesheet" href="{css_href}">',
_css_tag(css_content),
"</head>",
]
# opening body with optional class
body_attrs = []
if body_class:
body_attrs.append(f'class="{body_class}"')
Expand All @@ -35,21 +38,21 @@ def page_header(
body_tag += " " + " ".join(body_attrs)
body_tag += ">"
lines.append(body_tag)
# optional base target for frames
if base_target:
lines.append(f'<base target="{base_target}">')
return lines


def page_header_nobody(title: str, css_href: str) -> List[str]:
def page_header_nobody(title: str, css_content: str) -> List[str]:
"""Return HTML header lines without opening body (e.g., for frameset pages)."""
return [
"<!DOCTYPE html>",
'<html lang="en">',
"<head>",
' <meta charset="utf-8">',
' <meta name="viewport" content="width=device-width, initial-scale=1">',
f" <title>{html.escape(title)}</title>",
f' <link rel="stylesheet" href="{css_href}">',
_css_tag(css_content),
"</head>",
]

Expand Down
57 changes: 37 additions & 20 deletions FlashXTest/backend/Webview/modern_static/index_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,55 +10,62 @@
from .html_utils import zebra_row_class, page_header, page_footer
from .status import InvocationStatus, parse_build_status

_STATUS_SORT = {"red": "0", "yellow": "1", "green": "2"}


def generate_html(
board: Dict[str, Dict[str, InvocationStatus]],
sites: List[str],
invocations: List[str],
inv_dir_lookup: Dict[str, Dict[str, Path]],
css_content: str,
title: str = "FlashXTest Invocations",
) -> str:
"""Return *index.html* as a single string."""

lines: List[str] = page_header(title, css_href="style.css")
lines: List[str] = page_header(title, css_content)

# dynamic tooltip support: include tooltip script and container
# Tooltip support
lines.append('<script src="js/statsWindow.js"></script>')
lines.append("<script>window.onload=statsWindowInit;</script>")
lines.append("<script>window.onload = statsWindowInit;</script>")
lines.append('<div id="statsWindow" class="stats-window">')
lines.append(' <div id="statsHeader" class="stats-header"></div>')
lines.append(' <div id="statsBody" class="stats-body"></div>')
lines.append("</div>")

lines.append(f"<h1>{html.escape(title)}</h1>")

# Start the table with fixed layout
# Search bar to filter invocation rows by name
lines.append('<div class="toolbar">')
lines.append(
'<input type="text" id="invocationSearch" class="search-bar"'
' placeholder="Filter invocations by name...">'
)
lines.append("</div>")

lines.append('<table class="index-table">')
# header row
# Header row
lines.append(" <tr>")
lines.append(' <th class="col-invocation">Invocation</th>')
for site in sites:
lines.append(f" <th>{html.escape(site)}</th>")
lines.append(" </tr>")
# body rows

# Body rows
for idx, inv_name in enumerate(invocations):
lines.append(f' <tr class="{zebra_row_class(idx)}">')
# dynamic hover for invocation
inv_href = f"invocations/{html.escape(inv_name)}.html"

# header/body for tooltip
# Build execution summary across all sites
# Collect all builds for this invocation
builds = [] # list of (site, build_path)
# Build tooltip summary
builds = []
for site in sites:
inv_paths = inv_dir_lookup.get(inv_name, {})
site_inv = inv_paths.get(site)
site_inv = inv_dir_lookup.get(inv_name, {}).get(site)
if site_inv and site_inv.is_dir():
for b in site_inv.iterdir():
if b.is_dir():
builds.append((site, b))
total_tests = len(builds)
failing = [] # list of (site, build_name, exit_msg)
failing = []
for site, b in builds:
status, exit_msg = parse_build_status(b)
if status.colour != "green":
Expand All @@ -68,21 +75,31 @@ def generate_html(
else:
header_text = f"All {total_tests} tests completed successfully"
header_js = json.dumps(header_text)
body_lines = [f"{name} - {msg}" for _, name, msg in failing]
body_js = json.dumps("<br>".join(body_lines))
body_js = json.dumps("<br>".join(f"{name} - {msg}" for _, name, msg in failing))

lines.append(
f' <td class="col-invocation"><a class="cell-link" href="{inv_href}" '
+ f"onMouseOver='appear({header_js},{body_js})' onMouseOut='disappear()'>"
+ f"{html.escape(inv_name)}</a></td>"
f' <td class="col-invocation">'
f'<a class="cell-link" href="{inv_href}"'
f" onMouseOver='appear({header_js},{body_js})' onMouseOut='disappear()'>"
f"{html.escape(inv_name)}</a></td>"
)

for site in sites:
status = board.get(inv_name, {}).get(site)
if status is None:
lines.append(" <td>&nbsp;</td>")
else:
lines.append(f' <td class="{status.colour}">{status.emoji}</td>')
sort_val = _STATUS_SORT.get(status.colour, "9")
lines.append(
f' <td class="{status.colour}" data-sort-value="{sort_val}">'
f"{status.emoji}</td>"
)
lines.append(" </tr>")

lines.append("</table>")

# Search filter script
lines.append('<script src="js/table_sort.js"></script>')

lines.extend(page_footer())
return "\n".join(lines)
Loading
Loading