gabriel / muse public
test_code_language_config.py python
417 lines 16.5 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for language-config machinery in muse/plugins/code/ast_parser.py.
2
3 Coverage
4 --------
5 LangSpec.name field
6 - All 14 built-in specs (13 tree-sitter + bash) have a non-empty ``name``.
7 - Names are unique across the full _TS_LANG_SPECS list.
8 - ``name`` is lowercase and contains only alphanumeric chars / hyphens.
9
10 _BASH_SPEC
11 - Extensions include .sh, .bash, .zsh, .plugin.zsh.
12 - module_name is "tree_sitter_bash".
13 - query_str captures function_definition and variable_assignment.
14 - Included in _TS_LANG_SPECS.
15
16 CodeConfig
17 - Default instance has active_languages == None (all enabled).
18 - active_languages is frozenset when set.
19
20 load_code_config
21 - Missing file → CodeConfig() defaults (active_languages is None).
22 - Valid [code] languages list → frozenset of lowercased names.
23 - Names are lowercased and stripped.
24 - Absent [code] section → defaults.
25 - Malformed TOML → defaults (no crash).
26 - languages is not a list → defaults with warning.
27 - languages contains non-string entries → defaults with warning.
28 - Empty languages list → empty frozenset (zero grammars active).
29 - [code] present but [framework_detection] absent → CodeConfig parses OK.
30 - Both sections present → CodeConfig.active_languages comes only from [code].
31
32 configure_language_filter / reset_adapter_cache
33 - Setting filter to None → _LANGUAGE_FILTER is None.
34 - Setting filter to a frozenset → _LANGUAGE_FILTER matches.
35 - reset_adapter_cache() clears _ADAPTERS_CACHE and _SEM_EXT_CACHE.
36 - After reset + filter to {"python"} → only PythonAdapter in ADAPTERS.
37 - After reset + filter to {"python", "bash"} → Python + bash adapters present
38 (bash as FallbackAdapter when tree-sitter-bash not installed, real adapter
39 when installed).
40 - After reset + filter None → all built-in adapters present.
41 - Excluded spec extensions → adapter_for_path returns FallbackAdapter.
42 - Included spec extensions → adapter_for_path returns real adapter.
43 - Built-in adapters (markdown, html, toml) filtered by name correctly.
44
45 adapter_for_path routing
46 - .sh → bash adapter (or FallbackAdapter when not installed).
47 - .zsh → bash adapter (same package).
48 - .plugin.zsh → bash adapter.
49 - .bash → bash adapter.
50 - Unknown extension → FallbackAdapter regardless of filter.
51 """
52
53 from __future__ import annotations
54
55 import pathlib
56
57 import pytest
58
59 from muse.plugins.code.ast_parser import (
60 CodeConfig,
61 FallbackAdapter,
62 HtmlAdapter,
63 MarkdownAdapter,
64 PythonAdapter,
65 TomlAdapter,
66 TreeSitterAdapter,
67 _BASH_SPEC,
68 _BUILTIN_ADAPTER_NAMES,
69 _TS_LANG_SPECS,
70 adapter_for_path,
71 configure_language_filter,
72 load_code_config,
73 reset_adapter_cache,
74 )
75
76
77 # ---------------------------------------------------------------------------
78 # Helpers
79 # ---------------------------------------------------------------------------
80
81
82 def _write_toml(tmp: pathlib.Path, content: str) -> pathlib.Path:
83 """Write *content* to ``.muse/code_config.toml`` under *tmp* and return root."""
84 muse_dir = tmp / ".muse"
85 muse_dir.mkdir()
86 (muse_dir / "code_config.toml").write_text(content, encoding="utf-8")
87 return tmp
88
89
90 # ---------------------------------------------------------------------------
91 # LangSpec.name field
92 # ---------------------------------------------------------------------------
93
94
95 class TestLangSpecName:
96 def test_all_specs_have_name(self) -> None:
97 """Every entry in _TS_LANG_SPECS has a non-empty ``name`` field."""
98 for spec in _TS_LANG_SPECS:
99 assert spec["name"], f"spec for {spec['module_name']} has empty name"
100
101 def test_names_are_unique(self) -> None:
102 """No two specs share the same ``name``."""
103 names = [s["name"] for s in _TS_LANG_SPECS]
104 assert len(names) == len(set(names)), f"duplicate names: {names}"
105
106 def test_names_are_lowercase_alphanumeric(self) -> None:
107 """Names contain only lowercase letters, digits, or hyphens."""
108 for spec in _TS_LANG_SPECS:
109 name = spec["name"]
110 assert name == name.lower(), f"{name} is not lowercase"
111 assert all(c.isalnum() or c == "-" for c in name), (
112 f"{name!r} contains invalid characters"
113 )
114
115 def test_known_names_present(self) -> None:
116 """Spot-check a representative set of names are registered."""
117 names = {s["name"] for s in _TS_LANG_SPECS}
118 for expected in ("javascript", "typescript", "tsx", "go", "rust",
119 "java", "c", "cpp", "csharp", "ruby", "kotlin",
120 "swift", "css", "bash"):
121 assert expected in names, f"expected language {expected!r} missing from _TS_LANG_SPECS"
122
123
124 # ---------------------------------------------------------------------------
125 # _BASH_SPEC
126 # ---------------------------------------------------------------------------
127
128
129 class TestBashSpec:
130 def test_extensions_include_sh(self) -> None:
131 assert ".sh" in _BASH_SPEC["extensions"]
132
133 def test_extensions_include_bash(self) -> None:
134 assert ".bash" in _BASH_SPEC["extensions"]
135
136 def test_extensions_include_zsh(self) -> None:
137 assert ".zsh" in _BASH_SPEC["extensions"]
138
139 def test_extensions_include_plugin_zsh(self) -> None:
140 assert ".plugin.zsh" in _BASH_SPEC["extensions"]
141
142 def test_module_name(self) -> None:
143 assert _BASH_SPEC["module_name"] == "tree_sitter_bash"
144
145 def test_query_captures_function_definition(self) -> None:
146 assert "function_definition" in _BASH_SPEC["query_str"]
147
148 def test_query_captures_variable_assignment(self) -> None:
149 assert "variable_assignment" in _BASH_SPEC["query_str"]
150
151 def test_name_is_bash(self) -> None:
152 assert _BASH_SPEC["name"] == "bash"
153
154 def test_in_ts_lang_specs(self) -> None:
155 assert _BASH_SPEC in _TS_LANG_SPECS
156
157 def test_no_class_node_types(self) -> None:
158 assert _BASH_SPEC["class_node_types"] == frozenset()
159
160 def test_no_receiver_capture(self) -> None:
161 assert _BASH_SPEC["receiver_capture"] == ""
162
163 def test_no_async_child(self) -> None:
164 assert _BASH_SPEC["async_node_child"] == ""
165
166
167 # ---------------------------------------------------------------------------
168 # CodeConfig
169 # ---------------------------------------------------------------------------
170
171
172 class TestCodeConfig:
173 def test_default_active_languages_is_none(self) -> None:
174 """Default CodeConfig means all installed grammars are enabled."""
175 cfg = CodeConfig()
176 assert cfg.active_languages is None
177
178 def test_active_languages_is_frozenset_when_set(self) -> None:
179 cfg = CodeConfig(active_languages=frozenset({"python", "bash"}))
180 assert isinstance(cfg.active_languages, frozenset)
181 assert "python" in cfg.active_languages
182
183 def test_empty_frozenset_is_valid(self) -> None:
184 """Empty frozenset means zero grammars — valid config (user intent)."""
185 cfg = CodeConfig(active_languages=frozenset())
186 assert cfg.active_languages == frozenset()
187
188
189 # ---------------------------------------------------------------------------
190 # load_code_config
191 # ---------------------------------------------------------------------------
192
193
194 class TestLoadCodeConfig:
195 def test_missing_file_returns_defaults(self, tmp_path: pathlib.Path) -> None:
196 cfg = load_code_config(tmp_path)
197 assert cfg.active_languages is None
198
199 def test_valid_languages_list(self, tmp_path: pathlib.Path) -> None:
200 root = _write_toml(tmp_path, '[code]\nlanguages = ["python", "typescript", "bash"]\n')
201 cfg = load_code_config(root)
202 assert cfg.active_languages == frozenset({"python", "typescript", "bash"})
203
204 def test_names_are_lowercased(self, tmp_path: pathlib.Path) -> None:
205 root = _write_toml(tmp_path, '[code]\nlanguages = ["Python", "TypeScript"]\n')
206 cfg = load_code_config(root)
207 assert cfg.active_languages == frozenset({"python", "typescript"})
208
209 def test_names_are_stripped(self, tmp_path: pathlib.Path) -> None:
210 root = _write_toml(tmp_path, '[code]\nlanguages = [" python ", " bash"]\n')
211 cfg = load_code_config(root)
212 assert cfg.active_languages == frozenset({"python", "bash"})
213
214 def test_absent_code_section_returns_defaults(self, tmp_path: pathlib.Path) -> None:
215 root = _write_toml(tmp_path, '[framework_detection]\nauto_detect = true\n')
216 cfg = load_code_config(root)
217 assert cfg.active_languages is None
218
219 def test_malformed_toml_returns_defaults(self, tmp_path: pathlib.Path) -> None:
220 muse_dir = tmp_path / ".muse"
221 muse_dir.mkdir()
222 (muse_dir / "code_config.toml").write_bytes(b"not = valid [[[toml")
223 cfg = load_code_config(tmp_path)
224 assert cfg.active_languages is None
225
226 def test_languages_not_a_list_returns_defaults(self, tmp_path: pathlib.Path) -> None:
227 root = _write_toml(tmp_path, '[code]\nlanguages = "python"\n')
228 cfg = load_code_config(root)
229 assert cfg.active_languages is None
230
231 def test_languages_with_non_string_entry_returns_defaults(
232 self, tmp_path: pathlib.Path
233 ) -> None:
234 root = _write_toml(tmp_path, '[code]\nlanguages = ["python", 42]\n')
235 cfg = load_code_config(root)
236 assert cfg.active_languages is None
237
238 def test_empty_languages_list(self, tmp_path: pathlib.Path) -> None:
239 """Empty list is valid — user explicitly wants zero grammars."""
240 root = _write_toml(tmp_path, '[code]\nlanguages = []\n')
241 cfg = load_code_config(root)
242 assert cfg.active_languages == frozenset()
243
244 def test_both_sections_present(self, tmp_path: pathlib.Path) -> None:
245 toml = (
246 '[code]\n'
247 'languages = ["python", "bash"]\n'
248 '\n'
249 '[framework_detection]\n'
250 'auto_detect = false\n'
251 )
252 root = _write_toml(tmp_path, toml)
253 cfg = load_code_config(root)
254 assert cfg.active_languages == frozenset({"python", "bash"})
255
256 def test_code_section_without_languages_key(self, tmp_path: pathlib.Path) -> None:
257 """[code] present but languages key absent → all languages enabled."""
258 root = _write_toml(tmp_path, '[code]\n# no languages key\n')
259 cfg = load_code_config(root)
260 assert cfg.active_languages is None
261
262 def test_code_section_not_a_table(self, tmp_path: pathlib.Path) -> None:
263 """If [code] is not a table (e.g. scalar), return defaults."""
264 root = _write_toml(tmp_path, 'code = "invalid"\n')
265 cfg = load_code_config(root)
266 assert cfg.active_languages is None
267
268
269 # ---------------------------------------------------------------------------
270 # configure_language_filter / reset_adapter_cache
271 # ---------------------------------------------------------------------------
272
273
274 class TestLanguageFilter:
275 """These tests manipulate module-level state — always reset before and after."""
276
277 def setup_method(self) -> None:
278 configure_language_filter(None)
279 reset_adapter_cache()
280
281 def teardown_method(self) -> None:
282 configure_language_filter(None)
283 reset_adapter_cache()
284
285 def test_default_filter_is_none(self) -> None:
286 from muse.plugins.code import ast_parser
287 assert ast_parser._LANGUAGE_FILTER is None
288
289 def test_set_filter_stored(self) -> None:
290 from muse.plugins.code import ast_parser
291 configure_language_filter(frozenset({"python"}))
292 assert ast_parser._LANGUAGE_FILTER == frozenset({"python"})
293
294 def test_reset_to_none(self) -> None:
295 from muse.plugins.code import ast_parser
296 configure_language_filter(frozenset({"python"}))
297 configure_language_filter(None)
298 assert ast_parser._LANGUAGE_FILTER is None
299
300 def test_reset_cache_clears_adapters(self) -> None:
301 from muse.plugins.code import ast_parser
302 # Build cache
303 _ = adapter_for_path("test.py")
304 assert ast_parser._ADAPTERS_CACHE is not None
305 reset_adapter_cache()
306 assert ast_parser._ADAPTERS_CACHE is None
307 assert ast_parser._SEM_EXT_CACHE is None
308
309 def test_filter_python_only_includes_python_adapter(self) -> None:
310 configure_language_filter(frozenset({"python"}))
311 # adapter_for_path triggers cache build
312 adapter = adapter_for_path("script.py")
313 assert isinstance(adapter, PythonAdapter)
314
315 def test_filter_python_only_excludes_js(self) -> None:
316 configure_language_filter(frozenset({"python"}))
317 adapter = adapter_for_path("app.js")
318 assert isinstance(adapter, FallbackAdapter)
319
320 def test_filter_none_includes_python(self) -> None:
321 configure_language_filter(None)
322 adapter = adapter_for_path("script.py")
323 assert isinstance(adapter, PythonAdapter)
324
325 def test_filter_markdown_by_name(self) -> None:
326 configure_language_filter(frozenset({"markdown"}))
327 adapter = adapter_for_path("README.md")
328 assert isinstance(adapter, MarkdownAdapter)
329
330 def test_filter_html_by_name(self) -> None:
331 configure_language_filter(frozenset({"html"}))
332 adapter = adapter_for_path("index.html")
333 assert isinstance(adapter, HtmlAdapter)
334
335 def test_filter_toml_by_name(self) -> None:
336 configure_language_filter(frozenset({"toml"}))
337 adapter = adapter_for_path("pyproject.toml")
338 assert isinstance(adapter, TomlAdapter)
339
340 def test_filter_excludes_markdown_when_not_listed(self) -> None:
341 configure_language_filter(frozenset({"python"}))
342 adapter = adapter_for_path("README.md")
343 assert isinstance(adapter, FallbackAdapter)
344
345 def test_empty_filter_excludes_everything(self) -> None:
346 configure_language_filter(frozenset())
347 for path in ("script.py", "app.js", "README.md", "index.html"):
348 assert isinstance(adapter_for_path(path), FallbackAdapter), (
349 f"expected FallbackAdapter for {path} with empty filter"
350 )
351
352 def test_builtin_adapter_names_constant(self) -> None:
353 assert "python" in _BUILTIN_ADAPTER_NAMES
354 assert "markdown" in _BUILTIN_ADAPTER_NAMES
355 assert "html" in _BUILTIN_ADAPTER_NAMES
356 assert "toml" in _BUILTIN_ADAPTER_NAMES
357
358
359 # ---------------------------------------------------------------------------
360 # adapter_for_path routing — shell extensions
361 # ---------------------------------------------------------------------------
362
363
364 class TestShellAdapterRouting:
365 """Route shell extensions to the bash adapter (or FallbackAdapter if not installed)."""
366
367 def setup_method(self) -> None:
368 configure_language_filter(None)
369 reset_adapter_cache()
370
371 def teardown_method(self) -> None:
372 configure_language_filter(None)
373 reset_adapter_cache()
374
375 @pytest.mark.parametrize("path", [
376 "install.sh",
377 "bootstrap.bash",
378 "muse.plugin.zsh",
379 "config.zsh",
380 "src/helpers.sh",
381 ])
382 def test_shell_path_does_not_route_to_python(self, path: str) -> None:
383 """Shell files must never be parsed by PythonAdapter."""
384 adapter = adapter_for_path(path)
385 assert not isinstance(adapter, PythonAdapter), (
386 f"{path} routed to PythonAdapter"
387 )
388
389 @pytest.mark.parametrize("path", [
390 "install.sh",
391 "bootstrap.bash",
392 "muse.plugin.zsh",
393 "config.zsh",
394 ])
395 def test_shell_extension_routes_to_bash_or_fallback(self, path: str) -> None:
396 """Shell paths route to BashAdapter (tree-sitter-bash installed) or FallbackAdapter."""
397 adapter = adapter_for_path(path)
398 # Accept both — test is grammar-package-agnostic.
399 assert isinstance(adapter, (TreeSitterAdapter, FallbackAdapter)), (
400 f"{path} routed to unexpected adapter type {type(adapter).__name__}"
401 )
402
403 def test_unknown_extension_is_always_fallback(self) -> None:
404 adapter = adapter_for_path("binary.exe")
405 assert isinstance(adapter, FallbackAdapter)
406
407 def test_filter_bash_routes_sh_to_bash_adapter_if_installed(self) -> None:
408 """When filter includes 'bash', .sh files get the bash adapter (if grammar installed)."""
409 configure_language_filter(frozenset({"bash"}))
410 adapter = adapter_for_path("install.sh")
411 assert isinstance(adapter, (TreeSitterAdapter, FallbackAdapter))
412
413 def test_filter_without_bash_routes_sh_to_fallback(self) -> None:
414 """When filter excludes 'bash', .sh files fall through to FallbackAdapter."""
415 configure_language_filter(frozenset({"python", "typescript"}))
416 adapter = adapter_for_path("install.sh")
417 assert isinstance(adapter, FallbackAdapter)
File History 4 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago