Squashed 'modules/libjuju/' content from commit c50c361
git-subtree-dir: modules/libjuju
git-subtree-split: c50c361a8b9a3bbf1a33f5659e492b481f065cd2
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/unit/__init__.py
diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py
new file mode 100644
index 0000000..7828cf3
--- /dev/null
+++ b/tests/unit/test_client.py
@@ -0,0 +1,25 @@
+"""
+Tests for generated client code
+
+"""
+
+import mock
+import pytest
+
+
+from juju.client import client
+
+
+
+def test_basics():
+ assert client.CLIENTS
+ for i in range(1,5): # Assert versions 1-4 in client dict
+ assert str(i) in client.CLIENTS
+
+
+def test_from_connection():
+ connection = mock.Mock()
+ connection.facades = {"Action": 2}
+ action_facade = client.ActionFacade.from_connection(connection)
+
+
diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py
new file mode 100644
index 0000000..f69b8d6
--- /dev/null
+++ b/tests/unit/test_connection.py
@@ -0,0 +1,54 @@
+import asyncio
+import json
+import mock
+import pytest
+from collections import deque
+
+from websockets.exceptions import ConnectionClosed
+
+from .. import base
+from juju.client.connection import Connection
+
+
+class WebsocketMock:
+ def __init__(self, responses):
+ super().__init__()
+ self.responses = deque(responses)
+ self.open = True
+
+ async def send(self, message):
+ pass
+
+ async def recv(self):
+ if not self.responses:
+ await asyncio.sleep(1) # delay to give test time to finish
+ raise ConnectionClosed(0, 'ran out of responses')
+ return json.dumps(self.responses.popleft())
+
+ async def close(self):
+ self.open = False
+
+
+@pytest.mark.asyncio
+async def test_out_of_order(event_loop):
+ con = Connection(*[None]*4)
+ ws = WebsocketMock([
+ {'request-id': 1},
+ {'request-id': 3},
+ {'request-id': 2},
+ ])
+ expected_responses = [
+ {'request-id': 1},
+ {'request-id': 2},
+ {'request-id': 3},
+ ]
+ con._get_sll = mock.MagicMock()
+ try:
+ with mock.patch('websockets.connect', base.AsyncMock(return_value=ws)):
+ await con.open()
+ actual_responses = []
+ for i in range(3):
+ actual_responses.append(await con.rpc({'version': 1}))
+ assert actual_responses == expected_responses
+ finally:
+ await con.close()
diff --git a/tests/unit/test_constraints.py b/tests/unit/test_constraints.py
new file mode 100644
index 0000000..cb9d773
--- /dev/null
+++ b/tests/unit/test_constraints.py
@@ -0,0 +1,47 @@
+#
+# Test our constraints parser
+#
+
+import unittest
+
+from juju import constraints
+
+class TestConstraints(unittest.TestCase):
+
+ def test_mem_regex(self):
+ m = constraints.MEM
+ self.assertTrue(m.match("10G"))
+ self.assertTrue(m.match("1G"))
+ self.assertFalse(m.match("1Gb"))
+ self.assertFalse(m.match("a1G"))
+ self.assertFalse(m.match("1000"))
+
+ def test_normalize_key(self):
+ _ = constraints.normalize_key
+
+ self.assertEqual(_("test-key"), "test_key")
+ self.assertEqual(_("test-key "), "test_key")
+ self.assertEqual(_(" test-key"), "test_key")
+ self.assertEqual(_("TestKey"), "test_key")
+ self.assertEqual(_("testKey"), "test_key")
+
+ def test_normalize_val(self):
+ _ = constraints.normalize_value
+
+ self.assertEqual(_("10G"), 10 * 1024)
+ self.assertEqual(_("10M"), 10)
+ self.assertEqual(_("10"), 10)
+ self.assertEqual(_("foo,bar"), ["foo", "bar"])
+
+ def test_parse_constraints(self):
+ _ = constraints.parse
+
+ self.assertEqual(
+ _("mem=10G"),
+ {"mem": 10 * 1024}
+ )
+
+ self.assertEqual(
+ _("mem=10G foo=bar,baz"),
+ {"mem": 10 * 1024, "foo": ["bar", "baz"]}
+ )
diff --git a/tests/unit/test_loop.py b/tests/unit/test_loop.py
new file mode 100644
index 0000000..f12368e
--- /dev/null
+++ b/tests/unit/test_loop.py
@@ -0,0 +1,30 @@
+import asyncio
+import unittest
+import juju.loop
+
+
+class TestLoop(unittest.TestCase):
+ def setUp(self):
+ # new event loop for each test
+ policy = asyncio.get_event_loop_policy()
+ self.loop = policy.new_event_loop()
+ policy.set_event_loop(self.loop)
+
+ def tearDown(self):
+ self.loop.close()
+
+ def test_run(self):
+ assert asyncio.get_event_loop() == self.loop
+ async def _test():
+ return 'success'
+ self.assertEqual(juju.loop.run(_test()), 'success')
+
+ def test_run_interrupt(self):
+ async def _test():
+ juju.loop.run._sigint = True
+ self.assertRaises(KeyboardInterrupt, juju.loop.run, _test())
+
+ def test_run_exception(self):
+ async def _test():
+ raise ValueError()
+ self.assertRaises(ValueError, juju.loop.run, _test())
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
new file mode 100644
index 0000000..222d881
--- /dev/null
+++ b/tests/unit/test_model.py
@@ -0,0 +1,155 @@
+import unittest
+
+import mock
+import asynctest
+
+
+def _make_delta(entity, type_, data=None):
+ from juju.client.client import Delta
+ from juju.delta import get_entity_delta
+
+ delta = Delta([entity, type_, data])
+ return get_entity_delta(delta)
+
+
+class TestObserver(unittest.TestCase):
+ def _make_observer(self, *args):
+ from juju.model import _Observer
+ return _Observer(*args)
+
+ def test_cares_about_id(self):
+ id_ = 'foo'
+
+ o = self._make_observer(
+ None, None, None, id_, None)
+
+ delta = _make_delta(
+ 'application', 'change', dict(name=id_))
+
+ self.assertTrue(o.cares_about(delta))
+
+ def test_cares_about_type(self):
+ type_ = 'application'
+
+ o = self._make_observer(
+ None, type_, None, None, None)
+
+ delta = _make_delta(
+ type_, 'change', dict(name='foo'))
+
+ self.assertTrue(o.cares_about(delta))
+
+ def test_cares_about_action(self):
+ action = 'change'
+
+ o = self._make_observer(
+ None, None, action, None, None)
+
+ delta = _make_delta(
+ 'application', action, dict(name='foo'))
+
+ self.assertTrue(o.cares_about(delta))
+
+ def test_cares_about_predicate(self):
+ def predicate(delta):
+ return delta.data.get('fizz') == 'bang'
+
+ o = self._make_observer(
+ None, None, None, None, predicate)
+
+ delta = _make_delta(
+ 'application', 'change', dict(fizz='bang'))
+
+ self.assertTrue(o.cares_about(delta))
+
+
+class TestModelState(unittest.TestCase):
+ def test_apply_delta(self):
+ from juju.model import Model
+ from juju.application import Application
+
+ loop = mock.MagicMock()
+ model = Model(loop=loop)
+ delta = _make_delta('application', 'add', dict(name='foo'))
+
+ # test add
+ prev, new = model.state.apply_delta(delta)
+ self.assertEqual(
+ len(model.state.state[delta.entity][delta.get_id()]), 1)
+ self.assertIsNone(prev)
+ self.assertIsInstance(new, Application)
+
+ # test remove
+ delta.type = 'remove'
+ prev, new = model.state.apply_delta(delta)
+ # length of the entity history deque is now 3:
+ # - 1 for the first delta
+ # - 1 for the second delta
+ # - 1 for the None sentinel appended after the 'remove'
+ self.assertEqual(
+ len(model.state.state[delta.entity][delta.get_id()]), 3)
+ self.assertIsInstance(new, Application)
+ # new object is falsy because its data is None
+ self.assertFalse(new)
+ self.assertIsInstance(prev, Application)
+ self.assertTrue(prev)
+
+
+def test_get_series():
+ from juju.model import Model
+ model = Model()
+ entity = {
+ 'Meta': {
+ 'supported-series': {
+ 'SupportedSeries': [
+ 'xenial',
+ 'trusty',
+ ],
+ },
+ },
+ }
+ assert model._get_series('cs:trusty/ubuntu', entity) == 'trusty'
+ assert model._get_series('xenial/ubuntu', entity) == 'xenial'
+ assert model._get_series('~foo/xenial/ubuntu', entity) == 'xenial'
+ assert model._get_series('~foo/ubuntu', entity) == 'xenial'
+ assert model._get_series('ubuntu', entity) == 'xenial'
+ assert model._get_series('cs:ubuntu', entity) == 'xenial'
+
+
+class TestContextManager(asynctest.TestCase):
+ @asynctest.patch('juju.model.Model.disconnect')
+ @asynctest.patch('juju.model.Model.connect_current')
+ async def test_normal_use(self, mock_connect, mock_disconnect):
+ from juju.model import Model
+
+ async with Model() as model:
+ self.assertTrue(isinstance(model, Model))
+
+ self.assertTrue(mock_connect.called)
+ self.assertTrue(mock_disconnect.called)
+
+ @asynctest.patch('juju.model.Model.disconnect')
+ @asynctest.patch('juju.model.Model.connect_current')
+ async def test_exception(self, mock_connect, mock_disconnect):
+ from juju.model import Model
+
+ class SomeException(Exception):
+ pass
+
+ with self.assertRaises(SomeException):
+ async with Model():
+ raise SomeException()
+
+ self.assertTrue(mock_connect.called)
+ self.assertTrue(mock_disconnect.called)
+
+ @asynctest.patch('juju.client.connection.JujuData.current_controller')
+ async def test_no_current_connection(self, mock_current_controller):
+ from juju.model import Model
+ from juju.errors import JujuConnectionError
+
+ mock_current_controller.return_value = ""
+
+ with self.assertRaises(JujuConnectionError):
+ async with Model():
+ pass
diff --git a/tests/unit/test_overrides.py b/tests/unit/test_overrides.py
new file mode 100644
index 0000000..6485408
--- /dev/null
+++ b/tests/unit/test_overrides.py
@@ -0,0 +1,76 @@
+import pytest
+
+from juju.client.overrides import Number, Binary # noqa
+
+
+# test cases ported from:
+# https://github.com/juju/version/blob/master/version_test.go
+@pytest.mark.parametrize("input,expected", (
+ (None, Number(major=0, minor=0, patch=0, tag='', build=0)),
+ (Number(major=1, minor=0, patch=0), Number(major=1, minor=0, patch=0)),
+ ({'major': 1, 'minor': 0, 'patch': 0}, Number(major=1, minor=0, patch=0)),
+ ("0.0.1", Number(major=0, minor=0, patch=1)),
+ ("0.0.2", Number(major=0, minor=0, patch=2)),
+ ("0.1.0", Number(major=0, minor=1, patch=0)),
+ ("0.2.3", Number(major=0, minor=2, patch=3)),
+ ("1.0.0", Number(major=1, minor=0, patch=0)),
+ ("10.234.3456", Number(major=10, minor=234, patch=3456)),
+ ("10.234.3456.1", Number(major=10, minor=234, patch=3456, build=1)),
+ ("10.234.3456.64", Number(major=10, minor=234, patch=3456, build=64)),
+ ("10.235.3456", Number(major=10, minor=235, patch=3456)),
+ ("1.21-alpha1", Number(major=1, minor=21, patch=1, tag="alpha")),
+ ("1.21-alpha1.1", Number(major=1, minor=21, patch=1, tag="alpha",
+ build=1)),
+ ("1.21-alpha10", Number(major=1, minor=21, patch=10, tag="alpha")),
+ ("1.21.0", Number(major=1, minor=21)),
+ ("1234567890.2.1", TypeError),
+ ("0.2..1", TypeError),
+ ("1.21.alpha1", TypeError),
+ ("1.21-alpha", TypeError),
+ ("1.21-alpha1beta", TypeError),
+ ("1.21-alpha-dev", TypeError),
+ ("1.21-alpha_dev3", TypeError),
+ ("1.21-alpha123dev3", TypeError),
+))
+def test_number(input, expected):
+ if expected is TypeError:
+ with pytest.raises(expected):
+ Number.from_json(input)
+ else:
+ result = Number.from_json(input)
+ assert result == expected
+ if isinstance(input, str):
+ assert result.to_json() == input
+
+
+# test cases ported from:
+# https://github.com/juju/version/blob/master/version_test.go
+@pytest.mark.parametrize("input,expected", (
+ (None, Binary(Number(), None, None)),
+ (Binary(Number(1), 'trusty', 'amd64'), Binary(Number(1),
+ 'trusty', 'amd64')),
+ ({'number': {'major': 1},
+ 'series': 'trusty',
+ 'arch': 'amd64'}, Binary(Number(1), 'trusty', 'amd64')),
+ ("1.2.3-trusty-amd64", Binary(Number(1, 2, 3, "", 0),
+ "trusty", "amd64")),
+ ("1.2.3.4-trusty-amd64", Binary(Number(1, 2, 3, "", 4),
+ "trusty", "amd64")),
+ ("1.2-alpha3-trusty-amd64", Binary(Number(1, 2, 3, "alpha", 0),
+ "trusty", "amd64")),
+ ("1.2-alpha3.4-trusty-amd64", Binary(Number(1, 2, 3, "alpha", 4),
+ "trusty", "amd64")),
+ ("1.2.3", TypeError),
+ ("1.2-beta1", TypeError),
+ ("1.2.3--amd64", TypeError),
+ ("1.2.3-trusty-", TypeError),
+))
+def test_binary(input, expected):
+ if expected is TypeError:
+ with pytest.raises(expected):
+ Binary.from_json(input)
+ else:
+ result = Binary.from_json(input)
+ assert result == expected
+ if isinstance(input, str):
+ assert result.to_json() == input
diff --git a/tests/unit/test_placement.py b/tests/unit/test_placement.py
new file mode 100644
index 0000000..a78a28d
--- /dev/null
+++ b/tests/unit/test_placement.py
@@ -0,0 +1,20 @@
+#
+# Test our placement helper
+#
+
+import unittest
+
+from juju import placement
+from juju.client import client
+
+class TestPlacement(unittest.TestCase):
+
+ def test_parse_both_specified(self):
+ res = placement.parse("foo:bar")
+ self.assertEqual(res[0].scope, "foo")
+ self.assertEqual(res[0].directive, "bar")
+
+ def test_parse_machine(self):
+ res = placement.parse("22")
+ self.assertEqual(res[0].scope, "#")
+ self.assertEqual(res[0].directive, "22")