Пример #1
0
def _CheckRevertStatusOfSuspectedCL(suspected_cl):
    """Updates suspected_cl with findings about what happened to its revert CL.

  Args:
    suspected_cl (wf_suspected_cl): A WfSuspectedCL entity.

  Returns:
    processed (bool): True if the suspected cl's revert outcome was determined,
      False if reverting was not relevant. None if the cl needed reverting but
      an outcome could not be determined.
    url (str): The code review url for manual investigation.
    status (str): The eventual outcome of the revert cl.
  """
    revert_cl = suspected_cl.revert_cl

    if not revert_cl and not suspected_cl.should_be_reverted:
        # Findit did not deem this suspected cl as needing revert. No action needed.
        return False, None, None

    revision = suspected_cl.revision
    culprit_info = git.GetCodeReviewInfoForACommit(revision)
    review_server_host = culprit_info.get('review_server_host')
    change_id = culprit_info.get('review_change_id')

    if not review_server_host or not change_id:  # pragma: no cover
        # TODO(lijeffrey): Handle cases a patch was committed without review.
        return None, None, None

    code_review_settings = FinditConfig().Get().code_review_settings
    codereview = Gerrit(
        review_server_host
    ) if review_server_host and codereview_util.IsCodeReviewGerrit(
        review_server_host, code_review_settings) else None
    if not codereview:
        logging.error('Could not get codereview for %s/q/%s',
                      review_server_host, change_id)
        return None, None, None

    cl_info = codereview.GetClDetails(change_id)
    code_review_url = codereview.GetCodeReviewUrl(change_id)

    if not cl_info:
        logging.error('Could not get CL details for %s/q/%s',
                      review_server_host, change_id)
        return None, code_review_url, None
    reverts_to_check = cl_info.GetRevertCLsByRevision(revision)

    if not reverts_to_check:
        logging.error('Could not get revert info for %s/q/%s',
                      review_server_host, change_id)
        return None, code_review_url, None

    reverts_to_check.sort(key=lambda x: x.timestamp)

    if revert_cl:  # Findit attempted to create a revert cl.
        any_revert_committed = False
        # Check whose revert CL was first commited.
        for revert in reverts_to_check:  # pragma: no branch
            reverting_user = revert.reverting_user_email
            revert_commits = revert.reverting_cl.commits

            if revert_commits:  # pragma: no branch
                any_revert_committed = True
                revert_commit = revert_commits[0]

                if reverting_user == constants.DEFAULT_SERVICE_ACCOUNT:
                    # The sheriff used Findit's reverting CL.
                    cq_attempt = revert.reverting_cl.commit_attempts[
                        revert_commit.patchset_id]
                    _UpdateSuspectedCL(
                        suspected_cl,
                        sheriff_action_time=cq_attempt.last_cq_timestamp,
                        revert_commit_timestamp=revert_commit.timestamp,
                        revert_status=revert_cl_status.COMMITTED)
                    break
                else:
                    # Sheriff used own revert CL.
                    _UpdateSuspectedCL(
                        suspected_cl,
                        sheriff_action_time=revert_commit.timestamp,
                        revert_status=revert_cl_status.DUPLICATE)
                    break

        # No revert was ever committed. False positive.
        if not any_revert_committed:
            # TODO(crbug.com/702056): Close the revert CLs that were not used.
            _UpdateSuspectedCL(suspected_cl,
                               revert_status=revert_cl_status.FALSE_POSITIVE)

    elif suspected_cl.should_be_reverted:  # pragma: no branch
        # Findit could have created a revert CL, but the sheriff was too fast.
        # Find the first revert that was successfully landed.
        for revert in reverts_to_check:  # pragma: no branch
            revert_commits = revert.reverting_cl.commits
            if revert_commits:  # pragma: no branch
                _UpdateSuspectedCL(suspected_cl,
                                   sheriff_action_time=revert.timestamp)
                break

    return True, code_review_url, (revert_cl.status if revert_cl else
                                   revert_cl_status.DUPLICATE)
Пример #2
0
class GerritTest(testing.AppengineTestCase):

  def setUp(self):
    super(GerritTest, self).setUp()
    self.http_client = DummyHttpClient()
    self.server_hostname = 'server.host.name'
    self.gerrit = Gerrit(self.server_hostname)
    self.gerrit.HTTP_CLIENT = self.http_client
    self.maxDiff = None

  def testGetCodeReviewUrl(self):
    change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
    self.assertEqual(
      'https://server.host.name/q/%s' % change_id,
      self.gerrit.GetCodeReviewUrl(change_id))

  def testPostMessage(self):
    change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
    response_str = '{}'
    self.http_client._SetPostMessageResponse(
      self.server_hostname, change_id, response_str)
    # This message should not change when being urlencoded or jsonized
    message = 'FinditWasHere'
    self.assertTrue(self.gerrit.PostMessage(change_id, message))
    _url, data, _headers = self.http_client.requests[0]
    self.assertIn(message, data)

  def testAddReviewerNew(self):
    change_id = 'Iabc12345'
    self.http_client._SetReviewersResponse(self.server_hostname, change_id,
                                           ['*****@*****.**'])
    self.http_client._AddReviewerResponse(self.server_hostname, change_id,
                                          '*****@*****.**')
    self.assertTrue(self.gerrit.AddReviewers(change_id, ['*****@*****.**']))

  def testAddReviewerNewAliased(self):
    change_id = 'Iabc12345'
    self.http_client._SetReviewersResponse(self.server_hostname, change_id,
                                           ['*****@*****.**'])
    self.http_client._AddReviewerResponse(self.server_hostname, change_id,
                                          '*****@*****.**')
    self.assertTrue(self.gerrit.AddReviewers(change_id, ['*****@*****.**']))

  def testAddReviewerExisting(self):
    change_id = 'Iabc12345'
    self.http_client._SetReviewersResponse(self.server_hostname, change_id,
                                           ['*****@*****.**'])
    self.assertTrue(self.gerrit.AddReviewers(change_id, ['*****@*****.**']))

  def testAddReviewerExistingAliased(self):
    change_id = 'Iabc12345'
    self.http_client._SetReviewersResponse(self.server_hostname, change_id,
                                           ['*****@*****.**'])
    self.http_client._AddReviewerResponse(self.server_hostname, change_id,
                                          None)
    self.assertTrue(self.gerrit.AddReviewers(change_id, ['*****@*****.**']))

  def testAddReviewerMany(self):
    change_id = 'Iabc12345'
    self.http_client._SetReviewersResponse(self.server_hostname, change_id,
                                           ['*****@*****.**'])
    self.http_client._AddReviewerResponse(self.server_hostname, change_id,
                                          '*****@*****.**')
    self.http_client._AddReviewerResponse(self.server_hostname, change_id,
                                          '*****@*****.**')
    self.assertTrue(self.gerrit.AddReviewers(change_id, ['*****@*****.**',
                                                         '*****@*****.**',
                                                         '*****@*****.**']))

  def testAddReviewerFailure(self):
    change_id = 'Iabc12345'
    self.http_client._SetReviewersResponse(self.server_hostname, change_id,
                                           ['*****@*****.**'])
    self.assertFalse(self.gerrit.AddReviewers(change_id, ['*****@*****.**']))

  def testGetClInfoCQCommit(self):
    change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
    with mock.patch.object(
        self.gerrit,
        '_Get',
        return_value={
            'change_id': 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7',
            'status': 'MERGED',
            'owner': {
                'email': '*****@*****.**'
            },
            'submitted': '2017-03-24 01:07:39.000000000',
            'reviewers': {
                'REVIEWER': [
                    {'email': '*****@*****.**'},
                    {'email': '*****@*****.**'},
                    {'email': '*****@*****.**'},
                    {'email': '*****@*****.**'}
                ],
                'CC': [{'email': '*****@*****.**'}]
           },
            'messages': [{
                'author': {'email': '*****@*****.**'},
                'date': '2017-03-24 00:58:25.000000000',
                'message': 'Patch Set 3: Commit-Queue+2',
                '_revision_number': 3
            }],
            'current_revision': '4bd07b5148508d3d100d9d4eafb9f4d233d7fa15',
            'revisions': {
                '4bd07b5148508d3d100d9d4eafb9f4d233d7fa15': {
                  '_number': 3,
                  'commit': {
                      'committer': {
                          'email': '*****@*****.**'
                      },
                      'message': 'some message'
                  }
               }
           }
        }):
      cl_info = self.gerrit.GetClDetails(change_id)
    self.assertNotEqual(
      cl_info.commit_attempts.values()[0].last_cq_timestamp,
      cl_info.commits[0].timestamp)

  def testGetClInfoManualCommit(self):
    change_id = 'I7ecd56d7d0c3fef90cfe998a29b948c5032980e4'
    with mock.patch.object(
        self.gerrit,
        '_Get',
        return_value={
            'change_id': 'I7ecd56d7d0c3fef90cfe998a29b948c5032980e4',
            'status': 'MERGED',
            'owner': {
                'email': '*****@*****.**'
            },
            'submitted': '2017-03-08 04:43:30.000000000',
            'reviewers': {
                'REVIEWER': [
                    {'email': '*****@*****.**'},
                    {'email': '*****@*****.**'},
                    {'email': '*****@*****.**' },
                ],
                'CC': []
            },
            'messages': [],
            'current_revision': '9ff179a3962bf3465815d3a85ad46e1c3b4a9e27',
            'revisions': {
                '9ff179a3962bf3465815d3a85ad46e1c3b4a9e27': {
                    '_number': 2,
                    'commit': {
                        'committer': {
                          'email': '*****@*****.**',
                        },
                        'message': 'some message'
                    }
                }
            }
        }):
      cl_info = self.gerrit.GetClDetails(change_id)
    self.assertEqual(
      cl_info.commit_attempts.values()[0].last_cq_timestamp,
      cl_info.commits[0].timestamp)

  def testGetClInfoRevertedCommit(self):
    change_id = 'I4303e1b7166aaab873587a3fda0ec907d3d8ace0'
    with mock.patch.object(
        self.gerrit,
        '_Get',
        side_effect=[{
            'change_id': 'I4303e1b7166aaab873587a3fda0ec907d3d8ace0',
            'status': 'MERGED',
            'owner': {
                'email': '*****@*****.**'
            },
            'submitted': '2017-02-27 18:56:54.000000000',
            '_number': 446905,
            'reviewers': {
                'REVIEWER': [
                    {'email': '*****@*****.**'},
                    {'email': '*****@*****.**'},
                    {'email': '*****@*****.**'}
                ],
                'CC': []
            },
            'messages': [
                {
                    'id': 'b7d6785c324297ec4f1e6b2de34cf83f4c58e87c',
                    'author': {'email': '*****@*****.**'},
                    'date': '2017-02-27 18:47:15.000000000',
                    'message': 'Patch Set 1: Commit-Queue+2',
                    '_revision_number': 1
                },
                {
                    'id': 'b9e04aec4bffed0284c1f53cc5a9c88818807368',
                    'tag': 'autogenerated:gerrit:revert',
                    'author': {'email': '*****@*****.**'},
                    'date': '2017-02-27 19:04:51.000000000',
                    'message': 'Created a revert of this change as '
                               'If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
                    '_revision_number': 2
                }
            ],
            'current_revision': 'edda1046ce724695004242e943f59f5e1b2d00ff',
            'revisions': {
                'edda1046ce724695004242e943f59f5e1b2d00ff': {
                    '_number': 2,
                    'commit': {
                        'committer': {
                            'email': '*****@*****.**',
                        },
                      'message': 'cl title\n\nsome description\n\n'
                                 'NOAUTOREVERT= True\n\nChange-Id: '
                                 'someid\nReviewed-on: cl_url\nCommit-Queue: '
                                 'owner\nReviewed-by: reviewers\n\n'
                    },
                }
            }
        }, {
            'change_id': 'If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
            'status': 'MERGED',
            'submitted': '2017-02-27 19:05:03.000000000',
            'owner': {
                'email': '*****@*****.**'
            },
            '_number': 446788,
            'reviewers': {
                'REVIEWER': [
                    {
                        'email': '*****@*****.**',

                    },
                    {
                        'email': '*****@*****.**',

                    },
                    {
                        'email': '*****@*****.**',

                    }
                ]
            },
            'messages': [
                {
                    'id': '30496ce351a43c0b74d812e9e40b440f5acff9d5',
                    'author': {
                        'email': '*****@*****.**',

                    },
                    'date': '2017-02-27 19:04:53.000000000',
                    'message': 'Patch Set 1: Code-Review+1 Commit-Queue+2',
                    '_revision_number': 1
                },
            ],
            'current_revision': 'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824',
            'revisions': {
                'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824': {
                    '_number': 2,
                    'commit': {
                        'committer': {
                            'email': '*****@*****.**',
                            'date': '2017-02-27 19:05:03.000000000',
                            'tz': 0
                        },
                        'message': 'cl title\n\nsome description\n\n'
                                   'NOAUTOREVERT=TRUE\n\nChange-Id: '
                                   'someid\nReviewed-on: cl_url\nCommit-Queue: '
                                   'owner\nReviewed-by: reviewers\n\n'
                    },
                }
            }
        }]):
      cl_info = self.gerrit.GetClDetails(change_id)
    self.assertEqual(cl_info.serialize(), {
        'server_hostname': 'server.host.name',
        'auto_revert_off': True,
        'owner_email': '*****@*****.**',
        'reviewers': ['*****@*****.**',
                      '*****@*****.**',
                      '*****@*****.**'],
        'closed': True,
        'commits': [{'patchset_id': 2,
                      'timestamp': '2017-02-27 18:56:54 UTC',
                       'revision': 'edda1046ce724695004242e943f59f5e1b2d00ff'}],
        'cc': [],
        'change_id': 'I4303e1b7166aaab873587a3fda0ec907d3d8ace0',
        'reverts': [{'patchset_id': 2,
                     'reverting_user_email': '*****@*****.**',
                     'timestamp': '2017-02-27 19:04:51 UTC',
                     'reverting_cl': {
                         'server_hostname': 'server.host.name',
                         'auto_revert_off': True,
                         'owner_email': '*****@*****.**',
                         'reviewers': ['*****@*****.**',
                                       '*****@*****.**',
                                       '*****@*****.**'],
                         'closed': True,
                         'commits': [{
                             'patchset_id': 2,
                             'timestamp': '2017-02-27 19:05:03 UTC',
                             'revision':
                                   'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824'}],
                         'cc': [],
                         'change_id':
                               'If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
                               'reverts': [],
                               'commit_attempts': [{
                                   'patchset_id': 1,
                                   'timestamp': '2017-02-27 19:04:53 UTC',
                                   'committing_user_email':
                                       '*****@*****.**'}]}}],
        'commit_attempts': [{'patchset_id': 1,
                             'timestamp': '2017-02-27 18:47:15 UTC',
                             'committing_user_email': '*****@*****.**'}]})

  def testCreateRevertSuccessful(self):
    change_id = 'I123456'
    reverting_change_id = 'I987654'
    response = self.http_client._MakeResponse(
      {'change_id': reverting_change_id})
    url = 'https://%s/a/changes/%s/revert' % (self.server_hostname, change_id)
    self.http_client.SetResponse(url, (200, response))
    self.assertEqual(reverting_change_id, self.gerrit.CreateRevert(
      'Reason', change_id))

  def testCreateRevertFailure(self):
    change_id = 'I123456'
    self.assertFalse(self.gerrit.CreateRevert('Reason', change_id))

  def testRequestAddsAuthenticationPrefix(self):
    self.gerrit._AuthenticatedRequest(['changes', '123'])
    url, _payload, _headers = self.http_client.requests[0]
    self.assertEqual('https://server.host.name/a/changes/123', url)

  def testRequestKeepsAuthenticationPrefix(self):
    self.gerrit._AuthenticatedRequest(['a', 'changes', '123'])
    url, _payload, _headers = self.http_client.requests[0]
    self.assertEqual('https://server.host.name/a/changes/123', url)
Пример #3
0
class GerritTest(testing.AppengineTestCase):
    def setUp(self):
        super(GerritTest, self).setUp()
        self.http_client = DummyHttpClient()
        self.server_hostname = 'server.host.name'
        self.gerrit = Gerrit(self.server_hostname)
        self.gerrit.HTTP_CLIENT = self.http_client
        self.maxDiff = None

    def testGetCodeReviewUrl(self):
        change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
        self.assertEqual('https://server.host.name/q/%s' % change_id,
                         self.gerrit.GetCodeReviewUrl(change_id))

    def testPostMessage(self):
        change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
        response_str = '{}'
        self.http_client._SetPostMessageResponse(self.server_hostname,
                                                 change_id, response_str)
        # This message should not change when being urlencoded or jsonized
        message = 'FinditWasHere'
        self.assertTrue(self.gerrit.PostMessage(change_id, message))
        _url, data, _headers = self.http_client.requests[0]
        self.assertIn(message, data)

    def testPostMessageNoEmail(self):
        change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
        response_str = '{}'
        self.http_client._SetPostMessageResponse(self.server_hostname,
                                                 change_id, response_str)
        # This message should not change when being urlencoded or jsonized
        message = 'FinditWasHere'
        self.assertTrue(self.gerrit.PostMessage(change_id, message, False))
        _url, data, _headers = self.http_client.requests[0]
        self.assertIn(message, data)

    def testAddReviewerNew(self):
        change_id = 'Iabc12345'
        response = {
            'reviewers': {
                '*****@*****.**': {
                    'input': '*****@*****.**',
                    'reviewers': [{
                        'email': '*****@*****.**'
                    }]
                }
            }
        }
        self.http_client._SetPostMessageResponse(self.server_hostname,
                                                 change_id,
                                                 json.dumps(response))
        self.assertTrue(
            self.gerrit.AddReviewers(change_id, ['*****@*****.**'], 'message'))

    def testAddReviewerWrongformat(self):
        change_id = 'Iabc12345'
        self.assertTrue(
            self.gerrit.AddReviewers(change_id, ['[email protected]@d.com'],
                                     'message'))

    def testAddReviewerExisting(self):
        change_id = 'Iabc12345'
        response = {
            'reviewers': {
                '*****@*****.**': {
                    'input': '*****@*****.**',
                    'reviewers': []
                }
            }
        }
        self.http_client._SetPostMessageResponse(self.server_hostname,
                                                 change_id,
                                                 json.dumps(response))
        self.assertTrue(
            self.gerrit.AddReviewers(change_id, ['*****@*****.**'], 'message'))

    @mock.patch.object(logging, 'error')
    def testAddReviewerMany(self, mock_logging):
        # pylint:disable=unused-argument
        change_id = 'Iabc12345'
        response = {
            'reviewers': {
                '*****@*****.**': {
                    'input': '*****@*****.**',
                    'reviewers': [{
                        'email': '*****@*****.**'
                    }]
                },
                '*****@*****.**': {
                    'input': '*****@*****.**',
                    'reviewers': [{
                        'email': '*****@*****.**'
                    }]
                },
                '*****@*****.**': {
                    'input': '*****@*****.**',
                    'reviewers': [{
                        'email': '*****@*****.**'
                    }]
                },
            }
        }
        self.http_client._SetPostMessageResponse(self.server_hostname,
                                                 change_id,
                                                 json.dumps(response))
        self.assertTrue(
            self.gerrit.AddReviewers(change_id, [
                '*****@*****.**', '*****@*****.**', '*****@*****.**',
                '[email protected]@d.com'
            ], 'message'))
        # Assertions have never worked properly because we were using mock 1.0.1.
        # After rolling to mock 2.0.0, which fixes assertions, these assertions now
        # fail. https://crbug.com/947753.
        # pylint:disable=unused-argument
        # mock_logging.assert_has_called_with(
        #    'Reviewer\'s email is in wrong format: '
        #    '[email protected]@d.com')

    def testAddReviewerFailure(self):
        change_id = 'Iabc12345'
        self.assertFalse(self.gerrit.AddReviewers(change_id,
                                                  ['*****@*****.**']))

    def testGetClInfoCQCommit(self):
        change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
        with mock.patch.object(
                self.gerrit,
                '_Get',
                return_value={
                    'change_id':
                    'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7',
                    'status':
                    'MERGED',
                    'owner': {
                        'email': '*****@*****.**'
                    },
                    'submitted':
                    '2017-03-24 01:07:39.000000000',
                    'reviewers': {
                        'REVIEWER': [{
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }],
                        'CC': [{
                            'email': '*****@*****.**'
                        }]
                    },
                    'messages': [{
                        'author': {
                            'email': '*****@*****.**'
                        },
                        'date': '2017-03-24 00:58:25.000000000',
                        'message': 'Patch Set 3: Commit-Queue+2',
                        '_revision_number': 3
                    }],
                    'current_revision':
                    '4bd07b5148508d3d100d9d4eafb9f4d233d7fa15',
                    'revisions': {
                        '4bd07b5148508d3d100d9d4eafb9f4d233d7fa15': {
                            '_number': 3,
                            'commit': {
                                'committer': {
                                    'email': '*****@*****.**'
                                },
                                'message':
                                'some message',
                                'parents': [{
                                    'commit':
                                    ('ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f'
                                     ),
                                }]
                            }
                        }
                    },
                    'subject':
                    'subject'
                }):
            cl_info = self.gerrit.GetClDetails(change_id)
        self.assertNotEqual(
            cl_info.commit_attempts.values()[0].last_cq_timestamp,
            cl_info.commits[0].timestamp)

    def testGetClInfoManualCommit(self):
        change_id = 'I7ecd56d7d0c3fef90cfe998a29b948c5032980e4'
        with mock.patch.object(
                self.gerrit,
                '_Get',
                return_value={
                    'change_id': 'I7ecd56d7d0c3fef90cfe998a29b948c5032980e4',
                    'status': 'MERGED',
                    'owner': {
                        'email': '*****@*****.**'
                    },
                    'submitted': '2017-03-08 04:43:30.000000000',
                    'reviewers': {
                        'REVIEWER': [
                            {
                                'email': '*****@*****.**'
                            },
                            {
                                'email': '*****@*****.**'
                            },
                            {
                                'email': '*****@*****.**'
                            },
                        ],
                        'CC': []
                    },
                    'messages': [],
                    'current_revision':
                    '9ff179a3962bf3465815d3a85ad46e1c3b4a9e27',
                    'revisions': {
                        '9ff179a3962bf3465815d3a85ad46e1c3b4a9e27': {
                            '_number': 2,
                            'commit': {
                                'committer': {
                                    'email': '*****@*****.**',
                                },
                                'message':
                                'some message',
                                'parents': [{
                                    'commit':
                                    ('ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f'
                                     ),
                                }]
                            }
                        }
                    },
                    'subject': 'subject'
                }):
            cl_info = self.gerrit.GetClDetails(change_id)
        self.assertEqual(cl_info.commit_attempts.values()[0].last_cq_timestamp,
                         cl_info.commits[0].timestamp)

    def testGetClInfoRevertedCommit(self):
        change_id = 'I4303e1b7166aaab873587a3fda0ec907d3d8ace0'
        with mock.patch.object(
                self.gerrit,
                '_Get',
                side_effect=[{
                    'change_id':
                    'I4303e1b7166aaab873587a3fda0ec907d3d8ace0',
                    'status':
                    'MERGED',
                    'owner': {
                        'email': '*****@*****.**'
                    },
                    'submitted':
                    '2017-02-27 18:56:54.000000000',
                    '_number':
                    446905,
                    'reviewers': {
                        'REVIEWER': [{
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }],
                        'CC': []
                    },
                    'messages': [{
                        'id': 'b7d6785c324297ec4f1e6b2de34cf83f4c58e87c',
                        'author': {
                            'email': '*****@*****.**'
                        },
                        'date': '2017-02-27 18:47:15.000000000',
                        'message': 'Patch Set 1: Commit-Queue+2',
                        '_revision_number': 1
                    }, {
                        'id':
                        'b9e04aec4bffed0284c1f53cc5a9c88818807368',
                        'tag':
                        'autogenerated:gerrit:revert',
                        'author': {
                            'email': '*****@*****.**'
                        },
                        'date':
                        '2017-02-27 19:04:51.000000000',
                        'message':
                        'Created a revert of this change as '
                        'If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
                        '_revision_number':
                        2
                    }],
                    'current_revision':
                    'edda1046ce724695004242e943f59f5e1b2d00ff',
                    'revisions': {
                        'edda1046ce724695004242e943f59f5e1b2d00ff': {
                            '_number': 2,
                            'commit': {
                                'committer': {
                                    'email': '*****@*****.**',
                                },
                                'message':
                                'cl title\n\nsome description\n\n'
                                'NOAUTOREVERT= True\n\nChange-Id: '
                                'someid\nReviewed-on: cl_url\nCommit-Queue: '
                                'owner\nReviewed-by: reviewers\n\n'
                                'BUGS : 12345, 67890',
                                'parents': [{
                                    'commit':
                                    ('ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f'
                                     ),
                                }]
                            },
                        }
                    },
                    'subject':
                    'subject'
                }, {
                    'change_id':
                    'If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
                    'status':
                    'MERGED',
                    'submitted':
                    '2017-02-27 19:05:03.000000000',
                    'owner': {
                        'email': '*****@*****.**'
                    },
                    '_number':
                    446788,
                    'reviewers': {
                        'REVIEWER': [{
                            'email': '*****@*****.**',
                        }, {
                            'email': '*****@*****.**',
                        }, {
                            'email': '*****@*****.**',
                        }]
                    },
                    'messages': [
                        {
                            'id': '30496ce351a43c0b74d812e9e40b440f5acff9d5',
                            'author': {
                                'email': '*****@*****.**',
                            },
                            'date': '2017-02-27 19:04:53.000000000',
                            'message':
                            'Patch Set 1: Code-Review+1 Commit-Queue+2',
                            '_revision_number': 1
                        },
                    ],
                    'current_revision':
                    'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824',
                    'revisions': {
                        'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824': {
                            '_number': 2,
                            'commit': {
                                'committer': {
                                    'email': '*****@*****.**',
                                    'date': '2017-02-27 19:05:03.000000000',
                                    'tz': 0
                                },
                                'message':
                                'cl title\n\nsome description\n\n'
                                'NOAUTOREVERT=TRUE\n\nChange-Id: '
                                'someid\nReviewed-on: cl_url\nCommit-Queue: '
                                'owner\nReviewed-by: reviewers\n\n'
                                'BUG: 123455',
                                'parents': [{
                                    'commit':
                                    ('b63383e4e3f2b774ce0e6d3bfafb4adc84e48f2b'
                                     ),
                                }]
                            },
                        }
                    },
                    'subject':
                    'subject',
                    'revert_of':
                    446905
                }]):
            cl_info = self.gerrit.GetClDetails(change_id)

            expected_cl_info = {
                'server_hostname':
                'server.host.name',
                'auto_revert_off':
                True,
                'owner_email':
                '*****@*****.**',
                'reviewers': [
                    '*****@*****.**', '*****@*****.**',
                    '*****@*****.**'
                ],
                'closed':
                True,
                'commits': [{
                    'patchset_id':
                    2,
                    'timestamp':
                    '2017-02-27 18:56:54 UTC',
                    'revision':
                    'edda1046ce724695004242e943f59f5e1b2d00ff',
                    'parent_revisions':
                    ['ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f']
                }],
                'cc': [],
                'subject':
                'subject',
                'description':
                'cl title\n\nsome description\n\n'
                'NOAUTOREVERT= True\n\nChange-Id: '
                'someid\nReviewed-on: cl_url\nCommit-Queue: '
                'owner\nReviewed-by: reviewers\n\n'
                'BUGS : 12345, 67890',
                'change_id':
                'I4303e1b7166aaab873587a3fda0ec907d3d8ace0',
                'reverts': [{
                    'patchset_id': 2,
                    'reverting_user_email': '*****@*****.**',
                    'timestamp': '2017-02-27 19:04:51 UTC',
                    'reverting_cl': {
                        'server_hostname':
                        'server.host.name',
                        'auto_revert_off':
                        True,
                        'owner_email':
                        '*****@*****.**',
                        'reviewers': [
                            '*****@*****.**', '*****@*****.**',
                            '*****@*****.**'
                        ],
                        'closed':
                        True,
                        'commits': [{
                            'patchset_id':
                            2,
                            'timestamp':
                            '2017-02-27 19:05:03 UTC',
                            'revision':
                            'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824',
                            'parent_revisions':
                            ['b63383e4e3f2b774ce0e6d3bfafb4adc84e48f2b']
                        }],
                        'cc': [],
                        'subject':
                        'subject',
                        'description':
                        'cl title\n\nsome description\n\n'
                        'NOAUTOREVERT=TRUE\n\nChange-Id: '
                        'someid\nReviewed-on: cl_url\n'
                        'Commit-Queue: '
                        'owner\nReviewed-by: reviewers\n\n'
                        'BUG: 123455',
                        'change_id':
                        'If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
                        'reverts': [],
                        'commit_attempts': [{
                            'patchset_id':
                            1,
                            'timestamp':
                            '2017-02-27 19:04:53 UTC',
                            'committing_user_email':
                            '*****@*****.**'
                        }],
                        'revert_of':
                        446905,
                        'patchsets': {
                            'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824': {
                                'patchset_id':
                                2,
                                'revision':
                                'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824',
                                'parent_revisions':
                                ['b63383e4e3f2b774ce0e6d3bfafb4adc84e48f2b']
                            }
                        }
                    }
                }],
                'commit_attempts': [{
                    'patchset_id': 1,
                    'timestamp': '2017-02-27 18:47:15 UTC',
                    'committing_user_email': '*****@*****.**'
                }],
                'revert_of':
                None,
                'patchsets': {
                    'edda1046ce724695004242e943f59f5e1b2d00ff': {
                        'patchset_id':
                        2,
                        'revision':
                        'edda1046ce724695004242e943f59f5e1b2d00ff',
                        'parent_revisions':
                        ['ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f']
                    }
                }
            }
        self.assertEqual(cl_info.serialize(), expected_cl_info)

    def testCreateRevertSuccessful(self):
        change_id = 'I123456'

        reverting_change_id = 'I987654'
        response = self.http_client._MakeResponse(
            {'change_id': reverting_change_id})
        url = 'https://%s/a/changes/%s/revert' % (self.server_hostname,
                                                  change_id)
        self.http_client.SetResponse(url, (200, response))

        with mock.patch.object(self.gerrit,
                               '_GenerateRevertCLDescription',
                               return_value='Reason'):
            self.assertEqual(reverting_change_id,
                             self.gerrit.CreateRevert('Reason', change_id))

    def testCreateRevertFailure(self):
        change_id = 'I123456'
        with mock.patch.object(self.gerrit,
                               '_GenerateRevertCLDescription',
                               return_value='Reason'):
            self.assertFalse(self.gerrit.CreateRevert('Reason', change_id))

    def testRequestAddsAuthenticationPrefix(self):
        self.gerrit._AuthenticatedRequest(['changes', '123'])
        url, _payload, _headers = self.http_client.requests[0]
        self.assertEqual('https://server.host.name/a/changes/123', url)

    def testRequestKeepsAuthenticationPrefix(self):
        self.gerrit._AuthenticatedRequest(['a', 'changes', '123'])
        url, _payload, _headers = self.http_client.requests[0]
        self.assertEqual('https://server.host.name/a/changes/123', url)

    @mock.patch.object(time_util,
                       'GetUTCNow',
                       return_value=datetime.datetime(2017, 2, 27, 20, 0, 0))
    def testGenerateRevertCLDescriptionBypassCQ(self, _):
        change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
        reason = 'Reason'

        original_cl_description = textwrap.dedent("""
    cl title

    some description

    NOAUTOREVERT= True
    BUGS : 12345, 67890
    Cq-Include-Trybots: m1.b1:m2.b2
    Change-Id: someid
    Reviewed-on: cl_url
    Commit-Queue: owner
    Reviewed-by: reviewers
    """).strip()

        expected_description = textwrap.dedent("""
    Revert "cl title"

    This reverts commit edda1046ce724695004242e943f59f5e1b2d00ff.

    Reason for revert:
    Reason

    Original change's description:
    > cl title
    >
    > some description
    >
    > NOAUTOREVERT= True
    > BUGS : 12345, 67890
    > Cq-Include-Trybots: m1.b1:m2.b2
    > Change-Id: someid
    > Reviewed-on: cl_url
    > Commit-Queue: owner
    > Reviewed-by: reviewers

    No-Presubmit: true
    No-Tree-Checks: true
    No-Try: true
    BUGS : 12345, 67890
    Cq-Include-Trybots: m1.b1:m2.b2
    """).strip()

        with mock.patch.object(
                self.gerrit,
                '_Get',
                return_value={
                    'change_id':
                    'I4303e1b7166aaab873587a3fda0ec907d3d8ace0',
                    'status':
                    'MERGED',
                    'owner': {
                        'email': '*****@*****.**'
                    },
                    'submitted':
                    '2017-02-27 18:56:54.000000000',
                    '_number':
                    446905,
                    'reviewers': {
                        'REVIEWER': [{
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }],
                        'CC': []
                    },
                    'messages': [{
                        'id': 'b7d6785c324297ec4f1e6b2de34cf83f4c58e87c',
                        'author': {
                            'email': '*****@*****.**'
                        },
                        'date': '2017-02-27 18:47:15.000000000',
                        'message': 'Patch Set 1: Commit-Queue+2',
                        '_revision_number': 1
                    }],
                    'current_revision':
                    'edda1046ce724695004242e943f59f5e1b2d00ff',
                    'revisions': {
                        'edda1046ce724695004242e943f59f5e1b2d00ff': {
                            '_number': 2,
                            'commit': {
                                'committer': {
                                    'email': '*****@*****.**',
                                },
                                'message':
                                original_cl_description,
                                'parents': [{
                                    'commit':
                                    ('ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f'
                                     ),
                                }]
                            },
                        }
                    },
                    'subject':
                    'cl title'
                }):

            # Assertions have never worked properly because we were using mock 1.0.1.
            # After rolling to mock 2.0.0, which fixes assertions, these assertions
            # now fail. https://crbug.com/948213
            # pylint:disable=unused-argument
            # self.assertEqual(
            #     expected_description,
            #     self.gerrit._GenerateRevertCLDescription(change_id, reason))
            pass

    @mock.patch.object(time_util,
                       'GetUTCNow',
                       return_value=datetime.datetime(2017, 6, 1, 1, 0, 0))
    def testGenerateRevertCLDescription(self, _):
        change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
        reason = 'Reason'

        original_cl_description = textwrap.dedent("""
    cl title

    some description

    NOAUTOREVERT= True
    BUGS : 12345, 67890
    Cq-Include-Trybots: m1.b1:m2.b2
    Change-Id: someid
    Reviewed-on: cl_url
    Commit-Queue: owner
    Reviewed-by: reviewers
    """).strip()

        expected_description = textwrap.dedent("""
    Revert "cl title"

    This reverts commit edda1046ce724695004242e943f59f5e1b2d00ff.

    Reason for revert:
    Reason

    Original change's description:
    > cl title
    >
    > some description
    >
    > NOAUTOREVERT= True
    > BUGS : 12345, 67890
    > Cq-Include-Trybots: m1.b1:m2.b2
    > Change-Id: someid
    > Reviewed-on: cl_url
    > Commit-Queue: owner
    > Reviewed-by: reviewers

    # Not skipping CQ checks because original CL landed > 1 day ago.

    BUGS : 12345, 67890
    Cq-Include-Trybots: m1.b1:m2.b2
    """).strip()

        with mock.patch.object(
                self.gerrit,
                '_Get',
                return_value={
                    'change_id':
                    'I4303e1b7166aaab873587a3fda0ec907d3d8ace0',
                    'status':
                    'MERGED',
                    'owner': {
                        'email': '*****@*****.**'
                    },
                    'submitted':
                    '2017-02-27 18:56:54.000000000',
                    '_number':
                    446905,
                    'reviewers': {
                        'REVIEWER': [{
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }],
                        'CC': []
                    },
                    'messages': [{
                        'id': 'b7d6785c324297ec4f1e6b2de34cf83f4c58e87c',
                        'author': {
                            'email': '*****@*****.**'
                        },
                        'date': '2017-02-27 18:47:15.000000000',
                        'message': 'Patch Set 1: Commit-Queue+2',
                        '_revision_number': 1
                    }],
                    'current_revision':
                    'edda1046ce724695004242e943f59f5e1b2d00ff',
                    'revisions': {
                        'edda1046ce724695004242e943f59f5e1b2d00ff': {
                            '_number': 2,
                            'commit': {
                                'committer': {
                                    'email': '*****@*****.**',
                                },
                                'message':
                                original_cl_description,
                                'parents': [{
                                    'commit':
                                    ('ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f'
                                     ),
                                }]
                            },
                        }
                    },
                    'subject':
                    'cl title'
                }):

            # Assertions have never worked properly because we were using mock 1.0.1.
            # After rolling to mock 2.0.0, which fixes assertions, these assertions
            # now fail. https://crbug.com/948213
            # pylint:disable=unused-argument
            # self.assertEqual(
            #     expected_description,
            #     self.gerrit._GenerateRevertCLDescription(change_id, reason))
            pass

    def testGetBugLine(self):
        expected_results = {
            'message': '',
            'BUG: 24343\n': 'BUG: 24343\n',
            'Bug: 23234\n': 'Bug: 23234\n',
            'issue: 34254\n': 'issue: 34254\n'
        }

        for k, v in expected_results.iteritems():
            self.assertEqual(v, self.gerrit._GetBugLine(k))

        self.assertEqual('Bug: 12345\n', self.gerrit._GetBugLine('', '12345'))
        self.assertEqual('BUG: 12345, 67890\n',
                         self.gerrit._GetBugLine('BUG: 12345\n', '67890'))

    def testGetCQTryBotLine(self):
        expected_results = {
            'message':
            '',
            'cq-include-trybots: m1.b1:m2.b2\n':
            'cq-include-trybots: m1.b1:m2.b2\n',
            'CQ_INCLUDE_TRYBOTS= m1.b1:m2.b2 \n':
            'CQ_INCLUDE_TRYBOTS= m1.b1:m2.b2\n',
            'Cq_Include_Trybots= m1.b1:m2.b2\n':
            'Cq_Include_Trybots= m1.b1:m2.b2\n'
        }
        for k, v in expected_results.iteritems():
            self.assertEqual(v, self.gerrit._GetCQTryBotLine(k))

    @mock.patch.object(time_util,
                       'GetUTCNow',
                       return_value=datetime.datetime(2017, 2, 7, 1, 0, 0))
    def testGetCQFlagsOrExplanationWithinOneDay(self, _):
        time = datetime.datetime(2017, 2, 7, 0, 0, 0)
        self.assertEqual(
            'No-Presubmit: true\nNo-Tree-Checks: true\nNo-Try: true\n',
            self.gerrit._GetCQFlagsOrExplanation(time))

    def testSubmitRevert(self):
        change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
        response = self.http_client._MakeResponse({'change_id': change_id})
        self.http_client._SetSubmitRevertResponse(self.server_hostname,
                                                  change_id, response)
        self.assertTrue(self.gerrit.SubmitRevert(change_id))

    @mock.patch.object(logging, 'error')
    def testSubmitRevertSubmitRuleFailed(self, mock_logging):
        change_id = '123456'
        url = 'https://%s/a/changes/%s/submit' % (self.server_hostname,
                                                  change_id)
        response = 'Change 123456: needs Is-Pure-Revert'
        self.http_client.SetResponse(url, (409, response))
        self.assertFalse(self.gerrit.SubmitRevert(change_id))
        # Assertions have never worked properly because we were using mock 1.0.1.
        # After rolling to mock 2.0.0, which fixes assertions, these assertions now
        # fail. https://crbug.com/947753.
        # pylint:disable=unused-argument
        # mock_logging.assert_has_called_with(
        #     'Committing revert failed: Change 123456: needs Is-Pure-Revert')

    def testGetChangeIdFromReviewUrl(self):
        change_id = 'I40bc1e744806f2c4aadf0ce6609aaa61b4019fa7'
        url = 'https://server.host.name/q/%s' % change_id
        self.assertEqual(change_id, self.gerrit.GetChangeIdFromReviewUrl(url))

    def testGet(self):
        path_parts = ['changes', '12345', 'detail']

        with mock.patch.object(self.gerrit,
                               '_HandleResponse',
                               return_value='return_value'):
            self.assertEqual(self.gerrit._Get(path_parts), 'return_value')

    def testQueryClsNoQueryParames(self):
        self.assertEquals([], self.gerrit.QueryCls({}))

    def testQueryCls(self):
        with mock.patch.object(
                self.gerrit,
                '_Get',
                return_value=[{
                    'change_id':
                    'I4303e1b7166aaab873587a3fda0ec907d3d8ace0',
                    'status':
                    'MERGED',
                    'owner': {
                        'email': '*****@*****.**'
                    },
                    'submitted':
                    '2017-02-27 18:56:54.000000000',
                    '_number':
                    446905,
                    'reviewers': {
                        'REVIEWER': [{
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }, {
                            'email': '*****@*****.**'
                        }],
                        'CC': []
                    },
                    'messages': [{
                        'id': 'b7d6785c324297ec4f1e6b2de34cf83f4c58e87c',
                        'author': {
                            'email': '*****@*****.**'
                        },
                        'date': '2017-02-27 18:47:15.000000000',
                        'message': 'Patch Set 1: Commit-Queue+2',
                        '_revision_number': 1
                    }, {
                        'id':
                        'b9e04aec4bffed0284c1f53cc5a9c88818807368',
                        'author': {
                            'email': '*****@*****.**'
                        },
                        'date':
                        '2017-02-27 19:04:51.000000000',
                        'message':
                        'Created a revert of this change as '
                        'If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
                        '_revision_number':
                        2
                    }],
                    'current_revision':
                    'edda1046ce724695004242e943f59f5e1b2d00ff',
                    'revisions': {
                        'edda1046ce724695004242e943f59f5e1b2d00ff': {
                            '_number': 2,
                            'commit': {
                                'committer': {
                                    'email': '*****@*****.**',
                                },
                                'message':
                                'cl title\n\nsome description\n\n'
                                'NOAUTOREVERT= True\n\nChange-Id: '
                                'someid\nReviewed-on: cl_url\nCommit-Queue: '
                                'owner\nReviewed-by: reviewers\n\n'
                                'BUGS : 12345, 67890',
                                'parents': [{
                                    'commit':
                                    ('ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f'
                                     ),
                                }]
                            },
                        }
                    },
                    'subject':
                    'subject'
                }, {
                    'change_id':
                    'If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
                    'status':
                    'MERGED',
                    'submitted':
                    '2017-02-27 19:05:03.000000000',
                    'owner': {
                        'email': '*****@*****.**'
                    },
                    '_number':
                    446788,
                    'reviewers': {
                        'REVIEWER': [{
                            'email': '*****@*****.**',
                        }, {
                            'email': '*****@*****.**',
                        }, {
                            'email': '*****@*****.**',
                        }]
                    },
                    'messages': [
                        {
                            'id': '30496ce351a43c0b74d812e9e40b440f5acff9d5',
                            'author': {
                                'email': '*****@*****.**',
                            },
                            'date': '2017-02-27 19:04:53.000000000',
                            'message':
                            'Patch Set 1: Code-Review+1 Commit-Queue+2',
                            '_revision_number': 1
                        },
                    ],
                    'current_revision':
                    'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824',
                    'revisions': {
                        'bd1db4534d7dc3f3f9c693ca0ac3e67caf56789': {
                            '_number': 1,
                            'commit': {
                                'committer': {
                                    'email': '*****@*****.**',
                                    'date': '2017-02-27 19:05:03.000000000',
                                    'tz': 0
                                },
                                'message':
                                'cl title\n\nsome description\n\n'
                                'NOAUTOREVERT=TRUE\n\nChange-Id: '
                                'someid\nReviewed-on: cl_url\nCommit-Queue: '
                                'owner\nReviewed-by: reviewers\n\n'
                                'BUG: 123455',
                                'parents': [{
                                    'commit':
                                    ('b63383e4e3f2b774ce0e6d3bfafb4adc84e48f2b'
                                     ),
                                }]
                            },
                        },
                        'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824': {
                            '_number': 2,
                            'commit': {
                                'committer': {
                                    'email': '*****@*****.**',
                                    'date': '2017-02-27 19:05:03.000000000',
                                    'tz': 0
                                },
                                'message':
                                'cl title\n\nsome description\n\n'
                                'NOAUTOREVERT=TRUE\n\nChange-Id: '
                                'someid\nReviewed-on: cl_url\nCommit-Queue: '
                                'owner\nReviewed-by: reviewers\n\n'
                                'BUG: 123455',
                                'parents': [{
                                    'commit':
                                    ('b63383e4e3f2b774ce0e6d3bfafb4adc84e48f2b'
                                     ),
                                }]
                            },
                        }
                    },
                    'subject':
                    'subject',
                    'revert_of':
                    446905
                }]):
            cls_info = self.gerrit.QueryCls({'author': '*****@*****.**'})

            expected_cl_info = [{
                'server_hostname':
                'server.host.name',
                'auto_revert_off':
                True,
                'owner_email':
                '*****@*****.**',
                'reviewers': [
                    '*****@*****.**', '*****@*****.**',
                    '*****@*****.**'
                ],
                'closed':
                True,
                'commits': [{
                    'patchset_id':
                    2,
                    'timestamp':
                    '2017-02-27 18:56:54 UTC',
                    'revision':
                    'edda1046ce724695004242e943f59f5e1b2d00ff',
                    'parent_revisions':
                    ['ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f']
                }],
                'cc': [],
                'subject':
                'subject',
                'description':
                'cl title\n\nsome description\n\n'
                'NOAUTOREVERT= True\n\nChange-Id: '
                'someid\nReviewed-on: cl_url\nCommit-Queue: '
                'owner\nReviewed-by: reviewers\n\n'
                'BUGS : 12345, 67890',
                'change_id':
                'I4303e1b7166aaab873587a3fda0ec907d3d8ace0',
                'reverts': [],
                'commit_attempts': [{
                    'patchset_id': 1,
                    'timestamp': '2017-02-27 18:47:15 UTC',
                    'committing_user_email': '*****@*****.**'
                }],
                'revert_of':
                None,
                'patchsets': {
                    'edda1046ce724695004242e943f59f5e1b2d00ff': {
                        'patchset_id':
                        2,
                        'revision':
                        'edda1046ce724695004242e943f59f5e1b2d00ff',
                        'parent_revisions':
                        ['ef9ced2eee002d8c143d64d1e66e0a8eaeb8078f']
                    }
                }
            }, {
                'server_hostname':
                'server.host.name',
                'auto_revert_off':
                True,
                'owner_email':
                '*****@*****.**',
                'reviewers': [
                    '*****@*****.**', '*****@*****.**',
                    '*****@*****.**'
                ],
                'closed':
                True,
                'commits': [{
                    'patchset_id':
                    2,
                    'timestamp':
                    '2017-02-27 19:05:03 UTC',
                    'revision':
                    'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824',
                    'parent_revisions':
                    ['b63383e4e3f2b774ce0e6d3bfafb4adc84e48f2b']
                }],
                'cc': [],
                'subject':
                'subject',
                'description':
                'cl title\n\nsome description\n\n'
                'NOAUTOREVERT=TRUE\n\nChange-Id: '
                'someid\nReviewed-on: cl_url\n'
                'Commit-Queue: '
                'owner\nReviewed-by: reviewers\n\n'
                'BUG: 123455',
                'change_id':
                'If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
                'reverts': [],
                'commit_attempts': [{
                    'patchset_id': 1,
                    'timestamp': '2017-02-27 19:04:53 UTC',
                    'committing_user_email': '*****@*****.**'
                }],
                'revert_of':
                446905,
                'patchsets': {
                    'bd1db4534d7dc3f3f9c693ca0ac3e67caf56789': {
                        'patchset_id':
                        1,
                        'revision':
                        'bd1db4534d7dc3f3f9c693ca0ac3e67caf56789',
                        'parent_revisions':
                        ['b63383e4e3f2b774ce0e6d3bfafb4adc84e48f2b']
                    },
                    'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824': {
                        'patchset_id':
                        2,
                        'revision':
                        'bd1db4534d7dc3f3f9c693ca0ac3e67caf484824',
                        'parent_revisions':
                        ['b63383e4e3f2b774ce0e6d3bfafb4adc84e48f2b']
                    }
                }
            }]

            s_results = [cl_info.serialize() for cl_info in cls_info]
            self.assertEqual(expected_cl_info, s_results)