198 lines
8.0 KiB
TypeScript
198 lines
8.0 KiB
TypeScript
import type { CallRange, CallSymbol } from './model'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { collectCallEdges } from './walk'
|
|
|
|
function symbol(name: string, kind = 'function'): CallSymbol {
|
|
return { uri: `file:///${name}`, line: 0, character: 0, name, kind, path: `${name}.py` }
|
|
}
|
|
|
|
function range(line: number, character = 0): CallRange {
|
|
return { line, character, endLine: line, endCharacter: character + 3 }
|
|
}
|
|
|
|
function symbolIn(path: string, name: string): CallSymbol {
|
|
return { ...symbol(name), path }
|
|
}
|
|
|
|
describe('顺着调用者往上收集调用边', () => {
|
|
it('有环就停,不再问已经走过的那个', async () => {
|
|
const target = symbol('mask_phone')
|
|
const caller = symbol('get_profile')
|
|
const asked: string[] = []
|
|
const edges = await collectCallEdges(target, async (node) => {
|
|
asked.push(node.name)
|
|
if (node.name === 'mask_phone')
|
|
return [{ from: caller, ranges: [range(1)] }]
|
|
return [{ from: target, ranges: [range(2)] }]
|
|
}, async () => [])
|
|
expect(asked).toEqual(['mask_phone', 'get_profile'])
|
|
expect(edges).toEqual([{ caller, callee: target, at: range(1) }])
|
|
})
|
|
|
|
it('往上最多问 8 层,第 9 层的调用者不再问', async () => {
|
|
const names = ['t', ...Array.from({ length: 9 }, (_, index) => `n${index}`)]
|
|
const asked: string[] = []
|
|
await collectCallEdges(symbol('t'), async (node) => {
|
|
asked.push(node.name)
|
|
const index = names.indexOf(node.name)
|
|
const next = names[index + 1]
|
|
return next ? [{ from: symbol(next), ranges: [range(index + 1)] }] : []
|
|
}, async () => [])
|
|
expect(asked).toEqual(['t', 'n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6'])
|
|
})
|
|
|
|
it('重复的调用范围去掉,同一调用者的两段不同范围都留下', async () => {
|
|
const target = symbol('mask_phone')
|
|
const caller = symbol('serialize_user')
|
|
const edges = await collectCallEdges(target, async () => [{
|
|
from: caller,
|
|
ranges: [range(10), range(10), range(20)],
|
|
}], async () => [])
|
|
expect(edges.map(item => item.at.line)).toEqual([10, 20])
|
|
})
|
|
|
|
it('种类不同但名字起点相同的调用者是同一个人,只往上问一次', async () => {
|
|
const target = symbol('mask_phone')
|
|
const asFunction = symbol('get_profile', 'function')
|
|
const asClass = symbol('get_profile', 'class')
|
|
const asked: string[] = []
|
|
const edges = await collectCallEdges(target, async (node) => {
|
|
asked.push(`${node.name}:${node.kind}`)
|
|
if (node.name === 'mask_phone')
|
|
return [{ from: asFunction, ranges: [range(1)] }, { from: asClass, ranges: [range(1)] }]
|
|
return []
|
|
}, async () => [])
|
|
expect(asked.filter(item => item.startsWith('get_profile'))).toEqual(['get_profile:function'])
|
|
expect(edges).toHaveLength(1)
|
|
})
|
|
|
|
it('路径上的调用者还要问它调用了谁,不通向当前块的边也留下;当前块自己不再往下问', async () => {
|
|
const target = symbol('mask_phone')
|
|
const caller = symbol('serialize_user')
|
|
const side = symbol('format_name')
|
|
const outgoingAsked: string[] = []
|
|
const edges = await collectCallEdges(target, async (node) => {
|
|
if (node.name === 'mask_phone')
|
|
return [{ from: caller, ranges: [range(4)] }]
|
|
return []
|
|
}, async (node) => {
|
|
outgoingAsked.push(node.name)
|
|
return [
|
|
{ to: target, ranges: [range(4)] },
|
|
{ to: side, ranges: [range(8), range(8)] },
|
|
]
|
|
})
|
|
expect(outgoingAsked).toEqual(['serialize_user'])
|
|
expect(edges).toEqual([
|
|
{ caller, callee: target, at: range(4) },
|
|
{ caller, callee: side, at: range(8) },
|
|
])
|
|
})
|
|
|
|
it('业务代码的调用者里有测试函数:测试函数不当调用者,也不再往上问', async () => {
|
|
const target = symbolIn('app/sync.py', 'sync_users')
|
|
const job = symbolIn('app/jobs.py', 'run_jobs')
|
|
const inTestsDir = symbolIn('tests/test_sync.py', 'test_sync_runs')
|
|
const byFileName = symbolIn('test_x.py', 'test_x')
|
|
const asked: string[] = []
|
|
const outgoingAsked: string[] = []
|
|
const edges = await collectCallEdges(target, async (node) => {
|
|
asked.push(node.name)
|
|
if (node.name === 'sync_users') {
|
|
return [
|
|
{ from: inTestsDir, ranges: [range(3)] },
|
|
{ from: byFileName, ranges: [range(5)] },
|
|
{ from: job, ranges: [range(7)] },
|
|
]
|
|
}
|
|
return []
|
|
}, async (node) => {
|
|
outgoingAsked.push(node.name)
|
|
return []
|
|
})
|
|
expect(asked).toEqual(['sync_users', 'run_jobs'])
|
|
expect(outgoingAsked).toEqual(['run_jobs'])
|
|
expect(edges).toEqual([{ caller: job, callee: target, at: range(7) }])
|
|
})
|
|
|
|
it('往下问到的被调用方在测试文件里,这条边不收', async () => {
|
|
const target = symbolIn('app/sync.py', 'sync_users')
|
|
const job = symbolIn('app/jobs.py', 'run_jobs')
|
|
const fixture = symbolIn('tests/conftest.py', 'make_user')
|
|
const side = symbolIn('app/log.py', 'log_event')
|
|
const edges = await collectCallEdges(target, async (node) => {
|
|
if (node.name === 'sync_users')
|
|
return [{ from: job, ranges: [range(4)] }]
|
|
return []
|
|
}, async () => [
|
|
{ to: target, ranges: [range(4)] },
|
|
{ to: fixture, ranges: [range(6)] },
|
|
{ to: side, ranges: [range(8)] },
|
|
])
|
|
expect(edges).toEqual([
|
|
{ caller: job, callee: target, at: range(4) },
|
|
{ caller: job, callee: side, at: range(8) },
|
|
])
|
|
})
|
|
|
|
it('当前块本身在测试文件里:测试函数照样当调用者,往下问到的测试代码也收', async () => {
|
|
const target = symbolIn('tests/conftest.py', 'make_user')
|
|
const caller = symbolIn('tests/test_sync.py', 'test_sync_runs')
|
|
const helper = symbolIn('tests/helpers.py', 'fake_db')
|
|
const asked: string[] = []
|
|
const edges = await collectCallEdges(target, async (node) => {
|
|
asked.push(node.name)
|
|
if (node.name === 'make_user')
|
|
return [{ from: caller, ranges: [range(2)] }]
|
|
return []
|
|
}, async () => [
|
|
{ to: target, ranges: [range(2)] },
|
|
{ to: helper, ranges: [range(5)] },
|
|
])
|
|
expect(asked).toEqual(['make_user', 'test_sync_runs'])
|
|
expect(edges).toEqual([
|
|
{ caller, callee: target, at: range(2) },
|
|
{ caller, callee: helper, at: range(5) },
|
|
])
|
|
})
|
|
|
|
it('取消之后不再问下一个人', async () => {
|
|
const target = symbol('mask_phone')
|
|
const caller = symbol('get_profile')
|
|
let stop = false
|
|
const asked: string[] = []
|
|
const edges = await collectCallEdges(target, async (node) => {
|
|
asked.push(node.name)
|
|
stop = true
|
|
return [{ from: caller, ranges: [range(1)] }]
|
|
}, async () => [], () => stop)
|
|
expect(asked).toEqual(['mask_phone'])
|
|
expect(edges).toEqual([])
|
|
})
|
|
it('前端发请求的调用者带着请求写法进边,前端节点往上照常问;前端测试文件里发的请求不算', async () => {
|
|
const route = symbolIn('api/agent/routes.py', 'get_default_agent')
|
|
const fetchDefault = symbolIn('web/src/lib/agents.ts', 'fetchDefaultAgent')
|
|
const inTest = symbolIn('web/src/lib/__tests__/agents.ts', 'mocked')
|
|
const page = symbolIn('web/src/app/(main)/my-agents/page.tsx', 'MyAgentsPage')
|
|
const asked: string[] = []
|
|
const edges = await collectCallEdges(route, async (node) => {
|
|
asked.push(node.name)
|
|
if (node.name === 'get_default_agent') {
|
|
return [
|
|
{ from: fetchDefault, ranges: [range(10, 20)], request: 'GET /api/v1/agents/default' },
|
|
{ from: inTest, ranges: [range(3)], request: 'GET /api/v1/agents/default' },
|
|
{ from: symbolIn('web/src/lib/agents.test.ts', 'spec'), ranges: [range(4)], request: 'GET /api/v1/agents/default' },
|
|
]
|
|
}
|
|
if (node.name === 'fetchDefaultAgent')
|
|
return [{ from: page, ranges: [range(6)] }]
|
|
return []
|
|
}, async () => [])
|
|
expect(asked).toEqual(['get_default_agent', 'fetchDefaultAgent', 'MyAgentsPage'])
|
|
expect(edges).toEqual([
|
|
{ caller: fetchDefault, callee: route, at: range(10, 20), request: 'GET /api/v1/agents/default' },
|
|
{ caller: page, callee: fetchDefault, at: range(6) },
|
|
])
|
|
})
|
|
})
|