Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bf36471
Work around CUnit snprintf macro bug
ralight Sep 3, 2025
d16c3e1
Compile time fixes for tests on Windows
ralight Sep 3, 2025
cccd589
Windows: Fix long long int parsing.
ralight Sep 3, 2025
21092c5
CMake tests: Use consistent function for adding python tests to testing
ralight Sep 3, 2025
4d6a340
Test: make env_add_ld_library_path properly cross platform
ralight Sep 3, 2025
c8f114b
Apps: Progress toward working tests on Windows
ralight Sep 3, 2025
46d3f0c
Test: Refactor client argv tests
ralight Sep 3, 2025
5703da7
Test: Make client path cross platform
ralight Sep 3, 2025
67ddba7
Windows: Fix long long parsing to uint32 and varint properties
ralight Sep 3, 2025
c26a96b
Windows: Fix client tests
ralight Sep 3, 2025
67eb4ab
Clients: Fix session-expiry-interval error parsing
ralight Sep 3, 2025
5c6c9a8
Windows: Config files for client tests
ralight Sep 3, 2025
df3c437
Test: Don't hide socket errors when reading
ralight Sep 4, 2025
3672c58
Windows: Get build type from cmake
ralight Sep 4, 2025
02b7ff9
Test: Always use log file to capture stderr in python tests
ralight Sep 4, 2025
3ae06bf
Test: Cross platform broker terminate/reload
ralight Sep 4, 2025
a4ec92b
Test: Various fixes for Windows
ralight Sep 4, 2025
a66de32
Tests: Refactor plugin path config creation for cross platform ease
ralight Sep 4, 2025
5a11642
Tests: Add paths to libraries on Windows
ralight Sep 4, 2025
f51e30f
Windows: Test fixes for lib
ralight Sep 4, 2025
3ddfdc3
Windows: Fix slow tests
ralight Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
40 changes: 37 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,43 @@ option(WITH_BUNDLED_DEPS "Build with bundled dependencies?" ON)
option(WITH_TLS "Include SSL/TLS support?" ON)
option(WITH_TLS_PSK "Include TLS-PSK support (requires WITH_TLS)?" ON)
option(WITH_TESTS "Enable tests" ON)
if(WITH_TESTS)
find_package(GTest REQUIRED)
include(GoogleTest)
enable_testing()

function(test_set_windows_path TEST_NAME)
if(WIN32)
set(PATHMOD
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libmosquitto_common>"
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libmosquitto>"
"PATH=path_list_prepend:${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin"
)
set_property(TEST ${TEST_NAME} PROPERTY ENVIRONMENT_MODIFICATION "${PATHMOD}")
endif()
endfunction()

function(add_python_tests_from_current_directory2 PREFIX PY_TEST_FILES EXCLUDE_LIST)
foreach(PY_TEST_FILE ${PY_TEST_FILES})
get_filename_component(PY_TEST_NAME ${PY_TEST_FILE} NAME_WE)
if(${PY_TEST_NAME} IN_LIST EXCLUDE_LIST)
continue()
endif()
add_test(NAME ${PREFIX}-${PY_TEST_NAME} COMMAND python3 ${PY_TEST_FILE})
set_tests_properties(${PREFIX}-${PY_TEST_NAME}
PROPERTIES
ENVIRONMENT "BUILD_ROOT=${CMAKE_BINARY_DIR}"
)
test_set_windows_path(${PREFIX}-${PY_TEST_NAME})
endforeach()
endfunction()

function(add_python_tests_from_current_directory PREFIX EXCLUDE_LIST)
file(GLOB PY_TEST_FILES [0-9][0-9]-*.py)
add_python_tests_from_current_directory2("${PREFIX}" "${PY_TEST_FILES}" "${EXCLUDE_LIST}")
endfunction()
endif()

