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