From 1be326911c082443407d60da6317f237992732e3 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Mon, 23 Feb 2026 17:23:54 +0100 Subject: [PATCH 1/9] tests: Improve autocomplete unit tests It seems like we didn't even check whether the operations succeed or not. * `clear_empty()` Add assertion to confirm a newly created autocomplete object is empty. * `find_after_create()`: Add assertion to ensure `autocomplete_complete()` returns NULL when no entries are present in th object. --- tests/unittests/test_autocomplete.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unittests/test_autocomplete.c b/tests/unittests/test_autocomplete.c index 3988aaa4..0377fe87 100644 --- a/tests/unittests/test_autocomplete.c +++ b/tests/unittests/test_autocomplete.c @@ -9,7 +9,10 @@ void clear_empty(void** state) { Autocomplete ac = autocomplete_new(); + char* result = autocomplete_complete(ac, "test", TRUE, FALSE); + assert_null(result); autocomplete_free(ac); + free(result); } void @@ -25,6 +28,7 @@ find_after_create(void** state) { Autocomplete ac = autocomplete_new(); char* result = autocomplete_complete(ac, "hello", TRUE, FALSE); + assert_null(result); autocomplete_free(ac); free(result); } From 74bf33e727da6e4b374e1e6e60153c27fde01db2 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Mon, 23 Feb 2026 17:43:22 +0100 Subject: [PATCH 2/9] tests: Add test for get_expanded_path() --- tests/unittests/test_common.c | 26 ++++++++++++++++++++++++++ tests/unittests/test_common.h | 1 + tests/unittests/unittests.c | 1 + 3 files changed, 28 insertions(+) diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index 889b2202..67901443 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -327,6 +327,32 @@ strip_quotes_strips_both(void** state) free(result); } +void test_get_expanded_path(void** state) { + gchar* expanded_path; + + // `file://` prefix + expanded_path = get_expanded_path("file:///tmp/test.txt"); + assert_string_equal("/tmp/test.txt", expanded_path); + g_free(expanded_path); + + // `~/"` prefix + setenv("HOME", "/home/test", 1); + expanded_path = get_expanded_path("~/folder/file.txt"); + assert_string_equal("/home/test/folder/file.txt", expanded_path); + g_free(expanded_path); + unsetenv("HOME"); + + // regular path + expanded_path = get_expanded_path("/home/test/file.pdf"); + assert_string_equal("/home/test/file.pdf", expanded_path); + g_free(expanded_path); + + // empty path + expanded_path = get_expanded_path(""); + assert_string_equal("", expanded_path); + g_free(expanded_path); +} + typedef struct { char* template; diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index ffcc6c9d..32feed02 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -34,3 +34,4 @@ void prof_whole_occurrences_tests(void** state); void prof_occurrences_of_large_message_tests(void** state); void unique_filename_from_url_td(void** state); void format_call_external_argv_td(void** state); +void test_get_expanded_path(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 5e6833fe..e64ea002 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -94,6 +94,7 @@ main(int argc, char* argv[]) cmocka_unit_test(strip_quotes_strips_both), cmocka_unit_test(format_call_external_argv_td), cmocka_unit_test(unique_filename_from_url_td), + cmocka_unit_test(test_get_expanded_path), cmocka_unit_test(clear_empty), cmocka_unit_test(reset_after_create), From 5a7bba809357aaa593ef9f807904062a29ca62c1 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Mon, 23 Feb 2026 18:36:52 +0100 Subject: [PATCH 3/9] tests: Add test for string_to_verbosity() --- tests/unittests/test_common.c | 62 +++++++++++++++++++++++++++++++++++ tests/unittests/test_common.h | 1 + tests/unittests/unittests.c | 1 + 3 files changed, 64 insertions(+) diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index 67901443..e6b59956 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -353,6 +353,68 @@ void test_get_expanded_path(void** state) { g_free(expanded_path); } +void test_string_to_verbosity(void** state) { + int verbosity; + gchar* err_msg = NULL; + gboolean result; + + // Valid input string (0) + result = string_to_verbosity("0", &verbosity, &err_msg); + assert_true(result); + assert_int_equal(0, verbosity); + assert_null(err_msg); + g_free(err_msg); err_msg = NULL; // Clear for next test + + // Valid input string (1) + result = string_to_verbosity("1", &verbosity, &err_msg); + assert_true(result); + assert_int_equal(1, verbosity); + assert_null(err_msg); + g_free(err_msg); err_msg = NULL; + + // Valid input string (3) + result = string_to_verbosity("3", &verbosity, &err_msg); + assert_true(result); + assert_int_equal(3, verbosity); + assert_null(err_msg); + g_free(err_msg); err_msg = NULL; + + // Invalid input string (not a number) + result = string_to_verbosity("profanity", &verbosity, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"profanity\" to a number.", err_msg); + g_free(err_msg); + err_msg = NULL; + + // Valid input string, out of range (too low) + result = string_to_verbosity("-1", &verbosity, &err_msg); + assert_false(result); + assert_string_equal("Value -1 out of range. Must be in 0..3.", err_msg); + g_free(err_msg); err_msg = NULL; + + // Valid input string, out of range (too high) + result = string_to_verbosity("4", &verbosity, &err_msg); + assert_false(result); + assert_string_equal("Value 4 out of range. Must be in 0..3.", err_msg); + g_free(err_msg); err_msg = NULL; + + // NULL input string + result = string_to_verbosity(NULL, &verbosity, &err_msg); + assert_false(result); + assert_string_equal("'str' input pointer can not be NULL", err_msg); + g_free(err_msg); err_msg = NULL; + + // Empty input string + result = string_to_verbosity("", &verbosity, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"\" to a number.", err_msg); + g_free(err_msg); err_msg = NULL; + + // err_msg is NULL + result = string_to_verbosity("abc", &verbosity, NULL); + assert_false(result); +} + typedef struct { char* template; diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index 32feed02..044534a6 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -35,3 +35,4 @@ void prof_occurrences_of_large_message_tests(void** state); void unique_filename_from_url_td(void** state); void format_call_external_argv_td(void** state); void test_get_expanded_path(void** state); +void test_string_to_verbosity(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index e64ea002..693857eb 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -94,6 +94,7 @@ main(int argc, char* argv[]) cmocka_unit_test(strip_quotes_strips_both), cmocka_unit_test(format_call_external_argv_td), cmocka_unit_test(unique_filename_from_url_td), + cmocka_unit_test(test_string_to_verbosity), cmocka_unit_test(test_get_expanded_path), cmocka_unit_test(clear_empty), From a7be9f1280c4a4d2a653f1082521adc4e253ac09 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Mon, 23 Feb 2026 19:11:08 +0100 Subject: [PATCH 4/9] tests: Add tests for strtoi_range() --- tests/unittests/test_common.c | 107 ++++++++++++++++++++++++++++++++++ tests/unittests/test_common.h | 5 ++ tests/unittests/unittests.c | 5 ++ 3 files changed, 117 insertions(+) diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index e6b59956..327638a7 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -353,6 +353,113 @@ void test_get_expanded_path(void** state) { g_free(expanded_path); } +void test_strtoi_range_valid_input(void** state) { + int value; + gchar* err_msg = NULL; + gboolean result; + + result = strtoi_range("10", &value, 0, 20, &err_msg); + assert_true(result); + assert_int_equal(10, value); + assert_null(err_msg); + + result = strtoi_range("0", &value, 0, 20, &err_msg); + assert_true(result); + assert_int_equal(0, value); + assert_null(err_msg); + + result = strtoi_range("20", &value, 0, 20, &err_msg); + assert_true(result); + assert_int_equal(20, value); + assert_null(err_msg); + + result = strtoi_range("-5", &value, -10, 0, &err_msg); + assert_true(result); + assert_int_equal(-5, value); + assert_null(err_msg); +} + +void test_strtoi_range_out_of_range(void** state) { + int value; + gchar* err_msg = NULL; + gboolean result; + + // too low, negative + result = strtoi_range("-11", &value, -10, 0, &err_msg); + assert_false(result); + assert_string_equal("Value -11 out of range. Must be in -10..0.", err_msg); + g_free(err_msg); + err_msg = NULL; + + // too low + result = strtoi_range("-1", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Value -1 out of range. Must be in 0..10.", err_msg); + g_free(err_msg); + err_msg = NULL; + + // too high + result = strtoi_range("11", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Value 11 out of range. Must be in 0..10.", err_msg); + g_free(err_msg); + err_msg = NULL; +} + +void test_strtoi_range_invalid_input(void** state) { + int value; + gchar* err_msg = NULL; + gboolean result; + + // not a number + result = strtoi_range("profanity", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"profanity\" to a number.", err_msg); + g_free(err_msg); + err_msg = NULL; + + // not a number, start with digits + result = strtoi_range("23kk", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"23kk\" to a number.", err_msg); + g_free(err_msg); + err_msg = NULL; +} + +void test_strtoi_range_null_empty_input(void** state) { + int value; + gchar* err_msg = NULL; + gboolean result; + + // NULL input string + result = strtoi_range(NULL, &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("'str' input pointer can not be NULL", err_msg); + g_free(err_msg); + err_msg = NULL; + + // Empty input string + result = strtoi_range("", &value, 0, 10, &err_msg); + assert_false(result); + assert_string_equal("Could not convert \"\" to a number.", err_msg); + g_free(err_msg); + err_msg = NULL; +} + +void test_strtoi_range_null_err_msg(void** state) { + int value; + gboolean result; + + // valid conversion, err_msg is NULL + result = strtoi_range("5", &value, 0, 10, NULL); + assert_true(result); + assert_int_equal(5, value); + + // invalid conversion, err_msg is NULL + result = strtoi_range("profanity", &value, 0, 10, NULL); + assert_false(result); +} + void test_string_to_verbosity(void** state) { int verbosity; gchar* err_msg = NULL; diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index 044534a6..d3c94786 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -36,3 +36,8 @@ void unique_filename_from_url_td(void** state); void format_call_external_argv_td(void** state); void test_get_expanded_path(void** state); void test_string_to_verbosity(void** state); +void test_strtoi_range_valid_input(void** state); +void test_strtoi_range_out_of_range(void** state); +void test_strtoi_range_invalid_input(void** state); +void test_strtoi_range_null_empty_input(void** state); +void test_strtoi_range_null_err_msg(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 693857eb..928523a5 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -96,6 +96,11 @@ main(int argc, char* argv[]) cmocka_unit_test(unique_filename_from_url_td), cmocka_unit_test(test_string_to_verbosity), cmocka_unit_test(test_get_expanded_path), + cmocka_unit_test(test_strtoi_range_valid_input), + cmocka_unit_test(test_strtoi_range_out_of_range), + cmocka_unit_test(test_strtoi_range_invalid_input), + cmocka_unit_test(test_strtoi_range_null_empty_input), + cmocka_unit_test(test_strtoi_range_null_err_msg), cmocka_unit_test(clear_empty), cmocka_unit_test(reset_after_create), From b7d53adba60af412b999131d542b0eb1a228e76e Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 24 Feb 2026 20:00:57 +0100 Subject: [PATCH 5/9] tests: Add tests for string_matches_one_of() Writing this test I'm unsure whether a function like string_matches_one_of() should actually call cons_show(). Let's just note that for later. --- tests/unittests/test_common.c | 58 +++++++++++++++++++++++++++++++++++ tests/unittests/test_common.h | 1 + tests/unittests/unittests.c | 1 + 3 files changed, 60 insertions(+) diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index 327638a7..2e3c8155 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -2,6 +2,7 @@ #include "common.h" #include "prof_cmocka.h" #include +#include "tests/unittests/ui/stub_ui.h" // Include for mocking cons_show void replace_one_substr(void** state) @@ -1041,3 +1042,60 @@ prof_whole_occurrences_tests(void** state) g_slist_free(expected); expected = NULL; } + +void test_string_matches_one_of_edge_cases(void** state) { + // is is NULL, is_can_be_null is TRUE -> should return TRUE + assert_true(string_matches_one_of(NULL, NULL, TRUE, "option1", "option2", NULL)); + + // is is NULL, is_can_be_null is FALSE -> should return FALSE + assert_false(string_matches_one_of(NULL, NULL, FALSE, "option1", "option2", NULL)); + + // is matches one of the options + assert_true(string_matches_one_of("Test", "option1", FALSE, "option1", "option2", NULL)); + assert_true(string_matches_one_of("Test", "option2", FALSE, "option1", "option2", NULL)); + + // is does not match any of the options + expect_any_cons_show(); // For "Invalid Test: 'option3'" + expect_any_cons_show(); // For "Test must be one of: 'option1', or 'option2'." + assert_false(string_matches_one_of("Test", "option3", FALSE, "option1", "option2", NULL)); + + // what is NULL (no error message printed) + assert_false(string_matches_one_of(NULL, "option3", FALSE, "option1", "option2", NULL)); + + // Empty options list (first is NULL) + expect_any_cons_show(); // For "Invalid Test: 'option1'" + expect_any_cons_show(); // For "Test must be one of: ." (empty options list error message) + assert_false(string_matches_one_of("Test", "option1", FALSE, NULL, NULL)); + assert_false(string_matches_one_of(NULL, NULL, FALSE, NULL, NULL)); + assert_true(string_matches_one_of(NULL, NULL, TRUE, NULL, NULL)); + + // Single option, matches + assert_true(string_matches_one_of("Test", "single", FALSE, "single", NULL)); + + // Single option, no match + expect_any_cons_show(); // For "Invalid Test: 'nomatch'" + expect_any_cons_show(); // For "Test must be one of: 'single'." + assert_false(string_matches_one_of("Test", "nomatch", FALSE, "single", NULL)); + + // Multiple options, first matches + assert_true(string_matches_one_of("Test", "first", FALSE, "first", "second", "third", NULL)); + + // Multiple options, middle matches + assert_true(string_matches_one_of("Test", "second", FALSE, "first", "second", "third", NULL)); + + // Multiple options, last matches + assert_true(string_matches_one_of("Test", "third", FALSE, "first", "second", "third", NULL)); + + // Multiple options, no match + expect_any_cons_show(); // For "Invalid Test: 'none'" + expect_any_cons_show(); // For "Test must be one of: 'first', 'second', or 'third'." + assert_false(string_matches_one_of("Test", "none", FALSE, "first", "second", "third", NULL)); + + // is is an empty string, options are not + expect_any_cons_show(); // For "Invalid Test: ''" + expect_any_cons_show(); // For "Test must be one of: 'option1', or 'option2'." + assert_false(string_matches_one_of("Test", "", FALSE, "option1", "option2", NULL)); + + // is is an empty string, one of the options is an empty string + assert_true(string_matches_one_of("Test", "", FALSE, "option1", "", "option2", NULL)); +} diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index d3c94786..b828ee20 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -41,3 +41,4 @@ void test_strtoi_range_out_of_range(void** state); void test_strtoi_range_invalid_input(void** state); void test_strtoi_range_null_empty_input(void** state); void test_strtoi_range_null_err_msg(void** state); +void test_string_matches_one_of_edge_cases(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 928523a5..a3bc6801 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -101,6 +101,7 @@ main(int argc, char* argv[]) cmocka_unit_test(test_strtoi_range_invalid_input), cmocka_unit_test(test_strtoi_range_null_empty_input), cmocka_unit_test(test_strtoi_range_null_err_msg), + cmocka_unit_test(test_string_matches_one_of_edge_cases), cmocka_unit_test(clear_empty), cmocka_unit_test(reset_after_create), From 0c15cd1d937abdd57b0a40796b3cd8c0ced8d336 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 24 Feb 2026 20:12:53 +0100 Subject: [PATCH 6/9] tests: Add test for valid_tls_policy_option() See last commit with comment regarding cons_show(). In this test here we can see that if we pass NULL cons_show() isn't called. Making it a bit different that other invalid test cases. --- tests/unittests/test_common.c | 24 ++++++++++++++++++++++++ tests/unittests/test_common.h | 1 + tests/unittests/unittests.c | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index 2e3c8155..ed409d9d 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -328,6 +328,30 @@ strip_quotes_strips_both(void** state) free(result); } +void test_valid_tls_policy_option(void** state) { + // Valid inputs + assert_true(valid_tls_policy_option("force")); + assert_true(valid_tls_policy_option("allow")); + assert_true(valid_tls_policy_option("trust")); + assert_true(valid_tls_policy_option("disable")); + assert_true(valid_tls_policy_option("legacy")); + assert_true(valid_tls_policy_option("direct")); + + // Invalid inputs + // Not an option + expect_any_cons_show(); // For "Invalid TLS policy: 'profanity'" + expect_any_cons_show(); // For "TLS policy must be one of: 'force', 'allow', 'trust', 'disable', 'legacy', or 'direct'." + assert_false(valid_tls_policy_option("profanity")); + + // Empty + expect_any_cons_show(); // For "Invalid TLS policy: ''" + expect_any_cons_show(); // For "TLS policy must be one of: 'force', 'allow', 'trust', 'disable', 'legacy', or 'direct'." + assert_false(valid_tls_policy_option("")); + + // NULL + assert_true(valid_tls_policy_option(NULL)); +} + void test_get_expanded_path(void** state) { gchar* expanded_path; diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index b828ee20..e66f1121 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -42,3 +42,4 @@ void test_strtoi_range_invalid_input(void** state); void test_strtoi_range_null_empty_input(void** state); void test_strtoi_range_null_err_msg(void** state); void test_string_matches_one_of_edge_cases(void** state); +void test_valid_tls_policy_option(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index a3bc6801..35253e3a 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -102,7 +102,7 @@ main(int argc, char* argv[]) cmocka_unit_test(test_strtoi_range_null_empty_input), cmocka_unit_test(test_strtoi_range_null_err_msg), cmocka_unit_test(test_string_matches_one_of_edge_cases), - + cmocka_unit_test(test_valid_tls_policy_option), cmocka_unit_test(clear_empty), cmocka_unit_test(reset_after_create), cmocka_unit_test(find_after_create), From ca8cc8f6512e7bfeb74a90375ceb23a68ce385f3 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 25 Feb 2026 11:23:34 +0100 Subject: [PATCH 7/9] tests: Add test for get_mentions() --- tests/unittests/test_common.c | 67 +++++++++++++++++++++++++++++++++++ tests/unittests/test_common.h | 1 + tests/unittests/unittests.c | 1 + 3 files changed, 69 insertions(+) diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index ed409d9d..6e96ef18 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -864,6 +864,73 @@ prof_partial_occurrences_tests(void** state) expected = NULL; } +void +get_mentions_tests(void** state) +{ + GSList* actual = NULL; + GSList* expected = NULL; + + // Basic match, case sensitive + expected = g_slist_append(expected, GINT_TO_POINTER(6)); + actual = get_mentions(FALSE, TRUE, "hello boothj5", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Case insensitive match + expected = g_slist_append(expected, GINT_TO_POINTER(6)); + actual = get_mentions(FALSE, FALSE, "hello BOOTHJ5", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Whole word match + expected = g_slist_append(expected, GINT_TO_POINTER(0)); + actual = get_mentions(TRUE, TRUE, "boothj5 hello", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Whole word no match (partial) + actual = get_mentions(TRUE, TRUE, "boothj5hello", "boothj5"); + assert_true(_lists_equal(actual, expected)); // expected is NULL + g_slist_free(actual); actual = NULL; + + // Multiple matches + expected = g_slist_append(expected, GINT_TO_POINTER(0)); + expected = g_slist_append(expected, GINT_TO_POINTER(14)); + actual = get_mentions(FALSE, TRUE, "boothj5 hello boothj5", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Nick with punctuation (whole word) + expected = g_slist_append(expected, GINT_TO_POINTER(0)); + actual = get_mentions(TRUE, TRUE, "boothj5: hi", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Nick surrounded by punctuation + expected = g_slist_append(expected, GINT_TO_POINTER(1)); + actual = get_mentions(TRUE, TRUE, "(boothj5)", "boothj5"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; + + // Empty message + actual = get_mentions(FALSE, TRUE, "", "boothj5"); + assert_true(_lists_equal(actual, expected)); // expected is NULL + g_slist_free(actual); actual = NULL; + + // UTF-8 characters + expected = g_slist_append(expected, GINT_TO_POINTER(0)); + actual = get_mentions(TRUE, TRUE, "我能 hello", "我能"); + assert_true(_lists_equal(actual, expected)); + g_slist_free(actual); actual = NULL; + g_slist_free(expected); expected = NULL; +} + void prof_whole_occurrences_tests(void** state) { diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index e66f1121..04c9453c 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -43,3 +43,4 @@ void test_strtoi_range_null_empty_input(void** state); void test_strtoi_range_null_err_msg(void** state); void test_string_matches_one_of_edge_cases(void** state); void test_valid_tls_policy_option(void** state); +void get_mentions_tests(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 35253e3a..1b054eeb 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -634,6 +634,7 @@ main(int argc, char* argv[]) cmocka_unit_test(prof_partial_occurrences_tests), cmocka_unit_test(prof_whole_occurrences_tests), cmocka_unit_test(prof_occurrences_of_large_message_tests), + cmocka_unit_test(get_mentions_tests), cmocka_unit_test_setup_teardown(returns_no_commands, load_preferences, From ea7a59808a46bc54a85c43f8e2a59b9def428036 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 25 Feb 2026 11:41:30 +0100 Subject: [PATCH 8/9] refactor: optimize prof_occurrences() Optimize prof_occurrences by using pointer arithmetic and g_utf8_next_char instead of calls to g_utf8_offset_to_pointer. Improve list construction efficiency in prof_occurrences by using g_slist_prepend and g_slist_reverse instead of appends. Also fix a slight oversight that could lead to trouble: When empty search strings (e.g. empty nicks) are passed it caused matches at every position. Update the test case to test for empty nicks. --- src/common.c | 32 ++++++++++++++++++++++---------- tests/unittests/test_common.c | 5 +++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/common.c b/src/common.c index d5acb211..96f5cac6 100644 --- a/src/common.c +++ b/src/common.c @@ -578,36 +578,48 @@ is_notify_enabled(void) GSList* prof_occurrences(const char* const needle, const char* const haystack, int offset, gboolean whole_word, GSList** result) { - if (needle == NULL || haystack == NULL) { + if (needle == NULL || haystack == NULL || *needle == '\0') { return *result; } - do { - gchar* haystack_curr = g_utf8_offset_to_pointer(haystack, offset); - if (g_str_has_prefix(haystack_curr, needle)) { + size_t needle_len = strlen(needle); + gchar* p = g_utf8_offset_to_pointer(haystack, offset); + GSList* matches = NULL; + + while (p) { + if (g_str_has_prefix(p, needle)) { if (whole_word) { gunichar before = 0; - gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, haystack_curr); + gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, p); if (haystack_before_ch) { before = g_utf8_get_char(haystack_before_ch); } gunichar after = 0; - gchar* haystack_after_ch = haystack_curr + strlen(needle); - if (haystack_after_ch[0] != '\0') { + gchar* haystack_after_ch = p + needle_len; + if (*haystack_after_ch != '\0') { after = g_utf8_get_char(haystack_after_ch); } if (!g_unichar_isalnum(before) && !g_unichar_isalnum(after)) { - *result = g_slist_append(*result, GINT_TO_POINTER(offset)); + matches = g_slist_prepend(matches, GINT_TO_POINTER(offset)); } } else { - *result = g_slist_append(*result, GINT_TO_POINTER(offset)); + matches = g_slist_prepend(matches, GINT_TO_POINTER(offset)); } } + if (*p == '\0') { + break; + } + + p = g_utf8_next_char(p); offset++; - } while (g_strcmp0(g_utf8_offset_to_pointer(haystack, offset), "\0") != 0); + } + + if (matches) { + *result = g_slist_concat(*result, g_slist_reverse(matches)); + } return *result; } diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index 6e96ef18..40912679 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -923,6 +923,11 @@ get_mentions_tests(void** state) assert_true(_lists_equal(actual, expected)); // expected is NULL g_slist_free(actual); actual = NULL; + // Empty nick + actual = get_mentions(FALSE, TRUE, "hello", ""); + assert_true(_lists_equal(actual, expected)); // expected is NULL + g_slist_free(actual); actual = NULL; + // UTF-8 characters expected = g_slist_append(expected, GINT_TO_POINTER(0)); actual = get_mentions(TRUE, TRUE, "我能 hello", "我能"); From 549f28fa0d2b366b791077998edffc167977e8cb Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 25 Feb 2026 12:07:46 +0100 Subject: [PATCH 9/9] tests: Add test for release_is_new() Also refactor the function so we pass it the current version and don't rely on the define inside of it. This function only allows version format in the style of "x.y.z". --- src/common.c | 8 ++++++-- src/common.h | 2 +- src/ui/console.c | 2 +- tests/unittests/test_common.c | 36 +++++++++++++++++++++++++++++++++++ tests/unittests/test_common.h | 1 + tests/unittests/unittests.c | 1 + 6 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/common.c b/src/common.c index 96f5cac6..52aa0ee6 100644 --- a/src/common.c +++ b/src/common.c @@ -471,11 +471,15 @@ release_get_latest(void) } gboolean -release_is_new(char* found_version) +release_is_new(const char* const curr_version, const char* const found_version) { + if (!curr_version || !found_version) { + return FALSE; + } + int curr_maj, curr_min, curr_patch, found_maj, found_min, found_patch; - int parse_curr = sscanf(PACKAGE_VERSION, "%d.%d.%d", &curr_maj, &curr_min, + int parse_curr = sscanf(curr_version, "%d.%d.%d", &curr_maj, &curr_min, &curr_patch); int parse_found = sscanf(found_version, "%d.%d.%d", &found_maj, &found_min, &found_patch); diff --git a/src/common.h b/src/common.h index d7c7baa9..6d4c0007 100644 --- a/src/common.h +++ b/src/common.h @@ -193,7 +193,7 @@ gboolean string_matches_one_of(const char* what, const char* is, gboolean is_can gboolean valid_tls_policy_option(const char* is); char* release_get_latest(void); -gboolean release_is_new(char* found_version); +gboolean release_is_new(const char* const curr_version, const char* const found_version); char* strip_arg_quotes(const char* const input); gboolean is_notify_enabled(void); diff --git a/src/ui/console.c b/src/ui/console.c index 9302b9d3..bea715ac 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -395,7 +395,7 @@ cons_check_version(gboolean not_available_msg) gboolean relase_valid = g_regex_match_simple("^\\d+\\.\\d+\\.\\d+$", latest_release, 0, 0); if (relase_valid) { - if (release_is_new(latest_release)) { + if (release_is_new(PACKAGE_VERSION, latest_release)) { win_println(console, THEME_DEFAULT, "-", "A new version of Profanity is available: %s", latest_release); win_println(console, THEME_DEFAULT, "-", "Check for details."); win_println(console, THEME_DEFAULT, "-", ""); diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index 40912679..8926e57f 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -936,6 +936,42 @@ get_mentions_tests(void** state) g_slist_free(expected); expected = NULL; } +void +release_is_new_tests(void** state) +{ + // Higher major version + assert_true(release_is_new("0.16.0", "1.0.0")); + // Higher minor version + assert_true(release_is_new("0.16.0", "0.17.0")); + // Higher patch version + assert_true(release_is_new("0.16.0", "0.16.1")); + + // Same version + assert_false(release_is_new("0.16.0", "0.16.0")); + + // Lower major version + assert_false(release_is_new("0.16.0", "0.15.9")); + // Lower minor version + assert_false(release_is_new("0.16.0", "0.15.0")); + // Lower patch version + assert_false(release_is_new("0.16.1", "0.16.0")); + + // Higher version but with different current versions + assert_true(release_is_new("1.2.3", "1.2.4")); + assert_true(release_is_new("1.2.3", "1.3.0")); + assert_true(release_is_new("1.2.3", "2.0.0")); + + // Malformed version strings + assert_false(release_is_new("0.16.0", "0.16")); // Missing patch in found + assert_false(release_is_new("0.16", "0.16.0")); // Missing patch in curr + assert_false(release_is_new("0.16.0", "0.16.0.1")); // Extra part + + assert_false(release_is_new("0.16.0", "abc.def.ghi")); + assert_false(release_is_new("0.16.0", "")); + assert_false(release_is_new(NULL, "1.0.0")); + assert_false(release_is_new("1.0.0", NULL)); +} + void prof_whole_occurrences_tests(void** state) { diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index 04c9453c..6229d54a 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -44,3 +44,4 @@ void test_strtoi_range_null_err_msg(void** state); void test_string_matches_one_of_edge_cases(void** state); void test_valid_tls_policy_option(void** state); void get_mentions_tests(void** state); +void release_is_new_tests(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 1b054eeb..8bf25b9b 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -635,6 +635,7 @@ main(int argc, char* argv[]) cmocka_unit_test(prof_whole_occurrences_tests), cmocka_unit_test(prof_occurrences_of_large_message_tests), cmocka_unit_test(get_mentions_tests), + cmocka_unit_test(release_is_new_tests), cmocka_unit_test_setup_teardown(returns_no_commands, load_preferences,