tests: replace pycodestyle with black
[vpp.git] / src / scripts / fts.py
1 #!/usr/bin/env python3
2
3 import sys
4 import os
5 import os.path
6 import ipaddress
7 import yaml
8 from pprint import pprint
9 import re
10 from jsonschema import validate, exceptions
11 import argparse
12 from subprocess import run, PIPE
13 from io import StringIO
14 import urllib.parse
15
16 # VPP feature JSON schema
17 schema = {
18     "$schema": "http://json-schema.org/schema#",
19     "type": "object",
20     "properties": {
21         "name": {"type": "string"},
22         "description": {"type": "string"},
23         "maintainer": {"$ref": "#/definitions/maintainers"},
24         "state": {
25             "type": "string",
26             "enum": ["production", "experimental", "development"],
27         },
28         "features": {"$ref": "#/definitions/features"},
29         "missing": {"$ref": "#/definitions/features"},
30         "properties": {
31             "type": "array",
32             "items": {"type": "string", "enum": ["API", "CLI", "STATS", "MULTITHREAD"]},
33         },
34     },
35     "additionalProperties": False,
36     "definitions": {
37         "maintainers": {
38             "anyof": [
39                 {
40                     "type": "array",
41                     "items": {"type": "string"},
42                     "minItems": 1,
43                 },
44                 {"type": "string"},
45             ],
46         },
47         "featureobject": {
48             "type": "object",
49             "patternProperties": {
50                 "^.*$": {"$ref": "#/definitions/features"},
51             },
52         },
53         "features": {
54             "type": "array",
55             "items": {
56                 "anyOf": [
57                     {"$ref": "#/definitions/featureobject"},
58                     {"type": "string"},
59                 ]
60             },
61             "minItems": 1,
62         },
63     },
64 }
65
66 DEFAULT_REPO_LINK = "https://github.com/FDio/vpp/blob/master/"
67
68
69 def filelist_from_git_status():
70     filelist = []
71     git_status = "git status --porcelain */FEATURE*.yaml"
72     rv = run(git_status.split(), stdout=PIPE, stderr=PIPE)
73     if rv.returncode != 0:
74         sys.exit(rv.returncode)
75
76     for l in rv.stdout.decode("ascii").split("\n"):
77         if len(l):
78             filelist.append(l.split()[1])
79     return filelist
80
81
82 def filelist_from_git_ls():
83     filelist = []
84     git_ls = "git ls-files :(top)*/FEATURE*.yaml"
85     rv = run(git_ls.split(), stdout=PIPE, stderr=PIPE)
86     if rv.returncode != 0:
87         sys.exit(rv.returncode)
88
89     for l in rv.stdout.decode("ascii").split("\n"):
90         if len(l):
91             filelist.append(l)
92     return filelist
93
94
95 def version_from_git():
96     git_describe = "git describe"
97     rv = run(git_describe.split(), stdout=PIPE, stderr=PIPE)
98     if rv.returncode != 0:
99         sys.exit(rv.returncode)
100     return rv.stdout.decode("ascii").split("\n")[0]
101
102
103 class MarkDown:
104     _dispatch = {}
105
106     def __init__(self, stream):
107         self.stream = stream
108         self.toc = []
109
110     def print_maintainer(self, o):
111         write = self.stream.write
112         if type(o) is list:
113             write("Maintainers: " + ", ".join("{m}".format(m=m) for m in o) + "  \n")
114         else:
115             write("Maintainer: {o}  \n".format(o=o))
116
117     _dispatch["maintainer"] = print_maintainer
118
119     def print_features(self, o, indent=0):
120         write = self.stream.write
121         for f in o:
122             indentstr = " " * indent
123             if type(f) is dict:
124                 for k, v in f.items():
125                     write("{indentstr}- {k}\n".format(indentstr=indentstr, k=k))
126                     self.print_features(v, indent + 2)
127             else:
128                 write("{indentstr}- {f}\n".format(indentstr=indentstr, f=f))
129         write("\n")
130
131     _dispatch["features"] = print_features
132
133     def print_markdown_header(self, o):
134         write = self.stream.write
135         write("## {o}\n".format(o=o))
136
137     _dispatch["markdown_header"] = print_markdown_header
138
139     def print_name(self, o):
140         write = self.stream.write
141         write("### {o}\n".format(o=o))
142         self.toc.append(o)
143
144     _dispatch["name"] = print_name
145
146     def print_description(self, o):
147         write = self.stream.write
148         write("\n{o}\n\n".format(o=o))
149
150     _dispatch["description"] = print_description
151
152     def print_state(self, o):
153         write = self.stream.write
154         write("Feature maturity level: {o}  \n".format(o=o))
155
156     _dispatch["state"] = print_state
157
158     def print_properties(self, o):
159         write = self.stream.write
160         write("Supports: {s}  \n".format(s=" ".join(o)))
161
162     _dispatch["properties"] = print_properties
163
164     def print_missing(self, o):
165         write = self.stream.write
166         write("\nNot yet implemented:  \n")
167         self.print_features(o)
168
169     _dispatch["missing"] = print_missing
170
171     def print_code(self, o):
172         write = self.stream.write
173         write("Source Code: [{o}]({o}) \n".format(o=o))
174
175     _dispatch["code"] = print_code
176
177     def print(self, t, o):
178         write = self.stream.write
179         if t in self._dispatch:
180             self._dispatch[t](
181                 self,
182                 o,
183             )
184         else:
185             write("NOT IMPLEMENTED: {t}\n")
186
187
188 def output_toc(toc, stream):
189     write = stream.write
190     write("# VPP Supported Features\n")
191
192     for t in toc:
193         ref = t.lower().replace(" ", "-")
194         write("[{t}](#{ref})  \n".format(t=t, ref=ref))
195
196
197 def featuresort(k):
198     return k[1]["name"]
199
200
201 def featurelistsort(k):
202     orderedfields = {
203         "name": 0,
204         "maintainer": 1,
205         "description": 2,
206         "features": 3,
207         "state": 4,
208         "properties": 5,
209         "missing": 6,
210         "code": 7,
211     }
212     return orderedfields[k[0]]
213
214
215 def output_markdown(features, fields, notfields, repository_url):
216     stream = StringIO()
217     m = MarkDown(stream)
218     m.print("markdown_header", "Feature Details:")
219     for path, featuredef in sorted(features.items(), key=featuresort):
220         codeurl = urllib.parse.urljoin(repository_url, os.path.dirname(path))
221         featuredef["code"] = codeurl
222         for k, v in sorted(featuredef.items(), key=featurelistsort):
223             if notfields:
224                 if k not in notfields:
225                     m.print(k, v)
226             elif fields:
227                 if k in fields:
228                     m.print(k, v)
229             else:
230                 m.print(k, v)
231
232     tocstream = StringIO()
233     output_toc(m.toc, tocstream)
234     return tocstream, stream
235
236
237 def main():
238     parser = argparse.ArgumentParser(description="VPP Feature List.")
239     parser.add_argument(
240         "--validate",
241         dest="validate",
242         action="store_true",
243         help="validate the FEATURE.yaml file",
244     )
245     parser.add_argument(
246         "--repolink",
247         metavar="repolink",
248         default=DEFAULT_REPO_LINK,
249         help="Link to public repository [%s]" % DEFAULT_REPO_LINK,
250     )
251     parser.add_argument(
252         "--git-status",
253         dest="git_status",
254         action="store_true",
255         help="Get filelist from git status",
256     )
257     parser.add_argument(
258         "--all",
259         dest="all",
260         action="store_true",
261         help="Validate all files in repository",
262     )
263     parser.add_argument(
264         "--markdown",
265         dest="markdown",
266         action="store_true",
267         help="Output feature table in markdown",
268     )
269     parser.add_argument(
270         "infile", nargs="?", type=argparse.FileType("r"), default=sys.stdin
271     )
272     group = parser.add_mutually_exclusive_group()
273     group.add_argument("--include", help="List of fields to include")
274     group.add_argument("--exclude", help="List of fields to exclude")
275     args = parser.parse_args()
276     features = {}
277
278     if args.git_status:
279         filelist = filelist_from_git_status()
280     elif args.all:
281         filelist = filelist_from_git_ls()
282     else:
283         filelist = args.infile
284
285     if args.include:
286         fields = args.include.split(",")
287     else:
288         fields = []
289     if args.exclude:
290         notfields = args.exclude.split(",")
291     else:
292         notfields = []
293
294     for featurefile in filelist:
295         featurefile = featurefile.rstrip()
296
297         # Load configuration file
298         with open(featurefile, encoding="utf-8") as f:
299             cfg = yaml.load(f, Loader=yaml.SafeLoader)
300         try:
301             validate(instance=cfg, schema=schema)
302         except exceptions.ValidationError:
303             print(
304                 "File does not validate: {featurefile}".format(featurefile=featurefile),
305                 file=sys.stderr,
306             )
307             raise
308         features[featurefile] = cfg
309
310     if args.markdown:
311         stream = StringIO()
312         tocstream, stream = output_markdown(features, fields, notfields, args.repolink)
313         print(tocstream.getvalue())
314         print(stream.getvalue())
315         stream.close()
316
317
318 if __name__ == "__main__":
319     main()