Skip to content
Open
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
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ nosetests.xml
coverage.xml
*,cover

# Claude settings
.claude/*

# Virtual environments
venv/
ENV/
env.bak/
venv.bak/

# IDE files
.vscode/
.idea/
*.swp
*.swo
*~

# Poetry
# Do not ignore poetry.lock - it should be committed
# poetry.lock is intentionally not ignored

# Translations
*.mo
*.pot
Expand Down
89 changes: 89 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
[tool.poetry]
name = "speechmetrics"
version = "1.0.0"
description = "A Python library for speech quality metrics"
authors = ["Your Name <your.email@example.com>"]
readme = "README.md"
license = "MIT"
packages = [{include = "speechmetrics"}]

[tool.poetry.dependencies]
python = "^3.9"
numpy = "<1.24"
scipy = "*"
tqdm = "*"
resampy = "*"
pystoi = "*"
museval = "*"
tensorflow = ">=2.0.0"
librosa = "*"
pypesq = {git = "https://github.com/vBaiCai/python-pesq"}
srmrpy = {git = "https://github.com/jfsantos/SRMRpy"}
pesq = {git = "https://github.com/ludlows/python-pesq", develop = true}

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-mock = "^3.11.0"

[tool.poetry.scripts]
test = "pytest:main"
tests = "pytest:main"

[tool.pytest.ini_options]
minversion = "6.0"
addopts = [
"-ra",
"--strict-markers",
"--cov=speechmetrics",
"--cov-branch",
"--cov-report=term-missing:skip-covered",
"--cov-report=html",
"--cov-report=xml",
"--cov-fail-under=80",
]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"unit: marks tests as unit tests",
"integration: marks tests as integration tests",
"slow: marks tests as slow running",
]

[tool.coverage.run]
source = ["speechmetrics"]
branch = true
omit = [
"*/tests/*",
"*/__init__.py",
"*/setup.py",
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if __name__ == .__main__.:",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if False:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
precision = 2
show_missing = true
skip_covered = false

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
104 changes: 104 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Shared pytest fixtures and configuration for speechmetrics tests."""

import os
import tempfile
from pathlib import Path
from typing import Generator

import numpy as np
import pytest


@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
"""Create a temporary directory for test files."""
with tempfile.TemporaryDirectory() as tmp_dir:
yield Path(tmp_dir)


@pytest.fixture
def sample_audio_data() -> np.ndarray:
"""Generate sample audio data for testing."""
sample_rate = 16000
duration = 1.0 # 1 second
frequency = 440.0 # A4 note

t = np.linspace(0, duration, int(sample_rate * duration))
audio = np.sin(2 * np.pi * frequency * t)

return audio


@pytest.fixture
def sample_rate() -> int:
"""Standard sample rate for test audio."""
return 16000


@pytest.fixture
def mock_config() -> dict:
"""Mock configuration dictionary for testing."""
return {
"model_path": "/path/to/model",
"batch_size": 32,
"num_workers": 4,
"device": "cpu",
}


@pytest.fixture
def test_data_dir() -> Path:
"""Path to test data directory."""
return Path(__file__).parent / "data"


@pytest.fixture
def example_wav_file(temp_dir: Path, sample_audio_data: np.ndarray) -> Path:
"""Create a temporary WAV file for testing."""
import scipy.io.wavfile as wavfile

wav_path = temp_dir / "test_audio.wav"
wavfile.write(str(wav_path), 16000, (sample_audio_data * 32767).astype(np.int16))

return wav_path


@pytest.fixture(autouse=True)
def reset_environment():
"""Reset environment variables before each test."""
original_env = os.environ.copy()
yield
os.environ.clear()
os.environ.update(original_env)


@pytest.fixture
def mock_tensorflow(monkeypatch):
"""Mock TensorFlow imports for faster testing."""
import sys
from unittest.mock import MagicMock

mock_tf = MagicMock()
monkeypatch.setitem(sys.modules, "tensorflow", mock_tf)

return mock_tf


@pytest.fixture
def noisy_audio_pair(sample_audio_data: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Generate a clean and noisy audio pair for testing."""
clean = sample_audio_data
noise = np.random.normal(0, 0.1, size=clean.shape)
noisy = clean + noise

return clean, noisy


def pytest_configure(config):
"""Configure pytest with custom settings."""
config.addinivalue_line(
"markers", "requires_model: mark test as requiring a pre-trained model"
)
config.addinivalue_line(
"markers", "requires_gpu: mark test as requiring GPU support"
)
Empty file added tests/integration/__init__.py
Empty file.
71 changes: 71 additions & 0 deletions tests/test_setup_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Validation tests to ensure the testing infrastructure is properly set up."""

import sys
from pathlib import Path

import numpy as np
import pytest


class TestSetupValidation:
"""Test class to validate the testing infrastructure."""

@pytest.mark.unit
def test_pytest_is_installed(self):
"""Verify pytest is properly installed."""
assert "pytest" in sys.modules or True

@pytest.mark.unit
def test_fixtures_are_available(self, temp_dir, sample_audio_data, sample_rate):
"""Verify that custom fixtures are accessible."""
assert isinstance(temp_dir, Path)
assert temp_dir.exists()

assert isinstance(sample_audio_data, np.ndarray)
assert len(sample_audio_data) == sample_rate

assert isinstance(sample_rate, int)
assert sample_rate == 16000

@pytest.mark.unit
def test_markers_are_configured(self, request):
"""Verify custom markers are properly configured."""
markers = [marker.name for marker in request.node.iter_markers()]
assert "unit" in markers

@pytest.mark.integration
def test_temp_file_creation(self, example_wav_file):
"""Verify temporary file creation works."""
assert example_wav_file.exists()
assert example_wav_file.suffix == ".wav"

@pytest.mark.unit
def test_coverage_is_enabled(self):
"""Verify coverage reporting is enabled."""
try:
import coverage
assert coverage.__version__
except ImportError:
pytest.fail("Coverage module not installed")

@pytest.mark.unit
@pytest.mark.slow
def test_multiple_markers(self, request):
"""Test that multiple markers can be applied."""
markers = {marker.name for marker in request.node.iter_markers()}
assert "unit" in markers
assert "slow" in markers

@pytest.mark.unit
def test_speechmetrics_importable(self):
"""Verify the speechmetrics package can be imported."""
try:
import speechmetrics
assert speechmetrics
except ImportError:
pytest.skip("speechmetrics package not yet installed")

@pytest.mark.parametrize("test_value", [1, 2, 3])
def test_parametrization_works(self, test_value):
"""Verify pytest parametrization is functional."""
assert test_value in [1, 2, 3]
Empty file added tests/unit/__init__.py
Empty file.