~andreserl/pyjuju/maas_provider_distro_series

« back to all changes in this revision

Viewing changes to juju/hooks/tests/test_invoker.py

  • Committer: Jim Baker
  • Date: 2012-09-10 18:28:40 UTC
  • mfrom: (570.2.7 format-2-raw)
  • Revision ID: jim.baker@canonical.com-20120910182840-9rfts18sbp30id5g
merge format-2-raw [r=hazmat][f=1044632]

Modifies format 2 support so that it supports the use of raw strings.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- encoding: utf-8 -*-
2
2
 
3
3
from StringIO import StringIO
 
4
import base64
4
5
import json
5
6
import logging
6
7
import os
192
193
 
193
194
    def create_hook(self, hook, arguments):
194
195
        bin_path = self.get_cli_hook(hook)
195
 
        fn = self.makeFile("#!/bin/sh\n'%s' %s" % (bin_path, arguments))
 
196
        fn = self.makeFile("#!/bin/bash\n'%s' %s" % (bin_path, arguments))
196
197
        # make the hook executable
197
198
        os.chmod(fn, stat.S_IEXEC | stat.S_IREAD)
198
199
        return fn
1516
1517
        # we don't see units in the other container
1517
1518
        self.assertNotIn("mysql/0", self.log.getvalue())
1518
1519
 
1519
 
 
1520
1520
    @defer.inlineCallbacks
1521
1521
    def test_open_and_close_ports(self):
1522
1522
        """Verify that port hook commands run and changes are immediate."""
1547
1547
            [{"port": 80, "proto": "tcp"},
1548
1548
             {"port": 53, "proto": "udp"}])
1549
1549
 
1550
 
 
1551
1550
        result = yield exe(self.create_hook("close-port", "80/tcp"))
1552
1551
        self.assertEqual(result, 0)
1553
1552
        self.assertEqual(
1554
1553
            (yield unit_state.get_open_ports()),
1555
 
            [{"port": 53, "proto": "udp"} ,])
 
1554
            [{"port": 53, "proto": "udp"}])
1556
1555
        self.assertEqual(
1557
1556
            (yield container_state.get_open_ports()),
1558
 
            [{"port": 53, "proto": "udp"},])
 
1557
            [{"port": 53, "proto": "udp"}])
1559
1558
 
1560
1559
        yield exe.ended
1561
1560
        self.assertLogLines(
1658
1657
            "mysql", charm_name="mysql-format-v2")
1659
1658
        yield super(TestCharmFormatV2, self)._default_relations()
1660
1659
 
 
1660
    def make_zipped_file(self):
 
1661
        data_file = self.makeFile()
 
1662
        with open(data_file, "wb") as f:
 
1663
            # gzipped of 'abc' - however gzip will also includes the
 
1664
            # source file name, so easiest to keep it stable here as
 
1665
            # standard data
 
1666
            f.write("\x1f\x8b\x08\x08\xbb\x8bAP\x02\xfftmpr"
 
1667
                    "fyP0e\x00KLJ\x06\x00\xc2A$5\x03\x00\x00\x00")
 
1668
        return data_file
 
1669
 
1661
1670
    @defer.inlineCallbacks
1662
1671
    def test_environment(self):
1663
1672
        """Ensure that an explicit setting of format: 2 works properly"""
1667
1676
        self.assertEqual(env["_JUJU_CHARM_FORMAT"], "2")
1668
1677
 
1669
1678
    @defer.inlineCallbacks
1670
 
    def test_output(self):
 
1679
    def test_smart_output(self):
1671
1680
        """Verify roundtripping"""
1672
1681
        hook_debug_log = capture_separate_log("hook", level=logging.DEBUG)
1673
1682
        hook_log = capture_separate_log("hook", level=logging.INFO)
1675
1684
            "database:42", "add", "mysql/0", self.relation,
1676
1685
            client_id="client_id")
1677
1686
 
1678
 
        # Byte strings are also supported by staying completely in
1679
 
        # YAML, so test that corner case. This also means users need
1680
 
        # to present valid YAML for any input:
1681
 
        data_file = self.makeFile(
1682
 
            yaml.safe_dump("But when I do drink, I prefer \xCA\xFE"))
 
1687
        # Test the support of raw strings, both from a file and from
 
1688
        # command line. Unicode can also be used - this is just
 
1689
        # rendered as UTF-8 in the shell; the source here is also
 
1690
        # UTF-8 - note it is not a Unicode string, it's a bytestring.
 
1691
        data_file = self.make_zipped_file()
1683
1692
        set_hook = self.create_hook(
1684
1693
            "relation-set",
1685
 
            "b=true i=42 f=1.23 s=ascii u=中文 d=@%s" % data_file)
 
1694
            "b=true f=1.23 i=42 s=ascii u=中文 d=@%s "
 
1695
            "r=\"$(echo -en 'But when I do drink, I prefer \\xCA\\xFE')\"" % (
 
1696
                data_file))
1686
1697
        yield exe(set_hook)
 
1698
 
1687
1699
        result = yield exe(self.create_hook("relation-get", "- mysql/0"))
1688
1700
        self.assertEqual(result, 0)
1689
1701
 
1690
 
        # YAML guarantees that the keys will be sorted
1691
 
        # lexicographically; note that we output UTF-8 for Unicode
1692
 
        # when dumping YAML, so our source text (with this test file
1693
 
        # in UTF-8 itself) matches the output text, as seen in the
1694
 
        # characters for "zhongwen" (Chinese language).
 
1702
        # relation-get - uses YAML to dump keys. YAML guarantees that
 
1703
        # the keys will be sorted lexicographically; note that we
 
1704
        # output UTF-8 for Unicode when dumping YAML, so our source
 
1705
        # text (with this test file in UTF-8 itself) matches the
 
1706
        # output text, as seen in the characters for "zhongwen"
 
1707
        # (Chinese language).
1695
1708
        self.assertEqual(
1696
1709
            hook_log.getvalue(),
1697
 
            "b: true\n"
1698
 
            "d: !!binary |\n    QnV0IHdoZW4gSSBkbyBkcmluaywgSSBwcmVmZXIgyv4=\n"
1699
 
            "f: 1.23\n"
1700
 
            "i: 42\n"
 
1710
            "b: 'true'\n"
 
1711
            "d: !!binary |\n    H4sICLuLQVAC/3RtcHJmeVAwZQBLTEoGAMJBJDUDAAAA\n"
 
1712
            "f: '1.23'\n"
 
1713
            "i: '42'\n"
1701
1714
            "private-address: mysql-0.example.com\n"
 
1715
            "r: !!binary |\n    QnV0IHdoZW4gSSBkbyBkcmluaywgSSBwcmVmZXIgyv4=\n"
1702
1716
            "s: ascii\n"
1703
 
            "u: 中文\n\n")
 
1717
            "u: 中文\n")
1704
1718
 
1705
 
        # Log lines are not simply converted into Unicode, as in v1 format
 
1719
        # Note: backslashes are necessarily doubled here; r"XYZ"
 
1720
        # strings don't help with hexescapes
1706
1721
        self.assertLogLines(
1707
1722
            hook_debug_log.getvalue(),
1708
1723
            ["Flushed values for hook %r on 'database:42'" % (
1709
1724
                    os.path.basename(set_hook),),
1710
 
             "    Setting changed: 'b'=True (was unset)",
1711
 
             "    Setting changed: 'd'='But when I do drink, "
 
1725
             "    Setting changed: 'b'='true' (was unset)",
 
1726
             "    Setting changed: 'd'='\\x1f\\x8b\\x08\\x08\\xbb\\x8bAP\\x02"
 
1727
             "\\xfftmprfyP0e\\x00KLJ\\x06\\x00\\xc2A$5\\x03"
 
1728
             "\\x00\\x00\\x00' (was unset)",
 
1729
             "    Setting changed: 'f'='1.23' (was unset)",
 
1730
             "    Setting changed: 'i'='42' (was unset)",
 
1731
             "    Setting changed: 'r'='But when I do drink, "
1712
1732
             "I prefer \\xca\\xfe' (was unset)",
1713
 
             "    Setting changed: 'f'=1.23 (was unset)",
1714
 
             "    Setting changed: 'i'=42 (was unset)",
1715
1733
             "    Setting changed: 's'='ascii' (was unset)",
1716
 
             "    Setting changed: 'u'=u'\\u4e2d\\u6587' (was unset)"])
1717
 
 
1718
 
        # Also ensure that invalid YAML is rejected; unlike earlier,
1719
 
        # this was not encoded with yaml.safe_dump
1720
 
        data_file = self.makeFile(
1721
 
            "But when I do drink, I prefer \xCA\xFE")
1722
 
        hook = self.create_hook("relation-set", "d=@%s" % data_file)
1723
 
        e = yield self.assertFailure(exe(hook), errors.CharmInvocationError)
1724
 
        self.assertEqual(str(e), "Error processing %r: exit code 1." % hook)
1725
 
        self.assertIn(
1726
 
            "yaml.reader.ReaderError: \'utf8\' codec can\'t decode byte #xca: "
1727
 
            "invalid continuation byte\n  in \"<string>\", position 30",
1728
 
            hook_log.getvalue())
 
1734
             "    Setting changed: 'u'=u'\\u4e2d\\u6587' (was unset)"
 
1735
             ])
 
1736
 
 
1737
    @defer.inlineCallbacks
 
1738
    def test_exact_roundtrip_binary_data(self):
 
1739
        """Verify that binary data, including \x00, is roundtripped exactly"""
 
