# Unsloth - 2x faster, 60% less VRAM LLM training and finetuning
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.

"""Regression test for #6590: modern vLLM lazy-loads its compiled extensions, so
a bare ``import vllm`` succeeds even when ``vllm._C`` (or a sibling) is ABI-broken
and ``disable_broken_vllm`` missed it. GPU-free, via a synthetic vLLM."""

from __future__ import annotations

import contextlib
import importlib.abc
import importlib.machinery
import importlib.util
import sys
import types

import pytest


_LIBCUDART_ERROR = "libcudart.so.13: cannot open shared object file: No such file or directory"


class _ExtensionLoader(importlib.abc.Loader):
    """A compiled extension that loads cleanly or fails on dlopen."""

    def __init__(self, broken, error):
        self.broken = broken
        self.error = error

    def create_module(self, spec):
        return None

    def exec_module(self, module):
        if self.broken:
            raise ImportError(self.error)


class _FakeVllmFinder(importlib.abc.MetaPathFinder):
    """Lazy vLLM: ``import vllm`` succeeds; each ``vllm._*`` ext is healthy,
    ABI-broken, or absent, as real vLLM only loads ``_C`` & friends on use."""

    def __init__(self, present, broken, error):
        self.present = present
        self.broken = broken
        self.error = error

    def find_spec(
        self,
        fullname,
        path = None,
        target = None,
    ):
        if fullname in self.present:
            return importlib.machinery.ModuleSpec(
                name = fullname,
                loader = _ExtensionLoader(broken = fullname in self.broken, error = self.error),
                is_package = False,
            )
        return None  # absent -> ModuleNotFoundError, which the guard ignores


@contextlib.contextmanager
def _fake_vllm(
    present,
    broken,
    error = _LIBCUDART_ERROR,
):
    """Install a synthetic lazy vLLM, restoring VLLM_BROKEN, find_spec,
    meta_path, and the vllm* sys.modules entries on exit."""
    from unsloth import import_fixes

    submodules = import_fixes._VLLM_COMPILED_EXTENSIONS
    saved_meta_path = list(sys.meta_path)
    saved_find_spec = importlib.util.find_spec
    saved_broken = import_fixes.VLLM_BROKEN
    saved_modules = {n: sys.modules.get(n) for n in ("vllm", *submodules)}
    try:
        import_fixes.VLLM_BROKEN = False
        fake_vllm = types.ModuleType("vllm")
        fake_vllm.__path__ = []
        fake_vllm.__spec__ = importlib.machinery.ModuleSpec("vllm", loader = None, is_package = True)
        sys.modules["vllm"] = fake_vllm
        for name in submodules:
            sys.modules.pop(name, None)
        sys.meta_path.insert(0, _FakeVllmFinder(present, broken, error))
        yield import_fixes
    finally:
        import_fixes.VLLM_BROKEN = saved_broken
        sys.meta_path[:] = saved_meta_path
        importlib.util.find_spec = saved_find_spec
        for name, module in saved_modules.items():
            if module is None:
                sys.modules.pop(name, None)
            else:
                sys.modules[name] = module


@pytest.mark.parametrize(
    "broken_ext",
    ["vllm._C", "vllm._C_stable_libtorch"],
    ids = ["core_C", "sibling_C_stable_libtorch"],
)
def test_disable_broken_vllm_detects_lazy_loaded_broken_extension(broken_ext):
    # A CUDA-major mismatch breaks every ext; whichever one loads first must trip detection.
    present = {"vllm._C", "vllm._C_stable_libtorch"}
    with _fake_vllm(present = present, broken = {broken_ext}) as import_fixes:
        detected = import_fixes.disable_broken_vllm()

        assert detected is True, (
            f"disable_broken_vllm missed an ABI-broken {broken_ext} behind a "
            "lazily-importable vllm package — issue #6590 would resurface."
        )
        assert import_fixes.VLLM_BROKEN is True
        # Once disabled, vLLM must look absent so callers fall back cleanly.
        assert importlib.util.find_spec("vllm") is None


@pytest.mark.parametrize(
    "error",
    [
        "libnccl.so.2: cannot open shared object file: No such file or directory",
        "libcuda.so.1: cannot open shared object file: No such file or directory",
    ],
    ids = ["libnccl", "libcuda"],
)
def test_disable_broken_vllm_detects_non_cudart_so_failure(error):
    # A CUDA mismatch can surface through a non-libcudart .so (libnccl, libcuda),
    # which the old libcudart/libcublas/libnvrtc allow-list let slip through.
    with _fake_vllm(present = {"vllm._C"}, broken = {"vllm._C"}, error = error) as import_fixes:
        detected = import_fixes.disable_broken_vllm()

        assert detected is True, (
            f"disable_broken_vllm missed a present-but-broken vllm._C raising "
            f"{error!r} — vLLM would be left enabled and crash later."
        )
        assert import_fixes.VLLM_BROKEN is True


@pytest.mark.parametrize(
    "present",
    [{"vllm._C"}, {"vllm._C", "vllm._C_stable_libtorch", "vllm._moe_C"}],
    ids = ["core_only", "all_present"],
)
def test_disable_broken_vllm_keeps_healthy_vllm_enabled(present):
    # Healthy install: an absent sibling (ModuleNotFoundError) or an extra present
    # ext that loads cleanly must NOT be mistaken for an ABI break.
    with _fake_vllm(present = present, broken = set()) as import_fixes:
        detected = import_fixes.disable_broken_vllm()

        assert detected is False
        assert import_fixes.VLLM_BROKEN is False
        assert importlib.util.find_spec("vllm") is not None


if __name__ == "__main__":
    raise SystemExit(pytest.main([__file__, "-v"]))
