From 6d908aa702ad59971cd25420858e22a680d9ef6e Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Mon, 29 Sep 2025 14:32:02 -0600 Subject: [PATCH 01/20] `mlib_check` supports an explanatory string with all assertions --- src/common/src/mlib/test.h | 142 +++++++++++++++++++++++++---------- src/common/tests/test-mlib.c | 3 + 2 files changed, 104 insertions(+), 41 deletions(-) diff --git a/src/common/src/mlib/test.h b/src/common/src/mlib/test.h index 4527b2f3e91..c0f937fade9 100644 --- a/src/common/src/mlib/test.h +++ b/src/common/src/mlib/test.h @@ -129,56 +129,98 @@ typedef struct mlib_source_location { #define mlib_check(...) MLIB_ARGC_PICK(_mlib_check, #__VA_ARGS__, __VA_ARGS__) // One arg: #define _mlib_check_argc_2(ArgString, Condition) \ - _mlibCheckConditionSimple(Condition, ArgString, mlib_this_source_location()) + _mlibCheckConditionSimple(Condition, ArgString, NULL, mlib_this_source_location()) // Three args: #define _mlib_check_argc_4(ArgString, A, Operator, B) \ - MLIB_NOTHING(#A, #B) MLIB_PASTE(_mlibCheckCondition_, Operator)(A, B) + MLIB_NOTHING(#A, #B) MLIB_PASTE(_mlibCheckCondition_, Operator)(A, B, NULL) +// Five args: +#define _mlib_check_argc_6(ArgString, A, Operator, B, Infix, Reason) \ + MLIB_NOTHING(#A, #B) MLIB_PASTE(_mlib_check_with_suffix_, Infix)(A, Operator, B, Reason) +#define _mlib_check_with_suffix_because(A, Operator, B, Reason) \ + MLIB_NOTHING(#A, #B) MLIB_PASTE(_mlibCheckCondition_, Operator)(A, B, Reason) // String-compare: -#define _mlibCheckCondition_str_eq(A, B) _mlibCheckStrEq(A, B, #A, #B, mlib_this_source_location()) +#define _mlibCheckCondition_str_eq(A, B, Reason) _mlibCheckStrEq(A, B, #A, #B, Reason, mlib_this_source_location()) // Pointer-compare: -#define _mlibCheckCondition_ptr_eq(A, B) _mlibCheckPtrEq(A, B, #A, #B, mlib_this_source_location()) +#define _mlibCheckCondition_ptr_eq(A, B, Reason) _mlibCheckPtrEq(A, B, #A, #B, Reason, mlib_this_source_location()) // Integer-equal: -#define _mlibCheckCondition_eq(A, B) \ - _mlibCheckIntCmp( \ - mlib_equal, true, "==", mlib_upsize_integer(A), mlib_upsize_integer(B), #A, #B, mlib_this_source_location()) +#define _mlibCheckCondition_eq(A, B, Reason) \ + _mlibCheckIntCmp(mlib_equal, \ + true, \ + "==", \ + mlib_upsize_integer(A), \ + mlib_upsize_integer(B), \ + #A, \ + #B, \ + Reason, \ + mlib_this_source_location()) // Integer not-equal: -#define _mlibCheckCondition_neq(A, B) \ - _mlibCheckIntCmp( \ - mlib_equal, false, "≠", mlib_upsize_integer(A), mlib_upsize_integer(B), #A, #B, mlib_this_source_location()) -// Simple assertion with an explanatory string -#define _mlibCheckCondition_because(Cond, Msg) _mlibCheckConditionBecause(Cond, #Cond, Msg, mlib_this_source_location()) +#define _mlibCheckCondition_neq(A, B, Reason) \ + _mlibCheckIntCmp(mlib_equal, \ + false, \ + "!=", \ + mlib_upsize_integer(A), \ + mlib_upsize_integer(B), \ + #A, \ + #B, \ + Reason, \ + mlib_this_source_location()) // Integer comparisons: -#define _mlibCheckCondition_lt(A, B) \ - _mlibCheckIntCmp( \ - mlib_less, true, "<", mlib_upsize_integer(A), mlib_upsize_integer(B), #A, #B, mlib_this_source_location()) -#define _mlibCheckCondition_lte(A, B) \ - _mlibCheckIntCmp( \ - mlib_greater, false, "≤", mlib_upsize_integer(A), mlib_upsize_integer(B), #A, #B, mlib_this_source_location()) -#define _mlibCheckCondition_gt(A, B) \ - _mlibCheckIntCmp( \ - mlib_greater, true, ">", mlib_upsize_integer(A), mlib_upsize_integer(B), #A, #B, mlib_this_source_location()) -#define _mlibCheckCondition_gte(A, B) \ - _mlibCheckIntCmp( \ - mlib_less, false, "≥", mlib_upsize_integer(A), mlib_upsize_integer(B), #A, #B, mlib_this_source_location()) +#define _mlibCheckCondition_lt(A, B, Reason) \ + _mlibCheckIntCmp(mlib_less, \ + true, \ + "<", \ + mlib_upsize_integer(A), \ + mlib_upsize_integer(B), \ + #A, \ + #B, \ + Reason, \ + mlib_this_source_location()) +#define _mlibCheckCondition_lte(A, B, Reason) \ + _mlibCheckIntCmp(mlib_greater, \ + false, \ + "≤", \ + mlib_upsize_integer(A), \ + mlib_upsize_integer(B), \ + #A, \ + #B, \ + Reason, \ + mlib_this_source_location()) +#define _mlibCheckCondition_gt(A, B, Reason) \ + _mlibCheckIntCmp(mlib_greater, \ + true, \ + ">", \ + mlib_upsize_integer(A), \ + mlib_upsize_integer(B), \ + #A, \ + #B, \ + Reason, \ + mlib_this_source_location()) +#define _mlibCheckCondition_gte(A, B, Reason) \ + _mlibCheckIntCmp(mlib_less, \ + false, \ + "≥", \ + mlib_upsize_integer(A), \ + mlib_upsize_integer(B), \ + #A, \ + #B, \ + Reason, \ + mlib_this_source_location()) + +// Simple assertion with an explanatory string +#define _mlibCheckCondition_because(Cond, Reason, _null) \ + _mlibCheckConditionSimple(Cond, #Cond, Reason, mlib_this_source_location()) /// Check evaluator when given a single boolean static inline void -_mlibCheckConditionSimple(bool c, const char *expr, struct mlib_source_location here) +_mlibCheckConditionSimple(bool c, const char *expr, const char *reason, struct mlib_source_location here) { if (!c) { - fprintf(stderr, "%s:%d: in [%s]: Check condition ⟨%s⟩ failed\n", here.file, here.lineno, here.func, expr); - fflush(stderr); - abort(); - } -} - -static inline void -_mlibCheckConditionBecause(bool cond, const char *expr, const char *reason, mlib_source_location here) -{ - if (!cond) { - fprintf( - stderr, "%s:%d: in [%s]: Check condition ⟨%s⟩ failed (%s)\n", here.file, here.lineno, here.func, expr, reason); + fprintf(stderr, "%s:%d: in [%s]: Check condition ⟨%s⟩ failed", here.file, here.lineno, here.func, expr); + if (reason) { + fprintf(stderr, " (%s)", reason); + } + fprintf(stderr, "\n"); fflush(stderr); abort(); } @@ -193,6 +235,7 @@ _mlibCheckIntCmp(enum mlib_cmp_result cres, // The cmp result to check struct mlib_upsized_integer right, const char *left_expr, const char *right_expr, + const char *reason, struct mlib_source_location here) { if (((mlib_cmp)(left, right, 0) == cres) != cond) { @@ -218,6 +261,9 @@ _mlibCheckIntCmp(enum mlib_cmp_result cres, // The cmp result to check fprintf(stderr, "%llu", (unsigned long long)right.bits.as_unsigned); } fprintf(stderr, " ⟨%s⟩\n", right_expr); + if (reason) { + fprintf(stderr, "Because: %s\n", reason); + } fflush(stderr); abort(); } @@ -225,8 +271,12 @@ _mlibCheckIntCmp(enum mlib_cmp_result cres, // The cmp result to check // Pointer-comparison static inline void -_mlibCheckPtrEq( - const void *left, const void *right, const char *left_expr, const char *right_expr, struct mlib_source_location here) +_mlibCheckPtrEq(const void *left, + const void *right, + const char *left_expr, + const char *right_expr, + const char *reason, + struct mlib_source_location here) { if (left != right) { fprintf(stderr, @@ -243,6 +293,9 @@ _mlibCheckPtrEq( left_expr, right, right_expr); + if (reason) { + fprintf(stderr, "Because: %s\n", reason); + } fflush(stderr); abort(); } @@ -250,8 +303,12 @@ _mlibCheckPtrEq( // String-comparison static inline void -_mlibCheckStrEq( - const char *left, const char *right, const char *left_expr, const char *right_expr, struct mlib_source_location here) +_mlibCheckStrEq(const char *left, + const char *right, + const char *left_expr, + const char *right_expr, + const char *reason, + struct mlib_source_location here) { if (strcmp(left, right)) { fprintf(stderr, @@ -268,6 +325,9 @@ _mlibCheckStrEq( left_expr, right, right_expr); + if (reason) { + fprintf(stderr, "Because: %s\n", reason); + } fflush(stderr); abort(); } diff --git a/src/common/tests/test-mlib.c b/src/common/tests/test-mlib.c index 12405c2dbb4..cd71cae3483 100644 --- a/src/common/tests/test-mlib.c +++ b/src/common/tests/test-mlib.c @@ -75,6 +75,9 @@ _test_checks(void) mlib_assert_aborts () { mlib_check(3, gte, 5); } + + // An infix with a reason string + mlib_check(1, eq, 1, because, "1 = 1"); } static void From d636e33d0e638c92c74c5da14bf054eeabf1109d Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Mon, 29 Sep 2025 14:38:59 -0600 Subject: [PATCH 02/20] Define a mutable string type --- src/common/src/mlib/str.h | 349 +++++++++++++++++++++++++++++++++++ src/common/tests/test-mlib.c | 85 +++++++++ 2 files changed, 434 insertions(+) diff --git a/src/common/src/mlib/str.h b/src/common/src/mlib/str.h index 8f77fffe94f..70af68c950e 100644 --- a/src/common/src/mlib/str.h +++ b/src/common/src/mlib/str.h @@ -565,4 +565,353 @@ mstr_contains_any_of(mstr_view str, mstr_view needle) } #define mstr_contains_any_of(Str, Needle) mstr_contains_any_of(mstr_view_from(Str), mstr_view_from(Needle)) + +/** + * @brief A simple mutable string type, with a guaranteed null terminator. + * + * This type is a trivially relocatable aggregate type that contains a pointer `data` + * and a size `len`. If not null, the pointer `data` points to an array of mutable + * `char` of length `len + 1`, where the character at `data[len]` is always zero, + * and must not be modified. + */ +typedef struct mstr { + /** + * @brief Pointer to the first char in the string, or NULL if + * the string is null. + * + * The pointed-to character array has a length of `len + 1`, where + * the character at `data[len]` is always null. + * + * @warning Attempting to overwrite the null character at `data[len]` + * will result in undefined behavior! + * + * @note An empty string is not equivalent to a null string! An empty string + * will still point to an array of length 1, where the only char is the null + * terminator. + */ + char *data; + /** + * @brief The number of characters in the array pointed-to by `data` + * that preceed the null terminator. + */ + size_t len; +} mstr; + + +/** + * @brief Resize an existing or null `mstr`, without initializing any of the + * added content other than the null terminator. This operation is potientially + * UNSAFE, because it gives uninitialized memory to the caller. + * + * @param str Pointer to a valid `mstr`, or a null `mstr`. + * @param new_len The new length of the string. + * @return true If the operation succeeds + * @return false Otherwise + * + * If `str` is a null string, this function will initialize a new `mstr` object + * on-the-fly. + * + * If the operation increases the length of the string (or initializes a new string), + * then the new `char` in `str.data[str.len : new_len] will contain uninitialized + * values. The char at `str.data[new_len]` WILL be set to zero, to ensure there + * is a null terminator. The caller should always initialize the new string + * content to ensure that the string has a specified value. + */ +static inline bool +mstr_resize_for_overwrite(mstr *const str, const size_t new_len) +{ + // We need to allocate one additional char to hold the null terminator + size_t alloc_size = new_len; + if (mlib_add(&alloc_size, 1) || alloc_size > SSIZE_MAX) { + // Allocation size is too large + return false; + } + // Try to (re)allocate the region + char *data = (char *)realloc(str->data, alloc_size); + if (!data) { + // Failed to (re)allocate + return false; + } + // Note: We do not initialize any of the data in the newly allocated region. + // We only set the null terminator. It is up to the caller to do the rest of + // the init. + data[new_len] = 0; + // Update the final object + str->data = data; + str->len = new_len; + // Success + return true; +} + +/** + * @brief Given an existing `mstr`, resize it to hold `new_len` chars + * + * @param str Pointer to a string object to update, or a null `mstr` + * @param new_len The new length of the string, not including the implicit null terminator + * @return true If the operation succeeds + * @return false Otherwise + * + * @note If the operation fails, then `*str` is not modified. + */ +static inline bool +mstr_resize(mstr *str, size_t new_len) +{ + const size_t old_len = str->len; + if (!mstr_resize_for_overwrite(str, new_len)) { + // Failed to allocate new storage for the string + return false; + } + // Check how many chars we added/removed + const ptrdiff_t len_diff = new_len - str->len; + if (len_diff > 0) { + // We added new chars. Zero-init all the new chars + memset(str->data + old_len, 0, (size_t)len_diff); + } + // Success + return true; +} + +/** + * @brief Create a new `mstr` of the given length + * + * @param new_len The length of the new string, in characters, not including the null terminator + * @return mstr A new string. The string's `data` member is NULL in case of failure + * + * The character array allocated for the string will always be `new_len + 1` `char` in length, + * where the char at the index `new_len` is a null terminator. This means that a string of + * length zero will allocate a single character to store the null terminator. + * + * All characters in the new string are initialize to zero. If you want uninitialized + * string content, use `mstr_resize_for_overwrite`. + */ +static inline mstr +mstr_new(size_t new_len) +{ + mstr ret = {NULL, 0}; + // We can rely on `resize` to handle the null state properly. + mstr_resize(&ret, new_len); + return ret; +} + +/** + * @brief Delete an `mstr` that was created with an allocating API, including + * the resize APIs + * + * @param s An `mstr` object. If the object is null, this function is a no-op. + * + * After this call, the value of the `s` object has been consumed and is invalid. + */ +static inline void +mstr_delete(mstr s) +{ + free(s.data); +} + +/** + * @brief Replace the content of the given string, attempting to reuse the buffer + * + * @param inout Pointer to a valid or null `mstr` to be replaced + * @param s The new string contents + * @return true If the operation succeeded + * @return false Otherwise + * + * If the operation fails, `*inout` is not modified + */ +static inline bool +mstr_assign(mstr *inout, mstr_view s) +{ + if (!mstr_resize_for_overwrite(inout, s.len)) { + return false; + } + memcpy(inout->data, s.data, s.len); + return true; +} + +#define mstr_assign(InOut, S) mstr_assign((InOut), mstr_view_from((S))) + +/** + * @brief Create a mutable copy of the given string. + * + * @param sv The string to be copied + * @return mstr A new valid string, or a null string in case of allocation failure. + */ +static inline mstr +mstr_copy(mstr_view sv) +{ + mstr ret = {NULL, 0}; + mstr_assign(&ret, sv); + return ret; +} + +#define mstr_copy(S) mstr_copy(mstr_view_from((S))) +#define mstr_copy_cstring(S) mstr_copy(mstr_cstring((S))) + +/** + * @brief Concatenate two strings into a new mutable string + * + * @param a The left-hand string to be concatenated + * @param b The right-hand string to be concatenated + * @return mstr A new valid string composed by concatenating `a` with `b`, or + * a null string in case of allocation failure. + */ +static inline mstr +mstr_concat(mstr_view a, mstr_view b) +{ + mstr ret = {NULL, 0}; + size_t cat_len; + if (mlib_add(&cat_len, a.len, b.len)) { + // Size would overflow. No go. + return ret; + } + // Prepare the new string + if (!mstr_resize_for_overwrite(&ret, cat_len)) { + // Failed to allocate. The ret string is still null, and we can just return it + return ret; + } + // Copy in the characters from `a` + char *out = ret.data; + memcpy(out, a.data, a.len); + // Copy in the characters from `b` + out += a.len; + memcpy(out, b.data, b.len); + // Success + return ret; +} + +#define mstr_concat(A, B) mstr_concat(mstr_view_from((A)), mstr_view_from((B))) + +/** + * @brief Delete and/or insert characters into a string + * + * @param str The string object to be updated + * @param splice_pos The position at which to do the splice + * @param n_delete The number of characters to delete at `splice_pos` + * @param insert A string to be inserted at `split_pos` after chars are deleted + * @return true If the operation succeeds + * @return false Otherwise + * + * If `n_delete` is zero, then no characters are deleted. If `insert` is empty + * or null, then no characters are inserted. + */ +static inline bool +mstr_splice(mstr *str, size_t splice_pos, size_t n_delete, mstr_view insert) +{ + mlib_check(splice_pos <= str->len); + // How many chars is it possible to delete from `splice_pos`? + size_t n_chars_avail_to_delete = str->len - splice_pos; + mlib_check(n_delete <= n_chars_avail_to_delete); + // Compute the new string length + size_t new_len = str->len; + // This should never fail, because we should try to delete more chars than we have + mlib_check(!mlib_sub(&new_len, n_delete)); + // Check if appending would make too big of a string + if (mlib_add(&new_len, insert.len)) { + // New string will be too long + return false; + } + char *mut = str->data; + // We either resize first or resize last, depending on where we are shifting chars + if (new_len > str->len) { + // Do the resize first + if (!mstr_resize_for_overwrite(str, new_len)) { + // Failed to allocate + return false; + } + mut = str->data; + } + // Move to the splice position + mut += splice_pos; + // Shift the existing string parts around for the deletion operation + const size_t tail_len = n_chars_avail_to_delete - n_delete; + // Adjust to the begining of the string part that we want to keep + char *copy_from = mut + n_delete; + char *copy_to = mut + insert.len; + memmove(copy_to, copy_from, tail_len); + if (new_len < str->len) { + // We didn't resize first, so resize now. We are shrinking the string, so this + // will never fail, and does not create any uninitialized memory: + mlib_check(mstr_resize_for_overwrite(str, new_len)); + mut = str->data + splice_pos; + } + // Insert the new data + memcpy(mut, insert.data, insert.len); + return true; +} + +/** + * @brief Append a string to the end of some other string. + * + * @param str The string to be modified + * @param suffix The suffix string to be appended onto `*str` + * @return true If the operation was successful + * @return false Otherwise + * + * If case of failure, `*str` is not modified. + */ +static inline bool +mstr_append(mstr *str, mstr_view suffix) +{ + return mstr_splice(str, str->len, 0, suffix); +} + +#define mstr_append(Into, Suffix) mstr_append((Into), mstr_view_from((Suffix))) + +/** + * @brief Append a single character to the given string object + * + * @param str The string object to be updated + * @param c The single character that will be inserted at the end + * @return true If the operation succeeded + * @return false Otherwise + * + * In case of failure, the string is not modified. + */ +static inline bool +mstr_pushchar(mstr *str, char c) +{ + mstr_view one = mstr_view_data(&c, 1); + return mstr_append(str, one); +} + +/** + * @brief Replace every occurrence of `needle` in `str` with `sub` + * + * @param str The string object to be updated + * @param needle The non-empty needle string to be searched for.s + * @param sub The string to be inserted in place of each `needle` + * @return true If the operation succeeds + * @return false Otherwise + * + * @warning If the `needle` string is empty, then the program will terminate! + * @note If the operation fails, the content of `str` is an unspecified but valid + * string. + */ +static inline bool +mstr_replace(mstr *str, mstr_view needle, mstr_view sub) +{ + mlib_check(needle.len, neq, 0, because, "Trying to replace an empty string will result in an infinite loop"); + // Scan forward, starting from the first position: + size_t off = 0; + while (1) { + // Find the next occurrence, starting from the scan offset + off = mstr_find(*str, needle, off); + if (off == SIZE_MAX) { + // No more occurrences. + return true; + } + // Replace the needle string with the new value + if (!mstr_splice(str, off, needle.len, sub)) { + return false; + } + // Advance over the length of the replacement string, so we don't try to + // infinitely replace content if the replacement itself contains the needle + // string + if (mlib_add(&off, sub.len)) { + // Integer overflow while advancing the offset. No good. + return false; + } + } +} + + #endif // MLIB_STR_H_INCLUDED diff --git a/src/common/tests/test-mlib.c b/src/common/tests/test-mlib.c index cd71cae3483..86822c8578d 100644 --- a/src/common/tests/test-mlib.c +++ b/src/common/tests/test-mlib.c @@ -922,6 +922,90 @@ _test_str_view(void) } } +static inline void +_test_str(void) +{ + // Simple empty + { + mstr s = mstr_new(0); + // Length is zero + mlib_check(s.len, eq, 0); + // Data is not null for empty strings, since we want a null terminator + mlib_check(s.data != NULL); + // The null terminator is present: + mlib_check(s.data[0], eq, 0); + mstr_delete(s); + } + + // Simple copy of a C string + { + mstr s = mstr_copy_cstring("foo bar"); + mlib_check(s.len, eq, 7); + mlib_check(mstr_cmp(s, ==, mstr_cstring("foo bar"))); + mstr_delete(s); + } + + // Concat two strings + { + mstr s = mstr_concat(mstr_cstring("foo"), mstr_cstring("bar")); + mlib_check(mstr_cmp(s, ==, mstr_cstring("foobar"))); + mstr_delete(s); + } + + // Append individual characters + { + mstr s = mstr_new(0); + mstr_pushchar(&s, 'f'); + mstr_pushchar(&s, 'o'); + mstr_pushchar(&s, 'o'); + mlib_check(mstr_cmp(s, ==, mstr_cstring("foo"))); + mstr_delete(s); + } + + // Splice deletion + { + mstr s = mstr_copy_cstring("foo bar baz"); + mlib_check(mstr_splice(&s, 4, 3, mstr_cstring(""))); + mlib_check(mstr_cmp(s, ==, mstr_cstring("foo baz"))); + + mstr_assign(&s, mstr_cstring("foo bar baz")); + mlib_check(mstr_splice(&s, 4, 3, mstr_cstring("quux"))); + mlib_check(mstr_cmp(s, ==, mstr_cstring("foo quux baz"))); + + mstr_assign(&s, mstr_cstring("foo bar baz")); + mlib_check(mstr_splice(&s, 4, 0, mstr_cstring("quux "))); + mlib_check(mstr_cmp(s, ==, mstr_cstring("foo quux bar baz"))); + + mstr_delete(s); + } + + // Replacing + { + mstr s = mstr_copy_cstring("abcd abcd"); + mlib_check(mstr_replace(&s, mstr_cstring("b"), mstr_cstring("foo"))); + mlib_check(mstr_cmp(s, ==, mstr_cstring("afoocd afoocd"))); + + // Try to replace where the replacement contains the needle + mstr_assign(&s, mstr_cstring("foo bar baz")); + mlib_check(mstr_replace(&s, mstr_cstring("bar"), mstr_cstring("foo bar baz"))); + // A naive impl would explode into an infinite string, but we don't try to replace + // within the already-replaced content: + mlib_check(s.data, str_eq, "foo foo bar baz baz"); + + // Try to replace, where the needle is an empty string. This just produces a repetition of the needle + mstr_assign(&s, mstr_cstring("foo")); + mlib_assert_aborts () { + // A naive replacement of an empty string will result in an infinite string + // as it keeps matching the empty string forever, so we terminate rather than + // allocate forever: + mlib_check(mstr_replace(&s, mstr_cstring(""), mstr_cstring("a"))); + } + + mstr_delete(s); + } +} + + static void _test_duration(void) { @@ -1129,6 +1213,7 @@ test_mlib_install(TestSuite *suite) TestSuite_Add(suite, "/mlib/check-cast", _test_cast); TestSuite_Add(suite, "/mlib/ckdint-partial", _test_ckdint_partial); TestSuite_Add(suite, "/mlib/str_view", _test_str_view); + TestSuite_Add(suite, "/mlib/str", _test_str); TestSuite_Add(suite, "/mlib/duration", _test_duration); TestSuite_Add(suite, "/mlib/time_point", _test_time_point); TestSuite_Add(suite, "/mlib/sleep", _test_sleep); From 9675ae5bdcc8baabd5fbdbb23f5d82c0f3ed549d Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Mon, 29 Sep 2025 15:52:03 -0600 Subject: [PATCH 03/20] Add a "vector" template header --- src/common/src/mlib/vec.t.h | 432 +++++++++++++++++++++++++++++++++++ src/common/tests/test-mlib.c | 84 +++++++ 2 files changed, 516 insertions(+) create mode 100644 src/common/src/mlib/vec.t.h diff --git a/src/common/src/mlib/vec.t.h b/src/common/src/mlib/vec.t.h new file mode 100644 index 00000000000..50b5774919b --- /dev/null +++ b/src/common/src/mlib/vec.t.h @@ -0,0 +1,432 @@ +/** + * @file vec.t.h + * @brief Declare a new vector container data type + * @date 2024-10-02 + * + * To use this file: + * + * - #define a type `T` immediately before including this file. + * - Optional: Define an identifier `VecName` to the name of the vector. If unset, declares `_vec` + * - Optional: Define a `VecDestroyElement(Ptr)` macro to specify how the vector + * should destroy the element at `*Ptr`. If unset, destroying is a no-op. + * - Optional: Define `VecInitElement(Ptr, ...)` which initializes a new element. + * The first macro argument is a pointer to the element and subsequent arguments + * are unspecified and reserved for future use. Elements are zero-initialized + * before being passed to this macro. + * - Optional: Define `VecCopyElement(DstPtr, SrcPtr)` to copy data from `*SrcPtr` + * to `*DstPtr`. The vector's copying function is only defined if this macro + * is defined. This macro MUST evaluate to a boolean to indicate if the copy + * operation succeeded. If a copy fails, then the partially copied elements + * will be destroyed and the overall copy will fail. + * + * To add a trival copying function, define `VecCopyElement` to + * `VecTrivialCopyElement`. + * + * - NOTE: All of the above macros will be automatically undef'd after this file + * is included. + * + * Types stored in the vector must be trivially relocatable. + * + * @copyright Copyright 2009-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include + +#include // assert +#include // bool +#include // size_t +#include // calloc, realloc, free +#include // memcpy, memset + +// Check that the caller provided a `T` macro to be the element type +#ifndef T +#if defined(__clangd__) || defined(__INTELLISENSE__) +#define T int // Define a type for IDE diagnostics +#define VecCopyElement VecTrivialCopyElement // For IDE highlighting +#else +#error A type `T` should be defined before including this file +#endif +#endif + +#ifndef VecName +#define VecName MLIB_PASTE(T, _vec) +#endif + +#ifndef VecDestroyElement +#define VecDestroyElement(Ptr) ((void)(Ptr)) +#endif + +#ifndef VecInitElement +#define VecInitElement(Ptr) ((void)(Ptr)) +#endif + +#ifndef VecTrivialCopyElement +#define VecTrivialCopyElement(DstPtr, SrcPtr) ((*(DstPtr) = *(SrcPtr)), true) +#endif + +#pragma push_macro("vec_inline_spec") +#if !defined(vec_inline_spec) +#define vec_inline_spec static inline +#endif + +// The "fn" macro just adds a qualified name to the front of a function identifier +#pragma push_macro("fn") +#undef fn +#define fn(M) MLIB_PASTE_3(VecName, _, M) + +typedef struct VecName { + /** + * @private + * @brief Pointer to the first vector element, or NULL if the vector is + * empty. + * + * @note DO NOT MODIFY + */ + T *data; + /** + * @brief The number of elements in the vector. + * + * @note DO NOT MODIFY + */ + size_t size; + /** + * @brief The number of allocated storage elements. + * + * @note DO NOT MODIFY + */ + size_t capacity; + +#if mlib_is_cxx() + T * + begin() noexcept + { + return data; + } + T * + end() noexcept + { + return data + size; + } +#endif +} VecName; + +mlib_extern_c_begin(); + +/** + * @brief Obtain a pointer-to-mutable to the first element in the given vector + */ +vec_inline_spec T * +fn(begin(VecName *v)) mlib_noexcept +{ + return v->data; +} + +/** + * @brief Obtain a pointer-to-mutable past the last element in the given vector + */ +vec_inline_spec T * +fn(end(VecName *v)) mlib_noexcept +{ + return v->data + v->size; +} + +/** + * @brief Obtain a pointer-to-const to the first element in the given vector + */ +vec_inline_spec T const * +fn(cbegin(VecName const *v)) mlib_noexcept +{ + return v->data; +} + +/** + * @brief Obtain a pointer-to-const past the last element in the given vector + */ +vec_inline_spec T const * +fn(cend(VecName const *v)) mlib_noexcept +{ + return v->data + v->size; +} + +/** + * @brief Get the maximum number of elements that can be held in the vector of + * a certain type. + */ +vec_inline_spec size_t +fn(max_size(void)) mlib_noexcept +{ + // We compare against (signed) SSIZE_MAX because want to support the difference + // between two pointers. If we use the unsigned size, then we could have vectors + // with size that is too large to represent the difference between two sizes. + return SSIZE_MAX / sizeof(T); +} + +/** + * @brief Set the capacity of the given vector. + * + * @param self The vector object to be modified + * @param count The new capacity. If this is less than the current size, then + * the capacity will be capped at the size instead + * + * @retval true If-and-only-if the reallocation was successful + * @retval false If there was an error in allocating the buffer + */ +vec_inline_spec bool +fn(reserve(VecName *const self, size_t count)) mlib_noexcept +{ + // Check if this value is beyond the possible capacity of the vector + if (count > fn(max_size())) { + // Too many elements. We cannot allocate a region this large. + return false; + } + // Check if we are already at the requested capacity. + if (count == self->capacity) { + // No reallocation needed. + return true; + } + // Check if the caller is requesting a lower capacity than our current size + if (count < self->size) { + // We cannot shrink the capacity below the current size, so just shrink-to-fit + count = self->size; + } + // Impossible: We will never shrink below `self.size`, and if + // `self.size == 0` and `count == 0`, then we early-return'd above. + assert(count != 0); + // The number of bytes we need to allocate. Note that this cannot overflow + // because we guard against it by checking against `max_size()` + const size_t new_buffer_size = count * sizeof(T); + // Attempt to reallocate the region + T *const new_buffer = (T *)realloc(self->data, new_buffer_size); + if (!new_buffer) { + // Failed to reallocate a new storage region + return false; + } + // Successfully reallocated the buffer. Update our storage pointer. + self->data = new_buffer; + // Note the new capacity. + self->capacity = count; + return true; +} + +/** + * @brief Destroy elements in the vector at the specified range positions + * + * @param self The vector to be updated + * @param first Pointer to the first element to be destroyed + * @param last Pointer to the first element to NOT be destroyed + * + * Elements are destroyed and removed starting at the end. If `first == last`, + * this is a no-op. The given pointers must refer to vector elements, and `last` + * must be reachable by advancing `first` zero or more times. + */ +vec_inline_spec void +fn(erase(VecName *const self, T *const first, T *const last)) +{ + for (T *r_iter = last; r_iter != first; --r_iter) { + VecDestroyElement((r_iter - 1)); + --self->size; + } +} + +/** + * @brief Destroy a single element at the given zero-based index position + */ +vec_inline_spec void +fn(erase_at(VecName *const self, size_t pos)) +{ + fn(erase(self, fn(begin(self)) + pos, fn(end(self)))); +} + +/** + * @brief Resize the vector to hold the given number of elements + * + * Newly added elements are zero-initialized, or initailized using VecInitElement + * + * @retval true If-and-only-if the resize was successful + * @retval false If the function failed to allocate the new storage region + * + * @note Don't forget to check the return value for success! + */ +// mlib_nodiscard ("Check the returned bool to detect allocation failure") +vec_inline_spec bool +fn(resize(VecName *const self, size_t const count)) mlib_noexcept +{ + // Check if we aren't actually growing the vector. + if (count <= self->size) { + // We need to destroy elements at the tail. If `count == size`, this is a no-op. + fn(erase(self, fn(begin(self)) + count, fn(end(self)))); + return true; + } + + // We need to increase the capacity of the vector to hold the new elements + // Try to auto-grow capacity. Increase capacity by ×1.5 + const size_t half_current_capacity = self->capacity / 2; + size_t new_capacity = 0; + if (mlib_add(&new_capacity, self->size, half_current_capacity)) { + // The auto growth amount would overflow, so just cap to the max size. + new_capacity = fn(max_size()); + } + // Check if our automatic growth is big enough to hold the requested number of elements + if (new_capacity < count) { + // The automatic growth factor is actually smaller than the number of new elements + // the caller wants, so we need to increase capacity to that level instead. + new_capacity = count; + } + // Try to reserve more storage + if (!fn(reserve(self, new_capacity))) { + // We failed to reserve the new storage region. The requested capacity may be too large, + // or we may have just run out of memory. + return false; + } + + // Pointer to where the new end will be + T *const new_end = fn(begin(self)) + count; + // Create a zero-initialized object to copy over the top of each new element. + T zero; + memset(&zero, 0, sizeof zero); + // Call init() on ever new element up until the new size + for (T *iter = fn(end(self)); iter != new_end; ++iter) { + *iter = zero; + (void)(VecInitElement((iter))); + } + + // Update the stored size + self->size = count; + return true; +} + +/** + * @brief Append another element, returning a pointer to that element. + * + * @return T* A pointer to the newly added element, or NULL in case of allocation failure. + */ +// mlib_nodiscard ("Check the returned pointer for failure") +vec_inline_spec T * +fn(push(VecName *self)) mlib_noexcept +{ + size_t count = self->size; + if (mlib_add(&count, 1)) { + // Adding another element would overflow size_t. This is extremely unlikely, + // but precautionary. + return NULL; + } + if (!fn(resize(self, count))) { + // Failed to push another item + return NULL; + } + return fn(begin(self)) + count - 1; +} + +/** + * @brief Create a new empty vector + */ +vec_inline_spec VecName fn(new(void)) mlib_noexcept +{ + VecName ret = {NULL, 0, 0}; + return ret; +} + +/** + * @brief Destroy the pointed-to vector, freeing the associated data buffer. + * + * The pointed-to vector becomes valid storage for a new vector object. + */ +vec_inline_spec void +fn(destroy(VecName *self)) mlib_noexcept +{ + // Resizing to zero will destroy all elements + (void)fn(resize(self, 0)); + // Resizing won't necessarily free the data buffer. Do that now. + free(self->data); + self->capacity = 0; + self->data = NULL; +} + +/** + * @brief Create a new vector with `n` initialized elements + */ +vec_inline_spec VecName +fn(new_n(size_t n, bool *okay)) mlib_noexcept +{ + VecName ret = fn(new()); + *okay = fn(resize)(&ret, n); + return ret; +} + +#ifdef VecCopyElement +/** + * @brief Copy the data from the vector `src` into storage for a new vector `dst` + * + * @param dst_vec Pointer-to-storage for a new vector object to be initialized. + * @param src_vec Pointer to a vector whose elements will be copied into a new vector + * @retval true If-and-only-if the copy was successful. + * @retval false Otherwise + */ +vec_inline_spec bool +fn(init_copy(VecName *dst_vec, VecName const *src_vec)) mlib_noexcept +{ + VecName tmp = fn(new()); + // Try to reseve capacity for all new elements. Don't resize(), because we want + // uninitialized storage for the new data. + if (!fn(reserve(&tmp, src_vec->size))) { + // We failed to reserve capacity in the new vector + fn(destroy(&tmp)); + return false; + } + // Copy everything into the destination element-by-element + { + // Input iterator + T const *in_iter = fn(cbegin(src_vec)); + // Input stop position + T const *const in_stop = fn(cend(src_vec)); + // Output iterator + T *out_iter = tmp.data; + // Copy from the first to the last + for (; in_iter != in_stop; ++in_iter, ++out_iter) { + // Try to copy into the new element + if (!VecCopyElement((out_iter), (in_iter))) { + // Failed copying here. Undo everything by destroying the temporary + fn(destroy(&tmp)); + return false; + } + // Update the size of the temporary vec to record that it is holding the new + // element. This allows us to call `destroy()` to undo our work. + tmp.size++; + } + } + // Everything went okay. Give the temporary to the caller as the final result + *dst_vec = tmp; + return true; +} +#endif // VecCopyElement + +mlib_extern_c_end(); + +#ifndef mlib_vec_foreach +#define mlib_vec_foreach(Type, VarName, Vector) \ + for (Type *VarName = (Vector).data; VarName != (Vector).data + (Vector).size; ++VarName) +#endif + +#undef T +#undef VecName +#undef VecDestroyElement +#undef VecInitElement +#undef VecTrivialCopyElement +#ifdef VecCopyElement +#undef VecCopyElement +#endif +// These ones we want to pop, not undefine: +#pragma pop_macro("fn") +#pragma pop_macro("vec_inline_spec") diff --git a/src/common/tests/test-mlib.c b/src/common/tests/test-mlib.c index 86822c8578d..13c96d3d577 100644 --- a/src/common/tests/test-mlib.c +++ b/src/common/tests/test-mlib.c @@ -1197,6 +1197,88 @@ _test_timer(void) mlib_check(mlib_timer_is_expired(tm)); } +// Tests for `int_vec` assert the behavior of the vector type when handling trivial +// elements. +#define T int +#include +static void +_test_int_vec(void) +{ + int_vec ints = int_vec_new(); + mlib_check(ints.size, eq, 0, because, "Initial vector is empty"); + + // Append an element + int *el; + mlib_check(el = int_vec_push(&ints)); + *el = 42; + mlib_check(int_vec_begin(&ints)[0], eq, 42); + mlib_check(ints.size, eq, 1); + + int_vec_erase_at(&ints, 0); + mlib_check(ints.size, eq, 0, because, "We are back to an empty vector"); + + *int_vec_push(&ints) = 42; + *int_vec_push(&ints) = 1729; + *int_vec_push(&ints) = 123456; + *int_vec_push(&ints) = -7; + mlib_check(ints.size, eq, 4, because, "We added four elements from empty"); + // Erase in the middle + int_vec_erase(&ints, ints.data + 1, ints.data + 3); + mlib_check(ints.size, eq, 2, because, "We erased two elements"); + + int_vec_destroy(&ints); +} + +#define T char * +#define VecName cstring_vec +#define VecDestroyElement(CStrPtr) ((free(*CStrPtr), *CStrPtr = NULL)) +#define VecCopyElement(Dst, Src) ((*Dst = strdup(*Src))) +#include +static void +_test_cstring_vec(void) +{ + // Simple new and destroy + { + cstring_vec v = cstring_vec_new(); + mlib_check(v.size, eq, 0); + mlib_check(v.capacity, eq, 0); + cstring_vec_destroy(&v); + } + // Simple new and push an element + { + cstring_vec v = cstring_vec_new(); + *cstring_vec_push(&v) = strdup("Hey"); + mlib_check(v.size, eq, 1); + mlib_check(v.capacity, eq, 1); + mlib_check(cstring_vec_begin(&v)[0], str_eq, "Hey"); + cstring_vec_destroy(&v); + } + // Copy an empty + { + cstring_vec v = cstring_vec_new(); + cstring_vec b; + cstring_vec_init_copy(&b, &v); + cstring_vec_destroy(&v); + cstring_vec_destroy(&b); + } + // Copy non-empty + { + cstring_vec a = cstring_vec_new(); + *cstring_vec_push(&a) = strdup("Hello"); + *cstring_vec_push(&a) = strdup("world!"); + mlib_check(a.size, eq, 2); + mlib_check(a.capacity, eq, 2); + cstring_vec b; + mlib_check(cstring_vec_init_copy(&b, &a)); + mlib_check(a.size, eq, 2); + mlib_check(a.capacity, eq, 2); + mlib_check(cstring_vec_begin(&a)[0], str_eq, "Hello"); + mlib_check(cstring_vec_begin(&b)[1], str_eq, "world!"); + cstring_vec_destroy(&b); + cstring_vec_destroy(&a); + } +} + void test_mlib_install(TestSuite *suite) { @@ -1218,6 +1300,8 @@ test_mlib_install(TestSuite *suite) TestSuite_Add(suite, "/mlib/time_point", _test_time_point); TestSuite_Add(suite, "/mlib/sleep", _test_sleep); TestSuite_Add(suite, "/mlib/timer", _test_timer); + TestSuite_Add(suite, "/mlib/int-vector", _test_int_vec); + TestSuite_Add(suite, "/mlib/string-vector", _test_cstring_vec); } mlib_diagnostic_pop(); From 8f4ec31f390503a313d585a93711bc6216a5e1bf Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Tue, 30 Sep 2025 12:08:29 -0600 Subject: [PATCH 04/20] Wrapping indexing for vec --- src/common/src/mlib/vec.t.h | 16 ++++++++++++++++ src/common/tests/test-mlib.c | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/common/src/mlib/vec.t.h b/src/common/src/mlib/vec.t.h index 50b5774919b..793724c1000 100644 --- a/src/common/src/mlib/vec.t.h +++ b/src/common/src/mlib/vec.t.h @@ -43,6 +43,7 @@ */ #include #include +#include #include // assert #include // bool @@ -419,6 +420,21 @@ mlib_extern_c_end(); for (Type *VarName = (Vector).data; VarName != (Vector).data + (Vector).size; ++VarName) #endif +#ifndef mlib_vec_at +#define mlib_vec_at(Vec, Pos) ((Vec).data[_mlib_vec_index_adjust((Vec).size, mlib_upsize_integer(Pos))]) + +static inline size_t +_mlib_vec_index_adjust(size_t size, mlib_upsized_integer pos) +{ + if (pos.is_signed && pos.bits.as_signed < 0) { + return mlib_assert_add(size_t, size, pos.bits.as_signed); + } + mlib_check(pos.bits.as_unsigned, lte, size, because, "the vector index must be in-bounds for mlib_vec_at()"); + return pos.bits.as_unsigned; +} + +#endif + #undef T #undef VecName #undef VecDestroyElement diff --git a/src/common/tests/test-mlib.c b/src/common/tests/test-mlib.c index 13c96d3d577..191b1f5fbab 100644 --- a/src/common/tests/test-mlib.c +++ b/src/common/tests/test-mlib.c @@ -1226,6 +1226,8 @@ _test_int_vec(void) int_vec_erase(&ints, ints.data + 1, ints.data + 3); mlib_check(ints.size, eq, 2, because, "We erased two elements"); + mlib_check(mlib_vec_at(ints, -1), eq, -7, because, "Negative index wraps"); + int_vec_destroy(&ints); } From f97883e0a571e4c8a44191930df9d0b17b62fd17 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Wed, 1 Oct 2025 12:03:43 -0600 Subject: [PATCH 05/20] String functions for trimming whitespace --- src/common/src/mlib/str.h | 75 ++++++++++++++++++++++++++++++++++++ src/common/tests/test-mlib.c | 10 ++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/common/src/mlib/str.h b/src/common/src/mlib/str.h index 70af68c950e..1b53c3ad36f 100644 --- a/src/common/src/mlib/str.h +++ b/src/common/src/mlib/str.h @@ -288,6 +288,24 @@ _mstr_adjust_index(mstr_view s, mlib_upsized_integer pos, bool clamp_to_length) return pos.bits.as_unsigned; } +/** + * @brief Obtain the code unit at the given zero-based index, with negative index wrapping. + * + * This function asserts that the index is in-bounds for the given string. + * + * @param s The string to be inspected. + * @param pos The index to access. Zero is the first code unit, and -1 is the last. + * @return char The code unit at position `pos`. + */ +static inline char +mstr_at(mstr_view s, mlib_upsized_integer pos_) +{ + size_t pos = _mstr_adjust_index(s, pos_, false); + return s.data[pos]; +} + +#define mstr_at(S, Pos) (mstr_at)(mstr_view_from(S), mlib_upsize_integer(Pos)) + /** * @brief Create a new `mstr_view` that views a substring within another string * @@ -441,6 +459,63 @@ mstr_find_first_of(mstr_view hay, mstr_view const needles, mlib_upsized_integer #define _mstr_find_first_of_argc_3(Hay, Needle, Pos) _mstr_find_first_of_argc_4(Hay, Needle, Pos, SIZE_MAX) #define _mstr_find_first_of_argc_4(Hay, Needle, Pos, Len) mstr_find_first_of(Hay, Needle, mlib_upsize_integer(Pos), Len) +/** + * @brief Trim leading latin (ASCII) whitespace from the given string + * + * @param s The string to be inspected + * @return mstr_view A substring view of `s` that excludes any leading whitespace + */ +static inline mstr_view +mstr_trim_left(mstr_view s) +{ + while (s.len) { + char c = mstr_at(s, 0); + if (c == ' ' || c == '\n' || c == '\r' || c == '\t') { + s = mstr_substr(s, 1); + } else { + break; + } + } + return s; +} +#define mstr_trim_left(S) (mstr_trim_left)(mstr_view_from(S)) + +/** + * @brief Trim trailing latin (ASCII) whitespace from the given string + * + * @param s The string to be insepcted + * @return mstr_view A substring view of `s` that excludes any trailing whitespace. + */ +static inline mstr_view +mstr_trim_right(mstr_view s) +{ + while (s.len) { + char c = mstr_at(s, -1); + if (c == ' ' || c == '\n' || c == '\r' || c == '\t') { + s = mstr_slice(s, 0, -1); + } else { + break; + } + } + return s; +} +#define mstr_trim_right(S) (mstr_trim_right)(mstr_view_from(S)) + +/** + * @brief Trim leading and trailing latin (ASCII) whitespace from the string + * + * @param s The string to be inspected + * @return mstr_view A substring of `s` that excludes leading and trailing whitespace. + */ +static inline mstr_view +mstr_trim(mstr_view s) +{ + s = mstr_trim_left(s); + s = mstr_trim_right(s); + return s; +} +#define mstr_trim(S) (mstr_trim)(mstr_view_from(S)) + /** * @brief Split a single string view into two strings at the given position * diff --git a/src/common/tests/test-mlib.c b/src/common/tests/test-mlib.c index 191b1f5fbab..71526334e4d 100644 --- a/src/common/tests/test-mlib.c +++ b/src/common/tests/test-mlib.c @@ -920,6 +920,14 @@ _test_str_view(void) // But "Food" > "foo" when case-insensitive: mlib_check(mstr_latin_casecmp(mstr_cstring("Food"), >, mstr_cstring("foo"))); } + + // Trimming + { + mstr_view s = mstr_cstring(" foo bar \n"); + mlib_check(mstr_cmp(mstr_trim_left(s), ==, mstr_cstring("foo bar \n"))); + mlib_check(mstr_cmp(mstr_trim_right(s), ==, mstr_cstring(" foo bar"))); + mlib_check(mstr_cmp(mstr_trim(s), ==, mstr_cstring("foo bar"))); + } } static inline void @@ -1226,7 +1234,7 @@ _test_int_vec(void) int_vec_erase(&ints, ints.data + 1, ints.data + 3); mlib_check(ints.size, eq, 2, because, "We erased two elements"); - mlib_check(mlib_vec_at(ints, -1), eq, -7, because, "Negative index wraps"); + mlib_check(mlib_vec_at(ints, -1), eq, 1729, because, "Negative index wraps"); int_vec_destroy(&ints); } From 47629d7a79286a8b99bc1e632b2330b0d0ecfdbe Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Wed, 1 Oct 2025 12:15:19 -0600 Subject: [PATCH 06/20] Use mlib strings and vec types in TestSuite files --- .clang-format | 1 + src/common/src/mlib/str_vec.h | 31 +++ src/libmongoc/tests/TestSuite.c | 273 ++++++++------------------- src/libmongoc/tests/TestSuite.h | 109 ++++++++--- src/libmongoc/tests/json-test.c | 71 +++---- src/libmongoc/tests/test-libmongoc.h | 6 +- 6 files changed, 241 insertions(+), 250 deletions(-) create mode 100644 src/common/src/mlib/str_vec.h diff --git a/.clang-format b/.clang-format index a392e039ca1..bfd879b27f2 100644 --- a/.clang-format +++ b/.clang-format @@ -149,6 +149,7 @@ ForEachMacros: - mlib_foreach_urange - mlib_foreach - mlib_foreach_arr + - mlib_vec_foreach IfMacros: - mlib_assert_aborts - KJ_IF_MAYBE diff --git a/src/common/src/mlib/str_vec.h b/src/common/src/mlib/str_vec.h new file mode 100644 index 00000000000..bdfe723ed3e --- /dev/null +++ b/src/common/src/mlib/str_vec.h @@ -0,0 +1,31 @@ +/** + * @file str_vec.h + * @brief This file defines mstr_vec, a common "array of strings" type + * @date 2025-09-30 + * + * @copyright Copyright 2009-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MLIB_STR_VEC_H_INCLUDED +#define MLIB_STR_VEC_H_INCLUDED + +#include +#include + +#define T mstr +#define VecDestroyElement(Ptr) (mstr_delete(*Ptr), Ptr->data = NULL, Ptr->len = 0) +#define VecCopyElement(Dst, Src) (*Dst = mstr_copy(*Src), Dst->data != NULL) +#include + +#endif // MLIB_STR_VEC_H_INCLUDED diff --git a/src/libmongoc/tests/TestSuite.c b/src/libmongoc/tests/TestSuite.c index c82bc338361..baf9fc98748 100644 --- a/src/libmongoc/tests/TestSuite.c +++ b/src/libmongoc/tests/TestSuite.c @@ -21,6 +21,8 @@ #include +#include + #include @@ -131,9 +133,6 @@ TestSuite_Init(TestSuite *suite, const char *name, int argc, char **argv) suite->flags = 0; suite->prgname = bson_strdup(argv[0]); suite->silent = false; - suite->ctest_run = NULL; - _mongoc_array_init(&suite->match_patterns, sizeof(char *)); - _mongoc_array_init(&suite->failing_flaky_skips, sizeof(TestSkip *)); for (i = 1; i < argc; i++) { if (0 == strcmp("-d", argv[i])) { @@ -172,7 +171,7 @@ TestSuite_Init(TestSuite *suite, const char *name, int argc, char **argv) } else if ((0 == strcmp("-s", argv[i])) || (0 == strcmp("--silent", argv[i]))) { suite->silent = true; } else if ((0 == strcmp("--ctest-run", argv[i]))) { - if (suite->ctest_run) { + if (suite->ctest_run.data) { test_error("'--ctest-run' can only be specified once"); } if (argc - 1 == i) { @@ -180,14 +179,14 @@ TestSuite_Init(TestSuite *suite, const char *name, int argc, char **argv) } suite->flags |= TEST_NOFORK; suite->silent = true; - suite->ctest_run = bson_strdup(argv[++i]); + suite->ctest_run = mstr_copy_cstring(argv[i + 1]); + ++i; } else if ((0 == strcmp("-l", argv[i])) || (0 == strcmp("--match", argv[i]))) { - char *val; if (argc - 1 == i) { test_error("%s requires an argument.", argv[i]); } - val = bson_strdup(argv[++i]); - _mongoc_array_append_val(&suite->match_patterns, val); + ++i; + *mstr_vec_push(&suite->match_patterns) = mstr_copy_cstring(argv[i]); } else if (0 == strcmp("--skip-tests", argv[i])) { if (argc - 1 == i) { test_error("%s requires an argument.", argv[i]); @@ -201,7 +200,7 @@ TestSuite_Init(TestSuite *suite, const char *name, int argc, char **argv) } } - if (suite->match_patterns.len != 0 && suite->ctest_run != NULL) { + if (suite->match_patterns.size != 0 && suite->ctest_run.data) { test_error("'--ctest-run' cannot be specified with '-l' or '--match'"); } @@ -280,57 +279,28 @@ TestSuite_AddLive(TestSuite *suite, /* IN */ } -static void -_TestSuite_AddCheck(Test *test, CheckFunc check, const char *name) -{ - test->checks[test->num_checks] = check; - if (++test->num_checks > MAX_TEST_CHECK_FUNCS) { - MONGOC_STDERR_PRINTF("Too many check funcs for %s, increase MAX_TEST_CHECK_FUNCS " - "to more than %d\n", - name, - MAX_TEST_CHECK_FUNCS); - abort(); - } -} - - Test * _V_TestSuite_AddFull(TestSuite *suite, const char *name, TestFuncWC func, TestFuncDtor dtor, void *ctx, va_list ap) { - CheckFunc check; - Test *test; - Test *iter; - - if (suite->ctest_run && (0 != strcmp(suite->ctest_run, name))) { + if (suite->ctest_run.data && mstr_cmp(suite->ctest_run, !=, mstr_cstring(name))) { if (dtor) { dtor(ctx); } return NULL; } - test = (Test *)bson_malloc0(sizeof *test); - test->name = bson_strdup(name); + Test *test = TestVec_push(&suite->tests); + test->name = mstr_copy_cstring(name); test->func = func; - test->num_checks = 0; + CheckFunc check; while ((check = va_arg(ap, CheckFunc))) { - _TestSuite_AddCheck(test, check, name); + *CheckFuncVec_push(&test->checks) = check; } - test->next = NULL; test->dtor = dtor; test->ctx = ctx; TestSuite_SeedRand(suite, test); - - if (!suite->tests) { - suite->tests = test; - return test; - } - - for (iter = suite->tests; iter->next; iter = iter->next) { - } - - iter->next = test; return test; } @@ -348,7 +318,7 @@ _TestSuite_AddMockServerTest(TestSuite *suite, const char *name, TestFunc func, va_end(ap); if (test) { - _TestSuite_AddCheck(test, TestSuite_CheckMockServerAllowed, name); + *CheckFuncVec_push(&test->checks) = TestSuite_CheckMockServerAllowed; } } @@ -531,10 +501,9 @@ TestSuite_RunTest(TestSuite *suite, /* IN */ char name[MAX_TEST_NAME_LENGTH]; mcommon_string_append_t buf; mcommon_string_t *mock_server_log_buf; - size_t i; int status = 0; - bson_snprintf(name, sizeof name, "%s%s", suite->name, test->name); + bson_snprintf(name, sizeof name, "%s%.*s", suite->name, MSTR_FMT(test->name)); mcommon_string_new_as_append(&buf); @@ -542,19 +511,18 @@ TestSuite_RunTest(TestSuite *suite, /* IN */ test_msg("Begin %s, seed %u", name, test->seed); } - for (i = 0; i < suite->failing_flaky_skips.len; i++) { - TestSkip *skip = _mongoc_array_index(&suite->failing_flaky_skips, TestSkip *, i); - if (0 == strcmp(name, skip->test_name) && skip->subtest_desc == NULL) { - if (suite->ctest_run) { + mlib_vec_foreach (TestSkip, skip, suite->failing_flaky_skips) { + if (mstr_cmp(skip->test_name, ==, mstr_cstring(name)) && !skip->subtest_desc.data) { + if (suite->ctest_run.data) { /* Write a marker that tells CTest that we are skipping this test */ test_msg("@@ctest-skipped@@"); } if (!suite->silent) { mcommon_string_append_printf(&buf, - " { \"status\": \"skip\", \"test_file\": \"%s\"," - " \"reason\": \"%s\" }%s", - test->name, - skip->reason, + " { \"status\": \"skip\", \"test_file\": \"%.*s\"," + " \"reason\": \"%.*s\" }%s", + MSTR_FMT(test->name), + MSTR_FMT(skip->reason), ((*count) == 1) ? "" : ","); test_msg("%s", mcommon_str_from_append(&buf)); if (suite->outfile) { @@ -567,15 +535,17 @@ TestSuite_RunTest(TestSuite *suite, /* IN */ } } - for (i = 0; i < test->num_checks; i++) { - if (!test->checks[i]()) { - if (suite->ctest_run) { + mlib_vec_foreach (CheckFunc, check, test->checks) { + if (!(*check)()) { + if (suite->ctest_run.data) { /* Write a marker that tells CTest that we are skipping this test */ test_msg("@@ctest-skipped@@"); } if (!suite->silent) { - mcommon_string_append_printf( - &buf, " { \"status\": \"skip\", \"test_file\": \"%s\" }%s", test->name, ((*count) == 1) ? "" : ","); + mcommon_string_append_printf(&buf, + " { \"status\": \"skip\", \"test_file\": \"%.*s\" }%s", + MSTR_FMT(test->name), + ((*count) == 1) ? "" : ","); test_msg("%s", mcommon_str_from_append(&buf)); if (suite->outfile) { fprintf(suite->outfile, "%s", mcommon_str_from_append(&buf)); @@ -690,11 +660,9 @@ TestSuite_PrintHelp(TestSuite *suite) /* IN */ static void TestSuite_PrintTests(TestSuite *suite) /* IN */ { - Test *iter; - printf("\nTests:\n"); - for (iter = suite->tests; iter; iter = iter->next) { - printf("%s%s\n", suite->name, iter->name); + mlib_vec_foreach (Test, t, suite->tests) { + printf("%s%.*s\n", suite->name, MSTR_FMT(t->name)); } printf("\n"); @@ -870,7 +838,7 @@ TestSuite_TestMatchesName(const TestSuite *suite, const Test *test, const char * char name[128]; bool star = strlen(testname) && testname[strlen(testname) - 1] == '*'; - bson_snprintf(name, sizeof name, "%s%s", suite->name, test->name); + bson_snprintf(name, sizeof name, "%s%.*s", suite->name, MSTR_FMT(test->name)); if (star) { /* e.g. testname is "/Client*" and name is "/Client/authenticate" */ @@ -884,19 +852,18 @@ TestSuite_TestMatchesName(const TestSuite *suite, const Test *test, const char * bool test_matches(TestSuite *suite, Test *test) { - if (suite->ctest_run) { + if (suite->ctest_run.data) { /* We only want exactly the named test */ - return strcmp(test->name, suite->ctest_run) == 0; + return mstr_cmp(test->name, ==, suite->ctest_run); } /* If no match patterns were provided, then assume all match. */ - if (suite->match_patterns.len == 0) { + if (suite->match_patterns.size == 0) { return true; } - for (size_t i = 0u; i < suite->match_patterns.len; i++) { - char *pattern = _mongoc_array_index(&suite->match_patterns, char *, i); - if (TestSuite_TestMatchesName(suite, test, pattern)) { + mlib_vec_foreach (mstr, pat, suite->match_patterns) { + if (TestSuite_TestMatchesName(suite, test, pat->data)) { return true; } } @@ -905,23 +872,9 @@ test_matches(TestSuite *suite, Test *test) } void -_process_skip_file(const char *filename, mongoc_array_t *skips) +_process_skip_file(const char *filename, TestSkipVec *skips) { - const int max_lines = 1000; - int lines_read = 0; - char buffer[SKIP_LINE_BUFFER_SIZE]; - size_t buflen; FILE *skip_file; - char *fgets_ret; - TestSkip *skip; - char *test_name_end; - size_t comment_len; - char *comment_char; - char *comment_text; - size_t subtest_len; - size_t new_buflen; - char *subtest_start; - char *subtest_end; #ifdef _WIN32 if (0 != fopen_s(&skip_file, filename, "r")) { @@ -934,73 +887,47 @@ _process_skip_file(const char *filename, mongoc_array_t *skips) test_error("Failed to open skip file: %s: errno: %d", filename, errno); } - while (lines_read < max_lines) { - fgets_ret = fgets(buffer, sizeof(buffer), skip_file); - buflen = strlen(buffer); - - if (buflen == 0 || !fgets_ret) { - break; /* error or EOF */ + while (1) { + char buffer[SKIP_LINE_BUFFER_SIZE]; + if (!fgets(buffer, sizeof(buffer), skip_file)) { + break; /* error */ } - if (buffer[0] == '#' || buffer[0] == ' ' || buffer[0] == '\n') { - continue; /* Comment line or blank line */ + mstr_view line = mstr_cstring(buffer); + if (!line.len) { + // EOF + break; } - - skip = (TestSkip *)bson_malloc0(sizeof *skip); - if (buffer[buflen - 1] == '\n') - buflen--; - test_name_end = buffer + buflen; - - /* First get the comment, starting at '#' to EOL */ - comment_len = 0; - comment_char = strchr(buffer, '#'); - if (comment_char) { - test_name_end = comment_char; - comment_text = comment_char; - while (comment_text[0] == '#' || comment_text[0] == ' ' || comment_text[0] == '\t') { - if (++comment_text >= (buffer + buflen)) - break; - } - skip->reason = bson_strndup(comment_text, buflen - (comment_text - buffer)); - comment_len = buflen - (comment_char - buffer); - } else { - skip->reason = NULL; + // Remove whitespace + line = mstr_trim(line); + if (line.len == 0 || line.data[0] == '#') { + // Empty line or comment + continue; } - /* Next get the subtest name, from first '"' until last '"' */ - new_buflen = buflen - comment_len; - subtest_start = strstr(buffer, "/\""); - if (subtest_start && (!comment_char || (subtest_start < comment_char))) { - test_name_end = subtest_start; - subtest_start++; - /* find the second '"' that marks end of subtest name */ - subtest_end = subtest_start + 1; - while (subtest_end[0] != '\0' && subtest_end[0] != '"' && (subtest_end < buffer + new_buflen)) { - subtest_end++; - } - /* 'subtest_start + 1' to trim leading and trailing '"' */ - subtest_len = subtest_end - (subtest_start + 1); - skip->subtest_desc = bson_strndup(subtest_start + 1, subtest_len); - } else { - skip->subtest_desc = NULL; - } + TestSkip skip = {0}; + // If there is a trailing comment, drop that: + mstr_view comment = {0}; + mstr_split_around(line, mstr_cstring("#"), &line, &comment); + line = mstr_trim(line); + comment = mstr_trim(comment); - /* Next get the test name */ - while (test_name_end[-1] == ' ' && test_name_end > buffer) { - /* trailing space might be between test name and '#' */ - test_name_end--; + if (comment.len) { + skip.reason = mstr_copy(comment); } - skip->test_name = bson_strndup(buffer, test_name_end - buffer); - - _mongoc_array_append_val(skips, skip); - lines_read++; - } - if (lines_read == max_lines) { - test_error("Skip file: %s exceeded maximum lines: %d. Increase " - "max_lines in _process_skip_file", - filename, - max_lines); + // If it contains a '/"' substring, the quoted part is the subtest description, + // and everything before the '/' is the main test name. Split on that: + mstr_view test_name; + mstr_view subtest_desc; + if (mstr_split_around(line, mstr_cstring("/\""), &test_name, &subtest_desc)) { + // Drop trailing quote: + mlib_check(mstr_at(subtest_desc, -1), eq, '"', because, "Subtest description should end with a quote"); + subtest_desc = mstr_slice(subtest_desc, 0, -1); + skip.subtest_desc = mstr_copy(subtest_desc); + } + skip.test_name = mstr_copy(test_name); + *TestSkipVec_push(skips) = skip; } fclose(skip_file); } @@ -1008,30 +935,29 @@ _process_skip_file(const char *filename, mongoc_array_t *skips) static int TestSuite_RunAll(TestSuite *suite /* IN */) { - Test *test; int count = 0; int status = 0; ASSERT(suite); /* initialize "count" so we can omit comma after last test output */ - for (test = suite->tests; test; test = test->next) { - if (test_matches(suite, test)) { + mlib_vec_foreach (Test, t, suite->tests) { + if (test_matches(suite, t)) { count++; } } - if (suite->ctest_run) { + if (suite->ctest_run.data) { /* We should have matched *at most* one test */ ASSERT(count <= 1); if (count == 0) { - test_error("No such test '%s'", suite->ctest_run); + test_error("No such test '%.*s'", MSTR_FMT(suite->ctest_run)); } } - for (test = suite->tests; test; test = test->next) { - if (test_matches(suite, test)) { - status += TestSuite_RunTest(suite, test, &count); + mlib_vec_foreach (Test, t, suite->tests) { + if (test_matches(suite, t)) { + status += TestSuite_RunTest(suite, t, &count); count--; } } @@ -1075,39 +1001,20 @@ TestSuite_Run(TestSuite *suite) /* IN */ } start_us = bson_get_monotonic_time(); - if (suite->tests) { - failures += TestSuite_RunAll(suite); - } else if (!suite->silent) { - TestSuite_PrintJsonFooter(stdout); - if (suite->outfile) { - TestSuite_PrintJsonFooter(suite->outfile); - } - } + failures += TestSuite_RunAll(suite); MONGOC_DEBUG("Duration of all tests (s): %" PRId64, (bson_get_monotonic_time() - start_us) / (1000 * 1000)); return failures; } - void TestSuite_Destroy(TestSuite *suite) { - Test *test; - Test *tmp; - bson_mutex_lock(&gTestMutex); gTestSuite = NULL; bson_mutex_unlock(&gTestMutex); - for (test = suite->tests; test; test = tmp) { - tmp = test->next; - - if (test->dtor) { - test->dtor(test->ctx); - } - bson_free(test->name); - bson_free(test); - } + TestVec_destroy(&suite->tests); if (suite->outfile) { fclose(suite->outfile); @@ -1117,23 +1024,9 @@ TestSuite_Destroy(TestSuite *suite) bson_free(suite->name); bson_free(suite->prgname); - bson_free(suite->ctest_run); - for (size_t i = 0u; i < suite->match_patterns.len; i++) { - char *val = _mongoc_array_index(&suite->match_patterns, char *, i); - bson_free(val); - } - - _mongoc_array_destroy(&suite->match_patterns); - - for (size_t i = 0u; i < suite->failing_flaky_skips.len; i++) { - TestSkip *val = _mongoc_array_index(&suite->failing_flaky_skips, TestSkip *, i); - bson_free(val->test_name); - bson_free(val->subtest_desc); - bson_free(val->reason); - bson_free(val); - } - - _mongoc_array_destroy(&suite->failing_flaky_skips); + mstr_delete(suite->ctest_run); + mstr_vec_destroy(&suite->match_patterns); + TestSkipVec_destroy(&suite->failing_flaky_skips); } diff --git a/src/libmongoc/tests/TestSuite.h b/src/libmongoc/tests/TestSuite.h index c3627c71e40..9486010268a 100644 --- a/src/libmongoc/tests/TestSuite.h +++ b/src/libmongoc/tests/TestSuite.h @@ -25,6 +25,8 @@ #include +#include +#include #include #include @@ -655,37 +657,105 @@ typedef void (*TestFunc)(void); typedef void (*TestFuncWC)(void *); typedef void (*TestFuncDtor)(void *); typedef int (*CheckFunc)(void); -typedef struct _Test Test; -typedef struct _TestSuite TestSuite; +typedef struct Test Test; +typedef struct TestSuite TestSuite; typedef struct _TestFnCtx TestFnCtx; -typedef struct _TestSkip TestSkip; - - -struct _Test { - Test *next; - char *name; +typedef struct TestSkip TestSkip; + +#define T CheckFunc +#define VecName CheckFuncVec +#include + +struct Test { + /** + * @brief The C string that names the test case + */ + mstr name; + /** + * @brief The function that will be executed for the test case + */ TestFuncWC func; + /** + * @brief The function that destroys the context data associated with the test case + */ TestFuncDtor dtor; + /** + * @brief Pointer to arbitrary context data associated with the text case + */ void *ctx; + /** + * @brief The exit code that was received from the test function + */ int exit_code; + /** + * @brief Randomness seed for the test case + */ unsigned seed; - CheckFunc checks[MAX_TEST_CHECK_FUNCS]; - size_t num_checks; + /** + * @brief Array of check functions that determine whether this test case should be skipped + */ + CheckFuncVec checks; +}; + +static inline void +Test_Destroy(Test *t) +{ + if (t->dtor) { + t->dtor(t->ctx); + } + mstr_delete(t->name); + CheckFuncVec_destroy(&t->checks); +} + +#define T Test +#define VecName TestVec +#define VecDestroyElement Test_Destroy +#include + +/** + * @brief Information about a test that we plan to skip + */ +struct TestSkip { + /** + * @brief The name of the test that is being skipped + */ + mstr test_name; + /** + * @brief If not-null, the description of the sub-test that we are skipping. + */ + mstr subtest_desc; + /** + * @brief An explanatory string for why we are skipping the test + */ + mstr reason; }; +static inline void +TestSkip_Destroy(TestSkip *skip) +{ + mstr_delete(skip->test_name); + mstr_delete(skip->subtest_desc); + mstr_delete(skip->reason); +} + +#define T TestSkip +#define VecName TestSkipVec +#define VecDestroyElement(Skip) TestSkip_Destroy(Skip) +#include -struct _TestSuite { + +struct TestSuite { char *prgname; char *name; - mongoc_array_t match_patterns; - char *ctest_run; - Test *tests; + mstr_vec match_patterns; + mstr ctest_run; + TestVec tests; FILE *outfile; int flags; int silent; mcommon_string_t *mock_server_log_buf; FILE *mock_server_log; - mongoc_array_t failing_flaky_skips; + TestSkipVec failing_flaky_skips; }; @@ -695,13 +765,6 @@ struct _TestFnCtx { }; -struct _TestSkip { - char *test_name; - char *subtest_desc; - char *reason; -}; - - void TestSuite_Init(TestSuite *suite, const char *name, int argc, char **argv); void @@ -742,7 +805,7 @@ test_suite_debug_output(void); void test_suite_mock_server_log(const char *msg, ...); void -_process_skip_file(const char *, mongoc_array_t *); +_process_skip_file(const char *, TestSkipVec *); bool TestSuite_NoFork(TestSuite *suite); diff --git a/src/libmongoc/tests/json-test.c b/src/libmongoc/tests/json-test.c index 22e0953e891..61e37262f0b 100644 --- a/src/libmongoc/tests/json-test.c +++ b/src/libmongoc/tests/json-test.c @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -1884,9 +1885,11 @@ _install_json_test_suite_with_check(TestSuite *suite, const char *base, const ch bson_snprintf(joined, PATH_MAX, "%s/%s", base, subdir); ASSERT(realpath(joined, resolved)); - if (suite->ctest_run) { - const char *found = strstr(suite->ctest_run, subdir); - if (found != suite->ctest_run && found != suite->ctest_run + 1) { + if (suite->ctest_run.data) { + // If we're running a specific test, only register if the directory we are scanning + // is a prefix of the requested test pathname + size_t where = mstr_find(suite->ctest_run, mstr_cstring(subdir)); + if (where != 0 && where != 1) { return; } } @@ -1903,39 +1906,39 @@ _install_json_test_suite_with_check(TestSuite *suite, const char *base, const ch ext[0] = '\0'; test = _skip_if_unsupported(skip_json, test); - for (size_t j = 0u; j < suite->failing_flaky_skips.len; j++) { - TestSkip *skip = _mongoc_array_index(&suite->failing_flaky_skips, TestSkip *, j); - if (0 == strcmp(skip_json, skip->test_name)) { - /* Modify the test file to give applicable entries a skipReason */ - bson_t *modified = bson_new(); - bson_array_builder_t *modified_tests; - bson_iter_t iter; - - bson_copy_to_excluding_noinit(test, modified, "tests", NULL); - BSON_APPEND_ARRAY_BUILDER_BEGIN(modified, "tests", &modified_tests); - BSON_ASSERT(bson_iter_init_find(&iter, test, "tests")); - for (bson_iter_recurse(&iter, &iter); bson_iter_next(&iter);) { - bson_iter_t desc_iter; - uint32_t desc_len; - const char *desc; - bson_t original_test; - bson_t modified_test; - - bson_iter_bson(&iter, &original_test); - bson_iter_init_find(&desc_iter, &original_test, "description"); - desc = bson_iter_utf8(&desc_iter, &desc_len); - - bson_array_builder_append_document_begin(modified_tests, &modified_test); - bson_concat(&modified_test, &original_test); - if (!skip->subtest_desc || 0 == strcmp(skip->subtest_desc, desc)) { - BSON_APPEND_UTF8(&modified_test, "skipReason", skip->reason != NULL ? skip->reason : "(null)"); - } - bson_array_builder_append_document_end(modified_tests, &modified_test); + mlib_vec_foreach (TestSkip, skip, suite->failing_flaky_skips) { + if (mstr_cmp(mstr_cstring(skip_json), !=, skip->test_name)) { + continue; + } + /* Modify the test file to give applicable entries a skipReason */ + bson_t *modified = bson_new(); + bson_array_builder_t *modified_tests; + bson_iter_t iter; + + bson_copy_to_excluding_noinit(test, modified, "tests", NULL); + BSON_APPEND_ARRAY_BUILDER_BEGIN(modified, "tests", &modified_tests); + BSON_ASSERT(bson_iter_init_find(&iter, test, "tests")); + for (bson_iter_recurse(&iter, &iter); bson_iter_next(&iter);) { + bson_iter_t desc_iter; + uint32_t desc_len; + const char *desc; + bson_t original_test; + bson_t modified_test; + + bson_iter_bson(&iter, &original_test); + bson_iter_init_find(&desc_iter, &original_test, "description"); + desc = bson_iter_utf8(&desc_iter, &desc_len); + + bson_array_builder_append_document_begin(modified_tests, &modified_test); + bson_concat(&modified_test, &original_test); + if (!skip->subtest_desc.data || mstr_cmp(skip->subtest_desc, ==, mstr_cstring(desc))) { + BSON_APPEND_UTF8(&modified_test, "skipReason", skip->reason.data ? skip->reason.data : "(null)"); } - bson_append_array_builder_end(modified, modified_tests); - bson_destroy(test); - test = modified; + bson_array_builder_append_document_end(modified_tests, &modified_test); } + bson_append_array_builder_end(modified, modified_tests); + bson_destroy(test); + test = modified; } /* list of "check" functions that decide whether to skip the test */ va_start(ap, callback); diff --git a/src/libmongoc/tests/test-libmongoc.h b/src/libmongoc/tests/test-libmongoc.h index e3d05d2db06..e3b380455d3 100644 --- a/src/libmongoc/tests/test-libmongoc.h +++ b/src/libmongoc/tests/test-libmongoc.h @@ -21,14 +21,14 @@ #include -struct _TestSuite; +struct TestSuite; struct _bson_t; struct _server_version_t; void -test_libmongoc_init(struct _TestSuite *suite, int argc, char **argv); +test_libmongoc_init(struct TestSuite *suite, int argc, char **argv); void -test_libmongoc_destroy(struct _TestSuite *suite); +test_libmongoc_destroy(struct TestSuite *suite); mongoc_database_t * get_test_database(mongoc_client_t *client); From b39b7957860b86c9623d7874243a090457e41945 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 3 Oct 2025 10:10:16 -0600 Subject: [PATCH 07/20] Add support for test cases to declare tags (labels) This change allows for test cases to declare any number of associated "tags". The tags are specified after the test case name string as a list of bracketed tags, mimicking Catch2's syntax. The LoadTests.cmake script has been modified to apply test case's declared tags as CTest labels. This also gives us the ability to apply test fixture requirements granularly on only tests that declare their requirement (via a tag). --- build/cmake/LoadTests.cmake | 79 +++++++++++++---------- src/libmongoc/tests/TestSuite.c | 59 +++++++++++++---- src/libmongoc/tests/TestSuite.h | 30 +++++---- src/libmongoc/tests/test-mcd-azure-imds.c | 7 +- 4 files changed, 117 insertions(+), 58 deletions(-) diff --git a/build/cmake/LoadTests.cmake b/build/cmake/LoadTests.cmake index 262797b4779..8e6b9abde8a 100644 --- a/build/cmake/LoadTests.cmake +++ b/build/cmake/LoadTests.cmake @@ -3,46 +3,56 @@ # allowing CTest to control the execution, parallelization, and collection of # test results. -if (NOT EXISTS "${TEST_LIBMONGOC_EXE}") +if(NOT EXISTS "${TEST_LIBMONGOC_EXE}") # This will fail if 'test-libmongoc' is not compiled yet. - message (WARNING "The test executable ${TEST_LIBMONGOC_EXE} is not present. " + message(WARNING "The test executable ${TEST_LIBMONGOC_EXE} is not present. " "Its tests will not be registered") - add_test (mongoc/not-found NOT_FOUND) - return () -endif () + add_test(mongoc/not-found NOT_FOUND) + return() +endif() -# Get the list of tests -execute_process ( - COMMAND "${TEST_LIBMONGOC_EXE}" --list-tests --no-fork - OUTPUT_VARIABLE tests_out +# Get the list of tests. This command emits CMake code that defines variables for +# all test cases defined in the suite +execute_process( + COMMAND "${TEST_LIBMONGOC_EXE}" --tests-cmake --no-fork + OUTPUT_VARIABLE tests_cmake WORKING_DIRECTORY "${SRC_ROOT}" RESULT_VARIABLE retc ) -if (retc) +if(retc) # Failed to list the tests. That's bad. - message (FATAL_ERROR "Failed to run test-libmongoc to discover tests [${retc}]:\n${tests_out}") -endif () + message(FATAL_ERROR "Failed to run test-libmongoc to discover tests [${retc}]:\n${tests_out}") +endif() -# Split lines on newlines -string (REPLACE "\n" ";" lines "${tests_out}") +# Execute the code that defines the test case information +cmake_language(EVAL CODE "${tests_cmake}") -# TODO: Allow individual test cases to specify the fixtures they want. -set (all_fixtures "mongoc/fixtures/fake_kms_provider_server") -set (all_env +# Define environment variables that are common to all test cases +set(all_env TEST_KMS_PROVIDER_HOST=localhost:14987 # Refer: Fixtures.cmake ) -# Generate the test definitions -foreach (line IN LISTS lines) - if (NOT line MATCHES "^/") - # Only generate if the line begins with `/`, which all tests should. - continue () - endif () - # The new test name is prefixed with 'mongoc' - set (test "mongoc${line}") - # Define the test. Use `--ctest-run` to tell it that CTest is in control. - add_test ("${test}" "${TEST_LIBMONGOC_EXE}" --ctest-run "${line}") - set_tests_properties ("${test}" PROPERTIES +# The emitted code defines a list MONGOC_TESTS with the name of every test case +# in the suite. +foreach(casename IN LISTS MONGOC_TESTS) + set(name "mongoc${casename}") + # Run the program with --ctest-run to select only this one test case + add_test("${name}" "${TEST_LIBMONGOC_EXE}" --ctest-run "${casename}") + # The emitted code defines a TAGS list for every test case that it emits. We use + # these as the LABELS for the test case + set(labels "${MONGOC_TEST_${casename}_TAGS}") + + # Find what test fixtures the test wants by inspecting labels. Each "uses:" + # label defines the names of the test fixtures that a particular case requires + set(fixtures "${labels}") + list(FILTER fixtures INCLUDE REGEX "^uses:") + list(TRANSFORM fixtures REPLACE "^uses:(.*)$" "mongoc/fixtures/\\1") + + # Add a label for all test cases generated via this script so that they + # can be (de)selected separately: + list(APPEND labels test-libmongoc-generated) + # Set up the test: + set_tests_properties("${name}" PROPERTIES # test-libmongoc expects to execute in the root of the source directory WORKING_DIRECTORY "${SRC_ROOT}" # If a test emits '@@ctest-skipped@@', this tells us that the test is @@ -50,10 +60,11 @@ foreach (line IN LISTS lines) SKIP_REGULAR_EXPRESSION "@@ctest-skipped@@" # 45 seconds of timeout on each test. TIMEOUT 45 - FIXTURES_REQUIRED "${all_fixtures}" + # Common environment variables: ENVIRONMENT "${all_env}" - # Mark all tests generated from the executable, so they can be (de)selected - # for execution separately. - LABELS "test-libmongoc-generated" - ) -endforeach () + # Apply the labels + LABELS "${labels}" + # Fixture requirements: + FIXTURES_REQUIRED "${fixtures}" + ) +endforeach() diff --git a/src/libmongoc/tests/TestSuite.c b/src/libmongoc/tests/TestSuite.c index baf9fc98748..68d1876ca56 100644 --- a/src/libmongoc/tests/TestSuite.c +++ b/src/libmongoc/tests/TestSuite.c @@ -20,6 +20,7 @@ #include #include +#include #include @@ -168,6 +169,8 @@ TestSuite_Init(TestSuite *suite, const char *name, int argc, char **argv) suite->flags |= TEST_HELPTEXT; } else if (0 == strcmp("--list-tests", argv[i])) { suite->flags |= TEST_LISTTESTS; + } else if (0 == strcmp("--tests-cmake", argv[i])) { + suite->flags |= TEST_TESTS_CMAKE; } else if ((0 == strcmp("-s", argv[i])) || (0 == strcmp("--silent", argv[i]))) { suite->silent = true; } else if ((0 == strcmp("--ctest-run", argv[i]))) { @@ -280,9 +283,17 @@ TestSuite_AddLive(TestSuite *suite, /* IN */ Test * -_V_TestSuite_AddFull(TestSuite *suite, const char *name, TestFuncWC func, TestFuncDtor dtor, void *ctx, va_list ap) +_V_TestSuite_AddFull( + TestSuite *suite, const char *name_and_tags, TestFuncWC func, TestFuncDtor dtor, void *ctx, va_list ap) { - if (suite->ctest_run.data && mstr_cmp(suite->ctest_run, !=, mstr_cstring(name))) { + // Split the name and tags around the first whitespace: + mstr_view name, tags; + mstr_split_around(mstr_cstring(name_and_tags), mstr_cstring(" "), &name, &tags); + // Trim extras: + tags = mstr_trim(tags); + + if (suite->ctest_run.data && mstr_cmp(suite->ctest_run, !=, name)) { + // We are running CTest, and not running this particular test, so just skip registering it if (dtor) { dtor(ctx); } @@ -290,9 +301,19 @@ _V_TestSuite_AddFull(TestSuite *suite, const char *name, TestFuncWC func, TestFu } Test *test = TestVec_push(&suite->tests); - test->name = mstr_copy_cstring(name); + test->name = mstr_copy(name); test->func = func; + mstr_view tag, tail; + tail = tags; + while (tail.len) { + mlib_check(mstr_split_around(tail, mstr_cstring("["), NULL, &tag), + because, + "Expected an opening bracket for the next test tag"); + mlib_check(mstr_split_around(tag, mstr_cstring("]"), &tag, &tail), because, "Expected a closing bracket for tag"); + *mstr_vec_push(&test->tags) = mstr_copy(tag); + } + CheckFunc check; while ((check = va_arg(ap, CheckFunc))) { *CheckFuncVec_push(&test->checks) = check; @@ -334,13 +355,12 @@ TestSuite_AddWC(TestSuite *suite, /* IN */ } -void -_TestSuite_AddFull(TestSuite *suite, /* IN */ - const char *name, /* IN */ - TestFuncWC func, /* IN */ - TestFuncDtor dtor, /* IN */ - void *ctx, - ...) /* IN */ +void(TestSuite_AddFull)(TestSuite *suite, /* IN */ + const char *name, /* IN */ + TestFuncWC func, /* IN */ + TestFuncDtor dtor, /* IN */ + void *ctx, + ...) /* IN */ { va_list ap; @@ -640,6 +660,7 @@ TestSuite_PrintHelp(TestSuite *suite) /* IN */ "Options:\n" " -h, --help Show this help menu.\n" " --list-tests Print list of available tests.\n" + " --tests-cmake Print CMake code that defines test information.\n" " -f, --no-fork Do not spawn a process per test (abort on " "first error).\n" " -l, --match PATTERN Run test by name, e.g. \"/Client/command\" or " @@ -668,6 +689,18 @@ TestSuite_PrintTests(TestSuite *suite) /* IN */ printf("\n"); } +static void +TestSuite_PrintCMake(TestSuite *suite) +{ + printf("set(MONGOC_TESTS)\n"); + mlib_vec_foreach (Test, t, suite->tests) { + printf("list(APPEND MONGOC_TESTS [[%.*s]])\n", MSTR_FMT(t->name)); + printf("set(MONGOC_TEST_%.*s_TAGS)\n", MSTR_FMT(t->name)); + mlib_vec_foreach (mstr, tag, t->tags) { + printf("list(APPEND MONGOC_TEST_%.*s_TAGS [[%.*s]])\n", MSTR_FMT(t->name), MSTR_FMT(*tag)); + } + } +} static void TestSuite_PrintJsonSystemHeader(FILE *stream) @@ -989,7 +1022,11 @@ TestSuite_Run(TestSuite *suite) /* IN */ TestSuite_PrintTests(suite); } - if ((suite->flags & TEST_HELPTEXT) || (suite->flags & TEST_LISTTESTS)) { + if (suite->flags & TEST_TESTS_CMAKE) { + TestSuite_PrintCMake(suite); + } + + if ((suite->flags & TEST_HELPTEXT) || (suite->flags & TEST_LISTTESTS) || (suite->flags & TEST_TESTS_CMAKE)) { return 0; } diff --git a/src/libmongoc/tests/TestSuite.h b/src/libmongoc/tests/TestSuite.h index 9486010268a..432f4305fe2 100644 --- a/src/libmongoc/tests/TestSuite.h +++ b/src/libmongoc/tests/TestSuite.h @@ -96,6 +96,7 @@ bson_open(const char *filename, int flags, ...) #define TEST_DEBUGOUTPUT (1 << 3) #define TEST_TRACE (1 << 4) #define TEST_LISTTESTS (1 << 5) +#define TEST_TESTS_CMAKE (1 << 6) #define CERT_CA CERT_TEST_DIR "/ca.pem" @@ -671,6 +672,10 @@ struct Test { * @brief The C string that names the test case */ mstr name; + /** + * @brief Set of tags that are associated with this test case + */ + mstr_vec tags; /** * @brief The function that will be executed for the test case */ @@ -704,6 +709,7 @@ Test_Destroy(Test *t) t->dtor(t->ctx); } mstr_delete(t->name); + mstr_vec_destroy(&t->tags); CheckFuncVec_destroy(&t->checks); } @@ -764,11 +770,10 @@ struct _TestFnCtx { TestFuncDtor dtor; }; - void TestSuite_Init(TestSuite *suite, const char *name, int argc, char **argv); void -TestSuite_Add(TestSuite *suite, const char *name, TestFunc func); +TestSuite_Add(TestSuite *suite, const char *name_and_tags, TestFunc func); int TestSuite_CheckLive(void); void @@ -779,21 +784,22 @@ void _TestSuite_AddMockServerTest(TestSuite *suite, const char *name, TestFunc func, ...); #define TestSuite_AddMockServerTest(_suite, _name, ...) _TestSuite_AddMockServerTest(_suite, _name, __VA_ARGS__, NULL) void -TestSuite_AddWC(TestSuite *suite, const char *name, TestFuncWC func, TestFuncDtor dtor, void *ctx); +TestSuite_AddWC(TestSuite *suite, const char *name_and_tags, TestFuncWC func, TestFuncDtor dtor, void *ctx); Test * -_V_TestSuite_AddFull(TestSuite *suite, const char *name, TestFuncWC func, TestFuncDtor dtor, void *ctx, va_list ap); +_V_TestSuite_AddFull( + TestSuite *suite, const char *name_and_tags, TestFuncWC func, TestFuncDtor dtor, void *ctx, va_list ap); void -_TestSuite_AddFull(TestSuite *suite, const char *name, TestFuncWC func, TestFuncDtor dtor, void *ctx, ...); +TestSuite_AddFull(TestSuite *suite, const char *name_and_tags, TestFuncWC func, TestFuncDtor dtor, void *ctx, ...); void _TestSuite_TestFnCtxDtor(void *ctx); #define TestSuite_AddFull(_suite, _name, _func, _dtor, _ctx, ...) \ - _TestSuite_AddFull(_suite, _name, _func, _dtor, _ctx, __VA_ARGS__, NULL) -#define TestSuite_AddFullWithTestFn(_suite, _name, _func, _dtor, _test_fn, ...) \ - do { \ - TestFnCtx *ctx = bson_malloc(sizeof(TestFnCtx)); \ - ctx->test_fn = (TestFunc)(_test_fn); \ - ctx->dtor = _dtor; \ - _TestSuite_AddFull(_suite, _name, _func, _TestSuite_TestFnCtxDtor, ctx, __VA_ARGS__, NULL); \ + (TestSuite_AddFull)(_suite, _name, _func, _dtor, _ctx, __VA_ARGS__, NULL) +#define TestSuite_AddFullWithTestFn(_suite, _name, _func, _dtor, _test_fn, ...) \ + do { \ + TestFnCtx *ctx = bson_malloc(sizeof(TestFnCtx)); \ + ctx->test_fn = (TestFunc)(_test_fn); \ + ctx->dtor = _dtor; \ + TestSuite_AddFull(_suite, _name, _func, _TestSuite_TestFnCtxDtor, ctx, __VA_ARGS__, NULL); \ } while (0) int TestSuite_Run(TestSuite *suite); diff --git a/src/libmongoc/tests/test-mcd-azure-imds.c b/src/libmongoc/tests/test-mcd-azure-imds.c index e208c9638fa..785e22c9ca9 100644 --- a/src/libmongoc/tests/test-mcd-azure-imds.c +++ b/src/libmongoc/tests/test-mcd-azure-imds.c @@ -107,5 +107,10 @@ test_mcd_azure_imds_install(TestSuite *suite) { TestSuite_Add(suite, "/azure/imds/http/parse", _test_oauth_parse); TestSuite_Add(suite, "/azure/imds/http/request", _test_http_req); - TestSuite_AddFull(suite, "/azure/imds/http/talk", _test_with_mock_server, NULL, NULL, have_mock_server_env); + TestSuite_AddFull(suite, + "/azure/imds/http/talk [uses:fake_kms_provider_server]", + _test_with_mock_server, + NULL, + NULL, + have_mock_server_env); } From 4e844f6f612e406b6a4ebccee3dc0c7d8ef94626 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 3 Oct 2025 10:17:14 -0600 Subject: [PATCH 08/20] Add parens around assignment-as-condition --- src/common/tests/test-mlib.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/tests/test-mlib.c b/src/common/tests/test-mlib.c index 71526334e4d..4e3e295bdf7 100644 --- a/src/common/tests/test-mlib.c +++ b/src/common/tests/test-mlib.c @@ -1217,7 +1217,7 @@ _test_int_vec(void) // Append an element int *el; - mlib_check(el = int_vec_push(&ints)); + mlib_check((el = int_vec_push(&ints))); *el = 42; mlib_check(int_vec_begin(&ints)[0], eq, 42); mlib_check(ints.size, eq, 1); From 6837d55d4678766a72bb695e930f421f228b30be Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 3 Oct 2025 10:20:38 -0600 Subject: [PATCH 09/20] Add Earthly alias for CTest with uvx --- Earthfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Earthfile b/Earthfile index ba6af57c399..6ebb576e250 100644 --- a/Earthfile +++ b/Earthfile @@ -137,6 +137,7 @@ PREP_CMAKE: FUNCTION # Run all CMake commands using uvx: RUN __alias cmake uvx cmake + RUN __alias ctest uvx --from=cmake ctest # Executing any CMake command will warm the cache: RUN cmake --version | head -n 1 From 319e351950f4bed3a39e576737559b55e536f118 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 3 Oct 2025 10:39:49 -0600 Subject: [PATCH 10/20] The GCP test also requires the fake KMS server --- src/libmongoc/tests/test-service-gcp.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libmongoc/tests/test-service-gcp.c b/src/libmongoc/tests/test-service-gcp.c index 724c00eace7..36f9b2aed42 100644 --- a/src/libmongoc/tests/test-service-gcp.c +++ b/src/libmongoc/tests/test-service-gcp.c @@ -107,5 +107,10 @@ test_service_gcp_install(TestSuite *suite) { TestSuite_Add(suite, "/gcp/http/parse", _test_gcp_parse); TestSuite_Add(suite, "/gcp/http/request", _test_gcp_http_request); - TestSuite_AddFull(suite, "/gcp/http/talk", _test_with_mock_server, NULL, NULL, have_mock_server_env); + TestSuite_AddFull(suite, + "/gcp/http/talk [uses:fake_kms_provider_server]", + _test_with_mock_server, + NULL, + NULL, + have_mock_server_env); } From 9de54abeac7c6b512ea50a120b93db4d337c529f Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 3 Oct 2025 12:09:19 -0600 Subject: [PATCH 11/20] Windows-only compile error --- src/libmongoc/tests/TestSuite.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libmongoc/tests/TestSuite.c b/src/libmongoc/tests/TestSuite.c index 68d1876ca56..43b3ed4221d 100644 --- a/src/libmongoc/tests/TestSuite.c +++ b/src/libmongoc/tests/TestSuite.c @@ -406,7 +406,7 @@ TestSuite_RunFuncInChild(TestSuite *suite, /* IN */ si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); - cmdline = bson_strdup_printf("%s --silent --no-fork -l %s", suite->prgname, test->name); + cmdline = bson_strdup_printf("%s --silent --no-fork -l %.*s", suite->prgname, MSTR_FMT(test->name)); if (!CreateProcess(NULL, cmdline, From aed388da8cf406386d863e27f530a793d75453d4 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 3 Oct 2025 12:38:24 -0600 Subject: [PATCH 12/20] No SSIZE_MAX on Windows --- src/common/src/mlib/str.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/src/mlib/str.h b/src/common/src/mlib/str.h index 1b53c3ad36f..44a1a9c3914 100644 --- a/src/common/src/mlib/str.h +++ b/src/common/src/mlib/str.h @@ -33,6 +33,7 @@ #include #include +#include /** * @brief A simple non-owning string-view type. @@ -697,7 +698,7 @@ mstr_resize_for_overwrite(mstr *const str, const size_t new_len) { // We need to allocate one additional char to hold the null terminator size_t alloc_size = new_len; - if (mlib_add(&alloc_size, 1) || alloc_size > SSIZE_MAX) { + if (mlib_add(&alloc_size, 1) || alloc_size > PTRDIFF_MAX) { // Allocation size is too large return false; } From b4d93bbf530117cd754dd4adf5c2705338b93e8e Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 3 Oct 2025 13:54:28 -0600 Subject: [PATCH 13/20] uninit-var in MSVC --- src/common/src/mlib/str.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/src/mlib/str.h b/src/common/src/mlib/str.h index 44a1a9c3914..28c3fa2e01e 100644 --- a/src/common/src/mlib/str.h +++ b/src/common/src/mlib/str.h @@ -834,7 +834,7 @@ static inline mstr mstr_concat(mstr_view a, mstr_view b) { mstr ret = {NULL, 0}; - size_t cat_len; + size_t cat_len = 0; if (mlib_add(&cat_len, a.len, b.len)) { // Size would overflow. No go. return ret; From 11c9aa809eaf957daacc5b6000380029cfdd0ebd Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 3 Oct 2025 14:13:06 -0600 Subject: [PATCH 14/20] Fix UBSan issue for null pointer arithmetic --- src/common/src/mlib/vec.t.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/src/mlib/vec.t.h b/src/common/src/mlib/vec.t.h index 793724c1000..fd21d497ac0 100644 --- a/src/common/src/mlib/vec.t.h +++ b/src/common/src/mlib/vec.t.h @@ -417,7 +417,7 @@ mlib_extern_c_end(); #ifndef mlib_vec_foreach #define mlib_vec_foreach(Type, VarName, Vector) \ - for (Type *VarName = (Vector).data; VarName != (Vector).data + (Vector).size; ++VarName) + for (Type *VarName = (Vector).data; VarName && (VarName != (Vector).data + (Vector).size); ++VarName) #endif #ifndef mlib_vec_at From 91d517b02a3bdb585cc298557021c670b9271e3f Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Mon, 6 Oct 2025 12:20:48 -0600 Subject: [PATCH 15/20] Fix another nullptr-arith warning --- src/common/src/mlib/vec.t.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/src/mlib/vec.t.h b/src/common/src/mlib/vec.t.h index fd21d497ac0..921add05d2a 100644 --- a/src/common/src/mlib/vec.t.h +++ b/src/common/src/mlib/vec.t.h @@ -267,7 +267,9 @@ fn(resize(VecName *const self, size_t const count)) mlib_noexcept // Check if we aren't actually growing the vector. if (count <= self->size) { // We need to destroy elements at the tail. If `count == size`, this is a no-op. - fn(erase(self, fn(begin(self)) + count, fn(end(self)))); + if (self->data) { + fn(erase(self, fn(begin(self)) + count, fn(end(self)))); + } return true; } From 6a0c534e4db04e70eb552f51aad7c32c6006b7e3 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Mon, 6 Oct 2025 12:35:40 -0600 Subject: [PATCH 16/20] Tests can declare resource locks in tags --- build/cmake/LoadTests.cmake | 20 +++++++++++++++++--- src/libmongoc/tests/test-mcd-azure-imds.c | 2 +- src/libmongoc/tests/test-service-gcp.c | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/build/cmake/LoadTests.cmake b/build/cmake/LoadTests.cmake index 8e6b9abde8a..288d819b29e 100644 --- a/build/cmake/LoadTests.cmake +++ b/build/cmake/LoadTests.cmake @@ -44,9 +44,21 @@ foreach(casename IN LISTS MONGOC_TESTS) # Find what test fixtures the test wants by inspecting labels. Each "uses:" # label defines the names of the test fixtures that a particular case requires - set(fixtures "${labels}") - list(FILTER fixtures INCLUDE REGEX "^uses:") - list(TRANSFORM fixtures REPLACE "^uses:(.*)$" "mongoc/fixtures/\\1") + list( + TRANSFORM labels + REPLACE "^uses:(.*)$" "mongoc/fixtures/\\1" + REGEX "^uses:" + OUTPUT_VARIABLE fixtures + ) + + list(TRANSFORM labels + REPLACE "^lock:(.*)" "\\1" + REGEX "^lock:" + OUTPUT_VARIABLE lock + ) + if("live" IN_LIST labels) + list(APPEND lock live-server) + endif() # Add a label for all test cases generated via this script so that they # can be (de)selected separately: @@ -66,5 +78,7 @@ foreach(casename IN LISTS MONGOC_TESTS) LABELS "${labels}" # Fixture requirements: FIXTURES_REQUIRED "${fixtures}" + # Resources that need exclusive access by this test: + RESOURCE_LOCK "${lock}" ) endforeach() diff --git a/src/libmongoc/tests/test-mcd-azure-imds.c b/src/libmongoc/tests/test-mcd-azure-imds.c index 785e22c9ca9..8cc871796eb 100644 --- a/src/libmongoc/tests/test-mcd-azure-imds.c +++ b/src/libmongoc/tests/test-mcd-azure-imds.c @@ -108,7 +108,7 @@ test_mcd_azure_imds_install(TestSuite *suite) TestSuite_Add(suite, "/azure/imds/http/parse", _test_oauth_parse); TestSuite_Add(suite, "/azure/imds/http/request", _test_http_req); TestSuite_AddFull(suite, - "/azure/imds/http/talk [uses:fake_kms_provider_server]", + "/azure/imds/http/talk [uses:fake_kms_provider_server][lock:fake-kms]", _test_with_mock_server, NULL, NULL, diff --git a/src/libmongoc/tests/test-service-gcp.c b/src/libmongoc/tests/test-service-gcp.c index 36f9b2aed42..db634b92f71 100644 --- a/src/libmongoc/tests/test-service-gcp.c +++ b/src/libmongoc/tests/test-service-gcp.c @@ -108,7 +108,7 @@ test_service_gcp_install(TestSuite *suite) TestSuite_Add(suite, "/gcp/http/parse", _test_gcp_parse); TestSuite_Add(suite, "/gcp/http/request", _test_gcp_http_request); TestSuite_AddFull(suite, - "/gcp/http/talk [uses:fake_kms_provider_server]", + "/gcp/http/talk [uses:fake_kms_provider_server][lock:fake-kms]", _test_with_mock_server, NULL, NULL, From 5ac0881d0994f02e1711d97bae5e7a81edc687a5 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Tue, 7 Oct 2025 12:21:28 -0600 Subject: [PATCH 17/20] Yet another nullptr-arith --- src/common/src/mlib/vec.t.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/src/mlib/vec.t.h b/src/common/src/mlib/vec.t.h index 921add05d2a..7715d130789 100644 --- a/src/common/src/mlib/vec.t.h +++ b/src/common/src/mlib/vec.t.h @@ -118,7 +118,7 @@ typedef struct VecName { T * end() noexcept { - return data + size; + return data ? data + size : data; } #endif } VecName; @@ -140,7 +140,7 @@ fn(begin(VecName *v)) mlib_noexcept vec_inline_spec T * fn(end(VecName *v)) mlib_noexcept { - return v->data + v->size; + return v->data ? v->data + v->size : v->data; } /** @@ -158,7 +158,7 @@ fn(cbegin(VecName const *v)) mlib_noexcept vec_inline_spec T const * fn(cend(VecName const *v)) mlib_noexcept { - return v->data + v->size; + return v->data ? v->data + v->size : v->data; } /** From 5329c7a55f08f402e13ca321590d2b00f1625c17 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Tue, 7 Oct 2025 13:43:49 -0600 Subject: [PATCH 18/20] Suppress an over-eager Clang warning --- build/cmake/MongoC-Warnings.cmake | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build/cmake/MongoC-Warnings.cmake b/build/cmake/MongoC-Warnings.cmake index aad93436e29..26080beb01f 100644 --- a/build/cmake/MongoC-Warnings.cmake +++ b/build/cmake/MongoC-Warnings.cmake @@ -103,4 +103,7 @@ mongoc_add_warning_options ( # Aside: Disable CRT insecurity warnings msvc:/D_CRT_SECURE_NO_WARNINGS - ) + + # Old Clang has an over-aggressive missing-braces warning that warns on the `foo = {0}` idiom + clang:clang-lt10:-Wno-missing-braces +) From 24225c02f0cfc9883ee353b8d10941e2b44ebd92 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Tue, 7 Oct 2025 13:44:31 -0600 Subject: [PATCH 19/20] Clean up automatic test properties from tags --- build/cmake/LoadTests.cmake | 40 +++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/build/cmake/LoadTests.cmake b/build/cmake/LoadTests.cmake index 288d819b29e..d6df59789aa 100644 --- a/build/cmake/LoadTests.cmake +++ b/build/cmake/LoadTests.cmake @@ -6,7 +6,7 @@ if(NOT EXISTS "${TEST_LIBMONGOC_EXE}") # This will fail if 'test-libmongoc' is not compiled yet. message(WARNING "The test executable ${TEST_LIBMONGOC_EXE} is not present. " - "Its tests will not be registered") + "Its tests will not be registered") add_test(mongoc/not-found NOT_FOUND) return() endif() @@ -32,6 +32,14 @@ set(all_env TEST_KMS_PROVIDER_HOST=localhost:14987 # Refer: Fixtures.cmake ) +function(list_select list_var) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "SELECT;REPLACE;OUT" "") + set(seq "${${list_var}}") + list(FILTER seq INCLUDE REGEX "${arg_SELECT}") + list(TRANSFORM seq REPLACE "${arg_SELECT}" "${arg_REPLACE}") + set("${arg_OUT}" "${seq}" PARENT_SCOPE) +endfunction() + # The emitted code defines a list MONGOC_TESTS with the name of every test case # in the suite. foreach(casename IN LISTS MONGOC_TESTS) @@ -40,22 +48,24 @@ foreach(casename IN LISTS MONGOC_TESTS) add_test("${name}" "${TEST_LIBMONGOC_EXE}" --ctest-run "${casename}") # The emitted code defines a TAGS list for every test case that it emits. We use # these as the LABELS for the test case + unset(labels) set(labels "${MONGOC_TEST_${casename}_TAGS}") # Find what test fixtures the test wants by inspecting labels. Each "uses:" # label defines the names of the test fixtures that a particular case requires - list( - TRANSFORM labels - REPLACE "^uses:(.*)$" "mongoc/fixtures/\\1" - REGEX "^uses:" - OUTPUT_VARIABLE fixtures - ) + list_select(labels SELECT "^uses:(.*)$" REPLACE "mongoc/fixtures/\\1" OUT fixtures) - list(TRANSFORM labels - REPLACE "^lock:(.*)" "\\1" - REGEX "^lock:" - OUTPUT_VARIABLE lock - ) + # For any "lock:..." labels, add a resource lock with the corresponding name + list_select(labels SELECT "^lock:(.*)$" REPLACE "\\1" OUT lock) + + # Tests can set a timeout with a tag: + list_select(labels SELECT "^timeout:(.*)$" REPLACE "\\1" OUT timeout) + if(NOT timeout) + # Default timeout of 5 seconds + set(timeout 5) + endif() + + # If a test declares that it is "live", lock exclusive access to the live server if("live" IN_LIST labels) list(APPEND lock live-server) endif() @@ -70,15 +80,15 @@ foreach(casename IN LISTS MONGOC_TESTS) # If a test emits '@@ctest-skipped@@', this tells us that the test is # skipped. SKIP_REGULAR_EXPRESSION "@@ctest-skipped@@" - # 45 seconds of timeout on each test. - TIMEOUT 45 + # Apply a timeout to each test, either the default or one from test tags + TIMEOUT "${timeout}" # Common environment variables: ENVIRONMENT "${all_env}" # Apply the labels LABELS "${labels}" # Fixture requirements: FIXTURES_REQUIRED "${fixtures}" - # Resources that need exclusive access by this test: + # Test may lock resources: RESOURCE_LOCK "${lock}" ) endforeach() From 535b88b30b31cfdb992365dc1c0f596169feaab6 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Tue, 7 Oct 2025 13:56:47 -0600 Subject: [PATCH 20/20] No -Wmissing-braces anywhere --- CMakeLists.txt | 1 + build/cmake/MongoC-Warnings.cmake | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4343a19442b..bb56c31e0ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -285,6 +285,7 @@ if(ENABLE_MAINTAINER_FLAGS) gnu-like:-Wuninitialized # Disabled, for now: gnu-like:-Wno-strict-aliasing + gnu-like:-Wno-missing-braces # Sign-comparison-mismatch: gnu-like:-Wsign-compare msvc:/we4018 diff --git a/build/cmake/MongoC-Warnings.cmake b/build/cmake/MongoC-Warnings.cmake index 26080beb01f..f7489137d16 100644 --- a/build/cmake/MongoC-Warnings.cmake +++ b/build/cmake/MongoC-Warnings.cmake @@ -103,7 +103,4 @@ mongoc_add_warning_options ( # Aside: Disable CRT insecurity warnings msvc:/D_CRT_SECURE_NO_WARNINGS - - # Old Clang has an over-aggressive missing-braces warning that warns on the `foo = {0}` idiom - clang:clang-lt10:-Wno-missing-braces )