option(INC_MEMTRACK "Include memory tracking support?" ON)
if (WITH_TLS)
find_package(OpenSSL REQUIRED)
Expand Down Expand Up @@ -232,8 +269,5 @@ install(
# Testing
# ========================================
if(WITH_TESTS)
find_package(GTest REQUIRED)
include(GoogleTest)
enable_testing()
add_subdirectory(test)
endif()
34 changes: 24 additions & 10 deletions client/client_props.c
Original file line number Diff line number Diff line change
Expand Up @@ -175,22 +175,36 @@ int cfg_parse_property(struct mosq_config *cfg, int argc, char *argv[], int *idx
}
rc = mosquitto_property_add_int16(proplist, identifier, (uint16_t )tmpl);
break;
case MQTT_PROP_TYPE_INT32:
tmpl = atol(value);
if(tmpl < 0 || tmpl > UINT32_MAX){
fprintf(stderr, "Error: Property value (%ld) out of range for property %s.\n\n", tmpl, propname);
case MQTT_PROP_TYPE_INT32: {
char* endptr = NULL;
long long tmpll = strtoll(value, &endptr, 0);
if (endptr == value || endptr[0] != '\0') {
/* Entirety of argument wasn't a number */
fprintf(stderr, "Error: Property value '%s' not a number.\n\n", value);
return 1;
}
if (tmpll < 0 || tmpll > UINT32_MAX) {
fprintf(stderr, "Error: Property value (%lld) out of range for property %s.\n\n", tmpll, propname);
return MOSQ_ERR_INVAL;
}
rc = mosquitto_property_add_int32(proplist, identifier, (uint32_t )tmpl);
rc = mosquitto_property_add_int32(proplist, identifier, (uint32_t)tmpll);
break;
case MQTT_PROP_TYPE_VARINT:
tmpl = atol(value);
if(tmpl < 0 || tmpl > UINT32_MAX){
fprintf(stderr, "Error: Property value (%ld) out of range for property %s.\n\n", tmpl, propname);
}
case MQTT_PROP_TYPE_VARINT: {
char* endptr = NULL;
long long tmpll = strtoll(value, &endptr, 0);
if (endptr == value || endptr[0] != '\0') {
/* Entirety of argument wasn't a number */
fprintf(stderr, "Error: Property value '%s' not a number.\n\n", value);
return 1;
}
if (tmpll < 0 || tmpll > UINT32_MAX) {
fprintf(stderr, "Error: Property value (%lld) out of range for property %s.\n\n", tmpll, propname);
return MOSQ_ERR_INVAL;
}
rc = mosquitto_property_add_varint(proplist, identifier, (uint32_t )tmpl);
rc = mosquitto_property_add_varint(proplist, identifier, (uint32_t )tmpll);
break;
}
case MQTT_PROP_TYPE_BINARY:
szt = strlen(value);
if(szt > UINT16_MAX){
Expand Down
2 changes: 1 addition & 1 deletion client/client_shared.c
Original file line number Diff line number Diff line change
Expand Up @@ -1310,7 +1310,7 @@ int client_config_line_proc(struct mosq_config *cfg, int pub_or_sub, int argc, c
cfg->session_expiry_interval = UINT32_MAX;
}else{
char *endptr = NULL;
cfg->session_expiry_interval = strtol(argv[i+1], &endptr, 0);
cfg->session_expiry_interval = strtoll(argv[i+1], &endptr, 0);
if(endptr == argv[i+1] || endptr[0] != '\0'){
/* Entirety of argument wasn't a number */
fprintf(stderr, "Error: session-expiry-interval not a number.\n\n");
Expand Down
2 changes: 1 addition & 1 deletion client/client_shared.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ struct mosq_config {
bool pretty; /* sub, rr */
unsigned int timeout; /* sub */
int sub_opts; /* sub */
long session_expiry_interval;
long long session_expiry_interval;
int random_filter; /* sub */
int transport;
#ifndef WIN32
Expand Down
14 changes: 1 addition & 13 deletions test/apps/ctrl/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,7 @@ set(EXCLUDE_LIST
# none
)

foreach(PY_TEST_FILE ${PY_TEST_FILES})
get_filename_component(PY_TEST_NAME ${PY_TEST_FILE} NAME_WE)
if(${PY_TEST_NAME} IN_LIST EXCLUDE_LIST)
continue()
endif()
add_test(NAME apps-${PY_TEST_NAME}
COMMAND ${PY_TEST_FILE}
)
set_tests_properties(apps-${PY_TEST_NAME}
PROPERTIES
ENVIRONMENT "BUILD_ROOT=${CMAKE_BINARY_DIR}"
)
endforeach()
add_python_tests_from_current_directory2("apps" "${PY_TEST_FILES}" "${EXCLUDE_LIST}")


function(add_ctrl_shell_test TEST_NAME)
Expand Down
23 changes: 16 additions & 7 deletions test/apps/ctrl/ctrl-args.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

# Test parsing of command line args and errors. Does not test arg functionality.

import platform
from mosq_test_helper import *

def do_test(args, rc_expected, response=None):
proc = subprocess.run([mosq_test.get_build_root()+"/apps/mosquitto_ctrl/mosquitto_ctrl"]
proc = subprocess.run([mosquitto_ctrl_path]
+ args,
env=env, capture_output=True, encoding='utf-8', timeout=2)

Expand Down Expand Up @@ -137,20 +138,28 @@ def do_test(args, rc_expected, response=None):

# Missing file
env["HOME"] = "/tmp"
env["USERPROFILE"] = "D:"
do_test(["--cert", ssl_dir / "client.crt"], 1, response="Error: Both certfile and keyfile must be provided if one of them is set.\n")

# Invalid file
env["XDG_CONFIG_HOME"] = "."
with open("mosquitto_ctrl", "w") as f:
f.write(f"--cert {ssl_dir}/client.crt\n")
if platform.system() == 'Windows':
envvar = "USERPROFILE"
filename = "mosquitto_ctrl.conf"
else:
envvar = "XDG_CONFIG_HOME"
filename = "mosquitto_ctrl"

env[envvar] = "."
with open(filename, "w") as f:
f.write(f"--cert {ssl_dir / 'client.crt'}\n")
f.write(f"--key\n")
do_test(["broker"], 1, response="Error: --key argument given but no file specified.\n\n")

# Empty file
env["XDG_CONFIG_HOME"] = "."
with open("mosquitto_ctrl", "w") as f:
env[envvar] = "."
with open(filename, "w") as f:
pass
do_test(["--cert", ssl_dir / "client.crt"], 1, response="Error: Both certfile and keyfile must be provided if one of them is set.\n")
os.remove("mosquitto_ctrl")
os.remove(filename)

exit(0)
18 changes: 9 additions & 9 deletions test/apps/ctrl/ctrl-broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
def write_config(filename, ports):
with open(filename, 'w') as f:
f.write("enable_control_api true\n")
f.write(f"global_plugin {mosq_test.get_build_root()}/plugins/dynamic-security/mosquitto_dynamic_security.so\n")
f.write(f"plugin_opt_config_file {ports[0]}/dynamic-security.json\n")
f.write(f"global_plugin {mosq_plugins.DYNSEC_PLUGIN_PATH}\n")
f.write(f"plugin_opt_config_file {Path(str(ports[0]), 'dynamic-security.json')}\n")
f.write("allow_anonymous false\n")
f.write(f"listener {ports[0]}\n")
f.write(f"listener {ports[1]}\n")
f.write(f"certfile {ssl_dir}/server.crt\n")
f.write(f"keyfile {ssl_dir}/server.key\n")
f.write(f"certfile {Path(ssl_dir, 'server.crt')}\n")
f.write(f"keyfile {Path(ssl_dir, 'server.key')}\n")

def ctrl_cmd(cmd, args, ports, response=None):
opts = ["-u", "admin",
Expand All @@ -30,10 +30,10 @@ def ctrl_cmd(cmd, args, ports, response=None):
capture_output = False
else:
opts += ["-p", str(ports[1])]
opts += ["--cafile", f"{ssl_dir}/all-ca.crt"]
opts += ["--cafile", str(Path(ssl_dir, "all-ca.crt"))]
capture_output = True

proc = subprocess.run([mosq_test.get_build_root() + "/apps/mosquitto_ctrl/mosquitto_ctrl"]
proc = subprocess.run([mosquitto_ctrl_path]
+ opts + [cmd] + args,
env=env, capture_output=True, encoding='utf-8')

Expand All @@ -56,7 +56,7 @@ def ctrl_cmd(cmd, args, ports, response=None):
os.mkdir(str(ports[0]))

# Generate initial dynsec file
ctrl_cmd("dynsec", ["init", f"{ports[0]}/dynamic-security.json", "admin", "admin"], ports)
ctrl_cmd("dynsec", ["init", Path(str(ports[0]), "dynamic-security.json"), "admin", "admin"], ports)
ctrl_cmd("broker", ["help"], ports)

# Then start broker
Expand All @@ -79,12 +79,12 @@ def ctrl_cmd(cmd, args, ports, response=None):
finally:
os.remove(conf_file)
try:
os.remove(f"{ports[0]}/dynamic-security.json")
os.remove(Path(str(ports[0]), "dynamic-security.json"))
pass
except FileNotFoundError:
pass
shutil.rmtree(f"{ports[0]}")
broker.terminate()
mosq_test.terminate_broker(broker)
if mosq_test.wait_for_subprocess(broker):
print("broker not terminated")
if rc == 0: rc=1
Expand Down
28 changes: 14 additions & 14 deletions test/apps/ctrl/ctrl-dynsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

def write_config(filename, ports):
with open(filename, 'w') as f:
f.write(f"global_plugin {mosq_test.get_build_root()}/plugins/dynamic-security/mosquitto_dynamic_security.so\n")
f.write(f"plugin_opt_config_file {ports[0]}/dynamic-security.json\n")
f.write(f"global_plugin {mosq_plugins.DYNSEC_PLUGIN_PATH}\n")
f.write(f"plugin_opt_config_file {Path(str(ports[0]), 'dynamic-security.json')}\n")
f.write("allow_anonymous false\n")
f.write(f"listener {ports[0]}\n")
f.write(f"listener {ports[1]}\n")
f.write(f"certfile {ssl_dir}/server.crt\n")
f.write(f"keyfile {ssl_dir}/server.key\n")
f.write(f"certfile {Path(ssl_dir, 'server.crt')}\n")
f.write(f"keyfile {Path(ssl_dir, 'server.key')}\n")

def ctrl_dynsec_cmd(args, ports, response=None, input=None):
opts = ["-u", "admin",
Expand All @@ -26,9 +26,9 @@ def ctrl_dynsec_cmd(args, ports, response=None, input=None):
]
else:
opts += ["-p", str(ports[1])]
opts += ["--cafile", f"{ssl_dir}/all-ca.crt"]
opts += ["--cafile", Path(ssl_dir, "all-ca.crt")]

proc = subprocess.run([mosq_test.get_build_root()+"/apps/mosquitto_ctrl/mosquitto_ctrl"]
proc = subprocess.run([mosquitto_ctrl_path]
+ opts + ["dynsec"] + args,
env=env, capture_output=True, encoding='utf-8', timeout=2, input=input)

Expand All @@ -42,9 +42,9 @@ def ctrl_dynsec_cmd(args, ports, response=None, input=None):
raise ValueError(args)

def ctrl_dynsec_file_cmd(args, ports, response=None):
opts = ["-f", f"{ports[0]}/dynamic-security.json"]
opts = ["-f", Path(str(ports[0]), "dynamic-security.json")]

proc = subprocess.run([mosq_test.get_build_root()+"/apps/mosquitto_ctrl/mosquitto_ctrl"]
proc = subprocess.run([mosquitto_ctrl_path]
+ opts + ["dynsec"] + args,
env=env, capture_output=True, encoding='utf-8')

Expand All @@ -67,20 +67,20 @@ def ctrl_dynsec_file_cmd(args, ports, response=None):
os.mkdir(str(ports[0]))

# Generate initial dynsec file
ctrl_dynsec_cmd(["init", f"{ports[0]}/dynamic-security.json", "admin", "admin"], ports)
ctrl_dynsec_cmd(["init", Path(str(ports[0]), "dynamic-security.json"), "admin", "admin"], ports)
try:
# If we're root, set file ownership to "nobody", because that is the user
# the broker will change to.
os.chown(f"{ports[0]}/dynamic-security.json", 65534, 65534)
except PermissionError:
os.chown(Path(str(ports[0]), "dynamic-security.json"), 65534, 65534)
except (PermissionError, AttributeError):
pass

ctrl_dynsec_file_cmd(["help"], ports) # get the help, don't check the response though
ctrl_dynsec_file_cmd(["setClientPassword", "admin", "newadmin", "-i", "10000"], ports)
ctrl_dynsec_file_cmd(["setClientPassword", "admin", "newadmin"], ports)

# Then start broker
broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=ports[0], nolog=True)
broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=ports[0])

try:
rc = 1
Expand Down Expand Up @@ -226,12 +226,12 @@ def ctrl_dynsec_file_cmd(args, ports, response=None):
finally:
os.remove(conf_file)
try:
os.remove(f"{ports[0]}/dynamic-security.json")
os.remove(Path(str(ports[0]), "dynamic-security.json"))
pass
except FileNotFoundError:
pass
shutil.rmtree(f"{ports[0]}")
broker.terminate()
mosq_test.terminate_broker(broker)
if mosq_test.wait_for_subprocess(broker):
print("broker not terminated")
if rc == 0: rc=1
Expand Down
3 changes: 3 additions & 0 deletions test/apps/ctrl/mosq_test_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

ssl_dir = test_dir / "ssl"

import mosq_plugins
import mosq_test
import subprocess
import os

mosquitto_ctrl_path = Path(mosq_test.get_build_root(), "apps", "mosquitto_ctrl", mosq_test.get_build_type(), "mosquitto_ctrl")
14 changes: 1 addition & 13 deletions test/apps/db_dump/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,4 @@ set(EXCLUDE_LIST
# none
)

foreach(PY_TEST_FILE ${PY_TEST_FILES})
get_filename_component(PY_TEST_NAME ${PY_TEST_FILE} NAME_WE)
if(${PY_TEST_NAME} IN_LIST EXCLUDE_LIST)
continue()
endif()
add_test(NAME apps-${PY_TEST_NAME}
COMMAND ${PY_TEST_FILE}
)
set_tests_properties(apps-${PY_TEST_NAME}
PROPERTIES
ENVIRONMENT "BUILD_ROOT=${CMAKE_BINARY_DIR}"
)
endforeach()
add_python_tests_from_current_directory2("apps" "${PY_TEST_FILES}" "${EXCLUDE_LIST}")
7 changes: 4 additions & 3 deletions test/apps/db_dump/db-dump-client-stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ def do_test(file, counts):
f" {counts[4]}\n"

cmd = [
mosq_test.get_build_root()+'/apps/db_dump/mosquitto_db_dump',
mosquitto_db_dump_path,
'--client-stats',
f'{test_dir}/apps/db_dump/data/{file}'
Path(test_dir, "apps", "db_dump", "data", file)
]
env = mosq_test.env_add_ld_library_path()

res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=1, encoding='utf-8')
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=1, encoding='utf-8', env=env)
if res.stdout != stdout:
print(res.stdout)
print(stdout)
Expand Down
9 changes: 5 additions & 4 deletions test/apps/db_dump/db-dump-corrupt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
def do_test(file, stderr, rc_expected):

cmd = [
mosq_test.get_build_root()+'/apps/db_dump/mosquitto_db_dump',
f'{test_dir}/apps/db_dump/data/{file}'
mosquitto_db_dump_path,
Path(test_dir, "apps", "db_dump", "data", file)
]

res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=1, encoding='utf-8')
env = mosq_test.env_add_ld_library_path()
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=1, encoding='utf-8', env=env)
if res.stderr != stderr:
print(res.stderr)
raise mosq_test.TestError
Expand All @@ -18,7 +19,7 @@ def do_test(file, stderr, rc_expected):
print(res.returncode)
raise mosq_test.TestError

do_test('missing.test-db', f"Error: Unable to open {test_dir}/apps/db_dump/data/missing.test-db\n", 0)
do_test('missing.test-db', f"Error: Unable to open {str(Path(test_dir, 'apps', 'db_dump', 'data', 'missing.test-db'))}\n", 0)
do_test('bad-magic.test-db', "Error: Unrecognised file format.\n", 1)
do_test('short.test-db', "", 1)
do_test('bad-dbid-size.test-db', "Error: Incompatible database configuration (dbid size is 5 bytes, expected 8)", 1)
Expand Down
Loading