class KlassMapTest(TestCase): def setUp(self): adapter = mock.Mock() taskModel = mock.Mock() self.km = KlassMap(adapter=adapter, namespace="foo-ins1", taskModel=taskModel) self.tm1 = TaskMapBase(klass=0) self.tm1.__class__._Item = DummyTaskItem self.km.addTaskMap(self.tm1) self.tm2 = TaskMapBase(klass=1) self.tm2.__class__._Item = DummyTaskItem self.km.addTaskMap(self.tm2) self.am = mock.Mock() self.km.setAdapterMap(self.am) def test_create_init(self): self.assertEqual(self.km.namespace, "foo-ins1") self.assertEqual(len(self.km), 0) self.assertRaises(NotImplementedError, self.km.__setitem__, "what", "ever") # namespace is set self.assertEqual(self.tm1.namespace, "foo-ins1") def test_create_same_klass(self): # try add it again, but with the same class tm1_1 = TaskMapBase(klass=0) self.assertRaises(RuntimeError, self.km.addTaskMap, tm1_1) def test_task_add_update_delete(self): self.tm1.updateData({ "1": "task1", "2": "task2", }) self.assertEqual(len(self.km), 2) self.assertEqual(self.km["1"].value, "task1") self.assertEqual(self.km["1"].klass, 0) self.assertEqual(self.km["2"].value, "task2") self.assertEqual(self.km["2"].klass, 0) self.assertEqual(self.km["2"].isDeletionPending, False) # change item self.tm1.updateData({ "1": "task1!", "2": "task2", }) self.assertEqual(len(self.km), 2) self.assertEqual(self.km["1"].value, "task1!") self.assertEqual(self.km["2"].value, "task2") self.assertEqual(self.km["2"].isDeletionPending, False) # remove one item self.tm1.updateData({ "1": "task1!", }) self.assertEqual(len(self.km), 2) self.assertEqual(self.km["2"].isDeletionPending, True) # try to remove it again, shouldn't remove self.tm1.updateData({ "1": "task1!", }) self.assertEqual(len(self.km), 2) self.assertEqual(self.km["2"].isDeletionPending, True) # updateData with taskMap2 self.tm2.updateData({}) self.assertEqual(len(self.km), 1) self.assertRaises(KeyError, self.km.__getitem__, "2") def test_task_move(self): # order: 1, 2, then move 1 to 2 self.tm1.updateData({ "1": "task1", }) self.tm2.updateData({ "1": "task1??", }) self.tm1.updateData({}) self.assertEqual(self.km["1"].isDeletionPending, True) # until tm1 deletes it, it shouldn't be in tm2 self.assertEqual(len(self.tm1), 1) self.assertEqual(len(self.tm2), 0) # after tm2 updates again, it should be moved self.tm2.updateData({ "1": "task1??", }) self.assertEqual(len(self.tm1), 0) self.assertEqual(len(self.tm2), 1) self.assertEqual(self.tm2["1"].value, "task1??") self.assertEqual(self.tm2["1"].isDeletionPending, False) self.assertFalse(self.am.afterMove.called) def test_task_move_model_move_down(self): # set tm1 = [0,1,2,3] tm2=[4,5] self.tm1.updateData({ "0": "task0", }) self.tm1.updateData({ "0": "task0", "1": "task1", }) self.tm1.updateData({ "0": "task0", "1": "task1", "2": "task2", }) self.tm1.updateData({ "0": "task0", "1": "task1", "2": "task2", "3": "task3", }) self.tm2.updateData({ "4": "task4", "5": "task5", }) # move 2 from tm1 to tm2 self.assertFalse(self.km["2"].isDeletionPending) self.tm1.updateData({ "0": "task0", "1": "task1", "3": "task3", }) self.assertTrue(self.km["2"].isDeletionPending) self.tm2.updateData({ "2": "task2", "4": "task4", "5": "task5", }) self.assertFalse(self.km["2"].isDeletionPending) self.am.beforeMove.assert_called_with("foo-ins1", 2, 6) def test_task_move_model_move_up(self): # tm1 is empty; tm2's 3rd item is task2 self.tm1.updateData({}) self.tm2.updateData({ "0": "task0", "1": "task1", }) self.tm2.updateData({ "0": "task0", "1": "task1", "2": "task2", }) self.tm2.updateData({ "0": "task0", "1": "task1", "2": "task2", "3": "task3", "4": "task4", "5": "task5", }) # move task2 to tm1 from tm2 self.assertFalse(self.km["2"].isDeletionPending) self.tm2.updateData({ "0": "task0", "1": "task1", "3": "task3", "4": "task4", "5": "task5", }) self.assertTrue(self.km["2"].isDeletionPending) self.tm1.updateData({ "2": "task2moved", }) self.assertFalse(self.km["2"].isDeletionPending) self.am.beforeMove.assert_called_with("foo-ins1", 2, 0) def test_iter_index(self): self.tm1.updateData({ "1": 10, }) self.assertEqual(self.km.index("1"), 0) self.tm2.updateData({ "4": 41, }) self.assertEqual(self.km.index("4"), 1) self.tm2.updateData({ "4": 41, "2": 25, }) self.assertEqual(self.km.index("2"), 2) self.tm1.updateData({ "1": 10, "3": 37, }) self.assertEqual(self.km.index("3"), 1) # index() with self.assertRaises(ValueError): self.km.index("0") # __contains__ for key in ["1", "2", "3", "4"]: self.assertTrue(key in self.km, key) for key in ["5", "6", "7"]: self.assertFalse(key in self.km, key) # __iter__ self.assertListEqual(list(self.km.__iter__()), ['1', '3', '4', '2']) # items() self.assertListEqual( list(map(lambda pair: (pair[0], pair[1].value), self.km.items())), [("1", 10), ("3", 37), ("4", 41), ("2", 25)], ) # values() self.assertListEqual( list(map(lambda i: i.value, self.km.values())), [10, 37, 41, 25], ) def test_get_klassMap(self): self.assertEqual(self.km.klass(0), self.tm1) self.assertNotEqual(self.km.klass(1), self.tm1) def test_findItemKlass(self): self.tm1.updateData({ "1": 1, "2": 2, }) self.tm2.updateData({ "3": 3, }) self.assertEqual(self.km.findItemKlass("1"), 0) self.assertEqual(self.km.findItemKlass("2"), 0) self.assertEqual(self.km.findItemKlass("3"), 1) with self.assertRaises(KeyError): self.km.findItemKlass("4")
class XwareAdapter(QObject): initialized = pyqtSignal() infoUpdated = pyqtSignal() # daemon infoPolled Manifest = { "SupportedTypes": [ TaskCreationType.Normal, TaskCreationType.Emule, TaskCreationType.Magnet, TaskCreationType.RemoteTorrent, TaskCreationType.LocalTorrent, ], } def __init__(self, *, adapterConfig, taskModel, parent = None): super().__init__(parent) # Prepare XwareClient Variables self._ulSpeed = 0 self._dlSpeed = 0 self._runningTaskCount = 0 from .vanilla import GetSysInfo self._sysInfo = GetSysInfo(Return = 0, Network = 0, License = 0, Bound = 0, ActivateCode = "", Mount = 0, InternalVersion = "", Nickname = "", Unknown = "", UserId = 0, VipLevel = 0) self._xwareSettings = XwareSettings(self) # Prepare XwaredClient Variables self._xwaredRunning = False self._etmPid = 0 self._peerId = "" self._startEtmWhen = 1 self._adapterConfig = adapterConfig connection = parse.urlparse(self._adapterConfig["connection"], scheme = "file") self.xwaredSocket = None self.mountsFaker = None self.klassMap = KlassMap(adapter = self, namespace = self.namespace, taskModel = taskModel) self.klassMap.addTaskMap( TaskMap(klass = TaskClass.RUNNING) ) self.klassMap.addTaskMap( TaskMap(klass = TaskClass.COMPLETED) ) self.klassMap.addTaskMap( TaskMap(klass = TaskClass.RECYCLED) ) self.klassMap.addTaskMap( TaskMap(klass = TaskClass.FAILED_ON_SUBMISSION) ) self.useXwared = False self.isLocal = False _clientInitOptions = dict() if connection.scheme == "file": _clientInitOptions["host"] = "127.0.0.1" self.useXwared = True self.xwaredSocket = os.path.expanduser(connection.path) app.aboutToQuit.connect(lambda: self.do_daemon_quitFrontend()) self._notifyFrontendStart = True from .mounts import MountsFaker self.mountsFaker = MountsFaker(constants.MOUNTS_FILE) elif connection.scheme == "http": # assume etm is always running self._etmPid = 0xDEADBEEF host, port = connection.netloc.split(":") self._peerId = connection.query _clientInitOptions["host"] = host _clientInitOptions["port"] = port else: raise NotImplementedError() self._loop = None self._loop_executor = None self._xwareClient = None self._loop_thread = threading.Thread(daemon = True, target = self._startEventLoop, args = (_clientInitOptions,), name = adapterConfig.name) def start(self): self._loop_thread.start() def _startEventLoop(self, clientInitOptions = None): self._loop = asyncio.new_event_loop() self._loop.set_debug(True) self._loop_executor = ThreadPoolExecutor(max_workers = 1) self._loop.set_default_executor(self._loop_executor) asyncio.events.set_event_loop(self._loop) self._xwareClient = XwareClient() self.setClientOptions(clientInitOptions or dict()) asyncio.ensure_future(self.main()) self._loop.run_forever() @pyqtProperty(str, notify = initialized) def namespace(self): return "xware-" + self._adapterConfig.name[len("adapter-"):] @pyqtProperty(str, notify = initialized) def name(self): return self._adapterConfig["name"] @pyqtProperty(str, notify = initialized) def connection(self): return self._adapterConfig["connection"] @property def ulSpeed(self): return self._ulSpeed @property def dlSpeed(self): return self._dlSpeed @property def runningTaskCount(self): return self._runningTaskCount @property def backendSettings(self): return self._xwareSettings def setClientOptions(self, clientOptions: dict): host = clientOptions.get("host", None) if host in ("127.0.0.1", "localhost"): self.isLocal = True self._xwareClient.updateOptions(clientOptions) # =========================== PUBLIC =========================== @asyncio.coroutine def main(self): # Entry point of the thread "XwareAdapterEventLoop" # main() handles non-stop polling if getattr(self, "_notifyFrontendStart", False): self._loop.call_soon(self.daemon_start) while True: self._loop.call_soon(self.get_getsysinfo) self._loop.call_soon(self.get_list, TaskClass.RUNNING) self._loop.call_soon(self.get_list, TaskClass.COMPLETED) self._loop.call_soon(self.get_list, TaskClass.RECYCLED) self._loop.call_soon(self.get_list, TaskClass.FAILED_ON_SUBMISSION) if not self._xwareSettings.initialized: self._loop.call_soon(self.get_settings) if self.useXwared: self._loop.call_soon(self.daemon_infoPoll) yield from asyncio.sleep(_POLLING_INTERVAL) # =========================== META-PROGRAMMING MAGICS =========================== def __getattr__(self, name): if name.startswith("get_") or name.startswith("post_"): def method(*args): clientMethod = getattr(self._xwareClient, name) coro = clientMethod(*args) assert asyncio.iscoroutine(coro) future =asyncio.ensure_future(coro) cb = getattr(self, "_donecb_" + name, None) if cb: cb = partial(cb, *args) future.add_done_callback(cb) setattr(self, name, method) return method elif name.startswith("daemon_"): def method(*args): assert self.useXwared curried = partial(callXwared, self) clientMethodName = name[len("daemon_"):] asyncio.ensure_future(curried(clientMethodName, args)) setattr(self, name, method) return method raise AttributeError("XwareAdapter doesn't have a {name}.".format(**locals())) @property def sysInfo(self): return self._sysInfo def _donecb_get_getsysinfo(self, future): exception = future.exception() if not exception: result = future.result() self._sysInfo = result else: logging.error("get_getsysinfo failed.") def _donecb_get_list(self, klass, future): exception = future.exception() if not exception: result = future.result() if klass == TaskClass.RUNNING: self._ulSpeed = result["upSpeed"] self._dlSpeed = result["dlSpeed"] self._runningTaskCount = result["dlNum"] self.klassMap.klass(klass).updateData(result["tasks"]) else: logging.error("get_list failed.") def _donecb_get_settings(self, future): exception = future.exception() if not exception: result = future.result() self._xwareSettings.update(result) else: logging.error("get/post settings failed.") def _donecb_post_settings(self, _: "new settings", future): return self._donecb_get_settings(future) def do_pauseTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) self._loop.call_soon_threadsafe(self.post_pause, taskIds) def do_startTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) self._loop.call_soon_threadsafe(self.post_start, taskIds) def do_createTask(self, creation: TaskCreation) -> (bool, str): if creation.kind not in self.__class__.Manifest["SupportedTypes"]: return False, "Not a supported type." # convert path path = self.mountsFaker.convertToMappedPath(creation.path) if not path: return False, "Not pre-mounted." if creation.kind in (TaskCreationType.Normal, TaskCreationType.Emule) or \ creation.kind == TaskCreationType.RemoteTorrent: # TODO xware not properly support yet fileInfo = creation.subtaskInfo[0] # Workaround: xware doesn't acquire filename if not set. filename = fileInfo.name self._loop.call_soon_threadsafe(self.post_createTask, path, creation.url, filename) return True, None elif creation.kind == TaskCreationType.Magnet: # Note: # To add a magnet task, xware requires a name field, same as normal and emule tasks. # But xware will ignore the name parameter and acquire the name on its own. self._loop.call_soon_threadsafe(self.post_createTask, path, creation.url, "解析中的磁力链接") return True, None elif creation.kind == TaskCreationType.LocalTorrent: self._loop.call_soon_threadsafe(self.post_createBtTask, ) return False, "Not implemented." def do_delTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) self._loop.call_soon_threadsafe(self.post_del, taskIds, options["recycle"], options["delete"]) def do_restoreTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) self._loop.call_soon_threadsafe(self.post_restore, taskIds) def do_openLixianChannel(self, taskItem, enable: bool): taskId = taskItem.realid self._loop.call_soon_threadsafe(self.post_openLixianChannel, taskId, enable) def do_openVipChannel(self, taskItem): taskId = taskItem.realid self._loop.call_soon_threadsafe(self.post_openVipChannel, taskId) def do_applySettings(self, settings: dict): dLimit = settings.get("downloadSpeedLimit", -1) uLimit = settings.get("uploadSpeedLimit", -1) if dLimit != -1: self._adapterConfig.setint("dlspeedlimit", dLimit) if uLimit != -1: self._adapterConfig.setint("ulspeedlimit", uLimit) self._loop.call_soon_threadsafe(self.post_settings, settings) # ==================== DAEMON ==================== @pyqtProperty(bool, notify = infoUpdated) def xwaredRunning(self): return self._xwaredRunning @pyqtProperty(int, notify = infoUpdated) def etmPid(self): return self._etmPid @pyqtProperty(str, notify = infoUpdated) def peerId(self): return self._peerId @pyqtProperty(int, notify = infoUpdated) def startEtmWhen(self): return self._startEtmWhen @startEtmWhen.setter def startEtmWhen(self, value): self._loop.call_soon_threadsafe(self.daemon_setStartEtmWhen, value) def _donecb_daemon_infoPoll(self, data): error = data.get("error") if not error: result = data.get("result") self._xwaredRunning = True self._etmPid = result.get("etmPid") self._peerId = result.get("peerId") lcPort = result.get("lcPort") self._startEtmWhen = result.get("startEtmWhen") else: self._xwaredRunning = False self._etmPid = 0 self._peerId = "" lcPort = 0 self._startEtmWhen = 1 print("infoPoll failed with error", error, file = sys.stderr) self.setClientOptions({ "port": lcPort, }) self.infoUpdated.emit() def do_daemon_start(self): self._loop.call_soon_threadsafe(self.daemon_startETM) def do_daemon_restart(self): self._loop.call_soon_threadsafe(self.daemon_restartETM) def do_daemon_stop(self): self._loop.call_soon_threadsafe(self.daemon_stopETM) def do_daemon_startFrontend(self): raise NotImplementedError() # handled in main() # self._loop.call_soon_threadsafe(self.daemon_start) def do_daemon_quitFrontend(self): self._loop.call_soon_threadsafe(self.daemon_quit) @property def daemonManagedBySystemd(self): return os.path.lexists(constants.SYSTEMD_SERVICE_ENABLED_USERFILE) and \ os.path.lexists(constants.SYSTEMD_SERVICE_USERFILE) @daemonManagedBySystemd.setter def daemonManagedBySystemd(self, on): if on: tryMkdir(os.path.dirname(constants.SYSTEMD_SERVICE_ENABLED_USERFILE)) trySymlink(constants.SYSTEMD_SERVICE_FILE, constants.SYSTEMD_SERVICE_USERFILE) trySymlink(constants.SYSTEMD_SERVICE_USERFILE, constants.SYSTEMD_SERVICE_ENABLED_USERFILE) else: tryRemove(constants.SYSTEMD_SERVICE_ENABLED_USERFILE) tryRemove(constants.SYSTEMD_SERVICE_USERFILE) if getInitType() == InitType.SYSTEMD: os.system("systemctl --user daemon-reload") @property def daemonManagedByUpstart(self): return os.path.lexists(constants.UPSTART_SERVICE_USERFILE) @daemonManagedByUpstart.setter def daemonManagedByUpstart(self, on): if on: tryMkdir(os.path.dirname(constants.UPSTART_SERVICE_USERFILE)) trySymlink(constants.UPSTART_SERVICE_FILE, constants.UPSTART_SERVICE_USERFILE) else: tryRemove(constants.UPSTART_SERVICE_USERFILE) if getInitType() == InitType.UPSTART: os.system("initctl --user reload-configuration") @property def daemonManagedByAutostart(self): return os.path.lexists(constants.AUTOSTART_DESKTOP_USERFILE) @daemonManagedByAutostart.setter def daemonManagedByAutostart(self, on): if on: tryMkdir(os.path.dirname(constants.AUTOSTART_DESKTOP_USERFILE)) trySymlink(constants.AUTOSTART_DESKTOP_FILE, constants.AUTOSTART_DESKTOP_USERFILE) else: tryRemove(constants.AUTOSTART_DESKTOP_USERFILE)
class XwareAdapter(QObject): initialized = pyqtSignal() infoUpdated = pyqtSignal() # daemon infoPolled Manifest = { "SupportedTypes": [ TaskCreationType.Normal, TaskCreationType.Emule, TaskCreationType.Magnet, TaskCreationType.RemoteTorrent, TaskCreationType.LocalTorrent, ], } def __init__(self, *, adapterConfig, taskModel, parent = None): super().__init__(parent) # Prepare XwareClient Variables self._ulSpeed = 0 self._dlSpeed = 0 self._runningTaskCount = 0 from .vanilla import GetSysInfo self._sysInfo = GetSysInfo(Return = 0, Network = 0, License = 0, Bound = 0, ActivateCode = "", Mount = 0, InternalVersion = "", Nickname = "", Unknown = "", UserId = 0, VipLevel = 0) self._xwareSettings = XwareSettings(self) # Prepare XwaredClient Variables self._xwaredRunning = False self._etmPid = 0 self._peerId = "" self._startEtmWhen = 1 self._adapterConfig = adapterConfig connection = parse.urlparse(self._adapterConfig["connection"], scheme = "file") self.xwaredSocket = None self.mountsFaker = None self.klassMap = KlassMap(adapter = self, namespace = self.namespace, taskModel = taskModel) self.klassMap.addTaskMap( TaskMap(klass = TaskClass.RUNNING) ) self.klassMap.addTaskMap( TaskMap(klass = TaskClass.COMPLETED) ) self.klassMap.addTaskMap( TaskMap(klass = TaskClass.RECYCLED) ) self.klassMap.addTaskMap( TaskMap(klass = TaskClass.FAILED_ON_SUBMISSION) ) self.useXwared = False self.isLocal = False _clientInitOptions = dict() if connection.scheme == "file": _clientInitOptions["host"] = "127.0.0.1" self.useXwared = True self.xwaredSocket = os.path.expanduser(connection.path) app.aboutToQuit.connect(lambda: self.do_daemon_quitFrontend()) self._notifyFrontendStart = True from .mounts import MountsFaker self.mountsFaker = MountsFaker(constants.MOUNTS_FILE) elif connection.scheme == "http": # assume etm is always running self._etmPid = 0xDEADBEEF host, port = connection.netloc.split(":") self._peerId = connection.query _clientInitOptions["host"] = host _clientInitOptions["port"] = port else: raise NotImplementedError() self._loop = None self._loop_executor = None self._xwareClient = None self._loop_thread = threading.Thread(daemon = True, target = self._startEventLoop, args = (_clientInitOptions,), name = adapterConfig.name) def start(self): self._loop_thread.start() def _startEventLoop(self, clientInitOptions = None): self._loop = asyncio.new_event_loop() self._loop.set_debug(True) self._loop_executor = ThreadPoolExecutor(max_workers = 1) self._loop.set_default_executor(self._loop_executor) asyncio.events.set_event_loop(self._loop) self._xwareClient = XwareClient() self.setClientOptions(clientInitOptions or dict()) asyncio.async(self.main()) self._loop.run_forever() @pyqtProperty(str, notify = initialized) def namespace(self): return "xware-" + self._adapterConfig.name[len("adapter-"):] @pyqtProperty(str, notify = initialized) def name(self): return self._adapterConfig["name"] @pyqtProperty(str, notify = initialized) def connection(self): return self._adapterConfig["connection"] @property def ulSpeed(self): return self._ulSpeed @property def dlSpeed(self): return self._dlSpeed @property def runningTaskCount(self): return self._runningTaskCount @property def backendSettings(self): return self._xwareSettings def setClientOptions(self, clientOptions: dict): host = clientOptions.get("host", None) if host in ("127.0.0.1", "localhost"): self.isLocal = True self._xwareClient.updateOptions(clientOptions) # =========================== PUBLIC =========================== @asyncio.coroutine def main(self): # Entry point of the thread "XwareAdapterEventLoop" # main() handles non-stop polling if getattr(self, "_notifyFrontendStart", False): self._loop.call_soon(self.daemon_start) while True: self._loop.call_soon(self.get_getsysinfo) self._loop.call_soon(self.get_list, TaskClass.RUNNING) self._loop.call_soon(self.get_list, TaskClass.COMPLETED) self._loop.call_soon(self.get_list, TaskClass.RECYCLED) self._loop.call_soon(self.get_list, TaskClass.FAILED_ON_SUBMISSION) if not self._xwareSettings.initialized: self._loop.call_soon(self.get_settings) if self.useXwared: self._loop.call_soon(self.daemon_infoPoll) yield from asyncio.sleep(_POLLING_INTERVAL) # =========================== META-PROGRAMMING MAGICS =========================== def __getattr__(self, name): if name.startswith("get_") or name.startswith("post_"): def method(*args): clientMethod = getattr(self._xwareClient, name) coro = clientMethod(*args) assert asyncio.iscoroutine(coro) future =asyncio.async(coro) cb = getattr(self, "_donecb_" + name, None) if cb: cb = partial(cb, *args) future.add_done_callback(cb) setattr(self, name, method) return method elif name.startswith("daemon_"): def method(*args): assert self.useXwared curried = partial(callXwared, self) clientMethodName = name[len("daemon_"):] asyncio.async(curried(clientMethodName, args)) setattr(self, name, method) return method raise AttributeError("XwareAdapter doesn't have a {name}.".format(**locals())) @property def sysInfo(self): return self._sysInfo def _donecb_get_getsysinfo(self, future): exception = future.exception() if not exception: result = future.result() self._sysInfo = result else: logging.error("get_getsysinfo failed.") def _donecb_get_list(self, klass, future): exception = future.exception() if not exception: result = future.result() if klass == TaskClass.RUNNING: self._ulSpeed = result["upSpeed"] self._dlSpeed = result["dlSpeed"] self._runningTaskCount = result["dlNum"] self.klassMap.klass(klass).updateData(result["tasks"]) else: logging.error("get_list failed.") def _donecb_get_settings(self, future): exception = future.exception() if not exception: result = future.result() self._xwareSettings.update(result) else: logging.error("get/post settings failed.") def _donecb_post_settings(self, _: "new settings", future): return self._donecb_get_settings(future) def do_pauseTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) self._loop.call_soon_threadsafe(self.post_pause, taskIds) def do_startTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) self._loop.call_soon_threadsafe(self.post_start, taskIds) def do_createTask(self, creation: TaskCreation) -> (bool, str): if creation.kind not in self.__class__.Manifest["SupportedTypes"]: return False, "Not a supported type." # convert path path = self.mountsFaker.convertToMappedPath(creation.path) if not path: return False, "Not pre-mounted." if creation.kind in (TaskCreationType.Normal, TaskCreationType.Emule) or \ creation.kind == TaskCreationType.RemoteTorrent: # TODO xware not properly support yet fileInfo = creation.subtaskInfo[0] # Workaround: xware doesn't acquire filename if not set. filename = fileInfo.name self._loop.call_soon_threadsafe(self.post_createTask, path, creation.url, filename) return True, None elif creation.kind == TaskCreationType.Magnet: # Note: # To add a magnet task, xware requires a name field, same as normal and emule tasks. # But xware will ignore the name parameter and acquire the name on its own. self._loop.call_soon_threadsafe(self.post_createTask, path, creation.url, "解析中的磁力链接") return True, None elif creation.kind == TaskCreationType.LocalTorrent: self._loop.call_soon_threadsafe(self.post_createBtTask, ) return False, "Not implemented." def do_delTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) self._loop.call_soon_threadsafe(self.post_del, taskIds, options["recycle"], options["delete"]) def do_restoreTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) self._loop.call_soon_threadsafe(self.post_restore, taskIds) def do_openLixianChannel(self, taskItem, enable: bool): taskId = taskItem.realid self._loop.call_soon_threadsafe(self.post_openLixianChannel, taskId, enable) def do_openVipChannel(self, taskItem): taskId = taskItem.realid self._loop.call_soon_threadsafe(self.post_openVipChannel, taskId) def do_applySettings(self, settings: dict): dLimit = settings.get("downloadSpeedLimit", -1) uLimit = settings.get("uploadSpeedLimit", -1) if dLimit != -1: self._adapterConfig.setint("dlspeedlimit", dLimit) if uLimit != -1: self._adapterConfig.setint("ulspeedlimit", uLimit) self._loop.call_soon_threadsafe(self.post_settings, settings) # ==================== DAEMON ==================== @pyqtProperty(bool, notify = infoUpdated) def xwaredRunning(self): return self._xwaredRunning @pyqtProperty(int, notify = infoUpdated) def etmPid(self): return self._etmPid @pyqtProperty(str, notify = infoUpdated) def peerId(self): return self._peerId @pyqtProperty(int, notify = infoUpdated) def startEtmWhen(self): return self._startEtmWhen @startEtmWhen.setter def startEtmWhen(self, value): self._loop.call_soon_threadsafe(self.daemon_setStartEtmWhen, value) def _donecb_daemon_infoPoll(self, data): error = data.get("error") if not error: result = data.get("result") self._xwaredRunning = True self._etmPid = result.get("etmPid") self._peerId = result.get("peerId") lcPort = result.get("lcPort") self._startEtmWhen = result.get("startEtmWhen") else: self._xwaredRunning = False self._etmPid = 0 self._peerId = "" lcPort = 0 self._startEtmWhen = 1 print("infoPoll failed with error", error, file = sys.stderr) self.setClientOptions({ "port": lcPort, }) self.infoUpdated.emit() def do_daemon_start(self): self._loop.call_soon_threadsafe(self.daemon_startETM) def do_daemon_restart(self): self._loop.call_soon_threadsafe(self.daemon_restartETM) def do_daemon_stop(self): self._loop.call_soon_threadsafe(self.daemon_stopETM) def do_daemon_startFrontend(self): raise NotImplementedError() # handled in main() # self._loop.call_soon_threadsafe(self.daemon_start) def do_daemon_quitFrontend(self): self._loop.call_soon_threadsafe(self.daemon_quit) @property def daemonManagedBySystemd(self): return os.path.lexists(constants.SYSTEMD_SERVICE_ENABLED_USERFILE) and \ os.path.lexists(constants.SYSTEMD_SERVICE_USERFILE) @daemonManagedBySystemd.setter def daemonManagedBySystemd(self, on): if on: tryMkdir(os.path.dirname(constants.SYSTEMD_SERVICE_ENABLED_USERFILE)) trySymlink(constants.SYSTEMD_SERVICE_FILE, constants.SYSTEMD_SERVICE_USERFILE) trySymlink(constants.SYSTEMD_SERVICE_USERFILE, constants.SYSTEMD_SERVICE_ENABLED_USERFILE) else: tryRemove(constants.SYSTEMD_SERVICE_ENABLED_USERFILE) tryRemove(constants.SYSTEMD_SERVICE_USERFILE) if getInitType() == InitType.SYSTEMD: os.system("systemctl --user daemon-reload") @property def daemonManagedByUpstart(self): return os.path.lexists(constants.UPSTART_SERVICE_USERFILE) @daemonManagedByUpstart.setter def daemonManagedByUpstart(self, on): if on: tryMkdir(os.path.dirname(constants.UPSTART_SERVICE_USERFILE)) trySymlink(constants.UPSTART_SERVICE_FILE, constants.UPSTART_SERVICE_USERFILE) else: tryRemove(constants.UPSTART_SERVICE_USERFILE) if getInitType() == InitType.UPSTART: os.system("initctl --user reload-configuration") @property def daemonManagedByAutostart(self): return os.path.lexists(constants.AUTOSTART_DESKTOP_USERFILE) @daemonManagedByAutostart.setter def daemonManagedByAutostart(self, on): if on: tryMkdir(os.path.dirname(constants.AUTOSTART_DESKTOP_USERFILE)) trySymlink(constants.AUTOSTART_DESKTOP_FILE, constants.AUTOSTART_DESKTOP_USERFILE) else: tryRemove(constants.AUTOSTART_DESKTOP_USERFILE)
class Aria2Adapter(QObject): initialized = pyqtSignal() statPolled = pyqtSignal() Manifest = { "SupportedTypes": [ TaskCreationType.Normal, TaskCreationType.Magnet, TaskCreationType.RemoteTorrent, TaskCreationType.LocalTorrent, ], } def __init__(self, *, adapterConfig, taskModel, parent=None): super().__init__(parent) self._adapterConfig = adapterConfig self._ws = None self.klassMap = KlassMap(adapter=self, namespace=self.namespace, taskModel=taskModel) self.klassMap.addTaskMap(TaskMap(klass=Aria2TaskClass.Active)) self.klassMap.addTaskMap(TaskMap(klass=Aria2TaskClass.Waiting)) self.klassMap.addTaskMap(TaskMap(klass=Aria2TaskClass.Stopped)) # Stats self._ulSpeed = 0 self._dlSpeed = 0 self._activeCount = 0 self._waitingCount = 0 self._stoppedCount = 0 self._stoppedTotalCount = 0 self._ids = {} self._loop = None self._loop_executor = None self._loop_thread = threading.Thread(daemon=True, target=self._startEventLoop, name=adapterConfig.name) def start(self): self._loop_thread.start() def _startEventLoop(self): self._loop = asyncio.new_event_loop() self._loop.set_debug(True) self._loop_executor = ThreadPoolExecutor(max_workers=1) self._loop.set_default_executor(self._loop_executor) asyncio.events.set_event_loop(self._loop) asyncio.ensure_future(self.main()) self._loop.run_forever() def updateOptions(self, options): self._options.update(options) def _ready(self): if not self._ws: return False return self._ws.open @pyqtProperty(str, notify=initialized) def name(self): return self._adapterConfig["name"] @pyqtProperty(str, notify=initialized) def connection(self): return self._adapterConfig["connection"] @pyqtProperty(str, notify=initialized) def namespace(self): return "aria2-" + self._adapterConfig.name[len("adapter-"):] @asyncio.coroutine def main(self): asyncio.ensure_future(self._getMessage()) # It can handle reconnect while True: try: self._ws = yield from websockets.client.connect( self._adapterConfig["connection"]) except ConnectionRefusedError: yield from asyncio.sleep(1) continue while True: if not self._ready(): break asyncio.ensure_future( self._call(_Callable(Aria2Method.GetGlobalStat))) asyncio.ensure_future( self._call(_Callable(Aria2Method.TellActive))) asyncio.ensure_future( self._call(_Callable(Aria2Method.TellWaiting, 0, 100000))) asyncio.ensure_future( self._call(_Callable(Aria2Method.TellStopped, 0, 100000))) yield from asyncio.sleep(1) @asyncio.coroutine def _getMessage(self): while True: if not self._ready(): yield from asyncio.sleep(1) # try again in 1 sec continue msg = yield from self._ws.recv() if not msg: continue # disconnected datas = json.loads(msg) # make a single call's response look like being part of a batch call if not isinstance(datas, list): datas = (datas, ) for data in datas: assert data["jsonrpc"] == "2.0" if "error" in data: print("Error Handling", data) # TODO else: id_ = data.get("id", None) if id_: # response shortName = _getShortName(id_) result = data["result"] else: # notifications shortName = _getShortName(data["method"]) result = data["params"] cb = getattr(self, "_cb_" + shortName, None) if cb: assert asyncio.iscoroutinefunction(cb) yield from cb(result) @asyncio.coroutine def _call(self, callable_: _Callable): assert isinstance(callable_, _Callable) additionals = dict() if "rpc-secret" in self._adapterConfig: additionals["token"] = self._adapterConfig["rpc-secret"] payload = callable_.toString(**additionals) yield from self._ws.send(payload) def _callFromExternal(self, callable_: _Callable): self._loop.call_soon_threadsafe(asyncio.ensure_future, self._call(callable_)) def do_createTask(self, creation: TaskCreation) -> (bool, str): url = [creation.url] options = dict(dir=creation.path) if creation.kind in (TaskCreationType.Normal, ): # see if need to override filename fileInfo = creation.subtaskInfo[0] if fileInfo.name_userset: options["out"] = fileInfo.name self._callFromExternal(_Callable(Aria2Method.AddUri, url, options)) return True, None elif creation.kind == TaskCreationType.Magnet: self._callFromExternal(_Callable(Aria2Method.AddUri, url, dict())) return True, None return False, "Not implemented." def do_getFiles(self, gid): self._callFromExternal(_Callable(Aria2Method.GetFiles, gid)) def do_delTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) calls = map( lambda gid: _Callable(Aria2Method.RemoveDownloadResult, gid), taskIds) delMultiple = _Callable(Aria2Method.MultiCall, *calls) # TODO: aria2 don't remove files from filesystem. self._callFromExternal(delMultiple) @asyncio.coroutine def _cb_tellActive(self, result): self.klassMap.klass(Aria2TaskClass.Active).updateData(result) @asyncio.coroutine def _cb_tellWaiting(self, result): self.klassMap.klass(Aria2TaskClass.Waiting).updateData(result) @asyncio.coroutine def _cb_tellStopped(self, result): self.klassMap.klass(Aria2TaskClass.Stopped).updateData(result) @asyncio.coroutine def _cb_getGlobalOption(self, result): pass @asyncio.coroutine def _cb_getFiles(self, result): print("_cb_getFiles", result) @asyncio.coroutine def _cb_onDownloadStart(self, event): print("_cb_onDownloadComplete", event) @asyncio.coroutine def _cb_onDownloadPause(self, event): print("_cb_onDownloadPause", event) @asyncio.coroutine def _cb_onDownloadStop(self, event): print("_cb_onDownloadStop", event) @asyncio.coroutine def _cb_onDownloadComplete(self, event): print("_cb_onDownloadComplete", event) @asyncio.coroutine def _cb_onDownloadError(self, event): print("_cb_onDownloadError", event) @asyncio.coroutine def _cb_onBtDownloadComplete(self, event): print("_cb_onBtDownloadComplete", event) # Stats @pyqtProperty(int, notify=statPolled) def dlSpeed(self): return self._dlSpeed @pyqtProperty(int, notify=statPolled) def ulSpeed(self): return self._ulSpeed @pyqtProperty(int, notify=statPolled) def runningTaskCount(self): return 0 # TODO @asyncio.coroutine def _cb_getGlobalStat(self, result): self._dlSpeed = int(result["downloadSpeed"]) self._ulSpeed = int(result["uploadSpeed"]) # TODO: waiting, stopped, stoppedtotal, active self.statPolled.emit()
class Aria2Adapter(QObject): initialized = pyqtSignal() statPolled = pyqtSignal() Manifest = { "SupportedTypes": [ TaskCreationType.Normal, TaskCreationType.Magnet, TaskCreationType.RemoteTorrent, TaskCreationType.LocalTorrent, ], } def __init__(self, *, adapterConfig, taskModel, parent = None): super().__init__(parent) self._adapterConfig = adapterConfig self._ws = None self.klassMap = KlassMap(adapter = self, namespace = self.namespace, taskModel = taskModel) self.klassMap.addTaskMap( TaskMap(klass = Aria2TaskClass.Active) ) self.klassMap.addTaskMap( TaskMap(klass = Aria2TaskClass.Waiting) ) self.klassMap.addTaskMap( TaskMap(klass = Aria2TaskClass.Stopped) ) # Stats self._ulSpeed = 0 self._dlSpeed = 0 self._activeCount = 0 self._waitingCount = 0 self._stoppedCount = 0 self._stoppedTotalCount = 0 self._ids = {} self._loop = None self._loop_executor = None self._loop_thread = threading.Thread(daemon = True, target = self._startEventLoop, name = adapterConfig.name) def start(self): self._loop_thread.start() def _startEventLoop(self): self._loop = asyncio.new_event_loop() self._loop.set_debug(True) self._loop_executor = ThreadPoolExecutor(max_workers = 1) self._loop.set_default_executor(self._loop_executor) asyncio.events.set_event_loop(self._loop) asyncio.async(self.main()) self._loop.run_forever() def updateOptions(self, options): self._options.update(options) def _ready(self): if not self._ws: return False return self._ws.open @pyqtProperty(str, notify = initialized) def name(self): return self._adapterConfig["name"] @pyqtProperty(str, notify = initialized) def connection(self): return self._adapterConfig["connection"] @pyqtProperty(str, notify = initialized) def namespace(self): return "aria2-" + self._adapterConfig.name[len("adapter-"):] @asyncio.coroutine def main(self): asyncio.async(self._getMessage()) # It can handle reconnect while True: try: self._ws = yield from websockets.client.connect( self._adapterConfig["connection"] ) except ConnectionRefusedError: yield from asyncio.sleep(1) continue while True: if not self._ready(): break asyncio.async(self._call(_Callable(Aria2Method.GetGlobalStat))) asyncio.async(self._call(_Callable(Aria2Method.TellActive))) asyncio.async(self._call(_Callable(Aria2Method.TellWaiting, 0, 100000))) asyncio.async(self._call(_Callable(Aria2Method.TellStopped, 0, 100000))) yield from asyncio.sleep(1) @asyncio.coroutine def _getMessage(self): while True: if not self._ready(): yield from asyncio.sleep(1) # try again in 1 sec continue msg = yield from self._ws.recv() if not msg: continue # disconnected datas = json.loads(msg) # make a single call's response look like being part of a batch call if not isinstance(datas, list): datas = (datas,) for data in datas: assert data["jsonrpc"] == "2.0" if "error" in data: print("Error Handling", data) # TODO else: id_ = data.get("id", None) if id_: # response shortName = _getShortName(id_) result = data["result"] else: # notifications shortName = _getShortName(data["method"]) result = data["params"] cb = getattr(self, "_cb_" + shortName, None) if cb: assert asyncio.iscoroutinefunction(cb) yield from cb(result) @asyncio.coroutine def _call(self, callable_: _Callable): assert isinstance(callable_, _Callable) additionals = dict() if "rpc-secret" in self._adapterConfig: additionals["token"] = self._adapterConfig["rpc-secret"] payload = callable_.toString(**additionals) yield from self._ws.send(payload) def _callFromExternal(self, callable_: _Callable): self._loop.call_soon_threadsafe(asyncio.async, self._call(callable_)) def do_createTask(self, creation: TaskCreation) -> (bool, str): url = [creation.url] options = dict(dir = creation.path) if creation.kind in (TaskCreationType.Normal,): # see if need to override filename fileInfo = creation.subtaskInfo[0] if fileInfo.name_userset: options["out"] = fileInfo.name self._callFromExternal( _Callable(Aria2Method.AddUri, url, options) ) return True, None elif creation.kind == TaskCreationType.Magnet: self._callFromExternal( _Callable(Aria2Method.AddUri, url, dict()) ) return True, None return False, "Not implemented." def do_getFiles(self, gid): self._callFromExternal(_Callable(Aria2Method.GetFiles, gid)) def do_delTasks(self, tasks, options): taskIds = map(lambda t: t.realid, tasks) calls = map(lambda gid: _Callable(Aria2Method.RemoveDownloadResult, gid), taskIds) delMultiple = _Callable(Aria2Method.MultiCall, *calls) # TODO: aria2 don't remove files from filesystem. self._callFromExternal(delMultiple) @asyncio.coroutine def _cb_tellActive(self, result): self.klassMap.klass(Aria2TaskClass.Active).updateData(result) @asyncio.coroutine def _cb_tellWaiting(self, result): self.klassMap.klass(Aria2TaskClass.Waiting).updateData(result) @asyncio.coroutine def _cb_tellStopped(self, result): self.klassMap.klass(Aria2TaskClass.Stopped).updateData(result) @asyncio.coroutine def _cb_getGlobalOption(self, result): pass @asyncio.coroutine def _cb_getFiles(self, result): print("_cb_getFiles", result) @asyncio.coroutine def _cb_onDownloadStart(self, event): print("_cb_onDownloadComplete", event) @asyncio.coroutine def _cb_onDownloadPause(self, event): print("_cb_onDownloadPause", event) @asyncio.coroutine def _cb_onDownloadStop(self, event): print("_cb_onDownloadStop", event) @asyncio.coroutine def _cb_onDownloadComplete(self, event): print("_cb_onDownloadComplete", event) @asyncio.coroutine def _cb_onDownloadError(self, event): print("_cb_onDownloadError", event) @asyncio.coroutine def _cb_onBtDownloadComplete(self, event): print("_cb_onBtDownloadComplete", event) # Stats @pyqtProperty(int, notify = statPolled) def dlSpeed(self): return self._dlSpeed @pyqtProperty(int, notify = statPolled) def ulSpeed(self): return self._ulSpeed @pyqtProperty(int, notify = statPolled) def runningTaskCount(self): return 0 # TODO @asyncio.coroutine def _cb_getGlobalStat(self, result): self._dlSpeed = int(result["downloadSpeed"]) self._ulSpeed = int(result["uploadSpeed"]) # TODO: waiting, stopped, stoppedtotal, active self.statPolled.emit()
class KlassMapTest(TestCase): def setUp(self): adapter = mock.Mock() taskModel = mock.Mock() self.km = KlassMap(adapter = adapter, namespace = "foo-ins1", taskModel = taskModel) self.tm1 = TaskMapBase(klass = 0) self.tm1.__class__._Item = DummyTaskItem self.km.addTaskMap(self.tm1) self.tm2 = TaskMapBase(klass = 1) self.tm2.__class__._Item = DummyTaskItem self.km.addTaskMap(self.tm2) self.am = mock.Mock() self.km.setAdapterMap(self.am) def test_create_init(self): self.assertEqual(self.km.namespace, "foo-ins1") self.assertEqual(len(self.km), 0) self.assertRaises(NotImplementedError, self.km.__setitem__, "what", "ever") # namespace is set self.assertEqual(self.tm1.namespace, "foo-ins1") def test_create_same_klass(self): # try add it again, but with the same class tm1_1 = TaskMapBase(klass = 0) self.assertRaises(RuntimeError, self.km.addTaskMap, tm1_1) def test_task_add_update_delete(self): self.tm1.updateData({ "1": "task1", "2": "task2", }) self.assertEqual(len(self.km), 2) self.assertEqual(self.km["1"].value, "task1") self.assertEqual(self.km["1"].klass, 0) self.assertEqual(self.km["2"].value, "task2") self.assertEqual(self.km["2"].klass, 0) self.assertEqual(self.km["2"].isDeletionPending, False) # change item self.tm1.updateData({ "1": "task1!", "2": "task2", }) self.assertEqual(len(self.km), 2) self.assertEqual(self.km["1"].value, "task1!") self.assertEqual(self.km["2"].value, "task2") self.assertEqual(self.km["2"].isDeletionPending, False) # remove one item self.tm1.updateData({ "1": "task1!", }) self.assertEqual(len(self.km), 2) self.assertEqual(self.km["2"].isDeletionPending, True) # try to remove it again, shouldn't remove self.tm1.updateData({ "1": "task1!", }) self.assertEqual(len(self.km), 2) self.assertEqual(self.km["2"].isDeletionPending, True) # updateData with taskMap2 self.tm2.updateData({}) self.assertEqual(len(self.km), 1) self.assertRaises(KeyError, self.km.__getitem__, "2") def test_task_move(self): # order: 1, 2, then move 1 to 2 self.tm1.updateData({ "1": "task1", }) self.tm2.updateData({ "1": "task1??", }) self.tm1.updateData({}) self.assertEqual(self.km["1"].isDeletionPending, True) # until tm1 deletes it, it shouldn't be in tm2 self.assertEqual(len(self.tm1), 1) self.assertEqual(len(self.tm2), 0) # after tm2 updates again, it should be moved self.tm2.updateData({ "1": "task1??", }) self.assertEqual(len(self.tm1), 0) self.assertEqual(len(self.tm2), 1) self.assertEqual(self.tm2["1"].value, "task1??") self.assertEqual(self.tm2["1"].isDeletionPending, False) self.assertFalse(self.am.afterMove.called) def test_task_move_model_move_down(self): # set tm1 = [0,1,2,3] tm2=[4,5] self.tm1.updateData({ "0": "task0", }) self.tm1.updateData({ "0": "task0", "1": "task1", }) self.tm1.updateData({ "0": "task0", "1": "task1", "2": "task2", }) self.tm1.updateData({ "0": "task0", "1": "task1", "2": "task2", "3": "task3", }) self.tm2.updateData({ "4": "task4", "5": "task5", }) # move 2 from tm1 to tm2 self.assertFalse(self.km["2"].isDeletionPending) self.tm1.updateData({ "0": "task0", "1": "task1", "3": "task3", }) self.assertTrue(self.km["2"].isDeletionPending) self.tm2.updateData({ "2": "task2", "4": "task4", "5": "task5", }) self.assertFalse(self.km["2"].isDeletionPending) self.am.beforeMove.assert_called_with("foo-ins1", 2, 6) def test_task_move_model_move_up(self): # tm1 is empty; tm2's 3rd item is task2 self.tm1.updateData({}) self.tm2.updateData({ "0": "task0", "1": "task1", }) self.tm2.updateData({ "0": "task0", "1": "task1", "2": "task2", }) self.tm2.updateData({ "0": "task0", "1": "task1", "2": "task2", "3": "task3", "4": "task4", "5": "task5", }) # move task2 to tm1 from tm2 self.assertFalse(self.km["2"].isDeletionPending) self.tm2.updateData({ "0": "task0", "1": "task1", "3": "task3", "4": "task4", "5": "task5", }) self.assertTrue(self.km["2"].isDeletionPending) self.tm1.updateData({ "2": "task2moved", }) self.assertFalse(self.km["2"].isDeletionPending) self.am.beforeMove.assert_called_with("foo-ins1", 2, 0) def test_iter_index(self): self.tm1.updateData({ "1": 10, }) self.assertEqual(self.km.index("1"), 0) self.tm2.updateData({ "4": 41, }) self.assertEqual(self.km.index("4"), 1) self.tm2.updateData({ "4": 41, "2": 25, }) self.assertEqual(self.km.index("2"), 2) self.tm1.updateData({ "1": 10, "3": 37, }) self.assertEqual(self.km.index("3"), 1) # index() with self.assertRaises(ValueError): self.km.index("0") # __contains__ for key in ["1", "2", "3", "4"]: self.assertTrue(key in self.km, key) for key in ["5", "6", "7"]: self.assertFalse(key in self.km, key) # __iter__ self.assertListEqual(list(self.km.__iter__()), ['1', '3', '4', '2']) # items() self.assertListEqual( list(map(lambda pair: (pair[0], pair[1].value), self.km.items())), [("1", 10), ("3", 37), ("4", 41), ("2", 25)], ) # values() self.assertListEqual( list(map(lambda i: i.value, self.km.values())), [10, 37, 41, 25], ) def test_get_klassMap(self): self.assertEqual(self.km.klass(0), self.tm1) self.assertNotEqual(self.km.klass(1), self.tm1) def test_findItemKlass(self): self.tm1.updateData({ "1": 1, "2": 2, }) self.tm2.updateData({ "3": 3, }) self.assertEqual(self.km.findItemKlass("1"), 0) self.assertEqual(self.km.findItemKlass("2"), 0) self.assertEqual(self.km.findItemKlass("3"), 1) with self.assertRaises(KeyError): self.km.findItemKlass("4")