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