movie-barcodes 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of movie-barcodes might be problematic. Click here for more details.

tests/test_utility.py ADDED
@@ -0,0 +1,320 @@
1
+ import unittest
2
+ import argparse
3
+ from unittest.mock import patch, MagicMock, Mock
4
+ import cv2
5
+ import numpy as np
6
+
7
+
8
+ from src import utility
9
+
10
+
11
+ class TestUtility(unittest.TestCase):
12
+ """
13
+ Test the utility functions.
14
+ """
15
+
16
+ def setUp(self) -> None:
17
+ """
18
+ Set up the test case.
19
+ :return: None
20
+ """
21
+ super().setUp()
22
+ self.args = argparse.Namespace(
23
+ input_video_path="test_video.mp4",
24
+ destination_path="/test/output.png",
25
+ workers=4,
26
+ width=200,
27
+ all_methods=False,
28
+ method="avg",
29
+ )
30
+ self.frame_count = 300
31
+ self.MAX_PROCESSES = 8
32
+ self.MIN_FRAME_COUNT = 100
33
+ # Mock setup for path.exists and access to ensure they return True by default
34
+ patcher_exists = patch("src.utility.path.exists", return_value=True)
35
+ patcher_access = patch("src.utility.access", return_value=True)
36
+ self.addCleanup(patcher_exists.stop)
37
+ self.addCleanup(patcher_access.stop)
38
+ self.mock_exists = patcher_exists.start()
39
+ self.mock_access = patcher_access.start()
40
+
41
+ @patch("src.utility.path.exists")
42
+ def test_file_not_found_error(self, mock_exists: MagicMock) -> None:
43
+ """
44
+ Test that validate_args raises a FileNotFoundError when the input video file does not exist.
45
+ :param mock_exists: MagicMock object for path.exists function to return False
46
+ :return: None
47
+ """
48
+ mock_exists.return_value = False
49
+ with self.assertRaises(FileNotFoundError):
50
+ utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
51
+
52
+ @patch("src.utility.path.exists")
53
+ def test_invalid_extension_error(self, mock_exists: MagicMock) -> None:
54
+ """
55
+ Test that validate_args raises a ValueError when the input video file has an invalid extension.
56
+ :param mock_exists: MagicMock object for path.exists function to return True
57
+ :return: None
58
+ """
59
+ mock_exists.return_value = True
60
+ self.args.input_video_path = "invalid_extension.txt"
61
+ with self.assertRaises(ValueError):
62
+ utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
63
+
64
+ @patch("src.utility.path.exists")
65
+ @patch("src.utility.access")
66
+ def test_destination_path_not_writable(self, mock_access: MagicMock, mock_exists: MagicMock) -> None:
67
+ """
68
+ Test that validate_args raises a PermissionError when the destination path is not writable.
69
+ :param mock_access: MagicMock object for os.access function to return False
70
+ :param mock_exists: MagicMock object for path.exists function to return True
71
+ :return: None
72
+ """
73
+ mock_exists.return_value = True
74
+ mock_access.return_value = False
75
+ with self.assertRaises(PermissionError):
76
+ utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
77
+
78
+ @patch("src.utility.path.exists")
79
+ def test_workers_value_error(self, mock_exists: MagicMock) -> None:
80
+ """
81
+ Test that validate_args raises a ValueError when the number of workers is invalid.
82
+ :param mock_exists: MagicMock object for path.exists function to return True
83
+ :return: None
84
+ """
85
+ mock_exists.return_value = True
86
+ self.args.workers = 0 # Testing for < 1
87
+ with self.assertRaises(ValueError):
88
+ utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
89
+
90
+ self.args.workers = self.MAX_PROCESSES + 1 # Testing for > MAX_PROCESSES
91
+ with self.assertRaises(ValueError):
92
+ utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
93
+
94
+ def test_invalid_width(self) -> None:
95
+ """
96
+ Test that validate_args raises a ValueError when the width is invalid.
97
+ :return: None
98
+ """
99
+ self.args.width = 0 # Testing for <= 0
100
+ with self.assertRaises(ValueError):
101
+ utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
102
+
103
+ self.args.width = self.frame_count + 1 # Testing for > frame_count
104
+ with self.assertRaises(ValueError):
105
+ utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
106
+
107
+ def test_frame_count_too_low(self) -> None:
108
+ """
109
+ Test that validate_args raises a ValueError when the frame count is too low.
110
+ :return: None
111
+ """
112
+ low_frame_count = self.MIN_FRAME_COUNT - 1
113
+ with self.assertRaises(ValueError):
114
+ utility.validate_args(self.args, low_frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
115
+
116
+ def test_all_methods_and_method_error(self) -> None:
117
+ """
118
+ Test that validate_args raises a ValueError when the --all_methods flag is used with the --method argument.
119
+ :return: None
120
+ """
121
+ self.args.all_methods = True
122
+ self.args.method = "avg" # Explicitly setting method to simulate conflict
123
+ with self.assertRaises(ValueError):
124
+ utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
125
+
126
+ def test_no_error_raised(self) -> None:
127
+ """
128
+ Test that no error is raised when all arguments are valid.
129
+ :return: None
130
+ """
131
+ utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
132
+
133
+ @patch("src.utility.makedirs")
134
+ @patch("src.utility.path.exists")
135
+ def test_ensure_directory_creates_directory(self, mock_exists: MagicMock, mock_makedirs: MagicMock) -> None:
136
+ """
137
+ Test ensure_directory creates the directory when it does not exist.
138
+ :param mock_exists: MagicMock object for path.exists function to return False
139
+ :param mock_makedirs: MagicMock object for os.makedirs function
140
+ :return: None
141
+ """
142
+ mock_exists.return_value = False # Simulate directory does not exist
143
+
144
+ utility.ensure_directory(self.args.destination_path)
145
+
146
+ mock_exists.assert_called_once_with(self.args.destination_path)
147
+ mock_makedirs.assert_called_once_with(self.args.destination_path)
148
+
149
+ def test_format_time_seconds_only(self) -> None:
150
+ """
151
+ Test that format_time correctly formats times less than 60 seconds.
152
+ :return: None
153
+ """
154
+ time_seconds = 45.0 # 45 seconds
155
+ expected_format = "0m 45s"
156
+ self.assertEqual(utility.format_time(time_seconds), expected_format)
157
+
158
+ def test_format_time_minutes_and_seconds(self) -> None:
159
+ """
160
+ Test that format_time correctly formats times between 1 minute and less than 1 hour.
161
+ :return: None
162
+ """
163
+ time_seconds = 95.0 # 1 minute and 35 seconds
164
+ expected_format = "1m 35s"
165
+ self.assertEqual(utility.format_time(time_seconds), expected_format)
166
+
167
+ def test_format_time_hours_minutes_seconds(self) -> None:
168
+ """
169
+ Test that format_time correctly formats times with hours, minutes, and seconds.
170
+ :return: None
171
+ """
172
+ time_seconds = 3725.0 # 1 hour, 2 minutes, and 5 seconds
173
+ expected_format = "1h 2m 5s"
174
+ self.assertEqual(utility.format_time(time_seconds), expected_format)
175
+
176
+ def test_format_time_rounding(self) -> None:
177
+ """
178
+ Test that format_time correctly rounds seconds.
179
+ :return: None
180
+ """
181
+ time_seconds = 3661.6 # Should round to 1 hour, 1 minute, and 1 seconds
182
+ expected_format = "1h 1m 1s"
183
+ self.assertEqual(utility.format_time(time_seconds), expected_format)
184
+
185
+ def test_get_dominant_color_function_with_invalid_method(self) -> None:
186
+ """
187
+ Test that get_dominant_color_function raises ValueError for unsupported methods.
188
+ :return: None
189
+ """
190
+ with self.assertRaises(ValueError):
191
+ utility.get_dominant_color_function("unsupported_method")
192
+
193
+ def test_get_dominant_color_function_with_valid_methods(self) -> None:
194
+ """
195
+ Test that get_dominant_color_function returns a callable for supported methods.
196
+ :return: None
197
+ """
198
+ valid_methods = ["avg", "kmeans", "hsv", "bgr"]
199
+ for method in valid_methods:
200
+ result = utility.get_dominant_color_function(method)
201
+ self.assertTrue(callable(result), f"Method '{method}' should return a callable.")
202
+
203
+ def test_get_dominant_color_function_returns_specific_function(self) -> None:
204
+ """
205
+ Test that get_dominant_color_function returns the specific function associated with a method.
206
+ :return: None
207
+ """
208
+ # Mocking the specific functions to test if the correct one is returned
209
+ utility.get_dominant_color_mean = Mock(name="get_dominant_color_mean")
210
+ utility.get_dominant_color_kmeans = Mock(name="get_dominant_color_kmeans")
211
+ utility.get_dominant_color_hsv = Mock(name="get_dominant_color_hsv")
212
+ utility.get_dominant_color_bgr = Mock(name="get_dominant_color_bgr")
213
+
214
+ self.assertEqual(utility.get_dominant_color_function("avg"), utility.get_dominant_color_mean)
215
+ self.assertEqual(
216
+ utility.get_dominant_color_function("kmeans"),
217
+ utility.get_dominant_color_kmeans,
218
+ )
219
+ self.assertEqual(utility.get_dominant_color_function("hsv"), utility.get_dominant_color_hsv)
220
+ self.assertEqual(utility.get_dominant_color_function("bgr"), utility.get_dominant_color_bgr)
221
+
222
+ @patch("src.utility.path.getsize")
223
+ def test_get_video_properties(self, mock_getsize: MagicMock) -> None:
224
+ """
225
+ Test that get_video_properties correctly extracts video properties.
226
+ :param mock_getsize: MagicMock object for path.getsize function
227
+ :return: None
228
+ """
229
+ # Setup mock return values
230
+ total_frames = 100
231
+ fps = 25
232
+ video_duration = total_frames / fps
233
+ video_size = 1024000 # in bytes
234
+
235
+ # Configure the mock objects
236
+ mock_video = MagicMock()
237
+ mock_video.get.side_effect = [
238
+ total_frames,
239
+ fps,
240
+ ] # CAP_PROP_FRAME_COUNT, CAP_PROP_FPS
241
+ mock_getsize.return_value = video_size
242
+
243
+ args = argparse.Namespace(input_video_path="test/sample.mp4")
244
+
245
+ # Call the function with the mocked objects/return values
246
+ result = utility.get_video_properties(mock_video, args)
247
+
248
+ # Assert that the function returns the expected tuple
249
+ expected_result = (total_frames, fps, video_duration, video_size)
250
+ self.assertEqual(result, expected_result)
251
+
252
+ # Verify that the mocks were called as expected
253
+ mock_video.get.assert_any_call(cv2.CAP_PROP_FRAME_COUNT)
254
+ mock_video.get.assert_any_call(cv2.CAP_PROP_FPS)
255
+ mock_getsize.assert_called_once_with(args.input_video_path)
256
+
257
+ @patch("src.utility.path.join")
258
+ @patch("src.utility.Image.fromarray")
259
+ @patch("src.utility.path.dirname")
260
+ @patch("src.utility.path.abspath")
261
+ def test_save_barcode_image_variations(
262
+ self,
263
+ mock_abspath: MagicMock,
264
+ mock_dirname: MagicMock,
265
+ mock_fromarray: MagicMock,
266
+ mock_path_join: MagicMock,
267
+ ) -> None:
268
+ """
269
+ Test variations of save_barcode_image behavior based on different argument conditions.
270
+ :param mock_abspath: MagicMock object for path.abspath function
271
+ :param mock_dirname: MagicMock object for path.dirname function
272
+ :param mock_fromarray: MagicMock object for Image.fromarray function
273
+ :param mock_path_join: MagicMock object for path.join function
274
+ :return: None
275
+ """
276
+ # Mock the directory and path handling
277
+ mock_abspath.return_value = "/fake/dir/module.py"
278
+ mock_dirname.side_effect = lambda x: x.rsplit("/", 1)[0] # Simulates dirname behavior
279
+ mock_path_join.side_effect = lambda *args: "/".join(args) # Simulates os.path.join behavior
280
+
281
+ # Setup a fake barcode and base name
282
+ barcode = np.zeros((100, 100, 3), dtype=np.uint8)
283
+ base_name = "video_sample"
284
+
285
+ # Test without workers and without output name
286
+ args_without_workers = argparse.Namespace(
287
+ destination_path=None, output_name=None, barcode_type="type1", workers=None
288
+ )
289
+ utility.save_barcode_image(barcode, base_name, args_without_workers, "avg")
290
+ self.assertTrue("workers_" not in mock_path_join.call_args[0][-1])
291
+
292
+ # Test with workers
293
+ args_with_workers = argparse.Namespace(destination_path=None, output_name=None, barcode_type="type1", workers=4)
294
+ utility.save_barcode_image(barcode, base_name, args_with_workers, "avg")
295
+ self.assertTrue("workers_4" in mock_path_join.call_args[0][-1])
296
+
297
+ # Test with output name
298
+ args_with_output_name = argparse.Namespace(
299
+ destination_path=None,
300
+ output_name="custom_name",
301
+ barcode_type="type1",
302
+ workers=None,
303
+ )
304
+ utility.save_barcode_image(barcode, base_name, args_with_output_name, "avg")
305
+ self.assertIn("custom_name.png", mock_path_join.call_args[0][-1])
306
+
307
+ # Test file naming without output name
308
+ utility.save_barcode_image(barcode, base_name, args_without_workers, "avg")
309
+ expected_name_parts = [base_name, "avg", "type1"]
310
+ expected_name = "_".join(expected_name_parts) + ".png"
311
+ self.assertIn(expected_name, mock_path_join.call_args[0][-1])
312
+
313
+ # Test image saving
314
+ mock_image = mock_fromarray.return_value
315
+ utility.save_barcode_image(barcode, base_name, args_without_workers, "avg")
316
+ mock_image.save.assert_called() # Ensure the image is attempted to be saved
317
+
318
+
319
+ if __name__ == "__main__":
320
+ unittest.main()
@@ -0,0 +1,232 @@
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock
3
+ from src import video_processing
4
+
5
+
6
+ class TestVideoProcessing(unittest.TestCase):
7
+ """
8
+ Test the video processing functions.
9
+ """
10
+
11
+ def setUp(self) -> None:
12
+ """
13
+ Set up the test case.
14
+ :return: None
15
+ """
16
+ self.video_path = "dummy_video.mp4"
17
+ self.start_frame = 0
18
+ self.end_frame = 49
19
+ self.target_frames = 5
20
+
21
+ @staticmethod
22
+ def mock_color_extractor(frame):
23
+ """
24
+ Mock color extractor function.
25
+ :param frame:
26
+ :return: frame
27
+ """
28
+ return frame
29
+
30
+ @patch("src.video_processing.cv2.VideoCapture")
31
+ def test_load_video_raises_error_on_file_not_open(self, mock_video: MagicMock) -> None:
32
+ """
33
+ Test load_video raises ValueError when the video file cannot be opened.
34
+ :param mock_video: MagicMock object for cv2.VideoCapture
35
+ :return: None
36
+ """
37
+ mock_video.return_value.isOpened.return_value = False
38
+ with self.assertRaises(ValueError) as context:
39
+ video_processing.load_video(self.video_path)
40
+ self.assertIn(
41
+ "Could not open the video file: " + self.video_path,
42
+ str(context.exception),
43
+ )
44
+
45
+ @patch("src.video_processing.cv2.VideoCapture")
46
+ def test_load_video_raises_error_on_no_frames(self, mock_video: MagicMock) -> None:
47
+ """
48
+ Test load_video raises ValueError when the video has no frames.
49
+ :param mock_video: MagicMock object for cv2.VideoCapture
50
+ :return: None
51
+ """
52
+ mock_video.return_value.isOpened.return_value = True
53
+ mock_video.return_value.get.return_value = 0 # Mocking frame count as 0
54
+ with self.assertRaises(ValueError) as context:
55
+ video_processing.load_video(self.video_path)
56
+ self.assertIn(
57
+ "The video file " + self.video_path + " has no frames.",
58
+ str(context.exception),
59
+ )
60
+
61
+ @patch("src.video_processing.cv2.VideoCapture")
62
+ def test_load_video_raises_error_on_invalid_dimensions(self, mock_video: MagicMock) -> None:
63
+ """
64
+ Test load_video raises ValueError when video has invalid dimensions.
65
+ :param mock_video: MagicMock object for cv2.VideoCapture
66
+ :return: None
67
+ """
68
+ mock_video.return_value.isOpened.return_value = True
69
+ mock_video.return_value.get.side_effect = [
70
+ 1,
71
+ 0,
72
+ 0,
73
+ ] # Mocking frame count as 1 and dimensions as 0
74
+ with self.assertRaises(ValueError) as context:
75
+ video_processing.load_video(self.video_path)
76
+ self.assertIn(
77
+ "The video file " + self.video_path + " has invalid dimensions.",
78
+ str(context.exception),
79
+ )
80
+
81
+ @patch("src.video_processing.cv2.VideoCapture")
82
+ def test_load_video_success(self, mock_video: MagicMock) -> None:
83
+ """
84
+ Test load_video successfully returns video properties.
85
+ :param mock_video: MagicMock object for cv2.VideoCapture
86
+ :return: None
87
+ """
88
+ mock_video.return_value.isOpened.return_value = True
89
+ mock_video.return_value.get.side_effect = [
90
+ 100,
91
+ 1920,
92
+ 1080,
93
+ ] # Mocking frame count, width, and height
94
+ video, frame_count, frame_width, frame_height = video_processing.load_video(self.video_path)
95
+ self.assertEqual(frame_count, 100)
96
+ self.assertEqual(frame_width, 1920)
97
+ self.assertEqual(frame_height, 1080)
98
+
99
+ @patch("cv2.VideoCapture")
100
+ def test_extract_colors(self, mock_video: MagicMock) -> None:
101
+ """
102
+ Test extract_colors returns the expected colors.
103
+ :param mock_video: MagicMock object for cv2.VideoCapture
104
+ :return: None
105
+ """
106
+ # Mock the read method of VideoCapture
107
+ mock_video_instance = mock_video.return_value
108
+ mock_video_instance.read.side_effect = [(True, "frame_data")] * self.target_frames # Simulate frames
109
+
110
+ # Mock color_extractor to return a predictable color
111
+ def special_color_extractor(frame):
112
+ return "red" if frame == "frame_data" else "unknown"
113
+
114
+ # Expected result
115
+ expected_colors = ["red"] * self.target_frames
116
+
117
+ # Run the test
118
+ actual_colors = video_processing.extract_colors(
119
+ self.video_path,
120
+ self.start_frame,
121
+ self.end_frame,
122
+ special_color_extractor,
123
+ self.target_frames,
124
+ )
125
+
126
+ # Verify the result
127
+ self.assertEqual(actual_colors, expected_colors)
128
+
129
+ @patch("cv2.VideoCapture")
130
+ def test_frame_skipping_logic(self, mock_video: MagicMock) -> None:
131
+ """
132
+ Test that the frame skipping logic is working as expected.
133
+ :param mock_video: MagicMock object for cv2.VideoCapture
134
+ :return: None
135
+ """
136
+ mock_video_instance = mock_video.return_value
137
+
138
+ # Initialize a counter to keep track of the current frame position
139
+ current_frame = [0] # Use a list so it can be modified within the side_effect functions
140
+
141
+ # Define a side effect for the read method
142
+ def read_side_effect():
143
+ if current_frame[0] < self.end_frame:
144
+ frame_data = (True, f"frame_data_{current_frame[0]}")
145
+ current_frame[0] += 1
146
+ return frame_data
147
+ else:
148
+ return False, None # Simulate end of video
149
+
150
+ # Define a side effect for the grab method to simply increment the counter
151
+ def grab_side_effect():
152
+ if current_frame[0] < self.end_frame:
153
+ current_frame[0] += 1
154
+
155
+ mock_video_instance.read.side_effect = read_side_effect
156
+ mock_video_instance.grab.side_effect = grab_side_effect
157
+
158
+ expected_colors = [f"frame_data_{i*10}" for i in range(self.target_frames)]
159
+
160
+ actual_colors = video_processing.extract_colors(
161
+ self.video_path,
162
+ self.start_frame,
163
+ self.end_frame,
164
+ self.mock_color_extractor,
165
+ self.target_frames,
166
+ )
167
+
168
+ self.assertEqual(actual_colors, expected_colors)
169
+
170
+ @patch("cv2.VideoCapture")
171
+ def test_frame_skip_default(self, mock_video: MagicMock) -> None:
172
+ """
173
+ Test that the default frame_skip is 1.
174
+ :param mock_video: MagicMock object for cv2.VideoCapture
175
+ :return: None
176
+ """
177
+ # Setup mock to simulate frames in the video
178
+ mock_video_instance = mock_video.return_value
179
+ mock_video_instance.read.side_effect = [(True, f"frame_{i}") for i in range(self.end_frame)] + [(False, None)]
180
+
181
+ # Call without specifying target_frames
182
+ colors = video_processing.extract_colors(
183
+ self.video_path, self.start_frame, self.end_frame, self.mock_color_extractor
184
+ )
185
+
186
+ # Assert frame_skip was effectively 1 by checking all frames were processed
187
+ self.assertEqual(len(colors), self.end_frame)
188
+
189
+ @patch("cv2.VideoCapture")
190
+ def test_frame_skip_with_target_frames(self, mock_video: MagicMock) -> None:
191
+ """
192
+ Test that target_frames is respected when extracting colors.
193
+ :param mock_video: MagicMock object for cv2.VideoCapture
194
+ :return: None
195
+ """
196
+ # Setup mock to simulate frames in the video
197
+ mock_video_instance = mock_video.return_value
198
+ mock_video_instance.read.side_effect = [(True, f"frame_{i}") for i in range(self.end_frame)] + [(False, None)]
199
+
200
+ colors = video_processing.extract_colors(
201
+ self.video_path,
202
+ self.start_frame,
203
+ self.end_frame,
204
+ self.mock_color_extractor,
205
+ self.target_frames,
206
+ )
207
+
208
+ # Assert that we only processed the specified number of target_frames
209
+ self.assertEqual(len(colors), self.target_frames)
210
+
211
+ @patch("cv2.VideoCapture")
212
+ def test_len_colors_equals_len_video_no_target_frames(self, mock_video: MagicMock) -> None:
213
+ """
214
+ Test that the number of colors extracted is equal to the number of frames in the video
215
+ when target_frames is not given.
216
+ :param mock_video: MagicMock object for cv2.VideoCapture
217
+ :return: None
218
+ """
219
+ # Setup mock to simulate frames in the video
220
+ mock_video_instance = mock_video.return_value
221
+ mock_video_instance.read.side_effect = [(True, f"frame_{i}") for i in range(self.end_frame)] + [(False, None)]
222
+
223
+ colors = video_processing.extract_colors(
224
+ self.video_path, self.start_frame, self.end_frame, self.mock_color_extractor
225
+ )
226
+
227
+ # Assert that all frames were processed since target_frames was not given
228
+ self.assertEqual(len(colors), self.end_frame)
229
+
230
+
231
+ if __name__ == "__main__":
232
+ unittest.main()