| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- // DDL 指纹锁单测(AC-17 验收⑥的前置门禁)——设计 v0.1.2 §2.4「L2 DDL 哈希校验」。
- //
- // 目的:目标态 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 的内容
- //
- // 配合 AC-17 的人工/报告流程:对「文档副本」与「fixture」各跑一次 sha256 并与锁文件比对,
- // 三者一致才视为"文档与代码未漂移",结果写进一致性报告留证。
- //
- // 纪律(任一 DDL 变更必须**同一提交**内更新三处):
- // 1. 文档 `unified/03-migration/academic-model-ddl.sql`;
- // 2. 本仓 fixture `testdata/academic-model-ddl.sql`;
- // 3. 本仓锁文件 `testdata/academic-model-ddl.sha256`(单行 64 位小写 hex)。
- //
- // 为什么按「LF 归一」取指纹(不是裸字节):
- //
- // 开发者机器常见 `core.autocrlf=true`(Windows 检出为 CRLF),同一内容在 Linux CI 会得到
- // 不同 sha256 → 裸字节指纹必然产生"跨平台假红"。归一掉换行风格后,指纹只对**内容**敏感:
- // 任何字符级修改都会改变它,而"纯换行风格"这种无意义差异不会误报。
- // (实测:Windows 工作区与 git blob 均为 LF,两者指纹一致 = 7a25c918…75935。)
- package model
- import (
- "crypto/sha256"
- "encoding/hex"
- "os"
- "strings"
- "testing"
- )
- const (
- // ddlFixturePath DDL fixture 相对路径(go test 以包目录为工作目录)。
- ddlFixturePath = "testdata/academic-model-ddl.sql"
- // ddlHashPath DDL 指纹锁文件路径(单行 64 位小写 hex,可被 awk '{print $1}' 直接取用)。
- ddlHashPath = "testdata/academic-model-ddl.sha256"
- // ddlHashHexLen sha256 十六进制串长度。
- ddlHashHexLen = 64
- )
- // normalizeEOL 把 CRLF / 孤立 CR 统一为 LF(跨平台指纹稳定的唯一前提)。
- //
- // 入参: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)
- }
- // sha256Hex 计算内容的 sha256 十六进制摘要(入参应已归一)。
- //
- // 入参:b []byte。出参:64 位小写 hex。
- func sha256Hex(b []byte) string {
- sum := sha256.Sum256(b)
- return hex.EncodeToString(sum[:])
- }
- // readDDLHashLock 读取并校验锁文件格式(单行 64 位 hex;多余空白容忍,注释/多行拒绝)。
- //
- // 出参:锁文件中的 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)
- }
- }
- if len(lines) != 1 {
- t.Fatalf("锁文件 %s 应只含 1 行(64 位小写 hex),实得 %d 行: %v", ddlHashPath, len(lines), lines)
- }
- got := lines[0]
- if len(got) != ddlHashHexLen {
- t.Fatalf("锁文件 %s 内容长度 = %d,期望 %d(纯 hex,勿加注释/文件名)", ddlHashPath, len(got), ddlHashHexLen)
- }
- if strings.ToLower(got) != got {
- t.Fatalf("锁文件 %s 必须使用小写 hex,实得 %q", ddlHashPath, got)
- }
- for _, r := range got {
- if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')) {
- t.Fatalf("锁文件 %s 含非 hex 字符: %q", ddlHashPath, got)
- }
- }
- return got
- }
- // TestDDLFixtureHashLock fixture 内容必须与指纹锁一致(DDL 漂移的硬门禁)。
- func TestDDLFixtureHashLock(t *testing.T) {
- want := readDDLHashLock(t)
- raw, err := os.ReadFile(ddlFixturePath)
- if err != nil {
- t.Fatalf("读取 DDL fixture 失败(%s): %v", ddlFixturePath, err)
- }
- got := sha256Hex(normalizeEOL(raw))
- if got != want {
- t.Fatalf("DDL fixture 指纹漂移:\n 实际 = %s\n 锁定 = %s\n"+
- "若为有意的 DDL 变更,必须**同一提交**内同步三处:文档副本 / fixture / testdata/academic-model-ddl.sha256",
- got, want)
- }
- }
- // TestDDLFixtureHashMatchesEmbedded embed 进测试二进制的 DDL 与磁盘 fixture 内容一致
- // (防"改了 fixture 但 embed 走缓存/路径不一致"这类隐蔽漂移)。
- func TestDDLFixtureHashMatchesEmbedded(t *testing.T) {
- raw, err := os.ReadFile(ddlFixturePath)
- if err != nil {
- t.Fatalf("读取 DDL fixture 失败: %v", err)
- }
- if got, want := sha256Hex(normalizeEOL([]byte(ddlSQL))), sha256Hex(normalizeEOL(raw)); got != want {
- t.Fatalf("go:embed 的 DDL 与磁盘 fixture 不一致:embed=%s file=%s", got, want)
- }
- }
|