Skip to content

Commit dc75785

Browse files
seismanweiji14
andauthored
Fix issues in loading GMT's shared library (#977)
- Catch the GMTCLibError error from calling check_libgmt() - Skip a library path if it's known to fail in previous tries - Improve the error message, by combine error message of all tries - Add a new parameter lib_fullnames (default to clib_full_names()) to for easier testing - Add more tests Co-authored-by: Wei Ji <[email protected]>
1 parent 1690928 commit dc75785

File tree

2 files changed

+172
-36
lines changed

2 files changed

+172
-36
lines changed

pygmt/clib/loading.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@
1414
from pygmt.exceptions import GMTCLibError, GMTCLibNotFoundError, GMTOSError
1515

1616

17-
def load_libgmt():
17+
def load_libgmt(lib_fullnames=None):
1818
"""
1919
Find and load ``libgmt`` as a :py:class:`ctypes.CDLL`.
2020
21-
By default, will look for the shared library in the directory specified by
22-
the environment variable ``GMT_LIBRARY_PATH``. If it's not set, will let
23-
ctypes try to find the library.
21+
Will look for the GMT shared library in the directories determined by
22+
clib_full_names().
23+
24+
Parameters
25+
----------
26+
lib_fullnames : list of str or None
27+
List of possible full names of GMT's shared library. If ``None``, will
28+
default to ``clib_full_names()``.
2429
2530
Returns
2631
-------
@@ -33,22 +38,26 @@ def load_libgmt():
3338
If there was any problem loading the library (couldn't find it or
3439
couldn't access the functions).
3540
"""
36-
lib_fullnames = []
41+
if lib_fullnames is None:
42+
lib_fullnames = clib_full_names()
43+
3744
error = True
38-
for libname in clib_full_names():
39-
lib_fullnames.append(libname)
45+
error_msg = []
46+
failing_libs = []
47+
for libname in lib_fullnames:
4048
try:
41-
libgmt = ctypes.CDLL(libname)
42-
check_libgmt(libgmt)
43-
error = False
44-
break
45-
except OSError as err:
46-
error = err
49+
if libname not in failing_libs: # skip the lib if it's known to fail
50+
libgmt = ctypes.CDLL(libname)
51+
check_libgmt(libgmt)
52+
error = False
53+
break
54+
except (OSError, GMTCLibError) as err:
55+
error_msg.append(f"Error loading GMT shared library at '{libname}'.\n{err}")
56+
failing_libs.append(libname)
57+
4758
if error:
48-
raise GMTCLibNotFoundError(
49-
"Error loading the GMT shared library "
50-
f"{', '.join(lib_fullnames)}.\n {error}."
51-
)
59+
raise GMTCLibNotFoundError("\n".join(error_msg))
60+
5261
return libgmt
5362

5463

@@ -66,16 +75,14 @@ def clib_names(os_name):
6675
libnames : list of str
6776
List of possible names of GMT's shared library.
6877
"""
69-
if os_name.startswith("linux"):
78+
if os_name.startswith(("linux", "freebsd")):
7079
libnames = ["libgmt.so"]
7180
elif os_name == "darwin": # Darwin is macOS
7281
libnames = ["libgmt.dylib"]
7382
elif os_name == "win32":
7483
libnames = ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
75-
elif os_name.startswith("freebsd"): # FreeBSD
76-
libnames = ["libgmt.so"]
7784
else:
78-
raise GMTOSError(f'Operating system "{os_name}" not supported.')
85+
raise GMTOSError(f"Operating system '{os_name}' not supported.")
7986
return libnames
8087

8188

pygmt/tests/test_clib_loading.py

Lines changed: 144 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Test the functions that load libgmt.
33
"""
4+
import ctypes
45
import shutil
56
import subprocess
67
import sys
@@ -12,15 +13,23 @@
1213
from pygmt.exceptions import GMTCLibError, GMTCLibNotFoundError, GMTOSError
1314

1415

