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