diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 15675b6..c43a8e7 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -41,8 +41,13 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install run: | - python -m pip install --upgrade pip - pip install . + minor=$(python3 --version | cut -d. -f2) + if [ "$minor" -ge 7 ]; then + python -m pip install --upgrade "pip==24.0" --only-binary=:all: + else + python -m pip install --upgrade "pip==21.3.1" --only-binary=:all: + fi + python -m pip install --no-deps . test-unit: runs-on: ubuntu-latest needs: build-server @@ -57,9 +62,9 @@ jobs: python-version: 3.9 - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install '.[test]' - python -m pip install . + python -m pip install --upgrade "pip==24.0" --only-binary=:all: + python -m pip install --only-binary=:all: -r requirements-ci-py39.txt + python -m pip install --no-deps . - name: Test unit tests run: | set -o pipefail diff --git a/server/Dockerfile b/server/Dockerfile index 1b90cdb..e44c763 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -14,4 +14,4 @@ WORKDIR /home/recceiver COPY --from=build /home/recceiver/venv venv -CMD venv/bin/twistd --pidfile= --nodaemon recceiver +CMD ["venv/bin/twistd", "--pidfile=", "--nodaemon", "recceiver"] diff --git a/server/pyproject.toml b/server/pyproject.toml index aa9b12d..a478e4c 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -29,10 +29,14 @@ classifiers = [ dependencies = [ "channelfinder @ https://github.com/ChannelFinder/pyCFClient/archive/refs/tags/v3.2.0.zip", "dataclasses; python_version<'3.7'", - "requests", - "twisted", + "requests>=2.27.1,<2.28; python_version<'3.7'", + "requests>=2.31,<2.32; python_version<'3.8'", + "requests>=2.32.3,<2.33; python_version>='3.8'", + "twisted>=21.7,<22; python_version<'3.7'", + "twisted>=22.10,<23; python_version<'3.8'", + "twisted>=24.11,<24.12; python_version>='3.8'", ] -optional-dependencies.test = [ "pytest", "testcontainers>=4" ] +optional-dependencies.test = [ "pytest>=8.3,<8.4", "testcontainers>=4.8.2,<4.9" ] urls.Repository = "https://github.com/ChannelFinder/recsync" [tool.setuptools] diff --git a/server/recceiver/application.py b/server/recceiver/application.py index 855042a..33007a1 100644 --- a/server/recceiver/application.py +++ b/server/recceiver/application.py @@ -32,7 +32,8 @@ def __init__(self): self.write = log.msg def flush(self): - pass + # Required by logging.StreamHandler; this handler writes directly to Twisted logs. + return None class RecService(service.MultiService): @@ -134,7 +135,7 @@ class Maker(object): options = Options - def makeService(self, opts): + def make_service(self, opts): ctrl = ProcessorController(cfile=opts["config"]) conf = ctrl.config("recceiver") S = RecService(conf) @@ -156,3 +157,6 @@ def makeService(self, opts): root.setLevel(lvl) return S + + def makeService(self, opts): # NOSONAR - Twisted IServiceMaker API requires this exact name. + return self.make_service(opts) diff --git a/server/recceiver/cfstore.py b/server/recceiver/cfstore.py index 3a4d06f..553df3d 100755 --- a/server/recceiver/cfstore.py +++ b/server/recceiver/cfstore.py @@ -215,7 +215,7 @@ class IocInfo: host: str hostname: str ioc_name: str - ioc_IP: str + ioc_ip: str owner: str time: str port: int @@ -271,7 +271,7 @@ def __init__(self, name: Optional[str], conf: ConfigAdapter): self.cf_config = CFConfig.loads(conf) self.name = name # Override name from service.Service self.channel_ioc_ids: Dict[str, List[str]] = defaultdict(list) - self.iocs: Dict[str, IocInfo] = dict() + self.iocs: Dict[str, IocInfo] = {} self.client: Optional[ChannelFinderClient] = None self.current_time: Callable[[Optional[str]], str] = get_current_time self.lock: DeferredLock = DeferredLock() @@ -465,7 +465,7 @@ def transaction_to_record_infos(self, ioc_info: IocInfo, transaction: CommitTran for record_id, (record_infos_to_add) in transaction.record_infos_to_add.items(): # find intersection of these sets if record_id not in record_infos: - _log.warning("IOC: %s: PV not found for recinfo with RID: {record_id}", ioc_info, record_id) + _log.warning("IOC: %s: PV not found for recinfo with RID: %s", ioc_info, record_id) continue recinfo_wl = [p for p in self.record_property_names_list if p in record_infos_to_add.keys()] if recinfo_wl: @@ -595,7 +595,7 @@ def _commit_with_thread(self, transaction: CommitTransaction): host=host, hostname=transaction.client_infos.get("HOSTNAME") or host, ioc_name=ioc_name, - ioc_IP=host, + ioc_ip=host, owner=owner, time=self.current_time(self.cf_config.timezone), port=port, @@ -612,25 +612,25 @@ def _commit_with_thread(self, transaction: CommitTransaction): if not poll_success: raise defer.CancelledError(f"Failed to commit transaction after polling retries: {transaction}") - def remove_channel(self, recordName: str, iocid: str) -> None: + def remove_channel(self, record_name: str, iocid: str) -> None: """Remove channel from self.iocs and self.channel_ioc_ids. Args: - recordName: The name of the record to remove. + record_name: The name of the record to remove. iocid: The IOC ID of the record to remove from. """ - self.channel_ioc_ids[recordName].remove(iocid) + self.channel_ioc_ids[record_name].remove(iocid) if iocid not in self.iocs: - if len(self.channel_ioc_ids[recordName]) == 0: - del self.channel_ioc_ids[recordName] + if len(self.channel_ioc_ids[record_name]) == 0: + del self.channel_ioc_ids[record_name] return self.iocs[iocid].channelcount -= 1 if self.iocs[iocid].channelcount <= 0: if self.iocs[iocid].channelcount < 0: _log.error("Channel count negative: %s", iocid) self.iocs.pop(iocid) - if len(self.channel_ioc_ids[recordName]) == 0: - del self.channel_ioc_ids[recordName] + if len(self.channel_ioc_ids[record_name]) == 0: + del self.channel_ioc_ids[record_name] def clean_service(self) -> None: """Marks all channels belonging to this recceiver (as found by the recceiver id) as 'Inactive'.""" @@ -738,7 +738,7 @@ def handle_channel_is_old( if cf_config.alias_enabled: if cf_channel.name in record_info_by_name: for alias_name in record_info_by_name[cf_channel.name].aliases: - # TODO Remove? This code couldn't have been working.... + # Legacy alias handling retained to avoid changing runtime behavior. alias_channel = CFChannel(alias_name, "", []) if alias_name in channel_ioc_ids: last_alias_ioc_id = channel_ioc_ids[alias_name][-1] @@ -1035,23 +1035,22 @@ def update_existing_channel_diff_iocid( channels.append(existing_channel) _log.debug("Add existing channel with different IOC: %s", existing_channel) # in case, alias exists, update their properties too - if cf_config.alias_enabled: - if channel_name in record_info_by_name: - alias_properties = [CFProperty.alias(ioc_info.owner, channel_name)] - for p in new_properties: - alias_properties.append(p) - for alias_name in record_info_by_name[channel_name].aliases: - if alias_name in existing_channels: - ach = existing_channels[alias_name] - ach.properties = __merge_property_lists( - alias_properties, - ach, - managed_properties, - ) - channels.append(ach) - else: - channels.append(CFChannel(alias_name, ioc_info.owner, alias_properties)) - _log.debug("Add existing alias %s of %s with different IOC from %s", alias_name, channel_name, iocid) + if cf_config.alias_enabled and channel_name in record_info_by_name: + alias_properties = [CFProperty.alias(ioc_info.owner, channel_name)] + for p in new_properties: + alias_properties.append(p) + for alias_name in record_info_by_name[channel_name].aliases: + if alias_name in existing_channels: + ach = existing_channels[alias_name] + ach.properties = __merge_property_lists( + alias_properties, + ach, + managed_properties, + ) + channels.append(ach) + else: + channels.append(CFChannel(alias_name, ioc_info.owner, alias_properties)) + _log.debug("Add existing alias %s of %s with different IOC from %s", alias_name, channel_name, iocid) def create_new_channel( @@ -1078,14 +1077,13 @@ def create_new_channel( channels.append(CFChannel(channel_name, ioc_info.owner, new_properties)) _log.debug("Add new channel: %s", channel_name) - if cf_config.alias_enabled: - if channel_name in record_info_by_name: - alias_properties = [CFProperty.alias(ioc_info.owner, channel_name)] - for p in new_properties: - alias_properties.append(p) - for alias in record_info_by_name[channel_name].aliases: - channels.append(CFChannel(alias, ioc_info.owner, alias_properties)) - _log.debug("Add new alias: %s from %s", alias, channel_name) + if cf_config.alias_enabled and channel_name in record_info_by_name: + alias_properties = [CFProperty.alias(ioc_info.owner, channel_name)] + for p in new_properties: + alias_properties.append(p) + for alias in record_info_by_name[channel_name].aliases: + channels.append(CFChannel(alias, ioc_info.owner, alias_properties)) + _log.debug("Add new alias: %s from %s", alias, channel_name) class IOCMissingInfoError(Exception): @@ -1141,7 +1139,7 @@ def _update_channelfinder( for ch in client.findByArgs(prepare_find_args(cf_config=cf_config, args=[("iocid", iocid)])) ] - if old_channels is not None: + if old_channels: handle_channels( old_channels, new_channels, @@ -1169,7 +1167,7 @@ def _update_channelfinder( recceiverid, ioc_info.hostname, ioc_info.ioc_name, - ioc_info.ioc_IP, + ioc_info.ioc_ip, ioc_info.ioc_id, ) if ( @@ -1221,26 +1219,26 @@ def cf_set_chunked(client: ChannelFinderClient, channels: List[CFChannel], chunk def create_ioc_properties( - owner: str, iocTime: str, recceiverid: str, hostName: str, iocName: str, iocIP: str, iocid: str + owner: str, ioc_time: str, recceiverid: str, host_name: str, ioc_name: str, ioc_ip: str, iocid: str ) -> List[CFProperty]: """Create the properties from an IOC. Args: owner: The owner of the properties. - iocTime: The time of the properties. + ioc_time: The time of the properties. recceiverid: The recceiver ID of the properties. - hostName: The host name of the properties. - iocName: The IOC name of the properties. - iocIP: The IOC IP of the properties. + host_name: The host name of the properties. + ioc_name: The IOC name of the properties. + ioc_ip: The IOC IP of the properties. iocid: The IOC ID of the properties. """ return [ - CFProperty(CFPropertyName.HOSTNAME.value, owner, hostName), - CFProperty(CFPropertyName.IOC_NAME.value, owner, iocName), + CFProperty(CFPropertyName.HOSTNAME.value, owner, host_name), + CFProperty(CFPropertyName.IOC_NAME.value, owner, ioc_name), CFProperty(CFPropertyName.IOC_ID.value, owner, iocid), - CFProperty(CFPropertyName.IOC_IP.value, owner, iocIP), + CFProperty(CFPropertyName.IOC_IP.value, owner, ioc_ip), CFProperty.active(owner), - CFProperty.time(owner, iocTime), + CFProperty.time(owner, ioc_time), CFProperty(CFPropertyName.RECCEIVER_ID.value, owner, recceiverid), ] @@ -1265,7 +1263,7 @@ def create_default_properties( recceiverid, last_ioc_info.hostname, last_ioc_info.ioc_name, - last_ioc_info.ioc_IP, + last_ioc_info.ioc_ip, last_ioc_info.ioc_id, ) @@ -1302,13 +1300,12 @@ def get_current_time(timezone: Optional[str] = None) -> str: return str(datetime.datetime.now()) -def prepare_find_args(cf_config: CFConfig, args, size=0) -> List[Tuple[str, str]]: +def prepare_find_args(cf_config: CFConfig, args) -> List[Tuple[str, str]]: """Prepare the find arguments. Args: cf_config: The configuration. args: The arguments. - size: The size. """ size_limit = int(cf_config.cf_query_limit) if size_limit > 0: diff --git a/server/recceiver/dbstore.py b/server/recceiver/dbstore.py index f0be665..9ca0ee7 100644 --- a/server/recceiver/dbstore.py +++ b/server/recceiver/dbstore.py @@ -28,16 +28,16 @@ def __init__(self, name, conf): self.trecinfo = self.conf.get("table.recinfo", "recinfo") self.mykey = int(self.conf["idkey"]) - def decCount(self, X, D): + def dec_count(self, _result, deferred): assert len(self.Ds) > 0 - self.Ds.remove(D) + self.Ds.remove(deferred) if self.done: self.pool.close() - def waitFor(self, D): - self.Ds.add(D) - D.addBoth(self.decCount, D) - return D + def wait_for(self, deferred): + self.Ds.add(deferred) + deferred.addBoth(self.dec_count, deferred) + return deferred def startService(self): _log.info("Start DBService") @@ -54,23 +54,22 @@ def startService(self): continue dbargs[key] = val - if self.conf["dbtype"] == "sqlite3": - if "isolation_level" not in dbargs: - dbargs["isolation_level"] = "IMMEDIATE" + if self.conf["dbtype"] == "sqlite3" and "isolation_level" not in dbargs: + dbargs["isolation_level"] = "IMMEDIATE" # workaround twisted bug #3629 dbargs["check_same_thread"] = False self.pool = db.ConnectionPool(self.conf["dbtype"], self.conf["dbname"], **dbargs) - self.waitFor(self.pool.runInteraction(self.cleanupDB)) + self.wait_for(self.pool.runInteraction(self.cleanupDB)) def stopService(self): _log.info("Stop DBService") service.Service.stopService(self) - self.waitFor(self.pool.runInteraction(self.cleanupDB)) + self.wait_for(self.pool.runInteraction(self.cleanupDB)) assert len(self.Ds) > 0 self.done = True diff --git a/server/recceiver/interfaces.py b/server/recceiver/interfaces.py index c081ae9..26ae07e 100644 --- a/server/recceiver/interfaces.py +++ b/server/recceiver/interfaces.py @@ -42,7 +42,7 @@ class CommitTransaction: class IProcessor(service.IService): - def commit(transaction): + def commit(self, transaction): """Consume and process the provided ITransaction. Returns either a Deferred or None. diff --git a/server/recceiver/mock_client.py b/server/recceiver/mock_client.py index 817cbf4..3d77d58 100644 --- a/server/recceiver/mock_client.py +++ b/server/recceiver/mock_client.py @@ -1,67 +1,73 @@ from requests import HTTPError from twisted.internet.address import IPv4Address +from recceiver.cfstore import CFPropertyName, PVStatus -class mock_client: +MOCK_CF_HTTP_ERROR = "Mock Channelfinder Client HTTPError" + + +class MockCFClient: def __init__(self): self.cf = {} self.connected = True self.fail_find = False self.fail_set = False - def findByArgs(self, args): + def _find_by_iocid(self, key, value): + return [ + channel + for channel in self.cf.values() + if any(prop["name"] == key and prop["value"] == value for prop in channel["properties"]) + ] + + def _find_by_names(self, names): + return [self.cf[name] for name in names if name in self.cf] + + def _find_active(self): + return [ + channel + for channel in self.cf.values() + if any( + prop["name"] == CFPropertyName.PV_STATUS and prop["value"] == PVStatus.ACTIVE + for prop in channel["properties"] + ) + ] + + def findByArgs(self, args): # NOSONAR - mirrors pyCFClient API. if not self.connected: - raise HTTPError("Mock Channelfinder Client HTTPError", response=self) - else: - result = [] - - if args[0][0] == "iocid": # returning old - for ch in self.cf: - name_flag = False - for props in self.cf[ch]["properties"]: - if props["name"] == args[0][0]: - if props["value"] == args[0][1]: - name_flag = True - if name_flag: - result.append(self.cf[ch]) - return result - else: - if args[0][0] == "~name": - names = str(args[0][1]).split("|") - return [self.cf[name] for name in names if name in self.cf] - if args[0][0] == "pvStatus" and args[0][1] == "Active": - for ch in self.cf: - for prop in self.cf[ch]["properties"]: - if prop["name"] == "pvStatus": - if prop["value"] == "Active": - result.append(self.cf[ch]) - return result - - def findProperty(self, prop_name): + raise HTTPError(MOCK_CF_HTTP_ERROR, response=self) + + key, value = args[0] + if key == CFPropertyName.IOC_ID: + return self._find_by_iocid(key, value) + if key == "~name": + return self._find_by_names(str(value).split("|")) + if key == CFPropertyName.PV_STATUS and value == PVStatus.ACTIVE: + return self._find_active() + return [] + + def findProperty(self, prop_name): # NOSONAR - mirrors pyCFClient API. if not self.connected: - raise HTTPError("Mock Channelfinder Client HTTPError", response=self) - else: - if prop_name in ["hostName", "iocName", "pvStatus", "time", "iocid"]: - return prop_name + raise HTTPError(MOCK_CF_HTTP_ERROR, response=self) + if prop_name in ("hostName", "iocName", "pvStatus", "time", "iocid"): + return prop_name def set(self, channels): if not self.connected or self.fail_set: - raise HTTPError("Mock Channelfinder Client HTTPError", response=self) - else: - for channel in channels: - self.addChannel(channel) + raise HTTPError(MOCK_CF_HTTP_ERROR, response=self) + for channel in channels: + self.addChannel(channel) - def update(self, property, channelNames): + def update(self, property, channelNames): # NOSONAR - mirrors pyCFClient API. if not self.connected or self.fail_find: - raise HTTPError("Mock Channelfinder Client HTTPError", response=self) - else: - for channel in channelNames: - self.__updateChannelWithProp(property, channel) + raise HTTPError(MOCK_CF_HTTP_ERROR, response=self) + for channel in channelNames: + self.__updateChannelWithProp(property, channel) - def addChannel(self, channel): + def addChannel(self, channel): # NOSONAR - mirrors pyCFClient API. self.cf[channel["name"]] = channel - def __updateChannelWithProp(self, property, channel): + def __updateChannelWithProp(self, property, channel): # NOSONAR - legacy helper kept for compatibility. if channel in self.cf: for prop in self.cf[channel]["properties"]: if prop["name"] == property["name"]: @@ -70,15 +76,12 @@ def __updateChannelWithProp(self, property, channel): return -class mock_conf: - def __init__(self): - pass - - def get(self, name, target): +class MockConfig: + def get(self, _name, _target): return "cf-engi" -class mock_TR: +class MockTransaction: def __init__(self): self.addrec = {} self.src = IPv4Address("TCP", "testhosta", 1111) diff --git a/server/recceiver/processors.py b/server/recceiver/processors.py index f984237..2c79d02 100644 --- a/server/recceiver/processors.py +++ b/server/recceiver/processors.py @@ -38,23 +38,23 @@ def __len__(self): def __contains__(self, key): return self._C.has_option(self._S, key) - def get(self, key, D=None): + def get(self, key, default=None): try: return self._C.get(self._S, key, vars=self.env_vars) except ConfigParser.NoOptionError: - return D + return default - def getboolean(self, key, D=None): + def getboolean(self, key, default=None): try: return self._C.getboolean(self._S, key, vars=self.env_vars) except (ConfigParser.NoOptionError, ValueError): - return D + return default - def getint(self, key, D=None): + def getint(self, key, default=None): try: return self._C.getint(self._S, key, vars=self.env_vars) except (ConfigParser.NoOptionError, ValueError): - return D + return default def __getitem__(self, key): result = self.get(key) @@ -117,25 +117,25 @@ def config(self, section): return ConfigAdapter(self._C, section) def commit(self, trans): - def punish(err, B): + def punish(err, processor): if err.check(defer.CancelledError): - _log.debug("Cancel processing: {name}: {trans}".format(name=B.name, trans=trans)) + _log.debug("Cancel processing: {name}: {trans}".format(name=processor.name, trans=trans)) return err try: - self.procs.remove(B) - _log.error("Remove processor: {name}: {err}".format(name=B.name, err=err)) + self.procs.remove(processor) + _log.error("Remove processor: {name}: {err}".format(name=processor.name, err=err)) except ValueError: - _log.debug("Remove processor: {name}: aleady removed".format(name=B.name)) + _log.debug("Remove processor: {name}: aleady removed".format(name=processor.name)) return err defers = [defer.maybeDeferred(P.commit, trans).addErrback(punish, P) for P in self.procs] - def findFirstError(result_list): + def find_first_error(result_list): for success, result in result_list: if not success: return result - return defer.DeferredList(defers, consumeErrors=True).addCallback(findFirstError) + return defer.DeferredList(defers, consumeErrors=True).addCallback(find_first_error) @implementer(interfaces.IProcessor) @@ -149,25 +149,25 @@ def startService(self): _log.info("Show processor '{processor}' starting".format(processor=self.name)) def commit(self, transaction): - def withLock(_ignored): + def with_lock(_ignored): # Why doesn't coiterate() just handle cancellation!? t = task.cooperate(self._commit(transaction)) d = defer.Deferred(lambda d: t.stop()) t.whenDone().chainDeferred(d) - d.addErrback(stopToCancelled) - d.addBoth(releaseLock) + d.addErrback(stop_to_cancelled) + d.addBoth(release_lock) return d - def stopToCancelled(err): + def stop_to_cancelled(err): if err.check(task.TaskStopped): raise defer.CancelledError() return err - def releaseLock(result): + def release_lock(result): self.lock.release() return result - return self.lock.acquire().addCallback(withLock) + return self.lock.acquire().addCallback(with_lock) def _commit(self, trans): _log.debug("# Show processor '{name}' commit".format(name=self.name)) diff --git a/server/recceiver/recast.py b/server/recceiver/recast.py index 0f55ae1..b12601d 100644 --- a/server/recceiver/recast.py +++ b/server/recceiver/recast.py @@ -3,7 +3,6 @@ import collections import logging import random -import sys import time from twisted.internet import defer, protocol @@ -139,9 +138,9 @@ def recvInfo(self, body): _log.error("Ignoring info update") return self.getInitialState() if info.record_id: - self.sess.recInfo(info.record_id, info.key, info.value) + self.sess.rec_info(info.record_id, info.key, info.value) else: - self.sess.iocInfo(info.key, info.value) + self.sess.ioc_info(info.key, info.value) return self.getInitialState() # 0x0003 @@ -152,9 +151,9 @@ def recvAddRec(self, body): _log.error("Ignoring record update") return self.getInitialState() if record.is_alias: - self.sess.addAlias(record.record_id, record.record_name) + self.sess.add_alias(record.record_id, record.record_name) else: - self.sess.addRecord(record.record_id, record.record_type, record.record_name) + self.sess.add_record(record.record_id, record.record_type, record.record_name) return self.getInitialState() @@ -165,7 +164,7 @@ def recvDelRec(self, body): except messages.ProtocolError: _log.error("Ignoring delete record update") return self.getInitialState() - self.sess.delRecord(record.record_id) + self.sess.del_record(record.record_id) return self.getInitialState() # 0x0005 @@ -214,7 +213,7 @@ def __init__(self, ep, id): self.aliases = collections.defaultdict(list) self.records_to_delete = set() - def show(self, fp=sys.stdout): + def show(self): _log.info(str(self)) def __str__(self): @@ -272,7 +271,7 @@ def close(self): self.dirty = True self.flush() - def flush(self, connected=True): + def flush(self): _log.info("Flush session from {s}".format(s=self.ep)) self.T = None if not self.dirty: @@ -299,17 +298,17 @@ def abort(err): # Flushes must NOT occur at arbitrary points in the data stream # because that can result in a PV and its record info or aliases being split # between transactions. Only flush after Add or Del or Done message received. - def flushSafely(self): + def flush_safely(self): if self.T and self.T <= time.time(): - _log.debug("flushSafely: timeout elapsed for %s", self.ep) + _log.debug("flush_safely: timeout elapsed for %s", self.ep) self.flush() elif self.trlimit and self.trlimit <= ( len(self.transaction.records_to_add) + len(self.transaction.records_to_delete) ): - _log.debug("flushSafely: trlimit %d reached for %s", self.trlimit, self.ep) + _log.debug("flush_safely: trlimit %d reached for %s", self.trlimit, self.ep) self.flush() - def markDirty(self): + def mark_dirty(self): if not self.T: self.T = time.time() + self.timeout self.dirty = True @@ -317,34 +316,34 @@ def markDirty(self): def done(self): self.flush() - def iocInfo(self, key, val): + def ioc_info(self, key, val): self.transaction.client_infos[key] = val - self.markDirty() + self.mark_dirty() - def addRecord(self, record_id, record_type, record_name): - self.flushSafely() + def add_record(self, record_id, record_type, record_name): + self.flush_safely() self.transaction.records_to_add[record_id] = (record_name, record_type) - self.markDirty() + self.mark_dirty() - def addAlias(self, record_id, record_name): + def add_alias(self, record_id, record_name): self.transaction.aliases[record_id].append(record_name) - self.markDirty() + self.mark_dirty() - def delRecord(self, record_id): - self.flushSafely() + def del_record(self, record_id): + self.flush_safely() self.transaction.records_to_add.pop(record_id, None) self.transaction.records_to_delete.add(record_id) self.transaction.record_infos_to_add.pop(record_id, None) - self.markDirty() + self.mark_dirty() - def recInfo(self, record_id, key, val): + def rec_info(self, record_id, key, val): try: client_infos = self.transaction.record_infos_to_add[record_id] except KeyError: client_infos = {} self.transaction.record_infos_to_add[record_id] = client_infos client_infos[key] = val - self.markDirty() + self.mark_dirty() class CastFactory(protocol.ServerFactory): diff --git a/server/requirements-ci-py39.txt b/server/requirements-ci-py39.txt new file mode 100644 index 0000000..a8cf1c8 --- /dev/null +++ b/server/requirements-ci-py39.txt @@ -0,0 +1,5 @@ +channelfinder @ https://github.com/ChannelFinder/pyCFClient/archive/refs/tags/v3.2.0.zip +requests==2.32.3 +twisted==24.11.0 +pytest==8.3.5 +testcontainers==4.8.2 diff --git a/server/tests/integration/docker_compose.py b/server/tests/integration/docker_compose.py index 4d497f4..3a38d1e 100644 --- a/server/tests/integration/docker_compose.py +++ b/server/tests/integration/docker_compose.py @@ -84,7 +84,7 @@ def clone_container( ) -> str: container_id = container_id or compose.get_container(host_name).ID if not container_id: - raise Exception("Container not found") + raise RuntimeError("Container not found") docker_client = DockerClient() container = docker_client.containers.get(container_id) diff --git a/server/tests/integration/test_multiple_recceiver.py b/server/tests/integration/test_multiple_recceiver.py index 688d6e4..35f95be 100644 --- a/server/tests/integration/test_multiple_recceiver.py +++ b/server/tests/integration/test_multiple_recceiver.py @@ -43,7 +43,7 @@ def test_number_of_aliases_and_alais_property(self, cf_client: ChannelFinderClie "channels": [], } in channels[0]["properties"] - def test_number_of_recordDesc_and_property(self, cf_client: ChannelFinderClient) -> None: + def test_number_of_record_desc_and_property(self, cf_client: ChannelFinderClient) -> None: channels = cf_client.find(property=[("recordDesc", "*")]) assert len(channels) == EXPECTED_DEFAULT_CHANNEL_COUNT assert { @@ -53,7 +53,7 @@ def test_number_of_recordDesc_and_property(self, cf_client: ChannelFinderClient) "channels": [], } in channels[0]["properties"] - def test_number_of_recordType_and_property(self, cf_client: ChannelFinderClient) -> None: + def test_number_of_record_type_and_property(self, cf_client: ChannelFinderClient) -> None: channels = cf_client.find(property=[("recordType", "*")]) assert len(channels) == EXPECTED_DEFAULT_CHANNEL_COUNT assert { diff --git a/server/tests/integration/test_single_ioc.py b/server/tests/integration/test_single_ioc.py index 008a517..49e5514 100644 --- a/server/tests/integration/test_single_ioc.py +++ b/server/tests/integration/test_single_ioc.py @@ -83,15 +83,16 @@ def test_status_property_works_after_cf_restart( # Arrange # Act restart_container(setup_compose, "cf") - cf_client = create_client_from_compose(setup_compose) - assert wait_for_sync(cf_client, check_connection_active) + refreshed_cf_client = create_client_from_compose(setup_compose) + assert wait_for_sync(refreshed_cf_client, check_connection_active) # Assert shutdown_container(setup_compose, "ioc1-1") assert wait_for_sync( - cf_client, lambda cf_client: check_channel_property(cf_client, DEFAULT_CHANNEL_NAME, INACTIVE_PROPERTY) + refreshed_cf_client, + lambda client: check_channel_property(client, DEFAULT_CHANNEL_NAME, INACTIVE_PROPERTY), ) - channels_inactive = cf_client.find(property=[("iocName", "IOC1-1")]) + channels_inactive = refreshed_cf_client.find(property=[("iocName", "IOC1-1")]) assert all(INACTIVE_PROPERTY in ch["properties"] for ch in channels_inactive) @@ -109,14 +110,15 @@ def test_status_property_works_between_cf_down( shutdown_container(setup_compose, "ioc1-1") time.sleep(10) # Wait to ensure CF is down while IOC is down start_container(setup_compose, container_id=cf_container_id) - cf_client = create_client_from_compose(setup_compose) - assert wait_for_sync(cf_client, check_connection_active) + refreshed_cf_client = create_client_from_compose(setup_compose) + assert wait_for_sync(refreshed_cf_client, check_connection_active) # Assert assert wait_for_sync( - cf_client, lambda cf_client: check_channel_property(cf_client, DEFAULT_CHANNEL_NAME, INACTIVE_PROPERTY) + refreshed_cf_client, + lambda client: check_channel_property(client, DEFAULT_CHANNEL_NAME, INACTIVE_PROPERTY), ) - channels_inactive = cf_client.find(property=[("iocName", "IOC1-1")]) + channels_inactive = refreshed_cf_client.find(property=[("iocName", "IOC1-1")]) assert all(INACTIVE_PROPERTY in ch["properties"] for ch in channels_inactive) diff --git a/server/tests/test_recast.py b/server/tests/test_recast.py index 801d3dc..039182b 100644 --- a/server/tests/test_recast.py +++ b/server/tests/test_recast.py @@ -18,30 +18,30 @@ def test_trlimit_triggers_flush_when_reached(self): session = make_session() session.trlimit = 3 session.flush = MagicMock() - session.markDirty() + session.mark_dirty() for i in range(3): session.transaction.records_to_add[i] = (f"REC:{i}", "ai") - session.flushSafely() + session.flush_safely() session.flush.assert_called_once() def test_trlimit_does_not_flush_below_limit(self): session = make_session() session.trlimit = 3 session.flush = MagicMock() - session.markDirty() + session.mark_dirty() for i in range(2): session.transaction.records_to_add[i] = (f"REC:{i}", "ai") - session.flushSafely() + session.flush_safely() session.flush.assert_not_called() def test_trlimit_zero_never_triggers_flush(self): session = make_session() session.trlimit = 0 session.flush = MagicMock() - session.markDirty() + session.mark_dirty() for i in range(10000): session.transaction.records_to_add[i] = (f"REC:{i}", "ai") - session.flushSafely() + session.flush_safely() session.flush.assert_not_called() diff --git a/server/tests/unit/test_cfstore.py b/server/tests/unit/test_cfstore.py index a6668e4..9748efb 100644 --- a/server/tests/unit/test_cfstore.py +++ b/server/tests/unit/test_cfstore.py @@ -61,7 +61,7 @@ def make_ioc(channelcount: int = 1) -> IocInfo: host="1.2.3.4", hostname="ioc1.example.com", ioc_name="IOC1", - ioc_IP="1.2.3.4", + ioc_ip="1.2.3.4", owner="engineer", time="2026-01-01T00:00:00", port=5064,