From 81ea08d54648b07b90e3657f60f31452c53d5f7a Mon Sep 17 00:00:00 2001 From: Tsche Date: Mon, 17 Nov 2025 03:21:22 +0000 Subject: [PATCH 01/15] start implementing new execution model --- CMakeLists.txt | 13 +- cmake/rsl-test.cmake | 270 ++++++++++++++++++++++++++++ conanfile.py | 3 +- src/CMakeLists.txt | 2 +- src/main/CMakeLists.txt | 3 +- src/main/compile_pool.hpp | 104 +++++++++++ src/main/config_parser.hpp | 169 +++++++++++++++++ src/main/main.cpp | 194 +++++++++----------- src/main/old_main.cpp | 138 ++++++++++++++ src/main/platform/CMakeLists.txt | 8 + src/main/platform/library.hpp | 16 ++ src/main/platform/posix/library.cpp | 20 +++ src/main/platform/posix/taskset.cpp | 92 ++++++++++ src/main/platform/posix/watch.hpp | 213 ++++++++++++++++++++++ src/main/platform/taskset.hpp | 24 +++ src/main/platform/watch.hpp | 0 src/main/reporters/terminal.cpp | 12 +- test/CMakeLists.txt | 3 - test/dummy.cpp | 0 19 files changed, 1156 insertions(+), 128 deletions(-) create mode 100644 cmake/rsl-test.cmake create mode 100644 src/main/compile_pool.hpp create mode 100644 src/main/config_parser.hpp create mode 100644 src/main/old_main.cpp create mode 100644 src/main/platform/CMakeLists.txt create mode 100644 src/main/platform/library.hpp create mode 100644 src/main/platform/posix/library.cpp create mode 100644 src/main/platform/posix/taskset.cpp create mode 100644 src/main/platform/posix/watch.hpp create mode 100644 src/main/platform/taskset.hpp create mode 100644 src/main/platform/watch.hpp delete mode 100644 test/CMakeLists.txt create mode 100644 test/dummy.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e1a614..4be27ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,12 +43,15 @@ 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 testing + SOURCES test/ example/ + # PREFIX test_ + # PREFIX_REQUIRED + ) # enable_testing() # add_test(NAME rsltest_test COMMAND rsltest_example) diff --git a/cmake/rsl-test.cmake b/cmake/rsl-test.cmake new file mode 100644 index 0000000..38fe4ae --- /dev/null +++ b/cmake/rsl-test.cmake @@ -0,0 +1,270 @@ +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(TYPE ${dep_fixed} dep_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) + + # _impl_property(_dep_link_libs INTERFACE_LINK_LIBRARIES TARGET _dep_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}\", + \"${_lang}\": { + \"standard\": ${_std}, + \"extensions\": ${_std_ext} + } + } + } + }") + + 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/src/CMakeLists.txt b/src/CMakeLists.txt index 457a909..e0e8c56 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,4 +5,4 @@ target_sources(rsltest PUBLIC ) add_subdirectory(main) -add_subdirectory(coverage) \ No newline at end of file +add_subdirectory(coverage) diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index 4a6180f..3e733d2 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/compile_pool.hpp b/src/main/compile_pool.hpp new file mode 100644 index 0000000..6b565dd --- /dev/null +++ b/src/main/compile_pool.hpp @@ -0,0 +1,104 @@ +#pragma once +#include +#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 + } + } + + std::vector collect() { + wait(); + 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(); + } + + output_type result = {task, run_on_cpu(cpu, task.invocation)}; + { + 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..371380c --- /dev/null +++ b/src/main/config_parser.hpp @@ -0,0 +1,169 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include +#include "platform/taskset.hpp" + +namespace rsl::testing::_impl_main { +struct TestTU { + ProgramInvocation invocation; + std::filesystem::path out_path; +}; + +class ConfigParser { + static nlohmann::json json_from_file(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; + } + +public: + explicit ConfigParser(std::filesystem::path const& path) : ConfigParser(json_from_file(path)) {} + + explicit ConfigParser(nlohmann::json cfg) : config_(std::move(cfg)) { + validate_project_section(); + } + + static std::vector discover_tests(const std::filesystem::path& root) { + static constexpr std::array allowed{".cpp"}; + std::vector result; + + for (auto const& entry : std::filesystem::recursive_directory_iterator(root)) { + if (entry.is_regular_file()) { + auto const ext = entry.path().extension().string(); + if (std::ranges::contains(allowed, ext)) + result.push_back(entry.path()); + } + } + return result; + } + + [[nodiscard]] + TestTU make_invocation(std::string_view config_name, + const std::filesystem::path& test_path) const { + const auto& project = config_.at("project"); + const auto& options = config_.value("options", nlohmann::json::object()); + const auto& configurations = config_.at("configurations"); + + const std::filesystem::path build_path = project.at("build_path").get(); + const std::filesystem::path project_path = project.at("project_path").get(); + + const nlohmann::json& cfg = configurations.at(config_name.data()); + + const bool ext = cfg["CXX"].value("extensions", false); + const int ver = cfg["CXX"].value("standard", 17); + const std::string standard = std::format("-std={}++{}", (ext ? "gnu" : "c"), ver); + const std::string compiler_path = cfg.value("compiler_path", "c++"); + + std::filesystem::path out_path = + build_path / std::filesystem::relative(test_path, project_path); + out_path.replace_extension(".so"); + + std::vector cmd; + + cmd.push_back(compiler_path); + cmd.push_back(standard); + + if (options.contains("compile_options")) { + for (auto const& o : options["compile_options"]) { + auto value = o.get(); + if (!value.empty()) { + cmd.push_back(value); + } + } + } + + if (options.contains("include_dirs")) { + for (auto const& p : options["include_dirs"]) { + auto s = p.get(); + if (!s.empty()) { + cmd.push_back(std::format("-I{}", s)); + } + } + } + + if (options.contains("compile_definitions")) { + for (auto const& d : options["compile_definitions"]) { + auto value = d.get(); + if (!value.empty()) { + cmd.push_back(std::format("-D{}", value)); + } + } + } + + if (options.contains("link_libraries")) { + for (auto const& lib : options["link_libraries"]) { + auto value = lib.get(); + if (!value.empty()) { + if (value[0] == '/') { + cmd.push_back(std::format("-L{}", value)); + } else { + cmd.push_back(std::format("-l{}", value)); + } + } + } + } + + // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); + // cmd.push_back(std::format("-L{}", build_path.string())); + cmd.emplace_back("-fPIC"); + cmd.emplace_back("-shared"); + + // out file + cmd.emplace_back("-o"); + cmd.push_back(out_path.string()); + + // input file + cmd.push_back(test_path.string()); + + return {compiler_path, cmd, out_path}; + } + + std::vector expand() const { + const auto& project = config_.at("project"); + std::vector tests; + + for (auto const& p : project.at("test_path")) { + for (auto&& t : discover_tests(std::filesystem::path(p.get()))) { + tests.push_back(std::move(t)); + } + } + + std::vector test_tus; + for (auto const& [name, _] : config_.at("configurations").items()) { + for (auto const& test : tests) { + test_tus.push_back(make_invocation(name, test)); + } + } + return test_tus; + } + + nlohmann::json config_; +private: + + void validate_project_section() { + if (!config_.contains("project")) { + throw std::runtime_error("Missing required 'project' section in test-runner.nlohmann::json"); + } + + const auto& project = config_["project"]; + for (std::string_view key : {"build_path", "project_path", "test_path"}) { + if (!project.contains(key)) { + throw std::runtime_error(std::format("Missing required project.{} field", key)); + } + } + } +}; + +} // 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..46314a0 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -1,138 +1,104 @@ -#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); - } +#include +#include +#include - std::vector parts; +#include +#include - while (true) { - auto next = filter.find("::"); - auto templ = filter.find('<'); +#include "compile_pool.hpp" +#include "config_parser.hpp" +#include - if (templ != std::string_view::npos && (next == std::string_view::npos || templ < next)) { - parts.push_back(filter); - break; - } +#include "platform/library.hpp" - if (next == std::string_view::npos) { - parts.push_back(filter); - break; - } +#include "platform/posix/watch.hpp" +#include "rsl/testing/output.hpp" - parts.push_back(filter.substr(0, next)); - filter.remove_prefix(next + 2); - } +struct TestSet { + void* handle; + std::set tests; +}; - return parts; -} +int main() { + using namespace rsl::testing::_impl_main; + constexpr bool incremental = false; -void filter_test_tree(rsl::testing::TestRoot& root, - std::string_view filter, - std::vector subfilters) { - if (filter.empty() || filter == "[.],*") { - return; - } + const auto executable_path = std::filesystem::canonical("/proc/self/exe").parent_path(); + const std::filesystem::path config_path = executable_path / "test-runner.json"; - auto parts = split_filter_path(filter); + ConfigParser runner(config_path); + auto test_tus = runner.expand(); // TODO split config and test discovery - 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)); + CompilePool pool{}; + for (auto&& tu : test_tus) { + pool.submit(tu); } - [[= option]] void output(std::string filename) { - _output = std::make_unique(filename); - } + std::unordered_map test_sets; - [[= option]] void verbosity(std::string level) {} + for (auto&& [tu, result] : pool.collect()) { + if (result.exit_code != 0) { + std::println("ERROR!"); + continue; + } - explicit TestConfig() - : tree(rsl::testing::get_tests()) - , _output(new rsl::testing::ConsoleOutput()) {} + if (not rsl::testing::_testing_impl::registry().empty()) { + std::println("test registry not empty"); + rsl::testing::_testing_impl::registry().clear(); + } + // check if we have the key already, make sure old one is unloaded - void apply_filter() { - filter_test_tree(tree, filter, sections); + void* handle = load_library(tu.out_path.string()); + if (handle != nullptr) { + test_sets[tu.out_path] = {handle, rsl::testing::_testing_impl::registry()}; + } + rsl::testing::_testing_impl::registry().clear(); } - 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); - } + if (not rsl::testing::_testing_impl::registry().empty()) { + std::println("test registry not empty"); + } - 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); - } + rsl::testing::TestRoot root; + for (auto&& [path, test_set] : test_sets) { + std::println("{} -> {}", path.string(), test_set.tests.size()); + for (auto test : test_set.tests) { + root.insert(test()); } } - 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); - } + std::unique_ptr selected_reporter; + selected_reporter = rsl::testing::Reporter::make("plain"); + root.run(selected_reporter.get()); - if (list_tests) { - // tree.print(selected_reporter.get()); // TODO - selected_reporter->list_tests(tree); - } else { - tree.run(selected_reporter.get()); - } - selected_reporter->finalize(*_output); + for (auto& [path, test_set] : test_sets) { + dlclose(test_set.handle); + test_set.tests = {}; } -}; -#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(); + if (incremental) { + Watcher watch{}; + const auto test_paths = runner.config_.at("project").at("test_path"); + for (auto path : test_paths) { + watch.add_directory(path); + } + watch.watch([&](auto path, FileEvent event) { + if ((event & FileEvent::MODIFY) == FileEvent::MODIFY) { + // compile TU + + if (auto it = test_sets.find(path); it != test_sets.end()) { + TestSet& set = it->second; + + // clean up old test + if (set.handle != nullptr) { + dlclose(set.handle); + set.handle = nullptr; + } + set.tests = {}; + } + + // load TU + } + }); + } } \ No newline at end of file diff --git a/src/main/old_main.cpp b/src/main/old_main.cpp new file mode 100644 index 0000000..432153f --- /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/platform/CMakeLists.txt b/src/main/platform/CMakeLists.txt new file mode 100644 index 0000000..f5f2302 --- /dev/null +++ b/src/main/platform/CMakeLists.txt @@ -0,0 +1,8 @@ + +if(WIN32) +else() +target_sources(rsltest_main PRIVATE + posix/library.cpp + posix/taskset.cpp +) +endif() \ 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..62d76ac --- /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_view name); + + template + T* find_symbol(library_handle handle, std::string_view name) { + return reinterpret_cast(find_symbol(handle, name)); + } +} \ 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..2785c17 --- /dev/null +++ b/src/main/platform/posix/library.cpp @@ -0,0 +1,20 @@ +#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); +} + +void unload_library(library_handle handle) { + dlclose(handle); +} + +void* find_symbol(library_handle handle, std::string_view name) { + return dlsym(handle, std::string(name).c_str()); +} + +} // namespace rsl::testing::_main_impl \ 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..459907a --- /dev/null +++ b/src/main/platform/posix/taskset.cpp @@ -0,0 +1,92 @@ +#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]); + + 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.hpp b/src/main/platform/posix/watch.hpp new file mode 100644 index 0000000..6677f52 --- /dev/null +++ b/src/main/platform/posix/watch.hpp @@ -0,0 +1,213 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 Inotify { + int fd = -1; + Inotify(Inotify const&) = delete; + Inotify(Inotify&&) = default; + + Inotify& operator=(Inotify const&) = delete; + Inotify& operator=(Inotify&&) = delete; + + Inotify() : fd(::inotify_init1(IN_NONBLOCK)) { + if (fd < 0) { + throw std::runtime_error(std::format("inotify_init1: {}", std::strerror(errno))); + } + } + + ~Inotify() { + 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); + } +}; + +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; +} + +enum FileEvent { + ACCESS = 0x00000001, /* File was accessed. */ + MODIFY = 0x00000002, /* File was modified. */ + ATTRIB = 0x00000004, /* Metadata changed. */ + CLOSE_WRITE = 0x00000008, /* Writtable file was closed. */ + CLOSE_NOWRITE = 0x00000010, /* Unwrittable file closed. */ + CLOSE = (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE), /* Close. */ + OPEN = 0x00000020, /* File was opened. */ + MOVED_FROM = 0x00000040, /* File was moved from X. */ + MOVED_TO = 0x00000080, /* File was moved to Y. */ + MOVE = (IN_MOVED_FROM | IN_MOVED_TO), /* Moves. */ + CREATE = 0x00000100, /* Subfile was created. */ + DELETE = 0x00000200, /* Subfile was deleted. */ + DELETE_SELF = 0x00000400, /* Self was deleted. */ + MOVE_SELF = 0x00000800, /* Self was moved. */ + + ISDIR = 0x40000000, + + /* Events sent by the kernel. */ + UNMOUNT = 0x00002000, /* Backing fs was unmounted. */ + Q_OVERFLOW = 0x00004000, /* Event queued overflowed. */ + IGNORED = 0x00008000, /* File was ignored. */ +}; + +struct Watcher { + std::unordered_map watchers; + Inotify inotify; + + void add_directory(std::filesystem::path const& dir, bool recurse = true) { + if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) { + // throw std::runtime_error(std::format("not a directory: {}", dir.string())); + return; + } + + int top_wd = inotify.add_watch(dir); + watchers.emplace(top_wd, dir); + if (not recurse) { + return; + } + + for (auto const& ent : std::filesystem::recursive_directory_iterator(dir)) { + if (ent.is_directory()) { + try { + int wd = inotify.add_watch(ent.path()); + watchers.emplace(wd, ent.path()); + } 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 watch(auto&& fnc) { + std::vector pending; + pending.reserve(8192); + std::array tmpbuf; + + while (true) { + ssize_t n = ::read(inotify.fd, tmpbuf.data(), static_cast(tmpbuf.size())); + if (n < 0) { + if (errno == EAGAIN) { + ::usleep(100000); + continue; + } + std::println("read error: {}", std::strerror(errno)); + break; + } + if (n == 0) { + continue; + } + pending.insert(pending.end(), tmpbuf.data(), tmpbuf.data() + static_cast(n)); + + // decode complete events; any partial event bytes remain in pending + auto events = try_decode_events(pending); + + for (auto const& ev : events) { + auto it = watchers.find(ev.wd); + std::filesystem::path dir = (it != watchers.end()) ? it->second : std::filesystem::path{}; + std::filesystem::path full = ev.name.empty() ? dir : dir / ev.name; + fnc(full, FileEvent(ev.mask)); + + if ((ev.mask & FileEvent::CREATE) && (ev.mask & IN_ISDIR)) { + add_directory(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()) { + inotify.rm_watch(it->first); + watchers.erase(it); + } + } + } + } + } +}; diff --git a/src/main/platform/taskset.hpp b/src/main/platform/taskset.hpp new file mode 100644 index 0000000..4dbb1e5 --- /dev/null +++ b/src/main/platform/taskset.hpp @@ -0,0 +1,24 @@ +#pragma once +#include +#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); +} +} \ 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..e69de29 diff --git a/src/main/reporters/terminal.cpp b/src/main/reporters/terminal.cpp index aa355ce..a9cc133 100644 --- a/src/main/reporters/terminal.cpp +++ b/src/main/reporters/terminal.cpp @@ -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/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 From b98fb70c4407279199b67cacaf264513c3fb531c Mon Sep 17 00:00:00 2001 From: Tsche Date: Thu, 20 Nov 2025 04:54:37 +0000 Subject: [PATCH 02/15] some work on continuous mode --- CMakeLists.txt | 16 +- cmake/rsl-test.cmake | 8 +- example/always_passes.cpp | 2 +- example/always_passes2.cpp | 24 +++ .../rsl/testing/_testing_impl/discovery.hpp | 27 ++- include/rsl/testing/assert.hpp | 46 ++-- include/rsl/testing/test.hpp | 8 +- src/main/CMakeLists.txt | 2 +- src/main/config_parser.hpp | 169 --------------- src/main/incremental.hpp | 176 +++++++++++++++ src/main/incremental/CMakeLists.txt | 1 + src/main/{ => incremental}/compile_pool.hpp | 7 +- src/main/incremental/config_parser.hpp | 132 ++++++++++++ .../{ => incremental}/platform/CMakeLists.txt | 2 + src/main/incremental/platform/event_loop.hpp | 66 ++++++ .../{ => incremental}/platform/library.hpp | 0 .../incremental/platform/posix/event_loop.cpp | 98 +++++++++ .../platform/posix/library.cpp | 0 .../platform/posix/taskset.cpp | 0 .../platform/posix/watch.cpp} | 204 ++++++++++-------- src/main/incremental/platform/stdin.hpp | 28 +++ .../{ => incremental}/platform/taskset.hpp | 2 +- src/main/incremental/platform/watch.hpp | 24 +++ src/main/incremental/runner.cpp | 5 + src/main/main.cpp | 124 +++++------ src/main/platform/watch.hpp | 0 src/runner.cpp | 31 ++- src/test.cpp | 25 ++- 28 files changed, 827 insertions(+), 400 deletions(-) create mode 100644 example/always_passes2.cpp delete mode 100644 src/main/config_parser.hpp create mode 100644 src/main/incremental.hpp create mode 100644 src/main/incremental/CMakeLists.txt rename src/main/{ => incremental}/compile_pool.hpp (88%) create mode 100644 src/main/incremental/config_parser.hpp rename src/main/{ => incremental}/platform/CMakeLists.txt (71%) create mode 100644 src/main/incremental/platform/event_loop.hpp rename src/main/{ => incremental}/platform/library.hpp (100%) create mode 100644 src/main/incremental/platform/posix/event_loop.cpp rename src/main/{ => incremental}/platform/posix/library.cpp (100%) rename src/main/{ => incremental}/platform/posix/taskset.cpp (100%) rename src/main/{platform/posix/watch.hpp => incremental/platform/posix/watch.cpp} (59%) create mode 100644 src/main/incremental/platform/stdin.hpp rename src/main/{ => incremental}/platform/taskset.hpp (87%) create mode 100644 src/main/incremental/platform/watch.hpp create mode 100644 src/main/incremental/runner.cpp delete mode 100644 src/main/platform/watch.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4be27ea..e5f0250 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,8 +47,8 @@ if (BUILD_TESTING) # target_compile_definitions(rsltest_test PRIVATE RSL_TEST_NAMESPACE=testing) add_library(test_dummy test/dummy.cpp) target_enable_tests(test_dummy - NAMESPACE testing - SOURCES test/ example/ + NAMESPACE demo + SOURCES example/ # PREFIX test_ # PREFIX_REQUIRED ) @@ -62,9 +62,9 @@ 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 +# 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 index 38fe4ae..dcb3bfd 100644 --- a/cmake/rsl-test.cmake +++ b/cmake/rsl-test.cmake @@ -254,10 +254,10 @@ function(target_enable_tests _TEST_ARG_TARGET) \"configurations\": { \"default\": { \"compiler_path\": \"${_compiler}\", - \"${_lang}\": { - \"standard\": ${_std}, - \"extensions\": ${_std_ext} - } + \"mode\": \"${_lang}\", + \"standard\": ${_std}, + \"gnu_extensions\": ${_std_ext}, + \"options\": {} } } }") diff --git a/example/always_passes.cpp b/example/always_passes.cpp index 96bf6b8..ca583a6 100644 --- a/example/always_passes.cpp +++ b/example/always_passes.cpp @@ -18,7 +18,7 @@ auto zoinks(bool zoinks) { [[= rsl::test]] void always_passes() { std::cout << "foo\n"; std::cerr << "bar\n"; - zoinks(true); zoinks(false); + // zoinks(true); } } // 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..8b3b416 --- /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/include/rsl/testing/_testing_impl/discovery.hpp b/include/rsl/testing/_testing_impl/discovery.hpp index cb0adb9..a84fa9b 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,13 +56,22 @@ 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] == '_') { - return; - } - + if (!has_identifier(R)) { return; } + + auto identifier = identifier_of(R); + if (identifier[0] == '_') { return; } + if (!(is_function(R) || is_variable(R) || (is_complete_type(R) && is_class_type(R)))) { 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) { diff --git a/include/rsl/testing/assert.hpp b/include/rsl/testing/assert.hpp index d3bbb4a..fedeecb 100644 --- a/include/rsl/testing/assert.hpp +++ b/include/rsl/testing/assert.hpp @@ -1,7 +1,6 @@ #pragma once #include #include -#include #include #include @@ -16,41 +15,36 @@ struct assertion_failure : std::exception { : message(std::string(message)) , sloc(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; -}; - +struct AssertionTracker; AssertionTracker& assertion_counter(); +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/test.hpp b/include/rsl/testing/test.hpp index 2b34165..2891da5 100644 --- a/include/rsl/testing/test.hpp +++ b/include/rsl/testing/test.hpp @@ -40,6 +40,7 @@ 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 @@ -71,7 +72,7 @@ class Test { std::vector get_tests() const { return (this->*get_tests_impl)(); } }; -using TestDef = Test (*)(); +using TestDef = Test (*)(std::string const&); struct Reporter; struct TestNamespace { @@ -113,7 +114,8 @@ struct TestNamespace { [[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); + void insert(Test const& test, size_t i = 0); + void remove_by_path(std::string_view path); [[nodiscard]] std::size_t count() const; bool run(Reporter* reporter); @@ -122,7 +124,7 @@ struct TestNamespace { }; struct TestRoot : TestNamespace { - bool run(Reporter* reporter); + bool run(Reporter* reporter, bool summarize = true); }; TestRoot get_tests(); diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index 3e733d2..c2288e1 100644 --- a/src/main/CMakeLists.txt +++ b/src/main/CMakeLists.txt @@ -2,4 +2,4 @@ target_sources(rsltest_main PUBLIC main.cpp) target_include_directories(rsltest_main PRIVATE .) add_subdirectory(reporters) -add_subdirectory(platform) \ No newline at end of file +add_subdirectory(incremental) \ No newline at end of file diff --git a/src/main/config_parser.hpp b/src/main/config_parser.hpp deleted file mode 100644 index 371380c..0000000 --- a/src/main/config_parser.hpp +++ /dev/null @@ -1,169 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include - -#include -#include "platform/taskset.hpp" - -namespace rsl::testing::_impl_main { -struct TestTU { - ProgramInvocation invocation; - std::filesystem::path out_path; -}; - -class ConfigParser { - static nlohmann::json json_from_file(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; - } - -public: - explicit ConfigParser(std::filesystem::path const& path) : ConfigParser(json_from_file(path)) {} - - explicit ConfigParser(nlohmann::json cfg) : config_(std::move(cfg)) { - validate_project_section(); - } - - static std::vector discover_tests(const std::filesystem::path& root) { - static constexpr std::array allowed{".cpp"}; - std::vector result; - - for (auto const& entry : std::filesystem::recursive_directory_iterator(root)) { - if (entry.is_regular_file()) { - auto const ext = entry.path().extension().string(); - if (std::ranges::contains(allowed, ext)) - result.push_back(entry.path()); - } - } - return result; - } - - [[nodiscard]] - TestTU make_invocation(std::string_view config_name, - const std::filesystem::path& test_path) const { - const auto& project = config_.at("project"); - const auto& options = config_.value("options", nlohmann::json::object()); - const auto& configurations = config_.at("configurations"); - - const std::filesystem::path build_path = project.at("build_path").get(); - const std::filesystem::path project_path = project.at("project_path").get(); - - const nlohmann::json& cfg = configurations.at(config_name.data()); - - const bool ext = cfg["CXX"].value("extensions", false); - const int ver = cfg["CXX"].value("standard", 17); - const std::string standard = std::format("-std={}++{}", (ext ? "gnu" : "c"), ver); - const std::string compiler_path = cfg.value("compiler_path", "c++"); - - std::filesystem::path out_path = - build_path / std::filesystem::relative(test_path, project_path); - out_path.replace_extension(".so"); - - std::vector cmd; - - cmd.push_back(compiler_path); - cmd.push_back(standard); - - if (options.contains("compile_options")) { - for (auto const& o : options["compile_options"]) { - auto value = o.get(); - if (!value.empty()) { - cmd.push_back(value); - } - } - } - - if (options.contains("include_dirs")) { - for (auto const& p : options["include_dirs"]) { - auto s = p.get(); - if (!s.empty()) { - cmd.push_back(std::format("-I{}", s)); - } - } - } - - if (options.contains("compile_definitions")) { - for (auto const& d : options["compile_definitions"]) { - auto value = d.get(); - if (!value.empty()) { - cmd.push_back(std::format("-D{}", value)); - } - } - } - - if (options.contains("link_libraries")) { - for (auto const& lib : options["link_libraries"]) { - auto value = lib.get(); - if (!value.empty()) { - if (value[0] == '/') { - cmd.push_back(std::format("-L{}", value)); - } else { - cmd.push_back(std::format("-l{}", value)); - } - } - } - } - - // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); - // cmd.push_back(std::format("-L{}", build_path.string())); - cmd.emplace_back("-fPIC"); - cmd.emplace_back("-shared"); - - // out file - cmd.emplace_back("-o"); - cmd.push_back(out_path.string()); - - // input file - cmd.push_back(test_path.string()); - - return {compiler_path, cmd, out_path}; - } - - std::vector expand() const { - const auto& project = config_.at("project"); - std::vector tests; - - for (auto const& p : project.at("test_path")) { - for (auto&& t : discover_tests(std::filesystem::path(p.get()))) { - tests.push_back(std::move(t)); - } - } - - std::vector test_tus; - for (auto const& [name, _] : config_.at("configurations").items()) { - for (auto const& test : tests) { - test_tus.push_back(make_invocation(name, test)); - } - } - return test_tus; - } - - nlohmann::json config_; -private: - - void validate_project_section() { - if (!config_.contains("project")) { - throw std::runtime_error("Missing required 'project' section in test-runner.nlohmann::json"); - } - - const auto& project = config_["project"]; - for (std::string_view key : {"build_path", "project_path", "test_path"}) { - if (!project.contains(key)) { - throw std::runtime_error(std::format("Missing required project.{} field", key)); - } - } - } -}; - -} // 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..993032c --- /dev/null +++ b/src/main/incremental.hpp @@ -0,0 +1,176 @@ +#pragma once +#include + +#include "incremental/compile_pool.hpp" +#include "incremental/config_parser.hpp" +#include "incremental/platform/library.hpp" +#include "incremental/platform/watch.hpp" +#include "incremental/platform/event_loop.hpp" + +#include + +namespace rsl::testing::_impl_main { +struct TestSet { + void* handle; + std::set tests; +}; + +struct IncrementalRunner { + CompilePool pool; // TODO use more generic task pool? + RunnerConfig config; + std::unordered_map test_sets; + + // 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)) {} + + [[nodiscard]] + TestTU make_invocation(std::string_view config_name, + std::filesystem::path const& test_path) const { + const auto& options = config.options; + + const auto& configurations = config.configurations; + + const std::filesystem::path build_path = config.project.build_path; + const std::filesystem::path project_path = config.project.project_path; + + 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); + const std::string compiler_path = cfg.compiler_path; + + std::filesystem::path out_path = + build_path / std::filesystem::relative(test_path, project_path); + out_path.replace_extension(".so"); + + std::vector cmd; + + cmd.push_back(compiler_path); + 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)); + } + + for (auto const& lib : options.link_libraries) { + if (lib.is_absolute()) { + cmd.push_back(std::format("-L{}", lib.string())); + } else { + cmd.push_back(std::format("-l{}", lib.string())); + } + } + + // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); + // cmd.push_back(std::format("-L{}", build_path.string())); + cmd.emplace_back("-fPIC"); + cmd.emplace_back("-shared"); + + if (auto ns = config.project.namespace_; not ns.empty()) { + cmd.emplace_back("-DRSL_TEST_NAMESPACE=" + ns); + } + + cmd.emplace_back("-ftime-trace"); + + // out file + cmd.emplace_back("-o"); + cmd.push_back(out_path.string()); + + // input file + cmd.push_back(std::string(test_path.string())); + + return {compiler_path, cmd, out_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) { + std::vector test_tus; + for (auto const& test : tests) { + for (auto const& [name, _] : config.configurations) { + test_tus.push_back(make_invocation(name, test)); + } + } + return test_tus; + } + + void recompile(std::vector const& tus) { + //! this function is not thread-safe, it shall only be invoked from the main thread + + for (auto&& tu : tus) { + pool.submit(tu); + } + + pool.wait(); + + 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; + } + if (not rsl::testing::_testing_impl::registry().empty()) { + std::println("test registry not empty"); + rsl::testing::_testing_impl::registry().clear(); + } + // check if we have the key already, make sure old one is unloaded + unload(tu.out_path); + + library_handle handle = load_library(tu.out_path.string()); + if (handle != nullptr) { + test_sets[tu.out_path] = {handle, rsl::testing::_testing_impl::registry()}; + } + rsl::testing::_testing_impl::registry().clear(); + } + + if (not rsl::testing::_testing_impl::registry().empty()) { + std::println("test registry not empty"); + } + } + + void unload(std::filesystem::path const& file) { + if (auto it = test_sets.find(file); it != test_sets.end()) { + auto&[handle, tests] = it->second; + if (handle != nullptr) { + unload_library(handle); + tests = {}; + } + } + } +}; +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/incremental/CMakeLists.txt b/src/main/incremental/CMakeLists.txt new file mode 100644 index 0000000..2600b3c --- /dev/null +++ b/src/main/incremental/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(platform) \ No newline at end of file diff --git a/src/main/compile_pool.hpp b/src/main/incremental/compile_pool.hpp similarity index 88% rename from src/main/compile_pool.hpp rename to src/main/incremental/compile_pool.hpp index 6b565dd..a42172f 100644 --- a/src/main/compile_pool.hpp +++ b/src/main/incremental/compile_pool.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -63,7 +64,6 @@ class CompilePool { } std::vector collect() { - wait(); std::vector output; { std::scoped_lock lock(resultMutex); @@ -90,8 +90,11 @@ class CompilePool { 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)); diff --git a/src/main/incremental/config_parser.hpp b/src/main/incremental/config_parser.hpp new file mode 100644 index 0000000..cf6fd48 --- /dev/null +++ b/src/main/incremental/config_parser.hpp @@ -0,0 +1,132 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include +#include "platform/taskset.hpp" + +namespace rsl::testing::_impl_main { +struct TestTU { + ProgramInvocation invocation; + std::filesystem::path out_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 +}; + +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_} + }; +} +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; +}; +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} + }; +} +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; +}; +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} + }; +} +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; +}; + +void to_json(nlohmann::json& doc, RunnerConfig const& p) { + doc = { + { "target", p.target}, + { "project", p.project}, + { "options", p.options}, + {"configurations", p.configurations} + }; +} + +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/platform/CMakeLists.txt b/src/main/incremental/platform/CMakeLists.txt similarity index 71% rename from src/main/platform/CMakeLists.txt rename to src/main/incremental/platform/CMakeLists.txt index f5f2302..2ec4899 100644 --- a/src/main/platform/CMakeLists.txt +++ b/src/main/incremental/platform/CMakeLists.txt @@ -4,5 +4,7 @@ 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/incremental/platform/event_loop.hpp b/src/main/incremental/platform/event_loop.hpp new file mode 100644 index 0000000..f501704 --- /dev/null +++ b/src/main/incremental/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/incremental/platform/library.hpp similarity index 100% rename from src/main/platform/library.hpp rename to src/main/incremental/platform/library.hpp diff --git a/src/main/incremental/platform/posix/event_loop.cpp b/src/main/incremental/platform/posix/event_loop.cpp new file mode 100644 index 0000000..324d93b --- /dev/null +++ b/src/main/incremental/platform/posix/event_loop.cpp @@ -0,0 +1,98 @@ +#include "../event_loop.hpp" + +#include +#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/incremental/platform/posix/library.cpp similarity index 100% rename from src/main/platform/posix/library.cpp rename to src/main/incremental/platform/posix/library.cpp diff --git a/src/main/platform/posix/taskset.cpp b/src/main/incremental/platform/posix/taskset.cpp similarity index 100% rename from src/main/platform/posix/taskset.cpp rename to src/main/incremental/platform/posix/taskset.cpp diff --git a/src/main/platform/posix/watch.hpp b/src/main/incremental/platform/posix/watch.cpp similarity index 59% rename from src/main/platform/posix/watch.hpp rename to src/main/incremental/platform/posix/watch.cpp index 6677f52..97a3751 100644 --- a/src/main/platform/posix/watch.hpp +++ b/src/main/incremental/platform/posix/watch.cpp @@ -1,55 +1,26 @@ +#include "../watch.hpp" + #include #include -#include #include +#include #include #include #include #include #include #include +#include +#include #include #include #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 Inotify { - int fd = -1; - Inotify(Inotify const&) = delete; - Inotify(Inotify&&) = default; - - Inotify& operator=(Inotify const&) = delete; - Inotify& operator=(Inotify&&) = delete; - - Inotify() : fd(::inotify_init1(IN_NONBLOCK)) { - if (fd < 0) { - throw std::runtime_error(std::format("inotify_init1: {}", std::strerror(errno))); - } - } - - ~Inotify() { - 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); - } -}; - struct InotifyEvent { int wd; std::uint32_t mask; @@ -137,77 +108,122 @@ enum FileEvent { Q_OVERFLOW = 0x00004000, /* Event queued overflowed. */ IGNORED = 0x00008000, /* File was ignored. */ }; +} // namespace -struct Watcher { - std::unordered_map watchers; - Inotify inotify; +namespace rsl::testing::_impl_main { +struct WatcherImpl { + int fd = -1; + std::unordered_map last_modified; - void add_directory(std::filesystem::path const& dir, bool recurse = true) { - if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) { - // throw std::runtime_error(std::format("not a directory: {}", dir.string())); - return; + 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))); } + } - int top_wd = inotify.add_watch(dir); - watchers.emplace(top_wd, dir); - if (not recurse) { - return; + ~WatcherImpl() { + if (fd >= 0) { + ::close(fd); } + } - for (auto const& ent : std::filesystem::recursive_directory_iterator(dir)) { - if (ent.is_directory()) { - try { - int wd = inotify.add_watch(ent.path()); - watchers.emplace(wd, ent.path()); - } 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()); - } - } + [[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 watch(auto&& fnc) { - std::vector pending; - pending.reserve(8192); - std::array tmpbuf; - - while (true) { - ssize_t n = ::read(inotify.fd, tmpbuf.data(), static_cast(tmpbuf.size())); - if (n < 0) { - if (errno == EAGAIN) { - ::usleep(100000); - continue; - } - std::println("read error: {}", std::strerror(errno)); - break; + void rm_watch(int wd) { inotify_rm_watch(fd, wd); } +}; + +Watcher::Watcher() : impl(new WatcherImpl()) {} +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(100); + + pending.append_range(data); + auto events = try_decode_events(pending); + for (auto const& ev : events) { + auto it = watchers.find(ev.wd); + std::filesystem::path dir = (it != watchers.end()) ? it->second : std::filesystem::path{}; + std::filesystem::path full = ev.name.empty() ? dir : dir / ev.name; + std::println("dispatching {} {} ", full.string(), ev.mask); + + // fnc(full, FileEvent(ev.mask)); + + if ((ev.mask & FileEvent::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->first); + watchers.erase(it); } - if (n == 0) { + } + + if ((ev.mask & FileEvent::MODIFY)) { + auto now = std::chrono::steady_clock::now(); + if (auto it = impl->last_modified.find(dir); + it != impl->last_modified.end() && now - it->second < debounce_threshold) { continue; } - pending.insert(pending.end(), tmpbuf.data(), tmpbuf.data() + static_cast(n)); - - // decode complete events; any partial event bytes remain in pending - auto events = try_decode_events(pending); - - for (auto const& ev : events) { - auto it = watchers.find(ev.wd); - std::filesystem::path dir = (it != watchers.end()) ? it->second : std::filesystem::path{}; - std::filesystem::path full = ev.name.empty() ? dir : dir / ev.name; - fnc(full, FileEvent(ev.mask)); - - if ((ev.mask & FileEvent::CREATE) && (ev.mask & IN_ISDIR)) { - add_directory(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()) { - inotify.rm_watch(it->first); - watchers.erase(it); - } - } + impl->last_modified[dir] = now; + std::println("modified: {}", full.string()); + } + } +} + +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; + } + + for (auto const& [_, path] : watchers) { + if (dir == path) { + // already watching + return; + } + } + + int top_wd = impl->add_watch(dir); + watchers.emplace(top_wd, dir); + 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(wd, ent.path()); + } 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) { + // TODO +} + +} // namespace rsl::testing::_impl_main diff --git a/src/main/incremental/platform/stdin.hpp b/src/main/incremental/platform/stdin.hpp new file mode 100644 index 0000000..94c0753 --- /dev/null +++ b/src/main/incremental/platform/stdin.hpp @@ -0,0 +1,28 @@ +#pragma once +#include +#include + +#include + +#ifdef __unix__ +# include +#else +#endif + +namespace rsl::testing::_impl_main { +struct TerminalCommand { + std::string pending; +#ifdef __unix__ + uintptr_t handle = STDIN_FILENO; +#else + uintptr_t handle = 0; +#endif + + [[nodiscard]] uintptr_t get_handle() const { return handle; } + + void on_readable(std::span data) { + pending.append_range(data); + std::println("{}", pending); + } +}; +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/platform/taskset.hpp b/src/main/incremental/platform/taskset.hpp similarity index 87% rename from src/main/platform/taskset.hpp rename to src/main/incremental/platform/taskset.hpp index 4dbb1e5..3502bb4 100644 --- a/src/main/platform/taskset.hpp +++ b/src/main/incremental/platform/taskset.hpp @@ -18,7 +18,7 @@ struct ProgramInvocation { 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); + // std::println("running {} {}", invocation.program, invocation.arguments); return run_on_cpu(cpu, invocation.program, invocation.arguments); } } \ No newline at end of file diff --git a/src/main/incremental/platform/watch.hpp b/src/main/incremental/platform/watch.hpp new file mode 100644 index 0000000..3479e55 --- /dev/null +++ b/src/main/incremental/platform/watch.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include +namespace rsl::testing::_impl_main { + +struct WatcherImpl; +class Watcher { + WatcherImpl* impl; + std::unordered_map watchers; // TODO flip + std::vector pending; + +public: + Watcher(); + ~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); +}; +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/incremental/runner.cpp b/src/main/incremental/runner.cpp new file mode 100644 index 0000000..4bea961 --- /dev/null +++ b/src/main/incremental/runner.cpp @@ -0,0 +1,5 @@ +#include "../incremental.hpp" + +namespace rsl::testing::_impl_main { + +} \ No newline at end of file diff --git a/src/main/main.cpp b/src/main/main.cpp index 46314a0..f5c9656 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -1,104 +1,88 @@ #include -#include -#include #include #include -#include "compile_pool.hpp" -#include "config_parser.hpp" +#include "incremental.hpp" #include -#include "platform/library.hpp" - -#include "platform/posix/watch.hpp" +#include "incremental/platform/stdin.hpp" #include "rsl/testing/output.hpp" -struct TestSet { - void* handle; - std::set tests; -}; +#include +#include +#include + +void make_nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} int main() { using namespace rsl::testing::_impl_main; - constexpr bool incremental = false; + constexpr bool incremental = true; const auto executable_path = std::filesystem::canonical("/proc/self/exe").parent_path(); const std::filesystem::path config_path = executable_path / "test-runner.json"; - ConfigParser runner(config_path); - auto test_tus = runner.expand(); // TODO split config and test discovery - - CompilePool pool{}; - for (auto&& tu : test_tus) { - pool.submit(tu); - } - - std::unordered_map test_sets; - - for (auto&& [tu, result] : pool.collect()) { - if (result.exit_code != 0) { - std::println("ERROR!"); - continue; - } - - if (not rsl::testing::_testing_impl::registry().empty()) { - std::println("test registry not empty"); - rsl::testing::_testing_impl::registry().clear(); - } - // check if we have the key already, make sure old one is unloaded - - void* handle = load_library(tu.out_path.string()); - if (handle != nullptr) { - test_sets[tu.out_path] = {handle, rsl::testing::_testing_impl::registry()}; - } - rsl::testing::_testing_impl::registry().clear(); - } + auto runner = IncrementalRunner(config_path); + auto test_inputs = runner.discover_tests(); - if (not rsl::testing::_testing_impl::registry().empty()) { - std::println("test registry not empty"); - } + runner.recompile(runner.expand_tests(test_inputs)); rsl::testing::TestRoot root; - for (auto&& [path, test_set] : test_sets) { - std::println("{} -> {}", path.string(), test_set.tests.size()); - for (auto test : test_set.tests) { - root.insert(test()); + + auto update_tree = [&](auto file_path) { + // remove updated tests from tree + // for (auto&& [path, _] : runner.test_sets) { + // root.remove_by_path(path.string()); + // } + + // rebuild root + root = {}; + // insert + rsl::testing::TestRoot tests; + for (auto&& [path, test_set] : runner.test_sets) { + // std::println("{} -> {}", path.string(), test_set.tests.size()); + for (auto test_def : test_set.tests) { + auto test = test_def(path); + // root.insert(test); + // if (file_path == path) { + tests.insert(test); + // } + } } - } + return tests; + }; + update_tree(""); std::unique_ptr selected_reporter; selected_reporter = rsl::testing::Reporter::make("plain"); root.run(selected_reporter.get()); - for (auto& [path, test_set] : test_sets) { + for (auto& [path, test_set] : runner.test_sets) { dlclose(test_set.handle); test_set.tests = {}; } if (incremental) { Watcher watch{}; - const auto test_paths = runner.config_.at("project").at("test_path"); - for (auto path : test_paths) { - watch.add_directory(path); + for (auto const& path : runner.config.project.test_path) { + watch.add_watch(path); } - watch.watch([&](auto path, FileEvent event) { - if ((event & FileEvent::MODIFY) == FileEvent::MODIFY) { - // compile TU - - if (auto it = test_sets.find(path); it != test_sets.end()) { - TestSet& set = it->second; - - // clean up old test - if (set.handle != nullptr) { - dlclose(set.handle); - set.handle = nullptr; - } - set.tests = {}; - } - - // load TU - } - }); + + // auto watch_fnc = [&](auto path, FileEvent event) { + // if ((event & FileEvent::MODIFY) == FileEvent::MODIFY) { + // // compile TU + // runner.recompile(runner.expand_tests({path})); + // // load TU + // auto updated = update_tree(path); + // // updated.run(selected_reporter.get(), false); + // } + // }; + TerminalCommand commands; + + auto loop = EventLoop(commands, watch); + loop.run(); } } \ No newline at end of file diff --git a/src/main/platform/watch.hpp b/src/main/platform/watch.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/runner.cpp b/src/runner.cpp index 381368a..af02ace 100644 --- a/src/runner.cpp +++ b/src/runner.cpp @@ -18,6 +18,22 @@ #include "capture.hpp" #include +namespace rsl::testing::_testing_impl { +struct AssertionTracker { + std::vector assertions; + std::string test_name; +}; + +AssertionTracker& assertion_counter() { + static AssertionTracker counter{}; + return counter; +} + +void track_assertion(AssertionInfo info) { + assertion_counter().assertions.emplace_back(info); +} +} // namespace rsl::testing::_testing_impl + namespace { void cleanup_frames(cpptrace::stacktrace& trace, std::string_view test_name) { std::vector frames; @@ -68,20 +84,21 @@ void print_tests(rsl::testing::TestNamespace const& current, std::size_t indent } } - namespace rsl::testing { void Reporter::list_tests(TestNamespace const& tests) { print_tests(tests); } -bool TestRoot::run(Reporter* reporter) { +bool TestRoot::run(Reporter* reporter, bool summarize) { libassert::set_failure_handler(failure_handler); - std::println("failure handler set"); + // 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(); + if (summarize) { + reporter->after_run(); + } return status; } @@ -105,7 +122,7 @@ bool TestNamespace::run(Reporter* reporter) { tracker.test_name = join_str(test.full_name, "::"); reporter->before_test(test_run); - auto result = test_run.run(); + auto result = test_run.run(); result.assertions = tracker.assertions; reporter->after_test(result); @@ -172,7 +189,7 @@ Result TestCase::run() const { // rsltest_cov was linked in -> run with coverage rsl::coverage::CoverageReport* reports = nullptr; std::size_t report_count = 0; - auto finalize = [&] { + auto finalize = [&] { ret.coverage = filter_coverage(reports, report_count); free(reports); }; @@ -182,7 +199,7 @@ Result TestCase::run() const { &reports, &report_count); finalize(); - } catch (...) { + } catch (...) { finalize(); throw; } diff --git a/src/test.cpp b/src/test.cpp index 93890c2..8646d79 100644 --- a/src/test.cpp +++ b/src/test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -14,11 +14,6 @@ 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) { @@ -79,6 +74,22 @@ void TestNamespace::insert(Test const& test, std::size_t i) { it->insert(test, i + 1); } +void TestNamespace::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); +} + std::size_t TestNamespace::count() const { std::size_t total = tests.size(); for (auto const& ns : children) { @@ -115,7 +126,7 @@ void TestNamespace::filter(std::span parts) { TestRoot get_tests() { TestRoot root; for (auto test_def : rsl::testing::_testing_impl::registry()) { - auto test = test_def(); + auto test = test_def({}); root.insert(test); } return root; From 1dd68651a3acbcc81553bbea50b261e47c9c733f Mon Sep 17 00:00:00 2001 From: Tsche Date: Thu, 20 Nov 2025 05:32:09 +0000 Subject: [PATCH 03/15] cleanup --- src/main/incremental/config_parser.hpp | 16 ++++++++-------- src/main/{ => incremental}/incremental.hpp | 10 +++++----- src/main/incremental/platform/posix/watch.cpp | 3 ++- src/main/incremental/platform/stdin.hpp | 9 ++++++++- src/main/incremental/platform/watch.hpp | 13 +++++++++++-- src/main/main.cpp | 14 +++----------- 6 files changed, 37 insertions(+), 28 deletions(-) rename src/main/{ => incremental}/incremental.hpp (96%) diff --git a/src/main/incremental/config_parser.hpp b/src/main/incremental/config_parser.hpp index cf6fd48..5f9f646 100644 --- a/src/main/incremental/config_parser.hpp +++ b/src/main/incremental/config_parser.hpp @@ -29,7 +29,7 @@ struct Project { std::string namespace_; // TODO rename via annotation }; -void to_json(nlohmann::json& doc, Project const& p) { +inline void to_json(nlohmann::json& doc, Project const& p) { doc = { { "test_path", p.test_path}, { "build_path", p.build_path}, @@ -37,7 +37,7 @@ void to_json(nlohmann::json& doc, Project const& p) { { "namespace", p.namespace_} }; } -void from_json(nlohmann::json const& doc, Project& p) { +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); @@ -52,7 +52,7 @@ struct Options { std::vector link_options; std::vector link_libraries; }; -void to_json(nlohmann::json& doc, Options const& p) { +inline void to_json(nlohmann::json& doc, Options const& p) { doc = { { "include_dirs", p.include_dirs}, { "compile_options", p.compile_options}, @@ -61,7 +61,7 @@ void to_json(nlohmann::json& doc, Options const& p) { { "link_libraries", p.link_libraries} }; } -void from_json(nlohmann::json const& doc, Options& p) { +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); @@ -80,7 +80,7 @@ struct Configuration { unsigned standard = 26; bool gnu_extensions = false; }; -void to_json(nlohmann::json& doc, Configuration const& p) { +inline void to_json(nlohmann::json& doc, Configuration const& p) { doc = { { "compiler_path", p.compiler_path}, { "mode", p.mode}, @@ -88,7 +88,7 @@ void to_json(nlohmann::json& doc, Configuration const& p) { {"gnu_extensions", p.gnu_extensions} }; } -void from_json(nlohmann::json const& doc, Configuration& p) { +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); @@ -102,7 +102,7 @@ struct RunnerConfig { std::unordered_map configurations; }; -void to_json(nlohmann::json& doc, RunnerConfig const& p) { +inline void to_json(nlohmann::json& doc, RunnerConfig const& p) { doc = { { "target", p.target}, { "project", p.project}, @@ -111,7 +111,7 @@ void to_json(nlohmann::json& doc, RunnerConfig const& p) { }; } -void from_json(nlohmann::json const& doc, RunnerConfig& p) { +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); diff --git a/src/main/incremental.hpp b/src/main/incremental/incremental.hpp similarity index 96% rename from src/main/incremental.hpp rename to src/main/incremental/incremental.hpp index 993032c..7766d45 100644 --- a/src/main/incremental.hpp +++ b/src/main/incremental/incremental.hpp @@ -1,11 +1,11 @@ #pragma once #include -#include "incremental/compile_pool.hpp" -#include "incremental/config_parser.hpp" -#include "incremental/platform/library.hpp" -#include "incremental/platform/watch.hpp" -#include "incremental/platform/event_loop.hpp" +#include "compile_pool.hpp" +#include "config_parser.hpp" +#include "platform/library.hpp" +#include "platform/watch.hpp" +#include "platform/event_loop.hpp" #include diff --git a/src/main/incremental/platform/posix/watch.cpp b/src/main/incremental/platform/posix/watch.cpp index 97a3751..152a81a 100644 --- a/src/main/incremental/platform/posix/watch.cpp +++ b/src/main/incremental/platform/posix/watch.cpp @@ -16,6 +16,7 @@ #include #include #include +#include "incremental/incremental.hpp" namespace { constexpr std::uint32_t watch_mask = IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM | @@ -145,7 +146,7 @@ struct WatcherImpl { void rm_watch(int wd) { inotify_rm_watch(fd, wd); } }; -Watcher::Watcher() : impl(new WatcherImpl()) {} +Watcher::Watcher(IncrementalRunner& runner) : impl(new WatcherImpl()), runner(&runner) {} Watcher::~Watcher() noexcept { delete impl; } diff --git a/src/main/incremental/platform/stdin.hpp b/src/main/incremental/platform/stdin.hpp index 94c0753..be81822 100644 --- a/src/main/incremental/platform/stdin.hpp +++ b/src/main/incremental/platform/stdin.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include "../incremental.hpp" #ifdef __unix__ # include @@ -10,19 +12,24 @@ #endif namespace rsl::testing::_impl_main { -struct TerminalCommand { +class TerminalCommand { std::string pending; #ifdef __unix__ uintptr_t handle = STDIN_FILENO; #else uintptr_t handle = 0; #endif + IncrementalRunner* runner; +public: + explicit TerminalCommand(IncrementalRunner& runner) : runner(&runner) {} [[nodiscard]] uintptr_t get_handle() const { return handle; } void on_readable(std::span data) { pending.append_range(data); std::println("{}", pending); } + + }; } // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/incremental/platform/watch.hpp b/src/main/incremental/platform/watch.hpp index 3479e55..7856529 100644 --- a/src/main/incremental/platform/watch.hpp +++ b/src/main/incremental/platform/watch.hpp @@ -4,21 +4,30 @@ #include #include #include + namespace rsl::testing::_impl_main { +struct IncrementalRunner; struct WatcherImpl; + class Watcher { WatcherImpl* impl; - std::unordered_map watchers; // TODO flip + std::unordered_map watchers; // TODO flip std::vector pending; + IncrementalRunner* runner; public: - Watcher(); + 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 file_modified(std::filesystem::path const& path) {} + + void file_deleted(std::filesystem::path const& path) {} }; } // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/main.cpp b/src/main/main.cpp index f5c9656..90e3520 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -3,20 +3,12 @@ #include #include -#include "incremental.hpp" +#include "incremental/incremental.hpp" #include #include "incremental/platform/stdin.hpp" #include "rsl/testing/output.hpp" -#include -#include -#include - -void make_nonblocking(int fd) { - int flags = fcntl(fd, F_GETFL, 0); - fcntl(fd, F_SETFL, flags | O_NONBLOCK); -} int main() { using namespace rsl::testing::_impl_main; @@ -66,7 +58,7 @@ int main() { } if (incremental) { - Watcher watch{}; + Watcher watch{runner}; for (auto const& path : runner.config.project.test_path) { watch.add_watch(path); } @@ -80,7 +72,7 @@ int main() { // // updated.run(selected_reporter.get(), false); // } // }; - TerminalCommand commands; + TerminalCommand commands{runner}; auto loop = EventLoop(commands, watch); loop.run(); From a52befd5074062e5edadcc282e526aed69193540 Mon Sep 17 00:00:00 2001 From: Tsche Date: Thu, 20 Nov 2025 05:52:12 +0000 Subject: [PATCH 04/15] implement basic commands --- src/main/incremental/incremental.hpp | 1 - src/main/incremental/platform/posix/watch.cpp | 6 +++ src/main/incremental/platform/stdin.hpp | 40 +++++++++++++++++-- src/main/main.cpp | 12 +++--- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/main/incremental/incremental.hpp b/src/main/incremental/incremental.hpp index 7766d45..ea2c672 100644 --- a/src/main/incremental/incremental.hpp +++ b/src/main/incremental/incremental.hpp @@ -5,7 +5,6 @@ #include "config_parser.hpp" #include "platform/library.hpp" #include "platform/watch.hpp" -#include "platform/event_loop.hpp" #include diff --git a/src/main/incremental/platform/posix/watch.cpp b/src/main/incremental/platform/posix/watch.cpp index 152a81a..53da9cf 100644 --- a/src/main/incremental/platform/posix/watch.cpp +++ b/src/main/incremental/platform/posix/watch.cpp @@ -177,16 +177,20 @@ void Watcher::on_readable(std::span data) { impl->rm_watch(it->first); watchers.erase(it); } + file_deleted(full); } if ((ev.mask & FileEvent::MODIFY)) { auto now = std::chrono::steady_clock::now(); if (auto it = impl->last_modified.find(dir); it != impl->last_modified.end() && now - it->second < debounce_threshold) { + // debounce continue; } impl->last_modified[dir] = now; + std::println("modified: {}", full.string()); + file_modified(full); } } } @@ -204,6 +208,8 @@ void Watcher::add_watch(std::filesystem::path const& dir, bool recurse) { } } + std::println("watching {} for changes", dir.string()); + int top_wd = impl->add_watch(dir); watchers.emplace(top_wd, dir); if (not recurse) { diff --git a/src/main/incremental/platform/stdin.hpp b/src/main/incremental/platform/stdin.hpp index be81822..803465c 100644 --- a/src/main/incremental/platform/stdin.hpp +++ b/src/main/incremental/platform/stdin.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include #include +#include #include #include "../incremental.hpp" @@ -20,16 +20,48 @@ class TerminalCommand { uintptr_t handle = 0; #endif IncrementalRunner* runner; + Watcher* watcher; public: - explicit TerminalCommand(IncrementalRunner& runner) : runner(&runner) {} + 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); - std::println("{}", pending); + 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; + } + // 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 { + return; + } + + if (cmd == "add_watch") { + watcher->add_watch(argument); + } else if (cmd == "rm_watch") { + watcher->rm_watch(argument); + } else if (cmd == "run") { + // TODO + } else if (cmd == "stop_running") { + // TODO + } + } }; } // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/main.cpp b/src/main/main.cpp index 90e3520..a88822c 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -1,14 +1,14 @@ #include #include -#include -#include "incremental/incremental.hpp" #include +#include +#include "incremental/incremental.hpp" +#include "incremental/platform/library.hpp" #include "incremental/platform/stdin.hpp" -#include "rsl/testing/output.hpp" - +#include "incremental/platform/event_loop.hpp" int main() { using namespace rsl::testing::_impl_main; @@ -53,7 +53,7 @@ int main() { root.run(selected_reporter.get()); for (auto& [path, test_set] : runner.test_sets) { - dlclose(test_set.handle); + unload_library(test_set.handle); test_set.tests = {}; } @@ -72,7 +72,7 @@ int main() { // // updated.run(selected_reporter.get(), false); // } // }; - TerminalCommand commands{runner}; + TerminalCommand commands{runner, watch}; auto loop = EventLoop(commands, watch); loop.run(); From 96ef171b78851e1cbdeee939f93b7d7275bd9aa6 Mon Sep 17 00:00:00 2001 From: Tsche Date: Fri, 21 Nov 2025 04:19:01 +0000 Subject: [PATCH 05/15] watcher cleanup, doodles for dependency watching --- src/main/incremental/config_parser.hpp | 1 + src/main/incremental/incremental.hpp | 49 +++++----- .../incremental/platform/posix/taskset.cpp | 6 +- src/main/incremental/platform/posix/watch.cpp | 28 +++--- src/main/incremental/platform/taskset.hpp | 4 + src/main/incremental/platform/watch.hpp | 6 +- src/main/main.cpp | 93 ++++++++++++++++++- 7 files changed, 144 insertions(+), 43 deletions(-) diff --git a/src/main/incremental/config_parser.hpp b/src/main/incremental/config_parser.hpp index 5f9f646..a5b5f0c 100644 --- a/src/main/incremental/config_parser.hpp +++ b/src/main/incremental/config_parser.hpp @@ -14,6 +14,7 @@ namespace rsl::testing::_impl_main { struct TestTU { ProgramInvocation invocation; std::filesystem::path out_path; + std::filesystem::path source_path; }; template diff --git a/src/main/incremental/incremental.hpp b/src/main/incremental/incremental.hpp index ea2c672..97a090d 100644 --- a/src/main/incremental/incremental.hpp +++ b/src/main/incremental/incremental.hpp @@ -21,6 +21,7 @@ struct IncrementalRunner { // TODO move to config? static constexpr std::array allowed_extensions = {".cpp"}; + public: IncrementalRunner() = default; explicit IncrementalRunner(std::filesystem::path const& config_path) @@ -28,7 +29,8 @@ struct IncrementalRunner { [[nodiscard]] TestTU make_invocation(std::string_view config_name, - std::filesystem::path const& test_path) const { + std::filesystem::path const& test_path, + bool dump_dependencies = false) const { const auto& options = config.options; const auto& configurations = config.configurations; @@ -64,33 +66,37 @@ struct IncrementalRunner { cmd.push_back(std::format("-D{}", d)); } - for (auto const& lib : options.link_libraries) { - if (lib.is_absolute()) { - cmd.push_back(std::format("-L{}", lib.string())); - } else { - cmd.push_back(std::format("-l{}", lib.string())); + if (not dump_dependencies) { + for (auto const& lib : options.link_libraries) { + if (lib.is_absolute()) { + cmd.push_back(std::format("-L{}", lib.string())); + } else { + cmd.push_back(std::format("-l{}", lib.string())); + } } - } - // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); - // cmd.push_back(std::format("-L{}", build_path.string())); - cmd.emplace_back("-fPIC"); - cmd.emplace_back("-shared"); + // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); + // cmd.push_back(std::format("-L{}", build_path.string())); + cmd.emplace_back("-fPIC"); + cmd.emplace_back("-shared"); + + // out file + cmd.emplace_back("-o"); + cmd.push_back(out_path.string()); + } if (auto ns = config.project.namespace_; not ns.empty()) { cmd.emplace_back("-DRSL_TEST_NAMESPACE=" + ns); } - cmd.emplace_back("-ftime-trace"); - - // out file - cmd.emplace_back("-o"); - cmd.push_back(out_path.string()); - // input file cmd.push_back(std::string(test_path.string())); - return {compiler_path, cmd, out_path}; + 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) { @@ -117,11 +123,12 @@ struct IncrementalRunner { return result; } - std::vector expand_tests(std::vector const& tests) { + 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)); + test_tus.push_back(make_invocation(name, test, dump)); } } return test_tus; @@ -164,7 +171,7 @@ struct IncrementalRunner { void unload(std::filesystem::path const& file) { if (auto it = test_sets.find(file); it != test_sets.end()) { - auto&[handle, tests] = it->second; + auto& [handle, tests] = it->second; if (handle != nullptr) { unload_library(handle); tests = {}; diff --git a/src/main/incremental/platform/posix/taskset.cpp b/src/main/incremental/platform/posix/taskset.cpp index 459907a..f08b978 100644 --- a/src/main/incremental/platform/posix/taskset.cpp +++ b/src/main/incremental/platform/posix/taskset.cpp @@ -34,8 +34,9 @@ ProcessResult run_program_on_cpu(int cpu, const char* program, char* const argv[ close(out_pipe[1]); close(err_pipe[0]); close(err_pipe[1]); - - pin_to_cpu(cpu); + if (cpu >= 0) { + pin_to_cpu(cpu); + } execvp(program, argv); perror("execvp failed"); @@ -78,6 +79,7 @@ ProcessResult run_program_on_cpu(int cpu, const char* program, char* const argv[ 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) { diff --git a/src/main/incremental/platform/posix/watch.cpp b/src/main/incremental/platform/posix/watch.cpp index 53da9cf..9ff21fc 100644 --- a/src/main/incremental/platform/posix/watch.cpp +++ b/src/main/incremental/platform/posix/watch.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -160,12 +159,9 @@ void Watcher::on_readable(std::span data) { pending.append_range(data); auto events = try_decode_events(pending); for (auto const& ev : events) { - auto it = watchers.find(ev.wd); - std::filesystem::path dir = (it != watchers.end()) ? it->second : std::filesystem::path{}; + auto it = std::ranges::find_if(watchers, [&](auto&& obj) { return obj.second == ev.wd; }); + std::filesystem::path dir = (it != watchers.end()) ? it->first : std::filesystem::path{}; std::filesystem::path full = ev.name.empty() ? dir : dir / ev.name; - std::println("dispatching {} {} ", full.string(), ev.mask); - - // fnc(full, FileEvent(ev.mask)); if ((ev.mask & FileEvent::CREATE) && (ev.mask & IN_ISDIR)) { add_watch(full, true); @@ -174,7 +170,7 @@ void Watcher::on_readable(std::span data) { 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->first); + impl->rm_watch(it->second); watchers.erase(it); } file_deleted(full); @@ -201,17 +197,15 @@ void Watcher::add_watch(std::filesystem::path const& dir, bool recurse) { return; } - for (auto const& [_, path] : watchers) { - if (dir == path) { - // already watching - return; - } + if (watchers.contains(dir)) { + // already watching + return; } std::println("watching {} for changes", dir.string()); int top_wd = impl->add_watch(dir); - watchers.emplace(top_wd, dir); + watchers.emplace(dir, top_wd); if (not recurse) { return; } @@ -220,7 +214,7 @@ void Watcher::add_watch(std::filesystem::path const& dir, bool recurse) { if (ent.is_directory()) { try { int wd = impl->add_watch(ent.path()); - watchers.emplace(wd, 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()); @@ -230,7 +224,11 @@ void Watcher::add_watch(std::filesystem::path const& dir, bool recurse) { } void Watcher::rm_watch(std::filesystem::path const& dir) { - // TODO + 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/incremental/platform/taskset.hpp b/src/main/incremental/platform/taskset.hpp index 3502bb4..09140ee 100644 --- a/src/main/incremental/platform/taskset.hpp +++ b/src/main/incremental/platform/taskset.hpp @@ -21,4 +21,8 @@ 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/incremental/platform/watch.hpp b/src/main/incremental/platform/watch.hpp index 7856529..ce520ea 100644 --- a/src/main/incremental/platform/watch.hpp +++ b/src/main/incremental/platform/watch.hpp @@ -12,7 +12,7 @@ struct WatcherImpl; class Watcher { WatcherImpl* impl; - std::unordered_map watchers; // TODO flip + std::unordered_map watchers; // TODO flip std::vector pending; IncrementalRunner* runner; @@ -26,7 +26,9 @@ class Watcher { void add_watch(std::filesystem::path const& dir, bool recurse = true); void rm_watch(std::filesystem::path const& dir); - void file_modified(std::filesystem::path const& path) {} + void file_modified(std::filesystem::path const& path) { + + } void file_deleted(std::filesystem::path const& path) {} }; diff --git a/src/main/main.cpp b/src/main/main.cpp index a88822c..4c744d3 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -4,15 +4,73 @@ #include #include +#include +#include #include "incremental/incremental.hpp" #include "incremental/platform/library.hpp" #include "incremental/platform/stdin.hpp" #include "incremental/platform/event_loop.hpp" +#include "incremental/platform/taskset.hpp" + +struct HeaderDependencies { + std::filesystem::path file; + std::vector dependencies; +}; + +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); +} + +HeaderDependencies parse_dependencies(std::string_view input) { + auto colon = input.find(':'); + if (colon == std::string_view::npos) { + return {}; + } + input.remove_prefix(colon + 1); + + auto newline = input.find('\n'); + if (newline == std::string_view::npos) { + return {}; + } + auto tu = std::string_view(input.begin(), newline); + if (!tu.empty() && tu.back() == '\\') { + tu.remove_suffix(1); + } + input.remove_prefix(newline); + + 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); + } + + if (!line_sv.empty()) { + deps.emplace_back(trim(line_sv)); + } + } + + return {tu, std::move(deps)}; +} + +bool is_relative_to(std::filesystem::path const& path, std::filesystem::path const& base) { + auto abs_path = std::filesystem::weakly_canonical(path); + auto abs_base = std::filesystem::weakly_canonical(base); + auto [it_path, it_base] = std::ranges::mismatch(abs_path, abs_base); + return it_base == abs_base.end(); +} int main() { using namespace rsl::testing::_impl_main; - constexpr bool incremental = true; + constexpr bool incremental = false; + constexpr bool watch_dependencies = true; const auto executable_path = std::filesystem::canonical("/proc/self/exe").parent_path(); const std::filesystem::path config_path = executable_path / "test-runner.json"; @@ -20,6 +78,29 @@ int main() { auto runner = IncrementalRunner(config_path); auto test_inputs = runner.discover_tests(); + auto dep_map = std::unordered_map>(); + auto dep_folders = std::set(); + + for (auto&& tu : runner.expand_tests(test_inputs, true)) { + runner.pool.submit(tu); + } + runner.pool.wait(); + + for (auto [_, result] : runner.pool.collect()) { + if (result.exit_code != 0) { + continue; + } + auto [tu, dependencies] = parse_dependencies(result.stdout_str); + for (auto const& dependency : dependencies) { + if (not is_relative_to(dependency, runner.config.project.project_path)) { + continue; + } + + dep_folders.insert(std::filesystem::canonical(dependency).parent_path()); + dep_map[dependency].insert(tu); + } + } + runner.recompile(runner.expand_tests(test_inputs)); rsl::testing::TestRoot root; @@ -38,7 +119,7 @@ int main() { // std::println("{} -> {}", path.string(), test_set.tests.size()); for (auto test_def : test_set.tests) { auto test = test_def(path); - // root.insert(test); + root.insert(test); // if (file_path == path) { tests.insert(test); // } @@ -63,6 +144,12 @@ int main() { watch.add_watch(path); } + if (watch_dependencies) { + // compile TUs with -MM to collect dependencies, dedupe and watch unique parents + for (auto&& [path, _] : runner.test_sets) { + // runner.collect_dependencies(path); + } + } // auto watch_fnc = [&](auto path, FileEvent event) { // if ((event & FileEvent::MODIFY) == FileEvent::MODIFY) { // // compile TU @@ -73,7 +160,7 @@ int main() { // } // }; TerminalCommand commands{runner, watch}; - + auto loop = EventLoop(commands, watch); loop.run(); } From 4bddb7cb4a21506b80834c96b9de7182821140b5 Mon Sep 17 00:00:00 2001 From: Tsche Date: Sat, 22 Nov 2025 23:27:55 +0000 Subject: [PATCH 06/15] dependency watching, library bugfix, progress bar --- example/always_passes.cpp | 2 +- src/main/incremental/compile_pool.hpp | 27 ++++- src/main/incremental/incremental.hpp | 15 ++- src/main/incremental/platform/library.hpp | 2 +- .../incremental/platform/posix/library.cpp | 9 +- src/main/incremental/platform/posix/watch.cpp | 51 +++------ src/main/incremental/platform/stdin.hpp | 1 + src/main/incremental/platform/watch.hpp | 106 +++++++++++++++++- src/main/main.cpp | 85 +------------- 9 files changed, 167 insertions(+), 131 deletions(-) diff --git a/example/always_passes.cpp b/example/always_passes.cpp index ca583a6..816b368 100644 --- a/example/always_passes.cpp +++ b/example/always_passes.cpp @@ -21,4 +21,4 @@ auto zoinks(bool zoinks) { zoinks(false); // zoinks(true); } -} // namespace demo \ No newline at end of file +} // namespace demo diff --git a/src/main/incremental/compile_pool.hpp b/src/main/incremental/compile_pool.hpp index a42172f..7c5ab65 100644 --- a/src/main/incremental/compile_pool.hpp +++ b/src/main/incremental/compile_pool.hpp @@ -63,6 +63,25 @@ class CompilePool { } } + 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(); + } + on_update(lastCompleted, total); + } + std::vector collect() { std::vector output; { @@ -90,11 +109,11 @@ class CompilePool { task = std::move(tasks.front()); tasks.pop(); } - std::println("building {}", task.out_path.string()); - auto start_time = std::chrono::steady_clock::now(); + // 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)); + // 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)); diff --git a/src/main/incremental/incremental.hpp b/src/main/incremental/incremental.hpp index 97a090d..2cdc238 100644 --- a/src/main/incremental/incremental.hpp +++ b/src/main/incremental/incremental.hpp @@ -1,10 +1,10 @@ #pragma once #include +#include #include "compile_pool.hpp" #include "config_parser.hpp" #include "platform/library.hpp" -#include "platform/watch.hpp" #include @@ -141,7 +141,18 @@ struct IncrementalRunner { pool.submit(tu); } - pool.wait(); + 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); + }; + progress(0, tus.size()); + pool.wait(progress); + std::println(""); for (auto&& [tu, result] : pool.collect()) { if (result.exit_code != 0) { diff --git a/src/main/incremental/platform/library.hpp b/src/main/incremental/platform/library.hpp index 62d76ac..2391320 100644 --- a/src/main/incremental/platform/library.hpp +++ b/src/main/incremental/platform/library.hpp @@ -6,7 +6,7 @@ namespace rsl::testing::_impl_main { using library_handle = void*; library_handle load_library(std::string_view path); - void unload_library(library_handle handle); + void unload_library(library_handle& handle); void* find_symbol(library_handle handle, std::string_view name); template diff --git a/src/main/incremental/platform/posix/library.cpp b/src/main/incremental/platform/posix/library.cpp index 2785c17..037e529 100644 --- a/src/main/incremental/platform/posix/library.cpp +++ b/src/main/incremental/platform/posix/library.cpp @@ -9,12 +9,15 @@ library_handle load_library(std::string_view path) { return dlopen(std::string(path).c_str(), RTLD_NOW); } -void unload_library(library_handle handle) { - dlclose(handle); +void unload_library(library_handle& handle) { + if (handle != nullptr) { + dlclose(handle); + } + handle = nullptr; } void* find_symbol(library_handle handle, std::string_view name) { return dlsym(handle, std::string(name).c_str()); } -} // namespace rsl::testing::_main_impl \ No newline at end of file +} // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/incremental/platform/posix/watch.cpp b/src/main/incremental/platform/posix/watch.cpp index 9ff21fc..3415ce5 100644 --- a/src/main/incremental/platform/posix/watch.cpp +++ b/src/main/incremental/platform/posix/watch.cpp @@ -1,5 +1,6 @@ #include "../watch.hpp" +#include #include #include @@ -85,35 +86,19 @@ static std::vector try_decode_events(std::vector& pending) { return out; } -enum FileEvent { - ACCESS = 0x00000001, /* File was accessed. */ - MODIFY = 0x00000002, /* File was modified. */ - ATTRIB = 0x00000004, /* Metadata changed. */ - CLOSE_WRITE = 0x00000008, /* Writtable file was closed. */ - CLOSE_NOWRITE = 0x00000010, /* Unwrittable file closed. */ - CLOSE = (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE), /* Close. */ - OPEN = 0x00000020, /* File was opened. */ - MOVED_FROM = 0x00000040, /* File was moved from X. */ - MOVED_TO = 0x00000080, /* File was moved to Y. */ - MOVE = (IN_MOVED_FROM | IN_MOVED_TO), /* Moves. */ - CREATE = 0x00000100, /* Subfile was created. */ - DELETE = 0x00000200, /* Subfile was deleted. */ - DELETE_SELF = 0x00000400, /* Self was deleted. */ - MOVE_SELF = 0x00000800, /* Self was moved. */ - - ISDIR = 0x40000000, - - /* Events sent by the kernel. */ - UNMOUNT = 0x00002000, /* Backing fs was unmounted. */ - Q_OVERFLOW = 0x00004000, /* Event queued overflowed. */ - IGNORED = 0x00008000, /* File was ignored. */ -}; +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; + std::unordered_map last_modified; WatcherImpl(WatcherImpl const&) = delete; WatcherImpl(WatcherImpl&&) = default; @@ -154,16 +139,16 @@ uintptr_t Watcher::get_handle() const { } void Watcher::on_readable(std::span data) { - constexpr auto debounce_threshold = std::chrono::milliseconds(100); + 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()) ? it->first : std::filesystem::path{}; + std::filesystem::path dir = (it != watchers.end()) ? std::filesystem::weakly_canonical(it->first) : std::filesystem::path{}; std::filesystem::path full = ev.name.empty() ? dir : dir / ev.name; - if ((ev.mask & FileEvent::CREATE) && (ev.mask & IN_ISDIR)) { + if ((ev.mask & IN_CREATE) && (ev.mask & IN_ISDIR)) { add_watch(full, true); } @@ -176,16 +161,12 @@ void Watcher::on_readable(std::span data) { file_deleted(full); } - if ((ev.mask & FileEvent::MODIFY)) { - auto now = std::chrono::steady_clock::now(); - if (auto it = impl->last_modified.find(dir); - it != impl->last_modified.end() && now - it->second < debounce_threshold) { - // debounce + if ((ev.mask & IN_MODIFY)) { + auto info = path_stat(full); + if (impl->last_modified[full] >= info.st_mtime) { continue; } - impl->last_modified[dir] = now; - - std::println("modified: {}", full.string()); + impl->last_modified[full] = info.st_mtime; file_modified(full); } } diff --git a/src/main/incremental/platform/stdin.hpp b/src/main/incremental/platform/stdin.hpp index 803465c..8aac591 100644 --- a/src/main/incremental/platform/stdin.hpp +++ b/src/main/incremental/platform/stdin.hpp @@ -4,6 +4,7 @@ #include #include +#include "watch.hpp" #include "../incremental.hpp" #ifdef __unix__ diff --git a/src/main/incremental/platform/watch.hpp b/src/main/incremental/platform/watch.hpp index ce520ea..38a5c7c 100644 --- a/src/main/incremental/platform/watch.hpp +++ b/src/main/incremental/platform/watch.hpp @@ -1,21 +1,71 @@ #pragma once +#include #include #include #include #include +#include "../incremental.hpp" namespace rsl::testing::_impl_main { - -struct IncrementalRunner; 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 = std::filesystem::weakly_canonical(path); + auto abs_base = std::filesystem::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; // TODO flip + std::vector pending; IncrementalRunner* runner; + std::set tests; + std::unordered_map> dependencies; + public: Watcher() = delete; explicit Watcher(IncrementalRunner& runner); @@ -26,8 +76,58 @@ class Watcher { void add_watch(std::filesystem::path const& dir, bool recurse = true); void rm_watch(std::filesystem::path const& dir); - void file_modified(std::filesystem::path const& path) { + 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, _] = tests.insert(r.source_path); + for (auto dependency : parse_dependencies(result.stdout_str)) { + dependency = std::filesystem::canonical(dependency); + if (dependency == r.source_path) { continue; } + if (not is_relative_to(dependency, runner->config.project.project_path)) { + continue; + } + dependencies[dependency].insert(&*it); + 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 (tests.contains(canonical)) { + std::println("test modified: {}", canonical.string()); + update_dependencies(canonical); + runner->recompile(runner->expand_tests({path})); + } else { + std::println("test dependency modified: {} {}", canonical.string(), dependencies[canonical].size()); + std::vector affected; + for (auto* it : dependencies[canonical]) { + affected.push_back(*it); + } + runner->recompile(runner->expand_tests(affected)); + } } void file_deleted(std::filesystem::path const& path) {} diff --git a/src/main/main.cpp b/src/main/main.cpp index 4c744d3..b7286ce 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -11,65 +11,12 @@ #include "incremental/platform/library.hpp" #include "incremental/platform/stdin.hpp" #include "incremental/platform/event_loop.hpp" -#include "incremental/platform/taskset.hpp" - -struct HeaderDependencies { - std::filesystem::path file; - std::vector dependencies; -}; - -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); -} - -HeaderDependencies parse_dependencies(std::string_view input) { - auto colon = input.find(':'); - if (colon == std::string_view::npos) { - return {}; - } - input.remove_prefix(colon + 1); - - auto newline = input.find('\n'); - if (newline == std::string_view::npos) { - return {}; - } - auto tu = std::string_view(input.begin(), newline); - if (!tu.empty() && tu.back() == '\\') { - tu.remove_suffix(1); - } - input.remove_prefix(newline); - - 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); - } - - if (!line_sv.empty()) { - deps.emplace_back(trim(line_sv)); - } - } - return {tu, std::move(deps)}; -} -bool is_relative_to(std::filesystem::path const& path, std::filesystem::path const& base) { - auto abs_path = std::filesystem::weakly_canonical(path); - auto abs_base = std::filesystem::weakly_canonical(base); - auto [it_path, it_base] = std::ranges::mismatch(abs_path, abs_base); - return it_base == abs_base.end(); -} int main() { using namespace rsl::testing::_impl_main; - constexpr bool incremental = false; + constexpr bool incremental = true; constexpr bool watch_dependencies = true; const auto executable_path = std::filesystem::canonical("/proc/self/exe").parent_path(); @@ -78,29 +25,6 @@ int main() { auto runner = IncrementalRunner(config_path); auto test_inputs = runner.discover_tests(); - auto dep_map = std::unordered_map>(); - auto dep_folders = std::set(); - - for (auto&& tu : runner.expand_tests(test_inputs, true)) { - runner.pool.submit(tu); - } - runner.pool.wait(); - - for (auto [_, result] : runner.pool.collect()) { - if (result.exit_code != 0) { - continue; - } - auto [tu, dependencies] = parse_dependencies(result.stdout_str); - for (auto const& dependency : dependencies) { - if (not is_relative_to(dependency, runner.config.project.project_path)) { - continue; - } - - dep_folders.insert(std::filesystem::canonical(dependency).parent_path()); - dep_map[dependency].insert(tu); - } - } - runner.recompile(runner.expand_tests(test_inputs)); rsl::testing::TestRoot root; @@ -143,12 +67,9 @@ int main() { for (auto const& path : runner.config.project.test_path) { watch.add_watch(path); } - + if (watch_dependencies) { - // compile TUs with -MM to collect dependencies, dedupe and watch unique parents - for (auto&& [path, _] : runner.test_sets) { - // runner.collect_dependencies(path); - } + watch.update_dependencies(); } // auto watch_fnc = [&](auto path, FileEvent event) { // if ((event & FileEvent::MODIFY) == FileEvent::MODIFY) { From e4708711bdea8e33aac9c23782927dd3447df68a Mon Sep 17 00:00:00 2001 From: Tsche Date: Sun, 23 Nov 2025 04:30:11 +0000 Subject: [PATCH 07/15] compdb support --- src/main/incremental/compdb.hpp | 108 ++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/main/incremental/compdb.hpp diff --git a/src/main/incremental/compdb.hpp b/src/main/incremental/compdb.hpp new file mode 100644 index 0000000..abaf1af --- /dev/null +++ b/src/main/incremental/compdb.hpp @@ -0,0 +1,108 @@ +#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()) { + obj.emplace("arguments", *arguments); + } 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 { + if (directory != other.directory || file != other.file) { + return false; + } + return ((other.arguments.has_value() && arguments.has_value() && + other.arguments == arguments) || + (other.command.has_value() && command.has_value() && other.command == command)) && + (!other.output.has_value() && !output.has_value() || other.output == output); + } +}; + +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 From b488f40537d40225630ac0501ef2fa5d8cd1e3ab Mon Sep 17 00:00:00 2001 From: Tsche Date: Sun, 23 Nov 2025 04:39:46 +0000 Subject: [PATCH 08/15] refactor --- src/main/incremental/incremental.hpp | 106 +++++++++++++++++------- src/main/incremental/platform/watch.hpp | 2 +- src/main/main.cpp | 3 +- 3 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/main/incremental/incremental.hpp b/src/main/incremental/incremental.hpp index 2cdc238..85d4b73 100644 --- a/src/main/incremental/incremental.hpp +++ b/src/main/incremental/incremental.hpp @@ -1,57 +1,79 @@ #pragma once +#include #include #include +#include #include "compile_pool.hpp" #include "config_parser.hpp" +#include "compdb.hpp" #include "platform/library.hpp" #include namespace rsl::testing::_impl_main { struct TestSet { - void* handle; + void* handle = nullptr; std::set tests; + + void unload() { + if (handle != nullptr) { + unload_library(handle); + tests = {}; + } + } +}; + +struct TestUnit { + std::filesystem::path source_path; + + // library path -> metadata (path is different between configurations) + std::unordered_map sets; }; struct IncrementalRunner { CompilePool pool; // TODO use more generic task pool? RunnerConfig config; + + std::map units; + // reverse dependency map + std::unordered_map> dependencies; + + // TODO map source -> testset std::unordered_map test_sets; + CompileCommands compdb; + // 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)) {} - - [[nodiscard]] - TestTU make_invocation(std::string_view config_name, - std::filesystem::path const& test_path, - bool dump_dependencies = false) const { - const auto& options = config.options; - - const auto& configurations = config.configurations; + : config(load_runner_config(config_path)) { + compdb = CompileCommands(config.project.build_path / "compile_commands.json"); + } - const std::filesystem::path build_path = config.project.build_path; - const std::filesystem::path project_path = config.project.project_path; + void update_compdb() { + compdb.load(); + for (auto const& path : test_sets) { + auto tu = make_invocation("default", path.first); + compdb.append_if_missing({.directory = tu.source_path.parent_path(), + .file = tu.source_path, + .arguments = tu.invocation.arguments, + .output = tu.out_path}); + } + compdb.save(); + } - const auto& cfg = configurations.at(std::string(config_name)); + std::vector expand_options(std::string_view config_name) const { + const auto& options = config.options; + const auto& cfg = config.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); - const std::string compiler_path = cfg.compiler_path; - - std::filesystem::path out_path = - build_path / std::filesystem::relative(test_path, project_path); - out_path.replace_extension(".so"); - std::vector cmd; - - cmd.push_back(compiler_path); cmd.push_back(standard); for (auto const& o : options.compile_options) { @@ -66,8 +88,15 @@ struct IncrementalRunner { cmd.push_back(std::format("-D{}", d)); } - if (not dump_dependencies) { - for (auto const& lib : options.link_libraries) { + if (auto ns = config.project.namespace_; not ns.empty()) { + cmd.emplace_back("-DRSL_TEST_NAMESPACE=" + ns); + } + return cmd; + } + + std::vector expand_link_options() const { + std::vector cmd; + for (auto const& lib : config.options.link_libraries) { if (lib.is_absolute()) { cmd.push_back(std::format("-L{}", lib.string())); } else { @@ -79,16 +108,33 @@ struct IncrementalRunner { // cmd.push_back(std::format("-L{}", build_path.string())); cmd.emplace_back("-fPIC"); cmd.emplace_back("-shared"); + return cmd; + } + + [[nodiscard]] + TestTU make_invocation(std::string_view config_name, + std::filesystem::path const& test_path, + bool dump_dependencies = false) const { + 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 / std::filesystem::relative(test_path, project_path); + out_path.replace_extension(".so"); + + std::vector cmd = {compiler_path}; + cmd.append_range(expand_options(config_name)); + + if (not dump_dependencies) { + cmd.append_range(expand_link_options()); // out file cmd.emplace_back("-o"); cmd.push_back(out_path.string()); } - if (auto ns = config.project.namespace_; not ns.empty()) { - cmd.emplace_back("-DRSL_TEST_NAMESPACE=" + ns); - } - // input file cmd.push_back(std::string(test_path.string())); @@ -182,11 +228,7 @@ struct IncrementalRunner { void unload(std::filesystem::path const& file) { if (auto it = test_sets.find(file); it != test_sets.end()) { - auto& [handle, tests] = it->second; - if (handle != nullptr) { - unload_library(handle); - tests = {}; - } + it->second.unload(); } } }; diff --git a/src/main/incremental/platform/watch.hpp b/src/main/incremental/platform/watch.hpp index 38a5c7c..1bcb01c 100644 --- a/src/main/incremental/platform/watch.hpp +++ b/src/main/incremental/platform/watch.hpp @@ -58,7 +58,7 @@ inline bool is_relative_to(std::filesystem::path const& path, std::filesystem::p class Watcher { WatcherImpl* impl; - std::unordered_map watchers; // TODO flip + std::unordered_map watchers; std::vector pending; IncrementalRunner* runner; diff --git a/src/main/main.cpp b/src/main/main.cpp index b7286ce..b438eb7 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -24,8 +24,9 @@ int main() { auto runner = IncrementalRunner(config_path); auto test_inputs = runner.discover_tests(); - + runner.recompile(runner.expand_tests(test_inputs)); + runner.update_compdb(); rsl::testing::TestRoot root; From 45c76ff3bf85790842850705b283c168a47b94b8 Mon Sep 17 00:00:00 2001 From: Tsche Date: Sun, 23 Nov 2025 08:31:34 +0000 Subject: [PATCH 09/15] fix hot reloading, explore test-local registry --- example/params_debug.cpp | 31 --- .../rsl/testing/_testing_impl/discovery.hpp | 11 +- include/rsl/testing/all.hpp | 11 ++ src/main/incremental/compdb.hpp | 12 +- src/main/incremental/incremental.hpp | 177 ++++++++++++------ src/main/incremental/platform/library.hpp | 4 +- .../incremental/platform/posix/library.cpp | 6 +- src/main/incremental/platform/watch.hpp | 4 +- src/main/main.cpp | 54 ++---- 9 files changed, 175 insertions(+), 135 deletions(-) delete mode 100644 example/params_debug.cpp 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 a84fa9b..391d63c 100644 --- a/include/rsl/testing/_testing_impl/discovery.hpp +++ b/include/rsl/testing/_testing_impl/discovery.hpp @@ -128,11 +128,20 @@ struct TestDiscovery { }; std::set& registry(); +inline std::set& local_registry() { + static std::set reg; + return reg; +} + 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); +#ifdef RSL_TEST_UNIT + local_registry().insert(test); +#else + _testing_impl::registry().insert(test); +#endif } return true; } diff --git a/include/rsl/testing/all.hpp b/include/rsl/testing/all.hpp index 3c87d36..20d2d0f 100644 --- a/include/rsl/testing/all.hpp +++ b/include/rsl/testing/all.hpp @@ -65,3 +65,14 @@ 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 +extern "C" +__attribute__((__visibility__("default"))) +__attribute__((__used__)) +inline +std::set load_tests() { + return rsl::testing::_testing_impl::local_registry(); +} + +#endif \ No newline at end of file diff --git a/src/main/incremental/compdb.hpp b/src/main/incremental/compdb.hpp index abaf1af..c5a436a 100644 --- a/src/main/incremental/compdb.hpp +++ b/src/main/incremental/compdb.hpp @@ -22,7 +22,9 @@ struct CompileCommand { { "file", file} }; if (arguments.has_value()) { - obj.emplace("arguments", *arguments); + 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); } @@ -49,13 +51,7 @@ struct CompileCommand { } bool operator==(CompileCommand const& other) const { - if (directory != other.directory || file != other.file) { - return false; - } - return ((other.arguments.has_value() && arguments.has_value() && - other.arguments == arguments) || - (other.command.has_value() && command.has_value() && other.command == command)) && - (!other.output.has_value() && !output.has_value() || other.output == output); + return directory == other.directory && file == other.file; } }; diff --git a/src/main/incremental/incremental.hpp b/src/main/incremental/incremental.hpp index 85d4b73..55fc92f 100644 --- a/src/main/incremental/incremental.hpp +++ b/src/main/incremental/incremental.hpp @@ -1,4 +1,7 @@ #pragma once +#include +#include +#include #include #include #include @@ -9,6 +12,7 @@ #include "compdb.hpp" #include "platform/library.hpp" +#include #include namespace rsl::testing::_impl_main { @@ -16,19 +20,96 @@ 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); + // unload_library(handle); tests = {}; } } -}; +}; // namespace rsl::testing::_impl_main struct TestUnit { - std::filesystem::path source_path; - // 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) { + // if (not rsl::testing::_testing_impl::registry().empty()) { + // std::println("test registry not empty"); + // rsl::testing::_testing_impl::registry().clear(); + // } + auto path = std::filesystem::canonical(library_path); + auto tmp_path = std::filesystem::path(library_path).replace_extension(".so." + std::to_string(counter++)); + while (exists(tmp_path)) { + std::println("already got {} ", tmp_path.string()); + 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) { + // rsl::testing::_testing_impl::registry().clear(); + 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 = fnc(); + sets.insert_or_assign(path, TestSet{handle, test_sets}); + return test_sets; + } + + static void remove_stale(std::filesystem::path const& path){ + std::filesystem::path tmp_path = 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(std::filesystem::weakly_canonical(path)); it != sets.end()) { + it->second.unload(); + sets.erase(it); + } + } }; struct IncrementalRunner { @@ -38,10 +119,7 @@ struct IncrementalRunner { std::map units; // reverse dependency map std::unordered_map> dependencies; - - // TODO map source -> testset - std::unordered_map test_sets; - + std::unique_ptr reporter; CompileCommands compdb; // TODO move to config? @@ -51,14 +129,16 @@ struct IncrementalRunner { IncrementalRunner() = default; explicit IncrementalRunner(std::filesystem::path const& config_path) : config(load_runner_config(config_path)) { - compdb = CompileCommands(config.project.build_path / "compile_commands.json"); + // 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("plain"); } void update_compdb() { compdb.load(); - for (auto const& path : test_sets) { - auto tu = make_invocation("default", path.first); - compdb.append_if_missing({.directory = tu.source_path.parent_path(), + for (auto const& [path, unit] : units) { + auto tu = make_invocation("default", path, false, false); + compdb.append_if_missing({.directory = config.project.build_path, .file = tu.source_path, .arguments = tu.invocation.arguments, .output = tu.out_path}); @@ -68,11 +148,11 @@ struct IncrementalRunner { std::vector expand_options(std::string_view config_name) const { const auto& options = config.options; - const auto& cfg = config.configurations.at(std::string(config_name)); + const auto& cfg = config.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); + 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); @@ -91,43 +171,48 @@ struct IncrementalRunner { if (auto ns = config.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 : config.options.link_libraries) { - if (lib.is_absolute()) { - cmd.push_back(std::format("-L{}", lib.string())); - } else { - cmd.push_back(std::format("-l{}", lib.string())); - } + if (lib.is_absolute()) { + cmd.push_back(std::format("-L{}", lib.string())); + } else { + cmd.push_back(std::format("-l{}", lib.string())); } + } - // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); - // cmd.push_back(std::format("-L{}", build_path.string())); - cmd.emplace_back("-fPIC"); - cmd.emplace_back("-shared"); - return cmd; + // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); + // cmd.push_back(std::format("-L{}", build_path.string())); + cmd.emplace_back("-fPIC"); + cmd.emplace_back("-shared"); + return cmd; } + mutable size_t counter = 0; [[nodiscard]] TestTU make_invocation(std::string_view config_name, std::filesystem::path const& test_path, - bool dump_dependencies = false) const { + bool dump_dependencies = false, + bool link = true) const { 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 auto& cfg = config.configurations.at(std::string(config_name)); const std::string compiler_path = cfg.compiler_path; - std::filesystem::path out_path = build_path / std::filesystem::relative(test_path, project_path); + std::filesystem::path out_path = + build_path / std::filesystem::relative(test_path, project_path); out_path.replace_extension(".so"); + out_path = std::filesystem::weakly_canonical(out_path); std::vector cmd = {compiler_path}; cmd.append_range(expand_options(config_name)); - if (not dump_dependencies) { + if (link && not dump_dependencies) { cmd.append_range(expand_link_options()); // out file @@ -180,9 +265,9 @@ struct IncrementalRunner { return test_tus; } - void recompile(std::vector const& 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) { pool.submit(tu); } @@ -200,6 +285,7 @@ struct IncrementalRunner { pool.wait(progress); std::println(""); + TestRoot root; for (auto&& [tu, result] : pool.collect()) { if (result.exit_code != 0) { std::println("ERROR!"); @@ -207,29 +293,14 @@ struct IncrementalRunner { std::println("====== stderr ======\n{}", result.stderr_str); continue; } - if (not rsl::testing::_testing_impl::registry().empty()) { - std::println("test registry not empty"); - rsl::testing::_testing_impl::registry().clear(); + + 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); } - // check if we have the key already, make sure old one is unloaded - unload(tu.out_path); - - library_handle handle = load_library(tu.out_path.string()); - if (handle != nullptr) { - test_sets[tu.out_path] = {handle, rsl::testing::_testing_impl::registry()}; - } - rsl::testing::_testing_impl::registry().clear(); - } - - if (not rsl::testing::_testing_impl::registry().empty()) { - std::println("test registry not empty"); - } - } - - void unload(std::filesystem::path const& file) { - if (auto it = test_sets.find(file); it != test_sets.end()) { - it->second.unload(); } + return root; } }; } // namespace rsl::testing::_impl_main \ No newline at end of file diff --git a/src/main/incremental/platform/library.hpp b/src/main/incremental/platform/library.hpp index 2391320..6102bc9 100644 --- a/src/main/incremental/platform/library.hpp +++ b/src/main/incremental/platform/library.hpp @@ -7,10 +7,10 @@ namespace rsl::testing::_impl_main { library_handle load_library(std::string_view path); void unload_library(library_handle& handle); - void* find_symbol(library_handle handle, std::string_view name); + void* find_symbol(library_handle handle, std::string const& name); template - T* find_symbol(library_handle handle, std::string_view name) { + 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/incremental/platform/posix/library.cpp b/src/main/incremental/platform/posix/library.cpp index 037e529..b320b34 100644 --- a/src/main/incremental/platform/posix/library.cpp +++ b/src/main/incremental/platform/posix/library.cpp @@ -6,7 +6,7 @@ namespace rsl::testing::_impl_main { library_handle load_library(std::string_view path) { - return dlopen(std::string(path).c_str(), RTLD_NOW); + return dlopen(std::string(path).c_str(), RTLD_NOW | RTLD_LOCAL); } void unload_library(library_handle& handle) { @@ -16,8 +16,8 @@ void unload_library(library_handle& handle) { handle = nullptr; } -void* find_symbol(library_handle handle, std::string_view name) { - return dlsym(handle, std::string(name).c_str()); +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/incremental/platform/watch.hpp b/src/main/incremental/platform/watch.hpp index 1bcb01c..0a0d6df 100644 --- a/src/main/incremental/platform/watch.hpp +++ b/src/main/incremental/platform/watch.hpp @@ -119,14 +119,14 @@ class Watcher { if (tests.contains(canonical)) { std::println("test modified: {}", canonical.string()); update_dependencies(canonical); - runner->recompile(runner->expand_tests({path})); + runner->recompile({path}).run(runner->reporter.get(), false); } else { std::println("test dependency modified: {} {}", canonical.string(), dependencies[canonical].size()); std::vector affected; for (auto* it : dependencies[canonical]) { affected.push_back(*it); } - runner->recompile(runner->expand_tests(affected)); + runner->recompile(affected).run(runner->reporter.get(), false); } } diff --git a/src/main/main.cpp b/src/main/main.cpp index b438eb7..edefbc5 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -12,9 +12,22 @@ #include "incremental/platform/stdin.hpp" #include "incremental/platform/event_loop.hpp" +#include +#include +#include +#include +#include +void handler(int sig) { + void *bt[20]; + int n = backtrace(bt, 20); + backtrace_symbols_fd(bt, n, STDERR_FILENO); + _exit(1); +} int main() { + signal(SIGBUS, handler); + using namespace rsl::testing::_impl_main; constexpr bool incremental = true; constexpr bool watch_dependencies = true; @@ -25,50 +38,21 @@ int main() { auto runner = IncrementalRunner(config_path); auto test_inputs = runner.discover_tests(); - runner.recompile(runner.expand_tests(test_inputs)); - runner.update_compdb(); - - rsl::testing::TestRoot root; - - auto update_tree = [&](auto file_path) { - // remove updated tests from tree - // for (auto&& [path, _] : runner.test_sets) { - // root.remove_by_path(path.string()); - // } - - // rebuild root - root = {}; - // insert - rsl::testing::TestRoot tests; - for (auto&& [path, test_set] : runner.test_sets) { - // std::println("{} -> {}", path.string(), test_set.tests.size()); - for (auto test_def : test_set.tests) { - auto test = test_def(path); - root.insert(test); - // if (file_path == path) { - tests.insert(test); - // } - } - } - return tests; - }; - update_tree(""); - std::unique_ptr selected_reporter; selected_reporter = rsl::testing::Reporter::make("plain"); - root.run(selected_reporter.get()); - for (auto& [path, test_set] : runner.test_sets) { - unload_library(test_set.handle); - test_set.tests = {}; - } + auto root = runner.recompile(test_inputs); + runner.update_compdb(); + root.run(runner.reporter.get()); + + if (incremental) { Watcher watch{runner}; for (auto const& path : runner.config.project.test_path) { watch.add_watch(path); } - + if (watch_dependencies) { watch.update_dependencies(); } From 9102d3ab5803dac370d566aea4a00b347171a8f4 Mon Sep 17 00:00:00 2001 From: Tsche Date: Sun, 23 Nov 2025 08:34:29 +0000 Subject: [PATCH 10/15] cleanup --- src/main/incremental/incremental.hpp | 8 ++++---- src/main/incremental/platform/posix/watch.cpp | 2 +- src/main/incremental/platform/watch.hpp | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/incremental/incremental.hpp b/src/main/incremental/incremental.hpp index 55fc92f..76b8242 100644 --- a/src/main/incremental/incremental.hpp +++ b/src/main/incremental/incremental.hpp @@ -62,7 +62,7 @@ struct TestUnit { // std::println("test registry not empty"); // rsl::testing::_testing_impl::registry().clear(); // } - auto path = std::filesystem::canonical(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)) { std::println("already got {} ", tmp_path.string()); @@ -105,7 +105,7 @@ struct TestUnit { void unload(std::filesystem::path const& path) { remove_stale(path); - if (auto it = sets.find(std::filesystem::weakly_canonical(path)); it != sets.end()) { + if (auto it = sets.find(weakly_canonical(path)); it != sets.end()) { it->second.unload(); sets.erase(it); } @@ -205,9 +205,9 @@ struct IncrementalRunner { const std::string compiler_path = cfg.compiler_path; std::filesystem::path out_path = - build_path / std::filesystem::relative(test_path, project_path); + build_path / relative(test_path, project_path); out_path.replace_extension(".so"); - out_path = std::filesystem::weakly_canonical(out_path); + out_path = weakly_canonical(out_path); std::vector cmd = {compiler_path}; cmd.append_range(expand_options(config_name)); diff --git a/src/main/incremental/platform/posix/watch.cpp b/src/main/incremental/platform/posix/watch.cpp index 3415ce5..1d00491 100644 --- a/src/main/incremental/platform/posix/watch.cpp +++ b/src/main/incremental/platform/posix/watch.cpp @@ -145,7 +145,7 @@ void Watcher::on_readable(std::span 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()) ? std::filesystem::weakly_canonical(it->first) : std::filesystem::path{}; + 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)) { diff --git a/src/main/incremental/platform/watch.hpp b/src/main/incremental/platform/watch.hpp index 0a0d6df..a80007b 100644 --- a/src/main/incremental/platform/watch.hpp +++ b/src/main/incremental/platform/watch.hpp @@ -48,8 +48,8 @@ inline std::vector parse_dependencies(std::string_view in } inline bool is_relative_to(std::filesystem::path const& path, std::filesystem::path const& base) { - auto abs_path = std::filesystem::weakly_canonical(path); - auto abs_base = std::filesystem::weakly_canonical(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(); } @@ -99,7 +99,7 @@ class Watcher { auto [it, _] = tests.insert(r.source_path); for (auto dependency : parse_dependencies(result.stdout_str)) { - dependency = std::filesystem::canonical(dependency); + dependency = canonical(dependency); if (dependency == r.source_path) { continue; } if (not is_relative_to(dependency, runner->config.project.project_path)) { continue; From 9b4fb6fb761d1927bc5e4c17db3dc0832315795b Mon Sep 17 00:00:00 2001 From: Tsche Date: Sun, 30 Nov 2025 13:38:02 +0000 Subject: [PATCH 11/15] wip json output --- example/always_passes.cpp | 6 +- example/always_passes2.cpp | 4 +- example/include/dep.h | 6 + include/rsl/testing/all.hpp | 4 +- src/main/incremental/compile_pool.hpp | 1 - src/main/incremental/incremental.hpp | 95 ++++++++++----- src/main/incremental/platform/posix/watch.cpp | 5 +- src/main/incremental/platform/stdin.hpp | 31 +++-- src/main/incremental/platform/watch.hpp | 47 +++++--- src/main/main.cpp | 111 ++++++++++-------- src/main/reporters/json.cpp | 34 +++++- src/runner.cpp | 4 +- 12 files changed, 236 insertions(+), 112 deletions(-) create mode 100644 example/include/dep.h diff --git a/example/always_passes.cpp b/example/always_passes.cpp index 816b368..4bd43cc 100644 --- a/example/always_passes.cpp +++ b/example/always_passes.cpp @@ -1,4 +1,5 @@ #include +#include "include/dep.h" #include namespace demo { @@ -16,9 +17,10 @@ auto zoinks(bool zoinks) { } [[= rsl::test]] void always_passes() { - std::cout << "foo\n"; - std::cerr << "bar\n"; + // std::cout << "foo\n"; + // std::cerr << "bar\n"; zoinks(false); // zoinks(true); + zoinks(foo()); } } // namespace demo diff --git a/example/always_passes2.cpp b/example/always_passes2.cpp index 8b3b416..136eb2f 100644 --- a/example/always_passes2.cpp +++ b/example/always_passes2.cpp @@ -16,8 +16,8 @@ auto zoinks(bool zoinks) { } void test_always_passes() { - std::cout << "foo\n"; - std::cerr << "bar\n"; + // std::cout << "foo\n"; + // std::cerr << "bar\n"; zoinks(false); // zoinks(true); } 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/include/rsl/testing/all.hpp b/include/rsl/testing/all.hpp index 20d2d0f..6532375 100644 --- a/include/rsl/testing/all.hpp +++ b/include/rsl/testing/all.hpp @@ -71,8 +71,8 @@ extern "C" __attribute__((__visibility__("default"))) __attribute__((__used__)) inline -std::set load_tests() { - return rsl::testing::_testing_impl::local_registry(); +void* load_tests() { + return &rsl::testing::_testing_impl::local_registry(); } #endif \ No newline at end of file diff --git a/src/main/incremental/compile_pool.hpp b/src/main/incremental/compile_pool.hpp index 7c5ab65..b07b8b3 100644 --- a/src/main/incremental/compile_pool.hpp +++ b/src/main/incremental/compile_pool.hpp @@ -79,7 +79,6 @@ class CompilePool { std::this_thread::yield(); } - on_update(lastCompleted, total); } std::vector collect() { diff --git a/src/main/incremental/incremental.hpp b/src/main/incremental/incremental.hpp index 76b8242..d45e046 100644 --- a/src/main/incremental/incremental.hpp +++ b/src/main/incremental/incremental.hpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -58,45 +59,39 @@ struct TestUnit { size_t counter = 0; std::set load(std::filesystem::path const& library_path) { - // if (not rsl::testing::_testing_impl::registry().empty()) { - // std::println("test registry not empty"); - // rsl::testing::_testing_impl::registry().clear(); - // } auto path = canonical(library_path); - auto tmp_path = std::filesystem::path(library_path).replace_extension(".so." + std::to_string(counter++)); + auto tmp_path = + std::filesystem::path(library_path).replace_extension(".so." + std::to_string(counter++)); while (exists(tmp_path)) { - std::println("already got {} ", tmp_path.string()); - tmp_path = std::filesystem::path(library_path).replace_extension(".so." + std::to_string(counter++)); - + 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) { - // rsl::testing::_testing_impl::registry().clear(); std::println("unable to load library"); return {}; } - auto fnc = find_symbol()>(handle, "load_tests"); + auto fnc = find_symbol(handle, "load_tests"); if (fnc == nullptr) { std::println("load_tests missing"); return {}; } - auto test_sets = fnc(); - sets.insert_or_assign(path, TestSet{handle, test_sets}); - return test_sets; + 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){ - std::filesystem::path tmp_path = path; + 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 (!entry.is_regular_file()) { + continue; + } if (path.filename().string() == entry.path().stem().string()) { remove(entry.path()); } @@ -130,8 +125,8 @@ struct IncrementalRunner { 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("plain"); + compdb = CompileCommands(config.project.project_path / "compile_commands.json"); + reporter = rsl::testing::Reporter::make("json"); } void update_compdb() { @@ -204,8 +199,7 @@ struct IncrementalRunner { 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); + std::filesystem::path out_path = build_path / relative(test_path, project_path); out_path.replace_extension(".so"); out_path = weakly_canonical(out_path); @@ -269,21 +263,32 @@ struct IncrementalRunner { //! 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"); + // 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); + // 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(""); + // std::println(""); TestRoot root; for (auto&& [tu, result] : pool.collect()) { @@ -293,7 +298,7 @@ struct IncrementalRunner { 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); @@ -302,5 +307,37 @@ struct IncrementalRunner { } 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/incremental/platform/posix/watch.cpp b/src/main/incremental/platform/posix/watch.cpp index 1d00491..741f11b 100644 --- a/src/main/incremental/platform/posix/watch.cpp +++ b/src/main/incremental/platform/posix/watch.cpp @@ -18,6 +18,8 @@ #include #include "incremental/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; @@ -183,7 +185,8 @@ void Watcher::add_watch(std::filesystem::path const& dir, bool recurse) { return; } - std::println("watching {} for changes", dir.string()); + // 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); diff --git a/src/main/incremental/platform/stdin.hpp b/src/main/incremental/platform/stdin.hpp index 8aac591..6d7c4b6 100644 --- a/src/main/incremental/platform/stdin.hpp +++ b/src/main/incremental/platform/stdin.hpp @@ -20,14 +20,14 @@ class TerminalCommand { #else uintptr_t handle = 0; #endif - IncrementalRunner* runner; - Watcher* watcher; + IncrementalRunner* runner = nullptr; + Watcher* watcher = nullptr; public: - explicit TerminalCommand(IncrementalRunner& runner, Watcher& watcher) - : runner(&runner) - , watcher(&watcher) - {} +TerminalCommand() = default; + explicit TerminalCommand(IncrementalRunner& runner, Watcher& watcher) + : runner(&runner) + , watcher(&watcher) {} [[nodiscard]] uintptr_t get_handle() const { return handle; } @@ -43,14 +43,21 @@ class TerminalCommand { if (data == "exit") { std::exit(0); return; + } else if (data == "list") { + runner->list_all(); + return; + } else if (data == "run") { + runner->run_all(); + 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); + cmd = data.substr(0, it); argument = data.substr(it + 1); } else { + std::println("Missing argument or unrecognized command"); return; } @@ -58,10 +65,14 @@ class TerminalCommand { watcher->add_watch(argument); } else if (cmd == "rm_watch") { watcher->rm_watch(argument); - } else if (cmd == "run") { - // TODO - } else if (cmd == "stop_running") { + } 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"); } } }; diff --git a/src/main/incremental/platform/watch.hpp b/src/main/incremental/platform/watch.hpp index a80007b..8465306 100644 --- a/src/main/incremental/platform/watch.hpp +++ b/src/main/incremental/platform/watch.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -54,8 +56,6 @@ inline bool is_relative_to(std::filesystem::path const& path, std::filesystem::p return it_base == abs_base.end(); } - - class Watcher { WatcherImpl* impl; std::unordered_map watchers; @@ -63,7 +63,7 @@ class Watcher { std::vector pending; IncrementalRunner* runner; - std::set tests; + // std::set tests; std::unordered_map> dependencies; public: @@ -83,12 +83,12 @@ class Watcher { } 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) { @@ -96,19 +96,21 @@ class Watcher { } // TODO remove everything referring to r.source_path first - auto [it, _] = tests.insert(r.source_path); + 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 (dependency == r.source_path) { + continue; + } if (not is_relative_to(dependency, runner->config.project.project_path)) { continue; } - dependencies[dependency].insert(&*it); + dependencies[dependency].insert(&it->first); folders.insert(dependency.parent_path()); } } - + for (auto const& folder : folders) { add_watch(folder); } @@ -116,17 +118,34 @@ class Watcher { void file_modified(std::filesystem::path const& path) { auto canonical = std::filesystem::canonical(path); - if (tests.contains(canonical)) { - std::println("test modified: {}", canonical.string()); + 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(), false); + runner->recompile({path}).run(runner->reporter.get()); } else { - std::println("test dependency modified: {} {}", canonical.string(), dependencies[canonical].size()); + // std::println("test dependency modified: {} {}", canonical.string(), + // dependencies[canonical].size()); + std::vector affected; for (auto* it : dependencies[canonical]) { affected.push_back(*it); } - runner->recompile(affected).run(runner->reporter.get(), false); + std::println("{}", + nlohmann::json({ + { "action", "file_modified"}, + { "path", canonical.string()}, + {"dependency", true}, + { "affected", affected} + }) + .dump()); + runner->recompile(affected).run(runner->reporter.get()); } } diff --git a/src/main/main.cpp b/src/main/main.cpp index edefbc5..3b310d2 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -1,73 +1,92 @@ #include - -#include +#include #include #include -#include -#include #include "incremental/incremental.hpp" #include "incremental/platform/library.hpp" #include "incremental/platform/stdin.hpp" #include "incremental/platform/event_loop.hpp" +#include "output.hpp" + #include #include -#include -#include #include -void handler(int sig) { - void *bt[20]; - int n = backtrace(bt, 20); - backtrace_symbols_fd(bt, n, STDERR_FILENO); - _exit(1); +void sigbus_handler(int sig) { + void* bt[20]; + int n = backtrace(bt, 20); + backtrace_symbols_fd(bt, n, STDERR_FILENO); + _exit(1); } -int main() { - signal(SIGBUS, handler); +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; - using namespace rsl::testing::_impl_main; - constexpr bool incremental = true; - constexpr bool watch_dependencies = true; +public: + [[= 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; - const auto executable_path = std::filesystem::canonical("/proc/self/exe").parent_path(); - const std::filesystem::path config_path = executable_path / "test-runner.json"; + [[ = 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); + } - auto runner = IncrementalRunner(config_path); - auto test_inputs = runner.discover_tests(); - - std::unique_ptr selected_reporter; - selected_reporter = rsl::testing::Reporter::make("plain"); + [[= option]] void verbosity(std::string level) {} - auto root = runner.recompile(test_inputs); - runner.update_compdb(); + explicit CLI() : tree(rsl::testing::get_tests()), _output(new rsl::testing::ConsoleOutput()) {} - root.run(runner.reporter.get()); + void apply_filter() {} - - if (incremental) { - Watcher watch{runner}; - for (auto const& path : runner.config.project.test_path) { - watch.add_watch(path); + 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); } - if (watch_dependencies) { - watch.update_dependencies(); + 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); + } } - // auto watch_fnc = [&](auto path, FileEvent event) { - // if ((event & FileEvent::MODIFY) == FileEvent::MODIFY) { - // // compile TU - // runner.recompile(runner.expand_tests({path})); - // // load TU - // auto updated = update_tree(path); - // // updated.run(selected_reporter.get(), false); - // } - // }; - TerminalCommand commands{runner, watch}; - - auto loop = EventLoop(commands, watch); + } +}; + +int main(int argc, char** argv) { + signal(SIGBUS, sigbus_handler); + + 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); + // runner.update_compdb(); + + 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(); } -} \ No newline at end of file + + // if (watch_dependencies) { + // watch.update_dependencies(); + // } +} diff --git a/src/main/reporters/json.cpp b/src/main/reporters/json.cpp index 29ccb08..966ca73 100644 --- a/src/main/reporters/json.cpp +++ b/src/main/reporters/json.cpp @@ -3,7 +3,20 @@ #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/runner.cpp b/src/runner.cpp index af02ace..44db617 100644 --- a/src/runner.cpp +++ b/src/runner.cpp @@ -96,9 +96,7 @@ bool TestRoot::run(Reporter* reporter, bool summarize) { bool status = TestNamespace::run(reporter); libassert::set_failure_handler(libassert::default_failure_handler); // TODO after_run - if (summarize) { - reporter->after_run(); - } + reporter->after_run(); return status; } From 3af976fd6b7b784bdc778b56b2f5c217a15d444b Mon Sep 17 00:00:00 2001 From: Tsche Date: Sun, 30 Nov 2025 13:41:02 +0000 Subject: [PATCH 12/15] reorganize --- src/main/CMakeLists.txt | 2 +- src/main/{incremental => }/compdb.hpp | 0 src/main/{incremental => }/compile_pool.hpp | 0 src/main/{incremental => }/config_parser.hpp | 0 src/main/{incremental => }/incremental.hpp | 0 src/main/incremental/CMakeLists.txt | 1 - src/main/main.cpp | 8 ++++---- src/main/{incremental => }/platform/CMakeLists.txt | 0 src/main/{incremental => }/platform/event_loop.hpp | 0 src/main/{incremental => }/platform/library.hpp | 0 src/main/{incremental => }/platform/posix/event_loop.cpp | 1 - src/main/{incremental => }/platform/posix/library.cpp | 0 src/main/{incremental => }/platform/posix/taskset.cpp | 0 src/main/{incremental => }/platform/posix/watch.cpp | 2 +- src/main/{incremental => }/platform/stdin.hpp | 0 src/main/{incremental => }/platform/taskset.hpp | 0 src/main/{incremental => }/platform/watch.hpp | 0 src/main/{incremental => }/runner.cpp | 0 18 files changed, 6 insertions(+), 8 deletions(-) rename src/main/{incremental => }/compdb.hpp (100%) rename src/main/{incremental => }/compile_pool.hpp (100%) rename src/main/{incremental => }/config_parser.hpp (100%) rename src/main/{incremental => }/incremental.hpp (100%) delete mode 100644 src/main/incremental/CMakeLists.txt rename src/main/{incremental => }/platform/CMakeLists.txt (100%) rename src/main/{incremental => }/platform/event_loop.hpp (100%) rename src/main/{incremental => }/platform/library.hpp (100%) rename src/main/{incremental => }/platform/posix/event_loop.cpp (99%) rename src/main/{incremental => }/platform/posix/library.cpp (100%) rename src/main/{incremental => }/platform/posix/taskset.cpp (100%) rename src/main/{incremental => }/platform/posix/watch.cpp (99%) rename src/main/{incremental => }/platform/stdin.hpp (100%) rename src/main/{incremental => }/platform/taskset.hpp (100%) rename src/main/{incremental => }/platform/watch.hpp (100%) rename src/main/{incremental => }/runner.cpp (100%) diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index c2288e1..3e733d2 100644 --- a/src/main/CMakeLists.txt +++ b/src/main/CMakeLists.txt @@ -2,4 +2,4 @@ target_sources(rsltest_main PUBLIC main.cpp) target_include_directories(rsltest_main PRIVATE .) add_subdirectory(reporters) -add_subdirectory(incremental) \ No newline at end of file +add_subdirectory(platform) \ No newline at end of file diff --git a/src/main/incremental/compdb.hpp b/src/main/compdb.hpp similarity index 100% rename from src/main/incremental/compdb.hpp rename to src/main/compdb.hpp diff --git a/src/main/incremental/compile_pool.hpp b/src/main/compile_pool.hpp similarity index 100% rename from src/main/incremental/compile_pool.hpp rename to src/main/compile_pool.hpp diff --git a/src/main/incremental/config_parser.hpp b/src/main/config_parser.hpp similarity index 100% rename from src/main/incremental/config_parser.hpp rename to src/main/config_parser.hpp diff --git a/src/main/incremental/incremental.hpp b/src/main/incremental.hpp similarity index 100% rename from src/main/incremental/incremental.hpp rename to src/main/incremental.hpp diff --git a/src/main/incremental/CMakeLists.txt b/src/main/incremental/CMakeLists.txt deleted file mode 100644 index 2600b3c..0000000 --- a/src/main/incremental/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -add_subdirectory(platform) \ No newline at end of file diff --git a/src/main/main.cpp b/src/main/main.cpp index 3b310d2..353c3ec 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -4,10 +4,10 @@ #include #include -#include "incremental/incremental.hpp" -#include "incremental/platform/library.hpp" -#include "incremental/platform/stdin.hpp" -#include "incremental/platform/event_loop.hpp" +#include "incremental.hpp" +#include "platform/library.hpp" +#include "platform/stdin.hpp" +#include "platform/event_loop.hpp" #include "output.hpp" diff --git a/src/main/incremental/platform/CMakeLists.txt b/src/main/platform/CMakeLists.txt similarity index 100% rename from src/main/incremental/platform/CMakeLists.txt rename to src/main/platform/CMakeLists.txt diff --git a/src/main/incremental/platform/event_loop.hpp b/src/main/platform/event_loop.hpp similarity index 100% rename from src/main/incremental/platform/event_loop.hpp rename to src/main/platform/event_loop.hpp diff --git a/src/main/incremental/platform/library.hpp b/src/main/platform/library.hpp similarity index 100% rename from src/main/incremental/platform/library.hpp rename to src/main/platform/library.hpp diff --git a/src/main/incremental/platform/posix/event_loop.cpp b/src/main/platform/posix/event_loop.cpp similarity index 99% rename from src/main/incremental/platform/posix/event_loop.cpp rename to src/main/platform/posix/event_loop.cpp index 324d93b..3d9924a 100644 --- a/src/main/incremental/platform/posix/event_loop.cpp +++ b/src/main/platform/posix/event_loop.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include namespace { diff --git a/src/main/incremental/platform/posix/library.cpp b/src/main/platform/posix/library.cpp similarity index 100% rename from src/main/incremental/platform/posix/library.cpp rename to src/main/platform/posix/library.cpp diff --git a/src/main/incremental/platform/posix/taskset.cpp b/src/main/platform/posix/taskset.cpp similarity index 100% rename from src/main/incremental/platform/posix/taskset.cpp rename to src/main/platform/posix/taskset.cpp diff --git a/src/main/incremental/platform/posix/watch.cpp b/src/main/platform/posix/watch.cpp similarity index 99% rename from src/main/incremental/platform/posix/watch.cpp rename to src/main/platform/posix/watch.cpp index 741f11b..18f35f0 100644 --- a/src/main/incremental/platform/posix/watch.cpp +++ b/src/main/platform/posix/watch.cpp @@ -16,7 +16,7 @@ #include #include #include -#include "incremental/incremental.hpp" +#include "incremental.hpp" #include diff --git a/src/main/incremental/platform/stdin.hpp b/src/main/platform/stdin.hpp similarity index 100% rename from src/main/incremental/platform/stdin.hpp rename to src/main/platform/stdin.hpp diff --git a/src/main/incremental/platform/taskset.hpp b/src/main/platform/taskset.hpp similarity index 100% rename from src/main/incremental/platform/taskset.hpp rename to src/main/platform/taskset.hpp diff --git a/src/main/incremental/platform/watch.hpp b/src/main/platform/watch.hpp similarity index 100% rename from src/main/incremental/platform/watch.hpp rename to src/main/platform/watch.hpp diff --git a/src/main/incremental/runner.cpp b/src/main/runner.cpp similarity index 100% rename from src/main/incremental/runner.cpp rename to src/main/runner.cpp From 94c917ce3d1bfb1b58338a0ed55a9d46513649a0 Mon Sep 17 00:00:00 2001 From: Tsche Date: Sun, 30 Nov 2025 13:43:55 +0000 Subject: [PATCH 13/15] cleanup --- src/main/compile_pool.hpp | 2 -- src/main/config_parser.hpp | 2 -- src/main/incremental.hpp | 5 ++--- src/main/platform/event_loop.hpp | 4 ++-- src/main/platform/taskset.hpp | 1 - src/main/platform/watch.hpp | 1 - 6 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/main/compile_pool.hpp b/src/main/compile_pool.hpp index b07b8b3..b924661 100644 --- a/src/main/compile_pool.hpp +++ b/src/main/compile_pool.hpp @@ -1,9 +1,7 @@ #pragma once -#include #include #include #include -#include #include #include #include diff --git a/src/main/config_parser.hpp b/src/main/config_parser.hpp index a5b5f0c..082fd87 100644 --- a/src/main/config_parser.hpp +++ b/src/main/config_parser.hpp @@ -1,10 +1,8 @@ #pragma once #include -#include #include #include #include -#include #include #include diff --git a/src/main/incremental.hpp b/src/main/incremental.hpp index d45e046..a3fbcd4 100644 --- a/src/main/incremental.hpp +++ b/src/main/incremental.hpp @@ -1,6 +1,5 @@ #pragma once #include -#include #include #include #include @@ -43,11 +42,11 @@ struct TestSet { return *this; } - // ~TestSet() { unload(); } + ~TestSet() { unload(); } void unload() { if (handle != nullptr) { - // unload_library(handle); + unload_library(handle); tests = {}; } } diff --git a/src/main/platform/event_loop.hpp b/src/main/platform/event_loop.hpp index f501704..243f6d5 100644 --- a/src/main/platform/event_loop.hpp +++ b/src/main/platform/event_loop.hpp @@ -1,8 +1,8 @@ #pragma once +#include +#include #include #include -#include -#include #include namespace rsl::testing::_impl_main { diff --git a/src/main/platform/taskset.hpp b/src/main/platform/taskset.hpp index 09140ee..6801a30 100644 --- a/src/main/platform/taskset.hpp +++ b/src/main/platform/taskset.hpp @@ -1,5 +1,4 @@ #pragma once -#include #include #include #include diff --git a/src/main/platform/watch.hpp b/src/main/platform/watch.hpp index 8465306..1ff20e3 100644 --- a/src/main/platform/watch.hpp +++ b/src/main/platform/watch.hpp @@ -2,7 +2,6 @@ #include -#include #include #include #include From c3eee35dc593007f0d35c0463f7d99524b01b6c9 Mon Sep 17 00:00:00 2001 From: Tsche Date: Tue, 9 Dec 2025 21:44:58 +0000 Subject: [PATCH 14/15] first set of changes for tests as applications --- CMakeLists.txt | 7 +- cmake/rsl-test.cmake | 15 +- example/always_passes.cpp | 2 + include/rsl/testing/_testing_impl/capture.hpp | 111 ++++++++ .../rsl/testing/_testing_impl/discovery.hpp | 23 +- include/rsl/testing/all.hpp | 3 +- include/rsl/testing/assert.hpp | 61 +++-- include/rsl/testing/{ => ext}/output.hpp | 6 +- include/rsl/testing/test.hpp | 226 ++++++++++++++++- src/CMakeLists.txt | 2 - src/capture.cpp | 85 ------- src/capture.hpp | 31 --- src/main/config_parser.hpp | 45 ++++ src/main/incremental.hpp | 56 +---- src/main/main.cpp | 1 - src/main/platform/stdin.hpp | 3 + src/runner.cpp | 236 ++++-------------- src/test.cpp | 135 ---------- 18 files changed, 498 insertions(+), 550 deletions(-) create mode 100644 include/rsl/testing/_testing_impl/capture.hpp rename include/rsl/testing/{ => ext}/output.hpp (89%) delete mode 100644 src/capture.cpp delete mode 100644 src/capture.hpp delete mode 100644 src/test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e5f0250..c8fbab3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,15 +14,14 @@ target_compile_options(rsltest PUBLIC "-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) +# find_package(libassert REQUIRED) +# target_link_libraries(rsltest PUBLIC libassert::assert) target_link_libraries(rsltest_main PUBLIC rsltest) find_package(rsl-util REQUIRED) @@ -31,7 +30,7 @@ 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 rsl::config) target_link_libraries(rsltest_main PUBLIC rsl::xml) target_link_libraries(rsltest_main PUBLIC nlohmann_json::nlohmann_json) diff --git a/cmake/rsl-test.cmake b/cmake/rsl-test.cmake index dcb3bfd..87fa2dc 100644 --- a/cmake/rsl-test.cmake +++ b/cmake/rsl-test.cmake @@ -211,9 +211,18 @@ function(target_enable_tests _TEST_ARG_TARGET) target_link_libraries(_deps_target INTERFACE ${_TEST_ARG_TARGET}) target_link_libraries(_deps_target INTERFACE rsltest) - # _impl_property(_dep_link_libs INTERFACE_LINK_LIBRARIES TARGET _dep_target) - # _impl_property(_dep_includes INTERFACE_INCLUDE_DIRECTORIES TARGET _dep_target) - # _impl_property(_dep_defines INTERFACE_COMPILE_DEFINITIONS TARGET _dep_target) + 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) diff --git a/example/always_passes.cpp b/example/always_passes.cpp index 4bd43cc..3006da3 100644 --- a/example/always_passes.cpp +++ b/example/always_passes.cpp @@ -24,3 +24,5 @@ auto zoinks(bool zoinks) { zoinks(foo()); } } // namespace demo + +int main(){} \ No newline at end of file diff --git a/include/rsl/testing/_testing_impl/capture.hpp b/include/rsl/testing/_testing_impl/capture.hpp new file mode 100644 index 0000000..1e17c7c --- /dev/null +++ b/include/rsl/testing/_testing_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/_testing_impl/discovery.hpp b/include/rsl/testing/_testing_impl/discovery.hpp index 391d63c..2487cbb 100644 --- a/include/rsl/testing/_testing_impl/discovery.hpp +++ b/include/rsl/testing/_testing_impl/discovery.hpp @@ -56,15 +56,19 @@ struct TestDiscovery { std::meta::access_context ctx = std::meta::access_context::current(); consteval void handle_member(std::meta::info R) { - if (!has_identifier(R)) { return; } - + if (!has_identifier(R)) { + return; + } + auto identifier = identifier_of(R); - if (identifier[0] == '_') { return; } - + if (identifier[0] == '_') { + return; + } + if (!(is_function(R) || is_variable(R) || (is_complete_type(R) && is_class_type(R)))) { return; } - + if (identifier.starts_with("test_")) { if (is_complete_type(R) && is_class_type(R)) { tests.append_range(expand_class(R)); @@ -126,9 +130,8 @@ struct TestDiscovery { return discovery.tests; } }; -std::set& registry(); -inline std::set& local_registry() { +inline std::set& registry() { static std::set reg; return reg; } @@ -137,11 +140,7 @@ template bool enable_tests() { constexpr auto tests = define_static_array(_testing_impl::TestDiscovery::find_tests(NS)); for (auto const& test : tests) { -#ifdef RSL_TEST_UNIT - local_registry().insert(test); -#else - _testing_impl::registry().insert(test); -#endif + registry().insert(test); } return true; } diff --git a/include/rsl/testing/all.hpp b/include/rsl/testing/all.hpp index 6532375..4023b91 100644 --- a/include/rsl/testing/all.hpp +++ b/include/rsl/testing/all.hpp @@ -67,12 +67,13 @@ struct Anchor { #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::local_registry(); + return &rsl::testing::_testing_impl::registry(); } #endif \ No newline at end of file diff --git a/include/rsl/testing/assert.hpp b/include/rsl/testing/assert.hpp index fedeecb..ebaa9df 100644 --- a/include/rsl/testing/assert.hpp +++ b/include/rsl/testing/assert.hpp @@ -1,7 +1,9 @@ #pragma once #include #include +#include #include +#include #include @@ -21,30 +23,43 @@ struct AssertionInfo { std::string_view expanded; bool success; }; + namespace _testing_impl { -struct AssertionTracker; -AssertionTracker& assertion_counter(); -void track_assertion(AssertionInfo info); +struct AssertionTracker { + std::vector assertions; + std::string test_name; +}; + +AssertionTracker& assertion_counter() { + static AssertionTracker counter{}; + return counter; +} + +void track_assertion(AssertionInfo info) { + assertion_counter().assertions.emplace_back(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::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 +#ifdef RSL_TEST_UNIT +// #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 +#endif \ No newline at end of file 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..ebe3954 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 "../_testing_impl/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/test.hpp b/include/rsl/testing/test.hpp index 2891da5..6f9b05d 100644 --- a/include/rsl/testing/test.hpp +++ b/include/rsl/testing/test.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "result.hpp" @@ -12,14 +14,68 @@ #include #include +#include namespace rsl::testing { +namespace _impl { + void run_test(void const* test) { + (*static_cast const*>(test))(); +} +} + struct TestCase { class Test const* test; std::function fnc; std::string name; - [[nodiscard]] Result run() const; + [[nodiscard]] Result 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(_impl::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; + } }; struct FuzzTarget { @@ -93,7 +149,14 @@ struct TestNamespace { single_iterator current; std::deque elements; - void flatten(TestNamespace const& current); + 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; @@ -103,30 +166,169 @@ struct TestNamespace { using reference = Test const&; iterator() = default; - explicit iterator(TestNamespace const& ns); + 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++(); - bool operator==(iterator const& other) const; + 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); - void remove_by_path(std::string_view path); + 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)}); - [[nodiscard]] std::size_t count() const; - bool run(Reporter* reporter); + // TODO stringify skipped tests properly + auto result = Result{&test, std::string(test.name) + "(...)", TestOutcome::SKIP}; + reporter->after_test(result); + results.push_back(result); + } - void filter(std::span parts); + reporter->after_test_group(results); + } + if (!name.empty()) { + reporter->exit_namespace(name); + } + return status; + } }; struct TestRoot : TestNamespace { - bool run(Reporter* reporter, bool summarize = true); + 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 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/CMakeLists.txt b/src/CMakeLists.txt index e0e8c56..953f7f4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,4 @@ target_sources(rsltest PUBLIC - capture.cpp - test.cpp runner.cpp ) 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/config_parser.hpp b/src/main/config_parser.hpp index 082fd87..6d3fa7d 100644 --- a/src/main/config_parser.hpp +++ b/src/main/config_parser.hpp @@ -99,6 +99,51 @@ struct RunnerConfig { 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("{}", lib.string())); + } else { + cmd.push_back(std::format("-l{}", lib.string())); + } + } + + // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); + // cmd.push_back(std::format("-L{}", build_path.string())); + cmd.emplace_back("-fPIC"); + cmd.emplace_back("-shared"); + return cmd; + } }; inline void to_json(nlohmann::json& doc, RunnerConfig const& p) { diff --git a/src/main/incremental.hpp b/src/main/incremental.hpp index a3fbcd4..0573f0a 100644 --- a/src/main/incremental.hpp +++ b/src/main/incremental.hpp @@ -115,6 +115,7 @@ struct IncrementalRunner { 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"}; @@ -131,7 +132,7 @@ struct IncrementalRunner { void update_compdb() { compdb.load(); for (auto const& [path, unit] : units) { - auto tu = make_invocation("default", path, false, false); + 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, @@ -140,58 +141,11 @@ struct IncrementalRunner { compdb.save(); } - std::vector expand_options(std::string_view config_name) const { - const auto& options = config.options; - const auto& cfg = config.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 = config.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 : config.options.link_libraries) { - if (lib.is_absolute()) { - cmd.push_back(std::format("-L{}", lib.string())); - } else { - cmd.push_back(std::format("-l{}", lib.string())); - } - } - - // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); - // cmd.push_back(std::format("-L{}", build_path.string())); - cmd.emplace_back("-fPIC"); - cmd.emplace_back("-shared"); - return cmd; - } - - mutable size_t counter = 0; [[nodiscard]] TestTU make_invocation(std::string_view config_name, std::filesystem::path const& test_path, bool dump_dependencies = false, - bool link = true) const { + bool link = true) { const std::filesystem::path build_path = config.project.build_path; const std::filesystem::path project_path = config.project.project_path; @@ -203,10 +157,10 @@ struct IncrementalRunner { out_path = weakly_canonical(out_path); std::vector cmd = {compiler_path}; - cmd.append_range(expand_options(config_name)); + cmd.append_range(config.expand_options(config_name)); if (link && not dump_dependencies) { - cmd.append_range(expand_link_options()); + cmd.append_range(config.expand_link_options()); // out file cmd.emplace_back("-o"); diff --git a/src/main/main.cpp b/src/main/main.cpp index 353c3ec..49e02d7 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -74,7 +74,6 @@ int main(int argc, char** argv) { const std::filesystem::path config_path = executable_path / "test-runner.json"; auto runner = IncrementalRunner(config_path); - // runner.update_compdb(); auto args = CLI(); args.parse_args(argc, argv); diff --git a/src/main/platform/stdin.hpp b/src/main/platform/stdin.hpp index 6d7c4b6..5bf7053 100644 --- a/src/main/platform/stdin.hpp +++ b/src/main/platform/stdin.hpp @@ -49,6 +49,9 @@ TerminalCommand() = default; } 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; diff --git a/src/runner.cpp b/src/runner.cpp index 44db617..d3a4772 100644 --- a/src/runner.cpp +++ b/src/runner.cpp @@ -12,161 +12,71 @@ #include #include -#include -#include +// #include +// #include #include "capture.hpp" #include -namespace rsl::testing::_testing_impl { -struct AssertionTracker { - std::vector assertions; - std::string test_name; -}; - -AssertionTracker& assertion_counter() { - static AssertionTracker counter{}; - return counter; -} - -void track_assertion(AssertionInfo info) { - assertion_counter().assertions.emplace_back(info); -} -} // namespace rsl::testing::_testing_impl - 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); +// 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)); +// } - 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); - } - } -} namespace rsl::testing { -void Reporter::list_tests(TestNamespace const& tests) { - print_tests(tests); -} - -bool TestRoot::run(Reporter* reporter, bool summarize) { - 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) { @@ -176,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 8646d79..0000000 --- a/src/test.cpp +++ /dev/null @@ -1,135 +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; -} -} // 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); -} - -void TestNamespace::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); -} - -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 From 962495bf9d89d816b8db91057eb59a4df244ae3e Mon Sep 17 00:00:00 2001 From: Tsche Date: Thu, 18 Dec 2025 16:35:24 +0100 Subject: [PATCH 15/15] refactor for self-contained messages --- CMakeLists.txt | 69 +++-- cmake/rsl-test.cmake | 20 +- example/always_passes.cpp | 6 +- .../rsl/testing/_testing_impl/discovery.hpp | 5 +- include/rsl/testing/_testing_impl/expand.hpp | 20 +- include/rsl/testing/all.hpp | 2 +- include/rsl/testing/annotations.hpp | 16 +- include/rsl/testing/assert.hpp | 64 ++--- .../{_testing_impl => ext}/factory.hpp | 0 include/rsl/testing/ext/output.hpp | 2 +- .../_unit_impl}/capture.hpp | 0 .../{ => runner/_unit_impl}/result.hpp | 9 +- include/rsl/testing/runner/unit.cpp | 174 +++++++++++ include/rsl/testing/test.hpp | 270 +----------------- src/CMakeLists.txt | 8 +- src/main/CMakeLists.txt | 2 +- src/main/config_parser.hpp | 6 +- src/main/incremental.hpp | 2 +- src/main/main.cpp | 2 +- src/main/old_main.cpp | 2 +- src/main/output.hpp | 2 +- src/main/reporters/catch2xml.cpp | 2 +- src/main/reporters/json.cpp | 2 +- src/main/reporters/terminal.cpp | 2 +- src/main/reporters/xml.cpp | 2 +- src/runner.cpp | 2 +- src/test_namespace.hpp | 201 +++++++++++++ 27 files changed, 493 insertions(+), 399 deletions(-) rename include/rsl/testing/{_testing_impl => ext}/factory.hpp (100%) rename include/rsl/testing/{_testing_impl => runner/_unit_impl}/capture.hpp (100%) rename include/rsl/testing/{ => runner/_unit_impl}/result.hpp (82%) create mode 100644 include/rsl/testing/runner/unit.cpp create mode 100644 src/test_namespace.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c8fbab3..21936e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,41 +5,72 @@ 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 ) -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_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") include(${CMAKE_SOURCE_DIR}/cmake/rsl-test.cmake) @@ -56,11 +87,13 @@ if (BUILD_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) +########################## +## Coverage hooks ## +########################## +# TODO +# add_subdirectory(ext) + # if (BUILD_EXAMPLES) # add_executable(example_test) # add_subdirectory(example) diff --git a/cmake/rsl-test.cmake b/cmake/rsl-test.cmake index 87fa2dc..b8bd905 100644 --- a/cmake/rsl-test.cmake +++ b/cmake/rsl-test.cmake @@ -122,11 +122,12 @@ function(collect_interface_usage INTERFACE_TARGET OUT_INCLUDES OUT_DEFS OUT_OPTI # message("${dep} => ${dep_fixed}") if(dep_fixed AND TARGET ${dep_fixed}) # Recursive call, passing the same visited list - get_target_property(TYPE ${dep_fixed} dep_type) + 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() @@ -211,14 +212,15 @@ function(target_enable_tests _TEST_ARG_TARGET) target_link_libraries(_deps_target INTERFACE ${_TEST_ARG_TARGET}) target_link_libraries(_deps_target INTERFACE rsltest) - 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) + # 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) diff --git a/example/always_passes.cpp b/example/always_passes.cpp index 3006da3..2b4394e 100644 --- a/example/always_passes.cpp +++ b/example/always_passes.cpp @@ -5,7 +5,6 @@ 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"); @@ -13,6 +12,7 @@ auto zoinks(bool zoinks) { } else { x = false; } + ASSERT(zoinks == false); return x; } @@ -23,6 +23,4 @@ auto zoinks(bool zoinks) { // zoinks(true); zoinks(foo()); } -} // namespace demo - -int main(){} \ No newline at end of file +} // namespace demo \ 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 2487cbb..ad06be0 100644 --- a/include/rsl/testing/_testing_impl/discovery.hpp +++ b/include/rsl/testing/_testing_impl/discovery.hpp @@ -131,10 +131,7 @@ struct TestDiscovery { } }; -inline std::set& registry() { - static std::set reg; - return reg; -} +std::set& registry(); template bool enable_tests() { 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 4023b91..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; 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 ebaa9df..03dab35 100644 --- a/include/rsl/testing/assert.hpp +++ b/include/rsl/testing/assert.hpp @@ -3,19 +3,19 @@ #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 { @@ -25,41 +25,27 @@ struct AssertionInfo { }; namespace _testing_impl { -struct AssertionTracker { - std::vector assertions; - std::string test_name; -}; - -AssertionTracker& assertion_counter() { - static AssertionTracker counter{}; - return counter; -} - -void track_assertion(AssertionInfo info) { - assertion_counter().assertions.emplace_back(info); -} +void track_assertion(AssertionInfo info); } // namespace _testing_impl } // namespace rsl::testing -#ifdef RSL_TEST_UNIT -// #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 -#endif \ No newline at end of file +#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/ext/output.hpp b/include/rsl/testing/ext/output.hpp index ebe3954..10880f4 100644 --- a/include/rsl/testing/ext/output.hpp +++ b/include/rsl/testing/ext/output.hpp @@ -5,7 +5,7 @@ #include #include "../test.hpp" -#include "../_testing_impl/factory.hpp" +#include "factory.hpp" namespace rsl::testing { struct Output { diff --git a/include/rsl/testing/_testing_impl/capture.hpp b/include/rsl/testing/runner/_unit_impl/capture.hpp similarity index 100% rename from include/rsl/testing/_testing_impl/capture.hpp rename to include/rsl/testing/runner/_unit_impl/capture.hpp 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 6f9b05d..cb12288 100644 --- a/include/rsl/testing/test.hpp +++ b/include/rsl/testing/test.hpp @@ -6,83 +6,19 @@ #include #include #include - -#include "result.hpp" +#include #include "_testing_impl/util.hpp" #include "_testing_impl/expand.hpp" #include #include -#include namespace rsl::testing { -namespace _impl { - void run_test(void const* test) { - (*static_cast const*>(test))(); -} -} - struct TestCase { class Test const* test; std::function fnc; std::string name; - - [[nodiscard]] Result 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(_impl::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; - } -}; - -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 { @@ -103,7 +39,6 @@ class Test { 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) @@ -113,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)})); @@ -129,206 +63,4 @@ class Test { }; using TestDef = Test (*)(std::string const&); - -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; -} - } // namespace rsl::testing \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 953f7f4..5714066 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,4 @@ -target_sources(rsltest PUBLIC - runner.cpp -) +# add_subdirectory(main) +# add_subdirectory(coverage) -add_subdirectory(main) -add_subdirectory(coverage) +target_sources(rsltest_main PUBLIC ../include/rsl/testing/unit.cpp) \ No newline at end of file diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index 3e733d2..aaa983e 100644 --- a/src/main/CMakeLists.txt +++ b/src/main/CMakeLists.txt @@ -1,5 +1,5 @@ target_sources(rsltest_main PUBLIC main.cpp) target_include_directories(rsltest_main PRIVATE .) -add_subdirectory(reporters) +# add_subdirectory(reporters) add_subdirectory(platform) \ No newline at end of file diff --git a/src/main/config_parser.hpp b/src/main/config_parser.hpp index 6d3fa7d..cb17333 100644 --- a/src/main/config_parser.hpp +++ b/src/main/config_parser.hpp @@ -132,16 +132,12 @@ struct RunnerConfig { 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())); } } - - // cmd.push_back(std::format("-Wl,-rpath,{}", build_path.string())); - // cmd.push_back(std::format("-L{}", build_path.string())); - cmd.emplace_back("-fPIC"); - cmd.emplace_back("-shared"); return cmd; } }; diff --git a/src/main/incremental.hpp b/src/main/incremental.hpp index 0573f0a..5e2a7f9 100644 --- a/src/main/incremental.hpp +++ b/src/main/incremental.hpp @@ -12,7 +12,7 @@ #include "compdb.hpp" #include "platform/library.hpp" -#include +#include #include namespace rsl::testing::_impl_main { diff --git a/src/main/main.cpp b/src/main/main.cpp index 49e02d7..b59d3e8 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include "incremental.hpp" #include "platform/library.hpp" diff --git a/src/main/old_main.cpp b/src/main/old_main.cpp index 432153f..9bbd6b3 100644 --- a/src/main/old_main.cpp +++ b/src/main/old_main.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include #include 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/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 966ca73..f130232 100644 --- a/src/main/reporters/json.cpp +++ b/src/main/reporters/json.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include "rsl/testing/result.hpp" diff --git a/src/main/reporters/terminal.cpp b/src/main/reporters/terminal.cpp index a9cc133..f42db4a 100644 --- a/src/main/reporters/terminal.cpp +++ b/src/main/reporters/terminal.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include 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/runner.cpp b/src/runner.cpp index d3a4772..18040df 100644 --- a/src/runner.cpp +++ b/src/runner.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include // #include 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