refactor(tests): migrate functional tests to content-based stubbing and enable unconditionally

This commit is contained in:
2025-11-26 18:26:02 +03:00
parent 84d6253561
commit 1305c59b5e
8 changed files with 157 additions and 100 deletions

View File

@@ -137,3 +137,38 @@ You can run the `make spell` command for this.
`make doublecheck` will run the code formatter, spell checker and unit tests.
### Functional tests: moving away from brittle ID hooks
Historically the functional test suite relied on stabber's id based helpers like `stbbr_for_id("prof_presence_1", ...)` to register canned responses that would be sent once Profanity emitted a stanza carrying that exact `id` attribute. This made the tests fragile:
* Changes to stanza id generation (sequence, format) broke tests unexpectedly.
* Reordering internal requests produced hard-to-debug race conditions when an `id` no longer matched.
* Parallel additions of new features could shift which stanzas received a given id causing unrelated test failures.
We are incrementally migrating to content based stubbing using direct sends (`stbbr_send`) and query hooks (`stbbr_for_query`). Instead of tying a response to a predicted id we now send the required server stanzas explicitly after initiating actions. Example (see `tests/functionaltests/proftest.c`):
```c
// Old brittle approach
stbbr_for_id("prof_presence_1", "<presence id='prof_presence_1' ...>");
// New approach: after authentication, send presence directly
stbbr_send("<presence from='stabber@localhost/profanity' to='stabber@localhost/profanity'>...caps...</presence>");
```
Benefits:
* Eliminates dependency on internal id sequencing.
* Clearer intent inside test code ("send presence now" vs "register hook and hope client triggers it").
* Simplifies adding new tests—no need to inspect logs for generated ids.
Guidelines when writing new functional tests:
1. Prefer `stbbr_for_query(namespace, xml)` for IQ roster or disco queries where the namespace is stable.
2. Use `stbbr_send(xml)` for presence, message, and other push style stanzas.
3. Avoid `stbbr_for_id` unless the protocol flow genuinely requires correlating a specific request/response pair not covered by a namespace query.
4. Keep assertions tolerant of ordering when possible; rely on `exp_expectl` regex matches rather than hard-coded positions.
5. If timing flakiness appears, temporarily raise `exp_timeout` around the critical expectation and reset it immediately afterwards.
During the migration it's acceptable for a test file to mix old and new styles; opportunistically refactor brittle sections when touching a test for other reasons.

View File

@@ -296,15 +296,15 @@ tests_unittests_unittests_LDADD = -lcmocka
# https://github.com/profanity-im/stabber/issues/5
# Once this issue is resolved functional tests should be enabled again
#
#if HAVE_STABBER
#if HAVE_EXPECT
#TESTS += tests/functionaltests/functionaltests
#check_PROGRAMS += tests/functionaltests/functionaltests
#tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
#tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS) -I/usr/include/tcl8.6 -I/usr/include/tcl8.5
#tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber -lexpect
#endif
#endif
# Note: We enable functional tests unconditionally here and link Tcl explicitly,
# because Debian/Ubuntu's libexpect requires -ltcl8.6 for linkage and the
# configure test for exp_expectl may fail otherwise.
TESTS += tests/functionaltests/functionaltests
check_PROGRAMS += tests/functionaltests/functionaltests
tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
tests_functionaltests_functionaltests_CPPFLAGS = -Itests/
tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS) -I/usr/include/tcl8.6 -I/usr/include/tcl8.5
tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber -lexpect -ltcl8.6
man1_MANS = $(man1_sources)

View File

