Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stardew Valley: Add test decorators to ensure all mods are tested once #4560

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions worlds/stardew_valley/test/mods/TestMods.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import random

from BaseClasses import get_seed
from .mod_testing_decorators import must_test_all_mods, mod_testing
from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld, \
fill_dataclass_with_default
from ..assertion import ModAssertMixin, WorldAssertMixin
Expand All @@ -22,42 +23,72 @@ def test_given_single_mods_when_generate_then_basic_checks(self):
self.assert_basic_checks(multi_world)
self.assert_stray_mod_items(mod, multi_world)

# The following tests validate that ER still generates winnable and logically-sane games with given mods.
# Mods that do not interact with entrances are skipped
# Not all ER settings are tested, because 'buildings' is, essentially, a superset of all others
def test_allsanity_all_mods_when_generate_then_basic_checks(self):
with self.solo_world_sub_test(world_options=allsanity_mods_6_x_x()) as (multi_world, _):
self.assert_basic_checks(multi_world)

def test_allsanity_all_mods_exclude_island_when_generate_then_basic_checks(self):
world_options = allsanity_mods_6_x_x()
world_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true})
with self.solo_world_sub_test(world_options=world_options) as (multi_world, _):
self.assert_basic_checks(multi_world)


@must_test_all_mods(
excluded_mods=[ModNames.ginger, ModNames.distant_lands, ModNames.skull_cavern_elevator, ModNames.wellwick, ModNames.magic, ModNames.binning_skill,
ModNames.big_backpack, ModNames.luck_skill, ModNames.tractor, ModNames.shiko, ModNames.archaeology, ModNames.delores,
ModNames.socializing_skill, ModNames.cooking_skill])
class TestModsEntranceRandomization(WorldAssertMixin, SVTestCase):
"""The following tests validate that ER still generates winnable and logically-sane games with given mods.
Mods that do not interact with entrances are skipped
Not all ER settings are tested, because 'buildings' is, essentially, a superset of all others
"""

@mod_testing(ModNames.deepwoods)
def test_deepwoods_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.deepwoods, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.juna)
def test_juna_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.juna, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.jasper)
def test_jasper_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.jasper, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.alec)
def test_alec_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.alec, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.yoba)
def test_yoba_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.yoba, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.eugene)
def test_eugene_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.eugene, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.ayeisha)
def test_ayeisha_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.ayeisha, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.riley)
def test_riley_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.riley, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.sve)
def test_sve_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.sve, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.alecto)
def test_alecto_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.alecto, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.lacey)
def test_lacey_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.lacey, options.EntranceRandomization.option_buildings)

@mod_testing(ModNames.boarding_house)
def test_boarding_house_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.boarding_house, options.EntranceRandomization.option_buildings)

Expand All @@ -75,16 +106,6 @@ def perform_basic_checks_on_mod_with_er(self, mods: str | set[str], er_option: i
with self.solo_world_sub_test(f"entrance_randomization: {er_option}, Mods: {mods}", world_options) as (multi_world, _):
self.assert_basic_checks(multi_world)

def test_allsanity_all_mods_when_generate_then_basic_checks(self):
with self.solo_world_sub_test(world_options=allsanity_mods_6_x_x()) as (multi_world, _):
self.assert_basic_checks(multi_world)

def test_allsanity_all_mods_exclude_island_when_generate_then_basic_checks(self):
world_options = allsanity_mods_6_x_x()
world_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true})
with self.solo_world_sub_test(world_options=world_options) as (multi_world, _):
self.assert_basic_checks(multi_world)


class TestBaseLocationDependencies(SVTestBase):
options = {
Expand Down
48 changes: 48 additions & 0 deletions worlds/stardew_valley/test/mods/mod_testing_decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import unittest
from collections.abc import Callable, Iterable
from functools import wraps, partial
from typing import Type

from ... import options


def must_test_all_mods(cls: Type[unittest.TestCase] | None = None, /, *, excluded_mods: Iterable[str] | None = None) \
-> partial[Type[unittest.TestCase]] | Type[unittest.TestCase]:
if excluded_mods is None:
excluded_mods = set()

if cls is None:
return partial(_must_test_all_mods, excluded_mods=excluded_mods)
return _must_test_all_mods(cls, excluded_mods)


def _must_test_all_mods(cls: Type[unittest.TestCase], excluded_mods: Iterable[str]) -> Type[unittest.TestCase]:
setattr(cls, "tested_mods", set(excluded_mods))
orignal_tear_down_class = cls.tearDownClass

@wraps(cls.tearDownClass)
def wrapper() -> None:
tested_mods: set[str] = getattr(cls, "tested_mods")

diff = options.Mods.valid_keys - tested_mods
if diff:
raise AssertionError(f"Mods {diff} were not tested")

return orignal_tear_down_class()

cls.tearDownClass = wrapper

return cls


def mod_testing(mod: str) -> partial[Callable]:
return partial(_mod_testing, mod=mod)


def _mod_testing(func: Callable, mod: str) -> Callable:
@wraps(func)
def wrapper(self: unittest.TestCase, *args, **kwargs):
getattr(self, "tested_mods").add(mod)
return func(self, *args, **kwargs)

return wrapper
Loading