mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-06-05 22:43:56 +00:00
Initial commit.
This commit is contained in:
Vendored
+217
@@ -0,0 +1,217 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cache defines the interface for caching
|
||||
type Cache interface {
|
||||
Get(key string) (interface{}, bool)
|
||||
Set(key string, value interface{})
|
||||
Delete(key string)
|
||||
Clear() error
|
||||
}
|
||||
|
||||
// FileCache implements file-based caching
|
||||
type FileCache struct {
|
||||
directory string
|
||||
ttl time.Duration
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// cacheEntry wraps a cached value with expiration
|
||||
type cacheEntry struct {
|
||||
Value interface{}
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// NewFileCache creates a new file-based cache
|
||||
func NewFileCache(directory string, ttl time.Duration) (*FileCache, error) {
|
||||
// Create directory if it doesn't exist
|
||||
if err := os.MkdirAll(directory, 0750); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FileCache{
|
||||
directory: directory,
|
||||
ttl: ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get retrieves a value from the cache
|
||||
func (c *FileCache) Get(key string) (interface{}, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
path := c.keyToPath(key)
|
||||
|
||||
file, err := os.Open(path) // #nosec G304 -- path is internally generated hash
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var entry cacheEntry
|
||||
decoder := gob.NewDecoder(file)
|
||||
if err := decoder.Decode(&entry); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if time.Now().After(entry.ExpiresAt) {
|
||||
_ = os.Remove(path)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry.Value, true
|
||||
}
|
||||
|
||||
// Set stores a value in the cache
|
||||
func (c *FileCache) Set(key string, value interface{}) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
entry := cacheEntry{
|
||||
Value: value,
|
||||
ExpiresAt: time.Now().Add(c.ttl),
|
||||
}
|
||||
|
||||
path := c.keyToPath(key)
|
||||
|
||||
// Ensure parent directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Create(path) // #nosec G304 -- path is internally generated hash
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := gob.NewEncoder(file)
|
||||
_ = encoder.Encode(entry)
|
||||
}
|
||||
|
||||
// Delete removes a value from the cache
|
||||
func (c *FileCache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
path := c.keyToPath(key)
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
|
||||
// Clear removes all cached values
|
||||
func (c *FileCache) Clear() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return os.RemoveAll(c.directory)
|
||||
}
|
||||
|
||||
// keyToPath converts a cache key to a file path
|
||||
func (c *FileCache) keyToPath(key string) string {
|
||||
hash := sha256.Sum256([]byte(key))
|
||||
filename := hex.EncodeToString(hash[:8]) + ".gob"
|
||||
return filepath.Join(c.directory, filename)
|
||||
}
|
||||
|
||||
// NoopCache is a cache that doesn't cache anything
|
||||
type NoopCache struct{}
|
||||
|
||||
// NewNoopCache creates a new no-op cache
|
||||
func NewNoopCache() *NoopCache {
|
||||
return &NoopCache{}
|
||||
}
|
||||
|
||||
// Get always returns false
|
||||
func (c *NoopCache) Get(key string) (interface{}, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Set does nothing
|
||||
func (c *NoopCache) Set(key string, value interface{}) {}
|
||||
|
||||
// Delete does nothing
|
||||
func (c *NoopCache) Delete(key string) {}
|
||||
|
||||
// Clear does nothing
|
||||
func (c *NoopCache) Clear() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MemoryCache implements in-memory caching (useful for testing)
|
||||
type MemoryCache struct {
|
||||
data map[string]cacheEntry
|
||||
ttl time.Duration
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMemoryCache creates a new in-memory cache
|
||||
func NewMemoryCache(ttl time.Duration) *MemoryCache {
|
||||
return &MemoryCache{
|
||||
data: make(map[string]cacheEntry),
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a value from the cache
|
||||
func (c *MemoryCache) Get(key string) (interface{}, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
entry, ok := c.data[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if time.Now().After(entry.ExpiresAt) {
|
||||
delete(c.data, key)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry.Value, true
|
||||
}
|
||||
|
||||
// Set stores a value in the cache
|
||||
func (c *MemoryCache) Set(key string, value interface{}) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.data[key] = cacheEntry{
|
||||
Value: value,
|
||||
ExpiresAt: time.Now().Add(c.ttl),
|
||||
}
|
||||
}
|
||||
|
||||
// Delete removes a value from the cache
|
||||
func (c *MemoryCache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
delete(c.data, key)
|
||||
}
|
||||
|
||||
// Clear removes all cached values
|
||||
func (c *MemoryCache) Clear() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.data = make(map[string]cacheEntry)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register types for gob encoding
|
||||
func init() {
|
||||
// Register common types that might be cached
|
||||
gob.Register([]interface{}{})
|
||||
gob.Register(map[string]interface{}{})
|
||||
}
|
||||
Vendored
+290
@@ -0,0 +1,290 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFileCache_Basic(t *testing.T) {
|
||||
// Create temp directory for cache
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cache, err := NewFileCache(tempDir, time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test Set and Get
|
||||
cache.Set("test-key", "test-value")
|
||||
|
||||
value, ok := cache.Get("test-key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test-value", value)
|
||||
}
|
||||
|
||||
func TestFileCache_GetNonExistent(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cache, err := NewFileCache(tempDir, time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
value, ok := cache.Get("non-existent")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, value)
|
||||
}
|
||||
|
||||
func TestFileCache_Expiration(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Use a very short TTL
|
||||
cache, err := NewFileCache(tempDir, 50*time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
|
||||
cache.Set("expire-key", "expire-value")
|
||||
|
||||
// Should be available immediately
|
||||
value, ok := cache.Get("expire-key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "expire-value", value)
|
||||
|
||||
// Wait for expiration
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Should be expired now
|
||||
value, ok = cache.Get("expire-key")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, value)
|
||||
}
|
||||
|
||||
func TestFileCache_Delete(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cache, err := NewFileCache(tempDir, time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
cache.Set("delete-key", "delete-value")
|
||||
|
||||
// Verify it exists
|
||||
_, ok := cache.Get("delete-key")
|
||||
assert.True(t, ok)
|
||||
|
||||
// Delete it
|
||||
cache.Delete("delete-key")
|
||||
|
||||
// Should be gone
|
||||
value, ok := cache.Get("delete-key")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, value)
|
||||
}
|
||||
|
||||
func TestFileCache_Clear(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cache, err := NewFileCache(tempDir, time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add multiple entries
|
||||
cache.Set("key1", "value1")
|
||||
cache.Set("key2", "value2")
|
||||
cache.Set("key3", "value3")
|
||||
|
||||
// Clear the cache
|
||||
err = cache.Clear()
|
||||
require.NoError(t, err)
|
||||
|
||||
// All should be gone
|
||||
_, ok := cache.Get("key1")
|
||||
assert.False(t, ok)
|
||||
_, ok = cache.Get("key2")
|
||||
assert.False(t, ok)
|
||||
_, ok = cache.Get("key3")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestFileCache_ComplexValues(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cache, err := NewFileCache(tempDir, time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test with map
|
||||
mapValue := map[string]interface{}{
|
||||
"key1": "value1",
|
||||
"key2": 123,
|
||||
}
|
||||
cache.Set("map-key", mapValue)
|
||||
|
||||
retrieved, ok := cache.Get("map-key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, mapValue, retrieved)
|
||||
|
||||
// Test with slice
|
||||
sliceValue := []interface{}{"a", "b", "c"}
|
||||
cache.Set("slice-key", sliceValue)
|
||||
|
||||
retrieved, ok = cache.Get("slice-key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, sliceValue, retrieved)
|
||||
}
|
||||
|
||||
func TestFileCache_CreateDirectory(t *testing.T) {
|
||||
// Test that NewFileCache creates directory if it doesn't exist
|
||||
tempDir := filepath.Join(t.TempDir(), "nested", "cache", "dir")
|
||||
|
||||
cache, err := NewFileCache(tempDir, time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify directory was created
|
||||
info, err := os.Stat(tempDir)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
|
||||
// Should be usable
|
||||
cache.Set("key", "value")
|
||||
value, ok := cache.Get("key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "value", value)
|
||||
}
|
||||
|
||||
func TestMemoryCache_Basic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := NewMemoryCache(time.Hour)
|
||||
|
||||
// Test Set and Get
|
||||
cache.Set("test-key", "test-value")
|
||||
|
||||
value, ok := cache.Get("test-key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test-value", value)
|
||||
}
|
||||
|
||||
func TestMemoryCache_GetNonExistent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := NewMemoryCache(time.Hour)
|
||||
|
||||
value, ok := cache.Get("non-existent")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, value)
|
||||
}
|
||||
|
||||
func TestMemoryCache_Expiration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := NewMemoryCache(50 * time.Millisecond)
|
||||
|
||||
cache.Set("expire-key", "expire-value")
|
||||
|
||||
// Should be available immediately
|
||||
value, ok := cache.Get("expire-key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "expire-value", value)
|
||||
|
||||
// Wait for expiration
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Should be expired now
|
||||
value, ok = cache.Get("expire-key")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, value)
|
||||
}
|
||||
|
||||
func TestMemoryCache_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := NewMemoryCache(time.Hour)
|
||||
|
||||
cache.Set("delete-key", "delete-value")
|
||||
|
||||
// Verify it exists
|
||||
_, ok := cache.Get("delete-key")
|
||||
assert.True(t, ok)
|
||||
|
||||
// Delete it
|
||||
cache.Delete("delete-key")
|
||||
|
||||
// Should be gone
|
||||
value, ok := cache.Get("delete-key")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, value)
|
||||
}
|
||||
|
||||
func TestMemoryCache_Clear(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := NewMemoryCache(time.Hour)
|
||||
|
||||
// Add multiple entries
|
||||
cache.Set("key1", "value1")
|
||||
cache.Set("key2", "value2")
|
||||
cache.Set("key3", "value3")
|
||||
|
||||
// Clear the cache
|
||||
err := cache.Clear()
|
||||
require.NoError(t, err)
|
||||
|
||||
// All should be gone
|
||||
_, ok := cache.Get("key1")
|
||||
assert.False(t, ok)
|
||||
_, ok = cache.Get("key2")
|
||||
assert.False(t, ok)
|
||||
_, ok = cache.Get("key3")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestNoopCache_AlwaysReturnsFalse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := NewNoopCache()
|
||||
|
||||
// Set something
|
||||
cache.Set("key", "value")
|
||||
|
||||
// Get should return false
|
||||
value, ok := cache.Get("key")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, value)
|
||||
}
|
||||
|
||||
func TestNoopCache_DeleteAndClear(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := NewNoopCache()
|
||||
|
||||
// These should not panic or error
|
||||
cache.Delete("key")
|
||||
err := cache.Clear()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFileCache_KeyToPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := &FileCache{directory: "/tmp/cache"}
|
||||
|
||||
path1 := cache.keyToPath("key1")
|
||||
path2 := cache.keyToPath("key2")
|
||||
path1Again := cache.keyToPath("key1")
|
||||
|
||||
// Different keys should produce different paths
|
||||
assert.NotEqual(t, path1, path2)
|
||||
|
||||
// Same key should produce same path
|
||||
assert.Equal(t, path1, path1Again)
|
||||
|
||||
// Path should end with .gob
|
||||
assert.Contains(t, path1, ".gob")
|
||||
}
|
||||
|
||||
func TestCacheInterface(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Ensure all cache types implement the interface
|
||||
var _ Cache = (*FileCache)(nil)
|
||||
var _ Cache = (*MemoryCache)(nil)
|
||||
var _ Cache = (*NoopCache)(nil)
|
||||
}
|
||||
Reference in New Issue
Block a user