Merge lp:~dooferlad/linaro-ci-cli/get-command into lp:linaro-ci-cli
- get-command
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Milo Casagrande |
Approved revision: | 31 |
Merged at revision: | 18 |
Proposed branch: | lp:~dooferlad/linaro-ci-cli/get-command |
Merge into: | lp:linaro-ci-cli |
Diff against target: |
754 lines (+576/-33) 8 files modified
commands/add.py (+3/-2) commands/base_command.py (+30/-10) commands/get.py (+135/-6) commands/status.py (+48/-9) commands/tests/test_add.py (+1/-1) commands/tests/test_get.py (+347/-0) commands/tests/test_utilities.py (+6/-0) lci (+6/-5) |
To merge this branch: | bzr merge lp:~dooferlad/linaro-ci-cli/get-command |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Milo Casagrande (community) | Approve | ||
Review via email: mp+150549@code.launchpad.net |
Commit message
Description of the change
Added bulk of CLI get command.
lci get
- get status of all runs
lci get <job name>
- get status of all runs of job
lci get <job name> <build number>
- get status of single run. Includes listing of artefacts.
Once you have identified a build that you are interested in you can get 1/all files and the console output:
lci get <job name> <build number> --get-file <file name>
lci get <job name> <build number> --get-all-files
lci get <job name> <build number> --console
Paul Sokolovsky (pfalcon) wrote : | # |
Paul Sokolovsky (pfalcon) wrote : | # |
135 + if parameters[
138 + if parameters[
141 + if parameters[
Also, I'm all for useful and descriptive messages, but do we need to get *that* detailed? ("Unable to execute operation for more than one job/build" sounds pretty clear either.)
Paul Sokolovsky (pfalcon) wrote : | # |
> if(len(
PEP8 beeps.
Paul Sokolovsky (pfalcon) wrote : | # |
> +import simplejson
Why not standard json module?
- 25. By James Tunnicliffe
-
Monkey patch older requests libraries so they have a json function.
- 26. By James Tunnicliffe
-
Some code cleanups and added help text
- 27. By James Tunnicliffe
-
Fixed up get matching more than 1 build but no output requested
- 28. By James Tunnicliffe
-
Updated add command parameters to be more clear
- 29. By James Tunnicliffe
-
Improved status command
- 30. By James Tunnicliffe
-
Fixed up currnt/previous prints from status command
- 31. By James Tunnicliffe
-
"Cleaned up get Command.
_download_ file code -- moved prints out into run function where there was already sensible places to put them
Fixed up tests.
Clean error when unable to contact server."
Made status command have a more useful output.
James Tunnicliffe (dooferlad) wrote : | # |
Have addressed points brought up in review. There are a couple of
small changes made for the demo at Connect in this branch now as well
(commands/status.py has prettier printing, commands/
catches connection errors and prints a user friendly message). I can
split them out into another merge if you like.
Milo Casagrande (milo) wrote : | # |
Hey James,
forgot to update this. Looks good to go.
Thanks.
Preview Diff
1 | === modified file 'commands/add.py' |
2 | --- commands/add.py 2013-02-11 15:21:35 +0000 |
3 | +++ commands/add.py 2013-03-11 13:41:20 +0000 |
4 | @@ -46,8 +46,10 @@ |
5 | # [--run] |
6 | parser = argparse.ArgumentParser(description='Linaro CI: Add task.', |
7 | prog=self.program_name) |
8 | - parser.add_argument('vcs_url', type=str, |
9 | + parser.add_argument('--vcs-url', type=str, required=True, |
10 | help='URL to check out test configuration from.') |
11 | + parser.add_argument('--name', type=str, required=True, |
12 | + help='Name of job') |
13 | parser.add_argument('--file-name', type=str, |
14 | default=defaults.FILE_NAME, |
15 | help='Name of the configuration file.' |
16 | @@ -58,7 +60,6 @@ |
17 | 'Default is %s' % defaults.CLASS_NAME) |
18 | parser.add_argument('--run', action='store_true', |
19 | help='Enqueue job immediately') |
20 | - parser.add_argument('--name', type=str) |
21 | |
22 | args = parser.parse_args(parameters) |
23 | return vars(args) |
24 | |
25 | === modified file 'commands/base_command.py' |
26 | --- commands/base_command.py 2013-02-20 10:16:50 +0000 |
27 | +++ commands/base_command.py 2013-03-11 13:41:20 +0000 |
28 | @@ -21,7 +21,7 @@ |
29 | import requests |
30 | import sys |
31 | from urlparse import urljoin |
32 | - |
33 | +import json |
34 | |
35 | class BaseCommand(object): |
36 | def __init__(self, settings, name): |
37 | @@ -46,9 +46,23 @@ |
38 | |
39 | return url, params |
40 | |
41 | + def _json(self): |
42 | + return self.json_data |
43 | + |
44 | def get(self, url, in_params=None): |
45 | url, params = self._prep_request(url, in_params) |
46 | - return requests.get(url, params=params) |
47 | + try: |
48 | + request = requests.get(url, params=params) |
49 | + except requests.exceptions.ConnectionError: |
50 | + print >> sys.stderr, "Unable to connect to server." |
51 | + exit(1) |
52 | + |
53 | + # Monkey patch older requests objects so they have a json function. |
54 | + if not hasattr(request, 'json'): |
55 | + self.json_data = json.loads(request.content) |
56 | + setattr(request, 'json', self._json) |
57 | + |
58 | + return request |
59 | |
60 | def post(self, url, data, in_params=None): |
61 | url, params = self._prep_request(url, in_params) |
62 | @@ -72,20 +86,26 @@ |
63 | |
64 | |
65 | class CommandFailed(Exception): |
66 | - def __init__(self, command, params, response): |
67 | + def __init__(self, command, params=None, response=None, message=None): |
68 | self.command = command |
69 | self.params = params |
70 | self.response = response |
71 | + self.message = message |
72 | |
73 | def __str__(self): |
74 | - message = "An error occurred when executing the command %s" %\ |
75 | + message = "An error occurred when executing the command %s: " %\ |
76 | self.command |
77 | |
78 | - message += "\nURL %s\nReason %s\nHeaders %s\n\n%s" % ( |
79 | - self.response.url, |
80 | - self.response.reason, |
81 | - self.response.headers, |
82 | - self.response.text.decode("string-escape"), |
83 | - ) |
84 | + if self.message: |
85 | + message += self.message |
86 | + |
87 | + else: |
88 | + if self.response: |
89 | + message += "\nURL %s\nReason %s\nHeaders %s\n\n%s" % ( |
90 | + self.response.url, |
91 | + self.response.reason, |
92 | + self.response.headers, |
93 | + self.response.text.decode("string-escape"), |
94 | + ) |
95 | |
96 | return message |
97 | |
98 | === modified file 'commands/get.py' |
99 | --- commands/get.py 2013-02-07 12:19:48 +0000 |
100 | +++ commands/get.py 2013-03-11 13:41:20 +0000 |
101 | @@ -18,32 +18,161 @@ |
102 | # along with Linaro Image Tools. If not, see <http://www.gnu.org/licenses/>. |
103 | |
104 | import requests |
105 | +import re |
106 | from base_command import BaseCommandNeedsAuth, CommandFailed |
107 | import argparse |
108 | +import shutil |
109 | +import urllib2 |
110 | |
111 | """ |
112 | Defines the lci get command |
113 | """ |
114 | |
115 | + |
116 | class Command(BaseCommandNeedsAuth): |
117 | + def _download_file(self, file_name, job_data, output_file_name=None): |
118 | + """Download the requested file""" |
119 | + if not output_file_name: |
120 | + output_file_name = file_name |
121 | + |
122 | + if file_name in job_data["get_build_result"]: |
123 | + url = job_data["get_build_result"][file_name] |
124 | + |
125 | + # Use urllib2 here, not requests because the version of requests |
126 | + # that comes with Ubuntu 12.04 doesn't have the ability to download |
127 | + # a request in small chunks. This urllib2 call avoids downloading |
128 | + # the entire file into memory before writing it out to disk. |
129 | + req = urllib2.urlopen(url) |
130 | + with open(output_file_name, 'wb') as fp: |
131 | + shutil.copyfileobj(req, fp) |
132 | + else: |
133 | + raise CommandFailed("get", "Requested file not found.") |
134 | + |
135 | def run(self, args): |
136 | parameters = self._args_to_params(args) |
137 | |
138 | - print "Get output of task with params %s" % parameters |
139 | - |
140 | - r = self.get("get", parameters) |
141 | + r = self.get("runs", parameters) |
142 | |
143 | if r.status_code != requests.codes.ok: |
144 | raise CommandFailed(self.name, parameters, r) |
145 | |
146 | + if len(r.json()["objects"]) == 0 and parameters["loop__name"]: |
147 | + # We searched for something and didn't find it. |
148 | + raise CommandFailed(self.name, message="job or build not found") |
149 | + |
150 | + if len(r.json()["objects"]) == 1: |
151 | + # If user has requested data from a single job fetch it and exit. |
152 | + # Note that if a job has a single build, you don't need to specify |
153 | + # the build number - there is enough identifying information to |
154 | + # use these commands. |
155 | + job_data = r.json()["objects"][0] |
156 | + done_something = False |
157 | + |
158 | + if parameters["get_file"]: |
159 | + print "Fetching", parameters["get_file"] |
160 | + self._download_file(parameters["get_file"], job_data) |
161 | + done_something = True |
162 | + |
163 | + if parameters["get_all_files"]: |
164 | + file_name = "%s_%s.zip" % (job_data["name"], |
165 | + job_data["build_number"]) |
166 | + print "Fetching all files. Storing in", file_name |
167 | + self._download_file("*zip*/archive.zip", job_data, file_name) |
168 | + done_something = True |
169 | + |
170 | + if parameters["console"]: |
171 | + file_name = "%s_%s.txt" % (job_data["name"], |
172 | + job_data["build_number"]) |
173 | + print ("Fetching console output (will be stored as %s)." % |
174 | + (file_name)) |
175 | + self._download_file("*console_url*", job_data, file_name) |
176 | + |
177 | + # now print out the console |
178 | + with open(file_name) as console: |
179 | + for line in console.readlines(): |
180 | + print line, |
181 | + done_something = True |
182 | + |
183 | + if done_something: |
184 | + # Now exit. Don't print job data when downloading. |
185 | + return |
186 | + |
187 | + elif (parameters["get_file"] or |
188 | + parameters["get_all_files"] or |
189 | + parameters["console"]): |
190 | + # Error handling. If user has requested a file(s)/console but not |
191 | + # specified a build number this is an error. |
192 | + build_or_job = "build" if "build_number" in parameters else "job" |
193 | + raise CommandFailed(self.name, |
194 | + message="Found more than one result for this" |
195 | + " %s. Unable to fetch requested output." |
196 | + % (build_or_job)) |
197 | + |
198 | + # If we have got this far the user has not requested a file(s) or |
199 | + # console output so print a sensible amount of job status information. |
200 | + sorted_jobs = sorted(r.json()["objects"], |
201 | + key=lambda job_data: |
202 | + job_data["name"] + str(job_data["build_number"])) |
203 | + |
204 | + if not sorted_jobs: |
205 | + print "No jobs found" |
206 | + return |
207 | + |
208 | + # Only print list of artifacts if we have identified a single build, |
209 | + # else just print build number and status. |
210 | + if parameters["loop__name"]: |
211 | + data_format = "{build_number:12d} | {status:>8s}" |
212 | + else: |
213 | + data_format = "{name:20s} | {build_number:12d} | {status:>8s}" |
214 | + |
215 | + headings = { |
216 | + "name": "Job Name", |
217 | + "build_number": "Build Number", |
218 | + "status": "Status" |
219 | + } |
220 | + |
221 | + blank = {"name": "", "build_number": "", "status": ""} |
222 | + heading_format = re.sub("d}", "s}", data_format) |
223 | + |
224 | + if len(r.json()["objects"]) == 1: |
225 | + print heading_format.format(**headings) + " | Artifacts" |
226 | + else: |
227 | + print heading_format.format(**headings) |
228 | + |
229 | + for job_data in sorted_jobs: |
230 | + line = data_format.format(**job_data) |
231 | + |
232 | + # If user has requested a single build, pretty print artifact list. |
233 | + if (len(job_data["list_artifacts"]) and |
234 | + len(r.json()["objects"]) == 1): |
235 | + # Print first file name on the end of the job info line |
236 | + print line + " | " + job_data["list_artifacts"][0] |
237 | + |
238 | + # Print remainder of file names 1 per line. |
239 | + for file_name in job_data["list_artifacts"][1:]: |
240 | + print heading_format.format(**blank) +\ |
241 | + " | " + file_name |
242 | + else: |
243 | + print line |
244 | + |
245 | def _args_to_params(self, parameters): |
246 | # lci get <job ID> [--console] |
247 | parser = argparse.ArgumentParser( |
248 | description='Linaro CI: get task output.', |
249 | prog=self.program_name,) |
250 | - parser.add_argument('job_id', type=str, help='Job ID.') |
251 | + parser.add_argument('name', nargs='?', type=str, help='Job name') |
252 | + parser.add_argument('build_number', nargs='?', type=int, |
253 | + help='Build number') |
254 | parser.add_argument('--console', action='store_true', |
255 | help='Enqueue job immediately') |
256 | - |
257 | + parser.add_argument('--get-file', type=str, |
258 | + help='Name of file to download') |
259 | + parser.add_argument('--get-all-files', action='store_true', |
260 | + help='Download all files in a zip.') |
261 | args = parser.parse_args(parameters) |
262 | - return vars(args) |
263 | + args = vars(args) |
264 | + |
265 | + # Rename "name" to "loop__name" to match the filter format |
266 | + args["loop__name"] = args["name"] |
267 | + del(args["name"]) |
268 | + return args |
269 | |
270 | === modified file 'commands/status.py' |
271 | --- commands/status.py 2013-02-07 12:19:48 +0000 |
272 | +++ commands/status.py 2013-03-11 13:41:20 +0000 |
273 | @@ -20,7 +20,7 @@ |
274 | import requests |
275 | from base_command import BaseCommandNeedsAuth, CommandFailed |
276 | import argparse |
277 | -import sys, os |
278 | +import re |
279 | |
280 | """ |
281 | Defines the lci status command |
282 | @@ -29,20 +29,59 @@ |
283 | class Command(BaseCommandNeedsAuth): |
284 | def run(self, args): |
285 | parameters = self._args_to_params(args) |
286 | - r = self.get("loops", parameters) |
287 | - |
288 | - if r.status_code != requests.codes.ok: |
289 | - raise CommandFailed(self.name, parameters, r) |
290 | - |
291 | - print "Status of %s" % parameters |
292 | - print r.json() |
293 | + |
294 | + jobs = self.get("loops", parameters) |
295 | + if jobs.status_code != requests.codes.ok: |
296 | + raise CommandFailed(self.name, parameters, jobs) |
297 | + jobs = jobs.json()["objects"] |
298 | + |
299 | + runs = self.get("runs", parameters) |
300 | + if runs.status_code != requests.codes.ok: |
301 | + raise CommandFailed(self.name, parameters, runs) |
302 | + runs = runs.json()["objects"] |
303 | + |
304 | + data_format = "{name:20s} | {builds:12d} | {status:>8s}" |
305 | + headings = { |
306 | + "name": "Job Name", |
307 | + "builds": "Builds", |
308 | + "status": "Status" |
309 | + } |
310 | + |
311 | + heading_format = re.sub("d}", "s}", data_format) |
312 | + |
313 | + print heading_format.format(**headings) |
314 | + |
315 | + for job in jobs: |
316 | + name = job["name"] |
317 | + |
318 | + builds = 0 |
319 | + status = "not run" |
320 | + old_status = "" |
321 | + for run in runs: |
322 | + if run["name"] == name: |
323 | + if run["build_number"] > builds: |
324 | + builds = run["build_number"] |
325 | + status = run["status"] |
326 | + elif run["build_number"] == builds - 1: |
327 | + old_status = run["status"] |
328 | + |
329 | + if status in ["unscheduled", "scheduled", "running"]: |
330 | + status = old_status + " (build " + status + ")" |
331 | + |
332 | + data = { |
333 | + "name": name, |
334 | + "builds": builds, |
335 | + "status": status |
336 | + } |
337 | + |
338 | + print data_format.format(**data) |
339 | |
340 | def _args_to_params(self, parameters): |
341 | # lci status <job ID> [--group] %s [--user] %s [--job-name] %s |
342 | parser = argparse.ArgumentParser( |
343 | prog=self.program_name, |
344 | description='Linaro CI: get task output.') |
345 | - parser.add_argument('--job-id', type=str, help='Job ID.') |
346 | + parser.add_argument('--name', type=str, help='Job Name.') |
347 | parser.add_argument('--user', type=str, help='User owning job.') |
348 | parser.add_argument('--group', type=str, help='Group owning job.') |
349 | parser.add_argument('--job-name', type=str, help='Job name.') |
350 | |
351 | === modified file 'commands/tests/test_add.py' |
352 | --- commands/tests/test_add.py 2013-02-11 16:13:12 +0000 |
353 | +++ commands/tests/test_add.py 2013-03-11 13:41:20 +0000 |
354 | @@ -63,7 +63,7 @@ |
355 | |
356 | # Now we run an add command with the following arguments... |
357 | args = [ |
358 | - "vcsurl", |
359 | + "--vcs-url", "vcsurl", |
360 | "--name", "jobname", |
361 | ] |
362 | |
363 | |
364 | === added file 'commands/tests/test_get.py' |
365 | --- commands/tests/test_get.py 1970-01-01 00:00:00 +0000 |
366 | +++ commands/tests/test_get.py 2013-03-11 13:41:20 +0000 |
367 | @@ -0,0 +1,347 @@ |
368 | +# Copyright (C) 2013 Linaro |
369 | +# |
370 | +# Author: James Tunnicliffe <james.tunnicliffe@linaro.org> |
371 | +# |
372 | +# This file is part of Linaro CI CLI. |
373 | +# |
374 | +# Linaro CI CLI is free software: you can redistribute it and/or modify |
375 | +# it under the terms of the GNU General Public License as published by |
376 | +# the Free Software Foundation, either version 3 of the License, or |
377 | +# (at your option) any later version. |
378 | +# |
379 | +# Linaro CI CLI is distributed in the hope that it will be useful, |
380 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
381 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
382 | +# GNU General Public License for more details. |
383 | +# |
384 | +# You should have received a copy of the GNU General Public License |
385 | +# along with Linaro Image Tools. If not, see <http://www.gnu.org/licenses/>. |
386 | + |
387 | +from copy import copy |
388 | +import unittest |
389 | +from commands.defaults import ( |
390 | + CI_SERVER, |
391 | + API_PREFIX, |
392 | + ) |
393 | +import commands.login |
394 | +import commands.get |
395 | +from mock import patch, MagicMock |
396 | +from test_utilities import FakeRequest, expectedUrl, NetworkingMockMixin |
397 | +from StringIO import StringIO |
398 | +import re |
399 | + |
400 | + |
401 | +class TestGetCommand(unittest.TestCase, NetworkingMockMixin): |
402 | + def setUp(self): |
403 | + # Monkey patch the raw_input function imported by login |
404 | + self.fake_input = self.FakeRawInputLogin() |
405 | + commands.login.raw_input = self.fake_input.raw_input_response |
406 | + |
407 | + self.login_details = {"username": "me", |
408 | + "api_key": "12qwaszx"} |
409 | + self.server_details = {"ci_server": CI_SERVER, |
410 | + "api_prefix": API_PREFIX} |
411 | + self.settings = copy(self.server_details) |
412 | + self.settings.update(self.login_details) |
413 | + |
414 | + self.data_format = "{build_number:12d} | {status:>8s}" |
415 | + self.data_format_with_name = "{name:20s} | " + self.data_format |
416 | + self.heading_format = re.sub("d}", "s}", self.data_format) |
417 | + self.heading_format_with_name = "{name:20s} | " + self.heading_format |
418 | + self.headings = { |
419 | + "name": "Job Name", |
420 | + "build_number": "Build Number", |
421 | + "status": "Status" |
422 | + } |
423 | + self.empty_line = { |
424 | + "name": "", |
425 | + "build_number": "", |
426 | + "status": "" |
427 | + } |
428 | + |
429 | + |
430 | + def tearDown(self): |
431 | + # Restore the original raw_input |
432 | + commands.login.raw_input = raw_input |
433 | + |
434 | + def _login(self, mock_requests_get): |
435 | + # get command requires authentication so login is triggered. We pass an |
436 | + # empty username and api_key, forcing a prompt for those values. |
437 | + get_cmd = commands.get.Command(copy(self.settings), "test_logged_in") |
438 | + |
439 | + self.assertEqual(expectedUrl("login_test"), |
440 | + mock_requests_get.call_args[0][0]) |
441 | + self.assertDictContainsSubset(self.login_details, |
442 | + mock_requests_get.call_args[1]["params"]) |
443 | + |
444 | + return get_cmd |
445 | + |
446 | + def _check_call_params(self, mock_requests_get): |
447 | + self.assertEqual(expectedUrl("runs"), |
448 | + mock_requests_get.call_args[0][0]) |
449 | + self.assertDictContainsSubset(self.login_details, |
450 | + mock_requests_get.call_args[1]["params"]) |
451 | + |
452 | + def _heading_line(self): |
453 | + if self.args: |
454 | + heading = self.heading_format.format(**self.headings) |
455 | + else: |
456 | + heading = self.heading_format_with_name.format(**self.headings) |
457 | + |
458 | + if len(self.job_data["objects"]) == 1: |
459 | + heading += " | Artifacts" |
460 | + |
461 | + return heading |
462 | + |
463 | + def _data_lines(self): |
464 | + lines = [] |
465 | + sorted_jobs = sorted(self.job_data["objects"], |
466 | + key=lambda job_data: |
467 | + job_data["name"] + str(job_data["build_number"])) |
468 | + for data in sorted_jobs: |
469 | + if self.args: |
470 | + line_format = self.data_format |
471 | + heading_format = self.heading_format |
472 | + else: |
473 | + line_format = self.data_format_with_name |
474 | + heading_format = self.heading_format_with_name |
475 | + |
476 | + lines.append(line_format.format(**data)) |
477 | + if len(self.job_data["objects"]) == 1: |
478 | + if "list_artifacts" in data: |
479 | + lines[-1] += " | " + data["list_artifacts"][0] |
480 | + |
481 | + if len(data["list_artifacts"]) > 1: |
482 | + for file_name in data["list_artifacts"][1:]: |
483 | + lines.append( |
484 | + heading_format.format(**self.empty_line) + |
485 | + " | " + file_name) |
486 | + |
487 | + return lines |
488 | + |
489 | + @property |
490 | + def expected(self): |
491 | + return [self._heading_line()] + self._data_lines() |
492 | + |
493 | + @patch('sys.stdout') |
494 | + @patch('requests.get') |
495 | + def test_get_empty_job_list(self, mock_requests_get, mock_stdout): |
496 | + my_stdout = StringIO() |
497 | + mock_stdout.write = my_stdout.write |
498 | + mock_requests_get.return_value = FakeRequest(200) |
499 | + mock_requests_get.return_value.set_json({ |
500 | + "objects": [] |
501 | + }) |
502 | + |
503 | + # Login, get the command object |
504 | + get_cmd = self._login(mock_requests_get) |
505 | + |
506 | + # Now we run a get command with the following arguments... |
507 | + args = [] |
508 | + get_cmd.run(args) |
509 | + |
510 | + # Check that the URLs used in the REST query look sane |
511 | + self._check_call_params(mock_requests_get) |
512 | + |
513 | + # Check command output |
514 | + self.assertEqual("No jobs found\n", my_stdout.getvalue()) |
515 | + |
516 | + @patch('sys.stdout') |
517 | + @patch('requests.get') |
518 | + def _test_get(self, mock_requests_get, mock_stdout): |
519 | + my_stdout = StringIO() |
520 | + mock_stdout.write = my_stdout.write |
521 | + mock_requests_get.return_value = FakeRequest(200) |
522 | + mock_requests_get.return_value.set_json(self.job_data) |
523 | + |
524 | + # Login, get the command object |
525 | + get_cmd = self._login(mock_requests_get) |
526 | + |
527 | + # Now we run a get command with the following arguments... |
528 | + get_cmd.run(self.args) |
529 | + |
530 | + # Check that the URLs used in the REST query look sane |
531 | + self._check_call_params(mock_requests_get) |
532 | + |
533 | + # Check command output |
534 | + printed = my_stdout.getvalue().splitlines() |
535 | + self.assertEqual(self.expected, printed) |
536 | + |
537 | + def test_get_single_job_single_run(self): |
538 | + self.job_data = { |
539 | + "objects": [ |
540 | + { |
541 | + "name": "a name", |
542 | + "build_number": 1, |
543 | + "status": "passed", |
544 | + "list_artifacts": ["a_file"] |
545 | + } |
546 | + ] |
547 | + } |
548 | + |
549 | + self.args = [] |
550 | + self._test_get() |
551 | + |
552 | + def test_get_single_job_single_run_many_files(self): |
553 | + self.job_data = { |
554 | + "objects": [ |
555 | + { |
556 | + "name": "a name", |
557 | + "build_number": 1, |
558 | + "status": "passed", |
559 | + "list_artifacts": ["a_file", "AnotherFile.txt", ".foo"] |
560 | + } |
561 | + ] |
562 | + } |
563 | + |
564 | + self.args = [] |
565 | + self._test_get() |
566 | + |
567 | + def test_get_single_job_two_runs(self): |
568 | + self.job_data = { |
569 | + "objects": [ |
570 | + { |
571 | + "name": "a name", |
572 | + "build_number": 1, |
573 | + "status": "passed", |
574 | + "list_artifacts": ["a_file"] |
575 | + }, |
576 | + { |
577 | + "name": "a name", |
578 | + "build_number": 2, |
579 | + "status": "passed", |
580 | + "list_artifacts": ["a_file"] |
581 | + } |
582 | + ] |
583 | + } |
584 | + |
585 | + self.args = [] |
586 | + self._test_get() |
587 | + |
588 | + def test_get_two_jobs_two_runs(self): |
589 | + self.job_data = { |
590 | + "objects": [ |
591 | + { |
592 | + "name": "a job", |
593 | + "build_number": 1, |
594 | + "status": "passed", |
595 | + "list_artifacts": ["a_file"] |
596 | + }, |
597 | + { |
598 | + "name": "another job", |
599 | + "build_number": 1, |
600 | + "status": "passed", |
601 | + "list_artifacts": ["a_file"] |
602 | + } |
603 | + ] |
604 | + } |
605 | + |
606 | + self.args = [] |
607 | + self._test_get() |
608 | + |
609 | + def test_get_many_jobs_many_runs(self): |
610 | + self.job_data = { |
611 | + "objects": [] |
612 | + } |
613 | + |
614 | + for job_name in ["job1", "job2", "another job", "YetAnother Job!"]: |
615 | + for counter in range(1, 30): |
616 | + self.job_data["objects"].append({ |
617 | + "name": job_name, |
618 | + "build_number": counter, |
619 | + "status": "passed", |
620 | + "list_artifacts": ["a_file", "SomeFile.txt", ".hide"] |
621 | + }) |
622 | + |
623 | + self.args = [] |
624 | + self._test_get() |
625 | + |
626 | + @patch('sys.stdout') |
627 | + @patch('requests.get') |
628 | + @patch('urllib2.urlopen') |
629 | + @patch('shutil.copyfileobj') |
630 | + def _test_download_file(self, expected_file, expected_url, |
631 | + copyfileobj, urlopen, mock_requests_get, mock_stdout): |
632 | + |
633 | + my_stdout = StringIO() |
634 | + mock_stdout.write = my_stdout.write |
635 | + mock_requests_get.return_value = FakeRequest(200) |
636 | + mock_requests_get.return_value.set_json(self.job_data) |
637 | + |
638 | + # Login, get the command object |
639 | + get_cmd = self._login(mock_requests_get) |
640 | + |
641 | + mock_open = MagicMock() |
642 | + with patch('__builtin__.open', mock_open): |
643 | + # The easiest way to patch readlines is like this... |
644 | + with open("some_file") as some_file: |
645 | + some_file.readlines = MagicMock() |
646 | + some_file.readlines.return_value = [] |
647 | + |
648 | + # Run the command under test |
649 | + get_cmd.run(self.args) |
650 | + |
651 | + # Check that the URLs used in the REST query look sane |
652 | + self._check_call_params(mock_requests_get) |
653 | + |
654 | + # Check the URL we downloaded from and the file we wrote to |
655 | + self.assertEqual(mock_open.call_args[0], expected_file) |
656 | + self.assertEqual(urlopen.call_args[0], (expected_url,)) |
657 | + |
658 | + def test_get_single_job_download_single_file(self): |
659 | + self.job_data = { |
660 | + "objects": [ |
661 | + { |
662 | + "name": "a name", |
663 | + "build_number": 1, |
664 | + "status": "passed", |
665 | + "list_artifacts": ["a_file", "AnotherFile.txt", ".foo"], |
666 | + "get_build_result": {"a_file": "a_file_url"} |
667 | + } |
668 | + ] |
669 | + } |
670 | + |
671 | + self.args = ["--get-file", "a_file"] |
672 | + |
673 | + self._test_download_file(("a_file", "wb"), "a_file_url") |
674 | + |
675 | + def test_get_single_job_download_all_files(self): |
676 | + self.job_data = { |
677 | + "objects": [ |
678 | + { |
679 | + "name": "a name", |
680 | + "build_number": 1, |
681 | + "status": "passed", |
682 | + "list_artifacts": ["a_file", "AnotherFile.txt", ".foo"], |
683 | + "get_build_result": {"a_file": "a_file_url", |
684 | + "*zip*/archive.zip": "all_files_url", |
685 | + "*console_url*": "console_url"} |
686 | + } |
687 | + ] |
688 | + } |
689 | + |
690 | + self.args = ["--get-all-files"] |
691 | + |
692 | + self._test_download_file(("a name_1.zip", "wb"), "all_files_url") |
693 | + |
694 | + def test_get_single_job_download_console(self): |
695 | + self.job_data = { |
696 | + "objects": [ |
697 | + { |
698 | + "name": "a name", |
699 | + "build_number": 1, |
700 | + "status": "passed", |
701 | + "list_artifacts": ["a_file", "AnotherFile.txt", ".foo"], |
702 | + "get_build_result": {"a_file": "a_file_url", |
703 | + "*zip*/archive.zip": "all_files_url", |
704 | + "*console_url*": "console_url"} |
705 | + } |
706 | + ] |
707 | + } |
708 | + |
709 | + self.args = ["--console"] |
710 | + |
711 | + self._test_download_file(("a name_1.txt",), "console_url") |
712 | + |
713 | +if __name__ == '__main__': |
714 | + unittest.main() |
715 | |
716 | === modified file 'commands/tests/test_utilities.py' |
717 | --- commands/tests/test_utilities.py 2013-02-11 16:13:12 +0000 |
718 | +++ commands/tests/test_utilities.py 2013-03-11 13:41:20 +0000 |
719 | @@ -37,6 +37,12 @@ |
720 | else: |
721 | return self.return_codes |
722 | |
723 | + def set_json(self, dictionary): |
724 | + self.json_dict = dictionary |
725 | + |
726 | + def json(self): |
727 | + return self.json_dict |
728 | + |
729 | def expectedUrl(location): |
730 | url = urlparse.urljoin(CI_SERVER, API_PREFIX + location) |
731 | if url[-1] != "/": |
732 | |
733 | === modified file 'lci' |
734 | --- lci 2013-02-07 12:19:48 +0000 |
735 | +++ lci 2013-03-11 13:41:20 +0000 |
736 | @@ -101,12 +101,13 @@ |
737 | print """Linaro CI CLI -- interact with the Linaro CI service. |
738 | http://ci.linaro.org/ |
739 | |
740 | -Commands: |
741 | -... |
742 | +Basic commands: |
743 | + lci login Login to web service |
744 | + lci add Add a job to the CI service |
745 | + lci get Get output from a run of a job |
746 | + lci run Run a job |
747 | + lci status Get status of configured jobs and runs |
748 | """ |
749 | - # TODO: Should above import all commands and print info string from each? |
750 | - # TODO: Can we construct this with argparse from lots of commands? |
751 | - |
752 | |
753 | if __name__ == "__main__": |
754 | logging.basicConfig(format='%(message)s', level=logging.WARNING) |
I wondered where these magic filenames come from:
63 + if file_name == "*console_url*": archive. zip":
64 + print ("Fetching console output (will be stored as %s)." %
65 + (output_file_name))
66 + elif file_name == "*zip*/
And then figured it's Jenkins URLs components to download corresponding things. But shouldn't we abstract them?
133 + # Error handling. If user has requested a file(s)/console but not "get_file" ]: self.name,
134 + # specified a build number this is an error.
135 + if parameters[
136 + raise CommandFailed(
137 + message="Unable to --get-file for more than one job")
Comment appear to be inconsistent with the error message - does the latter wants to say "Unable to get for more than one *build*"?