From 16df13981f6c5dfd6b39fbec21ef14970eabdaca Mon Sep 17 00:00:00 2001 From: LightArrowsEXE Date: Wed, 23 Oct 2024 23:24:45 +0200 Subject: [PATCH] Base extension, assertions, and helpers --- .github/workflows/lint.yml | 39 +++++++ .github/workflows/matchers/mypy.json | 16 +++ .github/workflows/test.yml | 42 ++++++++ .gitignore | 151 +++++++++++++++++++++++++++ LICENSE | 21 ++++ README.md | 21 ++++ pytest.ini | 2 + requirements-dev.txt | 4 + requirements.txt | 2 + setup.py | 41 ++++++++ vsunittest/__init__.py | 6 ++ vsunittest/_metadata.py | 12 +++ vsunittest/assertions.py | 57 ++++++++++ vsunittest/core.py | 12 +++ vsunittest/py.typed | 0 vsunittest/types.py | 8 ++ vsunittest/utils.py | 19 ++++ 17 files changed, 453 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/matchers/mypy.json create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pytest.ini create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 vsunittest/__init__.py create mode 100644 vsunittest/_metadata.py create mode 100644 vsunittest/assertions.py create mode 100644 vsunittest/core.py create mode 100644 vsunittest/py.typed create mode 100644 vsunittest/types.py create mode 100644 vsunittest/utils.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..a0dfc95 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,39 @@ +name: Lint Python code with Ruff + +on: + push: + branches: + - master + paths: + - '**.py' + pull_request: + paths: + - '**.py' + +jobs: + windows: + runs-on: windows-latest + strategy: + matrix: + vs-versions: + - 68 + python-version: + - '3.12' + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install vapoursynth-portable==${{ matrix.vs-versions }} + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Running ruff + run: ruff check vsunittest diff --git a/.github/workflows/matchers/mypy.json b/.github/workflows/matchers/mypy.json new file mode 100644 index 0000000..c1e81a9 --- /dev/null +++ b/.github/workflows/matchers/mypy.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "mypy", + "pattern": [ + { + "regexp": "^(.+):(\\d+):\\s(error|warning|note):\\s(.+)$", + "file": 1, + "line": 2, + "severity": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..042c26f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,42 @@ +name: Test Python code + +on: + push: + branches: + - master + paths: + - '**.py' + pull_request: + paths: + - '**.py' + +jobs: + windows: + runs-on: windows-latest + strategy: + matrix: + vs-versions: + - 68 + python-version: + - '3.12' + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install vapoursynth-portable==${{ matrix.vs-versions }} + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Running tests + run: pytest --cov-report=term-missing:skip-covered --cov=vsunittest tests + + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2.3.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..784a86b --- /dev/null +++ b/.gitignore @@ -0,0 +1,151 @@ +*ffindex +.vsjet/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# code editors +.vscode/ +.idea/ + +# Pyre type checker +.pyre/ + +# Local test file +*test*.py + +# Unignore tests dir +!tests/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fafc675 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Jaded Encoding Thaumaturgy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..713d237 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# vs-unittest + +[![Coverage Status](https://coveralls.io/repos/github/Jaded-Encoding-Thaumaturgy/vs-unittest/badge.svg?branch=master)](https://coveralls.io/github/Jaded-Encoding-Thaumaturgy/vs-unittest?branch=master) + +This module contains utilities for writing unittest for VapourSynth scripts. + +For support you can check out the [JET Discord server](https://discord.gg/XTpc6Fa9eB).

+ +## How to install + +Install `vsunittest` with the following command: + +```sh +pip install vsunittest +``` + +Or if you want the latest git version, install it with this command: + +```sh +pip install git+https://github.com/Jaded-Encoding-Thaumaturgy/vs-unittest.git +``` diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..03f586d --- /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 new file mode 100644 index 0000000..0c519e5 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +packaging>=24.0 +pycodestyle>=2.11.1 +ruff>=0.6.5 +pytest-cov>=5.0.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..20ce522 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +VapourSynth>=68 +pytest>=7.3.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6eb4187 --- /dev/null +++ b/setup.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import setuptools +from pathlib import Path + +package_name = 'vsunittest' + +exec(Path(f'{package_name}/_metadata.py').read_text(), meta := dict[str, str]()) + +readme = Path('README.md').read_text() +requirements = Path('requirements.txt').read_text() + + +setuptools.setup( + name=package_name, + version=meta['__version__'], + author=meta['__author_name__'], + author_email=meta['__author_email__'], + maintainer=meta['__maintainer_name__'], + maintainer_email=meta['__maintainer_email__'], + description=meta['__doc__'], + long_description=readme, + long_description_content_type='text/markdown', + project_urls={ + 'Source Code': 'https://github.com/Jaded-Encoding-Thaumaturgy/vs-unittest', + 'Contact': 'https://discord.gg/XTpc6Fa9eB', + }, + install_requires=requirements, + python_requires='>=3.12', + packages=[ + package_name, + ], + package_data={ + package_name: ['py.typed'] + }, + classifiers=[ + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + ], +) diff --git a/vsunittest/__init__.py b/vsunittest/__init__.py new file mode 100644 index 0000000..767db97 --- /dev/null +++ b/vsunittest/__init__.py @@ -0,0 +1,6 @@ +# ruff: noqa: F401, F403 + +from .assertions import * +from .core import VSTestCase +from .types import * +from .utils import * diff --git a/vsunittest/_metadata.py b/vsunittest/_metadata.py new file mode 100644 index 0000000..19513af --- /dev/null +++ b/vsunittest/_metadata.py @@ -0,0 +1,12 @@ +"""Unittest utilities for VapourSynth.""" + +__version__ = '0.1.0' + +__author_name__, __author_email__ = 'LightArrowsEXE', 'lightarrowsreboot@gmail.com' +__maintainer_name__, __maintainer_email__ = __author_name__, __author_email__ + +__author__ = f'{__author_name__} <{__author_email__}>' +__maintainer__ = __author__ + +if __name__ == '__github__': + print(__version__) diff --git a/vsunittest/assertions.py b/vsunittest/assertions.py new file mode 100644 index 0000000..57246cc --- /dev/null +++ b/vsunittest/assertions.py @@ -0,0 +1,57 @@ +import vapoursynth as vs + +from .core import VSTestCase +from .types import HoldsVideoFrameT + +__all__ = [ + 'assert_props_equal', + 'assert_planestats_equal' +] + + +def assert_props_equal( + self, clip_a: HoldsVideoFrameT, clip_b: HoldsVideoFrameT, + props: list[str] | None = None +) -> None: + """Assert that two clips have equal properties.""" + + if props is None: + props = clip_a.get_frame(0).props.keys() + + for prop in props: + self.assertEqual( + clip_a.get_frame(0).props[prop], + clip_b.get_frame(0).props[prop], + f"Property '{prop}' does not match" + ) + +def assert_planestats_equal( + self, clip_a: HoldsVideoFrameT, clip_b: HoldsVideoFrameT, + plane: int = 0, tolerance: float = 0.0 +) -> None: + """Assert that two clips have equal planestats for a specific plane within a given tolerance.""" + + stats_a = self.core.std.ShufflePlanes( + self.core.std.PlaneStats(clip_a, plane=plane), + plane, vs.GRAY + ) + + stats_b = self.core.std.ShufflePlanes( + self.core.std.PlaneStats(clip_b, plane=plane), + plane, vs.GRAY + ) + + frame_a = stats_a.get_frame(0) + frame_b = stats_b.get_frame(0) + + for stat in ['PlaneStatsMin', 'PlaneStatsMax', 'PlaneStatsAverage']: + self.assertAlmostEqual( + frame_a.props[stat], + frame_b.props[stat], + delta=tolerance, + msg=f"{stat} for plane {plane} does not match within tolerance ({tolerance})" + ) + +# Add these methods to VSTestCase +VSTestCase.assert_props_equal = assert_props_equal +VSTestCase.assert_planestats_equal = assert_planestats_equal diff --git a/vsunittest/core.py b/vsunittest/core.py new file mode 100644 index 0000000..8228e39 --- /dev/null +++ b/vsunittest/core.py @@ -0,0 +1,12 @@ +import unittest + +import vapoursynth as vs + +__all__ = [ + 'VSTestCase' +] + + +class VSTestCase(unittest.TestCase): + def setUp(self) -> None: + self.core = vs.core diff --git a/vsunittest/py.typed b/vsunittest/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/vsunittest/types.py b/vsunittest/types.py new file mode 100644 index 0000000..2fc20c6 --- /dev/null +++ b/vsunittest/types.py @@ -0,0 +1,8 @@ +import vapoursynth as vs + +__all__ = [ + 'HoldsVideoFrameT' +] + + +HoldsVideoFrameT = vs.VideoFrame | vs.VideoNode diff --git a/vsunittest/utils.py b/vsunittest/utils.py new file mode 100644 index 0000000..6ca42f6 --- /dev/null +++ b/vsunittest/utils.py @@ -0,0 +1,19 @@ +from typing import Any + +import vapoursynth as vs + +__all__ = [ + 'create_blank_clip' +] + +core = vs.core + + +def create_blank_clip( + width: int = 1920, height: int = 1080, + format: vs.VideoFormat = vs.YUV420P8, + length: int = 1, **kwargs: Any +) -> vs.VideoNode: + """Create a blank clip for testing purposes.""" + + return core.std.BlankClip(width=width, height=height, format=format, length=length, **kwargs)