mirror of
https://github.com/evennia/evennia.git
synced 2026-03-27 02:06:32 +01:00
Add timer component and made unittests pass
This commit is contained in:
parent
175fcce405
commit
f180e160f2
2 changed files with 268 additions and 41 deletions
|
|
@ -207,7 +207,7 @@ class TraitTest(_TraitHandlerBase):
|
|||
"extra_val": 1000
|
||||
}
|
||||
expected = copy(dat) # we must break link or return === dat always
|
||||
self.assertEqual(expected, traits.Trait.validate_input(dat))
|
||||
self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))
|
||||
|
||||
# don't supply value, should get default
|
||||
dat = {
|
||||
|
|
@ -218,7 +218,7 @@ class TraitTest(_TraitHandlerBase):
|
|||
}
|
||||
expected = copy(dat)
|
||||
expected["value"] = traits.Trait.data_keys['value']
|
||||
self.assertEqual(expected, traits.Trait.validate_input(dat))
|
||||
self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))
|
||||
|
||||
# make sure extra values are cleaned if trait accepts no extras
|
||||
dat = {
|
||||
|
|
@ -232,7 +232,7 @@ class TraitTest(_TraitHandlerBase):
|
|||
expected.pop("extra_val1")
|
||||
expected.pop("extra_val2")
|
||||
with patch.object(traits.Trait, "allow_extra_properties", False):
|
||||
self.assertEqual(expected, traits.Trait.validate_input(dat))
|
||||
self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))
|
||||
|
||||
def test_validate_input__fail(self):
|
||||
"""Test failing validation"""
|
||||
|
|
@ -243,7 +243,7 @@ class TraitTest(_TraitHandlerBase):
|
|||
"extra_val": 1000
|
||||
}
|
||||
with self.assertRaises(traits.TraitException):
|
||||
traits.Trait.validate_input(dat)
|
||||
traits.Trait.validate_input(traits.Trait, dat)
|
||||
|
||||
# make value a required key
|
||||
mock_data_keys = {
|
||||
|
|
@ -257,7 +257,7 @@ class TraitTest(_TraitHandlerBase):
|
|||
"extra_val": 1000
|
||||
}
|
||||
with self.assertRaises(traits.TraitException):
|
||||
traits.Trait.validate_input(dat)
|
||||
traits.Trait.validate_input(traits.Trait, dat)
|
||||
|
||||
def test_trait_getset(self):
|
||||
"""Get-set-del operations on trait"""
|
||||
|
|
@ -433,6 +433,7 @@ class TestTraitCounter(_TraitHandlerBase):
|
|||
},
|
||||
"rate": 0,
|
||||
"ratetarget": None,
|
||||
"last_update": None,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -570,6 +571,84 @@ class TestTraitCounter(_TraitHandlerBase):
|
|||
self.assertEqual(self.trait.desc(), "range3")
|
||||
|
||||
|
||||
class TestTraitCounterTimed(_TraitHandlerBase):
|
||||
"""
|
||||
Test for trait with timer component
|
||||
"""
|
||||
@patch("evennia.contrib.traits.time", new=MagicMock(return_value=1000))
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.traithandler.add(
|
||||
"test1",
|
||||
name="Test1",
|
||||
trait_type='counter',
|
||||
base=1,
|
||||
mod=2,
|
||||
min=0,
|
||||
max=100,
|
||||
extra_val1="xvalue1",
|
||||
extra_val2="xvalue2",
|
||||
descs={
|
||||
0: "range0",
|
||||
2: "range1",
|
||||
5: "range2",
|
||||
7: "range3",
|
||||
},
|
||||
rate=1,
|
||||
ratetarget=None,
|
||||
)
|
||||
self.trait = self.traithandler.get("test1")
|
||||
|
||||
def _get_timer_data(self):
|
||||
return (self.trait.actual, self.trait.current, self.trait.rate,
|
||||
self.trait._data["last_update"], self.trait.ratetarget)
|
||||
|
||||
@patch("evennia.contrib.traits.time")
|
||||
def test_timer_rate(self, mock_time):
|
||||
"""Test time stepping"""
|
||||
mock_time.return_value = 1000
|
||||
self.assertEqual(self._get_timer_data(), (3, 1, 1, 1000, None))
|
||||
mock_time.return_value = 1001
|
||||
self.assertEqual(self._get_timer_data(), (4, 2, 1, 1001, None))
|
||||
mock_time.return_value = 1096
|
||||
self.assertEqual(self._get_timer_data(), (99, 97, 1, 1096, None))
|
||||
# hit maximum boundary
|
||||
mock_time.return_value = 1120
|
||||
self.assertEqual(self._get_timer_data(), (100, 98, 1, None, None))
|
||||
mock_time.return_value = 1200
|
||||
self.assertEqual(self._get_timer_data(), (100, 98, 1, None, None))
|
||||
# drop current
|
||||
self.trait.current = 50
|
||||
self.assertEqual(self._get_timer_data(), (52, 50, 1, 1200, None))
|
||||
# set a new rate
|
||||
self.trait.rate = 2
|
||||
mock_time.return_value = 1210
|
||||
self.assertEqual(self._get_timer_data(), (72, 70, 2, 1210, None))
|
||||
self.trait.rate = -10
|
||||
mock_time.return_value = 1214
|
||||
self.assertEqual(self._get_timer_data(), (32, 30, -10, 1214, None))
|
||||
mock_time.return_value = 1218
|
||||
self.assertEqual(self._get_timer_data(), (0, -2, -10, None, None))
|
||||
|
||||
@patch("evennia.contrib.traits.time")
|
||||
def test_timer_ratetarget(self, mock_time):
|
||||
"""test ratetarget"""
|
||||
mock_time.return_value = 1000
|
||||
self.trait.ratetarget = 60
|
||||
self.assertEqual(self._get_timer_data(), (3, 1, 1, 1000, 60))
|
||||
mock_time.return_value = 1056
|
||||
self.assertEqual(self._get_timer_data(), (59, 57, 1, 1056, 60))
|
||||
mock_time.return_value = 1057
|
||||
self.assertEqual(self._get_timer_data(), (60, 58, 1, None, 60))
|
||||
mock_time.return_value = 1060
|
||||
self.assertEqual(self._get_timer_data(), (60, 58, 1, None, 60))
|
||||
self.trait.ratetarget = 70
|
||||
mock_time.return_value = 1066
|
||||
self.assertEqual(self._get_timer_data(), (66, 64, 1, 1066, 70))
|
||||
mock_time.return_value = 1070
|
||||
self.assertEqual(self._get_timer_data(), (70, 68, 1, None, 70))
|
||||
|
||||
|
||||
class TestTraitGauge(_TraitHandlerBase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
@ -614,6 +693,7 @@ class TestTraitGauge(_TraitHandlerBase):
|
|||
},
|
||||
"rate": 0,
|
||||
"ratetarget": None,
|
||||
"last_update": None,
|
||||
}
|
||||
)
|
||||
def test_actual(self):
|
||||
|
|
@ -756,6 +836,85 @@ class TestTraitGauge(_TraitHandlerBase):
|
|||
self.assertEqual(self.trait.desc(), "range3")
|
||||
|
||||
|
||||
class TestTraitGaugeTimed(_TraitHandlerBase):
|
||||
"""
|
||||
Test for trait with timer component
|
||||
"""
|
||||
@patch("evennia.contrib.traits.time", new=MagicMock(return_value=1000))
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.traithandler.add(
|
||||
"test1",
|
||||
name="Test1",
|
||||
trait_type='gauge',
|
||||
base=98,
|
||||
mod=2,
|
||||
min=0,
|
||||
extra_val1="xvalue1",
|
||||
extra_val2="xvalue2",
|
||||
descs={
|
||||
0: "range0",
|
||||
2: "range1",
|
||||
5: "range2",
|
||||
7: "range3",
|
||||
},
|
||||
rate=1,
|
||||
ratetarget=None,
|
||||
)
|
||||
self.trait = self.traithandler.get("test1")
|
||||
|
||||
def _get_timer_data(self):
|
||||
return (self.trait.actual, self.trait.current, self.trait.rate,
|
||||
self.trait._data["last_update"], self.trait.ratetarget)
|
||||
|
||||
@patch("evennia.contrib.traits.time")
|
||||
def test_timer_rate(self, mock_time):
|
||||
"""Test time stepping"""
|
||||
mock_time.return_value = 1000
|
||||
self.trait.current = 1
|
||||
self.assertEqual(self._get_timer_data(), (1, 1, 1, 1000, None))
|
||||
mock_time.return_value = 1001
|
||||
self.assertEqual(self._get_timer_data(), (2, 2, 1, 1001, None))
|
||||
mock_time.return_value = 1096
|
||||
self.assertEqual(self._get_timer_data(), (97, 97, 1, 1096, None))
|
||||
# hit maximum boundary
|
||||
mock_time.return_value = 1120
|
||||
self.assertEqual(self._get_timer_data(), (100, 100, 1, None, None))
|
||||
mock_time.return_value = 1200
|
||||
self.assertEqual(self._get_timer_data(), (100, 100, 1, None, None))
|
||||
# drop current
|
||||
self.trait.current = 50
|
||||
self.assertEqual(self._get_timer_data(), (50, 50, 1, 1200, None))
|
||||
# set a new rate
|
||||
self.trait.rate = 2
|
||||
mock_time.return_value = 1210
|
||||
self.assertEqual(self._get_timer_data(), (70, 70, 2, 1210, None))
|
||||
self.trait.rate = -10
|
||||
mock_time.return_value = 1214
|
||||
self.assertEqual(self._get_timer_data(), (30, 30, -10, 1214, None))
|
||||
mock_time.return_value = 1218
|
||||
self.assertEqual(self._get_timer_data(), (0, 0, -10, None, None))
|
||||
|
||||
@patch("evennia.contrib.traits.time")
|
||||
def test_timer_ratetarget(self, mock_time):
|
||||
"""test ratetarget"""
|
||||
mock_time.return_value = 1000
|
||||
self.trait.current = 1
|
||||
self.trait.ratetarget = 60
|
||||
self.assertEqual(self._get_timer_data(), (1, 1, 1, 1000, 60))
|
||||
mock_time.return_value = 1056
|
||||
self.assertEqual(self._get_timer_data(), (57, 57, 1, 1056, 60))
|
||||
mock_time.return_value = 1059
|
||||
self.assertEqual(self._get_timer_data(), (60, 60, 1, None, 60))
|
||||
mock_time.return_value = 1060
|
||||
self.assertEqual(self._get_timer_data(), (60, 60, 1, None, 60))
|
||||
self.trait.ratetarget = 70
|
||||
mock_time.return_value = 1066
|
||||
self.assertEqual(self._get_timer_data(), (66, 66, 1, 1066, 70))
|
||||
mock_time.return_value = 1070
|
||||
self.assertEqual(self._get_timer_data(), (70, 70, 1, None, 70))
|
||||
|
||||
|
||||
class TestNumericTraitOperators(TestCase):
|
||||
"""Test case for numeric magic method implementations."""
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ class TraitHandler:
|
|||
trait_properties["trait_type"] = trait_type
|
||||
|
||||
# this will raise exception if input is insufficient
|
||||
trait_properties = trait_class.validate_input(trait_properties)
|
||||
trait_properties = trait_class.validate_input(trait_class, trait_properties)
|
||||
|
||||
self.trait_data[trait_key] = trait_properties
|
||||
|
||||
|
|
@ -563,7 +563,7 @@ class Trait:
|
|||
TraitException: If input-validation failed.
|
||||
|
||||
"""
|
||||
self._data = self.__class__.validate_input(trait_data)
|
||||
self._data = self.__class__.validate_input(self.__class__, trait_data)
|
||||
|
||||
if not isinstance(trait_data, _SaverDict):
|
||||
logger.log_warn(
|
||||
|
|
@ -571,7 +571,7 @@ class Trait:
|
|||
f"loaded for {type(self).__name__}."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@staticmethod
|
||||
def validate_input(cls, trait_data):
|
||||
"""
|
||||
Validate input
|
||||
|
|
@ -967,55 +967,90 @@ class CounterTrait(NumericTrait):
|
|||
"ratetarget": None
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def validate(cls, trait_data):
|
||||
@staticmethod
|
||||
def validate_input(cls, trait_data):
|
||||
"""Add extra validation for descs"""
|
||||
trait_data = Trait.validate_input(trait_data)
|
||||
trait_data = Trait.validate_input(cls, trait_data)
|
||||
# validate descs
|
||||
descs = trait_data['descs']
|
||||
if isinstance(descs, dict):
|
||||
if any(not (isinstance(key, (int, float)) and isinstance(value, str))
|
||||
for key in descs.items()):
|
||||
raise TraitException("Trait descs must be defined on the form {number:str}")
|
||||
for key, value in descs.items()):
|
||||
raise TraitException(
|
||||
f"Trait descs must be defined on the "
|
||||
f"form {{number:str}} (instead found {descs}).")
|
||||
# set up rate
|
||||
if trait_data['rate'] != 0:
|
||||
trait_data['last_update'] = time()
|
||||
else:
|
||||
trait_data['last_update'] = None
|
||||
return trait_data
|
||||
|
||||
# Helpers
|
||||
|
||||
def _within_boundaries(self, value):
|
||||
"""Check if given value is within boundaries"""
|
||||
return not (
|
||||
(self.min is not None and value <= self.min) or
|
||||
(self.max is not None and value >= self.max)
|
||||
)
|
||||
|
||||
def _enforce_boundaries(self, value):
|
||||
"""Ensures that incoming value falls within boundaries"""
|
||||
if self.min is not None and value <= self.min:
|
||||
return self.min
|
||||
if self.max is not None and value >= self.max:
|
||||
return self.max
|
||||
return value
|
||||
|
||||
# timer component
|
||||
|
||||
def _timer_running(self):
|
||||
"""Check if timer mechanism is running"""
|
||||
return self.rate != 0 and self._data['last_update'] is not None
|
||||
def _passed_ratetarget(self, value):
|
||||
"""Check if we passed the ratetarget in either direction."""
|
||||
ratetarget = self._data['ratetarget']
|
||||
return (ratetarget is not None and (
|
||||
(self.rate < 0 and value <= ratetarget) or
|
||||
(self.rate > 0 and value >= ratetarget)))
|
||||
|
||||
def _stop_timer(self):
|
||||
if self._timer_running():
|
||||
"""Stop rate-timer component."""
|
||||
if self.rate != 0 and self._data['last_update'] is not None:
|
||||
self._data['last_update'] = None
|
||||
|
||||
def _check_ratetarget(self):
|
||||
"""Check if we passed ratetarget."""
|
||||
ratetarget = self._data['ratetarget']
|
||||
return (ratetarget is not None and
|
||||
((self.rate < 0 and new_curr <= ratetarget) or
|
||||
(self.rate > 0 and new_curr >= ratetarget)))
|
||||
def _check_and_start_timer(self, value):
|
||||
"""Start timer if we are not at a boundary."""
|
||||
if self.rate != 0 and self._data['last_update'] is None:
|
||||
ratetarget = self._data['ratetarget']
|
||||
if self._within_boundaries(value) and not self._passed_ratetarget(value):
|
||||
# we are not at a boundary [anymore].
|
||||
self._data['last_update'] = time()
|
||||
return value
|
||||
|
||||
|
||||
def _update_current(self, current):
|
||||
"""Update current value, including any rate change"""
|
||||
if self.rate != 0 and self._data['last_update'] is not None:
|
||||
"""Update current value by scaling with rate and time passed."""
|
||||
rate = self.rate
|
||||
if rate != 0 and self._data['last_update'] is not None:
|
||||
now = time()
|
||||
tdiff = now - self._data['last_update']
|
||||
current += self.rate * tdiff
|
||||
return current
|
||||
current += rate * tdiff
|
||||
actual = current + self.mod
|
||||
|
||||
def _enforce_boundaries(self, value):
|
||||
"""Ensures that incoming value falls within trait's range."""
|
||||
if self.min is not None and value <= self.min:
|
||||
self._stop_timer()
|
||||
return self.min
|
||||
if self.max is not None and value >= self.max:
|
||||
self._stop_timer()
|
||||
return self.max
|
||||
if self._timer_running() and self._check_ratetarget():
|
||||
_stop_timer()
|
||||
return self._data['ratetarget']
|
||||
return value
|
||||
# we must make sure so we don't overstep our bounds
|
||||
# even if .mod is included
|
||||
|
||||
if self._passed_ratetarget(actual):
|
||||
current = self._data['ratetarget'] - self.mod
|
||||
self._stop_timer()
|
||||
elif not self._within_boundaries(actual):
|
||||
current = self._enforce_boundaries(actual) - self.mod
|
||||
self._stop_timer()
|
||||
else:
|
||||
self._data['last_update'] = now
|
||||
|
||||
self._data['current'] = current
|
||||
|
||||
return current
|
||||
|
||||
# properties
|
||||
|
||||
|
|
@ -1086,7 +1121,7 @@ class CounterTrait(NumericTrait):
|
|||
@current.setter
|
||||
def current(self, value):
|
||||
if type(value) in (int, float):
|
||||
self._data["current"] = self._enforce_boundaries(value)
|
||||
self._data["current"] = self._check_and_start_timer(self._enforce_boundaries(value))
|
||||
|
||||
@current.deleter
|
||||
def current(self):
|
||||
|
|
@ -1098,6 +1133,15 @@ class CounterTrait(NumericTrait):
|
|||
"The actual value of the Trait (current + mod)"
|
||||
return self._enforce_boundaries(self.current + self.mod)
|
||||
|
||||
@property
|
||||
def ratetarget(self):
|
||||
return self._data['ratetarget']
|
||||
|
||||
@ratetarget.setter
|
||||
def ratetarget(self, value):
|
||||
self._data['ratetarget'] = self._enforce_boundaries(value)
|
||||
self._check_and_start_timer(self.actual)
|
||||
|
||||
def percent(self, formatting="{:3.1f}%"):
|
||||
"""
|
||||
Return the current value as a percentage.
|
||||
|
|
@ -1188,6 +1232,30 @@ class GaugeTrait(CounterTrait):
|
|||
"ratetarget": None,
|
||||
}
|
||||
|
||||
def _update_current(self, current):
|
||||
"""Update current value by scaling with rate and time passed."""
|
||||
rate = self.rate
|
||||
if rate != 0 and self._data['last_update'] is not None:
|
||||
now = time()
|
||||
tdiff = now - self._data['last_update']
|
||||
current += rate * tdiff
|
||||
actual = current
|
||||
|
||||
# we don't worry about .mod for gauges
|
||||
|
||||
if self._passed_ratetarget(actual):
|
||||
current = self._data['ratetarget']
|
||||
self._stop_timer()
|
||||
elif not self._within_boundaries(actual):
|
||||
current = self._enforce_boundaries(actual)
|
||||
self._stop_timer()
|
||||
else:
|
||||
self._data['last_update'] = now
|
||||
|
||||
self._data['current'] = current
|
||||
|
||||
return current
|
||||
|
||||
def _enforce_boundaries(self, value):
|
||||
"""Ensures that incoming value falls within trait's range."""
|
||||
if self.min is not None and value <= self.min:
|
||||
|
|
@ -1258,7 +1326,7 @@ class GaugeTrait(CounterTrait):
|
|||
@current.setter
|
||||
def current(self, value):
|
||||
if type(value) in (int, float):
|
||||
self._data["current"] = self._enforce_boundaries(value)
|
||||
self._data["current"] = self._check_and_start_timer(self._enforce_boundaries(value))
|
||||
|
||||
@current.deleter
|
||||
def current(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue