diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e1a614..21936e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,63 +5,98 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 26) project(rsltest CXX) -add_library(rsltest SHARED) -add_library(rsltest_main SHARED) +########################## +## Primary test library ## +########################## +add_library(rsltest INTERFACE) set_target_properties(rsltest PROPERTIES OUTPUT_NAME rsltest) -target_compile_options(rsltest PUBLIC + +# TODO remove +target_compile_options(rsltest INTERFACE "-freflection-latest" # "-ftime-trace" # "-fconstexpr-steps=10000000" # required to scan the global namespace - "-ftemplate-backtrace-limit=0" ) -target_include_directories(rsltest PUBLIC - $ - $) - -find_package(libassert REQUIRED) -target_link_libraries(rsltest PUBLIC libassert::assert) -target_link_libraries(rsltest_main PUBLIC rsltest) +# TODO guard? this must happen for test TUs +include(FetchContent) +FetchContent_Declare( + libassert + GIT_REPOSITORY https://github.com/jeremy-rifkin/libassert.git + GIT_TAG v2.2.1 # + ) +FetchContent_MakeAvailable(libassert) +# TODO add cpptrace explicitly +# TODO use FetchContent for this? Can we assume rsl-util is findable through conan? find_package(rsl-util REQUIRED) + +target_link_libraries(rsltest INTERFACE rsl::util) +target_link_libraries(rsltest INTERFACE libassert::assert) + +file(GLOB_RECURSE RSL_TEST_HEADERS CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/include/**") + +target_sources(rsltest INTERFACE FILE_SET HEADERS + BASE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/include" + FILES ${RSL_TEST_HEADERS}) + +install(TARGETS rsltest + EXPORT rsltest-targets + FILE_SET HEADERS DESTINATION include) + + +########################## +## Test runner ## +########################## + +add_library(rsltest_main SHARED) find_package(rsl-config REQUIRED) find_package(rsl-xml REQUIRED) find_package(nlohmann_json REQUIRED) - -target_link_libraries(rsltest PUBLIC rsl::util) -target_link_libraries(rsltest PUBLIC rsl::config) +target_link_libraries(rsltest_main PUBLIC rsltest) +target_link_libraries(rsltest_main PUBLIC rsl::config) target_link_libraries(rsltest_main PUBLIC rsl::xml) target_link_libraries(rsltest_main PUBLIC nlohmann_json::nlohmann_json) +add_subdirectory(src) +install(TARGETS rsltest_main) -add_subdirectory(src) + +########################## +## Unit tests ## +########################## option(BUILD_TESTING "Enable tests" ON) option(ENABLE_COVERAGE "Enable coverage instrumentation" OFF) - if (BUILD_TESTING) message(STATUS "Building unit tests") - - add_executable(rsltest_test) - add_subdirectory(test) + include(${CMAKE_SOURCE_DIR}/cmake/rsl-test.cmake) # target_compile_definitions(rsltest_test PRIVATE RSL_TEST_NAMESPACE=testing) - target_link_libraries(rsltest_test PRIVATE rsltest) - target_link_libraries(rsltest_test PRIVATE rsltest_main) + add_library(test_dummy test/dummy.cpp) + target_enable_tests(test_dummy + NAMESPACE demo + SOURCES example/ + # PREFIX test_ + # PREFIX_REQUIRED + ) # enable_testing() # add_test(NAME rsltest_test COMMAND rsltest_example) endif() -add_subdirectory(ext) -install(TARGETS rsltest_main) -install(TARGETS rsltest) -install(DIRECTORY include/ DESTINATION include) -if (BUILD_EXAMPLES) - add_executable(example_test) - add_subdirectory(example) - target_link_libraries(example_test PRIVATE rsltest) - target_link_libraries(example_test PRIVATE rsltest_main) -endif() \ No newline at end of file +########################## +## Coverage hooks ## +########################## +# TODO +# add_subdirectory(ext) + +# if (BUILD_EXAMPLES) +# add_executable(example_test) +# add_subdirectory(example) +# target_link_libraries(example_test PRIVATE rsltest) +# target_link_libraries(example_test PRIVATE rsltest_main) +# endif() \ No newline at end of file diff --git a/cmake/rsl-test.cmake b/cmake/rsl-test.cmake new file mode 100644 index 0000000..b8bd905 --- /dev/null +++ b/cmake/rsl-test.cmake @@ -0,0 +1,281 @@ +macro(_impl_fix_bool VARIABLE) + if(${VARIABLE} MATCHES "(.*-NOTFOUND)|(IGNORE)|(NO)|(OFF)|0|(FALSE)" OR ${VARIABLE} STREQUAL "N" OR ${VARIABLE} STREQUAL "") + set(${VARIABLE} "false") + else() + set(${VARIABLE} "true") + endif() +endmacro() + +function(make_absolute_paths input_var) + set(input_list "${${input_var}}") + set(abs_list) + + foreach(p IN LISTS input_list) + get_filename_component(abs "${p}" REALPATH) + list(APPEND abs_list "${abs}") + endforeach() + + set(${input_var} "${abs_list}" PARENT_SCOPE) +endfunction() + +function(remove_empty_elements INPUT_LIST OUTPUT_LIST) + set(RESULT "") + foreach(ELEM ${${INPUT_LIST}}) + if(NOT ELEM STREQUAL "") + list(APPEND RESULT ${ELEM}) + endif() + endforeach() + set(${OUTPUT_LIST} "${RESULT}" PARENT_SCOPE) +endfunction() + +function(to_json_list _list _out) + set(_list_tmp "${${_list}}") + + list(TRANSFORM _list_tmp APPEND "\"") + list(TRANSFORM _list_tmp PREPEND "\"") + list(JOIN _list_tmp ", " _test_sources) + set(${_out} "[${_test_sources}]" PARENT_SCOPE) +endfunction() + +macro(_impl_property VARIABLE QUERY) + cmake_parse_arguments( + _PROP_ARG + "" # flags + "TARGET" # single value options + "OR" # multi value options + ${ARGN} + ) + if(NOT _PROP_ARG_TARGET) + set(_PROP_ARG_TARGET ${_TEST_ARG_TARGET}) + endif() + + get_target_property(${VARIABLE} ${_PROP_ARG_TARGET} ${QUERY}) + if(_PROP_ARG_OR) + foreach(_VAL IN LISTS _PROP_ARG_OR) + if(${VARIABLE} MATCHES ".*-NOTFOUND" OR ${VARIABLE} STREQUAL "") + set(${VARIABLE} "${_VAL}") + endif() + endforeach() + endif() + + if(${VARIABLE} MATCHES ".*-NOTFOUND" OR ${VARIABLE} STREQUAL "") + set(${VARIABLE} "") + endif() + unset(_PROP_ARG_TARGET) + unset(_PROP_ARG_OR) +endmacro() + +function(evaluate_config_expr CONFIG_EXPR OUTVAR) + set(RESULT "${CONFIG_EXPR}") + # Match all $<$:VALUE> patterns + string(REGEX MATCHALL "\\$<\\$:([^>]*)>" MATCHES "${CONFIG_EXPR}") + + foreach(MATCH ${MATCHES}) + # Extract CFG and VALUE + string(REGEX REPLACE "\\$<\\$:([^>]*)>" "\\1" CFG "${MATCH}") + string(REGEX REPLACE "\\$<\\$:([^>]*)>" "\\2" VALUE "${MATCH}") + + # Determine active configuration(s) + if(CMAKE_BUILD_TYPE STREQUAL "${CFG}" OR (DEFINED CMAKE_CONFIGURATION_TYPES AND CMAKE_CONFIGURATION_TYPES MATCHES "${CFG}")) + string(REPLACE "${MATCH}" "${VALUE}" RESULT "${RESULT}") + else() + string(REPLACE "${MATCH}" "" RESULT "${RESULT}") + endif() + endforeach() + + set(${OUTVAR} "${RESULT}" PARENT_SCOPE) +endfunction() + +function(collect_interface_usage INTERFACE_TARGET OUT_INCLUDES OUT_DEFS OUT_OPTIONS OUT_LIBS) + # Track visited targets + if(NOT DEFINED _visited_targets) + set(_visited_targets "") + endif() + + if(NOT TARGET ${INTERFACE_TARGET}) + return() + endif() + + list(FIND _visited_targets ${INTERFACE_TARGET} _already_visited) + if(NOT _already_visited EQUAL -1) + # Already visited, skip recursion + set(${OUT_INCLUDES} "" PARENT_SCOPE) + set(${OUT_DEFS} "" PARENT_SCOPE) + set(${OUT_OPTIONS} "" PARENT_SCOPE) + set(${OUT_LIBS} "" PARENT_SCOPE) + return() + endif() + list(APPEND _visited_targets ${INTERFACE_TARGET}) + set(_visited_targets "${_visited_targets}" PARENT_SCOPE) + + set(includes "") + set(defs "") + set(options "") + set(libs "") + + # Recurse over linked INTERFACE libraries + get_target_property(linked ${INTERFACE_TARGET} INTERFACE_LINK_LIBRARIES) + if(linked) + foreach(dep ${linked}) + evaluate_config_expr(${dep} dep_fixed) + # set(dep_fixed ${dep}) + # message("${dep} => ${dep_fixed}") + if(dep_fixed AND TARGET ${dep_fixed}) + # Recursive call, passing the same visited list + get_target_property(dep_type ${dep_fixed} TYPE) + collect_interface_usage("${dep_fixed}" dep_includes dep_defs dep_opts dep_libs) + list(APPEND includes ${dep_includes}) + list(APPEND defs ${dep_defs}) + list(APPEND options ${dep_opts}) + + if(dep_type STREQUAL "INTERFACE_LIBRARY") + list(APPEND libs ${dep_libs}) + else() + list(APPEND libs "$") + endif() + else() + # Non-targets (like system libraries) just append + list(APPEND libs ${dep}) + endif() + endforeach() + endif() + + # Append this target's own interface properties + get_target_property(my_includes ${INTERFACE_TARGET} INTERFACE_INCLUDE_DIRECTORIES) + if(my_includes) + list(APPEND includes ${my_includes}) + endif() + + get_target_property(my_defs ${INTERFACE_TARGET} INTERFACE_COMPILE_DEFINITIONS) + if(my_defs) + list(APPEND defs ${my_defs}) + endif() + + get_target_property(my_options ${INTERFACE_TARGET} INTERFACE_COMPILE_OPTIONS) + if(my_options) + list(APPEND options ${my_options}) + endif() + + # Return results + set(${OUT_INCLUDES} "${includes}" PARENT_SCOPE) + set(${OUT_DEFS} "${defs}" PARENT_SCOPE) + set(${OUT_OPTIONS} "${options}" PARENT_SCOPE) + set(${OUT_LIBS} "${libs}" PARENT_SCOPE) +endfunction() + + +function(target_enable_tests _TEST_ARG_TARGET) + cmake_parse_arguments( + _TEST_ARG + "" # flags + "NAMESPACE" # single value options + "SOURCES;DEPENDS" # multi value options + ${ARGN} + ) + + if(NOT TARGET ${_TEST_ARG_TARGET}) + message(FATAL_ERROR "Target ${_TEST_ARG_TARGET} does not exist") + endif() + + _impl_property(_lang LINKER_LANGUAGE OR CXX) + + set(_compiler "${CMAKE_${_lang}_COMPILER}") + string(TOUPPER "${CMAKE_BUILD_TYPE}" _build_type) + + set(_compile_flags "${CMAKE_${_lang}_FLAGS}") + if(CMAKE_BUILD_TYPE) + string(APPEND _compile_flags " ${CMAKE_${_lang}_FLAGS_${_build_type}}") + endif() + separate_arguments(_compile_flags NATIVE_COMMAND "${_compile_flags}") + + _impl_property(_std CXX_STANDARD OR "${CMAKE_CXX_STANDARD}") + _impl_property(_std_ext CXX_EXTENSIONS OR "${CMAKE_CXX_EXTENSIONS}" "ON") + _impl_fix_bool(_std_ext) + + _impl_property(_target_options COMPILE_OPTIONS) + _impl_property(_target_defs COMPILE_DEFINITIONS) + _impl_property(_target_includes INCLUDE_DIRECTORIES) + _impl_property(_link_opts LINK_OPTIONS) + _impl_property(_link_libs LINK_LIBRARIES) + + list(APPEND _compile_flags ${_target_options}) + + + # if (TARGET rsl-test-dependencies) + # get_target_property(_dep_target_type rsl-test-dependencies TYPE) + # if(NOT _dep_target_type STREQUAL "INTERFACE_LIBRARY") + # message("rsl-test-dependencies must be an INTERFACE library (got ${_dep_target_type})") + # # TODO if this isn't an interface library, it could be an old-style test => copy sources and deps + # endif() + + add_library(_deps_target INTERFACE) + target_link_libraries(_deps_target INTERFACE ${_TEST_ARG_TARGET}) + target_link_libraries(_deps_target INTERFACE rsltest) + + # TODO + # include(FetchContent) + # FetchContent_Declare( + # libassert + # GIT_REPOSITORY https://github.com/jeremy-rifkin/libassert.git + # GIT_TAG v2.2.1 # + # ) + # FetchContent_MakeAvailable(libassert) + # target_link_libraries(_deps_target INTERFACE libassert::assert) + + # _impl_property(_dep_link_libs INTERFACE_LINK_LIBRARIES TARGET _deps_target) + #_impl_property(_dep_includes INTERFACE_INCLUDE_DIRECTORIES TARGET _dep_target) + #_impl_property(_dep_defines INTERFACE_COMPILE_DEFINITIONS TARGET _dep_target) + + collect_interface_usage(_deps_target _transitive_inc _transitive_defs _transitive_opts _transitive_libs) + + list(APPEND _link_libs ${_dep_link_libs}) + list(APPEND _link_libs ${_transitive_libs}) + list(APPEND _target_includes ${_dep_includes}) + list(APPEND _target_includes ${_transitive_inc}) + list(APPEND _target_defs ${_dep_defines}) + list(APPEND _target_defs ${_transitive_defs}) + list(APPEND _compile_flags ${_transitive_opts}) + + make_absolute_paths(_TEST_ARG_SOURCES) + + to_json_list(_link_libs _link_libs) + to_json_list(_target_includes _target_includes) + to_json_list(_target_defs _target_defs) + to_json_list(_compile_flags _compile_flags) + to_json_list(_link_opts _link_opts) + to_json_list(_TEST_ARG_SOURCES _test_sources) + file(GENERATE + OUTPUT "test-runner.json" + CONTENT " + { + \"target\": \"${_TEST_ARG_TARGET}\", + \"options\": { + \"include_dirs\": ${_target_includes}, + \"compile_options\": ${_compile_flags}, + \"compile_definitions\": ${_target_defs}, + \"link_options\": ${_link_opts}, + \"link_libraries\": ${_link_libs} + }, + \"project\": { + \"test_path\": ${_test_sources}, + \"build_path\": \"${CMAKE_BINARY_DIR}\", + \"project_path\": \"${CMAKE_SOURCE_DIR}\", + \"namespace\": \"${_TEST_ARG_NAMESPACE}\" + }, + \"configurations\": { + \"default\": { + \"compiler_path\": \"${_compiler}\", + \"mode\": \"${_lang}\", + \"standard\": ${_std}, + \"gnu_extensions\": ${_std_ext}, + \"options\": {} + } + } + }") + + add_executable(${_TEST_ARG_TARGET}-test) + # target_sources(${_TEST_ARG_TARGET}-test PRIVATE "driver/driver.cpp") + target_link_libraries(${_TEST_ARG_TARGET}-test PUBLIC rsltest) + target_link_libraries(${_TEST_ARG_TARGET}-test PUBLIC rsltest_main) + target_link_libraries(${_TEST_ARG_TARGET}-test PUBLIC ${_TEST_ARG_TARGET}) +endfunction() diff --git a/conanfile.py b/conanfile.py index 2f4e3e7..7da51c8 100644 --- a/conanfile.py +++ b/conanfile.py @@ -71,7 +71,8 @@ def package(self): def package_info(self): self.cpp_info.set_property("cmake_file_name", "rsl-test") - + self.cpp_info.set_property("cmake_build_modules", ["cmake/rsl-test.cmake"]) + test = self.cpp_info.components["test"] test.set_property("cmake_target_name", "rsl::test") test.includedirs = ["include"] diff --git a/example/always_passes.cpp b/example/always_passes.cpp index 96bf6b8..2b4394e 100644 --- a/example/always_passes.cpp +++ b/example/always_passes.cpp @@ -1,10 +1,10 @@ #include +#include "include/dep.h" #include namespace demo { auto zoinks(bool zoinks) { bool x = true; - ASSERT(zoinks == false); if (zoinks) { for (int i = 0; i < 4; ++i) { x += std::puts("foo"); @@ -12,13 +12,15 @@ auto zoinks(bool zoinks) { } else { x = false; } + ASSERT(zoinks == false); return x; } [[= rsl::test]] void always_passes() { - std::cout << "foo\n"; - std::cerr << "bar\n"; - zoinks(true); + // std::cout << "foo\n"; + // std::cerr << "bar\n"; zoinks(false); + // zoinks(true); + zoinks(foo()); } } // namespace demo \ No newline at end of file diff --git a/example/always_passes2.cpp b/example/always_passes2.cpp new file mode 100644 index 0000000..136eb2f --- /dev/null +++ b/example/always_passes2.cpp @@ -0,0 +1,24 @@ +#include +#include + +namespace demo { +auto zoinks(bool zoinks) { + bool x = true; + ASSERT(zoinks == false); + if (zoinks) { + for (int i = 0; i < 4; ++i) { + x += std::puts("foo"); + } + } else { + x = false; + } + return x; +} + +void test_always_passes() { + // std::cout << "foo\n"; + // std::cerr << "bar\n"; + zoinks(false); + // zoinks(true); +} +} // namespace demo \ No newline at end of file diff --git a/example/include/dep.h b/example/include/dep.h new file mode 100644 index 0000000..9ce2700 --- /dev/null +++ b/example/include/dep.h @@ -0,0 +1,6 @@ +#pragma once + +inline bool foo() { + // return false; + return true; +} \ No newline at end of file diff --git a/example/params_debug.cpp b/example/params_debug.cpp deleted file mode 100644 index dcc29c8..0000000 --- a/example/params_debug.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include -#include - -std::vector> bar() { return {{42, 'd'}, {2, 'c'}}; } - -consteval std::vector foo() { return {{42, '3'}}; } - -[[= rsl::tparams{{^^int, 3}}]] -[[= rsl::params{foo}]] -constexpr static auto test = - [] - - (int, char) static { }; - -namespace Zoinks { -template -void foo(int, char) { - std::cout << "\ntest\n"; -} -} - - - - -int main() { - // auto runs = expand_test<^^Zoinks::foo, ^^test>(); - // std::cout << '\n' << runs.size(); - // for (auto r : runs) { - // std::cout << r.name << '\n'; - // } -} \ No newline at end of file diff --git a/include/rsl/testing/_testing_impl/discovery.hpp b/include/rsl/testing/_testing_impl/discovery.hpp index cb0adb9..ad06be0 100644 --- a/include/rsl/testing/_testing_impl/discovery.hpp +++ b/include/rsl/testing/_testing_impl/discovery.hpp @@ -19,12 +19,16 @@ namespace rsl::testing::_testing_impl { template -Test make_test_impl() { +Test make_test_impl(std::string const& path) { if constexpr (has_identifier(R) && identifier_of(R) == "_rsl_test_surrogate") { constexpr auto target = [:R:](); - return Test(target, R); + auto test = Test(target, R); + test.module_path = path; + return test; } else { - return Test(R, R); + auto test = Test(R, R); + test.module_path = path; + return test; } } @@ -52,7 +56,12 @@ struct TestDiscovery { std::meta::access_context ctx = std::meta::access_context::current(); consteval void handle_member(std::meta::info R) { - if (!has_identifier(R) || identifier_of(R)[0] == '_') { + if (!has_identifier(R)) { + return; + } + + auto identifier = identifier_of(R); + if (identifier[0] == '_') { return; } @@ -60,6 +69,14 @@ struct TestDiscovery { return; } + if (identifier.starts_with("test_")) { + if (is_complete_type(R) && is_class_type(R)) { + tests.append_range(expand_class(R)); + } else { + tests.emplace_back(make_test(R)); + } + } + auto annotations = annotations_of(R); for (auto annotation : annotations) { if (type_of(annotation) == ^^annotations::TestTag) { @@ -113,13 +130,14 @@ struct TestDiscovery { return discovery.tests; } }; + std::set& registry(); template bool enable_tests() { constexpr auto tests = define_static_array(_testing_impl::TestDiscovery::find_tests(NS)); for (auto const& test : tests) { - _testing_impl::registry().insert(test); + registry().insert(test); } return true; } diff --git a/include/rsl/testing/_testing_impl/expand.hpp b/include/rsl/testing/_testing_impl/expand.hpp index 04d788f..003fc68 100644 --- a/include/rsl/testing/_testing_impl/expand.hpp +++ b/include/rsl/testing/_testing_impl/expand.hpp @@ -8,27 +8,15 @@ #include #include #include - #include -#include #include "fixture.hpp" -namespace rsl::testing::_testing_impl { -template -struct FuzzRunner { - static int run(uint8_t const* Data, size_t Size) { - // TODO - return 0; - } - - // mutator must be able to consider domains - static size_t mutate(uint8_t* Data, size_t Size, size_t MaxSize, unsigned int Seed) { - // TODO - return 0; - } -}; +namespace rsl::testing { +class Test; +} +namespace rsl::testing::_testing_impl { template struct TestRunner { template diff --git a/include/rsl/testing/all.hpp b/include/rsl/testing/all.hpp index 3c87d36..293ff6e 100644 --- a/include/rsl/testing/all.hpp +++ b/include/rsl/testing/all.hpp @@ -15,7 +15,7 @@ using testing::params; using testing::tparams; using testing::expect_failure; -using testing::rename; +// using testing::rename; // TODO alias rsl::rename using testing::skip; using testing::skip_if; @@ -65,3 +65,15 @@ struct Anchor { #define RSLTEST_ANCHOR(...) RSLTEST_ANCHOR_IMPL(std::meta::info, (^^__VA_ARGS__)) #define RSLTEST_ANCHOR_OVERLOAD(TYPE, VALUE) RSLTEST_ANCHOR_IMPL(TYPE, VALUE) + +#ifdef RSL_TEST_UNIT +// TODO REMOVE +extern "C" +__attribute__((__visibility__("default"))) +__attribute__((__used__)) +inline +void* load_tests() { + return &rsl::testing::_testing_impl::registry(); +} + +#endif \ No newline at end of file diff --git a/include/rsl/testing/annotations.hpp b/include/rsl/testing/annotations.hpp index da7d808..a17a5b6 100644 --- a/include/rsl/testing/annotations.hpp +++ b/include/rsl/testing/annotations.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "_testing_impl/util.hpp" #include "_testing_impl/paramset.hpp" @@ -53,14 +54,6 @@ struct SkipIf { } }; -struct Rename { - rsl::string_view value; - - static consteval Rename operator()(std::string_view new_name) { - return Rename(define_static_string(new_name)); - } -}; - // parameterization struct TParams { rsl::span value; @@ -101,7 +94,6 @@ constexpr inline annotations::FuzzTag fuzz; constexpr inline annotations::ExpectFailureTag expect_failure; constexpr inline annotations::Skip skip; constexpr inline annotations::SkipIf skip_if; -constexpr inline annotations::Rename rename; using tparams = annotations::TParams; using params = annotations::Params; @@ -132,12 +124,8 @@ struct Annotations { // consteval-only } else if (type == ^^annotations::Skip) { // constexpr_assert(skip == nullptr, "Cannot have more than one skip annotation."); skip = extract(constant_of(annotation)).value; - } else if (type == ^^annotations::Rename) { - constexpr_assert(name.empty(), "Cannot rename more than once."); - name = extract(constant_of(annotation)).value; - } else if (type == ^^annotations::FuzzTag) { - is_fuzz_test = true; } + // TODO check if renamed } targets = define_static_array(tp_sets); diff --git a/include/rsl/testing/assert.hpp b/include/rsl/testing/assert.hpp index d3bbb4a..03dab35 100644 --- a/include/rsl/testing/assert.hpp +++ b/include/rsl/testing/assert.hpp @@ -1,56 +1,51 @@ #pragma once #include #include -#include +#include #include #include namespace rsl::testing { -struct assertion_failure : std::exception { +struct AssertionFailure { std::string message; rsl::source_location sloc; +}; +struct assertion_failure : AssertionFailure, std::exception { assertion_failure(std::string_view message, rsl::source_location sloc) - : message(std::string(message)) - , sloc(sloc) {} + : AssertionFailure(std::string(message), sloc) {} }; - + struct AssertionInfo { std::string_view raw; std::string_view expanded; bool success; }; -namespace _testing_impl { -struct AssertionTracker { - std::vector assertions; - std::string test_name; -}; -AssertionTracker& assertion_counter(); +namespace _testing_impl { +void track_assertion(AssertionInfo info); } // namespace _testing_impl } // namespace rsl::testing -#define LIBASSERT_ASSERT_MAIN_BODY(expr, \ - name, \ - type, \ - failaction, \ - decomposer_name, \ - condition_value, \ - pretty_function_arg, \ - ...) \ - rsl::testing::_testing_impl::assertion_counter().assertions.emplace_back(#expr, \ - "", \ - (condition_value)); \ - if (LIBASSERT_STRONG_EXPECT(!(condition_value), 0)) { \ - libassert::ERROR_ASSERTION_FAILURE_IN_CONSTEXPR_CONTEXT(); \ - LIBASSERT_BREAKPOINT_IF_DEBUGGING_ON_FAIL(); \ - failaction; \ - LIBASSERT_STATIC_DATA(name, libassert::assert_type::type, #expr, __VA_ARGS__) \ - libassert::detail::process_assert_fail(decomposer_name, \ - libassert_params LIBASSERT_VA_ARGS(__VA_ARGS__) \ - pretty_function_arg); \ +#define LIBASSERT_ASSERT_MAIN_BODY(expr, \ + name, \ + type, \ + failaction, \ + decomposer_name, \ + condition_value, \ + pretty_function_arg, \ + ...) \ + rsl::testing::_testing_impl::track_assertion({#expr, "", (condition_value)}); \ + if (LIBASSERT_STRONG_EXPECT(!(condition_value), 0)) { \ + libassert::ERROR_ASSERTION_FAILURE_IN_CONSTEXPR_CONTEXT(); \ + LIBASSERT_BREAKPOINT_IF_DEBUGGING_ON_FAIL(); \ + failaction; \ + LIBASSERT_STATIC_DATA(name, libassert::assert_type::type, #expr, __VA_ARGS__) \ + libassert::detail::process_assert_fail(decomposer_name, \ + libassert_params LIBASSERT_VA_ARGS(__VA_ARGS__) \ + pretty_function_arg); \ } #define LIBASSERT_BREAK_ON_FAIL #include \ No newline at end of file diff --git a/include/rsl/testing/_testing_impl/factory.hpp b/include/rsl/testing/ext/factory.hpp similarity index 100% rename from include/rsl/testing/_testing_impl/factory.hpp rename to include/rsl/testing/ext/factory.hpp diff --git a/include/rsl/testing/output.hpp b/include/rsl/testing/ext/output.hpp similarity index 89% rename from include/rsl/testing/output.hpp rename to include/rsl/testing/ext/output.hpp index 4c383ba..10880f4 100644 --- a/include/rsl/testing/output.hpp +++ b/include/rsl/testing/ext/output.hpp @@ -4,8 +4,8 @@ #include #include -#include "test.hpp" -#include "_testing_impl/factory.hpp" +#include "../test.hpp" +#include "factory.hpp" namespace rsl::testing { struct Output { @@ -31,7 +31,7 @@ struct Reporter : _testing_impl::Factory { virtual void before_test(TestCase const& test) = 0; virtual void after_test(Result const& result) = 0; - virtual void list_tests(TestNamespace const& tests); + virtual void list_tests(TestNamespace const& tests) {} virtual void enter_namespace(std::string_view name) {} virtual void exit_namespace(std::string_view name) {} diff --git a/include/rsl/testing/runner/_unit_impl/capture.hpp b/include/rsl/testing/runner/_unit_impl/capture.hpp new file mode 100644 index 0000000..1e17c7c --- /dev/null +++ b/include/rsl/testing/runner/_unit_impl/capture.hpp @@ -0,0 +1,111 @@ +#pragma once +#include +#include +#include + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +# include +# include +#else +# include +# include +# include +#endif + +namespace rsl::testing { + +namespace _impl { +inline int read_pipe(int fd, char* buffer, size_t size) { +#ifdef _WIN32 + HANDLE h = reinterpret_cast(_get_osfhandle(fd)); + DWORD available = 0; + if (PeekNamedPipe(h, nullptr, 0, nullptr, &available, nullptr) && available > 0) + return _read(fd, buffer, static_cast(size)); + return 0; +#else + ssize_t n = read(fd, buffer, size); + if (n > 0) { + return static_cast(n); + } + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return -1; +#endif +} + +} // namespace _impl + +struct RedirectedOutput { + FILE* redirected = nullptr; + FILE* underlying = nullptr; + + int redirected_fd = -1; + int underlying_fd = -1; + + RedirectedOutput() = default; + + RedirectedOutput(FILE* redirected_stream, int original_fd) + : redirected(redirected_stream) + , underlying(fdopen(original_fd, "w")) + , redirected_fd(fileno(redirected_stream)) // TODO suppress msvc warning C4996 or use _fileno + , underlying_fd(original_fd) {} +}; + +class Capture { + int pipe_fds_[2]{}; + std::string* target; + bool echo; + +public: + RedirectedOutput out; + + Capture(const Capture&) = delete; + Capture& operator=(const Capture&) = delete; + + // TODO this can be moveable + Capture(Capture&&) = delete; + Capture& operator=(Capture&&) = delete; + + Capture(FILE* stream, std::string& target, bool echo = false) : target(&target), echo(echo) { + (void)fflush(stream); + out = {stream, dup(fileno(stream))}; +#ifdef _WIN32 + _pipe(pipe_fds_, 8192, _O_BINARY); +#else + pipe(pipe_fds_); + fcntl(pipe_fds_[0], F_SETFL, O_NONBLOCK); +#endif + dup2(pipe_fds_[1], out.redirected_fd); + } + + ~Capture() { + (void)fflush(out.redirected); + dup2(out.underlying_fd, out.redirected_fd); + close(pipe_fds_[1]); + + drain(); // Final flush + + close(pipe_fds_[0]); + close(out.underlying_fd); + } + + void drain() { + (void)fflush(out.redirected); + char buffer[256]; + while (true) { + int n = _impl::read_pipe(pipe_fds_[0], &buffer[0], sizeof(buffer)); + if (n > 0) { + *target += std::string_view(&buffer[0], n); + if (echo) { + write(out.underlying_fd, &buffer[0], n); + } + } else { + break; + } + } + } +}; +} // namespace rsl::testing \ No newline at end of file diff --git a/include/rsl/testing/result.hpp b/include/rsl/testing/runner/_unit_impl/result.hpp similarity index 82% rename from include/rsl/testing/result.hpp rename to include/rsl/testing/runner/_unit_impl/result.hpp index 19d9e19..e16b367 100644 --- a/include/rsl/testing/result.hpp +++ b/include/rsl/testing/runner/_unit_impl/result.hpp @@ -2,9 +2,10 @@ #include #include -#include "assert.hpp" +#include namespace rsl::testing { +class Test; enum class TestOutcome: uint8_t { FAIL, @@ -23,13 +24,13 @@ struct FileCoverage { }; struct Result { - class Test const* test; + Test const* test; std::string name; TestOutcome outcome; double duration_ms; - std::optional failure; + std::optional failure; std::string exception; std::string stdout; std::string stderr; @@ -39,7 +40,7 @@ struct Result { }; struct TestResult { - class Test const* test; + Test const* test; std::vector results; }; } // namespace rsl::testing \ No newline at end of file diff --git a/include/rsl/testing/runner/unit.cpp b/include/rsl/testing/runner/unit.cpp new file mode 100644 index 0000000..266dd9a --- /dev/null +++ b/include/rsl/testing/runner/unit.cpp @@ -0,0 +1,174 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include + +namespace rsl::testing { +namespace _testing_impl { +std::set& registry() { + static std::set reg; + return reg; +} + +struct AssertionTracker { + std::vector assertions; + std::string test_name; +}; + +inline AssertionTracker& assertion_counter() { + static AssertionTracker counter{}; + return counter; +} + +void track_assertion(AssertionInfo info) { + assertion_counter().assertions.emplace_back(info); +} +} // namespace _testing_impl + +Result run(TestCase& tc) { + auto ret = Result{.test = tc.test, .name = tc.name}; + try { + Capture _out(stdout, ret.stdout); + Capture _err(stderr, ret.stderr); + + auto t0 = std::chrono::steady_clock::now(); + // if (_rsl_test_run_with_coverage != nullptr) { + // // rsltest_cov was linked in -> run with coverage + // rsl::coverage::CoverageReport* reports = nullptr; + // std::size_t report_count = 0; + // auto finalize = [&] { + // ret.coverage = filter_coverage(reports, report_count); + // free(reports); + // }; + // try { + // _rsl_test_run_with_coverage(_impl::run_test, + // static_cast(&fnc), + // &reports, + // &report_count); + // finalize(); + // } catch (...) { + // finalize(); + // throw; + // } + // } else { + tc.fnc(); + // } + auto t1 = std::chrono::steady_clock::now(); + + ret.outcome = TestOutcome(!tc.test->expect_failure); + ret.duration_ms = std::chrono::duration(t1 - t0).count(); + return ret; + } catch (assertion_failure const& failure) { + ret.failure = failure; + } catch (std::exception const& exc) { // + ret.exception += exc.what(); + } catch (std::string const& msg) { // + ret.exception += msg; + } catch (std::string_view msg) { // + ret.exception += msg; + } catch (char const* msg) { // + ret.exception += msg; + } catch (...) { ret.exception += "unknown exception thrown"; } + + ret.outcome = TestOutcome(tc.test->expect_failure); + return ret; +} + +} // namespace rsl::testing + +namespace { +void cleanup_frames(cpptrace::stacktrace& trace, std::string_view test_name) { + std::vector frames; + for (auto const& frame : trace.frames | std::views::drop(1)) { + frames.push_back(frame); + if (cpptrace::prune_symbol(frame.symbol) == test_name) { + break; + } + } + trace.frames = frames; +} + +void failure_handler(libassert::assertion_info const& info) { + // libassert::enable_virtual_terminal_processing_if_needed(); // for terminal colors on windows + constexpr bool Colorize = false; + auto width = libassert::terminal_width(libassert::stderr_fileno); + const auto& scheme = Colorize ? libassert::get_color_scheme() : libassert::color_scheme::blank; + std::string message = std::string(info.action()) + " at " + info.location() + ":"; + if (info.message) { + message += " " + *info.message; + } + message += "\n"; + message += + info.statement(scheme) + info.print_binary_diagnostics(width, scheme) + + info.print_extra_diagnostics(width, scheme); // + info.print_stacktrace(width, scheme); + + auto trace = info.get_stacktrace(); + cleanup_frames(trace, rsl::testing::_testing_impl::assertion_counter().test_name); + message += trace.to_string(Colorize); + throw rsl::testing::assertion_failure( + message, + rsl::source_location(info.file_name, info.function, info.line)); +} + +void list_tests() { + for (auto def : rsl::testing::_testing_impl::registry()) { + auto tests = def(""); + std::println("test: {}", tests.name); + } +} + +void run_tests(std::vector filters) { + std::println("test, filters {}", filters); + libassert::set_failure_handler(failure_handler); + + for (auto def : rsl::testing::_testing_impl::registry()) { + auto tests = def(""); + + rsl::testing::_testing_impl::assertion_counter().test_name = + tests.full_name | std::views::transform([](auto c) { return std::string(c); }) | + std::views::join_with(std::string("::")) | std::ranges::to(); + + std::println("{}", tests.full_name); + for (auto tc : tests.get_tests()) { + auto result = run(tc); + std::println("->{}", int(result.outcome)); + for (auto assertion : result.assertions) { + std::println("{} -> {} = {}", assertion.raw, assertion.expanded, assertion.success); + } + if (result.failure) { + std::println("{}", (*result.failure).message); + } + } + + rsl::testing::_testing_impl::assertion_counter().test_name = ""; + } + + libassert::set_failure_handler(libassert::default_failure_handler); +} +} // namespace + +int main(int argc, char** argv) { + assert(argc > 0); + auto& registry = rsl::testing::_testing_impl::registry(); + + if (argc == 1) { + list_tests(); + } else { + // run tests matching queries + // `::` matches all tests + std::vector filters; + for (auto idx : std::views::iota(1, argc)) { + filters.emplace_back(argv[idx]); + } + run_tests(filters); + } +} \ No newline at end of file diff --git a/include/rsl/testing/test.hpp b/include/rsl/testing/test.hpp index 2b34165..cb12288 100644 --- a/include/rsl/testing/test.hpp +++ b/include/rsl/testing/test.hpp @@ -4,8 +4,9 @@ #include #include #include - -#include "result.hpp" +#include +#include +#include #include "_testing_impl/util.hpp" #include "_testing_impl/expand.hpp" @@ -18,15 +19,6 @@ struct TestCase { class Test const* test; std::function fnc; std::string name; - - [[nodiscard]] Result run() const; -}; - -struct FuzzTarget { - // stringifying name is pointless here, perhaps do it after failure - class Test const* test; - int (*run)(uint8_t const*, size_t); - size_t (*mutate)(uint8_t*, size_t, size_t, unsigned int); }; class Test { @@ -40,13 +32,13 @@ class Test { public: std::source_location sloc; + std::string module_path; std::string_view name; // raw name std::string_view preferred_name; // from annotations std::span full_name; // fully qualified name bool expect_failure; // invert test checking bool (*skip)(); // function to support conditional skipping - bool is_fuzz_test; Test() = delete; consteval explicit Test(std::meta::info test, std::meta::info annotation_anchor) @@ -56,7 +48,6 @@ class Test { preferred_name = ann.name; expect_failure = ann.expect_failure; skip = ann.skip; - is_fuzz_test = ann.is_fuzz_test; get_tests_impl = extract( substitute(^^expand_test, {reflect_constant(test), std::meta::reflect_constant(ann)})); @@ -71,60 +62,5 @@ class Test { std::vector get_tests() const { return (this->*get_tests_impl)(); } }; -using TestDef = Test (*)(); - -struct Reporter; -struct TestNamespace { - std::string_view name; - std::vector tests; - std::vector children; - - class iterator { - struct single_iterator { - std::vector::const_iterator it; - std::vector::const_iterator end; - - bool operator==(single_iterator const& other) const { - return it == other.it && end == other.end; - } - }; - - single_iterator current; - std::deque elements; - - void flatten(TestNamespace const& current); - - public: - using iterator_category = std::input_iterator_tag; - using value_type = Test; - using difference_type = std::ptrdiff_t; - using pointer = Test const*; - using reference = Test const&; - - iterator() = default; - explicit iterator(TestNamespace const& ns); - - Test const& operator*() const { return *current.it; } - Test const* operator->() const { return &operator*(); } - iterator& operator++(); - bool operator==(iterator const& other) const; - }; - - [[nodiscard]] bool is_empty() const { return tests.empty() && children.empty(); } - [[nodiscard]] iterator begin() const { return iterator{*this}; } - [[nodiscard]] static iterator end() { return {}; } - void insert(const Test& test, size_t i = 0); - - [[nodiscard]] std::size_t count() const; - bool run(Reporter* reporter); - - void filter(std::span parts); -}; - -struct TestRoot : TestNamespace { - bool run(Reporter* reporter); -}; - -TestRoot get_tests(); - +using TestDef = Test (*)(std::string const&); } // namespace rsl::testing \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 457a909..5714066 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,8 +1,4 @@ -target_sources(rsltest PUBLIC - capture.cpp - test.cpp - runner.cpp -) +# add_subdirectory(main) +# add_subdirectory(coverage) -add_subdirectory(main) -add_subdirectory(coverage) \ No newline at end of file +target_sources(rsltest_main PUBLIC ../include/rsl/testing/unit.cpp) \ No newline at end of file diff --git a/src/capture.cpp b/src/capture.cpp deleted file mode 100644 index a526d46..0000000 --- a/src/capture.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "capture.hpp" - -#include -#include - -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#else -#include -#include -#include -#endif - - -namespace rsl::testing { - -RedirectedOutput::RedirectedOutput(FILE* redirected_stream, int original_fd) - : redirected(redirected_stream) - , underlying_fd(original_fd) { - underlying = fdopen(original_fd, "w"); - redirected_fd = fileno(redirected_stream); -} - -Capture::Capture(FILE* stream, std::string& target, bool echo ) - : target(&target) - , echo(echo) { - fflush(stream); - out = {stream, dup(fileno(stream))}; -#ifdef _WIN32 - _pipe(pipe_fds_, 8192, _O_BINARY); -#else - pipe(pipe_fds_); - fcntl(pipe_fds_[0], F_SETFL, O_NONBLOCK); -#endif - dup2(pipe_fds_[1], out.redirected_fd); -} - -Capture::~Capture() { - fflush(out.redirected); - dup2(out.underlying_fd, out.redirected_fd); - close(pipe_fds_[1]); - - drain(); // Final flush - - close(pipe_fds_[0]); - close(out.underlying_fd); -} - -static int read_pipe(int fd, char* buffer, size_t size) { -#ifdef _WIN32 - HANDLE h = reinterpret_cast(_get_osfhandle(fd)); - DWORD available = 0; - if (PeekNamedPipe(h, nullptr, 0, nullptr, &available, nullptr) && available > 0) - return _read(fd, buffer, static_cast(size)); - return 0; -#else - ssize_t n = read(fd, buffer, size); - if (n > 0) - return static_cast(n); - if (errno == EAGAIN || errno == EWOULDBLOCK) - return 0; - return -1; -#endif -} - -void Capture::drain() { - fflush(out.redirected); - char buffer[256]; - while (true) { - int n = read_pipe(pipe_fds_[0], buffer, sizeof(buffer)); - if (n > 0) { - *target += std::string_view(buffer, n); - if (echo) { - write(out.underlying_fd, buffer, n); - } - } else { - break; - } - } -} - -} // namespace rsl::testing \ No newline at end of file diff --git a/src/capture.hpp b/src/capture.hpp deleted file mode 100644 index 3ee5f01..0000000 --- a/src/capture.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include -#include - -namespace rsl::testing { - -struct RedirectedOutput { - FILE* redirected = nullptr; - FILE* underlying = nullptr; - - int redirected_fd = -1; - int underlying_fd = -1; - - RedirectedOutput() = default; - - RedirectedOutput(FILE* redirected_stream, int original_fd); -}; - -class Capture { - int pipe_fds_[2]{}; - std::string* target; - bool echo; - -public: - RedirectedOutput out; - - Capture(FILE* stream, std::string& target, bool echo = false); - ~Capture(); - void drain(); -}; -} // namespace rsl::testing \ No newline at end of file diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index 4a6180f..aaa983e 100644 --- a/src/main/CMakeLists.txt +++ b/src/main/CMakeLists.txt @@ -1,4 +1,5 @@ target_sources(rsltest_main PUBLIC main.cpp) target_include_directories(rsltest_main PRIVATE .) -add_subdirectory(reporters) \ No newline at end of file +# add_subdirectory(reporters) +add_subdirectory(platform) \ No newline at end of file diff --git a/src/main/compdb.hpp b/src/main/compdb.hpp new file mode 100644 index 0000000..c5a436a --- /dev/null +++ b/src/main/compdb.hpp @@ -0,0 +1,104 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace rsl::testing::_impl_main { +struct CompileCommand { + std::string directory; + std::string file; + + std::optional> arguments; + std::optional command; + + std::optional output; + + [[nodiscard]] nlohmann::json to_json() const { + auto obj = nlohmann::json{ + {"directory", directory}, + { "file", file} + }; + if (arguments.has_value()) { + auto cmd_range = *arguments | std::views::join_with(std::string_view(" ")); + std::string cmd(cmd_range.begin(), cmd_range.end()); + obj.emplace("command", cmd); + } else if (command.has_value()) { + obj.emplace("command", *command); + } + + if (output.has_value()) { + obj.emplace("output", *output); + } + return obj; + } + + static CompileCommand from_json(nlohmann::json const& doc) { + CompileCommand out{.directory = doc.at("directory").get(), + .file = doc.at("file").get()}; + if (doc.count("arguments") != 0) { + out.arguments = doc.at("arguments").get>(); + } else if (doc.count("command") != 0) { + out.command = doc.at("command").get(); + } + + if (doc.count("output") != 0) { + out.output = doc.at("output").get(); + } + return out; + } + + bool operator==(CompileCommand const& other) const { + return directory == other.directory && file == other.file; + } +}; + +class CompileCommands { + std::filesystem::path path_; + std::vector entries_; + +public: + explicit CompileCommands(std::filesystem::path path = "compile_commands.json") : path_(std::move(path)) {} + + void load() { + if (std::filesystem::exists(path_)) { + entries_.clear(); + std::ifstream f(path_); + nlohmann::json j; + f >> j; + for (auto& el : j) { + entries_.push_back(CompileCommand::from_json(el)); + } + } + } + + void save() const { + std::ofstream f(path_); + nlohmann::json j = nlohmann::json::array(); + for (auto const& e : entries_) { + j.push_back(e.to_json()); + } + f << j.dump(2) << "\n"; + } + + void append_if_missing(CompileCommand cmd) { + auto it = std::ranges::find(entries_, cmd); + if (it == entries_.end()) { + entries_.push_back(std::move(cmd)); + } + } + + std::optional find_for_file(const std::string& file) const { + auto it = std::ranges::find_if(entries_, [&](auto const& e) { return e.file == file; }); + if (it != entries_.end()) { + return *it; + } + return std::nullopt; + } + + const std::vector& entries() const { return entries_; } +}; + +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/compile_pool.hpp b/src/main/compile_pool.hpp new file mode 100644 index 0000000..b924661 --- /dev/null +++ b/src/main/compile_pool.hpp @@ -0,0 +1,123 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "config_parser.hpp" +#include "platform/taskset.hpp" + +namespace rsl::testing::_impl_main { + +class CompilePool { + using output_type = std::pair; + + std::vector workers; + std::queue tasks; + std::vector results; + + std::mutex queueMutex; + std::mutex resultMutex; + std::condition_variable cv; + std::atomic stop; + + std::atomic submittedCount; + std::atomic completedCount; + +public: + explicit CompilePool(size_t numThreads = std::thread::hardware_concurrency()) : stop(false), submittedCount(0), completedCount(0) { + auto num_cpu = std::thread::hardware_concurrency(); + // TODO hardware_concurrency might be 0 + for (size_t i = 0; i < numThreads; ++i) { + workers.emplace_back([this, cpu=i%num_cpu] { + workerLoop(static_cast(cpu)); + }); + } + } + + ~CompilePool() { + stop.store(true, std::memory_order_relaxed); + cv.notify_all(); + for (auto& t : workers) { + t.join(); + } + } + + void submit(TestTU const& task) { + { + std::scoped_lock lock(queueMutex); + tasks.push(task); + submittedCount.fetch_add(1, std::memory_order_relaxed); + } + cv.notify_one(); + } + + void wait() { + while (completedCount.load(std::memory_order_acquire) < + submittedCount.load(std::memory_order_acquire)) { + std::this_thread::yield(); // spin-wait + } + } + + void wait(auto&& on_update) { + size_t lastCompleted = completedCount.load(std::memory_order_acquire); + + //! assumes we don't call submit from other threads while waiting + size_t total = submittedCount.load(std::memory_order_acquire); + + while (lastCompleted < total) { + size_t currentCompleted = completedCount.load(std::memory_order_acquire); + + if (currentCompleted != lastCompleted) { + on_update(currentCompleted, total); + lastCompleted = currentCompleted; + } + + std::this_thread::yield(); + } + } + + std::vector collect() { + std::vector output; + { + std::scoped_lock lock(resultMutex); + output.swap(results); + } + submittedCount.store(0, std::memory_order_relaxed); + completedCount.store(0, std::memory_order_relaxed); + return output; + } + +private: + void workerLoop(int cpu) { + while (!stop.load(std::memory_order_relaxed)) { + TestTU task; + { + std::unique_lock lock(queueMutex); + if (tasks.empty()) { + cv.wait(lock, [&] { return stop.load() || !tasks.empty(); }); + if (stop.load() && tasks.empty()) { + return; + } + } + + task = std::move(tasks.front()); + tasks.pop(); + } + // std::println("building {}", task.out_path.string()); + // auto start_time = std::chrono::steady_clock::now(); + output_type result = {task, run_on_cpu(cpu, task.invocation)}; + // auto end_time = std::chrono::steady_clock::now(); + // std::println("{} - {}", task.out_path.string(), std::chrono::duration_cast(end_time - start_time)); + { + std::scoped_lock lock(resultMutex); + results.push_back(std::move(result)); + } + + completedCount.fetch_add(1, std::memory_order_release); + } + } +}; +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/config_parser.hpp b/src/main/config_parser.hpp new file mode 100644 index 0000000..cb17333 --- /dev/null +++ b/src/main/config_parser.hpp @@ -0,0 +1,172 @@ +#pragma once +#include +#include +#include +#include +#include + +#include +#include "platform/taskset.hpp" + +namespace rsl::testing::_impl_main { +struct TestTU { + ProgramInvocation invocation; + std::filesystem::path out_path; + std::filesystem::path source_path; +}; + +template +auto filter_empty(R&& range) { + return range | std::views::filter([](auto e) { return not e.empty(); }) | + std::ranges::to(); +} + +struct Project { + std::vector test_path; + std::filesystem::path build_path; + std::filesystem::path project_path; + std::string namespace_; // TODO rename via annotation +}; + +inline void to_json(nlohmann::json& doc, Project const& p) { + doc = { + { "test_path", p.test_path}, + { "build_path", p.build_path}, + {"project_path", p.project_path}, + { "namespace", p.namespace_} + }; +} +inline void from_json(nlohmann::json const& doc, Project& p) { + doc.at("test_path").get_to(p.test_path); + p.test_path = filter_empty(p.test_path); + doc.at("build_path").get_to(p.build_path); + doc.at("project_path").get_to(p.project_path); + doc.at("namespace").get_to(p.namespace_); +} + +struct Options { + std::vector include_dirs; + std::vector compile_options; + std::vector compile_definitions; + std::vector link_options; + std::vector link_libraries; +}; +inline void to_json(nlohmann::json& doc, Options const& p) { + doc = { + { "include_dirs", p.include_dirs}, + { "compile_options", p.compile_options}, + {"compile_definitions", p.compile_definitions}, + { "link_options", p.link_options}, + { "link_libraries", p.link_libraries} + }; +} +inline void from_json(nlohmann::json const& doc, Options& p) { + doc.at("include_dirs").get_to(p.include_dirs); + p.include_dirs = filter_empty(p.include_dirs); + doc.at("compile_options").get_to(p.compile_options); + p.compile_options = filter_empty(p.compile_options); + doc.at("compile_definitions").get_to(p.compile_definitions); + p.compile_definitions = filter_empty(p.compile_definitions); + doc.at("link_options").get_to(p.link_options); + p.link_options = filter_empty(p.link_options); + doc.at("link_libraries").get_to(p.link_libraries); + p.link_libraries = filter_empty(p.link_libraries); +} + +struct Configuration { + std::filesystem::path compiler_path = "c++"; + std::string mode = "CXX"; + unsigned standard = 26; + bool gnu_extensions = false; +}; +inline void to_json(nlohmann::json& doc, Configuration const& p) { + doc = { + { "compiler_path", p.compiler_path}, + { "mode", p.mode}, + { "standard", p.standard}, + {"gnu_extensions", p.gnu_extensions} + }; +} +inline void from_json(nlohmann::json const& doc, Configuration& p) { + doc.at("compiler_path").get_to(p.compiler_path); + doc.at("mode").get_to(p.mode); + doc.at("standard").get_to(p.standard); + doc.at("gnu_extensions").get_to(p.gnu_extensions); +} + +struct RunnerConfig { + std::string target; + Project project; + Options options; + std::unordered_map configurations; + + std::vector expand_options(std::string_view config_name) const { + const auto& cfg = configurations.at(std::string(config_name)); + + const bool ext = cfg.gnu_extensions; + const auto ver = cfg.standard; + const std::string standard = std::format("-std={}++{}", (ext ? "gnu" : "c"), ver); + std::vector cmd; + cmd.push_back(standard); + + for (auto const& o : options.compile_options) { + cmd.push_back(o); + } + + for (auto const& dir : options.include_dirs) { + cmd.push_back(std::format("-I{}", dir.string())); + } + + for (auto const& d : options.compile_definitions) { + cmd.push_back(std::format("-D{}", d)); + } + + if (auto ns = project.namespace_; not ns.empty()) { + cmd.emplace_back("-DRSL_TEST_NAMESPACE=" + ns); + } + cmd.emplace_back("-DRSL_TEST_UNIT"); + return cmd; + } + + std::vector expand_link_options() const { + std::vector cmd; + for (auto const& lib : options.link_libraries) { + if (lib.is_absolute()) { + cmd.push_back(std::format("-Wl,-rpath,'{}'", lib.parent_path().string())); + cmd.push_back(std::format("{}", lib.string())); + } else { + cmd.push_back(std::format("-l{}", lib.string())); + } + } + return cmd; + } +}; + +inline void to_json(nlohmann::json& doc, RunnerConfig const& p) { + doc = { + { "target", p.target}, + { "project", p.project}, + { "options", p.options}, + {"configurations", p.configurations} + }; +} + +inline void from_json(nlohmann::json const& doc, RunnerConfig& p) { + doc.at("target").get_to(p.target); + doc.at("project").get_to(p.project); + doc.at("options").get_to(p.options); + doc.at("configurations").get_to(p.configurations); +} + +inline RunnerConfig load_runner_config(std::filesystem::path const& path) { + std::ifstream stream{path}; + if (!stream) { + throw std::runtime_error("Unable to load config "); + } + + nlohmann::json config; + stream >> config; + return config.get(); +} + +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/incremental.hpp b/src/main/incremental.hpp new file mode 100644 index 0000000..5e2a7f9 --- /dev/null +++ b/src/main/incremental.hpp @@ -0,0 +1,296 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include "compile_pool.hpp" +#include "config_parser.hpp" +#include "compdb.hpp" +#include "platform/library.hpp" + +#include +#include + +namespace rsl::testing::_impl_main { +struct TestSet { + void* handle = nullptr; + std::set tests; + + TestSet() = default; + TestSet(TestSet const&) = delete; + TestSet& operator=(TestSet const&) = delete; + + TestSet(void* handle, std::set tests) + : handle(handle) + , tests(std::move(tests)) {} + + TestSet(TestSet&& other) : handle(other.handle), tests(std::move(other.tests)) { + other.handle = nullptr; + } + + TestSet& operator=(TestSet&& other) { + if (this == &other) { + return *this; + } + handle = other.handle; + tests = std::move(other.tests); + other.handle = nullptr; + return *this; + } + + ~TestSet() { unload(); } + + void unload() { + if (handle != nullptr) { + unload_library(handle); + tests = {}; + } + } +}; // namespace rsl::testing::_impl_main + +struct TestUnit { + // library path -> metadata (path is different between configurations) + std::unordered_map sets; + size_t counter = 0; + + std::set load(std::filesystem::path const& library_path) { + auto path = canonical(library_path); + auto tmp_path = + std::filesystem::path(library_path).replace_extension(".so." + std::to_string(counter++)); + while (exists(tmp_path)) { + tmp_path = + std::filesystem::path(library_path).replace_extension(".so." + std::to_string(counter++)); + } + + // check if we have the key already, make sure old one is unloaded + unload(path); + + std::filesystem::rename(library_path, tmp_path); + library_handle handle = load_library(tmp_path.string()); + if (handle == nullptr) { + std::println("unable to load library"); + return {}; + } + + auto fnc = find_symbol(handle, "load_tests"); + if (fnc == nullptr) { + std::println("load_tests missing"); + return {}; + } + auto test_sets = reinterpret_cast*>(fnc()); + sets.insert_or_assign(path, TestSet{handle, *test_sets}); + return *test_sets; + } + + static void remove_stale(std::filesystem::path const& path) { + for (const auto& entry : std::filesystem::directory_iterator(path.parent_path())) { + if (!entry.is_regular_file()) { + continue; + } + if (path.filename().string() == entry.path().stem().string()) { + remove(entry.path()); + } + } + } + + void unload(std::filesystem::path const& path) { + remove_stale(path); + if (auto it = sets.find(weakly_canonical(path)); it != sets.end()) { + it->second.unload(); + sets.erase(it); + } + } +}; + +struct IncrementalRunner { + CompilePool pool; // TODO use more generic task pool? + RunnerConfig config; + + std::map units; + // reverse dependency map + std::unordered_map> dependencies; + std::unique_ptr reporter; + CompileCommands compdb; + size_t invocation_counter = 0; + + // TODO move to config? + static constexpr std::array allowed_extensions = {".cpp"}; + +public: + IncrementalRunner() = default; + explicit IncrementalRunner(std::filesystem::path const& config_path) + : config(load_runner_config(config_path)) { + // TODO check build_path/compile_commands.json if this doesn't exist + compdb = CompileCommands(config.project.project_path / "compile_commands.json"); + reporter = rsl::testing::Reporter::make("json"); + } + + void update_compdb() { + compdb.load(); + for (auto const& [path, unit] : units) { + auto tu = make_invocation("default", path, false, true); + compdb.append_if_missing({.directory = config.project.build_path, + .file = tu.source_path, + .arguments = tu.invocation.arguments, + .output = tu.out_path}); + } + compdb.save(); + } + + [[nodiscard]] + TestTU make_invocation(std::string_view config_name, + std::filesystem::path const& test_path, + bool dump_dependencies = false, + bool link = true) { + const std::filesystem::path build_path = config.project.build_path; + const std::filesystem::path project_path = config.project.project_path; + + const auto& cfg = config.configurations.at(std::string(config_name)); + const std::string compiler_path = cfg.compiler_path; + + std::filesystem::path out_path = build_path / relative(test_path, project_path); + out_path.replace_extension(".so"); + out_path = weakly_canonical(out_path); + + std::vector cmd = {compiler_path}; + cmd.append_range(config.expand_options(config_name)); + + if (link && not dump_dependencies) { + cmd.append_range(config.expand_link_options()); + + // out file + cmd.emplace_back("-o"); + cmd.push_back(out_path.string()); + } + + // input file + cmd.push_back(std::string(test_path.string())); + + if (dump_dependencies) { + cmd.emplace_back("-MM"); + } + + return {compiler_path, cmd, out_path, test_path}; + } + + static std::vector discover_tests(std::filesystem::path const& root) { + std::vector result; + + for (auto const& entry : std::filesystem::recursive_directory_iterator(root)) { + if (not entry.is_regular_file()) { + continue; + } + + auto const ext = entry.path().extension().string(); + if (std::ranges::contains(allowed_extensions, ext)) { + result.push_back(entry.path()); + } + } + return result; + } + + std::vector discover_tests() { + std::vector result; + for (auto const& path : config.project.test_path) { + result.append_range(discover_tests(path)); + } + return result; + } + + std::vector expand_tests(std::vector const& tests, + bool dump = false) { + std::vector test_tus; + for (auto const& test : tests) { + for (auto const& [name, _] : config.configurations) { + test_tus.push_back(make_invocation(name, test, dump)); + } + } + return test_tus; + } + + TestRoot recompile(std::vector const& paths) { + //! this function is not thread-safe, it shall only be invoked from the main thread + auto tus = expand_tests(paths); + for (auto&& tu : tus) { + auto parent = std::filesystem::weakly_canonical(tu.out_path).parent_path(); + if (!parent.empty() && !exists(parent)) { + create_directories(parent); + } + pool.submit(tu); + } + + // std::println("Building {} test{}", tus.size(), tus.size() == 1 ? "" : "s"); + + auto progress = [](auto done, auto total) { + // constexpr static auto bar_width = 30; + // auto filled = static_cast((static_cast(done) / total) * bar_width); + // auto bar = std::string(filled, '#') + std::string(bar_width - filled, ' '); + // std::print("\r[{}] ({}/{})", bar, done, total); + // std::fflush(stdout); + auto doc = nlohmann::json({ + {"action", "batch_progress"}, + { "done", done}, + { "total", total} + }); + std::println("{}", doc.dump()); + std::fflush(stdout); + }; + progress(0, tus.size()); + pool.wait(progress); + // std::println(""); + + TestRoot root; + for (auto&& [tu, result] : pool.collect()) { + if (result.exit_code != 0) { + std::println("ERROR!"); + std::println("====== stdout ======\n{}", result.stdout_str); + std::println("====== stderr ======\n{}", result.stderr_str); + continue; + } + + auto test_defs = units[tu.source_path].load(tu.out_path); + for (auto def : test_defs) { + auto expanded = def(tu.source_path); + root.insert(expanded); + } + } + return root; + } + + void run_all() { + auto test_inputs = discover_tests(); + auto root = recompile(test_inputs); + root.run(reporter.get()); + update_compdb(); + } + + void list_all() { + auto test_inputs = discover_tests(); + auto root = recompile(test_inputs); + + auto tests = nlohmann::json::array(); + for (auto const& test : root) { + auto cases = nlohmann::json::array(); + for (auto const& test_case : test.get_tests()) { + cases.push_back(test_case.name); + } + + tests.push_back({ + { "name", test.name}, + {"full_name", test.full_name}, + { "cases", cases}, + {"path", test.sloc.file_name()} + }); + } + std::println("{}", + nlohmann::json({{"action", "list"}, + { "tests", tests}}) + .dump()); + std::fflush(stdout); + } +}; +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/main.cpp b/src/main/main.cpp index 432153f..b59d3e8 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -1,81 +1,40 @@ -#include -#include -#include -#include - +#include #include -#include -#include -#include -#include -#include "output.hpp" - -std::string_view base_name(std::string_view name) { - auto lt = name.find('<'); - auto paren = name.find('('); - auto pos = std::min(lt, paren); - return pos == std::string_view::npos ? name : name.substr(0, pos); -} -std::vector split_filter_path(std::string_view filter) { - if (filter.starts_with("::")) { - filter.remove_prefix(2); - } - - std::vector parts; +#include +#include - while (true) { - auto next = filter.find("::"); - auto templ = filter.find('<'); - - if (templ != std::string_view::npos && (next == std::string_view::npos || templ < next)) { - parts.push_back(filter); - break; - } +#include "incremental.hpp" +#include "platform/library.hpp" +#include "platform/stdin.hpp" +#include "platform/event_loop.hpp" - if (next == std::string_view::npos) { - parts.push_back(filter); - break; - } - - parts.push_back(filter.substr(0, next)); - filter.remove_prefix(next + 2); - } - - return parts; -} - -void filter_test_tree(rsl::testing::TestRoot& root, - std::string_view filter, - std::vector subfilters) { - if (filter.empty() || filter == "[.],*") { - return; - } +#include "output.hpp" - auto parts = split_filter_path(filter); +#include +#include +#include - for (auto part : std::ranges::reverse_view(parts)) { - subfilters.insert(subfilters.begin(), std::string(part)); - } - if (!subfilters.empty()) { - // cut off template arguments and parameters - subfilters.back() = base_name(subfilters.back()); - } - root.filter(subfilters); +void sigbus_handler(int sig) { + void* bt[20]; + int n = backtrace(bt, 20); + backtrace_symbols_fd(bt, n, STDERR_FILENO); + _exit(1); } -class[[= rsl::cli::description("rsl::test (in Catch2 v3.8.1 compatibility mode)")]] TestConfig +class[[= rsl::cli::description("rsl::test (in Catch2 v3.8.1 compatibility mode)")]] CLI : public rsl::cli { rsl::testing::TestRoot tree; std::vector sections; std::unique_ptr _output; public: - [[= positional]] std::string filter = ""; - [[= option]] std::string reporter = "plain"; - [[= option]] bool durations = true; - [[ = option, = flag ]] bool list_tests = false; - [[= option]] bool use_colour = true; + [[= positional]] std::string filter = ""; + [[= option]] std::string reporter = "plain"; + [[= option]] bool durations = true; + [[= option]] bool use_colour = true; + [[ = option, = flag ]] bool list_tests = false; + [[ = option, = flag ]] bool interactive = false; [[ = option, = shorthand("c") ]] void section(std::string part) { sections.emplace_back(std::move(part)); @@ -87,13 +46,9 @@ class[[= rsl::cli::description("rsl::test (in Catch2 v3.8.1 compatibility mode)" [[= option]] void verbosity(std::string level) {} - explicit TestConfig() - : tree(rsl::testing::get_tests()) - , _output(new rsl::testing::ConsoleOutput()) {} + explicit CLI() : tree(rsl::testing::get_tests()), _output(new rsl::testing::ConsoleOutput()) {} - void apply_filter() { - filter_test_tree(tree, filter, sections); - } + void apply_filter() {} static void print_tests(rsl::testing::TestNamespace const& current, std::size_t indent = 0) { auto current_indent = std::string(indent * 2, ' '); @@ -109,30 +64,28 @@ class[[= rsl::cli::description("rsl::test (in Catch2 v3.8.1 compatibility mode)" } } } +}; - void run() { - std::unique_ptr selected_reporter; - if (reporter.empty()) { - selected_reporter = rsl::testing::Reporter::make("plain"); - } else { - selected_reporter = rsl::testing::Reporter::make(reporter); - } +int main(int argc, char** argv) { + signal(SIGBUS, sigbus_handler); - if (list_tests) { - // tree.print(selected_reporter.get()); // TODO - selected_reporter->list_tests(tree); - } else { - tree.run(selected_reporter.get()); - } - selected_reporter->finalize(*_output); + using namespace rsl::testing::_impl_main; + const auto executable_path = std::filesystem::canonical("/proc/self/exe").parent_path(); + const std::filesystem::path config_path = executable_path / "test-runner.json"; + + auto runner = IncrementalRunner(config_path); + + auto args = CLI(); + args.parse_args(argc, argv); + + if (args.interactive) { + auto watch = Watcher(runner); + auto commands = TerminalCommand(runner, watch); + auto loop = EventLoop(commands, watch); + loop.run(); } -}; -#include -int main(int argc, char** argv) { - std::println(" {}", _rsl_test_run_with_coverage == nullptr); - auto config = TestConfig(); - config.parse_args(argc, argv); - config.apply_filter(); - config.run(); -} \ No newline at end of file + // if (watch_dependencies) { + // watch.update_dependencies(); + // } +} diff --git a/src/main/old_main.cpp b/src/main/old_main.cpp new file mode 100644 index 0000000..9bbd6b3 --- /dev/null +++ b/src/main/old_main.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include "output.hpp" + +std::string_view base_name(std::string_view name) { + auto lt = name.find('<'); + auto paren = name.find('('); + auto pos = std::min(lt, paren); + return pos == std::string_view::npos ? name : name.substr(0, pos); +} + +std::vector split_filter_path(std::string_view filter) { + if (filter.starts_with("::")) { + filter.remove_prefix(2); + } + + std::vector parts; + + while (true) { + auto next = filter.find("::"); + auto templ = filter.find('<'); + + if (templ != std::string_view::npos && (next == std::string_view::npos || templ < next)) { + parts.push_back(filter); + break; + } + + if (next == std::string_view::npos) { + parts.push_back(filter); + break; + } + + parts.push_back(filter.substr(0, next)); + filter.remove_prefix(next + 2); + } + + return parts; +} + +void filter_test_tree(rsl::testing::TestRoot& root, + std::string_view filter, + std::vector subfilters) { + if (filter.empty() || filter == "[.],*") { + return; + } + + auto parts = split_filter_path(filter); + + for (auto part : std::ranges::reverse_view(parts)) { + subfilters.insert(subfilters.begin(), std::string(part)); + } + if (!subfilters.empty()) { + // cut off template arguments and parameters + subfilters.back() = base_name(subfilters.back()); + } + root.filter(subfilters); +} + +class[[= rsl::cli::description("rsl::test (in Catch2 v3.8.1 compatibility mode)")]] TestConfig + : public rsl::cli { + rsl::testing::TestRoot tree; + std::vector sections; + std::unique_ptr _output; + +public: + [[= positional]] std::string filter = ""; + [[= option]] std::string reporter = "plain"; + [[= option]] bool durations = true; + [[ = option, = flag ]] bool list_tests = false; + [[= option]] bool use_colour = true; + + [[ = option, = shorthand("c") ]] void section(std::string part) { + sections.emplace_back(std::move(part)); + } + + [[= option]] void output(std::string filename) { + _output = std::make_unique(filename); + } + + [[= option]] void verbosity(std::string level) {} + + explicit TestConfig() + : tree(rsl::testing::get_tests()) + , _output(new rsl::testing::ConsoleOutput()) {} + + void apply_filter() { + filter_test_tree(tree, filter, sections); + } + + static void print_tests(rsl::testing::TestNamespace const& current, std::size_t indent = 0) { + auto current_indent = std::string(indent * 2, ' '); + for (auto const& ns : current.children) { + std::println("{}{}", current_indent, ns.name); + print_tests(ns, indent + 1); + } + + for (auto const& test : current.tests) { + std::println("{} - {}", current_indent, test.name); + for (auto const& run : test.get_tests()) { + std::println("{} - {}", std::string((indent + 1) * 2, ' '), run.name); + } + } + } + + void run() { + std::unique_ptr selected_reporter; + if (reporter.empty()) { + selected_reporter = rsl::testing::Reporter::make("plain"); + } else { + selected_reporter = rsl::testing::Reporter::make(reporter); + } + + if (list_tests) { + // tree.print(selected_reporter.get()); // TODO + selected_reporter->list_tests(tree); + } else { + tree.run(selected_reporter.get()); + } + selected_reporter->finalize(*_output); + } +}; + +#include +int main(int argc, char** argv) { + std::println(" {}", _rsl_test_run_with_coverage == nullptr); + auto config = TestConfig(); + config.parse_args(argc, argv); + config.apply_filter(); + config.run(); +} \ No newline at end of file diff --git a/src/main/output.hpp b/src/main/output.hpp index 2127271..be31392 100644 --- a/src/main/output.hpp +++ b/src/main/output.hpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include namespace rsl::testing { class ConsoleOutput : public Output { diff --git a/src/main/platform/CMakeLists.txt b/src/main/platform/CMakeLists.txt new file mode 100644 index 0000000..2ec4899 --- /dev/null +++ b/src/main/platform/CMakeLists.txt @@ -0,0 +1,10 @@ + +if(WIN32) +else() +target_sources(rsltest_main PRIVATE + posix/library.cpp + posix/taskset.cpp + posix/event_loop.cpp + posix/watch.cpp +) +endif() \ No newline at end of file diff --git a/src/main/platform/event_loop.hpp b/src/main/platform/event_loop.hpp new file mode 100644 index 0000000..243f6d5 --- /dev/null +++ b/src/main/platform/event_loop.hpp @@ -0,0 +1,66 @@ +#pragma once +#include +#include +#include +#include +#include +namespace rsl::testing::_impl_main { + +template +concept handler_like = requires(T& obj, std::span data) { + { obj.on_readable(data) } -> std::same_as; + { obj.get_handle() } -> std::same_as; +}; + +struct Dispatcher { + using dispatch_t = void(*)(void*, std::span); + void* obj; + dispatch_t dispatcher; + + void operator()(std::span data) const { + dispatcher(obj, data); + } +}; + +struct EventLoopImpl { +protected: + using dispatch_t = void(*)(void*, std::span); + std::span dispatchers; + + std::unordered_map map; + uintptr_t ep; + std::atomic running; + void init(); + void enable(uintptr_t handle, size_t idx); +public: + void run(); + void stop() { running = false; } +}; + +template +struct EventLoop : EventLoopImpl { + std::array callbacks; + std::array handler_fncs; + + EventLoop(Ts&... values) : callbacks({&values...}) { + template for (constexpr auto Idx : std::views::iota(0ZU, sizeof...(Ts))) { + handler_fncs[Idx] = {&values...[Idx], &dispatch}; + } + dispatchers = handler_fncs; + init(); + + size_t idx = 0; + (enable(values.get_handle(), idx++), ...); + } + + ~EventLoop() { + // close open handles + } + + template + static void dispatch(void* item, std::span data) { + static_cast(item)->on_readable(data); + } +}; + +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/platform/library.hpp b/src/main/platform/library.hpp new file mode 100644 index 0000000..6102bc9 --- /dev/null +++ b/src/main/platform/library.hpp @@ -0,0 +1,16 @@ +#pragma once +#include + +namespace rsl::testing::_impl_main { + + using library_handle = void*; + + library_handle load_library(std::string_view path); + void unload_library(library_handle& handle); + void* find_symbol(library_handle handle, std::string const& name); + + template + T* find_symbol(library_handle handle, std::string const& name) { + return reinterpret_cast(find_symbol(handle, name)); + } +} \ No newline at end of file diff --git a/src/main/platform/posix/event_loop.cpp b/src/main/platform/posix/event_loop.cpp new file mode 100644 index 0000000..3d9924a --- /dev/null +++ b/src/main/platform/posix/event_loop.cpp @@ -0,0 +1,97 @@ +#include "../event_loop.hpp" + +#include +#include +#include +#include + +namespace { +void make_nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} + +int init_epoll() { + int ep = epoll_create1(0); + if (ep < 0) { + perror("epoll_create1"); + } + return ep; +} + +void enable_fd(int ep, int fd) { + make_nonblocking(fd); + + epoll_event ev{}; + ev.events = EPOLLIN | EPOLLET; // edge-triggered + ev.data.fd = fd; + + if (epoll_ctl(ep, EPOLL_CTL_ADD, fd, &ev) < 0) { + perror("epoll_ctl"); + return; + } +} + +// void disable_fd(int ep, int fd) { +// epoll_ctl(ep, EPOLL_CTL_DEL, fd, nullptr); +// } + +} // namespace + +namespace rsl::testing::_impl_main { + +void EventLoopImpl::init() { + ep = init_epoll(); +} + +void EventLoopImpl::enable(uintptr_t handle, size_t idx) { + enable_fd((int)ep, (int)handle); + map[handle] = idx; +} + +void EventLoopImpl::run() { + running = true; + std::vector events(32); + + while (running) { + int n = epoll_wait((int)ep, events.data(), (int)events.size(), -1); + if (n < 0) { + if (errno == EINTR) { + continue; + } + perror("epoll_wait"); + return; + } + + for (int i = 0; i < n; ++i) { + int fd = events[i].data.fd; + std::vector buffer; + + while (true) { + char temp[4096]; + ssize_t r = read(fd, temp, sizeof(temp)); + + if (r > 0) { + buffer.append_range(std::span(temp, r)); + continue; // try reading more + } + if (r == 0) { + // EOF + close(fd); + break; + } + if (errno == EAGAIN || errno == EWOULDBLOCK) { + break; // drained fully + } + + perror("read"); + close(fd); + break; + } + + dispatchers[map[fd]](buffer); + } + } +} + +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/platform/posix/library.cpp b/src/main/platform/posix/library.cpp new file mode 100644 index 0000000..b320b34 --- /dev/null +++ b/src/main/platform/posix/library.cpp @@ -0,0 +1,23 @@ +#include + +#include +#include "../library.hpp" + +namespace rsl::testing::_impl_main { + +library_handle load_library(std::string_view path) { + return dlopen(std::string(path).c_str(), RTLD_NOW | RTLD_LOCAL); +} + +void unload_library(library_handle& handle) { + if (handle != nullptr) { + dlclose(handle); + } + handle = nullptr; +} + +void* find_symbol(library_handle handle, std::string const& name) { + return dlsym(handle, name.c_str()); +} + +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/platform/posix/taskset.cpp b/src/main/platform/posix/taskset.cpp new file mode 100644 index 0000000..f08b978 --- /dev/null +++ b/src/main/platform/posix/taskset.cpp @@ -0,0 +1,94 @@ +#include "../taskset.hpp" + +#include +#include +#include +#include +#include + +namespace rsl::testing::_impl_main { +namespace { +void pin_to_cpu(int cpu) { + cpu_set_t mask; + CPU_ZERO(&mask); + CPU_SET(cpu, &mask); + if (sched_setaffinity(0, sizeof(mask), &mask) != 0) { + perror("sched_setaffinity"); + _exit(1); + } +} + +ProcessResult run_program_on_cpu(int cpu, const char* program, char* const argv[]) { + int out_pipe[2], err_pipe[2]; + if (pipe(out_pipe) != 0 || pipe(err_pipe) != 0) { + perror("pipe"); + throw std::runtime_error("Failed to create pipe"); + } + + pid_t pid = fork(); + if (pid == 0) { + // redirect stdout/stderr + dup2(out_pipe[1], STDOUT_FILENO); + dup2(err_pipe[1], STDERR_FILENO); + close(out_pipe[0]); + close(out_pipe[1]); + close(err_pipe[0]); + close(err_pipe[1]); + if (cpu >= 0) { + pin_to_cpu(cpu); + } + + execvp(program, argv); + perror("execvp failed"); + _exit(1); + } else if (pid < 0) { + perror("fork failed"); + throw std::runtime_error("fork failed"); + } + + // close write ends + close(out_pipe[1]); + close(err_pipe[1]); + + std::string stdout_str; + std::string stderr_str; + char buffer[4096]; + ssize_t n = 0; + while ((n = read(out_pipe[0], &buffer[0], sizeof(buffer))) > 0) { + stdout_str.append(&buffer[0], n); + } + while ((n = read(err_pipe[0], &buffer[0], sizeof(buffer))) > 0) { + stderr_str.append(&buffer[0], n); + } + close(out_pipe[0]); + close(err_pipe[0]); + + // Wait for child + int status = 0; + waitpid(pid, &status, 0); + + int exit_code = 0; + if (WIFEXITED(status)) { + exit_code = WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + exit_code = 128 + WTERMSIG(status); + } else { + exit_code = -1; + } + + return {exit_code, stdout_str, stderr_str}; +} +} // namespace + +ProcessResult run_on_cpu(int cpu, std::string const& program, std::span argv) { + std::vector args; + if (argv.size() != 0 && argv[0] != program) { + args.push_back((char*)program.c_str()); + } + for (auto&& arg : argv) { + args.push_back((char*)arg.c_str()); + } + args.push_back(nullptr); + return run_program_on_cpu(cpu, program.data(), args.data()); +} +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/platform/posix/watch.cpp b/src/main/platform/posix/watch.cpp new file mode 100644 index 0000000..18f35f0 --- /dev/null +++ b/src/main/platform/posix/watch.cpp @@ -0,0 +1,218 @@ +#include "../watch.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "incremental.hpp" + +#include + +namespace { +constexpr std::uint32_t watch_mask = IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM | + IN_MOVED_TO | IN_ATTRIB | IN_DELETE_SELF | IN_MOVE_SELF; + +struct InotifyEvent { + int wd; + std::uint32_t mask; + std::uint32_t cookie; + std::string name; // empty when event->len == 0 +}; + +// Decode as many complete events as available in 'pending'. +// Leaves any trailing partial bytes in `pending` +static std::vector try_decode_events(std::vector& pending) { + std::vector out; + constexpr std::size_t header_size = sizeof(inotify_event::wd) + sizeof(inotify_event::mask) + + sizeof(inotify_event::cookie) + sizeof(inotify_event::len); + + std::size_t offset = 0; + while (true) { + if (pending.size() - offset < header_size) { + // not enough data, try again + break; + } + + InotifyEvent event{}; + + std::memcpy(&event.wd, pending.data() + offset, sizeof(inotify_event::wd)); + offset += sizeof(inotify_event::wd); + + std::memcpy(&event.mask, pending.data() + offset, sizeof(inotify_event::mask)); + offset += sizeof(inotify_event::mask); + + std::memcpy(&event.cookie, pending.data() + offset, sizeof(inotify_event::cookie)); + offset += sizeof(inotify_event::cookie); + + uint32_t len = 0; + std::memcpy(&len, pending.data() + offset, sizeof(inotify_event::len)); + offset += sizeof(inotify_event::len); + + // Total bytes this event occupies + if (pending.size() - offset < len) { + // not enough data, try again + offset -= header_size; + break; + } + + std::string name{}; + if (len > 0) { + name.assign(pending.data() + offset, pending.data() + offset + len); + if (!name.empty() && name.back() == '\0') { + // may include sentinel, strip it + name.pop_back(); + } + } + + out.push_back({event.wd, event.mask, event.cookie, name}); + offset += len; + } + + // Erase consumed bytes from the front of pending. + if (offset > 0) { + pending.erase(pending.begin(), pending.begin() + offset); + } + + return out; +} + +struct stat path_stat(std::filesystem::path const& path) { + struct stat out{}; + if (auto ret = ::stat(path.c_str(), &out); ret < 0) { + throw std::runtime_error(std::format("stat: {}", std::strerror(errno))); + } + return out; +} +} // namespace + +namespace rsl::testing::_impl_main { +struct WatcherImpl { + int fd = -1; + std::unordered_map last_modified; + + WatcherImpl(WatcherImpl const&) = delete; + WatcherImpl(WatcherImpl&&) = default; + + WatcherImpl& operator=(WatcherImpl const&) = delete; + WatcherImpl& operator=(WatcherImpl&&) = delete; + + WatcherImpl() : fd(::inotify_init1(IN_NONBLOCK)) { + if (fd < 0) { + throw std::runtime_error(std::format("inotify_init1: {}", std::strerror(errno))); + } + } + + ~WatcherImpl() { + if (fd >= 0) { + ::close(fd); + } + } + + [[nodiscard]] int add_watch(const std::filesystem::path& p) const { + int wd = ::inotify_add_watch(fd, p.c_str(), watch_mask); + if (wd < 0) { + throw std::runtime_error( + std::format("inotify_add_watch({}): {}", p.string(), std::strerror(errno))); + } + return wd; + } + + void rm_watch(int wd) { inotify_rm_watch(fd, wd); } +}; + +Watcher::Watcher(IncrementalRunner& runner) : impl(new WatcherImpl()), runner(&runner) {} +Watcher::~Watcher() noexcept { + delete impl; +} +uintptr_t Watcher::get_handle() const { + return impl->fd; +} + +void Watcher::on_readable(std::span data) { + constexpr auto debounce_threshold = std::chrono::milliseconds(500); + + pending.append_range(data); + auto events = try_decode_events(pending); + for (auto const& ev : events) { + auto it = std::ranges::find_if(watchers, [&](auto&& obj) { return obj.second == ev.wd; }); + std::filesystem::path dir = (it != watchers.end()) ? weakly_canonical(it->first) : std::filesystem::path{}; + std::filesystem::path full = ev.name.empty() ? dir : dir / ev.name; + + if ((ev.mask & IN_CREATE) && (ev.mask & IN_ISDIR)) { + add_watch(full, true); + } + + if ((ev.mask & IN_DELETE_SELF) || (ev.mask & IN_MOVE_SELF)) { + // watched directory was deleted or moved away, remove mapping + if (it != watchers.end()) { + impl->rm_watch(it->second); + watchers.erase(it); + } + file_deleted(full); + } + + if ((ev.mask & IN_MODIFY)) { + auto info = path_stat(full); + if (impl->last_modified[full] >= info.st_mtime) { + continue; + } + impl->last_modified[full] = info.st_mtime; + file_modified(full); + } + } +} + +void Watcher::add_watch(std::filesystem::path const& dir, bool recurse) { + if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) { + // throw std::runtime_error(std::format("not a directory: {}", dir.string())); + return; + } + + if (watchers.contains(dir)) { + // already watching + return; + } + + // std::println("watching {} for changes", dir.string()); + std::println("{}", nlohmann::json({{"action", "add_watch"}, {"path", dir.string()}}).dump()); + + int top_wd = impl->add_watch(dir); + watchers.emplace(dir, top_wd); + if (not recurse) { + return; + } + + for (auto const& ent : std::filesystem::recursive_directory_iterator(dir)) { + if (ent.is_directory()) { + try { + int wd = impl->add_watch(ent.path()); + watchers.emplace(ent.path(), wd); + } catch (const std::exception& ex) { + // Non-fatal: skip directories we can't watch (permission, etc.) + std::println("warning: cannot watch {}: {}", ent.path().string(), ex.what()); + } + } + } +} + +void Watcher::rm_watch(std::filesystem::path const& dir) { + auto it = watchers.find(dir); + if (it != watchers.end()) { + impl->rm_watch(it->second); + watchers.erase(it); + } +} + +} // namespace rsl::testing::_impl_main diff --git a/src/main/platform/stdin.hpp b/src/main/platform/stdin.hpp new file mode 100644 index 0000000..5bf7053 --- /dev/null +++ b/src/main/platform/stdin.hpp @@ -0,0 +1,82 @@ +#pragma once +#include +#include +#include + +#include +#include "watch.hpp" +#include "../incremental.hpp" + +#ifdef __unix__ +# include +#else +#endif + +namespace rsl::testing::_impl_main { +class TerminalCommand { + std::string pending; +#ifdef __unix__ + uintptr_t handle = STDIN_FILENO; +#else + uintptr_t handle = 0; +#endif + IncrementalRunner* runner = nullptr; + Watcher* watcher = nullptr; + +public: +TerminalCommand() = default; + explicit TerminalCommand(IncrementalRunner& runner, Watcher& watcher) + : runner(&runner) + , watcher(&watcher) {} + + [[nodiscard]] uintptr_t get_handle() const { return handle; } + + void on_readable(std::span data) { + pending.append_range(data); + if (auto it = pending.find('\n'); it != pending.npos) { + dispatch(std::string_view(pending.data(), it)); + pending = pending.substr(it + 1); + } + } + + void dispatch(std::string_view data) { + if (data == "exit") { + std::exit(0); + return; + } else if (data == "list") { + runner->list_all(); + return; + } else if (data == "run") { + runner->run_all(); + return; + } else if (data == "compdb") { + runner->update_compdb(); + return; + } + // all other commands require an argument, try splitting + std::string_view cmd; + std::string_view argument; + if (auto it = data.find(' '); it != data.npos) { + cmd = data.substr(0, it); + argument = data.substr(it + 1); + } else { + std::println("Missing argument or unrecognized command"); + return; + } + + if (cmd == "add_watch") { + watcher->add_watch(argument); + } else if (cmd == "rm_watch") { + watcher->rm_watch(argument); + } else if (cmd == "coverage") { + bool value = argument == "on"; + if (!value && argument != "off") { + std::println("Invalid argument - expected `on` or `off`"); + } + // TODO + } else { + std::println("Unrecognized command"); + } + } +}; +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/platform/taskset.hpp b/src/main/platform/taskset.hpp new file mode 100644 index 0000000..6801a30 --- /dev/null +++ b/src/main/platform/taskset.hpp @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include + +namespace rsl::testing::_impl_main { +struct ProcessResult { + int exit_code; + std::string stdout_str; + std::string stderr_str; +}; + +struct ProgramInvocation { + std::string program; + std::vector arguments; +}; + +ProcessResult run_on_cpu(int cpu, std::string const& program, std::span argv); +inline ProcessResult run_on_cpu(int cpu, ProgramInvocation const& invocation){ + // std::println("running {} {}", invocation.program, invocation.arguments); + return run_on_cpu(cpu, invocation.program, invocation.arguments); +} + +inline ProcessResult run_program(std::string const& program, std::span argv) { + return run_on_cpu(-1, program, argv); +} +} \ No newline at end of file diff --git a/src/main/platform/watch.hpp b/src/main/platform/watch.hpp new file mode 100644 index 0000000..1ff20e3 --- /dev/null +++ b/src/main/platform/watch.hpp @@ -0,0 +1,153 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include "../incremental.hpp" + +namespace rsl::testing::_impl_main { +struct WatcherImpl; + +struct HeaderDependencies { + std::filesystem::path file; + std::vector dependencies; +}; + +inline std::string_view trim(std::string_view data) { + auto first = data.find_first_not_of(" \t"); + auto last = data.find_last_not_of(" \t"); + if (first == last) { + return {}; + } + return data.substr(first, last - first + 1); +} + +inline std::vector parse_dependencies(std::string_view input) { + auto colon = input.find(':'); + if (colon == std::string_view::npos) { + return {}; + } + input.remove_prefix(colon + 1); + + std::vector deps; + + for (auto line : input | std::views::split('\n')) { + auto line_sv = std::string_view(&*line.begin(), std::ranges::distance(line)); + if (!line_sv.empty() && line_sv.back() == '\\') { + line_sv.remove_suffix(1); + } + line_sv = trim(line_sv); + if (!line_sv.empty()) { + deps.emplace_back(line_sv); + } + } + + return deps; +} + +inline bool is_relative_to(std::filesystem::path const& path, std::filesystem::path const& base) { + auto abs_path = weakly_canonical(path); + auto abs_base = weakly_canonical(base); + auto [it_path, it_base] = std::ranges::mismatch(abs_path, abs_base); + return it_base == abs_base.end(); +} + +class Watcher { + WatcherImpl* impl; + std::unordered_map watchers; + + std::vector pending; + IncrementalRunner* runner; + + // std::set tests; + std::unordered_map> dependencies; + +public: + Watcher() = delete; + explicit Watcher(IncrementalRunner& runner); + ~Watcher() noexcept; + + [[nodiscard]] uintptr_t get_handle() const; + void on_readable(std::span data); + void add_watch(std::filesystem::path const& dir, bool recurse = true); + void rm_watch(std::filesystem::path const& dir); + + void update_dependencies(std::filesystem::path const& path = {}) { + std::vector paths{}; + if (path.empty()) { + paths = runner->discover_tests(); + } else { + paths = {path}; + } + + for (auto&& tu : runner->expand_tests(paths, true)) { + runner->pool.submit(tu); + } + runner->pool.wait(); + + std::set folders; + for (auto [r, result] : runner->pool.collect()) { + if (result.exit_code != 0) { + continue; + } + + // TODO remove everything referring to r.source_path first + auto [it, _] = runner->units.insert({r.source_path, {}}); + + for (auto dependency : parse_dependencies(result.stdout_str)) { + dependency = canonical(dependency); + if (dependency == r.source_path) { + continue; + } + if (not is_relative_to(dependency, runner->config.project.project_path)) { + continue; + } + dependencies[dependency].insert(&it->first); + folders.insert(dependency.parent_path()); + } + } + + for (auto const& folder : folders) { + add_watch(folder); + } + } + + void file_modified(std::filesystem::path const& path) { + auto canonical = std::filesystem::canonical(path); + if (runner->units.contains(canonical)) { + // std::println("test modified: {}", canonical.string()); + std::println("{}", + nlohmann::json({ + {"action", "file_modified"}, + { "path", canonical.string()} + }) + .dump()); + + update_dependencies(canonical); + runner->recompile({path}).run(runner->reporter.get()); + } else { + // std::println("test dependency modified: {} {}", canonical.string(), + // dependencies[canonical].size()); + + std::vector affected; + for (auto* it : dependencies[canonical]) { + affected.push_back(*it); + } + std::println("{}", + nlohmann::json({ + { "action", "file_modified"}, + { "path", canonical.string()}, + {"dependency", true}, + { "affected", affected} + }) + .dump()); + runner->recompile(affected).run(runner->reporter.get()); + } + } + + void file_deleted(std::filesystem::path const& path) {} +}; +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/reporters/catch2xml.cpp b/src/main/reporters/catch2xml.cpp index 33338cb..eb5f749 100644 --- a/src/main/reporters/catch2xml.cpp +++ b/src/main/reporters/catch2xml.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include namespace rsl::testing::_xml_impl { diff --git a/src/main/reporters/json.cpp b/src/main/reporters/json.cpp index 29ccb08..f130232 100644 --- a/src/main/reporters/json.cpp +++ b/src/main/reporters/json.cpp @@ -1,9 +1,22 @@ -#include +#include #include #include #include "rsl/testing/result.hpp" +#include +#include +#include + namespace rsl::testing::_impl { + +std::string stringify_outcome(TestOutcome outcome) { + constexpr static auto names = + std::define_static_array(enumerators_of(^^TestOutcome) | std::views::transform([](auto x) { + return std::define_static_string(identifier_of(x)); + })); + return names[(size_t)outcome]; +} + class[[= rename("json")]] JsonReporter : public Reporter::Registrar { public: void before_run(TestNamespace const& tests) override {} @@ -11,8 +24,25 @@ class[[= rename("json")]] JsonReporter : public Reporter::Registrar results) override {} void exit_namespace(std::string_view name) override {} - void after_run() override {} + + nlohmann::json output = nlohmann::json::array(); + void after_test_group(std::span results) override { + for (auto const& result : results) { + output.push_back(nlohmann::json({ + { "name", result.name}, + {"full_name", result.test->full_name}, + { "duration", result.duration_ms}, + { "outcome", stringify_outcome(result.outcome)}, + {"exception", result.exception} + })); + } + } + + void after_run() override { + std::println("{}", nlohmann::json({{"action", "test_result"}, {"results", output}}).dump()); + std::fflush(stdout); + output.clear(); + } }; } // namespace rsl::testing::_impl diff --git a/src/main/reporters/terminal.cpp b/src/main/reporters/terminal.cpp index aa355ce..f42db4a 100644 --- a/src/main/reporters/terminal.cpp +++ b/src/main/reporters/terminal.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -20,13 +20,13 @@ class[[= rename("plain")]] ConsoleReporter : public Reporter::Registrarmessage); std::print("==== {}stdout{} ====\n{}\n", color[1], reset, result.stdout); std::print("==== {}stderr{} ====\n{}\n", color[1], reset, result.stderr); + } else { + std::print("[ SKIPPED ] {} ({:.3f} ms)\n", + result.name, + result.duration_ms); } + for (auto const& [file, coverage] : result.coverage) { std::println("Reached {} lines in file {}", coverage.size(), file); } + run_outcomes.push_back(result.outcome); for (auto const& assertion : result.assertions) { assertion_outcomes.push_back(assertion.success ? TestOutcome::PASS : TestOutcome::FAIL); diff --git a/src/main/reporters/xml.cpp b/src/main/reporters/xml.cpp index b352269..44d9a08 100644 --- a/src/main/reporters/xml.cpp +++ b/src/main/reporters/xml.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include namespace rsl::testing::_impl { diff --git a/src/main/runner.cpp b/src/main/runner.cpp new file mode 100644 index 0000000..4bea961 --- /dev/null +++ b/src/main/runner.cpp @@ -0,0 +1,5 @@ +#include "../incremental.hpp" + +namespace rsl::testing::_impl_main { + +} \ No newline at end of file diff --git a/src/runner.cpp b/src/runner.cpp index 381368a..18040df 100644 --- a/src/runner.cpp +++ b/src/runner.cpp @@ -9,149 +9,74 @@ #include #include #include -#include +#include #include -#include -#include +// #include +// #include #include "capture.hpp" #include namespace { -void cleanup_frames(cpptrace::stacktrace& trace, std::string_view test_name) { - std::vector frames; - for (auto const& frame : trace.frames | std::views::drop(1)) { - frames.push_back(frame); - if (cpptrace::prune_symbol(frame.symbol) == test_name) { - break; - } - } - trace.frames = frames; -} +// void cleanup_frames(cpptrace::stacktrace& trace, std::string_view test_name) { +// std::vector frames; +// for (auto const& frame : trace.frames | std::views::drop(1)) { +// frames.push_back(frame); +// if (cpptrace::prune_symbol(frame.symbol) == test_name) { +// break; +// } +// } +// trace.frames = frames; +// } } // namespace -void failure_handler(libassert::assertion_info const& info) { - // libassert::enable_virtual_terminal_processing_if_needed(); // for terminal colors on windows - constexpr bool Colorize = false; - auto width = libassert::terminal_width(libassert::stderr_fileno); - const auto& scheme = Colorize ? libassert::get_color_scheme() : libassert::color_scheme::blank; - std::string message = std::string(info.action()) + " at " + info.location() + ":"; - if (info.message) { - message += " " + *info.message; - } - message += "\n"; - message += - info.statement(scheme) + info.print_binary_diagnostics(width, scheme) + - info.print_extra_diagnostics(width, scheme); // + info.print_stacktrace(width, scheme); - - auto trace = info.get_stacktrace(); - cleanup_frames(trace, rsl::testing::_testing_impl::assertion_counter().test_name); - message += trace.to_string(Colorize); - throw rsl::testing::assertion_failure( - message, - rsl::source_location(info.file_name, info.function, info.line)); -} - -void print_tests(rsl::testing::TestNamespace const& current, std::size_t indent = 0) { - auto current_indent = std::string(indent * 2, ' '); - for (auto const& ns : current.children) { - std::println("{}{}", current_indent, ns.name); - print_tests(ns, indent + 1); - } - - for (auto const& test : current.tests) { - std::println("{}- {}", current_indent, test.name); - for (auto const& run : test.get_tests()) { - std::println("{}- {}", std::string((indent + 1) * 2, ' '), run.name); - } - } -} +// void failure_handler(libassert::assertion_info const& info) { +// // libassert::enable_virtual_terminal_processing_if_needed(); // for terminal colors on windows +// constexpr bool Colorize = false; +// auto width = libassert::terminal_width(libassert::stderr_fileno); +// const auto& scheme = Colorize ? libassert::get_color_scheme() : libassert::color_scheme::blank; +// std::string message = std::string(info.action()) + " at " + info.location() + ":"; +// if (info.message) { +// message += " " + *info.message; +// } +// message += "\n"; +// message += +// info.statement(scheme) + info.print_binary_diagnostics(width, scheme) + +// info.print_extra_diagnostics(width, scheme); // + info.print_stacktrace(width, scheme); + +// auto trace = info.get_stacktrace(); +// cleanup_frames(trace, rsl::testing::_testing_impl::assertion_counter().test_name); +// message += trace.to_string(Colorize); +// throw rsl::testing::assertion_failure( +// message, +// rsl::source_location(info.file_name, info.function, info.line)); +// } namespace rsl::testing { -void Reporter::list_tests(TestNamespace const& tests) { - print_tests(tests); -} - -bool TestRoot::run(Reporter* reporter) { - libassert::set_failure_handler(failure_handler); - std::println("failure handler set"); - reporter->before_run(*this); - bool status = TestNamespace::run(reporter); - libassert::set_failure_handler(libassert::default_failure_handler); - // TODO after_run - reporter->after_run(); - return status; -} - -bool TestNamespace::run(Reporter* reporter) { - if (!name.empty()) { - reporter->enter_namespace(name); - } - bool status = true; - for (auto& ns : children) { - status &= ns.run(reporter); - } - - for (auto& test : tests) { - auto runs = test.get_tests(); - reporter->before_test_group(test); - std::vector results; - if (!test.skip()) { - for (auto const& test_run : test.get_tests()) { - auto& tracker = _testing_impl::assertion_counter(); - tracker.assertions = {}; - tracker.test_name = join_str(test.full_name, "::"); - - reporter->before_test(test_run); - auto result = test_run.run(); - result.assertions = tracker.assertions; - - reporter->after_test(result); - results.push_back(result); - } - } else { - reporter->before_test(TestCase{&test, +[] {}, std::string(test.name)}); - - // TODO stringify skipped tests properly - auto result = Result{&test, std::string(test.name) + "(...)", TestOutcome::SKIP}; - reporter->after_test(result); - results.push_back(result); - } - - reporter->after_test_group(results); - } - if (!name.empty()) { - reporter->exit_namespace(name); - } - return status; -} - namespace { -void run_test(void const* test) { - (*static_cast const*>(test))(); -} -auto resolve_pc(std::uintptr_t pc) { - auto raw_trace = cpptrace::raw_trace{{pc}}; - auto trace = raw_trace.resolve(); - return trace.frames[0]; -} + +// auto resolve_pc(std::uintptr_t pc) { +// auto raw_trace = cpptrace::raw_trace{{pc}}; +// auto trace = raw_trace.resolve(); +// return trace.frames[0]; +// } auto filter_coverage(rsl::coverage::CoverageReport* data, std::size_t size) { std::unordered_map> coverage; - for (std::size_t idx = 0; idx < size; ++idx) { - auto resolved = resolve_pc(data[idx].pc); - if (resolved.filename.empty() || (int)resolved.line.value() < 0) { - continue; - } - if (resolved.filename.contains("/../include/c++/")) { - continue; - } - coverage[resolved.filename].push_back({resolved.line.value(), data[idx].hits}); - } + // for (std::size_t idx = 0; idx < size; ++idx) { + // auto resolved = resolve_pc(data[idx].pc); + // if (resolved.filename.empty() || (int)resolved.line.value() < 0) { + // continue; + // } + // if (resolved.filename.contains("/../include/c++/")) { + // continue; + // } + // coverage[resolved.filename].push_back({resolved.line.value(), data[idx].hits}); + // } std::vector result; for (auto const& [name, cov] : coverage) { @@ -161,52 +86,4 @@ auto filter_coverage(rsl::coverage::CoverageReport* data, std::size_t size) { } } // namespace -Result TestCase::run() const { - auto ret = Result{.test = test, .name = name}; - try { - // Capture _out(stdout, ret.stdout); - // Capture _err(stderr, ret.stderr); - - auto t0 = std::chrono::steady_clock::now(); - if (_rsl_test_run_with_coverage != nullptr) { - // rsltest_cov was linked in -> run with coverage - rsl::coverage::CoverageReport* reports = nullptr; - std::size_t report_count = 0; - auto finalize = [&] { - ret.coverage = filter_coverage(reports, report_count); - free(reports); - }; - try { - _rsl_test_run_with_coverage(run_test, - static_cast(&fnc), - &reports, - &report_count); - finalize(); - } catch (...) { - finalize(); - throw; - } - } else { - fnc(); - } - auto t1 = std::chrono::steady_clock::now(); - - ret.outcome = TestOutcome(!test->expect_failure); - ret.duration_ms = std::chrono::duration(t1 - t0).count(); - return ret; - } catch (assertion_failure const& failure) { - ret.failure = failure; - } catch (std::exception const& exc) { // - ret.exception += exc.what(); - } catch (std::string const& msg) { // - ret.exception += msg; - } catch (std::string_view msg) { // - ret.exception += msg; - } catch (char const* msg) { // - ret.exception += msg; - } catch (...) { ret.exception += "unknown exception thrown"; } - - ret.outcome = TestOutcome(test->expect_failure); - return ret; -} } // namespace rsl::testing \ No newline at end of file diff --git a/src/test.cpp b/src/test.cpp deleted file mode 100644 index 93890c2..0000000 --- a/src/test.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace rsl::testing { -namespace _testing_impl { -std::set& registry() { - static std::set data; - return data; -} - -AssertionTracker& assertion_counter() { - static AssertionTracker counter{}; - return counter; -} -} // namespace _testing_impl - -TestNamespace::iterator::iterator(TestNamespace const& ns) { - flatten(ns); - current = elements.front(); - elements.pop_front(); -} - -void TestNamespace::iterator::flatten(TestNamespace const& current) { - for (auto const& ns : current.children) { - flatten(ns); - } - if (!current.tests.empty()) { - elements.push_back({current.tests.begin(), current.tests.end()}); - } -} - -TestNamespace::iterator& TestNamespace::iterator::operator++() { - if (current.it == current.end) { - if (elements.empty()) { - current = {}; - return *this; - } - - current = elements.front(); - elements.pop_front(); - } else { - ++current.it; - if (current.it == current.end) { - return ++*this; - } - } - return *this; -} - -bool TestNamespace::iterator::operator==(iterator const& other) const { - if (current.it != other.current.it || current.end != other.current.end) { - return false; - } - return elements == other.elements; -} - -void TestNamespace::insert(Test const& test, std::size_t i) { - if (i == test.full_name.size() - 1) { - tests.push_back(test); - return; - } - - auto it = std::ranges::find_if(children, [&](const TestNamespace& ns) { - return ns.name == test.full_name[i]; - }); - - if (it == children.end()) { - children.emplace_back(test.full_name[i]); - it = std::prev(children.end()); - } - - it->insert(test, i + 1); -} - -std::size_t TestNamespace::count() const { - std::size_t total = tests.size(); - for (auto const& ns : children) { - total += ns.count(); - } - return total; -} - -void TestNamespace::filter(std::span parts) { - if (parts.empty()) { - return; - } - - std::string_view current = parts.front(); - std::span next = parts.subspan(1); - - auto it = std::ranges::find_if(children, [&](TestNamespace& ns) { return ns.name == current; }); - - if (it != children.end()) { - tests.clear(); - it->filter(next); - if (it->children.empty() && it->tests.empty()) { - children.clear(); - } else { - children = {*it}; - } - return; - } else { - std::erase_if(tests, [&](const Test& t) { return t.name != current; }); - children.clear(); - } -} - -TestRoot get_tests() { - TestRoot root; - for (auto test_def : rsl::testing::_testing_impl::registry()) { - auto test = test_def(); - root.insert(test); - } - return root; -} - -} // namespace rsl::testing \ No newline at end of file diff --git a/src/test_namespace.hpp b/src/test_namespace.hpp new file mode 100644 index 0000000..168fbbe --- /dev/null +++ b/src/test_namespace.hpp @@ -0,0 +1,201 @@ +// #pragma once +// struct Reporter; +// struct TestNamespace { +// std::string_view name; +// std::vector tests; +// std::vector children; + +// class iterator { +// struct single_iterator { +// std::vector::const_iterator it; +// std::vector::const_iterator end; + +// bool operator==(single_iterator const& other) const { +// return it == other.it && end == other.end; +// } +// }; + +// single_iterator current; +// std::deque elements; + +// void flatten(TestNamespace const& current) { +// for (auto const& ns : current.children) { +// flatten(ns); +// } +// if (!current.tests.empty()) { +// elements.push_back({current.tests.begin(), current.tests.end()}); +// } +// } + +// public: +// using iterator_category = std::input_iterator_tag; +// using value_type = Test; +// using difference_type = std::ptrdiff_t; +// using pointer = Test const*; +// using reference = Test const&; + +// iterator() = default; +// explicit iterator(TestNamespace const& ns) { +// flatten(ns); +// current = elements.front(); +// elements.pop_front(); +// } + +// Test const& operator*() const { return *current.it; } +// Test const* operator->() const { return &operator*(); } +// iterator& operator++() { +// if (current.it == current.end) { +// if (elements.empty()) { +// current = {}; +// return *this; +// } + +// current = elements.front(); +// elements.pop_front(); +// } else { +// ++current.it; +// if (current.it == current.end) { +// return ++*this; +// } +// } +// return *this; +// } +// bool operator==(iterator const& other) const { +// if (current.it != other.current.it || current.end != other.current.end) { +// return false; +// } +// return elements == other.elements; +// } +// }; + +// [[nodiscard]] bool is_empty() const { return tests.empty() && children.empty(); } +// [[nodiscard]] iterator begin() const { return iterator{*this}; } +// [[nodiscard]] static iterator end() { return {}; } +// void insert(Test const& test, size_t i = 0) { +// if (i == test.full_name.size() - 1) { +// tests.push_back(test); +// return; +// } + +// auto it = std::ranges::find_if(children, [&](const TestNamespace& ns) { +// return ns.name == test.full_name[i]; +// }); + +// if (it == children.end()) { +// children.emplace_back(test.full_name[i]); +// it = std::prev(children.end()); +// } + +// it->insert(test, i + 1); +// } +// void remove_by_path(std::string_view path) { +// auto matches_module_path = [&](Test const& test) { +// return std::filesystem::path(test.module_path) == std::filesystem::path(path); +// }; + +// auto is_empty_namespace = [](TestNamespace const& ns) { return ns.is_empty(); }; + +// for (auto& ns : children) { +// ns.remove_by_path(path); +// } +// std::erase_if(children, is_empty_namespace); +// std::erase_if(tests, matches_module_path); +// } + +// [[nodiscard]] std::size_t count() const { +// std::size_t total = tests.size(); +// for (auto const& ns : children) { +// total += ns.count(); +// } +// return total; +// } + +// void filter(std::span parts) { +// if (parts.empty()) { +// return; +// } + +// std::string_view current = parts.front(); +// std::span next = parts.subspan(1); + +// auto it = std::ranges::find_if(children, [&](TestNamespace& ns) { return ns.name == current; }); + +// if (it != children.end()) { +// tests.clear(); +// it->filter(next); +// if (it->children.empty() && it->tests.empty()) { +// children.clear(); +// } else { +// children = {*it}; +// } +// return; +// } else { +// std::erase_if(tests, [&](const Test& t) { return t.name != current; }); +// children.clear(); +// } +// } +// bool run(Reporter* reporter) { +// if (!name.empty()) { +// reporter->enter_namespace(name); +// } +// bool status = true; +// for (auto& ns : children) { +// status &= ns.run(reporter); +// } + +// for (auto& test : tests) { +// auto runs = test.get_tests(); +// reporter->before_test_group(test); +// std::vector results; +// if (!test.skip()) { +// for (auto const& test_run : test.get_tests()) { +// auto& tracker = _testing_impl::assertion_counter(); +// tracker.assertions = {}; +// tracker.test_name = join_str(test.full_name, "::"); + +// reporter->before_test(test_run); +// auto result = test_run.run(); +// result.assertions = tracker.assertions; + +// reporter->after_test(result); +// results.push_back(result); +// } +// } else { +// reporter->before_test(TestCase{&test, +[] {}, std::string(test.name)}); + +// // TODO stringify skipped tests properly +// auto result = Result{&test, std::string(test.name) + "(...)", TestOutcome::SKIP}; +// reporter->after_test(result); +// results.push_back(result); +// } + +// reporter->after_test_group(results); +// } +// if (!name.empty()) { +// reporter->exit_namespace(name); +// } +// return status; +// } +// }; + +// struct TestRoot : TestNamespace { +// bool run(Reporter* reporter, bool summarize = true) { +// // libassert::set_failure_handler(failure_handler); +// // std::println("failure handler set"); +// reporter->before_run(*this); +// bool status = TestNamespace::run(reporter); +// // libassert::set_failure_handler(libassert::default_failure_handler); +// // TODO after_run +// reporter->after_run(); +// return status; +// } +// }; + +// TestRoot get_tests() { +// TestRoot root; +// for (auto test_def : rsl::testing::_testing_impl::registry()) { +// auto test = test_def({}); +// root.insert(test); +// } +// return root; +// } \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index dcc3b97..0000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -target_sources(rsltest_test PRIVATE - always_passes.cpp -) \ No newline at end of file diff --git a/test/dummy.cpp b/test/dummy.cpp new file mode 100644 index 0000000..e69de29