auto-editor 26.3.0__py3-none-any.whl → 26.3.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.
auto_editor/cmds/test.py CHANGED
@@ -1,14 +1,17 @@
1
- from __future__ import annotations
2
-
1
+ import concurrent.futures
2
+ import hashlib
3
3
  import os
4
4
  import shutil
5
5
  import subprocess
6
6
  import sys
7
+ from collections.abc import Callable
7
8
  from dataclasses import dataclass, field
8
9
  from fractions import Fraction
10
+ from hashlib import sha256
11
+ from tempfile import mkdtemp
9
12
  from time import perf_counter
10
- from typing import TYPE_CHECKING
11
13
 
14
+ import av
12
15
  import numpy as np
13
16
 
14
17
  from auto_editor.ffwrapper import FileInfo, initFileInfo
@@ -19,12 +22,6 @@ from auto_editor.lib.err import MyError
19
22
  from auto_editor.utils.log import Log
20
23
  from auto_editor.vanparse import ArgumentParser
21
24
 
22
- if TYPE_CHECKING:
23
- from collections.abc import Callable
24
- from typing import Any
25
-
26
- from auto_editor.vanparse import ArgumentParser
27
-
28
25
 
29
26
  @dataclass(slots=True)
30
27
  class TestArgs:
@@ -52,38 +49,53 @@ def pipe_to_console(cmd: list[str]) -> tuple[int, str, str]:
52
49
  return process.returncode, stdout.decode("utf-8"), stderr.decode("utf-8")
53
50
 
54
51
 
52
+ all_files = (
53
+ "aac.m4a",
54
+ "alac.m4a",
55
+ "wav/pcm-f32le.wav",
56
+ "wav/pcm-s32le.wav",
57
+ "multi-track.mov",
58
+ "mov_text.mp4",
59
+ "testsrc.mkv",
60
+ )
61
+ log = Log()
62
+
63
+
64
+ def fileinfo(path: str) -> FileInfo:
65
+ return initFileInfo(path, log)
66
+
67
+
68
+ def calculate_sha256(filename: str) -> str:
69
+ sha256_hash = hashlib.sha256()
70
+ with open(filename, "rb") as f:
71
+ for byte_block in iter(lambda: f.read(4096), b""):
72
+ sha256_hash.update(byte_block)
73
+ return sha256_hash.hexdigest()
74
+
75
+
55
76
  class Runner:
56
77
  def __init__(self) -> None:
57
78
  self.program = [sys.executable, "-m", "auto_editor"]
58
-
59
- def main(
60
- self, inputs: list[str], cmd: list[str], output: str | None = None
61
- ) -> str | None:
62
- cmd = self.program + inputs + cmd + ["--no-open"]
63
-
64
- if output is not None:
79
+ self.temp_dir = mkdtemp()
80
+
81
+ def main(self, inputs: list[str], cmd: list[str], output: str | None = None) -> str:
82
+ assert inputs
83
+ cmd = self.program + inputs + cmd + ["--no-open", "--progress", "none"]
84
+ temp_dir = self.temp_dir
85
+ if not os.path.exists(temp_dir):
86
+ raise ValueError("Where's the temp dir")
87
+ if output is None:
88
+ new_root = sha256("".join(cmd).encode()).hexdigest()[:16]
89
+ output = os.path.join(temp_dir, new_root)
90
+ else:
65
91
  root, ext = os.path.splitext(output)
66
92
  if inputs and ext == "":
67
93
  output = root + os.path.splitext(inputs[0])[1]
68
- cmd += ["--output", output]
94
+ output = os.path.join(temp_dir, output)
69
95
 
70
- if output is None and inputs:
71
- root, ext = os.path.splitext(inputs[0])
72
-
73
- if "--export_as_json" in cmd:
74
- ext = ".json"
75
- elif "-exp" in cmd:
76
- ext = ".xml"
77
- elif "-exf" in cmd:
78
- ext = ".fcpxml"
79
- elif "-exs" in cmd:
80
- ext = ".mlt"
81
-
82
- output = f"{root}_ALTERED{ext}"
83
-
84
- returncode, stdout, stderr = pipe_to_console(cmd)
96
+ returncode, stdout, stderr = pipe_to_console(cmd + ["--output", output])
85
97
  if returncode > 0:
86
- raise Exception(f"{stdout}\n{stderr}\n")
98
+ raise Exception(f"Test returned: {returncode}\n{stdout}\n{stderr}\n")
87
99
 
88
100
  return output
89
101
 
@@ -105,177 +117,107 @@ class Runner:
105
117
  else:
106
118
  raise Exception("Program should not respond with code 0 but did!")
107
119
 
108
-
109
- def run_tests(tests: list[Callable], args: TestArgs) -> None:
110
- def clean_all() -> None:
111
- def clean(the_dir: str) -> None:
112
- for item in os.listdir(the_dir):
113
- if "_ALTERED" in item:
114
- os.remove(os.path.join(the_dir, item))
115
- if item.endswith("_tracks"):
116
- shutil.rmtree(os.path.join(the_dir, item))
117
-
118
- clean("resources")
119
- clean(os.getcwd())
120
-
121
- if args.only != []:
122
- tests = list(filter(lambda t: t.__name__ in args.only, tests))
123
-
124
- total_time = 0.0
125
-
126
- passed = 0
127
- total = len(tests)
128
- for index, test in enumerate(tests, start=1):
129
- name = test.__name__
130
- start = perf_counter()
131
- outputs = None
132
-
133
- try:
134
- outputs = test()
135
- dur = perf_counter() - start
136
- total_time += dur
137
- except KeyboardInterrupt:
138
- print("Testing Interrupted by User.")
139
- clean_all()
140
- sys.exit(1)
141
- except Exception as e:
142
- dur = perf_counter() - start
143
- total_time += dur
144
- print(
145
- f"{name:<24} ({index}/{total}) {round(dur, 2):<4} secs \033[1;31m[FAILED]\033[0m",
146
- flush=True,
147
- )
148
- if args.no_fail_fast:
149
- print(f"\n{e}")
150
- else:
151
- print("")
152
- clean_all()
153
- raise e
154
- else:
155
- passed += 1
156
- print(
157
- f"{name:<24} ({index}/{total}) {round(dur, 2):<4} secs [\033[1;32mPASSED\033[0m]",
158
- flush=True,
159
- )
160
- if outputs is not None:
161
- if isinstance(outputs, str):
162
- outputs = [outputs]
163
-
164
- for out in outputs:
165
- try:
166
- os.remove(out)
167
- except FileNotFoundError:
168
- pass
169
-
170
- print(f"\nCompleted\n{passed}/{total}\n{round(total_time, 2)} secs")
171
- clean_all()
172
-
173
-
174
- def main(sys_args: list[str] | None = None):
175
- if sys_args is None:
176
- sys_args = sys.argv[1:]
177
-
178
- args = test_options(ArgumentParser("test")).parse_args(TestArgs, sys_args)
179
-
180
- run = Runner()
181
- log = Log()
182
-
183
- def fileinfo(path: str) -> FileInfo:
184
- return initFileInfo(path, log)
185
-
186
- ### Tests ###
187
-
188
- all_files = (
189
- "aac.m4a",
190
- "alac.m4a",
191
- "wav/pcm-f32le.wav",
192
- "wav/pcm-s32le.wav",
193
- "multi-track.mov",
194
- "mov_text.mp4",
195
- "testsrc.mkv",
196
- )
197
-
198
- ## API Tests ##
199
-
200
- def help_tests():
120
+ def test_help(self):
201
121
  """check the help option, its short, and help on options and groups."""
202
- run.raw(["--help"])
203
- run.raw(["-h"])
204
- run.raw(["--margin", "--help"])
205
- run.raw(["--edit", "-h"])
206
- run.raw(["--help", "--help"])
207
- run.raw(["-h", "--help"])
208
- run.raw(["--help", "-h"])
209
-
210
- def version_test():
122
+ self.raw(["--help"])
123
+ self.raw(["-h"])
124
+ self.raw(["--margin", "--help"])
125
+ self.raw(["--edit", "-h"])
126
+ self.raw(["--help", "--help"])
127
+ self.raw(["-h", "--help"])
128
+ self.raw(["--help", "-h"])
129
+
130
+ def test_version(self):
211
131
  """Test version flags and debug by itself."""
212
- run.raw(["--version"])
213
- run.raw(["-V"])
214
-
215
- def parser_test():
216
- run.check(["example.mp4", "--video-speed"], "needs argument")
217
-
218
- def info():
219
- run.raw(["info", "example.mp4"])
220
- run.raw(["info", "resources/only-video/man-on-green-screen.mp4"])
221
- run.raw(["info", "resources/multi-track.mov"])
222
- run.raw(["info", "resources/new-commentary.mp3"])
223
- run.raw(["info", "resources/testsrc.mkv"])
224
-
225
- def levels():
226
- run.raw(["levels", "resources/multi-track.mov"])
227
- run.raw(["levels", "resources/new-commentary.mp3"])
228
-
229
- def subdump():
230
- run.raw(["subdump", "resources/mov_text.mp4"])
231
- run.raw(["subdump", "resources/webvtt.mkv"])
232
-
233
- def desc():
234
- run.raw(["desc", "example.mp4"])
235
-
236
- def example():
237
- out = run.main(inputs=["example.mp4"], cmd=[])
238
- cn = fileinfo(out)
239
- video = cn.videos[0]
240
-
241
- assert video.fps == 30
242
- # assert video.time_base == Fraction(1, 30)
243
- assert video.width == 1280
244
- assert video.height == 720
245
- assert video.codec == "h264"
246
- assert video.lang == "eng"
247
- assert cn.audios[0].codec == "aac"
248
- assert cn.audios[0].samplerate == 48000
249
- assert cn.audios[0].lang == "eng"
250
-
251
- return out
132
+ self.raw(["--version"])
133
+ self.raw(["-V"])
134
+
135
+ def test_parser(self):
136
+ self.check(["example.mp4", "--video-speed"], "needs argument")
137
+
138
+ def info(self):
139
+ self.raw(["info", "example.mp4"])
140
+ self.raw(["info", "resources/only-video/man-on-green-screen.mp4"])
141
+ self.raw(["info", "resources/multi-track.mov"])
142
+ self.raw(["info", "resources/new-commentary.mp3"])
143
+ self.raw(["info", "resources/testsrc.mkv"])
144
+
145
+ def levels(self):
146
+ self.raw(["levels", "resources/multi-track.mov"])
147
+ self.raw(["levels", "resources/new-commentary.mp3"])
148
+
149
+ def subdump(self):
150
+ self.raw(["subdump", "resources/mov_text.mp4"])
151
+ self.raw(["subdump", "resources/webvtt.mkv"])
152
+
153
+ def desc(self):
154
+ self.raw(["desc", "example.mp4"])
155
+
156
+ def test_example(self) -> None:
157
+ out = self.main(["example.mp4"], [], output="example_ALTERED.mp4")
158
+ with av.open(out) as container:
159
+ video = container.streams[0]
160
+ audio = container.streams[1]
161
+
162
+ assert isinstance(video, av.VideoStream)
163
+ assert isinstance(audio, av.AudioStream)
164
+ assert video.base_rate == 30
165
+ assert video.average_rate is not None
166
+ assert video.average_rate == 30, video.average_rate
167
+ assert (video.width, video.height) == (1280, 720)
168
+ assert video.codec.name == "h264"
169
+ assert video.language == "eng"
170
+ assert audio.codec.name == "aac"
171
+ assert audio.sample_rate == 48000
172
+ assert audio.language == "eng"
173
+
174
+ out1_sha = calculate_sha256(out)
175
+
176
+ out = self.main(["example.mp4"], ["--fragmented"], output="example_ALTERED.mp4")
177
+ with av.open(out) as container:
178
+ video = container.streams[0]
179
+ audio = container.streams[1]
180
+
181
+ assert isinstance(video, av.VideoStream)
182
+ assert isinstance(audio, av.AudioStream)
183
+ assert video.base_rate == 30
184
+ assert video.average_rate is not None
185
+ assert round(video.average_rate) == 30, video.average_rate
186
+ assert (video.width, video.height) == (1280, 720)
187
+ assert video.codec.name == "h264"
188
+ assert video.language == "eng"
189
+ assert audio.codec.name == "aac"
190
+ assert audio.sample_rate == 48000
191
+ assert audio.language == "eng"
192
+
193
+ assert calculate_sha256(out) != out1_sha, "Fragmented output should be diff."
252
194
 
253
195
  # PR #260
254
- def high_speed_test():
255
- return run.check(["example.mp4", "--video-speed", "99998"], "empty")
196
+ def test_high_speed(self):
197
+ self.check(["example.mp4", "--video-speed", "99998"], "empty")
256
198
 
257
199
  # Issue #184
258
- def units():
259
- run.main(["example.mp4"], ["--edit", "all/e", "--set-speed", "125%,-30,end"])
260
- return run.main(["example.mp4"], ["--edit", "audio:threshold=4%"])
200
+ def test_units(self):
201
+ self.main(["example.mp4"], ["--edit", "all/e", "--set-speed", "125%,-30,end"])
202
+ self.main(["example.mp4"], ["--edit", "audio:threshold=4%"])
261
203
 
262
- def sr_units():
263
- run.main(["example.mp4"], ["--sample_rate", "44100 Hz"])
264
- return run.main(["example.mp4"], ["--sample_rate", "44.1 kHz"])
204
+ def test_sr_units(self):
205
+ self.main(["example.mp4"], ["--sample_rate", "44100 Hz"])
206
+ self.main(["example.mp4"], ["--sample_rate", "44.1 kHz"])
265
207
 
266
- def video_speed():
267
- return run.main(["example.mp4"], ["--video-speed", "1.5"])
208
+ def test_video_speed(self):
209
+ self.main(["example.mp4"], ["--video-speed", "1.5"])
268
210
 
269
- def backwards_range():
211
+ def test_backwards_range(self):
270
212
  """
271
213
  Cut out the last 5 seconds of a media file by using negative number in the
272
214
  range.
273
215
  """
274
- run.main(["example.mp4"], ["--edit", "none", "--cut_out", "-5secs,end"])
275
- return run.main(["example.mp4"], ["--edit", "all/e", "--add_in", "-5secs,end"])
216
+ self.main(["example.mp4"], ["--edit", "none", "--cut_out", "-5secs,end"])
217
+ self.main(["example.mp4"], ["--edit", "all/e", "--add_in", "-5secs,end"])
276
218
 
277
- def cut_out():
278
- run.main(
219
+ def test_cut_out(self):
220
+ self.main(
279
221
  ["example.mp4"],
280
222
  [
281
223
  "--edit",
@@ -288,112 +230,93 @@ def main(sys_args: list[str] | None = None):
288
230
  "2secs,10secs",
289
231
  ],
290
232
  )
291
- return run.main(
233
+ self.main(
292
234
  ["example.mp4"],
293
235
  ["--edit", "all/e", "--video_speed", "2", "--add_in", "2secs,10secs"],
294
236
  )
295
237
 
296
- def gif():
238
+ def test_gif(self):
297
239
  """
298
240
  Feed auto-editor a gif file and make sure it can spit out a correctly formatted
299
241
  gif. No editing is requested.
300
242
  """
301
- out = run.main(
302
- ["resources/only-video/man-on-green-screen.gif"], ["--edit", "none"]
303
- )
243
+ input = ["resources/only-video/man-on-green-screen.gif"]
244
+ out = self.main(input, ["--edit", "none", "--cut-out", "2sec,end"], "out.gif")
304
245
  assert fileinfo(out).videos[0].codec == "gif"
305
246
 
306
- return out
307
-
308
- def margin_tests():
309
- run.main(["example.mp4"], ["-m", "3"])
310
- run.main(["example.mp4"], ["--margin", "3"])
311
- run.main(["example.mp4"], ["-m", "0.3sec"])
312
- run.main(["example.mp4"], ["-m", "6,-3secs"])
313
- return run.main(["example.mp4"], ["-m", "0.4 seconds", "--stats"])
247
+ def test_margin(self):
248
+ self.main(["example.mp4"], ["-m", "3"])
249
+ self.main(["example.mp4"], ["-m", "0.3sec"])
250
+ self.main(["example.mp4"], ["-m", "0.1 seconds"])
251
+ self.main(["example.mp4"], ["-m", "6,-3secs"])
314
252
 
315
- def input_extension():
253
+ def test_input_extension(self):
316
254
  """Input file must have an extension. Throw error if none is given."""
255
+ path = os.path.join(self.temp_dir, "example")
256
+ shutil.copy("example.mp4", path)
257
+ self.check([path, "--no-open"], "must have an extension")
317
258
 
318
- shutil.copy("example.mp4", "example")
319
- run.check(["example", "--no-open"], "must have an extension")
320
-
321
- return "example"
322
-
323
- def output_extension():
324
- # Add input extension to output name if no output extension is given.
325
- out = run.main(inputs=["example.mp4"], cmd=[], output="out")
326
-
327
- assert out == "out.mp4"
328
- assert fileinfo(out).videos[0].codec == "h264"
329
-
330
- out = run.main(inputs=["resources/testsrc.mkv"], cmd=[], output="out")
331
- assert out == "out.mkv"
332
- assert fileinfo(out).videos[0].codec == "h264"
259
+ def test_silent_threshold(self):
260
+ with av.open("resources/new-commentary.mp3") as container:
261
+ assert container.duration / av.time_base == 6.732
333
262
 
334
- return "out.mp4", "out.mkv"
335
-
336
- def progress():
337
- run.main(["example.mp4"], ["--progress", "machine"])
338
- run.main(["example.mp4"], ["--progress", "none"])
339
- return run.main(["example.mp4"], ["--progress", "ascii"])
340
-
341
- def silent_threshold():
342
- return run.main(
263
+ out = self.main(
343
264
  ["resources/new-commentary.mp3"], ["--edit", "audio:threshold=0.1"]
344
265
  )
266
+ out += ".mp3"
345
267
 
346
- def track_tests():
347
- out = run.main(["resources/multi-track.mov"], ["--keep_tracks_seperate"])
348
- assert len(fileinfo(out).audios) == 2
268
+ with av.open(out) as container:
269
+ assert container.duration / av.time_base == 6.552
349
270
 
350
- return out
271
+ def test_track(self):
272
+ out = self.main(["resources/multi-track.mov"], ["--keep_tracks_seperate"], "te")
273
+ assert len(fileinfo(out).audios) == 2
351
274
 
352
- def export_json_tests():
353
- out = run.main(["example.mp4"], ["--export_as_json"])
354
- out2 = run.main([out], [])
355
- return out, out2
275
+ def test_export_json(self):
276
+ out = self.main(["example.mp4"], ["--export_as_json"], "c77130d763d40e8.json")
277
+ self.main([out], [])
356
278
 
357
- def import_v1_tests():
358
- with open("v1.json", "w") as file:
279
+ def test_import_v1(self):
280
+ path = os.path.join(self.temp_dir, "v1.json")
281
+ with open(path, "w") as file:
359
282
  file.write(
360
283
  """{"version": "1", "source": "example.mp4", "chunks": [ [0, 26, 1.0], [26, 34, 0] ]}"""
361
284
  )
362
285
 
363
- return "v1.json", run.main(["v1.json"], [])
286
+ self.main([path], [])
364
287
 
365
- def premiere_named_export():
366
- run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
288
+ def test_premiere_named_export(self):
289
+ self.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
367
290
 
368
- def export_subtitles():
369
- # cn = fileinfo(run.main(["resources/mov_text.mp4"], []))
291
+ def test_export_subtitles(self):
292
+ # cn = fileinfo(self.main(["resources/mov_text.mp4"], [], "movtext_out.mp4"))
370
293
 
371
294
  # assert len(cn.videos) == 1
372
295
  # assert len(cn.audios) == 1
373
296
  # assert len(cn.subtitles) == 1
374
297
 
375
- cn = fileinfo(run.main(["resources/webvtt.mkv"], []))
376
-
298
+ cn = fileinfo(self.main(["resources/webvtt.mkv"], [], "webvtt_out.mkv"))
377
299
  assert len(cn.videos) == 1
378
300
  assert len(cn.audios) == 1
379
301
  assert len(cn.subtitles) == 1
380
302
 
381
- def resolution_and_scale():
382
- cn = fileinfo(run.main(["example.mp4"], ["--scale", "1.5"]))
383
-
303
+ def test_scale(self):
304
+ cn = fileinfo(self.main(["example.mp4"], ["--scale", "1.5"], "scale.mp4"))
384
305
  assert cn.videos[0].fps == 30
385
306
  assert cn.videos[0].width == 1920
386
307
  assert cn.videos[0].height == 1080
387
308
  assert cn.audios[0].samplerate == 48000
388
309
 
389
- cn = fileinfo(run.main(["example.mp4"], ["--scale", "0.2"]))
390
-
310
+ cn = fileinfo(self.main(["example.mp4"], ["--scale", "0.2"], "scale.mp4"))
391
311
  assert cn.videos[0].fps == 30
392
312
  assert cn.videos[0].width == 256
393
313
  assert cn.videos[0].height == 144
394
314
  assert cn.audios[0].samplerate == 48000
395
315
 
396
- out = run.main(["example.mp4"], ["-res", "700,380", "-b", "darkgreen"])
316
+ def test_resolution(self):
317
+ out = self.main(
318
+ ["example.mp4"], ["-res", "700,380", "-b", "darkgreen"], "green"
319
+ )
397
320
  cn = fileinfo(out)
398
321
 
399
322
  assert cn.videos[0].fps == 30
@@ -401,182 +324,159 @@ def main(sys_args: list[str] | None = None):
401
324
  assert cn.videos[0].height == 380
402
325
  assert cn.audios[0].samplerate == 48000
403
326
 
404
- return out
327
+ def test_premiere(self):
328
+ for test_name in all_files:
329
+ p_xml = self.main([f"resources/{test_name}"], ["-exp"], "out.xml")
330
+ self.main([p_xml], [])
405
331
 
406
- def premiere():
407
- results = set()
332
+ def test_export(self):
408
333
  for test_name in all_files:
409
334
  test_file = f"resources/{test_name}"
410
- p_xml = run.main([test_file], ["-exp"])
411
- results.add(p_xml)
412
- results.add(run.main([p_xml], []))
413
-
414
- return tuple(results)
415
-
416
- def export():
417
- results = set()
335
+ self.main([test_file], ["--export", "final-cut-pro:version=10"])
336
+ self.main([test_file], ["--export", "final-cut-pro:version=11"])
337
+ self.main([test_file], ["-exs"])
338
+ self.main([test_file], ["--stats"])
418
339
 
340
+ def test_clip_sequence(self):
419
341
  for test_name in all_files:
420
342
  test_file = f"resources/{test_name}"
421
- results.add(run.main([test_file], []))
422
- run.main([test_file], ["--edit", "none"])
423
- results.add(run.main([test_file], ["--export", "final-cut-pro:version=10"]))
424
- results.add(run.main([test_file], ["--export", "final-cut-pro:version=11"]))
425
- results.add(run.main([test_file], ["-exs"]))
426
- results.add(run.main([test_file], ["--export_as_clip_sequence"]))
427
- run.main([test_file], ["--stats"])
343
+ self.main([test_file], ["--export_as_clip_sequence"])
428
344
 
429
- return tuple(results)
430
-
431
- def codec_tests():
432
- run.main(["example.mp4"], ["--video_codec", "h264"])
433
- return run.main(["example.mp4"], ["--audio_codec", "ac3"])
345
+ def test_codecs(self):
346
+ self.main(["example.mp4"], ["--video_codec", "h264"])
347
+ self.main(["example.mp4"], ["--audio_codec", "ac3"])
434
348
 
435
349
  # Issue #241
436
- def multi_track_edit():
437
- out = run.main(
350
+ def test_multi_track_edit(self):
351
+ out = self.main(
438
352
  ["example.mp4", "resources/multi-track.mov"],
439
353
  ["--edit", "audio:stream=1"],
440
- "out.mov",
354
+ "multi-track_ALTERED.mov",
441
355
  )
442
356
  assert len(fileinfo(out).audios) == 1
443
357
 
444
- return out
358
+ def test_concat(self):
359
+ out = self.main(["example.mp4"], ["--cut-out", "0,171"], "hmm.mp4")
360
+ self.main(["example.mp4", out], ["--debug"])
445
361
 
446
- def concat():
447
- out = run.main(["example.mp4"], ["--cut-out", "0,171"], "hmm.mp4")
448
- out2 = run.main(["example.mp4", "hmm.mp4"], ["--debug"])
449
- return out, out2
450
-
451
- def concat_mux_tracks():
452
- out = run.main(["example.mp4", "resources/multi-track.mov"], [], "out.mov")
362
+ def test_concat_mux_tracks(self):
363
+ out = self.main(
364
+ ["example.mp4", "resources/multi-track.mov"], [], "concat_mux.mov"
365
+ )
453
366
  assert len(fileinfo(out).audios) == 1
454
367
 
455
- return out
456
-
457
- def concat_multiple_tracks():
458
- out = run.main(
368
+ def test_concat_multi_tracks(self):
369
+ out = self.main(
459
370
  ["resources/multi-track.mov", "resources/multi-track.mov"],
460
371
  ["--keep-tracks-separate"],
461
372
  "out.mov",
462
373
  )
463
374
  assert len(fileinfo(out).audios) == 2
464
- out = run.main(
375
+ out = self.main(
465
376
  ["example.mp4", "resources/multi-track.mov"],
466
377
  ["--keep-tracks-separate"],
467
378
  "out.mov",
468
379
  )
469
380
  assert len(fileinfo(out).audios) == 2
470
381
 
471
- return out
472
-
473
- def frame_rate():
474
- cn = fileinfo(run.main(["example.mp4"], ["-r", "15", "--no-seek"]))
382
+ def test_frame_rate(self):
383
+ cn = fileinfo(self.main(["example.mp4"], ["-r", "15", "--no-seek"], "fr.mp4"))
475
384
  video = cn.videos[0]
476
385
  assert video.fps == 15, video.fps
477
386
  assert video.duration - 17.33333333333333333333333 < 3, video.duration
478
387
 
479
- cn = fileinfo(run.main(["example.mp4"], ["-r", "20"]))
388
+ cn = fileinfo(self.main(["example.mp4"], ["-r", "20"], "fr.mp4"))
480
389
  video = cn.videos[0]
481
390
  assert video.fps == 20, video.fps
482
391
  assert video.duration - 17.33333333333333333333333 < 2
483
392
 
484
- cn = fileinfo(out := run.main(["example.mp4"], ["-r", "60"]))
393
+ def test_frame_rate_60(self):
394
+ cn = fileinfo(self.main(["example.mp4"], ["-r", "60"], "fr60.mp4"))
485
395
  video = cn.videos[0]
486
396
 
487
397
  assert video.fps == 60, video.fps
488
398
  assert video.duration - 17.33333333333333333333333 < 0.3
489
399
 
490
- return out
491
-
492
- # def embedded_image():
493
- # out1 = run.main(["resources/embedded-image/h264-png.mp4"], [])
400
+ # def embedded_image(self):
401
+ # out1 = self.main(["resources/embedded-image/h264-png.mp4"], [])
494
402
  # cn = fileinfo(out1)
495
403
  # assert cn.videos[0].codec == "h264"
496
404
  # assert cn.videos[1].codec == "png"
497
405
 
498
- # out2 = run.main(["resources/embedded-image/h264-mjpeg.mp4"], [])
406
+ # out2 = self.main(["resources/embedded-image/h264-mjpeg.mp4"], [])
499
407
  # cn = fileinfo(out2)
500
408
  # assert cn.videos[0].codec == "h264"
501
409
  # assert cn.videos[1].codec == "mjpeg"
502
410
 
503
- # out3 = run.main(["resources/embedded-image/h264-png.mkv"], [])
411
+ # out3 = self.main(["resources/embedded-image/h264-png.mkv"], [])
504
412
  # cn = fileinfo(out3)
505
413
  # assert cn.videos[0].codec == "h264"
506
414
  # assert cn.videos[1].codec == "png"
507
415
 
508
- # out4 = run.main(["resources/embedded-image/h264-mjpeg.mkv"], [])
416
+ # out4 = self.main(["resources/embedded-image/h264-mjpeg.mkv"], [])
509
417
  # cn = fileinfo(out4)
510
418
  # assert cn.videos[0].codec == "h264"
511
419
  # assert cn.videos[1].codec == "mjpeg"
512
420
 
513
- # return out1, out2, out3, out4
514
-
515
- def motion():
516
- out = run.main(
421
+ def test_motion(self):
422
+ self.main(
517
423
  ["resources/only-video/man-on-green-screen.mp4"],
518
424
  ["--edit", "motion", "--margin", "0"],
519
425
  )
520
- out2 = run.main(
426
+ self.main(
521
427
  ["resources/only-video/man-on-green-screen.mp4"],
522
428
  ["--edit", "motion:threshold=0,width=200"],
523
429
  )
524
- return out, out2
525
430
 
526
- def edit_positive_tests():
527
- run.main(["resources/multi-track.mov"], ["--edit", "audio:stream=all"])
528
- run.main(["resources/multi-track.mov"], ["--edit", "not audio:stream=all"])
529
- run.main(
431
+ def test_edit_positive(self):
432
+ self.main(["resources/multi-track.mov"], ["--edit", "audio:stream=all"])
433
+ self.main(["resources/multi-track.mov"], ["--edit", "not audio:stream=all"])
434
+ self.main(
530
435
  ["resources/multi-track.mov"],
531
436
  ["--edit", "(or (not audio:threshold=4%) audio:stream=1)"],
532
437
  )
533
- out = run.main(
438
+ self.main(
534
439
  ["resources/multi-track.mov"],
535
440
  ["--edit", "(or (not audio:threshold=4%) (not audio:stream=1))"],
536
441
  )
537
- return out
538
442
 
539
- def edit_negative_tests():
540
- run.check(
443
+ def test_edit_negative(self):
444
+ self.check(
541
445
  ["resources/wav/example-cut-s16le.wav", "--edit", "motion"],
542
- "video stream '0' does not ",
446
+ "video stream",
543
447
  )
544
- run.check(
448
+ self.check(
545
449
  ["resources/only-video/man-on-green-screen.gif", "--edit", "audio"],
546
- "audio stream '0' does not ",
450
+ "audio stream",
547
451
  )
548
452
 
549
- def yuv442p():
550
- return run.main(["resources/test_yuv422p.mp4"], [])
453
+ def test_yuv442p(self):
454
+ self.main(["resources/test_yuv422p.mp4"], [])
551
455
 
552
- def prores():
553
- run.main(["resources/testsrc.mp4", "-c:v", "prores", "-o", "out.mkv"], [])
554
- assert fileinfo("out.mkv").videos[0].pix_fmt == "yuv422p10le"
456
+ def test_prores(self):
457
+ out = self.main(["resources/testsrc.mp4"], ["-c:v", "prores"], "prores.mkv")
458
+ assert fileinfo(out).videos[0].pix_fmt == "yuv422p10le"
555
459
 
556
- run.main(["out.mkv", "-c:v", "prores", "-o", "out2.mkv"], [])
557
- assert fileinfo("out2.mkv").videos[0].pix_fmt == "yuv422p10le"
558
-
559
- return "out.mkv", "out2.mkv"
460
+ out2 = self.main([out], ["-c:v", "prores"], "prores2.mkv")
461
+ assert fileinfo(out2).videos[0].pix_fmt == "yuv422p10le"
560
462
 
561
463
  # Issue 280
562
- def SAR():
563
- out = run.main(["resources/SAR-2by3.mp4"], [])
464
+ def test_SAR(self):
465
+ out = self.main(["resources/SAR-2by3.mp4"], [], "2by3_out.mp4")
564
466
  assert fileinfo(out).videos[0].sar == Fraction(2, 3)
565
467
 
566
- return out
567
-
568
- def audio_norm_f():
569
- return run.main(["example.mp4"], ["--audio-normalize", "#f"])
468
+ def test_audio_norm_f(self):
469
+ return self.main(["example.mp4"], ["--audio-normalize", "#f"])
570
470
 
571
- def audio_norm_ebu():
572
- return run.main(
471
+ def test_audio_norm_ebu(self):
472
+ return self.main(
573
473
  ["example.mp4"], ["--audio-normalize", "ebu:i=-5,lra=20,gain=5,tp=-1"]
574
474
  )
575
475
 
576
- def palet_python_bridge():
476
+ def palet_python_bridge(self):
577
477
  env.update(make_standard_env())
578
478
 
579
- def cases(*cases: tuple[str, Any]) -> None:
479
+ def cases(*cases: tuple[str, object]) -> None:
580
480
  for text, expected in cases:
581
481
  try:
582
482
  parser = Parser(Lexer("repl", text))
@@ -724,65 +624,115 @@ def main(sys_args: list[str] | None = None):
724
624
  ('#(#("sym" "symbol?") "bool?")', [["sym", "symbol?"], "bool?"]),
725
625
  )
726
626
 
727
- def palet_scripts():
728
- run.raw(["palet", "resources/scripts/scope.pal"])
729
- run.raw(["palet", "resources/scripts/maxcut.pal"])
730
- run.raw(["palet", "resources/scripts/case.pal"])
731
- run.raw(["palet", "resources/scripts/testmath.pal"])
627
+ def palet_scripts(self):
628
+ self.raw(["palet", "resources/scripts/scope.pal"])
629
+ self.raw(["palet", "resources/scripts/maxcut.pal"])
630
+ self.raw(["palet", "resources/scripts/case.pal"])
631
+ self.raw(["palet", "resources/scripts/testmath.pal"])
632
+
732
633
 
634
+ def run_tests(runner: Runner, tests: list[Callable], args: TestArgs) -> None:
635
+ if args.only != []:
636
+ tests = list(filter(lambda t: t.__name__ in args.only, tests))
637
+
638
+ total_time = 0.0
639
+ real_time = perf_counter()
640
+ passed = 0
641
+ total = len(tests)
642
+
643
+ def timed_test(test_func):
644
+ start_time = perf_counter()
645
+ try:
646
+ test_func()
647
+ success = True
648
+ except Exception as e:
649
+ success = False
650
+ exception = e
651
+ end_time = perf_counter()
652
+ duration = end_time - start_time
653
+
654
+ if success:
655
+ return (True, duration, None)
656
+ else:
657
+ return (False, duration, exception)
658
+
659
+ with concurrent.futures.ThreadPoolExecutor() as executor:
660
+ future_to_data = {}
661
+ for test in tests:
662
+ future = executor.submit(timed_test, test)
663
+ future_to_data[future] = test
664
+
665
+ index = 0
666
+ for future in concurrent.futures.as_completed(future_to_data):
667
+ test = future_to_data[future]
668
+ name = test.__name__
669
+ success, dur, exception = future.result()
670
+ total_time += dur
671
+ index += 1
672
+
673
+ if success:
674
+ passed += 1
675
+ print(
676
+ f"{name:<26} ({index}/{total}) {round(dur, 2):<4} secs [\033[1;32mPASSED\033[0m]",
677
+ flush=True,
678
+ )
679
+ else:
680
+ print(
681
+ f"{name:<26} ({index}/{total}) {round(dur, 2):<4} secs \033[1;31m[FAILED]\033[0m",
682
+ flush=True,
683
+ )
684
+ if args.no_fail_fast:
685
+ print(f"\n{exception}")
686
+ else:
687
+ print("")
688
+ raise exception
689
+
690
+ real_time = round(perf_counter() - real_time, 2)
691
+ total_time = round(total_time, 2)
692
+ print(
693
+ f"\nCompleted {passed}/{total}\nreal time: {real_time} secs total: {total_time} secs"
694
+ )
695
+
696
+
697
+ def main(sys_args: list[str] | None = None) -> None:
698
+ if sys_args is None:
699
+ sys_args = sys.argv[1:]
700
+
701
+ args = test_options(ArgumentParser("test")).parse_args(TestArgs, sys_args)
702
+ run = Runner()
733
703
  tests = []
734
704
 
705
+ test_methods = {
706
+ name: getattr(run, name)
707
+ for name in dir(Runner)
708
+ if callable(getattr(Runner, name)) and name not in ["main", "raw", "check"]
709
+ }
710
+
735
711
  if args.category in {"palet", "all"}:
736
- tests.extend([palet_python_bridge, palet_scripts])
712
+ tests.extend(
713
+ [test_methods[name] for name in ["palet_python_bridge", "palet_scripts"]]
714
+ )
737
715
 
738
716
  if args.category in {"sub", "all"}:
739
- tests.extend([info, levels, subdump, desc])
717
+ tests.extend(
718
+ [test_methods[name] for name in ["info", "levels", "subdump", "desc"]]
719
+ )
740
720
 
741
721
  if args.category in {"cli", "all"}:
742
722
  tests.extend(
743
723
  [
744
- premiere,
745
- SAR,
746
- yuv442p,
747
- prores,
748
- edit_negative_tests,
749
- edit_positive_tests,
750
- audio_norm_f,
751
- audio_norm_ebu,
752
- export_json_tests,
753
- import_v1_tests,
754
- high_speed_test,
755
- video_speed,
756
- multi_track_edit,
757
- concat_mux_tracks,
758
- concat_multiple_tracks,
759
- frame_rate,
760
- help_tests,
761
- version_test,
762
- parser_test,
763
- concat,
764
- example,
765
- units,
766
- sr_units,
767
- backwards_range,
768
- cut_out,
769
- gif,
770
- margin_tests,
771
- input_extension,
772
- output_extension,
773
- progress,
774
- silent_threshold,
775
- track_tests,
776
- codec_tests,
777
- premiere_named_export,
778
- export_subtitles,
779
- export,
780
- motion,
781
- resolution_and_scale,
724
+ getattr(run, name)
725
+ for name in dir(Runner)
726
+ if callable(getattr(Runner, name)) and name.startswith("test_")
782
727
  ]
783
728
  )
784
-
785
- run_tests(tests, args)
729
+ try:
730
+ run_tests(run, tests, args)
731
+ except KeyboardInterrupt:
732
+ print("Testing Interrupted by User.")
733
+ shutil.rmtree(run.temp_dir)
734
+ sys.exit(1)
735
+ shutil.rmtree(run.temp_dir)
786
736
 
787
737
 
788
738
  if __name__ == "__main__":