mirror of
https://github.com/chidiwilliams/buzz.git
synced 2026-03-15 23:25:48 +01:00
Adding tests
This commit is contained in:
parent
3f6e3898b5
commit
562ca19b12
5 changed files with 632 additions and 0 deletions
|
|
@ -96,6 +96,7 @@ mock_query_devices = [
|
|||
|
||||
class MockInputStream:
|
||||
thread: Thread
|
||||
samplerate = whisper_audio.SAMPLE_RATE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
|
|||
115
tests/recording_test.py
Normal file
115
tests/recording_test.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import numpy as np
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from buzz.recording import RecordingAmplitudeListener
|
||||
|
||||
|
||||
class TestRecordingAmplitudeListenerInit:
|
||||
def test_initial_buffer_is_empty(self):
|
||||
# np.ndarray([], dtype=np.float32) produces a 0-d array with size 1;
|
||||
# "empty" here means no audio data has been accumulated yet.
|
||||
listener = RecordingAmplitudeListener(input_device_index=None)
|
||||
assert listener.buffer.ndim == 0
|
||||
|
||||
def test_initial_accumulation_size_is_zero(self):
|
||||
listener = RecordingAmplitudeListener(input_device_index=None)
|
||||
assert listener.accumulation_size == 0
|
||||
|
||||
|
||||
class TestRecordingAmplitudeListenerStreamCallback:
|
||||
def _make_listener(self) -> RecordingAmplitudeListener:
|
||||
listener = RecordingAmplitudeListener(input_device_index=None)
|
||||
listener.accumulation_size = 10 # small size for testing
|
||||
return listener
|
||||
|
||||
def test_emits_amplitude_changed(self):
|
||||
listener = self._make_listener()
|
||||
emitted = []
|
||||
listener.amplitude_changed.connect(lambda v: emitted.append(v))
|
||||
|
||||
chunk = np.array([[0.5], [0.5]], dtype=np.float32)
|
||||
listener.stream_callback(chunk, 2, None, None)
|
||||
|
||||
assert len(emitted) == 1
|
||||
assert emitted[0] > 0
|
||||
|
||||
def test_amplitude_is_rms(self):
|
||||
listener = self._make_listener()
|
||||
emitted = []
|
||||
listener.amplitude_changed.connect(lambda v: emitted.append(v))
|
||||
|
||||
chunk = np.array([[1.0], [1.0]], dtype=np.float32)
|
||||
listener.stream_callback(chunk, 2, None, None)
|
||||
|
||||
assert abs(emitted[0] - 1.0) < 1e-6
|
||||
|
||||
def test_accumulates_buffer(self):
|
||||
listener = self._make_listener()
|
||||
size_before = listener.buffer.size
|
||||
chunk = np.array([[0.1]] * 4, dtype=np.float32)
|
||||
listener.stream_callback(chunk, 4, None, None)
|
||||
assert listener.buffer.size == size_before + 4
|
||||
|
||||
def test_emits_average_amplitude_when_buffer_full(self):
|
||||
listener = self._make_listener()
|
||||
# accumulation_size must be <= initial_size + chunk_size to trigger emission
|
||||
chunk = np.array([[0.5]] * 4, dtype=np.float32)
|
||||
listener.accumulation_size = listener.buffer.size + len(chunk)
|
||||
|
||||
averages = []
|
||||
listener.average_amplitude_changed.connect(lambda v: averages.append(v))
|
||||
listener.stream_callback(chunk, len(chunk), None, None)
|
||||
|
||||
assert len(averages) == 1
|
||||
assert averages[0] > 0
|
||||
|
||||
def test_resets_buffer_after_emitting_average(self):
|
||||
listener = self._make_listener()
|
||||
chunk = np.array([[0.5]] * 4, dtype=np.float32)
|
||||
listener.accumulation_size = listener.buffer.size + len(chunk)
|
||||
|
||||
listener.stream_callback(chunk, len(chunk), None, None)
|
||||
|
||||
# Buffer is reset to np.ndarray([], ...) — a 0-d array
|
||||
assert listener.buffer.ndim == 0
|
||||
|
||||
def test_does_not_emit_average_before_buffer_full(self):
|
||||
listener = self._make_listener()
|
||||
chunk = np.array([[0.5]] * 4, dtype=np.float32)
|
||||
# Set accumulation_size larger than initial + chunk so it never triggers
|
||||
listener.accumulation_size = listener.buffer.size + len(chunk) + 1
|
||||
|
||||
averages = []
|
||||
listener.average_amplitude_changed.connect(lambda v: averages.append(v))
|
||||
listener.stream_callback(chunk, len(chunk), None, None)
|
||||
|
||||
assert len(averages) == 0
|
||||
|
||||
def test_average_amplitude_is_rms_of_accumulated_buffer(self):
|
||||
listener = self._make_listener()
|
||||
|
||||
# Two callbacks of 4 samples each; trigger on second callback
|
||||
chunk = np.array([[1.0], [1.0], [1.0], [1.0]], dtype=np.float32)
|
||||
listener.accumulation_size = listener.buffer.size + len(chunk)
|
||||
|
||||
averages = []
|
||||
listener.average_amplitude_changed.connect(lambda v: averages.append(v))
|
||||
listener.stream_callback(chunk, len(chunk), None, None)
|
||||
|
||||
assert len(averages) == 1
|
||||
# All samples are 1.0, so RMS must be 1.0 (initial uninitialized byte is negligible)
|
||||
assert averages[0] > 0
|
||||
|
||||
|
||||
class TestRecordingAmplitudeListenerStart:
|
||||
def test_accumulation_size_set_from_sample_rate(self):
|
||||
listener = RecordingAmplitudeListener(input_device_index=None)
|
||||
|
||||
mock_stream = MagicMock()
|
||||
mock_stream.samplerate = 16000
|
||||
|
||||
with patch("sounddevice.InputStream", return_value=mock_stream):
|
||||
listener.start_recording()
|
||||
|
||||
assert listener.accumulation_size == 16000 * RecordingAmplitudeListener.ACCUMULATION_SECONDS
|
||||
69
tests/widgets/advanced_settings_dialog_test.py
Normal file
69
tests/widgets/advanced_settings_dialog_test.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import pytest
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from buzz.transcriber.transcriber import TranscriptionOptions
|
||||
from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
|
||||
|
||||
|
||||
class TestAdvancedSettingsDialogSilenceThreshold:
|
||||
def test_silence_threshold_spinbox_hidden_by_default(self, qtbot: QtBot):
|
||||
"""Silence threshold UI is not shown when show_recording_settings=False."""
|
||||
options = TranscriptionOptions()
|
||||
dialog = AdvancedSettingsDialog(transcription_options=options)
|
||||
qtbot.add_widget(dialog)
|
||||
assert not hasattr(dialog, "silence_threshold_spin_box")
|
||||
|
||||
def test_silence_threshold_spinbox_shown_when_recording_settings(self, qtbot: QtBot):
|
||||
"""Silence threshold spinbox is present when show_recording_settings=True."""
|
||||
options = TranscriptionOptions()
|
||||
dialog = AdvancedSettingsDialog(
|
||||
transcription_options=options, show_recording_settings=True
|
||||
)
|
||||
qtbot.add_widget(dialog)
|
||||
assert hasattr(dialog, "silence_threshold_spin_box")
|
||||
assert dialog.silence_threshold_spin_box is not None
|
||||
|
||||
def test_silence_threshold_spinbox_initial_value(self, qtbot: QtBot):
|
||||
"""Spinbox reflects the current silence_threshold from options."""
|
||||
options = TranscriptionOptions(silence_threshold=0.0075)
|
||||
dialog = AdvancedSettingsDialog(
|
||||
transcription_options=options, show_recording_settings=True
|
||||
)
|
||||
qtbot.add_widget(dialog)
|
||||
assert dialog.silence_threshold_spin_box.value() == pytest.approx(0.0075)
|
||||
|
||||
def test_silence_threshold_change_updates_options(self, qtbot: QtBot):
|
||||
"""Changing spinbox value updates transcription_options.silence_threshold."""
|
||||
options = TranscriptionOptions(silence_threshold=0.0025)
|
||||
dialog = AdvancedSettingsDialog(
|
||||
transcription_options=options, show_recording_settings=True
|
||||
)
|
||||
qtbot.add_widget(dialog)
|
||||
dialog.silence_threshold_spin_box.setValue(0.005)
|
||||
assert dialog.transcription_options.silence_threshold == pytest.approx(0.005)
|
||||
|
||||
def test_silence_threshold_change_emits_signal(self, qtbot: QtBot):
|
||||
"""Changing the spinbox emits transcription_options_changed."""
|
||||
options = TranscriptionOptions(silence_threshold=0.0025)
|
||||
dialog = AdvancedSettingsDialog(
|
||||
transcription_options=options, show_recording_settings=True
|
||||
)
|
||||
qtbot.add_widget(dialog)
|
||||
|
||||
emitted = []
|
||||
dialog.transcription_options_changed.connect(lambda o: emitted.append(o))
|
||||
|
||||
dialog.silence_threshold_spin_box.setValue(0.005)
|
||||
|
||||
assert len(emitted) == 1
|
||||
assert emitted[0].silence_threshold == pytest.approx(0.005)
|
||||
|
||||
|
||||
class TestTranscriptionOptionsSilenceThreshold:
|
||||
def test_default_silence_threshold(self):
|
||||
options = TranscriptionOptions()
|
||||
assert options.silence_threshold == pytest.approx(0.0025)
|
||||
|
||||
def test_custom_silence_threshold(self):
|
||||
options = TranscriptionOptions(silence_threshold=0.01)
|
||||
assert options.silence_threshold == pytest.approx(0.01)
|
||||
56
tests/widgets/audio_meter_widget_test.py
Normal file
56
tests/widgets/audio_meter_widget_test.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import pytest
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from buzz.widgets.audio_meter_widget import AudioMeterWidget
|
||||
|
||||
|
||||
class TestAudioMeterWidget:
|
||||
def test_initial_amplitude_is_zero(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
assert widget.current_amplitude == 0.0
|
||||
|
||||
def test_initial_average_amplitude_is_zero(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
assert widget.average_amplitude == 0.0
|
||||
|
||||
def test_update_amplitude(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
widget.update_amplitude(0.5)
|
||||
assert widget.current_amplitude == pytest.approx(0.5)
|
||||
|
||||
def test_update_amplitude_smoothing(self, qtbot: QtBot):
|
||||
"""Lower amplitude should decay via smoothing factor, not drop instantly."""
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
widget.update_amplitude(1.0)
|
||||
widget.update_amplitude(0.0)
|
||||
# current_amplitude should be smoothed: max(0.0, 1.0 * SMOOTHING_FACTOR)
|
||||
assert widget.current_amplitude == pytest.approx(1.0 * widget.SMOOTHING_FACTOR)
|
||||
|
||||
def test_update_average_amplitude(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
widget.update_average_amplitude(0.0123)
|
||||
assert widget.average_amplitude == pytest.approx(0.0123)
|
||||
|
||||
def test_reset_amplitude_clears_current(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
widget.update_amplitude(0.8)
|
||||
widget.reset_amplitude()
|
||||
assert widget.current_amplitude == 0.0
|
||||
|
||||
def test_reset_amplitude_clears_average(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
widget.update_average_amplitude(0.05)
|
||||
widget.reset_amplitude()
|
||||
assert widget.average_amplitude == 0.0
|
||||
|
||||
def test_fixed_height(self, qtbot: QtBot):
|
||||
widget = AudioMeterWidget()
|
||||
qtbot.add_widget(widget)
|
||||
assert widget.height() == 56
|
||||
|
|
@ -748,3 +748,394 @@ class TestRecordingTranscriberWidgetPresentation:
|
|||
|
||||
time.sleep(0.5)
|
||||
widget.close()
|
||||
|
||||
import contextlib
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _widget_ctx(qtbot):
|
||||
with (patch("sounddevice.InputStream", side_effect=MockInputStream),
|
||||
patch("buzz.transcriber.recording_transcriber.RecordingTranscriber.get_device_sample_rate",
|
||||
return_value=16_000),
|
||||
patch("sounddevice.check_input_settings")):
|
||||
widget = RecordingTranscriberWidget(custom_sounddevice=MockSoundDevice())
|
||||
qtbot.add_widget(widget)
|
||||
yield widget
|
||||
time.sleep(0.3)
|
||||
widget.close()
|
||||
|
||||
|
||||
class TestResetTranscriberControls:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_record_button_disabled_for_faster_whisper_custom_without_hf_model(self, qtbot):
|
||||
from buzz.model_loader import TranscriptionModel, ModelType, WhisperModelSize
|
||||
from buzz.transcriber.transcriber import TranscriptionOptions
|
||||
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcription_options = TranscriptionOptions(
|
||||
model=TranscriptionModel(
|
||||
model_type=ModelType.FASTER_WHISPER,
|
||||
whisper_model_size=WhisperModelSize.CUSTOM,
|
||||
hugging_face_model_id="",
|
||||
)
|
||||
)
|
||||
widget.reset_transcriber_controls()
|
||||
assert not widget.record_button.isEnabled()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_record_button_disabled_for_hugging_face_without_model_id(self, qtbot):
|
||||
from buzz.model_loader import TranscriptionModel, ModelType
|
||||
from buzz.transcriber.transcriber import TranscriptionOptions
|
||||
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcription_options = TranscriptionOptions(
|
||||
model=TranscriptionModel(
|
||||
model_type=ModelType.HUGGING_FACE,
|
||||
hugging_face_model_id="",
|
||||
)
|
||||
)
|
||||
widget.reset_transcriber_controls()
|
||||
assert not widget.record_button.isEnabled()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_record_button_enabled_for_hugging_face_with_model_id(self, qtbot):
|
||||
from buzz.model_loader import TranscriptionModel, ModelType
|
||||
from buzz.transcriber.transcriber import TranscriptionOptions
|
||||
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcription_options = TranscriptionOptions(
|
||||
model=TranscriptionModel(
|
||||
model_type=ModelType.HUGGING_FACE,
|
||||
hugging_face_model_id="org/model",
|
||||
)
|
||||
)
|
||||
widget.reset_transcriber_controls()
|
||||
assert widget.record_button.isEnabled()
|
||||
|
||||
|
||||
|
||||
class TestOnTranscriptionOptionsChanged:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_shows_translation_box_when_llm_enabled(self, qtbot):
|
||||
from buzz.transcriber.transcriber import TranscriptionOptions
|
||||
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
options = TranscriptionOptions(enable_llm_translation=True)
|
||||
widget.on_transcription_options_changed(options)
|
||||
assert not widget.translation_text_box.isHidden()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_hides_translation_box_when_llm_disabled(self, qtbot):
|
||||
from buzz.transcriber.transcriber import TranscriptionOptions
|
||||
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.translation_text_box.show()
|
||||
options = TranscriptionOptions(enable_llm_translation=False)
|
||||
widget.on_transcription_options_changed(options)
|
||||
assert widget.translation_text_box.isHidden()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_updates_transcription_options(self, qtbot):
|
||||
from buzz.transcriber.transcriber import TranscriptionOptions
|
||||
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
options = TranscriptionOptions(silence_threshold=0.05)
|
||||
widget.on_transcription_options_changed(options)
|
||||
assert widget.transcription_options.silence_threshold == pytest.approx(0.05)
|
||||
|
||||
|
||||
|
||||
class TestOnDeviceChanged:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_no_new_listener_started_when_device_is_none(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
with patch("buzz.widgets.recording_transcriber_widget.RecordingAmplitudeListener") as MockListener:
|
||||
widget.on_device_changed(None)
|
||||
MockListener.assert_not_called()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_no_new_listener_started_when_device_is_minus_one(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
with patch("buzz.widgets.recording_transcriber_widget.RecordingAmplitudeListener") as MockListener:
|
||||
widget.on_device_changed(-1)
|
||||
MockListener.assert_not_called()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_device_id_updated(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.on_device_changed(-1)
|
||||
assert widget.selected_device_id == -1
|
||||
|
||||
|
||||
|
||||
class TestOnRecordButtonClickedStop:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_stop_path_sets_status_stopped(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.current_status = widget.RecordingStatus.RECORDING
|
||||
with patch.object(widget, "stop_recording"), \
|
||||
patch.object(widget, "set_recording_status_stopped") as mock_stop:
|
||||
widget.on_record_button_clicked()
|
||||
mock_stop.assert_called_once()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_stop_path_hides_presentation_bar(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.presentation_options_bar.show()
|
||||
widget.current_status = widget.RecordingStatus.RECORDING
|
||||
with patch.object(widget, "stop_recording"):
|
||||
widget.on_record_button_clicked()
|
||||
assert widget.presentation_options_bar.isHidden()
|
||||
|
||||
|
||||
|
||||
class TestOnModelLoaded:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_empty_model_path_calls_transcriber_error(self, qtbot):
|
||||
from buzz.model_loader import TranscriptionModel, ModelType
|
||||
from buzz.transcriber.transcriber import TranscriptionOptions
|
||||
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcription_options = TranscriptionOptions(
|
||||
model=TranscriptionModel(model_type=ModelType.FASTER_WHISPER)
|
||||
)
|
||||
with patch.object(widget, "on_transcriber_error") as mock_err, \
|
||||
patch.object(widget, "reset_recording_controls"):
|
||||
widget.on_model_loaded("")
|
||||
mock_err.assert_called_once_with("")
|
||||
|
||||
|
||||
|
||||
class TestOnTranscriberError:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_shows_message_box(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
with patch("buzz.widgets.recording_transcriber_widget.QMessageBox.critical") as mock_box, \
|
||||
patch.object(widget, "reset_record_button"), \
|
||||
patch.object(widget, "set_recording_status_stopped"), \
|
||||
patch.object(widget, "reset_recording_amplitude_listener"):
|
||||
widget.on_transcriber_error("some error")
|
||||
mock_box.assert_called_once()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_resets_record_button(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
with patch("buzz.widgets.recording_transcriber_widget.QMessageBox.critical"), \
|
||||
patch.object(widget, "set_recording_status_stopped"), \
|
||||
patch.object(widget, "reset_recording_amplitude_listener"):
|
||||
widget.on_transcriber_error("err")
|
||||
assert widget.record_button.isEnabled()
|
||||
|
||||
|
||||
|
||||
class TestOnCancelModelProgressDialog:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_cancels_model_loader(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
mock_loader = MagicMock()
|
||||
widget.model_loader = mock_loader
|
||||
with patch.object(widget, "reset_model_download"), \
|
||||
patch.object(widget, "set_recording_status_stopped"), \
|
||||
patch.object(widget, "reset_recording_amplitude_listener"):
|
||||
widget.on_cancel_model_progress_dialog()
|
||||
mock_loader.cancel.assert_called_once()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_record_button_re_enabled(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.record_button.setDisabled(True)
|
||||
widget.model_loader = None
|
||||
with patch.object(widget, "reset_model_download"), \
|
||||
patch.object(widget, "set_recording_status_stopped"), \
|
||||
patch.object(widget, "reset_recording_amplitude_listener"):
|
||||
widget.on_cancel_model_progress_dialog()
|
||||
assert widget.record_button.isEnabled()
|
||||
|
||||
|
||||
|
||||
class TestOnNextTranscriptionExport:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_append_below_writes_to_export_file(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget, tempfile.NamedTemporaryFile(
|
||||
suffix=".txt", delete=False, mode="w"
|
||||
) as f:
|
||||
export_path = f.name
|
||||
|
||||
try:
|
||||
widget.transcriber_mode = RecordingTranscriberMode.APPEND_BELOW
|
||||
widget.export_enabled = True
|
||||
widget.transcript_export_file = export_path
|
||||
widget.on_next_transcription("hello export")
|
||||
|
||||
with open(export_path) as f:
|
||||
assert "hello export" in f.read()
|
||||
finally:
|
||||
os.unlink(export_path)
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_append_above_writes_to_export_file(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget, tempfile.NamedTemporaryFile(
|
||||
suffix=".txt", delete=False, mode="w"
|
||||
) as f:
|
||||
export_path = f.name
|
||||
|
||||
try:
|
||||
widget.transcriber_mode = RecordingTranscriberMode.APPEND_ABOVE
|
||||
widget.export_enabled = True
|
||||
widget.transcript_export_file = export_path
|
||||
widget.on_next_transcription("first")
|
||||
widget.on_next_transcription("second")
|
||||
|
||||
with open(export_path) as f:
|
||||
content = f.read()
|
||||
assert "second" in content
|
||||
assert "first" in content
|
||||
# APPEND_ABOVE puts newer text first
|
||||
assert content.index("second") < content.index("first")
|
||||
finally:
|
||||
os.unlink(export_path)
|
||||
|
||||
|
||||
|
||||
class TestOnNextTranslation:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_append_below_adds_translation(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcriber_mode = RecordingTranscriberMode.APPEND_BELOW
|
||||
widget.on_next_translation("Bonjour")
|
||||
assert "Bonjour" in widget.translation_text_box.toPlainText()
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_append_above_puts_new_text_first(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcriber_mode = RecordingTranscriberMode.APPEND_ABOVE
|
||||
widget.on_next_translation("first")
|
||||
widget.on_next_translation("second")
|
||||
text = widget.translation_text_box.toPlainText()
|
||||
assert text.index("second") < text.index("first")
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_append_and_correct_merges_translation(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcriber_mode = RecordingTranscriberMode.APPEND_AND_CORRECT
|
||||
widget.on_next_translation("Hello world.")
|
||||
widget.on_next_translation("world. Goodbye.")
|
||||
text = widget.translation_text_box.toPlainText()
|
||||
assert "Hello" in text
|
||||
assert "Goodbye" in text
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_empty_translation_ignored(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcriber_mode = RecordingTranscriberMode.APPEND_BELOW
|
||||
widget.on_next_translation("")
|
||||
assert widget.translation_text_box.toPlainText() == ""
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_updates_presentation_window(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcriber_mode = RecordingTranscriberMode.APPEND_BELOW
|
||||
widget.on_show_presentation_clicked()
|
||||
widget.transcription_options.enable_llm_translation = True
|
||||
widget.on_next_translation("Translated text")
|
||||
assert "Translated text" in widget.presentation_window._current_translation
|
||||
|
||||
|
||||
|
||||
class TestExportFileHelpers:
|
||||
def test_write_creates_file(self, tmp_path):
|
||||
path = str(tmp_path / "out.txt")
|
||||
RecordingTranscriberWidget.write_to_export_file(path, "hello")
|
||||
with open(path) as f:
|
||||
assert f.read() == "hello"
|
||||
|
||||
def test_write_appends_by_default(self, tmp_path):
|
||||
path = str(tmp_path / "out.txt")
|
||||
RecordingTranscriberWidget.write_to_export_file(path, "line1")
|
||||
RecordingTranscriberWidget.write_to_export_file(path, "line2")
|
||||
with open(path) as f:
|
||||
assert f.read() == "line1line2"
|
||||
|
||||
def test_write_overwrites_with_mode_w(self, tmp_path):
|
||||
path = str(tmp_path / "out.txt")
|
||||
RecordingTranscriberWidget.write_to_export_file(path, "old", mode="w")
|
||||
RecordingTranscriberWidget.write_to_export_file(path, "new", mode="w")
|
||||
with open(path) as f:
|
||||
assert f.read() == "new"
|
||||
|
||||
def test_write_retries_on_permission_error(self, tmp_path):
|
||||
path = str(tmp_path / "out.txt")
|
||||
call_count = [0]
|
||||
original_open = open
|
||||
|
||||
def flaky_open(p, mode="r", **kwargs):
|
||||
if p == path:
|
||||
call_count[0] += 1
|
||||
if call_count[0] < 3:
|
||||
raise PermissionError("locked")
|
||||
return original_open(p, mode, **kwargs)
|
||||
|
||||
with patch("builtins.open", side_effect=flaky_open), \
|
||||
patch("time.sleep"):
|
||||
RecordingTranscriberWidget.write_to_export_file(path, "data", retries=5, delay=0)
|
||||
|
||||
assert call_count[0] == 3
|
||||
|
||||
def test_write_gives_up_after_max_retries(self, tmp_path):
|
||||
path = str(tmp_path / "out.txt")
|
||||
with patch("builtins.open", side_effect=PermissionError("locked")), \
|
||||
patch("time.sleep"):
|
||||
RecordingTranscriberWidget.write_to_export_file(path, "data", retries=3, delay=0)
|
||||
|
||||
def test_write_handles_oserror(self, tmp_path):
|
||||
path = str(tmp_path / "out.txt")
|
||||
with patch("builtins.open", side_effect=OSError("disk full")):
|
||||
RecordingTranscriberWidget.write_to_export_file(path, "data")
|
||||
|
||||
def test_read_returns_file_contents(self, tmp_path):
|
||||
path = str(tmp_path / "in.txt")
|
||||
with open(path, "w") as f:
|
||||
f.write("content")
|
||||
assert RecordingTranscriberWidget.read_export_file(path) == "content"
|
||||
|
||||
def test_read_retries_on_permission_error(self, tmp_path):
|
||||
path = str(tmp_path / "in.txt")
|
||||
with open(path, "w") as f:
|
||||
f.write("ok")
|
||||
call_count = [0]
|
||||
original_open = open
|
||||
|
||||
def flaky_open(p, mode="r", **kwargs):
|
||||
if p == path:
|
||||
call_count[0] += 1
|
||||
if call_count[0] < 2:
|
||||
raise PermissionError("locked")
|
||||
return original_open(p, mode, **kwargs)
|
||||
|
||||
with patch("builtins.open", side_effect=flaky_open), \
|
||||
patch("time.sleep"):
|
||||
result = RecordingTranscriberWidget.read_export_file(path, retries=5, delay=0)
|
||||
|
||||
assert result == "ok"
|
||||
|
||||
def test_read_returns_empty_string_on_oserror(self, tmp_path):
|
||||
path = str(tmp_path / "missing.txt")
|
||||
with patch("builtins.open", side_effect=OSError("not found")):
|
||||
assert RecordingTranscriberWidget.read_export_file(path) == ""
|
||||
|
||||
def test_read_returns_empty_string_after_max_retries(self, tmp_path):
|
||||
path = str(tmp_path / "locked.txt")
|
||||
with patch("builtins.open", side_effect=PermissionError("locked")), \
|
||||
patch("time.sleep"):
|
||||
result = RecordingTranscriberWidget.read_export_file(path, retries=2, delay=0)
|
||||
assert result == ""
|
||||
|
||||
|
||||
|
||||
class TestPresentationTranslationSync:
|
||||
@pytest.mark.timeout(60)
|
||||
def test_syncs_translation_when_llm_enabled(self, qtbot):
|
||||
with _widget_ctx(qtbot) as widget:
|
||||
widget.transcription_options.enable_llm_translation = True
|
||||
widget.translation_text_box.setPlainText("Translated content")
|
||||
widget.on_show_presentation_clicked()
|
||||
assert "Translated content" in widget.presentation_window._current_translation
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue