import importlib.util from pathlib import Path from unittest.mock import Mock import pytest def _load_progress_module(): module_path = ( Path(__file__).resolve().parents[2] / "src" / "fenrirscreenreader" / "commands" / "onScreenUpdate" / "65000-progress_detector.py" ) spec = importlib.util.spec_from_file_location( "fenrir_progress_detector", module_path ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module @pytest.mark.unit def test_progress_detector_skips_typing_delta(): progress_module = _load_progress_module() command = progress_module.command() command.env = { "commandBuffer": {"progress_monitoring": True}, "runtime": {"DebugManager": Mock(write_debug_out=Mock())}, "screen": {"new_delta": "x", "new_delta_is_typing": True}, } command.is_current_line_prompt = Mock(return_value=False) command.is_real_progress_update = Mock(return_value=True) command.detect_progress = Mock() command.run() command.is_real_progress_update.assert_not_called() command.detect_progress.assert_not_called() @pytest.mark.unit def test_progress_detector_allows_long_tqdm_transfer_delta(): progress_module = _load_progress_module() command = progress_module.command() sample = ( "88%|" "████████████████████████████████████████████████████████████████" "████████████████████████████████████████████████████████████████" "████████████████████████████████████████████████████████████████" "█████████████████████████▊ " "| 843M/954M [00:54<00:07, 15.2MB/s]" ) command.env = { "commandBuffer": {"progress_monitoring": True}, "runtime": { "DebugManager": Mock(write_debug_out=Mock()), "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), }, "screen": { "new_delta": sample, "new_content_text": sample, "old_cursor": {"x": 0, "y": 0}, "new_cursor": {"x": 0, "y": 0}, }, } assert len(sample) > 200 assert command.is_real_progress_update() @pytest.mark.unit def test_progress_detector_beeps_for_long_tqdm_transfer_delta(): progress_module = _load_progress_module() command = progress_module.command() sample = ( "90%|" "████████████████████████████████████████████████████████████████" "████████████████████████████████████████████████████████████████" "████████████████████████████████████████████████████████████████" "█████████████████████████████████████▍ " "| 856M/954M [00:56<00:14, 6.78MB/s]" ) command.env = { "commandBuffer": { "progress_monitoring": True, "lastProgressValue": -1, "lastProgressTime": 0, }, "runtime": { "DebugManager": Mock(write_debug_out=Mock()), "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), }, "screen": { "new_delta": sample, "new_delta_is_typing": False, "new_content_text": sample, "old_cursor": {"x": 0, "y": 0}, "new_cursor": {"x": 0, "y": 0}, }, } command.play_progress_tone = Mock() command.run() command.play_progress_tone.assert_called_once_with(90.0) assert command.env["commandBuffer"]["lastProgressValue"] == 90.0 @pytest.mark.unit def test_progress_detector_beeps_for_interruptible_status_without_ellipsis(): progress_module = _load_progress_module() command = progress_module.command() sample = "◦ Files: (1m 04s • esc to interrupt)" command.env = { "commandBuffer": { "progress_monitoring": True, "lastProgressValue": -1, "lastProgressTime": 0, }, "runtime": { "DebugManager": Mock(write_debug_out=Mock()), "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), }, "screen": { "new_delta": sample, "new_delta_is_typing": False, "new_content_text": sample, "old_cursor": {"x": 0, "y": 0}, "new_cursor": {"x": 0, "y": 0}, }, } command.play_activity_beep = Mock() command.run() command.play_activity_beep.assert_called_once_with() @pytest.mark.unit def test_progress_detector_allows_long_key_value_status_delta(): progress_module = _load_progress_module() command = progress_module.command() sample = ( "frame=33480 fps=429 q=28.0 size= 195328KiB " "time=00:23:16.26 bitrate=1146.0kbits/s dup=0 drop=1 " "speed=17.9x elapsed=0:01:18.01 " "filter=scale,format output=archive-video-final-render-pass.mkv " "metadata=preserve-chapters-and-stream-layout" ) command.env = { "commandBuffer": {"progress_monitoring": True}, "runtime": { "DebugManager": Mock(write_debug_out=Mock()), "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), }, "screen": { "new_delta": sample, "new_content_text": sample, "old_cursor": {"x": 0, "y": 0}, "new_cursor": {"x": 0, "y": 0}, }, } assert len(sample) > 200 assert command.is_real_progress_update() @pytest.mark.unit def test_progress_detector_beeps_for_ffmpeg_status_line(): progress_module = _load_progress_module() command = progress_module.command() sample = ( "frame=33480 fps=429 q=28.0 size= 195328KiB " "time=00:23:16.26 bitrate=1146.0kbits/s dup=0 drop=1 " "speed=17.9x elapsed=0:01:18.01" ) command.env = { "commandBuffer": { "progress_monitoring": True, "lastProgressValue": -1, "lastProgressTime": 0, }, "runtime": { "DebugManager": Mock(write_debug_out=Mock()), "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), }, "screen": { "new_delta": sample, "new_delta_is_typing": False, "new_content_text": sample, "old_cursor": {"x": 0, "y": 0}, "new_cursor": {"x": 0, "y": 0}, }, } command.play_activity_beep = Mock() command.run() command.play_activity_beep.assert_called_once_with() @pytest.mark.unit def test_progress_detector_ignores_generic_key_value_text(): progress_module = _load_progress_module() command = progress_module.command() sample = "host=server mode=debug user=Username retry=3 status=ready" command.env = { "commandBuffer": { "progress_monitoring": True, "lastProgressValue": -1, "lastProgressTime": 0, }, "runtime": { "DebugManager": Mock(write_debug_out=Mock()), "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), }, "screen": { "new_delta": sample, "new_delta_is_typing": False, "new_content_text": sample, "old_cursor": {"x": 0, "y": 0}, "new_cursor": {"x": 0, "y": 0}, }, } command.play_activity_beep = Mock() command.play_progress_tone = Mock() command.run() command.play_activity_beep.assert_not_called() command.play_progress_tone.assert_not_called() @pytest.mark.unit def test_progress_detector_beeps_for_pacman_total_in_multiline_delta(): progress_module = _load_progress_module() command = progress_module.command() total_line = ( "Total ( 3/749) " "830.7 MiB 4.70 MiB/s 04:05 " "[#################################################" "-----------------------------------------------------------------------] 41%" ) sample = ( "package-one 125.0 MiB 2.10 MiB/s 00:12 " "[####################--------------------] 50%\n" + total_line ) command.env = { "commandBuffer": { "progress_monitoring": True, "lastProgressValue": -1, "lastProgressTime": 0, }, "runtime": { "DebugManager": Mock(write_debug_out=Mock()), "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), }, "screen": { "new_delta": sample, "new_delta_is_typing": False, "new_content_text": sample, "old_cursor": {"x": 0, "y": 0}, "new_cursor": {"x": 0, "y": 0}, }, } command.play_progress_tone = Mock() command.run() command.play_progress_tone.assert_called_once_with(41.0) assert command.env["commandBuffer"]["lastProgressValue"] == 41.0 @pytest.mark.unit def test_explicit_progress_delta_ignores_percentage_in_bracketed_prose(): progress_module = _load_progress_module() command = progress_module.command() assert not command.is_explicit_progress_delta( "Release notes [issue #749] are 41% complete" ) @pytest.mark.unit def test_progress_detector_beeps_for_curl_classic_repaint_with_trailing_text(): progress_module = _load_progress_module() command = progress_module.command() sample = ( "53 537.8M 53 285.8M 0 0 9.48M 0 00:56 00:30 00:26 9.94M " "Executing cabextract -q -d /home/Username/.local/wine64/dosdevices/" "c:/windows/syswow64 -F dx8vb.dll /home/Username/.cache/winetricks/" "dx8vb/DX81NTger.exe" ) command.env = { "commandBuffer": { "progress_monitoring": True, "lastProgressValue": -1, "lastProgressTime": 0, }, "runtime": { "DebugManager": Mock(write_debug_out=Mock()), "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), }, "screen": { "new_delta": sample, "new_delta_is_typing": False, "new_content_text": sample, "old_cursor": {"x": 0, "y": 0}, "new_cursor": {"x": 0, "y": 0}, }, } command.play_progress_tone = Mock() assert len(sample) > 200 command.run() command.play_progress_tone.assert_called_once_with(53.0) assert command.env["commandBuffer"]["lastProgressValue"] == 53.0 @pytest.mark.unit def test_progress_detector_ignores_curl_like_fields_with_mismatched_percentages(): progress_module = _load_progress_module() command = progress_module.command() sample = ( "53 537.8M 52 285.8M 0 0 9.48M 0 00:56 00:30 00:26 9.94M " "ordinary numeric report with enough trailing text to cross the long " "delta filter without representing a coherent download progress row " "or providing trustworthy percentage information" ) command.env = { "commandBuffer": { "progress_monitoring": True, "lastProgressValue": -1, "lastProgressTime": 0, }, "runtime": { "DebugManager": Mock(write_debug_out=Mock()), "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), }, "screen": { "new_delta": sample, "new_delta_is_typing": False, "new_content_text": sample, "old_cursor": {"x": 0, "y": 0}, "new_cursor": {"x": 0, "y": 0}, }, } command.play_activity_beep = Mock() command.play_progress_tone = Mock() assert len(sample) > 200 command.run() command.play_activity_beep.assert_not_called() command.play_progress_tone.assert_not_called()