Merge lp:~ericsnowcurrently/fake-juju/python-lib-testing into lp:~landscape/fake-juju/trunk-old
- python-lib-testing
- Merge into trunk-old
Proposed by
Eric Snow
Status: | Superseded |
---|---|
Proposed branch: | lp:~ericsnowcurrently/fake-juju/python-lib-testing |
Merge into: | lp:~landscape/fake-juju/trunk-old |
Diff against target: |
1036 lines (+983/-0) 10 files modified
python/LICENSE (+191/-0) python/Makefile (+9/-0) python/README.md (+1/-0) python/fakejuju/__init__.py (+57/-0) python/fakejuju/failures.py (+65/-0) python/fakejuju/fakejuju.py (+145/-0) python/fakejuju/testing.py (+68/-0) python/fakejuju/tests/test_failures.py (+98/-0) python/fakejuju/tests/test_fakejuju.py (+280/-0) python/setup.py (+69/-0) |
To merge this branch: | bzr merge lp:~ericsnowcurrently/fake-juju/python-lib-testing |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Landscape | Pending | ||
Landscape | Pending | ||
Review via email: mp+307896@code.launchpad.net |
This proposal has been superseded by a proposal from 2016-10-06.
Commit message
Description of the change
Add the fakejuju.testing module.
This is essentially fake-juju part of txjuju.testing.
To post a comment you must log in.
Unmerged revisions
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'python' |
2 | === added file 'python/LICENSE' |
3 | --- python/LICENSE 1970-01-01 00:00:00 +0000 |
4 | +++ python/LICENSE 2016-10-06 22:53:21 +0000 |
5 | @@ -0,0 +1,191 @@ |
6 | +All files in this repository are licensed as follows. If you contribute |
7 | +to this repository, it is assumed that you license your contribution |
8 | +under the same license unless you state otherwise. |
9 | + |
10 | +All files Copyright (C) 2012-2016 Canonical Ltd. unless otherwise specified in the file. |
11 | + |
12 | +This software is licensed under the LGPLv3, included below. |
13 | + |
14 | +As a special exception to the GNU Lesser General Public License version 3 |
15 | +("LGPL3"), the copyright holders of this Library give you permission to |
16 | +convey to a third party a Combined Work that links statically or dynamically |
17 | +to this Library without providing any Minimal Corresponding Source or |
18 | +Minimal Application Code as set out in 4d or providing the installation |
19 | +information set out in section 4e, provided that you comply with the other |
20 | +provisions of LGPL3 and provided that you meet, for the Application the |
21 | +terms and conditions of the license(s) which apply to the Application. |
22 | + |
23 | +Except as stated in this special exception, the provisions of LGPL3 will |
24 | +continue to comply in full to this Library. If you modify this Library, you |
25 | +may apply this exception to your version of this Library, but you are not |
26 | +obliged to do so. If you do not wish to do so, delete this exception |
27 | +statement from your version. This exception does not (and cannot) modify any |
28 | +license terms which apply to the Application, with which you must still |
29 | +comply. |
30 | + |
31 | + |
32 | + GNU LESSER GENERAL PUBLIC LICENSE |
33 | + Version 3, 29 June 2007 |
34 | + |
35 | + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> |
36 | + Everyone is permitted to copy and distribute verbatim copies |
37 | + of this license document, but changing it is not allowed. |
38 | + |
39 | + |
40 | + This version of the GNU Lesser General Public License incorporates |
41 | +the terms and conditions of version 3 of the GNU General Public |
42 | +License, supplemented by the additional permissions listed below. |
43 | + |
44 | + 0. Additional Definitions. |
45 | + |
46 | + As used herein, "this License" refers to version 3 of the GNU Lesser |
47 | +General Public License, and the "GNU GPL" refers to version 3 of the GNU |
48 | +General Public License. |
49 | + |
50 | + "The Library" refers to a covered work governed by this License, |
51 | +other than an Application or a Combined Work as defined below. |
52 | + |
53 | + An "Application" is any work that makes use of an interface provided |
54 | +by the Library, but which is not otherwise based on the Library. |
55 | +Defining a subclass of a class defined by the Library is deemed a mode |
56 | +of using an interface provided by the Library. |
57 | + |
58 | + A "Combined Work" is a work produced by combining or linking an |
59 | +Application with the Library. The particular version of the Library |
60 | +with which the Combined Work was made is also called the "Linked |
61 | +Version". |
62 | + |
63 | + The "Minimal Corresponding Source" for a Combined Work means the |
64 | +Corresponding Source for the Combined Work, excluding any source code |
65 | +for portions of the Combined Work that, considered in isolation, are |
66 | +based on the Application, and not on the Linked Version. |
67 | + |
68 | + The "Corresponding Application Code" for a Combined Work means the |
69 | +object code and/or source code for the Application, including any data |
70 | +and utility programs needed for reproducing the Combined Work from the |
71 | +Application, but excluding the System Libraries of the Combined Work. |
72 | + |
73 | + 1. Exception to Section 3 of the GNU GPL. |
74 | + |
75 | + You may convey a covered work under sections 3 and 4 of this License |
76 | +without being bound by section 3 of the GNU GPL. |
77 | + |
78 | + 2. Conveying Modified Versions. |
79 | + |
80 | + If you modify a copy of the Library, and, in your modifications, a |
81 | +facility refers to a function or data to be supplied by an Application |
82 | +that uses the facility (other than as an argument passed when the |
83 | +facility is invoked), then you may convey a copy of the modified |
84 | +version: |
85 | + |
86 | + a) under this License, provided that you make a good faith effort to |
87 | + ensure that, in the event an Application does not supply the |
88 | + function or data, the facility still operates, and performs |
89 | + whatever part of its purpose remains meaningful, or |
90 | + |
91 | + b) under the GNU GPL, with none of the additional permissions of |
92 | + this License applicable to that copy. |
93 | + |
94 | + 3. Object Code Incorporating Material from Library Header Files. |
95 | + |
96 | + The object code form of an Application may incorporate material from |
97 | +a header file that is part of the Library. You may convey such object |
98 | +code under terms of your choice, provided that, if the incorporated |
99 | +material is not limited to numerical parameters, data structure |
100 | +layouts and accessors, or small macros, inline functions and templates |
101 | +(ten or fewer lines in length), you do both of the following: |
102 | + |
103 | + a) Give prominent notice with each copy of the object code that the |
104 | + Library is used in it and that the Library and its use are |
105 | + covered by this License. |
106 | + |
107 | + b) Accompany the object code with a copy of the GNU GPL and this license |
108 | + document. |
109 | + |
110 | + 4. Combined Works. |
111 | + |
112 | + You may convey a Combined Work under terms of your choice that, |
113 | +taken together, effectively do not restrict modification of the |
114 | +portions of the Library contained in the Combined Work and reverse |
115 | +engineering for debugging such modifications, if you also do each of |
116 | +the following: |
117 | + |
118 | + a) Give prominent notice with each copy of the Combined Work that |
119 | + the Library is used in it and that the Library and its use are |
120 | + covered by this License. |
121 | + |
122 | + b) Accompany the Combined Work with a copy of the GNU GPL and this license |
123 | + document. |
124 | + |
125 | + c) For a Combined Work that displays copyright notices during |
126 | + execution, include the copyright notice for the Library among |
127 | + these notices, as well as a reference directing the user to the |
128 | + copies of the GNU GPL and this license document. |
129 | + |
130 | + d) Do one of the following: |
131 | + |
132 | + 0) Convey the Minimal Corresponding Source under the terms of this |
133 | + License, and the Corresponding Application Code in a form |
134 | + suitable for, and under terms that permit, the user to |
135 | + recombine or relink the Application with a modified version of |
136 | + the Linked Version to produce a modified Combined Work, in the |
137 | + manner specified by section 6 of the GNU GPL for conveying |
138 | + Corresponding Source. |
139 | + |
140 | + 1) Use a suitable shared library mechanism for linking with the |
141 | + Library. A suitable mechanism is one that (a) uses at run time |
142 | + a copy of the Library already present on the user's computer |
143 | + system, and (b) will operate properly with a modified version |
144 | + of the Library that is interface-compatible with the Linked |
145 | + Version. |
146 | + |
147 | + e) Provide Installation Information, but only if you would otherwise |
148 | + be required to provide such information under section 6 of the |
149 | + GNU GPL, and only to the extent that such information is |
150 | + necessary to install and execute a modified version of the |
151 | + Combined Work produced by recombining or relinking the |
152 | + Application with a modified version of the Linked Version. (If |
153 | + you use option 4d0, the Installation Information must accompany |
154 | + the Minimal Corresponding Source and Corresponding Application |
155 | + Code. If you use option 4d1, you must provide the Installation |
156 | + Information in the manner specified by section 6 of the GNU GPL |
157 | + for conveying Corresponding Source.) |
158 | + |
159 | + 5. Combined Libraries. |
160 | + |
161 | + You may place library facilities that are a work based on the |
162 | +Library side by side in a single library together with other library |
163 | +facilities that are not Applications and are not covered by this |
164 | +License, and convey such a combined library under terms of your |
165 | +choice, if you do both of the following: |
166 | + |
167 | + a) Accompany the combined library with a copy of the same work based |
168 | + on the Library, uncombined with any other library facilities, |
169 | + conveyed under the terms of this License. |
170 | + |
171 | + b) Give prominent notice with the combined library that part of it |
172 | + is a work based on the Library, and explaining where to find the |
173 | + accompanying uncombined form of the same work. |
174 | + |
175 | + 6. Revised Versions of the GNU Lesser General Public License. |
176 | + |
177 | + The Free Software Foundation may publish revised and/or new versions |
178 | +of the GNU Lesser General Public License from time to time. Such new |
179 | +versions will be similar in spirit to the present version, but may |
180 | +differ in detail to address new problems or concerns. |
181 | + |
182 | + Each version is given a distinguishing version number. If the |
183 | +Library as you received it specifies that a certain numbered version |
184 | +of the GNU Lesser General Public License "or any later version" |
185 | +applies to it, you have the option of following the terms and |
186 | +conditions either of that published version or of any later version |
187 | +published by the Free Software Foundation. If the Library as you |
188 | +received it does not specify a version number of the GNU Lesser |
189 | +General Public License, you may choose any version of the GNU Lesser |
190 | +General Public License ever published by the Free Software Foundation. |
191 | + |
192 | + If the Library as you received it specifies that a proxy can decide |
193 | +whether future versions of the GNU Lesser General Public License shall |
194 | +apply, that proxy's public statement of acceptance of any version is |
195 | +permanent authorization for you to choose that version for the |
196 | +Library. |
197 | |
198 | === added file 'python/Makefile' |
199 | --- python/Makefile 1970-01-01 00:00:00 +0000 |
200 | +++ python/Makefile 2016-10-06 22:53:21 +0000 |
201 | @@ -0,0 +1,9 @@ |
202 | +PYTHON = python |
203 | + |
204 | +.PHONY: test |
205 | +test: |
206 | + $(PYTHON) -m unittest discover -t $(shell pwd) -s $(shell pwd)/fakejuju |
207 | + |
208 | +.PHONY: install-dev |
209 | +install-dev: |
210 | + ln -s $(shell pwd)/fakejuju /usr/local/lib/python2.7/dist-packages/fakejuju |
211 | |
212 | === added file 'python/README.md' |
213 | --- python/README.md 1970-01-01 00:00:00 +0000 |
214 | +++ python/README.md 2016-10-06 22:53:21 +0000 |
215 | @@ -0,0 +1,1 @@ |
216 | +# fakejuju |
217 | |
218 | === added directory 'python/fakejuju' |
219 | === added file 'python/fakejuju/__init__.py' |
220 | --- python/fakejuju/__init__.py 1970-01-01 00:00:00 +0000 |
221 | +++ python/fakejuju/__init__.py 2016-10-06 22:53:21 +0000 |
222 | @@ -0,0 +1,57 @@ |
223 | +# Copyright 2016 Canonical Limited. All rights reserved. |
224 | + |
225 | +"""Support for interaction with fake-juju. |
226 | + |
227 | +"fake-juju" is a combination of the juju and jujud commands that is |
228 | +suitable for use in integration tests. It exposes a limited subset |
229 | +of the standard juju subcommands (see FakeJuju in this module for |
230 | +specifics). When called without any arguments it runs jujud (using |
231 | +the dummy provider) with extra logging and testing hooks available to |
232 | +control failures. See https://launchpad.net/fake-juju for the project. |
233 | + |
234 | +The binary is named with the Juju version for which it was built. |
235 | +For example, for version 1.25.6 the file is named "fake-juju-1.25.6". |
236 | + |
237 | +fake-juju uses the normal Juju local config directory. This defaults |
238 | +to ~/.local/shared/juju and may be set using the JUJU_DATA environment |
239 | +variable (in 2.x, for 1.x it is JUJU_HOME). |
240 | + |
241 | +In addition to all the normal Juju environment variables (e.g. |
242 | +JUJU_DATA), fake-juju uses the following: |
243 | + |
244 | + FAKE_JUJU_FAILURES - the path to the failures file |
245 | + The Failures class below sets this to $JUJU_DATA/juju-failures. |
246 | + FAKE_JUJU_LOGS_DIR - the path to the logs directory |
247 | + This defaults to $JUJU_DATA. |
248 | + |
249 | +fake-juju also creates several extra files: |
250 | + |
251 | + $FAKE_JUJU_LOGS_DIR/fake-juju.log - where fake-juju logs are written |
252 | + $JUJU_DATA/fakejuju - fake-juju's data cache |
253 | + $JUJU_DATA/fifo - a FIFO file that triggers jujud shutdown |
254 | + $JUJU_DATA/cert.ca - the API's CA certificate |
255 | + |
256 | +Normal Juju logging for is written to $JUJU_DATA/fake-juju.log. |
257 | + |
258 | +Failures may be injected into a running fake-juju (or set before |
259 | +running). They may be injected by adding them to the file identified |
260 | +by $FAKE_JUJU_FAILURES. The format is a single failure definition per |
261 | +line. The syntax of the failure definition depends on the failure. |
262 | +The currently supported failures (with their definition syntax) are |
263 | +listed here: |
264 | + |
265 | + * when adding a unit with a specific ID |
266 | + format: "unit-<ID>" (e.g. unit-mysql/0) |
267 | + |
268 | +""" |
269 | + |
270 | +from .fakejuju import get_bootstrap_spec, get_filename, set_envvars, FakeJuju |
271 | + |
272 | + |
273 | +__all__ = [ |
274 | + "__version__", |
275 | + "get_bootstrap_spec", "get_filename", "set_envvars", |
276 | + "FakeJuju", |
277 | + ] |
278 | + |
279 | +__version__ = "0.9.0b1" |
280 | |
281 | === added file 'python/fakejuju/failures.py' |
282 | --- python/fakejuju/failures.py 1970-01-01 00:00:00 +0000 |
283 | +++ python/fakejuju/failures.py 2016-10-06 22:53:21 +0000 |
284 | @@ -0,0 +1,65 @@ |
285 | +# Copyright 2016 Canonical Limited. All rights reserved. |
286 | + |
287 | +import errno |
288 | +import os |
289 | +import os.path |
290 | + |
291 | + |
292 | +class Failures(object): |
293 | + """The collection of injected failures to use with a fake-juju. |
294 | + |
295 | + The failures are tracked here as well as injected into any |
296 | + fake-juju using the initial config dir (aka "juju home"). |
297 | + |
298 | + Note that fake-juju provides only limited capability for |
299 | + failure injection. |
300 | + """ |
301 | + |
302 | + def __init__(self, cfgdir, entities=None): |
303 | + """ |
304 | + @param cfgdir: The "juju home" directory into which the |
305 | + failures will be registered for injection. |
306 | + @param entities: The entity names to start with, if any. |
307 | + """ |
308 | + filename = os.path.join(cfgdir, "juju-failures") |
309 | + entities = set(unicode(tag) for tag in entities or ()) |
310 | + |
311 | + self._filename = unicode(filename) |
312 | + self._entities = entities |
313 | + |
314 | + @property |
315 | + def filename(self): |
316 | + """The path to the failures file the fake-juju reads.""" |
317 | + return self._filename |
318 | + |
319 | + @property |
320 | + def entities(self): |
321 | + """The IDs of the failing entities.""" |
322 | + return set(self._entities) |
323 | + |
324 | + def _flush(self): |
325 | + """Write the failures to disk.""" |
326 | + data = "\n".join(self._entities) + "\n" |
327 | + try: |
328 | + file = open(self._filename, "w") |
329 | + except IOError: |
330 | + dirname = os.path.dirname(self._filename) |
331 | + if not os.path.exists(dirname): |
332 | + os.makedirs(dirname) |
333 | + file = open(self._filename, "w") |
334 | + with file: |
335 | + file.write(data) |
336 | + |
337 | + def fail_entity(self, tag): |
338 | + """Inject a global failure for the identified Juju entity.""" |
339 | + self._entities.add(tag) |
340 | + self._flush() |
341 | + |
342 | + def clear(self): |
343 | + """Remove all injected failures.""" |
344 | + try: |
345 | + os.remove(self._filename) |
346 | + except OSError as e: |
347 | + if e.errno != errno.ENOENT: |
348 | + raise |
349 | + self._entities.clear() |
350 | |
351 | === added file 'python/fakejuju/fakejuju.py' |
352 | --- python/fakejuju/fakejuju.py 1970-01-01 00:00:00 +0000 |
353 | +++ python/fakejuju/fakejuju.py 2016-10-06 22:53:21 +0000 |
354 | @@ -0,0 +1,145 @@ |
355 | +# Copyright 2016 Canonical Limited. All rights reserved. |
356 | + |
357 | +from collections import namedtuple |
358 | +import os.path |
359 | + |
360 | +import txjuju.cli |
361 | + |
362 | +from .failures import Failures |
363 | + |
364 | + |
365 | +def get_bootstrap_spec(name, admin_secret=None): |
366 | + """Return the BootstrapSpec instance for the given controller. |
367 | + |
368 | + @param name: The controller name to set up. |
369 | + @param admin_secret: The admin user password to use. |
370 | + """ |
371 | + type = "dummy" |
372 | + default_series = None # Use the default. |
373 | + return txjuju.cli.BootstrapSpec(name, type, default_series, admin_secret) |
374 | + |
375 | + |
376 | +def get_filename(version, bindir=None): |
377 | + """Return the full path to the fake-juju binary for the given version. |
378 | + |
379 | + @param version: The Juju version to use. |
380 | + @param bindir: The directory containing the fake-juju binary. |
381 | + This defaults to /usr/bin. |
382 | + """ |
383 | + if not version: |
384 | + raise ValueError("version not provided") |
385 | + filename = "fake-juju-{}".format(version) |
386 | + if bindir is None: |
387 | + # XXX Search $PATH. |
388 | + bindir = "/usr/bin" |
389 | + return os.path.join(bindir, filename) |
390 | + |
391 | + |
392 | +def set_envvars(envvars, failures_filename=None, logsdir=None): |
393 | + """Return the environment variables with which to run fake-juju. |
394 | + |
395 | + @param envvars: The env dict to update. |
396 | + @param failures_filename: The path to the failures file that |
397 | + fake-juju will use. |
398 | + @params logsdir: The path to the directory where fake-juju will |
399 | + write its log files. |
400 | + """ |
401 | + envvars["FAKE_JUJU_FAILURES"] = failures_filename or "" |
402 | + envvars["FAKE_JUJU_LOGS_DIR"] = logsdir or "" |
403 | + |
404 | + |
405 | +class FakeJuju( |
406 | + namedtuple("FakeJuju", "filename version cfgdir logsdir failures")): |
407 | + """The fundamental details for fake-juju.""" |
408 | + |
409 | + @classmethod |
410 | + def from_version(cls, version, cfgdir, |
411 | + logsdir=None, failuresdir=None, bindir=None): |
412 | + """Return a new instance given the provided information. |
413 | + |
414 | + @param version: The Juju version to fake. |
415 | + @param cfgdir: The "juju home" directory to use. |
416 | + @param logsdir: The directory where logs will be written. |
417 | + This defaults to cfgdir. |
418 | + @params failuresdir: The directory where failure injection |
419 | + is managed. |
420 | + @param bindir: The directory containing the fake-juju binary. |
421 | + This defaults to /usr/bin. |
422 | + """ |
423 | + if logsdir is None: |
424 | + logsdir = cfgdir |
425 | + if failuresdir is None: |
426 | + failuresdir = cfgdir |
427 | + filename = get_filename(version, bindir=bindir) |
428 | + failures = Failures(failuresdir) |
429 | + return cls(filename, version, cfgdir, logsdir, failures) |
430 | + |
431 | + def __new__(cls, filename, version, cfgdir, logsdir=None, failures=None): |
432 | + """ |
433 | + @param filename: The path to the fake-juju binary. |
434 | + @param version: The Juju version to fake. |
435 | + @param cfgdir: The "juju home" directory to use. |
436 | + @param logsdir: The directory where logs will be written. |
437 | + This defaults to cfgdir. |
438 | + @param failures: The set of fake-juju failures to use. |
439 | + """ |
440 | + filename = unicode(filename) if filename else None |
441 | + version = unicode(version) if version else None |
442 | + cfgdir = unicode(cfgdir) if cfgdir else None |
443 | + logsdir = unicode(logsdir) if logsdir is not None else cfgdir |
444 | + if failures is None and cfgdir: |
445 | + failures = Failures(cfgdir) |
446 | + return super(FakeJuju, cls).__new__( |
447 | + cls, filename, version, cfgdir, logsdir, failures) |
448 | + |
449 | + def __init__(self, *args, **kwargs): |
450 | + if not self.filename: |
451 | + raise ValueError("missing filename") |
452 | + if not self.version: |
453 | + raise ValueError("missing version") |
454 | + if not self.cfgdir: |
455 | + raise ValueError("missing cfgdir") |
456 | + if not self.logsdir: |
457 | + raise ValueError("missing logsdir") |
458 | + if self.failures is None: |
459 | + raise ValueError("missing failures") |
460 | + |
461 | + @property |
462 | + def logfile(self): |
463 | + """The path to fake-juju's log file.""" |
464 | + return os.path.join(self.logsdir, "fake-juju.log") |
465 | + |
466 | + @property |
467 | + def infofile(self): |
468 | + """The path to fake-juju's data cache.""" |
469 | + return os.path.join(self.cfgdir, "fakejuju") |
470 | + |
471 | + @property |
472 | + def fifo(self): |
473 | + """The path to the fifo file that triggers shutdown.""" |
474 | + return os.path.join(self.cfgdir, "fifo") |
475 | + |
476 | + @property |
477 | + def cacertfile(self): |
478 | + """The path to the API server's certificate.""" |
479 | + return os.path.join(self.cfgdir, "cert.ca") |
480 | + |
481 | + def cli(self, envvars=None): |
482 | + """ |
483 | + |
484 | + Currently only the following juju subcommands are supported: |
485 | + |
486 | + * bootstrap |
487 | + Not that this only supports the dummy provider and the local |
488 | + system is only minimally impacted. |
489 | + * api-info |
490 | + Note that passwords are always omited, even if requested. |
491 | + * api-endpoints |
492 | + * destroy-environment |
493 | + """ |
494 | + if envvars is None: |
495 | + envvars = os.environ |
496 | + envvars = dict(envvars) |
497 | + set_envvars(envvars, self.failures._filename, self.logsdir) |
498 | + return txjuju.cli.CLI.from_version( |
499 | + self.filename, self.version, self.cfgdir, envvars) |
500 | |
501 | === added file 'python/fakejuju/testing.py' |
502 | --- python/fakejuju/testing.py 1970-01-01 00:00:00 +0000 |
503 | +++ python/fakejuju/testing.py 2016-10-06 22:53:21 +0000 |
504 | @@ -0,0 +1,68 @@ |
505 | +# Copyright 2016 Canonical Limited. All rights reserved. |
506 | + |
507 | +import txjuju |
508 | +from fixtures import Fixture, TempDir |
509 | +from testtools.content import content_from_file |
510 | + |
511 | +from . import fakejuju |
512 | + |
513 | + |
514 | +JUJU1_VER = "1.25.6" |
515 | +JUJU2_VER = "2.0-beta17" |
516 | +JUJU_VER = JUJU1_VER |
517 | + |
518 | + |
519 | +class FakeJujuFixture(Fixture): |
520 | + """Manages a fake-juju process.""" |
521 | + |
522 | + CONTROLLER = "test" |
523 | + ADMIN_SECRET = "sekret" |
524 | + VERSION = JUJU_VER |
525 | + |
526 | + def __init__(self, controller=None, password=None, logs_dir=None, |
527 | + version=None): |
528 | + """ |
529 | + @param logs_dir: If given, copy logs to this directory upon cleanup, |
530 | + otherwise, print it as test plain text detail upon failure. |
531 | + """ |
532 | + if controller is None: |
533 | + controller = self.CONTROLLER |
534 | + if password is None: |
535 | + password = self.ADMIN_SECRET |
536 | + if version is None: |
537 | + version = self.VERSION |
538 | + |
539 | + self._controller = controller |
540 | + self._password = password |
541 | + self._logs_dir = logs_dir |
542 | + self._version = version |
543 | + |
544 | + def setUp(self): |
545 | + super(FakeJujuFixture, self).setUp() |
546 | + self._juju_home = self.useFixture(TempDir()) |
547 | + self._juju = fakejuju.FakeJuju.make( |
548 | + self._juju_home.path, self._version, self._logs_dir) |
549 | + |
550 | + if not self._logs_dir: |
551 | + # Attach logs as testtools details. |
552 | + self.addDetail("log-file", content_from_file(self._juju.logfile)) |
553 | + |
554 | + spec = fakejuju.get_bootstrap_spec(self._controller, self._password) |
555 | + cfgfile = txjuju.prepare_for_bootstrap( |
556 | + spec, self._version, self._juju_home) |
557 | + cli = self._juju.cli() |
558 | + cli.bootstrap(spec, cfgfile=cfgfile) |
559 | + api_info = cli.api_info(spec.name) |
560 | + if self._version.startswith("1."): |
561 | + # fake-juju doesn't give us the password, so we have to |
562 | + # set it here. |
563 | + api_info = api_info._replace(password=self._password) |
564 | + self.api_info = api_info |
565 | + |
566 | + def cleanUp(self): |
567 | + self._juju.destroy_controller(self._controller) |
568 | + super(FakeJujuFixture, self).cleanUp() |
569 | + |
570 | + def add_failure(self, entity): |
571 | + """Make the given entity fail with an error status.""" |
572 | + self._juju.failures.fail_entity(entity) |
573 | |
574 | === added directory 'python/fakejuju/tests' |
575 | === added file 'python/fakejuju/tests/__init__.py' |
576 | === added file 'python/fakejuju/tests/test_failures.py' |
577 | --- python/fakejuju/tests/test_failures.py 1970-01-01 00:00:00 +0000 |
578 | +++ python/fakejuju/tests/test_failures.py 2016-10-06 22:53:21 +0000 |
579 | @@ -0,0 +1,98 @@ |
580 | +# Copyright 2016 Canonical Limited. All rights reserved. |
581 | + |
582 | +import os |
583 | +import os.path |
584 | +import shutil |
585 | +import tempfile |
586 | +import unittest |
587 | + |
588 | +from fakejuju.failures import Failures |
589 | + |
590 | + |
591 | +class FailuresTests(unittest.TestCase): |
592 | + |
593 | + def setUp(self): |
594 | + super(FailuresTests, self).setUp() |
595 | + self.dirname = tempfile.mkdtemp(prefix="fakejuju-test-") |
596 | + |
597 | + def tearDown(self): |
598 | + shutil.rmtree(self.dirname) |
599 | + super(FailuresTests, self).tearDown() |
600 | + |
601 | + def test_full(self): |
602 | + """Failures() works correctly when given all args.""" |
603 | + entities = [u"x", u"y", u"z"] |
604 | + failures = Failures(u"/some/dir", entities) |
605 | + |
606 | + self.assertEqual(failures.filename, u"/some/dir/juju-failures") |
607 | + self.assertEqual(failures.entities, set(entities)) |
608 | + |
609 | + def test_minimal(self): |
610 | + """Failures() works correctly when given minimal args.""" |
611 | + failures = Failures(u"/some/dir") |
612 | + |
613 | + self.assertEqual(failures.filename, u"/some/dir/juju-failures") |
614 | + self.assertEqual(failures.entities, set()) |
615 | + |
616 | + def test_conversion(self): |
617 | + """Failures() converts str to unicode.""" |
618 | + entities = ["x", "y", "z"] |
619 | + failures = Failures("/some/dir", entities) |
620 | + |
621 | + self.assertIsInstance(failures.filename, unicode) |
622 | + for id in failures.entities: |
623 | + self.assertIsInstance(id, unicode) |
624 | + |
625 | + def test_file_not_created_initially(self): |
626 | + """Failures() doesn't create a missing cfgdir until necessary.""" |
627 | + failures = Failures(self.dirname) |
628 | + |
629 | + self.assertFalse(os.path.exists(failures.filename)) |
630 | + |
631 | + def test_cfgdir_created(self): |
632 | + """Failures() creates a missing cfgdir as soon as it's needed.""" |
633 | + dirname = os.path.join(self.dirname, "subdir") |
634 | + self.assertFalse(os.path.exists(dirname)) |
635 | + failures = Failures(dirname) |
636 | + failures.fail_entity("unit-xyz") |
637 | + |
638 | + self.assertTrue(os.path.exists(dirname)) |
639 | + |
640 | + def test_fail_entity_one(self): |
641 | + """Failures,fail_entity() writes an initial entry to disk.""" |
642 | + failures = Failures(self.dirname) |
643 | + failures.fail_entity("unit-abc") |
644 | + with open(failures.filename) as file: |
645 | + data = file.read() |
646 | + |
647 | + self.assertEqual(data, "unit-abc\n") |
648 | + |
649 | + def test_fail_entity_multiple(self): |
650 | + """Failures.fail_entity() correctly writes multiple entries to disk.""" |
651 | + failures = Failures(self.dirname) |
652 | + failures.fail_entity("unit-abc") |
653 | + failures.fail_entity("unit-xyz") |
654 | + |
655 | + with open(failures.filename) as file: |
656 | + data = file.read() |
657 | + entities = set(tag for tag in data.splitlines() if tag) |
658 | + self.assertEqual(entities, failures.entities) |
659 | + self.assertTrue(data.endswith("\n")) |
660 | + |
661 | + def test_clear_exists(self): |
662 | + """Failures.clear() deletes the failures file if it exists.""" |
663 | + failures = Failures(self.dirname) |
664 | + failures.fail_entity("unit-abc") |
665 | + self.assertTrue(os.path.exists(failures.filename)) |
666 | + failures.clear() |
667 | + |
668 | + self.assertFalse(os.path.exists(failures.filename)) |
669 | + self.assertEqual(failures.entities, set()) |
670 | + |
671 | + def test_clear_not_exists(self): |
672 | + """Failures.clear() does nothing if the failures file is missing.""" |
673 | + failures = Failures(self.dirname) |
674 | + self.assertFalse(os.path.exists(failures.filename)) |
675 | + failures.clear() |
676 | + |
677 | + self.assertFalse(os.path.exists(failures.filename)) |
678 | |
679 | === added file 'python/fakejuju/tests/test_fakejuju.py' |
680 | --- python/fakejuju/tests/test_fakejuju.py 1970-01-01 00:00:00 +0000 |
681 | +++ python/fakejuju/tests/test_fakejuju.py 2016-10-06 22:53:21 +0000 |
682 | @@ -0,0 +1,280 @@ |
683 | +# Copyright 2016 Canonical Limited. All rights reserved. |
684 | + |
685 | +import os |
686 | +import unittest |
687 | + |
688 | +from txjuju import _juju1, _juju2 |
689 | +from txjuju._utils import Executable |
690 | +import txjuju.cli |
691 | + |
692 | +from fakejuju.failures import Failures |
693 | +from fakejuju.fakejuju import ( |
694 | + get_bootstrap_spec, get_filename, set_envvars, FakeJuju) |
695 | + |
696 | + |
697 | +class HelperTests(unittest.TestCase): |
698 | + |
699 | + def test_get_bootstrap_spec_full(self): |
700 | + """get_bootstrap_spec() works correctly when given all args.""" |
701 | + spec = get_bootstrap_spec("my-env", "pw") |
702 | + |
703 | + self.assertEqual( |
704 | + spec, |
705 | + txjuju.cli.BootstrapSpec("my-env", "dummy", admin_secret="pw")) |
706 | + |
707 | + def test_get_bootstrap_spec_minimal(self): |
708 | + """get_bootstrap_spec() works correctly when given minimal args.""" |
709 | + spec = get_bootstrap_spec("my-env") |
710 | + |
711 | + self.assertEqual(spec, txjuju.cli.BootstrapSpec("my-env", "dummy")) |
712 | + |
713 | + def test_get_filename_full(self): |
714 | + """get_filename() works correctly when given all args.""" |
715 | + filename = get_filename("1.25.6", "/spam") |
716 | + |
717 | + self.assertEqual(filename, "/spam/fake-juju-1.25.6") |
718 | + |
719 | + def test_get_filename_minimal(self): |
720 | + """get_filename() works correctly when given minimal args.""" |
721 | + filename = get_filename("1.25.6") |
722 | + |
723 | + self.assertEqual(filename, "/usr/bin/fake-juju-1.25.6") |
724 | + |
725 | + def test_get_filename_empty_bindir(self): |
726 | + """get_filename() works correctly when given an empty string |
727 | + for bindir.""" |
728 | + filename = get_filename("1.25.6", "") |
729 | + |
730 | + self.assertEqual(filename, "fake-juju-1.25.6") |
731 | + |
732 | + def test_get_filename_missing_version(self): |
733 | + """get_filename() fails if version is None or empty.""" |
734 | + with self.assertRaises(ValueError): |
735 | + get_filename(None) |
736 | + with self.assertRaises(ValueError): |
737 | + get_filename("") |
738 | + |
739 | + def test_set_envvars_full(self): |
740 | + """set_envvars() works correctly when given all args.""" |
741 | + envvars = {} |
742 | + set_envvars(envvars, "/spam/failures", "/eggs/logsdir") |
743 | + |
744 | + self.assertEqual(envvars, { |
745 | + "FAKE_JUJU_FAILURES": "/spam/failures", |
746 | + "FAKE_JUJU_LOGS_DIR": "/eggs/logsdir", |
747 | + }) |
748 | + |
749 | + def test_set_envvars_minimal(self): |
750 | + """set_envvars() works correctly when given minimal args.""" |
751 | + envvars = {} |
752 | + set_envvars(envvars) |
753 | + |
754 | + self.assertEqual(envvars, { |
755 | + "FAKE_JUJU_FAILURES": "", |
756 | + "FAKE_JUJU_LOGS_DIR": "", |
757 | + }) |
758 | + |
759 | + def test_set_envvars_start_empty(self): |
760 | + """set_envvars() sets all values on an empty dict.""" |
761 | + envvars = {} |
762 | + set_envvars(envvars, "x", "y") |
763 | + |
764 | + self.assertEqual(envvars, { |
765 | + "FAKE_JUJU_FAILURES": "x", |
766 | + "FAKE_JUJU_LOGS_DIR": "y", |
767 | + }) |
768 | + |
769 | + def test_set_envvars_no_collisions(self): |
770 | + """set_envvars() sets all values when none are set yet.""" |
771 | + envvars = {"SPAM": "eggs"} |
772 | + set_envvars(envvars, "x", "y") |
773 | + |
774 | + self.assertEqual(envvars, { |
775 | + "SPAM": "eggs", |
776 | + "FAKE_JUJU_FAILURES": "x", |
777 | + "FAKE_JUJU_LOGS_DIR": "y", |
778 | + }) |
779 | + |
780 | + def test_set_envvars_empty_to_nonempty(self): |
781 | + """set_envvars() updates empty values.""" |
782 | + envvars = { |
783 | + "FAKE_JUJU_FAILURES": "", |
784 | + "FAKE_JUJU_LOGS_DIR": "", |
785 | + } |
786 | + set_envvars(envvars, "x", "y") |
787 | + |
788 | + self.assertEqual(envvars, { |
789 | + "FAKE_JUJU_FAILURES": "x", |
790 | + "FAKE_JUJU_LOGS_DIR": "y", |
791 | + }) |
792 | + |
793 | + def test_set_envvars_nonempty_to_nonempty(self): |
794 | + """set_envvars() overwrites existing values.""" |
795 | + envvars = { |
796 | + "FAKE_JUJU_FAILURES": "spam", |
797 | + "FAKE_JUJU_LOGS_DIR": "ham", |
798 | + } |
799 | + set_envvars(envvars, "x", "y") |
800 | + |
801 | + self.assertEqual(envvars, { |
802 | + "FAKE_JUJU_FAILURES": "x", |
803 | + "FAKE_JUJU_LOGS_DIR": "y", |
804 | + }) |
805 | + |
806 | + def test_set_envvars_nonempty_to_empty(self): |
807 | + """set_envvars() with no args "unsets" existing values.""" |
808 | + envvars = { |
809 | + "FAKE_JUJU_FAILURES": "x", |
810 | + "FAKE_JUJU_LOGS_DIR": "y", |
811 | + } |
812 | + set_envvars(envvars) |
813 | + |
814 | + self.assertEqual(envvars, { |
815 | + "FAKE_JUJU_FAILURES": "", |
816 | + "FAKE_JUJU_LOGS_DIR": "", |
817 | + }) |
818 | + |
819 | + |
820 | +class FakeJujuTests(unittest.TestCase): |
821 | + |
822 | + def test_from_version_full(self): |
823 | + """FakeJuju.from_version() works correctly when given all args.""" |
824 | + juju = FakeJuju.from_version( |
825 | + "1.25.6", "/a/juju/home", "/logs/dir", "/failures/dir", "/bin/dir") |
826 | + |
827 | + self.assertEqual(juju.filename, "/bin/dir/fake-juju-1.25.6") |
828 | + self.assertEqual(juju.version, "1.25.6") |
829 | + self.assertEqual(juju.cfgdir, "/a/juju/home") |
830 | + self.assertEqual(juju.logsdir, "/logs/dir") |
831 | + self.assertEqual(juju.failures.filename, "/failures/dir/juju-failures") |
832 | + |
833 | + def test_from_version_minimal(self): |
834 | + """FakeJuju.from_version() works correctly when given minimal args.""" |
835 | + juju = FakeJuju.from_version("1.25.6", "/my/juju/home") |
836 | + |
837 | + self.assertEqual(juju.filename, "/usr/bin/fake-juju-1.25.6") |
838 | + self.assertEqual(juju.version, "1.25.6") |
839 | + self.assertEqual(juju.cfgdir, "/my/juju/home") |
840 | + self.assertEqual(juju.logsdir, "/my/juju/home") |
841 | + self.assertEqual(juju.failures.filename, "/my/juju/home/juju-failures") |
842 | + |
843 | + def test_full(self): |
844 | + """FakeJuju() works correctly when given all args.""" |
845 | + cfgdir = "/my/juju/home" |
846 | + failures = Failures(cfgdir) |
847 | + juju = FakeJuju("/fake-juju", "1.25.6", cfgdir, "/some/logs", failures) |
848 | + |
849 | + self.assertEqual(juju.filename, "/fake-juju") |
850 | + self.assertEqual(juju.version, "1.25.6") |
851 | + self.assertEqual(juju.cfgdir, cfgdir) |
852 | + self.assertEqual(juju.logsdir, "/some/logs") |
853 | + self.assertIs(juju.failures, failures) |
854 | + |
855 | + def test_minimal(self): |
856 | + """FakeJuju() works correctly when given minimal args.""" |
857 | + juju = FakeJuju("/fake-juju", "1.25.6", "/my/juju/home") |
858 | + |
859 | + self.assertEqual(juju.filename, "/fake-juju") |
860 | + self.assertEqual(juju.version, "1.25.6") |
861 | + self.assertEqual(juju.cfgdir, "/my/juju/home") |
862 | + self.assertEqual(juju.logsdir, "/my/juju/home") |
863 | + self.assertEqual(juju.failures.filename, "/my/juju/home/juju-failures") |
864 | + |
865 | + def test_conversions(self): |
866 | + """FakeJuju() converts str to unicode.""" |
867 | + juju = FakeJuju("/fake-juju", "1.25.6", "/x", "/y", Failures("/...")) |
868 | + |
869 | + self.assertIsInstance(juju.filename, unicode) |
870 | + self.assertIsInstance(juju.version, unicode) |
871 | + self.assertIsInstance(juju.cfgdir, unicode) |
872 | + self.assertIsInstance(juju.logsdir, unicode) |
873 | + |
874 | + def test_missing_filename(self): |
875 | + """FakeJuju() fails if filename is None or empty.""" |
876 | + with self.assertRaises(ValueError): |
877 | + FakeJuju(None, "1.25.6", "/my/juju/home") |
878 | + with self.assertRaises(ValueError): |
879 | + FakeJuju("", "1.25.6", "/my/juju/home") |
880 | + |
881 | + def test_missing_version(self): |
882 | + """FakeJuju() fails if version is None or empty.""" |
883 | + with self.assertRaises(ValueError): |
884 | + FakeJuju("/fake-juju", None, "/my/juju/home") |
885 | + with self.assertRaises(ValueError): |
886 | + FakeJuju("/fake-juju", "", "/my/juju/home") |
887 | + |
888 | + def test_missing_cfgdir(self): |
889 | + """FakeJuju() fails if cfgdir is None or empty.""" |
890 | + with self.assertRaises(ValueError): |
891 | + FakeJuju("/fake-juju", "1.25.6", None) |
892 | + with self.assertRaises(ValueError): |
893 | + FakeJuju("/fake-juju", "1.25.6", "") |
894 | + |
895 | + def test_logfile(self): |
896 | + """FakeJuju.logfile returns the path to the fake-juju log file.""" |
897 | + juju = FakeJuju("/fake-juju", "1.25.6", "/x", "/some/logs") |
898 | + |
899 | + self.assertEqual(juju.logfile, "/some/logs/fake-juju.log") |
900 | + |
901 | + def test_infofile(self): |
902 | + """FakeJuju.logfile returns the path to the fake-juju info file.""" |
903 | + juju = FakeJuju("/fake-juju", "1.25.6", "/x") |
904 | + |
905 | + self.assertEqual(juju.infofile, "/x/fakejuju") |
906 | + |
907 | + def test_fifo(self): |
908 | + """FakeJuju.logfile returns the path to the fake-juju fifo.""" |
909 | + juju = FakeJuju("/fake-juju", "1.25.6", "/x") |
910 | + |
911 | + self.assertEqual(juju.fifo, "/x/fifo") |
912 | + |
913 | + def test_cacertfile(self): |
914 | + """FakeJuju.cacertfile returns the path to the Juju API cert.""" |
915 | + juju = FakeJuju("/fake-juju", "1.25.6", "/x") |
916 | + |
917 | + self.assertEqual(juju.cacertfile, "/x/cert.ca") |
918 | + |
919 | + def test_cli_full(self): |
920 | + """FakeJuju.cli() works correctly when given all args.""" |
921 | + juju = FakeJuju("/fake-juju", "1.25.6", "/x") |
922 | + cli = juju.cli({"SPAM": "eggs"}) |
923 | + |
924 | + self.assertEqual( |
925 | + cli._exe, |
926 | + Executable("/fake-juju", { |
927 | + "SPAM": "eggs", |
928 | + "FAKE_JUJU_FAILURES": "/x/juju-failures", |
929 | + "FAKE_JUJU_LOGS_DIR": "/x", |
930 | + "JUJU_HOME": "/x", |
931 | + }), |
932 | + ) |
933 | + |
934 | + def test_cli_minimal(self): |
935 | + """FakeJuju.cli() works correctly when given minimal args.""" |
936 | + juju = FakeJuju("/fake-juju", "1.25.6", "/x") |
937 | + cli = juju.cli() |
938 | + |
939 | + self.assertEqual( |
940 | + cli._exe, |
941 | + Executable("/fake-juju", dict(os.environ, **{ |
942 | + "FAKE_JUJU_FAILURES": "/x/juju-failures", |
943 | + "FAKE_JUJU_LOGS_DIR": "/x", |
944 | + "JUJU_HOME": "/x", |
945 | + })), |
946 | + ) |
947 | + |
948 | + def test_cli_juju1(self): |
949 | + """FakeJuju.cli() works correctly for Juju 1.x.""" |
950 | + juju = FakeJuju.from_version("1.25.6", "/x") |
951 | + cli = juju.cli() |
952 | + |
953 | + self.assertEqual(cli._exe.envvars["JUJU_HOME"], "/x") |
954 | + self.assertIsInstance(cli._juju, _juju1.CLIHooks) |
955 | + |
956 | + def test_cli_juju2(self): |
957 | + """FakeJuju.cli() works correctly for Juju 2.x.""" |
958 | + juju = FakeJuju.from_version("2.0.0", "/x") |
959 | + cli = juju.cli() |
960 | + |
961 | + self.assertEqual(cli._exe.envvars["JUJU_DATA"], "/x") |
962 | + self.assertIsInstance(cli._juju, _juju2.CLIHooks) |
963 | |
964 | === added file 'python/setup.py' |
965 | --- python/setup.py 1970-01-01 00:00:00 +0000 |
966 | +++ python/setup.py 2016-10-06 22:53:21 +0000 |
967 | @@ -0,0 +1,69 @@ |
968 | +import os |
969 | +from importlib import import_module |
970 | +try: |
971 | + from setuptools import setup |
972 | +except ImportError: |
973 | + from distutils.core import setup |
974 | + |
975 | + |
976 | +basedir = os.path.abspath(os.path.dirname(__file__) or '.') |
977 | + |
978 | +# required data |
979 | + |
980 | +package_name = 'fakejuju' |
981 | +NAME = package_name |
982 | +SUMMARY = 'A limited adaptation of Juju\'s client, with testing hooks.' |
983 | +AUTHOR = 'Canonical Landscape team' |
984 | +EMAIL = 'juju@lists.ubuntu.com' |
985 | +PROJECT_URL = 'https://launchpad.net/fake-juju' |
986 | +LICENSE = 'LGPLv3' |
987 | + |
988 | +with open(os.path.join(basedir, 'README.md')) as readme_file: |
989 | + DESCRIPTION = readme_file.read() |
990 | + |
991 | +# dymanically generated data |
992 | + |
993 | +VERSION = import_module(package_name).__version__ |
994 | + |
995 | +# set up packages |
996 | + |
997 | +exclude_dirs = [ |
998 | + 'tests', |
999 | + ] |
1000 | + |
1001 | +PACKAGES = [] |
1002 | +for path, dirs, files in os.walk(package_name): |
1003 | + if "__init__.py" not in files: |
1004 | + continue |
1005 | + path = path.split(os.sep) |
1006 | + if path[-1] in exclude_dirs: |
1007 | + continue |
1008 | + PACKAGES.append(".".join(path)) |
1009 | + |
1010 | +# dependencies |
1011 | + |
1012 | +DEPS = ['yaml', |
1013 | + # for testing |
1014 | + 'txjuju', |
1015 | + 'fixtures', |
1016 | + 'testtools', |
1017 | + ] |
1018 | + |
1019 | + |
1020 | +if __name__ == "__main__": |
1021 | + setup(name=NAME, |
1022 | + version=VERSION, |
1023 | + author=AUTHOR, |
1024 | + author_email=EMAIL, |
1025 | + url=PROJECT_URL, |
1026 | + license=LICENSE, |
1027 | + description=SUMMARY, |
1028 | + long_description=DESCRIPTION, |
1029 | + packages=PACKAGES, |
1030 | + |
1031 | + # for distutils |
1032 | + requires=DEPS, |
1033 | + |
1034 | + # for setuptools |
1035 | + install_requires=DEPS, |
1036 | + ) |