From f1749e4873161c00fde48b00209b97c97cf7c470 Mon Sep 17 00:00:00 2001 From: "FeRD (Frank Dana)" Date: Fri, 28 May 2021 05:06:02 -0400 Subject: [PATCH 1/5] Unit tests: Add test for filter(intersect=) Also, make the test data slightly more interesting by varying Position values for the clips/transitions in the test data. --- src/tests/query_tests.py | 72 +++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/src/tests/query_tests.py b/src/tests/query_tests.py index c199064be4..69eeb1ba69 100644 --- a/src/tests/query_tests.py +++ b/src/tests/query_tests.py @@ -27,18 +27,17 @@ import sys import os -import random -import unittest -import uuid import json +import unittest + import openshot from PyQt5.QtGui import QGuiApplication try: # QtWebEngineWidgets must be loaded prior to creating a QApplication # But on systems with only WebKit, this will fail (and we ignore the failure) - from PyQt5.QtWebEngineWidgets import QWebEngineView + from PyQt5.QtWebEngineWidgets import QWebEngineView # noqa except ImportError: pass @@ -50,7 +49,6 @@ from classes.app import OpenShotApp from classes import info - app = None class TestQueryClass(unittest.TestCase): @@ -68,10 +66,14 @@ def setUpClass(TestQueryClass): # Import additional classes that need the app defined first from classes.query import Clip, File, Transition + clips = [] + # Insert some clips into the project data for num in range(5): # Create clip c = openshot.Clip(os.path.join(info.IMAGES_PATH, "AboutLogo.png")) + c.Position(num * 10.0) + c.End(5.0) # Parse JSON clip_data = json.loads(c.Json()) @@ -83,6 +85,7 @@ def setUpClass(TestQueryClass): # Keep track of the ids TestQueryClass.clip_ids.append(query_clip.id) + clips.append(query_clip) # Insert some files into the project data for num in range(5): @@ -103,12 +106,18 @@ def setUpClass(TestQueryClass): TestQueryClass.file_ids.append(query_file.id) # Insert some transitions into the project data - for num in range(5): + for c in clips: # Create mask object - transition_object = openshot.Mask() - transitions_data = json.loads(transition_object.Json()) + t = openshot.Mask() + # Place over the last second of current clip + pos = c.data.get("position", 0.0) + start = c.data.get("start", 0.0) + end = c.data.get("end", 0.0) + t.Position((pos - start + end) - 1.0) + t.End(1.0) # Insert into project data + transitions_data = json.loads(t.Json()) query_transition = Transition() query_transition.data = transitions_data query_transition.save() @@ -116,6 +125,9 @@ def setUpClass(TestQueryClass): # Keep track of the ids TestQueryClass.transition_ids.append(query_transition.id) + # Don't keep the full query objects around + del clips + @classmethod def tearDownClass(cls): "Hook method for deconstructing the class fixture after running all tests in the class." @@ -224,6 +236,50 @@ def test_get_clip(self): clip = Clip.get(id="invalidID") self.assertEqual(clip, None) + def test_intersect(self): + """ Test special filter argument 'intersect' """ + + # Import additional classes that need the app defined first + from classes.query import Clip, Transition + + trans = Transition.get(id=self.transition_ids[0]) + self.assertTrue(trans) + + pos = trans.data.get("position", -1.0) + duration = trans.data.get("duration", -1.0) + self.assertTrue(pos >= 0.0) + self.assertTrue(duration >= 0.0) + + time = pos + (duration / 2) + + def get_times(item): + pos = item.data.get("position", -1.0) + end = pos + item.data.get("duration", -1.0) + return (pos, end) + + t_intersect = Transition.filter(intersect=time) + t_ids = [t.id for t in t_intersect] + t_all = Transition.filter() + t_rest = [x for x in t_all if x.id not in t_ids] + + c_intersect = Clip.filter(intersect=time) + c_ids = [c.id for c in c_intersect] + c_all = Clip.filter() + c_rest = [x for x in c_all if x.id not in c_ids] + + for item in t_intersect + c_intersect: + item_id = item.id + pos, end = get_times(item) + self.assertTrue(pos <= time) + self.assertTrue(time <= end) + for item in t_rest + c_rest: + item_id = item.id + pos, end = get_times(item) + if pos < time: + self.assertTrue(end <= time) + if end > time: + self.assertTrue(pos >= time) + def test_update_File(self): """ Test the File.save method """ From cc29a507ebc5ff1f6e5020fb9c935a0917adb832 Mon Sep 17 00:00:00 2001 From: "FeRD (Frank Dana)" Date: Fri, 28 May 2021 05:41:50 -0400 Subject: [PATCH 2/5] classes.query: Don't hold reference to app.project By calling get_app() each time we need to access the project data, we delay the lookup until it's needed. As a result, query classes can now be imported even if an application instance has not been initialized. --- src/classes/query.py | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/classes/query.py b/src/classes/query.py index 658d054f4b..e97c312d12 100644 --- a/src/classes/query.py +++ b/src/classes/query.py @@ -32,11 +32,6 @@ from classes.app import get_app -# Get project data reference -app = get_app() -project = app.project - - class QueryObject: """ This class allows one or more project data objects to be queried """ @@ -56,7 +51,7 @@ def save(self, OBJECT_TYPE): if not self.id and self.type == "insert": # Insert record, and Generate id - self.id = project.generate_id() + self.id = get_app().project.generate_id() # save id in data (if attribute found) self.data["id"] = copy.deepcopy(self.id) @@ -67,7 +62,7 @@ def save(self, OBJECT_TYPE): self.key.append({"id": self.id}) # Insert into project data - app.updates.insert(copy.deepcopy(OBJECT_TYPE.object_key), copy.deepcopy(self.data)) + get_app().updates.insert(copy.deepcopy(OBJECT_TYPE.object_key), copy.deepcopy(self.data)) # Mark record as 'update' now... so another call to this method won't insert it again self.type = "update" @@ -75,7 +70,7 @@ def save(self, OBJECT_TYPE): elif self.id and self.type == "update": # Update existing project data - app.updates.update(self.key, self.data) + get_app().updates.update(self.key, self.data) def delete(self, OBJECT_TYPE): """ Delete the object from the project data store """ @@ -83,7 +78,7 @@ def delete(self, OBJECT_TYPE): # Delete if object found and not pending insert if self.id and self.type == "update": # Delete from project data store - app.updates.delete(self.key) + get_app().updates.delete(self.key) self.type = "delete" def title(self): @@ -95,7 +90,7 @@ def filter(OBJECT_TYPE, **kwargs): """ Take any arguments given as filters, and find a list of matching objects """ # Get a list of all objects of this type - parent = project.get(OBJECT_TYPE.object_key) + parent = get_app().project.get(OBJECT_TYPE.object_key) if not parent: return [] @@ -242,30 +237,25 @@ def get(**kwargs): def absolute_path(self): """ Get absolute file path of file """ - # Get project folder (if any) - project_folder = None - if project.current_filepath: - project_folder = os.path.dirname(project.current_filepath) - - # Convert relative file path into absolute (if needed) file_path = self.data["path"] - if not os.path.isabs(file_path) and project_folder: - file_path = os.path.abspath(os.path.join(project_folder, self.data["path"])) + if os.path.isabs(file_path): + return file_path + + # Try to expand path relative to project folder + app = get_app() + if (app and hasattr("project", app) + and hasattr("current_filepath", app.project)): + project_folder = os.path.dirname(app.project.current_filepath) + file_path = os.path.abspath(os.path.join(project_folder, file_path)) - # Return absolute path of file return file_path def relative_path(self): """ Get relative path (based on the current working directory) """ - # Get absolute file path file_path = self.absolute_path() - # Convert path to relative (based on current working directory of Python) - file_path = os.path.relpath(file_path, info.CWD) - - # Return relative path - return file_path + return os.path.relpath(file_path, info.CWD) class Marker(QueryObject): @@ -317,6 +307,7 @@ def __lt__(self, other): def __gt__(self, other): return self.data.get('number', 0) > other.data.get('number', 0) + class Effect(QueryObject): """ This class allows Effects to be queried, updated, and deleted from the project data. """ object_name = "effects" # Derived classes should define this @@ -334,7 +325,7 @@ def filter(**kwargs): """ Take any arguments given as filters, and find a list of matching objects """ # Get a list of clips - clips = project.get("clips") + clips = get_app().project.get("clips") matching_objects = [] # Loop through all clips From 3fa4308045eae9c7891b59ea934f67be910273b9 Mon Sep 17 00:00:00 2001 From: "FeRD (Frank Dana)" Date: Fri, 28 May 2021 05:45:17 -0400 Subject: [PATCH 3/5] Unit tests: Import query classes at top of file --- src/tests/query_tests.py | 66 +++------------------------------------- 1 file changed, 5 insertions(+), 61 deletions(-) diff --git a/src/tests/query_tests.py b/src/tests/query_tests.py index 69eeb1ba69..d24eb69cb5 100644 --- a/src/tests/query_tests.py +++ b/src/tests/query_tests.py @@ -47,6 +47,7 @@ sys.path.append(PATH) from classes.app import OpenShotApp +from classes.query import Clip, File, Transition from classes import info app = None @@ -130,22 +131,16 @@ def setUpClass(TestQueryClass): @classmethod def tearDownClass(cls): - "Hook method for deconstructing the class fixture after running all tests in the class." - TestQueryClass.app.quit() + """ Clean up after running all tests in the class. """ + cls.app.quit() def test_add_clip(self): - """ Test the Clip.save method by adding multiple clips """ - - # Import additional classes that need the app defined first - from classes.query import Clip # Find number of clips in project num_clips = len(Clip.filter()) # Create clip c = openshot.Clip(os.path.join(info.IMAGES_PATH, "AboutLogo.png")) - - # Parse JSON clip_data = json.loads(c.Json()) # Insert into project data @@ -158,16 +153,11 @@ def test_add_clip(self): # Save the clip again (which should not change the total # of clips) query_clip.save() - self.assertEqual(len(Clip.filter()), num_clips + 1) def test_update_clip(self): """ Test the Clip.save method """ - # Import additional classes that need the app defined first - from classes.query import Clip - - # Find a clip named file1 update_id = TestQueryClass.clip_ids[0] clip = Clip.get(id=update_id) self.assertTrue(clip) @@ -178,7 +168,6 @@ def test_update_clip(self): clip.save() # Verify updated data - # Get clip again clip = Clip.get(id=update_id) self.assertEqual(clip.data["layer"], 2) self.assertEqual(clip.data["title"], "My Title") @@ -186,15 +175,10 @@ def test_update_clip(self): def test_delete_clip(self): """ Test the Clip.delete method """ - # Import additional classes that need the app defined first - from classes.query import Clip - - # Find a clip named file1 delete_id = TestQueryClass.clip_ids[4] clip = Clip.get(id=delete_id) self.assertTrue(clip) - # Delete clip clip.delete() # Verify deleted data @@ -203,18 +187,12 @@ def test_delete_clip(self): # Delete clip again (should do nothing) clip.delete() - - # Verify deleted data deleted_clip = Clip.get(id=delete_id) self.assertFalse(deleted_clip) def test_filter_clip(self): """ Test the Clip.filter method """ - # Import additional classes that need the app defined first - from classes.query import Clip - - # Find all clips named file1 clips = Clip.filter(id=TestQueryClass.clip_ids[0]) self.assertTrue(clips) @@ -225,10 +203,6 @@ def test_filter_clip(self): def test_get_clip(self): """ Test the Clip.get method """ - # Import additional classes that need the app defined first - from classes.query import Clip - - # Find a clip named file1 clip = Clip.get(id=TestQueryClass.clip_ids[1]) self.assertTrue(clip) @@ -239,9 +213,6 @@ def test_get_clip(self): def test_intersect(self): """ Test special filter argument 'intersect' """ - # Import additional classes that need the app defined first - from classes.query import Clip, Transition - trans = Transition.get(id=self.transition_ids[0]) self.assertTrue(trans) @@ -283,10 +254,6 @@ def get_times(item): def test_update_File(self): """ Test the File.save method """ - # Import additional classes that need the app defined first - from classes.query import File - - # Find a File named file1 update_id = TestQueryClass.file_ids[0] file = File.get(id=update_id) self.assertTrue(file) @@ -297,7 +264,6 @@ def test_update_File(self): file.save() # Verify updated data - # Get File again file = File.get(id=update_id) self.assertEqual(file.data["height"], 1080) self.assertEqual(file.data["width"], 1920) @@ -305,35 +271,24 @@ def test_update_File(self): def test_delete_File(self): """ Test the File.delete method """ - # Import additional classes that need the app defined first - from classes.query import File - - # Find a File named file1 delete_id = TestQueryClass.file_ids[4] file = File.get(id=delete_id) self.assertTrue(file) - # Delete File file.delete() # Verify deleted data deleted_file = File.get(id=delete_id) self.assertFalse(deleted_file) - # Delete File again (should do nothing + # Delete File again (should do nothing) file.delete() - - # Verify deleted data deleted_file = File.get(id=delete_id) self.assertFalse(deleted_file) def test_filter_File(self): """ Test the File.filter method """ - # Import additional classes that need the app defined first - from classes.query import File - - # Find all Files named file1 files = File.filter(id=TestQueryClass.file_ids[0]) self.assertTrue(files) @@ -344,10 +299,6 @@ def test_filter_File(self): def test_get_File(self): """ Test the File.get method """ - # Import additional classes that need the app defined first - from classes.query import File - - # Find a File named file1 file = File.get(id=TestQueryClass.file_ids[1]) self.assertTrue(file) @@ -356,18 +307,12 @@ def test_get_File(self): self.assertEqual(file, None) def test_add_file(self): - """ Test the File.save method by adding multiple files """ - - # Import additional classes that need the app defined first - from classes.query import File # Find number of files in project num_files = len(File.filter()) # Create file r = openshot.DummyReader(openshot.Fraction(24, 1), 640, 480, 44100, 2, 30.0) - - # Parse JSON file_data = json.loads(r.Json()) # Insert into project data @@ -375,14 +320,13 @@ def test_add_file(self): query_file.data = file_data query_file.data["path"] = os.path.join(info.IMAGES_PATH, "AboutLogo.png") query_file.data["media_type"] = "image" - query_file.save() + query_file.save() self.assertTrue(query_file) self.assertEqual(len(File.filter()), num_files + 1) # Save the file again (which should not change the total # of files) query_file.save() - self.assertEqual(len(File.filter()), num_files + 1) From 43da4ac689a35f275462b4f2c67e7cbaf18c60cb Mon Sep 17 00:00:00 2001 From: Frank Dana Date: Tue, 29 Jun 2021 05:34:26 -0700 Subject: [PATCH 4/5] src/classes/query.py: Fix indentation --- src/classes/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/query.py b/src/classes/query.py index e97c312d12..f1aed97a0d 100644 --- a/src/classes/query.py +++ b/src/classes/query.py @@ -241,7 +241,7 @@ def absolute_path(self): if os.path.isabs(file_path): return file_path - # Try to expand path relative to project folder + # Try to expand path relative to project folder app = get_app() if (app and hasattr("project", app) and hasattr("current_filepath", app.project)): From f5b978f34812043152f09c05cf3da8c8f37908b0 Mon Sep 17 00:00:00 2001 From: "FeRD (Frank Dana)" Date: Tue, 29 Jun 2021 08:46:20 -0400 Subject: [PATCH 5/5] unit tests: Rename test class, use inheritance - Rename the `TestQueryClass` class to `QueryTests`, which is more in keeping with unittest norms. - Replace all explicit uses of `TestQueryClass` throughout the code with `cls` or `self` as appropriate, to eliminate friction against any future name changes. --- src/tests/query_tests.py | 41 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/tests/query_tests.py b/src/tests/query_tests.py index d24eb69cb5..afdb770ee1 100644 --- a/src/tests/query_tests.py +++ b/src/tests/query_tests.py @@ -52,20 +52,18 @@ app = None -class TestQueryClass(unittest.TestCase): + +class QueryTests(unittest.TestCase): """ Unit test class for Query class """ @classmethod - def setUpClass(TestQueryClass): + def setUpClass(cls): """ Init unit test data """ # Create Qt application - TestQueryClass.app = QGuiApplication.instance() - TestQueryClass.clip_ids = [] - TestQueryClass.file_ids = [] - TestQueryClass.transition_ids = [] - - # Import additional classes that need the app defined first - from classes.query import Clip, File, Transition + cls.app = QGuiApplication.instance() + cls.clip_ids = [] + cls.file_ids = [] + cls.transition_ids = [] clips = [] @@ -85,7 +83,7 @@ def setUpClass(TestQueryClass): query_clip.save() # Keep track of the ids - TestQueryClass.clip_ids.append(query_clip.id) + cls.clip_ids.append(query_clip.id) clips.append(query_clip) # Insert some files into the project data @@ -104,7 +102,7 @@ def setUpClass(TestQueryClass): query_file.save() # Keep track of the ids - TestQueryClass.file_ids.append(query_file.id) + cls.file_ids.append(query_file.id) # Insert some transitions into the project data for c in clips: @@ -124,7 +122,7 @@ def setUpClass(TestQueryClass): query_transition.save() # Keep track of the ids - TestQueryClass.transition_ids.append(query_transition.id) + cls.transition_ids.append(query_transition.id) # Don't keep the full query objects around del clips @@ -158,7 +156,7 @@ def test_add_clip(self): def test_update_clip(self): """ Test the Clip.save method """ - update_id = TestQueryClass.clip_ids[0] + update_id = self.clip_ids[0] clip = Clip.get(id=update_id) self.assertTrue(clip) @@ -172,10 +170,13 @@ def test_update_clip(self): self.assertEqual(clip.data["layer"], 2) self.assertEqual(clip.data["title"], "My Title") + clips = Clip.filter(layer=2) + self.assertEqual(len(clips), 1) + def test_delete_clip(self): """ Test the Clip.delete method """ - delete_id = TestQueryClass.clip_ids[4] + delete_id = self.clip_ids[4] clip = Clip.get(id=delete_id) self.assertTrue(clip) @@ -193,7 +194,7 @@ def test_delete_clip(self): def test_filter_clip(self): """ Test the Clip.filter method """ - clips = Clip.filter(id=TestQueryClass.clip_ids[0]) + clips = Clip.filter(id=self.clip_ids[0]) self.assertTrue(clips) # Do not find a clip @@ -203,7 +204,7 @@ def test_filter_clip(self): def test_get_clip(self): """ Test the Clip.get method """ - clip = Clip.get(id=TestQueryClass.clip_ids[1]) + clip = Clip.get(id=self.clip_ids[1]) self.assertTrue(clip) # Do not find a clip @@ -254,7 +255,7 @@ def get_times(item): def test_update_File(self): """ Test the File.save method """ - update_id = TestQueryClass.file_ids[0] + update_id = self.file_ids[0] file = File.get(id=update_id) self.assertTrue(file) @@ -271,7 +272,7 @@ def test_update_File(self): def test_delete_File(self): """ Test the File.delete method """ - delete_id = TestQueryClass.file_ids[4] + delete_id = self.file_ids[4] file = File.get(id=delete_id) self.assertTrue(file) @@ -289,7 +290,7 @@ def test_delete_File(self): def test_filter_File(self): """ Test the File.filter method """ - files = File.filter(id=TestQueryClass.file_ids[0]) + files = File.filter(id=self.file_ids[0]) self.assertTrue(files) # Do not find a File @@ -299,7 +300,7 @@ def test_filter_File(self): def test_get_File(self): """ Test the File.get method """ - file = File.get(id=TestQueryClass.file_ids[1]) + file = File.get(id=self.file_ids[1]) self.assertTrue(file) # Do not find a File