CI 测试 / go vet + go test (push) Successful in 49s
- 全项目版本号统一:v3.x 迭代号(26.9/26.9/26.9/26.9 及裸 v2/v3)→ 26.9, 覆盖 Go 注释 / 文档 / openapi.yaml / README×4 / 前端源码(80+ 处) - v31_test.go 更名 custom_code_test.go;TestV2AccessorDefaults → TestKVAccessorDefaults - docs/api/00-overview.md 更新日志合并为单条 26.9 条目(修复错位拼接) - .goreleaser.yaml 头部注释与实际一致(Pro 2.18.1 / GITEA_TOKEN / semver tag 要求) - CI:release-image.yml → ci.yml,仅保留 vet+test 门禁; 镜像发布移交 GoReleaser Pro(原 build-push 的 tag 校验与 26.9 版本方案冲突,历史 9 次失败) - 前端重建:server/web/dist 与 web-embed 同步(docs 文案嵌入更新)
198 lines
6.1 KiB
Go
198 lines
6.1 KiB
Go
package storage
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"io"
|
||
"strings"
|
||
"sync/atomic"
|
||
"testing"
|
||
)
|
||
|
||
// fakeEngine 可配置健康检查结果的桩引擎。
|
||
type fakeEngine struct{ failHealth bool }
|
||
|
||
func (f *fakeEngine) SaveFile(ctx context.Context, r io.Reader, savePath string) (int64, error) {
|
||
return 0, nil
|
||
}
|
||
func (f *fakeEngine) DeleteFile(ctx context.Context, savePath string) error { return nil }
|
||
func (f *fakeEngine) Open(ctx context.Context, savePath string, rng *Range) (*Download, error) {
|
||
return nil, ErrNotFound
|
||
}
|
||
func (f *fakeEngine) Stat(ctx context.Context, savePath string) (*FileMeta, error) {
|
||
return nil, ErrNotFound
|
||
}
|
||
func (f *fakeEngine) SaveChunk(ctx context.Context, uploadID string, chunkIndex int, r io.Reader, savePath string) (int64, error) {
|
||
return 0, nil
|
||
}
|
||
func (f *fakeEngine) MergeChunks(ctx context.Context, uploadID string, total int, verifyHash func(index int) (string, error), savePath string) (int64, string, error) {
|
||
return 0, "", nil
|
||
}
|
||
func (f *fakeEngine) CleanChunks(ctx context.Context, uploadID string, savePath string) error {
|
||
return nil
|
||
}
|
||
func (f *fakeEngine) FileExists(ctx context.Context, savePath string) (bool, error) {
|
||
return false, nil
|
||
}
|
||
func (f *fakeEngine) HeadMeta(ctx context.Context, savePath string, headBytes int64) (*FileMeta, []byte, error) {
|
||
return nil, nil, ErrNotSupported
|
||
}
|
||
func (f *fakeEngine) PresignGetURL(ctx context.Context, savePath string, expires int64) (string, error) {
|
||
return "", ErrNotSupported
|
||
}
|
||
func (f *fakeEngine) PresignPutURL(ctx context.Context, savePath string, expires int64) (string, error) {
|
||
return "", ErrNotSupported
|
||
}
|
||
func (f *fakeEngine) HealthCheck(ctx context.Context) error {
|
||
if f.failHealth {
|
||
return ErrUnavailable
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// newTestManager 构造测试用 Manager:local 健康引擎起步;s3/webdav 由计数器控制健康。
|
||
func newTestManager(s3Fail *atomic.Bool) *Manager {
|
||
build := func(name string) (Storage, error) {
|
||
switch name {
|
||
case "local":
|
||
return &fakeEngine{}, nil
|
||
case "s3":
|
||
return &fakeEngine{failHealth: s3Fail.Load()}, nil
|
||
case "webdav":
|
||
return &fakeEngine{}, nil
|
||
}
|
||
return nil, errors.New("unknown")
|
||
}
|
||
return NewManager("local", &fakeEngine{}, build)
|
||
}
|
||
|
||
// TestSwitchSuccessAndCurrentName 切换成功后当前引擎名与实例更新。
|
||
func TestSwitchSuccessAndCurrentName(t *testing.T) {
|
||
var s3Fail atomic.Bool
|
||
m := newTestManager(&s3Fail)
|
||
if m.CurrentName() != "local" {
|
||
t.Fatalf("初始引擎应为 local,得到 %s", m.CurrentName())
|
||
}
|
||
if _, err := m.Switch("s3"); err != nil {
|
||
t.Fatalf("Switch(s3) 失败: %v", err)
|
||
}
|
||
if m.CurrentName() != "s3" {
|
||
t.Fatalf("切换后引擎应为 s3,得到 %s", m.CurrentName())
|
||
}
|
||
if _, err := m.Switch("webdav"); err != nil {
|
||
t.Fatalf("Switch(webdav) 失败: %v", err)
|
||
}
|
||
if m.CurrentName() != "webdav" {
|
||
t.Fatalf("切换后引擎应为 webdav,得到 %s", m.CurrentName())
|
||
}
|
||
}
|
||
|
||
// TestSwitchFailureKeepsCurrent 健康检查失败时保持原引擎(26.9 核心语义)。
|
||
func TestSwitchFailureKeepsCurrent(t *testing.T) {
|
||
var s3Fail atomic.Bool
|
||
s3Fail.Store(true) // s3 不健康
|
||
m := newTestManager(&s3Fail)
|
||
if _, err := m.Switch("s3"); err == nil {
|
||
t.Fatal("s3 不健康时 Switch 应失败")
|
||
}
|
||
if m.CurrentName() != "local" {
|
||
t.Fatalf("切换失败后应保持 local,得到 %s", m.CurrentName())
|
||
}
|
||
// 恢复健康后可切换成功
|
||
s3Fail.Store(false)
|
||
if _, err := m.Switch("s3"); err != nil {
|
||
t.Fatalf("恢复健康后 Switch(s3) 应成功: %v", err)
|
||
}
|
||
if m.CurrentName() != "s3" {
|
||
t.Fatalf("恢复后引擎应为 s3,得到 %s", m.CurrentName())
|
||
}
|
||
}
|
||
|
||
// TestSwitchInvalidName 非法引擎名拒绝。
|
||
func TestSwitchInvalidName(t *testing.T) {
|
||
m := newTestManager(&atomic.Bool{})
|
||
if _, err := m.Switch("ftp"); err == nil || !strings.Contains(err.Error(), "未知存储引擎") {
|
||
t.Fatalf("非法引擎名应报未知存储引擎,得到 %v", err)
|
||
}
|
||
if !ValidEngine("local") || ValidEngine("ftp") {
|
||
t.Fatal("ValidEngine 判定错误")
|
||
}
|
||
}
|
||
|
||
// TestEngineOfCacheAndInvalidate EngineOf 缓存命中 + Invalidate 后重建(参数生效路径)。
|
||
func TestEngineOfCacheAndInvalidate(t *testing.T) {
|
||
var builds atomic.Int64
|
||
build := func(name string) (Storage, error) {
|
||
builds.Add(1)
|
||
return &fakeEngine{failHealth: false}, nil
|
||
}
|
||
m := NewManager("local", &fakeEngine{}, build)
|
||
|
||
s1, err := m.EngineOf("s3")
|
||
if err != nil {
|
||
t.Fatalf("EngineOf(s3): %v", err)
|
||
}
|
||
s2, err := m.EngineOf("s3")
|
||
if err != nil {
|
||
t.Fatalf("EngineOf(s3) second: %v", err)
|
||
}
|
||
if s1 != s2 {
|
||
t.Fatal("EngineOf 应命中缓存返回同一实例")
|
||
}
|
||
if n := builds.Load(); n != 1 {
|
||
t.Fatalf("应只构建 1 次,实际 %d", n)
|
||
}
|
||
|
||
// Invalidate 后下次取重建新实例
|
||
m.Invalidate("s3")
|
||
s3, err := m.EngineOf("s3")
|
||
if err != nil {
|
||
t.Fatalf("EngineOf(s3) after invalidate: %v", err)
|
||
}
|
||
if s3 == s1 {
|
||
t.Fatal("Invalidate 后应返回重建的新实例")
|
||
}
|
||
if n := builds.Load(); n != 2 {
|
||
t.Fatalf("Invalidate 后应再构建 1 次,实际累计 %d", n)
|
||
}
|
||
}
|
||
|
||
// TestInvalidateCurrentNoop Invalidate 当前引擎不生效(热路径实例保持)。
|
||
func TestInvalidateCurrentNoop(t *testing.T) {
|
||
m := newTestManager(&atomic.Bool{})
|
||
cur := m.Current()
|
||
m.Invalidate("local") // 当前引擎:应为 no-op
|
||
if m.Current() != cur {
|
||
t.Fatal("Invalidate 当前引擎不应替换实例")
|
||
}
|
||
}
|
||
|
||
// TestSwitchSameNameNoop 同名 Switch 幂等。
|
||
func TestSwitchSameNameNoop(t *testing.T) {
|
||
m := newTestManager(&atomic.Bool{})
|
||
s, err := m.Switch("local")
|
||
if err != nil {
|
||
t.Fatalf("Switch(local) 同名应成功: %v", err)
|
||
}
|
||
if s != m.Current() {
|
||
t.Fatal("同名 Switch 应返回当前实例")
|
||
}
|
||
}
|
||
|
||
// TestDelegateToCurrent 保存/读取类操作委托当前引擎(切换后指向新引擎)。
|
||
func TestDelegateToCurrent(t *testing.T) {
|
||
var s3Fail atomic.Bool
|
||
m := newTestManager(&s3Fail)
|
||
ctx := context.Background()
|
||
// local 引擎 HealthCheck 健康
|
||
if err := m.HealthCheck(ctx); err != nil {
|
||
t.Fatalf("委托 HealthCheck(local): %v", err)
|
||
}
|
||
if _, err := m.Switch("s3"); err != nil {
|
||
t.Fatalf("Switch(s3): %v", err)
|
||
}
|
||
if err := m.HealthCheck(ctx); err != nil {
|
||
t.Fatalf("委托 HealthCheck(s3): %v", err)
|
||
}
|
||
}
|