1"""
2Tests for msvc support module.
3"""
4
5import os
6import contextlib
7import distutils.errors
8import mock
9
10import pytest
11
12from . import contexts
13
14# importing only setuptools should apply the patch
15__import__('setuptools')
16
17pytest.importorskip("distutils.msvc9compiler")
18
19
20def mock_reg(hkcu=None, hklm=None):
21    """
22    Return a mock for distutils.msvc9compiler.Reg, patched
23    to mock out the functions that access the registry.
24    """
25
26    _winreg = getattr(distutils.msvc9compiler, '_winreg', None)
27    winreg = getattr(distutils.msvc9compiler, 'winreg', _winreg)
28
29    hives = {
30        winreg.HKEY_CURRENT_USER: hkcu or {},
31        winreg.HKEY_LOCAL_MACHINE: hklm or {},
32    }
33
34    @classmethod
35    def read_keys(cls, base, key):
36        """Return list of registry keys."""
37        hive = hives.get(base, {})
38        return [
39            k.rpartition('\\')[2]
40            for k in hive if k.startswith(key.lower())
41        ]
42
43    @classmethod
44    def read_values(cls, base, key):
45        """Return dict of registry keys and values."""
46        hive = hives.get(base, {})
47        return dict(
48            (k.rpartition('\\')[2], hive[k])
49            for k in hive if k.startswith(key.lower())
50        )
51
52    return mock.patch.multiple(
53        distutils.msvc9compiler.Reg,
54        read_keys=read_keys, read_values=read_values)
55
56
57class TestModulePatch:
58    """
59    Ensure that importing setuptools is sufficient to replace
60    the standard find_vcvarsall function with a version that
61    recognizes the "Visual C++ for Python" package.
62    """
63
64    key_32 = r'software\microsoft\devdiv\vcforpython\9.0\installdir'
65    key_64 = key_32.replace(r'\microsoft', r'\wow6432node\microsoft')
66
67    def test_patched(self):
68        "Test the module is actually patched"
69        mod_name = distutils.msvc9compiler.find_vcvarsall.__module__
70        assert mod_name == "setuptools.msvc", "find_vcvarsall unpatched"
71
72    def test_no_registry_entries_means_nothing_found(self):
73        """
74        No registry entries or environment variable should lead to an error
75        directing the user to download vcpython27.
76        """
77        find_vcvarsall = distutils.msvc9compiler.find_vcvarsall
78        query_vcvarsall = distutils.msvc9compiler.query_vcvarsall
79
80        with contexts.environment(VS90COMNTOOLS=None):
81            with mock_reg():
82                assert find_vcvarsall(9.0) is None
83
84                try:
85                    query_vcvarsall(9.0)
86                except Exception as exc:
87                    expected = distutils.errors.DistutilsPlatformError
88                    assert isinstance(exc, expected)
89                    assert 'aka.ms/vcpython27' in str(exc)
90
91    @pytest.yield_fixture
92    def user_preferred_setting(self):
93        """
94        Set up environment with different install dirs for user vs. system
95        and yield the user_install_dir for the expected result.
96        """
97        with self.mock_install_dir() as user_install_dir:
98            with self.mock_install_dir() as system_install_dir:
99                reg = mock_reg(
100                    hkcu={
101                        self.key_32: user_install_dir,
102                    },
103                    hklm={
104                        self.key_32: system_install_dir,
105                        self.key_64: system_install_dir,
106                    },
107                )
108                with reg:
109                    yield user_install_dir
110
111    def test_prefer_current_user(self, user_preferred_setting):
112        """
113        Ensure user's settings are preferred.
114        """
115        result = distutils.msvc9compiler.find_vcvarsall(9.0)
116        expected = os.path.join(user_preferred_setting, 'vcvarsall.bat')
117        assert expected == result
118
119    @pytest.yield_fixture
120    def local_machine_setting(self):
121        """
122        Set up environment with only the system environment configured.
123        """
124        with self.mock_install_dir() as system_install_dir:
125            reg = mock_reg(
126                hklm={
127                    self.key_32: system_install_dir,
128                },
129            )
130            with reg:
131                yield system_install_dir
132
133    def test_local_machine_recognized(self, local_machine_setting):
134        """
135        Ensure machine setting is honored if user settings are not present.
136        """
137        result = distutils.msvc9compiler.find_vcvarsall(9.0)
138        expected = os.path.join(local_machine_setting, 'vcvarsall.bat')
139        assert expected == result
140
141    @pytest.yield_fixture
142    def x64_preferred_setting(self):
143        """
144        Set up environment with 64-bit and 32-bit system settings configured
145        and yield the canonical location.
146        """
147        with self.mock_install_dir() as x32_dir:
148            with self.mock_install_dir() as x64_dir:
149                reg = mock_reg(
150                    hklm={
151                        # This *should* only exist on 32-bit machines
152                        self.key_32: x32_dir,
153                        # This *should* only exist on 64-bit machines
154                        self.key_64: x64_dir,
155                    },
156                )
157                with reg:
158                    yield x32_dir
159
160    def test_ensure_64_bit_preferred(self, x64_preferred_setting):
161        """
162        Ensure 64-bit system key is preferred.
163        """
164        result = distutils.msvc9compiler.find_vcvarsall(9.0)
165        expected = os.path.join(x64_preferred_setting, 'vcvarsall.bat')
166        assert expected == result
167
168    @staticmethod
169    @contextlib.contextmanager
170    def mock_install_dir():
171        """
172        Make a mock install dir in a unique location so that tests can
173        distinguish which dir was detected in a given scenario.
174        """
175        with contexts.tempdir() as result:
176            vcvarsall = os.path.join(result, 'vcvarsall.bat')
177            with open(vcvarsall, 'w'):
178                pass
179            yield result
180