From bcfff3e2c10f5529758cc37022e0597fef086f6a Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 20:33:33 +0900 Subject: [PATCH 01/37] =?UTF-8?q?TURN=20URI=20=E3=83=91=E3=83=BC=E3=82=B5?= =?UTF-8?q?=E3=83=BC=E3=81=A8=20TURN=20=E3=83=9D=E3=83=BC=E3=83=88?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SoraClient に turn_ports プロパティを追加 - RFC 7065 に準拠した TURN URI パーサーを実装 - UDP、TCP、TLS のポートを正しく分類 - test_turn.py を追加して TURN ポート取得をテスト - UDP ポートがエフェメラルポート範囲内であることを検証 --- tests/client.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_turn.py | 53 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 tests/test_turn.py diff --git a/tests/client.py b/tests/client.py index 856b5d80..df3e590c 100644 --- a/tests/client.py +++ b/tests/client.py @@ -327,6 +327,69 @@ def disconnect_message(self) -> Optional[dict[str, Any]]: def close_message(self) -> Optional[dict[str, Any]]: return self._close_message + @property + def turn_ports(self) -> dict[str, list[int]]: + """TURN URI (RFC 7065) からポート情報を抽出する""" + ports_map: dict[str, list[int]] = {"udp": [], "tcp": [], "tls": []} + if self._offer_message is None: + return ports_map + + config = self._offer_message.get("config") + if config is None: + return ports_map + + ice_servers = config.get("iceServers", []) + for server in ice_servers: + urls = server.get("urls", []) + if isinstance(urls, str): + urls = [urls] + for uri in urls: + if not (uri.startswith("turn:") or uri.startswith("turns:")): + continue + + # TURN URI を手動でパースする + # RFC 7065: turn:host:port?transport=udp + # RFC 7065: turns:host:port?transport=tcp + is_turns = uri.startswith("turns:") + scheme = "turns:" if is_turns else "turn:" + + # スキームを除去 + uri_without_scheme = uri[len(scheme):] + + # クエリパラメータを分離 + query = "" + if "?" in uri_without_scheme: + uri_without_scheme, query = uri_without_scheme.split("?", 1) + + # ホストとポートを分離 + if ":" not in uri_without_scheme: + continue + + # 最後のコロンでホストとポートを分割 + host_port = uri_without_scheme.rsplit(":", 1) + if len(host_port) != 2: + continue + + try: + port = int(host_port[1]) + except ValueError: + continue + + # turns: の場合は tls + if is_turns: + if port not in ports_map["tls"]: + ports_map["tls"].append(port) + # turn: の場合は query の transport を確認 + else: + transport = "udp" # デフォルトは udp + if query: + params = dict(param.split("=") for param in query.split("&") if "=" in param) + transport = params.get("transport", "udp") + if transport in ports_map and port not in ports_map[transport]: + ports_map[transport].append(port) + + return ports_map + @property def connected(self) -> bool: return self._connected.is_set() diff --git a/tests/test_turn.py b/tests/test_turn.py new file mode 100644 index 00000000..aee6dc18 --- /dev/null +++ b/tests/test_turn.py @@ -0,0 +1,53 @@ +import time + +from client import SoraClient, SoraRole + + +def test_turn_ports(settings): + """TURN ポートが正しく取得できることを確認する""" + with SoraClient( + settings, + SoraRole.SENDONLY, + audio=True, + video=False, + ) as sendonly: + time.sleep(3) + + # offer_message が受信されていることを確認 + assert sendonly.offer_message is not None + + # turn_ports プロパティから TURN ポートを取得 + turn_ports = sendonly.turn_ports + + # turn_ports が辞書であることを確認 + assert isinstance(turn_ports, dict) + + # 必須キーが存在することを確認 + assert "udp" in turn_ports + assert "tcp" in turn_ports + assert "tls" in turn_ports + + # 各値がリストであることを確認 + assert isinstance(turn_ports["udp"], list) + assert isinstance(turn_ports["tcp"], list) + assert isinstance(turn_ports["tls"], list) + + # UDP ポートは必ず1つ以上存在する + assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" + + # UDP ポートがエフェメラルポート範囲内であることを確認 + for port in turn_ports["udp"]: + assert 49152 <= port <= 65535, f"UDP ポートがエフェメラルポート範囲外: {port}" + + # デバッグ用にポート情報を出力 + print(f"TURN ports (UDP): {turn_ports['udp']}") + print(f"TURN ports (TCP): {turn_ports['tcp']}") + print(f"TURN ports (TLS): {turn_ports['tls']}") + + # offer_message の config.iceServers を確認 + if "config" in sendonly.offer_message: + config = sendonly.offer_message["config"] + if "iceServers" in config: + print(f"ICE Servers: {config['iceServers']}") + + sendonly.disconnect() From 1d21b3fe66729b12d50970fc72b99f8a056ac357 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 21:12:14 +0900 Subject: [PATCH 02/37] =?UTF-8?q?tc=20egress=20=E5=B8=AF=E5=9F=9F=E5=88=B6?= =?UTF-8?q?=E9=99=90=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - video_bit_rate を 1000 kbps に設定 - 帯域制限を 500 kbps に設定してテスト --- tests/test_tc.py | 420 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 tests/test_tc.py diff --git a/tests/test_tc.py b/tests/test_tc.py new file mode 100644 index 00000000..5742caa0 --- /dev/null +++ b/tests/test_tc.py @@ -0,0 +1,420 @@ +"""tc egress を使用した帯域制限のテスト。 + +このテストは pyroute2 を使用して Linux の tc (traffic control) により +ローカルインターフェースの egress (送信方向) に帯域制限を適用し、 +TURN 経由での接続に対する効果を検証する。 + +注意: このテストはグローバルにある Sora サーバーへ接続するため、 + tc egress (送信方向) のみを制御できる。 + tc ingress (受信方向) はリモートサーバー側のため制御不可。 +""" + +import os +import subprocess +import time +from typing import Optional + +import pyroute2 +import pytest + +from client import SoraClient, SoraRole + +# TC=1 環境変数が設定されている場合のみテストを実行 +pytestmark = pytest.mark.skipif( + os.getenv("TC") != "1", + reason="TC=1 環境変数が必要", +) + + +def get_default_interface() -> str: + """デフォルトのネットワークインターフェース名を取得する。 + + Returns: + デフォルトルートで使用されているインターフェース名 + """ + try: + with pyroute2.IPRoute() as ipr: + # デフォルトルートを取得(IPv4) + for route in ipr.get_routes(family=2): # AF_INET = 2 + # dst が存在しない場合がデフォルトルート + if not route.get_attr("RTA_DST"): + oif = route.get_attr("RTA_OIF") + if oif: + # インターフェース情報を取得 + links = ipr.get_links(oif) + if links: + ifname = links[0].get_attr("IFLA_IFNAME") + return ifname + except Exception as e: + print(f"デフォルトインターフェースの取得に失敗: {e}") + + # フォールバックとして eth0 を返す + return "eth0" + + +class TCEgressManager: + """tc netem qdisc を使用して egress (送信方向) のネットワーク帯域制限を管理する。""" + + def __init__(self, interface: str = "eth0") -> None: + """ + TC egress 帯域制限マネージャーを初期化する。 + + Args: + interface: tc ルールを適用するネットワークインターフェース + """ + self.interface: str = interface + self._bandwidth_applied: bool = False + self.ipr: Optional["pyroute2.IPRoute"] = None + + def __enter__(self): + """コンテキストマネージャーのエントリ。""" + self.ipr = pyroute2.IPRoute() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """コンテキストマネージャーの終了 - tc ルールをクリーンアップする。""" + try: + self.cleanup() + finally: + if self.ipr: + self.ipr.close() + + def add_bandwidth_limit(self, rate_kbps: int) -> None: + """ + インターフェースの egress に帯域制限を追加する。 + + Args: + rate_kbps: 帯域制限 (Kbps) + + Raises: + IndexError: インターフェースが見つからない場合 + Exception: tc 操作が失敗した場合 + """ + if not self.ipr: + raise RuntimeError("IPRoute が初期化されていません") + + # インターフェースインデックスを取得する + indices = self.ipr.link_lookup(ifname=self.interface) + if not indices: + raise IndexError(f"インターフェース '{self.interface}' が見つかりません") + idx = indices[0] + + # 帯域制限を指定した netem qdisc を追加する + # root=True を指定すると handle は自動的に 0x10000 (= "1:") に設定される + # rate は Kbit 単位で指定する + self.ipr.tc( + "add", + "netem", + idx, + root=True, + rate=f"{rate_kbps}kbit", + ) + self._bandwidth_applied = True + print(f"tc egress 帯域制限を追加: interface={self.interface}, rate={rate_kbps}kbps") + + def add_bandwidth_and_delay(self, rate_kbps: int, delay_ms: int) -> None: + """ + インターフェースの egress に帯域制限と遅延を同時に追加する。 + + Args: + rate_kbps: 帯域制限 (Kbps) + delay_ms: 遅延 (ミリ秒) + + Raises: + IndexError: インターフェースが見つからない場合 + Exception: tc 操作が失敗した場合 + """ + if not self.ipr: + raise RuntimeError("IPRoute が初期化されていません") + + # インターフェースインデックスを取得する + indices = self.ipr.link_lookup(ifname=self.interface) + if not indices: + raise IndexError(f"インターフェース '{self.interface}' が見つかりません") + idx = indices[0] + + # 帯域制限と遅延を同時に設定 + self.ipr.tc( + "add", + "netem", + idx, + root=True, + rate=f"{rate_kbps}kbit", + delay=delay_ms * 1000, # マイクロ秒に変換 + ) + self._bandwidth_applied = True + print( + f"tc egress 帯域制限と遅延を追加: interface={self.interface}, " + f"rate={rate_kbps}kbps, delay={delay_ms}ms" + ) + + def get_stats(self) -> dict: + """tc qdisc の統計情報を取得する。 + + Returns: + 統計情報を含む辞書 (sent_bytes, sent_packets, drops など) + """ + if not self.ipr: + raise RuntimeError("IPRoute が初期化されていません") + + # インターフェースインデックスを取得する + indices = self.ipr.link_lookup(ifname=self.interface) + if not indices: + raise IndexError(f"インターフェース '{self.interface}' が見つかりません") + idx = indices[0] + + # tc qdisc の情報を取得する + for qdisc in self.ipr.get_qdiscs(idx): + # netem qdisc の統計情報を抽出する + if qdisc.get_attr("TCA_OPTIONS"): + stats = { + "sent_bytes": qdisc.get("bytes", 0), + "sent_packets": qdisc.get("packets", 0), + "drops": qdisc.get("drops", 0), + "overlimits": qdisc.get("overlimits", 0), + "requeues": qdisc.get("requeues", 0), + } + return stats + + return {} + + def cleanup(self) -> None: + """インターフェースから tc 帯域制限設定を削除する。""" + if not self._bandwidth_applied: + return + + if not self.ipr: + return + + try: + indices = self.ipr.link_lookup(ifname=self.interface) + if not indices: + return + idx = indices[0] + # qdisc を削除する + # 削除時には kind を指定せず、index と root のみを指定する + self.ipr.tc("del", index=idx, root=True) + self._bandwidth_applied = False + print(f"tc egress 帯域制限を削除: interface={self.interface}") + except Exception as e: + # クリーンアップ時のエラーは無視する (qdisc が存在しない可能性がある) + print(f"tc egress 帯域制限の削除時にエラー (無視): {e}") + + +def verify_tc_settings(interface: str) -> bool: + """tc の設定が存在するか確認する。 + + Args: + interface: ネットワークインターフェース名 + + Returns: + 設定が存在する場合は True + """ + try: + result = subprocess.run( + ["tc", "qdisc", "show", "dev", interface], + capture_output=True, + text=True, + check=True, + ) + # netem が含まれているかチェック + return "netem" in result.stdout + except Exception as e: + print(f"tc 設定の確認に失敗: {e}") + return False + + +def show_tc_stats(interface: str) -> None: + """tc の統計情報を表示する。 + + Args: + interface: ネットワークインターフェース名 + """ + try: + result = subprocess.run( + ["tc", "-s", "qdisc", "show", "dev", interface], + capture_output=True, + text=True, + check=True, + ) + print(f"\ntc 統計情報 ({interface}):") + for line in result.stdout.strip().split("\n"): + print(f" {line}") + except Exception as e: + print(f"tc 統計情報の取得に失敗: {e}") + + +def test_tc_egress_bandwidth_limit(settings): + """TURN ポート取得後に tc egress で帯域制限をかける。""" + print("\n" + "=" * 60) + print("テスト: tc egress 帯域制限 (500kbps) の適用") + print("=" * 60) + + interface = get_default_interface() + print(f"使用するネットワークインターフェース: {interface}") + + with SoraClient( + settings, + SoraRole.SENDONLY, + audio=True, + video=True, + video_bit_rate=1000, + ) as sendonly: + time.sleep(3) + + # offer_message が受信されていることを確認 + assert sendonly.offer_message is not None + + # turn_ports プロパティから TURN ポートを取得 + turn_ports = sendonly.turn_ports + + # UDP ポートが取得できていることを確認 + assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" + + # 最初の UDP ポートを使用 + udp_port = turn_ports["udp"][0] + print(f"TURN UDP ポート: {udp_port}") + + # tc egress で帯域制限を設定 + with TCEgressManager(interface=interface) as tc: + # 帯域制限を設定 (500kbps) + bandwidth_kbps = 500 + print(f"\nステップ 1: tc egress 帯域制限 {bandwidth_kbps}kbps を適用") + tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) + + # tc の設定が存在することを確認 + print("\nステップ 2: tc 設定を確認") + assert verify_tc_settings(interface), "tc の設定が確認できません" + + # tc の統計情報を表示 (適用直後) + show_tc_stats(interface) + + # 接続を維持して帯域制限が有効な状態でテスト + print("\nステップ 3: 帯域制限が有効な状態で接続を維持") + time.sleep(5) + + # tc の統計情報を表示 (接続後) + show_tc_stats(interface) + + # 統計情報を取得 + stats = tc.get_stats() + print(f"\ntc 統計情報 (IPRoute):") + for key, value in stats.items(): + print(f" {key}: {value}") + + print("\n帯域制限が有効な状態でテスト完了") + + # クリーンアップ確認 + result = subprocess.run( + ["tc", "qdisc", "show", "dev", interface], + capture_output=True, + text=True, + check=True, + ) + print(f"\nクリーンアップ後の tc 設定:\n{result.stdout}") + + print("\n結果:") + print(" ✓ テスト成功 (tc egress 帯域制限が適用された)") + print("=" * 60 + "\n") + + +def test_tc_egress_multiple_bandwidth_limits(settings): + """複数の帯域制限パターンをテストする。""" + print("\n" + "=" * 60) + print("テスト: 複数の tc egress 帯域制限パターン") + print("=" * 60) + + interface = get_default_interface() + print(f"使用するネットワークインターフェース: {interface}") + + bandwidth_patterns = [2000, 1000, 500] # Kbps + + with SoraClient( + settings, + SoraRole.SENDRECV, + audio=True, + video=True, + ) as client: + time.sleep(3) + + # turn_ports プロパティから TURN ポートを取得 + turn_ports = client.turn_ports + + print(f"\n取得した TURN ポート:") + print(f" UDP: {turn_ports['udp']}") + print(f" TCP: {turn_ports['tcp']}") + print(f" TLS: {turn_ports['tls']}") + + for i, bandwidth_kbps in enumerate(bandwidth_patterns, 1): + print(f"\n--- パターン {i}: {bandwidth_kbps}kbps ---") + + with TCEgressManager(interface=interface) as tc: + tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) + + # tc の設定が存在することを確認 + assert verify_tc_settings(interface), "tc の設定が確認できません" + + # 統計情報を表示 + show_tc_stats(interface) + + # 接続を維持 + time.sleep(3) + + # 統計情報を再度表示 + stats = tc.get_stats() + print(f"tc 統計情報 (IPRoute):") + for key, value in stats.items(): + print(f" {key}: {value}") + + print("\n結果:") + print(" ✓ テスト成功 (複数の tc egress 帯域制限パターンが適用された)") + print("=" * 60 + "\n") + + +def test_tc_egress_bandwidth_with_delay(settings): + """tc egress で帯域制限と遅延を同時に適用する。""" + print("\n" + "=" * 60) + print("テスト: tc egress 帯域制限 + 遅延の適用") + print("=" * 60) + + interface = get_default_interface() + print(f"使用するネットワークインターフェース: {interface}") + + with SoraClient( + settings, + SoraRole.SENDONLY, + audio=True, + video=False, + ) as sendonly: + time.sleep(3) + + # turn_ports プロパティから TURN ポートを取得 + turn_ports = sendonly.turn_ports + assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" + udp_port = turn_ports["udp"][0] + print(f"TURN UDP ポート: {udp_port}") + + # 帯域制限と遅延を同時に適用 + with TCEgressManager(interface=interface) as tc: + # netem で帯域制限 (500kbps) と遅延 (50ms) を同時に設定 + bandwidth_kbps = 500 + delay_ms = 50 + print(f"\ntc egress netem: rate={bandwidth_kbps}kbps, delay={delay_ms}ms") + + tc.add_bandwidth_and_delay(rate_kbps=bandwidth_kbps, delay_ms=delay_ms) + + # tc の設定が存在することを確認 + assert verify_tc_settings(interface), "tc の設定が確認できません" + + # 統計情報を表示 + show_tc_stats(interface) + + # 接続を維持 + time.sleep(5) + + # 統計情報を再度表示 + show_tc_stats(interface) + + print("\n結果:") + print(" ✓ テスト成功 (tc egress 帯域制限と遅延が同時に適用された)") + print("=" * 60 + "\n") From 95967ce7911ab9c0c4703f503e9ecbf7b78bf3a2 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 21:12:43 +0900 Subject: [PATCH 03/37] =?UTF-8?q?Tailscale=20GitHub=20Action=20=E3=81=AE?= =?UTF-8?q?=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7=E3=83=B3=E3=82=92=20v4=20?= =?UTF-8?q?=E3=81=AB=E6=9B=B4=E6=96=B0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/e2e-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 11aa7555..c9a00033 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -116,7 +116,7 @@ jobs: # Tailscale のセットアップ(self-hosted runner 以外) - if: ${{ runner.environment != 'self-hosted' }} - uses: tailscale/github-action@v3 + uses: tailscale/github-action@v4 with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} From 868e94b7a0038aaf8053ce1d45ce02aef3fdaca8 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 21:12:51 +0900 Subject: [PATCH 04/37] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E4=BE=9D?= =?UTF-8?q?=E5=AD=98=E9=96=A2=E4=BF=82=E3=81=AB=20pytest-timeout=20?= =?UTF-8?q?=E3=81=A8=20pyroute2=20=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 11 ++++++++++- uv.lock | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a7a98fba..7fb27869 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,16 @@ build-backend = "setuptools.build_meta" [dependency-groups] dev = ["nanobind==2.9.2"] -test = ["pytest", "pytest-repeat", "pytest-xdist", "numpy", "httpx", "pyjwt"] +test = [ + "pytest", + "pytest-timeout", + "pytest-repeat", + "pytest-xdist", + "numpy", + "httpx", + "pyjwt", + "pyroute2", +] lint = ["ruff", "ty"] [tool.uv] diff --git a/uv.lock b/uv.lock index 3d441c3b..f788c1e3 100644 --- a/uv.lock +++ b/uv.lock @@ -224,6 +224,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] +[[package]] +name = "pyroute2" +version = "0.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "win-inet-pton", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/5e/fc64e211cce0078555c6db98aaf14348aed527565f3c4876913a290a5b2c/pyroute2-0.9.4.tar.gz", hash = "sha256:3cbccbe1af0c2b2aeae81b327e0e91aa94c81ab19f851e74b26bef70202f3070", size = 463980, upload-time = "2025-07-29T14:35:27.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/89/c011b555ccde0e5846ad3bb5a091fd0fcac997156406a9ad107f81cf91c9/pyroute2-0.9.4-py3-none-any.whl", hash = "sha256:4e12437d18f6f42912cbd3f870edf06896183a78fd0c8126ba7a72a81f28d6cf", size = 467555, upload-time = "2025-07-29T14:35:23.88Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -240,6 +252,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + [[package]] name = "pytest-repeat" version = "0.9.4" @@ -252,6 +277,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/d4/8b706b81b07b43081bd68a2c0359fe895b74bf664b20aca8005d2bb3be71/pytest_repeat-0.9.4-py3-none-any.whl", hash = "sha256:c1738b4e412a6f3b3b9e0b8b29fcd7a423e50f87381ad9307ef6f5a8601139f3", size = 4180, upload-time = "2025-04-07T14:59:51.492Z" }, ] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "pytest-xdist" version = "3.8.0" @@ -316,8 +353,11 @@ test = [ { name = "httpx" }, { name = "numpy" }, { name = "pyjwt" }, + { name = "pyroute2" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-repeat" }, + { name = "pytest-timeout" }, { name = "pytest-xdist" }, ] @@ -333,8 +373,11 @@ test = [ { name = "httpx" }, { name = "numpy" }, { name = "pyjwt" }, + { name = "pyroute2" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-repeat" }, + { name = "pytest-timeout" }, { name = "pytest-xdist" }, ] @@ -371,3 +414,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] + +[[package]] +name = "win-inet-pton" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/da/0b1487b5835497dea00b00d87c2aca168bb9ca2e2096981690239e23760a/win_inet_pton-1.1.0.tar.gz", hash = "sha256:dd03d942c0d3e2b1cf8bab511844546dfa5f74cb61b241699fa379ad707dea4f", size = 2949, upload-time = "2019-02-19T17:46:23.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/31/ff772a44aa56319df8afbb0b34f1a856f66f05b9d5f1fed917849e47fdae/win_inet_pton-1.1.0-py2.py3-none-any.whl", hash = "sha256:eaf0193cbe7152ac313598a0da7313fb479f769343c0c16c5308f64887dc885b", size = 4848, upload-time = "2019-02-19T17:46:22.182Z" }, +] From ccd7a6dacfc8b4955a4cf0f86dd57b010a223a96 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 21:13:02 +0900 Subject: [PATCH 05/37] =?UTF-8?q?e2e-test-tc=20=E3=83=AF=E3=83=BC=E3=82=AF?= =?UTF-8?q?=E3=83=95=E3=83=AD=E3=83=BC=E3=82=92=E6=96=B0=E8=A6=8F=E4=BD=9C?= =?UTF-8?q?=E6=88=90=E3=81=97=E3=80=81TC=20E2E=20=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=81=AE=E5=AE=9F=E8=A1=8C=E7=92=B0=E5=A2=83=E3=82=92?= =?UTF-8?q?=E6=A7=8B=E7=AF=89=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/e2e-test-tc.yml | 172 ++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 .github/workflows/e2e-test-tc.yml diff --git a/.github/workflows/e2e-test-tc.yml b/.github/workflows/e2e-test-tc.yml new file mode 100644 index 00000000..8a2196fc --- /dev/null +++ b/.github/workflows/e2e-test-tc.yml @@ -0,0 +1,172 @@ +name: e2e-test-tc + +on: + workflow_dispatch: + workflow_call: + inputs: + from_build: + type: boolean + default: false + description: "build.yml から呼び出されたかどうか" + push: + branches: + - develop + - "feature/**" + paths: + - ".github/workflows/e2e-test-tc.yml" + - "tests/test_tc.py" + schedule: + # UTC の 01:00 は JST だと 10:00 。 + # 1-5 で 月曜日から金曜日 + - cron: "0 1 * * 1-5" + +env: + TEST_SIGNALING_URLS: ${{ secrets.TEST_SIGNALING_URLS }} + TEST_CHANNEL_ID_PREFIX: ${{ secrets.TEST_CHANNEL_ID_PREFIX }} + TEST_SECRET_KEY: ${{ secrets.TEST_SECRET_KEY }} + TEST_API_URL: ${{ secrets.TEST_API_URL }} + OPENH264_VERSION: 2.6.0 + +permissions: + contents: read + actions: read + +jobs: + e2e_test_tc: + # TC テストは Linux でのみ実行(tc は Linux カーネル機能) + # Python 3.14 かつ Ubuntu 24.04 固定 + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + + # Tailscale のセットアップ + - uses: tailscale/github-action@v4 + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} + tags: tag:ci + use-cache: "true" + hostname: gha-tc-${{ github.run_id }}-${{ github.run_number }} + version: latest + timeout: 2m + retry: 5 + + # Linux 向けの依存関係インストール + - run: | + sudo apt-get update + sudo apt-get -y install libx11-dev libdrm-dev libva-dev + + # UV のセットアップ + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: false + # Python バージョンの設定(3.14 固定) + python-version: "3.14" + + # 依存関係のインストール (test グループのみ) + - run: uv sync --no-install-project --only-group test + + # Wheel ファイルのダウンロード (build.yml から呼び出された場合: 呼び出し元の artifact を使用) + - if: inputs.from_build == true + uses: actions/download-artifact@v5 + with: + name: ubuntu-24.04_x86_64_python-3.14 + path: dist/ + + # Wheel ファイルのダウンロード (直接実行の場合: 同一ブランチの最新成功ビルド artifact または GitHub Release から取得) + - if: inputs.from_build != true + uses: ./.github/actions/download-whl + id: download-whl + with: + platform_name: ubuntu-24.04_x86_64 + python_version: "3.14" + github_token: ${{ secrets.GITHUB_TOKEN }} + + # wheel ファイル名を取得 + - name: Find wheel file + id: find-wheel + shell: bash + run: | + WHEEL_FILE=$(find dist -name "*.whl" | head -1) + if [ -z "$WHEEL_FILE" ]; then + echo "No wheel file found in dist/" + exit 1 + fi + echo "wheel_file=$WHEEL_FILE" >> $GITHUB_OUTPUT + + # wheel ファイルのインストール + - name: Install wheel package + run: | + echo "Installing wheel: ${{ steps.find-wheel.outputs.wheel_file }}" + + # wheel ファイルの内容を確認 + echo "Checking wheel contents:" + unzip -l "${{ steps.find-wheel.outputs.wheel_file }}" | grep -E "sora_sdk|METADATA" | head -20 + + # uv pip install を使う理由: + # uv add はワークスペース依存関係の管理用で、プロジェクト名 (sora-sdk) と + # 同じ名前の wheel ファイルを追加しようとすると自己参照エラーになる + # uv pip install は仮想環境に直接インストールするため、この制限を回避できる + uv pip install "${{ steps.find-wheel.outputs.wheel_file }}" + + # OpenH264 ライブラリのダウンロード + - uses: shiguredo/github-actions/.github/actions/download-openh264@main + id: openh264 + with: + openh264_version: ${{ env.OPENH264_VERSION }} + use-cache: true + + # OpenH264 パスを環境変数に設定 + - name: Set OpenH264 path + run: echo "OPENH264_PATH=${{ steps.openh264.outputs.openh264_path }}" >> $GITHUB_ENV + + # ネットワークインターフェースの確認 + - name: Check network interface + run: | + echo "=== Network interfaces ===" + ip link show + echo "" + echo "=== Default route ===" + ip route show default + echo "" + echo "=== Current tc qdisc settings ===" + tc qdisc show || true + + # TC E2E テストの実行(root 権限が必要) + - name: Run TC E2E tests + run: | + # UV_NO_SYNC=1 を指定する理由: + # uv run はデフォルトで実行前に pyproject.toml/uv.lock と環境を同期する + # この同期により、uv pip install でインストールした wheel が削除され、 + # 元のソースコードの sora-sdk がインストールされてしまう + # UV_NO_SYNC=1 で同期をスキップし、インストール済みの wheel を使用する + + # tc の設定には root 権限が必要なため sudo で実行 + # -E オプションで環境変数を継承 + # $(which uv) で uv のフルパスを指定(sudo 実行時に PATH が変わるため) + sudo -E $(which uv) run pytest tests/test_tc.py -v --durations=0 + env: + UV_NO_SYNC: 1 + TC: 1 + + # テスト後の tc 設定のクリーンアップ確認 + - name: Verify tc cleanup + if: always() + run: | + echo "=== TC qdisc settings after test ===" + tc qdisc show || true + + slack_notify_failed: + needs: [e2e_test_tc] + runs-on: ubuntu-24.04 + if: failure() && inputs.from_build != true + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: sora-python-sdk + SLACK_COLOR: danger + SLACK_TITLE: "TC E2E Test FAILED" + SLACK_ICON_EMOJI: ":japanese_ogre:" + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} From dead5ca0d7fadcdf5957711dc23b3e5ca36f81e5 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 21:31:33 +0900 Subject: [PATCH 06/37] =?UTF-8?q?tc=20egress=20=E5=B8=AF=E5=9F=9F=E5=88=B6?= =?UTF-8?q?=E9=99=90=E3=82=92=20tbf=20=E3=81=A7=E5=AE=9F=E8=A3=85=E3=81=97?= =?UTF-8?q?=20pyroute2=20=E3=81=AB=E7=B5=B1=E4=B8=80=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - subprocess を削除し pyroute2.IPRoute に統一 - 帯域制限に tbf (Token Bucket Filter) を使用 - 遅延は netem qdisc で実装 - verify_tc_settings と show_tc_stats を IPRoute で実装 - qdisc の種類 (tbf/netem) を指定できるように改善 --- tests/test_tc.py | 111 ++++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 49 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 5742caa0..9693e0e6 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -10,7 +10,6 @@ """ import os -import subprocess import time from typing import Optional @@ -99,25 +98,28 @@ def add_bandwidth_limit(self, rate_kbps: int) -> None: raise IndexError(f"インターフェース '{self.interface}' が見つかりません") idx = indices[0] - # 帯域制限を指定した netem qdisc を追加する + # tbf (Token Bucket Filter) qdisc で帯域制限を追加する # root=True を指定すると handle は自動的に 0x10000 (= "1:") に設定される - # rate は Kbit 単位で指定する + # rate: 帯域制限 (文字列で "500kbit" のように指定) + # burst: バーストサイズ (bytes) + # latency: 最大遅延時間 (文字列で "50ms" のように指定) self.ipr.tc( "add", - "netem", + "tbf", idx, root=True, rate=f"{rate_kbps}kbit", + burst=32768, # 32KB + latency="50ms", ) self._bandwidth_applied = True print(f"tc egress 帯域制限を追加: interface={self.interface}, rate={rate_kbps}kbps") - def add_bandwidth_and_delay(self, rate_kbps: int, delay_ms: int) -> None: + def add_delay(self, delay_ms: int) -> None: """ - インターフェースの egress に帯域制限と遅延を同時に追加する。 + インターフェースの egress に遅延を追加する。 Args: - rate_kbps: 帯域制限 (Kbps) delay_ms: 遅延 (ミリ秒) Raises: @@ -133,20 +135,16 @@ def add_bandwidth_and_delay(self, rate_kbps: int, delay_ms: int) -> None: raise IndexError(f"インターフェース '{self.interface}' が見つかりません") idx = indices[0] - # 帯域制限と遅延を同時に設定 + # netem qdisc で遅延を設定 self.ipr.tc( "add", "netem", idx, root=True, - rate=f"{rate_kbps}kbit", delay=delay_ms * 1000, # マイクロ秒に変換 ) self._bandwidth_applied = True - print( - f"tc egress 帯域制限と遅延を追加: interface={self.interface}, " - f"rate={rate_kbps}kbps, delay={delay_ms}ms" - ) + print(f"tc egress 遅延を追加: interface={self.interface}, delay={delay_ms}ms") def get_stats(self) -> dict: """tc qdisc の統計情報を取得する。 @@ -201,24 +199,30 @@ def cleanup(self) -> None: print(f"tc egress 帯域制限の削除時にエラー (無視): {e}") -def verify_tc_settings(interface: str) -> bool: +def verify_tc_settings(interface: str, qdisc_type: str = "tbf") -> bool: """tc の設定が存在するか確認する。 Args: interface: ネットワークインターフェース名 + qdisc_type: 確認する qdisc の種類 (tbf または netem) Returns: 設定が存在する場合は True """ try: - result = subprocess.run( - ["tc", "qdisc", "show", "dev", interface], - capture_output=True, - text=True, - check=True, - ) - # netem が含まれているかチェック - return "netem" in result.stdout + with pyroute2.IPRoute() as ipr: + # インターフェースインデックスを取得 + indices = ipr.link_lookup(ifname=interface) + if not indices: + return False + idx = indices[0] + + # qdisc の情報を取得 + for qdisc in ipr.get_qdiscs(idx): + kind = qdisc.get_attr("TCA_KIND") + if kind == qdisc_type: + return True + return False except Exception as e: print(f"tc 設定の確認に失敗: {e}") return False @@ -231,15 +235,30 @@ def show_tc_stats(interface: str) -> None: interface: ネットワークインターフェース名 """ try: - result = subprocess.run( - ["tc", "-s", "qdisc", "show", "dev", interface], - capture_output=True, - text=True, - check=True, - ) - print(f"\ntc 統計情報 ({interface}):") - for line in result.stdout.strip().split("\n"): - print(f" {line}") + with pyroute2.IPRoute() as ipr: + # インターフェースインデックスを取得 + indices = ipr.link_lookup(ifname=interface) + if not indices: + print(f"インターフェース {interface} が見つかりません") + return + idx = indices[0] + + # qdisc の情報を取得して表示 + print(f"\ntc 統計情報 ({interface}):") + for qdisc in ipr.get_qdiscs(idx): + kind = qdisc.get_attr("TCA_KIND") + handle = qdisc.get("handle", 0) + parent = qdisc.get("parent", 0) + + # 統計情報 + sent_bytes = qdisc.get("bytes", 0) + sent_packets = qdisc.get("packets", 0) + drops = qdisc.get("drops", 0) + overlimits = qdisc.get("overlimits", 0) + + print(f" qdisc {kind} handle {handle:#x} parent {parent:#x}") + print(f" Sent {sent_bytes} bytes {sent_packets} packets") + print(f" drops {drops}, overlimits {overlimits}") except Exception as e: print(f"tc 統計情報の取得に失敗: {e}") @@ -305,13 +324,8 @@ def test_tc_egress_bandwidth_limit(settings): print("\n帯域制限が有効な状態でテスト完了") # クリーンアップ確認 - result = subprocess.run( - ["tc", "qdisc", "show", "dev", interface], - capture_output=True, - text=True, - check=True, - ) - print(f"\nクリーンアップ後の tc 設定:\n{result.stdout}") + print("\nクリーンアップ後の tc 設定:") + show_tc_stats(interface) print("\n結果:") print(" ✓ テスト成功 (tc egress 帯域制限が適用された)") @@ -371,10 +385,10 @@ def test_tc_egress_multiple_bandwidth_limits(settings): print("=" * 60 + "\n") -def test_tc_egress_bandwidth_with_delay(settings): - """tc egress で帯域制限と遅延を同時に適用する。""" +def test_tc_egress_delay(settings): + """tc egress で遅延を適用する。""" print("\n" + "=" * 60) - print("テスト: tc egress 帯域制限 + 遅延の適用") + print("テスト: tc egress 遅延の適用") print("=" * 60) interface = get_default_interface() @@ -394,17 +408,16 @@ def test_tc_egress_bandwidth_with_delay(settings): udp_port = turn_ports["udp"][0] print(f"TURN UDP ポート: {udp_port}") - # 帯域制限と遅延を同時に適用 + # 遅延を適用 with TCEgressManager(interface=interface) as tc: - # netem で帯域制限 (500kbps) と遅延 (50ms) を同時に設定 - bandwidth_kbps = 500 + # netem で遅延 (50ms) を設定 delay_ms = 50 - print(f"\ntc egress netem: rate={bandwidth_kbps}kbps, delay={delay_ms}ms") + print(f"\ntc egress netem: delay={delay_ms}ms") - tc.add_bandwidth_and_delay(rate_kbps=bandwidth_kbps, delay_ms=delay_ms) + tc.add_delay(delay_ms=delay_ms) - # tc の設定が存在することを確認 - assert verify_tc_settings(interface), "tc の設定が確認できません" + # tc の設定が存在することを確認(netem を使用) + assert verify_tc_settings(interface, qdisc_type="netem"), "tc の設定が確認できません" # 統計情報を表示 show_tc_stats(interface) @@ -416,5 +429,5 @@ def test_tc_egress_bandwidth_with_delay(settings): show_tc_stats(interface) print("\n結果:") - print(" ✓ テスト成功 (tc egress 帯域制限と遅延が同時に適用された)") + print(" ✓ テスト成功 (tc egress 遅延が適用された)") print("=" * 60 + "\n") From ed52387123d0e1c05027b21249d3492c45c00418 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 21:35:40 +0900 Subject: [PATCH 07/37] =?UTF-8?q?E2E=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AE=E5=AE=9F=E8=A1=8C=E6=99=82=E3=81=AB=E7=89=B9=E5=AE=9A?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E9=99=A4=E5=A4=96=E3=81=99=E3=82=8B=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/e2e-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index c9a00033..cc3011c8 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -15,6 +15,7 @@ on: paths: - ".github/workflows/e2e-test.yml" - "tests/**" + - "!tests/**/test_tc*" schedule: # UTC の 01:00 は JST だと 10:00 。 # 1-5 で 月曜日から金曜日 From 4d1209dec7cc0e5def720b178d7e2d5943821851 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 21:37:43 +0900 Subject: [PATCH 08/37] =?UTF-8?q?TC=20E2E=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AE=E5=AE=9F=E8=A1=8C=E6=99=82=E3=81=AB=20pytest=20?= =?UTF-8?q?=E3=81=AB=20-s=20=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/e2e-test-tc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-test-tc.yml b/.github/workflows/e2e-test-tc.yml index 8a2196fc..b1ff5376 100644 --- a/.github/workflows/e2e-test-tc.yml +++ b/.github/workflows/e2e-test-tc.yml @@ -145,7 +145,7 @@ jobs: # tc の設定には root 権限が必要なため sudo で実行 # -E オプションで環境変数を継承 # $(which uv) で uv のフルパスを指定(sudo 実行時に PATH が変わるため) - sudo -E $(which uv) run pytest tests/test_tc.py -v --durations=0 + sudo -E $(which uv) run pytest tests/test_tc.py -v --durations=0 -s env: UV_NO_SYNC: 1 TC: 1 From 00270c60185886c9d4e5443d6d298118fa933a6f Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 22:14:52 +0900 Subject: [PATCH 09/37] =?UTF-8?q?pyroute2=20=E3=81=AE=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=88=E3=82=92=E4=BE=8B=E5=A4=96=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=81=A7=E5=9B=B2=E3=81=BF=E3=80=81=E5=8D=B0=E5=88=B7?= =?UTF-8?q?=E6=96=87=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 9693e0e6..d66a2ce6 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -13,9 +13,11 @@ import time from typing import Optional -import pyroute2 +try: + import pyroute2 +except ImportError: + pass import pytest - from client import SoraClient, SoraRole # TC=1 環境変数が設定されている場合のみテストを実行 @@ -317,7 +319,7 @@ def test_tc_egress_bandwidth_limit(settings): # 統計情報を取得 stats = tc.get_stats() - print(f"\ntc 統計情報 (IPRoute):") + print("\ntc 統計情報 (IPRoute):") for key, value in stats.items(): print(f" {key}: {value}") @@ -354,7 +356,7 @@ def test_tc_egress_multiple_bandwidth_limits(settings): # turn_ports プロパティから TURN ポートを取得 turn_ports = client.turn_ports - print(f"\n取得した TURN ポート:") + print("\n取得した TURN ポート:") print(f" UDP: {turn_ports['udp']}") print(f" TCP: {turn_ports['tcp']}") print(f" TLS: {turn_ports['tls']}") @@ -376,7 +378,7 @@ def test_tc_egress_multiple_bandwidth_limits(settings): # 統計情報を再度表示 stats = tc.get_stats() - print(f"tc 統計情報 (IPRoute):") + print("tc 統計情報 (IPRoute):") for key, value in stats.items(): print(f" {key}: {value}") From da304d18ed8eecff13c54d3bc0694ebc74b2d160 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 22:20:19 +0900 Subject: [PATCH 10/37] =?UTF-8?q?pytest-asyncio=20=E3=83=91=E3=83=83?= =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=82=B8=E3=82=92=20uv.lock=20=E3=81=8B?= =?UTF-8?q?=E3=82=89=E5=89=8A=E9=99=A4=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uv.lock | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/uv.lock b/uv.lock index f788c1e3..3cf8f173 100644 --- a/uv.lock +++ b/uv.lock @@ -252,19 +252,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] -[[package]] -name = "pytest-asyncio" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, -] - [[package]] name = "pytest-repeat" version = "0.9.4" @@ -355,7 +342,6 @@ test = [ { name = "pyjwt" }, { name = "pyroute2" }, { name = "pytest" }, - { name = "pytest-asyncio" }, { name = "pytest-repeat" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, @@ -375,7 +361,6 @@ test = [ { name = "pyjwt" }, { name = "pyroute2" }, { name = "pytest" }, - { name = "pytest-asyncio" }, { name = "pytest-repeat" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, From 13fcb2737ed65b1611fa1067b5a9da4efb9886f7 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 22:34:14 +0900 Subject: [PATCH 11/37] =?UTF-8?q?pyroute2=20=E3=82=92=E3=81=84=E3=81=A3?= =?UTF-8?q?=E3=81=9F=E3=82=93=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=82=A2?= =?UTF-8?q?=E3=82=A6=E3=83=88=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/e2e-test-tc.yml | 1 + .github/workflows/e2e-test.yml | 2 +- pyproject.toml | 2 +- tests/test_tc.py | 846 +++++++++++++++--------------- uv.lock | 257 +++++---- 5 files changed, 542 insertions(+), 566 deletions(-) diff --git a/.github/workflows/e2e-test-tc.yml b/.github/workflows/e2e-test-tc.yml index b1ff5376..398669ea 100644 --- a/.github/workflows/e2e-test-tc.yml +++ b/.github/workflows/e2e-test-tc.yml @@ -33,6 +33,7 @@ permissions: jobs: e2e_test_tc: + if: false # TC テストは Linux でのみ実行(tc は Linux カーネル機能) # Python 3.14 かつ Ubuntu 24.04 固定 runs-on: ubuntu-24.04 diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index cc3011c8..fe93baef 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -243,7 +243,7 @@ jobs: # この同期により、uv pip install でインストールした wheel が削除され、 # 元のソースコードの sora-sdk がインストールされてしまう # UV_NO_SYNC=1 で同期をスキップし、インストール済みの wheel を使用する - uv run pytest ${{ matrix.platform.test_target || 'tests/' }} -v --durations=0 ${{ steps.pytest-args.outputs.pytest_extra_args }} + uv run pytest ${{ matrix.platform.test_target || 'tests/' }} -v --durations=0 ${{ steps.pytest-args.outputs.pytest_extra_args }} --ignore-glob=**/test_tc*.py -s env: UV_NO_SYNC: 1 diff --git a/pyproject.toml b/pyproject.toml index 7fb27869..26081e9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ test = [ "numpy", "httpx", "pyjwt", - "pyroute2", + # "pyroute2", ] lint = ["ruff", "ty"] diff --git a/tests/test_tc.py b/tests/test_tc.py index d66a2ce6..c76c3110 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -9,427 +9,425 @@ tc ingress (受信方向) はリモートサーバー側のため制御不可。 """ -import os -import time -from typing import Optional - -try: - import pyroute2 -except ImportError: - pass -import pytest -from client import SoraClient, SoraRole - -# TC=1 環境変数が設定されている場合のみテストを実行 -pytestmark = pytest.mark.skipif( - os.getenv("TC") != "1", - reason="TC=1 環境変数が必要", -) - - -def get_default_interface() -> str: - """デフォルトのネットワークインターフェース名を取得する。 - - Returns: - デフォルトルートで使用されているインターフェース名 - """ - try: - with pyroute2.IPRoute() as ipr: - # デフォルトルートを取得(IPv4) - for route in ipr.get_routes(family=2): # AF_INET = 2 - # dst が存在しない場合がデフォルトルート - if not route.get_attr("RTA_DST"): - oif = route.get_attr("RTA_OIF") - if oif: - # インターフェース情報を取得 - links = ipr.get_links(oif) - if links: - ifname = links[0].get_attr("IFLA_IFNAME") - return ifname - except Exception as e: - print(f"デフォルトインターフェースの取得に失敗: {e}") - - # フォールバックとして eth0 を返す - return "eth0" - - -class TCEgressManager: - """tc netem qdisc を使用して egress (送信方向) のネットワーク帯域制限を管理する。""" - - def __init__(self, interface: str = "eth0") -> None: - """ - TC egress 帯域制限マネージャーを初期化する。 - - Args: - interface: tc ルールを適用するネットワークインターフェース - """ - self.interface: str = interface - self._bandwidth_applied: bool = False - self.ipr: Optional["pyroute2.IPRoute"] = None - - def __enter__(self): - """コンテキストマネージャーのエントリ。""" - self.ipr = pyroute2.IPRoute() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """コンテキストマネージャーの終了 - tc ルールをクリーンアップする。""" - try: - self.cleanup() - finally: - if self.ipr: - self.ipr.close() - - def add_bandwidth_limit(self, rate_kbps: int) -> None: - """ - インターフェースの egress に帯域制限を追加する。 - - Args: - rate_kbps: 帯域制限 (Kbps) - - Raises: - IndexError: インターフェースが見つからない場合 - Exception: tc 操作が失敗した場合 - """ - if not self.ipr: - raise RuntimeError("IPRoute が初期化されていません") - - # インターフェースインデックスを取得する - indices = self.ipr.link_lookup(ifname=self.interface) - if not indices: - raise IndexError(f"インターフェース '{self.interface}' が見つかりません") - idx = indices[0] - - # tbf (Token Bucket Filter) qdisc で帯域制限を追加する - # root=True を指定すると handle は自動的に 0x10000 (= "1:") に設定される - # rate: 帯域制限 (文字列で "500kbit" のように指定) - # burst: バーストサイズ (bytes) - # latency: 最大遅延時間 (文字列で "50ms" のように指定) - self.ipr.tc( - "add", - "tbf", - idx, - root=True, - rate=f"{rate_kbps}kbit", - burst=32768, # 32KB - latency="50ms", - ) - self._bandwidth_applied = True - print(f"tc egress 帯域制限を追加: interface={self.interface}, rate={rate_kbps}kbps") - - def add_delay(self, delay_ms: int) -> None: - """ - インターフェースの egress に遅延を追加する。 - - Args: - delay_ms: 遅延 (ミリ秒) - - Raises: - IndexError: インターフェースが見つからない場合 - Exception: tc 操作が失敗した場合 - """ - if not self.ipr: - raise RuntimeError("IPRoute が初期化されていません") - - # インターフェースインデックスを取得する - indices = self.ipr.link_lookup(ifname=self.interface) - if not indices: - raise IndexError(f"インターフェース '{self.interface}' が見つかりません") - idx = indices[0] - - # netem qdisc で遅延を設定 - self.ipr.tc( - "add", - "netem", - idx, - root=True, - delay=delay_ms * 1000, # マイクロ秒に変換 - ) - self._bandwidth_applied = True - print(f"tc egress 遅延を追加: interface={self.interface}, delay={delay_ms}ms") - - def get_stats(self) -> dict: - """tc qdisc の統計情報を取得する。 - - Returns: - 統計情報を含む辞書 (sent_bytes, sent_packets, drops など) - """ - if not self.ipr: - raise RuntimeError("IPRoute が初期化されていません") - - # インターフェースインデックスを取得する - indices = self.ipr.link_lookup(ifname=self.interface) - if not indices: - raise IndexError(f"インターフェース '{self.interface}' が見つかりません") - idx = indices[0] - - # tc qdisc の情報を取得する - for qdisc in self.ipr.get_qdiscs(idx): - # netem qdisc の統計情報を抽出する - if qdisc.get_attr("TCA_OPTIONS"): - stats = { - "sent_bytes": qdisc.get("bytes", 0), - "sent_packets": qdisc.get("packets", 0), - "drops": qdisc.get("drops", 0), - "overlimits": qdisc.get("overlimits", 0), - "requeues": qdisc.get("requeues", 0), - } - return stats - - return {} - - def cleanup(self) -> None: - """インターフェースから tc 帯域制限設定を削除する。""" - if not self._bandwidth_applied: - return - - if not self.ipr: - return - - try: - indices = self.ipr.link_lookup(ifname=self.interface) - if not indices: - return - idx = indices[0] - # qdisc を削除する - # 削除時には kind を指定せず、index と root のみを指定する - self.ipr.tc("del", index=idx, root=True) - self._bandwidth_applied = False - print(f"tc egress 帯域制限を削除: interface={self.interface}") - except Exception as e: - # クリーンアップ時のエラーは無視する (qdisc が存在しない可能性がある) - print(f"tc egress 帯域制限の削除時にエラー (無視): {e}") - - -def verify_tc_settings(interface: str, qdisc_type: str = "tbf") -> bool: - """tc の設定が存在するか確認する。 - - Args: - interface: ネットワークインターフェース名 - qdisc_type: 確認する qdisc の種類 (tbf または netem) - - Returns: - 設定が存在する場合は True - """ - try: - with pyroute2.IPRoute() as ipr: - # インターフェースインデックスを取得 - indices = ipr.link_lookup(ifname=interface) - if not indices: - return False - idx = indices[0] - - # qdisc の情報を取得 - for qdisc in ipr.get_qdiscs(idx): - kind = qdisc.get_attr("TCA_KIND") - if kind == qdisc_type: - return True - return False - except Exception as e: - print(f"tc 設定の確認に失敗: {e}") - return False - - -def show_tc_stats(interface: str) -> None: - """tc の統計情報を表示する。 - - Args: - interface: ネットワークインターフェース名 - """ - try: - with pyroute2.IPRoute() as ipr: - # インターフェースインデックスを取得 - indices = ipr.link_lookup(ifname=interface) - if not indices: - print(f"インターフェース {interface} が見つかりません") - return - idx = indices[0] - - # qdisc の情報を取得して表示 - print(f"\ntc 統計情報 ({interface}):") - for qdisc in ipr.get_qdiscs(idx): - kind = qdisc.get_attr("TCA_KIND") - handle = qdisc.get("handle", 0) - parent = qdisc.get("parent", 0) - - # 統計情報 - sent_bytes = qdisc.get("bytes", 0) - sent_packets = qdisc.get("packets", 0) - drops = qdisc.get("drops", 0) - overlimits = qdisc.get("overlimits", 0) - - print(f" qdisc {kind} handle {handle:#x} parent {parent:#x}") - print(f" Sent {sent_bytes} bytes {sent_packets} packets") - print(f" drops {drops}, overlimits {overlimits}") - except Exception as e: - print(f"tc 統計情報の取得に失敗: {e}") - - -def test_tc_egress_bandwidth_limit(settings): - """TURN ポート取得後に tc egress で帯域制限をかける。""" - print("\n" + "=" * 60) - print("テスト: tc egress 帯域制限 (500kbps) の適用") - print("=" * 60) - - interface = get_default_interface() - print(f"使用するネットワークインターフェース: {interface}") - - with SoraClient( - settings, - SoraRole.SENDONLY, - audio=True, - video=True, - video_bit_rate=1000, - ) as sendonly: - time.sleep(3) - - # offer_message が受信されていることを確認 - assert sendonly.offer_message is not None - - # turn_ports プロパティから TURN ポートを取得 - turn_ports = sendonly.turn_ports - - # UDP ポートが取得できていることを確認 - assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" - - # 最初の UDP ポートを使用 - udp_port = turn_ports["udp"][0] - print(f"TURN UDP ポート: {udp_port}") - - # tc egress で帯域制限を設定 - with TCEgressManager(interface=interface) as tc: - # 帯域制限を設定 (500kbps) - bandwidth_kbps = 500 - print(f"\nステップ 1: tc egress 帯域制限 {bandwidth_kbps}kbps を適用") - tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) - - # tc の設定が存在することを確認 - print("\nステップ 2: tc 設定を確認") - assert verify_tc_settings(interface), "tc の設定が確認できません" - - # tc の統計情報を表示 (適用直後) - show_tc_stats(interface) - - # 接続を維持して帯域制限が有効な状態でテスト - print("\nステップ 3: 帯域制限が有効な状態で接続を維持") - time.sleep(5) - - # tc の統計情報を表示 (接続後) - show_tc_stats(interface) - - # 統計情報を取得 - stats = tc.get_stats() - print("\ntc 統計情報 (IPRoute):") - for key, value in stats.items(): - print(f" {key}: {value}") - - print("\n帯域制限が有効な状態でテスト完了") - - # クリーンアップ確認 - print("\nクリーンアップ後の tc 設定:") - show_tc_stats(interface) - - print("\n結果:") - print(" ✓ テスト成功 (tc egress 帯域制限が適用された)") - print("=" * 60 + "\n") - - -def test_tc_egress_multiple_bandwidth_limits(settings): - """複数の帯域制限パターンをテストする。""" - print("\n" + "=" * 60) - print("テスト: 複数の tc egress 帯域制限パターン") - print("=" * 60) - - interface = get_default_interface() - print(f"使用するネットワークインターフェース: {interface}") - - bandwidth_patterns = [2000, 1000, 500] # Kbps - - with SoraClient( - settings, - SoraRole.SENDRECV, - audio=True, - video=True, - ) as client: - time.sleep(3) - - # turn_ports プロパティから TURN ポートを取得 - turn_ports = client.turn_ports - - print("\n取得した TURN ポート:") - print(f" UDP: {turn_ports['udp']}") - print(f" TCP: {turn_ports['tcp']}") - print(f" TLS: {turn_ports['tls']}") - - for i, bandwidth_kbps in enumerate(bandwidth_patterns, 1): - print(f"\n--- パターン {i}: {bandwidth_kbps}kbps ---") - - with TCEgressManager(interface=interface) as tc: - tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) - - # tc の設定が存在することを確認 - assert verify_tc_settings(interface), "tc の設定が確認できません" - - # 統計情報を表示 - show_tc_stats(interface) - - # 接続を維持 - time.sleep(3) - - # 統計情報を再度表示 - stats = tc.get_stats() - print("tc 統計情報 (IPRoute):") - for key, value in stats.items(): - print(f" {key}: {value}") - - print("\n結果:") - print(" ✓ テスト成功 (複数の tc egress 帯域制限パターンが適用された)") - print("=" * 60 + "\n") - - -def test_tc_egress_delay(settings): - """tc egress で遅延を適用する。""" - print("\n" + "=" * 60) - print("テスト: tc egress 遅延の適用") - print("=" * 60) - - interface = get_default_interface() - print(f"使用するネットワークインターフェース: {interface}") - - with SoraClient( - settings, - SoraRole.SENDONLY, - audio=True, - video=False, - ) as sendonly: - time.sleep(3) - - # turn_ports プロパティから TURN ポートを取得 - turn_ports = sendonly.turn_ports - assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" - udp_port = turn_ports["udp"][0] - print(f"TURN UDP ポート: {udp_port}") - - # 遅延を適用 - with TCEgressManager(interface=interface) as tc: - # netem で遅延 (50ms) を設定 - delay_ms = 50 - print(f"\ntc egress netem: delay={delay_ms}ms") - - tc.add_delay(delay_ms=delay_ms) - - # tc の設定が存在することを確認(netem を使用) - assert verify_tc_settings(interface, qdisc_type="netem"), "tc の設定が確認できません" - - # 統計情報を表示 - show_tc_stats(interface) - - # 接続を維持 - time.sleep(5) - - # 統計情報を再度表示 - show_tc_stats(interface) - - print("\n結果:") - print(" ✓ テスト成功 (tc egress 遅延が適用された)") - print("=" * 60 + "\n") +# import os +# import time +# from typing import Optional +# +# import pyroute2 +# import pytest +# from client import SoraClient, SoraRole +# +# # TC=1 環境変数が設定されている場合のみテストを実行 +# pytestmark = pytest.mark.skipif( +# os.getenv("TC") != "1", +# reason="TC=1 環境変数が必要", +# ) +# +# +# def get_default_interface() -> str: +# """デフォルトのネットワークインターフェース名を取得する。 +# +# Returns: +# デフォルトルートで使用されているインターフェース名 +# """ +# try: +# with pyroute2.IPRoute() as ipr: +# # デフォルトルートを取得(IPv4) +# for route in ipr.get_routes(family=2): # AF_INET = 2 +# # dst が存在しない場合がデフォルトルート +# if not route.get_attr("RTA_DST"): +# oif = route.get_attr("RTA_OIF") +# if oif: +# # インターフェース情報を取得 +# links = ipr.get_links(oif) +# if links: +# ifname = links[0].get_attr("IFLA_IFNAME") +# return ifname +# except Exception as e: +# print(f"デフォルトインターフェースの取得に失敗: {e}") +# +# # フォールバックとして eth0 を返す +# return "eth0" +# +# +# class TCEgressManager: +# """tc netem qdisc を使用して egress (送信方向) のネットワーク帯域制限を管理する。""" +# +# def __init__(self, interface: str = "eth0") -> None: +# """ +# TC egress 帯域制限マネージャーを初期化する。 +# +# Args: +# interface: tc ルールを適用するネットワークインターフェース +# """ +# self.interface: str = interface +# self._bandwidth_applied: bool = False +# self.ipr: Optional["pyroute2.IPRoute"] = None +# +# def __enter__(self): +# """コンテキストマネージャーのエントリ。""" +# self.ipr = pyroute2.IPRoute() +# return self +# +# def __exit__(self, exc_type, exc_val, exc_tb): +# """コンテキストマネージャーの終了 - tc ルールをクリーンアップする。""" +# try: +# self.cleanup() +# finally: +# if self.ipr: +# self.ipr.close() +# +# def add_bandwidth_limit(self, rate_kbps: int) -> None: +# """ +# インターフェースの egress に帯域制限を追加する。 +# +# Args: +# rate_kbps: 帯域制限 (Kbps) +# +# Raises: +# IndexError: インターフェースが見つからない場合 +# Exception: tc 操作が失敗した場合 +# """ +# if not self.ipr: +# raise RuntimeError("IPRoute が初期化されていません") +# +# # インターフェースインデックスを取得する +# indices = self.ipr.link_lookup(ifname=self.interface) +# if not indices: +# raise IndexError(f"インターフェース '{self.interface}' が見つかりません") +# idx = indices[0] +# +# # tbf (Token Bucket Filter) qdisc で帯域制限を追加する +# # root=True を指定すると handle は自動的に 0x10000 (= "1:") に設定される +# # rate: 帯域制限 (文字列で "500kbit" のように指定) +# # burst: バーストサイズ (bytes) +# # latency: 最大遅延時間 (文字列で "50ms" のように指定) +# self.ipr.tc( +# "add", +# "tbf", +# idx, +# root=True, +# rate=f"{rate_kbps}kbit", +# burst=32768, # 32KB +# latency="50ms", +# ) +# self._bandwidth_applied = True +# print(f"tc egress 帯域制限を追加: interface={self.interface}, rate={rate_kbps}kbps") +# +# def add_delay(self, delay_ms: int) -> None: +# """ +# インターフェースの egress に遅延を追加する。 +# +# Args: +# delay_ms: 遅延 (ミリ秒) +# +# Raises: +# IndexError: インターフェースが見つからない場合 +# Exception: tc 操作が失敗した場合 +# """ +# if not self.ipr: +# raise RuntimeError("IPRoute が初期化されていません") +# +# # インターフェースインデックスを取得する +# indices = self.ipr.link_lookup(ifname=self.interface) +# if not indices: +# raise IndexError(f"インターフェース '{self.interface}' が見つかりません") +# idx = indices[0] +# +# # netem qdisc で遅延を設定 +# self.ipr.tc( +# "add", +# "netem", +# idx, +# root=True, +# delay=delay_ms * 1000, # マイクロ秒に変換 +# ) +# self._bandwidth_applied = True +# print(f"tc egress 遅延を追加: interface={self.interface}, delay={delay_ms}ms") +# +# def get_stats(self) -> dict: +# """tc qdisc の統計情報を取得する。 +# +# Returns: +# 統計情報を含む辞書 (sent_bytes, sent_packets, drops など) +# """ +# if not self.ipr: +# raise RuntimeError("IPRoute が初期化されていません") +# +# # インターフェースインデックスを取得する +# indices = self.ipr.link_lookup(ifname=self.interface) +# if not indices: +# raise IndexError(f"インターフェース '{self.interface}' が見つかりません") +# idx = indices[0] +# +# # tc qdisc の情報を取得する +# for qdisc in self.ipr.get_qdiscs(idx): +# # netem qdisc の統計情報を抽出する +# if qdisc.get_attr("TCA_OPTIONS"): +# stats = { +# "sent_bytes": qdisc.get("bytes", 0), +# "sent_packets": qdisc.get("packets", 0), +# "drops": qdisc.get("drops", 0), +# "overlimits": qdisc.get("overlimits", 0), +# "requeues": qdisc.get("requeues", 0), +# } +# return stats +# +# return {} +# +# def cleanup(self) -> None: +# """インターフェースから tc 帯域制限設定を削除する。""" +# if not self._bandwidth_applied: +# return +# +# if not self.ipr: +# return +# +# try: +# indices = self.ipr.link_lookup(ifname=self.interface) +# if not indices: +# return +# idx = indices[0] +# # qdisc を削除する +# # 削除時には kind を指定せず、index と root のみを指定する +# self.ipr.tc("del", index=idx, root=True) +# self._bandwidth_applied = False +# print(f"tc egress 帯域制限を削除: interface={self.interface}") +# except Exception as e: +# # クリーンアップ時のエラーは無視する (qdisc が存在しない可能性がある) +# print(f"tc egress 帯域制限の削除時にエラー (無視): {e}") +# +# +# def verify_tc_settings(interface: str, qdisc_type: str = "tbf") -> bool: +# """tc の設定が存在するか確認する。 +# +# Args: +# interface: ネットワークインターフェース名 +# qdisc_type: 確認する qdisc の種類 (tbf または netem) +# +# Returns: +# 設定が存在する場合は True +# """ +# try: +# with pyroute2.IPRoute() as ipr: +# # インターフェースインデックスを取得 +# indices = ipr.link_lookup(ifname=interface) +# if not indices: +# return False +# idx = indices[0] +# +# # qdisc の情報を取得 +# for qdisc in ipr.get_qdiscs(idx): +# kind = qdisc.get_attr("TCA_KIND") +# if kind == qdisc_type: +# return True +# return False +# except Exception as e: +# print(f"tc 設定の確認に失敗: {e}") +# return False +# +# +# def show_tc_stats(interface: str) -> None: +# """tc の統計情報を表示する。 +# +# Args: +# interface: ネットワークインターフェース名 +# """ +# try: +# with pyroute2.IPRoute() as ipr: +# # インターフェースインデックスを取得 +# indices = ipr.link_lookup(ifname=interface) +# if not indices: +# print(f"インターフェース {interface} が見つかりません") +# return +# idx = indices[0] +# +# # qdisc の情報を取得して表示 +# print(f"\ntc 統計情報 ({interface}):") +# for qdisc in ipr.get_qdiscs(idx): +# kind = qdisc.get_attr("TCA_KIND") +# handle = qdisc.get("handle", 0) +# parent = qdisc.get("parent", 0) +# +# # 統計情報 +# sent_bytes = qdisc.get("bytes", 0) +# sent_packets = qdisc.get("packets", 0) +# drops = qdisc.get("drops", 0) +# overlimits = qdisc.get("overlimits", 0) +# +# print(f" qdisc {kind} handle {handle:#x} parent {parent:#x}") +# print(f" Sent {sent_bytes} bytes {sent_packets} packets") +# print(f" drops {drops}, overlimits {overlimits}") +# except Exception as e: +# print(f"tc 統計情報の取得に失敗: {e}") +# +# +# def test_tc_egress_bandwidth_limit(settings): +# """TURN ポート取得後に tc egress で帯域制限をかける。""" +# print("\n" + "=" * 60) +# print("テスト: tc egress 帯域制限 (500kbps) の適用") +# print("=" * 60) +# +# interface = get_default_interface() +# print(f"使用するネットワークインターフェース: {interface}") +# +# with SoraClient( +# settings, +# SoraRole.SENDONLY, +# audio=True, +# video=True, +# video_bit_rate=1000, +# ) as sendonly: +# time.sleep(3) +# +# # offer_message が受信されていることを確認 +# assert sendonly.offer_message is not None +# +# # turn_ports プロパティから TURN ポートを取得 +# turn_ports = sendonly.turn_ports +# +# # UDP ポートが取得できていることを確認 +# assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" +# +# # 最初の UDP ポートを使用 +# udp_port = turn_ports["udp"][0] +# print(f"TURN UDP ポート: {udp_port}") +# +# # tc egress で帯域制限を設定 +# with TCEgressManager(interface=interface) as tc: +# # 帯域制限を設定 (500kbps) +# bandwidth_kbps = 500 +# print(f"\nステップ 1: tc egress 帯域制限 {bandwidth_kbps}kbps を適用") +# tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) +# +# # tc の設定が存在することを確認 +# print("\nステップ 2: tc 設定を確認") +# assert verify_tc_settings(interface), "tc の設定が確認できません" +# +# # tc の統計情報を表示 (適用直後) +# show_tc_stats(interface) +# +# # 接続を維持して帯域制限が有効な状態でテスト +# print("\nステップ 3: 帯域制限が有効な状態で接続を維持") +# time.sleep(5) +# +# # tc の統計情報を表示 (接続後) +# show_tc_stats(interface) +# +# # 統計情報を取得 +# stats = tc.get_stats() +# print("\ntc 統計情報 (IPRoute):") +# for key, value in stats.items(): +# print(f" {key}: {value}") +# +# print("\n帯域制限が有効な状態でテスト完了") +# +# # クリーンアップ確認 +# print("\nクリーンアップ後の tc 設定:") +# show_tc_stats(interface) +# +# print("\n結果:") +# print(" ✓ テスト成功 (tc egress 帯域制限が適用された)") +# print("=" * 60 + "\n") +# +# +# def test_tc_egress_multiple_bandwidth_limits(settings): +# """複数の帯域制限パターンをテストする。""" +# print("\n" + "=" * 60) +# print("テスト: 複数の tc egress 帯域制限パターン") +# print("=" * 60) +# +# interface = get_default_interface() +# print(f"使用するネットワークインターフェース: {interface}") +# +# bandwidth_patterns = [2000, 1000, 500] # Kbps +# +# with SoraClient( +# settings, +# SoraRole.SENDRECV, +# audio=True, +# video=True, +# ) as client: +# time.sleep(3) +# +# # turn_ports プロパティから TURN ポートを取得 +# turn_ports = client.turn_ports +# +# print("\n取得した TURN ポート:") +# print(f" UDP: {turn_ports['udp']}") +# print(f" TCP: {turn_ports['tcp']}") +# print(f" TLS: {turn_ports['tls']}") +# +# for i, bandwidth_kbps in enumerate(bandwidth_patterns, 1): +# print(f"\n--- パターン {i}: {bandwidth_kbps}kbps ---") +# +# with TCEgressManager(interface=interface) as tc: +# tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) +# +# # tc の設定が存在することを確認 +# assert verify_tc_settings(interface), "tc の設定が確認できません" +# +# # 統計情報を表示 +# show_tc_stats(interface) +# +# # 接続を維持 +# time.sleep(3) +# +# # 統計情報を再度表示 +# stats = tc.get_stats() +# print("tc 統計情報 (IPRoute):") +# for key, value in stats.items(): +# print(f" {key}: {value}") +# +# print("\n結果:") +# print(" ✓ テスト成功 (複数の tc egress 帯域制限パターンが適用された)") +# print("=" * 60 + "\n") +# +# +# def test_tc_egress_delay(settings): +# """tc egress で遅延を適用する。""" +# print("\n" + "=" * 60) +# print("テスト: tc egress 遅延の適用") +# print("=" * 60) +# +# interface = get_default_interface() +# print(f"使用するネットワークインターフェース: {interface}") +# +# with SoraClient( +# settings, +# SoraRole.SENDONLY, +# audio=True, +# video=False, +# ) as sendonly: +# time.sleep(3) +# +# # turn_ports プロパティから TURN ポートを取得 +# turn_ports = sendonly.turn_ports +# assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" +# udp_port = turn_ports["udp"][0] +# print(f"TURN UDP ポート: {udp_port}") +# +# # 遅延を適用 +# with TCEgressManager(interface=interface) as tc: +# # netem で遅延 (50ms) を設定 +# delay_ms = 50 +# print(f"\ntc egress netem: delay={delay_ms}ms") +# +# tc.add_delay(delay_ms=delay_ms) +# +# # tc の設定が存在することを確認(netem を使用) +# assert verify_tc_settings(interface, qdisc_type="netem"), "tc の設定が確認できません" +# +# # 統計情報を表示 +# show_tc_stats(interface) +# +# # 接続を維持 +# time.sleep(5) +# +# # 統計情報を再度表示 +# show_tc_stats(interface) +# +# print("\n結果:") +# print(" ✓ テスト成功 (tc egress 遅延が適用された)") +# print("=" * 60 + "\n") +# diff --git a/uv.lock b/uv.lock index 3cf8f173..ec16558a 100644 --- a/uv.lock +++ b/uv.lock @@ -91,11 +91,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] @@ -109,83 +109,83 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.3" +version = "2.3.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/45/e80d203ef6b267aa29b22714fb558930b27960a0c5ce3c19c999232bb3eb/numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d", size = 21259253, upload-time = "2025-09-09T15:56:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/52/18/cf2c648fccf339e59302e00e5f2bc87725a3ce1992f30f3f78c9044d7c43/numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569", size = 14450980, upload-time = "2025-09-09T15:56:05.926Z" }, - { url = "https://files.pythonhosted.org/packages/93/fb/9af1082bec870188c42a1c239839915b74a5099c392389ff04215dcee812/numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f", size = 5379709, upload-time = "2025-09-09T15:56:07.95Z" }, - { url = "https://files.pythonhosted.org/packages/75/0f/bfd7abca52bcbf9a4a65abc83fe18ef01ccdeb37bfb28bbd6ad613447c79/numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125", size = 6913923, upload-time = "2025-09-09T15:56:09.443Z" }, - { url = "https://files.pythonhosted.org/packages/79/55/d69adad255e87ab7afda1caf93ca997859092afeb697703e2f010f7c2e55/numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48", size = 14589591, upload-time = "2025-09-09T15:56:11.234Z" }, - { url = "https://files.pythonhosted.org/packages/10/a2/010b0e27ddeacab7839957d7a8f00e91206e0c2c47abbb5f35a2630e5387/numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6", size = 16938714, upload-time = "2025-09-09T15:56:14.637Z" }, - { url = "https://files.pythonhosted.org/packages/1c/6b/12ce8ede632c7126eb2762b9e15e18e204b81725b81f35176eac14dc5b82/numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa", size = 16370592, upload-time = "2025-09-09T15:56:17.285Z" }, - { url = "https://files.pythonhosted.org/packages/b4/35/aba8568b2593067bb6a8fe4c52babb23b4c3b9c80e1b49dff03a09925e4a/numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30", size = 18884474, upload-time = "2025-09-09T15:56:20.943Z" }, - { url = "https://files.pythonhosted.org/packages/45/fa/7f43ba10c77575e8be7b0138d107e4f44ca4a1ef322cd16980ea3e8b8222/numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57", size = 6599794, upload-time = "2025-09-09T15:56:23.258Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a2/a4f78cb2241fe5664a22a10332f2be886dcdea8784c9f6a01c272da9b426/numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa", size = 13088104, upload-time = "2025-09-09T15:56:25.476Z" }, - { url = "https://files.pythonhosted.org/packages/79/64/e424e975adbd38282ebcd4891661965b78783de893b381cbc4832fb9beb2/numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7", size = 10460772, upload-time = "2025-09-09T15:56:27.679Z" }, - { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload-time = "2025-09-09T15:56:29.966Z" }, - { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload-time = "2025-09-09T15:56:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload-time = "2025-09-09T15:56:34.175Z" }, - { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload-time = "2025-09-09T15:56:36.149Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload-time = "2025-09-09T15:56:40.548Z" }, - { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload-time = "2025-09-09T15:56:43.343Z" }, - { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload-time = "2025-09-09T15:56:46.141Z" }, - { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload-time = "2025-09-09T15:56:49.844Z" }, - { url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", size = 6320800, upload-time = "2025-09-09T15:56:52.499Z" }, - { url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", size = 12786615, upload-time = "2025-09-09T15:56:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", size = 10195936, upload-time = "2025-09-09T15:56:56.541Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" }, - { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" }, - { url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" }, - { url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" }, - { url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" }, - { url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" }, - { url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" }, - { url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" }, - { url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" }, - { url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" }, - { url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" }, - { url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" }, - { url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" }, - { url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" }, - { url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" }, - { url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" }, - { url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" }, - { url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952, upload-time = "2025-09-09T15:58:10.096Z" }, - { url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322, upload-time = "2025-09-09T15:58:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630, upload-time = "2025-09-09T15:58:14.64Z" }, - { url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" }, - { url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" }, - { url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" }, - { url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" }, - { url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371, upload-time = "2025-09-09T15:58:36.04Z" }, - { url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576, upload-time = "2025-09-09T15:58:37.927Z" }, - { url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f2/7e0a37cfced2644c9563c529f29fa28acbd0960dde32ece683aafa6f4949/numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e", size = 21131019, upload-time = "2025-09-09T15:58:42.838Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/3291f505297ed63831135a6cc0f474da0c868a1f31b0dd9a9f03a7a0d2ed/numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150", size = 14376288, upload-time = "2025-09-09T15:58:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4b/ae02e985bdeee73d7b5abdefeb98aef1207e96d4c0621ee0cf228ddfac3c/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3", size = 5305425, upload-time = "2025-09-09T15:58:48.6Z" }, - { url = "https://files.pythonhosted.org/packages/8b/eb/9df215d6d7250db32007941500dc51c48190be25f2401d5b2b564e467247/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0", size = 6819053, upload-time = "2025-09-09T15:58:50.401Z" }, - { url = "https://files.pythonhosted.org/packages/57/62/208293d7d6b2a8998a4a1f23ac758648c3c32182d4ce4346062018362e29/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e", size = 14420354, upload-time = "2025-09-09T15:58:52.704Z" }, - { url = "https://files.pythonhosted.org/packages/ed/0c/8e86e0ff7072e14a71b4c6af63175e40d1e7e933ce9b9e9f765a95b4e0c3/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", size = 16760413, upload-time = "2025-09-09T15:58:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/af/11/0cc63f9f321ccf63886ac203336777140011fb669e739da36d8db3c53b98/numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc", size = 12971844, upload-time = "2025-09-09T15:58:57.359Z" }, + { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, + { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, + { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, + { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, + { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, + { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, + { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, + { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, + { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, + { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, + { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, + { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, + { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, + { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, + { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, + { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, ] [[package]] @@ -224,18 +224,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] -[[package]] -name = "pyroute2" -version = "0.9.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "win-inet-pton", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b0/5e/fc64e211cce0078555c6db98aaf14348aed527565f3c4876913a290a5b2c/pyroute2-0.9.4.tar.gz", hash = "sha256:3cbccbe1af0c2b2aeae81b327e0e91aa94c81ab19f851e74b26bef70202f3070", size = 463980, upload-time = "2025-07-29T14:35:27.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/89/c011b555ccde0e5846ad3bb5a091fd0fcac997156406a9ad107f81cf91c9/pyroute2-0.9.4-py3-none-any.whl", hash = "sha256:4e12437d18f6f42912cbd3f870edf06896183a78fd0c8126ba7a72a81f28d6cf", size = 467555, upload-time = "2025-07-29T14:35:23.88Z" }, -] - [[package]] name = "pytest" version = "8.4.2" @@ -291,28 +279,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.0" +version = "0.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/b9/9bd84453ed6dd04688de9b3f3a4146a1698e8faae2ceeccce4e14c67ae17/ruff-0.14.0.tar.gz", hash = "sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57", size = 5452071, upload-time = "2025-10-07T18:21:55.763Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429, upload-time = "2025-10-16T18:05:41.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4e/79d463a5f80654e93fa653ebfb98e0becc3f0e7cf6219c9ddedf1e197072/ruff-0.14.0-py3-none-linux_armv6l.whl", hash = "sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3", size = 12494532, upload-time = "2025-10-07T18:21:00.373Z" }, - { url = "https://files.pythonhosted.org/packages/ee/40/e2392f445ed8e02aa6105d49db4bfff01957379064c30f4811c3bf38aece/ruff-0.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8", size = 13160768, upload-time = "2025-10-07T18:21:04.73Z" }, - { url = "https://files.pythonhosted.org/packages/75/da/2a656ea7c6b9bd14c7209918268dd40e1e6cea65f4bb9880eaaa43b055cd/ruff-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8", size = 12363376, upload-time = "2025-10-07T18:21:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/1ffef5a1875add82416ff388fcb7ea8b22a53be67a638487937aea81af27/ruff-0.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7", size = 12608055, upload-time = "2025-10-07T18:21:10.72Z" }, - { url = "https://files.pythonhosted.org/packages/4a/32/986725199d7cee510d9f1dfdf95bf1efc5fa9dd714d0d85c1fb1f6be3bc3/ruff-0.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7", size = 12318544, upload-time = "2025-10-07T18:21:13.741Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ed/4969cefd53315164c94eaf4da7cfba1f267dc275b0abdd593d11c90829a3/ruff-0.14.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2", size = 14001280, upload-time = "2025-10-07T18:21:16.411Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ad/96c1fc9f8854c37681c9613d825925c7f24ca1acfc62a4eb3896b50bacd2/ruff-0.14.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c", size = 15027286, upload-time = "2025-10-07T18:21:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/b3/00/1426978f97df4fe331074baf69615f579dc4e7c37bb4c6f57c2aad80c87f/ruff-0.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e", size = 14451506, upload-time = "2025-10-07T18:21:22.779Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/9c1cea6e493c0cf0647674cca26b579ea9d2a213b74b5c195fbeb9678e15/ruff-0.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206", size = 13437384, upload-time = "2025-10-07T18:21:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/29/b4/4cd6a4331e999fc05d9d77729c95503f99eae3ba1160469f2b64866964e3/ruff-0.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e", size = 13447976, upload-time = "2025-10-07T18:21:28.83Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c0/ac42f546d07e4f49f62332576cb845d45c67cf5610d1851254e341d563b6/ruff-0.14.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd", size = 13682850, upload-time = "2025-10-07T18:21:31.842Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c4/4b0c9bcadd45b4c29fe1af9c5d1dc0ca87b4021665dfbe1c4688d407aa20/ruff-0.14.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d", size = 12449825, upload-time = "2025-10-07T18:21:35.074Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a8/e2e76288e6c16540fa820d148d83e55f15e994d852485f221b9524514730/ruff-0.14.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f", size = 12272599, upload-time = "2025-10-07T18:21:38.08Z" }, - { url = "https://files.pythonhosted.org/packages/18/14/e2815d8eff847391af632b22422b8207704222ff575dec8d044f9ab779b2/ruff-0.14.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02", size = 13193828, upload-time = "2025-10-07T18:21:41.216Z" }, - { url = "https://files.pythonhosted.org/packages/44/c6/61ccc2987cf0aecc588ff8f3212dea64840770e60d78f5606cd7dc34de32/ruff-0.14.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296", size = 13628617, upload-time = "2025-10-07T18:21:44.04Z" }, - { url = "https://files.pythonhosted.org/packages/73/e6/03b882225a1b0627e75339b420883dc3c90707a8917d2284abef7a58d317/ruff-0.14.0-py3-none-win32.whl", hash = "sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543", size = 12367872, upload-time = "2025-10-07T18:21:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/41/77/56cf9cf01ea0bfcc662de72540812e5ba8e9563f33ef3d37ab2174892c47/ruff-0.14.0-py3-none-win_amd64.whl", hash = "sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2", size = 13464628, upload-time = "2025-10-07T18:21:50.318Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142, upload-time = "2025-10-07T18:21:53.577Z" }, + { url = "https://files.pythonhosted.org/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415, upload-time = "2025-10-16T18:04:48.227Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267, upload-time = "2025-10-16T18:04:52.515Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872, upload-time = "2025-10-16T18:04:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558, upload-time = "2025-10-16T18:04:58.166Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898, upload-time = "2025-10-16T18:05:01.455Z" }, + { url = "https://files.pythonhosted.org/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168, upload-time = "2025-10-16T18:05:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942, upload-time = "2025-10-16T18:05:07.102Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622, upload-time = "2025-10-16T18:05:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143, upload-time = "2025-10-16T18:05:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844, upload-time = "2025-10-16T18:05:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241, upload-time = "2025-10-16T18:05:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476, upload-time = "2025-10-16T18:05:22.163Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749, upload-time = "2025-10-16T18:05:25.162Z" }, + { url = "https://files.pythonhosted.org/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758, upload-time = "2025-10-16T18:05:28.018Z" }, + { url = "https://files.pythonhosted.org/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811, upload-time = "2025-10-16T18:05:30.707Z" }, + { url = "https://files.pythonhosted.org/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467, upload-time = "2025-10-16T18:05:33.261Z" }, + { url = "https://files.pythonhosted.org/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123, upload-time = "2025-10-16T18:05:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" }, ] [[package]] @@ -340,7 +328,6 @@ test = [ { name = "httpx" }, { name = "numpy" }, { name = "pyjwt" }, - { name = "pyroute2" }, { name = "pytest" }, { name = "pytest-repeat" }, { name = "pytest-timeout" }, @@ -359,7 +346,6 @@ test = [ { name = "httpx" }, { name = "numpy" }, { name = "pyjwt" }, - { name = "pyroute2" }, { name = "pytest" }, { name = "pytest-repeat" }, { name = "pytest-timeout" }, @@ -368,27 +354,27 @@ test = [ [[package]] name = "ty" -version = "0.0.1a22" +version = "0.0.1a23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/87/eab73cdc990d1141b60237379975efc0e913bfa0d19083daab0f497444a6/ty-0.0.1a22.tar.gz", hash = "sha256:b20ec5362830a1e9e05654c15e88607fdbb45325ec130a9a364c6dd412ecbf55", size = 4312182, upload-time = "2025-10-10T13:07:15.88Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/98/e9c6cc74e7f81d49f1c06db3a455a5bff6d9e47b73408d053e81daef77fb/ty-0.0.1a23.tar.gz", hash = "sha256:d3b4a81b47f306f571fd99bc71a4fa5607eae61079a18e77fadcf8401b19a6c9", size = 4360335, upload-time = "2025-10-16T18:18:59.475Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/30/83e2dbfbc70de8a1932b19daf05ce803d7d76cdc6251de1519a49cf1c27d/ty-0.0.1a22-py3-none-linux_armv6l.whl", hash = "sha256:6efba0c777881d2d072fa7375a64ad20357e825eff2a0b6ff9ec80399a04253b", size = 8581795, upload-time = "2025-10-10T13:06:44.396Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8c/5193534fc4a3569f517408828d077b26d6280fe8c2dd0bdc63db4403dcdb/ty-0.0.1a22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2ada020eebe1b44403affdf45cd5c8d3fb8312c3e80469d795690093c0921f55", size = 8682602, upload-time = "2025-10-10T13:06:46.44Z" }, - { url = "https://files.pythonhosted.org/packages/22/4a/7ba53493bf37b61d3e0dfe6df910e6bc74c40d16c3effd84e15c0863d34e/ty-0.0.1a22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ed4f11f1a5824ea10d3e46b1990d092c3f341b1d492c357d23bed2ac347fd253", size = 8278839, upload-time = "2025-10-10T13:06:48.688Z" }, - { url = "https://files.pythonhosted.org/packages/52/0a/d9862c41b9615de56d2158bfbb5177dbf5a65e94922d3dd13855f48cb91b/ty-0.0.1a22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56f48d8f94292909d596dbeb56ff7f9f070bd316aa628b45c02ca2b2f5797f31", size = 8421483, upload-time = "2025-10-10T13:06:50.75Z" }, - { url = "https://files.pythonhosted.org/packages/a5/cb/3ebe0e45b80724d4c2f849fdf304179727fd06df7fee7cd12fe6c3efe49d/ty-0.0.1a22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:733e9ac22885b6574de26bdbae439c960a06acc825a938d3780c9d498bb65339", size = 8419225, upload-time = "2025-10-10T13:06:52.533Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b5/da65f3f8ad31d881ca9987a3f6f26069a0cc649c9354adb7453ca62116bb/ty-0.0.1a22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5135d662484e56809c77b3343614005585caadaa5c1cf643ed6a09303497652b", size = 9352336, upload-time = "2025-10-10T13:06:54.476Z" }, - { url = "https://files.pythonhosted.org/packages/a3/24/9c46f2eb16734ab0fcf3291486b1c5c528a1569f94541dc1f19f97dd2a5b/ty-0.0.1a22-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:87f297f99a98154d33a3f21991979418c65d8bf480f6a1bad1e54d46d2dc7df7", size = 9857840, upload-time = "2025-10-10T13:06:56.514Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ae/930c94bbbe5c049eae5355a197c39522844f55c7ab7fccd0ba061f618541/ty-0.0.1a22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3310217eaa4dccf20b7336fcbeb072097addc6fde0c9d3f791dea437af0aa6dc", size = 9452611, upload-time = "2025-10-10T13:06:58.154Z" }, - { url = "https://files.pythonhosted.org/packages/a2/80/d8f594438465c352cf0ebd4072f5ca3be2871153a3cd279ed2f35ecd487c/ty-0.0.1a22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12b032e81012bf5228fd65f01b50e29eb409534b6aac28ee5c48ee3b7b860ddf", size = 9214875, upload-time = "2025-10-10T13:06:59.861Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/f852fb20ac27707de495c39a02aeb056e3368833b7e12888d43b1f61594d/ty-0.0.1a22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3ffda8149cab0000a21e7a078142073e27a1a9ac03b9a0837aa2f53d1fbebcb", size = 8906715, upload-time = "2025-10-10T13:07:01.926Z" }, - { url = "https://files.pythonhosted.org/packages/40/4d/0e0b85b4179891cc3067a6e717f5161921c07873a4f545963fdf1dd3619c/ty-0.0.1a22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:afa512e7dc78f0cf0b55f87394968ba59c46993c67bc0ef295962144fea85b12", size = 8350873, upload-time = "2025-10-10T13:07:03.999Z" }, - { url = "https://files.pythonhosted.org/packages/a1/1f/e70c63e12b4a0d97d4fd6f872dd199113666ad1b236e18838fa5e5d5502d/ty-0.0.1a22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:069cdbbea6025f7ebbb5e9043c8d0daf760358df46df8304ef5ca5bb3e320aef", size = 8442568, upload-time = "2025-10-10T13:07:05.745Z" }, - { url = "https://files.pythonhosted.org/packages/de/3b/55518906cb3598f2b99ff1e86c838d77d006cab70cdd2a0a625d02ccb52c/ty-0.0.1a22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:67d31d902e6fd67a4b3523604f635e71d2ec55acfb9118f984600584bfe0ff2a", size = 8896775, upload-time = "2025-10-10T13:07:08.02Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ea/60c654c27931bf84fa9cb463a4c4c49e8869c052fa607a6e930be717b619/ty-0.0.1a22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f9e154f262162e6f76b01f318e469ac6c22ffce22b010c396ed34e81d8369821", size = 9054544, upload-time = "2025-10-10T13:07:09.675Z" }, - { url = "https://files.pythonhosted.org/packages/6c/60/9a6d5530d6829ccf656e6ae0fb13d70a4e2514f4fb8910266ebd54286620/ty-0.0.1a22-py3-none-win32.whl", hash = "sha256:37525433ca7b02a8fca4b8fa9dcde818bf3a413b539b9dbc8f7b39d124eb7c49", size = 8165703, upload-time = "2025-10-10T13:07:11.378Z" }, - { url = "https://files.pythonhosted.org/packages/14/9c/ac08c832643850d4e18cbc959abc69cd51d531fe11bdb691098b3cf2f562/ty-0.0.1a22-py3-none-win_amd64.whl", hash = "sha256:75d21cdeba8bcef247af89518d7ce98079cac4a55c4160cb76682ea40a18b92c", size = 8828319, upload-time = "2025-10-10T13:07:12.815Z" }, - { url = "https://files.pythonhosted.org/packages/22/df/38068fc44e3cfb455aeb41d0ff1850a4d3c9988010466d4a8d19860b8b9a/ty-0.0.1a22-py3-none-win_arm64.whl", hash = "sha256:1c7f040fe311e9696917417434c2a0e58402235be842c508002c6a2eff1398b0", size = 8367136, upload-time = "2025-10-10T13:07:14.518Z" }, + { url = "https://files.pythonhosted.org/packages/9c/45/d662cd4c0c5f6254c4ff0d05edad9cbbac23e01bb277602eaed276bb53ba/ty-0.0.1a23-py3-none-linux_armv6l.whl", hash = "sha256:7c76debd57623ac8712a9d2a32529a2b98915434aa3521cab92318bfe3f34dfc", size = 8735928, upload-time = "2025-10-16T18:18:23.161Z" }, + { url = "https://files.pythonhosted.org/packages/db/89/8aa7c303a55181fc121ecce143464a156b51f03481607ef0f58f67dc936c/ty-0.0.1a23-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1d9b63c72cb94bcfe8f36b4527fd18abc46bdecc8f774001bcf7a8dd83e8c81a", size = 8584084, upload-time = "2025-10-16T18:18:25.579Z" }, + { url = "https://files.pythonhosted.org/packages/02/43/7a3bec50f440028153c0ee0044fd47e409372d41012f5f6073103a90beac/ty-0.0.1a23-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1a875135cdb77b60280eb74d3c97ce3c44f872bf4176f5e71602a0a9401341ca", size = 8061268, upload-time = "2025-10-16T18:18:27.668Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c2/75ddb10084cc7da8de077ae09fe5d8d76fec977c2ab71929c21b6fea622f/ty-0.0.1a23-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ddf5f4d057a023409a926e3be5ba0388aa8c93a01ddc6c87cca03af22c78a0c", size = 8319954, upload-time = "2025-10-16T18:18:29.54Z" }, + { url = "https://files.pythonhosted.org/packages/b2/57/0762763e9a29a1bd393b804a950c03d9ceb18aaf5e5baa7122afc50c2387/ty-0.0.1a23-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad89d894ef414d5607c3611ab68298581a444fd51570e0e4facdd7c8e8856748", size = 8550745, upload-time = "2025-10-16T18:18:31.548Z" }, + { url = "https://files.pythonhosted.org/packages/89/0a/855ca77e454955acddba2149ad7fe20fd24946289b8fd1d66b025b2afef1/ty-0.0.1a23-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6306ad146748390675871b0c7731e595ceb2241724bc7d2d46e56f392949fbb9", size = 8899930, upload-time = "2025-10-16T18:18:34.003Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f0/9282da70da435d1890c5b1dff844a3139fc520d0a61747bb1e84fbf311d5/ty-0.0.1a23-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa2155c0a66faeb515b88d7dc6b9f3fb393373798e97c01f05b1436c60d2c6b1", size = 9561714, upload-time = "2025-10-16T18:18:36.238Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/ffea2138629875a2083ccc64cc80585ecf0e487500835fe7c1b6f6305bf8/ty-0.0.1a23-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7d75d1f264afbe9a294d88e1e7736c003567a74f3a433c72231c36999a61e42", size = 9231064, upload-time = "2025-10-16T18:18:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/ff/92/dac340d2d10e81788801e7580bad0168b190ba5a5c6cf6e4f798e094ee80/ty-0.0.1a23-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af8eb2341e804f8e1748b6d638a314102020dca5591cacae67fe420211d59369", size = 9428468, upload-time = "2025-10-16T18:18:40.984Z" }, + { url = "https://files.pythonhosted.org/packages/37/21/d376393ecaf26cb84aa475f46137a59ae6d50508acbf1a044d414d8f6d47/ty-0.0.1a23-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7516ee783ba3eba373fb82db8b989a14ed8620a45a9bb6e3a90571bc83b3e2a", size = 8880687, upload-time = "2025-10-16T18:18:43.34Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f4/7cf58a02e0a8d062dd20d7816396587faba9ddfe4098ee88bb6ee3c272d4/ty-0.0.1a23-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c8f9a861b51bbcf10f35d134a3c568a79a3acd3b0f2f1c004a2ccb00efdf7c1", size = 8281532, upload-time = "2025-10-16T18:18:45.806Z" }, + { url = "https://files.pythonhosted.org/packages/14/1b/ae616bbc4588b50ff1875588e734572a2b00102415e131bc20d794827865/ty-0.0.1a23-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d44a7ca68f4e79e7f06f23793397edfa28c2ac38e1330bf7100dce93015e412a", size = 8579585, upload-time = "2025-10-16T18:18:47.638Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0c/3f4fc4721eb34abd7d86b43958b741b73727c9003f9977bacc3c91b3d7ca/ty-0.0.1a23-py3-none-musllinux_1_2_i686.whl", hash = "sha256:80a6818b22b25a27d5761a3cf377784f07d7a799f24b3ebcf9b4144b35b88871", size = 8675719, upload-time = "2025-10-16T18:18:49.536Z" }, + { url = "https://files.pythonhosted.org/packages/60/36/07d2c4e0230407419c10d3aa7c5035e023d9f70f07f4da2266fa0108109c/ty-0.0.1a23-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ef52c927ed6b5ebec290332ded02ce49ffdb3576683920b7013a7b2cd6bd5685", size = 8978349, upload-time = "2025-10-16T18:18:51.299Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f9/abf666971434ea259a8d2006d2943eac0727a14aeccd24359341d377c2d1/ty-0.0.1a23-py3-none-win32.whl", hash = "sha256:0cc7500131a6a533d4000401026427cd538e33fda4e9004d7ad0db5a6f5500b1", size = 8279664, upload-time = "2025-10-16T18:18:53.132Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/cb99e90adba6296f260ceaf3d02cc20563ec623b23a92ab94d17791cb537/ty-0.0.1a23-py3-none-win_amd64.whl", hash = "sha256:c89564e90dcc2f9564564d4a02cd703ed71cd9ccbb5a6a38ee49c44d86375f24", size = 8912398, upload-time = "2025-10-16T18:18:55.585Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/9fffb57f66317082fe3de4d08bb71557105c47676a114bdc9d52f6d3a910/ty-0.0.1a23-py3-none-win_arm64.whl", hash = "sha256:71aa203d6ae4de863a7f4626a8fe5f723beaa219988d176a6667f021b78a2af3", size = 8400343, upload-time = "2025-10-16T18:18:57.387Z" }, ] [[package]] @@ -399,12 +385,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] - -[[package]] -name = "win-inet-pton" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/da/0b1487b5835497dea00b00d87c2aca168bb9ca2e2096981690239e23760a/win_inet_pton-1.1.0.tar.gz", hash = "sha256:dd03d942c0d3e2b1cf8bab511844546dfa5f74cb61b241699fa379ad707dea4f", size = 2949, upload-time = "2019-02-19T17:46:23.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/31/ff772a44aa56319df8afbb0b34f1a856f66f05b9d5f1fed917849e47fdae/win_inet_pton-1.1.0-py2.py3-none-any.whl", hash = "sha256:eaf0193cbe7152ac313598a0da7313fb479f769343c0c16c5308f64887dc885b", size = 4848, upload-time = "2019-02-19T17:46:22.182Z" }, -] From 9cdb68ebdabb55e1584e61985180620dbd73cbad Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 22:37:24 +0900 Subject: [PATCH 12/37] =?UTF-8?q?pyroute2=20=E3=81=AF=E7=89=B9=E5=88=A5?= =?UTF-8?q?=E6=89=B1=E3=81=84=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 26081e9a..d83dafbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,8 +38,8 @@ test = [ "numpy", "httpx", "pyjwt", - # "pyroute2", ] +tc = ["pyroute2"] lint = ["ruff", "ty"] [tool.uv] From 2ef207e5ca9e86ef489cee66cf84cebb5d1d49eb Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 22:41:26 +0900 Subject: [PATCH 13/37] =?UTF-8?q?pyroute2=20=E3=83=91=E3=83=83=E3=82=B1?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=82=92=20uv.lock=20=E3=81=AB=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=97=E3=80=81win-inet-pton=20=E3=83=91=E3=83=83?= =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=82=B8=E3=82=82=E8=BF=BD=E5=8A=A0=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uv.lock | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/uv.lock b/uv.lock index ec16558a..110d00f1 100644 --- a/uv.lock +++ b/uv.lock @@ -224,6 +224,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] +[[package]] +name = "pyroute2" +version = "0.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "win-inet-pton", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/5e/fc64e211cce0078555c6db98aaf14348aed527565f3c4876913a290a5b2c/pyroute2-0.9.4.tar.gz", hash = "sha256:3cbccbe1af0c2b2aeae81b327e0e91aa94c81ab19f851e74b26bef70202f3070", size = 463980, upload-time = "2025-07-29T14:35:27.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/89/c011b555ccde0e5846ad3bb5a091fd0fcac997156406a9ad107f81cf91c9/pyroute2-0.9.4-py3-none-any.whl", hash = "sha256:4e12437d18f6f42912cbd3f870edf06896183a78fd0c8126ba7a72a81f28d6cf", size = 467555, upload-time = "2025-07-29T14:35:23.88Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -324,6 +336,9 @@ lint = [ { name = "ruff" }, { name = "ty" }, ] +tc = [ + { name = "pyroute2" }, +] test = [ { name = "httpx" }, { name = "numpy" }, @@ -342,6 +357,7 @@ lint = [ { name = "ruff" }, { name = "ty" }, ] +tc = [{ name = "pyroute2" }] test = [ { name = "httpx" }, { name = "numpy" }, @@ -385,3 +401,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] + +[[package]] +name = "win-inet-pton" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/da/0b1487b5835497dea00b00d87c2aca168bb9ca2e2096981690239e23760a/win_inet_pton-1.1.0.tar.gz", hash = "sha256:dd03d942c0d3e2b1cf8bab511844546dfa5f74cb61b241699fa379ad707dea4f", size = 2949, upload-time = "2019-02-19T17:46:23.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/31/ff772a44aa56319df8afbb0b34f1a856f66f05b9d5f1fed917849e47fdae/win_inet_pton-1.1.0-py2.py3-none-any.whl", hash = "sha256:eaf0193cbe7152ac313598a0da7313fb479f769343c0c16c5308f64887dc885b", size = 4848, upload-time = "2019-02-19T17:46:22.182Z" }, +] From 7eaa1616b7aec9b64f010dfbb7df19ff4e3e7b02 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 22:45:42 +0900 Subject: [PATCH 14/37] =?UTF-8?q?e2e-test-tc.yml=20=E3=81=AE=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=97=E3=80=81=E4=BE=9D?= =?UTF-8?q?=E5=AD=98=E9=96=A2=E4=BF=82=E3=81=AE=E3=82=A4=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=83=BC=E3=83=AB=E3=82=92=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=A8=20tc=20=E3=82=B0=E3=83=AB=E3=83=BC=E3=83=97=E3=81=AB?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/e2e-test-tc.yml | 5 +- tests/test_tc.py | 846 +++++++++++++++--------------- 2 files changed, 426 insertions(+), 425 deletions(-) diff --git a/.github/workflows/e2e-test-tc.yml b/.github/workflows/e2e-test-tc.yml index 398669ea..14c1f660 100644 --- a/.github/workflows/e2e-test-tc.yml +++ b/.github/workflows/e2e-test-tc.yml @@ -33,7 +33,6 @@ permissions: jobs: e2e_test_tc: - if: false # TC テストは Linux でのみ実行(tc は Linux カーネル機能) # Python 3.14 かつ Ubuntu 24.04 固定 runs-on: ubuntu-24.04 @@ -65,8 +64,8 @@ jobs: # Python バージョンの設定(3.14 固定) python-version: "3.14" - # 依存関係のインストール (test グループのみ) - - run: uv sync --no-install-project --only-group test + # 依存関係のインストール (test と tc グループ) + - run: uv sync --no-install-project --group test --group tc # Wheel ファイルのダウンロード (build.yml から呼び出された場合: 呼び出し元の artifact を使用) - if: inputs.from_build == true diff --git a/tests/test_tc.py b/tests/test_tc.py index c76c3110..d66a2ce6 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -9,425 +9,427 @@ tc ingress (受信方向) はリモートサーバー側のため制御不可。 """ -# import os -# import time -# from typing import Optional -# -# import pyroute2 -# import pytest -# from client import SoraClient, SoraRole -# -# # TC=1 環境変数が設定されている場合のみテストを実行 -# pytestmark = pytest.mark.skipif( -# os.getenv("TC") != "1", -# reason="TC=1 環境変数が必要", -# ) -# -# -# def get_default_interface() -> str: -# """デフォルトのネットワークインターフェース名を取得する。 -# -# Returns: -# デフォルトルートで使用されているインターフェース名 -# """ -# try: -# with pyroute2.IPRoute() as ipr: -# # デフォルトルートを取得(IPv4) -# for route in ipr.get_routes(family=2): # AF_INET = 2 -# # dst が存在しない場合がデフォルトルート -# if not route.get_attr("RTA_DST"): -# oif = route.get_attr("RTA_OIF") -# if oif: -# # インターフェース情報を取得 -# links = ipr.get_links(oif) -# if links: -# ifname = links[0].get_attr("IFLA_IFNAME") -# return ifname -# except Exception as e: -# print(f"デフォルトインターフェースの取得に失敗: {e}") -# -# # フォールバックとして eth0 を返す -# return "eth0" -# -# -# class TCEgressManager: -# """tc netem qdisc を使用して egress (送信方向) のネットワーク帯域制限を管理する。""" -# -# def __init__(self, interface: str = "eth0") -> None: -# """ -# TC egress 帯域制限マネージャーを初期化する。 -# -# Args: -# interface: tc ルールを適用するネットワークインターフェース -# """ -# self.interface: str = interface -# self._bandwidth_applied: bool = False -# self.ipr: Optional["pyroute2.IPRoute"] = None -# -# def __enter__(self): -# """コンテキストマネージャーのエントリ。""" -# self.ipr = pyroute2.IPRoute() -# return self -# -# def __exit__(self, exc_type, exc_val, exc_tb): -# """コンテキストマネージャーの終了 - tc ルールをクリーンアップする。""" -# try: -# self.cleanup() -# finally: -# if self.ipr: -# self.ipr.close() -# -# def add_bandwidth_limit(self, rate_kbps: int) -> None: -# """ -# インターフェースの egress に帯域制限を追加する。 -# -# Args: -# rate_kbps: 帯域制限 (Kbps) -# -# Raises: -# IndexError: インターフェースが見つからない場合 -# Exception: tc 操作が失敗した場合 -# """ -# if not self.ipr: -# raise RuntimeError("IPRoute が初期化されていません") -# -# # インターフェースインデックスを取得する -# indices = self.ipr.link_lookup(ifname=self.interface) -# if not indices: -# raise IndexError(f"インターフェース '{self.interface}' が見つかりません") -# idx = indices[0] -# -# # tbf (Token Bucket Filter) qdisc で帯域制限を追加する -# # root=True を指定すると handle は自動的に 0x10000 (= "1:") に設定される -# # rate: 帯域制限 (文字列で "500kbit" のように指定) -# # burst: バーストサイズ (bytes) -# # latency: 最大遅延時間 (文字列で "50ms" のように指定) -# self.ipr.tc( -# "add", -# "tbf", -# idx, -# root=True, -# rate=f"{rate_kbps}kbit", -# burst=32768, # 32KB -# latency="50ms", -# ) -# self._bandwidth_applied = True -# print(f"tc egress 帯域制限を追加: interface={self.interface}, rate={rate_kbps}kbps") -# -# def add_delay(self, delay_ms: int) -> None: -# """ -# インターフェースの egress に遅延を追加する。 -# -# Args: -# delay_ms: 遅延 (ミリ秒) -# -# Raises: -# IndexError: インターフェースが見つからない場合 -# Exception: tc 操作が失敗した場合 -# """ -# if not self.ipr: -# raise RuntimeError("IPRoute が初期化されていません") -# -# # インターフェースインデックスを取得する -# indices = self.ipr.link_lookup(ifname=self.interface) -# if not indices: -# raise IndexError(f"インターフェース '{self.interface}' が見つかりません") -# idx = indices[0] -# -# # netem qdisc で遅延を設定 -# self.ipr.tc( -# "add", -# "netem", -# idx, -# root=True, -# delay=delay_ms * 1000, # マイクロ秒に変換 -# ) -# self._bandwidth_applied = True -# print(f"tc egress 遅延を追加: interface={self.interface}, delay={delay_ms}ms") -# -# def get_stats(self) -> dict: -# """tc qdisc の統計情報を取得する。 -# -# Returns: -# 統計情報を含む辞書 (sent_bytes, sent_packets, drops など) -# """ -# if not self.ipr: -# raise RuntimeError("IPRoute が初期化されていません") -# -# # インターフェースインデックスを取得する -# indices = self.ipr.link_lookup(ifname=self.interface) -# if not indices: -# raise IndexError(f"インターフェース '{self.interface}' が見つかりません") -# idx = indices[0] -# -# # tc qdisc の情報を取得する -# for qdisc in self.ipr.get_qdiscs(idx): -# # netem qdisc の統計情報を抽出する -# if qdisc.get_attr("TCA_OPTIONS"): -# stats = { -# "sent_bytes": qdisc.get("bytes", 0), -# "sent_packets": qdisc.get("packets", 0), -# "drops": qdisc.get("drops", 0), -# "overlimits": qdisc.get("overlimits", 0), -# "requeues": qdisc.get("requeues", 0), -# } -# return stats -# -# return {} -# -# def cleanup(self) -> None: -# """インターフェースから tc 帯域制限設定を削除する。""" -# if not self._bandwidth_applied: -# return -# -# if not self.ipr: -# return -# -# try: -# indices = self.ipr.link_lookup(ifname=self.interface) -# if not indices: -# return -# idx = indices[0] -# # qdisc を削除する -# # 削除時には kind を指定せず、index と root のみを指定する -# self.ipr.tc("del", index=idx, root=True) -# self._bandwidth_applied = False -# print(f"tc egress 帯域制限を削除: interface={self.interface}") -# except Exception as e: -# # クリーンアップ時のエラーは無視する (qdisc が存在しない可能性がある) -# print(f"tc egress 帯域制限の削除時にエラー (無視): {e}") -# -# -# def verify_tc_settings(interface: str, qdisc_type: str = "tbf") -> bool: -# """tc の設定が存在するか確認する。 -# -# Args: -# interface: ネットワークインターフェース名 -# qdisc_type: 確認する qdisc の種類 (tbf または netem) -# -# Returns: -# 設定が存在する場合は True -# """ -# try: -# with pyroute2.IPRoute() as ipr: -# # インターフェースインデックスを取得 -# indices = ipr.link_lookup(ifname=interface) -# if not indices: -# return False -# idx = indices[0] -# -# # qdisc の情報を取得 -# for qdisc in ipr.get_qdiscs(idx): -# kind = qdisc.get_attr("TCA_KIND") -# if kind == qdisc_type: -# return True -# return False -# except Exception as e: -# print(f"tc 設定の確認に失敗: {e}") -# return False -# -# -# def show_tc_stats(interface: str) -> None: -# """tc の統計情報を表示する。 -# -# Args: -# interface: ネットワークインターフェース名 -# """ -# try: -# with pyroute2.IPRoute() as ipr: -# # インターフェースインデックスを取得 -# indices = ipr.link_lookup(ifname=interface) -# if not indices: -# print(f"インターフェース {interface} が見つかりません") -# return -# idx = indices[0] -# -# # qdisc の情報を取得して表示 -# print(f"\ntc 統計情報 ({interface}):") -# for qdisc in ipr.get_qdiscs(idx): -# kind = qdisc.get_attr("TCA_KIND") -# handle = qdisc.get("handle", 0) -# parent = qdisc.get("parent", 0) -# -# # 統計情報 -# sent_bytes = qdisc.get("bytes", 0) -# sent_packets = qdisc.get("packets", 0) -# drops = qdisc.get("drops", 0) -# overlimits = qdisc.get("overlimits", 0) -# -# print(f" qdisc {kind} handle {handle:#x} parent {parent:#x}") -# print(f" Sent {sent_bytes} bytes {sent_packets} packets") -# print(f" drops {drops}, overlimits {overlimits}") -# except Exception as e: -# print(f"tc 統計情報の取得に失敗: {e}") -# -# -# def test_tc_egress_bandwidth_limit(settings): -# """TURN ポート取得後に tc egress で帯域制限をかける。""" -# print("\n" + "=" * 60) -# print("テスト: tc egress 帯域制限 (500kbps) の適用") -# print("=" * 60) -# -# interface = get_default_interface() -# print(f"使用するネットワークインターフェース: {interface}") -# -# with SoraClient( -# settings, -# SoraRole.SENDONLY, -# audio=True, -# video=True, -# video_bit_rate=1000, -# ) as sendonly: -# time.sleep(3) -# -# # offer_message が受信されていることを確認 -# assert sendonly.offer_message is not None -# -# # turn_ports プロパティから TURN ポートを取得 -# turn_ports = sendonly.turn_ports -# -# # UDP ポートが取得できていることを確認 -# assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" -# -# # 最初の UDP ポートを使用 -# udp_port = turn_ports["udp"][0] -# print(f"TURN UDP ポート: {udp_port}") -# -# # tc egress で帯域制限を設定 -# with TCEgressManager(interface=interface) as tc: -# # 帯域制限を設定 (500kbps) -# bandwidth_kbps = 500 -# print(f"\nステップ 1: tc egress 帯域制限 {bandwidth_kbps}kbps を適用") -# tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) -# -# # tc の設定が存在することを確認 -# print("\nステップ 2: tc 設定を確認") -# assert verify_tc_settings(interface), "tc の設定が確認できません" -# -# # tc の統計情報を表示 (適用直後) -# show_tc_stats(interface) -# -# # 接続を維持して帯域制限が有効な状態でテスト -# print("\nステップ 3: 帯域制限が有効な状態で接続を維持") -# time.sleep(5) -# -# # tc の統計情報を表示 (接続後) -# show_tc_stats(interface) -# -# # 統計情報を取得 -# stats = tc.get_stats() -# print("\ntc 統計情報 (IPRoute):") -# for key, value in stats.items(): -# print(f" {key}: {value}") -# -# print("\n帯域制限が有効な状態でテスト完了") -# -# # クリーンアップ確認 -# print("\nクリーンアップ後の tc 設定:") -# show_tc_stats(interface) -# -# print("\n結果:") -# print(" ✓ テスト成功 (tc egress 帯域制限が適用された)") -# print("=" * 60 + "\n") -# -# -# def test_tc_egress_multiple_bandwidth_limits(settings): -# """複数の帯域制限パターンをテストする。""" -# print("\n" + "=" * 60) -# print("テスト: 複数の tc egress 帯域制限パターン") -# print("=" * 60) -# -# interface = get_default_interface() -# print(f"使用するネットワークインターフェース: {interface}") -# -# bandwidth_patterns = [2000, 1000, 500] # Kbps -# -# with SoraClient( -# settings, -# SoraRole.SENDRECV, -# audio=True, -# video=True, -# ) as client: -# time.sleep(3) -# -# # turn_ports プロパティから TURN ポートを取得 -# turn_ports = client.turn_ports -# -# print("\n取得した TURN ポート:") -# print(f" UDP: {turn_ports['udp']}") -# print(f" TCP: {turn_ports['tcp']}") -# print(f" TLS: {turn_ports['tls']}") -# -# for i, bandwidth_kbps in enumerate(bandwidth_patterns, 1): -# print(f"\n--- パターン {i}: {bandwidth_kbps}kbps ---") -# -# with TCEgressManager(interface=interface) as tc: -# tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) -# -# # tc の設定が存在することを確認 -# assert verify_tc_settings(interface), "tc の設定が確認できません" -# -# # 統計情報を表示 -# show_tc_stats(interface) -# -# # 接続を維持 -# time.sleep(3) -# -# # 統計情報を再度表示 -# stats = tc.get_stats() -# print("tc 統計情報 (IPRoute):") -# for key, value in stats.items(): -# print(f" {key}: {value}") -# -# print("\n結果:") -# print(" ✓ テスト成功 (複数の tc egress 帯域制限パターンが適用された)") -# print("=" * 60 + "\n") -# -# -# def test_tc_egress_delay(settings): -# """tc egress で遅延を適用する。""" -# print("\n" + "=" * 60) -# print("テスト: tc egress 遅延の適用") -# print("=" * 60) -# -# interface = get_default_interface() -# print(f"使用するネットワークインターフェース: {interface}") -# -# with SoraClient( -# settings, -# SoraRole.SENDONLY, -# audio=True, -# video=False, -# ) as sendonly: -# time.sleep(3) -# -# # turn_ports プロパティから TURN ポートを取得 -# turn_ports = sendonly.turn_ports -# assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" -# udp_port = turn_ports["udp"][0] -# print(f"TURN UDP ポート: {udp_port}") -# -# # 遅延を適用 -# with TCEgressManager(interface=interface) as tc: -# # netem で遅延 (50ms) を設定 -# delay_ms = 50 -# print(f"\ntc egress netem: delay={delay_ms}ms") -# -# tc.add_delay(delay_ms=delay_ms) -# -# # tc の設定が存在することを確認(netem を使用) -# assert verify_tc_settings(interface, qdisc_type="netem"), "tc の設定が確認できません" -# -# # 統計情報を表示 -# show_tc_stats(interface) -# -# # 接続を維持 -# time.sleep(5) -# -# # 統計情報を再度表示 -# show_tc_stats(interface) -# -# print("\n結果:") -# print(" ✓ テスト成功 (tc egress 遅延が適用された)") -# print("=" * 60 + "\n") -# +import os +import time +from typing import Optional + +try: + import pyroute2 +except ImportError: + pass +import pytest +from client import SoraClient, SoraRole + +# TC=1 環境変数が設定されている場合のみテストを実行 +pytestmark = pytest.mark.skipif( + os.getenv("TC") != "1", + reason="TC=1 環境変数が必要", +) + + +def get_default_interface() -> str: + """デフォルトのネットワークインターフェース名を取得する。 + + Returns: + デフォルトルートで使用されているインターフェース名 + """ + try: + with pyroute2.IPRoute() as ipr: + # デフォルトルートを取得(IPv4) + for route in ipr.get_routes(family=2): # AF_INET = 2 + # dst が存在しない場合がデフォルトルート + if not route.get_attr("RTA_DST"): + oif = route.get_attr("RTA_OIF") + if oif: + # インターフェース情報を取得 + links = ipr.get_links(oif) + if links: + ifname = links[0].get_attr("IFLA_IFNAME") + return ifname + except Exception as e: + print(f"デフォルトインターフェースの取得に失敗: {e}") + + # フォールバックとして eth0 を返す + return "eth0" + + +class TCEgressManager: + """tc netem qdisc を使用して egress (送信方向) のネットワーク帯域制限を管理する。""" + + def __init__(self, interface: str = "eth0") -> None: + """ + TC egress 帯域制限マネージャーを初期化する。 + + Args: + interface: tc ルールを適用するネットワークインターフェース + """ + self.interface: str = interface + self._bandwidth_applied: bool = False + self.ipr: Optional["pyroute2.IPRoute"] = None + + def __enter__(self): + """コンテキストマネージャーのエントリ。""" + self.ipr = pyroute2.IPRoute() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """コンテキストマネージャーの終了 - tc ルールをクリーンアップする。""" + try: + self.cleanup() + finally: + if self.ipr: + self.ipr.close() + + def add_bandwidth_limit(self, rate_kbps: int) -> None: + """ + インターフェースの egress に帯域制限を追加する。 + + Args: + rate_kbps: 帯域制限 (Kbps) + + Raises: + IndexError: インターフェースが見つからない場合 + Exception: tc 操作が失敗した場合 + """ + if not self.ipr: + raise RuntimeError("IPRoute が初期化されていません") + + # インターフェースインデックスを取得する + indices = self.ipr.link_lookup(ifname=self.interface) + if not indices: + raise IndexError(f"インターフェース '{self.interface}' が見つかりません") + idx = indices[0] + + # tbf (Token Bucket Filter) qdisc で帯域制限を追加する + # root=True を指定すると handle は自動的に 0x10000 (= "1:") に設定される + # rate: 帯域制限 (文字列で "500kbit" のように指定) + # burst: バーストサイズ (bytes) + # latency: 最大遅延時間 (文字列で "50ms" のように指定) + self.ipr.tc( + "add", + "tbf", + idx, + root=True, + rate=f"{rate_kbps}kbit", + burst=32768, # 32KB + latency="50ms", + ) + self._bandwidth_applied = True + print(f"tc egress 帯域制限を追加: interface={self.interface}, rate={rate_kbps}kbps") + + def add_delay(self, delay_ms: int) -> None: + """ + インターフェースの egress に遅延を追加する。 + + Args: + delay_ms: 遅延 (ミリ秒) + + Raises: + IndexError: インターフェースが見つからない場合 + Exception: tc 操作が失敗した場合 + """ + if not self.ipr: + raise RuntimeError("IPRoute が初期化されていません") + + # インターフェースインデックスを取得する + indices = self.ipr.link_lookup(ifname=self.interface) + if not indices: + raise IndexError(f"インターフェース '{self.interface}' が見つかりません") + idx = indices[0] + + # netem qdisc で遅延を設定 + self.ipr.tc( + "add", + "netem", + idx, + root=True, + delay=delay_ms * 1000, # マイクロ秒に変換 + ) + self._bandwidth_applied = True + print(f"tc egress 遅延を追加: interface={self.interface}, delay={delay_ms}ms") + + def get_stats(self) -> dict: + """tc qdisc の統計情報を取得する。 + + Returns: + 統計情報を含む辞書 (sent_bytes, sent_packets, drops など) + """ + if not self.ipr: + raise RuntimeError("IPRoute が初期化されていません") + + # インターフェースインデックスを取得する + indices = self.ipr.link_lookup(ifname=self.interface) + if not indices: + raise IndexError(f"インターフェース '{self.interface}' が見つかりません") + idx = indices[0] + + # tc qdisc の情報を取得する + for qdisc in self.ipr.get_qdiscs(idx): + # netem qdisc の統計情報を抽出する + if qdisc.get_attr("TCA_OPTIONS"): + stats = { + "sent_bytes": qdisc.get("bytes", 0), + "sent_packets": qdisc.get("packets", 0), + "drops": qdisc.get("drops", 0), + "overlimits": qdisc.get("overlimits", 0), + "requeues": qdisc.get("requeues", 0), + } + return stats + + return {} + + def cleanup(self) -> None: + """インターフェースから tc 帯域制限設定を削除する。""" + if not self._bandwidth_applied: + return + + if not self.ipr: + return + + try: + indices = self.ipr.link_lookup(ifname=self.interface) + if not indices: + return + idx = indices[0] + # qdisc を削除する + # 削除時には kind を指定せず、index と root のみを指定する + self.ipr.tc("del", index=idx, root=True) + self._bandwidth_applied = False + print(f"tc egress 帯域制限を削除: interface={self.interface}") + except Exception as e: + # クリーンアップ時のエラーは無視する (qdisc が存在しない可能性がある) + print(f"tc egress 帯域制限の削除時にエラー (無視): {e}") + + +def verify_tc_settings(interface: str, qdisc_type: str = "tbf") -> bool: + """tc の設定が存在するか確認する。 + + Args: + interface: ネットワークインターフェース名 + qdisc_type: 確認する qdisc の種類 (tbf または netem) + + Returns: + 設定が存在する場合は True + """ + try: + with pyroute2.IPRoute() as ipr: + # インターフェースインデックスを取得 + indices = ipr.link_lookup(ifname=interface) + if not indices: + return False + idx = indices[0] + + # qdisc の情報を取得 + for qdisc in ipr.get_qdiscs(idx): + kind = qdisc.get_attr("TCA_KIND") + if kind == qdisc_type: + return True + return False + except Exception as e: + print(f"tc 設定の確認に失敗: {e}") + return False + + +def show_tc_stats(interface: str) -> None: + """tc の統計情報を表示する。 + + Args: + interface: ネットワークインターフェース名 + """ + try: + with pyroute2.IPRoute() as ipr: + # インターフェースインデックスを取得 + indices = ipr.link_lookup(ifname=interface) + if not indices: + print(f"インターフェース {interface} が見つかりません") + return + idx = indices[0] + + # qdisc の情報を取得して表示 + print(f"\ntc 統計情報 ({interface}):") + for qdisc in ipr.get_qdiscs(idx): + kind = qdisc.get_attr("TCA_KIND") + handle = qdisc.get("handle", 0) + parent = qdisc.get("parent", 0) + + # 統計情報 + sent_bytes = qdisc.get("bytes", 0) + sent_packets = qdisc.get("packets", 0) + drops = qdisc.get("drops", 0) + overlimits = qdisc.get("overlimits", 0) + + print(f" qdisc {kind} handle {handle:#x} parent {parent:#x}") + print(f" Sent {sent_bytes} bytes {sent_packets} packets") + print(f" drops {drops}, overlimits {overlimits}") + except Exception as e: + print(f"tc 統計情報の取得に失敗: {e}") + + +def test_tc_egress_bandwidth_limit(settings): + """TURN ポート取得後に tc egress で帯域制限をかける。""" + print("\n" + "=" * 60) + print("テスト: tc egress 帯域制限 (500kbps) の適用") + print("=" * 60) + + interface = get_default_interface() + print(f"使用するネットワークインターフェース: {interface}") + + with SoraClient( + settings, + SoraRole.SENDONLY, + audio=True, + video=True, + video_bit_rate=1000, + ) as sendonly: + time.sleep(3) + + # offer_message が受信されていることを確認 + assert sendonly.offer_message is not None + + # turn_ports プロパティから TURN ポートを取得 + turn_ports = sendonly.turn_ports + + # UDP ポートが取得できていることを確認 + assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" + + # 最初の UDP ポートを使用 + udp_port = turn_ports["udp"][0] + print(f"TURN UDP ポート: {udp_port}") + + # tc egress で帯域制限を設定 + with TCEgressManager(interface=interface) as tc: + # 帯域制限を設定 (500kbps) + bandwidth_kbps = 500 + print(f"\nステップ 1: tc egress 帯域制限 {bandwidth_kbps}kbps を適用") + tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) + + # tc の設定が存在することを確認 + print("\nステップ 2: tc 設定を確認") + assert verify_tc_settings(interface), "tc の設定が確認できません" + + # tc の統計情報を表示 (適用直後) + show_tc_stats(interface) + + # 接続を維持して帯域制限が有効な状態でテスト + print("\nステップ 3: 帯域制限が有効な状態で接続を維持") + time.sleep(5) + + # tc の統計情報を表示 (接続後) + show_tc_stats(interface) + + # 統計情報を取得 + stats = tc.get_stats() + print("\ntc 統計情報 (IPRoute):") + for key, value in stats.items(): + print(f" {key}: {value}") + + print("\n帯域制限が有効な状態でテスト完了") + + # クリーンアップ確認 + print("\nクリーンアップ後の tc 設定:") + show_tc_stats(interface) + + print("\n結果:") + print(" ✓ テスト成功 (tc egress 帯域制限が適用された)") + print("=" * 60 + "\n") + + +def test_tc_egress_multiple_bandwidth_limits(settings): + """複数の帯域制限パターンをテストする。""" + print("\n" + "=" * 60) + print("テスト: 複数の tc egress 帯域制限パターン") + print("=" * 60) + + interface = get_default_interface() + print(f"使用するネットワークインターフェース: {interface}") + + bandwidth_patterns = [2000, 1000, 500] # Kbps + + with SoraClient( + settings, + SoraRole.SENDRECV, + audio=True, + video=True, + ) as client: + time.sleep(3) + + # turn_ports プロパティから TURN ポートを取得 + turn_ports = client.turn_ports + + print("\n取得した TURN ポート:") + print(f" UDP: {turn_ports['udp']}") + print(f" TCP: {turn_ports['tcp']}") + print(f" TLS: {turn_ports['tls']}") + + for i, bandwidth_kbps in enumerate(bandwidth_patterns, 1): + print(f"\n--- パターン {i}: {bandwidth_kbps}kbps ---") + + with TCEgressManager(interface=interface) as tc: + tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) + + # tc の設定が存在することを確認 + assert verify_tc_settings(interface), "tc の設定が確認できません" + + # 統計情報を表示 + show_tc_stats(interface) + + # 接続を維持 + time.sleep(3) + + # 統計情報を再度表示 + stats = tc.get_stats() + print("tc 統計情報 (IPRoute):") + for key, value in stats.items(): + print(f" {key}: {value}") + + print("\n結果:") + print(" ✓ テスト成功 (複数の tc egress 帯域制限パターンが適用された)") + print("=" * 60 + "\n") + + +def test_tc_egress_delay(settings): + """tc egress で遅延を適用する。""" + print("\n" + "=" * 60) + print("テスト: tc egress 遅延の適用") + print("=" * 60) + + interface = get_default_interface() + print(f"使用するネットワークインターフェース: {interface}") + + with SoraClient( + settings, + SoraRole.SENDONLY, + audio=True, + video=False, + ) as sendonly: + time.sleep(3) + + # turn_ports プロパティから TURN ポートを取得 + turn_ports = sendonly.turn_ports + assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" + udp_port = turn_ports["udp"][0] + print(f"TURN UDP ポート: {udp_port}") + + # 遅延を適用 + with TCEgressManager(interface=interface) as tc: + # netem で遅延 (50ms) を設定 + delay_ms = 50 + print(f"\ntc egress netem: delay={delay_ms}ms") + + tc.add_delay(delay_ms=delay_ms) + + # tc の設定が存在することを確認(netem を使用) + assert verify_tc_settings(interface, qdisc_type="netem"), "tc の設定が確認できません" + + # 統計情報を表示 + show_tc_stats(interface) + + # 接続を維持 + time.sleep(5) + + # 統計情報を再度表示 + show_tc_stats(interface) + + print("\n結果:") + print(" ✓ テスト成功 (tc egress 遅延が適用された)") + print("=" * 60 + "\n") From 88282fd31222b7f3b79cf36352190c388feee2b7 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 23:02:43 +0900 Subject: [PATCH 15/37] =?UTF-8?q?pyroute2=20=E3=81=AE=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=88=E6=96=B9=E6=B3=95=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E3=81=97=E3=80=81=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE?= =?UTF-8?q?=E8=AA=AC=E6=98=8E=E3=82=92=E6=9B=B4=E6=96=B0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index d66a2ce6..06435695 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -3,20 +3,12 @@ このテストは pyroute2 を使用して Linux の tc (traffic control) により ローカルインターフェースの egress (送信方向) に帯域制限を適用し、 TURN 経由での接続に対する効果を検証する。 - -注意: このテストはグローバルにある Sora サーバーへ接続するため、 - tc egress (送信方向) のみを制御できる。 - tc ingress (受信方向) はリモートサーバー側のため制御不可。 """ import os import time from typing import Optional -try: - import pyroute2 -except ImportError: - pass import pytest from client import SoraClient, SoraRole @@ -26,6 +18,8 @@ reason="TC=1 環境変数が必要", ) +pyroute2 = pytest.importorskip("pyroute2") + def get_default_interface() -> str: """デフォルトのネットワークインターフェース名を取得する。 From 4551e948394f51a3cdf4d8cbe38b808210347b30 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 23:08:18 +0900 Subject: [PATCH 16/37] =?UTF-8?q?tc=20=E3=83=86=E3=82=B9=E3=83=88=E3=81=AB?= =?UTF-8?q?=20WebRTC=20=E7=B5=B1=E8=A8=88=E6=83=85=E5=A0=B1=E3=81=AE?= =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=20pytest.importorskip=20=E3=82=92=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pytest.importorskip で pyroute2 のインポートを管理 - show_webrtc_stats 関数を追加し outbound-rtp 統計を表示 - test_tc_egress_bandwidth_limit で targetBitrate を確認 - targetBitrate が帯域制限値以下であることを assert で検証 - test_tc_egress_multiple_bandwidth_limits と test_tc_egress_delay を削除 --- tests/test_tc.py | 146 +++++++++++++++-------------------------------- 1 file changed, 46 insertions(+), 100 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 06435695..f2ced558 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -259,6 +259,30 @@ def show_tc_stats(interface: str) -> None: print(f"tc 統計情報の取得に失敗: {e}") +def show_webrtc_stats(client: SoraClient) -> None: + """WebRTC の統計情報を表示する。 + + Args: + client: SoraClient インスタンス + """ + try: + stats = client.get_stats() + print("\nWebRTC 統計情報:") + for stat in stats: + if stat.get("type") == "outbound-rtp": + print(" outbound-rtp:") + print(f" ssrc: {stat.get('ssrc')}") + print(f" mediaType: {stat.get('mediaType')}") + print(f" bytesSent: {stat.get('bytesSent')}") + print(f" packetsSent: {stat.get('packetsSent')}") + if "targetBitrate" in stat: + print(f" targetBitrate: {stat.get('targetBitrate')} bps") + if "totalPacketSendDelay" in stat: + print(f" totalPacketSendDelay: {stat.get('totalPacketSendDelay')} s") + except Exception as e: + print(f"WebRTC 統計情報の表示に失敗: {e}") + + def test_tc_egress_bandwidth_limit(settings): """TURN ポート取得後に tc egress で帯域制限をかける。""" print("\n" + "=" * 60) @@ -317,6 +341,28 @@ def test_tc_egress_bandwidth_limit(settings): for key, value in stats.items(): print(f" {key}: {value}") + # WebRTC 統計情報を表示 + print("\nステップ 4: WebRTC 統計情報を確認") + show_webrtc_stats(sendonly) + + # targetBitrate を確認 + stats = sendonly.get_stats() + outbound_rtp = next( + (stat for stat in stats if stat.get("type") == "outbound-rtp"), None + ) + assert outbound_rtp is not None, "outbound-rtp が取得できませんでした" + assert "targetBitrate" in outbound_rtp, "targetBitrate が存在しません" + + target_bitrate = outbound_rtp["targetBitrate"] + print( + f"\n確認: targetBitrate = {target_bitrate} bps ({target_bitrate / 1000} kbps)" + ) + print(f"期待値: {bandwidth_kbps} kbps 以下") + # 帯域制限が効いているか確認(多少のオーバーヘッドを考慮) + assert target_bitrate <= bandwidth_kbps * 1000 * 1.2, ( + f"targetBitrate が帯域制限を超えています: {target_bitrate} bps > {bandwidth_kbps * 1000} bps" + ) + print("\n帯域制限が有効な状態でテスト完了") # クリーンアップ確認 @@ -327,103 +373,3 @@ def test_tc_egress_bandwidth_limit(settings): print(" ✓ テスト成功 (tc egress 帯域制限が適用された)") print("=" * 60 + "\n") - -def test_tc_egress_multiple_bandwidth_limits(settings): - """複数の帯域制限パターンをテストする。""" - print("\n" + "=" * 60) - print("テスト: 複数の tc egress 帯域制限パターン") - print("=" * 60) - - interface = get_default_interface() - print(f"使用するネットワークインターフェース: {interface}") - - bandwidth_patterns = [2000, 1000, 500] # Kbps - - with SoraClient( - settings, - SoraRole.SENDRECV, - audio=True, - video=True, - ) as client: - time.sleep(3) - - # turn_ports プロパティから TURN ポートを取得 - turn_ports = client.turn_ports - - print("\n取得した TURN ポート:") - print(f" UDP: {turn_ports['udp']}") - print(f" TCP: {turn_ports['tcp']}") - print(f" TLS: {turn_ports['tls']}") - - for i, bandwidth_kbps in enumerate(bandwidth_patterns, 1): - print(f"\n--- パターン {i}: {bandwidth_kbps}kbps ---") - - with TCEgressManager(interface=interface) as tc: - tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) - - # tc の設定が存在することを確認 - assert verify_tc_settings(interface), "tc の設定が確認できません" - - # 統計情報を表示 - show_tc_stats(interface) - - # 接続を維持 - time.sleep(3) - - # 統計情報を再度表示 - stats = tc.get_stats() - print("tc 統計情報 (IPRoute):") - for key, value in stats.items(): - print(f" {key}: {value}") - - print("\n結果:") - print(" ✓ テスト成功 (複数の tc egress 帯域制限パターンが適用された)") - print("=" * 60 + "\n") - - -def test_tc_egress_delay(settings): - """tc egress で遅延を適用する。""" - print("\n" + "=" * 60) - print("テスト: tc egress 遅延の適用") - print("=" * 60) - - interface = get_default_interface() - print(f"使用するネットワークインターフェース: {interface}") - - with SoraClient( - settings, - SoraRole.SENDONLY, - audio=True, - video=False, - ) as sendonly: - time.sleep(3) - - # turn_ports プロパティから TURN ポートを取得 - turn_ports = sendonly.turn_ports - assert len(turn_ports["udp"]) > 0, "UDP ポートが取得できていません" - udp_port = turn_ports["udp"][0] - print(f"TURN UDP ポート: {udp_port}") - - # 遅延を適用 - with TCEgressManager(interface=interface) as tc: - # netem で遅延 (50ms) を設定 - delay_ms = 50 - print(f"\ntc egress netem: delay={delay_ms}ms") - - tc.add_delay(delay_ms=delay_ms) - - # tc の設定が存在することを確認(netem を使用) - assert verify_tc_settings(interface, qdisc_type="netem"), "tc の設定が確認できません" - - # 統計情報を表示 - show_tc_stats(interface) - - # 接続を維持 - time.sleep(5) - - # 統計情報を再度表示 - show_tc_stats(interface) - - print("\n結果:") - print(" ✓ テスト成功 (tc egress 遅延が適用された)") - print("=" * 60 + "\n") From 71859db966ec18d45c4ed89ee096799027c8b189 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 23:14:02 +0900 Subject: [PATCH 17/37] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=A7=20We?= =?UTF-8?q?bRTC=20=E7=B5=B1=E8=A8=88=E6=83=85=E5=A0=B1=E3=81=AE=E7=A2=BA?= =?UTF-8?q?=E8=AA=8D=E3=82=92=E5=8B=95=E7=94=BB=E3=83=A1=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=82=A2=E3=82=BF=E3=82=A4=E3=83=97=E3=81=AB=E9=99=90=E5=AE=9A?= =?UTF-8?q?=E3=81=97=E3=80=81=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=A1=E3=83=83?= =?UTF-8?q?=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E6=9B=B4=E6=96=B0=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index f2ced558..176b20d9 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -345,12 +345,17 @@ def test_tc_egress_bandwidth_limit(settings): print("\nステップ 4: WebRTC 統計情報を確認") show_webrtc_stats(sendonly) - # targetBitrate を確認 + # targetBitrate を確認 (video のみ) stats = sendonly.get_stats() outbound_rtp = next( - (stat for stat in stats if stat.get("type") == "outbound-rtp"), None + ( + stat + for stat in stats + if stat.get("type") == "outbound-rtp" and stat.get("mediaType") == "video" + ), + None, ) - assert outbound_rtp is not None, "outbound-rtp が取得できませんでした" + assert outbound_rtp is not None, "outbound-rtp (video) が取得できませんでした" assert "targetBitrate" in outbound_rtp, "targetBitrate が存在しません" target_bitrate = outbound_rtp["targetBitrate"] From 2923c45df2f63add18452be5dd3b7a94e99e303e Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 23:15:19 +0900 Subject: [PATCH 18/37] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE?= =?UTF-8?q?=E5=B8=AF=E5=9F=9F=E5=88=B6=E9=99=90=E7=B6=AD=E6=8C=81=E6=99=82?= =?UTF-8?q?=E9=96=93=E3=82=925=E7=A7=92=E3=81=8B=E3=82=8910=E7=A7=92?= =?UTF-8?q?=E3=81=AB=E5=BB=B6=E9=95=B7=E3=81=97=E3=80=81targetBitrate?= =?UTF-8?q?=E3=81=AE=E7=A2=BA=E8=AA=8D=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=82=92=E7=B0=A1=E7=95=A5=E5=8C=96=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 176b20d9..3a0cccb7 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -330,7 +330,7 @@ def test_tc_egress_bandwidth_limit(settings): # 接続を維持して帯域制限が有効な状態でテスト print("\nステップ 3: 帯域制限が有効な状態で接続を維持") - time.sleep(5) + time.sleep(10) # tc の統計情報を表示 (接続後) show_tc_stats(interface) @@ -359,9 +359,7 @@ def test_tc_egress_bandwidth_limit(settings): assert "targetBitrate" in outbound_rtp, "targetBitrate が存在しません" target_bitrate = outbound_rtp["targetBitrate"] - print( - f"\n確認: targetBitrate = {target_bitrate} bps ({target_bitrate / 1000} kbps)" - ) + print(f"\n確認: targetBitrate = {target_bitrate} bps ({target_bitrate / 1000} kbps)") print(f"期待値: {bandwidth_kbps} kbps 以下") # 帯域制限が効いているか確認(多少のオーバーヘッドを考慮) assert target_bitrate <= bandwidth_kbps * 1000 * 1.2, ( @@ -377,4 +375,3 @@ def test_tc_egress_bandwidth_limit(settings): print("\n結果:") print(" ✓ テスト成功 (tc egress 帯域制限が適用された)") print("=" * 60 + "\n") - From b8554bad17244dc21a4a2a4dc778cbf1888bed5d Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 23:18:29 +0900 Subject: [PATCH 19/37] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=A7=20We?= =?UTF-8?q?bRTC=20=E7=B5=B1=E8=A8=88=E6=83=85=E5=A0=B1=E3=81=AE=E3=83=A1?= =?UTF-8?q?=E3=83=87=E3=82=A3=E3=82=A2=E3=82=BF=E3=82=A4=E3=83=97=E3=81=AE?= =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E6=9D=A1=E4=BB=B6=E3=82=92=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E3=81=97=E3=80=81kind=20=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 3a0cccb7..693698cf 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -351,7 +351,7 @@ def test_tc_egress_bandwidth_limit(settings): ( stat for stat in stats - if stat.get("type") == "outbound-rtp" and stat.get("mediaType") == "video" + if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" ), None, ) From 531bd66f20e771fb528b5bdab3b099b7eda50051 Mon Sep 17 00:00:00 2001 From: voluntas Date: Sun, 19 Oct 2025 23:36:14 +0900 Subject: [PATCH 20/37] =?UTF-8?q?WebRTC=20=E7=B5=B1=E8=A8=88=E6=83=85?= =?UTF-8?q?=E5=A0=B1=E3=81=AE=E8=A1=A8=E7=A4=BA=E9=96=A2=E6=95=B0=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E3=81=97=E3=80=81=E5=BC=95=E6=95=B0=E3=82=92?= =?UTF-8?q?=20SoraClient=20=E3=81=8B=E3=82=89=E5=8F=96=E5=BE=97=E3=81=97?= =?UTF-8?q?=E3=81=9F=E7=B5=B1=E8=A8=88=E6=83=85=E5=A0=B1=E3=81=AE=E3=83=AA?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=80=82=E5=88=B6=E9=99=90=E5=89=8D=E5=BE=8C=E3=81=AE=E7=B5=B1?= =?UTF-8?q?=E8=A8=88=E6=83=85=E5=A0=B1=E3=82=92=E7=A2=BA=E8=AA=8D=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 693698cf..43d263d3 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -259,20 +259,19 @@ def show_tc_stats(interface: str) -> None: print(f"tc 統計情報の取得に失敗: {e}") -def show_webrtc_stats(client: SoraClient) -> None: +def show_webrtc_stats(stats: list) -> None: """WebRTC の統計情報を表示する。 Args: - client: SoraClient インスタンス + stats: get_stats() で取得した統計情報のリスト """ try: - stats = client.get_stats() print("\nWebRTC 統計情報:") for stat in stats: if stat.get("type") == "outbound-rtp": print(" outbound-rtp:") print(f" ssrc: {stat.get('ssrc')}") - print(f" mediaType: {stat.get('mediaType')}") + print(f" kind: {stat.get('kind')}") print(f" bytesSent: {stat.get('bytesSent')}") print(f" packetsSent: {stat.get('packetsSent')}") if "targetBitrate" in stat: @@ -314,6 +313,33 @@ def test_tc_egress_bandwidth_limit(settings): udp_port = turn_ports["udp"][0] print(f"TURN UDP ポート: {udp_port}") + # 制限前の WebRTC 統計情報を確認 + print("\n制限前の WebRTC 統計情報:") + time.sleep(3) + stats_before = sendonly.get_stats() + show_webrtc_stats(stats_before) + + # 制限前の targetBitrate を確認 (video のみ) + outbound_rtp_before = next( + ( + stat + for stat in stats_before + if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" + ), + None, + ) + assert outbound_rtp_before is not None, "outbound-rtp (video) が取得できませんでした" + assert "targetBitrate" in outbound_rtp_before, "targetBitrate が存在しません" + + target_bitrate_before = outbound_rtp_before["targetBitrate"] + print( + f"\n制限前の targetBitrate: {target_bitrate_before} bps ({target_bitrate_before / 1000} kbps)" + ) + # video_bit_rate=1000 を指定しているので、500kbps 以上あることを確認 + assert target_bitrate_before >= 500 * 1000, ( + f"制限前の targetBitrate が想定より低い: {target_bitrate_before} bps < 500000 bps" + ) + # tc egress で帯域制限を設定 with TCEgressManager(interface=interface) as tc: # 帯域制限を設定 (500kbps) @@ -342,11 +368,12 @@ def test_tc_egress_bandwidth_limit(settings): print(f" {key}: {value}") # WebRTC 統計情報を表示 - print("\nステップ 4: WebRTC 統計情報を確認") - show_webrtc_stats(sendonly) + print("\nステップ 4: 制限後の WebRTC 統計情報を確認") + stats_after = sendonly.get_stats() + show_webrtc_stats(stats_after) # targetBitrate を確認 (video のみ) - stats = sendonly.get_stats() + stats = stats_after outbound_rtp = next( ( stat From 81f37161a5bb3dbdfee11d79f50e724787bc40a4 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 22:12:47 +0900 Subject: [PATCH 21/37] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E5=AE=9F?= =?UTF-8?q?=E8=A1=8C=E6=9D=A1=E4=BB=B6=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=97?= =?UTF-8?q?=E3=80=81Linux=20=E7=92=B0=E5=A2=83=E3=81=A7=E3=81=AE=E5=AE=9F?= =?UTF-8?q?=E8=A1=8C=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 43d263d3..e2cad7b1 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -6,18 +6,20 @@ """ import os +import sys import time from typing import Optional import pytest from client import SoraClient, SoraRole -# TC=1 環境変数が設定されている場合のみテストを実行 +# TC=1 環境変数が設定されており、かつ Linux 環境の場合のみテストを実行 pytestmark = pytest.mark.skipif( - os.getenv("TC") != "1", - reason="TC=1 環境変数が必要", + os.getenv("TC") != "1" or sys.platform != "linux", + reason="TC=1 環境変数と Linux 環境が必要", ) +# pyroute2 がインストールされていない場合はスキップ pyroute2 = pytest.importorskip("pyroute2") From ff802c7e1139aa967390a9e4ef11c1a05e25d410 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 22:17:13 +0900 Subject: [PATCH 22/37] =?UTF-8?q?tc=20egress=20=E5=B8=AF=E5=9F=9F=E5=88=B6?= =?UTF-8?q?=E9=99=90=E3=82=92=20500kbps=20=E3=81=8B=E3=82=89=20250kbps=20?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=97=E3=80=81=E9=9F=B3=E5=A3=B0?= =?UTF-8?q?=E9=80=81=E4=BF=A1=E3=82=92=E7=84=A1=E5=8A=B9=E3=81=AB=E3=81=97?= =?UTF-8?q?=E3=81=A6=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE=E5=BE=85=E6=A9=9F?= =?UTF-8?q?=E6=99=82=E9=96=93=E3=82=92=E5=BB=B6=E9=95=B7=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index e2cad7b1..884087f2 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -287,7 +287,7 @@ def show_webrtc_stats(stats: list) -> None: def test_tc_egress_bandwidth_limit(settings): """TURN ポート取得後に tc egress で帯域制限をかける。""" print("\n" + "=" * 60) - print("テスト: tc egress 帯域制限 (500kbps) の適用") + print("テスト: tc egress 帯域制限 (250kbps) の適用") print("=" * 60) interface = get_default_interface() @@ -296,11 +296,11 @@ def test_tc_egress_bandwidth_limit(settings): with SoraClient( settings, SoraRole.SENDONLY, - audio=True, + audio=False, video=True, video_bit_rate=1000, ) as sendonly: - time.sleep(3) + time.sleep(10) # offer_message が受信されていることを確認 assert sendonly.offer_message is not None @@ -338,14 +338,14 @@ def test_tc_egress_bandwidth_limit(settings): f"\n制限前の targetBitrate: {target_bitrate_before} bps ({target_bitrate_before / 1000} kbps)" ) # video_bit_rate=1000 を指定しているので、500kbps 以上あることを確認 - assert target_bitrate_before >= 500 * 1000, ( + assert target_bitrate_before >= 750 * 1000, ( f"制限前の targetBitrate が想定より低い: {target_bitrate_before} bps < 500000 bps" ) # tc egress で帯域制限を設定 with TCEgressManager(interface=interface) as tc: - # 帯域制限を設定 (500kbps) - bandwidth_kbps = 500 + bandwidth_kbps = 250 + # 帯域制限を設定 print(f"\nステップ 1: tc egress 帯域制限 {bandwidth_kbps}kbps を適用") tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) From 2cdd5e884fa843b6eee0a2035aeea902152415cf Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 22:17:49 +0900 Subject: [PATCH 23/37] =?UTF-8?q?=E5=B8=AF=E5=9F=9F=E5=88=B6=E9=99=90?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=81=A7=E3=81=AE=E3=82=B3?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92=E6=9B=B4=E6=96=B0=E3=81=97?= =?UTF-8?q?=E3=80=81=E5=88=B6=E9=99=90=E5=80=A4=E3=82=92500kbps=E3=81=8B?= =?UTF-8?q?=E3=82=89750kbps=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 884087f2..2a6475b9 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -98,7 +98,7 @@ def add_bandwidth_limit(self, rate_kbps: int) -> None: # tbf (Token Bucket Filter) qdisc で帯域制限を追加する # root=True を指定すると handle は自動的に 0x10000 (= "1:") に設定される - # rate: 帯域制限 (文字列で "500kbit" のように指定) + # rate: 帯域制限 (文字列で "750 kbit" のように指定) # burst: バーストサイズ (bytes) # latency: 最大遅延時間 (文字列で "50ms" のように指定) self.ipr.tc( @@ -337,9 +337,9 @@ def test_tc_egress_bandwidth_limit(settings): print( f"\n制限前の targetBitrate: {target_bitrate_before} bps ({target_bitrate_before / 1000} kbps)" ) - # video_bit_rate=1000 を指定しているので、500kbps 以上あることを確認 + # video_bit_rate=1000 を指定しているので、750kbps 以上あることを確認 assert target_bitrate_before >= 750 * 1000, ( - f"制限前の targetBitrate が想定より低い: {target_bitrate_before} bps < 500000 bps" + f"制限前の targetBitrate が想定より低い: {target_bitrate_before} bps < 750000 bps" ) # tc egress で帯域制限を設定 From dcfe9c1d6c4648133ce5ed8463b3d942d07c0c7d Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 22:21:11 +0900 Subject: [PATCH 24/37] =?UTF-8?q?Python=20=E3=81=AE=E3=83=90=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=83=A7=E3=83=B3=E8=A6=81=E4=BB=B6=E3=82=92=203.11?= =?UTF-8?q?=20=E4=BB=A5=E4=B8=8A=E3=81=AB=E8=A8=AD=E5=AE=9A=E3=81=97?= =?UTF-8?q?=E3=80=81=E6=9C=AB=E5=B0=BE=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AE=E5=88=A9=E7=94=A8=E3=82=92=E7=A6=81=E6=AD=A2=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 375baee3..a6cf6c9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,3 +24,8 @@ - コミットメッセージは日本語で書くこと - コミットメッセージは命令形で書くこと - コミットメッセージは〜するという形で書くこと + +## Python について + +- Python のバージョンは 3.11 以上を前提とすること +- 末尾コメントを利用しないこと From cb9501a5894b328a1416ae121c2e9daea0a73652 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 22:21:15 +0900 Subject: [PATCH 25/37] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E5=80=A4=E3=82=92=E5=AE=9A=E6=95=B0=E5=8C=96?= =?UTF-8?q?=E3=81=97=E3=80=81=E5=B8=AF=E5=9F=9F=E5=88=B6=E9=99=90=E3=81=AE?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=82=92=E6=94=B9=E5=96=84=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 2a6475b9..9216d327 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -22,6 +22,11 @@ # pyroute2 がインストールされていない場合はスキップ pyroute2 = pytest.importorskip("pyroute2") +# テスト用の設定値 +INITIAL_BITRATE_KBPS = 1500 # 初期ビットレート (kbps) +BANDWIDTH_LIMIT_KBPS = 250 # 帯域制限値 (kbps) +MIN_BITRATE_BEFORE_LIMIT_KBPS = 750 # 制限前の最小ビットレート (kbps) + def get_default_interface() -> str: """デフォルトのネットワークインターフェース名を取得する。 @@ -287,7 +292,7 @@ def show_webrtc_stats(stats: list) -> None: def test_tc_egress_bandwidth_limit(settings): """TURN ポート取得後に tc egress で帯域制限をかける。""" print("\n" + "=" * 60) - print("テスト: tc egress 帯域制限 (250kbps) の適用") + print(f"テスト: tc egress 帯域制限 ({BANDWIDTH_LIMIT_KBPS}kbps) の適用") print("=" * 60) interface = get_default_interface() @@ -296,9 +301,10 @@ def test_tc_egress_bandwidth_limit(settings): with SoraClient( settings, SoraRole.SENDONLY, + simulcast=True, audio=False, video=True, - video_bit_rate=1000, + video_bit_rate=INITIAL_BITRATE_KBPS, ) as sendonly: time.sleep(10) @@ -337,17 +343,16 @@ def test_tc_egress_bandwidth_limit(settings): print( f"\n制限前の targetBitrate: {target_bitrate_before} bps ({target_bitrate_before / 1000} kbps)" ) - # video_bit_rate=1000 を指定しているので、750kbps 以上あることを確認 - assert target_bitrate_before >= 750 * 1000, ( - f"制限前の targetBitrate が想定より低い: {target_bitrate_before} bps < 750000 bps" + # video_bit_rate を指定しているので、MIN_BITRATE_BEFORE_LIMIT_KBPS 以上あることを確認 + assert target_bitrate_before >= MIN_BITRATE_BEFORE_LIMIT_KBPS * 1000, ( + f"制限前の targetBitrate が想定より低い: {target_bitrate_before} bps < {MIN_BITRATE_BEFORE_LIMIT_KBPS * 1000} bps" ) # tc egress で帯域制限を設定 with TCEgressManager(interface=interface) as tc: - bandwidth_kbps = 250 # 帯域制限を設定 - print(f"\nステップ 1: tc egress 帯域制限 {bandwidth_kbps}kbps を適用") - tc.add_bandwidth_limit(rate_kbps=bandwidth_kbps) + print(f"\nステップ 1: tc egress 帯域制限 {BANDWIDTH_LIMIT_KBPS}kbps を適用") + tc.add_bandwidth_limit(rate_kbps=BANDWIDTH_LIMIT_KBPS) # tc の設定が存在することを確認 print("\nステップ 2: tc 設定を確認") @@ -389,10 +394,10 @@ def test_tc_egress_bandwidth_limit(settings): target_bitrate = outbound_rtp["targetBitrate"] print(f"\n確認: targetBitrate = {target_bitrate} bps ({target_bitrate / 1000} kbps)") - print(f"期待値: {bandwidth_kbps} kbps 以下") + print(f"期待値: {BANDWIDTH_LIMIT_KBPS} kbps 以下") # 帯域制限が効いているか確認(多少のオーバーヘッドを考慮) - assert target_bitrate <= bandwidth_kbps * 1000 * 1.2, ( - f"targetBitrate が帯域制限を超えています: {target_bitrate} bps > {bandwidth_kbps * 1000} bps" + assert target_bitrate <= BANDWIDTH_LIMIT_KBPS * 1000 * 1.2, ( + f"targetBitrate が帯域制限を超えています: {target_bitrate} bps > {BANDWIDTH_LIMIT_KBPS * 1000} bps" ) print("\n帯域制限が有効な状態でテスト完了") From 7d9fa4326f9f8e6ce148305f15e1e28d04381d72 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 23:02:09 +0900 Subject: [PATCH 26/37] =?UTF-8?q?WebRTC=20=E7=B5=B1=E8=A8=88=E6=83=85?= =?UTF-8?q?=E5=A0=B1=E3=81=AE=E5=87=BA=E5=8A=9B=E3=82=92=E6=94=B9=E5=96=84?= =?UTF-8?q?=E3=81=97=E3=80=81simulcast=20=E3=82=B9=E3=83=88=E3=83=AA?= =?UTF-8?q?=E3=83=BC=E3=83=A0=E3=81=AE=20targetBitrate=20=E3=82=92?= =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E3=81=99=E3=82=8B=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 117 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 38 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 9216d327..75644414 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -112,7 +112,8 @@ def add_bandwidth_limit(self, rate_kbps: int) -> None: idx, root=True, rate=f"{rate_kbps}kbit", - burst=32768, # 32KB + # 32KB + burst=32768, latency="50ms", ) self._bandwidth_applied = True @@ -276,9 +277,13 @@ def show_webrtc_stats(stats: list) -> None: print("\nWebRTC 統計情報:") for stat in stats: if stat.get("type") == "outbound-rtp": - print(" outbound-rtp:") + rid = stat.get("rid", "") + rid_label = f" (rid={rid})" if rid else "" + print(f" outbound-rtp{rid_label}:") print(f" ssrc: {stat.get('ssrc')}") print(f" kind: {stat.get('kind')}") + if rid: + print(f" rid: {rid}") print(f" bytesSent: {stat.get('bytesSent')}") print(f" packetsSent: {stat.get('packetsSent')}") if "targetBitrate" in stat: @@ -324,28 +329,40 @@ def test_tc_egress_bandwidth_limit(settings): # 制限前の WebRTC 統計情報を確認 print("\n制限前の WebRTC 統計情報:") time.sleep(3) - stats_before = sendonly.get_stats() - show_webrtc_stats(stats_before) - - # 制限前の targetBitrate を確認 (video のみ) - outbound_rtp_before = next( - ( - stat - for stat in stats_before - if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" - ), - None, + stats = sendonly.get_stats() + show_webrtc_stats(stats) + + # 制限前の targetBitrate を確認 (video の全ての outbound-rtp を取得) + simulcast_outbound_rtp_stats = [ + stat + for stat in stats + if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" + ] + assert len(simulcast_outbound_rtp_stats) > 0, "outbound-rtp (video) が取得できませんでした" + + # rid でソート (r0, r1, r2 の順) + simulcast_outbound_rtp_stats.sort(key=lambda x: x.get("rid", "")) + + # simulcast が有効な場合は 3 つのストリーム (r0, r1, r2) が存在するはず + assert len(simulcast_outbound_rtp_stats) == 3, ( + f"simulcast のストリーム数が不正: {len(simulcast_outbound_rtp_stats)} (期待値: 3)" ) - assert outbound_rtp_before is not None, "outbound-rtp (video) が取得できませんでした" - assert "targetBitrate" in outbound_rtp_before, "targetBitrate が存在しません" - target_bitrate_before = outbound_rtp_before["targetBitrate"] + print("\n制限前の targetBitrate:") print( - f"\n制限前の targetBitrate: {target_bitrate_before} bps ({target_bitrate_before / 1000} kbps)" + f" rid={simulcast_outbound_rtp_stats[0].get('rid', 'none')}: {simulcast_outbound_rtp_stats[0]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[0]['targetBitrate'] / 1000} kbps)" ) - # video_bit_rate を指定しているので、MIN_BITRATE_BEFORE_LIMIT_KBPS 以上あることを確認 - assert target_bitrate_before >= MIN_BITRATE_BEFORE_LIMIT_KBPS * 1000, ( - f"制限前の targetBitrate が想定より低い: {target_bitrate_before} bps < {MIN_BITRATE_BEFORE_LIMIT_KBPS * 1000} bps" + print( + f" rid={simulcast_outbound_rtp_stats[1].get('rid', 'none')}: {simulcast_outbound_rtp_stats[1]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[1]['targetBitrate'] / 1000} kbps)" + ) + print( + f" rid={simulcast_outbound_rtp_stats[2].get('rid', 'none')}: {simulcast_outbound_rtp_stats[2]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[2]['targetBitrate'] / 1000} kbps)" + ) + + # r2 (最高画質) のビットレートが MIN_BITRATE_BEFORE_LIMIT_KBPS 以上あることを確認 + r2_bitrate = simulcast_outbound_rtp_stats[2]["targetBitrate"] + assert r2_bitrate >= MIN_BITRATE_BEFORE_LIMIT_KBPS * 1000, ( + f"制限前の r2 targetBitrate が想定より低い: {r2_bitrate} bps < {MIN_BITRATE_BEFORE_LIMIT_KBPS * 1000} bps" ) # tc egress で帯域制限を設定 @@ -376,28 +393,52 @@ def test_tc_egress_bandwidth_limit(settings): # WebRTC 統計情報を表示 print("\nステップ 4: 制限後の WebRTC 統計情報を確認") - stats_after = sendonly.get_stats() - show_webrtc_stats(stats_after) - - # targetBitrate を確認 (video のみ) - stats = stats_after - outbound_rtp = next( - ( - stat - for stat in stats - if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" - ), - None, + stats = sendonly.get_stats() + show_webrtc_stats(stats) + + # targetBitrate を確認 (video の全ての outbound-rtp を取得) + simulcast_outbound_rtp_stats = [ + stat + for stat in stats + if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" + ] + assert len(simulcast_outbound_rtp_stats) > 0, ( + "outbound-rtp (video) が取得できませんでした" ) - assert outbound_rtp is not None, "outbound-rtp (video) が取得できませんでした" - assert "targetBitrate" in outbound_rtp, "targetBitrate が存在しません" - target_bitrate = outbound_rtp["targetBitrate"] - print(f"\n確認: targetBitrate = {target_bitrate} bps ({target_bitrate / 1000} kbps)") + # rid でソート (r0, r1, r2 の順) + simulcast_outbound_rtp_stats.sort(key=lambda x: x.get("rid", "")) + + # simulcast が有効な場合は 3 つのストリーム (r0, r1, r2) が存在するはず + assert len(simulcast_outbound_rtp_stats) == 3, ( + f"simulcast のストリーム数が不正: {len(simulcast_outbound_rtp_stats)} (期待値: 3)" + ) + + print("\n制限後の targetBitrate:") + print( + f" rid={simulcast_outbound_rtp_stats[0].get('rid', 'none')}: {simulcast_outbound_rtp_stats[0]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[0]['targetBitrate'] / 1000} kbps)" + ) + print( + f" rid={simulcast_outbound_rtp_stats[1].get('rid', 'none')}: {simulcast_outbound_rtp_stats[1]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[1]['targetBitrate'] / 1000} kbps)" + ) + print( + f" rid={simulcast_outbound_rtp_stats[2].get('rid', 'none')}: {simulcast_outbound_rtp_stats[2]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[2]['targetBitrate'] / 1000} kbps)" + ) + + # 全てのストリームの targetBitrate の合計を確認 + total_target_bitrate = ( + simulcast_outbound_rtp_stats[0]["targetBitrate"] + + simulcast_outbound_rtp_stats[1]["targetBitrate"] + + simulcast_outbound_rtp_stats[2]["targetBitrate"] + ) + print( + f"\n確認: 合計 targetBitrate = {total_target_bitrate} bps ({total_target_bitrate / 1000} kbps)" + ) print(f"期待値: {BANDWIDTH_LIMIT_KBPS} kbps 以下") + # 帯域制限が効いているか確認(多少のオーバーヘッドを考慮) - assert target_bitrate <= BANDWIDTH_LIMIT_KBPS * 1000 * 1.2, ( - f"targetBitrate が帯域制限を超えています: {target_bitrate} bps > {BANDWIDTH_LIMIT_KBPS * 1000} bps" + assert total_target_bitrate <= BANDWIDTH_LIMIT_KBPS * 1000 * 1.2, ( + f"合計 targetBitrate が帯域制限を超えています: {total_target_bitrate} bps > {BANDWIDTH_LIMIT_KBPS * 1000} bps" ) print("\n帯域制限が有効な状態でテスト完了") From dbbf886b6ed82ed9573c994ec8a5d2eb9f859449 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 23:05:02 +0900 Subject: [PATCH 27/37] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=A7?= =?UTF-8?q?=E3=81=AE=E3=83=93=E3=83=87=E3=82=AA=E3=81=AE=E8=A7=A3=E5=83=8F?= =?UTF-8?q?=E5=BA=A6=E3=82=92=E8=A8=AD=E5=AE=9A=E3=81=97=E3=80=81=E5=B9=85?= =?UTF-8?q?=E3=82=92960px=E3=80=81=E9=AB=98=E3=81=95=E3=82=92540px?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_tc.py b/tests/test_tc.py index 75644414..0f726012 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -310,6 +310,8 @@ def test_tc_egress_bandwidth_limit(settings): audio=False, video=True, video_bit_rate=INITIAL_BITRATE_KBPS, + video_width=960, + video_height=540, ) as sendonly: time.sleep(10) From 3506ceac029456bbbd4fa43bf485730187e19cdf Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 23:13:19 +0900 Subject: [PATCH 28/37] =?UTF-8?q?=E5=88=9D=E6=9C=9F=E3=83=93=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=83=AC=E3=83=BC=E3=83=88=E3=82=921500kbps=E3=81=8B?= =?UTF-8?q?=E3=82=891200kbps=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=97=E3=80=81?= =?UTF-8?q?=E5=88=B6=E9=99=90=E5=89=8D=E3=81=AE=E6=9C=80=E5=B0=8F=E3=83=93?= =?UTF-8?q?=E3=83=83=E3=83=88=E3=83=AC=E3=83=BC=E3=83=88=E3=82=92750kbps?= =?UTF-8?q?=E3=81=8B=E3=82=89500kbps=E3=81=AB=E4=BF=AE=E6=AD=A3=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=80=82=E3=83=86=E3=82=B9=E3=83=88=E3=81=ABVP8?= =?UTF-8?q?=E3=83=93=E3=83=87=E3=82=AA=E3=82=B3=E3=83=BC=E3=83=87=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=82=A4=E3=83=97=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 0f726012..94ac17ee 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -23,9 +23,9 @@ pyroute2 = pytest.importorskip("pyroute2") # テスト用の設定値 -INITIAL_BITRATE_KBPS = 1500 # 初期ビットレート (kbps) +INITIAL_BITRATE_KBPS = 1200 # 初期ビットレート (kbps) BANDWIDTH_LIMIT_KBPS = 250 # 帯域制限値 (kbps) -MIN_BITRATE_BEFORE_LIMIT_KBPS = 750 # 制限前の最小ビットレート (kbps) +MIN_BITRATE_BEFORE_LIMIT_KBPS = 500 # 制限前の最小ビットレート (kbps) def get_default_interface() -> str: @@ -309,6 +309,7 @@ def test_tc_egress_bandwidth_limit(settings): simulcast=True, audio=False, video=True, + video_codec_type="VP8", video_bit_rate=INITIAL_BITRATE_KBPS, video_width=960, video_height=540, From 9f80738a2a3f095d820f7aa252bb050402983689 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 23:25:57 +0900 Subject: [PATCH 29/37] =?UTF-8?q?simulcast=20=E3=81=AE=E5=B8=AF=E5=9F=9F?= =?UTF-8?q?=E5=88=B6=E9=99=90=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E6=94=B9?= =?UTF-8?q?=E5=96=84=E3=81=97=E3=80=81targetBitrate=20=E3=81=AE=E7=A2=BA?= =?UTF-8?q?=E8=AA=8D=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 64 +++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 94ac17ee..3ec621a8 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -412,36 +412,50 @@ def test_tc_egress_bandwidth_limit(settings): # rid でソート (r0, r1, r2 の順) simulcast_outbound_rtp_stats.sort(key=lambda x: x.get("rid", "")) - # simulcast が有効な場合は 3 つのストリーム (r0, r1, r2) が存在するはず - assert len(simulcast_outbound_rtp_stats) == 3, ( - f"simulcast のストリーム数が不正: {len(simulcast_outbound_rtp_stats)} (期待値: 3)" - ) + print("\n制限後の outbound-rtp 統計情報:") + for stat in simulcast_outbound_rtp_stats: + rid = stat.get("rid", "none") + print(f"{rid} | outbound-rtp: {stats}") + bitrate = stat.get("targetBitrate") + if bitrate is not None: + print(f" rid={rid}: targetBitrate={bitrate} bps ({bitrate / 1000} kbps)") + else: + print(f" rid={rid}: targetBitrate なし (停止中)") + + # 帯域制限により r1/r2 の targetBitrate 項目が存在しなくなる + # r0 のみが targetBitrate を持つことを確認 + all_rids = [stat.get("rid") for stat in simulcast_outbound_rtp_stats] + active_rids = [ + stat.get("rid") for stat in simulcast_outbound_rtp_stats if "targetBitrate" in stat + ] + print(f"\n確認: 存在する rid = {all_rids}") + print(f"確認: targetBitrate を持つ rid = {active_rids}") + print("期待値: targetBitrate を持つのは r0 のみ") - print("\n制限後の targetBitrate:") - print( - f" rid={simulcast_outbound_rtp_stats[0].get('rid', 'none')}: {simulcast_outbound_rtp_stats[0]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[0]['targetBitrate'] / 1000} kbps)" - ) - print( - f" rid={simulcast_outbound_rtp_stats[1].get('rid', 'none')}: {simulcast_outbound_rtp_stats[1]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[1]['targetBitrate'] / 1000} kbps)" - ) - print( - f" rid={simulcast_outbound_rtp_stats[2].get('rid', 'none')}: {simulcast_outbound_rtp_stats[2]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[2]['targetBitrate'] / 1000} kbps)" + # r0 のみが targetBitrate を持つことを確認 + assert active_rids == ["r0"], ( + f"帯域制限後は r0 のみが targetBitrate を持つはず: {active_rids}" ) - # 全てのストリームの targetBitrate の合計を確認 - total_target_bitrate = ( - simulcast_outbound_rtp_stats[0]["targetBitrate"] - + simulcast_outbound_rtp_stats[1]["targetBitrate"] - + simulcast_outbound_rtp_stats[2]["targetBitrate"] - ) - print( - f"\n確認: 合計 targetBitrate = {total_target_bitrate} bps ({total_target_bitrate / 1000} kbps)" - ) + # r1 と r2 が存在する場合、targetBitrate が存在しないことを確認 + for stat in simulcast_outbound_rtp_stats: + rid = stat.get("rid", "") + if rid in ["r1", "r2"]: + assert "targetBitrate" not in stat, ( + f"{rid} に targetBitrate が存在しています (停止しているはず)" + ) + print(f"確認: {rid} は targetBitrate なし (停止中)") + + # r0 の targetBitrate が帯域制限以下であることを確認 + r0_stat = simulcast_outbound_rtp_stats[0] + assert "targetBitrate" in r0_stat, "r0 に targetBitrate が存在しません" + r0_bitrate = r0_stat["targetBitrate"] + print(f"\n確認: r0 targetBitrate = {r0_bitrate} bps ({r0_bitrate / 1000} kbps)") print(f"期待値: {BANDWIDTH_LIMIT_KBPS} kbps 以下") - # 帯域制限が効いているか確認(多少のオーバーヘッドを考慮) - assert total_target_bitrate <= BANDWIDTH_LIMIT_KBPS * 1000 * 1.2, ( - f"合計 targetBitrate が帯域制限を超えています: {total_target_bitrate} bps > {BANDWIDTH_LIMIT_KBPS * 1000} bps" + # 多少のオーバーヘッドを考慮 + assert r0_bitrate <= BANDWIDTH_LIMIT_KBPS * 1000 * 1.2, ( + f"r0 targetBitrate が帯域制限を超えています: {r0_bitrate} bps > {BANDWIDTH_LIMIT_KBPS * 1000} bps" ) print("\n帯域制限が有効な状態でテスト完了") From 333943fc95e2f2dc9185082d254b7436c068f645 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 3 Nov 2025 23:48:28 +0900 Subject: [PATCH 30/37] =?UTF-8?q?simulcast=20=E3=81=AE=E5=B8=AF=E5=9F=9F?= =?UTF-8?q?=E5=88=B6=E9=99=90=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E6=94=B9?= =?UTF-8?q?=E5=96=84=E3=81=97=E3=80=81targetBitrate=20=E3=81=AE=E7=A2=BA?= =?UTF-8?q?=E8=AA=8D=E3=82=92=E5=BC=B7=E5=8C=96=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 126 +++++++++++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 53 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 3ec621a8..607306bf 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -26,6 +26,7 @@ INITIAL_BITRATE_KBPS = 1200 # 初期ビットレート (kbps) BANDWIDTH_LIMIT_KBPS = 250 # 帯域制限値 (kbps) MIN_BITRATE_BEFORE_LIMIT_KBPS = 500 # 制限前の最小ビットレート (kbps) +BANDWIDTH_OVERHEAD_FACTOR = 1.2 # 帯域制限の許容オーバーヘッド (20%) def get_default_interface() -> str: @@ -341,32 +342,46 @@ def test_tc_egress_bandwidth_limit(settings): for stat in stats if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" ] - assert len(simulcast_outbound_rtp_stats) > 0, "outbound-rtp (video) が取得できませんでした" - # rid でソート (r0, r1, r2 の順) simulcast_outbound_rtp_stats.sort(key=lambda x: x.get("rid", "")) - # simulcast が有効な場合は 3 つのストリーム (r0, r1, r2) が存在するはず - assert len(simulcast_outbound_rtp_stats) == 3, ( - f"simulcast のストリーム数が不正: {len(simulcast_outbound_rtp_stats)} (期待値: 3)" - ) + # simulcast では r0/r1/r2 の 3 つのストリームが必ず存在する + assert len(simulcast_outbound_rtp_stats) == 3 + + # r0 の確認 + outbound_rtp_r0 = simulcast_outbound_rtp_stats[0] + assert "rid" in outbound_rtp_r0 + assert outbound_rtp_r0["rid"] == "r0" + assert "targetBitrate" in outbound_rtp_r0 + + # r1 の確認 + outbound_rtp_r1 = simulcast_outbound_rtp_stats[1] + assert "rid" in outbound_rtp_r1 + assert outbound_rtp_r1["rid"] == "r1" + assert "targetBitrate" in outbound_rtp_r1 + + # r2 の確認 + outbound_rtp_r2 = simulcast_outbound_rtp_stats[2] + assert "rid" in outbound_rtp_r2 + assert outbound_rtp_r2["rid"] == "r2" + assert "targetBitrate" in outbound_rtp_r2 print("\n制限前の targetBitrate:") print( - f" rid={simulcast_outbound_rtp_stats[0].get('rid', 'none')}: {simulcast_outbound_rtp_stats[0]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[0]['targetBitrate'] / 1000} kbps)" + f" rid={outbound_rtp_r0['rid']}: {outbound_rtp_r0['targetBitrate']} bps " + f"({outbound_rtp_r0['targetBitrate'] / 1000} kbps)" ) print( - f" rid={simulcast_outbound_rtp_stats[1].get('rid', 'none')}: {simulcast_outbound_rtp_stats[1]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[1]['targetBitrate'] / 1000} kbps)" + f" rid={outbound_rtp_r1['rid']}: {outbound_rtp_r1['targetBitrate']} bps " + f"({outbound_rtp_r1['targetBitrate'] / 1000} kbps)" ) print( - f" rid={simulcast_outbound_rtp_stats[2].get('rid', 'none')}: {simulcast_outbound_rtp_stats[2]['targetBitrate']} bps ({simulcast_outbound_rtp_stats[2]['targetBitrate'] / 1000} kbps)" + f" rid={outbound_rtp_r2['rid']}: {outbound_rtp_r2['targetBitrate']} bps " + f"({outbound_rtp_r2['targetBitrate'] / 1000} kbps)" ) # r2 (最高画質) のビットレートが MIN_BITRATE_BEFORE_LIMIT_KBPS 以上あることを確認 - r2_bitrate = simulcast_outbound_rtp_stats[2]["targetBitrate"] - assert r2_bitrate >= MIN_BITRATE_BEFORE_LIMIT_KBPS * 1000, ( - f"制限前の r2 targetBitrate が想定より低い: {r2_bitrate} bps < {MIN_BITRATE_BEFORE_LIMIT_KBPS * 1000} bps" - ) + assert outbound_rtp_r2["targetBitrate"] >= MIN_BITRATE_BEFORE_LIMIT_KBPS * 1000 # tc egress で帯域制限を設定 with TCEgressManager(interface=interface) as tc: @@ -405,58 +420,63 @@ def test_tc_egress_bandwidth_limit(settings): for stat in stats if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" ] - assert len(simulcast_outbound_rtp_stats) > 0, ( - "outbound-rtp (video) が取得できませんでした" - ) - # rid でソート (r0, r1, r2 の順) simulcast_outbound_rtp_stats.sort(key=lambda x: x.get("rid", "")) + # simulcast では r0/r1/r2 の 3 つのストリームが必ず存在する + assert len(simulcast_outbound_rtp_stats) == 3 + + # r0/r1/r2 の確認 + outbound_rtp_r0 = simulcast_outbound_rtp_stats[0] + assert "rid" in outbound_rtp_r0 + assert outbound_rtp_r0["rid"] == "r0" + + outbound_rtp_r1 = simulcast_outbound_rtp_stats[1] + assert "rid" in outbound_rtp_r1 + assert outbound_rtp_r1["rid"] == "r1" + + outbound_rtp_r2 = simulcast_outbound_rtp_stats[2] + assert "rid" in outbound_rtp_r2 + assert outbound_rtp_r2["rid"] == "r2" + print("\n制限後の outbound-rtp 統計情報:") for stat in simulcast_outbound_rtp_stats: rid = stat.get("rid", "none") - print(f"{rid} | outbound-rtp: {stats}") bitrate = stat.get("targetBitrate") + quality_limitation = stat.get("qualityLimitationReason", "none") if bitrate is not None: - print(f" rid={rid}: targetBitrate={bitrate} bps ({bitrate / 1000} kbps)") + print( + f" rid={rid}: targetBitrate={bitrate} bps ({bitrate / 1000} kbps), " + f"qualityLimitationReason={quality_limitation}" + ) else: - print(f" rid={rid}: targetBitrate なし (停止中)") - - # 帯域制限により r1/r2 の targetBitrate 項目が存在しなくなる - # r0 のみが targetBitrate を持つことを確認 - all_rids = [stat.get("rid") for stat in simulcast_outbound_rtp_stats] - active_rids = [ - stat.get("rid") for stat in simulcast_outbound_rtp_stats if "targetBitrate" in stat - ] - print(f"\n確認: 存在する rid = {all_rids}") - print(f"確認: targetBitrate を持つ rid = {active_rids}") - print("期待値: targetBitrate を持つのは r0 のみ") + print( + f" rid={rid}: targetBitrate なし (停止中), " + f"qualityLimitationReason={quality_limitation}" + ) - # r0 のみが targetBitrate を持つことを確認 - assert active_rids == ["r0"], ( - f"帯域制限後は r0 のみが targetBitrate を持つはず: {active_rids}" + # r0 の確認: targetBitrate が存在し、帯域制限以下であること + assert "targetBitrate" in outbound_rtp_r0 + r0_bitrate = outbound_rtp_r0["targetBitrate"] + assert r0_bitrate <= BANDWIDTH_LIMIT_KBPS * 1000 * BANDWIDTH_OVERHEAD_FACTOR + assert "qualityLimitationReason" in outbound_rtp_r0 + assert outbound_rtp_r0["qualityLimitationReason"] == "none" + print( + f"\n確認: r0 targetBitrate = {r0_bitrate} bps ({r0_bitrate / 1000} kbps), " + f"qualityLimitationReason={outbound_rtp_r0['qualityLimitationReason']}" ) - # r1 と r2 が存在する場合、targetBitrate が存在しないことを確認 - for stat in simulcast_outbound_rtp_stats: - rid = stat.get("rid", "") - if rid in ["r1", "r2"]: - assert "targetBitrate" not in stat, ( - f"{rid} に targetBitrate が存在しています (停止しているはず)" - ) - print(f"確認: {rid} は targetBitrate なし (停止中)") - - # r0 の targetBitrate が帯域制限以下であることを確認 - r0_stat = simulcast_outbound_rtp_stats[0] - assert "targetBitrate" in r0_stat, "r0 に targetBitrate が存在しません" - r0_bitrate = r0_stat["targetBitrate"] - print(f"\n確認: r0 targetBitrate = {r0_bitrate} bps ({r0_bitrate / 1000} kbps)") - print(f"期待値: {BANDWIDTH_LIMIT_KBPS} kbps 以下") - - # 多少のオーバーヘッドを考慮 - assert r0_bitrate <= BANDWIDTH_LIMIT_KBPS * 1000 * 1.2, ( - f"r0 targetBitrate が帯域制限を超えています: {r0_bitrate} bps > {BANDWIDTH_LIMIT_KBPS * 1000} bps" - ) + # r1 の確認: targetBitrate が存在せず、qualityLimitationReason が bandwidth であること + assert "targetBitrate" not in outbound_rtp_r1 + assert "qualityLimitationReason" in outbound_rtp_r1 + assert outbound_rtp_r1["qualityLimitationReason"] == "bandwidth" + print(f"確認: r1 は targetBitrate なし, qualityLimitationReason={outbound_rtp_r1['qualityLimitationReason']}") + + # r2 の確認: targetBitrate が存在せず、qualityLimitationReason が bandwidth であること + assert "targetBitrate" not in outbound_rtp_r2 + assert "qualityLimitationReason" in outbound_rtp_r2 + assert outbound_rtp_r2["qualityLimitationReason"] == "bandwidth" + print(f"確認: r2 は targetBitrate なし, qualityLimitationReason={outbound_rtp_r2['qualityLimitationReason']}") print("\n帯域制限が有効な状態でテスト完了") From ec101ce7d5b2b0631228caad1c6db6d69f6db0eb Mon Sep 17 00:00:00 2001 From: voluntas Date: Tue, 4 Nov 2025 00:01:40 +0900 Subject: [PATCH 31/37] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE?= =?UTF-8?q?=E5=87=BA=E5=8A=9B=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=82=92=E8=8B=B1=E8=AA=9E=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=97?= =?UTF-8?q?=E3=80=81=E5=B8=AF=E5=9F=9F=E5=88=B6=E9=99=90=E3=81=AE=E9=81=A9?= =?UTF-8?q?=E7=94=A8=E7=8A=B6=E6=B3=81=E3=82=92=E6=98=8E=E7=A2=BA=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 607306bf..f9f71d05 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -366,7 +366,7 @@ def test_tc_egress_bandwidth_limit(settings): assert outbound_rtp_r2["rid"] == "r2" assert "targetBitrate" in outbound_rtp_r2 - print("\n制限前の targetBitrate:") + print("\nBefore bandwidth limit - targetBitrate:") print( f" rid={outbound_rtp_r0['rid']}: {outbound_rtp_r0['targetBitrate']} bps " f"({outbound_rtp_r0['targetBitrate'] / 1000} kbps)" @@ -439,7 +439,7 @@ def test_tc_egress_bandwidth_limit(settings): assert "rid" in outbound_rtp_r2 assert outbound_rtp_r2["rid"] == "r2" - print("\n制限後の outbound-rtp 統計情報:") + print("\nAfter bandwidth limit - outbound-rtp stats:") for stat in simulcast_outbound_rtp_stats: rid = stat.get("rid", "none") bitrate = stat.get("targetBitrate") @@ -451,7 +451,7 @@ def test_tc_egress_bandwidth_limit(settings): ) else: print( - f" rid={rid}: targetBitrate なし (停止中), " + f" rid={rid}: targetBitrate=none (paused), " f"qualityLimitationReason={quality_limitation}" ) @@ -460,9 +460,9 @@ def test_tc_egress_bandwidth_limit(settings): r0_bitrate = outbound_rtp_r0["targetBitrate"] assert r0_bitrate <= BANDWIDTH_LIMIT_KBPS * 1000 * BANDWIDTH_OVERHEAD_FACTOR assert "qualityLimitationReason" in outbound_rtp_r0 - assert outbound_rtp_r0["qualityLimitationReason"] == "none" + assert outbound_rtp_r1["qualityLimitationReason"] == "bandwidth" print( - f"\n確認: r0 targetBitrate = {r0_bitrate} bps ({r0_bitrate / 1000} kbps), " + f"\nVerify r0: targetBitrate={r0_bitrate} bps ({r0_bitrate / 1000} kbps), " f"qualityLimitationReason={outbound_rtp_r0['qualityLimitationReason']}" ) @@ -470,20 +470,24 @@ def test_tc_egress_bandwidth_limit(settings): assert "targetBitrate" not in outbound_rtp_r1 assert "qualityLimitationReason" in outbound_rtp_r1 assert outbound_rtp_r1["qualityLimitationReason"] == "bandwidth" - print(f"確認: r1 は targetBitrate なし, qualityLimitationReason={outbound_rtp_r1['qualityLimitationReason']}") + print( + f"Verify r1: targetBitrate=none (paused), qualityLimitationReason={outbound_rtp_r1['qualityLimitationReason']}" + ) # r2 の確認: targetBitrate が存在せず、qualityLimitationReason が bandwidth であること assert "targetBitrate" not in outbound_rtp_r2 assert "qualityLimitationReason" in outbound_rtp_r2 assert outbound_rtp_r2["qualityLimitationReason"] == "bandwidth" - print(f"確認: r2 は targetBitrate なし, qualityLimitationReason={outbound_rtp_r2['qualityLimitationReason']}") + print( + f"Verify r2: targetBitrate=none (paused), qualityLimitationReason={outbound_rtp_r2['qualityLimitationReason']}" + ) - print("\n帯域制限が有効な状態でテスト完了") + print("\nTest completed with bandwidth limit applied") # クリーンアップ確認 - print("\nクリーンアップ後の tc 設定:") + print("\nAfter cleanup - tc settings:") show_tc_stats(interface) - print("\n結果:") - print(" ✓ テスト成功 (tc egress 帯域制限が適用された)") + print("\nResult:") + print(" ✓ Test passed: tc egress bandwidth limit applied successfully") print("=" * 60 + "\n") From 28e2e563937fe97bdbc0f988acd9c99dfb1cafb5 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 10 Nov 2025 20:14:56 +0900 Subject: [PATCH 32/37] =?UTF-8?q?tc=20=E7=B5=B1=E8=A8=88=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=82=92=E6=94=B9=E5=96=84=E3=81=97=E3=80=81?= =?UTF-8?q?tbf=20=E3=81=BE=E3=81=9F=E3=81=AF=20netem=20qdisc=20=E3=81=AE?= =?UTF-8?q?=E3=81=BF=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4=E3=80=82=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=81=8C=E3=81=AA=E3=81=84=E5=A0=B4=E5=90=88=E3=81=AE=E3=83=A1?= =?UTF-8?q?=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=82=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tc.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index f9f71d05..8edd108f 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -248,10 +248,16 @@ def show_tc_stats(interface: str) -> None: return idx = indices[0] - # qdisc の情報を取得して表示 + # qdisc の情報を取得して表示 (tbf/netem のみ) print(f"\ntc 統計情報 ({interface}):") + found = False for qdisc in ipr.get_qdiscs(idx): kind = qdisc.get_attr("TCA_KIND") + # tbf または netem qdisc のみを表示 + if kind not in ("tbf", "netem"): + continue + + found = True handle = qdisc.get("handle", 0) parent = qdisc.get("parent", 0) @@ -264,6 +270,9 @@ def show_tc_stats(interface: str) -> None: print(f" qdisc {kind} handle {handle:#x} parent {parent:#x}") print(f" Sent {sent_bytes} bytes {sent_packets} packets") print(f" drops {drops}, overlimits {overlimits}") + + if not found: + print(" (tc 設定なし)") except Exception as e: print(f"tc 統計情報の取得に失敗: {e}") From 8bc2210ae5b4e56b35fc5d36e1a23e74e36515e7 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 10 Nov 2025 20:22:07 +0900 Subject: [PATCH 33/37] =?UTF-8?q?tc=20=E7=B5=B1=E8=A8=88=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E3=81=AE=E5=8F=96=E5=BE=97=E6=96=B9=E6=B3=95=E3=82=92=20TCA=5F?= =?UTF-8?q?STATS2=20=E5=B1=9E=E6=80=A7=E3=81=8B=E3=82=89=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TCA_STATS2 から TCA_STATS_BASIC で bytes/packets を取得 - TCA_STATS2 から TCA_STATS_QUEUE で drops/overlimits/requeues を取得 - show_tc_stats と TCEgressManager.get_stats を統一的に修正 - tbf/netem qdisc のみをフィルタリングするように改善 --- tests/test_tc.py | 82 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 8edd108f..2fbaa078 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -166,18 +166,46 @@ def get_stats(self) -> dict: raise IndexError(f"インターフェース '{self.interface}' が見つかりません") idx = indices[0] - # tc qdisc の情報を取得する + # tc qdisc の情報を取得する (tbf または netem qdisc のみ) for qdisc in self.ipr.get_qdiscs(idx): - # netem qdisc の統計情報を抽出する - if qdisc.get_attr("TCA_OPTIONS"): - stats = { - "sent_bytes": qdisc.get("bytes", 0), - "sent_packets": qdisc.get("packets", 0), - "drops": qdisc.get("drops", 0), - "overlimits": qdisc.get("overlimits", 0), - "requeues": qdisc.get("requeues", 0), - } - return stats + kind = qdisc.get_attr("TCA_KIND") + if kind not in ("tbf", "netem"): + continue + + # 統計情報を取得 + # TCA_STATS2 属性から統計情報を取得 + stats2 = qdisc.get_attr("TCA_STATS2") + if stats2: + # TCA_STATS_BASIC から bytes と packets を取得 + stats_basic = stats2.get_attr("TCA_STATS_BASIC") + sent_bytes = stats_basic.get("bytes", 0) if stats_basic else 0 + sent_packets = stats_basic.get("packets", 0) if stats_basic else 0 + + # TCA_STATS_QUEUE から drops, overlimits, requeues を取得 + stats_queue = stats2.get_attr("TCA_STATS_QUEUE") + if stats_queue: + drops = stats_queue.get("drops", 0) + overlimits = stats_queue.get("overlimits", 0) + requeues = stats_queue.get("requeues", 0) + else: + drops = 0 + overlimits = 0 + requeues = 0 + else: + # フォールバック: qdisc オブジェクトから直接取得 + sent_bytes = qdisc.get("bytes", 0) + sent_packets = qdisc.get("packets", 0) + drops = qdisc.get("drops", 0) + overlimits = qdisc.get("overlimits", 0) + requeues = qdisc.get("requeues", 0) + + return { + "sent_bytes": sent_bytes, + "sent_packets": sent_packets, + "drops": drops, + "overlimits": overlimits, + "requeues": requeues, + } return {} @@ -261,11 +289,33 @@ def show_tc_stats(interface: str) -> None: handle = qdisc.get("handle", 0) parent = qdisc.get("parent", 0) - # 統計情報 - sent_bytes = qdisc.get("bytes", 0) - sent_packets = qdisc.get("packets", 0) - drops = qdisc.get("drops", 0) - overlimits = qdisc.get("overlimits", 0) + # 統計情報を取得 + # TCA_STATS2 属性から統計情報を取得 + stats2 = qdisc.get_attr("TCA_STATS2") + if stats2: + # TCA_STATS_BASIC から bytes と packets を取得 + stats_basic = stats2.get_attr("TCA_STATS_BASIC") + if stats_basic: + sent_bytes = stats_basic.get("bytes", 0) + sent_packets = stats_basic.get("packets", 0) + else: + sent_bytes = 0 + sent_packets = 0 + + # TCA_STATS_QUEUE から drops と overlimits を取得 + stats_queue = stats2.get_attr("TCA_STATS_QUEUE") + if stats_queue: + drops = stats_queue.get("drops", 0) + overlimits = stats_queue.get("overlimits", 0) + else: + drops = 0 + overlimits = 0 + else: + # フォールバック: qdisc オブジェクトから直接取得 + sent_bytes = qdisc.get("bytes", 0) + sent_packets = qdisc.get("packets", 0) + drops = qdisc.get("drops", 0) + overlimits = qdisc.get("overlimits", 0) print(f" qdisc {kind} handle {handle:#x} parent {parent:#x}") print(f" Sent {sent_bytes} bytes {sent_packets} packets") From 318e8fa460a22d4006c481f0b4dd4f58b5fab72c Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 10 Nov 2025 21:03:02 +0900 Subject: [PATCH 34/37] =?UTF-8?q?tc=20=E3=83=86=E3=82=B9=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=AE=E5=93=81=E8=B3=AA=E3=82=92=E6=94=B9?= =?UTF-8?q?=E5=96=84=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - アサーションエラーを修正 (r0 検証時に誤って r1 を確認していた) - 変数名を明確化 (stats を webrtc_stats, tc_stats, webrtc_stats_after に分離) - 重複コードを関数化 (get_simulcast_outbound_rtp_stats 関数を追加) --- tests/test_tc.py | 54 +++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 2fbaa078..2a904507 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -327,15 +327,33 @@ def show_tc_stats(interface: str) -> None: print(f"tc 統計情報の取得に失敗: {e}") -def show_webrtc_stats(stats: list) -> None: +def get_simulcast_outbound_rtp_stats(webrtc_stats: list) -> list: + """simulcast の outbound-rtp 統計情報を取得してソートする。 + + Args: + webrtc_stats: get_stats() で取得した統計情報のリスト + + Returns: + rid でソートされた outbound-rtp 統計情報のリスト + """ + simulcast_stats = [ + stat + for stat in webrtc_stats + if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" + ] + simulcast_stats.sort(key=lambda x: x.get("rid", "")) + return simulcast_stats + + +def show_webrtc_stats(webrtc_stats: list) -> None: """WebRTC の統計情報を表示する。 Args: - stats: get_stats() で取得した統計情報のリスト + webrtc_stats: get_stats() で取得した統計情報のリスト """ try: print("\nWebRTC 統計情報:") - for stat in stats: + for stat in webrtc_stats: if stat.get("type") == "outbound-rtp": rid = stat.get("rid", "") rid_label = f" (rid={rid})" if rid else "" @@ -392,17 +410,11 @@ def test_tc_egress_bandwidth_limit(settings): # 制限前の WebRTC 統計情報を確認 print("\n制限前の WebRTC 統計情報:") time.sleep(3) - stats = sendonly.get_stats() - show_webrtc_stats(stats) + webrtc_stats = sendonly.get_stats() + show_webrtc_stats(webrtc_stats) # 制限前の targetBitrate を確認 (video の全ての outbound-rtp を取得) - simulcast_outbound_rtp_stats = [ - stat - for stat in stats - if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" - ] - # rid でソート (r0, r1, r2 の順) - simulcast_outbound_rtp_stats.sort(key=lambda x: x.get("rid", "")) + simulcast_outbound_rtp_stats = get_simulcast_outbound_rtp_stats(webrtc_stats) # simulcast では r0/r1/r2 の 3 つのストリームが必ず存在する assert len(simulcast_outbound_rtp_stats) == 3 @@ -463,24 +475,18 @@ def test_tc_egress_bandwidth_limit(settings): show_tc_stats(interface) # 統計情報を取得 - stats = tc.get_stats() + tc_stats = tc.get_stats() print("\ntc 統計情報 (IPRoute):") - for key, value in stats.items(): + for key, value in tc_stats.items(): print(f" {key}: {value}") # WebRTC 統計情報を表示 print("\nステップ 4: 制限後の WebRTC 統計情報を確認") - stats = sendonly.get_stats() - show_webrtc_stats(stats) + webrtc_stats_after = sendonly.get_stats() + show_webrtc_stats(webrtc_stats_after) # targetBitrate を確認 (video の全ての outbound-rtp を取得) - simulcast_outbound_rtp_stats = [ - stat - for stat in stats - if stat.get("type") == "outbound-rtp" and stat.get("kind") == "video" - ] - # rid でソート (r0, r1, r2 の順) - simulcast_outbound_rtp_stats.sort(key=lambda x: x.get("rid", "")) + simulcast_outbound_rtp_stats = get_simulcast_outbound_rtp_stats(webrtc_stats_after) # simulcast では r0/r1/r2 の 3 つのストリームが必ず存在する assert len(simulcast_outbound_rtp_stats) == 3 @@ -519,7 +525,7 @@ def test_tc_egress_bandwidth_limit(settings): r0_bitrate = outbound_rtp_r0["targetBitrate"] assert r0_bitrate <= BANDWIDTH_LIMIT_KBPS * 1000 * BANDWIDTH_OVERHEAD_FACTOR assert "qualityLimitationReason" in outbound_rtp_r0 - assert outbound_rtp_r1["qualityLimitationReason"] == "bandwidth" + assert outbound_rtp_r0["qualityLimitationReason"] == "bandwidth" print( f"\nVerify r0: targetBitrate={r0_bitrate} bps ({r0_bitrate / 1000} kbps), " f"qualityLimitationReason={outbound_rtp_r0['qualityLimitationReason']}" From 04ef88d6ec66762e280bec9e6c24009e8221fdce Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 10 Nov 2025 21:07:34 +0900 Subject: [PATCH 35/37] =?UTF-8?q?tc=20=E7=B5=B1=E8=A8=88=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E3=81=AE=E3=83=95=E3=82=A9=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=AF=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TCA_STATS2 が存在しない場合は continue で次の qdisc をチェック - qdisc オブジェクトから直接統計情報を取得するフォールバックを削除 - Linux 2.6.x 以降では TCA_STATS2 が標準で提供されるため不要 - コードの複雑性を削減 --- tests/test_tc.py | 81 +++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 46 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index 2a904507..dde499a5 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -172,32 +172,26 @@ def get_stats(self) -> dict: if kind not in ("tbf", "netem"): continue - # 統計情報を取得 # TCA_STATS2 属性から統計情報を取得 stats2 = qdisc.get_attr("TCA_STATS2") - if stats2: - # TCA_STATS_BASIC から bytes と packets を取得 - stats_basic = stats2.get_attr("TCA_STATS_BASIC") - sent_bytes = stats_basic.get("bytes", 0) if stats_basic else 0 - sent_packets = stats_basic.get("packets", 0) if stats_basic else 0 + if not stats2: + continue - # TCA_STATS_QUEUE から drops, overlimits, requeues を取得 - stats_queue = stats2.get_attr("TCA_STATS_QUEUE") - if stats_queue: - drops = stats_queue.get("drops", 0) - overlimits = stats_queue.get("overlimits", 0) - requeues = stats_queue.get("requeues", 0) - else: - drops = 0 - overlimits = 0 - requeues = 0 + # TCA_STATS_BASIC から bytes と packets を取得 + stats_basic = stats2.get_attr("TCA_STATS_BASIC") + sent_bytes = stats_basic.get("bytes", 0) if stats_basic else 0 + sent_packets = stats_basic.get("packets", 0) if stats_basic else 0 + + # TCA_STATS_QUEUE から drops, overlimits, requeues を取得 + stats_queue = stats2.get_attr("TCA_STATS_QUEUE") + if stats_queue: + drops = stats_queue.get("drops", 0) + overlimits = stats_queue.get("overlimits", 0) + requeues = stats_queue.get("requeues", 0) else: - # フォールバック: qdisc オブジェクトから直接取得 - sent_bytes = qdisc.get("bytes", 0) - sent_packets = qdisc.get("packets", 0) - drops = qdisc.get("drops", 0) - overlimits = qdisc.get("overlimits", 0) - requeues = qdisc.get("requeues", 0) + drops = 0 + overlimits = 0 + requeues = 0 return { "sent_bytes": sent_bytes, @@ -289,33 +283,28 @@ def show_tc_stats(interface: str) -> None: handle = qdisc.get("handle", 0) parent = qdisc.get("parent", 0) - # 統計情報を取得 # TCA_STATS2 属性から統計情報を取得 stats2 = qdisc.get_attr("TCA_STATS2") - if stats2: - # TCA_STATS_BASIC から bytes と packets を取得 - stats_basic = stats2.get_attr("TCA_STATS_BASIC") - if stats_basic: - sent_bytes = stats_basic.get("bytes", 0) - sent_packets = stats_basic.get("packets", 0) - else: - sent_bytes = 0 - sent_packets = 0 - - # TCA_STATS_QUEUE から drops と overlimits を取得 - stats_queue = stats2.get_attr("TCA_STATS_QUEUE") - if stats_queue: - drops = stats_queue.get("drops", 0) - overlimits = stats_queue.get("overlimits", 0) - else: - drops = 0 - overlimits = 0 + if not stats2: + continue + + # TCA_STATS_BASIC から bytes と packets を取得 + stats_basic = stats2.get_attr("TCA_STATS_BASIC") + if stats_basic: + sent_bytes = stats_basic.get("bytes", 0) + sent_packets = stats_basic.get("packets", 0) else: - # フォールバック: qdisc オブジェクトから直接取得 - sent_bytes = qdisc.get("bytes", 0) - sent_packets = qdisc.get("packets", 0) - drops = qdisc.get("drops", 0) - overlimits = qdisc.get("overlimits", 0) + sent_bytes = 0 + sent_packets = 0 + + # TCA_STATS_QUEUE から drops と overlimits を取得 + stats_queue = stats2.get_attr("TCA_STATS_QUEUE") + if stats_queue: + drops = stats_queue.get("drops", 0) + overlimits = stats_queue.get("overlimits", 0) + else: + drops = 0 + overlimits = 0 print(f" qdisc {kind} handle {handle:#x} parent {parent:#x}") print(f" Sent {sent_bytes} bytes {sent_packets} packets") From 20585c1b29100ef52709ac911fa10e9d136960f1 Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 10 Nov 2025 21:10:39 +0900 Subject: [PATCH 36/37] =?UTF-8?q?WebRTC=20=E7=B5=B1=E8=A8=88=E6=83=85?= =?UTF-8?q?=E5=A0=B1=E3=82=92=20rid=20=E3=81=A7=E3=82=BD=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=97=E3=81=A6=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E6=94=B9=E5=96=84=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - show_webrtc_stats 関数内で outbound-rtp を抽出してソート - r0, r1, r2 の順序で一貫した表示を保証 --- tests/test_tc.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index dde499a5..d224b5f4 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -342,21 +342,25 @@ def show_webrtc_stats(webrtc_stats: list) -> None: """ try: print("\nWebRTC 統計情報:") - for stat in webrtc_stats: - if stat.get("type") == "outbound-rtp": - rid = stat.get("rid", "") - rid_label = f" (rid={rid})" if rid else "" - print(f" outbound-rtp{rid_label}:") - print(f" ssrc: {stat.get('ssrc')}") - print(f" kind: {stat.get('kind')}") - if rid: - print(f" rid: {rid}") - print(f" bytesSent: {stat.get('bytesSent')}") - print(f" packetsSent: {stat.get('packetsSent')}") - if "targetBitrate" in stat: - print(f" targetBitrate: {stat.get('targetBitrate')} bps") - if "totalPacketSendDelay" in stat: - print(f" totalPacketSendDelay: {stat.get('totalPacketSendDelay')} s") + outbound_rtp_stats = [ + stat for stat in webrtc_stats if stat.get("type") == "outbound-rtp" + ] + outbound_rtp_stats.sort(key=lambda x: x.get("rid", "")) + + for stat in outbound_rtp_stats: + rid = stat.get("rid", "") + rid_label = f" (rid={rid})" if rid else "" + print(f" outbound-rtp{rid_label}:") + print(f" ssrc: {stat.get('ssrc')}") + print(f" kind: {stat.get('kind')}") + if rid: + print(f" rid: {rid}") + print(f" bytesSent: {stat.get('bytesSent')}") + print(f" packetsSent: {stat.get('packetsSent')}") + if "targetBitrate" in stat: + print(f" targetBitrate: {stat.get('targetBitrate')} bps") + if "totalPacketSendDelay" in stat: + print(f" totalPacketSendDelay: {stat.get('totalPacketSendDelay')} s") except Exception as e: print(f"WebRTC 統計情報の表示に失敗: {e}") From 4221b2f168d593affc0b9ca511cc34524e71737c Mon Sep 17 00:00:00 2001 From: voluntas Date: Mon, 10 Nov 2025 21:25:43 +0900 Subject: [PATCH 37/37] =?UTF-8?q?tc=20=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA?= =?UTF-8?q?=E3=83=B3=E3=82=B0=E3=82=92=E6=94=B9=E5=96=84=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_default_interface でフォールバック処理を削除し例外を raise するように変更 - ルート権限チェックを pytestmark に統合 - スキップ条件を一元化し reason メッセージを明確化 --- tests/test_tc.py | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/tests/test_tc.py b/tests/test_tc.py index d224b5f4..7735080a 100644 --- a/tests/test_tc.py +++ b/tests/test_tc.py @@ -13,15 +13,15 @@ import pytest from client import SoraClient, SoraRole -# TC=1 環境変数が設定されており、かつ Linux 環境の場合のみテストを実行 -pytestmark = pytest.mark.skipif( - os.getenv("TC") != "1" or sys.platform != "linux", - reason="TC=1 環境変数と Linux 環境が必要", -) - # pyroute2 がインストールされていない場合はスキップ pyroute2 = pytest.importorskip("pyroute2") +# TC=1 環境変数、Linux 環境、ルート権限のいずれかが満たされない場合はスキップ +pytestmark = pytest.mark.skipif( + os.getenv("TC") != "1" or sys.platform != "linux" or os.geteuid() != 0, + reason="TC=1 環境変数、Linux 環境、ルート権限が必要", +) + # テスト用の設定値 INITIAL_BITRATE_KBPS = 1200 # 初期ビットレート (kbps) BANDWIDTH_LIMIT_KBPS = 250 # 帯域制限値 (kbps) @@ -34,25 +34,24 @@ def get_default_interface() -> str: Returns: デフォルトルートで使用されているインターフェース名 - """ - try: - with pyroute2.IPRoute() as ipr: - # デフォルトルートを取得(IPv4) - for route in ipr.get_routes(family=2): # AF_INET = 2 - # dst が存在しない場合がデフォルトルート - if not route.get_attr("RTA_DST"): - oif = route.get_attr("RTA_OIF") - if oif: - # インターフェース情報を取得 - links = ipr.get_links(oif) - if links: - ifname = links[0].get_attr("IFLA_IFNAME") - return ifname - except Exception as e: - print(f"デフォルトインターフェースの取得に失敗: {e}") - # フォールバックとして eth0 を返す - return "eth0" + Raises: + RuntimeError: デフォルトインターフェースが取得できない場合 + """ + with pyroute2.IPRoute() as ipr: + # デフォルトルートを取得(IPv4) + for route in ipr.get_routes(family=2): + # dst が存在しない場合がデフォルトルート + if not route.get_attr("RTA_DST"): + oif = route.get_attr("RTA_OIF") + if oif: + # インターフェース情報を取得 + links = ipr.get_links(oif) + if links: + ifname = links[0].get_attr("IFLA_IFNAME") + return ifname + + raise RuntimeError("デフォルトインターフェースが見つかりません") class TCEgressManager: