Add initial project structure and core functionality for ArchDoc

- Created `.gitignore` files for various directories to exclude unnecessary files.
- Added `PLAN.md` to outline the project goals and architecture documentation generation.
- Implemented the `archdoc-cli` with a command-line interface for initializing and generating documentation.
- Developed the `archdoc-core` library for analyzing Python projects and generating architecture documentation.
- Included caching mechanisms to optimize repeated analysis.
- Established a comprehensive test suite to ensure functionality and error handling.
- Updated `README.md` to provide an overview and installation instructions for ArchDoc.
This commit is contained in:
2026-01-25 20:17:37 +03:00
commit 3701cee205
36 changed files with 7394 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
//! Caching tests for ArchDoc
//!
//! These tests verify that the caching functionality works correctly.
use std::path::Path;
use std::fs;
use tempfile::TempDir;
use archdoc_core::{Config, python_analyzer::PythonAnalyzer};
#[test]
fn test_cache_store_and_retrieve() {
let config = Config::default();
let analyzer = PythonAnalyzer::new(config);
// Create a temporary Python file
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test.py");
let python_code = r#"
def hello():
return "Hello, World!"
class Calculator:
def add(self, a, b):
return a + b
"#;
fs::write(&temp_file, python_code).expect("Failed to write test file");
// Parse the module for the first time
let parsed_module1 = analyzer.parse_module(&temp_file)
.expect("Failed to parse module first time");
// Parse the module again - should come from cache
let parsed_module2 = analyzer.parse_module(&temp_file)
.expect("Failed to parse module second time");
// Both parses should return the same data
assert_eq!(parsed_module1.path, parsed_module2.path);
assert_eq!(parsed_module1.module_path, parsed_module2.module_path);
assert_eq!(parsed_module1.imports.len(), parsed_module2.imports.len());
assert_eq!(parsed_module1.symbols.len(), parsed_module2.symbols.len());
assert_eq!(parsed_module1.calls.len(), parsed_module2.calls.len());
}
#[test]
fn test_cache_invalidation_on_file_change() {
let config = Config::default();
let analyzer = PythonAnalyzer::new(config);
// Create a temporary Python file
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test.py");
let python_code1 = r#"
def hello():
return "Hello, World!"
"#;
fs::write(&temp_file, python_code1).expect("Failed to write test file");
// Parse the module for the first time
let parsed_module1 = analyzer.parse_module(&temp_file)
.expect("Failed to parse module first time");
// Modify the file
let python_code2 = r#"
def hello():
return "Hello, World!"
def goodbye():
return "Goodbye, World!"
"#;
fs::write(&temp_file, python_code2).expect("Failed to write test file");
// Parse the module again - should NOT come from cache due to file change
let parsed_module2 = analyzer.parse_module(&temp_file)
.expect("Failed to parse module second time");
// The second parse should have more symbols
assert!(parsed_module2.symbols.len() >= parsed_module1.symbols.len());
}
#[test]
fn test_cache_disabled() {
let mut config = Config::default();
config.caching.enabled = false;
let analyzer = PythonAnalyzer::new(config);
// Create a temporary Python file
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test.py");
let python_code = r#"
def hello():
return "Hello, World!"
"#;
fs::write(&temp_file, python_code).expect("Failed to write test file");
// Parse the module - should work even with caching disabled
let parsed_module = analyzer.parse_module(&temp_file)
.expect("Failed to parse module with caching disabled");
assert_eq!(parsed_module.symbols.len(), 1);
}

View File

@@ -0,0 +1,131 @@
//! Enhanced analysis tests for ArchDoc
//!
//! These tests verify that the enhanced analysis functionality works correctly
//! with complex code that includes integrations, calls, and docstrings.
use std::fs;
use std::path::Path;
use archdoc_core::{Config, scanner::FileScanner, python_analyzer::PythonAnalyzer};
#[test]
fn test_enhanced_analysis_with_integrations() {
// Print current directory for debugging
let current_dir = std::env::current_dir().unwrap();
println!("Current directory: {:?}", current_dir);
// Try different paths for the config file
let possible_paths = [
"tests/golden/test_project/archdoc.toml",
"../tests/golden/test_project/archdoc.toml",
];
let config_path = possible_paths.iter().find(|&path| {
Path::new(path).exists()
}).expect("Could not find config file in any expected location");
println!("Using config path: {:?}", config_path);
let config = Config::load_from_file(Path::new(config_path)).expect("Failed to load config");
// Initialize scanner with the correct root path
let project_root = Path::new("tests/golden/test_project");
let scanner = FileScanner::new(config.clone());
// Scan for Python files
let python_files = scanner.scan_python_files(project_root)
.expect("Failed to scan Python files");
println!("Found Python files: {:?}", python_files);
// Should find both example.py and advanced_example.py
assert_eq!(python_files.len(), 2);
// Initialize Python analyzer
let analyzer = PythonAnalyzer::new(config.clone());
// Parse each Python file
let mut parsed_modules = Vec::new();
for file_path in python_files {
println!("Parsing file: {:?}", file_path);
match analyzer.parse_module(&file_path) {
Ok(module) => {
println!("Successfully parsed module: {:?}", module.module_path);
println!("Imports: {:?}", module.imports);
println!("Symbols: {:?}", module.symbols.len());
println!("Calls: {:?}", module.calls.len());
parsed_modules.push(module);
},
Err(e) => {
panic!("Failed to parse {}: {}", file_path.display(), e);
}
}
}
println!("Parsed {} modules", parsed_modules.len());
// Resolve symbols and build project model
let project_model = analyzer.resolve_symbols(&parsed_modules)
.expect("Failed to resolve symbols");
println!("Project model modules: {}", project_model.modules.len());
println!("Project model files: {}", project_model.files.len());
println!("Project model symbols: {}", project_model.symbols.len());
// Add assertions to verify the project model
assert!(!project_model.modules.is_empty());
assert!(!project_model.files.is_empty());
assert!(!project_model.symbols.is_empty());
// Check that we have the right number of modules (2 files = 2 modules)
assert_eq!(project_model.modules.len(), 2);
// Check that we have the right number of files
assert_eq!(project_model.files.len(), 2);
// Check that we have the right number of symbols
// The actual number might be less due to deduplication or other factors
// but should be at least the sum of symbols from both files minus duplicates
assert!(project_model.symbols.len() >= 10);
// Check specific details about the advanced example module
let mut found_advanced_module = false;
for (_, module) in project_model.modules.iter() {
if module.path.contains("advanced_example.py") {
found_advanced_module = true;
break;
}
}
assert!(found_advanced_module);
// Check that we found the UserService class with DB integration
let user_service_symbol = project_model.symbols.values().find(|s| s.id == "UserService");
assert!(user_service_symbol.is_some());
assert_eq!(user_service_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Class);
// Check that we found the NotificationService class with queue integration
let notification_service_symbol = project_model.symbols.values().find(|s| s.id == "NotificationService");
assert!(notification_service_symbol.is_some());
assert_eq!(notification_service_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Class);
// Check that we found the fetch_external_user_data function with HTTP integration
let fetch_external_user_data_symbol = project_model.symbols.values().find(|s| s.id == "fetch_external_user_data");
assert!(fetch_external_user_data_symbol.is_some());
assert_eq!(fetch_external_user_data_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Function);
// Check file imports
let mut found_advanced_file = false;
for (_, file_doc) in project_model.files.iter() {
if file_doc.path.contains("advanced_example.py") {
found_advanced_file = true;
assert!(!file_doc.imports.is_empty());
// Should have imports for requests, sqlite3, redis, typing
let import_names: Vec<&String> = file_doc.imports.iter().collect();
assert!(import_names.contains(&&"requests".to_string()));
assert!(import_names.contains(&&"sqlite3".to_string()));
assert!(import_names.contains(&&"redis".to_string()));
assert!(import_names.contains(&&"typing.List".to_string()) || import_names.contains(&&"typing".to_string()));
break;
}
}
assert!(found_advanced_file);
}

View File

@@ -0,0 +1,83 @@
//! Error handling tests for ArchDoc
//!
//! These tests verify that ArchDoc properly handles various error conditions
//! and edge cases.
use std::path::Path;
use std::fs;
use tempfile::TempDir;
use archdoc_core::{Config, scanner::FileScanner, python_analyzer::PythonAnalyzer};
#[test]
fn test_scanner_nonexistent_directory() {
let config = Config::default();
let scanner = FileScanner::new(config);
// Try to scan a nonexistent directory
let result = scanner.scan_python_files(Path::new("/nonexistent/directory"));
assert!(result.is_err());
// Check that we get an IO error
match result.unwrap_err() {
archdoc_core::errors::ArchDocError::Io(_) => {},
_ => panic!("Expected IO error"),
}
}
#[test]
fn test_scanner_file_instead_of_directory() {
let config = Config::default();
let scanner = FileScanner::new(config);
// Create a temporary file
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test.txt");
fs::write(&temp_file, "test content").expect("Failed to write test file");
// Try to scan a file instead of a directory
let result = scanner.scan_python_files(&temp_file);
assert!(result.is_err());
// Check that we get an IO error
match result.unwrap_err() {
archdoc_core::errors::ArchDocError::Io(_) => {},
_ => panic!("Expected IO error"),
}
}
#[test]
fn test_analyzer_nonexistent_file() {
let config = Config::default();
let analyzer = PythonAnalyzer::new(config);
// Try to parse a nonexistent file
let result = analyzer.parse_module(Path::new("/nonexistent/file.py"));
assert!(result.is_err());
// Check that we get an IO error
match result.unwrap_err() {
archdoc_core::errors::ArchDocError::Io(_) => {},
_ => panic!("Expected IO error"),
}
}
#[test]
fn test_analyzer_invalid_python_syntax() {
let config = Config::default();
let analyzer = PythonAnalyzer::new(config);
// Create a temporary file with invalid Python syntax
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("invalid.py");
fs::write(&temp_file, "invalid python syntax @@#$%").expect("Failed to write test file");
// Try to parse the file
let result = analyzer.parse_module(&temp_file);
assert!(result.is_err());
// Check that we get a parse error
match result.unwrap_err() {
archdoc_core::errors::ArchDocError::ParseError { .. } => {},
_ => panic!("Expected parse error"),
}
}

View File

@@ -0,0 +1,60 @@
# Architecture Documentation
Generated at: 1970-01-01 00:00:00 UTC
## Overview
This document provides an overview of the architecture for the project.
## Modules
### example.py
File: `example.py`
#### Imports
- `os`
- `typing.List`
#### Symbols
##### Calculator
- Type: Class
- Signature: `class Calculator`
- Purpose: extracted from AST
##### Calculator.__init__
- Type: Function
- Signature: `def __init__(...)`
- Purpose: extracted from AST
##### Calculator.add
- Type: Function
- Signature: `def add(...)`
- Purpose: extracted from AST
##### Calculator.multiply
- Type: Function
- Signature: `def multiply(...)`
- Purpose: extracted from AST
##### process_numbers
- Type: Function
- Signature: `def process_numbers(...)`
- Purpose: extracted from AST
## Metrics
### Critical Components
No critical components identified.
### Component Dependencies
Dependency analysis not yet implemented.

View File

