comparison test/test_admin.py @ 8439:3bdae15252c6

feat: add support for ! history and readline command in roundup-admin Ad support to change input mode emacs/vi using new 'readline' roundup-admin command. Also bind keys to command/input strings, List numbered history and allow rerunning a command with !<number> or allow user to edit it using !<number>:p. admin_guide.txt: Added docs. admin.py: add functionality. Reconcile import commands to standard. Replace IOError with FileNotFoundError no that we have removed python 2.7 support. Add support for identifying backend used to supply line editing/history functions. Add support for saving commands sent on stdin to history to allow preloading of history. test_admin.py: Test code. Can't test mode changes as lack of pty when driving command line turns off line editing in readline/pyreadline3. Similarly can't test key bindings/settings. Some refactoring of test conditions that had to change because of additional output reporting backend library.
author John Rouillard <rouilj@ieee.org>
date Sun, 31 Aug 2025 16:54:17 -0400
parents 1a93dc58f975
children 254f70dfc585
comparison
equal deleted inserted replaced
8438:98e17dd0197f 8439:3bdae15252c6
3 # All rights reserved. 3 # All rights reserved.
4 # For license terms see the file COPYING.txt. 4 # For license terms see the file COPYING.txt.
5 # 5 #
6 6
7 from __future__ import print_function 7 from __future__ import print_function
8 import difflib
9 import errno
8 import fileinput 10 import fileinput
9 import unittest, os, shutil, errno, sys, difflib, re 11 import io
12 import os
13 import platform
14 import pytest
15 import re
16 import shutil
17 import sys
18 import unittest
10 19
11 from roundup.admin import AdminTool 20 from roundup.admin import AdminTool
12 21
13 from .test_mysql import skip_mysql 22 from .test_mysql import skip_mysql
14 from .test_postgresql import skip_postgresql 23 from .test_postgresql import skip_postgresql
79 88
80 backend = None 89 backend = None
81 90
82 def setUp(self): 91 def setUp(self):
83 self.dirname = '_test_admin' 92 self.dirname = '_test_admin'
93
94 @pytest.fixture(autouse=True)
95 def inject_fixtures(self, monkeypatch):
96 self._monkeypatch = monkeypatch
84 97
85 def tearDown(self): 98 def tearDown(self):
86 try: 99 try:
87 shutil.rmtree(self.dirname) 100 shutil.rmtree(self.dirname)
88 except OSError as error: 101 except OSError as error:
146 out = out.getvalue().strip() 159 out = out.getvalue().strip()
147 160
148 print(ret) 161 print(ret)
149 self.assertTrue(ret == 0) 162 self.assertTrue(ret == 0)
150 expected = 'ready for input.\nType "help" for help.' 163 expected = 'ready for input.\nType "help" for help.'
151 self.assertEqual(expected, out[-1*len(expected):]) 164 # back up by 30 to make sure 'ready for input' in slice.
165 self.assertIn(expected,
166 "\n".join(out.split('\n')[-3:-1]))
152 167
153 inputs = iter(["list user", "q"]) 168 inputs = iter(["list user", "q"])
154 169
155 AdminTool.my_input = lambda _self, _prompt: next(inputs) 170 AdminTool.my_input = lambda _self, _prompt: next(inputs)
156 171
159 174
160 out = out.getvalue().strip() 175 out = out.getvalue().strip()
161 176
162 print(ret) 177 print(ret)
163 self.assertTrue(ret == 0) 178 self.assertTrue(ret == 0)
164 expected = 'help.\n 1: admin\n 2: anonymous' 179 expected = ' 1: admin\n 2: anonymous'
165 self.assertEqual(expected, out[-1*len(expected):]) 180
181 self.assertEqual(expected,
182 "\n".join(out.split('\n')[-2:]))
166 183
167 184
168 AdminTool.my_input = orig_input 185 AdminTool.my_input = orig_input
169 186
170 def testGet(self): 187 def testGet(self):
1102 1119
1103 out = out.getvalue().strip().split('\n') 1120 out = out.getvalue().strip().split('\n')
1104 1121
1105 print(ret) 1122 print(ret)
1106 self.assertTrue(ret == 0) 1123 self.assertTrue(ret == 0)
1107 self.assertEqual('Reopening tracker', out[2]) 1124 self.assertEqual('Reopening tracker', out[3])
1108 expected = ' _reopen_tracker=True' 1125 expected = ' _reopen_tracker=True'
1109 self.assertIn(expected, out) 1126 self.assertIn(expected, out)
1110 1127
1111 # ----- 1128 # -----
1112 AdminTool.my_input = orig_input 1129 AdminTool.my_input = orig_input
1131 1148
1132 with captured_output() as (out, err): 1149 with captured_output() as (out, err):
1133 ret = self.admin.main() 1150 ret = self.admin.main()
1134 1151
1135 out = out.getvalue().strip().split('\n') 1152 out = out.getvalue().strip().split('\n')
1136 1153
1137 print(ret) 1154 print(ret)
1138 self.assertTrue(ret == 0) 1155 self.assertTrue(ret == 0)
1139 expected = ' verbose=True' 1156 expected = ' verbose=True'
1140 self.assertIn(expected, out) 1157 self.assertIn(expected, out)
1141 self.assertIn('descriptions...', out[-1]) 1158 self.assertIn('descriptions...', out[-1])
1153 1170
1154 with captured_output() as (out, err): 1171 with captured_output() as (out, err):
1155 ret = self.admin.main() 1172 ret = self.admin.main()
1156 1173
1157 out = out.getvalue().strip().split('\n') 1174 out = out.getvalue().strip().split('\n')
1158 1175
1159 print(ret) 1176 print(ret)
1160 self.assertTrue(ret == 0) 1177 self.assertTrue(ret == 0)
1161 expected = ' verbose=False' 1178 expected = ' verbose=False'
1162 self.assertIn(expected, out) 1179 self.assertIn(expected, out)
1163 1180
1808 out = out.getvalue().strip() 1825 out = out.getvalue().strip()
1809 err = err.getvalue().strip() 1826 err = err.getvalue().strip()
1810 self.assertEqual(out, expected) 1827 self.assertEqual(out, expected)
1811 self.assertEqual(len(err), 0) 1828 self.assertEqual(len(err), 0)
1812 1829
1830 def testReadline(self):
1831 ''' Note the tests will fail if you run this under pdb.
1832 the context managers capture the pdb prompts and this screws
1833 up the stdout strings with (pdb) prefixed to the line.
1834 '''
1835
1836 '''history didn't work when testing. The commands being
1837 executed aren't being sent into the history
1838 buffer. Failed under both windows and linux.
1839
1840 Explicitly using: readline.set_auto_history(True) in
1841 roundup-admin setup had no effect.
1842
1843 Looks like monkeypatching stdin is the issue since:
1844
1845 printf... | roundup-admin | tee
1846
1847 doesn't work either when printf uses
1848
1849 "readline vi\nreadline emacs\nreadline history\nquit\n"
1850
1851 Added explicit readline.add_history() if stdin or
1852 stdout are not a tty to admin.py:interactive().
1853
1854 Still no way to drive editing with control/escape
1855 chars to verify editing mode, check keybindings. Need
1856 to trick Admintool to believe it's running on a
1857 tty/pty/con in linux/windows to remove my hack.
1858 '''
1859
1860 # Put the init file in the tracker test directory so
1861 # we don't clobber user's actual init file.
1862 original_home = None
1863 if 'HOME' in os.environ:
1864 original_home = os.environ['HOME']
1865 os.environ['HOME'] = self.dirname
1866
1867 # same but for windows.
1868 original_userprofile = None
1869 if 'USERPROFILE' in os.environ:
1870 # windows
1871 original_userprofile = os.environ['USERPROFILE']
1872 os.environ['USERPROFILE'] = self.dirname
1873
1874 inputs = ["readline vi", "readline emacs", "readline reload", "quit"]
1875
1876 self._monkeypatch.setattr(
1877 'sys.stdin',
1878 io.StringIO("\n".join(inputs)))
1879
1880 self.install_init()
1881 self.admin=AdminTool()
1882
1883 # disable loading and saving history
1884 self.admin.settings['history_features'] = 3
1885
1886 # verify correct init file is being
1887 self.assertIn(os.path.join(os.path.expanduser("~"),
1888 ".roundup_admin_rlrc"),
1889 self.admin.get_readline_init_file())
1890
1891 # No exception is raised for missing file
1892 # under pyreadline3. Detect pyreadline3 looking for:
1893 # readline.Readline
1894 pyreadline = hasattr(self.admin.readline, "Readline")
1895
1896 sys.argv=['main', '-i', self.dirname]
1897
1898 with captured_output() as (out, err):
1899 ret = self.admin.main()
1900 out = out.getvalue().strip().split('\n')
1901
1902 print(ret)
1903 self.assertTrue(ret == 0)
1904
1905 expected = 'roundup> Enabled vi mode.'
1906 self.assertIn(expected, out)
1907
1908 expected = 'roundup> Enabled emacs mode.'
1909 self.assertIn(expected, out)
1910
1911 if not pyreadline:
1912 expected = ('roundup> Init file %s '
1913 'not found.' % self.admin.get_readline_init_file())
1914 self.assertIn(expected, out)
1915
1916 # --- test 2
1917
1918 inputs = ["readline reload", "q"]
1919
1920 self._monkeypatch.setattr(
1921 'sys.stdin',
1922 io.StringIO("\n".join(inputs)))
1923
1924 self.install_init()
1925 self.admin=AdminTool()
1926
1927 with open(self.admin.get_readline_init_file(),
1928 "w") as config_file:
1929 # there is no config line that works for all
1930 # pyreadline3 (windows), readline(*nix), or editline
1931 # (mac). So write empty file.
1932 config_file.write("")
1933
1934 # disable loading and saving history
1935 self.admin.settings['history_features'] = 3
1936 sys.argv=['main', '-i', self.dirname]
1937
1938 with captured_output() as (out, err):
1939 ret = self.admin.main()
1940 out = out.getvalue().strip().split('\n')
1941
1942 print(ret)
1943 self.assertTrue(ret == 0)
1944
1945 expected = ('roundup> File %s reloaded.' %
1946 self.admin.get_readline_init_file())
1947
1948 self.assertIn(expected, out)
1949
1950 # === cleanup
1951 if original_home:
1952 os.environ['HOME'] = original_home
1953 if original_userprofile:
1954 os.environ['USERPROFILE'] = original_userprofile
1955
1956 def test_admin_history_save_load(self):
1957 # To prevent overwriting/reading user's actual history,
1958 # change HOME enviroment var.
1959 original_home = None
1960 if 'HOME' in os.environ:
1961 original_home = os.environ['HOME']
1962 os.environ['HOME'] = self.dirname
1963 os.environ['HOME'] = self.dirname
1964
1965 # same idea but windows
1966 original_userprofile = None
1967 if 'USERPROFILE' in os.environ:
1968 # windows
1969 original_userprofile = os.environ['USERPROFILE']
1970 os.environ['USERPROFILE'] = self.dirname
1971
1972 # -- history test
1973 inputs = ["readline history", "q"]
1974
1975 self._monkeypatch.setattr(
1976 'sys.stdin',
1977 io.StringIO("\n".join(inputs)))
1978
1979 self.install_init()
1980 self.admin=AdminTool()
1981
1982 # use defaults load/save history
1983 self.admin.settings['history_features'] = 0
1984
1985 sys.argv=['main', '-i', self.dirname]
1986
1987 with captured_output() as (out, err):
1988 ret = self.admin.main()
1989 out = out.getvalue().strip().split('\n')
1990
1991 print(ret)
1992 self.assertTrue(ret == 0)
1993
1994 expected = 'roundup> history size 1'
1995 self.assertIn(expected, out)
1996
1997 expected = ' 1 readline history'
1998 self.assertIn(expected, out)
1999
2000 # -- history test 3 reruns readline vi
2001 inputs = ["readline vi", "readline history", "!3",
2002 "readline history", "!23s", "q"]
2003
2004 self._monkeypatch.setattr(
2005 'sys.stdin',
2006 io.StringIO("\n".join(inputs)))
2007
2008 # preserve directory self.install_init()
2009 self.admin=AdminTool()
2010
2011 # default use all features
2012 #self.admin.settings['history_features'] = 3
2013 sys.argv=['main', '-i', self.dirname]
2014
2015 with captured_output() as (out, err):
2016 ret = self.admin.main()
2017 out = out.getvalue().strip().split('\n')
2018
2019 print(ret)
2020 self.assertTrue(ret == 0)
2021
2022 # 4 includes 2 commands in saved history
2023 expected = 'roundup> history size 4'
2024 self.assertIn(expected, out)
2025
2026 expected = ' 4 readline history'
2027 self.assertIn(expected, out)
2028
2029 # Shouldn't work on windows.
2030 if platform.system() != "Windows":
2031 expected = ' 5 readline vi'
2032 self.assertIn(expected, out)
2033 else:
2034 # PYREADLINE UNDER WINDOWS
2035 # py3readline on windows can't replace
2036 # command strings in history when connected
2037 # to a console. (Console triggers autosave and
2038 # I have to turn !3 into it's substituted value.)
2039 # but in testing autosave is disabled so
2040 # I don't get the !number but the actual command
2041 # It should have
2042 #
2043 # expected = ' 5 !3'
2044 #
2045 # but it is the same as the unix case.
2046 expected = ' 5 readline vi'
2047 self.assertIn(expected, out)
2048
2049 expected = ('roundup> Unknown command "!23s" ("help commands" '
2050 'for a list)')
2051 self.assertIn(expected, out)
2052
2053 print(out)
2054 # can't test !#:p mode as readline editing doesn't work
2055 # if not in a tty.
2056
2057 # === cleanup
2058 if original_home:
2059 os.environ['HOME'] = original_home
2060 if original_userprofile:
2061 os.environ['USERPROFILE'] = original_userprofile
2062
2063 def test_admin_readline_history(self):
2064 original_home = os.environ['HOME']
2065 # To prevent overwriting/reading user's actual history,
2066 # change HOME enviroment var.
2067 os.environ['HOME'] = self.dirname
2068
2069 original_userprofile = None
2070 if 'USERPROFILE' in os.environ:
2071 # windows
2072 original_userprofile = os.environ['USERPROFILE']
2073 os.environ['USERPROFILE'] = self.dirname
2074
2075 # -- history test
2076 inputs = ["readline history", "q"]
2077
2078 self._monkeypatch.setattr(
2079 'sys.stdin',
2080 io.StringIO("\n".join(inputs)))
2081
2082 self.install_init()
2083 self.admin=AdminTool()
2084
2085 # disable loading, but save history
2086 self.admin.settings['history_features'] = 3
2087 sys.argv=['main', '-i', self.dirname]
2088
2089 with captured_output() as (out, err):
2090 ret = self.admin.main()
2091 out = out.getvalue().strip().split('\n')
2092
2093 print(ret)
2094 self.assertTrue(ret == 0)
2095
2096 expected = 'roundup> history size 1'
2097 self.assertIn(expected, out)
2098
2099 expected = ' 1 readline history'
2100 self.assertIn(expected, out)
2101
2102 # -- history test
2103 inputs = ["readline vi", "readline history", "!1", "!2", "q"]
2104
2105 self._monkeypatch.setattr(
2106 'sys.stdin',
2107 io.StringIO("\n".join(inputs)))
2108
2109 self.install_init()
2110 self.admin=AdminTool()
2111
2112 # disable loading, but save history
2113 self.admin.settings['history_features'] = 3
2114 sys.argv=['main', '-i', self.dirname]
2115
2116 with captured_output() as (out, err):
2117 ret = self.admin.main()
2118 out = out.getvalue().strip().split('\n')
2119
2120 print(ret)
2121 self.assertTrue(ret == 0)
2122
2123 expected = 'roundup> history size 2'
2124 self.assertIn(expected, out)
2125
2126 expected = ' 2 readline history'
2127 self.assertIn(expected, out)
2128
2129 # doesn't work on windows.
2130 if platform.system() != "Windows":
2131 expected = ' 4 readline history'
2132 self.assertIn(expected, out)
2133 else:
2134 # See
2135 # PYREADLINE UNDER WINDOWS
2136 # elsewhere in this file for why I am not checking for
2137 # expected = ' 4 !2'
2138 expected = ' 4 readline history'
2139 self.assertIn(expected, out)
2140
2141 # can't test !#:p mode as readline editing doesn't work
2142 # if not in a tty.
2143
2144 # === cleanup
2145 os.environ['HOME'] = original_home
2146 if original_userprofile:
2147 os.environ['USERPROFILE'] = original_userprofile
2148
1813 def testSpecification(self): 2149 def testSpecification(self):
1814 ''' Note the tests will fail if you run this under pdb. 2150 ''' Note the tests will fail if you run this under pdb.
1815 the context managers capture the pdb prompts and this screws 2151 the context managers capture the pdb prompts and this screws
1816 up the stdout strings with (pdb) prefixed to the line. 2152 up the stdout strings with (pdb) prefixed to the line.
1817 ''' 2153 '''

Roundup Issue Tracker: http://roundup-tracker.org/