Bug 1132771 - Add ability to audit file metadata draft
authorGregory Szorc <gps@mozilla.com>
Tue, 24 Feb 2015 16:25:36 -0800
changeset 245751 1fc4d116123b942fb4a63b5d1be53660e35c8437
parent 245750 c015fb838ecbfe0bf7fd4def92acf272fe8aa5b1
child 505435 3d67908ce3945847385c7650513c021ecdd5eeb2
push id795
push usergszorc@mozilla.com
push dateWed, 25 Feb 2015 00:26:50 +0000
bugs1132771
milestone39.0a1
Bug 1132771 - Add ability to audit file metadata Metadata stored in Files is only useful if it is accurate. We want a process to help ensure the data is proper. This patch introduces |mach file-info --audit|. It will read every moz.build file and look for invalid entries. We currently query Bugzilla for product and user info. If referenced entitites don't exist, we emit a warning. We can't hook this feature up to existing tests because it requires network access. Instead, we'll need to deploy a separate system for periodically running this audit and reporting on results. Something similar to the Jenkins job that's doing Sphinx documentation generation should suffice.
python/mozbuild/mozbuild/frontend/mach_commands.py
python/mozbuild/mozbuild/frontend/reader.py
--- a/python/mozbuild/mozbuild/frontend/mach_commands.py
+++ b/python/mozbuild/mozbuild/frontend/mach_commands.py
@@ -109,19 +109,32 @@ class MozbuildFileCommands(MachCommandBa
                 'Show files missing Bugzilla component info')
     @CommandArgument('paths', nargs='+',
                      help='Paths whose data to query')
     def file_info_missing_bugzilla(self, paths):
         for p, m in sorted(self._get_file_info(paths).items()):
             if 'BUG_COMPONENT' not in m:
                 print(p)
 
+    @SubCommand('file-info', 'audit',
+                'Audit state of file metadata to ensure accuracy')
+    def file_info_audit(self):
+        reader = self._get_reader()
+        res = 0
+        for context, error in reader.audit_file_metadata():
+            print('%s: %s' % (context.main_path, error))
+            res = 1
+        else:
+            print('file metadata validated without error')
+
+        return res
+
     def _get_reader(self):
-        from mozbuild.frontend.reader import BuildReader
-        config = self.config_environment
+        from mozbuild.frontend.reader import BuildReader, EmptyConfig
+        config = EmptyConfig(self.topsrcdir)
         return BuildReader(config)
 
     def _get_file_info(self, paths):
         relpaths = []
         for p in paths:
             a = mozpath.abspath(p)
             if not mozpath.basedir(a, [self.topsrcdir]):
                 print('path is not inside topsrcdir: %s' % p)
--- a/python/mozbuild/mozbuild/frontend/reader.py
+++ b/python/mozbuild/mozbuild/frontend/reader.py
@@ -14,24 +14,26 @@ to fill a Context, representing the outp
 
 The BuildReader contains basic logic for traversing a tree of mozbuild files.
 It does this by examining specific variables populated during execution.
 """
 
 from __future__ import print_function, unicode_literals
 
 import inspect
+import json
 import logging
 import os
 import sys
 import textwrap
 import time
 import tokenize
 import traceback
 import types
+import urllib2
 
 from collections import (
     defaultdict,
     OrderedDict,
 )
 from io import StringIO
 
 from mozbuild.util import (
@@ -1151,8 +1153,72 @@ class BuildReader(object):
 
                 relpath = mozpath.relpath(path, ctx.relsrcdir)
                 if mozpath.match(relpath, ctx.pattern):
                     flags += ctx
 
             r[path] = flags
 
         return r
+
+    def audit_file_metadata(self):
+        """Audit file metadata from all moz.build files."""
+
+        paths, contexts = self.read_relevant_mozbuilds(self.all_mozbuild_paths())
+
+        opener = urllib2.build_opener()
+        products = get_bugzilla_products(opener)
+
+        reviewer_exists = {}
+
+        for context in contexts:
+            if not isinstance(context, Files):
+                continue
+
+            if 'BUG_COMPONENT' in context:
+                bc = context['BUG_COMPONENT']
+                if bc.product not in products:
+                    yield context, 'Invalid Bugzilla product: %s' % bc.product
+                else:
+                    if not products[bc.product][0]:
+                        yield context, 'Bugzilla product not active: %s' % bc.product
+
+                    components = products[bc.product][1]
+
+                    if bc.component not in components:
+                        yield context, 'Invalid Bugzilla component: %s' % bc.component
+                    elif not components[bc.component]:
+                        yield context, 'Bugzilla component not active: %s' % bc.component
+
+            for reviewer in context['REVIEWERS']:
+                if reviewer not in reviewer_exists:
+                    reviewer_exists[reviewer] = bugzilla_user_exists(opener, reviewer)
+
+                if not reviewer_exists[reviewer]:
+                    yield context, 'Reviewer does not exist in Bugzilla: %s' % reviewer
+
+
+def get_bugzilla_products(opener):
+    fields = ['is_active', 'name', 'components.name', 'components.is_active']
+    res = opener.open('https://bugzilla.mozilla.org/rest/product'
+                      '?type=accessible'
+                      '&include_fields=%s' % ','.join(fields))
+    prodjson = json.load(res)
+
+    products = {}
+    for p in prodjson['products']:
+        components = {}
+        for c in p['components']:
+            components[c['name']] = c['is_active']
+
+        products[p['name']] = (p['is_active'], components)
+
+    return products
+
+
+def bugzilla_user_exists(opener, email):
+    url = 'https://bugzilla.mozilla.org/rest/user/%s' % email
+    try:
+        res = opener.open(url)
+        u = json.load(res)
+        return len(u['users']) > 0
+    except urllib2.HTTPError:
+        return False