@@ -44,12 +44,16 @@ class TestFile:
4444 source_dir : str
4545 file_name : str
4646
47- def build_destination (self ) -> str :
47+ def build_destination (self , skip_existing : bool = False ) -> Optional [ str ] :
4848 """Copies the file name to the destination directory.
49- Returns the destination file name.
49+ Returns the destination file name, or None if it was skipped because it already exists and
50+ `skip_existing` is set (used for common/shared files so a test's own file always wins).
5051 """
5152 source = path .join (self .source_dir , self .file_name )
5253 dest = path .join (self .dest_dir , self .file_name )
54+ if skip_existing and path .exists (dest ):
55+ logging .warning ("Skipping common file %s: test case already provides %s" , source , dest )
56+ return None
5357 logging .debug ("Copy file %s to %s" , source , dest )
5458 copy2 (source , dest )
5559 logging .debug ("Update file mode for %s" , dest )
@@ -68,13 +72,17 @@ class TestTemplate:
6872 env : Environment
6973 values : Dict [str , str ]
7074
71- def build_destination (self ) -> str :
75+ def build_destination (self , skip_existing : bool = False ) -> Optional [ str ] :
7276 """Renders the template to file in the destination directory. The resulting file has the same name as the
7377 template but with the .j2 or .jinja2 ending removed.
74- Returns the rendered file name.
78+ Returns the rendered file name, or None if it was skipped because it already exists and
79+ `skip_existing` is set (used for common/shared files so a test's own file always wins).
7580 """
7681 source = path .join (self .source_dir , self .file_name )
7782 dest = path .join (self .dest_dir , re .sub (PATTERN_EXTENSION_JINJA , "" , self .file_name ))
83+ if skip_existing and path .exists (dest ):
84+ logging .warning ("Skipping common template %s: test case already provides %s" , source , dest )
85+ return None
7886 logging .debug ("Render template %s to %s" , source , dest )
7987 template = self .env .get_template (self .file_name )
8088 with open (dest , encoding = "utf8" , mode = "w" ) as stream :
@@ -140,28 +148,50 @@ def tid(self) -> str:
140148 else :
141149 return name
142150
143- def expand (self , template_dir : str , target_dir : str , namespace : str ) -> None :
144- """Expand test case This will create the target folder, copy files and render render templates."""
151+ def expand (self , template_dir : str , target_dir : str , namespace : str , common_dir : Optional [str ] = None ) -> None :
152+ """Expand test case. This will create the target folder, copy files and render templates.
153+
154+ The test's own steps (from ``template_dir/<name>``) are rendered first. If ``common_dir`` is
155+ given and exists, its files are then rendered into the SAME test folder, so shared steps (e.g.
156+ a teardown) can live in a single place instead of being copied into every test. On a name
157+ collision the test's OWN file wins: a common file that would overwrite a file the test already
158+ provides is skipped (with a warning), so a shared file can never silently clobber a
159+ test-specific one.
160+ """
145161 logging .info ("Expanding test case id [%s]" , self .tid )
146- td_root = path .join (template_dir , self .name )
147162 tc_root = path .join (target_dir , self .name , self .tid )
148163 _mkdir_ignore_exists (tc_root )
149- test_env = Environment (loader = FileSystemLoader (path .join (template_dir , self .name )), trim_blocks = True )
150- test_env .globals ["lookup" ] = ansible_lookup
151- test_env .globals ["NAMESPACE" ] = determine_namespace (self .tid , namespace )
164+ namespace = determine_namespace (self .tid , namespace )
165+ self ._render_dir (path .join (template_dir , self .name ), tc_root , namespace )
166+ if common_dir :
167+ if path .isdir (common_dir ):
168+ logging .debug ("Rendering common steps from [%s] into [%s]" , common_dir , tc_root )
169+ self ._render_dir (common_dir , tc_root , namespace , skip_existing = True )
170+ else :
171+ logging .warning ("Common steps directory [%s] does not exist, skipping" , common_dir )
172+
173+ def _render_dir (self , source_root : str , tc_root : str , namespace : str , skip_existing : bool = False ) -> None :
174+ """Render/copy every file under ``source_root`` into ``tc_root``, preserving sub-directories.
175+
176+ When ``skip_existing`` is set, files whose destination already exists are skipped (used for
177+ common/shared files so a test's own file always takes precedence).
178+ """
179+ env = Environment (loader = FileSystemLoader (source_root ), trim_blocks = True )
180+ env .globals ["lookup" ] = ansible_lookup
181+ env .globals ["NAMESPACE" ] = namespace
152182 sub_level : int = 0
153- for root , dirs , files in walk (td_root ):
183+ for root , dirs , files in walk (source_root ):
154184 sub_level += 1
155185 if sub_level == 8 :
156186 # Sanity check
157187 raise ValueError ("Maximum recursive level (8) reached." )
158188 for dir_name in dirs :
159- _mkdir_ignore_exists (path .join (tc_root , root [len (td_root ) + 1 :], dir_name ))
189+ _mkdir_ignore_exists (path .join (tc_root , root [len (source_root ) + 1 :], dir_name ))
160190 for file_name in files :
161191 test_source = make_test_source_with_context (
162- file_name , root , path .join (tc_root , root [len (td_root ) + 1 :]), test_env , self .values
192+ file_name , root , path .join (tc_root , root [len (source_root ) + 1 :]), env , self .values
163193 )
164- test_source .build_destination ()
194+ test_source .build_destination (skip_existing = skip_existing )
165195
166196
167197@dataclass (frozen = True , eq = True )
@@ -336,6 +366,7 @@ def expand(
336366 output_dir : str ,
337367 kuttl_tests : str ,
338368 namespace : str ,
369+ common_dir : Optional [str ] = None ,
339370) -> int :
340371 """Expand test suite."""
341372 try :
@@ -344,7 +375,7 @@ def expand(
344375 _mkdir_ignore_exists (output_dir )
345376 _expand_kuttl_tests (ets .test_cases , output_dir , kuttl_tests )
346377 for test_case in ets .test_cases :
347- test_case .expand (template_dir , output_dir , namespace )
378+ test_case .expand (template_dir , output_dir , namespace , common_dir )
348379 except StopIteration as exc :
349380 raise ValueError (f"Cannot expand test suite [{ suite } ] because cannot find it in [{ kuttl_tests } ]" ) from exc
350381 return 0
0 commit comments