diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33dddb93..d2f59237 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,14 +2,14 @@ name: build-workflow on: workflow_dispatch: - push: - paths-ignore: - - "doc/**" - - "html/**" - - "**.md" - - "THANKS" - - "LICENSE" - - "NOTICE" + # push: + # paths-ignore: + # - "doc/**" + # - "html/**" + # - "**.md" + # - "THANKS" + # - "LICENSE" + # - "NOTICE" jobs: build-windows: diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 00000000..9b387639 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,54 @@ +name: e2e-test + +on: + push: + paths-ignore: + - "doc/**" + - "html/**" + - "**.md" + - "THANKS" + - "LICENSE" + - "NOTICE" + +jobs: + e2e-test-ubuntu: + strategy: + fail-fast: false + matrix: + os: + - ubuntu-22.04 + # - ubuntu-24.04 + env: + TEST_SIGNALING_URLS: ${{ secrets.TEST_SIGNALING_URLS }} + TEST_CHANNEL_ID_PREFIX: ${{ secrets.TEST_CHANNEL_ID_PREFIX }} + TEST_SECRET_KEY: ${{ secrets.TEST_SECRET_KEY }} + MOMO_VERSION: "2024.1.0" + MOMO_ARCH: "x86_64" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - run: | + sudo apt update + sudo apt install -y ffmpeg v4l2loopback-dkms v4l2loopback-utils linux-modules-extra-$(uname -r) + - run: | + curl -LO https://github.com/shiguredo/momo/releases/download/${{ env.MOMO_VERSION}}/momo-${{ env.MOMO_VERSION }}_${{ matrix.os }}_${{ env.MOMO_ARCH }}.tar.gz + tar -xzf momo-${{ env.MOMO_VERSION }}_${{ matrix.os }}_${{ env.MOMO_ARCH }}.tar.gz + mkdir -p _build/${{ matrix.os }}_${{ env.MOMO_ARCH }}/release/momo + # chmod 755 momo-${{ env.MOMO_VERSION }}_${{ matrix.os }}_${{ env.MOMO_ARCH }}/momo + chmod 755 momo/momo + # mv momo-${{ env.MOMO_VERSION }}_${{ matrix.os }}_${{ env.MOMO_ARCH }}/momo ${{ github.workspace }}/_build/${{ matrix.os }}_${{ env.MOMO_ARCH }}/release/momo/ + mv momo/momo ${{ github.workspace }}/_build/${{ matrix.os }}_${{ env.MOMO_ARCH }}/release/momo/ + - uses: astral-sh/setup-uv@v3 + - name: setup v4l2loopback + run: | + sudo modprobe v4l2loopback devices=1 video_nr=0 exclusive_caps=1 card_label='VCamera' + ls -l /dev/video0 + - run: | + sudo v4l2-ctl --device /dev/video0 --all + sudo v4l2-ctl --device /dev/video0 --list-formats-ext + - run: /home/runner/work/momo/momo/_build/ubuntu-22.04_x86_64/release/momo/momo --version + - run: uv sync + working-directory: ./test + - run: | + uv run pytest test_momo.py -s + working-directory: ./test \ No newline at end of file diff --git a/.gitignore b/.gitignore index d9bb54c3..d9ea684a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,15 @@ webrtc_logs_0 /_source /_install /_package + +# python +.venv +.pytest_cache +__pycache__ + +# vscode +build + +# .env +.env +!.env.template diff --git a/CHANGES.md b/CHANGES.md index b283fee9..07b0f4ae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -97,6 +97,8 @@ ### misc +- [ADD] pytest を利用した E2E テストを追加 + - @voluntas - [CHANGE] SDL2 のダウンロード先を GitHub に変更する - @voluntas - [UPDATE] Github Actions の actions/download-artifact をアップデート diff --git a/test/.env.template b/test/.env.template new file mode 100644 index 00000000..8a764035 --- /dev/null +++ b/test/.env.template @@ -0,0 +1,4 @@ +TEST_SIGNALING_URLS=wss://sora.example.com/signaling +TEST_CHANNEL_ID_PREFIX=momo_ +TEST_SECRET_KEY=secret +TEST_API_URL=https://sora.example.com/api diff --git a/test/.python-version b/test/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/test/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..914df6d9 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,36 @@ +import os + +import pytest +from dotenv import load_dotenv + + +@pytest.fixture +def setup(): + # 環境変数読み込み + load_dotenv() + + # signaling_url 単体か複数かをランダムで決めてテストする + if (test_signaling_urls := os.environ.get("TEST_SIGNALING_URLS")) is None: + raise ValueError("TEST_SIGNALING_URLS is required.") + + # , で区切って ['wss://...', ...] に変換 + test_signaling_urls = test_signaling_urls.split(",") + + if (test_channel_id_prefix := os.environ.get("TEST_CHANNEL_ID_PREFIX")) is None: + raise ValueError("TEST_CHANNEL_ID_PREFIX is required.") + + if (test_secret_key := os.environ.get("TEST_SECRET_KEY")) is None: + raise ValueError("TEST_SECRET_KEY is required.") + + # if (test_api_url := os.environ.get("TEST_API_URL")) is None: + # raise ValueError("TEST_API_URL is required.") + + return { + "signaling_urls": test_signaling_urls, + "channel_id_prefix": test_channel_id_prefix, + # "secret": test_secret_key, + # "api_url": test_api_url, + "metadata": {"access_token": test_secret_key}, + # openh264_path は str | None でよい + # "openh264_path": os.environ.get("OPENH264_PATH"), + } diff --git a/test/momo_sora.py b/test/momo_sora.py new file mode 100644 index 00000000..7b5567e4 --- /dev/null +++ b/test/momo_sora.py @@ -0,0 +1,113 @@ +import json +import platform +import signal +import subprocess +import sys +import threading +import time +import uuid +from pathlib import Path + +# プラットフォームに応じたリリースディレクトリの設定 +RELEASE_DIR = Path(__file__).resolve().parent.parent / Path("_build/") + +# TODO: 環境変数で CI か Local で見に行くパスを変えるようにする +if platform.system() == "Darwin": + if platform.machine() == "arm64": + RELEASE_DIR = RELEASE_DIR / "macos_arm64/release/momo" +elif platform.system() == "Linux": + # ubuntu 24.04 かどうかの確認が必要 + # ubuntu 22.04 と 24.04 がある + RELEASE_DIR = RELEASE_DIR / "ubuntu-22.04_x86_64/release/momo" +else: + raise OSError(f"Unsupported platform: {platform.system()}") + + +class Momo: + signaling_urls: list[str] + channel_id_prefix: str + metadata: dict[str, str] + port: int + + def __init__( + self, + signaling_urls: list[str], + channel_id_prefix: str, + metadata: dict[str, str], + ): + self.signaling_urls = signaling_urls + self.channel_id_prefix = channel_id_prefix + self.metadata = metadata + + self.channel_id = f"{self.channel_id_prefix}_{uuid.uuid4()}" + + self.port = 5000 + + self.executable = RELEASE_DIR / "momo" + assert self.executable.exists() + self.process = None + self.thread = None + self.is_running = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + + def run_app(self): + print(self.executable) + args = [ + str(self.executable), + "sora", + "--signaling-urls", + ",".join(self.signaling_urls), + "--channel-id", + self.channel_id_prefix, + # "--video-device", + # これは GitHub Actions 用 + # "VCamera", + "--metadata", + json.dumps(self.metadata), + ] + try: + self.process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.process.wait() + except Exception as e: + print(f"Error running momo: {e}", file=sys.stderr) + finally: + self.is_running = False + + def start(self): + if not self.executable.exists(): + raise FileNotFoundError(f"Executable file not found: {self.executable}") + + self.thread = threading.Thread(target=self.run_app) + self.thread.start() + self.is_running = True + + # momoの起動を確認 + start_time = time.time() + while time.time() - start_time < 10: # 10秒のタイムアウト + if self.process and self.process.poll() is None: + print("Momo started") + return + time.sleep(0.1) + + raise TimeoutError("Momo failed to start within the timeout period") + + def stop(self): + if self.is_running and self.process: + # SIGINT を送信 (Ctrl+C と同等) + self.process.send_signal(signal.SIGINT) + try: + # プロセスが終了するのを最大5秒間待つ + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + # タイムアウトした場合、強制終了 + print("Momo did not terminate gracefully. Forcing termination.", file=sys.stderr) + self.process.kill() + + self.thread.join(timeout=5) + self.is_running = False + print("Momo stopped") diff --git a/test/pyproject.toml b/test/pyproject.toml new file mode 100644 index 00000000..3a615da8 --- /dev/null +++ b/test/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "momo-e2e-test" +version = "2013.3.8" +requires-python = ">= 3.12" +dependencies = [ + "pytest>=8.3", + "pytest-timeout>=2.3", + "python-dotenv>=1.0", +] + +[tool.uv] +package = false +managed = true +dev-dependencies = ["ruff"] + +[tool.pytest.ini_options] +timeout = 45 + +[tool.ruff] +target-version = "py312" +line-length = 100 diff --git a/test/test_momo.py b/test/test_momo.py new file mode 100644 index 00000000..2fc442e7 --- /dev/null +++ b/test/test_momo.py @@ -0,0 +1,12 @@ +import time + +from momo_sora import Momo + + +def test_start_and_stop(setup): + with Momo(**setup) as momo: + momo.start() + + time.sleep(3) + + momo.stop() diff --git a/test/uv.lock b/test/uv.lock new file mode 100644 index 00000000..0ad4f555 --- /dev/null +++ b/test/uv.lock @@ -0,0 +1,124 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "momo-e2e-test" +version = "2013.3.8" +source = { virtual = "." } +dependencies = [ + { name = "pytest" }, + { name = "pytest-timeout" }, + { name = "python-dotenv" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "pytest", specifier = ">=8.3" }, + { name = "pytest-timeout", specifier = ">=2.3" }, + { name = "python-dotenv", specifier = ">=1.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "ruff" }] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-timeout" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "ruff" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/51/231bb3790e5b0b9fd4131f9a231d73d061b3667522e3f406fd9b63334d0e/ruff-0.7.2.tar.gz", hash = "sha256:2b14e77293380e475b4e3a7a368e14549288ed2931fce259a6f99978669e844f", size = 3210036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/56/0caa2b5745d66a39aa239c01059f6918fc76ed8380033d2f44bf297d141d/ruff-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:b73f873b5f52092e63ed540adefc3c36f1f803790ecf2590e1df8bf0a9f72cb8", size = 10373973 }, + { url = "https://files.pythonhosted.org/packages/1a/33/cad6ff306731f335d481c50caa155b69a286d5b388e87ff234cd2a4b3557/ruff-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5b813ef26db1015953daf476202585512afd6a6862a02cde63f3bafb53d0b2d4", size = 10171140 }, + { url = "https://files.pythonhosted.org/packages/97/f5/6a2ca5c9ba416226eac9cf8121a1baa6f06655431937e85f38ffcb9d0d01/ruff-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:853277dbd9675810c6826dad7a428d52a11760744508340e66bf46f8be9701d9", size = 9809333 }, + { url = "https://files.pythonhosted.org/packages/16/83/e3e87f13d1a1dc205713632978cd7bc287a59b08bc95780dbe359b9aefcb/ruff-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21aae53ab1490a52bf4e3bf520c10ce120987b047c494cacf4edad0ba0888da2", size = 10622987 }, + { url = "https://files.pythonhosted.org/packages/22/16/97ccab194480e99a2e3c77ae132b3eebfa38c2112747570c403a4a13ba3a/ruff-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc7e0fc6e0cb3168443eeadb6445285abaae75142ee22b2b72c27d790ab60ba", size = 10184640 }, + { url = "https://files.pythonhosted.org/packages/97/1b/82ff05441b036f68817296c14f24da47c591cb27acfda473ee571a5651ac/ruff-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd77877a4e43b3a98e5ef4715ba3862105e299af0c48942cc6d51ba3d97dc859", size = 11210203 }, + { url = "https://files.pythonhosted.org/packages/a6/96/7ecb30a7ef7f942e2d8e0287ad4c1957dddc6c5097af4978c27cfc334f97/ruff-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e00163fb897d35523c70d71a46fbaa43bf7bf9af0f4534c53ea5b96b2e03397b", size = 11870894 }, + { url = "https://files.pythonhosted.org/packages/06/6a/c716bb126218227f8e604a9c484836257708a05ee3d2ebceb666ff3d3867/ruff-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3c54b538633482dc342e9b634d91168fe8cc56b30a4b4f99287f4e339103e88", size = 11449533 }, + { url = "https://files.pythonhosted.org/packages/e6/2f/3a5f9f9478904e5ae9506ea699109070ead1e79aac041e872cbaad8a7458/ruff-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b792468e9804a204be221b14257566669d1db5c00d6bb335996e5cd7004ba80", size = 12607919 }, + { url = "https://files.pythonhosted.org/packages/a0/57/4642e57484d80d274750dcc872ea66655bbd7e66e986fede31e1865b463d/ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba53ed84ac19ae4bfb4ea4bf0172550a2285fa27fbb13e3746f04c80f7fa088", size = 11016915 }, + { url = "https://files.pythonhosted.org/packages/4d/6d/59be6680abee34c22296ae3f46b2a3b91662b8b18ab0bf388b5eb1355c97/ruff-0.7.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b19fafe261bf741bca2764c14cbb4ee1819b67adb63ebc2db6401dcd652e3748", size = 10625424 }, + { url = "https://files.pythonhosted.org/packages/82/e7/f6a643683354c9bc7879d2f228ee0324fea66d253de49273a0814fba1927/ruff-0.7.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28bd8220f4d8f79d590db9e2f6a0674f75ddbc3847277dd44ac1f8d30684b828", size = 10233692 }, + { url = "https://files.pythonhosted.org/packages/d7/48/b4e02fc835cd7ed1ee7318d9c53e48bcf6b66301f55925a7dcb920e45532/ruff-0.7.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9fd67094e77efbea932e62b5d2483006154794040abb3a5072e659096415ae1e", size = 10751825 }, + { url = "https://files.pythonhosted.org/packages/1e/06/6c5ee6ab7bb4cbad9e8bb9b2dd0d818c759c90c1c9e057c6ed70334b97f4/ruff-0.7.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:576305393998b7bd6c46018f8104ea3a9cb3fa7908c21d8580e3274a3b04b691", size = 11074811 }, + { url = "https://files.pythonhosted.org/packages/a1/16/8969304f25bcd0e4af1778342e63b715e91db8a2dbb51807acd858cba915/ruff-0.7.2-py3-none-win32.whl", hash = "sha256:fa993cfc9f0ff11187e82de874dfc3611df80852540331bc85c75809c93253a8", size = 8650268 }, + { url = "https://files.pythonhosted.org/packages/d9/18/c4b00d161def43fe5968e959039c8f6ce60dca762cec4a34e4e83a4210a0/ruff-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dd8800cbe0254e06b8fec585e97554047fb82c894973f7ff18558eee33d1cb88", size = 9433693 }, + { url = "https://files.pythonhosted.org/packages/7f/7b/c920673ac01c19814dd15fc617c02301c522f3d6812ca2024f4588ed4549/ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760", size = 8735845 }, +]