blob: 6eda45f523e9ca2b81f57d0f2825d997386cf27f [file] [log] [blame]
Milosz Wasilewski68279b22018-02-21 12:41:02 +00001import collections
Milosz Wasilewskif9e70c02020-09-25 13:19:44 +01002import datetime
Milosz Wasilewski9c00f732020-08-04 10:29:35 +01003import logging
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +01004import os
Milosz Wasilewski923ed1b2020-08-10 16:15:46 +01005import pdfkit
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +01006import subprocess
7import yaml
8from argparse import ArgumentParser
Milosz Wasilewskib5045412016-12-07 11:27:10 +00009from csv import DictWriter
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010010from jinja2 import Environment, FileSystemLoader
11
12
Milosz Wasilewski9c00f732020-08-04 10:29:35 +010013logger = logging.getLogger()
14
15
Milosz Wasilewski68279b22018-02-21 12:41:02 +000016class PrependOrderedDict(collections.OrderedDict):
17
18 def prepend(self, key, value, dict_setitem=dict.__setitem__):
Milosz Wasilewski9c00f732020-08-04 10:29:35 +010019 self[key] = value
20 self.move_to_end(key, last=False)
Milosz Wasilewski68279b22018-02-21 12:41:02 +000021
22
Milosz Wasilewskie6f16322020-08-04 10:30:05 +010023def render(obj, template="testplan.html", templates_dir=None, name=None):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010024 if name is None:
25 name = template
Milosz Wasilewskie6f16322020-08-04 10:30:05 +010026 if templates_dir is None:
27 templates_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
Milosz Wasilewskif761ab92018-02-13 11:21:18 +000028 _env = Environment(loader=FileSystemLoader(templates_dir))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010029 _template = _env.get_template(template)
Milosz Wasilewskif9e70c02020-09-25 13:19:44 +010030 obj['metadata']['now'] = datetime.date.today().strftime("%B %d, %Y")
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010031 _obj = _template.render(obj=obj)
32 with open("{}".format(name), "wb") as _file:
33 _file.write(_obj.encode('utf-8'))
34
Nicolas Dechesnecd2e0f12020-09-29 11:20:25 +020035 # if the template is a .textile template, let's convert the output file to html
36 if os.path.splitext(name)[1] == '.textile':
37 import textile
38 with open("{}".format(name), "r") as _file:
39 data = _file.read()
40 with open("{}{}".format(os.path.splitext(name)[0], ".html"), "w") as _file:
41 _file.write(textile.textile(data))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010042
43# get list of repositories and cache them
44def repository_list(testplan):
45 repositories = set()
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +000046 tp_version = testplan['metadata']['format']
47 if tp_version == "Linaro Test Plan v2":
48 if 'manual' in testplan['tests'].keys() and testplan['tests']['manual'] is not None:
49 for test in testplan['tests']['manual']:
50 repositories.add(test['repository'])
51
52 if 'automated' in testplan['tests'].keys() and testplan['tests']['automated'] is not None:
53 for test in testplan['tests']['automated']:
54 repositories.add(test['repository'])
55 if tp_version == "Linaro Test Plan v1":
56 for req in testplan['requirements']:
57 if 'tests' in req.keys() and req['tests'] is not None:
58 if 'manual' in req['tests'].keys() and req['tests']['manual'] is not None:
59 for test in req['tests']['manual']:
60 repositories.add(test['repository'])
61 if 'automated' in req['tests'].keys() and req['tests']['automated'] is not None:
62 for test in req['tests']['automated']:
63 repositories.add(test['repository'])
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010064 return repositories
65
66
67def clone_repository(repository_url, base_path, ignore=False):
68 path_suffix = repository_url.rsplit("/", 1)[1]
69 if path_suffix.endswith(".git"):
70 path_suffix = path_suffix[:-4]
71
Milosz Wasilewskib5045412016-12-07 11:27:10 +000072 path = os.path.abspath(os.path.join(base_path, path_suffix))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010073 if os.path.exists(path) and ignore:
74 return(repository_url, path)
75 # git clone repository_url
76 subprocess.call(['git', 'clone', repository_url, path])
77 # return tuple (repository_url, system_path)
78 return (repository_url, path)
79
80
Milosz Wasilewskib5045412016-12-07 11:27:10 +000081def test_exists(test, repositories, args):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010082 test_file_path = os.path.join(
83 repositories[test['repository']],
84 test['path']
85 )
86 current_dir = os.getcwd()
Milosz Wasilewski9c00f732020-08-04 10:29:35 +010087 logger.debug("Current dir: {}".format(current_dir))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010088 os.chdir(repositories[test['repository']])
89 if 'revision' in test.keys():
90 subprocess.call(['git', 'checkout', test['revision']])
Milosz Wasilewski9a83b8e2020-07-10 11:43:33 +010091 elif 'branch' in test.keys():
92 subprocess.call(['git', 'checkout', test['branch']])
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010093 else:
94 # if no revision is specified, use current HEAD
95 output = subprocess.check_output(['git', 'rev-parse', 'HEAD'])
Milosz Wasilewskif9e70c02020-09-25 13:19:44 +010096 test['revision'] = output.decode('utf-8').strip()
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010097
98 if not os.path.exists(test_file_path) or not os.path.isfile(test_file_path):
99 test['missing'] = True
100 os.chdir(current_dir)
101 return not test['missing']
102 test['missing'] = False
103 # open the file and render the test
104 subprocess.call(['git', 'checkout', 'master'])
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100105 logger.debug("Current dir: {}".format(current_dir))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100106 os.chdir(current_dir)
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100107 logger.debug("CWD: {}".format(os.getcwd()))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100108 test_file = open(test_file_path, "r")
Ryan Harkin646fbff2020-07-09 22:11:11 +0100109 test_yaml = yaml.load(test_file.read(), Loader=yaml.FullLoader)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100110 params_string = ""
111 if 'parameters' in test.keys():
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100112 params_string = "_".join(["{0}-{1}".format(param_name, param_value).replace("/", "").replace(" ", "") for param_name, param_value in test['parameters'].items()])
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100113 test_yaml['params'].update(test['parameters'])
Nicolas Dechesne0d0f5a42020-09-29 11:26:49 +0200114
115 # add all default params from YAML test def in the test object
116 if args.single_output:
117 if 'params' in test_yaml.keys():
118 if 'parameters' not in test:
119 test['parameters'] = {}
120 for param_name, param_value in test_yaml['params'].items():
121 if param_name not in test['parameters'].keys():
122 test['parameters'].update({param_name: param_value})
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100123 logger.debug("PARAM strings: {}".format(params_string))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100124 test_name = "{0}_{1}.html".format(test_yaml['metadata']['name'], params_string)
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000125 if not args.single_output:
126 test['filename'] = test_name
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000127 test_path = os.path.join(os.path.abspath(args.output), test_name)
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000128 if args.single_output:
129 # update test plan object
130 test.update(test_yaml['run'])
Milosz Wasilewski68279b22018-02-21 12:41:02 +0000131 # prepend in reversed order so 'name' is on top
132 test.prepend("os", test_yaml['metadata']['os'])
133 test.prepend("scope", test_yaml['metadata']['scope'])
134 test.prepend("description", test_yaml['metadata']['description'])
135 test.prepend("name", test_yaml['metadata']['name'])
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000136 else:
Milosz Wasilewskie6f16322020-08-04 10:30:05 +0100137 render(test_yaml, templates_dir=args.templates_directory, template=args.test_template_name, name=test_path)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100138 return not test['missing']
139
140
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000141def add_csv_row(requirement, test, args, manual=False):
142 fieldnames = [
143 "req_name",
144 "req_owner",
145 "req_category",
146 "path",
147 "repository",
148 "revision",
149 "parameters",
150 "mandatory",
151 "kind",
152 ]
153 csv_file_path = os.path.join(os.path.abspath(args.output), args.csv_name)
154 has_header = False
155 if os.path.isfile(csv_file_path):
156 has_header = True
157 with open(csv_file_path, "ab+") as csv_file:
158 csvdict = DictWriter(csv_file, fieldnames=fieldnames)
159 if not has_header:
160 csvdict.writeheader()
161 csvdict.writerow(
162 {
163 "req_name": requirement.get('name'),
164 "req_owner": requirement.get('owner'),
165 "req_category": requirement.get('category'),
166 "path": test.get('path'),
167 "repository": test.get('repository'),
168 "revision": test.get('revision'),
169 "parameters": test.get('parameters'),
170 "mandatory": test.get('mandatory'),
171 "kind": "manual" if manual else "automated",
172 }
173 )
174
175
176def check_coverage(requirement, repositories, args):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100177 requirement['covered'] = False
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000178 if 'tests' not in requirement.keys() or requirement['tests'] is None:
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100179 return
180 if 'manual' in requirement['tests'].keys() and requirement['tests']['manual'] is not None:
181 for test in requirement['tests']['manual']:
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000182 if test_exists(test, repositories, args):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100183 requirement['covered'] = True
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000184 if args.csv_name:
185 add_csv_row(requirement, test, args, True)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100186 if 'automated' in requirement['tests'].keys() and requirement['tests']['automated'] is not None:
187 for test in requirement['tests']['automated']:
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000188 if test_exists(test, repositories, args):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100189 requirement['covered'] = True
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000190 if args.csv_name:
191 add_csv_row(requirement, test, args)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100192
193
Milosz Wasilewski68279b22018-02-21 12:41:02 +0000194def dict_representer(dumper, data):
195 return dumper.represent_dict(data.iteritems())
196
197
198def dict_constructor(loader, node):
199 return PrependOrderedDict(loader.construct_pairs(node))
200
201
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100202def main():
203 parser = ArgumentParser()
204 parser.add_argument("-f",
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000205 "--file",
206 dest="testplan_list",
207 required=True,
208 nargs="+",
209 help="Test plan file to be used")
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100210 parser.add_argument("-r",
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000211 "--repositories",
212 dest="repository_path",
213 default="repositories",
214 help="Test plan file to be used")
215 parser.add_argument("-o",
216 "--output",
217 dest="output",
218 default="output",
219 help="Destination directory for generated files")
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100220 parser.add_argument("-i",
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000221 "--ignore-clone",
222 dest="ignore_clone",
223 action="store_true",
224 default=False,
225 help="Ignore cloning repositories and use previously cloned")
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000226 parser.add_argument("-s",
227 "--single-file-output",
228 dest="single_output",
229 action="store_true",
230 default=False,
231 help="""Render test plan into single HTML file. This option ignores
232 any metadata that is available in test cases""")
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000233 parser.add_argument("-c",
234 "--csv",
235 dest="csv_name",
236 required=False,
237 help="Name of CSV to store overall list of requirements and test. If name is absent, the file will not be generated")
Milosz Wasilewskie6f16322020-08-04 10:30:05 +0100238 parser.add_argument("--test-template-name",
239 default="test.html",
240 help="Name of the template used for rendering individual tests")
241 parser.add_argument("--testplan-template-name",
242 default="testplan.html",
243 help="Name of the template used for rendering testsplans")
244 parser.add_argument("--templates-directory",
245 default=None,
246 help="Directory where the templates are located (absolute path)")
Milosz Wasilewski923ed1b2020-08-10 16:15:46 +0100247 parser.add_argument("--pdf",
248 default=None,
249 help="Path to the output pdf file. Only works if output generates HTML")
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100250
Milosz Wasilewski68279b22018-02-21 12:41:02 +0000251 _mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
252 yaml.add_representer(PrependOrderedDict, dict_representer)
253 yaml.add_constructor(_mapping_tag, dict_constructor)
254
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100255 args = parser.parse_args()
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000256 if not os.path.exists(os.path.abspath(args.output)):
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100257 os.makedirs(os.path.abspath(args.output), mode=0o755)
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000258 for testplan in args.testplan_list:
259 if os.path.exists(testplan) and os.path.isfile(testplan):
260 testplan_file = open(testplan, "r")
Ryan Harkin646fbff2020-07-09 22:11:11 +0100261 tp_obj = yaml.load(testplan_file.read(), Loader=yaml.FullLoader)
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000262 repo_list = repository_list(tp_obj)
263 repositories = {}
264 for repo in repo_list:
265 repo_url, repo_path = clone_repository(repo, args.repository_path, args.ignore_clone)
266 repositories.update({repo_url: repo_path})
267 # ToDo: check test plan structure
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +0000268
269 tp_version = tp_obj['metadata']['format']
270 if tp_version == "Linaro Test Plan v1":
271 for requirement in tp_obj['requirements']:
272 check_coverage(requirement, repositories, args)
273 if tp_version == "Linaro Test Plan v2":
274 if 'manual' in tp_obj['tests'].keys() and tp_obj['tests']['manual'] is not None:
275 for test in tp_obj['tests']['manual']:
276 test_exists(test, repositories, args)
277 if 'automated' in tp_obj['tests'].keys() and tp_obj['tests']['automated'] is not None:
278 for test in tp_obj['tests']['automated']:
279 test_exists(test, repositories, args)
Milosz Wasilewskif9e70c02020-09-25 13:19:44 +0100280 # same filename extension as the template
281 tp_name = tp_obj['metadata']['name'] + os.path.splitext(args.testplan_template_name)[1]
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000282 tp_file_name = os.path.join(os.path.abspath(args.output), tp_name)
Milosz Wasilewskie6f16322020-08-04 10:30:05 +0100283 render(tp_obj, templates_dir=args.templates_directory, template=args.testplan_template_name, name=tp_file_name)
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000284 testplan_file.close()
Milosz Wasilewski923ed1b2020-08-10 16:15:46 +0100285 if args.pdf is not None:
286 pdfkit.from_file(tp_file_name, args.pdf)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100287# go through requiremets and for each test:
288# - if file exists render test as separate html file
289# - if file is missing, indicate missing test (red)
290# render test plan with links to test files
291# add option to render as single file (for pdf generation)
292
Milosz Wasilewski60152062020-03-02 18:53:29 +0000293
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100294if __name__ == "__main__":
295 main()