Browse Source

Initial high level tests for the editor app.

startup
Ben Niemann 3 years ago
parent
commit
d456ba030d
  1. 1
      noisicaa/ui/CMakeLists.txt
  2. 3
      noisicaa/ui/editor_app.py
  3. 222
      noisicaa/ui/editor_app_test.py
  4. 14
      noisicaa/ui/editor_window.py
  5. 4
      noisicaa/ui/open_project_dialog.py
  6. 3
      noisicaa/ui/project_registry.py

1
noisicaa/ui/CMakeLists.txt

@ -25,6 +25,7 @@ add_python_package(
device_list.py
dynamic_layout.py
editor_app.py
editor_app_test.py
editor_window.py
flowlayout.py
gain_slider.py

3
noisicaa/ui/editor_app.py

@ -431,6 +431,9 @@ class EditorApp(ui_base.AbstractEditorApp):
if not self.__windows:
self.quit()
def windows(self) -> List[editor_window.EditorWindow]:
return self.__windows
def quit(self, exit_code: int = 0) -> None:
# TODO: quit() is not a method of ProcessBase, only in UIProcess. Find some way to
# fix that without a cyclic import.

222
noisicaa/ui/editor_app_test.py

@ -0,0 +1,222 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
import asyncio
import os
import os.path
import shutil
import time
from PyQt5 import QtCore
from PyQt5 import QtWidgets
from noisidev import qttest
from noisidev import unittest_mixins
from noisicaa.constants import TEST_OPTS
from noisicaa import runtime_settings
from noisicaa.core import storage
from noisicaa.core import process_manager
from noisicaa.music import project as project_lib
from . import editor_app
class EditorAppTest(unittest_mixins.ProcessManagerMixin, qttest.QtTestCase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.settings = None
self.runtime_settings = None
self.process = None
self.app = None
async def setup_testcase(self):
self.music_dir = os.path.join(TEST_OPTS.TMP_DIR, 'projects')
if os.path.isdir(self.music_dir):
shutil.rmtree(self.music_dir)
os.makedirs(self.music_dir)
self.setup_node_db_process(inline=True)
self.setup_urid_mapper_process(inline=True)
self.setup_instrument_db_process(inline=True)
self.setup_audioproc_process(inline=True)
self.setup_writer_process(inline=True)
self.settings = QtCore.QSettings()
self.settings.setValue('audio/backend', 'null')
self.settings.setValue('project_folders', [self.music_dir])
self.runtime_settings = runtime_settings.RuntimeSettings()
self.process = process_manager.ProcessBase(
name='ui',
manager=self.process_manager_client,
event_loop=self.loop,
tmp_dir=TEST_OPTS.TMP_DIR)
await self.process.setup()
async def cleanup_testcase(self):
if self.app is not None:
await self.app.cleanup()
if self.process is not None:
await self.process.cleanup()
async def create_blank_project(self, name):
path = os.path.join(self.music_dir, name)
pool = project_lib.Pool()
project = pool.create(project_lib.Project)
pool.set_root(project)
ps = storage.ProjectStorage.create(path)
ps.add_checkpoint(project.serialize_object(project))
ps.close()
return path
async def create_app(self, paths=None):
self.app = editor_app.EditorApp(
qt_app=self.qt_app,
process=self.process,
paths=paths or [],
runtime_settings=self.runtime_settings,
settings=self.settings)
await self.app.setup()
async def get_initial_window(self):
self.assertEqual(len(self.app.windows()), 1)
win = self.app.windows()[0]
self.assertEqual(win.windowTitle(), 'noisicaä')
return win
async def window_get_project_tab(self, win, idx):
tabs = win.findChild(QtWidgets.QTabWidget, 'project-tabs')
assert tabs is not None
return tabs.widget(idx)
async def window_num_project_tabs(self, win):
tabs = win.findChild(QtWidgets.QTabWidget, 'project-tabs')
assert tabs is not None
return tabs.count()
async def window_close_current_project(self, win):
close_project_action = win.findChild(QtWidgets.QAction, 'close-project')
assert close_project_action is not None
close_project_action.trigger()
async def project_tab_wait_for_page(self, tab, name):
page_changed = asyncio.Event(loop=self.loop)
conn = tab.currentPageChanged.connect(lambda _: page_changed.set())
try:
# Wait until the "open project" dialog is visible.
t0 = time.time()
while tab.page().objectName() != name and time.time() < t0 + 10:
try:
await asyncio.wait_for(page_changed.wait(), timeout=1.0, loop=self.loop)
except asyncio.TimeoutError:
pass
else:
page_changed.clear()
self.assertEqual(tab.page().objectName(), name)
finally:
tab.currentPageChanged.disconnect(conn)
async def project_tab_open_project(self, tab, row):
open_project_dialog = tab.findChild(QtWidgets.QWidget, 'open-project-dialog')
assert open_project_dialog is not None
open_button = open_project_dialog.findChild(QtWidgets.QAbstractButton, 'open')
assert open_button is not None
self.assertFalse(open_button.isEnabled())
project_list = open_project_dialog.findChild(QtWidgets.QListView, 'project-list')
assert project_list is not None
project_list.setCurrentIndex(project_list.model().index(0, 0))
self.assertTrue(open_button.isEnabled())
open_button.click()
async def test_open_blank_project(self):
await self.create_blank_project('blank-project')
await self.create_app()
# Get initial project tab.
win = await self.get_initial_window()
tab = await self.window_get_project_tab(win, 0)
# Wait until the "open project" widget shows up.
await self.project_tab_wait_for_page(tab, 'open-project')
# Select the first (and only) project.
await self.project_tab_open_project(tab, 0)
# Wait until the project view is visible.
await self.project_tab_wait_for_page(tab, 'project-view')
# Close the project.
await self.window_close_current_project(win)
# Wait until the "open project" dialog is visible again.
await self.project_tab_wait_for_page(tab, 'open-project')
async def test_open_project_from_cmdline(self):
path = await self.create_blank_project('blank-project')
await self.create_app([path])
# Get initial project tab.
win = await self.get_initial_window()
tab = await self.window_get_project_tab(win, 0)
# Wait until the project view is visible.
await self.project_tab_wait_for_page(tab, 'project-view')
# Close the project.
await self.window_close_current_project(win)
# Wait until the "open project" dialog is visible again.
await self.project_tab_wait_for_page(tab, 'open-project')
async def test_open_corrupt_project(self):
path = await self.create_blank_project('broken-project')
os.unlink(os.path.join(path, 'log.index'))
await self.create_app()
# Get initial project tab.
win = await self.get_initial_window()
tab = await self.window_get_project_tab(win, 0)
# Wait until the "open project" widget shows up.
await self.project_tab_wait_for_page(tab, 'open-project')
# Select the first (and only) project.
await self.project_tab_open_project(tab, 0)
# Wait for the error dialog to pop up.
t0 = time.time()
while t0 < time.time() + 10:
error_dialog = win.findChild(QtWidgets.QMessageBox, 'project-open-error')
if error_dialog is not None:
error_dialog.accept()
break
await asyncio.sleep(0.2, loop=self.loop)
# Wait until the "open project" dialog is visible again.
await self.project_tab_wait_for_page(tab, 'open-project')

14
noisicaa/ui/editor_window.py

@ -84,6 +84,8 @@ class SetupProgressWidget(QtWidgets.QWidget):
class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
currentPageChanged = QtCore.pyqtSignal(QtWidgets.QWidget)
def __init__(self, parent: QtWidgets.QTabWidget, **kwargs: Any) -> None:
super().__init__(parent=parent, **kwargs)
@ -105,6 +107,9 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
def projectDebugger(self) -> Optional[project_debugger.ProjectDebugger]:
return self.__project_debugger
def page(self) -> QtWidgets.QWidget:
return self.__page
def __setPage(
self,
name: str,
@ -123,6 +128,7 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
self.__layout.addWidget(page)
self.__page = page
self.__page_cleanup_func = cleanup_func
self.currentPageChanged.emit(self.__page)
def showOpenDialog(self) -> None:
dialog = open_project_dialog.OpenProjectDialog(
@ -146,6 +152,7 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
l2.addStretch(1)
page = QtWidgets.QWidget(self)
page.setObjectName('open-project')
page.setLayout(l2)
self.__setPage("Open project...", page, dialog.cleanup)
@ -154,6 +161,7 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
logger.error(traceback.format_exc())
dialog = QtWidgets.QMessageBox(self)
dialog.setObjectName('project-open-error')
dialog.setWindowTitle("noisicaä - Error")
dialog.setIcon(QtWidgets.QMessageBox.Critical)
dialog.setText(message)
@ -183,6 +191,7 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
else:
view = project_view.ProjectView(project_connection=project, context=self.context)
view.setObjectName('project-view')
await view.setup()
self.__project_view = view
self.__setPage(project.name, view)
@ -202,6 +211,7 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
else:
await self.app.project_registry.refresh()
view = project_view.ProjectView(project_connection=project, context=self.context)
view.setObjectName('project-view')
await view.setup()
self.__project_view = view
self.__setPage(project.name, view)
@ -210,12 +220,14 @@ class ProjectTabPage(ui_base.CommonMixin, QtWidgets.QWidget):
self.showLoadSpinner(project.name, "Loading project \"%s\"..." % project.name)
await self.app.setup_complete.wait()
debugger = project_debugger.ProjectDebugger(project=project, context=self.context)
debugger.setObjectName('project-debugger')
await debugger.setup()
self.__project_debugger = debugger
self.__setPage(project.name, debugger)
def showLoadSpinner(self, name: str, message: str) -> None:
page = QtWidgets.QWidget(self)
page.setObjectName('load-spinner')
label = QtWidgets.QLabel(page)
label.setText(message)
@ -283,6 +295,7 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
self.loopEnabledChanged.connect(self.onLoopEnabledChanged)
self.__project_tabs = QtWidgets.QTabWidget(self)
self.__project_tabs.setObjectName('project-tabs')
self.__project_tabs.setTabBarAutoHide(True)
self.__project_tabs.setUsesScrollButtons(True)
self.__project_tabs.setTabsClosable(True)
@ -379,6 +392,7 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
self._render_action.triggered.connect(self.onRender)
self._close_current_project_action = QtWidgets.QAction("Close", self)
self._close_current_project_action.setObjectName('close-project')
self._close_current_project_action.setShortcut(QtGui.QKeySequence.Close)
self._close_current_project_action.setStatusTip("Close the current project")
self._close_current_project_action.triggered.connect(self.onCloseCurrentProject)

4
noisicaa/ui/open_project_dialog.py

@ -360,6 +360,8 @@ class OpenProjectDialog(ui_base.CommonMixin, QtWidgets.QWidget):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.setObjectName('open-project-dialog')
self.__filter_model = FilterModel()
self.__filter_model.setSourceModel(FlatProjectListModel(self.app.project_registry))
self.__filter_model.setSortRole(project_registry_lib.Project.NameRole)
@ -399,6 +401,7 @@ class OpenProjectDialog(ui_base.CommonMixin, QtWidgets.QWidget):
self.__updateSort()
self.__open_button = QtWidgets.QPushButton(self)
self.__open_button.setObjectName('open')
self.__open_button.setIcon(QtGui.QIcon.fromTheme('document-open'))
self.__open_button.setText("Open")
self.__open_button.clicked.connect(self.__openClicked)
@ -429,6 +432,7 @@ class OpenProjectDialog(ui_base.CommonMixin, QtWidgets.QWidget):
self.__more_button.setMenu(self.__more_menu)
self.__list = ProjectListView(self)
self.__list.setObjectName('project-list')
self.__list.setModel(self.__filter_model)
self.__list.numProjectsSelected.connect(lambda _: self.__updateButtons())
self.__list.itemDoubleClicked.connect(self.__itemDoubleClicked)

3
noisicaa/ui/project_registry.py

@ -211,8 +211,7 @@ class ProjectRegistry(ui_base.CommonMixin, QtCore.QAbstractItemModel):
return list(self.__root.projects())
async def refresh(self) -> None:
# TODO: get list of directories from settings
directories = ['~/Music/Noisicaä']
directories = self.app.settings.value('project_folders', ['~/Music/Noisicaä'])
paths = await self.event_loop.run_in_executor(None, self.__scan_projects, directories)

Loading…
Cancel
Save