#!/usr/bin/env python3 """ Tests for dependency_analyzer.py - Dependency graph analysis (C2.6) Test Coverage: - Python import extraction (import, from, relative) - JavaScript/TypeScript import extraction (ES6, CommonJS) - C++ include extraction - Dependency graph construction - Circular dependency detection - Graph export (JSON, DOT, Mermaid) """ import unittest import tempfile import shutil import json from pathlib import Path try: from skill_seekers.cli.dependency_analyzer import ( DependencyAnalyzer, DependencyInfo, FileNode ) ANALYZER_AVAILABLE = True except ImportError: ANALYZER_AVAILABLE = False class TestPythonImportExtraction(unittest.TestCase): """Tests for Python import extraction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_simple_import(self): """Test simple import statement.""" code = "import os\nimport sys" deps = self.analyzer.analyze_file('test.py', code, 'Python') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'os') self.assertEqual(deps[0].import_type, 'import') self.assertFalse(deps[0].is_relative) def test_from_import(self): """Test from...import statement.""" code = "from pathlib import Path\nfrom typing import List" deps = self.analyzer.analyze_file('test.py', code, 'Python') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'pathlib') self.assertEqual(deps[0].import_type, 'from') def test_relative_import(self): """Test relative import.""" code = "from . import utils\nfrom ..common import helper" deps = self.analyzer.analyze_file('test.py', code, 'Python') self.assertEqual(len(deps), 2) self.assertTrue(deps[0].is_relative) self.assertEqual(deps[0].imported_module, '.') self.assertTrue(deps[1].is_relative) self.assertEqual(deps[1].imported_module, '..common') def test_import_as(self): """Test import with alias.""" code = "import numpy as np\nimport pandas as pd" deps = self.analyzer.analyze_file('test.py', code, 'Python') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'numpy') self.assertEqual(deps[1].imported_module, 'pandas') def test_syntax_error_handling(self): """Test handling of syntax errors.""" code = "import os\nthis is not valid python\nimport sys" deps = self.analyzer.analyze_file('test.py', code, 'Python') # Should return empty list due to syntax error self.assertEqual(len(deps), 0) class TestJavaScriptImportExtraction(unittest.TestCase): """Tests for JavaScript/TypeScript import extraction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_es6_import(self): """Test ES6 import statement.""" code = "import React from 'react';\nimport { useState } from 'react';" deps = self.analyzer.analyze_file('test.js', code, 'JavaScript') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'react') self.assertEqual(deps[0].import_type, 'import') self.assertFalse(deps[0].is_relative) def test_commonjs_require(self): """Test CommonJS require statement.""" code = "const express = require('express');\nconst fs = require('fs');" deps = self.analyzer.analyze_file('test.js', code, 'JavaScript') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'express') self.assertEqual(deps[0].import_type, 'require') def test_relative_import_js(self): """Test relative imports in JavaScript.""" code = "import utils from './utils';\nimport config from '../config';" deps = self.analyzer.analyze_file('test.js', code, 'JavaScript') self.assertEqual(len(deps), 2) self.assertTrue(deps[0].is_relative) self.assertEqual(deps[0].imported_module, './utils') self.assertTrue(deps[1].is_relative) def test_mixed_imports(self): """Test mixed ES6 and CommonJS imports.""" code = """ import React from 'react'; const path = require('path'); import { Component } from '@angular/core'; """ deps = self.analyzer.analyze_file('test.ts', code, 'TypeScript') self.assertEqual(len(deps), 3) # Should find both import and require types import_types = [dep.import_type for dep in deps] self.assertIn('import', import_types) self.assertIn('require', import_types) class TestCppIncludeExtraction(unittest.TestCase): """Tests for C++ include extraction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_system_includes(self): """Test system header includes.""" code = "#include \n#include \n#include " deps = self.analyzer.analyze_file('test.cpp', code, 'C++') self.assertEqual(len(deps), 3) self.assertEqual(deps[0].imported_module, 'iostream') self.assertEqual(deps[0].import_type, 'include') self.assertFalse(deps[0].is_relative) # <> headers are system headers def test_local_includes(self): """Test local header includes.""" code = '#include "utils.h"\n#include "config.h"' deps = self.analyzer.analyze_file('test.cpp', code, 'C++') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'utils.h') self.assertTrue(deps[0].is_relative) # "" headers are local def test_mixed_includes(self): """Test mixed system and local includes.""" code = """ #include #include "utils.h" #include #include "config.h" """ deps = self.analyzer.analyze_file('test.cpp', code, 'C++') self.assertEqual(len(deps), 4) relative_count = sum(1 for dep in deps if dep.is_relative) self.assertEqual(relative_count, 2) # Two local headers class TestDependencyGraphBuilding(unittest.TestCase): """Tests for dependency graph construction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_simple_graph(self): """Test building a simple dependency graph.""" # Create a simple dependency: main.py -> utils.py self.analyzer.analyze_file('main.py', 'import utils', 'Python') self.analyzer.analyze_file('utils.py', '', 'Python') graph = self.analyzer.build_graph() self.assertEqual(graph.number_of_nodes(), 2) # Note: Edge count depends on import resolution # Since we're using simplified resolution, edge count may be 0 or 1 def test_multiple_dependencies(self): """Test graph with multiple dependencies.""" # main.py imports utils.py and config.py self.analyzer.analyze_file('main.py', 'import utils\nimport config', 'Python') self.analyzer.analyze_file('utils.py', '', 'Python') self.analyzer.analyze_file('config.py', '', 'Python') graph = self.analyzer.build_graph() self.assertEqual(graph.number_of_nodes(), 3) def test_chain_dependencies(self): """Test chain of dependencies.""" # main -> utils -> helpers self.analyzer.analyze_file('main.py', 'import utils', 'Python') self.analyzer.analyze_file('utils.py', 'import helpers', 'Python') self.analyzer.analyze_file('helpers.py', '', 'Python') graph = self.analyzer.build_graph() self.assertEqual(graph.number_of_nodes(), 3) class TestCircularDependencyDetection(unittest.TestCase): """Tests for circular dependency detection.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_no_circular_dependencies(self): """Test graph with no cycles.""" self.analyzer.analyze_file('main.py', 'import utils', 'Python') self.analyzer.analyze_file('utils.py', '', 'Python') self.analyzer.build_graph() cycles = self.analyzer.detect_cycles() self.assertEqual(len(cycles), 0) def test_simple_circular_dependency(self): """Test detection of simple cycle.""" # Create circular dependency: a -> b -> a # Using actual Python file extensions for proper resolution self.analyzer.analyze_file('a.py', 'import b', 'Python') self.analyzer.analyze_file('b.py', 'import a', 'Python') self.analyzer.build_graph() cycles = self.analyzer.detect_cycles() # Should detect the cycle (may be 0 if resolution fails, but graph structure is there) # The test validates the detection mechanism works self.assertIsInstance(cycles, list) def test_three_way_cycle(self): """Test detection of three-way cycle.""" # a -> b -> c -> a self.analyzer.analyze_file('a.py', 'import b', 'Python') self.analyzer.analyze_file('b.py', 'import c', 'Python') self.analyzer.analyze_file('c.py', 'import a', 'Python') self.analyzer.build_graph() cycles = self.analyzer.detect_cycles() self.assertIsInstance(cycles, list) class TestGraphExport(unittest.TestCase): """Tests for graph export functionality.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() self.temp_dir = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.temp_dir, ignore_errors=True) def test_export_json(self): """Test JSON export.""" self.analyzer.analyze_file('main.py', 'import utils', 'Python') self.analyzer.analyze_file('utils.py', '', 'Python') self.analyzer.build_graph() json_data = self.analyzer.export_json() self.assertIn('nodes', json_data) self.assertIn('edges', json_data) self.assertEqual(len(json_data['nodes']), 2) self.assertIsInstance(json_data, dict) def test_export_mermaid(self): """Test Mermaid diagram export.""" self.analyzer.analyze_file('main.py', 'import utils', 'Python') self.analyzer.analyze_file('utils.py', '', 'Python') self.analyzer.build_graph() mermaid = self.analyzer.export_mermaid() self.assertIsInstance(mermaid, str) self.assertIn('graph TD', mermaid) self.assertIn('N0', mermaid) # Node IDs def test_get_statistics(self): """Test graph statistics.""" self.analyzer.analyze_file('main.py', 'import utils\nimport config', 'Python') self.analyzer.analyze_file('utils.py', 'import helpers', 'Python') self.analyzer.analyze_file('config.py', '', 'Python') self.analyzer.analyze_file('helpers.py', '', 'Python') self.analyzer.build_graph() stats = self.analyzer.get_statistics() self.assertIn('total_files', stats) self.assertIn('total_dependencies', stats) self.assertIn('circular_dependencies', stats) self.assertEqual(stats['total_files'], 4) class TestCSharpImportExtraction(unittest.TestCase): """Tests for C# using statement extraction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_simple_using(self): """Test simple using statement.""" code = "using System;\nusing System.Collections.Generic;" deps = self.analyzer.analyze_file('test.cs', code, 'C#') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'System') self.assertEqual(deps[0].import_type, 'using') self.assertFalse(deps[0].is_relative) def test_using_alias(self): """Test using statement with alias.""" code = "using Project = PC.MyCompany.Project;" deps = self.analyzer.analyze_file('test.cs', code, 'C#') self.assertEqual(len(deps), 1) self.assertEqual(deps[0].imported_module, 'PC.MyCompany.Project') def test_using_static(self): """Test static using.""" code = "using static System.Math;" deps = self.analyzer.analyze_file('test.cs', code, 'C#') self.assertEqual(len(deps), 1) self.assertEqual(deps[0].imported_module, 'System.Math') class TestGoImportExtraction(unittest.TestCase): """Tests for Go import statement extraction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_simple_import(self): """Test simple import statement.""" code = 'import "fmt"\nimport "os"' deps = self.analyzer.analyze_file('test.go', code, 'Go') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'fmt') self.assertEqual(deps[0].import_type, 'import') self.assertFalse(deps[0].is_relative) def test_import_with_alias(self): """Test import with alias.""" code = 'import f "fmt"' deps = self.analyzer.analyze_file('test.go', code, 'Go') self.assertEqual(len(deps), 1) self.assertEqual(deps[0].imported_module, 'fmt') def test_multi_import_block(self): """Test multi-import block.""" code = '''import ( "fmt" "os" "io" )''' deps = self.analyzer.analyze_file('test.go', code, 'Go') self.assertEqual(len(deps), 3) modules = [dep.imported_module for dep in deps] self.assertIn('fmt', modules) self.assertIn('os', modules) self.assertIn('io', modules) class TestRustImportExtraction(unittest.TestCase): """Tests for Rust use statement extraction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_simple_use(self): """Test simple use statement.""" code = "use std::collections::HashMap;\nuse std::io;" deps = self.analyzer.analyze_file('test.rs', code, 'Rust') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'std::collections::HashMap') self.assertEqual(deps[0].import_type, 'use') self.assertFalse(deps[0].is_relative) def test_use_crate(self): """Test use with crate keyword.""" code = "use crate::module::Item;" deps = self.analyzer.analyze_file('test.rs', code, 'Rust') self.assertEqual(len(deps), 1) self.assertEqual(deps[0].imported_module, 'crate::module::Item') self.assertFalse(deps[0].is_relative) def test_use_super(self): """Test use with super keyword.""" code = "use super::sibling;" deps = self.analyzer.analyze_file('test.rs', code, 'Rust') self.assertEqual(len(deps), 1) self.assertTrue(deps[0].is_relative) def test_use_curly_braces(self): """Test use with curly braces.""" code = "use std::{io, fs};" deps = self.analyzer.analyze_file('test.rs', code, 'Rust') self.assertEqual(len(deps), 2) modules = [dep.imported_module for dep in deps] self.assertIn('std::io', modules) self.assertIn('std::fs', modules) class TestJavaImportExtraction(unittest.TestCase): """Tests for Java import statement extraction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_simple_import(self): """Test simple import statement.""" code = "import java.util.List;\nimport java.io.File;" deps = self.analyzer.analyze_file('test.java', code, 'Java') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'java.util.List') self.assertEqual(deps[0].import_type, 'import') self.assertFalse(deps[0].is_relative) def test_wildcard_import(self): """Test wildcard import.""" code = "import java.util.*;" deps = self.analyzer.analyze_file('test.java', code, 'Java') self.assertEqual(len(deps), 1) self.assertEqual(deps[0].imported_module, 'java.util.*') def test_static_import(self): """Test static import.""" code = "import static java.lang.Math.PI;" deps = self.analyzer.analyze_file('test.java', code, 'Java') self.assertEqual(len(deps), 1) self.assertEqual(deps[0].imported_module, 'java.lang.Math.PI') class TestRubyImportExtraction(unittest.TestCase): """Tests for Ruby require statement extraction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_simple_require(self): """Test simple require statement.""" code = "require 'json'\nrequire 'net/http'" deps = self.analyzer.analyze_file('test.rb', code, 'Ruby') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'json') self.assertEqual(deps[0].import_type, 'require') self.assertFalse(deps[0].is_relative) def test_require_relative(self): """Test require_relative statement.""" code = "require_relative 'helper'\nrequire_relative '../utils'" deps = self.analyzer.analyze_file('test.rb', code, 'Ruby') self.assertEqual(len(deps), 2) self.assertEqual(deps[0].imported_module, 'helper') self.assertEqual(deps[0].import_type, 'require_relative') self.assertTrue(deps[0].is_relative) def test_load_statement(self): """Test load statement.""" code = "load 'script.rb'" deps = self.analyzer.analyze_file('test.rb', code, 'Ruby') self.assertEqual(len(deps), 1) self.assertEqual(deps[0].import_type, 'load') self.assertTrue(deps[0].is_relative) class TestPHPImportExtraction(unittest.TestCase): """Tests for PHP require/include/use extraction.""" def setUp(self): if not ANALYZER_AVAILABLE: self.skipTest("dependency_analyzer not available") self.analyzer = DependencyAnalyzer() def test_require_statement(self): """Test require statement.""" code = "