Repository: stan-dev/cmdstanpy
Pull request #87 (2019-07-24 07:33:09)

Add appveyor
#### Submission Checklist

- [x] Run unit tests
- [x] Declare copyright holder and open-source license: see below

#### Summary

Add .appveyor.yml
Uses RTools
Updates tests to work on Windows.

#### Copyright and Licensing

Please list the copyright holder for the work you are submitting (this will be you or your assignee, such as a university or company):

Ari Hartikainen

By submitting this pull request, the copyright holder is agreeing to license the submitted work under the following licenses:
- Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)




Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
new file mode 100644
index 0000000..14ba87e
--- /dev/null
+++ b/.appveyor.yml
@@ -0,0 +1,71 @@
+skip_branch_with_pr: true
+version: '{build}'
+image: Visual Studio 2017
+
+platform:
+  - x64
+
+cache:
+  - C:\Users\appveyor\.cmdstanpy
+  #- C:\RTools
+
+init:
+  - git config --global core.autocrlf input
+
+clone_folder: c:\projects\cmdstanpy
+shallow_clone: true
+clone_depth: 1
+
+environment:
+  matrix:
+    - PYTHON: "C:\\Python37-x64"
+      PYTHON_VERSION: "3.7"
+      PYTHON_ARCH: "64"
+
+install:
+  # If there is a newer build queued for the same PR, cancel this one.
+  # The AppVeyor 'rollout builds' option is supposed to serve the same
+  # purpose but it is problematic because it tends to cancel builds pushed
+  # directly to master instead of just PR builds (or the converse).
+  # credits: JuliaLang developers.
+  - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod `
+      https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | `
+      Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
+        throw "There are newer queued builds for this pull request, failing early." }
+
+  # Install Python (from the official .msi of https://python.org) and pip when
+  # not already installed.
+  - ps: if (-not(Test-Path($env:PYTHON))) { & appveyor\install.ps1 }
+
+  # Prepend newly installed Python to the PATH of this build (this cannot be
+  # done from inside the powershell script as it would require to restart
+  # the parent CMD process).
+  - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
+
+  # Install RTools
+  - ps: Start-FileDownload 'https://cran.r-project.org/bin/windows/Rtools/Rtools35.exe'
+  - "Rtools35.exe /VERYSILENT /SP- /NORESTART"  #TODO: dont download and install if c:\rtools folder exists.
+  - "SET PATH=C:\\Rtools\\bin;C:\\Rtools\\mingw_64\\bin;%PATH%"
+  - "SET MAKE=mingw32-make.exe"
+  - "gcc --version"
+  - "mingw32-make --version"
+
+  # Check that we have the expected version and architecture for Python
+  - "python --version"
+  - "python -c \"import struct; print(struct.calcsize('P') * 8)\""
+
+  - "python -m pip install --quiet --disable-pip-version-check --upgrade pip"
+  - "python -m pip install --quiet -r requirements.txt"
+  - "python -m pip install --quiet pytest"
+
+  - "python .\\bin\\install_cmdstan"
+
+  - "python -m pip install --quiet -e ."
+  - "pip freeze"
+
+build: false
+
+test_script:
+  - "SET PATH=C:\\Rtools\\bin;C:\\Rtools\\mingw_64\\bin;%PATH%"
+  - "SET MAKE=mingw32-make.exe"
+  - "python -m pytest test"
diff --git a/bin/install_cmdstan b/bin/install_cmdstan
index 7b7001f..98d6ad2 100755
--- a/bin/install_cmdstan
+++ b/bin/install_cmdstan
@@ -47,7 +47,6 @@ def install_version(cmdstan_version):
         proc = subprocess.Popen(
             cmd, cwd=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE
         )
-        proc.wait()
         stdout, stderr = proc.communicate()
         if proc.returncode:
             print('Command "make build" failed')
diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py
index 9788937..31cfb13 100644
--- a/cmdstanpy/model.py
+++ b/cmdstanpy/model.py
@@ -60,7 +60,7 @@ class Model(object):
             if self._name is None:
                 self._name, _ = os.path.splitext(exename)
             else:
