Merge lp:~milo/linaro-patchmetrics/bug1013577 into lp:linaro-patchmetrics

Proposed by Milo Casagrande
Status: Merged
Merged at revision: 363
Proposed branch: lp:~milo/linaro-patchmetrics/bug1013577
Merge into: lp:linaro-patchmetrics
Diff against target: 394 lines (+248/-47)
4 files modified
apps/patchmetrics/bin/sync-gerrit-changes.py (+16/-9)
apps/patchmetrics/gerrit.py (+167/-36)
apps/patchmetrics/gerrit_json.py (+20/-0)
apps/patchmetrics/tests/test_gerrit.py (+45/-2)
To merge this branch: bzr merge lp:~milo/linaro-patchmetrics/bug1013577
Reviewer Review Type Date Requested Status
Deepti B. Kalakeri (community) Approve
Linaro Infrastructure Pending
Review via email: mp+111432@code.launchpad.net

Description of the change

Hello,

I would like to propose the following changes to linaro-patchmetrics in order to fix the problems we are facing with gerrit and its transition to a new REST interface from the JSON-RPC one.

The merge proposal does not fix all the problems, it is still a work in progress, since the JSON output provided by the new REST interface is different than the old one, and we need to make further changes in the code base (I need to retrieve some JSON output from an old version of Gerrit, probably using Linaro instance on Gerrit, and compare it with the new one).

Since there are already substantial changes, I'm asking for a preliminary review.

What is included in this merge proposal is:
- PEP8 fixes.
- A fix to the call invoked by the script 'sync-gerrit-changes.py' (the script itself did not change): this fix introduces new functions in the 'patchmetrics/gerrit.py' file, adding new functionalities in order to address the construction of the REST query string. What I tried to do has been to keep the new functions as small as possible, in order for them to be easier to maintain and to be as "testable" as possible. I tried to keep the function also as reusable as possible, in order to use them in the future to fix the other JSON-RPC deprecation we might encouter (and I guess there will be more). The new functions basically create the query strings from the parameters passed: there is a specific function to create the correct query string, since it has a particular construct.
- I also wrote new tests for the new functions added.

To post a comment you must log in.
363. By Milo Casagrande

Fixed error on test, fixed indentation.

Revision history for this message
Deepti B. Kalakeri (deeptik) wrote :

The changes looks good. One typo needs to be fixed

"more complex queries need to be though of, since" , should be "more complex queries need to be thought of, since"

Also, I was not able to find any information on how to run the tests.
Can you point me where this information is present OR If it does not exist then could you add a README somewhere ?

You can make these changes while merging. I will approve this so that we can go ahead with trying to see if it fixes our initial problems.

review: Approve
364. By Milo Casagrande

Use the new JSON format to create the necessary objects.

Revision history for this message
Milo Casagrande (milo) wrote :

Hello,

first, thanks Deepti for the first review!
I updated the code again, pushing also the final fixes for the gerrit problem.

With the latest push:
- I introduced a new python module, 'gerrit_json.py', that only holds variables that are the names used inside the Gerrit JSON output: these variables are used to retrieve the values from the output, and might be used also somewhere else in the code, that is why I created a separate module.
- I fixed all the old calls in the code that were used to access the JSON values: before we had a list of elements, now we have a list of dictionaries whose values we need.
- Cleaned up also other parts of the code, in particular:
 * Use "COMPATC_JSON" as the default JSON output, so that it should make a little bit smaller and hopefully faster.
 * Use "gzip" compression on the output from the server: probably needs more testing to see if it really is beneficial, but if we get back a lot of results from the server, we can save a little bit on space and gain some peed.
 * Cleaned up a little bit the tests.
 * Added some links as a reference to Gerrit REST API.

For the tests part, they should be called directly from the command line from django, but I have problem with my django setup: for my own tests, I created a small unittest class that calls all the necessary parts to be tested.

Revision history for this message
Milo Casagrande (milo) wrote :

Oh, I forgot, I removed also one entire function that was not needed anymore: before, some of the necessary information, were retrieved via two calls to the Java Servlet: one to get the actual changes, and another one to get the details of the change. Now all the information is stored in the same output.

For a comparison of the two JSON output, look at the bug report from the merge proposal, I attached there the output we get from android-review.googlesource.com with the new REST API, and the output we get from the Linaro gerrit instance using the old methods. Both the request are made using a user that has changes in both instances.