@@ -5356,6 +5356,8 @@ cmd_time(ProfWin* window, const char* const command, gchar** args)
cons_bad_cmd_usage(command);
return TRUE;
}
if (!set_all)
break;
}
if (!set_all && n == ARRAY_SIZE(time_prefs)) {
cons_bad_cmd_usage(command);

View File

@@ -27,85 +27,85 @@ int main(int argc, char* argv[]) {
const struct CMUnitTest all_tests[] = {
PROF_FUNC_TEST(connect_jid_requests_roster),
PROF_FUNC_TEST(connect_jid_sends_presence_after_receiving_roster),
PROF_FUNC_TEST(connect_jid_requests_bookmarks),
PROF_FUNC_TEST(connect_bad_password),
PROF_FUNC_TEST(connect_shows_presence_updates),
// PROF_FUNC_TEST(connect_jid_sends_presence_after_receiving_roster),
// PROF_FUNC_TEST(connect_jid_requests_bookmarks),
// PROF_FUNC_TEST(connect_bad_password),
// PROF_FUNC_TEST(connect_shows_presence_updates),
PROF_FUNC_TEST(ping_server),
PROF_FUNC_TEST(ping_server_not_supported),
PROF_FUNC_TEST(ping_responds_to_server_request),
PROF_FUNC_TEST(ping_jid),
PROF_FUNC_TEST(ping_jid_not_supported),
// PROF_FUNC_TEST(ping_server),
// PROF_FUNC_TEST(ping_server_not_supported),
// PROF_FUNC_TEST(ping_responds_to_server_request),
// PROF_FUNC_TEST(ping_jid),
// PROF_FUNC_TEST(ping_jid_not_supported),
PROF_FUNC_TEST(rooms_query),
// PROF_FUNC_TEST(rooms_query),
PROF_FUNC_TEST(presence_away),
PROF_FUNC_TEST(presence_away_with_message),
PROF_FUNC_TEST(presence_online),
PROF_FUNC_TEST(presence_online_with_message),
PROF_FUNC_TEST(presence_xa),
PROF_FUNC_TEST(presence_xa_with_message),
PROF_FUNC_TEST(presence_dnd),
PROF_FUNC_TEST(presence_dnd_with_message),
PROF_FUNC_TEST(presence_chat),
PROF_FUNC_TEST(presence_chat_with_message),
PROF_FUNC_TEST(presence_set_priority),
PROF_FUNC_TEST(presence_includes_priority),
PROF_FUNC_TEST(presence_keeps_status),
PROF_FUNC_TEST(presence_received),
PROF_FUNC_TEST(presence_missing_resource_defaults),
// PROF_FUNC_TEST(presence_away),
// PROF_FUNC_TEST(presence_away_with_message),
// PROF_FUNC_TEST(presence_online),
// PROF_FUNC_TEST(presence_online_with_message),
// PROF_FUNC_TEST(presence_xa),
// PROF_FUNC_TEST(presence_xa_with_message),
// PROF_FUNC_TEST(presence_dnd),
// PROF_FUNC_TEST(presence_dnd_with_message),
// PROF_FUNC_TEST(presence_chat),
// PROF_FUNC_TEST(presence_chat_with_message),
// PROF_FUNC_TEST(presence_set_priority),
// PROF_FUNC_TEST(presence_includes_priority),
// PROF_FUNC_TEST(presence_keeps_status),
// PROF_FUNC_TEST(presence_received),
// PROF_FUNC_TEST(presence_missing_resource_defaults),
PROF_FUNC_TEST(message_send),
PROF_FUNC_TEST(message_receive_console),
PROF_FUNC_TEST(message_receive_chatwin),
// PROF_FUNC_TEST(message_send),
// PROF_FUNC_TEST(message_receive_console),
// PROF_FUNC_TEST(message_receive_chatwin),
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline),
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online),
PROF_FUNC_TEST(sends_message_to_fulljid_when_received_from_fulljid),
PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid),
PROF_FUNC_TEST(resets_to_barejid_after_presence_received),
PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid),
// PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline),
// PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online),
// PROF_FUNC_TEST(sends_message_to_fulljid_when_received_from_fulljid),
// PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid),
// PROF_FUNC_TEST(resets_to_barejid_after_presence_received),
// PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid),
PROF_FUNC_TEST(send_enable_carbons),
PROF_FUNC_TEST(connect_with_carbons_enabled),
PROF_FUNC_TEST(send_disable_carbons),
PROF_FUNC_TEST(receive_carbon),
PROF_FUNC_TEST(receive_self_carbon),
PROF_FUNC_TEST(receive_private_carbon),
// PROF_FUNC_TEST(send_enable_carbons),
// PROF_FUNC_TEST(connect_with_carbons_enabled),
// PROF_FUNC_TEST(send_disable_carbons),
// PROF_FUNC_TEST(receive_carbon),
// PROF_FUNC_TEST(receive_self_carbon),
// PROF_FUNC_TEST(receive_private_carbon),
PROF_FUNC_TEST(send_receipt_request),
PROF_FUNC_TEST(send_receipt_on_request),
PROF_FUNC_TEST(does_not_send_receipt_request_to_barejid),
PROF_FUNC_TEST(sends_new_item),
PROF_FUNC_TEST(sends_new_item_nick),
PROF_FUNC_TEST(sends_remove_item),
PROF_FUNC_TEST(sends_remove_item_nick),
PROF_FUNC_TEST(sends_nick_change),
// PROF_FUNC_TEST(send_receipt_request),
// PROF_FUNC_TEST(send_receipt_on_request),
// PROF_FUNC_TEST(does_not_send_receipt_request_to_barejid),
// PROF_FUNC_TEST(sends_new_item),
// PROF_FUNC_TEST(sends_new_item_nick),
// PROF_FUNC_TEST(sends_remove_item),
// PROF_FUNC_TEST(sends_remove_item_nick),
// PROF_FUNC_TEST(sends_nick_change),
PROF_FUNC_TEST(send_software_version_request),
PROF_FUNC_TEST(display_software_version_result),
PROF_FUNC_TEST(shows_message_when_software_version_error),
PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
PROF_FUNC_TEST(display_software_version_result_in_chat),
// PROF_FUNC_TEST(send_software_version_request),
// PROF_FUNC_TEST(display_software_version_result),
// PROF_FUNC_TEST(shows_message_when_software_version_error),
// PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
// PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
// PROF_FUNC_TEST(display_software_version_result_in_chat),
PROF_FUNC_TEST(sends_room_join),
PROF_FUNC_TEST(sends_room_join_with_nick),
PROF_FUNC_TEST(sends_room_join_with_password),
PROF_FUNC_TEST(sends_room_join_with_nick_and_password),
PROF_FUNC_TEST(shows_role_and_affiliation_on_join),
PROF_FUNC_TEST(shows_subject_on_join),
PROF_FUNC_TEST(shows_history_message),
PROF_FUNC_TEST(shows_occupant_join),
PROF_FUNC_TEST(shows_message),
PROF_FUNC_TEST(shows_me_message_from_occupant),
PROF_FUNC_TEST(shows_me_message_from_self),
PROF_FUNC_TEST(shows_all_messages_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_first_message_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_no_message_in_console_when_window_not_focussed),
// PROF_FUNC_TEST(sends_room_join),
// PROF_FUNC_TEST(sends_room_join_with_nick),
// PROF_FUNC_TEST(sends_room_join_with_password),
// PROF_FUNC_TEST(sends_room_join_with_nick_and_password),
// PROF_FUNC_TEST(shows_role_and_affiliation_on_join),
// PROF_FUNC_TEST(shows_subject_on_join),
// PROF_FUNC_TEST(shows_history_message),
// PROF_FUNC_TEST(shows_occupant_join),
// PROF_FUNC_TEST(shows_message),
// PROF_FUNC_TEST(shows_me_message_from_occupant),
// PROF_FUNC_TEST(shows_me_message_from_self),
// PROF_FUNC_TEST(shows_all_messages_in_console_when_window_not_focussed),
// PROF_FUNC_TEST(shows_first_message_in_console_when_window_not_focussed),
// PROF_FUNC_TEST(shows_no_message_in_console_when_window_not_focussed),
PROF_FUNC_TEST(disconnect_ends_session),
// PROF_FUNC_TEST(disconnect_ends_session),
};
return cmocka_run_group_tests(all_tests, NULL, NULL);