-                if self._name != ''.join([exename, EXTENSION]):
+                if self._name != os.path.splitext(exename)[0]:
                     raise ValueError(
                         'name mismatch between Stan file and compiled'
                         ' executable, expecting basename: {}'
@@ -76,7 +76,7 @@ class Model(object):
     def code(self) -> str:
         """Return Stan program as a string."""
         if not self._stan_file:
-            raise RuntimeError("Please specify source file")
+            raise RuntimeError('Please specify source file')
 
         code = None
         try:
@@ -123,7 +123,7 @@ class Model(object):
             look for files to include in compilation of the C++ executable.
         """
         if not self._stan_file:
-            raise RuntimeError("Please specify source file")
+            raise RuntimeError('Please specify source file')
 
         if self._exe_file is not None and not overwrite:
             self._logger.warning('model is already compiled')
diff --git a/cmdstanpy/stanfit.py b/cmdstanpy/stanfit.py
index 0940bf2..2efb982 100644
--- a/cmdstanpy/stanfit.py
+++ b/cmdstanpy/stanfit.py
@@ -1,5 +1,6 @@
 import os
 import re
+import platform
 import shutil
 import tempfile
 from typing import List, Tuple
@@ -11,6 +12,7 @@ import logging
 from cmdstanpy import TMPDIR
 from cmdstanpy.utils import (
     check_csv,
+    create_named_text_file,
     EXTENSION,
     cmdstan_path,
     do_command,
@@ -36,32 +38,30 @@ class StanFit(object):
                 'found {i]}'.format(chains)
             )
         self.csv_files = []
-        """per-chain sample csv files."""
+        # per-chain sample csv files.
         if args.output_basename is None:
             csv_basename = 'stan-{}-draws'.format(args.model_name)
             for i in range(chains):
-                fd = tempfile.NamedTemporaryFile(
-                    mode='w+',
+                fd_name = create_named_text_file(
+                    dir=TMPDIR,
                     prefix='{}-{}-'.format(csv_basename, i + 1),
                     suffix='.csv',
-                    dir=TMPDIR,
-                    delete=False,
                 )
-                self.csv_files.append(fd.name)
+                self.csv_files.append(fd_name)
         else:
             for i in range(chains):
                 self.csv_files.append(
                     '{}-{}.csv'.format(args.output_basename, i + 1)
                 )
         self.console_files = []
-        """per-chain sample console output files."""
+        # per-chain sample console output files.
         for i in range(chains):
             txt_file = ''.join([os.path.splitext(self.csv_files[i])[0], '.txt'])
             self.console_files.append(txt_file)
         self.cmds = [
             args.compose_command(i, self.csv_files[i]) for i in range(chains)
         ]
-        """per-chain sampler command."""
+        # per-chain sampler command.
         self._retcodes = [-1 for _ in range(chains)]
         self._draws = None
         self._column_names = None
@@ -81,7 +81,7 @@ class StanFit(object):
 
     @property
     def model(self) -> str:
-        """Stan model name"""
+        """Stan model name."""
         return self._args.model_name
 
     @property
@@ -113,7 +113,7 @@ class StanFit(object):
 
     @property
     def metric_type(self) -> str:
-        """Metric type, either 'diag_e' or 'dense_e'"""
+        """Metric type, either 'diag_e' or 'dense_e'."""
         return self._metric_type
 
     @property
@@ -132,27 +132,28 @@ class StanFit(object):
 
     @property
     def is_optimizing(self) -> bool:
-        """Returns true if we are optimizing rather than sampling"""
+        """Returns true if we are optimizing rather than sampling."""
         return self._is_optimizing
 
     @property
     def optimized_params_np(self) -> np.array:
-        """returns optimized params as numpy array"""
+        """Returns optimized params as numpy array."""
         return self._first_draw
 
     @property
     def optimized_params_pd(self) -> pd.DataFrame:
-        """returns optimized params as pandas DataFrame"""
+        """Returns optimized params as pandas DataFrame."""
         return pd.DataFrame([self._first_draw], columns=self.column_names)
 
     @property
     def optimized_params_dict(self) -> OrderedDict:
-        """returns optimized params as Dict"""
+        """Returns optimized params as Dict."""
         return OrderedDict(zip(self.column_names, self._first_draw))
 
     def _sampling_only(self):
+        """Raise RuntimeError if method is not sampling."""
         if self.is_optimizing:
-            raise RuntimeError("Method available only when sampling!")
+            raise RuntimeError('Method available only when sampling!')
 
     @property
     def sample(self) -> np.ndarray:
@@ -176,7 +177,7 @@ class StanFit(object):
         return True
 
     def _retcode(self, idx: int) -> int:
-        """get retcode for chain[idx]."""
+        """Get retcode for chain[idx]."""
         return self._retcodes[idx]
 
     def _set_retcode(self, idx: int, val: int) -> None:
@@ -299,24 +300,22 @@ class StanFit(object):
         tmp_csv_file = 'stansummary-{}-{}-chains-'.format(
             self._args.model_name, self.chains
         )
-        fd, tmp_csv_path = tempfile.mkstemp(
-            suffix='.csv', prefix=tmp_csv_file, dir=TMPDIR, text=True
-        )
-        cmd = '{} --csv_file={} {}'.format(
-            cmd_path, tmp_csv_path, ' '.join(self.csv_files)
-        )
-        # breaks on all whitespace
-        do_command(cmd.split(), logger=self._logger)
-        summary_data = pd.read_csv(
-            tmp_csv_path, delimiter=',', header=0, index_col=0, comment='#'
+        tmp_csv_path = create_named_text_file(
+            dir=TMPDIR, prefix=tmp_csv_file, suffix='.csv'
         )
+        cmd = [cmd_path, '--csv_file={}'.format(tmp_csv_path)] + self.csv_files
+        do_command(cmd, logger=self._logger)
+        with open(tmp_csv_path, 'rb') as fd:
+            summary_data = pd.read_csv(
+                fd, delimiter=',', header=0, index_col=0, comment='#'
+            )
         mask = [x == 'lp__' or not x.endswith('__') for x in summary_data.index]
         return summary_data[mask]
 
     def diagnose(self) -> str:
         """
         Run cmdstan/bin/diagnose over all output csv files.
-        Returns output of diagnose (stdout/stderr)
+        Returns output of diagnose (stdout/stderr).
 
         The diagnose utility reads the outputs of all chains
         and checks for the following potential problems:
@@ -332,9 +331,8 @@ class StanFit(object):
         self._sampling_only()
 
         cmd_path = os.path.join(cmdstan_path(), 'bin', 'diagnose' + EXTENSION)
-        csv_files = ' '.join(self.csv_files)
-        cmd = '{} {} '.format(cmd_path, csv_files)
-        result = do_command(cmd=cmd.split(), logger=self._logger)
+        cmd = [cmd_path] + self.csv_files
+        result = do_command(cmd=cmd, logger=self._logger)
         if result:
             self._logger.warning(result)
         return result
@@ -402,7 +400,7 @@ class StanFit(object):
                 )
                 shutil.move(self.csv_files[i], to_path)
                 self.csv_files[i] = to_path
