Repository: nerdvegas/rez
Pull request #775 (2019-10-25 15:50:55)

Fixes some failing tests on windows
I took a stab at addressing some of the failing tests under python3+windows.

* corrects failing tests in `rez/tests/test_packages.py` on windows
* adds `%SYSTEMROOT%` envvar to Python(ActionInterpreter) so `rez/tests/test_context.py` tests pass on windows


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa0a0f4b..601351b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Change Log
 
+
+## 2.47.10 (2019-10-26)
+[Source](https://github.com/repos/nerdvegas/rez/tree/2.47.10) | [Diff](https://github.com/repos/nerdvegas/rez/compare/2.47.9...2.47.10)
+
+Notes
+
+* corrects failing tests in `rez/tests/test_packages.py` on windows
+* adds %SYSTEMROOT% envvar to Python(ActionInterpreter) so `rez/tests/test_context.py` tests pass on windows
+
+
 ## 2.47.9 (2019-10-25)
 [Source](https://github.com/repos/nerdvegas/rez/tree/2.47.9) | [Diff](https://github.com/repos/nerdvegas/rez/compare/2.47.8...2.47.9)
 
diff --git a/src/rez/rex.py b/src/rez/rex.py
index 1fd3bdd0..4f0c22d7 100644
--- a/src/rez/rex.py
+++ b/src/rez/rex.py
@@ -591,6 +591,7 @@ class Python(ActionInterpreter):
                                  "interpreter before using it.")
 
         self.target_environ.update(self.manager.environ)
+        self.adjust_env_for_platform(self.target_environ)
 
     def get_output(self, style=OutputStyle.file):
         self.apply_environ()
@@ -633,6 +634,7 @@ class Python(ActionInterpreter):
     def subprocess(self, args, **subproc_kwargs):
         if self.manager:
             self.target_environ.update(self.manager.environ)
+        self.adjust_env_for_platform(self.target_environ)
 
         shell_mode = isinstance(args, basestring)
         return Popen(args,
@@ -684,6 +686,54 @@ class Python(ActionInterpreter):
         # here because the API requires it.
         return "${%s}" % key
 
+    def adjust_env_for_platform(self, env):
+        """ Make required platform-specific adjustments to env.
+        """
+        if sys.platform.startswith('win'):
+            self._add_systemroot_to_env_win32(env)
+
+    def _add_systemroot_to_env_win32(self, env):
+        """ Sets ``%SYSTEMROOT%`` environment variable, if not present
+        in :py:attr:`target_environ` .
+
+        Args:
+            env (dict): desired environment variables
+
+        Notes:
+            on windows, python-3.6 startup fails within an environment
+            where it ``%PATH%`` includes python3, but ``%SYSTEMROOT%`` is not
+            present.
+
+            for example.
+
+            .. code-block:: python
+
+                from subprocess import Popen
+                cmds = ['python', '--version']
+
+                # successful
+                Popen(cmds)
+                Popen(cmds, env={'PATH': 'C:\\Python-3.6.5',
+                                 'SYSTEMROOT': 'C:\Windows'})
+
+                # failure
+                Popen(cmds, env={'PATH': 'C:\\Python-3.6.5'})
+
+                #> Fatal Python Error: failed to get random numbers to initialize Python
+
+        """
+        # 'SYSTEMROOT' unecessary unless 'PATH' is set.
+        if env is None:
+            return
+        # leave SYSTEMROOT alone if set by user
+        if 'SYSTEMROOT' in env:
+            return
+        # not enough info to set SYSTEMROOT
+        if 'SYSTEMROOT' not in os.environ:
+            return
+
+        env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
+
 
 #===============================================================================
 # String manipulation
diff --git a/src/rez/utils/_version.py b/src/rez/utils/_version.py
index be052760..7848f88d 100644
--- a/src/rez/utils/_version.py
+++ b/src/rez/utils/_version.py
@@ -1,7 +1,7 @@
 
 
 # Update this value to version up Rez. Do not place anything else in this file.
-_rez_version = "2.47.9"
+_rez_version = "2.47.10"
 
 
 # Copyright 2013-2016 Allan Johns.
diff --git a/src/rez/utils/filesystem.py b/src/rez/utils/filesystem.py
index b7a386dd..5e8b8874 100644
--- a/src/rez/utils/filesystem.py
+++ b/src/rez/utils/filesystem.py
@@ -19,6 +19,7 @@ import stat
 import platform
 
 from rez.vendor.six import six
+from rez.utils.platform_ import platform_
 
 
 class TempDirs(object):
@@ -424,7 +425,8 @@ def safe_chmod(path, mode):
 
 
 def to_nativepath(path):
-    return os.path.join(path.split('/'))
+    path = path.replace('\\', '/')
+    return os.path.join(*path.split('/'))
 
 
 def to_ntpath(path):
@@ -435,6 +437,33 @@ def to_posixpath(path):
     return posixpath.sep.join(path.split(ntpath.sep))
 
 
+def canonical_path(path, platform=None):
+    """ Resolves symlinks, and formats filepath.
+
+    Resolves symlinks, lowercases if filesystem is case-insensitive,
+    formats filepath using slashes appropriate for platform.
+
+    Args:
+        path (str):
+            filepath being formatted
+
+        platform (rez.utils.platform_.Platform, NoneType):
+            indicates platform path is being formatted for.
+            Defaults to current platform.
+
+    Returns:
+        str: provided path, formatted for platform.
+    """
+    if platform is None:
+        platform = platform_
+
+    path = os.path.normpath(os.path.realpath(path))
+
+    if not platform.has_case_sensitive_filesystem:
+        return path.lower()
+    return path
+
+
 def encode_filesystem_name(input_str):
     """Encodes an arbitrary unicode string to a generic filesystem-compatible
     non-unicode filename.
diff --git a/src/rezplugins/package_repository/filesystem.py b/src/rezplugins/package_repository/filesystem.py
index 8becadbc..0c283364 100644
--- a/src/rezplugins/package_repository/filesystem.py
+++ b/src/rezplugins/package_repository/filesystem.py
@@ -21,7 +21,7 @@ from rez.utils.formatting import is_valid_package_name
 from rez.utils.resources import cached_property
 from rez.utils.logging_ import print_warning
 from rez.utils.memcached import memcached, pool_memcached_connections
-from rez.utils.filesystem import make_path_writable
+from rez.utils.filesystem import make_path_writable, canonical_path
 from rez.utils.platform_ import platform_
 from rez.serialise import load_from_file, FileFormat
 from rez.config import config
@@ -470,8 +470,7 @@ class FileSystemPackageRepository(PackageRepository):
 
         # ensure that differing case doesn't get interpreted as different repos
         # on case-insensitive platforms (eg windows)
-        if not platform_.has_case_sensitive_filesystem:
-            location = location.lower()
+        location = canonical_path(location, platform_)
 
         super(FileSystemPackageRepository, self).__init__(location, resource_pool)
 


Test patch:
diff --git a/src/rez/tests/test_packages.py b/src/rez/tests/test_packages.py
index 7a781696..4fc79867 100644
--- a/src/rez/tests/test_packages.py
+++ b/src/rez/tests/test_packages.py
@@ -3,15 +3,17 @@ test package iteration, serialization etc
 """
 from rez.packages_ import iter_package_families, iter_packages, get_package, \
     create_package, get_developer_package
-from rez.package_resources_ import package_release_keys
-from rez.package_repository import create_memory_package_repository
 from rez.package_py_utils import expand_requirement
+from rez.package_repository import create_memory_package_repository
+from rez.package_resources_ import package_release_keys
 from rez.tests.util import TestBase, TempdirMixin
 from rez.utils.formatting import PackageRequest
+from rez.utils.platform_ import platform_
 from rez.utils.sourcecode import SourceCode
 import unittest
 from rez.vendor.version.version import Version
 from rez.vendor.version.util import VersionError
+from rez.utils.filesystem import canonical_path
 import os.path
 import os
 
@@ -119,21 +121,20 @@ class TestPackages(TestBase, TempdirMixin):
 
     def test_3(self):
         """check package contents."""
-
         # a py-based package
         package = get_package("versioned", "3.0")
         expected_data = dict(
             name="versioned",
             version=Version("3.0"),
-            base=os.path.join(self.py_packages_path, "versioned", "3.0"),
+            base=canonical_path(os.path.join(self.py_packages_path, "versioned", "3.0")),
             commands=SourceCode('env.PATH.append("{root}/bin")'))
         data = package.validated_data()
         self.assertDictEqual(data, expected_data)
 
         # a yaml-based package
         package = get_package("versioned", "2.0")
-        expected_uri = os.path.join(self.yaml_packages_path,
-                                    "versioned", "2.0", "package.yaml")
+        expected_uri = canonical_path(os.path.join(self.yaml_packages_path,
+                                            "versioned", "2.0", "package.yaml"))
         self.assertEqual(package.uri, expected_uri)
 
         # a py-based package with late binding attribute functions
@@ -142,7 +143,7 @@ class TestPackages(TestBase, TempdirMixin):
 
         # a 'combined' type package
         package = get_package("multi", "1.0")
-        expected_uri = os.path.join(self.yaml_packages_path, "multi.yaml<1.0>")
+        expected_uri = canonical_path(os.path.join(self.yaml_packages_path, "multi.yaml<1.0>"))
         self.assertEqual(package.uri, expected_uri)
         expected_data = dict(
             name="multi",
@@ -153,7 +154,7 @@ class TestPackages(TestBase, TempdirMixin):
 
         # a 'combined' type package, with version overrides
         package = get_package("multi", "1.1")
-        expected_uri = os.path.join(self.yaml_packages_path, "multi.yaml<1.1>")
+        expected_uri = canonical_path(os.path.join(self.yaml_packages_path, "multi.yaml<1.1>"))
         self.assertEqual(package.uri, expected_uri)
         expected_data = dict(
             name="multi",
@@ -164,7 +165,7 @@ class TestPackages(TestBase, TempdirMixin):
 
         # check that visibility of 'combined' packages is correct
         package = get_package("multi", "2.0")
-        expected_uri = os.path.join(self.py_packages_path, "multi.py<2.0>")
+        expected_uri = canonical_path(os.path.join(self.py_packages_path, "multi.py<2.0>"))
         self.assertEqual(package.uri, expected_uri)
 
     def test_4(self):
@@ -309,11 +310,12 @@ class TestPackages(TestBase, TempdirMixin):
 
     def test_6(self):
         """test variant iteration."""
+        base = canonical_path(os.path.join(self.py_packages_path, "variants_py", "2.0"))
         expected_data = dict(
             name="variants_py",
             version=Version("2.0"),
             description="package with variants",
-            base=os.path.join(self.py_packages_path, "variants_py", "2.0"),
+            base=base,
             requires=[PackageRequest("python-2.7")],
             commands=SourceCode('env.PATH.append("{root}/bin")'))
 
diff --git a/src/rez/tests/test_utils.py b/src/rez/tests/test_utils.py
new file mode 100644
index 00000000..c79721e7
--- /dev/null
+++ b/src/rez/tests/test_utils.py
@@ -0,0 +1,62 @@
+"""
+unit tests for 'utils.filesystem' module
+"""
+import os
+from rez.tests.util import TestBase
+from rez.utils import filesystem
+from rez.utils.platform_ import Platform, platform_
+
+
+class TestCanonicalPath(TestBase):
+    class CaseSensitivePlatform(Platform):
+        @property
+        def has_case_sensitive_filesystem(self):
+            return True
+
+    class CaseInsensitivePlatform(Platform):
+        @property
+        def has_case_sensitive_filesystem(self):
+            return False
+
+    def test_win32_case_insensitive(self):
+        if platform_.name != 'windows':
+            self.skipTest('on linux/macos, `os.path.realpath()` treats windows '
+                          'abspaths as relpaths, and prepends `os.getcwd()`')
+        platform = self.CaseInsensitivePlatform()
+        path = filesystem.canonical_path('C:\\dir\\File.txt', platform)
+        expects = 'c:\\dir\\file.txt'.replace('\\', os.sep)
+        self.assertEqual(path, expects)
+
+    def test_unix_case_sensistive_platform(self):
+        if platform_.name == 'windows':
+            self.skipTest('on windows, `os.path.realpath()` treats unix abspaths '
+                          'as relpaths, and prepends `os.getcwd()`')
+        platform = self.CaseSensitivePlatform()
+        path = filesystem.canonical_path('/a/b/File.txt', platform)
+        expects = '/a/b/File.txt'.replace('\\', os.sep)
+        self.assertEqual(path, expects)
+
+    def test_unix_case_insensistive_platform(self):
+        if platform_.name == 'windows':
+            self.skipTest('on windows, `os.path.realpath()` treats unix abspaths '
+                          'as relpaths, and prepends `os.getcwd()`')
+        platform = self.CaseInsensitivePlatform()
+        path = filesystem.canonical_path('/a/b/File.txt', platform)
+        expects = '/a/b/file.txt'.replace('\\', os.sep)
+        self.assertEqual(path, expects)
+
+
+# Copyright 2013-2016 Allan Johns.
+#
+# This library is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation, either
+# version 3 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.  If not, see <http://www.gnu.org/licenses/>.