PageRenderTime 50ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/tests.py

https://bitbucket.org/vinay.sajip/pylauncher/
Python | 533 lines | 467 code | 34 blank | 32 comment | 21 complexity | 8f102f41f9752f467c0fcd379bfc9b5b MD5 | raw file
Possible License(s): BSD-3-Clause
  1. #!python3
  2. # this test requires:
  3. # - python2.x 32bit
  4. # - python2.x 64bit
  5. # - python3.x 32bit
  6. # - python3.x 64bit
  7. # to be installed
  8. import sys
  9. if sys.version_info[0] < 3:
  10. raise ImportError("These tests require Python 3 to run.")
  11. import ctypes
  12. import logging
  13. import os
  14. import os.path
  15. import shutil
  16. import subprocess
  17. import tempfile
  18. import unittest
  19. import winreg
  20. logger = logging.getLogger()
  21. SCRIPT_TEMPLATE='''%(shebang_line)s%(coding_line)simport sys
  22. print(sys.version)
  23. print(sys.argv)
  24. %(comment)s'''
  25. BOM_UTF8 = b'\xEF\xBB\xBF'
  26. LAUNCHER = os.path.join('Debug', 'py.exe')
  27. IS_W = sys.executable.endswith("w.exe")
  28. SUPPORT_VENV_IN_SHEBANG = False
  29. SHEBANGS = {
  30. 'NONE': '',
  31. 'ENV_PY': '#!/usr/bin/env python\n',
  32. 'ENV_PY2': '#!/usr/bin/env python2\n',
  33. 'ENV_PY3': '#!/usr/bin/env python3\n',
  34. 'BIN_PY': '#!/usr/bin/python\n',
  35. 'BIN_PY2': '#!/usr/bin/python2\n',
  36. 'BIN_PY3': '#!/usr/bin/python3\n',
  37. 'LBIN_PY': '#!/usr/local/bin/python\n',
  38. 'LBIN_PY2': '#!/usr/local/bin/python2\n',
  39. 'LBIN_PY3': '#!/usr/local/bin/python3\n',
  40. 'PY': '#!python\n',
  41. 'PY2': '#!python2\n',
  42. 'PY3': '#!python3\n',
  43. }
  44. COMMENT_WITH_UNICODE = '# Libert\xe9, \xe9galit\xe9, fraternit\xe9\n'
  45. VIRT_PATHS = [
  46. '/usr/bin/env ',
  47. '/usr/bin/env ', # test extra whitespace before command
  48. '/usr/bin/',
  49. '/usr/local/bin/',
  50. '',
  51. ]
  52. class VirtualPath: # think a C struct...
  53. def __init__(self, version, bits, executable):
  54. self.version = version
  55. self.bits = bits
  56. self.executable = executable
  57. def is_64_bit_os():
  58. i = ctypes.c_int()
  59. kernel32 = ctypes.windll.kernel32
  60. process = kernel32.GetCurrentProcess()
  61. kernel32.IsWow64Process(process, ctypes.byref(i))
  62. return i.value
  63. def locate_pythons_for_key(root, flags, infos):
  64. executable = 'pythonw.exe' if IS_W else 'python.exe'
  65. python_path = r'SOFTWARE\Python\PythonCore'
  66. try:
  67. core_root = winreg.OpenKeyEx(root, python_path, 0, flags)
  68. except WindowsError:
  69. return
  70. try:
  71. i = 0
  72. while True:
  73. try:
  74. verspec = winreg.EnumKey(core_root, i)
  75. except WindowsError:
  76. break
  77. try:
  78. ip_path = python_path + '\\' + verspec + '\\' + 'InstallPath'
  79. key_installed_path = winreg.OpenKeyEx(root, ip_path, 0, flags)
  80. try:
  81. install_path, typ = winreg.QueryValueEx(key_installed_path,
  82. None)
  83. finally:
  84. winreg.CloseKey(key_installed_path)
  85. if typ==winreg.REG_SZ:
  86. for check in ['', 'pcbuild', 'pcbuild/amd64']:
  87. maybe = os.path.join(install_path, check, executable)
  88. if os.path.isfile(maybe):
  89. if ' ' in maybe:
  90. maybe = '"' + maybe + '"'
  91. infos.append(VirtualPath(verspec, 32, maybe))
  92. #debug("found version %s at '%s'" % (verspec, maybe))
  93. break
  94. except WindowsError:
  95. pass
  96. i += 1
  97. finally:
  98. winreg.CloseKey(core_root)
  99. # Locate all installed Python versions, reverse-sorted by their version
  100. # number - the sorting allows a simplistic linear scan to find the higest
  101. # matching version number.
  102. def locate_all_pythons():
  103. infos = []
  104. if not is_64_bit_os():
  105. locate_pythons_for_key(winreg.HKEY_CURRENT_USER, winreg.KEY_READ,
  106. infos)
  107. locate_pythons_for_key(winreg.HKEY_LOCAL_MACHINE, winreg.KEY_READ,
  108. infos)
  109. else:
  110. locate_pythons_for_key(winreg.HKEY_CURRENT_USER,
  111. winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
  112. infos)
  113. locate_pythons_for_key(winreg.HKEY_LOCAL_MACHINE,
  114. winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
  115. infos)
  116. locate_pythons_for_key(winreg.HKEY_CURRENT_USER,
  117. winreg.KEY_READ | winreg.KEY_WOW64_32KEY,
  118. infos)
  119. locate_pythons_for_key(winreg.HKEY_LOCAL_MACHINE,
  120. winreg.KEY_READ | winreg.KEY_WOW64_32KEY,
  121. infos)
  122. return sorted(infos, reverse=True, key=lambda info: (info.version, -info.bits))
  123. # These are defined later in the main clause.
  124. #ALL_PYTHONS = DEFAULT_PYTHON2 = DEFAULT_PYTHON3 = None
  125. # locate a specific python version - some version must be specified (although
  126. # it may be just a major version)
  127. def locate_python_ver(spec):
  128. assert spec
  129. for info in ALL_PYTHONS:
  130. if info.version.startswith(spec):
  131. return info
  132. return None
  133. def locate_python(spec):
  134. if len(spec)==1:
  135. # just a major version was specified - see if the environment
  136. # has a default for that version.
  137. spec = os.environ.get('PY_DEFAULT_PYTHON'+spec, spec)
  138. if spec:
  139. ret = locate_python_ver(spec)
  140. else:
  141. # No python spec - see if the environment has a default.
  142. spec = os.environ.get('PY_DEFAULT_PYTHON')
  143. if spec:
  144. ret = locate_python_ver(spec)
  145. else:
  146. # hrmph - still no spec - prefer python 2 if installed.
  147. ret = locate_python_ver('2')
  148. if ret is None:
  149. ret = locate_python_ver('3')
  150. # may still be none, but we are out of search options.
  151. logger.debug('located python: %s -> %s', spec, (ret.version, ret.bits, ret.executable))
  152. return ret
  153. def update_for_installed_pythons(*pythons):
  154. for python in pythons:
  155. python.bversion = python.version.encode('ascii')
  156. python.dir = 'Python%s' % python.version.replace('.', '')
  157. python.bdir = python.dir.encode('ascii')
  158. python.output_version = b'Python ' + python.bversion
  159. # Add additional shebangs for the versions we know are present
  160. major = python.version[0]
  161. upd_templates = {
  162. 'ENV_PY%s_MIN': '#!/usr/bin/env python%s\n',
  163. 'ENV_PY%s_MIN_BITS': '#!/usr/bin/env python%s-32\n',
  164. 'BIN_PY%s_MIN': '#!/usr/bin/python%s\n',
  165. 'BIN_PY%s_MIN_BITS': '#!/usr/bin/python%s-32\n',
  166. 'LBIN_PY%s_MIN': '#!/usr/local/bin/python%s\n',
  167. 'LBIN_PY%s_MIN_BITS': '#!/usr/local/bin/python%s-32\n',
  168. 'PY%s_MIN': '#!/usr/local/bin/python%s\n',
  169. 'PY%s_MIN_BITS': '#!/usr/local/bin/python%s-32\n',
  170. }
  171. for k, v in upd_templates.items():
  172. key = k % major
  173. value = v % python.version
  174. assert key not in SHEBANGS # sanity check
  175. SHEBANGS[key] = value
  176. class ScriptMaker:
  177. def setUp(self):
  178. self.work_dir = tempfile.mkdtemp()
  179. logger.debug('Starting test %s', self._testMethodName)
  180. def tearDown(self):
  181. logger.debug('Ending test %s', self._testMethodName)
  182. shutil.rmtree(self.work_dir)
  183. def make_script(self, shebang_line='', coding_line='', encoding='ascii',
  184. bom=b'', comment=''):
  185. script = (SCRIPT_TEMPLATE % locals())
  186. script = script.replace('\r', '').replace('\n',
  187. '\r\n').encode(encoding)
  188. if bom and not script.startswith(bom):
  189. script = bom + script
  190. path = os.path.join(self.work_dir, 'showver.py')
  191. with open(path, 'wb') as f:
  192. f.write(script)
  193. self.last_script = script
  194. logger.debug('Made script: %r' % script)
  195. return path
  196. def save_script(self):
  197. with open('last_failed.py', 'wb') as f:
  198. f.write(self.last_script)
  199. def matches(self, stdout, pyinfo):
  200. result = stdout.startswith(pyinfo.bversion)
  201. if not result:
  202. self.save_script()
  203. logger.debug('Match failed - expected: %s', pyinfo.bversion)
  204. for i, s in enumerate(self.last_streams):
  205. stream = 'stderr' if i else 'stdout'
  206. logger.debug('%s: %r', stream, s)
  207. return result
  208. def is_encoding_error(self, message):
  209. return b'but no encoding declared; see' in message
  210. def run_child(self, path, env=None):
  211. if isinstance(path, (list, tuple)):
  212. cmdline = [LAUNCHER] + list(path)
  213. else:
  214. cmdline = [LAUNCHER, path]
  215. p = subprocess.Popen(cmdline, stdout=subprocess.PIPE,
  216. stderr=subprocess.PIPE, shell=False,
  217. env=env)
  218. stdout, stderr = p.communicate()
  219. self.last_streams = stdout, stderr
  220. return stdout, stderr
  221. def get_python_for_shebang(self, shebang):
  222. if 'python3' in shebang:
  223. result = DEFAULT_PYTHON3
  224. elif not shebang: # changed to return Python 3 in this case
  225. result = DEFAULT_PYTHON3
  226. else:
  227. result = DEFAULT_PYTHON2
  228. return result
  229. def get_coding_line(self, coding):
  230. return '# -*- coding: %s -*-\n' % coding
  231. class BasicTest(ScriptMaker, unittest.TestCase):
  232. def test_help(self):
  233. "Test help invocation"
  234. p = subprocess.Popen([LAUNCHER, '-h'], stdout=subprocess.PIPE,
  235. stderr=subprocess.PIPE)
  236. stdout, stderr = p.communicate()
  237. self.assertTrue(stdout.startswith(b'Python Launcher for Windows'))
  238. self.assertIn(b'The following help text is from Python:\r\n\r\nusage: ', stdout)
  239. def test_version_specifier(self):
  240. """Test that files named like a version specifier do not get
  241. misinterpreted as a version specifier when it does not have a shebang."""
  242. for nohyphen in ['t3', 'x2.6', '_3.1-32']:
  243. with open(nohyphen, 'w') as f:
  244. f.write('import sys\nprint(sys.version)\nprint(sys.argv)')
  245. try:
  246. script = self.make_script(shebang_line='')
  247. p = subprocess.Popen([LAUNCHER, nohyphen, script],
  248. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  249. stdout, stderr = p.communicate()
  250. self.last_streams = stdout, stderr
  251. self.assertTrue(self.matches(stdout, DEFAULT_PYTHON3))
  252. finally:
  253. os.remove(nohyphen)
  254. # Tests with ASCII Python sources
  255. def test_shebang_ascii(self):
  256. "Test shebangs in ASCII files"
  257. for shebang in SHEBANGS.values():
  258. path = self.make_script(shebang_line=shebang)
  259. stdout, stderr = self.run_child(path)
  260. python = self.get_python_for_shebang(shebang)
  261. matches = self.matches(stdout, python)
  262. self.assertTrue(matches, 'Failed for shebang: %s' % shebang)
  263. # Tests with UTF-8 Python sources with no BOM
  264. def test_shebang_utf8_nobom(self):
  265. "Test shebangs in UTF-8 files with no BOM"
  266. for shebang in SHEBANGS.values():
  267. # If there's no Unicode, all should be well
  268. path = self.make_script(shebang_line=shebang, encoding='utf-8')
  269. stdout, stderr = self.run_child(path)
  270. python = self.get_python_for_shebang(shebang)
  271. self.assertTrue(self.matches(stdout, python))
  272. # If there's a Unicode comment with no coding line to alert,
  273. # we should see those errors from the spawned Python
  274. path = self.make_script(shebang_line=shebang, encoding='utf-8',
  275. comment=COMMENT_WITH_UNICODE)
  276. stdout, stderr = self.run_child(path)
  277. # Python3 reads Unicode without BOM as UTF-8
  278. self.assertTrue(self.is_encoding_error(stderr) or '3' in shebang or not shebang)
  279. path = self.make_script(shebang_line=shebang, encoding='utf-8',
  280. comment=COMMENT_WITH_UNICODE,
  281. coding_line=self.get_coding_line('utf-8'))
  282. stdout, stderr = self.run_child(path)
  283. self.assertTrue(self.matches(stdout, python))
  284. # Tests with UTF-8 Python sources with BOM
  285. def test_shebang_utf8_bom(self):
  286. "Test shebangs in UTF-8 files with BOM"
  287. for shebang in SHEBANGS.values():
  288. # If there's no Unicode, all should be well
  289. path = self.make_script(shebang_line=shebang, encoding='utf-8',
  290. bom=BOM_UTF8)
  291. stdout, stderr = self.run_child(path)
  292. python = self.get_python_for_shebang(shebang)
  293. self.assertTrue(self.matches(stdout, python))
  294. # If there's a Unicode comment, we should still be fine as
  295. # there's a BOM
  296. path = self.make_script(shebang_line=shebang, encoding='utf-8',
  297. comment=COMMENT_WITH_UNICODE,
  298. bom=BOM_UTF8)
  299. stdout, stderr = self.run_child(path)
  300. python = self.get_python_for_shebang(shebang)
  301. self.assertTrue(self.matches(stdout, python))
  302. def test_venv(self):
  303. "Test correct operation in a virtualenv"
  304. if not os.path.isdir('venv34'):
  305. raise unittest.SkipTest('a venv is needed for this test')
  306. # Check interactive operation
  307. env = os.environ.copy()
  308. stdout, stderr = self.run_child(['-c', 'import sys; print(sys.version)'], env=env)
  309. # Not true any more: We favour 3 over 2 if not explicitly specified
  310. # self.assertTrue(stdout.startswith(b'2.7'))
  311. env['VIRTUAL_ENV'] = os.path.abspath('venv34')
  312. stdout, stderr = self.run_child(['-c', 'import sys; print(sys.version)'], env=env)
  313. self.assertTrue(stdout.startswith(b'3.4'))
  314. if SUPPORT_VENV_IN_SHEBANG:
  315. for key in ('PY', 'ENV_PY'):
  316. shebang = SHEBANGS[key]
  317. path = self.make_script(shebang_line=shebang, encoding='utf-8')
  318. stdout, stderr = self.run_child(path, env=env)
  319. self.assertEqual(stdout.startswith(b'3.4'), key == 'ENV_PY')
  320. self.assertEqual(stdout.startswith(b'2.7'), key != 'ENV_PY')
  321. def read_data(path):
  322. if not os.path.exists(path):
  323. result = None
  324. else:
  325. with open(path, 'r') as f:
  326. result = f.read()
  327. return result
  328. def write_data(path, value):
  329. with open(path, 'w') as f:
  330. f.write(value)
  331. class ConfiguredScriptMaker(ScriptMaker):
  332. def setUp(self):
  333. ScriptMaker.setUp(self)
  334. self.local_ini = path = os.path.join(os.environ['LOCALAPPDATA'],
  335. 'py.ini')
  336. self.local_config = read_data(path)
  337. self.global_ini = path = os.path.join(os.path.dirname(LAUNCHER),
  338. 'py.ini')
  339. self.global_config = read_data(path)
  340. def tearDown(self):
  341. if self.local_config is not None:
  342. write_data(self.local_ini, self.local_config)
  343. if self.global_config is not None:
  344. write_data(self.global_ini, self.global_config)
  345. ScriptMaker.tearDown(self)
  346. LOCAL_INI_IN = '''[commands]
  347. h3 = {p3.executable} --help
  348. v3 = {p3.executable} --version
  349. v2a = {p2.executable} -v
  350. [defaults]
  351. python=3
  352. python3={p3.version}
  353. '''
  354. GLOBAL_INI_IN = '''[commands]
  355. h2 = {p2.executable} -h
  356. h3 = {p3.executable} -h
  357. v2 = {p2.executable} -V
  358. v3 = {p3.executable} -V
  359. v3a = {p3.executable} -v
  360. shell = cmd /c
  361. [defaults]
  362. python=2
  363. python2={p2.version}
  364. '''
  365. class ConfigurationTest(ConfiguredScriptMaker, unittest.TestCase):
  366. def test_basic(self):
  367. "Test basic configuration"
  368. # We're specifying Python 3 in the local ini...
  369. write_data(self.local_ini, LOCAL_INI)
  370. write_data(self.global_ini, GLOBAL_INI)
  371. shebang = SHEBANGS['PY'] # just 'python' ...
  372. path = self.make_script(shebang_line=shebang)
  373. stdout, stderr = self.run_child(path)
  374. self.assertTrue(self.matches(stdout, DEFAULT_PYTHON3))
  375. # Now zap the local configuration ... should get Python 2
  376. write_data(self.local_ini, '')
  377. stdout, stderr = self.run_child(path)
  378. self.assertTrue(self.matches(stdout, DEFAULT_PYTHON2))
  379. def test_customised(self):
  380. "Test customised commands"
  381. write_data(self.local_ini, LOCAL_INI)
  382. write_data(self.global_ini, GLOBAL_INI)
  383. # Python 3 with help
  384. shebang = '#!h3\n'
  385. path = self.make_script(shebang_line=shebang)
  386. stdout, stderr = self.run_child(path)
  387. self.assertTrue(stdout.startswith(b'usage: '))
  388. # Assumes standard Python installation directory
  389. self.assertIn(DEFAULT_PYTHON3.bdir, stdout)
  390. # Python 2 with help
  391. shebang = '#!h2\n'
  392. path = self.make_script(shebang_line=shebang)
  393. stdout, stderr = self.run_child(path)
  394. self.assertTrue(stdout.startswith(b'usage: '))
  395. # Assumes standard Python installation directory
  396. self.assertIn(DEFAULT_PYTHON2.bdir, stdout)
  397. # Python 3 version
  398. for prefix in VIRT_PATHS:
  399. shebang = '#!%sv3\n' % prefix
  400. path = self.make_script(shebang_line=shebang)
  401. stdout, stderr = self.run_child(path)
  402. self.assertTrue(stdout.startswith(DEFAULT_PYTHON3.output_version) or
  403. stderr.startswith(DEFAULT_PYTHON3.output_version))
  404. # Python 2 version
  405. for prefix in VIRT_PATHS:
  406. shebang = '#!%sv2\n' % prefix
  407. path = self.make_script(shebang_line=shebang)
  408. stdout, stderr = self.run_child(path)
  409. self.assertTrue(stderr.startswith(DEFAULT_PYTHON2.output_version))
  410. VERBOSE_START = b'# installing zipimport hook'
  411. VERBOSE_START_3 = b'import _frozen_importlib # frozen'
  412. # Python 3 with -v
  413. shebang = '#!v3a\n'
  414. path = self.make_script(shebang_line=shebang)
  415. stdout, stderr = self.run_child(path)
  416. self.assertTrue(-1 != stderr.find(VERBOSE_START))
  417. # Assumes standard Python installation directory
  418. self.assertIn(DEFAULT_PYTHON3.bdir, stderr)
  419. # Python 2 with -v
  420. shebang = '#!v2a\n'
  421. path = self.make_script(shebang_line=shebang)
  422. stdout, stderr = self.run_child(path)
  423. self.assertTrue(stderr.startswith(VERBOSE_START))
  424. self.assertIn(DEFAULT_PYTHON2.bdir, stderr)
  425. # Python 2 with -V via cmd.exe /C
  426. shebang = '#!shell %s -V\n' % DEFAULT_PYTHON2.executable
  427. path = self.make_script(shebang_line=shebang)
  428. stdout, stderr = self.run_child(path)
  429. self.assertTrue(stderr.startswith(DEFAULT_PYTHON2.output_version))
  430. shebang = '#!shell %s -v\n' % DEFAULT_PYTHON2.executable
  431. path = self.make_script(shebang_line=shebang)
  432. stdout, stderr = self.run_child(path)
  433. self.assertTrue(stdout.startswith(DEFAULT_PYTHON2.bversion))
  434. self.assertTrue(stderr.startswith(VERBOSE_START))
  435. def test_environment(self):
  436. "Test configuration via the environment"
  437. "Test basic configuration"
  438. # We're specifying Python 3 in the local ini...
  439. write_data(self.local_ini, LOCAL_INI)
  440. write_data(self.global_ini, GLOBAL_INI)
  441. shebang = SHEBANGS['PY'] # just 'python' ...
  442. path = self.make_script(shebang_line=shebang)
  443. stdout, stderr = self.run_child(path)
  444. self.assertTrue(self.matches(stdout, DEFAULT_PYTHON3))
  445. # Now, override in the environment
  446. env = os.environ.copy()
  447. env['PY_PYTHON'] = '2'
  448. stdout, stderr = self.run_child(path, env=env)
  449. self.assertTrue(self.matches(stdout, DEFAULT_PYTHON2))
  450. # And again without the environment change
  451. stdout, stderr = self.run_child(path)
  452. self.assertTrue(self.matches(stdout, DEFAULT_PYTHON3))
  453. if __name__ == '__main__':
  454. logging.basicConfig(filename='tests.log', filemode='w', level=logging.DEBUG,
  455. format='%(lineno)4d %(message)s')
  456. global DEFAULT_PYTHON2, DEFAULT_PYTHON3, ALL_PYTHONS, LOCAL_INI, GLOBAL_INI
  457. ALL_PYTHONS = locate_all_pythons()
  458. DEFAULT_PYTHON2 = locate_python('2')
  459. assert DEFAULT_PYTHON2, "You don't appear to have Python 2 installed"
  460. DEFAULT_PYTHON3 = locate_python('3')
  461. assert DEFAULT_PYTHON3, "You don't appear to have Python 3 installed"
  462. update_for_installed_pythons(DEFAULT_PYTHON2, DEFAULT_PYTHON3)
  463. LOCAL_INI = LOCAL_INI_IN.format(p2=DEFAULT_PYTHON2, p3=DEFAULT_PYTHON3)
  464. GLOBAL_INI = GLOBAL_INI_IN.format(p2=DEFAULT_PYTHON2, p3=DEFAULT_PYTHON3)
  465. unittest.main()