235 lines
10 KiB
TypeScript
235 lines
10 KiB
TypeScript
import type { CallEdge, CallRange, CallSymbol, CallTreeNode } from './model'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { buildCallTrees, nextPageUrl } from './model'
|
|
|
|
function symbol(name: string, line = 0, extra: Partial<CallSymbol> = {}): CallSymbol {
|
|
return {
|
|
uri: `file:///${name}`,
|
|
line,
|
|
character: 0,
|
|
name,
|
|
kind: 'function',
|
|
path: `${name}.py`,
|
|
...extra,
|
|
}
|
|
}
|
|
|
|
function at(line: number, character = 0): CallRange {
|
|
return { line, character, endLine: line, endCharacter: character + 4 }
|
|
}
|
|
|
|
function edge(caller: CallSymbol, callee: CallSymbol, line: number, character = 0): CallEdge {
|
|
return { caller, callee, at: at(line, character) }
|
|
}
|
|
|
|
function currents(node: CallTreeNode): CallTreeNode[] {
|
|
return [...(node.current ? [node] : []), ...node.children.flatMap(currents)]
|
|
}
|
|
|
|
describe('调用边收成调用树', () => {
|
|
const mask = symbol('mask_phone', 27, { path: 'utils/phone.py' })
|
|
|
|
it('两个没有更上层调用者的根是两棵树,每棵记下当前块出现几次', () => {
|
|
const read = symbol('读取用户资料', 0, { path: 'routes/user.py' })
|
|
const profile = symbol('get_profile', 10, { path: 'services/user.py' })
|
|
const serialize = symbol('serialize_user', 20, { path: 'services/user.py' })
|
|
const load = symbol('load_user', 30, { path: 'services/user.py' })
|
|
const exp = symbol('导出通讯录', 0, { path: 'routes/export.py' })
|
|
const trees = buildCallTrees([
|
|
edge(serialize, mask, 40),
|
|
edge(load, mask, 50),
|
|
edge(profile, serialize, 12),
|
|
edge(profile, load, 14),
|
|
edge(read, profile, 3),
|
|
edge(exp, mask, 8),
|
|
], mask)
|
|
expect(trees.map(tree => tree.name)).toEqual(['读取用户资料', '导出通讯录'])
|
|
expect(trees.map(tree => tree.hits)).toEqual([2, 1])
|
|
expect(trees[0].root.detail).toBe('入口')
|
|
expect(trees[0].root.where).toBe('routes/user.py:1')
|
|
expect(currents(trees[0].root).map(node => node.where)).toEqual(['utils/phone.py:28', 'utils/phone.py:28'])
|
|
expect(currents(trees[1].root)).toHaveLength(1)
|
|
})
|
|
|
|
it('同一个调用者的两段不同范围是当前块的两次出现', () => {
|
|
const caller = symbol('serialize_user', 20, { path: 'services/user.py' })
|
|
const root = symbol('读取用户资料', 0, { path: 'routes/user.py' })
|
|
const trees = buildCallTrees([
|
|
edge(caller, mask, 10),
|
|
edge(caller, mask, 20),
|
|
edge(root, caller, 3),
|
|
], mask)
|
|
expect(trees).toHaveLength(1)
|
|
expect(trees[0].hits).toBe(2)
|
|
const parent = trees[0].root.children[0]
|
|
expect(parent.name).toBe('serialize_user')
|
|
expect(parent.children.map(node => node.current)).toEqual([true, true])
|
|
expect(parent.otherCalls).toBeUndefined()
|
|
})
|
|
|
|
it('不在通向当前块的路径上的调用收成一行,不把那些符号画出来', () => {
|
|
const caller = symbol('serialize_user', 20, { path: 'services/user.py' })
|
|
const root = symbol('读取用户资料', 0, { path: 'routes/user.py' })
|
|
const trees = buildCallTrees([
|
|
edge(caller, mask, 10),
|
|
edge(caller, symbol('format_name'), 11),
|
|
edge(caller, symbol('format_name'), 12, 2),
|
|
edge(root, caller, 3),
|
|
edge(root, symbol('sideways'), 4),
|
|
edge(root, symbol('leftover'), 5),
|
|
], mask)
|
|
expect(trees).toHaveLength(1)
|
|
expect(trees[0].root.otherCalls).toBe(2)
|
|
expect(trees[0].root.children).toHaveLength(1)
|
|
expect(trees[0].root.children[0].otherCalls).toBe(2)
|
|
expect(trees[0].root.children[0].children.map(node => node.name)).toEqual(['mask_phone'])
|
|
const text = JSON.stringify(trees)
|
|
expect(text).not.toContain('format_name')
|
|
expect(text).not.toContain('sideways')
|
|
expect(text).not.toContain('leftover')
|
|
})
|
|
|
|
it('调用边绕回去就停,不把当前块再画到自己上面', () => {
|
|
const caller = symbol('get_profile')
|
|
const trees = buildCallTrees([
|
|
edge(caller, mask, 1),
|
|
edge(mask, caller, 2),
|
|
], mask)
|
|
expect(trees).toHaveLength(1)
|
|
expect(trees[0].name).toBe('get_profile')
|
|
expect(trees[0].root.children.map(node => node.name)).toEqual(['mask_phone'])
|
|
expect(trees[0].root.children[0].current).toBe(true)
|
|
expect(trees[0].root.children[0].children).toEqual([])
|
|
expect(trees[0].hits).toBe(1)
|
|
})
|
|
|
|
it('往上最多 8 层,再高的调用者不进这棵树', () => {
|
|
const above = Array.from({ length: 9 }, (_, index) => symbol(`n${index}`, index))
|
|
const edges = [edge(above[0], mask, 1)]
|
|
for (let index = 1; index < above.length; index++)
|
|
edges.push(edge(above[index], above[index - 1], index + 1))
|
|
const trees = buildCallTrees(edges, mask)
|
|
expect(trees).toHaveLength(1)
|
|
expect(trees[0].name).toBe('n7')
|
|
expect(JSON.stringify(trees)).not.toContain('n8')
|
|
})
|
|
|
|
it('重复的同一段调用范围只算一次', () => {
|
|
const caller = symbol('serialize_user')
|
|
const trees = buildCallTrees([
|
|
edge(caller, mask, 10),
|
|
edge(caller, mask, 10),
|
|
], mask)
|
|
expect(trees).toHaveLength(1)
|
|
expect(trees[0].hits).toBe(1)
|
|
})
|
|
|
|
it('同一次查询里种类变了仍是同一个符号,不拆成两棵', () => {
|
|
const asFunction = symbol('get_profile', 4, { uri: 'file:///services/user.py', path: 'services/user.py', kind: 'function' })
|
|
const asClass = symbol('get_profile', 4, { uri: 'file:///services/user.py', path: 'services/user.py', kind: 'class' })
|
|
const trees = buildCallTrees([
|
|
edge(asFunction, mask, 8),
|
|
edge(asClass, mask, 8),
|
|
], mask)
|
|
expect(trees).toHaveLength(1)
|
|
expect(trees[0].hits).toBe(1)
|
|
expect(trees[0].root.kind).toBe('function')
|
|
})
|
|
|
|
it('没有任何调用者就没有树', () => {
|
|
expect(buildCallTrees([], mask)).toEqual([])
|
|
})
|
|
|
|
it('选中的是类时,直接调用它的那一层是「新建」,类自己标当前,不编造方法', () => {
|
|
const order = symbol('OrderService', 13, { uri: 'file:///billing/order.py', path: 'billing/order.py', kind: 'class' })
|
|
const create = symbol('create_order', 20, { path: 'billing/order.py', detail: '创建一笔订单' })
|
|
const post = symbol('POST /orders', 0, { path: 'routes/order.py' })
|
|
const trees = buildCallTrees([
|
|
edge(create, order, 39),
|
|
edge(post, create, 5),
|
|
edge(create, symbol('charge'), 41),
|
|
], order, { classTarget: true })
|
|
expect(trees).toHaveLength(1)
|
|
expect(trees[0].hits).toBe(1)
|
|
const created = trees[0].root.children[0]
|
|
expect(created.name).toBe('create_order')
|
|
expect(created.detail).toBe('创建一笔订单')
|
|
expect(created.otherCalls).toBe(1)
|
|
const made = created.children[0]
|
|
expect(made.name).toBe('新建 OrderService')
|
|
expect(made.detail).toBe('进入这个类')
|
|
expect(made.where).toBe('billing/order.py:40')
|
|
expect(made.jump).toEqual({ uri: 'file:///create_order', line: 39, character: 0 })
|
|
expect(made.current).toBe(false)
|
|
expect(made.kind).toBe('class')
|
|
expect(made.children).toEqual([{
|
|
name: 'OrderService',
|
|
kind: 'class',
|
|
where: 'billing/order.py:14',
|
|
jump: { uri: 'file:///billing/order.py', line: 13, character: 0 },
|
|
current: true,
|
|
children: [],
|
|
}])
|
|
expect(JSON.stringify(trees)).not.toContain('方法')
|
|
})
|
|
})
|
|
|
|
describe('接口的调用树', () => {
|
|
const route = symbol('get_default_agent', 78, { uri: 'file:///api/agent/routes.py', path: 'api/agent/routes.py' })
|
|
|
|
it('前端发请求的那一行单独成「请求」节点,点了跳到那一行;Next.js 页面入口第二行写页面网址', () => {
|
|
const fetchDefault = symbol('fetchDefaultAgent', 9, { uri: 'file:///web/src/lib/agents.ts', path: 'web/src/lib/agents.ts' })
|
|
const page = symbol('MyAgentsPage', 4, { uri: 'file:///web/src/app/(main)/my-agents/page.tsx', path: 'web/src/app/(main)/my-agents/page.tsx' })
|
|
const sidebar = symbol('useSidebar', 3, { uri: 'file:///web/src/components/sidebar/useSidebar.ts', path: 'web/src/components/sidebar/useSidebar.ts' })
|
|
const trees = buildCallTrees([
|
|
{ ...edge(fetchDefault, route, 10, 20), request: 'GET /api/v1/agents/default' },
|
|
edge(page, fetchDefault, 6),
|
|
edge(sidebar, fetchDefault, 8),
|
|
], route)
|
|
expect(trees.map(tree => [tree.name, tree.root.detail])).toEqual([['MyAgentsPage', '页面 /my-agents'], ['useSidebar', '入口']])
|
|
const request = trees[0].root.children[0].children[0]
|
|
expect(request).toEqual({
|
|
name: '请求 GET /api/v1/agents/default',
|
|
kind: 'request',
|
|
where: 'web/src/lib/agents.ts:11',
|
|
jump: { uri: 'file:///web/src/lib/agents.ts', line: 10, character: 20 },
|
|
current: false,
|
|
children: [{
|
|
name: 'get_default_agent',
|
|
kind: 'function',
|
|
where: 'api/agent/routes.py:79',
|
|
jump: { uri: 'file:///api/agent/routes.py', line: 78, character: 0 },
|
|
current: true,
|
|
children: [],
|
|
}],
|
|
})
|
|
expect(trees[0].hits).toBe(1)
|
|
expect(trees[0].root.jump).toEqual({ uri: 'file:///web/src/app/(main)/my-agents/page.tsx', line: 4, character: 0 })
|
|
})
|
|
|
|
it('在 Python 里直接调用路由函数的不加「请求」节点', () => {
|
|
const job = symbol('nightly_job', 2, { path: 'jobs/nightly.py' })
|
|
const trees = buildCallTrees([edge(job, route, 5)], route)
|
|
expect(trees[0].root.children.map(node => node.name)).toEqual(['get_default_agent'])
|
|
})
|
|
})
|
|
|
|
describe('按 Next.js 的约定算页面网址', () => {
|
|
it('按 app 目录下的目录算:去掉路由分组和并行路由,动态段原样留', () => {
|
|
expect(nextPageUrl('web/src/app/(main)/my-agents/page.tsx')).toBe('/my-agents')
|
|
expect(nextPageUrl('app/page.tsx')).toBe('/')
|
|
expect(nextPageUrl('client/src/app/(main)/billing/orders/[orderId]/page.tsx')).toBe('/billing/orders/[orderId]')
|
|
expect(nextPageUrl('app/docs/[[...section]]/page.js')).toBe('/docs/[[...section]]')
|
|
expect(nextPageUrl('app/dashboard/@team/settings/page.jsx')).toBe('/dashboard/settings')
|
|
expect(nextPageUrl('app/(marketing)/page.ts')).toBe('/')
|
|
})
|
|
|
|
it('不是 app 目录下的 page 文件就不是页面', () => {
|
|
expect(nextPageUrl('app/layout.tsx')).toBeUndefined()
|
|
expect(nextPageUrl('pages/index.tsx')).toBeUndefined()
|
|
expect(nextPageUrl('src/apps/page.tsx')).toBeUndefined()
|
|
expect(nextPageUrl('app/page.mdx')).toBeUndefined()
|
|
expect(nextPageUrl('page.tsx')).toBeUndefined()
|
|
})
|
|
})
|