@@ -0,0 +1,107 @@
//! Golden tests for ArchDoc
//!
//! These tests generate documentation for test projects and compare the output
//! with expected "golden" files to ensure consistency.
mod test_utils;
use std::fs;
use std::path::Path;
use archdoc_core::{Config, scanner::FileScanner, python_analyzer::PythonAnalyzer};
#[test]
fn test_simple_project_generation() {
// Print current directory for debugging
let current_dir = std::env::current_dir().unwrap();
println!("Current directory: {:?}", current_dir);
// Try different paths for the config file
let possible_paths = [
"tests/golden/test_project/archdoc.toml",
"../tests/golden/test_project/archdoc.toml",
];
let config_path = possible_paths.iter().find(|&path| {
Path::new(path).exists()
}).expect("Could not find config file in any expected location");
println!("Using config path: {:?}", config_path);
let config = Config::load_from_file(Path::new(config_path)).expect("Failed to load config");
// Initialize scanner with the correct root path
let project_root = Path::new("tests/golden/test_project");
let scanner = FileScanner::new(config.clone());
// Scan for Python files
let python_files = scanner.scan_python_files(project_root)
.expect("Failed to scan Python files");
println!("Found Python files: {:?}", python_files);
// Initialize Python analyzer
let analyzer = PythonAnalyzer::new(config.clone());
// Parse each Python file
let mut parsed_modules = Vec::new();
for file_path in python_files {
println!("Parsing file: {:?}", file_path);
match analyzer.parse_module(&file_path) {
Ok(module) => {
println!("Successfully parsed module: {:?}", module.module_path);
println!("Imports: {:?}", module.imports);
println!("Symbols: {:?}", module.symbols.len());
println!("Calls: {:?}", module.calls.len());
parsed_modules.push(module);
},
Err(e) => {
panic!("Failed to parse {}: {}", file_path.display(), e);
}
}
}
println!("Parsed {} modules", parsed_modules.len());
// Resolve symbols and build project model
let project_model = analyzer.resolve_symbols(&parsed_modules)
.expect("Failed to resolve symbols");
println!("Project model modules: {}", project_model.modules.len());
println!("Project model files: {}", project_model.files.len());
println!("Project model symbols: {}", project_model.symbols.len());
// Add assertions to verify the project model
assert!(!project_model.modules.is_empty());
assert!(!project_model.files.is_empty());
assert!(!project_model.symbols.is_empty());
// Check specific details about the parsed modules
// Now we have 2 modules (example.py and advanced_example.py)
assert_eq!(project_model.modules.len(), 2);
// Find the example.py module
let mut found_example_module = false;
for (_, module) in project_model.modules.iter() {
if module.path.contains("example.py") {
found_example_module = true;
break;
}
}
assert!(found_example_module);
// Check that we found the Calculator class
let calculator_symbol = project_model.symbols.values().find(|s| s.id == "Calculator");
assert!(calculator_symbol.is_some());
assert_eq!(calculator_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Class);
// Check that we found the process_numbers function
let process_numbers_symbol = project_model.symbols.values().find(|s| s.id == "process_numbers");
assert!(process_numbers_symbol.is_some());
assert_eq!(process_numbers_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Function);
// Check file imports
assert!(!project_model.files.is_empty());
let file_entry = project_model.files.iter().next().unwrap();
let file_doc = file_entry.1;
assert!(!file_doc.imports.is_empty());
}

View File

@@ -0,0 +1,107 @@
"""Advanced example module for testing with integrations."""
import requests
import sqlite3
import redis
from typing import List, Dict
class UserService:
"""A service for managing users with database integration."""
def __init__(self, db_path: str = "users.db"):
"""Initialize the user service with database path."""
self.db_path = db_path
self._init_db()
def _init_db(self):
"""Initialize the database."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
""")
conn.commit()
conn.close()
def create_user(self, name: str, email: str) -> Dict:
"""Create a new user in the database."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
(name, email)
)
user_id = cursor.lastrowid
conn.commit()
conn.close()
return {"id": user_id, "name": name, "email": email}
def get_user(self, user_id: int) -> Dict:
"""Get a user by ID from the database."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
row = cursor.fetchone()
conn.close()
if row:
return {"id": row[0], "name": row[1], "email": row[2]}
return None
class NotificationService:
"""A service for sending notifications with queue integration."""
def __init__(self, redis_url: str = "redis://localhost:6379"):
"""Initialize the notification service with Redis URL."""
self.redis_client = redis.Redis.from_url(redis_url)
def send_email_notification(self, user_id: int, message: str) -> bool:
"""Send an email notification by queuing it."""
notification = {
"user_id": user_id,
"message": message,
"type": "email"
}
# Push to Redis queue
self.redis_client.lpush("notifications", str(notification))
return True
def fetch_external_user_data(user_id: int) -> Dict:
"""Fetch user data from an external API."""
response = requests.get(f"https://api.example.com/users/{user_id}")
if response.status_code == 200:
return response.json()
return {}
def process_users(user_ids: List[int]) -> List[Dict]:
"""Process a list of users with various integrations."""
# Database integration
user_service = UserService()
# Queue integration
notification_service = NotificationService()
results = []
for user_id in user_ids:
# Database operation
user = user_service.get_user(user_id)
if user:
# External API integration
external_data = fetch_external_user_data(user_id)
user.update(external_data)
# Queue operation
notification_service.send_email_notification(
user_id,
f"Processing user {user['name']}"
)
results.append(user)
return results

View File

@@ -0,0 +1,29 @@
"""Example module for testing."""
import os
from typing import List
class Calculator:
"""A simple calculator class."""
def __init__(self):
"""Initialize the calculator."""
pass
def add(self, a: int, b: int) -> int:
"""Add two numbers."""
return a + b
def multiply(self, a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
def process_numbers(numbers: List[int]) -> List[int]:
"""Process a list of numbers."""
calc = Calculator()
return [calc.add(n, 1) for n in numbers]
if __name__ == "__main__":
numbers = [1, 2, 3, 4, 5]
result = process_numbers(numbers)
print(f"Processed numbers: {result}")

View File

@@ -0,0 +1,21 @@
//! Test utilities for golden tests
use std::fs;
use std::path::Path;
/// Read a file and return its contents
pub fn read_test_file(path: &str) -> String {
fs::read_to_string(path).expect(&format!("Failed to read test file: {}", path))
}
/// Write content to a file for testing
pub fn write_test_file(path: &str, content: &str) {
fs::write(path, content).expect(&format!("Failed to write test file: {}", path))
}
/// Compare two strings and panic if they don't match
pub fn assert_strings_equal(actual: &str, expected: &str, message: &str) {
if actual != expected {
panic!("{}: Strings do not match\nActual:\n{}\nExpected:\n{}", message, actual, expected);
}
}

View File

@@ -0,0 +1,134 @@
//! Integration detection tests for ArchDoc
//!
//! These tests verify that the integration detection functionality works correctly.
use std::fs;
use tempfile::TempDir;
use archdoc_core::{Config, python_analyzer::PythonAnalyzer};
#[test]
fn test_http_integration_detection() {
let config = Config::default();
let analyzer = PythonAnalyzer::new(config);
// Create a temporary Python file with HTTP integration
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test.py");
let python_code = r#"
import requests
def fetch_data():
response = requests.get("https://api.example.com/data")
return response.json()
"#;
fs::write(&temp_file, python_code).expect("Failed to write test file");
// Parse the module
let parsed_module = analyzer.parse_module(&temp_file)
.expect("Failed to parse module");
// Check that we found the function
assert_eq!(parsed_module.symbols.len(), 1);
let symbol = &parsed_module.symbols[0];
assert_eq!(symbol.id, "fetch_data");
// Check that HTTP integration is detected
assert!(symbol.integrations_flags.http);
assert!(!symbol.integrations_flags.db);
assert!(!symbol.integrations_flags.queue);
}
#[test]
fn test_db_integration_detection() {
let config = Config::default();
let analyzer = PythonAnalyzer::new(config);
// Create a temporary Python file with DB integration
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test.py");
let python_code = r#"
import sqlite3
def get_user(user_id):
conn = sqlite3.connect("database.db")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
return cursor.fetchone()
"#;
fs::write(&temp_file, python_code).expect("Failed to write test file");
// Parse the module
let parsed_module = analyzer.parse_module(&temp_file)
.expect("Failed to parse module");
// Check that we found the function
assert_eq!(parsed_module.symbols.len(), 1);
let symbol = &parsed_module.symbols[0];
assert_eq!(symbol.id, "get_user");
// Check that DB integration is detected
assert!(!symbol.integrations_flags.http);
assert!(symbol.integrations_flags.db);
assert!(!symbol.integrations_flags.queue);
}
#[test]
fn test_queue_integration_detection() {
let config = Config::default();
let analyzer = PythonAnalyzer::new(config);
// Create a temporary Python file with queue integration
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test.py");
let python_code = r#"
import redis
def process_job(job_data):
client = redis.Redis()
client.lpush("job_queue", job_data)
"#;
fs::write(&temp_file, python_code).expect("Failed to write test file");
// Parse the module
let parsed_module = analyzer.parse_module(&temp_file)
.expect("Failed to parse module");
// Check that we found the function
assert_eq!(parsed_module.symbols.len(), 1);
let symbol = &parsed_module.symbols[0];
assert_eq!(symbol.id, "process_job");
// Check that queue integration is detected
assert!(!symbol.integrations_flags.http);
assert!(!symbol.integrations_flags.db);
assert!(symbol.integrations_flags.queue);
}
#[test]
fn test_no_integration_detection() {
let config = Config::default();
let analyzer = PythonAnalyzer::new(config);
// Create a temporary Python file with no integrations
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test.py");
let python_code = r#"
def calculate_sum(a, b):
return a + b
"#;
fs::write(&temp_file, python_code).expect("Failed to write test file");
// Parse the module
let parsed_module = analyzer.parse_module(&temp_file)
.expect("Failed to parse module");
// Check that we found the function
assert_eq!(parsed_module.symbols.len(), 1);
let symbol = &parsed_module.symbols[0];
assert_eq!(symbol.id, "calculate_sum");
// Check that no integrations are detected
assert!(!symbol.integrations_flags.http);
assert!(!symbol.integrations_flags.db);
assert!(!symbol.integrations_flags.queue);
}

View File

@@ -0,0 +1,13 @@
//! Integration tests for ArchDoc
// Include golden tests
mod golden;
mod error_handling;
mod caching;
mod integration_detection;
mod enhanced_analysis;
// Run all tests
fn main() {
// This is just a placeholder - tests are run by cargo test
}

View File

@@ -0,0 +1,93 @@
//! Tests for analyzing the test project
use archdoc_core::{
config::Config,
python_analyzer::PythonAnalyzer,
};
use std::path::Path;
#[test]
fn test_project_analysis() {
// Load config from test project
let config = Config::load_from_file(Path::new("../test-project/archdoc.toml")).unwrap();
// Initialize analyzer
let analyzer = PythonAnalyzer::new(config);
// Parse core module
let core_module = analyzer.parse_module(Path::new("../test-project/src/core.py")).unwrap();
println!("Core module symbols: {}", core_module.symbols.len());
for symbol in &core_module.symbols {
println!(" Symbol: {} ({:?}), DB: {}, HTTP: {}", symbol.id, symbol.kind, symbol.integrations_flags.db, symbol.integrations_flags.http);
}
println!("Core module calls: {}", core_module.calls.len());
for call in &core_module.calls {
println!(" Call: {} -> {}", call.caller_symbol, call.callee_expr);
}
// Check that we found symbols
assert!(!core_module.symbols.is_empty()); // Should find at least the main symbols
// Check that we found calls
assert!(!core_module.calls.is_empty());
// Check that integrations are detected
let db_integration_found = core_module.symbols.iter().any(|s| s.integrations_flags.db);
let http_integration_found = core_module.symbols.iter().any(|s| s.integrations_flags.http);
assert!(db_integration_found, "Database integration should be detected");
assert!(http_integration_found, "HTTP integration should be detected");
// Parse utils module
let utils_module = analyzer.parse_module(Path::new("../test-project/src/utils.py")).unwrap();
println!("Utils module symbols: {}", utils_module.symbols.len());
for symbol in &utils_module.symbols {
println!(" Symbol: {} ({:?}), DB: {}, HTTP: {}", symbol.id, symbol.kind, symbol.integrations_flags.db, symbol.integrations_flags.http);
}
// Check that we found symbols
assert!(!utils_module.symbols.is_empty());
}
#[test]
fn test_full_project_resolution() {
// Load config from test project
let config = Config::load_from_file(Path::new("../test-project/archdoc.toml")).unwrap();
// Initialize analyzer
let analyzer = PythonAnalyzer::new(config);
// Parse all modules
let core_module = analyzer.parse_module(Path::new("../test-project/src/core.py")).unwrap();
let utils_module = analyzer.parse_module(Path::new("../test-project/src/utils.py")).unwrap();
let modules = vec![core_module, utils_module];
// Resolve symbols
let project_model = analyzer.resolve_symbols(&modules).unwrap();
// Check project model
assert!(!project_model.modules.is_empty());
assert!(!project_model.symbols.is_empty());
assert!(!project_model.files.is_empty());
// Check that integrations are preserved in the project model
let db_integration_found = project_model.symbols.values().any(|s| s.integrations_flags.db);
let http_integration_found = project_model.symbols.values().any(|s| s.integrations_flags.http);
assert!(db_integration_found, "Database integration should be preserved in project model");
assert!(http_integration_found, "HTTP integration should be preserved in project model");
println!("Project modules: {:?}", project_model.modules.keys().collect::<Vec<_>>());
println!("Project symbols: {}", project_model.symbols.len());
// Print integration information
for (id, symbol) in &project_model.symbols {
if symbol.integrations_flags.db || symbol.integrations_flags.http {
println!("Symbol {} has DB: {}, HTTP: {}", id, symbol.integrations_flags.db, symbol.integrations_flags.http);
}
}
}

View File

@@ -0,0 +1,85 @@
//! Tests for the renderer functionality
use archdoc_core::{
model::{ProjectModel, Symbol, SymbolKind, IntegrationFlags, SymbolMetrics},
renderer::Renderer,
};
use std::collections::HashMap;
#[test]
fn test_render_with_integrations() {
// Create a mock project model with integration information
let mut project_model = ProjectModel::new();
// Add a symbol with database integration
let db_symbol = Symbol {
id: "DatabaseManager".to_string(),
kind: SymbolKind::Class,
module_id: "test_module".to_string(),
file_id: "test_file.py".to_string(),
qualname: "DatabaseManager".to_string(),
signature: "class DatabaseManager".to_string(),
annotations: None,
docstring_first_line: None,
purpose: "test".to_string(),
outbound_calls: vec![],
inbound_calls: vec![],
integrations_flags: IntegrationFlags {
db: true,
http: false,
queue: false,
},
metrics: SymbolMetrics {
fan_in: 0,
fan_out: 0,
is_critical: false,
cycle_participant: false,
},
};
// Add a symbol with HTTP integration
let http_symbol = Symbol {
id: "fetch_data".to_string(),
kind: SymbolKind::Function,
module_id: "test_module".to_string(),
file_id: "test_file.py".to_string(),
qualname: "fetch_data".to_string(),
signature: "def fetch_data()".to_string(),
annotations: None,
docstring_first_line: None,
purpose: "test".to_string(),
outbound_calls: vec![],
inbound_calls: vec![],
integrations_flags: IntegrationFlags {
db: false,
http: true,
queue: false,
},
metrics: SymbolMetrics {
fan_in: 0,
fan_out: 0,
is_critical: false,
cycle_participant: false,
},
};
project_model.symbols.insert("DatabaseManager".to_string(), db_symbol);
project_model.symbols.insert("fetch_data".to_string(), http_symbol);
// Initialize renderer
let renderer = Renderer::new();
// Render architecture documentation
let result = renderer.render_architecture_md(&project_model);
assert!(result.is_ok());
let rendered_content = result.unwrap();
println!("Rendered content:\n{}", rendered_content);
// Check that integration sections are present
assert!(rendered_content.contains("## Integrations"));
assert!(rendered_content.contains("### Database Integrations"));
assert!(rendered_content.contains("### HTTP/API Integrations"));
assert!(rendered_content.contains("DatabaseManager in test_file.py"));
assert!(rendered_content.contains("fetch_data in test_file.py"));
}