Skip to content

Commit 7e9cca6

Browse files
committed
Add test for loading libraries via ctypes
1 parent 16a9db0 commit 7e9cca6

File tree

3 files changed

+139
-2
lines changed

3 files changed

+139
-2
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
2+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
3+
#
4+
# The Universal Permissive License (UPL), Version 1.0
5+
#
6+
# Subject to the condition set forth below, permission is hereby granted to any
7+
# person obtaining a copy of this software, associated documentation and/or
8+
# data (collectively the "Software"), free of charge and under any and all
9+
# copyright rights in the Software, and any and all patent rights owned or
10+
# freely licensable by each licensor hereunder covering either (i) the
11+
# unmodified Software as contributed to or provided by such licensor, or (ii)
12+
# the Larger Works (as defined below), to deal in both
13+
#
14+
# (a) the Software, and
15+
#
16+
# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
17+
# one is included with the Software each a "Larger Work" to which the Software
18+
# is contributed by such licensors),
19+
#
20+
# without restriction, including without limitation the rights to copy, create
21+
# derivative works of, display, perform, and distribute the Software and make,
22+
# use, sell, offer for sale, import, export, have made, and have sold the
23+
# Software and the Larger Work(s), and to sublicense the foregoing rights on
24+
# either these or other terms.
25+
#
26+
# This license is subject to the following condition:
27+
#
28+
# The above copyright notice and either this complete permission notice or at a
29+
# minimum a reference to the UPL must be included in all copies or substantial
30+
# portions of the Software.
31+
#
32+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38+
# SOFTWARE.
39+
40+
import shutil
41+
import subprocess
42+
import sys
43+
import tempfile
44+
import textwrap
45+
import unittest
46+
from pathlib import Path
47+
48+
from tests.testlib_helper import build_testlib
49+
50+
51+
class TestCtypesInterop(unittest.TestCase):
52+
@classmethod
53+
def setUpClass(cls):
54+
orig_root = Path(__file__).parent.resolve()
55+
orig_testlib = orig_root / "testlib"
56+
cls.tmpdir = Path(tempfile.mkdtemp(prefix="testctypes_tmp_"))
57+
cls.lib_path = build_testlib(cls.tmpdir, orig_testlib)
58+
59+
@classmethod
60+
def tearDownClass(cls):
61+
shutil.rmtree(cls.tmpdir, ignore_errors=True)
62+
63+
def run_in_subprocess(self, code, *args):
64+
proc = subprocess.run(
65+
[sys.executable, "-c", code, *args],
66+
stdout=subprocess.PIPE,
67+
stderr=subprocess.PIPE,
68+
text=True,
69+
)
70+
if proc.returncode != 0:
71+
self.fail(
72+
"Subprocess failed with exit code {}\nstdout:\n{}\nstderr:\n{}".format(
73+
proc.returncode, proc.stdout, proc.stderr
74+
)
75+
)
76+
77+
def test_ctypes_load_and_call(self):
78+
# Pass the library path as an argument
79+
code = textwrap.dedent(
80+
"""
81+
import sys
82+
from ctypes import CDLL, c_int
83+
lib_path = sys.argv[1]
84+
lib = CDLL(lib_path)
85+
get_answer = lib.get_answer
86+
get_answer.restype = c_int
87+
result = get_answer()
88+
assert result == 42, f'expected 42, got {result}'
89+
"""
90+
)
91+
self.run_in_subprocess(code, str(self.lib_path))
92+
93+
@unittest.skipIf(sys.platform != "win32", "Windows-only test")
94+
def test_os_add_dll_directory_and_unload(self):
95+
# Pass the library dir as argument
96+
code = textwrap.dedent(
97+
"""
98+
import os
99+
import sys
100+
from pathlib import Path
101+
from ctypes import CDLL, c_int
102+
lib_dir = Path(sys.argv[1])
103+
dll_dir = lib_dir.parent
104+
dll_name = lib_dir.name
105+
# Should fail to load when DLL dir not added
106+
try:
107+
CDLL(dll_name)
108+
except OSError:
109+
pass
110+
else:
111+
raise AssertionError("CDLL(dll_name) should fail outside dll dir context")
112+
# Should succeed when DLL dir is temporarily added
113+
with os.add_dll_directory(dll_dir):
114+
lib = CDLL(dll_name)
115+
get_answer = lib.get_answer
116+
get_answer.restype = c_int
117+
result = get_answer()
118+
assert result == 42, f'expected 42, got {result}'
119+
"""
120+
)
121+
self.run_in_subprocess(code, str(self.lib_path))
122+
123+
124+
if __name__ == "__main__":
125+
unittest.main()

graalpython/com.oracle.graal.python.test/src/tests/test_wheel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def test_build_install_and_run(self):
6565
repaired_dir = tmpdir / "repaired"
6666

6767
shutil.copytree(orig_test_wheel, tmp_test_wheel)
68-
testlib_build = build_testlib(tmpdir, orig_testlib)
68+
testlib_build = build_testlib(tmpdir, orig_testlib).parent
6969

7070
cmd = [sys.executable, "-m", "venv", str(venv_dir)]
7171
print("Running:", shlex.join(cmd))

graalpython/com.oracle.graal.python.test/src/tests/testlib_helper.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import shlex
4141
import shutil
4242
import subprocess
43+
import sys
4344

4445

4546
def build_testlib(tmpdir, orig_testlib):
@@ -56,4 +57,15 @@ def build_testlib(tmpdir, orig_testlib):
5657
cmd = ["cmake", "--build", ".", "--config", "Release"]
5758
print("Running:", shlex.join(cmd))
5859
subprocess.check_call(cmd, cwd=str(testlib_build))
59-
return testlib_build
60+
61+
if sys.platform == 'win32':
62+
lib_name = "answer.dll"
63+
elif sys.platform == 'darwin':
64+
lib_name = "libanswer.dylib"
65+
else:
66+
lib_name = 'libanswer.so'
67+
lib_path = testlib_build / lib_name
68+
if not lib_path.exists():
69+
raise FileNotFoundError(f"Failed to locate built library at: {lib_path}")
70+
71+
return lib_path

0 commit comments

Comments
 (0)