diff --git a/cmd/params/params.go b/cmd/params/params.go index eee50074..c9a971fc 100644 --- a/cmd/params/params.go +++ b/cmd/params/params.go @@ -226,8 +226,11 @@ func LoadAPIParams(v *viper.Viper) (API, error) { backoffAtStr := vipertools.GetString(v, "internal.backoff_at") if backoffAtStr != "" { parsed, err := time.Parse(ini.DateFormat, backoffAtStr) + // nolint:gocritic if err != nil { log.Warnf("failed to parse backoff_at: %s", err) + } else if parsed.After(time.Now()) { + backoffAt = time.Now() } else { backoffAt = parsed } @@ -664,8 +667,11 @@ func LoadOfflineParams(v *viper.Viper) Offline { lastSentAtStr := vipertools.GetString(v, "internal.heartbeats_last_sent_at") if lastSentAtStr != "" { parsed, err := time.Parse(ini.DateFormat, lastSentAtStr) + // nolint:gocritic if err != nil { log.Warnf("failed to parse heartbeats_last_sent_at: %s", err) + } else if parsed.After(time.Now()) { + lastSentAt = time.Now() } else { lastSentAt = parsed } diff --git a/cmd/params/params_test.go b/cmd/params/params_test.go index 60ff54d8..9d29ce74 100644 --- a/cmd/params/params_test.go +++ b/cmd/params/params_test.go @@ -1771,6 +1771,16 @@ func TestLoadOfflineParams_LastSentAt_Err(t *testing.T) { assert.Zero(t, params.LastSentAt) } +func TestLoadOfflineParams_LastSentAtFuture(t *testing.T) { + v := viper.New() + lastSentAt := time.Now().Add(time.Duration(2) * time.Hour) + v.Set("internal.heartbeats_last_sent_at", lastSentAt.Format(inipkg.DateFormat)) + + params := cmdparams.LoadOfflineParams(v) + + assert.LessOrEqual(t, params.LastSentAt, time.Now()) +} + func TestLoadOfflineParams_SyncMax(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", 42) @@ -2148,6 +2158,22 @@ func TestLoadAPIParams_BackoffAtErr(t *testing.T) { }, params) } +func TestLoadAPIParams_BackoffAtFuture(t *testing.T) { + v := viper.New() + backoff := time.Now().Add(time.Duration(2) * time.Hour) + + v.Set("hostname", "my-computer") + v.Set("key", "00000000-0000-4000-8000-000000000000") + v.Set("internal.backoff_at", backoff.Format(inipkg.DateFormat)) + v.Set("internal.backoff_retries", "3") + + params, err := cmdparams.LoadAPIParams(v) + require.NoError(t, err) + + assert.Equal(t, 3, params.BackoffRetries) + assert.LessOrEqual(t, params.BackoffAt, time.Now()) +} + func TestLoadAPIParams_DisableSSLVerify_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") diff --git a/cmd/root.go b/cmd/root.go index 516dfd06..92373ebd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -100,7 +100,7 @@ func setFlags(cmd *cobra.Command, v *viper.Viper) { flags.String( "entity-type", "", - "Entity type for this heartbeat. Can be \"file\", \"domain\" or \"app\". Defaults to \"file\".", + "Entity type for this heartbeat. Can be \"file\", \"domain\", \"url\", or \"app\". Defaults to \"file\".", ) flags.StringSlice( "exclude", diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index ab274459..cf6eb8a1 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -63,12 +63,12 @@ func WithLengthValidator() heartbeat.HandleOption { func Filter(h heartbeat.Heartbeat, config Config) error { // filter by pattern if err := filterByPattern(h.Entity, config.Include, config.Exclude); err != nil { - return fmt.Errorf(fmt.Sprintf("filter by pattern: %s", err)) + return fmt.Errorf("filter by pattern: %s", err) } err := filterFileEntity(h, config) if err != nil { - return fmt.Errorf(fmt.Sprintf("filter file: %s", err)) + return fmt.Errorf("filter file: %s", err) } return nil @@ -92,7 +92,7 @@ func filterByPattern(entity string, include, exclude []regex.Regex) error { // filter by exclude pattern for _, pattern := range exclude { if pattern.MatchString(entity) { - return fmt.Errorf(fmt.Sprintf("skipping because matches exclude pattern %q", pattern.String())) + return fmt.Errorf("skipping because matches exclude pattern %q", pattern.String()) } } @@ -123,7 +123,7 @@ func filterFileEntity(h heartbeat.Heartbeat, config Config) error { // skip files that don't exist on disk if _, err := os.Stat(entity); os.IsNotExist(err) { - return fmt.Errorf(fmt.Sprintf("skipping because of non-existing file %q", entity)) + return fmt.Errorf("skipping because of non-existing file %q", entity) } // when including only with project file, skip files when the project doesn't have a .wakatime-project file diff --git a/pkg/heartbeat/entity.go b/pkg/heartbeat/entity.go index 4b64bbf9..17e0714c 100644 --- a/pkg/heartbeat/entity.go +++ b/pkg/heartbeat/entity.go @@ -13,6 +13,10 @@ const ( FileType EntityType = iota // DomainType represents a domain entity. DomainType + // URLType represents a url entity without the url params. + URLType + // EventType represents a meeting or calendar event. + EventType // AppType represents an app entity. AppType ) @@ -20,6 +24,8 @@ const ( const ( fileTypeString = "file" domainTypeString = "domain" + urlTypeString = "url" + eventTypeString = "event" appTypeString = "app" ) @@ -30,6 +36,10 @@ func ParseEntityType(s string) (EntityType, error) { return FileType, nil case domainTypeString: return DomainType, nil + case urlTypeString: + return URLType, nil + case eventTypeString: + return EventType, nil case appTypeString: return AppType, nil default: @@ -68,6 +78,10 @@ func (t EntityType) String() string { return fileTypeString case DomainType: return domainTypeString + case URLType: + return urlTypeString + case EventType: + return eventTypeString case AppType: return appTypeString default: diff --git a/pkg/heartbeat/entity_modifier_internal_test.go b/pkg/heartbeat/entity_modifier_internal_test.go index 6b31371b..a528d652 100644 --- a/pkg/heartbeat/entity_modifier_internal_test.go +++ b/pkg/heartbeat/entity_modifier_internal_test.go @@ -41,6 +41,30 @@ func TestIsXCodePlayground(t *testing.T) { } } +func TestIsXCodeProject(t *testing.T) { + tests := map[string]struct { + Dir string + Expected bool + }{ + "project directory": { + Dir: setupTestXCodePlayground(t, "wakatime.xcodeproj"), + Expected: true, + }, + "not project": { + Dir: setupTestXCodePlayground(t, "wakatime"), + Expected: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ret := isXCodeProject(test.Dir) + + assert.Equal(t, test.Expected, ret) + }) + } +} + func setupTestXCodePlayground(t *testing.T, dir string) string { tmpDir := t.TempDir() diff --git a/pkg/heartbeat/entity_modify.go b/pkg/heartbeat/entity_modify.go index 7977995f..f25a7d53 100644 --- a/pkg/heartbeat/entity_modify.go +++ b/pkg/heartbeat/entity_modify.go @@ -19,6 +19,10 @@ func WithEntityModifier() HandleOption { if h.EntityType == FileType && isXCodePlayground(h.Entity) { hh[n].Entity = filepath.Join(h.Entity, "Contents.swift") } + // Support XCode projects + if h.EntityType == FileType && isXCodeProject(h.Entity) { + hh[n].Entity = filepath.Join(h.Entity, "project.pbxproj") + } } return next(hh) @@ -35,3 +39,11 @@ func isXCodePlayground(fp string) bool { return isDir(fp) } + +func isXCodeProject(fp string) bool { + if !(strings.HasSuffix(fp, ".xcodeproj")) { + return false + } + + return isDir(fp) +} diff --git a/pkg/heartbeat/entity_test.go b/pkg/heartbeat/entity_test.go index 7922c279..2d61afa8 100644 --- a/pkg/heartbeat/entity_test.go +++ b/pkg/heartbeat/entity_test.go @@ -14,6 +14,8 @@ func typeTests() map[string]heartbeat.EntityType { return map[string]heartbeat.EntityType{ "file": heartbeat.FileType, "domain": heartbeat.DomainType, + "url": heartbeat.URLType, + "event": heartbeat.EventType, "app": heartbeat.AppType, } }