@@ -703,3 +703,132 @@ func TestFilterUnits_SliceIncludeThenUnitInclude_ExtendsCapture(t *testing.T) {
703703 t .Errorf ("extra.service should be included via unit-include rule" )
704704 }
705705}
706+
707+ // Test three-rule scenario: slice-include + unit-include (extra) + unit-exclude (from slice)
708+ func TestFilterUnits_SliceIncludeUnitIncludeUnitExclude (t * testing.T ) {
709+ sliceMap := map [string ]string {
710+ "app1.service" : "myapp.slice" ,
711+ "app2.service" : "myapp.slice" ,
712+ "excluded.service" : "myapp.slice" , // In the slice but will be excluded
713+ "extra.service" : "system.slice" , // NOT in myapp.slice, but will be included
714+ "other.service" : "system.slice" ,
715+ }
716+ mockConn := createMockConn (sliceMap )
717+
718+ // Three-rule scenario:
719+ // 1. slice-include=myapp.slice (capture units in that slice)
720+ // 2. unit-include=^extra\.service$ (extend capture to this service)
721+ // 3. unit-exclude=^excluded\.service$ (remove this specific unit from slice)
722+ filterRules := []FilterRule {
723+ {Type : FilterTypeSlice , Action : FilterActionInclude , Pattern : "myapp.slice" },
724+ {Type : FilterTypeUnit , Action : FilterActionInclude , Pattern : regexp .MustCompile (`^(?:extra\.service)$` )},
725+ {Type : FilterTypeUnit , Action : FilterActionExclude , Pattern : regexp .MustCompile (`^(?:excluded\.service)$` )},
726+ }
727+
728+ collector := createTestCollector (filterRules )
729+
730+ testUnitsScenario := []dbus.UnitStatus {
731+ {Name : "app1.service" , LoadState : "loaded" },
732+ {Name : "app2.service" , LoadState : "loaded" },
733+ {Name : "excluded.service" , LoadState : "loaded" },
734+ {Name : "extra.service" , LoadState : "loaded" },
735+ {Name : "other.service" , LoadState : "loaded" },
736+ }
737+
738+ filtered := collector .filterUnitsOrderAware (testUnitsScenario , mockConn )
739+
740+ // Expected:
741+ // - app1.service: in myapp.slice (rule 1) -> INCLUDED
742+ // - app2.service: in myapp.slice (rule 1) -> INCLUDED
743+ // - excluded.service: in myapp.slice (rule 1) then excluded (rule 3) -> EXCLUDED
744+ // - extra.service: not in myapp.slice, but matched by rule 2 -> INCLUDED
745+ // - other.service: not matched by any rule -> EXCLUDED (default for include-only)
746+ expectedUnits := map [string ]bool {
747+ "app1.service" : true ,
748+ "app2.service" : true ,
749+ "extra.service" : true ,
750+ }
751+
752+ if len (filtered ) != len (expectedUnits ) {
753+ names := make ([]string , 0 , len (filtered ))
754+ for _ , u := range filtered {
755+ names = append (names , u .Name )
756+ }
757+ t .Errorf ("Expected %d units, got %d: %v" , len (expectedUnits ), len (filtered ), names )
758+ }
759+
760+ for _ , unit := range filtered {
761+ if ! expectedUnits [unit .Name ] {
762+ t .Errorf ("Unexpected unit in result: %s" , unit .Name )
763+ }
764+ }
765+
766+ // Verify excluded.service is NOT in results
767+ for _ , unit := range filtered {
768+ if unit .Name == "excluded.service" {
769+ t .Errorf ("excluded.service should have been excluded by unit-exclude rule" )
770+ }
771+ }
772+ }
773+
774+ // Test: slice-include + unit-include (broad) + slice-exclude
775+ // Three slices with an app in each. Include slice1, include all apps, exclude slice3.
776+ // Expected: app1 (from slice1) and app2 (from unit-include) but NOT app3 (slice-exclude overrides)
777+ func TestFilterUnits_SliceIncludeUnitIncludeSliceExclude (t * testing.T ) {
778+ sliceMap := map [string ]string {
779+ "app1.service" : "slice1.slice" ,
780+ "app2.service" : "slice2.slice" ,
781+ "app3.service" : "slice3.slice" ,
782+ }
783+ mockConn := createMockConn (sliceMap )
784+
785+ // Rules:
786+ // 1. slice-include=slice1 (capture slice1)
787+ // 2. unit-include=^app.*\.service$ (extend to all app services)
788+ // 3. slice-exclude=slice3 (exclude slice3, overriding unit-include for app3)
789+ filterRules := []FilterRule {
790+ {Type : FilterTypeSlice , Action : FilterActionInclude , Pattern : "slice1" },
791+ {Type : FilterTypeUnit , Action : FilterActionInclude , Pattern : regexp .MustCompile (`^(?:app.*\.service)$` )},
792+ {Type : FilterTypeSlice , Action : FilterActionExclude , Pattern : "slice3" },
793+ }
794+
795+ collector := createTestCollector (filterRules )
796+
797+ testUnitsScenario := []dbus.UnitStatus {
798+ {Name : "app1.service" , LoadState : "loaded" },
799+ {Name : "app2.service" , LoadState : "loaded" },
800+ {Name : "app3.service" , LoadState : "loaded" },
801+ }
802+
803+ filtered := collector .filterUnitsOrderAware (testUnitsScenario , mockConn )
804+
805+ // Expected:
806+ // - app1.service: in slice1 (rule 1) -> INCLUDED
807+ // - app2.service: matches app.* (rule 2) -> INCLUDED
808+ // - app3.service: matches app.* (rule 2), but in slice3 (rule 3) -> EXCLUDED
809+ expectedUnits := map [string ]bool {
810+ "app1.service" : true ,
811+ "app2.service" : true ,
812+ }
813+
814+ if len (filtered ) != len (expectedUnits ) {
815+ names := make ([]string , 0 , len (filtered ))
816+ for _ , u := range filtered {
817+ names = append (names , u .Name )
818+ }
819+ t .Errorf ("Expected %d units, got %d: %v" , len (expectedUnits ), len (filtered ), names )
820+ }
821+
822+ for _ , unit := range filtered {
823+ if ! expectedUnits [unit .Name ] {
824+ t .Errorf ("Unexpected unit in result: %s" , unit .Name )
825+ }
826+ }
827+
828+ // Verify app3.service is NOT in results
829+ for _ , unit := range filtered {
830+ if unit .Name == "app3.service" {
831+ t .Errorf ("app3.service should have been excluded by slice-exclude rule" )
832+ }
833+ }
834+ }
0 commit comments