blob: b3ac410710c7683a377f806ef778ab61f11c5bf0 [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):
Milosz Wasilewski68279b22018-02-21 12:41:02 +000019 def prepend(self, key, value, dict_setitem=dict.__setitem__):
Milosz Wasilewski9c00f732020-08-04 10:29:35 +010020 self[key] = value
21 self.move_to_end(key, last=False)
Milosz Wasilewski68279b22018-02-21 12:41:02 +000022
23
Milosz Wasilewskie6f16322020-08-04 10:30:05 +010024def render(obj, template="testplan.html", templates_dir=None, name=None):
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010025 if name is None:
26 name = template
Milosz Wasilewskie6f16322020-08-04 10:30:05 +010027 if templates_dir is None:
Benjamin Copeland15d743e2021-02-22 08:35:10 +000028 templates_dir = os.path.join(
29 os.path.dirname(os.path.abspath(__file__)), "templates"
30 )
Milosz Wasilewskif761ab92018-02-13 11:21:18 +000031 _env = Environment(loader=FileSystemLoader(templates_dir))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010032 _template = _env.get_template(template)
Benjamin Copeland15d743e2021-02-22 08:35:10 +000033 obj["metadata"]["now"] = datetime.date.today().strftime("%B %d, %Y")
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010034 _obj = _template.render(obj=obj)
35 with open("{}".format(name), "wb") as _file:
Benjamin Copeland15d743e2021-02-22 08:35:10 +000036 _file.write(_obj.encode("utf-8"))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010037
Nicolas Dechesnecd2e0f12020-09-29 11:20:25 +020038 # if the template is a .textile template, let's convert the output file to html
Benjamin Copeland15d743e2021-02-22 08:35:10 +000039 if os.path.splitext(name)[1] == ".textile":
Nicolas Dechesnecd2e0f12020-09-29 11:20:25 +020040 import textile
Benjamin Copeland15d743e2021-02-22 08:35:10 +000041
Nicolas Dechesnecd2e0f12020-09-29 11:20:25 +020042 with open("{}".format(name), "r") as _file:
43 data = _file.read()
44 with open("{}{}".format(os.path.splitext(name)[0], ".html"), "w") as _file:
45 _file.write(textile.textile(data))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010046
Nicolas Dechesned9325552020-09-29 13:20:44 +020047
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010048# get list of repositories and cache them
49def repository_list(testplan):
50 repositories = set()
Benjamin Copeland15d743e2021-02-22 08:35:10 +000051 tp_version = testplan["metadata"]["format"]
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +000052 if tp_version == "Linaro Test Plan v2":
Benjamin Copeland15d743e2021-02-22 08:35:10 +000053 if (
54 "manual" in testplan["tests"].keys()
55 and testplan["tests"]["manual"] is not None
56 ):
57 for test in testplan["tests"]["manual"]:
58 repositories.add(test["repository"])
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +000059
Benjamin Copeland15d743e2021-02-22 08:35:10 +000060 if (
61 "automated" in testplan["tests"].keys()
62 and testplan["tests"]["automated"] is not None
63 ):
64 for test in testplan["tests"]["automated"]:
65 repositories.add(test["repository"])
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +000066 if tp_version == "Linaro Test Plan v1":
Benjamin Copeland15d743e2021-02-22 08:35:10 +000067 for req in testplan["requirements"]:
68 if "tests" in req.keys() and req["tests"] is not None:
69 if (
70 "manual" in req["tests"].keys()
71 and req["tests"]["manual"] is not None
72 ):
73 for test in req["tests"]["manual"]:
74 repositories.add(test["repository"])
75 if (
76 "automated" in req["tests"].keys()
77 and req["tests"]["automated"] is not None
78 ):
79 for test in req["tests"]["automated"]:
80 repositories.add(test["repository"])
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010081 return repositories
82
83
84def clone_repository(repository_url, base_path, ignore=False):
85 path_suffix = repository_url.rsplit("/", 1)[1]
86 if path_suffix.endswith(".git"):
87 path_suffix = path_suffix[:-4]
88
Milosz Wasilewskib5045412016-12-07 11:27:10 +000089 path = os.path.abspath(os.path.join(base_path, path_suffix))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010090 if os.path.exists(path) and ignore:
Benjamin Copeland15d743e2021-02-22 08:35:10 +000091 return (repository_url, path)
Nicolas Dechesne1f3fa5b2020-10-02 12:15:19 +020092
93 # if the user does not use --ignore-clone, let's default to updating our local copy
94 if os.path.exists(path):
Benjamin Copeland15d743e2021-02-22 08:35:10 +000095 subprocess.call(["git", "pull", "--ff-only"], cwd=path)
96 return (repository_url, path)
Nicolas Dechesne1f3fa5b2020-10-02 12:15:19 +020097
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +010098 # git clone repository_url
Benjamin Copeland15d743e2021-02-22 08:35:10 +000099 subprocess.call(["git", "clone", repository_url, path])
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100100 # return tuple (repository_url, system_path)
101 return (repository_url, path)
102
103
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000104def test_exists(test, repositories, args):
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000105 test_file_path = os.path.join(repositories[test["repository"]], test["path"])
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100106 current_dir = os.getcwd()
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100107 logger.debug("Current dir: {}".format(current_dir))
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000108 os.chdir(repositories[test["repository"]])
109 if "revision" in test.keys():
110 subprocess.call(["git", "checkout", test["revision"]])
111 elif "branch" in test.keys():
112 subprocess.call(["git", "checkout", test["branch"]])
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100113 else:
114 # if no revision is specified, use current HEAD
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000115 output = subprocess.check_output(["git", "rev-parse", "HEAD"])
116 test["revision"] = output.decode("utf-8").strip()
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100117
118 if not os.path.exists(test_file_path) or not os.path.isfile(test_file_path):
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000119 test["missing"] = True
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100120 os.chdir(current_dir)
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000121 return not test["missing"]
122 test["missing"] = False
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100123 # open the file and render the test
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000124 subprocess.call(["git", "checkout", "-q", "master"])
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100125 logger.debug("Current dir: {}".format(current_dir))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100126 os.chdir(current_dir)
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100127 logger.debug("CWD: {}".format(os.getcwd()))
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100128 test_file = open(test_file_path, "r")
Ryan Harkin646fbff2020-07-09 22:11:11 +0100129 test_yaml = yaml.load(test_file.read(), Loader=yaml.FullLoader)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100130 params_string = ""
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000131 if "parameters" in test.keys():
132 params_string = "_".join(
133 [
134 "{0}-{1}".format(param_name, param_value)
135 .replace("/", "")
136 .replace(" ", "")
137 for param_name, param_value in test["parameters"].items()
138 ]
139 )
140 test_yaml["params"].update(test["parameters"])
Nicolas Dechesne0d0f5a42020-09-29 11:26:49 +0200141
142 # add all default params from YAML test def in the test object
143 if args.single_output:
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000144 if "params" in test_yaml.keys():
145 if "parameters" not in test:
146 test["parameters"] = {}
147 for param_name, param_value in test_yaml["params"].items():
148 if param_name not in test["parameters"].keys():
149 test["parameters"].update({param_name: param_value})
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100150 logger.debug("PARAM strings: {}".format(params_string))
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000151 test_name = "{0}_{1}.html".format(test_yaml["metadata"]["name"], params_string)
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000152 if not args.single_output:
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000153 test["filename"] = test_name
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000154 test_path = os.path.join(os.path.abspath(args.output), test_name)
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000155 if args.single_output:
156 # update test plan object
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000157 test.update(test_yaml["run"])
Milosz Wasilewski68279b22018-02-21 12:41:02 +0000158 # prepend in reversed order so 'name' is on top
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000159 test.prepend("os", test_yaml["metadata"]["os"])
160 test.prepend("scope", test_yaml["metadata"]["scope"])
161 test.prepend("description", test_yaml["metadata"]["description"])
162 if "name" not in test:
163 test.prepend("name", test_yaml["metadata"]["name"])
Milosz Wasilewski807c0ea2018-02-13 19:14:01 +0000164 else:
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000165 render(
166 test_yaml,
167 templates_dir=args.templates_directory,
168 template=args.test_template_name,
169 name=test_path,
170 )
171 return not test["missing"]
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100172
173
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000174def add_csv_row(requirement, test, args, manual=False):
175 fieldnames = [
176 "req_name",
177 "req_owner",
178 "req_category",
179 "path",
180 "repository",
181 "revision",
182 "parameters",
183 "mandatory",
184 "kind",
185 ]
186 csv_file_path = os.path.join(os.path.abspath(args.output), args.csv_name)
187 has_header = False
188 if os.path.isfile(csv_file_path):
189 has_header = True
190 with open(csv_file_path, "ab+") as csv_file:
191 csvdict = DictWriter(csv_file, fieldnames=fieldnames)
192 if not has_header:
193 csvdict.writeheader()
194 csvdict.writerow(
195 {
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000196 "req_name": requirement.get("name"),
197 "req_owner": requirement.get("owner"),
198 "req_category": requirement.get("category"),
199 "path": test.get("path"),
200 "repository": test.get("repository"),
201 "revision": test.get("revision"),
202 "parameters": test.get("parameters"),
203 "mandatory": test.get("mandatory"),
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000204 "kind": "manual" if manual else "automated",
205 }
206 )
207
208
209def check_coverage(requirement, repositories, args):
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000210 requirement["covered"] = False
211 if "tests" not in requirement.keys() or requirement["tests"] is None:
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100212 return
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000213 if (
214 "manual" in requirement["tests"].keys()
215 and requirement["tests"]["manual"] is not None
216 ):
217 for test in requirement["tests"]["manual"]:
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000218 if test_exists(test, repositories, args):
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000219 requirement["covered"] = True
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000220 if args.csv_name:
221 add_csv_row(requirement, test, args, True)
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000222 if (
223 "automated" in requirement["tests"].keys()
224 and requirement["tests"]["automated"] is not None
225 ):
226 for test in requirement["tests"]["automated"]:
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000227 if test_exists(test, repositories, args):
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000228 requirement["covered"] = True
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000229 if args.csv_name:
230 add_csv_row(requirement, test, args)
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100231
232
Milosz Wasilewski68279b22018-02-21 12:41:02 +0000233def dict_representer(dumper, data):
234 return dumper.represent_dict(data.iteritems())
235
236
237def dict_constructor(loader, node):
238 return PrependOrderedDict(loader.construct_pairs(node))
239
240
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100241def main():
242 parser = ArgumentParser()
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000243 parser.add_argument(
244 "-f",
245 "--file",
246 dest="testplan_list",
247 required=True,
248 nargs="+",
249 help="Test plan file to be used",
250 )
251 parser.add_argument(
252 "-r",
253 "--repositories",
254 dest="repository_path",
255 default="repositories",
256 help="Test plan file to be used",
257 )
258 parser.add_argument(
259 "-o",
260 "--output",
261 dest="output",
262 default="output",
263 help="Destination directory for generated files",
264 )
265 parser.add_argument(
266 "-i",
267 "--ignore-clone",
268 dest="ignore_clone",
269 action="store_true",
270 default=False,
271 help="Ignore cloning repositories and use previously cloned",
272 )
273 parser.add_argument(
274 "-s",
275 "--single-file-output",
276 dest="single_output",
277 action="store_true",
278 default=False,
279 help="""Render test plan into single HTML file. This option ignores
280 any metadata that is available in test cases""",
281 )
282 parser.add_argument(
283 "-c",
284 "--csv",
285 dest="csv_name",
286 required=False,
287 help="Name of CSV to store overall list of requirements and test. If name is absent, the file will not be generated",
288 )
289 parser.add_argument(
290 "--test-template-name",
291 default="test.html",
292 help="Name of the template used for rendering individual tests",
293 )
294 parser.add_argument(
295 "--testplan-template-name",
296 help="Name of the template used for rendering testsplans",
297 )
298 parser.add_argument(
299 "--templates-directory",
300 default=None,
301 help="Directory where the templates are located (absolute path)",
302 )
303 parser.add_argument(
304 "--pdf",
305 default=None,
306 help="Path to the output pdf file. Only works if output generates HTML",
307 )
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100308
Milosz Wasilewski68279b22018-02-21 12:41:02 +0000309 _mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
310 yaml.add_representer(PrependOrderedDict, dict_representer)
311 yaml.add_constructor(_mapping_tag, dict_constructor)
312
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100313 args = parser.parse_args()
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000314 if not os.path.exists(os.path.abspath(args.output)):
Milosz Wasilewski9c00f732020-08-04 10:29:35 +0100315 os.makedirs(os.path.abspath(args.output), mode=0o755)
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000316 for testplan in args.testplan_list:
317 if os.path.exists(testplan) and os.path.isfile(testplan):
318 testplan_file = open(testplan, "r")
Ryan Harkin646fbff2020-07-09 22:11:11 +0100319 tp_obj = yaml.load(testplan_file.read(), Loader=yaml.FullLoader)
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000320 repo_list = repository_list(tp_obj)
321 repositories = {}
322 for repo in repo_list:
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000323 repo_url, repo_path = clone_repository(
324 repo, args.repository_path, args.ignore_clone
325 )
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000326 repositories.update({repo_url: repo_path})
327 # ToDo: check test plan structure
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +0000328
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000329 tp_version = tp_obj["metadata"]["format"]
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +0000330 if tp_version == "Linaro Test Plan v1":
Nicolas Dechesne8ee692d2021-02-02 00:00:00 +0100331 testplan_template = args.testplan_template_name or "testplan.html"
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000332 for requirement in tp_obj["requirements"]:
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +0000333 check_coverage(requirement, repositories, args)
334 if tp_version == "Linaro Test Plan v2":
Nicolas Dechesne8ee692d2021-02-02 00:00:00 +0100335 testplan_template = args.testplan_template_name or "testplan_v2.html"
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000336 if (
337 "manual" in tp_obj["tests"].keys()
338 and tp_obj["tests"]["manual"] is not None
339 ):
340 for test in tp_obj["tests"]["manual"]:
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +0000341 test_exists(test, repositories, args)
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000342 if (
343 "automated" in tp_obj["tests"].keys()
344 and tp_obj["tests"]["automated"] is not None
345 ):
346 for test in tp_obj["tests"]["automated"]:
Milosz Wasilewskid5e1dd92018-02-12 12:08:44 +0000347 test_exists(test, repositories, args)
Milosz Wasilewskif9e70c02020-09-25 13:19:44 +0100348 # same filename extension as the template
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000349 tp_name = (
350 tp_obj["metadata"]["name"] + os.path.splitext(testplan_template)[1]
351 )
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000352 tp_file_name = os.path.join(os.path.abspath(args.output), tp_name)
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000353 render(
354 tp_obj,
355 templates_dir=args.templates_directory,
356 template=testplan_template,
357 name=tp_file_name,
358 )
Milosz Wasilewskib5045412016-12-07 11:27:10 +0000359 testplan_file.close()
Milosz Wasilewski923ed1b2020-08-10 16:15:46 +0100360 if args.pdf is not None:
361 pdfkit.from_file(tp_file_name, args.pdf)
Benjamin Copeland15d743e2021-02-22 08:35:10 +0000362
363
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100364# go through requiremets and for each test:
365# - if file exists render test as separate html file
366# - if file is missing, indicate missing test (red)
367# render test plan with links to test files
368# add option to render as single file (for pdf generation)
369
Milosz Wasilewski60152062020-03-02 18:53:29 +0000370
Milosz Wasilewskif5ccdbd2016-10-25 18:49:20 +0100371if __name__ == "__main__":
372 main()