예제 #1
0
def test_compile_fail():
    src = 'answer\n'

    with pytest.raises(CompilationError) as err:
        Worker._compile(src)

    assert 'Traceback' in str(err)
예제 #2
0
def test_compile_wrong_type():
    src = 'run = 42\n'

    with pytest.raises(CompilationError) as err:
        Worker._compile(src)

    assert 'must be a function' in str(err)
예제 #3
0
def test_func_not_found():
    src = 'answer = 42\n'

    with pytest.raises(CompilationError) as err:
        Worker._compile(src)

    assert 'not found' in str(err)
예제 #4
0
def test_join_func_setter():
    w = Worker(None, None, None)

    assert w.join_func == {}

    w.join_func = 'def run(): pass\n'

    assert w.join_func['src'] == 'def run(): pass\n'
    assert w.join_func['func']
    assert w.join_func['context']
예제 #5
0
def test_odf_confs_setter():
    w = Worker(None, None, None)

    assert w.odf_confs == []

    w.odf_confs = [{'src': None, 'topic': 'test'}]

    assert w.odf_confs == [{
        'src': None,
        'func': None,
        'context': None,
        'topic': 'test'}]
예제 #6
0
def test_compile():
    src = 'def run(x): return int(x)\n'

    func, context = Worker._compile(src)

    assert context['run'] is func
    assert func('42') == 42
예제 #7
0
    def __init__(self, *args, **kwargs):
        self.funcs = []

        self.esm_fixed_topic = self.esm_topic  # get a random generated topic
        self.esm_worker = Worker()

        super(type(self), self).__init__(*args, **kwargs)
예제 #8
0
def test_compile_context():
    src = (
        'answer = 42\n'
        'def run(x):\n'
        '  pass\n'
    )

    func, context = Worker._compile(src)

    assert context['answer'] == 42
예제 #9
0
def test_compile_none():
    assert Worker._compile(None) ==  (None, None)
예제 #10
0
class Graph(BaseAPIHandler):
    '''
    The graph api handler

    FIXME: the request-response model need timer for checking timeout.
    FIXME: stop the esm worker process if no link.
    '''

    def __init__(self, *args, **kwargs):
        self.funcs = []

        self.esm_fixed_topic = self.esm_topic  # get a random generated topic
        self.esm_worker = Worker()

        super(type(self), self).__init__(*args, **kwargs)

    @property
    def esm_topic(self):
        return 'iottalk/esm/{}'.format(uuid4())

    def on_req(self, client, userdata, msg):
        payload = msg.payload

        log.debug('Graph request: %r', payload)

        # FIXME: check ``data`` format
        opcode = payload['op']
        if opcode == 'add_link':
            self.add_link(payload)
        elif opcode == 'rm_link':
            self.rm_link(payload)
        elif opcode == 'add_funcs':
            self.add_funcs(payload)
        elif opcode == 'rm_funcs':
            self.rm_funcs(payload)
        elif opcode == 'set_join':
            self.set_join(payload)
        elif opcode == 'attach':
            log.warning('client attach again, ignore.')
        elif opcode == 'detach':
            self.detach()
        else:
            raise NotImplementedError()

    def response_func(self, payload_tmpl, on_success=None, on_error=None):
        '''
        Wrap functions as partial functions that accept a payload parameter.

        Those functions are handlers of device response.
        A handler should accept the first argument as ``payload`` which
        is a dict make from ``payload_tmpl`` and the real response from device
        application, and return a payload which will be sent as the response of
        this api call.

        :param payload_tmpl: the payload template.
        :param on_success: the callback handler on success
        :param on_error: the callback handler on error
        :return: the ``(on_success, on_error)`` pair
        :rtype: tuple
        '''
        def factory(func):
            def wrapper(payload):
                msg_id = payload['msg_id']
                log.debug('The response msg_id: %r', msg_id)

                del payload['msg_id']
                wrapper.tmpl.update(payload)

                response = func(wrapper.tmpl.copy()) if func else wrapper.tmpl

                log.debug('The payload for %r callback: %r',
                          msg_id, wrapper.tmpl)
                log.debug('The response of %r api call: %r', msg_id, response)

                return wrapper.res(response)

            wrapper.tmpl = payload_tmpl.copy()
            wrapper.res = self.res

            return wraps(func)(wrapper) if func else wrapper

        return factory(on_success), factory(on_error)

    def get_mode(self, keys):
        '''
        Determine *mode* (idf or odf) for the ``keys`` tuple

        :param keys: iterable
        :return: ``idf`` or ``odf``
        :raise: ``ValueError`` if non-deterministic
        '''
        modes = ('idf', 'odf')
        mode = None

        for k in keys:
            if k in modes and mode is None:
                mode = k
            elif k in modes and mode:
                raise ValueError('mode duplicated')

        if mode is None:
            raise ValueError('mode not found')

        return mode

    def send_error(self, payload, reason):
        '''
        Send the error response
        '''
        payload['state'] = 'error'
        payload['reason'] = reason
        return self.res(payload)

    def add_link(self, payload):
        '''
        This add link command will send a ``CONNECT`` control message
        to device application.

        We will check the device feature lock as first.
        Then, record the link to ``Link`` for future lookup.
        '''
        da_id = UUID(payload['da_id'])
        mode = self.get_mode(payload.keys())
        feature = payload[mode]

        if Link.select(payload['da_id'], feature, mode):
            return self.send_error(payload, reason='Link already exists')
        elif payload.get('func') and not UserFunction.select(payload['func']):
            return self.send_error(
                payload, reason='Function unknown. Please add it first.')
        return self._add_link(da_id, mode, feature, payload)

    def _add_link_on_success(self, topic):
        '''
        Get the success callback function on ``add_link``.

        :return: The handler for device application.
        '''
        def success(payload):
            '''
            :return: the response of ``add_link`` api call. This handle return
                *ok* if the ``Link`` record create successfully.
                The sqlite in-memory will be executed in ``serializable``
                ioslation level.  If the writting transaction of ``Link``
                is failed, we will return a *error* state as response.
            '''
            mode = self.get_mode(payload)

            try:
                Link.add(payload['da_id'], payload[mode], mode, topic)
            except ValueError as err:
                payload['state'] = 'error'
                payload['reason'] = 'Link already exists'

            # update esm worker config and (re)start
            func = UserFunction.select(payload['func']) if payload.get('func') else None
            if mode == 'idf':
                conf = self.esm_worker.idf_confs.copy()
                conf.append({'src': func, 'topic': topic})
                self.esm_worker.idf_confs = conf
            elif mode == 'odf':
                conf = self.esm_worker.odf_confs.copy()
                conf.append({'src': func, 'topic': topic})
                self.esm_worker.odf_confs = conf
            self.esm_worker.start()

            return payload

        return success

    def _add_link(self, id_, mode, feature, payload):
        '''
        :param id_: the ``UUID`` object
        :param mode: ``idf`` or ``odf``
        :param feature: the feature name
        :param payload: the full payload

        .. todo::
            - Speed up special case:
              If there is no any function and only *one* IDF, we just
              bind to ``esm_fixed_topic`` in order to do direct data
              transfer.
        '''
        # FIXME: pub can be none, if da do not register
        pub = iot_conn_mgr.conns[id_].ctrl.pub
        msg_id = str(uuid4())  # random
        topic = self.esm_topic

        iot_conn_mgr.conns[id_].ctrl.add_res_callback(
            msg_id,
            *self.response_func(
                payload,
                on_success=self._add_link_on_success(topic),
                on_error=None))

        return ctrl.connect(msg_id, mode, feature, topic, pub)

    def rm_link(self, payload):
        '''
        This ``rm_link`` command will send a ``DISCONNECT`` control message
        to device application.
        '''
        da_id = payload['da_id']
        msg_id = str(uuid4())  # random
        mode = self.get_mode(payload)
        feature = payload[mode]
        pub = iot_conn_mgr.conns[UUID(payload['da_id'])].ctrl.pub

        try:
            topic = Link.pop(da_id, feature, mode)
        except ValueError as err:
            return self.send_error(payload, reason=str(err))

        iot_conn_mgr.conns[UUID(da_id)].ctrl.add_res_callback(
            msg_id,
            *self.response_func(
                payload,
                on_success=self.rm_link_on_success(),
                on_error=None
            )
        )

        return ctrl.disconnect(msg_id, mode, feature, topic, pub)

    def rm_link_on_success(self):
        '''
        Get the success callback function on ``rm_link``.

        :return: The handler for device application.
        '''
        def success(payload):
            '''
            If the record of ``Link`` removes failed,
            we will return a error payload.
            '''
            mode = self.get_mode(payload)

            try:
                Link.rm(payload['da_id'], payload[mode], mode)
            except ValueError as err:
                payload['state'] = 'error'
                payload['reason'] = str(err)

            return payload

        return success

    def add_funcs(self, payload):
        '''
        Add functions into in-memory storage.
        '''
        codes, digests = payload['codes'], payload['digests']

        if len(codes) != len(digests):
            self.send_error(
                payload, 'The numbers of codes and digests are not consistant')
            return

        # FIXME: potential crash when digest and function mismatched
        deque(starmap(UserFunction.add, zip(digests, codes)), maxlen=0)
        return self.res({
            'op': 'add_funcs',
            'state': 'ok',
            'digests': digests
        })

    def rm_funcs(self, payload):
        '''
        Remove functions from in-memory storage.
        '''
        digests = payload['digests']

        deque(map(UserFunction.rm, digests), maxlen=0)
        return self.res({
            'op': 'rm_funcs',
            'state': 'ok',
            'digests': digests,
        })

    def set_join(self, payload):
        '''
        Setup or change the join function.

        If ``payload['prev']`` is not the same as current running in ESM
        process, we will reject this request.
        '''
        if self.esm_worker.join_func['src'] != payload['prev']:
            self.send_error(payload, reason='`prev` field mismatch')
            return
        elif payload['new'] is None:
            self.send_error(payload, reason='`new` can not be null')
            return

        func = UserFunction.select(payload['new'])
        if not func:
            self.send_error(payload, reason='New function not found')

        self.esm_worker.join_func = {'src': func}
        self.esm_worker.start()

        return self.res({
            'op': 'set_join',
            'prev': payload['prev'],
            'new': payload['new'],
            'state': 'ok',
        })