OLD | NEW |
| (Empty) |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 import os.path # for initializing constants | |
6 | |
7 # Directories that we run presubmit checks on. | |
8 PRESUBMIT_PATH = os.path.normpath('chrome/common/extensions/PRESUBMIT.py') | |
9 API_DIR = os.path.normpath('chrome/common/extensions/api') | |
10 DOC_DIR = os.path.normpath('chrome/common/extensions/docs') | |
11 BUILD_DIR = os.path.join(DOC_DIR, 'build') | |
12 TEMPLATE_DIR = os.path.join(DOC_DIR, 'template') | |
13 JS_DIR = os.path.join(DOC_DIR, 'js') | |
14 CSS_DIR = os.path.join(DOC_DIR, 'css') | |
15 STATIC_DIR = os.path.join(DOC_DIR, 'static') | |
16 SAMPLES_DIR = os.path.join(DOC_DIR, 'examples') | |
17 APPS_DIR = os.path.join(DOC_DIR, 'apps') | |
18 EXTENSIONS_DIR = os.path.join(DOC_DIR, 'extensions') | |
19 | |
20 EXCEPTIONS = ['README', 'README.txt', 'OWNERS'] | |
21 | |
22 # Presubmit messages. | |
23 README = os.path.join(DOC_DIR, 'README.txt') | |
24 REBUILD_WARNING = ( | |
25 'This change modifies the extension docs but the generated docs have ' | |
26 'not been updated properly. See %s for more info.' % README) | |
27 BUILD_SCRIPT = os.path.join(BUILD_DIR, 'build.py') | |
28 REBUILD_INSTRUCTIONS = ( | |
29 'First build DumpRenderTree, then update the docs by running:\n %s' | |
30 ' --page-name=<apiName>' % | |
31 BUILD_SCRIPT) | |
32 | |
33 | |
34 def CheckChangeOnUpload(input_api, output_api): | |
35 return (CheckPresubmitChanges(input_api, output_api) + | |
36 CheckDocChanges(input_api, output_api)) | |
37 | |
38 def CheckChangeOnCommit(input_api, output_api): | |
39 return (CheckPresubmitChanges(input_api, output_api) + | |
40 CheckDocChanges(input_api, output_api, strict=False)) | |
41 | |
42 def CheckPresubmitChanges(input_api, output_api): | |
43 for PRESUBMIT_PATH in input_api.LocalPaths(): | |
44 return input_api.canned_checks.RunUnitTests(input_api, output_api, | |
45 ['./PRESUBMIT_test.py']) | |
46 return [] | |
47 | |
48 def CheckDocChanges(input_api, output_api, strict=True): | |
49 warnings = [] | |
50 | |
51 for af in input_api.AffectedFiles(): | |
52 path = af.LocalPath() | |
53 if IsSkippedFile(path, input_api): | |
54 continue | |
55 | |
56 elif (IsApiFile(path, input_api) or | |
57 IsBuildFile(path, input_api) or | |
58 IsTemplateFile(path, input_api) or | |
59 IsJsFile(path, input_api) or | |
60 IsCssFile(path, input_api)): | |
61 # These files do not always cause changes to the generated docs | |
62 # so we can ignore them if not running strict checks. | |
63 if strict and not DocsGenerated(input_api): | |
64 warnings.append('Docs out of sync with %s changes.' % path) | |
65 | |
66 elif IsStaticDoc(path, input_api): | |
67 if not StaticDocBuilt(af, input_api): | |
68 warnings.append('Changes to %s not reflected in generated doc.' % path) | |
69 | |
70 elif IsSampleFile(path, input_api): | |
71 if not SampleZipped(af, input_api): | |
72 warnings.append('Changes to sample %s have not been re-zipped.' % path) | |
73 | |
74 elif IsGeneratedDoc(path, input_api): | |
75 if not NonGeneratedFilesEdited(input_api): | |
76 warnings.append('Changes to generated doc %s not reflected in ' | |
77 'non-generated files.' % path) | |
78 | |
79 if warnings: | |
80 warnings.sort() | |
81 warnings = [' - %s\n' % w for w in warnings] | |
82 # Prompt user if they want to continue. | |
83 return [output_api.PresubmitPromptWarning(REBUILD_WARNING + '\n' + | |
84 ''.join(warnings) + | |
85 REBUILD_INSTRUCTIONS)] | |
86 return [] | |
87 | |
88 def IsSkippedFile(path, input_api): | |
89 return input_api.os_path.basename(path) in EXCEPTIONS | |
90 | |
91 def IsApiFile(path, input_api): | |
92 return (input_api.os_path.dirname(path) == API_DIR and | |
93 (path.endswith('.json') or path.endswith('.idl'))) | |
94 | |
95 def IsBuildFile(path, input_api): | |
96 return input_api.os_path.dirname(path) == BUILD_DIR | |
97 | |
98 def IsTemplateFile(path, input_api): | |
99 return input_api.os_path.dirname(path) == TEMPLATE_DIR | |
100 | |
101 def IsJsFile(path, input_api): | |
102 return (input_api.os_path.dirname(path) == JS_DIR and | |
103 path.endswith('.js')) | |
104 | |
105 def IsCssFile(path, input_api): | |
106 return (input_api.os_path.dirname(path) == CSS_DIR and | |
107 path.endswith('.css')) | |
108 | |
109 def IsStaticDoc(path, input_api): | |
110 return (input_api.os_path.dirname(path) == STATIC_DIR and | |
111 path.endswith('.html')) | |
112 | |
113 def IsSampleFile(path, input_api): | |
114 return input_api.os_path.dirname(path).startswith(SAMPLES_DIR) | |
115 | |
116 def IsGeneratedDoc(path, input_api): | |
117 return (input_api.os_path.dirname(path) in [APPS_DIR, EXTENSIONS_DIR] and | |
118 path.endswith('.html')) | |
119 | |
120 def DocsGenerated(input_api): | |
121 """Return True if there are any generated docs in this change. | |
122 | |
123 Generated docs are the files that are the output of build.py. Typically | |
124 all docs changes will contain both generated docs and non-generated files. | |
125 """ | |
126 return any(IsGeneratedDoc(path, input_api) | |
127 for path in input_api.LocalPaths()) | |
128 | |
129 def NonGeneratedFilesEdited(input_api): | |
130 """Return True if there are any non-generated files in this change. | |
131 | |
132 Non-generated files are those that are the input to build.py. Typically | |
133 all docs changes will contain both non-generated files and generated docs. | |
134 """ | |
135 return any(IsApiFile(path, input_api) or | |
136 IsBuildFile(path, input_api) or | |
137 IsTemplateFile(path, input_api) or | |
138 IsJsFile(path, input_api) or | |
139 IsCssFile(path, input_api) or | |
140 IsStaticDoc(path, input_api) or | |
141 IsSampleFile(path, input_api) | |
142 for path in input_api.LocalPaths()) | |
143 | |
144 def StaticDocBuilt(static_file, input_api): | |
145 """Return True if the generated doc that corresponds to the |static_file| | |
146 is also in this change. Both files must also contain matching changes. | |
147 """ | |
148 for subdir in [APPS_DIR, EXTENSIONS_DIR]: | |
149 generated_file = _FindFileInAlternateDir(static_file, subdir, input_api) | |
150 if _ChangesMatch(generated_file, static_file): | |
151 return True | |
152 return False | |
153 | |
154 def _FindFileInAlternateDir(affected_file, alt_dir, input_api): | |
155 """Return an AffectFile for the file in |alt_dir| that corresponds to | |
156 |affected_file|. | |
157 | |
158 If the file does not exist in the is change, return None. | |
159 """ | |
160 alt_path = _AlternateFilePath(affected_file.LocalPath(), alt_dir, input_api) | |
161 for f in input_api.AffectedFiles(): | |
162 if f.LocalPath() == alt_path: | |
163 return f | |
164 | |
165 def _AlternateFilePath(path, alt_dir, input_api): | |
166 """Return a path with the same basename as |path| but in |alt_dir| directory. | |
167 | |
168 This is useful for finding corresponding static and generated docs. | |
169 | |
170 Example: | |
171 _AlternateFilePath('/foo/bar', '/alt/dir', ...) == '/alt/dir/bar') | |
172 """ | |
173 base_name = input_api.os_path.basename(path) | |
174 return input_api.os_path.join(alt_dir, base_name) | |
175 | |
176 def _ChangesMatch(generated_file, static_file): | |
177 """Return True if the two files contain the same textual changes. | |
178 | |
179 There may be extra generated lines and generated lines are still considered | |
180 to "match" static ones even if they have extra formatting/text at their | |
181 beginnings and ends. | |
182 Line numbers may differ but order may not. | |
183 """ | |
184 if not generated_file and not static_file: | |
185 return True # Neither file affected. | |
186 | |
187 if not generated_file or not static_file: | |
188 return False # One file missing. | |
189 | |
190 generated_changes = generated_file.ChangedContents() | |
191 static_changes = static_file.ChangedContents() | |
192 # ChangedContents() is a list of (line number, text) for all new lines. | |
193 # Ignore the line number, but check that the text for each new line matches. | |
194 | |
195 next_generated = 0 | |
196 start_pos = 0 | |
197 for next_static in range(len(static_changes)): | |
198 _, static_text = static_changes[next_static] | |
199 | |
200 # Search generated changes for this static text. | |
201 found = False | |
202 while not found and next_generated < len(generated_changes): | |
203 _, generated_text = generated_changes[next_generated] | |
204 # Text need not be identical but the entire static line should be | |
205 # in the generated one (e.g. generated text might have extra formatting). | |
206 found_at = generated_text[start_pos:].find(static_text) | |
207 if found_at != -1: | |
208 # Next search starts on the same line, after the substring matched. | |
209 start_pos = found_at + len(static_text) | |
210 found = True | |
211 else: | |
212 next_generated += 1 | |
213 start_pos = 0 | |
214 | |
215 if not found: | |
216 return False | |
217 | |
218 return True | |
219 | |
220 def SampleZipped(sample_file, input_api): | |
221 """Return True if the zipfile that should contain |sample_file| is in | |
222 this change. | |
223 """ | |
224 sample_path = sample_file.LocalPath() | |
225 for af in input_api.AffectedFiles(): | |
226 root, ext = input_api.os_path.splitext(af.LocalPath()) | |
227 if ext == '.zip' and sample_path.startswith(root): | |
228 return True | |
229 return False | |
OLD | NEW |