bug 1361037, optionally track file status in standard Observer, r=flod, stas
authorAxel Hecht <axel@pike.org>
Wed, 17 May 2017 15:19:53 +0200
changeset 238 af79116532204027f849c5c673b1f0fad9e39b1c
parent 237 f9fb89c4d86ad2e1f6a84b24340ad1c88b74a8b1
child 239 24750ee963fd51e76410593f1ed5d703b6545bc2
push id48
push useraxel@mozilla.com
push dateFri, 26 May 2017 11:10:47 +0000
reviewersflod, stas
bugs1361037
bug 1361037, optionally track file status in standard Observer, r=flod, stas This functionality previously lived in https://github.com/Pike/master-ball/blob/b19f6163ff96e445cc37bade88c55338c52ee1da/vendor-local/l10ninsp/slave.py#L23-L34. But that requires that I only run one project at a time, as we otherwise lose which files are in which project. Also, track all metrics, not just unchanged and missing. MozReview-Commit-ID: 4ANzjwcARjQ
compare_locales/compare.py
compare_locales/paths.py
compare_locales/tests/test_compare.py
--- a/compare_locales/compare.py
+++ b/compare_locales/compare.py
@@ -147,61 +147,73 @@ class AddRemove(SequenceMatcher):
                 for item in self.b[j1:j2]:
                     yield ('add', item)
 
 
 class Observer(object):
     stat_cats = ['missing', 'obsolete', 'missingInFiles', 'report',
                  'changed', 'unchanged', 'keys']
 
-    def __init__(self):
-        class intdict(defaultdict):
-            def __init__(self):
-                defaultdict.__init__(self, int)
-
-        self.summary = defaultdict(intdict)
+    def __init__(self, file_stats=False):
+        self.summary = defaultdict(lambda: defaultdict(int))
         self.details = Tree(dict)
         self.filter = None
+        self.file_stats = None
+        if file_stats:
+            self.file_stats = defaultdict(lambda: defaultdict(dict))
 
     # support pickling
     def __getstate__(self):
-        return dict(summary=self.getSummary(), details=self.details)
+        state = dict(summary=self._dictify(self.summary), details=self.details)
+        if self.file_stats is not None:
+            state['file_stats'] = self._dictify(self.file_stats)
+        return state
 
     def __setstate__(self, state):
-        class intdict(defaultdict):
-            def __init__(self):
-                defaultdict.__init__(self, int)
-
-        self.summary = defaultdict(intdict)
+        self.summary = defaultdict(lambda: defaultdict(int))
         if 'summary' in state:
             for loc, stats in state['summary'].iteritems():
                 self.summary[loc].update(stats)
+        self.file_stats = None
+        if 'file_stats' in state:
+            self.file_stats = defaultdict(lambda: defaultdict(dict))
+            for k, d in state['file_stats'].iteritems():
+                self.file_stats[k].update(d)
         self.details = state['details']
         self.filter = None
 
-    def getSummary(self):
+    def _dictify(self, d):
         plaindict = {}
-        for k, v in self.summary.iteritems():
+        for k, v in d.iteritems():
             plaindict[k] = dict(v)
         return plaindict
 
     def toJSON(self):
-        return dict(summary=self.getSummary(), details=self.details.toJSON())
+        # Don't export file stats, even if we collected them.
+        # Those are not part of the data we use toJSON for.
+        return {
+            'summary': self._dictify(self.summary),
+            'details': self.details.toJSON()
+        }
 
     def notify(self, category, file, data):
         rv = "error"
         if category in self.stat_cats:
             # these get called post reporting just for stats
             # return "error" to forward them to other other_observers
             # in multi-project scenarios, this file might not be ours,
             # check that.
             if (self.filter is not None and
                     self.filter(file) in (None, 'ignore')):
                 return 'ignore'
             self.summary[file.locale][category] += data
+            if self.file_stats is not None:
+                # missingInFiles should just be "missing" in file stats
+                cat = category if category != 'missingInFiles' else 'missing'
+                self.file_stats[file.locale][file.localpath][cat] = data
             # keep track of how many strings are in a missing file
             # we got the {'missingFile': 'error'} from the first pass
             if category == 'missingInFiles':
                 self.details[file]['strings'] = data
             return "error"
         if category in ['missingFile', 'obsoleteFile']:
             if self.filter is not None:
                 rv = self.filter(file)
@@ -554,23 +566,24 @@ class ContentComparer:
         pass
 
     def doChanged(self, file, ref_entity, l10n_entity):
         # overload this if needed
         pass
 
 
 def compareProjects(project_configs, other_observer=None,
+                    file_stats=False,
                     merge_stage=None, clobber_merge=False):
     comparer = ContentComparer()
     if other_observer is not None:
         comparer.add_observer(other_observer)
     locales = set()
     for project in project_configs:
-        observer = Observer()
+        observer = Observer(file_stats=file_stats)
         observer.filter = project.filter
         comparer.observers.append(observer)
         locales.update(project.locales)
     for locale in sorted(locales):
         files = paths.ProjectFiles(locale, *project_configs)
         if merge_stage is not None:
             mergedir = merge_stage.format(ab_CD=locale)
             comparer.set_merge_stage(mergedir)
--- a/compare_locales/paths.py
+++ b/compare_locales/paths.py
@@ -421,21 +421,25 @@ class File(object):
         self.module = module
         self.locale = locale
         pass
 
     def getContents(self):
         # open with universal line ending support and read
         return open(self.fullpath, 'rU').read()
 
-    def __hash__(self):
+    @property
+    def localpath(self):
         f = self.file
         if self.module:
-            f = self.module + '/' + f
-        return hash(f)
+            f = mozpath.join(self.module, f)
+        return f
+
+    def __hash__(self):
+        return hash(self.localpath)
 
     def __str__(self):
         return self.fullpath
 
     def __cmp__(self, other):
         if not isinstance(other, File):
             raise NotImplementedError
         rv = cmp(self.module, other.module)
--- a/compare_locales/tests/test_compare.py
+++ b/compare_locales/tests/test_compare.py
@@ -1,15 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import unittest
 
-from compare_locales import compare
+from compare_locales import compare, paths
+from cPickle import loads, dumps
 
 
 class TestTree(unittest.TestCase):
     '''Test the Tree utility class
 
     Tree value classes need to be in-place editable
     '''
 
@@ -83,8 +84,97 @@ two/other
                             ('other',
                              {'value': {'leaf': 2}}
                              )
                         ]
                     })
                 ]
             }
         )
+
+
+class TestObserver(unittest.TestCase):
+    def test_simple(self):
+        obs = compare.Observer()
+        f = paths.File('/some/real/sub/path', 'sub/path', locale='de')
+        obs.notify('missingEntity', f, ['one', 'two'])
+        obs.notify('missing', f, 15)
+        self.assertDictEqual(obs.toJSON(), {
+            'summary': {
+                'de': {
+                    'missing': 15
+                }
+            },
+            'details': {
+                'children': [
+                    ('de/sub/path',
+                     {'value': {'missingEntity': [['one', 'two']]}}
+                     )
+                ]
+            }
+        })
+        clone = loads(dumps(obs))
+        self.assertDictEqual(clone.summary, obs.summary)
+        self.assertDictEqual(clone.details.toJSON(), obs.details.toJSON())
+        self.assertIsNone(clone.file_stats)
+
+    def test_module(self):
+        obs = compare.Observer(file_stats=True)
+        f = paths.File('/some/real/sub/path', 'path',
+                       module='sub', locale='de')
+        obs.notify('missingEntity', f, ['one', 'two'])
+        obs.notify('missing', f, 15)
+        self.assertDictEqual(obs.toJSON(), {
+            'summary': {
+                'de': {
+                    'missing': 15
+                }
+            },
+            'details': {
+                'children': [
+                    ('de/sub/path',
+                     {'value': {'missingEntity': [['one', 'two']]}}
+                     )
+                ]
+            }
+        })
+        self.assertDictEqual(obs.file_stats, {
+            'de': {
+                'sub/path': {
+                    'missing': 15
+                }
+            }
+        })
+        clone = loads(dumps(obs))
+        self.assertDictEqual(clone.summary, obs.summary)
+        self.assertDictEqual(clone.details.toJSON(), obs.details.toJSON())
+        self.assertDictEqual(clone.file_stats, obs.file_stats)
+
+    def test_file_stats(self):
+        obs = compare.Observer(file_stats=True)
+        f = paths.File('/some/real/sub/path', 'sub/path', locale='de')
+        obs.notify('missingEntity', f, ['one', 'two'])
+        obs.notify('missing', f, 15)
+        self.assertDictEqual(obs.toJSON(), {
+            'summary': {
+                'de': {
+                    'missing': 15
+                }
+            },
+            'details': {
+                'children': [
+                    ('de/sub/path',
+                     {'value': {'missingEntity': [['one', 'two']]}}
+                     )
+                ]
+            }
+        })
+        self.assertDictEqual(obs.file_stats, {
+            'de': {
+                'sub/path': {
+                    'missing': 15
+                }
+            }
+        })
+        clone = loads(dumps(obs))
+        self.assertDictEqual(clone.summary, obs.summary)
+        self.assertDictEqual(clone.details.toJSON(), obs.details.toJSON())
+        self.assertDictEqual(clone.file_stats, obs.file_stats)