autoupdate: refactor into class and write test

This commit is contained in:
James Woglom
2022-01-07 00:32:50 -05:00
parent 8b1ece3ac3
commit 7b22d612c3
8 changed files with 588 additions and 91 deletions
Generated
+6 -6
View File
@@ -144,11 +144,11 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5", "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
"sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df" "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.27.0" "version": "==2.27.1"
}, },
"requests-mock": { "requests-mock": {
"hashes": [ "hashes": [
@@ -229,11 +229,11 @@
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4", "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65",
"sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c" "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"
], ],
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==2.11.1" "version": "==2.11.2"
}, },
"wcwidth": { "wcwidth": {
"hashes": [ "hashes": [
+4 -2
View File
@@ -7,7 +7,7 @@ import pkg_resources
from .api import TConnectApi from .api import TConnectApi
from .process import process_time_range from .process import process_time_range
from .autoupdate import process_auto_update from .autoupdate import Autoupdate
from .check import check_login from .check import check_login
from .nightscout import NightscoutApi from .nightscout import NightscoutApi
from .features import DEFAULT_FEATURES, ALL_FEATURES from .features import DEFAULT_FEATURES, ALL_FEATURES
@@ -19,6 +19,7 @@ try:
NS_URL, NS_URL,
NS_SECRET NS_SECRET
) )
from . import secret
except Exception: except Exception:
print('Unable to read secret.py') print('Unable to read secret.py')
sys.exit(1) sys.exit(1)
@@ -82,7 +83,8 @@ def main(*args, **kwargs):
if args.auto_update: if args.auto_update:
print("Starting auto-update between", time_start, "and", time_end, "(PRETEND)" if args.pretend else "") print("Starting auto-update between", time_start, "and", time_end, "(PRETEND)" if args.pretend else "")
process_auto_update(tconnect, nightscout, time_start, time_end, args.pretend, features=args.features) u = Autoupdate(secret)
sys.exit(u.process(tconnect, nightscout, time_start, time_end, args.pretend, features=args.features))
else: else:
print("Processing data between", time_start, "and", time_end, "(PRETEND)" if args.pretend else "") print("Processing data between", time_start, "and", time_end, "(PRETEND)" if args.pretend else "")
added = process_time_range(tconnect, nightscout, time_start, time_end, args.pretend, features=args.features) added = process_time_range(tconnect, nightscout, time_start, time_end, args.pretend, features=args.features)
+3 -4
View File
@@ -11,13 +11,12 @@ class TConnectApi:
email = None email = None
password = None password = None
_ciq = None
_ws2 = None
_android = None
def __init__(self, email, password): def __init__(self, email, password):
self.email = email self.email = email
self.password = password self.password = password
self._ciq = None
self._ws2 = None
self._android = None
@property @property
+178 -73
View File
@@ -2,100 +2,205 @@ import time
import logging import logging
import sys import sys
from .api import TConnectApi
from .process import process_time_range from .process import process_time_range
from .features import DEFAULT_FEATURES from .features import DEFAULT_FEATURES
from .secret import ( from . import secret
TCONNECT_EMAIL,
TCONNECT_PASSWORD,
PUMP_SERIAL_NUMBER,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS,
AUTOUPDATE_MAX_SLEEP_SECONDS,
AUTOUPDATE_USE_FIXED_SLEEP,
AUTOUPDATE_FAILURE_MINUTES,
AUTOUPDATE_RESTART_ON_FAILURE
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
""" class Autoupdate:
Performs the auto-update functionality. Runs indefinitely in a loop """Wrap access to secrets for easier testing."""
until stopped (ctrl+c). def __init__(self, secret):
""" self.secret = secret
def process_auto_update(tconnect, nightscout, time_start, time_end, pretend, features=DEFAULT_FEATURES): self.autoupdate_invocations = 0
# Read from android api, find exact interval to cut down on API calls self.last_event_index = None
# Refresh API token. If failure, die, have wrapper script re-run. self.last_event_time = None
self.last_successful_process_time_range = None
self.time_diffs_between_updates = []
self.last_attempt_time = None
self.time_diffs_between_attempts = []
last_event_index = None """
last_event_time = None Performs the auto-update functionality. Runs indefinitely in a loop
last_process_time_range = None until stopped (ctrl+c), or a maximum of AUTOUPDATE_MAX_LOOP_INVOCATIONS times.
time_diffs = [] Stops if AUTOUPDATE_RESTART_ON_FAILURE is set and an error occurs.
while True: """
last_event = tconnect.android.last_event_uploaded(PUMP_SERIAL_NUMBER) def process(self, tconnect, nightscout, time_start, time_end, pretend, features=None):
if not last_event_index or last_event['maxPumpEventIndex'] > last_event_index: if features is None:
features = DEFAULT_FEATURES
# Read from android api, find exact interval to cut down on API calls
# Refresh API token. If failure, die, have wrapper script re-run.
self.autoupdate_start = time.time()
while True:
logger.debug("autoupdate loop")
now = time.time() now = time.time()
logger.info('New reported t:connect data. (event index: %s last: %s)' % (last_event['maxPumpEventIndex'], last_event_index)) last_event = tconnect.android.last_event_uploaded(self.secret.PUMP_SERIAL_NUMBER)
if not self.last_event_index or last_event['maxPumpEventIndex'] > self.last_event_index:
logger.info('New reported t:connect data. (event index: %s last: %s)' % (last_event['maxPumpEventIndex'], self.last_event_index))
if pretend: if pretend:
logger.info('Would update now if not in pretend mode') logger.info('Would update now if not in pretend mode')
else:
added = process_time_range(tconnect, nightscout, time_start, time_end, pretend, features=features)
logger.info('Added %d items from process_time_range' % added)
if added == 0:
if last_event_index:
logger.error('An event index change was recorded, but no new data was found via the API. ' +
'If this error reoccurs, try restarting tconnectsync.')
logger.info('Resetting TConnectApi')
tconnect = TConnectApi(TCONNECT_EMAIL, TCONNECT_PASSWORD)
else: else:
last_process_time_range = now added = process_time_range(tconnect, nightscout, time_start, time_end, pretend, features=features)
logger.info('Added %d items from process_time_range' % added)
if added == 0:
# If we've been unable to find new events, but the last_event_index is increasing,
# suggesting there are more events being added, we might be in a bugged
# situation where we can't get any more data without restarting.
# We skip this check on the first process cycle, since we might
# just already be in sync with tconnect's pump data.
if self.last_event_index:
# Find the timestamp of the last time we've successfully obtained data,
# or the time when the autoupdate run started, if we haven't at all.
last_action_or_start = self.last_successful_process_time_range
if not last_action_or_start:
last_action_or_start = self.autoupdate_start
# If it's been AUTOUPDATE_FAILURE_MINUTES in the state of not seeing
# event index changes reflected in the tconnect data we're pulling,
# raise an error and potentially restart.
# This is likely a tconnectsync problem, not a problem with the pump or app
# (we can see the indexes increasing, so we know something's happening!)
if (now - last_action_or_start) >= 60 * self.secret.AUTOUPDATE_FAILURE_MINUTES:
logger.error(AutoupdateFailureError(
"An event index change was recorded, but no new data was found via the API. " +
"The %s was %d minutes ago. This is a problem with tconnectsync." %
("last processed event" if self.last_successful_process_time_range else "start of autoupdate", (now - last_action_or_start)//60)))
if self.secret.AUTOUPDATE_RESTART_ON_FAILURE:
logger.error("Exiting with error code due to AUTOUPDATE_RESTART_ON_FAILURE")
return 1
else:
logger.warn(AutoupdateFailureWarning("An event index change was recorded, but no new data was found via the API. " +
"The %s was %d minutes ago. Resetting TConnectApi to attempt to solve this problem." %
("last processed event" if self.last_successful_process_time_range else "start of autoupdate", (now - last_action_or_start)//60)))
# As a stop-gap, try to re-initialize TConnectApi (triggering a re-login)
# Use __class__ instead of direct TConnectApi invocation to avoid initializing a real TConnectApi over a fake
tconnect = tconnect.__class__(self.secret.TCONNECT_EMAIL, self.secret.TCONNECT_PASSWORD)
else:
# Mark the last successful time we got data from tconnect
self.last_successful_process_time_range = now
if last_event_index: # Track the time it took to find a new event between runs,
time_diffs.append(now - last_event_time) # but skip this calculation the first process cycle (since
logger.debug('Updating tracking of time since last update: %s' % time_diffs) # we don't know at what exact point the event index changed)
if self.last_event_index:
self.time_diffs_between_updates.append(now - self.last_event_time)
logger.debug('Updating tracking of time since last update: %s' % self.time_diffs_between_updates)
last_event_index = last_event['maxPumpEventIndex'] # Mark the last event index uploaded from the pump and timestamp
last_event_time = now self.last_event_index = last_event['maxPumpEventIndex']
else: self.last_event_time = now
logger.info('No new reported t:connect data. (last event index: %s)' % last_event['maxPumpEventIndex']) self.last_attempt_time = now
now = time.time() self.time_diffs_between_attempts = []
else:
logger.info('No new reported t:connect data. (last event index: %s)' % last_event['maxPumpEventIndex'])
if last_event_time and (now - last_event_time) >= 60 * AUTOUPDATE_FAILURE_MINUTES: # If we haven't seen the pump event index update in AUTOUPDATE_NO_DATA_FAILURE_MINUTES,
logger.error(AutoupdateFailureException("No new data event indexes have been detected for over %d minutes. " % AUTOUPDATE_FAILURE_MINUTES + # then trigger an error and potentially restart.
"The t:connect app might no longer be functioning.")) # The most likely case here is that the pump isn't uploading right now.
if self.last_event_time and (now - self.last_event_time) >= 60 * self.secret.AUTOUPDATE_NO_DATA_FAILURE_MINUTES:
logger.error(AutoupdateNoEventIndexesDetectedError(
"No new data event indexes have been detected for %d minutes. " % ((now - self.last_event_time)//60) +
"The t:connect app might no longer be functioning."))
if AUTOUPDATE_RESTART_ON_FAILURE: # TODO: restarting doesn't really help anything here.
sys.exit(1) # Should we notify the user?
if self.secret.AUTOUPDATE_RESTART_ON_FAILURE:
logger.error("Exiting with error code due to AUTOUPDATE_RESTART_ON_FAILURE")
return 1
elif last_process_time_range and (now - last_process_time_range) >= 60 * AUTOUPDATE_FAILURE_MINUTES: # Similarly, if we HAVE seen pump event indexes update but have not successfully
logger.error(AutoupdateFailureException("No new data has been found via the API for over %d minutes. " % AUTOUPDATE_FAILURE_MINUTES + # found any associated data updates from the tconnect API for AUTOUPDATE_NO_DATA_FAILURE_MINUTES,
"tconnectsync might not be functioning properly.")) # trigger an error and potentially restart. This could either be a tconnectsync problem,
# where we can see the indexes increasing, but it takes us until a period of no index
# update to reach our AUTOUPDATE_FAILURE_MINUTES threshold; or, a side effect of the
# above no indexes warning.
elif self.last_successful_process_time_range and (now - self.last_successful_process_time_range) >= 60 * self.secret.AUTOUPDATE_FAILURE_MINUTES:
logger.error(AutoupdateNoNewDataDetectedError(
"No new data has been detected via the API for %d minutes. " % (now - self.last_successful_process_time_range)//60 +
"tconnectsync might not be functioning properly."))
if AUTOUPDATE_RESTART_ON_FAILURE: if self.secret.AUTOUPDATE_RESTART_ON_FAILURE:
sys.exit(1) logger.error("Exiting with error code due to AUTOUPDATE_RESTART_ON_FAILURE")
return 1
# Track how long we've been retrying
if self.last_attempt_time:
self.time_diffs_between_attempts.append(now - self.last_attempt_time)
self.last_attempt_time = now
# If it's been 3 loops since the last time we found new data,
# then we're not in sync with the rate at which pump data is being
# uploaded, so
if len(self.time_diffs_between_attempts) >= 3:
# The pump hasn't sent us data that, based on previous cadence, we were expecting
logger.warn(AutoupdateNoIndexChangeWarning("Sleeping %d seconds after unexpected no index change based on previous cadence. (New data might be delayed.)" %
int(self.secret.AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS)))
logger.debug("Last event time: %s, time diffs between attempts: %s" % (self.last_event_time, self.time_diffs_between_attempts))
time.sleep(self.secret.AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS)
# Since we bail early, update the invocations count and potentially exit after sleeping.
self.autoupdate_invocations += 1
if self.secret.AUTOUPDATE_MAX_LOOP_INVOCATIONS > 0 and self.autoupdate_invocations >= self.secret.AUTOUPDATE_MAX_LOOP_INVOCATIONS:
return 0
continue
sleep_secs = self.secret.AUTOUPDATE_DEFAULT_SLEEP_SECONDS
# Sleep for a rolling average of time between updates
if self.secret.AUTOUPDATE_USE_FIXED_SLEEP != 1:
logger.debug("Time diffs between updates: %s" % self.time_diffs_between_updates)
# Only keep the 10 latest time diffs
if len(self.time_diffs_between_updates) > 10:
self.time_diffs_between_updates = self.time_diffs_between_updates[1:]
# If we have less than 3 data points,
if len(self.time_diffs_between_updates) > 2:
sleep_secs = sum(self.time_diffs_between_updates) / len(self.time_diffs_between_updates)
# At minimum, update every AUTOUPDATE_MAX_SLEEP_SECONDS regardless
# of how often we're seeing new data appear
if sleep_secs > self.secret.AUTOUPDATE_MAX_SLEEP_SECONDS:
sleep_secs = self.secret.AUTOUPDATE_MAX_SLEEP_SECONDS
logger.info('Sleeping for %0.01f sec' % sleep_secs)
time.sleep(sleep_secs)
self.autoupdate_invocations += 1
if self.secret.AUTOUPDATE_MAX_LOOP_INVOCATIONS > 0 and self.autoupdate_invocations >= self.secret.AUTOUPDATE_MAX_LOOP_INVOCATIONS:
return 0
if len(time_diffs) > 2: class AutoupdateError(RuntimeError):
logger.info('Sleeping 60 seconds after unexpected no index change. (New data might be delayed.)') def __str__(self):
time.sleep(60) return "%s: %s" % (self.__class__.__name__, super().__str__())
continue
sleep_secs = AUTOUPDATE_DEFAULT_SLEEP_SECONDS class AutoupdateWarning(RuntimeWarning):
if AUTOUPDATE_USE_FIXED_SLEEP != 1: def __str__(self):
if len(time_diffs) > 10: return "%s: %s" % (self.__class__.__name__, super().__str__())
time_diffs = time_diffs[1:] class AutoupdateFailureError(AutoupdateError):
pass
if len(time_diffs) > 2: class AutoupdateFailureWarning(AutoupdateWarning):
sleep_secs = sum(time_diffs) / len(time_diffs) pass
if sleep_secs > AUTOUPDATE_MAX_SLEEP_SECONDS: class AutoupdateNoEventIndexesDetectedError(AutoupdateError):
sleep_secs = AUTOUPDATE_MAX_SLEEP_SECONDS pass
# Sleep for a rolling average of time between updates class AutoupdateNoNewDataDetectedError(AutoupdateError):
logger.info('Sleeping for %d sec' % sleep_secs) pass
time.sleep(sleep_secs)
class AutoupdateFailureException(RuntimeError): class AutoupdateNoIndexChangeWarning(AutoupdateWarning):
pass pass
+6 -3
View File
@@ -19,7 +19,7 @@ def get(val, default=None):
def get_number(name, default): def get_number(name, default):
val = get(name, default) val = get(name, default)
try: try:
return int(val) return float(val)
except ValueError: except ValueError:
print("Error: %s must be a number." % name) print("Error: %s must be a number." % name)
print("Current value: %s" % val) print("Current value: %s" % val)
@@ -50,9 +50,12 @@ if not get('TIMEZONE_NAME') and get('TZ'):
AUTOUPDATE_DEFAULT_SLEEP_SECONDS = get_number('AUTOUPDATE_DEFAULT_SLEEP_SECONDS', '300') # 5 minutes AUTOUPDATE_DEFAULT_SLEEP_SECONDS = get_number('AUTOUPDATE_DEFAULT_SLEEP_SECONDS', '300') # 5 minutes
AUTOUPDATE_MAX_SLEEP_SECONDS = get_number('AUTOUPDATE_MAX_SLEEP_SECONDS', '1500') # 25 minutes AUTOUPDATE_MAX_SLEEP_SECONDS = get_number('AUTOUPDATE_MAX_SLEEP_SECONDS', '1500') # 25 minutes
AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS = get_number('AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS', '60') # 1 minute
AUTOUPDATE_USE_FIXED_SLEEP = get_bool('AUTOUPDATE_USE_FIXED_SLEEP', 'false') AUTOUPDATE_USE_FIXED_SLEEP = get_bool('AUTOUPDATE_USE_FIXED_SLEEP', 'false')
AUTOUPDATE_FAILURE_MINUTES = get_number('AUTOUPDATE_FAILURE_MINUTES', '180') # 3 hours AUTOUPDATE_NO_DATA_FAILURE_MINUTES = get_number('AUTOUPDATE_NO_DATA_FAILURE_MINUTES', '180') # 3 hours
AUTOUPDATE_RESTART_ON_FAILURE = get_bool('AUTOUPDATE_RESTART_ON_FAILURE', 'false') AUTOUPDATE_FAILURE_MINUTES = get_number('AUTOUPDATE_FAILURE_MINUTES', '15') # 15 minutes
AUTOUPDATE_RESTART_ON_FAILURE = get_bool('AUTOUPDATE_RESTART_ON_FAILURE', 'true')
AUTOUPDATE_MAX_LOOP_INVOCATIONS = get_number('AUTOUPDATE_MAX_LOOP_INVOCATIONS', '-1')
ENABLE_TESTING_MODES = get_bool('ENABLE_TESTING_MODES', 'false') ENABLE_TESTING_MODES = get_bool('ENABLE_TESTING_MODES', 'false')
SKIP_NS_LAST_UPLOADED_CHECK = get_bool('SKIP_NS_LAST_UPLOADED_CHECK', 'false') SKIP_NS_LAST_UPLOADED_CHECK = get_bool('SKIP_NS_LAST_UPLOADED_CHECK', 'false')
+1 -1
View File
@@ -69,7 +69,7 @@ def process_ciq_basal_events(data):
seconds = (existingTime - unprocessedTime).seconds seconds = (existingTime - unprocessedTime).seconds
newEvent = TConnectEntry.manual_suspension_to_basal_entry(suspension, seconds) newEvent = TConnectEntry.manual_suspension_to_basal_entry(suspension, seconds)
logger.debug("Creating basal event for unprocessed suspension: %s" % newEvent) logger.debug("Adding basal event for unprocessed suspension: %s" % newEvent)
newEvents.append(newEvent) newEvents.append(newEvent)
# Any remaining suspensions which have not been processed have not ended, # Any remaining suspensions which have not been processed have not ended,
+5 -2
View File
@@ -39,8 +39,11 @@ class AndroidApi(tconnectsync.api.android.AndroidApi):
raise NotImplementedError raise NotImplementedError
class TConnectApi(tconnectsync.api.TConnectApi): class TConnectApi(tconnectsync.api.TConnectApi):
def __init__(self): def __init__(self, email=None, password=None):
pass if email is not None and password is not None:
self.with_credentials = True
else:
self.with_credentials = False
_ciq = ControlIQApi() _ciq = ControlIQApi()
_ws2 = WS2Api() _ws2 = WS2Api()
+385
View File
@@ -0,0 +1,385 @@
#!/usr/bin/env python3
import os
import logging
import time
import unittest
import datetime
import importlib
import contextlib
from unittest.mock import patch
from tconnectsync.autoupdate import Autoupdate, AutoupdateFailureError, AutoupdateFailureWarning, AutoupdateNoEventIndexesDetectedError, AutoupdateNoIndexChangeWarning
from tconnectsync.secret import AUTOUPDATE_FAILURE_MINUTES, AUTOUPDATE_NO_DATA_FAILURE_MINUTES, AUTOUPDATE_RESTART_ON_FAILURE, TCONNECT_PASSWORD
from .api.fake import TConnectApi
from .nightscout_fake import NightscoutApi
logger = logging.getLogger(__name__)
def stub(*args, **kwargs):
pass
@contextlib.contextmanager
def build_mock_logger():
def fake_error(*args, **kwargs):
logger.error(*args, **kwargs)
def fake_warn(*args, **kwargs):
logger.warn(*args, **kwargs)
with patch("tconnectsync.autoupdate.logger.error") as mock_error, patch("tconnectsync.autoupdate.logger.warn") as mock_warn:
mock_error.side_effect = fake_error
mock_warn.side_effect = fake_warn
yield (mock_error, mock_warn)
def num_instances_of(cls, m):
return sum([isinstance(i[0][0], cls) for i in m.call_args_list])
class TestAutoupdate(unittest.TestCase):
maxDiff = None
# datetimes are unused
start = datetime.datetime(2021, 4, 20, 12, 0)
end = datetime.datetime(2021, 4, 21, 12, 0)
# each time the returned function is called, it returns the next argument
# until the end is reached, at which point it will repeat the last argument
def fake_last_event_uploaded(self, *indexes):
index = 0
def fake(*args, **kwargs):
nonlocal index
if index < len(indexes):
index += 1
data = {'maxPumpEventIndex': indexes[index-1], 'processingStatus': 1}
return data
return fake
# each time the returned function is called, it returns the next argument
# until the end is reached, at which point it will repeat the last argument
def fake_process_time_range(self, *returns):
index = 0
def fake(*args, **kwargs):
nonlocal index
if index < len(returns):
index += 1
return returns[index - 1]
return fake
def build_secrets(self, **kwargs):
secret = importlib.reload(importlib.import_module("tconnectsync.secret"))
class FakeSecret(object):
pass
fake = FakeSecret()
for k in dir(secret):
setattr(fake, k, getattr(secret, k))
for k, v in kwargs.items():
setattr(fake, k, v)
return fake
"""process_time_range should always be invoked the first time"""
def test_process_time_range_called_on_start(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1)
nightscout = NightscoutApi()
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=1,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=0
)
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range:
mock_process_time_range.return_value = 0
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=False)
self.assertEqual(ret, 0)
self.assertEqual(mock_process_time_range.call_count, 1)
self.assertEqual(u.autoupdate_invocations, 1)
self.assertEqual(u.last_event_index, 1)
"""process_time_range should never be called with pretend"""
def test_process_time_range_never_called_with_pretend(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1)
nightscout = NightscoutApi()
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=1,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=0
)
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range:
mock_process_time_range.return_value = 0
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=True)
self.assertEqual(ret, 0)
self.assertEqual(mock_process_time_range.call_count, 0)
self.assertEqual(u.autoupdate_invocations, 1)
self.assertEqual(u.last_event_index, 1)
"""
If the event index increases without process_time_range detecting new data,
AutoupdateFailureWarning should be raised.
"""
def test_autoupdate_failure_warning_on_index_process_time_range_discrepancy(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1, 2)
nightscout = NightscoutApi()
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=2,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=0,
TCONNECT_EMAIL="test@email.com",
TCONNECT_PASSWORD="testpassword"
)
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range, build_mock_logger() as (mock_error, mock_warn):
mock_process_time_range.side_effect = self.fake_process_time_range(0, 0)
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=False)
self.assertEqual(ret, 0)
self.assertEqual(mock_process_time_range.call_count, 2)
self.assertEqual(u.autoupdate_invocations, 2)
self.assertEqual(u.last_event_index, 2)
self.assertEqual(num_instances_of(AutoupdateFailureWarning, mock_warn), 1)
"""
On the first attempt, an AutoupdateFailureWarning should never be raised.
"""
def test_autoupdate_no_failure_warning_on_index_process_time_range_discrepancy_first_attempt(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1)
nightscout = NightscoutApi()
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=1,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=0
)
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range, build_mock_logger() as (mock_error, mock_warn):
mock_process_time_range.side_effect = self.fake_process_time_range(0, 0)
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=False)
self.assertEqual(ret, 0)
self.assertEqual(mock_process_time_range.call_count, 1)
self.assertEqual(u.autoupdate_invocations, 1)
self.assertEqual(u.last_event_index, 1)
self.assertEqual(num_instances_of(AutoupdateFailureWarning, mock_warn), 0)
"""
If the event index increases without process_time_range detecting new data
for AUTOUPDATE_FAILURE_MINUTES, an AutoupdateFailureError should be raised.
"""
def test_autoupdate_failure_error_on_index_process_time_range_discrepancy_for_failure_minutes(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1, 2)
nightscout = NightscoutApi()
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=2,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=0,
AUTOUPDATE_FAILURE_MINUTES=0,
AUTOUPDATE_RESTART_ON_FAILURE=False,
)
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range, build_mock_logger() as (mock_error, mock_warn):
mock_process_time_range.side_effect = self.fake_process_time_range(0, 0)
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=False)
self.assertEqual(ret, 0)
self.assertEqual(mock_process_time_range.call_count, 2)
self.assertEqual(u.autoupdate_invocations, 2)
self.assertEqual(u.last_event_index, 2)
self.assertEqual(num_instances_of(AutoupdateFailureError, mock_error), 1)
"""
If the event index increases without process_time_range detecting new data
for AUTOUPDATE_FAILURE_MINUTES, and AUTOUPDATE_RESTART_ON_FAILURE is true,
then an AutoupdateFailureError should be raised AND 1 should be returned.
"""
def test_autoupdate_failure_error_on_index_process_time_range_discrepancy_for_failure_minutes_performs_restart(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1, 2)
nightscout = NightscoutApi()
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=3,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=0,
AUTOUPDATE_FAILURE_MINUTES=0,
AUTOUPDATE_RESTART_ON_FAILURE=True,
)
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range, build_mock_logger() as (mock_error, mock_warn):
mock_process_time_range.side_effect = self.fake_process_time_range(0, 0)
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=False)
self.assertEqual(ret, 1)
self.assertEqual(mock_process_time_range.call_count, 2)
self.assertEqual(u.autoupdate_invocations, 1) # exits before invocations is incremented
self.assertEqual(u.last_event_index, 1) # exits before changed to 2
self.assertEqual(num_instances_of(AutoupdateFailureError, mock_error), 1)
"""Validate state after first successful update"""
def test_state_after_first_successful_update(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1)
nightscout = NightscoutApi()
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=1,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=0
)
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range:
mock_process_time_range.side_effect = self.fake_process_time_range(1)
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=False)
self.assertEqual(ret, 0)
self.assertEqual(mock_process_time_range.call_count, 1)
self.assertEqual(u.autoupdate_invocations, 1)
self.assertEqual(u.last_event_index, 1)
self.assertTrue(u.last_event_time == u.last_attempt_time == u.last_successful_process_time_range)
self.assertEqual(len(u.time_diffs_between_updates), 0)
"""Validate sleep occurs for the given fixed length when set"""
def test_sleep_for_fixed_length_when_set(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1)
nightscout = NightscoutApi()
sleep_length = 3 # sentinel
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=1,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=sleep_length
)
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range, patch("tconnectsync.autoupdate.time.sleep") as mock_sleep:
mock_process_time_range.side_effect = self.fake_process_time_range(1)
mock_sleep.side_effect = None
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=False)
self.assertEqual(ret, 0)
self.assertEqual(mock_process_time_range.call_count, 1)
self.assertEqual(u.autoupdate_invocations, 1)
self.assertEqual(u.last_event_index, 1)
self.assertTrue(mock_sleep.called)
self.assertEqual(mock_sleep.call_args[0], (sleep_length,))
"""
If there is no event index update for AUTOUPDATE_NO_DATA_FAILURE_MINUTES,
then a AutoupdateNoEventIndexesDetectedError should be raised and
a restart should be triggered with AUTOUPDATE_RESTART_ON_FAILURE.
"""
def test_autoupdate_no_event_indexes_detected_error_on_no_index_change_less_than_three_iterations(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1)
nightscout = NightscoutApi()
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=100,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=0.1,
AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS=0.2,
AUTOUPDATE_RESTART_ON_FAILURE=True,
AUTOUPDATE_NO_DATA_FAILURE_MINUTES=1/600 # 0.1 second
)
# with more than 3 iterations, the sleep iteration will change
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range, build_mock_logger() as (mock_error, mock_warn):
mock_process_time_range.side_effect = self.fake_process_time_range(1, 0)
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=False)
self.assertEqual(ret, 1)
self.assertEqual(num_instances_of(AutoupdateNoEventIndexesDetectedError, mock_error), 1)
"""
If there has been 3 failed attempts since the last time we found new data,
we should sleep for AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS and
a AutoupdateNoIndexChangeWarning should be logged.
"""
def test_autoupdate_no_index_change_warning_on_unexpected_no_index_sleep(self):
tconnect = TConnectApi()
tconnect._android.last_event_uploaded = self.fake_last_event_uploaded(1)
nightscout = NightscoutApi()
secret = self.build_secrets(
AUTOUPDATE_MAX_LOOP_INVOCATIONS=5,
AUTOUPDATE_USE_FIXED_SLEEP=True,
AUTOUPDATE_DEFAULT_SLEEP_SECONDS=0.1,
AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS=0.2
)
with patch("tconnectsync.autoupdate.process_time_range") as mock_process_time_range, patch("tconnectsync.autoupdate.time.sleep") as mock_sleep, build_mock_logger() as (mock_error, mock_warn):
mock_process_time_range.side_effect = self.fake_process_time_range(0)
mock_sleep.side_effect = None
u = Autoupdate(secret)
ret = u.process(tconnect, nightscout, self.start, self.end, pretend=False)
self.assertEqual(ret, 0)
self.assertEqual(u.autoupdate_invocations, 5)
self.assertEqual(u.last_event_index, 1)
self.assertTrue(mock_sleep.called)
self.assertEqual(len(mock_sleep.call_args_list), 5)
self.assertEqual(mock_sleep.call_args_list[0][0], (0.1,))
self.assertEqual(mock_sleep.call_args_list[1][0], (0.1,))
self.assertEqual(mock_sleep.call_args_list[2][0], (0.1,))
self.assertEqual(mock_sleep.call_args_list[3][0], (0.2,))
self.assertEqual(mock_sleep.call_args_list[4][0], (0.2,))
self.assertEqual(num_instances_of(AutoupdateNoIndexChangeWarning, mock_warn), 2)