Skip to content

Commit 898154c

Browse files
committedFeb 21, 2025·
refactor: language mapping augmentation of durations
1 parent 3ee63bb commit 898154c

File tree

8 files changed

+2793
-602
lines changed

8 files changed

+2793
-602
lines changed
 

‎coverage/coverage.out

+570-555
Large diffs are not rendered by default.

‎main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func main() {
182182
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
183183
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
184184
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
185-
durationService = services.NewDurationService(durationRepository, heartbeatService, userService)
185+
durationService = services.NewDurationService(durationRepository, heartbeatService, userService, languageMappingService)
186186
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, durationService, aliasService, projectLabelService)
187187
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService, durationService)
188188
reportService = services.NewReportService(summaryService, userService, mailService)

‎mocks/language_mapping_service.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package mocks
2+
3+
import (
4+
"github.com/muety/wakapi/models"
5+
"github.com/stretchr/testify/mock"
6+
)
7+
8+
type LanguageMappingServiceMock struct {
9+
mock.Mock
10+
}
11+
12+
func (l *LanguageMappingServiceMock) GetById(u uint) (*models.LanguageMapping, error) {
13+
args := l.Called(u)
14+
return args.Get(0).(*models.LanguageMapping), args.Error(1)
15+
}
16+
17+
func (l *LanguageMappingServiceMock) GetByUser(s string) ([]*models.LanguageMapping, error) {
18+
args := l.Called(s)
19+
return args.Get(0).([]*models.LanguageMapping), args.Error(1)
20+
}
21+
22+
func (l *LanguageMappingServiceMock) ResolveByUser(s string) (map[string]string, error) {
23+
args := l.Called(s)
24+
return args.Get(0).(map[string]string), args.Error(1)
25+
}
26+
27+
func (l *LanguageMappingServiceMock) Create(m *models.LanguageMapping) (*models.LanguageMapping, error) {
28+
args := l.Called(m)
29+
return args.Get(0).(*models.LanguageMapping), args.Error(1)
30+
}
31+
32+
func (l *LanguageMappingServiceMock) Delete(m *models.LanguageMapping) error {
33+
args := l.Called(m)
34+
return args.Error(0)
35+
}

‎models/duration.go

+15
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"fmt"
55
"github.com/cespare/xxhash/v2"
66
"github.com/gohugoio/hashstructure"
7+
"github.com/muety/wakapi/models/lib"
78
"log/slog"
9+
"strings"
810
"time"
911
"unicode"
1012
)
@@ -92,6 +94,19 @@ func (d *Duration) Hashed() *Duration {
9294
return d
9395
}
9496

