|
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # http://www.apache.org/licenses/LICENSE-2.0 |
4 | 4 | # |
5 | | -"""Tests for File, Image, Text, and Model media types.""" |
| 5 | +"""Tests for File, Image, Text, Video, and Model media types.""" |
6 | 6 |
|
7 | 7 | import os |
8 | 8 | import tempfile |
| 9 | +from types import SimpleNamespace |
9 | 10 | from unittest.mock import MagicMock, patch |
10 | 11 |
|
11 | 12 | import pytest |
12 | | -from litlogger.media import File, Image, Model, Text, _sanitize_version_for_model_name |
| 13 | +from litlogger.media import File, Image, Model, Text, Video, _sanitize_version_for_model_name |
13 | 14 | from litlogger.types import MediaType |
14 | 15 |
|
15 | 16 |
|
@@ -406,6 +407,163 @@ def test_object_repr_before_render(self): |
406 | 407 | assert repr(Image(pil)) == "Image('')" |
407 | 408 |
|
408 | 409 |
|
| 410 | +# --------------------------------------------------------------------------- |
| 411 | +# Video |
| 412 | +# --------------------------------------------------------------------------- |
| 413 | + |
| 414 | + |
| 415 | +class TestVideoInit: |
| 416 | + """Test Video construction.""" |
| 417 | + |
| 418 | + def test_from_path(self): |
| 419 | + video = Video("clip.mp4") |
| 420 | + assert video.path == "clip.mp4" |
| 421 | + assert video._data == "clip.mp4" |
| 422 | + assert video._format == "mp4" |
| 423 | + assert video._fps is None |
| 424 | + |
| 425 | + def test_from_path_with_description(self): |
| 426 | + video = Video("clip.mp4", description="preview clip") |
| 427 | + assert video.description == "preview clip" |
| 428 | + |
| 429 | + def test_custom_format_and_fps(self): |
| 430 | + video = Video("clip.mov", format="mov", fps=12) |
| 431 | + assert video._format == "mov" |
| 432 | + assert video._fps == 12 |
| 433 | + |
| 434 | + def test_media_type(self): |
| 435 | + assert Video("x.mp4")._media_type == MediaType.VIDEO |
| 436 | + |
| 437 | + |
| 438 | +class TestVideoUploadPath: |
| 439 | + """Test Video._get_upload_path for different data types.""" |
| 440 | + |
| 441 | + def test_string_path_nonexistent_returns_path(self): |
| 442 | + video = Video("nonexistent.mp4") |
| 443 | + assert video._get_upload_path() == "nonexistent.mp4" |
| 444 | + |
| 445 | + @patch.object(Video, "_write_moviepy_clip") |
| 446 | + @patch.object(Video, "_moviepy_clip_from_array") |
| 447 | + def test_numpy_frames_render_to_temp(self, mock_clip_from_array, mock_write_moviepy_clip): |
| 448 | + try: |
| 449 | + import numpy as np |
| 450 | + except ImportError: |
| 451 | + pytest.skip("numpy not available") |
| 452 | + |
| 453 | + clip = object() |
| 454 | + mock_clip_from_array.return_value = clip |
| 455 | + |
| 456 | + frames = np.zeros((2, 8, 8, 3), dtype=np.uint8) |
| 457 | + video = Video(frames, fps=12) |
| 458 | + path = video._get_upload_path() |
| 459 | + |
| 460 | + assert path.endswith(".mp4") |
| 461 | + assert os.path.exists(path) |
| 462 | + mock_clip_from_array.assert_called_once() |
| 463 | + mock_write_moviepy_clip.assert_called_once_with(clip, path, 12) |
| 464 | + video._cleanup() |
| 465 | + |
| 466 | + def test_moviepy_clip_from_array_transposes_tchw_and_scales_floats(self): |
| 467 | + try: |
| 468 | + import numpy as np |
| 469 | + except ImportError: |
| 470 | + pytest.skip("numpy not available") |
| 471 | + |
| 472 | + captured: dict[str, object] = {} |
| 473 | + |
| 474 | + class FakeImageSequenceClip: |
| 475 | + def __init__(self, frames, fps): |
| 476 | + captured["frames"] = frames |
| 477 | + captured["fps"] = fps |
| 478 | + |
| 479 | + def fake_import_module(name: str): |
| 480 | + if name == "numpy": |
| 481 | + return np |
| 482 | + if name == "moviepy.video.io.ImageSequenceClip": |
| 483 | + return SimpleNamespace(ImageSequenceClip=FakeImageSequenceClip) |
| 484 | + raise ImportError(name) |
| 485 | + |
| 486 | + frames = np.full((2, 3, 4, 5), 0.5, dtype=np.float32) |
| 487 | + video = Video(frames) |
| 488 | + with patch("litlogger.media.import_module", side_effect=fake_import_module): |
| 489 | + clip = video._moviepy_clip_from_array(frames, fps=7) |
| 490 | + |
| 491 | + assert isinstance(clip, FakeImageSequenceClip) |
| 492 | + assert captured["fps"] == 7 |
| 493 | + rendered_frames = captured["frames"] |
| 494 | + assert isinstance(rendered_frames, list) |
| 495 | + assert len(rendered_frames) == 2 |
| 496 | + assert rendered_frames[0].shape == (4, 5, 3) |
| 497 | + assert rendered_frames[0].dtype == np.uint8 |
| 498 | + assert rendered_frames[0][0, 0, 0] in (127, 128) |
| 499 | + |
| 500 | + def test_moviepy_clip_from_array_promotes_grayscale_to_rgb(self): |
| 501 | + try: |
| 502 | + import numpy as np |
| 503 | + except ImportError: |
| 504 | + pytest.skip("numpy not available") |
| 505 | + |
| 506 | + captured: dict[str, object] = {} |
| 507 | + |
| 508 | + class FakeImageSequenceClip: |
| 509 | + def __init__(self, frames, fps): |
| 510 | + captured["frames"] = frames |
| 511 | + captured["fps"] = fps |
| 512 | + |
| 513 | + def fake_import_module(name: str): |
| 514 | + if name == "numpy": |
| 515 | + return np |
| 516 | + if name == "moviepy.video.io.ImageSequenceClip": |
| 517 | + return SimpleNamespace(ImageSequenceClip=FakeImageSequenceClip) |
| 518 | + raise ImportError(name) |
| 519 | + |
| 520 | + frames = np.zeros((2, 6, 7), dtype=np.uint8) |
| 521 | + video = Video(frames) |
| 522 | + with patch("litlogger.media.import_module", side_effect=fake_import_module): |
| 523 | + video._moviepy_clip_from_array(frames, fps=5) |
| 524 | + |
| 525 | + rendered_frames = captured["frames"] |
| 526 | + assert isinstance(rendered_frames, list) |
| 527 | + assert rendered_frames[0].shape == (6, 7, 3) |
| 528 | + assert captured["fps"] == 5 |
| 529 | + |
| 530 | + def test_numpy_unsupported_shape_raises(self): |
| 531 | + try: |
| 532 | + import numpy as np |
| 533 | + except ImportError: |
| 534 | + pytest.skip("numpy not available") |
| 535 | + |
| 536 | + video = Video(np.zeros((2, 3, 4, 5, 6), dtype=np.uint8)) |
| 537 | + with pytest.raises(ValueError, match="Unsupported array shape"): |
| 538 | + video._moviepy_clip_from_array(video._data, fps=Video.DEFAULT_FPS) |
| 539 | + |
| 540 | + def test_unsupported_type_raises(self): |
| 541 | + video = Video({"not": "a video"}) |
| 542 | + with pytest.raises(TypeError, match="Unsupported video type"): |
| 543 | + video._get_upload_path() |
| 544 | + |
| 545 | + |
| 546 | +class TestVideoCleanup: |
| 547 | + """Test Video._cleanup removes rendered temp files.""" |
| 548 | + |
| 549 | + @patch.object(Video, "_write_moviepy_clip") |
| 550 | + @patch.object(Video, "_moviepy_clip_from_array") |
| 551 | + def test_cleanup_removes_rendered_temp(self, mock_clip_from_array, mock_write_moviepy_clip): |
| 552 | + try: |
| 553 | + import numpy as np |
| 554 | + except ImportError: |
| 555 | + pytest.skip("numpy not available") |
| 556 | + |
| 557 | + mock_clip_from_array.return_value = object() |
| 558 | + video = Video(np.zeros((1, 4, 4, 3), dtype=np.uint8)) |
| 559 | + path = video._get_upload_path() |
| 560 | + assert os.path.exists(path) |
| 561 | + |
| 562 | + video._cleanup() |
| 563 | + assert not os.path.exists(path) |
| 564 | + assert video._temp_path is None |
| 565 | + |
| 566 | + |
409 | 567 | # --------------------------------------------------------------------------- |
410 | 568 | # Text |
411 | 569 | # --------------------------------------------------------------------------- |
|
0 commit comments