1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 from perfdocs
.logger
import PerfDocLogger
10 from perfdocs
.utils
import (
19 logger
= PerfDocLogger()
22 class Generator(object):
24 After each perfdocs directory was validated, the generator uses the templates
25 for each framework, fills them with the test descriptions in config and saves
26 the perfdocs in the form index.rst as index file and suite_name.rst for
27 each suite of tests in the framework.
30 def __init__(self
, verifier
, workspace
, generate
=False):
32 Initialize the Generator.
34 :param verifier: Verifier object. It should not be a fresh Verifier object,
35 but an initialized one with validate_tree() method already called
36 :param workspace: Path to the top-level checkout directory.
37 :param generate: Flag for generating the documentation
39 self
._workspace
= workspace
40 if not self
._workspace
:
41 raise Exception("PerfDocs Generator requires a workspace directory.")
42 # Template documents without added information reside here
43 self
.templates_path
= pathlib
.Path(
44 self
._workspace
, "tools", "lint", "perfdocs", "templates"
46 self
.perfdocs_path
= pathlib
.Path(
47 self
._workspace
, "testing", "perfdocs", "generated"
50 self
._generate
= generate
51 self
._verifier
= verifier
52 self
._perfdocs
_tree
= self
._verifier
._gatherer
.perfdocs_tree
54 def build_perfdocs_from_tree(self
):
56 Builds up a document for each framework that was found.
58 :return dict: A dictionary containing a mapping from each framework
59 to the document that was built for it, i.e:
61 framework_name: framework_document,
66 # Using the verified `perfdocs_tree`, build up the documentation.
68 for framework
in self
._perfdocs
_tree
:
69 yaml_content
= read_yaml(pathlib
.Path(framework
["path"], framework
["yml"]))
70 rst_content
= read_file(
71 pathlib
.Path(framework
["path"], framework
["rst"]), stringify
=True
73 gatherer
= self
._verifier
._gatherer
.framework_gatherers
[
77 metrics_rst_content
= None
78 metrics_info
= self
._verifier
.metrics_info
[yaml_content
["name"]]
79 if framework
.get("metrics"):
80 metrics_rst_content
= read_file(
81 pathlib
.Path(framework
["path"], framework
["metrics"]),
85 # Gather all tests and descriptions and format them into
86 # documentation content
88 suites
= yaml_content
["suites"]
89 for suite_name
in sorted(suites
.keys()):
90 suite_info
= suites
[suite_name
]
92 # Add the suite section
94 self
._verifier
._gatherer
.framework_gatherers
[
96 ].build_suite_section(suite_name
, suite_info
["description"])
99 tests
= suite_info
.get("tests", {})
100 for test_name
in sorted(tests
.keys()):
101 test_description
= gatherer
.build_test_description(
103 test_description
=tests
[test_name
],
104 suite_name
=suite_name
,
105 metrics_info
=metrics_info
,
107 documentation
.extend(test_description
)
108 documentation
.append("")
110 # For each framework, we may want to organize the metrics differently
111 # so delegate the complete setup of the metric documentation to the
112 # framework-specific gatherers
114 if metrics_rst_content
:
115 metrics_documentation
= gatherer
.build_metrics_documentation(
118 metrics_rst
= re
.sub(
119 r
"{metrics_documentation}",
120 "\n".join(metrics_documentation
),
123 rst_content
= re
.sub(
124 r
"{metrics_rst_name}",
125 f
"{yaml_content['name']}-metrics",
129 # Insert documentation into `.rst` file
130 framework_rst
= re
.sub(
131 r
"{documentation}", "\n".join(documentation
), rst_content
133 frameworks_info
[yaml_content
["name"]] = {
134 "dynamic": framework_rst
,
135 "metrics": metrics_rst
,
139 # For static `.rst` file
140 for static_file
in framework
["static"]:
141 if static_file
.endswith("rst"):
142 frameworks_info
[yaml_content
["name"]]["static"].append(
145 "content": read_file(
146 pathlib
.Path(framework
["path"], static_file
),
152 frameworks_info
[yaml_content
["name"]]["static"].append(
155 "content": pathlib
.Path(framework
["path"], static_file
),
159 return frameworks_info
161 def _create_temp_dir(self
):
163 Create a temp directory as preparation of saving the documentation tree.
164 :return: str the location of perfdocs_tmpdir
166 # Build the directory that will contain the final result (a tmp dir
167 # that will be moved to the final location afterwards)
169 tmpdir
= pathlib
.Path(tempfile
.mkdtemp())
170 perfdocs_tmpdir
= pathlib
.Path(tmpdir
, "generated")
171 perfdocs_tmpdir
.mkdir(parents
=True, exist_ok
=True)
172 perfdocs_tmpdir
.chmod(0o766)
174 logger
.critical("Error creating temp file: {}".format(e
))
176 if perfdocs_tmpdir
.is_dir():
177 return perfdocs_tmpdir
180 def _create_perfdocs(self
):
182 Creates the perfdocs documentation.
183 :return: str path of the temp dir it is saved in
185 # All directories that are kept in the perfdocs tree are valid,
186 # so use it to build up the documentation.
187 framework_docs
= self
.build_perfdocs_from_tree()
188 perfdocs_tmpdir
= self
._create
_temp
_dir
()
190 # Save the documentation files
192 for framework_name
in sorted(framework_docs
.keys()):
193 frameworks
.append(framework_name
)
195 framework_docs
[framework_name
]["dynamic"],
196 pathlib
.Path(perfdocs_tmpdir
, framework_name
),
199 if framework_docs
[framework_name
]["metrics"]:
201 framework_docs
[framework_name
]["metrics"],
202 pathlib
.Path(perfdocs_tmpdir
, f
"{framework_name}-metrics"),
205 for static_name
in framework_docs
[framework_name
]["static"]:
206 if static_name
["file"].endswith(".rst"):
207 # XXX Replace this with a shutil.copy call (like below)
209 static_name
["content"],
211 perfdocs_tmpdir
, static_name
["file"].split(".")[0]
216 static_name
["content"],
217 pathlib
.Path(perfdocs_tmpdir
, static_name
["file"]),
220 # Get the main page and add the framework links to it
221 mainpage
= read_file(
222 pathlib
.Path(self
.templates_path
, "index.rst"), stringify
=True
225 fmt_frameworks
= "\n".join([" * :doc:`%s`" % name
for name
in frameworks
])
226 fmt_toctree
= "\n".join([" %s" % name
for name
in frameworks
])
228 fmt_mainpage
= re
.sub(r
"{toctree_documentation}", fmt_toctree
, mainpage
)
229 fmt_mainpage
= re
.sub(r
"{test_documentation}", fmt_frameworks
, fmt_mainpage
)
231 save_file(fmt_mainpage
, pathlib
.Path(perfdocs_tmpdir
, "index"))
233 return perfdocs_tmpdir
235 def _save_perfdocs(self
, perfdocs_tmpdir
):
237 Copies the perfdocs tree after it was saved into the perfdocs_tmpdir
238 :param perfdocs_tmpdir: str location of the temp dir where the
241 # Remove the old docs and copy the new version there without
242 # checking if they need to be regenerated.
243 logger
.log("Regenerating perfdocs...")
245 if self
.perfdocs_path
.exists():
246 shutil
.rmtree(str(self
.perfdocs_path
))
249 saved
= shutil
.copytree(str(perfdocs_tmpdir
), str(self
.perfdocs_path
))
252 "Documentation saved to {}/".format(
253 re
.sub(".*testing", "testing", str(self
.perfdocs_path
))
256 except Exception as e
:
258 "There was an error while saving the documentation: {}".format(e
)
261 def generate_perfdocs(self
):
263 Generate the performance documentation.
265 If `self._generate` is True, then the documentation will be regenerated
266 without any checks. Otherwise, if it is False, the new documentation will be
267 prepare and compare with the existing documentation to determine if
268 it should be regenerated.
270 :return bool: True/False - For True, if `self._generate` is True, then the
271 docs were regenerated. If `self._generate` is False, then True will mean
272 that the docs should be regenerated, and False means that they do not
273 need to be regenerated.
276 def get_possibly_changed_files():
278 Returns files that might have been modified
279 (used to output a linter warning for regeneration)
280 :return: list - files that might have been modified
282 # Returns files that might have been modified
283 # (used to output a linter warning for regeneration)
285 for entry
in self
._perfdocs
_tree
:
288 pathlib
.Path(entry
["path"], entry
["yml"]),
289 pathlib
.Path(entry
["path"], entry
["rst"]),
294 # Throw a warning if there's no need for generating
295 if not self
.perfdocs_path
.exists() and not self
._generate
:
296 # If they don't exist and we are not generating, then throw
297 # a linting error and exit.
299 "PerfDocs need to be regenerated.", files
=get_possibly_changed_files()
303 perfdocs_tmpdir
= self
._create
_perfdocs
()
305 self
._save
_perfdocs
(perfdocs_tmpdir
)
306 elif not are_dirs_equal(perfdocs_tmpdir
, self
.perfdocs_path
):
307 # If we are not generating, then at least check if they
308 # should be regenerated by comparing the directories.
310 "PerfDocs are outdated, run ./mach lint -l perfdocs --fix .` "
311 + "to update them. You can also apply the "
312 + f
"{'perfdocs.diff' if ON_TRY else 'diff.txt'} patch file "
313 + f
"{'produced from this reviewbot test ' if ON_TRY else ''}"
314 + "to fix the issue.",
315 files
=get_changed_files(self
._workspace
),