2 # Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Unit tests for gather_builder_stats."""
8 from __future__ import print_function
17 sys.path.insert(0, os.path.abspath('%s/../..' % os.path.dirname(__file__)))
18 from chromite.lib import cros_build_lib
19 from chromite.lib import cros_test_lib
20 from chromite.scripts import gather_builder_stats
21 from chromite.cbuildbot import metadata_lib
22 from chromite.cbuildbot import constants
27 REASON_BAD_CL = gather_builder_stats.CLStats.REASON_BAD_CL
29 PRE_CQ = constants.PRE_CQ
32 class TestCLActionLogic(unittest.TestCase):
33 """Ensures that CL action analysis logic is correct."""
35 def _getTestBuildData(self, cq):
36 """Generate a return test data.
39 cq: Whether this is a CQ run. If False, this is a Pre-CQ run.
42 A list of metadata_lib.BuildData objects to use as
43 test data for CL action summary logic.
45 # Mock patches for test data.
46 c1p1 = metadata_lib.GerritPatchTuple(1, 1, False)
47 c2p1 = metadata_lib.GerritPatchTuple(2, 1, True)
48 c2p2 = metadata_lib.GerritPatchTuple(2, 2, True)
49 c3p1 = metadata_lib.GerritPatchTuple(3, 1, True)
50 c3p2 = metadata_lib.GerritPatchTuple(3, 2, True)
51 c4p1 = metadata_lib.GerritPatchTuple(4, 1, True)
52 c4p2 = metadata_lib.GerritPatchTuple(4, 2, True)
54 # Mock builder status dictionaries
55 passed_status = {'status' : constants.FINAL_STATUS_PASSED}
56 failed_status = {'status' : constants.FINAL_STATUS_FAILED}
59 bot_config = (constants.CQ_MASTER if cq
60 else constants.PRE_CQ_GROUP_GS_LOCATION)
62 # pylint: disable=W0212
64 # Build 1 picks up no patches.
65 metadata_lib.CBuildbotMetadata(
66 ).UpdateWithDict({'build-number' : 1,
67 'bot-config' : bot_config,
69 'status' : passed_status}),
70 # Build 2 picks up c1p1 and does nothing.
71 metadata_lib.CBuildbotMetadata(
72 ).UpdateWithDict({'build-number' : 2,
73 'bot-config' : bot_config,
75 'status' : failed_status,
76 'changes': [c1p1._asdict()]}
77 ).RecordCLAction(c1p1, constants.CL_ACTION_PICKED_UP, t.next()),
78 # Build 3 picks up c1p1 and c2p1 and rejects both.
79 # c3p1 is not included in the run because it fails to apply.
80 metadata_lib.CBuildbotMetadata(
81 ).UpdateWithDict({'build-number' : 3,
82 'bot-config' : bot_config,
84 'status' : failed_status,
85 'changes': [c1p1._asdict(),
87 ).RecordCLAction(c1p1, constants.CL_ACTION_PICKED_UP, t.next()
88 ).RecordCLAction(c2p1, constants.CL_ACTION_PICKED_UP, t.next()
89 ).RecordCLAction(c1p1, constants.CL_ACTION_KICKED_OUT, t.next()
90 ).RecordCLAction(c2p1, constants.CL_ACTION_KICKED_OUT, t.next()
91 ).RecordCLAction(c3p1, constants.CL_ACTION_KICKED_OUT, t.next()),
92 # Build 4 picks up c4p1 and rejects it.
93 metadata_lib.CBuildbotMetadata(
94 ).UpdateWithDict({'build-number' : 3,
95 'bot-config' : bot_config,
97 'status' : failed_status,
98 'changes': [c4p1._asdict()]}
99 ).RecordCLAction(c4p2, constants.CL_ACTION_PICKED_UP, t.next()
100 ).RecordCLAction(c4p2, constants.CL_ACTION_KICKED_OUT, t.next()),
104 # Build 4 picks up c1p1 and c2p2 and submits both.
105 # So c1p1 should be detected as a 1-time rejected good patch,
106 # and c2p1 should be detected as a possibly bad patch.
107 metadata_lib.CBuildbotMetadata(
108 ).UpdateWithDict({'build-number' : 4,
109 'bot-config' : bot_config,
111 'status' : passed_status,
112 'changes': [c1p1._asdict(),
114 ).RecordCLAction(c1p1, constants.CL_ACTION_PICKED_UP, t.next()
115 ).RecordCLAction(c2p2, constants.CL_ACTION_PICKED_UP, t.next()
116 ).RecordCLAction(c3p2, constants.CL_ACTION_PICKED_UP, t.next()
117 ).RecordCLAction(c4p1, constants.CL_ACTION_PICKED_UP, t.next()
118 ).RecordCLAction(c1p1, constants.CL_ACTION_SUBMITTED, t.next()
119 ).RecordCLAction(c2p2, constants.CL_ACTION_SUBMITTED, t.next()
120 ).RecordCLAction(c3p2, constants.CL_ACTION_SUBMITTED, t.next()
121 ).RecordCLAction(c4p2, constants.CL_ACTION_SUBMITTED, t.next()),
125 metadata_lib.CBuildbotMetadata(
126 ).UpdateWithDict({'build-number' : 5,
127 'bot-config' : bot_config,
129 'status' : failed_status,
130 'changes': [c4p1._asdict()]}
131 ).RecordCLAction(c4p1, constants.CL_ACTION_PICKED_UP, t.next()
132 ).RecordCLAction(c4p1, constants.CL_ACTION_KICKED_OUT, t.next()),
133 metadata_lib.CBuildbotMetadata(
134 ).UpdateWithDict({'build-number' : 6,
135 'bot-config' : bot_config,
137 'status' : failed_status,
138 'changes': [c4p1._asdict()]}
139 ).RecordCLAction(c1p1, constants.CL_ACTION_PICKED_UP, t.next()
140 ).RecordCLAction(c1p1, constants.CL_ACTION_KICKED_OUT, t.next())
142 # pylint: enable=W0212
144 # TEST_METADATA should not be guaranteed to be ordered by build number
145 # so shuffle it, but use the same seed each time so that unit test is
148 random.shuffle(TEST_METADATA)
150 # Wrap the test metadata into BuildData objects.
151 TEST_BUILDDATA = [metadata_lib.BuildData('', d.GetDict())
152 for d in TEST_METADATA]
154 return TEST_BUILDDATA
157 def testCLStatsSummary(self):
158 with cros_build_lib.ContextManagerStack() as stack:
159 pre_cq_builddata = self._getTestBuildData(cq=False)
160 cq_builddata = self._getTestBuildData(cq=True)
161 stack.Add(mock.patch.object, gather_builder_stats.StatsManager,
162 '_FetchBuildData', side_effect=[cq_builddata, pre_cq_builddata])
163 stack.Add(mock.patch.object, gather_builder_stats, '_PrepareCreds')
164 stack.Add(mock.patch.object, gather_builder_stats.CLStats,
165 'GatherFailureReasons')
166 cl_stats = gather_builder_stats.CLStats('foo@bar.com')
167 cl_stats.Gather(datetime.date.today())
168 cl_stats.reasons = {1: '', 2: '', 3: REASON_BAD_CL, 4: REASON_BAD_CL}
169 cl_stats.blames = {1: '', 2: '', 3: 'crosreview.com/1',
170 4: 'crosreview.com/1'}
171 summary = cl_stats.Summarize()
174 'mean_good_patch_rejections': 0.5,
176 'unique_blames_change_count': 0,
177 'total_cl_actions': 28,
178 'good_patch_rejection_breakdown': [(0, 3), (1, 0), (2, 1)],
179 'good_patch_rejection_count': {CQ: 1, PRE_CQ: 1},
180 'good_patch_rejections': 2,
181 'false_rejection_rate': {CQ: 20., PRE_CQ: 20., 'combined': 100./3,},
182 'submitted_patches': 4,
185 'median_handling_time': -1, # This will be ignored in comparison
186 'patch_handling_time': -1, # This will be ignored in comparison
187 'bad_cl_candidates': {
188 CQ: [metadata_lib.GerritChangeTuple(gerrit_number=2,
190 PRE_CQ: [metadata_lib.GerritChangeTuple(gerrit_number=2,
192 metadata_lib.GerritChangeTuple(gerrit_number=4,
195 'correctly_rejected_by_stage': {CQ: {}, PRE_CQ: {}},
196 'incorrectly_rejected_by_stage': {PRE_CQ: {}},
198 # Ignore handling times in comparison, since these are not fully
199 # reproducible from run to run of the unit test.
200 summary['median_handling_time'] = expected['median_handling_time']
201 summary['patch_handling_time'] = expected['patch_handling_time']
203 self.assertEqual(summary, expected)
205 def testProcessBlameString(self):
206 """Tests that bug and CL links are correctly parsed."""
207 blame = ('some words then crbug.com/1234, then other junk and '
208 'https://code.google.com/p/chromium/issues/detail?id=4321 '
209 'then some stuff and other stuff and b/2345 and also '
210 'https://b.corp.google.com/issue?id=5432&query=5432 '
211 'and then some crosreview.com/3456 or some '
212 'https://chromium-review.googlesource.com/#/c/6543/ and '
213 'then crosreview.com/i/9876 followed by '
214 'https://chrome-internal-review.googlesource.com/#/c/6789/ '
215 'blah https://gutsv3.corp.google.com/#ticket/1234 t/4321')
216 expected = ['crbug.com/1234',
220 'crosreview.com/3456',
221 'crosreview.com/6543',
222 'crosreview.com/i/9876',
223 'crosreview.com/i/6789',
226 self.assertEqual(gather_builder_stats.CLStats.ProcessBlameString(blame),
230 if __name__ == '__main__':