Baseline note: TestMetricHistoryMinuteFold fails identically on the pristine B3 tip in the same wall-clock window (minute-parity sensitive seed), passes in others — pre-existing, untouched path.
170 lines
5.0 KiB
Go
170 lines
5.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// probeTCP against a live listener yields a real non-negative
|
|
// latency; a closed port and a malformed target both file -1; 0 is
|
|
// never a latency (contract: failures are negative or absent).
|
|
func TestProbeTCPOutcomes(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
conn.Close()
|
|
}
|
|
}()
|
|
|
|
ms, sampled := probeTCP(ln.Addr().String(), nil)
|
|
if !sampled || ms < 0 {
|
|
t.Fatalf("live target: ms=%d sampled=%v", ms, sampled)
|
|
}
|
|
ms, sampled = probeTCP("127.0.0.1:1", nil)
|
|
if !sampled || ms != -1 {
|
|
t.Fatalf("closed port: ms=%d sampled=%v", ms, sampled)
|
|
}
|
|
ms, sampled = probeTCP("not-a-target", nil)
|
|
if !sampled || ms != -1 {
|
|
t.Fatalf("malformed: ms=%d sampled=%v", ms, sampled)
|
|
}
|
|
ms, sampled = probeTCP("256.256.256.256:80", nil)
|
|
if !sampled || ms != -1 {
|
|
t.Fatalf("unresolvable: ms=%d sampled=%v", ms, sampled)
|
|
}
|
|
}
|
|
|
|
// applyTasks: an unchanged task keeps its loop; a changed target or
|
|
// interval restarts it; a removed task is cancelled; the set is
|
|
// truncated at the cap with intervals clamped into range.
|
|
func TestProbeRunnerApplyTasks(t *testing.T) {
|
|
r := newProbeRunner()
|
|
defer r.stop()
|
|
|
|
r.applyTasks([]ProbeTask{{ID: 1, Target: "127.0.0.1:1", Interval: 60}})
|
|
if len(r.tasks) != 1 {
|
|
t.Fatalf("tasks %v", r.tasks)
|
|
}
|
|
first := r.tasks[1]
|
|
|
|
// Unchanged: same loop object survives.
|
|
r.applyTasks([]ProbeTask{{ID: 1, Target: "127.0.0.1:1", Interval: 60}})
|
|
if r.tasks[1] != first {
|
|
t.Fatal("unchanged task restarted")
|
|
}
|
|
// Changed interval: replaced, clamped values are what runs.
|
|
r.applyTasks([]ProbeTask{{ID: 1, Target: "127.0.0.1:1", Interval: 120}})
|
|
if r.tasks[1] == first || r.tasks[1].interval != 120 {
|
|
t.Fatal("changed task not restarted")
|
|
}
|
|
// Removed: cancelled.
|
|
r.applyTasks(nil)
|
|
if len(r.tasks) != 0 {
|
|
t.Fatalf("tasks after clear %v", r.tasks)
|
|
}
|
|
|
|
// Clamp + cap.
|
|
many := make([]ProbeTask, 0, 70)
|
|
for i := 1; i <= 70; i++ {
|
|
many = append(many, ProbeTask{ID: int64(i), Target: "127.0.0.1:1", Interval: 1})
|
|
}
|
|
r.applyTasks(many)
|
|
if len(r.tasks) != maxProbeTasksRun {
|
|
t.Fatalf("cap: %d running", len(r.tasks))
|
|
}
|
|
if r.tasks[1].interval != minProbeIntervalSec {
|
|
t.Fatalf("clamp: interval %d", r.tasks[1].interval)
|
|
}
|
|
}
|
|
|
|
// A running loop files samples into the buffer; drain takes them and
|
|
// requeue restores order ahead of newer samples.
|
|
func TestProbeRunnerResultFlow(t *testing.T) {
|
|
r := newProbeRunner()
|
|
defer r.stop()
|
|
r.applyTasks([]ProbeTask{{ID: 9, Target: "127.0.0.1:1", Interval: 3600}})
|
|
// The loop probes immediately on start.
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if got := r.drainResults(); len(got) > 0 {
|
|
if got[0].TaskID != 9 || got[0].LatencyMs != -1 {
|
|
t.Fatalf("sample %+v", got[0])
|
|
}
|
|
goto drained
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
t.Fatal("no immediate sample from a fresh task")
|
|
drained:
|
|
|
|
r.addResult(ProbeResult{TaskID: 1, LatencyMs: 5})
|
|
batch := r.drainResults()
|
|
r.addResult(ProbeResult{TaskID: 1, LatencyMs: 7})
|
|
r.requeueResults(batch)
|
|
got := r.drainResults()
|
|
if len(got) != 2 || got[0].LatencyMs != 5 || got[1].LatencyMs != 7 {
|
|
t.Fatalf("requeue order %+v", got)
|
|
}
|
|
if again := r.drainResults(); again != nil {
|
|
t.Fatalf("second drain %+v", again)
|
|
}
|
|
}
|
|
|
|
// Buffer overflow drops the oldest samples, keeping the newest cap.
|
|
func TestProbeRunnerBufferCap(t *testing.T) {
|
|
r := newProbeRunner()
|
|
defer r.stop()
|
|
for i := 0; i < probeResultCap+10; i++ {
|
|
r.addResult(ProbeResult{TaskID: 1, LatencyMs: int64(i)})
|
|
}
|
|
got := r.drainResults()
|
|
if len(got) != probeResultCap {
|
|
t.Fatalf("buffer %d", len(got))
|
|
}
|
|
if got[0].LatencyMs != 10 || got[len(got)-1].LatencyMs != int64(probeResultCap+9) {
|
|
t.Fatalf("kept range %d..%d", got[0].LatencyMs, got[len(got)-1].LatencyMs)
|
|
}
|
|
if r.dropped != 10 {
|
|
t.Fatalf("dropped %d", r.dropped)
|
|
}
|
|
}
|
|
|
|
// The wire shapes: a result marshals as taskId/latencyMs, and the
|
|
// config decode distinguishes an absent probe_tasks (old master,
|
|
// keep current set) from an empty one (clear all).
|
|
func TestProbeWireShapes(t *testing.T) {
|
|
body, err := json.Marshal(ProbeResult{TaskID: 3, LatencyMs: -1})
|
|
if err != nil || string(body) != `{"taskId":3,"latencyMs":-1}` {
|
|
t.Fatalf("result wire: %s %v", body, err)
|
|
}
|
|
decode := func(s string) *[]ProbeTask {
|
|
var result struct {
|
|
ProbeTasks *[]ProbeTask `json:"probe_tasks"`
|
|
}
|
|
if err := json.Unmarshal([]byte(s), &result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return result.ProbeTasks
|
|
}
|
|
if got := decode(`{"config_hash":"x"}`); got != nil {
|
|
t.Fatalf("absent field decoded as %v", got)
|
|
}
|
|
if got := decode(`{"probe_tasks":[]}`); got == nil || len(*got) != 0 {
|
|
t.Fatalf("empty list decoded as %v", got)
|
|
}
|
|
got := decode(`{"probe_tasks":[{"id":2,"target":"1.1.1.1:443","interval":30}]}`)
|
|
if got == nil || len(*got) != 1 || (*got)[0].ID != 2 || (*got)[0].Target != "1.1.1.1:443" || (*got)[0].Interval != 30 {
|
|
t.Fatalf("task decode %+v", got)
|
|
}
|
|
}
|