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/. 4from __future__ import absolute_import 5 6import os 7import re 8import shutil 9import tempfile 10 11from perfdocs.logger import PerfDocLogger 12from perfdocs.utils import ( 13 are_dirs_equal, 14 read_file, 15 read_yaml, 16 save_file 17) 18 19logger = PerfDocLogger() 20 21 22class Generator(object): 23 ''' 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. 28 ''' 29 def __init__(self, verifier, workspace, generate=False): 30 ''' 31 Initialize the Generator. 32 33 :param verifier: Verifier object. It should not be a fresh Verifier object, 34 but an initialized one with validate_tree() method already called 35 :param workspace: Path to the top-level checkout directory. 36 :param generate: Flag for generating the documentation 37 ''' 38 self._workspace = workspace 39 if not self._workspace: 40 raise Exception("PerfDocs Generator requires a workspace directory.") 41 # Template documents without added information reside here 42 self.templates_path = os.path.join( 43 self._workspace, 'tools', 'lint', 'perfdocs', 'templates') 44 self.perfdocs_path = os.path.join( 45 self._workspace, 'testing', 'perfdocs', 'generated') 46 47 self._generate = generate 48 self._verifier = verifier 49 self._perfdocs_tree = self._verifier._gatherer.perfdocs_tree 50 51 def build_perfdocs_from_tree(self): 52 ''' 53 Builds up a document for each framework that was found. 54 55 :return dict: A dictionary containing a mapping from each framework 56 to the document that was built for it, i.e: 57 { 58 framework_name: framework_document, 59 ... 60 } 61 ''' 62 def _append_rst_section(title, content, documentation, type=None): 63 ''' 64 Adds a section to the documentation with the title as the type mentioned 65 and paragraph as content mentioned. 66 :param title: title of the section 67 :param content: content of section paragraph 68 :param documentation: documentation object to add section to 69 :param type: type of the title heading 70 ''' 71 heading_map = { 72 'H4': '-', 73 'H5': '^' 74 } 75 heading_symbol = heading_map.get(type, '-') 76 documentation.extend([title, heading_symbol * len(title), content, '']) 77 78 # Using the verified `perfdocs_tree`, build up the documentation. 79 frameworks_info = {} 80 for framework in self._perfdocs_tree: 81 yaml_content = read_yaml(os.path.join(framework['path'], framework['yml'])) 82 rst_content = read_file( 83 os.path.join(framework['path'], framework['rst']), 84 stringify=True) 85 86 # Gather all tests and descriptions and format them into 87 # documentation content 88 documentation = [] 89 suites = yaml_content['suites'] 90 for suite_name in sorted(suites.keys()): 91 suite_info = suites[suite_name] 92 93 # Add the suite with an H4 heading 94 _append_rst_section( 95 suite_name.capitalize(), suite_info['description'], documentation, type="H4") 96 tests = suite_info.get('tests', {}) 97 for test_name in sorted(tests.keys()): 98 documentation.extend( 99 self._verifier 100 ._gatherer 101 .framework_gatherers[yaml_content["name"]] 102 .build_test_description( 103 test_name, tests[test_name] 104 ) 105 ) 106 107 # Insert documentation into `.rst` file 108 framework_rst = re.sub( 109 r'{documentation}', 110 os.linesep.join(documentation), 111 rst_content 112 ) 113 frameworks_info[yaml_content['name']] = framework_rst 114 115 return frameworks_info 116 117 def _create_temp_dir(self): 118 ''' 119 Create a temp directory as preparation of saving the documentation tree. 120 :return: str the location of perfdocs_tmpdir 121 ''' 122 # Build the directory that will contain the final result (a tmp dir 123 # that will be moved to the final location afterwards) 124 try: 125 tmpdir = tempfile.mkdtemp() 126 perfdocs_tmpdir = os.path.join(tmpdir, 'generated') 127 os.mkdir(perfdocs_tmpdir) 128 except OSError as e: 129 logger.critical("Error creating temp file: {}".format(e)) 130 131 success = False or os.path.isdir(perfdocs_tmpdir) 132 if success: 133 return perfdocs_tmpdir 134 else: 135 return success 136 137 def _create_perfdocs(self): 138 ''' 139 Creates the perfdocs documentation. 140 :return: str path of the temp dir it is saved in 141 ''' 142 # All directories that are kept in the perfdocs tree are valid, 143 # so use it to build up the documentation. 144 framework_docs = self.build_perfdocs_from_tree() 145 perfdocs_tmpdir = self._create_temp_dir() 146 147 # Save the documentation files 148 frameworks = [] 149 for framework_name in sorted(framework_docs.keys()): 150 frameworks.append(framework_name) 151 save_file( 152 framework_docs[framework_name], 153 os.path.join(perfdocs_tmpdir, framework_name) 154 ) 155 156 # Get the main page and add the framework links to it 157 mainpage = read_file(os.path.join(self.templates_path, "index.rst"), stringify=True) 158 fmt_frameworks = os.linesep.join([' :doc:`%s`' % name for name in frameworks]) 159 fmt_mainpage = re.sub(r"{test_documentation}", fmt_frameworks, mainpage) 160 save_file(fmt_mainpage, os.path.join(perfdocs_tmpdir, 'index')) 161 162 return perfdocs_tmpdir 163 164 def _save_perfdocs(self, perfdocs_tmpdir): 165 ''' 166 Copies the perfdocs tree after it was saved into the perfdocs_tmpdir 167 :param perfdocs_tmpdir: str location of the temp dir where the 168 perfdocs was saved 169 ''' 170 # Remove the old docs and copy the new version there without 171 # checking if they need to be regenerated. 172 logger.log("Regenerating perfdocs...") 173 174 if os.path.exists(self.perfdocs_path): 175 shutil.rmtree(self.perfdocs_path) 176 177 try: 178 saved = shutil.copytree(perfdocs_tmpdir, self.perfdocs_path) 179 if saved: 180 logger.log("Documentation saved to {}/".format( 181 re.sub(".*testing", "testing", self.perfdocs_path))) 182 except Exception as e: 183 logger.critical("There was an error while saving the documentation: {}".format(e)) 184 185 def generate_perfdocs(self): 186 ''' 187 Generate the performance documentation. 188 189 If `self._generate` is True, then the documentation will be regenerated 190 without any checks. Otherwise, if it is False, the new documentation will be 191 prepare and compare with the existing documentation to determine if 192 it should be regenerated. 193 194 :return bool: True/False - For True, if `self._generate` is True, then the 195 docs were regenerated. If `self._generate` is False, then True will mean 196 that the docs should be regenerated, and False means that they do not 197 need to be regenerated. 198 ''' 199 200 def get_possibly_changed_files(): 201 ''' 202 Returns files that might have been modified 203 (used to output a linter warning for regeneration) 204 :return: list - files that might have been modified 205 ''' 206 # Returns files that might have been modified 207 # (used to output a linter warning for regeneration) 208 files = [] 209 for entry in self._perfdocs_tree: 210 files.extend( 211 [os.path.join(entry['path'], entry['yml']), 212 os.path.join(entry['path'], entry['rst'])] 213 ) 214 return files 215 216 # Throw a warning if there's no need for generating 217 if not os.path.exists(self.perfdocs_path) and not self._generate: 218 # If they don't exist and we are not generating, then throw 219 # a linting error and exit. 220 logger.warning( 221 "PerfDocs need to be regenerated.", 222 files=get_possibly_changed_files() 223 ) 224 return True 225 226 perfdocs_tmpdir = self._create_perfdocs() 227 if self._generate: 228 self._save_perfdocs(perfdocs_tmpdir) 229 else: 230 # If we are not generating, then at least check if they 231 # should be regenerated by comparing the directories. 232 if not are_dirs_equal(perfdocs_tmpdir, self.perfdocs_path): 233 logger.warning( 234 "PerfDocs are outdated, run ./mach lint -l perfdocs --fix` to update them.", 235 files=get_possibly_changed_files() 236 ) 237