You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

152 lines
3.9 KiB

package sensor
import (
"context"
"reflect"
"sync"
"testing"
"testing/synctest"
"time"
"mqtt_washingmachine/internal/status"
)
func TestSamplerIntervals(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var sampler Sampler
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
ticks := make(chan time.Time)
done := make(chan struct{})
go func() { sampler.Run(ctx, ticks); close(done) }()
tick := func() { ticks <- time.Now(); synctest.Wait() }
receive := func(payload string) {
t.Helper()
if err := sampler.Receive([]byte(payload)); err != nil {
t.Fatal(err)
}
}
if got := sampler.Status(); got.State != "waiting" || len(got.Readings) != 0 {
t.Fatalf("startup: %+v", got)
}
receive(`{"ENERGY":{"Power":100}}`)
receive(`{"ENERGY":{"Power":47.7}}`)
if err := sampler.Receive([]byte(`{"ENERGY":{}}`)); err == nil {
t.Fatal("invalid payload accepted")
}
if got := sampler.Status(); len(got.Readings) != 0 {
t.Fatal("message recorded before tick")
}
tick()
if got := sampler.Status(); !reflect.DeepEqual(got.Readings, []status.Sample{{Power: 47.7, Valid: true}}) {
t.Fatalf("latest valid measurement lost: %+v", got)
}
if err := sampler.Receive([]byte(`broken`)); err == nil {
t.Fatal("malformed payload accepted")
}
tick()
receive(`{"ENERGY":{"Power":0}}`)
tick()
want := []status.Sample{{Power: 47.7, Valid: true}, {}, {Valid: true}}
if got := sampler.Status(); got.State != "incomplete" || !reflect.DeepEqual(got.Readings, want) {
t.Fatalf("missing interval: %+v", got)
}
for range 2 {
receive(`{"ENERGY":{"Power":2.99}}`)
tick()
}
if got := sampler.Status(); got.State != "done" {
t.Fatalf("gap did not age out: %+v", got)
}
receive(`{"ENERGY":{"Power":3.00}}`)
tick()
if got := sampler.Status(); got.State != "running" {
t.Fatalf("threshold: %+v", got)
}
cancel()
synctest.Wait()
select {
case <-done:
default:
t.Fatal("Run did not stop on cancellation")
}
})
}
func TestSamplerClosedTicks(t *testing.T) {
var sampler Sampler
ticks := make(chan time.Time)
close(ticks)
sampler.Run(t.Context(), ticks)
if len(sampler.Status().Readings) != 0 {
t.Fatal("closed ticks created a sample")
}
}
func TestSamplerOnReceive(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var sampler Sampler
calls := 0
ticks := make(chan time.Time)
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
go sampler.Run(ctx, ticks)
sampler.OnReceive = func() {
calls++
// Both operations acquire the sampler lock: the callback must run
// after storage and outside that lock.
ticks <- time.Now()
synctest.Wait()
got := sampler.Status().Readings
if got[len(got)-1] != (status.Sample{Power: 47.7, Valid: true}) {
t.Errorf("callback ran before storage: %+v", got)
}
}
for range 2 {
if err := sampler.Receive([]byte(`{"ENERGY":{"Power":47.7}}`)); err != nil {
t.Fatal(err)
}
}
for _, payload := range []string{`broken`, `{}`, `{"ENERGY":{}}`, `{"ENERGY":{"Power":"bad"}}`} {
if err := sampler.Receive([]byte(payload)); err == nil {
t.Fatalf("accepted %s", payload)
}
}
if calls != 2 {
t.Fatalf("callback calls = %d; want 2", calls)
}
})
}
func TestSamplerConcurrentAccess(t *testing.T) {
var sampler Sampler
ctx, cancel := context.WithCancel(t.Context())
ticks := make(chan time.Time)
done := make(chan struct{})
go func() { sampler.Run(ctx, ticks); close(done) }()
var workers sync.WaitGroup
for range 10 {
workers.Go(func() {
for range 100 {
if err := sampler.Receive([]byte(`{"ENERGY":{"Power":1}}`)); err != nil {
t.Error(err)
}
snapshot := sampler.Status()
if len(snapshot.Readings) > 0 {
snapshot.Readings[0].Power = 999
}
}
})
}
for range 100 {
ticks <- time.Now()
}
workers.Wait()
cancel()
<-done
for _, sample := range sampler.Status().Readings {
if sample.Power == 999 {
t.Fatal("snapshot changed shared state")
}
}
}