Merge lp:~ericsnowcurrently/fake-juju/python-lib-testing into lp:~landscape/fake-juju/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
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.

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

Subscribers

People subscribed via source and target branches

to all changes: