mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
199 lines
6.5 KiB
Go
199 lines
6.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func Test_repeatLogger(t *testing.T) {
|
|
var repeats repeatLogger
|
|
for _, step := range []struct {
|
|
name string
|
|
key string
|
|
value string
|
|
wantChanged bool
|
|
wantRepeats uint64
|
|
}{
|
|
{"the first value of a key is a change", "pf anchor", "intact", true, 0},
|
|
{"the same value repeats", "pf anchor", "intact", false, 1},
|
|
{"the same value repeats again", "pf anchor", "intact", false, 2},
|
|
{"a new value reports the run before it", "pf anchor", "missing", true, 2},
|
|
{"the new value repeats", "pf anchor", "missing", false, 1},
|
|
{"the value before is a change again", "pf anchor", "intact", true, 1},
|
|
{"a second key starts on its own", "tunnels", "utun0", true, 0},
|
|
{"the second key repeats", "tunnels", "utun0", false, 1},
|
|
{"the first key keeps its own count", "pf anchor", "intact", false, 1},
|
|
} {
|
|
t.Run(step.name, func(t *testing.T) {
|
|
changed, count := repeats.changed(step.key, step.value)
|
|
if changed != step.wantChanged || count != step.wantRepeats {
|
|
t.Errorf("changed(%q, %q) = %v, %d, want %v, %d",
|
|
step.key, step.value, changed, count, step.wantChanged, step.wantRepeats)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_wakeReporter(t *testing.T) {
|
|
base := time.Date(2026, 9, 17, 10, 0, 0, 0, time.UTC)
|
|
var wake wakeReporter
|
|
var armed []func()
|
|
wake.after = func(_ time.Duration, fn func()) { armed = append(armed, fn) }
|
|
var emitted []wakeReport
|
|
emit := func(report wakeReport) { emitted = append(emitted, report) }
|
|
fireHeld := func() {
|
|
for _, fn := range armed {
|
|
fn()
|
|
}
|
|
armed = nil
|
|
}
|
|
|
|
// netmon reports first and knows no gap, so its report waits.
|
|
wake.note(base, wakeReport{source: "netmon"}, emit)
|
|
if len(emitted) != 0 || len(armed) != 1 {
|
|
t.Fatalf("a report without a gap must wait: emitted %d, timers %d", len(emitted), len(armed))
|
|
}
|
|
// The detector reports inside the hold with the gap, so it replaces the
|
|
// held report.
|
|
wake.note(base.Add(2*time.Second), wakeReport{source: "detector", gap: 90 * time.Second}, emit)
|
|
fireHeld()
|
|
if len(emitted) != 1 || emitted[0].source != "detector" {
|
|
t.Fatalf("emitted %+v, want the detector report alone", emitted)
|
|
}
|
|
// Every other report of this wake is the same wake.
|
|
wake.note(base.Add(10*time.Second), wakeReport{source: "netmon"}, emit)
|
|
wake.note(base.Add(12*time.Second), wakeReport{source: "detector", gap: 91 * time.Second}, emit)
|
|
if len(emitted) != 1 {
|
|
t.Fatalf("emitted %d reports inside the window, want 1", len(emitted))
|
|
}
|
|
|
|
// A wake that only netmon sees reports without a gap when the hold ends.
|
|
wake.note(base.Add(40*time.Second), wakeReport{source: "netmon"}, emit)
|
|
if len(emitted) != 1 || len(armed) != 1 {
|
|
t.Fatalf("a new wake must arm one hold: emitted %d, timers %d", len(emitted), len(armed))
|
|
}
|
|
fireHeld()
|
|
if len(emitted) != 2 || emitted[1].source != "netmon" || emitted[1].gap != 0 {
|
|
t.Fatalf("emitted %+v, want the netmon report after the hold", emitted)
|
|
}
|
|
// A gap that arrives after the hold is too late: one event per wake.
|
|
wake.note(base.Add(50*time.Second), wakeReport{source: "detector", gap: 92 * time.Second}, emit)
|
|
if len(emitted) != 2 {
|
|
t.Fatalf("emitted %d reports, want no second report of one wake", len(emitted))
|
|
}
|
|
|
|
// The detector first reports at once, and netmon adds nothing.
|
|
wake.note(base.Add(80*time.Second), wakeReport{source: "detector", gap: 93 * time.Second}, emit)
|
|
wake.note(base.Add(82*time.Second), wakeReport{source: "netmon"}, emit)
|
|
fireHeld()
|
|
if len(emitted) != 3 || emitted[2].source != "detector" || len(armed) != 0 {
|
|
t.Fatalf("emitted %+v with %d timers, want one detector report and no hold", emitted, len(armed))
|
|
}
|
|
}
|
|
|
|
func Test_noiseCoalescer(t *testing.T) {
|
|
base := time.Date(2026, 9, 17, 10, 0, 0, 0, time.UTC)
|
|
|
|
t.Run("a storm reports once every ten minutes", func(t *testing.T) {
|
|
const deltas = 100
|
|
const cadence = 15 * time.Second
|
|
var noise noiseCoalescer
|
|
var got []noiseSummary
|
|
for i := 0; i < deltas; i++ {
|
|
interfaces := []string{"awdl0"}
|
|
if i%2 == 1 {
|
|
interfaces = []string{"llw0", "awdl0"}
|
|
}
|
|
summary, emit := noise.add(interfaces, base.Add(time.Duration(i)*cadence))
|
|
if !emit {
|
|
continue
|
|
}
|
|
got = append(got, summary)
|
|
}
|
|
want := []noiseSummary{{
|
|
Count: 1,
|
|
First: base,
|
|
Last: base,
|
|
Interfaces: []string{"awdl0"},
|
|
}, {
|
|
Count: 40,
|
|
First: base.Add(cadence),
|
|
Last: base.Add(10 * time.Minute),
|
|
Interfaces: []string{"awdl0", "llw0"},
|
|
}, {
|
|
Count: 40,
|
|
First: base.Add(10*time.Minute + cadence),
|
|
Last: base.Add(20 * time.Minute),
|
|
Interfaces: []string{"awdl0", "llw0"},
|
|
}}
|
|
assertSummaries(t, got, want)
|
|
})
|
|
|
|
t.Run("a flush reports the open window", func(t *testing.T) {
|
|
var noise noiseCoalescer
|
|
if _, emit := noise.flush(base); emit {
|
|
t.Fatal("an empty coalescer reported a summary")
|
|
}
|
|
for _, offset := range []time.Duration{0, time.Minute, 2 * time.Minute} {
|
|
noise.add([]string{"awdl0"}, base.Add(offset))
|
|
}
|
|
summary, emit := noise.flush(base.Add(3 * time.Minute))
|
|
if !emit {
|
|
t.Fatal("the flush reported no summary for an open window")
|
|
}
|
|
assertSummaries(t, []noiseSummary{summary}, []noiseSummary{{
|
|
Count: 2,
|
|
First: base.Add(time.Minute),
|
|
Last: base.Add(2 * time.Minute),
|
|
Interfaces: []string{"awdl0"},
|
|
}})
|
|
if _, emit = noise.flush(base.Add(4 * time.Minute)); emit {
|
|
t.Fatal("the flush reported the same window twice")
|
|
}
|
|
})
|
|
|
|
t.Run("a gap longer than the interval starts a new run", func(t *testing.T) {
|
|
var noise noiseCoalescer
|
|
var got []noiseSummary
|
|
for _, offset := range []time.Duration{0, time.Minute, 2 * time.Minute, 17 * time.Minute} {
|
|
summary, emit := noise.add([]string{"awdl0"}, base.Add(offset))
|
|
if !emit {
|
|
continue
|
|
}
|
|
got = append(got, summary)
|
|
}
|
|
want := []noiseSummary{{
|
|
Count: 1,
|
|
First: base,
|
|
Last: base,
|
|
Interfaces: []string{"awdl0"},
|
|
}, {
|
|
Count: 1,
|
|
First: base.Add(17 * time.Minute),
|
|
Last: base.Add(17 * time.Minute),
|
|
Interfaces: []string{"awdl0"},
|
|
}}
|
|
assertSummaries(t, got, want)
|
|
})
|
|
}
|
|
|
|
func assertSummaries(t *testing.T, got, want []noiseSummary) {
|
|
t.Helper()
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %d summaries, want %d: %+v", len(got), len(want), got)
|
|
}
|
|
for i := range want {
|
|
if !sameSummary(got[i], want[i]) {
|
|
t.Errorf("summary %d = %+v, want %+v", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func sameSummary(got, want noiseSummary) bool {
|
|
return got.Count == want.Count &&
|
|
got.First.Equal(want.First) &&
|
|
got.Last.Equal(want.Last) &&
|
|
slices.Equal(got.Interfaces, want.Interfaces)
|
|
}
|