Skip to content

Commit 02eefc9

Browse files
authored
Merge pull request #1154 from wakatime/bugfix/unhandled-default-params
Fix unhandled default params
2 parents 92addb5 + 15f70f2 commit 02eefc9

File tree

12 files changed

+279
-32
lines changed

12 files changed

+279
-32
lines changed

USAGE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ some/submodule/name = new project name
111111
| hostname | Optional name of local machine. By default, auto-detects the local machine’s hostname. | _string_ | |
112112
| log_file | Optional log file path. | _filepath_ | `~/.wakatime/wakatime.log` |
113113
| import_cfg | Optional path to another wakatime.cfg file to import. If set it will overwrite values loaded from $WAKATIME_HOME/.wakatime.cfg file. | _filepath_ | |
114-
| metrics | When set, collects metrics usage in '~/.wakatime/metrics' folder. For further reference visit <https://go.dev/blog/pprof>. | _bool_ | `false` |
114+
| metrics | When set, collects metrics usage in `~/.wakatime/metrics` folder. For further reference visit <https://go.dev/blog/pprof>. | _bool_ | `false` |
115115
| guess_language | When `true`, enables detecting programming language from file contents. | _bool_ | `false` |
116116

117117
### Project Map Section

cmd/heartbeat/heartbeat.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,8 @@ func Run(ctx context.Context, v *viper.Viper) (int, error) {
4343
if err != nil {
4444
var errauth api.ErrAuth
4545

46-
// api.ErrAuth represents an error when parsing api key.
47-
// Save heartbeats to offline db even when api key invalid.
48-
// It avoids losing heartbeats when api key is invalid.
46+
// api.ErrAuth represents an error when parsing api key or timeout.
47+
// Save heartbeats to offline db when api.ErrAuth as it avoids losing heartbeats.
4948
if errors.As(err, &errauth) {
5049
if err := offlinecmd.SaveHeartbeats(ctx, v, nil, queueFilepath); err != nil {
5150
logger.Errorf("failed to save heartbeats to offline queue: %s", err)

cmd/params/params.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/wakatime/wakatime-cli/pkg/heartbeat"
2323
"github.com/wakatime/wakatime-cli/pkg/ini"
2424
"github.com/wakatime/wakatime-cli/pkg/log"
25+
"github.com/wakatime/wakatime-cli/pkg/offline"
2526
"github.com/wakatime/wakatime-cli/pkg/output"
2627
"github.com/wakatime/wakatime-cli/pkg/project"
2728
"github.com/wakatime/wakatime-cli/pkg/regex"
@@ -296,10 +297,10 @@ func LoadAPIParams(ctx context.Context, v *viper.Viper) (API, error) {
296297
}
297298
}
298299

299-
var timeout time.Duration
300+
timeout := api.DefaultTimeoutSecs
300301

301302
if timeoutSecs, ok := vipertools.FirstNonEmptyInt(v, "timeout", "settings.timeout"); ok {
302-
timeout = time.Duration(timeoutSecs) * time.Second
303+
timeout = timeoutSecs
303304
}
304305

305306
return API{
@@ -312,7 +313,7 @@ func LoadAPIParams(ctx context.Context, v *viper.Viper) (API, error) {
312313
Plugin: vipertools.GetString(v, "plugin"),
313314
ProxyURL: proxyURL,
314315
SSLCertFilepath: sslCertFilepath,
315-
Timeout: timeout,
316+
Timeout: time.Duration(timeout) * time.Second,
316317
URL: apiURL.String(),
317318
}, nil
318319
}
@@ -658,17 +659,26 @@ func LoadOfflineParams(ctx context.Context, v *viper.Viper) Offline {
658659

659660
logger := log.Extract(ctx)
660661

661-
rateLimit, _ := vipertools.FirstNonEmptyInt(v, "heartbeat-rate-limit-seconds", "settings.heartbeat_rate_limit_seconds")
662-
if rateLimit < 0 {
663-
logger.Warnf("argument --heartbeat-rate-limit-seconds must be zero or a positive integer number, got %d", rateLimit)
662+
rateLimit := offline.RateLimitDefaultSeconds
664663

665-
rateLimit = 0
664+
if rateLimitSecs, ok := vipertools.FirstNonEmptyInt(v,
665+
"heartbeat-rate-limit-seconds",
666+
"settings.heartbeat_rate_limit_seconds"); ok {
667+
rateLimit = rateLimitSecs
668+
669+
if rateLimit < 0 {
670+
logger.Warnf(
671+
"argument --heartbeat-rate-limit-seconds must be zero or a positive integer number, got %d",
672+
rateLimit,
673+
)
674+
675+
rateLimit = 0
676+
}
666677
}
667678

668679
syncMax := v.GetInt("sync-offline-activity")
669680
if syncMax < 0 {
670681
logger.Warnf("argument --sync-offline-activity must be zero or a positive integer number, got %d", syncMax)
671-
672682
syncMax = 0
673683
}
674684

@@ -1100,7 +1110,7 @@ func (p Offline) String() string {
11001110
}
11011111

11021112
return fmt.Sprintf(
1103-
"disabled: %t, last sent at: '%s', print max: %d, num rate limit: %d, num sync max: %d",
1113+
"disabled: %t, last sent at: '%s', print max: %d, rate limit: %s, num sync max: %d",
11041114
p.Disabled,
11051115
lastSentAt,
11061116
p.PrintMax,

cmd/params/params_test.go

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ import (
2020
"github.com/wakatime/wakatime-cli/pkg/heartbeat"
2121
inipkg "github.com/wakatime/wakatime-cli/pkg/ini"
2222
"github.com/wakatime/wakatime-cli/pkg/log"
23+
"github.com/wakatime/wakatime-cli/pkg/offline"
2324
"github.com/wakatime/wakatime-cli/pkg/output"
2425
"github.com/wakatime/wakatime-cli/pkg/project"
2526
"github.com/wakatime/wakatime-cli/pkg/regex"
26-
"gopkg.in/ini.v1"
2727

2828
"github.com/spf13/viper"
2929
"github.com/stretchr/testify/assert"
3030
"github.com/stretchr/testify/require"
31+
"gopkg.in/ini.v1"
3132
)
3233

3334
func TestLoadHeartbeatParams_AlternateProject(t *testing.T) {
@@ -1781,6 +1782,50 @@ func TestLoadAPIParams_Timeout_FromConfig(t *testing.T) {
17811782
assert.Equal(t, 10*time.Second, params.Timeout)
17821783
}
17831784

1785+
func TestLoadAPIParams_Timeout_Zero(t *testing.T) {
1786+
v := viper.New()
1787+
v.Set("key", "00000000-0000-4000-8000-000000000000")
1788+
v.Set("timeout", 0)
1789+
1790+
params, err := cmdparams.LoadAPIParams(context.Background(), v)
1791+
require.NoError(t, err)
1792+
1793+
assert.Zero(t, params.Timeout)
1794+
}
1795+
1796+
func TestLoadAPIParams_Timeout_Default(t *testing.T) {
1797+
v := viper.New()
1798+
v.Set("key", "00000000-0000-4000-8000-000000000000")
1799+
v.SetDefault("timeout", api.DefaultTimeoutSecs)
1800+
1801+
params, err := cmdparams.LoadAPIParams(context.Background(), v)
1802+
require.NoError(t, err)
1803+
1804+
assert.Equal(t, time.Duration(api.DefaultTimeoutSecs)*time.Second, params.Timeout)
1805+
}
1806+
1807+
func TestLoadAPIParams_Timeout_NegativeNumber(t *testing.T) {
1808+
v := viper.New()
1809+
v.Set("key", "00000000-0000-4000-8000-000000000000")
1810+
v.Set("timeout", 0)
1811+
1812+
params, err := cmdparams.LoadAPIParams(context.Background(), v)
1813+
require.NoError(t, err)
1814+
1815+
assert.Zero(t, params.Timeout)
1816+
}
1817+
1818+
func TestLoadAPIParams_Timeout_NonIntegerValue(t *testing.T) {
1819+
v := viper.New()
1820+
v.Set("key", "00000000-0000-4000-8000-000000000000")
1821+
v.Set("timeout", "invalid")
1822+
1823+
params, err := cmdparams.LoadAPIParams(context.Background(), v)
1824+
require.NoError(t, err)
1825+
1826+
assert.Equal(t, time.Duration(api.DefaultTimeoutSecs)*time.Second, params.Timeout)
1827+
}
1828+
17841829
func TestLoadOfflineParams_Disabled_ConfigTakesPrecedence(t *testing.T) {
17851830
v := viper.New()
17861831
v.Set("disable-offline", false)
@@ -1832,7 +1877,7 @@ func TestLoadOfflineParams_RateLimit_FromConfig(t *testing.T) {
18321877

18331878
func TestLoadOfflineParams_RateLimit_Zero(t *testing.T) {
18341879
v := viper.New()
1835-
v.Set("heartbeat-rate-limit-seconds", "0")
1880+
v.Set("heartbeat-rate-limit-seconds", 0)
18361881

18371882
params := cmdparams.LoadOfflineParams(context.Background(), v)
18381883

@@ -1841,11 +1886,11 @@ func TestLoadOfflineParams_RateLimit_Zero(t *testing.T) {
18411886

18421887
func TestLoadOfflineParams_RateLimit_Default(t *testing.T) {
18431888
v := viper.New()
1844-
v.SetDefault("heartbeat-rate-limit-seconds", 20)
1889+
v.SetDefault("heartbeat-rate-limit-seconds", offline.RateLimitDefaultSeconds)
18451890

18461891
params := cmdparams.LoadOfflineParams(context.Background(), v)
18471892

1848-
assert.Equal(t, time.Duration(20)*time.Second, params.RateLimit)
1893+
assert.Equal(t, time.Duration(offline.RateLimitDefaultSeconds)*time.Second, params.RateLimit)
18491894
}
18501895

18511896
func TestLoadOfflineParams_RateLimit_NegativeNumber(t *testing.T) {
@@ -1863,7 +1908,7 @@ func TestLoadOfflineParams_RateLimit_NonIntegerValue(t *testing.T) {
18631908

18641909
params := cmdparams.LoadOfflineParams(context.Background(), v)
18651910

1866-
assert.Zero(t, params.RateLimit)
1911+
assert.Equal(t, time.Duration(offline.RateLimitDefaultSeconds)*time.Second, params.RateLimit)
18671912
}
18681913

18691914
func TestLoadOfflineParams_LastSentAt(t *testing.T) {
@@ -1889,7 +1934,7 @@ func TestLoadOfflineParams_LastSentAt_Err(t *testing.T) {
18891934

18901935
func TestLoadOfflineParams_LastSentAtFuture(t *testing.T) {
18911936
v := viper.New()
1892-
lastSentAt := time.Now().Add(time.Duration(2) * time.Hour)
1937+
lastSentAt := time.Now().Add(2 * time.Hour)
18931938
v.Set("internal.heartbeats_last_sent_at", lastSentAt.Format(inipkg.DateFormat))
18941939

18951940
params := cmdparams.LoadOfflineParams(context.Background(), v)
@@ -1984,6 +2029,7 @@ func TestLoadAPIParams_APIKey(t *testing.T) {
19842029
t.Run(name, func(t *testing.T) {
19852030
v := viper.New()
19862031
v.Set("hostname", "my-computer")
2032+
v.Set("timeout", 0)
19872033
v.Set("key", test.ViperAPIKey)
19882034
v.Set("settings.api_key", test.ViperAPIKeyConfig)
19892035
v.Set("settings.apikey", test.ViperAPIKeyConfigOld)
@@ -2193,6 +2239,7 @@ func TestLoadAPIParams_APIUrl(t *testing.T) {
21932239
t.Run(name, func(t *testing.T) {
21942240
v := viper.New()
21952241
v.Set("hostname", "my-computer")
2242+
v.Set("timeout", 0)
21962243
v.Set("key", "00000000-0000-4000-8000-000000000000")
21972244
v.Set("api-url", test.ViperAPIUrl)
21982245
v.Set("apiurl", test.ViperAPIUrlOld)
@@ -2232,6 +2279,7 @@ func TestLoadAPIParams_Url_InvalidFormat(t *testing.T) {
22322279
func TestLoadAPIParams_BackoffAt(t *testing.T) {
22332280
v := viper.New()
22342281
v.Set("hostname", "my-computer")
2282+
v.Set("timeout", 0)
22352283
v.Set("key", "00000000-0000-4000-8000-000000000000")
22362284
v.Set("internal.backoff_at", "2021-08-30T18:50:42-03:00")
22372285
v.Set("internal.backoff_retries", "3")
@@ -2255,19 +2303,15 @@ func TestLoadAPIParams_BackoffAtErr(t *testing.T) {
22552303
v := viper.New()
22562304
v.Set("hostname", "my-computer")
22572305
v.Set("key", "00000000-0000-4000-8000-000000000000")
2306+
v.Set("timeout", 0)
22582307
v.Set("internal.backoff_at", "2021-08-30")
22592308
v.Set("internal.backoff_retries", "2")
22602309

22612310
params, err := cmdparams.LoadAPIParams(context.Background(), v)
22622311
require.NoError(t, err)
22632312

2264-
assert.Equal(t, cmdparams.API{
2265-
BackoffAt: time.Time{},
2266-
BackoffRetries: 2,
2267-
Key: "00000000-0000-4000-8000-000000000000",
2268-
URL: "https://api.wakatime.com/api/v1",
2269-
Hostname: "my-computer",
2270-
}, params)
2313+
assert.Equal(t, 2, params.BackoffRetries)
2314+
assert.Empty(t, params.BackoffAt)
22712315
}
22722316

22732317
func TestLoadAPIParams_BackoffAtFuture(t *testing.T) {
@@ -2658,14 +2702,14 @@ func TestOffline_String(t *testing.T) {
26582702
Disabled: true,
26592703
LastSentAt: lastSentAt,
26602704
PrintMax: 6,
2661-
RateLimit: 15,
2705+
RateLimit: time.Duration(15) * time.Second,
26622706
SyncMax: 12,
26632707
}
26642708

26652709
assert.Equal(
26662710
t,
26672711
"disabled: true, last sent at: '2021-08-30T18:50:42-03:00', print max: 6,"+
2668-
" num rate limit: 15, num sync max: 12",
2712+
" rate limit: 15s, num sync max: 12",
26692713
offline.String(),
26702714
)
26712715
}

cmd/run_internal_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,10 @@ func TestParseConfigFiles(t *testing.T) {
415415
assert.Equal(t,
416416
"2006-01-02T15:04:05Z07:00",
417417
v.GetString("internal.backoff_at"))
418+
assert.Equal(t,
419+
"2025-01-05T22:21:51Z03:00",
420+
v.GetString("internal.heartbeats_last_sent_at"),
421+
)
418422
}
419423

420424
func TestParseConfigFiles_MissingAPIKey(t *testing.T) {

cmd/testdata/.wakatime-internal.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[internal]
22
backoff_retries = 1
33
backoff_at = 2006-01-02T15:04:05Z07:00
4+
heartbeats_last_sent_at = 2025-01-05T22:21:51Z03:00

main_test.go

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,98 @@ func TestSendHeartbeats_SecondaryApiKey(t *testing.T) {
249249
assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond)
250250
}
251251

252+
func TestSendHeartbeats_Timeout(t *testing.T) {
253+
apiURL, router, close := setupTestServer()
254+
defer close()
255+
256+
ctx := context.Background()
257+
258+
// to avoid race condition
259+
wg := sync.WaitGroup{}
260+
wg.Add(1)
261+
262+
var numCalls int
263+
264+
go func() {
265+
router.HandleFunc("/users/current/heartbeats.bulk", func(w http.ResponseWriter, _ *http.Request) {
266+
defer wg.Done()
267+
268+
numCalls++
269+
270+
time.Sleep(1010 * time.Millisecond) // simulate a slow server to force a timeout
271+
272+
// write response
273+
f, err := os.Open("testdata/api_heartbeats_response.json")
274+
require.NoError(t, err)
275+
276+
w.WriteHeader(http.StatusCreated)
277+
_, err = io.Copy(w, f)
278+
require.NoError(t, err)
279+
})
280+
}()
281+
282+
tmpDir := t.TempDir()
283+
284+
offlineQueueFile, err := os.CreateTemp(tmpDir, "")
285+
require.NoError(t, err)
286+
287+
defer offlineQueueFile.Close()
288+
289+
offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "")
290+
require.NoError(t, err)
291+
292+
// close the file to avoid "The process cannot access the file because it is being used by another process" error
293+
offlineQueueFileLegacy.Close()
294+
295+
tmpConfigFile, err := os.CreateTemp(tmpDir, "wakatime.cfg")
296+
require.NoError(t, err)
297+
298+
defer tmpConfigFile.Close()
299+
300+
tmpInternalConfigFile, err := os.CreateTemp(tmpDir, "wakatime-internal.cfg")
301+
require.NoError(t, err)
302+
303+
defer tmpInternalConfigFile.Close()
304+
305+
projectFolder, err := filepath.Abs(".")
306+
require.NoError(t, err)
307+
308+
out := runWakatimeCliExpectErr(
309+
t,
310+
exitcode.ErrGeneric,
311+
"--api-url", apiURL,
312+
"--key", "00000000-0000-4000-8000-000000000000",
313+
"--config", tmpConfigFile.Name(),
314+
"--internal-config", tmpInternalConfigFile.Name(),
315+
"--entity", "testdata/main.go",
316+
"--cursorpos", "12",
317+
"--offline-queue-file", offlineQueueFile.Name(),
318+
"--offline-queue-file-legacy", offlineQueueFileLegacy.Name(),
319+
"--line-additions", "123",
320+
"--line-deletions", "456",
321+
"--lineno", "42",
322+
"--lines-in-file", "100",
323+
"--time", "1585598059",
324+
"--hide-branch-names", ".*",
325+
"--project", "wakatime-cli",
326+
"--project-folder", projectFolder,
327+
"--timeout", "1", // very short timeout to force a timeout error
328+
"--write",
329+
"--verbose",
330+
)
331+
332+
assert.Empty(t, out)
333+
334+
offlineCount, err := offline.CountHeartbeats(ctx, offlineQueueFile.Name())
335+
require.NoError(t, err)
336+
337+
assert.Equal(t, 1, offlineCount)
338+
339+
wg.Wait()
340+
341+
assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond)
342+
}
343+
252344
func TestSendHeartbeats_ExtraHeartbeats(t *testing.T) {
253345
apiURL, router, close := setupTestServer()
254346
defer close()
@@ -455,7 +547,6 @@ func TestSendHeartbeats_ExtraHeartbeats_SyncLegacyOfflineActivity(t *testing.T)
455547
"--offline-queue-file-legacy", offlineQueueFileLegacy.Name(),
456548
"--lineno", "42",
457549
"--lines-in-file", "100",
458-
"--heartbeat-rate-limit-seconds", "0",
459550
"--time", "1585598059",
460551
"--hide-branch-names", ".*",
461552
"--write",

0 commit comments

Comments
 (0)