Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let PyGMT work with the conda GMT package on Windows #434

Merged
merged 9 commits into from
May 20, 2020
Merged

Conversation

seisman
Copy link
Member

@seisman seisman commented May 19, 2020

Description of proposed changes

This PR fixes the PyGMT crash with the conda GMT package on Windows. It's a follow-up of RP #313. The commit history and the changed files in PR #313 is a little messy, so I cherry-picked 4 related commits from PR #313 and then work on top of it.

To make PyGMT work on Windows, users have to manually add two environmental variables:

  • GMT_LIBRARY_PATH C:\Miniconda\envs\pygmt\Library\bin
  • GMT_SHAREDIR C:\Miniconda\envs\pygmt\Library\share\gmt

ctypes.CDLL cannot find the libraries provided by conda, possibly because conda doesn't add the library path into Windows' library search path. Thus, we have to add the GMT_LIBRARY_PATH variable to tell pygmt (i.e., ctypes.CDLL) where to find the gmt library file.

GMT needs to know the path of its share directory to work. To determine the share directory, GMT checks a few environmental variables (GMT6_SHAREDIR, GMT5_SHAREDIR and GMT_SHARE). If none of them are defined, then GMT checks the variable GMT_SHARE_DIR which is set during building GMT. Since the conda GMT package was not built on users' computer, thus the GMT_SHARED_DIR directory doesn't exist at all. Then GMT tries to guess the share directory path based on the relative locations among the library, bin and share directories.
The guessing function works well on macOS and Linux, but not on Windows, and that function crashes.

PyGMT works well with the official GMT Windows installers, simply because the installers add the GMT6_SHAREDIR variable automatically. I believe it's a GMT bug, but we may need more time to find the exact crash location. Currently, the simplest workaround is adding the GMT_SHAREDIR variable manually.

The Windows CI jobs now work well, except 11 failing tests. These failing tests will be addressed in other PRs.

Fixes #46, #353. Closes #313.

Reminders

  • Run make format and make check to make sure the code follows the style guide.
  • Add tests for new features or tests that would have caught the bug that you're fixing.
  • Add new public functions/methods/classes to doc/api/index.rst.
  • Write detailed docstrings for all functions/methods.
  • If adding new functionality, add an example to docstrings or tutorials.

leouieda and others added 7 commits May 18, 2020 23:09
Co-Authored-By: Dongdong Tian <seisman.info@gmail.com>
Here we need to set `error` to True first, so if libnames is empty, than
we raise the GMTCLibNotFoundError error.

On Windows, libnames is a list of possible DLL names, "gmt.dll", "gmt_w64.dll", and "gmt_w32.dll".
If any one of them is found, then we set `error` to False and break to for loop.

If none of them are found, then we raise the GMTCLibNotFoundError error:
```
Error loading the GMT shared library 'gmt.dll, gmt_w64.dll, gmt_w32.dll'.
```

The error message above can be improved, but it's not a big issue.
To make PyGMT work with conda's GMT package on Windows, we have to set
two environmental variables:

- GMT_LIBRARY_PATH: `C:\Miniconda\envs\testing\Library\bin`
- GMT_SHAREDIR: `C:\Miniconda\envs\testing\Library\share\gmt`
@weiji14
Copy link
Member

weiji14 commented May 19, 2020

Awesome detective work @seisman! I've had a look through the changes and they look fine. There seems to be a lot of timeout tests, and a couple of minor png image differences for logo

================================== FAILURES ===================================
__________________ [doctest] pygmt.clib.conversion._as_array __________________
221 
222     Examples
223     --------
224 
225     >>> import pandas as pd
226     >>> x_series = pd.Series(data=[1, 2, 3, 4])
227     >>> x_array = _as_array(x_series)
228     >>> type(x_array)
229     <class 'numpy.ndarray'>
230     >>> x_array
Expected:
   array([1, 2, 3, 4])
Got:
   array([1, 2, 3, 4], dtype=int64)

