Merge lp:~ericsnowcurrently/fake-juju/python-lib-classes into lp:~landscape/fake-juju/trunk-old

Proposed by Eric Snow
Status: Merged
Approved by: Eric Snow
Approved revision: 53
Merged at revision: 39
Proposed branch: lp:~ericsnowcurrently/fake-juju/python-lib-classes
Merge into: lp:~landscape/fake-juju/trunk-old
Prerequisite: lp:~ericsnowcurrently/fake-juju/python-lib-helpers
Diff against target: 479 lines (+427/-1)
5 files modified
python/fakejuju/__init__.py (+8/-0)
python/fakejuju/failures.py (+65/-0)
python/fakejuju/fakejuju.py (+100/-0)
python/fakejuju/tests/test_failures.py (+101/-0)
python/fakejuju/tests/test_fakejuju.py (+153/-1)
To merge this branch: bzr merge lp:~ericsnowcurrently/fake-juju/python-lib-classes
Reviewer Review Type Date Requested Status
🤖 Landscape Builder test results Approve
Free Ekanayaka (community) Approve
Review via email: mp+307895@code.launchpad.net

This proposal supersedes a proposal from 2016-10-06.

Commit message

Add the FakeJuju and Failures classes.

Description of the change

Add the FakeJuju and Failures classes.

Testing instructions:

Run the unit tests.

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 48
Branch: lp:~ericsnowcurrently/fake-juju/python-lib-classes
Jenkins: https://ci.lscape.net/job/latch-test-xenial/17/

review: Approve (test results)
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Looks mostly good to me, marking as N/I just to see what you think of the comments, but I'll be also happy to approve this MP as it is, in case.

review: Needs Information
Revision history for this message
Eric Snow (ericsnowcurrently) :
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

+1 with a comment about YAGNI-ing the logsdir parameter.

review: Approve
49. By Eric Snow

Merge from parent.

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 49
Branch: lp:~ericsnowcurrently/fake-juju/python-lib-classes
Jenkins: https://ci.lscape.net/job/latch-test-xenial/20/

review: Approve (test results)
50. By Eric Snow

Failures() doesn' do any coercion.

51. By Eric Snow

FakeJuju() doesn't do any coercion.

52. By Eric Snow

Do not use namedtuple for FakeJuju.

53. By Eric Snow

Fix a docstring.

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 53
Branch: lp:~ericsnowcurrently/fake-juju/python-lib-classes
Jenkins: https://ci.lscape.net/job/latch-test-xenial/72/

