kopia lustrzana https://github.com/Yakifo/amqtt
using 'n' messages to force amqtt_sub to exit, instead of SIGINT which seems to cause the process to force quit instead of causing a keyboard interupt
rodzic
dd666811a5
commit
9d3d9bd7e2
|
@ -70,7 +70,7 @@ jobs:
|
||||||
run: uv sync --locked --dev
|
run: uv sync --locked --dev
|
||||||
|
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
run: uv run --frozen pytest tests --cov=./ --cov-report=xml --junitxml=pytest-report.xml
|
run: uv run --frozen pytest tests/ --cov=./ --cov-report=xml --junitxml=pytest-report.xml
|
||||||
|
|
||||||
# https://github.com/actions/upload-artifact
|
# https://github.com/actions/upload-artifact
|
||||||
- name: Upload test report
|
- name: Upload test report
|
||||||
|
|
|
@ -16,8 +16,6 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
- --fix
|
|
||||||
- --unsafe-fixes
|
|
||||||
- --line-length=130
|
- --line-length=130
|
||||||
- --exit-non-zero-on-fix
|
- --exit-non-zero-on-fix
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ class MQTTClient:
|
||||||
msg = "Future or Task was cancelled"
|
msg = "Future or Task was cancelled"
|
||||||
raise ConnectError(msg) from e
|
raise ConnectError(msg) from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Connection failed: {e}")
|
self.logger.warning(f"Connection failed: {e!r}")
|
||||||
if not self.config.get("auto_reconnect", False):
|
if not self.config.get("auto_reconnect", False):
|
||||||
raise
|
raise
|
||||||
return await self.reconnect()
|
return await self.reconnect()
|
||||||
|
@ -230,9 +230,11 @@ class MQTTClient:
|
||||||
msg = "Future or Task was cancelled"
|
msg = "Future or Task was cancelled"
|
||||||
raise ConnectError(msg) from e
|
raise ConnectError(msg) from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Reconnection attempt failed: {e}", exc_info=True)
|
self.logger.warning(f"Reconnection attempt failed: {e!r}")
|
||||||
|
self.logger.debug("", exc_info=True)
|
||||||
if reconnect_retries < nb_attempt: # reconnect_retries >= 0 and
|
if reconnect_retries < nb_attempt: # reconnect_retries >= 0 and
|
||||||
self.logger.exception("Maximum connection attempts reached. Reconnection aborted.")
|
self.logger.exception("Maximum connection attempts reached. Reconnection aborted.")
|
||||||
|
self.logger.debug("", exc_info=True)
|
||||||
msg = "Too many failed attempts"
|
msg = "Too many failed attempts"
|
||||||
raise ConnectError(msg) from e
|
raise ConnectError(msg) from e
|
||||||
delay = min(reconnect_max_interval, 2**nb_attempt)
|
delay = min(reconnect_max_interval, 2**nb_attempt)
|
||||||
|
@ -475,6 +477,7 @@ class MQTTClient:
|
||||||
self.session.remote_port,
|
self.session.remote_port,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
reader = StreamReaderAdapter(conn_reader)
|
reader = StreamReaderAdapter(conn_reader)
|
||||||
writer = StreamWriterAdapter(conn_writer)
|
writer = StreamWriterAdapter(conn_writer)
|
||||||
elif scheme in ("ws", "wss") and self.session.broker_uri:
|
elif scheme in ("ws", "wss") and self.session.broker_uri:
|
||||||
|
@ -511,7 +514,7 @@ class MQTTClient:
|
||||||
self.logger.debug(f"Connected to {self.session.remote_address}:{self.session.remote_port}")
|
self.logger.debug(f"Connected to {self.session.remote_address}:{self.session.remote_port}")
|
||||||
|
|
||||||
except (InvalidURI, InvalidHandshake, ProtocolHandlerError, ConnectionError, OSError) as e:
|
except (InvalidURI, InvalidHandshake, ProtocolHandlerError, ConnectionError, OSError) as e:
|
||||||
self.logger.warning(f"Connection failed : {self.session.broker_uri} : {e}")
|
self.logger.debug(f"Connection failed : {self.session.broker_uri} [{e!r}]")
|
||||||
self.session.transitions.disconnect()
|
self.session.transitions.disconnect()
|
||||||
raise ConnectError(e) from e
|
raise ConnectError(e) from e
|
||||||
return return_code
|
return return_code
|
||||||
|
|
|
@ -87,7 +87,8 @@ class PluginManager:
|
||||||
obj = plugin(plugin_context)
|
obj = plugin(plugin_context)
|
||||||
return Plugin(ep.name, ep, obj)
|
return Plugin(ep.name, ep, obj)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
self.logger.warning(f"Plugin {ep!r} import failed", exc_info=True)
|
self.logger.warning(f"Plugin {ep!r} import failed")
|
||||||
|
self.logger.debug("", exc_info=True)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ async def do_pub(
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
logger.info(f"{client.client_id} Disconnected from broker")
|
logger.info(f"{client.client_id} Disconnected from broker")
|
||||||
except ConnectError as ce:
|
except ConnectError as ce:
|
||||||
logger.fatal(f"Connection to '{url}' failed: {ce!r}")
|
logger.fatal(f"Connection to '{client.session.broker_uri if client.session else url}' failed")
|
||||||
raise ConnectError from ce
|
raise ConnectError from ce
|
||||||
except asyncio.CancelledError as ce:
|
except asyncio.CancelledError as ce:
|
||||||
logger.fatal("Publish canceled due to previous error")
|
logger.fatal("Publish canceled due to previous error")
|
||||||
|
|
|
@ -38,7 +38,7 @@ def broker_config():
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def broker_config_file(broker_config, tmp_path):
|
def broker_config_file(broker_config, tmp_path):
|
||||||
config_path = tmp_path / "config.yaml"
|
config_path = tmp_path / "broker.yaml"
|
||||||
with config_path.open("w") as f:
|
with config_path.open("w") as f:
|
||||||
yaml.dump(broker_config, f)
|
yaml.dump(broker_config, f)
|
||||||
return str(config_path)
|
return str(config_path)
|
||||||
|
@ -53,13 +53,12 @@ async def broker(broker_config_file):
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
# Give broker time to start
|
# Give broker time to start
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(4)
|
||||||
yield proc
|
yield proc
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
proc.wait()
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.timeout(12)
|
|
||||||
def test_cli_help_messages():
|
def test_cli_help_messages():
|
||||||
"""Test that help messages are displayed correctly."""
|
"""Test that help messages are displayed correctly."""
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
|
@ -79,7 +78,7 @@ def test_cli_help_messages():
|
||||||
output = subprocess.check_output([amqtt_pub_path, "--help"], env=env, text=True)
|
output = subprocess.check_output([amqtt_pub_path, "--help"], env=env, text=True)
|
||||||
assert "Usage: amqtt_pub" in output
|
assert "Usage: amqtt_pub" in output
|
||||||
|
|
||||||
@pytest.mark.timeout(12)
|
|
||||||
def test_broker_version():
|
def test_broker_version():
|
||||||
"""Test broker version command."""
|
"""Test broker version command."""
|
||||||
output = subprocess.check_output(["amqtt", "--version"])
|
output = subprocess.check_output(["amqtt", "--version"])
|
||||||
|
@ -87,7 +86,6 @@ def test_broker_version():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.timeout(12)
|
|
||||||
async def test_broker_start_stop(broker_config_file):
|
async def test_broker_start_stop(broker_config_file):
|
||||||
"""Test broker start and stop with config file."""
|
"""Test broker start and stop with config file."""
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
|
@ -109,7 +107,6 @@ async def test_broker_start_stop(broker_config_file):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.timeout(12)
|
|
||||||
async def test_publish_subscribe(broker):
|
async def test_publish_subscribe(broker):
|
||||||
"""Test pub/sub CLI tools with running broker."""
|
"""Test pub/sub CLI tools with running broker."""
|
||||||
|
|
||||||
|
@ -162,7 +159,6 @@ async def test_publish_subscribe(broker):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.timeout(12)
|
|
||||||
async def test_pub_sub_retain(broker):
|
async def test_pub_sub_retain(broker):
|
||||||
"""Test various pub/sub will retain options."""
|
"""Test various pub/sub will retain options."""
|
||||||
# Test publishing with retain flag
|
# Test publishing with retain flag
|
||||||
|
@ -195,32 +191,38 @@ async def test_pub_sub_retain(broker):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.timeout(12)
|
async def test_pub_errors(client_config_file):
|
||||||
async def test_pub_errors():
|
|
||||||
"""Test error handling in pub/sub tools."""
|
"""Test error handling in pub/sub tools."""
|
||||||
# Test connection to non-existent broker
|
# Test connection to non-existent broker
|
||||||
pub_proc = subprocess.run(
|
cmd = [
|
||||||
[
|
|
||||||
"amqtt_pub",
|
"amqtt_pub",
|
||||||
"--url", "mqtt://127.0.0.1:9999", # Wrong port
|
"--url", "mqtt://127.0.0.1:9999", # Wrong port
|
||||||
"-t", "test/topic",
|
"-t", "test/topic",
|
||||||
"-m", "test",
|
"-m", "test",
|
||||||
],
|
"-c", client_config_file,
|
||||||
capture_output=True,
|
]
|
||||||
|
|
||||||
|
proc = await asyncio.create_subprocess_shell(
|
||||||
|
" ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
)
|
)
|
||||||
assert pub_proc.returncode != 0, f"publisher error code: {pub_proc.returncode}"
|
stdout, stderr = await proc.communicate()
|
||||||
assert "Connection failed" in str(pub_proc.stderr)
|
logger.debug(f"Command: {cmd}")
|
||||||
|
logger.debug(f"Stdout: {stdout.decode()}")
|
||||||
|
logger.debug(f"Stderr: {stderr.decode()}")
|
||||||
|
|
||||||
|
assert proc.returncode != 0, f"publisher error code: {proc.returncode}"
|
||||||
|
assert "Connection failed" in str(stderr)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.timeout(12)
|
async def test_sub_errors(client_config_file):
|
||||||
async def test_sub_errors():
|
|
||||||
# Test invalid URL format
|
# Test invalid URL format
|
||||||
sub_proc = subprocess.run(
|
sub_proc = subprocess.run(
|
||||||
[
|
[
|
||||||
"amqtt_sub",
|
"amqtt_sub",
|
||||||
"--url", "invalid://url",
|
"--url", "invalid://url",
|
||||||
"-t", "test/topic",
|
"-t", "test/topic",
|
||||||
|
"-c", client_config_file
|
||||||
],
|
],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
)
|
)
|
||||||
|
@ -234,18 +236,7 @@ def client_config():
|
||||||
"ping_delay": 1,
|
"ping_delay": 1,
|
||||||
"default_qos": 0,
|
"default_qos": 0,
|
||||||
"default_retain": False,
|
"default_retain": False,
|
||||||
"auto_reconnect": True,
|
"auto_reconnect": False,
|
||||||
"reconnect_max_interval": 5,
|
|
||||||
"reconnect_retries": 10,
|
|
||||||
"topics": {
|
|
||||||
"test": {
|
|
||||||
"qos": 0
|
|
||||||
},
|
|
||||||
"some_topic": {
|
|
||||||
"qos": 2,
|
|
||||||
"retain": True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"will": {
|
"will": {
|
||||||
"topic": "test/will/topic",
|
"topic": "test/will/topic",
|
||||||
"message": "client ABC has disconnected",
|
"message": "client ABC has disconnected",
|
||||||
|
@ -260,49 +251,56 @@ def client_config():
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client_config_file(client_config, tmp_path):
|
def client_config_file(client_config, tmp_path):
|
||||||
config_path = tmp_path / "config.yaml"
|
config_path = tmp_path / "client.yaml"
|
||||||
with config_path.open("w") as f:
|
with config_path.open("w") as f:
|
||||||
yaml.dump(client_config, f)
|
yaml.dump(client_config, f)
|
||||||
return str(config_path)
|
return str(config_path)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.timeout(12)
|
@pytest.mark.asyncio
|
||||||
def test_pub_client_config(broker, client_config_file):
|
async def test_pub_client_config(broker, client_config_file):
|
||||||
pub_proc = subprocess.run(
|
await asyncio.sleep(1)
|
||||||
[
|
cmd = [
|
||||||
"amqtt_pub",
|
"amqtt_pub",
|
||||||
"-t", "test/topic",
|
"-t", "test/topic",
|
||||||
"-m", "test",
|
"-m", "test",
|
||||||
"-c", client_config_file
|
"-c", client_config_file
|
||||||
],
|
]
|
||||||
capture_output=True,
|
proc = await asyncio.create_subprocess_shell(
|
||||||
|
" ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
)
|
)
|
||||||
assert pub_proc.returncode == 0, f"publisher error code: {pub_proc.returncode}"
|
stdout, stderr = await proc.communicate()
|
||||||
|
logger.debug(f"Command: {cmd}")
|
||||||
|
logger.debug(f"Stdout: {stdout.decode()}")
|
||||||
|
logger.debug(f"Stderr: {stderr.decode()}")
|
||||||
|
|
||||||
|
assert proc.returncode == 0, f"publisher error code: {proc.returncode}"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.timeout(12)
|
async def test_pub_client_config_will(broker, client_config_file):
|
||||||
async def test_pub_client_config_will(broker, client_config, client_config_file):
|
|
||||||
|
|
||||||
# verifying client script functionality of will topic (publisher)
|
# verifying client script functionality of will topic (publisher)
|
||||||
# https://github.com/Yakifo/amqtt/issues/159
|
# https://github.com/Yakifo/amqtt/issues/159
|
||||||
|
await asyncio.sleep(1)
|
||||||
client1 = MQTTClient(client_id="client1")
|
client1 = MQTTClient(client_id="client1")
|
||||||
await client1.connect('mqtt://localhost:1884')
|
await client1.connect('mqtt://localhost:1884')
|
||||||
await client1.subscribe([
|
await client1.subscribe([
|
||||||
("test/will/topic", QOS_0)
|
("test/will/topic", QOS_0)
|
||||||
])
|
])
|
||||||
|
|
||||||
pub_proc = subprocess.run(
|
cmd = ["amqtt_pub",
|
||||||
[
|
|
||||||
"amqtt_pub",
|
|
||||||
"-t", "test/topic",
|
"-t", "test/topic",
|
||||||
"-m", "test of regular topic",
|
"-m", "\"test of regular topic\"",
|
||||||
"-c", client_config_file
|
"-c", client_config_file]
|
||||||
],
|
|
||||||
capture_output=True,
|
proc = await asyncio.create_subprocess_shell(
|
||||||
|
" ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
)
|
)
|
||||||
assert pub_proc.returncode == 0, f"publisher error code: {pub_proc.returncode}"
|
stdout, stderr = await proc.communicate()
|
||||||
|
logger.debug(f"Command: {cmd}")
|
||||||
|
logger.debug(f"Stdout: {stdout.decode()}")
|
||||||
|
logger.debug(f"Stderr: {stderr.decode()}")
|
||||||
|
|
||||||
message = await client1.deliver_message(timeout_duration=1)
|
message = await client1.deliver_message(timeout_duration=1)
|
||||||
assert message.topic == 'test/will/topic'
|
assert message.topic == 'test/will/topic'
|
||||||
|
@ -310,8 +308,8 @@ async def test_pub_client_config_will(broker, client_config, client_config_file)
|
||||||
await client1.disconnect()
|
await client1.disconnect()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.timeout(12)
|
@pytest.mark.timeout(20)
|
||||||
async def test_sub_client_config_will(broker, client_config_file):
|
async def test_sub_client_config_will(broker, client_config, client_config_file):
|
||||||
|
|
||||||
# verifying client script functionality of will topic (subscriber)
|
# verifying client script functionality of will topic (subscriber)
|
||||||
# https://github.com/Yakifo/amqtt/issues/159
|
# https://github.com/Yakifo/amqtt/issues/159
|
||||||
|
@ -322,23 +320,29 @@ async def test_sub_client_config_will(broker, client_config_file):
|
||||||
("test/will/topic", QOS_0)
|
("test/will/topic", QOS_0)
|
||||||
])
|
])
|
||||||
|
|
||||||
sub_proc = subprocess.Popen(
|
cmd = ["amqtt_sub",
|
||||||
[
|
|
||||||
"amqtt_sub",
|
|
||||||
"-t", "test/topic",
|
"-t", "test/topic",
|
||||||
"-c", client_config_file
|
"-c", client_config_file,
|
||||||
],
|
"-n", "1"]
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
proc = await asyncio.create_subprocess_shell(
|
||||||
text=True
|
" ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
)
|
)
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(2)
|
||||||
sub_proc.send_signal(signal.SIGINT)
|
|
||||||
_, _ = sub_proc.communicate()
|
|
||||||
|
|
||||||
assert sub_proc.returncode == 0, f"subscriber error code: {sub_proc.returncode}"
|
# cause `amqtt_sub` to exit after receiving this one message
|
||||||
|
await client1.publish("test/topic", b'my test message')
|
||||||
|
|
||||||
|
|
||||||
|
# validate the 'will' message was received correctly
|
||||||
message = await client1.deliver_message(timeout_duration=3)
|
message = await client1.deliver_message(timeout_duration=3)
|
||||||
assert message.topic == 'test/will/topic'
|
assert message.topic == 'test/will/topic'
|
||||||
assert message.data == b'client ABC has disconnected'
|
assert message.data == b'client ABC has disconnected'
|
||||||
await client1.disconnect()
|
await client1.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
stdout, stderr = await proc.communicate()
|
||||||
|
logger.debug(f"Command: {cmd}")
|
||||||
|
logger.debug(f"Stdout: {stdout.decode()}")
|
||||||
|
logger.debug(f"Stderr: {stderr.decode()}")
|
||||||
|
|
|
@ -265,7 +265,6 @@ def client_config():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.timeout(12)
|
|
||||||
async def test_client_publish_will_with_retain(broker_fixture, client_config):
|
async def test_client_publish_will_with_retain(broker_fixture, client_config):
|
||||||
|
|
||||||
# verifying client functionality of will topic
|
# verifying client functionality of will topic
|
||||||
|
|
Ładowanie…
Reference in New Issue