C:\Miniconda\envs\testing\lib\site-packages\pygmt\clib\conversion.py:230: DocTestFailure
_____________ [doctest] pygmt.clib.conversion.dataarray_to_matrix _____________
041     ------
042     GMTInvalidInput
043         If the grid has more than two dimensions or variable grid spacing.
044 
045     Examples
046     --------
047 
048     >>> from pygmt.datasets import load_earth_relief
049     >>> # Use the global Earth relief grid with 1 degree spacing (60')
050     >>> grid = load_earth_relief(resolution='60m')
UNEXPECTED EXCEPTION: FileNotFoundError("File '@earth_relief_60m' not found.",)
Traceback (most recent call last):
 File "C:\Miniconda\envs\testing\lib\doctest.py", line 1330, in __run
   compileflags, 1), test.globs)
 File "<doctest pygmt.clib.conversion.dataarray_to_matrix[1]>", line 1, in <module>
 File "C:\Miniconda\envs\testing\lib\site-packages\pygmt\datasets\earth_relief.py", line 38, in load_earth_relief
   fname = which("@earth_relief_{}".format(resolution), download="u")
 File "C:\Miniconda\envs\testing\lib\site-packages\pygmt\helpers\decorators.py", line 183, in new_module
   return module_func(*args, **kwargs)
 File "C:\Miniconda\envs\testing\lib\site-packages\pygmt\modules.py", line 143, in which
   raise FileNotFoundError("File '{}' not found.".format(fname))
FileNotFoundError: File '@earth_relief_60m' not found.
C:\Miniconda\envs\testing\lib\site-packages\pygmt\clib\conversion.py:50: UnexpectedException
---------------------------- Captured stderr call -----------------------------
earth_relief_60m: Download file from the GMT data server [data set size is 106K].
earth_relief_60m: Earth Relief at 60x60 arc minutes obtained by Gaussian Cartesian filtering (111 km fullwidth) of SRTM15+V2 [Tozer et al., 2019].
gmtwhich [ERROR]: Libcurl Error: Timeout was reached
gmtwhich [ERROR]: You can turn remote file download off by setting GMT_DATA_SERVER_LIMIT = 0.
gmtwhich [ERROR]: File earth_relief_60m.grd not found!
___________ [doctest] pygmt.clib.conversion.kwargs_to_ctypes_array ____________
263     Returns
264     -------
265     ctypes_value : ctypes array or None
266 
267     Examples
268     --------
269 
270     >>> import ctypes as ct
271     >>> value = kwargs_to_ctypes_array('bla', {'bla': [10, 10]}, ct.c_int*2)
272     >>> type(value)
Expected:
   <class 'pygmt.clib.conversion.c_int_Array_2'>
Got:
   <class 'pygmt.clib.conversion.c_long_Array_2'>

C:\Miniconda\envs\testing\lib\site-packages\pygmt\clib\conversion.py:272: DocTestFailure
_____________________________ test_basemap_polar ______________________________
Error: Image dimensions did not match.
 Expected shape: (1958, 1822)
   D:\a\1\s\tmp-test-dir-with-unique-name\results\tmpuvl6y42z\baseline-test_basemap_polar.png
 Actual shape: (1958, 1821)
   D:\a\1\s\tmp-test-dir-with-unique-name\results\tmpuvl6y42z\test_basemap_polar.png
______________________ test_blockmedian_input_dataframe _______________________

   def test_blockmedian_input_dataframe():
       """
       Run blockmedian by passing in a pandas.DataFrame as input
       """
>       dataframe = load_sample_bathymetry()

C:\Miniconda\envs\testing\lib\site-packages\pygmt\tests\test_blockmedian.py:20: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Miniconda\envs\testing\lib\site-packages\pygmt\datasets\tutorial.py:80: in load_sample_bathymetry
   fname = which("@tut_ship.xyz", download="c")
C:\Miniconda\envs\testing\lib\site-packages\pygmt\helpers\decorators.py:183: in new_module
   return module_func(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

fname = '@tut_ship.xyz', kwargs = {'G': 'c'}
tmpfile = <pygmt.helpers.tempfile.GMTTempFile object at 0x0000014FBA288898>
arg_str = '@tut_ship.xyz -Gc ->C:\\Users\\VSSADM~1\\AppData\\Local\\Temp\\pygmt-4ll01yrg.txt'
lib = <pygmt.clib.session.Session object at 0x0000014FBA2889B0>, path = ''

   @fmt_docstring
   @use_alias(G="download")
   def which(fname, **kwargs):
       """
       Find the full path to specified files.
   
       Reports the full paths to the files given through *fname*. We look for
       the file in (1) the current directory, (2) in $GMT_USERDIR (if defined),
       (3) in $GMT_DATADIR (if defined), or (4) in $GMT_CACHEDIR (if defined).
   
       *fname* can also be a downloadable file (either a full URL, a
       `@file` special file for downloading from the GMT Site Cache, or
       `@earth_relief_*` topography grids). In these cases, use option *download*
       to set the desired behavior. If *download* is not used (or False), the file
       will not be found.
   
       Full option list at :gmt-docs:`gmtwhich.html`
   
       {aliases}
   
       Parameters
       ----------
       fname : str
           The file name that you want to check.
       G : bool or str
           If the file is downloadable and not found, we will try to download the
           it. Use True or 'l' (default) to download to the current directory. Use
           'c' to place in the user cache directory or 'u' user data directory
           instead.
   
       Returns
       -------
       path : str
           The path of the file, depending on the options used.
   
       Raises
       ------
       FileNotFoundError
           If the file is not found.
   
       """
       with GMTTempFile() as tmpfile:
           arg_str = " ".join([fname, build_arg_string(kwargs), "->" + tmpfile.name])
           with Session() as lib:
               lib.call_module("which", arg_str)
           path = tmpfile.read().strip()
       if not path:
>           raise FileNotFoundError("File '{}' not found.".format(fname))
E           FileNotFoundError: File '@tut_ship.xyz' not found.

C:\Miniconda\envs\testing\lib\site-packages\pygmt\modules.py:143: FileNotFoundError
---------------------------- Captured stderr call -----------------------------
gmtwhich [ERROR]: Libcurl Error: Timeout was reached
gmtwhich [ERROR]: You can turn remote file download off by setting GMT_DATA_SERVER_LIMIT = 0.
gmtwhich [ERROR]: File tut_ship.xyz not found!
______________________________ test_japan_quakes ______________________________

   def test_japan_quakes():
       "Check that the dataset loads without errors"
>       data = load_japan_quakes()

C:\Miniconda\envs\testing\lib\site-packages\pygmt\tests\test_datasets.py:20: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Miniconda\envs\testing\lib\site-packages\pygmt\datasets\tutorial.py:27: in load_japan_quakes
   fname = which("@tut_quakes.ngdc", download="c")
C:\Miniconda\envs\testing\lib\site-packages\pygmt\helpers\decorators.py:183: in new_module
   return module_func(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

fname = '@tut_quakes.ngdc', kwargs = {'G': 'c'}
tmpfile = <pygmt.helpers.tempfile.GMTTempFile object at 0x0000014FBA381F98>
arg_str = '@tut_quakes.ngdc -Gc ->C:\\Users\\VSSADM~1\\AppData\\Local\\Temp\\pygmt-60z1jjtv.txt'
lib = <pygmt.clib.session.Session object at 0x0000014FBA381C88>, path = ''

   @fmt_docstring
   @use_alias(G="download")
   def which(fname, **kwargs):
       """
       Find the full path to specified files.
   
       Reports the full paths to the files given through *fname*. We look for
       the file in (1) the current directory, (2) in $GMT_USERDIR (if defined),
       (3) in $GMT_DATADIR (if defined), or (4) in $GMT_CACHEDIR (if defined).
   
       *fname* can also be a downloadable file (either a full URL, a
       `@file` special file for downloading from the GMT Site Cache, or
       `@earth_relief_*` topography grids). In these cases, use option *download*
       to set the desired behavior. If *download* is not used (or False), the file
       will not be found.
   
       Full option list at :gmt-docs:`gmtwhich.html`
   
       {aliases}
   
       Parameters
       ----------
       fname : str
           The file name that you want to check.
       G : bool or str
           If the file is downloadable and not found, we will try to download the
           it. Use True or 'l' (default) to download to the current directory. Use
           'c' to place in the user cache directory or 'u' user data directory
           instead.
   
       Returns
       -------
       path : str
           The path of the file, depending on the options used.
   
       Raises
       ------
       FileNotFoundError
           If the file is not found.
   
       """
       with GMTTempFile() as tmpfile:
           arg_str = " ".join([fname, build_arg_string(kwargs), "->" + tmpfile.name])
           with Session() as lib:
               lib.call_module("which", arg_str)
           path = tmpfile.read().strip()
       if not path:
>           raise FileNotFoundError("File '{}' not found.".format(fname))
E           FileNotFoundError: File '@tut_quakes.ngdc' not found.

C:\Miniconda\envs\testing\lib\site-packages\pygmt\modules.py:143: FileNotFoundError
---------------------------- Captured stderr call -----------------------------
gmtwhich [ERROR]: Libcurl Error: Timeout was reached
gmtwhich [ERROR]: You can turn remote file download off by setting GMT_DATA_SERVER_LIMIT = 0.
gmtwhich [ERROR]: File tut_quakes.ngdc not found!
__________________________________ test_logo __________________________________
Error: Image files did not match.
 RMS Value: 7.485386103399013
 Expected:  
   D:\a\1\s\tmp-test-dir-with-unique-name\results\tmpxl74dzfk\baseline-test_logo.png
 Actual:    
   D:\a\1\s\tmp-test-dir-with-unique-name\results\tmpxl74dzfk\test_logo.png
 Difference:
   D:\a\1\s\tmp-test-dir-with-unique-name\results\tmpxl74dzfk\test_logo-failed-diff.png
 Tolerance: 
   2
_____________________________ test_logo_on_a_map ______________________________
Error: Image files did not match.
 RMS Value: 2.40664730778399
 Expected:  
   D:\a\1\s\tmp-test-dir-with-unique-name\results\tmpjakdon0f\baseline-test_logo_on_a_map.png
 Actual:    
   D:\a\1\s\tmp-test-dir-with-unique-name\results\tmpjakdon0f\test_logo_on_a_map.png
 Difference:
   D:\a\1\s\tmp-test-dir-with-unique-name\results\tmpjakdon0f\test_logo_on_a_map-failed-diff.png
 Tolerance: 
   2
______________________________ test_pygmtscraper ______________________________

   @pytest.mark.skipif(sphinx_gallery is None, reason="requires sphinx-gallery")
   def test_pygmtscraper():
       "Make sure the scraper finds the figures and removes them from the pool."
   
       showed = [fig for fig in SHOWED_FIGURES]
       for _ in range(len(SHOWED_FIGURES)):
           SHOWED_FIGURES.pop()
       try:
           fig = Figure()
           fig.coast(region="BR", projection="M6i", land="gray", frame=True)
           fig.show()
           assert len(SHOWED_FIGURES) == 1
           assert SHOWED_FIGURES[0] is fig
           scraper = PyGMTScraper()
           with TemporaryDirectory() as tmpdir:
               conf = {"src_dir": "meh"}
               fname = os.path.join(tmpdir, "meh.png")
               block_vars = {"image_path_iterator": (i for i in [fname])}
               assert not os.path.exists(fname)
>               scraper(None, block_vars, conf)

C:\Miniconda\envs\testing\lib\site-packages\pygmt\tests\test_sphinx_gallery.py:36: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Miniconda\envs\testing\lib\site-packages\pygmt\sphinx_gallery.py:35: in __call__
   return figure_rst(image_names, gallery_conf["src_dir"])
C:\Miniconda\envs\testing\lib\site-packages\sphinx_gallery\scrapers.py:266: in figure_rst
   for figure_path in figure_list]
C:\Miniconda\envs\testing\lib\site-packages\sphinx_gallery\scrapers.py:264: in <listcomp>
   figure_paths = [os.path.relpath(figure_path, sources_dir)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

path = 'C:\\Users\\VSSADM~1\\AppData\\Local\\Temp\\tmp1pxf_t82\\meh.png'
start = 'meh'

   def relpath(path, start=None):
       """Return a relative version of a path"""
       path = os.fspath(path)
       if isinstance(path, bytes):
           sep = b'\\'
           curdir = b'.'
           pardir = b'..'
       else:
           sep = '\\'
           curdir = '.'
           pardir = '..'
   
       if start is None:
           start = curdir
   
       if not path:
           raise ValueError("no path specified")
   
       start = os.fspath(start)
       try:
           start_abs = abspath(normpath(start))
           path_abs = abspath(normpath(path))
           start_drive, start_rest = splitdrive(start_abs)
           path_drive, path_rest = splitdrive(path_abs)
           if normcase(start_drive) != normcase(path_drive):
               raise ValueError("path is on mount %r, start on mount %r" % (
>                   path_drive, start_drive))
E                   ValueError: path is on mount 'C:', start on mount 'D:'

C:\Miniconda\envs\testing\lib\ntpath.py:584: ValueError
_______________________ test_surface_with_outfile_param _______________________

   def test_surface_with_outfile_param():
       """
       Run surface with the -Goutputfile.nc parameter
       """
       ship_data = load_sample_bathymetry()
       data = ship_data.values  # convert pandas.DataFrame to numpy.ndarray
       try:
           output = surface(
               data=data, spacing="5m", region=[245, 255, 20, 30], outfile=TEMP_GRID
           )
           assert output is None  # check that output is None since outfile is set
           assert os.path.exists(path=TEMP_GRID)  # check that outfile exists at path
           grid = xr.open_dataarray(TEMP_GRID)
           assert isinstance(grid, xr.DataArray)  # check that netcdf grid loaded properly
       finally:
>           os.remove(path=TEMP_GRID)
E           PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'C:\\Miniconda\\envs\\testing\\lib\\site-packages\\pygmt\\tests\\data\\tmp_grid.nc'

C:\Miniconda\envs\testing\lib\site-packages\pygmt\tests\test_surface.py:96: PermissionError
_________________________ test_surface_short_aliases __________________________

   def test_surface_short_aliases():
       """
       Run surface using short aliases -I for spacing, -R for region, -G for
       outfile
       """
       ship_data = load_sample_bathymetry()
       data = ship_data.values  # convert pandas.DataFrame to numpy.ndarray
       try:
           output = surface(data=data, I="5m", R=[245, 255, 20, 30], G=TEMP_GRID)
           assert output is None  # check that output is None since outfile is set
           assert os.path.exists(path=TEMP_GRID)  # check that outfile exists at path
           grid = xr.open_dataarray(TEMP_GRID)
           assert isinstance(grid, xr.DataArray)  # check that netcdf grid loaded properly
       finally:
>           os.remove(path=TEMP_GRID)
E           PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'C:\\Miniconda\\envs\\testing\\lib\\site-packages\\pygmt\\tests\\data\\tmp_grid.nc'

C:\Miniconda\envs\testing\lib\site-packages\pygmt\tests\test_surface.py:114: PermissionError
_________________________________ test_which __________________________________

   def test_which():
       "Make sure which returns file paths for @files correctly without errors"
       for fname in "tut_quakes.ngdc tut_bathy.nc".split():
>           cached_file = which("@{}".format(fname), download="c")

C:\Miniconda\envs\testing\lib\site-packages\pygmt\tests\test_which.py:15: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Miniconda\envs\testing\lib\site-packages\pygmt\helpers\decorators.py:183: in new_module
   return module_func(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

fname = '@tut_quakes.ngdc', kwargs = {'G': 'c'}
tmpfile = <pygmt.helpers.tempfile.GMTTempFile object at 0x0000014FBA534E80>
arg_str = '@tut_quakes.ngdc -Gc ->C:\\Users\\VSSADM~1\\AppData\\Local\\Temp\\pygmt-8d7_p_jk.txt'
lib = <pygmt.clib.session.Session object at 0x0000014FBA534E48>, path = ''

   @fmt_docstring
   @use_alias(G="download")
   def which(fname, **kwargs):
       """
       Find the full path to specified files.
   
       Reports the full paths to the files given through *fname*. We look for
       the file in (1) the current directory, (2) in $GMT_USERDIR (if defined),
       (3) in $GMT_DATADIR (if defined), or (4) in $GMT_CACHEDIR (if defined).
   
       *fname* can also be a downloadable file (either a full URL, a
       `@file` special file for downloading from the GMT Site Cache, or
       `@earth_relief_*` topography grids). In these cases, use option *download*
       to set the desired behavior. If *download* is not used (or False), the file
       will not be found.
   
       Full option list at :gmt-docs:`gmtwhich.html`
   
       {aliases}
   
       Parameters
       ----------
       fname : str
           The file name that you want to check.
       G : bool or str
           If the file is downloadable and not found, we will try to download the
           it. Use True or 'l' (default) to download to the current directory. Use
           'c' to place in the user cache directory or 'u' user data directory
           instead.
   
       Returns
       -------
       path : str
           The path of the file, depending on the options used.
   
       Raises
       ------
       FileNotFoundError
           If the file is not found.
   
       """
       with GMTTempFile() as tmpfile:
           arg_str = " ".join([fname, build_arg_string(kwargs), "->" + tmpfile.name])
           with Session() as lib:
               lib.call_module("which", arg_str)
           path = tmpfile.read().strip()
       if not path:
>           raise FileNotFoundError("File '{}' not found.".format(fname))
E           FileNotFoundError: File '@tut_quakes.ngdc' not found.

C:\Miniconda\envs\testing\lib\site-packages\pygmt\modules.py:143: FileNotFoundError
---------------------------- Captured stderr call -----------------------------
gmtwhich [ERROR]: Libcurl Error: Timeout was reached
gmtwhich [ERROR]: You can turn remote file download off by setting GMT_DATA_SERVER_LIMIT = 0.
gmtwhich [ERROR]: File tut_quakes.ngdc not found!
============================== warnings summary ===============================
C:\Miniconda\envs\testing\lib\site-packages\win32\lib\pywintypes.py:2
C:\Miniconda\envs\testing\lib\site-packages\win32\lib\pywintypes.py:2: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses

I'll be heading in to uni tomorrow and should have access to a Windows computer to test this branch a bit better. But we should definitely merge this in soon and tidy up all the stray pieces.

@seisman
Copy link
Member Author

seisman commented May 19, 2020

There seems to be a lot of timeout tests.

It seems Azure Pipelines's Linux and Windows agents cannot access any external websites, including GMT's server. However, macOS agents don't have the problem. The GMT repository also has the same problem and we have to cache all the remote files (e.g., @earth_relief_60m.grd) on the macOS agent, then let the Linux and Windows agents access the macOS caches. See GenericMappingTools/gmt#3056. Perhaps an issue of Azure Pipelines, but I didn't find any reports or solutions.

I'll be heading in to uni tomorrow and should have access to a Windows computer to test this branch a bit better.

That would be great!

FYI, I finally find why GMT can't guess its share directory location. See the bug report GenericMappingTools/gmt#3353.

@weiji14
Copy link
Member

weiji14 commented May 19, 2020

It seems Azure Pipelines's Linux and Windows agents cannot access any external websites, including GMT's server. However, macOS agents don't have the problem. The GMT repository also has the same problem and we have to cache all the remote files (e.g., @earth_relief_60m.grd) on the macOS agent, then let the Linux and Windows agents access the macOS caches. See GenericMappingTools/gmt#3056. Perhaps an issue of Azure Pipelines, but I didn't find any reports or solutions.

Ah okay, I was working on consolidating all our Linux/MacOS/Windows tests on Github Actions, not sure if that will fix it, since Azure and Github are both Microsoft owned 😆 I could make that happen once this PR is merged, and work on #345 too.

Edit: Same with Github Actions, MacOS works but Linux/Windows doesn't. Might be a DNS related issue? Closest thing I can find is https://stackoverflow.com/questions/58034882/vstest-unit-tests-ping-of-a-dns-server-8-8-8-8-fails-when-running-within-azur,

FYI, I finally find why GMT can't guess its share directory location. See the bug report GenericMappingTools/gmt#3353.

Good to know, I think we just set GMT_SHAREDIR as a workaround for now instead of waiting for GMT 6.1.0.

@seisman seisman added this to the 0.2.0 milestone May 19, 2020
@weiji14
Copy link
Member

weiji14 commented May 20, 2020

Ok, these are the 5 test failures I'm getting locally on Windows:

================================================================================================== FAILURES ==================================================================================================
_________________________________________________________________________________ [doctest] pygmt.clib.conversion._as_array __________________________________________________________________________________
221
222     Examples
223     --------
224
225     >>> import pandas as pd
226     >>> x_series = pd.Series(data=[1, 2, 3, 4])
227     >>> x_array = _as_array(x_series)
228     >>> type(x_array)
229     <class 'numpy.ndarray'>
230     >>> x_array
Expected:
    array([1, 2, 3, 4])
Got:
    array([1, 2, 3, 4], dtype=int64)

H:\github\pygmt\pygmt\clib\conversion.py:230: DocTestFailure
___________________________________________________________________________ [doctest] pygmt.clib.conversion.kwargs_to_ctypes_array ___________________________________________________________________________
263     Returns
264     -------
265     ctypes_value : ctypes array or None
266
267     Examples
268     --------
269
270     >>> import ctypes as ct
271     >>> value = kwargs_to_ctypes_array('bla', {'bla': [10, 10]}, ct.c_int*2)
272     >>> type(value)
Expected:
    <class 'pygmt.clib.conversion.c_int_Array_2'>
Got:
    <class 'pygmt.clib.conversion.c_long_Array_2'>

H:\github\pygmt\pygmt\clib\conversion.py:272: DocTestFailure
_____________________________________________________________________________________________ test_pygmtscraper ______________________________________________________________________________________________

    @pytest.mark.skipif(sphinx_gallery is None, reason="requires sphinx-gallery")
    def test_pygmtscraper():
        "Make sure the scraper finds the figures and removes them from the pool."

        showed = [fig for fig in SHOWED_FIGURES]
        for _ in range(len(SHOWED_FIGURES)):
            SHOWED_FIGURES.pop()
        try:
            fig = Figure()
            fig.coast(region="BR", projection="M6i", land="gray", frame=True)
            fig.show()
            assert len(SHOWED_FIGURES) == 1
            assert SHOWED_FIGURES[0] is fig
            scraper = PyGMTScraper()
            with TemporaryDirectory() as tmpdir:
                conf = {"src_dir": "meh"}
                fname = os.path.join(tmpdir, "meh.png")
                block_vars = {"image_path_iterator": (i for i in [fname])}
                assert not os.path.exists(fname)
>               scraper(None, block_vars, conf)

pygmt\tests\test_sphinx_gallery.py:36:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pygmt\sphinx_gallery.py:35: in __call__
    return figure_rst(image_names, gallery_conf["src_dir"])
C:\Users\leongw1\AppData\Local\Continuum\miniconda3\envs\pygmt\lib\site-packages\sphinx_gallery\scrapers.py:266: in figure_rst
    for figure_path in figure_list]
C:\Users\leongw1\AppData\Local\Continuum\miniconda3\envs\pygmt\lib\site-packages\sphinx_gallery\scrapers.py:266: in <listcomp>
    for figure_path in figure_list]
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

path = 'C:\\Users\\leongw1\\AppData\\Local\\Temp\\tmp36122_v1\\meh.png', start = 'meh'

    def relpath(path, start=None):
        """Return a relative version of a path"""
        path = os.fspath(path)
        if isinstance(path, bytes):
            sep = b'\\'
            curdir = b'.'
            pardir = b'..'
        else:
            sep = '\\'
            curdir = '.'
            pardir = '..'

        if start is None:
            start = curdir

        if not path:
            raise ValueError("no path specified")

        start = os.fspath(start)
        try:
            start_abs = abspath(normpath(start))
            path_abs = abspath(normpath(path))
            start_drive, start_rest = splitdrive(start_abs)
            path_drive, path_rest = splitdrive(path_abs)
            if normcase(start_drive) != normcase(path_drive):
                raise ValueError("path is on mount %r, start on mount %r" % (
>                   path_drive, start_drive))
E                   ValueError: path is on mount 'C:', start on mount 'H:'

C:\Users\leongw1\AppData\Local\Continuum\miniconda3\envs\pygmt\lib\ntpath.py:562: ValueError
-------------------------------------------------------------------------------------------- Captured stderr call --------------------------------------------------------------------------------------------
psconvert [ERROR]: Error opening HKLM key
psconvert [ERROR]: Error opening HKLM key
______________________________________________________________________________________ test_surface_with_outfile_param _______________________________________________________________________________________

    def test_surface_with_outfile_param():
        """
        Run surface with the -Goutputfile.nc parameter
        """
        ship_data = load_sample_bathymetry()
        data = ship_data.values  # convert pandas.DataFrame to numpy.ndarray
        try:
            output = surface(
                data=data, spacing="5m", region=[245, 255, 20, 30], outfile=TEMP_GRID
            )
            assert output is None  # check that output is None since outfile is set
            assert os.path.exists(path=TEMP_GRID)  # check that outfile exists at path
            grid = xr.open_dataarray(TEMP_GRID)
            assert isinstance(grid, xr.DataArray)  # check that netcdf grid loaded properly
        finally:
>           os.remove(path=TEMP_GRID)
E           PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'H:\\github\\pygmt\\pygmt\\tests\\data\\tmp_grid.nc'

pygmt\tests\test_surface.py:96: PermissionError
_________________________________________________________________________________________ test_surface_short_aliases _________________________________________________________________________________________

    def test_surface_short_aliases():
        """
        Run surface using short aliases -I for spacing, -R for region, -G for
        outfile
        """
        ship_data = load_sample_bathymetry()
        data = ship_data.values  # convert pandas.DataFrame to numpy.ndarray
        try:
            output = surface(data=data, I="5m", R=[245, 255, 20, 30], G=TEMP_GRID)
            assert output is None  # check that output is None since outfile is set
            assert os.path.exists(path=TEMP_GRID)  # check that outfile exists at path
            grid = xr.open_dataarray(TEMP_GRID)
            assert isinstance(grid, xr.DataArray)  # check that netcdf grid loaded properly
        finally:
>           os.remove(path=TEMP_GRID)
E           PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'H:\\github\\pygmt\\pygmt\\tests\\data\\tmp_grid.nc'

pygmt\tests\test_surface.py:114: PermissionError
========================================================================================== short test summary info ===========================================================================================
FAILED H:\github\pygmt\pygmt\clib\conversion.py::pygmt.clib.conversion._as_array
FAILED H:\github\pygmt\pygmt\clib\conversion.py::pygmt.clib.conversion.kwargs_to_ctypes_array
FAILED H:\github\pygmt\pygmt\tests\test_sphinx_gallery.py::test_pygmtscraper - ValueError: path is on mount 'C:', start on mount 'H:'
FAILED H:\github\pygmt\pygmt\tests\test_surface.py::test_surface_with_outfile_param - PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'H:\\gi...
FAILED H:\github\pygmt\pygmt\tests\test_surface.py::test_surface_short_aliases - PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'H:\\github\...
================================================================================= 5 failed, 246 passed in 151.48s (0:02:31) ==================================================================================

2 are with clib, 1 with pygmtscraper, and 2 with surface. I should be able to resolve the 2 surface ones, just need a with statement to close the file properly.

@seisman
Copy link
Member Author

seisman commented May 20, 2020

I think this PR is good to merge, unless you want to add more tests.

So that the file is closed properly for deletion later.
Copy link
Member

@weiji14 weiji14 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, the test_surface.py errors are gone, and somehow the .clib ones disappeared on my system (Edit: it only fails when I run pygmt.test() from ipython, not in the terminal).

The pygmtscraper test seems to be failing because my stuff is on H:\ instead of C:\. Same with Azure where they use D:\ instead of C:\. One possible solution is to change this line:

with TemporaryDirectory() as tmpdir:

to this:

        with TemporaryDirectory(dir=os.getcwd()) as tmpdir:

But I'm happy to let that slide for now 😄

@seisman seisman merged commit f3bc5c3 into master May 20, 2020
@seisman seisman deleted the fix-windows branch May 20, 2020 05:29
@seisman
Copy link
Member Author

seisman commented May 20, 2020

Merged. We can fix other failing tests later.

@seisman seisman mentioned this pull request May 21, 2020
8 tasks
@seisman seisman modified the milestones: 0.2.x, 0.1.x May 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for Windows
3 participants