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']]) |
Milosz Wasilewski | 9a83b8e | 2020-07-10 11:43:33 +0100 | [diff] [blame^] | 89 | elif 'branch' in test.keys(): |
| 90 | subprocess.call(['git', 'checkout', test['branch']]) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 91 | else: |
| 92 | # if no revision is specified, use current HEAD |
| 93 | output = subprocess.check_output(['git', 'rev-parse', 'HEAD']) |
| 94 | test['revision'] = output |
| 95 | |
| 96 | if not os.path.exists(test_file_path) or not os.path.isfile(test_file_path): |
| 97 | test['missing'] = True |
| 98 | os.chdir(current_dir) |
| 99 | return not test['missing'] |
| 100 | test['missing'] = False |
| 101 | # open the file and render the test |
| 102 | subprocess.call(['git', 'checkout', 'master']) |
| 103 | print current_dir |
| 104 | os.chdir(current_dir) |
| 105 | print os.getcwd() |
| 106 | test_file = open(test_file_path, "r") |
| 107 | test_yaml = yaml.load(test_file.read()) |
| 108 | params_string = "" |
| 109 | if 'parameters' in test.keys(): |
| 110 | params_string = "_".join(["{0}-{1}".format(param_name, param_value).replace("/", "").replace(" ", "") for param_name, param_value in test['parameters'].iteritems()]) |
| 111 | test_yaml['params'].update(test['parameters']) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 112 | if args.single_output: |
| 113 | # update parameters in test |
| 114 | if 'params' in test_yaml.keys(): |
| 115 | for param_name, param_value in test_yaml['params'].iteritems(): |
| 116 | if param_name not in test['parameters'].keys(): |
| 117 | test['parameters'].update({param_name: param_value}) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 118 | print params_string |
| 119 | test_name = "{0}_{1}.html".format(test_yaml['metadata']['name'], params_string) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 120 | if not args.single_output: |
| 121 | test['filename'] = test_name |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 122 | test_path = os.path.join(os.path.abspath(args.output), test_name) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 123 | if args.single_output: |
| 124 | # update test plan object |
| 125 | test.update(test_yaml['run']) |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 126 | # prepend in reversed order so 'name' is on top |
| 127 | test.prepend("os", test_yaml['metadata']['os']) |
| 128 | test.prepend("scope", test_yaml['metadata']['scope']) |
| 129 | test.prepend("description", test_yaml['metadata']['description']) |
| 130 | test.prepend("name", test_yaml['metadata']['name']) |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 131 | else: |
| 132 | render(test_yaml, template="test.html", name=test_path) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 133 | return not test['missing'] |
| 134 | |
| 135 | |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 136 | def add_csv_row(requirement, test, args, manual=False): |
| 137 | fieldnames = [ |
| 138 | "req_name", |
| 139 | "req_owner", |
| 140 | "req_category", |
| 141 | "path", |
| 142 | "repository", |
| 143 | "revision", |
| 144 | "parameters", |
| 145 | "mandatory", |
| 146 | "kind", |
| 147 | ] |
| 148 | csv_file_path = os.path.join(os.path.abspath(args.output), args.csv_name) |
| 149 | has_header = False |
| 150 | if os.path.isfile(csv_file_path): |
| 151 | has_header = True |
| 152 | with open(csv_file_path, "ab+") as csv_file: |
| 153 | csvdict = DictWriter(csv_file, fieldnames=fieldnames) |
| 154 | if not has_header: |
| 155 | csvdict.writeheader() |
| 156 | csvdict.writerow( |
| 157 | { |
| 158 | "req_name": requirement.get('name'), |
| 159 | "req_owner": requirement.get('owner'), |
| 160 | "req_category": requirement.get('category'), |
| 161 | "path": test.get('path'), |
| 162 | "repository": test.get('repository'), |
| 163 | "revision": test.get('revision'), |
| 164 | "parameters": test.get('parameters'), |
| 165 | "mandatory": test.get('mandatory'), |
| 166 | "kind": "manual" if manual else "automated", |
| 167 | } |
| 168 | ) |
| 169 | |
| 170 | |
| 171 | def check_coverage(requirement, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 172 | requirement['covered'] = False |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 173 | if 'tests' not in requirement.keys() or requirement['tests'] is None: |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 174 | return |
| 175 | if 'manual' in requirement['tests'].keys() and requirement['tests']['manual'] is not None: |
| 176 | for test in requirement['tests']['manual']: |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 177 | if test_exists(test, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 178 | requirement['covered'] = True |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 179 | if args.csv_name: |
| 180 | add_csv_row(requirement, test, args, True) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 181 | if 'automated' in requirement['tests'].keys() and requirement['tests']['automated'] is not None: |
| 182 | for test in requirement['tests']['automated']: |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 183 | if test_exists(test, repositories, args): |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 184 | requirement['covered'] = True |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 185 | if args.csv_name: |
| 186 | add_csv_row(requirement, test, args) |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 187 | |
| 188 | |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 189 | def dict_representer(dumper, data): |
| 190 | return dumper.represent_dict(data.iteritems()) |
| 191 | |
| 192 | |
| 193 | def dict_constructor(loader, node): |
| 194 | return PrependOrderedDict(loader.construct_pairs(node)) |
| 195 | |
| 196 | |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 197 | def main(): |
| 198 | parser = ArgumentParser() |
| 199 | parser.add_argument("-f", |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 200 | "--file", |
| 201 | dest="testplan_list", |
| 202 | required=True, |
| 203 | nargs="+", |
| 204 | help="Test plan file to be used") |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 205 | parser.add_argument("-r", |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 206 | "--repositories", |
| 207 | dest="repository_path", |
| 208 | default="repositories", |
| 209 | help="Test plan file to be used") |
| 210 | parser.add_argument("-o", |
| 211 | "--output", |
| 212 | dest="output", |
| 213 | default="output", |
| 214 | help="Destination directory for generated files") |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 215 | parser.add_argument("-i", |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 216 | "--ignore-clone", |
| 217 | dest="ignore_clone", |
| 218 | action="store_true", |
| 219 | default=False, |
| 220 | help="Ignore cloning repositories and use previously cloned") |
Milosz Wasilewski | 807c0ea | 2018-02-13 19:14:01 +0000 | [diff] [blame] | 221 | parser.add_argument("-s", |
| 222 | "--single-file-output", |
| 223 | dest="single_output", |
| 224 | action="store_true", |
| 225 | default=False, |
| 226 | help="""Render test plan into single HTML file. This option ignores |
| 227 | any metadata that is available in test cases""") |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 228 | parser.add_argument("-c", |
| 229 | "--csv", |
| 230 | dest="csv_name", |
| 231 | required=False, |
| 232 | 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] | 233 | |
Milosz Wasilewski | 68279b2 | 2018-02-21 12:41:02 +0000 | [diff] [blame] | 234 | _mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG |
| 235 | yaml.add_representer(PrependOrderedDict, dict_representer) |
| 236 | yaml.add_constructor(_mapping_tag, dict_constructor) |
| 237 | |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 238 | args = parser.parse_args() |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 239 | if not os.path.exists(os.path.abspath(args.output)): |
| 240 | os.makedirs(os.path.abspath(args.output), 0755) |
| 241 | for testplan in args.testplan_list: |
| 242 | if os.path.exists(testplan) and os.path.isfile(testplan): |
| 243 | testplan_file = open(testplan, "r") |
| 244 | tp_obj = yaml.load(testplan_file.read()) |
| 245 | repo_list = repository_list(tp_obj) |
| 246 | repositories = {} |
| 247 | for repo in repo_list: |
| 248 | repo_url, repo_path = clone_repository(repo, args.repository_path, args.ignore_clone) |
| 249 | repositories.update({repo_url: repo_path}) |
| 250 | # ToDo: check test plan structure |
Milosz Wasilewski | d5e1dd9 | 2018-02-12 12:08:44 +0000 | [diff] [blame] | 251 | |
| 252 | tp_version = tp_obj['metadata']['format'] |
| 253 | if tp_version == "Linaro Test Plan v1": |
| 254 | for requirement in tp_obj['requirements']: |
| 255 | check_coverage(requirement, repositories, args) |
| 256 | if tp_version == "Linaro Test Plan v2": |
| 257 | if 'manual' in tp_obj['tests'].keys() and tp_obj['tests']['manual'] is not None: |
| 258 | for test in tp_obj['tests']['manual']: |
| 259 | test_exists(test, repositories, args) |
| 260 | if 'automated' in tp_obj['tests'].keys() and tp_obj['tests']['automated'] is not None: |
| 261 | for test in tp_obj['tests']['automated']: |
| 262 | test_exists(test, repositories, args) |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 263 | tp_name = tp_obj['metadata']['name'] + ".html" |
| 264 | 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] | 265 | if tp_version == "Linaro Test Plan v1": |
| 266 | render(tp_obj, name=tp_file_name) |
| 267 | if tp_version == "Linaro Test Plan v2": |
| 268 | render(tp_obj, name=tp_file_name, template="testplan_v2.html") |
Milosz Wasilewski | b504541 | 2016-12-07 11:27:10 +0000 | [diff] [blame] | 269 | testplan_file.close() |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 270 | # go through requiremets and for each test: |
| 271 | # - if file exists render test as separate html file |
| 272 | # - if file is missing, indicate missing test (red) |
| 273 | # render test plan with links to test files |
| 274 | # add option to render as single file (for pdf generation) |
| 275 | |
Milosz Wasilewski | 6015206 | 2020-03-02 18:53:29 +0000 | [diff] [blame] | 276 | |
Milosz Wasilewski | f5ccdbd | 2016-10-25 18:49:20 +0100 | [diff] [blame] | 277 | if __name__ == "__main__": |
| 278 | main() |