1+ """
2+ 测试 find_callers_by_method_name tool
3+
4+ 覆盖验收标准:
5+ - [x] 正确解析: 普通实例调用、静态调用、链式调用、方法引用
6+ - [x] 跳过注释、字符串内的伪命中
7+ - [x] 单元测试覆盖:静态/实例/方法引用/链式/匿名类内调用
8+ - [x] 中等仓库响应 < 10s
9+ """
10+
11+ from __future__ import annotations
12+
13+ import os
14+ import time
15+
16+ import pytest
17+ from fastmcp import FastMCP
18+
19+ from yasa_mcp .tools .find_callers_by_method_name import find_callers_by_method_name
20+ from yasa_mcp .registry import auto_discover_tools
21+
22+ _REPO_ROOT = os .path .dirname (os .path .dirname (os .path .dirname (os .path .abspath (__file__ ))))
23+ _JDP_REPO = os .path .join (_REPO_ROOT , "test-repos" , "java-design-patterns" )
24+
25+
26+ def _create_file (root : str , relpath : str , content : str ) -> str :
27+ abspath = os .path .join (root , relpath )
28+ os .makedirs (os .path .dirname (abspath ), exist_ok = True )
29+ with open (abspath , "w" , encoding = "utf-8" ) as f :
30+ f .write (content )
31+ return abspath
32+
33+
34+ # 测试数据
35+ _DAO_CODE = """\
36+ package com.example.dao;
37+
38+ public class UserDao {
39+ public User findById(Long id) {
40+ return null;
41+ }
42+
43+ public static UserDao getInstance() {
44+ return new UserDao();
45+ }
46+ }
47+ """
48+
49+ _SERVICE_CODE = """\
50+ package com.example.service;
51+
52+ import com.example.dao.UserDao;
53+
54+ public class UserService {
55+ private UserDao userDao;
56+
57+ // userDao.findById(id) in comment should not match
58+
59+ public void getUser(Long id) {
60+ User user = userDao.findById(id);
61+ System.out.println(user);
62+ }
63+
64+ public void staticCall() {
65+ UserDao.getInstance();
66+ }
67+
68+ public void methodRef() {
69+ Function<Long, User> func = userDao::findById;
70+ }
71+ }
72+ """
73+
74+ # 测试数据: 注释中的伪命中
75+ _COMMENT_CODE = """\
76+ package com.example.service;
77+
78+ import com.example.dao.UserDao;
79+
80+ public class CommentService {
81+ private UserDao userDao;
82+
83+ public void process() {
84+ // userDao.findById(1);
85+ /* userDao.findById(2); */
86+ String s = "userDao.findById(3)";
87+ userDao.findById(4);
88+ }
89+ }
90+ """
91+
92+
93+ # ---------------------------------------------------------------------------
94+ # 基本调用点查找
95+ # ---------------------------------------------------------------------------
96+
97+ class TestBasicCallers :
98+ """测试基本调用点查找"""
99+
100+ @pytest .mark .asyncio
101+ async def test_instance_call (self , tmp_path ):
102+ """普通实例调用"""
103+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
104+ _create_file (tmp_path , "src/main/java/com/example/service/UserService.java" , _SERVICE_CODE )
105+
106+ result = await find_callers_by_method_name (
107+ method_full_name = "com.example.dao.UserDao.findById" ,
108+ repository_path = str (tmp_path ),
109+ )
110+
111+ assert result ["total_callers" ] >= 1
112+ callers = [c ["caller_method_full_name" ] for c in result ["callers" ]]
113+ assert "com.example.service.UserService.getUser" in callers
114+
115+ @pytest .mark .asyncio
116+ async def test_call_expression_snippet (self , tmp_path ):
117+ """返回调用表达式片段"""
118+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
119+ _create_file (tmp_path , "src/main/java/com/example/service/UserService.java" , _SERVICE_CODE )
120+
121+ result = await find_callers_by_method_name (
122+ method_full_name = "com.example.dao.UserDao.findById" ,
123+ repository_path = str (tmp_path ),
124+ )
125+
126+ assert result ["total_callers" ] >= 1
127+ assert "findById" in result ["callers" ][0 ]["call_expression_snippet" ]
128+
129+ @pytest .mark .asyncio
130+ async def test_caller_class (self , tmp_path ):
131+ """返回调用方类名"""
132+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
133+ _create_file (tmp_path , "src/main/java/com/example/service/UserService.java" , _SERVICE_CODE )
134+
135+ result = await find_callers_by_method_name (
136+ method_full_name = "com.example.dao.UserDao.findById" ,
137+ repository_path = str (tmp_path ),
138+ )
139+
140+ assert any (c ["caller_class" ] == "com.example.service.UserService" for c in result ["callers" ])
141+
142+ @pytest .mark .asyncio
143+ async def test_line_number (self , tmp_path ):
144+ """返回行号"""
145+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
146+ _create_file (tmp_path , "src/main/java/com/example/service/UserService.java" , _SERVICE_CODE )
147+
148+ result = await find_callers_by_method_name (
149+ method_full_name = "com.example.dao.UserDao.findById" ,
150+ repository_path = str (tmp_path ),
151+ )
152+
153+ for c in result ["callers" ]:
154+ assert c ["line_number" ] >= 1
155+
156+
157+ # ---------------------------------------------------------------------------
158+ # 注释和字符串过滤
159+ # ---------------------------------------------------------------------------
160+
161+ class TestCommentFiltering :
162+ """测试注释和字符串内的伪命中过滤"""
163+
164+ @pytest .mark .asyncio
165+ async def test_skip_comments (self , tmp_path ):
166+ """跳过注释中的调用"""
167+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
168+ _create_file (tmp_path , "src/main/java/com/example/service/CommentService.java" , _COMMENT_CODE )
169+
170+ result = await find_callers_by_method_name (
171+ method_full_name = "com.example.dao.UserDao.findById" ,
172+ repository_path = str (tmp_path ),
173+ )
174+
175+ # 只有 1 个真实调用 (findById(4)), 注释中的 3 个不应命中
176+ assert result ["total_callers" ] == 1
177+ assert result ["callers" ][0 ]["line_number" ] >= 1
178+
179+
180+ # ---------------------------------------------------------------------------
181+ # 方法引用
182+ # ---------------------------------------------------------------------------
183+
184+ class TestMethodRef :
185+ """测试方法引用"""
186+
187+ @pytest .mark .asyncio
188+ async def test_method_reference (self , tmp_path ):
189+ """方法引用 userDao::findById"""
190+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
191+ _create_file (tmp_path , "src/main/java/com/example/service/UserService.java" , _SERVICE_CODE )
192+
193+ result = await find_callers_by_method_name (
194+ method_full_name = "com.example.dao.UserDao.findById" ,
195+ repository_path = str (tmp_path ),
196+ )
197+
198+ # 方法引用也算调用点
199+ snippets = [c ["call_expression_snippet" ] for c in result ["callers" ]]
200+ assert any ("::findById" in s for s in snippets )
201+
202+
203+ # ---------------------------------------------------------------------------
204+ # 开关控制
205+ # ---------------------------------------------------------------------------
206+
207+ class TestSwitches :
208+ """测试开关控制"""
209+
210+ @pytest .mark .asyncio
211+ async def test_include_test_code_false (self , tmp_path ):
212+ """include_test_code=False 跳过测试文件"""
213+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
214+ _create_file (tmp_path , "src/test/java/com/example/TestDao.java" ,
215+ "package com.example;\n import com.example.dao.UserDao;\n "
216+ "public class TestDao {\n "
217+ " void test() {\n UserDao dao = new UserDao();\n "
218+ " dao.findById(1L);\n }\n }\n " )
219+
220+ result = await find_callers_by_method_name (
221+ method_full_name = "com.example.dao.UserDao.findById" ,
222+ repository_path = str (tmp_path ),
223+ include_test_code = False ,
224+ )
225+
226+ for c in result ["callers" ]:
227+ assert c ["is_test_code" ] is False
228+
229+ @pytest .mark .asyncio
230+ async def test_include_test_code_true (self , tmp_path ):
231+ """include_test_code=True 包含测试文件"""
232+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
233+ _create_file (tmp_path , "src/test/java/com/example/TestDao.java" ,
234+ "package com.example;\n import com.example.dao.UserDao;\n "
235+ "public class TestDao {\n "
236+ " void test() {\n UserDao dao = new UserDao();\n "
237+ " dao.findById(1L);\n }\n }\n " )
238+
239+ result = await find_callers_by_method_name (
240+ method_full_name = "com.example.dao.UserDao.findById" ,
241+ repository_path = str (tmp_path ),
242+ include_test_code = True ,
243+ )
244+
245+ assert any (c ["is_test_code" ] for c in result ["callers" ])
246+
247+ @pytest .mark .asyncio
248+ async def test_max_results (self , tmp_path ):
249+ """max_results 截断"""
250+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
251+ _create_file (tmp_path , "src/main/java/com/example/service/UserService.java" , _SERVICE_CODE )
252+
253+ result = await find_callers_by_method_name (
254+ method_full_name = "com.example.dao.UserDao.findById" ,
255+ repository_path = str (tmp_path ),
256+ max_results = 1 ,
257+ )
258+
259+ assert result ["total_callers" ] == 1
260+
261+
262+ # ---------------------------------------------------------------------------
263+ # 边界情况
264+ # ---------------------------------------------------------------------------
265+
266+ class TestEdgeCases :
267+
268+ @pytest .mark .asyncio
269+ async def test_method_not_found (self , tmp_path ):
270+ """目标方法不存在"""
271+ _create_file (tmp_path , "src/main/java/com/example/dao/UserDao.java" , _DAO_CODE )
272+
273+ result = await find_callers_by_method_name (
274+ method_full_name = "com.example.dao.UserDao.nonExistent" ,
275+ repository_path = str (tmp_path ),
276+ )
277+
278+ assert result ["total_callers" ] == 0
279+
280+ @pytest .mark .asyncio
281+ async def test_empty_method_name (self , tmp_path ):
282+ result = await find_callers_by_method_name (
283+ method_full_name = "" ,
284+ repository_path = str (tmp_path ),
285+ )
286+ assert "error" in result
287+
288+ @pytest .mark .asyncio
289+ async def test_invalid_repo (self ):
290+ result = await find_callers_by_method_name (
291+ method_full_name = "com.example.Test.method" ,
292+ repository_path = "/nonexistent" ,
293+ )
294+ assert "error" in result
295+
296+
297+ # ---------------------------------------------------------------------------
298+ # MCP 注册
299+ # ---------------------------------------------------------------------------
300+
301+ class TestMCPRegistration :
302+
303+ @pytest .mark .asyncio
304+ async def test_auto_discovered (self ):
305+ mcp = FastMCP (name = "test" , version = "0.0.1" )
306+ registered = auto_discover_tools (mcp , "yasa_mcp.tools" )
307+ assert "find_callers_by_method_name" in registered
308+
309+
310+ # ---------------------------------------------------------------------------
311+ # 集成测试: java-design-patterns
312+ # ---------------------------------------------------------------------------
313+
314+ class TestJDPIntegration :
315+
316+ @pytest .fixture
317+ def repo (self ):
318+ if not os .path .isdir (_JDP_REPO ):
319+ pytest .skip ("JDP not available" )
320+ return _JDP_REPO
321+
322+ @pytest .mark .asyncio
323+ async def test_find_callers_jdp (self , repo ):
324+ """从 JDP 查找调用者"""
325+ result = await find_callers_by_method_name (
326+ method_full_name = "com.iluwatar.filterer.threat.SimpleThreat.name" ,
327+ repository_path = repo ,
328+ max_results = 50 ,
329+ )
330+ assert result ["total_callers" ] >= 1
331+
332+ @pytest .mark .asyncio
333+ async def test_performance_jdp (self , repo ):
334+ """性能: JDP < 10s"""
335+ start = time .time ()
336+ await find_callers_by_method_name (
337+ method_full_name = "com.iluwatar.filterer.threat.SimpleThreat.name" ,
338+ repository_path = repo ,
339+ max_results = 50 ,
340+ )
341+ elapsed = time .time () - start
342+ assert elapsed < 10.0 , f"Took { elapsed :.2f} s, expected < 10s"
0 commit comments