from __future__ import annotations
from pathlib import Path
from tempfile import TemporaryDirectory
import sys
import unittest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from uipath_explainator.pipeline import ProjectPipeline
from uipath_explainator.scanner import crawl_dependencies, extract_dependencies, strip_comment_out_blocks
MAIN_XAML = """
"""
ACTIVE_XAML = """
["Scripts/Keep.bas"]
"""
OLD_XAML = """
"""
class StubAnalyzer:
def analyze(self, relative_path: Path, content: str) -> str:
return f"# {relative_path.as_posix()}\n\n{len(content)}"
class FlakyAnalyzer:
def analyze(self, relative_path: Path, content: str) -> str:
if relative_path.name == "Active.xaml":
raise RuntimeError("HTTP 429 quota exhausted")
return f"# {relative_path.as_posix()}\n\n{len(content)}"
class RecordingAnalyzer:
def __init__(self) -> None:
self.paths: list[str] = []
def analyze(self, relative_path: Path, content: str) -> str:
self.paths.append(relative_path.as_posix())
return f"# {relative_path.as_posix()}\n\n{len(content)}"
class PipelineTests(unittest.TestCase):
def test_strip_comment_out_blocks_removes_nested_blocks(self) -> None:
source = ""
cleaned = strip_comment_out_blocks(source)
self.assertEqual(cleaned, "")
def test_extract_dependencies_reads_property_element_paths(self) -> None:
with TemporaryDirectory() as tmp:
root = Path(tmp)
flows = root / "Flows"
flows.mkdir()
(root / "Main.xaml").write_text(ACTIVE_XAML, encoding="utf-8")
(root / "Scripts").mkdir()
(root / "Scripts" / "Keep.bas").write_text("Sub Keep()", encoding="utf-8")
dependencies, warnings = extract_dependencies(root, root / "Main.xaml")
self.assertEqual(len(warnings), 0)
self.assertEqual(len(dependencies), 1)
self.assertEqual(dependencies[0].target, root / "Scripts" / "Keep.bas")
def test_pipeline_prunes_files_only_reachable_before_comment_cleanup(self) -> None:
with TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
project_root = tmp_path / "project"
output_root = tmp_path / "workspace"
code_root = output_root / "code"
docs_root = output_root / "docs"
(project_root / "Flows").mkdir(parents=True)
(project_root / "Flows" / "Active.xaml").write_text(ACTIVE_XAML, encoding="utf-8")
(project_root / "Flows" / "Old.xaml").write_text(OLD_XAML, encoding="utf-8")
(project_root / "Scripts").mkdir()
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nEnd Sub", encoding="utf-8")
(project_root / "Scripts" / "Drop.bas").write_text("Sub Drop()\nEnd Sub", encoding="utf-8")
(project_root / "main.xaml").write_text(MAIN_XAML, encoding="utf-8")
initial_scan = crawl_dependencies(project_root, project_root / "main.xaml")
initial_files = {path.relative_to(project_root).as_posix() for path in initial_scan.files}
self.assertIn("Flows/Old.xaml", initial_files)
self.assertIn("Scripts/Drop.bas", initial_files)
report = ProjectPipeline(project_root, output_root, "main.xaml", force=True).run(StubAnalyzer())
final_files = {path.as_posix() for path in report.final_files}
self.assertIn("main.xaml", final_files)
self.assertIn("Flows/Active.xaml", final_files)
self.assertIn("Scripts/Keep.bas", final_files)
self.assertNotIn("Flows/Old.xaml", final_files)
self.assertNotIn("Scripts/Drop.bas", final_files)
self.assertEqual(report.code_root, code_root.resolve())
self.assertEqual(report.docs_root, docs_root.resolve())
self.assertFalse((code_root / "Flows" / "Old.xaml").exists())
self.assertFalse((code_root / "Scripts" / "Drop.bas").exists())
self.assertTrue((code_root / "Flows" / "Active.xaml").exists())
self.assertTrue((docs_root / "Flows" / "Active.xaml.analysis.md").exists())
self.assertTrue((docs_root / "manifest.json").exists())
overview = (docs_root / "OVERVIEW.md").read_text(encoding="utf-8")
self.assertIn("## Processing Logic", overview)
self.assertIn("Initial Scan", overview)
self.assertIn("## How To Read This Output", overview)
self.assertIn("## Cleaned XAML Files", overview)
self.assertIn("Code Root", overview)
self.assertIn("Docs Root", overview)
def test_pipeline_emits_stage_logs(self) -> None:
with TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
project_root = tmp_path / "project"
output_root = tmp_path / "workspace"
(project_root / "Flows").mkdir(parents=True)
(project_root / "Flows" / "Active.xaml").write_text(ACTIVE_XAML, encoding="utf-8")
(project_root / "Flows" / "Old.xaml").write_text(OLD_XAML, encoding="utf-8")
(project_root / "Scripts").mkdir()
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nEnd Sub", encoding="utf-8")
(project_root / "Scripts" / "Drop.bas").write_text("Sub Drop()\nEnd Sub", encoding="utf-8")
(project_root / "main.xaml").write_text(MAIN_XAML, encoding="utf-8")
with self.assertLogs("uipath_explainator", level="INFO") as captured:
ProjectPipeline(project_root, output_root, "main.xaml", force=True).run(StubAnalyzer())
combined = "\n".join(captured.output)
self.assertIn("Starting pipeline:", combined)
self.assertIn("Initial scan complete:", combined)
self.assertIn("Copied 5 files and cleaned 1 XAML files", combined)
self.assertIn("Final scan complete:", combined)
self.assertIn("Pipeline completed in", combined)
def test_pipeline_keeps_running_when_single_analysis_fails(self) -> None:
with TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
project_root = tmp_path / "project"
output_root = tmp_path / "workspace"
docs_root = output_root / "docs"
(project_root / "Flows").mkdir(parents=True)
(project_root / "Flows" / "Active.xaml").write_text(ACTIVE_XAML, encoding="utf-8")
(project_root / "Scripts").mkdir()
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nEnd Sub", encoding="utf-8")
(project_root / "main.xaml").write_text(MAIN_XAML, encoding="utf-8")
report = ProjectPipeline(project_root, output_root, "main.xaml", force=True).run(FlakyAnalyzer())
self.assertTrue((docs_root / "Flows" / "Active.xaml.analysis.md").exists())
fallback = (docs_root / "Flows" / "Active.xaml.analysis.md").read_text(encoding="utf-8")
self.assertIn("Gemini 分析失败", fallback)
self.assertIn("HTTP 429 quota exhausted", fallback)
self.assertTrue((docs_root / "Scripts" / "Keep.bas.analysis.md").exists())
self.assertTrue(any("Analysis failed for Flows/Active.xaml" in item for item in report.warnings))
def test_pipeline_resume_skips_successfully_cached_analyses(self) -> None:
with TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
project_root = tmp_path / "project"
output_root = tmp_path / "workspace"
(project_root / "Flows").mkdir(parents=True)
(project_root / "Flows" / "Active.xaml").write_text(ACTIVE_XAML, encoding="utf-8")
(project_root / "Scripts").mkdir()
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nEnd Sub", encoding="utf-8")
(project_root / "main.xaml").write_text(MAIN_XAML, encoding="utf-8")
first = RecordingAnalyzer()
ProjectPipeline(project_root, output_root, "main.xaml", force=True).run(first)
self.assertEqual(
first.paths,
["Flows/Active.xaml", "main.xaml", "Scripts/Keep.bas"],
)
second = RecordingAnalyzer()
ProjectPipeline(project_root, output_root, "main.xaml", force=False).run(second)
self.assertEqual(second.paths, [])
def test_pipeline_resume_retries_failed_analysis_and_reanalyzes_changed_files(self) -> None:
with TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
project_root = tmp_path / "project"
output_root = tmp_path / "workspace"
(project_root / "Flows").mkdir(parents=True)
(project_root / "Flows" / "Active.xaml").write_text(ACTIVE_XAML, encoding="utf-8")
(project_root / "Scripts").mkdir()
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nEnd Sub", encoding="utf-8")
(project_root / "main.xaml").write_text(MAIN_XAML, encoding="utf-8")
ProjectPipeline(project_root, output_root, "main.xaml", force=True).run(FlakyAnalyzer())
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nMsgBox \"updated\"\nEnd Sub", encoding="utf-8")
retry = RecordingAnalyzer()
ProjectPipeline(project_root, output_root, "main.xaml", force=False).run(retry)
self.assertEqual(retry.paths, ["Flows/Active.xaml", "Scripts/Keep.bas"])
if __name__ == "__main__":
unittest.main()