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
|
userService services.IUserService
|
||||||
languageMappingService services.ILanguageMappingService
|
languageMappingService services.ILanguageMappingService
|
||||||
projectLabelService services.IProjectLabelService
|
projectLabelService services.IProjectLabelService
|
||||||
|
durationService services.IDurationService
|
||||||
summaryService services.ISummaryService
|
summaryService services.ISummaryService
|
||||||
aggregationService services.IAggregationService
|
aggregationService services.IAggregationService
|
||||||
mailService services.IMailService
|
mailService services.IMailService
|
||||||
@ -154,7 +155,8 @@ func main() {
|
|||||||
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
|
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
|
||||||
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
|
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
|
||||||
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
|
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)
|
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
||||||
keyValueService = services.NewKeyValueService(keyValueRepository)
|
keyValueService = services.NewKeyValueService(keyValueRepository)
|
||||||
reportService = services.NewReportService(summaryService, userService, mailService)
|
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
|
SendReport(*models.User, *models.Report) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IDurationService interface {
|
||||||
|
Get(time.Time, time.Time, *models.User) ([]*models.Duration, error)
|
||||||
|
}
|
||||||
|
|
||||||
type ISummaryService interface {
|
type ISummaryService interface {
|
||||||
Aliased(time.Time, time.Time, *models.User, SummaryRetriever, bool) (*models.Summary, error)
|
Aliased(time.Time, time.Time, *models.User, SummaryRetriever, bool) (*models.Summary, error)
|
||||||
Retrieve(time.Time, time.Time, *models.User) (*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/models"
|
||||||
"github.com/muety/wakapi/repositories"
|
"github.com/muety/wakapi/repositories"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"math"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const HeartbeatDiffThreshold = 2 * time.Minute
|
|
||||||
|
|
||||||
type SummaryService struct {
|
type SummaryService struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
cache *cache.Cache
|
cache *cache.Cache
|
||||||
eventBus *hub.Hub
|
eventBus *hub.Hub
|
||||||
repository repositories.ISummaryRepository
|
repository repositories.ISummaryRepository
|
||||||
heartbeatService IHeartbeatService
|
durationService IDurationService
|
||||||
aliasService IAliasService
|
aliasService IAliasService
|
||||||
projectLabelService IProjectLabelService
|
projectLabelService IProjectLabelService
|
||||||
}
|
}
|
||||||
|
|
||||||
type SummaryRetriever func(f, t time.Time, u *models.User) (*models.Summary, error)
|
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{
|
srv := &SummaryService{
|
||||||
config: config.Get(),
|
config: config.Get(),
|
||||||
cache: cache.New(24*time.Hour, 24*time.Hour),
|
cache: cache.New(24*time.Hour, 24*time.Hour),
|
||||||
eventBus: config.EventBus(),
|
eventBus: config.EventBus(),
|
||||||
repository: summaryRepo,
|
repository: summaryRepo,
|
||||||
heartbeatService: heartbeatService,
|
durationService: durationService,
|
||||||
aliasService: aliasService,
|
aliasService: aliasService,
|
||||||
projectLabelService: projectLabelService,
|
projectLabelService: projectLabelService,
|
||||||
}
|
}
|
||||||
@ -99,7 +96,7 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*mod
|
|||||||
return nil, err
|
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)
|
missingIntervals := srv.getMissingIntervals(from, to, summaries)
|
||||||
for _, interval := range missingIntervals {
|
for _, interval := range missingIntervals {
|
||||||
if s, err := srv.Summarize(interval.Start, interval.End, user); err == nil {
|
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) {
|
func (srv *SummaryService) Summarize(from, to time.Time, user *models.User) (*models.Summary, error) {
|
||||||
// Initialize and fetch data
|
// Initialize and fetch data
|
||||||
var heartbeats models.Heartbeats
|
var durations models.Durations
|
||||||
if rawHeartbeats, err := srv.heartbeatService.GetAllWithin(from, to, user); err == nil {
|
if result, err := srv.durationService.Get(from, to, user); err == nil {
|
||||||
heartbeats = rawHeartbeats
|
durations = result
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -132,10 +129,10 @@ func (srv *SummaryService) Summarize(from, to time.Time, user *models.User) (*mo
|
|||||||
typedAggregations := make(chan models.SummaryItemContainer)
|
typedAggregations := make(chan models.SummaryItemContainer)
|
||||||
defer close(typedAggregations)
|
defer close(typedAggregations)
|
||||||
for _, t := range types {
|
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 projectItems []*models.SummaryItem
|
||||||
var languageItems []*models.SummaryItem
|
var languageItems []*models.SummaryItem
|
||||||
var editorItems []*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 {
|
if durations.Len() > 0 {
|
||||||
from = time.Time(heartbeats.First().Time)
|
from = time.Time(durations.First().Time)
|
||||||
to = time.Time(heartbeats.Last().Time)
|
to = time.Time(durations.Last().Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
summary := &models.Summary{
|
summary := &models.Summary{
|
||||||
@ -195,34 +192,15 @@ func (srv *SummaryService) Insert(summary *models.Summary) error {
|
|||||||
|
|
||||||
// Private summary generation and utility methods
|
// Private summary generation and utility methods
|
||||||
|
|
||||||
func (srv *SummaryService) aggregateBy(heartbeats []*models.Heartbeat, summaryType uint8, c chan models.SummaryItemContainer) {
|
func (srv *SummaryService) aggregateBy(durations []*models.Duration, summaryType uint8, c chan models.SummaryItemContainer) {
|
||||||
durations := make(map[string]time.Duration)
|
mapping := make(map[string]time.Duration)
|
||||||
|
|
||||||
for i, h := range heartbeats {
|
for _, d := range durations {
|
||||||
key := h.GetKey(summaryType)
|
mapping[d.GetKey(summaryType)] += d.Duration
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items := make([]*models.SummaryItem, 0)
|
items := make([]*models.SummaryItem, 0)
|
||||||
for k, v := range durations {
|
for k, v := range mapping {
|
||||||
items = append(items, &models.SummaryItem{
|
items = append(items, &models.SummaryItem{
|
||||||
Key: k,
|
Key: k,
|
||||||
Total: v / time.Second,
|
Total: v / time.Second,
|
||||||
|
@ -13,33 +13,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TestUserId = "muety"
|
TestProjectLabel1 = "private"
|
||||||
TestProject1 = "test-project-1"
|
TestProjectLabel2 = "work"
|
||||||
TestProject2 = "test-project-2"
|
TestProjectLabel3 = "non-existing"
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SummaryServiceTestSuite struct {
|
type SummaryServiceTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
TestUser *models.User
|
TestUser *models.User
|
||||||
TestStartTime time.Time
|
TestStartTime time.Time
|
||||||
TestHeartbeats []*models.Heartbeat
|
TestDurations []*models.Duration
|
||||||
TestLabels []*models.ProjectLabel
|
TestLabels []*models.ProjectLabel
|
||||||
SummaryRepository *mocks.SummaryRepositoryMock
|
SummaryRepository *mocks.SummaryRepositoryMock
|
||||||
HeartbeatService *mocks.HeartbeatServiceMock
|
DurationService *mocks.DurationServiceMock
|
||||||
AliasService *mocks.AliasServiceMock
|
AliasService *mocks.AliasServiceMock
|
||||||
ProjectLabelService *mocks.ProjectLabelServiceMock
|
ProjectLabelService *mocks.ProjectLabelServiceMock
|
||||||
}
|
}
|
||||||
@ -48,9 +34,8 @@ func (suite *SummaryServiceTestSuite) SetupSuite() {
|
|||||||
suite.TestUser = &models.User{ID: TestUserId}
|
suite.TestUser = &models.User{ID: TestUserId}
|
||||||
|
|
||||||
suite.TestStartTime = time.Unix(0, MinUnixTime1)
|
suite.TestStartTime = time.Unix(0, MinUnixTime1)
|
||||||
suite.TestHeartbeats = []*models.Heartbeat{
|
suite.TestDurations = []*models.Duration{
|
||||||
{
|
{
|
||||||
ID: rand.Uint64(),
|
|
||||||
UserID: TestUserId,
|
UserID: TestUserId,
|
||||||
Project: TestProject1,
|
Project: TestProject1,
|
||||||
Language: TestLanguageGo,
|
Language: TestLanguageGo,
|
||||||
@ -58,19 +43,19 @@ func (suite *SummaryServiceTestSuite) SetupSuite() {
|
|||||||
OperatingSystem: TestOsLinux,
|
OperatingSystem: TestOsLinux,
|
||||||
Machine: TestMachine1,
|
Machine: TestMachine1,
|
||||||
Time: models.CustomTime(suite.TestStartTime),
|
Time: models.CustomTime(suite.TestStartTime),
|
||||||
|
Duration: 150 * time.Second,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: rand.Uint64(),
|
|
||||||
UserID: TestUserId,
|
UserID: TestUserId,
|
||||||
Project: TestProject1,
|
Project: TestProject1,
|
||||||
Language: TestLanguageGo,
|
Language: TestLanguageGo,
|
||||||
Editor: TestEditorGoland,
|
Editor: TestEditorGoland,
|
||||||
OperatingSystem: TestOsLinux,
|
OperatingSystem: TestOsLinux,
|
||||||
Machine: TestMachine1,
|
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,
|
UserID: TestUserId,
|
||||||
Project: TestProject1,
|
Project: TestProject1,
|
||||||
Language: TestLanguageGo,
|
Language: TestLanguageGo,
|
||||||
@ -78,6 +63,7 @@ func (suite *SummaryServiceTestSuite) SetupSuite() {
|
|||||||
OperatingSystem: TestOsLinux,
|
OperatingSystem: TestOsLinux,
|
||||||
Machine: TestMachine1,
|
Machine: TestMachine1,
|
||||||
Time: models.CustomTime(suite.TestStartTime.Add(3 * time.Minute)),
|
Time: models.CustomTime(suite.TestStartTime.Add(3 * time.Minute)),
|
||||||
|
Duration: 15 * time.Second,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
suite.TestLabels = []*models.ProjectLabel{
|
suite.TestLabels = []*models.ProjectLabel{
|
||||||
@ -98,7 +84,7 @@ func (suite *SummaryServiceTestSuite) SetupSuite() {
|
|||||||
|
|
||||||
func (suite *SummaryServiceTestSuite) BeforeTest(suiteName, testName string) {
|
func (suite *SummaryServiceTestSuite) BeforeTest(suiteName, testName string) {
|
||||||
suite.SummaryRepository = new(mocks.SummaryRepositoryMock)
|
suite.SummaryRepository = new(mocks.SummaryRepositoryMock)
|
||||||
suite.HeartbeatService = new(mocks.HeartbeatServiceMock)
|
suite.DurationService = new(mocks.DurationServiceMock)
|
||||||
suite.AliasService = new(mocks.AliasServiceMock)
|
suite.AliasService = new(mocks.AliasServiceMock)
|
||||||
suite.ProjectLabelService = new(mocks.ProjectLabelServiceMock)
|
suite.ProjectLabelService = new(mocks.ProjectLabelServiceMock)
|
||||||
}
|
}
|
||||||
@ -108,7 +94,7 @@ func TestSummaryServiceTestSuite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
|
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 (
|
var (
|
||||||
from time.Time
|
from time.Time
|
||||||
@ -119,7 +105,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
|
|||||||
|
|
||||||
/* TEST 1 */
|
/* TEST 1 */
|
||||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(-1*time.Minute)
|
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)
|
result, err = sut.Summarize(from, to, suite.TestUser)
|
||||||
|
|
||||||
@ -132,36 +118,36 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() {
|
|||||||
|
|
||||||
/* TEST 2 */
|
/* TEST 2 */
|
||||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Second)
|
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)
|
result, err = sut.Summarize(from, to, suite.TestUser)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
assert.Equal(suite.T(), suite.TestHeartbeats[0].Time.T(), result.FromTime.T())
|
assert.Equal(suite.T(), suite.TestDurations[0].Time.T(), result.FromTime.T())
|
||||||
assert.Equal(suite.T(), suite.TestHeartbeats[0].Time.T(), result.ToTime.T())
|
assert.Equal(suite.T(), suite.TestDurations[0].Time.T(), result.ToTime.T())
|
||||||
assert.Zero(suite.T(), result.TotalTime())
|
assert.Equal(suite.T(), 150*time.Second, result.TotalTime())
|
||||||
assertNumAllItems(suite.T(), 1, result, "")
|
assertNumAllItems(suite.T(), 1, result, "")
|
||||||
|
|
||||||
/* TEST 3 */
|
/* TEST 3 */
|
||||||
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
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)
|
result, err = sut.Summarize(from, to, suite.TestUser)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
assert.Equal(suite.T(), suite.TestHeartbeats[0].Time.T(), result.FromTime.T())
|
assert.Equal(suite.T(), suite.TestDurations[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(), suite.TestDurations[len(suite.TestDurations)-1].Time.T(), result.ToTime.T())
|
||||||
assert.Equal(suite.T(), 150*time.Second, result.TotalTime())
|
assert.Equal(suite.T(), 185*time.Second, result.TotalTime())
|
||||||
assert.Equal(suite.T(), 30*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorGoland))
|
assert.Equal(suite.T(), 170*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorGoland))
|
||||||
assert.Equal(suite.T(), 120*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorVscode))
|
assert.Equal(suite.T(), 15*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorVscode))
|
||||||
assert.Len(suite.T(), result.Editors, 2)
|
assert.Len(suite.T(), result.Editors, 2)
|
||||||
assertNumAllItems(suite.T(), 1, result, "e")
|
assertNumAllItems(suite.T(), 1, result, "e")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
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 (
|
var (
|
||||||
summaries []*models.Summary
|
summaries []*models.Summary
|
||||||
@ -194,8 +180,8 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suite.SummaryRepository.On("GetByUserWithin", suite.TestUser, from, to).Return(summaries, nil)
|
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.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser).Return([]*models.Duration{}, nil)
|
||||||
suite.HeartbeatService.On("GetAllWithin", summaries[0].ToTime.T(), to, suite.TestUser).Return([]*models.Heartbeat{}, nil)
|
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, suite.TestUser).Return([]*models.Duration{}, nil)
|
||||||
|
|
||||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
result, err = sut.Retrieve(from, to, suite.TestUser)
|
||||||
|
|
||||||
@ -203,7 +189,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
|
|||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
assert.Len(suite.T(), result.Projects, 1)
|
assert.Len(suite.T(), result.Projects, 1)
|
||||||
assert.Equal(suite.T(), summaries[0].Projects[0].Total*time.Second, result.TotalTime())
|
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 */
|
/* TEST 2 */
|
||||||
from, to = suite.TestStartTime.Add(-10*time.Minute), suite.TestStartTime.Add(12*time.Hour)
|
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.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)
|
result, err = sut.Retrieve(from, to, suite.TestUser)
|
||||||
|
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
assert.Len(suite.T(), result.Projects, 2)
|
assert.Len(suite.T(), result.Projects, 2)
|
||||||
assert.Equal(suite.T(), 150*time.Second+90*time.Minute, result.TotalTime())
|
assert.Equal(suite.T(), 185*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+45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject1))
|
||||||
assert.Equal(suite.T(), 45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject2))
|
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 */
|
/* 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
|
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.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)
|
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(), 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, TestProject1))
|
||||||
assert.Equal(suite.T(), 45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject2))
|
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() {
|
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)
|
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!
|
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.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.DurationService.On("Get", from, summaries[0].FromTime.T(), suite.TestUser).Return([]*models.Duration{}, nil)
|
||||||
suite.HeartbeatService.On("GetAllWithin", summaries[0].ToTime.T(), to, suite.TestUser).Return([]*models.Heartbeat{}, nil)
|
suite.DurationService.On("Get", summaries[0].ToTime.T(), to, suite.TestUser).Return([]*models.Duration{}, nil)
|
||||||
|
|
||||||
result, err = sut.Retrieve(from, to, suite.TestUser)
|
result, err = sut.Retrieve(from, to, suite.TestUser)
|
||||||
|
|
||||||
@ -356,11 +342,11 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DuplicateSumma
|
|||||||
assert.NotNil(suite.T(), result)
|
assert.NotNil(suite.T(), result)
|
||||||
assert.Len(suite.T(), result.Projects, 1)
|
assert.Len(suite.T(), result.Projects, 1)
|
||||||
assert.Equal(suite.T(), summaries[0].Projects[0].Total*time.Second, result.TotalTime())
|
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() {
|
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)
|
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)
|
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
||||||
|
|
||||||
heartbeats := filter(from, to, suite.TestHeartbeats)
|
durations := filterDurations(from, to, suite.TestDurations)
|
||||||
heartbeats = append(heartbeats, &models.Heartbeat{
|
durations = append(durations, &models.Duration{
|
||||||
ID: rand.Uint64(),
|
|
||||||
UserID: TestUserId,
|
UserID: TestUserId,
|
||||||
Project: TestProject2,
|
Project: TestProject2,
|
||||||
Language: TestLanguageGo,
|
Language: TestLanguageGo,
|
||||||
Editor: TestEditorGoland,
|
Editor: TestEditorGoland,
|
||||||
OperatingSystem: TestOsLinux,
|
OperatingSystem: TestOsLinux,
|
||||||
Machine: TestMachine1,
|
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("InitializeUser", TestUserId).Return(nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject2, nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject2, nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject2).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() {
|
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 (
|
var (
|
||||||
from time.Time
|
from time.Time
|
||||||
@ -412,20 +398,20 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased_ProjectLabels()
|
|||||||
|
|
||||||
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour)
|
||||||
|
|
||||||
heartbeats := filter(from, to, suite.TestHeartbeats)
|
durations := filterDurations(from, to, suite.TestDurations)
|
||||||
heartbeats = append(heartbeats, &models.Heartbeat{
|
durations = append(durations, &models.Duration{
|
||||||
ID: rand.Uint64(),
|
|
||||||
UserID: TestUserId,
|
UserID: TestUserId,
|
||||||
Project: TestProject2,
|
Project: TestProject2,
|
||||||
Language: TestLanguageGo,
|
Language: TestLanguageGo,
|
||||||
Editor: TestEditorGoland,
|
Editor: TestEditorGoland,
|
||||||
OperatingSystem: TestOsLinux,
|
OperatingSystem: TestOsLinux,
|
||||||
Machine: TestMachine1,
|
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.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("InitializeUser", TestUserId).Return(nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject1, nil)
|
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject1).Return(TestProject1, nil)
|
||||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, TestProject2).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.Nil(suite.T(), err)
|
||||||
assert.NotNil(suite.T(), result)
|
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 {
|
func filterDurations(from, to time.Time, durations []*models.Duration) []*models.Duration {
|
||||||
filtered := make([]*models.Heartbeat, 0, len(heartbeats))
|
filtered := make([]*models.Duration, 0, len(durations))
|
||||||
for _, h := range heartbeats {
|
for _, d := range durations {
|
||||||
if (h.Time.T().Equal(from) || h.Time.T().After(from)) && h.Time.T().Before(to) {
|
if (d.Time.T().Equal(from) || d.Time.T().After(from)) && d.Time.T().Before(to) {
|
||||||
filtered = append(filtered, h)
|
filtered = append(filtered, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filtered
|
return filtered
|
||||||
|
@ -1 +1 @@
|
|||||||
1.30.4
|
2.0.0-SNAPSHOT-01
|
Loading…
Reference in New Issue
Block a user