| #!/usr/bin/env python |
| # Copyright (c) 2012 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. |
| |
| """Unit tests for rietveld.py.""" |
| |
| import httplib |
| import logging |
| import os |
| import socket |
| import ssl |
| import StringIO |
| import sys |
| import tempfile |
| import time |
| import traceback |
| import unittest |
| import urllib2 |
| |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| |
| from testing_support.patches_data import GIT, RAW |
| from testing_support import auto_stub |
| from third_party import httplib2 |
| |
| import patch |
| import rietveld |
| |
| |
| def _api(files): |
| """Mock a rietveld api request.""" |
| return rietveld.json.dumps({'files': files}) |
| |
| |
| def _file( |
| status, is_binary=False, num_chunks=1, chunk_id=789, property_changes=''): |
| """Mock a file in a rietveld api request.""" |
| return { |
| 'status': status, |
| 'is_binary': is_binary, |
| 'num_chunks': num_chunks, |
| 'id': chunk_id, |
| 'property_changes': property_changes, |
| } |
| |
| |
| class BaseFixture(unittest.TestCase): |
| # Override. |
| TESTED_CLASS = Exception |
| |
| def setUp(self): |
| super(BaseFixture, self).setUp() |
| # Access to a protected member XX of a client class |
| # pylint: disable=protected-access |
| self.rietveld = self.TESTED_CLASS('url', None, 'email') |
| self.rietveld._send = self._rietveld_send |
| self.requests = [] |
| |
| def tearDown(self): |
| self.assertEqual([], self.requests) |
| super(BaseFixture, self).tearDown() |
| |
| def _rietveld_send(self, url, *args, **kwargs): |
| self.assertTrue(self.requests, url) |
| request = self.requests.pop(0) |
| self.assertEqual(2, len(request)) |
| self.assertEqual(url, request[0]) |
| return request[1] |
| |
| def _check_patch(self, |
| p, |
| filename, |
| diff, |
| source_filename=None, |
| is_binary=False, |
| is_delete=False, |
| is_git_diff=False, |
| is_new=False, |
| patchlevel=0, |
| svn_properties=None): |
| svn_properties = svn_properties or [] |
| self.assertEqual(p.filename, filename) |
| self.assertEqual(p.source_filename, source_filename) |
| self.assertEqual(p.is_binary, is_binary) |
| self.assertEqual(p.is_delete, is_delete) |
| if hasattr(p, 'is_git_diff'): |
| self.assertEqual(p.is_git_diff, is_git_diff) |
| self.assertEqual(p.is_new, is_new) |
| if hasattr(p, 'patchlevel'): |
| self.assertEqual(p.patchlevel, patchlevel) |
| if diff: |
| self.assertEqual(p.get(True), diff) |
| if hasattr(p, 'svn_properties'): |
| self.assertEqual(p.svn_properties, svn_properties) |
| |
| |
| class RietveldTest(BaseFixture): |
| TESTED_CLASS = rietveld.Rietveld |
| |
| def test_get_patch_empty(self): |
| self.requests = [('/api/123/456', '{}')] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertTrue(isinstance(patches, patch.PatchSet)) |
| self.assertEqual([], patches.patches) |
| |
| def test_get_patch_no_status(self): |
| self.requests = [ |
| ( '/api/123/456', |
| _api( |
| { |
| 'tools/clang_check/README.chromium': { |
| 'status': None, |
| 'id': 789, |
| }})), |
| ('/download/issue123_456_789.diff', RAW.DELETE), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(1, len(patches.patches)) |
| self._check_patch( |
| patches.patches[0], |
| 'tools/clang_check/README.chromium', |
| RAW.DELETE, |
| is_delete=True) |
| |
| def test_get_patch_2_files(self): |
| self.requests = [ |
| ('/api/123/456', |
| _api({'foo': _file('A'), 'file_a': _file('M', chunk_id=790)})), |
| ('/download/issue123_456_789.diff', RAW.NEW), |
| ('/download/issue123_456_790.diff', RAW.NEW_NOT_NULL), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(2, len(patches.patches)) |
| self._check_patch( |
| patches.patches[0], 'file_a', RAW.NEW_NOT_NULL, is_new=True) |
| self._check_patch(patches.patches[1], 'foo', RAW.NEW, is_new=True) |
| |
| def test_get_patch_add(self): |
| self.requests = [ |
| ('/api/123/456', _api({'foo': _file('A')})), |
| ('/download/issue123_456_789.diff', RAW.NEW), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(1, len(patches.patches)) |
| self._check_patch(patches.patches[0], 'foo', RAW.NEW, is_new=True) |
| |
| def test_invalid_status(self): |
| self.requests = [ |
| ('/api/123/456', _api({'file_a': _file('B')})), |
| ] |
| try: |
| self.rietveld.get_patch(123, 456) |
| self.fail() |
| except patch.UnsupportedPatchFormat, e: |
| self.assertEqual('file_a', e.filename) |
| |
| def test_add_plus_merge(self): |
| # svn:mergeinfo is dropped. |
| properties = ( |
| '\nAdded: svn:mergeinfo\n' |
| ' Merged /branches/funky/file_b:r69-2775\n') |
| self.requests = [ |
| ('/api/123/456', |
| _api({'pp': _file('A+', property_changes=properties)})), |
| ('/download/issue123_456_789.diff', GIT.COPY), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(1, len(patches.patches)) |
| self._check_patch( |
| patches.patches[0], |
| 'pp', |
| GIT.COPY, |
| is_git_diff=True, |
| is_new=True, |
| patchlevel=1, |
| source_filename='PRESUBMIT.py') |
| |
| def test_add_plus_eol_style(self): |
| properties = '\nAdded: svn:eol-style\n + LF\n' |
| self.requests = [ |
| ('/api/123/456', |
| _api({'pp': _file('A+', property_changes=properties)})), |
| ('/download/issue123_456_789.diff', GIT.COPY), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(1, len(patches.patches)) |
| self._check_patch( |
| patches.patches[0], |
| 'pp', |
| GIT.COPY, |
| is_git_diff=True, |
| is_new=True, |
| patchlevel=1, |
| source_filename='PRESUBMIT.py', |
| svn_properties=[('svn:eol-style', 'LF')]) |
| |
| def test_add_empty(self): |
| self.requests = [ |
| ('/api/123/456', _api({'__init__.py': _file('A ', num_chunks=0)})), |
| ('/download/issue123_456_789.diff', RAW.CRAP_ONLY), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(1, len(patches.patches)) |
| self._check_patch( |
| patches.patches[0], |
| '__init__.py', |
| RAW.CRAP_ONLY, |
| is_new=True) |
| |
| def test_delete(self): |
| name = 'tools/clang_check/README.chromium' |
| self.requests = [ |
| ('/api/123/456', _api({name: _file('D')})), |
| ('/download/issue123_456_789.diff', RAW.DELETE), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(1, len(patches.patches)) |
| self._check_patch(patches.patches[0], name, RAW.DELETE, is_delete=True) |
| |
| def test_delete_empty(self): |
| name = 'tests/__init__.py' |
| self.requests = [ |
| ('/api/123/456', _api({name: _file('D')})), |
| ('/download/issue123_456_789.diff', GIT.DELETE_EMPTY), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(1, len(patches.patches)) |
| self._check_patch( |
| patches.patches[0], |
| name, |
| GIT.DELETE_EMPTY, |
| is_delete=True, |
| is_git_diff=True, |
| patchlevel=1) |
| |
| def test_m_plus(self): |
| properties = '\nAdded: svn:eol-style\n + LF\n' |
| self.requests = [ |
| ('/api/123/456', |
| _api({'chrome/file.cc': _file('M+', property_changes=properties)})), |
| ('/download/issue123_456_789.diff', RAW.PATCH), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(1, len(patches.patches)) |
| self._check_patch( |
| patches.patches[0], |
| 'chrome/file.cc', |
| RAW.PATCH, |
| svn_properties=[('svn:eol-style', 'LF')]) |
| |
| def test_m_plus_unknown_prop(self): |
| properties = '\nAdded: svn:foobar\n + stuff\n' |
| self.requests = [ |
| ('/api/123/456', |
| _api({'file_a': _file('M+', property_changes=properties)})), |
| ] |
| try: |
| self.rietveld.get_patch(123, 456) |
| self.fail() |
| except patch.UnsupportedPatchFormat, e: |
| self.assertEqual('file_a', e.filename) |
| |
| def test_get_patch_moved(self): |
| self.requests = [ |
| ('/api/123/456', _api({'file_b': _file('A+')})), |
| ('/download/issue123_456_789.diff', RAW.MINIMAL_RENAME), |
| ] |
| patches = self.rietveld.get_patch(123, 456) |
| self.assertEqual(1, len(patches.patches)) |
| self._check_patch( |
| patches.patches[0], |
| 'file_b', |
| RAW.MINIMAL_RENAME, |
| source_filename='file_a', |
| is_new=True) |
| |
| def test_svn_properties(self): |
| # pylint: disable=line-too-long |
| |
| # To test one of these, run something like |
| # import json, pprint, urllib |
| # url = 'http://codereview.chromium.org/api/202046/1' |
| # pprint.pprint(json.load(urllib.urlopen(url))['files']) |
| |
| # svn:mergeinfo across branches: |
| # http://codereview.chromium.org/202046/diff/1/third_party/libxml/xmlcatalog_dummy.cc |
| self.assertEqual( |
| [('svn:eol-style', 'LF')], |
| rietveld.Rietveld.parse_svn_properties( |
| u'\nAdded: svn:eol-style\n + LF\n', 'foo')) |
| |
| # svn:eol-style property that is lost in the diff |
| # http://codereview.chromium.org/202046/diff/1/third_party/libxml/xmllint_dummy.cc |
| self.assertEqual( |
| [], |
| rietveld.Rietveld.parse_svn_properties( |
| u'\nAdded: svn:mergeinfo\n' |
| ' Merged /branches/chrome_webkit_merge_branch/third_party/' |
| 'libxml/xmldummy_mac.cc:r69-2775\n', |
| 'foo')) |
| |
| self.assertEqual( |
| [], |
| rietveld.Rietveld.parse_svn_properties(u'', 'foo')) |
| |
| |
| # http://codereview.chromium.org/api/7834045/15001 |
| self.assertEqual( |
| [('svn:executable', '*'), ('svn:eol-style', 'LF')], |
| rietveld.Rietveld.parse_svn_properties( |
| '\n' |
| 'Added: svn:executable\n' |
| ' + *\n' |
| 'Added: svn:eol-style\n' |
| ' + LF\n', |
| 'foo')) |
| |
| # http://codereview.chromium.org/api/9139006/7001 |
| self.assertEqual( |
| [('svn:mime-type', 'image/png')], |
| rietveld.Rietveld.parse_svn_properties( |
| '\n' |
| 'Added: svn:mime-type\n' |
| ' + image/png\n', |
| 'foo')) |
| |
| def test_bad_svn_properties(self): |
| try: |
| rietveld.Rietveld.parse_svn_properties(u'\n', 'foo') |
| self.fail() |
| except rietveld.patch.UnsupportedPatchFormat, e: |
| self.assertEqual('foo', e.filename) |
| # TODO(maruel): Change with no diff, only svn property change: |
| # http://codereview.chromium.org/6462019/ |
| |
| def test_search_all_empty(self): |
| url = ( |
| '/search?format=json' |
| '&base=base' |
| '&created_after=2010-01-02' |
| '&created_before=2010-01-01' |
| '&modified_after=2010-02-02' |
| '&modified_before=2010-02-01' |
| '&owner=owner%40example.com' |
| '&reviewer=reviewer%40example.com' |
| '&closed=2' |
| '&commit=2' |
| '&private=2' |
| '&keys_only=True' |
| '&with_messages=True' |
| '&limit=23') |
| self.requests = [ |
| (url, '{}'), |
| ] |
| results = list(self.rietveld.search( |
| '[email protected]', |
| '[email protected]', |
| 'base', |
| True, |
| True, |
| True, |
| '2010-01-01', |
| '2010-01-02', |
| '2010-02-01', |
| '2010-02-02', |
| 23, |
| True, |
| True, |
| )) |
| self.assertEqual([], results) |
| |
| def test_results_cursor(self): |
| # Verify cursor iteration is transparent. |
| self.requests = [ |
| ('/search?format=json&base=base', |
| rietveld.json.dumps({ |
| 'cursor': 'MY_CURSOR', |
| 'results': [{'foo': 'bar'}, {'foo': 'baz'}], |
| })), |
| ('/search?format=json&base=base&cursor=MY_CURSOR', |
| rietveld.json.dumps({ |
| 'cursor': 'NEXT', |
| 'results': [{'foo': 'prout'}], |
| })), |
| ('/search?format=json&base=base&cursor=NEXT', |
| rietveld.json.dumps({ |
| 'cursor': 'VOID', |
| 'results': [], |
| })), |
| ] |
| expected = [ |
| {'foo': 'bar'}, |
| {'foo': 'baz'}, |
| {'foo': 'prout'}, |
| ] |
| for i in self.rietveld.search(base='base'): |
| self.assertEqual(expected.pop(0), i) |
| self.assertEqual([], expected) |
| |
| |
| class CachingRietveldTest(BaseFixture): |
| # Tests only one request is done. |
| TESTED_CLASS = rietveld.CachingRietveld |
| |
| def test_get_description(self): |
| self.requests = [ |
| ('/1/description', 'Blah blah blah'), |
| ] |
| expected = 'Blah blah blah' |
| self.assertEqual(expected, self.rietveld.get_description(1)) |
| self.assertEqual(expected, self.rietveld.get_description(1)) |
| |
| def test_get_issue_properties(self): |
| data = {'description': 'wow\r\nno CR!', 'messages': 'foo'} |
| self.requests = [ |
| ('/api/1?messages=true', rietveld.json.dumps(data)), |
| ] |
| expected = {u'description': u'wow\nno CR!'} |
| expected_msg = {u'description': u'wow\nno CR!', u'messages': u'foo'} |
| self.assertEqual(expected, self.rietveld.get_issue_properties(1, False)) |
| self.assertEqual(expected_msg, self.rietveld.get_issue_properties(1, True)) |
| |
| def test_get_patchset_properties(self): |
| self.requests = [ |
| ('/api/1/2', '{}'), |
| ] |
| expected = {} |
| self.assertEqual(expected, self.rietveld.get_patchset_properties(1, 2)) |
| self.assertEqual(expected, self.rietveld.get_patchset_properties(1, 2)) |
| |
| |
| class ProbeException(Exception): |
| """Deep-probe a value.""" |
| value = None |
| |
| def __init__(self, value): |
| super(ProbeException, self).__init__() |
| self.value = value |
| |
| |
| def MockSend(*args, **kwargs): |
| """Mock upload.py's Send() to probe the timeout value""" |
| raise ProbeException(kwargs['timeout']) |
| |
| |
| def MockSendTimeout(*args, **kwargs): |
| """Mock upload.py's Send() to raise SSLError""" |
| raise ssl.SSLError('The read operation timed out') |
| |
| |
| def MockSocketConnectTimeout(*args, **kwargs): |
| """Mock upload.py's Send() to raise socket.timeout""" |
| raise socket.timeout('timed out') |
| |
| |
| class DefaultTimeoutTest(auto_stub.TestCase): |
| TESTED_CLASS = rietveld.Rietveld |
| |
| def setUp(self): |
| super(DefaultTimeoutTest, self).setUp() |
| self.rietveld = self.TESTED_CLASS('url', None, 'email') |
| self.mock(self.rietveld.rpc_server, 'Send', MockSend) |
| self.sleep_time = 0 |
| |
| def test_timeout_get(self): |
| with self.assertRaises(ProbeException) as cm: |
| self.rietveld.get('/api/1234') |
| |
| self.assertIsNotNone(cm.exception.value, 'Rietveld timeout was not set: %s' |
| % traceback.format_exc()) |
| |
| def test_timeout_post(self): |
| with self.assertRaises(ProbeException) as cm: |
| self.rietveld.post('/api/1234', [('key', 'data')]) |
| |
| self.assertIsNotNone(cm.exception.value, 'Rietveld timeout was not set: %s' |
| % traceback.format_exc()) |
| |
| def MockSleep(self, t): |
| self.sleep_time = t |
| |
| def test_ssl_timeout_post(self): |
| self.mock(self.rietveld.rpc_server, 'Send', MockSendTimeout) |
| self.mock(time, 'sleep', self.MockSleep) |
| with self.assertRaises(ssl.SSLError): |
| self.rietveld.post('/api/1234', [('key', 'data')]) |
| self.assertNotEqual(self.sleep_time, 0) |
| |
| def test_socket_connect_timeout_post(self): |
| self.mock(self.rietveld.rpc_server, 'Send', MockSocketConnectTimeout) |
| self.mock(time, 'sleep', self.MockSleep) |
| with self.assertRaises(socket.timeout): |
| self.rietveld.post('/api/1234', [('key', 'data')]) |
| self.assertNotEqual(self.sleep_time, 0) |
| |
| |
| class OAuthRpcServerTest(auto_stub.TestCase): |
| def setUp(self): |
| super(OAuthRpcServerTest, self).setUp() |
| self.rpc_server = rietveld.OAuthRpcServer( |
| 'http://www.example.com', 'foo', 'bar') |
| |
| def set_mock_response(self, status): |
| def mock_http_request(*args, **kwargs): |
| return (httplib2.Response({'status': status}), 'body') |
| self.mock(self.rpc_server._http, 'request', mock_http_request) |
| |
| def test_404(self): |
| self.set_mock_response(404) |
| with self.assertRaises(urllib2.HTTPError) as ctx: |
| self.rpc_server.Send('/foo') |
| self.assertEquals(404, ctx.exception.code) |
| |
| def test_200(self): |
| self.set_mock_response(200) |
| ret = self.rpc_server.Send('/foo') |
| self.assertEquals('body', ret) |
| |
| |
| class RietveldOAuthRpcServerTest(auto_stub.TestCase): |
| def setUp(self): |
| super(RietveldOAuthRpcServerTest, self).setUp() |
| with tempfile.NamedTemporaryFile() as private_key_file: |
| self.rietveld = rietveld.JwtOAuth2Rietveld( |
| 'http://www.example.com', 'foo', private_key_file.name, maxtries=2) |
| |
| self.mock(time, 'sleep', lambda duration: None) |
| |
| def test_retries_500(self): |
| urls = [] |
| def mock_http_request(url, *args, **kwargs): |
| urls.append(url) |
| return (httplib2.Response({'status': 500}), 'body') |
| self.mock(self.rietveld.rpc_server._http, 'request', mock_http_request) |
| |
| with self.assertRaises(urllib2.HTTPError) as ctx: |
| self.rietveld.get('/foo') |
| self.assertEquals(500, ctx.exception.code) |
| |
| self.assertEqual(2, len(urls)) # maxtries was 2 |
| self.assertEqual(['https://www.example.com/foo'] * 2, urls) |
| |
| def test_does_not_retry_404(self): |
| urls = [] |
| def mock_http_request(url, *args, **kwargs): |
| urls.append(url) |
| return (httplib2.Response({'status': 404}), 'body') |
| self.mock(self.rietveld.rpc_server._http, 'request', mock_http_request) |
| |
| with self.assertRaises(urllib2.HTTPError) as ctx: |
| self.rietveld.get('/foo') |
| self.assertEquals(404, ctx.exception.code) |
| |
| self.assertEqual(1, len(urls)) # doesn't retry |
| |
| def test_retries_404_when_requested(self): |
| urls = [] |
| def mock_http_request(url, *args, **kwargs): |
| urls.append(url) |
| return (httplib2.Response({'status': 404}), 'body') |
| self.mock(self.rietveld.rpc_server._http, 'request', mock_http_request) |
| |
| with self.assertRaises(urllib2.HTTPError) as ctx: |
| self.rietveld.get('/foo', retry_on_404=True) |
| self.assertEquals(404, ctx.exception.code) |
| |
| self.assertEqual(2, len(urls)) # maxtries was 2 |
| |
| def test_socket_timeout(self): |
| urls = [] |
| def mock_http_request(url, *args, **kwargs): |
| urls.append(url) |
| raise socket.error('timed out') |
| self.mock(self.rietveld.rpc_server._http, 'request', mock_http_request) |
| |
| with self.assertRaises(socket.error): |
| self.rietveld.get('/foo') |
| |
| self.assertEqual(2, len(urls)) # maxtries was 2 |
| |
| def test_other_socket_error(self): |
| urls = [] |
| def mock_http_request(url, *args, **kwargs): |
| urls.append(url) |
| raise socket.error('something else') |
| self.mock(self.rietveld.rpc_server._http, 'request', mock_http_request) |
| |
| with self.assertRaises(socket.error): |
| self.rietveld.get('/foo') |
| |
| self.assertEqual(1, len(urls)) |
| |
| |
| if __name__ == '__main__': |
| logging.basicConfig(level=[ |
| logging.ERROR, logging.INFO, logging.DEBUG][min(2, sys.argv.count('-v'))]) |
| unittest.main() |