From 188593ed4e5772524b2c151f560d9c1339b57b67 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone <exarkun@twistedmatrix.com> Date: Mon, 16 Dec 2019 10:54:31 -0500 Subject: [PATCH] A couple simple starter tests for lease maintenance --- src/_zkapauthorizer/lease_maintenance.py | 27 +++++--- src/_zkapauthorizer/tests/matchers.py | 21 +++++++ .../tests/test_lease_maintenance.py | 61 +++++++++++++++++-- 3 files changed, 96 insertions(+), 13 deletions(-) diff --git a/src/_zkapauthorizer/lease_maintenance.py b/src/_zkapauthorizer/lease_maintenance.py index 3101909..ea4e920 100644 --- a/src/_zkapauthorizer/lease_maintenance.py +++ b/src/_zkapauthorizer/lease_maintenance.py @@ -268,6 +268,8 @@ def lease_maintenance_service( secret_holder, last_run, random, + interval_mean=None, + interval_range=None, ): """ Get an ``IService`` which will maintain leases on ``root_node`` and any @@ -293,15 +295,26 @@ def lease_maintenance_service( :param random: An object like ``random.Random`` which can be used as a source of scheduling delay. + + :param timedelta interval_mean: The mean time between lease renewal checks. + + :param timedelta interval_range: The range of the uniform distribution of + lease renewal checks (centered on ``interval_mean``). """ - mean = timedelta(days=26).total_seconds() - halfrange = timedelta(days=2).total_seconds() + if interval_mean is None: + interval_mean = timedelta(days=26) + if interval_range is None: + interval_range = timedelta(days=4) + halfrange = interval_range / 2 + min_lease_remaining = timedelta(days=3) - sample_interval_distribution = partial( - random.uniform, - mean - halfrange, - mean + halfrange, - ) + def sample_interval_distribution(): + return timedelta( + seconds=random.uniform( + (interval_mean - halfrange).total_seconds(), + (interval_mean + halfrange).total_seconds(), + ), + ) if last_run is None: initial_interval = sample_interval_distribution() else: diff --git a/src/_zkapauthorizer/tests/matchers.py b/src/_zkapauthorizer/tests/matchers.py index bcb4edb..0a52380 100644 --- a/src/_zkapauthorizer/tests/matchers.py +++ b/src/_zkapauthorizer/tests/matchers.py @@ -23,6 +23,11 @@ from testtools.matchers import ( Mismatch, ContainsDict, Always, + MatchesAll, + MatchesAny, + GreaterThan, + LessThan, + Equals, ) @attr.s @@ -78,3 +83,19 @@ class _Returns(Matcher): def __str__(self): return "Returns({})".format(self.result_matcher) + + +def between(low, high): + """ + Matches a value in the range [low, high]. + """ + return MatchesAll( + MatchesAny( + Equals(low), + GreaterThan(low), + ), + MatchesAny( + Equals(high), + LessThan(high), + ), + ) diff --git a/src/_zkapauthorizer/tests/test_lease_maintenance.py b/src/_zkapauthorizer/tests/test_lease_maintenance.py index b559dfd..5718c8b 100644 --- a/src/_zkapauthorizer/tests/test_lease_maintenance.py +++ b/src/_zkapauthorizer/tests/test_lease_maintenance.py @@ -22,7 +22,7 @@ from __future__ import ( ) from datetime import ( - datetime, + # datetime, timedelta, ) @@ -31,12 +31,17 @@ import attr from testtools import ( TestCase, ) +from hypothesis import ( + given, +) from hypothesis.strategies import ( builds, binary, integers, lists, + floats, dictionaries, + randoms, ) from twisted.internet.task import ( @@ -62,12 +67,13 @@ from ..foolscap import ( from .matchers import ( Provides, + between, ) from .strategies import ( storage_indexes, ) -from ..lease_maintenace import ( +from ..lease_maintenance import ( lease_maintenance_service, ) @@ -140,13 +146,13 @@ class LeaseMaintenanceServiceTests(TestCase): """ Tests for the service returned by ``lease_maintenance_service``. """ - def test_interface(self): + @given(randoms()) + def test_interface(self, random): """ The service provides ``IService``. """ clock = Clock() root_node = object() - random = object() lease_secret = b"\0" * CRYPTO_VAL_SIZE convergence_secret = b"\1" * CRYPTO_VAL_SIZE service = lease_maintenance_service( @@ -154,10 +160,53 @@ class LeaseMaintenanceServiceTests(TestCase): root_node, DummyStorageBroker(clock, []), SecretHolder(lease_secret, convergence_secret), - datetime.utcfromtimestamp(0), + None, random, ) self.assertThat( service, - Provides(IService), + Provides([IService]), + ) + + @given( + randoms(), + floats( + # It doesn't make sense to have a negative check interval mean. + min_value=0, + # We can't make this value too large or it isn't convertable to a + # timedelta. Also, even values as large as this one are of + # questionable value. + max_value=60 * 60 * 24 * 365), + ) + def test_initial_interval(self, random, mean): + """ + When constructed without a value for ``last_run``, + ``lease_maintenance_service`` schedules its first run to take place + after an interval that falls uniformly in range centered on ``mean`` + with a size of ``range``. + """ + clock = Clock() + root_node = object() + lease_secret = b"\0" * CRYPTO_VAL_SIZE + convergence_secret = b"\1" * CRYPTO_VAL_SIZE + + # Construct a range that fits in with the mean + range_ = random.uniform(0, mean) + + service = lease_maintenance_service( + clock, + root_node, + DummyStorageBroker(clock, []), + SecretHolder(lease_secret, convergence_secret), + None, + random, + timedelta(seconds=mean), + timedelta(seconds=range_), + ) + service.startService() + + [maintenance_call] = clock.getDelayedCalls() + self.assertThat( + maintenance_call.getTime(), + between(mean - (range_ / 2), mean + (range_ / 2)), ) -- GitLab