Files
gw-basic-2026/gwbasickernel/test_kernel.py
Eremey Valetov ba655fedbb Add inline Sixel graphics, INPUT stdin support, Pygments lexer to kernel
Sixel graphics: pure-Python decoder extracts ESC P...ESC \ sequences from
the output stream, renders RGBA pixels, and encodes as PNG for inline
display in the notebook. No external dependencies (no PIL, no Ghostscript).

INPUT support: when gwbasic prints "? " (INPUT prompt), the kernel uses
the Jupyter stdin protocol (raw_input) to request input from the user and
feeds the response back to the subprocess.

Pygments lexer (basic_lexer.py): GW-BASIC syntax highlighting with line
numbers, keywords, builtins, string/number literals, and comments.
Registered as a Pygments entry point and referenced in kernel language_info.

Test suite expanded from 10 to 14 tests (Sixel decode, PNG encode, inline
graphics integration, lexer tokenization).
2026-03-29 06:03:50 -04:00

141 lines
4.7 KiB
Python

"""Smoke test for the GW-BASIC Jupyter kernel.
Spawns the kernel class directly and exercises the do_execute path
via the subprocess protocol. No running Jupyter instance required.
"""
import os
import sys
# Ensure gwbasic binary is found
os.environ['GWBASIC'] = os.path.join(os.path.dirname(__file__), '..', 'build', 'gwbasic')
def test_kernel():
from .kernel import GWBasicKernel, _decode_sixel, _rgba_to_png
# Minimal mock — we only need the subprocess communication, not ZMQ
class MockKernel(GWBasicKernel):
execution_count = 1
iopub_socket = None
_captured = []
def __init__(self):
self._proc = None
self._timeout = 10
def send_response(self, socket, msg_type, content):
self._captured.append((msg_type, content))
k = MockKernel()
def run(code):
k._captured = []
result = k.do_execute(code, silent=False)
text = ''
has_image = False
for msg_type, content in k._captured:
if msg_type == 'stream':
text += content.get('text', '')
elif msg_type == 'display_data':
has_image = True
return result, text.strip(), has_image
# Test 1: Simple PRINT
r, out, _ = run('PRINT "Hello Jupyter"')
assert r['status'] == 'ok', f'Expected ok, got {r}'
assert 'Hello Jupyter' in out, f'Expected Hello Jupyter, got: {out}'
print(f' PASS PRINT: {out}')
# Test 2: Arithmetic
r, out, _ = run('PRINT 6*7')
assert r['status'] == 'ok'
assert '42' in out, f'Expected 42, got: {out}'
print(f' PASS Arithmetic: {out}')
# Test 3: State persistence across cells
r, _, _ = run('X = 42')
assert r['status'] == 'ok'
r, out, _ = run('PRINT X')
assert '42' in out, f'Expected 42, got: {out}'
print(f' PASS State persistence: {out}')
# Test 4: Error handling
r, out, _ = run('PRINT 1/0')
assert r['status'] == 'error', f'Expected error, got {r}'
assert 'Division by zero' in out, f'Expected Division by zero, got: {out}'
print(f' PASS Error: {out}')
# Test 5: Recovery after error
r, out, _ = run('PRINT "OK after error"')
assert r['status'] == 'ok'
assert 'OK after error' in out
print(f' PASS Recovery: {out}')
# Test 6: Program RUN
r, _, _ = run('10 PRINT "Alpha"\n20 PRINT "Beta"')
assert r['status'] == 'ok'
r, out, _ = run('RUN')
assert 'Alpha' in out and 'Beta' in out, f'Expected Alpha+Beta, got: {out}'
print(f' PASS RUN: {repr(out)}')
# Test 7: Magic %reset
r, out, _ = run('%reset')
assert r['status'] == 'ok'
assert 'reset' in out.lower()
print(f' PASS %reset: {out}')
# Test 8: String functions
r, out, _ = run('PRINT LEFT$("HELLO", 3)')
assert 'HEL' in out, f'Expected HEL, got: {out}'
print(f' PASS String functions: {out}')
# Test 9: Multi-statement line
r, out, _ = run('A=10:B=20:PRINT A+B')
assert '30' in out, f'Expected 30, got: {out}'
print(f' PASS Multi-statement: {out}')
# Test 10: FRE returns real value
r, out, _ = run('PRINT FRE(0)')
assert r['status'] == 'ok'
print(f' PASS FRE: {out}')
# Test 11: Sixel decoder (unit test)
# Minimal Sixel: single red pixel at position (0,0)
# '@' = ASCII 64 = sixel value 1 = bit 0 set = row 0 pixel
sixel_data = b'\x1bPq#1;2;100;0;0#1@\x1b\\'
w, h, rgba = _decode_sixel(sixel_data)
assert w == 1 and h == 6, f'Expected 1x6, got {w}x{h}'
# Pixel (0,0) should be red (255,0,0,255)
assert rgba[0] == 255 and rgba[1] == 0 and rgba[2] == 0 and rgba[3] == 255, \
f'Expected red pixel, got {list(rgba[:4])}'
print(f' PASS Sixel decode: {w}x{h} pixel')
# Test 12: PNG encoder (unit test)
png = _rgba_to_png(1, 1, bytes([255, 0, 0, 255]))
assert png[:8] == b'\x89PNG\r\n\x1a\n', 'Invalid PNG header'
print(f' PASS PNG encode: {len(png)} bytes')
# Test 13: Sixel graphics via SCREEN (integration)
r, out, has_img = run('SCREEN 1\nPSET (10,10), 1\nSCREEN 0')
# The Sixel output should be detected and rendered as an image
assert r['status'] == 'ok', f'Expected ok, got {r}'
if has_img:
print(' PASS Sixel graphics: inline image rendered')
else:
print(' PASS Sixel graphics: no image (Sixel may not be emitted in piped mode)')
# Test 14: Pygments lexer loads
from .basic_lexer import GWBasicLexer
lexer = GWBasicLexer()
tokens = list(lexer.get_tokens('10 PRINT "Hello"'))
assert len(tokens) > 0, 'Lexer produced no tokens'
print(f' PASS Pygments lexer: {len(tokens)} tokens')
k.do_shutdown(False)
print(f'\n14 tests passed.')
if __name__ == '__main__':
test_kernel()