1 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 # Licensed under the Apache License, Version 2.0 (the "License").
4 # You may not use this file except in compliance with the License.
5 # A copy of the License is located at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # or in the "license" file accompanying this file. This file is distributed
10 # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11 # express or implied. See the License for the specific language governing
12 # permissions and limitations under the License.
22 import lib.litani_report
34 string = re.sub(match, repl, string)
38 def html_escape(string):
48 string = re.sub(match, repl, string)
52 class DependencyNode(Node):
53 def __init__(self, fyle, line_width=40, **style):
57 path = pathlib.Path(fyle)
58 _, ext = os.path.splitext(path.name)
62 path_name = "\n".join(textwrap.wrap(path.name, width=line_width))
63 self.style["label"] = f"{path_name}{ext}"
70 def __eq__(self, other):
71 return self.id == other.id
75 return '"{id}" [{style}];'.format(
76 id=self.id, style=",".join([
77 f'"{key}"="{Node.escape(value)}"'
78 for key, value in self.style.items()]))
82 class CommandNode(Node):
84 self, pipeline_name, description, command, line_width=40, **style):
85 self.id = hash(command)
86 self.pipeline_name = '<BR/>'.join(
87 textwrap.wrap(Node.html_escape(pipeline_name), width=line_width))
89 self.description = '<BR/>'.join(
90 textwrap.wrap(Node.html_escape(description), width=line_width))
93 self.command = '<BR/>'.join(
94 textwrap.wrap(Node.html_escape(command), width=line_width))
97 self.style["shape"] = "plain"
105 def __eq__(self, other):
106 return self.id == other.id
111 desc_cell = f"\n<TD><B>{self.description}</B></TD>"
114 return '''"{id}" [label=<
115 <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
117 <TD><B>{pipeline_name}</B></TD>{desc_cell}
120 <TD COLSPAN="2">{command}</TD>
122 </TABLE>> {style}];'''.format(
123 id=self.id, command=self.command,
125 pipeline_name=self.pipeline_name,
127 f'{key}="{Node.escape(value)}"'
128 for key, value in self.style.items()]))
133 def __init__(self, src, dst, **style):
138 def __eq__(self, other):
139 return all((self.src == other.src, self.dst == other.dst))
143 return hash(self.src) + hash(self.dst)
147 return '"{src}" -> "{dst}" [{style}];'.format(
148 src=self.src.id, dst=self.dst.id,
150 f'"{key}"="{value}"' for key, value in self.style.items()]))
154 @dataclasses.dataclass
155 class SinglePipelineGraph:
157 nodes: set = dataclasses.field(default_factory=set)
158 edges: set = dataclasses.field(default_factory=set)
162 for stage in self.pipe["ci_stages"]:
163 for job in stage["jobs"]:
168 for job in self.iter_jobs():
169 args = job["wrapper_arguments"]
170 cmd_node = self._make_cmd_node(
171 job["complete"], job.get("outcome", None),
172 args["pipeline_name"], args["description"], args["command"])
173 self.nodes.add(cmd_node)
175 for inputt in args.get("inputs") or []:
176 in_node = lib.graph.DependencyNode(inputt)
177 self.nodes.add(in_node)
178 self.edges.add(lib.graph.Edge(src=in_node, dst=cmd_node))
180 for output in args.get("outputs") or []:
181 out_node = lib.graph.DependencyNode(output)
182 self.nodes.add(out_node)
183 self.edges.add(lib.graph.Edge(src=cmd_node, dst=out_node))
187 def _make_cmd_node(complete, outcome, pipeline_name, description, command):
188 cmd_style = {"style": "filled"}
189 if complete and outcome == "success":
190 cmd_style["fillcolor"] = "#90caf9"
191 elif complete and outcome == "fail_ignored":
192 cmd_style["fillcolor"] = "#ffecb3"
193 elif complete and outcome == "fail":
194 cmd_style["fillcolor"] = "#ef9a9a"
196 raise RuntimeError("Unknown outcome '%s'" % outcome)
198 cmd_style["fillcolor"] = "#eceff1"
199 return lib.graph.CommandNode(
200 pipeline_name, description, command, **cmd_style)
204 buf = ["digraph G {"]
205 buf.append('bgcolor="transparent"')
206 buf.extend([(" %s" % str(n)) for n in self.nodes])
207 buf.extend([(" %s" % str(e)) for e in self.edges])
209 return "\n".join(buf)
214 spg = SinglePipelineGraph(pipe)
220 @dataclasses.dataclass
221 class PipelineChooser:
225 def should_skip(self, pipeline):
226 return self.pipelines and not pipeline in self.pipelines
230 @dataclasses.dataclass
233 pipeline_chooser: PipelineChooser
237 for pipe in self.run["pipelines"]:
238 if self.pipeline_chooser.should_skip(pipe["name"]):
240 for stage in pipe["ci_stages"]:
241 for job in stage["jobs"]:
246 buf = ["digraph G {"]
251 for job in self.iter_jobs():
252 args = job["wrapper_arguments"]
253 cmd_node = CommandNode(
254 args["pipeline_name"], args["description"], args["command"])
257 for output in args["outputs"]:
258 out_node = DependencyNode(output)
260 edges.add(Edge(src=cmd_node, dst=out_node))
262 for inputt in args["inputs"]:
263 in_node = DependencyNode(inputt)
265 edges.add(Edge(src=in_node, dst=cmd_node))
267 buf.extend([(" %s" % str(n)) for n in nodes])
268 buf.extend([(" %s" % str(e)) for e in edges])
270 return "\n".join(buf)
274 async def print_graph(args):
275 lib.litani.add_jobs_to_cache()
276 run = lib.litani_report.get_run_data(lib.litani.get_cache_dir())
278 pc = PipelineChooser(args.pipelines)
280 graph = Graph(run=run, pipeline_chooser=pc)