From f4363939650b871af5cc37a9bb0d074e1094cb0a Mon Sep 17 00:00:00 2001 From: Jon Lundy Date: Sat, 6 Aug 2022 09:52:36 -0600 Subject: [PATCH] tests: add locker and math tests --- Makefile | 7 ++ main.go | 92 +++++++++++++++----------- pkg/es/driver/disk-store/disk-store.go | 7 +- pkg/es/es.go | 8 +-- pkg/es/es_test.go | 27 +++----- pkg/es/event/events_test.go | 7 +- pkg/es/event/reflect.go | 69 +++++++++++++------ pkg/locker/locker.go | 15 ++++- pkg/locker/locker_test.go | 62 +++++++++++++++++ pkg/math/math.go | 20 +++--- pkg/math/math_test.go | 48 ++++++++++++++ 11 files changed, 268 insertions(+), 94 deletions(-) create mode 100644 Makefile create mode 100644 pkg/locker/locker_test.go create mode 100644 pkg/math/math_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1aedcc1 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +export EV_DATA = mem: +# export EV_HTTP = :8080 + +run: + go run . +test: + go test -cover -race ./... \ No newline at end of file diff --git a/main.go b/main.go index d9be497..c108df0 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,8 @@ import ( "github.com/sour-is/ev/pkg/es" "github.com/sour-is/ev/pkg/es/driver" - "github.com/sour-is/ev/pkg/es/driver/disk-store" + diskstore "github.com/sour-is/ev/pkg/es/driver/disk-store" + memstore "github.com/sour-is/ev/pkg/es/driver/mem-store" "github.com/sour-is/ev/pkg/es/event" ) @@ -32,12 +33,15 @@ func main() { log.Fatal(err) } } - func run(ctx context.Context) error { - event.Register(&PostEvent{}) - diskstore.Init(ctx) + if err := event.Register(ctx, &PostEvent{}); err != nil { + return err + } - es, err := es.Open(ctx, "file:data") + diskstore.Init(ctx) + memstore.Init(ctx) + + es, err := es.Open(ctx, env("EV_DATA", "file:data")) if err != nil { return err } @@ -47,7 +51,7 @@ func run(ctx context.Context) error { } s := http.Server{ - Addr: ":8080", + Addr: env("EV_HTTP", ":8080"), } http.HandleFunc("/event/", svc.event) @@ -67,6 +71,13 @@ func run(ctx context.Context) error { return g.Wait() } +func env(name, defaultValue string) string { + if v := os.Getenv(name); v != "" { + log.Println("# ", name, " = ", v) + return v + } + return defaultValue +} type service struct { es driver.EventStore @@ -84,7 +95,8 @@ func (s *service) event(w http.ResponseWriter, r *http.Request) { return } - if r.Method == http.MethodGet { + switch r.Method { + case http.MethodGet: if name == "" { w.WriteHeader(http.StatusNotFound) return @@ -93,14 +105,14 @@ func (s *service) event(w http.ResponseWriter, r *http.Request) { var pos, count int64 = -1, -99 qry := r.URL.Query() - if i, err := strconv.ParseInt(qry.Get("pos"), 10, 64); err == nil { + if i, err := strconv.ParseInt(qry.Get("idx"), 10, 64); err == nil { pos = i } if i, err := strconv.ParseInt(qry.Get("n"), 10, 64); err == nil { count = i } - log.Print("name=", name, ", pos=", pos, ", n=", count) + log.Print("GET topic=", name, " idx=", pos, " n=", count) events, err := s.es.Read(ctx, "post-"+name, pos, count) if err != nil { log.Print(err) @@ -113,37 +125,39 @@ func (s *service) event(w http.ResponseWriter, r *http.Request) { } return + case http.MethodPost, http.MethodPut: + b, err := io.ReadAll(io.LimitReader(r.Body, 64*1024)) + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + return + } + r.Body.Close() + + if name == "" { + w.WriteHeader(http.StatusNotFound) + return + } + + events := event.NewEvents(&PostEvent{ + Payload: b, + Tags: strings.Split(tags, "/"), + }) + _, err = s.es.Append(r.Context(), "post-"+name, events) + if err != nil { + log.Print(err) + + w.WriteHeader(http.StatusInternalServerError) + return + } + + m := events.First().EventMeta() + w.WriteHeader(http.StatusAccepted) + log.Print("POST topic=", name, " tags=", tags, " idx=", m.Position, " id=", m.EventID) + fmt.Fprintf(w, "OK %d %s", m.Position, m.EventID) + default: + w.WriteHeader(http.StatusMethodNotAllowed) } - - b, err := io.ReadAll(io.LimitReader(r.Body, 64*1024)) - if err != nil { - log.Print(err) - w.WriteHeader(http.StatusBadRequest) - return - } - r.Body.Close() - - if name == "" { - w.WriteHeader(http.StatusNotFound) - return - } - - log.Print(name, tags) - events := event.NewEvents(&PostEvent{ - Payload: b, - Tags: strings.Split(tags, "/"), - }) - _, err = s.es.Append(r.Context(), "post-"+name, events) - if err != nil { - log.Print(err) - - w.WriteHeader(http.StatusInternalServerError) - return - } - - m := events.First().EventMeta() - w.WriteHeader(http.StatusAccepted) - fmt.Fprintf(w, "OK %d %s", m.Position, m.EventID) } type PostEvent struct { diff --git a/pkg/es/driver/disk-store/disk-store.go b/pkg/es/driver/disk-store/disk-store.go index b50d8bb..ac15d59 100644 --- a/pkg/es/driver/disk-store/disk-store.go +++ b/pkg/es/driver/disk-store/disk-store.go @@ -21,8 +21,9 @@ type diskStore struct { var _ driver.Driver = (*diskStore)(nil) -func Init(ctx context.Context) { +func Init(ctx context.Context) error { es.Register(ctx, "file", &diskStore{}) + return nil } func (diskStore) Open(dsn string) (driver.EventStore, error) { @@ -143,7 +144,7 @@ func (es *diskStore) Load(ctx context.Context, agg event.Aggregate) error { if err != nil { return err } - events[i], err = event.UnmarshalText(b, first+i) + events[i], err = event.UnmarshalText(ctx, b, first+i) if err != nil { return err } @@ -193,7 +194,7 @@ func (es *diskStore) Read(ctx context.Context, streamID string, pos, count int64 if err != nil { return events, err } - events[i], err = event.UnmarshalText(b, start) + events[i], err = event.UnmarshalText(ctx, b, start) if err != nil { return events, err } diff --git a/pkg/es/es.go b/pkg/es/es.go index aa8267f..0acb99a 100644 --- a/pkg/es/es.go +++ b/pkg/es/es.go @@ -15,11 +15,11 @@ type config struct { } var ( - Config = locker.New(&config{drivers: make(map[string]driver.Driver)}) + drivers = locker.New(&config{drivers: make(map[string]driver.Driver)}) ) -func Register(ctx context.Context, name string, d driver.Driver) { - Config.Modify(ctx, func(c *config) error { +func Register(ctx context.Context, name string, d driver.Driver) error { + return drivers.Modify(ctx, func(c *config) error { if _, set := c.drivers[name]; set { return fmt.Errorf("driver %s already set", name) } @@ -35,7 +35,7 @@ func Open(ctx context.Context, dsn string) (driver.EventStore, error) { } var d driver.Driver - Config.Modify(ctx,func(c *config) error { + drivers.Modify(ctx,func(c *config) error { var ok bool d, ok = c.drivers[name] if !ok { diff --git a/pkg/es/es_test.go b/pkg/es/es_test.go index a95c6ee..49f569f 100644 --- a/pkg/es/es_test.go +++ b/pkg/es/es_test.go @@ -6,47 +6,42 @@ import ( "testing" "time" + "github.com/matryer/is" + "github.com/sour-is/ev/pkg/es" memstore "github.com/sour-is/ev/pkg/es/driver/mem-store" "github.com/sour-is/ev/pkg/es/event" ) func TestES(t *testing.T) { + is := is.New(t) ctx := context.Background() - event.Register(&ValueSet{}) + err := event.Register(ctx, &ValueSet{}) + is.NoErr(err) memstore.Init(ctx) es, err := es.Open(ctx, "mem:") - if err != nil { - t.Fatal(err) - } + is.NoErr(err) thing := &Thing{Name: "time"} err = es.Load(ctx, thing) - if err != nil { - t.Fatal(err) - } + is.NoErr(err) + t.Log(thing.StreamVersion(), thing.Name, thing.Value) err = thing.OnSetValue(time.Now().String()) - if err != nil { - t.Fatal(err) - } + is.NoErr(err) i, err := es.Save(ctx, thing) - if err != nil { - t.Fatal(err) - } + is.NoErr(err) t.Log(thing.StreamVersion(), thing.Name, thing.Value) t.Log("Wrote: ", i) events, err := es.Read(ctx, "thing-time", -1, -11) - if err != nil { - t.Fatal(err) - } + is.NoErr(err) for i, e := range events { t.Logf("event %d %d - %v\n", i, e.EventMeta().Position, e) diff --git a/pkg/es/event/events_test.go b/pkg/es/event/events_test.go index 0935ab4..691e2f7 100644 --- a/pkg/es/event/events_test.go +++ b/pkg/es/event/events_test.go @@ -2,6 +2,7 @@ package event_test import ( "bytes" + "context" "testing" "github.com/matryer/is" @@ -30,8 +31,10 @@ func (e *DummyEvent) SetEventMeta(eventMeta event.Meta) { func TestEventEncode(t *testing.T) { is := is.New(t) + ctx := context.Background() - event.Register(&DummyEvent{}) + err := event.Register(ctx, &DummyEvent{}) + is.NoErr(err) var lis event.Events = event.NewEvents( &DummyEvent{Value: "testA"}, @@ -50,7 +53,7 @@ func TestEventEncode(t *testing.T) { is.Equal(string(sp[2]), "event_test.DummyEvent") } - chk, err := event.DecodeEvents(blis...) + chk, err := event.DecodeEvents(ctx, blis...) is.NoErr(err) for i := range chk { diff --git a/pkg/es/event/reflect.go b/pkg/es/event/reflect.go index 1bb33d7..014f042 100644 --- a/pkg/es/event/reflect.go +++ b/pkg/es/event/reflect.go @@ -2,6 +2,7 @@ package event import ( "bytes" + "context" "encoding" "encoding/json" "fmt" @@ -9,11 +10,16 @@ import ( "net/url" "reflect" "strings" - "sync" + + "github.com/sour-is/ev/pkg/locker" ) +type config struct { + eventTypes map[string]reflect.Type +} + var ( - eventTypes sync.Map + eventTypes = locker.New(&config{eventTypes: make(map[string]reflect.Type)}) ) type UnknownEvent struct { @@ -63,35 +69,56 @@ func (u *UnknownEvent) MarshalJSON() ([]byte, error) { } // Register a type container for Unmarshalling values into. The type must implement Event and not be a nil value. -func Register(lis ...Event) { +func Register(ctx context.Context, lis ...Event) error { for _, e := range lis { + if err := ctx.Err(); err != nil { + return err + } if e == nil { - panic(fmt.Sprintf("can't register event.Event of type=%T with value=%v", e, e)) + return fmt.Errorf("can't register event.Event of type=%T with value=%v", e, e) } value := reflect.ValueOf(e) if value.IsNil() { - panic(fmt.Sprintf("can't register event.Event of type=%T with value=%v", e, e)) + return fmt.Errorf("can't register event.Event of type=%T with value=%v", e, e) } value = reflect.Indirect(value) + + name := TypeOf(e) typ := value.Type() - eventTypes.LoadOrStore(TypeOf(e), typ) - } -} -func GetContainer(s string) Event { - if typ, ok := eventTypes.Load(s); ok { - if typ, ok := typ.(reflect.Type); ok { - newType := reflect.New(typ) - newInterface := newType.Interface() - if typ, ok := newInterface.(Event); ok { - return typ - } + if err := eventTypes.Modify(ctx, func(c *config) error { + c.eventTypes[name] = typ + return nil + }); err != nil { + return err } } - return &UnknownEvent{eventType: s} + return nil +} +func GetContainer(ctx context.Context, s string) Event { + var e Event + + eventTypes.Modify(ctx, func(c *config) error { + typ, ok := c.eventTypes[s] + if !ok { + return fmt.Errorf("not defined") + } + newType := reflect.New(typ) + newInterface := newType.Interface() + if iface, ok := newInterface.(Event); ok { + e = iface + return nil + } + return fmt.Errorf("failed") + }) + if e == nil { + e = &UnknownEvent{eventType: s} + } + + return e } func MarshalText(e Event) (txt []byte, err error) { @@ -123,7 +150,7 @@ func MarshalText(e Event) (txt []byte, err error) { return b.Bytes(), err } -func UnmarshalText(txt []byte, pos uint64) (e Event, err error) { +func UnmarshalText(ctx context.Context, txt []byte, pos uint64) (e Event, err error) { sp := bytes.SplitN(txt, []byte{'\t'}, 4) if len(sp) != 4 { return nil, fmt.Errorf("invalid format. expected=4, got=%d", len(sp)) @@ -138,7 +165,7 @@ func UnmarshalText(txt []byte, pos uint64) (e Event, err error) { m.Position = pos eventType := string(sp[2]) - e = GetContainer(eventType) + e = GetContainer(ctx, eventType) if enc, ok := e.(encoding.TextUnmarshaler); ok { if err = enc.UnmarshalText(sp[3]); err != nil { @@ -163,12 +190,12 @@ func writeMarshaler(out io.Writer, in encoding.TextMarshaler) (int, error) { } // DecodeEvents unmarshals the byte list into Events. -func DecodeEvents(lis ...[]byte) (Events, error) { +func DecodeEvents(ctx context.Context, lis ...[]byte) (Events, error) { elis := make([]Event, len(lis)) var err error for i, txt := range lis { - elis[i], err = UnmarshalText(txt, uint64(i)) + elis[i], err = UnmarshalText(ctx, txt, uint64(i)) if err != nil { return nil, err } diff --git a/pkg/locker/locker.go b/pkg/locker/locker.go index e467b67..85a9e37 100644 --- a/pkg/locker/locker.go +++ b/pkg/locker/locker.go @@ -25,4 +25,17 @@ func (s *Locked[T]) Modify(ctx context.Context, fn func(*T) error) error { case <-ctx.Done(): return ctx.Err() } -} \ No newline at end of file +} + +func (s *Locked[T]) Copy(ctx context.Context) (T, error) { + var t T + + err := s.Modify(ctx, func(c *T) error { + if c != nil { + t = *c + } + return nil + }) + + return t, err +} diff --git a/pkg/locker/locker_test.go b/pkg/locker/locker_test.go new file mode 100644 index 0000000..bad312b --- /dev/null +++ b/pkg/locker/locker_test.go @@ -0,0 +1,62 @@ +package locker_test + +import ( + "context" + "testing" + + "github.com/matryer/is" + + "github.com/sour-is/ev/pkg/locker" +) + +type config struct { + Value string + Counter int +} + +func TestLocker(t *testing.T) { + is := is.New(t) + + value := locker.New(&config{}) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := value.Modify(ctx, func(c *config) error { + c.Value = "one" + c.Counter++ + return nil + }) + is.NoErr(err) + + c, err := value.Copy(context.Background()) + + is.NoErr(err) + is.Equal(c.Value, "one") + is.Equal(c.Counter, 1) + + wait := make(chan struct{}) + + go value.Modify(ctx, func(c *config) error { + c.Value = "two" + c.Counter++ + close(wait) + return nil + }) + + <-wait + cancel() + + err = value.Modify(ctx, func(c *config) error { + c.Value = "three" + c.Counter++ + return nil + }) + is.True(err != nil) + + c, err = value.Copy(context.Background()) + + is.NoErr(err) + is.Equal(c.Value, "two") + is.Equal(c.Counter, 2) +} diff --git a/pkg/math/math.go b/pkg/math/math.go index 3566347..7e20efd 100644 --- a/pkg/math/math.go +++ b/pkg/math/math.go @@ -22,15 +22,19 @@ func Abs[T signed](i T) T { } return -i } -func Max[T ordered](i, j T) T { - if i > j { - return i +func Max[T ordered](i T, candidates ...T) T { + for _, j := range candidates { + if i < j { + i = j + } } - return j + return i } -func Min[T ordered](i, j T) T { - if i < j { - return i +func Min[T ordered](i T, candidates ...T) T { + for _, j := range candidates { + if i > j { + i = j + } } - return j + return i } diff --git a/pkg/math/math_test.go b/pkg/math/math_test.go new file mode 100644 index 0000000..3c183a6 --- /dev/null +++ b/pkg/math/math_test.go @@ -0,0 +1,48 @@ +package math_test + +import ( + "testing" + + "github.com/matryer/is" + "github.com/sour-is/ev/pkg/math" +) + +func TestMath(t *testing.T) { + is := is.New(t) + + is.Equal(5, math.Abs(-5)) + is.Equal(math.Abs(5), math.Abs(-5)) + + is.Equal(10, math.Max(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + is.Equal(1, math.Min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + + is.Equal(1, math.Min(89, 71, 54, 48, 49, 1, 72, 88, 25, 69)) + is.Equal(89, math.Max(89, 71, 54, 48, 49, 1, 72, 88, 25, 69)) + + is.Equal(0.9348207729, math.Max( + 0.3943310720, + 0.1090868377, + 0.9348207729, + 0.3525527584, + 0.4359833682, + 0.7958538081, + 0.1439352569, + 0.1547311967, + 0.6403818871, + 0.8618832818, + )) + + is.Equal(0.1090868377, math.Min( + 0.3943310720, + 0.1090868377, + 0.9348207729, + 0.3525527584, + 0.4359833682, + 0.7958538081, + 0.1439352569, + 0.1547311967, + 0.6403818871, + 0.8618832818, + )) + +}