1740
        hook_log = capture_separate_log("hook", level=logging.INFO)
 
1741
        exe = yield self.ua.get_invoker(
 
1742
            "database:42", "add", "mysql/0", self.relation,
 
1743
            client_id="client_id")
 
1744
        data_file = self.make_zipped_file()
 
1745
 
 
1746
        # relation-set can only read null bytes from a file; bash
 
1747
        # would otherwise silently drop
 
1748
        set_hook = self.create_hook("relation-set", "zipped=@%s" % (
 
1749
                data_file))
 
1750
        result = yield exe(set_hook)
 
1751
        self.assertEqual(result, 0)
 
1752
 
 
1753
        # Abuse the create_hook method a little bit by adding a pipe
 
1754
        get_hook = self.create_hook("relation-get", "zipped mysql/0 | zcat")
 
1755
        result = yield exe(get_hook)
 
1756
        self.assertEqual(result, 0)
 
1757
 
 
1758
        # Using the hook log for this verification does generate one
 
1759
        # extra \n (seen elsewhere in our tests), but this is just
 
1760
        # test noise: we are guaranteed roundtrip fidelity by using
 
1761
        # the picky tool that is zcat - no extraneous data accepted.
 
1762
        self.assertEqual(hook_log.getvalue(), "abc\n")
 
1763
 
 
1764
    @defer.inlineCallbacks
 
1765
    def test_json_output(self):
 
1766
        """Verify roundtripping"""
 
1767
        hook_log = capture_separate_log("hook", level=logging.INFO)
 
1768
        exe = yield self.ua.get_invoker(
 
1769
            "database:42", "add", "mysql/0", self.relation,
 
1770
            client_id="client_id")
 
1771
 
 
1772
        # Test the support of raw strings, both from a file and from
 
1773
        # command line. In addition, test Unicode indirectly by using
 
1774
        # UTF-8. Because the source of this file is marked as UTF-8,
 
1775
        # we can embed such characters directly in bytestrings, not
 
1776
        # just Unicode strings. This also works within the context of
 
1777
        # the shell.
 
1778
        raw = "But when I do drink, I prefer \xca\xfe"
 
1779
        data_file = self.makeFile(raw)
 
1780
        set_hook = self.create_hook(
 
1781
            "relation-set",
 
1782
            "b=true f=1.23 i=42 s=ascii u=中文 d=@%s "
 
1783
            "r=\"$(echo -en 'But when I do drink, I prefer \\xCA\\xFE')\"" % (
 
1784
                data_file,))
 
1785
        yield exe(set_hook)
 
1786
 
 
1787
        result = yield exe(self.create_hook(
 
1788
                "relation-get", "--format=json - mysql/0"))
 
1789
        self.assertEqual(result, 0)
 
1790
 
 
1791
        # YAML serialization internally has converted (transparently)
 
1792
        # UTF-8 to Unicode, which can be rendered by JSON. However the
 
1793
        # "cafe" bytestring is invalid JSON, so verify that it's been
 
1794
        # Base64 encoded.
 
1795
        encoded = base64.b64encode(raw)
 
1796
        self.assertEqual(
 
1797
            hook_log.getvalue(),
 
1798
            '{"b": "true", '
 
1799
            '"d": "%s", '
 
1800
            '"f": "1.23", '
 
1801
            '"i": "42", '
 
1802
            '"private-address": "mysql-0.example.com", '
 
1803
            '"s": "ascii", '
 
1804
            '"r": "%s", '
 
1805
            '"u": "\\u4e2d\\u6587"}\n' % (encoded, encoded))
 
1806
 
 
1807
    @defer.inlineCallbacks
 
1808
    def common_relation_set(self):
 
1809
        hook_log = capture_separate_log("hook", level=logging.INFO)
 
1810
        exe = yield self.ua.get_invoker(
 
1811
            "database:42", "add", "mysql/0",
 
1812
            self.relation, client_id="client_id")
 
1813
        raw = "But when I do drink, I prefer \xCA\xFE"
 
1814
        data_file = self.makeFile(raw)
 
1815
        set_hook = self.create_hook(
 
1816
            "relation-set",
 
1817
            "s='some text' u=中文 d=@%s "
 
1818
            "r=\"$(echo -en 'But when I do drink, I prefer \\xCA\\xFE')\"" % (
 
1819
                data_file))
 
1820
        result = yield exe(set_hook)
 
1821
        self.assertEqual(result, 0)
 
1822
        defer.returnValue((exe, hook_log))
 
1823
 
 
1824
    @defer.inlineCallbacks
 
1825
    def test_relation_get_ascii(self):
 
1826
        """Verify that ascii data is roundtripped"""
 
1827
        exe, hook_log = yield self.common_relation_set()
 
1828
        result = yield exe(self.create_hook("relation-get", "s mysql/0"))
 
1829
        self.assertEqual(result, 0)
 
1830
        self.assertEqual(hook_log.getvalue(), "some text\n")
 
1831
 
 
1832
    @defer.inlineCallbacks
 
1833
    def test_relation_get_raw(self):
 
1834
        """Verify that raw data is roundtripped"""
 
1835
        exe, hook_log = yield self.common_relation_set()
 
1836
        result = yield exe(self.create_hook("relation-get", "r mysql/0"))
 
1837
        self.assertEqual(result, 0)
 
1838
        self.assertEqual(
 
1839
            hook_log.getvalue(), "But when I do drink, I prefer \xca\xfe\n")
 
1840
 
 
1841
    @defer.inlineCallbacks
 
1842
    def test_relation_get_unicode(self):
 
1843
        """Verify Unicode is roundtripped (via UTF-8) through the shell"""
 
1844
        exe, hook_log = yield self.common_relation_set()
 
1845
 
 
1846
        result = yield exe(self.create_hook("relation-get", "u mysql/0"))
 
1847
        self.assertEqual(result, 0)
 
1848
        self.assertEqual(hook_log.getvalue(), "中文\n")
 
1849
 
 
1850
    @defer.inlineCallbacks
 
1851
    def setup_config(self):
 
1852
        hook_log = self.capture_logging("hook")
 
1853
        exe = yield self.ua.get_invoker(
 
1854
            "db:42", "add", "mysql/0", self.relation, client_id="client_id")
 
1855
        context = yield self.ua.get_context("client_id")
 
1856
        config = yield context.get_config()
 
1857
        with open(self.make_zipped_file(), "rb") as f:
 
1858
            data = f.read()
 
1859
        config.update({
 
1860
            "b": True,
 
1861
            "f": 1.23,
 
1862
            "i": 42,
 
1863
            "s": "some text",
 
1864
            # uses UTF-8 encoding in this test script
 
1865
            "u": "中文",
 
1866
            # use high byte and null byte characters
 
1867
            "r": data
 
1868
            })
 
1869
        yield config.write()
 
1870
        defer.returnValue((exe, hook_log))
 
1871
 
 
1872
    @defer.inlineCallbacks
 
1873
    def test_config_get_boolean(self):
 
1874
        """Validate that config-get returns lowercase names of booleans."""
 
1875
        exe, hook_log = yield self.setup_config()
 
1876
        result = yield exe(self.create_hook("config-get", "b"))
 
1877
        self.assertEqual(result, 0)
 
1878
        self.assertEqual(hook_log.getvalue(), "true\n")
 
1879
 
 
1880
    @defer.inlineCallbacks
 
1881
    def test_config_get_float(self):
 
1882
        """Validate that config-get returns floats without quotes."""
 
1883
        exe, hook_log = yield self.setup_config()
 
1884
        result = yield exe(self.create_hook("config-get", "f"))
 
1885
        self.assertEqual(result, 0)
 
1886
        self.assertEqual(hook_log.getvalue(), "1.23\n")
 
1887
 
 
1888
    @defer.inlineCallbacks
 
1889
    def test_config_get_int(self):
 
1890
        """Validate that config-get returns ints without quotes."""
 
1891
        exe, hook_log = yield self.setup_config()
 
1892
        result = yield exe(self.create_hook("config-get", "i"))
 
1893
        self.assertEqual(result, 0)
 
1894
        self.assertEqual(hook_log.getvalue(), "42\n")
 
1895
 
 
1896
    @defer.inlineCallbacks
 
1897
    def test_config_get_ascii(self):
 
1898
        """Validate that config-get returns ascii strings."""
 
1899
        exe, hook_log = yield self.setup_config()
 
1900
        result = yield exe(self.create_hook("config-get", "s"))
 
1901
        self.assertEqual(result, 0)
 
1902
        self.assertEqual(hook_log.getvalue(), "some text\n")
 
1903
 
 
1904
    @defer.inlineCallbacks
 
1905
    def test_config_get_raw(self):
 
1906
        """Validate config-get can work with high and null bytes."""
 
1907
        exe, hook_log = yield self.setup_config()
 
1908
        result = yield exe(self.create_hook("config-get", "r | zcat"))
 
1909
        self.assertEqual(result, 0)
 
1910
        self.assertEqual(hook_log.getvalue(), "abc\n")
 
1911
 
 
1912
    @defer.inlineCallbacks
 
1913
    def test_config_get_unicode(self):
 
1914
        """Validate that config-get returns raw strings containing UTF-8."""
 
1915
        exe, hook_log = yield self.setup_config()
 
1916
        result = yield exe(self.create_hook("config-get", "u"))
 
1917
        self.assertEqual(result, 0)
 
1918
        self.assertEqual(hook_log.getvalue(), "中文\n")