Skip to content

Commit

Permalink
Stop skipping pytests
Browse files Browse the repository at this point in the history
  • Loading branch information
cclauss committed Oct 30, 2024
1 parent 12145bc commit 5ec1fc9
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
pip install --editable .
- name: Run tests
run: pytest
run: pytest -s -vvv --timeout=600

build:
runs-on: ubuntu-latest
Expand Down
32 changes: 24 additions & 8 deletions pyttsx3/engine.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from . import driver
from __future__ import annotations

import traceback
import weakref

from . import driver


class Engine(object):
"""
Expand All @@ -17,7 +20,7 @@ class Engine(object):
@type _debug: bool
"""

def __init__(self, driverName=None, debug=False):
def __init__(self, driverName: str | None = None, debug: bool = False):
"""
Constructs a new TTS engine instance.
Expand All @@ -28,11 +31,24 @@ def __init__(self, driverName=None, debug=False):
@type debug: bool
"""
self.proxy = driver.DriverProxy(weakref.proxy(self), driverName, debug)
# initialize other vars
self.name = self.proxy._module.__name__.split('.')[-1]
self._connects = {}
self._inLoop = False
self._driverLoop = True
self._debug = debug
self._driverLoop = True
self._inLoop = False

def __repr__(self) -> str:
"""
repr(pyttsx3.init('nsss')) -> "pyttsx3.engine.Engine('nsss', debug=False)"
"""
module_and_class = f"{self.__class__.__module__}.{self.__class__.__name__}"
return f"{module_and_class}('{self.name}', debug={self._debug})"

def __str__(self) -> str:
"""
str(pyttsx3.init('nsss')) -> 'nsss'
"""
return self.name

def _notify(self, topic, **kwargs):
"""
Expand Down Expand Up @@ -97,10 +113,10 @@ def say(self, text, name=None):
notifications about this utterance.
@type name: str
"""
if text == None:
return "Argument value can't be none or empty"
else:
if text and str(text).strip():
self.proxy.say(text, name)
else:
return "Argument value can't be none or empty"

def stop(self):
"""
Expand Down
118 changes: 76 additions & 42 deletions tests/test_pyttsx3.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import os
from collections.abc import Iterator
import sys
from unittest import mock

Expand All @@ -7,6 +7,8 @@

import pyttsx3

quick_brown_fox = "The quick brown fox jumped over the lazy dog."


@pytest.fixture
def engine():
Expand All @@ -16,18 +18,60 @@ def engine():
engine.stop() # Ensure the engine stops after tests


# Test for speaking text
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
def test_engine_name(engine):
expected = {
'darwin': 'nsss',
'win32': 'sapi5'
}.get(sys.platform, 'espeak')
assert engine.name == expected
assert str(engine) == expected
assert repr(engine) == f"pyttsx3.engine.Engine('{expected}', debug=False)"


@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows")
def test_speaking_text(engine):
engine.say("Sally sells seashells by the seashore.")
engine.say("The quick brown fox jumped over the lazy dog.")
engine.say(quick_brown_fox)
print(list(pyttsx3._activeEngines.values()))
engine.runAndWait()


@pytest.mark.skipif(sys.platform not in ("darwin", "ios"), reason="Testing only on macOS and iOS")
def test_apple__nsss_voices(engine):
import platform

macos_version, _, macos_hardware = platform.mac_ver()
print(f"{sys.platform = }, {macos_version = } on {macos_hardware = }")
print(list(pyttsx3._activeEngines))
print(engine)
assert str(engine) == "nsss", "Expected engine name to be nsss on macOS and iOS"
voice = engine.getProperty("voice")
# On macOS v14.x, the default nsss voice is com.apple.voice.compact.en-US.Samantha.
# ON macOS v15.x, the default nsss voice is ""
assert voice in ("", "com.apple.voice.compact.en-US.Samantha"), (
"Expected default voice to be com.apple.voice.compact.en-US.Samantha on macOS and iOS"
)
voices = engine.getProperty("voices")
# On macOS v14.x, nsss has 143 voices.
# On macOS v15.x, nsss has 176 voices
print(f"On macOS v{macos_version}, {engine} has {len(voices) = } voices.")
assert len(voices) in (176, 143), "Expected 176 or 143 voices on macOS and iOS"
# print("\n".join(voice.id for voice in voices))
en_us_voices = [voice for voice in voices if voice.id.startswith("com.apple.eloquence.en-US.")]
assert len(en_us_voices) == 8, "Expected 8 com.apple.eloquence.en-US voices on macOS and iOS"
names = []
for _voice in en_us_voices:
engine.setProperty("voice", _voice.id)
name = _voice.id.split(".")[-1]
names.append(name)
engine.say(f"{name} says {quick_brown_fox}")
name_str = ", ".join(names)
assert name_str == "Eddy, Flo, Grandma, Grandpa, Reed, Rocko, Sandy, Shelley"
print(f"({name_str})", end=" ", flush=True)
engine.runAndWait()
engine.setProperty("voice", voice) # Reset voice to original value


# Test for saving voice to a file with additional validation
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
# @pytest.mark.skipif(sys.platform in ("linux", "win32"), reason="TODO: Fix this test to pass on Linux and Windows")
@pytest.mark.xfail(sys.platform == "darwin", reason="TODO: Fix this test to pass on macOS")
def test_saving_to_file(engine, tmp_path):
test_file = tmp_path / "test.wav" # Using .wav for easier validation
Expand All @@ -49,9 +93,7 @@ def test_saving_to_file(engine, tmp_path):
assert wf.getframerate() == 22050, "The audio file framerate should be 22050 Hz"


# Test for listening for events
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
@pytest.mark.skipif(sys.platform in ("linux", "win32"), reason="TODO: Fix this test to pass on Linux and Windows")
@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows")
def test_listening_for_events(engine):
onStart = mock.Mock()
onWord = mock.Mock()
Expand All @@ -61,7 +103,7 @@ def test_listening_for_events(engine):
engine.connect("started-word", onWord)
engine.connect("finished-utterance", onEnd)

engine.say("The quick brown fox jumped over the lazy dog.")
engine.say(quick_brown_fox)
engine.runAndWait()

# Ensure the event handlers were called
Expand All @@ -70,8 +112,6 @@ def test_listening_for_events(engine):
assert onEnd.called


# Test for interrupting an utterance
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
@pytest.mark.skipif(sys.platform in ("linux", "win32"), reason="TODO: Fix this test to pass on Linux and Windows")
def test_interrupting_utterance(engine):
def onWord(name, location, length):
Expand All @@ -80,47 +120,43 @@ def onWord(name, location, length):

onWord_mock = mock.Mock(side_effect=onWord)
engine.connect("started-word", onWord_mock)
engine.say("The quick brown fox jumped over the lazy dog.")
engine.say(quick_brown_fox)
engine.runAndWait()

# Check that stop was called
assert onWord_mock.called


# Test for changing voices
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
@pytest.mark.skipif(sys.platform in ("linux", "win32"), reason="TODO: Fix this test to pass on Linux and Windows")
def test_changing_voices(engine):
voices = engine.getProperty("voices")
for voice in voices:
engine.setProperty("voice", voice.id)
engine.say("The quick brown fox jumped over the lazy dog.")
engine.runAndWait()


# Test for changing speech rate
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
@pytest.mark.skipif(sys.platform in ("linux", "win32"), reason="TODO: Fix this test to pass on Linux and Windows")
@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows")
def test_changing_speech_rate(engine):
rate = engine.getProperty("rate")
engine.setProperty("rate", rate + 50)
engine.say("The quick brown fox jumped over the lazy dog.")
rate_plus_fifty = rate + 50
engine.setProperty("rate", rate_plus_fifty)
engine.say(f"{rate_plus_fifty = } {quick_brown_fox}")
engine.runAndWait()
engine.setProperty("rate", rate) # Reset rate to original value


# Test for changing volume
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
@pytest.mark.skipif(sys.platform in ("linux", "win32"), reason="TODO: Fix this test to pass on Linux and Windows")
@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows")
def test_changing_volume(engine):
volume = engine.getProperty("volume")
engine.setProperty("volume", volume - 0.25)
engine.say("The quick brown fox jumped over the lazy dog.")
volume_minus_a_quarter = volume - 0.25
engine.setProperty("volume", volume_minus_a_quarter)
engine.say(f"{volume_minus_a_quarter = } {quick_brown_fox}")
engine.runAndWait()
engine.setProperty("volume", volume) # Reset volume to original value


# Test for running a driver event loop
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
@pytest.mark.skipif(sys.platform in ("linux", "win32"), reason="TODO: Fix this test to pass on Linux and Windows")
@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows")
def test_changing_voices(engine):
voices = engine.getProperty("voices")
for voice in voices: # TODO: This could be lots of voices! (e.g. 176 on macOS v15.x)
engine.setProperty("voice", voice.id)
engine.say(f"{voice.id = }. {quick_brown_fox}")
engine.runAndWait()


@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows")
def test_running_driver_event_loop(engine):
def onStart(name):
print("starting", name)
Expand All @@ -137,19 +173,17 @@ def onEnd(name, completed):
engine.connect("started-utterance", onStart)
engine.connect("started-word", onWord)
engine.connect("finished-utterance", onEnd)
engine.say("The quick brown fox jumped over the lazy dog.", "fox")
engine.say(quick_brown_fox, "fox")
engine.startLoop()


# Test for using an external event loop
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
@pytest.mark.skipif(sys.platform in ("linux", "win32"), reason="TODO: Fix this test to pass on Linux and Windows")
def test_external_event_loop(engine):
def externalLoop():
for _ in range(5):
engine.iterate()

engine.say("The quick brown fox jumped over the lazy dog.", "fox")
engine.say(quick_brown_fox, "fox")
engine.startLoop(False)
externalLoop()
engine.endLoop()

0 comments on commit 5ec1fc9

Please sign in to comment.