def test_compile_fail(): src = 'answer\n' with pytest.raises(CompilationError) as err: Worker._compile(src) assert 'Traceback' in str(err)
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)
def test_func_not_found(): src = 'answer = 42\n' with pytest.raises(CompilationError) as err: Worker._compile(src) assert 'not found' in str(err)
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']
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'}]
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
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)
def test_compile_context(): src = ( 'answer = 42\n' 'def run(x):\n' ' pass\n' ) func, context = Worker._compile(src) assert context['answer'] == 42
def test_compile_none(): assert Worker._compile(None) == (None, None)
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', })