__main__.py 8.01 KB
Newer Older
1 2 3 4 5 6 7 8 9
#!/usr/bin/env python3

import slixmpp
import asyncio
import logging
import signal
import atexit
import sys
from functools import partial
louiz’'s avatar
louiz’ committed
10
from slixmpp.xmlstream.matcher.base import MatcherBase
11 12


louiz’'s avatar
louiz’ committed
13
class MatchAll(MatcherBase):
14 15 16 17 18 19
    """match everything"""

    def match(self, xml):
        return True


20 21 22 23
class StanzaError(Exception):
    pass


24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
class XMPPComponent(slixmpp.BaseXMPP):
    """
    XMPPComponent sending a “scenario” of stanzas, checking that the responses
    match the expected results.
    """

    def __init__(self, scenario, biboumi):
        super().__init__(jid="biboumi.localhost", default_ns="jabber:component:accept")
        self.is_component = True
        self.stream_header = '<stream:stream %s %s from="%s" id="%s">' % (
            'xmlns="jabber:component:accept"',
            'xmlns:stream="%s"' % self.stream_ns,
            self.boundjid, self.get_id())
        self.stream_footer = "</stream:stream>"

        self.register_handler(slixmpp.Callback('Match All',
                                               MatchAll(None),
                                               self.handle_incoming_stanza))

        self.add_event_handler("session_end", self.on_end_session)

        asyncio.async(self.accept_routine())

        self.scenario = scenario
        self.biboumi = biboumi
49 50 51
        # A callable, taking a stanza as argument and raising a StanzaError
        # exception if the test should fail.
        self.stanza_checker = None
52 53 54 55 56 57 58 59 60 61 62 63
        self.failed = False
        self.accepting_server = None

    def error(self, message):
        print("Failure: %s" % (message,))
        self.scenario.steps = []
        self.failed = True

    def on_end_session(self, event):
        self.loop.stop()

    def handle_incoming_stanza(self, stanza):
64 65 66 67 68 69
        if self.stanza_checker:
            try:
                self.stanza_checker(stanza)
            except StanzaError as e:
                self.error(e)
            self.stanza_checker = None
70 71 72 73 74 75 76 77 78 79 80 81 82 83
        self.run_scenario()

    def run_scenario(self):
        if scenario.steps:
            step = scenario.steps.pop(0)
            step(self, self.biboumi)
        else:
            self.biboumi.stop()

    @asyncio.coroutine
    def accept_routine(self):
        self.accepting_server = yield from self.loop.create_server(lambda: self,
                                                                   "127.0.0.1", "8811", reuse_address=True)

louiz’'s avatar
louiz’ committed
84

85 86 87 88
def check_xpath(xpath, stanza):
    matched = slixmpp.xmlstream.matcher.xpath.MatchXPath(xpath).match(stanza)
    if not matched:
        raise StanzaError("Received stanza “%s” did not match expected xpath “%s”" % (stanza, self.expected_xpath))
89

louiz’'s avatar
louiz’ committed
90

91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
class Scenario:
    """Defines a list of actions that are executed in sequence, until one of
    them throws an exception, or until the end.  An action can be something
    like “send a stanza”, “receive the next stanza and check that it matches
    the given XPath”, “send a signal”, “wait for the end of the process”,
    etc
    """

    def __init__(self, name, steps):
        """
        Steps is a list of 2-tuple:
        [(action, answer), (action, answer)]
        """
        self.name = name
        self.steps = steps


class BiboumiRunner:
    def __init__(self, name, with_valgrind):
        self.name = name
        self.fd = open("biboumi_%s_output.txt" % (name,), "w")
        if with_valgrind:
louiz’'s avatar
louiz’ committed
113 114 115
            self.create = asyncio.create_subprocess_exec("valgrind", "--leak-check=full", "--show-leak-kinds=all",
                                                         "--errors-for-leak-kinds=all", "--error-exitcode=16",
                                                         "./biboumi", "test.conf", stdin=None, stdout=self.fd,
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
                                                         stderr=self.fd, loop=None, limit=None)
        else:
            self.create = asyncio.create_subprocess_exec("./biboumi", "test.conf", stdin=None, stdout=self.fd,
                                                         stderr=self.fd, loop=None, limit=None)
        self.process = None

        self.signal_sent = False

    @asyncio.coroutine
    def start(self):
        self.process = yield from self.create

    @asyncio.coroutine
    def wait(self):
        code = yield from self.process.wait()
        return code

    def stop(self):
        if not self.signal_sent:
            self.signal_sent = True
            if self.process:
                self.process.send_signal(signal.SIGINT)


def send_stanza(stanza, xmpp, biboumi):
    xmpp.send_raw(stanza)
    asyncio.get_event_loop().call_soon(xmpp.run_scenario)


def expect_stanza(xpath, xmpp, biboumi):
146
    xmpp.stanza_checker = partial(check_xpath, xpath)
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169


class BiboumiTest:
    """
    Spawns a biboumi process and a fake XMPP Component that will run a
    Scenario.  It redirects the outputs of the subprocess into separated
    files, and detects any failure in the running of the scenario.
    """

    def __init__(self, scenario, expected_code=0):
        self.scenario = scenario
        self.expected_code = 0

    def run(self, with_valgrind=True):
        print("Running scenario: %s%s" % (self.scenario.name, " (with valgrind)" if with_valgrind else ''))
        # Redirect the slixmpp logging into a specific file
        output_filename = "slixmpp_%s_output.txt" % (self.scenario.name,)
        with open(output_filename, "w"):
            pass
        logging.basicConfig(level=logging.DEBUG,
                            format='%(levelname)-8s %(message)s',
                            filename=output_filename)

170 171 172
        with open("test.conf", "w") as fd:
            fd.write(confs['basic'])

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
        # Start the XMPP component and biboumi
        biboumi = BiboumiRunner(scenario.name, with_valgrind)
        xmpp = XMPPComponent(self.scenario, biboumi)
        asyncio.get_event_loop().run_until_complete(biboumi.start())

        asyncio.get_event_loop().call_soon(xmpp.run_scenario)

        xmpp.process()

        code = asyncio.get_event_loop().run_until_complete(biboumi.wait())
        failed = False
        if not xmpp.failed:
            if code != self.expected_code:
                xmpp.error("Wrong return code from biboumi's process: %d" % (code,))
                failed = True
            else:
                print("Success!")
        else:
            failed = True

        if xmpp.server:
            xmpp.accepting_server.close()

        return not failed

louiz’'s avatar
louiz’ committed
198

199 200 201 202 203
confs = {'basic':
"""hostname=biboumi.localhost
password=coucou
db_name=biboumi.sqlite
port=8811"""}
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220

if __name__ == '__main__':

    atexit.register(asyncio.get_event_loop().close)

    # Start the test component, accepting connections on the configured
    # port.
    scenarios = (
        Scenario("basic_handshake_success",
                 [
                     partial(expect_stanza, "{jabber:component:accept}handshake"),
                     partial(send_stanza, "<handshake xmlns='jabber:component:accept'/>"),
                 ]),
        Scenario("channel_join",
                 [
                     partial(expect_stanza, "{jabber:component:accept}handshake"),
                     partial(send_stanza, "<handshake xmlns='jabber:component:accept'/>"),
louiz’'s avatar
louiz’ committed
221 222
                     partial(send_stanza,
                             "<presence from='me@example.com/Nick' to='#foo%irc.localhost@biboumi.localhost' />"),
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
                     partial(expect_stanza, "{jabber:component:accept}message/body"),
                 ]),
    )

    failures = 0

    print("Running %s checks for biboumi." % (len(scenarios)))

    for scenario in scenarios:
        test = BiboumiTest(scenario)
        if not test.run(False):
            print("You can check the files slixmpp_%s_output.txt and biboumi_%s_output.txt to help you debug." %
                  (scenario.name, scenario.name))
            failures += 1

    if failures:
        print("%d test%s failed, please fix %s." % (failures, 's' if failures > 1 else '',
                                                    'them' if failures > 1 else 'it'))
        sys.exit(1)
    else:
        print("All tests passed successfully")