From e80e41f611b781fd52b1885e1362adbae119940d Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Wed, 12 Feb 2025 08:52:38 -0700 Subject: [PATCH] Update python tests --- .../targets/INST2/lib/sim_inst.py | 2 +- .../scripts/running_script.py | 2 +- .../openc3/topics/decom_interface_topic.rb | 3 +- openc3/python/openc3/api/cmd_api.py | 6 +- .../interfaces/tcpip_server_interface.py | 4 +- openc3/python/openc3/logs/log_writer.py | 24 ++--- .../microservices/interface_microservice.py | 20 ++--- openc3/python/openc3/top_level.py | 14 ++- .../openc3/topics/decom_interface_topic.py | 5 +- .../python/openc3/topics/interface_topic.py | 4 +- openc3/python/openc3/utilities/aws_bucket.py | 1 - .../openc3/utilities/bucket_utilities.py | 19 ++-- openc3/python/openc3/utilities/metric.py | 4 +- .../python/openc3/utilities/store_queued.py | 16 ++-- openc3/python/poetry.lock | 33 ++++++- openc3/python/pyproject.toml | 1 + openc3/python/test/api/test_cmd_api.py | 42 ++++----- openc3/python/test/api/test_interface_api.py | 35 ++++---- .../protocols/test_cmd_response_protocol.py | 9 +- .../protocols/test_ignore_packet_protocol.py | 12 +-- .../protocols/test_template_protocol.py | 88 +++++++++---------- .../python/test/interfaces/test_interface.py | 64 ++++++++------ .../test/interfaces/test_udp_interface.py | 6 +- openc3/python/test/logs/test_stream_log.py | 79 ++++++++--------- openc3/python/test/test_helper.py | 37 ++++---- 25 files changed, 271 insertions(+), 259 deletions(-) diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/sim_inst.py b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/sim_inst.py index bbaf9e5e21..0135dcc3c0 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/sim_inst.py +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/sim_inst.py @@ -238,7 +238,7 @@ def solar_panel_thread_method(self): self.solar_panel_thread_cancel = False break - def graceful_kill(self): + def graceful_kill(self, timeout): self.solar_panel_thread_cancel = True def read(self, count_100hz, time): diff --git a/openc3-cosmos-script-runner-api/scripts/running_script.py b/openc3-cosmos-script-runner-api/scripts/running_script.py index b3eb394b69..342513c86c 100644 --- a/openc3-cosmos-script-runner-api/scripts/running_script.py +++ b/openc3-cosmos-script-runner-api/scripts/running_script.py @@ -424,7 +424,7 @@ def as_json(self): # Private methods - def graceful_kill(self): + def graceful_kill(self, timeout): self.stop = True def initialize_variables(self): diff --git a/openc3/lib/openc3/topics/decom_interface_topic.rb b/openc3/lib/openc3/topics/decom_interface_topic.rb index da5316ef76..7785a40940 100644 --- a/openc3/lib/openc3/topics/decom_interface_topic.rb +++ b/openc3/lib/openc3/topics/decom_interface_topic.rb @@ -20,7 +20,7 @@ module OpenC3 class DecomInterfaceTopic < Topic - def self.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, scope:) + def self.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, timeout: 5, scope:) data = {} data['target_name'] = target_name.to_s.upcase data['cmd_name'] = cmd_name.to_s.upcase @@ -34,7 +34,6 @@ def self.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, scope:) Topic.update_topic_offsets([ack_topic]) decom_id = Topic.write_topic("#{scope}__DECOMINTERFACE__{#{target_name}}", { 'build_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100) - timeout = 5 # Arbitrary 5s timeout time = Time.now while (Time.now - time) < timeout Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis| diff --git a/openc3/python/openc3/api/cmd_api.py b/openc3/python/openc3/api/cmd_api.py index 973f2f7309..ee945d5335 100644 --- a/openc3/python/openc3/api/cmd_api.py +++ b/openc3/python/openc3/api/cmd_api.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -163,7 +163,7 @@ def cmd_raw_no_checks(*args, **kwargs): # Build a command binary -def build_cmd(*args, range_check=True, raw=False, scope=OPENC3_SCOPE, manual=False): +def build_cmd(*args, range_check=True, raw=False, timeout=5, scope=OPENC3_SCOPE, manual=False): match len(args): case 1: target_name, cmd_name, cmd_params = extract_fields_from_cmd_text(args[0]) @@ -181,7 +181,7 @@ def build_cmd(*args, range_check=True, raw=False, scope=OPENC3_SCOPE, manual=Fal cmd_name = cmd_name.upper() cmd_params = {k.upper(): v for k, v in cmd_params.items()} authorize(permission="cmd_info", target_name=target_name, scope=scope, manual=manual) - return DecomInterfaceTopic.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, scope) + return DecomInterfaceTopic.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, timeout, scope) # build_command is DEPRECATED diff --git a/openc3/python/openc3/interfaces/tcpip_server_interface.py b/openc3/python/openc3/interfaces/tcpip_server_interface.py index b09ef95ecc..6ce99b084b 100644 --- a/openc3/python/openc3/interfaces/tcpip_server_interface.py +++ b/openc3/python/openc3/interfaces/tcpip_server_interface.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -215,7 +215,7 @@ def disconnect(self): super().disconnect() # Gracefully kill all the threads - def graceful_kill(self): + def graceful_kill(self, timeout): # This method is just here to prevent warnings pass diff --git a/openc3/python/openc3/logs/log_writer.py b/openc3/python/openc3/logs/log_writer.py index 8c57b65ac5..bc839e20a8 100644 --- a/openc3/python/openc3/logs/log_writer.py +++ b/openc3/python/openc3/logs/log_writer.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -125,15 +125,13 @@ def start(self): # Stops all logging and closes the current log file. def stop(self): - threads = None with self.mutex: - threads = self.close_file(False) + self.close_file(False) self.logging_enabled = False - return threads # Stop all logging, close the current log file, and kill the logging threads. def shutdown(self): - threads = self.stop() + self.stop() with LogWriter.mutex: LogWriter.instances.remove(self) if len(LogWriter.instances) <= 0: @@ -142,13 +140,9 @@ def shutdown(self): if LogWriter.cycle_thread: kill_thread(self, LogWriter.cycle_thread) LogWriter.cycle_thread = None - # Wait for BucketUtilities to finish move_log_file_to_bucket_thread - for thread in threads: - thread.join() self.tmp_dir.cleanup() - return threads - def graceful_kill(self): + def graceful_kill(self, timeout): self.cancel_threads = True # implementation details @@ -277,7 +271,7 @@ def prepare_write( if allow_new_file: self.start_new_file() elif self.cycle_size and ((self.file_size + data_length) > self.cycle_size): - Logger.debug("Log writer start new file due to cycle size {self.cycle_size}") + Logger.debug(f"Log writer start new file due to cycle size {self.cycle_size}") if allow_new_file: self.start_new_file() elif ( @@ -289,7 +283,7 @@ def prepare_write( # Changed to just a error to prevent file thrashing if not self.out_of_order: Logger.error( - "Log writer out of order time detected (increase buffer depth?): {Time.from_nsec_from_epoch(self.previous_time_nsec_since_epoch)} {Time.from_nsec_from_epoch(time_nsec_since_epoch)}" + f"Log writer out of order time detected (increase buffer depth?): {from_nsec_from_epoch(self.previous_time_nsec_since_epoch)} {from_nsec_from_epoch(time_nsec_since_epoch)}" ) self.out_of_order = True # This is needed for the redis offset marker entry at the end of the log file @@ -301,7 +295,7 @@ def prepare_write( # to keep a full file's worth of data in the stream. This is what prevents continuous stream growth. # Returns thread that moves log to bucket def close_file(self, take_mutex=True): - threads = [] + thread = None if take_mutex: self.mutex.acquire() try: @@ -313,7 +307,8 @@ def close_file(self, take_mutex=True): # Cleanup timestamps here so they are unset for the next file self.first_time = None self.last_time = None - threads.append(BucketUtilities.move_log_file_to_bucket(self.filename, bucket_key)) + thread = BucketUtilities.move_log_file_to_bucket(self.filename, bucket_key) + thread.join() # Now that the file is in storage, trim the Redis stream after a delay self.cleanup_offsets.append({}) for redis_topic, last_offset in self.last_offsets: @@ -328,7 +323,6 @@ def close_file(self, take_mutex=True): finally: if take_mutex: self.mutex.release() - return threads def bucket_filename(self): return f"{self.first_timestamp()}__{self.last_timestamp()}" + self.extension() diff --git a/openc3/python/openc3/microservices/interface_microservice.py b/openc3/python/openc3/microservices/interface_microservice.py index 32a45f6e93..45e6624f0c 100644 --- a/openc3/python/openc3/microservices/interface_microservice.py +++ b/openc3/python/openc3/microservices/interface_microservice.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -81,11 +81,11 @@ def start(self): self.thread.start() return self.thread - def stop(self): - kill_thread(self, self.thread) + def stop(self, timeout=1.0): + kill_thread(self, self.thread, timeout=timeout) - def graceful_kill(self): - InterfaceTopic.shutdown(self.interface, scope=self.scope) + def graceful_kill(self, timeout=1.0): + InterfaceTopic.shutdown(self.interface, timeout=timeout, scope=self.scope) time.sleep(0.001) # Allow other threads to run def run(self): @@ -356,11 +356,11 @@ def start(self): self.thread.start() return self.thread - def stop(self): - kill_thread(self, self.thread) + def stop(self, timeout=1.0): + kill_thread(self, self.thread, timeout=timeout) - def graceful_kill(self): - RouterTopic.shutdown(self.router, scope=self.scope) + def graceful_kill(self, timeout=1.0): + RouterTopic.shutdown(self.router, timeout=timeout, scope=self.scope) time.sleep(0.001) # Allow other threads to run def run(self): @@ -815,7 +815,7 @@ def shutdown(self, sig=None): self.stop() super().shutdown() - def graceful_kill(self): + def graceful_kill(self, timeout=1.0): pass # Just to avoid warning diff --git a/openc3/python/openc3/top_level.py b/openc3/python/openc3/top_level.py index 53f9302d5a..470f536b6b 100644 --- a/openc3/python/openc3/top_level.py +++ b/openc3/python/openc3/top_level.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -93,17 +93,13 @@ def set_working_dir(working_dir): # Attempt to gracefully kill a thread # @param owner Object that owns the thread and may have a graceful_kill method # @param thread The thread to gracefully kill -# @param graceful_timeout Timeout in seconds to wait for it to die gracefully -# @param timeout_interval How often to poll for aliveness -# @param hard_timeout Timeout in seconds to wait for it to die ungracefully -def kill_thread(owner, thread, graceful_timeout=1, timeout_interval=0.01, hard_timeout=1): +# @param timeout Timeout in seconds to wait for it to die +def kill_thread(owner, thread, timeout=1.0): if thread: if owner and hasattr(owner, "graceful_kill"): if threading.current_thread() != thread: - owner.graceful_kill() - end_time = time.time() + graceful_timeout - while thread.is_alive() and ((end_time - time.time()) > 0): - time.sleep(timeout_interval) + owner.graceful_kill(timeout=timeout) + thread.join(timeout=timeout) else: Logger.warn("Threads cannot graceful_kill themselves") elif owner: diff --git a/openc3/python/openc3/topics/decom_interface_topic.py b/openc3/python/openc3/topics/decom_interface_topic.py index 6606cb9644..c62616bdfe 100644 --- a/openc3/python/openc3/topics/decom_interface_topic.py +++ b/openc3/python/openc3/topics/decom_interface_topic.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -23,7 +23,7 @@ class DecomInterfaceTopic(Topic): @classmethod - def build_cmd(cls, target_name, cmd_name, cmd_params, range_check, raw, scope=OPENC3_SCOPE): + def build_cmd(cls, target_name, cmd_name, cmd_params, range_check, raw, timeout=5, scope=OPENC3_SCOPE): data = {} data["target_name"] = target_name.upper() data["cmd_name"] = cmd_name.upper() @@ -41,7 +41,6 @@ def build_cmd(cls, target_name, cmd_name, cmd_params, range_check, raw, scope=OP "*", 100, ) - timeout = 5 # Arbitrary 5s timeout start_time = time.time() while (time.time() - start_time) < timeout: for _topic, _msg_id, msg_hash, _redis in Topic.read_topics([ack_topic]): diff --git a/openc3/python/openc3/topics/interface_topic.py b/openc3/python/openc3/topics/interface_topic.py index 02f37663f9..3351d637b7 100644 --- a/openc3/python/openc3/topics/interface_topic.py +++ b/openc3/python/openc3/topics/interface_topic.py @@ -95,7 +95,7 @@ def stop_raw_logging(cls, interface_name, scope=OPENC3_SCOPE): ) @classmethod - def shutdown(cls, interface, scope=OPENC3_SCOPE): + def shutdown(cls, interface, timeout=1.0, scope=OPENC3_SCOPE): InterfaceTopic.while_receive_commands = False Topic.write_topic( f"{{{scope}__CMD}}INTERFACE__{interface.name}", @@ -103,7 +103,7 @@ def shutdown(cls, interface, scope=OPENC3_SCOPE): "*", 100, ) - time.sleep(1) # Give some time for the interface to shutdown + time.sleep(timeout) # Give some time for the interface to shutdown InterfaceTopic.clear_topics(InterfaceTopic.topics(interface, scope=scope)) @classmethod diff --git a/openc3/python/openc3/utilities/aws_bucket.py b/openc3/python/openc3/utilities/aws_bucket.py index 051b131dd3..edae0c331a 100644 --- a/openc3/python/openc3/utilities/aws_bucket.py +++ b/openc3/python/openc3/utilities/aws_bucket.py @@ -40,7 +40,6 @@ s3_endpoint_url = f"https://s3.{AWS_REGION}.amazonaws.com" s3_session = boto3.session.Session(region_name=AWS_REGION) - class AwsBucket(Bucket): CREATE_CHECK_COUNT = 100 # 10 seconds diff --git a/openc3/python/openc3/utilities/bucket_utilities.py b/openc3/python/openc3/utilities/bucket_utilities.py index af2da3242c..1ceecce9d1 100644 --- a/openc3/python/openc3/utilities/bucket_utilities.py +++ b/openc3/python/openc3/utilities/bucket_utilities.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -14,17 +14,13 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. -from openc3.utilities.bucket import Bucket - -# from openc3.utilities.target_file import TargetFile -from openc3.utilities.logger import Logger -from openc3.models.reducer_model import ReducerModel -from openc3.environment import OPENC3_LOGS_BUCKET -import zlib import os +import zlib import time import threading - +from openc3.utilities.bucket import Bucket +from openc3.utilities.logger import Logger +from openc3.models.reducer_model import ReducerModel class BucketUtilities: FILE_TIMESTAMP_FORMAT = "%Y%m%d%H%M%S%N" @@ -66,6 +62,7 @@ def move_log_file_to_bucket_thread(cls, filename, bucket_key, metadata={}): filename = cls.compress_file(filename) bucket_key += ".gz" + bucket_name = os.environ.get('OPENC3_LOGS_BUCKET') retry_count = 0 while retry_count < 3: try: @@ -74,7 +71,7 @@ def move_log_file_to_bucket_thread(cls, filename, bucket_key, metadata={}): # to be held in memory! with open(filename, "rb") as file: client.put_object( - bucket=OPENC3_LOGS_BUCKET, + bucket=bucket_name, key=bucket_key, body=file, metadata=metadata, @@ -88,7 +85,7 @@ def move_log_file_to_bucket_thread(cls, filename, bucket_key, metadata={}): Logger.warn(f"Error saving log file to bucket - retry {retry_count}: {filename}\n{str(err)}") time.sleep(1) - Logger.debug(f"wrote {OPENC3_LOGS_BUCKET}/{bucket_key}") + Logger.debug(f"wrote {bucket_name}/{bucket_key}") ReducerModel.add_file(bucket_key) # Record the new file for data reduction if orig_filename: diff --git a/openc3/python/openc3/utilities/metric.py b/openc3/python/openc3/utilities/metric.py index 7896293241..e906785ad0 100644 --- a/openc3/python/openc3/utilities/metric.py +++ b/openc3/python/openc3/utilities/metric.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -112,7 +112,7 @@ def shutdown(self): kill_thread(self, Metric.update_thread) Metric.update_thread = None - def graceful_kill(self): + def graceful_kill(self, timeout): pass @classmethod diff --git a/openc3/python/openc3/utilities/store_queued.py b/openc3/python/openc3/utilities/store_queued.py index ade426eb13..5abdf3e1ba 100644 --- a/openc3/python/openc3/utilities/store_queued.py +++ b/openc3/python/openc3/utilities/store_queued.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -27,17 +27,13 @@ # Attempt to gracefully kill a thread # @param owner Object that owns the thread and may have a graceful_kill method # @param thread The thread to gracefully kill -# @param graceful_timeout Timeout in seconds to wait for it to die gracefully -# @param timeout_interval How often to poll for aliveness -# @param hard_timeout Timeout in seconds to wait for it to die ungracefully -def kill_thread(owner, thread, graceful_timeout=1, timeout_interval=0.01, hard_timeout=1): +# @param timeout Timeout in seconds to wait for it to die gracefully +def kill_thread(owner, thread, timeout=1.0): if thread: if owner and hasattr(owner, "graceful_kill"): if threading.current_thread() != thread: - owner.graceful_kill() - end_time = time.time() + graceful_timeout - while thread.is_alive() and ((end_time - time.time()) > 0): - time.sleep(timeout_interval) + owner.graceful_kill(timeout=timeout) + thread.join(timeout=timeout) class StoreQueued(metaclass=StoreMeta): @@ -119,7 +115,7 @@ def method(*args, **kwargs): def store_instance(self): return Store.instance() - def graceful_kill(self): + def graceful_kill(self, timeout): # Do nothing pass diff --git a/openc3/python/poetry.lock b/openc3/python/poetry.lock index 24b12f31de..cb95832061 100644 --- a/openc3/python/poetry.lock +++ b/openc3/python/poetry.lock @@ -411,6 +411,18 @@ json = ["jsonpath-ng (>=1.6,<2.0)"] lua = ["lupa (>=2.1,<3.0)"] probabilistic = ["pyprobables (>=0.6,<0.7)"] +[[package]] +name = "gprof2dot" +version = "2024.6.6" +description = "Generate a dot graph from the output of several profilers." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "gprof2dot-2024.6.6-py2.py3-none-any.whl", hash = "sha256:45b14ad7ce64e299c8f526881007b9eb2c6b75505d5613e96e66ee4d5ab33696"}, + {file = "gprof2dot-2024.6.6.tar.gz", hash = "sha256:fa1420c60025a9eb7734f65225b4da02a10fc6dd741b37fa129bc6b41951e5ab"}, +] + [[package]] name = "hiredis" version = "3.1.0" @@ -859,6 +871,23 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-profiling" +version = "1.8.1" +description = "Profiling plugin for py.test" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pytest-profiling-1.8.1.tar.gz", hash = "sha256:3f171fa69d5c82fa9aab76d66abd5f59da69135c37d6ae5bf7557f1b154cb08d"}, + {file = "pytest_profiling-1.8.1-py3-none-any.whl", hash = "sha256:3dd8713a96298b42d83de8f5951df3ada3e61b3e5d2a06956684175529e17aea"}, +] + +[package.dependencies] +gprof2dot = "*" +pytest = "*" +six = "*" + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -983,7 +1012,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1157,4 +1186,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "8fd070f9df0a6ee2888c90a29937d60325bd8d8bd7de186b1dcb3f27abc25470" +content-hash = "1f7ef100015fed58fcfae465b832e3d301011f65e9a88717b119cea69b17b08c" diff --git a/openc3/python/pyproject.toml b/openc3/python/pyproject.toml index 33f25f0f48..7e773b169b 100644 --- a/openc3/python/pyproject.toml +++ b/openc3/python/pyproject.toml @@ -28,6 +28,7 @@ coverage = "^7.6.10" fakeredis = "^2.26.2" pytest = "^8.3.4" ruff = "^0.9.4" +pytest-profiling = "^1.8.1" [build-system] requires = ["poetry-core"] diff --git a/openc3/python/test/api/test_cmd_api.py b/openc3/python/test/api/test_cmd_api.py index 865ee8a4bc..45145e5afb 100644 --- a/openc3/python/test/api/test_cmd_api.py +++ b/openc3/python/test/api/test_cmd_api.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -83,12 +83,13 @@ def xread_side_effect(*args, **kwargs): model.create() InterfaceStatusModel.set(self.interface.as_json(), scope="DEFAULT") - self.thread = InterfaceCmdHandlerThread(self.interface, None, scope="DEFAULT") - self.thread.start() - time.sleep(0.5) + self.icht = InterfaceCmdHandlerThread(self.interface, None, scope="DEFAULT") + self.thread = self.icht.start() + time.sleep(0.001) - def tearDown(self) -> None: - self.thread.stop() + def tearDown(self): + self.icht.graceful_kill(timeout=0.001) + self.thread.join() def test_cmd_complains_about_unknown_targets_commands_and_parameters(self): with self.assertRaisesRegex(RuntimeError, "does not exist"): @@ -329,8 +330,8 @@ def test_times_out_if_the_interface_does_not_process_the_command(self): with self.assertRaisesRegex(RuntimeError, "Must be numeric"): func("INST", "ABORT", timeout="YES") self.process = False - with self.assertRaisesRegex(RuntimeError, "Timeout of 30s waiting for cmd ack"): - func("INST", "ABORT") + with self.assertRaisesRegex(RuntimeError, "Timeout of 0.003s waiting for cmd ack"): + func("INST", "ABORT", timeout=0.003) def test_cmd_log_message_output(self): for name in [ @@ -525,7 +526,7 @@ def test_send_raw_raises_on_unknown_interfaces(self): def test_send_raw_sends_raw_data_to_an_interface(self): send_raw("inst_int", b"\x00\x01\x02\x03") - time.sleep(0.01) + time.sleep(0.001) self.assertEqual(MyInterface.interface_data, b"\x00\x01\x02\x03") def test_get_all_commands_complains_with_a_unknown_target(self): @@ -651,7 +652,7 @@ def test_get_cmd_hazardous_raises_with_the_wrong_number_of_arguments(self): def test_get_cmd_value_returns_command_values(self): now = time.time() cmd("INST COLLECT with TYPE NORMAL, DURATION 5") - time.sleep(0.01) + time.sleep(0.001) self.assertEqual(get_cmd_value("inst collect type"), "NORMAL") self.assertEqual(get_cmd_value("inst collect type", type="RAW"), 0) self.assertEqual(get_cmd_value("INST COLLECT DURATION"), 5.0) @@ -660,14 +661,14 @@ def test_get_cmd_value_returns_command_values(self): self.assertEqual(get_cmd_value("INST COLLECT RECEIVED_COUNT"), 1) cmd("INST COLLECT with TYPE NORMAL, DURATION 7") - time.sleep(0.01) + time.sleep(0.001) self.assertEqual(get_cmd_value("INST COLLECT RECEIVED_COUNT"), 2) self.assertEqual(get_cmd_value("INST COLLECT DURATION"), 7.0) def test_get_cmd_value_returns_command_values_old_style(self): now = time.time() cmd("INST COLLECT with TYPE NORMAL, DURATION 5") - time.sleep(0.01) + time.sleep(0.001) self.assertEqual(get_cmd_value("inst", "collect", "type"), "NORMAL") self.assertEqual(get_cmd_value("INST", "COLLECT", "DURATION"), 5.0) self.assertAlmostEqual(get_cmd_value("INST", "COLLECT", "RECEIVED_TIMESECONDS"), now, 1) @@ -675,14 +676,14 @@ def test_get_cmd_value_returns_command_values_old_style(self): self.assertEqual(get_cmd_value("INST", "COLLECT", "RECEIVED_COUNT"), 1) cmd("INST COLLECT with TYPE NORMAL, DURATION 7") - time.sleep(0.01) + time.sleep(0.001) self.assertEqual(get_cmd_value("INST", "COLLECT", "RECEIVED_COUNT"), 2) self.assertEqual(get_cmd_value("INST", "COLLECT", "DURATION"), 7.0) def test_get_cmd_time_returns_command_times(self): now = time.time() cmd("INST COLLECT with TYPE NORMAL, DURATION 5") - time.sleep(0.01) + time.sleep(0.001) result = get_cmd_time("inst", "collect") self.assertEqual(result[0], ("INST")) self.assertEqual(result[1], ("COLLECT")) @@ -700,7 +701,7 @@ def test_get_cmd_time_returns_command_times(self): now = time.time() cmd("INST ABORT") - time.sleep(0.01) + time.sleep(0.001) result = get_cmd_time("INST") self.assertEqual(result[0], ("INST")) self.assertEqual(result[1], ("ABORT")) # New latest is ABORT @@ -730,7 +731,7 @@ def test_get_cmd_cnt_returns_the_transmit_count(self): # Send unrelated commands to ensure specific command count cmd("INST ABORT") cmd_no_hazardous_check("INST CLEAR") - time.sleep(0.01) + time.sleep(0.001) count = get_cmd_cnt("INST", "COLLECT") self.assertEqual(count, start + 1) @@ -740,13 +741,13 @@ def test_get_cmd_cnt_returns_the_transmit_count(self): def test_get_cmd_cnts_returns_transmit_count_for_commands(self): cmd("INST ABORT") cmd("INST COLLECT with TYPE NORMAL, DURATION 5") - time.sleep(0.01) + time.sleep(0.001) cnts = get_cmd_cnts([["inst", "abort"], ["INST", "COLLECT"]]) self.assertEqual(cnts, ([1, 1])) cmd("INST ABORT") cmd("INST ABORT") cmd("INST COLLECT with TYPE NORMAL, DURATION 5") - time.sleep(0.01) + time.sleep(0.001) cnts = get_cmd_cnts([["INST", "ABORT"], ["INST", "COLLECT"]]) self.assertEqual(cnts, ([3, 2])) @@ -757,7 +758,6 @@ class BuildCommand(unittest.TestCase): def setUp(self, mock_let, mock_system): redis = mock_redis(self) setup_system() - mock_s3(self) orig_xread = redis.xread @@ -784,8 +784,8 @@ def tearDown(self): time.sleep(0.001) def test_complains_about_unknown_targets(self): - with self.assertRaisesRegex(RuntimeError, "Timeout of 5s waiting for cmd ack. Does target 'BLAH' exist?"): - build_cmd("BLAH COLLECT") + with self.assertRaisesRegex(RuntimeError, "Timeout of 0.001s waiting for cmd ack. Does target 'BLAH' exist?"): + build_cmd("BLAH COLLECT", timeout=0.001) def test_complains_about_unknown_commands(self): with self.assertRaisesRegex(RuntimeError, "does not exist"): diff --git a/openc3/python/test/api/test_interface_api.py b/openc3/python/test/api/test_interface_api.py index c573be6ad8..19656cf761 100644 --- a/openc3/python/test/api/test_interface_api.py +++ b/openc3/python/test/api/test_interface_api.py @@ -75,18 +75,19 @@ def build(): self.im = InterfaceMicroservice("DEFAULT__INTERFACE__INST_INT") self.im_thread = threading.Thread(target=self.im.run) self.im_thread.start() - time.sleep(0.02) # Allow the thread to run + time.sleep(0.002) # Allow the thread to run def tearDown(self): self.im.shutdown() - time.sleep(0.001) + self.im.graceful_kill(timeout=0.001) + self.im_thread.join() def test_returns_interface_hash(self): interface = get_interface("INST_INT") self.assertEqual(type(interface), dict) self.assertEqual(interface["name"], "INST_INT") # Verify it also includes the status - self.assertEqual(interface["state"], "CONNECTED") + self.assertEqual(interface["state"], "ATTEMPTING") self.assertEqual(interface["clients"], 0) def test_returns_all_interface_names(self): @@ -97,31 +98,31 @@ def test_returns_all_interface_names(self): self.assertEqual(get_interface_names(), ["INST_INT", "INT1", "INT2"]) def test_connects_the_interface(self): - self.assertEqual(get_interface("INST_INT")["state"], "CONNECTED") + self.assertIn(get_interface("INST_INT")["state"], ["ATTEMPTING", "CONNECTED"]) disconnect_interface("INST_INT") - time.sleep(0.1) + time.sleep(0.001) self.assertEqual(get_interface("INST_INT")["state"], "DISCONNECTED") connect_interface("INST_INT") - time.sleep(0.1) + time.sleep(0.001) self.assertIn(get_interface("INST_INT")["state"], ["ATTEMPTING", "CONNECTED"]) def test_should_start_and_stop_raw_logging_on_the_interface(self): self.assertIsNone(self.im.interface.stream_log_pair) start_raw_logging_interface("INST_INT") - time.sleep(0.1) + time.sleep(0.001) self.assertTrue(self.im.interface.stream_log_pair.read_log.logging_enabled) self.assertTrue(self.im.interface.stream_log_pair.write_log.logging_enabled) stop_raw_logging_interface("INST_INT") - time.sleep(0.1) + time.sleep(0.001) self.assertFalse(self.im.interface.stream_log_pair.read_log.logging_enabled) self.assertFalse(self.im.interface.stream_log_pair.write_log.logging_enabled) start_raw_logging_interface("ALL") - time.sleep(0.1) + time.sleep(0.001) self.assertTrue(self.im.interface.stream_log_pair.read_log.logging_enabled) self.assertTrue(self.im.interface.stream_log_pair.write_log.logging_enabled) stop_raw_logging_interface("ALL") - time.sleep(0.1) + time.sleep(0.001) self.assertFalse(self.im.interface.stream_log_pair.read_log.logging_enabled) self.assertFalse(self.im.interface.stream_log_pair.write_log.logging_enabled) # TODO: Need to explicitly shutdown stream_log_pair once started @@ -130,7 +131,7 @@ def test_should_start_and_stop_raw_logging_on_the_interface(self): def test_gets_interface_name_and_all_info(self): info = get_all_interface_info() self.assertEqual(info[0][0], "INST_INT") - self.assertEqual(info[0][1], "CONNECTED") + self.assertEqual(info[0][1], "ATTEMPTING") def test_successfully_maps_a_target_to_an_interface(self): TargetModel(name="INST", scope="DEFAULT").create() @@ -170,19 +171,19 @@ def test_successfully_maps_a_target_to_an_interface(self): def test_sends_an_interface_cmd(self): TestInterfaceApi.interface_cmd_data = {} interface_cmd("INST_INT", "cmd1") - time.sleep(0.1) + time.sleep(0.001) self.assertEqual(list(TestInterfaceApi.interface_cmd_data.keys()), ["cmd1"]) self.assertEqual(TestInterfaceApi.interface_cmd_data["cmd1"], ()) TestInterfaceApi.interface_cmd_data = {} interface_cmd("INST_INT", "cmd2", "param1") - time.sleep(0.1) + time.sleep(0.001) self.assertEqual(list(TestInterfaceApi.interface_cmd_data.keys()), ["cmd2"]) self.assertEqual(TestInterfaceApi.interface_cmd_data["cmd2"], ("param1",)) TestInterfaceApi.interface_cmd_data = {} interface_cmd("INST_INT", "cmd3", "param1", "param2") - time.sleep(0.1) + time.sleep(0.001) self.assertEqual(list(TestInterfaceApi.interface_cmd_data.keys()), ["cmd3"]) self.assertEqual( TestInterfaceApi.interface_cmd_data["cmd3"], @@ -195,19 +196,19 @@ def test_sends_an_interface_cmd(self): def test_sends_a_protocol_cmd(self): TestInterfaceApi.protocol_cmd_data = {} interface_protocol_cmd("INST_INT", "cmd1") - time.sleep(0.1) + time.sleep(0.001) self.assertEqual(list(TestInterfaceApi.protocol_cmd_data.keys()), ["cmd1"]) self.assertEqual(TestInterfaceApi.protocol_cmd_data["cmd1"], ()) TestInterfaceApi.protocol_cmd_data = {} interface_protocol_cmd("INST_INT", "cmd2", "param1") - time.sleep(0.1) + time.sleep(0.001) self.assertEqual(list(TestInterfaceApi.protocol_cmd_data.keys()), ["cmd2"]) self.assertEqual(TestInterfaceApi.protocol_cmd_data["cmd2"], ("param1",)) TestInterfaceApi.protocol_cmd_data = {} interface_protocol_cmd("INST_INT", "cmd3", "param1", "param2") - time.sleep(0.1) + time.sleep(0.001) self.assertEqual(list(TestInterfaceApi.protocol_cmd_data.keys()), ["cmd3"]) self.assertEqual( TestInterfaceApi.protocol_cmd_data["cmd3"], diff --git a/openc3/python/test/interfaces/protocols/test_cmd_response_protocol.py b/openc3/python/test/interfaces/protocols/test_cmd_response_protocol.py index 4a0e4f225b..f7e325826a 100644 --- a/openc3/python/test/interfaces/protocols/test_cmd_response_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_cmd_response_protocol.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -79,9 +79,8 @@ def my_write(): thread = threading.Thread(target=my_write) thread.start() - time.sleep(0.1) + time.sleep(0.001) self.interface.disconnect() - time.sleep(0.1) thread.join() def test_works_without_a_response(self): @@ -168,13 +167,13 @@ def test_processes_responses_with_no_id_fields(self, mock_system): # write blocks waiting for the response so spawn a thread def my_read(): - time.sleep(0.5) + time.sleep(0.001) self.read_result = self.interface.read() thread = threading.Thread(target=my_read) thread.start() self.interface.write(packet) - time.sleep(0.55) + time.sleep(0.003) self.assertEqual(TestCmdResponseProtocol.write_buffer, b"SOUR:VOLT 11, (@1)") self.assertEqual(self.read_result.read("VOLTAGE"), (10)) diff --git a/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py b/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py index 3de3f0a658..b2363ef809 100644 --- a/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -46,7 +46,7 @@ def disconnect(self): def read(self): if self.run: - time.sleep(0.01) + time.sleep(0.001) return TestIgnorePacketProtocol.buffer else: raise RuntimeError("Done") @@ -112,7 +112,7 @@ def my_read(): thread = threading.Thread(target=my_read) thread.start() - time.sleep(0.1) + time.sleep(0.001) self.interface.disconnect() self.interface.stream.disconnect() self.assertIsNone(TestIgnorePacketProtocol.packet) @@ -149,7 +149,7 @@ def my_read(): thread = threading.Thread(target=my_read) thread.start() - time.sleep(0.1) + time.sleep(0.001) self.interface.disconnect() self.interface.stream.disconnect() thread.join() @@ -179,7 +179,7 @@ def my_read2(): thread = threading.Thread(target=my_read2) thread.start() - time.sleep(0.1) + time.sleep(0.001) self.interface.disconnect() self.interface.stream.disconnect() thread.join() @@ -261,7 +261,7 @@ def my_read(): thread = threading.Thread(target=my_read) thread.start() - time.sleep(0.1) + time.sleep(0.001) self.interface.disconnect() self.interface.stream.disconnect() thread.join() diff --git a/openc3/python/test/interfaces/protocols/test_template_protocol.py b/openc3/python/test/interfaces/protocols/test_template_protocol.py index aef4d64dbf..323afe625f 100644 --- a/openc3/python/test/interfaces/protocols/test_template_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_template_protocol.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -64,45 +64,45 @@ def test_initializes_attributes(self): def test_supports_an_initial_read_delay(self): self.interface.stream = TestTemplateProtocol.TemplateStream() - self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD", 0, 2], "READ_WRITE") + self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD", 0, 0.02], "READ_WRITE") start = time.time() self.interface.connect() - self.assertGreaterEqual(self.interface.read_protocols[0].connect_complete_time, (start + 2.0)) - - # def test_unblocks_writes_waiting_for_responses(self): - # self.interface.stream = TestTemplateProtocol.TemplateStream() - # self.interface.add_protocol( - # TemplateProtocol, ["0xABCD", "0xABCD"], "READ_WRITE" - # ) - # packet = Packet("TGT", "CMD") - # packet.append_item("CMD_TEMPLATE", 1024, "STRING") - # packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT'" - # packet.append_item("RSP_TEMPLATE", 1024, "STRING") - # packet.get_item("RSP_TEMPLATE").default = "" - # packet.append_item("RSP_PACKET", 1024, "STRING") - # packet.get_item("RSP_PACKET").default = "READ_VOLTAGE" - # packet.restore_defaults() - # # write blocks waiting for the response so spawn a thread - # thread = threading.Thread(target=self.interface.write, args=[packet]) - # thread.start() - # time.sleep(0.1) - # self.interface.disconnect() - # time.sleep(0.1) - # thread.join() + self.assertGreaterEqual(self.interface.read_protocols[0].connect_complete_time, (start + 0.02)) + self.assertLessEqual(self.interface.read_protocols[0].connect_complete_time, (start + 0.05)) + + def test_unblocks_writes_waiting_for_responses(self): + self.interface.stream = TestTemplateProtocol.TemplateStream() + self.interface.add_protocol( + TemplateProtocol, ["0xABCD", "0xABCD"], "READ_WRITE" + ) + packet = Packet("TGT", "CMD") + packet.append_item("CMD_TEMPLATE", 1024, "STRING") + packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT'" + packet.append_item("RSP_TEMPLATE", 1024, "STRING") + packet.get_item("RSP_TEMPLATE").default = "" + packet.append_item("RSP_PACKET", 1024, "STRING") + packet.get_item("RSP_PACKET").default = "READ_VOLTAGE" + packet.restore_defaults() + # write blocks waiting for the response so spawn a thread + thread = threading.Thread(target=self.interface.write, args=[packet]) + thread.start() + time.sleep(0.001) + self.interface.disconnect() + thread.join() def test_ignores_all_data_during_the_connect_period(self): self.interface.stream = TestTemplateProtocol.TemplateStream() - self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD", 0, 1.5], "READ_WRITE") + self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD", 0, 0.01], "READ_WRITE") start = time.time() self.interface.connect() TestTemplateProtocol.read_buffer = b"\x31\x30\xAB\xCD" data = self.interface.read() - self.assertAlmostEqual(time.time() - start, 1.5, places=1) + self.assertAlmostEqual(time.time() - start, 0.01, places=1) self.assertEqual(data.buffer, b"\x31\x30") def test_waits_before_writing_during_the_initial_delay_period(self): self.interface.stream = TestTemplateProtocol.TemplateStream() - self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD", 0, 1.5], "READ_WRITE") + self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD", 0, 0.02], "READ_WRITE") packet = Packet("TGT", "CMD") packet.append_item("VOLTAGE", 16, "UINT") packet.get_item("VOLTAGE").default = 1 @@ -114,7 +114,7 @@ def test_waits_before_writing_during_the_initial_delay_period(self): self.interface.connect() write = time.time() self.interface.write(packet) - self.assertAlmostEqual(time.time() - write, 1.5, places=1) + self.assertAlmostEqual(time.time() - write, 0.02, places=1) def test_works_without_a_response(self): self.interface.stream = TestTemplateProtocol.TemplateStream() @@ -134,7 +134,7 @@ def test_logs_an_error_if_it_doesnt_receive_a_response(self): self.interface.stream = TestTemplateProtocol.TemplateStream() self.interface.add_protocol( TemplateProtocol, - ["0xA", "0xA", 0, None, 1, True, 0, None, False, 1.5], + ["0xA", "0xA", 0, None, 1, True, 0, None, False, 0.03], "READ_WRITE", ) self.interface.target_names = ["TGT"] @@ -154,13 +154,13 @@ def test_logs_an_error_if_it_doesnt_receive_a_response(self): "Timeout waiting for response", stdout.getvalue(), ) - self.assertAlmostEqual(time.time() - start, 1.5, places=1) + self.assertAlmostEqual(time.time() - start, 0.03, places=1) def test_disconnects_if_it_doesnt_receive_a_response(self): self.interface.stream = TestTemplateProtocol.TemplateStream() self.interface.add_protocol( TemplateProtocol, - ["0xA", "0xA", 0, None, 1, True, 0, None, False, 1.5, 0.02, True], + ["0xA", "0xA", 0, None, 1, True, 0, None, False, 0.04, 0.02, True], "READ_WRITE", ) self.interface.target_names = ["TGT"] @@ -176,7 +176,7 @@ def test_disconnects_if_it_doesnt_receive_a_response(self): start = time.time() with self.assertRaisesRegex(RuntimeError, "Timeout waiting for response"): self.interface.write(packet) - self.assertAlmostEqual(time.time() - start, 1.5, places=1) + self.assertAlmostEqual(time.time() - start, 0.04, places=1) def test_doesnt_expect_responses_for_empty_response_fields(self): self.interface.stream = TestTemplateProtocol.TemplateStream() @@ -233,13 +233,13 @@ def test_processes_responses_with_no_id_fields(self, mock_system): TestTemplateProtocol.read_buffer = b"\x31\x30\xAB\xCD" # ASCII 31, 30 is '10' def do_read(self): - time.sleep(0.5) + time.sleep(0.001) self.read_result = self.interface.read() thread = threading.Thread(target=do_read, args=[self]) thread.start() self.interface.write(packet) - time.sleep(0.55) + time.sleep(0.003) self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.1)\xAB\xCD") self.assertEqual(self.read_result.read("VOLTAGE"), (10)) @@ -281,13 +281,13 @@ def test_sets_the_response_id_to_the_defined_id_value(self, mock_system): TestTemplateProtocol.read_buffer = b"\x31\x30\xAB\xCD" # ASCII 31, 30 is '10' def do_read(self): - time.sleep(0.5) + time.sleep(0.001) self.read_result = self.interface.read() thread = threading.Thread(target=do_read, args=[self]) thread.start() self.interface.write(packet) - time.sleep(0.55) + time.sleep(0.003) self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.1)\xAB\xCD") self.assertEqual(self.read_result.read("PKT_ID"), (1)) # Result ID set to the defined value) self.assertEqual(self.read_result.read("VOLTAGE"), (10)) @@ -336,14 +336,14 @@ def test_handles_multiple_response_ids(self, mock_system): TestTemplateProtocol.read_buffer = b"\x31\x30\xAB\xCD" # ASCII 31, 30 is '10' def do_read(self): - time.sleep(0.5) + time.sleep(0.001) self.read_result = self.interface.read() thread = threading.Thread(target=do_read, args=[self]) thread.start() self.interface.write(packet) - time.sleep(0.55) + time.sleep(0.003) self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.1)\xAB\xCD") self.assertEqual(self.read_result.read("APID"), (10)) # ID item set to the defined value) self.assertEqual(self.read_result.read("PKTID"), (20)) # ID item set to the defined value) @@ -382,14 +382,14 @@ def test_handles_templates_with_more_values_than_the_response(self, mock_system) TestTemplateProtocol.read_buffer = b"\x31\x30\xAB\xCD" # ASCII 31, 30 is '10' def do_read(self): - time.sleep(0.5) + time.sleep(0.001) self.read_result = self.interface.read() thread = threading.Thread(target=do_read, args=[self]) thread.start() for stdout in capture_io(): self.interface.write(packet) - time.sleep(0.55) + time.sleep(0.003) self.assertIn( "Unexpected response:", stdout.getvalue(), @@ -431,7 +431,7 @@ def test_handles_responses_with_more_values_than_the_template(self, mock_system) TestTemplateProtocol.read_buffer = b"\x31\x30\x3B\x31\x31\xAB\xCD" # ASCII is '10;11' def do_read(self): - time.sleep(0.5) + time.sleep(0.001) self.read_result = self.interface.read() thread = threading.Thread(target=do_read, args=[self]) @@ -439,7 +439,7 @@ def do_read(self): for stdout in capture_io(): self.interface.write(packet) - time.sleep(0.55) + time.sleep(0.003) self.assertIn( "Could not write value 10;11", stdout.getvalue(), @@ -478,7 +478,7 @@ def test_ignores_response_lines(self, mock_system): TestTemplateProtocol.read_buffer = b"\x31\x30\x0A\x31\x32\x0A" # ASCII: 30:'0', 31:'1', etc def do_read(self): - time.sleep(0.5) + time.sleep(0.001) self.read_result = self.interface.read() thread = threading.Thread(target=do_read, args=[self]) @@ -514,7 +514,7 @@ def test_allows_multiple_response_lines(self, mock_system): TestTemplateProtocol.read_buffer = b"\x4F\x70\x65\x0A\x6E\x43\x33\x0A" # ASCII def do_read(self): - time.sleep(0.5) + time.sleep(0.001) self.read_result = self.interface.read() thread = threading.Thread(target=do_read, args=[self]) diff --git a/openc3/python/test/interfaces/test_interface.py b/openc3/python/test/interfaces/test_interface.py index c52f478d18..8a50f84d34 100644 --- a/openc3/python/test/interfaces/test_interface.py +++ b/openc3/python/test/interfaces/test_interface.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -17,9 +17,11 @@ import time import unittest import threading +from unittest.mock import patch from openc3.interfaces.interface import Interface from openc3.interfaces.protocols.protocol import Protocol from openc3.packets.packet import Packet +from test.test_helper import BucketMock gvPacket = None gvData = None @@ -122,12 +124,13 @@ def test_write_raw_allowed_is_true(self): class ReadInterface(unittest.TestCase): def setUp(self): - pass - # TODO: This doesn't seem to do anything ... trying to avoid "Error saving log file to bucket" messages - # mock = Mock(spec=BucketUtilities) - # patcher = patch("openc3.utilities.bucket_utilities", return_value=mock) - # patcher.start() - # self.addCleanup(patcher.stop) + self.mock_s3 = BucketMock.getClient() + self.mock_s3.clear() + self.patcher = patch("openc3.utilities.bucket_utilities.Bucket", BucketMock) + self.patcher.start() + + def tearDown(self): + self.patcher.stop() def test_connection_string(self): class MyInterface(Interface): @@ -161,9 +164,9 @@ def read_interface(self): self.assertEqual(interface.bytes_read, 4) filename = interface.stream_log_pair.read_log.filename interface.stop_raw_logging() - file = open(filename, "rb") - self.assertEqual(file.read(), b"\x01\x02\x03\x04") - file.close() + filename = self.mock_s3.files()[0] + self.assertIn("myinterface_stream_read.bin.gz", filename) + self.assertEqual(self.mock_s3.data(filename), b"\x01\x02\x03\x04") interface.stream_log_pair.shutdown() def test_aborts_and_doesnt_log_if_no_data_is_returned_from_read_interface(self): @@ -248,12 +251,11 @@ def read_interface(self): self.assertEqual(packet.buffer, b"\x01\x02\x03\x04\x05\x06") self.assertEqual(interface.read_count, 1) self.assertEqual(interface.bytes_read, 4) - filename = interface.stream_log_pair.read_log.filename interface.stop_raw_logging() # Raw logging is still the original read_data return - file = open(filename, "rb") - self.assertEqual(file.read(), b"\x01\x02\x03\x04") - file.close() + filename = self.mock_s3.files()[0] + self.assertIn("myinterface_stream_read.bin.gz", filename) + self.assertEqual(self.mock_s3.data(filename), b"\x01\x02\x03\x04") interface.stream_log_pair.shutdown() def test_aborts_if_protocol_read_data_returns_disconnect(self): @@ -273,11 +275,10 @@ def read_interface(self): self.assertIsNone(packet) self.assertEqual(interface.read_count, 0) self.assertEqual(interface.bytes_read, 4) - filename = interface.stream_log_pair.read_log.filename interface.stop_raw_logging() - file = open(filename, "rb") - self.assertEqual(file.read(), b"\x01\x02\x03\x04") - file.close() + filename = self.mock_s3.files()[0] + self.assertIn("myinterface_stream_read.bin.gz", filename) + self.assertEqual(self.mock_s3.data(filename), b"\x01\x02\x03\x04") interface.stream_log_pair.shutdown() def test_gets_more_data_if_a_protocol_read_data_returns_stop(self): @@ -297,11 +298,10 @@ def read_interface(self): self.assertEqual(packet.buffer, b"\x01\x02\x03\x04") self.assertEqual(interface.read_count, 1) self.assertEqual(interface.bytes_read, 8) - filename = interface.stream_log_pair.read_log.filename interface.stop_raw_logging() - file = open(filename, "rb") - self.assertEqual(file.read(), b"\x01\x02\x03\x04\x01\x02\x03\x04") - file.close() + filename = self.mock_s3.files()[0] + self.assertIn("myinterface_stream_read.bin.gz", filename) + self.assertEqual(self.mock_s3.data(filename), b"\x01\x02\x03\x04\x01\x02\x03\x04") interface.stream_log_pair.shutdown() def test_allows_protocol_read_packet_to_manipulate_packet(self): @@ -377,6 +377,13 @@ def read_interface(self): class WriteInterface(unittest.TestCase): def setUp(self): self.packet = Packet("TGT", "PKT", "BIG_ENDIAN", "Packet", b"\x01\x02\x03\x04") + self.mock_s3 = BucketMock.getClient() + self.mock_s3.clear() + self.patcher = patch("openc3.utilities.bucket_utilities.Bucket", BucketMock) + self.patcher.start() + + def tearDown(self): + self.patcher.stop() def test_raises_an_error_if_not_connected(self): class MyInterface(Interface): @@ -446,9 +453,9 @@ def write_interface(self, data, extra=None): self.assertEqual(interface.bytes_written, 6) filename = interface.stream_log_pair.write_log.filename interface.stop_raw_logging() - file = open(filename, "rb") - self.assertEqual(file.read(), b"\x01\x02\x03\x04\x05\x06") - file.close() + filename = self.mock_s3.files()[0] + self.assertIn("myinterface_stream_write.bin.gz", filename) + self.assertEqual(self.mock_s3.data(filename), b"\x01\x02\x03\x04\x05\x06") interface.stream_log_pair.shutdown() def test_aborts_if_write_packet_returns_disconnect(self): @@ -495,11 +502,10 @@ def write_interface(self, data, extra=None): interface.write(self.packet) self.assertEqual(interface.write_count, 1) self.assertEqual(interface.bytes_written, 6) - filename = interface.stream_log_pair.write_log.filename interface.stop_raw_logging() - file = open(filename, "rb") - self.assertEqual(file.read(), b"\x01\x02\x03\x04\x08\x07") - file.close() + filename = self.mock_s3.files()[0] + self.assertIn("myinterface_stream_write.bin.gz", filename) + self.assertEqual(self.mock_s3.data(filename), b"\x01\x02\x03\x04\x08\x07") interface.stream_log_pair.shutdown() def test_aborts_if_write_data_returns_disconnect(self): diff --git a/openc3/python/test/interfaces/test_udp_interface.py b/openc3/python/test/interfaces/test_udp_interface.py index 52640a701a..81f0a112af 100644 --- a/openc3/python/test/interfaces/test_udp_interface.py +++ b/openc3/python/test/interfaces/test_udp_interface.py @@ -1,4 +1,4 @@ -# Copyright 2024 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -141,7 +141,7 @@ def test_stops_the_read_thread_if_there_is_an_ioerror(self, mock_socket): i.connect() thread = threading.Thread(target=i.read) thread.start() - time.sleep(0.1) + time.sleep(0.001) self.assertFalse(thread.is_alive()) def test_counts_the_packets_received(self): @@ -195,7 +195,6 @@ def test_logs_the_raw_data(self, move_log_file): i.disconnect() close_socket(write) i.stream_log_pair.shutdown() - time.sleep(0.01) def test_write_complains_if_write_dest_not_given(self): i = UdpInterface("localhost", "None", "8889") @@ -256,4 +255,3 @@ def test_write_logs_the_raw_data(self, move_log_file): i.disconnect() close_socket(read) i.stream_log_pair.shutdown() - time.sleep(0.01) diff --git a/openc3/python/test/logs/test_stream_log.py b/openc3/python/test/logs/test_stream_log.py index 2607c569fa..e96a2e0a15 100644 --- a/openc3/python/test/logs/test_stream_log.py +++ b/openc3/python/test/logs/test_stream_log.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -14,78 +14,77 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. -import zlib import time import unittest -from unittest.mock import * -from test.test_helper import * +from unittest.mock import patch +from test.test_helper import mock_redis, capture_io, BucketMock from openc3.logs.stream_log import StreamLog - class TestStreamLog(unittest.TestCase): def setUp(self): mock_redis(self) - self.mock = mock_s3(self) + self.mock_s3 = BucketMock.getClient() + self.mock_s3.clear() + self.patcher = patch("openc3.utilities.bucket_utilities.Bucket", BucketMock) + self.patcher.start() - def tearDown(self) -> None: + def tearDown(self): if hasattr(self, "stream_log"): self.stream_log.shutdown() - time.sleep(0.1) + self.patcher.stop() def test_complains_with_not_enough_arguments(self): with self.assertRaisesRegex(TypeError, "log_type"): - StreamLog("MYINT") + StreamLog("SLINT") def test_complains_with_an_unknown_log_type(self): with self.assertRaisesRegex(RuntimeError, "log_type must be 'READ' or 'WRITE'"): - StreamLog("MYINT", "BOTH") + StreamLog("SLINT", "BOTH") def test_creates_a_raw_write_log(self): - self.stream_log = StreamLog("MYINT", "WRITE") + self.stream_log = StreamLog("SLINT", "WRITE") self.stream_log.write(b"\x00\x01\x02\x03") self.stream_log.stop() - time.sleep(0.1) - key = list(self.mock.files.keys())[0] - self.assertIn("myint_stream_write.bin.gz", key) - bin = zlib.decompress(self.mock.files[key]) - self.assertEqual(bin, b"\x00\x01\x02\x03") + time.sleep(0.001) + key = self.mock_s3.files()[0] + self.assertIn("slint_stream_write.bin.gz", key) + self.assertEqual(self.mock_s3.data(key), b"\x00\x01\x02\x03") def test_creates_a_raw_read_log(self): - self.stream_log = StreamLog("MYINT", "READ") + self.stream_log = StreamLog("SLINT", "READ") self.stream_log.write(b"\x01\x02\x03\x04") self.stream_log.stop() - time.sleep(0.1) - key = list(self.mock.files.keys())[0] - self.assertIn("myint_stream_read.bin.gz", key) - bin = zlib.decompress(self.mock.files[key]) - self.assertEqual(bin, b"\x01\x02\x03\x04") + time.sleep(0.001) + key = self.mock_s3.files()[0] + self.assertIn("slint_stream_read.bin.gz", key) + self.assertEqual(self.mock_s3.data(key), b"\x01\x02\x03\x04") def test_does_not_write_data_if_logging_is_disabled(self): - self.stream_log = StreamLog("MYINT", "WRITE") + self.stream_log = StreamLog("SLINT", "WRITE") self.stream_log.stop() - time.sleep(0.1) + time.sleep(0.001) self.stream_log.write(b"\x00\x01\x02\x03") self.assertEqual(self.stream_log.file_size, 0) - self.assertEqual(len(self.mock.files), 0) + self.assertEqual(len(self.mock_s3.files()), 0) def test_cycles_the_log_when_it_a_size(self): - self.stream_log = StreamLog("MYINT", "WRITE", 300, 2000) + self.stream_log = StreamLog("SLINT", "WRITE", 300, 2000) self.stream_log.write(b"\x00\x01\x02\x03" * 250) # size 1000 self.stream_log.write(b"\x00\x01\x02\x03" * 250) # size 2000 - self.assertEqual(len(self.mock.files.keys()), 0) # hasn't cycled yet - time.sleep(0.1) + self.assertEqual(len(self.mock_s3.files()), 0) # hasn't cycled yet + time.sleep(0.001) self.stream_log.write(b"\x00") # size 200001 - time.sleep(0.1) - self.assertEqual(len(self.mock.files.keys()), 1) + time.sleep(0.001) + self.assertEqual(len(self.mock_s3.files()), 1) self.stream_log.stop() - time.sleep(0.1) - self.assertEqual(len(self.mock.files.keys()), 2) + time.sleep(0.001) + self.assertEqual(len(self.mock_s3.files()), 2) def test_handles_errors_creating_the_log_file(self): with patch("builtins.open") as mock_file: mock_file.side_effect = IOError() for stdout in capture_io(): - self.stream_log = StreamLog("MYINT", "WRITE") + self.stream_log = StreamLog("SLINT", "WRITE") self.stream_log.write(b"\x00\x01\x02\x03") self.stream_log.stop() self.assertIn( @@ -97,26 +96,26 @@ def test_handles_errors_moving_the_log_file(self): with patch("zlib.compressobj") as zlib: zlib.side_effect = RuntimeError("PROBLEM!") for stdout in capture_io(): - self.stream_log = StreamLog("MYINT", "WRITE") + self.stream_log = StreamLog("SLINT", "WRITE") self.stream_log.write(b"\x00\x01\x02\x03") self.stream_log.stop() - time.sleep(0.1) + time.sleep(0.001) self.assertIn( "Error saving log file to bucket", stdout.getvalue(), ) def test_enables_and_disable_logging(self): - self.stream_log = StreamLog("MYINT", "WRITE") + self.stream_log = StreamLog("SLINT", "WRITE") self.assertTrue(self.stream_log.logging_enabled) self.stream_log.write(b"\x00\x01\x02\x03") self.stream_log.stop() - time.sleep(0.1) + time.sleep(0.001) self.assertFalse(self.stream_log.logging_enabled) - self.assertEqual(len(self.mock.files), 1) + self.assertEqual(len(self.mock_s3.files()), 1) self.stream_log.start() self.assertTrue(self.stream_log.logging_enabled) self.stream_log.write(b"\x00\x01\x02\x03") self.stream_log.stop() - time.sleep(0.1) - self.assertEqual(len(self.mock.files), 2) + time.sleep(0.001) + self.assertEqual(len(self.mock_s3.files()), 2) diff --git a/openc3/python/test/test_helper.py b/openc3/python/test/test_helper.py index 41d7df0355..e971b645fd 100644 --- a/openc3/python/test/test_helper.py +++ b/openc3/python/test/test_helper.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2025 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -132,32 +132,31 @@ def mock_redis(self): return redis -class MockS3: +import zlib +class BucketMock: + instance = None def __init__(self): - self.clear() - self.files = {} + self.objs = {} - def client(self, *args, **kwags): - return self + @classmethod + def getClient(cls): + if cls.instance: + return cls.instance + cls.instance = cls() + return cls.instance def put_object(self, *args, **kwargs): - self.files[kwargs["Key"]] = kwargs["Body"].read() + self.objs[kwargs["key"]] = kwargs["body"].read() def clear(self): - self.files = {} + self.objs = {} + def files(self): + return list(self.objs.keys()) -# Create a MockS3 to make this a singleton -mocks3 = MockS3() - - -def mock_s3(self): - """Clear it out every time it is used""" - mocks3.clear() - patcher = patch("boto3.session.Session", return_value=mocks3) - patcher.start() - self.addCleanup(patcher.stop) - return mocks3 + def data(self, key): + data = self.objs[key] + return zlib.decompress(data) def capture_io():