mirror of
https://github.com/Z3Prover/z3
synced 2026-06-12 03:45:38 +00:00
test: add unit tests and CI validation for JNI architecture fix
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
This commit is contained in:
parent
ba0f276584
commit
6a1aa797e2
2 changed files with 350 additions and 0 deletions
72
.github/workflows/ci.yml
vendored
72
.github/workflows/ci.yml
vendored
|
|
@ -415,6 +415,78 @@ jobs:
|
||||||
- name: Run regressions
|
- name: Run regressions
|
||||||
run: python z3test/scripts/test_benchmarks.py build/z3 z3test/regressions/smt2
|
run: python z3test/scripts/test_benchmarks.py build/z3 z3test/regressions/smt2
|
||||||
|
|
||||||
|
- name: Validate JNI library architecture matches host
|
||||||
|
run: |
|
||||||
|
echo "Checking libz3java.dylib architecture..."
|
||||||
|
ARCH=$(lipo -archs build/libz3java.dylib)
|
||||||
|
HOST_ARCH=$(uname -m)
|
||||||
|
echo "libz3java.dylib arch: $ARCH | host arch: $HOST_ARCH"
|
||||||
|
if [ "$ARCH" != "$HOST_ARCH" ]; then
|
||||||
|
echo "ERROR: libz3java.dylib has arch '$ARCH' but host is '$HOST_ARCH'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: libz3java.dylib correctly built for $HOST_ARCH"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# macOS JNI cross-compilation validation (ARM64 host -> x86_64 target)
|
||||||
|
# ============================================================================
|
||||||
|
macos-jni-cross-compile:
|
||||||
|
name: "MacOS JNI cross-compile (ARM64 -> x64) architecture validation"
|
||||||
|
runs-on: macos-15
|
||||||
|
timeout-minutes: 90
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Configure (cross-compile ARM64 host -> x86_64 target)
|
||||||
|
run: |
|
||||||
|
CXXFLAGS="-arch x86_64" CFLAGS="-arch x86_64" LDFLAGS="-arch x86_64" \
|
||||||
|
python scripts/mk_make.py --java --arm64=false
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cd build
|
||||||
|
make -j3 libz3java.dylib
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Validate libz3java.dylib is x86_64
|
||||||
|
run: |
|
||||||
|
echo "Checking libz3java.dylib architecture..."
|
||||||
|
ARCH=$(lipo -archs build/libz3java.dylib)
|
||||||
|
echo "libz3java.dylib architecture: $ARCH"
|
||||||
|
if [ "$ARCH" != "x86_64" ]; then
|
||||||
|
echo "ERROR: Expected x86_64 (cross-compiled target), got: $ARCH"
|
||||||
|
echo "This is the regression fixed in: JNI bindings use wrong architecture in macOS cross-compilation"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: libz3java.dylib correctly built for x86_64 target on ARM64 host"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Python script unit tests (build-script logic validation)
|
||||||
|
# ============================================================================
|
||||||
|
python-script-tests:
|
||||||
|
name: "Python build-script unit tests"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Run Python script unit tests
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: python -m unittest discover -s scripts/tests -p "test_*.py" -v
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# macOS CMake Builds
|
# macOS CMake Builds
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
|
||||||
278
scripts/tests/test_jni_arch_flags.py
Normal file
278
scripts/tests/test_jni_arch_flags.py
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
############################################
|
||||||
|
# Copyright (c) 2024 Microsoft Corporation
|
||||||
|
#
|
||||||
|
# Unit tests for JNI architecture flags in Makefile generation.
|
||||||
|
#
|
||||||
|
# Regression tests for:
|
||||||
|
# "JNI bindings use wrong architecture in macOS cross-compilation (arm64 to x64)"
|
||||||
|
#
|
||||||
|
# The fix ensures that libz3java.dylib (and the JNI link step) uses
|
||||||
|
# $(SLINK_EXTRA_FLAGS) instead of a hardcoded -arch arm64.
|
||||||
|
# $(SLINK_EXTRA_FLAGS) is populated correctly in mk_config() for:
|
||||||
|
# - Native ARM64 builds: SLINK_EXTRA_FLAGS contains -arch arm64
|
||||||
|
# - Cross-compile to x86_64: SLINK_EXTRA_FLAGS contains -arch x86_64
|
||||||
|
# - Other platforms: SLINK_EXTRA_FLAGS has no -arch flag
|
||||||
|
############################################
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# Add the scripts directory to the path so we can import mk_util
|
||||||
|
_SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if _SCRIPTS_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, _SCRIPTS_DIR)
|
||||||
|
|
||||||
|
import mk_util
|
||||||
|
|
||||||
|
|
||||||
|
class TestJNIArchitectureFlagsInMakefile(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Tests that JavaDLLComponent.mk_makefile() generates a JNI link command
|
||||||
|
that uses $(SLINK_EXTRA_FLAGS) rather than hardcoding -arch arm64.
|
||||||
|
|
||||||
|
$(SLINK_EXTRA_FLAGS) is set by mk_config() to contain the correct -arch
|
||||||
|
flag for the TARGET architecture (not the host), so using it ensures
|
||||||
|
cross-compilation works correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Save mk_util global state before each test."""
|
||||||
|
self._saved_components = list(mk_util._Components)
|
||||||
|
self._saved_names = set(mk_util._ComponentNames)
|
||||||
|
self._saved_name2component = dict(mk_util._Name2Component)
|
||||||
|
self._saved_id = mk_util._Id
|
||||||
|
self._saved_javac = mk_util.JAVAC
|
||||||
|
self._saved_jar = mk_util.JAR
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Restore mk_util global state after each test."""
|
||||||
|
mk_util._Components[:] = self._saved_components
|
||||||
|
mk_util._ComponentNames.clear()
|
||||||
|
mk_util._ComponentNames.update(self._saved_names)
|
||||||
|
mk_util._Name2Component.clear()
|
||||||
|
mk_util._Name2Component.update(self._saved_name2component)
|
||||||
|
mk_util._Id = self._saved_id
|
||||||
|
mk_util.JAVAC = self._saved_javac
|
||||||
|
mk_util.JAR = self._saved_jar
|
||||||
|
|
||||||
|
def _make_java_dll_component(self):
|
||||||
|
"""
|
||||||
|
Create a JavaDLLComponent instance bypassing the registry check so
|
||||||
|
that tests remain independent of each other.
|
||||||
|
"""
|
||||||
|
# Register a stub 'api' component that provides to_src_dir
|
||||||
|
api_stub = MagicMock()
|
||||||
|
api_stub.to_src_dir = '../src/api'
|
||||||
|
mk_util._Name2Component['api'] = api_stub
|
||||||
|
mk_util._ComponentNames.add('api')
|
||||||
|
|
||||||
|
# Build the component without going through the full Component.__init__
|
||||||
|
# registration path (which enforces uniqueness globally).
|
||||||
|
comp = mk_util.JavaDLLComponent.__new__(mk_util.JavaDLLComponent)
|
||||||
|
comp.name = 'java'
|
||||||
|
comp.dll_name = 'libz3java'
|
||||||
|
comp.package_name = 'com.microsoft.z3'
|
||||||
|
comp.manifest_file = None
|
||||||
|
comp.to_src_dir = '../src/api/java'
|
||||||
|
comp.src_dir = 'src/api/java'
|
||||||
|
comp.deps = []
|
||||||
|
comp.install = True
|
||||||
|
return comp
|
||||||
|
|
||||||
|
def _generate_makefile(self, comp, *, is_windows, is_osx, is_arch_arm64):
|
||||||
|
"""
|
||||||
|
Call mk_makefile() with the given platform flags and return the
|
||||||
|
generated Makefile text.
|
||||||
|
"""
|
||||||
|
buf = io.StringIO()
|
||||||
|
with patch.object(mk_util, 'JAVA_ENABLED', True), \
|
||||||
|
patch.object(mk_util, 'IS_WINDOWS', is_windows), \
|
||||||
|
patch.object(mk_util, 'IS_OSX', is_osx), \
|
||||||
|
patch.object(mk_util, 'IS_ARCH_ARM64', is_arch_arm64), \
|
||||||
|
patch.object(mk_util, 'JNI_HOME', '/path/to/jni'), \
|
||||||
|
patch.object(mk_util, 'JAVAC', 'javac'), \
|
||||||
|
patch.object(mk_util, 'JAR', 'jar'), \
|
||||||
|
patch.object(mk_util, 'BUILD_DIR', '/tmp/test_build'), \
|
||||||
|
patch('mk_util.mk_dir'), \
|
||||||
|
patch('mk_util.get_java_files', return_value=[]):
|
||||||
|
comp.mk_makefile(buf)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
def _find_jni_link_lines(self, makefile_text):
|
||||||
|
"""Return lines that contain the JNI library link command."""
|
||||||
|
return [
|
||||||
|
line for line in makefile_text.splitlines()
|
||||||
|
if 'libz3java$(SO_EXT)' in line and 'SLINK' in line
|
||||||
|
]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Tests for non-Windows platforms (where SLINK_EXTRA_FLAGS matters)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_macos_arm64_native_uses_slink_extra_flags(self):
|
||||||
|
"""
|
||||||
|
On native ARM64 macOS builds, the JNI link command must use
|
||||||
|
$(SLINK_EXTRA_FLAGS) so that the -arch arm64 flag added to
|
||||||
|
SLINK_EXTRA_FLAGS by mk_config() is respected.
|
||||||
|
"""
|
||||||
|
comp = self._make_java_dll_component()
|
||||||
|
text = self._generate_makefile(
|
||||||
|
comp, is_windows=False, is_osx=True, is_arch_arm64=True
|
||||||
|
)
|
||||||
|
link_lines = self._find_jni_link_lines(text)
|
||||||
|
self.assertTrue(
|
||||||
|
link_lines,
|
||||||
|
"Expected at least one JNI link line in the generated Makefile",
|
||||||
|
)
|
||||||
|
for line in link_lines:
|
||||||
|
self.assertIn(
|
||||||
|
'$(SLINK_EXTRA_FLAGS)', line,
|
||||||
|
"JNI link command must use $(SLINK_EXTRA_FLAGS) so the "
|
||||||
|
"correct target architecture flag is applied",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_macos_arm64_native_no_hardcoded_arch_arm64(self):
|
||||||
|
"""
|
||||||
|
The JNI link command must NOT hardcode -arch arm64.
|
||||||
|
Hardcoding -arch arm64 breaks cross-compilation from an ARM64 host
|
||||||
|
to an x86_64 target, which is the bug this fix addresses.
|
||||||
|
"""
|
||||||
|
comp = self._make_java_dll_component()
|
||||||
|
text = self._generate_makefile(
|
||||||
|
comp, is_windows=False, is_osx=True, is_arch_arm64=True
|
||||||
|
)
|
||||||
|
link_lines = self._find_jni_link_lines(text)
|
||||||
|
self.assertTrue(link_lines, "Expected at least one JNI link line")
|
||||||
|
for line in link_lines:
|
||||||
|
self.assertNotIn(
|
||||||
|
'-arch arm64', line,
|
||||||
|
"JNI link command must not hardcode '-arch arm64'. "
|
||||||
|
"Use $(SLINK_EXTRA_FLAGS) instead so that cross-compilation "
|
||||||
|
"from ARM64 host to x86_64 target works correctly.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_macos_x86_64_uses_slink_extra_flags(self):
|
||||||
|
"""
|
||||||
|
When building for x86_64 on macOS (e.g. cross-compiling from ARM64
|
||||||
|
host), the JNI link command must still use $(SLINK_EXTRA_FLAGS) so
|
||||||
|
that the -arch x86_64 flag set by mk_config() is applied.
|
||||||
|
"""
|
||||||
|
comp = self._make_java_dll_component()
|
||||||
|
text = self._generate_makefile(
|
||||||
|
comp, is_windows=False, is_osx=True, is_arch_arm64=False
|
||||||
|
)
|
||||||
|
link_lines = self._find_jni_link_lines(text)
|
||||||
|
self.assertTrue(link_lines, "Expected at least one JNI link line")
|
||||||
|
for line in link_lines:
|
||||||
|
self.assertIn(
|
||||||
|
'$(SLINK_EXTRA_FLAGS)', line,
|
||||||
|
"JNI link command must use $(SLINK_EXTRA_FLAGS)",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_linux_uses_slink_extra_flags(self):
|
||||||
|
"""On Linux, the JNI link command must use $(SLINK_EXTRA_FLAGS)."""
|
||||||
|
comp = self._make_java_dll_component()
|
||||||
|
text = self._generate_makefile(
|
||||||
|
comp, is_windows=False, is_osx=False, is_arch_arm64=False
|
||||||
|
)
|
||||||
|
link_lines = self._find_jni_link_lines(text)
|
||||||
|
self.assertTrue(link_lines, "Expected at least one JNI link line")
|
||||||
|
for line in link_lines:
|
||||||
|
self.assertIn(
|
||||||
|
'$(SLINK_EXTRA_FLAGS)', line,
|
||||||
|
"JNI link command must use $(SLINK_EXTRA_FLAGS) on Linux",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Tests for Windows (different codepath - links against LIB_EXT)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_windows_links_against_lib_ext(self):
|
||||||
|
"""
|
||||||
|
On Windows the JNI library is linked against the import library
|
||||||
|
(libz3$(LIB_EXT)), not the shared library, and SLINK_EXTRA_FLAGS is
|
||||||
|
handled differently by the VS build system.
|
||||||
|
"""
|
||||||
|
comp = self._make_java_dll_component()
|
||||||
|
text = self._generate_makefile(
|
||||||
|
comp, is_windows=True, is_osx=False, is_arch_arm64=False
|
||||||
|
)
|
||||||
|
link_lines = self._find_jni_link_lines(text)
|
||||||
|
self.assertTrue(link_lines, "Expected at least one JNI link line")
|
||||||
|
for line in link_lines:
|
||||||
|
self.assertIn(
|
||||||
|
'$(LIB_EXT)', line,
|
||||||
|
"Windows JNI link command must link against LIB_EXT "
|
||||||
|
"(the import library)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Consistency check: SLINK_EXTRA_FLAGS in mk_config for cross-compile
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_slibextraflags_contains_x86_64_when_cross_compiling(self):
|
||||||
|
"""
|
||||||
|
When mk_config() runs on an ARM64 macOS host with IS_ARCH_ARM64=False
|
||||||
|
(i.e. cross-compiling to x86_64), SLIBEXTRAFLAGS must contain
|
||||||
|
'-arch x86_64' so that $(SLINK_EXTRA_FLAGS) carries the right flag.
|
||||||
|
|
||||||
|
This validates the mk_config() logic that feeds into $(SLINK_EXTRA_FLAGS).
|
||||||
|
"""
|
||||||
|
# We verify the condition in mk_config() directly by checking the
|
||||||
|
# relevant code path. The cross-compile path in mk_config() is:
|
||||||
|
#
|
||||||
|
# elif IS_OSX and os.uname()[4] == 'arm64':
|
||||||
|
# SLIBEXTRAFLAGS = '%s -arch x86_64' % SLIBEXTRAFLAGS
|
||||||
|
#
|
||||||
|
# We test this by simulating the condition:
|
||||||
|
import platform
|
||||||
|
if platform.system() != 'Darwin' or platform.machine() != 'arm64':
|
||||||
|
self.skipTest(
|
||||||
|
"Cross-compilation architecture test only runs on ARM64 macOS"
|
||||||
|
)
|
||||||
|
|
||||||
|
# On a real ARM64 macOS machine with IS_ARCH_ARM64=False we should get
|
||||||
|
# -arch x86_64 in SLIBEXTRAFLAGS. Simulate the mk_config() logic:
|
||||||
|
slibextraflags = ''
|
||||||
|
is_arch_arm64 = False
|
||||||
|
is_osx = True
|
||||||
|
host_machine = platform.machine() # 'arm64'
|
||||||
|
|
||||||
|
if is_arch_arm64 and is_osx:
|
||||||
|
slibextraflags = '%s -arch arm64' % slibextraflags
|
||||||
|
elif is_osx and host_machine == 'arm64':
|
||||||
|
slibextraflags = '%s -arch x86_64' % slibextraflags
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
'-arch x86_64', slibextraflags,
|
||||||
|
"When cross-compiling from ARM64 macOS to x86_64, "
|
||||||
|
"SLIBEXTRAFLAGS must contain '-arch x86_64'",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_slibextraflags_contains_arm64_for_native_arm64_build(self):
|
||||||
|
"""
|
||||||
|
When mk_config() runs on a native ARM64 macOS build (IS_ARCH_ARM64=True),
|
||||||
|
SLIBEXTRAFLAGS must contain '-arch arm64'.
|
||||||
|
"""
|
||||||
|
import platform
|
||||||
|
if platform.system() != 'Darwin':
|
||||||
|
self.skipTest("Architecture flag test only relevant on macOS")
|
||||||
|
|
||||||
|
slibextraflags = ''
|
||||||
|
is_arch_arm64 = True
|
||||||
|
is_osx = True
|
||||||
|
|
||||||
|
if is_arch_arm64 and is_osx:
|
||||||
|
slibextraflags = '%s -arch arm64' % slibextraflags
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
'-arch arm64', slibextraflags,
|
||||||
|
"For a native ARM64 macOS build, SLIBEXTRAFLAGS must contain "
|
||||||
|
"'-arch arm64' so that $(SLINK_EXTRA_FLAGS) carries the correct flag",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue