diff --git a/.github/workflows/irc.yml b/.github/workflows/irc.yml index 1bfc96877..eccd27d08 100644 --- a/.github/workflows/irc.yml +++ b/.github/workflows/irc.yml @@ -4,6 +4,7 @@ on: [push] jobs: notification: + if: github.repository == 'moddevices/mod-ui' runs-on: ubuntu-latest name: IRC notification steps: diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index ce3a1944a..31a635ba5 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -19,6 +19,9 @@ jobs: pip3 install pylint - name: Run pylint run: | + export MOD_DEV_HOST=1 + export MOD_DEV_ENVIRONMENT=0 + virtualenv modui-env source modui-env/bin/activate pylint -E mod/*.py diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 000000000..f69d68a25 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,58 @@ +name: unittest + +on: [push, pull_request] + +jobs: + unittest: + runs-on: ubuntu-22.04 + name: unittest + steps: + - uses: actions/checkout@v3 + - name: Install virtualenv + run: | + sudo apt install python3-virtualenv + - name: Setup utils + run: | + sudo apt install python3-dev build-essential libasound2-dev libjack-jackd2-dev liblilv-dev libjpeg-dev zlib1g-dev + make -C utils + - name: Setup unittest + run: | + virtualenv modui-env + source modui-env/bin/activate + pip3 install -r requirements.txt + # pytest + pip3 install pytest pytest-cov + sed -i -e 's/collections.MutableMapping/collections.abc.MutableMapping/' modui-env/lib/python3.10/site-packages/tornado/httputil.py + # mod + python3 setup.py develop + - name: Run unittest + run: | + virtualenv modui-env + source modui-env/bin/activate + pytest --cov=mod --cov-report=html --cov-report=term --cov-report=xml + - name: Archive coverage files + uses: actions/upload-artifact@v3 + with: + name: coverage + path: | + .coverage + htmlcov/** + cobertura.xml + - name: Code Coverage Report + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: '15 30' +# - name: Add Coverage PR Comment +# uses: marocchino/sticky-pull-request-comment@v2 +# if: github.event_name == 'pull_request' +# with: +# recreate: true +# path: code-coverage-results.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index c9fea7225..3e2fca109 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ package-lock.json *.tar.xz mod-env/ .env/ -host/ +/host/ *.crf *.axf *.hex @@ -37,3 +37,7 @@ utils/test *node_modules/ .idea *.egg-info +# Coverage +.coverage +coverage.xml +htmlcov/ \ No newline at end of file diff --git a/README.rst b/README.rst index 8c9ac1da5..957f4a4a9 100644 --- a/README.rst +++ b/README.rst @@ -58,3 +58,33 @@ And now you are ready to start the webserver:: Setting the environment variables is needed when developing on a PC. Open your browser and point to http://localhost:8888/. + +Development +----------- + +The source code is expected to be ran on a Python 3.4 version. + +The source code is organized with the following structure + +* ``html``: Frontend source code +* ``mod``: Backend source code + * ``controller/``: Tornado related source code + * ``rest/``: REST classes + * ``websocket/``: web-socket classes + * ``dto/``: Utility model representation + * ``handler/``: Common tornado handlers + * ``file_receiver/``: Common tornado file receivers + * ``model/``: Model abstraction (snapshot, pedalboard, bank) + * ``service/``: High level layer for ``mod-host`` usage + * ``util/``: Common utility source code + * ``settings.py``: Application parameters + * ``host.py``: ``mod-host`` interface for communication with application + * ``webserver.py``: Web-server initialization +* ``modtools``: +* ``test``: Python unit tests +* ``utils``: C++ utility code + + +Some IDEs can improve the code completion if you install mod-ui as an local packaged on virtualenv:: + + $ python3 setup.py develop diff --git a/mod/controller/__init__.py b/mod/controller/__init__.py new file mode 100644 index 000000000..498793f03 --- /dev/null +++ b/mod/controller/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/mod/controller/handler/__init__.py b/mod/controller/handler/__init__.py new file mode 100644 index 000000000..498793f03 --- /dev/null +++ b/mod/controller/handler/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/mod/controller/handler/json_request_handler.py b/mod/controller/handler/json_request_handler.py new file mode 100644 index 000000000..13c2a954b --- /dev/null +++ b/mod/controller/handler/json_request_handler.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +import json + +from tornado.util import unicode_type + +from mod.controller.handler.timeless_request_handler import TimelessRequestHandler + + +class JsonRequestHandler(TimelessRequestHandler): + def write(self, data): + # FIXME: something is sending strings out, need to investigate what later.. + # it's likely something using write(json.dumps(...)) + # we want to prevent that as it causes issues under Mac OS + + if isinstance(data, (bytes, unicode_type, dict)): + TimelessRequestHandler.write(self, data) + self.finish() + return + + elif data is True: + data = "true" + self.set_header("Content-Type", "application/json; charset=UTF-8") + + elif data is False: + data = "false" + self.set_header("Content-Type", "application/json; charset=UTF-8") + + # TESTING for data types, remove this later + #elif not isinstance(data, list): + #print("=== TESTING: Got new data type for RequestHandler.write():", type(data), "msg:", data) + #data = json.dumps(data) + #self.set_header('Content-type', 'application/json') + + else: + data = json.dumps(data) + self.set_header("Content-Type", "application/json; charset=UTF-8") + + TimelessRequestHandler.write(self, data) + self.finish() diff --git a/mod/controller/handler/timeless_request_handler.py b/mod/controller/handler/timeless_request_handler.py new file mode 100644 index 000000000..47b29da43 --- /dev/null +++ b/mod/controller/handler/timeless_request_handler.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +from tornado import web + + +class TimelessRequestHandler(web.RequestHandler): + def compute_etag(self): + return None + + def set_default_headers(self): + self._headers.pop("Date") + + def should_return_304(self): + return False diff --git a/mod/controller/rest/__init__.py b/mod/controller/rest/__init__.py new file mode 100644 index 000000000..498793f03 --- /dev/null +++ b/mod/controller/rest/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/mod/controller/rest/snapshot.py b/mod/controller/rest/snapshot.py new file mode 100644 index 000000000..f2ec4dca3 --- /dev/null +++ b/mod/controller/rest/snapshot.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +from tornado import gen, web + +from mod.controller.handler.json_request_handler import JsonRequestHandler +from mod.session import SESSION +from mod.settings import DEFAULT_SNAPSHOT_NAME + + +class SnapshotName(JsonRequestHandler): + + # TODO: Replace GET /snapshot/name + # to GET /pedalboards//snapshots/ + def get(self): + """ + Remove the snapshot name of identifier ``id`` of the loaded pedalboard; + If is requested by an invalid ``id``, will be returned the default snapshot name. + + .. code-block:: json + + { + "ok": true, + "name": "Pedalboard name" + } + + :return: Snapshot name + """ + idx = int(self.get_argument('id')) + name = SESSION.host.snapshot_name(idx) or DEFAULT_SNAPSHOT_NAME + self.write({ + 'ok': bool(name), # FIXME: Always true + 'name': name + }) + + +class SnapshotList(JsonRequestHandler): + + # TODO: Replace GET /snapshot/list + # to GET /pedalboards//snapshots/ + def get(self): + """ + Get snapshots name of the loaded pedalboard + + .. code-block:: json + + { + 0: "First snapshot", + 1: "Second snapshot" + } + + :return: names of the current pedalboard snapshots + """ + snapshots = SESSION.host.pedalboard_snapshots + snapshots = dict((i, snapshots[i]['name']) for i in range(len(snapshots)) if snapshots[i] is not None) + self.write(snapshots) + + +class SnapshotSave(JsonRequestHandler): + + # TODO: Replace POST /snapshot/save + # to POST /pedalboards/current/snapshots/current + def post(self): + """ + Update the current snapshot status + + :return: `true` if it was successfully updated + """ + ok = SESSION.host.snapshot_save() + self.write(ok) + + +class SnapshotSaveAs(JsonRequestHandler): + + # TODO: Replace GET /snapshot/saveas + # to POST /pedalboards/current/snapshots/saveas + @web.asynchronous + @gen.engine + def get(self): + """ + Create a new snapshot with the suggested ``title`` based on the current pedalboard status; + .. code-block:: json + + { + "ok": true, + "id": 1, + "title": "Snapshot name" + } + + :return: `true` if it was successfully deleted + """ + title = self.get_argument('title') # TODO rename to name (standardization) + idx = SESSION.host.snapshot_saveas(title) + title = SESSION.host.snapshot_name(idx) + + yield gen.Task(SESSION.host.hmi_report_ss_name_if_current, idx) + + self.write({ + 'ok': idx is not None, # FIXME: Always true + 'id': idx, + 'title': title, # TODO rename to name (standardization) + }) + + +class SnapshotRemove(JsonRequestHandler): + + # TODO: Replace GET /snapshot/remove?id= + # to DELETE /pedalboards//snapshots/ + def get(self): + """ + Remove the snapshot of identifier ``id`` of the loaded pedalboard + + :return: `true` if it was successfully deleted + """ + idx = int(self.get_argument('id')) + ok = SESSION.host.snapshot_remove(idx) + + self.write(ok) + + +class SnapshotRename(JsonRequestHandler): + + # TODO: Replace GET /snapshot/rename?id=&?title= + # to PATCH /pedalboards//snapshots/ + @web.asynchronous + @gen.engine + def get(self): + """ + Rename the snapshot of ``ìd`` identifier with the suggested ``title``. + + .. code-block:: json + + { + "ok": true, + "title": "Snapshot name" + } + + :return: new snapshot name + """ + idx = int(self.get_argument('id')) + title = self.get_argument('title') # TODO rename to name (standardization) + ok = SESSION.host.snapshot_rename(idx, title) + + if ok: + title = SESSION.host.snapshot_name(idx) + + yield gen.Task(SESSION.host.hmi_report_ss_name_if_current, idx) + + self.write({ + 'ok': ok, + 'title': title, # TODO rename to name (standardization) + }) + + +class SnapshotLoad(JsonRequestHandler): + + # TODO: Replace GET /snapshot/load?id= + # to POST /pedalboards/current/snapshots//load + @web.asynchronous + @gen.engine + def get(self): + """ + Loads the snapshot of ``ìd`` identifier. + + :return: Was snapshot loaded? + """ + idx = int(self.get_argument('id')) + abort_catcher = SESSION.host.abort_previous_loading_progress("web SnapshotLoad") + ok = yield gen.Task(SESSION.host.snapshot_load_gen_helper, idx, False, abort_catcher) + self.write(ok) diff --git a/mod/host.py b/mod/host.py index 23a468985..690d0d297 100644 --- a/mod/host.py +++ b/mod/host.py @@ -3003,8 +3003,15 @@ def _snapshot_unique_name(self, name): return get_unique_name(name, names) or name def snapshot_make(self, name): + """ + Create a snapshot based on current pedalboard + + :param name: snapshot name + :return: snapshot created + """ self.pedalboard_modified = True + # TODO Create Snapshot model class snapshot = { "name": name, "data": {}, @@ -3081,6 +3088,7 @@ def snapshot_remove(self, idx): def snapshot_load_gen_helper(self, idx, from_hmi, abort_catcher, callback): self.snapshot_load(idx, from_hmi, abort_catcher, callback) + # FIXME: Rename callback argument (https://stackoverflow.com/a/57121126) @gen.coroutine def snapshot_load(self, idx, from_hmi, abort_catcher, callback): if idx in (self.HMI_SNAPSHOTS_1, self.HMI_SNAPSHOTS_2, self.HMI_SNAPSHOTS_3): diff --git a/mod/webserver.py b/mod/webserver.py index a4de1f17f..b9d448e04 100644 --- a/mod/webserver.py +++ b/mod/webserver.py @@ -10,16 +10,20 @@ import subprocess import sys import time - from base64 import b64decode, b64encode from datetime import timedelta from random import randint -from tornado import gen, iostream, web, websocket +from uuid import uuid4 + +from tornado import gen, web, websocket from tornado.escape import squeeze, url_escape, xhtml_escape from tornado.ioloop import IOLoop from tornado.template import Loader -from tornado.util import unicode_type -from uuid import uuid4 + +from mod.controller.handler.json_request_handler import JsonRequestHandler +from mod.controller.handler.timeless_request_handler import TimelessRequestHandler +from mod.controller.rest.snapshot import SnapshotName, SnapshotList, SnapshotSave, SnapshotSaveAs, SnapshotRemove, \ + SnapshotRename, SnapshotLoad try: from signal import signal, SIGUSR1, SIGUSR2 @@ -36,13 +40,12 @@ LV2_PLUGIN_DIR, LV2_PEDALBOARDS_DIR, IMAGE_VERSION, UPDATE_CC_FIRMWARE_FILE, UPDATE_MOD_OS_FILE, UPDATE_MOD_OS_HERLPER_FILE, USING_256_FRAMES_FILE, DEFAULT_ICON_TEMPLATE, DEFAULT_SETTINGS_TEMPLATE, DEFAULT_ICON_IMAGE, - DEFAULT_PEDALBOARD, DEFAULT_SNAPSHOT_NAME, DATA_DIR, KEYS_PATH, USER_FILES_DIR, + DEFAULT_PEDALBOARD, DATA_DIR, KEYS_PATH, USER_FILES_DIR, FAVORITES_JSON_FILE, PREFERENCES_JSON_FILE, USER_ID_JSON_FILE, DEV_HOST, UNTITLED_PEDALBOARD_NAME, MODEL_CPU, MODEL_TYPE, PEDALBOARDS_LABS_HTTP_ADDRESS) from mod import ( - TextFileFlusher, WINDOWS, - check_environment, jsoncall, safe_json_load, + TextFileFlusher, check_environment, jsoncall, safe_json_load, get_hardware_descriptor, get_unique_name, os_sync, symbolify, ) from mod.bank import list_banks, save_banks, remove_pedalboard_from_banks @@ -207,15 +210,6 @@ def reset_get_all_pedalboards_cache_with_refresh(ptype): reset_get_all_pedalboards_cache(ptype) IOLoop.instance().add_callback(_reset_get_all_pedalboards_cache_with_refresh_2) -class TimelessRequestHandler(web.RequestHandler): - def compute_etag(self): - return None - - def set_default_headers(self): - self._headers.pop("Date") - - def should_return_304(self): - return False class TimelessStaticFileHandler(web.StaticFileHandler): def compute_etag(self): @@ -235,37 +229,6 @@ def get_cache_time(self, path, modified, mime_type): def get_modified_time(self): return None -class JsonRequestHandler(TimelessRequestHandler): - def write(self, data): - # FIXME: something is sending strings out, need to investigate what later.. - # it's likely something using write(json.dumps(...)) - # we want to prevent that as it causes issues under Mac OS - - if isinstance(data, (bytes, unicode_type, dict)): - TimelessRequestHandler.write(self, data) - self.finish() - return - - elif data is True: - data = "true" - self.set_header("Content-Type", "application/json; charset=UTF-8") - - elif data is False: - data = "false" - self.set_header("Content-Type", "application/json; charset=UTF-8") - - # TESTING for data types, remove this later - #elif not isinstance(data, list): - #print("=== TESTING: Got new data type for RequestHandler.write():", type(data), "msg:", data) - #data = json.dumps(data) - #self.set_header('Content-type', 'application/json') - - else: - data = json.dumps(data) - self.set_header("Content-Type", "application/json; charset=UTF-8") - - TimelessRequestHandler.write(self, data) - self.finish() class CachedJsonRequestHandler(JsonRequestHandler): def set_default_headers(self): @@ -1593,74 +1556,7 @@ def post(self, mode): ok = yield gen.Task(SESSION.web_set_sync_mode, transport_sync) self.write(ok) -class SnapshotSave(JsonRequestHandler): - def post(self): - ok = SESSION.host.snapshot_save() - self.write(ok) -class SnapshotSaveAs(JsonRequestHandler): - @web.asynchronous - @gen.engine - def get(self): - title = self.get_argument('title') - idx = SESSION.host.snapshot_saveas(title) - title = SESSION.host.snapshot_name(idx) - - yield gen.Task(SESSION.host.hmi_report_ss_name_if_current, idx) - - self.write({ - 'ok': idx is not None, - 'id': idx, - 'title': title, - }) - -class SnapshotRename(JsonRequestHandler): - @web.asynchronous - @gen.engine - def get(self): - idx = int(self.get_argument('id')) - title = self.get_argument('title') - ok = SESSION.host.snapshot_rename(idx, title) - - if ok: - title = SESSION.host.snapshot_name(idx) - - yield gen.Task(SESSION.host.hmi_report_ss_name_if_current, idx) - - self.write({ - 'ok': ok, - 'title': title, - }) - -class SnapshotRemove(JsonRequestHandler): - def get(self): - idx = int(self.get_argument('id')) - ok = SESSION.host.snapshot_remove(idx) - self.write(ok) - -class SnapshotList(JsonRequestHandler): - def get(self): - snapshots = SESSION.host.pedalboard_snapshots - snapshots = dict((i, snapshots[i]['name']) for i in range(len(snapshots)) if snapshots[i] is not None) - self.write(snapshots) - -class SnapshotName(JsonRequestHandler): - def get(self): - idx = int(self.get_argument('id')) - name = SESSION.host.snapshot_name(idx) or DEFAULT_SNAPSHOT_NAME - self.write({ - 'ok' : bool(name), - 'name': name - }) - -class SnapshotLoad(JsonRequestHandler): - @web.asynchronous - @gen.engine - def get(self): - idx = int(self.get_argument('id')) - abort_catcher = SESSION.host.abort_previous_loading_progress("web SnapshotLoad") - ok = yield gen.Task(SESSION.host.snapshot_load_gen_helper, idx, False, abort_catcher) - self.write(ok) class DashboardClean(JsonRequestHandler): @web.asynchronous diff --git a/test/controller/__init__.py b/test/controller/__init__.py new file mode 100644 index 000000000..498793f03 --- /dev/null +++ b/test/controller/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/test/controller/snapshot_list_test.py b/test/controller/snapshot_list_test.py new file mode 100644 index 000000000..06d36b1a2 --- /dev/null +++ b/test/controller/snapshot_list_test.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +# This test uses coroutine style. +import json +from uuid import uuid4 + +from tornado.testing import AsyncHTTPTestCase + +from mod.webserver import application + + +class SnapshotListTestCase(AsyncHTTPTestCase): + def get_app(self): + return application + + # Pedalboard starts empty, but after there is one, it isn't possible to exclude all of them + # def test_empty(self): + # response = self.fetch("/snapshot/list") + # + # self.assertEqual(response.code, 200) + # self.assert_equal([], response.body) + + def test_populated_name(self): + original_snapshots = json.loads(self.fetch("/snapshot/list").body) + + # Populate + expected_name_0 = str(uuid4()) + expected_name_1 = str(uuid4()) + + self.fetch("/snapshot/saveas?title=" + expected_name_0) + self.fetch("/snapshot/saveas?title=" + expected_name_1) + + response = self.fetch("/snapshot/list") + + # Assert list + new_snapshots = self._names(original_snapshots) + [expected_name_0, expected_name_1] + self.assert_equal(new_snapshots, response.body) + + # Clear created + for index in reversed(range(len(original_snapshots), len(new_snapshots)+1)): + self.fetch("/snapshot/remove?id=" + str(index)) + + def assert_equal(self, lista, body): + self.assertDictEqual( + {str(index): item for index, item in enumerate(lista)}, + json.loads(body) + ) + + def _names(self, snapshots): + return list(snapshots.values()) diff --git a/test/controller/snapshot_load_test.py b/test/controller/snapshot_load_test.py new file mode 100644 index 000000000..d704aacfc --- /dev/null +++ b/test/controller/snapshot_load_test.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +# This test uses coroutine style. +import json +from uuid import uuid4 + +from tornado.httpclient import HTTPRequest +from tornado.testing import AsyncHTTPTestCase + +from mod.webserver import application + + +class SnapshotLoadTestCase(AsyncHTTPTestCase): + + def get_app(self): + return application + + def test_load_invalid_index(self): + response = self.fetch("/snapshot/load?id=" + str(-1)) + self.assertEqual(response.code, 200) + self.assertFalse(json.loads(response.body)) + + response = self.fetch("/snapshot/load?id=" + str(1000)) + self.assertEqual(response.code, 200) + self.assertFalse(json.loads(response.body)) + + # TODO Test load valid snapshot + # but it is probably better to test host.py directly + + def test_load_invalid_index(self): + # Create a snapshot + name = str(uuid4()) + snapshot_index = self._save_as(name) + + response = self.fetch("/snapshot/load?id=" + str(snapshot_index)) + + # Assert is loaded + self.assertEqual(response.code, 200) + self.assertTrue(json.loads(response.body)) + + # Clean + self.fetch("/snapshot/remove?id=" + str(snapshot_index)) + + def _save_as(self, name): + response = json.loads(self.fetch("/snapshot/saveas?title=" + name).body) + return response['id'] diff --git a/test/controller/snapshot_name_test.py b/test/controller/snapshot_name_test.py new file mode 100644 index 000000000..46c245fee --- /dev/null +++ b/test/controller/snapshot_name_test.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +# This test uses coroutine style. +import json +from uuid import uuid4 + +from tornado.testing import AsyncHTTPTestCase + +from mod.settings import DEFAULT_SNAPSHOT_NAME +from mod.webserver import application + + +class SnapshotNameTestCase(AsyncHTTPTestCase): + def get_app(self): + return application + + def test_name_missing_snapshot(self): + response = self.fetch("/snapshot/name?id=1000") + + self.assertEqual(response.code, 200) + self.assert_equal(DEFAULT_SNAPSHOT_NAME, response.body) + + def test_name_negative_snapshot(self): + response = self.fetch("/snapshot/name?id=-1") + + self.assertEqual(response.code, 200) + self.assert_equal(DEFAULT_SNAPSHOT_NAME, response.body) + + def test_name(self): + expected_name_0 = str(uuid4()) + expected_name_1 = str(uuid4()) + + self.fetch("/snapshot/saveas?title=" + expected_name_0) + self.fetch("/snapshot/saveas?title=" + expected_name_1) + + snapshots = self.saved_items() + + response_0 = self.fetch("/snapshot/name?id=" + snapshots[expected_name_0]) + response_1 = self.fetch("/snapshot/name?id=" + snapshots[expected_name_1]) + + self.assert_equal(expected_name_0, response_0.body) + self.assert_equal(expected_name_1, response_1.body) + + for index in reversed(range(len(snapshots), 0)): + self.fetch("/snapshot/remove?id=" + str(index)) + + def assert_equal(self, name, body): + self.assertDictEqual( + { + 'ok': True, # FIXME: Always true + 'name': name + }, + json.loads(body) + ) + + def saved_items(self): + items = self.fetch("/snapshot/list") + + return { + value: key + for key, value in json.loads(items.body).items() + } diff --git a/test/controller/snapshot_remove_test.py b/test/controller/snapshot_remove_test.py new file mode 100644 index 000000000..7b257b276 --- /dev/null +++ b/test/controller/snapshot_remove_test.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +# This test uses coroutine style. +import json +from uuid import uuid4 + +from tornado.httpclient import HTTPRequest +from tornado.testing import AsyncHTTPTestCase + +from mod.webserver import application + + +class SnapshotRemoveTestCase(AsyncHTTPTestCase): + + def get_app(self): + return application + + def test_remove_non_current_snapshot(self): + # Create a default snapshot + name = str(uuid4()) + snapshot_1_index = self._save_as(name) + + # Create a snapshot to be deleted + name = str(uuid4()) + snapshot_2_index = self._save_as(name) + + # Delete snapshot + response = self.fetch("/snapshot/remove?id=" + str(snapshot_2_index)) + self.assertEqual(response.code, 200) + self.assertTrue(json.loads(response.body)) + + # Clear + self.fetch("/snapshot/remove?id=" + str(snapshot_1_index)) + + def test_remove_invalid_snapshot(self): + response = self.fetch("/snapshot/remove?id=" + str(-1)) + self.assertEqual(response.code, 200) + self.assertFalse(json.loads(response.body)) + + response = self.fetch("/snapshot/remove?id=" + str(1000)) + self.assertEqual(response.code, 200) + self.assertFalse(json.loads(response.body)) + + def test_remove_current_snapshot(self): + """ + It's possible to delete current snapshot + """ + # Create a default snapshot + name = str(uuid4()) + snapshot_1_index = self._save_as(name) + + # Create a snapshot to be deleted + name = str(uuid4()) + snapshot_2_index = self._save_as(name) + + # Load save snapshot to be deleted + response = self.fetch("/snapshot/load?id=" + str(snapshot_2_index)) + + # Assert is loaded + self.assertEqual(response.code, 200) + self.assertTrue(json.loads(response.body)) + + # Delete loaded snapshot + response = self.fetch("/snapshot/remove?id=" + str(snapshot_2_index)) + self.assertEqual(response.code, 200) + self.assertTrue(json.loads(response.body)) + + # Clear + self.fetch("/snapshot/remove?id=" + str(snapshot_1_index)) + + def test_remove_all_snapshots(self): + for i in range(5): + self._save_as("Snapshot-" + str(i)) + + snapshots = json.loads(self.fetch("/snapshot/list").body) + + # Remove all snapshots except the last + for i in range(len(snapshots) - 1): + response = self.fetch("/snapshot/remove?id=0") + self.assertEqual(response.code, 200) + self.assertTrue(json.loads(response.body)) + + # Try to remove the last snapshot without success, as expected + for i in range(len(snapshots) - 1): + response = self.fetch("/snapshot/remove?id=0") + self.assertEqual(response.code, 200) + self.assertFalse(json.loads(response.body)) + + def post(self, url): + self.http_client.fetch(HTTPRequest(self.get_url(url), "POST", allow_nonstandard_methods=True), self.stop) + return self.wait() + + def _save_as(self, name): + response = json.loads(self.fetch("/snapshot/saveas?title=" + name).body) + return response['id'] diff --git a/test/controller/snapshot_rename_test.py b/test/controller/snapshot_rename_test.py new file mode 100644 index 000000000..9c8703de8 --- /dev/null +++ b/test/controller/snapshot_rename_test.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +# This test uses coroutine style. +import json +from uuid import uuid4 + +from tornado.testing import AsyncHTTPTestCase + +from mod.webserver import application + + +class SnapshotRenameTestCase(AsyncHTTPTestCase): + def get_app(self): + return application + + def test_rename_unique_name(self): + original_name = str(uuid4()) + new_name = str(uuid4()) + + other = str(uuid4()) + + self.assertNotEqual(original_name, new_name) + + snapshot_1_index = self._save_as(original_name) + snapshot_2_index = self._save_as(other) + + # Assert rename works + snapshot = self._rename(snapshot_1_index, new_name) + self.assertDictEqual( + { + 'ok': True, + 'title': new_name + }, + snapshot + ) + + # Assert rename doesn't change snapshot's order + snapshot = self.fetch("/snapshot/name?id=" + str(snapshot_1_index)) + self.assertEqual(new_name, json.loads(snapshot.body)['name']) + + self.fetch("/snapshot/remove?id=" + str(snapshot_2_index)) + self.fetch("/snapshot/remove?id=" + str(snapshot_1_index)) + + def test_rename_to_the_same_name(self): + name = str(uuid4()) + + self.assertEqual(name, name) + + snapshot_index = self._save_as(name) + + # Assert rename + snapshot = self._rename(snapshot_index, name) + self.assertDictEqual( + { + 'ok': True, + 'title': name + }, + snapshot + ) + + self.fetch("/snapshot/remove?id=" + str(snapshot_index)) + + def test_rename_invalid_snapshot(self): + name = str(uuid4()) + + snapshot = self._rename(-1, name) + self.assertDictEqual( + {'ok': False, 'title': name}, + snapshot + ) + + snapshot = self._rename(1000, name) + self.assertDictEqual( + {'ok': False, 'title': name}, + snapshot + ) + + def test_rename_duplicated_name(self): + snapshot_sample_1 = str(uuid4()) + snapshot_sample_2 = str(uuid4()) + snapshot_sample_3 = str(uuid4()) + + snapshot_1_index = self._save_as(snapshot_sample_1) + snapshot_2_index = self._save_as(snapshot_sample_2) + snapshot_3_index = self._save_as(snapshot_sample_3) + + snapshot = self._rename(snapshot_1_index, snapshot_sample_3) + self.assertDictEqual( + { + 'ok': True, + 'title': snapshot_sample_3 + " (2)" + }, + snapshot + ) + + snapshot = self._rename(snapshot_2_index, snapshot_sample_3) + self.assertDictEqual( + { + 'ok': True, + 'title': snapshot_sample_3 + " (3)" + }, + snapshot + ) + + self.fetch("/snapshot/remove?id=" + str(snapshot_3_index)) + self.fetch("/snapshot/remove?id=" + str(snapshot_2_index)) + self.fetch("/snapshot/remove?id=" + str(snapshot_1_index)) + + def _rename(self, index, name): + response = self.fetch("/snapshot/rename?id=" + str(index) + "&title=" + name) + self.assertEqual(response.code, 200) + return json.loads(response.body) + + def _save_as(self, name): + response = json.loads(self.fetch("/snapshot/saveas?title=" + name).body) + return response['id'] diff --git a/test/controller/snapshot_save_as_test.py b/test/controller/snapshot_save_as_test.py new file mode 100644 index 000000000..cd37eaae5 --- /dev/null +++ b/test/controller/snapshot_save_as_test.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +# This test uses coroutine style. +import json +from uuid import uuid4 + +from tornado.httpclient import HTTPRequest +from tornado.testing import AsyncHTTPTestCase + +from mod.webserver import application + + +class SnapshotSaveAsTestCase(AsyncHTTPTestCase): + def get_app(self): + return application + + def test_save_as(self): + # Create a snapshot + name = str(uuid4()) + snapshot = self._save_as(name) + + # Assert is saved + self.assertDictEqual( + { + "ok": True, + "id": snapshot['id'], + "title": name + }, + snapshot + ) + + # Clear + self.fetch("/snapshot/remove?id=" + str(snapshot['id'])) + + def test_save_as_deleted_snapshot(self): + """ + It's possible, because the snapshot is created based on pedalboard's + current state instead of a last snapshot loaded + """ + # Create two snapshots + # because it is necessary to exists at least one + snapshot_1_name = str(uuid4()) + snapshot_2_name = str(uuid4()) + + snapshot_1 = self._save_as(snapshot_1_name) + snapshot_2 = self._save_as(snapshot_2_name) + + # Save snapshot created + self.fetch("/snapshot/load?id=" + str(snapshot_2['id'])) + response = self.post("/snapshot/save") + + # Assert is saved + self.assertTrue(json.loads(response.body)) + + # Delete created snapshot + self.fetch("/snapshot/remove?id=" + str(snapshot_2['id'])) + + # Save as deleted snapshot + snapshot_3_name = str(uuid4()) + snapshot_3 = self._save_as(snapshot_3_name) + self.assertEqual(response.code, 200) + self.assertDictEqual( + { + "ok": True, + "id": snapshot_3['id'], + "title": snapshot_3_name + }, + snapshot_3 + ) + + # Clear + self.fetch("/snapshot/remove?id=" + str(snapshot_3['id'])) + self.fetch("/snapshot/remove?id=" + str(snapshot_1['id'])) + + def test_save_duplicated_name(self): + snapshot_1_name = snapshot_2_name = str(uuid4()) + + self.assertEqual(snapshot_1_name, snapshot_2_name) + + snapshot_1 = self._save_as(snapshot_1_name) + snapshot_2 = self._save_as(snapshot_2_name) + + self.assertNotEqual(snapshot_1['title'], snapshot_2['title']) + self.assertTrue(snapshot_1['title'] < snapshot_2['title']) + + # Clear + self.fetch("/snapshot/remove?id=" + str(snapshot_2['id'])) + self.fetch("/snapshot/remove?id=" + str(snapshot_1['id'])) + + def post(self, url): + self.http_client.fetch(HTTPRequest(self.get_url(url), "POST", allow_nonstandard_methods=True), self.stop) + return self.wait() + + def _save_as(self, name): + response = self.fetch("/snapshot/saveas?title=" + name) + self.assertEqual(response.code, 200) + + return json.loads(response.body) diff --git a/test/controller/snapshot_save_test.py b/test/controller/snapshot_save_test.py new file mode 100644 index 000000000..82eb4fd3b --- /dev/null +++ b/test/controller/snapshot_save_test.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later +# This test uses coroutine style. +import json +from uuid import uuid4 + +from tornado.httpclient import HTTPRequest +from tornado.testing import AsyncHTTPTestCase + +from mod.webserver import application + + +class SnapshotSaveTestCase(AsyncHTTPTestCase): + def get_app(self): + return application + + def test_save(self): + # Create a snapshot + name = str(uuid4()) + snapshot_index = self._save_as(name) + + # Load and save snapshot created + self.fetch("/snapshot/load?id=" + str(snapshot_index)) + response = self.post("/snapshot/save") + + # Assert is saved + self.assertEqual(response.code, 200) + self.assertTrue(json.loads(response.body)) + + # Clear + self.fetch("/snapshot/remove?id=" + str(snapshot_index)) + + def test_save_deleted_snapshot(self): + # Create two snapshots + # because it is necessary to exists at least one + snapshot_1 = str(uuid4()) + snapshot_2 = str(uuid4()) + + snapshot_1_index = self._save_as(snapshot_1) + snapshot_2_index = self._save_as(snapshot_2) + + # Save snapshot created + self.fetch("/snapshot/load?id=" + str(snapshot_2_index)) + response = self.post("/snapshot/save") + + # Assert is saved + self.assertEqual(response.code, 200) + self.assertTrue(json.loads(response.body)) + + # Delete created snapshot + self.fetch("/snapshot/remove?id=" + str(snapshot_2_index)) + + # Try to save deleted snapshot + response = self.post("/snapshot/save") + self.assertEqual(response.code, 200) + self.assertFalse(json.loads(response.body)) + + # Clear + self.fetch("/snapshot/remove?id=" + str(snapshot_1_index)) + + def post(self, url): + self.http_client.fetch(HTTPRequest(self.get_url(url), "POST", allow_nonstandard_methods=True), self.stop) + return self.wait() + + def _save_as(self, name): + response = json.loads(self.fetch("/snapshot/saveas?title=" + name).body) + return response['id'] diff --git a/test/host/__init__.py b/test/host/__init__.py new file mode 100644 index 000000000..498793f03 --- /dev/null +++ b/test/host/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/test/host/host_hmi_snapshot_test.py b/test/host/host_hmi_snapshot_test.py new file mode 100644 index 000000000..3c985869b --- /dev/null +++ b/test/host/host_hmi_snapshot_test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +import unittest + +from mod.development import FakeHost, FakeHMI +from mod.protocol import Protocol +from mod.session import UserPreferences + + +def create_host(): + # return SESSION.host + + # Avoid to except "Command is already registered" + Protocol.COMMANDS_USED = [] + + callback_hmi = lambda: None + callback_host = lambda: None + + hmi = FakeHMI(callback_hmi) + return FakeHost(hmi, UserPreferences(), callback_host) + + +class HostHmiSnapshotTestCase(unittest.TestCase): + + def test(self): + # TODO + # host.hmi_list_pedalboard_snapshots() + # host.hmi_pedalboard_reorder_snapshots() + # host.hmi_pedalboard_snapshot_save() + # host.hmi_pedalboard_snapshot_delete() + # host.hmi_pedalboard_snapshot_load() + # host.hmi_snapshot_save() + # host.hmi_snapshot_load() + # host.hmi_clear_ss_name() + # host.hmi_report_ss_name_if_current() + pass + diff --git a/test/host/host_snapshot_test.py b/test/host/host_snapshot_test.py new file mode 100644 index 000000000..9033565c4 --- /dev/null +++ b/test/host/host_snapshot_test.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +import unittest +from uuid import uuid4 + +from tornado.gen import Task +from tornado.testing import AsyncTestCase, gen_test + +from mod.development import FakeHost, FakeHMI +from mod.protocol import Protocol +from mod.session import UserPreferences +from mod.settings import DEFAULT_SNAPSHOT_NAME + + +def create_host(): + # Avoid to except "Command is already registered" + Protocol.COMMANDS_USED = [] + + callback_hmi = lambda: None + message_callback = lambda text: print(text) + + hmi = FakeHMI(callback_hmi) + return FakeHost(hmi, UserPreferences(), message_callback) + + +class HostSnapshotTestCase(AsyncTestCase): + # OK + # host.snapshot_name() + # host.snapshot_rename() + # host.snapshot_save() + # host.snapshot_saveas() + # host.snapshot_remove() + # host.snapshot_clear() + + # Private methods + # host._snapshot_unique_name() + + # Doing + # host.snapshot_make() + # host.snapshot_load() + + # Consider change them to private method + # host.load_pb_snapshots() + # host.save_state_snapshots() + # host.readdress_snapshots() + + def test_empty_state(self): + host = create_host() + self.assertListEqual([], host.pedalboard_snapshots) + self.assertEqual(-1, host.current_pedalboard_snapshot_id) + + self.assertListEqual([None, None, None], host.hmi_snapshots) + + def test__snapshot_unique_name(self): + host = create_host() + + name = str(uuid4()) + self.assertEqual(name, host._snapshot_unique_name(name)) + + host.pedalboard_snapshots.append({'name': name}) + self.assertEqual(name + " (2)", host._snapshot_unique_name(name)) + host.pedalboard_snapshots.append({'name': name + " (2)"}) + self.assertEqual(name + " (3)", host._snapshot_unique_name(name)) + + def test_initial_snapshot_name(self): + host = create_host() + + self.assertEqual(None, host.snapshot_name()) + + def test_invalid_index_snapshot_name(self): + host = create_host() + + self.assertEqual(None, host.snapshot_name(-1)) + self.assertEqual(None, host.snapshot_name(1000)) + + def test_snapshot_name(self): + host = create_host() + + snapshot_0 = str(uuid4()) + snapshot_1 = str(uuid4()) + + host.snapshot_saveas(snapshot_0) + host.snapshot_saveas(snapshot_1) + + self.assertEqual(snapshot_0, host.snapshot_name(0)) + self.assertEqual(snapshot_1, host.snapshot_name(1)) + + def test_snapshot_rename_invalid_index(self): + host = create_host() + + name = "MOD" + + self.assertEqual(False, host.snapshot_rename(-1, name)) + self.assertEqual(False, host.snapshot_rename(1000, name)) + + self.assertFalse(host.pedalboard_modified) + + def test_snapshot_rename(self): + host = create_host() + + name = str(uuid4()) + new_name = str(uuid4()) + + self.assertNotEqual(name, new_name) + + # Prepare + host.snapshot_saveas(name) + self.assertEqual(name, host.snapshot_name(0)) + + host.pedalboard_modified = False + + # Rename + self.assertTrue(host.snapshot_rename(0, new_name)) + self.assertEqual(new_name, host.snapshot_name(0)) + self.assertTrue(host.pedalboard_modified) + + def test_snapshot_rename_same_name(self): + host = create_host() + + name = new_name = str(uuid4()) + self.assertEqual(name, new_name) + + # Prepare + host.snapshot_saveas(name) + self.assertEqual(name, host.snapshot_name(0)) + + host.pedalboard_modified = False + + # Rename + self.assertTrue(host.snapshot_rename(0, new_name)) + self.assertEqual(new_name, host.snapshot_name(0)) + self.assertFalse(host.pedalboard_modified) + + def test_snapshot_rename_duplicated_name(self): + host = create_host() + + snapshot_1 = str(uuid4()) + snapshot_2 = str(uuid4()) + snapshot_3 = str(uuid4()) + + # Prepare + host.snapshot_saveas(snapshot_1) + host.snapshot_saveas(snapshot_2) + host.snapshot_saveas(snapshot_3) + + # Rename + self.assertTrue(host.snapshot_rename(0, snapshot_3)) + self.assertTrue(host.snapshot_rename(1, snapshot_3)) + + self.assertEqual(snapshot_3 + " (2)", host.snapshot_name(0)) + self.assertEqual(snapshot_3 + " (3)", host.snapshot_name(1)) + + def test_snapshot_save_empty_snapshot_list(self): + host = create_host() + + # Ensure non empty for improve test quality + self.assertEqual(0, len(host.pedalboard_snapshots)) + self.assertEqual(-1, host.current_pedalboard_snapshot_id) + + self.assertFalse(host.pedalboard_modified) + + self.assertFalse(host.snapshot_save()) + self.assertFalse(host.pedalboard_modified) + + def test_snapshot_save(self): + host = create_host() + + # Create at least one snapshot for enable saving + host.snapshot_saveas("Test") + host.pedalboard_modified = False + + self.assertTrue(host.snapshot_save()) + self.assertTrue(host.pedalboard_modified) + + def test_snapshot_save_as_empty(self): + """ + The snapshot creation is based on current pedalboard status. + So, there isn't any problem if there are any snapshots + """ + host = create_host() + + self.assertListEqual([], host.pedalboard_snapshots) + + snapshot_1 = str(uuid4()) + + self.assertEqual(0, host.snapshot_saveas(snapshot_1)) + + def test_snapshot_save_as(self): + host = create_host() + + snapshot_1 = str(uuid4()) + snapshot_2 = str(uuid4()) + snapshot_3 = str(uuid4()) + + self.assertEqual(0, host.snapshot_saveas(snapshot_1)) + self.assertEqual(1, host.snapshot_saveas(snapshot_2)) + self.assertEqual(2, host.snapshot_saveas(snapshot_3)) + + def test_snapshot_saveas_duplicated_name(self): + host = create_host() + + common_name = str(uuid4()) + + self.assertEqual(0, host.snapshot_saveas(common_name)) + self.assertEqual(1, host.snapshot_saveas(common_name)) + self.assertEqual(2, host.snapshot_saveas(common_name)) + + self.assertEqual(common_name, host.snapshot_name(0)) + self.assertEqual(common_name + " (2)", host.snapshot_name(1)) + self.assertEqual(common_name + " (3)", host.snapshot_name(2)) + + def test_snapshot_make_empty_pedalboard(self): + host = create_host() + + name = str(uuid4()) + snapshot = host.snapshot_make(name) + + self.assertEqual(name, snapshot["name"]) + self.assertTrue("data" in snapshot) + + self.assertDictEqual({}, snapshot['data']) + + def test_snapshot_make(self): + # FIXME + pass + + @gen_test + def test_snapshot_load_invalid_index(self): + host = create_host() + + invalid_indexes = (-1, 1000) + for index in invalid_indexes: + loaded = yield Task(host.snapshot_load_gen_helper, index, False, {}) + self.assertFalse(loaded, msg="index sent: " + str(index)) + + @gen_test + def test_snapshot_load_idx_in_hmi_snapshot(self): + # FIXME + host = create_host() + + @gen_test + def test_snapshot_load_abort_catcher_is_true(self): + host = create_host() + + host.snapshot_saveas("first") + host.snapshot_saveas("second") + + abort_catcher = {'abort': True} + + loaded = yield Task(host.snapshot_load_gen_helper, 0, False, abort_catcher) + self.assertFalse(loaded) + + @gen_test + def test_snapshot_load(self): + # FIXME - Add plugins + host = create_host() + + host.snapshot_saveas("first") + host.snapshot_saveas("second") + + loaded = yield Task(host.snapshot_load_gen_helper, 0, False, {}) + self.assertTrue(loaded) + + def test_snapshot_remove_invalid_index(self): + host = create_host() + + # Ensure non empty for improve test quality + self.assertEqual(0, host.snapshot_saveas("test")) + + host.pedalboard_modified = False + + self.assertFalse(host.snapshot_remove(-1)) + self.assertFalse(host.pedalboard_modified) + self.assertFalse(host.snapshot_remove(100)) + self.assertFalse(host.pedalboard_modified) + + def test_snapshot_dont_remove_unique_snapshot(self): + host = create_host() + + # Ensure non empty for improve test quality + self.assertEqual(0, host.snapshot_saveas("test")) + self.assertEqual(1, host.snapshot_saveas("test 2")) + + self.assertEqual(2, len(host.pedalboard_snapshots)) + + host.pedalboard_modified = False + + self.assertTrue(host.snapshot_remove(0)) + self.assertTrue(host.pedalboard_modified) + + host.pedalboard_modified = False + + # Assert non delete unique snapshot + self.assertFalse(host.snapshot_remove(0)) + self.assertFalse(host.pedalboard_modified) + + self.assertEqual(1, len(host.pedalboard_snapshots)) + + def test_snapshot_remove_mark_pedalboard_as_modified(self): + host = create_host() + + # Ensure non empty for improve test quality + self.assertEqual(0, host.snapshot_saveas("test")) + self.assertEqual(1, host.snapshot_saveas("test 2")) + + self.assertEqual(2, len(host.pedalboard_snapshots)) + host.pedalboard_modified = False + + self.assertTrue(host.snapshot_remove(0)) + self.assertTrue(host.pedalboard_modified) + + def test_snapshot_remove_current_snapshot(self): + host = create_host() + + self.assertEqual(0, host.snapshot_saveas("test")) + self.assertEqual(0, host.current_pedalboard_snapshot_id) + + self.assertEqual(1, host.snapshot_saveas("test 2")) + self.assertEqual(1, host.current_pedalboard_snapshot_id) + + self.assertEqual(2, len(host.pedalboard_snapshots)) + host.pedalboard_modified = False + + # Current snapshot is the hast one + self.assertTrue(host.snapshot_remove(1)) + self.assertTrue(host.pedalboard_modified) + self.assertEqual(-1, host.current_pedalboard_snapshot_id) + + def test_snapshot_clear(self): + host = create_host() + + # Ensure non empty for improve test quality + self.assertEqual(0, len(host.pedalboard_snapshots)) + self.assertEqual(-1, host.current_pedalboard_snapshot_id) + + self.assertFalse(host.pedalboard_modified) + host.snapshot_clear() + self.assertTrue(host.pedalboard_modified) + + self.assertEqual(1, len(host.pedalboard_snapshots)) + self.assertEqual(0, host.current_pedalboard_snapshot_id) + + self.assertEqual(DEFAULT_SNAPSHOT_NAME, host.snapshot_name(0))