#!/usr/bin/env python3 """ End-to-End Test Suite for Dictation Service Tests the complete dictation pipeline from keybindings to audio processing """ import os import sys import time import subprocess import tempfile import threading import queue import json from pathlib import Path try: import sounddevice as sd import numpy as np from vosk import Model, KaldiRecognizer AUDIO_DEPS_AVAILABLE = True except ImportError: AUDIO_DEPS_AVAILABLE = False # Test configuration TEST_DIR = Path("/mnt/storage/Development/dictation-service") LOCK_FILES = { "dictation": TEST_DIR / "listening.lock", "conversation": TEST_DIR / "conversation.lock", } class DictationServiceTester: def __init__(self): self.results = [] self.errors = [] def log(self, message, level="INFO"): """Log test results""" timestamp = time.strftime("%H:%M:%S") print(f"[{timestamp}] {level}: {message}") self.results.append(f"{level}: {message}") def error(self, message): """Log errors""" self.log(message, "ERROR") self.errors.append(message) def test_lock_file_operations(self): """Test 1: Lock file creation and removal""" self.log("Testing lock file operations...") # Test dictation lock dictation_lock = LOCK_FILES["dictation"] # Ensure clean state if dictation_lock.exists(): dictation_lock.unlink() # Test creation dictation_lock.touch() if dictation_lock.exists(): self.log("✓ Dictation lock file creation works") else: self.error("✗ Dictation lock file creation failed") # Test removal dictation_lock.unlink() if not dictation_lock.exists(): self.log("✓ Dictation lock file removal works") else: self.error("✗ Dictation lock file removal failed") # Test conversation lock conv_lock = LOCK_FILES["conversation"] # Ensure clean state if conv_lock.exists(): conv_lock.unlink() # Test creation conv_lock.touch() if conv_lock.exists(): self.log("✓ Conversation lock file creation works") else: self.error("✗ Conversation lock file creation failed") conv_lock.unlink() def test_toggle_scripts(self): """Test 2: Toggle script functionality""" self.log("Testing toggle scripts...") # Test dictation toggle toggle_script = TEST_DIR / "scripts" / "toggle-dictation.sh" # Ensure clean state if LOCK_FILES["dictation"].exists(): LOCK_FILES["dictation"].unlink() # Run toggle script result = subprocess.run([str(toggle_script)], capture_output=True, text=True) if result.returncode == 0: self.log("✓ Dictation toggle script executed successfully") if LOCK_FILES["dictation"].exists(): self.log("✓ Dictation lock file created by script") else: self.error("✗ Dictation lock file not created by script") else: self.error(f"✗ Dictation toggle script failed: {result.stderr}") # Toggle again to remove lock result = subprocess.run([str(toggle_script)], capture_output=True, text=True) if result.returncode == 0 and not LOCK_FILES["dictation"].exists(): self.log("✓ Dictation toggle script properly removes lock file") else: self.error("✗ Dictation toggle script failed to remove lock file") def test_service_status(self): """Test 3: Service status and responsiveness""" self.log("Testing service status...") # Check if dictation service is running result = subprocess.run( ["systemctl", "--user", "is-active", "dictation.service"], capture_output=True, text=True, ) if result.returncode == 0 and result.stdout.strip() == "active": self.log("✓ Dictation service is active") else: self.error(f"✗ Dictation service not active: {result.stdout.strip()}") # Check keybinding listener service result = subprocess.run( ["systemctl", "--user", "is-active", "keybinding-listener.service"], capture_output=True, text=True, ) if result.returncode == 0 and result.stdout.strip() == "active": self.log("✓ Keybinding listener service is active") else: self.error( f"✗ Keybinding listener service not active: {result.stdout.strip()}" ) def test_audio_devices(self): """Test 4: Audio device availability""" self.log("Testing audio devices...") if not AUDIO_DEPS_AVAILABLE: self.error("✗ Audio dependencies not available") return try: devices = sd.query_devices() input_devices = [] # Handle different sounddevice API versions if isinstance(devices, list): for i, device in enumerate(devices): try: if ( hasattr(device, "get") and device.get("max_input_channels", 0) > 0 ): input_devices.append(device) elif ( hasattr(device, "__getitem__") and len(device) > 2 and device[2] > 0 ): input_devices.append(device) except: continue if input_devices: self.log(f"✓ Found {len(input_devices)} audio input device(s)") try: default_input = sd.query_devices(kind="input") if default_input: device_name = ( default_input.get("name", "Unknown") if hasattr(default_input, "get") else str(default_input) ) self.log(f"✓ Default input device available") else: self.error("✗ No default input device found") except: self.log("✓ Audio devices found (default device check skipped)") else: self.error("✗ No audio input devices found") except Exception as e: self.error(f"✗ Audio device test failed: {e}") def test_vosk_model(self): """Test 5: Vosk model loading and recognition""" self.log("Testing Vosk model...") if not AUDIO_DEPS_AVAILABLE: self.error("✗ Audio dependencies not available for Vosk testing") return try: model_path = ( TEST_DIR / "src" / "dictation_service" / "vosk-model-small-en-us-0.15" ) if model_path.exists(): self.log("✓ Vosk model directory exists") # Try to load model model = Model(str(model_path)) self.log("✓ Vosk model loaded successfully") # Test recognizer rec = KaldiRecognizer(model, 16000) self.log("✓ Vosk recognizer created successfully") # Test with dummy audio data dummy_audio = np.random.randint(-32768, 32767, 1600, dtype=np.int16) if rec.AcceptWaveform(dummy_audio.tobytes()): result = json.loads(rec.Result()) self.log( f"✓ Vosk recognition test passed: {result.get('text', 'no text')}" ) else: self.log("✓ Vosk recognition accepts audio data") else: self.error("✗ Vosk model directory not found") except Exception as e: self.error(f"✗ Vosk model test failed: {e}") def test_keybinding_simulation(self): """Test 6: Keybinding simulation""" self.log("Testing keybinding simulation...") # Test direct script execution toggle_script = TEST_DIR / "scripts" / "toggle-dictation.sh" # Clean state if LOCK_FILES["dictation"].exists(): LOCK_FILES["dictation"].unlink() # Simulate keybinding by running script result = subprocess.run( [str(toggle_script)], capture_output=True, text=True, env={"DISPLAY": ":1", "XAUTHORITY": "/run/user/1000/gdm/Xauthority"}, ) if result.returncode == 0: self.log("✓ Keybinding simulation (script execution) works") if LOCK_FILES["dictation"].exists(): self.log("✓ Lock file created via simulated keybinding") else: self.error("✗ Lock file not created via simulated keybinding") else: self.error(f"✗ Keybinding simulation failed: {result.stderr}") def test_service_logs(self): """Test 7: Check service logs for errors""" self.log("Checking service logs...") # Check dictation service logs result = subprocess.run( [ "journalctl", "--user", "-u", "dictation.service", "-n", "10", "--no-pager", ], capture_output=True, text=True, ) if "error" in result.stdout.lower() or "exception" in result.stdout.lower(): self.error("✗ Errors found in dictation service logs") self.log(f"Log excerpt: {result.stdout[-500:]}") else: self.log("✓ No obvious errors in dictation service logs") # Check keybinding listener logs result = subprocess.run( [ "journalctl", "--user", "-u", "keybinding-listener.service", "-n", "10", "--no-pager", ], capture_output=True, text=True, ) if "error" in result.stdout.lower() or "exception" in result.stdout.lower(): self.error("✗ Errors found in keybinding listener logs") self.log(f"Log excerpt: {result.stdout[-500:]}") else: self.log("✓ No obvious errors in keybinding listener logs") def test_end_to_end_flow(self): """Test 8: End-to-end dictation flow""" self.log("Testing end-to-end dictation flow...") # This is a simplified e2e test - in a real scenario we'd need to: # 1. Start dictation mode # 2. Send audio data # 3. Check if text is generated # 4. Stop dictation mode # For now, just test the basic flow self.log("Note: Full e2e audio processing test requires manual testing") self.log("Basic components tested above should enable manual e2e testing") def run_all_tests(self): """Run all tests""" self.log("Starting Dictation Service E2E Test Suite") self.log("=" * 50) test_methods = [ self.test_lock_file_operations, self.test_toggle_scripts, self.test_service_status, self.test_audio_devices, self.test_vosk_model, self.test_keybinding_simulation, self.test_service_logs, self.test_end_to_end_flow, ] for test_method in test_methods: try: test_method() self.log("-" * 30) except Exception as e: self.error(f"Test {test_method.__name__} crashed: {e}") self.log("-" * 30) # Summary self.log("=" * 50) self.log("TEST SUMMARY") self.log(f"Total tests: {len(test_methods)}") self.log(f"Errors: {len(self.errors)}") if self.errors: self.log("FAILED TESTS:") for error in self.errors: self.log(f" - {error}") return False else: self.log("ALL TESTS PASSED ✓") return True def main(): tester = DictationServiceTester() success = tester.run_all_tests() # Print full results print("\n" + "=" * 50) print("FULL TEST RESULTS:") for result in tester.results: print(result) return 0 if success else 1 if __name__ == "__main__": sys.exit(main())