Skip to content

Commit 3a68fd3

Browse files
committed
Improve test coverage
1 parent 823a321 commit 3a68fd3

File tree

4 files changed

+162
-63
lines changed

4 files changed

+162
-63
lines changed

scripts/test-firstrun.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Simple script to allow running manage/firstrun.py without rebuilding.
2+
3+
You'll need to build the test module (_msbuild_test.py).
4+
"""
5+
6+
import os
7+
import pathlib
8+
import sys
9+
10+
11+
ROOT = pathlib.Path(__file__).absolute().parent.parent / "src"
12+
sys.path.append(str(ROOT))
13+
14+
15+
import _native
16+
if not hasattr(_native, "coinitialize"):
17+
import _native_test
18+
for k in dir(_native_test):
19+
if k[:1] not in ("", "_"):
20+
setattr(_native, k, getattr(_native_test, k))
21+
22+
23+
import manage.commands
24+
cmd = manage.commands.FirstRun([], ROOT)
25+
sys.exit(cmd.execute() or 0)

src/manage/firstrun.py

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,6 @@
22
import sys
33
import time
44

5-
6-
if __name__ == "__main__":
7-
__package__ = "manage"
8-
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
9-
10-
import _native
11-
if not hasattr(_native, "coinitialize"):
12-
import _native_test
13-
for k in dir(_native_test):
14-
if k[:1] not in ("", "_"):
15-
setattr(_native, k, getattr(_native_test, k))
16-
17-
185
from . import logging
196
from .pathutils import Path
207

@@ -160,7 +147,7 @@ def do_global_dir_on_path(cmd):
160147
initial, kind = winreg.QueryValueEx(key, "Path")
161148
LOGGER.debug("Initial path: %s", initial)
162149
if kind not in (winreg.REG_SZ, winreg.REG_EXPAND_SZ) or not isinstance(initial, str):
163-
LOGGER.debug("Value kind is %s and not REG_[EXPAND_]SZ. Aborting.")
150+
LOGGER.debug("Value kind is %s and not REG_[EXPAND_]SZ. Aborting.", kind)
164151
return
165152
for p in initial.split(";"):
166153
if not p:
@@ -390,49 +377,3 @@ def first_run(cmd):
390377
"manager!W! from your Start menu, or !B!py install "
391378
"--configure!W! from the terminal.", wrap=True)
392379
line_break()
393-
394-
395-
if __name__ == "__main__":
396-
class TestCommand:
397-
enabled = True
398-
global_dir = Path(os.path.expandvars(r"%LocalAppData%\Python\bin"))
399-
explicit = False
400-
confirm = True
401-
check_app_alias = True
402-
check_long_paths = True
403-
check_py_on_path = True
404-
check_any_install = True
405-
check_global_dir = True
406-
check_default_tag = True
407-
408-
def get_installs(self, *args, **kwargs):
409-
import json
410-
root = Path(os.path.expandvars(r"%LocalAppData%\Python"))
411-
result = []
412-
for d in root.iterdir():
413-
inst = d / "__install__.json"
414-
try:
415-
result.append(json.loads(inst.read_text()))
416-
except FileNotFoundError:
417-
pass
418-
return result
419-
420-
def _ask(self, fmt, *args, yn_text="Y/n", expect_char="y"):
421-
if not self.confirm:
422-
return True
423-
LOGGER.print(f"{fmt} [{yn_text}] ", *args, end="")
424-
try:
425-
resp = input().casefold()
426-
except Exception:
427-
return False
428-
return not resp or resp.startswith(expect_char.casefold())
429-
430-
def ask_yn(self, fmt, *args):
431-
"Returns True if the user selects 'yes' or confirmations are skipped."
432-
return self._ask(fmt, *args)
433-
434-
def ask_ny(self, fmt, *args):
435-
"Returns True if the user selects 'no' or confirmations are skipped."
436-
return self._ask(fmt, *args, yn_text="y/N", expect_char="n")
437-
438-
first_run(TestCommand())

tests/conftest.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ def localserver():
146146

147147

148148
class FakeConfig:
149-
def __init__(self, installs=[]):
149+
def __init__(self, global_dir, installs=[]):
150+
self.global_dir = global_dir
150151
self.installs = list(installs)
151152
self.shebang_can_run_anything = True
152153
self.shebang_can_run_anything_silently = False
@@ -161,8 +162,8 @@ def get_install_to_run(self, tag):
161162

162163

163164
@pytest.fixture
164-
def fake_config():
165-
return FakeConfig()
165+
def fake_config(tmp_path):
166+
return FakeConfig(tmp_path / "bin")
166167

167168

168169
REG_TEST_ROOT = r"Software\Python\PyManagerTesting"

tests/test_firstrun.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import pytest
3+
import winreg
34

45
from pathlib import Path
56

@@ -105,3 +106,134 @@ def test_welcome(assert_log):
105106
assert_log(".*Welcome.*", assert_log.end_of_log())
106107
welcome()
107108
assert_log(".*Welcome.*", assert_log.end_of_log())
109+
110+
111+
112+
def test_firstrun_command(monkeypatch):
113+
from manage import commands
114+
115+
called_first_run = False
116+
called_show_usage = False
117+
118+
def fake_first_run(*args):
119+
nonlocal called_first_run
120+
called_first_run = True
121+
122+
def fake_show_usage(*args):
123+
nonlocal called_show_usage
124+
called_show_usage = True
125+
126+
monkeypatch.setattr(firstrun, "first_run", fake_first_run)
127+
monkeypatch.setattr(commands.FirstRun, "confirm", False)
128+
monkeypatch.setattr(commands.FirstRun, "show_usage", fake_show_usage)
129+
cmd = commands.find_command(["**first_run"], None)
130+
cmd.execute()
131+
assert called_first_run
132+
assert called_show_usage
133+
134+
135+
def test_install_configure_command(monkeypatch):
136+
from manage import commands
137+
138+
called_first_run = False
139+
called_show_usage = False
140+
141+
def fake_first_run(*args):
142+
nonlocal called_first_run
143+
called_first_run = True
144+
145+
def fake_show_usage(*args):
146+
nonlocal called_show_usage
147+
called_show_usage = True
148+
149+
monkeypatch.setattr(firstrun, "first_run", fake_first_run)
150+
monkeypatch.setattr(commands.FirstRun, "confirm", False)
151+
monkeypatch.setattr(commands.FirstRun, "show_usage", fake_show_usage)
152+
cmd = commands.find_command(["install", "--configure"], None)
153+
cmd.execute()
154+
assert called_first_run
155+
assert not called_show_usage
156+
157+
158+
def _create_key_read_only(key, subkey, *args, **kwargs):
159+
return winreg.OpenKeyEx(key, subkey)
160+
161+
162+
def _raise_oserror(*args, **kwargs):
163+
raise OSError("injected error")
164+
165+
166+
@pytest.fixture
167+
def protect_reg(monkeypatch):
168+
import _native
169+
monkeypatch.setattr(winreg, "CreateKeyEx", _create_key_read_only)
170+
monkeypatch.setattr(winreg, "SetValueEx", _raise_oserror)
171+
monkeypatch.setattr(_native, "broadcast_settings_change", lambda *a: None)
172+
173+
174+
def test_do_global_dir_open_fail(protect_reg, fake_config, assert_log, monkeypatch):
175+
monkeypatch.setattr(winreg, "OpenKeyEx", _raise_oserror)
176+
firstrun.do_global_dir_on_path(fake_config)
177+
assert_log(assert_log.skip_until("Failed to update PATH.+"))
178+
179+
180+
def test_do_global_dir_read_fail(protect_reg, fake_config, assert_log, monkeypatch):
181+
monkeypatch.setattr(winreg, "QueryValueEx", _raise_oserror)
182+
firstrun.do_global_dir_on_path(fake_config)
183+
assert_log(assert_log.skip_until("Failed to update PATH.+"))
184+
185+
186+
def test_do_global_dir_read_kind_fail(protect_reg, fake_config, assert_log, monkeypatch):
187+
monkeypatch.setattr(winreg, "QueryValueEx", lambda *a: (100, winreg.REG_DWORD))
188+
firstrun.do_global_dir_on_path(fake_config)
189+
assert_log(
190+
assert_log.skip_until("Initial path: %s", (100,)),
191+
("Value kind is %s.+", (winreg.REG_DWORD,)),
192+
)
193+
194+
195+
def test_do_global_dir_path_already_set(protect_reg, fake_config, assert_log, monkeypatch):
196+
monkeypatch.setattr(winreg, "QueryValueEx", lambda *a: (f"{fake_config.global_dir};b;c", winreg.REG_SZ))
197+
firstrun.do_global_dir_on_path(fake_config)
198+
assert_log(assert_log.skip_until("Path is already found"))
199+
200+
monkeypatch.setattr(winreg, "QueryValueEx", lambda *a: (f"a;{fake_config.global_dir};c", winreg.REG_SZ))
201+
firstrun.do_global_dir_on_path(fake_config)
202+
assert_log(assert_log.skip_until("Path is already found"))
203+
204+
monkeypatch.setattr(winreg, "QueryValueEx", lambda *a: (f"a;b;{fake_config.global_dir}", winreg.REG_SZ))
205+
firstrun.do_global_dir_on_path(fake_config)
206+
assert_log(assert_log.skip_until("Path is already found"))
207+
208+
209+
def test_do_global_dir_path_lost_race(protect_reg, fake_config, assert_log, monkeypatch):
210+
paths = ["a;b", "a;b;c"]
211+
monkeypatch.setattr(winreg, "QueryValueEx", lambda *a: (paths.pop(), winreg.REG_SZ))
212+
firstrun.do_global_dir_on_path(fake_config)
213+
assert_log(
214+
assert_log.skip_until("New path: %s", None),
215+
"Path is added successfully",
216+
"PATH has changed.+",
217+
)
218+
219+
220+
def test_do_global_dir_write_same_kind(protect_reg, fake_config, monkeypatch):
221+
saved = []
222+
monkeypatch.setattr(winreg, "SetValueEx", lambda *a: saved.append(a))
223+
224+
monkeypatch.setattr(winreg, "QueryValueEx", lambda *a: ("a;", winreg.REG_SZ))
225+
firstrun.do_global_dir_on_path(fake_config)
226+
assert saved[-1][1:] == ("Path", 0, winreg.REG_SZ, f"a;{fake_config.global_dir}")
227+
228+
monkeypatch.setattr(winreg, "QueryValueEx", lambda *a: ("a", winreg.REG_EXPAND_SZ))
229+
firstrun.do_global_dir_on_path(fake_config)
230+
assert saved[-1][1:] == ("Path", 0, winreg.REG_EXPAND_SZ, f"a;{fake_config.global_dir}")
231+
232+
233+
def test_do_global_dir_path_fail_broadcast(protect_reg, fake_config, assert_log, monkeypatch):
234+
import _native
235+
monkeypatch.setattr(_native, "broadcast_settings_change", _raise_oserror)
236+
monkeypatch.setattr(winreg, "QueryValueEx", lambda *a: ("a;", winreg.REG_SZ))
237+
monkeypatch.setattr(winreg, "SetValueEx", lambda *a: None)
238+
firstrun.do_global_dir_on_path(fake_config)
239+
assert_log(assert_log.skip_until("Failed to notify of PATH environment.+"))

0 commit comments

Comments
 (0)