From 95616e0fb8d28b581c11ba5e7130ea886c50479b Mon Sep 17 00:00:00 2001 From: cws Date: Mon, 5 Jun 2017 18:37:46 -0400 Subject: [PATCH 1/4] stats: WIP demo of stats * Added a plugin for stats * Using BoltDB for ease of not screwing with SQL ** Perhaps everything should be Bolt... * Should be relatively easy to add new stat counters: ** Create a function that makes the stat{} struct ** Add it to the list of active counters * Should be relatively easy to add a views page later --- config/config.go | 4 + main.go | 5 +- plugins/stats/stats.go | 216 ++++++++++++++++++++++++++++++++++++ plugins/stats/stats_test.go | 177 +++++++++++++++++++++++++++++ 4 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 plugins/stats/stats.go create mode 100644 plugins/stats/stats_test.go diff --git a/config/config.go b/config/config.go index eafa002..7a0553d 100644 --- a/config/config.go +++ b/config/config.go @@ -75,6 +75,10 @@ type Config struct { Reminder struct { MaxBatchAdd int } + Stats struct { + DBPath string + Sightings []string + } } // Readconfig loads the config data out of a JSON file located in cfile diff --git a/main.go b/main.go index f542e30..ad3c375 100644 --- a/main.go +++ b/main.go @@ -14,11 +14,11 @@ import ( "github.com/velour/catbase/plugins/beers" "github.com/velour/catbase/plugins/counter" "github.com/velour/catbase/plugins/dice" - "github.com/velour/catbase/plugins/downtime" "github.com/velour/catbase/plugins/fact" "github.com/velour/catbase/plugins/leftpad" "github.com/velour/catbase/plugins/reminder" "github.com/velour/catbase/plugins/rss" + "github.com/velour/catbase/plugins/stats" "github.com/velour/catbase/plugins/talker" "github.com/velour/catbase/plugins/your" "github.com/velour/catbase/plugins/zork" @@ -46,9 +46,10 @@ func main() { // b.AddHandler(plugins.NewTestPlugin(b)) b.AddHandler("admin", admin.New(b)) + b.AddHandler("stats", stats.New(b)) // b.AddHandler("first", plugins.NewFirstPlugin(b)) b.AddHandler("leftpad", leftpad.New(b)) - b.AddHandler("downtime", downtime.New(b)) + // b.AddHandler("downtime", downtime.New(b)) b.AddHandler("talker", talker.New(b)) b.AddHandler("dice", dice.New(b)) b.AddHandler("beers", beers.New(b)) diff --git a/plugins/stats/stats.go b/plugins/stats/stats.go new file mode 100644 index 0000000..33d37ec --- /dev/null +++ b/plugins/stats/stats.go @@ -0,0 +1,216 @@ +// © 2016 the CatBase Authors under the WTFPL license. See AUTHORS for the list of authors. + +// Stats contains the plugin that allows the bot record chat statistics +package stats + +import ( + "bytes" + "encoding/gob" + "log" + "strings" + "time" + + "github.com/boltdb/bolt" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/config" +) + +type StatsPlugin struct { + bot bot.Bot + config *config.Config +} + +// New creates a new StatsPlugin with the Plugin interface +func New(bot bot.Bot) *StatsPlugin { + p := StatsPlugin{ + bot: bot, + config: bot.Config(), + } + return &p + +} + +type stat struct { + bucket string + key string + val value +} + +// The value type is here in the future growth case that we might want to put a +// struct of more interesting information into the DB +type value int + +func (v value) Bytes() ([]byte, error) { + var b bytes.Buffer + enc := gob.NewEncoder(&b) + err := enc.Encode(v) + return b.Bytes(), err +} + +func valueFromBytes(b []byte) (value, error) { + var v value + buf := bytes.NewReader(b) + dec := gob.NewDecoder(buf) + err := dec.Decode(&v) + return v, err +} + +type stats []stat + +func mkStat(bucket, key, val []byte) (stat, error) { + v, err := valueFromBytes(val) + if err != nil { + log.Printf("mkStat: error getting value from bytes: %s", err) + return stat{}, err + } + return stat{ + bucket: string(bucket), + key: string(key), + val: v, + }, nil +} + +// Another future-proofing function I shouldn't have written +func (v value) add(other value) value { + return v + other +} + +func statFromDB(path, bucket, key string) (stat, error) { + db, err := bolt.Open(path, 0600, &bolt.Options{ + Timeout: 1 * time.Second, + }) + buk := []byte(bucket) + k := []byte(key) + if err != nil { + log.Printf("Couldn't open BoltDB for stats: %s", err) + return stat{}, err + } + defer db.Close() + + tx, err := db.Begin(true) + if err != nil { + log.Println("statFromDB: Error beginning the Tx") + return stat{}, err + } + defer tx.Rollback() + + b, err := tx.CreateBucketIfNotExists(buk) + if err != nil { + log.Println("statFromDB: Error creating the bucket") + return stat{}, err + } + v := b.Get(k) + + if err := tx.Commit(); err != nil { + log.Println("statFromDB: Error commiting the Tx") + return stat{}, err + } + + return mkStat(buk, k, v) +} + +func (s stats) toDB(path string) error { + db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + log.Printf("Couldn't open BoltDB for stats: %s", err) + return err + } + defer db.Close() + + for _, stat := range s { + err = db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte(stat.bucket)) + if err != nil { + log.Println("toDB: Error creating bucket") + return err + } + + valueInDB := b.Get([]byte(stat.key)) + if valueInDB != nil { + val, err := valueFromBytes(valueInDB) + if err != nil { + log.Println("toDB: Error getting value from bytes") + return err + } + stat.val = stat.val.add(val) + } + + v, err := stat.val.Bytes() + if err != nil { + return err + } + err = b.Put([]byte(stat.key), v) + return err + }) + if err != nil { + return err + } + } + return nil +} + +func (p *StatsPlugin) record(message msg.Message) { + statGenerators := []func(msg.Message) stats{ + p.mkUserStat, + p.mkHourStat, + p.mkChannelStat, + p.mkSightingStat, + } + + allStats := stats{} + + for _, mk := range statGenerators { + stats := mk(message) + if stats != nil { + allStats = append(allStats, stats...) + } + } + + allStats.toDB(p.bot.Config().Stats.DBPath) +} + +func (p *StatsPlugin) Message(message msg.Message) bool { + p.record(message) + return false +} + +func (p *StatsPlugin) Event(e string, message msg.Message) bool { + p.record(message) + return false +} + +func (p *StatsPlugin) BotMessage(message msg.Message) bool { + p.record(message) + return false +} + +func (p *StatsPlugin) Help(e string, m []string) { +} + +func (p *StatsPlugin) RegisterWeb() *string { + return nil +} + +func (p *StatsPlugin) mkUserStat(message msg.Message) stats { + return stats{stat{"user", message.User.Name, 1}} +} + +func (p *StatsPlugin) mkHourStat(message msg.Message) stats { + hr := time.Now().Hour() + return stats{stat{"user", string(hr), 1}} +} + +func (p *StatsPlugin) mkSightingStat(message msg.Message) stats { + stats := stats{} + for _, name := range p.bot.Config().Stats.Sightings { + if strings.Contains(message.Body, name+" sighting") { + stats = append(stats, stat{"sighting", name, 1}) + } + } + return stats +} + +func (p *StatsPlugin) mkChannelStat(message msg.Message) stats { + return stats{stat{"channel", message.Channel, 1}} +} diff --git a/plugins/stats/stats_test.go b/plugins/stats/stats_test.go new file mode 100644 index 0000000..5d801c5 --- /dev/null +++ b/plugins/stats/stats_test.go @@ -0,0 +1,177 @@ +package stats + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "testing" +) + +func TestJSON(t *testing.T) { + expected := 5 + b, err := json.Marshal(expected) + if err != nil { + t.Error(err) + } + t.Logf("%+v", expected) + t.Log(string(b)) +} + +func TestValueConversion(t *testing.T) { + expected := value(5) + + b, err := expected.Bytes() + if err != nil { + t.Error(err) + return + } + + t.Log(string(b)) + + actual, err := valueFromBytes(b) + if err != nil { + t.Error(err) + return + } + + if fmt.Sprintf("%+v", actual) != fmt.Sprintf("%+v", expected) { + t.Errorf("Did not get equivalent objects: %+v != %+v", actual, expected) + } +} + +func rmDB(t *testing.T, dbPath string) { + err := os.Remove(dbPath) + if err != nil && !strings.Contains(err.Error(), "no such file or directory") { + t.Fatal(err) + } +} + +func TestWithDB(t *testing.T) { + dbPath := "test.db" + rmDB(t, dbPath) + + t.Run("TestDBReadWrite", func(t *testing.T) { + bucket := "testBucket" + key := "testKey" + + expected := stats{stat{ + bucket, + key, + 1, + }} + + err := expected.toDB(dbPath) + if err != nil { + t.Fatalf("Error writing to DB: %s", err) + } + + actual, err := statFromDB(dbPath, bucket, key) + if err != nil { + t.Fatalf("Error reading DB: %s", err) + } + + if actual != expected[0] { + t.Fatalf("%+v != %+v", actual, expected) + } + + }) + + rmDB(t, dbPath) + + t.Run("TestDBAddStatInLoop", func(t *testing.T) { + bucket := "testBucket" + key := "testKey" + expected := value(25) + + os.Remove(dbPath) + defer func() { + err := os.Remove(dbPath) + if err != nil { + t.Fatal(err) + } + }() + + statPack := stats{stat{ + bucket, + key, + 5, + }} + + for i := 0; i < 5; i++ { + err := statPack.toDB(dbPath) + if err != nil { + t.Fatalf("Error writing to DB: %s", err) + } + } + + actual, err := statFromDB(dbPath, bucket, key) + if err != nil { + t.Fatalf("Error reading DB: %s", err) + } + + if actual.val != expected { + t.Fatalf("%+v != %+v", actual.val, expected) + } + }) + + rmDB(t, dbPath) + + t.Run("TestDBAddStats", func(t *testing.T) { + bucket := "testBucket" + key := "testKey" + expected := value(5) + + os.Remove(dbPath) + defer func() { + err := os.Remove(dbPath) + if err != nil { + t.Fatal(err) + } + }() + + statPack := stats{ + stat{ + bucket, + key, + 1, + }, + stat{ + bucket, + key, + 1, + }, + stat{ + bucket, + key, + 1, + }, + stat{ + bucket, + key, + 1, + }, + stat{ + bucket, + key, + 1, + }, + } + + err := statPack.toDB(dbPath) + if err != nil { + t.Fatalf("Error writing to DB: %s", err) + } + + actual, err := statFromDB(dbPath, bucket, key) + if err != nil { + t.Fatalf("Error reading DB: %s", err) + } + + if actual.val != expected { + t.Fatalf("%+v != %+v", actual.val, expected) + } + }) + + rmDB(t, dbPath) +} From e1e58591ec3d951f063596e5ce85494155f795d3 Mon Sep 17 00:00:00 2001 From: cws Date: Mon, 5 Jun 2017 20:51:29 -0400 Subject: [PATCH 2/4] stats: Add tests and clean a few things up * Make statFromDB return 0 if no stats are in the DB instead of error * Convert tests to use the assert library * Add tests for actual messages --- plugins/stats/stats.go | 4 + plugins/stats/stats_test.go | 248 ++++++++++++++++++++++++++---------- 2 files changed, 188 insertions(+), 64 deletions(-) diff --git a/plugins/stats/stats.go b/plugins/stats/stats.go index 33d37ec..2123f9b 100644 --- a/plugins/stats/stats.go +++ b/plugins/stats/stats.go @@ -107,6 +107,10 @@ func statFromDB(path, bucket, key string) (stat, error) { return stat{}, err } + if v == nil { + return stat{bucket, key, 0}, nil + } + return mkStat(buk, k, v) } diff --git a/plugins/stats/stats_test.go b/plugins/stats/stats_test.go index 5d801c5..2c95bf4 100644 --- a/plugins/stats/stats_test.go +++ b/plugins/stats/stats_test.go @@ -2,18 +2,22 @@ package stats import ( "encoding/json" - "fmt" "os" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/bot/user" ) +var dbPath = "test.db" + func TestJSON(t *testing.T) { expected := 5 b, err := json.Marshal(expected) - if err != nil { - t.Error(err) - } + assert.Nil(t, err) t.Logf("%+v", expected) t.Log(string(b)) } @@ -22,25 +26,17 @@ func TestValueConversion(t *testing.T) { expected := value(5) b, err := expected.Bytes() - if err != nil { - t.Error(err) - return - } + assert.Nil(t, err) t.Log(string(b)) actual, err := valueFromBytes(b) - if err != nil { - t.Error(err) - return - } + assert.Nil(t, err) - if fmt.Sprintf("%+v", actual) != fmt.Sprintf("%+v", expected) { - t.Errorf("Did not get equivalent objects: %+v != %+v", actual, expected) - } + assert.Equal(t, actual, expected) } -func rmDB(t *testing.T, dbPath string) { +func rmDB(t *testing.T) { err := os.Remove(dbPath) if err != nil && !strings.Contains(err.Error(), "no such file or directory") { t.Fatal(err) @@ -48,8 +44,7 @@ func rmDB(t *testing.T, dbPath string) { } func TestWithDB(t *testing.T) { - dbPath := "test.db" - rmDB(t, dbPath) + rmDB(t) t.Run("TestDBReadWrite", func(t *testing.T) { bucket := "testBucket" @@ -62,36 +57,22 @@ func TestWithDB(t *testing.T) { }} err := expected.toDB(dbPath) - if err != nil { - t.Fatalf("Error writing to DB: %s", err) - } + assert.Nil(t, err) actual, err := statFromDB(dbPath, bucket, key) - if err != nil { - t.Fatalf("Error reading DB: %s", err) - } + assert.Nil(t, err) - if actual != expected[0] { - t.Fatalf("%+v != %+v", actual, expected) - } + assert.Equal(t, actual, expected[0]) }) - rmDB(t, dbPath) + rmDB(t) t.Run("TestDBAddStatInLoop", func(t *testing.T) { bucket := "testBucket" key := "testKey" expected := value(25) - os.Remove(dbPath) - defer func() { - err := os.Remove(dbPath) - if err != nil { - t.Fatal(err) - } - }() - statPack := stats{stat{ bucket, key, @@ -100,36 +81,22 @@ func TestWithDB(t *testing.T) { for i := 0; i < 5; i++ { err := statPack.toDB(dbPath) - if err != nil { - t.Fatalf("Error writing to DB: %s", err) - } + assert.Nil(t, err) } actual, err := statFromDB(dbPath, bucket, key) - if err != nil { - t.Fatalf("Error reading DB: %s", err) - } + assert.Nil(t, err) - if actual.val != expected { - t.Fatalf("%+v != %+v", actual.val, expected) - } + assert.Equal(t, actual.val, expected) }) - rmDB(t, dbPath) + rmDB(t) t.Run("TestDBAddStats", func(t *testing.T) { bucket := "testBucket" key := "testKey" expected := value(5) - os.Remove(dbPath) - defer func() { - err := os.Remove(dbPath) - if err != nil { - t.Fatal(err) - } - }() - statPack := stats{ stat{ bucket, @@ -159,19 +126,172 @@ func TestWithDB(t *testing.T) { } err := statPack.toDB(dbPath) - if err != nil { - t.Fatalf("Error writing to DB: %s", err) - } + assert.Nil(t, err) actual, err := statFromDB(dbPath, bucket, key) - if err != nil { - t.Fatalf("Error reading DB: %s", err) - } + assert.Nil(t, err) - if actual.val != expected { - t.Fatalf("%+v != %+v", actual.val, expected) - } + assert.Equal(t, actual.val, expected) }) - rmDB(t, dbPath) + rmDB(t) +} + +func makeMessage(payload string) msg.Message { + isCmd := strings.HasPrefix(payload, "!") + if isCmd { + payload = payload[1:] + } + return msg.Message{ + User: &user.User{Name: "tester"}, + Channel: "test", + Body: payload, + Command: isCmd, + } +} + +func testUserCounter(t *testing.T, count int) { + expected := value(count) + mb := bot.NewMockBot() + mb.Cfg.Stats.DBPath = dbPath + s := New(mb) + assert.NotNil(t, s) + + for i := 0; i < count; i++ { + s.Message(makeMessage("test")) + } + + _, err := os.Stat(dbPath) + assert.Nil(t, err) + + stat, err := statFromDB(mb.Config().Stats.DBPath, "user", "tester") + assert.Nil(t, err) + actual := stat.val + assert.Equal(t, actual, expected) +} + +func TestMessages(t *testing.T) { + _, err := os.Stat(dbPath) + assert.NotNil(t, err) + + t.Run("TestOneUserCounter", func(t *testing.T) { + count := 5 + expected := value(count) + mb := bot.NewMockBot() + mb.Cfg.Stats.DBPath = dbPath + s := New(mb) + assert.NotNil(t, s) + + for i := 0; i < count; i++ { + s.Message(makeMessage("test")) + } + + _, err := os.Stat(dbPath) + assert.Nil(t, err) + + stat, err := statFromDB(mb.Config().Stats.DBPath, "user", "tester") + assert.Nil(t, err) + actual := stat.val + assert.Equal(t, actual, expected) + }) + + rmDB(t) + + t.Run("TestTenUserCounter", func(t *testing.T) { + count := 5 + expected := value(count) + mb := bot.NewMockBot() + mb.Cfg.Stats.DBPath = dbPath + s := New(mb) + assert.NotNil(t, s) + + for i := 0; i < count; i++ { + s.Message(makeMessage("test")) + } + + _, err := os.Stat(dbPath) + assert.Nil(t, err) + + stat, err := statFromDB(mb.Config().Stats.DBPath, "user", "tester") + assert.Nil(t, err) + actual := stat.val + assert.Equal(t, actual, expected) + }) + + rmDB(t) + + t.Run("TestChannelCounter", func(t *testing.T) { + count := 5 + expected := value(count) + mb := bot.NewMockBot() + mb.Cfg.Stats.DBPath = dbPath + s := New(mb) + assert.NotNil(t, s) + + for i := 0; i < count; i++ { + s.Message(makeMessage("test")) + } + + _, err := os.Stat(dbPath) + assert.Nil(t, err) + + stat, err := statFromDB(mb.Config().Stats.DBPath, "channel", "test") + assert.Nil(t, err) + actual := stat.val + assert.Equal(t, actual, expected) + }) + + rmDB(t) + + t.Run("TestSightingCounter", func(t *testing.T) { + count := 5 + expected := value(count) + mb := bot.NewMockBot() + + mb.Cfg.Stats.DBPath = dbPath + mb.Cfg.Stats.Sightings = []string{"user", "nobody"} + + s := New(mb) + assert.NotNil(t, s) + + for i := 0; i < count; i++ { + s.Message(makeMessage("user sighting")) + } + + _, err := os.Stat(dbPath) + assert.Nil(t, err) + + stat, err := statFromDB(mb.Config().Stats.DBPath, "sighting", "user") + assert.Nil(t, err) + actual := stat.val + assert.Equal(t, actual, expected) + }) + + rmDB(t) + + t.Run("TestSightingCounterNoResults", func(t *testing.T) { + count := 5 + expected := value(0) + mb := bot.NewMockBot() + + mb.Cfg.Stats.DBPath = dbPath + mb.Cfg.Stats.Sightings = []string{} + + s := New(mb) + assert.NotNil(t, s) + + for i := 0; i < count; i++ { + s.Message(makeMessage("user sighting")) + } + + _, err := os.Stat(dbPath) + assert.Nil(t, err) + + stat, err := statFromDB(mb.Config().Stats.DBPath, "sighting", "user") + assert.Nil(t, err) + actual := stat.val + assert.Equal(t, actual, expected) + }) + + rmDB(t) } From de19f6a4e94137c5004fc76ad1df5b56457a0ac7 Mon Sep 17 00:00:00 2001 From: cws Date: Mon, 5 Jun 2017 21:03:23 -0400 Subject: [PATCH 3/4] stat: change encoding to JSON --- plugins/stats/stats.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/plugins/stats/stats.go b/plugins/stats/stats.go index 2123f9b..01cabcf 100644 --- a/plugins/stats/stats.go +++ b/plugins/stats/stats.go @@ -4,8 +4,7 @@ package stats import ( - "bytes" - "encoding/gob" + "encoding/json" "log" "strings" "time" @@ -42,17 +41,13 @@ type stat struct { type value int func (v value) Bytes() ([]byte, error) { - var b bytes.Buffer - enc := gob.NewEncoder(&b) - err := enc.Encode(v) - return b.Bytes(), err + b, err := json.Marshal(v) + return b, err } func valueFromBytes(b []byte) (value, error) { var v value - buf := bytes.NewReader(b) - dec := gob.NewDecoder(buf) - err := dec.Decode(&v) + err := json.Unmarshal(b, &v) return v, err } From 4213243744e7d960fbb1b98a3ad5a68cee95c68e Mon Sep 17 00:00:00 2001 From: cws Date: Mon, 5 Jun 2017 21:20:08 -0400 Subject: [PATCH 4/4] stats: Add a day categorization above all others --- plugins/stats/stats.go | 49 +++++++++++++++++++++++-------- plugins/stats/stats_test.go | 57 ++++++++++++++++--------------------- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/plugins/stats/stats.go b/plugins/stats/stats.go index 01cabcf..58d652c 100644 --- a/plugins/stats/stats.go +++ b/plugins/stats/stats.go @@ -31,9 +31,17 @@ func New(bot bot.Bot) *StatsPlugin { } type stat struct { + // date formatted: "2006-01-02" + day string + // category bucket string - key string - val value + // specific unique individual + key string + val value +} + +func mkDay() string { + return time.Now().Format("2006-01-02") } // The value type is here in the future growth case that we might want to put a @@ -53,13 +61,16 @@ func valueFromBytes(b []byte) (value, error) { type stats []stat -func mkStat(bucket, key, val []byte) (stat, error) { +// mkStat converts raw data to a stat struct +// Expected a string representation of the date formatted: "2006-01-02" +func mkStat(day string, bucket, key, val []byte) (stat, error) { v, err := valueFromBytes(val) if err != nil { log.Printf("mkStat: error getting value from bytes: %s", err) return stat{}, err } return stat{ + day: day, bucket: string(bucket), key: string(key), val: v, @@ -71,7 +82,9 @@ func (v value) add(other value) value { return v + other } -func statFromDB(path, bucket, key string) (stat, error) { +// statFromDB takes a location specification and returns the data at that path +// Expected a string representation of the date formatted: "2006-01-02" +func statFromDB(path, day, bucket, key string) (stat, error) { db, err := bolt.Open(path, 0600, &bolt.Options{ Timeout: 1 * time.Second, }) @@ -90,11 +103,17 @@ func statFromDB(path, bucket, key string) (stat, error) { } defer tx.Rollback() - b, err := tx.CreateBucketIfNotExists(buk) + d, err := tx.CreateBucketIfNotExists([]byte(day)) if err != nil { log.Println("statFromDB: Error creating the bucket") return stat{}, err } + b, err := d.CreateBucketIfNotExists(buk) + if err != nil { + log.Println("statFromDB: Error creating the bucket") + return stat{}, err + } + v := b.Get(k) if err := tx.Commit(); err != nil { @@ -103,12 +122,13 @@ func statFromDB(path, bucket, key string) (stat, error) { } if v == nil { - return stat{bucket, key, 0}, nil + return stat{day, bucket, key, 0}, nil } - return mkStat(buk, k, v) + return mkStat(day, buk, k, v) } +// toDB takes a stat and records it, adding to the value in the DB if necessary func (s stats) toDB(path string) error { db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second}) if err != nil { @@ -119,7 +139,12 @@ func (s stats) toDB(path string) error { for _, stat := range s { err = db.Update(func(tx *bolt.Tx) error { - b, err := tx.CreateBucketIfNotExists([]byte(stat.bucket)) + d, err := tx.CreateBucketIfNotExists([]byte(stat.day)) + if err != nil { + log.Println("toDB: Error creating bucket") + return err + } + b, err := d.CreateBucketIfNotExists([]byte(stat.bucket)) if err != nil { log.Println("toDB: Error creating bucket") return err @@ -192,24 +217,24 @@ func (p *StatsPlugin) RegisterWeb() *string { } func (p *StatsPlugin) mkUserStat(message msg.Message) stats { - return stats{stat{"user", message.User.Name, 1}} + return stats{stat{mkDay(), "user", message.User.Name, 1}} } func (p *StatsPlugin) mkHourStat(message msg.Message) stats { hr := time.Now().Hour() - return stats{stat{"user", string(hr), 1}} + return stats{stat{mkDay(), "user", string(hr), 1}} } func (p *StatsPlugin) mkSightingStat(message msg.Message) stats { stats := stats{} for _, name := range p.bot.Config().Stats.Sightings { if strings.Contains(message.Body, name+" sighting") { - stats = append(stats, stat{"sighting", name, 1}) + stats = append(stats, stat{mkDay(), "sighting", name, 1}) } } return stats } func (p *StatsPlugin) mkChannelStat(message msg.Message) stats { - return stats{stat{"channel", message.Channel, 1}} + return stats{stat{mkDay(), "channel", message.Channel, 1}} } diff --git a/plugins/stats/stats_test.go b/plugins/stats/stats_test.go index 2c95bf4..d5597f4 100644 --- a/plugins/stats/stats_test.go +++ b/plugins/stats/stats_test.go @@ -47,10 +47,12 @@ func TestWithDB(t *testing.T) { rmDB(t) t.Run("TestDBReadWrite", func(t *testing.T) { + day := mkDay() bucket := "testBucket" key := "testKey" expected := stats{stat{ + day, bucket, key, 1, @@ -59,7 +61,7 @@ func TestWithDB(t *testing.T) { err := expected.toDB(dbPath) assert.Nil(t, err) - actual, err := statFromDB(dbPath, bucket, key) + actual, err := statFromDB(dbPath, day, bucket, key) assert.Nil(t, err) assert.Equal(t, actual, expected[0]) @@ -69,11 +71,13 @@ func TestWithDB(t *testing.T) { rmDB(t) t.Run("TestDBAddStatInLoop", func(t *testing.T) { + day := mkDay() bucket := "testBucket" key := "testKey" expected := value(25) statPack := stats{stat{ + day, bucket, key, 5, @@ -84,7 +88,7 @@ func TestWithDB(t *testing.T) { assert.Nil(t, err) } - actual, err := statFromDB(dbPath, bucket, key) + actual, err := statFromDB(dbPath, day, bucket, key) assert.Nil(t, err) assert.Equal(t, actual.val, expected) @@ -93,42 +97,25 @@ func TestWithDB(t *testing.T) { rmDB(t) t.Run("TestDBAddStats", func(t *testing.T) { + day := mkDay() bucket := "testBucket" key := "testKey" expected := value(5) - statPack := stats{ - stat{ + statPack := stats{} + for i := 0; i < 5; i++ { + statPack = append(statPack, stat{ + day, bucket, key, 1, - }, - stat{ - bucket, - key, - 1, - }, - stat{ - bucket, - key, - 1, - }, - stat{ - bucket, - key, - 1, - }, - stat{ - bucket, - key, - 1, - }, + }) } err := statPack.toDB(dbPath) assert.Nil(t, err) - actual, err := statFromDB(dbPath, bucket, key) + actual, err := statFromDB(dbPath, day, bucket, key) assert.Nil(t, err) assert.Equal(t, actual.val, expected) @@ -151,6 +138,7 @@ func makeMessage(payload string) msg.Message { } func testUserCounter(t *testing.T, count int) { + day := mkDay() expected := value(count) mb := bot.NewMockBot() mb.Cfg.Stats.DBPath = dbPath @@ -164,7 +152,7 @@ func testUserCounter(t *testing.T, count int) { _, err := os.Stat(dbPath) assert.Nil(t, err) - stat, err := statFromDB(mb.Config().Stats.DBPath, "user", "tester") + stat, err := statFromDB(mb.Config().Stats.DBPath, day, "user", "tester") assert.Nil(t, err) actual := stat.val assert.Equal(t, actual, expected) @@ -175,6 +163,7 @@ func TestMessages(t *testing.T) { assert.NotNil(t, err) t.Run("TestOneUserCounter", func(t *testing.T) { + day := mkDay() count := 5 expected := value(count) mb := bot.NewMockBot() @@ -189,7 +178,7 @@ func TestMessages(t *testing.T) { _, err := os.Stat(dbPath) assert.Nil(t, err) - stat, err := statFromDB(mb.Config().Stats.DBPath, "user", "tester") + stat, err := statFromDB(mb.Config().Stats.DBPath, day, "user", "tester") assert.Nil(t, err) actual := stat.val assert.Equal(t, actual, expected) @@ -198,6 +187,7 @@ func TestMessages(t *testing.T) { rmDB(t) t.Run("TestTenUserCounter", func(t *testing.T) { + day := mkDay() count := 5 expected := value(count) mb := bot.NewMockBot() @@ -212,7 +202,7 @@ func TestMessages(t *testing.T) { _, err := os.Stat(dbPath) assert.Nil(t, err) - stat, err := statFromDB(mb.Config().Stats.DBPath, "user", "tester") + stat, err := statFromDB(mb.Config().Stats.DBPath, day, "user", "tester") assert.Nil(t, err) actual := stat.val assert.Equal(t, actual, expected) @@ -221,6 +211,7 @@ func TestMessages(t *testing.T) { rmDB(t) t.Run("TestChannelCounter", func(t *testing.T) { + day := mkDay() count := 5 expected := value(count) mb := bot.NewMockBot() @@ -235,7 +226,7 @@ func TestMessages(t *testing.T) { _, err := os.Stat(dbPath) assert.Nil(t, err) - stat, err := statFromDB(mb.Config().Stats.DBPath, "channel", "test") + stat, err := statFromDB(mb.Config().Stats.DBPath, day, "channel", "test") assert.Nil(t, err) actual := stat.val assert.Equal(t, actual, expected) @@ -244,6 +235,7 @@ func TestMessages(t *testing.T) { rmDB(t) t.Run("TestSightingCounter", func(t *testing.T) { + day := mkDay() count := 5 expected := value(count) mb := bot.NewMockBot() @@ -261,7 +253,7 @@ func TestMessages(t *testing.T) { _, err := os.Stat(dbPath) assert.Nil(t, err) - stat, err := statFromDB(mb.Config().Stats.DBPath, "sighting", "user") + stat, err := statFromDB(mb.Config().Stats.DBPath, day, "sighting", "user") assert.Nil(t, err) actual := stat.val assert.Equal(t, actual, expected) @@ -270,6 +262,7 @@ func TestMessages(t *testing.T) { rmDB(t) t.Run("TestSightingCounterNoResults", func(t *testing.T) { + day := mkDay() count := 5 expected := value(0) mb := bot.NewMockBot() @@ -287,7 +280,7 @@ func TestMessages(t *testing.T) { _, err := os.Stat(dbPath) assert.Nil(t, err) - stat, err := statFromDB(mb.Config().Stats.DBPath, "sighting", "user") + stat, err := statFromDB(mb.Config().Stats.DBPath, day, "sighting", "user") assert.Nil(t, err) actual := stat.val assert.Equal(t, actual, expected)