litellm/tests/code_coverage_tests/code_qa_check_tests.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

181 lines
6.4 KiB
Python

import ast
import os
def check_for_litellm_module_deletion(base_dir):
"""
Checks for code patterns that delete litellm modules from sys.modules
in the test_litellm directory.
Specifically looks for patterns like:
for module in list(sys.modules.keys()):
if module.startswith("litellm"):
del sys.modules[module]
"""
problematic_files = []
candidate_dirs = [os.path.join(base_dir, name) for name in ("test_litellm", "unit")]
test_dirs = [test_dir for test_dir in candidate_dirs if os.path.exists(test_dir)]
if not test_dirs:
print(f"Warning: None of {candidate_dirs} exist.")
return []
print(f"Checking directories: {test_dirs}")
for root, _, files in (entry for test_dir in test_dirs for entry in os.walk(test_dir)):
for file in files:
if file.endswith(".py"):
file_path = os.path.join(root, file)
try:
with open(file_path, "r") as f:
tree = ast.parse(f.read())
except SyntaxError:
print(f"Warning: Syntax error in file {file_path}")
continue
# Check for litellm module deletion patterns
if has_litellm_module_deletion(tree):
relative_path = os.path.relpath(file_path, base_dir)
problematic_files.append(relative_path)
print(f"Found litellm module deletion in: {relative_path}")
return problematic_files
def has_litellm_module_deletion(tree):
"""
Checks if the AST contains patterns that delete litellm modules from sys.modules.
Looks for:
1. Loops over sys.modules.keys()
2. Conditions checking if module startswith "litellm"
3. del sys.modules[module] statements
"""
class LiteLLMDeletionVisitor(ast.NodeVisitor):
def __init__(self):
self.has_sys_modules_loop = False
self.has_litellm_check = False
self.has_del_sys_modules = False
self.current_for_target = None
def visit_For(self, node):
# Check if we're looping over sys.modules.keys()
if (
isinstance(node.iter, ast.Call)
and isinstance(node.iter.func, ast.Attribute)
and isinstance(node.iter.func.value, ast.Attribute)
and isinstance(node.iter.func.value.value, ast.Name)
and node.iter.func.value.value.id == "sys"
and node.iter.func.value.attr == "modules"
and node.iter.func.attr == "keys"
):
self.has_sys_modules_loop = True
if isinstance(node.target, ast.Name):
self.current_for_target = node.target.id
# Check the body of the for loop
for stmt in node.body:
self.visit(stmt)
# Also check for list(sys.modules.keys()) pattern
elif (
isinstance(node.iter, ast.Call)
and isinstance(node.iter.func, ast.Name)
and node.iter.func.id == "list"
and len(node.iter.args) == 1
and isinstance(node.iter.args[0], ast.Call)
and isinstance(node.iter.args[0].func, ast.Attribute)
and isinstance(node.iter.args[0].func.value, ast.Attribute)
and isinstance(node.iter.args[0].func.value.value, ast.Name)
and node.iter.args[0].func.value.value.id == "sys"
and node.iter.args[0].func.value.attr == "modules"
and node.iter.args[0].func.attr == "keys"
):
self.has_sys_modules_loop = True
if isinstance(node.target, ast.Name):
self.current_for_target = node.target.id
# Check the body of the for loop
for stmt in node.body:
self.visit(stmt)
self.generic_visit(node)
def visit_If(self, node):
# Check for conditions like module.startswith("litellm")
if (
isinstance(node.test, ast.Call)
and isinstance(node.test.func, ast.Attribute)
and isinstance(node.test.func.value, ast.Name)
and node.test.func.value.id == self.current_for_target
and node.test.func.attr == "startswith"
and len(node.test.args) == 1
and isinstance(node.test.args[0], ast.Constant)
and node.test.args[0].value == "litellm"
):
self.has_litellm_check = True
# Check the body of the if statement
for stmt in node.body:
self.visit(stmt)
self.generic_visit(node)
def visit_Delete(self, node):
# Check for del sys.modules[module]
for target in node.targets:
if (
isinstance(target, ast.Subscript)
and isinstance(target.value, ast.Attribute)
and isinstance(target.value.value, ast.Name)
and target.value.value.id == "sys"
and target.value.attr == "modules"
and isinstance(target.slice, ast.Name)
and target.slice.id == self.current_for_target
):
self.has_del_sys_modules = True
self.generic_visit(node)
visitor = LiteLLMDeletionVisitor()
visitor.visit(tree)
return (
visitor.has_sys_modules_loop
and visitor.has_litellm_check
and visitor.has_del_sys_modules
)
def main():
"""
Main function to check for litellm module deletion patterns in test files.
"""
# local dir
# tests_dir = "../../tests/"
# ci/cd dir
tests_dir = "./tests/"
problematic_files = check_for_litellm_module_deletion(tests_dir)
if problematic_files:
print("\nERROR: Found files that delete litellm modules from sys.modules:")
for file_path in problematic_files:
print(f" - {file_path}")
raise Exception(
f"Found {len(problematic_files)} file(s) that delete litellm modules from sys.modules. "
f"This can cause import issues and test failures. Files: {problematic_files}"
)
else:
print("✓ No litellm module deletion patterns found in tests/test_litellm or tests/unit.")
if __name__ == "__main__":
main()