From e25375d80d69d0ad6d05f9186f318a064f781c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Guard=C3=A3o=20Gandarez?= Date: Tue, 23 Jul 2024 08:45:26 -0300 Subject: [PATCH 1/4] Trim spaces in summary tests --- pkg/summary/summary_test.go | 4 ++-- pkg/summary/testdata/statusbar_today.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/summary/summary_test.go b/pkg/summary/summary_test.go index cade7465..d9b03cc2 100644 --- a/pkg/summary/summary_test.go +++ b/pkg/summary/summary_test.go @@ -36,7 +36,7 @@ func TestRenderToday(t *testing.T) { rendered, err := summary.RenderToday(testSummary(), false, test.Output) require.NoError(t, err) - assert.Equal(t, strings.TrimSpace(test.Expected), strings.TrimSpace(rendered)) + assert.Equal(t, test.Expected, rendered) }) } } @@ -62,7 +62,7 @@ func readFile(t *testing.T, fp string) string { data, err := os.ReadFile(fp) require.NoError(t, err) - return string(data) + return strings.TrimSpace(string(data)) } func testSummary() *summary.Summary { diff --git a/pkg/summary/testdata/statusbar_today.json b/pkg/summary/testdata/statusbar_today.json index ade8eba9..cc422884 100644 --- a/pkg/summary/testdata/statusbar_today.json +++ b/pkg/summary/testdata/statusbar_today.json @@ -1 +1 @@ -{"cached_at":"2023-01-29T17:32:05Z","data":{"categories":[{"decimal":"2.28","digital":"2:17:36","hours":2,"minutes":17,"name":"Coding","percent":99.02,"seconds":36,"text":"2 hrs 17 mins","total_seconds":8256.598234},{"decimal":"0.00","digital":"0:00:07","hours":0,"minutes":0,"name":"Debugging","percent":0.08,"seconds":7,"text":"7 secs","total_seconds":7.100772}],"dependencies":[{"decimal":"1.25","digital":"1:15:44","hours":1,"minutes":15,"name":"strings","percent":64.82,"seconds":44,"text":"1 hr 15 mins","total_seconds":4544.055638},{"decimal":"0.82","digital":"0:49:06","hours":0,"minutes":49,"name":"io","percent":35.18,"seconds":6,"text":"49 mins","total_seconds":2946.01205}],"editors":[{"decimal":"2.07","digital":"2:04:07","hours":2,"minutes":4,"name":"VS Code","percent":90.2,"seconds":7,"text":"2 hrs 4 mins","total_seconds":7447.112447},{"decimal":"0.22","digital":"0:13:29","hours":0,"minutes":13,"name":"Zsh-Wakatime-Sobolevn","percent":9.8,"seconds":29,"text":"13 mins","total_seconds":809.485787}],"grand_total":{"decimal":"2.28","digital":"2:17","hours":2,"minutes":17,"text":"2 hrs 17 mins","total_seconds":8256.598234},"languages":[{"decimal":"1.93","digital":"1:56:49","hours":1,"minutes":56,"name":"Go","percent":86.15,"seconds":49,"text":"1 hr 56 mins","total_seconds":7009.317188},{"decimal":"0.27","digital":"0:16:11","hours":0,"minutes":16,"name":"Other","percent":13.85,"seconds":11,"text":"16 mins","total_seconds":971.489169}],"machines":[{"decimal":"2.28","digital":"2:17:36","hours":2,"machine_name_id":"370471e8-b6dd-41aa-a94e-d4fb59a7db85","minutes":17,"name":"WakaMachine","percent":100,"seconds":36,"text":"2 hrs 17 mins","total_seconds":8256.598234}],"operating_systems":[{"decimal":"2.28","digital":"2:17:36","hours":2,"minutes":17,"name":"Mac","percent":100,"seconds":36,"text":"2 hrs 17 mins","total_seconds":8256.598234}],"projects":[{"decimal":"2.05","digital":"2:03:44","hours":2,"minutes":3,"name":"wakatime-cli","percent":97.53,"seconds":44,"text":"2 hrs 3 mins","total_seconds":7424.621273},{"decimal":"0.05","digital":"0:03:02","hours":0,"minutes":3,"name":"Terminal","percent":2.46,"seconds":2,"text":"3 mins","total_seconds":182.934009}],"range":{"date":"2023-01-29","end":"2023-01-30T02:59:59Z","start":"2023-01-29T03:00:00Z","text":"Sun Jan 29th 2023","timezone":"America/Sao_Paulo"}},"has_team_features":true} \ No newline at end of file +{"cached_at":"2023-01-29T17:32:05Z","data":{"categories":[{"decimal":"2.28","digital":"2:17:36","hours":2,"minutes":17,"name":"Coding","percent":99.02,"seconds":36,"text":"2 hrs 17 mins","total_seconds":8256.598234},{"decimal":"0.00","digital":"0:00:07","hours":0,"minutes":0,"name":"Debugging","percent":0.08,"seconds":7,"text":"7 secs","total_seconds":7.100772}],"dependencies":[{"decimal":"1.25","digital":"1:15:44","hours":1,"minutes":15,"name":"strings","percent":64.82,"seconds":44,"text":"1 hr 15 mins","total_seconds":4544.055638},{"decimal":"0.82","digital":"0:49:06","hours":0,"minutes":49,"name":"io","percent":35.18,"seconds":6,"text":"49 mins","total_seconds":2946.01205}],"editors":[{"decimal":"2.07","digital":"2:04:07","hours":2,"minutes":4,"name":"VS Code","percent":90.2,"seconds":7,"text":"2 hrs 4 mins","total_seconds":7447.112447},{"decimal":"0.22","digital":"0:13:29","hours":0,"minutes":13,"name":"Zsh-Wakatime-Sobolevn","percent":9.8,"seconds":29,"text":"13 mins","total_seconds":809.485787}],"grand_total":{"decimal":"2.28","digital":"2:17","hours":2,"minutes":17,"text":"2 hrs 17 mins","total_seconds":8256.598234},"languages":[{"decimal":"1.93","digital":"1:56:49","hours":1,"minutes":56,"name":"Go","percent":86.15,"seconds":49,"text":"1 hr 56 mins","total_seconds":7009.317188},{"decimal":"0.27","digital":"0:16:11","hours":0,"minutes":16,"name":"Other","percent":13.85,"seconds":11,"text":"16 mins","total_seconds":971.489169}],"machines":[{"decimal":"2.28","digital":"2:17:36","hours":2,"machine_name_id":"370471e8-b6dd-41aa-a94e-d4fb59a7db85","minutes":17,"name":"WakaMachine","percent":100,"seconds":36,"text":"2 hrs 17 mins","total_seconds":8256.598234}],"operating_systems":[{"decimal":"2.28","digital":"2:17:36","hours":2,"minutes":17,"name":"Mac","percent":100,"seconds":36,"text":"2 hrs 17 mins","total_seconds":8256.598234}],"projects":[{"decimal":"2.05","digital":"2:03:44","hours":2,"minutes":3,"name":"wakatime-cli","percent":97.53,"seconds":44,"text":"2 hrs 3 mins","total_seconds":7424.621273},{"decimal":"0.05","digital":"0:03:02","hours":0,"minutes":3,"name":"Terminal","percent":2.46,"seconds":2,"text":"3 mins","total_seconds":182.934009}],"range":{"date":"2023-01-29","end":"2023-01-30T02:59:59Z","start":"2023-01-29T03:00:00Z","text":"Sun Jan 29th 2023","timezone":"America/Sao_Paulo"}},"has_team_features":true} From f210c74fc9020cd71e1891146d1644d43d91337c Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Wed, 24 Jul 2024 09:17:06 +0200 Subject: [PATCH 2/4] Rate limiting by default --- .golangci.yml | 1 + USAGE.md | 1 + cmd/configwrite/configwrite_test.go | 6 +- cmd/heartbeat/heartbeat.go | 61 +++++ cmd/heartbeat/heartbeat_test.go | 115 +++++++- cmd/offlinesync/offlinesync.go | 38 ++- cmd/offlinesync/offlinesync_test.go | 282 +++++++++++++++++++ cmd/params/params.go | 33 ++- cmd/params/params_test.go | 402 +++++++++++++++------------- cmd/root.go | 7 + cmd/run.go | 4 +- pkg/offline/offline.go | 3 + 12 files changed, 762 insertions(+), 191 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index a8ae4e1d..341fd9a0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -67,3 +67,4 @@ issues: - gosec include: - EXC0002 + fix: true diff --git a/USAGE.md b/USAGE.md index eb438dd5..30dfe773 100644 --- a/USAGE.md +++ b/USAGE.md @@ -91,6 +91,7 @@ some/submodule/name = new project name | api_key | Your wakatime api key. | _string_ | | | api_key_vault_cmd | A command to get your api key, perhaps from some sort of secure vault. Actually a space-separated list of an executable and its arguments. Executables in PATH can be referred to by their basenames. Shell syntax not supported. | _string_ | | | api_url | The WakaTime API base url. | _string_ | | +| heartbeat_rate_limit_seconds | Rate limit sending heartbeats to the API once per duration. Set to 0 to disable rate limiting. | _int_ | `120` | | hide_file_names | Obfuscate filenames. Will not send file names to api. | _bool_;_list_ | `false` | | hide_project_names | Obfuscate project names. When a project folder is detected instead of using the folder name as the project, a `.wakatime-project file` is created with a random project name. | _bool_;_list_ | `false` | | hide_branch_names | Obfuscate branch names. Will not send revision control branch names to api. | _bool_;_list_ | `false` | diff --git a/cmd/configwrite/configwrite_test.go b/cmd/configwrite/configwrite_test.go index f244b920..d96e0b7a 100644 --- a/cmd/configwrite/configwrite_test.go +++ b/cmd/configwrite/configwrite_test.go @@ -132,7 +132,7 @@ func TestWriteErr(t *testing.T) { func TestWriteSaveErr(t *testing.T) { v := viper.New() - w := &writerMock{ + w := &mockWriter{ WriteFn: func(section string, keyValue map[string]string) error { assert.Equal(t, "settings", section) assert.Equal(t, map[string]string{"debug": "false"}, keyValue) @@ -148,10 +148,10 @@ func TestWriteSaveErr(t *testing.T) { assert.Error(t, err) } -type writerMock struct { +type mockWriter struct { WriteFn func(section string, keyValue map[string]string) error } -func (m *writerMock) Write(section string, keyValue map[string]string) error { +func (m *mockWriter) Write(section string, keyValue map[string]string) error { return m.WriteFn(section, keyValue) } diff --git a/cmd/heartbeat/heartbeat.go b/cmd/heartbeat/heartbeat.go index ef8847b7..8799a8e2 100644 --- a/cmd/heartbeat/heartbeat.go +++ b/cmd/heartbeat/heartbeat.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strings" + "time" apicmd "github.com/wakatime/wakatime-cli/cmd/api" offlinecmd "github.com/wakatime/wakatime-cli/cmd/offline" @@ -16,6 +17,7 @@ import ( "github.com/wakatime/wakatime-cli/pkg/filestats" "github.com/wakatime/wakatime-cli/pkg/filter" "github.com/wakatime/wakatime-cli/pkg/heartbeat" + "github.com/wakatime/wakatime-cli/pkg/ini" "github.com/wakatime/wakatime-cli/pkg/language" _ "github.com/wakatime/wakatime-cli/pkg/lexer" // force to load all lexers "github.com/wakatime/wakatime-cli/pkg/log" @@ -76,6 +78,19 @@ func SendHeartbeats(v *viper.Viper, queueFilepath string) error { setLogFields(params) log.Debugf("params: %s", params) + if RateLimited(RateLimitParams{ + Disabled: params.Offline.Disabled, + LastSentAt: params.Offline.LastSentAt, + Timeout: params.Offline.RateLimit, + }) { + if err = offlinecmd.SaveHeartbeats(v, nil, queueFilepath); err == nil { + return nil + } + + // log offline db error then try to send heartbeats to API so they're not lost + log.Errorf("failed to save rate limited heartbeats: %s", err) + } + heartbeats := buildHeartbeats(params) var chOfflineSave = make(chan bool) @@ -143,6 +158,10 @@ func SendHeartbeats(v *viper.Viper, queueFilepath string) error { } } + if err := ResetRateLimit(v); err != nil { + log.Errorf("failed to reset rate limit: %s", err) + } + return nil } @@ -170,6 +189,48 @@ func LoadParams(v *viper.Viper) (paramscmd.Params, error) { }, nil } +// RateLimitParams contains params for the RateLimited function. +type RateLimitParams struct { + Disabled bool + LastSentAt time.Time + Timeout time.Duration +} + +// RateLimited determines if we should send heartbeats to the API or save to the offline db. +func RateLimited(params RateLimitParams) bool { + if params.Disabled { + return false + } + + if params.Timeout == 0 { + return false + } + + if params.LastSentAt.IsZero() { + return false + } + + return time.Since(params.LastSentAt) < params.Timeout +} + +// ResetRateLimit updates the internal.heartbeats_last_sent_at timestamp. +func ResetRateLimit(v *viper.Viper) error { + w, err := ini.NewWriter(v, ini.InternalFilePath) + if err != nil { + return fmt.Errorf("failed to parse config file: %s", err) + } + + keyValue := map[string]string{ + "heartbeats_last_sent_at": time.Now().Format(ini.DateFormat), + } + + if err := w.Write("internal", keyValue); err != nil { + return fmt.Errorf("failed to write to internal config file: %s", err) + } + + return nil +} + func buildHeartbeats(params paramscmd.Params) []heartbeat.Heartbeat { heartbeats := []heartbeat.Heartbeat{} diff --git a/cmd/heartbeat/heartbeat_test.go b/cmd/heartbeat/heartbeat_test.go index 21261ee6..9e34a203 100644 --- a/cmd/heartbeat/heartbeat_test.go +++ b/cmd/heartbeat/heartbeat_test.go @@ -47,6 +47,8 @@ func TestSendHeartbeats(t *testing.T) { subfolders := project.CountSlashesInProjectFolder(projectFolder) router.HandleFunc("/users/current/heartbeats.bulk", func(w http.ResponseWriter, req *http.Request) { + numCalls++ + // check request assert.Equal(t, http.MethodPost, req.Method) assert.Equal(t, []string{"application/json"}, req.Header["Accept"]) @@ -85,7 +87,47 @@ func TestSendHeartbeats(t *testing.T) { _, err = io.Copy(w, f) require.NoError(t, err) + }) + + v := viper.New() + v.SetDefault("sync-offline-activity", 1000) + v.Set("api-url", testServerURL) + v.Set("category", "debugging") + v.Set("cursorpos", 42) + v.Set("entity", "testdata/main.go") + v.Set("entity-type", "file") + v.Set("key", "00000000-0000-4000-8000-000000000000") + v.Set("language", "Go") + v.Set("alternate-language", "Golang") + v.Set("hide-branch-names", true) + v.Set("project", "wakatime-cli") + v.Set("lineno", 13) + v.Set("local-file", "testdata/localfile.go") + v.Set("plugin", plugin) + v.Set("time", 1585598059.1) + v.Set("timeout", 5) + v.Set("write", true) + + offlineQueueFile, err := os.CreateTemp(t.TempDir(), "") + require.NoError(t, err) + + err = cmdheartbeat.SendHeartbeats(v, offlineQueueFile.Name()) + require.NoError(t, err) + + assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) +} + +func TestSendHeartbeats_RateLimited(t *testing.T) { + testServerURL, router, tearDown := setupTestServer() + defer tearDown() + + var ( + plugin = "plugin/0.0.1" + numCalls int + ) + router.HandleFunc("/users/current/heartbeats.bulk", func(_ http.ResponseWriter, _ *http.Request) { + // Should not be called numCalls++ }) @@ -107,6 +149,8 @@ func TestSendHeartbeats(t *testing.T) { v.Set("time", 1585598059.1) v.Set("timeout", 5) v.Set("write", true) + v.Set("heartbeat-rate-limit-seconds", 500) + v.Set("internal.heartbeats_last_sent_at", time.Now().Add(-time.Minute).Format(time.RFC3339)) offlineQueueFile, err := os.CreateTemp(t.TempDir(), "") require.NoError(t, err) @@ -114,7 +158,7 @@ func TestSendHeartbeats(t *testing.T) { err = cmdheartbeat.SendHeartbeats(v, offlineQueueFile.Name()) require.NoError(t, err) - assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) + assert.Zero(t, numCalls) } func TestSendHeartbeats_WithFiltering_Exclude(t *testing.T) { @@ -1052,6 +1096,75 @@ func TestSendHeartbeats_ObfuscateProjectNotBranch(t *testing.T) { assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) } +func TestRateLimited(t *testing.T) { + p := cmdheartbeat.RateLimitParams{ + Timeout: time.Duration(offline.RateLimitDefaultSeconds) * time.Second, + LastSentAt: time.Now(), + } + + assert.True(t, cmdheartbeat.RateLimited(p)) +} + +func TestRateLimited_NotLimited(t *testing.T) { + p := cmdheartbeat.RateLimitParams{ + LastSentAt: time.Now().Add(time.Duration(-offline.RateLimitDefaultSeconds*2) * time.Second), + Timeout: time.Duration(offline.RateLimitDefaultSeconds) * time.Second, + } + + assert.False(t, cmdheartbeat.RateLimited(p)) +} + +func TestRateLimited_Disabled(t *testing.T) { + p := cmdheartbeat.RateLimitParams{ + Disabled: true, + } + + assert.False(t, cmdheartbeat.RateLimited(p)) +} + +func TestRateLimited_TimeoutZero(t *testing.T) { + p := cmdheartbeat.RateLimitParams{ + LastSentAt: time.Time{}, + } + + assert.False(t, cmdheartbeat.RateLimited(p)) +} + +func TestRateLimited_LastSentAtZero(t *testing.T) { + p := cmdheartbeat.RateLimitParams{ + Timeout: 0, + } + + assert.False(t, cmdheartbeat.RateLimited(p)) +} + +func TestResetRateLimit(t *testing.T) { + tmpFile, err := os.CreateTemp(t.TempDir(), "wakatime") + require.NoError(t, err) + + defer tmpFile.Close() + + v := viper.New() + v.Set("config", tmpFile.Name()) + v.Set("internal-config", tmpFile.Name()) + + writer, err := ini.NewWriter(v, func(vp *viper.Viper) (string, error) { + assert.Equal(t, v, vp) + return tmpFile.Name(), nil + }) + require.NoError(t, err) + + err = cmdheartbeat.ResetRateLimit(v) + require.NoError(t, err) + + err = writer.File.Reload() + require.NoError(t, err) + + lastSentAt := writer.File.Section("internal").Key("heartbeats_last_sent_at").MustTimeFormat(ini.DateFormat) + + assert.WithinDuration(t, time.Now(), lastSentAt, 1*time.Second) +} + func setupTestServer() (string, *http.ServeMux, func()) { router := http.NewServeMux() srv := httptest.NewServer(router) diff --git a/cmd/offlinesync/offlinesync.go b/cmd/offlinesync/offlinesync.go index 48574e8a..878834e5 100644 --- a/cmd/offlinesync/offlinesync.go +++ b/cmd/offlinesync/offlinesync.go @@ -5,6 +5,7 @@ import ( "os" cmdapi "github.com/wakatime/wakatime-cli/cmd/api" + cmdheartbeat "github.com/wakatime/wakatime-cli/cmd/heartbeat" "github.com/wakatime/wakatime-cli/cmd/params" "github.com/wakatime/wakatime-cli/pkg/apikey" "github.com/wakatime/wakatime-cli/pkg/exitcode" @@ -16,8 +17,33 @@ import ( "github.com/spf13/viper" ) -// Run executes the sync-offline-activity command. -func Run(v *viper.Viper) (int, error) { +// RunWithoutRateLimiting executes the sync-offline-activity command without rate limiting. +func RunWithoutRateLimiting(v *viper.Viper) (int, error) { + return run(v) +} + +// RunWithRateLimiting executes sync-offline-activity command with rate limiting enabled. +func RunWithRateLimiting(v *viper.Viper) (int, error) { + paramOffline := params.LoadOfflineParams(v) + + if cmdheartbeat.RateLimited(cmdheartbeat.RateLimitParams{ + Disabled: paramOffline.Disabled, + LastSentAt: paramOffline.LastSentAt, + Timeout: paramOffline.RateLimit, + }) { + log.Debugln("skip syncing offline activity to respect rate limit") + return exitcode.Success, nil + } + + return run(v) +} + +func run(v *viper.Viper) (int, error) { + paramOffline := params.LoadOfflineParams(v) + if paramOffline.Disabled { + return exitcode.Success, nil + } + queueFilepath, err := offline.QueueFilepath() if err != nil { return exitcode.ErrGeneric, fmt.Errorf( @@ -97,8 +123,6 @@ func syncOfflineActivityLegacy(v *viper.Viper, queueFilepath string) error { // SyncOfflineActivity syncs offline activity by sending heartbeats // from the offline queue to the WakaTime API. func SyncOfflineActivity(v *viper.Viper, queueFilepath string) error { - paramOffline := params.LoadOfflineParams(v) - paramAPI, err := params.LoadAPIParams(v) if err != nil { return fmt.Errorf("failed to load API parameters: %w", err) @@ -109,6 +133,8 @@ func SyncOfflineActivity(v *viper.Viper, queueFilepath string) error { return fmt.Errorf("failed to initialize api client: %w", err) } + paramOffline := params.LoadOfflineParams(v) + if paramOffline.QueueFile != "" { queueFilepath = paramOffline.QueueFile } @@ -126,5 +152,9 @@ func SyncOfflineActivity(v *viper.Viper, queueFilepath string) error { return err } + if err := cmdheartbeat.ResetRateLimit(v); err != nil { + log.Errorf("failed to reset rate limit: %s", err) + } + return nil } diff --git a/cmd/offlinesync/offlinesync_test.go b/cmd/offlinesync/offlinesync_test.go index cb7abead..50754c0f 100644 --- a/cmd/offlinesync/offlinesync_test.go +++ b/cmd/offlinesync/offlinesync_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/wakatime/wakatime-cli/cmd/offlinesync" + "github.com/wakatime/wakatime-cli/pkg/exitcode" "github.com/wakatime/wakatime-cli/pkg/heartbeat" "github.com/spf13/viper" @@ -20,6 +21,198 @@ import ( bolt "go.etcd.io/bbolt" ) +func TestRunWithRateLimiting(t *testing.T) { + testServerURL, router, tearDown := setupTestServer() + defer tearDown() + + var ( + plugin = "plugin/0.0.1" + numCalls int + ) + + router.HandleFunc("/users/current/heartbeats.bulk", func(w http.ResponseWriter, req *http.Request) { + numCalls++ + + // check request + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, []string{"application/json"}, req.Header["Accept"]) + assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) + assert.True(t, strings.HasSuffix(req.Header["User-Agent"][0], plugin), fmt.Sprintf( + "%q should have suffix %q", + req.Header["User-Agent"][0], + plugin, + )) + + expectedBody, err := os.ReadFile("testdata/api_heartbeats_request_template.json") + require.NoError(t, err) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + + assert.JSONEq(t, string(expectedBody), string(body)) + + // send response + w.WriteHeader(http.StatusCreated) + + f, err := os.Open("testdata/api_heartbeats_response.json") + require.NoError(t, err) + defer f.Close() + + _, err = io.Copy(w, f) + require.NoError(t, err) + }) + + // setup offline queue + f, err := os.CreateTemp(t.TempDir(), "") + require.NoError(t, err) + + db, err := bolt.Open(f.Name(), 0600, nil) + require.NoError(t, err) + + dataGo, err := os.ReadFile("testdata/heartbeat_go.json") + require.NoError(t, err) + + dataPy, err := os.ReadFile("testdata/heartbeat_py.json") + require.NoError(t, err) + + dataJs, err := os.ReadFile("testdata/heartbeat_js.json") + require.NoError(t, err) + + insertHeartbeatRecords(t, db, "heartbeats", []heartbeatRecord{ + { + ID: "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", + Heartbeat: string(dataGo), + }, + { + ID: "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", + Heartbeat: string(dataPy), + }, + { + ID: "1592868394.084354-file-building-wakatime-todaygoal-/tmp/main.js-false", + Heartbeat: string(dataJs), + }, + }) + + err = db.Close() + require.NoError(t, err) + + v := viper.New() + v.Set("api-url", testServerURL) + v.Set("key", "00000000-0000-4000-8000-000000000000") + v.Set("offline-queue-file", f.Name()) + v.Set("sync-offline-activity", 100) + v.Set("plugin", plugin) + + code, err := offlinesync.RunWithRateLimiting(v) + require.NoError(t, err) + + assert.Equal(t, exitcode.Success, code) + assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) +} + +func TestRunWithoutRateLimiting(t *testing.T) { + testServerURL, router, tearDown := setupTestServer() + defer tearDown() + + var ( + plugin = "plugin/0.0.1" + numCalls int + ) + + router.HandleFunc("/users/current/heartbeats.bulk", func(w http.ResponseWriter, req *http.Request) { + numCalls++ + + // check request + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, []string{"application/json"}, req.Header["Accept"]) + assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) + assert.True(t, strings.HasSuffix(req.Header["User-Agent"][0], plugin), fmt.Sprintf( + "%q should have suffix %q", + req.Header["User-Agent"][0], + plugin, + )) + + expectedBody, err := os.ReadFile("testdata/api_heartbeats_request_template.json") + require.NoError(t, err) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + + assert.JSONEq(t, string(expectedBody), string(body)) + + // send response + w.WriteHeader(http.StatusCreated) + + f, err := os.Open("testdata/api_heartbeats_response.json") + require.NoError(t, err) + defer f.Close() + + _, err = io.Copy(w, f) + require.NoError(t, err) + }) + + // setup offline queue + f, err := os.CreateTemp(t.TempDir(), "") + require.NoError(t, err) + + db, err := bolt.Open(f.Name(), 0600, nil) + require.NoError(t, err) + + dataGo, err := os.ReadFile("testdata/heartbeat_go.json") + require.NoError(t, err) + + dataPy, err := os.ReadFile("testdata/heartbeat_py.json") + require.NoError(t, err) + + dataJs, err := os.ReadFile("testdata/heartbeat_js.json") + require.NoError(t, err) + + insertHeartbeatRecords(t, db, "heartbeats", []heartbeatRecord{ + { + ID: "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", + Heartbeat: string(dataGo), + }, + { + ID: "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", + Heartbeat: string(dataPy), + }, + { + ID: "1592868394.084354-file-building-wakatime-todaygoal-/tmp/main.js-false", + Heartbeat: string(dataJs), + }, + }) + + err = db.Close() + require.NoError(t, err) + + v := viper.New() + v.Set("api-url", testServerURL) + v.Set("key", "00000000-0000-4000-8000-000000000000") + v.Set("offline-queue-file", f.Name()) + v.Set("sync-offline-activity", 100) + v.Set("plugin", plugin) + + code, err := offlinesync.RunWithoutRateLimiting(v) + require.NoError(t, err) + + assert.Equal(t, exitcode.Success, code) + assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) +} + +func TestRunWithRateLimiting_RateLimited(t *testing.T) { + v := viper.New() + v.Set("key", "00000000-0000-4000-8000-000000000000") + v.Set("heartbeat-rate-limit-seconds", 500) + v.Set("internal.heartbeats_last_sent_at", time.Now().Add(-time.Minute).Format(time.RFC3339)) + + code, err := offlinesync.RunWithRateLimiting(v) + require.NoError(t, err) + + assert.Equal(t, exitcode.Success, code) +} + func TestSyncOfflineActivity(t *testing.T) { testServerURL, router, tearDown := setupTestServer() defer tearDown() @@ -108,6 +301,95 @@ func TestSyncOfflineActivity(t *testing.T) { assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) } +func TestSyncOfflineActivity_QueueFileFromConfig(t *testing.T) { + testServerURL, router, tearDown := setupTestServer() + defer tearDown() + + var ( + plugin = "plugin/0.0.1" + numCalls int + ) + + router.HandleFunc("/users/current/heartbeats.bulk", func(w http.ResponseWriter, req *http.Request) { + numCalls++ + + // check request + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, []string{"application/json"}, req.Header["Accept"]) + assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) + assert.True(t, strings.HasSuffix(req.Header["User-Agent"][0], plugin), fmt.Sprintf( + "%q should have suffix %q", + req.Header["User-Agent"][0], + plugin, + )) + + expectedBody, err := os.ReadFile("testdata/api_heartbeats_request_template.json") + require.NoError(t, err) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + + assert.JSONEq(t, string(expectedBody), string(body)) + + // send response + w.WriteHeader(http.StatusCreated) + + f, err := os.Open("testdata/api_heartbeats_response.json") + require.NoError(t, err) + defer f.Close() + + _, err = io.Copy(w, f) + require.NoError(t, err) + }) + + // setup offline queue + f, err := os.CreateTemp(t.TempDir(), "") + require.NoError(t, err) + + db, err := bolt.Open(f.Name(), 0600, nil) + require.NoError(t, err) + + dataGo, err := os.ReadFile("testdata/heartbeat_go.json") + require.NoError(t, err) + + dataPy, err := os.ReadFile("testdata/heartbeat_py.json") + require.NoError(t, err) + + dataJs, err := os.ReadFile("testdata/heartbeat_js.json") + require.NoError(t, err) + + insertHeartbeatRecords(t, db, "heartbeats", []heartbeatRecord{ + { + ID: "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", + Heartbeat: string(dataGo), + }, + { + ID: "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", + Heartbeat: string(dataPy), + }, + { + ID: "1592868394.084354-file-building-wakatime-todaygoal-/tmp/main.js-false", + Heartbeat: string(dataJs), + }, + }) + + err = db.Close() + require.NoError(t, err) + + v := viper.New() + v.Set("api-url", testServerURL) + v.Set("key", "00000000-0000-4000-8000-000000000000") + v.Set("offline-queue-file", f.Name()) + v.Set("sync-offline-activity", 100) + v.Set("plugin", plugin) + + err = offlinesync.SyncOfflineActivity(v, "/another/file") + require.NoError(t, err) + + assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) +} + func TestSyncOfflineActivity_MultipleApiKey(t *testing.T) { testServerURL, router, tearDown := setupTestServer() defer tearDown() diff --git a/cmd/params/params.go b/cmd/params/params.go index eea875f8..f43a3c30 100644 --- a/cmd/params/params.go +++ b/cmd/params/params.go @@ -131,9 +131,11 @@ type ( // Offline contains offline related parameters. Offline struct { Disabled bool + LastSentAt time.Time PrintMax int QueueFile string QueueFileLegacy string + RateLimit time.Duration SyncMax int } @@ -647,6 +649,13 @@ func LoadOfflineParams(v *viper.Viper) Offline { disabled = !b } + rateLimit, _ := vipertools.FirstNonEmptyInt(v, "heartbeat-rate-limit-seconds", "settings.heartbeat_rate_limit_seconds") + if rateLimit < 0 { + log.Warnf("argument --heartbeat-rate-limit-seconds must be zero or a positive integer number, got %d", rateLimit) + + rateLimit = 0 + } + syncMax := v.GetInt("sync-offline-activity") if syncMax < 0 { log.Warnf("argument --sync-offline-activity must be zero or a positive integer number, got %d", syncMax) @@ -654,11 +663,25 @@ func LoadOfflineParams(v *viper.Viper) Offline { syncMax = 0 } + var lastSentAt time.Time + + lastSentAtStr := vipertools.GetString(v, "internal.heartbeats_last_sent_at") + if lastSentAtStr != "" { + parsed, err := time.Parse(ini.DateFormat, lastSentAtStr) + if err != nil { + log.Warnf("failed to parse heartbeats_last_sent_at: %s", err) + } else { + lastSentAt = parsed + } + } + return Offline{ Disabled: disabled, + LastSentAt: lastSentAt, PrintMax: v.GetInt("print-offline-heartbeats"), QueueFile: vipertools.GetString(v, "offline-queue-file"), QueueFileLegacy: vipertools.GetString(v, "offline-queue-file-legacy"), + RateLimit: time.Duration(rateLimit) * time.Second, SyncMax: syncMax, } } @@ -1038,12 +1061,20 @@ func (p Heartbeat) String() string { // String implements fmt.Stringer interface. func (p Offline) String() string { + var lastSentAt string + if !p.LastSentAt.IsZero() { + lastSentAt = p.LastSentAt.Format(ini.DateFormat) + } + return fmt.Sprintf( - "disabled: %t, print max: %d, queue file: '%s', queue file legacy: '%s', num sync max: %d", + "disabled: %t, last sent at: '%s', print max: %d, queue file: '%s', queue file legacy: '%s',"+ + " num rate limit: %d, num sync max: %d", p.Disabled, + lastSentAt, p.PrintMax, p.QueueFile, p.QueueFileLegacy, + p.RateLimit, p.SyncMax, ) } diff --git a/cmd/params/params_test.go b/cmd/params/params_test.go index a7286bc4..cda37cee 100644 --- a/cmd/params/params_test.go +++ b/cmd/params/params_test.go @@ -28,7 +28,7 @@ import ( "gopkg.in/ini.v1" ) -func TestLoadParams_AlternateProject(t *testing.T) { +func TestLoadHeartbeatParams_AlternateProject(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("alternate-project", "web") @@ -39,7 +39,7 @@ func TestLoadParams_AlternateProject(t *testing.T) { assert.Equal(t, "web", params.Project.Alternate) } -func TestLoadParams_AlternateProject_Unset(t *testing.T) { +func TestLoadHeartbeatParams_AlternateProject_Unset(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") @@ -49,7 +49,7 @@ func TestLoadParams_AlternateProject_Unset(t *testing.T) { assert.Empty(t, params.Project.Alternate) } -func TestLoadParams_Category(t *testing.T) { +func TestLoadHeartbeatParams_Category(t *testing.T) { tests := map[string]heartbeat.Category{ "advising": heartbeat.AdvisingCategory, "browsing": heartbeat.BrowsingCategory, @@ -85,7 +85,7 @@ func TestLoadParams_Category(t *testing.T) { } } -func TestLoadParams_Category_Default(t *testing.T) { +func TestLoadHeartbeatParams_Category_Default(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") @@ -95,7 +95,7 @@ func TestLoadParams_Category_Default(t *testing.T) { assert.Equal(t, heartbeat.CodingCategory, params.Category) } -func TestLoadParams_Category_Invalid(t *testing.T) { +func TestLoadHeartbeatParams_Category_Invalid(t *testing.T) { v := viper.New() v.SetDefault("sync-offline-activity", 1000) v.Set("category", "invalid") @@ -106,7 +106,7 @@ func TestLoadParams_Category_Invalid(t *testing.T) { assert.Equal(t, "failed to parse category: invalid category \"invalid\"", err.Error()) } -func TestLoadParams_CursorPosition(t *testing.T) { +func TestLoadHeartbeatParams_CursorPosition(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("cursorpos", 42) @@ -117,7 +117,7 @@ func TestLoadParams_CursorPosition(t *testing.T) { assert.Equal(t, 42, *params.CursorPosition) } -func TestLoadParams_CursorPosition_Zero(t *testing.T) { +func TestLoadHeartbeatParams_CursorPosition_Zero(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("cursorpos", 0) @@ -128,7 +128,7 @@ func TestLoadParams_CursorPosition_Zero(t *testing.T) { assert.Zero(t, *params.CursorPosition) } -func TestLoadParams_CursorPosition_Unset(t *testing.T) { +func TestLoadHeartbeatParams_CursorPosition_Unset(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -139,7 +139,7 @@ func TestLoadParams_CursorPosition_Unset(t *testing.T) { assert.Nil(t, params.CursorPosition) } -func TestLoadParams_Entity_EntityFlagTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_Entity_EntityFlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("file", "ignored") @@ -150,7 +150,7 @@ func TestLoadParams_Entity_EntityFlagTakesPrecedence(t *testing.T) { assert.Equal(t, "/path/to/file", params.Entity) } -func TestLoadParams_Entity_FileFlag(t *testing.T) { +func TestLoadHeartbeatParams_Entity_FileFlag(t *testing.T) { v := viper.New() v.Set("file", "~/path/to/file") @@ -163,7 +163,7 @@ func TestLoadParams_Entity_FileFlag(t *testing.T) { assert.Equal(t, filepath.Join(home, "/path/to/file"), params.Entity) } -func TestLoadParams_Entity_Unset(t *testing.T) { +func TestLoadHeartbeatParams_Entity_Unset(t *testing.T) { v := viper.New() _, err := paramscmd.LoadHeartbeatParams(v) @@ -172,7 +172,7 @@ func TestLoadParams_Entity_Unset(t *testing.T) { assert.Equal(t, "failed to retrieve entity", err.Error()) } -func TestLoadParams_EntityType(t *testing.T) { +func TestLoadHeartbeatParams_EntityType(t *testing.T) { tests := map[string]heartbeat.EntityType{ "file": heartbeat.FileType, "domain": heartbeat.DomainType, @@ -193,7 +193,7 @@ func TestLoadParams_EntityType(t *testing.T) { } } -func TestLoadParams_EntityType_Default(t *testing.T) { +func TestLoadHeartbeatParams_EntityType_Default(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") @@ -203,7 +203,7 @@ func TestLoadParams_EntityType_Default(t *testing.T) { assert.Equal(t, heartbeat.FileType, params.EntityType) } -func TestLoadParams_EntityType_Invalid(t *testing.T) { +func TestLoadHeartbeatParams_EntityType_Invalid(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("entity-type", "invalid") @@ -217,7 +217,7 @@ func TestLoadParams_EntityType_Invalid(t *testing.T) { err.Error()) } -func TestLoadParams_ExtraHeartbeats(t *testing.T) { +func TestLoadHeartbeatParams_ExtraHeartbeats(t *testing.T) { r, w, err := os.Pipe() require.NoError(t, err) @@ -289,7 +289,7 @@ func TestLoadParams_ExtraHeartbeats(t *testing.T) { }, params.ExtraHeartbeats) } -func TestLoadParams_ExtraHeartbeats_WithStringValues(t *testing.T) { +func TestLoadHeartbeatParams_ExtraHeartbeats_WithStringValues(t *testing.T) { r, w, err := os.Pipe() require.NoError(t, err) @@ -356,7 +356,7 @@ func TestLoadParams_ExtraHeartbeats_WithStringValues(t *testing.T) { }, params.ExtraHeartbeats) } -func TestLoadParams_ExtraHeartbeats_WithEOF(t *testing.T) { +func TestLoadHeartbeatParams_ExtraHeartbeats_WithEOF(t *testing.T) { r, w, err := os.Pipe() require.NoError(t, err) @@ -429,7 +429,7 @@ func TestLoadParams_ExtraHeartbeats_WithEOF(t *testing.T) { }, params.ExtraHeartbeats) } -func TestLoadParams_ExtraHeartbeats_NoData(t *testing.T) { +func TestLoadHeartbeatParams_ExtraHeartbeats_NoData(t *testing.T) { r, w, err := os.Pipe() require.NoError(t, err) @@ -502,7 +502,7 @@ func TestLoadHeartbeat_GuessLanguage_Default(t *testing.T) { assert.False(t, params.GuessLanguage) } -func TestLoadParams_IsUnsavedEntity(t *testing.T) { +func TestLoadHeartbeatParams_IsUnsavedEntity(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("is-unsaved-entity", true) @@ -513,7 +513,7 @@ func TestLoadParams_IsUnsavedEntity(t *testing.T) { assert.True(t, params.IsUnsavedEntity) } -func TestLoadParams_IsWrite(t *testing.T) { +func TestLoadHeartbeatParams_IsWrite(t *testing.T) { tests := map[string]bool{ "is write": true, "is no write": false, @@ -533,7 +533,7 @@ func TestLoadParams_IsWrite(t *testing.T) { } } -func TestLoadParams_IsWrite_Unset(t *testing.T) { +func TestLoadHeartbeatParams_IsWrite_Unset(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") @@ -543,7 +543,7 @@ func TestLoadParams_IsWrite_Unset(t *testing.T) { assert.Nil(t, params.IsWrite) } -func TestLoadParams_Language(t *testing.T) { +func TestLoadHeartbeatParams_Language(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("language", "Go") @@ -554,7 +554,7 @@ func TestLoadParams_Language(t *testing.T) { assert.Equal(t, heartbeat.LanguageGo.String(), *params.Language) } -func TestLoadParams_LanguageAlternate(t *testing.T) { +func TestLoadHeartbeatParams_LanguageAlternate(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("alternate-language", "Go") @@ -566,7 +566,7 @@ func TestLoadParams_LanguageAlternate(t *testing.T) { assert.Nil(t, params.Language) } -func TestLoadParams_LineNumber(t *testing.T) { +func TestLoadHeartbeatParams_LineNumber(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("lineno", 42) @@ -577,7 +577,7 @@ func TestLoadParams_LineNumber(t *testing.T) { assert.Equal(t, 42, *params.LineNumber) } -func TestLoadParams_LineNumber_Zero(t *testing.T) { +func TestLoadHeartbeatParams_LineNumber_Zero(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("lineno", 0) @@ -588,7 +588,7 @@ func TestLoadParams_LineNumber_Zero(t *testing.T) { assert.Zero(t, *params.LineNumber) } -func TestLoadParams_LineNumber_Unset(t *testing.T) { +func TestLoadHeartbeatParams_LineNumber_Unset(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") @@ -598,7 +598,7 @@ func TestLoadParams_LineNumber_Unset(t *testing.T) { assert.Nil(t, params.LineNumber) } -func TestLoadParams_LocalFile(t *testing.T) { +func TestLoadHeartbeatParams_LocalFile(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("local-file", "/path/to/file") @@ -609,7 +609,7 @@ func TestLoadParams_LocalFile(t *testing.T) { assert.Equal(t, "/path/to/file", params.LocalFile) } -func TestLoadParams_Project(t *testing.T) { +func TestLoadHeartbeatParams_Project(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("project", "billing") @@ -620,7 +620,7 @@ func TestLoadParams_Project(t *testing.T) { assert.Equal(t, "billing", params.Project.Override) } -func TestLoadParams_Project_Unset(t *testing.T) { +func TestLoadHeartbeatParams_Project_Unset(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") @@ -630,7 +630,7 @@ func TestLoadParams_Project_Unset(t *testing.T) { assert.Empty(t, params.Project.Override) } -func TestLoadParams_ProjectMap(t *testing.T) { +func TestLoadHeartbeatParams_ProjectMap(t *testing.T) { tests := map[string]struct { Entity string Regex regex.Regex @@ -675,7 +675,7 @@ func TestLoadParams_ProjectMap(t *testing.T) { } } -func TestLoadParams_ProjectApiKey(t *testing.T) { +func TestLoadAPIParams_ProjectApiKey(t *testing.T) { tests := map[string]struct { Entity string Regex regex.Regex @@ -733,7 +733,7 @@ func TestLoadParams_ProjectApiKey(t *testing.T) { } } -func TestLoadParams_ProjectApiKey_ParseConfig(t *testing.T) { +func TestLoadAPIParams_ProjectApiKey_ParseConfig(t *testing.T) { v := viper.New() v.Set("config", "testdata/.wakatime.cfg") v.Set("entity", "testdata/heartbeat_go.json") @@ -757,7 +757,15 @@ func TestLoadParams_ProjectApiKey_ParseConfig(t *testing.T) { assert.Equal(t, expected, params.KeyPatterns) } -func TestLoadParams_Time(t *testing.T) { +func TestLoadAPIParams_APIKeyPrefixSupported(t *testing.T) { + v := viper.New() + v.Set("key", "waka_00000000-0000-4000-8000-000000000000") + + _, err := paramscmd.LoadAPIParams(v) + require.NoError(t, err) +} + +func TestLoadHeartbeatParams_Time(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("time", 1590609206.1) @@ -768,7 +776,7 @@ func TestLoadParams_Time(t *testing.T) { assert.Equal(t, 1590609206.1, params.Time) } -func TestLoadParams_Time_Default(t *testing.T) { +func TestLoadHeartbeatParams_Time_Default(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") @@ -780,7 +788,7 @@ func TestLoadParams_Time_Default(t *testing.T) { assert.GreaterOrEqual(t, params.Time, now-60) } -func TestLoadParams_Filter_Exclude(t *testing.T) { +func TestLoadHeartbeatParams_Filter_Exclude(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("exclude", []string{".*", "wakatime.*"}) @@ -799,7 +807,7 @@ func TestLoadParams_Filter_Exclude(t *testing.T) { assert.Equal(t, "(?i)wakatime.?", params.Filter.Exclude[5].String()) } -func TestLoadParams_Filter_Exclude_Multiline(t *testing.T) { +func TestLoadHeartbeatParams_Filter_Exclude_Multiline(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.ignore", "\t.?\n\twakatime.? \t\n") @@ -812,7 +820,7 @@ func TestLoadParams_Filter_Exclude_Multiline(t *testing.T) { assert.Equal(t, "(?i)wakatime.?", params.Filter.Exclude[1].String()) } -func TestLoadParams_Filter_Exclude_IgnoresInvalidRegex(t *testing.T) { +func TestLoadHeartbeatParams_Filter_Exclude_IgnoresInvalidRegex(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("exclude", []string{".*", "["}) @@ -824,7 +832,7 @@ func TestLoadParams_Filter_Exclude_IgnoresInvalidRegex(t *testing.T) { assert.Equal(t, "(?i).*", params.Filter.Exclude[0].String()) } -func TestLoadParams_Filter_Exclude_PerlRegexPatterns(t *testing.T) { +func TestLoadHeartbeatParams_Filter_Exclude_PerlRegexPatterns(t *testing.T) { tests := map[string]string{ "negative lookahead": `^/var/(?!www/).*`, "positive lookahead": `^/var/(?=www/).*`, @@ -845,7 +853,7 @@ func TestLoadParams_Filter_Exclude_PerlRegexPatterns(t *testing.T) { } } -func TestLoadParams_Filter_ExcludeUnknownProject(t *testing.T) { +func TestLoadHeartbeatParams_Filter_ExcludeUnknownProject(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("exclude-unknown-project", true) @@ -856,7 +864,7 @@ func TestLoadParams_Filter_ExcludeUnknownProject(t *testing.T) { assert.True(t, params.Filter.ExcludeUnknownProject) } -func TestLoadParams_Filter_ExcludeUnknownProject_FromConfig(t *testing.T) { +func TestLoadHeartbeatParams_Filter_ExcludeUnknownProject_FromConfig(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("exclude-unknown-project", false) @@ -868,7 +876,7 @@ func TestLoadParams_Filter_ExcludeUnknownProject_FromConfig(t *testing.T) { assert.True(t, params.Filter.ExcludeUnknownProject) } -func TestLoadParams_Filter_Include(t *testing.T) { +func TestLoadHeartbeatParams_Filter_Include(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("include", []string{".*", "wakatime.*"}) @@ -884,7 +892,7 @@ func TestLoadParams_Filter_Include(t *testing.T) { assert.Equal(t, "(?i)wakatime.+", params.Filter.Include[3].String()) } -func TestLoadParams_Filter_Include_IgnoresInvalidRegex(t *testing.T) { +func TestLoadHeartbeatParams_Filter_Include_IgnoresInvalidRegex(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("include", []string{".*", "["}) @@ -896,7 +904,7 @@ func TestLoadParams_Filter_Include_IgnoresInvalidRegex(t *testing.T) { assert.Equal(t, "(?i).*", params.Filter.Include[0].String()) } -func TestLoadParams_Filter_Include_PerlRegexPatterns(t *testing.T) { +func TestLoadHeartbeatParams_Filter_Include_PerlRegexPatterns(t *testing.T) { tests := map[string]string{ "negative lookahead": `^/var/(?!www/).*`, "positive lookahead": `^/var/(?=www/).*`, @@ -917,7 +925,7 @@ func TestLoadParams_Filter_Include_PerlRegexPatterns(t *testing.T) { } } -func TestLoadParams_Filter_IncludeOnlyWithProjectFile(t *testing.T) { +func TestLoadHeartbeatParams_Filter_IncludeOnlyWithProjectFile(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("include-only-with-project-file", true) @@ -928,7 +936,7 @@ func TestLoadParams_Filter_IncludeOnlyWithProjectFile(t *testing.T) { assert.True(t, params.Filter.IncludeOnlyWithProjectFile) } -func TestLoadParams_Filter_IncludeOnlyWithProjectFile_FromConfig(t *testing.T) { +func TestLoadHeartbeatParams_Filter_IncludeOnlyWithProjectFile_FromConfig(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("include-only-with-project-file", false) @@ -940,7 +948,7 @@ func TestLoadParams_Filter_IncludeOnlyWithProjectFile_FromConfig(t *testing.T) { assert.True(t, params.Filter.IncludeOnlyWithProjectFile) } -func TestLoadParams_SanitizeParams_HideBranchNames_True(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_True(t *testing.T) { tests := map[string]string{ "lowercase": "true", "uppercase": "TRUE", @@ -963,7 +971,7 @@ func TestLoadParams_SanitizeParams_HideBranchNames_True(t *testing.T) { } } -func TestLoadParams_SanitizeParams_HideBranchNames_False(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_False(t *testing.T) { tests := map[string]string{ "lowercase": "false", "uppercase": "FALSE", @@ -986,7 +994,7 @@ func TestLoadParams_SanitizeParams_HideBranchNames_False(t *testing.T) { } } -func TestLoadParams_SanitizeParams_HideBranchNames_List(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_List(t *testing.T) { tests := map[string]struct { ViperValue string Expected []regex.Regex @@ -1022,7 +1030,7 @@ func TestLoadParams_SanitizeParams_HideBranchNames_List(t *testing.T) { } } -func TestLoadParams_SanitizeParams_HideBranchNames_FlagTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("hide-branch-names", true) @@ -1038,7 +1046,7 @@ func TestLoadParams_SanitizeParams_HideBranchNames_FlagTakesPrecedence(t *testin }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideBranchNames_ConfigTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_ConfigTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hide_branch_names", "true") @@ -1053,7 +1061,7 @@ func TestLoadParams_SanitizeParams_HideBranchNames_ConfigTakesPrecedence(t *test }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideBranchNames_ConfigDeprecatedOneTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_ConfigDeprecatedOneTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hide_branchnames", "true") @@ -1067,7 +1075,7 @@ func TestLoadParams_SanitizeParams_HideBranchNames_ConfigDeprecatedOneTakesPrece }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideBranchNames_ConfigDeprecatedTwo(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_ConfigDeprecatedTwo(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hidebranchnames", "true") @@ -1080,7 +1088,7 @@ func TestLoadParams_SanitizeParams_HideBranchNames_ConfigDeprecatedTwo(t *testin }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideBranchNames_InvalidRegex(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_InvalidRegex(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("hide-branch-names", ".*secret.*\n[0-9+") @@ -1096,7 +1104,7 @@ func TestLoadParams_SanitizeParams_HideBranchNames_InvalidRegex(t *testing.T) { )) } -func TestLoadParams_SanitizeParams_HideProjectNames_True(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_True(t *testing.T) { tests := map[string]string{ "lowercase": "true", "uppercase": "TRUE", @@ -1119,7 +1127,7 @@ func TestLoadParams_SanitizeParams_HideProjectNames_True(t *testing.T) { } } -func TestLoadParams_SanitizeParams_HideProjectNames_False(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_False(t *testing.T) { tests := map[string]string{ "lowercase": "false", "uppercase": "FALSE", @@ -1142,7 +1150,7 @@ func TestLoadParams_SanitizeParams_HideProjectNames_False(t *testing.T) { } } -func TestLoadParams_SanitizeParams_HideProjecthNames_List(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjecthNames_List(t *testing.T) { tests := map[string]struct { ViperValue string Expected []regex.Regex @@ -1178,7 +1186,7 @@ func TestLoadParams_SanitizeParams_HideProjecthNames_List(t *testing.T) { } } -func TestLoadParams_SanitizeParams_HideProjectNames_FlagTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("hide-project-names", "true") @@ -1194,7 +1202,7 @@ func TestLoadParams_SanitizeParams_HideProjectNames_FlagTakesPrecedence(t *testi }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideProjectNames_ConfigTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_ConfigTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hide_project_names", "true") @@ -1209,7 +1217,7 @@ func TestLoadParams_SanitizeParams_HideProjectNames_ConfigTakesPrecedence(t *tes }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideProjectNames_ConfigDeprecatedOneTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_ConfigDeprecatedOneTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hide_projectnames", "true") @@ -1223,7 +1231,7 @@ func TestLoadParams_SanitizeParams_HideProjectNames_ConfigDeprecatedOneTakesPrec }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideProjectNames_ConfigDeprecatedTwo(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_ConfigDeprecatedTwo(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hideprojectnames", "true") @@ -1236,7 +1244,7 @@ func TestLoadParams_SanitizeParams_HideProjectNames_ConfigDeprecatedTwo(t *testi }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideProjectNames_InvalidRegex(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_InvalidRegex(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("hide-project-names", ".*secret.*\n[0-9+") @@ -1252,7 +1260,7 @@ func TestLoadParams_SanitizeParams_HideProjectNames_InvalidRegex(t *testing.T) { )) } -func TestLoadParams_SanitizeParams_HideFileNames_True(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_True(t *testing.T) { tests := map[string]string{ "lowercase": "true", "uppercase": "TRUE", @@ -1275,7 +1283,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_True(t *testing.T) { } } -func TestLoadParams_SanitizeParams_HideFileNames_False(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_False(t *testing.T) { tests := map[string]string{ "lowercase": "false", "uppercase": "FALSE", @@ -1298,7 +1306,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_False(t *testing.T) { } } -func TestLoadParams_SanitizeParams_HideFileNames_List(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_List(t *testing.T) { tests := map[string]struct { ViperValue string Expected []regex.Regex @@ -1334,7 +1342,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_List(t *testing.T) { } } -func TestLoadParams_SanitizeParams_HideFileNames_FlagTakesPrecedence(t *testing.T) { +func TestLoadheartbeatParams_SanitizeParams_HideFileNames_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("hide-file-names", "true") @@ -1352,7 +1360,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_FlagTakesPrecedence(t *testing. }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideFileNames_FlagDeprecatedOneTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_FlagDeprecatedOneTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("hide-filenames", "true") @@ -1369,7 +1377,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_FlagDeprecatedOneTakesPrecedenc }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideFileNames_FlagDeprecatedTwoTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_FlagDeprecatedTwoTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("hidefilenames", "true") @@ -1385,7 +1393,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_FlagDeprecatedTwoTakesPrecedenc }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideFileNames_ConfigTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_ConfigTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hide_file_names", "true") @@ -1400,7 +1408,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_ConfigTakesPrecedence(t *testin }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideFileNames_ConfigDeprecatedOneTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_ConfigDeprecatedOneTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hide_filenames", "true") @@ -1414,7 +1422,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_ConfigDeprecatedOneTakesPrecede }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideFileNames_ConfigDeprecatedTwo(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_ConfigDeprecatedTwo(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hidefilenames", "true") @@ -1427,7 +1435,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_ConfigDeprecatedTwo(t *testing. }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideFileNames_InvalidRegex(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_InvalidRegex(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("hide-file-names", ".*secret.*\n[0-9+") @@ -1443,7 +1451,7 @@ func TestLoadParams_SanitizeParams_HideFileNames_InvalidRegex(t *testing.T) { )) } -func TestLoadParams_SanitizeParams_HideProjectFolder(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjectFolder(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("hide-project-folder", true) @@ -1456,7 +1464,7 @@ func TestLoadParams_SanitizeParams_HideProjectFolder(t *testing.T) { }, params.Sanitize) } -func TestLoadParams_SanitizeParams_HideProjectFolder_ConfigTakesPrecedence(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_HideProjectFolder_ConfigTakesPrecedence(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("settings.hide_project_folder", true) @@ -1469,7 +1477,7 @@ func TestLoadParams_SanitizeParams_HideProjectFolder_ConfigTakesPrecedence(t *te }, params.Sanitize) } -func TestLoadParams_SanitizeParams_OverrideProjectPath(t *testing.T) { +func TestLoadHeartbeatParams_SanitizeParams_OverrideProjectPath(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") v.Set("project-folder", "/custom-path") @@ -1482,7 +1490,7 @@ func TestLoadParams_SanitizeParams_OverrideProjectPath(t *testing.T) { }, params.Sanitize) } -func TestLoadParams_SubmodulesDisabled_True(t *testing.T) { +func TestLoadHeartbeatParams_SubmodulesDisabled_True(t *testing.T) { tests := map[string]string{ "lowercase": "true", "uppercase": "TRUE", @@ -1503,7 +1511,7 @@ func TestLoadParams_SubmodulesDisabled_True(t *testing.T) { } } -func TestLoadParams_SubmodulesDisabled_False(t *testing.T) { +func TestLoadHeartbeatParams_SubmodulesDisabled_False(t *testing.T) { tests := map[string]string{ "lowercase": "false", "uppercase": "FALSE", @@ -1524,7 +1532,7 @@ func TestLoadParams_SubmodulesDisabled_False(t *testing.T) { } } -func TestLoadParams_SubmodulesDisabled_List(t *testing.T) { +func TestLoadHeartbeatsParams_SubmodulesDisabled_List(t *testing.T) { tests := map[string]struct { ViperValue string Expected []regex.Regex @@ -1559,7 +1567,7 @@ func TestLoadParams_SubmodulesDisabled_List(t *testing.T) { } } -func TestLoadParams_SubmoduleProjectMap(t *testing.T) { +func TestLoadHeartbeatsParams_SubmoduleProjectMap(t *testing.T) { tests := map[string]struct { Entity string Regex regex.Regex @@ -1604,7 +1612,7 @@ func TestLoadParams_SubmoduleProjectMap(t *testing.T) { } } -func TestLoadParams_Plugin(t *testing.T) { +func TestLoadAPIParams_Plugin(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("plugin", "plugin/10.0.0") @@ -1615,7 +1623,7 @@ func TestLoadParams_Plugin(t *testing.T) { assert.Equal(t, "plugin/10.0.0", params.Plugin) } -func TestLoadParams_Plugin_Unset(t *testing.T) { +func TestLoadAPIParams_Plugin_Unset(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -1625,7 +1633,7 @@ func TestLoadParams_Plugin_Unset(t *testing.T) { assert.Empty(t, params.Plugin) } -func TestLoadParams_Timeout_FlagTakesPrecedence(t *testing.T) { +func TestLoadAPIParams_Timeout_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("timeout", 5) @@ -1637,7 +1645,7 @@ func TestLoadParams_Timeout_FlagTakesPrecedence(t *testing.T) { assert.Equal(t, 5*time.Second, params.Timeout) } -func TestLoadParams_Timeout_FromConfig(t *testing.T) { +func TestLoadAPIParams_Timeout_FromConfig(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.timeout", 10) @@ -1648,7 +1656,7 @@ func TestLoadParams_Timeout_FromConfig(t *testing.T) { assert.Equal(t, 10*time.Second, params.Timeout) } -func TestLoad_OfflineDisabled_ConfigTakesPrecedence(t *testing.T) { +func TestLoadOfflineParams_Disabled_ConfigTakesPrecedence(t *testing.T) { v := viper.New() v.Set("disable-offline", false) v.Set("disableoffline", false) @@ -1659,7 +1667,7 @@ func TestLoad_OfflineDisabled_ConfigTakesPrecedence(t *testing.T) { assert.True(t, params.Disabled) } -func TestLoad_OfflineDisabled_FlagDeprecatedTakesPrecedence(t *testing.T) { +func TestLoadOfflineParams_Disabled_FlagDeprecatedTakesPrecedence(t *testing.T) { v := viper.New() v.Set("disable-offline", false) v.Set("disableoffline", true) @@ -1669,7 +1677,7 @@ func TestLoad_OfflineDisabled_FlagDeprecatedTakesPrecedence(t *testing.T) { assert.True(t, params.Disabled) } -func TestLoad_OfflineDisabled_FromFlag(t *testing.T) { +func TestLoadOfflineParams_Disabled_FromFlag(t *testing.T) { v := viper.New() v.Set("disable-offline", true) @@ -1678,7 +1686,83 @@ func TestLoad_OfflineDisabled_FromFlag(t *testing.T) { assert.True(t, params.Disabled) } -func TestLoad_OfflineQueueFile(t *testing.T) { +func TestLoadOfflineParams_RateLimit_FlagTakesPrecedence(t *testing.T) { + v := viper.New() + v.Set("heartbeat-rate-limit-seconds", 5) + v.Set("settings.heartbeat_rate_limit_seconds", 10) + + params := paramscmd.LoadOfflineParams(v) + + assert.Equal(t, time.Duration(5)*time.Second, params.RateLimit) +} + +func TestLoadOfflineParams_RateLimit_FromConfig(t *testing.T) { + v := viper.New() + v.Set("settings.heartbeat_rate_limit_seconds", 10) + + params := paramscmd.LoadOfflineParams(v) + + assert.Equal(t, time.Duration(10)*time.Second, params.RateLimit) +} + +func TestLoadOfflineParams_RateLimit_Zero(t *testing.T) { + v := viper.New() + v.Set("heartbeat-rate-limit-seconds", "0") + + params := paramscmd.LoadOfflineParams(v) + + assert.Zero(t, params.RateLimit) +} + +func TestLoadOfflineParams_RateLimit_Default(t *testing.T) { + v := viper.New() + v.SetDefault("heartbeat-rate-limit-seconds", 20) + + params := paramscmd.LoadOfflineParams(v) + + assert.Equal(t, time.Duration(20)*time.Second, params.RateLimit) +} + +func TestLoadOfflineParams_RateLimit_NegativeNumber(t *testing.T) { + v := viper.New() + v.Set("heartbeat-rate-limit-seconds", -1) + + params := paramscmd.LoadOfflineParams(v) + + assert.Zero(t, params.RateLimit) +} + +func TestLoadOfflineParams_RateLimit_NonIntegerValue(t *testing.T) { + v := viper.New() + v.Set("heartbeat-rate-limit-seconds", "invalid") + + params := paramscmd.LoadOfflineParams(v) + + assert.Zero(t, params.RateLimit) +} + +func TestLoadOfflineParams_LastSentAt(t *testing.T) { + v := viper.New() + v.Set("internal.heartbeats_last_sent_at", "2021-08-30T18:50:42-03:00") + + params := paramscmd.LoadOfflineParams(v) + + lastSentAt, err := time.Parse(inipkg.DateFormat, "2021-08-30T18:50:42-03:00") + require.NoError(t, err) + + assert.Equal(t, lastSentAt, params.LastSentAt) +} + +func TestLoadOfflineParams_LastSentAt_Err(t *testing.T) { + v := viper.New() + v.Set("internal.heartbeats_last_sent_at", "2021-08-30") + + params := paramscmd.LoadOfflineParams(v) + + assert.Zero(t, params.LastSentAt) +} + +func TestLoadOfflineParams_QueueFile(t *testing.T) { v := viper.New() v.Set("offline-queue-file", "/path/to/file") @@ -1687,7 +1771,7 @@ func TestLoad_OfflineQueueFile(t *testing.T) { assert.Equal(t, "/path/to/file", params.QueueFile) } -func TestLoad_OfflineQueueFileLegacy(t *testing.T) { +func TestLoadOfflineParams_QueueFileLegacy(t *testing.T) { v := viper.New() v.Set("offline-queue-file-legacy", "/path/to/file") @@ -1696,7 +1780,7 @@ func TestLoad_OfflineQueueFileLegacy(t *testing.T) { assert.Equal(t, "/path/to/file", params.QueueFileLegacy) } -func TestLoad_OfflineSyncMax(t *testing.T) { +func TestLoadOfflineParams_SyncMax(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", 42) @@ -1705,7 +1789,7 @@ func TestLoad_OfflineSyncMax(t *testing.T) { assert.Equal(t, 42, params.SyncMax) } -func TestLoad_OfflineSyncMax_Zero(t *testing.T) { +func TestLoadOfflineParams_SyncMax_Zero(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", "0") @@ -1714,7 +1798,7 @@ func TestLoad_OfflineSyncMax_Zero(t *testing.T) { assert.Zero(t, params.SyncMax) } -func TestLoad_OfflineSyncMax_Default(t *testing.T) { +func TestLoadOfflineParams_SyncMax_Default(t *testing.T) { v := viper.New() v.SetDefault("sync-offline-activity", 1000) @@ -1723,25 +1807,25 @@ func TestLoad_OfflineSyncMax_Default(t *testing.T) { assert.Equal(t, 1000, params.SyncMax) } -func TestLoad_OfflineSyncMax_NegativeNumber(t *testing.T) { +func TestLoadOfflineParams_SyncMax_NegativeNumber(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", -1) params := paramscmd.LoadOfflineParams(v) - assert.Equal(t, 0, params.SyncMax) + assert.Zero(t, params.SyncMax) } -func TestLoad_OfflineSyncMax_NonIntegerValue(t *testing.T) { +func TestLoadOfflineParams_SyncMax_NonIntegerValue(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", "invalid") params := paramscmd.LoadOfflineParams(v) - assert.Equal(t, 0, params.SyncMax) + assert.Zero(t, params.SyncMax) } -func TestLoad_API_APIKey(t *testing.T) { +func TestLoadAPIParams_APIKey(t *testing.T) { tests := map[string]struct { ViperAPIKey string ViperAPIKeyConfig string @@ -1793,7 +1877,7 @@ func TestLoad_API_APIKey(t *testing.T) { } } -func TestLoad_API_APIKeyUnset(t *testing.T) { +func TestLoadAPIParams_APIKeyUnset(t *testing.T) { v := viper.New() v.Set("key", "") @@ -1807,7 +1891,7 @@ func TestLoad_API_APIKeyUnset(t *testing.T) { assert.EqualError(t, errauth, "api key not found or empty") } -func TestLoad_API_APIKeyInvalid(t *testing.T) { +func TestLoadAPIParams_APIKeyInvalid(t *testing.T) { tests := map[string]string{ "invalid format 1": "not-uuid", "invalid format 2": "00000000-0000-0000-8000-000000000000", @@ -1830,7 +1914,7 @@ func TestLoad_API_APIKeyInvalid(t *testing.T) { } } -func TestLoadParams_ApiKey_SettingTakePrecedence(t *testing.T) { +func TestLoadAPIParams_ApiKey_SettingTakePrecedence(t *testing.T) { v := viper.New() v.Set("config", "testdata/.wakatime.cfg") v.Set("entity", "testdata/heartbeat_go.json") @@ -1847,7 +1931,7 @@ func TestLoadParams_ApiKey_SettingTakePrecedence(t *testing.T) { assert.Equal(t, "00000000-0000-4000-8000-000000000000", params.Key) } -func TestLoadParams_ApiKey_FromVault(t *testing.T) { +func TestLoadAPIParams_ApiKey_FromVault(t *testing.T) { v := viper.New() v.Set("config", "testdata/.wakatime-vault.cfg") v.Set("entity", "testdata/heartbeat_go.json") @@ -1884,7 +1968,7 @@ func TestLoadParams_ApiKey_FromVault_Err_Darwin(t *testing.T) { assert.EqualError(t, err, "failed to read api key from vault: exit status 1") } -func TestLoad_API_APIKeyFromEnv(t *testing.T) { +func TestLoadAPIParams_APIKeyFromEnv(t *testing.T) { v := viper.New() err := os.Setenv("WAKATIME_API_KEY", "00000000-0000-4000-8000-000000000000") @@ -1898,7 +1982,7 @@ func TestLoad_API_APIKeyFromEnv(t *testing.T) { assert.Equal(t, "00000000-0000-4000-8000-000000000000", params.Key) } -func TestLoad_API_APIKeyFromEnvInvalid(t *testing.T) { +func TestLoadAPIParams_APIKeyFromEnvInvalid(t *testing.T) { v := viper.New() err := os.Setenv("WAKATIME_API_KEY", "00000000-0000-4000-0000-000000000000") @@ -1916,7 +2000,7 @@ func TestLoad_API_APIKeyFromEnvInvalid(t *testing.T) { assert.EqualError(t, errauth, "invalid api key format") } -func TestLoad_API_APIKeyFromEnv_ConfigTakesPrecedence(t *testing.T) { +func TestLoadAPIParams_APIKeyFromEnv_ConfigTakesPrecedence(t *testing.T) { v := viper.New() v.Set("settings.api_key", "00000000-0000-4000-8000-000000000000") @@ -1931,7 +2015,7 @@ func TestLoad_API_APIKeyFromEnv_ConfigTakesPrecedence(t *testing.T) { assert.Equal(t, "00000000-0000-4000-8000-000000000000", params.Key) } -func TestLoad_API_APIUrl(t *testing.T) { +func TestLoadAPIParams_APIUrl(t *testing.T) { tests := map[string]struct { ViperAPIUrl string ViperAPIUrlConfig string @@ -2008,7 +2092,7 @@ func TestLoad_API_APIUrl(t *testing.T) { } } -func TestLoad_APIUrl_Default(t *testing.T) { +func TestLoadAPIParams_Url_Default(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -2018,7 +2102,7 @@ func TestLoad_APIUrl_Default(t *testing.T) { assert.Equal(t, api.BaseURL, params.URL) } -func TestLoad_APIUrl_InvalidFormat(t *testing.T) { +func TestLoadAPIParams_Url_InvalidFormat(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("api-url", "http://in valid") @@ -2032,7 +2116,7 @@ func TestLoad_APIUrl_InvalidFormat(t *testing.T) { assert.EqualError(t, errauth, `invalid api url: parse "http://in valid": invalid character " " in host name`) } -func TestLoad_API_BackoffAt(t *testing.T) { +func TestLoadAPIParams_BackoffAt(t *testing.T) { v := viper.New() v.Set("hostname", "my-computer") v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -2054,7 +2138,7 @@ func TestLoad_API_BackoffAt(t *testing.T) { }, params) } -func TestLoad_API_BackoffAtErr(t *testing.T) { +func TestLoadAPIParams_BackoffAtErr(t *testing.T) { v := viper.New() v.Set("hostname", "my-computer") v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -2073,47 +2157,7 @@ func TestLoad_API_BackoffAtErr(t *testing.T) { }, params) } -func TestLoad_API_Plugin(t *testing.T) { - v := viper.New() - v.Set("hostname", "my-computer") - v.Set("key", "00000000-0000-4000-8000-000000000000") - v.Set("plugin", "plugin/10.0.0") - - params, err := paramscmd.LoadAPIParams(v) - require.NoError(t, err) - - assert.Equal(t, paramscmd.API{ - Key: "00000000-0000-4000-8000-000000000000", - URL: "https://api.wakatime.com/api/v1", - Plugin: "plugin/10.0.0", - Hostname: "my-computer", - }, params) -} - -func TestLoad_API_Timeout_FlagTakesPrecedence(t *testing.T) { - v := viper.New() - v.Set("key", "00000000-0000-4000-8000-000000000000") - v.Set("timeout", 5) - v.Set("settings.timeout", 10) - - params, err := paramscmd.LoadAPIParams(v) - require.NoError(t, err) - - assert.Equal(t, 5*time.Second, params.Timeout) -} - -func TestLoad_API_Timeout_FromConfig(t *testing.T) { - v := viper.New() - v.Set("key", "00000000-0000-4000-8000-000000000000") - v.Set("settings.timeout", 10) - - params, err := paramscmd.LoadAPIParams(v) - require.NoError(t, err) - - assert.Equal(t, 10*time.Second, params.Timeout) -} - -func TestLoad_API_DisableSSLVerify_FlagTakesPrecedence(t *testing.T) { +func TestLoadAPIParams_DisableSSLVerify_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("no-ssl-verify", true) @@ -2125,7 +2169,7 @@ func TestLoad_API_DisableSSLVerify_FlagTakesPrecedence(t *testing.T) { assert.True(t, params.DisableSSLVerify) } -func TestLoad_API_DisableSSLVerify_FromConfig(t *testing.T) { +func TestLoadAPIParams_DisableSSLVerify_FromConfig(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.no_ssl_verify", true) @@ -2136,7 +2180,7 @@ func TestLoad_API_DisableSSLVerify_FromConfig(t *testing.T) { assert.True(t, params.DisableSSLVerify) } -func TestLoad_API_DisableSSLVerify_Default(t *testing.T) { +func TestLoadAPIParams_DisableSSLVerify_Default(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -2146,7 +2190,7 @@ func TestLoad_API_DisableSSLVerify_Default(t *testing.T) { assert.False(t, params.DisableSSLVerify) } -func TestLoad_API_ProxyURL(t *testing.T) { +func TestLoadAPIParams_ProxyURL(t *testing.T) { tests := map[string]string{ "https": "https://john:secret@example.org:8888", "http": "http://john:secret@example.org:8888", @@ -2169,7 +2213,7 @@ func TestLoad_API_ProxyURL(t *testing.T) { } } -func TestLoad_API_ProxyURL_FlagTakesPrecedence(t *testing.T) { +func TestLoadAPIParams_ProxyURL_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("proxy", "https://john:secret@example.org:8888") @@ -2181,7 +2225,7 @@ func TestLoad_API_ProxyURL_FlagTakesPrecedence(t *testing.T) { assert.Equal(t, "https://john:secret@example.org:8888", params.ProxyURL) } -func TestLoad_API_ProxyURL_UserDefinedTakesPrecedenceOverEnvironment(t *testing.T) { +func TestLoadAPIParams_ProxyURL_UserDefinedTakesPrecedenceOverEnvironment(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("proxy", "https://john:secret@example.org:8888") @@ -2197,7 +2241,7 @@ func TestLoad_API_ProxyURL_UserDefinedTakesPrecedenceOverEnvironment(t *testing. assert.Equal(t, "https://john:secret@example.org:8888", params.ProxyURL) } -func TestLoad_API_ProxyURL_FromConfig(t *testing.T) { +func TestLoadAPIParams_ProxyURL_FromConfig(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.proxy", "https://john:secret@example.org:8888") @@ -2208,7 +2252,7 @@ func TestLoad_API_ProxyURL_FromConfig(t *testing.T) { assert.Equal(t, "https://john:secret@example.org:8888", params.ProxyURL) } -func TestLoad_API_ProxyURL_FromEnvironment(t *testing.T) { +func TestLoadAPIParams_ProxyURL_FromEnvironment(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -2223,7 +2267,7 @@ func TestLoad_API_ProxyURL_FromEnvironment(t *testing.T) { assert.Equal(t, "https://john:secret@example.org:8888", params.ProxyURL) } -func TestLoad_API_ProxyURL_NoProxyFromEnvironment(t *testing.T) { +func TestLoadAPIParams_ProxyURL_NoProxyFromEnvironment(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -2238,7 +2282,7 @@ func TestLoad_API_ProxyURL_NoProxyFromEnvironment(t *testing.T) { assert.Empty(t, params.ProxyURL) } -func TestLoad_API_ProxyURL_InvalidFormat(t *testing.T) { +func TestLoadAPIParams_ProxyURL_InvalidFormat(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("proxy", "ftp://john:secret@example.org:8888") @@ -2256,7 +2300,7 @@ func TestLoad_API_ProxyURL_InvalidFormat(t *testing.T) { " 'socks5://user:pass@host:port' or 'domain\\\\user:pass.'") } -func TestLoad_API_SSLCertFilepath_FlagTakesPrecedence(t *testing.T) { +func TestLoadAPIParams_SSLCertFilepath_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("ssl-certs-file", "~/path/to/cert.pem") @@ -2270,7 +2314,7 @@ func TestLoad_API_SSLCertFilepath_FlagTakesPrecedence(t *testing.T) { assert.Equal(t, filepath.Join(home, "/path/to/cert.pem"), params.SSLCertFilepath) } -func TestLoad_API_SSLCertFilepath_FromConfig(t *testing.T) { +func TestLoadAPIParams_SSLCertFilepath_FromConfig(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.ssl_certs_file", "/path/to/cert.pem") @@ -2281,7 +2325,7 @@ func TestLoad_API_SSLCertFilepath_FromConfig(t *testing.T) { assert.Equal(t, "/path/to/cert.pem", params.SSLCertFilepath) } -func TestLoadParams_Hostname_FlagTakesPrecedence(t *testing.T) { +func TestLoadAPIParams_Hostname_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("hostname", "my-machine") @@ -2298,7 +2342,7 @@ func TestLoadParams_Hostname_FlagTakesPrecedence(t *testing.T) { assert.Equal(t, "my-machine", params.Hostname) } -func TestLoadParams_Hostname_FromConfig(t *testing.T) { +func TestLoadAPIParams_Hostname_FromConfig(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.hostname", "my-machine") @@ -2309,7 +2353,7 @@ func TestLoadParams_Hostname_FromConfig(t *testing.T) { assert.Equal(t, "my-machine", params.Hostname) } -func TestLoadParams_Hostname_FromConfig_ConfigTakesPrecedence(t *testing.T) { +func TestLoadAPIParams_Hostname_ConfigTakesPrecedence(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.hostname", "my-machine") @@ -2325,7 +2369,7 @@ func TestLoadParams_Hostname_FromConfig_ConfigTakesPrecedence(t *testing.T) { assert.Equal(t, "my-machine", params.Hostname) } -func TestLoadParams_Hostname_FromGitpodEnv(t *testing.T) { +func TestLoadAPIParams_Hostname_FromGitpodEnv(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -2340,7 +2384,7 @@ func TestLoadParams_Hostname_FromGitpodEnv(t *testing.T) { assert.Equal(t, "Gitpod", params.Hostname) } -func TestLoadParams_Hostname_DefaultFromSystem(t *testing.T) { +func TestLoadAPIParams_Hostname_DefaultFromSystem(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") @@ -2353,7 +2397,7 @@ func TestLoadParams_Hostname_DefaultFromSystem(t *testing.T) { assert.Equal(t, expected, params.Hostname) } -func TestLoadParams_StatusBar_HideCategories_FlagTakesPrecedence(t *testing.T) { +func TestLoadStatusBarParams_HideCategories_FlagTakesPrecedence(t *testing.T) { v := viper.New() v.Set("today-hide-categories", true) v.Set("settings.status_bar_hide_categories", "ignored") @@ -2364,7 +2408,7 @@ func TestLoadParams_StatusBar_HideCategories_FlagTakesPrecedence(t *testing.T) { assert.True(t, params.HideCategories) } -func TestLoadParams_StatusBar_HideCategories_ConfigTakesPrecedence(t *testing.T) { +func TestLoadStatusBarParams_HideCategories_ConfigTakesPrecedence(t *testing.T) { v := viper.New() v.Set("settings.status_bar_hide_categories", true) @@ -2374,7 +2418,7 @@ func TestLoadParams_StatusBar_HideCategories_ConfigTakesPrecedence(t *testing.T) assert.True(t, params.HideCategories) } -func TestLoadParams_StatusBar_Output(t *testing.T) { +func TestLoadStatusBarParams_Output(t *testing.T) { tests := map[string]output.Output{ "text": output.TextOutput, "json": output.JSONOutput, @@ -2393,7 +2437,7 @@ func TestLoadParams_StatusBar_Output(t *testing.T) { } } -func TestLoadParams_StatusBar_Output_Default(t *testing.T) { +func TestLoadStatusBarParams_Output_Default(t *testing.T) { v := viper.New() params, err := paramscmd.LoadStatusBarParams(v) @@ -2402,7 +2446,7 @@ func TestLoadParams_StatusBar_Output_Default(t *testing.T) { assert.Equal(t, output.TextOutput, params.Output) } -func TestLoadParams_StatusBar_Output_Invalid(t *testing.T) { +func TestLoadStatusBarParams_Output_Invalid(t *testing.T) { v := viper.New() v.Set("output", "invalid") @@ -2495,18 +2539,24 @@ func TestHeartbeat_String(t *testing.T) { } func TestOffline_String(t *testing.T) { + lastSentAt, err := time.Parse(inipkg.DateFormat, "2021-08-30T18:50:42-03:00") + require.NoError(t, err) + offline := paramscmd.Offline{ Disabled: true, + LastSentAt: lastSentAt, PrintMax: 6, QueueFile: "/path/to/queue.file", QueueFileLegacy: "/path/to/legacy.file", + RateLimit: 15, SyncMax: 12, } assert.Equal( t, - "disabled: true, print max: 6, queue file: '/path/to/queue.file',"+ - " queue file legacy: '/path/to/legacy.file', num sync max: 12", + "disabled: true, last sent at: '2021-08-30T18:50:42-03:00', print max: 6,"+ + " queue file: '/path/to/queue.file', queue file legacy: '/path/to/legacy.file',"+ + " num rate limit: 15, num sync max: 12", offline.String(), ) } @@ -2530,7 +2580,7 @@ func TestProjectParams_String(t *testing.T) { ) } -func TestLoadParams_ProjectFromGitRemote(t *testing.T) { +func TestLoadHeartbeatParams_ProjectFromGitRemote(t *testing.T) { v := viper.New() v.Set("git.project_from_git_remote", true) v.Set("entity", "/path/to/file") @@ -2571,14 +2621,6 @@ func TestStatusBar_String(t *testing.T) { ) } -func TestLoadParams_APIKeyPrefixSupported(t *testing.T) { - v := viper.New() - v.Set("key", "waka_00000000-0000-4000-8000-000000000000") - - _, err := paramscmd.LoadAPIParams(v) - require.NoError(t, err) -} - func captureLogs(dest io.Writer) func() { // set verbose log.SetVerbose(true) diff --git a/cmd/root.go b/cmd/root.go index c0dc86f3..516dfd06 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -124,6 +124,13 @@ func setFlags(cmd *cobra.Command, v *viper.Viper) { "guess-language", false, "Enable detecting language from file contents.") + flags.Int( + "heartbeat-rate-limit-seconds", + offline.RateLimitDefaultSeconds, + fmt.Sprintf("Only sync heartbeats to the API once per these seconds, instead"+ + " saving to the offline db. Defaults to %d. Use zero to disable.", + offline.RateLimitDefaultSeconds), + ) flags.String("hide-branch-names", "", "Obfuscate branch names. Will not send revision control branch names to api.") flags.String("hide-file-names", "", "Obfuscate filenames. Will not send file names to api.") flags.String("hide-filenames", "", "(deprecated) Obfuscate filenames. Will not send file names to api.") diff --git a/cmd/run.go b/cmd/run.go index 25811c73..73b4f97a 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -138,7 +138,7 @@ func RunE(cmd *cobra.Command, v *viper.Viper) error { if v.IsSet("sync-offline-activity") { log.Debugln("command: sync-offline-activity") - return RunCmd(v, logFileParams.Verbose, logFileParams.SendDiagsOnErrors, offlinesync.Run) + return RunCmd(v, logFileParams.Verbose, logFileParams.SendDiagsOnErrors, offlinesync.RunWithoutRateLimiting) } if v.GetBool("offline-count") { @@ -273,7 +273,7 @@ func RunCmdWithOfflineSync(v *viper.Viper, verbose bool, sendDiagsOnErrors bool, return err } - return runCmd(v, verbose, sendDiagsOnErrors, offlinesync.Run) + return runCmd(v, verbose, sendDiagsOnErrors, offlinesync.RunWithRateLimiting) } // runCmd contains the main logic of RunCmd. diff --git a/pkg/offline/offline.go b/pkg/offline/offline.go index 59de11c9..1f4ed907 100644 --- a/pkg/offline/offline.go +++ b/pkg/offline/offline.go @@ -27,6 +27,9 @@ const ( maxRequeueAttempts = 3 // PrintMaxDefault is the default maximum number of heartbeats to print. PrintMaxDefault = 10 + // RateLimitDefaultSeconds is the default seconds between sending heartbeats + // to the API. If not enough time has passed, heartbeats are saved to the offline queue. + RateLimitDefaultSeconds = 120 // SendLimit is the maximum number of heartbeats, which will be sent at once // to the WakaTime API. SendLimit = 25 From 0797fbb1d7a0ea4513bc35d1490798997e0b020d Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Wed, 24 Jul 2024 13:03:06 +0200 Subject: [PATCH 3/4] Only read extra heartbeats from stdin once --- cmd/params/params.go | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/cmd/params/params.go b/cmd/params/params.go index f43a3c30..21147c4c 100644 --- a/cmd/params/params.go +++ b/cmd/params/params.go @@ -13,6 +13,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "github.com/wakatime/wakatime-cli/pkg/api" @@ -398,10 +399,7 @@ func LoadHeartbeatParams(v *viper.Viper) (Heartbeat, error) { var extraHeartbeats []heartbeat.Heartbeat if v.GetBool("extra-heartbeats") { - extraHeartbeats, err = readExtraHeartbeats() - if err != nil { - log.Errorf("failed to read extra heartbeats: %s", err) - } + extraHeartbeats = readExtraHeartbeats() } var isWrite *bool @@ -752,20 +750,27 @@ func readAPIKeyFromCommand(cmdStr string) (string, error) { return strings.TrimSpace(string(out)), nil } -func readExtraHeartbeats() ([]heartbeat.Heartbeat, error) { - in := bufio.NewReader(os.Stdin) +var extraHeartbeatsCache *[]heartbeat.Heartbeat // nolint:gochecknoglobals +var once sync.Once // nolint:gochecknoglobals - input, err := in.ReadString('\n') - if err != nil && err != io.EOF { - log.Debugf("failed to read data from stdin: %s", err) - } +func readExtraHeartbeats() []heartbeat.Heartbeat { + once.Do(func() { + in := bufio.NewReader(os.Stdin) - heartbeats, err := parseExtraHeartbeats(input) - if err != nil { - return nil, fmt.Errorf("failed parsing: %s", err) - } + input, err := in.ReadString('\n') + if err != nil && err != io.EOF { + log.Debugf("failed to read data from stdin: %s", err) + } - return heartbeats, nil + heartbeats, err := parseExtraHeartbeats(input) + if err != nil { + log.Errorf("failed parsing: %s", err) + } + + extraHeartbeatsCache = &heartbeats + }) + + return *extraHeartbeatsCache } func parseExtraHeartbeats(data string) ([]heartbeat.Heartbeat, error) { From ac9acf6eab3b75fc30815f20131fae61f5fdff01 Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Wed, 24 Jul 2024 13:22:31 +0200 Subject: [PATCH 4/4] Fix tests with Once singleton --- cmd/heartbeat/heartbeat_test.go | 10 + cmd/params/params.go | 6 +- cmd/params/params_test.go | 385 ++++++++++++++++---------------- 3 files changed, 211 insertions(+), 190 deletions(-) diff --git a/cmd/heartbeat/heartbeat_test.go b/cmd/heartbeat/heartbeat_test.go index 9e34a203..d6d09e63 100644 --- a/cmd/heartbeat/heartbeat_test.go +++ b/cmd/heartbeat/heartbeat_test.go @@ -10,11 +10,13 @@ import ( "path/filepath" "runtime" "strings" + "sync" "testing" "time" "github.com/wakatime/wakatime-cli/cmd" cmdheartbeat "github.com/wakatime/wakatime-cli/cmd/heartbeat" + cmdparams "github.com/wakatime/wakatime-cli/cmd/params" "github.com/wakatime/wakatime-cli/pkg/api" "github.com/wakatime/wakatime-cli/pkg/heartbeat" "github.com/wakatime/wakatime-cli/pkg/ini" @@ -290,6 +292,8 @@ func TestSendHeartbeats_ExtraHeartbeats(t *testing.T) { os.Stdin = r + cmdparams.Once = sync.Once{} + data, err := os.ReadFile("testdata/extra_heartbeats.json") require.NoError(t, err) @@ -372,6 +376,8 @@ func TestSendHeartbeats_ExtraHeartbeats_Sanitize(t *testing.T) { os.Stdin = r + cmdparams.Once = sync.Once{} + data, err := os.ReadFile("testdata/extra_heartbeats.json") require.NoError(t, err) @@ -580,6 +586,8 @@ func TestSendHeartbeats_IsUnsavedEntity(t *testing.T) { os.Stdin = inr + cmdparams.Once = sync.Once{} + data, err := os.ReadFile("testdata/extra_heartbeats_is_unsaved_entity.json") require.NoError(t, err) @@ -710,6 +718,8 @@ func TestSendHeartbeats_NonExistingExtraHeartbeatsEntity(t *testing.T) { os.Stdin = inr + cmdparams.Once = sync.Once{} + data, err := os.ReadFile("testdata/extra_heartbeats_nonexisting_entity.json") require.NoError(t, err) diff --git a/cmd/params/params.go b/cmd/params/params.go index 21147c4c..72a63ae2 100644 --- a/cmd/params/params.go +++ b/cmd/params/params.go @@ -751,10 +751,12 @@ func readAPIKeyFromCommand(cmdStr string) (string, error) { } var extraHeartbeatsCache *[]heartbeat.Heartbeat // nolint:gochecknoglobals -var once sync.Once // nolint:gochecknoglobals + +// Once prevents reading from stdin twice. +var Once sync.Once // nolint:gochecknoglobals func readExtraHeartbeats() []heartbeat.Heartbeat { - once.Do(func() { + Once.Do(func() { in := bufio.NewReader(os.Stdin) input, err := in.ReadString('\n') diff --git a/cmd/params/params_test.go b/cmd/params/params_test.go index cda37cee..4738f53f 100644 --- a/cmd/params/params_test.go +++ b/cmd/params/params_test.go @@ -9,10 +9,11 @@ import ( "regexp" "runtime" "strings" + "sync" "testing" "time" - paramscmd "github.com/wakatime/wakatime-cli/cmd/params" + cmdparams "github.com/wakatime/wakatime-cli/cmd/params" "github.com/wakatime/wakatime-cli/pkg/api" "github.com/wakatime/wakatime-cli/pkg/apikey" "github.com/wakatime/wakatime-cli/pkg/heartbeat" @@ -33,7 +34,7 @@ func TestLoadHeartbeatParams_AlternateProject(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("alternate-project", "web") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, "web", params.Project.Alternate) @@ -43,7 +44,7 @@ func TestLoadHeartbeatParams_AlternateProject_Unset(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Empty(t, params.Project.Alternate) @@ -77,7 +78,7 @@ func TestLoadHeartbeatParams_Category(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("category", name) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, category, params.Category) @@ -89,7 +90,7 @@ func TestLoadHeartbeatParams_Category_Default(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, heartbeat.CodingCategory, params.Category) @@ -100,7 +101,7 @@ func TestLoadHeartbeatParams_Category_Invalid(t *testing.T) { v.SetDefault("sync-offline-activity", 1000) v.Set("category", "invalid") - _, err := paramscmd.LoadHeartbeatParams(v) + _, err := cmdparams.LoadHeartbeatParams(v) require.Error(t, err) assert.Equal(t, "failed to parse category: invalid category \"invalid\"", err.Error()) @@ -111,7 +112,7 @@ func TestLoadHeartbeatParams_CursorPosition(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("cursorpos", 42) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, 42, *params.CursorPosition) @@ -122,7 +123,7 @@ func TestLoadHeartbeatParams_CursorPosition_Zero(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("cursorpos", 0) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Zero(t, *params.CursorPosition) @@ -133,7 +134,7 @@ func TestLoadHeartbeatParams_CursorPosition_Unset(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("key", "00000000-0000-4000-8000-000000000000") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Nil(t, params.CursorPosition) @@ -144,7 +145,7 @@ func TestLoadHeartbeatParams_Entity_EntityFlagTakesPrecedence(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("file", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, "/path/to/file", params.Entity) @@ -157,7 +158,7 @@ func TestLoadHeartbeatParams_Entity_FileFlag(t *testing.T) { home, err := os.UserHomeDir() require.NoError(t, err) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, filepath.Join(home, "/path/to/file"), params.Entity) @@ -166,7 +167,7 @@ func TestLoadHeartbeatParams_Entity_FileFlag(t *testing.T) { func TestLoadHeartbeatParams_Entity_Unset(t *testing.T) { v := viper.New() - _, err := paramscmd.LoadHeartbeatParams(v) + _, err := cmdparams.LoadHeartbeatParams(v) require.Error(t, err) assert.Equal(t, "failed to retrieve entity", err.Error()) @@ -185,7 +186,7 @@ func TestLoadHeartbeatParams_EntityType(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("entity-type", name) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, entityType, params.EntityType) @@ -197,7 +198,7 @@ func TestLoadHeartbeatParams_EntityType_Default(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, heartbeat.FileType, params.EntityType) @@ -208,7 +209,7 @@ func TestLoadHeartbeatParams_EntityType_Invalid(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("entity-type", "invalid") - _, err := paramscmd.LoadHeartbeatParams(v) + _, err := cmdparams.LoadHeartbeatParams(v) require.Error(t, err) assert.Equal( @@ -232,6 +233,8 @@ func TestLoadHeartbeatParams_ExtraHeartbeats(t *testing.T) { os.Stdin = r + cmdparams.Once = sync.Once{} + data, err := os.ReadFile("testdata/extra_heartbeats.json") require.NoError(t, err) @@ -246,7 +249,7 @@ func TestLoadHeartbeatParams_ExtraHeartbeats(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("extra-heartbeats", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Len(t, params.ExtraHeartbeats, 2) @@ -304,6 +307,8 @@ func TestLoadHeartbeatParams_ExtraHeartbeats_WithStringValues(t *testing.T) { os.Stdin = r + cmdparams.Once = sync.Once{} + data, err := os.ReadFile("testdata/extra_heartbeats_with_string_values.json") require.NoError(t, err) @@ -318,7 +323,7 @@ func TestLoadHeartbeatParams_ExtraHeartbeats_WithStringValues(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("extra-heartbeats", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Len(t, params.ExtraHeartbeats, 2) @@ -371,6 +376,8 @@ func TestLoadHeartbeatParams_ExtraHeartbeats_WithEOF(t *testing.T) { os.Stdin = r + cmdparams.Once = sync.Once{} + data, err := os.ReadFile("testdata/extra_heartbeats.json") require.NoError(t, err) @@ -386,7 +393,7 @@ func TestLoadHeartbeatParams_ExtraHeartbeats_WithEOF(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("extra-heartbeats", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Len(t, params.ExtraHeartbeats, 2) @@ -449,6 +456,8 @@ func TestLoadHeartbeatParams_ExtraHeartbeats_NoData(t *testing.T) { os.Stdin = r + cmdparams.Once = sync.Once{} + go func() { _, err := w.Write([]byte{}) require.NoError(t, err) @@ -460,7 +469,7 @@ func TestLoadHeartbeatParams_ExtraHeartbeats_NoData(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("extra-heartbeats", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Empty(t, params.ExtraHeartbeats) @@ -475,7 +484,7 @@ func TestLoadHeartbeat_GuessLanguage_FlagTakesPrecedence(t *testing.T) { v.Set("guess-language", true) v.Set("settings.guess_language", false) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.True(t, params.GuessLanguage) @@ -486,7 +495,7 @@ func TestLoadHeartbeat_GuessLanguage_FromConfig(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("settings.guess_language", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.True(t, params.GuessLanguage) @@ -496,7 +505,7 @@ func TestLoadHeartbeat_GuessLanguage_Default(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.False(t, params.GuessLanguage) @@ -507,7 +516,7 @@ func TestLoadHeartbeatParams_IsUnsavedEntity(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("is-unsaved-entity", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.True(t, params.IsUnsavedEntity) @@ -525,7 +534,7 @@ func TestLoadHeartbeatParams_IsWrite(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("write", isWrite) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, isWrite, *params.IsWrite) @@ -537,7 +546,7 @@ func TestLoadHeartbeatParams_IsWrite_Unset(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Nil(t, params.IsWrite) @@ -548,7 +557,7 @@ func TestLoadHeartbeatParams_Language(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("language", "Go") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, heartbeat.LanguageGo.String(), *params.Language) @@ -559,7 +568,7 @@ func TestLoadHeartbeatParams_LanguageAlternate(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("alternate-language", "Go") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, heartbeat.LanguageGo.String(), params.LanguageAlternate) @@ -571,7 +580,7 @@ func TestLoadHeartbeatParams_LineNumber(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("lineno", 42) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, 42, *params.LineNumber) @@ -582,7 +591,7 @@ func TestLoadHeartbeatParams_LineNumber_Zero(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("lineno", 0) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Zero(t, *params.LineNumber) @@ -592,7 +601,7 @@ func TestLoadHeartbeatParams_LineNumber_Unset(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Nil(t, params.LineNumber) @@ -603,7 +612,7 @@ func TestLoadHeartbeatParams_LocalFile(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("local-file", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, "/path/to/file", params.LocalFile) @@ -614,7 +623,7 @@ func TestLoadHeartbeatParams_Project(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("project", "billing") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, "billing", params.Project.Override) @@ -624,7 +633,7 @@ func TestLoadHeartbeatParams_Project_Unset(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Empty(t, params.Project.Override) @@ -667,7 +676,7 @@ func TestLoadHeartbeatParams_ProjectMap(t *testing.T) { v.Set("entity", test.Entity) v.Set(fmt.Sprintf("projectmap.%s", test.Regex.String()), test.Project) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, test.Expected, params.Project.MapPatterns) @@ -725,7 +734,7 @@ func TestLoadAPIParams_ProjectApiKey(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set(fmt.Sprintf("project_api_key.%s", test.Regex.String()), test.APIKey) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, test.Expected, params.KeyPatterns) @@ -744,7 +753,7 @@ func TestLoadAPIParams_ProjectApiKey_ParseConfig(t *testing.T) { err = inipkg.ReadInConfig(v, configFile) require.NoError(t, err) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) expected := []apikey.MapPattern{ @@ -761,7 +770,7 @@ func TestLoadAPIParams_APIKeyPrefixSupported(t *testing.T) { v := viper.New() v.Set("key", "waka_00000000-0000-4000-8000-000000000000") - _, err := paramscmd.LoadAPIParams(v) + _, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) } @@ -770,7 +779,7 @@ func TestLoadHeartbeatParams_Time(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("time", 1590609206.1) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, 1590609206.1, params.Time) @@ -780,7 +789,7 @@ func TestLoadHeartbeatParams_Time_Default(t *testing.T) { v := viper.New() v.Set("entity", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) now := float64(time.Now().UnixNano()) / 1000000000 @@ -795,7 +804,7 @@ func TestLoadHeartbeatParams_Filter_Exclude(t *testing.T) { v.Set("settings.exclude", []string{".+", "wakatime.+"}) v.Set("settings.ignore", []string{".?", "wakatime.?"}) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) require.Len(t, params.Filter.Exclude, 6) @@ -812,7 +821,7 @@ func TestLoadHeartbeatParams_Filter_Exclude_Multiline(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("settings.ignore", "\t.?\n\twakatime.? \t\n") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) require.Len(t, params.Filter.Exclude, 2) @@ -825,7 +834,7 @@ func TestLoadHeartbeatParams_Filter_Exclude_IgnoresInvalidRegex(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("exclude", []string{".*", "["}) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) require.Len(t, params.Filter.Exclude, 1) @@ -844,7 +853,7 @@ func TestLoadHeartbeatParams_Filter_Exclude_PerlRegexPatterns(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("exclude", []string{pattern}) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) require.Len(t, params.Filter.Exclude, 1) @@ -858,7 +867,7 @@ func TestLoadHeartbeatParams_Filter_ExcludeUnknownProject(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("exclude-unknown-project", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.True(t, params.Filter.ExcludeUnknownProject) @@ -870,7 +879,7 @@ func TestLoadHeartbeatParams_Filter_ExcludeUnknownProject_FromConfig(t *testing. v.Set("exclude-unknown-project", false) v.Set("settings.exclude_unknown_project", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.True(t, params.Filter.ExcludeUnknownProject) @@ -882,7 +891,7 @@ func TestLoadHeartbeatParams_Filter_Include(t *testing.T) { v.Set("include", []string{".*", "wakatime.*"}) v.Set("settings.include", []string{".+", "wakatime.+"}) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) require.Len(t, params.Filter.Include, 4) @@ -897,7 +906,7 @@ func TestLoadHeartbeatParams_Filter_Include_IgnoresInvalidRegex(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("include", []string{".*", "["}) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) require.Len(t, params.Filter.Include, 1) @@ -916,7 +925,7 @@ func TestLoadHeartbeatParams_Filter_Include_PerlRegexPatterns(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("include", []string{pattern}) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) require.Len(t, params.Filter.Include, 1) @@ -930,7 +939,7 @@ func TestLoadHeartbeatParams_Filter_IncludeOnlyWithProjectFile(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("include-only-with-project-file", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.True(t, params.Filter.IncludeOnlyWithProjectFile) @@ -942,7 +951,7 @@ func TestLoadHeartbeatParams_Filter_IncludeOnlyWithProjectFile_FromConfig(t *tes v.Set("include-only-with-project-file", false) v.Set("settings.include_only_with_project_file", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.True(t, params.Filter.IncludeOnlyWithProjectFile) @@ -961,10 +970,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_True(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("hide-branch-names", viperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideBranchNames: []regex.Regex{regex.MustCompile(".*")}, }, params.Sanitize) }) @@ -984,10 +993,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_False(t *testing.T) v.Set("entity", "/path/to/file") v.Set("hide-branch-names", viperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideBranchNames: []regex.Regex{regex.MustCompile("a^")}, }, params.Sanitize) }) @@ -1020,10 +1029,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_List(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("hide-branch-names", test.ViperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideBranchNames: test.Expected, }, params.Sanitize) }) @@ -1038,10 +1047,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_FlagTakesPrecedence( v.Set("settings.hide_branchnames", "ignored") v.Set("settings.hidebranchnames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideBranchNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1053,10 +1062,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_ConfigTakesPrecedenc v.Set("settings.hide_branchnames", "ignored") v.Set("settings.hidebranchnames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideBranchNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1067,10 +1076,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_ConfigDeprecatedOneT v.Set("settings.hide_branchnames", "true") v.Set("settings.hidebranchnames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideBranchNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1080,10 +1089,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_ConfigDeprecatedTwo( v.Set("entity", "/path/to/file") v.Set("settings.hidebranchnames", "true") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideBranchNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1093,7 +1102,7 @@ func TestLoadHeartbeatParams_SanitizeParams_HideBranchNames_InvalidRegex(t *test v.Set("entity", "/path/to/file") v.Set("hide-branch-names", ".*secret.*\n[0-9+") - _, err := paramscmd.LoadHeartbeatParams(v) + _, err := cmdparams.LoadHeartbeatParams(v) require.Error(t, err) assert.True(t, strings.HasPrefix( @@ -1117,10 +1126,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_True(t *testing.T) v.Set("entity", "/path/to/file") v.Set("hide-project-names", viperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideProjectNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) }) @@ -1140,10 +1149,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_False(t *testing.T) v.Set("entity", "/path/to/file") v.Set("hide-project-names", viperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideProjectNames: []regex.Regex{regexp.MustCompile("a^")}, }, params.Sanitize) }) @@ -1176,10 +1185,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjecthNames_List(t *testing.T) v.Set("entity", "/path/to/file") v.Set("hide-project-names", test.ViperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideProjectNames: test.Expected, }, params.Sanitize) }) @@ -1194,10 +1203,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_FlagTakesPrecedence v.Set("settings.hide_projectnames", "ignored") v.Set("settings.hideprojectnames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideProjectNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1209,10 +1218,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_ConfigTakesPreceden v.Set("settings.hide_projectnames", "ignored") v.Set("settings.hideprojectnames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideProjectNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1223,10 +1232,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_ConfigDeprecatedOne v.Set("settings.hide_projectnames", "true") v.Set("settings.hideprojectnames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideProjectNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1236,10 +1245,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_ConfigDeprecatedTwo v.Set("entity", "/path/to/file") v.Set("settings.hideprojectnames", "true") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideProjectNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1249,7 +1258,7 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjectNames_InvalidRegex(t *tes v.Set("entity", "/path/to/file") v.Set("hide-project-names", ".*secret.*\n[0-9+") - _, err := paramscmd.LoadHeartbeatParams(v) + _, err := cmdparams.LoadHeartbeatParams(v) require.Error(t, err) assert.True(t, strings.HasPrefix( @@ -1273,10 +1282,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_True(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("hide-file-names", viperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideFileNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) }) @@ -1296,10 +1305,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_False(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("hide-file-names", viperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideFileNames: []regex.Regex{regexp.MustCompile("a^")}, }, params.Sanitize) }) @@ -1332,10 +1341,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_List(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("hide-file-names", test.ViperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideFileNames: test.Expected, }, params.Sanitize) }) @@ -1352,10 +1361,10 @@ func TestLoadheartbeatParams_SanitizeParams_HideFileNames_FlagTakesPrecedence(t v.Set("settings.hide_filenames", "ignored") v.Set("settings.hidefilenames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideFileNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1369,10 +1378,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_FlagDeprecatedOneTakes v.Set("settings.hide_filenames", "ignored") v.Set("settings.hidefilenames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideFileNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1385,10 +1394,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_FlagDeprecatedTwoTakes v.Set("settings.hide_filenames", "ignored") v.Set("settings.hidefilenames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideFileNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1400,10 +1409,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_ConfigTakesPrecedence( v.Set("settings.hide_filenames", "ignored") v.Set("settings.hidefilenames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideFileNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1414,10 +1423,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_ConfigDeprecatedOneTak v.Set("settings.hide_filenames", "true") v.Set("settings.hidefilenames", "ignored") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideFileNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1427,10 +1436,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_ConfigDeprecatedTwo(t v.Set("entity", "/path/to/file") v.Set("settings.hidefilenames", "true") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideFileNames: []regex.Regex{regexp.MustCompile(".*")}, }, params.Sanitize) } @@ -1440,7 +1449,7 @@ func TestLoadHeartbeatParams_SanitizeParams_HideFileNames_InvalidRegex(t *testin v.Set("entity", "/path/to/file") v.Set("hide-file-names", ".*secret.*\n[0-9+") - _, err := paramscmd.LoadHeartbeatParams(v) + _, err := cmdparams.LoadHeartbeatParams(v) require.Error(t, err) assert.True(t, strings.HasPrefix( @@ -1456,10 +1465,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjectFolder(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("hide-project-folder", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideProjectFolder: true, }, params.Sanitize) } @@ -1469,10 +1478,10 @@ func TestLoadHeartbeatParams_SanitizeParams_HideProjectFolder_ConfigTakesPrecede v.Set("entity", "/path/to/file") v.Set("settings.hide_project_folder", true) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ HideProjectFolder: true, }, params.Sanitize) } @@ -1482,10 +1491,10 @@ func TestLoadHeartbeatParams_SanitizeParams_OverrideProjectPath(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("project-folder", "/custom-path") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.SanitizeParams{ + assert.Equal(t, cmdparams.SanitizeParams{ ProjectPathOverride: "/custom-path", }, params.Sanitize) } @@ -1503,7 +1512,7 @@ func TestLoadHeartbeatParams_SubmodulesDisabled_True(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("git.submodules_disabled", viperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, []regex.Regex{regexp.MustCompile(".*")}, params.Project.SubmodulesDisabled) @@ -1524,7 +1533,7 @@ func TestLoadHeartbeatParams_SubmodulesDisabled_False(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("git.submodules_disabled", viperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, params.Project.SubmodulesDisabled, []regex.Regex{regexp.MustCompile("a^")}) @@ -1559,7 +1568,7 @@ func TestLoadHeartbeatsParams_SubmodulesDisabled_List(t *testing.T) { v.Set("entity", "/path/to/file") v.Set("git.submodules_disabled", test.ViperValue) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, test.Expected, params.Project.SubmodulesDisabled) @@ -1604,7 +1613,7 @@ func TestLoadHeartbeatsParams_SubmoduleProjectMap(t *testing.T) { v.Set("entity", test.Entity) v.Set(fmt.Sprintf("git_submodule_projectmap.%s", test.Regex.String()), test.Project) - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.Equal(t, test.Expected, params.Project.SubmoduleMapPatterns) @@ -1617,7 +1626,7 @@ func TestLoadAPIParams_Plugin(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("plugin", "plugin/10.0.0") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "plugin/10.0.0", params.Plugin) @@ -1627,7 +1636,7 @@ func TestLoadAPIParams_Plugin_Unset(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Empty(t, params.Plugin) @@ -1639,7 +1648,7 @@ func TestLoadAPIParams_Timeout_FlagTakesPrecedence(t *testing.T) { v.Set("timeout", 5) v.Set("settings.timeout", 10) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, 5*time.Second, params.Timeout) @@ -1650,7 +1659,7 @@ func TestLoadAPIParams_Timeout_FromConfig(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.timeout", 10) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, 10*time.Second, params.Timeout) @@ -1662,7 +1671,7 @@ func TestLoadOfflineParams_Disabled_ConfigTakesPrecedence(t *testing.T) { v.Set("disableoffline", false) v.Set("settings.offline", false) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.True(t, params.Disabled) } @@ -1672,7 +1681,7 @@ func TestLoadOfflineParams_Disabled_FlagDeprecatedTakesPrecedence(t *testing.T) v.Set("disable-offline", false) v.Set("disableoffline", true) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.True(t, params.Disabled) } @@ -1681,7 +1690,7 @@ func TestLoadOfflineParams_Disabled_FromFlag(t *testing.T) { v := viper.New() v.Set("disable-offline", true) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.True(t, params.Disabled) } @@ -1691,7 +1700,7 @@ func TestLoadOfflineParams_RateLimit_FlagTakesPrecedence(t *testing.T) { v.Set("heartbeat-rate-limit-seconds", 5) v.Set("settings.heartbeat_rate_limit_seconds", 10) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Equal(t, time.Duration(5)*time.Second, params.RateLimit) } @@ -1700,7 +1709,7 @@ func TestLoadOfflineParams_RateLimit_FromConfig(t *testing.T) { v := viper.New() v.Set("settings.heartbeat_rate_limit_seconds", 10) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Equal(t, time.Duration(10)*time.Second, params.RateLimit) } @@ -1709,7 +1718,7 @@ func TestLoadOfflineParams_RateLimit_Zero(t *testing.T) { v := viper.New() v.Set("heartbeat-rate-limit-seconds", "0") - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Zero(t, params.RateLimit) } @@ -1718,7 +1727,7 @@ func TestLoadOfflineParams_RateLimit_Default(t *testing.T) { v := viper.New() v.SetDefault("heartbeat-rate-limit-seconds", 20) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Equal(t, time.Duration(20)*time.Second, params.RateLimit) } @@ -1727,7 +1736,7 @@ func TestLoadOfflineParams_RateLimit_NegativeNumber(t *testing.T) { v := viper.New() v.Set("heartbeat-rate-limit-seconds", -1) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Zero(t, params.RateLimit) } @@ -1736,7 +1745,7 @@ func TestLoadOfflineParams_RateLimit_NonIntegerValue(t *testing.T) { v := viper.New() v.Set("heartbeat-rate-limit-seconds", "invalid") - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Zero(t, params.RateLimit) } @@ -1745,7 +1754,7 @@ func TestLoadOfflineParams_LastSentAt(t *testing.T) { v := viper.New() v.Set("internal.heartbeats_last_sent_at", "2021-08-30T18:50:42-03:00") - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) lastSentAt, err := time.Parse(inipkg.DateFormat, "2021-08-30T18:50:42-03:00") require.NoError(t, err) @@ -1757,7 +1766,7 @@ func TestLoadOfflineParams_LastSentAt_Err(t *testing.T) { v := viper.New() v.Set("internal.heartbeats_last_sent_at", "2021-08-30") - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Zero(t, params.LastSentAt) } @@ -1766,7 +1775,7 @@ func TestLoadOfflineParams_QueueFile(t *testing.T) { v := viper.New() v.Set("offline-queue-file", "/path/to/file") - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Equal(t, "/path/to/file", params.QueueFile) } @@ -1775,7 +1784,7 @@ func TestLoadOfflineParams_QueueFileLegacy(t *testing.T) { v := viper.New() v.Set("offline-queue-file-legacy", "/path/to/file") - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Equal(t, "/path/to/file", params.QueueFileLegacy) } @@ -1784,7 +1793,7 @@ func TestLoadOfflineParams_SyncMax(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", 42) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Equal(t, 42, params.SyncMax) } @@ -1793,7 +1802,7 @@ func TestLoadOfflineParams_SyncMax_Zero(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", "0") - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Zero(t, params.SyncMax) } @@ -1802,7 +1811,7 @@ func TestLoadOfflineParams_SyncMax_Default(t *testing.T) { v := viper.New() v.SetDefault("sync-offline-activity", 1000) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Equal(t, 1000, params.SyncMax) } @@ -1811,7 +1820,7 @@ func TestLoadOfflineParams_SyncMax_NegativeNumber(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", -1) - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Zero(t, params.SyncMax) } @@ -1820,7 +1829,7 @@ func TestLoadOfflineParams_SyncMax_NonIntegerValue(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", "invalid") - params := paramscmd.LoadOfflineParams(v) + params := cmdparams.LoadOfflineParams(v) assert.Zero(t, params.SyncMax) } @@ -1830,13 +1839,13 @@ func TestLoadAPIParams_APIKey(t *testing.T) { ViperAPIKey string ViperAPIKeyConfig string ViperAPIKeyConfigOld string - Expected paramscmd.API + Expected cmdparams.API }{ "api key flag takes precedence": { ViperAPIKey: "00000000-0000-4000-8000-000000000000", ViperAPIKeyConfig: "10000000-0000-4000-8000-000000000000", ViperAPIKeyConfigOld: "20000000-0000-4000-8000-000000000000", - Expected: paramscmd.API{ + Expected: cmdparams.API{ Key: "00000000-0000-4000-8000-000000000000", URL: "https://api.wakatime.com/api/v1", Hostname: "my-computer", @@ -1845,7 +1854,7 @@ func TestLoadAPIParams_APIKey(t *testing.T) { "api from config takes precedence": { ViperAPIKeyConfig: "00000000-0000-4000-8000-000000000000", ViperAPIKeyConfigOld: "10000000-0000-4000-8000-000000000000", - Expected: paramscmd.API{ + Expected: cmdparams.API{ Key: "00000000-0000-4000-8000-000000000000", URL: "https://api.wakatime.com/api/v1", Hostname: "my-computer", @@ -1853,7 +1862,7 @@ func TestLoadAPIParams_APIKey(t *testing.T) { }, "api key from config deprecated": { ViperAPIKeyConfigOld: "00000000-0000-4000-8000-000000000000", - Expected: paramscmd.API{ + Expected: cmdparams.API{ Key: "00000000-0000-4000-8000-000000000000", URL: "https://api.wakatime.com/api/v1", Hostname: "my-computer", @@ -1869,7 +1878,7 @@ func TestLoadAPIParams_APIKey(t *testing.T) { v.Set("settings.api_key", test.ViperAPIKeyConfig) v.Set("settings.apikey", test.ViperAPIKeyConfigOld) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, test.Expected, params) @@ -1881,7 +1890,7 @@ func TestLoadAPIParams_APIKeyUnset(t *testing.T) { v := viper.New() v.Set("key", "") - _, err := paramscmd.LoadAPIParams(v) + _, err := cmdparams.LoadAPIParams(v) require.Error(t, err) var errauth api.ErrAuth @@ -1903,7 +1912,7 @@ func TestLoadAPIParams_APIKeyInvalid(t *testing.T) { v := viper.New() v.Set("key", value) - _, err := paramscmd.LoadAPIParams(v) + _, err := cmdparams.LoadAPIParams(v) require.Error(t, err) var errauth api.ErrAuth @@ -1925,7 +1934,7 @@ func TestLoadAPIParams_ApiKey_SettingTakePrecedence(t *testing.T) { err = inipkg.ReadInConfig(v, configFile) require.NoError(t, err) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "00000000-0000-4000-8000-000000000000", params.Key) @@ -1942,7 +1951,7 @@ func TestLoadAPIParams_ApiKey_FromVault(t *testing.T) { err = inipkg.ReadInConfig(v, configFile) require.NoError(t, err) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "00000000-0000-4000-8000-000000000000", params.Key) @@ -1963,7 +1972,7 @@ func TestLoadParams_ApiKey_FromVault_Err_Darwin(t *testing.T) { err = inipkg.ReadInConfig(v, configFile) require.NoError(t, err) - _, err = paramscmd.LoadAPIParams(v) + _, err = cmdparams.LoadAPIParams(v) assert.EqualError(t, err, "failed to read api key from vault: exit status 1") } @@ -1976,7 +1985,7 @@ func TestLoadAPIParams_APIKeyFromEnv(t *testing.T) { defer os.Unsetenv("WAKATIME_API_KEY") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "00000000-0000-4000-8000-000000000000", params.Key) @@ -1990,7 +1999,7 @@ func TestLoadAPIParams_APIKeyFromEnvInvalid(t *testing.T) { defer os.Unsetenv("WAKATIME_API_KEY") - _, err = paramscmd.LoadAPIParams(v) + _, err = cmdparams.LoadAPIParams(v) require.Error(t, err) var errauth api.ErrAuth @@ -2009,7 +2018,7 @@ func TestLoadAPIParams_APIKeyFromEnv_ConfigTakesPrecedence(t *testing.T) { defer os.Unsetenv("WAKATIME_API_KEY") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "00000000-0000-4000-8000-000000000000", params.Key) @@ -2020,13 +2029,13 @@ func TestLoadAPIParams_APIUrl(t *testing.T) { ViperAPIUrl string ViperAPIUrlConfig string ViperAPIUrlOld string - Expected paramscmd.API + Expected cmdparams.API }{ "api url flag takes precedence": { ViperAPIUrl: "http://localhost:8080", ViperAPIUrlConfig: "http://localhost:8081", ViperAPIUrlOld: "http://localhost:8082", - Expected: paramscmd.API{ + Expected: cmdparams.API{ Key: "00000000-0000-4000-8000-000000000000", URL: "http://localhost:8080", Hostname: "my-computer", @@ -2035,7 +2044,7 @@ func TestLoadAPIParams_APIUrl(t *testing.T) { "api url deprecated flag takes precedence": { ViperAPIUrlConfig: "http://localhost:8081", ViperAPIUrlOld: "http://localhost:8082", - Expected: paramscmd.API{ + Expected: cmdparams.API{ Key: "00000000-0000-4000-8000-000000000000", URL: "http://localhost:8082", Hostname: "my-computer", @@ -2043,7 +2052,7 @@ func TestLoadAPIParams_APIUrl(t *testing.T) { }, "api url from config": { ViperAPIUrlConfig: "http://localhost:8081", - Expected: paramscmd.API{ + Expected: cmdparams.API{ Key: "00000000-0000-4000-8000-000000000000", URL: "http://localhost:8081", Hostname: "my-computer", @@ -2051,7 +2060,7 @@ func TestLoadAPIParams_APIUrl(t *testing.T) { }, "api url with legacy heartbeats endpoint": { ViperAPIUrl: "http://localhost:8080/api/v1/heartbeats.bulk", - Expected: paramscmd.API{ + Expected: cmdparams.API{ Key: "00000000-0000-4000-8000-000000000000", URL: "http://localhost:8080/api/v1", Hostname: "my-computer", @@ -2059,7 +2068,7 @@ func TestLoadAPIParams_APIUrl(t *testing.T) { }, "api url with trailing slash": { ViperAPIUrl: "http://localhost:8080/api/", - Expected: paramscmd.API{ + Expected: cmdparams.API{ Key: "00000000-0000-4000-8000-000000000000", URL: "http://localhost:8080/api", Hostname: "my-computer", @@ -2067,7 +2076,7 @@ func TestLoadAPIParams_APIUrl(t *testing.T) { }, "api url with wakapi style endpoint": { ViperAPIUrl: "http://localhost:8080/api/heartbeat", - Expected: paramscmd.API{ + Expected: cmdparams.API{ Key: "00000000-0000-4000-8000-000000000000", URL: "http://localhost:8080/api", Hostname: "my-computer", @@ -2084,7 +2093,7 @@ func TestLoadAPIParams_APIUrl(t *testing.T) { v.Set("apiurl", test.ViperAPIUrlOld) v.Set("settings.api_url", test.ViperAPIUrlConfig) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, test.Expected, params) @@ -2096,7 +2105,7 @@ func TestLoadAPIParams_Url_Default(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, api.BaseURL, params.URL) @@ -2107,7 +2116,7 @@ func TestLoadAPIParams_Url_InvalidFormat(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("api-url", "http://in valid") - _, err := paramscmd.LoadAPIParams(v) + _, err := cmdparams.LoadAPIParams(v) var errauth api.ErrAuth @@ -2123,13 +2132,13 @@ func TestLoadAPIParams_BackoffAt(t *testing.T) { v.Set("internal.backoff_at", "2021-08-30T18:50:42-03:00") v.Set("internal.backoff_retries", "3") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) backoffAt, err := time.Parse(inipkg.DateFormat, "2021-08-30T18:50:42-03:00") require.NoError(t, err) - assert.Equal(t, paramscmd.API{ + assert.Equal(t, cmdparams.API{ BackoffAt: backoffAt, BackoffRetries: 3, Key: "00000000-0000-4000-8000-000000000000", @@ -2145,10 +2154,10 @@ func TestLoadAPIParams_BackoffAtErr(t *testing.T) { v.Set("internal.backoff_at", "2021-08-30") v.Set("internal.backoff_retries", "2") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) - assert.Equal(t, paramscmd.API{ + assert.Equal(t, cmdparams.API{ BackoffAt: time.Time{}, BackoffRetries: 2, Key: "00000000-0000-4000-8000-000000000000", @@ -2163,7 +2172,7 @@ func TestLoadAPIParams_DisableSSLVerify_FlagTakesPrecedence(t *testing.T) { v.Set("no-ssl-verify", true) v.Set("settings.no_ssl_verify", false) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.True(t, params.DisableSSLVerify) @@ -2174,7 +2183,7 @@ func TestLoadAPIParams_DisableSSLVerify_FromConfig(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.no_ssl_verify", true) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.True(t, params.DisableSSLVerify) @@ -2184,7 +2193,7 @@ func TestLoadAPIParams_DisableSSLVerify_Default(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.False(t, params.DisableSSLVerify) @@ -2205,7 +2214,7 @@ func TestLoadAPIParams_ProxyURL(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("proxy", proxyURL) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, proxyURL, params.ProxyURL) @@ -2219,7 +2228,7 @@ func TestLoadAPIParams_ProxyURL_FlagTakesPrecedence(t *testing.T) { v.Set("proxy", "https://john:secret@example.org:8888") v.Set("settings.proxy", "ignored") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "https://john:secret@example.org:8888", params.ProxyURL) @@ -2235,7 +2244,7 @@ func TestLoadAPIParams_ProxyURL_UserDefinedTakesPrecedenceOverEnvironment(t *tes defer os.Unsetenv("HTTPS_PROXY") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "https://john:secret@example.org:8888", params.ProxyURL) @@ -2246,7 +2255,7 @@ func TestLoadAPIParams_ProxyURL_FromConfig(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.proxy", "https://john:secret@example.org:8888") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "https://john:secret@example.org:8888", params.ProxyURL) @@ -2261,7 +2270,7 @@ func TestLoadAPIParams_ProxyURL_FromEnvironment(t *testing.T) { defer os.Unsetenv("HTTPS_PROXY") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "https://john:secret@example.org:8888", params.ProxyURL) @@ -2276,7 +2285,7 @@ func TestLoadAPIParams_ProxyURL_NoProxyFromEnvironment(t *testing.T) { defer os.Unsetenv("NO_PROXY") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Empty(t, params.ProxyURL) @@ -2287,7 +2296,7 @@ func TestLoadAPIParams_ProxyURL_InvalidFormat(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("proxy", "ftp://john:secret@example.org:8888") - _, err := paramscmd.LoadAPIParams(v) + _, err := cmdparams.LoadAPIParams(v) var errauth api.ErrAuth @@ -2308,7 +2317,7 @@ func TestLoadAPIParams_SSLCertFilepath_FlagTakesPrecedence(t *testing.T) { home, err := os.UserHomeDir() require.NoError(t, err) - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, filepath.Join(home, "/path/to/cert.pem"), params.SSLCertFilepath) @@ -2319,7 +2328,7 @@ func TestLoadAPIParams_SSLCertFilepath_FromConfig(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.ssl_certs_file", "/path/to/cert.pem") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "/path/to/cert.pem", params.SSLCertFilepath) @@ -2336,7 +2345,7 @@ func TestLoadAPIParams_Hostname_FlagTakesPrecedence(t *testing.T) { defer os.Unsetenv("GITPOD_WORKSPACE_ID") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "my-machine", params.Hostname) @@ -2347,7 +2356,7 @@ func TestLoadAPIParams_Hostname_FromConfig(t *testing.T) { v.Set("key", "00000000-0000-4000-8000-000000000000") v.Set("settings.hostname", "my-machine") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "my-machine", params.Hostname) @@ -2363,7 +2372,7 @@ func TestLoadAPIParams_Hostname_ConfigTakesPrecedence(t *testing.T) { defer os.Unsetenv("GITPOD_WORKSPACE_ID") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "my-machine", params.Hostname) @@ -2378,7 +2387,7 @@ func TestLoadAPIParams_Hostname_FromGitpodEnv(t *testing.T) { defer os.Unsetenv("GITPOD_WORKSPACE_ID") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) assert.Equal(t, "Gitpod", params.Hostname) @@ -2388,7 +2397,7 @@ func TestLoadAPIParams_Hostname_DefaultFromSystem(t *testing.T) { v := viper.New() v.Set("key", "00000000-0000-4000-8000-000000000000") - params, err := paramscmd.LoadAPIParams(v) + params, err := cmdparams.LoadAPIParams(v) require.NoError(t, err) expected, err := os.Hostname() @@ -2402,7 +2411,7 @@ func TestLoadStatusBarParams_HideCategories_FlagTakesPrecedence(t *testing.T) { v.Set("today-hide-categories", true) v.Set("settings.status_bar_hide_categories", "ignored") - params, err := paramscmd.LoadStatusBarParams(v) + params, err := cmdparams.LoadStatusBarParams(v) require.NoError(t, err) assert.True(t, params.HideCategories) @@ -2412,7 +2421,7 @@ func TestLoadStatusBarParams_HideCategories_ConfigTakesPrecedence(t *testing.T) v := viper.New() v.Set("settings.status_bar_hide_categories", true) - params, err := paramscmd.LoadStatusBarParams(v) + params, err := cmdparams.LoadStatusBarParams(v) require.NoError(t, err) assert.True(t, params.HideCategories) @@ -2429,7 +2438,7 @@ func TestLoadStatusBarParams_Output(t *testing.T) { v := viper.New() v.Set("output", name) - params, err := paramscmd.LoadStatusBarParams(v) + params, err := cmdparams.LoadStatusBarParams(v) require.NoError(t, err) assert.Equal(t, out, params.Output) @@ -2440,7 +2449,7 @@ func TestLoadStatusBarParams_Output(t *testing.T) { func TestLoadStatusBarParams_Output_Default(t *testing.T) { v := viper.New() - params, err := paramscmd.LoadStatusBarParams(v) + params, err := cmdparams.LoadStatusBarParams(v) require.NoError(t, err) assert.Equal(t, output.TextOutput, params.Output) @@ -2450,7 +2459,7 @@ func TestLoadStatusBarParams_Output_Invalid(t *testing.T) { v := viper.New() v.Set("output", "invalid") - _, err := paramscmd.LoadStatusBarParams(v) + _, err := cmdparams.LoadStatusBarParams(v) require.Error(t, err) assert.Equal(t, "failed to parse output: invalid output \"invalid\"", err.Error()) @@ -2460,7 +2469,7 @@ func TestAPI_String(t *testing.T) { backoffat, err := time.Parse(inipkg.DateFormat, "2021-08-30T18:50:42-03:00") require.NoError(t, err) - api := paramscmd.API{ + api := cmdparams.API{ BackoffAt: backoffat, BackoffRetries: 5, DisableSSLVerify: true, @@ -2490,7 +2499,7 @@ func TestAPI_String(t *testing.T) { } func TestFilterParams_String(t *testing.T) { - filterparams := paramscmd.FilterParams{ + filterparams := cmdparams.FilterParams{ Exclude: []regex.Regex{regex.MustCompile("^/exclude")}, ExcludeUnknownProject: true, Include: []regex.Regex{regex.MustCompile("^/include")}, @@ -2506,7 +2515,7 @@ func TestFilterParams_String(t *testing.T) { } func TestHeartbeat_String(t *testing.T) { - heartbeat := paramscmd.Heartbeat{ + heartbeat := cmdparams.Heartbeat{ Category: heartbeat.CodingCategory, CursorPosition: heartbeat.PointerTo(15), Entity: "path/to/entity.go", @@ -2542,7 +2551,7 @@ func TestOffline_String(t *testing.T) { lastSentAt, err := time.Parse(inipkg.DateFormat, "2021-08-30T18:50:42-03:00") require.NoError(t, err) - offline := paramscmd.Offline{ + offline := cmdparams.Offline{ Disabled: true, LastSentAt: lastSentAt, PrintMax: 6, @@ -2562,7 +2571,7 @@ func TestOffline_String(t *testing.T) { } func TestProjectParams_String(t *testing.T) { - projectparams := paramscmd.ProjectParams{ + projectparams := cmdparams.ProjectParams{ Alternate: "alternate", BranchAlternate: "branch-alternate", MapPatterns: []project.MapPattern{{Name: "project-1", Regex: regex.MustCompile("^/regex")}}, @@ -2585,14 +2594,14 @@ func TestLoadHeartbeatParams_ProjectFromGitRemote(t *testing.T) { v.Set("git.project_from_git_remote", true) v.Set("entity", "/path/to/file") - params, err := paramscmd.LoadHeartbeatParams(v) + params, err := cmdparams.LoadHeartbeatParams(v) require.NoError(t, err) assert.True(t, params.Project.ProjectFromGitRemote) } func TestSanitizeParams_String(t *testing.T) { - sanitizeparams := paramscmd.SanitizeParams{ + sanitizeparams := cmdparams.SanitizeParams{ HideBranchNames: []regex.Regex{regex.MustCompile("^/hide")}, HideProjectFolder: true, HideFileNames: []regex.Regex{regex.MustCompile("^/hide")}, @@ -2609,7 +2618,7 @@ func TestSanitizeParams_String(t *testing.T) { } func TestStatusBar_String(t *testing.T) { - statusbar := paramscmd.StatusBar{ + statusbar := cmdparams.StatusBar{ HideCategories: true, Output: output.JSONOutput, }