Skip to content

Commit 6a878b9

Browse files
committed
improve testing
1 parent f0f7b6a commit 6a878b9

9 files changed

+319
-135
lines changed

cli/clistat/cgroup_linux.go

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,83 +3,86 @@ package clistat
33
import (
44
"bufio"
55
"bytes"
6-
"os"
76
"strconv"
87
"time"
98

9+
"github.com/spf13/afero"
1010
"golang.org/x/xerrors"
1111
"tailscale.com/types/ptr"
1212
)
1313

14-
const ()
14+
const (
15+
cgroupV2CPUMax = "/sys/fs/cgroup/cpu.max"
16+
cgroupV2CPUStat = "/sys/fs/cgroup/cpu.stat"
17+
)
1518

1619
// CGroupCPU returns the CPU usage of the container cgroup.
1720
// On non-Linux platforms, this always returns nil.
1821
func (s *Statter) ContainerCPU() (*Result, error) {
1922
// Firstly, check if we are containerized.
20-
if ok, err := IsContainerized(); err != nil || !ok {
23+
if ok, err := IsContainerized(s.fs); err != nil || !ok {
2124
return nil, nil //nolint: nilnil
2225
}
2326

24-
used1, total, err := cgroupCPU()
27+
used1, total, err := s.cgroupCPU()
2528
if err != nil {
2629
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
2730
}
2831
<-time.After(s.sampleInterval)
2932

3033
// total is unlikely to change. Use the first value.
31-
used2, _, err := cgroupCPU()
34+
used2, _, err := s.cgroupCPU()
3235
if err != nil {
3336
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
3437
}
3538

3639
r := &Result{
3740
Unit: "cores",
3841
Used: (used2 - used1).Seconds(),
39-
Total: ptr.To(total.Seconds()), // close enough to the truth
42+
Total: ptr.To(total.Seconds() / s.sampleInterval.Seconds()), // close enough to the truth
4043
}
4144
return r, nil
4245
}
4346

44-
func cgroupCPU() (used, total time.Duration, err error) {
45-
if isCGroupV2() {
46-
return cGroupV2CPU()
47+
func (s *Statter) cgroupCPU() (used, total time.Duration, err error) {
48+
if s.isCGroupV2() {
49+
return s.cGroupV2CPU()
4750
}
4851

4952
// Fall back to CGroupv1
50-
return cGroupV1CPU()
53+
return s.cGroupV1CPU()
5154
}
5255

53-
func isCGroupV2() bool {
56+
func (s *Statter) isCGroupV2() bool {
5457
// Check for the presence of /sys/fs/cgroup/cpu.max
55-
_, err := os.Stat("/sys/fs/cgroup/cpu.max")
58+
_, err := s.fs.Stat("/sys/fs/cgroup/cpu.max")
5659
return err == nil
5760
}
5861

59-
func cGroupV2CPU() (used, total time.Duration, err error) {
60-
total, err = cGroupv2CPUTotal()
62+
func (s *Statter) cGroupV2CPU() (used, total time.Duration, err error) {
63+
total, err = s.cGroupv2CPUTotal()
6164
if err != nil {
6265
return 0, 0, xerrors.Errorf("get cgroup v2 CPU cores: %w", err)
6366
}
6467

65-
used, err = cGroupv2CPUUsed()
68+
used, err = s.cGroupv2CPUUsed()
6669
if err != nil {
6770
return 0, 0, xerrors.Errorf("get cgroup v2 CPU used: %w", err)
6871
}
6972

7073
return used, total, nil
7174
}
7275

73-
func cGroupv2CPUUsed() (used time.Duration, err error) {
76+
func (s *Statter) cGroupv2CPUUsed() (used time.Duration, err error) {
7477
var data []byte
75-
data, err = os.ReadFile("/sys/fs/cgroup/cpu.stat")
78+
data, err = afero.ReadFile(s.fs, cgroupV2CPUStat)
7679
if err != nil {
7780
return 0, xerrors.Errorf("read /sys/fs/cgroup/cpu.stat: %w", err)
7881
}
7982

80-
s := bufio.NewScanner(bytes.NewReader(data))
81-
for s.Scan() {
82-
line := s.Bytes()
83+
bs := bufio.NewScanner(bytes.NewReader(data))
84+
for bs.Scan() {
85+
line := bs.Bytes()
8386
if !bytes.HasPrefix(line, []byte("usage_usec ")) {
8487
continue
8588
}
@@ -100,10 +103,10 @@ func cGroupv2CPUUsed() (used time.Duration, err error) {
100103
return 0, xerrors.Errorf("did not find expected usage_usec in /sys/fs/cgroup/cpu.stat")
101104
}
102105

103-
func cGroupv2CPUTotal() (total time.Duration, err error) {
106+
func (s *Statter) cGroupv2CPUTotal() (total time.Duration, err error) {
104107
var data []byte
105108
var quotaUs int
106-
data, err = os.ReadFile("/sys/fs/cgroup/cpu.max")
109+
data, err = afero.ReadFile(s.fs, "/sys/fs/cgroup/cpu.max")
107110
if err != nil {
108111
return 0, xerrors.Errorf("read /sys/fs/cgroup/cpu.max: %w", err)
109112
}
@@ -120,7 +123,7 @@ func cGroupv2CPUTotal() (total time.Duration, err error) {
120123
}
121124

122125
if bytes.Equal(parts[0], []byte("max")) {
123-
quotaUs = nproc * int(time.Second.Microseconds())
126+
quotaUs = s.nproc * int(time.Second.Microseconds())
124127
} else {
125128
quotaUs, err = strconv.Atoi(string(parts[0]))
126129
if err != nil {
@@ -131,30 +134,30 @@ func cGroupv2CPUTotal() (total time.Duration, err error) {
131134
return time.Duration(quotaUs) * time.Microsecond, nil
132135
}
133136

134-
func cGroupV1CPU() (time.Duration, time.Duration, error) {
137+
func (*Statter) cGroupV1CPU() (time.Duration, time.Duration, error) {
135138
// TODO: implement
136139
return 0, 0, nil
137140
}
138141

139142
func (s *Statter) ContainerMemory() (*Result, error) {
140-
if ok, err := IsContainerized(); err != nil || !ok {
143+
if ok, err := IsContainerized(s.fs); err != nil || !ok {
141144
return nil, nil
142145
}
143146

144-
if isCGroupV2() {
145-
return cGroupv2Memory()
147+
if s.isCGroupV2() {
148+
return s.cGroupv2Memory()
146149
}
147150

148151
// Fall back to CGroupv1
149-
return cGroupv1Memory()
152+
return s.cGroupv1Memory()
150153
}
151154

152-
func cGroupv2Memory() (*Result, error) {
155+
func (*Statter) cGroupv2Memory() (*Result, error) {
153156
// TODO implement
154157
return nil, nil
155158
}
156159

157-
func cGroupv1Memory() (*Result, error) {
160+
func (*Statter) cGroupv1Memory() (*Result, error) {
158161
// TODO implement
159162
return nil, nil
160163
}

cli/clistat/container.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
package clistat
44

5+
import "github.com/spf13/afero"
6+
57
// IsContainerized returns whether the host is containerized.
68
// This is adapted from https://github.com/elastic/go-sysinfo/tree/main/providers/linux/container.go#L31
79
// with modifications to support Sysbox containers.
810
// On non-Linux platforms, it always returns false.
9-
func IsContainerized() (bool, error) {
11+
func IsContainerized(_ afero.Fs) (bool, error) {
1012
return false, nil
1113
}

cli/clistat/container_linux.go

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"sync"
88

9+
"github.com/spf13/afero"
910
"go.uber.org/atomic"
1011
"golang.org/x/xerrors"
1112
)
@@ -16,32 +17,37 @@ var (
1617
isContainerizedCacheOnce sync.Once
1718
)
1819

20+
const (
21+
procOneCgroup = "/proc/1/cgroup"
22+
procMounts = "/proc/mounts"
23+
)
24+
1925
// IsContainerized returns whether the host is containerized.
2026
// This is adapted from https://github.com/elastic/go-sysinfo/tree/main/providers/linux/container.go#L31
2127
// with modifications to support Sysbox containers.
2228
// On non-Linux platforms, it always returns false.
2329
// The result is only computed once and stored for subsequent calls.
24-
func IsContainerized() (ok bool, err error) {
30+
func IsContainerized(fs afero.Fs) (ok bool, err error) {
2531
isContainerizedCacheOnce.Do(func() {
26-
ok, err = isContainerizedOnce()
32+
ok, err = isContainerizedOnce(fs)
2733
isContainerizedCacheOK.Store(ok)
2834
isContainerizedCacheErr.Store(err)
2935
})
3036
return isContainerizedCacheOK.Load(), isContainerizedCacheErr.Load()
3137
}
3238

33-
func isContainerizedOnce() (bool, error) {
34-
data, err := os.ReadFile(procOneCgroup)
39+
func isContainerizedOnce(fs afero.Fs) (bool, error) {
40+
cgData, err := afero.ReadFile(fs, procOneCgroup)
3541
if err != nil {
36-
if os.IsNotExist(err) { // how?
37-
return false, nil
42+
if os.IsNotExist(err) {
43+
return false, nil // how?
3844
}
39-
return false, xerrors.Errorf("read process cgroups: %w", err)
45+
return false, xerrors.Errorf("read file %s: %w", procOneCgroup, err)
4046
}
4147

42-
s := bufio.NewScanner(bytes.NewReader(data))
43-
for s.Scan() {
44-
line := s.Bytes()
48+
scn := bufio.NewScanner(bytes.NewReader(cgData))
49+
for scn.Scan() {
50+
line := scn.Bytes()
4551
if bytes.Contains(line, []byte("docker")) ||
4652
bytes.Contains(line, []byte(".slice")) ||
4753
bytes.Contains(line, []byte("lxc")) ||
@@ -52,15 +58,18 @@ func isContainerizedOnce() (bool, error) {
5258

5359
// Last-ditch effort to detect Sysbox containers.
5460
// Check if we have anything mounted as type sysboxfs in /proc/mounts
55-
data, err = os.ReadFile("/proc/mounts")
61+
mountsData, err := afero.ReadFile(fs, procMounts)
5662
if err != nil {
57-
return false, xerrors.Errorf("read /proc/mounts: %w", err)
63+
if os.IsNotExist(err) {
64+
return false, nil // how??
65+
}
66+
return false, xerrors.Errorf("read file %s: %w", procMounts, err)
5867
}
5968

60-
s = bufio.NewScanner(bytes.NewReader(data))
61-
for s.Scan() {
62-
line := s.Bytes()
63-
if bytes.HasPrefix(line, []byte("sysboxfs")) {
69+
scn = bufio.NewScanner(bytes.NewReader(mountsData))
70+
for scn.Scan() {
71+
line := scn.Bytes()
72+
if bytes.Contains(line, []byte("sysboxfs")) {
6473
return true, nil
6574
}
6675
}

cli/clistat/disk.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
// Disk returns the disk usage of the given path.
1212
// If path is empty, it returns the usage of the root directory.
13-
func (s *Statter) Disk(path string) (*Result, error) {
13+
func (*Statter) Disk(path string) (*Result, error) {
1414
if path == "" {
1515
path = "/"
1616
}

cli/clistat/disk_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77

88
// Disk returns the disk usage of the given path.
99
// If path is empty, it defaults to C:\
10-
func (s *Statter) Disk(path string) (*Result, error) {
10+
func (*Statter) Disk(path string) (*Result, error) {
1111
if path == "" {
1212
path = `C:\`
1313
}

cli/clistat/stat.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@ import (
77
"time"
88

99
"github.com/elastic/go-sysinfo"
10+
"github.com/spf13/afero"
1011
"golang.org/x/xerrors"
1112
"tailscale.com/types/ptr"
1213

1314
sysinfotypes "github.com/elastic/go-sysinfo/types"
1415
)
1516

16-
const procOneCgroup = "/proc/1/cgroup"
17-
18-
var nproc = runtime.NumCPU()
19-
2017
// Result is a generic result type for a statistic.
2118
// Total is the total amount of the resource available.
2219
// It is nil if the resource is not a finite quantity.
@@ -50,7 +47,9 @@ func (r *Result) String() string {
5047
// It is a thin wrapper around the elastic/go-sysinfo library.
5148
type Statter struct {
5249
hi sysinfotypes.Host
50+
fs afero.Fs
5351
sampleInterval time.Duration
52+
nproc int
5453
}
5554

5655
type Option func(*Statter)
@@ -62,14 +61,23 @@ func WithSampleInterval(d time.Duration) Option {
6261
}
6362
}
6463

64+
// WithFS sets the fs for the statter.
65+
func WithFS(fs afero.Fs) Option {
66+
return func(s *Statter) {
67+
s.fs = fs
68+
}
69+
}
70+
6571
func New(opts ...Option) (*Statter, error) {
6672
hi, err := sysinfo.Host()
6773
if err != nil {
6874
return nil, xerrors.Errorf("get host info: %w", err)
6975
}
7076
s := &Statter{
7177
hi: hi,
78+
fs: afero.NewReadOnlyFs(afero.NewOsFs()),
7279
sampleInterval: 100 * time.Millisecond,
80+
nproc: runtime.NumCPU(),
7381
}
7482
for _, opt := range opts {
7583
opt(s)
@@ -87,7 +95,7 @@ func New(opts ...Option) (*Statter, error) {
8795
func (s *Statter) HostCPU() (*Result, error) {
8896
r := &Result{
8997
Unit: "cores",
90-
Total: ptr.To(float64(nproc)),
98+
Total: ptr.To(float64(s.nproc)),
9199
}
92100
c1, err := s.hi.CPUTime()
93101
if err != nil {
@@ -101,7 +109,7 @@ func (s *Statter) HostCPU() (*Result, error) {
101109
total := c2.Total() - c1.Total()
102110
idle := c2.Idle - c1.Idle
103111
used := total - idle
104-
scaleFactor := float64(nproc) / total.Seconds()
112+
scaleFactor := float64(s.nproc) / total.Seconds()
105113
r.Used = used.Seconds() * scaleFactor
106114
return r, nil
107115
}
@@ -129,7 +137,7 @@ func (s *Statter) Uptime() (*Result, error) {
129137
Total: nil, // Is time a finite quantity? For this purpose, no.
130138
}
131139

132-
if ok, err := IsContainerized(); err == nil && ok {
140+
if ok, err := IsContainerized(s.fs); err == nil && ok {
133141
procStat, err := sysinfo.Process(1)
134142
if err != nil {
135143
return nil, xerrors.Errorf("get pid 1 info: %w", err)

0 commit comments

Comments
 (0)