diff --git a/.travis.yml b/.travis.yml index eca32f5c..9f49da0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ env: - JULIA_VERSION=nightly global: - TOXENV=py + - PYJULIA_TEST_INCOMPATIBLE_PYTHONS=/usr/bin/python2 matrix: # Python environment is not functional on OS X include: @@ -29,12 +30,14 @@ matrix: - language: generic env: - PYTHON=python3 + - PYJULIA_TEST_INCOMPATIBLE_PYTHONS=python2 - JULIA_VERSION=1.0 # - JULIA_VERSION=nightly os: osx - language: generic env: - PYTHON=python3 + - PYJULIA_TEST_INCOMPATIBLE_PYTHONS=python2 - JULIA_VERSION=0.6.4 - CROSS_VERSION=1 os: osx @@ -59,6 +62,15 @@ before_script: - julia --color=yes -e 'VERSION >= v"0.7.0-DEV.5183" && using Pkg; Pkg.add("PyCall")' script: + # Point PYJULIA_TEST_INCOMPATIBLE_PYTHONS to incompatible Python + # executable (see: test/test_compatible_exe.py). + - if [ "$PYJULIA_TEST_INCOMPATIBLE_PYTHONS" = "$PYTHON" ]; then + PYJULIA_TEST_INCOMPATIBLE_PYTHONS=""; + elif ! which "$PYJULIA_TEST_INCOMPATIBLE_PYTHONS"; then + PYJULIA_TEST_INCOMPATIBLE_PYTHONS=""; + fi + - echo "$PYJULIA_TEST_INCOMPATIBLE_PYTHONS" + # "py,py27" below would be redundant when the main interpreter is # Python 2.7 but it simplifies the CI setup. - if [ "$CROSS_VERSION" = "1" ]; then diff --git a/appveyor.yml b/appveyor.yml index 11d057f5..37db3a1b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,6 +4,11 @@ environment: TOX_TESTENV_PASSENV: DISTUTILS_USE_SDK MSSdk INCLUDE LIB # https://packaging.python.org/guides/supporting-windows-using-appveyor/#testing-with-tox + # Point PYJULIA_TEST_INCOMPATIBLE_PYTHONS to incompatible Python + # executable (see: test/test_compatible_exe.py). MUST specify + # Python versions NOT listed in the matrix below: + PYJULIA_TEST_INCOMPATIBLE_PYTHONS: python3.7 + # for more python versions have a look at # https://github.com/ogrisel/python-appveyor-demo/blob/master/appveyor.yml matrix: diff --git a/ci/appveyor/win32/python3.7.bat b/ci/appveyor/win32/python3.7.bat new file mode 100644 index 00000000..79b64a14 --- /dev/null +++ b/ci/appveyor/win32/python3.7.bat @@ -0,0 +1 @@ +@C:\Python37\python.exe %* diff --git a/ci/appveyor/win64/python3.7.bat b/ci/appveyor/win64/python3.7.bat new file mode 100644 index 00000000..8b6d1155 --- /dev/null +++ b/ci/appveyor/win64/python3.7.bat @@ -0,0 +1 @@ +@C:\Python37-x64\python.exe %* diff --git a/test/test_compatible_exe.py b/test/test_compatible_exe.py new file mode 100644 index 00000000..9a34d7d6 --- /dev/null +++ b/test/test_compatible_exe.py @@ -0,0 +1,120 @@ +from __future__ import print_function + +import os +import subprocess +import sys +import textwrap + +import pytest + +from .test_core import julia +from julia.core import _enviorn, which + +is_linux = sys.platform.startswith("linux") +is_windows = os.name == "nt" +is_apple = sys.platform == "darwin" + + +def _get_paths(path): + return filter(None, path.split(":")) + + +# Environment variable PYJULIA_TEST_INCOMPATIBLE_PYTHONS is the +# :-separated list of Python executables incompatible with the current +# Python: +incompatible_pythons = _get_paths(os.getenv("PYJULIA_TEST_INCOMPATIBLE_PYTHONS", "")) + + +try: + from types import SimpleNamespace +except ImportError: + from argparse import Namespace as SimpleNamespace + + +def _run_fallback(args, input=None, **kwargs): + process = subprocess.Popen(args, stdin=subprocess.PIPE, **kwargs) + stdout, stderr = process.communicate(input) + retcode = process.wait() + return SimpleNamespace(args=args, stdout=stdout, stderr=stderr, returncode=retcode) + + +try: + from subprocess import run +except ImportError: + run = _run_fallback + + +def runcode(python, code): + """Run `code` in `python`.""" + return run( + [python], + input=textwrap.dedent(code), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + env=dict( + _enviorn, + # Make PyJulia importable: + PYTHONPATH=os.path.dirname(os.path.dirname(os.path.realpath(__file__))), + ), + ) + + +def print_completed_proc(proc): + # Print output (pytest will hide it by default): + print("Ran:", *proc.args) + if proc.stdout: + print("# --- STDOUT from", *proc.args) + print(proc.stdout) + if proc.stderr: + print("# --- STDERR from", *proc.args) + print(proc.stderr) + print("# ---") + + +def is_dynamically_linked(executable): + path = which(executable) + assert os.path.exists(path) + if is_linux and which("ldd"): + proc = run( + ["ldd", path], stdout=subprocess.PIPE, env=_enviorn, universal_newlines=True + ) + print_completed_proc(proc) + return "libpython" in proc.stdout + elif is_apple and which("otool"): + proc = run( + ["otool", "-L", path], + stdout=subprocess.PIPE, + env=_enviorn, + universal_newlines=True, + ) + print_completed_proc(proc) + return "libpython" in proc.stdout or "/Python" in proc.stdout + # TODO: support Windows + return None + + +@pytest.mark.parametrize("python", incompatible_pythons) +def test_incompatible_python(python): + if julia.eval("(VERSION.major, VERSION.minor)") == (0, 6): + # Julia 0.6 implements mixed version + return + + python = which(python) + proc = runcode( + python, + """ + import os + from julia import Julia + Julia(runtime=os.getenv("JULIA_EXE"), debug=True) + """, + ) + print_completed_proc(proc) + + assert proc.returncode == 1 + assert "It seems your Julia and PyJulia setup are not supported." in proc.stderr + dynamic = is_dynamically_linked(python) + if dynamic is True: + assert "`libpython` have to match" in proc.stderr + elif dynamic is False: + assert "is statically linked to libpython" in proc.stderr diff --git a/tox.ini b/tox.ini index 2713953e..c3421a47 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,9 @@ passenv = PYJULIA_TEST_REBUILD JULIA_EXE + # See: test/test_compatible_exe.py + PYJULIA_TEST_INCOMPATIBLE_PYTHONS + # See: https://coveralls-python.readthedocs.io/en/latest/usage/tox.html#travisci TRAVIS TRAVIS_*