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
=== modified file 'python/fakejuju/__init__.py'
--- python/fakejuju/__init__.py 2016-10-13 18:20:43 +0000
+++ python/fakejuju/__init__.py 2016-10-17 15:55:46 +0000
@@ -45,6 +45,14 @@
4545
46"""46"""
4747
48from .fakejuju import get_filename, set_envvars, FakeJuju
49
50
51__all__ = [
52 "__version__",
53 "get_bootstrap_spec", "get_filename", "set_envvars",
54 "FakeJuju",
55 ]
4856
49__version__ = "0.9.0b1"57__version__ = "0.9.0b1"
5058
5159
=== added file 'python/fakejuju/failures.py'
--- python/fakejuju/failures.py 1970-01-01 00:00:00 +0000
+++ python/fakejuju/failures.py 2016-10-17 15:55:46 +0000
@@ -0,0 +1,65 @@
1# Copyright 2016 Canonical Limited. All rights reserved.
2
3import errno
4import os
5import os.path
6
7
8class Failures(object):
9 """The collection of injected failures to use with a fake-juju.
10
11 The failures are tracked here as well as injected into any
12 fake-juju using the initial config dir (aka "juju home").
13
14 Note that fake-juju provides only limited capability for
15 failure injection.
16 """
17
18 def __init__(self, cfgdir, entities=None):
19 """
20 @param cfgdir: The "juju home" directory into which the
21 failures will be registered for injection.
22 @param entities: The entity names to start with, if any.
23 """
24 filename = os.path.join(cfgdir, "juju-failures")
25 entities = set(entities or ())
26
27 self._filename = filename
28 self._entities = entities
29
30 @property
31 def filename(self):
32 """The path to the failures file the fake-juju reads."""
33 return self._filename
34
35 @property
36 def entities(self):
37 """The IDs of the failing entities."""
38 return set(self._entities)
39
40 def _flush(self):
41 """Write the failures to disk."""
42 data = "\n".join(self._entities) + "\n"
43 try:
44 file = open(self._filename, "w")
45 except IOError:
46 dirname = os.path.dirname(self._filename)
47 if not os.path.exists(dirname):
48 os.makedirs(dirname)
49 file = open(self._filename, "w")
50 with file:
51 file.write(data)
52
53 def fail_entity(self, tag):
54 """Inject a global failure for the identified Juju entity."""
55 self._entities.add(tag)
56 self._flush()
57
58 def clear(self):
59 """Remove all injected failures."""
60 try:
61 os.remove(self._filename)
62 except OSError as e:
63 if e.errno != errno.ENOENT:
64 raise
65 self._entities.clear()
066
=== modified file 'python/fakejuju/fakejuju.py'
--- python/fakejuju/fakejuju.py 2016-10-13 18:20:43 +0000
+++ python/fakejuju/fakejuju.py 2016-10-17 15:55:46 +0000
@@ -2,6 +2,10 @@
22
3import os.path3import os.path
44
5import txjuju.cli
6
7from .failures import Failures
8
59
6def get_filename(version, bindir=None):10def get_filename(version, bindir=None):
7 """Return the full path to the fake-juju binary for the given version.11 """Return the full path to the fake-juju binary for the given version.
@@ -30,3 +34,99 @@
30 """34 """
31 envvars["FAKE_JUJU_FAILURES"] = failures_filename or ""35 envvars["FAKE_JUJU_FAILURES"] = failures_filename or ""
32 envvars["FAKE_JUJU_LOGS_DIR"] = logsdir or ""36 envvars["FAKE_JUJU_LOGS_DIR"] = logsdir or ""
37
38
39class FakeJuju(object):
40 """The fundamental details for fake-juju."""
41
42 @classmethod
43 def from_version(cls, version, cfgdir,
44 logsdir=None, failuresdir=None, bindir=None):
45 """Return a new instance given the provided information.
46
47 @param version: The Juju version to fake.
48 @param cfgdir: The "juju home" directory to use.
49 @param logsdir: The directory where logs will be written.
50 This defaults to cfgdir.
51 @params failuresdir: The directory where failure injection
52 is managed.
53 @param bindir: The directory containing the fake-juju binary.
54 This defaults to /usr/bin.
55 """
56 if logsdir is None:
57 logsdir = cfgdir
58 if failuresdir is None:
59 failuresdir = cfgdir
60 filename = get_filename(version, bindir=bindir)
61 failures = Failures(failuresdir)
62 return cls(filename, version, cfgdir, logsdir, failures)
63
64 def __init__(self, filename, version, cfgdir, logsdir=None, failures=None):
65 """
66 @param filename: The path to the fake-juju binary.
67 @param version: The Juju version to fake.
68 @param cfgdir: The "juju home" directory to use.
69 @param logsdir: The directory where logs will be written.
70 This defaults to cfgdir.
71 @param failures: The set of fake-juju failures to use.
72 """
73 logsdir = logsdir if logsdir is not None else cfgdir
74 if failures is None and cfgdir:
75 failures = Failures(cfgdir)
76
77 if not filename:
78 raise ValueError("missing filename")
79 if not version:
80 raise ValueError("missing version")
81 if not cfgdir:
82 raise ValueError("missing cfgdir")
83 if not logsdir:
84 raise ValueError("missing logsdir")
85 if failures is None:
86 raise ValueError("missing failures")
87
88 self.filename = filename
89 self.version = version
90 self.cfgdir = cfgdir
91 self.logsdir = logsdir
92 self.failures = failures
93
94 @property
95 def logfile(self):
96 """The path to fake-juju's log file."""
97 return os.path.join(self.logsdir, "fake-juju.log")
98
99 @property
100 def infofile(self):
101 """The path to fake-juju's data cache."""
102 return os.path.join(self.cfgdir, "fakejuju")
103
104 @property
105 def fifo(self):
106 """The path to the fifo file that triggers shutdown."""
107 return os.path.join(self.cfgdir, "fifo")
108
109 @property
110 def cacertfile(self):
111 """The path to the API server's certificate."""
112 return os.path.join(self.cfgdir, "cert.ca")
113
114 def cli(self, envvars=None):
115 """Return the txjuju.cli.CLI for this fake-juju.
116
117 Currently fake-juju supports only the following juju subcommands:
118
119 * bootstrap
120 Not that this only supports the dummy provider and the local
121 system is only minimally impacted.
122 * api-info
123 Note that passwords are always omited, even if requested.
124 * api-endpoints
125 * destroy-environment
126 """
127 if envvars is None:
128 envvars = os.environ
129 envvars = dict(envvars)
130 set_envvars(envvars, self.failures._filename, self.logsdir)
131 return txjuju.cli.CLI.from_version(
132 self.filename, self.version, self.cfgdir, envvars)
33133
=== added file 'python/fakejuju/tests/test_failures.py'
--- python/fakejuju/tests/test_failures.py 1970-01-01 00:00:00 +0000
+++ python/fakejuju/tests/test_failures.py 2016-10-17 15:55:46 +0000
@@ -0,0 +1,101 @@
1# Copyright 2016 Canonical Limited. All rights reserved.
2
3import os
4import os.path
5import shutil
6import tempfile
7import unittest
8
9from fakejuju.failures import Failures
10
11
12class FailuresTests(unittest.TestCase):
13
14 def setUp(self):
15 super(FailuresTests, self).setUp()
16 self.dirname = tempfile.mkdtemp(prefix="fakejuju-test-")
17
18 def tearDown(self):
19 shutil.rmtree(self.dirname)
20 super(FailuresTests, self).tearDown()
21
22 def test_full(self):
23 """Failures() works correctly when given all args."""
24 entities = [u"x", u"y", u"z"]
25 failures = Failures(u"/some/dir", entities)
26
27 self.assertEqual(failures.filename, u"/some/dir/juju-failures")
28 self.assertEqual(failures.entities, set(entities))
29
30 def test_minimal(self):
31 """Failures() works correctly when given minimal args."""
32 failures = Failures(u"/some/dir")
33
34 self.assertEqual(failures.filename, u"/some/dir/juju-failures")
35 self.assertEqual(failures.entities, set())
36
37 def test_conversion(self):
38 """Failures() doesn't convert any values."""
39 failures_str = Failures("/some/dir", ["x", "y", "z"])
40 failures_unicode = Failures(u"/some/dir", [u"x", u"y", u"z"])
41
42 self.assertIsInstance(failures_str.filename, str)
43 self.assertIsInstance(failures_unicode.filename, unicode)
44 for id in failures_str.entities:
45 self.assertIsInstance(id, str)
46 for id in failures_unicode.entities:
47 self.assertIsInstance(id, unicode)
48
49 def test_file_not_created_initially(self):
50 """Failures() doesn't create a missing cfgdir until necessary."""
51 failures = Failures(self.dirname)
52
53 self.assertFalse(os.path.exists(failures.filename))
54
55 def test_cfgdir_created(self):
56 """Failures() creates a missing cfgdir as soon as it's needed."""
57 dirname = os.path.join(self.dirname, "subdir")
58 self.assertFalse(os.path.exists(dirname))
59 failures = Failures(dirname)
60 failures.fail_entity("unit-xyz")
61
62 self.assertTrue(os.path.exists(dirname))
63
64 def test_fail_entity_one(self):
65 """Failures,fail_entity() writes an initial entry to disk."""
66 failures = Failures(self.dirname)
67 failures.fail_entity("unit-abc")
68 with open(failures.filename) as file:
69 data = file.read()
70
71 self.assertEqual(data, "unit-abc\n")
72
73 def test_fail_entity_multiple(self):
74 """Failures.fail_entity() correctly writes multiple entries to disk."""
75 failures = Failures(self.dirname)
76 failures.fail_entity("unit-abc")
77 failures.fail_entity("unit-xyz")
78
79 with open(failures.filename) as file:
80 data = file.read()
81 entities = set(tag for tag in data.splitlines() if tag)
82 self.assertEqual(entities, failures.entities)
83 self.assertTrue(data.endswith("\n"))
84
85 def test_clear_exists(self):
86 """Failures.clear() deletes the failures file if it exists."""
87 failures = Failures(self.dirname)
88 failures.fail_entity("unit-abc")
89 self.assertTrue(os.path.exists(failures.filename))
90 failures.clear()
91
92 self.assertFalse(os.path.exists(failures.filename))
93 self.assertEqual(failures.entities, set())
94
95 def test_clear_not_exists(self):
96 """Failures.clear() does nothing if the failures file is missing."""
97 failures = Failures(self.dirname)
98 self.assertFalse(os.path.exists(failures.filename))
99 failures.clear()
100
101 self.assertFalse(os.path.exists(failures.filename))
0102
=== modified file 'python/fakejuju/tests/test_fakejuju.py'
--- python/fakejuju/tests/test_fakejuju.py 2016-10-13 18:25:28 +0000
+++ python/fakejuju/tests/test_fakejuju.py 2016-10-17 15:55:46 +0000
@@ -1,8 +1,13 @@
1# Copyright 2016 Canonical Limited. All rights reserved.1# Copyright 2016 Canonical Limited. All rights reserved.
22
3import os
3import unittest4import unittest
45
5from fakejuju.fakejuju import get_filename, set_envvars6from txjuju import _juju1, _juju2
7from txjuju._utils import Executable
8
9from fakejuju.failures import Failures
10from fakejuju.fakejuju import get_filename, set_envvars, FakeJuju
611
712
8class GetFilenameTests(unittest.TestCase):13class GetFilenameTests(unittest.TestCase):
@@ -115,3 +120,150 @@
115 "FAKE_JUJU_FAILURES": "",120 "FAKE_JUJU_FAILURES": "",
116 "FAKE_JUJU_LOGS_DIR": "",121 "FAKE_JUJU_LOGS_DIR": "",
117 })122 })
123
124
125class FakeJujuTests(unittest.TestCase):
126
127 def test_from_version_full(self):
128 """FakeJuju.from_version() works correctly when given all args."""
129 juju = FakeJuju.from_version(
130 "1.25.6", "/a/juju/home", "/logs/dir", "/failures/dir", "/bin/dir")
131
132 self.assertEqual(juju.filename, "/bin/dir/fake-juju-1.25.6")
133 self.assertEqual(juju.version, "1.25.6")
134 self.assertEqual(juju.cfgdir, "/a/juju/home")
135 self.assertEqual(juju.logsdir, "/logs/dir")
136 self.assertEqual(juju.failures.filename, "/failures/dir/juju-failures")
137
138 def test_from_version_minimal(self):
139 """FakeJuju.from_version() works correctly when given minimal args."""
140 juju = FakeJuju.from_version("1.25.6", "/my/juju/home")
141
142 self.assertEqual(juju.filename, "/usr/bin/fake-juju-1.25.6")
143 self.assertEqual(juju.version, "1.25.6")
144 self.assertEqual(juju.cfgdir, "/my/juju/home")
145 self.assertEqual(juju.logsdir, "/my/juju/home")
146 self.assertEqual(juju.failures.filename, "/my/juju/home/juju-failures")
147
148 def test_full(self):
149 """FakeJuju() works correctly when given all args."""
150 cfgdir = "/my/juju/home"
151 failures = Failures(cfgdir)
152 juju = FakeJuju("/fake-juju", "1.25.6", cfgdir, "/some/logs", failures)
153
154 self.assertEqual(juju.filename, "/fake-juju")
155 self.assertEqual(juju.version, "1.25.6")
156 self.assertEqual(juju.cfgdir, cfgdir)
157 self.assertEqual(juju.logsdir, "/some/logs")
158 self.assertIs(juju.failures, failures)
159
160 def test_minimal(self):
161 """FakeJuju() works correctly when given minimal args."""
162 juju = FakeJuju("/fake-juju", "1.25.6", "/my/juju/home")
163
164 self.assertEqual(juju.filename, "/fake-juju")
165 self.assertEqual(juju.version, "1.25.6")
166 self.assertEqual(juju.cfgdir, "/my/juju/home")
167 self.assertEqual(juju.logsdir, "/my/juju/home")
168 self.assertEqual(juju.failures.filename, "/my/juju/home/juju-failures")
169
170 def test_conversions(self):
171 """FakeJuju() doesn't convert the type of any value."""
172 juju_str = FakeJuju(
173 "/fake-juju", "1.25.6", "/x", "/y", Failures("/..."))
174 juju_unicode = FakeJuju(
175 u"/fake-juju", u"1.25.6", u"/x", u"/y", Failures(u"/..."))
176
177 for name in ('filename version cfgdir logsdir'.split()):
178 self.assertIsInstance(getattr(juju_str, name), str)
179 self.assertIsInstance(getattr(juju_unicode, name), unicode)
180
181 def test_missing_filename(self):
182 """FakeJuju() fails if filename is None or empty."""
183 with self.assertRaises(ValueError):
184 FakeJuju(None, "1.25.6", "/my/juju/home")
185 with self.assertRaises(ValueError):
186 FakeJuju("", "1.25.6", "/my/juju/home")
187
188 def test_missing_version(self):
189 """FakeJuju() fails if version is None or empty."""
190 with self.assertRaises(ValueError):
191 FakeJuju("/fake-juju", None, "/my/juju/home")
192 with self.assertRaises(ValueError):
193 FakeJuju("/fake-juju", "", "/my/juju/home")
194
195 def test_missing_cfgdir(self):
196 """FakeJuju() fails if cfgdir is None or empty."""
197 with self.assertRaises(ValueError):
198 FakeJuju("/fake-juju", "1.25.6", None)
199 with self.assertRaises(ValueError):
200 FakeJuju("/fake-juju", "1.25.6", "")
201
202 def test_logfile(self):
203 """FakeJuju.logfile returns the path to the fake-juju log file."""
204 juju = FakeJuju("/fake-juju", "1.25.6", "/x", "/some/logs")
205
206 self.assertEqual(juju.logfile, "/some/logs/fake-juju.log")
207
208 def test_infofile(self):
209 """FakeJuju.logfile returns the path to the fake-juju info file."""
210 juju = FakeJuju("/fake-juju", "1.25.6", "/x")
211
212 self.assertEqual(juju.infofile, "/x/fakejuju")
213
214 def test_fifo(self):
215 """FakeJuju.logfile returns the path to the fake-juju fifo."""
216 juju = FakeJuju("/fake-juju", "1.25.6", "/x")
217
218 self.assertEqual(juju.fifo, "/x/fifo")
219
220 def test_cacertfile(self):
221 """FakeJuju.cacertfile returns the path to the Juju API cert."""
222 juju = FakeJuju("/fake-juju", "1.25.6", "/x")
223
224 self.assertEqual(juju.cacertfile, "/x/cert.ca")
225
226 def test_cli_full(self):
227 """FakeJuju.cli() works correctly when given all args."""
228 juju = FakeJuju("/fake-juju", "1.25.6", "/x")
229 cli = juju.cli({"SPAM": "eggs"})
230
231 self.assertEqual(
232 cli._exe,
233 Executable("/fake-juju", {
234 "SPAM": "eggs",
235 "FAKE_JUJU_FAILURES": "/x/juju-failures",
236 "FAKE_JUJU_LOGS_DIR": "/x",
237 "JUJU_HOME": "/x",
238 }),
239 )
240
241 def test_cli_minimal(self):
242 """FakeJuju.cli() works correctly when given minimal args."""
243 juju = FakeJuju("/fake-juju", "1.25.6", "/x")
244 cli = juju.cli()
245
246 self.assertEqual(
247 cli._exe,
248 Executable("/fake-juju", dict(os.environ, **{
249 "FAKE_JUJU_FAILURES": "/x/juju-failures",
250 "FAKE_JUJU_LOGS_DIR": "/x",
251 "JUJU_HOME": "/x",
252 })),
253 )
254
255 def test_cli_juju1(self):
256 """FakeJuju.cli() works correctly for Juju 1.x."""
257 juju = FakeJuju.from_version("1.25.6", "/x")
258 cli = juju.cli()
259
260 self.assertEqual(cli._exe.envvars["JUJU_HOME"], "/x")
261 self.assertIsInstance(cli._juju, _juju1.CLIHooks)
262
263 def test_cli_juju2(self):
264 """FakeJuju.cli() works correctly for Juju 2.x."""
265 juju = FakeJuju.from_version("2.0.0", "/x")
266 cli = juju.cli()
267
268 self.assertEqual(cli._exe.envvars["JUJU_DATA"], "/x")
269 self.assertIsInstance(cli._juju, _juju2.CLIHooks)

Subscribers

People subscribed via source and target branches

to all changes: