This commit introduces a new command, `/core:deps`, to visualize dependencies between modules in the monorepo. The command parses a `repos.yaml` file to build a dependency graph and supports the following functionalities: - Displaying a full dependency tree for all modules. - Displaying a dependency tree for a single module. - Displaying reverse dependencies for a single module using the `--reverse` flag. - Detecting and reporting circular dependencies. The implementation consists of a Python script that handles the core logic and a command definition file that connects the command to the script. A comprehensive test suite is included to ensure the correctness of the implementation. Co-authored-by: Claude <noreply@anthropic.com>
162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
|
|
import io
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import patch, mock_open
|
|
from deps import (
|
|
parse_dependencies,
|
|
find_circular_dependencies,
|
|
print_dependency_tree,
|
|
print_reverse_dependencies,
|
|
main
|
|
)
|
|
|
|
class TestDeps(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.yaml_content = """
|
|
repos:
|
|
core-tenant:
|
|
depends: [core-php]
|
|
core-admin:
|
|
depends: [core-php, core-tenant]
|
|
core-php:
|
|
depends: []
|
|
core-api:
|
|
depends: [core-php]
|
|
core-analytics:
|
|
depends: [core-php, core-api]
|
|
"""
|
|
self.graph = {
|
|
'core-tenant': ['core-php'],
|
|
'core-admin': ['core-php', 'core-tenant'],
|
|
'core-php': [],
|
|
'core-api': ['core-php'],
|
|
'core-analytics': ['core-php', 'core-api'],
|
|
}
|
|
self.circular_yaml_content = """
|
|
repos:
|
|
module-a:
|
|
depends: [module-b]
|
|
module-b:
|
|
depends: [module-c]
|
|
module-c:
|
|
depends: [module-a]
|
|
"""
|
|
self.circular_graph = {
|
|
'module-a': ['module-b'],
|
|
'module-b': ['module-c'],
|
|
'module-c': ['module-a'],
|
|
}
|
|
|
|
def test_parse_dependencies(self):
|
|
with patch("builtins.open", mock_open(read_data=self.yaml_content)):
|
|
graph = parse_dependencies("dummy_path.yaml")
|
|
self.assertEqual(graph, self.graph)
|
|
|
|
def test_find_circular_dependencies(self):
|
|
cycles = find_circular_dependencies(self.circular_graph)
|
|
self.assertEqual(len(cycles), 1)
|
|
self.assertIn('module-a', cycles[0])
|
|
self.assertIn('module-b', cycles[0])
|
|
self.assertIn('module-c', cycles[0])
|
|
|
|
def test_find_no_circular_dependencies(self):
|
|
cycles = find_circular_dependencies(self.graph)
|
|
self.assertEqual(len(cycles), 0)
|
|
|
|
@patch('sys.stdout', new_callable=io.StringIO)
|
|
def test_print_dependency_tree(self, mock_stdout):
|
|
print_dependency_tree(self.graph, 'core-admin')
|
|
expected_output = (
|
|
"core-admin\n"
|
|
"├── core-php\n"
|
|
"└── core-tenant\n"
|
|
" └── core-php\n"
|
|
)
|
|
self.assertEqual(mock_stdout.getvalue().strip(), expected_output.strip())
|
|
|
|
@patch('sys.stdout', new_callable=io.StringIO)
|
|
def test_print_dependency_tree_no_deps(self, mock_stdout):
|
|
print_dependency_tree(self.graph, 'core-php')
|
|
expected_output = "core-php\n"
|
|
self.assertEqual(mock_stdout.getvalue().strip(), expected_output.strip())
|
|
|
|
|
|
@patch('sys.stdout', new_callable=io.StringIO)
|
|
def test_print_reverse_dependencies(self, mock_stdout):
|
|
print_reverse_dependencies(self.graph, 'core-php')
|
|
expected_output = (
|
|
"├── core-admin\n"
|
|
"├── core-analytics\n"
|
|
"├── core-api\n"
|
|
"└── core-tenant"
|
|
)
|
|
self.assertEqual(mock_stdout.getvalue().strip(), expected_output.strip())
|
|
|
|
@patch('sys.stdout', new_callable=io.StringIO)
|
|
def test_print_reverse_dependencies_no_deps(self, mock_stdout):
|
|
print_reverse_dependencies(self.graph, 'core-admin')
|
|
expected_output = "(no modules depend on core-admin)"
|
|
self.assertEqual(mock_stdout.getvalue().strip(), expected_output.strip())
|
|
|
|
@patch('deps.find_repos_yaml', return_value='dummy_path.yaml')
|
|
@patch('sys.stdout', new_callable=io.StringIO)
|
|
def test_main_no_args(self, mock_stdout, mock_find_yaml):
|
|
with patch("builtins.open", mock_open(read_data=self.yaml_content)):
|
|
with patch.object(sys, 'argv', ['deps.py']):
|
|
main()
|
|
output = mock_stdout.getvalue()
|
|
self.assertIn("core-admin dependencies:", output)
|
|
self.assertIn("core-tenant dependencies:", output)
|
|
|
|
@patch('deps.find_repos_yaml', return_value='dummy_path.yaml')
|
|
@patch('sys.stdout', new_callable=io.StringIO)
|
|
def test_main_module_arg(self, mock_stdout, mock_find_yaml):
|
|
with patch("builtins.open", mock_open(read_data=self.yaml_content)):
|
|
with patch.object(sys, 'argv', ['deps.py', 'core-tenant']):
|
|
main()
|
|
expected_output = (
|
|
"core-tenant dependencies:\n"
|
|
"└── core-php\n"
|
|
)
|
|
self.assertEqual(mock_stdout.getvalue().strip(), expected_output.strip())
|
|
|
|
@patch('deps.find_repos_yaml', return_value='dummy_path.yaml')
|
|
@patch('sys.stdout', new_callable=io.StringIO)
|
|
def test_main_reverse_arg(self, mock_stdout, mock_find_yaml):
|
|
with patch("builtins.open", mock_open(read_data=self.yaml_content)):
|
|
with patch.object(sys, 'argv', ['deps.py', '--reverse', 'core-api']):
|
|
main()
|
|
expected_output = (
|
|
"Modules that depend on core-api:\n"
|
|
"└── core-analytics"
|
|
)
|
|
self.assertEqual(mock_stdout.getvalue().strip(), expected_output.strip())
|
|
|
|
@patch('deps.find_repos_yaml', return_value='dummy_path.yaml')
|
|
@patch('sys.stdout', new_callable=io.StringIO)
|
|
def test_main_circular_dep(self, mock_stdout, mock_find_yaml):
|
|
with patch("builtins.open", mock_open(read_data=self.circular_yaml_content)):
|
|
with patch.object(sys, 'argv', ['deps.py']):
|
|
with self.assertRaises(SystemExit) as cm:
|
|
main()
|
|
self.assertEqual(cm.exception.code, 1)
|
|
output = mock_stdout.getvalue()
|
|
self.assertIn("Error: Circular dependencies detected!", output)
|
|
self.assertIn("module-a -> module-b -> module-c -> module-a", output)
|
|
|
|
@patch('deps.find_repos_yaml', return_value='dummy_path.yaml')
|
|
@patch('sys.stdout', new_callable=io.StringIO)
|
|
def test_main_non_existent_module(self, mock_stdout, mock_find_yaml):
|
|
with patch("builtins.open", mock_open(read_data=self.yaml_content)):
|
|
with patch.object(sys, 'argv', ['deps.py', 'non-existent-module']):
|
|
with self.assertRaises(SystemExit) as cm:
|
|
main()
|
|
self.assertEqual(cm.exception.code, 1)
|
|
output = mock_stdout.getvalue()
|
|
self.assertIn("Error: Module 'non-existent-module' not found in repos.yaml.", output)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|