From 59f9b4e4509be67494f3d45489fa55523175ff69 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 5 Jan 2021 09:13:15 +0200 Subject: [PATCH] bpo-42681: Fix test_curses failures related to color pairs (GH-24089) On ncurses 6.1 pair numbers are limited by SHORT_MAX-1, even with extended color support. Improve error reporting and tests for color functions. --- Lib/test/test_curses.py | 53 +++++++++++++++++++++++++++--------- Modules/_cursesmodule.c | 60 +++++++++++++++++++++++++++-------------- 2 files changed, 81 insertions(+), 32 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 7b40d714742..f2cad05b2c9 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -47,6 +47,7 @@ def requires_colors(test): 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', @@ -327,11 +328,20 @@ class TestCurses(unittest.TestCase): def bad_pairs(self): return (-1, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64) + 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) - curses.color_content(curses.COLORS - 1) + maxcolor = curses.COLORS - 1 + curses.color_content(maxcolor) for color in self.bad_colors(): self.assertRaises(ValueError, curses.color_content, color) @@ -352,11 +362,12 @@ class TestCurses(unittest.TestCase): curses.init_color(0, 1000, 1000, 1000) self.assertEqual(curses.color_content(0), (1000, 1000, 1000)) - old = curses.color_content(curses.COLORS - 1) - curses.init_color(curses.COLORS - 1, *old) - self.addCleanup(curses.init_color, curses.COLORS - 1, *old) - curses.init_color(curses.COLORS - 1, 0, 500, 1000) - self.assertEqual(curses.color_content(curses.COLORS - 1), (0, 500, 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) @@ -365,13 +376,25 @@ class TestCurses(unittest.TestCase): 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) + return pair_limit + @requires_colors def test_pair_content(self): if not hasattr(curses, 'use_default_colors'): self.assertEqual(curses.pair_content(0), (curses.COLOR_WHITE, curses.COLOR_BLACK)) curses.pair_content(0) - curses.pair_content(curses.COLOR_PAIRS - 1) + 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) @@ -384,11 +407,15 @@ class TestCurses(unittest.TestCase): curses.init_pair(1, 0, 0) self.assertEqual(curses.pair_content(1), (0, 0)) - curses.init_pair(1, curses.COLORS - 1, curses.COLORS - 1) - self.assertEqual(curses.pair_content(1), - (curses.COLORS - 1, curses.COLORS - 1)) - curses.init_pair(curses.COLOR_PAIRS - 1, 2, 3) - self.assertEqual(curses.pair_content(curses.COLOR_PAIRS - 1), (2, 3)) + 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) @@ -582,6 +609,8 @@ class MiscTests(unittest.TestCase): @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) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 23f6d96f514..7175c722965 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -135,29 +135,28 @@ typedef chtype attr_t; /* No attr_t type is available */ #define STRICT_SYSV_CURSES #endif -#if defined(NCURSES_EXT_COLORS) && defined(NCURSES_EXT_FUNCS) +#if NCURSES_EXT_COLORS+0 && NCURSES_EXT_FUNCS+0 #define _NCURSES_EXTENDED_COLOR_FUNCS 1 #else #define _NCURSES_EXTENDED_COLOR_FUNCS 0 #endif /* defined(NCURSES_EXT_COLORS) && defined(NCURSES_EXT_FUNCS) */ #if _NCURSES_EXTENDED_COLOR_FUNCS -#define _NCURSES_COLOR_VAL_TYPE int +#define _CURSES_COLOR_VAL_TYPE int +#define _CURSES_COLOR_NUM_TYPE int #define _CURSES_INIT_COLOR_FUNC init_extended_color #define _CURSES_INIT_PAIR_FUNC init_extended_pair #define _COLOR_CONTENT_FUNC extended_color_content -#define _CURSES_PAIR_NUMBER_FUNC extended_pair_content +#define _CURSES_PAIR_CONTENT_FUNC extended_pair_content #else -#define _NCURSES_COLOR_VAL_TYPE short +#define _CURSES_COLOR_VAL_TYPE short +#define _CURSES_COLOR_NUM_TYPE short #define _CURSES_INIT_COLOR_FUNC init_color #define _CURSES_INIT_PAIR_FUNC init_pair #define _COLOR_CONTENT_FUNC color_content -#define _CURSES_PAIR_NUMBER_FUNC pair_content +#define _CURSES_PAIR_CONTENT_FUNC pair_content #endif /* _NCURSES_EXTENDED_COLOR_FUNCS */ -#define _CURSES_INIT_COLOR_FUNC_NAME Py_STRINGIFY(_CURSES_INIT_COLOR_FUNC) -#define _CURSES_INIT_PAIR_FUNC_NAME Py_STRINGIFY(_CURSES_INIT_PAIR_FUNC) - /*[clinic input] module _curses class _curses.window "PyCursesWindowObject *" "&PyCursesWindow_Type" @@ -2737,18 +2736,18 @@ static PyObject * _curses_color_content_impl(PyObject *module, int color_number) /*[clinic end generated code: output=17b466df7054e0de input=03b5ed0472662aea]*/ { - _NCURSES_COLOR_VAL_TYPE r,g,b; + _CURSES_COLOR_VAL_TYPE r,g,b; PyCursesInitialised; PyCursesInitialisedColor; - if (_COLOR_CONTENT_FUNC(color_number, &r, &g, &b) != ERR) - return Py_BuildValue("(iii)", r, g, b); - else { - PyErr_SetString(PyCursesError, - "Argument 1 was out of range. Check value of COLORS."); + if (_COLOR_CONTENT_FUNC(color_number, &r, &g, &b) == ERR) { + PyErr_Format(PyCursesError, "%s() returned ERR", + Py_STRINGIFY(_COLOR_CONTENT_FUNC)); return NULL; } + + return Py_BuildValue("(iii)", r, g, b); } /*[clinic input] @@ -3190,7 +3189,8 @@ _curses_init_color_impl(PyObject *module, int color_number, short r, short g, PyCursesInitialised; PyCursesInitialisedColor; - return PyCursesCheckERR(_CURSES_INIT_COLOR_FUNC(color_number, r, g, b), _CURSES_INIT_COLOR_FUNC_NAME); + return PyCursesCheckERR(_CURSES_INIT_COLOR_FUNC(color_number, r, g, b), + Py_STRINGIFY(_CURSES_INIT_COLOR_FUNC)); } /*[clinic input] @@ -3217,7 +3217,20 @@ _curses_init_pair_impl(PyObject *module, int pair_number, int fg, int bg) PyCursesInitialised; PyCursesInitialisedColor; - return PyCursesCheckERR(_CURSES_INIT_PAIR_FUNC(pair_number, fg, bg), _CURSES_INIT_PAIR_FUNC_NAME); + if (_CURSES_INIT_PAIR_FUNC(pair_number, fg, bg) == ERR) { + if (pair_number >= COLOR_PAIRS) { + PyErr_Format(PyExc_ValueError, + "Color pair is greater than COLOR_PAIRS-1 (%d).", + COLOR_PAIRS - 1); + } + else { + PyErr_Format(PyCursesError, "%s() returned ERR", + Py_STRINGIFY(_CURSES_INIT_PAIR_FUNC)); + } + return NULL; + } + + Py_RETURN_NONE; } static PyObject *ModDict; @@ -3845,14 +3858,21 @@ static PyObject * _curses_pair_content_impl(PyObject *module, int pair_number) /*[clinic end generated code: output=4a726dd0e6885f3f input=03970f840fc7b739]*/ { - _NCURSES_COLOR_VAL_TYPE f, b; + _CURSES_COLOR_NUM_TYPE f, b; PyCursesInitialised; PyCursesInitialisedColor; - if (_CURSES_PAIR_NUMBER_FUNC(pair_number, &f, &b)==ERR) { - PyErr_SetString(PyCursesError, - "Argument 1 was out of range. (0..COLOR_PAIRS-1)"); + if (_CURSES_PAIR_CONTENT_FUNC(pair_number, &f, &b) == ERR) { + if (pair_number >= COLOR_PAIRS) { + PyErr_Format(PyExc_ValueError, + "Color pair is greater than COLOR_PAIRS-1 (%d).", + COLOR_PAIRS - 1); + } + else { + PyErr_Format(PyCursesError, "%s() returned ERR", + Py_STRINGIFY(_CURSES_PAIR_CONTENT_FUNC)); + } return NULL; }