• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

heathcliff26 / speedtest-exporter / 19512334048

19 Nov 2025 06:33PM UTC coverage: 76.978% (-0.4%) from 77.355%
19512334048

push

github

heathcliff26
cache: Fix wrong error message when cache file is empty

When the cache is empty, it means it has not been used yet.
This should not result in an error message.

Signed-off-by: Heathcliff <heathcliff@heathcliff.eu>

1 of 4 new or added lines in 1 file covered. (25.0%)

428 of 556 relevant lines covered (76.98%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.42
/pkg/cache/cache.go
1
package cache
2

3
import (
4
        "io"
5
        "log/slog"
6
        "os"
7
        "sync"
8
        "time"
9

10
        "github.com/heathcliff26/speedtest-exporter/pkg/speedtest"
11
)
12

13
type Cache struct {
14
        persist      bool
15
        path         string
16
        cacheTime    time.Duration
17
        cachedResult *speedtest.SpeedtestResult
18

19
        sync.RWMutex
20
}
21

22
// Create a new Cache instance and try to initialize it from disk if persist is true.
23
// If the path is not writable, the cache will not persist to disk.
24
// This function does not fail if it cannot read from disk, it will just log the error.
25
func NewCache(persist bool, path string, cacheTime time.Duration) *Cache {
1✔
26
        cache := &Cache{
1✔
27
                persist:   persist,
1✔
28
                path:      path,
1✔
29
                cacheTime: cacheTime,
1✔
30
        }
1✔
31

1✔
32
        if path == "" {
2✔
33
                cache.persist = false
1✔
34
        }
1✔
35
        if !cache.persist {
2✔
36
                return cache
1✔
37
        }
1✔
38

39
        // #nosec G302: Cache does not contain sensitive data, can be world readable
40
        f, err := os.OpenFile(cache.path, os.O_CREATE|os.O_RDWR, 0644)
1✔
41
        if err != nil {
2✔
42
                slog.Info("Failed to open cache file, will not persist cache to disk", slog.String("file", cache.path), slog.Any("error", err))
1✔
43
                cache.persist = false
1✔
44
                return cache
1✔
45
        }
1✔
46
        defer f.Close()
1✔
47

1✔
48
        data, err := io.ReadAll(f)
1✔
49
        if err != nil {
1✔
50
                slog.Info("Could not initialize cache from disk", slog.String("file", cache.path), slog.Any("error", err))
×
51
                return cache
×
52
        }
×
53

54
        if len(data) == 0 {
1✔
NEW
55
                slog.Info("Cache file is empty, starting with empty cache", slog.String("file", cache.path))
×
NEW
56
                return cache
×
NEW
57
        }
×
58

59
        cachedResult := &speedtest.SpeedtestResult{}
1✔
60
        err = cachedResult.UnmarshalJSON(data)
1✔
61
        if err != nil {
2✔
62
                slog.Info("Could not unmarshal cache data from disk", slog.String("file", cache.path), slog.Any("error", err))
1✔
63
        } else {
2✔
64
                slog.Info("Initialized cache from disk", slog.String("path", cache.path))
1✔
65
                cache.cachedResult = cachedResult
1✔
66
        }
1✔
67
        return cache
1✔
68
}
69

70
// Return the currently cached result and whether it is still valid.
71
// This method is safe to call even if the Cache instance is nil.
72
func (c *Cache) Read() (result *speedtest.SpeedtestResult, valid bool) {
1✔
73
        if c == nil {
2✔
74
                return nil, false
1✔
75
        }
1✔
76
        c.RLock()
1✔
77
        defer c.RUnlock()
1✔
78

1✔
79
        if c.cachedResult == nil {
2✔
80
                return nil, false
1✔
81
        }
1✔
82

83
        timestamp := c.cachedResult.TimestampAsTime()
1✔
84

1✔
85
        return c.cachedResult, timestamp.Add(c.cacheTime).After(time.Now())
1✔
86
}
87

88
// Save the given result to the cache.
89
// Attempt to persist to disk if enabled, but do not fail if it fails.
90
// This method is safe to call even if the Cache instance is nil.
91
func (c *Cache) Save(result *speedtest.SpeedtestResult) {
1✔
92
        if c == nil {
2✔
93
                return
1✔
94
        }
1✔
95
        c.Lock()
1✔
96
        defer c.Unlock()
1✔
97

1✔
98
        c.cachedResult = result
1✔
99
        if !c.persist {
2✔
100
                return
1✔
101
        }
1✔
102

103
        data, err := result.MarshalJSON()
1✔
104
        if err != nil {
1✔
105
                slog.Error("Could not marshal result to JSON", slog.Any("error", err))
×
106
                return
×
107
        }
×
108
        // #nosec G306: Cache does not contain sensitive data, can be world readable
109
        err = os.WriteFile(c.path, data, 0644)
1✔
110
        if err != nil {
1✔
111
                slog.Error("Could not write cache to disk", slog.String("file", c.path), slog.Any("error", err))
×
112
        }
×
113
}
114

115
// Return when the cache will expire
116
func (c *Cache) ExpiresAt() time.Time {
1✔
117
        if c == nil || c.cachedResult == nil {
2✔
118
                return time.Time{}
1✔
119
        }
1✔
120
        c.RLock()
1✔
121
        defer c.RUnlock()
1✔
122

1✔
123
        timestamp := c.cachedResult.TimestampAsTime()
1✔
124
        return timestamp.Add(c.cacheTime)
1✔
125
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc