continuous-integration/drone/push Build is passing
- Add POSTROUTING MASQUERADE rule alongside DNAT rules to ensure return traffic from container targets can route back through the same interface, matching legacy shell script behavior - Enhance FileWatcher to trigger periodic state reconciliation every tick regardless of config file changes, ensuring desired state is maintained after container restarts or iptables flushes
175 lines
4.2 KiB
Go
175 lines
4.2 KiB
Go
package watcher
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestWatcherDetectsChange(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.json")
|
|
if err := os.WriteFile(path, []byte(`{"version": 1}`), 0644); err != nil {
|
|
t.Fatalf("failed to write test file: %v", err)
|
|
}
|
|
|
|
changeDetected := make(chan bool, 1)
|
|
onChange := func() {
|
|
select {
|
|
case changeDetected <- true:
|
|
default:
|
|
}
|
|
}
|
|
|
|
fw := NewFileWatcher(path, 100*time.Millisecond, onChange)
|
|
fw.Start()
|
|
defer fw.Stop()
|
|
|
|
// Wait for initial hash to be computed
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Modify the file
|
|
if err := os.WriteFile(path, []byte(`{"version": 2}`), 0644); err != nil {
|
|
t.Fatalf("failed to modify test file: %v", err)
|
|
}
|
|
|
|
// Wait for change detection
|
|
select {
|
|
case detected := <-changeDetected:
|
|
if !detected {
|
|
t.Error("expected change detection, got false")
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Error("timeout waiting for file change detection")
|
|
}
|
|
}
|
|
|
|
func TestWatcherNoChange(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.json")
|
|
if err := os.WriteFile(path, []byte(`{"version": 1}`), 0644); err != nil {
|
|
t.Fatalf("failed to write test file: %v", err)
|
|
}
|
|
|
|
// With periodic reconciliation, onChange will be called every period.
|
|
// Count how many times it's called within the wait period.
|
|
callCount := 0
|
|
onChange := func() {
|
|
callCount++
|
|
}
|
|
|
|
fw := NewFileWatcher(path, 100*time.Millisecond, onChange)
|
|
fw.Start()
|
|
defer fw.Stop()
|
|
|
|
// Wait without modifying the file
|
|
time.Sleep(350 * time.Millisecond)
|
|
|
|
// onChange should have been called ~3 times (0s, 0.1s, 0.2s, 0.3s) for periodic reconciliation
|
|
if callCount < 1 {
|
|
t.Errorf("expected at least 1 periodic reconciliation call, got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestWatcherMultipleChanges(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.json")
|
|
if err := os.WriteFile(path, []byte(`{"v": 1}`), 0644); err != nil {
|
|
t.Fatalf("failed to write test file: %v", err)
|
|
}
|
|
|
|
changeCount := 0
|
|
var mu chan struct{}
|
|
|
|
onChange := func() {
|
|
changeCount++
|
|
if mu != nil {
|
|
mu <- struct{}{}
|
|
}
|
|
}
|
|
|
|
fw := NewFileWatcher(path, 50*time.Millisecond, onChange)
|
|
fw.Start()
|
|
defer fw.Stop()
|
|
|
|
// Wait for initial hash
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Make 3 changes
|
|
for i := 2; i <= 4; i++ {
|
|
if err := os.WriteFile(path, []byte(`{"v": `+string(rune('0'+i))+`}`), 0644); err != nil {
|
|
t.Fatalf("failed to modify file: %v", err)
|
|
}
|
|
time.Sleep(150 * time.Millisecond)
|
|
}
|
|
|
|
if changeCount < 2 {
|
|
t.Errorf("expected at least 2 change detections, got %d", changeCount)
|
|
}
|
|
}
|
|
|
|
func TestWatcherStop(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.json")
|
|
if err := os.WriteFile(path, []byte(`{"v": 1}`), 0644); err != nil {
|
|
t.Fatalf("failed to write test file: %v", err)
|
|
}
|
|
|
|
fw := NewFileWatcher(path, 50*time.Millisecond, func() {})
|
|
fw.Start()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
fw.Stop()
|
|
|
|
// Should not panic on double stop or further operations
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
func TestWatcherNonexistentFile(t *testing.T) {
|
|
onChange := func() {
|
|
t.Error("onChange should not be called for nonexistent file")
|
|
}
|
|
|
|
fw := NewFileWatcher("/nonexistent/file.json", 100*time.Millisecond, onChange)
|
|
fw.Start()
|
|
defer fw.Stop()
|
|
|
|
// Wait — should not call onChange since file doesn't exist
|
|
time.Sleep(300 * time.Millisecond)
|
|
}
|
|
|
|
func TestWatcherReproducible(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.json")
|
|
if err := os.WriteFile(path, []byte(`{"data": "value"}`), 0644); err != nil {
|
|
t.Fatalf("failed to write test file: %v", err)
|
|
}
|
|
|
|
// Create two watchers on the same file
|
|
var detected1, detected2 bool
|
|
|
|
fw1 := NewFileWatcher(path, 50*time.Millisecond, func() { detected1 = true })
|
|
fw2 := NewFileWatcher(path, 50*time.Millisecond, func() { detected2 = true })
|
|
|
|
fw1.Start()
|
|
fw2.Start()
|
|
defer fw1.Stop()
|
|
defer fw2.Stop()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Modify file
|
|
if err := os.WriteFile(path, []byte(`{"data": "changed"}`), 0644); err != nil {
|
|
t.Fatalf("failed to modify file: %v", err)
|
|
}
|
|
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
if !detected1 {
|
|
t.Error("watcher 1 did not detect change")
|
|
}
|
|
if !detected2 {
|
|
t.Error("watcher 2 did not detect change")
|
|
}
|
|
} |