Skip to content
Merged
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
28 changes: 26 additions & 2 deletions lib/multimer/_hostloop.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,14 @@ def _crashed():
"""True when the script body died with an uncaught exception."""
if _state["crashed"]:
return True
if _impl() != "cpython":
# MicroPython's sys.exc_info() (only some ports have it: windows does,
# unix doesn't) can still report an exception that was raised and
# handled -- appdev's own getattr(board_config, "joystick_driver", None)
# leaves one -- so it can't tell a crash from a clean end, and trusting
# it skipped the keep-alive loop at random. Without it every MicroPython
# port behaves as unix always has.
return False
try:
return sys.exc_info()[0] is not None
except Exception:
Expand Down Expand Up @@ -343,14 +351,30 @@ def _run_loop():
break


def _note(msg):
"""One line on stderr: the exit hook has no caller to raise to."""
try:
print("multimer: " + msg, file=sys.stderr)
except Exception:
pass


def _exit_hook():
# Never let an exception escape: MicroPython turns an uncaught exception
# in sys.atexit into "FATAL: uncaught NLR", CPython prints a traceback.
# But say why the program didn't stay up, or a skipped loop looks exactly
# like a clean exit.
try:
if not _state["claimed"] and not _crashed():
if _state["claimed"]:
pass # run_until() drove the loop itself
elif _crashed():
_note("not keeping the program alive: the script ended with an uncaught exception")
else:
_run_loop()
except BaseException:
except (KeyboardInterrupt, SystemExit):
pass
except BaseException as e:
_note("the keep-alive loop stopped: " + repr(e))
_stop()
try:
from . import _dispatch
Expand Down
36 changes: 35 additions & 1 deletion tests/test_hostloop.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# SPDX-License-Identifier: MIT
"""multimer._hostloop: who owns the main thread after the script body ends."""

import contextlib
import io
import unittest

import _env # noqa: F401
Expand Down Expand Up @@ -77,8 +79,40 @@ def test_crashed_script_does_not_enter_the_loop(self):
multimer.every(5, lambda t: count.append(1))
self._force(_hostloop.EXIT_HOOK)
_hostloop.mark_crashed()
_hostloop._exit_hook()
err = io.StringIO()
with contextlib.redirect_stderr(err):
_hostloop._exit_hook()
self.assertEqual(0, len(count))
self.assertIn("uncaught exception", err.getvalue(), "a skipped loop must say why")

def test_micropython_ignores_a_stale_exc_info(self):
"""MicroPython's sys.exc_info() can outlive a handled exception
(windows port), so it never counts as a crash there."""
self._patch(_hostloop, "_impl", lambda: "micropython")
try:
raise AttributeError("handled")
except AttributeError:
self.assertFalse(_hostloop._crashed())

def test_cpython_reads_exc_info(self):
try:
raise AttributeError("in flight")
except AttributeError:
self.assertTrue(_hostloop._crashed())

def test_a_failing_loop_says_so(self):
multimer.keepalive()
multimer.every(5, lambda t: None)
self._force(_hostloop.EXIT_HOOK)

def boom():
raise RuntimeError("boom")

self._patch(_hostloop, "_run_loop", boom)
err = io.StringIO()
with contextlib.redirect_stderr(err):
_hostloop._exit_hook()
self.assertIn("RuntimeError", err.getvalue())

def test_no_keepalive_means_the_hook_returns_at_once(self):
"""A bare timer script behaves like a daemon thread."""
Expand Down
30 changes: 30 additions & 0 deletions tests/test_uwin32.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,33 @@ def test_enumeration_agrees_with_itself(self):
self.assertIsInstance(w.midiOutGetDevName(i), str)
for i in range(w.midiInGetNumDevs()):
self.assertIsInstance(w.midiInGetDevName(i), str)


@unittest.skipUnless(sys.platform == "win32", "uwin32 is Windows only")
class Uwin32KeyMappingTests(unittest.TestCase):
"""WinDisplay's keys arrive as the codes SDLDisplay gives for the same keys."""

def test_numpad_is_the_keypad(self):
import keys
import uwin32 as w

digits = [keys.K_KP_0, keys.K_KP_1, keys.K_KP_2, keys.K_KP_3, keys.K_KP_4,
keys.K_KP_5, keys.K_KP_6, keys.K_KP_7, keys.K_KP_8, keys.K_KP_9]
for n, want in enumerate(digits):
self.assertEqual(want, w.virtual_key_to_sdl(0x60 + n), "VK_NUMPAD%d" % n)
for vk, want in ((0x6A, keys.K_KP_MULTIPLY), (0x6B, keys.K_KP_PLUS), (0x6D, keys.K_KP_MINUS),
(0x6E, keys.K_KP_PERIOD), (0x6F, keys.K_KP_DIVIDE)):
self.assertEqual(want, w.virtual_key_to_sdl(vk), hex(vk))

def test_main_row_punctuation_is_its_character(self):
import uwin32 as w

for vk, ch in ((0xBA, ";"), (0xBB, "="), (0xBC, ","), (0xBD, "-"), (0xBE, "."), (0xBF, "/"),
(0xC0, "`"), (0xDB, "["), (0xDC, "\\"), (0xDD, "]"), (0xDE, "'")):
self.assertEqual(ord(ch), w.virtual_key_to_sdl(vk), hex(vk))

def test_letters_and_digits_are_unchanged(self):
import uwin32 as w

self.assertEqual(ord("a"), w.virtual_key_to_sdl(0x41))
self.assertEqual(ord("7"), w.virtual_key_to_sdl(0x37))
29 changes: 29 additions & 0 deletions utils/uwin32.py
Original file line number Diff line number Diff line change
Expand Up @@ -1961,6 +1961,35 @@ def modifier_mask():
VK_PRIOR: 1073741899,
VK_NEXT: 1073741902,
VK_INSERT: 1073741897,
# Numeric keypad (NumLock on) -> SDL keypad keycodes, as SDLDisplay delivers
# them; without these VK_NUMPAD0..9 fell through as 'a'..'i'.
0x60: 1073741922, # VK_NUMPAD0 -> K_KP_0
0x61: 1073741913, # VK_NUMPAD1 -> K_KP_1
0x62: 1073741914,
0x63: 1073741915,
0x64: 1073741916,
0x65: 1073741917,
0x66: 1073741918,
0x67: 1073741919,
0x68: 1073741920,
0x69: 1073741921, # VK_NUMPAD9 -> K_KP_9
0x6A: 1073741909, # VK_MULTIPLY -> K_KP_MULTIPLY
0x6B: 1073741911, # VK_ADD -> K_KP_PLUS
0x6D: 1073741910, # VK_SUBTRACT -> K_KP_MINUS
0x6E: 1073741923, # VK_DECIMAL -> K_KP_PERIOD
0x6F: 1073741908, # VK_DIVIDE -> K_KP_DIVIDE
# Main-row punctuation (US layout), which fell through as raw VK_OEM_* codes.
0xBA: 59, # VK_OEM_1 ;
0xBB: 61, # VK_OEM_PLUS =
0xBC: 44, # VK_OEM_COMMA ,
0xBD: 45, # VK_OEM_MINUS -
0xBE: 46, # VK_OEM_PERIOD .
0xBF: 47, # VK_OEM_2 /
0xC0: 96, # VK_OEM_3 `
0xDB: 91, # VK_OEM_4 [
0xDC: 92, # VK_OEM_5 \
0xDD: 93, # VK_OEM_6 ]
0xDE: 39, # VK_OEM_7 '
}


Expand Down
Loading