forked from Matoking/protontricks
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtest_gui.py
478 lines (390 loc) · 16.1 KB
/
test_gui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
import contextlib
import shutil
from subprocess import CalledProcessError
import pytest
from conftest import MockResult
from PIL import Image
from protontricks.gui import (prompt_filesystem_access,
select_steam_app_with_gui,
select_steam_installation)
from protontricks.steam import SteamApp
@pytest.fixture(scope="function")
def broken_zenity(gui_provider, monkeypatch):
"""
Mock a broken Zenity executable that prints an error as described in
the following GitHub issue:
https://github.com/Matoking/protontricks/issues/20
"""
def mock_subprocess_run(args, **kwargs):
gui_provider.args = args
raise CalledProcessError(
returncode=-6,
cmd=args,
output=gui_provider.mock_stdout,
stderr=b"free(): double free detected in tcache 2\n"
)
monkeypatch.setattr(
"protontricks.gui.run",
mock_subprocess_run
)
yield gui_provider
@pytest.fixture(scope="function")
def locale_error_zenity(gui_provider, monkeypatch):
"""
Mock a Zenity executable returning a 255 error due to a locale issue
on first run and working normally on second run
"""
def mock_subprocess_run(args, **kwargs):
if not gui_provider.args:
gui_provider.args = args
raise CalledProcessError(
returncode=255,
cmd=args,
output="",
stderr=(
b"This option is not available. "
b"Please see --help for all possible usages."
)
)
return MockResult(stdout=gui_provider.mock_stdout.encode("utf-8"))
monkeypatch.setattr(
"protontricks.gui.run",
mock_subprocess_run
)
monkeypatch.setenv("PROTONTRICKS_GUI", "zenity")
yield gui_provider
class TestSelectApp:
def test_select_game(self, gui_provider, steam_app_factory, steam_dir):
"""
Select a game using the GUI
"""
steam_apps = [
steam_app_factory(name="Fake game 1", appid=10),
steam_app_factory(name="Fake game 2", appid=20)
]
# Fake user selecting 'Fake game 2'
gui_provider.mock_stdout = "Fake game 2: 20"
steam_app = select_steam_app_with_gui(
steam_apps=steam_apps, steam_path=steam_dir
)
assert steam_app == steam_apps[1]
input_ = gui_provider.kwargs["input"]
# Check that choices were displayed
assert b"Fake game 1: 10\n" in input_
assert b"Fake game 2: 20" in input_
def test_select_game_icons(
self, gui_provider, steam_app_factory, steam_dir):
"""
Select a game using the GUI. Ensure that icons are used in the dialog
whenever available.
"""
steam_app_factory(name="Fake game 1", appid=10)
steam_app_factory(name="Fake game 2", appid=20)
steam_app_factory(name="Fake game 3", appid=30)
# Create icons for game 1 and 3
# Old location for 10
Image.new("RGB", (32, 32)).save(
steam_dir / "appcache" / "librarycache" / "10_icon.jpg"
)
# New location for 30
(steam_dir / "appcache" / "librarycache" / "30").mkdir()
Image.new("RGB", (32, 32)).save(
steam_dir / "appcache" / "librarycache" / "30"
/ "ffffffffffffffffffffffffffffffffffffffff.jpg"
)
# Read Steam apps using `SteamApp.from_appmanifest` to ensure
# icon paths are detected correctly
steam_apps = [
SteamApp.from_appmanifest(
steam_dir / "steamapps" / f"appmanifest_{appid}.acf",
steam_path=steam_dir,
steam_lib_paths=[steam_dir]
)
for appid in (10, 20, 30)
]
gui_provider.mock_stdout = "Fake game 2: 20"
select_steam_app_with_gui(steam_apps=steam_apps, steam_path=steam_dir)
input_ = gui_provider.kwargs["input"]
assert b"librarycache/10_icon.jpg\nFake game 1" in input_
assert b"icon_placeholder.png\nFake game 2" in input_
assert b"librarycache/30/ffffffffffffffffffffffffffffffffffffffff.jpg\nFake game 3" \
in input_
def test_select_game_icons_ensure_resize(
self, gui_provider, steam_app_factory, steam_dir, home_dir):
"""
Select a game using the GUI. Ensure custom icons with sizes other than
32x32 are resized.
"""
steam_apps = [
steam_app_factory(name="Fake game 1", appid=10)
]
Image.new("RGB", (64, 64)).save(
steam_dir / "appcache" / "librarycache" / "10_icon.jpg"
)
gui_provider.mock_stdout = "Fake game 1: 10"
select_steam_app_with_gui(steam_apps=steam_apps, steam_path=steam_dir)
# Resized icon should have been created with the correct size and used
resized_icon_path = \
home_dir / ".cache" / "protontricks" / "app_icons" / "10.jpg"
assert resized_icon_path.is_file()
with Image.open(resized_icon_path) as img:
assert img.size == (32, 32)
input_ = gui_provider.kwargs["input"]
assert f"{resized_icon_path}\nFake game 1".encode("utf-8") in input_
# Any existing icon should be overwritten if it already exists
resized_icon_path.write_bytes(b"not valid")
select_steam_app_with_gui(steam_apps=steam_apps, steam_path=steam_dir)
with Image.open(resized_icon_path) as img:
assert img.size == (32, 32)
def test_select_game_unidentifiable_icon_skipped(
self, gui_provider, steam_app_factory, steam_dir, home_dir, caplog):
"""
Select a game using the GUI. Ensure a custom icon that's not
identifiable by Pillow is skipped.
"""
steam_apps = [
steam_app_factory(name="Fake game 1", appid=10)
]
icon_path = steam_dir / "appcache" / "librarycache" / "10_icon.jpg"
icon_path.write_bytes(b"")
gui_provider.mock_stdout = "Fake game 1: 10"
selected_app = select_steam_app_with_gui(
steam_apps=steam_apps, steam_path=steam_dir
)
# Warning about icon was logged, but the app was selected successfully
record = caplog.records[-1]
assert record.message.startswith(f"Could not resize {icon_path}")
assert selected_app.appid == 10
def test_select_game_no_choice(
self, gui_provider, steam_app_factory, steam_dir):
"""
Try choosing a game but make no choice
"""
steam_apps = [steam_app_factory(name="Fake game 1", appid=10)]
# Fake user doesn't select any game
gui_provider.mock_stdout = ""
with pytest.raises(SystemExit) as exc:
select_steam_app_with_gui(
steam_apps=steam_apps, steam_path=steam_dir
)
assert exc.value.code == 1
def test_select_game_broken_zenity(
self, broken_zenity, monkeypatch, steam_app_factory, steam_dir):
"""
Try choosing a game with a broken Zenity executable that
prints a specific error message that Protontricks knows how to ignore
"""
monkeypatch.setenv("PROTONTRICKS_GUI", "zenity")
steam_apps = [
steam_app_factory(name="Fake game 1", appid=10),
steam_app_factory(name="Fake game 2", appid=20)
]
# Fake user selecting 'Fake game 2'
broken_zenity.mock_stdout = "Fake game 2: 20"
steam_app = select_steam_app_with_gui(
steam_apps=steam_apps, steam_path=steam_dir)
assert steam_app == steam_apps[1]
def test_select_game_locale_error(
self, locale_error_zenity, steam_app_factory, steam_dir, caplog):
"""
Try choosing a game with an environment that can't handle non-ASCII
characters
"""
steam_apps = [
steam_app_factory(name="Fäke game 1", appid=10),
steam_app_factory(name="Fäke game 2", appid=20)
]
# Fake user selecting 'Fäke game 2'. The non-ASCII character 'ä'
# is stripped since Zenity wouldn't be able to display the character.
locale_error_zenity.mock_stdout = "Fke game 2: 20"
steam_app = select_steam_app_with_gui(
steam_apps=steam_apps, steam_path=steam_dir
)
assert steam_app == steam_apps[1]
assert (
"Your system locale is incapable of displaying all characters"
in caplog.records[-1].message
)
@pytest.mark.parametrize("gui_cmd", ["yad", "zenity"])
def test_select_game_gui_provider_env(
self, gui_provider, steam_app_factory, monkeypatch, gui_cmd,
steam_dir):
"""
Test that the correct GUI provider is selected based on the
`PROTONTRICKS_GUI` environment variable
"""
monkeypatch.setenv("PROTONTRICKS_GUI", gui_cmd)
steam_apps = [
steam_app_factory(name="Fake game 1", appid=10),
steam_app_factory(name="Fake game 2", appid=20)
]
gui_provider.mock_stdout = "Fake game 2: 20"
select_steam_app_with_gui(
steam_apps=steam_apps, steam_path=steam_dir
)
# The flags should differ slightly depending on which provider is in
# use
if gui_cmd == "yad":
assert gui_provider.args[0] == "yad"
assert gui_provider.args[2] == "--no-headers"
elif gui_cmd == "zenity":
assert gui_provider.args[0] == "zenity"
assert gui_provider.args[2] == "--hide-header"
class TestSelectSteamInstallation:
@pytest.mark.usefixtures("flatpak_sandbox")
@pytest.mark.parametrize("gui_cmd", ["yad", "zenity"])
def test_select_steam_gui_provider_env(
self, gui_provider, monkeypatch, gui_cmd, steam_dir,
flatpak_steam_dir):
"""
Test that the correct GUI provider is selected based on the
`PROTONTRICKS_GUI` environment variable
"""
monkeypatch.setenv("PROTONTRICKS_GUI", gui_cmd)
gui_provider.mock_stdout = "1: Flatpak - /foo/bar"
select_steam_installation([
(steam_dir, steam_dir),
(flatpak_steam_dir, flatpak_steam_dir)
])
# The flags should differ slightly depending on which provider is in
# use
if gui_cmd == "yad":
assert gui_provider.args[0] == "yad"
assert gui_provider.args[2] == "--no-headers"
elif gui_cmd == "zenity":
assert gui_provider.args[0] == "zenity"
assert gui_provider.args[2] == "--hide-header"
@pytest.mark.parametrize(
"path,label",
[
(".steam", "Native"),
(".local/share/Steam", "Native"),
(".var/app/com.valvesoftware.Steam/.local/share/Steam", "Flatpak"),
("snap/steam/common/.local/share/Steam", "Snap")
]
)
def test_correct_labels_detected(
self, gui_provider, steam_dir, home_dir, path, label):
"""
Test that the Steam installation selection dialog uses the correct
label for each Steam installation depending on its type
"""
steam_new_dir = home_dir / path
with contextlib.suppress(FileExistsError):
# First test cases try copying against existing dirs, this can be
# ignored
shutil.copytree(steam_dir, steam_new_dir)
select_steam_installation([
(steam_new_dir, steam_new_dir),
# Use an additional nonsense path; there need to be at least
# two paths or user won't be prompted as there is no need
("/mock-steam", "/mock-steam")
])
prompt_input = gui_provider.kwargs["input"].decode("utf-8")
assert f"{label} - {steam_new_dir}" in prompt_input
@pytest.mark.usefixtures("flatpak_sandbox")
class TestPromptFilesystemAccess:
def test_prompt_without_desktop(self, home_dir, caplog):
"""
Test that calling 'prompt_filesystem_access' without showing the dialog
only generates a warning
"""
prompt_filesystem_access(
[home_dir / "fake_path", "/mnt/fake_SSD", "/mnt/fake_SSD_2"],
show_dialog=False
)
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelname == "WARNING"
assert "Protontricks does not appear to have access" in record.message
assert "--filesystem=/mnt/fake_SSD" in record.message
assert "--filesystem=/mnt/fake_SSD_2" in record.message
assert str(home_dir / "fake_path") not in record.message
def test_prompt_home_dir(self, home_dir, tmp_path, caplog):
"""
Test that calling 'prompt_filesystem_access' with a path
in the home directory will result in the command using a tilde slash
as the shorthand instead
"""
flatpak_info_path = tmp_path / "flatpak-info"
flatpak_info_path.write_text(
"[Application]\n"
"name=fake.flatpak.Protontricks\n"
"\n"
"[Instance]\n"
"flatpak-version=1.12.1\n"
"\n"
"[Context]\n"
"filesystems=/mnt/SSD_A"
)
prompt_filesystem_access(
[home_dir / "fake_path", "/mnt/SSD_A"],
show_dialog=False
)
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelname == "WARNING"
assert "Protontricks does not appear to have access" in record.message
assert "--filesystem='~/fake_path'" in record.message
assert "/mnt/SSD_A" not in record.message
def test_prompt_with_desktop_no_dialog(self, home_dir, gui_provider):
"""
Test that calling 'prompt_filesystem_access' with 'show_dialog'
displays a dialog
"""
prompt_filesystem_access(
[home_dir / "fake_path", "/mnt/fake_SSD", "/mnt/fake_SSD_2"],
show_dialog=True
)
input_ = gui_provider.kwargs["input"].decode("utf-8")
assert str(home_dir / "fake_path") not in input_
assert "--filesystem=/mnt/fake_SSD" in input_
assert "--filesystem=/mnt/fake_SSD_2" in input_
def test_prompt_with_desktop_dialog(self, home_dir, gui_provider):
"""
Test that calling 'prompt_filesystem_access' with 'show_dialog'
displays a dialog
"""
# Mock the user closing the dialog without ignoring the messages
gui_provider.returncode = 1
prompt_filesystem_access(
[home_dir / "fake_path", "/mnt/fake_SSD", "/mnt/fake_SSD_2"],
show_dialog=True
)
input_ = gui_provider.kwargs["input"].decode("utf-8")
# Dialog was displayed
assert "/mnt/fake_SSD" in input_
assert "/mnt/fake_SSD_2" in input_
# Mock the user selecting "Ignore, don't ask again"
gui_provider.returncode = 0
gui_provider.kwargs["input"] = None
prompt_filesystem_access(
[home_dir / "fake_path", "/mnt/fake_SSD", "/mnt/fake_SSD_2"],
show_dialog=True
)
# Dialog is still displayed, but it won't be the next time
input_ = gui_provider.kwargs["input"].decode("utf-8")
assert "/mnt/fake_SSD" in input_
assert "/mnt/fake_SSD_2" in input_
gui_provider.kwargs["input"] = None
prompt_filesystem_access(
[home_dir / "fake_path", "/mnt/fake_SSD", "/mnt/fake_SSD_2"],
show_dialog=True
)
# Dialog is not shown, since the user has opted to ignore the warning
# for the current paths
assert not gui_provider.kwargs["input"]
# A new path makes the warning reappear
prompt_filesystem_access(
[
home_dir / "fake_path",
"/mnt/fake_SSD",
"/mnt/fake_SSD_2",
"/mnt/fake_SSD_3"
],
show_dialog=True
)
input_ = gui_provider.kwargs["input"].decode("utf-8")
assert "/mnt/fake_SSD " not in input_
assert "/mnt/fake_SSD_2" not in input_
assert "/mnt/fake_SSD_3" in input_