@@ -6,10 +6,18 @@ package cpu
66import (
77 "context"
88 "testing"
9+ "time"
910
11+ "github.com/stretchr/testify/assert"
1012 "github.com/stretchr/testify/require"
1113)
1214
15+ // timesTotalSlack is the per-logical-CPU margin added on top of the measured
16+ // wall clock time when comparing cpu-total against the sum of the per-CPU stats.
17+ // It only has to absorb the counter granularity, since the time the two Times
18+ // calls are actually apart is measured rather than assumed.
19+ const timesTotalSlack = 0.1 // seconds per logical CPU
20+
1321// TestPerfInfoMatchesLogicalCount ensures perfInfo() returns one entry per logical
1422// CPU on the host. This guards against regressions like issue #887 where only the
1523// calling thread's processor group was reported on hosts with more than 64 CPUs.
@@ -22,3 +30,73 @@ func TestPerfInfoMatchesLogicalCount(t *testing.T) {
2230
2331 require .Len (t , info , n , "perfInfo must return one entry per logical CPU across all processor groups" )
2432}
33+
34+ // TestTimesTotalMatchesPerCPUSum ensures the cpu-total entry is the field-wise sum
35+ // of the per-CPU entries. Both are derived from the same perfInfo() counters, so
36+ // they must agree apart from the counters advancing between the two calls. It
37+ // guards the accumulation loop in TimesWithContext: a mis-mapped field, a dropped
38+ // counter or a skipped processor group shows up as a mismatch.
39+ //
40+ // Note this does not by itself detect a revert to a GetSystemTimes-based total:
41+ // that call reports the sum over the calling thread's processor group, which is
42+ // the whole machine on a single-group host, so User/System/Idle would still match.
43+ // Only the Irq assertion would catch it, and only once enough interrupt time has
44+ // accumulated, since GetSystemTimes cannot report it at all.
45+ func TestTimesTotalMatchesPerCPUSum (t * testing.T ) {
46+ start := time .Now ()
47+
48+ perCPU , err := Times (true )
49+ require .NoError (t , err )
50+ require .NotEmpty (t , perCPU )
51+
52+ total , err := Times (false )
53+ require .NoError (t , err )
54+ elapsed := time .Since (start )
55+ require .Len (t , total , 1 )
56+ require .Equal (t , "cpu-total" , total [0 ].CPU )
57+
58+ var want TimesStat
59+ for _ , c := range perCPU {
60+ want .User += c .User
61+ want .System += c .System
62+ want .Idle += c .Idle
63+ want .Irq += c .Irq
64+ }
65+
66+ // Each logical CPU can advance by at most the wall clock time between the two
67+ // calls, so scale the tolerance by what actually elapsed. Assuming the calls
68+ // are close together would make this flaky whenever the test goroutine is
69+ // descheduled on a busy CI host.
70+ slack := (elapsed .Seconds () + timesTotalSlack ) * float64 (len (perCPU ))
71+ assert .InDelta (t , want .User , total [0 ].User , slack )
72+ assert .InDelta (t , want .System , total [0 ].System , slack )
73+ assert .InDelta (t , want .Idle , total [0 ].Idle , slack )
74+ assert .InDelta (t , want .Irq , total [0 ].Irq , slack )
75+ }
76+
77+ // TestSystemTimes exercises the GetSystemTimes fallback directly. TimesWithContext
78+ // only reaches it when perfInfo() fails, which never happens on a healthy host, so
79+ // without this test the fallback would ship untested.
80+ func TestSystemTimes (t * testing.T ) {
81+ total , err := systemTimes ()
82+ require .NoError (t , err )
83+ require .Len (t , total , 1 )
84+
85+ assert .Equal (t , "cpu-total" , total [0 ].CPU )
86+ assert .Positive (t , total [0 ].User )
87+ assert .Positive (t , total [0 ].Idle )
88+ assert .GreaterOrEqual (t , total [0 ].System , 0.0 )
89+ // GetSystemTimes reports no interrupt time, so Irq stays unset here. This is
90+ // the one field that differs from the perfInfo-based total.
91+ assert .Zero (t , total [0 ].Irq )
92+
93+ // The counters are cumulative since boot, so a second call must not go
94+ // backwards. This catches a broken FILETIME recombination, which would
95+ // otherwise only show up as an implausible absolute value.
96+ again , err := systemTimes ()
97+ require .NoError (t , err )
98+ require .Len (t , again , 1 )
99+ assert .GreaterOrEqual (t , again [0 ].User , total [0 ].User )
100+ assert .GreaterOrEqual (t , again [0 ].System , total [0 ].System )
101+ assert .GreaterOrEqual (t , again [0 ].Idle , total [0 ].Idle )
102+ }
0 commit comments