350 lines
12 KiB
Python
350 lines
12 KiB
Python
import importlib.util
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
MODULE_PATH = (
|
|
Path(__file__).resolve().parents[1] / "src/simplecadapi/auto_tools/auto_docs_gen.py"
|
|
)
|
|
MODULE_SPEC = importlib.util.spec_from_file_location(
|
|
"simplecadapi_auto_docs_gen",
|
|
MODULE_PATH,
|
|
)
|
|
if MODULE_SPEC is None or MODULE_SPEC.loader is None:
|
|
raise RuntimeError(f"Unable to load module spec for {MODULE_PATH}")
|
|
|
|
auto_docs_gen = importlib.util.module_from_spec(MODULE_SPEC)
|
|
sys.modules[MODULE_SPEC.name] = auto_docs_gen
|
|
MODULE_SPEC.loader.exec_module(auto_docs_gen)
|
|
|
|
|
|
class TestAutoDocsGenPathResolution(unittest.TestCase):
|
|
def test_resolve_source_files_from_source_checkout(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
project_root = Path(tmp_dir)
|
|
(project_root / "pyproject.toml").write_text(
|
|
"[project]\nname = 'demo'\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
module_file = project_root / "src/simplecadapi/auto_tools/auto_docs_gen.py"
|
|
module_file.parent.mkdir(parents=True, exist_ok=True)
|
|
module_file.write_text("", encoding="utf-8")
|
|
|
|
resolved = auto_docs_gen._resolve_source_files(
|
|
None, module_file=module_file
|
|
)
|
|
package_root = project_root / "src/simplecadapi"
|
|
expected = [
|
|
(package_root / name).resolve()
|
|
for name in auto_docs_gen.DEFAULT_SOURCE_FILENAMES
|
|
]
|
|
|
|
self.assertEqual(resolved, expected)
|
|
|
|
def test_resolve_source_files_from_site_packages_install(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
venv_root = Path(tmp_dir) / ".venv/lib/python3.12/site-packages"
|
|
module_file = venv_root / "simplecadapi/auto_tools/auto_docs_gen.py"
|
|
module_file.parent.mkdir(parents=True, exist_ok=True)
|
|
module_file.write_text("", encoding="utf-8")
|
|
|
|
resolved = auto_docs_gen._resolve_source_files(
|
|
None, module_file=module_file
|
|
)
|
|
package_root = venv_root / "simplecadapi"
|
|
expected = [
|
|
(package_root / name).resolve()
|
|
for name in auto_docs_gen.DEFAULT_SOURCE_FILENAMES
|
|
]
|
|
|
|
self.assertEqual(resolved, expected)
|
|
|
|
def test_resolve_output_dirs_from_source_checkout_uses_repo_docs(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
project_root = Path(tmp_dir)
|
|
(project_root / "pyproject.toml").write_text(
|
|
"[project]\nname = 'demo'\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
module_file = project_root / "src/simplecadapi/auto_tools/auto_docs_gen.py"
|
|
module_file.parent.mkdir(parents=True, exist_ok=True)
|
|
module_file.write_text("", encoding="utf-8")
|
|
|
|
resolved = auto_docs_gen._resolve_output_dirs(None, module_file=module_file)
|
|
|
|
self.assertEqual(resolved, [(project_root / "docs/api").resolve()])
|
|
|
|
def test_resolve_output_dirs_from_site_packages_install_uses_cwd(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
tmp_path = Path(tmp_dir)
|
|
workspace_root = tmp_path / "workspace"
|
|
workspace_root.mkdir()
|
|
|
|
venv_root = tmp_path / ".venv/lib/python3.12/site-packages"
|
|
module_file = venv_root / "simplecadapi/auto_tools/auto_docs_gen.py"
|
|
module_file.parent.mkdir(parents=True, exist_ok=True)
|
|
module_file.write_text("", encoding="utf-8")
|
|
|
|
resolved = auto_docs_gen._resolve_output_dirs(
|
|
None,
|
|
module_file=module_file,
|
|
cwd=workspace_root,
|
|
)
|
|
|
|
self.assertEqual(resolved, [(workspace_root / "docs/api").resolve()])
|
|
|
|
def test_default_source_files_include_v2_public_modules(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
package_root = Path(tmp_dir) / "src/simplecadapi"
|
|
package_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
resolved = auto_docs_gen._default_source_files(package_root)
|
|
|
|
resolved_names = [path.relative_to(package_root).as_posix() for path in resolved]
|
|
self.assertIn("serializer.py", resolved_names)
|
|
self.assertIn("graph.py", resolved_names)
|
|
self.assertIn("expr.py", resolved_names)
|
|
self.assertIn("sketch.py", resolved_names)
|
|
self.assertIn("math.py", resolved_names)
|
|
self.assertIn("translator/freecad_translator/api.py", resolved_names)
|
|
self.assertIn(
|
|
"translator/freecad_translator/script_translator.py",
|
|
resolved_names,
|
|
)
|
|
|
|
def test_default_stdlib_source_files_include_standard_modules(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
package_root = Path(tmp_dir) / "src/simplecadapi"
|
|
package_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
resolved = auto_docs_gen._default_stdlib_source_files(package_root)
|
|
|
|
self.assertEqual(
|
|
resolved,
|
|
[
|
|
package_root / "std/bearing.py",
|
|
package_root / "std/gear.py",
|
|
],
|
|
)
|
|
|
|
def test_resolve_stdlib_output_dirs_from_source_checkout_uses_repo_docs(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
project_root = Path(tmp_dir)
|
|
(project_root / "pyproject.toml").write_text(
|
|
"[project]\nname = 'demo'\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
module_file = project_root / "src/simplecadapi/auto_tools/auto_docs_gen.py"
|
|
module_file.parent.mkdir(parents=True, exist_ok=True)
|
|
module_file.write_text("", encoding="utf-8")
|
|
|
|
resolved = auto_docs_gen._resolve_stdlib_output_dirs(
|
|
None,
|
|
module_file=module_file,
|
|
)
|
|
|
|
self.assertEqual(resolved, [(project_root / "docs/stdlib").resolve()])
|
|
|
|
|
|
class TestAutoDocsGenExtraction(unittest.TestCase):
|
|
def test_extract_apis_from_v2_public_modules(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
tmp_path = Path(tmp_dir)
|
|
source_file = tmp_path / "serializer.py"
|
|
source_file.write_text(
|
|
"""
|
|
def export_model_json(session, indent=2):
|
|
\"\"\"Export the canonical 2.0 model seed JSON.\"\"\"
|
|
return \"{}\"
|
|
|
|
|
|
def _internal_helper():
|
|
\"\"\"Should not be documented.\"\"\"
|
|
return None
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
output_dir = tmp_path / "docs/api"
|
|
|
|
generator = auto_docs_gen.APIDocumentGenerator(
|
|
source_files=[source_file],
|
|
output_dirs=[output_dir],
|
|
quiet=True,
|
|
)
|
|
|
|
apis = generator.extract_apis()
|
|
|
|
self.assertEqual([api.name for api in apis], ["export_model_json"])
|
|
|
|
def test_generate_markdown_includes_v2_model_api_entry(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
tmp_path = Path(tmp_dir)
|
|
source_file = tmp_path / "serializer.py"
|
|
source_file.write_text(
|
|
"""
|
|
def export_model_json(session, indent=2):
|
|
\"\"\"Export the canonical 2.0 model seed JSON.
|
|
|
|
Args:
|
|
session: Recorded graph session.
|
|
indent: JSON indentation level.
|
|
|
|
Returns:
|
|
JSON string representation.
|
|
\"\"\"
|
|
return \"{}\"
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
output_dir = tmp_path / "docs/api"
|
|
|
|
generator = auto_docs_gen.APIDocumentGenerator(
|
|
source_files=[source_file],
|
|
output_dirs=[output_dir],
|
|
quiet=True,
|
|
)
|
|
generator.extract_apis()
|
|
generator.generate_markdown_docs()
|
|
|
|
readme = (output_dir / "README.md").read_text(encoding="utf-8")
|
|
page = (output_dir / "export_model_json.md").read_text(encoding="utf-8")
|
|
|
|
self.assertIn("[export_model_json](export_model_json.md)", readme)
|
|
self.assertIn("def export_model_json(session, indent = 2)", page)
|
|
self.assertIn("Export the canonical 2.0 model seed JSON.", page)
|
|
|
|
def test_generate_markdown_avoids_case_insensitive_filename_collisions(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
tmp_path = Path(tmp_dir)
|
|
source_file = tmp_path / "expr.py"
|
|
source_file.write_text(
|
|
"""
|
|
class Const:
|
|
\"\"\"Constant node.\"\"\"
|
|
|
|
|
|
def const(value):
|
|
\"\"\"Constant constructor.\"\"\"
|
|
return value
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
output_dir = tmp_path / "docs/api"
|
|
|
|
generator = auto_docs_gen.APIDocumentGenerator(
|
|
source_files=[source_file],
|
|
output_dirs=[output_dir],
|
|
quiet=True,
|
|
)
|
|
generator.extract_apis()
|
|
generator.generate_markdown_docs()
|
|
|
|
readme = (output_dir / "README.md").read_text(encoding="utf-8")
|
|
|
|
self.assertTrue((output_dir / "Const.md").exists())
|
|
self.assertTrue((output_dir / "const_function.md").exists())
|
|
self.assertIn("[Const](Const.md)", readme)
|
|
self.assertIn("[const](const_function.md)", readme)
|
|
|
|
def test_generate_markdown_includes_math_helper_category(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
tmp_path = Path(tmp_dir)
|
|
init_file = tmp_path / "__init__.py"
|
|
init_file.write_text(
|
|
"__all__ = ['BSplineFitResult', 'fit_cubic_bspline_control_points']\n",
|
|
encoding="utf-8",
|
|
)
|
|
source_file = tmp_path / "math.py"
|
|
source_file.write_text(
|
|
'''
|
|
class BSplineFitResult:
|
|
"""B-spline fitting result."""
|
|
|
|
|
|
def fit_cubic_bspline_control_points(sample_points, *, tolerance=1e-3):
|
|
"""Fit sampled points to cubic B-spline controls."""
|
|
return BSplineFitResult()
|
|
'''.strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
output_dir = tmp_path / "docs/api"
|
|
|
|
generator = auto_docs_gen.APIDocumentGenerator(
|
|
source_files=[source_file],
|
|
output_dirs=[output_dir],
|
|
quiet=True,
|
|
)
|
|
generator.extract_apis()
|
|
generator.generate_markdown_docs()
|
|
|
|
readme = (output_dir / "README.md").read_text(encoding="utf-8")
|
|
|
|
self.assertIn("## Math Helpers", readme)
|
|
self.assertIn("[BSplineFitResult](BSplineFitResult.md)", readme)
|
|
self.assertIn(
|
|
"[fit_cubic_bspline_control_points](fit_cubic_bspline_control_points.md)",
|
|
readme,
|
|
)
|
|
|
|
def test_generate_stdlib_markdown_uses_stdlib_index_and_import_surface(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
tmp_path = Path(tmp_dir)
|
|
source_dir = tmp_path / "std"
|
|
source_dir.mkdir()
|
|
source_file = source_dir / "gear.py"
|
|
source_file.write_text(
|
|
'''
|
|
def make_spur_gear_rsolid(n_teeth: int, module: float):
|
|
"""Create a test spur gear.
|
|
|
|
Parameters
|
|
----------
|
|
n_teeth : int
|
|
Number of teeth.
|
|
module : float
|
|
Gear module.
|
|
"""
|
|
return None
|
|
|
|
|
|
def _private_helper():
|
|
"""Should not be documented."""
|
|
return None
|
|
'''.strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
output_dir = tmp_path / "docs/stdlib"
|
|
|
|
generator = auto_docs_gen.StdlibDocumentGenerator(
|
|
source_files=[source_file],
|
|
output_dirs=[output_dir],
|
|
quiet=True,
|
|
)
|
|
generator.extract_apis()
|
|
generator.generate_markdown_docs()
|
|
|
|
readme = (output_dir / "README.md").read_text(encoding="utf-8")
|
|
page = (output_dir / "make_spur_gear_rsolid.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
self.assertIn("# SimpleCAD Standard Library Index", readme)
|
|
self.assertIn("[make_spur_gear_rsolid](make_spur_gear_rsolid.md)", readme)
|
|
self.assertIn("scad.std.gear.make_spur_gear_rsolid", page)
|
|
self.assertIn("**Type**: `int`", page)
|
|
self.assertNotIn("_private_helper", readme)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|