Merge lp:~dooferlad/linaro-ci-cli/get-command into lp:linaro-ci-cli

Proposed by James Tunnicliffe
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
Reviewer Review Type Date Requested Status
Milo Casagrande (community) Approve
Review via email: mp+150549@code.launchpad.net

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

To post a comment you must log in.
Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

I wondered where these magic filenames come from:

63 + if file_name == "*console_url*":
64 + print ("Fetching console output (will be stored as %s)." %
65 + (output_file_name))
66 + elif file_name == "*zip*/archive.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
134 + # specified a build number this is an error.
135 + if parameters["get_file"]:
136 + raise CommandFailed(self.name,
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*"?

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

135 + if parameters["get_file"]:
138 + if parameters["get_all_files"]:
141 + if parameters["console"]:

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.)

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

> if(len(job_data["list_artifacts"]) and

PEP8 beeps.

Revision history for this message
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.

Revision history for this message
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/base_command.py
catches connection errors and prints a user friendly message). I can
split them out into another merge if you like.

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

Hey James,

forgot to update this. Looks good to go.
Thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches