diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b792f36e..49922b76 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,11 +26,14 @@ jobs: pip install -r requirements-dev.txt pip install --force git+https://github.com/Setsugennoao/stgpytools.git - name: Running flake8 - run: flake8 vstools + run: | + flake8 vstools + flake8 tests - name: Running mypy run: | echo "::add-matcher::.github/workflows/matchers/mypy.json" mypy --no-pretty vstools + mypy --no-pretty tests echo "::remove-matcher owner=mypy::" continue-on-error: true - name: Smoke test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..67296bb4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: test + +on: [push, pull_request] + +jobs: + windows: + runs-on: windows-latest + strategy: + matrix: + versions: + - 68 + python-version: + - '3.12' + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install vapoursynth-portable==${{ matrix.versions }} + pip install -r requirements.txt + pip install -r requirements-dev.txt + - name: Running tests + run: pytest tests \ No newline at end of file diff --git a/.gitignore b/.gitignore index 17a3320a..999ac5e5 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,7 @@ dmypy.json # code editors .vscode/ +.idea/ # Pyre type checker .pyre/ diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..03f586d4 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 2542031e..a76659e6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,4 +3,6 @@ mypy>=1.11.2 mypy-extensions>=1.0.0 packaging>=24.0 pycodestyle>=2.11.1 +pytest>=7.3.1 +pytest-cov>=5.0.0 typing-extensions>=4.12.2 \ No newline at end of file diff --git a/tests/enums/test_color.py b/tests/enums/test_color.py new file mode 100644 index 00000000..485e49c5 --- /dev/null +++ b/tests/enums/test_color.py @@ -0,0 +1,358 @@ +from unittest import TestCase + +import pytest + +from vstools import ( + ColorRange, Matrix, Primaries, Transfer, UnsupportedMatrixError, UnsupportedPrimariesError, + UnsupportedTransferError, vs +) + + +class TestMatrix(TestCase): + def test_is_unknown(self) -> None: + self.assertTrue(Matrix.is_unknown(Matrix.UNKNOWN)) + self.assertTrue(Matrix.is_unknown(2)) + self.assertFalse(Matrix.is_unknown(Matrix.RGB)) + self.assertFalse(Matrix.is_unknown(0)) + + def test_from_res_rgb(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = Matrix.from_res(clip) + self.assertEqual(result, Matrix.RGB) + + def test_from_res_uhd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + result = Matrix.from_res(clip) + self.assertEqual(result, Matrix.BT709) + + def test_from_res_hd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + result = Matrix.from_res(clip) + self.assertEqual(result, Matrix.BT709) + + def test_from_res_sd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=640, height=480) + result = Matrix.from_res(clip) + self.assertEqual(result, Matrix.SMPTE170M) + + def test_from_res_pal(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1024, height=576) + result = Matrix.from_res(clip) + self.assertEqual(result, Matrix.BT470BG) + + def test_from_video_rgb(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = Matrix.from_video(clip) + self.assertEqual(result, Matrix.RGB) + + def test_from_video_property(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + clip = vs.core.std.SetFrameProp(clip, "_Matrix", Matrix.BT709) + result = Matrix.from_video(clip) + self.assertEqual(result, Matrix.BT709) + + def test_apply(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + clip = Matrix.BT709.apply(clip) + result = Matrix.from_video(clip) + self.assertEqual(result, Matrix.BT709) + + def test_from_video_uhd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + result = Matrix.from_video(clip) + self.assertEqual(result, Matrix.BT709) + + def test_from_video_hd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + result = Matrix.from_video(clip) + self.assertEqual(result, Matrix.BT709) + + def test_from_video_sd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=640, height=480) + result = Matrix.from_video(clip) + self.assertEqual(result, Matrix.SMPTE170M) + + def test_from_video_pal(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1024, height=576) + result = Matrix.from_video(clip) + self.assertEqual(result, Matrix.BT470BG) + + def test_from_transfer_unknown(self) -> None: + result = Matrix.from_transfer(Transfer.UNKNOWN) + self.assertEqual(result, Matrix.UNKNOWN) + + def test_from_transfer_unknown_strict(self) -> None: + with self.assertRaises(UnsupportedTransferError): + Matrix.from_transfer(Transfer.UNKNOWN, strict=True) + + def test_from_primaries_unknown(self) -> None: + result = Matrix.from_primaries(Primaries.UNKNOWN) + self.assertEqual(result, Matrix.UNKNOWN) + + def test_from_primaries_unknown_strict(self) -> None: + with self.assertRaises(UnsupportedPrimariesError): + Matrix.from_primaries(Primaries.UNKNOWN, strict=True) + + +class TestTransfer(TestCase): + def test_is_unknown(self) -> None: + self.assertTrue(Transfer.is_unknown(Transfer.UNKNOWN)) + self.assertTrue(Transfer.is_unknown(2)) + self.assertFalse(Transfer.is_unknown(Transfer.BT709)) + self.assertFalse(Transfer.is_unknown(1)) + + def test_from_res_rgb(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = Transfer.from_res(clip) + self.assertEqual(result, Transfer.SRGB) + + def test_from_res_uhd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + result = Transfer.from_res(clip) + self.assertEqual(result, Transfer.BT709) + + def test_from_res_uhd_10b(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P10, width=3840, height=2160) + result = Transfer.from_res(clip) + self.assertEqual(result, Transfer.BT709) + + def test_from_res_uhd_12b(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P12, width=3840, height=2160) + result = Transfer.from_res(clip) + self.assertEqual(result, Transfer.BT709) + + def test_from_res_hd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + result = Transfer.from_res(clip) + self.assertEqual(result, Transfer.BT709) + + def test_from_res_sd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=640, height=480) + result = Transfer.from_res(clip) + self.assertEqual(result, Transfer.BT601) + + def test_from_res_pal(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1024, height=576) + result = Transfer.from_res(clip) + self.assertEqual(result, Transfer.BT601) + + def test_from_video_property(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + clip = vs.core.std.SetFrameProp(clip, "_Transfer", Transfer.BT709) + result = Transfer.from_video(clip) + self.assertEqual(result, Transfer.BT709) + + def test_apply(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + clip = Transfer.BT709.apply(clip) + result = Transfer.from_video(clip) + self.assertEqual(result, Transfer.BT709) + + def test_from_video_rgb(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = Transfer.from_video(clip) + self.assertEqual(result, Transfer.SRGB) + + def test_from_video_uhd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + result = Transfer.from_video(clip) + self.assertEqual(result, Transfer.BT709) + + def test_from_video_uhd_10b(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P10, width=3840, height=2160) + result = Transfer.from_video(clip) + self.assertEqual(result, Transfer.BT709) + + def test_from_video_uhd_12b(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P12, width=3840, height=2160) + result = Transfer.from_video(clip) + self.assertEqual(result, Transfer.BT709) + + def test_from_video_hd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + result = Transfer.from_video(clip) + self.assertEqual(result, Transfer.BT709) + + def test_from_video_sd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=640, height=480) + result = Transfer.from_video(clip) + self.assertEqual(result, Transfer.BT601) + + def test_from_video_pal(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1024, height=576) + result = Transfer.from_video(clip) + self.assertEqual(result, Transfer.BT601) + + def test_from_matrix(self) -> None: + result = Transfer.from_matrix(Matrix.RGB) + self.assertEqual(result, Transfer.SRGB) + + result = Transfer.from_matrix(Matrix.BT709) + self.assertEqual(result, Transfer.BT709) + + result = Transfer.from_matrix(Matrix.BT470BG) + self.assertEqual(result, Transfer.BT470BG) + + result = Transfer.from_matrix(Matrix.SMPTE170M) + self.assertEqual(result, Transfer.BT601) + + result = Transfer.from_matrix(Matrix.SMPTE240M) + self.assertEqual(result, Transfer.SMPTE240M) + + result = Transfer.from_matrix(Matrix.CHROMACL) + self.assertEqual(result, Transfer.SRGB) + + result = Transfer.from_matrix(Matrix.ICTCP) + self.assertEqual(result, Transfer.BT2020_10) + + def test_from_matrix_unknown(self) -> None: + result = Transfer.from_matrix(Matrix.UNKNOWN) + self.assertEqual(result, Transfer.UNKNOWN) + + def test_from_matrix_unknown_strict(self) -> None: + with self.assertRaises(UnsupportedMatrixError): + Transfer.from_matrix(Matrix.UNKNOWN, strict=True) + + def test_from_primaries_unknown(self) -> None: + result = Transfer.from_primaries(Primaries.BT709) + self.assertEqual(result, Transfer.BT709) + + def test_from_primaries_unknown_strict(self) -> None: + with self.assertRaises(UnsupportedPrimariesError): + Transfer.from_primaries(Primaries.BT709, strict=True) + + +class TestPrimaries(TestCase): + def test_is_unknown(self) -> None: + self.assertTrue(Primaries.is_unknown(Primaries.UNKNOWN)) + self.assertTrue(Primaries.is_unknown(2)) + self.assertFalse(Primaries.is_unknown(Primaries.BT709)) + self.assertFalse(Primaries.is_unknown(1)) + + def test_from_res_rgb(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = Primaries.from_res(clip) + self.assertEqual(result, Primaries.BT709) + + def test_from_res_uhd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + result = Primaries.from_res(clip) + self.assertEqual(result, Primaries.BT709) + + def test_from_res_hd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + result = Primaries.from_res(clip) + self.assertEqual(result, Primaries.BT709) + + def test_from_res_sd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=640, height=480) + result = Primaries.from_res(clip) + self.assertEqual(result, Primaries.SMPTE170M) + + def test_from_res_pal(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1024, height=576) + result = Primaries.from_res(clip) + self.assertEqual(result, Primaries.BT470BG) + + def test_from_video_property(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + clip = vs.core.std.SetFrameProp(clip, "_Primaries", Primaries.BT709) + result = Primaries.from_video(clip) + self.assertEqual(result, Primaries.BT709) + + def test_apply(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + clip = Primaries.BT709.apply(clip) + result = Primaries.from_video(clip) + self.assertEqual(result, Primaries.BT709) + + def test_from_video_rgb(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = Primaries.from_video(clip) + self.assertEqual(result, Primaries.BT709) + + def test_from_video_uhd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + result = Primaries.from_video(clip) + self.assertEqual(result, Primaries.BT709) + + def test_from_video_hd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + result = Primaries.from_video(clip) + self.assertEqual(result, Primaries.BT709) + + def test_from_video_sd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=640, height=480) + result = Primaries.from_video(clip) + self.assertEqual(result, Primaries.SMPTE170M) + + def test_from_video_pal(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1024, height=576) + result = Primaries.from_video(clip) + self.assertEqual(result, Primaries.BT470BG) + + def test_from_matrix_unknown(self) -> None: + result = Primaries.from_matrix(Matrix.BT709) + self.assertEqual(result, Primaries.BT709) + + def test_from_matrix_unknown_strict(self) -> None: + with self.assertRaises(UnsupportedMatrixError): + Primaries.from_matrix(Matrix.BT709, strict=True) + + def test_from_transfer_unknown(self) -> None: + result = Primaries.from_transfer(Transfer.BT709) + self.assertEqual(result, Primaries.BT709) + + def test_from_transfer_unknown_strict(self) -> None: + with self.assertRaises(UnsupportedTransferError): + Primaries.from_transfer(Transfer.BT709, strict=True) + + +class TestColorRange(TestCase): + def test_from_res_rgb(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = ColorRange.from_res(clip) + self.assertEqual(result, ColorRange.FULL) + + def test_from_res_yuv(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + result = ColorRange.from_res(clip) + self.assertEqual(result, ColorRange.LIMITED) + + def test_from_video_property(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + clip = vs.core.std.SetFrameProp(clip, "_ColorRange", ColorRange.FULL) + result = ColorRange.from_video(clip) + self.assertEqual(result, ColorRange.FULL) + + def test_apply(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + clip = ColorRange.FULL.apply(clip) + result = ColorRange.from_video(clip) + self.assertEqual(result, ColorRange.FULL) + + def test_from_video_rgb(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = ColorRange.from_video(clip) + self.assertEqual(result, ColorRange.FULL) + + def test_from_video_yuv(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + result = ColorRange.from_video(clip) + self.assertEqual(result, ColorRange.LIMITED) + + def test_value_vs(self) -> None: + self.assertEqual(ColorRange.LIMITED.value_vs, 1) + self.assertEqual(ColorRange.FULL.value_vs, 0) + + def test_value_zimg(self) -> None: + self.assertEqual(ColorRange.LIMITED.value_zimg, 0) + self.assertEqual(ColorRange.FULL.value_zimg, 1) + + def test_value_is_limited(self) -> None: + self.assertTrue(ColorRange.LIMITED.is_limited) + self.assertFalse(ColorRange.FULL.is_limited) + + def test_value_is_full(self) -> None: + self.assertFalse(ColorRange.LIMITED.is_full) + self.assertTrue(ColorRange.FULL.is_full) diff --git a/tests/enums/test_generic.py b/tests/enums/test_generic.py new file mode 100644 index 00000000..73961579 --- /dev/null +++ b/tests/enums/test_generic.py @@ -0,0 +1,133 @@ +from unittest import TestCase + +import pytest + +from vstools import ChromaLocation, FieldBased, Resolution, UnsupportedFieldBasedError, vs + + +class TestChromaLocation(TestCase): + def test_from_res_uhd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + result = ChromaLocation.from_res(clip) + self.assertEqual(result, ChromaLocation.LEFT) + + def test_from_res_hd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + result = ChromaLocation.from_res(clip) + self.assertEqual(result, ChromaLocation.LEFT) + + def test_from_res_sd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=640, height=480) + result = ChromaLocation.from_res(clip) + self.assertEqual(result, ChromaLocation.LEFT) + + def test_from_video_uhd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=3840, height=2160) + result = ChromaLocation.from_video(clip) + self.assertEqual(result, ChromaLocation.LEFT) + + def test_from_video_hd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + result = ChromaLocation.from_video(clip) + self.assertEqual(result, ChromaLocation.LEFT) + + def test_from_video_sd(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=640, height=480) + result = ChromaLocation.from_video(clip) + self.assertEqual(result, ChromaLocation.LEFT) + + def test_get_offsets_from_chroma_location(self) -> None: + off_left, off_top = ChromaLocation.get_offsets(ChromaLocation.LEFT) + self.assertEqual(off_left, -0.5) + self.assertEqual(off_top, 0.0) + + off_left, off_top = ChromaLocation.get_offsets(ChromaLocation.CENTER) + self.assertEqual(off_left, 0.0) + self.assertEqual(off_top, 0.0) + + off_left, off_top = ChromaLocation.get_offsets(ChromaLocation.TOP_LEFT) + self.assertEqual(off_left, -0.5) + self.assertEqual(off_top, -0.5) + + off_left, off_top = ChromaLocation.get_offsets(ChromaLocation.TOP) + self.assertEqual(off_left, 0.0) + self.assertEqual(off_top, -0.5) + + off_left, off_top = ChromaLocation.get_offsets(ChromaLocation.BOTTOM_LEFT) + self.assertEqual(off_left, -0.5) + self.assertEqual(off_top, 0.5) + + off_left, off_top = ChromaLocation.get_offsets(ChromaLocation.BOTTOM) + self.assertEqual(off_left, 0.0) + self.assertEqual(off_top, 0.5) + + def test_get_offsets_from_video(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + off_left, off_top = ChromaLocation.get_offsets(clip) + self.assertEqual(off_left, 0.5) + self.assertEqual(off_top, 0.0) + + clip = vs.core.std.BlankClip(format=vs.YUV422P8) + off_left, off_top = ChromaLocation.get_offsets(clip) + self.assertEqual(off_left, 0.5) + self.assertEqual(off_top, 0.0) + + clip = vs.core.std.BlankClip(format=vs.YUV444P8) + off_left, off_top = ChromaLocation.get_offsets(clip) + self.assertEqual(off_left, 0.0) + self.assertEqual(off_top, 0.0) + + clip = vs.core.std.BlankClip(format=vs.YUV411P8) + off_left, off_top = ChromaLocation.get_offsets(clip) + self.assertEqual(off_left, 2.5) + self.assertEqual(off_top, 0.0) + + clip = vs.core.std.BlankClip(format=vs.YUV410P8) + off_left, off_top = ChromaLocation.get_offsets(clip) + self.assertEqual(off_left, 2.5) + self.assertEqual(off_top, 1.0) + + +class TestFieldBased(TestCase): + def test_from_res(self) -> None: + clip = vs.core.std.BlankClip() + result = FieldBased.from_res(clip) + self.assertEqual(result, FieldBased.PROGRESSIVE) + + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + result = FieldBased.from_res(clip) + self.assertEqual(result, FieldBased.PROGRESSIVE) + + def test_from_video(self) -> None: + clip = vs.core.std.BlankClip() + result = FieldBased.from_video(clip) + self.assertEqual(result, FieldBased.PROGRESSIVE) + + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + result = FieldBased.from_video(clip) + self.assertEqual(result, FieldBased.PROGRESSIVE) + + def test_is_inter(self) -> None: + self.assertTrue(FieldBased.TFF.is_inter) + self.assertTrue(FieldBased.BFF.is_inter) + self.assertFalse(FieldBased.PROGRESSIVE.is_inter) + + def test_field(self) -> None: + self.assertEqual(FieldBased.TFF.field, 1) + self.assertEqual(FieldBased.BFF.field, 0) + with self.assertRaises(UnsupportedFieldBasedError): + FieldBased.PROGRESSIVE.field + + def test_is_tff(self) -> None: + self.assertTrue(FieldBased.TFF.is_tff) + self.assertFalse(FieldBased.BFF.is_tff) + self.assertFalse(FieldBased.PROGRESSIVE.is_tff) + + +class TestResolution(TestCase): + def test_from_video(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=640, height=480) + self.assertEqual(Resolution.from_video(clip), (640, 480)) + + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + self.assertEqual(Resolution.from_video(clip), (1920, 1080)) diff --git a/tests/enums/test_other.py b/tests/enums/test_other.py new file mode 100644 index 00000000..ad39b4dc --- /dev/null +++ b/tests/enums/test_other.py @@ -0,0 +1,70 @@ +from fractions import Fraction +from unittest import TestCase + +from vstools import Dar, Direction, Region, Sar, get_prop, vs + + +class TestDirection(TestCase): + def test_is_axis(self) -> None: + self.assertTrue(Direction.HORIZONTAL.is_axis) + self.assertTrue(Direction.VERTICAL.is_axis) + self.assertFalse(Direction.LEFT.is_axis) + self.assertFalse(Direction.RIGHT.is_axis) + self.assertFalse(Direction.UP.is_axis) + self.assertFalse(Direction.DOWN.is_axis) + + def test_is_way(self) -> None: + self.assertFalse(Direction.HORIZONTAL.is_way) + self.assertFalse(Direction.VERTICAL.is_way) + self.assertTrue(Direction.LEFT.is_way) + self.assertTrue(Direction.RIGHT.is_way) + self.assertTrue(Direction.UP.is_way) + self.assertTrue(Direction.DOWN.is_way) + + +class TestDar(TestCase): + def test_from_size_width_height(self) -> None: + result = Dar.from_size(1920, 1080) + self.assertEqual(result, Dar(16, 9)) + + def test_from_size_clip(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + result = Dar.from_size(clip) + self.assertEqual(result, Dar(16, 9)) + + def test_to_sar(self) -> None: + self.assertEqual(Dar(16, 9).to_sar(1.0, 1080), Sar(1920, 1)) + + +class TestSar(TestCase): + def test_from_ar(self) -> None: + self.assertEqual(Sar.from_ar(16, 9, 1.0, 1080), Sar(1920, 1)) + + def test_from_dar(self) -> None: + self.assertEqual(Sar.from_dar(Dar(16, 9), 1.0, 1080), Sar(1920, 1)) + + def test_apply(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + clip = Sar(1920, 1).apply(clip) + self.assertEqual(get_prop(clip, "_SARNum", int), 1920) + self.assertEqual(get_prop(clip, "_SARDen", int), 1) + + +class TestRegion(TestCase): + def test_framerate(self) -> None: + self.assertEqual(Region.UNKNOWN.framerate, Fraction(0)) + self.assertEqual(Region.NTSC.framerate, Fraction(30000, 1001)) + self.assertEqual(Region.NTSCi.framerate, Fraction(60000, 1001)) + self.assertEqual(Region.PAL.framerate, Fraction(25, 1)) + self.assertEqual(Region.PALi.framerate, Fraction(50, 1)) + self.assertEqual(Region.FILM.framerate, Fraction(24, 1)) + self.assertEqual(Region.NTSC_FILM.framerate, Fraction(24000, 1001)) + + def test_from_framerate(self) -> None: + self.assertEqual(Region.from_framerate(Fraction(0)), Region.UNKNOWN) + self.assertEqual(Region.from_framerate(Fraction(30000, 1001)), Region.NTSC) + self.assertEqual(Region.from_framerate(Fraction(60000, 1001)), Region.NTSCi) + self.assertEqual(Region.from_framerate(Fraction(25, 1)), Region.PAL) + self.assertEqual(Region.from_framerate(Fraction(50, 1)), Region.PALi) + self.assertEqual(Region.from_framerate(Fraction(24, 1)), Region.FILM) + self.assertEqual(Region.from_framerate(Fraction(24000, 1001)), Region.NTSC_FILM) diff --git a/tests/functions/test_clip.py b/tests/functions/test_clip.py new file mode 100644 index 00000000..f00c6f41 --- /dev/null +++ b/tests/functions/test_clip.py @@ -0,0 +1,25 @@ +from unittest import TestCase + +from vstools import FramesLengthError, shift_clip, shift_clip_multi, vs + + +class TestClip(TestCase): + def test_shift_clip(self) -> None: + clip = vs.core.std.BlankClip(length=12) + result = shift_clip(clip, 1) + self.assertEqual(result.num_frames, 12) + + def test_shift_clip_negative(self) -> None: + clip = vs.core.std.BlankClip(length=12) + result = shift_clip(clip, -1) + self.assertEqual(result.num_frames, 12) + + def test_shift_clip_errors_if_offset_too_long(self) -> None: + clip = vs.core.std.BlankClip(length=12) + with self.assertRaises(FramesLengthError): + shift_clip(clip, 12) + + def test_shift_clip_multi(self) -> None: + clip = vs.core.std.BlankClip(length=12) + results = shift_clip_multi(clip, (-3, 3)) + self.assertEqual(len(results), 7) diff --git a/tests/functions/test_funcs.py b/tests/functions/test_funcs.py new file mode 100644 index 00000000..34e0a689 --- /dev/null +++ b/tests/functions/test_funcs.py @@ -0,0 +1,27 @@ +from typing import Callable, cast +from unittest import TestCase + +from vstools import fallback, iterate, kwargs_fallback, vs + + +class TestFuncs(TestCase): + def test_iterate(self) -> None: + result = iterate(5, cast(Callable[[int], int], lambda x: x * 2), 2) + self.assertEqual(result, 20) + + def test_iterate_clip(self) -> None: + clip = vs.core.std.BlankClip() + result = iterate(clip, vs.core.std.Maximum, 3, threshold=0.5) + self.assertEqual(type(result), vs.VideoNode) + + def test_fallback(self) -> None: + self.assertEqual(fallback(5, 6), 5) + self.assertEqual(fallback(None, 6), 6) + + def test_kwargs_fallback(self) -> None: + kwargs = dict( + overlap=1, search=2, block_size=4, sad_mode=8, motion=12, thSAD=16 + ) + self.assertEqual(kwargs_fallback(5, (kwargs, "block_size"), 8), 5) + self.assertEqual(kwargs_fallback(None, (kwargs, "block_size"), 8), 4) + self.assertEqual(kwargs_fallback(None, (dict(), "block_size"), 8), 8) diff --git a/tests/functions/test_normalize.py b/tests/functions/test_normalize.py new file mode 100644 index 00000000..0fb24722 --- /dev/null +++ b/tests/functions/test_normalize.py @@ -0,0 +1,16 @@ +from unittest import TestCase + +from vstools import flatten, normalize_ranges, vs + + +class TestNormalize(TestCase): + def test_flatten(self) -> None: + result: list[str] = flatten(["a", "b", ["c", "d", ["e"]]]) # type: ignore + self.assertEqual(list(result), ["a", "b", "c", "d", "e"]) + + def test_normalize_ranges(self) -> None: + clip = vs.core.std.BlankClip(length=1000) + + self.assertEqual(normalize_ranges(clip, (None, None)), [(0, 999)]) + self.assertEqual(normalize_ranges(clip, (24, -24)), [(24, 975)]) + self.assertEqual(normalize_ranges(clip, [(24, 100), (80, 150)]), [(24, 150)]) diff --git a/tests/functions/test_utils.py b/tests/functions/test_utils.py new file mode 100644 index 00000000..4f1b7f31 --- /dev/null +++ b/tests/functions/test_utils.py @@ -0,0 +1,129 @@ +from unittest import TestCase + +from vstools import ( + ColorRange, DitherType, InvalidColorFamilyError, depth, get_b, get_g, get_r, get_u, get_v, get_y, plane, vs +) + + +class TestDitherType(TestCase): + def test_should_dither_to_float(self) -> None: + result = DitherType.ERROR_DIFFUSION.should_dither(vs.YUV444P8, vs.YUV444PS) + self.assertFalse(result) + + def test_should_dither_from_float(self) -> None: + result = DitherType.ERROR_DIFFUSION.should_dither(vs.YUV444PS, vs.YUV444P8) + self.assertTrue(result) + + def test_should_dither_range_change(self) -> None: + result = DitherType.ERROR_DIFFUSION.should_dither( + vs.YUV444P8, + vs.YUV444P8, + in_range=ColorRange.LIMITED, + out_range=ColorRange.FULL, + ) + self.assertTrue(result) + + result = DitherType.ERROR_DIFFUSION.should_dither( + vs.YUV444P8, + vs.YUV444P8, + in_range=ColorRange.FULL, + out_range=ColorRange.LIMITED, + ) + self.assertTrue(result) + + def test_should_dither_bits_same(self) -> None: + result = DitherType.ERROR_DIFFUSION.should_dither(vs.YUV444P8, vs.YUV444P8) + self.assertFalse(result) + + def test_should_dither_bits_increase(self) -> None: + result = DitherType.ERROR_DIFFUSION.should_dither(vs.YUV444P8, vs.YUV444P16) + self.assertFalse(result) + + def test_should_dither_bits_decrease(self) -> None: + result = DitherType.ERROR_DIFFUSION.should_dither(vs.YUV444P16, vs.YUV444P8) + self.assertTrue(result) + + +class TestUtils(TestCase): + def test_depth(self) -> None: + src_8 = vs.core.std.BlankClip(format=vs.YUV420P8) + src_10 = depth(src_8, 10) + assert src_10.format + self.assertEqual(src_10.format.name, "YUV420P10") + + src2_10 = vs.core.std.BlankClip(format=vs.RGB30) + src2_8 = depth(src2_10, 8, dither_type=DitherType.RANDOM) + assert src2_8.format + self.assertEqual(src2_8.format.name, "RGB24") + + def test_get_y(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + result = get_y(clip) + assert result.format + self.assertEqual(result.format.name, "Gray8") + + def test_get_y_invalid(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + with self.assertRaises(InvalidColorFamilyError): + get_y(clip) + + def test_get_u(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + result = get_u(clip) + assert result.format + self.assertEqual(result.format.name, "Gray8") + + def test_get_u_invalid(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + with self.assertRaises(InvalidColorFamilyError): + get_u(clip) + + def test_get_v(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + result = get_v(clip) + assert result.format + self.assertEqual(result.format.name, "Gray8") + + def test_get_v_invalid(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + with self.assertRaises(InvalidColorFamilyError): + get_v(clip) + + def test_get_r(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = get_r(clip) + assert result.format + self.assertEqual(result.format.name, "Gray8") + + def test_get_r_invalid(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + with self.assertRaises(InvalidColorFamilyError): + get_r(clip) + + def test_get_g(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = get_g(clip) + assert result.format + self.assertEqual(result.format.name, "Gray8") + + def test_get_g_invalid(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + with self.assertRaises(InvalidColorFamilyError): + get_g(clip) + + def test_get_b(self) -> None: + clip = vs.core.std.BlankClip(format=vs.RGB24) + result = get_b(clip) + assert result.format + self.assertEqual(result.format.name, "Gray8") + + def test_get_b_invalid(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + with self.assertRaises(InvalidColorFamilyError): + get_b(clip) + + def test_plane(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8) + result = plane(clip, 0) + assert result.format + self.assertEqual(result.format.name, "Gray8") diff --git a/tests/utils/test_clips.py b/tests/utils/test_clips.py new file mode 100644 index 00000000..ee545a98 --- /dev/null +++ b/tests/utils/test_clips.py @@ -0,0 +1,48 @@ +from unittest import TestCase + +from vstools import Matrix, Primaries, Transfer, finalize_clip, get_prop, initialize_clip, vs + + +class TestClips(TestCase): + def test_finalize_clip(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + clip = finalize_clip(clip, clamp_tv_range=True) + assert clip.format + self.assertEqual(clip.format.bits_per_sample, 10) + + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + clip = finalize_clip(clip, clamp_tv_range=False) + assert clip.format + self.assertEqual(clip.format.bits_per_sample, 10) + + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + clip = finalize_clip(clip, bits=16) + assert clip.format + self.assertEqual(clip.format.bits_per_sample, 16) + + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + clip = finalize_clip(clip, bits=None) + assert clip.format + self.assertEqual(clip.format.bits_per_sample, 8) + + def test_initialize_clip(self) -> None: + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + clip = initialize_clip(clip) + assert clip.format + self.assertEqual(clip.format.bits_per_sample, 16) + self.assertEqual(get_prop(clip, "_Matrix", int), 1) + self.assertEqual(get_prop(clip, "_Primaries", int), 1) + self.assertEqual(get_prop(clip, "_Transfer", int), 1) + + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + clip = initialize_clip( + clip, + matrix=Matrix.SMPTE170M, + transfer=Transfer.BT470BG, + primaries=Primaries.BT470BG, + ) + assert clip.format + self.assertEqual(clip.format.bits_per_sample, 16) + self.assertEqual(get_prop(clip, "_Matrix", int), 6) + self.assertEqual(get_prop(clip, "_Primaries", int), 5) + self.assertEqual(get_prop(clip, "_Transfer", int), 5) diff --git a/tests/utils/test_info.py b/tests/utils/test_info.py new file mode 100644 index 00000000..7af84757 --- /dev/null +++ b/tests/utils/test_info.py @@ -0,0 +1,21 @@ +from unittest import TestCase + +import pytest + +from vstools import get_h, get_w, vs + + +class TestInfo(TestCase): + def test_get_w(self) -> None: + self.assertEqual(get_w(1080, 16 / 9), 1920) + self.assertEqual(get_w(1080, 4 / 3), 1440) + + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + self.assertEqual(get_w(1080, clip), 1920) + + def test_get_h(self) -> None: + self.assertEqual(get_h(1920, 16 / 9), 1080) + self.assertEqual(get_h(1440, 4 / 3), 1080) + + clip = vs.core.std.BlankClip(format=vs.YUV420P8, width=1920, height=1080) + self.assertEqual(get_h(1920, clip), 1080) diff --git a/tests/utils/test_scale.py b/tests/utils/test_scale.py new file mode 100644 index 00000000..e844f4c3 --- /dev/null +++ b/tests/utils/test_scale.py @@ -0,0 +1,112 @@ +from unittest import TestCase + +from vstools import ColorRange, scale_8bit, scale_value, vs + + +class TestScale(TestCase): + def test_scale_value_no_change(self) -> None: + result = scale_value(0, 8, 8) + self.assertEqual(result, 0) + + result = scale_value(24, 8, 8) + self.assertEqual(result, 24) + + result = scale_value(64, 8, 8) + self.assertEqual(result, 64) + + result = scale_value(255, 8, 8) + self.assertEqual(result, 255) + + def test_scale_value_to_10bit(self) -> None: + result = scale_value(0, 8, 10) + self.assertEqual(result, 0) + + result = scale_value(24, 8, 10) + self.assertEqual(result, 96) + + result = scale_value(64, 8, 10) + self.assertEqual(result, 256) + + result = scale_value(255, 8, 10) + self.assertEqual(result, 1020) + + def test_scale_value_from_10bit(self) -> None: + result = scale_value(0, 10, 8) + self.assertEqual(result, 0) + + result = scale_value(96, 10, 8) + self.assertEqual(result, 24) + + result = scale_value(256, 10, 8) + self.assertEqual(result, 64) + + result = scale_value(1020, 10, 8) + self.assertEqual(result, 255) + + def test_scale_value_to_float(self) -> None: + result = scale_value(0, 8, vs.YUV444PS) + self.assertEqual(result, 0) + + result = scale_value(24, 8, vs.YUV444PS) + self.assertEqual(result, 0.0365296803652968) + + result = scale_value(64, 8, vs.YUV444PS) + self.assertEqual(result, 0.2191780821917808) + + result = scale_value(255, 8, vs.YUV444PS) + self.assertEqual(result, 1) + + def test_scale_value_from_float(self) -> None: + result = scale_value(0, vs.YUV444PS, 8) + self.assertEqual(result, 0) + + result = scale_value(0.1, vs.YUV444PS, 8) + self.assertEqual(result, 26) + + result = scale_value(0.25, vs.YUV444PS, 8) + self.assertEqual(result, 64) + + result = scale_value(1, vs.YUV444PS, 8) + self.assertEqual(result, 255) + + def test_scale_value_to_limited(self) -> None: + result = scale_value( + 0, 8, 8, range_in=ColorRange.FULL, range_out=ColorRange.LIMITED + ) + self.assertEqual(result, 16) + + result = scale_value( + 24, 8, 8, range_in=ColorRange.FULL, range_out=ColorRange.LIMITED + ) + self.assertEqual(result, 37) + + result = scale_value( + 64, 8, 8, range_in=ColorRange.FULL, range_out=ColorRange.LIMITED + ) + self.assertEqual(result, 71) + + result = scale_value( + 255, 8, 8, range_in=ColorRange.FULL, range_out=ColorRange.LIMITED + ) + self.assertEqual(result, 235) + + def test_scale_value_from_limited(self) -> None: + result = scale_value( + 0, 8, 8, range_in=ColorRange.LIMITED, range_out=ColorRange.FULL + ) + self.assertEqual(result, 0) + + result = scale_value( + 24, 8, 8, range_in=ColorRange.LIMITED, range_out=ColorRange.FULL + ) + self.assertEqual(result, 9) + + result = scale_value( + 64, 8, 8, range_in=ColorRange.LIMITED, range_out=ColorRange.FULL + ) + self.assertEqual(result, 56) + + result = scale_value( + 235, 8, 8, range_in=ColorRange.LIMITED, range_out=ColorRange.FULL + ) + self.assertEqual(result, 255) diff --git a/vstools/enums/generic.py b/vstools/enums/generic.py index 7530b516..abb6f30b 100644 --- a/vstools/enums/generic.py +++ b/vstools/enums/generic.py @@ -6,7 +6,8 @@ from stgpytools import FuncExceptT from ..exceptions import ( - UndefinedChromaLocationError, UndefinedFieldBasedError, UnsupportedChromaLocationError, UnsupportedFieldBasedError + UndefinedChromaLocationError, UndefinedFieldBasedError, UnsupportedChromaLocationError, + UnsupportedFieldBasedError, UnsupportedSubsamplingError ) from .stubs import _base_from_video, _ChromaLocationMeta, _FieldBasedMeta @@ -102,6 +103,10 @@ def get_offsets( offsets = (2.5, 0) elif subsampling == (2, 2): offsets = (2.5, 1) + elif subsampling == (0, 0): + offsets = (0, 0) + else: + raise UnsupportedSubsamplingError('Unknown subsampling.', cls) return offsets diff --git a/vstools/utils/scale.py b/vstools/utils/scale.py index ffa79938..2b1cb886 100644 --- a/vstools/utils/scale.py +++ b/vstools/utils/scale.py @@ -143,8 +143,10 @@ def scale_value( elif range_out.is_limited: out_value += 16 << (out_fmt.bits_per_sample - 8) + out_value = max(min(out_value, get_peak_value(out_fmt, range_in=ColorRange.FULL)), 0.0) + if out_fmt.sample_type is vs.INTEGER: - out_value = max(min(out_value, get_peak_value(out_fmt, range_in=ColorRange.FULL)), 0.0) + out_value = round(out_value) return out_value