Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 1 | import collections |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 2 | import os |
| 3 | import subprocess |
| 4 | import yaml |
| 5 | from argparse import ArgumentParser |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 6 | from csv import DictWriter |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 7 | from jinja2 import Environment, FileSystemLoader |
| 8 | |
| 9 | |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 10 | class PrependOrderedDict(collections.OrderedDict): |
| 11 | |
| 12 | def prepend(self, key, value, dict_setitem=dict.__setitem__): |
| 13 | |
| 14 | root = self._OrderedDict__root |
| 15 | first = root[1] |
| 16 | |
| 17 | if key in self: |
| 18 | link = self._OrderedDict__map[key] |
| 19 | link_prev, link_next, _ = link |
| 20 | link_prev[1] = link_next |
| 21 | link_next[0] = link_prev |
| 22 | link[0] = root |
| 23 | link[1] = first |
| 24 | root[1] = first[0] = link |
| 25 | else: |
| 26 | root[1] = first[0] = self._OrderedDict__map[key] = [root, first, key] |
| 27 | dict_setitem(self, key, value) |
| 28 | |
| 29 | |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 30 | def render(obj, template="testplan.html", name=None): |
| 31 | if name is None: |
| 32 | name = template |
Milosz Wasilewski | f761ab9 | 2018-02-13 11:21:18 +0000 | [diff] [blame] | 33 | templates_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") |
| 34 | _env = Environment(loader=FileSystemLoader(templates_dir)) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 35 | _template = _env.get_template(template) |
| 36 | _obj = _template.render(obj=obj) |
| 37 | with open("{}".format(name), "wb") as _file: |
| 38 | _file.write(_obj.encode('utf-8')) |
| 39 | |
| 40 | |
| 41 | # get list of repositories and cache them |
| 42 | def repository_list(testplan): |
| 43 | repositories = set() |
Milosz Wasilewski | d5e1dd9 | 2018-02-12 12:08:44 +0000 | [diff] [blame] | 44 | tp_version = testplan['metadata']['format'] |
| 45 | if tp_version == "Linaro Test Plan v2": |
| 46 | if 'manual' in testplan['tests'].keys() and testplan['tests']['manual'] is not None: |
| 47 | for test in testplan['tests']['manual']: |
| 48 | repositories.add(test['repository']) |
| 49 | |
| 50 | if 'automated' in testplan['tests'].keys() and testplan['tests']['automated'] is not None: |
| 51 | for test in testplan['tests']['automated']: |
| 52 | repositories.add(test['repository']) |
| 53 | if tp_version == "Linaro Test Plan v1": |
| 54 | for req in testplan['requirements']: |
| 55 | if 'tests' in req.keys() and req['tests'] is not None: |
| 56 | if 'manual' in req['tests'].keys() and req['tests']['manual'] is not None: |
| 57 | for test in req['tests']['manual']: |
| 58 | repositories.add(test['repository']) |
| 59 | if 'automated' in req['tests'].keys() and req['tests']['automated'] is not None: |
| 60 | for test in req['tests']['automated']: |
| 61 | repositories.add(test['repository']) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 62 | return repositories |
| 63 | |
| 64 | |
| 65 | def clone_repository(repository_url, base_path, ignore=False): |
| 66 | path_suffix = repository_url.rsplit("/", 1)[1] |
| 67 | if path_suffix.endswith(".git"): |
| 68 | path_suffix = path_suffix[:-4] |
| 69 | |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 70 | path = os.path.abspath(os.path.join(base_path, path_suffix)) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 71 | if os.path.exists(path) and ignore: |
| 72 | return(repository_url, path) |
| 73 | # git clone repository_url |
| 74 | subprocess.call(['git', 'clone', repository_url, path]) |
| 75 | # return tuple (repository_url, system_path) |
| 76 | return (repository_url, path) |
| 77 | |
| 78 | |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 79 | def test_exists(test, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 80 | test_file_path = os.path.join( |
| 81 | repositories[test['repository']], |
| 82 | test['path'] |
| 83 | ) |
| 84 | current_dir = os.getcwd() |
| 85 | print current_dir |
| 86 | os.chdir(repositories[test['repository']]) |
| 87 | if 'revision' in test.keys(): |
| 88 | subprocess.call(['git', 'checkout', test['revision']]) |
| 89 | else: |
| 90 | # if no revision is specified, use current HEAD |
| 91 | output = subprocess.check_output(['git', 'rev-parse', 'HEAD']) |
| 92 | test['revision'] = output |
| 93 | |
| 94 | if not os.path.exists(test_file_path) or not os.path.isfile(test_file_path): |
| 95 | test['missing'] = True |
| 96 | os.chdir(current_dir) |
| 97 | return not test['missing'] |
| 98 | test['missing'] = False |
| 99 | # open the file and render the test |
| 100 | subprocess.call(['git', 'checkout', 'master']) |
| 101 | print current_dir |
| 102 | os.chdir(current_dir) |
| 103 | print os.getcwd() |
| 104 | test_file = open(test_file_path, "r") |
| 105 | test_yaml = yaml.load(test_file.read()) |
| 106 | params_string = "" |
| 107 | if 'parameters' in test.keys(): |
| 108 | params_string = "_".join(["{0}-{1}".format(param_name, param_value).replace("/", "").replace(" ", "") for param_name, param_value in test['parameters'].iteritems()]) |
| 109 | test_yaml['params'].update(test['parameters']) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 110 | if args.single_output: |
| 111 | # update parameters in test |
| 112 | if 'params' in test_yaml.keys(): |
| 113 | for param_name, param_value in test_yaml['params'].iteritems(): |
| 114 | if param_name not in test['parameters'].keys(): |
| 115 | test['parameters'].update({param_name: param_value}) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 116 | print params_string |
| 117 | test_name = "{0}_{1}.html".format(test_yaml['metadata']['name'], params_string) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 118 | if not args.single_output: |
| 119 | test['filename'] = test_name |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 120 | test_path = os.path.join(os.path.abspath(args.output), test_name) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 121 | if args.single_output: |
| 122 | # update test plan object |
| 123 | test.update(test_yaml['run']) |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 124 | # prepend in reversed order so 'name' is on top |
| 125 | test.prepend("os", test_yaml['metadata']['os']) |
| 126 | test.prepend("scope", test_yaml['metadata']['scope']) |
| 127 | test.prepend("description", test_yaml['metadata']['description']) |
| 128 | test.prepend("name", test_yaml['metadata']['name']) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 129 | else: |
| 130 | render(test_yaml, template="test.html", name=test_path) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 131 | return not test['missing'] |
| 132 | |
| 133 | |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 134 | def add_csv_row(requirement, test, args, manual=False): |
| 135 | fieldnames = [ |
| 136 | "req_name", |
| 137 | "req_owner", |
| 138 | "req_category", |
| 139 | "path", |
| 140 | "repository", |
| 141 | "revision", |
| 142 | "parameters", |
| 143 | "mandatory", |
| 144 | "kind", |
| 145 | ] |
| 146 | csv_file_path = os.path.join(os.path.abspath(args.output), args.csv_name) |
| 147 | has_header = False |
| 148 | if os.path.isfile(csv_file_path): |
| 149 | has_header = True |
| 150 | with open(csv_file_path, "ab+") as csv_file: |
| 151 | csvdict = DictWriter(csv_file, fieldnames=fieldnames) |
| 152 | if not has_header: |
| 153 | csvdict.writeheader() |
| 154 | csvdict.writerow( |
| 155 | { |
| 156 | "req_name": requirement.get('name'), |
| 157 | "req_owner": requirement.get('owner'), |
| 158 | "req_category": requirement.get('category'), |
| 159 | "path": test.get('path'), |
| 160 | "repository": test.get('repository'), |
| 161 | "revision": test.get('revision'), |
| 162 | "parameters": test.get('parameters'), |
| 163 | "mandatory": test.get('mandatory'), |
| 164 | "kind": "manual" if manual else "automated", |
| 165 | } |
| 166 | ) |
| 167 | |
| 168 | |
| 169 | def check_coverage(requirement, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 170 | requirement['covered'] = False |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 171 | if 'tests' not in requirement.keys() or requirement['tests'] is None: |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 172 | return |
| 173 | if 'manual' in requirement['tests'].keys() and requirement['tests']['manual'] is not None: |
| 174 | for test in requirement['tests']['manual']: |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 175 | if test_exists(test, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 176 | requirement['covered'] = True |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 177 | if args.csv_name: |
| 178 | add_csv_row(requirement, test, args, True) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 179 | if 'automated' in requirement['tests'].keys() and requirement['tests']['automated'] is not None: |
| 180 | for test in requirement['tests']['automated']: |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 181 | if test_exists(test, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 182 | requirement['covered'] = True |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 183 | if args.csv_name: |
| 184 | add_csv_row(requirement, test, args) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 185 | |
| 186 | |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 187 | def dict_representer(dumper, data): |
| 188 | return dumper.represent_dict(data.iteritems()) |
| 189 | |
| 190 | |
| 191 | def dict_constructor(loader, node): |
| 192 | return PrependOrderedDict(loader.construct_pairs(node)) |
| 193 | |
| 194 | |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 195 | def main(): |
| 196 | parser = ArgumentParser() |
| 197 | parser.add_argument("-f", |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 198 | "--file", |
| 199 | dest="testplan_list", |
| 200 | required=True, |
| 201 | nargs="+", |
| 202 | help="Test plan file to be used") |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 203 | parser.add_argument("-r", |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 204 | "--repositories", |
| 205 | dest="repository_path", |
| 206 | default="repositories", |
| 207 | help="Test plan file to be used") |
| 208 | parser.add_argument("-o", |
| 209 | "--output", |
| 210 | dest="output", |
| 211 | default="output", |
| 212 | help="Destination directory for generated files") |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 213 | parser.add_argument("-i", |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 214 | "--ignore-clone", |
| 215 | dest="ignore_clone", |
| 216 | action="store_true", |
| 217 | default=False, |
| 218 | help="Ignore cloning repositories and use previously cloned") |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 219 | parser.add_argument("-s", |
| 220 | "--single-file-output", |
| 221 | dest="single_output", |
| 222 | action="store_true", |
| 223 | default=False, |
| 224 | help="""Render test plan into single HTML file. This option ignores |
| 225 | any metadata that is available in test cases""") |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 226 | parser.add_argument("-c", |
| 227 | "--csv", |
| 228 | dest="csv_name", |
| 229 | required=False, |
| 230 | help="Name of CSV to store overall list of requirements and test. If name is absent, the file will not be generated") |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 231 | |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 232 | _mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG |
| 233 | yaml.add_representer(PrependOrderedDict, dict_representer) |
| 234 | yaml.add_constructor(_mapping_tag, dict_constructor) |
| 235 | |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 236 | args = parser.parse_args() |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 237 | if not os.path.exists(os.path.abspath(args.output)): |
| 238 | os.makedirs(os.path.abspath(args.output), 0755) |
| 239 | for testplan in args.testplan_list: |
| 240 | if os.path.exists(testplan) and os.path.isfile(testplan): |
| 241 | testplan_file = open(testplan, "r") |
| 242 | tp_obj = yaml.load(testplan_file.read()) |
| 243 | repo_list = repository_list(tp_obj) |
| 244 | repositories = {} |
| 245 | for repo in repo_list: |
| 246 | repo_url, repo_path = clone_repository(repo, args.repository_path, args.ignore_clone) |
| 247 | repositories.update({repo_url: repo_path}) |
| 248 | # ToDo: check test plan structure |
Milosz Wasilewski | d5e1dd9 | 2018-02-12 12:08:44 +0000 | [diff] [blame] | 249 | |
| 250 | tp_version = tp_obj['metadata']['format'] |
| 251 | if tp_version == "Linaro Test Plan v1": |
| 252 | for requirement in tp_obj['requirements']: |
| 253 | check_coverage(requirement, repositories, args) |
| 254 | if tp_version == "Linaro Test Plan v2": |
| 255 | if 'manual' in tp_obj['tests'].keys() and tp_obj['tests']['manual'] is not None: |
| 256 | for test in tp_obj['tests']['manual']: |
| 257 | test_exists(test, repositories, args) |
| 258 | if 'automated' in tp_obj['tests'].keys() and tp_obj['tests']['automated'] is not None: |
| 259 | for test in tp_obj['tests']['automated']: |
| 260 | test_exists(test, repositories, args) |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 261 | tp_name = tp_obj['metadata']['name'] + ".html" |
| 262 | tp_file_name = os.path.join(os.path.abspath(args.output), tp_name) |
Milosz Wasilewski | d5e1dd9 | 2018-02-12 12:08:44 +0000 | [diff] [blame] | 263 | if tp_version == "Linaro Test Plan v1": |
| 264 | render(tp_obj, name=tp_file_name) |
| 265 | if tp_version == "Linaro Test Plan v2": |
| 266 | render(tp_obj, name=tp_file_name, template="testplan_v2.html") |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 267 | testplan_file.close() |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 268 | # go through requiremets and for each test: |
| 269 | # - if file exists render test as separate html file |
| 270 | # - if file is missing, indicate missing test (red) |
| 271 | # render test plan with links to test files |
| 272 | # add option to render as single file (for pdf generation) |
| 273 | |
Milosz Wasilewski | 6015206 | 2020-03-02 18:53:29 +0000 | [diff] [blame] | 274 | |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 275 | if __name__ == "__main__": |
| 276 | main() |