|
@@ -1,30 +1,36 @@
|
|
|
-// DDL 指纹锁单测(AC-17 验收⑥的前置门禁)——设计 v0.1.2 §2.4「L2 DDL 哈希校验」。
|
|
|
|
|
|
|
+// DDL 指纹锁单测(AC-17 验收⑥的前置门禁)——设计 **v0.1.3 §2.6「sha256-lf-v1 口径」**。
|
|
|
//
|
|
//
|
|
|
-// 目的:目标态 DDL 是**跨仓双份**存在的(文档 `unified/03-migration/academic-model-ddl.sql`
|
|
|
|
|
-// 与代码仓 fixture `internal/academic/model/testdata/academic-model-ddl.sql`)。文档不在代码仓,
|
|
|
|
|
-// 跨仓字节相等无法由 Go 测试直接判定,因此本仓这一侧做「指纹锁」:
|
|
|
|
|
|
|
+// 目标态 DDL 跨仓双份存在(文档 `unified/03-migration/academic-model-ddl.sql` 与代码仓 fixture
|
|
|
|
|
+// `internal/academic/model/testdata/academic-model-ddl.sql`)。文档不在代码仓,跨仓字节相等无法由
|
|
|
|
|
+// Go 测试直接判定,因此本仓这一侧做「指纹锁」:
|
|
|
//
|
|
//
|
|
|
-// sha256(fixture, LF 归一) == testdata/academic-model-ddl.sha256 的内容
|
|
|
|
|
|
|
+// sha256(lf-normalize(fixture)) == testdata/academic-model-ddl.sha256 的 digest 行
|
|
|
//
|
|
//
|
|
|
-// 配合 AC-17 的人工/报告流程:对「文档副本」与「fixture」各跑一次 sha256 并与锁文件比对,
|
|
|
|
|
-// 三者一致才视为"文档与代码未漂移",结果写进一致性报告留证。
|
|
|
|
|
|
|
+// 配合 AC-17 人工/报告流程:对「文档副本」与「fixture」各跑一次 sha256(用 `tr -d '\r'` 复现同一
|
|
|
|
|
+// 归一)后与锁文件比对,三者一致才视为未漂移,结论写进一致性报告留证。
|
|
|
//
|
|
//
|
|
|
// 纪律(任一 DDL 变更必须**同一提交**内更新三处):
|
|
// 纪律(任一 DDL 变更必须**同一提交**内更新三处):
|
|
|
// 1. 文档 `unified/03-migration/academic-model-ddl.sql`;
|
|
// 1. 文档 `unified/03-migration/academic-model-ddl.sql`;
|
|
|
// 2. 本仓 fixture `testdata/academic-model-ddl.sql`;
|
|
// 2. 本仓 fixture `testdata/academic-model-ddl.sql`;
|
|
|
-// 3. 本仓锁文件 `testdata/academic-model-ddl.sha256`(单行 64 位小写 hex)。
|
|
|
|
|
|
|
+// 3. 本仓锁文件 `testdata/academic-model-ddl.sha256`。
|
|
|
//
|
|
//
|
|
|
-// 为什么按「LF 归一」取指纹(不是裸字节):
|
|
|
|
|
|
|
+// ── sha256-lf-v1 口径定义(§2.6 约束①,**本文件是实现,改动须先改设计**)──
|
|
|
//
|
|
//
|
|
|
-// 开发者机器常见 `core.autocrlf=true`(Windows 检出为 CRLF),同一内容在 Linux CI 会得到
|
|
|
|
|
-// 不同 sha256 → 裸字节指纹必然产生"跨平台假红"。归一掉换行风格后,指纹只对**内容**敏感:
|
|
|
|
|
-// 任何字符级修改都会改变它,而"纯换行风格"这种无意义差异不会误报。
|
|
|
|
|
-// (实测:Windows 工作区与 git blob 均为 LF,两者指纹一致 = 7a25c918…75935。)
|
|
|
|
|
|
|
+// 归一:**仅** `\r\n → \n`(一行 ReplaceAll)。
|
|
|
|
|
+// 单 CR、末尾换行增删、BOM、行尾空白、大小写 **一律不归一**(它们是内容差异,必须红)。
|
|
|
|
|
+// 摘要:sha256(归一字节) → 64 位小写 hex。
|
|
|
|
|
+// 自描述:锁文件允许 `#` 注释行,**必须**含 `# algo=sha256-lf-v1`;digest 为第一个非注释行。
|
|
|
|
|
+// 目的:将来若换口径(如 eol=lf 落地后改裸字节),不会出现"静默变红且原因不明"。
|
|
|
|
|
+//
|
|
|
|
|
+// 为什么需要 LF 归一(而非裸字节):本仓 `core.autocrlf=true`,同一内容 Windows 检出为 CRLF、
|
|
|
|
|
+// Linux CI 为 LF;裸字节指纹必然跨平台假红,而这种红是纯噪声。归一只放过"换行风格"这一种
|
|
|
|
|
+// 无意义差异,任何字符级修改仍会改变摘要(负例见 TestDDLHashAlgoContract)。
|
|
|
package model
|
|
package model
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
"crypto/sha256"
|
|
"crypto/sha256"
|
|
|
"encoding/hex"
|
|
"encoding/hex"
|
|
|
|
|
+ "fmt"
|
|
|
"os"
|
|
"os"
|
|
|
"strings"
|
|
"strings"
|
|
|
"testing"
|
|
"testing"
|
|
@@ -33,86 +39,227 @@ import (
|
|
|
const (
|
|
const (
|
|
|
// ddlFixturePath DDL fixture 相对路径(go test 以包目录为工作目录)。
|
|
// ddlFixturePath DDL fixture 相对路径(go test 以包目录为工作目录)。
|
|
|
ddlFixturePath = "testdata/academic-model-ddl.sql"
|
|
ddlFixturePath = "testdata/academic-model-ddl.sql"
|
|
|
- // ddlHashPath DDL 指纹锁文件路径(单行 64 位小写 hex,可被 awk '{print $1}' 直接取用)。
|
|
|
|
|
|
|
+ // ddlHashPath DDL 指纹锁文件路径。
|
|
|
ddlHashPath = "testdata/academic-model-ddl.sha256"
|
|
ddlHashPath = "testdata/academic-model-ddl.sha256"
|
|
|
// ddlHashHexLen sha256 十六进制串长度。
|
|
// ddlHashHexLen sha256 十六进制串长度。
|
|
|
ddlHashHexLen = 64
|
|
ddlHashHexLen = 64
|
|
|
|
|
+ // ddlHashAlgo 口径标识(§2.6 约束①:锁文件必须自描述,换口径须同步改本常量与设计)。
|
|
|
|
|
+ ddlHashAlgo = "sha256-lf-v1"
|
|
|
|
|
+ // ddlHashAlgoMarker 锁文件内的口径声明行(必须是合法注释行)。
|
|
|
|
|
+ ddlHashAlgoMarker = "# algo=" + ddlHashAlgo
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
-// normalizeEOL 把 CRLF / 孤立 CR 统一为 LF(跨平台指纹稳定的唯一前提)。
|
|
|
|
|
|
|
+// lfNormalize 按 sha256-lf-v1 口径归一:**仅** CRLF → LF。
|
|
|
|
|
+//
|
|
|
|
|
+// 入参:b []byte——文件原始字节。出参:[]byte——归一字节(其余字符一字不动)。
|
|
|
//
|
|
//
|
|
|
-// 入参:b []byte——文件原始字节。出参:[]byte——换行归一后的字节(不改其它任何字符)。
|
|
|
|
|
-func normalizeEOL(b []byte) []byte {
|
|
|
|
|
- s := strings.ReplaceAll(string(b), "\r\n", "\n")
|
|
|
|
|
- s = strings.ReplaceAll(s, "\r", "\n")
|
|
|
|
|
- return []byte(s)
|
|
|
|
|
|
|
+// 反例(必须保持原样,不得"顺手"归一):孤立 CR、末尾换行、BOM、行尾空格、大小写。
|
|
|
|
|
+func lfNormalize(b []byte) []byte {
|
|
|
|
|
+ return []byte(strings.ReplaceAll(string(b), "\r\n", "\n"))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// sha256Hex 计算内容的 sha256 十六进制摘要(入参应已归一)。
|
|
|
|
|
-//
|
|
|
|
|
-// 入参:b []byte。出参:64 位小写 hex。
|
|
|
|
|
|
|
+// sha256Hex 计算内容 sha256 的 64 位小写 hex 摘要。
|
|
|
func sha256Hex(b []byte) string {
|
|
func sha256Hex(b []byte) string {
|
|
|
sum := sha256.Sum256(b)
|
|
sum := sha256.Sum256(b)
|
|
|
return hex.EncodeToString(sum[:])
|
|
return hex.EncodeToString(sum[:])
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// readDDLHashLock 读取并校验锁文件格式(单行 64 位 hex;多余空白容忍,注释/多行拒绝)。
|
|
|
|
|
|
|
+// ddlHashLock 锁文件解析结果。
|
|
|
|
|
+type ddlHashLock struct {
|
|
|
|
|
+ // Algo 口径标识(必须等于 ddlHashAlgo)。
|
|
|
|
|
+ Algo string
|
|
|
|
|
+ // Digest 期望摘要(64 位小写 hex)。
|
|
|
|
|
+ Digest string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseDDLHashLock 解析锁文件内容(纯函数,便于负例单测覆盖格式校验)。
|
|
|
//
|
|
//
|
|
|
-// 出参:锁文件中的 hex 摘要(小写)。
|
|
|
|
|
-func readDDLHashLock(t *testing.T) string {
|
|
|
|
|
- t.Helper()
|
|
|
|
|
- raw, err := os.ReadFile(ddlHashPath)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.Fatalf("读取 DDL 指纹锁失败(%s): %v", ddlHashPath, err)
|
|
|
|
|
- }
|
|
|
|
|
- lines := make([]string, 0, 2)
|
|
|
|
|
- for _, line := range strings.Split(string(normalizeEOL(raw)), "\n") {
|
|
|
|
|
- if trimmed := strings.TrimSpace(line); trimmed != "" {
|
|
|
|
|
- lines = append(lines, trimmed)
|
|
|
|
|
|
|
+// 入参:raw []byte——锁文件原始字节(容忍 CRLF 检出)。
|
|
|
|
|
+// 出参:
|
|
|
|
|
+// - ddlHashLock:Algo + Digest;
|
|
|
|
|
+// - error:缺 algo 声明 / algo 未识别 / digest 缺失或多行 / 非 64 位小写 hex。
|
|
|
|
|
+//
|
|
|
|
|
+// 格式(§2.6 约束①):
|
|
|
|
|
+//
|
|
|
|
|
+// # algo=sha256-lf-v1 ← 必须存在(注释行与顺序无关,但必须是 # 开头)
|
|
|
|
|
+// # 任意其它说明 ← 可选
|
|
|
|
|
+// <64 位小写 hex> ← 第一个非注释行,且必须唯一
|
|
|
|
|
+func parseDDLHashLock(raw []byte) (ddlHashLock, error) {
|
|
|
|
|
+ var lock ddlHashLock
|
|
|
|
|
+ digestLines := make([]string, 0, 2)
|
|
|
|
|
+ for _, line := range strings.Split(string(lfNormalize(raw)), "\n") {
|
|
|
|
|
+ trimmed := strings.TrimSpace(line)
|
|
|
|
|
+ if trimmed == "" {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if strings.HasPrefix(trimmed, "#") {
|
|
|
|
|
+ // 注释体去空白后再匹配(容忍 `# algo = sha256-lf-v1` 这类手写形态)
|
|
|
|
|
+ body := strings.ReplaceAll(strings.TrimSpace(strings.TrimPrefix(trimmed, "#")), " ", "")
|
|
|
|
|
+ if algo, ok := strings.CutPrefix(body, "algo="); ok {
|
|
|
|
|
+ lock.Algo = algo
|
|
|
|
|
+ }
|
|
|
|
|
+ continue
|
|
|
}
|
|
}
|
|
|
|
|
+ digestLines = append(digestLines, trimmed)
|
|
|
}
|
|
}
|
|
|
- if len(lines) != 1 {
|
|
|
|
|
- t.Fatalf("锁文件 %s 应只含 1 行(64 位小写 hex),实得 %d 行: %v", ddlHashPath, len(lines), lines)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if lock.Algo != ddlHashAlgo {
|
|
|
|
|
+ return lock, fmt.Errorf("锁文件口径声明不符:期望 %q,实得 %q(缺 `%s` 行?)",
|
|
|
|
|
+ ddlHashAlgo, lock.Algo, ddlHashAlgoMarker)
|
|
|
}
|
|
}
|
|
|
- got := lines[0]
|
|
|
|
|
- if len(got) != ddlHashHexLen {
|
|
|
|
|
- t.Fatalf("锁文件 %s 内容长度 = %d,期望 %d(纯 hex,勿加注释/文件名)", ddlHashPath, len(got), ddlHashHexLen)
|
|
|
|
|
|
|
+ if len(digestLines) != 1 {
|
|
|
|
|
+ return lock, fmt.Errorf("锁文件应只含 1 个 digest 行,实得 %d 个: %v", len(digestLines), digestLines)
|
|
|
}
|
|
}
|
|
|
- if strings.ToLower(got) != got {
|
|
|
|
|
- t.Fatalf("锁文件 %s 必须使用小写 hex,实得 %q", ddlHashPath, got)
|
|
|
|
|
|
|
+ digest := digestLines[0]
|
|
|
|
|
+ if len(digest) != ddlHashHexLen {
|
|
|
|
|
+ return lock, fmt.Errorf("digest 长度 = %d,期望 %d(纯 hex,勿带文件名/模式标记)", len(digest), ddlHashHexLen)
|
|
|
}
|
|
}
|
|
|
- for _, r := range got {
|
|
|
|
|
|
|
+ if strings.ToLower(digest) != digest {
|
|
|
|
|
+ return lock, fmt.Errorf("digest 必须小写 hex,实得 %q", digest)
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, r := range digest {
|
|
|
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')) {
|
|
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')) {
|
|
|
- t.Fatalf("锁文件 %s 含非 hex 字符: %q", ddlHashPath, got)
|
|
|
|
|
|
|
+ return lock, fmt.Errorf("digest 含非 hex 字符: %q", digest)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- return got
|
|
|
|
|
|
|
+ lock.Digest = digest
|
|
|
|
|
+ return lock, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// readDDLHashLock 读取真实锁文件(解析失败即 fail-fast)。
|
|
|
|
|
+func readDDLHashLock(t *testing.T) ddlHashLock {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ raw, err := os.ReadFile(ddlHashPath)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("读取 DDL 指纹锁失败(%s): %v", ddlHashPath, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ lock, err := parseDDLHashLock(raw)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("解析 DDL 指纹锁失败(%s): %v", ddlHashPath, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return lock
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TestDDLFixtureHashLock fixture 内容必须与指纹锁一致(DDL 漂移的硬门禁)。
|
|
// TestDDLFixtureHashLock fixture 内容必须与指纹锁一致(DDL 漂移的硬门禁)。
|
|
|
func TestDDLFixtureHashLock(t *testing.T) {
|
|
func TestDDLFixtureHashLock(t *testing.T) {
|
|
|
- want := readDDLHashLock(t)
|
|
|
|
|
|
|
+ lock := readDDLHashLock(t)
|
|
|
|
|
|
|
|
raw, err := os.ReadFile(ddlFixturePath)
|
|
raw, err := os.ReadFile(ddlFixturePath)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
t.Fatalf("读取 DDL fixture 失败(%s): %v", ddlFixturePath, err)
|
|
t.Fatalf("读取 DDL fixture 失败(%s): %v", ddlFixturePath, err)
|
|
|
}
|
|
}
|
|
|
- got := sha256Hex(normalizeEOL(raw))
|
|
|
|
|
- if got != want {
|
|
|
|
|
- t.Fatalf("DDL fixture 指纹漂移:\n 实际 = %s\n 锁定 = %s\n"+
|
|
|
|
|
|
|
+ got := sha256Hex(lfNormalize(raw))
|
|
|
|
|
+ if got != lock.Digest {
|
|
|
|
|
+ t.Fatalf("DDL fixture 指纹漂移:\n 实际 = %s\n 锁定 = %s(algo=%s)\n"+
|
|
|
"若为有意的 DDL 变更,必须**同一提交**内同步三处:文档副本 / fixture / testdata/academic-model-ddl.sha256",
|
|
"若为有意的 DDL 变更,必须**同一提交**内同步三处:文档副本 / fixture / testdata/academic-model-ddl.sha256",
|
|
|
- got, want)
|
|
|
|
|
|
|
+ got, lock.Digest, lock.Algo)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TestDDLFixtureHashMatchesEmbedded embed 进测试二进制的 DDL 与磁盘 fixture 内容一致
|
|
// TestDDLFixtureHashMatchesEmbedded embed 进测试二进制的 DDL 与磁盘 fixture 内容一致
|
|
|
-// (防"改了 fixture 但 embed 走缓存/路径不一致"这类隐蔽漂移)。
|
|
|
|
|
|
|
+// (防"改了 fixture 但 embed 路径/缓存不一致"这类隐蔽漂移)。
|
|
|
func TestDDLFixtureHashMatchesEmbedded(t *testing.T) {
|
|
func TestDDLFixtureHashMatchesEmbedded(t *testing.T) {
|
|
|
raw, err := os.ReadFile(ddlFixturePath)
|
|
raw, err := os.ReadFile(ddlFixturePath)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
t.Fatalf("读取 DDL fixture 失败: %v", err)
|
|
t.Fatalf("读取 DDL fixture 失败: %v", err)
|
|
|
}
|
|
}
|
|
|
- if got, want := sha256Hex(normalizeEOL([]byte(ddlSQL))), sha256Hex(normalizeEOL(raw)); got != want {
|
|
|
|
|
|
|
+ if got, want := sha256Hex(lfNormalize([]byte(ddlSQL))), sha256Hex(lfNormalize(raw)); got != want {
|
|
|
t.Fatalf("go:embed 的 DDL 与磁盘 fixture 不一致:embed=%s file=%s", got, want)
|
|
t.Fatalf("go:embed 的 DDL 与磁盘 fixture 不一致:embed=%s file=%s", got, want)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// TestDDLHashAlgoContract 口径契约(§2.6 约束②,**负例不得删减**):
|
|
|
|
|
+// 归一只放过"换行风格",任何字符级差异必须改变摘要。
|
|
|
|
|
+func TestDDLHashAlgoContract(t *testing.T) {
|
|
|
|
|
+ content := []byte("CREATE TABLE `t_x` (\n `a` INT NOT NULL\n) ENGINE=InnoDB;\n")
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("LF 与 CRLF 同摘要(归一目的)", func(t *testing.T) {
|
|
|
|
|
+ lf := content
|
|
|
|
|
+ crlf := []byte(strings.ReplaceAll(string(content), "\n", "\r\n"))
|
|
|
|
|
+ if got, want := sha256Hex(lfNormalize(crlf)), sha256Hex(lfNormalize(lf)); got != want {
|
|
|
|
|
+ t.Fatalf("CRLF 检出应归一到同一摘要:crlf=%s lf=%s", got, want)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ negative := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ mod []byte
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"末尾追加空格(行尾空白是内容差异)", append([]byte(string(content)+" "), '\n')},
|
|
|
|
|
+ {"末尾追加换行(末尾换行增删是内容差异)", append([]byte(string(content)), '\n')},
|
|
|
|
|
+ {"单字节替换(a→b)", []byte(strings.Replace(string(content), "`a`", "`b`", 1))},
|
|
|
|
|
+ {"单字节删除", []byte(strings.Replace(string(content), "\n", "", 1))},
|
|
|
|
|
+ {"大小写变化(INT→int)", []byte(strings.Replace(string(content), "INT", "int", 1))},
|
|
|
|
|
+ {"BOM 前缀(BOM 不归一)", append([]byte{0xEF, 0xBB, 0xBF}, content...)},
|
|
|
|
|
+ {"孤立 CR 插入(单 CR 不归一)", []byte(strings.Replace(string(content), "\n", "\r", 1))},
|
|
|
|
|
+ }
|
|
|
|
|
+ base := sha256Hex(lfNormalize(content))
|
|
|
|
|
+ for _, tc := range negative {
|
|
|
|
|
+ t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
+ if got := sha256Hex(lfNormalize(tc.mod)); got == base {
|
|
|
|
|
+ t.Fatalf("该修改必须改变摘要(口径只放过 CRLF→LF),实得与基线相同: %s", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ t.Run("孤立 CR 不被归一掉", func(t *testing.T) {
|
|
|
|
|
+ if got, want := string(lfNormalize([]byte("a\rb"))), "a\rb"; got != want {
|
|
|
|
|
+ t.Fatalf("单 CR 不应被归一:期望 %q,实得 %q", want, got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got, want := string(lfNormalize([]byte("a\r\nb"))), "a\nb"; got != want {
|
|
|
|
|
+ t.Fatalf("CRLF 应归一为 LF:期望 %q,实得 %q", want, got)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDDLHashLockParserAccepts 锁文件格式正例(含 CRLF 检出、注释顺序变化、多注释行)。
|
|
|
|
|
+func TestDDLHashLockParserAccepts(t *testing.T) {
|
|
|
|
|
+ const digest = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ raw string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"最简形态", "# algo=sha256-lf-v1\n" + digest + "\n"},
|
|
|
|
|
+ {"CRLF 检出", "# algo=sha256-lf-v1\r\n" + digest + "\r\n"},
|
|
|
|
|
+ {"多注释 + 空行", "# 说明一\n\n# algo=sha256-lf-v1\n# 说明二\n\n" + digest + "\n"},
|
|
|
|
|
+ {"注释在 digest 之后", "# algo=sha256-lf-v1\n" + digest + "\n# 尾注\n"},
|
|
|
|
|
+ {"无末尾换行", "# algo=sha256-lf-v1\n" + digest},
|
|
|
|
|
+ {"algo 行带多余空白", "# algo = sha256-lf-v1 \n" + digest + "\n"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, tc := range cases {
|
|
|
|
|
+ t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
+ lock, err := parseDDLHashLock([]byte(tc.raw))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("应解析成功,实得错误: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if lock.Algo != ddlHashAlgo || lock.Digest != digest {
|
|
|
|
|
+ t.Fatalf("解析结果不符:algo=%q digest=%q", lock.Algo, lock.Digest)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDDLHashLockParserRejects 锁文件格式负例(防"静默变红且原因不明":缺声明必须显式报错)。
|
|
|
|
|
+func TestDDLHashLockParserRejects(t *testing.T) {
|
|
|
|
|
+ const digest = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ raw string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"缺少 algo 声明", digest + "\n"},
|
|
|
|
|
+ {"algo 未识别", "# algo=sha256-raw-v1\n" + digest + "\n"},
|
|
|
|
|
+ {"无 digest", "# algo=sha256-lf-v1\n"},
|
|
|
|
|
+ {"两个 digest 行", "# algo=sha256-lf-v1\n" + digest + "\n" + digest + "\n"},
|
|
|
|
|
+ {"digest 长度不足", "# algo=sha256-lf-v1\n" + digest[:63] + "\n"},
|
|
|
|
|
+ {"digest 大写", "# algo=sha256-lf-v1\n" + strings.ToUpper(digest) + "\n"},
|
|
|
|
|
+ {"digest 含非 hex", "# algo=sha256-lf-v1\n" + digest[:63] + "z\n"},
|
|
|
|
|
+ {"digest 带 sha256sum 模式标记", "# algo=sha256-lf-v1\n" + digest + " testdata/x.sql\n"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, tc := range cases {
|
|
|
|
|
+ t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
+ if _, err := parseDDLHashLock([]byte(tc.raw)); err == nil {
|
|
|
|
|
+ t.Fatal("该锁文件格式应被拒绝,实得 nil error")
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|