Some checks are pending
CI / Test (Python 3.10 on macos-latest) (push) Waiting to run
CI / Test (Python 3.11 on macos-latest) (push) Waiting to run
CI / Test (Python 3.12 on macos-latest) (push) Waiting to run
CI / Test (Python 3.8 on macos-latest) (push) Waiting to run
CI / Test (Python 3.9 on macos-latest) (push) Waiting to run
CI / Test (Python 3.10 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.11 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.12 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.8 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.9 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.10 on windows-latest) (push) Waiting to run
CI / Test (Python 3.11 on windows-latest) (push) Waiting to run
CI / Test (Python 3.12 on windows-latest) (push) Waiting to run
CI / Test (Python 3.8 on windows-latest) (push) Waiting to run
CI / Test (Python 3.9 on windows-latest) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Release (push) Blocked by required conditions
Documentation / Build Documentation (push) Waiting to run
361 lines
9.6 KiB
Python
361 lines
9.6 KiB
Python
# sikuwa/incremental/tests/test_incremental.py
|
||
"""
|
||
减量编译系统测试
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import tempfile
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
# 添加父目录到路径
|
||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||
|
||
from incremental.core import (
|
||
IncrementalCompiler,
|
||
CompilationUnit,
|
||
Snapshot,
|
||
ChangeDetector,
|
||
CompilationCache,
|
||
UnitType,
|
||
UnitState
|
||
)
|
||
from incremental.analyzer import PythonAnalyzer, BlockType
|
||
|
||
|
||
class TestPythonAnalyzer(unittest.TestCase):
|
||
"""测试 Python 分析器"""
|
||
|
||
def setUp(self):
|
||
self.analyzer = PythonAnalyzer()
|
||
|
||
def test_analyze_function(self):
|
||
"""测试函数分析"""
|
||
code = '''
|
||
def hello(name):
|
||
"""Say hello"""
|
||
print(f"Hello, {name}!")
|
||
'''
|
||
blocks = self.analyzer.analyze(code, "test.py")
|
||
|
||
# 应该检测到函数块
|
||
func_blocks = [b for b in blocks if b.type == BlockType.FUNCTION]
|
||
self.assertEqual(len(func_blocks), 1)
|
||
self.assertEqual(func_blocks[0].name, "hello")
|
||
|
||
def test_analyze_class(self):
|
||
"""测试类分析"""
|
||
code = '''
|
||
class MyClass:
|
||
def __init__(self):
|
||
self.value = 0
|
||
|
||
def increment(self):
|
||
self.value += 1
|
||
'''
|
||
blocks = self.analyzer.analyze(code, "test.py")
|
||
|
||
# 应该检测到类块
|
||
class_blocks = [b for b in blocks if b.type == BlockType.CLASS]
|
||
self.assertEqual(len(class_blocks), 1)
|
||
self.assertEqual(class_blocks[0].name, "MyClass")
|
||
|
||
def test_analyze_import(self):
|
||
"""测试导入分析"""
|
||
code = '''
|
||
import os
|
||
from sys import path
|
||
from pathlib import Path
|
||
'''
|
||
blocks = self.analyzer.analyze(code, "test.py")
|
||
|
||
import_blocks = [b for b in blocks if b.type == BlockType.IMPORT]
|
||
self.assertEqual(len(import_blocks), 3)
|
||
|
||
def test_dependency_extraction(self):
|
||
"""测试依赖提取"""
|
||
code = '''
|
||
def outer():
|
||
def inner():
|
||
return x
|
||
return inner()
|
||
'''
|
||
blocks = self.analyzer.analyze(code, "test.py")
|
||
func_blocks = [b for b in blocks if b.type == BlockType.FUNCTION]
|
||
|
||
# outer 函数应该依赖 x
|
||
self.assertEqual(len(func_blocks), 1)
|
||
self.assertIn('x', func_blocks[0].references)
|
||
|
||
|
||
class TestChangeDetector(unittest.TestCase):
|
||
"""测试变更检测器"""
|
||
|
||
def setUp(self):
|
||
self.detector = ChangeDetector()
|
||
|
||
def test_detect_addition(self):
|
||
"""测试新增检测"""
|
||
old = Snapshot()
|
||
old.units = {}
|
||
|
||
new_unit = CompilationUnit(
|
||
id="u1", content="def foo(): pass",
|
||
start_line=1, end_line=1, file_path="test.py"
|
||
)
|
||
new_unit.compute_hash()
|
||
|
||
new = Snapshot()
|
||
new.units = {"u1": new_unit}
|
||
|
||
changes = self.detector.detect_changes(old, new)
|
||
|
||
self.assertEqual(len(changes), 1)
|
||
self.assertEqual(changes[0].unit_id, "u1")
|
||
self.assertEqual(changes[0].change_type, UnitState.ADDED)
|
||
|
||
def test_detect_modification(self):
|
||
"""测试修改检测"""
|
||
old_unit = CompilationUnit(
|
||
id="u1", content="def foo(): pass",
|
||
start_line=1, end_line=1, file_path="test.py"
|
||
)
|
||
old_unit.compute_hash()
|
||
|
||
old = Snapshot()
|
||
old.units = {"u1": old_unit}
|
||
|
||
new_unit = CompilationUnit(
|
||
id="u1", content="def foo(): return 1",
|
||
start_line=1, end_line=1, file_path="test.py"
|
||
)
|
||
new_unit.compute_hash()
|
||
|
||
new = Snapshot()
|
||
new.units = {"u1": new_unit}
|
||
|
||
changes = self.detector.detect_changes(old, new)
|
||
|
||
self.assertEqual(len(changes), 1)
|
||
self.assertEqual(changes[0].unit_id, "u1")
|
||
self.assertEqual(changes[0].change_type, UnitState.MODIFIED)
|
||
|
||
def test_detect_deletion(self):
|
||
"""测试删除检测"""
|
||
old_unit = CompilationUnit(
|
||
id="u1", content="def foo(): pass",
|
||
start_line=1, end_line=1, file_path="test.py"
|
||
)
|
||
old_unit.compute_hash()
|
||
|
||
old = Snapshot()
|
||
old.units = {"u1": old_unit}
|
||
|
||
new = Snapshot()
|
||
new.units = {}
|
||
|
||
changes = self.detector.detect_changes(old, new)
|
||
|
||
self.assertEqual(len(changes), 1)
|
||
self.assertEqual(changes[0].change_type, UnitState.DELETED)
|
||
|
||
|
||
class TestCompilationCache(unittest.TestCase):
|
||
"""测试编译缓存"""
|
||
|
||
def setUp(self):
|
||
self.temp_dir = tempfile.mkdtemp()
|
||
self.cache = CompilationCache(self.temp_dir)
|
||
|
||
def tearDown(self):
|
||
import shutil
|
||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||
|
||
def test_put_get(self):
|
||
"""测试缓存存取"""
|
||
self.cache.put("key1", "value1", "hash1")
|
||
result = self.cache.get("key1")
|
||
self.assertEqual(result, "value1")
|
||
|
||
def test_get_nonexistent(self):
|
||
"""测试获取不存在的键"""
|
||
result = self.cache.get("nonexistent")
|
||
self.assertEqual(result, "") # 返回空字符串
|
||
|
||
def test_persistence(self):
|
||
"""测试持久化"""
|
||
self.cache.put("key1", "value1", "hash1")
|
||
self.cache.save()
|
||
|
||
# 创建新缓存实例
|
||
cache2 = CompilationCache(self.temp_dir)
|
||
result = cache2.get("key1")
|
||
self.assertEqual(result, "value1")
|
||
|
||
|
||
class TestIncrementalCompiler(unittest.TestCase):
|
||
"""测试减量编译器"""
|
||
|
||
def setUp(self):
|
||
self.temp_dir = tempfile.mkdtemp()
|
||
self.compiler = IncrementalCompiler(self.temp_dir)
|
||
|
||
# 设置简单的编译器(返回大写代码)
|
||
self.compiler.set_compiler(lambda unit: unit.content.upper())
|
||
|
||
def tearDown(self):
|
||
import shutil
|
||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||
|
||
def test_initial_compile(self):
|
||
"""测试初始编译"""
|
||
code = '''
|
||
def hello():
|
||
print("Hello")
|
||
|
||
def world():
|
||
print("World")
|
||
'''
|
||
self.compiler.analyze_source("test.py", code)
|
||
changes = self.compiler.update_source("test.py", code)
|
||
|
||
# 首次编译,所有单元都应该是新的
|
||
self.assertGreater(len(changes), 0)
|
||
|
||
# 编译
|
||
outputs = self.compiler.compile_all_pending()
|
||
self.assertGreater(len(outputs), 0)
|
||
|
||
def test_incremental_compile(self):
|
||
"""测试增量编译"""
|
||
# 初始代码
|
||
code1 = '''
|
||
def hello():
|
||
print("Hello")
|
||
|
||
def world():
|
||
print("World")
|
||
'''
|
||
self.compiler.analyze_source("test.py", code1)
|
||
self.compiler.update_source("test.py", code1)
|
||
outputs1 = self.compiler.compile_all_pending()
|
||
|
||
# 修改一个函数
|
||
code2 = '''
|
||
def hello():
|
||
print("Hello Modified")
|
||
|
||
def world():
|
||
print("World")
|
||
'''
|
||
changes = self.compiler.update_source("test.py", code2)
|
||
|
||
# 应该有变更
|
||
self.assertGreater(len(changes), 0)
|
||
|
||
# 再次编译
|
||
outputs2 = self.compiler.compile_all_pending()
|
||
|
||
# 验证有输出
|
||
self.assertGreater(len(outputs1) + len(outputs2), 0)
|
||
|
||
def test_dependency_propagation(self):
|
||
"""测试依赖传播"""
|
||
code = '''
|
||
x = 10
|
||
|
||
def get_x():
|
||
return x
|
||
|
||
def double_x():
|
||
return get_x() * 2
|
||
'''
|
||
self.compiler.analyze_source("test.py", code)
|
||
self.compiler.update_source("test.py", code)
|
||
self.compiler.compile_all_pending()
|
||
|
||
# 修改 x 的值
|
||
code2 = '''
|
||
x = 20
|
||
|
||
def get_x():
|
||
return x
|
||
|
||
def double_x():
|
||
return get_x() * 2
|
||
'''
|
||
changes = self.compiler.update_source("test.py", code2)
|
||
|
||
# 应该检测到变更(x 变了,依赖它的也应该被标记)
|
||
self.assertGreater(len(changes), 0)
|
||
|
||
def test_combined_output(self):
|
||
"""测试合并输出"""
|
||
code = '''
|
||
import os
|
||
|
||
def hello():
|
||
print("Hello")
|
||
|
||
def world():
|
||
print("World")
|
||
'''
|
||
self.compiler.analyze_source("test.py", code)
|
||
self.compiler.update_source("test.py", code)
|
||
self.compiler.compile_all_pending()
|
||
|
||
combined = self.compiler.get_combined_output("test.py")
|
||
|
||
# 合并输出应该包含所有编译产物
|
||
self.assertGreater(len(combined), 0)
|
||
|
||
|
||
class TestBlockBoundary(unittest.TestCase):
|
||
"""测试边界触发器"""
|
||
|
||
def setUp(self):
|
||
self.analyzer = PythonAnalyzer()
|
||
|
||
def test_class_contains_methods(self):
|
||
"""测试类包含其方法"""
|
||
code = '''
|
||
class MyClass:
|
||
def method1(self):
|
||
pass
|
||
|
||
def method2(self):
|
||
pass
|
||
'''
|
||
blocks = self.analyzer.analyze(code, "test.py")
|
||
|
||
class_blocks = [b for b in blocks if b.type == BlockType.CLASS]
|
||
self.assertEqual(len(class_blocks), 1)
|
||
|
||
# 类块应该包含整个类定义
|
||
class_block = class_blocks[0]
|
||
self.assertIn("method1", class_block.content)
|
||
self.assertIn("method2", class_block.content)
|
||
|
||
|
||
def run_tests():
|
||
"""运行所有测试"""
|
||
loader = unittest.TestLoader()
|
||
suite = unittest.TestSuite()
|
||
|
||
suite.addTests(loader.loadTestsFromTestCase(TestPythonAnalyzer))
|
||
suite.addTests(loader.loadTestsFromTestCase(TestChangeDetector))
|
||
suite.addTests(loader.loadTestsFromTestCase(TestCompilationCache))
|
||
suite.addTests(loader.loadTestsFromTestCase(TestIncrementalCompiler))
|
||
suite.addTests(loader.loadTestsFromTestCase(TestBlockBoundary))
|
||
|
||
runner = unittest.TextTestRunner(verbosity=2)
|
||
result = runner.run(suite)
|
||
|
||
return result.wasSuccessful()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
success = run_tests()
|
||
sys.exit(0 if success else 1)
|