mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Add RCLOL_CLIENT_IN_GAME_TRICKS=1 as the practical launcher switch. It reads lol-user-experience from LCULocalPreferences.yaml: - potatoModeEnabled false or unreadable: no action; - closeLeagueClientDuringGame true: close the client UI; - closeLeagueClientDuringGame false: hide the client windows. The existing close and hide switches remain low-level options when the automatic switch is not set. The hide helper accepts the master switch, and the resolver plus preference cases are covered by tests and documentation. The local .desktop entry was validated with the master switch; the installed preferences select hide on this machine.
241 lines
9.5 KiB
Python
241 lines
9.5 KiB
Python
"""Exercise the in-match client window hide with fake /proc entries and X11."""
|
|
import importlib.util
|
|
import io
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import Mock, patch
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
spec = importlib.util.spec_from_file_location(
|
|
'hide_in_game', ROOT / 'tools/hide-league-client-in-game.py')
|
|
hide_in_game = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(hide_in_game)
|
|
|
|
GAME = '/Applications/League of Legends.app/Contents/LoL/LeagueofLegends'
|
|
MAIN = '/Applications/League of Legends.app/Contents/LoL/LeagueClientUx.app/Contents/MacOS/LeagueClientUx'
|
|
RIOT = '/Applications/Riot Client.app/Contents/MacOS/RiotClientServices'
|
|
|
|
|
|
class FakeX11:
|
|
def __init__(self):
|
|
self.unmapped = []
|
|
self.shown = []
|
|
|
|
def unmap(self, window):
|
|
self.unmapped.append(window)
|
|
|
|
def show(self, window):
|
|
self.shown.append(window)
|
|
|
|
|
|
class HideClientInGameTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name)
|
|
self.proc = self.root / 'proc'
|
|
self.proc.mkdir()
|
|
self.prefix = self.root / '.darling'
|
|
self.env_patch = patch.dict(os.environ, {'DPREFIX': str(self.prefix)})
|
|
self.env_patch.start()
|
|
self.addCleanup(self.env_patch.stop)
|
|
|
|
def environment(self, prefix):
|
|
return b'%s=%s\0' % (
|
|
hide_in_game.SOCK_VARIABLE,
|
|
os.fsencode(prefix / hide_in_game.SOCK_NAME),
|
|
)
|
|
|
|
def process(self, pid, executable=GAME, prefix='own', environ=None, args=('--flag',)):
|
|
folder = self.proc / str(pid)
|
|
folder.mkdir()
|
|
(folder / 'cmdline').write_bytes(
|
|
executable.encode() + b'\0' +
|
|
b''.join(arg.encode() + b'\0' for arg in args))
|
|
if environ is None:
|
|
environ = self.environment(self.prefix if prefix == 'own' else prefix)
|
|
(folder / 'environ').write_bytes(environ)
|
|
(folder / 'stat').write_text(f'{pid} (game) S')
|
|
return folder
|
|
|
|
def test_game_running_for_own_prefix(self):
|
|
self.process(123)
|
|
self.assertTrue(hide_in_game.game_running(self.prefix, self.proc))
|
|
|
|
def test_no_game_is_false(self):
|
|
self.process(123, executable=MAIN)
|
|
self.assertFalse(hide_in_game.game_running(self.prefix, self.proc))
|
|
|
|
def test_game_of_another_prefix_is_false(self):
|
|
self.process(123, prefix=self.root / 'other')
|
|
self.assertFalse(hide_in_game.game_running(self.prefix, self.proc))
|
|
|
|
def test_game_without_socket_is_false(self):
|
|
self.process(123, environ=b'')
|
|
self.assertFalse(hide_in_game.game_running(self.prefix, self.proc))
|
|
|
|
def test_unreadable_environment_is_unknown(self):
|
|
folder = self.process(123)
|
|
original = Path.read_bytes
|
|
|
|
def read(path):
|
|
if path.name == 'environ':
|
|
raise PermissionError()
|
|
return original(path)
|
|
|
|
with patch.object(Path, 'read_bytes', read):
|
|
self.assertIsNone(hide_in_game.game_running(self.prefix, self.proc))
|
|
|
|
def test_unreadable_process_directory_is_unknown(self):
|
|
class UnreadableProc:
|
|
def iterdir(self):
|
|
raise OSError()
|
|
|
|
self.assertIsNone(hide_in_game.game_running(self.prefix, UnreadableProc()))
|
|
|
|
def test_host_socket_for_guest_uses_nspid(self):
|
|
host = self.proc / '999'
|
|
host.mkdir()
|
|
(host / 'status').write_text('Name:\tCrBrowserMain\nNSpid:\t999\t5877\n')
|
|
(host / 'environ').write_bytes(self.environment(self.prefix))
|
|
self.assertEqual(
|
|
hide_in_game.host_socket_for_guest(5877, self.proc),
|
|
os.fsencode(self.prefix / hide_in_game.SOCK_NAME),
|
|
)
|
|
|
|
def test_window_belongs_to_prefix(self):
|
|
socket = os.fsencode(self.prefix / hide_in_game.SOCK_NAME)
|
|
with patch.object(hide_in_game, 'window_pid', return_value=5877), \
|
|
patch.object(hide_in_game, 'host_socket_for_guest', return_value=socket):
|
|
self.assertTrue(hide_in_game.window_belongs_to_prefix('0x10', self.prefix))
|
|
self.assertFalse(hide_in_game.window_belongs_to_prefix(
|
|
'0x10', self.root / 'other'))
|
|
|
|
def test_once_runs_without_opt_in(self):
|
|
output = io.StringIO()
|
|
with patch.dict(os.environ, {
|
|
'RCLOL_HIDE_CLIENT_IN_GAME': '',
|
|
'RCLOL_CLOSE_CLIENT_IN_GAME': '',
|
|
}), patch.object(hide_in_game, 'target_prefix',
|
|
return_value=self.prefix), \
|
|
patch.object(hide_in_game, 'game_running', return_value=True), \
|
|
patch.object(hide_in_game.sys, 'stdout', output):
|
|
self.assertEqual(hide_in_game.main(['--once']), 0)
|
|
self.assertIn('match=running', output.getvalue())
|
|
|
|
def test_target_windows_ignores_game_class(self):
|
|
with patch.object(hide_in_game, 'xprop_root',
|
|
return_value=['0x10', '0x20', '0x30']), \
|
|
patch.object(hide_in_game, 'window_class',
|
|
side_effect=[
|
|
'wm_class(string) = "leagueclientux"',
|
|
'wm_class(string) = "leagueoflegends"',
|
|
'wm_class(string) = "riot client"',
|
|
]), \
|
|
patch.object(hide_in_game, 'window_belongs_to_prefix', return_value=True):
|
|
self.assertEqual(hide_in_game.target_windows(self.prefix), [0x10, 0x30])
|
|
|
|
def test_target_windows_requires_this_prefix(self):
|
|
with patch.object(hide_in_game, 'xprop_root',
|
|
return_value=['0x10', '0x20']), \
|
|
patch.object(hide_in_game, 'window_class',
|
|
return_value='wm_class(string) = "leagueclientux"'), \
|
|
patch.object(hide_in_game, 'window_belongs_to_prefix',
|
|
side_effect=[True, False]):
|
|
self.assertEqual(hide_in_game.target_windows(self.prefix), [0x10])
|
|
|
|
def test_target_windows_caches_non_empty_classes(self):
|
|
cache = {}
|
|
with patch.object(hide_in_game, 'xprop_root', return_value=['0x10']), \
|
|
patch.object(hide_in_game, 'window_class',
|
|
return_value='wm_class(string) = "leagueclientux"') as classes, \
|
|
patch.object(hide_in_game, 'window_belongs_to_prefix', return_value=True):
|
|
self.assertEqual(hide_in_game.target_windows(self.prefix, cache), [0x10])
|
|
self.assertEqual(hide_in_game.target_windows(self.prefix, cache), [0x10])
|
|
self.assertEqual(classes.call_count, 1)
|
|
|
|
def test_hides_then_shows_on_match_end(self):
|
|
states = iter([True, True, False])
|
|
x11 = FakeX11()
|
|
output = io.StringIO()
|
|
watcher = hide_in_game.Watcher(
|
|
self.prefix, x11,
|
|
window_source=lambda _prefix, _cache: [0xabc],
|
|
state_source=lambda _prefix: next(states),
|
|
sleep=lambda _seconds: None,
|
|
out=output,
|
|
)
|
|
watcher.run(max_steps=3)
|
|
self.assertEqual(x11.unmapped, [0xabc])
|
|
self.assertEqual(x11.shown, [0xabc])
|
|
self.assertIn('hidden 0xabc', output.getvalue())
|
|
self.assertIn('the match ended', output.getvalue())
|
|
self.assertIn('shown 0xabc', output.getvalue())
|
|
|
|
def test_unreadable_state_restores_the_window(self):
|
|
states = iter([True, None])
|
|
x11 = FakeX11()
|
|
output = io.StringIO()
|
|
watcher = hide_in_game.Watcher(
|
|
self.prefix, x11,
|
|
window_source=lambda _prefix, _cache: [0xdef],
|
|
state_source=lambda _prefix: next(states),
|
|
sleep=lambda _seconds: None,
|
|
out=output,
|
|
)
|
|
watcher.run(max_steps=2)
|
|
self.assertEqual(x11.unmapped, [0xdef])
|
|
self.assertEqual(x11.shown, [0xdef])
|
|
self.assertIn('unreadable /proc state', output.getvalue())
|
|
|
|
def test_no_game_does_not_touch_windows(self):
|
|
x11 = FakeX11()
|
|
watcher = hide_in_game.Watcher(
|
|
self.prefix, x11,
|
|
window_source=lambda _prefix, _cache: [0xabc],
|
|
state_source=lambda _prefix: False,
|
|
sleep=lambda _seconds: None,
|
|
out=io.StringIO(),
|
|
)
|
|
watcher.run(max_steps=1)
|
|
self.assertEqual(x11.unmapped, [])
|
|
self.assertEqual(x11.shown, [])
|
|
|
|
def test_main_is_disabled_by_default(self):
|
|
with patch.dict(os.environ, {
|
|
'RCLOL_HIDE_CLIENT_IN_GAME': '',
|
|
'RCLOL_CLOSE_CLIENT_IN_GAME': '',
|
|
}):
|
|
self.assertEqual(hide_in_game.main([]), 0)
|
|
|
|
def test_close_mode_takes_precedence(self):
|
|
with patch.dict(os.environ, {
|
|
'RCLOL_HIDE_CLIENT_IN_GAME': '1',
|
|
'RCLOL_CLOSE_CLIENT_IN_GAME': '1',
|
|
}):
|
|
self.assertEqual(hide_in_game.main([]), 0)
|
|
|
|
def test_master_tricks_switch_enables_hide(self):
|
|
class FakeX11:
|
|
def close(self):
|
|
pass
|
|
|
|
watcher = Mock()
|
|
with patch.dict(os.environ, {
|
|
'RCLOL_CLIENT_IN_GAME_TRICKS': '1',
|
|
'RCLOL_HIDE_CLIENT_IN_GAME': '',
|
|
'RCLOL_CLOSE_CLIENT_IN_GAME': '',
|
|
}), patch.object(hide_in_game, 'target_prefix',
|
|
return_value=self.prefix), \
|
|
patch.object(hide_in_game, 'X11', return_value=FakeX11()), \
|
|
patch.object(hide_in_game, 'Watcher', return_value=watcher):
|
|
self.assertEqual(hide_in_game.main([]), 0)
|
|
watcher.run.assert_called_once_with()
|
|
watcher.restore.assert_called_once_with()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|