mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
refactor: introduce concept of durations (resolve #261)
This commit is contained in:
parent
15c8838fea
commit
c8a07cee36
4
main.go
4
main.go
@ -64,6 +64,7 @@ var (
|
||||
userService services.IUserService
|
||||
languageMappingService services.ILanguageMappingService
|
||||
projectLabelService services.IProjectLabelService
|
||||
durationService services.IDurationService
|
||||
summaryService services.ISummaryService
|
||||
aggregationService services.IAggregationService
|
||||
mailService services.IMailService
|
||||
@ -154,7 +155,8 @@ func main() {
|
||||
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
|
||||
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
|
||||
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
|
||||
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService, projectLabelService)
|
||||
durationService = services.NewDurationService(heartbeatService)
|
||||
summaryService = services.NewSummaryService(summaryRepository, durationService, aliasService, projectLabelService)
|
||||
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
||||
keyValueService = services.NewKeyValueService(keyValueRepository)
|
||||
reportService = services.NewReportService(summaryService, userService, mailService)
|
||||
|
16
mocks/duration_service.go
Normal file
16
mocks/duration_service.go
Normal file
@ -0,0 +1,16 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DurationServiceMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *DurationServiceMock) Get(time time.Time, time2 time.Time, user *models.User) ([]*models.Duration, error) {
|
||||
args := m.Called(time, time2, user)
|
||||
return args.Get(0).([]*models.Duration), args.Error(1)
|
||||
}
|
64
models/duration.go
Normal file
64
models/duration.go
Normal file
@ -0,0 +1,64 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Duration struct {
|
||||
UserID string `json:"user_id"`
|
||||
Time CustomTime `json:"time" hash:"ignore"`
|
||||
Duration time.Duration `json:"duration" hash:"ignore"`
|
||||
Project string `json:"project"`
|
||||
Language string `json:"language"`
|
||||
Editor string `json:"editor"`
|
||||
OperatingSystem string `json:"operating_system"`
|
||||
Machine string `json:"machine"`
|
||||
GroupHash string `json:"-" hash:"ignore"`
|
||||
}
|
||||
|
||||
func NewDurationFromHeartbeat(h *Heartbeat) *Duration {
|
||||
d := &Duration{
|
||||
UserID: h.UserID,
|
||||
Time: h.Time,
|
||||
Duration: 0,
|
||||
Project: h.Project,
|
||||
Language: h.Language,
|
||||
Editor: h.Editor,
|
||||
OperatingSystem: h.OperatingSystem,
|
||||
Machine: h.Machine,
|
||||
}
|
||||
return d.Hashed()
|
||||
}
|
||||
|
||||
func (d *Duration) Hashed() *Duration {
|
||||
hash, err := hashstructure.Hash(d, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
logbuch.Error("CRITICAL ERROR: failed to hash struct – %v", err)
|
||||
}
|
||||
d.GroupHash = fmt.Sprintf("%x", hash)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *Duration) GetKey(t uint8) (key string) {
|
||||
switch t {
|
||||
case SummaryProject:
|
||||
key = d.Project
|
||||
case SummaryEditor:
|
||||
key = d.Editor
|
||||
case SummaryLanguage:
|
||||
key = d.Language
|
||||
case SummaryOS:
|
||||
key = d.OperatingSystem
|
||||
case SummaryMachine:
|
||||
key = d.Machine
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
key = UnknownSummaryKey
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
38
models/durations.go
Normal file
38
models/durations.go
Normal file
@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import "sort"
|
||||
|
||||
type Durations []*Duration
|
||||
|
||||
func (d Durations) Len() int {
|
||||
return len(d)
|
||||
}
|
||||
|
||||
func (d Durations) Less(i, j int) bool {
|
||||
return d[i].Time.T().Before(d[j].Time.T())
|
||||
}
|
||||
|
||||
func (d Durations) Swap(i, j int) {
|
||||
d[i], d[j] = d[j], d[i]
|
||||
}
|
||||
|
||||
func (d *Durations) Sorted() *Durations {
|
||||
sort.Sort(d)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *Durations) First() *Duration {
|
||||
// assumes slice to be sorted
|
||||
if d.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
return (*d)[0]
|
||||
}
|
||||
|
||||
func (d *Durations) Last() *Duration {
|
||||
// assumes slice to be sorted
|
||||
if d.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
return (*d)[d.Len()-1]
|
||||
}
|
77
services/duration.go
Normal file
77
services/duration.go
Normal file
@ -0,0 +1,77 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
const HeartbeatDiffThreshold = 2 * time.Minute
|
||||
|
||||
type DurationService struct {
|
||||
config *config.Config
|
||||
heartbeatService IHeartbeatService
|
||||
}
|
||||
|
||||
func NewDurationService(heartbeatService IHeartbeatService) *DurationService {
|
||||
srv := &DurationService{
|
||||
config: config.Get(),
|
||||
heartbeatService: heartbeatService,
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
func (srv *DurationService) Get(from, to time.Time, user *models.User) ([]*models.Duration, error) {
|
||||
heartbeats, err := srv.heartbeatService.GetAllWithin(from, to, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Aggregation
|
||||
var count int
|
||||
var latest *models.Duration
|
||||
|
||||
mapping := make(map[string][]*models.Duration)
|
||||
|
||||
for _, h := range heartbeats {
|
||||
d1 := models.NewDurationFromHeartbeat(h)
|
||||
|
||||
if list, ok := mapping[d1.GroupHash]; !ok || len(list) < 1 {
|
||||
mapping[d1.GroupHash] = []*models.Duration{d1}
|
||||
}
|
||||
|
||||
if latest == nil {
|
||||
latest = d1
|
||||
continue
|
||||
}
|
||||
|
||||
dur := d1.Time.T().Sub(latest.Time.T().Add(latest.Duration))
|
||||
if dur > HeartbeatDiffThreshold {
|
||||
dur = HeartbeatDiffThreshold
|
||||
}
|
||||
latest.Duration += dur
|
||||
|
||||
if dur >= HeartbeatDiffThreshold || latest.GroupHash != d1.GroupHash {
|
||||
list := mapping[d1.GroupHash]
|
||||
if d0 := list[len(list)-1]; d0 != d1 {
|
||||
mapping[d1.GroupHash] = append(mapping[d1.GroupHash], d1)
|
||||
}
|
||||
latest = d1
|
||||
}
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
durations := make([]*models.Duration, 0, count)
|
||||
|
||||
for _, list := range mapping {
|
||||
for _, d := range list {
|
||||
if d.Duration == 0 {
|
||||
d.Duration = HeartbeatDiffThreshold
|
||||
}
|
||||
durations = append(durations, d)
|
||||
}
|
||||
}
|
||||
|
||||
return durations, nil
|
||||
}
|
168
services/duration_test.go
Normal file
168
services/duration_test.go
Normal file
@ -0,0 +1,168 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/mocks"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
TestUserId = "muety"
|
||||
TestProject1 = "test-project-1"
|
||||
TestProject2 = "test-project-2"
|
||||
TestLanguageGo = "Go"
|
||||
TestLanguageJava = "Java"
|
||||
TestLanguagePython = "Python"
|
||||
TestEditorGoland = "GoLand"
|
||||
TestEditorIntellij = "idea"
|
||||
TestEditorVscode = "vscode"
|
||||
TestOsLinux = "Linux"
|
||||
TestOsWin = "Windows"
|
||||
TestMachine1 = "muety-desktop"
|
||||
TestMachine2 = "muety-work"
|
||||
MinUnixTime1 = 1601510400000 * 1e6
|
||||
)
|
||||
|
||||
type DurationServiceTestSuite struct {
|
||||
suite.Suite
|
||||
TestUser *models.User
|
||||
TestStartTime time.Time
|
||||
TestHeartbeats []*models.Heartbeat
|
||||
TestLabels []*models.ProjectLabel
|
||||
HeartbeatService *mocks.HeartbeatServiceMock
|
||||
}
|
||||
|
||||
func (suite *DurationServiceTestSuite) SetupSuite() {
|
||||
suite.TestUser = &models.User{ID: TestUserId}
|
||||
|
||||
suite.TestStartTime = time.Unix(0, MinUnixTime1)
|
||||
suite.TestHeartbeats = []*models.Heartbeat{
|
||||
{
|
||||
ID: uint(rand.Uint32()),
|
||||
UserID: TestUserId,
|
||||
Project: TestProject1,
|
||||
Language: TestLanguageGo,
|
||||
Editor: TestEditorGoland,
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(suite.TestStartTime), // 0:00
|
||||
},
|
||||
{
|
||||
ID: uint(rand.Uint32()),
|
||||
UserID: TestUserId,
|
||||
Project: TestProject1,
|
||||
Language: TestLanguageGo,
|
||||
Editor: TestEditorGoland,
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(suite.TestStartTime.Add(30 * time.Second)), // 0:30
|
||||
},
|
||||
{
|
||||
ID: uint(rand.Uint32()),
|
||||
UserID: TestUserId,
|
||||
Project: TestProject1,
|
||||
Language: TestLanguageGo,
|
||||
Editor: TestEditorGoland,
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(suite.TestStartTime.Add((30 + 130) * time.Second)), // 2:40
|
||||
},
|
||||
{
|
||||
ID: uint(rand.Uint32()),
|
||||
UserID: TestUserId,
|
||||
Project: TestProject1,
|
||||
Language: TestLanguageGo,
|
||||
Editor: TestEditorVscode,
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(suite.TestStartTime.Add(3 * time.Minute)), // 3:00
|
||||
},
|
||||
{
|
||||
ID: uint(rand.Uint32()),
|
||||
UserID: TestUserId,
|
||||
Project: TestProject1,
|
||||
Language: TestLanguageGo,
|
||||
Editor: TestEditorVscode,
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(suite.TestStartTime.Add(3*time.Minute + 10*time.Second)), // 3:10
|
||||
},
|
||||
{
|
||||
ID: uint(rand.Uint32()),
|
||||
UserID: TestUserId,
|
||||
Project: TestProject1,
|
||||
Language: TestLanguageGo,
|
||||
Editor: TestEditorVscode,
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(suite.TestStartTime.Add(3*time.Minute + 15*time.Second)), // 3:15
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *DurationServiceTestSuite) BeforeTest(suiteName, testName string) {
|
||||
suite.HeartbeatService = new(mocks.HeartbeatServiceMock)
|
||||
}
|
||||
|
||||
func TestDurationServiceTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(DurationServiceTestSuite))
|
||||
}
|
||||
|
||||
func (suite *DurationServiceTestSuite) TestDurationService_Get() {
|
||||
sut := NewDurationService(suite.HeartbeatService)
|
||||
|
||||
var (
|
||||
from time.Time
|
||||
to time.Time
|
||||
durations models.Durations
|
||||
err error
|
||||
)
|
||||
|
||||
/* TEST 1 */
|
||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(-1*time.Minute)
|
||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filterHeartbeats(from, to, suite.TestHeartbeats), nil)
|
||||
|
||||
durations, err = sut.Get(from, to, suite.TestUser)
|
||||
|
||||
assert.Nil(suite.T(), err)
|
||||
assert.Empty(suite.T(), durations)
|
||||
|
||||
/* TEST 2 */
|
||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Second)
|
||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filterHeartbeats(from, to, suite.TestHeartbeats), nil)
|
||||
|
||||
durations, err = sut.Get(from, to, suite.TestUser)
|
||||
|
||||
assert.Nil(suite.T(), err)
|
||||
assert.Len(suite.T(), durations, 1)
|
||||
assert.Equal(suite.T(), HeartbeatDiffThreshold, durations.First().Duration)
|
||||
|
||||
/* TEST 3 */
|
||||
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filterHeartbeats(from, to, suite.TestHeartbeats), nil)
|
||||
|
||||
durations, err = sut.Get(from, to, suite.TestUser)
|
||||
|
||||
assert.Nil(suite.T(), err)
|
||||
assert.Len(suite.T(), durations, 3)
|
||||
assert.Equal(suite.T(), 150*time.Second, durations[0].Duration)
|
||||
assert.Equal(suite.T(), 20*time.Second, durations[1].Duration)
|
||||
assert.Equal(suite.T(), 15*time.Second, durations[2].Duration)
|
||||
assert.Equal(suite.T(), TestEditorGoland, durations[0].Editor)
|
||||
assert.Equal(suite.T(), TestEditorGoland, durations[1].Editor)
|
||||
assert.Equal(suite.T(), TestEditorVscode, durations[2].Editor)
|
||||
}
|
||||
|
||||
func filterHeartbeats(from, to time.Time, heartbeats []*models.Heartbeat) []*models.Heartbeat {
|
||||
filtered := make([]*models.Heartbeat, 0, len(heartbeats))
|
||||
for _, h := range heartbeats {
|
||||
if (h.Time.T().Equal(from) || h.Time.T().After(from)) && h.Time.T().Before(to) {
|
||||
filtered = append(filtered, h)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
@ -74,6 +74,10 @@ type IMailService interface {
|
||||
SendReport(*models.User, *models.Report) error
|
||||
}
|
||||
|
||||
type IDurationService interface {
|
||||
Get(time.Time, time.Time, *models.User) ([]*models.Duration, error)
|
||||
}
|
||||
|
||||
type ISummaryService interface {
|
||||
Aliased(time.Time, time.Time, *models.User, SummaryRetriever, bool) (*models.Summary, error)
|
||||
Retrieve(time.Time, time.Time, *models.User) (*models.Summary, error)
|
||||
|
@ -9,33 +9,30 @@ import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/repositories"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const HeartbeatDiffThreshold = 2 * time.Minute
|
||||
|
||||
type SummaryService struct {
|
||||
config *config.Config
|
||||
cache *cache.Cache
|
||||
eventBus *hub.Hub
|
||||
repository repositories.ISummaryRepository
|
||||
heartbeatService IHeartbeatService
|
||||
durationService IDurationService
|
||||
aliasService IAliasService
|
||||
projectLabelService IProjectLabelService
|
||||
}
|
||||
|
||||
type SummaryRetriever func(f, t time.Time, u *models.User) (*models.Summary, error)
|
||||
|
||||
func NewSummaryService(summaryRepo repositories.ISummaryRepository, heartbeatService IHeartbeatService, aliasService IAliasService, projectLabelService IProjectLabelService) *SummaryService {
|
||||
func NewSummaryService(summaryRepo repositories.ISummaryRepository, durationService IDurationService, aliasService IAliasService, projectLabelService IProjectLabelService) *SummaryService {
|
||||
srv := &SummaryService{
|
||||
config: config.Get(),
|
||||
cache: cache.New(24*time.Hour, 24*time.Hour),
|
||||
eventBus: config.EventBus(),
|
||||
repository: summaryRepo,
|
||||
heartbeatService: heartbeatService,
|
||||
durationService: durationService,
|
||||
aliasService: aliasService,
|
||||
projectLabelService: projectLabelService,
|
||||
}
|
||||
@ -99,7 +96,7 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*mod
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate missing slots (especially before and after existing summaries) from raw heartbeats
|
||||
// Generate missing slots (especially before and after existing summaries) from durations (formerly raw heartbeats)
|
||||
missingIntervals := srv.getMissingIntervals(from, to, summaries)
|
||||
for _, interval := range missingIntervals {
|
||||
if s, err := srv.Summarize(interval.Start, interval.End, user); err == nil {
|
||||
@ -120,9 +117,9 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*mod
|
||||
|
||||
func (srv *SummaryService) Summarize(from, to time.Time, user *models.User) (*models.Summary, error) {
|
||||
// Initialize and fetch data
|
||||
var heartbeats models.Heartbeats
|
||||
if rawHeartbeats, err := srv.heartbeatService.GetAllWithin(from, to, user); err == nil {
|
||||
heartbeats = rawHeartbeats
|
||||
var durations models.Durations
|
||||
if result, err := srv.durationService.Get(from, to, user); err == nil {
|
||||
durations = result
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
@ -132,10 +129,10 @@ func (srv *SummaryService) Summarize(from, to time.Time, user *models.User) (*mo
|
||||
typedAggregations := make(chan models.SummaryItemContainer)
|
||||
defer close(typedAggregations)
|
||||
for _, t := range types {
|
||||
go srv.aggregateBy(heartbeats, t, typedAggregations)
|
||||
go srv.aggregateBy(durations, t, typedAggregations)
|
||||
}
|
||||
|
||||
// Aggregate raw heartbeats by types in parallel and collect them
|
||||
// Aggregate durations (formerly raw heartbeats) by types in parallel and collect them
|
||||
var projectItems []*models.SummaryItem
|
||||
var languageItems []*models.SummaryItem
|
||||
var editorItems []*models.SummaryItem
|
||||
@ -158,9 +155,9 @@ func (srv *SummaryService) Summarize(from, to time.Time, user *models.User) (*mo
|
||||
}
|
||||
}
|
||||
|
||||
if heartbeats.Len() > 0 {
|
||||
from = time.Time(heartbeats.First().Time)
|
||||
to = time.Time(heartbeats.Last().Time)
|
||||
if durations.Len() > 0 {
|
||||
from = time.Time(durations.First().Time)
|
||||
to = time.Time(durations.Last().Time)
|
||||
}
|
||||
|
||||
summary := &models.Summary{
|
||||
@ -195,34 +192,15 @@ func (srv *SummaryService) Insert(summary *models.Summary) error {
|
||||
|
||||
// Private summary generation and utility methods
|
||||
|
||||
func (srv *SummaryService) aggregateBy(heartbeats []*models.Heartbeat, summaryType uint8, c chan models.SummaryItemContainer) {
|
||||
durations := make(map[string]time.Duration)
|
||||
func (srv *SummaryService) aggregateBy(durations []*models.Duration, summaryType uint8, c chan models.SummaryItemContainer) {
|
||||
mapping := make(map[string]time.Duration)
|
||||
|
||||
for i, h := range heartbeats {
|
||||
key := h.GetKey(summaryType)
|
||||
|
||||
if _, ok := durations[key]; !ok {
|
||||
durations[key] = time.Duration(0)
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
t1, t2, tdiff := h.Time.T(), heartbeats[i-1].Time.T(), time.Duration(0)
|
||||
// This is a hack. The time difference between two heartbeats from two subsequent day (e.g. 23:59:59 and 00:00:01) are ignored.
|
||||
// This is to prevent a discrepancy between summaries computed solely from heartbeats and summaries involving pre-aggregated per-day summaries.
|
||||
// For the latter, a duration is already pre-computed and information about individual heartbeats is lost, so there can be no cross-day overflow.
|
||||
// Essentially, we simply ignore such edge-case heartbeats here, which makes the eventual total duration potentially a bit shorter.
|
||||
if t1.Day() == t2.Day() {
|
||||
timePassed := t1.Sub(t2)
|
||||
tdiff = time.Duration(int64(math.Min(float64(timePassed), float64(HeartbeatDiffThreshold))))
|
||||
}
|
||||
durations[key] += tdiff
|
||||
for _, d := range durations {
|
||||
mapping[d.GetKey(summaryType)] += d.Duration
|
||||
}
|
||||
|
||||
items := make([]*models.SummaryItem, 0)
|
||||
for k, v := range durations {
|
||||
for k, v := range mapping {
|
||||
items = append(items, &models.SummaryItem{
|
||||
Key: k,
|
||||
Total: v / time.Second,
|
||||
|
@ -13,33 +13,19 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
TestUserId = "muety"
|
||||
TestProject1 = "test-project-1"
|
||||
TestProject2 = "test-project-2"
|
||||
TestProjectLabel1 = "private"
|
||||
TestProjectLabel2 = "work"
|
||||
TestProjectLabel3 = "non-existing"
|
||||
TestLanguageGo = "Go"
|
||||
TestLanguageJava = "Java"
|
||||
TestLanguagePython = "Python"
|
||||
TestEditorGoland = "GoLand"
|
||||
TestEditorIntellij = "idea"
|
||||
TestEditorVscode = "vscode"
|
||||
TestOsLinux = "Linux"
|
||||
TestOsWin = "Windows"
|
||||
TestMachine1 = "muety-desktop"
|
||||
TestMachine2 = "muety-work"
|
||||
MinUnixTime1 = 1601510400000 * 1e6
|
||||
TestProjectLabel1 = "private"
|
||||
TestProjectLabel2 = "work"
|
||||
TestProjectLabel3 = "non-existing"
|
||||
)
|
||||
|
||||
type SummaryServiceTestSuite struct {
|
||||
suite.Suite
|
||||
TestUser *models.User
|
||||
TestStartTime time.Time
|
||||
TestHeartbeats []*models.Heartbeat
|
||||
TestDurations []*models.Duration
|
||||
TestLabels []*models.ProjectLabel
|
||||
SummaryRepository *mocks.SummaryRepositoryMock
|
||||
HeartbeatService *mocks.HeartbeatServiceMock
|
||||
DurationService *mocks.DurationServiceMock
|
||||
AliasService *mocks.AliasServiceMock
|
||||
ProjectLabelService *mocks.ProjectLabelServiceMock
|
||||
}
|
||||
@ -48,9 +34,8 @@ func (suite *SummaryServiceTestSuite) SetupSuite() {
|
||||
suite.TestUser = &models.User{ID: TestUserId}
|
||||
|
||||
suite.TestStartTime = time.Unix(0, MinUnixTime1)
|
||||
suite.TestHeartbeats = []*models.Heartbeat{
|
||||
suite.TestDurations = []*models.Duration{
|
||||
{
|
||||
ID: rand.Uint64(),
|
||||
UserID: TestUserId,
|
||||
Project: TestProject1,
|
||||
Language: TestLanguageGo,
|
||||
@ -58,19 +43,19 @@ func (suite *SummaryServiceTestSuite) SetupSuite() {
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(suite.TestStartTime),
|
||||
Duration: 150 * time.Second,
|
||||
},
|
||||
{
|
||||
ID: rand.Uint64(),
|
||||
UserID: TestUserId,
|
||||
Project: TestProject1,
|
||||
Language: TestLanguageGo,
|
||||
Editor: TestEditorGoland,
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(suite.TestStartTime.Add(30 * time.Second)),
|
||||
Time: models.CustomTime(suite.TestStartTime.Add((30 + 130) * time.Second)),
|
||||
Duration: 20 * time.Second,
|
||||
},
|
||||
{
|
||||
ID: rand.Uint64(),
|
||||
UserID: TestUserId,
|
||||
Project: TestProject1,
|
||||
Language: TestLanguageGo,
|
||||
@ -78,6 +63,7 @@ func (suite *SummaryServiceTestSuite) SetupSuite() {
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(suite.TestStartTime.Add(3 * time.Minute)),
|
||||
Duration: 15 * time.Second,
|
||||
},
|
||||
}
|
||||
suite.TestLabels = []*models.ProjectLabel{
|
||||
@ -98,7 +84,7 @@ func (suite *SummaryServiceTestSuite) SetupSuite() {
|
||||
|
||||
func (suite *SummaryServiceTestSuite) BeforeTest(suiteName, testName string) {
|
||||
suite.SummaryRepository = new(mocks.SummaryRepositoryMock)
|
||||
suite.HeartbeatService = new(mocks.HeartbeatServiceMock)
|
||||
suite.DurationService = new(mocks.DurationServiceMock)
|
||||
suite.AliasService = new(mocks.AliasServiceMock)
|
||||
suite.ProjectLabelService = new(mocks.ProjectLabelServiceMock)
|
||||
}
|
||||
@ -108,7 +94,7 @@ func TestSummaryServiceTestSuite(t *testing.T) {
|
||||
}
|
||||
|
||||
func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.HeartbeatService, suite.AliasService, suite.ProjectLabelService)
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
|
||||
|
||||
var (
|
||||
from time.Time
|
||||
@ -119,7 +105,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
|
||||
|
||||
/* TEST 1 */
|
||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(-1*time.Minute)
|
||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filter(from, to, suite.TestHeartbeats), nil)
|
||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(filterDurations(from, to, suite.TestDurations), nil)
|
||||
|
||||
result, err = sut.Summarize(from, to, suite.TestUser)
|
||||
|
||||
@ -132,36 +118,36 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
|
||||
|
||||
/* TEST 2 */
|
||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Second)
|
||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filter(from, to, suite.TestHeartbeats), nil)
|
||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(filterDurations(from, to, suite.TestDurations), nil)
|
||||
|
||||
result, err = sut.Summarize(from, to, suite.TestUser)
|
||||
|
||||
assert.Nil(suite.T(), err)
|
||||
assert.NotNil(suite.T(), result)
|
||||
assert.Equal(suite.T(), suite.TestHeartbeats[0].Time.T(), result.FromTime.T())
|
||||
assert.Equal(suite.T(), suite.TestHeartbeats[0].Time.T(), result.ToTime.T())
|
||||
assert.Zero(suite.T(), result.TotalTime())
|
||||
assert.Equal(suite.T(), suite.TestDurations[0].Time.T(), result.FromTime.T())
|
||||
assert.Equal(suite.T(), suite.TestDurations[0].Time.T(), result.ToTime.T())
|
||||
assert.Equal(suite.T(), 150*time.Second, result.TotalTime())
|
||||
assertNumAllItems(suite.T(), 1, result, "")
|
||||
|
||||
/* TEST 3 */
|
||||
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(filter(from, to, suite.TestHeartbeats), nil)
|
||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(filterDurations(from, to, suite.TestDurations), nil)
|
||||
|
||||
result, err = sut.Summarize(from, to, suite.TestUser)
|
||||
|
||||
assert.Nil(suite.T(), err)
|
||||
assert.NotNil(suite.T(), result)
|
||||
assert.Equal(suite.T(), suite.TestHeartbeats[0].Time.T(), result.FromTime.T())
|
||||
assert.Equal(suite.T(), suite.TestHeartbeats[len(suite.TestHeartbeats)-1].Time.T(), result.ToTime.T())
|
||||
assert.Equal(suite.T(), 150*time.Second, result.TotalTime())
|
||||
assert.Equal(suite.T(), 30*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorGoland))
|
||||
assert.Equal(suite.T(), 120*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorVscode))
|
||||
assert.Equal(suite.T(), suite.TestDurations[0].Time.T(), result.FromTime.T())
|
||||
assert.Equal(suite.T(), suite.TestDurations[len(suite.TestDurations)-1].Time.T(), result.ToTime.T())
|
||||
assert.Equal(suite.T(), 185*time.Second, result.TotalTime())
|
||||
assert.Equal(suite.T(), 170*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorGoland))
|
||||
assert.Equal(suite.T(), 15*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorVscode))
|
||||
assert.Len(suite.T(), result.Editors, 2)
|
||||
assertNumAllItems(suite.T(), 1, result, "e")
|
||||
}
|
||||
|
||||
func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.HeartbeatService, suite.AliasService, suite.ProjectLabelService)
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
|
||||
|
||||
var (
|
||||
summaries []*models.Summary
|
||||
@ -194,8 +180,8 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
||||
}
|
||||
|
||||
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
||||
suite.HeartbeatService.On("GetAllWithin", from, summaries[0].FromTime.T(), suite.TestUser).Return([]*models.Heartbeat{}, nil)
|
||||
suite.HeartbeatService.On("GetAllWithin", summaries[0].ToTime.T(), to, suite.TestUser).Return([]*models.Heartbeat{}, nil)
|
||||
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser).Return([]*models.Duration{}, nil)
|
||||
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, suite.TestUser).Return([]*models.Duration{}, nil)
|
||||
|
||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
||||
|
||||
@ -203,7 +189,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
||||
assert.NotNil(suite.T(), result)
|
||||
assert.Len(suite.T(), result.Projects, 1)
|
||||
assert.Equal(suite.T(), summaries[0].Projects[0].Total*time.Second, result.TotalTime())
|
||||
suite.HeartbeatService.AssertNumberOfCalls(suite.T(), "GetAllWithin", 2)
|
||||
suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2)
|
||||
|
||||
/* TEST 2 */
|
||||
from, to = suite.TestStartTime.Add(-10*time.Minute), suite.TestStartTime.Add(12*time.Hour)
|
||||
@ -245,17 +231,17 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
||||
}
|
||||
|
||||
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
||||
suite.HeartbeatService.On("GetAllWithin", from, summaries[0].FromTime.T(), suite.TestUser).Return(filter(from, summaries[0].FromTime.T(), suite.TestHeartbeats), nil)
|
||||
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser).Return(filterDurations(from, summaries[0].FromTime.T(), suite.TestDurations), nil)
|
||||
|
||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
||||
|
||||
assert.Nil(suite.T(), err)
|
||||
assert.NotNil(suite.T(), result)
|
||||
assert.Len(suite.T(), result.Projects, 2)
|
||||
assert.Equal(suite.T(), 150*time.Second+90*time.Minute, result.TotalTime())
|
||||
assert.Equal(suite.T(), 150*time.Second+45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject1))
|
||||
assert.Equal(suite.T(), 185*time.Second+90*time.Minute, result.TotalTime())
|
||||
assert.Equal(suite.T(), 185*time.Second+45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject1))
|
||||
assert.Equal(suite.T(), 45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject2))
|
||||
suite.HeartbeatService.AssertNumberOfCalls(suite.T(), "GetAllWithin", 2+1)
|
||||
suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2+1)
|
||||
|
||||
/* TEST 3 */
|
||||
from = time.Date(suite.TestStartTime.Year(), suite.TestStartTime.Month(), suite.TestStartTime.Day()+1, 0, 0, 0, 0, suite.TestStartTime.Location()) // start of next day
|
||||
@ -298,7 +284,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
||||
}
|
||||
|
||||
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
||||
suite.HeartbeatService.On("GetAllWithin", summaries[0].ToTime.T(), summaries[1].FromTime.T(), suite.TestUser).Return(filter(summaries[0].ToTime.T(), summaries[1].FromTime.T(), suite.TestHeartbeats), nil)
|
||||
suite.DurationService.On("Get", summaries[0].ToTime.T(), summaries[1].FromTime.T(), suite.TestUser).Return(filterDurations(summaries[0].ToTime.T(), summaries[1].FromTime.T(), suite.TestDurations), nil)
|
||||
|
||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
||||
|
||||
@ -308,11 +294,11 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
||||
assert.Equal(suite.T(), 90*time.Minute, result.TotalTime())
|
||||
assert.Equal(suite.T(), 45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject1))
|
||||
assert.Equal(suite.T(), 45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject2))
|
||||
suite.HeartbeatService.AssertNumberOfCalls(suite.T(), "GetAllWithin", 2+1+1)
|
||||
suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2+1+1)
|
||||
}
|
||||
|
||||
func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DuplicateSummaries() {
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.HeartbeatService, suite.AliasService, suite.ProjectLabelService)
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
|
||||
|
||||
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return([]*models.ProjectLabel{}, nil)
|
||||
|
||||
@ -347,8 +333,8 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DuplicateSumma
|
||||
summaries = append(summaries, &(*summaries[0])) // add same summary again -> mustn't be counted twice!
|
||||
|
||||
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
||||
suite.HeartbeatService.On("GetAllWithin", from, summaries[0].FromTime.T(), suite.TestUser).Return([]*models.Heartbeat{}, nil)
|
||||
suite.HeartbeatService.On("GetAllWithin", summaries[0].ToTime.T(), to, suite.TestUser).Return([]*models.Heartbeat{}, nil)
|
||||
suite.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser).Return([]*models.Duration{}, nil)
|
||||
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, suite.TestUser).Return([]*models.Duration{}, nil)
|
||||
|
||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
||||
|
||||
@ -356,11 +342,11 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DuplicateSumma
|
||||
assert.NotNil(suite.T(), result)
|
||||
assert.Len(suite.T(), result.Projects, 1)
|
||||
assert.Equal(suite.T(), summaries[0].Projects[0].Total*time.Second, result.TotalTime())
|
||||
suite.HeartbeatService.AssertNumberOfCalls(suite.T(), "GetAllWithin", 2)
|
||||
suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2)
|
||||
}
|
||||
|
||||
func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.HeartbeatService, suite.AliasService, suite.ProjectLabelService)
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
|
||||
|
||||
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return([]*models.ProjectLabel{}, nil)
|
||||
|
||||
@ -373,19 +359,19 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
|
||||
|
||||
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
||||
|
||||
heartbeats := filter(from, to, suite.TestHeartbeats)
|
||||
heartbeats = append(heartbeats, &models.Heartbeat{
|
||||
ID: rand.Uint64(),
|
||||
durations := filterDurations(from, to, suite.TestDurations)
|
||||
durations = append(durations, &models.Duration{
|
||||
UserID: TestUserId,
|
||||
Project: TestProject2,
|
||||
Language: TestLanguageGo,
|
||||
Editor: TestEditorGoland,
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(heartbeats[len(heartbeats)-1].Time.T().Add(10 * time.Second)),
|
||||
Time: models.CustomTime(durations[len(durations)-1].Time.T().Add(10 * time.Second)),
|
||||
Duration: 0, // not relevant here
|
||||
})
|
||||
|
||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(heartbeats, nil)
|
||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(durations, nil)
|
||||
suite.AliasService.On("InitializeUser", TestUserId).Return(nil)
|
||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject2, nil)
|
||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject2).Return(TestProject2, nil)
|
||||
@ -401,7 +387,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
|
||||
}
|
||||
|
||||
func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased_ProjectLabels() {
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.HeartbeatService, suite.AliasService, suite.ProjectLabelService)
|
||||
sut := NewSummaryService(suite.SummaryRepository, suite.DurationService, suite.AliasService, suite.ProjectLabelService)
|
||||
|
||||
var (
|
||||
from time.Time
|
||||
@ -412,20 +398,20 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased_ProjectLabels()
|
||||
|
||||
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
||||
|
||||
heartbeats := filter(from, to, suite.TestHeartbeats)
|
||||
heartbeats = append(heartbeats, &models.Heartbeat{
|
||||
ID: rand.Uint64(),
|
||||
durations := filterDurations(from, to, suite.TestDurations)
|
||||
durations = append(durations, &models.Duration{
|
||||
UserID: TestUserId,
|
||||
Project: TestProject2,
|
||||
Language: TestLanguageGo,
|
||||
Editor: TestEditorGoland,
|
||||
OperatingSystem: TestOsLinux,
|
||||
Machine: TestMachine1,
|
||||
Time: models.CustomTime(heartbeats[len(heartbeats)-1].Time.T().Add(10 * time.Second)),
|
||||
Time: models.CustomTime(durations[len(durations)-1].Time.T().Add(10 * time.Second)),
|
||||
Duration: 10 * time.Second,
|
||||
})
|
||||
|
||||
suite.ProjectLabelService.On("GetByUser", suite.TestUser.ID).Return(suite.TestLabels, nil).Once()
|
||||
suite.HeartbeatService.On("GetAllWithin", from, to, suite.TestUser).Return(heartbeats, nil)
|
||||
suite.DurationService.On("Get", from, to, suite.TestUser).Return(durations, nil)
|
||||
suite.AliasService.On("InitializeUser", TestUserId).Return(nil)
|
||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject1, nil)
|
||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject2).Return(TestProject1, nil)
|
||||
@ -435,14 +421,14 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased_ProjectLabels()
|
||||
|
||||
assert.Nil(suite.T(), err)
|
||||
assert.NotNil(suite.T(), result)
|
||||
assert.Equal(suite.T(), 160*time.Second, result.TotalTimeByKey(models.SummaryLabel, TestProjectLabel1))
|
||||
assert.Equal(suite.T(), 195*time.Second, result.TotalTimeByKey(models.SummaryLabel, TestProjectLabel1))
|
||||
}
|
||||
|
||||
func filter(from, to time.Time, heartbeats []*models.Heartbeat) []*models.Heartbeat {
|
||||
filtered := make([]*models.Heartbeat, 0, len(heartbeats))
|
||||
for _, h := range heartbeats {
|
||||
if (h.Time.T().Equal(from) || h.Time.T().After(from)) && h.Time.T().Before(to) {
|
||||
filtered = append(filtered, h)
|
||||
func filterDurations(from, to time.Time, durations []*models.Duration) []*models.Duration {
|
||||
filtered := make([]*models.Duration, 0, len(durations))
|
||||
for _, d := range durations {
|
||||
if (d.Time.T().Equal(from) || d.Time.T().After(from)) && d.Time.T().Before(to) {
|
||||
filtered = append(filtered, d)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
|
@ -1 +1 @@
|
||||
1.30.4
|
||||
2.0.0-SNAPSHOT-01
|
Loading…
Reference in New Issue
Block a user