gitlink-cli/scripts/research/test_lineage.py

261 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""test_lineage.py — lineage.py 纯函数单元测试。
不联网、不调 gitlink-cli。把「取数」与「算法」分离算法函数接收已取好的
Python 数据结构,本测试用 mock 数据喂算法。
运行: python test_lineage.py
"""
from __future__ import annotations
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# 仅 import 算法纯函数,绝不触发联网取数
import lineage as L # noqa: E402
class TestExperimentClassifier(unittest.TestCase):
def test_is_experiment_file_hits(self):
cases = [
"experiments/mnist/run.py",
"experiment_train.py",
"benchmark/imagenet/eval.py",
"eval/metrics.py",
"tests/test_model.py",
"test/foo.py",
"data/dataset.csv",
"src/benchmark_infer.py",
]
for p in cases:
with self.subTest(p=p):
self.assertTrue(L.is_experiment_file(p), f"应判为实验文件: {p}")
def test_is_experiment_file_misses(self):
cases = [
"src/model.py",
"README.md",
"docs/index.md",
"main.go",
"config.yaml",
"pkg/utils/util.py",
]
for p in cases:
with self.subTest(p=p):
self.assertFalse(L.is_experiment_file(p), f"不应判为实验文件: {p}")
def test_empty(self):
self.assertFalse(L.is_experiment_file(""))
self.assertFalse(L.is_experiment_file(None)) # type: ignore[arg-type]
class TestDocClassifier(unittest.TestCase):
def test_md_anywhere(self):
self.assertTrue(L.is_doc_file("README.md"))
self.assertTrue(L.is_doc_file("docs/guide.md"))
self.assertTrue(L.is_doc_file("deep/nested/notes.md"))
self.assertTrue(L.is_doc_file("GUIDE.MD")) # 大小写不敏感
def test_docs_dir(self):
self.assertTrue(L.is_doc_file("docs/index.html"))
self.assertTrue(L.is_doc_file("docs/config.yaml"))
self.assertTrue(L.is_doc_file("a/docs/b.txt"))
def test_non_doc(self):
self.assertFalse(L.is_doc_file("src/main.py"))
self.assertFalse(L.is_doc_file("tests/x.py"))
self.assertFalse(L.is_doc_file("data.csv"))
self.assertFalse(L.is_doc_file(""))
class TestTimestampParse(unittest.TestCase):
def test_iso_string(self):
t = L._to_epoch("2024-05-01T08:00:00Z")
self.assertGreater(t, 0)
# 反解回日期
self.assertTrue(L._iso_date(t).startswith("2024-05-01"))
def test_int_seconds(self):
self.assertAlmostEqual(L._to_epoch(1714521600), 1714521600.0)
def test_int_millis(self):
self.assertAlmostEqual(L._to_epoch(1714521600000), 1714521600.0)
def test_garbage(self):
self.assertEqual(L._to_epoch(None), 0.0)
self.assertEqual(L._to_epoch(""), 0.0)
self.assertEqual(L._to_epoch("not-a-date"), 0.0)
def test_iso_date_zero(self):
self.assertEqual(L._iso_date(0.0), "")
class TestBranchMap(unittest.TestCase):
def test_single_default_branch(self):
commits = [
{"timestamp": "2024-01-01T00:00:00Z"},
{"timestamp": "2024-06-01T00:00:00Z"},
{"timestamp": "2024-03-01T00:00:00Z"},
]
bm = L.build_branch_map(commits, "master")
self.assertEqual(len(bm), 1)
b = bm[0]
self.assertEqual(b["name"], "master")
self.assertTrue(b["is_default"])
self.assertEqual(b["commits"], 3)
# 最后活跃应取最大值 2024-06-01
self.assertTrue(b["last_active"].startswith("2024-06-01"))
def test_empty(self):
bm = L.build_branch_map([], "main")
self.assertEqual(bm, [{"name": "main", "commits": 0,
"last_active": "", "is_default": True}])
def test_non_list(self):
bm = L.build_branch_map(None, "main") # type: ignore[arg-type]
self.assertEqual(bm[0]["commits"], 0)
class TestCommitTimeline(unittest.TestCase):
def test_aggregation_and_sort(self):
commits = [
{"timestamp": "2024-01-02T00:00:00Z"},
{"timestamp": "2024-01-02T12:00:00Z"},
{"timestamp": "2024-01-01T00:00:00Z"},
]
tl = L.commit_timeline(commits, bucket="day")
self.assertEqual(tl, [
{"date": "2024-01-01", "count": 1},
{"date": "2024-01-02", "count": 2},
])
def test_skips_garbage(self):
commits = [{"timestamp": "bad"}, {"timestamp": ""}]
self.assertEqual(L.commit_timeline(commits), [])
class TestPrMergePatterns(unittest.TestCase):
def test_extract_and_sort(self):
prs = [
{"index": 10, "title": "feat A", "status": 1,
"pr_created_unix": 1717200000, "changed_files": 5},
{"index": 2, "title": "feat B", "status": 1,
"pr_merged_unix": 1714521600, "changed_files": 20},
{"index": 5, "title": "feat C", "status": 1,
"pr_created_unix": 1715000000, "additions": 3, "deletions": 4},
]
out = L.pr_merge_patterns(prs)
self.assertEqual(len(out), 3)
# 升序1714521600(2024-05-01) < 1715000000 < 1717200000
self.assertEqual(out[0]["number"], 2)
self.assertEqual(out[0]["changed_files"], 20)
self.assertTrue(out[0]["merged_time"].startswith("2024-05"))
# additions/deletions 兜底近似
last = next(p for p in out if p["number"] == 5)
self.assertEqual(last["changed_files"], 7)
# 空 merged_time 排末尾
no_time = L.pr_merge_patterns([
{"index": 1, "title": "x", "status": 1},
{"index": 2, "title": "y", "status": 1, "pr_created_unix": 1714521600},
])
self.assertEqual(no_time[-1]["number"], 1)
def test_empty(self):
self.assertEqual(L.pr_merge_patterns([]), [])
class TestDocEvolution(unittest.TestCase):
def test_filter_docs(self):
tree = [
{"name": "guide.md", "path": "docs/guide.md", "date": "2024-03-01"},
{"name": "index.html", "path": "docs/index.html"},
{"name": "model.py", "path": "src/model.py"}, # 排除
{"name": "old_2022-01-01.md", "path": "docs/old_2022-01-01.md"},
]
out = L.doc_evolution(tree)
files = {d["file"] for d in out}
self.assertIn("guide.md", files)
self.assertIn("index.html", files)
self.assertIn("old_2022-01-01.md", files)
self.assertNotIn("model.py", files)
# 文件名日期回退
old = next(d for d in out if d["file"] == "old_2022-01-01.md")
self.assertEqual(old["last_date"], "2022-01-01")
# 显式 date 字段优先
g = next(d for d in out if d["file"] == "guide.md")
self.assertEqual(g["last_date"], "2024-03-01")
class TestInnovationPoints(unittest.TestCase):
def test_high_impact_and_milestone(self):
prs = [
{"index": 1, "title": "chore: typo", "status": 1,
"pr_created_unix": 1714521600, "changed_files": 2}, # 低影响,无关键词
{"index": 2, "title": "feat: add transformer model", "status": 1,
"pr_created_unix": 1715000000, "changed_files": 25}, # 大规模+关键词
{"index": 3, "title": "implement benchmark suite", "status": 1,
"pr_created_unix": 1717200000, "changed_files": 4}, # 仅关键词
]
out = L.innovation_points(prs, commits=[])
# 应识别出 PR#2 和 PR#3PR#1 被过滤
nums = sorted(it["description"] for it in out)
self.assertTrue(any("transformer" in n.lower() for n in nums))
self.assertTrue(any("benchmark" in n.lower() for n in nums))
cats = [it["category"] for it in out]
self.assertIn("大规模重构/新特性", cats)
self.assertIn("特性引入", cats)
# 大规模 PR 排前impact 更高)
self.assertIn("transformer", out[0]["description"].lower())
# 每条都带证据
for it in out:
self.assertTrue(it["evidence"])
self.assertTrue(it["category"])
def test_empty(self):
self.assertEqual(L.innovation_points([], []), [])
def test_top_limit(self):
prs = [{"index": i, "title": f"add feature {i}", "status": 1,
"pr_created_unix": 1714521600 + i * 86400, "changed_files": 15}
for i in range(20)]
out = L.innovation_points(prs, commits=[], top=5)
self.assertEqual(len(out), 5)
class TestRender(unittest.TestCase):
"""渲染函数不抛异常、产出非空。"""
def _result(self):
return {
"scenario": "S1_repository_research_insight",
"repo": "o/r", "default_branch": "master",
"commit_timeline": [{"date": "2024-01-01", "count": 3}],
"branch_map": [{"name": "master", "commits": 5, "last_active": "2024-06-01",
"is_default": True}],
"pr_merge_patterns": [{"number": 2, "title": "feat A", "status": 1,
"merged_time": "2024-06-01", "changed_files": 9}],
"doc_evolution": [{"file": "guide.md", "last_date": "2024-03-01"}],
"experiment_files": ["benchmark/eval.py"],
"innovation_points": [{"description": "feat A", "evidence": "PR #2",
"category": "特性引入"}],
"meta": {"commit_count": 5, "merged_pr_count": 1, "doc_count": 1,
"experiment_file_count": 1},
}
def test_report(self):
r = L.render_report(self._result())
self.assertIn("仓库级科研项目洞悉报告", r)
self.assertIn("master", r)
self.assertIn("feat A", r)
def test_mermaid(self):
m = L.render_mermaid(self._result())
self.assertIn("gitGraph", m)
self.assertIn("master", m)
if __name__ == "__main__":
unittest.main(verbosity=2)