2019-12-17 17:54:29 +03:00
|
|
|
# !/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
@author: Sam Schott (ss2151@cam.ac.uk)
|
|
|
|
|
|
|
|
(c) Sam Schott; This work is licensed under a Creative Commons
|
|
|
|
Attribution-NonCommercial-NoDerivs 2.0 UK: England & Wales License.
|
|
|
|
|
|
|
|
"""
|
2020-03-15 02:29:23 +03:00
|
|
|
import timeit
|
2020-03-08 07:28:56 +03:00
|
|
|
from watchdog.events import (
|
2020-02-19 15:58:07 +03:00
|
|
|
FileCreatedEvent, FileDeletedEvent, FileModifiedEvent, FileMovedEvent,
|
2020-03-06 00:32:15 +03:00
|
|
|
DirCreatedEvent, DirDeletedEvent, DirMovedEvent,
|
2020-02-19 15:58:07 +03:00
|
|
|
)
|
2020-03-08 07:28:56 +03:00
|
|
|
from maestral.sync import UpDownSync
|
2019-12-17 17:54:29 +03:00
|
|
|
|
|
|
|
|
2020-03-04 15:52:34 +03:00
|
|
|
class DummyUpDownSync(UpDownSync):
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
pass
|
|
|
|
|
2020-03-06 01:55:36 +03:00
|
|
|
def _should_split_excluded(self, event):
|
2020-03-04 15:52:34 +03:00
|
|
|
return False
|
|
|
|
|
2020-03-05 00:55:02 +03:00
|
|
|
def is_excluded(self, dbx_path):
|
|
|
|
return False
|
|
|
|
|
2020-03-04 15:52:34 +03:00
|
|
|
|
2019-12-17 17:54:29 +03:00
|
|
|
def test_clean_local_events():
|
|
|
|
|
2020-03-15 02:16:25 +03:00
|
|
|
def path(i):
|
|
|
|
return f'/test {i}'
|
|
|
|
|
|
|
|
# Simple cases
|
|
|
|
file_events_test0 = [
|
|
|
|
# created + deleted -> None
|
|
|
|
FileCreatedEvent(path(1)),
|
|
|
|
FileDeletedEvent(path(1)),
|
|
|
|
# deleted + created -> modified
|
|
|
|
FileDeletedEvent(path(2)),
|
|
|
|
FileCreatedEvent(path(2)),
|
|
|
|
]
|
|
|
|
|
|
|
|
res0 = [
|
|
|
|
# created + deleted -> None
|
|
|
|
# deleted + created -> modified
|
|
|
|
FileModifiedEvent(path(2))
|
|
|
|
]
|
|
|
|
|
|
|
|
# Single file events, keep as is
|
|
|
|
file_events_test1 = [
|
|
|
|
FileModifiedEvent(path(1)),
|
|
|
|
FileCreatedEvent(path(2)),
|
|
|
|
FileDeletedEvent(path(3)),
|
|
|
|
FileMovedEvent(path(4), path(5)),
|
|
|
|
]
|
|
|
|
|
|
|
|
res1 = [
|
|
|
|
FileModifiedEvent(path(1)),
|
|
|
|
FileCreatedEvent(path(2)),
|
|
|
|
FileDeletedEvent(path(3)),
|
|
|
|
FileMovedEvent(path(4), path(5)),
|
|
|
|
]
|
|
|
|
|
|
|
|
# Difficult move cases
|
|
|
|
file_events_test2 = [
|
|
|
|
# created + moved -> created
|
|
|
|
FileCreatedEvent(path(1)),
|
|
|
|
FileMovedEvent(path(1), path(2)),
|
|
|
|
# moved + deleted -> deleted
|
|
|
|
FileMovedEvent(path(1), path(4)),
|
|
|
|
FileDeletedEvent(path(4)),
|
|
|
|
# moved + moved back -> modified
|
|
|
|
FileMovedEvent(path(5), path(6)),
|
|
|
|
FileMovedEvent(path(6), path(5)),
|
2020-03-26 16:54:10 +03:00
|
|
|
# moved + moved -> deleted + created
|
|
|
|
# (this is currently not handled as a single moved)
|
2020-03-15 02:16:25 +03:00
|
|
|
FileMovedEvent(path(7), path(8)),
|
|
|
|
FileMovedEvent(path(8), path(9)),
|
|
|
|
]
|
|
|
|
|
|
|
|
res2 = [
|
|
|
|
# created + moved -> created
|
|
|
|
FileCreatedEvent(path(2)),
|
|
|
|
# moved + deleted -> deleted
|
|
|
|
FileDeletedEvent(path(1)),
|
|
|
|
# moved + moved back -> modified
|
|
|
|
FileModifiedEvent(path(5)),
|
2020-03-26 16:54:10 +03:00
|
|
|
# moved + moved -> deleted + created
|
|
|
|
# (this is currently not handled as a single moved)
|
2020-03-15 02:16:25 +03:00
|
|
|
FileDeletedEvent(path(7)),
|
|
|
|
FileCreatedEvent(path(9)),
|
|
|
|
]
|
|
|
|
|
|
|
|
# Gedit save event
|
|
|
|
file_events_test3 = [
|
2020-03-26 16:54:10 +03:00
|
|
|
FileCreatedEvent('.gedit-save-UR4EC0'), # save new version to tmp file
|
2020-03-15 02:16:25 +03:00
|
|
|
FileModifiedEvent('.gedit-save-UR4EC0'), # modify tmp file
|
2020-03-26 16:54:10 +03:00
|
|
|
FileMovedEvent(path(1), path(1) + '~'), # move old version to backup
|
|
|
|
FileMovedEvent('.gedit-save-UR4EC0', path(1)), # replace old version with tmp
|
2020-03-15 02:16:25 +03:00
|
|
|
]
|
|
|
|
|
|
|
|
res3 = [
|
2020-03-26 16:54:10 +03:00
|
|
|
FileModifiedEvent(path(1)), # modified file
|
2020-03-15 02:16:25 +03:00
|
|
|
FileCreatedEvent(path(1) + '~'), # backup
|
|
|
|
]
|
|
|
|
|
|
|
|
# macOS safe-save event
|
|
|
|
file_events_test4 = [
|
|
|
|
FileMovedEvent(path(1), path(1) + '.sb-b78ef837-dLht38'), # move to backup
|
2020-03-26 16:54:10 +03:00
|
|
|
FileCreatedEvent(path(1)), # create new version
|
|
|
|
FileDeletedEvent(path(1) + '.sb-b78ef837-dLht38'), # delete backup
|
2020-03-15 02:16:25 +03:00
|
|
|
]
|
|
|
|
|
|
|
|
res4 = [
|
|
|
|
FileModifiedEvent(path(1)), # modified file
|
|
|
|
]
|
|
|
|
|
|
|
|
# Word on macOS created event
|
|
|
|
file_events_test5 = [
|
|
|
|
FileCreatedEvent(path(1)),
|
|
|
|
FileDeletedEvent(path(1)),
|
|
|
|
FileCreatedEvent(path(1)),
|
|
|
|
FileCreatedEvent('~$' + path(1)),
|
|
|
|
]
|
|
|
|
|
|
|
|
res5 = [
|
2020-03-26 16:54:10 +03:00
|
|
|
FileCreatedEvent(path(1)), # created file
|
|
|
|
FileCreatedEvent('~$' + path(1)), # backup
|
2020-03-15 02:16:25 +03:00
|
|
|
]
|
|
|
|
|
|
|
|
# simple type changes
|
|
|
|
file_events_test6 = [
|
|
|
|
# keep as is
|
|
|
|
FileDeletedEvent(path(1)),
|
|
|
|
DirCreatedEvent(path(1)),
|
|
|
|
# keep as is
|
|
|
|
DirDeletedEvent(path(2)),
|
|
|
|
FileCreatedEvent(path(2)),
|
|
|
|
]
|
|
|
|
|
|
|
|
res6 = [
|
|
|
|
# keep as is
|
|
|
|
FileDeletedEvent(path(1)),
|
|
|
|
DirCreatedEvent(path(1)),
|
|
|
|
# keep as is
|
|
|
|
DirDeletedEvent(path(2)),
|
|
|
|
FileCreatedEvent(path(2)),
|
|
|
|
]
|
|
|
|
|
|
|
|
# difficult type changes
|
|
|
|
file_events_test7 = [
|
|
|
|
# convert to FileDeleted -> DirCreated
|
|
|
|
FileModifiedEvent(path(1)),
|
|
|
|
FileDeletedEvent(path(1)),
|
|
|
|
FileCreatedEvent(path(1)),
|
|
|
|
FileDeletedEvent(path(1)),
|
|
|
|
DirCreatedEvent(path(1)),
|
|
|
|
# convert to FileDeleted(path1) -> DirCreated(path2)
|
|
|
|
FileModifiedEvent(path(2)),
|
|
|
|
FileDeletedEvent(path(2)),
|
|
|
|
FileCreatedEvent(path(2)),
|
|
|
|
FileDeletedEvent(path(2)),
|
|
|
|
DirCreatedEvent(path(2)),
|
|
|
|
DirMovedEvent(path(2), path(3)),
|
|
|
|
]
|
|
|
|
|
|
|
|
res7 = [
|
|
|
|
FileDeletedEvent(path(1)),
|
|
|
|
DirCreatedEvent(path(1)),
|
|
|
|
|
|
|
|
FileDeletedEvent(path(2)),
|
|
|
|
DirCreatedEvent(path(3)),
|
|
|
|
]
|
|
|
|
|
|
|
|
# event hierarchies
|
|
|
|
file_events_test8 = [
|
|
|
|
# convert to a single DirDeleted
|
|
|
|
DirDeletedEvent(path(1)),
|
|
|
|
FileDeletedEvent(path(1) + '/file1.txt'),
|
|
|
|
FileDeletedEvent(path(1) + '/file2.txt'),
|
|
|
|
DirDeletedEvent(path(1) + '/sub'),
|
|
|
|
FileDeletedEvent(path(1) + '/sub/file3.txt'),
|
|
|
|
# convert to a single DirMoved
|
|
|
|
DirMovedEvent(path(2), path(3)),
|
|
|
|
FileMovedEvent(path(2) + '/file1.txt', path(3) + '/file1.txt'),
|
|
|
|
FileMovedEvent(path(2) + '/file2.txt', path(3) + '/file2.txt'),
|
|
|
|
DirMovedEvent(path(2) + '/sub', path(3) + '/sub'),
|
|
|
|
FileMovedEvent(path(2) + '/sub/file3.txt', path(3) + '/sub/file3.txt'),
|
|
|
|
]
|
|
|
|
|
|
|
|
res8 = [
|
|
|
|
DirDeletedEvent(path(1)),
|
|
|
|
DirMovedEvent(path(2), path(3)),
|
|
|
|
]
|
|
|
|
|
2020-03-15 22:32:16 +03:00
|
|
|
# performance test:
|
|
|
|
# 15,000 nested deleted events (10,000 folders, 5,000 files)
|
|
|
|
# 15,000 nested moved events (10,000 folders, 5,000 files)
|
|
|
|
# 4,995 unrelated created events
|
2020-03-26 16:54:10 +03:00
|
|
|
file_events_test9 = [DirDeletedEvent(n * path(1)) for n in range(1, 10001)]
|
|
|
|
file_events_test9 += [FileDeletedEvent(n * path(1) + '.txt') for n in range(1, 5001)]
|
|
|
|
file_events_test9 += [DirMovedEvent(n * path(2), n * path(3)) for n in range(1, 10001)]
|
|
|
|
file_events_test9 += [FileMovedEvent(n * path(2) + '.txt', n * path(3) + '.txt')
|
|
|
|
for n in range(1, 5001)]
|
|
|
|
file_events_test9 += [FileCreatedEvent(path(n)) for n in range(5, 5001)]
|
2020-03-15 02:16:25 +03:00
|
|
|
|
|
|
|
res9 = [
|
|
|
|
DirDeletedEvent(path(1)),
|
2020-03-15 22:32:16 +03:00
|
|
|
DirMovedEvent(path(2), path(3)),
|
|
|
|
FileDeletedEvent(path(1) + '.txt'),
|
|
|
|
FileMovedEvent(path(2) + '.txt', path(3) + '.txt'),
|
2020-03-15 02:16:25 +03:00
|
|
|
]
|
2020-03-26 16:54:10 +03:00
|
|
|
res9 += [FileCreatedEvent(path(n)) for n in range(5, 5001)]
|
2020-03-15 02:16:25 +03:00
|
|
|
|
2020-03-04 15:52:34 +03:00
|
|
|
sync = DummyUpDownSync()
|
|
|
|
|
|
|
|
cleaned_file_events_test0 = sync._clean_local_events(file_events_test0)
|
|
|
|
cleaned_file_events_test1 = sync._clean_local_events(file_events_test1)
|
|
|
|
cleaned_file_events_test2 = sync._clean_local_events(file_events_test2)
|
|
|
|
cleaned_file_events_test3 = sync._clean_local_events(file_events_test3)
|
|
|
|
cleaned_file_events_test4 = sync._clean_local_events(file_events_test4)
|
|
|
|
cleaned_file_events_test5 = sync._clean_local_events(file_events_test5)
|
2020-03-06 00:32:15 +03:00
|
|
|
cleaned_file_events_test6 = sync._clean_local_events(file_events_test6)
|
2020-03-14 15:56:14 +03:00
|
|
|
cleaned_file_events_test7 = sync._clean_local_events(file_events_test7)
|
|
|
|
cleaned_file_events_test8 = sync._clean_local_events(file_events_test8)
|
2020-03-15 02:29:23 +03:00
|
|
|
cleaned_file_events_test9 = sync._clean_local_events(file_events_test9)
|
2019-12-17 17:54:29 +03:00
|
|
|
|
|
|
|
assert set(cleaned_file_events_test0) == set(res0)
|
|
|
|
assert set(cleaned_file_events_test1) == set(res1)
|
|
|
|
assert set(cleaned_file_events_test2) == set(res2)
|
|
|
|
assert set(cleaned_file_events_test3) == set(res3)
|
|
|
|
assert set(cleaned_file_events_test4) == set(res4)
|
|
|
|
assert set(cleaned_file_events_test5) == set(res5)
|
2020-03-06 00:32:15 +03:00
|
|
|
assert set(cleaned_file_events_test6) == set(res6)
|
2020-03-14 15:56:14 +03:00
|
|
|
assert set(cleaned_file_events_test7) == set(res7)
|
|
|
|
assert set(cleaned_file_events_test8) == set(res8)
|
2020-03-15 02:29:23 +03:00
|
|
|
assert set(cleaned_file_events_test9) == set(res9)
|
2020-02-21 00:09:26 +03:00
|
|
|
|
2020-03-15 02:16:25 +03:00
|
|
|
n_loops = 4
|
2020-03-15 02:33:59 +03:00
|
|
|
duration = timeit.timeit(lambda: sync._clean_local_events(file_events_test9),
|
|
|
|
number=n_loops)
|
2020-03-15 02:16:25 +03:00
|
|
|
|
2020-03-15 22:32:16 +03:00
|
|
|
assert duration < 5 * n_loops # less than 5 sec per call
|
2020-03-15 02:16:25 +03:00
|
|
|
|
2020-02-22 19:08:19 +03:00
|
|
|
|
|
|
|
# Create a Dropbox test account to automate the below test.
|
|
|
|
# We will not test individual methods of `maestral.sync` but rather ensure
|
|
|
|
# an effective result: successful syncing and conflict resolution in
|
|
|
|
# standard and challenging cases.
|
2020-02-21 00:09:26 +03:00
|
|
|
|
|
|
|
def test_sync_cases():
|
2020-03-08 16:18:33 +03:00
|
|
|
# Currently those tests are performed manually with the following test cases:
|
|
|
|
#
|
2020-03-15 02:52:51 +03:00
|
|
|
# CC = conflicting copy
|
|
|
|
#
|
2020-04-16 23:02:37 +03:00
|
|
|
# * Remote file replaced with a folder (OK): Check ctime and create CC of local file
|
2020-03-06 02:45:29 +03:00
|
|
|
# if necessary.
|
|
|
|
# * Remote folder replaced with a file (OK):
|
|
|
|
# Recurse through ctimes of children and check if we have any un-synced changes.
|
|
|
|
# If yes, create CCs for those items. Others will be deleted.
|
2020-03-06 02:21:41 +03:00
|
|
|
# * Local file replaced with a folder (OK):
|
2020-04-18 21:52:36 +03:00
|
|
|
# Check if server-modified time > laest_sync of file and only delete file if older.
|
|
|
|
# Otherwise, let Dropbox handle creating a CC.
|
2020-03-06 02:21:41 +03:00
|
|
|
# * Local folder replaced with a file (NOK):
|
2020-03-15 02:52:51 +03:00
|
|
|
# Remote folder is currently not checked for unsynced changes but a CC is created
|
|
|
|
# by default. We could recurse through all remote files and check for unsynced
|
|
|
|
# changes.
|
2020-02-21 00:09:26 +03:00
|
|
|
# * Remote and local items modified during sync pause (OK)
|
|
|
|
# * Remote and local items created during sync pause (OK)
|
|
|
|
# * Remote and local items deleted during sync pause (OK)
|
|
|
|
# * Local item created / modified -> registered for upload -> deleted before up: (OK)
|
|
|
|
# Ok because FileNotFoundError will be caught silently.
|
|
|
|
# * Local item created / modified -> uploaded -> deleted before re-download: (OK)
|
|
|
|
# Will not be re-downloaded if rev is still in index.
|
|
|
|
# * Local item created / modified -> uploaded -> modified before re-download: (OK)
|
|
|
|
# Will not be re-downloaded if rev is the same.
|
|
|
|
# * Local item deleted -> uploaded -> created before re-download: (OK)
|
|
|
|
# Local rev == remote rev == None. Deletion will not be carried out.
|
|
|
|
# * Remote item created -> registered -> local item created before download (OK):
|
2020-03-06 02:45:29 +03:00
|
|
|
# Local rev is None but file exists => CC created.
|
2020-02-21 00:09:26 +03:00
|
|
|
# * Remote item deleted -> registered -> local item created before download (OK):
|
|
|
|
# Local rev == remote rev == None. Deletion will not be carried out.
|
|
|
|
# * Remote item deleted -> registered -> local item deleted before download (OK):
|
|
|
|
# Local rev exists: deletion will be carried out locally and fail silently.
|
2020-02-22 17:59:57 +03:00
|
|
|
# * Remote item deleted -> registered -> local item modified before download (OK):
|
2020-04-16 23:02:37 +03:00
|
|
|
# Local rev != remote rev (= None), different file contents. Compare ctime and
|
|
|
|
# keep local item if local ctime > last_sync.
|
2020-02-22 17:59:57 +03:00
|
|
|
# * Remote item modified -> registered -> local item modified before download (OK):
|
2020-03-06 02:45:29 +03:00
|
|
|
# Local rev != remote rev. Compare ctime and create CC if ctime > last_sync and
|
|
|
|
# file contents are different.
|
|
|
|
#
|
2020-02-21 00:09:26 +03:00
|
|
|
|
|
|
|
pass
|