tests: add locker and math tests

This commit is contained in:
Jon Lundy 2022-08-06 09:52:36 -06:00
parent 189cb5c968
commit f436393965
Signed by untrusted user who does not match committer: xuu
GPG Key ID: C63E6D61F3035024
11 changed files with 268 additions and 94 deletions

7
Makefile Normal file
View File

@ -0,0 +1,7 @@
export EV_DATA = mem:
# export EV_HTTP = :8080
run:
go run .
test:
go test -cover -race ./...

38
main.go
View File

@ -17,7 +17,8 @@ import (
"github.com/sour-is/ev/pkg/es" "github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver" "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" "github.com/sour-is/ev/pkg/es/event"
) )
@ -32,12 +33,15 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
} }
func run(ctx context.Context) error { func run(ctx context.Context) error {
event.Register(&PostEvent{}) if err := event.Register(ctx, &PostEvent{}); err != nil {
diskstore.Init(ctx) 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 { if err != nil {
return err return err
} }
@ -47,7 +51,7 @@ func run(ctx context.Context) error {
} }
s := http.Server{ s := http.Server{
Addr: ":8080", Addr: env("EV_HTTP", ":8080"),
} }
http.HandleFunc("/event/", svc.event) http.HandleFunc("/event/", svc.event)
@ -67,6 +71,13 @@ func run(ctx context.Context) error {
return g.Wait() 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 { type service struct {
es driver.EventStore es driver.EventStore
@ -84,7 +95,8 @@ func (s *service) event(w http.ResponseWriter, r *http.Request) {
return return
} }
if r.Method == http.MethodGet { switch r.Method {
case http.MethodGet:
if name == "" { if name == "" {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
@ -93,14 +105,14 @@ func (s *service) event(w http.ResponseWriter, r *http.Request) {
var pos, count int64 = -1, -99 var pos, count int64 = -1, -99
qry := r.URL.Query() 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 pos = i
} }
if i, err := strconv.ParseInt(qry.Get("n"), 10, 64); err == nil { if i, err := strconv.ParseInt(qry.Get("n"), 10, 64); err == nil {
count = i 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) events, err := s.es.Read(ctx, "post-"+name, pos, count)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@ -113,8 +125,7 @@ func (s *service) event(w http.ResponseWriter, r *http.Request) {
} }
return return
} case http.MethodPost, http.MethodPut:
b, err := io.ReadAll(io.LimitReader(r.Body, 64*1024)) b, err := io.ReadAll(io.LimitReader(r.Body, 64*1024))
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@ -128,7 +139,6 @@ func (s *service) event(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Print(name, tags)
events := event.NewEvents(&PostEvent{ events := event.NewEvents(&PostEvent{
Payload: b, Payload: b,
Tags: strings.Split(tags, "/"), Tags: strings.Split(tags, "/"),
@ -143,7 +153,11 @@ func (s *service) event(w http.ResponseWriter, r *http.Request) {
m := events.First().EventMeta() m := events.First().EventMeta()
w.WriteHeader(http.StatusAccepted) 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) fmt.Fprintf(w, "OK %d %s", m.Position, m.EventID)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
} }
type PostEvent struct { type PostEvent struct {

View File

@ -21,8 +21,9 @@ type diskStore struct {
var _ driver.Driver = (*diskStore)(nil) var _ driver.Driver = (*diskStore)(nil)
func Init(ctx context.Context) { func Init(ctx context.Context) error {
es.Register(ctx, "file", &diskStore{}) es.Register(ctx, "file", &diskStore{})
return nil
} }
func (diskStore) Open(dsn string) (driver.EventStore, error) { 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 { if err != nil {
return err return err
} }
events[i], err = event.UnmarshalText(b, first+i) events[i], err = event.UnmarshalText(ctx, b, first+i)
if err != nil { if err != nil {
return err return err
} }
@ -193,7 +194,7 @@ func (es *diskStore) Read(ctx context.Context, streamID string, pos, count int64
if err != nil { if err != nil {
return events, err return events, err
} }
events[i], err = event.UnmarshalText(b, start) events[i], err = event.UnmarshalText(ctx, b, start)
if err != nil { if err != nil {
return events, err return events, err
} }

View File

@ -15,11 +15,11 @@ type config struct {
} }
var ( 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) { func Register(ctx context.Context, name string, d driver.Driver) error {
Config.Modify(ctx, func(c *config) error { return drivers.Modify(ctx, func(c *config) error {
if _, set := c.drivers[name]; set { if _, set := c.drivers[name]; set {
return fmt.Errorf("driver %s already set", name) 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 var d driver.Driver
Config.Modify(ctx,func(c *config) error { drivers.Modify(ctx,func(c *config) error {
var ok bool var ok bool
d, ok = c.drivers[name] d, ok = c.drivers[name]
if !ok { if !ok {

View File

@ -6,47 +6,42 @@ import (
"testing" "testing"
"time" "time"
"github.com/matryer/is"
"github.com/sour-is/ev/pkg/es" "github.com/sour-is/ev/pkg/es"
memstore "github.com/sour-is/ev/pkg/es/driver/mem-store" memstore "github.com/sour-is/ev/pkg/es/driver/mem-store"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
) )
func TestES(t *testing.T) { func TestES(t *testing.T) {
is := is.New(t)
ctx := context.Background() ctx := context.Background()
event.Register(&ValueSet{}) err := event.Register(ctx, &ValueSet{})
is.NoErr(err)
memstore.Init(ctx) memstore.Init(ctx)
es, err := es.Open(ctx, "mem:") es, err := es.Open(ctx, "mem:")
if err != nil { is.NoErr(err)
t.Fatal(err)
}
thing := &Thing{Name: "time"} thing := &Thing{Name: "time"}
err = es.Load(ctx, thing) err = es.Load(ctx, thing)
if err != nil { is.NoErr(err)
t.Fatal(err)
}
t.Log(thing.StreamVersion(), thing.Name, thing.Value) t.Log(thing.StreamVersion(), thing.Name, thing.Value)
err = thing.OnSetValue(time.Now().String()) err = thing.OnSetValue(time.Now().String())
if err != nil { is.NoErr(err)
t.Fatal(err)
}
i, err := es.Save(ctx, thing) i, err := es.Save(ctx, thing)
if err != nil { is.NoErr(err)
t.Fatal(err)
}
t.Log(thing.StreamVersion(), thing.Name, thing.Value) t.Log(thing.StreamVersion(), thing.Name, thing.Value)
t.Log("Wrote: ", i) t.Log("Wrote: ", i)
events, err := es.Read(ctx, "thing-time", -1, -11) events, err := es.Read(ctx, "thing-time", -1, -11)
if err != nil { is.NoErr(err)
t.Fatal(err)
}
for i, e := range events { for i, e := range events {
t.Logf("event %d %d - %v\n", i, e.EventMeta().Position, e) t.Logf("event %d %d - %v\n", i, e.EventMeta().Position, e)

View File

@ -2,6 +2,7 @@ package event_test
import ( import (
"bytes" "bytes"
"context"
"testing" "testing"
"github.com/matryer/is" "github.com/matryer/is"
@ -30,8 +31,10 @@ func (e *DummyEvent) SetEventMeta(eventMeta event.Meta) {
func TestEventEncode(t *testing.T) { func TestEventEncode(t *testing.T) {
is := is.New(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( var lis event.Events = event.NewEvents(
&DummyEvent{Value: "testA"}, &DummyEvent{Value: "testA"},
@ -50,7 +53,7 @@ func TestEventEncode(t *testing.T) {
is.Equal(string(sp[2]), "event_test.DummyEvent") is.Equal(string(sp[2]), "event_test.DummyEvent")
} }
chk, err := event.DecodeEvents(blis...) chk, err := event.DecodeEvents(ctx, blis...)
is.NoErr(err) is.NoErr(err)
for i := range chk { for i := range chk {

View File

@ -2,6 +2,7 @@ package event
import ( import (
"bytes" "bytes"
"context"
"encoding" "encoding"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -9,11 +10,16 @@ import (
"net/url" "net/url"
"reflect" "reflect"
"strings" "strings"
"sync"
"github.com/sour-is/ev/pkg/locker"
) )
type config struct {
eventTypes map[string]reflect.Type
}
var ( var (
eventTypes sync.Map eventTypes = locker.New(&config{eventTypes: make(map[string]reflect.Type)})
) )
type UnknownEvent struct { 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. // 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 { for _, e := range lis {
if err := ctx.Err(); err != nil {
return err
}
if e == nil { 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) value := reflect.ValueOf(e)
if value.IsNil() { 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) value = reflect.Indirect(value)
name := TypeOf(e)
typ := value.Type() typ := value.Type()
eventTypes.LoadOrStore(TypeOf(e), typ) if err := eventTypes.Modify(ctx, func(c *config) error {
c.eventTypes[name] = typ
return nil
}); err != nil {
return err
} }
}
return nil
} }
func GetContainer(s string) Event { func GetContainer(ctx context.Context, s string) Event {
if typ, ok := eventTypes.Load(s); ok { var e Event
if typ, ok := typ.(reflect.Type); ok {
eventTypes.Modify(ctx, func(c *config) error {
typ, ok := c.eventTypes[s]
if !ok {
return fmt.Errorf("not defined")
}
newType := reflect.New(typ) newType := reflect.New(typ)
newInterface := newType.Interface() newInterface := newType.Interface()
if typ, ok := newInterface.(Event); ok { if iface, ok := newInterface.(Event); ok {
return typ e = iface
return nil
} }
return fmt.Errorf("failed")
})
if e == nil {
e = &UnknownEvent{eventType: s}
} }
}
return &UnknownEvent{eventType: s} return e
} }
func MarshalText(e Event) (txt []byte, err error) { func MarshalText(e Event) (txt []byte, err error) {
@ -123,7 +150,7 @@ func MarshalText(e Event) (txt []byte, err error) {
return b.Bytes(), err 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) sp := bytes.SplitN(txt, []byte{'\t'}, 4)
if len(sp) != 4 { if len(sp) != 4 {
return nil, fmt.Errorf("invalid format. expected=4, got=%d", len(sp)) 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 m.Position = pos
eventType := string(sp[2]) eventType := string(sp[2])
e = GetContainer(eventType) e = GetContainer(ctx, eventType)
if enc, ok := e.(encoding.TextUnmarshaler); ok { if enc, ok := e.(encoding.TextUnmarshaler); ok {
if err = enc.UnmarshalText(sp[3]); err != nil { 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. // 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)) elis := make([]Event, len(lis))
var err error var err error
for i, txt := range lis { for i, txt := range lis {
elis[i], err = UnmarshalText(txt, uint64(i)) elis[i], err = UnmarshalText(ctx, txt, uint64(i))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -26,3 +26,16 @@ func (s *Locked[T]) Modify(ctx context.Context, fn func(*T) error) error {
return ctx.Err() return ctx.Err()
} }
} }
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
}

62
pkg/locker/locker_test.go Normal file
View File

@ -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)
}

View File

@ -22,15 +22,19 @@ func Abs[T signed](i T) T {
} }
return -i return -i
} }
func Max[T ordered](i, j T) T { func Max[T ordered](i T, candidates ...T) T {
if i > j { for _, j := range candidates {
return i
}
return j
}
func Min[T ordered](i, j T) T {
if i < j { if i < j {
return i i = j
} }
return j }
return i
}
func Min[T ordered](i T, candidates ...T) T {
for _, j := range candidates {
if i > j {
i = j
}
}
return i
} }

48
pkg/math/math_test.go Normal file
View File

@ -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,
))
}