-            except (IOError, OSError) as e:
+            except (IOError, OSError, PermissionError) as e:
                 raise ValueError(
                     'cannot save to file: {}'.format(to_path)
                 ) from e
diff --git a/cmdstanpy/utils.py b/cmdstanpy/utils.py
index 9cd9824..0593026 100644
--- a/cmdstanpy/utils.py
+++ b/cmdstanpy/utils.py
@@ -51,17 +51,16 @@ def get_latest_cmdstan(dot_dir: str) -> str:
 class MaybeDictToFilePath(object):
     def __init__(self, *objs: Union[str, dict], logger: logging.Logger = None):
         self._unlink = [False] * len(objs)
-        self._paths = [""] * len(objs)
+        self._paths = [''] * len(objs)
         self._logger = logger or get_logger()
         i = 0
         for o in objs:
             if isinstance(o, dict):
-                with tempfile.NamedTemporaryFile(
-                    mode='w+', suffix='.json', dir=TMPDIR, delete=False
-                ) as fd:
-                    data_file = fd.name
-                    self._logger.debug('input tempfile: %s', fd.name)
-                    jsondump(data_file, o)
+                data_file = create_named_text_file(
+                    dir=TMPDIR, prefix='', suffix='.json'
+                )
+                self._logger.debug('input tempfile: %s', data_file)
+                jsondump(data_file, o)
                 self._paths[i] = data_file
                 self._unlink[i] = True
             elif isinstance(o, str):
@@ -71,7 +70,7 @@ class MaybeDictToFilePath(object):
             elif o is None:
                 self._paths[i] = None
             else:
-                raise ValueError("data must be string or dict")
+                raise ValueError('data must be string or dict')
             i += 1
 
     def __enter__(self):
@@ -80,7 +79,10 @@ class MaybeDictToFilePath(object):
     def __exit__(self, exc_type, exc_val, exc_tb):
         for can_unlink, path in zip(self._unlink, self._paths):
             if can_unlink and path:
-                os.remove(path)
+                try:
+                    os.remove(path)
+                except PermissionError:
+                    pass
 
 
 def validate_cmdstan_path(path: str) -> None:
@@ -101,15 +103,25 @@ class TemporaryCopiedFile(object):
     def __init__(self, file_path: str):
         self._path = None
         self._tmpdir = None
-        if " " in file_path:
+        if ' ' in os.path.abspath(file_path) and platform.system() == 'Windows':
+            base_path, file_name = os.path.split(os.path.abspath(file_path))
+            os.makedirs(base_path, exist_ok=True)
+            try:
+                short_base_path = windows_short_path(base_path)
+                if os.path.exists(short_base_path):
+                    file_path = os.path.join(short_base_path, file_name)
+            except RuntimeError:
+                pass
+
+        if ' ' in os.path.abspath(file_path):
             tmpdir = tempfile.mkdtemp()
-            if " " in tmpdir:
+            if ' ' in tmpdir:
                 raise RuntimeError(
-                    "Unable to generate temporary path without spaces! \n"
-                    + "Please move your stan file to location without spaces."
+                    'Unable to generate temporary path without spaces! \n'
+                    + 'Please move your stan file to location without spaces.'
                 )
 
-            _, path = tempfile.mkstemp(suffix=".stan", dir=tmpdir)
+            _, path = tempfile.mkstemp(suffix='.stan', dir=tmpdir)
 
             shutil.copy(file_path, path)
             self._path = path
@@ -414,12 +426,14 @@ def read_rdump_metric(path: str) -> List[int]:
         data = fp.read().replace('\n', '')
         m1 = re.search(r'inv_metric\s*<-\s*structure\(\s*c\(', data)
         if not m1:
-            return None
-        m2 = re.search(r'\.Dim\s*=\s*c\(([^)]+)\)', data, m1.end())
-        if not m2:
-            return None
-        dims = m2.group(1).split(',')
-        return [int(d) for d in dims]
+            return_value = None
+        else:
+            m2 = re.search(r'\.Dim\s*=\s*c\(([^)]+)\)', data, m1.end())
+            if not m2:
+                return_value = None
+            dims = m2.group(1).split(',')
+            return_value = [int(d) for d in dims]
+    return return_value
 
 
 def do_command(cmd: str, cwd: str = None, logger: logging.Logger = None) -> str:
@@ -432,7 +446,6 @@ def do_command(cmd: str, cwd: str = None, logger: logging.Logger = None) -> str:
     proc = subprocess.Popen(
         cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
     )
-    proc.wait()
     stdout, stderr = proc.communicate()
     if proc.returncode:
         if stderr:
@@ -441,3 +454,69 @@ def do_command(cmd: str, cwd: str = None, logger: logging.Logger = None) -> str:
     if stdout:
         return stdout.decode('utf-8').strip()
     return None
+
+
+def windows_short_path(path: str) -> str:
+    """
+    Gets the short path name of a given long path.
+    http://stackoverflow.com/a/23598461/200291
+
+    On non-Windows platforms, returns the path
+
+    If (base)path does not exist, function raises RuntimeError
+    """
+    if platform.system() != 'Windows':
+        return path
+
+    if os.path.isfile(path) or (
+        not os.path.isdir(path) and os.path.splitext(path)[1] != ''
+    ):
+        base_path, file_name = os.path.split(path)
+    else:
+        base_path, file_name = path, ''
+
+    if not os.path.exists(base_path):
+        raise RuntimeError(
+            'Windows short path function needs a valid directory. '
+            'Base directory does not exist: "{}"'.format(base_path)
+        )
+
+    import ctypes
+    from ctypes import wintypes
+
+    _GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW
+    _GetShortPathNameW.argtypes = [
+        wintypes.LPCWSTR,
+        wintypes.LPWSTR,
+        wintypes.DWORD,
+    ]
+    _GetShortPathNameW.restype = wintypes.DWORD
+
+    output_buf_size = 0
+    while True:
+        output_buf = ctypes.create_unicode_buffer(output_buf_size)
+        needed = _GetShortPathNameW(base_path, output_buf, output_buf_size)
+        if output_buf_size >= needed:
+            short_base_path = output_buf.value
+            break
+        else:
+            output_buf_size = needed
+
+    short_path = (
+        os.path.join(short_base_path, file_name)
+        if file_name
+        else short_base_path
+    )
+    return short_path
+
+
+def create_named_text_file(dir: str, prefix: str, suffix: str) -> str:
+    """
+    Create a named unique file.
+    """
+    fd = tempfile.NamedTemporaryFile(
+        mode='w+', prefix=prefix, suffix=suffix, dir=dir, delete=False
+    )
+    path = fd.name
+    fd.close()
+    return path


Test patch:
diff --git a/test/test_cmdstan_args.py b/test/test_cmdstan_args.py
index ad59bc4..ca69bdb 100644
--- a/test/test_cmdstan_args.py
+++ b/test/test_cmdstan_args.py
@@ -9,33 +9,33 @@ from cmdstanpy.cmdstan_args import (
     OptimizeArgs,
 )
 
-datafiles_path = os.path.join("test", "data")
+datafiles_path = os.path.join('test', 'data')
 
 
 class OptimizeArgsTest(unittest.TestCase):
     def test_args_algorithm(self):
-        args = OptimizeArgs(algorithm="xxx")
+        args = OptimizeArgs(algorithm='non-valid_algorithm')
         self.assertRaises(ValueError, lambda: args.validate())
-        args = OptimizeArgs(algorithm="Newton")
+        args = OptimizeArgs(algorithm='Newton')
         args.validate()
         cmd = args.compose(None, 'output')
-        self.assertIn("algorithm=newton", cmd)
+        self.assertIn('algorithm=newton', cmd)
 
     def test_args_algorithm_init_alpha(self):
         args = OptimizeArgs(init_alpha=2e-4)
         args.validate()
         cmd = args.compose(None, 'output')
-        self.assertIn("init_alpha=0.0002", cmd)
+        self.assertIn('init_alpha=0.0002', cmd)
         args = OptimizeArgs(init_alpha=-1.0)
         self.assertRaises(ValueError, lambda: args.validate())
-        args = OptimizeArgs(init_alpha=1.0, algorithm="Newton")
+        args = OptimizeArgs(init_alpha=1.0, algorithm='Newton')
         self.assertRaises(ValueError, lambda: args.validate())
 
     def test_args_algorithm_iter(self):
         args = OptimizeArgs(iter=400)
         args.validate()
         cmd = args.compose(None, 'output')
-        self.assertIn("iter=400", cmd)
+        self.assertIn('iter=400', cmd)
         args = OptimizeArgs(iter=-1)
         self.assertRaises(ValueError, lambda: args.validate())
 
@@ -220,7 +220,7 @@ class CmdStanArgsTest(unittest.TestCase):
             inits=jinits,
             method_args=sampler_args,
         )
-        self.assertIn("init=", cmdstan_args.compose_command(None, "out.csv"))
+        self.assertIn('init=', cmdstan_args.compose_command(None, 'out.csv'))
 
         with self.assertRaises(ValueError):
             CmdStanArgs(
diff --git a/test/test_model.py b/test/test_model.py
index 304e591..caea9b4 100644
--- a/test/test_model.py
+++ b/test/test_model.py
@@ -5,7 +5,8 @@ from cmdstanpy.utils import EXTENSION
 from cmdstanpy.model import Model
 import numpy as np
 
-datafiles_path = os.path.join('test', 'data')
+here = os.path.dirname(os.path.abspath(__file__))
+datafiles_path = os.path.join(here, 'data')
 
 code = '''data {
   int<lower=0> N;
@@ -74,16 +75,16 @@ class ModelTest(unittest.TestCase):
         model = Model(stan_file=stan)
         self.assertEqual(None, model.exe_file)
         model.compile()
-        self.assertTrue(model.exe_file.endswith(exe))
+        self.assertTrue(model.exe_file.endswith(exe.replace('\\', '/')))
 
         model = Model(stan_file=stan)
         if os.path.exists(exe):
             os.remove(exe)
         model.compile()
-        self.assertTrue(model.exe_file.endswith(exe))
+        self.assertTrue(model.exe_file.endswith(exe.replace('\\', '/')))
 
         stan = os.path.join(datafiles_path, 'bernoulli_include.stan')
-        exe = os.path.join(datafiles_path, 'bernoulli_include')
+        exe = os.path.join(datafiles_path, 'bernoulli_include' + EXTENSION)
         here = os.path.dirname(os.path.abspath(__file__))
         datafiles_abspath = os.path.join(here, 'data')
         include_paths = [datafiles_abspath]
@@ -92,7 +93,7 @@ class ModelTest(unittest.TestCase):
         model = Model(stan_file=stan)
         model.compile(include_paths=include_paths)
         self.assertEqual(stan, model.stan_file)
-        self.assertTrue(model.exe_file.endswith(exe))
+        self.assertTrue(model.exe_file.endswith(exe.replace('\\', '/')))
 
     # TODO: test compile with existing exe - timestamp on exe unchanged
     # TODO: test overwrite with existing exe - timestamp on exe updated
@@ -100,7 +101,7 @@ class ModelTest(unittest.TestCase):
 
 class OptimizeTest(unittest.TestCase):
     def test_optimize_works(self):
-        exe = os.path.join(datafiles_path, 'bernoulli')
+        exe = os.path.join(datafiles_path, 'bernoulli' + EXTENSION)
         stan = os.path.join(datafiles_path, 'bernoulli.stan')
         model = Model(stan_file=stan, exe_file=exe)
         jdata = os.path.join(datafiles_path, 'bernoulli.data.json')
@@ -109,7 +110,7 @@ class OptimizeTest(unittest.TestCase):
             data=jdata,
             seed=1239812093,
             inits=jinit,
-            algorithm="BFGS",
+            algorithm='BFGS',
             init_alpha=0.001,
             iter=100,
         )
@@ -128,24 +129,24 @@ class OptimizeTest(unittest.TestCase):
 
         # test pandas output
         self.assertEqual(
-            fit.optimized_params_np[0], fit.optimized_params_pd["lp__"][0]
+            fit.optimized_params_np[0], fit.optimized_params_pd['lp__'][0]
         )
         self.assertEqual(
-            fit.optimized_params_np[1], fit.optimized_params_pd["theta"][0]
+            fit.optimized_params_np[1], fit.optimized_params_pd['theta'][0]
         )
 
         # test dict output
         self.assertEqual(
-            fit.optimized_params_np[0], fit.optimized_params_dict["lp__"]
+            fit.optimized_params_np[0], fit.optimized_params_dict['lp__']
         )
         self.assertEqual(
-            fit.optimized_params_np[1], fit.optimized_params_dict["theta"]
+            fit.optimized_params_np[1], fit.optimized_params_dict['theta']
         )
 
     def test_optimize_works_dict(self):
         import json
 
-        exe = os.path.join(datafiles_path, 'bernoulli')
+        exe = os.path.join(datafiles_path, 'bernoulli' + EXTENSION)
         stan = os.path.join(datafiles_path, 'bernoulli.stan')
         model = Model(stan_file=stan, exe_file=exe)
         with open(os.path.join(datafiles_path, 'bernoulli.data.json')) as d:
@@ -156,7 +157,7 @@ class OptimizeTest(unittest.TestCase):
             data=data,
             seed=1239812093,
             inits=init,
-            algorithm="BFGS",
+            algorithm='BFGS',
             init_alpha=0.001,
             iter=100,
         )
diff --git a/test/test_stanfit.py b/test/test_stanfit.py
index fc94bb4..8868125 100644
--- a/test/test_stanfit.py
+++ b/test/test_stanfit.py
@@ -6,14 +6,15 @@ from cmdstanpy.utils import EXTENSION
 from cmdstanpy.stanfit import StanFit
 from cmdstanpy.model import Model
 
-datafiles_path = os.path.join('test', 'data')
+here = os.path.dirname(os.path.abspath(__file__))
+datafiles_path = os.path.join(here, 'data')
 goodfiles_path = os.path.join(datafiles_path, 'runset-good')
 badfiles_path = os.path.join(datafiles_path, 'runset-bad')
 
 
 class StanFitTest(unittest.TestCase):
     def test_check_retcodes(self):
-        exe = os.path.join(datafiles_path, 'bernoulli')
+        exe = os.path.join(datafiles_path, 'bernoulli' + EXTENSION)
         jdata = os.path.join(datafiles_path, 'bernoulli.data.json')
         sampler_args = SamplerArgs()
         cmdstan_args = CmdStanArgs(
@@ -39,7 +40,7 @@ class StanFitTest(unittest.TestCase):
 
     def test_validate_good_run(self):
         # construct fit using existing sampler output
-        exe = os.path.join(datafiles_path, 'bernoulli')
+        exe = os.path.join(datafiles_path, 'bernoulli' + EXTENSION)
         jdata = os.path.join(datafiles_path, 'bernoulli.data.json')
         output = os.path.join(goodfiles_path, 'bern')
         sampler_args = SamplerArgs(
@@ -84,10 +85,12 @@ class StanFitTest(unittest.TestCase):
                 '\nEffective sample size satisfactory.',
             ]
         )
-        self.assertIn(expected, fit.diagnose())
+        self.assertIn(expected, fit.diagnose().replace("\r\n", "\n"))
 
     def test_validate_big_run(self):
-        exe = os.path.join(datafiles_path, 'bernoulli')  # fake out validation
+        exe = os.path.join(
+            datafiles_path, 'bernoulli' + EXTENSION
+        )  # fake out validation
         output = os.path.join(datafiles_path, 'runset-big', 'output_icar_nyc')
         sampler_args = SamplerArgs()
         cmdstan_args = CmdStanArgs(
@@ -171,7 +174,9 @@ class StanFitTest(unittest.TestCase):
             os.remove(bern_fit.console_files[i])
 
     def test_diagnose_divergences(self):
-        exe = os.path.join(datafiles_path, 'bernoulli')  # fake out validation
+        exe = os.path.join(
+            datafiles_path, 'bernoulli' + EXTENSION
+        )  # fake out validation
         output = os.path.join(
             datafiles_path, 'diagnose-good', 'corr_gauss_depth8'
         )
@@ -195,10 +200,10 @@ class StanFitTest(unittest.TestCase):
                 'For optimal performance, increase this limit.',
             ]
         )
-        self.assertIn(expected, fit.diagnose())
+        self.assertIn(expected, fit.diagnose().replace("\r\n", "\n"))
 
     def test_validate_bad_run(self):
-        exe = os.path.join(datafiles_path, 'bernoulli')
+        exe = os.path.join(datafiles_path, 'bernoulli' + EXTENSION)
         jdata = os.path.join(datafiles_path, 'bernoulli.data.json')
         sampler_args = SamplerArgs(
             sampling_iters=100, max_treedepth=11, adapt_delta=0.95
diff --git a/test/test_utils.py b/test/test_utils.py
index 13b2777..f93cd80 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -1,10 +1,12 @@
 import os
 import os.path
 import unittest
+import platform
 import tempfile
 import shutil
 import json
 
+from cmdstanpy import TMPDIR
 from cmdstanpy.utils import (
     cmdstan_path,
     set_cmdstan_path,
@@ -14,10 +16,11 @@ from cmdstanpy.utils import (
     MaybeDictToFilePath,
     read_metric,
     TemporaryCopiedFile,
+    windows_short_path,
 )
 
-
-datafiles_path = os.path.join('test', 'data')
+here = os.path.dirname(os.path.abspath(__file__))
+datafiles_path = os.path.join(here, 'data')
 
 rdump = '''N <- 10
 y <- c(0, 1, 0, 0, 0, 0, 0, 0, 0, 1)
@@ -32,16 +35,16 @@ class CmdStanPathTest(unittest.TestCase):
         self.assertTrue(cmdstan_path().startswith(abs_rel_path))
 
     def test_non_spaces_location(self):
-        good_path = "/tmp/"
+        good_path = os.path.join(TMPDIR, 'good_dir')
         with TemporaryCopiedFile(good_path) as (p, is_changed):
             self.assertEqual(p, good_path)
             self.assertFalse(is_changed)
 
         # prepare files for test
-        bad_path = os.path.join(tempfile.mkdtemp(), "bad dir")
-        os.mkdir(bad_path)
+        bad_path = os.path.join(TMPDIR, 'bad dir')
+        os.makedirs(bad_path, exist_ok=True)
         stan = os.path.join(datafiles_path, 'bernoulli.stan')
-        stan_bad = os.path.join(bad_path, "bad name.stan")
+        stan_bad = os.path.join(bad_path, 'bad name.stan')
         shutil.copy(stan, stan_bad)
 
         stan_copied = None
@@ -49,13 +52,14 @@ class CmdStanPathTest(unittest.TestCase):
             with TemporaryCopiedFile(stan_bad) as (p, is_changed):
                 stan_copied = p
                 self.assertTrue(os.path.exists(stan_copied))
-                self.assertTrue(" " not in stan_copied)
+                self.assertTrue(' ' not in stan_copied)
                 self.assertTrue(is_changed)
                 raise RuntimeError
         except RuntimeError:
             pass
 
-        self.assertFalse(os.path.exists(stan_copied))
+        if platform.system() != 'Windows':
+            self.assertFalse(os.path.exists(stan_copied))
 
         # cleanup after test
         shutil.rmtree(bad_path, ignore_errors=True)
@@ -84,7 +88,7 @@ class CmdStanPathTest(unittest.TestCase):
 
     def test_dict_to_file(self):
         file_good = os.path.join(datafiles_path, 'bernoulli_output_1.csv')
-        dict_good = {"a": "A"}
+        dict_good = {'a': 'A'}
         created_tmp = None
         with MaybeDictToFilePath(file_good, dict_good) as (f1, f2):
             self.assertTrue(os.path.exists(f1))
@@ -202,5 +206,47 @@ class ReadMetricTest(unittest.TestCase):
             dims = read_metric(metric_file)
 
 
+class WindowsShortPath(unittest.TestCase):
+    def test_windows_short_path_directory(self):
+        if platform.system() != 'Windows':
+            return
+        original_path = os.path.join(TMPDIR, 'new path')
+        os.makedirs(original_path, exist_ok=True)
+        assert os.path.exists(original_path)
+        assert ' ' in original_path
+        short_path = windows_short_path(original_path)
+        assert os.path.exists(short_path)
+        assert original_path != short_path
+        assert ' ' not in short_path
+
+    def test_windows_short_path_file(self):
+        if platform.system() != 'Windows':
+            return
+        original_path = os.path.join(TMPDIR, 'new path', 'my_file.csv')
+        os.makedirs(os.path.split(original_path)[0], exist_ok=True)
+        assert os.path.exists(os.path.split(original_path)[0])
+        assert ' ' in original_path
+        assert '.csv' == os.path.splitext(original_path)[1]
+        short_path = windows_short_path(original_path)
+        assert os.path.exists(os.path.split(short_path)[0])
+        assert original_path != short_path
+        assert ' ' not in short_path
+        assert '.csv' == os.path.splitext(short_path)[1]
+
+    def test_windows_short_path_file_with_space(self):
+        """Test that the function doesn't touch filename."""
+        if platform.system() != 'Windows':
+            return
+        original_path = os.path.join(TMPDIR, 'new path', 'my file.csv')
+        os.makedirs(os.path.split(original_path)[0], exist_ok=True)
+        assert os.path.exists(os.path.split(original_path)[0])
+        assert ' ' in original_path
+        short_path = windows_short_path(original_path)
+        assert os.path.exists(os.path.split(short_path)[0])
+        assert original_path != short_path
+        assert ' ' in short_path
+        assert '.csv' == os.path.splitext(short_path)[1]
+
+
 if __name__ == '__main__':
     unittest.main()