Skip to content

Commit ceecbf1

Browse files
yannrichetclaude
andcommitted
test: add 14 Windows mock-based negative tests + dedicated Windows CI workflow
Add TestWindowsShErrorReporting class with mock-based tests covering missing bash, missing PATH commands, CRLF line endings, chmod no-op, and WinError codes. Add windows-error-reporting.yml CI with two jobs: full MSYS2 and mock-only (no bash required). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1533d46 commit ceecbf1

2 files changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Windows Error Reporting
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
workflow_dispatch:
9+
10+
jobs:
11+
windows-with-msys2:
12+
name: Windows + MSYS2 (full error reporting tests)
13+
runs-on: windows-latest
14+
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Python 3.12
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: '3.12'
23+
24+
- name: Install MSYS2 with bash and Unix utilities
25+
shell: pwsh
26+
run: |
27+
Write-Host "Installing MSYS2 with bash and Unix utilities..."
28+
choco install msys2 -y --params="/NoUpdate"
29+
30+
$env:MSYSTEM = "MSYS"
31+
C:\msys64\usr\bin\bash.exe -lc "pacman -Sy --noconfirm"
32+
C:\msys64\usr\bin\bash.exe -lc "pacman -S --noconfirm bash grep gawk sed bc coreutils"
33+
34+
# Add MSYS2 bin directory to PATH
35+
$env:PATH = "C:\msys64\usr\bin;$env:PATH"
36+
echo "C:\msys64\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
37+
Write-Host "MSYS2 installation complete"
38+
39+
- name: Install Python dependencies
40+
run: |
41+
python -m pip install --upgrade pip
42+
pip install -e .
43+
pip install pandas pytest getpass4
44+
45+
- name: Run all error reporting tests
46+
run: |
47+
pytest tests/test_error_reporting.py -v --tb=long
48+
49+
windows-without-msys2:
50+
name: Windows without MSYS2 (mock-based tests only)
51+
runs-on: windows-latest
52+
53+
steps:
54+
- name: Checkout code
55+
uses: actions/checkout@v4
56+
57+
- name: Set up Python 3.12
58+
uses: actions/setup-python@v5
59+
with:
60+
python-version: '3.12'
61+
62+
- name: Install Python dependencies
63+
run: |
64+
python -m pip install --upgrade pip
65+
pip install -e .
66+
pip install pandas pytest getpass4
67+
68+
- name: Run mock-based error reporting tests (no bash required)
69+
run: |
70+
pytest tests/test_error_reporting.py -v --tb=long -k "TestClassifyError or TestShClassifier or TestSshClassifier or TestSlurmClassifier or TestFunzClassifier or TestCommonClassifier or TestClassifyErrorDispatch or TestClassifyErrorNegative or TestWindowsShErrorReporting or TestCLIFormatOutput"

tests/test_error_reporting.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,172 @@ def test_fzr_success_no_error_in_info(self, tmp_path):
683683
assert "Error:" not in history_content
684684

685685

686+
# ===========================================================================
687+
# 1c. Windows-specific mock-based negative tests
688+
# ===========================================================================
689+
690+
class TestWindowsShErrorReporting:
691+
"""Mock-based tests simulating Windows failure modes.
692+
693+
These tests use unittest.mock to simulate Windows-specific scenarios
694+
(missing bash, line endings, chmod no-op, WinError codes) without
695+
needing an actual Windows machine. They verify correct classification
696+
AND that wrong labels are NOT applied.
697+
"""
698+
699+
# --- A. Missing bash on Windows ---
700+
701+
def test_windows_no_bash_reports_helpful_error(self):
702+
"""WinError 2 stderr should mention MSYS2 or FZ_SHELL_PATH."""
703+
msg = _classify_sh_error(
704+
"[WinError 2] The system cannot find the file specified",
705+
1, "script.sh",
706+
)
707+
assert msg is not None
708+
assert "msys2" in msg.lower() or "fz_shell_path" in msg.lower()
709+
710+
def test_windows_no_bash_does_not_say_permission(self):
711+
"""Missing bash should NOT be classified as permission denied."""
712+
msg = _classify_sh_error(
713+
"[WinError 2] The system cannot find the file specified",
714+
1, "script.sh",
715+
)
716+
assert msg is not None
717+
assert "permission denied" not in msg.lower()
718+
719+
def test_windows_no_bash_does_not_say_remote(self):
720+
"""Missing bash on Windows should NOT say remote."""
721+
msg = classify_error(
722+
"[WinError 2] The system cannot find the file specified",
723+
exit_code=1, command="script.sh", protocol="sh",
724+
)
725+
assert "remote" not in msg.lower()
726+
727+
# --- B. Missing commands in PATH (MSYS2 installed but incomplete) ---
728+
729+
def test_windows_grep_not_found_reports_command_not_found(self):
730+
"""'grep' not recognized on Windows should say 'command not found locally'."""
731+
msg = classify_error(
732+
"'grep' is not recognized as an internal or external command",
733+
exit_code=1, command="grep pattern file.txt", protocol="sh",
734+
)
735+
assert "command not found" in msg.lower() or "not found locally" in msg.lower()
736+
737+
def test_windows_awk_not_found_reports_command_not_found(self):
738+
"""'awk' not recognized on Windows should say 'command not found locally'."""
739+
msg = classify_error(
740+
"'awk' is not recognized as an internal or external command",
741+
exit_code=1, command="awk '{print $1}' file.txt", protocol="sh",
742+
)
743+
assert "command not found" in msg.lower() or "not found locally" in msg.lower()
744+
745+
def test_windows_missing_command_does_not_say_line_ending(self):
746+
"""Missing command should NOT be classified as line ending error."""
747+
msg = classify_error(
748+
"'grep' is not recognized as an internal or external command",
749+
exit_code=1, command="grep pattern file.txt", protocol="sh",
750+
)
751+
assert "line ending" not in msg.lower()
752+
753+
# --- C. Line endings (scripts created on Windows) ---
754+
755+
def test_windows_crlf_reports_line_ending_not_command_not_found(self):
756+
"""'\\r: command not found' should say 'line ending', NOT 'command not found locally'."""
757+
msg = classify_error(
758+
"\r: command not found", exit_code=127,
759+
command="script.sh", protocol="sh",
760+
)
761+
assert "line ending" in msg.lower()
762+
assert "command not found locally" not in msg.lower()
763+
764+
def test_windows_bad_interpreter_reports_line_ending_not_permission(self):
765+
"""'bad interpreter' should say 'line ending', NOT 'permission denied'."""
766+
msg = classify_error(
767+
"bad interpreter: No such file or directory", exit_code=126,
768+
command="script.sh", protocol="sh",
769+
)
770+
assert "line ending" in msg.lower()
771+
assert "permission denied" not in msg.lower()
772+
773+
# --- D. chmod no-op on Windows ---
774+
775+
@patch("fz.runners.platform.system", return_value="Windows")
776+
def test_windows_exit_126_reports_chmod_noop(self, mock_platform):
777+
"""Exit code 126 on Windows should mention 'chmod +x is a no-op'."""
778+
msg = _classify_sh_error("", 126, "./script.sh")
779+
assert msg is not None
780+
assert "chmod" in msg.lower() or "no-op" in msg.lower()
781+
assert "bash" in msg.lower()
782+
783+
@patch("fz.runners.platform.system", return_value="Windows")
784+
def test_windows_exit_126_does_not_say_remote(self, mock_platform):
785+
"""Exit 126 on Windows should NOT say 'remote'."""
786+
msg = _classify_sh_error("", 126, "./script.sh")
787+
assert msg is not None
788+
assert "remote" not in msg.lower()
789+
790+
# --- E. WinError codes ---
791+
792+
def test_windows_winerror2_not_misclassified_as_syntax(self):
793+
"""WinError 2 should NOT be classified as syntax error."""
794+
msg = _classify_sh_error(
795+
"[WinError 2] The system cannot find the file specified",
796+
1, "script.sh",
797+
)
798+
assert msg is not None
799+
assert "syntax error" not in msg.lower()
800+
801+
def test_windows_winerror193_not_misclassified_as_oom(self):
802+
"""WinError 193 should NOT be classified as OOM."""
803+
msg = classify_error(
804+
"[WinError 193] %1 is not a valid Win32 application",
805+
exit_code=1, command="script.sh", protocol="sh",
806+
)
807+
assert "memory" not in msg.lower()
808+
assert "oom" not in msg.lower()
809+
810+
# --- F. End-to-end mock with run_local_calculation ---
811+
812+
@patch("fz.runners.platform.system", return_value="Windows")
813+
@patch("fz.runners.run_command")
814+
def test_run_local_calc_windows_no_bash_error_in_result(
815+
self, mock_run_cmd, mock_platform, input_dir, simple_model
816+
):
817+
"""Mock Windows + no bash → result['error'] should be descriptive."""
818+
mock_run_cmd.side_effect = FileNotFoundError(
819+
"[WinError 2] The system cannot find the file specified"
820+
)
821+
result = run_local_calculation(
822+
working_dir=input_dir,
823+
command="script.sh",
824+
model=simple_model,
825+
timeout=10,
826+
original_cwd=str(input_dir),
827+
input_files_list=["input.txt"],
828+
)
829+
assert "error" in result
830+
assert len(result["error"]) > 10
831+
832+
@patch("fz.runners.platform.system", return_value="Windows")
833+
@patch("fz.runners.run_command")
834+
def test_run_local_calc_windows_no_bash_not_done(
835+
self, mock_run_cmd, mock_platform, input_dir, simple_model
836+
):
837+
"""Mock Windows + no bash → result['status'] should NOT be 'done'."""
838+
mock_run_cmd.side_effect = FileNotFoundError(
839+
"[WinError 2] The system cannot find the file specified"
840+
)
841+
result = run_local_calculation(
842+
working_dir=input_dir,
843+
command="script.sh",
844+
model=simple_model,
845+
timeout=10,
846+
original_cwd=str(input_dir),
847+
input_files_list=["input.txt"],
848+
)
849+
assert result["status"] != "done"
850+
851+
686852
# ===========================================================================
687853
# 2. sh:// (local shell) error reporting
688854
# ===========================================================================

0 commit comments

Comments
 (0)