874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
104 lines
4.7 KiB
Python
104 lines
4.7 KiB
Python
"""A tool or harness that starts the game must not write into a real player's folders.
|
|
|
|
The game logs into HORIZON_HOME and falls back to the player's own game folder when it is unset, so every
|
|
spawn goes through lab.game_env, which sets fresh scratch folders. This test fails for a script in tools/
|
|
that starts the game (or horizon-ctl, the launcher or the dedicated server) without it."""
|
|
|
|
import ast
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from tools import lab
|
|
|
|
TOOLS = Path(lab.ROOT) / "tools"
|
|
|
|
# Strings that mean "this script starts the game": a binary a harness runs, or the audio switch every game run sets.
|
|
SPAWN_MARKERS = {
|
|
"HORIZON_NO_AUDIO", "horizon-ctl", "horizon-ctl.exe", "horizon-server", "horizon-server.exe",
|
|
"horizon-client.exe", "release/horizon-client", "fast/horizon-client", "debug/horizon-client",
|
|
}
|
|
|
|
# Scripts that mention those strings without starting a game themselves, and why.
|
|
EXEMPT = {
|
|
"lab.py": "defines game_env",
|
|
"parity.py": "runs the harnesses and gives each its own HORIZON_HOME",
|
|
"package_native_client.py": "packs the binaries into an archive",
|
|
"build_release.py": "builds and packs the release",
|
|
"release_train.py": "release bookkeeping",
|
|
"pr_ready.py": "runs cargo checks and parity.py",
|
|
"test_friends_integration_parity.py": "builds binaries; test_friends.Game isolates every game through integration_env",
|
|
"port_harness.py": "ports Java code, starts no game",
|
|
}
|
|
|
|
|
|
def constants(tree):
|
|
"""Every string in a script that is a whole literal or a keyword name (docstrings are longer text)."""
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
yield node.value
|
|
elif isinstance(node, ast.keyword) and node.arg:
|
|
yield node.arg
|
|
|
|
|
|
def uses_game_env(tree):
|
|
"""The script calls lab.game_env, or starts its servers through test_dedicated_server_parity.Server, which does."""
|
|
return any(isinstance(node, ast.Attribute) and node.attr == "game_env"
|
|
or isinstance(node, ast.Name) and node.id == "game_env"
|
|
or isinstance(node, ast.ImportFrom) and (node.module or "").endswith("test_dedicated_server_parity")
|
|
for node in ast.walk(tree))
|
|
|
|
|
|
def sets_audio_by_hand(tree):
|
|
return "HORIZON_NO_AUDIO" in set(constants(tree))
|
|
|
|
|
|
class GameEnvTests(unittest.TestCase):
|
|
def test_gives_every_game_scratch_folders(self):
|
|
with mock.patch.dict(os.environ, {}, clear=False):
|
|
for key in ("HORIZON_HOME", "HORIZON_LAUNCHER_HOME", "XDG_DATA_HOME", "XDG_CONFIG_HOME"):
|
|
os.environ.pop(key, None)
|
|
first, second = lab.game_env(), lab.game_env()
|
|
self.assertEqual(first["HORIZON_NO_AUDIO"], "1")
|
|
self.assertEqual(first["HORIZON_LAUNCHER_NO_SELF_UPDATE"], "1")
|
|
for key in ("HORIZON_HOME", "HORIZON_LAUNCHER_HOME", "XDG_DATA_HOME", "XDG_CONFIG_HOME"):
|
|
self.assertTrue(Path(first[key]).is_dir(), key)
|
|
self.assertNotEqual(first[key], second[key], key)
|
|
|
|
def test_keeps_the_callers_folders(self):
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
env = lab.game_env({"PATH": "x", "HORIZON_HOME": "/from/base"}, HORIZON_LAUNCHER_HOME=Path(temp), A=1)
|
|
self.assertEqual(env["HORIZON_HOME"], "/from/base")
|
|
self.assertEqual(env["HORIZON_LAUNCHER_HOME"], temp)
|
|
self.assertEqual((env["PATH"], env["A"]), ("x", "1"))
|
|
|
|
def test_does_not_touch_the_process_environment(self):
|
|
before = dict(os.environ)
|
|
lab.game_env()
|
|
self.assertEqual(dict(os.environ), before)
|
|
|
|
|
|
class SpawnerTests(unittest.TestCase):
|
|
def test_every_script_that_starts_the_game_uses_game_env(self):
|
|
offenders = []
|
|
for path in sorted(TOOLS.rglob("*.py")):
|
|
if path.name in EXEMPT:
|
|
continue
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
if SPAWN_MARKERS & set(constants(tree)) and not uses_game_env(tree):
|
|
offenders.append(str(path.relative_to(lab.ROOT)))
|
|
self.assertEqual(offenders, [], "these start the game without lab.game_env (HORIZON_HOME falls back to a "
|
|
"real player's folder); use it, or list the script in EXEMPT")
|
|
|
|
def test_nobody_sets_the_audio_switch_by_hand(self):
|
|
offenders = [str(path.relative_to(lab.ROOT)) for path in sorted(TOOLS.rglob("*.py"))
|
|
if path.name not in EXEMPT
|
|
and sets_audio_by_hand(ast.parse(path.read_text(encoding="utf-8"), filename=str(path)))]
|
|
self.assertEqual(offenders, [], "set it through lab.game_env, which also gives the game scratch folders")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|