Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop skipping pytests unless they currently fail #353

Merged
merged 2 commits into from
Nov 4, 2024
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
36 changes: 19 additions & 17 deletions pyttsx3/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ def __init__(self, driverName: str | None = None, debug: bool = False):
self.driver_name = driverName or default_engine_by_sys_platform()
self.proxy = driver.DriverProxy(weakref.proxy(self), self.driver_name, debug)
self._connects = {}
self._inLoop = False
self._driverLoop = True
self._debug = debug
self._driverLoop = True
self._inLoop = False

def __repr__(self) -> str:
"""
Expand All @@ -75,7 +75,7 @@ def __str__(self) -> str:
"""
return self.driver_name

def _notify(self, topic, **kwargs):
def _notify(self, topic: str, **kwargs) -> None:
"""
Invokes callbacks for an event topic.

Expand All @@ -91,7 +91,7 @@ def _notify(self, topic, **kwargs):
if self._debug:
traceback.print_exc()

def connect(self, topic, cb):
def connect(self, topic: str, cb: callable) -> dict:
"""
Registers a callback for an event topic. Valid topics and their
associated values:
Expand All @@ -112,7 +112,7 @@ def connect(self, topic, cb):
arr.append(cb)
return {"topic": topic, "cb": cb}

def disconnect(self, token):
def disconnect(self, token: dict) -> None:
"""
Unregisters a callback for an event topic.

Expand All @@ -128,7 +128,7 @@ def disconnect(self, token):
if len(arr) == 0:
del self._connects[topic]

def say(self, text, name=None):
def say(self, text: str | None, name: str | None = None):
"""
Adds an utterance to speak to the event queue.

Expand All @@ -138,18 +138,18 @@ 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 str(text or "").strip():
self.proxy.say(text, name)
else:
return "Argument value can't be None or empty"

def stop(self):
def stop(self) -> None:
"""
Stops the current utterance and clears the event queue.
"""
self.proxy.stop()

def save_to_file(self, text, filename, name=None):
def save_to_file(self, text: str, filename: str, name: str | None = None) -> None:
"""
Adds an utterance to speak to the event queue.

Expand All @@ -160,16 +160,17 @@ def save_to_file(self, text, filename, name=None):
notifications about this utterance.
@type name: str
"""
assert text and filename
self.proxy.save_to_file(text, filename, name)

def isBusy(self):
def isBusy(self) -> bool:
"""
@return: True if an utterance is currently being spoken, false if not
@rtype: bool
"""
return self.proxy.isBusy()

def getProperty(self, name):
def getProperty(self, name: str) -> object:
"""
Gets the current value of a property. Valid names and values include:

Expand All @@ -187,9 +188,10 @@ def getProperty(self, name):
@rtype: object
@raise KeyError: When the property name is unknown
"""
assert name
return self.proxy.getProperty(name)

def setProperty(self, name, value):
def setProperty(self, name: str, value: str | float) -> None:
"""
Adds a property value to set to the event queue. Valid names and values
include:
Expand All @@ -209,7 +211,7 @@ def setProperty(self, name, value):
"""
self.proxy.setProperty(name, value)

def runAndWait(self):
def runAndWait(self) -> None:
"""
Runs an event loop until all commands queued up until this method call
complete. Blocks during the event loop and returns when the queue is
Expand All @@ -223,7 +225,7 @@ def runAndWait(self):
self._driverLoop = True
self.proxy.runAndWait()

def startLoop(self, useDriverLoop=True):
def startLoop(self, useDriverLoop: bool = True) -> None:
"""
Starts an event loop to process queued commands and callbacks.

Expand All @@ -239,7 +241,7 @@ def startLoop(self, useDriverLoop=True):
self._driverLoop = useDriverLoop
self.proxy.startLoop(self._driverLoop)

def endLoop(self):
def endLoop(self) -> None:
"""
Stops a running event loop.

Expand Down
121 changes: 74 additions & 47 deletions tests/test_pyttsx3.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import pyttsx3

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


@pytest.fixture
def engine(driver_name: str | None = None) -> pyttsx3.engine.Engine:
Expand All @@ -24,20 +26,58 @@ def test_engine_name(engine):
assert repr(engine) == f"pyttsx3.engine.Engine('{expected}', debug=False)"


# Test for speaking text
# @pytest.mark.timeout(10) # Set timeout to 10 seconds
@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()


# 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.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


@pytest.mark.xfail(
sys.platform == "darwin", reason="TODO: Fix this test to pass on macOS"
)
Expand All @@ -61,11 +101,8 @@ 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",
sys.platform == "win32", reason="TODO: Fix this test to pass on Windows"
)
def test_listening_for_events(engine):
onStart = mock.Mock()
Expand All @@ -76,7 +113,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 @@ -85,8 +122,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",
Expand All @@ -98,58 +133,52 @@ 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",
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:
engine.setProperty("voice", voice.id)
engine.say("The quick brown fox jumped over the lazy dog.")
def test_changing_speech_rate(engine):
rate = engine.getProperty("rate")
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 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",
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.")
def test_changing_volume(engine):
volume = engine.getProperty("volume")
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 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",
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.")
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()


# 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",
sys.platform == "win32", reason="TODO: Fix this test to pass on Windows"
)
def test_running_driver_event_loop(engine):
def onStart(name):
Expand All @@ -167,12 +196,10 @@ 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",
Expand All @@ -182,7 +209,7 @@ 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()