diff --git a/.azure/build.yml b/.azure/build.yml index ae8f53c4e..1ebe24559 100644 --- a/.azure/build.yml +++ b/.azure/build.yml @@ -15,7 +15,7 @@ trigger: - test/* variables: -# indicate whether the testsuite should skip long running tests or not. +# indicate whether the testsuite should skip long-running tests or not. - name: jpypetest.fast value: 'false' @@ -50,41 +50,59 @@ jobs: dependsOn: Deps strategy: matrix: - linux-3.8: + # Linux + linux-py3.8-jdk8: # todo: 3.8 will be EOL on October 31, 2024 imageName: "ubuntu-latest" python.version: '3.8' - linux-3.9: + jdk.version: '8' + linux-py3.9-jdk11: imageName: "ubuntu-latest" python.version: '3.9' - linux-3.10: + jdk.version: '11' + linux-py3.10-jdk17: imageName: "ubuntu-latest" python.version: '3.10' - linux-3.11: + jdk.version: '17' + linux-py3.11-jdk17: imageName: "ubuntu-latest" python.version: '3.11' - linux-3.12: + jdk.version: '17' + linux-py3.12-jdk17: imageName: "ubuntu-latest" python.version: '3.12' - windows-3.8: + jdk.version: '17' + # Windows + windows-py3.8-jdk8: imageName: "windows-2019" - python.version: '3.8' - windows-3.9: + python.version: '3.8' # todo: 3.8 will be EOL on October 31, 2024 + jdk.version: '8' + windows-py3.9-jdk11: imageName: "windows-2019" python.version: '3.9' - #jpypetest.fast: 'true' - windows-3.10: + jdk.version: '11' + windows-py3.10-jdk8: imageName: "windows-2019" python.version: '3.10' - windows-3.11: + jdk.version: '8' + windows-py3.11-jdk17: imageName: "windows-2019" python.version: '3.11' - windows-3.12: + jdk.version: '17' + windows-py3.12-jdk21: imageName: "windows-2019" python.version: '3.12' - mac-3.9: - imageName: "macos-11" - python.version: '3.9' + jdk.version: '21' + # OSX, we only test an old Python version with JDK8 and recent Py with recent JDK. + mac-py3.8-jdk8: + imageName: "macos-12" + python.version: '3.8' # todo: 3.8 will be EOL on October 31, 2024 jpypetest.fast: 'true' + jdk.version: '8' + mac-py3.12-jdk17: + imageName: "macos-12" + python.version: '3.12' + jpypetest.fast: 'true' + jdk.version: '17' pool: vmImage: $(imageName) @@ -97,13 +115,12 @@ jobs: dependsOn: Deps strategy: matrix: - linux-3.8: + linux-py3.8-jdk11: imageName: "ubuntu-16.04" - jdk_version: "1.11" + jdk.version: "11" python.version: '3.8' pool: vmImage: $(imageName) steps: - template: scripts/deps.yml - template: scripts/debug.yml - diff --git a/.azure/scripts/coverage.yml b/.azure/scripts/coverage.yml index 77c8ae1d8..2f68c2b35 100644 --- a/.azure/scripts/coverage.yml +++ b/.azure/scripts/coverage.yml @@ -28,18 +28,18 @@ steps: bash <(curl -s https://codecov.io/bash) -f coverage.xml -f coverage_py.xml -f coverage_java.xml -X gcov displayName: 'Report' -- task: PublishCodeCoverageResults@1 +- task: PublishCodeCoverageResults@2 inputs: codeCoverageTool: 'JaCoCo' summaryFileLocation: coverage_java.xml pathToSources: native/java -- task: PublishCodeCoverageResults@1 +- task: PublishCodeCoverageResults@2 inputs: codeCoverageTool: 'cobertura' summaryFileLocation: coverage.xml -- task: PublishCodeCoverageResults@1 +- task: PublishCodeCoverageResults@2 inputs: codeCoverageTool: 'cobertura' summaryFileLocation: coverage_py.xml diff --git a/.azure/scripts/debug.yml b/.azure/scripts/debug.yml index 4f8d9ca08..7341ed53c 100644 --- a/.azure/scripts/debug.yml +++ b/.azure/scripts/debug.yml @@ -5,6 +5,10 @@ steps: inputs: versionSpec: '$(python.version)' +- template: jdk.yml + parameters: + version: '$(jdk.version)' + - script: | sudo apt install gdb pip install ./ diff --git a/.azure/scripts/jdk.yml b/.azure/scripts/jdk.yml index d37fd5808..25121dfc2 100644 --- a/.azure/scripts/jdk.yml +++ b/.azure/scripts/jdk.yml @@ -1,14 +1,15 @@ parameters: - name: version type: string - default: 8 + default: '8' steps: -- script: | - set v="##vso[task.setvariable variable=JAVA_HOME]%JAVA_HOME_${{parameters.version}}_X64%" - echo %v:"=% - condition: eq(variables['Agent.OS'], 'Windows_NT') - -- script: | - echo "##vso[task.setvariable variable=JAVA_HOME]$(JAVA_HOME_${{parameters.version}}_X64)" - condition: ne(variables['Agent.OS'], 'Windows_NT') + - task: JavaToolInstaller@0 + inputs: + versionSpec: ${{ parameters.version }} + jdkArchitectureOption: 'x64' + jdkSourceOption: 'PreInstalled' + - bash: | + echo AGENT_JOBSTATUS = $AGENT_JOBSTATUS + if [[ "$AGENT_JOBSTATUS" == "SucceededWithIssues" ]]; then exit 1; fi + displayName: JDK ${{ parameters.version }} set as JAVA_HOME. diff --git a/.azure/scripts/test.yml b/.azure/scripts/test.yml index c67cdcc2a..4a2e3df39 100644 --- a/.azure/scripts/test.yml +++ b/.azure/scripts/test.yml @@ -6,7 +6,7 @@ steps: - template: jdk.yml parameters: - version: 11 + version: '$(jdk.version)' - script: | python -m pip install --upgrade pytest setuptools @@ -25,12 +25,12 @@ steps: - script: | python -m pytest -v --junit-xml=build/test/test.xml test/jpypetest --checkjni - displayName: 'Test JDK 11' + displayName: 'Test JDK $(jdk.version) and Python $(python.version)' condition: eq(variables['jpypetest.fast'], 'false') - script: | python -m pytest -v --junit-xml=build/test/test.xml test/jpypetest --checkjni --fast - displayName: 'Test JDK 11 (fast)' + displayName: 'Test JDK $(jdk.version) and Python $(python.version) (fast)' condition: eq(variables['jpypetest.fast'], 'true') # presence of jpype/ seems to confuse entry_points so `cd` elsewhere @@ -45,5 +45,4 @@ steps: condition: succeededOrFailed() inputs: testResultsFiles: 'build/test/test.xml' - testRunTitle: 'Publish test results for Python $(python.version) with JDK 11' - + testRunTitle: 'Publish test results for Python $(python.version) with JDK $(jdk.version)' diff --git a/AUTHORS.rst b/AUTHORS.rst index 45b48bf96..029f3e03a 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,8 +2,8 @@ Authors ------- The original author: Steve Menard - -Current Maintainer: Luis Nell +Current Lead Developer: Karl Einar Nelson +Current Maintainer: Martin K. Scherer Huge thanks to these CONTRIBUTORS: diff --git a/jpype/_jvmfinder.py b/jpype/_jvmfinder.py index 299d76f32..dcf423345 100644 --- a/jpype/_jvmfinder.py +++ b/jpype/_jvmfinder.py @@ -21,27 +21,25 @@ import sys __all__ = ['getDefaultJVMPath', - 'JVMNotFoundException', 'JVMNotSupportedException'] + 'get_default_jvm_path', + 'JVMNotFoundException', + 'JVMNotSupportedException'] -try: - import winreg -except ImportError: - winreg = None # type: ignore[assignment] +from typing import Sequence, Tuple class JVMNotFoundException(ValueError): - """ Exception raised when no JVM was found in the search path. + """Exception raised when no JVM was found in the search path. This exception is raised when the all of the places searched did not contain a JVM. The locations searched depend on the machine architecture. To avoid this exception specify the JAVA_HOME environment variable as a valid jre or jdk root directory. """ - pass class JVMNotSupportedException(ValueError): - """ Exception raised when the JVM is not supported. + """Exception raised when the JVM is not supported. This exception is raised after a search found a valid Java home directory was found, but the JVM shared library found is not supported. Typically @@ -49,12 +47,10 @@ class JVMNotSupportedException(ValueError): 32 vs 64 bit, or the JVM is older than the version used to compile JPype. """ - pass -def getDefaultJVMPath(): - """ - Retrieves the path to the default or first found JVM library +def getDefaultJVMPath() -> str: + """Retrieves the path to the default or first found JVM library. Returns: The path to the JVM shared library file @@ -74,32 +70,27 @@ def getDefaultJVMPath(): return finder.get_jvm_path() -class JVMFinder(object): - """ - JVM library finder base class - """ +get_default_jvm_path = getDefaultJVMPath - def __init__(self): - """ - Sets up members - """ - # Library file name - self._libfile = "libjvm.so" - # Predefined locations - self._locations = ("/usr/lib/jvm", "/usr/java") +class JVMFinder: + """JVM library finder base class.""" + # Library file name + _libfile: str = "libjvm.so" + # Predefined locations + _locations: Tuple[str, ...] = ("/usr/lib/jvm", "/usr/java") + + def __init__(self): # Search methods self._methods = (self._get_from_java_home, self._get_from_known_locations) def find_libjvm(self, java_home): - """ - Recursively looks for the given file + """Recursively looks for the given file. Parameters: java_home(str): A Java home folder - filename(str): filename: Name of the file to find Returns: The first found file path, or None @@ -130,7 +121,8 @@ def find_libjvm(self, java_home): "environment variable is pointing " "to correct installation.") - def find_possible_homes(self, parents): + @staticmethod + def find_possible_homes(parents): """ Generator that looks for the first-level children folders that could be Java installations, according to their name @@ -249,25 +241,19 @@ def _get_from_known_locations(self): class LinuxJVMFinder(JVMFinder): - """ - Linux JVM library finder class - """ + """Linux JVM library finder class.""" - def __init__(self): - """ - Sets up members - """ - # Call the parent constructor - JVMFinder.__init__(self) + # Java bin file + _java = "/usr/bin/java" - # Java bin file - self._java = "/usr/bin/java" + # Library file name + _libfile = "libjvm.so" - # Library file name - self._libfile = "libjvm.so" + # Predefined locations + _locations = ("/usr/lib/jvm", "/usr/java", "/opt/sun") - # Predefined locations - self._locations = ("/usr/lib/jvm", "/usr/java", "/opt/sun") + def __init__(self): + super().__init__() # Search methods self._methods = (self._get_from_java_home, @@ -296,23 +282,20 @@ class DarwinJVMFinder(LinuxJVMFinder): """ Mac OS X JVM library finder class """ + # Library file name + _libfile = "libjli.dylib" + # Predefined locations + _locations = ('/Library/Java/JavaVirtualMachines',) # type: ignore def __init__(self): """ Sets up members """ - # Call the parent constructor - LinuxJVMFinder.__init__(self) - - # Library file name - self._libfile = "libjli.dylib" + super().__init__() self._methods = list(self._methods) self._methods.append(self._javahome_binary) - # Predefined locations - self._locations = ('/Library/Java/JavaVirtualMachines',) - def _javahome_binary(self): """ for osx > 10.5 we have the nice util /usr/libexec/java_home available. Invoke it and @@ -323,7 +306,8 @@ def _javahome_binary(self): from packaging.version import Version current = Version(platform.mac_ver()[0][:4]) - if current >= Version('10.6') and current < Version('10.9'): + # TODO: check if the java_home tool is still available and fix the version boundaries. + if Version('10.6') <= current: #< Version('10.9'): return subprocess.check_output( ['/usr/libexec/java_home']).strip() @@ -358,25 +342,18 @@ def _checkJVMArch(jvmPath, maxsize=sys.maxsize): raise JVMNotSupportedException("Unable to determine JVM Type") -reg_keys = [r"SOFTWARE\JavaSoft\Java Runtime Environment", - r"SOFTWARE\JavaSoft\JRE", - ] - - class WindowsJVMFinder(JVMFinder): """ Windows JVM library finder class """ + reg_keys = [r"SOFTWARE\JavaSoft\Java Runtime Environment", + r"SOFTWARE\JavaSoft\JRE", + ] + # Library file name + _libfile = "jvm.dll" def __init__(self): - """ - Sets up members - """ - # Call the parent constructor - JVMFinder.__init__(self) - - # Library file name - self._libfile = "jvm.dll" + super().__init__() # Search methods self._methods = (self._get_from_java_home, self._get_from_registry) @@ -384,16 +361,20 @@ def __init__(self): def check(self, jvm): _checkJVMArch(jvm) - def _get_from_registry(self): + @staticmethod + def _get_from_registry(): """ Retrieves the path to the default Java installation stored in the Windows registry :return: The path found in the registry, or None """ - if not winreg: + try: + import winreg + except ImportError: return None - for location in reg_keys: + + for location in WindowsJVMFinder.reg_keys: try: jreKey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, location) cv = winreg.QueryValueEx(jreKey, "CurrentVersion") diff --git a/native/jni_include/jni.h b/native/jni_include/jni.h index a380acf71..04e7dbfc6 100644 --- a/native/jni_include/jni.h +++ b/native/jni_include/jni.h @@ -1179,6 +1179,11 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved); #define JNI_VERSION_1_6 0x00010006 #define JNI_VERSION_1_7 0x00010007 #define JNI_VERSION_1_8 0x00010008 +#define JNI_VERSION_9 0x00090000 +#define JNI_VERSION_10 0x000a0000 +#define JNI_VERSION_19 0x00130000 +#define JNI_VERSION_20 0x00140000 +#define JNI_VERSION_21 0x00150000 #define JNI_OK (0) /* no error */ #define JNI_ERR (-1) /* generic error */ diff --git a/test/jpypetest/common.py b/test/jpypetest/common.py index 05e00e1e2..8f7d45b2d 100644 --- a/test/jpypetest/common.py +++ b/test/jpypetest/common.py @@ -15,13 +15,13 @@ # See NOTICE file for details. # # ***************************************************************************** +from functools import lru_cache import pytest import _jpype import jpype import logging from os import path -import sys import unittest # Extensively used as common.unittest. CLASSPATH = None @@ -145,5 +145,12 @@ def useEqualityFunc(self, func): return UseFunc(self, func, 'assertEqual') -if __name__ == '__main__': - unittest.main() +@lru_cache(1) +def java_version(): + import subprocess + import sys + java_version = str(subprocess.check_output([sys.executable, "-c", + "import jpype; jpype.startJVM(); " + "print(jpype.java.lang.System.getProperty('java.version'))"]), + encoding='ascii') + return tuple(map(int, java_version.split("."))) diff --git a/test/jpypetest/conftest.py b/test/jpypetest/conftest.py index ecdf6ee03..e613f87d9 100755 --- a/test/jpypetest/conftest.py +++ b/test/jpypetest/conftest.py @@ -15,9 +15,9 @@ # See NOTICE file for details. # # ***************************************************************************** + import pytest -import _jpype -import jpype + import common diff --git a/test/jpypetest/test_jvmfinder.py b/test/jpypetest/test_jvmfinder.py index 4e16a7b2e..8c1b3c34d 100644 --- a/test/jpypetest/test_jvmfinder.py +++ b/test/jpypetest/test_jvmfinder.py @@ -16,15 +16,15 @@ # # ***************************************************************************** # part of JPype1; author Martin K. Scherer; 2014 - - +# from unittest import mock +import os +import pathlib +import sys import unittest from unittest import mock -import common -import os -import jpype._jvmfinder -from jpype._jvmfinder import * +from jpype._jvmfinder import (LinuxJVMFinder, JVMNotSupportedException, DarwinJVMFinder, + WindowsJVMFinder) class JVMFinderTest(unittest.TestCase): @@ -52,7 +52,7 @@ def test_find_libjvm(self): # contains broken and working jvms mockwalk.return_value = walk_fake - finder = jpype._jvmfinder.LinuxJVMFinder() + finder = LinuxJVMFinder() p = finder.find_libjvm('arbitrary java home') self.assertEqual( p, os.path.join('jre/lib/amd64/server', 'libjvm.so'), 'wrong jvm returned') @@ -62,7 +62,7 @@ def test_find_libjvm(self): walk_fake[-1] = ((), (), (),) mockwalk.return_value = walk_fake - finder = jpype._jvmfinder.LinuxJVMFinder() + finder = LinuxJVMFinder() with self.assertRaises(JVMNotSupportedException) as context: finder.find_libjvm('arbitrary java home') @@ -81,7 +81,7 @@ def test_get_from_bin(self, mock_real_path, mock_path_exists, mock_os_walk): mock_path_exists.return_value = True mock_real_path.return_value = '/usr/lib/jvm/java-6-openjdk-amd64/bin/java' - finder = jpype._jvmfinder.LinuxJVMFinder() + finder = LinuxJVMFinder() p = finder._get_from_bin() self.assertEqual( @@ -94,7 +94,7 @@ def testDarwinBinary(self, mock_mac_ver): expected = '/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home\n' - finder = jpype._jvmfinder.DarwinJVMFinder() + finder = DarwinJVMFinder() # fake check_output with mock.patch('subprocess.check_output') as mock_checkoutput: @@ -107,29 +107,8 @@ def testDarwinBinary(self, mock_mac_ver): p = finder._javahome_binary() self.assertEqual(p, None) - # FIXME this is testing the details of the implementation rather than the results. - # it is included only for coverage purposes. Revise this to be a more meaningful test - # next time it breaks. - # FIXME this test does passes locally but not in the CI. Disabling for now. - @common.unittest.skip # type: ignore - def testPlatform(self): - with mock.patch('jpype._jvmfinder.sys') as mocksys, mock.patch('jpype._jvmfinder.WindowsJVMFinder') as finder: - mocksys.platform = 'win32' - jpype._jvmfinder.getDefaultJVMPath() - self.assertIn(finder().get_jvm_path, finder.mock_calls) - - with mock.patch('jpype._jvmfinder.sys') as mocksys, mock.patch('jpype._jvmfinder.LinuxJVMFinder') as finder: - mocksys.platform = 'linux' - getDefaultJVMPath() - self.assertIn(finder().get_jvm_path, finder.mock_calls) - - with mock.patch('jpype._jvmfinder.sys') as mocksys, mock.patch('jpype._jvmfinder.DarwinJVMFinder') as finder: - mocksys.platform = 'darwin' - getDefaultJVMPath() - self.assertIn(finder().get_jvm_path, finder.mock_calls) - def testLinuxGetFromBin(self): - finder = jpype._jvmfinder.LinuxJVMFinder() + finder = LinuxJVMFinder() def f(s): return s @@ -144,45 +123,12 @@ def f(s): self.assertEqual( pathmock.dirname.mock_calls[0][1], (finder._java,)) - # FIXME this test is faking files using the mock system. Replace it with stub - # files so that we have a more accurate test rather than just testing the implementation. - # FIXME this fails in the CI but works locally. Disabling this for now. - @common.unittest.skip # type: ignore - def testCheckArch(self): - import struct - with mock.patch("builtins.open", mock.mock_open(read_data="data")) as mock_file, \ - self.assertRaises(JVMNotSupportedException): - jpype._jvmfinder._checkJVMArch('path', 2**32) - - data = struct.pack('