On hardened kernels / newer glibc (e.g. WSL2 glibc 2.43) CTranslate2's shared
object is rejected at load with 'libctranslate2…cannot enable executable stack'
— an OSError, not ImportError. The WhisperX/faster-whisper is_available() probes
only caught ImportError, so the OSError escaped and crashed the ASR/dub
preflight ('ASR backend initialization failed: …').
- Both probes now also catch the non-ImportError load failure and REPORT
(False, 'failed to load …') instead of raising — a probe must never raise.
- _auto_detect() routes every probe through a never-raising _probe_available()
so no exploding probe can crash engine selection; it falls through to
pytorch-whisper (transformers, no CTranslate2), which works on CUDA/CPU.
Regression tests cover the raising probe, the .so-load OSError surfacing as
unavailable, and auto-detect falling back to pytorch-whisper.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 lines
2.1 KiB
Python
51 lines
2.1 KiB
Python
"""Regression tests for #692 — CTranslate2's native lib is rejected by hardened
|
|
kernels / newer glibc with "libctranslate2…cannot enable executable stack" (an
|
|
OSError, not ImportError). The ASR availability probes must REPORT this rather
|
|
than raise, and engine auto-detect must fall back to a non-CTranslate2 engine
|
|
(pytorch-whisper) instead of crashing the dub/transcribe preflight.
|
|
"""
|
|
import builtins
|
|
|
|
from services import asr_backend as ab
|
|
|
|
_EXECSTACK = OSError(
|
|
"libctranslate2-d3638643.so.4.4.0: cannot enable executable stack "
|
|
"as shared object requires: Invalid argument"
|
|
)
|
|
|
|
|
|
def test_probe_available_never_raises():
|
|
class Boom:
|
|
@classmethod
|
|
def is_available(cls):
|
|
raise _EXECSTACK
|
|
assert ab._probe_available(Boom) is False
|
|
|
|
|
|
def test_auto_detect_falls_back_to_pytorch_when_ctranslate2_so_fails(monkeypatch):
|
|
def boom():
|
|
raise _EXECSTACK
|
|
monkeypatch.setattr(ab.WhisperXBackend, "is_available", classmethod(lambda cls: boom()))
|
|
monkeypatch.setattr(ab.FasterWhisperBackend, "is_available", classmethod(lambda cls: boom()))
|
|
# Pin MLX unavailable so the result is platform-independent (MPS hosts would
|
|
# otherwise probe it before the pytorch-whisper last resort).
|
|
monkeypatch.setattr(ab.MLXWhisperBackend, "is_available", classmethod(lambda cls: (False, "n/a")))
|
|
assert ab._auto_detect() == "pytorch-whisper"
|
|
|
|
|
|
def test_is_available_reports_not_raises_on_so_load_failure(monkeypatch):
|
|
"""whisperx + faster-whisper probes return (False, reason) — not raise — when
|
|
the import dies loading the native .so (a non-ImportError)."""
|
|
real_import = builtins.__import__
|
|
|
|
def fake_import(name, *args, **kwargs):
|
|
if name in ("whisperx", "faster_whisper"):
|
|
raise _EXECSTACK
|
|
return real_import(name, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(builtins, "__import__", fake_import)
|
|
okx, msgx = ab.WhisperXBackend.is_available()
|
|
okf, msgf = ab.FasterWhisperBackend.is_available()
|
|
assert okx is False and "failed to load" in msgx and "executable stack" in msgx
|
|
assert okf is False and "failed to load" in msgf and "executable stack" in msgf
|