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

heathcliff26 / speedtest-exporter / 19512255103

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

Pull #124

github

web-flow
Merge ffc932d34 into c87b23bcb
Pull Request #124: cache: Fix wrong error message when cache file is empty

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