diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d4aeee..97d84e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,8 @@ jobs: - name: Check ten dependencies run: | - tman install + task install + task test - name: Release if: startsWith(github.ref, 'refs/tags/') diff --git a/.gitignore b/.gitignore index 27b55f5..2b86234 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.pyc .ten +__pycache__ +.pytest_cache diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..c8eaf6e --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,26 @@ +version: '3' + +tasks: + clean: + desc: clean up + cmds: + - rm -rf .ten .pytest_cache + - find . -type d -name __pycache__ -exec rm -rf {} \; || true + + test: + desc: run tests + cmds: + - ./tests/bin/start {{ .CLI_ARGS }} + + install: + desc: install dependencies + cmds: + - tman install + - pip install -r tests/requirements.txt + + lint: + desc: lint codes + env: + PYTHONPATH: "{{.USER_WORKING_DIR}}/.ten/app/ten_packages/system/ten_runtime_python/lib:{{.USER_WORKING_DIR}}/.ten/app/ten_packages/system/ten_runtime_python/interface" + cmds: + - pylint ./*.py \ No newline at end of file diff --git a/http_server_extension.py b/http_server_extension.py index 2987798..f156fb6 100644 --- a/http_server_extension.py +++ b/http_server_extension.py @@ -8,6 +8,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler import threading from functools import partial +import re class HTTPHandler(BaseHTTPRequestHandler): @@ -18,23 +19,34 @@ def __init__(self, ten: TenEnv, *args, directory=None, **kwargs): def do_POST(self): self.ten.log_debug(f"post request incoming {self.path}") - if self.path == "/cmd": + + # match path /cmd/ + match = re.match(r"^/cmd/([^/]+)$", self.path) + if match: + cmd_name = match.group(1) try: content_length = int(self.headers["Content-Length"]) input = self.rfile.read(content_length).decode("utf-8") - self.ten.log_info(f"incoming request {input}") + self.ten.log_info(f"incoming request {self.path} {input}") # processing by send_cmd cmd_result_event = threading.Event() cmd_result: CmdResult - def cmd_callback(_, result): + + def cmd_callback(_, result, ten_error): nonlocal cmd_result_event nonlocal cmd_result cmd_result = result - self.ten.log_info("cmd callback result: {}".format(cmd_result.to_json())) + self.ten.log_info( + "cmd callback result: {}".format( + cmd_result.get_property_to_json("") + ) + ) cmd_result_event.set() - self.ten.send_cmd(Cmd.create_from_json(input),cmd_callback) + cmd = Cmd.create(cmd_name) + cmd.set_property_from_json("", input) + self.ten.send_cmd(cmd, cmd_callback) event_got = cmd_result_event.wait(timeout=5) # return response @@ -42,10 +54,14 @@ def cmd_callback(_, result): self.send_response_only(504) self.end_headers() return - self.send_response(200 if cmd_result.get_status_code() == StatusCode.OK else 502) - self.send_header('Content-Type', 'application/json') + self.send_response( + 200 if cmd_result.get_status_code() == StatusCode.OK else 502 + ) + self.send_header("Content-Type", "application/json") self.end_headers() - self.wfile.write(cmd_result.to_json().encode(encoding='utf_8')) + self.wfile.write( + cmd_result.get_property_to_json("").encode(encoding="utf_8") + ) except Exception as e: self.ten.log_warn("failed to handle request, err {}".format(e)) self.send_response_only(500) diff --git a/tests/bin/start b/tests/bin/start new file mode 100755 index 0000000..ca5fbbd --- /dev/null +++ b/tests/bin/start @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +cd "$(dirname "${BASH_SOURCE[0]}")/../.." + +export PYTHONPATH=.ten/app/ten_packages/system/ten_runtime_python/lib:.ten/app/ten_packages/system/ten_runtime_python/interface + +# If the Python app imports some modules that are compiled with a different +# version of libstdc++ (ex: PyTorch), the Python app may encounter confusing +# errors. To solve this problem, we can preload the correct version of +# libstdc++. +# +# export LD_PRELOAD=/lib/x86_64-linux-gnu/libstdc++.so.6 +# +# Another solution is to make sure the module 'ten_runtime_python' is imported +# _after_ the module that requires another version of libstdc++ is imported. +# +# Refer to https://github.com/pytorch/pytorch/issues/102360?from_wecom=1#issuecomment-1708989096 + +pytest -s tests/ "$@" diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..7922838 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1 @@ +httpx \ No newline at end of file diff --git a/tests/test_invalid_path.py b/tests/test_invalid_path.py new file mode 100644 index 0000000..8219e8b --- /dev/null +++ b/tests/test_invalid_path.py @@ -0,0 +1,52 @@ +# +# Copyright © 2025 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +from pathlib import Path +from typing import Optional +from ten import ( + ExtensionTester, + TenEnvTester, + Cmd, + CmdResult, + StatusCode, + TenError, +) +import httpx + + +class ExtensionTesterInvalidPathCase1(ExtensionTester): + def on_start(self, ten_env: TenEnvTester) -> None: + ten_env.on_start_done() + + property_json = {"num": 1, "str": "111"} + r = httpx.post("http://127.0.0.1:8888/cmd", json=property_json) + print(r) + if r.status_code == httpx.codes.NOT_FOUND: + ten_env.stop_test() + + +class ExtensionTesterInvalidPathCase2(ExtensionTester): + def on_start(self, ten_env: TenEnvTester) -> None: + ten_env.on_start_done() + + property_json = {"num": 1, "str": "111"} + r = httpx.post("http://127.0.0.1:8888/cmd/aaa/123", json=property_json) + print(r) + if r.status_code == httpx.codes.NOT_FOUND: + ten_env.stop_test() + + +# TODO: disable it currently since second tester seems can not run. enable it once fixed +# def test_invalid_path(): +# tester = ExtensionTesterInvalidPathCase1() +# tester.add_addon_base_dir(str(Path(__file__).resolve().parent.parent)) +# tester.set_test_mode_single("http_server_python") +# tester.run() + +# tester2 = ExtensionTesterInvalidPathCase2() +# tester2.add_addon_base_dir(str(Path(__file__).resolve().parent.parent)) +# tester2.set_test_mode_single("http_server_python") +# tester2.run() diff --git a/tests/test_timeout.py b/tests/test_timeout.py new file mode 100644 index 0000000..b3d5d92 --- /dev/null +++ b/tests/test_timeout.py @@ -0,0 +1,41 @@ +# +# Copyright © 2025 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +from pathlib import Path +from typing import Optional +from ten import ( + ExtensionTester, + TenEnvTester, + Cmd, + CmdResult, + StatusCode, + TenError, +) +import httpx + + +class ExtensionTesterTimeout(ExtensionTester): + def on_cmd(self, ten_env: TenEnvTester, cmd: Cmd) -> None: + print(f"on_cmd name {cmd.get_name()}") + # NOTE: DON'T return result so that timeout will occur + # ten_env.return_result(CmdResult.create(StatusCode.OK), cmd) + pass + + def on_start(self, ten_env: TenEnvTester) -> None: + ten_env.on_start_done() + + property_json = {"num": 1, "str": "111"} + r = httpx.post("http://127.0.0.1:8888/cmd/abc", json=property_json, timeout=10) + print(r) + if r.status_code == httpx.codes.GATEWAY_TIMEOUT: + ten_env.stop_test() + + +def test_timeout(): + tester = ExtensionTesterTimeout() + tester.add_addon_base_dir(str(Path(__file__).resolve().parent.parent)) + tester.set_test_mode_single("http_server_python") + tester.run()