test_data_manager.py•16.2 kB
"""
Property-Based Tests for Data Manager
Tests correctness properties using Hypothesis for property-based testing.
"""
import json
import os
import tempfile
import shutil
from hypothesis import given, strategies as st, settings, HealthCheck
import pytest
from data_manager import (
load_best_practices,
get_fallback_guidance,
FileLoadError,
ValidationError
)
# Test fixtures and helpers
@pytest.fixture
def temp_best_practices_file():
"""Create a temporary best practices file for testing."""
temp_dir = tempfile.mkdtemp()
temp_file = os.path.join(temp_dir, "test_best_practices.json")
# Create a valid test file
test_data = {
"python_best_practices": {
"test_category": {
"test_topic": {
"description": "Test description",
"examples": {
"example1": "test code"
}
}
}
}
}
with open(temp_file, 'w') as f:
json.dump(test_data, f)
# Set environment variable
old_env = os.environ.get("BEST_PRACTICES_FILE")
os.environ["BEST_PRACTICES_FILE"] = temp_file
yield temp_file
# Cleanup
if old_env:
os.environ["BEST_PRACTICES_FILE"] = old_env
else:
os.environ.pop("BEST_PRACTICES_FILE", None)
shutil.rmtree(temp_dir)
# Property 11: Dynamic database reloading
# Feature: bestpractices-mcp-server, Property 11: Dynamic database reloading
def test_dynamic_reload_property(temp_best_practices_file):
"""
Property 11: For any database modification, the next request should reflect the updated content.
Validates: Requirements 3.4, 9.2
"""
# Load initial data
initial_data = load_best_practices()
initial_categories = list(initial_data["python_best_practices"].keys())
# Modify the file
modified_data = {
"python_best_practices": {
"new_category": {
"new_topic": {
"description": "New description",
"examples": {
"new_example": "new code"
}
}
}
}
}
with open(temp_best_practices_file, 'w') as f:
json.dump(modified_data, f)
# Load again - should reflect changes
updated_data = load_best_practices()
updated_categories = list(updated_data["python_best_practices"].keys())
# Verify the data changed
assert initial_categories != updated_categories
assert "new_category" in updated_categories
assert "new_category" not in initial_categories
assert updated_data["python_best_practices"]["new_category"]["new_topic"]["description"] == "New description"
@settings(max_examples=100, suppress_health_check=[HealthCheck.function_scoped_fixture])
@given(
category_name=st.text(min_size=1, max_size=50, alphabet=st.characters(whitelist_categories=('Lu', 'Ll', 'Nd')) | st.just('_')),
topic_name=st.text(min_size=1, max_size=50, alphabet=st.characters(whitelist_categories=('Lu', 'Ll', 'Nd')) | st.just('_')),
description=st.text(min_size=1, max_size=200)
)
def test_dynamic_reload_with_random_data(temp_best_practices_file, category_name, topic_name, description):
"""
Property 11 (extended): Dynamic reload works with various data structures.
"""
# Create data with random content
test_data = {
"python_best_practices": {
category_name: {
topic_name: {
"description": description,
"examples": {
"example1": "test"
}
}
}
}
}
with open(temp_best_practices_file, 'w') as f:
json.dump(test_data, f)
# Load and verify
loaded_data = load_best_practices()
assert category_name in loaded_data["python_best_practices"]
assert topic_name in loaded_data["python_best_practices"][category_name]
assert loaded_data["python_best_practices"][category_name][topic_name]["description"] == description
# Additional tests for file loading
def test_load_missing_file():
"""Test that missing file raises appropriate error."""
old_env = os.environ.get("BEST_PRACTICES_FILE")
os.environ["BEST_PRACTICES_FILE"] = "/nonexistent/file.json"
try:
with pytest.raises(FileLoadError) as exc_info:
load_best_practices()
assert "not found" in str(exc_info.value).lower()
finally:
if old_env:
os.environ["BEST_PRACTICES_FILE"] = old_env
else:
os.environ.pop("BEST_PRACTICES_FILE", None)
def test_load_invalid_json(temp_best_practices_file):
"""Test that invalid JSON raises appropriate error."""
# Write invalid JSON
with open(temp_best_practices_file, 'w') as f:
f.write("{ invalid json }")
with pytest.raises(ValidationError) as exc_info:
load_best_practices()
assert "parse" in str(exc_info.value).lower() or "json" in str(exc_info.value).lower()
def test_load_missing_root_key(temp_best_practices_file):
"""Test that missing root key raises validation error."""
with open(temp_best_practices_file, 'w') as f:
json.dump({"wrong_key": {}}, f)
with pytest.raises(ValidationError) as exc_info:
load_best_practices()
assert "python_best_practices" in str(exc_info.value)
def test_fallback_guidance():
"""Test that fallback guidance is provided."""
guidance = get_fallback_guidance()
assert "error" in guidance
assert "fallback_guidance" in guidance
assert "python_basics" in guidance["fallback_guidance"]
assert "fastapi_basics" in guidance["fallback_guidance"]
assert len(guidance["fallback_guidance"]["python_basics"]) > 0
assert len(guidance["fallback_guidance"]["fastapi_basics"]) > 0
# Property 28: JSON validation before processing
# Feature: bestpractices-mcp-server, Property 28: JSON validation before processing
def test_json_validation_before_processing_invalid_structure(temp_best_practices_file):
"""
Property 28: For any database read, validation should happen before processing.
Invalid JSON should return validation errors, not processing errors.
Validates: Requirements 9.5
"""
# Test with invalid structure - should get ValidationError, not processing error
invalid_structures = [
{}, # Missing root key
{"python_best_practices": "not a dict"}, # Wrong type
{"python_best_practices": {}}, # Empty categories
{"python_best_practices": {"cat": "not a dict"}}, # Category not dict
{"python_best_practices": {"cat": {"topic": {}}}}, # Missing description
{"python_best_practices": {"cat": {"topic": {"description": "test"}}}}, # Missing examples
]
for invalid_data in invalid_structures:
with open(temp_best_practices_file, 'w') as f:
json.dump(invalid_data, f)
with pytest.raises(ValidationError):
load_best_practices()
@settings(max_examples=100, suppress_health_check=[HealthCheck.function_scoped_fixture])
@given(
has_root=st.booleans(),
has_categories=st.booleans(),
has_description=st.booleans(),
has_examples=st.booleans()
)
def test_json_validation_property(temp_best_practices_file, has_root, has_categories, has_description, has_examples):
"""
Property 28 (extended): Validation catches various structural issues.
"""
# Build data based on flags
if not has_root:
data = {"wrong_key": {}}
elif not has_categories:
data = {"python_best_practices": {}}
else:
topic_data = {}
if has_description:
topic_data["description"] = "test"
if has_examples:
topic_data["examples"] = {"ex1": "code"}
data = {
"python_best_practices": {
"test_cat": {
"test_topic": topic_data
}
}
}
with open(temp_best_practices_file, 'w') as f:
json.dump(data, f)
# Should succeed only if all required fields present
if has_root and has_categories and has_description and has_examples:
result = load_best_practices()
assert "python_best_practices" in result
else:
with pytest.raises((ValidationError, FileLoadError)):
load_best_practices()
def test_validation_with_tools_instead_of_examples(temp_best_practices_file):
"""Test that 'tools' field is accepted as alternative to 'examples'."""
data = {
"python_best_practices": {
"test_category": {
"test_topic": {
"description": "Test with tools",
"tools": {
"tool1": "tool code"
}
}
}
}
}
with open(temp_best_practices_file, 'w') as f:
json.dump(data, f)
# Should load successfully
result = load_best_practices()
assert "test_category" in result["python_best_practices"]
# Property 1: Search completeness
# Feature: bestpractices-mcp-server, Property 1: Search completeness
@settings(max_examples=100)
@given(keyword=st.text(min_size=1, max_size=50))
def test_search_completeness_property(keyword):
"""
Property 1: For any keyword, all returned search results should contain the keyword.
Validates: Requirements 1.1
"""
from data_manager import search_practices, load_best_practices
try:
data = load_best_practices()
result = search_practices(keyword, data=data)
if "matches" in result and result["count"] > 0:
keyword_lower = keyword.lower().strip()
for match in result["matches"]:
# Convert match to searchable text
match_text = json.dumps(match).lower()
assert keyword_lower in match_text, f"Keyword '{keyword}' not found in match: {match['topic']}"
except Exception:
# If loading fails, that's okay for this test
pass
# Property 2: Search result structure
# Feature: bestpractices-mcp-server, Property 2: Search result structure
def test_search_result_structure_property():
"""
Property 2: For any search that returns results, each result should include required fields.
Validates: Requirements 1.2
"""
from data_manager import search_practices
# Search for a term we know exists
result = search_practices("async")
if result.get("count", 0) > 0:
for match in result["matches"]:
assert "category" in match
assert "topic" in match
assert "description" in match
assert "examples" in match
assert "related_topics" in match
assert isinstance(match["examples"], list)
assert isinstance(match["related_topics"], list)
# Property 3: Search fallback suggestions
# Feature: bestpractices-mcp-server, Property 3: Search fallback suggestions
def test_search_fallback_suggestions_property():
"""
Property 3: For any keyword that produces no exact matches, system should return suggestions.
Validates: Requirements 1.3
"""
from data_manager import search_practices
# Search for something that doesn't exist
result = search_practices("xyznonexistent123")
if result.get("count", 0) == 0:
assert "suggestions" in result or "message" in result
# Property 5: Category completeness
# Feature: bestpractices-mcp-server, Property 5: Category completeness
@settings(max_examples=20)
@given(category=st.sampled_from(["general_coding", "fastapi_specific", "performance", "code_quality"]))
def test_category_completeness_property(category):
"""
Property 5: For any valid category, requesting it should return all topics.
Validates: Requirements 2.1
"""
from data_manager import get_category, load_best_practices
data = load_best_practices()
result = get_category(category, data=data)
# Get expected topics from raw data
expected_topics = set(data["python_best_practices"][category].keys())
returned_topics = set(t["name"] for t in result.get("topics", []))
assert expected_topics == returned_topics
# Property 7: Invalid category suggestions
# Feature: bestpractices-mcp-server, Property 7: Invalid category suggestions
def test_invalid_category_suggestions_property():
"""
Property 7: For any invalid category name, system should suggest valid categories.
Validates: Requirements 2.3
"""
from data_manager import get_category
result = get_category("invalid_category_xyz")
assert "error" in result
assert "suggestions" in result or "available_categories" in result
# Property 8: Hierarchical structure preservation
# Feature: bestpractices-mcp-server, Property 8: Hierarchical structure preservation
def test_hierarchical_structure_preservation_property():
"""
Property 8: For any category with nested topics, response should preserve structure.
Validates: Requirements 2.5
"""
from data_manager import get_category
result = get_category("fastapi_specific")
# Check structure is preserved
assert "topics" in result
for topic in result["topics"]:
assert "name" in topic
assert "description" in topic
assert "examples" in topic
# Property 9: Category list completeness
# Feature: bestpractices-mcp-server, Property 9: Category list completeness
def test_category_list_completeness_property():
"""
Property 9: Category list should include all top-level categories from database.
Validates: Requirements 3.1
"""
from data_manager import list_all_categories, load_best_practices
data = load_best_practices()
result = list_all_categories(data=data)
expected_categories = set(data["python_best_practices"].keys())
returned_categories = set(c["name"] for c in result["categories"])
assert expected_categories == returned_categories
# Property 10: Category list structure
# Feature: bestpractices-mcp-server, Property 10: Category list structure
def test_category_list_structure_property():
"""
Property 10: For any category in list, it should include description and topic count.
Validates: Requirements 3.2, 3.3
"""
from data_manager import list_all_categories
result = list_all_categories()
assert "categories" in result
for category in result["categories"]:
assert "name" in category
assert "description" in category
assert "topic_count" in category
assert isinstance(category["topic_count"], int)
assert category["topic_count"] > 0
# Property 12: Example completeness
# Feature: bestpractices-mcp-server, Property 12: Example completeness
def test_example_completeness_property():
"""
Property 12: For any valid topic, all associated code examples should be returned.
Validates: Requirements 4.1
"""
from data_manager import get_examples, load_best_practices
data = load_best_practices()
# Test with a known topic
topic = "type_hints"
result = get_examples(topic, data=data)
# Get expected examples from raw data
for cat_data in data["python_best_practices"].values():
if topic in cat_data:
expected_examples = cat_data[topic].get("examples", {})
assert result["count"] == len(expected_examples)
break
# Property 15: Example formatting
# Feature: bestpractices-mcp-server, Property 15: Example formatting
def test_example_formatting_property():
"""
Property 15: For any code example, it should be formatted with syntax highlighting markers.
Validates: Requirements 4.4
"""
from data_manager import get_examples
result = get_examples("type_hints")
if "examples" in result:
for example in result["examples"]:
assert "code" in example
# Check for markdown code block markers
assert "```" in example["code"]