Files
MyGoNavi/internal/app/explain_parse_common_test.go
Syngnat 1a8effb51f 🐛 fix(export,ssh,secret,explain): 修复 SQL 导出注入与六处静默数据丢失/资源泄漏
- SQL 导出:formatSQLValue 按方言转义反斜杠,修复 MySQL 系 dump 还原时静默改写数据、
  且以反斜杠结尾的值吞掉闭合引号致源库恶意行可执行任意 SQL 的问题;非 MySQL 方言保持原样
- 明文凭据:daily_secrets.json 由 0o644 改为 0o600、目录改 0o700,并对历史文件显式 Chmod
- SSH 隧道:RegisterSSHNetwork 改为确定性 network 名并复用缓存客户端,消除驱动全局 dialer
  表随重连线性增长、永久钉住 ssh.Client 的连接与 goroutine 泄漏
- xlsx 导入:单元格 r 属性列号增加 OOXML 16384 上限并提前熔断,避免篡改文件放大分配致 OOM
- 数据导出:三处非 SQL 导出入口捕获 file.Close 错误,不再在落盘失败时返回“导出成功”
- 数据根迁移:copyFile 捕获 Close 错误并补 Sync,避免被截断的副本被判定为迁移成功
- 执行计划:节点 ID 改为按 ExplainResult 派生,移除进程级全局计数器与 reset,
  修复并发诊断互相踩踏编号导致的重复 node ID 与错挂父子边
- 补充 5 个回归测试文件,含“导出→切分”闭环断言与未转义时的反向对照
2026-07-26 19:40:33 +08:00

97 lines
3.7 KiB
Go
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.
package app
import (
"fmt"
"sync"
"testing"
"GoNavi-Wails/internal/connection"
)
// TestAppendExplainChildNumbersPerResult 覆盖节点 ID 的按解析隔离。
//
// 回归背景:节点 ID 原先由包级全局计数器 explainNodeIDCounter 生成,每个方言解析器在入口
// 调用 resetExplainNodeID() 归零。DiagnoseQuery 是 Wails 绑定方法,每个前端调用都在独立
// goroutine 中派发,两次并发诊断会互相踩踏该计数器,导致同一份 ExplainResult 内出现重复
// node ID、Edges 的 From/To 指向歧义节点、前端计划图渲染错乱。
func TestAppendExplainChildNumbersPerResult(t *testing.T) {
var first connection.ExplainResult
rootID := appendExplainChild(&first, "", connection.ExplainNode{OpDetail: "root"})
childID := appendExplainChild(&first, rootID, connection.ExplainNode{OpDetail: "child"})
grandID := appendExplainChild(&first, childID, connection.ExplainNode{OpDetail: "grand"})
if rootID != "n1" || childID != "n2" || grandID != "n3" {
t.Fatalf("编号序列 = %s/%s/%s期望 n1/n2/n3", rootID, childID, grandID)
}
// 另一份 result 必须从 n1 重新开始,不受前一份影响。
var second connection.ExplainResult
if got := appendExplainChild(&second, "", connection.ExplainNode{OpDetail: "root"}); got != "n1" {
t.Fatalf("第二份 result 的首个节点 ID = %s期望 n1编号未按 result 隔离)", got)
}
// Edge 必须指向正确的父子节点。
if len(first.Edges) != 2 {
t.Fatalf("Edges 数量 = %d期望 2", len(first.Edges))
}
if first.Edges[0].From != "n1" || first.Edges[0].To != "n2" {
t.Errorf("Edges[0] = %s→%s期望 n1→n2", first.Edges[0].From, first.Edges[0].To)
}
if first.Edges[1].From != "n2" || first.Edges[1].To != "n3" {
t.Errorf("Edges[1] = %s→%s期望 n2→n3", first.Edges[1].From, first.Edges[1].To)
}
}
// TestAppendExplainChildConcurrentResultsHaveUniqueIDs 并发构建多份 ExplainResult
// 断言每份内部的节点 ID 都不重复——这是原全局计数器方案会失败的场景。
func TestAppendExplainChildConcurrentResultsHaveUniqueIDs(t *testing.T) {
const parsers = 8
const nodesPerParser = 60
results := make([]connection.ExplainResult, parsers)
var wg sync.WaitGroup
wg.Add(parsers)
for i := 0; i < parsers; i++ {
go func(idx int) {
defer wg.Done()
parentID := appendExplainChild(&results[idx], "", connection.ExplainNode{OpDetail: "root"})
for n := 1; n < nodesPerParser; n++ {
parentID = appendExplainChild(&results[idx], parentID, connection.ExplainNode{
OpDetail: fmt.Sprintf("node-%d", n),
})
}
}(i)
}
wg.Wait()
for idx := range results {
seen := make(map[string]int, nodesPerParser)
for _, node := range results[idx].Nodes {
seen[node.ID]++
if seen[node.ID] > 1 {
t.Fatalf("result[%d] 出现重复节点 ID %s并发解析互相踩踏编号", idx, node.ID)
}
}
if len(results[idx].Nodes) != nodesPerParser {
t.Errorf("result[%d] 节点数 = %d期望 %d", idx, len(results[idx].Nodes), nodesPerParser)
}
// 每个 Edge 的两端都必须存在于本 result 的节点集合内。
for _, edge := range results[idx].Edges {
if _, ok := seen[edge.From]; !ok {
t.Errorf("result[%d] Edge.From=%s 不存在于本 result 节点集合", idx, edge.From)
}
if _, ok := seen[edge.To]; !ok {
t.Errorf("result[%d] Edge.To=%s 不存在于本 result 节点集合", idx, edge.To)
}
}
}
}
// TestAppendExplainChildKeepsPresetNodeID 保证显式指定的 ID 不被覆盖。
func TestAppendExplainChildKeepsPresetNodeID(t *testing.T) {
var result connection.ExplainResult
if got := appendExplainChild(&result, "", connection.ExplainNode{ID: "custom", OpDetail: "root"}); got != "custom" {
t.Fatalf("预设 ID 被覆盖为 %s", got)
}
}