11package engine
22
33import (
4+ "os"
45 "sync/atomic"
56 "testing"
67 "time"
78
9+ "github.com/h2non/bimg"
810 "github.com/stretchr/testify/assert"
911)
1012
@@ -636,7 +638,6 @@ func TestIdleCleanupManager_Integration_WithProcessing(t *testing.T) {
636638
637639 // Start the manager
638640 mgr .Start ()
639- defer mgr .Stop ()
640641
641642 // Simulate concurrent image processing
642643 processingCount := 20
@@ -660,6 +661,10 @@ func TestIdleCleanupManager_Integration_WithProcessing(t *testing.T) {
660661 <- processingDone
661662 }
662663
664+ // Give time for any goroutines blocked on RLock to complete
665+ // With RWMutex locking, if cleanup is running, some goroutines might be waiting
666+ time .Sleep (200 * time .Millisecond )
667+
663668 // Verify all processes ended
664669 assert .Equal (t , int32 (0 ), mgr .activeProcesses .Load (), "all processes should be done" )
665670
@@ -671,6 +676,8 @@ func TestIdleCleanupManager_Integration_WithProcessing(t *testing.T) {
671676
672677 // Manager should still be running
673678 assert .NotNil (t , mgr )
679+
680+ mgr .Stop ()
674681}
675682
676683// TestIdleCleanupManager_RaceCondition_Prevention verifies cleanup never runs during processing
@@ -754,3 +761,195 @@ func BenchmarkIdleCleanupManager_BeginEndProcessing_Parallel(b *testing.B) {
754761 }
755762 })
756763}
764+
765+ // TestSafeVipsCleanup_DoesNotCrash verifies that safe cleanup doesn't crash
766+ func TestSafeVipsCleanup_DoesNotCrash (t * testing.T ) {
767+ t .Parallel ()
768+
769+ // Should not panic or crash
770+ assert .NotPanics (t , func () {
771+ safeVipsCleanup ()
772+ }, "safeVipsCleanup should not panic" )
773+ }
774+
775+ // TestSafeVipsCleanup_AllowsSubsequentProcessing verifies image processing works after cleanup
776+ func TestSafeVipsCleanup_AllowsSubsequentProcessing (t * testing.T ) {
777+ t .Parallel ()
778+
779+ // Perform cleanup
780+ safeVipsCleanup ()
781+
782+ // Try to load and get metadata from an image immediately after cleanup
783+ // This should NOT crash (unlike VipsCacheDropAll which corrupts state)
784+ data , err := os .ReadFile ("testdata/small.jpg" )
785+ assert .NoError (t , err , "should read test image" )
786+ if err == nil {
787+ // Try to get metadata - this uses libvips and would crash if state is corrupted
788+ metadata , err := bimg .Metadata (data )
789+ assert .NoError (t , err , "should get metadata after cleanup without crashing" )
790+ assert .Greater (t , metadata .Size .Width , 0 , "should have valid image dimensions" )
791+ }
792+ }
793+
794+ // TestSafeVipsCleanup_MultipleCycles verifies multiple cleanup cycles work
795+ func TestSafeVipsCleanup_MultipleCycles (t * testing.T ) {
796+ t .Parallel ()
797+
798+ // Run cleanup multiple times
799+ for i := 0 ; i < 5 ; i ++ {
800+ assert .NotPanics (t , func () {
801+ safeVipsCleanup ()
802+ }, "cleanup cycle %d should not panic" , i + 1 )
803+
804+ // Brief pause between cycles
805+ time .Sleep (50 * time .Millisecond )
806+ }
807+ }
808+
809+ // TestSafeVipsCleanup_ReducesMemory verifies cleanup actually frees memory
810+ func TestSafeVipsCleanup_ReducesMemory (t * testing.T ) {
811+ t .Parallel ()
812+
813+ // Load test image data
814+ data , err := os .ReadFile ("testdata/small.jpg" )
815+ if err != nil {
816+ t .Skip ("test image not available" )
817+ }
818+
819+ // Process several images to build up cache
820+ for i := 0 ; i < 10 ; i ++ {
821+ img := bimg .NewImage (data )
822+ // Do some processing to populate cache
823+ img .Resize (100 , 100 )
824+ }
825+
826+ // Get memory before cleanup
827+ memBefore := bimg .VipsMemory ()
828+
829+ // Perform cleanup
830+ safeVipsCleanup ()
831+
832+ // Wait a bit for memory to be freed
833+ time .Sleep (200 * time .Millisecond )
834+
835+ // Get memory after cleanup
836+ memAfter := bimg .VipsMemory ()
837+
838+ // Memory should be reduced (or at least not increased)
839+ assert .LessOrEqual (t , memAfter .Memory , memBefore .Memory ,
840+ "memory after cleanup should be less than or equal to before" )
841+ }
842+
843+ // TestSafeVipsCleanup_ConcurrentWithProcessing verifies cleanup is safe during processing
844+ func TestSafeVipsCleanup_ConcurrentWithProcessing (t * testing.T ) {
845+ t .Parallel ()
846+
847+ // Load test image data
848+ data , err := os .ReadFile ("testdata/small.jpg" )
849+ if err != nil {
850+ t .Skip ("test image not available" )
851+ }
852+
853+ done := make (chan bool , 2 )
854+
855+ // Start image processing in background
856+ go func () {
857+ for i := 0 ; i < 5 ; i ++ {
858+ img := bimg .NewImage (data )
859+ img .Resize (100 , 100 )
860+ time .Sleep (50 * time .Millisecond )
861+ }
862+ done <- true
863+ }()
864+
865+ // Run cleanup concurrently
866+ go func () {
867+ for i := 0 ; i < 3 ; i ++ {
868+ time .Sleep (75 * time .Millisecond )
869+ safeVipsCleanup ()
870+ }
871+ done <- true
872+ }()
873+
874+ // Wait for both to complete
875+ <- done
876+ <- done
877+
878+ // Should complete without crashing
879+ }
880+
881+ // TestSafeVipsCleanup_RestoresCacheLimits verifies cache limits are restored
882+ func TestSafeVipsCleanup_RestoresCacheLimits (t * testing.T ) {
883+ t .Parallel ()
884+
885+ // Load test image data
886+ data , err := os .ReadFile ("testdata/small.jpg" )
887+ if err != nil {
888+ t .Skip ("test image not available" )
889+ }
890+
891+ // Perform cleanup
892+ safeVipsCleanup ()
893+
894+ // After cleanup, cache limits should be restored to reasonable values
895+ // We can verify this by processing multiple images successfully
896+ for i := 0 ; i < 10 ; i ++ {
897+ img := bimg .NewImage (data )
898+ // Should be able to process without crashes
899+ _ , err := img .Resize (100 , 100 )
900+ assert .NoError (t , err , "image %d should process correctly after cleanup" , i + 1 )
901+ }
902+ }
903+
904+ // TestSafeVipsCleanup_IntegrationWithIdleCleanupManager verifies cleanup works with manager
905+ func TestSafeVipsCleanup_IntegrationWithIdleCleanupManager (t * testing.T ) {
906+ t .Parallel ()
907+
908+ // Load test image data
909+ data , err := os .ReadFile ("testdata/small.jpg" )
910+ if err != nil {
911+ t .Skip ("test image not available" )
912+ }
913+
914+ // Create manager that uses real safe cleanup (not mock)
915+ mgr := NewIdleCleanupManager (true , 15 )
916+
917+ // Verify cleanupFunc is set to safeVipsCleanup
918+ assert .NotNil (t , mgr .cleanupFunc , "cleanupFunc should be set" )
919+
920+ // Call the cleanup function through the manager
921+ assert .NotPanics (t , func () {
922+ mgr .cleanupFunc ()
923+ }, "calling cleanupFunc should not panic" )
924+
925+ // Verify image processing still works after cleanup
926+ metadata , err := bimg .Metadata (data )
927+ assert .NoError (t , err , "should get metadata after manager cleanup" )
928+ assert .Greater (t , metadata .Size .Width , 0 , "should have valid image dimensions" )
929+ }
930+
931+ // TestSafeVipsCleanup_RestoresOriginalSettings verifies original cache settings are restored
932+ // Note: Not using t.Parallel() because this test checks global libvips cache settings
933+ func TestSafeVipsCleanup_RestoresOriginalSettings (t * testing.T ) {
934+ // Set known cache settings
935+ bimg .VipsCacheSetMax (200 )
936+ bimg .VipsCacheSetMaxMem (75 * 1024 * 1024 )
937+
938+ // Give settings time to apply
939+ time .Sleep (10 * time .Millisecond )
940+
941+ // Get settings before cleanup
942+ origMax := vipsCacheGetMax ()
943+ origMaxMem := vipsCacheGetMaxMem ()
944+
945+ // Verify our settings were applied
946+ assert .Equal (t , 200 , origMax , "should have set cache max to 200" )
947+ assert .Equal (t , 75 * 1024 * 1024 , origMaxMem , "should have set cache max mem to 75MB" )
948+
949+ // Perform cleanup (which temporarily sets to 1/1, then restores)
950+ safeVipsCleanup ()
951+
952+ // Verify settings are restored to exactly what they were before cleanup
953+ assert .Equal (t , origMax , vipsCacheGetMax (), "cache max should be restored to original" )
954+ assert .Equal (t , origMaxMem , vipsCacheGetMaxMem (), "cache max mem should be restored to original" )
955+ }
0 commit comments