feat: 添加本地缓存功能,减少API调用
- 实现SQLite缓存模块,支持高效查询和存储 - 添加缓存键生成策略(基于原文+语言对的SHA256哈希) - 集成缓存到Translator类,先查缓存再调用API - 添加缓存管理命令:cache clear, cache stats, cache cleanup - 实现组合缓存清理策略(数量限制+时间过期) - 添加完整的单元测试 - 更新配置文件模板,添加缓存配置 - 更新文档和版本记录 版本: v0.5.1
This commit is contained in:
325
internal/cache/cache_test.go
vendored
Normal file
325
internal/cache/cache_test.go
vendored
Normal file
@@ -0,0 +1,325 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateCacheKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
fromLang string
|
||||
toLang string
|
||||
wantSame bool
|
||||
}{
|
||||
{
|
||||
name: "相同输入应生成相同键",
|
||||
text: "Hello world",
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
wantSame: true,
|
||||
},
|
||||
{
|
||||
name: "不同文本应生成不同键",
|
||||
text: "Hello universe",
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
wantSame: false,
|
||||
},
|
||||
{
|
||||
name: "不同语言对应生成不同键",
|
||||
text: "Hello world",
|
||||
fromLang: "en",
|
||||
toLang: "zh-TW",
|
||||
wantSame: false,
|
||||
},
|
||||
{
|
||||
name: "大小写不敏感的语言代码",
|
||||
text: "Hello world",
|
||||
fromLang: "EN",
|
||||
toLang: "zh-cn",
|
||||
wantSame: true,
|
||||
},
|
||||
{
|
||||
name: "多余空白字符应规范化",
|
||||
text: " Hello world ",
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
wantSame: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
key1 := GenerateCacheKey(tt.text, tt.fromLang, tt.toLang)
|
||||
key2 := GenerateCacheKey(tt.text, tt.fromLang, tt.toLang)
|
||||
|
||||
if key1 != key2 {
|
||||
t.Errorf("相同输入生成了不同的键: %s != %s", key1, key2)
|
||||
}
|
||||
|
||||
// 检查键的长度(SHA256哈希应为64个字符)
|
||||
if len(key1) != 64 {
|
||||
t.Errorf("缓存键长度不正确: got %d, want 64", len(key1))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteCache(t *testing.T) {
|
||||
// 创建临时目录
|
||||
tmpDir, err := os.MkdirTemp("", "cache_test")
|
||||
if err != nil {
|
||||
t.Fatalf("创建临时目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test_cache.db")
|
||||
|
||||
// 创建缓存配置
|
||||
config := &CacheConfig{
|
||||
Enabled: true,
|
||||
MaxRecords: 100,
|
||||
ExpireDays: 1,
|
||||
DBPath: dbPath,
|
||||
}
|
||||
|
||||
// 创建缓存实例
|
||||
cache, err := NewSQLiteCache(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建缓存实例失败: %v", err)
|
||||
}
|
||||
defer cache.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 测试设置缓存
|
||||
entry := &CacheEntry{
|
||||
CacheKey: "test_key_1",
|
||||
OriginalText: "Hello world",
|
||||
TranslatedText: "你好世界",
|
||||
FromLang: "en",
|
||||
ToLang: "zh-CN",
|
||||
Model: "gpt-3.5-turbo",
|
||||
PromptName: "simple",
|
||||
PromptContent: "请用简单易懂的语言翻译以下内容。",
|
||||
PromptTokens: 10,
|
||||
CompletionTokens: 5,
|
||||
TotalTokens: 15,
|
||||
}
|
||||
|
||||
err = cache.Set(ctx, entry)
|
||||
if err != nil {
|
||||
t.Fatalf("设置缓存失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试获取缓存
|
||||
cachedEntry, err := cache.Get(ctx, "test_key_1")
|
||||
if err != nil {
|
||||
t.Fatalf("获取缓存失败: %v", err)
|
||||
}
|
||||
if cachedEntry == nil {
|
||||
t.Fatal("缓存未命中")
|
||||
}
|
||||
|
||||
if cachedEntry.TranslatedText != "你好世界" {
|
||||
t.Errorf("缓存翻译结果不正确: got %s, want 你好世界", cachedEntry.TranslatedText)
|
||||
}
|
||||
|
||||
// 测试缓存未命中
|
||||
missingEntry, err := cache.Get(ctx, "non_existent_key")
|
||||
if err != nil {
|
||||
t.Fatalf("查询不存在的缓存失败: %v", err)
|
||||
}
|
||||
if missingEntry != nil {
|
||||
t.Error("不存在的缓存应该返回nil")
|
||||
}
|
||||
|
||||
// 测试统计信息
|
||||
stats, err := cache.Stats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取统计信息失败: %v", err)
|
||||
}
|
||||
if stats.TotalRecords != 1 {
|
||||
t.Errorf("统计记录数不正确: got %d, want 1", stats.TotalRecords)
|
||||
}
|
||||
|
||||
// 测试删除缓存
|
||||
err = cache.Delete(ctx, "test_key_1")
|
||||
if err != nil {
|
||||
t.Fatalf("删除缓存失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证删除
|
||||
deletedEntry, err := cache.Get(ctx, "test_key_1")
|
||||
if err != nil {
|
||||
t.Fatalf("查询已删除的缓存失败: %v", err)
|
||||
}
|
||||
if deletedEntry != nil {
|
||||
t.Error("已删除的缓存应该返回nil")
|
||||
}
|
||||
|
||||
// 测试清空缓存
|
||||
err = cache.Set(ctx, entry)
|
||||
if err != nil {
|
||||
t.Fatalf("设置缓存失败: %v", err)
|
||||
}
|
||||
|
||||
err = cache.Clear(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("清空缓存失败: %v", err)
|
||||
}
|
||||
|
||||
stats, err = cache.Stats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取统计信息失败: %v", err)
|
||||
}
|
||||
if stats.TotalRecords != 0 {
|
||||
t.Errorf("清空后记录数不正确: got %d, want 0", stats.TotalRecords)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheExpiration(t *testing.T) {
|
||||
// 创建临时目录
|
||||
tmpDir, err := os.MkdirTemp("", "cache_test")
|
||||
if err != nil {
|
||||
t.Fatalf("创建临时目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test_cache.db")
|
||||
|
||||
// 创建缓存配置,设置很短的过期时间
|
||||
config := &CacheConfig{
|
||||
Enabled: true,
|
||||
MaxRecords: 100,
|
||||
ExpireDays: 0, // 0天表示立即过期
|
||||
DBPath: dbPath,
|
||||
}
|
||||
|
||||
// 创建缓存实例
|
||||
cache, err := NewSQLiteCache(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建缓存实例失败: %v", err)
|
||||
}
|
||||
defer cache.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 设置缓存
|
||||
entry := &CacheEntry{
|
||||
CacheKey: "test_key_1",
|
||||
OriginalText: "Hello world",
|
||||
TranslatedText: "你好世界",
|
||||
FromLang: "en",
|
||||
ToLang: "zh-CN",
|
||||
Model: "gpt-3.5-turbo",
|
||||
PromptTokens: 10,
|
||||
CompletionTokens: 5,
|
||||
TotalTokens: 15,
|
||||
}
|
||||
|
||||
err = cache.Set(ctx, entry)
|
||||
if err != nil {
|
||||
t.Fatalf("设置缓存失败: %v", err)
|
||||
}
|
||||
|
||||
// 立即清理(应该删除所有记录,因为过期时间为0)
|
||||
err = cache.Cleanup(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("清理缓存失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查统计信息
|
||||
stats, err := cache.Stats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取统计信息失败: %v", err)
|
||||
}
|
||||
if stats.TotalRecords != 0 {
|
||||
t.Errorf("清理后记录数不正确: got %d, want 0", stats.TotalRecords)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeText(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "正常文本",
|
||||
input: "Hello world",
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
name: "多余空白字符",
|
||||
input: " Hello world ",
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
name: "制表符和换行符",
|
||||
input: "Hello\tworld\n",
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
name: "空字符串",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := normalizeText(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("normalizeText(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLanguageCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "正常语言代码",
|
||||
input: "zh-CN",
|
||||
expected: "zh-cn",
|
||||
},
|
||||
{
|
||||
name: "大写语言代码",
|
||||
input: "EN-US",
|
||||
expected: "en-us",
|
||||
},
|
||||
{
|
||||
name: "空字符串",
|
||||
input: "",
|
||||
expected: "auto",
|
||||
},
|
||||
{
|
||||
name: "auto",
|
||||
input: "auto",
|
||||
expected: "auto",
|
||||
},
|
||||
{
|
||||
name: "前后空白",
|
||||
input: " zh-CN ",
|
||||
expected: "zh-cn",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := normalizeLanguageCode(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("normalizeLanguageCode(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user