1717
1818logger = logging .getLogger (__name__ )
1919
20+ # Mapping of feature tags to container names
21+ TAG_CONTAINER_MAP : dict [str , str ] = {
22+ "needs-postgres" : "postgres" ,
23+ "needs-kafka" : "kafka" ,
24+ "needs-elasticsearch" : "elasticsearch" ,
25+ "needs-minio" : "minio" ,
26+ "needs-keycloak" : "keycloak" ,
27+ "needs-redis" : "redis" ,
28+ }
29+
2030
2131class ContainerManager :
2232 """Registry for managing all test containers."""
2333
2434 _containers = {}
2535 _container_instances = {}
2636 _started = False
37+ _started_containers : set [str ] = set ()
2738
2839 @classmethod
2940 def register (cls , name : str ):
@@ -36,18 +47,29 @@ def decorator(container_class):
3647
3748 @classmethod
3849 def get_container (cls , name : str , ** kwargs ):
39- """Get a container instance by name."""
50+ """Get a container instance by name.
51+
52+ If the container is not started, it will be started lazily.
53+ """
4054 if name not in cls ._containers :
4155 raise KeyError (f"Container '{ name } ' not found. Available: { list (cls ._containers .keys ())} " )
4256
4357 # Return stored instance if available (Singleton pattern ensures same instance)
4458 if name in cls ._container_instances :
45- return cls ._container_instances [name ]
59+ instance = cls ._container_instances [name ]
60+ # Start container if not already running (lazy startup)
61+ if name not in cls ._started_containers :
62+ instance .start ()
63+ cls ._started_containers .add (name )
64+ return instance
4665
4766 # Create new instance if not stored yet
4867 container_class = cls ._containers [name ]
4968 instance = container_class (** kwargs )
5069 cls ._container_instances [name ] = instance
70+ # Start container lazily
71+ instance .start ()
72+ cls ._started_containers .add (name )
5173 return instance
5274
5375 @classmethod
@@ -61,21 +83,72 @@ def start_all(cls):
6183 container = container_class ()
6284 cls ._container_instances [name ] = container
6385 container .start ()
86+ cls ._started_containers .add (name )
6487
6588 cls ._started = True
6689 logger .info ("All test containers started" )
6790
91+ @classmethod
92+ def start_containers (cls , container_names : list [str ]):
93+ """Start specific containers by name.
94+
95+ Args:
96+ container_names: List of container names to start
97+ """
98+ for name in container_names :
99+ if name not in cls ._containers :
100+ logger .warning (f"Container '{ name } ' not found. Available: { list (cls ._containers .keys ())} " )
101+ continue
102+
103+ if name in cls ._started_containers :
104+ logger .debug (f"Container '{ name } ' already started, skipping" )
105+ continue
106+
107+ logger .info (f"Starting { name } container..." )
108+ # get_container will start the container and add it to _started_containers
109+ cls .get_container (name )
110+
111+ logger .info (f"Started containers: { sorted (cls ._started_containers )} " )
112+
113+ @classmethod
114+ def extract_containers_from_tags (cls , tags : list [str ]) -> set [str ]:
115+ """Extract container names from feature/scenario tags.
116+
117+ Args:
118+ tags: List of tag strings (e.g., ["needs-postgres", "needs-kafka"])
119+
120+ Returns:
121+ Set of container names that should be started
122+ """
123+ containers : set [str ] = set ()
124+ for tag in tags :
125+ # Remove @ prefix if present
126+ tag_name = tag .lstrip ("@" )
127+ if tag_name in TAG_CONTAINER_MAP :
128+ container_name = TAG_CONTAINER_MAP [tag_name ]
129+ containers .add (container_name )
130+ logger .debug (f"Tag '{ tag } ' maps to container '{ container_name } '" )
131+ else :
132+ # Only log warning for tags that look like container tags but aren't mapped
133+ if tag_name .startswith ("needs-" ):
134+ logger .warning (f"Unknown container tag '{ tag } '. Available tags: { list (TAG_CONTAINER_MAP .keys ())} " )
135+
136+ return containers
137+
68138 @classmethod
69139 def stop_all (cls ):
70- """Stop all registered containers."""
71- if not cls ._started :
140+ """Stop all started containers."""
141+ if not cls ._started_containers :
72142 return
73143
74- for name , instance in cls ._container_instances .items ():
75- logger .info (f"Stopping { name } container..." )
76- instance .stop ()
144+ for name in list (cls ._started_containers ):
145+ if name in cls ._container_instances :
146+ logger .info (f"Stopping { name } container..." )
147+ instance = cls ._container_instances [name ]
148+ instance .stop ()
77149
78150 cls ._container_instances .clear ()
151+ cls ._started_containers .clear ()
79152 cls ._started = False
80153 logger .info ("All test containers stopped" )
81154
@@ -85,6 +158,7 @@ def reset(cls):
85158 cls .stop_all ()
86159 cls ._containers .clear ()
87160 cls ._container_instances .clear ()
161+ cls ._started_containers .clear ()
88162 cls ._started = False
89163
90164 @classmethod
0 commit comments