Skip to content

Commit ab1eec7

Browse files
committed
Add tests
1 parent 9dc3104 commit ab1eec7

File tree

3 files changed

+154
-23
lines changed

3 files changed

+154
-23
lines changed

src/manage/firstrun.py

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
from .pathutils import Path
1818

1919

20+
def _package_name():
21+
from _native import get_current_package
22+
return get_current_package()
23+
24+
2025
def check_app_alias(cmd):
2126
LOGGER.debug("Checking app execution aliases")
22-
from _native import get_current_package, read_alias_package
2327
# Expected identities:
2428
# Side-loaded MSIX
2529
# * "PythonSoftwareFoundation.PythonManager_3847v3x7pw1km",
@@ -30,13 +34,15 @@ def check_app_alias(cmd):
3034
# MSI/dev install
3135
# * None
3236
try:
33-
pkg = get_current_package()
37+
pkg = _package_name()
3438
except OSError:
3539
LOGGER.debug("Failed to get current package name.", exc_info=True)
3640
pkg = None
3741
if not pkg:
3842
LOGGER.debug("Check skipped: MSI install can't do this check")
3943
return "skip"
44+
45+
from _native import read_alias_package
4046
LOGGER.debug("Checking for %s", pkg)
4147
root = Path(os.environ["LocalAppData"]) / "Microsoft/WindowsApps"
4248
for name in ["py.exe", "pyw.exe", "python.exe", "pythonw.exe", "python3.exe", "pymanager.exe"]:
@@ -59,7 +65,8 @@ def check_long_paths(cmd):
5965
LOGGER.debug("Checking long paths setting")
6066
import winreg
6167
try:
62-
with winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, r"System\CurrentControlSet\Control\FileSystem") as key:
68+
with winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE,
69+
r"System\CurrentControlSet\Control\FileSystem") as key:
6370
if winreg.QueryValueEx(key, "LongPathsEnabled") == (1, winreg.REG_DWORD):
6471
LOGGER.debug("Check passed: registry key is OK")
6572
return True
@@ -71,9 +78,14 @@ def check_long_paths(cmd):
7178

7279
def check_py_on_path(cmd):
7380
LOGGER.debug("Checking for legacy py.exe on PATH")
74-
from _native import get_current_package, read_alias_package
75-
if not get_current_package():
76-
LOGGER.debug("Check skipped: MSI install can't do this check")
81+
from _native import read_alias_package
82+
try:
83+
if not _package_name():
84+
LOGGER.debug("Check skipped: MSI install can't do this check")
85+
return "skip"
86+
except OSError:
87+
LOGGER.debug("Failed to get current package name.", exc_info=True)
88+
LOGGER.debug("Check skipped: can't do this check")
7789
return "skip"
7890
for p in os.environ["PATH"].split(";"):
7991
if not p:
@@ -90,6 +102,8 @@ def check_py_on_path(cmd):
90102
# Probably not an alias, so we're not good
91103
LOGGER.debug("Check failed: found %s on PATH", py)
92104
return False
105+
LOGGER.debug("Check passed: no py.exe on PATH at all")
106+
return True
93107

94108

95109
def check_global_dir(cmd):
@@ -104,28 +118,35 @@ def check_global_dir(cmd):
104118
LOGGER.debug("Check passed: %s is on PATH", p)
105119
return True
106120
# In case user has updated their registry but not the terminal
107-
import winreg
108121
try:
109-
with winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, "Environment") as key:
110-
path, kind = winreg.QueryValueEx(key, "Path")
111-
LOGGER.debug("Current registry path: %s", path)
112-
if kind == winreg.REG_EXPAND_SZ:
113-
path = os.path.expandvars(path)
114-
elif kind != winreg.REG_SZ:
115-
LOGGER.debug("Check skipped: PATH registry key is not a string.")
116-
return "skip"
117-
for p in path.split(";"):
118-
if not p:
119-
continue
120-
if Path(p).absolute().match(cmd.global_dir):
121-
LOGGER.debug("Check skipped: %s will be on PATH after restart", p)
122-
return True
122+
r = _check_global_dir_registry(cmd)
123+
if r:
124+
return r
123125
except Exception:
124126
LOGGER.debug("Failed to read PATH setting from registry", exc_info=True)
125127
LOGGER.debug("Check failed: %s not found in PATH", cmd.global_dir)
126128
return False
127129

128130

131+
def _check_global_dir_registry(cmd):
132+
import winreg
133+
with winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, "Environment") as key:
134+
path, kind = winreg.QueryValueEx(key, "Path")
135+
LOGGER.debug("Current registry path: %s", path)
136+
if kind == winreg.REG_EXPAND_SZ:
137+
path = os.path.expandvars(path)
138+
elif kind != winreg.REG_SZ:
139+
LOGGER.debug("Check skipped: PATH registry key is not a string.")
140+
return "skip"
141+
for p in path.split(";"):
142+
if not p:
143+
continue
144+
if Path(p).absolute().match(cmd.global_dir):
145+
LOGGER.debug("Check skipped: %s will be on PATH after restart", p)
146+
return True
147+
return False
148+
149+
129150
def do_global_dir_on_path(cmd):
130151
import winreg
131152
added = notified = False

tests/conftest.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def skip_until(self, pattern, args=()):
6666
def not_logged(self, pattern, args=()):
6767
return ('not', pattern, args)
6868

69+
def end_of_log(self):
70+
return ('eol', None, None)
71+
6972
def __call__(self, *cmp):
7073
i = 0
7174
for y in cmp:
@@ -84,6 +87,11 @@ def __call__(self, *cmp):
8487
return
8588
continue
8689

90+
if op == 'eol':
91+
if i < len(self):
92+
pytest.fail(f"Expected end of log; found {self[i]}")
93+
return
94+
8795
while True:
8896
try:
8997
x = self[i]
@@ -92,7 +100,10 @@ def __call__(self, *cmp):
92100
pytest.fail(f"Not enough elements were logged looking for {pat}")
93101
if op == 'until' and not re.match(pat, x[0], flags=re.S):
94102
continue
95-
assert re.match(pat, x[0], flags=re.S)
103+
if not pat:
104+
assert not x[0]
105+
else:
106+
assert re.match(pat, x[0], flags=re.S)
96107
if args is not None:
97108
assert tuple(x[1]) == tuple(args)
98109
break
@@ -140,7 +151,7 @@ def __init__(self, installs=[]):
140151
self.shebang_can_run_anything = True
141152
self.shebang_can_run_anything_silently = False
142153

143-
def get_installs(self):
154+
def get_installs(self, *, include_unmanaged=True, set_default=True):
144155
return self.installs
145156

146157
def get_install_to_run(self, tag):

tests/test_firstrun.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import os
2+
import pytest
3+
4+
from pathlib import Path
5+
6+
from manage import firstrun
7+
from _native import get_current_package, read_alias_package
8+
9+
def test_get_current_package():
10+
# The only circumstance where this may be different is if we're running
11+
# tests in a Store install of Python without a virtualenv. That should never
12+
# happen, because we run with 3.14 and there are no more Store installs.
13+
assert get_current_package() is None
14+
15+
16+
def test_read_alias_package():
17+
# Hopefully there's at least one package add with an alias on this machine.
18+
root = Path(os.environ["LocalAppData"]) / "Microsoft/WindowsApps"
19+
if not root.is_dir() or not any(root.rglob("*.exe")):
20+
pytest.skip("Requires an installed app")
21+
for f in root.rglob("*.exe"):
22+
print("Reading package name from", f)
23+
p = read_alias_package(f)
24+
print("Read:", p)
25+
# One is sufficient
26+
return
27+
pytest.skip("Requires an installed app")
28+
29+
30+
def fake_package_name():
31+
return "PythonSoftwareFoundation.PythonManager_m8z88z54g2w36"
32+
33+
34+
def fake_package_name_error():
35+
raise OSError("injected failure")
36+
37+
38+
def fake_package_name_none():
39+
return None
40+
41+
42+
def test_check_app_alias(fake_config, monkeypatch):
43+
monkeypatch.setattr(firstrun, "_package_name", fake_package_name)
44+
assert firstrun.check_app_alias(fake_config) in (True, False)
45+
46+
monkeypatch.setattr(firstrun, "_package_name", fake_package_name_error)
47+
assert firstrun.check_app_alias(fake_config) == "skip"
48+
49+
monkeypatch.setattr(firstrun, "_package_name", fake_package_name_none)
50+
assert firstrun.check_app_alias(fake_config) == "skip"
51+
52+
53+
def test_check_long_paths(fake_config):
54+
assert firstrun.check_long_paths(fake_config) in (True, False)
55+
56+
57+
def test_check_py_on_path(fake_config, monkeypatch):
58+
monkeypatch.setattr(firstrun, "_package_name", fake_package_name)
59+
assert firstrun.check_py_on_path(fake_config) in (True, False)
60+
61+
monkeypatch.setattr(firstrun, "_package_name", fake_package_name_error)
62+
assert firstrun.check_py_on_path(fake_config) == "skip"
63+
64+
monkeypatch.setattr(firstrun, "_package_name", fake_package_name_none)
65+
assert firstrun.check_py_on_path(fake_config) == "skip"
66+
67+
68+
def test_check_global_dir(fake_config, monkeypatch, tmp_path):
69+
fake_config.global_dir = str(tmp_path)
70+
assert firstrun.check_global_dir(fake_config) == False
71+
72+
monkeypatch.setattr(firstrun, "_check_global_dir_registry", lambda *a: "called")
73+
assert firstrun.check_global_dir(fake_config) == "called"
74+
75+
monkeypatch.setitem(os.environ, "PATH", f"{os.environ['PATH']};{tmp_path}")
76+
assert firstrun.check_global_dir(fake_config) == True
77+
78+
79+
def test_check_global_dir_registry(fake_config, monkeypatch, tmp_path):
80+
fake_config.global_dir = str(tmp_path)
81+
assert firstrun._check_global_dir_registry(fake_config) == False
82+
# Deliberately not going to modify the registry for this test.
83+
# Integration testing will verify that it reads correctly.
84+
85+
86+
def test_check_any_install(fake_config):
87+
assert firstrun.check_any_install(fake_config) == False
88+
89+
fake_config.installs.append("an install")
90+
assert firstrun.check_any_install(fake_config) == True
91+
92+
93+
def test_welcome(assert_log):
94+
welcome = firstrun._Welcome()
95+
assert_log(assert_log.end_of_log())
96+
welcome()
97+
assert_log(".*Welcome.*", "", assert_log.end_of_log())
98+
welcome()
99+
assert_log(".*Welcome.*", "", assert_log.end_of_log())

0 commit comments

Comments
 (0)