diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b806efad4..628f7369b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -49,4 +49,4 @@ jobs: - name: Test with pytest run: | source .venv/bin/activate - pytest --ignore-glob="*to_be_updated*" --ignore-glob=*pipeline_source* src tests + pytest diff --git a/README.md b/README.md index 52c6f831b..4f4e08771 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,6 @@ To keep things lightweight, only the core framework is included by default. If y pip install "pipecat-ai[option,...]" ``` -Or you can install all of them with: - -```shell -pip install "pipecat-ai[all]" -``` - Available options include: | Category | Services | Install Command Example | @@ -195,7 +189,7 @@ pip install "path_to_this_repo[option,...]" From the root directory, run: ```shell -pytest --doctest-modules --ignore-glob="*to_be_updated*" --ignore-glob=*pipeline_source* src tests +pytest ``` ## Setting up your editor diff --git a/pyproject.toml b/pyproject.toml index 38bf6d902..fff189c6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,10 @@ openrouter = [ "openai~=1.59.6" ] where = ["src"] [tool.pytest.ini_options] +addopts = "--verbose --disable-warnings" +testpaths = ["tests"] pythonpath = ["src"] +asyncio_default_fixture_loop_scope = "function" [tool.setuptools_scm] local_scheme = "no-local-version" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_aggregators.py b/tests/test_aggregators.py index dcf27ad6e..463c2ffed 100644 --- a/tests/test_aggregators.py +++ b/tests/test_aggregators.py @@ -1,3 +1,9 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + import asyncio import doctest import functools diff --git a/tests/test_ai_services.py b/tests/test_ai_services.py index 13aa20467..4b3a28408 100644 --- a/tests/test_ai_services.py +++ b/tests/test_ai_services.py @@ -1,3 +1,9 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + import unittest from typing import AsyncGenerator diff --git a/tests/test_daily_transport_service.py b/tests/test_daily_transport_service.py index 8c2788c9e..aabbd733d 100644 --- a/tests/test_daily_transport_service.py +++ b/tests/test_daily_transport_service.py @@ -1,3 +1,9 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + import unittest diff --git a/tests/test_filters.py b/tests/test_filters.py new file mode 100644 index 000000000..706ca1cca --- /dev/null +++ b/tests/test_filters.py @@ -0,0 +1,42 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import unittest + +from pipecat.frames.frames import ( + TranscriptionFrame, + UserStartedSpeakingFrame, + UserStoppedSpeakingFrame, +) +from pipecat.processors.filters.identity_filter import IdentityFilter +from pipecat.processors.filters.wake_check_filter import WakeCheckFilter +from tests.utils import run_test + + +class TestIdentifyFilter(unittest.IsolatedAsyncioTestCase): + async def test_identity(self): + filter = IdentityFilter() + frames_to_send = [UserStartedSpeakingFrame(), UserStoppedSpeakingFrame()] + expected_returned_frames = [UserStartedSpeakingFrame, UserStoppedSpeakingFrame] + await run_test(filter, frames_to_send, expected_returned_frames) + + +class TestWakeCheckFilter(unittest.IsolatedAsyncioTestCase): + async def test_no_wake_word(self): + filter = WakeCheckFilter(wake_phrases=["Hey, Pipecat"]) + frames_to_send = [TranscriptionFrame(user_id="test", text="Phrase 1", timestamp="")] + expected_returned_frames = [] + await run_test(filter, frames_to_send, expected_returned_frames) + + async def test_wake_word(self): + filter = WakeCheckFilter(wake_phrases=["Hey, Pipecat"]) + frames_to_send = [ + TranscriptionFrame(user_id="test", text="Hey, Pipecat", timestamp=""), + TranscriptionFrame(user_id="test", text="Phrase 1", timestamp=""), + ] + expected_returned_frames = [TranscriptionFrame, TranscriptionFrame] + (received_down, _) = await run_test(filter, frames_to_send, expected_returned_frames) + assert received_down[-1].text == "Phrase 1" diff --git a/tests/test_openai_tts.py b/tests/test_openai_tts.py index 1dc3929a6..171542232 100644 --- a/tests/test_openai_tts.py +++ b/tests/test_openai_tts.py @@ -1,3 +1,9 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + import asyncio import unittest diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 7d703d5ed..0acdb034d 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -1,3 +1,9 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + import asyncio import unittest from unittest.mock import Mock diff --git a/tests/test_protobuf_serializer.py b/tests/test_protobuf_serializer.py index 7f9841622..5e289286f 100644 --- a/tests/test_protobuf_serializer.py +++ b/tests/test_protobuf_serializer.py @@ -1,3 +1,9 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + import unittest from pipecat.frames.frames import AudioRawFrame, TextFrame, TranscriptionFrame diff --git a/tests/test_websocket_transport.py b/tests/test_websocket_transport.py index b24caa5b9..c918acd8a 100644 --- a/tests/test_websocket_transport.py +++ b/tests/test_websocket_transport.py @@ -1,3 +1,9 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + # import asyncio # import unittest # from unittest.mock import AsyncMock, patch, Mock diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..95da9f4aa --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,96 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio +from dataclasses import dataclass +from typing import Sequence, Tuple + +from pipecat.clocks.system_clock import SystemClock +from pipecat.frames.frames import ( + ControlFrame, + Frame, + StartFrame, +) +from pipecat.processors.frame_processor import FrameDirection, FrameProcessor + + +@dataclass +class EndTestFrame(ControlFrame): + pass + + +class QueuedFrameProcessor(FrameProcessor): + def __init__(self, queue: asyncio.Queue, ignore_start: bool = True): + super().__init__() + self._queue = queue + self._ignore_start = ignore_start + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + if self._ignore_start and isinstance(frame, StartFrame): + return + await self._queue.put(frame) + + +async def run_test( + processor: FrameProcessor, + frames_to_send: Sequence[Frame], + expected_down_frames: Sequence[type], + expected_up_frames: Sequence[type] = [], +) -> Tuple[Sequence[Frame], Sequence[Frame]]: + received_up = asyncio.Queue() + received_down = asyncio.Queue() + up_processor = QueuedFrameProcessor(received_up) + down_processor = QueuedFrameProcessor(received_down) + + up_processor.link(processor) + processor.link(down_processor) + + await processor.queue_frame(StartFrame(clock=SystemClock())) + + for frame in frames_to_send: + await processor.process_frame(frame, FrameDirection.DOWNSTREAM) + + await processor.queue_frame(EndTestFrame()) + await processor.queue_frame(EndTestFrame(), FrameDirection.UPSTREAM) + + # + # Down frames + # + received_down_frames: Sequence[Frame] = [] + running = True + while running: + frame = await received_down.get() + running = not isinstance(frame, EndTestFrame) + if running: + received_down_frames.append(frame) + + print("received DOWN frames =", received_down_frames) + + assert len(received_down_frames) == len(expected_down_frames) + + for real, expected in zip(received_down_frames, expected_down_frames): + assert isinstance(real, expected) + + # + # Up frames + # + received_up_frames: Sequence[Frame] = [] + running = True + while running: + frame = await received_up.get() + running = not isinstance(frame, EndTestFrame) + if running: + received_up_frames.append(frame) + + print("received UP frames =", received_up_frames) + + assert len(received_up_frames) == len(expected_up_frames) + + for real, expected in zip(received_up_frames, expected_up_frames): + assert isinstance(real, expected) + + return (received_down_frames, received_up_frames)