Option C Tools
Loading...
Searching...
No Matches
test_skeleton_exporter.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/tests/tests_tools/test_skeleton_exporter.py
4
5"""
6Purpose
7-------
8Tests for the skeleton exporter: core extraction, docstring stripping,
9non-Python placeholders, and integration.
10
11Responsibilities
12----------------
13- Verify extract_skeleton produces correct output for various Python patterns.
14- Verify _strip_docstring_section removes targeted sections.
15- Verify non-Python files produce one-line placeholders.
16- Verify write_skeleton_file_for_dir and clean_skeleton_dirs work end-to-end.
17
18Diagnostics
19-----------
20Domain: OCT-TEST
21Levels:
22 L2 — lifecycle
23 L3 — semantic details
24 L4 — deep tracing
25
26Contracts
27---------
28- All tests are self-contained with no external dependencies beyond pytest.
29"""
30
31import textwrap
32from pathlib import Path
33
34import pytest
35
36from oct.tools.skeleton_exporter import (
37 extract_skeleton,
38 _strip_docstring_section,
39 _skeleton_for_non_python,
40 write_skeleton_file_for_dir,
41 clean_skeleton_dirs,
42)
43
44
45# ============================================================
46# Test fixtures
47# ============================================================
48
49SIMPLE_MODULE = textwrap.dedent('''\
50 #!/usr/bin/env python3
51 # -*- coding: utf-8 -*-
52 # myproject/simple.py
53
54 """
55 Purpose
56 -------
57 A simple module.
58
59 Responsibilities
60 ----------------
61 - Do things.
62
63 Diagnostics
64 -----------
65 Domain: TEST
66 Levels:
67 L2 — lifecycle
68 L3 — details
69 L4 — trace
70
71 Contracts
72 ---------
73 Inputs: none
74 Outputs: none
75 """
76
77 MAX_RETRIES = 3
78
79 def hello(name: str) -> str:
80 """Greet someone."""
81 return f"Hello, {name}!"
82
83 def _private(x):
84 # internal helper
85 for i in range(x):
86 print(i)
87 return x * 2
88''')
89
90CLASS_MODULE = textwrap.dedent('''\
91 #!/usr/bin/env python3
92 # -*- coding: utf-8 -*-
93 # myproject/models.py
94
95 """
96 Purpose
97 -------
98 Models module.
99
100 Responsibilities
101 ----------------
102 - Define models.
103 """
104
105 class Base:
106 """Base class."""
107
108 _REGISTRY = {}
109
110 def save(self) -> None:
111 """Persist to storage."""
112 self._write_to_db()
113 self._update_cache()
114
115 class User(Base, metaclass=type):
116 """User model."""
117
118 @property
119 def name(self) -> str:
120 """The user name."""
121 return self._name
122
123 @staticmethod
124 def create(data: dict) -> "User":
125 """Factory method."""
126 u = User()
127 u._name = data["name"]
128 return u
129
130 class Meta:
131 """Inner meta class."""
132 table_name = "users"
133''')
134
135ASYNC_MODULE = textwrap.dedent('''\
136 #!/usr/bin/env python3
137 # -*- coding: utf-8 -*-
138 # myproject/async_mod.py
139
140 async def fetch(url: str, *, timeout: int = 30) -> bytes:
141 """Fetch data from URL."""
142 async with session.get(url, timeout=timeout) as resp:
143 return await resp.read()
144''')
145
146DECORATOR_MODULE = textwrap.dedent('''\
147 #!/usr/bin/env python3
148 # -*- coding: utf-8 -*-
149 # myproject/routes.py
150
151 @app.route("/api/v1", methods=["GET", "POST"])
152 def api_handler(request):
153 """Handle API requests."""
154 return process(request)
155''')
156
157
158# ============================================================
159# TestExtractSkeleton
160# ============================================================
161
163 """Unit tests for extract_skeleton()."""
164
166 skeleton = extract_skeleton(SIMPLE_MODULE, "myproject/simple.py")
167 assert "#!/usr/bin/env python3" in skeleton
168 assert "# -*- coding: utf-8 -*-" in skeleton
169 assert "# myproject/simple.py" in skeleton
170
172 skeleton = extract_skeleton(SIMPLE_MODULE, "myproject/simple.py")
173 assert "Purpose" in skeleton
174 assert "A simple module." in skeleton
175
177 skeleton = extract_skeleton(SIMPLE_MODULE, "myproject/simple.py",
178 no_diagnostics=True)
179 assert "Purpose" in skeleton
180 assert "Responsibilities" in skeleton
181 assert "Domain: TEST" not in skeleton
182 assert "L2" not in skeleton
183
185 skeleton = extract_skeleton(SIMPLE_MODULE, "myproject/simple.py",
186 no_diagnostics=True)
187 assert "Contracts" in skeleton
188 assert "Inputs: none" in skeleton
189
191 skeleton = extract_skeleton(SIMPLE_MODULE, "myproject/simple.py")
192 assert "def hello(name: str) -> str:" in skeleton
193 assert "def _private(x):" in skeleton
194
196 skeleton = extract_skeleton(SIMPLE_MODULE, "myproject/simple.py")
197 assert "Greet someone." in skeleton
198
200 skeleton = extract_skeleton(SIMPLE_MODULE, "myproject/simple.py")
201 assert 'f"Hello, {name}!"' not in skeleton
202 assert "range(x)" not in skeleton
203 assert "print(i)" not in skeleton
204 assert "return x * 2" not in skeleton
205
207 skeleton = extract_skeleton(SIMPLE_MODULE, "myproject/simple.py")
208 assert "MAX_RETRIES = ..." in skeleton
209
211 skeleton = extract_skeleton(CLASS_MODULE, "myproject/models.py")
212 assert "class Base:" in skeleton
213 assert "class User(Base, metaclass=type):" in skeleton
214 assert "def save(self) -> None:" in skeleton
215 assert "User model." in skeleton
216
218 skeleton = extract_skeleton(CLASS_MODULE, "myproject/models.py")
219 assert "@property" in skeleton
220 assert "@staticmethod" in skeleton
221
223 skeleton = extract_skeleton(DECORATOR_MODULE, "myproject/routes.py")
224 # ast.unparse may use single quotes; check structural content
225 assert "@app.route(" in skeleton
226 assert "methods=" in skeleton
227
229 skeleton = extract_skeleton(ASYNC_MODULE, "myproject/async_mod.py")
230 # ast.unparse may collapse spaces around = in defaults
231 assert "async def fetch(" in skeleton
232 assert "url: str" in skeleton
233 assert "timeout: int" in skeleton
234 assert "-> bytes:" in skeleton
235
237 skeleton = extract_skeleton(CLASS_MODULE, "myproject/models.py")
238 assert "class Meta:" in skeleton
239 assert "Inner meta class." in skeleton
240
242 skeleton = extract_skeleton(CLASS_MODULE, "myproject/models.py")
243 assert "_write_to_db" not in skeleton
244 assert "_update_cache" not in skeleton
245
247 bad_code = "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n# bad.py\ndef ("
248 skeleton = extract_skeleton(bad_code, "bad.py")
249 assert "#!/usr/bin/env python3" in skeleton
250 assert "syntax error" in skeleton.lower()
251
253 skeleton = extract_skeleton("", "empty.py")
254 assert skeleton.strip() == ""
255
257 skeleton = extract_skeleton(SIMPLE_MODULE, "myproject/simple.py")
258 assert " ..." in skeleton
259
260
261# ============================================================
262# TestStripDocstringSection
263# ============================================================
264
266 """Unit tests for _strip_docstring_section."""
267
268 DOCSTRING = textwrap.dedent("""\
269 Purpose
270 -------
271 Do stuff.
272
273 Responsibilities
274 ----------------
275 - Thing one.
276
277 Diagnostics
278 -----------
279 Domain: TEST
280 L2: summary
281
282 Contracts
283 ---------
284 Inputs: none
285 """)
286
288 result = _strip_docstring_section(self.DOCSTRING, "Diagnostics")
289 assert "Purpose" in result
290 assert "Responsibilities" in result
291 assert "Domain: TEST" not in result
292 assert "Contracts" in result
293
295 result = _strip_docstring_section(self.DOCSTRING, "Contracts")
296 assert "Diagnostics" in result
297 assert "Inputs: none" not in result
298
300 result = _strip_docstring_section(self.DOCSTRING, "NonExistent")
301 assert "Purpose" in result
302 assert "Diagnostics" in result
303 assert "Contracts" in result
304
306 result = _strip_docstring_section(self.DOCSTRING, "Responsibilities")
307 assert "Purpose" in result
308 assert "Thing one." not in result
309 assert "Diagnostics" in result
310
312 """OI-415: section headers starting with digits must be handled."""
313 docstring = textwrap.dedent("""\
314 Purpose
315 -------
316 Stuff.
317
318 Layer 3
319 -------
320 Low-level notes.
321
322 Contracts
323 ---------
324 Inputs: none
325 """)
326 # Ensure the 'Layer 3' section terminates correctly (\S matching)
327 result = _strip_docstring_section(docstring, "Purpose")
328 assert "Stuff." not in result
329 assert "Layer 3" in result
330 assert "Contracts" in result
331
333 """OI-415: non-ASCII section headers must still terminate matching."""
334 docstring = textwrap.dedent("""\
335 Purpose
336 -------
337 Do stuff.
338
339 Résumé
340 ------
341 Summary line.
342
343 Contracts
344 ---------
345 Inputs: none
346 """)
347 result = _strip_docstring_section(docstring, "Purpose")
348 assert "Do stuff." not in result
349 assert "Résumé" in result
350 assert "Summary line." in result
351
353 """OI-415: headers with trailing whitespace should still be matched."""
354 docstring = (
355 "Purpose\n"
356 "-------\n"
357 "Do stuff.\n"
358 "\n"
359 "Diagnostics \n"
360 "-----------\n"
361 "Domain: TEST\n"
362 "\n"
363 "Contracts\n"
364 "---------\n"
365 "Inputs: none\n"
366 )
367 result = _strip_docstring_section(docstring, "Diagnostics")
368 assert "Domain: TEST" not in result
369 assert "Purpose" in result
370 assert "Contracts" in result
371
372
373# ============================================================
374# TestNonPythonSkeleton
375# ============================================================
376
378 """Tests for non-Python file placeholder output."""
379
380 def test_json_placeholder(self, tmp_path):
381 f = tmp_path / "config.json"
382 f.write_text('{"key": "value"}\n', encoding="utf-8")
383 result = _skeleton_for_non_python(f)
384 assert "config.json" in result
385 assert "line" in result
386 assert "KB" in result
387
388 def test_markdown_placeholder(self, tmp_path):
389 f = tmp_path / "README.md"
390 f.write_text("# Title\n\nSome content.\n", encoding="utf-8")
391 result = _skeleton_for_non_python(f)
392 assert "README.md" in result
393 assert "KB" in result
394
395
396# ============================================================
397# TestSkeletonIntegration
398# ============================================================
399
401 """Integration tests using write_skeleton_file_for_dir and clean."""
402
403 @pytest.fixture
404 def project(self, tmp_path):
405 """Create a minimal project structure."""
406 src = tmp_path / "myproject"
407 src.mkdir()
408 (src / "docs").mkdir()
409 (src / "tests").mkdir()
410 (src / "oc_diagnostics").mkdir()
411
412 py_file = src / "main.py"
413 py_file.write_text(textwrap.dedent('''\
414 #!/usr/bin/env python3
415 # -*- coding: utf-8 -*-
416 # myproject/main.py
417
418 def run():
419 """Start the app."""
420 print("running")
421 '''), encoding="utf-8")
422
423 json_file = src / "config.json"
424 json_file.write_text('{"debug": true}\n', encoding="utf-8")
425
426 return src
427
428 def test_writes_output_file(self, project, tmp_path):
429 cfg = {
430 "include_extensions": [".py", ".json"],
431 "exclude_dirs": set(),
432 "wildcard_exclude_dirs": [],
433 "header_text": "SKELETON\n",
434 "separator_file": "--- {num}: {path}\n",
435 "separator_subdir": "=== {num}: {dirname}\n",
436 "root_header": "ROOT\n",
437 "max_safe_path_len": 240,
438 }
439 out = tmp_path / "output.txt"
440 count, *_ = write_skeleton_file_for_dir(project, out, project, cfg)
441 assert count > 0
442 content = out.read_text(encoding="utf-8")
443 assert "def run():" in content
444 assert "config.json" in content
445 # No implementation body
446 assert 'print("running")' not in content
447
449 skel_dir = project / "_skeleton_code-20260408-1500"
450 skel_dir.mkdir()
451 (skel_dir / "test.txt").write_text("test", encoding="utf-8")
452
453 clean_skeleton_dirs(project)
454
455 assert not skel_dir.exists()
456
457 def test_exclusions_respected(self, project, tmp_path):
458 # Create an excluded dir and a secret file
459 venv = project / ".git"
460 venv.mkdir()
461 (venv / "config").write_text("secret", encoding="utf-8")
462
463 env_file = project / ".env"
464 env_file.write_text("SECRET=abc", encoding="utf-8")
465
466 cfg = {
467 "include_extensions": [".py", ".json", ".env"],
468 "exclude_dirs": {".git"},
469 "wildcard_exclude_dirs": [],
470 "header_text": "SKELETON\n",
471 "separator_file": "--- {num}: {path}\n",
472 "separator_subdir": "=== {num}: {dirname}\n",
473 "root_header": "ROOT\n",
474 "max_safe_path_len": 240,
475 }
476 out = tmp_path / "output.txt"
477 write_skeleton_file_for_dir(project, out, project, cfg)
478 content = out.read_text(encoding="utf-8")
479 assert ".git" not in content
480 assert ".env" not in content
481 assert "SECRET=abc" not in content