@@ -323,6 +323,10 @@ func TestDownRemoveImages(t *testing.T) {
323323 },
324324 }}, nil ).AnyTimes ()
325325
326+ api .EXPECT ().ImageList (gomock .Any (), client.ImageListOptions {
327+ Filters : projectFilter (strings .ToLower (testProject )).Add ("dangling" , "true" ),
328+ }).Return (client.ImageListResult {}, nil ).AnyTimes ()
329+
326330 imagesToBeInspected := map [string ]bool {
327331 "testproject-local-anonymous" : true ,
328332 "local-named-image" : true ,
@@ -413,6 +417,10 @@ func TestDownRemoveImages_NoLabel(t *testing.T) {
413417 Filters : projectFilter (strings .ToLower (testProject )).Add ("dangling" , "false" ),
414418 }).Return (client.ImageListResult {}, nil )
415419
420+ api .EXPECT ().ImageList (gomock .Any (), client.ImageListOptions {
421+ Filters : projectFilter (strings .ToLower (testProject )).Add ("dangling" , "true" ),
422+ }).Return (client.ImageListResult {}, nil )
423+
416424 api .EXPECT ().ImageInspect (gomock .Any (), "testproject-service1" , gomock .Any ()).Return (client.ImageInspectResult {}, nil )
417425 api .EXPECT ().ContainerStop (gomock .Any (), "123" , client.ContainerStopOptions {}).Return (client.ContainerStopResult {}, nil )
418426 api .EXPECT ().ContainerRemove (gomock .Any (), "123" , client.ContainerRemoveOptions {Force : true }).Return (client.ContainerRemoveResult {}, nil )
@@ -452,6 +460,66 @@ func prepareMocks(mockCtrl *gomock.Controller) (*mocks.MockAPIClient, *mocks.Moc
452460 return api , cli
453461}
454462
463+ // TestPruneDanglingProjectImages_ReportsOnceAndToleratesFailures guards that
464+ // dangling images are reported as a single grouped event regardless of
465+ // count, and that one failed removal doesn't stop the others or the caller.
466+ func TestPruneDanglingProjectImages_ReportsOnceAndToleratesFailures (t * testing.T ) {
467+ mockCtrl := gomock .NewController (t )
468+ defer mockCtrl .Finish ()
469+
470+ apiClient , cli := prepareMocks (mockCtrl )
471+ rec := & capturingEvents {}
472+ svcIface , err := NewComposeService (cli , WithEventProcessor (rec ))
473+ assert .NilError (t , err )
474+ svc := svcIface .(* composeService )
475+
476+ pruner := NewImagePruner (apiClient , & types.Project {Name : "prj" })
477+ apiClient .EXPECT ().ImageList (gomock .Any (), client.ImageListOptions {
478+ Filters : projectFilter ("prj" ).Add ("dangling" , "true" ),
479+ }).Return (client.ImageListResult {Items : []image.Summary {
480+ {ID : "sha256:aaa" },
481+ {ID : "sha256:bbb" },
482+ }}, nil )
483+ apiClient .EXPECT ().ImageRemove (gomock .Any (), "sha256:aaa" , client.ImageRemoveOptions {}).
484+ Return (client.ImageRemoveResult {}, nil )
485+ apiClient .EXPECT ().ImageRemove (gomock .Any (), "sha256:bbb" , client.ImageRemoveOptions {}).
486+ Return (client.ImageRemoveResult {}, errdefs .ErrNotFound .WithMessage ("already removed" ))
487+
488+ err = svc .pruneDanglingProjectImages (t .Context (), pruner )
489+ assert .NilError (t , err )
490+
491+ events := make ([]string , len (rec .resources ))
492+ for i , e := range rec .resources {
493+ events [i ] = e .ID + ": " + e .Text
494+ }
495+ assert .DeepEqual (t , events , []string {
496+ "Dangling images: Removing" ,
497+ "Dangling images: Removed" ,
498+ })
499+ }
500+
501+ // TestPruneDanglingProjectImages_NoneFound guards that no event is reported
502+ // when there's nothing to prune.
503+ func TestPruneDanglingProjectImages_NoneFound (t * testing.T ) {
504+ mockCtrl := gomock .NewController (t )
505+ defer mockCtrl .Finish ()
506+
507+ apiClient , cli := prepareMocks (mockCtrl )
508+ rec := & capturingEvents {}
509+ svcIface , err := NewComposeService (cli , WithEventProcessor (rec ))
510+ assert .NilError (t , err )
511+ svc := svcIface .(* composeService )
512+
513+ pruner := NewImagePruner (apiClient , & types.Project {Name : "prj" })
514+ apiClient .EXPECT ().ImageList (gomock .Any (), client.ImageListOptions {
515+ Filters : projectFilter ("prj" ).Add ("dangling" , "true" ),
516+ }).Return (client.ImageListResult {}, nil )
517+
518+ err = svc .pruneDanglingProjectImages (t .Context (), pruner )
519+ assert .NilError (t , err )
520+ assert .Equal (t , len (rec .resources ), 0 )
521+ }
522+
455523// TestDownRemovesRetainedPreStartHookContainers verifies that compose down finds and
456524// removes pre_start hook containers that were retained after a failed hook run.
457525// These containers lack ConfigHashLabel so the normal getContainers path never sees them.
0 commit comments