#! /usr/bin/env python3 import argparse import inspect import os from pathlib import Path import shutil import subprocess import sys import tempfile from types import FunctionType from typing import NamedTuple from python_appimage.manylinux import PythonVersion ARGS = None def assert_eq(expected, found): if expected != found: raise AssertionError('expected "{}", found "{}"'.format( expected, found)) class Script(NamedTuple): '''Python script wrapper''' content: str def run(self, appimage: Path): '''Run the script through an appimage''' with tempfile.TemporaryDirectory() as tmpdir: script = f'{tmpdir}/script.py' with open(script, 'w') as f: f.write(inspect.getsource(assert_eq)) f.write(os.linesep) f.write(self.content) return system(f'{appimage} {script}') def system(cmd): '''Run a system command''' r = subprocess.run(cmd, capture_output=True, shell=True) if r.returncode != 0: raise ValueError(r.stderr.decode()) else: return r.stdout.decode() class TestContext: '''Context for testing an image''' def __init__(self, appimage): self.appimage = appimage # Guess python version from appimage name. version, _, abi, *_ = appimage.name.split('-', 3) version = version[6:] if abi.endswith('t'): version += '-nogil' self.version = PythonVersion.from_str(version) # Get some specific AppImage env variables. self.env = eval(Script(''' import os appdir = os.environ['APPDIR'] env = {} for var in ('SSL_CERT_FILE', 'TCL_LIBRARY', 'TK_LIBRARY', 'TKPATH'): env[var] = os.environ[var].replace(appdir, '$APPDIR') print(env) ''').run(appimage)) # Extract the AppImage. tmpdir = tempfile.TemporaryDirectory() dst = Path(tmpdir.name) / appimage.name shutil.copy(appimage, dst) system(f'cd {tmpdir.name} && ./{appimage.name} --appimage-extract') self.appdir = Path(tmpdir.name) / 'squashfs-root' self.tmpdir = tmpdir def list_content(self, path=None): '''List the content of an extracted directory''' path = self.appdir if path is None else self.appdir / path return sorted(os.listdir(path)) def run(self): '''Run all tests''' tests = [] for key, value in self.__class__.__dict__.items(): if isinstance(value, FunctionType): if key.startswith('test_'): tests.append(value) n = len(tests) m = max(len(test.__doc__) for test in tests) for i, test in enumerate(tests): sys.stdout.write( f'[ {self.appimage.name} | {i + 1:2}/{n} ] {test.__doc__:{m}}' ) sys.stdout.flush() try: test(self) except Exception as e: sys.stdout.write(f' -> FAILED ({test.__name__}){os.linesep}') sys.stdout.flush() raise e else: sys.stdout.write(f' -> OK{os.linesep}') sys.stdout.flush() def test_root_content(self): '''Check the appimage root content''' content = self.list_content() expected = ['.DirIcon', 'AppRun', 'opt', 'python.png', f'python{self.version.long()}.desktop', 'usr'] assert_eq(expected, content) def test_python_content(self): '''Check the appimage python content''' prefix = f'opt/python{self.version.flavoured()}' content = self.list_content(prefix) assert_eq(['bin', 'include', 'lib'], content) content = self.list_content(f'{prefix}/bin') assert_eq( [f'pip{self.version.short()}', f'python{self.version.flavoured()}'], content ) content = self.list_content(f'{prefix}/include') assert_eq([f'python{self.version.flavoured()}'], content) content = self.list_content(f'{prefix}/lib') assert_eq([f'python{self.version.flavoured()}'], content) def test_system_content(self): '''Check the appimage system content''' content = self.list_content('usr') assert_eq(['bin', 'lib', 'share'], content) content = self.list_content('usr/bin') expected = [ 'pip', f'pip{self.version.major}', f'pip{self.version.short()}', 'python', f'python{self.version.major}', f'python{self.version.short()}' ] assert_eq(expected, content) def test_tcltk_bundling(self): '''Check Tcl/Tk bundling''' for var in ('TCL_LIBRARY', 'TK_LIBRARY', 'TKPATH'): path = Path(self.env[var].replace('$APPDIR', str(self.appdir))) assert path.exists() def test_ssl_bundling(self): '''Check SSL certs bundling''' var = 'SSL_CERT_FILE' path = Path(self.env[var].replace('$APPDIR', str(self.appdir))) assert path.exists() def test_bin_symlinks(self): '''Check /usr/bin symlinks''' assert_eq( (self.appdir / f'opt/python{self.version.flavoured()}/bin/pip{self.version.short()}'), (self.appdir / f'usr/bin/pip{self.version.short()}').resolve() ) assert_eq( f'pip{self.version.short()}', str((self.appdir / f'usr/bin/pip{self.version.major}').readlink()) ) assert_eq( f'pip{self.version.major}', str((self.appdir / 'usr/bin/pip').readlink()) ) assert_eq( f'python{self.version.short()}', str((self.appdir / f'usr/bin/python{self.version.major}').readlink()) ) assert_eq( f'python{self.version.major}', str((self.appdir / 'usr/bin/python').readlink()) ) def test_appimage_hook(self): '''Test the appimage hook''' Script(f''' import os assert_eq(os.environ['APPIMAGE_COMMAND'], '{self.appimage}') import sys assert_eq('{self.appimage}', sys.executable) assert_eq('{self.appimage}', sys._base_executable) ''').run(self.appimage) def test_python_prefix(self): '''Test the python prefix''' Script(f''' import os import sys expected = os.environ["APPDIR"] + '/opt/python{self.version.flavoured()}' assert_eq(expected, sys.prefix) ''').run(self.appimage) def test_ssl_request(self): '''Test SSL request (see issue #24)''' if self.version.major > 2: Script(''' from http import HTTPStatus import urllib.request with urllib.request.urlopen('https://wikipedia.org') as r: assert_eq(r.status, HTTPStatus.OK) ''').run(self.appimage) def test_pip_install(self): '''Test pip installing to an extracted AppImage''' r = system(f'{self.appdir}/AppRun -m pip install pip-install-test') assert('Successfully installed pip-install-test' in r) path = self.appdir / f'opt/python{self.version.flavoured()}/lib/python{self.version.flavoured()}/site-packages/pip_install_test' assert(path.exists()) def test_tkinter_usage(self): '''Test basic tkinter usage''' tkinter = 'tkinter' if self.version.major > 2 else 'Tkinter' Script(f''' import {tkinter} as tkinter tkinter.Tk() ''').run(self.appimage) def test_venv_usage(self): '''Test venv creation''' if self.version.major > 2: system(' && '.join(( f'cd {self.tmpdir.name}', f'./{self.appimage.name} -m venv ENV', '. ENV/bin/activate', ))) python = Path(f'{self.tmpdir.name}/ENV/bin/python') assert_eq(self.appimage.name, str(python.readlink())) def test(): '''Test Python AppImage(s)''' for appimage in ARGS.appimage: context = TestContext(appimage) context.run() if __name__ == '__main__': parser = argparse.ArgumentParser(description = test.__doc__) parser.add_argument('appimage', help = 'path to appimage(s)', nargs = '+', type = lambda x: Path(x).absolute() ) ARGS = parser.parse_args() test()