| #!/usr/bin/env python |
| |
| import requests |
| import re |
| from in_file import InFile |
| |
| BRANCH_FORMAT = "https://src.chromium.org/blink/branches/chromium/%s/%s" |
| TRUNK_PATH = "Source/platform/RuntimeEnabledFeatures.in" |
| TRUNK_URL = "https://src.chromium.org/blink/trunk/%s" % TRUNK_PATH |
| |
| |
| def features_path(branch): |
| # RuntimeEnabledFeatures has only existed since April 2013: |
| if branch <= 1453: |
| return None |
| # Source/core/page/RuntimeEnabledFeatures.in existed by 1547 |
| # but was in an old format without status= arguments. |
| if branch <= 1547: |
| return None |
| if branch <= 1650: |
| return "Source/core/page/RuntimeEnabledFeatures.in" |
| # Modern location: |
| return TRUNK_PATH |
| |
| |
| def parse_features_file(features_text): |
| valid_values = { |
| 'status': ['stable', 'experimental', 'test'], |
| } |
| defaults = { |
| 'condition': None, |
| 'depends_on': [], |
| 'custom': False, |
| 'status': None, |
| } |
| |
| # FIXME: in_file.py manually calls str.strip so conver to str here. |
| features_lines = str(features_text).split("\n") |
| return InFile(features_lines, defaults, valid_values) |
| |
| |
| def stable_features(in_file): |
| return [feature['name'] for feature in in_file.name_dictionaries if feature['status'] == 'stable'] |
| |
| |
| def branch_from_version(version_string): |
| # Format: 31.0.1650.63, the second digit was only ever used for M4 |
| # no clue what it's actually intended for. |
| version_regexp = r"(?P<major>\d+)\.\d+\.(?P<branch>\d+)\.(?P<minor>\d+)" |
| match = re.match(version_regexp, version_string) |
| # if match == None, we'll blow up, so at least provide some debugging information: |
| if not match: |
| print version_string |
| return int(match.group('branch')) |
| |
| |
| def print_feature_diff(added_features, removed_features): |
| for feature in added_features: |
| print "+ %s" % feature |
| for feature in removed_features: |
| print "- %s" % feature |
| |
| |
| def historical_versions(os_string, channel): |
| url_pattern = "http://omahaproxy.appspot.com/history?os=%s&channel=%s" |
| url = url_pattern % (os_string, channel) |
| releases_csv = requests.get(url).text.strip("\n") |
| # Format: os,channel,version_string,date_string |
| lines = releases_csv.split('\n') |
| # As of June 2014, omahaproxy is now including headers: |
| assert(lines[0] == 'os,channel,version,timestamp') |
| # FIXME: We could replace this with more generic CSV parsing now that we have headers. |
| return [line.split(',')[2] for line in lines[1:]] |
| |
| |
| def feature_file_url_for_branch(branch): |
| path = features_path(branch) |
| if not path: |
| return None |
| return BRANCH_FORMAT % (branch, path) |
| |
| |
| def feature_file_for_branch(branch): |
| url = feature_file_url_for_branch(branch) |
| if not url: |
| return None |
| return parse_features_file(requests.get(url).text) |
| |
| |
| def historical_feature_tuples(os_string, channel): |
| feature_tuples = [] |
| version_strings = reversed(historical_versions(os_string, channel)) |
| seen_branches = set() |
| |
| for version in version_strings: |
| branch = branch_from_version(version) |
| if branch in seen_branches: |
| continue |
| seen_branches.add(branch) |
| |
| feature_file = feature_file_for_branch(branch) |
| if not feature_file: |
| continue |
| feature_tuple = (version, feature_file) |
| feature_tuples.append(feature_tuple) |
| return feature_tuples |
| |
| |
| class FeatureAuditor(object): |
| def __init__(self): |
| self.last_features = [] |
| |
| def add_version(self, version_name, feature_file): |
| features = stable_features(feature_file) |
| if self.last_features: |
| added_features = list(set(features) - set(self.last_features)) |
| removed_features = list(set(self.last_features) - set(features)) |
| |
| print "\n%s:" % version_name |
| print_feature_diff(added_features, removed_features) |
| |
| self.last_features = features |
| |
| |
| def active_feature_tuples(os_string): |
| feature_tuples = [] |
| current_releases_url = "http://omahaproxy.appspot.com/all.json" |
| trains = requests.get(current_releases_url).json() |
| train = next(train for train in trains if train['os'] == os_string) |
| # FIXME: This is depending on the ordering of the json, we could |
| # use use sorted() with true_branch, but that would put None first. |
| for version in reversed(train['versions']): |
| # FIXME: This is lame to exclude stable, the caller should |
| # ignore it if it doesn't want it. |
| if version['channel'] == 'stable': |
| continue # handled by historical_feature_tuples |
| branch = version['true_branch'] |
| if branch: |
| feature_file = feature_file_for_branch(branch) |
| else: |
| feature_file = parse_features_file(requests.get(TRUNK_URL).text) |
| |
| name = "%(version)s %(channel)s" % version |
| feature_tuples.append((name, feature_file)) |
| return feature_tuples |
| |
| |
| # FIXME: This only really needs feature_files. |
| def stale_features(tuples): |
| last_features = None |
| can_be_removed = set() |
| for _, feature_file in tuples: |
| features = stable_features(feature_file) |
| if last_features: |
| can_be_removed.update(set(features)) |
| removed_features = list(set(last_features) - set(features)) |
| can_be_removed.difference_update(set(removed_features)) |
| last_features = features |
| return sorted(can_be_removed) |
| |
| |
| def main(): |
| historical_tuples = historical_feature_tuples("win", "stable") |
| active_tuples = active_feature_tuples("win") |
| |
| auditor = FeatureAuditor() |
| for version, feature_file in historical_tuples + active_tuples: |
| auditor.add_version(version, feature_file) |
| |
| print "\nConsider for removal (have been stable for at least one release):" |
| for feature in stale_features(historical_tuples): |
| print feature |
| |
| |
| if __name__ == "__main__": |
| main() |