Skip to content

drop the gui bus upload of resources #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Nov 5, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -5,17 +5,6 @@ GUI messagebus service, manages GUI state and implements the [gui protocol](./pr
GUI clients (the application that actually draws the GUI) connect to this service


# Plugins

plugins provide platform specific GUI functionality, such as determining when to show a homescreen or close a window

you should usually not need any of these unless instructed to install it from a GUI client application

- https://github.com/OpenVoiceOS/ovos-gui-plugin-shell-companion
- https://github.com/OpenVoiceOS/ovos-gui-plugin-mobile
- https://github.com/OpenVoiceOS/ovos-gui-plugin-plasmoid
- https://github.com/OpenVoiceOS/ovos-gui-plugin-bigscreen

# Configuration

under mycroft.conf
@@ -36,18 +25,8 @@ under mycroft.conf
"homescreen_supported": false
},

// Optional file server support for remote clients
// "gui_file_server": true,
// "file_server_port": 8000,

// Optional support for collecting GUI files for container support
// The ovos-gui container path for these files will be {XDG_CACHE_HOME}/ovos_gui_file_server.
// With the below configuration, the GUI client will have files prefixed with the configured host path,
// so the example below describes a situation where `{XDG_CACHE_HOME}/ovos_gui_file_server` maps
// to `/tmp/gui_files` on the filesystem where the GUI client is running.
// "gui_file_host_path": "/tmp/gui_files",

// Optionally specify a default qt version for connected clients that don't report it
// NOTE: currently only QT5 clients exist
"default_qt_version": 5
},

@@ -60,3 +39,26 @@ under mycroft.conf
}
}
```

# Plugins

plugins provide platform specific GUI functionality, such as determining when to show a homescreen or close a window

you should usually not need any of these unless instructed to install it from a GUI client application

- https://github.com/OpenVoiceOS/ovos-gui-plugin-shell-companion
- https://github.com/OpenVoiceOS/ovos-gui-plugin-mobile
- https://github.com/OpenVoiceOS/ovos-gui-plugin-plasmoid
- https://github.com/OpenVoiceOS/ovos-gui-plugin-bigscreen


# Limitations

gui resources files are populated under `~/.cache/mycrot/ovos-gui` by skills and other OVOS components and are expectd to be accessible by GUI client applications

This means GUI clients are expected to be running under the same machine or implement their own access to the resource files (resolving page names to uris is the client app responsibility)

> TODO: new repository with the removed GUI file server, serve files from `~/.cache/mycrot/ovos-gui` to be handled by client apps
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NeonDaniel not sure if you had a specific use case in mind, but please see this note

In case of containers a shared volume should be mounted between ovos-gui, skills and gui client apps

30 changes: 7 additions & 23 deletions ovos_gui/bus.py
Original file line number Diff line number Diff line change
@@ -124,22 +124,6 @@ def on_close(self):
LOG.info('Closing {}'.format(id(self)))
GUIWebsocketHandler.clients.remove(self)

def get_client_pages(self, namespace):
"""
Get a list of client page URLs for the given namespace
@param namespace: Namespace to get pages for
@return: list of page URIs for this GUI Client
"""
client_pages = []
server_url = self.ns_manager.gui_file_server.url if \
self.ns_manager.gui_file_server else \
self.ns_manager.gui_file_host_path
for page in namespace.pages:
uri = page.get_uri(self.framework, server_url)
client_pages.append(uri)

return client_pages

def synchronize(self):
"""
Upload namespaces, pages and data to the last connected client.
@@ -155,11 +139,13 @@ def synchronize(self):
"data": [{"skill_id": namespace.skill_id}]
})
# Insert pages
# if uri (path) can not be resolved, it might exist client side
# if path doesn't exist in client side, client is responsible for resolving page by namespace/name
self.send({"type": "mycroft.gui.list.insert",
"namespace": namespace.skill_id,
"position": 0,
"data": [{"url": url} for url in
self.get_client_pages(namespace)]
"data": [{"url": page.get_uri(self.framework), "page": page.name}
for page in namespace.pages]
})
# Insert data
for key, value in namespace.data.items():
@@ -260,16 +246,14 @@ def send_gui_pages(self, pages: List[GuiPage], namespace: str,
@param namespace: namespace to put GuiPages in
@param position: position to insert pages at
"""
server_url = self.ns_manager.gui_file_server.url if \
self.ns_manager.gui_file_server else \
self.ns_manager.gui_file_host_path
framework = self.framework

# if uri (path) can not be resolved, it might exist client side
# if path doesn't exist in client side, client is responsible for resolving page by namespace/name
message = {
"type": "mycroft.gui.list.insert",
"namespace": namespace,
"position": position,
"data": [{"url": page.get_uri(framework, server_url)}
"data": [{"url": page.get_uri(framework), "page": page.name}
for page in pages]
}
LOG.debug(f"Showing pages: {message['data']}")
4 changes: 4 additions & 0 deletions ovos_gui/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ovos_config.locations import get_xdg_cache_save_path

GUI_CACHE_PATH = get_xdg_cache_save_path('ovos_gui')

58 changes: 0 additions & 58 deletions ovos_gui/gui_file_server.py

This file was deleted.

195 changes: 21 additions & 174 deletions ovos_gui/namespace.py
Original file line number Diff line number Diff line change
@@ -40,13 +40,12 @@
over the GUI message bus.
"""
import shutil
from os import makedirs
from os.path import join, dirname, isfile, exists
from threading import Event, Lock, Timer
from typing import List, Union, Optional, Dict

from ovos_config.config import Configuration
from ovos_utils.log import LOG, log_deprecation
from ovos_utils.log import LOG

from ovos_bus_client import Message, MessageBusClient
from ovos_gui.bus import (
@@ -55,9 +54,8 @@
get_gui_websocket_config,
send_message_to_gui, GUIWebsocketHandler
)
from ovos_gui.gui_file_server import start_gui_http_server
from ovos_gui.page import GuiPage

from ovos_gui.constants import GUI_CACHE_PATH
namespace_lock = Lock()

RESERVED_KEYS = ['__from', '__idle']
@@ -72,9 +70,9 @@ def _validate_page_message(message: Message) -> bool:
@returns: True if request is valid, else False
"""
valid = (
"page" in message.data
"page_names" in message.data
and "__from" in message.data
and isinstance(message.data["page"], list)
and isinstance(message.data["page_names"], list)
)
if not valid:
if message.msg_type == "gui.page.show":
@@ -296,7 +294,7 @@ def load_pages(self, pages: List[GuiPage], show_index: int = 0):
target_page = pages[show_index]

for page in pages:
if page.id not in [p.id for p in self.pages]:
if page.name not in [p.name for p in self.pages]:
new_pages.append(page)

self.pages.extend(new_pages)
@@ -335,7 +333,7 @@ def focus_page(self, page):
# set the index of the page in the self.pages list
page_index = None
for i, p in enumerate(self.pages):
if p.id == page.id:
if p.name == page.name:
# save page index
page_index = i
break
@@ -431,39 +429,16 @@ def __init__(self, core_bus: MessageBusClient):
self.idle_display_skill = _get_idle_display_config()
self.active_extension = _get_active_gui_extension()
self._system_res_dir = join(dirname(__file__), "res", "gui")
self._ready_event = Event()
self.gui_file_server = None
self.gui_file_path = None # HTTP Server local path
self.gui_file_host_path = None # Docker host path
self._connected_frameworks: List[str] = list()
self._init_gui_file_share()
self._define_message_handlers()

@property
def _active_homescreen(self) -> str:
return Configuration().get('gui', {}).get('idle_display_skill')

def _init_gui_file_share(self):
"""
Initialize optional GUI file collection. if `gui_file_path` is
defined, resources are assumed to be referenced outside this container.
If `gui_file_server` is defined, resources will be served via HTTP
"""
config = Configuration().get("gui", {})
self.gui_file_host_path = config.get("gui_file_host_path")

# Check for GUI file sharing via HTTP server or mounted host path
if config.get("gui_file_server") or self.gui_file_host_path:
from ovos_utils.xdg_utils import xdg_cache_home
if config.get("server_path"):
log_deprecation("`server_path` configuration is deprecated. "
"Files will always be saved to "
"XDG_CACHE_HOME/ovos_gui_file_server", "0.1.0")
self.gui_file_path = config.get("server_path") or \
join(xdg_cache_home(), "ovos_gui_file_server")
if config.get("gui_file_server"):
self.gui_file_server = start_gui_http_server(self.gui_file_path)
self._upload_system_resources()
self._cache_system_resources()

def _define_message_handlers(self):
"""
@@ -474,88 +449,12 @@ def _define_message_handlers(self):
self.core_bus.on("gui.page.delete", self.handle_delete_page)
self.core_bus.on("gui.page.delete.all", self.handle_delete_all_pages)
self.core_bus.on("gui.page.show", self.handle_show_page)
self.core_bus.on("gui.page.upload", self.handle_receive_gui_pages)
self.core_bus.on("gui.status.request", self.handle_status_request)
self.core_bus.on("gui.value.set", self.handle_set_value)
self.core_bus.on("mycroft.gui.connected", self.handle_client_connected)
self.core_bus.on("gui.page_interaction", self.handle_page_interaction)
self.core_bus.on("gui.page_gained_focus", self.handle_page_gained_focus)
self.core_bus.on("mycroft.gui.screen.close", self.handle_namespace_global_back)
self.core_bus.on("gui.volunteer_page_upload", self.handle_gui_pages_available)

# TODO - deprecate this, only needed for gui bus upload
# Bus is connected, check if the skills service is ready
resp = self.core_bus.wait_for_response(
Message("mycroft.skills.is_ready",
context={"source": "gui", "destination": ["skills"]}))
if resp and resp.data.get("status"):
LOG.debug("Skills service already running")
self._ready_event.set()
else:
self.core_bus.on("mycroft.skills.trained", self.handle_ready)

def handle_ready(self, message):
self._ready_event.set()

def handle_gui_pages_available(self, message: Message):
"""
Handle a skill or plugin advertising that it has GUI pages available to
upload. If there are connected clients, request pages for each connected
GUI framework.
@param message: `gui.volunteer_page_upload` message
"""
if not any((self.gui_file_host_path, self.gui_file_server)):
LOG.debug("No GUI file server running or host path configured")
return

LOG.debug(f"Requesting resources for {self._connected_frameworks}")
for framework in self._connected_frameworks:
skill_id = message.data.get("skill_id")
self.core_bus.emit(message.reply("gui.request_page_upload",
{'skill_id': skill_id,
'framework': framework},
{"source": "gui",
"destination": ["skills",
"PHAL"]}))

def handle_receive_gui_pages(self, message: Message):
"""
Handle GUI resources from a skill or plugin. Pages are written to
`self.server_path` which is accessible via a lightweight HTTP server and
may additionally be mounted to a host path/volume in container setups.
@param message: Message containing UI resource file contents and meta
message.data:
pages: dict page_filename to encoded bytes content;
paths are relative to the `framework` directory, so a page
for framework `all` could be `qt5/subdir/file.qml` and the
equivalent page for framework `qt5` would be
`subdir/file.qml`
framework: `all` if all GUI resources are included, else the
specific GUI framework (i.e. `qt5`, `qt6`)
__from: skill_id of module uploading GUI resources
"""
for page, contents in message.data["pages"].items():
try:
if message.data.get("framework") == "all":
# All GUI resources are uploaded
resource_base_path = join(self.gui_file_path,
message.data['__from'])
else:
resource_base_path = join(self.gui_file_path,
message.data['__from'],
message.data.get('framework') or
"qt5")
byte_contents = bytes.fromhex(contents)
file_path = join(resource_base_path, page)
LOG.debug(f"writing UI file: {file_path}")
makedirs(dirname(file_path), exist_ok=True)
with open(file_path, 'wb+') as f:
f.write(byte_contents)
except Exception as e:
LOG.exception(f"Failed to write {page}: {e}")
if message.data["__from"] == self._active_homescreen:
# Configured home screen skill just uploaded pages, show it again
self.core_bus.emit(message.forward("homescreen.manager.show_active"))

def handle_clear_namespace(self, message: Message):
"""
@@ -621,8 +520,7 @@ def handle_delete_page(self, message: Message):
message_is_valid = _validate_page_message(message)
if message_is_valid:
namespace_name = message.data["__from"]
pages_to_remove = message.data.get("page_names") or \
message.data.get("page") # backwards compat
pages_to_remove = message.data.get("page_names")
LOG.debug(f"Got {namespace_name} request to delete: {pages_to_remove}")
with namespace_lock:
self._remove_pages(namespace_name, pages_to_remove)
@@ -665,24 +563,6 @@ def _parse_persistence(persistence: Optional[Union[int, bool]]) -> \
# Defines default behavior as displaying for 30 seconds
return False, 30

def _legacy_show_page(self, message: Message) -> List[GuiPage]:
"""
Backwards-compat method to handle messages without ui_directories and
page_names.
@param message: message requesting to display pages
@return: list of GuiPage objects
"""
pages_to_show = message.data["page"]
LOG.info(f"Handling legacy page show request. pages={pages_to_show}")

pages_to_load = list()
persist, duration = self._parse_persistence(message.data["__idle"])
for page in pages_to_show:
name = page.split('/')[-1]
# check if persistence is type of int or bool
pages_to_load.append(GuiPage(page, name, persist, duration))
return pages_to_load

def handle_show_page(self, message: Message):
"""
Handles a request to show one or more pages on the screen.
@@ -695,39 +575,19 @@ def handle_show_page(self, message: Message):

namespace_name = message.data["__from"]
page_ids_to_show = message.data.get('page_names')
page_resource_dirs = message.data.get('ui_directories')
persistence = message.data["__idle"]
show_index = message.data.get("index", None)
show_index = message.data.get("index", 0)

LOG.debug(f"Got {namespace_name} request to show: {page_ids_to_show} at index: {show_index}")

if not page_resource_dirs and page_ids_to_show and \
all((x.startswith("SYSTEM") for x in page_ids_to_show)):
page_resource_dirs = {"all": self._system_res_dir}

if not all((page_ids_to_show, page_resource_dirs)):
LOG.warning(f"GUI resources have not yet been uploaded for namespace: {namespace_name}")
pages = self._legacy_show_page(message)
else:
pages = list()
persist, duration = self._parse_persistence(message.data["__idle"])
for page in page_ids_to_show:
url = None
name = page
if isfile(page):
LOG.warning(f"Expected resource name but got file: {url}")
name = page.split('/')[-1]
url = f"file://{page}"
elif "://" in page:
LOG.warning(f"Expected resource name but got URI: {page}")
name = page.split('/')[-1]
url = page
pages.append(GuiPage(url, name, persist, duration,
page, namespace_name, page_resource_dirs))
pages = list()
persist, duration = self._parse_persistence(message.data["__idle"])
for page in page_ids_to_show:
pages.append(GuiPage(name=page, persistent=persist, duration=duration,
namespace=namespace_name))

if not pages:
LOG.error(f"Activated namespace '{namespace_name}' has no pages! "
f"Did you provide 'ui_directories' ?")
LOG.error(f"Activated namespace '{namespace_name}' has no pages!")
LOG.error(f"Can't show page, bad message: {message.data}")
return

@@ -961,23 +821,9 @@ def handle_client_connected(self, message: Message):
websocket_config = get_gui_websocket_config()
port = websocket_config["base_port"]
message = message.forward("mycroft.gui.port",
dict(port=port, gui_id=gui_id))
dict(port=port, gui_id=gui_id, framework=framework))
self.core_bus.emit(message)

if self.gui_file_path or self.gui_file_host_path:
if not self._ready_event.wait(90):
LOG.warning("Not reported ready after 90s")
if framework not in self._connected_frameworks:
LOG.debug(f"Requesting page upload for {framework}")
self.core_bus.emit(Message("gui.request_page_upload",
{'framework': framework},
{"source": "gui",
"destination": ["skills", "PHAL"]}))

if framework not in self._connected_frameworks:
LOG.debug(f"Connecting framework: {framework}")
self._connected_frameworks.append(framework)

def handle_page_interaction(self, message: Message):
"""
Handles an event from the GUI indicating a page has been interacted with.
@@ -1025,7 +871,8 @@ def handle_namespace_global_back(self, message: Optional[Message]):
@param message: the event sent by the GUI
"""
if not self.active_namespaces:
LOG.error("received 'back' signal but there are no active namespaces")
LOG.debug("received 'back' signal but there are no active namespaces, attempting to show homescreen")
self.core_bus.emit(Message("homescreen.manager.show_active"))
return

namespace_name = self.active_namespaces[0].skill_id
@@ -1046,13 +893,13 @@ def _del_namespace_in_remove_timers(self, namespace_name: str):
if namespace_name in self.remove_namespace_timers:
del self.remove_namespace_timers[namespace_name]

def _upload_system_resources(self):
def _cache_system_resources(self):
"""
Copy system GUI resources to the served file path
"""
output_path = join(self.gui_file_path, "system")
output_path = f"{GUI_CACHE_PATH}/system"
if exists(output_path):
LOG.info(f"Removing existing system resources before updating")
shutil.rmtree(output_path)
shutil.copytree(self._system_res_dir, output_path)
LOG.debug(f"Copied system resources to {self.gui_file_path}")
LOG.debug(f"Copied system resources from {self._system_res_dir} to {output_path}")
65 changes: 15 additions & 50 deletions ovos_gui/page.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
from typing import Union, Optional
from dataclasses import dataclass
from ovos_utils.log import LOG
from ovos_gui.constants import GUI_CACHE_PATH


@dataclass
@@ -10,28 +11,15 @@ class GuiPage:
A GuiPage represents a single GUI Display within a given namespace.
A Page can either be `persistent` or be removed after some `duration`.
Note that a page is generally framework-independent
@param url: URI (local or network path) of the GUI Page
@param name: Name of the page as shown in its namespace (could
@param persistent: If True, page is displayed indefinitely
@param duration: Number of seconds to display the page for
@param namespace: Skill/component identifier
@param page_id: Page identifier
(file path relative to gui_framework directory with no extension)
"""
url: Optional[str] # This param is left for backwards-compat.
name: str
persistent: bool
duration: Union[int, bool]
page_id: Optional[str] = None
namespace: Optional[str] = None
resource_dirs: Optional[dict] = None

@property
def id(self):
"""
Get a unique identifier for this page.
"""
return self.page_id or self.url

@staticmethod
def get_file_extension(framework: str) -> str:
@@ -44,45 +32,22 @@ def get_file_extension(framework: str) -> str:
return "qml"
return ""

def get_uri(self, framework: str = "qt5", server_url: str = None) -> str:
@property
def res_namespace(self):
return "system" if self.name.startswith("SYSTEM") else self.namespace

def get_uri(self, framework: str = "qt5") -> Optional[str]:
"""
Get a valid URI for this Page.
@param framework: String GUI framework to get resources for
@param server_url: String server URL if available; this could be for a
web server (http://), or a container host path (file://)
@param framework: String GUI framework to get resources for (currently only 'qt5')
@return: Absolute path to the requested resource
"""
if self.url:
LOG.warning(f"Static URI: {self.url}")
return self.url

res_filename = f"{self.page_id}.{self.get_file_extension(framework)}"
res_namespace = "system" if self.page_id.startswith("SYSTEM") else \
self.namespace
if server_url:
if "://" not in server_url:
if server_url.startswith("/"):
LOG.debug(f"No schema in server_url, assuming 'file'")
server_url = f"file://{server_url}"
else:
LOG.debug(f"No schema in server_url, assuming 'http'")
server_url = f"http://{server_url}"
path = f"{server_url}/{res_namespace}/{framework}/{res_filename}"
LOG.info(f"Resolved server URI: {path}")
res_filename = f"{self.name}.{self.get_file_extension(framework)}"
path = f"{GUI_CACHE_PATH}/{self.res_namespace}/{framework}/{res_filename}"
LOG.debug(f"Resolved page URI: {path}")
if isfile(path):
return path
base_path = self.resource_dirs.get(framework)
if not base_path and self.resource_dirs.get("all"):
file_path = join(self.resource_dirs.get('all'), framework,
res_filename)
else:
file_path = join(base_path, res_filename)
if isfile(file_path):
return file_path
# Check system resources
file_path = join(dirname(__file__), "res", "gui", framework,
res_filename)
if isfile(file_path):
return file_path
raise FileNotFoundError(f"Unable to resolve resource file for "
f"resource {res_filename} for framework "
f"{framework}")
LOG.warning(f"Unable to resolve resource file for "
f"resource {res_filename} for framework "
f"{framework}")
return None
60 changes: 0 additions & 60 deletions ovos_gui/res/gui/qt5/SYSTEM_AdditionalSettings.qml

This file was deleted.

6 changes: 5 additions & 1 deletion protocol.md
Original file line number Diff line number Diff line change
@@ -61,13 +61,17 @@ Each active skill is associated with a list of uris to the QML files of all gui

Non QT GUIS get sent other file extensions such as .jsx or .html using the same message format

If a gui resource can not be resolved to a url (*url* may be `None`!), it might still exist client side, it is the clients responsibility to handle the namespace/page in that case

> eg, a client could map namespaces/page to a remote http server url
## Insert new page at position
```javascript
{
"type": "mycroft.gui.list.insert",
"namespace": "mycroft.weather"
"position": 2
"values": [{"url": "file://..../currentWeather.qml"}, ...] //values must always be in array form
"values": [{"url": "file://..../currentWeather.qml", "page": "currentWeather"}, ...] //values must always be in array form
}
```

2 changes: 1 addition & 1 deletion requirements/extras.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ovos-gui-plugin-shell-companion>=1.0.0,<2.0.0
ovos-gui-plugin-shell-companion>=1.0.1,<2.0.0
4 changes: 2 additions & 2 deletions requirements/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ovos_bus_client>=0.0.7,<1.0.0
ovos_bus_client>=1.0.0,<2.0.0
ovos-utils>=0.0.37,<1.0.0
ovos-config>=0.0.12,<1.0.0
tornado~=6.0, >=6.0.3
ovos-plugin-manager>=0.0.24,<1.0.0
ovos-plugin-manager>=0.5.5,<1.0.0
72 changes: 25 additions & 47 deletions test/unittests/test_bus.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest
from unittest.mock import patch, Mock

from typing import List
import ovos_gui.bus


@@ -80,6 +80,19 @@ def test_on_close(self):
# TODO
pass

def _get_client_pages(self, namespace) -> List[str]:
"""
Get a list of client page URLs for the given namespace
@param namespace: Namespace to get pages for
@return: list of page URIs for this GUI Client
"""
client_pages = []
for page in namespace.pages:
# NOTE: in here page is resolved to a full URI (path)
uri = page.get_uri("qt5")
client_pages.append(uri)
return client_pages

def test_get_client_pages(self):
from ovos_gui.namespace import Namespace
test_namespace = Namespace("test")
@@ -89,31 +102,11 @@ def test_get_client_pages(self):
page_2.get_uri.return_value = "page_2_uri"
test_namespace.pages = [page_1, page_2]

# Specify no host path mapping
self.handler.ns_manager.gui_file_host_path = None

# Test no server_url
self.handler.ns_manager.gui_file_server = None
pages = self.handler.get_client_pages(test_namespace)
page_1.get_uri.assert_called_once_with(self.handler.framework, None)
page_2.get_uri.assert_called_once_with(self.handler.framework, None)
pages = self._get_client_pages(test_namespace)
page_1.get_uri.assert_called_once_with(self.handler.framework)
page_2.get_uri.assert_called_once_with(self.handler.framework)
self.assertEqual(pages, ["page_1_uri", "page_2_uri"])

# Test host path mapping
test_path = "/test/ovos-gui-file-server"
self.handler.ns_manager.gui_file_host_path = test_path
pages = self.handler.get_client_pages(test_namespace)
page_1.get_uri.assert_called_with(self.handler.framework, test_path)
page_2.get_uri.assert_called_with(self.handler.framework, test_path)
self.assertEqual(pages, ["page_1_uri", "page_2_uri"])

# Test with server_url
self.handler.ns_manager.gui_file_server = Mock()
self.handler.ns_manager.gui_file_server.url = "server_url"
pages = self.handler.get_client_pages(test_namespace)
page_1.get_uri.assert_called_with(self.handler.framework, "server_url")
page_2.get_uri.assert_called_with(self.handler.framework, "server_url")
self.assertEqual(pages, ["page_1_uri", "page_2_uri"])

def test_synchronize(self):
# TODO
@@ -134,47 +127,32 @@ def test_send_gui_pages(self):
test_pos = 0

from ovos_gui.page import GuiPage
page_1 = GuiPage(None, "", False, False)
page_1 = GuiPage("p1", "", False, False)
page_1.get_uri = Mock(return_value="page_1")

page_2 = GuiPage(None, "", False, False)
page_2 = GuiPage("p2", "", False, False)
page_2.get_uri = Mock(return_value="page_2")

# Specify no host path mapping
self.handler.ns_manager.gui_file_host_path = None

# Test no server_url
self.handler.ns_manager.gui_file_server = None
self.handler._framework = "qt5"
self.handler.send_gui_pages([page_1, page_2], test_ns, test_pos)
page_1.get_uri.assert_called_once_with("qt5", None)
page_2.get_uri.assert_called_once_with("qt5", None)
page_1.get_uri.assert_called_once_with("qt5")
page_2.get_uri.assert_called_once_with("qt5")
self.handler.send.assert_called_once_with(
{"type": "mycroft.gui.list.insert",
"namespace": test_ns,
"position": test_pos,
"data": [{"url": "page_1"}, {"url": "page_2"}]})

# Test host path mapping
test_path = "/test/ovos-gui-file-server"
self.handler.ns_manager.gui_file_host_path = test_path
self.handler.send_gui_pages([page_1, page_2], test_ns, test_pos)
page_1.get_uri.assert_called_with(self.handler.framework, test_path)
page_2.get_uri.assert_called_with(self.handler.framework, test_path)
"data": [{"url": "page_1", "page": "p1"}, {"url": "page_2", "page": "p2"}]})

# Test with server_url
self.handler.ns_manager.gui_file_server = Mock()
self.handler.ns_manager.gui_file_server.url = "server_url"
self.handler._framework = "qt6"
test_pos = 3
self.handler.send_gui_pages([page_2, page_1], test_ns, test_pos)
page_1.get_uri.assert_called_with("qt6", "server_url")
page_2.get_uri.assert_called_with("qt6", "server_url")
page_1.get_uri.assert_called_with("qt6")
page_2.get_uri.assert_called_with("qt6")
self.handler.send.assert_called_with(
{"type": "mycroft.gui.list.insert",
"namespace": test_ns,
"position": test_pos,
"data": [{"url": "page_2"}, {"url": "page_1"}]})
"data": [{"url": "page_2", "page": "p2"}, {"url": "page_1", "page": "p1"}]})

self.handler.send = real_send

15 changes: 0 additions & 15 deletions test/unittests/test_gui_file_server.py

This file was deleted.

98 changes: 34 additions & 64 deletions test/unittests/test_namespace.py
Original file line number Diff line number Diff line change
@@ -13,15 +13,15 @@
# limitations under the License.
#
"""Tests for the GUI namespace helper class."""
from os import makedirs
from os.path import join, dirname, isdir, isfile
from os.path import join, isdir, isfile
from shutil import rmtree
from unittest import TestCase, mock
from unittest.mock import Mock

from ovos_bus_client.message import Message
from ovos_utils.messagebus import FakeBus

from ovos_gui.constants import GUI_CACHE_PATH
from ovos_gui.namespace import Namespace
from ovos_gui.page import GuiPage

@@ -64,11 +64,11 @@ def test_add(self):

def test_activate(self):
self.namespace.load_pages([
GuiPage(name="foo", url="", persistent=False, duration=False),
GuiPage(name="bar", url="", persistent=False, duration=False),
GuiPage(name="foobar", url="", persistent=False, duration=False),
GuiPage(name="baz", url="", persistent=False, duration=False),
GuiPage(name="foobaz", url="", persistent=False, duration=False)
GuiPage(name="foo", persistent=False, duration=False),
GuiPage(name="bar", persistent=False, duration=False),
GuiPage(name="foobar", persistent=False, duration=False),
GuiPage(name="baz", persistent=False, duration=False),
GuiPage(name="foobaz", persistent=False, duration=False)
])
activate_namespace_message = {
"type": "mycroft.session.list.move",
@@ -129,9 +129,9 @@ def test_set_persistence_boolean(self):
self.assertTrue(self.namespace.persistent)

def test_load_pages_new(self):
self.namespace.pages = [GuiPage(name="foo", url="foo.qml", persistent=True, duration=0),
GuiPage(name="bar", url="bar.qml", persistent=False, duration=30)]
new_pages = [GuiPage(name="foobar", url="foobar.qml", persistent=False, duration=30)]
self.namespace.pages = [GuiPage(name="foo", persistent=True, duration=0),
GuiPage(name="bar", persistent=False, duration=30)]
new_pages = [GuiPage(name="foobar", persistent=False, duration=30)]
load_page_message = dict(
type="mycroft.events.triggered",
namespace="foo",
@@ -146,9 +146,9 @@ def test_load_pages_new(self):
self.assertListEqual(self.namespace.pages, self.namespace.pages)

def test_load_pages_existing(self):
self.namespace.pages = [GuiPage(name="foo", url="foo.qml", persistent=True, duration=0),
GuiPage(name="bar", url="bar.qml", persistent=False, duration=30)]
new_pages = [GuiPage(name="foo", url="foo.qml", persistent=True, duration=0)]
self.namespace.pages = [GuiPage(name="foo", persistent=True, duration=0),
GuiPage(name="bar", persistent=False, duration=30)]
new_pages = [GuiPage(name="foo", persistent=True, duration=0)]
load_page_message = dict(
type="mycroft.events.triggered",
namespace="foo",
@@ -171,9 +171,9 @@ def test_activate_page(self):
pass

def test_remove_pages(self):
self.namespace.pages = [GuiPage(name="foo", url="", persistent=False, duration=False),
GuiPage(name="bar", url="", persistent=False, duration=False),
GuiPage(name="foobar", url="", persistent=False, duration=False)]
self.namespace.pages = [GuiPage(name="foo", persistent=False, duration=False),
GuiPage(name="bar", persistent=False, duration=False),
GuiPage(name="foobar", persistent=False, duration=False)]
remove_page_message = dict(
type="mycroft.gui.list.remove",
namespace="foo",
@@ -217,18 +217,6 @@ def setUp(self):
with mock.patch(PATCH_MODULE + ".create_gui_service"):
self.namespace_manager = NamespaceManager(FakeBus())

def test_init_gui_file_share(self):
# TODO
pass

def test_handle_gui_pages_available(self):
# TODO
pass

def test_handle_receive_gui_pages(self):
# TODO
pass

def test_handle_clear_namespace_active(self):
namespace = Namespace("foo")
namespace.remove = mock.Mock()
@@ -264,12 +252,12 @@ def test_handle_send_event(self):

def test_handle_delete_page_active_namespace(self):
namespace = Namespace("foo")
namespace.pages = [GuiPage(name="bar", url="bar.qml", persistent=True, duration=0)]
namespace.pages = [GuiPage(name="bar", persistent=True, duration=0)]
namespace.remove_pages = mock.Mock()
self.namespace_manager.loaded_namespaces = dict(foo=namespace)
self.namespace_manager.active_namespaces = [namespace]

message_data = {"__from": "foo", "page": ["bar"]}
message_data = {"__from": "foo", "page_names": ["bar"]}
message = Message("gui.clear.namespace", data=message_data)
self.namespace_manager.handle_delete_page(message)
namespace.remove_pages.assert_called_with([0])
@@ -302,32 +290,23 @@ def test_parse_persistence(self):
with self.assertRaises(ValueError):
self.namespace_manager._parse_persistence(-10)

def test_legacy_show_page(self):
message = Message("gui.page.show", data={"__from": "foo",
"__idle": 10,
"page": ["bar", "test/baz"]})
pages = self.namespace_manager._legacy_show_page(message)
self.assertEqual(pages, [GuiPage('bar', 'bar', False, 10),
GuiPage('test/baz', 'baz', False, 10)])

def test_handle_show_page(self):
real_legacy_show_page = self.namespace_manager._legacy_show_page
real_activate_namespace = self.namespace_manager._activate_namespace
real_load_pages = self.namespace_manager._load_pages
real_update_persistence = self.namespace_manager._update_namespace_persistence
self.namespace_manager._legacy_show_page = Mock(return_value=["pages"])
self.namespace_manager._activate_namespace = Mock()
self.namespace_manager._load_pages = Mock()
self.namespace_manager._update_namespace_persistence = Mock()

# Legacy message
message = Message("gui.page.show", data={"__from": "foo",
"__idle": 10,
"page": ["bar", "test/baz"]})
"page_names": ["bar", "test/baz"]})
self.namespace_manager.handle_show_page(message)
self.namespace_manager._legacy_show_page.assert_called_once_with(message)
self.namespace_manager._activate_namespace.assert_called_with("foo")
self.namespace_manager._load_pages.assert_called_with(["pages"], None)
self.namespace_manager._load_pages.assert_called_with(
[GuiPage(name='bar', persistent=False, duration=10, namespace='foo'),
GuiPage(name='test/baz', persistent=False, duration=10, namespace='foo')], 0)
self.namespace_manager._update_namespace_persistence. \
assert_called_with(10)

@@ -336,15 +315,11 @@ def test_handle_show_page(self):
message = Message("test", {"__from": "skill",
"__idle": False,
"index": 1,
"page": ["/gui/page_1", "/gui/test/page_2"],
"page_names": ["page_1", "test/page_2"],
"ui_directories": ui_directories})
self.namespace_manager.handle_show_page(message)
expected_page1 = GuiPage(None, "page_1", False, 0, "page_1", "skill",
ui_directories)
expected_page2 = GuiPage(None, "test/page_2", False, 0, "test/page_2",
"skill", ui_directories)
self.namespace_manager._legacy_show_page.assert_called_once()
expected_page1 = GuiPage("page_1", False, 0, "skill")
expected_page2 = GuiPage("test/page_2", False, 0, "skill")
self.namespace_manager._activate_namespace.assert_called_with("skill")
self.namespace_manager._load_pages.assert_called_with([expected_page1,
expected_page2],
@@ -359,10 +334,7 @@ def test_handle_show_page(self):
"page": ["/gui/SYSTEM_TextFrame.qml"],
"page_names": ["SYSTEM_TextFrame"]})
self.namespace_manager.handle_show_page(message)
expected_page = GuiPage(None, "SYSTEM_TextFrame", True, 0,
"SYSTEM_TextFrame", "skill_no_res",
{"all": self.namespace_manager._system_res_dir})
self.namespace_manager._legacy_show_page.assert_called_once()
expected_page = GuiPage("SYSTEM_TextFrame", True, 0, "skill_no_res")
self.namespace_manager._activate_namespace.assert_called_with(
"skill_no_res")
self.namespace_manager._load_pages.assert_called_with([expected_page],
@@ -371,7 +343,6 @@ def test_handle_show_page(self):
assert_called_with(True)
# TODO: Test page_names with files and URIs

self.namespace_manager._legacy_show_page = real_legacy_show_page
self.namespace_manager._activate_namespace = real_activate_namespace
self.namespace_manager._load_pages = real_load_pages
self.namespace_manager._update_namespace_persistence = \
@@ -455,16 +426,15 @@ def test_del_namespace_in_remove_timers(self):
pass

def test_upload_system_resources(self):
test_dir = join(dirname(__file__), "upload_test")
makedirs(test_dir, exist_ok=True)
self.namespace_manager.gui_file_path = test_dir
self.namespace_manager._upload_system_resources()
self.assertTrue(isdir(join(test_dir, "system", "qt5")))
self.assertTrue(isfile(join(test_dir, "system", "qt5",
p = f"{GUI_CACHE_PATH}/system"
rmtree(p)
self.namespace_manager._cache_system_resources()
self.assertTrue(isdir(join(p, "qt5")))
self.assertTrue(isfile(join(p, "qt5",
"SYSTEM_TextFrame.qml")))
# Test repeated copy doesn't raise any exception
self.namespace_manager._upload_system_resources()
self.assertTrue(isdir(join(test_dir, "system", "qt5")))
self.assertTrue(isfile(join(test_dir, "system", "qt5",
self.namespace_manager._cache_system_resources()
self.assertTrue(isdir(join(p, "qt5")))
self.assertTrue(isfile(join(p, "qt5",
"SYSTEM_TextFrame.qml")))
rmtree(test_dir)
rmtree(p)
91 changes: 0 additions & 91 deletions test/unittests/test_page.py

This file was deleted.