test-appimage.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. #! /usr/bin/env python3
  2. import argparse
  3. import inspect
  4. import os
  5. from pathlib import Path
  6. import shutil
  7. import subprocess
  8. import sys
  9. import tempfile
  10. from types import FunctionType
  11. from typing import NamedTuple
  12. from python_appimage.manylinux import PythonVersion
  13. ARGS = None
  14. def assert_eq(expected, found):
  15. if expected != found:
  16. raise AssertionError('expected "{}", found "{}"'.format(
  17. expected, found))
  18. class Script(NamedTuple):
  19. '''Python script wrapper'''
  20. content: str
  21. def run(self, appimage: Path):
  22. '''Run the script through an appimage'''
  23. with tempfile.TemporaryDirectory() as tmpdir:
  24. script = f'{tmpdir}/script.py'
  25. with open(script, 'w') as f:
  26. f.write(inspect.getsource(assert_eq))
  27. f.write(os.linesep)
  28. f.write(self.content)
  29. return system(f'{appimage} {script}')
  30. def system(cmd):
  31. '''Run a system command'''
  32. r = subprocess.run(cmd, capture_output=True, shell=True)
  33. if r.returncode != 0:
  34. raise ValueError(r.stderr.decode())
  35. else:
  36. return r.stdout.decode()
  37. class TestContext:
  38. '''Context for testing an image'''
  39. def __init__(self, appimage):
  40. self.appimage = appimage
  41. # Guess python version from appimage name.
  42. version, _, abi, *_ = appimage.name.split('-', 3)
  43. version = version[6:]
  44. if abi.endswith('t'):
  45. version += '-nogil'
  46. self.version = PythonVersion.from_str(version)
  47. # Get some specific AppImage env variables.
  48. self.env = eval(Script('''
  49. import os
  50. appdir = os.environ['APPDIR']
  51. env = {}
  52. for var in ('SSL_CERT_FILE', 'TCL_LIBRARY', 'TK_LIBRARY', 'TKPATH'):
  53. env[var] = os.environ[var].replace(appdir, '$APPDIR')
  54. print(env)
  55. ''').run(appimage))
  56. # Extract the AppImage.
  57. tmpdir = tempfile.TemporaryDirectory()
  58. dst = Path(tmpdir.name) / appimage.name
  59. shutil.copy(appimage, dst)
  60. system(f'cd {tmpdir.name} && ./{appimage.name} --appimage-extract')
  61. self.appdir = Path(tmpdir.name) / 'squashfs-root'
  62. self.tmpdir = tmpdir
  63. def list_content(self, path=None):
  64. '''List the content of an extracted directory'''
  65. path = self.appdir if path is None else self.appdir / path
  66. return sorted(os.listdir(path))
  67. def run(self):
  68. '''Run all tests'''
  69. tests = []
  70. for key, value in self.__class__.__dict__.items():
  71. if isinstance(value, FunctionType):
  72. if key.startswith('test_'):
  73. tests.append(value)
  74. n = len(tests)
  75. m = max(len(test.__doc__) for test in tests)
  76. for i, test in enumerate(tests):
  77. sys.stdout.write(
  78. f'[ {self.appimage.name} | {i + 1:2}/{n} ] {test.__doc__:{m}}'
  79. )
  80. sys.stdout.flush()
  81. try:
  82. test(self)
  83. except Exception as e:
  84. sys.stdout.write(f' -> FAILED ({test.__name__}){os.linesep}')
  85. sys.stdout.flush()
  86. raise e
  87. else:
  88. sys.stdout.write(f' -> OK{os.linesep}')
  89. sys.stdout.flush()
  90. def test_root_content(self):
  91. '''Check the appimage root content'''
  92. content = self.list_content()
  93. expected = ['.DirIcon', 'AppRun', 'opt', 'python.png',
  94. f'python{self.version.long()}.desktop', 'usr']
  95. assert_eq(expected, content)
  96. def test_python_content(self):
  97. '''Check the appimage python content'''
  98. prefix = f'opt/python{self.version.flavoured()}'
  99. content = self.list_content(prefix)
  100. assert_eq(['bin', 'include', 'lib'], content)
  101. content = self.list_content(f'{prefix}/bin')
  102. assert_eq(
  103. [f'pip{self.version.short()}', f'python{self.version.flavoured()}'],
  104. content
  105. )
  106. content = self.list_content(f'{prefix}/include')
  107. assert_eq([f'python{self.version.flavoured()}'], content)
  108. content = self.list_content(f'{prefix}/lib')
  109. assert_eq([f'python{self.version.flavoured()}'], content)
  110. def test_system_content(self):
  111. '''Check the appimage system content'''
  112. content = self.list_content('usr')
  113. assert_eq(['bin', 'lib', 'share'], content)
  114. content = self.list_content('usr/bin')
  115. expected = [
  116. 'pip', f'pip{self.version.major}', f'pip{self.version.short()}',
  117. 'python', f'python{self.version.major}',
  118. f'python{self.version.short()}'
  119. ]
  120. assert_eq(expected, content)
  121. def test_tcltk_bundling(self):
  122. '''Check Tcl/Tk bundling'''
  123. for var in ('TCL_LIBRARY', 'TK_LIBRARY', 'TKPATH'):
  124. path = Path(self.env[var].replace('$APPDIR', str(self.appdir)))
  125. assert path.exists()
  126. def test_ssl_bundling(self):
  127. '''Check SSL certs bundling'''
  128. var = 'SSL_CERT_FILE'
  129. path = Path(self.env[var].replace('$APPDIR', str(self.appdir)))
  130. assert path.exists()
  131. def test_bin_symlinks(self):
  132. '''Check /usr/bin symlinks'''
  133. assert_eq(
  134. (self.appdir /
  135. f'opt/python{self.version.flavoured()}/bin/pip{self.version.short()}'),
  136. (self.appdir / f'usr/bin/pip{self.version.short()}').resolve()
  137. )
  138. assert_eq(
  139. f'pip{self.version.short()}',
  140. str((self.appdir / f'usr/bin/pip{self.version.major}').readlink())
  141. )
  142. assert_eq(
  143. f'pip{self.version.major}',
  144. str((self.appdir / 'usr/bin/pip').readlink())
  145. )
  146. assert_eq(
  147. f'python{self.version.short()}',
  148. str((self.appdir / f'usr/bin/python{self.version.major}').readlink())
  149. )
  150. assert_eq(
  151. f'python{self.version.major}',
  152. str((self.appdir / 'usr/bin/python').readlink())
  153. )
  154. def test_appimage_hook(self):
  155. '''Test the appimage hook'''
  156. Script(f'''
  157. import os
  158. assert_eq(os.environ['APPIMAGE_COMMAND'], '{self.appimage}')
  159. import sys
  160. assert_eq('{self.appimage}', sys.executable)
  161. assert_eq('{self.appimage}', sys._base_executable)
  162. ''').run(self.appimage)
  163. def test_python_prefix(self):
  164. '''Test the python prefix'''
  165. Script(f'''
  166. import os
  167. import sys
  168. expected = os.environ["APPDIR"] + '/opt/python{self.version.flavoured()}'
  169. assert_eq(expected, sys.prefix)
  170. ''').run(self.appimage)
  171. def test_ssl_request(self):
  172. '''Test SSL request (see issue #24)'''
  173. if self.version.major > 2:
  174. Script('''
  175. from http import HTTPStatus
  176. import urllib.request
  177. with urllib.request.urlopen('https://wikipedia.org') as r:
  178. assert_eq(r.status, HTTPStatus.OK)
  179. ''').run(self.appimage)
  180. def test_pip_install(self):
  181. '''Test pip installing to an extracted AppImage'''
  182. r = system(f'{self.appdir}/AppRun -m pip install pip-install-test')
  183. assert('Successfully installed pip-install-test' in r)
  184. path = self.appdir / f'opt/python{self.version.flavoured()}/lib/python{self.version.flavoured()}/site-packages/pip_install_test'
  185. assert(path.exists())
  186. def test_tkinter_usage(self):
  187. '''Test basic tkinter usage'''
  188. tkinter = 'tkinter' if self.version.major > 2 else 'Tkinter'
  189. Script(f'''
  190. import {tkinter} as tkinter
  191. tkinter.Tk()
  192. ''').run(self.appimage)
  193. def test_venv_usage(self):
  194. '''Test venv creation'''
  195. if self.version.major > 2:
  196. system(' && '.join((
  197. f'cd {self.tmpdir.name}',
  198. f'./{self.appimage.name} -m venv ENV',
  199. '. ENV/bin/activate',
  200. )))
  201. python = Path(f'{self.tmpdir.name}/ENV/bin/python')
  202. assert_eq(self.appimage.name, str(python.readlink()))
  203. def test():
  204. '''Test Python AppImage(s)'''
  205. for appimage in ARGS.appimage:
  206. context = TestContext(appimage)
  207. context.run()
  208. if __name__ == '__main__':
  209. parser = argparse.ArgumentParser(description = test.__doc__)
  210. parser.add_argument('appimage',
  211. help = 'path to appimage(s)',
  212. nargs = '+',
  213. type = lambda x: Path(x).absolute()
  214. )
  215. ARGS = parser.parse_args()
  216. test()