litellm/tests/unit/test_check_licenses.py
yuneng-jiang f6882246d4
test: move tests/test_litellm root and small trees into tests/unit (#43186)
* ci: run the unit_selection.sh shard files on every event instead of only fork pull requests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ci: rename fork-flag to unit-flag now that it applies on every event

* test: move tests/test_litellm root and small trees into tests/unit

Pure renames, no content changes. Follow-up commits in this PR fix
references, merge the three files that already existed in tests/unit,
keep live-provider tests in tests/test_litellm and wire CI.

* test: carry tests/test_litellm conftest isolation into tests/unit

Callback lists, routing fallbacks, cached HTTP clients, logger state, AWS,
proxy-URL and keychain env, and session-end client cleanup now reset for
unit tests too. The environment isolation owns its MonkeyPatch so a test's
own monkeypatch is undone before the model-cost teardown runs.

* test: merge, split and prune the moved root and small-tree tests

Merge batches/test_batch_utils.py and the chat_completions and messages
dispatch tests into the files that already existed in tests/unit. Keep
the live Gemini interactions tests, the async image-fetch format test and
the OpenAI embedding scorer test in tests/test_litellm since they need
real network or keys. Put test_router.py under tests/unit/test_router so
the existing package no longer shadows it. Delete eight tests the audit
found superseded by stronger ones kept in this move.

* ci: run the moved root and small-tree tests under their legacy flags

Add the misc and responses-caching-types flags to unit_selection.sh and
CircleCI, extend enterprise-routing and mcp-integration, and point the
legacy GHA shards, Makefile, redis-compat workflow, merge smoke manifest
and change classifier at the new paths.

* test: make the new tests/unit directories packages

tests/unit/test_package_layout.py requires every directory to carry an
__init__.py, and without one the moved and retained
test_litellm_responses_bridge.py modules collide on import.

* test: scope the unit socket block to tests/unit in shared sessions

The GHA shards collect the legacy test-path and the unit selection in one
pytest session. The unit conftest's loopback-only block leaked into legacy
modules that reach the network at import. The legacy conftest now lifts the
restriction at collect and setup time, and the unit conftest re-applies it
when collecting its own modules.

* test: give the shard-script tests their own GITHUB_OUTPUT

They only passed where the runner set it. The CircleCI unit job's env
allowlist drops it, so the script's redirect failed there.

* test: point the router and module-deletion checks at tests/unit

router_code_coverage and code_qa_check_tests only searched tests/test_litellm,
so the moved router tests no longer counted. The two silent-experiment tests
the audit deleted were the only direct callers of those methods; they are
replaced with tests that assert the forwarded shadow request and the
recursion guard.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-25 11:30:43 -07:00

282 lines
9 KiB
Python

"""Tests for the dependency license checker at tests/code_coverage_tests/check_licenses.py.
Focus: PEP 639 license metadata. Packages that adopt PEP 639 publish their
license as an SPDX expression in ``info.license_expression`` and often leave the
legacy ``info.license`` field null, so the checker must read the new field (and
fall back to trove classifiers) instead of reporting "Unknown license".
PyPI HTTP responses are mocked — these tests never hit the network.
"""
import os
import sys
from pathlib import Path
import requests
_CODE_COVERAGE_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "code_coverage_tests"
)
sys.path.insert(0, _CODE_COVERAGE_DIR)
import check_licenses # noqa: E402
_LICCHECK_INI = Path(_CODE_COVERAGE_DIR) / "liccheck.ini"
class _FakeResponse:
def __init__(self, payload):
self._payload = payload
def raise_for_status(self):
return None
def json(self):
return self._payload
def _make_checker():
return check_licenses.LicenseChecker(config_file=_LICCHECK_INI)
def _patch_pypi(monkeypatch, info):
"""Make PyPI return a JSON response with the given ``info`` block."""
def _fake_get(url, timeout=None):
return _FakeResponse({"info": info})
monkeypatch.setattr(check_licenses.requests, "get", _fake_get)
# --------------------------------------------------------------------------
# get_package_license_from_pypi: license metadata resolution
# --------------------------------------------------------------------------
def test_get_license_prefers_license_expression(monkeypatch):
"""(a) PEP 639 packages publish the SPDX expression in license_expression."""
_patch_pypi(
monkeypatch,
{"license_expression": "MIT", "license": None, "classifiers": []},
)
checker = _make_checker()
assert checker.get_package_license_from_pypi("black", "26.3.1") == "MIT"
def test_license_expression_wins_when_both_present(monkeypatch):
"""license_expression takes precedence over the legacy license field."""
_patch_pypi(
monkeypatch,
{"license_expression": "Apache-2.0", "license": "stale free text"},
)
checker = _make_checker()
assert checker.get_package_license_from_pypi("pkg", "1.0.0") == "Apache-2.0"
def test_get_license_falls_back_to_legacy_license(monkeypatch):
"""(b) Pre-PEP-639 packages only set the legacy free-text license field."""
_patch_pypi(
monkeypatch,
{"license_expression": None, "license": "MIT License", "classifiers": []},
)
checker = _make_checker()
assert checker.get_package_license_from_pypi("pkg", "1.0.0") == "MIT License"
def test_get_license_falls_back_to_classifiers(monkeypatch):
"""(c) Some packages express the license only through trove classifiers."""
_patch_pypi(
monkeypatch,
{
"license_expression": None,
"license": None,
"classifiers": [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
],
},
)
checker = _make_checker()
assert (
checker.get_package_license_from_pypi("pkg", "1.0.0")
== "Apache Software License"
)
def test_get_license_returns_none_when_unset(monkeypatch):
"""(d) With no license metadata at all the license stays unknown."""
_patch_pypi(
monkeypatch,
{"license_expression": None, "license": None, "classifiers": []},
)
checker = _make_checker()
assert checker.get_package_license_from_pypi("pkg", "1.0.0") is None
def test_get_license_returns_none_on_request_failure(monkeypatch):
"""Network/HTTP failures are swallowed and reported as unknown."""
def _boom(url, timeout=None):
raise RuntimeError("network down")
monkeypatch.setattr(check_licenses.requests, "get", _boom)
checker = _make_checker()
assert checker.get_package_license_from_pypi("pkg", "1.0.0") is None
def test_get_license_retries_connection_error_then_resolves_license():
responses = iter(
(
requests.ConnectionError("connection reset"),
requests.ConnectionError("connection reset"),
_FakeResponse({"info": {"license_expression": "MIT"}}),
)
)
calls = []
sleeps = []
def _fake_get(url, timeout=None):
calls.append((url, timeout))
response = next(responses)
if isinstance(response, Exception):
raise response
return response
checker = check_licenses.LicenseChecker(
config_file=_LICCHECK_INI,
http_get=_fake_get,
sleep=sleeps.append,
)
assert checker.get_package_license_from_pypi("pkg", "1.0.0") == "MIT"
assert len(calls) == 3
assert len(sleeps) == 2
def test_get_license_does_not_retry_not_found_http_error():
response = requests.Response()
response.status_code = 404
calls = []
sleeps = []
def _fake_get(url, timeout=None):
calls.append((url, timeout))
raise requests.HTTPError("not found", response=response)
checker = check_licenses.LicenseChecker(
config_file=_LICCHECK_INI,
http_get=_fake_get,
sleep=sleeps.append,
)
assert checker.get_package_license_from_pypi("pkg", "1.0.0") is None
assert len(calls) == 1
assert sleeps == []
def test_get_license_returns_none_after_connection_retry_limit():
calls = []
sleeps = []
def _fake_get(url, timeout=None):
calls.append((url, timeout))
raise requests.ConnectionError("connection reset")
checker = check_licenses.LicenseChecker(
config_file=_LICCHECK_INI,
http_get=_fake_get,
sleep=sleeps.append,
)
assert checker.get_package_license_from_pypi("pkg", "1.0.0") is None
assert len(calls) == 3
assert len(sleeps) == 2
# --------------------------------------------------------------------------
# is_license_acceptable: SPDX identifiers and compound expressions
# --------------------------------------------------------------------------
def test_spdx_identifiers_are_authorized():
"""Plain SPDX identifiers match the legacy-spelled authorized list as-is."""
checker = _make_checker()
for identifier in ("MIT", "Apache-2.0", "BSD-3-Clause"):
is_ok, reason = checker.is_license_acceptable(identifier)
assert is_ok is True, f"{identifier}: {reason}"
def test_spdx_compound_or_expression_is_authorized():
checker = _make_checker()
is_ok, reason = checker.is_license_acceptable("MIT OR Apache-2.0")
assert is_ok is True, reason
def test_spdx_with_exception_in_compound_is_authorized():
"""The 'WITH <exception>' suffix is stripped; the base license is checked."""
checker = _make_checker()
is_ok, reason = checker.is_license_acceptable(
"Apache-2.0 WITH LLVM-exception OR MIT"
)
assert is_ok is True, reason
def test_spdx_gpl3_is_rejected():
"""GPL-3.0 spellings must fail — they match no authorized license."""
checker = _make_checker()
for expr in ("GPL-3.0-only", "GPL-3.0-or-later"):
is_ok, reason = checker.is_license_acceptable(expr)
assert is_ok is False, f"{expr} unexpectedly accepted: {reason}"
def test_spdx_compound_with_copyleft_component_is_rejected():
"""A permissive-OR-copyleft expression is conservatively rejected."""
checker = _make_checker()
is_ok, _ = checker.is_license_acceptable("MIT OR GPL-3.0-only")
assert is_ok is False
def test_or_later_identifier_is_not_split_as_operator():
"""The lowercase '-or-later' inside an identifier is not the SPDX OR operator."""
assert (
check_licenses.LicenseChecker._split_spdx_expression("GPL-2.0-or-later") is None
)
def test_free_text_license_is_not_treated_as_spdx():
"""Free-text license blobs fall back to whole-string substring matching."""
free_text = "MIT License AND additional redistribution permissions"
assert check_licenses.LicenseChecker._split_spdx_expression(free_text) is None
checker = _make_checker()
assert checker.is_license_acceptable(free_text)[0] is True
def test_unknown_license_is_reported():
checker = _make_checker()
is_ok, reason = checker.is_license_acceptable(None)
assert is_ok is False
assert reason == "Unknown license"
# --------------------------------------------------------------------------
# check_package: end-to-end resolution + acceptability
# --------------------------------------------------------------------------
def test_check_package_accepts_pep639_package(monkeypatch):
"""A PEP 639 package whose license lives only in license_expression passes."""
_patch_pypi(
monkeypatch,
{"license_expression": "MIT", "license": None, "classifiers": []},
)
checker = _make_checker()
assert checker.check_package("some-pep639-pkg", "1.0.0") is True
def test_check_package_rejects_package_without_license(monkeypatch):
_patch_pypi(
monkeypatch,
{"license_expression": None, "license": None, "classifiers": []},
)
checker = _make_checker()
assert checker.check_package("mystery-pkg", "1.0.0") is False