The difference from the old approach, is that now if a user is not in the Gerrit database, we receive back an error 400, that we catch and deal with.

365. By Milo Casagrande

Fixed indentation; re-added return statement, problem with tests.

Revision history for this message
Deepti B. Kalakeri (deeptik) wrote :

> Hello,
>
> first, thanks Deepti for the first review!
> I updated the code again, pushing also the final fixes for the gerrit problem.
>
> With the latest push:
> - I introduced a new python module, 'gerrit_json.py', that only holds
> variables that are the names used inside the Gerrit JSON output: these
> variables are used to retrieve the values from the output, and might be used
> also somewhere else in the code, that is why I created a separate module.
> - I fixed all the old calls in the code that were used to access the JSON
> values: before we had a list of elements, now we have a list of dictionaries
> whose values we need.
> - Cleaned up also other parts of the code, in particular:
> * Use "COMPATC_JSON" as the default JSON output, so that it should make a
> little bit smaller and hopefully faster.
> * Use "gzip" compression on the output from the server: probably needs more
> testing to see if it really is beneficial, but if we get back a lot of results
> from the server, we can save a little bit on space and gain some peed.
> * Cleaned up a little bit the tests.
> * Added some links as a reference to Gerrit REST API.
>
> For the tests part, they should be called directly from the command line from
> django, but I have problem with my django setup: for my own tests, I created a
> small unittest class that calls all the necessary parts to be tested.

It would be good if you added your test script here as well.

Revision history for this message
Milo Casagrande (milo) wrote :

Hi Deepti,

I attached it to the bug report, since here it is not possible to insert files, and I would prefer not to upload it in the bzr branch, as it is not really related to the test infrastructure there. I have that file in the 'apps/patchmetrics/test/ directory'.

You might need to tweak your PYTHONPATH to make it work, since it needs to pick-up other files from the project while it imports the other statements.
Otherwise, I have another file that is a copy of the modified functions in 'gerrit.py' and that is possible to test directly without importing all the rest of the project.

Revision history for this message
Deepti B. Kalakeri (deeptik) wrote :

Thanks for the fixes:

161 # The request in this case needs to be a GET.
162 json_string = sanitize_json_response(urllib2.urlopen(request).read())
163 return json_string
It is probably good to initialize json_string in gerrit.py to None here before we called sanitize_json_response.

175 def sanitize_json_response(json_string):
176 """Cleans up the JSON response got from the REST interface, in order to be
177 valid JSON.
178
179 :param json_string: the JSON string to clean up.
180 """
181 # The JSON response is not valid JSON, there is a magic sequence at the
182 # beginning of the result. We are paranoid, and remove also other white
183 # spaces.
184 if json_string.startswith(GERRIT_SANITIZABLE_VALUE):
185 return json_string.lstrip(GERRIT_SANITIZABLE_VALUE).strip()

Are we sure we that the json_string should always start with GERRIT_SANITIZABLE_VALUE ??
If not, then we need to return json_string even when json_string does not start with GERRIT_SANITIZABLE_VALUE.

review: Needs Fixing
Revision history for this message
Milo Casagrande (milo) wrote :

> Thanks for the fixes:
>
> 161 # The request in this case needs to be a GET.
> 162 json_string =
> sanitize_json_response(urllib2.urlopen(request).read())
> 163 return json_string
> It is probably good to initialize json_string in gerrit.py to None here
> before we called sanitize_json_response.

Sure, fixed.

> 175 def sanitize_json_response(json_string):
> 176 """Cleans up the JSON response got from the REST interface, in order
> to be
> 177 valid JSON.
> 178
> 179 :param json_string: the JSON string to clean up.
> 180 """
> 181 # The JSON response is not valid JSON, there is a magic sequence at
> the
> 182 # beginning of the result. We are paranoid, and remove also other
> white
> 183 # spaces.
> 184 if json_string.startswith(GERRIT_SANITIZABLE_VALUE):
> 185 return json_string.lstrip(GERRIT_SANITIZABLE_VALUE).strip()
>
> Are we sure we that the json_string should always start with
> GERRIT_SANITIZABLE_VALUE ??
> If not, then we need to return json_string even when json_string does not
> start with GERRIT_SANITIZABLE_VALUE.

As for Gerrit documentation, it should be. Link where it is written is here:
https://gerrit-review.googlesource.com/Documentation/rest-api.html

But better handle that case too.
Thanks.

366. By Milo Casagrande

Fixed review comments.

367. By Milo Casagrande

Return the json object, not the string.

Revision history for this message
Deepti B. Kalakeri (deeptik) wrote :

+1 Looks good.

review: Approve
368. By Milo Casagrande

Removed unused function.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'apps/patchmetrics/bin/sync-gerrit-changes.py'
2--- apps/patchmetrics/bin/sync-gerrit-changes.py 2012-01-23 18:41:51 +0000
3+++ apps/patchmetrics/bin/sync-gerrit-changes.py 2012-06-25 12:32:18 +0000
4@@ -6,9 +6,14 @@
5
6 from patchmetrics.models import TeamMembership
7 from patchmetrics.gerrit import (
8+ GerritError,
9+ create_or_update_patches_from_aosp_changes,
10 get_aosp_changes_from,
11- create_or_update_patches_from_aosp_changes,
12- GerritError)
13+ )
14+from patchmetrics.gerrit_json import (
15+ GERRIT_MORE_CHANGES,
16+ GERRIT_SORT_KEY,
17+)
18
19
20 linaro_engineers = Person.objects.filter(
21@@ -17,8 +22,8 @@
22 start_after = None
23 while True:
24 try:
25- result = get_aosp_changes_from(
26- person.email, start_after=start_after)
27+ results = get_aosp_changes_from(person.email,
28+ start_after=start_after)
29 except GerritError, e:
30 if '%s not found' % person.email in str(e):
31 # Gerrit returns an error when you search for changes owned by
32@@ -26,15 +31,17 @@
33 # just skip to the next email.
34 break
35 raise
36- at_end = result['atEnd']
37- changes = result['changes']
38+ # If we get all the changes in a single batch, the GERRIT_MORE_CHANGES
39+ # key will not be in the JSON output, so we set it to False.
40+ more_changes = results[-1].get(GERRIT_MORE_CHANGES) or False
41 created_or_updated_patches = (
42- create_or_update_patches_from_aosp_changes(changes, person))
43+ create_or_update_patches_from_aosp_changes(results, person))
44
45 # If there are less created/updated changes than the number of changes
46 # returned by Gerrit we know we can stop as they're sorted most
47 # recently updated first.
48- if len(changes) != len(created_or_updated_patches) or at_end:
49+ if len(results) != len(created_or_updated_patches) or \
50+ (not more_changes):
51 break
52
53- start_after = changes[-1]['sortKey']
54+ start_after = results[-1].get(GERRIT_SORT_KEY)
55
56=== modified file 'apps/patchmetrics/gerrit.py'
57--- apps/patchmetrics/gerrit.py 2012-01-31 18:32:44 +0000
58+++ apps/patchmetrics/gerrit.py 2012-06-25 12:32:18 +0000
59@@ -1,14 +1,37 @@
60 from datetime import datetime
61 import email
62 import urllib2
63+from urllib import urlencode
64
65 import json
66
67 from patchwork.models import Patch, Project, State
68 from patchmetrics.models import GerritChange
69+from patchmetrics.gerrit_json import (
70+ GERRIT_CREATED_KEY,
71+ GERRIT_ID_KEY,
72+ GERRIT_NUMBER_KEY,
73+ GERRIT_PROJECT_KEY,
74+ GERRIT_SANITIZABLE_VALUE,
75+ GERRIT_STATUS_KEY,
76+ GERRIT_SUBJECT_KEY,
77+ GERRIT_UPDATED_KEY,
78+)
79
80
81 ROOT_URL = 'https://android-review.googlesource.com/gerrit/rpc'
82+# This is the URL for the REST interface.
83+ROOT_REST_URL = 'https://android-review.googlesource.com'
84+# The default path for the REST interface.
85+CHANGES_QUERY_PATH = '/changes/'
86+# This is the query prefix used for gerrit REST interface.
87+GERRIT_QUERY_PREFIX = 'q='
88+# Separator for gerrit query parameters.
89+GERRIT_QUERY_SEPARATOR = '+'
90+# Default separator for URL query string.
91+QUERY_STRING_SEP = '&'
92+# Default character for starting a query string.
93+QUERY_STRING_START = '?'
94 # A mapping from AOSP states to our internal ones.
95 STATE_MAP = {'MERGED': 'Accepted', 'NEW': 'New', 'ABANDONED': 'Rejected'}
96
97@@ -36,32 +59,141 @@
98 return response.get('result')
99
100
101-def get_aosp_change_details(change_id):
102- """Get the details of the Gerrit change with the given ID."""
103- return jsonrpc_request(
104- '/ChangeDetailService',
105- 'changeDetail', [dict(id=change_id)])['change']
106-
107-
108-def get_aosp_changes_from(email, start_after=None, page_size=100):
109+def get_aosp_changes_from(email, start_after=None, page_size=25,
110+ return_format='JSON_COMPACT'):
111 """Get the list of changes authored by the given email address.
112
113- :param start_after: Only include changes whose sortKey is lower than
114- this. Use None if you want to start from the beginning.
115- :param page_size: Number of items to include in the list.
116- """
117- if start_after is None:
118- start_after = 'z'
119- query = "owner: %s" % email
120- return jsonrpc_request(
121- '/ChangeListService',
122- 'allQueryNext', [query, start_after, page_size])
123-
124-
125-def create_or_update_patches_from_aosp_changes(changes, author):
126- """Create or update Patch objects from the given list of changes.
127-
128- For each of the changes in the list, make sure there's a matching Patch
129+ :param email: The email of the user to get the changes of.
130+ :param start_after: Only include changes whose 'sortkey_after' is lower
131+ than this. Use None if you want to start from the
132+ beginning.
133+ :param page_size: Number of items to include in the list, defaults to 25.
134+ :param return_format: The format of the response, defaults to
135+ 'JSON_COMPACT' for JSON format without spaces, also
136+ possible to specify 'JSON'.
137+ """
138+ params = {'format': return_format, 'n': page_size}
139+ query_params = {'owner': email, 'sortkey_after': start_after}
140+ return rest_api_request(ROOT_REST_URL, CHANGES_QUERY_PATH, params,
141+ query_params)
142+
143+
144+def rest_api_request(url, path, params, query_params):
145+ """Calls the gerrit REST interface and return the JSON response.
146+
147+ :param url: the root URL of the web service.
148+ :param path: the path of the REST interface, after the URL.
149+ :param params: the parameters for the query, can be a dictionary or tuple.
150+ :param query_params: the parameters for the gerrit query interface, must
151+ be a dictionary.
152+ """
153+ request_url = create_request_query_url(url, path, params, query_params)
154+ return gerrit_request(request_url)
155+
156+
157+def create_request_query_url(url, path, params, query_params):
158+ """Creates the full query URL from the various elements.
159+
160+ :param url: the root URL of the web service, without path to the REST
161+ interface.
162+ :param path: the path of the REST interface, after the URL.
163+ :param params: the parameters for the query, can be a dictionary or tuple.
164+ :param query_params: the parameters for the gerrit query interface, must
165+ be a dictionary.
166+ """
167+ if not url.endswith('/') and not path.startswith('/'):
168+ url += '/'
169+ if not path.endswith('/'):
170+ path += '/'
171+ request_url = url + path + QUERY_STRING_START
172+
173+ request_url += urlencode(params)
174+ request_url += create_gerrit_rest_query(query_params)
175+
176+ return request_url
177+
178+
179+def create_gerrit_rest_query(query_params):
180+ """Creates the gerrit query based on the passed parametes.
181+
182+ The query result should be in the form of:
183+ &q=key:value+key:value[+key:value ...]
184+
185+ :param query_params: a dictionary containing the (key,value) pairs that
186+ will be used for the query.
187+ """
188+ if not isinstance(query_params, dict):
189+ raise TypeError("'query_params' must be of type 'dict', got '%s'" %
190+ type(query_params).__name__)
191+
192+ params_list = []
193+ # Where we will store the query.
194+ gerrit_query = ''
195+ for key, value in query_params.iteritems():
196+ # We cannot have None values here, we just skip them.
197+ if value is not None:
198+ params_list.append(":".join((key, value)))
199+ # The resulting query string, without separators or prefixes, should be in
200+ # the form of:
201+ # key:value+key:value
202+ # This is a normal query, more complex queries need to be thought of, since
203+ # we can express also OR, AND and NOT operators. By default it is an AND.
204+ if params_list:
205+ gerrit_query = QUERY_STRING_SEP + GERRIT_QUERY_PREFIX + \
206+ GERRIT_QUERY_SEPARATOR.join(params_list)
207+
208+ return gerrit_query
209+
210+
211+def gerrit_request(request_url):
212+ """Performs the REST call, opening the connection to the web service.
213+
214+ :param request_url: the full URL of the request to be performed.
215+ """
216+ json_object = None
217+ try:
218+ request = urllib2.Request(request_url)
219+ # Looks like, starting with gerrit version 2.5, we need to do this to
220+ # talk with it. More info here:
221+ # https://groups.google.com/group/repo-discuss/msg/78e70b07ad1aacce
222+ # https://gerrit-review.googlesource.com/Documentation/rest-api.html
223+ request.add_header("Accept", "application/json")
224+ request.add_header("Accept-Encoding", "gzip")
225+ # The request in this case needs to be a GET.
226+ json_string = sanitize_json_response(urllib2.urlopen(request).read())
227+ json_object = json.loads(json_string)
228+ except urllib2.HTTPError, ex:
229+ # If we get back an error code 400, it probably means that what we are
230+ # looking for does not exist. We need to make sure of that and handle
231+ # it. As it is now, we raise the same expected exception with the same
232+ # message as before.
233+ if ex.code == 400:
234+ raise GerritError(ex.read())
235+ else:
236+ raise(ex)
237+
238+ return json_object
239+
240+
241+def sanitize_json_response(json_string):
242+ """Cleans up the JSON response got from the REST interface, in order to be
243+ valid JSON.
244+
245+ :param json_string: the JSON string to clean up.
246+ """
247+ # The JSON response is not valid JSON, there is a magic sequence at the
248+ # beginning of the result. We are paranoid, and remove also other white
249+ # spaces.
250+ valid_json = json_string
251+ if json_string.startswith(GERRIT_SANITIZABLE_VALUE):
252+ valid_json = json_string.lstrip(GERRIT_SANITIZABLE_VALUE)
253+ return valid_json.strip()
254+
255+
256+def create_or_update_patches_from_aosp_changes(results, author):
257+ """Create or update Patch objects from the given list of results.
258+
259+ For each of the result in the list, make sure there's a matching Patch
260 (and GerritChange) object in our DB matching it and update it to be in
261 sync with the remote change.
262
263@@ -69,23 +201,22 @@
264 """
265 print "Checking changes submitted by %s ..." % author.email
266 updated_changes = []
267- for change in changes:
268- gerrit_id = change['id']['id']
269- last_updated_on = datetime.strptime(
270- change['lastUpdatedOn'], '%Y-%m-%d %H:%M:%S.%f000')
271+ for result in results:
272+ gerrit_id = result.get(GERRIT_NUMBER_KEY)
273+ last_updated_on = datetime.strptime(result.get(GERRIT_UPDATED_KEY),
274+ '%Y-%m-%d %H:%M:%S.%f000')
275 try:
276 existing = GerritChange.objects.get(gerrit_id=gerrit_id)
277 except GerritChange.DoesNotExist:
278 existing = None
279- # Must get the change's details because we need their creation date.
280- details = get_aosp_change_details(gerrit_id)
281- date_created = datetime.strptime(
282- details['createdOn'], '%Y-%m-%d %H:%M:%S.%f000')
283+ date_created = datetime.strptime(result.get(GERRIT_CREATED_KEY),
284+ '%Y-%m-%d %H:%M:%S.%f000')
285 change_dict = dict(
286- id=gerrit_id, project=change['project']['key']['name'],
287- last_updated_on=last_updated_on, status=change['status'],
288- subject=change['subject'], date_created=date_created,
289- change_id=change['key']['id'])
290+ id=gerrit_id, project=result.get(GERRIT_PROJECT_KEY),
291+ last_updated_on=last_updated_on,
292+ status=result.get(GERRIT_STATUS_KEY),
293+ subject=result.get(GERRIT_SUBJECT_KEY), date_created=date_created,
294+ change_id=result.get(GERRIT_ID_KEY))
295 if existing is None:
296 create_patch_from_aosp_change(change_dict, author)
297 updated_changes.append(gerrit_id)
298
299=== added file 'apps/patchmetrics/gerrit_json.py'
300--- apps/patchmetrics/gerrit_json.py 1970-01-01 00:00:00 +0000
301+++ apps/patchmetrics/gerrit_json.py 2012-06-25 12:32:18 +0000
302@@ -0,0 +1,20 @@
303+# File with all the gerrit JSON valid keys.
304+# For more information about the values defined here, look:
305+# https://gerrit-review.googlesource.com/Documentation/json.html
306+# Also, on bug https://bugs.launchpad.net/linaro-patchmetrics/+bug/1013577,
307+# there is a sample JSON output from Gerrit new REST API.
308+GERRIT_CREATED_KEY = 'created'
309+GERRIT_ID_KEY = 'id'
310+GERRIT_MORE_CHANGES = '_more_changes'
311+GERRIT_NUMBER_KEY = '_number'
312+GERRIT_OWNER_KEY = 'owner'
313+GERRIT_OWNER_NAME_KEY = 'name'
314+GERRIT_SORT_KEY = '_sortkey'
315+GERRIT_UPDATED_KEY = 'updated'
316+GERRIT_STATUS_KEY = 'status'
317+GERRIT_PROJECT_KEY = 'project'
318+GERRIT_SUBJECT_KEY = 'subject'
319+# Gerrit JSON output starts with a magic prefix, making it not valid JSON.
320+# For more info:
321+# https://gerrit-review.googlesource.com/Documentation/rest-api.html
322+GERRIT_SANITIZABLE_VALUE = ")]}'"
323
324=== modified file 'apps/patchmetrics/tests/test_gerrit.py'
325--- apps/patchmetrics/tests/test_gerrit.py 2011-07-19 11:56:25 +0000
326+++ apps/patchmetrics/tests/test_gerrit.py 2012-06-25 12:32:18 +0000
327@@ -4,10 +4,16 @@
328 from django.test import TestCase
329
330 from patchmetrics.gerrit import (
331+ CHANGES_QUERY_PATH,
332+ QUERY_STRING_START,
333+ ROOT_REST_URL,
334+ create_gerrit_rest_query,
335 create_patch_from_aosp_change,
336+ create_request_query_url,
337 get_project_for_aosp_name,
338 get_project_name_for_aosp_name,
339- update_local_gerrit_change_from_aosp_change)
340+ update_local_gerrit_change_from_aosp_change,
341+ )
342 from patchmetrics.tests.factory import factory
343
344
345@@ -50,7 +56,6 @@
346 # date_last_state_change set to last_updated_on.
347 change_dict = copy(self.change_dict)
348 change_dict['status'] = 'MERGED'
349- author = factory.makePerson()
350 patch = create_patch_from_aosp_change(
351 change_dict, factory.makePerson())
352 self.assertEqual('Accepted', patch.state.name)
353@@ -67,3 +72,41 @@
354 self.assertEqual('MERGED', gerrit_change.status)
355 self.assertEqual('Accepted', gerrit_change.patch.state.name)
356 self.assertEqual(change_dict['subject'], gerrit_change.patch.name)
357+
358+ def test_create_gerrit_rest_query(self):
359+ """Tests the creation of a valid gerrit query."""
360+ query_params = {'key1': 'value1', 'key2': 'value2'}
361+ self.assertEqual('&q=key2:value2+key1:value1',
362+ create_gerrit_rest_query(query_params))
363+
364+ def test_create_gerrit_rest_query_empty(self):
365+ """Tests gerrit query creation with empty dictionary."""
366+ self.assertEqual('', create_gerrit_rest_query({}))
367+
368+ def test_create_gerrit_rest_query_with_none(self):
369+ """Tests that if a value is None it is not added."""
370+ query_params = {'key1': 'value1', 'key2': None}
371+ self.assertEqual('&q=key1:value1',
372+ create_gerrit_rest_query(query_params))
373+
374+ def test_create_gerrit_request_query_with_list(self):
375+ """Tests that the function accepts only a dictionary, passing a list.
376+ """
377+ self.assertRaises(TypeError, create_gerrit_rest_query, [])
378+
379+ def test_create_gerrit_request_query_with_tuple(self):
380+ """Tests that the function accepts only a dictionary, passing a tuple.
381+ """
382+ self.assertRaises(TypeError, create_gerrit_rest_query, ())
383+
384+ def test_create_request_query_url(self):
385+ """Tests the creation of a valid request URL."""
386+ params = {'format': 'JSON', 'n': 100}
387+ query_params = {'owner': 'person@example.org', 'sortkey_after': 'abcd'}
388+ expected_result = ROOT_REST_URL + CHANGES_QUERY_PATH + \
389+ QUERY_STRING_START + \
390+ "n=100&format=JSON&q=owner:person@example.org+sortkey_after:abcd"
391+ self.assertEquals(expected_result,
392+ create_request_query_url(ROOT_REST_URL,
393+ CHANGES_QUERY_PATH,
394+ params, query_params))

Subscribers

People subscribed via source and target branches