Files
contentdb/app/tests/unit/logic/test_game_support.py

557 lines
17 KiB
Python

# ContentDB
# Copyright (C) rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import List
from app.logic.game_support import GSPackage, GameSupport
from app.models import PackageType
def make_mod(name: str, provides: List[str], deps: List[str]) -> GSPackage:
ret = GSPackage("author", name, PackageType.MOD, set(provides))
ret.depends.update(deps)
return ret
def make_game(name: str, provides: List[str]) -> GSPackage:
return GSPackage("author", name, PackageType.GAME, set(provides))
def test_game_supports_itself():
"""
Games obviously support themselves
"""
support = GameSupport()
game = support.add(make_game("game1", ["default"]))
assert not support.has_errors
assert game.is_confirmed
assert len(game.detected_supported_games) == 0
support.on_update(game)
assert not support.has_errors
assert game.is_confirmed
assert len(game.detected_supported_games) == 0
def test_no_deps():
"""
Test a mod with no dependencies supports all games
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
mod1 = support.add(make_mod("mod1", ["mod1"], []))
support.on_update(mod1)
assert not support.has_errors
assert mod1.is_confirmed
assert len(mod1.detected_supported_games) == 0
def test_direct_game_dep():
"""
Test that depending on a mod in a game works
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
mod1 = support.add(make_mod("mod1", ["mod1"], ["default"]))
support.on_update(mod1)
assert not support.has_errors
assert mod1.is_confirmed
assert mod1.detected_supported_games == {"game1"}
def test_indirect_game_dep():
"""
Test that depending on a mod that depends on a game works
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
mod1 = support.add(make_mod("mod1", ["mod1"], ["default"]))
mod2 = support.add(make_mod("mod2", ["mod2"], ["mod1"]))
support.on_update(mod2)
assert not support.has_errors
assert mod1.is_confirmed
assert mod1.detected_supported_games == {"game1"}
assert mod2.is_confirmed
assert mod2.detected_supported_games == {"game1"}
def test_multiple_game_dep():
"""
Test with multiple games, with dependencies in games and as standalone mods
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["core", "mod_b"]))
lib = support.add(make_mod("lib", ["lib"], []))
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b", "lib"]))
support.on_update(modA)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1", "game2"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
assert lib.is_confirmed
assert len(lib.detected_supported_games) == 0
def test_dependency_supports_all():
"""
Test with dependencies that support all games
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
mod1 = support.add(make_mod("mod1", ["mod1"], ["default"]))
lib = support.add(make_mod("lib", ["lib"], []))
mod2 = support.add(make_mod("mod2", ["mod2"], ["mod1", "lib"]))
support.on_update(mod2)
assert not support.has_errors
assert mod1.is_confirmed
assert mod1.detected_supported_games == {"game1"}
assert mod2.is_confirmed
assert mod2.detected_supported_games == {"game1"}
assert lib.is_confirmed
assert len(lib.detected_supported_games) == 0
def test_dependency_supports_all2():
"""
Test with dependencies that support all games, but are also in games
"""
support = GameSupport()
support.add(make_game("game1", ["default", "lib"]))
lib = support.add(make_mod("lib", ["lib"], []))
mod1 = support.add(make_mod("mod1", ["mod1"], ["lib"]))
support.on_update(mod1)
assert not support.has_errors
assert mod1.is_confirmed
assert len(mod1.detected_supported_games) == 0
assert lib.is_confirmed
assert len(lib.detected_supported_games) == 0
def test_dependency_game_conflict():
"""
Test situation where a mod is not installable in any games
"""
support = GameSupport()
support.add(make_game("game1", ["default", "mod_b"]))
support.add(make_game("game2", ["default", "mod_c"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b", "mod_c"]))
support.on_update(modA)
assert not modA.is_confirmed
assert len(modA.detected_supported_games) == 0
assert support.all_errors == {
"author/mod_a: Game support conflict, unable to install package on any games"
}
def test_missing_hard_dep():
"""
Test missing hard dependency
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["nonexist"]))
support.on_update(modA)
assert not modA.is_confirmed
assert len(modA.detected_supported_games) == 0
assert support.all_errors == {
"author/mod_a: Unable to fulfill dependency nonexist"
}
def test_cycle():
"""
Test for dependency cycles
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_mod("mod_b", ["mod_b"], ["mod_a"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
support.on_update(modA)
assert support.all_errors == {
"author/mod_a: Dependency cycle detected: author/mod_b -> author/mod_a -> author/mod_b",
"author/mod_b: Dependency cycle detected: author/mod_a -> author/mod_b -> author/mod_a",
"author/mod_a: Dependency cycle detected: author/mod_a -> author/mod_b -> author/mod_a",
"author/mod_b: Unable to fulfill dependency mod_a",
"author/mod_b: Dependency cycle detected: author/mod_b -> author/mod_a -> author/mod_b",
"author/mod_a: Unable to fulfill dependency mod_b"
}
def test_cycle_fails_safely():
"""
A dependency cycle shouldn't completely break the graph if a mod is
available elsewhere
"""
support = GameSupport()
support.add(make_game("game1", ["default", "mod_d"]))
modC = support.add(make_mod("mod_c", ["mod_c"], ["mod_b"]))
modB = support.add(make_mod("mod_b", ["mod_b"], ["mod_c"]))
support.add(make_mod("mod_d", ["mod_d"], ["mod_b"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_d"]))
support.on_update(modA)
assert not modC.is_confirmed
assert not modB.is_confirmed
assert modA.is_confirmed
assert len(modA.errors) == 0
assert modA.detected_supported_games == {"game1"}
assert support.all_errors == {
"author/mod_b: Unable to fulfill dependency mod_c",
"author/mod_d: Unable to fulfill dependency mod_b",
"author/mod_c: Unable to fulfill dependency mod_b",
"author/mod_c: Dependency cycle detected: author/mod_b -> author/mod_c -> author/mod_b",
"author/mod_b: Dependency cycle detected: author/mod_b -> author/mod_c -> author/mod_b"
}
def test_update():
"""
Test updating a mod will update mods that depend on it
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
game2 = support.add(make_game("game2", ["core"]))
lib = support.add(make_mod("lib", ["lib"], []))
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b", "lib"]))
support.on_update(modA)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
assert lib.is_confirmed
assert len(lib.detected_supported_games) == 0
game2.provides.add("mod_b")
support.on_update(game2)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1", "game2"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
assert lib.is_confirmed
assert len(lib.detected_supported_games) == 0
def test_update_new_mod():
"""
Test that adding a mod will update mods that depend on the modname
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["core", "mod_b"]))
lib = support.add(make_mod("lib", ["lib"], []))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b", "lib"]))
support.on_update(modA)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game2"}
assert lib.is_confirmed
assert len(lib.detected_supported_games) == 0
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
support.on_update(modB)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1", "game2"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
assert lib.is_confirmed
assert len(lib.detected_supported_games) == 0
def test_update_remove_mod():
"""
Test that removing a mod from provides will update mods that depended on the modname
"""
support = GameSupport()
support.add(make_game("game1", ["default"]))
game2 = support.add(make_game("game2", ["core", "mod_b"]))
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
lib = support.add(make_mod("lib", ["lib"], []))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b", "lib"]))
support.on_first_run()
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1", "game2"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
assert lib.is_confirmed
assert len(lib.detected_supported_games) == 0
old_provides = game2.provides.copy()
game2.provides.remove("mod_b")
support.on_update(game2, old_provides)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
assert lib.is_confirmed
assert len(lib.detected_supported_games) == 0
def test_update_cycle():
"""
Test that updating a package with a cycle depending on it doesn't break
"""
support = GameSupport()
game1 = support.add(make_game("game1", ["default"]))
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
support.on_update(modA)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
support.add(make_mod("mod_c", ["mod_c"], ["mod_a"]))
modA.depends.add("mod_c")
support.on_update(game1)
assert support.all_errors == {
"author/mod_c: Dependency cycle detected: author/mod_a -> author/mod_c -> author/mod_a",
"author/mod_a: Dependency cycle detected: author/mod_a -> author/mod_c -> author/mod_a",
"author/mod_a: Dependency cycle detected: author/mod_c -> author/mod_a -> author/mod_c",
"author/mod_a: Unable to fulfill dependency mod_c",
"author/mod_c: Dependency cycle detected: author/mod_c -> author/mod_a -> author/mod_c",
"author/mod_c: Unable to fulfill dependency mod_a"
}
def test_remove():
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["core", "mod_b"]))
support.add(make_mod("lib", ["lib"], []))
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b", "lib"]))
support.on_update(modA)
assert not support.has_errors
assert modA.is_confirmed
assert len(modA.detected_supported_games) == 2
assert "game1" in modA.detected_supported_games
assert "game2" in modA.detected_supported_games
assert modB.is_confirmed
assert len(modB.detected_supported_games) == 1
assert "game1" in modB.detected_supported_games
support.on_remove(modB)
assert not support.has_errors
assert modA.is_confirmed
assert len(modA.detected_supported_games) == 1
assert "game2" in modA.detected_supported_games
def test_propagates_user_unsupported_games():
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["default"]))
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
modB.user_supported_games.add("game1")
modB.user_unsupported_games.add("game2")
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
support.on_update(modA)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1"}
assert modA.supported_games == {"game1"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1"}
assert modB.supported_games == {"game1"}
def test_propagates_user_supported_games():
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["default"]))
modB = support.add(make_mod("mod_b", ["mod_b"], []))
modB.user_supported_games.add("game1")
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
support.on_update(modA)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1"}
assert modB.is_confirmed
assert len(modB.detected_supported_games) == 0
def test_validate_inconsistent_user_supported_game():
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["core"]))
support.add(make_mod("mod_b", ["mod_b"], ["core"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
modA.user_supported_games.add("game1")
support.on_update(modA)
assert support.all_errors == {
"author/mod_a: `game1` is specified in supported_games but it is impossible to run mod_a in that game. "
"Its dependencies can only be fulfilled in `game2`. "
"Check your hard dependencies.",
}
def test_validate_inconsistent_supports_all():
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["core"]))
support.add(make_mod("mod_b", ["mod_b"], ["core"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
modA.supports_all_games = True
support.on_update(modA)
assert support.all_errors == {
"author/mod_a: This package cannot support all games as some dependencies require specific game(s): `game2`",
}
def test_enable_detection():
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["default"]))
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
modB.user_supported_games.add("game1")
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
support.on_update(modA)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1", "game2"}
assert modA.supported_games == {"game1", "game2"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1", "game2"}
assert modB.supported_games == {"game1", "game2"}
def test_disable_detection():
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["default"]))
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
modB.user_supported_games.add("game1")
modB.detection_disabled = True
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b"]))
support.on_update(modA)
assert not support.has_errors
assert modA.is_confirmed
assert modA.detected_supported_games == {"game1"}
assert modA.supported_games == {"game1"}
assert modB.is_confirmed
assert modB.detected_supported_games == {"game1", "game2"}
assert modB.supported_games == {"game1"}
def test_first_run():
support = GameSupport()
support.add(make_game("game1", ["default"]))
support.add(make_game("game2", ["core", "mod_b"]))
lib = support.add(make_mod("lib", ["lib"], []))
modB = support.add(make_mod("mod_b", ["mod_b"], ["default"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["mod_b", "lib"]))
modF = support.add(make_mod("mod_f", ["mod_f"], []))
assert not support.all_confirmed
support.on_first_run()
assert support.all_confirmed
assert not support.has_errors
assert modA.detected_supported_games == {"game1", "game2"}
assert modB.detected_supported_games == {"game1"}
assert len(lib.detected_supported_games) == 0
assert len(modF.detected_supported_games) == 0
def test_ignores_mtg_in_violating_games():
support = GameSupport()
support.add(make_game("minetest_game", ["default"]))
support.add(make_game("tutorial", ["default", "tutorial"]))
modA = support.add(make_mod("mod_a", ["mod_a"], ["default"]))
modB = support.add(make_mod("mod_b", ["mod_b"], ["tutorial"]))
support.on_first_run()
assert not support.has_errors
assert modA.detected_supported_games == {"minetest_game"}
assert modB.detected_supported_games == {"tutorial"}