refactor: language mapping augmentation of durations

This commit is contained in:
Ferdinand Mütsch
2025-02-21 14:25:36 +01:00
parent 3ee63bba65
commit 898154c5db
8 changed files with 2793 additions and 602 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -182,7 +182,7 @@ func main() {
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
durationService = services.NewDurationService(durationRepository, heartbeatService, userService)
durationService = services.NewDurationService(durationRepository, heartbeatService, userService, languageMappingService)
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, durationService, aliasService, projectLabelService)
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService, durationService)
reportService = services.NewReportService(summaryService, userService, mailService)

View File

@@ -0,0 +1,35 @@
package mocks
import (
"github.com/muety/wakapi/models"
"github.com/stretchr/testify/mock"
)
type LanguageMappingServiceMock struct {
mock.Mock
}
func (l *LanguageMappingServiceMock) GetById(u uint) (*models.LanguageMapping, error) {
args := l.Called(u)
return args.Get(0).(*models.LanguageMapping), args.Error(1)
}
func (l *LanguageMappingServiceMock) GetByUser(s string) ([]*models.LanguageMapping, error) {
args := l.Called(s)
return args.Get(0).([]*models.LanguageMapping), args.Error(1)
}
func (l *LanguageMappingServiceMock) ResolveByUser(s string) (map[string]string, error) {
args := l.Called(s)
return args.Get(0).(map[string]string), args.Error(1)
}
func (l *LanguageMappingServiceMock) Create(m *models.LanguageMapping) (*models.LanguageMapping, error) {
args := l.Called(m)
return args.Get(0).(*models.LanguageMapping), args.Error(1)
}
func (l *LanguageMappingServiceMock) Delete(m *models.LanguageMapping) error {
args := l.Called(m)
return args.Error(0)
}

View File

@@ -4,7 +4,9 @@ import (
"fmt"
"github.com/cespare/xxhash/v2"
"github.com/gohugoio/hashstructure"
"github.com/muety/wakapi/models/lib"
"log/slog"
"strings"
"time"
"unicode"
)
@@ -92,6 +94,19 @@ func (d *Duration) Hashed() *Duration {
return d
}
func (d *Duration) Augmented(languageMappings map[string]string) *Duration {
for ext, targetLang := range languageMappings {
langs, ok := lib.LanguagesByExtension["."+ext]
if !ok {
continue
}
if lang := langs[0]; strings.ToLower(d.Language) == strings.ToLower(lang) {
d.Language = targetLang
}
}
return d
}
func (d *Duration) GetKey(t uint8) (key string) {
switch t {
case SummaryProject:

View File

@@ -44,3 +44,10 @@ func (d *Durations) Last() *Duration {
}
return (*d)[d.Len()-1]
}
func (d Durations) Augmented(languageMappings map[string]string) Durations {
for _, item := range d {
item.Augmented(languageMappings)
}
return d
}

2097
models/lib/extensions.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,24 +20,26 @@ const heartbeatPadding = 0 * time.Second
const generateDurationsInterval = 12 * time.Hour
type DurationService struct {
config *config.Config
eventBus *hub.Hub
durationRepository repositories.IDurationRepository
heartbeatService IHeartbeatService
userService IUserService
lastUserJob map[string]time.Time
queue *artifex.Dispatcher
config *config.Config
eventBus *hub.Hub
durationRepository repositories.IDurationRepository
heartbeatService IHeartbeatService
userService IUserService
LanguageMappingService ILanguageMappingService
lastUserJob map[string]time.Time
queue *artifex.Dispatcher
}
func NewDurationService(durationRepository repositories.IDurationRepository, heartbeatService IHeartbeatService, userService IUserService) *DurationService {
func NewDurationService(durationRepository repositories.IDurationRepository, heartbeatService IHeartbeatService, userService IUserService, languageMappingService ILanguageMappingService) *DurationService {
srv := &DurationService{
config: config.Get(),
eventBus: config.EventBus(),
heartbeatService: heartbeatService,
userService: userService,
durationRepository: durationRepository,
lastUserJob: make(map[string]time.Time),
queue: config.GetQueue(config.QueueProcessing),
config: config.Get(),
eventBus: config.EventBus(),
heartbeatService: heartbeatService,
userService: userService,
LanguageMappingService: languageMappingService,
durationRepository: durationRepository,
lastUserJob: make(map[string]time.Time),
queue: config.GetQueue(config.QueueProcessing),
}
// 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
}
}(&sub1)
sub2 := srv.eventBus.Subscribe(0, config.EventLanguageMappingsChanged)
go func(sub *hub.Subscription) {
for m := range sub.Receiver {
userId := m.Fields[config.FieldUserId].(string)
user, err := srv.userService.GetUserById(userId)
if err != nil {
config.Log().Error("user not found for regenerating durations after language mapping change", "user", userId)
continue
}
slog.Info("regenerating durations because language mappings were updated", "user", userId)
srv.queue.Dispatch(func() {
srv.Regenerate(user, true)
})
}
}(&sub2)
return srv
}
@@ -95,7 +80,8 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User, filters *
// get cached
cached, err := srv.getCached(from, to, user, filters)
if err != nil {
return nil, err
config.Log().Error("failed to get cached durations", "user", user.ID, "from", from, "to", to, "error", err)
cached = models.Durations{}
}
// fill missing
@@ -167,11 +153,15 @@ func (srv *DurationService) RegenerateAll() {
}
func (srv *DurationService) getCached(from, to time.Time, user *models.User, filters *models.Filters) (models.Durations, error) {
languageMappings, err := srv.LanguageMappingService.ResolveByUser(user.ID)
if err != nil {
return nil, err
}
durations, err := srv.durationRepository.GetAllWithinByFilters(from, to, user, srv.filtersToColumnMap(filters))
if err != nil {
return nil, err
}
return models.Durations(durations).Sorted(), nil
return models.Durations(durations).Augmented(languageMappings).Sorted(), nil
}
func (srv *DurationService) getLive(from, to time.Time, user *models.User, interval time.Duration) (models.Durations, error) {

View File

@@ -38,13 +38,14 @@ const (
type DurationServiceTestSuite struct {
suite.Suite
TestUser *models.User
TestStartTime time.Time
TestHeartbeats []*models.Heartbeat
TestLabels []*models.ProjectLabel
DurationRepository *mocks.DurationRepositoryMock
HeartbeatService *mocks.HeartbeatServiceMock
UserService *mocks.UserServiceMock
TestUser *models.User
TestStartTime time.Time
TestHeartbeats []*models.Heartbeat
TestLabels []*models.ProjectLabel
DurationRepository *mocks.DurationRepositoryMock
HeartbeatService *mocks.HeartbeatServiceMock
UserService *mocks.UserServiceMock
LanguageMappingService *mocks.LanguageMappingServiceMock
}
func (suite *DurationServiceTestSuite) SetupSuite() {
@@ -131,6 +132,9 @@ func (suite *DurationServiceTestSuite) BeforeTest(suiteName, testName string) {
suite.DurationRepository = new(mocks.DurationRepositoryMock)
suite.HeartbeatService = new(mocks.HeartbeatServiceMock)
suite.UserService = new(mocks.UserServiceMock)
suite.LanguageMappingService = new(mocks.LanguageMappingServiceMock)
suite.LanguageMappingService.On("ResolveByUser", suite.TestUser.ID).Return(make(map[string]string), nil)
}
func TestDurationServiceTestSuite(t *testing.T) {
@@ -139,7 +143,7 @@ func TestDurationServiceTestSuite(t *testing.T) {
func (suite *DurationServiceTestSuite) TestDurationService_Get() {
// https://anchr.io/i/F0HEK.jpg
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
var (
from time.Time
@@ -188,7 +192,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() {
}
func (suite *DurationServiceTestSuite) TestDurationService_Get_Filtered() {
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
var (
from time.Time
@@ -211,7 +215,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_Filtered() {
}
func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomTimeout() {
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
var (
from time.Time
@@ -267,7 +271,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomTimeout() {
}
func (suite *DurationServiceTestSuite) TestDurationService_Get_Cached() {
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
var (
from time.Time
@@ -309,7 +313,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_Cached() {
}
func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomInterval() {
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
var (
from time.Time
@@ -329,6 +333,34 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomInterval()
assert.Empty(suite.T(), suite.DurationRepository.Calls)
}
func (suite *DurationServiceTestSuite) TestDurationService_Get_WithLanguageMapping() {
suite.LanguageMappingService.ExpectedCalls[0].Unset()
suite.LanguageMappingService.On("ResolveByUser", suite.TestUser.ID).Return(map[string]string{"go": "Golang"}, nil)
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
var (
from time.Time
to time.Time
durations models.Durations
err error
)
testDurations := []*models.Duration{
models.NewDurationFromHeartbeat(suite.TestHeartbeats[0]),
}
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Hour)
suite.DurationRepository.On("GetAllWithinByFilters", from, to, suite.TestUser, mock.Anything).Return(testDurations, nil)
suite.HeartbeatService.On("StreamAllWithin", mock.Anything, mock.Anything, suite.TestUser).Return(streamSlice([]*models.Heartbeat{}), nil)
durations, err = sut.Get(from, to, suite.TestUser, nil, nil, false)
assert.Nil(suite.T(), err)
assert.Len(suite.T(), durations, 1)
assert.Equal(suite.T(), "Golang", durations.First().Language)
}
func filterHeartbeats(from, to time.Time, heartbeats []*models.Heartbeat) []*models.Heartbeat {
filtered := make([]*models.Heartbeat, 0, len(heartbeats))
for _, h := range heartbeats {