#!/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 = "engine/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 # engine/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\d+)\.\d+\.(?P\d+)\.(?P\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()