review: Approve (test results)
Revision history for this message
Eric Snow (ericsnowcurrently) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'python/fakejuju/__init__.py'
2--- python/fakejuju/__init__.py 2016-10-13 18:20:43 +0000
3+++ python/fakejuju/__init__.py 2016-10-17 15:55:46 +0000
4@@ -45,6 +45,14 @@
5
6 """
7
8+from .fakejuju import get_filename, set_envvars, FakeJuju
9+
10+
11+__all__ = [
12+ "__version__",
13+ "get_bootstrap_spec", "get_filename", "set_envvars",
14+ "FakeJuju",
15+ ]
16
17 __version__ = "0.9.0b1"
18
19
20=== added file 'python/fakejuju/failures.py'
21--- python/fakejuju/failures.py 1970-01-01 00:00:00 +0000
22+++ python/fakejuju/failures.py 2016-10-17 15:55:46 +0000
23@@ -0,0 +1,65 @@
24+# Copyright 2016 Canonical Limited. All rights reserved.
25+
26+import errno
27+import os
28+import os.path
29+
30+
31+class Failures(object):
32+ """The collection of injected failures to use with a fake-juju.
33+
34+ The failures are tracked here as well as injected into any
35+ fake-juju using the initial config dir (aka "juju home").
36+
37+ Note that fake-juju provides only limited capability for
38+ failure injection.
39+ """
40+
41+ def __init__(self, cfgdir, entities=None):
42+ """
43+ @param cfgdir: The "juju home" directory into which the
44+ failures will be registered for injection.
45+ @param entities: The entity names to start with, if any.
46+ """
47+ filename = os.path.join(cfgdir, "juju-failures")
48+ entities = set(entities or ())
49+
50+ self._filename = filename
51+ self._entities = entities
52+
53+ @property
54+ def filename(self):
55+ """The path to the failures file the fake-juju reads."""
56+ return self._filename
57+
58+ @property
59+ def entities(self):
60+ """The IDs of the failing entities."""
61+ return set(self._entities)
62+
63+ def _flush(self):
64+ """Write the failures to disk."""
65+ data = "\n".join(self._entities) + "\n"
66+ try:
67+ file = open(self._filename, "w")
68+ except IOError:
69+ dirname = os.path.dirname(self._filename)
70+ if not os.path.exists(dirname):
71+ os.makedirs(dirname)
72+ file = open(self._filename, "w")
73+ with file:
74+ file.write(data)
75+
76+ def fail_entity(self, tag):
77+ """Inject a global failure for the identified Juju entity."""
78+ self._entities.add(tag)
79+ self._flush()
80+
81+ def clear(self):
82+ """Remove all injected failures."""
83+ try:
84+ os.remove(self._filename)
85+ except OSError as e:
86+ if e.errno != errno.ENOENT:
87+ raise
88+ self._entities.clear()
89
90=== modified file 'python/fakejuju/fakejuju.py'
91--- python/fakejuju/fakejuju.py 2016-10-13 18:20:43 +0000
92+++ python/fakejuju/fakejuju.py 2016-10-17 15:55:46 +0000
93@@ -2,6 +2,10 @@
94
95 import os.path
96
97+import txjuju.cli
98+
99+from .failures import Failures
100+
101
102 def get_filename(version, bindir=None):
103 """Return the full path to the fake-juju binary for the given version.
104@@ -30,3 +34,99 @@
105 """
106 envvars["FAKE_JUJU_FAILURES"] = failures_filename or ""
107 envvars["FAKE_JUJU_LOGS_DIR"] = logsdir or ""
108+
109+
110+class FakeJuju(object):
111+ """The fundamental details for fake-juju."""
112+
113+ @classmethod
114+ def from_version(cls, version, cfgdir,
115+ logsdir=None, failuresdir=None, bindir=None):
116+ """Return a new instance given the provided information.
117+
118+ @param version: The Juju version to fake.
119+ @param cfgdir: The "juju home" directory to use.
120+ @param logsdir: The directory where logs will be written.
121+ This defaults to cfgdir.
122+ @params failuresdir: The directory where failure injection
123+ is managed.
124+ @param bindir: The directory containing the fake-juju binary.
125+ This defaults to /usr/bin.
126+ """
127+ if logsdir is None:
128+ logsdir = cfgdir
129+ if failuresdir is None:
130+ failuresdir = cfgdir
131+ filename = get_filename(version, bindir=bindir)
132+ failures = Failures(failuresdir)
133+ return cls(filename, version, cfgdir, logsdir, failures)
134+
135+ def __init__(self, filename, version, cfgdir, logsdir=None, failures=None):
136+ """
137+ @param filename: The path to the fake-juju binary.
138+ @param version: The Juju version to fake.
139+ @param cfgdir: The "juju home" directory to use.
140+ @param logsdir: The directory where logs will be written.
141+ This defaults to cfgdir.
142+ @param failures: The set of fake-juju failures to use.
143+ """
144+ logsdir = logsdir if logsdir is not None else cfgdir
145+ if failures is None and cfgdir:
146+ failures = Failures(cfgdir)
147+
148+ if not filename:
149+ raise ValueError("missing filename")
150+ if not version:
151+ raise ValueError("missing version")
152+ if not cfgdir:
153+ raise ValueError("missing cfgdir")
154+ if not logsdir:
155+ raise ValueError("missing logsdir")
156+ if failures is None:
157+ raise ValueError("missing failures")
158+
159+ self.filename = filename
160+ self.version = version
161+ self.cfgdir = cfgdir
162+ self.logsdir = logsdir
163+ self.failures = failures
164+
165+ @property
166+ def logfile(self):
167+ """The path to fake-juju's log file."""
168+ return os.path.join(self.logsdir, "fake-juju.log")
169+
170+ @property
171+ def infofile(self):
172+ """The path to fake-juju's data cache."""
173+ return os.path.join(self.cfgdir, "fakejuju")
174+
175+ @property
176+ def fifo(self):
177+ """The path to the fifo file that triggers shutdown."""
178+ return os.path.join(self.cfgdir, "fifo")
179+
180+ @property
181+ def cacertfile(self):
182+ """The path to the API server's certificate."""
183+ return os.path.join(self.cfgdir, "cert.ca")
184+
185+ def cli(self, envvars=None):
186+ """Return the txjuju.cli.CLI for this fake-juju.
187+
188+ Currently fake-juju supports only the following juju subcommands:
189+
190+ * bootstrap
191+ Not that this only supports the dummy provider and the local
192+ system is only minimally impacted.
193+ * api-info
194+ Note that passwords are always omited, even if requested.
195+ * api-endpoints
196+ * destroy-environment
197+ """
198+ if envvars is None:
199+ envvars = os.environ
200+ envvars = dict(envvars)
201+ set_envvars(envvars, self.failures._filename, self.logsdir)
202+ return txjuju.cli.CLI.from_version(
203+ self.filename, self.version, self.cfgdir, envvars)
204
205=== added file 'python/fakejuju/tests/test_failures.py'
206--- python/fakejuju/tests/test_failures.py 1970-01-01 00:00:00 +0000
207+++ python/fakejuju/tests/test_failures.py 2016-10-17 15:55:46 +0000
208@@ -0,0 +1,101 @@
209+# Copyright 2016 Canonical Limited. All rights reserved.
210+
211+import os
212+import os.path
213+import shutil
214+import tempfile
215+import unittest
216+
217+from fakejuju.failures import Failures
218+
219+
220+class FailuresTests(unittest.TestCase):
221+
222+ def setUp(self):
223+ super(FailuresTests, self).setUp()
224+ self.dirname = tempfile.mkdtemp(prefix="fakejuju-test-")
225+
226+ def tearDown(self):
227+ shutil.rmtree(self.dirname)
228+ super(FailuresTests, self).tearDown()
229+
230+ def test_full(self):
231+ """Failures() works correctly when given all args."""
232+ entities = [u"x", u"y", u"z"]
233+ failures = Failures(u"/some/dir", entities)
234+
235+ self.assertEqual(failures.filename, u"/some/dir/juju-failures")
236+ self.assertEqual(failures.entities, set(entities))
237+
238+ def test_minimal(self):
239+ """Failures() works correctly when given minimal args."""
240+ failures = Failures(u"/some/dir")
241+
242+ self.assertEqual(failures.filename, u"/some/dir/juju-failures")
243+ self.assertEqual(failures.entities, set())
244+
245+ def test_conversion(self):
246+ """Failures() doesn't convert any values."""
247+ failures_str = Failures("/some/dir", ["x", "y", "z"])
248+ failures_unicode = Failures(u"/some/dir", [u"x", u"y", u"z"])
249+
250+ self.assertIsInstance(failures_str.filename, str)
251+ self.assertIsInstance(failures_unicode.filename, unicode)
252+ for id in failures_str.entities:
253+ self.assertIsInstance(id, str)
254+ for id in failures_unicode.entities:
255+ self.assertIsInstance(id, unicode)
256+
257+ def test_file_not_created_initially(self):
258+ """Failures() doesn't create a missing cfgdir until necessary."""
259+ failures = Failures(self.dirname)
260+
261+ self.assertFalse(os.path.exists(failures.filename))
262+
263+ def test_cfgdir_created(self):
264+ """Failures() creates a missing cfgdir as soon as it's needed."""
265+ dirname = os.path.join(self.dirname, "subdir")
266+ self.assertFalse(os.path.exists(dirname))
267+ failures = Failures(dirname)
268+ failures.fail_entity("unit-xyz")
269+
270+ self.assertTrue(os.path.exists(dirname))
271+
272+ def test_fail_entity_one(self):
273+ """Failures,fail_entity() writes an initial entry to disk."""
274+ failures = Failures(self.dirname)
275+ failures.fail_entity("unit-abc")
276+ with open(failures.filename) as file:
277+ data = file.read()
278+
279+ self.assertEqual(data, "unit-abc\n")
280+
281+ def test_fail_entity_multiple(self):
282+ """Failures.fail_entity() correctly writes multiple entries to disk."""
283+ failures = Failures(self.dirname)
284+ failures.fail_entity("unit-abc")
285+ failures.fail_entity("unit-xyz")
286+
287+ with open(failures.filename) as file:
288+ data = file.read()
289+ entities = set(tag for tag in data.splitlines() if tag)
290+ self.assertEqual(entities, failures.entities)
291+ self.assertTrue(data.endswith("\n"))
292+
293+ def test_clear_exists(self):
294+ """Failures.clear() deletes the failures file if it exists."""
295+ failures = Failures(self.dirname)
296+ failures.fail_entity("unit-abc")
297+ self.assertTrue(os.path.exists(failures.filename))
298+ failures.clear()
299+
300+ self.assertFalse(os.path.exists(failures.filename))
301+ self.assertEqual(failures.entities, set())
302+
303+ def test_clear_not_exists(self):
304+ """Failures.clear() does nothing if the failures file is missing."""
305+ failures = Failures(self.dirname)
306+ self.assertFalse(os.path.exists(failures.filename))
307+ failures.clear()
308+
309+ self.assertFalse(os.path.exists(failures.filename))
310
311=== modified file 'python/fakejuju/tests/test_fakejuju.py'
312--- python/fakejuju/tests/test_fakejuju.py 2016-10-13 18:25:28 +0000
313+++ python/fakejuju/tests/test_fakejuju.py 2016-10-17 15:55:46 +0000
314@@ -1,8 +1,13 @@
315 # Copyright 2016 Canonical Limited. All rights reserved.
316
317+import os
318 import unittest
319
320-from fakejuju.fakejuju import get_filename, set_envvars
321+from txjuju import _juju1, _juju2
322+from txjuju._utils import Executable
323+
324+from fakejuju.failures import Failures
325+from fakejuju.fakejuju import get_filename, set_envvars, FakeJuju
326
327
328 class GetFilenameTests(unittest.TestCase):
329@@ -115,3 +120,150 @@
330 "FAKE_JUJU_FAILURES": "",
331 "FAKE_JUJU_LOGS_DIR": "",
332 })
333+
334+
335+class FakeJujuTests(unittest.TestCase):
336+
337+ def test_from_version_full(self):
338+ """FakeJuju.from_version() works correctly when given all args."""
339+ juju = FakeJuju.from_version(
340+ "1.25.6", "/a/juju/home", "/logs/dir", "/failures/dir", "/bin/dir")
341+
342+ self.assertEqual(juju.filename, "/bin/dir/fake-juju-1.25.6")
343+ self.assertEqual(juju.version, "1.25.6")
344+ self.assertEqual(juju.cfgdir, "/a/juju/home")
345+ self.assertEqual(juju.logsdir, "/logs/dir")
346+ self.assertEqual(juju.failures.filename, "/failures/dir/juju-failures")
347+
348+ def test_from_version_minimal(self):
349+ """FakeJuju.from_version() works correctly when given minimal args."""
350+ juju = FakeJuju.from_version("1.25.6", "/my/juju/home")
351+
352+ self.assertEqual(juju.filename, "/usr/bin/fake-juju-1.25.6")
353+ self.assertEqual(juju.version, "1.25.6")
354+ self.assertEqual(juju.cfgdir, "/my/juju/home")
355+ self.assertEqual(juju.logsdir, "/my/juju/home")
356+ self.assertEqual(juju.failures.filename, "/my/juju/home/juju-failures")
357+
358+ def test_full(self):
359+ """FakeJuju() works correctly when given all args."""
360+ cfgdir = "/my/juju/home"
361+ failures = Failures(cfgdir)
362+ juju = FakeJuju("/fake-juju", "1.25.6", cfgdir, "/some/logs", failures)
363+
364+ self.assertEqual(juju.filename, "/fake-juju")
365+ self.assertEqual(juju.version, "1.25.6")
366+ self.assertEqual(juju.cfgdir, cfgdir)
367+ self.assertEqual(juju.logsdir, "/some/logs")
368+ self.assertIs(juju.failures, failures)
369+
370+ def test_minimal(self):
371+ """FakeJuju() works correctly when given minimal args."""
372+ juju = FakeJuju("/fake-juju", "1.25.6", "/my/juju/home")
373+
374+ self.assertEqual(juju.filename, "/fake-juju")
375+ self.assertEqual(juju.version, "1.25.6")
376+ self.assertEqual(juju.cfgdir, "/my/juju/home")
377+ self.assertEqual(juju.logsdir, "/my/juju/home")
378+ self.assertEqual(juju.failures.filename, "/my/juju/home/juju-failures")
379+
380+ def test_conversions(self):
381+ """FakeJuju() doesn't convert the type of any value."""
382+ juju_str = FakeJuju(
383+ "/fake-juju", "1.25.6", "/x", "/y", Failures("/..."))
384+ juju_unicode = FakeJuju(
385+ u"/fake-juju", u"1.25.6", u"/x", u"/y", Failures(u"/..."))
386+
387+ for name in ('filename version cfgdir logsdir'.split()):
388+ self.assertIsInstance(getattr(juju_str, name), str)
389+ self.assertIsInstance(getattr(juju_unicode, name), unicode)
390+
391+ def test_missing_filename(self):
392+ """FakeJuju() fails if filename is None or empty."""
393+ with self.assertRaises(ValueError):
394+ FakeJuju(None, "1.25.6", "/my/juju/home")
395+ with self.assertRaises(ValueError):
396+ FakeJuju("", "1.25.6", "/my/juju/home")
397+
398+ def test_missing_version(self):
399+ """FakeJuju() fails if version is None or empty."""
400+ with self.assertRaises(ValueError):
401+ FakeJuju("/fake-juju", None, "/my/juju/home")
402+ with self.assertRaises(ValueError):
403+ FakeJuju("/fake-juju", "", "/my/juju/home")
404+
405+ def test_missing_cfgdir(self):
406+ """FakeJuju() fails if cfgdir is None or empty."""
407+ with self.assertRaises(ValueError):
408+ FakeJuju("/fake-juju", "1.25.6", None)
409+ with self.assertRaises(ValueError):
410+ FakeJuju("/fake-juju", "1.25.6", "")
411+
412+ def test_logfile(self):
413+ """FakeJuju.logfile returns the path to the fake-juju log file."""
414+ juju = FakeJuju("/fake-juju", "1.25.6", "/x", "/some/logs")
415+
416+ self.assertEqual(juju.logfile, "/some/logs/fake-juju.log")
417+
418+ def test_infofile(self):
419+ """FakeJuju.logfile returns the path to the fake-juju info file."""
420+ juju = FakeJuju("/fake-juju", "1.25.6", "/x")
421+
422+ self.assertEqual(juju.infofile, "/x/fakejuju")
423+
424+ def test_fifo(self):
425+ """FakeJuju.logfile returns the path to the fake-juju fifo."""
426+ juju = FakeJuju("/fake-juju", "1.25.6", "/x")
427+
428+ self.assertEqual(juju.fifo, "/x/fifo")
429+
430+ def test_cacertfile(self):
431+ """FakeJuju.cacertfile returns the path to the Juju API cert."""
432+ juju = FakeJuju("/fake-juju", "1.25.6", "/x")
433+
434+ self.assertEqual(juju.cacertfile, "/x/cert.ca")
435+
436+ def test_cli_full(self):
437+ """FakeJuju.cli() works correctly when given all args."""
438+ juju = FakeJuju("/fake-juju", "1.25.6", "/x")
439+ cli = juju.cli({"SPAM": "eggs"})
440+
441+ self.assertEqual(
442+ cli._exe,
443+ Executable("/fake-juju", {
444+ "SPAM": "eggs",
445+ "FAKE_JUJU_FAILURES": "/x/juju-failures",
446+ "FAKE_JUJU_LOGS_DIR": "/x",
447+ "JUJU_HOME": "/x",
448+ }),
449+ )
450+
451+ def test_cli_minimal(self):
452+ """FakeJuju.cli() works correctly when given minimal args."""
453+ juju = FakeJuju("/fake-juju", "1.25.6", "/x")
454+ cli = juju.cli()
455+
456+ self.assertEqual(
457+ cli._exe,
458+ Executable("/fake-juju", dict(os.environ, **{
459+ "FAKE_JUJU_FAILURES": "/x/juju-failures",
460+ "FAKE_JUJU_LOGS_DIR": "/x",
461+ "JUJU_HOME": "/x",
462+ })),
463+ )
464+
465+ def test_cli_juju1(self):
466+ """FakeJuju.cli() works correctly for Juju 1.x."""
467+ juju = FakeJuju.from_version("1.25.6", "/x")
468+ cli = juju.cli()
469+
470+ self.assertEqual(cli._exe.envvars["JUJU_HOME"], "/x")
471+ self.assertIsInstance(cli._juju, _juju1.CLIHooks)
472+
473+ def test_cli_juju2(self):
474+ """FakeJuju.cli() works correctly for Juju 2.x."""
475+ juju = FakeJuju.from_version("2.0.0", "/x")
476+ cli = juju.cli()
477+
478+ self.assertEqual(cli._exe.envvars["JUJU_DATA"], "/x")
479+ self.assertIsInstance(cli._juju, _juju2.CLIHooks)

Subscribers

People subscribed via source and target branches

to all changes: