Skip to content
Merged
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
36 changes: 33 additions & 3 deletions matlab/tests/run_matlab_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"jitter": "run_jitter_load",
}

_WINDOWS_SCRIPT_SUFFIXES = {".bat", ".cmd"}


def _repo_root() -> Path:
return Path(__file__).resolve().parents[2]
Expand Down Expand Up @@ -52,8 +54,30 @@ def _standard_install_candidates() -> list[Path]:
return candidates


def _has_windows_pe_signature(path: Path) -> bool:
try:
with path.open("rb") as handle:
dos_header = handle.read(64)
if len(dos_header) < 64 or dos_header[:2] != b"MZ":
return False
pe_offset = int.from_bytes(dos_header[0x3C:0x40], byteorder="little")
if pe_offset < 64:
return False
handle.seek(pe_offset)
return handle.read(4) == b"PE\0\0"
except OSError:
return False


def _is_executable_file(path: Path) -> bool:
return path.is_file() and os.access(path, os.X_OK)
if not path.is_file():
return False
if os.name == "nt":
suffix = path.suffix.lower()
if suffix in _WINDOWS_SCRIPT_SUFFIXES:
return True
return suffix == ".exe" and _has_windows_pe_signature(path)
return os.access(path, os.X_OK)


def _resolve_matlab_candidate(candidate: str) -> Path | None:
Expand All @@ -63,7 +87,9 @@ def _resolve_matlab_candidate(candidate: str) -> Path | None:

resolved = shutil.which(candidate)
if resolved:
return Path(resolved)
resolved_path = Path(resolved)
if _is_executable_file(resolved_path):
return resolved_path

return None

Expand Down Expand Up @@ -162,7 +188,11 @@ def main(argv: list[str] | None = None) -> int:
if args.dry_run:
return 0

completed = subprocess.run(command, cwd=_repo_root(), check=False)
try:
completed = subprocess.run(command, cwd=_repo_root(), check=False)
except OSError as exc:
print(f"Failed to execute MATLAB executable {executable}: {exc}", file=sys.stderr)
return INVALID_MATLAB_EXECUTABLE_EXIT_CODE
return completed.returncode


Expand Down
44 changes: 44 additions & 0 deletions python/tests/unit/test_matlab_test_runner.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import importlib.util
import os
import shutil
import sys
from pathlib import Path
from types import SimpleNamespace


def _make_executable(path: Path) -> Path:
if os.name == "nt":
path = path.with_suffix(".exe")
shutil.copyfile(sys.executable, path)
return path
path.write_text("")
path.chmod(0o755)
return path
Expand Down Expand Up @@ -69,6 +76,28 @@ def test_explicit_non_executable_matlab_path_is_rejected(tmp_path, capsys):
assert "not found or is not executable" in capsys.readouterr().err


def test_explicit_invalid_windows_exe_is_rejected(monkeypatch, tmp_path, capsys):
runner = _load_runner()
monkeypatch.setattr(runner.os, "name", "nt")
executable = tmp_path / "matlab.exe"
executable.write_text("")

exit_code = runner.main(["common", "--matlab-executable", str(executable), "--dry-run"])

assert exit_code == runner.INVALID_MATLAB_EXECUTABLE_EXIT_CODE
assert "not found or is not executable" in capsys.readouterr().err


def test_which_result_is_rechecked_before_accepting(monkeypatch, tmp_path):
runner = _load_runner()
monkeypatch.setattr(runner.os, "name", "nt")
executable = tmp_path / "matlab.exe"
executable.write_text("")
monkeypatch.setattr(runner.shutil, "which", lambda _name: str(executable))

assert runner.find_matlab_executable("matlab") is None


def test_dry_run_prints_command_without_executing(tmp_path, capsys):
runner = _load_runner()
executable = _make_executable(tmp_path / "matlab")
Expand Down Expand Up @@ -99,6 +128,21 @@ def fake_print(*args, **kwargs):
assert "run_common" in print_calls[0][0][0]


def test_oserror_while_running_returns_invalid_executable(monkeypatch, tmp_path, capsys):
runner = _load_runner()
executable = _make_executable(tmp_path / "matlab")
monkeypatch.setattr(
runner.subprocess,
"run",
lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("cannot execute")),
)

exit_code = runner.main(["common", "--matlab-executable", str(executable)])

assert exit_code == runner.INVALID_MATLAB_EXECUTABLE_EXIT_CODE
assert "Failed to execute MATLAB executable" in capsys.readouterr().err


def test_build_command_escapes_matlab_root(tmp_path):
runner = _load_runner()
executable = tmp_path / "matlab"
Expand Down