test_unicode.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Notes about unicode handling in psutil
  6. ======================================.
  7. Starting from version 5.3.0 psutil adds unicode support, see:
  8. https://github.com/giampaolo/psutil/issues/1040
  9. The notes below apply to *any* API returning a string such as
  10. process exe(), cwd() or username():
  11. * all strings are encoded by using the OS filesystem encoding
  12. (sys.getfilesystemencoding()) which varies depending on the platform
  13. (e.g. "UTF-8" on macOS, "mbcs" on Win)
  14. * no API call is supposed to crash with UnicodeDecodeError
  15. * instead, in case of badly encoded data returned by the OS, the
  16. following error handlers are used to replace the corrupted characters in
  17. the string:
  18. * sys.getfilesystemencodeerrors() or "surrogatescape" on POSIX and
  19. "replace" on Windows.
  20. For a detailed explanation of how psutil handles unicode see #1040.
  21. Tests
  22. =====
  23. List of APIs returning or dealing with a string:
  24. ('not tested' means they are not tested to deal with non-ASCII strings):
  25. * Process.cmdline()
  26. * Process.cwd()
  27. * Process.environ()
  28. * Process.exe()
  29. * Process.memory_maps()
  30. * Process.name()
  31. * Process.net_connections('unix')
  32. * Process.open_files()
  33. * Process.username() (not tested)
  34. * disk_io_counters() (not tested)
  35. * disk_partitions() (not tested)
  36. * disk_usage(str)
  37. * net_connections('unix')
  38. * net_if_addrs() (not tested)
  39. * net_if_stats() (not tested)
  40. * net_io_counters() (not tested)
  41. * sensors_fans() (not tested)
  42. * sensors_temperatures() (not tested)
  43. * users() (not tested)
  44. * WindowsService.binpath() (not tested)
  45. * WindowsService.description() (not tested)
  46. * WindowsService.display_name() (not tested)
  47. * WindowsService.name() (not tested)
  48. * WindowsService.status() (not tested)
  49. * WindowsService.username() (not tested)
  50. In here we create a unicode path with a funky non-ASCII name and (where
  51. possible) make psutil return it back (e.g. on name(), exe(), open_files(),
  52. etc.) and make sure that:
  53. * psutil never crashes with UnicodeDecodeError
  54. * the returned path matches
  55. """
  56. import os
  57. import shutil
  58. import warnings
  59. from contextlib import closing
  60. import psutil
  61. from psutil import BSD
  62. from psutil import POSIX
  63. from psutil import WINDOWS
  64. from psutil.tests import ASCII_FS
  65. from psutil.tests import CI_TESTING
  66. from psutil.tests import HAS_ENVIRON
  67. from psutil.tests import HAS_MEMORY_MAPS
  68. from psutil.tests import HAS_NET_CONNECTIONS_UNIX
  69. from psutil.tests import INVALID_UNICODE_SUFFIX
  70. from psutil.tests import PYPY
  71. from psutil.tests import TESTFN_PREFIX
  72. from psutil.tests import UNICODE_SUFFIX
  73. from psutil.tests import PsutilTestCase
  74. from psutil.tests import bind_unix_socket
  75. from psutil.tests import chdir
  76. from psutil.tests import copyload_shared_lib
  77. from psutil.tests import create_py_exe
  78. from psutil.tests import get_testfn
  79. from psutil.tests import pytest
  80. from psutil.tests import safe_mkdir
  81. from psutil.tests import safe_rmpath
  82. from psutil.tests import skip_on_access_denied
  83. from psutil.tests import spawn_testproc
  84. from psutil.tests import terminate
  85. def try_unicode(suffix):
  86. """Return True if both the fs and the subprocess module can
  87. deal with a unicode file name.
  88. """
  89. sproc = None
  90. testfn = get_testfn(suffix=suffix)
  91. try:
  92. safe_rmpath(testfn)
  93. create_py_exe(testfn)
  94. sproc = spawn_testproc(cmd=[testfn])
  95. shutil.copyfile(testfn, testfn + '-2')
  96. safe_rmpath(testfn + '-2')
  97. except (UnicodeEncodeError, OSError):
  98. return False
  99. else:
  100. return True
  101. finally:
  102. if sproc is not None:
  103. terminate(sproc)
  104. safe_rmpath(testfn)
  105. # ===================================================================
  106. # FS APIs
  107. # ===================================================================
  108. class BaseUnicodeTest(PsutilTestCase):
  109. funky_suffix = None
  110. @classmethod
  111. def setUpClass(cls):
  112. super().setUpClass()
  113. cls.skip_tests = False
  114. cls.funky_name = None
  115. if cls.funky_suffix is not None:
  116. if not try_unicode(cls.funky_suffix):
  117. cls.skip_tests = True
  118. else:
  119. cls.funky_name = get_testfn(suffix=cls.funky_suffix)
  120. create_py_exe(cls.funky_name)
  121. def setUp(self):
  122. super().setUp()
  123. if self.skip_tests:
  124. raise pytest.skip("can't handle unicode str")
  125. @pytest.mark.xdist_group(name="serial")
  126. @pytest.mark.skipif(ASCII_FS, reason="ASCII fs")
  127. class TestFSAPIs(BaseUnicodeTest):
  128. """Test FS APIs with a funky, valid, UTF8 path name."""
  129. funky_suffix = UNICODE_SUFFIX
  130. def expect_exact_path_match(self):
  131. with warnings.catch_warnings():
  132. warnings.simplefilter("ignore")
  133. return self.funky_name in os.listdir(".")
  134. # ---
  135. def test_proc_exe(self):
  136. cmd = [
  137. self.funky_name,
  138. "-c",
  139. "import time; [time.sleep(0.1) for x in range(100)]",
  140. ]
  141. subp = self.spawn_testproc(cmd)
  142. p = psutil.Process(subp.pid)
  143. exe = p.exe()
  144. assert isinstance(exe, str)
  145. if self.expect_exact_path_match():
  146. assert os.path.normcase(exe) == os.path.normcase(self.funky_name)
  147. def test_proc_name(self):
  148. cmd = [
  149. self.funky_name,
  150. "-c",
  151. "import time; [time.sleep(0.1) for x in range(100)]",
  152. ]
  153. subp = self.spawn_testproc(cmd)
  154. name = psutil.Process(subp.pid).name()
  155. assert isinstance(name, str)
  156. if self.expect_exact_path_match():
  157. assert name == os.path.basename(self.funky_name)
  158. def test_proc_cmdline(self):
  159. cmd = [
  160. self.funky_name,
  161. "-c",
  162. "import time; [time.sleep(0.1) for x in range(100)]",
  163. ]
  164. subp = self.spawn_testproc(cmd)
  165. p = psutil.Process(subp.pid)
  166. cmdline = p.cmdline()
  167. for part in cmdline:
  168. assert isinstance(part, str)
  169. if self.expect_exact_path_match():
  170. assert cmdline == cmd
  171. def test_proc_cwd(self):
  172. dname = self.funky_name + "2"
  173. self.addCleanup(safe_rmpath, dname)
  174. safe_mkdir(dname)
  175. with chdir(dname):
  176. p = psutil.Process()
  177. cwd = p.cwd()
  178. assert isinstance(p.cwd(), str)
  179. if self.expect_exact_path_match():
  180. assert cwd == dname
  181. @pytest.mark.skipif(PYPY and WINDOWS, reason="fails on PYPY + WINDOWS")
  182. def test_proc_open_files(self):
  183. p = psutil.Process()
  184. start = set(p.open_files())
  185. with open(self.funky_name, 'rb'):
  186. new = set(p.open_files())
  187. path = (new - start).pop().path
  188. assert isinstance(path, str)
  189. if BSD and not path:
  190. # XXX - see https://github.com/giampaolo/psutil/issues/595
  191. raise pytest.skip("open_files on BSD is broken")
  192. if self.expect_exact_path_match():
  193. assert os.path.normcase(path) == os.path.normcase(self.funky_name)
  194. @pytest.mark.skipif(not POSIX, reason="POSIX only")
  195. def test_proc_net_connections(self):
  196. name = self.get_testfn(suffix=self.funky_suffix)
  197. sock = bind_unix_socket(name)
  198. with closing(sock):
  199. conn = psutil.Process().net_connections('unix')[0]
  200. assert isinstance(conn.laddr, str)
  201. assert conn.laddr == name
  202. @pytest.mark.skipif(not POSIX, reason="POSIX only")
  203. @pytest.mark.skipif(
  204. not HAS_NET_CONNECTIONS_UNIX, reason="can't list UNIX sockets"
  205. )
  206. @skip_on_access_denied()
  207. def test_net_connections(self):
  208. def find_sock(cons):
  209. for conn in cons:
  210. if os.path.basename(conn.laddr).startswith(TESTFN_PREFIX):
  211. return conn
  212. raise ValueError("connection not found")
  213. name = self.get_testfn(suffix=self.funky_suffix)
  214. sock = bind_unix_socket(name)
  215. with closing(sock):
  216. cons = psutil.net_connections(kind='unix')
  217. conn = find_sock(cons)
  218. assert isinstance(conn.laddr, str)
  219. assert conn.laddr == name
  220. def test_disk_usage(self):
  221. dname = self.funky_name + "2"
  222. self.addCleanup(safe_rmpath, dname)
  223. safe_mkdir(dname)
  224. psutil.disk_usage(dname)
  225. @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported")
  226. @pytest.mark.skipif(PYPY, reason="unstable on PYPY")
  227. def test_memory_maps(self):
  228. with copyload_shared_lib(suffix=self.funky_suffix) as funky_path:
  229. def normpath(p):
  230. return os.path.realpath(os.path.normcase(p))
  231. libpaths = [
  232. normpath(x.path) for x in psutil.Process().memory_maps()
  233. ]
  234. # ...just to have a clearer msg in case of failure
  235. libpaths = [x for x in libpaths if TESTFN_PREFIX in x]
  236. assert normpath(funky_path) in libpaths
  237. for path in libpaths:
  238. assert isinstance(path, str)
  239. @pytest.mark.skipif(CI_TESTING, reason="unreliable on CI")
  240. class TestFSAPIsWithInvalidPath(TestFSAPIs):
  241. """Test FS APIs with a funky, invalid path name."""
  242. funky_suffix = INVALID_UNICODE_SUFFIX
  243. def expect_exact_path_match(self):
  244. return True
  245. # ===================================================================
  246. # Non fs APIs
  247. # ===================================================================
  248. class TestNonFSAPIS(BaseUnicodeTest):
  249. """Unicode tests for non fs-related APIs."""
  250. funky_suffix = UNICODE_SUFFIX
  251. @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported")
  252. @pytest.mark.skipif(PYPY and WINDOWS, reason="segfaults on PYPY + WINDOWS")
  253. def test_proc_environ(self):
  254. # Note: differently from others, this test does not deal
  255. # with fs paths.
  256. env = os.environ.copy()
  257. env['FUNNY_ARG'] = self.funky_suffix
  258. sproc = self.spawn_testproc(env=env)
  259. p = psutil.Process(sproc.pid)
  260. env = p.environ()
  261. for k, v in env.items():
  262. assert isinstance(k, str)
  263. assert isinstance(v, str)
  264. assert env['FUNNY_ARG'] == self.funky_suffix