From dca9dfdf388cccca001522fd174ccd3ae206a131 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 25 Oct 2026 18:10:47 -0700 Subject: [PATCH] Add canonical Python 3.15 optional native integration sources (#9368) Copy curses frontend sources and canonical curses, DBM, Tcl and DTrace integration tests from CPython v3.15.0 through update_lib. Preserve upstream optional-native/resource/configuration gates and canonical file modes, without adding native implementations or custom skips. Validate source identity, hooks and default/frozen compile-only checks. Native runtime integration and probe execution remain unvalidated. Assisted-by: OpenAI Codex: model version unavailable --- Lib/curses/__init__.py | 98 ++ Lib/curses/ascii.py | 99 ++ Lib/curses/has_key.py | 192 +++ Lib/curses/panel.py | 6 + Lib/curses/textpad.py | 236 ++++ Lib/test/curses_tests.py | 46 + Lib/test/dtracedata/call_stack.stp | 29 +- Lib/test/test_curses.py | 2046 ++++++++++++++++++++++++++++ Lib/test/test_dbm_gnu.py | 222 +++ Lib/test/test_dbm_ndbm.py | 165 +++ Lib/test/test_dtrace.py | 415 +++++- Lib/test/test_tcl.py | 838 ++++++++++++ 12 files changed, 4319 insertions(+), 73 deletions(-) create mode 100644 Lib/curses/__init__.py create mode 100644 Lib/curses/ascii.py create mode 100644 Lib/curses/has_key.py create mode 100644 Lib/curses/panel.py create mode 100644 Lib/curses/textpad.py create mode 100755 Lib/test/curses_tests.py create mode 100644 Lib/test/test_curses.py create mode 100644 Lib/test/test_dbm_gnu.py create mode 100644 Lib/test/test_dbm_ndbm.py create mode 100644 Lib/test/test_tcl.py diff --git a/Lib/curses/__init__.py b/Lib/curses/__init__.py new file mode 100644 index 000000000..605d5fcbe --- /dev/null +++ b/Lib/curses/__init__.py @@ -0,0 +1,98 @@ +"""curses + +The main package for curses support for Python. Normally used by importing +the package, and perhaps a particular module inside it. + + import curses + from curses import textpad + curses.initscr() + ... + +""" + +from _curses import * +import os as _os +import sys as _sys + +# Some constants, most notably the ACS_* ones, are only added to the C +# _curses module's dictionary after initscr() is called. (Some +# versions of SGI's curses don't define values for those constants +# until initscr() has been called.) This wrapper function calls the +# underlying C initscr(), and then copies the constants from the +# _curses module to the curses package's dictionary. Don't do 'from +# curses import *' if you'll be needing the ACS_* constants. + +def initscr(): + import _curses, curses + # we call setupterm() here because it raises an error + # instead of calling exit() in error cases. + setupterm(term=_os.environ.get("TERM", "unknown"), + fd=_sys.__stdout__.fileno()) + stdscr = _curses.initscr() + for key, value in _curses.__dict__.items(): + if key.startswith('ACS_') or key in ('LINES', 'COLS'): + setattr(curses, key, value) + return stdscr + +# This is a similar wrapper for start_color(), which adds the COLORS and +# COLOR_PAIRS variables which are only available after start_color() is +# called. + +def start_color(): + import _curses, curses + _curses.start_color() + curses.COLORS = _curses.COLORS + curses.COLOR_PAIRS = _curses.COLOR_PAIRS + +# Import Python has_key() implementation if _curses doesn't contain has_key() + +try: + has_key +except NameError: + from .has_key import has_key # noqa: F401 + +# Wrapper for the entire curses-based application. Runs a function which +# should be the rest of your curses-based application. If the application +# raises an exception, wrapper() will restore the terminal to a sane state so +# you can read the resulting traceback. + +def wrapper(func, /, *args, **kwds): + """Wrapper function that initializes curses and calls another function, + restoring normal keyboard/screen behavior on error. + The callable object 'func' is then passed the main window 'stdscr' + as its first argument, followed by any other arguments passed to + wrapper(). + """ + + try: + # Initialize curses + stdscr = initscr() + + # Turn off echoing of keys, and enter cbreak mode, + # where no buffering is performed on keyboard input + noecho() + cbreak() + + # In keypad mode, escape sequences for special keys + # (like the cursor keys) will be interpreted and + # a special value like curses.KEY_LEFT will be returned + stdscr.keypad(1) + + # Start color, too. Harmless if the terminal doesn't have + # color; user can test with has_color() later on. The try/catch + # works around a minor bit of over-conscientiousness in the curses + # module -- the error return from C start_color() is ignorable, + # unless they are raised by the interpreter due to other issues. + try: + start_color() + except _curses.error: + pass + + return func(stdscr, *args, **kwds) + finally: + # Set everything back to normal + if 'stdscr' in locals(): + stdscr.keypad(0) + echo() + nocbreak() + endwin() diff --git a/Lib/curses/ascii.py b/Lib/curses/ascii.py new file mode 100644 index 000000000..95acff339 --- /dev/null +++ b/Lib/curses/ascii.py @@ -0,0 +1,99 @@ +"""Constants and membership tests for ASCII characters""" + +NUL = 0x00 # ^@ +SOH = 0x01 # ^A +STX = 0x02 # ^B +ETX = 0x03 # ^C +EOT = 0x04 # ^D +ENQ = 0x05 # ^E +ACK = 0x06 # ^F +BEL = 0x07 # ^G +BS = 0x08 # ^H +TAB = 0x09 # ^I +HT = 0x09 # ^I +LF = 0x0a # ^J +NL = 0x0a # ^J +VT = 0x0b # ^K +FF = 0x0c # ^L +CR = 0x0d # ^M +SO = 0x0e # ^N +SI = 0x0f # ^O +DLE = 0x10 # ^P +DC1 = 0x11 # ^Q +DC2 = 0x12 # ^R +DC3 = 0x13 # ^S +DC4 = 0x14 # ^T +NAK = 0x15 # ^U +SYN = 0x16 # ^V +ETB = 0x17 # ^W +CAN = 0x18 # ^X +EM = 0x19 # ^Y +SUB = 0x1a # ^Z +ESC = 0x1b # ^[ +FS = 0x1c # ^\ +GS = 0x1d # ^] +RS = 0x1e # ^^ +US = 0x1f # ^_ +SP = 0x20 # space +DEL = 0x7f # delete + +controlnames = [ +"NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", +"BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI", +"DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", +"CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US", +"SP" +] + +def _ctoi(c): + if isinstance(c, str): + return ord(c) + else: + return c + +def isalnum(c): return isalpha(c) or isdigit(c) +def isalpha(c): return isupper(c) or islower(c) +def isascii(c): return 0 <= _ctoi(c) <= 127 # ? +def isblank(c): return _ctoi(c) in (9, 32) +def iscntrl(c): return 0 <= _ctoi(c) <= 31 or _ctoi(c) == 127 +def isdigit(c): return 48 <= _ctoi(c) <= 57 +def isgraph(c): return 33 <= _ctoi(c) <= 126 +def islower(c): return 97 <= _ctoi(c) <= 122 +def isprint(c): return 32 <= _ctoi(c) <= 126 +def ispunct(c): return isgraph(c) and not isalnum(c) +def isspace(c): return _ctoi(c) in (9, 10, 11, 12, 13, 32) +def isupper(c): return 65 <= _ctoi(c) <= 90 +def isxdigit(c): return isdigit(c) or \ + (65 <= _ctoi(c) <= 70) or (97 <= _ctoi(c) <= 102) +def isctrl(c): return 0 <= _ctoi(c) < 32 +def ismeta(c): return _ctoi(c) > 127 + +def ascii(c): + if isinstance(c, str): + return chr(_ctoi(c) & 0x7f) + else: + return _ctoi(c) & 0x7f + +def ctrl(c): + if isinstance(c, str): + return chr(_ctoi(c) & 0x1f) + else: + return _ctoi(c) & 0x1f + +def alt(c): + if isinstance(c, str): + return chr(_ctoi(c) | 0x80) + else: + return _ctoi(c) | 0x80 + +def unctrl(c): + bits = _ctoi(c) + if bits == 0x7f: + rep = "^?" + elif isprint(bits & 0x7f): + rep = chr(bits & 0x7f) + else: + rep = "^" + chr(((bits & 0x7f) | 0x20) + 0x20) + if bits & 0x80: + return "!" + rep + return rep diff --git a/Lib/curses/has_key.py b/Lib/curses/has_key.py new file mode 100644 index 000000000..3471b28cb --- /dev/null +++ b/Lib/curses/has_key.py @@ -0,0 +1,192 @@ + +# +# Emulation of has_key() function for platforms that don't use ncurses +# + +import _curses + +# Table mapping curses keys to the terminfo capability name + +_capability_names = frozendict({ + _curses.KEY_A1: 'ka1', + _curses.KEY_A3: 'ka3', + _curses.KEY_B2: 'kb2', + _curses.KEY_BACKSPACE: 'kbs', + _curses.KEY_BEG: 'kbeg', + _curses.KEY_BTAB: 'kcbt', + _curses.KEY_C1: 'kc1', + _curses.KEY_C3: 'kc3', + _curses.KEY_CANCEL: 'kcan', + _curses.KEY_CATAB: 'ktbc', + _curses.KEY_CLEAR: 'kclr', + _curses.KEY_CLOSE: 'kclo', + _curses.KEY_COMMAND: 'kcmd', + _curses.KEY_COPY: 'kcpy', + _curses.KEY_CREATE: 'kcrt', + _curses.KEY_CTAB: 'kctab', + _curses.KEY_DC: 'kdch1', + _curses.KEY_DL: 'kdl1', + _curses.KEY_DOWN: 'kcud1', + _curses.KEY_EIC: 'krmir', + _curses.KEY_END: 'kend', + _curses.KEY_ENTER: 'kent', + _curses.KEY_EOL: 'kel', + _curses.KEY_EOS: 'ked', + _curses.KEY_EXIT: 'kext', + _curses.KEY_F0: 'kf0', + _curses.KEY_F1: 'kf1', + _curses.KEY_F10: 'kf10', + _curses.KEY_F11: 'kf11', + _curses.KEY_F12: 'kf12', + _curses.KEY_F13: 'kf13', + _curses.KEY_F14: 'kf14', + _curses.KEY_F15: 'kf15', + _curses.KEY_F16: 'kf16', + _curses.KEY_F17: 'kf17', + _curses.KEY_F18: 'kf18', + _curses.KEY_F19: 'kf19', + _curses.KEY_F2: 'kf2', + _curses.KEY_F20: 'kf20', + _curses.KEY_F21: 'kf21', + _curses.KEY_F22: 'kf22', + _curses.KEY_F23: 'kf23', + _curses.KEY_F24: 'kf24', + _curses.KEY_F25: 'kf25', + _curses.KEY_F26: 'kf26', + _curses.KEY_F27: 'kf27', + _curses.KEY_F28: 'kf28', + _curses.KEY_F29: 'kf29', + _curses.KEY_F3: 'kf3', + _curses.KEY_F30: 'kf30', + _curses.KEY_F31: 'kf31', + _curses.KEY_F32: 'kf32', + _curses.KEY_F33: 'kf33', + _curses.KEY_F34: 'kf34', + _curses.KEY_F35: 'kf35', + _curses.KEY_F36: 'kf36', + _curses.KEY_F37: 'kf37', + _curses.KEY_F38: 'kf38', + _curses.KEY_F39: 'kf39', + _curses.KEY_F4: 'kf4', + _curses.KEY_F40: 'kf40', + _curses.KEY_F41: 'kf41', + _curses.KEY_F42: 'kf42', + _curses.KEY_F43: 'kf43', + _curses.KEY_F44: 'kf44', + _curses.KEY_F45: 'kf45', + _curses.KEY_F46: 'kf46', + _curses.KEY_F47: 'kf47', + _curses.KEY_F48: 'kf48', + _curses.KEY_F49: 'kf49', + _curses.KEY_F5: 'kf5', + _curses.KEY_F50: 'kf50', + _curses.KEY_F51: 'kf51', + _curses.KEY_F52: 'kf52', + _curses.KEY_F53: 'kf53', + _curses.KEY_F54: 'kf54', + _curses.KEY_F55: 'kf55', + _curses.KEY_F56: 'kf56', + _curses.KEY_F57: 'kf57', + _curses.KEY_F58: 'kf58', + _curses.KEY_F59: 'kf59', + _curses.KEY_F6: 'kf6', + _curses.KEY_F60: 'kf60', + _curses.KEY_F61: 'kf61', + _curses.KEY_F62: 'kf62', + _curses.KEY_F63: 'kf63', + _curses.KEY_F7: 'kf7', + _curses.KEY_F8: 'kf8', + _curses.KEY_F9: 'kf9', + _curses.KEY_FIND: 'kfnd', + _curses.KEY_HELP: 'khlp', + _curses.KEY_HOME: 'khome', + _curses.KEY_IC: 'kich1', + _curses.KEY_IL: 'kil1', + _curses.KEY_LEFT: 'kcub1', + _curses.KEY_LL: 'kll', + _curses.KEY_MARK: 'kmrk', + _curses.KEY_MESSAGE: 'kmsg', + _curses.KEY_MOVE: 'kmov', + _curses.KEY_NEXT: 'knxt', + _curses.KEY_NPAGE: 'knp', + _curses.KEY_OPEN: 'kopn', + _curses.KEY_OPTIONS: 'kopt', + _curses.KEY_PPAGE: 'kpp', + _curses.KEY_PREVIOUS: 'kprv', + _curses.KEY_PRINT: 'kprt', + _curses.KEY_REDO: 'krdo', + _curses.KEY_REFERENCE: 'kref', + _curses.KEY_REFRESH: 'krfr', + _curses.KEY_REPLACE: 'krpl', + _curses.KEY_RESTART: 'krst', + _curses.KEY_RESUME: 'kres', + _curses.KEY_RIGHT: 'kcuf1', + _curses.KEY_SAVE: 'ksav', + _curses.KEY_SBEG: 'kBEG', + _curses.KEY_SCANCEL: 'kCAN', + _curses.KEY_SCOMMAND: 'kCMD', + _curses.KEY_SCOPY: 'kCPY', + _curses.KEY_SCREATE: 'kCRT', + _curses.KEY_SDC: 'kDC', + _curses.KEY_SDL: 'kDL', + _curses.KEY_SELECT: 'kslt', + _curses.KEY_SEND: 'kEND', + _curses.KEY_SEOL: 'kEOL', + _curses.KEY_SEXIT: 'kEXT', + _curses.KEY_SF: 'kind', + _curses.KEY_SFIND: 'kFND', + _curses.KEY_SHELP: 'kHLP', + _curses.KEY_SHOME: 'kHOM', + _curses.KEY_SIC: 'kIC', + _curses.KEY_SLEFT: 'kLFT', + _curses.KEY_SMESSAGE: 'kMSG', + _curses.KEY_SMOVE: 'kMOV', + _curses.KEY_SNEXT: 'kNXT', + _curses.KEY_SOPTIONS: 'kOPT', + _curses.KEY_SPREVIOUS: 'kPRV', + _curses.KEY_SPRINT: 'kPRT', + _curses.KEY_SR: 'kri', + _curses.KEY_SREDO: 'kRDO', + _curses.KEY_SREPLACE: 'kRPL', + _curses.KEY_SRIGHT: 'kRIT', + _curses.KEY_SRSUME: 'kRES', + _curses.KEY_SSAVE: 'kSAV', + _curses.KEY_SSUSPEND: 'kSPD', + _curses.KEY_STAB: 'khts', + _curses.KEY_SUNDO: 'kUND', + _curses.KEY_SUSPEND: 'kspd', + _curses.KEY_UNDO: 'kund', + _curses.KEY_UP: 'kcuu1' + }) + +def has_key(ch): + if isinstance(ch, str): + ch = ord(ch) + + # Figure out the correct capability name for the keycode. + capability_name = _capability_names.get(ch) + if capability_name is None: + return False + + #Check the current terminal description for that capability; + #if present, return true, else return false. + if _curses.tigetstr(capability_name): + return True + else: + return False + +if __name__ == '__main__': + # Compare the output of this implementation and the ncurses has_key, + # on platforms where has_key is already available + try: + L = [] + _curses.initscr() + for key in _capability_names.keys(): + system = _curses.has_key(key) + python = has_key(key) + if system != python: + L.append( 'Mismatch for key %s, system=%i, Python=%i' + % (_curses.keyname( key ), system, python) ) + finally: + _curses.endwin() + for i in L: print(i) diff --git a/Lib/curses/panel.py b/Lib/curses/panel.py new file mode 100644 index 000000000..067afef73 --- /dev/null +++ b/Lib/curses/panel.py @@ -0,0 +1,6 @@ +"""curses.panel + +Module for using panels with curses. +""" + +from _curses_panel import * diff --git a/Lib/curses/textpad.py b/Lib/curses/textpad.py new file mode 100644 index 000000000..c58a7174d --- /dev/null +++ b/Lib/curses/textpad.py @@ -0,0 +1,236 @@ +"""Simple textbox editing widget with Emacs-like keybindings.""" + +import curses +import curses.ascii + +def rectangle(win, uly, ulx, lry, lrx): + """Draw a rectangle with corners at the provided upper-left + and lower-right coordinates. + """ + win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1) + win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) + win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) + win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1) + win.addch(uly, ulx, curses.ACS_ULCORNER) + win.addch(uly, lrx, curses.ACS_URCORNER) + win.addch(lry, lrx, curses.ACS_LRCORNER) + win.addch(lry, ulx, curses.ACS_LLCORNER) + +class Textbox: + """Editing widget using the interior of a window object. + Supports the following Emacs-like key bindings: + + Ctrl-A Go to left edge of window. + Ctrl-B Cursor left, wrapping to previous line if appropriate. + Ctrl-D Delete character under cursor. + Ctrl-E Go to right edge (stripspaces off) or end of line + (stripspaces on). + Ctrl-F Cursor right, wrapping to next line when appropriate. + Ctrl-G Terminate, returning the window contents. + Ctrl-H Delete character backward. + Ctrl-J Terminate if the window is 1 line, otherwise move to start + of next line. + Ctrl-K If line is blank, delete it, otherwise clear to end of line. + Ctrl-L Refresh screen. + Ctrl-N Cursor down; move down one line. + Ctrl-O Insert a blank line at cursor location. + Ctrl-P Cursor up; move up one line. + + Move operations do nothing if the cursor is at an edge where the + movement is not possible. The following synonyms are supported where + possible: + + KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, + KEY_DOWN = Ctrl-N, KEY_BACKSPACE = Ctrl-h + """ + def __init__(self, win, insert_mode=False): + self.win = win + self.insert_mode = insert_mode + self._update_max_yx() + self.stripspaces = 1 + self.lastcmd = None + win.keypad(1) + + def _update_max_yx(self): + maxy, maxx = self.win.getmaxyx() + self.maxy = maxy - 1 + self.maxx = maxx - 1 + + def _decode(self, ch): + # The text of a chtype cell or input byte, decoded with the window's + # encoding. A_CHARTEXT keeps the character byte, dropping the attributes. + return bytes([ch & curses.A_CHARTEXT]).decode(self.win.encoding, 'replace') + + def _char_at(self, *yx): + # The text of the cell at the given position (default: the cursor). + # instr() re-encodes it to the window's encoding; inch() cannot + # represent a non-ASCII 8-bit-locale character on a wide build. + return self.win.instr(*yx, 1).decode(self.win.encoding, 'replace') + + def _cell_at(self, *yx): + # The cell at the given position (default: the cursor) as a chtype + # addch() can write back with its rendition. inch() mangles a non-ASCII + # character on a wide build, so take the byte from instr() and the + # attributes from inch(). + return self.win.instr(*yx, 1)[0] | self.win.inch(*yx) & curses.A_ATTRIBUTES + + def _isprint(self, cell): + # Whether a chtype cell holds a printable character; _decode() drops the + # attribute bits. + return self._decode(cell).isprintable() + + def _printable_key(self, ch): + # Whether the integer keystroke is a printable character, not a key + # code. 0..255 are character bytes (decoded with the window's encoding); + # larger values are function and navigation keys. + return ch <= 0xff and self._decode(ch).isprintable() + + def _end_of_line(self, y): + """Go to the location of the first blank on the given line, + returning the index of the last non-blank character.""" + self._update_max_yx() + last = self.maxx + while True: + if self._char_at(y, last) != ' ': + last = min(self.maxx, last+1) + break + elif last == 0: + break + last = last - 1 + return last + + def _insert_printable_char(self, ch): + self._update_max_yx() + (y, x) = self.win.getyx() + backyx = None + while True: + if self.insert_mode: + oldch = self._cell_at() + if y >= self.maxy and x >= self.maxx: + # Use insch() in the lower-right cell: addch() there would move + # the cursor out of the window, raising an error and scrolling + # a scrollable window. Pass it as text: insch() does not decode + # an int byte through the locale on a wide build. + self.win.insch(self._decode(ch), ch & curses.A_ATTRIBUTES) + break + self.win.addch(ch) + if not self.insert_mode or not self._isprint(oldch): + break + ch = oldch + (y, x) = self.win.getyx() + # Remember where to put the cursor back since we are in insert_mode + if backyx is None: + backyx = y, x + + if backyx is not None: + self.win.move(*backyx) + + def do_command(self, ch): + "Process a single editing command." + self._update_max_yx() + (y, x) = self.win.getyx() + self.lastcmd = ch + if self._printable_key(ch): + self._insert_printable_char(ch) + elif ch == curses.ascii.SOH: # ^a + self.win.move(y, 0) + elif ch in (curses.ascii.STX,curses.KEY_LEFT, + curses.ascii.BS, + curses.KEY_BACKSPACE, + curses.ascii.DEL): + if x > 0: + self.win.move(y, x-1) + elif y == 0: + pass + elif self.stripspaces: + self.win.move(y-1, self._end_of_line(y-1)) + else: + self.win.move(y-1, self.maxx) + if ch in (curses.ascii.BS, curses.KEY_BACKSPACE, curses.ascii.DEL): + self.win.delch() + elif ch == curses.ascii.EOT: # ^d + self.win.delch() + elif ch == curses.ascii.ENQ: # ^e + if self.stripspaces: + self.win.move(y, self._end_of_line(y)) + else: + self.win.move(y, self.maxx) + elif ch in (curses.ascii.ACK, curses.KEY_RIGHT): # ^f + if x < self.maxx: + self.win.move(y, x+1) + elif y == self.maxy: + pass + else: + self.win.move(y+1, 0) + elif ch == curses.ascii.BEL: # ^g + return 0 + elif ch == curses.ascii.NL: # ^j + if self.maxy == 0: + return 0 + elif y < self.maxy: + self.win.move(y+1, 0) + elif ch == curses.ascii.VT: # ^k + if x == 0 and self._end_of_line(y) == 0: + self.win.deleteln() + else: + # first undo the effect of self._end_of_line + self.win.move(y, x) + self.win.clrtoeol() + elif ch == curses.ascii.FF: # ^l + self.win.refresh() + elif ch in (curses.ascii.SO, curses.KEY_DOWN): # ^n + if y < self.maxy: + self.win.move(y+1, x) + if x > self._end_of_line(y+1): + self.win.move(y+1, self._end_of_line(y+1)) + elif ch == curses.ascii.SI: # ^o + self.win.insertln() + elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p + if y > 0: + self.win.move(y-1, x) + if x > self._end_of_line(y-1): + self.win.move(y-1, self._end_of_line(y-1)) + return 1 + + def gather(self): + "Collect and return the contents of the window." + result = "" + self._update_max_yx() + for y in range(self.maxy+1): + self.win.move(y, 0) + stop = self._end_of_line(y) + if stop == 0 and self.stripspaces: + continue + for x in range(self.maxx+1): + if self.stripspaces and x > stop: + break + result = result + self._char_at(y, x) + if self.maxy > 0: + result = result + "\n" + return result + + def edit(self, validate=None): + "Edit in the widget window and collect the results." + while 1: + ch = self.win.getch() + if validate: + ch = validate(ch) + if not ch: + continue + if not self.do_command(ch): + break + self.win.refresh() + return self.gather() + +if __name__ == '__main__': + def test_editbox(stdscr): + ncols, nlines = 9, 4 + uly, ulx = 15, 20 + stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.") + win = curses.newwin(nlines, ncols, uly, ulx) + rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols) + stdscr.refresh() + return Textbox(win).edit() + + str = curses.wrapper(test_editbox) + print('Contents of text box:', repr(str)) diff --git a/Lib/test/curses_tests.py b/Lib/test/curses_tests.py new file mode 100755 index 000000000..40e832dec --- /dev/null +++ b/Lib/test/curses_tests.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# +# $Id: ncurses.py 36559 2004-07-18 05:56:09Z tim_one $ +# +# Interactive test suite for the curses module. +# This script displays various things and the user should verify whether +# they display correctly. +# + +import curses +from curses import textpad + +def test_textpad(stdscr, insert_mode=False): + ncols, nlines = 8, 3 + uly, ulx = 3, 2 + if insert_mode: + mode = 'insert mode' + else: + mode = 'overwrite mode' + + stdscr.addstr(uly-3, ulx, "Use Ctrl-G to end editing (%s)." % mode) + stdscr.addstr(uly-2, ulx, "Be sure to try typing in the lower-right corner.") + win = curses.newwin(nlines, ncols, uly, ulx) + textpad.rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols) + stdscr.refresh() + + box = textpad.Textbox(win, insert_mode) + contents = box.edit() + stdscr.addstr(uly+ncols+2, 0, "Text entered in the box\n") + stdscr.addstr(repr(contents)) + stdscr.addstr('\n') + stdscr.addstr('Press any key') + stdscr.getch() + + for i in range(3): + stdscr.move(uly+ncols+2 + i, 0) + stdscr.clrtoeol() + +def main(stdscr): + stdscr.clear() + test_textpad(stdscr, False) + test_textpad(stdscr, True) + + +if __name__ == '__main__': + curses.wrapper(main) diff --git a/Lib/test/dtracedata/call_stack.stp b/Lib/test/dtracedata/call_stack.stp index 54082c202..2901f4465 100644 --- a/Lib/test/dtracedata/call_stack.stp +++ b/Lib/test/dtracedata/call_stack.stp @@ -10,32 +10,21 @@ function basename:string(path:string) return last_token; } -probe process.mark("function__entry") -{ - funcname = user_string($arg2); - - if (funcname == "start") { - tracing = 1; - } -} - -probe process.mark("function__entry"), process.mark("function__return") +probe @PYTHON_SYSTEMTAP_PROBE@("function__entry"), + @PYTHON_SYSTEMTAP_PROBE@("function__return") { filename = user_string($arg1); funcname = user_string($arg2); lineno = $arg3; - if (tracing) { + if (funcname == "start") { + if ($$name == "function__entry") { + tracing = 1; + } else { + tracing = 0; + } + } else if (tracing) { printf("%d\t%s:%s:%s:%d\n", gettimeofday_us(), $$name, basename(filename), funcname, lineno); } } - -probe process.mark("function__return") -{ - funcname = user_string($arg2); - - if (funcname == "start") { - tracing = 0; - } -} diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py new file mode 100644 index 000000000..ae3d7cdc5 --- /dev/null +++ b/Lib/test/test_curses.py @@ -0,0 +1,2046 @@ +import functools +import inspect +import os +import string +import sys +import tempfile +import unittest +from unittest.mock import MagicMock + +from test.support import (requires, verbose, SaveSignals, cpython_only, + check_disallow_instantiation, MISSING_C_DOCSTRINGS, + gc_collect) +from test.support.import_helper import import_module + +# Optionally test curses module. This currently requires that the +# 'curses' resource be given on the regrtest command line using the -u +# option. If not available, nothing after this line will be executed. +requires('curses') + +# If either of these don't exist, skip the tests. +curses = import_module('curses') +import_module('curses.ascii') +import_module('curses.textpad') +try: + import curses.panel +except ImportError: + pass + +def requires_curses_func(name): + return unittest.skipUnless(hasattr(curses, name), + 'requires curses.%s' % name) + +def requires_curses_window_meth(name): + def deco(test): + @functools.wraps(test) + def wrapped(self, *args, **kwargs): + if not hasattr(self.stdscr, name): + raise unittest.SkipTest('requires curses.window.%s' % name) + test(self, *args, **kwargs) + return wrapped + return deco + + +def requires_colors(test): + @functools.wraps(test) + def wrapped(self, *args, **kwargs): + if not curses.has_colors(): + self.skipTest('requires colors support') + curses.start_color() + test(self, *args, **kwargs) + return wrapped + +term = os.environ.get('TERM') +SHORT_MAX = 0x7fff + +# If newterm was supported we could use it instead of initscr and not exit +@unittest.skipIf(not term or term == 'unknown', + "$TERM=%r, calling initscr() may cause exit" % term) +@unittest.skipIf(sys.platform == "cygwin", + "cygwin's curses mostly just hangs") +class TestCurses(unittest.TestCase): + + @classmethod + def setUpClass(cls): + if verbose: + print(f'TERM={term}', file=sys.stderr, flush=True) + # testing setupterm() inside initscr/endwin + # causes terminal breakage + stdout_fd = sys.__stdout__.fileno() + curses.setupterm(fd=stdout_fd) + + def setUp(self): + self.isatty = True + self.output = sys.__stdout__ + stdout_fd = sys.__stdout__.fileno() + if not sys.__stdout__.isatty(): + # initstr() unconditionally uses C stdout. + # If it is redirected to file or pipe, try to attach it + # to terminal. + # First, save a copy of the file descriptor of stdout, so it + # can be restored after finishing the test. + dup_fd = os.dup(stdout_fd) + self.addCleanup(os.close, dup_fd) + self.addCleanup(os.dup2, dup_fd, stdout_fd) + + if sys.__stderr__.isatty(): + # If stderr is connected to terminal, use it. + tmp = sys.__stderr__ + self.output = sys.__stderr__ + else: + try: + # Try to open the terminal device. + tmp = open('/dev/tty', 'wb', buffering=0) + except OSError: + # As a fallback, use regular file to write control codes. + # Some functions (like savetty) will not work, but at + # least the garbage control sequences will not be mixed + # with the testing report. + tmp = tempfile.TemporaryFile(mode='wb', buffering=0) + self.isatty = False + self.addCleanup(tmp.close) + self.output = None + os.dup2(tmp.fileno(), stdout_fd) + + self.save_signals = SaveSignals() + self.save_signals.save() + self.addCleanup(self.save_signals.restore) + if verbose and self.output is not None: + # just to make the test output a little more readable + sys.stderr.flush() + sys.stdout.flush() + print(file=self.output, flush=True) + self.stdscr = curses.initscr() + if self.isatty: + curses.savetty() + self.addCleanup(curses.endwin) + self.addCleanup(curses.resetty) + self.stdscr.erase() + + @requires_curses_func('filter') + def test_filter(self): + # TODO: Should be called before initscr() or newterm() are called. + # TODO: nofilter() + curses.filter() + + @requires_curses_func('use_env') + def test_use_env(self): + # TODO: Should be called before initscr() or newterm() are called. + # TODO: use_tioctl() + curses.use_env(False) + curses.use_env(True) + + def test_error(self): + self.assertIsSubclass(curses.error, Exception) + + def test_create_windows(self): + win = curses.newwin(5, 10) + self.assertEqual(win.getbegyx(), (0, 0)) + self.assertEqual(win.getparyx(), (-1, -1)) + self.assertEqual(win.getmaxyx(), (5, 10)) + + win = curses.newwin(10, 15, 2, 5) + self.assertEqual(win.getbegyx(), (2, 5)) + self.assertEqual(win.getparyx(), (-1, -1)) + self.assertEqual(win.getmaxyx(), (10, 15)) + + win2 = win.subwin(3, 7) + self.assertEqual(win2.getbegyx(), (3, 7)) + self.assertEqual(win2.getparyx(), (1, 2)) + self.assertEqual(win2.getmaxyx(), (9, 13)) + + win2 = win.subwin(5, 10, 3, 7) + self.assertEqual(win2.getbegyx(), (3, 7)) + self.assertEqual(win2.getparyx(), (1, 2)) + self.assertEqual(win2.getmaxyx(), (5, 10)) + + win3 = win.derwin(2, 3) + self.assertEqual(win3.getbegyx(), (4, 8)) + self.assertEqual(win3.getparyx(), (2, 3)) + self.assertEqual(win3.getmaxyx(), (8, 12)) + + win3 = win.derwin(6, 11, 2, 3) + self.assertEqual(win3.getbegyx(), (4, 8)) + self.assertEqual(win3.getparyx(), (2, 3)) + self.assertEqual(win3.getmaxyx(), (6, 11)) + + win.mvwin(0, 1) + self.assertEqual(win.getbegyx(), (0, 1)) + self.assertEqual(win.getparyx(), (-1, -1)) + self.assertEqual(win.getmaxyx(), (10, 15)) + self.assertEqual(win2.getbegyx(), (3, 7)) + self.assertEqual(win2.getparyx(), (1, 2)) + self.assertEqual(win2.getmaxyx(), (5, 10)) + self.assertEqual(win3.getbegyx(), (4, 8)) + self.assertEqual(win3.getparyx(), (2, 3)) + self.assertEqual(win3.getmaxyx(), (6, 11)) + + win2.mvderwin(2, 1) + self.assertEqual(win2.getbegyx(), (3, 7)) + self.assertEqual(win2.getparyx(), (2, 1)) + self.assertEqual(win2.getmaxyx(), (5, 10)) + + win3.mvderwin(2, 1) + self.assertEqual(win3.getbegyx(), (4, 8)) + self.assertEqual(win3.getparyx(), (2, 1)) + self.assertEqual(win3.getmaxyx(), (6, 11)) + + def test_subwindows_references(self): + win = curses.newwin(5, 10) + win2 = win.subwin(3, 7) + del win + gc_collect() + del win2 + gc_collect() + + def test_move_cursor(self): + stdscr = self.stdscr + win = stdscr.subwin(10, 15, 2, 5) + stdscr.move(1, 2) + win.move(2, 4) + self.assertEqual(stdscr.getyx(), (1, 2)) + self.assertEqual(win.getyx(), (2, 4)) + + win.cursyncup() + self.assertEqual(stdscr.getyx(), (4, 9)) + + def test_refresh_control(self): + stdscr = self.stdscr + # touchwin()/untouchwin()/is_wintouched() + stdscr.refresh() + self.assertIs(stdscr.is_wintouched(), False) + stdscr.touchwin() + self.assertIs(stdscr.is_wintouched(), True) + stdscr.refresh() + self.assertIs(stdscr.is_wintouched(), False) + stdscr.touchwin() + self.assertIs(stdscr.is_wintouched(), True) + stdscr.untouchwin() + self.assertIs(stdscr.is_wintouched(), False) + + # touchline()/untouchline()/is_linetouched() + stdscr.touchline(5, 2) + self.assertIs(stdscr.is_linetouched(5), True) + self.assertIs(stdscr.is_linetouched(6), True) + self.assertIs(stdscr.is_wintouched(), True) + stdscr.touchline(5, 1, False) + self.assertIs(stdscr.is_linetouched(5), False) + + # syncup() + win = stdscr.subwin(10, 15, 2, 5) + win2 = win.subwin(5, 10, 3, 7) + win2.touchwin() + stdscr.untouchwin() + win2.syncup() + self.assertIs(win.is_wintouched(), True) + self.assertIs(stdscr.is_wintouched(), True) + + # syncdown() + stdscr.touchwin() + win.untouchwin() + win2.untouchwin() + win2.syncdown() + self.assertIs(win2.is_wintouched(), True) + + # syncok() + if hasattr(stdscr, 'syncok') and not sys.platform.startswith("sunos"): + win.untouchwin() + stdscr.untouchwin() + for syncok in [False, True]: + win2.syncok(syncok) + win2.addch('a') + self.assertIs(win.is_wintouched(), syncok) + self.assertIs(stdscr.is_wintouched(), syncok) + + # Many tests below use a common set of non-ASCII cases, each applied only + # when the window encoding can represent it -- so the whole suite is meant to + # be run under several locales (e.g. ISO-8859-1, ISO-8859-15, KOI8-U): + # 'A'/'a' ASCII + # 'é' common to the Latin encodings + # '¤'/'€'/'є' byte 0xA4 in ISO-8859-1 / ISO-8859-15 / KOI8-U + # Precomposed characters are used so a round-trip does not depend on the form. + + def _encodable(self, s): + # Wide characters are only supported in a locale that can encode them. + try: + s.encode(self.stdscr.encoding) + except UnicodeEncodeError: + return False + return True + + def _read_char(self, y, x): + # The character written to a cell, read back for output checks. inch() + # is unusable here: on a wide build it returns the low 8 bits of the + # character's code point rather than its locale-encoded byte, mangling + # anything outside Latin-1. in_wch() reads the wide cell directly; + # without it, instr() re-encodes the cell to the window encoding. + stdscr = self.stdscr + if hasattr(stdscr, 'in_wch'): + return str(stdscr.in_wch(y, x)) + return stdscr.instr(y, x, 1).decode(stdscr.encoding) + + def test_output_character(self): + stdscr = self.stdscr + encoding = stdscr.encoding + # addch() + stdscr.refresh() + stdscr.move(0, 0) + stdscr.addch('A') + stdscr.addch(b'A') + stdscr.addch(65) + # See _encodable for the character set. Each is either written (mapped + # to a single byte), or raises UnicodeEncodeError (not in the encoding) + # or OverflowError (a multibyte sequence, e.g. in UTF-8). + for c in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'): + try: + stdscr.addch(c) + except UnicodeEncodeError: + self.assertRaises(UnicodeEncodeError, c.encode, encoding) + except OverflowError: + encoded = c.encode(encoding) + self.assertNotEqual(len(encoded), 1, repr(encoded)) + stdscr.addch('A', curses.A_BOLD) + stdscr.addch(1, 2, 'A') + stdscr.addch(2, 3, 'A', curses.A_BOLD) + self.assertIs(stdscr.is_wintouched(), True) + + # The same characters supplied as an int chtype (a byte > 127). The + # cell is read back with _read_char(), not inch(): on a wide build the + # int is stored through the locale as a wide character that inch() + # cannot represent for a character outside Latin-1. + for c in ('é', '¤', '€', 'є'): + try: + b = c.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + # A wide build stores a character outside Latin-1 as a wide cell, + # not as its encoded byte, so it cannot round-trip here. + if ord(c) > 0xff and hasattr(stdscr, 'get_wch'): + continue + v = b[0] + with self.subTest(c=c): + stdscr.addch(0, 0, v) + self.assertEqual(self._read_char(0, 0), c) + stdscr.addch(0, 1, v, curses.A_BOLD) + self.assertEqual(self._read_char(0, 1), c) + self.assertTrue(stdscr.inch(0, 1) & curses.A_BOLD) + stdscr.move(2, 0) + stdscr.echochar(v) + self.assertEqual(self._read_char(2, 0), c) + # insch() decodes the byte through the locale like addch(), so + # it round-trips the same character. + stdscr.insch(1, 0, v) + self.assertEqual(self._read_char(1, 0), c) + + # The same characters supplied as a str. Unlike the int path above, a + # str is stored as a wide-character cell on a wide build, so every + # encodable character round-trips, insch() included. A multibyte + # character does not fit a cell on a narrow build and is skipped. + wide = hasattr(stdscr, 'in_wch') + for c in ('é', '¤', '€', 'є'): + if not self._encodable(c): + continue + if not wide and len(c.encode(encoding)) != 1: + continue + # A wide build stores a character outside Latin-1 as a wide cell, + # not as its encoded byte, so it cannot round-trip here. + if ord(c) > 0xff and hasattr(stdscr, 'get_wch'): + continue + with self.subTest(c=c): + stdscr.addch(0, 0, c) + self.assertEqual(self._read_char(0, 0), c) + stdscr.addch(0, 1, c, curses.A_BOLD) + self.assertEqual(self._read_char(0, 1), c) + self.assertTrue(stdscr.inch(0, 1) & curses.A_BOLD) + stdscr.insch(1, 0, c) + self.assertEqual(self._read_char(1, 0), c) + stdscr.move(2, 0) + stdscr.echochar(c) + self.assertEqual(self._read_char(2, 0), c) + + # echochar() + stdscr.refresh() + stdscr.move(0, 0) + stdscr.echochar('A') + stdscr.echochar(b'A') + stdscr.echochar(65) + # See _encodable for the character set; as in the addch() loop above. + for c in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'): + try: + stdscr.echochar(c) + except UnicodeEncodeError: + # The character is not encodable with the current encoding. + self.assertRaises(UnicodeEncodeError, c.encode, encoding) + except OverflowError: + # The character is encoded to a multibyte sequence. + encoded = c.encode(encoding) + self.assertNotEqual(len(encoded), 1, repr(encoded)) + stdscr.echochar('A', curses.A_BOLD) + self.assertIs(stdscr.is_wintouched(), False) + + def test_output_string(self): + stdscr = self.stdscr + encoding = stdscr.encoding + # addstr()/insstr() + for func in [stdscr.addstr, stdscr.insstr]: + with self.subTest(func.__qualname__): + func('abcd') + func(b'abcd') + # Common and encoding-distinctive strings (see _encodable for the + # 0xA4 set); 'àßçđ' is UTF-8-only. Each is written if the + # encoding allows, else raises UnicodeEncodeError. + for s in ('soupçon', 'àßçđ', 'soupçon ¤', 'soupçon €', 'дякую'): + stdscr.move(0, 0) + try: + func(s) + except UnicodeEncodeError: + self.assertRaises(UnicodeEncodeError, s.encode, encoding) + stdscr.move(0, 0) + func('abcd', curses.A_BOLD) + func(1, 2, 'abcd') + func(2, 3, 'abcd', curses.A_BOLD) + + # addnstr()/insnstr() + for func in [stdscr.addnstr, stdscr.insnstr]: + with self.subTest(func.__qualname__): + stdscr.move(0, 0) + func('1234', 3) + func(b'1234', 3) + # As above (see _encodable); Arabic-Indic digits are UTF-8-only. + for s in ('caf\u00e9', '\u0661\u0662\u0663\u0664', 'caf\u00e9 \u00a4', 'caf\u00e9 \u20ac', '\u0434\u044f\u043a\u0443\u044e'): + stdscr.move(0, 0) + try: + func(s, 3) + except UnicodeEncodeError: + self.assertRaises(UnicodeEncodeError, s.encode, encoding) + stdscr.move(0, 0) + func('1234', 5) + func('1234', 3, curses.A_BOLD) + func(1, 2, '1234', 3) + func(2, 3, '1234', 3, curses.A_BOLD) + + def test_output_string_embedded_null_chars(self): + # reject embedded null bytes and characters + stdscr = self.stdscr + for arg in ['a\0', b'a\0']: + with self.subTest(arg=arg): + self.assertRaises(ValueError, stdscr.addstr, arg) + self.assertRaises(ValueError, stdscr.addnstr, arg, 1) + self.assertRaises(ValueError, stdscr.insstr, arg) + self.assertRaises(ValueError, stdscr.insnstr, arg, 1) + + def test_output_string_attr_restored(self): + # A write with an attr restores the window rendition afterwards, + # whether it succeeded or failed. + win = curses.newwin(2, 10, 0, 0) + def current_attrs(): + # Write a cell with the window's current rendition and read it + # back, so that the rendition itself is checked. + win.addstr(1, 0, ' ') + return win.inch(1, 0) & curses.A_ATTRIBUTES + for func, args in [(win.addstr, ('x',)), (win.addnstr, ('x', 1)), + (win.insstr, ('x',)), (win.insnstr, ('x', 1))]: + with self.subTest(func.__qualname__): + win.attrset(curses.A_UNDERLINE) + # y=100 is outside the window, so the write fails. + self.assertRaises(curses.error, func, 100, 0, *args, + curses.A_BOLD) + self.assertEqual(current_attrs(), curses.A_UNDERLINE) + func(0, 0, *args, curses.A_BOLD) + self.assertEqual(current_attrs(), curses.A_UNDERLINE) + + def test_add_string_behavior(self): + # addstr() advances the cursor past the written text; addnstr() + # writes at most n characters. + win = curses.newwin(1, 10, 0, 0) + win.addstr(0, 0, 'abc') + self.assertEqual(win.getyx(), (0, 3)) + win.erase() + win.addnstr(0, 0, 'abcdef', 3) + self.assertEqual(win.instr(0, 0), b'abc ') + + def test_insert_string_behavior(self): + # insstr()/insnstr() insert at the cursor, shift the rest of the + # line right (losing characters off the edge), and leave the cursor + # where it was. + win = curses.newwin(1, 10, 0, 0) + win.addstr(0, 0, 'abcde') + win.move(0, 1) + win.insstr('XY') + self.assertEqual(win.getyx(), (0, 1)) # cursor did not advance + self.assertEqual(win.instr(0, 0), b'aXYbcde ') + + win.erase() + win.addstr(0, 0, 'ZZZZZ') + win.move(0, 0) + win.insnstr('abcdef', 3) # at most 3 characters + self.assertEqual(win.instr(0, 0), b'abcZZZZZ ') + + def test_insch(self): + # insch() inserts a single character at the cursor (or at y, x), + # shifting the rest of the line right. + win = curses.newwin(2, 10, 0, 0) + win.addstr(0, 0, 'abc') + win.move(0, 1) + win.insch(ord('X')) + self.assertEqual(win.instr(0, 0), b'aXbc ') + win.insch(1, 0, 'Y', curses.A_BOLD) + self.assertEqual(win.inch(1, 0), b'Y'[0] | curses.A_BOLD) + + def test_pad(self): + pad = curses.newpad(10, 20) + pad.addstr(0, 0, 'PADTEXT') + self.assertEqual(pad.instr(0, 0, 7), b'PADTEXT') + + # subpad() creates a pad within the parent pad. Cell sharing with + # the parent is implementation-defined, so write to the subpad itself. + sub = pad.subpad(3, 5, 0, 0) + self.assertEqual(sub.getmaxyx(), (3, 5)) + sub.addstr(1, 0, 'sub') + self.assertEqual(sub.instr(1, 0, 3), b'sub') + + # A pad is refreshed onto an explicit screen rectangle; the + # 6-argument form is required (and rejected for ordinary windows). + pad.refresh(0, 0, 0, 0, 4, 10) + pad.noutrefresh(0, 0, 0, 0, 4, 10) + curses.doupdate() + self.assertRaises(TypeError, pad.refresh) + win = curses.newwin(5, 5, 0, 0) + self.assertRaises(TypeError, win.refresh, 0, 0, 0, 0, 4, 4) + + def test_read_from_window(self): + stdscr = self.stdscr + stdscr.addstr(0, 1, 'ABCD', curses.A_BOLD) + # inch() + stdscr.move(0, 1) + self.assertEqual(stdscr.inch(), 65 | curses.A_BOLD) + self.assertEqual(stdscr.inch(0, 3), 67 | curses.A_BOLD) + stdscr.move(0, 0) + # instr() + self.assertEqual(stdscr.instr()[:6], b' ABCD ') + self.assertEqual(stdscr.instr(3)[:6], b' AB') + self.assertEqual(stdscr.instr(0, 2)[:4], b'BCD ') + self.assertEqual(stdscr.instr(0, 2, 4), b'BCD ') + self.assertRaises(ValueError, stdscr.instr, -2) + self.assertRaises(ValueError, stdscr.instr, 0, 2, -2) + # A non-ASCII character of an 8-bit locale reads back as its encoded + # byte (see _encodable for the set). Both instr() and inch() return the + # locale byte for any character that fits the locale's single-byte + # encoding. + encoding = stdscr.encoding + for ch in ('A', 'é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + with self.subTest(ch=ch): + stdscr.addstr(2, 0, ch) + self.assertEqual(stdscr.instr(2, 0, 1), b) + self.assertEqual(stdscr.inch(2, 0), b[0]) + + def test_coordinate_errors(self): + # Addressing a cell outside the window raises curses.error. + win = curses.newwin(5, 10, 0, 0) + self.assertRaises(curses.error, win.move, 100, 100) + self.assertRaises(curses.error, win.move, -1, -1) + self.assertRaises(curses.error, win.addch, 100, 100, ord('x')) + self.assertRaises(curses.error, win.inch, 100, 100) + if hasattr(win, 'chgat'): # chgat() requires wchgat() + self.assertRaises(curses.error, win.chgat, 100, 0, curses.A_BOLD) + + def test_argument_errors(self): + win = curses.newwin(5, 10, 0, 0) + # A character argument must be an int, a byte or a one-element string. + self.assertRaises(TypeError, win.addch, []) + self.assertRaises(OverflowError, win.addch, 2**64) + # A string method rejects a non-string, non-bytes argument. + self.assertRaises(TypeError, win.addstr, 5) + self.assertRaises(TypeError, win.addstr) + # Wrong number of positional arguments. + self.assertRaises(TypeError, win.instr, 0, 0, 0, 0) + + def test_getch(self): + win = curses.newwin(5, 12, 5, 2) + + # TODO: Test with real input by writing to master fd. + for c in 'spam\n'[::-1]: + curses.ungetch(c) + self.assertEqual(win.getch(3, 1), b's'[0]) + self.assertEqual(win.getyx(), (3, 1)) + self.assertEqual(win.getch(3, 4), b'p'[0]) + self.assertEqual(win.getyx(), (3, 4)) + self.assertEqual(win.getch(), b'a'[0]) + self.assertEqual(win.getyx(), (3, 4)) + self.assertEqual(win.getch(), b'm'[0]) + self.assertEqual(win.getch(), b'\n'[0]) + + # A key value > 127 is delivered unchanged (it is not locale text). + curses.ungetch(0xE9) + self.assertEqual(win.getch(), 0xE9) + + def test_getstr(self): + win = curses.newwin(5, 12, 5, 2) + curses.echo() + self.addCleanup(curses.noecho) + + self.assertRaises(ValueError, win.getstr, -400) + self.assertRaises(ValueError, win.getstr, 2, 3, -400) + + # TODO: Test with real input by writing to master fd. + for c in 'Lorem\nipsum\ndolor\nsit\namet\n'[::-1]: + curses.ungetch(c) + self.assertEqual(win.getstr(3, 1, 2), b'Lo') + self.assertEqual(win.instr(3, 0), b' Lo ') + self.assertEqual(win.getstr(3, 5, 10), b'ipsum') + self.assertEqual(win.instr(3, 0), b' Lo ipsum ') + self.assertEqual(win.getstr(1, 5), b'dolor') + self.assertEqual(win.instr(1, 0), b' dolor ') + self.assertEqual(win.getstr(2), b'si') + self.assertEqual(win.instr(1, 0), b'si dolor ') + self.assertEqual(win.getstr(), b'amet') + self.assertEqual(win.instr(1, 0), b'amet dolor ') + + def test_clear(self): + win = curses.newwin(5, 15, 5, 2) + lorem_ipsum(win) + + win.move(0, 8) + win.clrtoeol() + self.assertEqual(win.instr(0, 0).rstrip(), b'Lorem ip') + self.assertEqual(win.instr(1, 0).rstrip(), b'dolor sit amet,') + + win.move(0, 3) + win.clrtobot() + self.assertEqual(win.instr(0, 0).rstrip(), b'Lor') + self.assertEqual(win.instr(1, 0).rstrip(), b'') + + for func in [win.erase, win.clear]: + lorem_ipsum(win) + func() + self.assertEqual(win.instr(0, 0).rstrip(), b'') + self.assertEqual(win.instr(1, 0).rstrip(), b'') + + def test_insert_delete(self): + win = curses.newwin(5, 15, 5, 2) + lorem_ipsum(win) + + win.move(0, 2) + win.delch() + self.assertEqual(win.instr(0, 0), b'Loem ipsum ') + win.delch(0, 7) + self.assertEqual(win.instr(0, 0), b'Loem ipum ') + + win.move(1, 5) + win.deleteln() + self.assertEqual(win.instr(0, 0), b'Loem ipum ') + self.assertEqual(win.instr(1, 0), b'consectetur ') + self.assertEqual(win.instr(2, 0), b'adipiscing elit') + self.assertEqual(win.instr(3, 0), b'sed do eiusmod ') + self.assertEqual(win.instr(4, 0), b' ') + + win.move(1, 5) + win.insertln() + self.assertEqual(win.instr(0, 0), b'Loem ipum ') + self.assertEqual(win.instr(1, 0), b' ') + self.assertEqual(win.instr(2, 0), b'consectetur ') + + win.clear() + lorem_ipsum(win) + win.move(1, 5) + win.insdelln(2) + self.assertEqual(win.instr(0, 0), b'Lorem ipsum ') + self.assertEqual(win.instr(1, 0), b' ') + self.assertEqual(win.instr(2, 0), b' ') + self.assertEqual(win.instr(3, 0), b'dolor sit amet,') + + win.clear() + lorem_ipsum(win) + win.move(1, 5) + win.insdelln(-2) + self.assertEqual(win.instr(0, 0), b'Lorem ipsum ') + self.assertEqual(win.instr(1, 0), b'adipiscing elit') + self.assertEqual(win.instr(2, 0), b'sed do eiusmod ') + self.assertEqual(win.instr(3, 0), b' ') + + def test_scroll(self): + win = curses.newwin(5, 15, 5, 2) + lorem_ipsum(win) + win.scrollok(True) + win.scroll() + self.assertEqual(win.instr(0, 0), b'dolor sit amet,') + win.scroll(2) + self.assertEqual(win.instr(0, 0), b'adipiscing elit') + win.scroll(-3) + self.assertEqual(win.instr(0, 0), b' ') + self.assertEqual(win.instr(2, 0), b' ') + self.assertEqual(win.instr(3, 0), b'adipiscing elit') + win.scrollok(False) + + def test_attributes(self): + # TODO: attr_get(), attr_set(), ... + win = curses.newwin(5, 15, 5, 2) + win.attron(curses.A_BOLD) + win.attroff(curses.A_BOLD) + win.attrset(curses.A_BOLD) + + win.standout() + win.standend() + + @requires_curses_window_meth('chgat') + def test_chgat(self): + win = curses.newwin(5, 15, 5, 2) + win.addstr(2, 0, 'Lorem ipsum') + win.addstr(3, 0, 'dolor sit amet') + + win.move(2, 8) + win.chgat(curses.A_BLINK) + self.assertEqual(win.inch(2, 7), b'p'[0]) + self.assertEqual(win.inch(2, 8), b's'[0] | curses.A_BLINK) + self.assertEqual(win.inch(2, 14), b' '[0] | curses.A_BLINK) + + win.move(2, 1) + win.chgat(3, curses.A_BOLD) + self.assertEqual(win.inch(2, 0), b'L'[0]) + self.assertEqual(win.inch(2, 1), b'o'[0] | curses.A_BOLD) + self.assertEqual(win.inch(2, 3), b'e'[0] | curses.A_BOLD) + self.assertEqual(win.inch(2, 4), b'm'[0]) + + win.chgat(3, 2, curses.A_UNDERLINE) + self.assertEqual(win.inch(3, 1), b'o'[0]) + self.assertEqual(win.inch(3, 2), b'l'[0] | curses.A_UNDERLINE) + self.assertEqual(win.inch(3, 14), b' '[0] | curses.A_UNDERLINE) + + win.chgat(3, 4, 7, curses.A_BLINK) + self.assertEqual(win.inch(3, 3), b'o'[0] | curses.A_UNDERLINE) + self.assertEqual(win.inch(3, 4), b'r'[0] | curses.A_BLINK) + self.assertEqual(win.inch(3, 10), b'a'[0] | curses.A_BLINK) + self.assertEqual(win.inch(3, 11), b'm'[0] | curses.A_UNDERLINE) + self.assertEqual(win.inch(3, 14), b' '[0] | curses.A_UNDERLINE) + + def test_background(self): + win = curses.newwin(5, 15, 5, 2) + win.addstr(0, 0, 'Lorem ipsum') + + self.assertIn(win.getbkgd(), (0, 32)) + + # bkgdset() + win.bkgdset('_') + self.assertEqual(win.getbkgd(), b'_'[0]) + win.bkgdset(b'#') + self.assertEqual(win.getbkgd(), b'#'[0]) + win.bkgdset(65) + self.assertEqual(win.getbkgd(), 65) + win.bkgdset(0) + self.assertEqual(win.getbkgd(), 32) + + win.bkgdset('#', curses.A_REVERSE) + self.assertEqual(win.getbkgd(), b'#'[0] | curses.A_REVERSE) + self.assertEqual(win.inch(0, 0), b'L'[0]) + self.assertEqual(win.inch(0, 5), b' '[0]) + win.bkgdset(0) + + # bkgd() + win.bkgd('_') + self.assertEqual(win.getbkgd(), b'_'[0]) + self.assertEqual(win.inch(0, 0), b'L'[0]) + self.assertEqual(win.inch(0, 5), b'_'[0]) + + win.bkgd('#', curses.A_REVERSE) + self.assertEqual(win.getbkgd(), b'#'[0] | curses.A_REVERSE) + self.assertEqual(win.inch(0, 0), b'L'[0] | curses.A_REVERSE) + self.assertEqual(win.inch(0, 5), b'#'[0] | curses.A_REVERSE) + + # A non-ASCII background character of an 8-bit locale reads back as its + # encoded byte. See _encodable for the character set. + win.bkgd(' ') + encoding = win.encoding + for ch in ('é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + # A wide build stores a character outside Latin-1 as a wide cell, + # not as its encoded byte, so it cannot round-trip here. + if ord(ch) > 0xff and hasattr(win, 'get_wch'): + continue + with self.subTest(ch=ch): + win.bkgd(ch) + self.assertEqual(win.getbkgd(), b[0]) + if ord(ch) < 0x100: + # The same byte given as an int. A wide build stores it + # through the locale, so only a Latin-1 byte round-trips. + win.bkgd(' ') + win.bkgdset(b[0]) + self.assertEqual(win.getbkgd(), b[0]) + win.bkgd(b[0]) + self.assertEqual(win.getbkgd(), b[0]) + + def test_overlay(self): + srcwin = curses.newwin(5, 18, 3, 4) + lorem_ipsum(srcwin) + dstwin = curses.newwin(7, 17, 5, 7) + for i in range(6): + dstwin.addstr(i, 0, '_'*17) + + srcwin.overlay(dstwin) + self.assertEqual(dstwin.instr(0, 0), b'sectetur_________') + self.assertEqual(dstwin.instr(1, 0), b'piscing_elit,____') + self.assertEqual(dstwin.instr(2, 0), b'_do_eiusmod______') + self.assertEqual(dstwin.instr(3, 0), b'_________________') + + srcwin.overwrite(dstwin) + self.assertEqual(dstwin.instr(0, 0), b'sectetur __') + self.assertEqual(dstwin.instr(1, 0), b'piscing elit, __') + self.assertEqual(dstwin.instr(2, 0), b' do eiusmod __') + self.assertEqual(dstwin.instr(3, 0), b'_________________') + + srcwin.overlay(dstwin, 1, 4, 3, 2, 4, 11) + self.assertEqual(dstwin.instr(3, 0), b'__r_sit_amet_____') + self.assertEqual(dstwin.instr(4, 0), b'__ectetur________') + self.assertEqual(dstwin.instr(5, 0), b'_________________') + + srcwin.overwrite(dstwin, 1, 4, 3, 2, 4, 11) + self.assertEqual(dstwin.instr(3, 0), b'__r sit amet_____') + self.assertEqual(dstwin.instr(4, 0), b'__ectetur _____') + self.assertEqual(dstwin.instr(5, 0), b'_________________') + + def test_refresh(self): + win = curses.newwin(5, 15, 2, 5) + win.noutrefresh() + win.redrawln(1, 2) + win.redrawwin() + win.refresh() + curses.doupdate() + + @requires_curses_window_meth('resize') + def test_resize(self): + win = curses.newwin(5, 15, 2, 5) + win.resize(4, 20) + self.assertEqual(win.getmaxyx(), (4, 20)) + win.resize(5, 15) + self.assertEqual(win.getmaxyx(), (5, 15)) + + @requires_curses_window_meth('enclose') + def test_enclose(self): + win = curses.newwin(5, 15, 2, 5) + self.assertIs(win.enclose(2, 5), True) + self.assertIs(win.enclose(1, 5), False) + self.assertIs(win.enclose(2, 4), False) + self.assertIs(win.enclose(6, 19), True) + self.assertIs(win.enclose(7, 19), False) + self.assertIs(win.enclose(6, 20), False) + + def test_putwin(self): + win = curses.newwin(5, 12, 1, 2) + win.addstr(2, 1, 'Lorem ipsum') + with tempfile.TemporaryFile() as f: + win.putwin(f) + del win + f.seek(0) + win = curses.getwin(f) + self.assertEqual(win.getbegyx(), (1, 2)) + self.assertEqual(win.getmaxyx(), (5, 12)) + self.assertEqual(win.instr(2, 0), b' Lorem ipsum') + + def test_borders_and_lines(self): + win = curses.newwin(5, 10, 5, 2) + win.border('|', '!', '-', '_', + '+', '\\', '#', '/') + self.assertEqual(win.instr(0, 0), b'+--------\\') + self.assertEqual(win.instr(1, 0), b'| !') + self.assertEqual(win.instr(4, 0), b'#________/') + win.border(b'|', b'!', b'-', b'_', + b'+', b'\\', b'#', b'/') + win.border(65, 66, 67, 68, + 69, 70, 71, 72) + self.assertRaises(TypeError, win.border, + 65, 66, 67, 68, 69, [], 71, 72) + self.assertRaises(TypeError, win.border, + 65, 66, 67, 68, 69, 70, 71, 72, 73) + self.assertRaises(TypeError, win.border, + 65, 66, 67, 68, 69, 70, 71, 72, 73) + win.border(65, 66, 67, 68, 69, 70, 71) + win.border(65, 66, 67, 68, 69, 70) + win.border(65, 66, 67, 68, 69) + win.border(65, 66, 67, 68) + win.border(65, 66, 67) + win.border(65, 66) + win.border(65) + win.border() + # With no arguments, border() fills the edges with ACS line and corner + # characters. + chartext = curses.A_CHARTEXT + maxy, maxx = win.getmaxyx() + self.assertEqual(win.inch(0, 0) & chartext, curses.ACS_ULCORNER & chartext) + self.assertEqual(win.inch(0, maxx-1) & chartext, curses.ACS_URCORNER & chartext) + self.assertEqual(win.inch(maxy-1, 0) & chartext, curses.ACS_LLCORNER & chartext) + self.assertEqual(win.inch(maxy-1, maxx-1) & chartext, curses.ACS_LRCORNER & chartext) + self.assertEqual(win.inch(0, 1) & chartext, curses.ACS_HLINE & chartext) + self.assertEqual(win.inch(1, 0) & chartext, curses.ACS_VLINE & chartext) + + win.box(':', '~') + self.assertEqual(win.instr(0, 1, 8), b'~~~~~~~~') + self.assertEqual(win.instr(1, 0), b': :') + self.assertEqual(win.instr(4, 1, 8), b'~~~~~~~~') + win.box(b':', b'~') + win.box(65, 67) + self.assertRaises(TypeError, win.box, 65, 66, 67) + self.assertRaises(TypeError, win.box, 65) + win.box() + # With no arguments, box() likewise draws ACS corners and lines. + self.assertEqual(win.inch(0, 0) & chartext, curses.ACS_ULCORNER & chartext) + self.assertEqual(win.inch(0, maxx-1) & chartext, curses.ACS_URCORNER & chartext) + self.assertEqual(win.inch(0, 1) & chartext, curses.ACS_HLINE & chartext) + self.assertEqual(win.inch(1, 0) & chartext, curses.ACS_VLINE & chartext) + + win.move(1, 2) + win.hline('-', 5) + self.assertEqual(win.instr(1, 1, 7), b' ----- ') + win.hline(b'-', 5) + win.hline(45, 5) + win.hline('-', 5, curses.A_BOLD) + win.hline(1, 1, '-', 5) + win.hline(1, 1, '-', 5, curses.A_BOLD) + + win.move(1, 2) + win.vline('a', 3) + win.vline(b'a', 3) + win.vline(97, 3) + win.vline('a', 3, curses.A_STANDOUT) + win.vline(1, 1, 'a', 3) + win.vline(1, 1, ';', 2, curses.A_STANDOUT) + self.assertEqual(win.inch(1, 1), b';'[0] | curses.A_STANDOUT) + self.assertEqual(win.inch(2, 1), b';'[0] | curses.A_STANDOUT) + self.assertEqual(win.inch(3, 1), b'a'[0]) + + # A border or line character of an 8-bit locale round-trips as its + # encoded byte. See _encodable for the character set. + encoding = win.encoding + for ch in ('é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + # A wide build stores a character outside Latin-1 as a wide cell, + # not as its encoded byte, so it cannot round-trip here. + if ord(ch) > 0xff and hasattr(win, 'get_wch'): + continue + with self.subTest(ch=ch): + win.erase() + win.hline(2, 0, ch, 5) + self.assertEqual(win.instr(2, 0, 5), b * 5) + win.vline(0, 0, ch, 3) + self.assertEqual(win.instr(0, 0, 1), b) + self.assertEqual(win.instr(1, 0, 1), b) + win.border(ch, ch, ch, ch, ch, ch, ch, ch) + self.assertEqual(win.instr(0, 0), b * maxx) + if ord(ch) < 0x100: + # The same byte given as an int. A wide build stores it + # through the locale, so only a Latin-1 byte round-trips. + v = b[0] + win.erase() + win.hline(2, 0, v, 5) + self.assertEqual(win.instr(2, 0, 5), b * 5) + win.vline(0, 0, v, 3) + self.assertEqual(win.instr(1, 0, 1), b) + win.border(v, v, v, v, v, v, v, v) + self.assertEqual(win.instr(0, 0), b * maxx) + win.box(v, v) + self.assertEqual(win.instr(0, 1, 1), b) + + def test_unctrl(self): + # TODO: wunctrl() + self.assertEqual(curses.unctrl(b'A'), b'A') + self.assertEqual(curses.unctrl('A'), b'A') + self.assertEqual(curses.unctrl(65), b'A') + self.assertEqual(curses.unctrl(b'\n'), b'^J') + self.assertEqual(curses.unctrl('\n'), b'^J') + self.assertEqual(curses.unctrl(10), b'^J') + # A printable non-ASCII byte of an 8-bit locale is returned unchanged. + # See _encodable for the character set. + encoding = self.stdscr.encoding + for ch in ('é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + with self.subTest(ch=ch): + self.assertEqual(curses.unctrl(ch), b) + self.assertEqual(curses.unctrl(b[0]), b) # the byte as an int + self.assertRaises(TypeError, curses.unctrl, b'') + self.assertRaises(TypeError, curses.unctrl, b'AB') + self.assertRaises(TypeError, curses.unctrl, '') + self.assertRaises(TypeError, curses.unctrl, 'AB') + self.assertRaises(OverflowError, curses.unctrl, 2**64) + + def test_endwin(self): + if not self.isatty: + self.skipTest('requires terminal') + self.assertIs(curses.isendwin(), False) + curses.endwin() + self.assertIs(curses.isendwin(), True) + curses.doupdate() + self.assertIs(curses.isendwin(), False) + + def test_terminfo(self): + self.assertIsInstance(curses.tigetflag('hc'), int) + self.assertEqual(curses.tigetflag('cols'), -1) + self.assertEqual(curses.tigetflag('cr'), -1) + + self.assertIsInstance(curses.tigetnum('cols'), int) + self.assertEqual(curses.tigetnum('hc'), -2) + self.assertEqual(curses.tigetnum('cr'), -2) + + self.assertIsInstance(curses.tigetstr('cr'), (bytes, type(None))) + self.assertIsNone(curses.tigetstr('hc')) + self.assertIsNone(curses.tigetstr('cols')) + + cud = curses.tigetstr('cud') + if cud is not None: + # See issue10570. + self.assertIsInstance(cud, bytes) + curses.tparm(cud, 2) + cud_2 = curses.tparm(cud, 2) + self.assertIsInstance(cud_2, bytes) + curses.putp(cud_2) + + curses.putp(b'abc\n') + + def test_misc_module_funcs(self): + curses.delay_output(1) + curses.flushinp() + + curses.doupdate() + self.assertIs(curses.isendwin(), False) + + curses.napms(100) + + curses.newpad(50, 50) + + def test_env_queries(self): + # TODO: term_attrs(), erasewchar(), killwchar() + self.assertIsInstance(curses.termname(), bytes) + self.assertIsInstance(curses.longname(), bytes) + self.assertIsInstance(curses.baudrate(), int) + self.assertIsInstance(curses.has_ic(), bool) + self.assertIsInstance(curses.has_il(), bool) + self.assertIsInstance(curses.termattrs(), int) + + c = curses.killchar() + self.assertIsInstance(c, bytes) + self.assertEqual(len(c), 1) + c = curses.erasechar() + self.assertIsInstance(c, bytes) + self.assertEqual(len(c), 1) + + def test_output_options(self): + stdscr = self.stdscr + + stdscr.clearok(True) + stdscr.clearok(False) + + stdscr.idcok(True) + stdscr.idcok(False) + + stdscr.idlok(False) + stdscr.idlok(True) + + if hasattr(stdscr, 'immedok'): + stdscr.immedok(True) + stdscr.immedok(False) + + stdscr.leaveok(True) + stdscr.leaveok(False) + + stdscr.scrollok(True) + stdscr.scrollok(False) + + stdscr.setscrreg(5, 10) + + curses.nonl() + curses.nl(True) + curses.nl(False) + curses.nl() + + def test_input_options(self): + stdscr = self.stdscr + + if self.isatty: + curses.nocbreak() + curses.cbreak() + curses.cbreak(False) + curses.cbreak(True) + + curses.intrflush(True) + curses.intrflush(False) + + curses.raw() + curses.raw(False) + curses.raw(True) + curses.noraw() + + curses.noecho() + curses.echo() + curses.echo(False) + curses.echo(True) + + curses.halfdelay(255) + curses.halfdelay(1) + + stdscr.keypad(True) + stdscr.keypad(False) + + curses.meta(True) + curses.meta(False) + + stdscr.nodelay(True) + stdscr.nodelay(False) + + curses.noqiflush() + curses.qiflush(True) + curses.qiflush(False) + curses.qiflush() + + stdscr.notimeout(True) + stdscr.notimeout(False) + + stdscr.timeout(-1) + stdscr.timeout(0) + stdscr.timeout(5) + + @requires_curses_func('typeahead') + def test_typeahead(self): + curses.typeahead(sys.__stdin__.fileno()) + curses.typeahead(-1) + + def test_prog_mode(self): + if not self.isatty: + self.skipTest('requires terminal') + curses.def_prog_mode() + curses.reset_prog_mode() + # def_shell_mode()/reset_shell_mode() are intentionally not exercised + # here: they capture and restore curses' "shell mode" terminal state, + # which is only meaningful before initscr(). Calling them mid-suite + # corrupts the modes that endwin() restores and breaks later tests. + + def test_beep(self): + if (curses.tigetstr("bel") is not None + or curses.tigetstr("flash") is not None): + curses.beep() + else: + try: + curses.beep() + except curses.error: + self.skipTest('beep() failed') + + def test_flash(self): + if (curses.tigetstr("bel") is not None + or curses.tigetstr("flash") is not None): + curses.flash() + else: + try: + curses.flash() + except curses.error: + self.skipTest('flash() failed') + + def test_curs_set(self): + for vis, cap in [(0, 'civis'), (2, 'cvvis'), (1, 'cnorm')]: + if curses.tigetstr(cap) is not None: + curses.curs_set(vis) + else: + try: + curses.curs_set(vis) + except curses.error: + pass + + @requires_curses_func('get_escdelay') + def test_escdelay(self): + escdelay = curses.get_escdelay() + self.assertIsInstance(escdelay, int) + curses.set_escdelay(25) + self.assertEqual(curses.get_escdelay(), 25) + curses.set_escdelay(escdelay) + + @requires_curses_func('get_tabsize') + def test_tabsize(self): + tabsize = curses.get_tabsize() + self.assertIsInstance(tabsize, int) + curses.set_tabsize(4) + self.assertEqual(curses.get_tabsize(), 4) + curses.set_tabsize(tabsize) + + @requires_curses_func('getsyx') + def test_getsyx(self): + y, x = curses.getsyx() + self.assertIsInstance(y, int) + self.assertIsInstance(x, int) + curses.setsyx(4, 5) + self.assertEqual(curses.getsyx(), (4, 5)) + + def bad_colors(self): + return (-1, curses.COLORS, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64) + + def bad_colors2(self): + return (curses.COLORS, 2**31, 2**63, 2**64) + + def bad_pairs(self): + return (-1, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64) + + def test_has_colors(self): + self.assertIsInstance(curses.has_colors(), bool) + self.assertIsInstance(curses.can_change_color(), bool) + + def test_start_color(self): + if not curses.has_colors(): + self.skipTest('requires colors support') + curses.start_color() + if verbose: + print(f'COLORS = {curses.COLORS}', file=sys.stderr) + print(f'COLOR_PAIRS = {curses.COLOR_PAIRS}', file=sys.stderr) + + @requires_colors + def test_color_content(self): + self.assertEqual(curses.color_content(curses.COLOR_BLACK), (0, 0, 0)) + curses.color_content(0) + maxcolor = curses.COLORS - 1 + curses.color_content(maxcolor) + + for color in self.bad_colors(): + self.assertRaises(ValueError, curses.color_content, color) + + @requires_colors + def test_init_color(self): + if not curses.can_change_color(): + self.skipTest('cannot change color') + + old = curses.color_content(0) + try: + curses.init_color(0, *old) + except curses.error: + self.skipTest('cannot change color (init_color() failed)') + self.addCleanup(curses.init_color, 0, *old) + curses.init_color(0, 0, 0, 0) + self.assertEqual(curses.color_content(0), (0, 0, 0)) + curses.init_color(0, 1000, 1000, 1000) + self.assertEqual(curses.color_content(0), (1000, 1000, 1000)) + + maxcolor = curses.COLORS - 1 + old = curses.color_content(maxcolor) + curses.init_color(maxcolor, *old) + self.addCleanup(curses.init_color, maxcolor, *old) + curses.init_color(maxcolor, 0, 500, 1000) + self.assertEqual(curses.color_content(maxcolor), (0, 500, 1000)) + + for color in self.bad_colors(): + self.assertRaises(ValueError, curses.init_color, color, 0, 0, 0) + for comp in (-1, 1001): + self.assertRaises(ValueError, curses.init_color, 0, comp, 0, 0) + self.assertRaises(ValueError, curses.init_color, 0, 0, comp, 0) + self.assertRaises(ValueError, curses.init_color, 0, 0, 0, comp) + + def get_pair_limit(self): + pair_limit = curses.COLOR_PAIRS + if hasattr(curses, 'ncurses_version'): + if curses.has_extended_color_support(): + pair_limit += 2*curses.COLORS + 1 + if (not curses.has_extended_color_support() + or (6, 1) <= curses.ncurses_version < (6, 2)): + pair_limit = min(pair_limit, SHORT_MAX) + # If use_default_colors() is called, the upper limit of the extended + # range may be restricted, so we need to check if the limit is still + # correct + try: + curses.init_pair(pair_limit - 1, 0, 0) + except ValueError: + pair_limit = curses.COLOR_PAIRS + return pair_limit + + @requires_colors + def test_pair_content(self): + curses.pair_content(0) + maxpair = self.get_pair_limit() - 1 + if maxpair > 0: + curses.pair_content(maxpair) + + for pair in self.bad_pairs(): + self.assertRaises(ValueError, curses.pair_content, pair) + + @requires_colors + def test_init_pair(self): + old = curses.pair_content(1) + curses.init_pair(1, *old) + self.addCleanup(curses.init_pair, 1, *old) + + curses.init_pair(1, 0, 0) + self.assertEqual(curses.pair_content(1), (0, 0)) + maxcolor = curses.COLORS - 1 + curses.init_pair(1, maxcolor, 0) + self.assertEqual(curses.pair_content(1), (maxcolor, 0)) + curses.init_pair(1, 0, maxcolor) + self.assertEqual(curses.pair_content(1), (0, maxcolor)) + maxpair = self.get_pair_limit() - 1 + if maxpair > 1: + curses.init_pair(maxpair, 0, 0) + self.assertEqual(curses.pair_content(maxpair), (0, 0)) + + for pair in self.bad_pairs(): + self.assertRaises(ValueError, curses.init_pair, pair, 0, 0) + for color in self.bad_colors2(): + self.assertRaises(ValueError, curses.init_pair, 1, color, 0) + self.assertRaises(ValueError, curses.init_pair, 1, 0, color) + + @requires_colors + def test_color_attrs(self): + for pair in 0, 1, 255: + attr = curses.color_pair(pair) + self.assertEqual(curses.pair_number(attr), pair, attr) + self.assertEqual(curses.pair_number(attr | curses.A_BOLD), pair) + self.assertEqual(curses.color_pair(0), 0) + self.assertEqual(curses.pair_number(0), 0) + + @requires_curses_func('use_default_colors') + @requires_colors + def test_use_default_colors(self): + try: + curses.use_default_colors() + except curses.error: + self.skipTest('cannot change color (use_default_colors() failed)') + self.assertEqual(curses.pair_content(0), (-1, -1)) + + @requires_curses_func('assume_default_colors') + @requires_colors + def test_assume_default_colors(self): + try: + curses.assume_default_colors(-1, -1) + except curses.error: + self.skipTest('cannot change color (assume_default_colors() failed)') + self.assertEqual(curses.pair_content(0), (-1, -1)) + curses.assume_default_colors(curses.COLOR_YELLOW, curses.COLOR_BLUE) + self.assertEqual(curses.pair_content(0), (curses.COLOR_YELLOW, curses.COLOR_BLUE)) + curses.assume_default_colors(curses.COLOR_RED, -1) + self.assertEqual(curses.pair_content(0), (curses.COLOR_RED, -1)) + curses.assume_default_colors(-1, curses.COLOR_GREEN) + self.assertEqual(curses.pair_content(0), (-1, curses.COLOR_GREEN)) + curses.assume_default_colors(-1, -1) + + def test_keyname(self): + # TODO: key_name() + self.assertEqual(curses.keyname(65), b'A') + self.assertEqual(curses.keyname(13), b'^M') + self.assertEqual(curses.keyname(127), b'^?') + self.assertEqual(curses.keyname(0), b'^@') + self.assertRaises(ValueError, curses.keyname, -1) + self.assertIsInstance(curses.keyname(256), bytes) + + @requires_curses_func('has_key') + def test_has_key(self): + self.assertIsInstance(curses.has_key(13), bool) + self.assertIsInstance(curses.has_key(curses.KEY_LEFT), bool) + + @requires_curses_func('getmouse') + def test_getmouse(self): + (availmask, oldmask) = curses.mousemask(curses.BUTTON1_PRESSED) + if availmask == 0: + self.skipTest('mouse stuff not available') + curses.mouseinterval(10) + # just verify these don't cause errors + curses.ungetmouse(0, 0, 0, 0, curses.BUTTON1_PRESSED) + m = curses.getmouse() + + @requires_curses_func('panel') + def test_userptr_without_set(self): + w = curses.newwin(10, 10) + p = curses.panel.new_panel(w) + # try to access userptr() before calling set_userptr() -- segfaults + with self.assertRaises(curses.panel.error, + msg='userptr should fail since not set'): + p.userptr() + + @requires_curses_func('panel') + def test_userptr_memory_leak(self): + w = curses.newwin(10, 10) + p = curses.panel.new_panel(w) + obj = object() + nrefs = sys.getrefcount(obj) + for i in range(100): + p.set_userptr(obj) + + p.set_userptr(None) + self.assertEqual(sys.getrefcount(obj), nrefs, + "set_userptr leaked references") + + @requires_curses_func('panel') + def test_userptr_segfault(self): + w = curses.newwin(10, 10) + panel = curses.panel.new_panel(w) + class A: + def __del__(self): + panel.set_userptr(None) + panel.set_userptr(A()) + panel.set_userptr(None) + + @cpython_only + @requires_curses_func('panel') + def test_disallow_instantiation(self): + # Ensure that the type disallows instantiation (bpo-43916) + w = curses.newwin(10, 10) + panel = curses.panel.new_panel(w) + check_disallow_instantiation(self, type(panel)) + + @requires_curses_func('panel') + def test_panel_stack(self): + panel = curses.panel + # new_panel() puts the panel on top of the stack, so the three + # panels end up ordered bottom -> top as p1, p2, p3. + p1 = panel.new_panel(curses.newwin(3, 6, 0, 0)) + p2 = panel.new_panel(curses.newwin(3, 6, 1, 1)) + p3 = panel.new_panel(curses.newwin(3, 6, 2, 2)) + self.addCleanup(self._delete_panels, p1, p2, p3) + + # The most recently created panel is on top. + self.assertIs(panel.top_panel(), p3) + # window() returns the wrapped window. + self.assertEqual(p2.window().getbegyx(), (1, 1)) + + # above()/below() walk the stack one step at a time. + self.assertIs(p1.above(), p2) + self.assertIs(p2.above(), p3) + self.assertIsNone(p3.above()) # nothing above the top panel + self.assertIs(p3.below(), p2) + self.assertIs(p2.below(), p1) + + # top() raises a panel to the top, bottom() lowers it to the bottom. + p1.top() + self.assertIs(panel.top_panel(), p1) + self.assertIsNone(p1.above()) + p1.bottom() + self.assertIs(panel.bottom_panel(), p1) + self.assertIsNone(p1.below()) + + # update_panels() refreshes the virtual screen from the stack. + panel.update_panels() + + @requires_curses_func('panel') + def test_panel_hide_show(self): + p = curses.panel.new_panel(curses.newwin(3, 6, 0, 0)) + self.addCleanup(self._delete_panels, p) + self.assertIs(p.hidden(), False) + p.hide() + self.assertIs(p.hidden(), True) + p.show() + self.assertIs(p.hidden(), False) + + @requires_curses_func('panel') + def test_panel_move(self): + win = curses.newwin(3, 6, 1, 2) + p = curses.panel.new_panel(win) + self.addCleanup(self._delete_panels, p) + self.assertEqual(win.getbegyx(), (1, 2)) + p.move(4, 5) + self.assertEqual(win.getbegyx(), (4, 5)) + + @requires_curses_func('panel') + def test_panel_replace(self): + win1 = curses.newwin(3, 6, 0, 0) + win2 = curses.newwin(4, 8, 1, 1) + p = curses.panel.new_panel(win1) + self.addCleanup(self._delete_panels, p) + self.assertIs(p.window(), win1) + p.replace(win2) + self.assertIs(p.window(), win2) + + @requires_curses_func('panel') + def test_panel_userptr(self): + p = curses.panel.new_panel(curses.newwin(3, 6, 0, 0)) + self.addCleanup(self._delete_panels, p) + obj = ['userptr'] + p.set_userptr(obj) + self.assertIs(p.userptr(), obj) + + def _delete_panels(self, *panels): + # Drop the panels from the global stack so they do not leak into + # later tests that inspect top_panel()/bottom_panel(). + for p in panels: + try: + p.bottom() + except curses.panel.error: + pass + del panels + gc_collect() + + def _make_textbox(self, nlines, ncols, *, insert_mode=False, stripspaces=1): + win = curses.newwin(nlines, ncols, 0, 0) + box = curses.textpad.Textbox(win, insert_mode=insert_mode) + box.stripspaces = stripspaces + return box, win + + def _type(self, box, text): + for ch in text: + box.do_command(ch if isinstance(ch, int) else ord(ch)) + + def test_textbox_gather(self): + # Typed text is read back by gather(). With stripspaces on (the + # default) gather() keeps a single trailing blank on a line and + # drops trailing empty lines. + box, win = self._make_textbox(3, 10) + self._type(box, 'Hello') + self.assertEqual(box.gather(), 'Hello \n') + + def test_textbox_gather_multiline(self): + box, win = self._make_textbox(3, 10) + self._type(box, 'ab') + box.do_command(curses.ascii.NL) # ^j -> start of next line + self._type(box, 'cd') + self.assertEqual(box.gather(), 'ab \ncd \n') + + def test_textbox_stripspaces(self): + box, win = self._make_textbox(1, 8, stripspaces=1) + self._type(box, 'hi') + self.assertEqual(box.gather(), 'hi ') + + box, win = self._make_textbox(1, 8, stripspaces=0) + self._type(box, 'hi') + self.assertEqual(box.gather(), 'hi ') + + def test_textbox_insert_mode(self): + # In insert mode a typed character shifts the rest of the line right. + box, win = self._make_textbox(1, 10, insert_mode=True) + self._type(box, 'aXc') + win.move(0, 1) + self._type(box, 'b') + self.assertEqual(box.gather(), 'abXc ') + + def test_textbox_fill_last_cell(self): + # The lower-right cell can be written, even though addch() there + # cannot advance the cursor past the end of the window. + box, win = self._make_textbox(1, 4, stripspaces=0) + self._type(box, 'abcd') + self.assertEqual(box.gather(), 'abcd') + + def test_textbox_fill_last_cell_multiline(self): + box, win = self._make_textbox(2, 3, stripspaces=0) + self._type(box, 'abc') + box.do_command(curses.ascii.NL) # ^j -> start of next line + self._type(box, 'def') # 'f' lands in the lower-right cell + self.assertEqual(box.gather(), 'abc\ndef\n') + + def test_textbox_fill_last_cell_insert_mode(self): + box, win = self._make_textbox(1, 4, insert_mode=True, stripspaces=0) + self._type(box, 'abcd') + self.assertEqual(box.gather(), 'abcd') + + def test_textbox_fill_last_cell_scrollok(self): + # Writing the lower-right cell must not scroll the window even if it + # has scrolling enabled. + box, win = self._make_textbox(2, 3, stripspaces=0) + win.scrollok(True) + self._type(box, 'abc') + box.do_command(curses.ascii.NL) + self._type(box, 'def') + self.assertEqual(box.gather(), 'abc\ndef\n') + + def test_textbox_8bit(self): + # A character of an 8-bit locale encoding is entered and read back + # through the byte API. The byte path also runs on a wide build, so the + # test is not skipped there. Run the suite under an 8-bit locale + # (ISO-8859-1, ISO-8859-15 or KOI8-U) to reach the non-ASCII cases; each + # string is used only if the encoding maps it to single bytes. 'abc' is + # ASCII, 'café' is common to the Latin encodings, and the rest are + # distinctive (byte 0xA4 is '¤'/'€'/'є' in ISO-8859-1/-15/KOI8-U). + encoding = self.stdscr.encoding + for text in ['abc', 'café', 'naïve ¤¦', 'café €Šž', 'дякую єі']: + try: + data = text.encode(encoding) + except UnicodeEncodeError: + continue + if len(data) != len(text): + continue # a multibyte encoding is not the 8-bit byte path + with self.subTest(text=text): + box, win = self._make_textbox(1, 16) + for byte in data: + box.do_command(byte) + self.assertEqual(box.gather(), text + ' ') + + def test_textbox_8bit_insert(self): + # Insert mode shifts the rest of the line right by reading each cell back + # and rewriting it; a non-ASCII 8-bit-locale character must survive the + # shift, even on a wide build where inch() mangles it. See + # test_textbox_8bit for the character choices. + encoding = self.stdscr.encoding + for ch in ['é', '¤', '€', 'є']: + try: + data = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(data) != 1: + continue + with self.subTest(ch=ch): + box, win = self._make_textbox(1, 10, insert_mode=True) + for byte in ('a' + ch + 'c').encode(encoding): + box.do_command(byte) + win.move(0, 1) + box.do_command(ord('b')) # insert 'b', shifting ch and 'c' right + self.assertEqual(box.gather(), 'ab' + ch + 'c ') + + def test_textbox_8bit_fill_last_cell(self): + # A non-ASCII 8-bit-locale character must survive being written to the + # lower-right cell, which uses insch() rather than addch(). See + # test_textbox_8bit for the character choices. + encoding = self.stdscr.encoding + for ch in ['é', '¤', '€', 'є']: + try: + data = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(data) != 1: + continue + with self.subTest(ch=ch): + text = 'ab' + ch # the last character fills the corner + box, win = self._make_textbox(1, len(text), stripspaces=0) + for byte in text.encode(encoding): + box.do_command(byte) + self.assertEqual(box.gather(), text) + + def test_textbox_movement(self): + box, win = self._make_textbox(3, 10) + self._type(box, 'abc') + box.do_command(curses.ascii.SOH) # ^a -> left edge + self.assertEqual(win.getyx(), (0, 0)) + box.do_command(curses.ascii.ENQ) # ^e -> end of line + self.assertEqual(win.getyx(), (0, 3)) + + def test_textbox_kill_to_eol(self): + box, win = self._make_textbox(1, 10) + self._type(box, 'abcdef') + win.move(0, 3) + box.do_command(curses.ascii.VT) # ^k -> clear to end of line + self.assertEqual(box.gather(), 'abc ') + + def test_textbox_backspace(self): + box, win = self._make_textbox(1, 10) + self._type(box, 'abc') + box.do_command(curses.ascii.BS) # ^h -> delete backward + self.assertEqual(box.gather(), 'ab ') + + def test_textbox_edit(self): + # edit() reads characters until Ctrl-G and returns the contents. + box, win = self._make_textbox(1, 10) + for ch in reversed('Hi' + chr(curses.ascii.BEL)): + curses.ungetch(ch) + self.assertEqual(box.edit(), 'Hi ') + + def test_textbox_edit_validate(self): + # The validate hook can rewrite an incoming keystroke. + box, win = self._make_textbox(1, 10) + for ch in reversed('abc' + chr(curses.ascii.BEL)): + curses.ungetch(ch) + box.edit(lambda ch: ord('X') if ch == ord('b') else ch) + self.assertEqual(box.gather(), 'aXc ') + + def test_textpad_rectangle(self): + # rectangle() draws a box with ACS line/corner characters. + win = curses.newwin(6, 12, 0, 0) + curses.textpad.rectangle(win, 0, 0, 4, 8) + chartext = curses.A_CHARTEXT + self.assertEqual(win.inch(0, 0) & chartext, + curses.ACS_ULCORNER & chartext) + self.assertEqual(win.inch(0, 8) & chartext, + curses.ACS_URCORNER & chartext) + self.assertEqual(win.inch(4, 0) & chartext, + curses.ACS_LLCORNER & chartext) + self.assertEqual(win.inch(4, 8) & chartext, + curses.ACS_LRCORNER & chartext) + self.assertEqual(win.inch(0, 1) & chartext, + curses.ACS_HLINE & chartext) + self.assertEqual(win.inch(1, 0) & chartext, + curses.ACS_VLINE & chartext) + + def test_wrapper(self): + # wrapper() sets up curses, passes the screen to the callable along + # with extra arguments, returns its result and restores the terminal. + if not self.isatty: + self.skipTest('requires terminal') + + def body(stdscr, a, b): + self.assertIsInstance(stdscr, type(self.stdscr)) + self.assertIs(curses.isendwin(), False) + return a + b + + self.assertEqual(curses.wrapper(body, 2, 3), 5) + self.assertIs(curses.isendwin(), True) + # wrapper() left the screen ended; revive it so the per-test + # endwin() cleanup does not fail with ERR. + curses.doupdate() + + @requires_curses_func('is_term_resized') + def test_is_term_resized(self): + lines, cols = curses.LINES, curses.COLS + self.assertIs(curses.is_term_resized(lines, cols), False) + self.assertIs(curses.is_term_resized(lines-1, cols-1), True) + + @requires_curses_func('resize_term') + def test_resize_term(self): + curses.update_lines_cols() + lines, cols = curses.LINES, curses.COLS + new_lines = lines - 1 + new_cols = cols + 1 + curses.resize_term(new_lines, new_cols) + self.assertEqual(curses.LINES, new_lines) + self.assertEqual(curses.COLS, new_cols) + + curses.resize_term(lines, cols) + self.assertEqual(curses.LINES, lines) + self.assertEqual(curses.COLS, cols) + + with self.assertRaises(OverflowError): + curses.resize_term(35000, 1) + with self.assertRaises(OverflowError): + curses.resize_term(1, 35000) + # GH-120378: Overflow failure in resize_term() causes refresh to fail + tmp = curses.initscr() + tmp.erase() + + @requires_curses_func('resizeterm') + def test_resizeterm(self): + curses.update_lines_cols() + lines, cols = curses.LINES, curses.COLS + new_lines = lines - 1 + new_cols = cols + 1 + curses.resizeterm(new_lines, new_cols) + self.assertEqual(curses.LINES, new_lines) + self.assertEqual(curses.COLS, new_cols) + + curses.resizeterm(lines, cols) + self.assertEqual(curses.LINES, lines) + self.assertEqual(curses.COLS, cols) + + with self.assertRaises(OverflowError): + curses.resizeterm(35000, 1) + with self.assertRaises(OverflowError): + curses.resizeterm(1, 35000) + # GH-120378: Overflow failure in resizeterm() causes refresh to fail + tmp = curses.initscr() + tmp.erase() + + def test_ungetch(self): + curses.ungetch(b'A') + self.assertEqual(self.stdscr.getkey(), 'A') + curses.ungetch('B') + self.assertEqual(self.stdscr.getkey(), 'B') + curses.ungetch(67) + self.assertEqual(self.stdscr.getkey(), 'C') + + def test_issue6243(self): + curses.ungetch(1025) + self.stdscr.getkey() + + @requires_curses_func('unget_wch') + @unittest.skipIf(getattr(curses, 'ncurses_version', (99,)) < (5, 8), + "unget_wch is broken in ncurses 5.7 and earlier") + def test_unget_wch(self): + stdscr = self.stdscr + encoding = stdscr.encoding + # See _encodable for the character set, plus a non-BMP character. + for ch in ('a', '\xe9', '\xa4', '\u20ac', '\u0454', '\U0010FFFF'): + try: + ch.encode(encoding) + except UnicodeEncodeError: + continue + try: + curses.unget_wch(ch) + except Exception as err: + self.fail("unget_wch(%a) failed with encoding %s: %s" + % (ch, stdscr.encoding, err)) + read = stdscr.get_wch() + self.assertEqual(read, ch) + + code = ord(ch) + curses.unget_wch(code) + read = stdscr.get_wch() + self.assertEqual(read, ch) + + def test_encoding(self): + stdscr = self.stdscr + import codecs + encoding = stdscr.encoding + codecs.lookup(encoding) + with self.assertRaises(TypeError): + stdscr.encoding = 10 + stdscr.encoding = encoding + with self.assertRaises(TypeError): + del stdscr.encoding + + @unittest.skipIf(MISSING_C_DOCSTRINGS, + "Signature information for builtins requires docstrings") + def test_issue21088(self): + stdscr = self.stdscr + # + # http://bugs.python.org/issue21088 + # + # the bug: + # when converting curses.window.addch to Argument Clinic + # the first two parameters were switched. + + # if someday we can represent the signature of addch + # we will need to rewrite this test. + try: + signature = inspect.signature(stdscr.addch) + self.assertFalse(signature) + except ValueError: + # not generating a signature is fine. + pass + + # So. No signature for addch. + # But Argument Clinic gave us a human-readable equivalent + # as the first line of the docstring. So we parse that, + # and ensure that the parameters appear in the correct order. + # Since this is parsing output from Argument Clinic, we can + # be reasonably certain the generated parsing code will be + # correct too. + human_readable_signature = stdscr.addch.__doc__.split("\n")[0] + self.assertIn("[y, x,]", human_readable_signature) + + @requires_curses_window_meth('resize') + def test_issue13051(self): + win = curses.newwin(5, 15, 2, 5) + box = curses.textpad.Textbox(win, insert_mode=True) + lines, cols = win.getmaxyx() + win.resize(lines-2, cols-2) + # this may cause infinite recursion, leading to a RuntimeError + box._insert_printable_char('a') + + +class MiscTests(unittest.TestCase): + + @requires_curses_func('update_lines_cols') + def test_update_lines_cols(self): + curses.update_lines_cols() + lines, cols = curses.LINES, curses.COLS + curses.LINES = curses.COLS = 0 + curses.update_lines_cols() + self.assertEqual(curses.LINES, lines) + self.assertEqual(curses.COLS, cols) + + @requires_curses_func('ncurses_version') + def test_ncurses_version(self): + v = curses.ncurses_version + if verbose: + print(f'ncurses_version = {curses.ncurses_version}', flush=True) + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 3) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.patch, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.patch) + self.assertGreaterEqual(v.major, 0) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.patch, 0) + + def test_has_extended_color_support(self): + r = curses.has_extended_color_support() + self.assertIsInstance(r, bool) + + +class TestAscii(unittest.TestCase): + + def test_controlnames(self): + for name in curses.ascii.controlnames: + self.assertHasAttr(curses.ascii, name) + + def test_ctypes(self): + def check(func, expected): + with self.subTest(ch=c, func=func): + self.assertEqual(func(i), expected) + self.assertEqual(func(c), expected) + + for i in range(256): + c = chr(i) + b = bytes([i]) + check(curses.ascii.isalnum, b.isalnum()) + check(curses.ascii.isalpha, b.isalpha()) + check(curses.ascii.isdigit, b.isdigit()) + check(curses.ascii.islower, b.islower()) + check(curses.ascii.isspace, b.isspace()) + check(curses.ascii.isupper, b.isupper()) + + check(curses.ascii.isascii, i < 128) + check(curses.ascii.ismeta, i >= 128) + check(curses.ascii.isctrl, i < 32) + check(curses.ascii.iscntrl, i < 32 or i == 127) + check(curses.ascii.isblank, c in ' \t') + check(curses.ascii.isgraph, 32 < i <= 126) + check(curses.ascii.isprint, 32 <= i <= 126) + check(curses.ascii.ispunct, c in string.punctuation) + check(curses.ascii.isxdigit, c in string.hexdigits) + + for i in (-2, -1, 256, sys.maxunicode, sys.maxunicode+1): + self.assertFalse(curses.ascii.isalnum(i)) + self.assertFalse(curses.ascii.isalpha(i)) + self.assertFalse(curses.ascii.isdigit(i)) + self.assertFalse(curses.ascii.islower(i)) + self.assertFalse(curses.ascii.isspace(i)) + self.assertFalse(curses.ascii.isupper(i)) + + self.assertFalse(curses.ascii.isascii(i)) + self.assertFalse(curses.ascii.isctrl(i)) + self.assertFalse(curses.ascii.iscntrl(i)) + self.assertFalse(curses.ascii.isblank(i)) + self.assertFalse(curses.ascii.isgraph(i)) + self.assertFalse(curses.ascii.isprint(i)) + self.assertFalse(curses.ascii.ispunct(i)) + self.assertFalse(curses.ascii.isxdigit(i)) + + self.assertFalse(curses.ascii.ismeta(-1)) + + def test_ascii(self): + ascii = curses.ascii.ascii + self.assertEqual(ascii('\xc1'), 'A') + self.assertEqual(ascii('A'), 'A') + self.assertEqual(ascii(ord('\xc1')), ord('A')) + + def test_ctrl(self): + ctrl = curses.ascii.ctrl + self.assertEqual(ctrl('J'), '\n') + self.assertEqual(ctrl('\n'), '\n') + self.assertEqual(ctrl('@'), '\0') + self.assertEqual(ctrl(ord('J')), ord('\n')) + + def test_alt(self): + alt = curses.ascii.alt + self.assertEqual(alt('\n'), '\x8a') + self.assertEqual(alt('A'), '\xc1') + self.assertEqual(alt(ord('A')), 0xc1) + + def test_unctrl(self): + unctrl = curses.ascii.unctrl + self.assertEqual(unctrl('a'), 'a') + self.assertEqual(unctrl('A'), 'A') + self.assertEqual(unctrl(';'), ';') + self.assertEqual(unctrl(' '), ' ') + self.assertEqual(unctrl('\x7f'), '^?') + self.assertEqual(unctrl('\n'), '^J') + self.assertEqual(unctrl('\0'), '^@') + self.assertEqual(unctrl(ord('A')), 'A') + self.assertEqual(unctrl(ord('\n')), '^J') + # Meta-bit characters + self.assertEqual(unctrl('\x8a'), '!^J') + self.assertEqual(unctrl('\xc1'), '!A') + self.assertEqual(unctrl(ord('\x8a')), '!^J') + self.assertEqual(unctrl(ord('\xc1')), '!A') + + +def lorem_ipsum(win): + text = [ + 'Lorem ipsum', + 'dolor sit amet,', + 'consectetur', + 'adipiscing elit,', + 'sed do eiusmod', + 'tempor incididunt', + 'ut labore et', + 'dolore magna', + 'aliqua.', + ] + maxy, maxx = win.getmaxyx() + for y, line in enumerate(text[:maxy]): + win.addstr(y, 0, line[:maxx - (y == maxy - 1)]) + + +class TextboxTest(unittest.TestCase): + def setUp(self): + self.mock_win = MagicMock(spec=curses.window) + self.mock_win.getyx.return_value = (1, 1) + self.mock_win.getmaxyx.return_value = (10, 20) + self.mock_win.encoding = 'utf-8' + # A non-blank cell so that _end_of_line() reports a full line: instr() + # backs the text reads, inch() the insert-mode shift. + self.mock_win.instr.return_value = b'x' + self.mock_win.inch.return_value = ord('x') + self.textbox = curses.textpad.Textbox(self.mock_win) + + def test_init(self): + """Test textbox initialization.""" + self.mock_win.reset_mock() + tb = curses.textpad.Textbox(self.mock_win) + self.mock_win.getmaxyx.assert_called_once_with() + self.mock_win.keypad.assert_called_once_with(1) + self.assertEqual(tb.insert_mode, False) + self.assertEqual(tb.stripspaces, 1) + self.assertIsNone(tb.lastcmd) + self.mock_win.reset_mock() + + def test_insert(self): + """Test inserting a printable character.""" + self.mock_win.reset_mock() + self.textbox.do_command(ord('a')) + self.mock_win.addch.assert_called_with(ord('a')) + self.textbox.do_command(ord('b')) + self.mock_win.addch.assert_called_with(ord('b')) + self.textbox.do_command(ord('c')) + self.mock_win.addch.assert_called_with(ord('c')) + self.mock_win.reset_mock() + + def test_delete(self): + """Test deleting a character.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.ascii.BS) + self.textbox.do_command(curses.KEY_BACKSPACE) + self.textbox.do_command(curses.ascii.DEL) + assert self.mock_win.delch.call_count == 3 + self.mock_win.reset_mock() + + def test_move_left(self): + """Test moving the cursor left.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_LEFT) + self.mock_win.move.assert_called_with(1, 0) + self.mock_win.reset_mock() + + def test_move_right(self): + """Test moving the cursor right.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_RIGHT) + self.mock_win.move.assert_called_with(1, 2) + self.mock_win.reset_mock() + + def test_move_left_and_right(self): + """Test moving the cursor left and then right.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_LEFT) + self.mock_win.move.assert_called_with(1, 0) + self.textbox.do_command(curses.KEY_RIGHT) + self.mock_win.move.assert_called_with(1, 2) + self.mock_win.reset_mock() + + def test_move_up(self): + """Test moving the cursor up.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_UP) + self.mock_win.move.assert_called_with(0, 1) + self.mock_win.reset_mock() + + def test_move_down(self): + """Test moving the cursor down.""" + self.mock_win.reset_mock() + self.textbox.do_command(curses.KEY_DOWN) + self.mock_win.move.assert_called_with(2, 1) + self.mock_win.reset_mock() + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_dbm_gnu.py b/Lib/test/test_dbm_gnu.py new file mode 100644 index 000000000..66268c42a --- /dev/null +++ b/Lib/test/test_dbm_gnu.py @@ -0,0 +1,222 @@ +import os +import unittest +from test import support +from test.support import cpython_only, import_helper +from test.support.os_helper import (TESTFN, TESTFN_NONASCII, FakePath, + create_empty_file, temp_dir, unlink) + +gdbm = import_helper.import_module("dbm.gnu") # skip if not supported + +filename = TESTFN + +class TestGdbm(unittest.TestCase): + @staticmethod + def setUpClass(): + if support.verbose: + try: + from _gdbm import _GDBM_VERSION as version + except ImportError: + pass + else: + print(f"gdbm version: {version}") + + def setUp(self): + self.g = None + + def tearDown(self): + if self.g is not None: + self.g.close() + unlink(filename) + + @cpython_only + def test_disallow_instantiation(self): + # Ensure that the type disallows instantiation (bpo-43916) + self.g = gdbm.open(filename, 'c') + support.check_disallow_instantiation(self, type(self.g)) + + def test_key_methods(self): + self.g = gdbm.open(filename, 'c') + self.assertEqual(self.g.keys(), []) + self.g['a'] = 'b' + self.g['12345678910'] = '019237410982340912840198242' + self.g[b'bytes'] = b'data' + key_set = set(self.g.keys()) + self.assertEqual(key_set, set([b'a', b'bytes', b'12345678910'])) + self.assertIn('a', self.g) + self.assertIn(b'a', self.g) + self.assertEqual(self.g[b'bytes'], b'data') + key = self.g.firstkey() + while key: + self.assertIn(key, key_set) + key_set.remove(key) + key = self.g.nextkey(key) + # get() and setdefault() work as in the dict interface + self.assertEqual(self.g.get(b'a'), b'b') + self.assertIsNone(self.g.get(b'xxx')) + self.assertEqual(self.g.get(b'xxx', b'foo'), b'foo') + with self.assertRaises(KeyError): + self.g['xxx'] + self.assertEqual(self.g.setdefault(b'xxx', b'foo'), b'foo') + self.assertEqual(self.g[b'xxx'], b'foo') + + def test_error_conditions(self): + # Try to open a non-existent database. + unlink(filename) + self.assertRaises(gdbm.error, gdbm.open, filename, 'r') + # Try to access a closed database. + self.g = gdbm.open(filename, 'c') + self.g.close() + self.assertRaises(gdbm.error, lambda: self.g['a']) + # try pass an invalid open flag + self.assertRaises(gdbm.error, lambda: gdbm.open(filename, 'rx').close()) + + def test_flags(self): + # Test the flag parameter open() by trying all supported flag modes. + all = set(gdbm.open_flags) + # Test standard flags (presumably "crwn"). + modes = all - set('fsu') + for mode in sorted(modes): # put "c" mode first + self.g = gdbm.open(filename, mode) + self.g.close() + + # Test additional flags (presumably "fsu"). + flags = all - set('crwn') + for mode in modes: + for flag in flags: + self.g = gdbm.open(filename, mode + flag) + self.g.close() + + def test_reorganize(self): + self.g = gdbm.open(filename, 'c') + size0 = os.path.getsize(filename) + + # bpo-33901: on macOS with gdbm 1.15, an empty database uses 16 MiB + # and adding an entry of 10,000 B has no effect on the file size. + # Add size0 bytes to make sure that the file size changes. + value_size = max(size0, 10000) + self.g['x'] = 'x' * value_size + size1 = os.path.getsize(filename) + self.assertGreater(size1, size0) + + del self.g['x'] + # 'size' is supposed to be the same even after deleting an entry. + self.assertEqual(os.path.getsize(filename), size1) + + self.g.reorganize() + size2 = os.path.getsize(filename) + self.assertLess(size2, size1) + self.assertGreaterEqual(size2, size0) + + def test_context_manager(self): + with gdbm.open(filename, 'c') as db: + db["gdbm context manager"] = "context manager" + + with gdbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), [b"gdbm context manager"]) + + with self.assertRaises(gdbm.error) as cm: + db.keys() + self.assertEqual(str(cm.exception), + "GDBM object has already been closed") + + def test_bool_empty(self): + with gdbm.open(filename, 'c') as db: + self.assertFalse(bool(db)) + + def test_bool_not_empty(self): + with gdbm.open(filename, 'c') as db: + db['a'] = 'b' + self.assertTrue(bool(db)) + + def test_bool_on_closed_db_raises(self): + with gdbm.open(filename, 'c') as db: + db['a'] = 'b' + self.assertRaises(gdbm.error, bool, db) + + def test_bytes(self): + with gdbm.open(filename, 'c') as db: + db[b'bytes key \xbd'] = b'bytes value \xbd' + with gdbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), [b'bytes key \xbd']) + self.assertTrue(b'bytes key \xbd' in db) + self.assertEqual(db[b'bytes key \xbd'], b'bytes value \xbd') + + def test_unicode(self): + with gdbm.open(filename, 'c') as db: + db['Unicode key \U0001f40d'] = 'Unicode value \U0001f40d' + with gdbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), ['Unicode key \U0001f40d'.encode()]) + self.assertTrue('Unicode key \U0001f40d'.encode() in db) + self.assertTrue('Unicode key \U0001f40d' in db) + self.assertEqual(db['Unicode key \U0001f40d'.encode()], + 'Unicode value \U0001f40d'.encode()) + self.assertEqual(db['Unicode key \U0001f40d'], + 'Unicode value \U0001f40d'.encode()) + + def test_write_readonly_file(self): + with gdbm.open(filename, 'c') as db: + db[b'bytes key'] = b'bytes value' + with gdbm.open(filename, 'r') as db: + with self.assertRaises(gdbm.error): + del db[b'not exist key'] + with self.assertRaises(gdbm.error): + del db[b'bytes key'] + with self.assertRaises(gdbm.error): + db[b'not exist key'] = b'not exist value' + + @unittest.skipUnless(TESTFN_NONASCII, + 'requires OS support of non-ASCII encodings') + def test_nonascii_filename(self): + filename = TESTFN_NONASCII + self.addCleanup(unlink, filename) + with gdbm.open(filename, 'c') as db: + db[b'key'] = b'value' + self.assertTrue(os.path.exists(filename)) + with gdbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), [b'key']) + self.assertTrue(b'key' in db) + self.assertEqual(db[b'key'], b'value') + + def test_nonexisting_file(self): + nonexisting_file = 'nonexisting-file' + with self.assertRaises(gdbm.error) as cm: + gdbm.open(nonexisting_file) + self.assertIn(nonexisting_file, str(cm.exception)) + self.assertEqual(cm.exception.filename, nonexisting_file) + + def test_open_with_pathlib_path(self): + gdbm.open(FakePath(filename), "c").close() + + def test_open_with_bytes_path(self): + gdbm.open(os.fsencode(filename), "c").close() + + def test_open_with_pathlib_bytes_path(self): + gdbm.open(FakePath(os.fsencode(filename)), "c").close() + + def test_clear(self): + kvs = [('foo', 'bar'), ('1234', '5678')] + with gdbm.open(filename, 'c') as db: + for k, v in kvs: + db[k] = v + self.assertIn(k, db) + self.assertEqual(len(db), len(kvs)) + + db.clear() + for k, v in kvs: + self.assertNotIn(k, db) + self.assertEqual(len(db), 0) + + @support.run_with_locale( + 'LC_ALL', + 'fr_FR.iso88591', 'ja_JP.sjis', 'zh_CN.gbk', + 'fr_FR.utf8', 'en_US.utf8', + '', + ) + def test_localized_error(self): + with temp_dir() as d: + create_empty_file(os.path.join(d, 'test')) + self.assertRaises(gdbm.error, gdbm.open, filename, 'r') + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_dbm_ndbm.py b/Lib/test/test_dbm_ndbm.py new file mode 100644 index 000000000..e0f31c9a9 --- /dev/null +++ b/Lib/test/test_dbm_ndbm.py @@ -0,0 +1,165 @@ +from test.support import import_helper +from test.support import os_helper +import_helper.import_module("dbm.ndbm") #skip if not supported +import os +import unittest +import dbm.ndbm +from dbm.ndbm import error + +class DbmTestCase(unittest.TestCase): + + def setUp(self): + self.filename = os_helper.TESTFN + self.d = dbm.ndbm.open(self.filename, 'c') + self.d.close() + + def tearDown(self): + for suffix in ['', '.pag', '.dir', '.db']: + os_helper.unlink(self.filename + suffix) + + def test_keys(self): + self.d = dbm.ndbm.open(self.filename, 'c') + self.assertEqual(self.d.keys(), []) + self.d['a'] = 'b' + self.d[b'bytes'] = b'data' + self.d['12345678910'] = '019237410982340912840198242' + self.d.keys() + self.assertIn('a', self.d) + self.assertIn(b'a', self.d) + self.assertEqual(self.d[b'bytes'], b'data') + # get() and setdefault() work as in the dict interface + self.assertEqual(self.d.get(b'a'), b'b') + self.assertIsNone(self.d.get(b'xxx')) + self.assertEqual(self.d.get(b'xxx', b'foo'), b'foo') + with self.assertRaises(KeyError): + self.d['xxx'] + self.assertEqual(self.d.setdefault(b'xxx', b'foo'), b'foo') + self.assertEqual(self.d[b'xxx'], b'foo') + self.d.close() + + def test_empty_value(self): + if dbm.ndbm.library == 'Berkeley DB': + self.skipTest("Berkeley DB doesn't distinguish the empty value " + "from the absent one") + self.d = dbm.ndbm.open(self.filename, 'c') + self.assertEqual(self.d.keys(), []) + self.d['empty'] = '' + self.assertEqual(self.d.keys(), [b'empty']) + self.assertIn(b'empty', self.d) + self.assertEqual(self.d[b'empty'], b'') + self.assertEqual(self.d.get(b'empty'), b'') + self.assertEqual(self.d.setdefault(b'empty'), b'') + self.d.close() + + def test_modes(self): + for mode in ['r', 'rw', 'w', 'n']: + try: + self.d = dbm.ndbm.open(self.filename, mode) + self.d.close() + except error: + self.fail() + + def test_context_manager(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db["ndbm context manager"] = "context manager" + + with dbm.ndbm.open(self.filename, 'r') as db: + self.assertEqual(list(db.keys()), [b"ndbm context manager"]) + + with self.assertRaises(dbm.ndbm.error) as cm: + db.keys() + self.assertEqual(str(cm.exception), + "DBM object has already been closed") + + def test_bytes(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db[b'bytes key \xbd'] = b'bytes value \xbd' + with dbm.ndbm.open(self.filename, 'r') as db: + self.assertEqual(list(db.keys()), [b'bytes key \xbd']) + self.assertTrue(b'bytes key \xbd' in db) + self.assertEqual(db[b'bytes key \xbd'], b'bytes value \xbd') + + def test_unicode(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db['Unicode key \U0001f40d'] = 'Unicode value \U0001f40d' + with dbm.ndbm.open(self.filename, 'r') as db: + self.assertEqual(list(db.keys()), ['Unicode key \U0001f40d'.encode()]) + self.assertTrue('Unicode key \U0001f40d'.encode() in db) + self.assertTrue('Unicode key \U0001f40d' in db) + self.assertEqual(db['Unicode key \U0001f40d'.encode()], + 'Unicode value \U0001f40d'.encode()) + self.assertEqual(db['Unicode key \U0001f40d'], + 'Unicode value \U0001f40d'.encode()) + + def test_write_readonly_file(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db[b'bytes key'] = b'bytes value' + with dbm.ndbm.open(self.filename, 'r') as db: + with self.assertRaises(error): + del db[b'not exist key'] + with self.assertRaises(error): + del db[b'bytes key'] + with self.assertRaises(error): + db[b'not exist key'] = b'not exist value' + + @unittest.skipUnless(os_helper.TESTFN_NONASCII, + 'requires OS support of non-ASCII encodings') + def test_nonascii_filename(self): + filename = os_helper.TESTFN_NONASCII + for suffix in ['', '.pag', '.dir', '.db']: + self.addCleanup(os_helper.unlink, filename + suffix) + with dbm.ndbm.open(filename, 'c') as db: + db[b'key'] = b'value' + self.assertTrue(any(os.path.exists(filename + suffix) + for suffix in ['', '.pag', '.dir', '.db'])) + with dbm.ndbm.open(filename, 'r') as db: + self.assertEqual(list(db.keys()), [b'key']) + self.assertTrue(b'key' in db) + self.assertEqual(db[b'key'], b'value') + + def test_nonexisting_file(self): + nonexisting_file = 'nonexisting-file' + with self.assertRaises(dbm.ndbm.error) as cm: + dbm.ndbm.open(nonexisting_file) + self.assertIn(nonexisting_file, str(cm.exception)) + self.assertEqual(cm.exception.filename, nonexisting_file) + + def test_open_with_pathlib_path(self): + dbm.ndbm.open(os_helper.FakePath(self.filename), "c").close() + + def test_open_with_bytes_path(self): + dbm.ndbm.open(os.fsencode(self.filename), "c").close() + + def test_open_with_pathlib_bytes_path(self): + dbm.ndbm.open(os_helper.FakePath(os.fsencode(self.filename)), "c").close() + + def test_bool_empty(self): + with dbm.ndbm.open(self.filename, 'c') as db: + self.assertFalse(bool(db)) + + def test_bool_not_empty(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db['a'] = 'b' + self.assertTrue(bool(db)) + + def test_bool_on_closed_db_raises(self): + with dbm.ndbm.open(self.filename, 'c') as db: + db['a'] = 'b' + self.assertRaises(dbm.ndbm.error, bool, db) + + def test_clear(self): + kvs = [('foo', 'bar'), ('1234', '5678')] + with dbm.ndbm.open(self.filename, 'c') as db: + for k, v in kvs: + db[k] = v + self.assertIn(k, db) + self.assertEqual(len(db), len(kvs)) + + db.clear() + for k, v in kvs: + self.assertNotIn(k, db) + self.assertEqual(len(db), 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_dtrace.py b/Lib/test/test_dtrace.py index ba2fa9970..41b52662b 100644 --- a/Lib/test/test_dtrace.py +++ b/Lib/test/test_dtrace.py @@ -1,24 +1,57 @@ import dis import os.path import re +import shlex +import signal import subprocess import sys import sysconfig +import tempfile import types import unittest from test import support from test.support import findfile, MS_WINDOWS +from test.support import os_helper if not support.has_subprocess_support: raise unittest.SkipTest("test module requires subprocess") +if not sysconfig.get_config_var('WITH_DTRACE'): + raise unittest.SkipTest( + "CPython must be configured with the --with-dtrace option." + ) def abspath(filename): return os.path.abspath(findfile(filename, subdir="dtracedata")) +def get_probe_binary(): + binary = sys.executable + if sysconfig.get_config_var("Py_ENABLE_SHARED"): + lib_dir = sysconfig.get_config_var("LIBDIR") + if not lib_dir or sysconfig.is_python_build(): + lib_dir = os.path.abspath(os.path.dirname(sys.executable)) + + lib_names = [] + for name in ( + sysconfig.get_config_var("INSTSONAME"), + sysconfig.get_config_var("LDLIBRARY"), + ): + if name and name not in lib_names: + lib_names.append(name) + + if lib_dir: + for name in lib_names: + libpython_path = os.path.join(lib_dir, name) + if os.path.exists(libpython_path): + binary = libpython_path + break + + return binary + + def normalize_trace_output(output): """Normalize DTrace output for comparison. @@ -33,27 +66,93 @@ def normalize_trace_output(output): result = [ row.split("\t") for row in output.splitlines() - if row and not row.startswith('#') + if row and not row.startswith('#') and not row.startswith('@') ] result.sort(key=lambda row: int(row[0])) result = [row[1] for row in result] - return "\n".join(result) + # Normalize paths to basenames (bpftrace outputs full paths) + normalized = [] + for line in result: + # Replace full paths with just the filename + line = re.sub(r'/[^:]+/([^/:]+\.py)', r'\1', line) + normalized.append(line) + return "\n".join(normalized) except (IndexError, ValueError): raise AssertionError( "tracer produced unparsable output:\n{}".format(output) ) +USE_PROCESS_GROUP = (hasattr(os, "setsid") and hasattr(os, "killpg")) +TERMINATE_TIMEOUT = 10 + +def create_process_group(*args, **kwargs): + if USE_PROCESS_GROUP: + kwargs['start_new_session'] = True + return subprocess.Popen(*args, **kwargs) + +def terminate_process_group(proc): + if USE_PROCESS_GROUP: + try: + os.killpg(proc.pid, signal.SIGTERM) + except ProcessLookupError: + pass + else: + proc.terminate() + + try: + proc.communicate(timeout=TERMINATE_TIMEOUT) + except subprocess.TimeoutExpired: + if USE_PROCESS_GROUP: + try: + os.killpg(proc.pid, signal.SIGKILL) + except ProcessLookupError: + pass + else: + proc.kill() + proc.communicate(timeout=TERMINATE_TIMEOUT) # Clean up + + +def run_readelf(cmd): + # Force the C locale to disable localization. + env = dict(os.environ, LC_ALL="C") + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + ) + except OSError: + raise unittest.SkipTest("Couldn't find readelf on the path") + + with proc: + stdout, stderr = proc.communicate() + + if proc.returncode: + raise AssertionError( + f"Command {shlex.join(cmd)!r} failed " + f"with exit code {proc.returncode}: " + f"stdout={stdout!r} stderr={stderr!r}" + ) + + return stdout + + class TraceBackend: EXTENSION = None COMMAND = None COMMAND_ARGS = [] def run_case(self, name, optimize_python=None): - actual_output = normalize_trace_output(self.trace_python( - script_file=abspath(name + self.EXTENSION), - python_file=abspath(name + ".py"), - optimize_python=optimize_python)) + try: + actual_output = normalize_trace_output(self.trace_python( + script_file=abspath(name + self.EXTENSION), + python_file=abspath(name + ".py"), + optimize_python=optimize_python)) + except subprocess.TimeoutExpired: + raise AssertionError(f"{self.COMMAND[0]} timed out") with open(abspath(name + self.EXTENSION + ".expected")) as f: expected_output = f.read().rstrip() @@ -66,25 +165,42 @@ class TraceBackend: command += ["-c", subcommand] return command - def trace(self, script_file, subcommand=None): + def trace(self, script_file, subcommand=None, *, timeout=None, + check_returncode=False): command = self.generate_trace_command(script_file, subcommand) - stdout, _ = subprocess.Popen(command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True).communicate() + proc = create_process_group(command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True) + try: + stdout, _ = proc.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + terminate_process_group(proc) + raise + if check_returncode and proc.returncode: + raise AssertionError( + f"Command {shlex.join(command)!r} failed " + f"with exit code {proc.returncode}: output={stdout!r}" + ) return stdout def trace_python(self, script_file, python_file, optimize_python=None): python_flags = [] if optimize_python: python_flags.extend(["-O"] * optimize_python) - subcommand = " ".join([sys.executable] + python_flags + [python_file]) - return self.trace(script_file, subcommand) + subcommand = shlex.join([sys.executable] + python_flags + [python_file]) + return self.trace(script_file, subcommand, timeout=60, + check_returncode=True) def assert_usable(self): try: - output = self.trace(abspath("assert_usable" + self.EXTENSION)) + output = self.trace(abspath("assert_usable" + self.EXTENSION), + timeout=10) output = output.strip() + except subprocess.TimeoutExpired: + raise unittest.SkipTest( + f"{self.COMMAND[0]} timed out during usability check" + ) except (FileNotFoundError, NotADirectoryError, PermissionError) as fnfe: output = str(fnfe) if output != "probe: success": @@ -96,11 +212,223 @@ class TraceBackend: class DTraceBackend(TraceBackend): EXTENSION = ".d" COMMAND = ["dtrace", "-q", "-s"] + if sys.platform == "sunos5": + COMMAND.insert(2, "-Z") class SystemTapBackend(TraceBackend): EXTENSION = ".stp" COMMAND = ["stap", "-g"] + PROBE_PLACEHOLDER = "@PYTHON_SYSTEMTAP_PROBE@" + + @staticmethod + def quote_systemtap_string(value): + return value.replace("\\", "\\\\").replace('"', '\\"') + + def python_probe(self): + executable = self.quote_systemtap_string(sys.executable) + probe_binary = get_probe_binary() + if probe_binary == sys.executable: + return f'process("{executable}").mark' + + # Python built with --enable-shared + probe_binary = self.quote_systemtap_string(probe_binary) + return f'process("{executable}").library("{probe_binary}").mark' + + def render_script(self, filename): + with open(filename) as fp: + script = fp.read() + + return script.replace(self.PROBE_PLACEHOLDER, self.python_probe()) + + def trace(self, script_file, subcommand=None, *, timeout=None, + check_returncode=False): + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", suffix=self.EXTENSION, delete=False + ) as script: + script.write(self.render_script(script_file)) + generated_script_file = script.name + + try: + return super().trace( + generated_script_file, + subcommand, + timeout=timeout, + check_returncode=check_returncode, + ) + finally: + os_helper.unlink(generated_script_file) + + +class BPFTraceBackend(TraceBackend): + EXTENSION = ".bt" + COMMAND = ["bpftrace"] + + # Inline bpftrace programs for each test case + PROGRAMS = { + "call_stack": """ + usdt:{python}:python:function__entry {{ + printf("%lld\\tfunction__entry:%s:%s:%d\\n", + nsecs, str(arg0), str(arg1), arg2); + }} + usdt:{python}:python:function__return {{ + printf("%lld\\tfunction__return:%s:%s:%d\\n", + nsecs, str(arg0), str(arg1), arg2); + }} + """, + "gc": """ + usdt:{python}:python:function__entry {{ + if (str(arg1) == "start") {{ @tracing = 1; }} + }} + usdt:{python}:python:function__return {{ + if (str(arg1) == "start") {{ @tracing = 0; }} + }} + usdt:{python}:python:gc__start {{ + if (@tracing) {{ + printf("%lld\\tgc__start:%d\\n", nsecs, arg0); + }} + }} + usdt:{python}:python:gc__done {{ + if (@tracing) {{ + printf("%lld\\tgc__done:%lld\\n", nsecs, arg0); + }} + }} + END {{ clear(@tracing); }} + """, + } + + # Which test scripts to filter by filename (None = use @tracing flag) + FILTER_BY_FILENAME = {"call_stack": "call_stack.py"} + + @staticmethod + def _filter_probe_rows(output): + return "\n".join( + line for line in output.splitlines() + if line.partition("\t")[0].isdigit() + ) + + # Expected outputs for each test case + # Note: bpftrace captures entry/return and may have slight timing + # differences compared to SystemTap due to probe firing order + EXPECTED = { + "call_stack": """function__entry:call_stack.py::0 +function__entry:call_stack.py:start:23 +function__entry:call_stack.py:function_1:1 +function__entry:call_stack.py:function_3:9 +function__return:call_stack.py:function_3:10 +function__return:call_stack.py:function_1:2 +function__entry:call_stack.py:function_2:5 +function__entry:call_stack.py:function_1:1 +function__entry:call_stack.py:function_3:9 +function__return:call_stack.py:function_3:10 +function__return:call_stack.py:function_1:2 +function__return:call_stack.py:function_2:6 +function__entry:call_stack.py:function_3:9 +function__return:call_stack.py:function_3:10 +function__entry:call_stack.py:function_4:13 +function__return:call_stack.py:function_4:14 +function__entry:call_stack.py:function_5:18 +function__return:call_stack.py:function_5:21 +function__return:call_stack.py:start:28 +function__return:call_stack.py::30""", + "gc": """gc__start:0 +gc__done:0 +gc__start:1 +gc__done:0 +gc__start:2 +gc__done:0 +gc__start:2 +gc__done:1""", + } + + def run_case(self, name, optimize_python=None): + if name not in self.PROGRAMS: + raise unittest.SkipTest(f"No bpftrace program for {name}") + + python_file = abspath(name + ".py") + python_flags = [] + if optimize_python: + python_flags.extend(["-O"] * optimize_python) + + subcommand = [sys.executable] + python_flags + [python_file] + program = self.PROGRAMS[name].format(python=get_probe_binary()) + + try: + proc = create_process_group( + ["bpftrace", "-e", program, "-c", shlex.join(subcommand)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = proc.communicate(timeout=60) + except subprocess.TimeoutExpired: + terminate_process_group(proc) + raise AssertionError("bpftrace timed out") + except (FileNotFoundError, PermissionError) as e: + raise unittest.SkipTest(f"bpftrace not available: {e}") + + if proc.returncode != 0: + raise AssertionError( + f"bpftrace failed with code {proc.returncode}:\n{stderr}" + ) + + stdout = self._filter_probe_rows(stdout) + + # Filter output by filename if specified (bpftrace captures everything) + if name in self.FILTER_BY_FILENAME: + filter_filename = self.FILTER_BY_FILENAME[name] + filtered_lines = [ + line for line in stdout.splitlines() + if filter_filename in line + ] + stdout = "\n".join(filtered_lines) + + actual_output = normalize_trace_output(stdout) + expected_output = self.EXPECTED[name].strip() + + return (expected_output, actual_output) + + def assert_usable(self): + # Check if bpftrace is available and can attach to USDT probes + program = f'usdt:{get_probe_binary()}:python:function__entry {{ printf("probe: success\\n"); exit(); }}' + try: + proc = create_process_group( + ["bpftrace", "-e", program, "-c", + shlex.join([sys.executable, "-c", "pass"])], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = proc.communicate(timeout=10) + except subprocess.TimeoutExpired: + terminate_process_group(proc) + raise unittest.SkipTest("bpftrace timed out during usability check") + except OSError as e: + raise unittest.SkipTest(f"bpftrace not available: {e}") + + # Check for permission errors (bpftrace usually requires root) + if proc.returncode != 0: + raise unittest.SkipTest( + f"bpftrace(1) failed with code {proc.returncode}: {stderr}" + ) + + if "probe: success" not in stdout: + raise unittest.SkipTest( + f"bpftrace(1) failed: stdout={stdout!r} stderr={stderr!r}" + ) + + +class BPFTraceOutputTests(unittest.TestCase): + def test_filter_probe_rows_ignores_warnings(self): + output = """stdin:1-19: WARNING: found external warnings +HINT: include/vmlinux.h:1439:3: warning: declaration does not declare anything +4623214882928\tgc__start:0 +4623214885575\tgc__done:0 +""" + self.assertEqual( + BPFTraceBackend._filter_probe_rows(output), + "4623214882928\tgc__start:0\n4623214885575\tgc__done:0", + ) @unittest.skipIf(MS_WINDOWS, "Tests not compliant with trace on Windows.") @@ -127,7 +455,8 @@ class TraceTests: def test_verify_call_opcodes(self): """Ensure our call stack test hits all function call opcodes""" - opcodes = set(["CALL_FUNCTION", "CALL_FUNCTION_EX", "CALL_FUNCTION_KW"]) + # Modern Python uses CALL, CALL_KW, and CALL_FUNCTION_EX + opcodes = set(["CALL", "CALL_FUNCTION_EX", "CALL_KW"]) with open(abspath("call_stack.py")) as f: code_string = f.read() @@ -152,9 +481,6 @@ class TraceTests: def test_gc(self): self.run_case("gc") - def test_line(self): - self.run_case("line") - class DTraceNormalTests(TraceTests, unittest.TestCase): backend = DTraceBackend() @@ -175,38 +501,28 @@ class SystemTapOptimizedTests(TraceTests, unittest.TestCase): backend = SystemTapBackend() optimize_python = 2 + +class BPFTraceNormalTests(TraceTests, unittest.TestCase): + backend = BPFTraceBackend() + optimize_python = 0 + + +class BPFTraceOptimizedTests(TraceTests, unittest.TestCase): + backend = BPFTraceBackend() + optimize_python = 2 + + class CheckDtraceProbes(unittest.TestCase): @classmethod def setUpClass(cls): - if sysconfig.get_config_var('WITH_DTRACE'): - readelf_major_version, readelf_minor_version = cls.get_readelf_version() - if support.verbose: - print(f"readelf version: {readelf_major_version}.{readelf_minor_version}") - else: - raise unittest.SkipTest("CPython must be configured with the --with-dtrace option.") + readelf_major_version, readelf_minor_version = cls.get_readelf_version() + if support.verbose: + print(f"readelf version: {readelf_major_version}.{readelf_minor_version}") @staticmethod def get_readelf_version(): - try: - cmd = ["readelf", "--version"] - proc = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - ) - with proc: - version, stderr = proc.communicate() - - if proc.returncode: - raise Exception( - f"Command {' '.join(cmd)!r} failed " - f"with exit code {proc.returncode}: " - f"stdout={version!r} stderr={stderr!r}" - ) - except OSError: - raise unittest.SkipTest("Couldn't find readelf on the path") + version = run_readelf(["readelf", "--version"]) # Regex to parse: # 'GNU readelf (GNU Binutils) 2.40.0\n' -> 2.40 @@ -217,14 +533,7 @@ class CheckDtraceProbes(unittest.TestCase): return int(match.group(1)), int(match.group(2)) def get_readelf_output(self): - command = ["readelf", "-n", sys.executable] - stdout, _ = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - ).communicate() - return stdout + return run_readelf(["readelf", "-n", get_probe_binary()]) def test_check_probes(self): readelf_output = self.get_readelf_output() @@ -235,6 +544,8 @@ class CheckDtraceProbes(unittest.TestCase): "Name: audit", "Name: gc__start", "Name: gc__done", + "Name: function__entry", + "Name: function__return", ] for probe_name in available_probe_names: @@ -247,8 +558,6 @@ class CheckDtraceProbes(unittest.TestCase): # Missing probes will be added in the future. missing_probe_names = [ - "Name: function__entry", - "Name: function__return", "Name: line", ] diff --git a/Lib/test/test_tcl.py b/Lib/test/test_tcl.py new file mode 100644 index 000000000..f6e49840d --- /dev/null +++ b/Lib/test/test_tcl.py @@ -0,0 +1,838 @@ +import unittest +import subprocess +import sys +import os +from test import support +from test.support import import_helper +from test.support import os_helper + +# Skip this test if the _tkinter module wasn't built. +_tkinter = import_helper.import_module('_tkinter') + +import tkinter +from tkinter import Tcl +from _tkinter import TclError + +try: + from _testcapi import INT_MAX, PY_SSIZE_T_MAX +except ImportError: + INT_MAX = PY_SSIZE_T_MAX = sys.maxsize + +tcl_version = tuple(map(int, _tkinter.TCL_VERSION.split('.'))) + + +class TkinterTest(unittest.TestCase): + + def testFlattenLen(self): + # Object without length. + self.assertRaises(TypeError, _tkinter._flatten, True) + # Object with length, but not sequence. + self.assertRaises(TypeError, _tkinter._flatten, {}) + # Sequence or set, but not tuple or list. + # (issue44608: there were leaks in the following cases) + self.assertRaises(TypeError, _tkinter._flatten, 'string') + self.assertRaises(TypeError, _tkinter._flatten, {'set'}) + + +class TclTest(unittest.TestCase): + + def setUp(self): + self.interp = Tcl() + self.wantobjects = self.interp.tk.wantobjects() + + def passValue(self, value): + return self.interp.call('set', '_', value) + + def testEval(self): + tcl = self.interp + tcl.eval('set a 1') + self.assertEqual(tcl.eval('set a'),'1') + + def test_eval_null_in_result(self): + tcl = self.interp + self.assertEqual(tcl.eval('set a "a\\0b"'), 'a\x00b') + + def test_eval_surrogates_in_result(self): + tcl = self.interp + result = tcl.eval(r'set a "<\ud83d\udcbb>"') + if sys.platform == 'win32' and tcl_version >= (9, 0): + self.assertEqual('<\ud83d\udcbb>', result) + else: + self.assertEqual('<\U0001f4bb>', result) + + def testEvalException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.eval,'set a') + + def testEvalException2(self): + tcl = self.interp + self.assertRaises(TclError,tcl.eval,'this is wrong') + + def test_eval_returns_tcl_obj(self): + tcl = self.interp.tk + tcl.eval(r'set a "\u20ac \ud83d\udcbb \0 \udcab"; regexp -about $a') + a = tcl.eval('set a') + expected = '\u20ac \U0001f4bb \0 \udced\udcb2\udcab' + self.assertEqual(a, expected) + + def testCall(self): + tcl = self.interp + tcl.call('set','a','1') + self.assertEqual(tcl.call('set','a'),'1') + + def test_call_passing_null(self): + tcl = self.interp + tcl.call('set', 'a', 'a\0b') # ASCII-only + self.assertEqual(tcl.getvar('a'), 'a\x00b') + self.assertEqual(tcl.call('set', 'a'), 'a\x00b') + self.assertEqual(tcl.eval('set a'), 'a\x00b') + + tcl.call('set', 'a', '\u20ac\0') # non-ASCII + self.assertEqual(tcl.getvar('a'), '\u20ac\x00') + self.assertEqual(tcl.call('set', 'a'), '\u20ac\x00') + self.assertEqual(tcl.eval('set a'), '\u20ac\x00') + + def testCallException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.call,'set','a') + + def testCallException2(self): + tcl = self.interp + self.assertRaises(TclError,tcl.call,'this','is','wrong') + + def test_call_returns_tcl_obj(self): + tcl = self.interp.tk + tcl.eval(r'set a "\u20ac \ud83d\udcbb \0 \udcab"; regexp -about $a') + a = tcl.call('set', 'a') + expected = '\u20ac \U0001f4bb \0 \udced\udcb2\udcab' + if self.wantobjects: + self.assertEqual(str(a), expected) + self.assertEqual(a.string, expected) + self.assertEqual(a.typename, 'regexp') + else: + self.assertEqual(a, expected) + + def testSetVar(self): + tcl = self.interp + tcl.setvar('a','1') + self.assertEqual(tcl.eval('set a'),'1') + + def test_setvar_passing_null(self): + tcl = self.interp + tcl.setvar('a', 'a\0b') # ASCII-only + self.assertEqual(tcl.getvar('a'), 'a\x00b') + self.assertEqual(tcl.call('set', 'a'), 'a\x00b') + self.assertEqual(tcl.eval('set a'), 'a\x00b') + + tcl.setvar('a', '\u20ac\0') # non-ASCII + self.assertEqual(tcl.getvar('a'), '\u20ac\x00') + self.assertEqual(tcl.call('set', 'a'), '\u20ac\x00') + self.assertEqual(tcl.eval('set a'), '\u20ac\x00') + + def testSetVarArray(self): + tcl = self.interp + tcl.setvar('a(1)','1') + self.assertEqual(tcl.eval('set a(1)'),'1') + + def testGetVar(self): + tcl = self.interp + tcl.eval('set a 1') + self.assertEqual(tcl.getvar('a'),'1') + + def testGetVarArray(self): + tcl = self.interp + tcl.eval('set a(1) 1') + self.assertEqual(tcl.getvar('a(1)'),'1') + + def testGetVarException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.getvar,'a') + + def testGetVarArrayException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.getvar,'a(1)') + + def test_getvar_returns_tcl_obj(self): + tcl = self.interp.tk + tcl.eval(r'set a "\u20ac \ud83d\udcbb \0 \udcab"; regexp -about $a') + a = tcl.getvar('a') + expected = '\u20ac \U0001f4bb \0 \udced\udcb2\udcab' + if self.wantobjects: + self.assertEqual(str(a), expected) + self.assertEqual(a.string, expected) + self.assertEqual(a.typename, 'regexp') + else: + self.assertEqual(a, expected) + + def testUnsetVar(self): + tcl = self.interp + tcl.setvar('a',1) + self.assertEqual(tcl.eval('info exists a'),'1') + tcl.unsetvar('a') + self.assertEqual(tcl.eval('info exists a'),'0') + + def testUnsetVarArray(self): + tcl = self.interp + tcl.setvar('a(1)',1) + tcl.setvar('a(2)',2) + self.assertEqual(tcl.eval('info exists a(1)'),'1') + self.assertEqual(tcl.eval('info exists a(2)'),'1') + tcl.unsetvar('a(1)') + self.assertEqual(tcl.eval('info exists a(1)'),'0') + self.assertEqual(tcl.eval('info exists a(2)'),'1') + + def testUnsetVarException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.unsetvar,'a') + + def get_integers(self): + return (0, 1, -1, + 2**31-1, -2**31, 2**31, -2**31-1, + 2**63-1, -2**63, 2**63, -2**63-1, + 2**1000, -2**1000) + + def test_getint(self): + tcl = self.interp.tk + for i in self.get_integers(): + self.assertEqual(tcl.getint(' %d ' % i), i) + self.assertEqual(tcl.getint(' %#o ' % i), i) + # Numbers starting with 0 are parsed as decimal in Tcl 9.0 + # and as octal in older versions. + self.assertEqual(tcl.getint((' %#o ' % i).replace('o', '')), + i if tcl_version < (9, 0) else int('%o' % i)) + self.assertEqual(tcl.getint(' %#x ' % i), i) + self.assertEqual(tcl.getint(42), 42) + self.assertRaises(TypeError, tcl.getint) + self.assertRaises(TypeError, tcl.getint, '42', '10') + self.assertRaises(TypeError, tcl.getint, b'42') + self.assertRaises(TypeError, tcl.getint, 42.0) + self.assertRaises(TclError, tcl.getint, 'a') + self.assertRaises((TypeError, ValueError, TclError), + tcl.getint, '42\0') + self.assertRaises((UnicodeEncodeError, ValueError, TclError), + tcl.getint, '42\ud800') + + def test_getdouble(self): + tcl = self.interp.tk + self.assertEqual(tcl.getdouble(' 42 '), 42.0) + self.assertEqual(tcl.getdouble(' 42.5 '), 42.5) + self.assertEqual(tcl.getdouble(42.5), 42.5) + self.assertEqual(tcl.getdouble(42), 42.0) + self.assertRaises(TypeError, tcl.getdouble) + self.assertRaises(TypeError, tcl.getdouble, '42.5', '10') + self.assertRaises(TypeError, tcl.getdouble, b'42.5') + self.assertRaises(TclError, tcl.getdouble, 'a') + self.assertRaises((TypeError, ValueError, TclError), + tcl.getdouble, '42.5\0') + self.assertRaises((UnicodeEncodeError, ValueError, TclError), + tcl.getdouble, '42.5\ud800') + + def test_getboolean(self): + tcl = self.interp.tk + self.assertIs(tcl.getboolean('on'), True) + self.assertIs(tcl.getboolean('1'), True) + self.assertIs(tcl.getboolean(42), True) + self.assertIs(tcl.getboolean(0), False) + self.assertRaises(TypeError, tcl.getboolean) + self.assertRaises(TypeError, tcl.getboolean, 'on', '1') + self.assertRaises(TypeError, tcl.getboolean, b'on') + self.assertRaises(TypeError, tcl.getboolean, 1.0) + self.assertRaises(TclError, tcl.getboolean, 'a') + self.assertRaises((TypeError, ValueError, TclError), + tcl.getboolean, 'on\0') + self.assertRaises((UnicodeEncodeError, ValueError, TclError), + tcl.getboolean, 'on\ud800') + + def testEvalFile(self): + tcl = self.interp + filename = os_helper.TESTFN_ASCII + self.addCleanup(os_helper.unlink, filename) + with open(filename, 'w') as f: + f.write("""set a 1 + set b 2 + set c [ expr $a + $b ] + """) + tcl.evalfile(filename) + self.assertEqual(tcl.eval('set a'),'1') + self.assertEqual(tcl.eval('set b'),'2') + self.assertEqual(tcl.eval('set c'),'3') + + def test_evalfile_null_in_result(self): + tcl = self.interp + filename = os_helper.TESTFN_ASCII + self.addCleanup(os_helper.unlink, filename) + with open(filename, 'w') as f: + f.write(""" + set a "a\0b" + set b "a\\0b" + """) + tcl.evalfile(filename) + self.assertEqual(tcl.eval('set a'), 'a\x00b') + self.assertEqual(tcl.eval('set b'), 'a\x00b') + + def test_evalfile_surrogates_in_result(self): + tcl = self.interp + encoding = tcl.call('encoding', 'system') + self.addCleanup(tcl.call, 'encoding', 'system', encoding) + tcl.call('encoding', 'system', 'utf-8') + + filename = os_helper.TESTFN_ASCII + self.addCleanup(os_helper.unlink, filename) + with open(filename, 'wb') as f: + f.write(b""" + set a "<\xed\xa0\xbd\xed\xb2\xbb>" + """) + if tcl_version >= (9, 0): + self.assertRaises(TclError, tcl.evalfile, filename) + else: + tcl.evalfile(filename) + self.assertEqual(tcl.eval('set a'), '<\U0001f4bb>') + + with open(filename, 'wb') as f: + f.write(b""" + set b "<\\ud83d\\udcbb>" + """) + tcl.evalfile(filename) + result = tcl.eval('set b') + if sys.platform == 'win32' and tcl_version >= (9, 0): + self.assertEqual('<\ud83d\udcbb>', result) + else: + self.assertEqual('<\U0001f4bb>', result) + + def testEvalFileException(self): + tcl = self.interp + filename = "doesnotexists" + try: + os.remove(filename) + except Exception as e: + pass + self.assertRaises(TclError,tcl.evalfile,filename) + + def testPackageRequireException(self): + tcl = self.interp + self.assertRaises(TclError,tcl.eval,'package require DNE') + + @unittest.skipUnless(sys.platform == 'win32', 'Requires Windows') + def testLoadWithUNC(self): + # Build a UNC path from the regular path. + # Something like + # \\%COMPUTERNAME%\c$\python27\python.exe + + fullname = os.path.abspath(sys.executable) + if fullname[1] != ':': + raise unittest.SkipTest('Absolute path should have drive part') + unc_name = r'\\%s\%s$\%s' % (os.environ['COMPUTERNAME'], + fullname[0], + fullname[3:]) + if not os.path.exists(unc_name): + raise unittest.SkipTest('Cannot connect to UNC Path') + + with os_helper.EnvironmentVarGuard() as env: + env.unset("TCL_LIBRARY") + stdout = subprocess.check_output( + [unc_name, '-c', 'import tkinter; print(tkinter)']) + + self.assertIn(b'tkinter', stdout) + + def test_exprstring(self): + tcl = self.interp + tcl.call('set', 'a', 3) + tcl.call('set', 'b', 6) + def check(expr, expected): + result = tcl.exprstring(expr) + self.assertEqual(result, expected) + self.assertIsInstance(result, str) + + self.assertRaises(TypeError, tcl.exprstring) + self.assertRaises(TypeError, tcl.exprstring, '8.2', '+6') + self.assertRaises(TypeError, tcl.exprstring, b'8.2 + 6') + self.assertRaises(TclError, tcl.exprstring, 'spam') + check('', '0') + check('8.2 + 6', '14.2') + check('3.1 + $a', '6.1') + check('2 + "$a.$b"', '5.6') + check('4*[llength "6 2"]', '8') + check('{word one} < "word $a"', '0') + check('4*2 < 7', '0') + check('hypot($a, 4)', '5.0') + check('5 / 4', '1') + check('5 / 4.0', '1.25') + check('5 / ( [string length "abcd"] + 0.0 )', '1.25') + check('20.0/5.0', '4.0') + check('"0x03" > "2"', '1') + check('[string length "a\xbd\u20ac"]', '3') + check(r'[string length "a\xbd\u20ac"]', '3') + check('"abc"', 'abc') + check('"a\xbd\u20ac"', 'a\xbd\u20ac') + check(r'"a\xbd\u20ac"', 'a\xbd\u20ac') + check(r'"a\0b"', 'a\x00b') + check('2**64', str(2**64)) + + def test_exprdouble(self): + tcl = self.interp + tcl.call('set', 'a', 3) + tcl.call('set', 'b', 6) + def check(expr, expected): + result = tcl.exprdouble(expr) + self.assertEqual(result, expected) + self.assertIsInstance(result, float) + + self.assertRaises(TypeError, tcl.exprdouble) + self.assertRaises(TypeError, tcl.exprdouble, '8.2', '+6') + self.assertRaises(TypeError, tcl.exprdouble, b'8.2 + 6') + self.assertRaises(TclError, tcl.exprdouble, 'spam') + check('', 0.0) + check('8.2 + 6', 14.2) + check('3.1 + $a', 6.1) + check('2 + "$a.$b"', 5.6) + check('4*[llength "6 2"]', 8.0) + check('{word one} < "word $a"', 0.0) + check('4*2 < 7', 0.0) + check('hypot($a, 4)', 5.0) + check('5 / 4', 1.0) + check('5 / 4.0', 1.25) + check('5 / ( [string length "abcd"] + 0.0 )', 1.25) + check('20.0/5.0', 4.0) + check('"0x03" > "2"', 1.0) + check('[string length "a\xbd\u20ac"]', 3.0) + check(r'[string length "a\xbd\u20ac"]', 3.0) + self.assertRaises(TclError, tcl.exprdouble, '"abc"') + check('2**64', float(2**64)) + + def test_exprlong(self): + tcl = self.interp + tcl.call('set', 'a', 3) + tcl.call('set', 'b', 6) + def check(expr, expected): + result = tcl.exprlong(expr) + self.assertEqual(result, expected) + self.assertIsInstance(result, int) + + self.assertRaises(TypeError, tcl.exprlong) + self.assertRaises(TypeError, tcl.exprlong, '8.2', '+6') + self.assertRaises(TypeError, tcl.exprlong, b'8.2 + 6') + self.assertRaises(TclError, tcl.exprlong, 'spam') + check('', 0) + check('8.2 + 6', 14) + check('3.1 + $a', 6) + check('2 + "$a.$b"', 5) + check('4*[llength "6 2"]', 8) + check('{word one} < "word $a"', 0) + check('4*2 < 7', 0) + check('hypot($a, 4)', 5) + check('5 / 4', 1) + check('5 / 4.0', 1) + check('5 / ( [string length "abcd"] + 0.0 )', 1) + check('20.0/5.0', 4) + check('"0x03" > "2"', 1) + check('[string length "a\xbd\u20ac"]', 3) + check(r'[string length "a\xbd\u20ac"]', 3) + self.assertRaises(TclError, tcl.exprlong, '"abc"') + self.assertRaises(TclError, tcl.exprlong, '2**64') + + def test_exprboolean(self): + tcl = self.interp + tcl.call('set', 'a', 3) + tcl.call('set', 'b', 6) + def check(expr, expected): + result = tcl.exprboolean(expr) + self.assertEqual(result, expected) + self.assertIsInstance(result, int) + self.assertNotIsInstance(result, bool) + + self.assertRaises(TypeError, tcl.exprboolean) + self.assertRaises(TypeError, tcl.exprboolean, '8.2', '+6') + self.assertRaises(TypeError, tcl.exprboolean, b'8.2 + 6') + self.assertRaises(TclError, tcl.exprboolean, 'spam') + check('', False) + for value in ('0', 'false', 'no', 'off'): + check(value, False) + check('"%s"' % value, False) + check('{%s}' % value, False) + for value in ('1', 'true', 'yes', 'on'): + check(value, True) + check('"%s"' % value, True) + check('{%s}' % value, True) + check('8.2 + 6', True) + check('3.1 + $a', True) + check('2 + "$a.$b"', True) + check('4*[llength "6 2"]', True) + check('{word one} < "word $a"', False) + check('4*2 < 7', False) + check('hypot($a, 4)', True) + check('5 / 4', True) + check('5 / 4.0', True) + check('5 / ( [string length "abcd"] + 0.0 )', True) + check('20.0/5.0', True) + check('"0x03" > "2"', True) + check('[string length "a\xbd\u20ac"]', True) + check(r'[string length "a\xbd\u20ac"]', True) + self.assertRaises(TclError, tcl.exprboolean, '"abc"') + check('2**64', True) + + def test_booleans(self): + tcl = self.interp + def check(expr, expected): + result = tcl.call('expr', expr) + if tcl.wantobjects(): + self.assertEqual(result, expected) + self.assertIsInstance(result, int) + else: + self.assertIn(result, (expr, str(int(expected)))) + self.assertIsInstance(result, str) + check('true', True) + check('yes', True) + check('on', True) + check('false', False) + check('no', False) + check('off', False) + check('1 < 2', True) + check('1 > 2', False) + + def test_expr_bignum(self): + tcl = self.interp + for i in self.get_integers(): + result = tcl.call('expr', str(i)) + if self.wantobjects: + self.assertEqual(result, i) + self.assertIsInstance(result, int) + else: + self.assertEqual(result, str(i)) + self.assertIsInstance(result, str) + + def test_passing_values(self): + passValue = self.passValue + + self.assertEqual(passValue(True), True if self.wantobjects else '1') + self.assertEqual(passValue(False), False if self.wantobjects else '0') + self.assertEqual(passValue('string'), 'string') + self.assertEqual(passValue('string\u20ac'), 'string\u20ac') + self.assertEqual(passValue('string\U0001f4bb'), 'string\U0001f4bb') + self.assertEqual(passValue('str\x00ing'), 'str\x00ing') + self.assertEqual(passValue('str\x00ing\xbd'), 'str\x00ing\xbd') + self.assertEqual(passValue('str\x00ing\u20ac'), 'str\x00ing\u20ac') + self.assertEqual(passValue('str\x00ing\U0001f4bb'), + 'str\x00ing\U0001f4bb') + if sys.platform != 'win32': + self.assertEqual(passValue('<\udce2\udc82\udcac>'), + '<\u20ac>') + self.assertEqual(passValue('<\udced\udca0\udcbd\udced\udcb2\udcbb>'), + '<\U0001f4bb>') + self.assertEqual(passValue(b'str\x00ing'), + b'str\x00ing' if self.wantobjects else 'str\x00ing') + self.assertEqual(passValue(b'str\xc0\x80ing'), + b'str\xc0\x80ing' if self.wantobjects else 'str\xc0\x80ing') + self.assertEqual(passValue(b'str\xbding'), + b'str\xbding' if self.wantobjects else 'str\xbding') + for i in self.get_integers(): + self.assertEqual(passValue(i), i if self.wantobjects else str(i)) + for f in (0.0, 1.0, -1.0, 1/3, + sys.float_info.min, sys.float_info.max, + -sys.float_info.min, -sys.float_info.max): + if self.wantobjects: + self.assertEqual(passValue(f), f) + else: + self.assertEqual(float(passValue(f)), f) + if self.wantobjects: + f = passValue(float('nan')) + self.assertNotEqual(f, f) + self.assertEqual(passValue(float('inf')), float('inf')) + self.assertEqual(passValue(-float('inf')), -float('inf')) + else: + self.assertEqual(float(passValue(float('inf'))), float('inf')) + self.assertEqual(float(passValue(-float('inf'))), -float('inf')) + # XXX NaN representation can be not parsable by float() + self.assertEqual(passValue((1, '2', (3.4,))), + (1, '2', (3.4,)) if self.wantobjects else '1 2 3.4') + self.assertEqual(passValue(['a', ['b', 'c']]), + ('a', ('b', 'c')) if self.wantobjects else 'a {b c}') + + def test_set_object_concurrent_mutation_in_sequence_conversion(self): + # Prevent SIGSEV when the object to convert is concurrently mutated. + # See: https://github.com/python/cpython/issues/143310. + + string = "value" + + class Value: + def __str__(self): + values.clear() + return string + + class List(list): + pass + + expect = (string, "pad") if self.wantobjects else f"{string} pad" + self.assertEqual(self.passValue(values := [Value(), "pad"]), expect) + self.assertEqual(self.passValue(values := List([Value(), "pad"])), expect) + + def test_user_command(self): + result = None + def testfunc(arg): + nonlocal result + result = arg + return arg + self.interp.createcommand('testfunc', testfunc) + self.addCleanup(self.interp.tk.deletecommand, 'testfunc') + def check(value, expected1=None, expected2=None, *, eq=self.assertEqual): + expected = value + if self.wantobjects >= 2: + if expected2 is not None: + expected = expected2 + expected_type = type(expected) + else: + if expected1 is not None: + expected = expected1 + expected_type = str + nonlocal result + result = None + r = self.interp.call('testfunc', value) + self.assertIsInstance(result, expected_type) + eq(result, expected) + self.assertIsInstance(r, expected_type) + eq(r, expected) + def float_eq(actual, expected): + self.assertAlmostEqual(float(actual), expected, + delta=abs(expected) * 1e-10) + + check(True, '1', 1) + check(False, '0', 0) + check('string') + check('string\xbd') + check('string\u20ac') + check('string\U0001f4bb') + if sys.platform != 'win32': + check('<\udce2\udc82\udcac>', '<\u20ac>', '<\u20ac>') + check('<\udced\udca0\udcbd\udced\udcb2\udcbb>', '<\U0001f4bb>', '<\U0001f4bb>') + check('') + check(b'string', 'string') + check(b'string\xe2\x82\xac', 'string\xe2\x82\xac') + check(b'string\xbd', 'string\xbd') + check(b'', '') + check('str\x00ing') + check('str\x00ing\xbd') + check('str\x00ing\u20ac') + check(b'str\x00ing', 'str\x00ing') + check(b'str\xc0\x80ing', 'str\xc0\x80ing') + check(b'str\xc0\x80ing\xe2\x82\xac', 'str\xc0\x80ing\xe2\x82\xac') + for i in self.get_integers(): + check(i, str(i)) + for f in (0.0, 1.0, -1.0): + check(f, repr(f)) + for f in (1/3.0, sys.float_info.min, sys.float_info.max, + -sys.float_info.min, -sys.float_info.max): + check(f, eq=float_eq) + check(float('inf'), eq=float_eq) + check(-float('inf'), eq=float_eq) + # XXX NaN representation can be not parsable by float() + check((), '', '') + check((1, (2,), (3, 4), '5 6', ()), + '1 2 {3 4} {5 6} {}', + (1, (2,), (3, 4), '5 6', '')) + check([1, [2,], [3, 4], '5 6', []], + '1 2 {3 4} {5 6} {}', + (1, (2,), (3, 4), '5 6', '')) + + def test_passing_tcl_obj(self): + tcl = self.interp.tk + a = None + def testfunc(arg): + nonlocal a + a = arg + self.interp.createcommand('testfunc', testfunc) + self.addCleanup(self.interp.tk.deletecommand, 'testfunc') + tcl.eval(r'set a "\u20ac \ud83d\udcbb \0 \udcab"; regexp -about $a') + tcl.eval(r'testfunc $a') + expected = '\u20ac \U0001f4bb \0 \udced\udcb2\udcab' + if self.wantobjects >= 2: + self.assertEqual(str(a), expected) + self.assertEqual(a.string, expected) + self.assertEqual(a.typename, 'regexp') + else: + self.assertEqual(a, expected) + + def test_splitlist(self): + splitlist = self.interp.tk.splitlist + call = self.interp.tk.call + self.assertRaises(TypeError, splitlist) + self.assertRaises(TypeError, splitlist, 'a', 'b') + self.assertRaises(TypeError, splitlist, 2) + testcases = [ + ('2', ('2',)), + ('', ()), + ('{}', ('',)), + ('""', ('',)), + ('a\n b\t\r c\n ', ('a', 'b', 'c')), + (b'a\n b\t\r c\n ', ('a', 'b', 'c')), + ('a \u20ac', ('a', '\u20ac')), + ('a \U0001f4bb', ('a', '\U0001f4bb')), + (b'a \xe2\x82\xac', ('a', '\u20ac')), + (b'a \xf0\x9f\x92\xbb', ('a', '\U0001f4bb')), + (b'a \xed\xa0\xbd\xed\xb2\xbb', ('a', '\U0001f4bb')), + (b'a\xc0\x80b c\xc0\x80d', ('a\x00b', 'c\x00d')), + ('a {b c}', ('a', 'b c')), + (r'a b\ c', ('a', 'b c')), + (('a', 'b c'), ('a', 'b c')), + ('a 2', ('a', '2')), + (('a', 2), ('a', 2)), + ('a 3.4', ('a', '3.4')), + (('a', 3.4), ('a', 3.4)), + ((), ()), + ([], ()), + (['a', ['b', 'c']], ('a', ['b', 'c'])), + (call('list', 1, '2', (3.4,)), + (1, '2', (3.4,)) if self.wantobjects else + ('1', '2', '3.4')), + ] + if not self.wantobjects: + expected = ('12', '\u20ac', '\xe2\x82\xac', '3.4') + else: + expected = (12, '\u20ac', b'\xe2\x82\xac', (3.4,)) + testcases += [ + (call('dict', 'create', 12, '\u20ac', b'\xe2\x82\xac', (3.4,)), + expected), + ] + dbg_info = ('want objects? %s, Tcl version: %s, Tcl patchlevel: %s' + % (self.wantobjects, tcl_version, self.interp.info_patchlevel())) + for arg, res in testcases: + self.assertEqual(splitlist(arg), res, + 'arg=%a, %s' % (arg, dbg_info)) + self.assertRaises(TclError, splitlist, '{') + + def test_splitdict(self): + splitdict = tkinter._splitdict + tcl = self.interp.tk + + arg = '-a {1 2 3} -something foo status {}' + self.assertEqual(splitdict(tcl, arg, False), + {'-a': '1 2 3', '-something': 'foo', 'status': ''}) + self.assertEqual(splitdict(tcl, arg), + {'a': '1 2 3', 'something': 'foo', 'status': ''}) + + arg = ('-a', (1, 2, 3), '-something', 'foo', 'status', '{}') + self.assertEqual(splitdict(tcl, arg, False), + {'-a': (1, 2, 3), '-something': 'foo', 'status': '{}'}) + self.assertEqual(splitdict(tcl, arg), + {'a': (1, 2, 3), 'something': 'foo', 'status': '{}'}) + + self.assertRaises(RuntimeError, splitdict, tcl, '-a b -c ') + self.assertRaises(RuntimeError, splitdict, tcl, ('-a', 'b', '-c')) + + arg = tcl.call('list', + '-a', (1, 2, 3), '-something', 'foo', 'status', ()) + self.assertEqual(splitdict(tcl, arg), + {'a': (1, 2, 3) if self.wantobjects else '1 2 3', + 'something': 'foo', 'status': ''}) + + arg = tcl.call('dict', 'create', + '-a', (1, 2, 3), '-something', 'foo', 'status', ()) + if not self.wantobjects: + expected = {'a': '1 2 3', 'something': 'foo', 'status': ''} + else: + expected = {'a': (1, 2, 3), 'something': 'foo', 'status': ''} + self.assertEqual(splitdict(tcl, arg), expected) + + def test_join(self): + join = tkinter._join + tcl = self.interp.tk + def unpack(s): + return tcl.call('lindex', s, 0) + def check(value): + self.assertEqual(unpack(join([value])), value) + self.assertEqual(unpack(join([value, 0])), value) + self.assertEqual(unpack(unpack(join([[value]]))), value) + self.assertEqual(unpack(unpack(join([[value, 0]]))), value) + self.assertEqual(unpack(unpack(join([[value], 0]))), value) + self.assertEqual(unpack(unpack(join([[value, 0], 0]))), value) + check('') + check('spam') + check('sp am') + check('sp\tam') + check('sp\nam') + check(' \t\n') + check('{spam}') + check('{sp am}') + check('"spam"') + check('"sp am"') + check('{"spam"}') + check('"{spam}"') + check('sp\\am') + check('"sp\\am"') + check('"{}" "{}"') + check('"\\') + check('"{') + check('"}') + check('\n\\') + check('\n{') + check('\n}') + check('\\\n') + check('{\n') + check('}\n') + + @support.cpython_only + def test_new_tcl_obj(self): + support.check_disallow_instantiation(self, _tkinter.Tcl_Obj) + support.check_disallow_instantiation(self, _tkinter.TkttType) + support.check_disallow_instantiation(self, _tkinter.TkappType) + + +class BigmemTclTest(unittest.TestCase): + + def setUp(self): + self.interp = Tcl() + + @support.cpython_only + @unittest.skipUnless(INT_MAX < PY_SSIZE_T_MAX, "needs UINT_MAX < SIZE_MAX") + @support.bigmemtest(size=INT_MAX + 1, memuse=5, dry_run=False) + def test_huge_string_call(self, size): + value = ' ' * size + self.assertRaises(OverflowError, self.interp.call, 'string', 'index', value, 0) + + @support.cpython_only + @unittest.skipUnless(INT_MAX < PY_SSIZE_T_MAX, "needs UINT_MAX < SIZE_MAX") + @support.bigmemtest(size=INT_MAX + 1, memuse=3, dry_run=False) + def test_huge_string_builtins(self, size): + tk = self.interp.tk + value = '1' + ' ' * size + self.assertRaises(OverflowError, tk.getint, value) + self.assertRaises(OverflowError, tk.getdouble, value) + self.assertRaises(OverflowError, tk.getboolean, value) + self.assertRaises(OverflowError, tk.eval, value) + self.assertRaises(OverflowError, tk.evalfile, value) + self.assertRaises(OverflowError, tk.record, value) + self.assertRaises(OverflowError, tk.adderrorinfo, value) + self.assertRaises(OverflowError, tk.setvar, value, 'x', 'a') + self.assertRaises(OverflowError, tk.setvar, 'x', value, 'a') + self.assertRaises(OverflowError, tk.unsetvar, value) + self.assertRaises(OverflowError, tk.unsetvar, 'x', value) + self.assertRaises(OverflowError, tk.adderrorinfo, value) + self.assertRaises(OverflowError, tk.exprstring, value) + self.assertRaises(OverflowError, tk.exprlong, value) + self.assertRaises(OverflowError, tk.exprboolean, value) + self.assertRaises(OverflowError, tk.splitlist, value) + self.assertRaises(OverflowError, tk.createcommand, value, max) + self.assertRaises(OverflowError, tk.deletecommand, value) + + @support.cpython_only + @unittest.skipUnless(INT_MAX < PY_SSIZE_T_MAX, "needs UINT_MAX < SIZE_MAX") + @support.bigmemtest(size=INT_MAX + 1, memuse=6, dry_run=False) + def test_huge_string_builtins2(self, size): + # These commands require larger memory for possible error messages + tk = self.interp.tk + value = '1' + ' ' * size + self.assertRaises(OverflowError, tk.evalfile, value) + self.assertRaises(OverflowError, tk.unsetvar, value) + self.assertRaises(OverflowError, tk.unsetvar, 'x', value) + + +def setUpModule(): + wantobjects = support.get_resource_value('wantobjects') + if wantobjects is not None: + unittest.enterModuleContext( + support.swap_attr(tkinter, 'wantobjects', int(wantobjects))) + if support.verbose: + tcl = Tcl() + print('patchlevel =', tcl.call('info', 'patchlevel'), flush=True) + + +if __name__ == "__main__": + unittest.main()