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
--- 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)