| Index: infra/tools/cq_stats/test/cq_stats_test.py
|
| diff --git a/infra/tools/cq_stats/test/cq_stats_test.py b/infra/tools/cq_stats/test/cq_stats_test.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..0669d04c80132bb71be6e2f4dc32f9f26dbcbf7b
|
| --- /dev/null
|
| +++ b/infra/tools/cq_stats/test/cq_stats_test.py
|
| @@ -0,0 +1,671 @@
|
| +# Copyright 2015 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +import argparse
|
| +import collections
|
| +import copy
|
| +import datetime
|
| +import itertools
|
| +import logging
|
| +import subprocess
|
| +import time
|
| +import unittest
|
| +import urllib2
|
| +
|
| +import dateutil
|
| +
|
| +from testing_support import auto_stub
|
| +
|
| +from infra.tools.cq_stats import cq_stats
|
| +
|
| +
|
| +class Args(object):
|
| + def __init__(self, **kwargs):
|
| + self.project = 'test_project'
|
| + self.list_rejections = False
|
| + self.list_false_rejections = False
|
| + self.use_logs = False
|
| + self.date = datetime.datetime(2014, 1, 1)
|
| + self.range = 'week'
|
| + self.verbose = 'error'
|
| + self.seq = 'false'
|
| + self.thread_pool = 3
|
| + for name, val in kwargs.iteritems():
|
| + self.__dict__[name] = val
|
| +
|
| +
|
| +class ResponseMock(object):
|
| + """Mock out Response class for urllib2.urlopen()."""
|
| + def __init__(self, lines, retries):
|
| + self.lines = lines
|
| + self.retries = retries
|
| +
|
| + def read(self):
|
| + return '\n'.join(self.lines)
|
| +
|
| + def __iter__(self):
|
| + return self.lines.__iter__()
|
| +
|
| +
|
| +def urlopen_mock(lines, retries=0):
|
| + obj = ResponseMock(lines, retries)
|
| + def func(_):
|
| + if obj.retries:
|
| + obj.retries -= 1
|
| + raise IOError('mock error')
|
| + return obj
|
| + return func
|
| +
|
| +
|
| +def ensure_serializable(obj):
|
| + if isinstance(obj, dict):
|
| + return {ensure_serializable(k): ensure_serializable(v)
|
| + for k, v in obj.iteritems()}
|
| + elif isinstance(obj, (list, set)):
|
| + return [ensure_serializable(i) for i in obj]
|
| + elif isinstance(obj, datetime.datetime):
|
| + return obj.isoformat()
|
| + elif isinstance(obj, float):
|
| + # Ensure consistent float results - generally float arithmetic
|
| + # can be slightly different between CPUs and implementations.
|
| + return round(obj, 2)
|
| + else:
|
| + return obj
|
| +
|
| +
|
| +class TestCQStats(auto_stub.TestCase):
|
| + def setUp(self):
|
| + super(TestCQStats, self).setUp()
|
| + self.expectations = []
|
| +
|
| + def tearDown(self):
|
| + self.expectations = []
|
| + super(TestCQStats, self).tearDown()
|
| +
|
| + def print_mock(self, fmt='', *args):
|
| + # Make sure lines are correctly split when \n is in the string.
|
| + # This preserves the expectations when going from
|
| + # print;print('string') to print('\nstring').
|
| + self.expectations += ((fmt + '\n') % args).splitlines()
|
| +
|
| + def test_output(self):
|
| + cq_stats.output('')
|
| +
|
| + def test_parse_args(self):
|
| + self.mock(argparse.ArgumentParser, 'parse_args',
|
| + lambda *_: Args(date='2014-01-01'))
|
| + self.assertIsNotNone(cq_stats.parse_args())
|
| + self.mock(argparse.ArgumentParser, 'parse_args',
|
| + lambda *_: Args(date=None))
|
| + self.assertIsNotNone(cq_stats.parse_args())
|
| +
|
| + def test_date_from_string(self):
|
| + self.assertRaises(ValueError, cq_stats.date_from_string, 'bad time')
|
| + self.assertEqual(cq_stats.date_from_string('2014-10-15'),
|
| + datetime.datetime(2014, 10, 15))
|
| +
|
| + def test_date_from_timestamp(self):
|
| + self.assertIs(type(cq_stats.date_from_timestamp(12345678.9)),
|
| + datetime.datetime)
|
| +
|
| + def test_date_from_git(self):
|
| + self.assertIsNone(cq_stats.date_from_git(''))
|
| + self.assertIsNone(cq_stats.date_from_git('bad time'))
|
| + self.assertEqual(cq_stats.date_from_git('Tue Oct 21 22:38:39 2014'),
|
| + datetime.datetime(2014, 10, 21, 22, 38, 39))
|
| +
|
| + def test_fetch_json(self):
|
| + self.mock(time, 'sleep', lambda n: None)
|
| +
|
| + self.mock(urllib2, 'urlopen', urlopen_mock(['{"a": "b"}']))
|
| + self.assertEqual(cq_stats.fetch_json('https://'), {'a': 'b'})
|
| +
|
| + self.mock(urllib2, 'urlopen', urlopen_mock(['{"a": "b"}'], retries=1))
|
| + self.assertEqual(cq_stats.fetch_json('https://'), {'a': 'b'})
|
| +
|
| + self.mock(urllib2, 'urlopen', urlopen_mock(['{"a": "b"}'], retries=100))
|
| + self.assertEqual(cq_stats.fetch_json('https://'), {'error': '404'})
|
| +
|
| + self.mock(urllib2, 'urlopen', urlopen_mock(['{([bad json']))
|
| + self.assertEqual(cq_stats.fetch_json('https://'), {'error': '404'})
|
| +
|
| + def test_fetch_tree_status(self):
|
| + # Invalid result
|
| + self.mock(cq_stats, 'fetch_json', lambda url: {})
|
| + self.assertEqual([], cq_stats.fetch_tree_status(
|
| + 'chromium', datetime.datetime(2014, 10, 15)))
|
| + # Valid result
|
| + res = [{'date': '2014-10-01 14:54:44.553',
|
| + 'general_state': 'open'},
|
| + {'date': '2014-10-14 10:54:44',
|
| + 'general_state': 'closed'},
|
| + {'date': '2014-10-16 10:54:44',
|
| + 'general_state': 'closed'},
|
| + ]
|
| + self.mock(cq_stats, 'fetch_json', lambda url: res)
|
| +
|
| + status1 = cq_stats.fetch_tree_status(
|
| + 'chromium', datetime.datetime(2014, 10, 15))
|
| +
|
| + status2 = cq_stats.fetch_tree_status(
|
| + 'chromium', datetime.datetime(2014, 10, 17),
|
| + start_date= datetime.datetime(2014, 10, 15))
|
| +
|
| + return map(ensure_serializable, [status1, status2])
|
| +
|
| + def test_fetch_git_page(self):
|
| + self.mock(urllib2, 'urlopen', urlopen_mock(['{([bad json']))
|
| + self.assertEqual({}, cq_stats.fetch_git_page('url'))
|
| + self.mock(urllib2, 'urlopen', urlopen_mock([
|
| + ")]}'", '{"json": 1}',
|
| + ]))
|
| + self.assertEqual({'json': 1}, cq_stats.fetch_git_page('url'))
|
| + self.assertEqual({'json': 1},
|
| + cq_stats.fetch_git_page('url', cursor='cursor'))
|
| +
|
| + def test_fetch_git_logs(self):
|
| + pages = [
|
| + {'log': [
|
| + {'author': {'email': 'noone@chromium.org'},
|
| + 'committer': {'email': 'commit-bot@chromium.org',
|
| + 'time': 'Tue Dec 23 22:38:39 2014'}},
|
| + {'author': {'email': 'noone@chromium.org'},
|
| + 'committer': {'email': 'commit-bot@chromium.org',
|
| + 'time': 'Tue Nov 23 22:38:39 2014'}},
|
| + {'author': {'email': 'someone@chromium.org'},
|
| + 'committer': {'email': 'anyone@chromium.org',
|
| + 'time': 'Tue Oct 22 22:38:39 2014'}},
|
| + {'author': {'email': 'blink-deps-roller@chromium.org'},
|
| + 'committer': {'email': 'commit-bot@chromium.org',
|
| + 'time': 'Tue Oct 21 23:38:39 2014'}},
|
| + {'author': {'email': 'blink-deps-roller@chromium.org'},
|
| + 'committer': {'email': 'blink-deps-roller@chromium.org',
|
| + 'time': 'Tue Oct 21 22:38:39 2014'}}
|
| + ],
|
| + 'next': 1,
|
| + },
|
| + {'log': [
|
| + {'author': {'email': 'someone@chromium.org'},
|
| + 'committer': {'email': 'anyone@chromium.org'}},
|
| + {'author': {'email': 'nobody@chromium.org'},
|
| + 'committer': {'email': 'commit-bot@chromium.org',
|
| + 'time': 'Tue Sep 21 22:38:39 2014'}},
|
| + ],
|
| + },
|
| + ]
|
| + # Unused arguments: pylint: disable=W0613
|
| + def fetch_mock(repo_url, cursor=None, page_size=2000):
|
| + if not cursor:
|
| + cursor = 0
|
| + return pages[int(cursor)]
|
| +
|
| + self.mock(cq_stats, 'fetch_git_page', fetch_mock)
|
| +
|
| + data = cq_stats.fetch_git_logs(
|
| + 'chromium',
|
| + datetime.datetime(2014, 10, 1),
|
| + datetime.datetime(2014, 12, 1))
|
| +
|
| + derived_data = cq_stats.derive_git_stats(
|
| + 'chromium',
|
| + datetime.datetime(2014, 9, 1),
|
| + datetime.datetime(2014, 12, 1),
|
| + ['blink-deps-roller@chromium.org'])
|
| +
|
| + return map(ensure_serializable, [data, derived_data])
|
| +
|
| + def test_fetch_svn_logs(self):
|
| + xml = """<?xml version="1.0" encoding="UTF-8"?>
|
| +<log>
|
| +<logentry
|
| + revision="184775">
|
| +<author>amikhaylova@google.com</author>
|
| +<date>2014-11-01T20:49:20.468030Z</date>
|
| +<msg>Move Promise Tracker out of hidden experiments.
|
| +
|
| +BUG=348919
|
| +
|
| +Review URL: https://codereview.chromium.org/697833002</msg>
|
| +<revprops>
|
| +<property
|
| + name="commit-bot">commit-bot@chromium.org</property>
|
| +</revprops>
|
| +</logentry>
|
| +<logentry
|
| + revision="184774">
|
| +<author>amikhaylova@google.com</author>
|
| +<date>2014-11-01T20:49:20.468030Z</date>
|
| +<msg>Move Promise Tracker out of hidden experiments.
|
| +
|
| +BUG=348919
|
| +
|
| +Review URL: https://codereview.chromium.org/697833002</msg>
|
| +<revprops>
|
| +<property
|
| + name="foo">bar</property>
|
| +</revprops>
|
| +</logentry>
|
| +<logentry
|
| + revision="184773">
|
| +<author>amikhaylova@google.com</author>
|
| +<date>2014-11-01T20:49:20.468030Z</date>
|
| +<msg>Move Promise Tracker out of hidden experiments.
|
| +
|
| +BUG=348919
|
| +
|
| +Review URL: https://codereview.chromium.org/697833002</msg>
|
| +</logentry>
|
| +</log>
|
| +"""
|
| + self.mock(subprocess, 'check_output', lambda *_: xml)
|
| + data = cq_stats.fetch_svn_logs(
|
| + 'chromium',
|
| + datetime.datetime(2014, 1, 1),
|
| + datetime.datetime(2014, 1, 1))
|
| +
|
| + derived_data = cq_stats.derive_svn_stats(
|
| + 'chromium',
|
| + datetime.datetime(2014, 1, 1),
|
| + datetime.datetime(2014, 1, 1),
|
| + [])
|
| +
|
| + return map(ensure_serializable, [data, derived_data])
|
| +
|
| + def test_fetch_stats(self):
|
| + self.mock(cq_stats, 'fetch_json', lambda _: 'json')
|
| + self.assertEqual('json', cq_stats.fetch_stats(Args()))
|
| + self.assertEqual('json', cq_stats.fetch_stats(Args(date=None)))
|
| + self.assertEqual('json', cq_stats.fetch_stats(
|
| + Args(), datetime.datetime(2014, 10, 15)))
|
| + self.assertEqual('json', cq_stats.fetch_stats(
|
| + Args(), datetime.datetime(2014, 10, 15), 'day'))
|
| +
|
| + def test_fetch_cq_logs(self):
|
| + def mkresults(series):
|
| + return [{'a': n} for n in series]
|
| + pages_default = [
|
| + {'more': True,
|
| + 'cursor': '!@#$%^',
|
| + 'results': mkresults(range(1, 3)),
|
| + },
|
| + {'more': False,
|
| + 'results': mkresults(range(3, 6)),
|
| + },
|
| + ]
|
| + expected_result = mkresults(range(1, 6))
|
| +
|
| + start_date = datetime.datetime(2014, 10, 15)
|
| + end_date = datetime.datetime(2014, 10, 20)
|
| + pages = []
|
| +
|
| + def fetch_json_mock(_):
|
| + return pages.pop(0)
|
| +
|
| + self.mock(cq_stats, 'fetch_json', fetch_json_mock)
|
| + pages[:] = pages_default
|
| + self.assertEqual(cq_stats.fetch_cq_logs(), expected_result)
|
| + pages[:] = pages_default
|
| + self.assertEqual(cq_stats.fetch_cq_logs(start_date=start_date),
|
| + expected_result)
|
| + pages[:] = pages_default
|
| + self.assertEqual(cq_stats.fetch_cq_logs(end_date=end_date),
|
| + expected_result)
|
| +
|
| + def test_organize_stats(self):
|
| + stats = {'results': [
|
| + {'begin': t,
|
| + 'stats': [
|
| + {'count': 3, 'type': 'count',
|
| + 'name': 'attempt-count'},
|
| + {'count': 2, 'type': 'count',
|
| + 'name': 'trybot-bot-false-reject-count'},
|
| + {'count': 1, 'type': 'count',
|
| + 'name': 'trybot-bot-pass-count'},
|
| + {'description': 'Total time spent per CQ attempt.',
|
| + 'max': 9999.99999,
|
| + 'percentile_25': 2512.34567,
|
| + 'percentile_75': 7512.34567,
|
| + 'percentile_10': 1012.34567,
|
| + 'unit': 'seconds',
|
| + 'name': 'attempt-durations',
|
| + 'percentile_50': 5012.34567,
|
| + 'min': 0.00001,
|
| + 'sample_size': 10000,
|
| + 'percentile_90': 9012.34567,
|
| + 'percentile_95': 9512.34567,
|
| + 'percentile_99': 9912.34567,
|
| + 'type': 'list',
|
| + 'mean': 5555.555555},
|
| + ],
|
| + 'interval_minutes': 15,
|
| + 'project': 'chromium',
|
| + 'key': 5976204561612800,
|
| + 'end': t + 900} for t in [1415138400, 1415139300]]}
|
| +
|
| + result = cq_stats.organize_stats(stats)
|
| +
|
| + # Test that the result stats have the minimal expected dict keys
|
| + # for print_stats().
|
| + expected_keys = set(cq_stats.default_stats().keys())
|
| + self.assertFalse(expected_keys - set(result['latest'].keys()))
|
| + self.assertFalse(expected_keys - set(result['previous'].keys()))
|
| +
|
| + self.assertIsNone(cq_stats.organize_stats({}))
|
| +
|
| + return ensure_serializable(result)
|
| +
|
| + def test_derive_list_stats(self):
|
| + series = range(100)
|
| + stats = cq_stats.derive_list_stats(series)
|
| + # Ensure consistent float results - generally float arithmetic
|
| + # can be slightly different between CPUs and implementations.
|
| + stats = {k: round(v, 2) for k, v in stats.iteritems()}
|
| + self.assertDictEqual({
|
| + '10': 9.9,
|
| + '25': 24.75,
|
| + '50': 49.5,
|
| + '75': 74.25,
|
| + '90': 89.1,
|
| + '95': 94.05,
|
| + '99': 98.01,
|
| + 'max': 99.0,
|
| + 'mean': 49.5,
|
| + 'min': 0.0,
|
| + 'size': 100.0,
|
| + }, stats)
|
| +
|
| + self.assertEqual(cq_stats.derive_list_stats([])['size'], 1)
|
| +
|
| + def mock_derive_patch_stats(self, _, patch_id):
|
| + # The original function expects patch_id to be a 2-tuple.
|
| + self.assertIsInstance(patch_id, tuple)
|
| + self.assertEqual(len(patch_id), 2)
|
| + # Note: these fields are required by derive_stats(). Make sure
|
| + # they are present in the unit tests for derive_patch_stats().
|
| + stats = {
|
| + 'attempts': 3,
|
| + 'false-rejections': 1,
|
| + 'rejections': 2,
|
| + 'committed': True,
|
| + 'patchset-duration-wallclock': 1234.56,
|
| + 'patchset-duration': 999.99,
|
| + 'failed-jobs-details': {'tester': 2},
|
| + }
|
| + return patch_id, stats
|
| +
|
| + def test_derive_stats(self):
|
| + # Unused args: pylint: disable=W0613
|
| + def mock_fetch_cq_logs_0(begin_date=None, end_date=None, filters=None):
|
| + return []
|
| + # Unused args: pylint: disable=W0613
|
| + def mock_fetch_cq_logs(begin_date=None, end_date=None, filters=None):
|
| + return [
|
| + {'fields': {'issue': 12345, 'patchset': 1},
|
| + 'timestamp': 1415150483.18568,
|
| + },
|
| + ]
|
| +
|
| + self.mock(cq_stats, 'derive_patch_stats', self.mock_derive_patch_stats)
|
| + # Test empty logs.
|
| + self.mock(cq_stats, 'fetch_cq_logs', mock_fetch_cq_logs_0)
|
| + self.assertEqual(dict, type(cq_stats.derive_stats(
|
| + Args(), datetime.datetime(2014, 10, 15))))
|
| + # Non-empty logs.
|
| + self.mock(cq_stats, 'fetch_cq_logs', mock_fetch_cq_logs)
|
| + self.assertEqual(dict, type(cq_stats.derive_stats(
|
| + Args(seq=False), datetime.datetime(2014, 10, 15))))
|
| + self.assertEqual(dict, type(cq_stats.derive_stats(
|
| + Args(seq=True), datetime.datetime(2014, 10, 15))))
|
| +
|
| + def test_stats_by_count_entry(self):
|
| + common = {'failed-jobs-details': 'jobs', 'reason1': 2, 'reason2': 3}
|
| + patch_stats = {'some-count': 5}
|
| + patch_stats.update(common)
|
| + expected = {'count': 5, 'patch_id': 'patch'}
|
| + expected.update(common)
|
| + self.assertEqual(expected, cq_stats.stats_by_count_entry(
|
| + patch_stats, 'some-count', 'patch', ['reason1', 'reason2']))
|
| +
|
| + def test_parse_json(self):
|
| + self.assertEqual({'a': 5}, cq_stats.parse_json('{"a": 5}'))
|
| + self.assertEqual({'a': 5}, cq_stats.parse_json({'a': 5}))
|
| + self.assertEqual('bad json)}', cq_stats.parse_json('bad json)}'))
|
| + self.assertEqual({}, cq_stats.parse_json('bad json)}', return_type=dict))
|
| +
|
| + def test_parse_failing_tryjobs(self):
|
| + message = (
|
| + 'Try jobs failed on following builders:\n'
|
| + ' try_rel on tryserver.fake (http://url.com/8633)\n'
|
| + ' dont_try_rel on tryserver.fake (http://url.com/8634)')
|
| + self.assertEqual(['try_rel', 'dont_try_rel'],
|
| + cq_stats.parse_failing_tryjobs(message))
|
| + self.assertEqual([], cq_stats.parse_failing_tryjobs(''))
|
| + self.assertEqual([], cq_stats.parse_failing_tryjobs('single line'))
|
| + self.assertEqual([], cq_stats.parse_failing_tryjobs('empty line\n\n'))
|
| +
|
| + def test_derive_patch_stats(self):
|
| + time_obj = {'time': 1415150492.4}
|
| + def attempt(message, commit=False, reason=''):
|
| + time_obj['time'] += 1.37 # Trick python to use global var.
|
| + entries = []
|
| + entries.append({'fields': {'action': 'patch_start'},
|
| + 'timestamp': time_obj['time']})
|
| + time_obj['time'] += 1.37
|
| + if commit:
|
| + entries.append({'fields': {'action': 'patch_committed'},
|
| + 'timestamp': time_obj['time']})
|
| + else:
|
| + entries.append({'fields': {'action': 'patch_failed',
|
| + 'reason': {'fail_type': reason}},
|
| + 'timestamp': time_obj['time']})
|
| + time_obj['time'] += 1.37
|
| + entries.append({'fields': {'action': 'patch_stop', 'message': message},
|
| + 'timestamp': time_obj['time']})
|
| + return entries
|
| +
|
| + attempts = [
|
| + attempt('CQ bit was unchecked on CL'),
|
| + attempt('No LGTM from valid reviewers', reason='reviewer_lgtm'),
|
| + attempt('A disapproval has been posted'),
|
| + attempt('Transient error: Invalid delimiter'),
|
| + attempt('Failed to commit', reason='commit'),
|
| + attempt('Failed to apply patch'),
|
| + attempt('Presubmit check'),
|
| + attempt('Try jobs failed:\n test_dbg', reason='simple try job'),
|
| + attempt('Try jobs failed:\n chromium_presubmit'),
|
| + attempt('Exceeded time limit waiting for builds to trigger'),
|
| + attempt('Some totally random unknown reason') + [
|
| + {'fields': {'action': 'random garbage'},
|
| + 'timestamp': time_obj['time'] + 0.5}],
|
| + attempt('', commit=True),
|
| + ]
|
| +
|
| + # Dangerous default value, unused args: pylint: disable=W0102,W0613
|
| + def mock_fetch_cq_logs(begin_date=None, end_date=None, filters=[]):
|
| + entries = list(itertools.chain(*attempts))
|
| + entries.reverse()
|
| + return entries
|
| +
|
| + # Dangerous default value, unused args: pylint: disable=W0102,W0613
|
| + def mock_fetch_cq_logs_0(begin_date=None, end_date=None, filters=[]):
|
| + return []
|
| +
|
| + # Dangerous default value, unused args: pylint: disable=W0102,W0613
|
| + def mock_fetch_cq_logs_junk(begin_date=None, end_date=None, filters=[]):
|
| + return [{'fields': {'action': 'cq_start'}, 'timestamp': 1415150662.3}]
|
| +
|
| + self.mock(cq_stats, 'fetch_cq_logs', mock_fetch_cq_logs)
|
| +
|
| + patch_id = ('pid', 5)
|
| + pid, stats = cq_stats.derive_patch_stats(
|
| + datetime.datetime(2014, 10, 15), patch_id)
|
| + self.assertEqual(patch_id, pid)
|
| + # Check required fields in the result.
|
| + for k in self.mock_derive_patch_stats(None, patch_id)[1]:
|
| + self.assertIsNotNone(stats.get(k))
|
| + # A few sanity checks.
|
| + self.assertEqual(stats['attempts'], len(attempts))
|
| + self.assertEqual(stats['committed'], True)
|
| + self.assertGreater(stats['false-rejections'], 0)
|
| +
|
| + self.mock(cq_stats, 'fetch_cq_logs', mock_fetch_cq_logs_0)
|
| + pid, stats = cq_stats.derive_patch_stats(
|
| + datetime.datetime(2014, 10, 15), patch_id)
|
| + # Cover the case when there are actions, but no CQ attempts.
|
| + self.mock(cq_stats, 'fetch_cq_logs', mock_fetch_cq_logs_junk)
|
| + pid, stats = cq_stats.derive_patch_stats(
|
| + datetime.datetime(2014, 10, 15), patch_id)
|
| +
|
| +
|
| + def test_derive_tree_stats(self):
|
| + def makeDate(days=0, hours=0, minutes=0, seconds=0):
|
| + start_date = datetime.datetime(2014, 10, 1, 15, 20, 12, 345)
|
| + return start_date + datetime.timedelta(
|
| + days=days, seconds=hours*3600+minutes*60+seconds)
|
| +
|
| + events = [
|
| + {'date': makeDate(-1),
|
| + 'open': True},
|
| + {'date': makeDate(0, 12, 35, 11),
|
| + 'open': False},
|
| + {'date': makeDate(0, 12, 45, 53),
|
| + 'open': True},
|
| + {'date': makeDate(0, 23, 59, 51),
|
| + 'open': False},
|
| + {'date': makeDate(0, 23, 59, 55),
|
| + 'open': True},
|
| + {'date': makeDate(1, 3, 43, 32),
|
| + 'open': False},
|
| + ]
|
| + # pylint: disable=unused-argument
|
| + def mock_fetch(_project, end_date, _start_date=None, limit=1000):
|
| + return [e for e in events if e['date'] <= end_date]
|
| +
|
| + self.mock(cq_stats, 'fetch_tree_status', mock_fetch)
|
| + self.assertEqual(
|
| + cq_stats.derive_tree_stats('project', makeDate(0), makeDate(1)),
|
| + {'open': 85754.0, 'total': 3600.0 * 24})
|
| + self.assertEqual(
|
| + cq_stats.derive_tree_stats('project', makeDate(0), makeDate(2)),
|
| + {'open': 99166.0, 'total': 3600.0 * 24 * 2})
|
| +
|
| + def empty_fetch(_project, end_date, _start_date=None, limit=1000):
|
| + return []
|
| + self.mock(cq_stats, 'fetch_tree_status', empty_fetch)
|
| + self.assertEqual(
|
| + cq_stats.derive_tree_stats('project', makeDate(0), makeDate(1)),
|
| + {'open': 0.0, 'total': 3600.0 * 24})
|
| +
|
| + def test_print_attempt_counts(self):
|
| + self.mock(cq_stats, 'output', self.print_mock)
|
| +
|
| + stats = cq_stats.default_stats()
|
| + stats['patch_stats'] = {
|
| + (123, 1): {
|
| + 'attempts': 1,
|
| + 'false-rejections': 0,
|
| + 'rejections': 1,
|
| + 'committed': False,
|
| + 'patchset-duration': 3600,
|
| + 'patchset-duration-wallclock': 3600,
|
| + 'failed-jobs-details': {
|
| + 'builder_a': 1,
|
| + },
|
| + },
|
| + }
|
| + cq_stats._derive_stats_from_patch_stats(stats)
|
| +
|
| + cq_stats.print_attempt_counts(
|
| + stats, 'rejections', 'were unsuccessful',
|
| + item_name=None, committed=False, details=True)
|
| +
|
| + cq_stats.print_attempt_counts(
|
| + stats, 'rejections', 'failed jobs',
|
| + item_name=None, committed=False)
|
| +
|
| + return self.expectations
|
| +
|
| + def test_print_duration(self):
|
| + self.mock(cq_stats, 'output', self.print_mock)
|
| +
|
| + cq_stats.print_duration('mean', Args(), cq_stats.default_stats(), None)
|
| + return self.expectations
|
| +
|
| + def test_print_usage(self):
|
| + self.mock(cq_stats, 'output', self.print_mock)
|
| +
|
| + stats = cq_stats.default_stats()
|
| + stats['usage'] = cq_stats.derive_log_stats([], [])
|
| + cq_stats.print_usage(Args(), stats, stats)
|
| +
|
| + stats['usage']['bot_manual_commits'] += 1
|
| + cq_stats.print_usage(Args(), stats, stats)
|
| +
|
| + return self.expectations
|
| +
|
| + # Expectation: must print stats in a certain format.
|
| + # Assumption: input stats at minimum have the keys from
|
| + # default_stats(). This is verified in test_organize_stats().
|
| + def test_print_stats(self):
|
| + self.mock(cq_stats, 'output', self.print_mock)
|
| + args = Args()
|
| + stats_set = cq_stats.default_stats()
|
| + stats_set['begin'] = args.date
|
| + stats_set['end'] = args.date + datetime.timedelta(days=7)
|
| +
|
| + stats_set['jobs'].update({
|
| + 'foo_builder': {
|
| + 'pass-count': 100,
|
| + 'false-reject-count': 1,
|
| + },
|
| + })
|
| +
|
| + swapped_stats = copy.deepcopy(stats_set)
|
| + swapped_stats['begin'], swapped_stats['end'] = (
|
| + swapped_stats['end'], swapped_stats['begin'])
|
| +
|
| + cq_stats.print_stats(args, {'latest': None, 'previous': stats_set})
|
| + cq_stats.print_stats(args, {'latest': stats_set, 'previous': None})
|
| + cq_stats.print_stats(args, {'latest': swapped_stats, 'previous': stats_set})
|
| + cq_stats.print_stats(args, {'latest': stats_set, 'previous': stats_set})
|
| + return self.expectations
|
| +
|
| + def test_print_log_stats(self):
|
| + self.mock(cq_stats, 'output', self.print_mock)
|
| + args = Args(use_logs=True)
|
| + stats_set = cq_stats.default_stats()
|
| + stats_set['begin'] = args.date
|
| + stats_set['end'] = args.date + datetime.timedelta(days=7)
|
| +
|
| + cq_stats.print_stats(args, {'latest': stats_set, 'previous': stats_set})
|
| + return self.expectations
|
| +
|
| + def test_acquire_stats(self):
|
| + self.mock(cq_stats, 'fetch_json', lambda _: 'json')
|
| + self.mock(cq_stats, 'organize_stats',
|
| + lambda *_args, **_kwargs: {
|
| + 'latest': cq_stats.default_stats(),
|
| + 'previous': cq_stats.default_stats()})
|
| + self.mock(cq_stats, 'derive_stats', lambda *_args, **_kwargs: {})
|
| + self.mock(cq_stats, 'derive_tree_stats',
|
| + lambda *_: {'open': 0.0, 'total': 3600.0})
|
| + self.mock(cq_stats, 'derive_git_stats', lambda *_: {})
|
| + self.mock(cq_stats, 'derive_svn_stats', lambda *_: {})
|
| +
|
| + cq_stats.acquire_stats(Args(project='blink', bots=[]))
|
| + cq_stats.acquire_stats(Args(project='chromium', bots=[]))
|
| + cq_stats.acquire_stats(Args(
|
| + project='chromium', bots=[], use_logs=True, range='week'))
|
| + cq_stats.acquire_stats(Args(
|
| + project='chromium', bots=[], use_logs=True, range='day'))
|
| + cq_stats.acquire_stats(Args(
|
| + project='chromium', bots=[], use_logs=True, range='hour'))
|
| +
|
| + def test_main(self):
|
| + self.mock(cq_stats, 'output', self.print_mock)
|
| + self.mock(cq_stats, 'parse_args', lambda: Args(
|
| + project='chromium', log_level=logging.CRITICAL, logs_black_list=None))
|
| + self.mock(cq_stats, 'acquire_stats', lambda _: cq_stats.default_stats())
|
| + cq_stats.main()
|
| + return self.expectations
|
|
|