Skip to content

Commit

Permalink
Merge pull request #346 from cclauss/patch-1
Browse files Browse the repository at this point in the history
Add willwade's test_pyttsx3.py
  • Loading branch information
willwade authored Oct 25, 2024
2 parents 5464739 + 6144471 commit 4beb058
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 1 deletion.
88 changes: 88 additions & 0 deletions .github/workflows/python_publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# This workflows will upload a Python Package using Twine when a release is created.
# https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]
release:
types: [created] # Only publish on tagged releases

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', '3.11', '3.13']
max-parallel: 9
runs-on: ${{ matrix.os }}
timeout-minutes: 10 # Save resources while our pytests are hanging
steps:
- if: runner.os == 'Linux'
run: |
sudo apt-get update -q -q
sudo apt-get install --yes espeak espeak-ng ffmpeg libespeak1
espeak --version
espeak-ng --version
ffmpeg --version || true
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
pip install --upgrade pip
pip install pytest pytest-timeout
pip install --editable .
- name: Run tests
run: pytest

build:
runs-on: ubuntu-latest
needs: [test] # This ensures tests pass before build
permissions:
id-token: write

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install dependencies
run: |
pip install --upgrade pip
pip install build twine
- name: Clean previous builds
run: |
rm -rf dist
- name: Build package
run: |
python -m build
python -m twine check --strict dist/*
deploy:
needs: [build]
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'created' # Only on release creation

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_PASSWORD }}
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[metadata]
description-file = README.md
long_description = file: README.md
long_description_content_type = text/markdown
155 changes: 155 additions & 0 deletions tests/test_pyttsx3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import os
import sys
from unittest import mock

import pytest
import wave

import pyttsx3


@pytest.fixture
def engine():
"""Fixture for initializing pyttsx3 engine."""
engine = pyttsx3.init()
yield engine
engine.stop() # Ensure the engine stops after tests


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

# Save the speech to a file
engine.save_to_file("Hello World", str(test_file))
engine.runAndWait()

# Check if the file was created
assert test_file.exists(), "The audio file was not created"

# Check if the file is not empty
assert test_file.stat().st_size > 0, "The audio file is empty"

# Check if the file is a valid .wav file using the wave module
with wave.open(str(test_file), "rb") as wf:
assert wf.getnchannels() == 1, "The audio file should have 1 channel (mono)"
assert wf.getsampwidth() == 2, "The audio file sample width should be 2 bytes"
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")
def test_listening_for_events(engine):
onStart = mock.Mock()
onWord = mock.Mock()
onEnd = mock.Mock()

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.")
engine.runAndWait()

# Ensure the event handlers were called
assert onStart.called
assert onWord.called
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):
if location > 10:
engine.stop()

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.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")
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.")
engine.runAndWait()


# 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")
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.")
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")
def test_running_driver_event_loop(engine):
def onStart(name):
print("starting", name)

def onWord(name, location, length):
print("word", name, location, length)

def onEnd(name, completed):
if name == "fox":
engine.say("What a lazy dog!", "dog")
elif name == "dog":
engine.endLoop()

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.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.startLoop(False)
externalLoop()
engine.endLoop()

0 comments on commit 4beb058

Please sign in to comment.