/
/
/
1"""Tests verifying that importing vendored_clap does not pollute the process warnings filter."""
2
3from __future__ import annotations
4
5import ast
6import importlib
7import inspect
8import warnings
9from pathlib import Path
10
11import music_assistant.providers.sonic_analysis.vendored_clap.clap_wrapper as cw_module
12
13
14def test_import_does_not_add_unscoped_ignore_all_filter() -> None:
15 """
16 Importing clap_wrapper must not inject a process-wide ignore-all entry into warnings.filters.
17
18 An unscoped filterwarnings("ignore") at module level would silently suppress
19 all subsequent warnings across every other MA provider for the lifetime of the
20 process. The filter must live inside a with-catch_warnings block in load_clap().
21 """
22 filters_before = list(warnings.filters)
23
24 # Force a fresh evaluation of the module-level code. importlib.reload re-executes
25 # all module-level statements, so any bare filterwarnings("ignore") call would fire.
26 importlib.reload(cw_module)
27
28 filters_after = list(warnings.filters)
29
30 # The reload must not have added an unscoped "ignore all" entry.
31 new_entries = [f for f in filters_after if f not in filters_before]
32 unscoped_ignore_all = [
33 f
34 for f in new_entries
35 if f[0] == "ignore" and f[2] is Warning and f[1] is None and f[3] is None
36 ]
37 assert unscoped_ignore_all == [], (
38 f"clap_wrapper module-level code installed a process-wide ignore-all filter: "
39 f"{unscoped_ignore_all}. "
40 f"Use 'with warnings.catch_warnings(): warnings.filterwarnings(\"ignore\")' "
41 f"inside load_clap() instead."
42 )
43
44
45def test_no_module_level_filterwarnings_call_in_source() -> None:
46 """
47 The clap_wrapper source must not contain a bare filterwarnings call at module scope.
48
49 Parses the AST to detect any top-level Expr containing a call to
50 warnings.filterwarnings(), which would install a permanent process-wide filter.
51 """
52 clap_wrapper_path = (
53 Path(__file__).parent.parent.parent.parent
54 / "music_assistant"
55 / "providers"
56 / "sonic_analysis"
57 / "vendored_clap"
58 / "clap_wrapper.py"
59 )
60 source = clap_wrapper_path.read_text(encoding="utf-8")
61 tree = ast.parse(source)
62
63 for node in ast.iter_child_nodes(tree):
64 # We only care about top-level expression statements (bare calls)
65 if not isinstance(node, ast.Expr):
66 continue
67 call = node.value
68 if not isinstance(call, ast.Call):
69 continue
70 func = call.func
71 # Matches both `warnings.filterwarnings(...)` and bare `filterwarnings(...)`
72 is_filterwarnings = (isinstance(func, ast.Attribute) and func.attr == "filterwarnings") or (
73 isinstance(func, ast.Name) and func.id == "filterwarnings"
74 )
75 assert not is_filterwarnings, (
76 f"clap_wrapper.py has a module-level call to filterwarnings() at line {node.lineno}. "
77 f"Move it inside 'with warnings.catch_warnings():' in load_clap()."
78 )
79
80
81def test_load_clap_uses_catch_warnings_context_manager() -> None:
82 """load_clap() must wrap model-loading code in a with warnings.catch_warnings() block."""
83 source = inspect.getsource(cw_module.CLAPWrapper.load_clap)
84 assert "catch_warnings" in source, (
85 "CLAPWrapper.load_clap() does not use warnings.catch_warnings(). "
86 "The filterwarnings('ignore') call must be scoped inside that context manager."
87 )
88