"""Unit tests for devx.ci.doc_coverage.""" from pathlib import Path import pytest from click.testing import CliRunner from devx.ci.doc_coverage import ( check_command_documented, check_module_documented, extract_cli_commands, main, ) class TestExtractCliCommands: def test_extracts_commands(self) -> None: commands = extract_cli_commands() # devx CLI has commands under ci, tools, and molecule groups assert "auto-merge" in commands assert "release" in commands assert "publish" in commands assert "setup" in commands assert "install-tools" in commands def test_returns_list(self) -> None: commands = extract_cli_commands() assert isinstance(commands, list) assert len(commands) > 0 def test_no_cli_file(self, monkeypatch: pytest.MonkeyPatch) -> None: """Returns empty list when CLI file doesn't exist.""" from devx.ci import doc_coverage monkeypatch.setattr(doc_coverage, "CLI_FILE", Path("/nonexistent/cli.py")) commands = extract_cli_commands() assert commands == [] def test_def_fallback_no_explicit_name(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """When a command decorator has no explicit name, falls back to the def name.""" from devx.ci import doc_coverage fake_cli = tmp_path / "cli.py" fake_cli.write_text("@click.group()\ndef cli():\n pass\n@cli.command()\ndef my_command():\n pass\n") monkeypatch.setattr(doc_coverage, "CLI_FILE", fake_cli) commands = extract_cli_commands() assert "my_command" in commands def test_command_decorator_no_def_fallback(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """When a command decorator has no name and no following def, it is skipped.""" from devx.ci import doc_coverage fake_cli = tmp_path / "cli.py" # The last @cli.command() has no explicit name and no def statement after it fake_cli.write_text( "@click.group()\ndef cli():\n pass\n@cli.command()\ndef real_cmd():\n pass\n@cli.command()\npass\n" ) monkeypatch.setattr(doc_coverage, "CLI_FILE", fake_cli) commands = extract_cli_commands() # real_cmd should be found via def fallback; the bare @cli.command() is skipped assert "real_cmd" in commands assert "pass" not in commands class TestCheckCommandDocumented: def test_finds_command_in_heading(self) -> None: content = "## auto-merge\n\nAuto-merge PR." assert check_command_documented("auto-merge", content) is True def test_finds_command_in_code_block(self) -> None: content = "```bash\ndevx ci release --dry-run\n```" assert check_command_documented("release", content) is True def test_finds_command_with_devx_prefix(self) -> None: content = "Use `devx tools setup` to install." assert check_command_documented("setup", content) is True def test_missing_command(self) -> None: content = "## Other stuff\n\nNo commands here." assert check_command_documented("release", content) is False class TestCheckModuleDocumented: def test_finds_module(self) -> None: content = "The cli.py module handles..." assert check_module_documented("cli.py", content) is True def test_missing_module(self) -> None: content = "No modules mentioned." assert check_module_documented("cli.py", content) is False class TestMain: def test_all_present(self, tmp_path: Path) -> None: """When all docs exist and cover all commands/modules, exit 0.""" docs = tmp_path / "docs" (docs / "user").mkdir(parents=True) (docs / "tech").mkdir(parents=True) # Get actual commands from the CLI commands = extract_cli_commands() # Write cli-commands.md with all commands cli_content = "\n".join(f"## {cmd}" for cmd in commands) (docs / "user" / "cli-commands.md").write_text(cli_content) # Write architecture.md with all modules from devx.ci.doc_coverage import REQUIRED_MODULES, REQUIRED_SCRIPTS (docs / "tech" / "architecture.md").write_text(" ".join(REQUIRED_MODULES)) # Write ci-cd-workflow.md with all scripts (docs / "tech" / "ci-cd-workflow.md").write_text(" ".join(REQUIRED_SCRIPTS)) runner = CliRunner() result = runner.invoke(main, ["--docs-dir", str(docs)]) assert result.exit_code == 0 assert "100%" in result.output def test_missing_docs_fail(self, tmp_path: Path) -> None: """When docs are missing and --fail-on-missing is set, exit 1.""" docs = tmp_path / "docs" (docs / "user").mkdir(parents=True) (docs / "tech").mkdir(parents=True) (docs / "user" / "cli-commands.md").write_text("No commands here.") (docs / "tech" / "architecture.md").write_text("No modules here.") (docs / "tech" / "ci-cd-workflow.md").write_text("No scripts here.") runner = CliRunner() result = runner.invoke(main, ["--docs-dir", str(docs), "--fail-on-missing"]) assert result.exit_code == 1 def test_missing_docs_warn_only(self, tmp_path: Path) -> None: """Without --fail-on-missing, missing docs only warn (exit 0).""" docs = tmp_path / "docs" (docs / "user").mkdir(parents=True) (docs / "tech").mkdir(parents=True) (docs / "user" / "cli-commands.md").write_text("No commands here.") (docs / "tech" / "architecture.md").write_text("No modules here.") (docs / "tech" / "ci-cd-workflow.md").write_text("No scripts here.") runner = CliRunner() result = runner.invoke(main, ["--docs-dir", str(docs)]) assert result.exit_code == 0 assert "MISSING" in result.output