mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-24 03:34:12 -05:00
autoupdate: refactor into class and write test
This commit is contained in:
Generated
+6
-6
@@ -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": [
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
Reference in New Issue
Block a user