working on valuemonitor
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
TheTestFilename = "settings_test.settings"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
exitCode := func() int {
|
||||
if err := os.Remove(TheTestFilename); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
panic("error cleaning up " + TheTestFilename + " from previous run: " + err.Error())
|
||||
} else if err := os.WriteFile(TheTestFilename, nil, 0664); err != nil {
|
||||
panic("error creating empty " + TheTestFilename + ": " + err.Error())
|
||||
}
|
||||
defer os.Remove(TheTestFilename)
|
||||
|
||||
TheSettings = NewSettings(TheTestFilename, false)
|
||||
|
||||
return m.Run()
|
||||
}()
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
+70
-8
@@ -1,8 +1,10 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -24,11 +26,67 @@ var (
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
mutex sync.RWMutex
|
||||
contents map[string]string
|
||||
filename string
|
||||
modtime time.Time
|
||||
LogUpdates atomic.Bool
|
||||
mutex sync.RWMutex
|
||||
context context.Context
|
||||
contents map[string]string
|
||||
monitorKeys map[string]chan<- string
|
||||
filename string
|
||||
modtime time.Time
|
||||
LogUpdates atomic.Bool
|
||||
}
|
||||
|
||||
func insertOrTimeout(ctx context.Context, c chan<- string, s string) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case c <- s:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Settings) RegisterKeyToMonitor(key string, updateOutputChannel chan<- string) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
if s.monitorKeys == nil {
|
||||
s.monitorKeys = make(map[string]chan<- string, 1)
|
||||
}
|
||||
if _, found := s.monitorKeys[key]; found {
|
||||
return fmt.Errorf("key %q already in monitorKeys map", key)
|
||||
}
|
||||
if err := insertOrTimeout(s.context, updateOutputChannel, s.contents[key]); err != nil {
|
||||
return fmt.Errorf("error inserting: %w", err)
|
||||
}
|
||||
s.monitorKeys[key] = updateOutputChannel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Settings) Dump() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
if err := s.DumpToWriter(&buffer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *Settings) DumpToWriter(w io.Writer) error {
|
||||
writer := bufio.NewWriter(w)
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
var index int
|
||||
for k, v := range s.contents {
|
||||
writer.WriteString(fmt.Sprintf("%s=%s", k, v))
|
||||
if index < len(s.contents)-1 {
|
||||
writer.WriteByte('\n')
|
||||
}
|
||||
index++
|
||||
}
|
||||
return writer.Flush()
|
||||
}
|
||||
|
||||
func (s *Settings) DumpToJson() ([]byte, error) {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
return json.Marshal(s.contents)
|
||||
}
|
||||
|
||||
func (s *Settings) GetFilename() string {
|
||||
@@ -68,6 +126,9 @@ func (s *Settings) lockedSetKeyValue(key, value string) bool {
|
||||
log.Printf("Settings::lockedKeyValue %s: storing key '%s' with value '%s'\n", baseFilename, key, value)
|
||||
}
|
||||
s.contents[strings.Clone(key)] = strings.Clone(value)
|
||||
if updateChannel, found := s.monitorKeys[key]; found {
|
||||
insertOrTimeout(s.context, updateChannel, value)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -132,10 +193,10 @@ func (s *Settings) Update() error {
|
||||
return s.lockedUpdate()
|
||||
}
|
||||
|
||||
func (s *Settings) maintenanceRoutine(ctx context.Context) {
|
||||
func (s *Settings) maintenanceRoutine() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-s.context.Done():
|
||||
return
|
||||
case <-time.After(MaintenanceRoutinePace):
|
||||
err := s.Update()
|
||||
@@ -154,6 +215,7 @@ func NewSettings(filename string, logUpdates bool) *Settings {
|
||||
|
||||
func NewSettingsWithContext(ctx context.Context, filename string, logUpdates bool) *Settings {
|
||||
settings := &Settings{
|
||||
context: ctx,
|
||||
contents: make(map[string]string),
|
||||
filename: strings.Clone(filename),
|
||||
}
|
||||
@@ -161,7 +223,7 @@ func NewSettingsWithContext(ctx context.Context, filename string, logUpdates boo
|
||||
if err := settings.lockedUpdate(); err != nil && (!errors.Is(err, os.ErrNotExist) || WarnIfFileNotFound) {
|
||||
log.Printf("NewSettingsWithContext %s error from initial update: %v\n", filepath.Base(filename), err)
|
||||
}
|
||||
go settings.maintenanceRoutine(ctx)
|
||||
go settings.maintenanceRoutine()
|
||||
return settings
|
||||
}
|
||||
|
||||
|
||||
+160
-6
@@ -2,8 +2,11 @@ package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"os"
|
||||
@@ -12,10 +15,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
TheTestFilename = "settings_test.settings"
|
||||
)
|
||||
|
||||
var (
|
||||
AdditionalKeyValuePairs = map[string]string{
|
||||
"D": "d",
|
||||
@@ -43,8 +42,11 @@ func TestSettings(t *testing.T) {
|
||||
t.Fatalf("error cleaning up %s from this run: %v\n", TheTestFilename, err)
|
||||
}
|
||||
}()
|
||||
settings := NewSettingsWithContext(t.Context(), TheTestFilename, false)
|
||||
if want, got := TheTestFilename, settings.GetFilename(); want != got {
|
||||
settings := TheSettings
|
||||
err = settings.Update()
|
||||
if err != nil {
|
||||
t.Fatalf("error updating settings: %v\n", err)
|
||||
} else if want, got := TheTestFilename, settings.GetFilename(); want != got {
|
||||
t.Fatalf("error: wanted '%s'; got '%s'\n", want, got)
|
||||
} else if got := settings.modtime; got.Equal(time.Time{}) {
|
||||
t.Fatalf("error: modtime == (time.Time{}) - not updated when initialized")
|
||||
@@ -125,3 +127,155 @@ func TestSettings(t *testing.T) {
|
||||
t.Fatalf("error: wanted error indicating 'no such file or directory'; got %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump(t *testing.T) {
|
||||
dump, err := TheSettings.Dump()
|
||||
if err != nil {
|
||||
t.Fatalf("error dumping settings: %v\n", err)
|
||||
}
|
||||
scanner := NewScanner(bytes.NewReader(dump))
|
||||
var found int
|
||||
loop:
|
||||
for {
|
||||
key, value, err := scanner.ReadNextLine()
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("error reading next line: %v\n", err)
|
||||
}
|
||||
break loop
|
||||
}
|
||||
if want, got := TheSettings.GetKeyValue(key), value; want != got {
|
||||
t.Fatalf("error: wanted key %q value %q; got %q\n", key, want, got)
|
||||
}
|
||||
found++
|
||||
}
|
||||
if want, got := len(TheSettings.contents), found; want != got {
|
||||
t.Fatalf("error: wanted %d entries; got %d\n", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDumpJson(t *testing.T) {
|
||||
marshaled, err := TheSettings.DumpToJson()
|
||||
if err != nil {
|
||||
t.Fatalf("error dumping to json: %v\n", err)
|
||||
}
|
||||
var unmarshaled map[string]string
|
||||
err = json.Unmarshal(marshaled, &unmarshaled)
|
||||
if err != nil {
|
||||
t.Fatalf("error unmarshaling json dump: %v\n", err)
|
||||
}
|
||||
if want, got := TheSettings.contents, unmarshaled; !maps.Equal(want, got) {
|
||||
t.Fatalf("error: wanted %v; got %v\n", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetKeyValue(t *testing.T) {
|
||||
key, value := rand.Text(), rand.Text()
|
||||
if !TheSettings.SetKeyValue(key, value) {
|
||||
t.Fatalf("SetKeyValue didn't update?\n")
|
||||
}
|
||||
if want, got := value, TheSettings.GetKeyValue(key); want != got {
|
||||
t.Fatalf("error: wanted %q; got %q\n", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBool(t *testing.T) {
|
||||
TheTestItems := map[string]bool{
|
||||
"yes": true,
|
||||
"on": true,
|
||||
"true": true,
|
||||
"True": true,
|
||||
"TRUE": true,
|
||||
"no": false,
|
||||
"off": false,
|
||||
"false": false,
|
||||
}
|
||||
for boolString, want := range TheTestItems {
|
||||
got, err := ParseBool(boolString)
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v\n", err)
|
||||
}
|
||||
if want != got {
|
||||
t.Fatalf("error: wanted %s=%v; got %v\n", boolString, want, got)
|
||||
}
|
||||
}
|
||||
TheBadTestItems := []string{
|
||||
"blue",
|
||||
"cat",
|
||||
"https://google.com",
|
||||
}
|
||||
for _, boolString := range TheBadTestItems {
|
||||
got, err := ParseBool(boolString)
|
||||
if err == nil {
|
||||
t.Fatalf("error: successfully parsed %s as bool %v...\n", boolString, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStringOrDefault(t *testing.T) {
|
||||
dump, err := TheSettings.Dump()
|
||||
if err != nil {
|
||||
t.Fatalf("error getting dump: %v\n", err)
|
||||
}
|
||||
scanner := NewScanner(bytes.NewReader(dump))
|
||||
loop:
|
||||
for {
|
||||
key, value, err := scanner.ReadNextLine()
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("error reading from dump: %v\n", err)
|
||||
}
|
||||
break loop
|
||||
}
|
||||
if want, got := value, TheSettings.GetStringOrDefault(key, "SomeInvalidDefault"); want != got {
|
||||
t.Fatalf("error: wanted key %q=%q; got %q\n", key, want, got)
|
||||
}
|
||||
}
|
||||
badKey := "some new key"
|
||||
badValue := "some invalid value"
|
||||
if want, got := badValue, TheSettings.GetStringOrDefault(badKey, badValue); want != got {
|
||||
t.Fatalf("error: wanted key %q=%q; got %q\n", badKey, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBoolOrDefault(t *testing.T) {
|
||||
key, value := "some bool value", "on"
|
||||
TheSettings.SetKeyValue(key, value)
|
||||
if want, got := true, TheSettings.GetBoolOrDefault(key, false); want != got {
|
||||
t.Fatalf("error: wanted %q=%v; got %v\n", key, want, got)
|
||||
}
|
||||
value = "shark"
|
||||
TheSettings.SetKeyValue(key, value)
|
||||
if want, got := false, TheSettings.GetBoolOrDefault(key, false); want != got {
|
||||
t.Fatalf("error: wanted %q=%v; got %v\n", key, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDurationOrDefault(t *testing.T) {
|
||||
key, value := "some duration value", "10s"
|
||||
TheSettings.SetKeyValue(key, value)
|
||||
if want, got := time.Second*10, TheSettings.GetDurationOrDefault(key, time.Minute*10); want != got {
|
||||
t.Fatalf("error: wanted %q=%s; got %s\n", key, want, got)
|
||||
}
|
||||
value = "loud"
|
||||
TheSettings.SetKeyValue(key, value)
|
||||
if want, got := time.Minute*10, TheSettings.GetDurationOrDefault(key, time.Minute*10); want != got {
|
||||
t.Fatalf("error: wanted %q=%s; got %s\n", key, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIntOrDefault(t *testing.T) {
|
||||
key, value := "some int value", "10"
|
||||
TheSettings.SetKeyValue(key, value)
|
||||
if want, got := 10, TheSettings.GetIntOrDefault(key, 200); want != got {
|
||||
t.Fatalf("error: wanted %q=%d; got %d\n", key, want, got)
|
||||
}
|
||||
value = "cat"
|
||||
TheSettings.SetKeyValue(key, value)
|
||||
if want, got := 200, TheSettings.GetIntOrDefault(key, 200); want != got {
|
||||
t.Fatalf("error: wanted default value %d for bad key; got %d\n", want, got)
|
||||
}
|
||||
if want, got := 200, TheSettings.GetIntOrDefault("some fake bad key", 200); want != got {
|
||||
t.Fatalf("error: wanted default value %d for bad key; got %d\n", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ValueMonitorParseFunc[T any] func(string) (T, error)
|
||||
|
||||
type ValueMonitor[T any] struct {
|
||||
mutex sync.RWMutex
|
||||
parent *Settings
|
||||
updates <-chan string
|
||||
key string
|
||||
parserFunc ValueMonitorParseFunc[T]
|
||||
value T
|
||||
}
|
||||
|
||||
func (v *ValueMonitor[T]) GetValue() T {
|
||||
v.mutex.RLock()
|
||||
defer v.mutex.RUnlock()
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *ValueMonitor[T]) maintenanceThread() {
|
||||
ctx := v.parent.context
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case s, ok := <-v.updates:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
t, err := v.parserFunc(s)
|
||||
if err != nil {
|
||||
log.Printf("ValueMonitor error parsing key %q value %q from channel: %v\n", v.key, s, err)
|
||||
continue
|
||||
}
|
||||
func() {
|
||||
v.mutex.RLock()
|
||||
defer v.mutex.RUnlock()
|
||||
v.value = t
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewValueMonitor[T any](settings *Settings, key string, parserFunc ValueMonitorParseFunc[T]) (*ValueMonitor[T], error) {
|
||||
monitorChannel := make(chan string, 1)
|
||||
err := settings.RegisterKeyToMonitor(key, monitorChannel)
|
||||
if err != nil {
|
||||
close(monitorChannel)
|
||||
return nil, fmt.Errorf("error registering ValueMonitor %w", err)
|
||||
}
|
||||
initialString, ok := <-monitorChannel
|
||||
if !ok {
|
||||
close(monitorChannel)
|
||||
return nil, errors.New("channel closed before reading initial value")
|
||||
}
|
||||
parsed, err := parserFunc(initialString)
|
||||
if err != nil {
|
||||
close(monitorChannel)
|
||||
return nil, fmt.Errorf("error parsing initial value from %q: %v", initialString, err)
|
||||
}
|
||||
go func() {
|
||||
<-settings.context.Done()
|
||||
close(monitorChannel)
|
||||
}()
|
||||
results := &ValueMonitor[T]{
|
||||
parent: settings,
|
||||
updates: monitorChannel,
|
||||
key: key,
|
||||
parserFunc: parserFunc,
|
||||
value: parsed,
|
||||
}
|
||||
go results.maintenanceThread()
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringValueMonitor(t *testing.T) {
|
||||
key, value := "my key to monitor", "first value"
|
||||
monitor, err := NewValueMonitor(TheSettings, key, func(s string) (string, error) { return value, nil })
|
||||
if err != nil {
|
||||
t.Fatalf("error opening new value monitor: %v\n", err)
|
||||
}
|
||||
if want, got := value, monitor.GetValue(); want != got {
|
||||
t.Fatalf("error: wanted %q; got %q\n", want, got)
|
||||
}
|
||||
value = "second value"
|
||||
TheSettings.SetKeyValue(key, value)
|
||||
if want, got := value, monitor.GetValue(); want != got {
|
||||
t.Fatalf("error: wanted %q; got %q\n", want, got)
|
||||
}
|
||||
value = "third value"
|
||||
f, err := os.OpenFile(TheSettings.GetFilename(), os.O_APPEND|os.O_WRONLY, 0664)
|
||||
if err != nil {
|
||||
t.Fatalf("error opening file writer to append settings: %v\n", err)
|
||||
}
|
||||
_, err = f.WriteString(fmt.Sprintf("\n%s=%s\n", key, value))
|
||||
if err != nil {
|
||||
t.Fatalf("error appending to file: %v\n", err)
|
||||
}
|
||||
err = TheSettings.Update()
|
||||
if err != nil {
|
||||
t.Fatalf("error updating the settings: %v\n", err)
|
||||
}
|
||||
if want, got := value, monitor.GetValue(); want != got {
|
||||
t.Fatalf("error: wanted %q; got %q\n", want, got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user