Files
AIPass/tests/test_seedgo_plugins.py
T
AIOSAIandClaude Opus 4.6 a295e18b03 fix(seedgo): resolve ruff lint errors in test files
- Remove sys.path.insert() blocks from test files (E402) — package is
  installed via pip install -e . so path manipulation is unnecessary
- Move import importlib.util and import subprocess to top of file (E402)
- Remove unused sys import from test_seedgo_core.py (F401)
- Fix undefined _src_path reference in test_seedgo_cli.py (F821)
- Remove unused result variable assignment in TestCLIInit (F841)
- Remove extraneous f prefix from f-string in cli.py (F541)

All 249 tests pass. Zero ruff violations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:25:28 -08:00

766 lines
28 KiB
Python

"""
Seed Go Plugin Tests — Phase 3
Tests for all 5 starter plugins:
- no-bare-except
- type-hints-required
- docstring-coverage
- function-length
- file-structure
Each plugin has:
- A clean-file test (should pass)
- A violation test (should fail)
- Edge case tests (empty files, comments, binary-safe reads)
- Configurable option tests where applicable
Uses tmp_path for isolated test directories.
"""
import importlib.util
from pathlib import Path
import pytest
_repo_root = Path(__file__).parent.parent
def _load_plugin(plugin_filename: str | Path):
"""Load a plugin module by filename from the plugins directory."""
plugin_path = _repo_root / "src" / "seedgo" / "plugins" / plugin_filename
spec = importlib.util.spec_from_file_location(Path(plugin_filename).stem, plugin_path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
# ---------------------------------------------------------------------------
# Plugin: no-bare-except
# ---------------------------------------------------------------------------
class TestNoBareExcept:
@pytest.fixture
def plugin(self):
return _load_plugin(Path("no_bare_except.py"))
def test_plugin_name(self, plugin):
assert plugin.PLUGIN_NAME == "no-bare-except"
def test_plugin_has_description(self, plugin):
assert isinstance(plugin.PLUGIN_DESCRIPTION, str)
assert len(plugin.PLUGIN_DESCRIPTION) > 0
def test_plugin_has_file_types(self, plugin):
assert plugin.FILE_TYPES == ["*.py"]
def test_clean_file_passes(self, plugin, tmp_path):
f = tmp_path / "clean.py"
f.write_text(
"try:\n"
" do_something()\n"
"except Exception:\n"
" pass\n"
)
result = plugin.check(str(f))
assert result.passed is True
assert result.plugin == "no-bare-except"
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 0
def test_bare_except_fails(self, plugin, tmp_path):
f = tmp_path / "bad.py"
f.write_text(
"try:\n"
" do_something()\n"
"except:\n"
" pass\n"
)
result = plugin.check(str(f))
assert result.passed is False
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 1
assert failed[0].line == 3
def test_multiple_bare_excepts(self, plugin, tmp_path):
f = tmp_path / "multi.py"
f.write_text(
"try:\n"
" x()\n"
"except:\n"
" pass\n"
"try:\n"
" y()\n"
"except:\n"
" pass\n"
)
result = plugin.check(str(f))
assert result.passed is False
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 2
def test_bare_except_in_comment_ignored(self, plugin, tmp_path):
f = tmp_path / "comment.py"
f.write_text(
"# except: <- this is just a comment\n"
"x = 1\n"
)
result = plugin.check(str(f))
assert result.passed is True
def test_bare_except_in_string_ignored(self, plugin, tmp_path):
f = tmp_path / "string.py"
f.write_text(
'x = """This shows except: usage"""\n'
"y = 1\n"
)
result = plugin.check(str(f))
assert result.passed is True
def test_empty_file_passes(self, plugin, tmp_path):
f = tmp_path / "empty.py"
f.write_text("")
result = plugin.check(str(f))
assert result.passed is True
def test_file_with_only_comments_passes(self, plugin, tmp_path):
f = tmp_path / "comments.py"
f.write_text(
"# This is a comment\n"
"# except: not a real bare except\n"
)
result = plugin.check(str(f))
assert result.passed is True
def test_nonexistent_file_returns_passing(self, plugin, tmp_path):
result = plugin.check(str(tmp_path / "does_not_exist.py"))
assert result.passed is True
assert result.metadata.get("skipped") is True
def test_bare_except_with_inline_comment_still_flagged(self, plugin, tmp_path):
f = tmp_path / "inline.py"
f.write_text(
"try:\n"
" x()\n"
"except: # bad practice\n"
" pass\n"
)
result = plugin.check(str(f))
assert result.passed is False
def test_specific_exception_passes(self, plugin, tmp_path):
f = tmp_path / "specific.py"
f.write_text(
"try:\n"
" x()\n"
"except (ValueError, TypeError):\n"
" pass\n"
)
result = plugin.check(str(f))
assert result.passed is True
def test_result_has_file_path(self, plugin, tmp_path):
f = tmp_path / "test.py"
f.write_text("x = 1\n")
result = plugin.check(str(f))
assert result.file_path == str(f)
# ---------------------------------------------------------------------------
# Plugin: type-hints-required
# ---------------------------------------------------------------------------
class TestTypeHintsRequired:
@pytest.fixture
def plugin(self):
return _load_plugin(Path("type_hints_required.py"))
def test_plugin_name(self, plugin):
assert plugin.PLUGIN_NAME == "type-hints-required"
def test_clean_file_passes(self, plugin, tmp_path):
f = tmp_path / "clean.py"
f.write_text(
"def greet(name: str) -> str:\n"
" return f'Hello {name}'\n"
"\n"
"def add(a: int, b: int) -> int:\n"
" return a + b\n"
)
result = plugin.check(str(f))
assert result.passed is True
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 0
def test_missing_return_type_fails(self, plugin, tmp_path):
f = tmp_path / "bad.py"
f.write_text(
"def greet(name: str):\n"
" return f'Hello {name}'\n"
)
result = plugin.check(str(f))
assert result.passed is False
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 1
assert "greet" in failed[0].message
def test_private_function_skipped(self, plugin, tmp_path):
f = tmp_path / "private.py"
f.write_text(
"def _helper():\n"
" return 42\n"
"\n"
"def __dunder():\n"
" return 42\n"
)
result = plugin.check(str(f))
assert result.passed is True
def test_init_skipped(self, plugin, tmp_path):
f = tmp_path / "cls.py"
f.write_text(
"class Foo:\n"
" def __init__(self, x: int):\n"
" self.x = x\n"
)
result = plugin.check(str(f))
assert result.passed is True
def test_dunder_methods_skipped(self, plugin, tmp_path):
f = tmp_path / "dunders.py"
f.write_text(
"class Foo:\n"
" def __str__(self):\n"
" return 'Foo'\n"
" def __repr__(self):\n"
" return 'Foo()'\n"
" def __len__(self):\n"
" return 0\n"
)
result = plugin.check(str(f))
assert result.passed is True
def test_empty_file_passes(self, plugin, tmp_path):
f = tmp_path / "empty.py"
f.write_text("")
result = plugin.check(str(f))
assert result.passed is True
def test_async_function_checked(self, plugin, tmp_path):
f = tmp_path / "async_fn.py"
f.write_text(
"async def fetch_data(url: str):\n"
" pass\n"
" pass\n"
" pass\n"
)
result = plugin.check(str(f))
assert result.passed is False
failed = [c for c in result.checks if not c.passed]
assert any("fetch_data" in c.message for c in failed)
def test_async_function_with_return_type_passes(self, plugin, tmp_path):
f = tmp_path / "async_ok.py"
f.write_text(
"async def fetch_data(url: str) -> bytes:\n"
" return b''\n"
)
result = plugin.check(str(f))
assert result.passed is True
def test_nonexistent_file_returns_passing(self, plugin, tmp_path):
result = plugin.check(str(tmp_path / "ghost.py"))
assert result.passed is True
assert result.metadata.get("skipped") is True
def test_syntax_error_skipped(self, plugin, tmp_path):
f = tmp_path / "bad_syntax.py"
f.write_text("def broken(\n x:\n")
result = plugin.check(str(f))
assert result.passed is True
assert result.metadata.get("skipped") is True
def test_violation_has_line_number(self, plugin, tmp_path):
f = tmp_path / "line.py"
f.write_text(
"x = 1\n"
"y = 2\n"
"def my_func(a: int):\n"
" return a\n"
)
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
assert any(c.line == 3 for c in failed)
def test_violation_has_fix_hint(self, plugin, tmp_path):
f = tmp_path / "hint.py"
f.write_text(
"def process(data: list):\n"
" return data\n"
)
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
assert len(failed) > 0
assert failed[0].fix_hint is not None
# ---------------------------------------------------------------------------
# Plugin: docstring-coverage
# ---------------------------------------------------------------------------
class TestDocstringCoverage:
@pytest.fixture
def plugin(self):
return _load_plugin(Path("docstring_coverage.py"))
def test_plugin_name(self, plugin):
assert plugin.PLUGIN_NAME == "docstring-coverage"
def test_clean_function_passes(self, plugin, tmp_path):
f = tmp_path / "clean.py"
f.write_text(
'def greet(name: str) -> str:\n'
' """Greet the user by name."""\n'
' return f"Hello {name}"\n'
' return f"Hello {name}"\n'
)
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 0
def test_missing_docstring_flagged(self, plugin, tmp_path):
f = tmp_path / "no_doc.py"
f.write_text(
"def process(data: list) -> list:\n"
" result = []\n"
" for item in data:\n"
" result.append(item)\n"
" return result\n"
)
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 1
assert "process" in failed[0].message
def test_info_severity(self, plugin, tmp_path):
f = tmp_path / "info.py"
f.write_text(
"def process(data: list) -> list:\n"
" result = []\n"
" for item in data:\n"
" result.append(item)\n"
" return result\n"
)
result = plugin.check(str(f))
from seedgo.models import Severity
failed = [c for c in result.checks if not c.passed]
assert all(c.severity == Severity.INFO for c in failed)
def test_missing_docstring_still_passes_overall(self, plugin, tmp_path):
"""INFO violations should not cause overall failure."""
f = tmp_path / "no_doc.py"
f.write_text(
"def process(data: list) -> list:\n"
" result = []\n"
" for item in data:\n"
" result.append(item)\n"
" return result\n"
)
result = plugin.check(str(f))
# Overall passed=True even when there are INFO violations
assert result.passed is True
def test_private_function_skipped(self, plugin, tmp_path):
f = tmp_path / "private.py"
f.write_text(
"def _helper(x: int) -> int:\n"
" y = x + 1\n"
" z = y * 2\n"
" return z\n"
)
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 0
def test_short_function_skipped(self, plugin, tmp_path):
"""Functions with < 3 body statements are exempt."""
f = tmp_path / "short.py"
f.write_text(
"def tiny(x):\n"
" return x\n"
)
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 0
def test_class_without_docstring_flagged(self, plugin, tmp_path):
f = tmp_path / "class_no_doc.py"
f.write_text(
"class MyService:\n"
" pass\n"
)
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 1
assert "MyService" in failed[0].message
def test_class_with_docstring_passes(self, plugin, tmp_path):
f = tmp_path / "class_doc.py"
f.write_text(
'class MyService:\n'
' """Service for handling requests."""\n'
' pass\n'
)
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
# MyService class should pass
class_fails = [c for c in failed if "MyService" in c.message]
assert len(class_fails) == 0
def test_empty_file_passes(self, plugin, tmp_path):
f = tmp_path / "empty.py"
f.write_text("")
result = plugin.check(str(f))
assert result.passed is True
def test_nonexistent_file_passes(self, plugin, tmp_path):
result = plugin.check(str(tmp_path / "ghost.py"))
assert result.passed is True
def test_syntax_error_skipped(self, plugin, tmp_path):
f = tmp_path / "bad.py"
f.write_text("def broken(\n")
result = plugin.check(str(f))
assert result.passed is True
# ---------------------------------------------------------------------------
# Plugin: function-length
# ---------------------------------------------------------------------------
class TestFunctionLength:
@pytest.fixture
def plugin(self):
return _load_plugin(Path("function_length.py"))
def test_plugin_name(self, plugin):
assert plugin.PLUGIN_NAME == "function-length"
def test_short_function_passes(self, plugin, tmp_path):
f = tmp_path / "short.py"
lines = ["def small_func() -> None:\n"]
lines += [f" x_{i} = {i}\n" for i in range(10)]
f.write_text("".join(lines))
result = plugin.check(str(f))
assert result.passed is True
def test_long_function_fails(self, plugin, tmp_path):
f = tmp_path / "long.py"
lines = ["def huge_func() -> None:\n"]
lines += [f" x_{i} = {i}\n" for i in range(55)]
f.write_text("".join(lines))
result = plugin.check(str(f))
assert result.passed is False
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 1
assert "huge_func" in failed[0].message
def test_default_max_lines_is_50(self, plugin, tmp_path):
"""A function of exactly 50 lines should pass."""
f = tmp_path / "border.py"
lines = ["def border_func() -> None:\n"]
lines += [f" x_{i} = {i}\n" for i in range(49)]
f.write_text("".join(lines))
result = plugin.check(str(f))
assert result.passed is True
def test_function_at_51_lines_fails(self, plugin, tmp_path):
f = tmp_path / "just_over.py"
lines = ["def over_func() -> None:\n"]
lines += [f" x_{i} = {i}\n" for i in range(50)]
f.write_text("".join(lines))
result = plugin.check(str(f))
assert result.passed is False
def test_custom_max_lines_config(self, plugin, tmp_path):
"""With max_lines=10, a 15-line function should fail."""
f = tmp_path / "custom.py"
lines = ["def medium_func() -> None:\n"]
lines += [f" x_{i} = {i}\n" for i in range(15)]
f.write_text("".join(lines))
result = plugin.check(str(f), config={"max_lines": 10})
assert result.passed is False
def test_custom_max_lines_pass(self, plugin, tmp_path):
"""With max_lines=100, a 50-line function should pass."""
f = tmp_path / "custom_pass.py"
lines = ["def medium_func() -> None:\n"]
lines += [f" x_{i} = {i}\n" for i in range(50)]
f.write_text("".join(lines))
result = plugin.check(str(f), config={"max_lines": 100})
assert result.passed is True
def test_multiple_long_functions_all_reported(self, plugin, tmp_path):
f = tmp_path / "multi.py"
lines = []
for fn in ["func_a", "func_b"]:
lines.append(f"def {fn}() -> None:\n")
lines += [f" x_{i} = {i}\n" for i in range(55)]
f.write_text("".join(lines))
result = plugin.check(str(f))
assert result.passed is False
failed = [c for c in result.checks if not c.passed]
assert len(failed) == 2
def test_empty_file_passes(self, plugin, tmp_path):
f = tmp_path / "empty.py"
f.write_text("")
result = plugin.check(str(f))
assert result.passed is True
def test_nonexistent_file_passes(self, plugin, tmp_path):
result = plugin.check(str(tmp_path / "ghost.py"))
assert result.passed is True
def test_syntax_error_skipped(self, plugin, tmp_path):
f = tmp_path / "bad.py"
f.write_text("def broken(\n")
result = plugin.check(str(f))
assert result.passed is True
def test_violation_has_line_number(self, plugin, tmp_path):
f = tmp_path / "line.py"
lines = ["def long_fn() -> None:\n"]
lines += [f" x_{i} = {i}\n" for i in range(55)]
f.write_text("".join(lines))
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
assert len(failed) > 0
assert failed[0].line == 1
def test_metadata_contains_max_lines(self, plugin, tmp_path):
f = tmp_path / "meta.py"
f.write_text("x = 1\n")
result = plugin.check(str(f), config={"max_lines": 30})
assert result.metadata.get("max_lines") == 30
def test_violation_has_fix_hint(self, plugin, tmp_path):
f = tmp_path / "hint.py"
lines = ["def big_func() -> None:\n"]
lines += [f" x_{i} = {i}\n" for i in range(55)]
f.write_text("".join(lines))
result = plugin.check(str(f))
failed = [c for c in result.checks if not c.passed]
assert len(failed) > 0
assert failed[0].fix_hint is not None
# ---------------------------------------------------------------------------
# Plugin: file-structure
# ---------------------------------------------------------------------------
class TestFileStructure:
@pytest.fixture
def plugin(self):
return _load_plugin(Path("file_structure.py"))
@pytest.fixture
def project_with_seedgo(self, tmp_path):
"""Create a minimal project with .seedgo marker."""
(tmp_path / ".seedgo").mkdir()
return tmp_path
def test_plugin_name(self, plugin):
assert plugin.PLUGIN_NAME == "file-structure"
def test_source_file_in_package_passes(self, plugin, project_with_seedgo):
"""A .py file inside a proper package (with __init__.py) should pass."""
pkg = project_with_seedgo / "src" / "mypackage"
pkg.mkdir(parents=True)
(pkg / "__init__.py").write_text("")
src = pkg / "module.py"
src.write_text("x = 1\n")
result = plugin.check(str(src))
failed = [c for c in result.checks if not c.passed]
# Should not have missing-init-py violation
init_fails = [c for c in failed if c.name == "missing-init-py"]
assert len(init_fails) == 0
def test_test_file_in_tests_dir_passes(self, plugin, project_with_seedgo):
"""test_*.py inside tests/ should pass the placement check."""
tests_dir = project_with_seedgo / "tests"
tests_dir.mkdir()
test_file = tests_dir / "test_module.py"
test_file.write_text("def test_something(): pass\n")
result = plugin.check(str(test_file))
failed = [c for c in result.checks if not c.passed]
placement_fails = [c for c in failed if c.name == "test-file-placement"]
assert len(placement_fails) == 0
def test_test_file_at_root_fails(self, plugin, project_with_seedgo):
"""test_*.py at the project root should fail the placement check."""
test_file = project_with_seedgo / "test_something.py"
test_file.write_text("def test_foo(): pass\n")
result = plugin.check(str(test_file))
failed = [c for c in result.checks if not c.passed]
placement_fails = [c for c in failed if c.name == "test-file-placement"]
assert len(placement_fails) == 1
def test_source_file_at_root_fails(self, plugin, project_with_seedgo):
"""A regular .py file at project root (not in allowed_root_files) should fail."""
src = project_with_seedgo / "my_module.py"
src.write_text("x = 1\n")
result = plugin.check(str(src))
failed = [c for c in result.checks if not c.passed]
root_fails = [c for c in failed if c.name == "root-python-file"]
assert len(root_fails) == 1
def test_allowed_root_file_passes(self, plugin, project_with_seedgo):
"""setup.py at root is allowed by default."""
setup = project_with_seedgo / "setup.py"
setup.write_text("from setuptools import setup\nsetup()\n")
result = plugin.check(str(setup))
failed = [c for c in result.checks if not c.passed]
root_fails = [c for c in failed if c.name == "root-python-file"]
assert len(root_fails) == 0
def test_custom_allowed_root_files(self, plugin, project_with_seedgo):
"""Custom allowed_root_files config should be respected."""
custom = project_with_seedgo / "fabfile.py"
custom.write_text("x = 1\n")
# With default config, fabfile.py is already in DEFAULT_ALLOWED_ROOT_FILES
# so let's test a truly custom file
myfile = project_with_seedgo / "myapp.py"
myfile.write_text("x = 1\n")
# Without config — should fail
result_no_config = plugin.check(str(myfile))
failed_no = [c for c in result_no_config.checks if not c.passed and c.name == "root-python-file"]
assert len(failed_no) == 1
# With custom config allowing myapp.py — should pass
result_with_config = plugin.check(str(myfile), config={"allowed_root_files": ["myapp.py"]})
failed_yes = [c for c in result_with_config.checks if not c.passed and c.name == "root-python-file"]
assert len(failed_yes) == 0
def test_missing_init_py_flagged(self, plugin, project_with_seedgo):
"""A package directory without __init__.py should be flagged."""
pkg = project_with_seedgo / "src" / "mypackage"
pkg.mkdir(parents=True)
# Note: no __init__.py
src = pkg / "module.py"
src.write_text("x = 1\n")
# Also create another .py so the dir clearly has sources
(pkg / "other.py").write_text("y = 1\n")
result = plugin.check(str(src))
failed = [c for c in result.checks if not c.passed]
init_fails = [c for c in failed if c.name == "missing-init-py"]
assert len(init_fails) == 1
def test_nonexistent_file_passes(self, plugin, tmp_path):
result = plugin.check(str(tmp_path / "ghost.py"))
assert result.passed is True
def test_conftest_at_root_passes(self, plugin, project_with_seedgo):
"""conftest.py is a standard root-level file."""
conf = project_with_seedgo / "conftest.py"
conf.write_text("import pytest\n")
result = plugin.check(str(conf))
failed = [c for c in result.checks if not c.passed]
root_fails = [c for c in failed if c.name == "root-python-file"]
assert len(root_fails) == 0
def test_result_has_plugin_name(self, plugin, tmp_path):
f = tmp_path / "x.py"
f.write_text("")
result = plugin.check(str(f))
assert result.plugin == "file-structure"
# ---------------------------------------------------------------------------
# Plugin discovery integration tests
# ---------------------------------------------------------------------------
class TestPluginDiscovery:
def test_all_plugins_discoverable(self):
"""All 5 plugins should be discovered as built-ins."""
from seedgo.discovery import discover_plugins
plugins = discover_plugins()
names = {p["name"] for p in plugins}
assert "no-bare-except" in names
assert "type-hints-required" in names
assert "docstring-coverage" in names
assert "function-length" in names
assert "file-structure" in names
def test_all_plugins_have_required_attributes(self):
"""Every discovered plugin must have PLUGIN_NAME, check(), and FILE_TYPES."""
from seedgo.discovery import discover_plugins
plugins = discover_plugins()
for p in plugins:
module = p["module"]
assert isinstance(getattr(module, "PLUGIN_NAME", None), str)
assert callable(getattr(module, "check", None))
assert isinstance(getattr(module, "FILE_TYPES", None), list)
def test_plugins_return_check_result(self, tmp_path):
"""Every plugin's check() must return a CheckResult instance."""
from seedgo.discovery import discover_plugins
from seedgo.models import CheckResult
# Create a minimal Python file to run checks on
test_file = tmp_path / "test_subject.py"
test_file.write_text("x = 1\n")
plugins = discover_plugins()
for p in plugins:
module = p["module"]
result = module.check(str(test_file))
assert isinstance(result, CheckResult), (
f"Plugin {p['name']} returned {type(result)} instead of CheckResult"
)
def test_runner_executes_all_plugins(self, tmp_path):
"""run_checks should execute all enabled plugins."""
import json
from seedgo.runner import run_checks
# Set up minimal project
seedgo_dir = tmp_path / ".seedgo"
seedgo_dir.mkdir()
(seedgo_dir / "plugins").mkdir()
config = {
"version": "1.0.0",
"plugins": {
"enabled": [
"no-bare-except",
"type-hints-required",
"docstring-coverage",
"function-length",
"file-structure",
],
"disabled": [],
"config": {},
},
"scoring": {"threshold": 75},
"paths": {"include": ["."], "exclude": []},
"overrides": [],
}
(seedgo_dir / "config.json").write_text(json.dumps(config))
# Create a simple Python source file
src = tmp_path / "hello.py"
src.write_text(
'"""A simple module."""\n'
"\n"
"def hello() -> str:\n"
' """Say hello."""\n'
" return 'Hello'\n"
)
results, overall = run_checks(str(tmp_path), files=[str(src)])
assert isinstance(results, list)
assert len(results) > 0
plugin_names_run = {r.plugin for r in results}
# At least some of our plugins should have run
assert len(plugin_names_run) > 0