Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 1 | import collections |
Milosz Wasilewski | f9e70c0 | 2020-09-25 13:19:44 +0100 | [diff] [blame] | 2 | import datetime |
Milosz Wasilewski | 9c00f73 | 2020-08-04 10:29:35 +0100 | [diff] [blame] | 3 | import logging |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 4 | import os |
Milosz Wasilewski | 923ed1b | 2020-08-10 16:15:46 +0100 | [diff] [blame] | 5 | import pdfkit |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 6 | import subprocess |
| 7 | import yaml |
| 8 | from argparse import ArgumentParser |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 9 | from csv import DictWriter |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 10 | from jinja2 import Environment, FileSystemLoader |
| 11 | |
| 12 | |
Milosz Wasilewski | 9c00f73 | 2020-08-04 10:29:35 +0100 | [diff] [blame] | 13 | logger = logging.getLogger() |
| 14 | |
| 15 | |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 16 | class PrependOrderedDict(collections.OrderedDict): |
| 17 | |
| 18 | def prepend(self, key, value, dict_setitem=dict.__setitem__): |
Milosz Wasilewski | 9c00f73 | 2020-08-04 10:29:35 +0100 | [diff] [blame] | 19 | self[key] = value |
| 20 | self.move_to_end(key, last=False) |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 21 | |
| 22 | |
Milosz Wasilewski | e6f1632 | 2020-08-04 10:30:05 +0100 | [diff] [blame] | 23 | def render(obj, template="testplan.html", templates_dir=None, name=None): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 24 | if name is None: |
| 25 | name = template |
Milosz Wasilewski | e6f1632 | 2020-08-04 10:30:05 +0100 | [diff] [blame] | 26 | if templates_dir is None: |
| 27 | templates_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") |
Milosz Wasilewski | f761ab9 | 2018-02-13 11:21:18 +0000 | [diff] [blame] | 28 | _env = Environment(loader=FileSystemLoader(templates_dir)) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 29 | _template = _env.get_template(template) |
Milosz Wasilewski | f9e70c0 | 2020-09-25 13:19:44 +0100 | [diff] [blame] | 30 | obj['metadata']['now'] = datetime.date.today().strftime("%B %d, %Y") |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 31 | _obj = _template.render(obj=obj) |
| 32 | with open("{}".format(name), "wb") as _file: |
| 33 | _file.write(_obj.encode('utf-8')) |
| 34 | |
Nicolas Dechesne | cd2e0f1 | 2020-09-29 11:20:25 +0200 | [diff] [blame] | 35 | # 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 Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 42 | |
| 43 | # get list of repositories and cache them |
| 44 | def repository_list(testplan): |
| 45 | repositories = set() |
Milosz Wasilewski | d5e1dd9 | 2018-02-12 12:08:44 +0000 | [diff] [blame] | 46 | 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 Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 64 | return repositories |
| 65 | |
| 66 | |
| 67 | def 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 Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 72 | path = os.path.abspath(os.path.join(base_path, path_suffix)) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 73 | 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 Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 81 | def test_exists(test, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 82 | test_file_path = os.path.join( |
| 83 | repositories[test['repository']], |
| 84 | test['path'] |
| 85 | ) |
| 86 | current_dir = os.getcwd() |
Milosz Wasilewski | 9c00f73 | 2020-08-04 10:29:35 +0100 | [diff] [blame] | 87 | logger.debug("Current dir: {}".format(current_dir)) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 88 | os.chdir(repositories[test['repository']]) |
| 89 | if 'revision' in test.keys(): |
| 90 | subprocess.call(['git', 'checkout', test['revision']]) |
Milosz Wasilewski | 9a83b8e | 2020-07-10 11:43:33 +0100 | [diff] [blame] | 91 | elif 'branch' in test.keys(): |
| 92 | subprocess.call(['git', 'checkout', test['branch']]) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 93 | else: |
| 94 | # if no revision is specified, use current HEAD |
| 95 | output = subprocess.check_output(['git', 'rev-parse', 'HEAD']) |
Milosz Wasilewski | f9e70c0 | 2020-09-25 13:19:44 +0100 | [diff] [blame] | 96 | test['revision'] = output.decode('utf-8').strip() |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 97 | |
| 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 Wasilewski | 9c00f73 | 2020-08-04 10:29:35 +0100 | [diff] [blame] | 105 | logger.debug("Current dir: {}".format(current_dir)) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 106 | os.chdir(current_dir) |
Milosz Wasilewski | 9c00f73 | 2020-08-04 10:29:35 +0100 | [diff] [blame] | 107 | logger.debug("CWD: {}".format(os.getcwd())) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 108 | test_file = open(test_file_path, "r") |
Ryan Harkin | 646fbff | 2020-07-09 22:11:11 +0100 | [diff] [blame] | 109 | test_yaml = yaml.load(test_file.read(), Loader=yaml.FullLoader) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 110 | params_string = "" |
| 111 | if 'parameters' in test.keys(): |
Milosz Wasilewski | 9c00f73 | 2020-08-04 10:29:35 +0100 | [diff] [blame] | 112 | params_string = "_".join(["{0}-{1}".format(param_name, param_value).replace("/", "").replace(" ", "") for param_name, param_value in test['parameters'].items()]) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 113 | test_yaml['params'].update(test['parameters']) |
Nicolas Dechesne | 0d0f5a4 | 2020-09-29 11:26:49 +0200 | [diff] [blame^] | 114 | |
| 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 Wasilewski | 9c00f73 | 2020-08-04 10:29:35 +0100 | [diff] [blame] | 123 | logger.debug("PARAM strings: {}".format(params_string)) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 124 | test_name = "{0}_{1}.html".format(test_yaml['metadata']['name'], params_string) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 125 | if not args.single_output: |
| 126 | test['filename'] = test_name |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 127 | test_path = os.path.join(os.path.abspath(args.output), test_name) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 128 | if args.single_output: |
| 129 | # update test plan object |
| 130 | test.update(test_yaml['run']) |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 131 | # 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 Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 136 | else: |
Milosz Wasilewski | e6f1632 | 2020-08-04 10:30:05 +0100 | [diff] [blame] | 137 | render(test_yaml, templates_dir=args.templates_directory, template=args.test_template_name, name=test_path) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 138 | return not test['missing'] |
| 139 | |
| 140 | |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 141 | def 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 | |
| 176 | def check_coverage(requirement, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 177 | requirement['covered'] = False |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 178 | if 'tests' not in requirement.keys() or requirement['tests'] is None: |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 179 | return |
| 180 | if 'manual' in requirement['tests'].keys() and requirement['tests']['manual'] is not None: |
| 181 | for test in requirement['tests']['manual']: |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 182 | if test_exists(test, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 183 | requirement['covered'] = True |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 184 | if args.csv_name: |
| 185 | add_csv_row(requirement, test, args, True) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 186 | if 'automated' in requirement['tests'].keys() and requirement['tests']['automated'] is not None: |
| 187 | for test in requirement['tests']['automated']: |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 188 | if test_exists(test, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 189 | requirement['covered'] = True |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 190 | if args.csv_name: |
| 191 | add_csv_row(requirement, test, args) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 192 | |
| 193 | |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 194 | def dict_representer(dumper, data): |
| 195 | return dumper.represent_dict(data.iteritems()) |
| 196 | |
| 197 | |
| 198 | def dict_constructor(loader, node): |
| 199 | return PrependOrderedDict(loader.construct_pairs(node)) |
| 200 | |
| 201 | |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 202 | def main(): |
| 203 | parser = ArgumentParser() |
| 204 | parser.add_argument("-f", |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 205 | "--file", |
| 206 | dest="testplan_list", |
| 207 | required=True, |
| 208 | nargs="+", |
| 209 | help="Test plan file to be used") |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 210 | parser.add_argument("-r", |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 211 | "--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 Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 220 | parser.add_argument("-i", |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 221 | "--ignore-clone", |
| 222 | dest="ignore_clone", |
| 223 | action="store_true", |
| 224 | default=False, |
| 225 | help="Ignore cloning repositories and use previously cloned") |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 226 | 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 Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 233 | 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 Wasilewski | e6f1632 | 2020-08-04 10:30:05 +0100 | [diff] [blame] | 238 | 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 Wasilewski | 923ed1b | 2020-08-10 16:15:46 +0100 | [diff] [blame] | 247 | parser.add_argument("--pdf", |
| 248 | default=None, |
| 249 | help="Path to the output pdf file. Only works if output generates HTML") |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 250 | |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 251 | _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 Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 255 | args = parser.parse_args() |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 256 | if not os.path.exists(os.path.abspath(args.output)): |
Milosz Wasilewski | 9c00f73 | 2020-08-04 10:29:35 +0100 | [diff] [blame] | 257 | os.makedirs(os.path.abspath(args.output), mode=0o755) |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 258 | for testplan in args.testplan_list: |
| 259 | if os.path.exists(testplan) and os.path.isfile(testplan): |
| 260 | testplan_file = open(testplan, "r") |
Ryan Harkin | 646fbff | 2020-07-09 22:11:11 +0100 | [diff] [blame] | 261 | tp_obj = yaml.load(testplan_file.read(), Loader=yaml.FullLoader) |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 262 | 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 Wasilewski | d5e1dd9 | 2018-02-12 12:08:44 +0000 | [diff] [blame] | 268 | |
| 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 Wasilewski | f9e70c0 | 2020-09-25 13:19:44 +0100 | [diff] [blame] | 280 | # same filename extension as the template |
| 281 | tp_name = tp_obj['metadata']['name'] + os.path.splitext(args.testplan_template_name)[1] |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 282 | tp_file_name = os.path.join(os.path.abspath(args.output), tp_name) |
Milosz Wasilewski | e6f1632 | 2020-08-04 10:30:05 +0100 | [diff] [blame] | 283 | render(tp_obj, templates_dir=args.templates_directory, template=args.testplan_template_name, name=tp_file_name) |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 284 | testplan_file.close() |
Milosz Wasilewski | 923ed1b | 2020-08-10 16:15:46 +0100 | [diff] [blame] | 285 | if args.pdf is not None: |
| 286 | pdfkit.from_file(tp_file_name, args.pdf) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 287 | # 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 Wasilewski | 6015206 | 2020-03-02 18:53:29 +0000 | [diff] [blame] | 293 | |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 294 | if __name__ == "__main__": |
| 295 | main() |