First Commit
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package traffic
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// openLegacyIndex builds the index exactly as the pre-reclamation Open did: a
|
||||
// plain-path DSN, pragmas via the pool, and auto_vacuum left at its default 0.
|
||||
func openLegacyIndex(t *testing.T, dir string) *sql.DB {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "_index"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old, err := sql.Open("sqlite", filepath.Join(dir, "_index", "index.sqlite"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range []string{"PRAGMA journal_mode=WAL", "PRAGMA busy_timeout=5000"} {
|
||||
if _, err := old.Exec(p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := old.Exec(indexSchema); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return old
|
||||
}
|
||||
|
||||
// TestUpgradeFromOldInstall guards the upgrade path. Open now names the database
|
||||
// through a file: URI so per-connection pragmas can ride in the DSN, and a
|
||||
// driver that did not treat that as a URI would quietly open a file literally
|
||||
// named "file:/…" — an empty index, with every recorded exchange apparently
|
||||
// gone. The assertions below are what prove that does not happen.
|
||||
func TestUpgradeFromOldInstall(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "_index", "index.sqlite")
|
||||
old := openLegacyIndex(t, dir)
|
||||
if _, err := old.Exec(ftsSchema); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 三条历史流量,含一条 legacy path<>'' 的行
|
||||
for i, row := range [][]any{
|
||||
{"1700000000-0001", "old.example.com", ""},
|
||||
{"1700000000-0002", "old.example.com", ""},
|
||||
{"1700000000-0003", "legacy.example.com", "legacy.example.com/GET/x"},
|
||||
} {
|
||||
if _, err := old.Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
|
||||
VALUES(?,?,?,'GET','/x','http://x/x',200,'text/html',0,9,?)`, row[0], 1700000000+i, row[1], row[2]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := old.Exec(`INSERT INTO exchange_bodies(id,req_head,req_body,resp_head,resp_body)
|
||||
VALUES(?,'GET /x','','HTTP 200','老数据正文')`, row[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := old.Exec(`INSERT INTO ex_fts(rowid,content) VALUES(?,?)`, i+1, "老数据正文 secret-token"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := old.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// ---- 新版本接管
|
||||
tr, err := Open(dir, "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("新版本无法打开旧库: %v", err)
|
||||
}
|
||||
defer tr.Close()
|
||||
|
||||
// 1. 必须是同一个文件,不能悄悄开了个新空库
|
||||
if st2, err := os.Stat(path); err != nil || st2.Size() == 0 {
|
||||
t.Fatalf("原索引文件异常: size=%v err=%v", st2, err)
|
||||
}
|
||||
if entries, _ := os.ReadDir(filepath.Join(dir, "_index")); len(entries) > 3 {
|
||||
for _, e := range entries {
|
||||
t.Logf("_index 下: %s", e.Name())
|
||||
}
|
||||
t.Fatal("_index 下出现了预期外的文件,DSN 可能指向了别的库")
|
||||
}
|
||||
t.Logf("旧库 %d 字节,新版本接管后仍是同一文件", stat.Size())
|
||||
|
||||
// 2. 历史数据全部可见
|
||||
n, err := tr.Count()
|
||||
if err != nil || n != 3 {
|
||||
t.Fatalf("Count=(%d,%v),应为 (3,nil) —— 历史流量丢失", n, err)
|
||||
}
|
||||
// 3. 历史全文索引仍可搜
|
||||
if tr.fts {
|
||||
rows, err := tr.query("old.example.com", "", "secret-token", 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("历史全文搜索失败: %v", err)
|
||||
}
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("历史全文搜索命中 %d 条,应为 2", len(rows))
|
||||
}
|
||||
}
|
||||
// 4. 历史正文仍可读
|
||||
if _, resp, err := tr.Get("1700000000-0001"); err != nil {
|
||||
t.Fatalf("读取历史正文失败: %v", err)
|
||||
} else if resp == "" {
|
||||
t.Fatal("历史响应为空")
|
||||
}
|
||||
// 5. 旧库不会被误判为已启用增量回收
|
||||
if tr.incrementalVacuum {
|
||||
t.Fatal("旧库被误判为已启用增量回收")
|
||||
}
|
||||
// 6. 删除仍然正常工作,且回收流程在旧库上能收敛
|
||||
deleted, err := tr.DeleteHostsExact([]string{"old.example.com"})
|
||||
if err != nil || deleted != 2 {
|
||||
t.Fatalf("DeleteHostsExact=(%d,%v),应为 (2,nil)", deleted, err)
|
||||
}
|
||||
tr.reaping.Wait()
|
||||
if n, err := tr.Count(); err != nil || n != 1 {
|
||||
t.Fatalf("删除后 Count=(%d,%v),应为 (1,nil)", n, err)
|
||||
}
|
||||
// 7. legacy path<>'' 的行没被牵连
|
||||
var legacyPath string
|
||||
if err := tr.DB().QueryRow(`SELECT path FROM exchanges`).Scan(&legacyPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if legacyPath == "" {
|
||||
t.Fatal("legacy 行的 path 被清空了")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDowngradeToOldBinary covers a rollback: a database created with
|
||||
// auto_vacuum=incremental must stay readable and writable by a build that knows
|
||||
// nothing about it. auto_vacuum only changes where SQLite tracks free pages, so
|
||||
// the old binary simply goes back to never returning them.
|
||||
func TestDowngradeToOldBinary(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
tr, err := Open(dir, "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !tr.incrementalVacuum {
|
||||
t.Fatal("新库应启用增量回收")
|
||||
}
|
||||
bulkRecord(tr, "keep.example.com", 5, 100*1024)
|
||||
if err := tr.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
old := openLegacyIndex(t, dir) // 旧版本二进制接管
|
||||
defer old.Close()
|
||||
var n int
|
||||
if err := old.QueryRow(`SELECT COUNT(*) FROM exchanges`).Scan(&n); err != nil || n != 5 {
|
||||
t.Fatalf("旧版本读到 (%d,%v),应为 (5,nil)", n, err)
|
||||
}
|
||||
if _, err := old.Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path)
|
||||
VALUES('x',1,'new.example.com','GET','/x','http://x/x',200,'',0,0,'')`); err != nil {
|
||||
t.Fatalf("旧版本写入失败: %v", err)
|
||||
}
|
||||
if _, err := old.Exec(`DELETE FROM exchanges WHERE host='keep.example.com'`); err != nil {
|
||||
t.Fatalf("旧版本删除失败: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user