1
- from typing import Dict , List , TypeVar , Type , Tuple
1
+ """Expand kuttl tests from templates."""
2
+ import logging
3
+ import os
4
+ import re
5
+ import sys
2
6
from argparse import ArgumentParser , Namespace
3
7
from dataclasses import dataclass , field
4
8
from itertools import product , chain
5
- from jinja2 import Environment , FileSystemLoader
6
9
from os import walk , path , makedirs
7
10
from shutil import copy2
8
- from sys import exit
9
- from yaml import safe_load
10
- import logging
11
- import re
12
- import os
11
+ from typing import Dict , List , TypeVar , Type , Tuple
13
12
13
+ from jinja2 import Environment , FileSystemLoader
14
+ from yaml import safe_load
14
15
15
- __version_info__ = (0 , 0 , 5 )
16
+ __version_info__ = (0 , 0 , '6-dev' )
16
17
__version__ = "." .join (map (str , __version_info__ ))
17
18
18
19
@@ -22,7 +23,7 @@ def ansible_lookup(loc: str, what: str) -> str:
22
23
Simulates the Ansible `lookup()` function which is made available by Ansible Jinja templates.
23
24
Raises an exception if `loc` is not `env`.
24
25
"""
25
- if not loc is 'env' :
26
+ if loc != 'env' :
26
27
raise ValueError ("Can only lookup() in 'env'" )
27
28
result = ""
28
29
try :
@@ -34,6 +35,7 @@ def ansible_lookup(loc: str, what: str) -> str:
34
35
35
36
@dataclass
36
37
class TestCase :
38
+ """Test case definition."""
37
39
name : str
38
40
values : Dict [str , str ]
39
41
tid : str = field (init = False )
@@ -48,7 +50,8 @@ def __post_init__(self):
48
50
)
49
51
50
52
def expand (self , template_dir : str , target_dir : str ) -> None :
51
- logging .info (f"Expanding test case id [{ self .tid } ]" )
53
+ """Expand test case."""
54
+ logging .info ("Expanding test case id [%s]" , self .tid )
52
55
td_root = path .join (template_dir , self .name )
53
56
tc_root = path .join (target_dir , self .name , self .tid )
54
57
_mkdir_ignore_exists (tc_root )
@@ -63,115 +66,121 @@ def expand(self, template_dir: str, target_dir: str) -> None:
63
66
if sub_level == 5 :
64
67
# Sanity check
65
68
raise ValueError ("Maximum recursive level (5) reached." )
66
- for d in dirs :
69
+ for dir_name in dirs :
67
70
_mkdir_ignore_exists (
68
- path .join (tc_root , root [len (td_root ) + 1 :], d ))
69
- for f in files :
70
- source = path .join (root , f )
71
+ path .join (tc_root , root [len (td_root ) + 1 :], dir_name ))
72
+ for file_name in files :
73
+ source = path .join (root , file_name )
71
74
dest = ""
72
75
f_mode = os .stat (source ).st_mode
73
- if f .endswith (".j2" ):
74
- logging .debug (f"Render template { f } to { dest } " )
75
- dest = path .join (tc_root , root [len (td_root ) + 1 :], f [:- 3 :])
76
- self ._expand_template (f , dest , test_env )
76
+ if file_name .endswith (".j2" ):
77
+ logging .debug ("Render template %s to %s" , file_name , dest )
78
+ dest = path .join (
79
+ tc_root , root [len (td_root ) + 1 :], file_name [:- 3 :])
80
+ self ._expand_template (file_name , dest , test_env )
77
81
else :
78
- dest = path .join (tc_root , root [len (td_root ) + 1 :], f )
79
- logging .debug (f"Copy file { f } to { dest } " )
82
+ dest = path .join (
83
+ tc_root , root [len (td_root ) + 1 :], file_name )
84
+ logging .debug ("Copy file %s to %s" , file_name , dest )
80
85
copy2 (source , dest )
81
86
# restore file permissions (especially the executable bit is important here)
82
- logging .debug (f "Update file mode for { dest } " )
87
+ logging .debug ("Update file mode for %s" , dest )
83
88
os .chmod (dest , f_mode )
84
89
85
- def _expand_template (self , template : str , dest : str , env : Environment ) -> None :
86
- logging .debug (f "Expanding template { template } " )
87
- t = env .get_template (template )
90
+ def _expand_template (self , template_file : str , dest : str , env : Environment ) -> None :
91
+ logging .debug ("Expanding template %s" , template_file )
92
+ template = env .get_template (template_file )
88
93
with open (dest , encoding = "utf8" , mode = "w" ) as stream :
89
94
print (
90
- t .render ({"test_scenario" : {"values" : self .values }}), file = stream )
95
+ template .render ({"test_scenario" : {"values" : self .values }}), file = stream )
91
96
92
97
93
98
@dataclass
94
99
class TestDimension :
100
+ """Test dimension."""
95
101
name : str
96
102
values : List [str ]
97
103
98
104
def expand (self ) -> List [Tuple [str , str ]]:
105
+ """Return a list of tuples in the form of (<dimension>, <value>)"""
99
106
return [(self .name , v ) for v in self .values ]
100
107
101
108
102
109
@dataclass
103
110
class TestDefinition :
111
+ """Test case definition."""
104
112
name : str
105
113
dimensions : List [str ]
106
114
107
115
108
- TTestSuite = TypeVar ("TTestSuite" , bound = "TestSuite" )
116
+ TTestSuite = TypeVar ( # pylint: disable=invalid-name
117
+ "TTestSuite" , bound = "TestSuite" )
109
118
110
119
111
- @dataclass
120
+ @dataclass ( frozen = True )
112
121
class TestSuite :
113
- source : str
114
- test_cases : List [TestCase ]
122
+ """Test suite template."""
123
+ source : str = field ()
124
+ test_cases : List [TestCase ] = field (default_factory = list )
115
125
116
- def __init__ (self , source : str ) -> None :
117
- self .source = source
118
- with open (source , encoding = "utf8" ) as stream :
126
+ def __post_init__ (self ) -> None :
127
+ with open (self .source , encoding = "utf8" ) as stream :
119
128
tin = safe_load (stream )
120
129
dimensions = [
121
130
TestDimension (d ["name" ], d ["values" ]) for d in tin ["dimensions" ]
122
131
]
123
132
test_def = [
124
133
TestDefinition (t ["name" ], t ["dimensions" ]) for t in tin ["tests" ]
125
134
]
126
- self .test_cases = self ._build (dimensions , test_def )
135
+ self .test_cases . extend ( self ._build (dimensions , test_def ) )
127
136
128
137
@classmethod
129
138
def _build (
130
- cls : Type [TTestSuite ], dims : List [TestDimension ], tests : List [TestDefinition ]
139
+ cls : Type [TTestSuite ], dims : List [TestDimension ], tests : List [TestDefinition ]
131
140
) -> List [TestCase ]:
132
141
"""
133
142
>>> TestSuite._build([TestDimension(name='trino', values=['234', '235'])],
134
143
... [TestDefinition(name='smoke', dimensions=['trino'])])
135
- [TestCase(name='smoke', values={'trino': '234'}, name='smoke_trino-234'), TestCase(name='smoke', values={'trino': '235'}, name='smoke_trino-235')]
144
+ [TestCase(name='smoke', values={'trino': '234'}, name='smoke_trino-234'), \
145
+ TestCase(name='smoke', values={'trino': '235'}, name='smoke_trino-235')]
136
146
"""
137
147
result = []
138
- for t in tests :
139
- used_dims = [d for d in dims if d .name in t .dimensions ]
148
+ for test in tests :
149
+ used_dims = [d for d in dims if d .name in test .dimensions ]
140
150
expanded_test_dims : List [List [Tuple [str , str ]]] = [
141
151
d .expand () for d in used_dims
142
152
]
143
153
for tc_dim in product (* expanded_test_dims ):
144
- result .append (TestCase (name = t .name , values = dict (tc_dim )))
154
+ result .append (TestCase (name = test .name , values = dict (tc_dim )))
145
155
return result
146
156
147
157
def __repr__ (self ) -> str :
148
158
return f"TestSuite(source={ self .source } )"
149
159
150
160
def expand (self , template_dir : str , output_dir : str , kuttl_tests : str ) -> int :
151
- logging .info (f"Expanding test suite from { self .source } " )
152
- self ._sanity_checks (template_dir , output_dir , kuttl_tests )
161
+ """Expand test suite."""
162
+ logging .info ("Expanding test suite from %s" , self .source )
163
+ self ._sanity_checks (template_dir , kuttl_tests )
153
164
_mkdir_ignore_exists (output_dir )
154
165
self ._expand_kuttl_tests (output_dir , kuttl_tests )
155
166
for test_case in self .test_cases :
156
167
test_case .expand (template_dir , output_dir )
157
168
return 0
158
169
159
- def _sanity_checks (
160
- self , template_dir : str , output_dir : str , kuttl_tests : str
161
- ) -> None :
162
- for tc in self .test_cases :
163
- td_root = path .join (template_dir , tc .name )
170
+ def _sanity_checks (self , template_dir : str , kuttl_tests : str ) -> None :
171
+ for test_case in self .test_cases :
172
+ td_root = path .join (template_dir , test_case .name )
164
173
if not path .isdir (td_root ):
165
174
raise ValueError (
166
175
f"Test definition directory not found [{ td_root } ]" )
167
- if not ( path .isfile (kuttl_tests ) ):
176
+ if not path .isfile (kuttl_tests ):
168
177
raise ValueError (
169
178
f"Kuttl test config template not found [{ kuttl_tests } ]" )
170
179
171
180
def _expand_kuttl_tests (self , output_dir : str , kuttl_tests : str ) -> None :
172
181
env = Environment (loader = FileSystemLoader (path .dirname (kuttl_tests )))
173
182
kt_base_name = path .basename (kuttl_tests )
174
- t = env .get_template (kt_base_name )
183
+ template = env .get_template (kt_base_name )
175
184
kt_dest_name = re .sub (r"\.j(inja)?2$" , "" , kt_base_name )
176
185
# Compatibility warning: Assume output_dir ends with 'tests' and remove
177
186
# it from the destination file
@@ -181,20 +190,21 @@ def _expand_kuttl_tests(self, output_dir: str, kuttl_tests: str) -> None:
181
190
"tests" : [{"name" : tn } for tn in {tc .name for tc in self .test_cases }]
182
191
}
183
192
}
184
- logging .debug (f "kuttl vars { kuttl_vars } " )
193
+ logging .debug ("kuttl vars %s" , kuttl_vars )
185
194
with open (dest , encoding = "utf8" , mode = "w" ) as stream :
186
- print (t .render (kuttl_vars ), file = stream )
195
+ print (template .render (kuttl_vars ), file = stream )
187
196
188
197
189
198
def parse_cli_args () -> Namespace :
199
+ """Parse command line args."""
190
200
parser = ArgumentParser (
191
201
description = "Kuttl test expander for the Stackable Data Platform" )
192
202
parser .add_argument (
193
203
"-v" ,
194
204
"--version" ,
195
205
help = "Display application version" ,
196
206
action = 'version' ,
197
- version = '%(prog)s {version}' . format ( version = __version__ )
207
+ version = f '%(prog)s { __version__ } '
198
208
)
199
209
200
210
parser .add_argument (
@@ -247,27 +257,27 @@ def parse_cli_args() -> Namespace:
247
257
def _cli_log_level (cli_arg : str ) -> int :
248
258
if cli_arg == "debug" :
249
259
return logging .DEBUG
250
- else :
251
- return logging .INFO
260
+ return logging .INFO
252
261
253
262
254
- def _mkdir_ignore_exists (path : str ) -> None :
263
+ def _mkdir_ignore_exists (dir_name : str ) -> None :
255
264
try :
256
- logging .debug (f "Creating directory { path } " )
257
- makedirs (path )
265
+ logging .debug ("Creating directory %s" , dir_name )
266
+ makedirs (dir_name )
258
267
except FileExistsError :
259
268
pass
260
269
261
270
262
271
def main () -> int :
272
+ """Main"""
263
273
cli_args = parse_cli_args ()
264
274
logging .basicConfig (
265
275
encoding = "utf-8" , level = _cli_log_level (cli_args .log_level ))
266
- ts = TestSuite (cli_args .test_definition )
276
+ test_suite = TestSuite (cli_args .test_definition )
267
277
# Compatibility warning: add 'tests' to output_dir
268
278
output_dir = path .join (cli_args .output_dir , "tests" )
269
- return ts .expand (cli_args .template_dir , output_dir , cli_args .kuttl_test )
279
+ return test_suite .expand (cli_args .template_dir , output_dir , cli_args .kuttl_test )
270
280
271
281
272
282
if __name__ == "__main__" :
273
- exit (main ())
283
+ sys . exit (main ())
0 commit comments