Skip to content

Commit 4ce0b64

Browse files
corylanouclaude
andcommitted
feat(directory-watcher): add validation and test improvements
- Add validation: return error when no databases found in directory without watch enabled - Split EmptyDirectory test to validate both watch enabled/disabled scenarios - Add test for recursive mode detecting nested databases - Fix race condition in Store.Close() by cloning dbs slice while holding lock 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 7bb344b commit 4ce0b64

File tree

3 files changed

+69
-2
lines changed

3 files changed

+69
-2
lines changed

cmd/litestream/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,10 @@ func NewDBsFromDirectoryConfig(dbc *DBConfig) ([]*litestream.DB, error) {
594594
return nil, fmt.Errorf("failed to scan directory %s: %w", dirPath, err)
595595
}
596596

597+
if len(dbPaths) == 0 && !dbc.Watch {
598+
return nil, fmt.Errorf("no SQLite databases found in directory %s with pattern %s", dirPath, dbc.Pattern)
599+
}
600+
597601
// Create DB instances for each found database
598602
var dbs []*litestream.DB
599603
for _, dbPath := range dbPaths {

cmd/litestream/main_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1929,7 +1929,7 @@ func TestNewDBsFromDirectoryConfig_ReplicasArray(t *testing.T) {
19291929
}
19301930
}
19311931

1932-
func TestNewDBsFromDirectoryConfig_EmptyDirectory(t *testing.T) {
1932+
func TestNewDBsFromDirectoryConfig_EmptyDirectoryRequiresDatabases(t *testing.T) {
19331933
tmpDir := t.TempDir()
19341934
replicaDir := filepath.Join(tmpDir, "replica")
19351935

@@ -1939,6 +1939,22 @@ func TestNewDBsFromDirectoryConfig_EmptyDirectory(t *testing.T) {
19391939
Replica: &main.ReplicaConfig{Type: "file", Path: replicaDir},
19401940
}
19411941

1942+
if _, err := main.NewDBsFromDirectoryConfig(config); err == nil {
1943+
t.Fatalf("expected error for empty directory when watch disabled")
1944+
}
1945+
}
1946+
1947+
func TestNewDBsFromDirectoryConfig_EmptyDirectoryWithWatch(t *testing.T) {
1948+
tmpDir := t.TempDir()
1949+
replicaDir := filepath.Join(tmpDir, "replica")
1950+
1951+
config := &main.DBConfig{
1952+
Dir: tmpDir,
1953+
Pattern: "*.db",
1954+
Watch: true,
1955+
Replica: &main.ReplicaConfig{Type: "file", Path: replicaDir},
1956+
}
1957+
19421958
dbs, err := main.NewDBsFromDirectoryConfig(config)
19431959
if err != nil {
19441960
t.Fatalf("unexpected error: %v", err)
@@ -2003,6 +2019,49 @@ func TestDirectoryMonitor_DetectsDatabaseLifecycle(t *testing.T) {
20032019
}
20042020
}
20052021

2022+
func TestDirectoryMonitor_RecursiveDetectsNestedDatabases(t *testing.T) {
2023+
ctx := context.Background()
2024+
rootDir := t.TempDir()
2025+
replicaDir := filepath.Join(t.TempDir(), "replicas")
2026+
2027+
config := &main.DBConfig{
2028+
Dir: rootDir,
2029+
Pattern: "*.db",
2030+
Recursive: true,
2031+
Watch: true,
2032+
Replica: &main.ReplicaConfig{Type: "file", Path: replicaDir},
2033+
}
2034+
2035+
storeConfig := main.DefaultConfig()
2036+
store := litestream.NewStore(nil, storeConfig.CompactionLevels())
2037+
store.CompactionMonitorEnabled = false
2038+
if err := store.Open(ctx); err != nil {
2039+
t.Fatalf("unexpected error opening store: %v", err)
2040+
}
2041+
defer func() {
2042+
if err := store.Close(context.Background()); err != nil {
2043+
t.Fatalf("unexpected error closing store: %v", err)
2044+
}
2045+
}()
2046+
2047+
monitor, err := main.NewDirectoryMonitor(ctx, store, config, nil)
2048+
if err != nil {
2049+
t.Fatalf("failed to initialize directory monitor: %v", err)
2050+
}
2051+
defer monitor.Close()
2052+
2053+
deepDir := filepath.Join(rootDir, "tenant", "nested", "deeper")
2054+
if err := os.MkdirAll(deepDir, 0755); err != nil {
2055+
t.Fatalf("failed to create nested directories: %v", err)
2056+
}
2057+
deepDB := filepath.Join(deepDir, "deep.db")
2058+
createSQLiteDB(t, deepDB)
2059+
2060+
if !waitForCondition(5*time.Second, func() bool { return hasDBPath(store.DBs(), deepDB) }) {
2061+
t.Fatalf("expected nested database %s to be detected", deepDB)
2062+
}
2063+
}
2064+
20062065
// createSQLiteDB creates a minimal SQLite database file for testing
20072066
func createSQLiteDB(t *testing.T, path string) {
20082067
t.Helper()

store.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ func (s *Store) Open(ctx context.Context) error {
135135
}
136136

137137
func (s *Store) Close(ctx context.Context) (err error) {
138-
for _, db := range s.dbs {
138+
s.mu.Lock()
139+
dbs := slices.Clone(s.dbs)
140+
s.mu.Unlock()
141+
142+
for _, db := range dbs {
139143
if e := db.Close(ctx); e != nil && err == nil {
140144
err = e
141145
}

0 commit comments

Comments
 (0)