Home | History | Annotate | Download | only in scripts
      1 #!/usr/bin/env python
      2 
      3 import requests
      4 import re
      5 from in_file import InFile
      6 
      7 BRANCH_FORMAT = "https://src.chromium.org/blink/branches/chromium/%s/%s"
      8 TRUNK_PATH = "Source/platform/RuntimeEnabledFeatures.in"
      9 TRUNK_URL = "https://src.chromium.org/blink/trunk/%s" % TRUNK_PATH
     10 
     11 
     12 def features_path(branch):
     13     # RuntimeEnabledFeatures has only existed since April 2013:
     14     if branch <= 1453:
     15         return None
     16     # Source/core/page/RuntimeEnabledFeatures.in existed by 1547
     17     # but was in an old format without status= arguments.
     18     if branch <= 1547:
     19         return None
     20     if branch <= 1650:
     21         return "Source/core/page/RuntimeEnabledFeatures.in"
     22     # Modern location:
     23     return TRUNK_PATH
     24 
     25 
     26 def parse_features_file(features_text):
     27     valid_values = {
     28         'status': ['stable', 'experimental', 'deprecated', 'test'],
     29     }
     30     defaults = {
     31         'condition': None,
     32         'depends_on': [],
     33         'custom': False,
     34         'status': None,
     35     }
     36 
     37     # FIXME: in_file.py manually calls str.strip so conver to str here.
     38     features_lines = str(features_text).split("\n")
     39     return InFile(features_lines, defaults, valid_values)
     40 
     41 
     42 def stable_features(in_file):
     43     return [feature['name'] for feature in in_file.name_dictionaries if feature['status'] == 'stable']
     44 
     45 
     46 def branch_from_version(version_string):
     47     # Format: 31.0.1650.63, the second digit was only ever used for M4
     48     # no clue what it's actually intended for.
     49     version_regexp = r"(?P<major>\d+)\.\d+\.(?P<branch>\d+)\.(?P<minor>\d+)"
     50     match = re.match(version_regexp, version_string)
     51     # if match == None, we'll blow up, so at least provide some debugging information:
     52     if not match:
     53         print version_string
     54     return int(match.group('branch'))
     55 
     56 
     57 def print_feature_diff(added_features, removed_features):
     58     for feature in added_features:
     59         print "+ %s" % feature
     60     for feature in removed_features:
     61         print "- %s" % feature
     62 
     63 
     64 def historical_versions(os_string, channel):
     65     url_pattern = "http://omahaproxy.appspot.com/history?os=%s&channel=%s"
     66     url = url_pattern % (os_string, channel)
     67     releases_csv = requests.get(url).text.strip("\n")
     68     # Format: os,channel,version_string,date_string
     69     lines = releases_csv.split('\n')
     70     # As of June 2014, omahaproxy is now including headers:
     71     assert(lines[0] == 'os,channel,version,timestamp')
     72     # FIXME: We could replace this with more generic CSV parsing now that we have headers.
     73     return [line.split(',')[2] for line in lines[1:]]
     74 
     75 
     76 def feature_file_url_for_branch(branch):
     77     path = features_path(branch)
     78     if not path:
     79         return None
     80     return BRANCH_FORMAT % (branch, path)
     81 
     82 
     83 def feature_file_for_branch(branch):
     84     url = feature_file_url_for_branch(branch)
     85     if not url:
     86         return None
     87     return parse_features_file(requests.get(url).text)
     88 
     89 
     90 def historical_feature_tuples(os_string, channel):
     91     feature_tuples = []
     92     version_strings = reversed(historical_versions(os_string, channel))
     93     seen_branches = set()
     94 
     95     for version in version_strings:
     96         branch = branch_from_version(version)
     97         if branch in seen_branches:
     98             continue
     99         seen_branches.add(branch)
    100 
    101         feature_file = feature_file_for_branch(branch)
    102         if not feature_file:
    103             continue
    104         feature_tuple = (version, feature_file)
    105         feature_tuples.append(feature_tuple)
    106     return feature_tuples
    107 
    108 
    109 class FeatureAuditor(object):
    110     def __init__(self):
    111         self.last_features = []
    112 
    113     def add_version(self, version_name, feature_file):
    114         features = stable_features(feature_file)
    115         if self.last_features:
    116             added_features = list(set(features) - set(self.last_features))
    117             removed_features = list(set(self.last_features) - set(features))
    118 
    119             print "\n%s:" % version_name
    120             print_feature_diff(added_features, removed_features)
    121 
    122         self.last_features = features
    123 
    124 
    125 def active_feature_tuples(os_string):
    126     feature_tuples = []
    127     current_releases_url = "http://omahaproxy.appspot.com/all.json"
    128     trains = requests.get(current_releases_url).json()
    129     train = next(train for train in trains if train['os'] == os_string)
    130     # FIXME: This is depending on the ordering of the json, we could
    131     # use use sorted() with true_branch, but that would put None first.
    132     for version in reversed(train['versions']):
    133         # FIXME: This is lame to exclude stable, the caller should
    134         # ignore it if it doesn't want it.
    135         if version['channel'] == 'stable':
    136             continue  # handled by historical_feature_tuples
    137         branch = version['true_branch']
    138         if branch:
    139             feature_file = feature_file_for_branch(branch)
    140         else:
    141             feature_file = parse_features_file(requests.get(TRUNK_URL).text)
    142 
    143         name = "%(version)s %(channel)s" % version
    144         feature_tuples.append((name, feature_file))
    145     return feature_tuples
    146 
    147 
    148 # FIXME: This only really needs feature_files.
    149 def stale_features(tuples):
    150     last_features = None
    151     can_be_removed = set()
    152     for _, feature_file in tuples:
    153         features = stable_features(feature_file)
    154         if last_features:
    155             can_be_removed.update(set(features))
    156             removed_features = list(set(last_features) - set(features))
    157             can_be_removed.difference_update(set(removed_features))
    158         last_features = features
    159     return sorted(can_be_removed)
    160 
    161 
    162 def main():
    163     historical_tuples = historical_feature_tuples("win", "stable")
    164     active_tuples = active_feature_tuples("win")
    165 
    166     auditor = FeatureAuditor()
    167     for version, feature_file in historical_tuples + active_tuples:
    168         auditor.add_version(version, feature_file)
    169 
    170     print "\nConsider for removal (have been stable for at least one release):"
    171     for feature in stale_features(historical_tuples):
    172         print feature
    173 
    174 
    175 if __name__ == "__main__":
    176     main()
    177