Initial QSfera import
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import urllib.request
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.common.exceptions import TimeoutException
|
||||
|
||||
from pageObjects.SyncConnection import SyncConnection
|
||||
from helpers.ConfigHelper import get_config, is_linux, is_windows
|
||||
from helpers.FilesHelper import sanitize_path
|
||||
|
||||
if is_windows():
|
||||
from helpers.WinPipeHelper import WinPipeConnect as SocketConnect
|
||||
else:
|
||||
# NOTE: 'syncstate.py' was removed from client
|
||||
# and is now available at https://github.com/opencloud-eu/desktop-shell-integration-nautilus
|
||||
# check if 'syncstate.py' is available, if not, download it
|
||||
custom_lib = get_config('custom_lib')
|
||||
syncstate_lib_file = os.path.join(custom_lib, 'syncstate.py')
|
||||
os.makedirs(custom_lib, exist_ok=True)
|
||||
|
||||
if not os.path.exists(syncstate_lib_file):
|
||||
urllib.request.urlretrieve(
|
||||
'https://raw.githubusercontent.com/opencloud-eu/desktop-shell-integration-nautilus/refs/heads/main/src/syncstate.py',
|
||||
syncstate_lib_file,
|
||||
)
|
||||
# do not instantiate SocketConnect in the script.
|
||||
with open(syncstate_lib_file, 'r') as f:
|
||||
content = f.read()
|
||||
content = content.replace('socketConnect = SocketConnect()', '')
|
||||
content = content.replace(
|
||||
'from gi.repository import GObject, Nautilus',
|
||||
'import gi\n\ngi.require_version(\'Nautilus\', \'4.0\')\nfrom gi.repository import GObject, Nautilus',
|
||||
)
|
||||
|
||||
with open(syncstate_lib_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# the script needs to use the system-wide python
|
||||
# to switch from the built-in interpreter
|
||||
# see https://kb.froglogic.com/squish/howto/using-external-python-interpreter-squish-6-6/
|
||||
# if the IDE fails to reference the script,
|
||||
# add the folder in Edit->Preferences->PyDev->Interpreters->Libraries
|
||||
from helpers.custom_lib.syncstate import SocketConnect
|
||||
|
||||
# socket messages
|
||||
socket_messages = []
|
||||
SOCKET_CONNECT = None
|
||||
# Whether wait has been made or not after account is set up
|
||||
# This is useful for waiting only for the first time
|
||||
WAITED_AFTER_SYNC = False
|
||||
|
||||
# File syncing in client has the following status
|
||||
SYNC_STATUS = {
|
||||
'SYNC': 'STATUS:SYNC', # sync in progress
|
||||
'OK': 'STATUS:OK', # sync completed
|
||||
'OKAL': 'STATUS:OK+AL', # sync completed (Always Local)
|
||||
'OKOO': 'STATUS:OK+OO', # sync completed (Online Only)
|
||||
'ERROR': 'STATUS:ERROR', # sync error
|
||||
'IGNORE': 'STATUS:IGNORE', # sync ignored
|
||||
'NOP': 'STATUS:NOP', # not in sync yet
|
||||
'REGISTER': 'REGISTER_PATH',
|
||||
'UNREGISTER': 'UNREGISTER_PATH',
|
||||
'UPDATE': 'UPDATE_VIEW',
|
||||
}
|
||||
|
||||
SYNC_PATTERNS = {
|
||||
# default sync patterns for the initial sync (after adding account)
|
||||
# the pattern can be of TWO types depending on the available resources (files/folders)
|
||||
'initial': [
|
||||
# when adding account via New Account wizard
|
||||
[
|
||||
SYNC_STATUS['NOP'],
|
||||
SYNC_STATUS['REGISTER'],
|
||||
SYNC_STATUS['UPDATE'],
|
||||
],
|
||||
# when syncing empty account (hidden files are ignored)
|
||||
# [SYNC_STATUS['UPDATE'], SYNC_STATUS['OK']],
|
||||
# [SYNC_STATUS['UPDATE'], SYNC_STATUS['OKAL']],
|
||||
# when syncing an account that has some files/folders
|
||||
# [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']],
|
||||
# initial root sync
|
||||
[
|
||||
SYNC_STATUS['OK'],
|
||||
SYNC_STATUS['OK'],
|
||||
SYNC_STATUS['UPDATE'],
|
||||
],
|
||||
],
|
||||
'root_synced': [
|
||||
[
|
||||
SYNC_STATUS['OK'],
|
||||
SYNC_STATUS['OK'],
|
||||
SYNC_STATUS['UPDATE'],
|
||||
],
|
||||
# [
|
||||
# SYNC_STATUS['SYNC'],
|
||||
# SYNC_STATUS['OK'],
|
||||
# SYNC_STATUS['OK'],
|
||||
# SYNC_STATUS['OK'],
|
||||
# SYNC_STATUS['UPDATE'],
|
||||
# ],
|
||||
# [
|
||||
# SYNC_STATUS['SYNC'],
|
||||
# SYNC_STATUS['UPDATE'],
|
||||
# SYNC_STATUS['OK'],
|
||||
# SYNC_STATUS['OK'],
|
||||
# SYNC_STATUS['OK'],
|
||||
# SYNC_STATUS['UPDATE'],
|
||||
# ],
|
||||
# # used for local resource creation and deletion
|
||||
# [
|
||||
# SYNC_STATUS['OKAL'],
|
||||
# SYNC_STATUS['OK'],
|
||||
# SYNC_STATUS['OK'],
|
||||
# SYNC_STATUS['UPDATE'],
|
||||
# ],
|
||||
],
|
||||
'single_synced': [
|
||||
[SYNC_STATUS['SYNC'], SYNC_STATUS['OK']],
|
||||
# file/folder deletion
|
||||
[SYNC_STATUS['SYNC'], SYNC_STATUS['NOP']],
|
||||
],
|
||||
'error': [SYNC_STATUS['ERROR']],
|
||||
}
|
||||
|
||||
|
||||
def get_socket_connection():
|
||||
global SOCKET_CONNECT
|
||||
if not SOCKET_CONNECT or not SOCKET_CONNECT.connected:
|
||||
SOCKET_CONNECT = SocketConnect()
|
||||
return SOCKET_CONNECT
|
||||
|
||||
|
||||
def read_socket_messages():
|
||||
messages = []
|
||||
socket_connect = get_socket_connection()
|
||||
socket_connect.read_socket_data_with_timeout(0.1)
|
||||
for line in socket_connect.get_available_responses():
|
||||
messages.append(line)
|
||||
return messages
|
||||
|
||||
|
||||
def read_and_update_socket_messages():
|
||||
messages = read_socket_messages()
|
||||
return update_socket_messages(messages)
|
||||
|
||||
|
||||
def update_socket_messages(messages):
|
||||
socket_messages.extend(filter_sync_messages(messages))
|
||||
return socket_messages
|
||||
|
||||
|
||||
def clear_socket_messages(resource=''):
|
||||
global socket_messages
|
||||
if resource:
|
||||
resource_messages = set(filter_messages_for_item(socket_messages, resource))
|
||||
socket_messages = [
|
||||
msg for msg in socket_messages if msg not in resource_messages
|
||||
]
|
||||
else:
|
||||
socket_messages.clear()
|
||||
|
||||
|
||||
def close_socket_connection():
|
||||
socket_messages.clear()
|
||||
if SOCKET_CONNECT:
|
||||
SOCKET_CONNECT.connected = False
|
||||
if is_windows():
|
||||
SOCKET_CONNECT.close_conn()
|
||||
elif is_linux():
|
||||
SOCKET_CONNECT._sock.close() # pylint: disable=protected-access
|
||||
|
||||
|
||||
def get_initial_sync_patterns():
|
||||
return SYNC_PATTERNS['initial']
|
||||
|
||||
|
||||
def get_synced_pattern(resource=''):
|
||||
# get only the resource path
|
||||
sync_path = get_config('currentUserSyncPath')
|
||||
sync_path = os.path.join(sync_path, get_config('syncConnectionName'))
|
||||
|
||||
if resource := resource.replace(sync_path, '').strip('\\').strip('/'):
|
||||
return SYNC_PATTERNS['single_synced']
|
||||
return SYNC_PATTERNS['root_synced']
|
||||
|
||||
|
||||
# generate sync pattern from the socket messages
|
||||
#
|
||||
# returns List
|
||||
# e.g: ['UPDATE_VIEW', 'STATUS:OK']
|
||||
def generate_sync_pattern_from_messages(messages):
|
||||
pattern = []
|
||||
if not messages:
|
||||
return pattern
|
||||
|
||||
sync_messages = filter_sync_messages(messages)
|
||||
for message in sync_messages:
|
||||
# E.g; from;
|
||||
# Linux: "STATUS:OK:/tmp/client-bdd/Alice/"
|
||||
# Win: "STATUS:OK:C:\tmp\client-bdd\Alice\"
|
||||
# excludes ":/tmp/client-bdd/Alice/"
|
||||
# adds only "STATUS:OK" to the pattern list
|
||||
if match := re.search(r':(/|[A-Za-z]:[\\/]).*', message):
|
||||
end, _ = match.span()
|
||||
# shared resources will have status like "STATUS:OK+SWM"
|
||||
status = message[:end].replace('+SWM', '')
|
||||
pattern.append(status)
|
||||
return pattern
|
||||
|
||||
|
||||
# strip out the messages that are not related to sync
|
||||
def filter_sync_messages(messages):
|
||||
start_idx = 0
|
||||
if 'GET_STRINGS:END' in messages:
|
||||
start_idx = messages.index('GET_STRINGS:END') + 1
|
||||
return messages[start_idx:]
|
||||
|
||||
|
||||
def filter_messages_for_item(messages, item):
|
||||
filtered_messages = []
|
||||
for msg in messages:
|
||||
msg = msg.rstrip('/').rstrip('\\')
|
||||
item = item.rstrip('/').rstrip('\\')
|
||||
if msg.endswith(item):
|
||||
filtered_messages.append(msg)
|
||||
return filtered_messages
|
||||
|
||||
|
||||
def listen_sync_status_for_item(item, resource_type='FOLDER'):
|
||||
if (resource_type := resource_type.upper()) not in ('FILE', 'FOLDER'):
|
||||
raise ValueError('resource_type must be "FILE" or "FOLDER"')
|
||||
socket_connect = get_socket_connection()
|
||||
item = item.rstrip('\\').rstrip('/')
|
||||
socket_connect.sendCommand(f'RETRIEVE_{resource_type}_STATUS:{item}\n')
|
||||
|
||||
|
||||
def get_current_sync_status(resource, resource_type):
|
||||
listen_sync_status_for_item(resource, resource_type)
|
||||
messages = filter_messages_for_item(read_socket_messages(), resource)
|
||||
# return the last message from the list
|
||||
return messages[-1]
|
||||
|
||||
|
||||
def wait_for_resource_to_sync(
|
||||
resource, resource_type='FOLDER', patterns=None, force_sync=False
|
||||
):
|
||||
listen_sync_status_for_item(resource, resource_type)
|
||||
|
||||
initial_timeout = 0
|
||||
timeout = get_config('maxSyncTimeout') * 1000
|
||||
|
||||
if patterns is None:
|
||||
patterns = get_synced_pattern(resource)
|
||||
|
||||
if force_sync:
|
||||
initial_timeout = 5000
|
||||
# first try with 5 seconds timeout
|
||||
synced = wait_for(
|
||||
lambda: has_sync_pattern(patterns, resource),
|
||||
initial_timeout,
|
||||
)
|
||||
if not synced:
|
||||
# trigger force sync if the current status is OK
|
||||
status = get_current_sync_status(resource, resource_type)
|
||||
if status.startswith(SYNC_STATUS['OK']):
|
||||
print('[WARN] Retrying sync pattern check with force sync')
|
||||
SyncConnection.force_sync()
|
||||
else:
|
||||
clear_socket_messages(resource)
|
||||
return
|
||||
|
||||
synced = wait_for(
|
||||
lambda: has_sync_pattern(patterns, resource),
|
||||
timeout - initial_timeout,
|
||||
)
|
||||
|
||||
messages = read_and_update_socket_messages()
|
||||
messages = filter_messages_for_item(messages, resource)
|
||||
clear_socket_messages(resource)
|
||||
if synced:
|
||||
return
|
||||
elif not force_sync:
|
||||
# if the sync pattern doesn't match then check the last sync status
|
||||
# and pass the step if the last sync status is STATUS:OK
|
||||
status = get_current_sync_status(resource, resource_type)
|
||||
if status.startswith(SYNC_STATUS['OK']):
|
||||
print(
|
||||
'[WARN] Failed to match sync pattern for resource: '
|
||||
+ resource
|
||||
+ f'\nBut its last status is "{SYNC_STATUS["OK"]}"'
|
||||
+ '. So passing the step.'
|
||||
)
|
||||
return
|
||||
raise TimeoutError(
|
||||
'Timeout while waiting for sync to complete for '
|
||||
+ str(timeout)
|
||||
+ ' milliseconds'
|
||||
)
|
||||
|
||||
|
||||
def wait_for_initial_sync_to_complete(path):
|
||||
wait_for_resource_to_sync(
|
||||
path,
|
||||
'FOLDER',
|
||||
get_initial_sync_patterns(),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
def has_sync_pattern(patterns, resource=None):
|
||||
if isinstance(patterns[0], str):
|
||||
patterns = [patterns]
|
||||
messages = read_and_update_socket_messages()
|
||||
if resource:
|
||||
messages = filter_messages_for_item(messages, resource)
|
||||
for pattern in patterns:
|
||||
pattern_len = len(pattern)
|
||||
for idx, _ in enumerate(messages):
|
||||
actual_pattern = generate_sync_pattern_from_messages(
|
||||
messages[idx : idx + pattern_len]
|
||||
)
|
||||
if len(actual_pattern) < pattern_len:
|
||||
break
|
||||
if pattern_len == len(actual_pattern) and pattern == actual_pattern:
|
||||
print("MATCHED SYNC PATTERN:", pattern)
|
||||
return True
|
||||
# 100 milliseconds polling interval
|
||||
time.sleep(0.1)
|
||||
return False
|
||||
|
||||
|
||||
# Using socket API to check file sync status
|
||||
def has_sync_status(item_name, status):
|
||||
sync_messages = read_and_update_socket_messages()
|
||||
sync_messages = filter_messages_for_item(sync_messages, item_name)
|
||||
for line in sync_messages:
|
||||
line = line.rstrip('/').rstrip('\\')
|
||||
item_name = item_name.rstrip('/').rstrip('\\')
|
||||
if line.startswith(status) and line.endswith(item_name):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# useful for checking sync status such as 'error', 'ignore'
|
||||
# but not quite so reliable for checking 'ok' sync status
|
||||
def wait_for_resource_to_have_sync_status(
|
||||
resource, resource_type, status=SYNC_STATUS['OK'], timeout=None
|
||||
):
|
||||
resource = sanitize_path(resource)
|
||||
|
||||
listen_sync_status_for_item(resource, resource_type)
|
||||
|
||||
if not timeout:
|
||||
timeout = get_config('maxSyncTimeout') * 1000
|
||||
|
||||
result = wait_for(
|
||||
lambda: has_sync_status(resource, status),
|
||||
timeout,
|
||||
)
|
||||
|
||||
if not result:
|
||||
if status == SYNC_STATUS['ERROR']:
|
||||
expected = 'have sync error'
|
||||
elif status == SYNC_STATUS['IGNORE']:
|
||||
expected = 'be sync ignored'
|
||||
else:
|
||||
expected = 'be synced'
|
||||
raise ValueError(
|
||||
f'Expected {resource_type} "{resource}" to {expected}, but not.'
|
||||
)
|
||||
|
||||
|
||||
def wait_for_resource_to_have_sync_error(resource, resource_type):
|
||||
wait_for_resource_to_have_sync_status(resource, resource_type, SYNC_STATUS['ERROR'])
|
||||
|
||||
|
||||
# performing actions immediately after completing the sync from the server does not work
|
||||
# The test should wait for a while before performing the action
|
||||
# issue: https://github.com/owncloud/client/issues/8832
|
||||
def wait_for_client_to_be_ready():
|
||||
global WAITED_AFTER_SYNC
|
||||
if not WAITED_AFTER_SYNC:
|
||||
time.sleep(get_config('minSyncTimeout'))
|
||||
WAITED_AFTER_SYNC = True
|
||||
|
||||
|
||||
def clear_waited_after_sync():
|
||||
global WAITED_AFTER_SYNC
|
||||
WAITED_AFTER_SYNC = False
|
||||
|
||||
|
||||
def perform_file_explorer_vfs_action(resource_path, action):
|
||||
if action == 'Free up space':
|
||||
make_online_only(resource_path)
|
||||
elif action == 'Always keep on this device':
|
||||
make_available_locally(resource_path)
|
||||
else:
|
||||
raise ValueError(f'Invalid file explorer action: {action}')
|
||||
|
||||
|
||||
def make_online_only(resource_path):
|
||||
socket_connect = get_socket_connection()
|
||||
resource_path = resource_path.rstrip('\\').rstrip('/')
|
||||
socket_connect.sendCommand(f'MAKE_ONLINE_ONLY:{resource_path}\n')
|
||||
|
||||
|
||||
def make_available_locally(resource_path):
|
||||
socket_connect = get_socket_connection()
|
||||
resource_path = resource_path.rstrip('\\').rstrip('/')
|
||||
socket_connect.sendCommand(f'MAKE_AVAILABLE_LOCALLY:{resource_path}\n')
|
||||
|
||||
|
||||
def wait_for(condition, timeout, interval=0.5):
|
||||
from helpers.AppHelper import app
|
||||
|
||||
wait = WebDriverWait(app(), timeout / 1000, poll_frequency=interval)
|
||||
try:
|
||||
wait.until(lambda _: condition())
|
||||
return True
|
||||
except TimeoutException:
|
||||
return False
|
||||
Reference in New Issue
Block a user