Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ jobs:

- name: Check ten dependencies
run: |
tman install
task install
task test

- name: Release
if: startsWith(github.ref, 'refs/tags/')
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
*.pyc
.ten
__pycache__
.pytest_cache
26 changes: 26 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
@@ -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
32 changes: 24 additions & 8 deletions http_server_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
from functools import partial
import re


class HTTPHandler(BaseHTTPRequestHandler):
Expand All @@ -18,34 +19,49 @@ 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/<cmd_name>
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
if not event_got: # timeout
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)
Expand Down
21 changes: 21 additions & 0 deletions tests/bin/start
Original file line number Diff line number Diff line change
@@ -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/ "$@"
1 change: 1 addition & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
httpx
52 changes: 52 additions & 0 deletions tests/test_invalid_path.py
Original file line number Diff line number Diff line change
@@ -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()
41 changes: 41 additions & 0 deletions tests/test_timeout.py
Original file line number Diff line number Diff line change
@@ -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()
Loading