""" ONLY BASIC TEST SET. The additional ones are in test_pythonpackage.py. These are in a separate file because these were picked to run in github-actions, while the other additional ones aren't (for build time reasons). """ import os import shutil import sys import subprocess import tempfile import textwrap from unittest import mock from pythonforandroid.pythonpackage import ( _extract_info_from_package, get_dep_names_of_package, get_package_name, _get_system_python_executable, is_filesystem_path, parse_as_folder_reference, transform_dep_for_pip, ) def local_repo_folder(): return os.path.abspath(os.path.join( os.path.dirname(__file__), ".." )) def fake_metadata_extract(dep_name, output_folder, debug=False): # Helper function to write out fake metadata. with open(os.path.join(output_folder, "METADATA"), "w") as f: f.write(textwrap.dedent("""\ Metadata-Version: 2.1 Name: testpackage Version: 0.1 Requires-Dist: testpkg Requires-Dist: testpkg2 Lorem Ipsum""" )) with open(os.path.join(output_folder, "metadata_source"), "w") as f: f.write(u"wheel") # since we have no pyproject.toml def test__extract_info_from_package(): import pythonforandroid.pythonpackage # noqa with mock.patch("pythonforandroid.pythonpackage." "extract_metainfo_files_from_package", fake_metadata_extract): assert _extract_info_from_package( "whatever", extract_type="name" ) == "testpackage" assert set(_extract_info_from_package( "whatever", extract_type="dependencies" )) == {"testpkg", "testpkg2"} def test_get_package_name(): # TEST 1 from external ref with mock.patch("pythonforandroid.pythonpackage." "extract_metainfo_files_from_package", fake_metadata_extract): assert get_package_name("TeStPackaGe") == "testpackage" # TEST 2 from a local folder, for which we'll create a fake package: temp_d = tempfile.mkdtemp(prefix="p4a-pythonpackage-test-tmp-") try: with open(os.path.join(temp_d, "setup.py"), "w") as f: f.write(textwrap.dedent("""\ from setuptools import setup setup(name="testpackage") """ )) pkg_name = get_package_name(temp_d) assert pkg_name == "testpackage" finally: shutil.rmtree(temp_d) def test_get_dep_names_of_package(): # TEST 1 from external ref: # Check that colorama is returned without the install condition when # just getting the names (it has a "; ..." conditional originally): dep_names = get_dep_names_of_package("python-for-android") assert "colorama" in dep_names assert "setuptools" not in dep_names try: dep_names = get_dep_names_of_package( "python-for-android", include_build_requirements=True, verbose=True, ) except NotImplementedError as e: # If python-for-android was fetched as wheel then build requirements # cannot be obtained (since that is not implemented for wheels). # Check for the correct error message: assert "wheel" in str(e) # (And yes it would be better to do a local test with something # that is guaranteed to be a wheel and not remote on pypi, # but that might require setting up a full local pypiserver. # Not worth the test complexity for now, but if anyone has an # idea in the future feel free to replace this subtest.) else: # We managed to obtain build requirements! # Check setuptools is in here: assert "setuptools" in dep_names # TEST 2 from local folder: assert "colorama" in get_dep_names_of_package(local_repo_folder()) # Now test that exact version pins are kept, but others aren't: test_fake_package = tempfile.mkdtemp() try: with open(os.path.join(test_fake_package, "setup.py"), "w") as f: f.write(textwrap.dedent("""\ from setuptools import setup setup(name='fakeproject', description='fake for testing', install_requires=['buildozer==0.39', 'python-for-android>=0.5.1'], ) """)) # See that we get the deps with the exact version pin kept but # the other version restriction gone: assert set(get_dep_names_of_package( test_fake_package, recursive=False, keep_version_pins=True, verbose=True )) == {"buildozer==0.39", "python-for-android"} # Make sure we also can get the fully cleaned up variant: assert set(get_dep_names_of_package( test_fake_package, recursive=False, keep_version_pins=False, verbose=True )) == {"buildozer", "python-for-android"} # Test with build requirements included: dep_names = get_dep_names_of_package( test_fake_package, recursive=False, keep_version_pins=False, verbose=True, include_build_requirements=True ) assert len( {"buildozer", "python-for-android", "setuptools"}.intersection( dep_names ) ) == 3 # all three must be included finally: shutil.rmtree(test_fake_package) def test_transform_dep_for_pip(): # A reminder, this entire function we test here is just a workaround # for https://github.com/pypa/pip/issues/6097 (and not a nice one.) # As soon as upstream fixes it, we should throw it & this test out transformed = ( transform_dep_for_pip( "python-for-android @ https://github.com/kivy/" + "python-for-android/archive/master.zip" ), transform_dep_for_pip( "python-for-android @ https://github.com/kivy/" + "python-for-android/archive/master.zip" + "#egg=python-for-android-master" ), transform_dep_for_pip( "python-for-android @ https://github.com/kivy/" + "python-for-android/archive/master.zip" + "#" # common hack variant used by others to make pip parse it ), ) expected = ( "https://github.com/kivy/python-for-android/archive/master.zip" + "#egg=python-for-android" ) assert transformed == (expected, expected, expected) assert transform_dep_for_pip("https://a@b/") == "https://a@b/" def test_is_filesystem_path(): assert is_filesystem_path("/some/test") assert not is_filesystem_path("https://blubb") assert not is_filesystem_path("test @ bla") assert is_filesystem_path("/abc/c@d") assert not is_filesystem_path("https://user:pw@host/") assert is_filesystem_path(".") assert is_filesystem_path("") def test_parse_as_folder_reference(): assert parse_as_folder_reference("file:///a%20test") == "/a test" assert parse_as_folder_reference("https://github.com") is None assert parse_as_folder_reference("/a/folder") == "/a/folder" assert parse_as_folder_reference("test @ /abc") == "/abc" assert parse_as_folder_reference("test @ https://bla") is None class TestGetSystemPythonExecutable(): """ This contains all tests for _get_system_python_executable(). ULTRA IMPORTANT THING TO UNDERSTAND: (if you touch this) This code runs things with other python interpreters NOT in the tox environment/virtualenv. E.g. _get_system_python_executable() is outside in the regular host environment! That also means all dependencies can be possibly not present! This is kind of absurd that we need this to run the test at all, but we can't test this inside tox's virtualenv: """ def test_basic(self): # Tests function inside tox env with no further special setup. # Get system-wide python bin: pybin = _get_system_python_executable() # The python binary needs to match our major version to be correct: pyversion = subprocess.check_output([ pybin, "-c", "import sys; print(sys.version)" ], stderr=subprocess.STDOUT).decode("utf-8", "replace") assert pyversion.strip() == sys.version.strip() def run__get_system_python_executable(self, pybin): """ Helper function to run our function. We want to see what _get_system_python_executable() does given a specific python, so we need to make it import it and run it, with that TARGET python, which this function does. """ cmd = [ pybin, "-c", "import importlib\n" "import json\n" "import os\n" "import sys\n" "sys.path = [os.path.dirname(sys.argv[1])] + sys.path\n" "m = importlib.import_module(\n" " os.path.basename(sys.argv[1]).partition('.')[0]\n" ")\n" "print(m._get_system_python_executable())", os.path.join(os.path.dirname(__file__), "..", "pythonforandroid", "pythonpackage.py"), ] # Actual call to python: try: return subprocess.check_output( cmd, stderr=subprocess.STDOUT ).decode("utf-8", "replace").strip() except subprocess.CalledProcessError as e: raise RuntimeError("call failed, with output: " + str(e.output)) def test_systemwide_python(self): # Get system-wide python bin seen from here first: pybin = _get_system_python_executable() # (this call was not a test, we really just need the path here) # Check that in system-wide python, the system-wide python is returned: # IMPORTANT: understand that this runs OUTSIDE of any virtualenv. try: p1 = os.path.normpath( self.run__get_system_python_executable(pybin) ) p2 = os.path.normpath(pybin) assert p1 == p2 except RuntimeError as e: # (remember this is not in a virtualenv) # Some deps may not be installed, so we just avoid to raise # an exception here, as a missing dep should not make the test # fail. if "pep517" in str(e.args): # System python probably doesn't have pep517 available! pass elif "toml" in str(e.args): # System python probably doesn't have toml available! pass else: raise def test_venv(self): """ Verifies that _get_system_python_executable() works correctly in a 'venv' (Python 3 only feature). """ # Get system-wide python bin seen from here first: pybin = _get_system_python_executable() # (this call was not a test, we really just need the path here) test_dir = tempfile.mkdtemp() try: # Check that in a venv/pyvenv, the system-wide python is returned: subprocess.check_output([ pybin, "-m", "venv", "--", os.path.join(test_dir, "venv") ]) subprocess.check_output([ os.path.join(test_dir, "venv", "bin", "pip"), "install", "-U", "pip" ]) subprocess.check_output([ os.path.join(test_dir, "venv", "bin", "pip"), "install", "-U", "pep517" ]) subprocess.check_output([ os.path.join(test_dir, "venv", "bin", "pip"), "install", "-U", "toml" ]) sys_python_path = self.run__get_system_python_executable( os.path.join(test_dir, "venv", "bin", "python") ) assert os.path.normpath(sys_python_path).startswith( os.path.normpath(pybin) ) finally: shutil.rmtree(test_dir)