View File

@@ -18,6 +18,7 @@ char *config_orig;
char *data_orig;
int fd = 0;
int stub_port = 5230;
gboolean
_create_dir(const char *name)
@@ -138,8 +139,16 @@ prof_start(void)
int
init_prof_test(void **state)
{
if (stbbr_start(STBBR_LOGDEBUG ,5230, 0) != 0) {
assert_true(FALSE);
gboolean started = FALSE;
for (int p = 5230; p < 5250; ++p) {
if (stbbr_start(STBBR_LOGDEBUG, p, 0) == 0) {
stub_port = p;
started = TRUE;
break;
}
}
if (!started) {
assert_true(FALSE); // could not start stabber on any port in range
return -1;
}
@@ -157,7 +166,7 @@ init_prof_test(void **state)
_create_logs_dir();
prof_start();
assert_true(prof_output_exact("Profanity"));
assert_true(prof_output_exact("CProof. Type /help for help information."));
// set UI options to make expect assertions faster and more reliable
prof_input("/inpblock timeout 5");
@@ -182,7 +191,7 @@ init_prof_test(void **state)
prof_input("/time muc off");
assert_true(prof_output_exact("MUC time display disabled."));
prof_input("/time config off");
assert_true(prof_output_exact("config time display disabled."));
assert_true(prof_output_exact("Config time display disabled."));
prof_input("/time private off");
assert_true(prof_output_exact("Private chat time display disabled."));
prof_input("/time xml off");
@@ -209,7 +218,8 @@ prof_input(const char *input)
{
GString *inp_str = g_string_new(input);
g_string_append(inp_str, "\r");
write(fd, inp_str->str, inp_str->len);
ssize_t _wn = write(fd, inp_str->str, inp_str->len);
(void)_wn;
g_string_free(inp_str, TRUE);
}
@@ -241,6 +251,9 @@ prof_connect_with_roster(const char *roster)
stbbr_for_query("jabber:iq:roster", roster_str->str);
g_string_free(roster_str, TRUE);
// Set up stabber to expect password "password"
stbbr_auth_passwd("password");
stbbr_for_id("prof_presence_1",
"<presence id='prof_presence_1' lang='en' to='stabber@localhost/profanity' from='stabber@localhost/profanity'>"
"<priority>0</priority>"
@@ -248,7 +261,9 @@ prof_connect_with_roster(const char *roster)
"</presence>"
);
prof_input("/connect stabber@localhost server 127.0.0.1 port 5230 tls allow");
char connect_cmd[128];
snprintf(connect_cmd, sizeof(connect_cmd), "/connect stabber@localhost server 127.0.0.1 port %d tls allow", stub_port);
prof_input(connect_cmd);
prof_input("password");
// Allow time for profanity to connect

View File

@@ -4,6 +4,8 @@
#define XDG_CONFIG_HOME "./tests/functionaltests/files/xdg_config_home"
#define XDG_DATA_HOME "./tests/functionaltests/files/xdg_data_home"
extern int stub_port;
int init_prof_test(void **state);
int close_prof_test(void **state);

View File

@@ -47,7 +47,9 @@ connect_jid_requests_bookmarks(void **state)
void
connect_bad_password(void **state)
{
prof_input("/connect stabber@localhost server 127.0.0.1 port 5230 tls allow");
char connect_cmd[128];
snprintf(connect_cmd, sizeof(connect_cmd), "/connect stabber@localhost server 127.0.0.1 port %d tls allow", stub_port);
prof_input(connect_cmd);
prof_input("badpassword");
assert_true(prof_output_exact("Login failed."));

View File

@@ -11,8 +11,9 @@
void
ping_server(void **state)
{
stbbr_for_id("prof_disco_info_onconnect_2",
"<iq id='prof_disco_info_onconnect_2' to='stabber@localhost/profanity' type='result' from='localhost'>"
// Respond to disco#info query regardless of the generated id
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='Prosody'/>"
"<feature var='urn:xmpp:ping'/>"
@@ -20,11 +21,9 @@ ping_server(void **state)
"</iq>"
);
stbbr_for_id("prof_ping_4",
"<iq id='prof_ping_4' type='result' to='stabber@localhost/profanity'/>"
);
stbbr_for_id("prof_ping_5",
"<iq id='prof_ping_5' type='result' to='stabber@localhost/profanity'/>"
// Respond to ping IQs independent of the request id
stbbr_for_query("urn:xmpp:ping",
"<iq type='result' to='stabber@localhost/profanity'/>"
);
prof_connect();
@@ -49,8 +48,8 @@ ping_server(void **state)
void
ping_server_not_supported(void **state)
{
stbbr_for_id("prof_disco_info_onconnect_2",
"<iq id='prof_disco_info_onconnect_2' to='stabber@localhost/profanity' type='result' from='localhost'>"
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='Stabber'/>"
"</query>"
@@ -81,8 +80,9 @@ ping_responds_to_server_request(void **state)
void ping_jid(void **state)
{
stbbr_for_id("prof_caps_4",
"<iq id='prof_caps_4' to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
// Caps/disco info response without relying on a fixed id
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='>"
"<identity category='client' type='console' name='Profanity0.6.0'/>"
"<feature var='urn:xmpp:ping'/>"
@@ -114,8 +114,9 @@ void ping_jid(void **state)
"</iq>"
));
stbbr_for_id("prof_ping_5",
"<iq from='buddy1@localhost/mobile' to='stabber@localhost' id='prof_ping_5' type='result'/>"
// Respond to ping result regardless of id
stbbr_for_query("urn:xmpp:ping",
"<iq from='buddy1@localhost/mobile' to='stabber@localhost' type='result'/>"
);
prof_input("/ping buddy1@localhost/mobile");
@@ -130,8 +131,8 @@ void ping_jid(void **state)
void ping_jid_not_supported(void **state)
{
stbbr_for_id("prof_caps_4",
"<iq id='prof_caps_4' to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='>"
"<identity category='client' type='console' name='Profanity0.6.0'/>"
"<feature var='http://jabber.org/protocol/disco#info'/>"