auto-editor 26.3.1__py3-none-any.whl → 26.3.3__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]
94
+ output = os.path.join(temp_dir, output)
70
95
 
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}"
84
-
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,111 @@ 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")
218
-
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"])
225
-
226
- def levels():
227
- run.raw(["levels", "resources/multi-track.mov"])
228
- run.raw(["levels", "resources/new-commentary.mp3"])
229
-
230
- def subdump():
231
- run.raw(["subdump", "resources/mov_text.mp4"])
232
- run.raw(["subdump", "resources/webvtt.mkv"])
233
-
234
- def desc():
235
- run.raw(["desc", "example.mp4"])
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_movflags(self) -> None:
157
+ file = "resources/testsrc.mp4"
158
+ out = self.main([file], ["--faststart"]) + ".mp4"
159
+ fast = calculate_sha256(out)
160
+ with av.open(out) as container:
161
+ assert isinstance(container.streams[0], av.VideoStream)
162
+ assert isinstance(container.streams[1], av.AudioStream)
236
163
 
237
- def example():
238
- out = run.main(inputs=["example.mp4"], cmd=[])
164
+ out = self.main([file], ["--no-faststart"]) + ".mp4"
165
+ nofast = calculate_sha256(out)
166
+ with av.open(out) as container:
167
+ assert isinstance(container.streams[0], av.VideoStream)
168
+ assert isinstance(container.streams[1], av.AudioStream)
239
169
 
170
+ out = self.main([file], ["--fragmented"]) + ".mp4"
171
+ frag = calculate_sha256(out)
240
172
  with av.open(out) as container:
241
- assert container.streams[0].type == "video"
242
- assert container.streams[1].type == "audio"
173
+ assert isinstance(container.streams[0], av.VideoStream)
174
+ assert isinstance(container.streams[1], av.AudioStream)
243
175
 
244
- cn = fileinfo(out)
245
- video = cn.videos[0]
176
+ assert fast != nofast, "+faststart is not being applied"
177
+ assert frag not in (fast, nofast), "fragmented output should diff."
246
178
 
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
179
+ def test_example(self) -> None:
180
+ out = self.main(["example.mp4"], [], output="example_ALTERED.mp4")
181
+ with av.open(out) as container:
182
+ assert container.duration is not None
183
+ assert container.duration > 17300000 and container.duration < 2 << 24
184
+
185
+ video = container.streams[0]
186
+ audio = container.streams[1]
187
+ assert isinstance(video, av.VideoStream)
188
+ assert isinstance(audio, av.AudioStream)
189
+ assert video.base_rate == 30
190
+ assert video.average_rate is not None
191
+ assert video.average_rate == 30, video.average_rate
192
+ assert (video.width, video.height) == (1280, 720)
193
+ assert video.codec.name == "h264"
194
+ assert video.language == "eng"
195
+ assert audio.codec.name == "aac"
196
+ assert audio.sample_rate == 48000
197
+ assert audio.language == "eng"
258
198
 
259
199
  # PR #260
260
- def high_speed_test():
261
- return run.check(["example.mp4", "--video-speed", "99998"], "empty")
200
+ def test_high_speed(self):
201
+ self.check(["example.mp4", "--video-speed", "99998"], "empty")
262
202
 
263
203
  # 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%"])
204
+ def test_units(self):
205
+ self.main(["example.mp4"], ["--edit", "all/e", "--set-speed", "125%,-30,end"])
206
+ self.main(["example.mp4"], ["--edit", "audio:threshold=4%"])
267
207
 
268
- def sr_units():
269
- run.main(["example.mp4"], ["--sample_rate", "44100 Hz"])
270
- return run.main(["example.mp4"], ["--sample_rate", "44.1 kHz"])
208
+ def test_sr_units(self):
209
+ self.main(["example.mp4"], ["--sample_rate", "44100 Hz"])
210
+ self.main(["example.mp4"], ["--sample_rate", "44.1 kHz"])
271
211
 
272
- def video_speed():
273
- return run.main(["example.mp4"], ["--video-speed", "1.5"])
212
+ def test_video_speed(self):
213
+ self.main(["example.mp4"], ["--video-speed", "1.5"])
274
214
 
275
- def backwards_range():
215
+ def test_backwards_range(self):
276
216
  """
277
217
  Cut out the last 5 seconds of a media file by using negative number in the
278
218
  range.
279
219
  """
280
- run.main(["example.mp4"], ["--edit", "none", "--cut_out", "-5secs,end"])
281
- return run.main(["example.mp4"], ["--edit", "all/e", "--add_in", "-5secs,end"])
220
+ self.main(["example.mp4"], ["--edit", "none", "--cut_out", "-5secs,end"])
221
+ self.main(["example.mp4"], ["--edit", "all/e", "--add_in", "-5secs,end"])
282
222
 
283
- def cut_out():
284
- run.main(
223
+ def test_cut_out(self):
224
+ self.main(
285
225
  ["example.mp4"],
286
226
  [
287
227
  "--edit",
@@ -294,112 +234,93 @@ def main(sys_args: list[str] | None = None):
294
234
  "2secs,10secs",
295
235
  ],
296
236
  )
297
- return run.main(
237
+ self.main(
298
238
  ["example.mp4"],
299
239
  ["--edit", "all/e", "--video_speed", "2", "--add_in", "2secs,10secs"],
300
240
  )
301
241
 
302
- def gif():
242
+ def test_gif(self):
303
243
  """
304
244
  Feed auto-editor a gif file and make sure it can spit out a correctly formatted
305
245
  gif. No editing is requested.
306
246
  """
307
- out = run.main(
308
- ["resources/only-video/man-on-green-screen.gif"], ["--edit", "none"]
309
- )
247
+ input = ["resources/only-video/man-on-green-screen.gif"]
248
+ out = self.main(input, ["--edit", "none", "--cut-out", "2sec,end"], "out.gif")
310
249
  assert fileinfo(out).videos[0].codec == "gif"
311
250
 
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"])
251
+ def test_margin(self):
252
+ self.main(["example.mp4"], ["-m", "3"])
253
+ self.main(["example.mp4"], ["-m", "0.3sec"])
254
+ self.main(["example.mp4"], ["-m", "0.1 seconds"])
255
+ self.main(["example.mp4"], ["-m", "6,-3secs"])
320
256
 
321
- def input_extension():
257
+ def test_input_extension(self):
322
258
  """Input file must have an extension. Throw error if none is given."""
259
+ path = os.path.join(self.temp_dir, "example")
260
+ shutil.copy("example.mp4", path)
261
+ self.check([path, "--no-open"], "must have an extension")
323
262
 
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"
263
+ def test_silent_threshold(self):
264
+ with av.open("resources/new-commentary.mp3") as container:
265
+ assert container.duration / av.time_base == 6.732
339
266
 
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"])
346
-
347
- def silent_threshold():
348
- return run.main(
267
+ out = self.main(
349
268
  ["resources/new-commentary.mp3"], ["--edit", "audio:threshold=0.1"]
350
269
  )
270
+ out += ".mp3"
351
271
 
352
- def track_tests():
353
- out = run.main(["resources/multi-track.mov"], ["--keep_tracks_seperate"])
354
- assert len(fileinfo(out).audios) == 2
272
+ with av.open(out) as container:
273
+ assert container.duration / av.time_base == 6.552
355
274
 
356
- return out
275
+ def test_track(self):
276
+ out = self.main(["resources/multi-track.mov"], ["--keep_tracks_seperate"], "te")
277
+ assert len(fileinfo(out).audios) == 2
357
278
 
358
- def export_json_tests():
359
- out = run.main(["example.mp4"], ["--export_as_json"])
360
- out2 = run.main([out], [])
361
- return out, out2
279
+ def test_export_json(self):
280
+ out = self.main(["example.mp4"], ["--export_as_json"], "c77130d763d40e8.json")
281
+ self.main([out], [])
362
282
 
363
- def import_v1_tests():
364
- with open("v1.json", "w") as file:
283
+ def test_import_v1(self):
284
+ path = os.path.join(self.temp_dir, "v1.json")
285
+ with open(path, "w") as file:
365
286
  file.write(
366
287
  """{"version": "1", "source": "example.mp4", "chunks": [ [0, 26, 1.0], [26, 34, 0] ]}"""
367
288
  )
368
289
 
369
- return "v1.json", run.main(["v1.json"], [])
290
+ self.main([path], [])
370
291
 
371
- def premiere_named_export():
372
- run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
292
+ def test_premiere_named_export(self):
293
+ self.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
373
294
 
374
- def export_subtitles():
375
- # cn = fileinfo(run.main(["resources/mov_text.mp4"], []))
295
+ def test_export_subtitles(self):
296
+ # cn = fileinfo(self.main(["resources/mov_text.mp4"], [], "movtext_out.mp4"))
376
297
 
377
298
  # assert len(cn.videos) == 1
378
299
  # assert len(cn.audios) == 1
379
300
  # assert len(cn.subtitles) == 1
380
301
 
381
- cn = fileinfo(run.main(["resources/webvtt.mkv"], []))
382
-
302
+ cn = fileinfo(self.main(["resources/webvtt.mkv"], [], "webvtt_out.mkv"))
383
303
  assert len(cn.videos) == 1
384
304
  assert len(cn.audios) == 1
385
305
  assert len(cn.subtitles) == 1
386
306
 
387
- def resolution_and_scale():
388
- cn = fileinfo(run.main(["example.mp4"], ["--scale", "1.5"]))
389
-
307
+ def test_scale(self):
308
+ cn = fileinfo(self.main(["example.mp4"], ["--scale", "1.5"], "scale.mp4"))
390
309
  assert cn.videos[0].fps == 30
391
310
  assert cn.videos[0].width == 1920
392
311
  assert cn.videos[0].height == 1080
393
312
  assert cn.audios[0].samplerate == 48000
394
313
 
395
- cn = fileinfo(run.main(["example.mp4"], ["--scale", "0.2"]))
396
-
314
+ cn = fileinfo(self.main(["example.mp4"], ["--scale", "0.2"], "scale.mp4"))
397
315
  assert cn.videos[0].fps == 30
398
316
  assert cn.videos[0].width == 256
399
317
  assert cn.videos[0].height == 144
400
318
  assert cn.audios[0].samplerate == 48000
401
319
 
402
- out = run.main(["example.mp4"], ["-res", "700,380", "-b", "darkgreen"])
320
+ def test_resolution(self):
321
+ out = self.main(
322
+ ["example.mp4"], ["-res", "700,380", "-b", "darkgreen"], "green"
323
+ )
403
324
  cn = fileinfo(out)
404
325
 
405
326
  assert cn.videos[0].fps == 30
@@ -407,182 +328,159 @@ def main(sys_args: list[str] | None = None):
407
328
  assert cn.videos[0].height == 380
408
329
  assert cn.audios[0].samplerate == 48000
409
330
 
410
- return out
331
+ def test_premiere(self):
332
+ for test_name in all_files:
333
+ p_xml = self.main([f"resources/{test_name}"], ["-exp"], "out.xml")
334
+ self.main([p_xml], [])
411
335
 
412
- def premiere():
413
- results = set()
336
+ def test_export(self):
414
337
  for test_name in all_files:
415
338
  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()
339
+ self.main([test_file], ["--export", "final-cut-pro:version=10"])
340
+ self.main([test_file], ["--export", "final-cut-pro:version=11"])
341
+ self.main([test_file], ["-exs"])
342
+ self.main([test_file], ["--stats"])
424
343
 
344
+ def test_clip_sequence(self):
425
345
  for test_name in all_files:
426
346
  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"])
347
+ self.main([test_file], ["--export_as_clip_sequence"])
434
348
 
435
- return tuple(results)
436
-
437
- def codec_tests():
438
- run.main(["example.mp4"], ["--video_codec", "h264"])
439
- return run.main(["example.mp4"], ["--audio_codec", "ac3"])
349
+ def test_codecs(self):
350
+ self.main(["example.mp4"], ["--video_codec", "h264"])
351
+ self.main(["example.mp4"], ["--audio_codec", "ac3"])
440
352
 
441
353
  # Issue #241
442
- def multi_track_edit():
443
- out = run.main(
354
+ def test_multi_track_edit(self):
355
+ out = self.main(
444
356
  ["example.mp4", "resources/multi-track.mov"],
445
357
  ["--edit", "audio:stream=1"],
446
- "out.mov",
358
+ "multi-track_ALTERED.mov",
447
359
  )
448
360
  assert len(fileinfo(out).audios) == 1
449
361
 
450
- return out
362
+ def test_concat(self):
363
+ out = self.main(["example.mp4"], ["--cut-out", "0,171"], "hmm.mp4")
364
+ self.main(["example.mp4", out], ["--debug"])
451
365
 
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
456
-
457
- def concat_mux_tracks():
458
- out = run.main(["example.mp4", "resources/multi-track.mov"], [], "out.mov")
366
+ def test_concat_mux_tracks(self):
367
+ out = self.main(
368
+ ["example.mp4", "resources/multi-track.mov"], [], "concat_mux.mov"
369
+ )
459
370
  assert len(fileinfo(out).audios) == 1
460
371
 
461
- return out
462
-
463
- def concat_multiple_tracks():
464
- out = run.main(
372
+ def test_concat_multi_tracks(self):
373
+ out = self.main(
465
374
  ["resources/multi-track.mov", "resources/multi-track.mov"],
466
375
  ["--keep-tracks-separate"],
467
376
  "out.mov",
468
377
  )
469
378
  assert len(fileinfo(out).audios) == 2
470
- out = run.main(
379
+ out = self.main(
471
380
  ["example.mp4", "resources/multi-track.mov"],
472
381
  ["--keep-tracks-separate"],
473
382
  "out.mov",
474
383
  )
475
384
  assert len(fileinfo(out).audios) == 2
476
385
 
477
- return out
478
-
479
- def frame_rate():
480
- cn = fileinfo(run.main(["example.mp4"], ["-r", "15", "--no-seek"]))
386
+ def test_frame_rate(self):
387
+ cn = fileinfo(self.main(["example.mp4"], ["-r", "15", "--no-seek"], "fr.mp4"))
481
388
  video = cn.videos[0]
482
389
  assert video.fps == 15, video.fps
483
390
  assert video.duration - 17.33333333333333333333333 < 3, video.duration
484
391
 
485
- cn = fileinfo(run.main(["example.mp4"], ["-r", "20"]))
392
+ cn = fileinfo(self.main(["example.mp4"], ["-r", "20"], "fr.mp4"))
486
393
  video = cn.videos[0]
487
394
  assert video.fps == 20, video.fps
488
395
  assert video.duration - 17.33333333333333333333333 < 2
489
396
 
490
- cn = fileinfo(out := run.main(["example.mp4"], ["-r", "60"]))
397
+ def test_frame_rate_60(self):
398
+ cn = fileinfo(self.main(["example.mp4"], ["-r", "60"], "fr60.mp4"))
491
399
  video = cn.videos[0]
492
400
 
493
401
  assert video.fps == 60, video.fps
494
402
  assert video.duration - 17.33333333333333333333333 < 0.3
495
403
 
496
- return out
497
-
498
- # def embedded_image():
499
- # out1 = run.main(["resources/embedded-image/h264-png.mp4"], [])
404
+ # def embedded_image(self):
405
+ # out1 = self.main(["resources/embedded-image/h264-png.mp4"], [])
500
406
  # cn = fileinfo(out1)
501
407
  # assert cn.videos[0].codec == "h264"
502
408
  # assert cn.videos[1].codec == "png"
503
409
 
504
- # out2 = run.main(["resources/embedded-image/h264-mjpeg.mp4"], [])
410
+ # out2 = self.main(["resources/embedded-image/h264-mjpeg.mp4"], [])
505
411
  # cn = fileinfo(out2)
506
412
  # assert cn.videos[0].codec == "h264"
507
413
  # assert cn.videos[1].codec == "mjpeg"
508
414
 
509
- # out3 = run.main(["resources/embedded-image/h264-png.mkv"], [])
415
+ # out3 = self.main(["resources/embedded-image/h264-png.mkv"], [])
510
416
  # cn = fileinfo(out3)
511
417
  # assert cn.videos[0].codec == "h264"
512
418
  # assert cn.videos[1].codec == "png"
513
419
 
514
- # out4 = run.main(["resources/embedded-image/h264-mjpeg.mkv"], [])
420
+ # out4 = self.main(["resources/embedded-image/h264-mjpeg.mkv"], [])
515
421
  # cn = fileinfo(out4)
516
422
  # assert cn.videos[0].codec == "h264"
517
423
  # assert cn.videos[1].codec == "mjpeg"
518
424
 
519
- # return out1, out2, out3, out4
520
-
521
- def motion():
522
- out = run.main(
425
+ def test_motion(self):
426
+ self.main(
523
427
  ["resources/only-video/man-on-green-screen.mp4"],
524
428
  ["--edit", "motion", "--margin", "0"],
525
429
  )
526
- out2 = run.main(
430
+ self.main(
527
431
  ["resources/only-video/man-on-green-screen.mp4"],
528
432
  ["--edit", "motion:threshold=0,width=200"],
529
433
  )
530
- return out, out2
531
434
 
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(
435
+ def test_edit_positive(self):
436
+ self.main(["resources/multi-track.mov"], ["--edit", "audio:stream=all"])
437
+ self.main(["resources/multi-track.mov"], ["--edit", "not audio:stream=all"])
438
+ self.main(
536
439
  ["resources/multi-track.mov"],
537
440
  ["--edit", "(or (not audio:threshold=4%) audio:stream=1)"],
538
441
  )
539
- out = run.main(
442
+ self.main(
540
443
  ["resources/multi-track.mov"],
541
444
  ["--edit", "(or (not audio:threshold=4%) (not audio:stream=1))"],
542
445
  )
543
- return out
544
446
 
545
- def edit_negative_tests():
546
- run.check(
447
+ def test_edit_negative(self):
448
+ self.check(
547
449
  ["resources/wav/example-cut-s16le.wav", "--edit", "motion"],
548
- "video stream '0' does not ",
450
+ "video stream",
549
451
  )
550
- run.check(
452
+ self.check(
551
453
  ["resources/only-video/man-on-green-screen.gif", "--edit", "audio"],
552
- "audio stream '0' does not ",
454
+ "audio stream",
553
455
  )
554
456
 
555
- def yuv442p():
556
- return run.main(["resources/test_yuv422p.mp4"], [])
457
+ def test_yuv442p(self):
458
+ self.main(["resources/test_yuv422p.mp4"], [])
557
459
 
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"
460
+ def test_prores(self):
461
+ out = self.main(["resources/testsrc.mp4"], ["-c:v", "prores"], "prores.mkv")
462
+ assert fileinfo(out).videos[0].pix_fmt == "yuv422p10le"
561
463
 
562
- run.main(["out.mkv", "-c:v", "prores", "-o", "out2.mkv"], [])
563
- assert fileinfo("out2.mkv").videos[0].pix_fmt == "yuv422p10le"
564
-
565
- return "out.mkv", "out2.mkv"
464
+ out2 = self.main([out], ["-c:v", "prores"], "prores2.mkv")
465
+ assert fileinfo(out2).videos[0].pix_fmt == "yuv422p10le"
566
466
 
567
467
  # Issue 280
568
- def SAR():
569
- out = run.main(["resources/SAR-2by3.mp4"], [])
468
+ def test_SAR(self):
469
+ out = self.main(["resources/SAR-2by3.mp4"], [], "2by3_out.mp4")
570
470
  assert fileinfo(out).videos[0].sar == Fraction(2, 3)
571
471
 
572
- return out
573
-
574
- def audio_norm_f():
575
- return run.main(["example.mp4"], ["--audio-normalize", "#f"])
472
+ def test_audio_norm_f(self):
473
+ return self.main(["example.mp4"], ["--audio-normalize", "#f"])
576
474
 
577
- def audio_norm_ebu():
578
- return run.main(
475
+ def test_audio_norm_ebu(self):
476
+ return self.main(
579
477
  ["example.mp4"], ["--audio-normalize", "ebu:i=-5,lra=20,gain=5,tp=-1"]
580
478
  )
581
479
 
582
- def palet_python_bridge():
480
+ def palet_python_bridge(self):
583
481
  env.update(make_standard_env())
584
482
 
585
- def cases(*cases: tuple[str, Any]) -> None:
483
+ def cases(*cases: tuple[str, object]) -> None:
586
484
  for text, expected in cases:
587
485
  try:
588
486
  parser = Parser(Lexer("repl", text))
@@ -730,65 +628,115 @@ def main(sys_args: list[str] | None = None):
730
628
  ('#(#("sym" "symbol?") "bool?")', [["sym", "symbol?"], "bool?"]),
731
629
  )
732
630
 
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"])
631
+ def palet_scripts(self):
632
+ self.raw(["palet", "resources/scripts/scope.pal"])
633
+ self.raw(["palet", "resources/scripts/maxcut.pal"])
634
+ self.raw(["palet", "resources/scripts/case.pal"])
635
+ self.raw(["palet", "resources/scripts/testmath.pal"])
636
+
738
637
 
638
+ def run_tests(runner: Runner, tests: list[Callable], args: TestArgs) -> None:
639
+ if args.only != []:
640
+ tests = list(filter(lambda t: t.__name__ in args.only, tests))
641
+
642
+ total_time = 0.0
643
+ real_time = perf_counter()
644
+ passed = 0
645
+ total = len(tests)
646
+
647
+ def timed_test(test_func):
648
+ start_time = perf_counter()
649
+ try:
650
+ test_func()
651
+ success = True
652
+ except Exception as e:
653
+ success = False
654
+ exception = e
655
+ end_time = perf_counter()
656
+ duration = end_time - start_time
657
+
658
+ if success:
659
+ return (True, duration, None)
660
+ else:
661
+ return (False, duration, exception)
662
+
663
+ with concurrent.futures.ThreadPoolExecutor() as executor:
664
+ future_to_data = {}
665
+ for test in tests:
666
+ future = executor.submit(timed_test, test)
667
+ future_to_data[future] = test
668
+
669
+ index = 0
670
+ for future in concurrent.futures.as_completed(future_to_data):
671
+ test = future_to_data[future]
672
+ name = test.__name__
673
+ success, dur, exception = future.result()
674
+ total_time += dur
675
+ index += 1
676
+
677
+ if success:
678
+ passed += 1
679
+ print(
680
+ f"{name:<26} ({index}/{total}) {round(dur, 2):<4} secs [\033[1;32mPASSED\033[0m]",
681
+ flush=True,
682
+ )
683
+ else:
684
+ print(
685
+ f"{name:<26} ({index}/{total}) {round(dur, 2):<4} secs \033[1;31m[FAILED]\033[0m",
686
+ flush=True,
687
+ )
688
+ if args.no_fail_fast:
689
+ print(f"\n{exception}")
690
+ else:
691
+ print("")
692
+ raise exception
693
+
694
+ real_time = round(perf_counter() - real_time, 2)
695
+ total_time = round(total_time, 2)
696
+ print(
697
+ f"\nCompleted {passed}/{total}\nreal time: {real_time} secs total: {total_time} secs"
698
+ )
699
+
700
+
701
+ def main(sys_args: list[str] | None = None) -> None:
702
+ if sys_args is None:
703
+ sys_args = sys.argv[1:]
704
+
705
+ args = test_options(ArgumentParser("test")).parse_args(TestArgs, sys_args)
706
+ run = Runner()
739
707
  tests = []
740
708
 
709
+ test_methods = {
710
+ name: getattr(run, name)
711
+ for name in dir(Runner)
712
+ if callable(getattr(Runner, name)) and name not in ["main", "raw", "check"]
713
+ }
714
+
741
715
  if args.category in {"palet", "all"}:
742
- tests.extend([palet_python_bridge, palet_scripts])
716
+ tests.extend(
717
+ [test_methods[name] for name in ["palet_python_bridge", "palet_scripts"]]
718
+ )
743
719
 
744
720
  if args.category in {"sub", "all"}:
745
- tests.extend([info, levels, subdump, desc])
721
+ tests.extend(
722
+ [test_methods[name] for name in ["info", "levels", "subdump", "desc"]]
723
+ )
746
724
 
747
725
  if args.category in {"cli", "all"}:
748
726
  tests.extend(
749
727
  [
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,
728
+ getattr(run, name)
729
+ for name in dir(Runner)
730
+ if callable(getattr(Runner, name)) and name.startswith("test_")
788
731
  ]
789
732
  )
790
-
791
- run_tests(tests, args)
733
+ try:
734
+ run_tests(run, tests, args)
735
+ except KeyboardInterrupt:
736
+ print("Testing Interrupted by User.")
737
+ shutil.rmtree(run.temp_dir)
738
+ sys.exit(1)
739
+ shutil.rmtree(run.temp_dir)
792
740
 
793
741
 
794
742
  if __name__ == "__main__":