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/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 diff --git a/build/cmake/LoadTests.cmake b/build/cmake/LoadTests.cmake index 262797b4779..d6df59789aa 100644 --- a/build/cmake/LoadTests.cmake +++ b/build/cmake/LoadTests.cmake @@ -3,57 +3,92 @@ # 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. " - "Its tests will not be registered") - 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 + 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() + +# 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 +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) + 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 + 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_select(labels SELECT "^uses:(.*)$" REPLACE "mongoc/fixtures/\\1" OUT fixtures) + + # 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() + + # 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 # skipped. SKIP_REGULAR_EXPRESSION "@@ctest-skipped@@" - # 45 seconds of timeout on each test. - TIMEOUT 45 - FIXTURES_REQUIRED "${all_fixtures}" + # Apply a timeout to each test, either the default or one from test tags + TIMEOUT "${timeout}" + # 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}" + # Test may lock resources: + RESOURCE_LOCK "${lock}" + ) +endforeach() 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 +) diff --git a/src/common/src/mlib/str.h b/src/common/src/mlib/str.h index 8f77fffe94f..28c3fa2e01e 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. @@ -288,6 +289,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 +460,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 * @@ -565,4 +641,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 > PTRDIFF_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 = 0; + 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/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/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/src/mlib/vec.t.h b/src/common/src/mlib/vec.t.h new file mode 100644 index 00000000000..7715d130789 --- /dev/null +++ b/src/common/src/mlib/vec.t.h @@ -0,0 +1,450 @@ +/** + * @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 + +#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 ? data + size : data; + } +#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->data + v->size : v->data; +} + +/** + * @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->data + v->size : v->data; +} + +/** + * @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. + if (self->data) { + 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 && (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 +#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 12405c2dbb4..4e3e295bdf7 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 @@ -917,8 +920,100 @@ _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 +_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) { @@ -1110,6 +1205,90 @@ _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"); + + mlib_check(mlib_vec_at(ints, -1), eq, 1729, because, "Negative index wraps"); + + 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) { @@ -1126,10 +1305,13 @@ 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); 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(); diff --git a/src/libmongoc/tests/TestSuite.c b/src/libmongoc/tests/TestSuite.c index c82bc338361..43b3ed4221d 100644 --- a/src/libmongoc/tests/TestSuite.c +++ b/src/libmongoc/tests/TestSuite.c @@ -20,6 +20,9 @@ #include #include +#include + +#include #include @@ -131,9 +134,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])) { @@ -169,10 +169,12 @@ 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]))) { - if (suite->ctest_run) { + if (suite->ctest_run.data) { test_error("'--ctest-run' can only be specified once"); } if (argc - 1 == i) { @@ -180,14 +182,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 +203,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 +282,46 @@ 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) +_V_TestSuite_AddFull( + TestSuite *suite, const char *name_and_tags, 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))) { + // 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); } return NULL; } - test = (Test *)bson_malloc0(sizeof *test); - test->name = bson_strdup(name); + Test *test = TestVec_push(&suite->tests); + test->name = mstr_copy(name); test->func = func; - test->num_checks = 0; + 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))) { - _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 +339,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; } } @@ -364,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; @@ -416,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, @@ -531,10 +521,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 +531,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 +555,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)); @@ -670,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 " @@ -690,16 +681,26 @@ 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"); } +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) @@ -870,7 +871,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 +885,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 +905,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 +920,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 +968,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--; } } @@ -1063,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; } @@ -1075,39 +1038,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 +1061,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..432f4305fe2 100644 --- a/src/libmongoc/tests/TestSuite.h +++ b/src/libmongoc/tests/TestSuite.h @@ -25,6 +25,8 @@ #include +#include +#include #include #include @@ -94,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" @@ -655,37 +658,110 @@ 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 Set of tags that are associated with this test case + */ + mstr_vec tags; + /** + * @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); + mstr_vec_destroy(&t->tags); + 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; +}; -struct _TestSuite { +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 { 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; }; @@ -694,18 +770,10 @@ struct _TestFnCtx { TestFuncDtor dtor; }; - -struct _TestSkip { - char *test_name; - char *subtest_desc; - char *reason; -}; - - 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 @@ -716,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); @@ -742,7 +811,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); diff --git a/src/libmongoc/tests/test-mcd-azure-imds.c b/src/libmongoc/tests/test-mcd-azure-imds.c index e208c9638fa..8cc871796eb 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][lock:fake-kms]", + _test_with_mock_server, + NULL, + NULL, + have_mock_server_env); } diff --git a/src/libmongoc/tests/test-service-gcp.c b/src/libmongoc/tests/test-service-gcp.c index 724c00eace7..db634b92f71 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][lock:fake-kms]", + _test_with_mock_server, + NULL, + NULL, + have_mock_server_env); }