16+
class FakedLibGMT: # pylint: disable=too-few-public-methods
17+
"""
18+
Class for faking a GMT library.
19+
"""
20+
21+
def __init__(self, name):
22+
self._name = name
23+
24+
def __str__(self):
25+
return self._name
26+
27+
1528
def test_check_libgmt():
1629
"""
1730
Make sure check_libgmt fails when given a bogus library.
1831
"""
19-
# create a fake library with a "_name" property
20-
def libgmt():
21-
pass
22-
23-
libgmt._name = "/path/to/libgmt.so" # pylint: disable=protected-access
32+
libgmt = FakedLibGMT("/path/to/libgmt.so")
2433
msg = (
2534
# pylint: disable=protected-access
2635
f"Error loading '{libgmt._name}'. "
@@ -33,6 +42,22 @@ def libgmt():
3342
check_libgmt(libgmt)
3443

3544

45+
def test_clib_names():
46+
"""
47+
Make sure we get the correct library name for different OS names.
48+
"""
49+
for linux in ["linux", "linux2", "linux3"]:
50+
assert clib_names(linux) == ["libgmt.so"]
51+
assert clib_names("darwin") == ["libgmt.dylib"]
52+
assert clib_names("win32") == ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
53+
for freebsd in ["freebsd10", "freebsd11", "freebsd12"]:
54+
assert clib_names(freebsd) == ["libgmt.so"]
55+
with pytest.raises(GMTOSError):
56+
clib_names("meh")
57+
58+
59+
###############################################################################
60+
# Tests for load_libgmt
3661
def test_load_libgmt():
3762
"""
3863
Test that loading libgmt works and doesn't crash.
@@ -64,18 +89,122 @@ def test_load_libgmt_with_a_bad_library_path(monkeypatch):
6489
assert check_libgmt(load_libgmt()) is None
6590

6691

67-
def test_clib_names():
92+
class TestLibgmtBrokenLibs:
6893
"""
69-
Make sure we get the correct library name for different OS names.
94+
Test that load_libgmt still works when a broken library is found.
7095
"""
71-
for linux in ["linux", "linux2", "linux3"]:
72-
assert clib_names(linux) == ["libgmt.so"]
73-
assert clib_names("darwin") == ["libgmt.dylib"]
74-
assert clib_names("win32") == ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
75-
for freebsd in ["freebsd10", "freebsd11", "freebsd12"]:
76-
assert clib_names(freebsd) == ["libgmt.so"]
77-
with pytest.raises(GMTOSError):
78-
clib_names("meh")
96+
97+
# load the GMT library before mocking the ctypes.CDLL function
98+
loaded_libgmt = load_libgmt()
99+
invalid_path = "/invalid/path/to/libgmt.so"
100+
faked_libgmt1 = FakedLibGMT("/path/to/faked/libgmt1.so")
101+
faked_libgmt2 = FakedLibGMT("/path/to/faked/libgmt2.so")
102+
103+
def _mock_ctypes_cdll_return(self, libname):
104+
"""
105+
Mock the return value of ctypes.CDLL.
106+
107+
Parameters
108+
----------
109+
libname : str or FakedLibGMT or ctypes.CDLL
110+
Path to the GMT library, a faked GMT library, or a working library
111+
loaded as ctypes.CDLL.
112+
113+
Return
114+
------
115+
object
116+
Either the loaded GMT library or the faked GMT library.
117+
"""
118+
if isinstance(libname, FakedLibGMT):
119+
# libname is a faked GMT library, return the faked library
120+
return libname
121+
if isinstance(libname, str):
122+
# libname is an invalid library path in string type,
123+
# raise OSError like the original ctypes.CDLL
124+
raise OSError(f"Unable to find '{libname}'")
125+
# libname is a loaded GMT library
126+
return self.loaded_libgmt
127+
128+
@pytest.fixture
129+
def mock_ctypes(self, monkeypatch):
130+
"""
131+
Patch the ctypes.CDLL function.
132+
"""
133+
monkeypatch.setattr(ctypes, "CDLL", self._mock_ctypes_cdll_return)
134+
135+
def test_two_broken_libraries(self, mock_ctypes): # pylint: disable=unused-argument
136+
"""
137+
Case 1: two broken libraries.
138+
139+
Raise the GMTCLibNotFoundError exception. Error message should contain
140+
information of both libraries that failed to load properly.
141+
"""
142+
# pylint: disable=protected-access
143+
lib_fullnames = [self.faked_libgmt1, self.faked_libgmt2]
144+
msg_regex = (
145+
fr"Error loading GMT shared library at '{self.faked_libgmt1._name}'.\n"
146+
fr"Error loading '{self.faked_libgmt1._name}'. Couldn't access.*\n"
147+
fr"Error loading GMT shared library at '{self.faked_libgmt2._name}'.\n"
148+
f"Error loading '{self.faked_libgmt2._name}'. Couldn't access.*"
149+
)
150+
with pytest.raises(GMTCLibNotFoundError, match=msg_regex):
151+
load_libgmt(lib_fullnames=lib_fullnames)
152+
153+
def test_load_brokenlib_invalidpath(
154+
self, mock_ctypes
155+
): # pylint: disable=unused-argument
156+
"""
157+
Case 2: broken library + invalid path.
158+
159+
Raise the GMTCLibNotFoundError exception. Error message should contain
160+
information of one library that failed to load and one invalid path.
161+
"""
162+
# pylint: disable=protected-access
163+
lib_fullnames = [self.faked_libgmt1, self.invalid_path]
164+
msg_regex = (
165+
fr"Error loading GMT shared library at '{self.faked_libgmt1._name}'.\n"
166+
fr"Error loading '{self.faked_libgmt1._name}'. Couldn't access.*\n"
167+
fr"Error loading GMT shared library at '{self.invalid_path}'.\n"
168+
f"Unable to find '{self.invalid_path}'"
169+
)
170+
with pytest.raises(GMTCLibNotFoundError, match=msg_regex):
171+
load_libgmt(lib_fullnames=lib_fullnames)
172+
173+
def test_brokenlib_invalidpath_workinglib(
174+
self, mock_ctypes
175+
): # pylint: disable=unused-argument
176+
"""
177+
Case 3: broken library + invalid path + working library.
178+
"""
179+
lib_fullnames = [self.faked_libgmt1, self.invalid_path, self.loaded_libgmt]
180+
assert check_libgmt(load_libgmt(lib_fullnames=lib_fullnames)) is None
181+
182+
def test_invalidpath_brokenlib_workinglib(
183+
self, mock_ctypes
184+
): # pylint: disable=unused-argument
185+
"""
186+
Case 4: invalid path + broken library + working library.
187+
"""
188+
lib_fullnames = [self.invalid_path, self.faked_libgmt1, self.loaded_libgmt]
189+
assert check_libgmt(load_libgmt(lib_fullnames=lib_fullnames)) is None
190+
191+
def test_workinglib_brokenlib_invalidpath(
192+
self, mock_ctypes
193+
): # pylint: disable=unused-argument
194+
"""
195+
Case 5: working library + broken library + invalid path.
196+
"""
197+
lib_fullnames = [self.loaded_libgmt, self.faked_libgmt1, self.invalid_path]
198+
assert check_libgmt(load_libgmt(lib_fullnames=lib_fullnames)) is None
199+
200+
def test_brokenlib_brokenlib_workinglib(
201+
self, mock_ctypes
202+
): # pylint: disable=unused-argument
203+
"""
204+
Case 6: repeating broken libraries + working library.
205+
"""
206+
lib_fullnames = [self.faked_libgmt1, self.faked_libgmt1, self.loaded_libgmt]
207+
assert check_libgmt(load_libgmt(lib_fullnames=lib_fullnames)) is None
79208

80209

81210
###############################################################################

0 commit comments

Comments
 (0)