@@ -514,3 +514,252 @@ func BenchmarkResourceMonitor_IsResourceConstrained(b *testing.B) {
514514 _ = rm .IsResourceConstrained ()
515515 }
516516}
517+
518+ // TestClampPercent_NegativeValue tests clamping of negative percentage values
519+ func TestClampPercent_NegativeValue (t * testing.T ) {
520+ // Test negative value clamping directly
521+ result := clampPercent (- 10.0 )
522+ if result != 0.0 {
523+ t .Errorf ("negative value should be clamped to 0.0, got %f" , result )
524+ }
525+
526+ // Test value > 100 clamping directly
527+ result = clampPercent (150.0 )
528+ if result != 100.0 {
529+ t .Errorf ("value > 100 should be clamped to 100.0, got %f" , result )
530+ }
531+
532+ // Test normal value (should pass through)
533+ result = clampPercent (50.0 )
534+ if result != 50.0 {
535+ t .Errorf ("normal value should pass through, got %f" , result )
536+ }
537+
538+ // Test through memoryUsagePercent with custom reader
539+ rm := NewResourceMonitor (50 * time .Millisecond , 80.0 , 70.0 , CPUUsageModeHeuristic , func () (float64 , error ) {
540+ return - 10.0 , nil // Return negative value
541+ })
542+ defer rm .Close ()
543+
544+ stats := rm .collectStats ()
545+ // Should clamp negative to 0
546+ if stats .MemoryUsedPercent < 0 {
547+ t .Errorf ("memory percent should be clamped to >= 0, got %f" , stats .MemoryUsedPercent )
548+ }
549+ if stats .MemoryUsedPercent != 0.0 {
550+ t .Errorf ("negative value should be clamped to 0.0, got %f" , stats .MemoryUsedPercent )
551+ }
552+ }
553+
554+ // TestValidatePercent_NaNInf tests handling of NaN and Inf values
555+ func TestValidatePercent_NaNInf (t * testing.T ) {
556+ // Test NaN handling directly
557+ result := validatePercent (math .NaN ())
558+ if math .IsNaN (result ) {
559+ t .Error ("NaN should be converted to 0.0" )
560+ }
561+ if result != 0.0 {
562+ t .Errorf ("NaN should be converted to 0.0, got %f" , result )
563+ }
564+
565+ // Test positive Inf
566+ result = validatePercent (math .Inf (1 ))
567+ if math .IsInf (result , 0 ) {
568+ t .Error ("Inf should be converted to 0.0" )
569+ }
570+ if result != 0.0 {
571+ t .Errorf ("Inf should be converted to 0.0, got %f" , result )
572+ }
573+
574+ // Test negative Inf
575+ result = validatePercent (math .Inf (- 1 ))
576+ if math .IsInf (result , 0 ) {
577+ t .Error ("Negative Inf should be converted to 0.0" )
578+ }
579+ if result != 0.0 {
580+ t .Errorf ("Negative Inf should be converted to 0.0, got %f" , result )
581+ }
582+
583+ // Test through validateResourceStats
584+ stats := & ResourceStats {
585+ MemoryUsedPercent : 50.0 ,
586+ CPUUsagePercent : math .NaN (),
587+ GoroutineCount : 10 ,
588+ Timestamp : time .Now (),
589+ }
590+
591+ validateResourceStats (stats )
592+ if math .IsNaN (stats .CPUUsagePercent ) {
593+ t .Error ("CPU percent should not be NaN after validation" )
594+ }
595+ if stats .CPUUsagePercent != 0.0 {
596+ t .Errorf ("NaN should be converted to 0.0, got %f" , stats .CPUUsagePercent )
597+ }
598+ }
599+
600+ // TestValidateResourceStats_TimestampPaths tests the timestamp validation paths directly
601+ func TestValidateResourceStats_TimestampPaths (t * testing.T ) {
602+ t .Run ("zero timestamp refresh" , func (t * testing.T ) {
603+ stats := & ResourceStats {
604+ MemoryUsedPercent : 50.0 ,
605+ CPUUsagePercent : 40.0 ,
606+ GoroutineCount : 10 ,
607+ Timestamp : time.Time {}, // Zero timestamp
608+ }
609+
610+ validateResourceStats (stats )
611+ if stats .Timestamp .IsZero () {
612+ t .Error ("timestamp should not be zero after validation" )
613+ }
614+ if time .Since (stats .Timestamp ) > time .Second {
615+ t .Error ("timestamp should be recent after refresh" )
616+ }
617+ })
618+
619+ t .Run ("old timestamp refresh" , func (t * testing.T ) {
620+ oldTime := time .Now ().Add (- 2 * time .Minute ) // More than 1 minute ago
621+ stats := & ResourceStats {
622+ MemoryUsedPercent : 50.0 ,
623+ CPUUsagePercent : 40.0 ,
624+ GoroutineCount : 10 ,
625+ Timestamp : oldTime ,
626+ }
627+
628+ validateResourceStats (stats )
629+ if stats .Timestamp .Equal (oldTime ) {
630+ t .Error ("timestamp should be refreshed when older than 1 minute" )
631+ }
632+ if time .Since (stats .Timestamp ) > time .Second {
633+ t .Error ("timestamp should be recent after refresh" )
634+ }
635+ })
636+ }
637+
638+ // TestMemoryUsagePercent_CustomReaderErrorFallback tests the fallback when custom memory reader fails
639+ func TestMemoryUsagePercent_CustomReaderErrorFallback (t * testing.T ) {
640+ callCount := 0
641+ rm := NewResourceMonitor (50 * time .Millisecond , 80.0 , 70.0 , CPUUsageModeHeuristic , func () (float64 , error ) {
642+ callCount ++
643+ return 0 , fmt .Errorf ("custom reader error" )
644+ })
645+ defer rm .Close ()
646+
647+ // collectStats should call memoryUsagePercent which should try custom reader,
648+ // get error, and fall back to system memory
649+ stats := rm .collectStats ()
650+
651+ if callCount == 0 {
652+ t .Error ("custom memory reader should have been called" )
653+ }
654+
655+ // Should fall back to system memory, so value should be valid
656+ if stats .MemoryUsedPercent < 0 || stats .MemoryUsedPercent > 100 {
657+ t .Errorf ("memory percent should be valid after fallback, got %f" , stats .MemoryUsedPercent )
658+ }
659+ }
660+
661+ // TestInitSampler tests the initSampler method directly using reflection
662+ func TestInitSampler (t * testing.T ) {
663+ t .Run ("heuristic mode" , func (t * testing.T ) {
664+ rm := & ResourceMonitor {
665+ sampleInterval : 50 * time .Millisecond ,
666+ memoryThreshold : 80.0 ,
667+ cpuThreshold : 70.0 ,
668+ cpuMode : CPUUsageModeHeuristic ,
669+ done : make (chan struct {}),
670+ }
671+
672+ rm .initSampler ()
673+
674+ if rm .sampler == nil {
675+ t .Fatal ("sampler should not be nil" )
676+ }
677+ if rm .cpuMode != CPUUsageModeHeuristic {
678+ t .Errorf ("cpuMode should remain Heuristic, got %v" , rm .cpuMode )
679+ }
680+
681+ // Verify it's a heuristic sampler by checking behavior
682+ percent := rm .sampler .Sample (100 * time .Millisecond )
683+ if percent < 0.0 || percent > 100.0 {
684+ t .Errorf ("CPU percent should be between 0 and 100, got %v" , percent )
685+ }
686+ })
687+
688+ t .Run ("measured mode success" , func (t * testing.T ) {
689+ rm := & ResourceMonitor {
690+ sampleInterval : 50 * time .Millisecond ,
691+ memoryThreshold : 80.0 ,
692+ cpuThreshold : 70.0 ,
693+ cpuMode : CPUUsageModeMeasured ,
694+ done : make (chan struct {}),
695+ }
696+
697+ rm .initSampler ()
698+
699+ if rm .sampler == nil {
700+ t .Fatal ("sampler should not be nil" )
701+ }
702+
703+ // On darwin, NewProcessSampler should succeed, so cpuMode should be Measured
704+ // (or Heuristic if it fell back, but that's unlikely on darwin)
705+ if rm .cpuMode != CPUUsageModeMeasured && rm .cpuMode != CPUUsageModeHeuristic {
706+ t .Errorf ("cpuMode should be Measured or Heuristic (fallback), got %v" , rm .cpuMode )
707+ }
708+
709+ // Should be able to sample
710+ percent := rm .sampler .Sample (100 * time .Millisecond )
711+ if percent < 0.0 || percent > 100.0 {
712+ t .Errorf ("CPU percent should be between 0 and 100, got %v" , percent )
713+ }
714+
715+ // Verify stats were collected
716+ stats := rm .stats .Load ()
717+ if stats == nil {
718+ t .Error ("stats should be initialized after initSampler" )
719+ }
720+ })
721+
722+ t .Run ("measured mode fallback structure" , func (t * testing.T ) {
723+ // This test verifies that the fallback code path exists in initSampler.
724+ // The actual fallback (lines 105-108 in resource_monitor.go) is tested on
725+ // platforms where NewProcessSampler naturally fails (e.g., unsupported platforms
726+ // via cpu_fallback.go build tag: !linux && !darwin && !windows).
727+ //
728+ // On darwin, NewProcessSampler typically succeeds, so we verify:
729+ // 1. The code structure supports fallback (cpuMode can change from Measured to Heuristic)
730+ // 2. The sampler is always initialized (even if fallback occurs)
731+ // 3. Stats are collected after initialization
732+
733+ rm := & ResourceMonitor {
734+ sampleInterval : 50 * time .Millisecond ,
735+ memoryThreshold : 80.0 ,
736+ cpuThreshold : 70.0 ,
737+ cpuMode : CPUUsageModeMeasured ,
738+ done : make (chan struct {}),
739+ }
740+
741+ initialMode := rm .cpuMode
742+ rm .initSampler ()
743+
744+ // Sampler should always be initialized
745+ if rm .sampler == nil {
746+ t .Fatal ("sampler should not be nil, even if fallback occurs" )
747+ }
748+
749+ // Mode should either remain Measured (success) or change to Heuristic (fallback)
750+ if rm .cpuMode != CPUUsageModeMeasured && rm .cpuMode != CPUUsageModeHeuristic {
751+ t .Errorf ("cpuMode should be Measured or Heuristic after initSampler, got %v" , rm .cpuMode )
752+ }
753+
754+ // If mode changed, it means fallback occurred
755+ if rm .cpuMode != initialMode {
756+ t .Logf ("Fallback occurred: cpuMode changed from %v to %v" , initialMode , rm .cpuMode )
757+ }
758+
759+ // Stats should be initialized
760+ stats := rm .stats .Load ()
761+ if stats == nil {
762+ t .Error ("stats should be initialized after initSampler" )
763+ }
764+ })
765+ }
0 commit comments