diff --git a/.gitignore b/.gitignore index 37dad702..8ad9f55d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,9 +40,6 @@ nosetests.xml # PyCharm .idea/* -# Test files -*test.py - # created by distutils during build process MANIFEST diff --git a/README.md b/README.md index 876e6976..fe95e2d8 100644 --- a/README.md +++ b/README.md @@ -394,7 +394,7 @@ $ tox The full syntax for invoking `tox` is ```shell -[PYJULIA_TEST_REBUILD=yes] [JULIA_EXE=] tox [options] [-- pytest options] +[PYJULIA_TEST_REBUILD=yes] [PYJULIA_TEST_RUNTIME=] tox [options] [-- pytest options] ``` * `PYJULIA_TEST_REBUILD`: *Be careful using this environment @@ -405,14 +405,14 @@ The full syntax for invoking `tox` is also that it does not work if you unconditionally set `PYTHON` environment variable in your Julia startup file. -* `JULIA_EXE`: `julia` executable to be used for testing. +* `PYJULIA_TEST_RUNTIME`: `julia` executable to be used for testing. * Positional arguments after `--` are passed to `pytest`. For example, ```console -$ PYJULIA_TEST_REBUILD=yes JULIA_EXE=~/julia/julia tox -e py37 -- -s +$ PYJULIA_TEST_REBUILD=yes PYJULIA_TEST_RUNTIME=~/julia/julia tox -e py37 -- -s ``` means to execute tests with diff --git a/julia/core.py b/julia/core.py index 2509445d..0e23c6fa 100644 --- a/julia/core.py +++ b/julia/core.py @@ -530,18 +530,22 @@ class LibJulia(BaseLibJulia): An easy way to create a `LibJulia` object is `LibJulia.load`: - >>> api = LibJulia.load() + >>> api = LibJulia.load() # doctest: +SKIP Or, equivalently, - >>> api = LibJulia.load(julia="julia") - >>> api = LibJulia.from_juliainfo(JuliaInfo.load()) + >>> api = LibJulia.load(julia="julia") # doctest: +SKIP + >>> api = LibJulia.from_juliainfo(JuliaInfo.load()) # doctest: +SKIP You can pass a path to the Julia executable using `julia` keyword argument: >>> api = LibJulia.load(julia="PATH/TO/CUSTOM/julia") # doctest: +SKIP + .. Do not run doctest with non-default libjulia.so. + >>> _ = getfixture("julia") + >>> api = get_libjulia() + Path to the system image can be configured before initializing Julia: >>> api.image_file # doctest: +SKIP diff --git a/julia/pytestplugin.py b/julia/pytestplugin.py new file mode 100644 index 00000000..8110dbe1 --- /dev/null +++ b/julia/pytestplugin.py @@ -0,0 +1,34 @@ +from __future__ import print_function, absolute_import + +import pytest + + +def pytest_addoption(parser): + import os + + parser.addoption( + "--no-julia", + action="store_false", + dest="julia", + help="Skip tests that require julia.", + ) + parser.addoption( + "--julia-runtime", + help=""" + Julia executable to be used. Defaults to environment variable + `$PYJULIA_TEST_RUNTIME`. + """, + default=os.getenv("PYJULIA_TEST_RUNTIME", "julia"), + ) + + +@pytest.fixture(scope="session") +def julia(request): + """ pytest fixture for providing a `Julia` instance. """ + if not request.config.getoption("julia"): + pytest.skip("--no-julia is given.") + + from julia.core import Julia + + jl = Julia(runtime=request.config.getoption("julia_runtime"), debug=True) + return jl diff --git a/julia/with_rebuilt.py b/julia/with_rebuilt.py index af1bf47e..0ab1c025 100644 --- a/julia/with_rebuilt.py +++ b/julia/with_rebuilt.py @@ -104,10 +104,10 @@ def main(args=None): variable in your Julia startup file. """) parser.add_argument( - '--julia', default=os.getenv('JULIA_EXE', 'julia'), + '--julia', default=os.getenv('PYJULIA_TEST_RUNTIME', 'julia'), help=""" Julia executable to be used. - Default to the value of environment variable JULIA_EXE if set. + Default to the value of environment variable PYJULIA_TEST_RUNTIME if set. """) parser.add_argument( 'command', nargs='+', diff --git a/setup.py b/setup.py index cea83b68..9abfd390 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,9 @@ def pyload(path): "console_scripts": [ "python-jl = julia.python_jl:main", ], + "pytest11": [ + "pyjulia = julia.pytestplugin", + ], }, # We bundle Julia scripts etc. inside `julia` directory. Thus, # this directory must exist in the file system (not in a zip diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..cf791b86 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,9 @@ +import pytest + + +@pytest.fixture(scope="session") +def Main(julia): + """ pytest fixture for providing a Julia `Main` name space. """ + from julia import Main + + return Main diff --git a/test/test_compatible_exe.py b/test/test_compatible_exe.py index 20ea8deb..5777bf02 100644 --- a/test/test_compatible_exe.py +++ b/test/test_compatible_exe.py @@ -118,8 +118,7 @@ def is_dynamically_linked(executable): @pytest.mark.parametrize("python", incompatible_pythons) -def test_incompatible_python(python): - from .test_core import julia +def test_incompatible_python(python, julia): if julia.eval("(VERSION.major, VERSION.minor)") == (0, 6): # Julia 0.6 implements mixed version @@ -131,7 +130,7 @@ def test_incompatible_python(python): """ import os from julia import Julia - Julia(runtime=os.getenv("JULIA_EXE"), debug=True) + Julia(runtime=os.getenv("PYJULIA_TEST_RUNTIME"), debug=True) """, ) diff --git a/test/test_core.py b/test/test_core.py index fbcdc6c7..59336a80 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -3,11 +3,9 @@ import array import math import subprocess -import unittest -from contextlib import contextmanager from types import ModuleType -from julia import Julia, JuliaError +from julia import JuliaError from julia.core import jl_name, py_name import sys import os @@ -18,133 +16,163 @@ orig_env = os.environ.copy() -julia = Julia(runtime=os.getenv("JULIA_EXE"), debug=True) - - -class JuliaTest(unittest.TestCase): - - def test_call(self): - julia._call('1 + 1') - julia._call('sqrt(2.0)') - - def test_eval(self): - self.assertEqual(2, julia.eval('1 + 1')) - self.assertEqual(math.sqrt(2.0), julia.eval('sqrt(2.0)')) - self.assertEqual(1, julia.eval('PyObject(1)')) - self.assertEqual(1000, julia.eval('PyObject(1000)')) - self.assertEqual((1, 2, 3), julia.eval('PyObject((1, 2, 3))')) - - def test_call_error(self): - msg = "Error with message" - try: - julia._call('error("{}")'.format(msg)) - self.fail('No error?') - except JuliaError as err: - self.assertIn(msg, err.args[0]) - - def test_call_julia_function_with_python_args(self): - self.assertEqual(['A', 'B', 'C'], - list(julia.map(julia.uppercase, - array.array('u', [u'a', u'b', u'c'])))) - self.assertEqual([1.0, 2.0, 3.0], - list(julia.map(julia.floor, [1.1, 2.2, 3.3]))) - self.assertEqual(1.0, julia.cos(0)) - - def test_call_julia_with_python_callable(self): - def add(a, b): - return a + b - self.assertSequenceEqual([1, 4, 9], - list(julia.map(lambda x: x * x, [1, 2, 3]))) - self.assertTrue(all(x == y for x, y in zip([11, 11, 11], - julia.map(lambda x: x + 1, - array.array('I', [10, 10, 10]))))) - self.assertEqual(6, julia.reduce(add, [1, 2, 3])) - - def test_call_python_with_julia_args(self): - self.assertEqual(6, sum(julia.eval('(1, 2, 3)'))) - self.assertEqual([1, 4, 9], list(map(julia.eval("x->x^2"), [1, 2, 3]))) - - def test_import_julia_functions(self): - if (python_version.major < 3 or - (python_version.major == 3 and python_version.minor < 3)): - import julia.sum as julia_sum - self.assertEqual(6, julia_sum([1, 2, 3])) - else: - pass - - def test_import_julia_module_existing_function(self): - from julia import Base - assert Base.mod(2, 2) == 0 - - def test_from_import_existing_julia_function(self): - from julia.Base import divrem - assert divrem(7, 3) == (2, 1) - - def test_import_julia_module_non_existing_name(self): - from julia import Base - try: - Base.spamspamspam - self.fail('No AttributeError') - except AttributeError: - pass - - def test_from_import_non_existing_julia_name(self): - try: - from Base import spamspamspam - except ImportError: - pass - else: - assert not spamspamspam - - def test_julia_module_bang(self): - from julia.Base import Channel, put_b, take_b - chan = Channel(1) - sent = 123 - put_b(chan, sent) - received = take_b(chan) - assert sent == received - - def test_import_julia_submodule(self): - from julia.Base import Enums - assert isinstance(Enums, ModuleType) - - def test_star_import_julia_module(self): - from . import _star_import - _star_import.Enum - - def test_main_module(self): - from julia import Main - Main.x = x = 123456 - assert julia.eval('x') == x - - def test_module_all(self): - from julia import Base - assert 'resize_b' in Base.__all__ - - def test_module_dir(self): - from julia import Base - assert 'resize_b' in dir(Base) - - @pytest.mark.skipif( - "JULIA_EXE" in orig_env, - reason=("cannot be tested with custom Julia executable;" - " JULIA_EXE is set to {}".format(orig_env.get("JULIA_EXE")))) - def test_import_without_setup(self): - command = [sys.executable, '-c', 'from julia import Base'] - print('Executing:', *command) - subprocess.check_call(command, env=orig_env) - - #TODO: this causes a segfault - """ - def test_import_julia_modules(self): - import julia.PyCall as pycall - self.assertEquals(6, pycall.pyeval('2 * 3')) - """ - - def test_jlpy_identity(self): - for name in ['normal', 'resize!']: - self.assertEqual(jl_name(py_name(name)), name) - - def test_pyjl_identity(self): - for name in ['normal', 'resize_b']: - self.assertEqual(py_name(jl_name(name)), name) + + +def test_call(julia): + julia._call("1 + 1") + julia._call("sqrt(2.0)") + + +def test_eval(julia): + assert julia.eval("1 + 1") == 2 + assert julia.eval("sqrt(2.0)") == math.sqrt(2.0) + assert julia.eval("PyObject(1)") == 1 + assert julia.eval("PyObject(1000)") == 1000 + assert julia.eval("PyObject((1, 2, 3))") == (1, 2, 3) + + +def test_call_error(julia): + msg = "Error with message" + with pytest.raises(JuliaError) as excinfo: + julia._call('error("{}")'.format(msg)) + assert msg in str(excinfo.value) + + +def test_call_julia_function_with_python_args(Main): + assert list(Main.map(Main.uppercase, array.array("u", [u"a", u"b", u"c"]))) == [ + "A", + "B", + "C", + ] + assert list(Main.map(Main.floor, [1.1, 2.2, 3.3])) == [1.0, 2.0, 3.0] + assert Main.cos(0) == 1.0 + + +def test_call_julia_with_python_callable(Main): + def add(a, b): + return a + b + + assert list(Main.map(lambda x: x * x, [1, 2, 3])) == [1, 4, 9] + assert all( + x == y + for x, y in zip( + [11, 11, 11], Main.map(lambda x: x + 1, array.array("I", [10, 10, 10])) + ) + ) + assert Main.reduce(add, [1, 2, 3]) == 6 + + +def test_call_python_with_julia_args(julia): + assert sum(julia.eval("(1, 2, 3)")) == 6 + assert list(map(julia.eval("x->x^2"), [1, 2, 3])) == [1, 4, 9] + + +def test_import_julia_functions(julia): + if python_version.major < 3 or ( + python_version.major == 3 and python_version.minor < 3 + ): + import julia.sum as julia_sum + + assert julia_sum([1, 2, 3]) == 6 + else: + pass + + +def test_import_julia_module_existing_function(julia): + from julia import Base + + assert Base.mod(2, 2) == 0 + + +def test_from_import_existing_julia_function(julia): + from julia.Base import divrem + + assert divrem(7, 3) == (2, 1) + + +def test_import_julia_module_non_existing_name(julia): + from julia import Base + + with pytest.raises(AttributeError): + Base.spamspamspam + + +def test_from_import_non_existing_julia_name(julia): + try: + from Base import spamspamspam + except ImportError: + pass + else: + assert not spamspamspam + + +def test_julia_module_bang(julia): + from julia.Base import Channel, put_b, take_b + + chan = Channel(1) + sent = 123 + put_b(chan, sent) + received = take_b(chan) + assert sent == received + + +def test_import_julia_submodule(julia): + from julia.Base import Enums + + assert isinstance(Enums, ModuleType) + + +def test_star_import_julia_module(julia): + from . import _star_import + + _star_import.Enum + + +def test_main_module(julia, Main): + Main.x = x = 123456 + assert julia.eval("x") == x + + +def test_module_all(julia): + from julia import Base + + assert "resize_b" in Base.__all__ + + +def test_module_dir(julia): + from julia import Base + + assert "resize_b" in dir(Base) + + +@pytest.mark.skipif( + "PYJULIA_TEST_RUNTIME" in orig_env, + reason=( + "cannot be tested with custom Julia executable;" + " PYJULIA_TEST_RUNTIME is set to {}".format( + orig_env.get("PYJULIA_TEST_RUNTIME") + ) + ), +) +def test_import_without_setup(): + command = [sys.executable, "-c", "from julia import Base"] + print("Executing:", *command) + subprocess.check_call(command, env=orig_env) + + +# TODO: this causes a segfault +""" +def test_import_julia_modules(julia): + import julia.PyCall as pycall + assert pycall.pyeval('2 * 3') == 6 +""" + + +@pytest.mark.parametrize("name", ["normal", "resize!"]) +def test_jlpy_identity(name): + assert jl_name(py_name(name)) == name + + +@pytest.mark.parametrize("name", ["normal", "resize_b"]) +def test_pyjl_identity(name): + assert py_name(jl_name(name)) == name diff --git a/test/test_juliainfo.py b/test/test_juliainfo.py index ccc10c66..3fbc5711 100644 --- a/test/test_juliainfo.py +++ b/test/test_juliainfo.py @@ -11,7 +11,7 @@ def check_core_juliainfo(jlinfo): def test_juliainfo_normal(): - jlinfo = JuliaInfo.load(os.getenv("JULIA_EXE", "julia")) + jlinfo = JuliaInfo.load(os.getenv("PYJULIA_TEST_RUNTIME", "julia")) check_core_juliainfo(jlinfo) assert os.path.exists(jlinfo.python) # Note: jlinfo.libpython is probably not a full path so we are not @@ -23,7 +23,7 @@ def test_juliainfo_without_pycall(tmpdir): `juliainfo` should not fail even when PyCall.jl is not installed. """ - runtime = os.getenv("JULIA_EXE", "julia") + runtime = os.getenv("PYJULIA_TEST_RUNTIME", "julia") env_var = subprocess.check_output( [runtime, "--startup-file=no", "-e", """ diff --git a/test/test_magic.py b/test/test_magic.py index 70344d4c..aeadfb15 100644 --- a/test/test_magic.py +++ b/test/test_magic.py @@ -1,34 +1,32 @@ from IPython.testing.globalipapp import get_ipython -import julia.magic +from julia import magic +import pytest -def get_julia_magics(): - return julia.magic.JuliaMagics(shell=get_ipython()) +@pytest.fixture +def julia_magics(julia): + return magic.JuliaMagics(shell=get_ipython()) -def test_register_magics(): - julia.magic.load_ipython_extension(get_ipython()) +def test_register_magics(julia): + magic.load_ipython_extension(get_ipython()) -def test_success_line(): - jm = get_julia_magics() - ans = jm.julia('1') +def test_success_line(julia_magics): + ans = julia_magics.julia('1') assert ans == 1 -def test_success_cell(): - jm = get_julia_magics() - ans = jm.julia(None, '2') +def test_success_cell(julia_magics): + ans = julia_magics.julia(None, '2') assert ans == 2 -def test_failure_line(): - jm = get_julia_magics() - ans = jm.julia('pop!([])') +def test_failure_line(julia_magics): + ans = julia_magics.julia('pop!([])') assert ans is None -def test_failure_cell(): - jm = get_julia_magics() - ans = jm.julia(None, '1 += 1') +def test_failure_cell(julia_magics): + ans = julia_magics.julia(None, '1 += 1') assert ans is None diff --git a/test/test_python_jl.py b/test/test_python_jl.py index 373f2690..359f854d 100644 --- a/test/test_python_jl.py +++ b/test/test_python_jl.py @@ -11,7 +11,7 @@ is_windows = os.name == "nt" PYJULIA_TEST_REBUILD = os.environ.get("PYJULIA_TEST_REBUILD", "no") == "yes" -JULIA = os.environ.get("JULIA_EXE") +JULIA = os.environ.get("PYJULIA_TEST_RUNTIME") @pytest.mark.parametrize("args", [ diff --git a/test/test_utils.py b/test/test_utils.py index 13e49a62..2e70003b 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -50,7 +50,7 @@ def test_atexit(): ''' import os from julia import Julia - jl = Julia(runtime=os.getenv("JULIA_EXE"), debug=True) + jl = Julia(runtime=os.getenv("PYJULIA_TEST_RUNTIME"), debug=True) jl_atexit = jl.eval(""" function(f) diff --git a/tox.ini b/tox.ini index a5aa2a9e..a65fbc11 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,7 @@ passenv = # See: julia/with_rebuilt.py PYJULIA_TEST_REBUILD - JULIA_EXE + PYJULIA_TEST_RUNTIME # See: test/test_compatible_exe.py PYJULIA_TEST_INCOMPATIBLE_PYTHONS