blob: a00b8d67ff7dd2de357b54d106ad4ef683155c57 [file] [log] [blame]
Nicolas Dechesne3f225752020-09-29 13:22:53 +02001#!/usr/bin/env python3
2
Milosz Wasilewski68279b22018-02-21 12:41:02 +00003import collections
Milosz Wasilewskif9e70c02020-09-25 13:19:44 +01004import datetime
Milosz Wasilewski9c00f732020-08-04 10:29:35 +01005import logging
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +01006import os
Milosz Wasilewski923ed1b2020-08-10 16:15:46 +01007import pdfkit
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +01008import subprocess
9import yaml
10from argparse import ArgumentParser
Milosz Wasilewskib5045412016-12-07 11:27:10 +000011from csv import DictWriter
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010012from jinja2 import Environment, FileSystemLoader
13
14
Milosz Wasilewski9c00f732020-08-04 10:29:35 +010015logger = logging.getLogger()
16
17
Milosz Wasilewski68279b22018-02-21 12:41:02 +000018class PrependOrderedDict(collections.OrderedDict):
19
20 def prepend(self, key, value, dict_setitem=dict.__setitem__):
Milosz Wasilewski9c00f732020-08-04 10:29:35 +010021 self[key] = value
22 self.move_to_end(key, last=False)
Milosz Wasilewski68279b22018-02-21 12:41:02 +000023
24
Milosz Wasilewskie6f16322020-08-04 10:30:05 +010025def render(obj, template="testplan.html", templates_dir=None, name=None):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010026 if name is None:
27 name = template
Milosz Wasilewskie6f16322020-08-04 10:30:05 +010028 if templates_dir is None:
29 templates_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
Milosz Wasilewskif761ab92018-02-13 11:21:18 +000030 _env = Environment(loader=FileSystemLoader(templates_dir))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010031 _template = _env.get_template(template)
Milosz Wasilewskif9e70c02020-09-25 13:19:44 +010032 obj['metadata']['now'] = datetime.date.today().strftime("%B %d, %Y")
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010033 _obj = _template.render(obj=obj)
34 with open("{}".format(name), "wb") as _file:
35 _file.write(_obj.encode('utf-8'))
36
Nicolas Dechesnecd2e0f12020-09-29 11:20:25 +020037 # if the template is a .textile template, let's convert the output file to html
38 if os.path.splitext(name)[1] == '.textile':
39 import textile
40 with open("{}".format(name), "r") as _file:
41 data = _file.read()
42 with open("{}{}".format(os.path.splitext(name)[0], ".html"), "w") as _file:
43 _file.write(textile.textile(data))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010044
Nicolas Dechesned9325552020-09-29 13:20:44 +020045
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010046# get list of repositories and cache them
47def repository_list(testplan):
48 repositories = set()
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +000049 tp_version = testplan['metadata']['format']
50 if tp_version == "Linaro Test Plan v2":
51 if 'manual' in testplan['tests'].keys() and testplan['tests']['manual'] is not None:
52 for test in testplan['tests']['manual']:
53 repositories.add(test['repository'])
54
55 if 'automated' in testplan['tests'].keys() and testplan['tests']['automated'] is not None:
56 for test in testplan['tests']['automated']:
57 repositories.add(test['repository'])
58 if tp_version == "Linaro Test Plan v1":
59 for req in testplan['requirements']:
60 if 'tests' in req.keys() and req['tests'] is not None:
61 if 'manual' in req['tests'].keys() and req['tests']['manual'] is not None:
62 for test in req['tests']['manual']:
63 repositories.add(test['repository'])
64 if 'automated' in req['tests'].keys() and req['tests']['automated'] is not None:
65 for test in req['tests']['automated']:
66 repositories.add(test['repository'])
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010067 return repositories
68
69
70def clone_repository(repository_url, base_path, ignore=False):
71 path_suffix = repository_url.rsplit("/", 1)[1]
72 if path_suffix.endswith(".git"):
73 path_suffix = path_suffix[:-4]
74
Milosz Wasilewskib5045412016-12-07 11:27:10 +000075 path = os.path.abspath(os.path.join(base_path, path_suffix))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010076 if os.path.exists(path) and ignore:
77 return(repository_url, path)
Nicolas Dechesne1f3fa5b2020-10-02 12:15:19 +020078
79 # if the user does not use --ignore-clone, let's default to updating our local copy
80 if os.path.exists(path):
81 subprocess.call(['git', 'pull', '--ff-only'], cwd=path)
82 return(repository_url, path)
83
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010084 # git clone repository_url
85 subprocess.call(['git', 'clone', repository_url, path])
86 # return tuple (repository_url, system_path)
87 return (repository_url, path)
88
89
Milosz Wasilewskib5045412016-12-07 11:27:10 +000090def test_exists(test, repositories, args):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010091 test_file_path = os.path.join(
92 repositories[test['repository']],
93 test['path']
94 )
95 current_dir = os.getcwd()
Milosz Wasilewski9c00f732020-08-04 10:29:35 +010096 logger.debug("Current dir: {}".format(current_dir))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010097 os.chdir(repositories[test['repository']])
98 if 'revision' in test.keys():
99 subprocess.call(['git', 'checkout', test['revision']])
Milosz Wasilewski9a83b8e2020-07-10 11:43:33 +0100100 elif 'branch' in test.keys():
101 subprocess.call(['git', 'checkout', test['branch']])
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100102 else:
103 # if no revision is specified, use current HEAD
104 output = subprocess.check_output(['git', 'rev-parse', 'HEAD'])
Milosz Wasilewskif9e70c02020-09-25 13:19:44 +0100105 test['revision'] = output.decode('utf-8').strip()
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100106
107 if not os.path.exists(test_file_path) or not os.path.isfile(test_file_path):
108 test['missing'] = True
109 os.chdir(current_dir)
110 return not test['missing']
111 test['missing'] = False
112 # open the file and render the test
Nicolas Dechesnec13d8932020-10-02 12:17:23 +0200113 subprocess.call(['git', 'checkout', '-q', 'master'])
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100114 logger.debug("Current dir: {}".format(current_dir))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100115 os.chdir(current_dir)
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100116 logger.debug("CWD: {}".format(os.getcwd()))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100117 test_file = open(test_file_path, "r")
Ryan Harkin646fbff2020-07-09 22:11:11 +0100118 test_yaml = yaml.load(test_file.read(), Loader=yaml.FullLoader)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100119 params_string = ""
120 if 'parameters' in test.keys():
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100121 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 +0100122 test_yaml['params'].update(test['parameters'])
Nicolas Dechesne0d0f5a42020-09-29 11:26:49 +0200123
124 # add all default params from YAML test def in the test object
125 if args.single_output:
126 if 'params' in test_yaml.keys():
127 if 'parameters' not in test:
128 test['parameters'] = {}
129 for param_name, param_value in test_yaml['params'].items():
130 if param_name not in test['parameters'].keys():
131 test['parameters'].update({param_name: param_value})
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100132 logger.debug("PARAM strings: {}".format(params_string))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100133 test_name = "{0}_{1}.html".format(test_yaml['metadata']['name'], params_string)
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000134 if not args.single_output:
135 test['filename'] = test_name
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000136 test_path = os.path.join(os.path.abspath(args.output), test_name)
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000137 if args.single_output:
138 # update test plan object
139 test.update(test_yaml['run'])
Milosz Wasilewski68279b22018-02-21 12:41:02 +0000140 # prepend in reversed order so 'name' is on top
141 test.prepend("os", test_yaml['metadata']['os'])
142 test.prepend("scope", test_yaml['metadata']['scope'])
143 test.prepend("description", test_yaml['metadata']['description'])
Nicolas Dechesne548701d2020-10-02 11:42:18 +0200144 if 'name' not in test:
145 test.prepend("name", test_yaml['metadata']['name'])
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000146 else:
Milosz Wasilewskie6f16322020-08-04 10:30:05 +0100147 render(test_yaml, templates_dir=args.templates_directory, template=args.test_template_name, name=test_path)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100148 return not test['missing']
149
150
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000151def add_csv_row(requirement, test, args, manual=False):
152 fieldnames = [
153 "req_name",
154 "req_owner",
155 "req_category",
156 "path",
157 "repository",
158 "revision",
159 "parameters",
160 "mandatory",
161 "kind",
162 ]
163 csv_file_path = os.path.join(os.path.abspath(args.output), args.csv_name)
164 has_header = False
165 if os.path.isfile(csv_file_path):
166 has_header = True
167 with open(csv_file_path, "ab+") as csv_file:
168 csvdict = DictWriter(csv_file, fieldnames=fieldnames)
169 if not has_header:
170 csvdict.writeheader()
171 csvdict.writerow(
172 {
173 "req_name": requirement.get('name'),
174 "req_owner": requirement.get('owner'),
175 "req_category": requirement.get('category'),
176 "path": test.get('path'),
177 "repository": test.get('repository'),
178 "revision": test.get('revision'),
179 "parameters": test.get('parameters'),
180 "mandatory": test.get('mandatory'),
181 "kind": "manual" if manual else "automated",
182 }
183 )
184
185
186def check_coverage(requirement, repositories, args):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100187 requirement['covered'] = False
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000188 if 'tests' not in requirement.keys() or requirement['tests'] is None:
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100189 return
190 if 'manual' in requirement['tests'].keys() and requirement['tests']['manual'] is not None:
191 for test in requirement['tests']['manual']:
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000192 if test_exists(test, repositories, args):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100193 requirement['covered'] = True
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000194 if args.csv_name:
195 add_csv_row(requirement, test, args, True)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100196 if 'automated' in requirement['tests'].keys() and requirement['tests']['automated'] is not None:
197 for test in requirement['tests']['automated']:
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000198 if test_exists(test, repositories, args):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100199 requirement['covered'] = True
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000200 if args.csv_name:
201 add_csv_row(requirement, test, args)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100202
203
Milosz Wasilewski68279b22018-02-21 12:41:02 +0000204def dict_representer(dumper, data):
205 return dumper.represent_dict(data.iteritems())
206
207
208def dict_constructor(loader, node):
209 return PrependOrderedDict(loader.construct_pairs(node))
210
211
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100212def main():
213 parser = ArgumentParser()
214 parser.add_argument("-f",
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000215 "--file",
216 dest="testplan_list",
217 required=True,
218 nargs="+",
219 help="Test plan file to be used")
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100220 parser.add_argument("-r",
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000221 "--repositories",
222 dest="repository_path",
223 default="repositories",
224 help="Test plan file to be used")
225 parser.add_argument("-o",
226 "--output",
227 dest="output",
228 default="output",
229 help="Destination directory for generated files")
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100230 parser.add_argument("-i",
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000231 "--ignore-clone",
232 dest="ignore_clone",
233 action="store_true",
234 default=False,
235 help="Ignore cloning repositories and use previously cloned")
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000236 parser.add_argument("-s",
237 "--single-file-output",
238 dest="single_output",
239 action="store_true",
240 default=False,
241 help="""Render test plan into single HTML file. This option ignores
242 any metadata that is available in test cases""")
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000243 parser.add_argument("-c",
244 "--csv",
245 dest="csv_name",
246 required=False,
247 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 +0100248 parser.add_argument("--test-template-name",
249 default="test.html",
250 help="Name of the template used for rendering individual tests")
251 parser.add_argument("--testplan-template-name",
Milosz Wasilewskie6f16322020-08-04 10:30:05 +0100252 help="Name of the template used for rendering testsplans")
253 parser.add_argument("--templates-directory",
254 default=None,
255 help="Directory where the templates are located (absolute path)")
Milosz Wasilewski923ed1b2020-08-10 16:15:46 +0100256 parser.add_argument("--pdf",
257 default=None,
258 help="Path to the output pdf file. Only works if output generates HTML")
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100259
Milosz Wasilewski68279b22018-02-21 12:41:02 +0000260 _mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
261 yaml.add_representer(PrependOrderedDict, dict_representer)
262 yaml.add_constructor(_mapping_tag, dict_constructor)
263
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100264 args = parser.parse_args()
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000265 if not os.path.exists(os.path.abspath(args.output)):
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100266 os.makedirs(os.path.abspath(args.output), mode=0o755)
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000267 for testplan in args.testplan_list:
268 if os.path.exists(testplan) and os.path.isfile(testplan):
269 testplan_file = open(testplan, "r")
Ryan Harkin646fbff2020-07-09 22:11:11 +0100270 tp_obj = yaml.load(testplan_file.read(), Loader=yaml.FullLoader)
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000271 repo_list = repository_list(tp_obj)
272 repositories = {}
273 for repo in repo_list:
274 repo_url, repo_path = clone_repository(repo, args.repository_path, args.ignore_clone)
275 repositories.update({repo_url: repo_path})
276 # ToDo: check test plan structure
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +0000277
278 tp_version = tp_obj['metadata']['format']
279 if tp_version == "Linaro Test Plan v1":
Nicolas Dechesne8ee692d2021-02-02 00:00:00 +0100280 testplan_template = args.testplan_template_name or "testplan.html"
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +0000281 for requirement in tp_obj['requirements']:
282 check_coverage(requirement, repositories, args)
283 if tp_version == "Linaro Test Plan v2":
Nicolas Dechesne8ee692d2021-02-02 00:00:00 +0100284 testplan_template = args.testplan_template_name or "testplan_v2.html"
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +0000285 if 'manual' in tp_obj['tests'].keys() and tp_obj['tests']['manual'] is not None:
286 for test in tp_obj['tests']['manual']:
287 test_exists(test, repositories, args)
288 if 'automated' in tp_obj['tests'].keys() and tp_obj['tests']['automated'] is not None:
289 for test in tp_obj['tests']['automated']:
290 test_exists(test, repositories, args)
Milosz Wasilewskif9e70c02020-09-25 13:19:44 +0100291 # same filename extension as the template
Nicolas Dechesne8ee692d2021-02-02 00:00:00 +0100292 tp_name = tp_obj['metadata']['name'] + os.path.splitext(testplan_template)[1]
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000293 tp_file_name = os.path.join(os.path.abspath(args.output), tp_name)
Nicolas Dechesne8ee692d2021-02-02 00:00:00 +0100294 render(tp_obj, templates_dir=args.templates_directory, template=testplan_template, name=tp_file_name)
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000295 testplan_file.close()
Milosz Wasilewski923ed1b2020-08-10 16:15:46 +0100296 if args.pdf is not None:
297 pdfkit.from_file(tp_file_name, args.pdf)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100298# go through requiremets and for each test:
299# - if file exists render test as separate html file
300# - if file is missing, indicate missing test (red)
301# render test plan with links to test files
302# add option to render as single file (for pdf generation)
303
Milosz Wasilewski60152062020-03-02 18:53:29 +0000304
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100305if __name__ == "__main__":
306 main()