97+
func (d *Duration) Augmented(languageMappings map[string]string) *Duration {
98+
for ext, targetLang := range languageMappings {
99+
langs, ok := lib.LanguagesByExtension["."+ext]
100+
if !ok {
101+
continue
102+
}
103+
if lang := langs[0]; strings.ToLower(d.Language) == strings.ToLower(lang) {
104+
d.Language = targetLang
105+
}
106+
}
107+
return d
108+
}
109+
95110
func (d *Duration) GetKey(t uint8) (key string) {
96111
switch t {
97112
case SummaryProject:

‎models/durations.go

+7
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,10 @@ func (d *Durations) Last() *Duration {
4444
}
4545
return (*d)[d.Len()-1]
4646
}
47+
48+
func (d Durations) Augmented(languageMappings map[string]string) Durations {
49+
for _, item := range d {
50+
item.Augmented(languageMappings)
51+
}
52+
return d
53+
}

‎models/lib/extensions.go

+2,097
Large diffs are not rendered by default.

‎services/duration.go

+24-34
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,26 @@ const heartbeatPadding = 0 * time.Second
2020
const generateDurationsInterval = 12 * time.Hour
2121

2222
type DurationService struct {
23-
config *config.Config
24-
eventBus *hub.Hub
25-
durationRepository repositories.IDurationRepository
26-
heartbeatService IHeartbeatService
27-
userService IUserService
28-
lastUserJob map[string]time.Time
29-
queue *artifex.Dispatcher
23+
config *config.Config
24+
eventBus *hub.Hub
25+
durationRepository repositories.IDurationRepository
26+
heartbeatService IHeartbeatService
27+
userService IUserService
28+
LanguageMappingService ILanguageMappingService
29+
lastUserJob map[string]time.Time
30+
queue *artifex.Dispatcher
3031
}
3132

32-
func NewDurationService(durationRepository repositories.IDurationRepository, heartbeatService IHeartbeatService, userService IUserService) *DurationService {
33+
func NewDurationService(durationRepository repositories.IDurationRepository, heartbeatService IHeartbeatService, userService IUserService, languageMappingService ILanguageMappingService) *DurationService {
3334
srv := &DurationService{
34-
config: config.Get(),
35-
eventBus: config.EventBus(),
36-
heartbeatService: heartbeatService,
37-
userService: userService,
38-
durationRepository: durationRepository,
39-
lastUserJob: make(map[string]time.Time),
40-
queue: config.GetQueue(config.QueueProcessing),
35+
config: config.Get(),
36+
eventBus: config.EventBus(),
37+
heartbeatService: heartbeatService,
38+
userService: userService,
39+
LanguageMappingService: languageMappingService,
40+
durationRepository: durationRepository,
41+
lastUserJob: make(map[string]time.Time),
42+
queue: config.GetQueue(config.QueueProcessing),
4143
}
4244

4345
// TODO: refactor to updating durations on-the-fly as heartbeats flow in, instead of batch-wise
@@ -56,23 +58,6 @@ func NewDurationService(durationRepository repositories.IDurationRepository, hea
5658
}
5759
}(&sub1)
5860

59-
sub2 := srv.eventBus.Subscribe(0, config.EventLanguageMappingsChanged)
60-
go func(sub *hub.Subscription) {
61-
for m := range sub.Receiver {
62-
userId := m.Fields[config.FieldUserId].(string)
63-
user, err := srv.userService.GetUserById(userId)
64-
if err != nil {
65-
config.Log().Error("user not found for regenerating durations after language mapping change", "user", userId)
66-
continue
67-
}
68-
69-
slog.Info("regenerating durations because language mappings were updated", "user", userId)
70-
srv.queue.Dispatch(func() {
71-
srv.Regenerate(user, true)
72-
})
73-
}
74-
}(&sub2)
75-
7661
return srv
7762
}
7863

@@ -95,7 +80,8 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User, filters *
9580
// get cached
9681
cached, err := srv.getCached(from, to, user, filters)
9782
if err != nil {
98-
return nil, err
83+
config.Log().Error("failed to get cached durations", "user", user.ID, "from", from, "to", to, "error", err)
84+
cached = models.Durations{}
9985
}
10086

10187
// fill missing
@@ -167,11 +153,15 @@ func (srv *DurationService) RegenerateAll() {
167153
}
168154

169155
func (srv *DurationService) getCached(from, to time.Time, user *models.User, filters *models.Filters) (models.Durations, error) {
156+
languageMappings, err := srv.LanguageMappingService.ResolveByUser(user.ID)
157+
if err != nil {
158+
return nil, err
159+
}
170160
durations, err := srv.durationRepository.GetAllWithinByFilters(from, to, user, srv.filtersToColumnMap(filters))
171161
if err != nil {
172162
return nil, err
173163
}
174-
return models.Durations(durations).Sorted(), nil
164+
return models.Durations(durations).Augmented(languageMappings).Sorted(), nil
175165
}
176166

177167
func (srv *DurationService) getLive(from, to time.Time, user *models.User, interval time.Duration) (models.Durations, error) {

‎services/duration_test.go

+44-12
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@ const (
3838

3939
type DurationServiceTestSuite struct {
4040
suite.Suite
41-
TestUser *models.User
42-
TestStartTime time.Time
43-
TestHeartbeats []*models.Heartbeat
44-
TestLabels []*models.ProjectLabel
45-
DurationRepository *mocks.DurationRepositoryMock
46-
HeartbeatService *mocks.HeartbeatServiceMock
47-
UserService *mocks.UserServiceMock
41+
TestUser *models.User
42+
TestStartTime time.Time
43+
TestHeartbeats []*models.Heartbeat
44+
TestLabels []*models.ProjectLabel
45+
DurationRepository *mocks.DurationRepositoryMock
46+
HeartbeatService *mocks.HeartbeatServiceMock
47+
UserService *mocks.UserServiceMock
48+
LanguageMappingService *mocks.LanguageMappingServiceMock
4849
}
4950

5051
func (suite *DurationServiceTestSuite) SetupSuite() {
@@ -131,6 +132,9 @@ func (suite *DurationServiceTestSuite) BeforeTest(suiteName, testName string) {
131132
suite.DurationRepository = new(mocks.DurationRepositoryMock)
132133
suite.HeartbeatService = new(mocks.HeartbeatServiceMock)
133134
suite.UserService = new(mocks.UserServiceMock)
135+
suite.LanguageMappingService = new(mocks.LanguageMappingServiceMock)
136+
137+
suite.LanguageMappingService.On("ResolveByUser", suite.TestUser.ID).Return(make(map[string]string), nil)
134138
}
135139

136140
func TestDurationServiceTestSuite(t *testing.T) {
@@ -139,7 +143,7 @@ func TestDurationServiceTestSuite(t *testing.T) {
139143

140144
func (suite *DurationServiceTestSuite) TestDurationService_Get() {
141145
// https://anchr.io/i/F0HEK.jpg
142-
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
146+
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
143147

144148
var (
145149
from time.Time
@@ -188,7 +192,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() {
188192
}
189193

190194
func (suite *DurationServiceTestSuite) TestDurationService_Get_Filtered() {
191-
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
195+
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
192196

193197
var (
194198
from time.Time
@@ -211,7 +215,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_Filtered() {
211215
}
212216

213217
func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomTimeout() {
214-
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
218+
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
215219

216220
var (
217221
from time.Time
@@ -267,7 +271,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomTimeout() {
267271
}
268272

269273
func (suite *DurationServiceTestSuite) TestDurationService_Get_Cached() {
270-
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
274+
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
271275

272276
var (
273277
from time.Time
@@ -309,7 +313,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_Cached() {
309313
}
310314

311315
func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomInterval() {
312-
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
316+
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
313317

314318
var (
315319
from time.Time
@@ -329,6 +333,34 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomInterval()
329333
assert.Empty(suite.T(), suite.DurationRepository.Calls)
330334
}
331335

336+
func (suite *DurationServiceTestSuite) TestDurationService_Get_WithLanguageMapping() {
337+
suite.LanguageMappingService.ExpectedCalls[0].Unset()
338+
suite.LanguageMappingService.On("ResolveByUser", suite.TestUser.ID).Return(map[string]string{"go": "Golang"}, nil)
339+
340+
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
341+
342+
var (
343+
from time.Time
344+
to time.Time
345+
durations models.Durations
346+
err error
347+
)
348+
349+
testDurations := []*models.Duration{
350+
models.NewDurationFromHeartbeat(suite.TestHeartbeats[0]),
351+
}
352+
353+
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Hour)
354+
suite.DurationRepository.On("GetAllWithinByFilters", from, to, suite.TestUser, mock.Anything).Return(testDurations, nil)
355+
suite.HeartbeatService.On("StreamAllWithin", mock.Anything, mock.Anything, suite.TestUser).Return(streamSlice([]*models.Heartbeat{}), nil)
356+
357+
durations, err = sut.Get(from, to, suite.TestUser, nil, nil, false)
358+
359+
assert.Nil(suite.T(), err)
360+
assert.Len(suite.T(), durations, 1)
361+
assert.Equal(suite.T(), "Golang", durations.First().Language)
362+
}
363+
332364
func filterHeartbeats(from, to time.Time, heartbeats []*models.Heartbeat) []*models.Heartbeat {
333365
filtered := make([]*models.Heartbeat, 0, len(heartbeats))
334366
for _, h := range heartbeats {

0 commit comments

Comments
 (0)
Please sign in to comment.