feat: upgrade to core v0.8.0-alpha.1, replace banned stdlib imports
Replace fmt, errors, strings, path/filepath with Core primitives across 8 files. Keep strings for SplitSeq/FieldsSeq/Builder/Repeat. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2678f5cde
commit
c3de82b207
10 changed files with 91 additions and 91 deletions
|
|
@ -2,8 +2,9 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
// Supplemental benchmarks beyond the core Set/Get/GetAll/FileBacked benchmarks
|
||||
|
|
@ -14,7 +15,7 @@ func BenchmarkGetAll_VaryingSize(b *testing.B) {
|
|||
sizes := []int{10, 100, 1_000, 10_000}
|
||||
|
||||
for _, size := range sizes {
|
||||
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
|
||||
b.Run(core.Sprintf("size=%d", size), func(b *testing.B) {
|
||||
s, err := New(":memory:")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
|
|
@ -22,7 +23,7 @@ func BenchmarkGetAll_VaryingSize(b *testing.B) {
|
|||
defer s.Close()
|
||||
|
||||
for i := range size {
|
||||
_ = s.Set("bench", fmt.Sprintf("key-%d", i), "value")
|
||||
_ = s.Set("bench", core.Sprintf("key-%d", i), "value")
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
|
|
@ -48,7 +49,7 @@ func BenchmarkSetGet_Parallel(b *testing.B) {
|
|||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
key := core.Sprintf("key-%d", i)
|
||||
_ = s.Set("parallel", key, "value")
|
||||
_, _ = s.Get("parallel", key)
|
||||
i++
|
||||
|
|
@ -64,7 +65,7 @@ func BenchmarkCount_10K(b *testing.B) {
|
|||
defer s.Close()
|
||||
|
||||
for i := range 10_000 {
|
||||
_ = s.Set("bench", fmt.Sprintf("key-%d", i), "value")
|
||||
_ = s.Set("bench", core.Sprintf("key-%d", i), "value")
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
|
|
@ -84,14 +85,14 @@ func BenchmarkDelete(b *testing.B) {
|
|||
|
||||
// Pre-populate keys that will be deleted.
|
||||
for i := range b.N {
|
||||
_ = s.Set("bench", fmt.Sprintf("key-%d", i), "value")
|
||||
_ = s.Set("bench", core.Sprintf("key-%d", i), "value")
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := range b.N {
|
||||
_ = s.Delete("bench", fmt.Sprintf("key-%d", i))
|
||||
_ = s.Delete("bench", core.Sprintf("key-%d", i))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +107,7 @@ func BenchmarkSetWithTTL(b *testing.B) {
|
|||
b.ResetTimer()
|
||||
|
||||
for i := range b.N {
|
||||
_ = s.SetWithTTL("bench", fmt.Sprintf("key-%d", i), "value", 60_000_000_000) // 60s
|
||||
_ = s.SetWithTTL("bench", core.Sprintf("key-%d", i), "value", 60_000_000_000) // 60s
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ func BenchmarkRender(b *testing.B) {
|
|||
defer s.Close()
|
||||
|
||||
for i := range 50 {
|
||||
_ = s.Set("bench", fmt.Sprintf("key%d", i), fmt.Sprintf("val%d", i))
|
||||
_ = s.Set("bench", core.Sprintf("key%d", i), core.Sprintf("val%d", i))
|
||||
}
|
||||
|
||||
tmpl := `{{ .key0 }} {{ .key25 }} {{ .key49 }}`
|
||||
|
|
|
|||
|
|
@ -5,26 +5,25 @@ import (
|
|||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepoConventions_Good_BannedImports(t *testing.T) {
|
||||
files := repoGoFiles(t, func(name string) bool {
|
||||
return strings.HasSuffix(name, ".go")
|
||||
return core.HasSuffix(name, ".go")
|
||||
})
|
||||
|
||||
var banned []string
|
||||
for _, path := range files {
|
||||
file := parseGoFile(t, path)
|
||||
for _, spec := range file.Imports {
|
||||
importPath := strings.Trim(spec.Path.Value, `"`)
|
||||
if strings.HasPrefix(importPath, "forge.lthn.ai/") {
|
||||
importPath := core.TrimPrefix(core.TrimSuffix(spec.Path.Value, `"`), `"`)
|
||||
if core.HasPrefix(importPath, "forge.lthn.ai/") {
|
||||
banned = append(banned, path+": "+importPath)
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +35,7 @@ func TestRepoConventions_Good_BannedImports(t *testing.T) {
|
|||
|
||||
func TestRepoConventions_Good_TestNaming(t *testing.T) {
|
||||
files := repoGoFiles(t, func(name string) bool {
|
||||
return strings.HasSuffix(name, "_test.go")
|
||||
return core.HasSuffix(name, "_test.go")
|
||||
})
|
||||
|
||||
var invalid []string
|
||||
|
|
@ -48,10 +47,10 @@ func TestRepoConventions_Good_TestNaming(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
name := fn.Name.Name
|
||||
if !strings.HasPrefix(name, "Test") || name == "TestMain" {
|
||||
if !core.HasPrefix(name, "Test") || name == "TestMain" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(name, "_Good") || strings.Contains(name, "_Bad") || strings.Contains(name, "_Ugly") {
|
||||
if core.Contains(name, "_Good") || core.Contains(name, "_Bad") || core.Contains(name, "_Ugly") {
|
||||
continue
|
||||
}
|
||||
invalid = append(invalid, path+": "+name)
|
||||
|
|
@ -73,7 +72,7 @@ func repoGoFiles(t *testing.T, keep func(name string) bool) []string {
|
|||
if entry.IsDir() || !keep(entry.Name()) {
|
||||
continue
|
||||
}
|
||||
files = append(files, filepath.Clean(entry.Name()))
|
||||
files = append(files, core.CleanPath(entry.Name(), "/"))
|
||||
}
|
||||
|
||||
slices.Sort(files)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ package store
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -20,7 +19,7 @@ func TestNew_Bad_SchemaConflict(t *testing.T) {
|
|||
// CREATE TABLE IF NOT EXISTS kv, SQLite returns an error because the
|
||||
// name "kv" is already taken by the index.
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "conflict.db")
|
||||
dbPath := core.JoinPath(dir, "conflict.db")
|
||||
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -82,7 +81,7 @@ func TestGetAll_Bad_RowsError(t *testing.T) {
|
|||
// Trigger rows.Err() by corrupting the database file so that iteration
|
||||
// starts successfully but encounters a malformed page mid-scan.
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "corrupt-getall.db")
|
||||
dbPath := core.JoinPath(dir, "corrupt-getall.db")
|
||||
|
||||
s, err := New(dbPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -91,8 +90,8 @@ func TestGetAll_Bad_RowsError(t *testing.T) {
|
|||
const rows = 5000
|
||||
for i := range rows {
|
||||
require.NoError(t, s.Set("g",
|
||||
fmt.Sprintf("key-%06d", i),
|
||||
fmt.Sprintf("value-with-padding-%06d-xxxxxxxxxxxxxxxxxxxxxxxx", i)))
|
||||
core.Sprintf("key-%06d", i),
|
||||
core.Sprintf("value-with-padding-%06d-xxxxxxxxxxxxxxxxxxxxxxxx", i)))
|
||||
}
|
||||
s.Close()
|
||||
|
||||
|
|
@ -176,7 +175,7 @@ func TestRender_Bad_ScanError(t *testing.T) {
|
|||
func TestRender_Bad_RowsError(t *testing.T) {
|
||||
// Same corruption technique as TestGetAll_Bad_RowsError.
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "corrupt-render.db")
|
||||
dbPath := core.JoinPath(dir, "corrupt-render.db")
|
||||
|
||||
s, err := New(dbPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -184,8 +183,8 @@ func TestRender_Bad_RowsError(t *testing.T) {
|
|||
const rows = 5000
|
||||
for i := range rows {
|
||||
require.NoError(t, s.Set("g",
|
||||
fmt.Sprintf("key-%06d", i),
|
||||
fmt.Sprintf("value-with-padding-%06d-xxxxxxxxxxxxxxxxxxxxxxxx", i)))
|
||||
core.Sprintf("key-%06d", i),
|
||||
core.Sprintf("value-with-padding-%06d-xxxxxxxxxxxxxxxxxxxxxxxx", i)))
|
||||
}
|
||||
s.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -248,7 +248,7 @@ func TestWatch_Good_BufferFullDoesNotBlock(t *testing.T) {
|
|||
go func() {
|
||||
defer close(done)
|
||||
for i := range 32 {
|
||||
require.NoError(t, s.Set("g", fmt.Sprintf("k%d", i), "v"))
|
||||
require.NoError(t, s.Set("g", core.Sprintf("k%d", i), "v"))
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
@ -318,7 +318,7 @@ func TestWatch_Good_ConcurrentWatchUnwatch(t *testing.T) {
|
|||
// Writers — continuously mutate the store.
|
||||
wg.Go(func() {
|
||||
for i := range goroutines * ops {
|
||||
_ = s.Set("g", fmt.Sprintf("k%d", i), "v")
|
||||
_ = s.Set("g", core.Sprintf("k%d", i), "v")
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -3,6 +3,7 @@ module dappco.re/go/core/store
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
dappco.re/go/core v0.8.0-alpha.1
|
||||
dappco.re/go/core/log v0.1.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
modernc.org/sqlite v1.47.0
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -1,3 +1,5 @@
|
|||
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
|
|
|
|||
11
scope.go
11
scope.go
|
|
@ -1,12 +1,11 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
|
|
@ -33,7 +32,7 @@ type ScopedStore struct {
|
|||
// characters and hyphens.
|
||||
func NewScoped(store *Store, namespace string) (*ScopedStore, error) {
|
||||
if !validNamespace.MatchString(namespace) {
|
||||
return nil, coreerr.E("store.NewScoped", fmt.Sprintf("namespace %q is invalid (must be non-empty, alphanumeric + hyphens)", namespace), nil)
|
||||
return nil, coreerr.E("store.NewScoped", core.Sprintf("namespace %q is invalid (must be non-empty, alphanumeric + hyphens)", namespace), nil)
|
||||
}
|
||||
return &ScopedStore{store: store, namespace: namespace}, nil
|
||||
}
|
||||
|
|
@ -133,7 +132,7 @@ func (s *ScopedStore) checkQuota(group, key string) error {
|
|||
// Key exists — this is an upsert, no quota check needed.
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
if !core.Is(err, ErrNotFound) {
|
||||
// A database error occurred, not just a "not found" result.
|
||||
return coreerr.E("store.ScopedStore", "quota check", err)
|
||||
}
|
||||
|
|
@ -145,7 +144,7 @@ func (s *ScopedStore) checkQuota(group, key string) error {
|
|||
return coreerr.E("store.ScopedStore", "quota check", err)
|
||||
}
|
||||
if count >= s.quota.MaxKeys {
|
||||
return coreerr.E("store.ScopedStore", fmt.Sprintf("key limit (%d)", s.quota.MaxKeys), ErrQuotaExceeded)
|
||||
return coreerr.E("store.ScopedStore", core.Sprintf("key limit (%d)", s.quota.MaxKeys), ErrQuotaExceeded)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +164,7 @@ func (s *ScopedStore) checkQuota(group, key string) error {
|
|||
count++
|
||||
}
|
||||
if count >= s.quota.MaxGroups {
|
||||
return coreerr.E("store.ScopedStore", fmt.Sprintf("group limit (%d)", s.quota.MaxGroups), ErrQuotaExceeded)
|
||||
return coreerr.E("store.ScopedStore", core.Sprintf("group limit (%d)", s.quota.MaxGroups), ErrQuotaExceeded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -85,7 +85,7 @@ func TestScopedStore_Good_PrefixedInUnderlyingStore(t *testing.T) {
|
|||
|
||||
// Direct access without prefix should fail.
|
||||
_, err = s.Get("config", "key")
|
||||
assert.True(t, errors.Is(err, ErrNotFound))
|
||||
assert.True(t, core.Is(err, ErrNotFound))
|
||||
}
|
||||
|
||||
func TestScopedStore_Good_NamespaceIsolation(t *testing.T) {
|
||||
|
|
@ -116,7 +116,7 @@ func TestScopedStore_Good_Delete(t *testing.T) {
|
|||
require.NoError(t, sc.Delete("g", "k"))
|
||||
|
||||
_, err := sc.Get("g", "k")
|
||||
assert.True(t, errors.Is(err, ErrNotFound))
|
||||
assert.True(t, core.Is(err, ErrNotFound))
|
||||
}
|
||||
|
||||
func TestScopedStore_Good_DeleteGroup(t *testing.T) {
|
||||
|
|
@ -187,7 +187,7 @@ func TestScopedStore_Good_SetWithTTL_Expires(t *testing.T) {
|
|||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
_, err := sc.Get("g", "k")
|
||||
assert.True(t, errors.Is(err, ErrNotFound))
|
||||
assert.True(t, core.Is(err, ErrNotFound))
|
||||
}
|
||||
|
||||
func TestScopedStore_Good_Render(t *testing.T) {
|
||||
|
|
@ -221,7 +221,7 @@ func TestQuota_Good_MaxKeys(t *testing.T) {
|
|||
// 6th key should fail.
|
||||
err = sc.Set("g", "overflow", "v")
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrQuotaExceeded), "expected ErrQuotaExceeded, got: %v", err)
|
||||
assert.True(t, core.Is(err, ErrQuotaExceeded), "expected ErrQuotaExceeded, got: %v", err)
|
||||
}
|
||||
|
||||
func TestQuota_Good_MaxKeys_AcrossGroups(t *testing.T) {
|
||||
|
|
@ -236,7 +236,7 @@ func TestQuota_Good_MaxKeys_AcrossGroups(t *testing.T) {
|
|||
|
||||
// Total is now 3 — any new key should fail regardless of group.
|
||||
err := sc.Set("g4", "d", "4")
|
||||
assert.True(t, errors.Is(err, ErrQuotaExceeded))
|
||||
assert.True(t, core.Is(err, ErrQuotaExceeded))
|
||||
}
|
||||
|
||||
func TestQuota_Good_UpsertDoesNotCount(t *testing.T) {
|
||||
|
|
@ -303,7 +303,7 @@ func TestQuota_Good_ExpiredKeysExcluded(t *testing.T) {
|
|||
|
||||
// Now at 3 — next should fail.
|
||||
err := sc.Set("g", "new3", "v")
|
||||
assert.True(t, errors.Is(err, ErrQuotaExceeded))
|
||||
assert.True(t, core.Is(err, ErrQuotaExceeded))
|
||||
}
|
||||
|
||||
func TestQuota_Good_SetWithTTL_Enforced(t *testing.T) {
|
||||
|
|
@ -316,7 +316,7 @@ func TestQuota_Good_SetWithTTL_Enforced(t *testing.T) {
|
|||
require.NoError(t, sc.SetWithTTL("g", "b", "2", time.Hour))
|
||||
|
||||
err := sc.SetWithTTL("g", "c", "3", time.Hour)
|
||||
assert.True(t, errors.Is(err, ErrQuotaExceeded))
|
||||
assert.True(t, core.Is(err, ErrQuotaExceeded))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -336,7 +336,7 @@ func TestQuota_Good_MaxGroups(t *testing.T) {
|
|||
// 4th group should fail.
|
||||
err := sc.Set("g4", "k", "v")
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrQuotaExceeded))
|
||||
assert.True(t, core.Is(err, ErrQuotaExceeded))
|
||||
}
|
||||
|
||||
func TestQuota_Good_MaxGroups_ExistingGroupOK(t *testing.T) {
|
||||
|
|
@ -405,7 +405,7 @@ func TestQuota_Good_BothLimits(t *testing.T) {
|
|||
|
||||
// Group limit hit.
|
||||
err := sc.Set("g3", "c", "3")
|
||||
assert.True(t, errors.Is(err, ErrQuotaExceeded))
|
||||
assert.True(t, core.Is(err, ErrQuotaExceeded))
|
||||
|
||||
// But adding to existing groups is fine (within key limit).
|
||||
require.NoError(t, sc.Set("g1", "d", "4"))
|
||||
|
|
@ -425,11 +425,11 @@ func TestQuota_Good_DoesNotAffectOtherNamespaces(t *testing.T) {
|
|||
|
||||
// a is at limit — but b's keys don't count against a.
|
||||
err := a.Set("g", "a3", "v")
|
||||
assert.True(t, errors.Is(err, ErrQuotaExceeded))
|
||||
assert.True(t, core.Is(err, ErrQuotaExceeded))
|
||||
|
||||
// b is also at limit independently.
|
||||
err = b.Set("g", "b3", "v")
|
||||
assert.True(t, errors.Is(err, ErrQuotaExceeded))
|
||||
assert.True(t, core.Is(err, ErrQuotaExceeded))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
9
store.go
9
store.go
|
|
@ -9,6 +9,7 @@ import (
|
|||
"text/template"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
|
@ -67,7 +68,7 @@ func New(dbPath string) (*Store, error) {
|
|||
// Ensure the expires_at column exists for databases created before TTL support.
|
||||
if _, err := db.Exec("ALTER TABLE kv ADD COLUMN expires_at INTEGER"); err != nil {
|
||||
// SQLite returns "duplicate column name" if it already exists.
|
||||
if !strings.Contains(err.Error(), "duplicate column name") {
|
||||
if !core.Contains(err.Error(), "duplicate column name") {
|
||||
db.Close()
|
||||
return nil, coreerr.E("store.New", "migration", err)
|
||||
}
|
||||
|
|
@ -347,9 +348,9 @@ func (s *Store) GroupsSeq(prefix string) iter.Seq2[string, error] {
|
|||
|
||||
// escapeLike escapes SQLite LIKE wildcards and the escape character itself.
|
||||
func escapeLike(s string) string {
|
||||
s = strings.ReplaceAll(s, "^", "^^")
|
||||
s = strings.ReplaceAll(s, "%", "^%")
|
||||
s = strings.ReplaceAll(s, "_", "^_")
|
||||
s = core.Replace(s, "^", "^^")
|
||||
s = core.Replace(s, "%", "^%")
|
||||
s = core.Replace(s, "_", "^_")
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@ package store
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -28,7 +26,7 @@ func TestNew_Good_Memory(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNew_Good_FileBacked(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "test.db")
|
||||
dbPath := core.JoinPath(t.TempDir(), "test.db")
|
||||
s, err := New(dbPath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, s)
|
||||
|
|
@ -58,7 +56,7 @@ func TestNew_Bad_InvalidPath(t *testing.T) {
|
|||
func TestNew_Bad_CorruptFile(t *testing.T) {
|
||||
// A file that exists but is not a valid SQLite database should fail.
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "corrupt.db")
|
||||
dbPath := core.JoinPath(dir, "corrupt.db")
|
||||
require.NoError(t, os.WriteFile(dbPath, []byte("not a sqlite database"), 0644))
|
||||
|
||||
_, err := New(dbPath)
|
||||
|
|
@ -69,7 +67,7 @@ func TestNew_Bad_CorruptFile(t *testing.T) {
|
|||
func TestNew_Bad_ReadOnlyDir(t *testing.T) {
|
||||
// A path in a read-only directory should fail when SQLite tries to create the WAL file.
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "readonly.db")
|
||||
dbPath := core.JoinPath(dir, "readonly.db")
|
||||
|
||||
// Create a valid DB first, then make the directory read-only.
|
||||
s, err := New(dbPath)
|
||||
|
|
@ -90,7 +88,7 @@ func TestNew_Bad_ReadOnlyDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNew_Good_WALMode(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "wal.db")
|
||||
dbPath := core.JoinPath(t.TempDir(), "wal.db")
|
||||
s, err := New(dbPath)
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
|
@ -140,7 +138,7 @@ func TestGet_Bad_NotFound(t *testing.T) {
|
|||
|
||||
_, err := s.Get("config", "missing")
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrNotFound), "should wrap ErrNotFound")
|
||||
assert.True(t, core.Is(err, ErrNotFound), "should wrap ErrNotFound")
|
||||
}
|
||||
|
||||
func TestGet_Bad_NonExistentGroup(t *testing.T) {
|
||||
|
|
@ -149,7 +147,7 @@ func TestGet_Bad_NonExistentGroup(t *testing.T) {
|
|||
|
||||
_, err := s.Get("no-such-group", "key")
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrNotFound))
|
||||
assert.True(t, core.Is(err, ErrNotFound))
|
||||
}
|
||||
|
||||
func TestGet_Bad_ClosedStore(t *testing.T) {
|
||||
|
|
@ -233,7 +231,7 @@ func TestCount_Good_BulkInsert(t *testing.T) {
|
|||
|
||||
const total = 500
|
||||
for i := range total {
|
||||
require.NoError(t, s.Set("bulk", fmt.Sprintf("key-%04d", i), "v"))
|
||||
require.NoError(t, s.Set("bulk", core.Sprintf("key-%04d", i), "v"))
|
||||
}
|
||||
n, err := s.Count("bulk")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -521,7 +519,7 @@ func TestStore_Good_GroupIsolation(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestConcurrent_Good_ReadWrite(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "concurrent.db")
|
||||
dbPath := core.JoinPath(t.TempDir(), "concurrent.db")
|
||||
s, err := New(dbPath)
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
|
@ -537,12 +535,12 @@ func TestConcurrent_Good_ReadWrite(t *testing.T) {
|
|||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
group := fmt.Sprintf("grp-%d", id)
|
||||
group := core.Sprintf("grp-%d", id)
|
||||
for i := range opsPerGoroutine {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
val := fmt.Sprintf("val-%d-%d", id, i)
|
||||
key := core.Sprintf("key-%d", i)
|
||||
val := core.Sprintf("val-%d-%d", id, i)
|
||||
if err := s.Set(group, key, val); err != nil {
|
||||
errs <- fmt.Errorf("writer %d: %w", id, err)
|
||||
errs <- core.Wrap(err, "writer", core.Sprintf("%d", id))
|
||||
}
|
||||
}
|
||||
}(g)
|
||||
|
|
@ -553,13 +551,13 @@ func TestConcurrent_Good_ReadWrite(t *testing.T) {
|
|||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
group := fmt.Sprintf("grp-%d", id)
|
||||
group := core.Sprintf("grp-%d", id)
|
||||
for i := range opsPerGoroutine {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
key := core.Sprintf("key-%d", i)
|
||||
_, err := s.Get(group, key)
|
||||
// ErrNotFound is acceptable — the writer may not have written yet.
|
||||
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||
errs <- fmt.Errorf("reader %d: %w", id, err)
|
||||
if err != nil && !core.Is(err, ErrNotFound) {
|
||||
errs <- core.Wrap(err, "reader", core.Sprintf("%d", id))
|
||||
}
|
||||
}
|
||||
}(g)
|
||||
|
|
@ -574,7 +572,7 @@ func TestConcurrent_Good_ReadWrite(t *testing.T) {
|
|||
|
||||
// After all writers finish, every key should be present.
|
||||
for g := range goroutines {
|
||||
group := fmt.Sprintf("grp-%d", g)
|
||||
group := core.Sprintf("grp-%d", g)
|
||||
n, err := s.Count(group)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, opsPerGoroutine, n, "group %s should have all keys", group)
|
||||
|
|
@ -582,13 +580,13 @@ func TestConcurrent_Good_ReadWrite(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConcurrent_Good_GetAll(t *testing.T) {
|
||||
s, err := New(filepath.Join(t.TempDir(), "getall.db"))
|
||||
s, err := New(core.JoinPath(t.TempDir(), "getall.db"))
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
// Seed data.
|
||||
for i := range 50 {
|
||||
require.NoError(t, s.Set("shared", fmt.Sprintf("k%d", i), fmt.Sprintf("v%d", i)))
|
||||
require.NoError(t, s.Set("shared", core.Sprintf("k%d", i), core.Sprintf("v%d", i)))
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
|
@ -608,7 +606,7 @@ func TestConcurrent_Good_GetAll(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConcurrent_Good_DeleteGroup(t *testing.T) {
|
||||
s, err := New(filepath.Join(t.TempDir(), "delgrp.db"))
|
||||
s, err := New(core.JoinPath(t.TempDir(), "delgrp.db"))
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
|
|
@ -617,9 +615,9 @@ func TestConcurrent_Good_DeleteGroup(t *testing.T) {
|
|||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
grp := fmt.Sprintf("g%d", id)
|
||||
grp := core.Sprintf("g%d", id)
|
||||
for i := range 20 {
|
||||
_ = s.Set(grp, fmt.Sprintf("k%d", i), "v")
|
||||
_ = s.Set(grp, core.Sprintf("k%d", i), "v")
|
||||
}
|
||||
_ = s.DeleteGroup(grp)
|
||||
}(g)
|
||||
|
|
@ -637,7 +635,7 @@ func TestErrNotFound_Good_Is(t *testing.T) {
|
|||
|
||||
_, err := s.Get("g", "k")
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrNotFound), "error should be ErrNotFound via errors.Is")
|
||||
assert.True(t, core.Is(err, ErrNotFound), "error should be ErrNotFound via core.Is")
|
||||
assert.Contains(t, err.Error(), "g/k", "error message should include group/key")
|
||||
}
|
||||
|
||||
|
|
@ -651,7 +649,7 @@ func BenchmarkSet(b *testing.B) {
|
|||
|
||||
b.ResetTimer()
|
||||
for i := range b.N {
|
||||
_ = s.Set("bench", fmt.Sprintf("key-%d", i), "value")
|
||||
_ = s.Set("bench", core.Sprintf("key-%d", i), "value")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -662,12 +660,12 @@ func BenchmarkGet(b *testing.B) {
|
|||
// Pre-populate.
|
||||
const keys = 10000
|
||||
for i := range keys {
|
||||
_ = s.Set("bench", fmt.Sprintf("key-%d", i), "value")
|
||||
_ = s.Set("bench", core.Sprintf("key-%d", i), "value")
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := range b.N {
|
||||
_, _ = s.Get("bench", fmt.Sprintf("key-%d", i%keys))
|
||||
_, _ = s.Get("bench", core.Sprintf("key-%d", i%keys))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -677,7 +675,7 @@ func BenchmarkGetAll(b *testing.B) {
|
|||
|
||||
const keys = 10000
|
||||
for i := range keys {
|
||||
_ = s.Set("bench", fmt.Sprintf("key-%d", i), "value")
|
||||
_ = s.Set("bench", core.Sprintf("key-%d", i), "value")
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
|
@ -687,13 +685,13 @@ func BenchmarkGetAll(b *testing.B) {
|
|||
}
|
||||
|
||||
func BenchmarkSet_FileBacked(b *testing.B) {
|
||||
dbPath := filepath.Join(b.TempDir(), "bench.db")
|
||||
dbPath := core.JoinPath(b.TempDir(), "bench.db")
|
||||
s, _ := New(dbPath)
|
||||
defer s.Close()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := range b.N {
|
||||
_ = s.Set("bench", fmt.Sprintf("key-%d", i), "value")
|
||||
_ = s.Set("bench", core.Sprintf("key-%d", i), "value")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -741,7 +739,7 @@ func TestSetWithTTL_Good_ExpiresOnGet(t *testing.T) {
|
|||
|
||||
_, err := s.Get("g", "ephemeral")
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrNotFound), "expired key should be ErrNotFound")
|
||||
assert.True(t, core.Is(err, ErrNotFound), "expired key should be ErrNotFound")
|
||||
}
|
||||
|
||||
func TestSetWithTTL_Good_ExcludedFromCount(t *testing.T) {
|
||||
|
|
@ -901,7 +899,7 @@ func TestPurgeExpired_Good_BackgroundPurge(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSchemaUpgrade_Good_ExistingDB(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "upgrade.db")
|
||||
dbPath := core.JoinPath(t.TempDir(), "upgrade.db")
|
||||
|
||||
// Open, write, close.
|
||||
s1, err := New(dbPath)
|
||||
|
|
@ -927,7 +925,7 @@ func TestSchemaUpgrade_Good_ExistingDB(t *testing.T) {
|
|||
|
||||
func TestSchemaUpgrade_Good_PreTTLDatabase(t *testing.T) {
|
||||
// Simulate a database created before TTL support (no expires_at column).
|
||||
dbPath := filepath.Join(t.TempDir(), "pre-ttl.db")
|
||||
dbPath := core.JoinPath(t.TempDir(), "pre-ttl.db")
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
require.NoError(t, err)
|
||||
db.SetMaxOpenConns(1)
|
||||
|
|
@ -966,7 +964,7 @@ func TestSchemaUpgrade_Good_PreTTLDatabase(t *testing.T) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestConcurrent_Good_TTL(t *testing.T) {
|
||||
s, err := New(filepath.Join(t.TempDir(), "concurrent-ttl.db"))
|
||||
s, err := New(core.JoinPath(t.TempDir(), "concurrent-ttl.db"))
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
|
|
@ -978,9 +976,9 @@ func TestConcurrent_Good_TTL(t *testing.T) {
|
|||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
grp := fmt.Sprintf("ttl-%d", id)
|
||||
grp := core.Sprintf("ttl-%d", id)
|
||||
for i := range ops {
|
||||
key := fmt.Sprintf("k%d", i)
|
||||
key := core.Sprintf("k%d", i)
|
||||
if i%2 == 0 {
|
||||
_ = s.SetWithTTL(grp, key, "v", 50*time.Millisecond)
|
||||
} else {
|
||||
|
|
@ -995,7 +993,7 @@ func TestConcurrent_Good_TTL(t *testing.T) {
|
|||
time.Sleep(60 * time.Millisecond)
|
||||
|
||||
for g := range goroutines {
|
||||
grp := fmt.Sprintf("ttl-%d", g)
|
||||
grp := core.Sprintf("ttl-%d", g)
|
||||
n, err := s.Count(grp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ops/2, n, "only non-TTL keys should remain in %s", grp)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue