Chromium Code Reviews| Index: reviewbot/handlers/policy_checklist.py |
| =================================================================== |
| --- reviewbot/handlers/policy_checklist.py (revision 0) |
| +++ reviewbot/handlers/policy_checklist.py (revision 0) |
| @@ -0,0 +1,263 @@ |
| +# Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +import re |
| + |
| +import model.app_config |
| +import util |
| + |
| + |
| +POLICY_TEMPLATES_FILE = 'chrome/app/policy/policy_templates.json' |
| +ID_LINE_COMMENT = 'For your editing convenience: highest ID currently used:' |
| +CONTEXT_THRESHOLD = 12 |
|
iannucci
2013/08/15 03:48:05
Should we make a separate config file specifically
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
I'd expect that if we need changes, not only the c
|
| +PROPERTY_NAME_RE = re.compile('\'(\\w+)\'\\s*:') |
|
iannucci
2013/08/15 03:48:05
may be worth using a raw string to reduce the back
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
Good idea, done.
|
| +MAX_LINE = 1000000 |
| + |
| +REVIEW_MESSAGE = ''' |
| +Thanks for improving Chromium enterprise features! In general, |
| +enterprise policies should work across all platforms and need to remain |
| +backward compatible across several Chrome versions. To help avoid common |
| +pitfalls, policy review bot has analyzed your change and added comments |
| +with checklists of things to watch out for. |
| + |
| +Please direct comments and complaints about this service to |
| +chrome-enterprise@google.com and/or mnissler@chromium.org. |
| +''' |
| + |
| +ADDITION_COMMENT = ''' |
| +When adding a new policy setting, please consider the following: |
| + |
| +- Ideally, your new policy should be expressing the intention of an |
| + administrator and/or cover a well-scoped user-visible behavior of the |
| + browser as opposed to an implementation-specific parameter. Consider |
| + that your implementation may change in the future, and you might have |
| + to re-implement your policy on top of the new implementation. |
|
iannucci
2013/08/15 03:48:05
I'm assuming these strings have all been vetted by
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
Yes, I've discussed them in a doc with the chrome/
|
| +- Make sure your policy definition has proper supported_on declarations |
| + specifying the platform and versions this policy is supported on. |
| +- Make sure feature flags are correct. In particular: |
| + - dynamic_refresh - whether your feature can adjust to changes in |
| + policy at run time. You typically use PrefChangeRegistrar to do so. |
| + - per_profile - whether your policy is browser-global or can be set |
| + independently for each Chrome Profile. This is usually true if you |
| + read the policy value from the Profile's PrefService and false if |
| + you read it from the local_state PrefService. |
| + - can_be_recommended - whether your feature supports admin-supplied |
| + default values that the user can override. |
| +- Make sure you put a helpful policy description: |
| + - Describe the effect of setting the policy in a way that makes sense |
| + to somebody who is not intimately familiar with your feature, such |
| + as administrators and other Chromium developers. |
| + - Do mention behavior for the cases where the policy gets ignored |
| + (i.e. not configured, booleans only one value is effective). It's |
| + nice for completeness and admins have been asking specifically for |
| + this piece of information in the past. |
| +- Write a browser_test for you new policy. The ideal test would fire up |
| + the browser with the policy set and check whether the policy affects |
| + user-visible behavior in the intended way. See |
| + chrome/browser/policy/policy_browsertest.cc for examples. |
| +- If you're adding a device policy for Chrome OS, be sure to update |
| + chrome/browser/chromeos/policy/device_policy_decoder_chromeos.{h,cc} |
| + so the policy shows up on the chrome://policy page. |
|
iannucci
2013/08/15 03:48:05
It may make sense to actually turn these strings i
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
Good point. Done.
|
| +''' |
| + |
| +MODIFICATION_COMMENT = ''' |
| +When modifying an existing policy setting, please consider the following: |
| + |
| +- Make sure the policy meta data is up-to-date, in particular |
| + supported_on, and the feature flags. |
| +- In general, don't change policy semantics in a way that is |
| + incompatible (as determined by user/admin-visible behavior) with |
| + previous semantics. In particular, consider that existing policy |
| + deployments may affect both old and new browser versions, and both |
| + should behave according to the admin's intentions. |
| +- It is OK to expand semantics of policy values as long as the previous |
| + policy description is compatible with the new behavior. |
| +- It is OK to update feature implementations and the policy description |
| + when Chrome changes as long as the intended effect of the policy |
| + remains intact. |
| +- The process for removing policies is to deprecate them first, wait a |
| + few releases (if possible) and then remove them. Make sure you put the |
| + deprecated flag if you deprecate a policy. |
| +''' |
| + |
| + |
| +def nmin(*args): |
| + """Calculates the minimum of |args|, ignoring None entries.""" |
| + try: |
| + return min(v for v in args if v is not None) |
| + except ValueError: |
|
iannucci
2013/08/15 03:48:05
This seems sketchy. Why ValueError?
Also, I think
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
min raises ValueError if the sequence is empty. I'
|
| + return None |
| + |
| + |
| +def nmax(*args): |
| + """Calculates the maximum of |args|, ignoring None entries.""" |
| + try: |
| + return max(v for v in args if v is not None) |
| + except ValueError: |
| + return None |
| + |
| + |
| +def nsub(a, b): |
| + """Calculates a - b, returning None if either a or b is None""" |
| + return None if (a is None or b is None) else a - b |
| + |
| + |
| +def indent(line): |
| + """Returns the indent level (number of leading spaces) for |line|.""" |
| + nspaces = len(line) - len(line.lstrip(' ')) |
| + return None if nspaces == 0 else nspaces |
|
iannucci
2013/08/15 03:48:05
You may want to take a look at the textwrap module
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
This actually determines the indent from a given l
|
| + |
| + |
| +class PolicyChangeParser(object): |
|
iannucci
2013/08/15 03:48:05
Needs docs, code is hard to read without context/e
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
I've added documentation and some illustrative uni
|
| + def __init__(self, lines): |
| + self.lines = lines |
| + self.chunks_list = [] |
| + self.reset() |
| + |
| + def run(self): |
| + self.chunks_list = [] |
| + self.last_change = [None, None] |
| + cursor = [None, None] |
| + self.reset() |
| + for (a_line, b_line, line) in self.lines: |
| + # Skip comment lines. |
| + if line.startswith('#'): |
| + continue |
| + |
| + # See whether the current line has a JSON property. |
| + keyword = None |
| + match = PROPERTY_NAME_RE.search(line) |
| + if match: |
| + keyword = match.group(1).lower() |
| + |
| + |
| + # Check whether the current block closes. |
| + line_indent = indent(line) |
| + if (self.block_indent is not None and |
| + line_indent is not None and |
| + line_indent < self.block_indent): |
| + self.block_closed = True |
| + |
| + if (keyword == 'name' and self.block_closed): |
| + # If we see the 'name' property, that likely indicates the start of a |
| + # new policy. Start a new chunk. |
| + self.flush_chunk() |
| + |
| + # Update various cursors. |
| + cursor = [nmax(a_line, cursor[0]), nmax(b_line, cursor[1])] |
| + offset = nmin(nsub(cursor[0], self.last_change[0]), |
| + nsub(cursor[1], self.last_change[1])) |
| + |
| + if a_line > 0 and b_line == 0: |
| + self.removals = True |
| + self.last_change[0] = a_line |
| + elif a_line == 0 and b_line > 0: |
| + self.additions = True |
| + self.last_change[1] = b_line |
| + |
| + if (offset is not None and |
| + (offset > CONTEXT_THRESHOLD or |
| + (offset > 1 and self.block_closed))): |
| + # If the last chunk is too far away, assume a new one starts. |
| + self.flush_chunk() |
| + |
| + # Try to figure out block indent from properties exclusively used for |
| + # policy definitions. |
| + if keyword in ('id', 'schema', 'future', 'items', 'features', |
| + 'supported_on', 'example_value', 'deprecated'): |
| + self.block_indent = line_indent |
| + |
| + # Prefer the comment on the policy name property if we see it fly by. |
| + if keyword == 'name': |
| + # Attempt to filter out name labels on enum items. |
| + if self.block_indent is not None and self.block_indent != line_indent: |
| + pass |
| + elif a_line > 0 and b_line == 0: |
| + self.comment_pos[0] = a_line |
| + elif a_line == 0 and b_line > 0: |
| + self.comment_pos[1] = b_line |
| + |
| + self.chunk_start = [nmin(self.last_change[0], self.chunk_start[0]), |
| + nmin(self.last_change[1], self.chunk_start[1])] |
| + |
| + # Flush the last chunk. |
| + if self.chunk_start != [None, None]: |
| + self.flush_chunk() |
| + |
| + @property |
| + def chunks(self): |
| + return self.chunks_list |
| + |
| + def flush_chunk(self): |
| + self.comment_pos = [nmax(self.chunk_start[0], self.comment_pos[0]), |
| + nmax(self.chunk_start[1], self.comment_pos[1])] |
| + self.chunks_list.append( |
| + util.ObjectDict( |
| + { 'start': self.chunk_start, |
| + 'end': self.last_change, |
| + 'comment_pos': self.comment_pos, |
| + 'additions': self.additions, |
| + 'removals': self.removals })) |
| + self.reset() |
| + |
| + def reset(self): |
| + # This is called from __init__. |
| + # pylint: disable=W0201 |
| + self.chunk_start = [None, None] |
| + self.last_change = [None, None] |
| + self.comment_pos = [None, None] |
| + self.block_indent = None |
| + self.block_closed = False |
| + self.additions = False |
| + self.removals = False |
| + |
| + |
| +def process(addr, message, review, rietveld): |
| + """Handles reviews for chrome/app/policy/policy_templates.json. |
| + |
| + This looks at the patch to identify additions/modifications to policy |
| + definitions and posts comments with a checklist intended for the author and |
| + reviewer to go through in order to catch common mistakes. |
| + """ |
| + |
| + if POLICY_TEMPLATES_FILE not in review.latest_patchset.files: |
| + return |
| + |
| + # Only process the change if the mail is directly to us or we haven't |
| + # processed this review yet. |
| + client_id = model.app_config.get().client_id |
| + if (not addr in util.get_emails(getattr(message, 'to', '')) and |
| + client_id in [m.sender for m in review.issue_data.messages]): |
| + return |
|
iannucci
2013/08/15 03:48:05
So the idea here is that you could add reviewbot a
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
Correct. Once we've done so a couple times and con
|
| + |
| + # Don't process reverts. |
| + if 'revert' in review.issue_data.description.lower(): |
| + return |
| + |
| + # Parse the patch, look at the chunks and generate inline comments. |
| + parser = PolicyChangeParser( |
|
iannucci
2013/08/15 03:48:05
why not just make PolicyChangeParser a function re
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
It's got all this state to carry around, which is
|
| + review.latest_patchset.files[POLICY_TEMPLATES_FILE].patch.lines) |
| + parser.run() |
| + for chunk in parser.chunks: |
| + if chunk.additions and not chunk.removals: |
| + message = ADDITION_COMMENT |
| + else: |
| + message = MODIFICATION_COMMENT |
| + |
| + if chunk.comment_pos[1] is not None: |
| + line, side = chunk.comment_pos[1], 'b' |
| + elif chunk.comment_pos[0] is not None: |
| + line, side = chunk.comment_pos[0], 'a' |
| + else: |
| + # No suitable position? |
| + continue |
| + |
| + rietveld.add_inline_comment( |
| + review.issue_id, review.latest_patchset.patchset, |
| + review.latest_patchset.files[POLICY_TEMPLATES_FILE].id, |
| + line, side, message) |
|
iannucci
2013/08/15 03:48:05
Maybe I'm unfamiliar with the types of changes her
Mattias Nissler (ping if slow)
2013/08/16 12:49:22
I've tried it on GIT history and it usually posted
|
| + |
| + # Finally, post all inline comments. |
| + if len(parser.chunks) > 0: |
| + rietveld.post_comment(review.issue_id, REVIEW_MESSAGE, True) |
| Property changes on: reviewbot/handlers/policy_checklist.py |
| ___________________________________________________________________ |
| Added: svn:eol-style |
| + LF |