comparison env/lib/python3.7/site-packages/psutil/tests/test_unicode.py @ 0:26e78fe6e8c4 draft

"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author shellac
date Sat, 02 May 2020 07:14:21 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:26e78fe6e8c4
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3
4 # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
5 # Use of this source code is governed by a BSD-style license that can be
6 # found in the LICENSE file.
7
8 """
9 Notes about unicode handling in psutil
10 ======================================
11
12 Starting from version 5.3.0 psutil adds unicode support, see:
13 https://github.com/giampaolo/psutil/issues/1040
14 The notes below apply to *any* API returning a string such as
15 process exe(), cwd() or username():
16
17 * all strings are encoded by using the OS filesystem encoding
18 (sys.getfilesystemencoding()) which varies depending on the platform
19 (e.g. "UTF-8" on macOS, "mbcs" on Win)
20 * no API call is supposed to crash with UnicodeDecodeError
21 * instead, in case of badly encoded data returned by the OS, the
22 following error handlers are used to replace the corrupted characters in
23 the string:
24 * Python 3: sys.getfilesystemencodeerrors() (PY 3.6+) or
25 "surrogatescape" on POSIX and "replace" on Windows
26 * Python 2: "replace"
27 * on Python 2 all APIs return bytes (str type), never unicode
28 * on Python 2, you can go back to unicode by doing:
29
30 >>> unicode(p.exe(), sys.getdefaultencoding(), errors="replace")
31
32 For a detailed explanation of how psutil handles unicode see #1040.
33
34 Tests
35 =====
36
37 List of APIs returning or dealing with a string:
38 ('not tested' means they are not tested to deal with non-ASCII strings):
39
40 * Process.cmdline()
41 * Process.connections('unix')
42 * Process.cwd()
43 * Process.environ()
44 * Process.exe()
45 * Process.memory_maps()
46 * Process.name()
47 * Process.open_files()
48 * Process.username() (not tested)
49
50 * disk_io_counters() (not tested)
51 * disk_partitions() (not tested)
52 * disk_usage(str)
53 * net_connections('unix')
54 * net_if_addrs() (not tested)
55 * net_if_stats() (not tested)
56 * net_io_counters() (not tested)
57 * sensors_fans() (not tested)
58 * sensors_temperatures() (not tested)
59 * users() (not tested)
60
61 * WindowsService.binpath() (not tested)
62 * WindowsService.description() (not tested)
63 * WindowsService.display_name() (not tested)
64 * WindowsService.name() (not tested)
65 * WindowsService.status() (not tested)
66 * WindowsService.username() (not tested)
67
68 In here we create a unicode path with a funky non-ASCII name and (where
69 possible) make psutil return it back (e.g. on name(), exe(), open_files(),
70 etc.) and make sure that:
71
72 * psutil never crashes with UnicodeDecodeError
73 * the returned path matches
74 """
75
76 import os
77 import traceback
78 import warnings
79 from contextlib import closing
80
81 from psutil import BSD
82 from psutil import MACOS
83 from psutil import OPENBSD
84 from psutil import POSIX
85 from psutil import WINDOWS
86 from psutil._compat import PY3
87 from psutil._compat import u
88 from psutil.tests import APPVEYOR
89 from psutil.tests import CIRRUS
90 from psutil.tests import ASCII_FS
91 from psutil.tests import bind_unix_socket
92 from psutil.tests import chdir
93 from psutil.tests import copyload_shared_lib
94 from psutil.tests import create_exe
95 from psutil.tests import get_test_subprocess
96 from psutil.tests import HAS_CONNECTIONS_UNIX
97 from psutil.tests import HAS_ENVIRON
98 from psutil.tests import HAS_MEMORY_MAPS
99 from psutil.tests import PYPY
100 from psutil.tests import reap_children
101 from psutil.tests import safe_mkdir
102 from psutil.tests import safe_rmpath as _safe_rmpath
103 from psutil.tests import skip_on_access_denied
104 from psutil.tests import TESTFILE_PREFIX
105 from psutil.tests import TESTFN
106 from psutil.tests import TESTFN_UNICODE
107 from psutil.tests import TRAVIS
108 from psutil.tests import unittest
109 from psutil.tests import unix_socket_path
110 import psutil
111
112
113 def safe_rmpath(path):
114 if APPVEYOR:
115 # TODO - this is quite random and I'm not sure why it happens,
116 # nor I can reproduce it locally:
117 # https://ci.appveyor.com/project/giampaolo/psutil/build/job/
118 # jiq2cgd6stsbtn60
119 # safe_rmpath() happens after reap_children() so this is weird
120 # Perhaps wait_procs() on Windows is broken? Maybe because
121 # of STILL_ACTIVE?
122 # https://github.com/giampaolo/psutil/blob/
123 # 68c7a70728a31d8b8b58f4be6c4c0baa2f449eda/psutil/arch/
124 # windows/process_info.c#L146
125 try:
126 return _safe_rmpath(path)
127 except WindowsError:
128 traceback.print_exc()
129 else:
130 return _safe_rmpath(path)
131
132
133 def subprocess_supports_unicode(name):
134 """Return True if both the fs and the subprocess module can
135 deal with a unicode file name.
136 """
137 if PY3:
138 return True
139 try:
140 safe_rmpath(name)
141 create_exe(name)
142 get_test_subprocess(cmd=[name])
143 except UnicodeEncodeError:
144 return False
145 else:
146 return True
147 finally:
148 reap_children()
149
150
151 # An invalid unicode string.
152 if PY3:
153 INVALID_NAME = (TESTFN.encode('utf8') + b"f\xc0\x80").decode(
154 'utf8', 'surrogateescape')
155 else:
156 INVALID_NAME = TESTFN + "f\xc0\x80"
157
158
159 # ===================================================================
160 # FS APIs
161 # ===================================================================
162
163
164 class _BaseFSAPIsTests(object):
165 funky_name = None
166
167 @classmethod
168 def setUpClass(cls):
169 safe_rmpath(cls.funky_name)
170 create_exe(cls.funky_name)
171
172 @classmethod
173 def tearDownClass(cls):
174 reap_children()
175 safe_rmpath(cls.funky_name)
176
177 def tearDown(self):
178 reap_children()
179
180 def expect_exact_path_match(self):
181 raise NotImplementedError("must be implemented in subclass")
182
183 def test_proc_exe(self):
184 subp = get_test_subprocess(cmd=[self.funky_name])
185 p = psutil.Process(subp.pid)
186 exe = p.exe()
187 self.assertIsInstance(exe, str)
188 if self.expect_exact_path_match():
189 self.assertEqual(os.path.normcase(exe),
190 os.path.normcase(self.funky_name))
191
192 def test_proc_name(self):
193 subp = get_test_subprocess(cmd=[self.funky_name])
194 name = psutil.Process(subp.pid).name()
195 self.assertIsInstance(name, str)
196 if self.expect_exact_path_match():
197 self.assertEqual(name, os.path.basename(self.funky_name))
198
199 def test_proc_cmdline(self):
200 subp = get_test_subprocess(cmd=[self.funky_name])
201 p = psutil.Process(subp.pid)
202 cmdline = p.cmdline()
203 for part in cmdline:
204 self.assertIsInstance(part, str)
205 if self.expect_exact_path_match():
206 self.assertEqual(cmdline, [self.funky_name])
207
208 def test_proc_cwd(self):
209 dname = self.funky_name + "2"
210 self.addCleanup(safe_rmpath, dname)
211 safe_mkdir(dname)
212 with chdir(dname):
213 p = psutil.Process()
214 cwd = p.cwd()
215 self.assertIsInstance(p.cwd(), str)
216 if self.expect_exact_path_match():
217 self.assertEqual(cwd, dname)
218
219 @unittest.skipIf(PYPY and WINDOWS, "fails on PYPY + WINDOWS")
220 def test_proc_open_files(self):
221 p = psutil.Process()
222 start = set(p.open_files())
223 with open(self.funky_name, 'rb'):
224 new = set(p.open_files())
225 path = (new - start).pop().path
226 self.assertIsInstance(path, str)
227 if BSD and not path:
228 # XXX - see https://github.com/giampaolo/psutil/issues/595
229 return self.skipTest("open_files on BSD is broken")
230 if self.expect_exact_path_match():
231 self.assertEqual(os.path.normcase(path),
232 os.path.normcase(self.funky_name))
233
234 @unittest.skipIf(not POSIX, "POSIX only")
235 def test_proc_connections(self):
236 suffix = os.path.basename(self.funky_name)
237 with unix_socket_path(suffix=suffix) as name:
238 try:
239 sock = bind_unix_socket(name)
240 except UnicodeEncodeError:
241 if PY3:
242 raise
243 else:
244 raise unittest.SkipTest("not supported")
245 with closing(sock):
246 conn = psutil.Process().connections('unix')[0]
247 self.assertIsInstance(conn.laddr, str)
248 # AF_UNIX addr not set on OpenBSD
249 if not OPENBSD and not CIRRUS: # XXX
250 self.assertEqual(conn.laddr, name)
251
252 @unittest.skipIf(not POSIX, "POSIX only")
253 @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets")
254 @skip_on_access_denied()
255 def test_net_connections(self):
256 def find_sock(cons):
257 for conn in cons:
258 if os.path.basename(conn.laddr).startswith(TESTFILE_PREFIX):
259 return conn
260 raise ValueError("connection not found")
261
262 suffix = os.path.basename(self.funky_name)
263 with unix_socket_path(suffix=suffix) as name:
264 try:
265 sock = bind_unix_socket(name)
266 except UnicodeEncodeError:
267 if PY3:
268 raise
269 else:
270 raise unittest.SkipTest("not supported")
271 with closing(sock):
272 cons = psutil.net_connections(kind='unix')
273 # AF_UNIX addr not set on OpenBSD
274 if not OPENBSD:
275 conn = find_sock(cons)
276 self.assertIsInstance(conn.laddr, str)
277 self.assertEqual(conn.laddr, name)
278
279 def test_disk_usage(self):
280 dname = self.funky_name + "2"
281 self.addCleanup(safe_rmpath, dname)
282 safe_mkdir(dname)
283 psutil.disk_usage(dname)
284
285 @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported")
286 @unittest.skipIf(not PY3, "ctypes does not support unicode on PY2")
287 @unittest.skipIf(PYPY and WINDOWS,
288 "copyload_shared_lib() unsupported on PYPY + WINDOWS")
289 def test_memory_maps(self):
290 # XXX: on Python 2, using ctypes.CDLL with a unicode path
291 # opens a message box which blocks the test run.
292 with copyload_shared_lib(dst_prefix=self.funky_name) as funky_path:
293 def normpath(p):
294 return os.path.realpath(os.path.normcase(p))
295 libpaths = [normpath(x.path)
296 for x in psutil.Process().memory_maps()]
297 # ...just to have a clearer msg in case of failure
298 libpaths = [x for x in libpaths if TESTFILE_PREFIX in x]
299 self.assertIn(normpath(funky_path), libpaths)
300 for path in libpaths:
301 self.assertIsInstance(path, str)
302
303
304 # https://travis-ci.org/giampaolo/psutil/jobs/440073249
305 @unittest.skipIf(PYPY and TRAVIS, "unreliable on PYPY + TRAVIS")
306 @unittest.skipIf(MACOS and TRAVIS, "unreliable on TRAVIS") # TODO
307 @unittest.skipIf(ASCII_FS, "ASCII fs")
308 @unittest.skipIf(not subprocess_supports_unicode(TESTFN_UNICODE),
309 "subprocess can't deal with unicode")
310 class TestFSAPIs(_BaseFSAPIsTests, unittest.TestCase):
311 """Test FS APIs with a funky, valid, UTF8 path name."""
312 funky_name = TESTFN_UNICODE
313
314 @classmethod
315 def expect_exact_path_match(cls):
316 # Do not expect psutil to correctly handle unicode paths on
317 # Python 2 if os.listdir() is not able either.
318 here = '.' if isinstance(cls.funky_name, str) else u('.')
319 with warnings.catch_warnings():
320 warnings.simplefilter("ignore")
321 return cls.funky_name in os.listdir(here)
322
323
324 @unittest.skipIf(PYPY and TRAVIS, "unreliable on PYPY + TRAVIS")
325 @unittest.skipIf(MACOS and TRAVIS, "unreliable on TRAVIS") # TODO
326 @unittest.skipIf(PYPY, "unreliable on PYPY")
327 @unittest.skipIf(not subprocess_supports_unicode(INVALID_NAME),
328 "subprocess can't deal with invalid unicode")
329 class TestFSAPIsWithInvalidPath(_BaseFSAPIsTests, unittest.TestCase):
330 """Test FS APIs with a funky, invalid path name."""
331 funky_name = INVALID_NAME
332
333 @classmethod
334 def expect_exact_path_match(cls):
335 # Invalid unicode names are supposed to work on Python 2.
336 return True
337
338
339 # ===================================================================
340 # Non fs APIs
341 # ===================================================================
342
343
344 class TestNonFSAPIS(unittest.TestCase):
345 """Unicode tests for non fs-related APIs."""
346
347 def tearDown(self):
348 reap_children()
349
350 @unittest.skipIf(not HAS_ENVIRON, "not supported")
351 @unittest.skipIf(PYPY and WINDOWS, "segfaults on PYPY + WINDOWS")
352 def test_proc_environ(self):
353 # Note: differently from others, this test does not deal
354 # with fs paths. On Python 2 subprocess module is broken as
355 # it's not able to handle with non-ASCII env vars, so
356 # we use "è", which is part of the extended ASCII table
357 # (unicode point <= 255).
358 env = os.environ.copy()
359 funky_str = TESTFN_UNICODE if PY3 else 'è'
360 env['FUNNY_ARG'] = funky_str
361 sproc = get_test_subprocess(env=env)
362 p = psutil.Process(sproc.pid)
363 env = p.environ()
364 for k, v in env.items():
365 self.assertIsInstance(k, str)
366 self.assertIsInstance(v, str)
367 self.assertEqual(env['FUNNY_ARG'], funky_str)
368
369
370 if __name__ == '__main__':
371 from psutil.tests.runner import run
372 run(__file__)