Skip to content

Commit e927011

Browse files
committed
feat: add more test cases
1 parent 456d3fe commit e927011

File tree

15 files changed

+773
-24
lines changed

15 files changed

+773
-24
lines changed

packages/example_extensions/pcm_file_reader_python/extension.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def __init__(self, name: str) -> None:
2626
self._audio_frame_name: str = "pcm_frame"
2727
self._pcm_data: bytes = b""
2828
self._is_running: bool = False
29-
self._send_task: asyncio.Task | None = None
29+
self._send_task: asyncio.Task[None] | None = None
3030

3131
async def on_init(self, ten_env: AsyncTenEnv) -> None:
3232
ten_env.log(LogLevel.DEBUG, "on_init")
@@ -123,11 +123,11 @@ async def _send_audio_frames(self, ten_env: AsyncTenEnv) -> None:
123123
"""Send audio frames at specified interval."""
124124
# Calculate frame size: samples_per_frame * bytes_per_sample * channels
125125
# samples_per_frame = sample_rate * frame_interval_ms / 1000
126-
samples_per_frame = (
127-
self._sample_rate * self._frame_interval_ms // 1000
128-
)
126+
samples_per_frame = self._sample_rate * self._frame_interval_ms // 1000
129127
frame_size = (
130-
samples_per_frame * self._bytes_per_sample * self._number_of_channels
128+
samples_per_frame
129+
* self._bytes_per_sample
130+
* self._number_of_channels
131131
)
132132

133133
ten_env.log(
@@ -180,7 +180,9 @@ async def _send_audio_frames(self, ten_env: AsyncTenEnv) -> None:
180180
)
181181

182182
offset += current_frame_size
183-
timestamp += self._frame_interval_ms * 1000 # Convert to microseconds
183+
timestamp += (
184+
self._frame_interval_ms * 1000
185+
) # Convert to microseconds
184186

185187
# Wait for next frame interval
186188
if self._is_running and offset < len(self._pcm_data):
@@ -211,7 +213,15 @@ async def on_cmd(self, ten_env: AsyncTenEnv, cmd: Cmd) -> None:
211213
cmd_name = cmd.get_name()
212214
ten_env.log(LogLevel.DEBUG, f"on_cmd name {cmd_name}")
213215

214-
cmd_result = CmdResult.create(StatusCode.OK, cmd)
215-
cmd_result.set_property_string("detail", "ok")
216-
217-
await ten_env.return_result(cmd_result)
216+
if cmd_name == "get_status":
217+
cmd_result = CmdResult.create(StatusCode.OK, cmd)
218+
cmd_result.set_property_string(
219+
"status", "running" if self._is_running else "stopped"
220+
)
221+
await ten_env.return_result(cmd_result)
222+
else:
223+
cmd_result = CmdResult.create(StatusCode.ERROR, cmd)
224+
cmd_result.set_property_string(
225+
"detail", f"Unknown command: {cmd_name}"
226+
)
227+
await ten_env.return_result(cmd_result)

packages/example_extensions/pcm_file_reader_python/manifest.json

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,33 @@
3535
],
3636
"api": {
3737
"property": {
38-
"audio_file_path": {
39-
"type": "string"
40-
},
41-
"sample_rate": {
42-
"type": "int32"
43-
},
44-
"bytes_per_sample": {
45-
"type": "int32"
46-
},
47-
"number_of_channels": {
48-
"type": "int32"
49-
},
50-
"frame_interval_ms": {
51-
"type": "int32"
38+
"properties": {
39+
"audio_file_path": {
40+
"type": "string"
41+
},
42+
"sample_rate": {
43+
"type": "int32"
44+
},
45+
"bytes_per_sample": {
46+
"type": "int32"
47+
},
48+
"number_of_channels": {
49+
"type": "int32"
50+
},
51+
"frame_interval_ms": {
52+
"type": "int32"
53+
}
5254
}
5355
},
5456
"audio_frame_out": [
5557
{
5658
"name": "pcm_frame"
5759
}
60+
],
61+
"cmd_in": [
62+
{
63+
"name": "get_status"
64+
}
5865
]
5966
}
6067
}

tests/ten_runtime/integration/python/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ group("python") {
6868
"trigger_life_cycle_python",
6969
"two_async_exts_one_group_python",
7070
"two_async_exts_python",
71+
"two_exts_graph_test_python",
7172
"websocket_server_python",
7273
]
7374

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#
2+
# Copyright © 2025 Agora
3+
# This file is part of TEN Framework, an open source project.
4+
# Licensed under the Apache License, Version 2.0, with certain conditions.
5+
# Refer to the "LICENSE" file in the root directory for more information.
6+
#
7+
import("//build/ten_runtime/feature/test.gni")
8+
import("//build/ten_runtime/ten.gni")
9+
10+
ten_package_test_prepare_app("two_exts_graph_test_python_app") {
11+
src_app = "default_app_python"
12+
src_app_language = "python"
13+
generated_app_src_root_dir_name = "two_exts_graph_test_python_app"
14+
15+
replace_paths_after_install_app = [
16+
"two_exts_graph_test_python_app/manifest.json",
17+
"two_exts_graph_test_python_app/property.json",
18+
]
19+
20+
test_files = exec_script(
21+
"//.gnfiles/build/scripts/glob_file.py",
22+
[
23+
"--dir",
24+
rebase_path(
25+
"//tests/ten_runtime/integration/python/two_exts_graph_test_python/two_exts_graph_test_python_app/tests/**/*"),
26+
"--dir-base",
27+
rebase_path(
28+
"//tests/ten_runtime/integration/python/two_exts_graph_test_python/two_exts_graph_test_python_app/tests"),
29+
"--recursive",
30+
"--only-output-file",
31+
],
32+
"json")
33+
34+
replace_paths_after_install_all = []
35+
foreach(test_file, test_files) {
36+
test_file_rel_path = test_file.relative_path
37+
replace_paths_after_install_all +=
38+
[ "two_exts_graph_test_python_app/tests/${test_file_rel_path}" ]
39+
}
40+
41+
if (ten_enable_ten_manager) {
42+
deps = [
43+
"//core/src/ten_manager",
44+
"//packages/core_apps/default_app_python:upload_default_app_python_to_server",
45+
"//packages/core_systems/pytest_ten:upload_pytest_ten_to_server",
46+
"//packages/example_extensions/pcm_file_reader_python:upload_pcm_file_reader_python_to_server",
47+
"//packages/example_extensions/simple_echo_python:upload_simple_echo_python_to_server",
48+
"//packages/example_extensions/simple_http_server_cpp:upload_simple_http_server_cpp_to_server",
49+
]
50+
}
51+
}
52+
53+
ten_package_test_prepare_auxiliary_resources(
54+
"two_exts_graph_test_python_test_files") {
55+
resources = [
56+
"__init__.py",
57+
"test_case.py",
58+
]
59+
60+
utils_files = exec_script("//.gnfiles/build/scripts/glob_file.py",
61+
[
62+
"--dir",
63+
rebase_path("//tests/utils/**/*"),
64+
"--dir-base",
65+
rebase_path("//tests/utils"),
66+
"--recursive",
67+
"--only-output-file",
68+
],
69+
"json")
70+
71+
foreach(utils_file, utils_files) {
72+
utils_file_rel_path = utils_file.relative_path
73+
resources +=
74+
[ "//tests/utils/${utils_file_rel_path}=>utils/${utils_file_rel_path}" ]
75+
}
76+
}
77+
78+
group("two_exts_graph_test_python") {
79+
deps = [
80+
":two_exts_graph_test_python_app",
81+
":two_exts_graph_test_python_test_files",
82+
]
83+
}

tests/ten_runtime/integration/python/two_exts_graph_test_python/__init__.py

Whitespace-only changes.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
Test two_exts_graph_test_python.
3+
"""
4+
5+
import subprocess
6+
import os
7+
import sys
8+
from sys import stdout
9+
from .utils import build_config, build_pkg, fs_utils
10+
11+
12+
def test_two_exts_graph_test_python():
13+
"""Test client and app server."""
14+
base_path = os.path.dirname(os.path.abspath(__file__))
15+
root_dir = os.path.join(base_path, "../../../../../")
16+
17+
my_env = os.environ.copy()
18+
19+
# Set the required environment variables for the test.
20+
my_env["PYTHONMALLOC"] = "malloc"
21+
my_env["PYTHONDEVMODE"] = "1"
22+
23+
app_dir_name = "two_exts_graph_test_python_app"
24+
app_root_path = os.path.join(base_path, "two_exts_graph_test_python_app")
25+
app_language = "python"
26+
27+
build_config_args = build_config.parse_build_config(
28+
os.path.join(root_dir, "tgn_args.txt"),
29+
)
30+
31+
# Before starting, cleanup the old app package.
32+
fs_utils.remove_tree(app_root_path)
33+
34+
print(f'Assembling and building package "{app_dir_name}".')
35+
36+
rc = build_pkg.prepare_and_build_app(
37+
build_config_args,
38+
root_dir,
39+
base_path,
40+
app_dir_name,
41+
app_language,
42+
)
43+
if rc != 0:
44+
assert False, "Failed to build package."
45+
46+
# Step 1: Bootstrap Python dependencies (update pyproject.toml and sync)
47+
print("Bootstrapping Python dependencies...")
48+
rc = build_pkg.bootstrap_python_dependencies(
49+
app_root_path, my_env, log_level=1
50+
)
51+
if rc != 0:
52+
assert False, "Failed to bootstrap Python dependencies."
53+
54+
# Step 2: Setup AddressSanitizer if needed
55+
if sys.platform == "linux":
56+
if build_config_args.enable_sanitizer:
57+
libasan_path = os.path.join(
58+
base_path,
59+
(
60+
"two_exts_graph_test_python_app/ten_packages/system/"
61+
"ten_runtime/lib/libasan.so"
62+
),
63+
)
64+
65+
if os.path.exists(libasan_path):
66+
print("Using AddressSanitizer library.")
67+
my_env["LD_PRELOAD"] = libasan_path
68+
69+
tests_dir = os.path.join(app_root_path, "tests")
70+
71+
# Step 3:
72+
#
73+
# Run the test using uv run pytest.
74+
# When sanitizer is enabled, we need to bypass `uv run` because `uv` itself
75+
# may trigger memory leak reports (false positives from the tool itself),
76+
# causing the test to fail.
77+
if sys.platform == "linux" and build_config_args.enable_sanitizer:
78+
print("Starting pytest with python from venv (bypassing uv run)...")
79+
venv_path = os.path.join(tests_dir, ".venv")
80+
python_exe = os.path.join(venv_path, "bin", "python")
81+
uv_run_pytest_cmd = [python_exe, "-m", "pytest", "-s"]
82+
my_env["VIRTUAL_ENV"] = venv_path
83+
else:
84+
uv_run_pytest_cmd = [
85+
"uv",
86+
"run",
87+
"pytest",
88+
"-s",
89+
]
90+
91+
try:
92+
tester_process = subprocess.Popen(
93+
uv_run_pytest_cmd,
94+
stdout=stdout,
95+
stderr=subprocess.STDOUT,
96+
env=my_env,
97+
cwd=tests_dir,
98+
)
99+
100+
tester_rc = tester_process.wait()
101+
assert tester_rc == 0
102+
finally:
103+
if build_config_args.ten_enable_tests_cleanup is True:
104+
fs_utils.remove_tree(app_root_path)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"type": "app",
3+
"name": "default_app_python",
4+
"version": "0.11.51",
5+
"dependencies": [
6+
{
7+
"type": "system",
8+
"name": "ten_runtime",
9+
"version": "0.11.51"
10+
},
11+
{
12+
"type": "system",
13+
"name": "ten_runtime_python",
14+
"version": "0.11.51"
15+
},
16+
{
17+
"type": "extension",
18+
"name": "simple_http_server_cpp",
19+
"version": "0.11.51"
20+
},
21+
{
22+
"type": "extension",
23+
"name": "pcm_file_reader_python",
24+
"version": "0.11.51"
25+
},
26+
{
27+
"type": "extension",
28+
"name": "simple_echo_python",
29+
"version": "0.11.51"
30+
}
31+
],
32+
"dev_dependencies": [
33+
{
34+
"type": "system",
35+
"name": "pytest_ten",
36+
"version": "0.11.51"
37+
}
38+
]
39+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
6+
7+
export PYTHONPATH=ten_packages/system/ten_runtime_python/lib:ten_packages/system/ten_runtime_python/interface:ten_packages/system/pytest_ten:.
8+
9+
# If the Python app imports some modules that are compiled with a different
10+
# version of libstdc++ (ex: PyTorch), the Python app may encounter confusing
11+
# errors. To solve this problem, we can preload the correct version of
12+
# libstdc++.
13+
#
14+
# export LD_PRELOAD=/lib/x86_64-linux-gnu/libstdc++.so.6
15+
#
16+
# Another solution is to make sure the module 'ten_runtime_python' is imported
17+
# _after_ the module that requires another version of libstdc++ is imported.
18+
#
19+
# Refer to https://github.com/pytorch/pytorch/issues/102360?from_wecom=1#issuecomment-1708989096
20+
21+
python -m pytest -s tests/ "$@"

0 commit comments

Comments
 (0)