| #!/usr/bin/python |
| |
| # Copyright (c) 2014 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Unit tests for gather_builder_stats.""" |
| |
| import datetime |
| import itertools |
| import os |
| import random |
| import sys |
| import unittest |
| |
| sys.path.insert(0, os.path.abspath('%s/../..' % os.path.dirname(__file__))) |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_test_lib |
| from chromite.scripts import gather_builder_stats |
| from chromite.buildbot import cbuildbot_metadata |
| from chromite.buildbot import constants |
| |
| import mock |
| |
| |
| REASON_BAD_CL = gather_builder_stats.CLStats.REASON_BAD_CL |
| CQ = constants.CQ |
| |
| |
| class TestCLActionLogic(unittest.TestCase): |
| """Ensures that CL action analysis logic is correct.""" |
| |
| def _getTestBuildData(self): |
| """Generate a return test data. |
| |
| Returns: |
| A list of cbuildbot_metadata.BuildData objects to use as |
| test data for CL action summary logic. |
| """ |
| # Mock patches for test data. |
| c1p1 = cbuildbot_metadata.GerritPatchTuple(1, 1, False) |
| c2p1 = cbuildbot_metadata.GerritPatchTuple(2, 1, True) |
| c2p2 = cbuildbot_metadata.GerritPatchTuple(2, 2, True) |
| |
| # Mock builder status dictionaries |
| passed_status = {'status' : constants.FINAL_STATUS_PASSED} |
| failed_status = {'status' : constants.FINAL_STATUS_FAILED} |
| |
| t = itertools.count() |
| |
| TEST_METADATA = [ |
| # Build 1 picks up no patches. |
| cbuildbot_metadata.CBuildbotMetadata( |
| ).UpdateWithDict({'build-number' : 1, |
| 'bot-config' : constants.CQ_MASTER, |
| 'results' : [], |
| 'status' : passed_status}), |
| # Build 2 picks up c1p1 and does nothing. |
| cbuildbot_metadata.CBuildbotMetadata( |
| ).UpdateWithDict({'build-number' : 2, |
| 'bot-config' : constants.CQ_MASTER, |
| 'results' : [], |
| 'status' : failed_status} |
| ).RecordCLAction(c1p1, constants.CL_ACTION_PICKED_UP, t.next()), |
| # Build 3 picks up c1p1 and c2p1 and rejects both. |
| cbuildbot_metadata.CBuildbotMetadata( |
| ).UpdateWithDict({'build-number' : 3, |
| 'bot-config' : constants.CQ_MASTER, |
| 'results' : [], |
| 'status' : failed_status} |
| ).RecordCLAction(c1p1, constants.CL_ACTION_PICKED_UP, t.next() |
| ).RecordCLAction(c2p1, constants.CL_ACTION_PICKED_UP, t.next() |
| ).RecordCLAction(c1p1, constants.CL_ACTION_KICKED_OUT, t.next() |
| ).RecordCLAction(c2p1, constants.CL_ACTION_KICKED_OUT, t.next()), |
| # Build 4 picks up c1p1 and c2p2 and submits both. |
| # So c1p1 should be detected as a 1-time rejected good patch, |
| # and c2p1 should be detected as a possibly bad patch. |
| cbuildbot_metadata.CBuildbotMetadata( |
| ).UpdateWithDict({'build-number' : 4, |
| 'bot-config' : constants.CQ_MASTER, |
| 'results' : [], |
| 'status' : passed_status} |
| ).RecordCLAction(c1p1, constants.CL_ACTION_PICKED_UP, t.next() |
| ).RecordCLAction(c2p2, constants.CL_ACTION_PICKED_UP, t.next() |
| ).RecordCLAction(c1p1, constants.CL_ACTION_SUBMITTED, t.next() |
| ).RecordCLAction(c2p2, constants.CL_ACTION_SUBMITTED, t.next()) |
| ] |
| |
| # TEST_METADATA should not be guaranteed to be ordered by build number |
| # so shuffle it, but use the same seed each time so that unit test is |
| # deterministic. |
| random.seed(0) |
| random.shuffle(TEST_METADATA) |
| |
| # Wrap the test metadata into BuildData objects. |
| TEST_BUILDDATA = [cbuildbot_metadata.BuildData('', d.GetDict()) |
| for d in TEST_METADATA] |
| |
| return TEST_BUILDDATA |
| |
| |
| def testCLStatsSummary(self): |
| with cros_build_lib.ContextManagerStack() as stack: |
| test_builddata = self._getTestBuildData() |
| stack.Add(mock.patch.object, gather_builder_stats.StatsManager, |
| '_FetchBuildData', side_effect=[test_builddata, []]) |
| stack.Add(mock.patch.object, gather_builder_stats, '_PrepareCreds') |
| stack.Add(mock.patch.object, gather_builder_stats.CLStats, |
| 'GatherFailureReasons') |
| cl_stats = gather_builder_stats.CLStats('foo@bar.com') |
| cl_stats.Gather(datetime.date.today()) |
| cl_stats.reasons = {1: '', 2: '', 3: REASON_BAD_CL, 4: REASON_BAD_CL} |
| cl_stats.blames = {1: '', 2: '', 3: 'crosreview.com/1', |
| 4: 'crosreview.com/1'} |
| summary = cl_stats.Summarize() |
| |
| expected = { |
| 'mean_good_patch_rejections': 0.5, |
| 'unique_patches': 3, |
| 'unique_blames_change_count': 0, |
| 'total_cl_actions': 9, |
| 'good_patch_rejection_breakdown': [(0, 1), (1, 1)], |
| 'good_patch_rejection_count': 1, |
| 'good_patches_rejected': 1, |
| 'submitted_patches': 2, |
| 'submit_fails': 0, |
| 'unique_cls': 2, |
| 'median_handling_time': -1, # This will be ignored in comparison |
| 'patch_handling_time': -1, # This will be ignored in comparison |
| 'bad_cl_candidates': {CQ: [ |
| cbuildbot_metadata.GerritChangeTuple(gerrit_number=2, |
| internal=True)]}, |
| 'correctly_rejected_by_stage': {CQ: {}}, |
| 'incorrectly_rejected_by_stage': {}, |
| 'rejections': 2} |
| # Ignore handling times in comparison, since these are not fully |
| # reproducible from run to run of the unit test. |
| summary['median_handling_time'] = expected['median_handling_time'] |
| summary['patch_handling_time'] = expected['patch_handling_time'] |
| self.maxDiff = None |
| self.assertEqual(summary, expected) |
| |
| def testProcessBlameString(self): |
| """Tests that bug and CL links are correctly parsed.""" |
| blame = ('some words then crbug.com/1234, then other junk and ' |
| 'https://code.google.com/p/chromium/issues/detail?id=4321 ' |
| 'then some stuff and other stuff and b/2345 and also ' |
| 'https://b.corp.google.com/issue?id=5432&query=5432 ' |
| 'and then some crosreview.com/3456 or some ' |
| 'https://chromium-review.googlesource.com/#/c/6543/ and ' |
| 'then crosreview.com/i/9876 followed by ' |
| 'https://chrome-internal-review.googlesource.com/#/c/6789/') |
| expected = ['crbug.com/1234', |
| 'crbug.com/4321', |
| 'b/2345', |
| 'b/5432', |
| 'crosreview.com/3456', |
| 'crosreview.com/6543', |
| 'crosreview.com/i/9876', |
| 'crosreview.com/i/6789'] |
| self.assertEqual(gather_builder_stats.CLStats.ProcessBlameString(blame), |
| expected) |
| |
| |
| if __name__ == '__main__': |
| cros_test_lib.main() |