diff options
31 files changed, 1227 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ed344fc --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +BasedOnStyle: LLVM +UseTab: Never +TabWidth: 2 +IndentWidth: 2 + +Language: Cpp +Standard: Auto + +ColumnLimit: 70 +MaxEmptyLinesToKeep: 1 +SpacesBeforeTrailingComments: 1 +AccessModifierOffset: -2 +PenaltyBreakAssignment: 100 +PenaltyReturnTypeOnItsOwnLine: 200 + +IndentCaseLabels: true +IndentExternBlock: NoIndent +IndentGotoLabels: false +IndentPPDirectives: AfterHash +IndentRequires: true +IndentWrappedFunctionNames: false +NamespaceIndentation: All + +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: true +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInSquareBrackets: false + +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: Consecutive +AlignConsecutiveBitFields: Consecutive +AlignConsecutiveDeclarations: Consecutive +AlignEscapedNewlines: Left +AllowShortLambdasOnASingleLine: All +AllowShortFunctionsOnASingleLine: Empty +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BreakBeforeConceptDeclarations: true +BreakConstructorInitializers: AfterColon +Cpp11BracedListStyle: false +EmptyLineBeforeAccessModifier: LogicalBlock +FixNamespaceComments: false +PointerAlignment: Right +ReflowComments: true +SortIncludes: CaseInsensitive +SortUsingDeclarations: true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..50aad28 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,120 @@ +name: Build and test + +on: + push: + branches: [ dev, stable ] + pull_request: + branches: [ dev, stable ] + schedule: + - cron: '0 0 */16 * *' + workflow_dispatch: + +env: + BUILD_TYPE: Debug + +jobs: + build: + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-2019 ] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + + - name: Set up CMake 3.18 + uses: jwlawson/actions-setup-cmake@v1.8 + with: + cmake-version: 3.18 + + - name: Install gcovr + if: runner.os != 'Windows' + run: | + python3 -m pip install gcovr==5.0 + + - name: Install OpenCppCoverage + if: runner.os == 'Windows' + shell: bash + run: | + choco install opencppcoverage + echo "C:/Program Files/OpenCppCoverage" >> $GITHUB_PATH + + - name: Build + if: runner.os != 'Windows' + run: | + cmake -D CMAKE_BUILD_TYPE=$BUILD_TYPE -B build -S . + cmake --build build --config $BUILD_TYPE + + - name: Build with Visual Studio + if: runner.os == 'Windows' + shell: bash + run: | + cmake -B build -S . + cmake --build build --config $BUILD_TYPE + + - name: Run tests & generate coverage on Linux + if: runner.os == 'Linux' + working-directory: ${{ github.workspace }}/build + run: | + ctest -V -C $BUILD_TYPE + gcovr --gcov-executable gcov-11 -j $(nproc) --delete --root ../source/ --exclude '\.\./source/test/' --print-summary --xml-pretty --xml coverage.xml . + + - name: Run tests & generate coverage on macOS + if: runner.os == 'macOS' + working-directory: ${{ github.workspace }}/build + run: | + ctest -V -C $BUILD_TYPE + gcovr --gcov-executable gcov-11 -j 2 --delete --root ../source/ --exclude '\.\./source/test/' --print-summary --xml-pretty --xml coverage.xml . + + - name: Run tests & generate coverage on Windows + if: runner.os == 'Windows' + shell: bash + working-directory: ${{ github.workspace }}/build + run: | + OpenCppCoverage.exe --sources source\\* --excluded_sources source\\test\\* --export_type cobertura:coverage.xml --cover_children -- ctest -V -C $BUILD_TYPE + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + with: + files: ./build/coverage.xml + flags: ${{ runner.os }} + name: ${{ runner.os }} build + fail_ci_if_error: false + + integration: + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-2019 ] + name: [ fetch_content, find_package ] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + + - name: Set up CMake 3.18 + uses: jwlawson/actions-setup-cmake@v1.8 + with: + cmake-version: 3.18 + + - name: Build with GCC + if: runner.os != 'Windows' + run: | + cmake -D CMAKE_BUILD_TYPE=$BUILD_TYPE -B build -S . + cmake --build build --config $BUILD_TYPE + working-directory: ./source/test/integration/${{ matrix.name }} + + - name: Build with Visual Studio + if: runner.os == 'Windows' + shell: bash + run: | + cmake -B build -S . + cmake --build build --config $BUILD_TYPE + working-directory: ./source/test/integration/${{ matrix.name }} + + - name: Run + shell: bash + run: | + ctest -V -C $BUILD_TYPE + working-directory: ./source/test/integration/${{ matrix.name }}/build diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml new file mode 100644 index 0000000..74e8d4a --- /dev/null +++ b/.github/workflows/cleanup.yml @@ -0,0 +1,21 @@ +name: Cleanup + +on: + schedule: + - cron: '0 0 */16 * *' + workflow_dispatch: + +jobs: + delete_runs: + permissions: + actions: write + runs-on: ubuntu-latest + + steps: + - name: Delete workflow runs + uses: Mattraks/delete-workflow-runs@main + with: + token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + retain_days: 30 + keep_minimum_runs: 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f2be3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/*build*/ + +/.vs/ +/.idea/ +*.sln +*.vcxproj +*.filters +*.user diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..79e4216 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,129 @@ +cmake_minimum_required(VERSION 3.16) + +option(KIT_ENABLE_LIBRARY "Enable library" ON) +option(KIT_ENABLE_TESTING "Enable testing" ON) + +set(KIT_PROJECT kit) +set(KIT_LIBRARY kit) +set(KIT_TEST_LIBRARY kit_test) +set(KIT_TEST_SUITE kit_test_suite) +set(KIT_TARGETS kit-targets) +set(KIT_UNITTESTS kit-unittests) + +if(NOT DEFINED CMAKE_BUILD_PARALLEL_LEVEL) + set(CMAKE_BUILD_PARALLEL_LEVEL 4) +endif() + +project( + ${KIT_PROJECT} + VERSION 0.1.1 + DESCRIPTION "Kit" + LANGUAGES C) + +if(KIT_ENABLE_LIBRARY OR KIT_ENABLE_TESTING) + add_library(${KIT_LIBRARY} STATIC) + add_library(${KIT_PROJECT}::${KIT_LIBRARY} ALIAS ${KIT_LIBRARY}) + target_include_directories( + ${KIT_LIBRARY} INTERFACE + $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/source>) + target_compile_features(${KIT_LIBRARY} PUBLIC c_std_11) + + add_library(${KIT_TEST_LIBRARY} STATIC) + add_library(${KIT_PROJECT}::${KIT_TEST_LIBRARY} ALIAS ${KIT_TEST_LIBRARY}) + target_include_directories( + ${KIT_TEST_LIBRARY} INTERFACE + $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/source>) + target_compile_features(${KIT_TEST_LIBRARY} PUBLIC c_std_11) +endif() + +enable_testing() + +if(KIT_ENABLE_TESTING) + add_executable(${KIT_TEST_SUITE}) + add_executable(${KIT_PROJECT}::${KIT_TEST_SUITE} ALIAS ${KIT_TEST_SUITE}) + target_compile_features(${KIT_TEST_SUITE} PRIVATE c_std_11) + target_link_libraries(${KIT_TEST_SUITE} PRIVATE ${KIT_LIBRARY} ${KIT_TEST_LIBRARY}) + + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options( + ${KIT_LIBRARY} PUBLIC + -fsanitize=undefined,address + --coverage -O0 -g) + target_link_options( + ${KIT_LIBRARY} PUBLIC + -fsanitize=undefined,address + --coverage) + endif() + + add_test( + NAME ${KIT_UNITTESTS} + COMMAND ${KIT_TEST_SUITE}) + + set_tests_properties( + ${KIT_UNITTESTS} + PROPERTIES + TIMEOUT "30") +endif() + +add_subdirectory(source) + +if(KIT_ENABLE_LIBRARY) + include(GNUInstallDirs) + + file( + GLOB_RECURSE headers_ + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/source/kit + ${CMAKE_CURRENT_SOURCE_DIR}/source/kit/*.h) + + foreach(path_ ${headers_}) + get_filename_component(dir_ "${path_}" DIRECTORY) + + install( + FILES ${CMAKE_CURRENT_SOURCE_DIR}/source/kit/${path_} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/kit/${dir_}) + endforeach() + + unset(headers_) + unset(path_) + unset(dir_) + + install( + TARGETS ${KIT_LIBRARY} ${KIT_TEST_LIBRARY} + EXPORT ${KIT_TARGETS} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + + install( + EXPORT ${KIT_TARGETS} + FILE ${KIT_TARGETS}.cmake + NAMESPACE ${PROJECT_NAME}:: + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") + + export( + EXPORT ${KIT_TARGETS} + FILE "${CMAKE_CURRENT_BINARY_DIR}/cmake/${KIT_TARGETS}.cmake" + NAMESPACE ${PROJECT_NAME}::) + + include(CMakePackageConfigHelpers) + + string(TOLOWER ${PROJECT_NAME} project_lower_) + + configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/${project_lower_}-config.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) + + write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/${project_lower_}-config-version.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY AnyNewerVersion) + + install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/${project_lower_}-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${project_lower_}-config-version.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) + + unset(project_lower_) +endif() diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..deacc03 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,8 @@ +## Minimalist Code of Conduct +We believe that an elaborate code of conduct invites a lot of fighting over intricate details, much like law, and we do not have the resources to build up the equivalent of a legal system. Therefore, we prefer keeping our rules as short as possible and filling the gaps with the mortar of human interaction: empathy. + +All we ask of members of this project is this: +1. Please treat each other with respect and understanding. +2. Please respect our wish to not serve as a stage for disputes about fairness or personal differences. + +If you can agree to these conditions, your contributions are welcome. If you can not, please don’t spoil it for the rest of us. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a3c6315 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +## Contributing +To contribute, follow these steps: +1. Find an applicable issue or open a new one with feature proposal or bug spotting. +2. Fork the repository. +3. Write tests for the issue. +4. Implement your solution for the issue. +5. Make sure all tests pass and cover your code. +6. Make a pull-request to the `dev` branch or to the corresponding feature branch if there is any. @@ -0,0 +1,19 @@ +Copyright (c) 2022 Mitya Selivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c71b735 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# kit +- Async function +- Unit-testing diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..8478b39 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,6 @@ +coverage: + status: + project: + default: + threshold: 2% + patch: off diff --git a/config.cmake.in b/config.cmake.in new file mode 100644 index 0000000..cc1cb07 --- /dev/null +++ b/config.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/@AF_TARGETS@.cmake") diff --git a/gen_cmake.py b/gen_cmake.py new file mode 100755 index 0000000..3d1a520 --- /dev/null +++ b/gen_cmake.py @@ -0,0 +1,89 @@ +#!/usr/bin/python3 + +import os, glob + +def get_subdirs(folder: str): + dirs = list() + for f in glob.glob(os.path.join(folder, '*', '')): + dirs.append(os.path.basename(os.path.normpath(f))) + return dirs + +def get_files(folder: str, ext: str): + files = list() + for f in glob.glob(os.path.join(folder, ext)): + files.append(os.path.basename(f)) + return files + +def check_subdirs(folder): + for r, d, f in os.walk(folder): + for file in f: + return True + return False + +def print_list(s: list, offset: int): + buf = '' + char_count = offset + for i in range(char_count - 1): + buf += ' ' + for f in s: + char_count += len(f) + 1 + if char_count >= 64: + char_count = 0 + if char_count == 0: + buf += '\n' + for i in range(offset - 1): + buf += ' ' + char_count = offset - 1 + buf += ' ' + f + return buf + +def print_sources(folder: str, target_name: str): + buf = '' + srcs = get_files(folder, '*.c') + hdrs = get_files(folder, '*.h') + if len(srcs) > 0 or len(hdrs) > 0: + buf += 'target_sources(\n ' + target_name + if len(srcs) > 0: + buf += '\n PRIVATE\n' + print_list(srcs, 6) + if len(hdrs) > 0: + buf += '\n PUBLIC' + for f in hdrs: + buf += '\n $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/' + f + '>' + buf += ')\n' + return buf + +def print_subdirs(folder: str): + buf = '' + dirs = get_subdirs(folder) + for f in dirs: + if check_subdirs(os.path.join(folder, f)): + buf += 'add_subdirectory(' + f + ')\n' + return buf + +def write_subdirs(folder: str, target_name: str): + if check_subdirs(folder): + out = open(os.path.join(folder, 'CMakeLists.txt'), 'w') + out.write(print_sources(folder, target_name)) + out.write(print_subdirs(folder)) + out.close() + + for dir in get_subdirs(folder): + write_subdirs(os.path.join(folder, dir), target_name) + +def clean_subdirs(folder: str): + for r, d, f in os.walk(folder): + for file in f: + if file == 'CMakeLists.txt': + os.remove(os.path.join(r, file)) + +def gen_cmake(folder: str, target_name: str): + clean_subdirs(folder) + write_subdirs(folder, target_name) + +def main(): + gen_cmake(os.path.join('source', 'kit'), '${KIT_LIBRARY}') + gen_cmake(os.path.join('source', 'kit_test'), '${KIT_TEST_LIBRARY}') + gen_cmake(os.path.join('source', 'test', 'unittests'), '${KIT_TEST_SUITE}') + +if __name__ == '__main__': + main() diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt new file mode 100644 index 0000000..1444b6a --- /dev/null +++ b/source/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(kit) +add_subdirectory(kit_test) + +if(KIT_ENABLE_TESTING) + add_subdirectory(test) +endif() diff --git a/source/kit/CMakeLists.txt b/source/kit/CMakeLists.txt new file mode 100644 index 0000000..24d1aed --- /dev/null +++ b/source/kit/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources( + ${KIT_LIBRARY} + PRIVATE + async_function.c + PUBLIC + $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/async_function.h>) diff --git a/source/kit/async_function.c b/source/kit/async_function.c new file mode 100644 index 0000000..a62114d --- /dev/null +++ b/source/kit/async_function.c @@ -0,0 +1 @@ +#include "async_function.h" diff --git a/source/kit/async_function.h b/source/kit/async_function.h new file mode 100644 index 0000000..77a88ef --- /dev/null +++ b/source/kit/async_function.h @@ -0,0 +1,234 @@ +#ifndef AF_AF_H +#define AF_AF_H + +#ifdef __cplusplus +extern "C" { +#endif + +enum af_request { + af_request_resume, + af_request_join, + af_request_resume_and_join, + af_request_execute +}; + +typedef struct { + int _; +} af_void; + +typedef void (*af_state_machine)(void *self_void_, int request_); +typedef void (*af_execute)(void *state, void *coro_state, + int request); + +typedef struct { + long long _internal; + void *state; + af_execute execute; +} af_execution_context; + +typedef struct { + int _index; + af_state_machine _state_machine; + af_execution_context _context; +} af_type_void; + +#ifndef AF_DISABLE_SELF_SHORTCUT +# define af self-> +#endif + +#define AF_INTERNAL(coro_) (*((af_type_void *) (coro_))) + +#define AF_STATE(ret_type_, name_, ...) \ + struct name_##_coro_state_ { \ + int _index; \ + af_state_machine _state_machine; \ + af_execution_context _context; \ + ret_type_ return_value; \ + __VA_ARGS__ \ + } + +#define AF_DECL(name_) \ + void name_##_coro_(void *self_void_, int request_) + +#define CORO_IMPL(name_) \ + AF_DECL(name_) { \ + struct name_##_coro_state_ *self = \ + (struct name_##_coro_state_ *) self_void_; \ + if (request_ != af_request_execute) { \ + if (self->_context.execute != NULL) \ + self->_context.execute(self->_context.state, self_void_, \ + request_); \ + else if (request_ == af_request_join || \ + request_ == af_request_resume_and_join) \ + self->_state_machine(self_void_, af_request_execute); \ + return; \ + } \ + switch (self->_index) { \ + case 0:; + +#define AF_LINE() __LINE__ + +#define CORO_END \ + } \ + self->_index = -1; \ + } + +#define CORO_DECL(ret_type_, name_, ...) \ + AF_STATE(ret_type_, name_, __VA_ARGS__); \ + AF_DECL(name_) + +#define CORO(ret_type_, name_, ...) \ + AF_STATE(ret_type_, name_, __VA_ARGS__); \ + CORO_IMPL(name_) + +#define CORO_DECL_VOID(name_, ...) \ + CORO_DECL(af_void, name_, __VA_ARGS__) + +#define CORO_VOID(name_, ...) CORO(af_void, name_, __VA_ARGS__) + +#define AF_YIELD(...) \ + { \ + self->_index = AF_LINE(); \ + self->return_value = __VA_ARGS__; \ + return; \ + case AF_LINE():; \ + } + +#define AF_YIELD_VOID \ + { \ + self->_index = AF_LINE(); \ + return; \ + case AF_LINE():; \ + } + +#define AF_RETURN(...) \ + { \ + self->_index = -1; \ + self->return_value = __VA_ARGS__; \ + return; \ + } + +#define AF_RETURN_VOID \ + { \ + self->_index = -1; \ + return; \ + } + +#define AF_AWAIT(promise_) \ + { \ + case AF_LINE(): \ + if ((promise_)._index != -1) { \ + self->_index = AF_LINE(); \ + (promise_)._state_machine(&(promise_), \ + af_request_resume_and_join); \ + } \ + if ((promise_)._index != -1) \ + return; \ + } + +#define AF_YIELD_AWAIT(promise_) \ + { \ + case AF_LINE(): \ + if ((promise_)._index != -1) { \ + self->_index = AF_LINE(); \ + (promise_)._state_machine(&(promise_), \ + af_request_resume_and_join); \ + self->return_value = (promise_).return_value; \ + return; \ + } \ + } + +#define AF_TYPE(coro_) struct coro_##_coro_state_ + +#define AF_INITIAL(coro_) \ + ._index = 0, ._state_machine = coro_##_coro_, \ + ._context = { .state = NULL, .execute = NULL } + +#define AF_CREATE(promise_, coro_, ...) \ + AF_TYPE(coro_) \ + promise_ = { AF_INITIAL(coro_), __VA_ARGS__ } + +#define AF_INIT(promise_, coro_, ...) \ + { \ + AF_CREATE(af_temp_, coro_, __VA_ARGS__); \ + (promise_) = af_temp_; \ + } + +#define AF_EXECUTION_CONTEXT(promise_, ...) \ + { \ + af_execution_context af_temp_ = { ._internal = 0, __VA_ARGS__ }; \ + (promise_)._context = af_temp_; \ + } + +#define AF_RESUME(promise_) \ + (promise_)._state_machine(&(promise_), af_request_resume) + +#define AF_RESUME_N(promises_, size_) \ + for (int af_index_ = 0; af_index_ < (size_); af_index_++) \ + AF_RESUME((promises_)[af_index_]) + +#define AF_JOIN(promise_) \ + ((promise_)._state_machine(&(promise_), af_request_join), \ + (promise_).return_value) + +#define AF_JOIN_N(promises_, size_) \ + for (int af_index_ = 0; af_index_ < (size_); af_index_++) \ + AF_JOIN((promises_)[af_index_]) + +#define AF_RESUME_AND_JOIN(promise_) \ + ((promise_)._state_machine(&(promise_), \ + af_request_resume_and_join), \ + (promise_).return_value) + +#define AF_RESUME_AND_JOIN_N(promises_, size_) \ + AF_RESUME_N((promises_), (size_)); \ + AF_JOIN_N((promises_), (size_)) + +#define AF_RESUME_ALL(promises_) \ + AF_RESUME_N((promises_), sizeof(promises_) / sizeof((promises_)[0])) + +#define AF_JOIN_ALL(promises_) \ + AF_JOIN_N((promises_), sizeof(promises_) / sizeof((promises_)[0])) + +#define AF_RESUME_AND_JOIN_ALL(promises_) \ + AF_RESUME_AND_JOIN_N((promises_), \ + sizeof(promises_) / sizeof((promises_)[0])) + +#define AF_FINISHED(promise_) ((promise_)._index == -1) + +#define AF_FINISHED_N(return_, promises_, size_) \ + { \ + (return_) = true; \ + for (int af_index_ = 0; af_index_ < (size_); af_index_++) \ + if (!AF_FINISHED((promises_)[af_index_])) { \ + (return_) = false; \ + break; \ + } \ + } + +#define AF_FINISHED_ALL(return_, promises_, size_) \ + AF_FINISHED_N((return_), (promises_), \ + sizeof(promises_) / sizeof((promises_)[0])) + +#define AF_AWAIT_N(promises_, size_) \ + { \ + case AF_LINE(): \ + self->_index = AF_LINE(); \ + AF_RESUME_AND_JOIN_N((promises_), (size_)); \ + bool af_done_; \ + AF_FINISHED_N(af_done_, (promises_), (size_)); \ + if (!af_done_) \ + return; \ + } + +#define AF_AWAIT_ALL(promises_) \ + AF_AWAIT_N((promises_), sizeof(promises_) / sizeof((promises_)[0])) + +#define AF_EXECUTE(coro_state_) \ + AF_INTERNAL(coro_state_)._state_machine(coro, af_request_execute) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/source/kit_test/CMakeLists.txt b/source/kit_test/CMakeLists.txt new file mode 100644 index 0000000..f7315a1 --- /dev/null +++ b/source/kit_test/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources( + ${KIT_TEST_LIBRARY} + PRIVATE + run_tests.c + PUBLIC + $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/test.h>) diff --git a/source/kit_test/run_tests.c b/source/kit_test/run_tests.c new file mode 100644 index 0000000..191982a --- /dev/null +++ b/source/kit_test/run_tests.c @@ -0,0 +1,112 @@ +#include "test.h" + +#include <stdio.h> +#include <time.h> + +struct kit_tests_list kit_tests_list = { 0 }; + +static void report(int i, char const *file, int line, bool ok) { + int const n = kit_tests_list.tests[i].assertions++; + + kit_tests_list.tests[i].file[n] = file; + kit_tests_list.tests[i].line[n] = line; + kit_tests_list.tests[i].status[n] = ok; +} + +static long long ns_to_ms(long long ns) { + return (ns + 500000) / 1000000; +} + +static long long sec_to_ms(long long sec) { + return 1000 * sec; +} + +enum code_value { white, yellow, red, green }; + +static void color_code(bool term_color, int c) { + if (term_color) { + if (c == white) + printf("\x1b[37m"); + if (c == yellow) + printf("\x1b[33m"); + if (c == red) + printf("\x1b[31m"); + if (c == green) + printf("\x1b[32m"); + } +} + +int kit_run_tests(int argc, char **argv) { + int fail_test_count = 0; + int fail_assertion_count = 0; + int total_assertion_count = 0; + int status = 0; + bool term_color = true; + + for (int i = 0; i < argc; i++) + if (strcmp("--no-term-color", argv[i]) == 0) + term_color = false; + + for (int i = 0; i < kit_tests_list.size; i++) { + color_code(term_color, yellow); + printf("[ RUN... ] %s ", kit_tests_list.tests[i].test_name); + color_code(term_color, white); + + struct timespec begin, end; + timespec_get(&begin, TIME_UTC); + + kit_tests_list.tests[i].test_fn(i, report); + + timespec_get(&end, TIME_UTC); + int duration = (int) (ns_to_ms(end.tv_nsec - begin.tv_nsec) + + sec_to_ms(end.tv_sec - begin.tv_sec)); + + printf("\r"); + + bool test_status = true; + + for (int j = 0; j < kit_tests_list.tests[i].assertions; j++) + if (kit_tests_list.tests[i].status[j] == false) { + fail_assertion_count++; + test_status = false; + } + + total_assertion_count += kit_tests_list.tests[i].assertions; + + if (test_status == false) { + color_code(term_color, red); + printf("[ RUN ] %s\n", kit_tests_list.tests[i].test_name); + printf("[ FAILED ] %s - %d ms\n", + kit_tests_list.tests[i].test_name, duration); + color_code(term_color, white); + fail_test_count++; + status = 1; + } else { + color_code(term_color, green); + printf("[ RUN ] %s\n", kit_tests_list.tests[i].test_name); + printf("[ OK ] %s - %d ms\n", + kit_tests_list.tests[i].test_name, duration); + color_code(term_color, white); + } + } + + printf("\n%d of %d tests passed.\n", + kit_tests_list.size - fail_test_count, kit_tests_list.size); + + printf("%d of %d assertions passed.\n\n", + total_assertion_count - fail_assertion_count, + total_assertion_count); + + if (status != 0) { + for (int i = 0; i < kit_tests_list.size; i++) + for (int j = 0; j < kit_tests_list.tests[i].assertions; j++) + if (!kit_tests_list.tests[i].status[j]) + printf("Assertion on line %d in \"%s\" failed\n", + kit_tests_list.tests[i].line[j], + kit_tests_list.tests[i].file[j]); + + printf("\n"); + } + + return status; +} diff --git a/source/kit_test/test.h b/source/kit_test/test.h new file mode 100644 index 0000000..da6c59b --- /dev/null +++ b/source/kit_test/test.h @@ -0,0 +1,96 @@ +#ifndef KIT_TEST_TEST_H +#define KIT_TEST_TEST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stdbool.h> +#include <stddef.h> +#include <string.h> + +#ifndef KIT_TEST_FILE +# define kit_test +#endif + +#ifndef KIT_TESTS_SIZE_LIMIT +# define KIT_TESTS_SIZE_LIMIT 0x1000 +#endif + +#ifndef KIT_TEST_ASSERTIONS_LIMIT +# define KIT_TEST_ASSERTIONS_LIMIT 0x50 +#endif + +#ifndef KIT_TEST_STRING_SIZE +# define KIT_TEST_STRING_SIZE 0x100 +#endif + +typedef void (*kit_test_report)(int, char const *file, int line, + bool); +typedef void (*kit_test_function)(int, kit_test_report); + +struct kit_test_case { + char test_name[KIT_TEST_STRING_SIZE]; + kit_test_function test_fn; + int assertions; + char const *file[KIT_TEST_ASSERTIONS_LIMIT]; + int line[KIT_TEST_ASSERTIONS_LIMIT]; + bool status[KIT_TEST_ASSERTIONS_LIMIT]; +}; + +struct kit_tests_list { + int size; + struct kit_test_case tests[KIT_TESTS_SIZE_LIMIT]; +}; + +extern struct kit_tests_list kit_tests_list; + +#ifdef _MSC_VER +# pragma section(".CRT$XCU", read) +# define KIT_TEST_ON_START_2(f, p) \ + static void f(void); \ + __declspec(allocate(".CRT$XCU")) void (*f##_)(void) = f; \ + __pragma(comment(linker, "/include:" p #f "_")) static void f( \ + void) +# ifdef _WIN64 +# define KIT_TEST_ON_START(f) KIT_TEST_ON_START_2(f, "") +# else +# define KIT_TEST_ON_START(f) KIT_TEST_ON_START_2(f, "_") +# endif +#else +# define KIT_TEST_ON_START(f) \ + static void f(void) __attribute__((constructor)); \ + static void f(void) +#endif + +#define KIT_TEST_CONCAT4(a, b, c, d) a##b##c##d +#define KIT_TEST_CONCAT3(a, b, c) KIT_TEST_CONCAT4(a, b, _, c) + +#define TEST(name) \ + static void KIT_TEST_CONCAT3(kit_test_run_, __LINE__, \ + KIT_TEST_FILE)(int, kit_test_report); \ + KIT_TEST_ON_START( \ + KIT_TEST_CONCAT3(kit_test_case_, __LINE__, KIT_TEST_FILE)) { \ + int n = kit_tests_list.size; \ + if (n < KIT_TESTS_SIZE_LIMIT) { \ + kit_tests_list.size++; \ + kit_tests_list.tests[n].test_fn = KIT_TEST_CONCAT3( \ + kit_test_run_, __LINE__, KIT_TEST_FILE); \ + strcpy(kit_tests_list.tests[n].test_name, name); \ + kit_tests_list.tests[n].assertions = 0; \ + } \ + } \ + static void KIT_TEST_CONCAT3(kit_test_run_, __LINE__, \ + KIT_TEST_FILE)( \ + int kit_test_index_, kit_test_report kit_test_report_) + +#define REQUIRE(ok) \ + kit_test_report_(kit_test_index_, __FILE__, __LINE__, (ok)) + +int kit_run_tests(int argc, char **argv); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/source/test/CMakeLists.txt b/source/test/CMakeLists.txt new file mode 100644 index 0000000..740981e --- /dev/null +++ b/source/test/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(unittests) diff --git a/source/test/integration/fetch_content/.gitignore b/source/test/integration/fetch_content/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/source/test/integration/fetch_content/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/source/test/integration/fetch_content/CMakeLists.txt b/source/test/integration/fetch_content/CMakeLists.txt new file mode 100644 index 0000000..efa83d0 --- /dev/null +++ b/source/test/integration/fetch_content/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.16) +set(NAME kit-integration-fetch-content) +project(${NAME} C) + +include(FetchContent) +FetchContent_Declare( + kit + GIT_REPOSITORY https://github.com/automainint/kit.git + GIT_TAG dev) +set(KIT_ENABLE_TESTING OFF) +FetchContent_MakeAvailable(kit) + +add_executable(${NAME}) +target_sources(${NAME} PRIVATE main.c) +target_link_libraries(${NAME} PRIVATE kit::kit) + +enable_testing() +add_test(NAME ${NAME}-run COMMAND ${NAME}) +set_tests_properties(${NAME}-run PROPERTIES TIMEOUT "15") diff --git a/source/test/integration/fetch_content/main.c b/source/test/integration/fetch_content/main.c new file mode 100644 index 0000000..c9dc882 --- /dev/null +++ b/source/test/integration/fetch_content/main.c @@ -0,0 +1,5 @@ +#include <kit/async_function.h> + +int main() { + return 0; +} diff --git a/source/test/integration/find_package/.gitignore b/source/test/integration/find_package/.gitignore new file mode 100644 index 0000000..0ba45dd --- /dev/null +++ b/source/test/integration/find_package/.gitignore @@ -0,0 +1,2 @@ +/build/ +/install/ diff --git a/source/test/integration/find_package/CMakeLists.txt b/source/test/integration/find_package/CMakeLists.txt new file mode 100644 index 0000000..f21cdae --- /dev/null +++ b/source/test/integration/find_package/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) +set(NAME kit-integration-find-package) +project(${NAME}-root) + +include(ExternalProject) + +ExternalProject_Add( + kit + GIT_REPOSITORY https://github.com/automainint/kit.git + GIT_TAG dev + CMAKE_ARGS + -D CMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_SOURCE_DIR}/install + -D KIT_ENABLE_TESTING=OFF) + +ExternalProject_Add( + ${NAME} + SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/source + CMAKE_ARGS + -D CMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_SOURCE_DIR}/install) + +ExternalProject_Add_StepDependencies(${NAME} build kit) + +include(GNUInstallDirs) + +enable_testing() + +add_test( + NAME ${NAME}-run + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/install/${CMAKE_INSTALL_BINDIR}/${NAME}${CMAKE_EXECUTABLE_SUFFIX}) + +set_tests_properties( + ${NAME}-run + PROPERTIES + TIMEOUT "15") diff --git a/source/test/integration/find_package/source/CMakeLists.txt b/source/test/integration/find_package/source/CMakeLists.txt new file mode 100644 index 0000000..2dc34a1 --- /dev/null +++ b/source/test/integration/find_package/source/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) +set(NAME kit-integration-find-package) +project(${NAME} C) + +find_package(kit REQUIRED) + +add_executable(${NAME}) +target_sources(${NAME} PRIVATE main.c) +target_link_libraries(${NAME} PRIVATE kit::kit) +install(TARGETS ${NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/source/test/integration/find_package/source/main.c b/source/test/integration/find_package/source/main.c new file mode 100644 index 0000000..c9dc882 --- /dev/null +++ b/source/test/integration/find_package/source/main.c @@ -0,0 +1,5 @@ +#include <kit/async_function.h> + +int main() { + return 0; +} diff --git a/source/test/unittests/CMakeLists.txt b/source/test/unittests/CMakeLists.txt new file mode 100644 index 0000000..d5cc573 --- /dev/null +++ b/source/test/unittests/CMakeLists.txt @@ -0,0 +1,4 @@ +target_sources( + ${KIT_TEST_SUITE} + PRIVATE + async_function.test.c main.test.c) diff --git a/source/test/unittests/async_function.test.c b/source/test/unittests/async_function.test.c new file mode 100644 index 0000000..1f50445 --- /dev/null +++ b/source/test/unittests/async_function.test.c @@ -0,0 +1,200 @@ +#include "../../kit/async_function.h" + +#define KIT_TEST_FILE async_function_test +#include "../../kit_test/test.h" + +CORO(int, test_foo) { + AF_RETURN(42); +} +CORO_END + +CORO(int, test_bar) { + AF_YIELD_VOID; + AF_RETURN(42); +} +CORO_END + +CORO(int, test_gen, int i; int min; int max;) { + for (af i = af min; af i < af max; af i++) AF_YIELD(af i); + AF_RETURN(af max); +} +CORO_END + +CORO_VOID(test_task) { + AF_YIELD_VOID; + AF_YIELD_VOID; + AF_RETURN_VOID; +} +CORO_END + +CORO_VOID(test_nest_task, AF_TYPE(test_task) promise;) { + AF_INIT(af promise, test_task); + AF_AWAIT(af promise); + AF_AWAIT(af promise); + AF_AWAIT(af promise); +} +CORO_END + +CORO(int, test_nest_generator, AF_TYPE(test_gen) promise;) { + AF_INIT(af promise, test_gen, .min = 1, .max = 3); + AF_YIELD_AWAIT(af promise); +} +CORO_END + +CORO(int, test_join_multiple, AF_TYPE(test_bar) promises[3];) { + for (int i = 0; i < 3; i++) + AF_INIT(af promises[i], test_bar, .return_value = 0); + AF_RESUME_AND_JOIN_ALL(af promises); + AF_RETURN(af promises[0].return_value + + af promises[1].return_value + + af promises[2].return_value); +} +CORO_END + +CORO(int, test_await_multiple, AF_TYPE(test_bar) promises[3];) { + for (int i = 0; i < 3; i++) + AF_INIT(af promises[i], test_bar, .return_value = 0); + AF_AWAIT_ALL(af promises); + AF_RETURN(af promises[0].return_value + + af promises[1].return_value + + af promises[2].return_value); +} +CORO_END + +void test_execute_lazy(void *_, void *coro, int request) { + if (request == af_request_resume) + return; + AF_EXECUTE(coro); +} + +void test_execute_immediate(void *_, void *coro, int request) { + if (request == af_request_join) + return; + AF_EXECUTE(coro); +} + +TEST("coroutine create") { + AF_CREATE(promise, test_foo); + REQUIRE(!AF_FINISHED(promise)); +} + +TEST("coroutine init") { + AF_TYPE(test_foo) promise; + AF_INIT(promise, test_foo); + REQUIRE(!AF_FINISHED(promise)); +} + +TEST("coroutine init with value") { + AF_TYPE(test_foo) promise; + AF_INIT(promise, test_foo, .return_value = 42); + REQUIRE(promise.return_value == 42); + REQUIRE(!AF_FINISHED(promise)); +} + +TEST("coroutine resume") { + AF_CREATE(promise, test_foo, .return_value = -1); + AF_RESUME(promise); + REQUIRE(promise.return_value == -1); + REQUIRE(!AF_FINISHED(promise)); +} + +TEST("coroutine resume and join") { + AF_CREATE(promise, test_foo); + REQUIRE(AF_RESUME_AND_JOIN(promise) == 42); + REQUIRE(AF_FINISHED(promise)); +} + +TEST("coroutine resume and join manually") { + AF_CREATE(promise, test_foo); + AF_RESUME(promise); + REQUIRE(AF_JOIN(promise) == 42); + REQUIRE(AF_FINISHED(promise)); +} + +TEST("coroutine suspend") { + AF_CREATE(promise, test_bar, .return_value = 0); + REQUIRE(AF_RESUME_AND_JOIN(promise) == 0); + REQUIRE(AF_RESUME_AND_JOIN(promise) == 42); +} + +TEST("coroutine generator") { + AF_CREATE(promise, test_gen, .min = 10, .max = 15); + for (int i = 0; i <= 5; i++) + REQUIRE(AF_RESUME_AND_JOIN(promise) == 10 + i); +} + +TEST("coroutine status finished") { + AF_CREATE(promise, test_bar); + REQUIRE(!AF_FINISHED(promise)); + AF_RESUME_AND_JOIN(promise); + REQUIRE(!AF_FINISHED(promise)); + AF_RESUME_AND_JOIN(promise); + REQUIRE(AF_FINISHED(promise)); +} + +TEST("coroutine task") { + AF_CREATE(promise, test_task); + AF_RESUME_AND_JOIN(promise); + REQUIRE(!AF_FINISHED(promise)); + AF_RESUME_AND_JOIN(promise); + REQUIRE(!AF_FINISHED(promise)); + AF_RESUME_AND_JOIN(promise); + REQUIRE(AF_FINISHED(promise)); +} + +TEST("coroutine nested task") { + AF_CREATE(promise, test_nest_task); + AF_RESUME_AND_JOIN(promise); + REQUIRE(!AF_FINISHED(promise)); + AF_RESUME_AND_JOIN(promise); + REQUIRE(!AF_FINISHED(promise)); + AF_RESUME_AND_JOIN(promise); + REQUIRE(AF_FINISHED(promise)); +} + +TEST("coroutine nested generator") { + AF_CREATE(promise, test_nest_generator); + REQUIRE(AF_RESUME_AND_JOIN(promise) == 1); + REQUIRE(AF_RESUME_AND_JOIN(promise) == 2); + REQUIRE(AF_RESUME_AND_JOIN(promise) == 3); + REQUIRE(!AF_FINISHED(promise)); + AF_RESUME_AND_JOIN(promise); + REQUIRE(AF_FINISHED(promise)); +} + +TEST("coroutine join multiple") { + AF_CREATE(promise, test_join_multiple); + REQUIRE(AF_RESUME_AND_JOIN(promise) == 0); + REQUIRE(AF_FINISHED(promise)); +} + +TEST("coroutine await multiple") { + AF_CREATE(promise, test_await_multiple); + REQUIRE(AF_RESUME_AND_JOIN(promise) == 0); + REQUIRE(AF_RESUME_AND_JOIN(promise) == 42 * 3); + REQUIRE(AF_FINISHED(promise)); +} + +TEST("coroutine custom execution context lazy") { + AF_CREATE(promise, test_foo, .return_value = 0); + AF_EXECUTION_CONTEXT(promise, .state = NULL, + .execute = test_execute_lazy); + AF_RESUME(promise); + REQUIRE(promise.return_value == 0); + REQUIRE(!AF_FINISHED(promise)); + AF_JOIN(promise); + REQUIRE(promise.return_value == 42); + REQUIRE(AF_FINISHED(promise)); +} + +TEST("coroutine custom execution context immediate") { + AF_CREATE(promise, test_foo, .return_value = 0); + AF_EXECUTION_CONTEXT(promise, .state = NULL, + .execute = test_execute_immediate); + AF_RESUME(promise); + REQUIRE(promise.return_value == 42); + REQUIRE(AF_FINISHED(promise)); + AF_JOIN(promise); + REQUIRE(promise.return_value == 42); + REQUIRE(AF_FINISHED(promise)); +} diff --git a/source/test/unittests/main.test.c b/source/test/unittests/main.test.c new file mode 100644 index 0000000..b072c67 --- /dev/null +++ b/source/test/unittests/main.test.c @@ -0,0 +1,5 @@ +#include "../../kit_test/test.h" + +int main(int argc, char **argv) { + return kit_run_tests(argc, argv); +} |