async def test_setting_cancels_and_resyncs(reader: ReaderHelper, ui_server: UiServer, config: Config, server, session, drive: DriveSource, coord: Coordinator): # Create a blocking sync task coord._sync_wait.set() sync = asyncio.create_task(coord.sync(), name="Sync from saving settings") await coord._sync_start.wait() assert not sync.cancelled() assert not sync.done() # Change some config update = { "config": { "days_between_snapshots": 20, "drive_ipv4": "" }, "snapshot_folder": "unused" } assert await reader.postjson("saveconfig", json=update) == { 'message': 'Settings saved' } # verify the previous sync is done and another one is running assert sync.done() assert coord.isSyncing()
async def test_update_disable_drive(reader: ReaderHelper, server, coord: Coordinator, config: Config, drive_requests: DriveRequests): # Disable drive drive_requests.creds = None os.remove(config.get(Setting.CREDENTIALS_FILE_PATH)) assert not coord.enabled() await coord.sync() assert len(coord.snapshots()) == 0 # Disable Drive Upload update = { "config": { Setting.ENABLE_DRIVE_UPLOAD.value: False }, "snapshot_folder": "" } assert await reader.postjson("saveconfig", json=update) == { 'message': 'Settings saved', "reload_page": True } assert config.get(Setting.ENABLE_DRIVE_UPLOAD) is False # Verify the app is working fine. assert coord.enabled() await coord.waitForSyncToFinish() assert len(coord.snapshots()) == 1
async def test_check_size_sync(coord: Coordinator, source: HelperTestSource, dest: HelperTestSource, time, fs: FsFaker, global_info: GlobalInfo): skipForWindows() fs.setFreeBytes(0) await coord.sync() assert len(coord.snapshots()) == 0 assert global_info._last_error is not None await coord.sync() assert len(coord.snapshots()) == 0 assert global_info._last_error is not None # Verify it resets the global size skip check, but gets through once global_info.setSkipSpaceCheckOnce(True) await coord.sync() assert len(coord.snapshots()) == 1 assert global_info._last_error is None assert not global_info.isSkipSpaceCheckOnce() # Next attempt to snapshot shoudl fail again. time.advance(days=7) await coord.sync() assert len(coord.snapshots()) == 1 assert global_info._last_error is not None
async def test_update_ignore(reader: ReaderHelper, time: FakeTime, coord: Coordinator, config: Config, supervisor: SimulatedSupervisor, ha: HaSource, drive: DriveSource): config.override(Setting.IGNORE_UPGRADE_SNAPSHOTS, True) config.override(Setting.DAYS_BETWEEN_SNAPSHOTS, 0) # make an ignored_snapshot slug = await supervisor.createSnapshot( { 'name': "Ignore_me", 'folders': ['homeassistant'], 'addons': [] }, date=time.now()) await coord.sync() assert len(await drive.get()) == 0 assert len(await ha.get()) == 1 assert len(coord.snapshots()) == 1 # Disable Drive Upload update = { "ignore": False, "slug": slug, } await reader.postjson("ignore", json=update) await coord.waitForSyncToFinish() assert len(coord.snapshots()) == 1 assert len(await drive.get()) == 1 assert len(await ha.get()) == 1
async def test_sync(reader, ui_server, coord: Coordinator, time: FakeTime, session): assert len(coord.snapshots()) == 0 status = await reader.getjson("sync") assert len(coord.snapshots()) == 1 assert status == await reader.getjson("getstatus") time.advance(days=7) assert len((await reader.getjson("sync"))['snapshots']) == 2
async def test_working_through_upload(coord: Coordinator, global_info: GlobalInfo, dest): coord._sync_wait.clear() assert not coord.isWorkingThroughUpload() sync_task = asyncio.create_task(coord.sync()) await coord._sync_start.wait() assert not coord.isWorkingThroughUpload() dest.working = True assert coord.isWorkingThroughUpload() coord._sync_wait.set() await asyncio.wait([sync_task]) assert not coord.isWorkingThroughUpload()
async def test_disabled_at_install(coord: Coordinator, dest, time): """ Verifies that at install time, if some snapshots are already present the addon doesn't try to sync over and over when drive is disabled. This was a problem at one point. """ dest.setEnabled(True) await coord.sync() assert len(coord.snapshots()) == 1 dest.setEnabled(False) time.advance(days=5) assert coord.check() await coord.sync() assert not coord.check()
async def test_new_snapshot(coord: Coordinator, time: FakeTime, source, dest): await coord.startSnapshot(CreateOptions(time.now(), "Test Name")) snapshots = coord.snapshots() assert len(snapshots) == 1 assert snapshots[0].name() == "Test Name" assert snapshots[0].getSource(source.name()) is not None assert snapshots[0].getSource(dest.name()) is None
async def test_sync(coord: Coordinator, global_info: GlobalInfo, time: FakeTime): await coord.sync() assert global_info._syncs == 1 assert global_info._successes == 1 assert global_info._last_sync_start == time.now() assert len(coord.snapshots()) == 1
async def test_new_backup(coord: Coordinator, time: FakeTime, source, dest): await coord.startBackup(CreateOptions(time.now(), "Test Name")) backups = coord.backups() assert len(backups) == 1 assert backups[0].name() == "Test Name" assert backups[0].getSource(source.name()) is not None assert backups[0].getSource(dest.name()) is None
async def test_delete(coord: Coordinator, snapshot, source, dest): assert snapshot.getSource(source.name()) is not None assert snapshot.getSource(dest.name()) is not None await coord.delete([source.name()], snapshot.slug()) assert len(coord.snapshots()) == 1 assert snapshot.getSource(source.name()) is None assert snapshot.getSource(dest.name()) is not None await coord.delete([dest.name()], snapshot.slug()) assert snapshot.getSource(source.name()) is None assert snapshot.getSource(dest.name()) is None assert snapshot.isDeleted() assert len(coord.snapshots()) == 0 await coord.sync() assert len(coord.snapshots()) == 1 await coord.delete([source.name(), dest.name()], coord.snapshots()[0].slug()) assert len(coord.snapshots()) == 0
async def test_only_source_configured(coord: Coordinator, dest: HelperTestSource, time, source: HelperTestSource): source.setEnabled(True) dest.setEnabled(False) dest.setNeedsConfiguration(False) await coord.sync() assert len(coord.snapshots()) == 1
async def test_delete(coord: Coordinator, backup, source, dest): assert backup.getSource(source.name()) is not None assert backup.getSource(dest.name()) is not None await coord.delete([source.name()], backup.slug()) assert len(coord.backups()) == 1 assert backup.getSource(source.name()) is None assert backup.getSource(dest.name()) is not None await coord.delete([dest.name()], backup.slug()) assert backup.getSource(source.name()) is None assert backup.getSource(dest.name()) is None assert backup.isDeleted() assert len(coord.backups()) == 0 await coord.sync() assert len(coord.backups()) == 1 await coord.delete([source.name(), dest.name()], coord.backups()[0].slug()) assert len(coord.backups()) == 0
async def test_freshness(coord: Coordinator, source: HelperTestSource, dest: HelperTestSource, snapshot: Snapshot, time: FakeTime): source.setMax(2) dest.setMax(2) await coord.sync() assert snapshot.getPurges() == {source.name(): False, dest.name(): False} source.setMax(1) dest.setMax(1) await coord.sync() assert snapshot.getPurges() == {source.name(): True, dest.name(): True} dest.setMax(0) await coord.sync() assert snapshot.getPurges() == {source.name(): True, dest.name(): False} source.setMax(0) await coord.sync() assert snapshot.getPurges() == {source.name(): False, dest.name(): False} source.setMax(2) dest.setMax(2) time.advance(days=7) await coord.sync() assert len(coord.snapshots()) == 2 assert snapshot.getPurges() == {source.name(): True, dest.name(): True} assert coord.snapshots()[1].getPurges() == { source.name(): False, dest.name(): False } # should refresh on delete source.setMax(1) dest.setMax(1) await coord.delete([source.name()], snapshot.slug()) assert coord.snapshots()[0].getPurges() == {dest.name(): True} assert coord.snapshots()[1].getPurges() == { source.name(): True, dest.name(): False } # should update on retain await coord.retain({dest.name(): True}, snapshot.slug()) assert coord.snapshots()[0].getPurges() == {dest.name(): False} assert coord.snapshots()[1].getPurges() == { source.name(): True, dest.name(): True } # should update on upload await coord.uploadSnapshot(coord.snapshots()[0].slug()) assert coord.snapshots()[0].getPurges() == { dest.name(): False, source.name(): True } assert coord.snapshots()[1].getPurges() == { source.name(): False, dest.name(): True }
async def test_max_sync_interval_next_sync_attempt(coord: Coordinator, model, source: HelperTestSource, dest: HelperTestSource, backup, time: FakeTime, simple_config: Config): """ Next backup is after max sync interval is reached """ simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) simple_config.override(Setting.MAX_SYNC_INTERVAL_SECONDS, 60 * 60) time.setTimeZone("Europe/Stockholm") simple_config.override(Setting.BACKUP_TIME_OF_DAY, "03:23") simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) source.setMax(10) source.insert("Fri", time.toUtc(time.local(2020, 3, 16, 3, 33))) time.setNow(time.local(2020, 3, 17, 1, 29)) model.reinitialize() coord.reset() await coord.sync() assert coord.nextSyncAttempt() == time.local(2020, 3, 17, 2, 29) assert coord.nextBackupTime() > coord.nextSyncAttempt()
async def test_schedule_snapshot_next_sync_attempt(coord: Coordinator, model, source: HelperTestSource, dest: HelperTestSource, snapshot, time: FakeTime, simple_config: Config): """ Next snapshot is before max sync interval is reached """ simple_config.override(Setting.DAYS_BETWEEN_SNAPSHOTS, 1) simple_config.override(Setting.MAX_SYNC_INTERVAL_SECONDS, 60 * 60) time.setTimeZone("Europe/Stockholm") simple_config.override(Setting.SNAPSHOT_TIME_OF_DAY, "03:23") simple_config.override(Setting.DAYS_BETWEEN_SNAPSHOTS, 1) source.setMax(10) source.insert("Fri", time.toUtc(time.local(2020, 3, 16, 3, 33))) time.setNow(time.local(2020, 3, 17, 2, 29)) model.reinitialize() coord.reset() await coord.sync() assert coord.nextSnapshotTime() == time.local(2020, 3, 17, 3, 23) assert coord.nextSnapshotTime() == coord.nextSyncAttempt()
async def test_backup_now(reader, ui_server, time: FakeTime, snapshot: Snapshot, coord: Coordinator): assert len(coord.snapshots()) == 1 assert ( await reader.getjson("getstatus"))["snapshots"][0]["date"] == time.toLocal( time.now()).strftime("%c") time.advance(hours=1) assert await reader.getjson( "snapshot?custom_name=TestName&retain_drive=False&retain_ha=False" ) == { 'message': "Requested snapshot 'TestName'" } status = await reader.getjson('getstatus') assert len(status["snapshots"]) == 2 assert status["snapshots"][1]["date"] == time.toLocal( time.now()).strftime("%c") assert status["snapshots"][1]["name"] == "TestName" assert not status["snapshots"][1]["driveRetain"] assert not status["snapshots"][1]["haRetain"] time.advance(hours=1) assert await reader.getjson( "snapshot?custom_name=TestName2&retain_drive=True&retain_ha=False" ) == { 'message': "Requested snapshot 'TestName2'" } await coord.sync() status = await reader.getjson('getstatus') assert len(status["snapshots"]) == 3 assert not status["snapshots"][1]["driveRetain"] assert status["snapshots"][2]["date"] == time.toLocal( time.now()).strftime("%c") assert status["snapshots"][2]["name"] == "TestName2" assert not status["snapshots"][2]["haRetain"] assert status["snapshots"][2]["driveRetain"] time.advance(hours=1) assert await reader.getjson( "snapshot?custom_name=TestName3&retain_drive=False&retain_ha=True" ) == { 'message': "Requested snapshot 'TestName3'" } await coord.sync() status = await reader.getjson('getstatus') assert len(status["snapshots"]) == 4 assert not status["snapshots"][1]["driveRetain"] assert status["snapshots"][3]["date"] == time.toLocal( time.now()).strftime("%c") assert status["snapshots"][3]["name"] == "TestName3" assert status["snapshots"][3]["haRetain"] assert not status["snapshots"][3]["driveRetain"]
async def test_backup_now(reader, ui_server, time: FakeTime, backup: Backup, coord: Coordinator): assert len(coord.backups()) == 1 assert (await reader.getjson("getstatus"))["backups"][0]["date"] == time.toLocal( time.now()).strftime("%c") time.advance(hours=1) assert await reader.getjson( "backup?custom_name=TestName&retain_drive=False&retain_ha=False") == { 'message': "Requested backup 'TestName'" } status = await reader.getjson('getstatus') assert len(status["backups"]) == 2 assert status["backups"][1]["date"] == time.toLocal( time.now()).strftime("%c") assert status["backups"][1]["name"] == "TestName" assert status["backups"][1]['sources'][0]['retained'] is False assert len(status["backups"][1]['sources']) == 1 time.advance(hours=1) assert await reader.getjson( "backup?custom_name=TestName2&retain_drive=True&retain_ha=False") == { 'message': "Requested backup 'TestName2'" } await coord.sync() status = await reader.getjson('getstatus') assert len(status["backups"]) == 3 assert status["backups"][2]["date"] == time.toLocal( time.now()).strftime("%c") assert status["backups"][2]["name"] == "TestName2" assert status["backups"][2]['sources'][0]['retained'] is False assert status["backups"][2]['sources'][1]['retained'] is True time.advance(hours=1) assert await reader.getjson( "backup?custom_name=TestName3&retain_drive=False&retain_ha=True") == { 'message': "Requested backup 'TestName3'" } await coord.sync() status = await reader.getjson('getstatus') assert len(status["backups"]) == 4 assert status["backups"][3]['sources'][0]['retained'] is True assert status["backups"][3]['sources'][1]['retained'] is False assert status["backups"][3]["date"] == time.toLocal( time.now()).strftime("%c") assert status["backups"][3]["name"] == "TestName3"
async def test_blocking(coord: Coordinator): # This just makes sure the wait thread is blocked while we do stuff event_start = asyncio.Event() event_end = asyncio.Event() asyncio.create_task(coord._withSoftLock(lambda: sleepHelper(event_start, event_end))) await event_start.wait() # Make sure PleaseWait gets called on these with raises(PleaseWait): await coord.delete(None, None) with raises(PleaseWait): await coord.sync() with raises(PleaseWait): await coord.uploadBackups(None) with raises(PleaseWait): await coord.startBackup(None) event_end.set()
async def test_alternate_timezone(coord: Coordinator, time: FakeTime, model: Model, dest, source, simple_config: Config): time.setTimeZone("Europe/Stockholm") simple_config.override(Setting.BACKUP_TIME_OF_DAY, "12:00") simple_config.override(Setting.DAYS_BETWEEN_BACKUPS, 1) source.setMax(10) source.insert("Fri", time.toUtc(time.local(2020, 3, 16, 18, 5))) time.setNow(time.local(2020, 3, 16, 18, 6)) model.reinitialize() coord.reset() await coord.sync() assert not coord.check() assert coord.nextBackupTime() == time.local(2020, 3, 17, 12) time.setNow(time.local(2020, 3, 17, 11, 59)) await coord.sync() assert not coord.check() time.setNow(time.local(2020, 3, 17, 12)) assert coord.check()
async def test_backoff(coord: Coordinator, model, source: HelperTestSource, dest: HelperTestSource, snapshot, time: FakeTime, simple_config: Config): assert coord.check() simple_config.override(Setting.DAYS_BETWEEN_SNAPSHOTS, 1) simple_config.override(Setting.MAX_SYNC_INTERVAL_SECONDS, 60 * 60 * 6) assert coord.nextSyncAttempt() == time.now() + timedelta(hours=6) assert not coord.check() error = Exception("BOOM") old_sync = model.sync model.sync = lambda s: doRaise(error) await coord.sync() # first backoff should be 0 seconds assert coord.nextSyncAttempt() == time.now() assert coord.check() # backoff maxes out at 1 hr = 3600 seconds for seconds in [ 10, 20, 40, 80, 160, 320, 640, 1280, 2560, 3600, 3600, 3600 ]: await coord.sync() assert coord.nextSyncAttempt() == time.now() + timedelta( seconds=seconds) assert not coord.check() assert not coord.check() assert not coord.check() # a good sync resets it back to 6 hours from now model.sync = old_sync await coord.sync() assert coord.nextSyncAttempt() == time.now() + timedelta(hours=6) assert not coord.check() # if the next snapshot is less that 6 hours from the last one, that that shoudl be when we sync simple_config.override(Setting.DAYS_BETWEEN_SNAPSHOTS, 1.0 / 24.0) assert coord.nextSyncAttempt() == time.now() + timedelta(hours=1) assert not coord.check() time.advance(hours=2) assert coord.nextSyncAttempt() == time.now() - timedelta(hours=1) assert coord.check()
async def test_cancel(coord: Coordinator, global_info: GlobalInfo): coord._sync_wait.clear() asyncio.create_task(coord.sync()) await coord._sync_start.wait() await coord.cancel() assert isinstance(global_info._last_error, UserCancelledError)
def coord(model, time, simple_config, global_info, estimator): return Coordinator(model, time, simple_config, global_info, estimator)
async def test_enabled(coord: Coordinator, dest, time): dest.setEnabled(True) assert coord.enabled() dest.setEnabled(False) assert not coord.enabled()