Skip to content

Commit 1f938b2

Browse files
author
William Yang
committed
test: Add unit tests for language detection and ChecksManager
- TestDetectLanguage: 13 tests covering all detection scenarios - TestLanguageExtensions: 2 tests for extension mapping - TestChecksManager: 9 tests for caching logic - TestChecksManagerIntegration: 1 test for batch download Total: 25 new tests (41 total unit tests)
1 parent bea02e4 commit 1f938b2

File tree

1 file changed

+287
-0
lines changed

1 file changed

+287
-0
lines changed

tests/unit/test_checks.py

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
"""
2+
Unit tests for language detection and checks management.
3+
"""
4+
5+
import base64
6+
import shutil
7+
import tempfile
8+
import time
9+
import unittest
10+
from pathlib import Path
11+
from unittest.mock import patch, MagicMock
12+
13+
from bootcs.__main__ import detect_language, LANGUAGE_EXTENSIONS
14+
from bootcs.api.checks import ChecksManager, CACHE_TTL
15+
from bootcs.api.client import APIError
16+
17+
18+
class TestDetectLanguage(unittest.TestCase):
19+
"""Test language detection from files."""
20+
21+
def setUp(self):
22+
"""Set up test fixtures."""
23+
self.temp_dir = tempfile.TemporaryDirectory()
24+
self.root = Path(self.temp_dir.name)
25+
26+
def tearDown(self):
27+
"""Clean up test fixtures."""
28+
self.temp_dir.cleanup()
29+
30+
def test_explicit_override(self):
31+
"""Explicit language always wins."""
32+
(self.root / "hello.c").touch()
33+
lang = detect_language(self.root, explicit="python")
34+
self.assertEqual(lang, "python")
35+
36+
def test_detect_c(self):
37+
"""Detect C from .c file."""
38+
(self.root / "main.c").touch()
39+
lang = detect_language(self.root)
40+
self.assertEqual(lang, "c")
41+
42+
def test_detect_c_header(self):
43+
"""Detect C from .h file."""
44+
(self.root / "header.h").touch()
45+
lang = detect_language(self.root)
46+
self.assertEqual(lang, "c")
47+
48+
def test_detect_python(self):
49+
"""Detect Python from .py file."""
50+
(self.root / "script.py").touch()
51+
lang = detect_language(self.root)
52+
self.assertEqual(lang, "python")
53+
54+
def test_detect_javascript(self):
55+
"""Detect JavaScript from .js file."""
56+
(self.root / "app.js").touch()
57+
lang = detect_language(self.root)
58+
self.assertEqual(lang, "javascript")
59+
60+
def test_detect_go(self):
61+
"""Detect Go from .go file."""
62+
(self.root / "main.go").touch()
63+
lang = detect_language(self.root)
64+
self.assertEqual(lang, "go")
65+
66+
def test_detect_rust(self):
67+
"""Detect Rust from .rs file."""
68+
(self.root / "main.rs").touch()
69+
lang = detect_language(self.root)
70+
self.assertEqual(lang, "rust")
71+
72+
def test_most_common_wins(self):
73+
"""When multiple languages, most common wins."""
74+
(self.root / "main.c").touch()
75+
(self.root / "util.c").touch()
76+
(self.root / "helper.c").touch()
77+
(self.root / "script.py").touch()
78+
lang = detect_language(self.root)
79+
self.assertEqual(lang, "c")
80+
81+
def test_python_wins_when_more(self):
82+
"""Python wins when more Python files."""
83+
(self.root / "main.py").touch()
84+
(self.root / "util.py").touch()
85+
(self.root / "helper.c").touch()
86+
lang = detect_language(self.root)
87+
self.assertEqual(lang, "python")
88+
89+
def test_empty_directory_defaults_to_c(self):
90+
"""Empty directory defaults to C."""
91+
lang = detect_language(self.root)
92+
self.assertEqual(lang, "c")
93+
94+
def test_no_recognized_files_defaults_to_c(self):
95+
"""Unknown extensions default to C."""
96+
(self.root / "readme.txt").touch()
97+
(self.root / "data.json").touch()
98+
lang = detect_language(self.root)
99+
self.assertEqual(lang, "c")
100+
101+
def test_hidden_files_ignored(self):
102+
"""Hidden files (starting with .) are ignored."""
103+
(self.root / ".hidden.py").touch()
104+
(self.root / "main.c").touch()
105+
lang = detect_language(self.root)
106+
self.assertEqual(lang, "c")
107+
108+
def test_none_directory_uses_cwd(self):
109+
"""None directory uses current working directory."""
110+
# Just verify it doesn't crash
111+
lang = detect_language(None)
112+
self.assertIsInstance(lang, str)
113+
114+
115+
class TestLanguageExtensions(unittest.TestCase):
116+
"""Test language extension mapping completeness."""
117+
118+
def test_all_common_extensions_covered(self):
119+
"""Common programming extensions are mapped."""
120+
expected = ['.c', '.h', '.py', '.js', '.go', '.rs']
121+
for ext in expected:
122+
self.assertIn(ext, LANGUAGE_EXTENSIONS)
123+
124+
def test_extension_values_are_strings(self):
125+
"""All extension values are language strings."""
126+
for ext, lang in LANGUAGE_EXTENSIONS.items():
127+
self.assertIsInstance(lang, str)
128+
self.assertTrue(len(lang) > 0)
129+
130+
131+
class TestChecksManager(unittest.TestCase):
132+
"""Test ChecksManager caching logic."""
133+
134+
def setUp(self):
135+
"""Set up test fixtures."""
136+
self.temp_dir = tempfile.TemporaryDirectory()
137+
self.cache_dir = Path(self.temp_dir.name)
138+
self.manager = ChecksManager(token="test_token", cache_dir=self.cache_dir)
139+
140+
def tearDown(self):
141+
"""Clean up test fixtures."""
142+
self.temp_dir.cleanup()
143+
144+
def test_cache_dir_created(self):
145+
"""Cache directory is created on init."""
146+
self.assertTrue(self.cache_dir.exists())
147+
148+
def test_invalid_slug_format(self):
149+
"""Invalid slug format raises ValueError."""
150+
with self.assertRaises(ValueError) as ctx:
151+
self.manager.get_checks("invalid-slug")
152+
self.assertIn("Invalid slug format", str(ctx.exception))
153+
154+
@patch.object(ChecksManager, 'get_all_checks')
155+
def test_uses_cache_when_valid(self, mock_get_all):
156+
"""Uses cache when valid, doesn't call API."""
157+
# Pre-populate cache
158+
cache_path = self.cache_dir / "cs50" / "c" / "hello"
159+
cache_path.mkdir(parents=True)
160+
(cache_path / "__init__.py").write_text("# check")
161+
(cache_path / ".version").write_text("abc123")
162+
163+
result = self.manager.get_checks("cs50/hello", language="c")
164+
165+
self.assertEqual(result, cache_path)
166+
mock_get_all.assert_not_called()
167+
168+
@patch.object(ChecksManager, 'get_all_checks')
169+
def test_force_update_ignores_cache(self, mock_get_all):
170+
"""force_update=True ignores cache and calls API."""
171+
# Pre-populate cache
172+
cache_path = self.cache_dir / "cs50" / "c" / "hello"
173+
cache_path.mkdir(parents=True)
174+
(cache_path / "__init__.py").write_text("# check")
175+
(cache_path / ".version").write_text("abc123")
176+
177+
self.manager.get_checks("cs50/hello", language="c", force_update=True)
178+
179+
mock_get_all.assert_called_once()
180+
181+
def test_clear_all_cache(self):
182+
"""Clear all cache removes everything."""
183+
# Create some cache
184+
(self.cache_dir / "cs50" / "c" / "hello").mkdir(parents=True)
185+
(self.cache_dir / "cs50" / "c" / "hello" / "test.py").touch()
186+
187+
self.manager.clear_cache()
188+
189+
# Directory should be empty (recreated)
190+
self.assertEqual(list(self.cache_dir.iterdir()), [])
191+
192+
def test_clear_course_cache(self):
193+
"""Clear cache for specific course."""
194+
# Create cache for two courses
195+
(self.cache_dir / "cs50" / "c" / "hello").mkdir(parents=True)
196+
(self.cache_dir / "other" / "c" / "test").mkdir(parents=True)
197+
198+
self.manager.clear_cache(slug="cs50")
199+
200+
self.assertFalse((self.cache_dir / "cs50").exists())
201+
self.assertTrue((self.cache_dir / "other").exists())
202+
203+
def test_list_cache_empty(self):
204+
"""List empty cache returns empty list."""
205+
result = self.manager.list_cache()
206+
self.assertEqual(result, [])
207+
208+
def test_list_cache_with_entries(self):
209+
"""List cache returns cached entries."""
210+
# Create cache entry
211+
cache_path = self.cache_dir / "cs50" / "c" / "hello"
212+
cache_path.mkdir(parents=True)
213+
(cache_path / ".version").write_text("abc12345")
214+
215+
result = self.manager.list_cache()
216+
217+
self.assertEqual(len(result), 1)
218+
self.assertEqual(result[0]["course"], "cs50")
219+
self.assertEqual(result[0]["language"], "c")
220+
self.assertEqual(result[0]["stage"], "hello")
221+
222+
def test_write_stage_cache(self):
223+
"""Write stage cache correctly decodes base64."""
224+
stage_path = self.cache_dir / "cs50" / "c" / "test"
225+
files = [
226+
{"path": "check.py", "content": base64.b64encode(b"# test check").decode()},
227+
{"path": ".bootcs.yml", "content": base64.b64encode(b"checks: check.py").decode()},
228+
]
229+
230+
self.manager._write_stage_cache(stage_path, files)
231+
232+
self.assertTrue((stage_path / "check.py").exists())
233+
self.assertEqual((stage_path / "check.py").read_text(), "# test check")
234+
self.assertTrue((stage_path / ".bootcs.yml").exists())
235+
236+
237+
class TestChecksManagerIntegration(unittest.TestCase):
238+
"""Integration tests for ChecksManager with mocked API."""
239+
240+
def setUp(self):
241+
"""Set up test fixtures."""
242+
self.temp_dir = tempfile.TemporaryDirectory()
243+
self.cache_dir = Path(self.temp_dir.name)
244+
245+
def tearDown(self):
246+
"""Clean up test fixtures."""
247+
self.temp_dir.cleanup()
248+
249+
@patch('bootcs.api.checks.APIClient')
250+
def test_get_all_checks_batch_download(self, MockAPIClient):
251+
"""get_all_checks downloads and caches all stages."""
252+
mock_client = MagicMock()
253+
mock_client.get.return_value = {
254+
"courseSlug": "cs50",
255+
"language": "c",
256+
"version": "abc123",
257+
"checks": [
258+
{
259+
"stageSlug": "hello",
260+
"files": [
261+
{"path": "__init__.py", "content": base64.b64encode(b"# hello").decode()},
262+
]
263+
},
264+
{
265+
"stageSlug": "mario",
266+
"files": [
267+
{"path": "__init__.py", "content": base64.b64encode(b"# mario").decode()},
268+
]
269+
},
270+
]
271+
}
272+
MockAPIClient.return_value = mock_client
273+
274+
manager = ChecksManager(token="test", cache_dir=self.cache_dir)
275+
result = manager.get_all_checks("cs50", language="c")
276+
277+
self.assertEqual(len(result), 2)
278+
self.assertIn("hello", result)
279+
self.assertIn("mario", result)
280+
281+
# Verify files were written
282+
self.assertTrue((self.cache_dir / "cs50" / "c" / "hello" / "__init__.py").exists())
283+
self.assertTrue((self.cache_dir / "cs50" / "c" / "mario" / "__init__.py").exists())
284+
285+
286+
if __name__ == "__main__":
287+
unittest.main()

0 commit comments

Comments
 (0)