Skip to content

Commit 155bffb

Browse files
cursoragentomiq
andcommitted
Fix charset lower CHR$ raw screen codes; add wasm charset CI tests
- gfx_put_byte: for charset_lowercase, non A-Za-z use sc=b (CHR byte is screen code) - tests/wasm_canvas_charset_test.py: hEY hEY, CHR32 vs space, CHR65 vs A; petscii on/off - make wasm-canvas-charset-test; wasm-tests.yml on push/PR main; tag+nightly WASM steps Co-authored-by: Chris Garrett <chris@chrisg.com>
1 parent 14e99ad commit 155bffb

9 files changed

Lines changed: 207 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ jobs:
231231
run: |
232232
python3 tests/wasm_browser_test.py
233233
python3 tests/wasm_browser_canvas_test.py
234+
python3 tests/wasm_canvas_charset_test.py
234235
python3 tests/wasm_tutorial_embed_test.py
235236
236237
- name: Package WASM artifact

.github/workflows/nightly.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ jobs:
229229
run: |
230230
python3 tests/wasm_browser_test.py
231231
python3 tests/wasm_browser_canvas_test.py
232+
python3 tests/wasm_canvas_charset_test.py
232233
python3 tests/wasm_tutorial_embed_test.py
233234
234235
- name: Package WASM artifact

.github/workflows/wasm-tests.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Run headless browser WASM tests on every push to main (charset, canvas, terminal, tutorial).
2+
name: WASM browser tests
3+
4+
on:
5+
push:
6+
branches: [main]
7+
pull_request:
8+
branches: [main]
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
wasm-playwright:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Install Emscripten (emsdk)
21+
run: |
22+
sudo apt-get update
23+
sudo apt-get install -y python3 git
24+
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$GITHUB_WORKSPACE/emsdk"
25+
cd "$GITHUB_WORKSPACE/emsdk"
26+
./emsdk install latest
27+
./emsdk activate latest
28+
29+
- name: Build WASM targets
30+
run: |
31+
source "$GITHUB_WORKSPACE/emsdk/emsdk_env.sh"
32+
emcc --version
33+
make basic-wasm basic-wasm-modular basic-wasm-canvas
34+
35+
- uses: actions/setup-python@v5
36+
with:
37+
python-version: "3.12"
38+
39+
- name: Install Playwright
40+
run: |
41+
pip install -r tests/requirements-wasm.txt
42+
python -m playwright install chromium --with-deps
43+
44+
- name: Run WASM browser tests
45+
run: |
46+
python3 tests/wasm_browser_test.py
47+
python3 tests/wasm_browser_canvas_test.py
48+
python3 tests/wasm_canvas_charset_test.py
49+
python3 tests/wasm_tutorial_embed_test.py

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ done
3838

3939
**GFX unit test**: `./gfx_video_test` (headless, no display needed).
4040

41-
**Browser / WASM** (**emsdk** `emcc`, not distro `apt` emscripten — CI clones emsdk and `install latest`) + Playwright: `make basic-wasm` then `pip install -r tests/requirements-wasm.txt`, `python3 -m playwright install chromium`, then `make wasm-test` (or `python3 tests/wasm_browser_test.py`). **Canvas PETSCII**: `make basic-wasm-canvas` and `make wasm-canvas-test` (or `python3 tests/wasm_browser_canvas_test.py`). **Tutorial embeds**: `make basic-wasm-modular` and `make wasm-tutorial-test` (`tests/wasm_tutorial_embed_test.py`); see **`docs/tutorial-embedding.md`**. Same WASM jobs run in **tag** and **nightly** GitHub Actions. Demos use **Asyncify**; **INPUT** is inline, **GET**/**INKEY$** use **`wasm_push_key`** (terminal: focus output panel; canvas: focus canvas). **Pause**/**Resume** use **`Module.wasmPaused`** on both **`index.html`** and **`canvas.html`**; **Stop** uses **`wasmStopRequested`**.
41+
**Browser / WASM** (**emsdk** `emcc`, not distro `apt` emscripten — CI clones emsdk and `install latest`) + Playwright: `make basic-wasm` then `pip install -r tests/requirements-wasm.txt`, `python3 -m playwright install chromium`, then `make wasm-test` (or `python3 tests/wasm_browser_test.py`). **Canvas PETSCII**: `make basic-wasm-canvas` and `make wasm-canvas-test` (or `python3 tests/wasm_browser_canvas_test.py`). **Charset lower regression** (mixed-case + `CHR$(32)` / `CHR$(65)`, PETSCII on/off): **`make wasm-canvas-charset-test`** (`tests/wasm_canvas_charset_test.py`). **Tutorial embeds**: `make basic-wasm-modular` and `make wasm-tutorial-test` (`tests/wasm_tutorial_embed_test.py`); see **`docs/tutorial-embedding.md`**. WASM Playwright suite also runs on **push to `main`** (`.github/workflows/wasm-tests.yml`) and in **tag** release WASM job. Demos use **Asyncify**; **INPUT** is inline, **GET**/**INKEY$** use **`wasm_push_key`** (terminal: focus output panel; canvas: focus canvas). **Pause**/**Resume** use **`Module.wasmPaused`** on both **`index.html`** and **`canvas.html`**; **Stop** uses **`wasmStopRequested`**.
4242

43-
**Canvas WASM** (PETSCII 40×25, GfxVideoState): `make basic-wasm-canvas` produces `web/basic-canvas.js` + `basic-canvas.wasm`; open **`web/canvas.html`**. **INPUT**/**GET** type on the **canvas** (focus it first); keys go to a **heap ring** (see `canvas.html`). Headless regression: **`make wasm-canvas-test`** (`tests/wasm_browser_canvas_test.py`). Sprites are stubbed; use **`basic_load_and_run_gfx`** and **`wasm_gfx_render_rgba`** (see `canvas.html`).
43+
**Canvas WASM** (PETSCII 40×25, GfxVideoState, bitmap + PNG sprites): `make basic-wasm-canvas` produces `web/basic-canvas.js` + `basic-canvas.wasm`; open **`web/canvas.html`**. **INPUT**/**GET** type on the **canvas** (focus it first). Headless: **`make wasm-canvas-test`**, **`make wasm-canvas-charset-test`**. Use **`basic_load_and_run_gfx`** and the RGBA refresh path (see `canvas.html`).
4444

4545
### Caveats
4646

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Unreleased
44

5+
- **Charset lower + CHR$**: With lowercase char ROM, non-letter bytes from **`CHR$(n)`** (and punctuation) use **raw screen code** `sc = byte`; **`petscii_to_screencode(65)`** was mapping to lowercase glyphs. **Regression**: `tests/wasm_canvas_charset_test.py`, **`make wasm-canvas-charset-test`**; CI workflow **`wasm-tests.yml`** runs full WASM Playwright suite on **push/PR to `main`**; release WASM job runs charset test too.
6+
57
- **Browser canvas**: `#OPTION charset lower` (and charset from CLI before run) now applies when video state attaches — fixes PETSCII space (screen code 32) drawing as wrong glyph (e.g. `!`) because the uppercase char ROM was still active.
68
- **INPUT (canvas / GFX)**: When **Stop** / watchdog sets `wasmStopRequested` while waiting in `gfx_read_line`, report **"Stopped"** instead of **"Unexpected end of input"** (misleading; not EOF).
79

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ wasm-test: basic-wasm
102102
wasm-canvas-test: basic-wasm-canvas
103103
python3 tests/wasm_browser_canvas_test.py
104104

105+
wasm-canvas-charset-test: basic-wasm-canvas
106+
python3 tests/wasm_canvas_charset_test.py
107+
105108
wasm-tutorial-test: basic-wasm-modular
106109
python3 tests/wasm_tutorial_embed_test.py
107110

@@ -111,6 +114,6 @@ clean:
111114
$(RM) web/basic-canvas.js web/basic-canvas.wasm web/basic-canvas.wasm.map 2>/dev/null || true
112115
$(RM) web/basic-modular.js web/basic-modular.wasm web/basic-modular.wasm.map 2>/dev/null || true
113116

114-
.PHONY: all clean gfx_video_test gfx-demo basic-gfx basic-wasm basic-wasm-modular basic-wasm-canvas wasm-test wasm-canvas-test wasm-tutorial-test
117+
.PHONY: all clean gfx_video_test gfx-demo basic-gfx basic-wasm basic-wasm-modular basic-wasm-canvas wasm-test wasm-canvas-test wasm-canvas-charset-test wasm-tutorial-test
115118

116119
# End of Makefile

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ Produces `web/basic.js` and `web/basic.wasm` (Asyncify-enabled), **`web/basic-mo
519519

520520
**PETSCII canvas** (no Raylib): `make basic-wasm-canvas` produces `web/basic-canvas.js`, `web/basic-canvas.wasm`, and `web/canvas.html`. The page refreshes the canvas during `SLEEP` and loops via a shared RGBA framebuffer (PETSCII, `SCREEN 1` bitmap, and **`LOADSPRITE`/`DRAWSPRITE`** like **basic-gfx**). Try **`examples/gfx_canvas_demo.bas`**: paste into the canvas page, use **Upload to VFS** to add **`gfx_canvas_demo.png`** (same file is copied to **`web/gfx_canvas_demo.png`** for local fetch tests), then Run.
521521

522-
**Automated WASM smoke tests** (headless Chromium via Playwright): install `pip install -r tests/requirements-wasm.txt`, run `python3 -m playwright install chromium`, then `make wasm-test`, `make wasm-canvas-test`, and **`make wasm-tutorial-test`**. These run in GitHub Actions for **tagged releases** and the **nightly** workflow; artifacts include `rgc-basic-wasm.tar.gz` (terminal, modular tutorial files, and canvas).
522+
**Automated WASM smoke tests** (headless Chromium via Playwright): install `pip install -r tests/requirements-wasm.txt`, run `python3 -m playwright install chromium`, then **`make wasm-test`**, **`make wasm-canvas-test`**, **`make wasm-canvas-charset-test`** (regression for `#OPTION charset lower`, `CHR$(32)` / `CHR$(65)`, with and without **`-petscii`**), and **`make wasm-tutorial-test`**. On **push to `main`**, workflow **`.github/workflows/wasm-tests.yml`** runs the same Playwright suite. Tagged releases and **nightly** also build WASM; artifacts include `rgc-basic-wasm.tar.gz` (terminal, modular tutorial files, and canvas).
523523

524524
#### Manual compilation
525525

basic.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,12 +1602,13 @@ static void gfx_put_byte(unsigned char b)
16021602
} else if (gfx_raw_screen_codes) {
16031603
sc = petscii_to_screencode(b);
16041604
} else if (gfx_vs && gfx_vs->charset_lowercase && b >= 32 && b <= 126) {
1605-
/* Lowercase char ROM + ASCII string literals: only A–Z / a–z are ASCII letters.
1606-
* CHR$(n) and punctuation use PETSCII semantics (see FN_CHR raw byte in gfx). */
1605+
/* Lowercase char ROM: quoted text in the editor is ASCII — map A–Z/a–z only.
1606+
* CHR$(n) emits a raw byte n: petscii_to_screencode would map PETSCII 65→sc 1
1607+
* (wrong; user expects CHR$(65) to show the same as "A" → screen 65). */
16071608
if ((b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')) {
16081609
sc = gfx_ascii_to_screencode_lowcharset(b);
16091610
} else {
1610-
sc = petscii_to_screencode(b);
1611+
sc = (uint8_t)b;
16111612
}
16121613
} else if (petscii_mode) {
16131614
sc = petscii_to_screencode(b);

tests/wasm_canvas_charset_test.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env python3
2+
"""Headless regression tests for #OPTION charset lower + ASCII/CHR$ on canvas WASM.
3+
4+
Catches wrong gfx_put_byte mapping (space as !, CHR$(65) wrong vs "A", IDE without -petscii).
5+
Requires: make basic-wasm-canvas, pip install -r tests/requirements-wasm.txt, playwright install chromium.
6+
"""
7+
from __future__ import annotations
8+
9+
import http.server
10+
import socketserver
11+
import sys
12+
import threading
13+
import time
14+
from functools import partial
15+
from pathlib import Path
16+
17+
ROOT = Path(__file__).resolve().parents[1]
18+
WEB = ROOT / "web"
19+
20+
21+
def _serve_web() -> tuple[socketserver.TCPServer, int]:
22+
Handler = partial(http.server.SimpleHTTPRequestHandler, directory=str(WEB))
23+
socketserver.TCPServer.allow_reuse_address = True
24+
httpd = socketserver.TCPServer(("127.0.0.1", 0), Handler)
25+
port = httpd.server_address[1]
26+
27+
def run() -> None:
28+
httpd.serve_forever()
29+
30+
threading.Thread(target=run, daemon=True).start()
31+
return httpd, port
32+
33+
34+
def _click_run(page) -> None:
35+
page.evaluate("document.getElementById('run').click()")
36+
37+
38+
def _canvas_pixel_rgba(page, x: int, y: int) -> tuple[int, int, int, int]:
39+
return page.evaluate(
40+
"""([x, y]) => {
41+
const c = document.getElementById('screen');
42+
const ctx = c.getContext('2d');
43+
const d = ctx.getImageData(x, y, 1, 1).data;
44+
return [d[0], d[1], d[2], d[3]];
45+
}""",
46+
[x, y],
47+
)
48+
49+
50+
def _set_petscii(page, on: bool) -> None:
51+
page.evaluate(f"() => {{ document.getElementById('optPetscii').checked = {str(on).lower()}; }}")
52+
53+
54+
def _run_charset_suite(page, *, petscii_checked: bool) -> None:
55+
"""Assert mixed-case string, space, and CHR$(65) match \"A\" on same charset."""
56+
label = "petscii on" if petscii_checked else "petscii off"
57+
_set_petscii(page, petscii_checked)
58+
page.wait_for_function("() => !document.getElementById('run').disabled", timeout=60000)
59+
60+
# Line 0: mixed case + spaces (user-reported pattern)
61+
page.fill(
62+
"#program",
63+
'#OPTION charset lower\n'
64+
'10 COLOR 1\n'
65+
'20 BACKGROUND 6\n'
66+
'30 PRINT "hEY hEY"\n'
67+
'40 PRINT CHR$(32)\n'
68+
'50 PRINT CHR$(65)\n'
69+
'60 PRINT "A"\n'
70+
"70 END\n",
71+
)
72+
_click_run(page)
73+
page.wait_for_function(
74+
"() => (window.Module && Module.wasmGfxRunDone === 1)",
75+
timeout=120000,
76+
)
77+
log = page.text_content("#log") or ""
78+
if log.strip():
79+
raise RuntimeError(f"{label}: error log: {log!r}")
80+
81+
# Row 0 "hEY hEY" — space between words at column 3 (0-based), center x = 3*8+4 = 28
82+
px_h = _canvas_pixel_rgba(page, 4, 4)
83+
px_space_word = _canvas_pixel_rgba(page, 28, 4)
84+
if list(px_h[:3]) == list(px_space_word[:3]):
85+
raise RuntimeError(
86+
f"{label}: first 'h' vs space in 'hEY hEY' should differ, got h={px_h!r} sp={px_space_word!r}"
87+
)
88+
89+
# CHR$(32) line: only a space — interior of cell should match word-space (both true space)
90+
px_chr32 = _canvas_pixel_rgba(page, 4, 12)
91+
if list(px_chr32[:3]) != list(px_space_word[:3]):
92+
raise RuntimeError(
93+
f"{label}: CHR$(32) center should match literal space pixel, "
94+
f"chr32={px_chr32!r} lit_sp={px_space_word!r}"
95+
)
96+
97+
# CHR$(65) vs "A" on next lines (y=20 and y=28 for rows 2 and 3 at cell center y=4+8*r)
98+
px_chr65 = _canvas_pixel_rgba(page, 4, 20)
99+
px_quote_a = _canvas_pixel_rgba(page, 4, 28)
100+
if list(px_chr65[:3]) != list(px_quote_a[:3]):
101+
raise RuntimeError(
102+
f"{label}: CHR$(65) vs PRINT \"A\" should match at (4,20) vs (4,28), "
103+
f"chr65={px_chr65!r} qA={px_quote_a!r}"
104+
)
105+
106+
107+
def main() -> int:
108+
if not (WEB / "basic-canvas.js").is_file() or not (WEB / "basic-canvas.wasm").is_file():
109+
print("error: run make basic-wasm-canvas first", file=sys.stderr)
110+
return 1
111+
try:
112+
from playwright.sync_api import sync_playwright
113+
except ImportError:
114+
print(
115+
"error: pip install -r tests/requirements-wasm.txt && playwright install chromium",
116+
file=sys.stderr,
117+
)
118+
return 1
119+
120+
httpd, port = _serve_web()
121+
url = f"http://127.0.0.1:{port}/canvas.html"
122+
try:
123+
with sync_playwright() as p:
124+
browser = p.chromium.launch(headless=True)
125+
page = browser.new_page(viewport={"width": 1100, "height": 900})
126+
page.goto(url, wait_until="networkidle", timeout=120000)
127+
page.wait_for_function("() => !document.getElementById('run').disabled", timeout=120000)
128+
129+
_run_charset_suite(page, petscii_checked=True)
130+
_run_charset_suite(page, petscii_checked=False)
131+
_set_petscii(page, True)
132+
133+
browser.close()
134+
finally:
135+
httpd.shutdown()
136+
httpd.server_close()
137+
138+
print("wasm_canvas_charset_test: OK")
139+
return 0
140+
141+
142+
if __name__ == "__main__":
143+
raise SystemExit(main())

0 commit comments

Comments
 (0)