Skip to content

Commit 67244e8

Browse files
committed
Improve the loading of GMT's shared library
1. Need to catch `GMTCLibError` error from calling `check_libgmt()` 2. Add a new parameter `lib_fullnames` (default to `clib_full_names()`) to `load_libgmt()` so that we can test more cases 3. Skip a library path if it's known to fail in previous tries 4. Improve the error message, because each library path may fail due to different reasons. 5. Add more tests
1 parent 6a6171e commit 67244e8

File tree

2 files changed

+131
-28
lines changed

2 files changed

+131
-28
lines changed

pygmt/clib/loading.py

Lines changed: 25 additions & 13 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,29 @@ 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:
49+
if libname in failing_libs: # libname is known to fail, so skip it
50+
continue
4151
libgmt = ctypes.CDLL(libname)
4252
check_libgmt(libgmt)
4353
error = False
4454
break
45-
except OSError as err:
46-
error = err
55+
except (OSError, GMTCLibError) as err:
56+
error_msg.append(
57+
f"Error loading the GMT shared library '{libname}'.\n{err}"
58+
)
59+
failing_libs.append(libname)
60+
4761
if error:
48-
raise GMTCLibNotFoundError(
49-
"Error loading the GMT shared library "
50-
f"{', '.join(lib_fullnames)}.\n {error}."
51-
)
62+
raise GMTCLibNotFoundError("\n".join(error_msg))
63+
5264
return libgmt
5365

5466

pygmt/tests/test_clib_loading.py

Lines changed: 106 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,19 +89,85 @@ 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+
def test_load_libgmt_with_broken_libraries(monkeypatch):
6893
"""
69-
Make sure we get the correct library name for different OS names.
94+
Test 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+
# load the GMT library before mocking the ctypes.CDLL function
97+
loaded_libgmt = load_libgmt()
98+
99+
def mock_ctypes_cdll_return(libname):
100+
"""
101+
Mock the return value of ctypes.CDLL.
102+
103+
Parameters
104+
----------
105+
libname : str or FakedLibGMT or ctypes.CDLL
106+
Path to the GMT library, a faked GMT library or a working library
107+
loaded as ctypes.CDLL.
108+
109+
Return
110+
------
111+
object
112+
Either the loaded GMT library or the faked GMT library.
113+
"""
114+
if isinstance(libname, FakedLibGMT):
115+
# libname is a faked GMT library, return the faked library
116+
return libname
117+
if isinstance(libname, str):
118+
# libname is an invalid library path in str type,
119+
# raise OSError like the original ctypes.CDLL
120+
raise OSError(f"Unable to find '{libname}'")
121+
# libname is a loaded GMT library
122+
return loaded_libgmt
123+
124+
with monkeypatch.context() as mpatch:
125+
# pylint: disable=protected-access
126+
# mock the ctypes.CDLL using mock_ctypes_cdll_return()
127+
mpatch.setattr(ctypes, "CDLL", mock_ctypes_cdll_return)
128+
129+
faked_libgmt1 = FakedLibGMT("/path/to/faked/libgmt1.so")
130+
faked_libgmt2 = FakedLibGMT("/path/to/faked/libgmt2.so")
131+
132+
# case 1: two broken libraries
133+
# Raise the GMTCLibNotFoundError exception
134+
# The error message should contains information of both libraries
135+
lib_fullnames = [faked_libgmt1, faked_libgmt2]
136+
msg_regex = (
137+
fr"Error loading the GMT shared library '{faked_libgmt1._name}'.\n"
138+
fr"Error loading '{faked_libgmt1._name}'. Couldn't access.*\n"
139+
fr"Error loading the GMT shared library '{faked_libgmt2._name}'.\n"
140+
fr"Error loading '{faked_libgmt2._name}'. Couldn't access.*"
141+
)
142+
with pytest.raises(GMTCLibNotFoundError, match=msg_regex):
143+
load_libgmt(lib_fullnames=lib_fullnames)
144+
145+
# case 2: broken library + invalid path
146+
lib_fullnames = [faked_libgmt1, "/invalid/path/to/libgmt.so"]
147+
msg_regex = (
148+
fr"Error loading the GMT shared library '{faked_libgmt1._name}'.\n"
149+
fr"Error loading '{faked_libgmt1._name}'. Couldn't access.*\n"
150+
"Error loading the GMT shared library '/invalid/path/to/libgmt.so'.\n"
151+
"Unable to find '/invalid/path/to/libgmt.so'"
152+
)
153+
with pytest.raises(GMTCLibNotFoundError, match=msg_regex):
154+
load_libgmt(lib_fullnames=lib_fullnames)
155+
156+
# case 3: broken library + invalid path + working library
157+
lib_fullnames = [faked_libgmt1, "/invalid/path/to/libgmt.so", loaded_libgmt]
158+
assert check_libgmt(load_libgmt(lib_fullnames=lib_fullnames)) is None
159+
160+
# case 4: invalid path + broken library + working library
161+
lib_fullnames = ["/invalid/path/to/libgmt.so", faked_libgmt1, loaded_libgmt]
162+
assert check_libgmt(load_libgmt(lib_fullnames=lib_fullnames)) is None
163+
164+
# case 5: working library + broken library + invalid path
165+
lib_fullnames = [loaded_libgmt, faked_libgmt1, "/invalid/path/to/libgmt.so"]
166+
assert check_libgmt(load_libgmt(lib_fullnames=lib_fullnames)) is None
79167

168+
# case 6: repeated broken library + working library
169+
lib_fullnames = [faked_libgmt1, faked_libgmt1, loaded_libgmt]
170+
assert check_libgmt(load_libgmt(lib_fullnames=lib_fullnames)) is None
80171

81172
###############################################################################
82173
# Tests for clib_full_names

0 commit comments

Comments
 (0)