Files
counters/internal/watcher/watcher_test.go
T

190 lines
4.5 KiB
Go

package watcher
import (
"errors"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
)
func write(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
// replace mimics the agent's atomic deploy: write a temp file next to the
// target and rename it over the target.
func replace(t *testing.T, path, content string) {
t.Helper()
tmp := path + ".tmp"
write(t, tmp, content)
if err := os.Rename(tmp, path); err != nil {
t.Fatal(err)
}
}
type counter struct {
n atomic.Int32
done chan struct{}
err error
}
func newCounter() *counter { return &counter{done: make(chan struct{}, 16)} }
func (c *counter) reload() error {
c.n.Add(1)
select {
case c.done <- struct{}{}:
default:
}
return c.err
}
func (c *counter) wait(t *testing.T) {
t.Helper()
select {
case <-c.done:
case <-time.After(5 * time.Second):
t.Fatal("timeout waiting for reload")
}
}
// start creates and starts a watcher; debounce must be set before Start so
// the watcher goroutine never races with the test.
func start(t *testing.T, path string, c *counter, debounce time.Duration) *Watcher {
t.Helper()
w, err := New([]string{path}, c.reload)
if err != nil {
t.Fatal(err)
}
w.debounce = debounce
w.Start()
t.Cleanup(w.Stop)
time.Sleep(50 * time.Millisecond) // let the watches settle
return w
}
func TestWatcher_ReloadsOnInPlaceWrite(t *testing.T) {
path := filepath.Join(t.TempDir(), "rspamd_domains_db")
write(t, path, "v1")
c := newCounter()
start(t, path, c, 100*time.Millisecond)
write(t, path, "v2")
c.wait(t)
if c.n.Load() < 1 {
t.Errorf("reloads = %d, want >= 1", c.n.Load())
}
}
// The agent deploys the CDB with mv; after the first replacement the old
// kqueue file watch is stale, so a second replacement must still be seen.
func TestWatcher_ReloadsOnAtomicReplaceTwice(t *testing.T) {
path := filepath.Join(t.TempDir(), "rspamd_domains_db")
write(t, path, "v1")
c := newCounter()
start(t, path, c, 100*time.Millisecond)
replace(t, path, "v2")
c.wait(t)
first := c.n.Load()
time.Sleep(150 * time.Millisecond)
replace(t, path, "v3")
c.wait(t)
if c.n.Load() <= first {
t.Errorf("second replacement not detected: reloads %d -> %d", first, c.n.Load())
}
}
func TestWatcher_IgnoresOtherFilesInDir(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "rspamd_domains_db")
write(t, path, "v1")
c := newCounter()
start(t, path, c, 100*time.Millisecond)
write(t, filepath.Join(dir, "other_file"), "x")
time.Sleep(400 * time.Millisecond)
if c.n.Load() != 0 {
t.Errorf("reloads = %d for an unrelated file, want 0", c.n.Load())
}
}
func TestWatcher_DebouncesBurst(t *testing.T) {
path := filepath.Join(t.TempDir(), "rspamd_domains_db")
write(t, path, "v1")
c := newCounter()
start(t, path, c, 200*time.Millisecond)
for i := 0; i < 5; i++ {
write(t, path, "burst")
time.Sleep(20 * time.Millisecond)
}
c.wait(t)
time.Sleep(500 * time.Millisecond)
if n := c.n.Load(); n > 2 {
t.Errorf("reloads = %d, want 1-2 (debounced)", n)
}
}
func TestWatcher_ContinuesAfterReloadError(t *testing.T) {
path := filepath.Join(t.TempDir(), "rspamd_domains_db")
write(t, path, "v1")
c := newCounter()
c.err = errors.New("boom")
start(t, path, c, 100*time.Millisecond)
write(t, path, "v2")
c.wait(t)
time.Sleep(150 * time.Millisecond)
write(t, path, "v3")
c.wait(t)
if c.n.Load() < 2 {
t.Errorf("reloads = %d, want >= 2 (a failing reload must not stop the watcher)", c.n.Load())
}
}
func TestWatcher_StopPreventsReload(t *testing.T) {
path := filepath.Join(t.TempDir(), "rspamd_domains_db")
write(t, path, "v1")
c := newCounter()
w := start(t, path, c, 100*time.Millisecond)
w.Stop()
write(t, path, "v2")
time.Sleep(400 * time.Millisecond)
if c.n.Load() != 0 {
t.Errorf("reloads = %d after Stop, want 0", c.n.Load())
}
w.Stop() // idempotent
}
func TestNew_MissingDirectory(t *testing.T) {
if _, err := New([]string{"/nonexistent-dir-for-test/rspamd_domains_db"}, func() error { return nil }); err == nil {
t.Fatal("expected error for a missing directory")
}
}
func TestNew_MissingFileInExistingDirIsFine(t *testing.T) {
// The CDB may be deployed after the daemon starts; the directory watch
// picks up its creation.
path := filepath.Join(t.TempDir(), "rspamd_domains_db")
c := newCounter()
w, err := New([]string{path}, c.reload)
if err != nil {
t.Fatal(err)
}
w.debounce = 100 * time.Millisecond
w.Start()
defer w.Stop()
time.Sleep(50 * time.Millisecond)
write(t, path, "v1")
c.wait(t)
}