diff --git a/contrib/kyua/cli/cmd_debug.cpp b/contrib/kyua/cli/cmd_debug.cpp --- a/contrib/kyua/cli/cmd_debug.cpp +++ b/contrib/kyua/cli/cmd_debug.cpp @@ -29,6 +29,7 @@ #include "cli/cmd_debug.hpp" #include +#include #include "cli/common.ipp" #include "drivers/debug_test.hpp" @@ -38,13 +39,76 @@ #include "utils/cmdline/parser.ipp" #include "utils/cmdline/ui.hpp" #include "utils/format/macros.hpp" +#include "utils/process/executor.hpp" namespace cmdline = utils::cmdline; namespace config = utils::config; +namespace executor = utils::process::executor; using cli::cmd_debug; +namespace { + + +const cmdline::bool_option pause_before_cleanup_upon_fail_option( + 'p', + "pause-before-cleanup-upon-fail", + "Pauses right before the test cleanup upon fail"); + + +const cmdline::bool_option pause_before_cleanup_option( + "pause-before-cleanup", + "Pauses right before the test cleanup"); + + +/// The debugger interface implementation. +class dbg : public engine::debugger { + /// Object to interact with the I/O of the program. + cmdline::ui* _ui; + + /// Representation of the command line to the subcommand. + const cmdline::parsed_cmdline& _cmdline; + +public: + /// Constructor. + /// + /// \param ui_ Object to interact with the I/O of the program. + /// \param cmdline Representation of the command line to the subcommand. + dbg(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline) : + _ui(ui), _cmdline(cmdline) + {} + + void before_cleanup( + const model::test_program_ptr&, + const model::test_case&, + optional< model::test_result >& result, + executor::exit_handle& eh) const + { + if (_cmdline.has_option(pause_before_cleanup_upon_fail_option + .long_name())) { + if (result && !result.get().good()) { + _ui->out("The test failed and paused right before its cleanup " + "routine."); + _ui->out(F("Test work dir: %s") % eh.work_directory().str()); + _ui->out("Press any key to continue..."); + (void) std::cin.get(); + } + } else if (_cmdline.has_option(pause_before_cleanup_option + .long_name())) { + _ui->out("The test paused right before its cleanup routine."); + _ui->out(F("Test work dir: %s") % eh.work_directory().str()); + _ui->out("Press any key to continue..."); + (void) std::cin.get(); + } + }; + +}; + + +} // anonymous namespace + + /// Default constructor for cmd_debug. cmd_debug::cmd_debug(void) : cli_command( "debug", "test_case", 1, 1, @@ -53,6 +117,9 @@ add_option(build_root_option); add_option(kyuafile_option); + add_option(pause_before_cleanup_upon_fail_option); + add_option(pause_before_cleanup_option); + add_option(cmdline::path_option( "stdout", "Where to direct the standard output of the test case", "path", "/dev/stdout")); @@ -82,7 +149,10 @@ const engine::test_filter filter = engine::test_filter::parse( test_case_name); + auto debugger = std::shared_ptr< engine::debugger >(new dbg(ui, cmdline)); + const drivers::debug_test::result result = drivers::debug_test::drive( + debugger, kyuafile_path(cmdline), build_root_path(cmdline), filter, user_config, cmdline.get_option< cmdline::path_option >("stdout"), cmdline.get_option< cmdline::path_option >("stderr")); diff --git a/contrib/kyua/doc/kyua-debug.1.in b/contrib/kyua/doc/kyua-debug.1.in --- a/contrib/kyua/doc/kyua-debug.1.in +++ b/contrib/kyua/doc/kyua-debug.1.in @@ -25,7 +25,7 @@ .\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE .\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -.Dd October 13, 2014 +.Dd March 25, 2025 .Dt KYUA-DEBUG 1 .Os .Sh NAME @@ -35,6 +35,8 @@ .Nm .Op Fl -build-root Ar path .Op Fl -kyuafile Ar file +.Op Fl -pause-before-cleanup-upon-fail +.Op Fl -pause-before-cleanup .Op Fl -stdout Ar path .Op Fl -stderr Ar path .Ar test_case @@ -84,6 +86,23 @@ Defaults to .Pa Kyuafile file in the current directory. +.It Fl -pause-before-cleanup-upon-fail , Fl p +Causes Kyua to pause right before the test cleanup routine is invoked if the +test fails. +When paused, Kyua waits for input on stdin, and any key press resumes +execution. +.sp +This can be useful for debugging system or end-to-end tests +which alter the system by creating artifacts such as files, network +interfaces, routing entries, firewall configuration, containers, etc. +This flag makes it possible to preserve such artifacts immediately after a test +failure, simplifying debugging. +Otherwise, these artifacts would be removed by the test cleanup routine or +by Kyua built-in cleanup mechanism. +.It Fl -pause-before-cleanup +The unconditional variant of the previous option. +This can be helpful for reproducing the infrastructure or fixture of a passing +test for further development or additional analysis. .It Fl -stderr Ar path Specifies the file to which to send the standard error of the test program's body. diff --git a/contrib/kyua/drivers/debug_test.hpp b/contrib/kyua/drivers/debug_test.hpp --- a/contrib/kyua/drivers/debug_test.hpp +++ b/contrib/kyua/drivers/debug_test.hpp @@ -36,12 +36,15 @@ #if !defined(DRIVERS_DEBUG_TEST_HPP) #define DRIVERS_DEBUG_TEST_HPP +#include "engine/debugger.hpp" #include "engine/filters.hpp" #include "model/test_result.hpp" #include "utils/config/tree_fwd.hpp" #include "utils/fs/path_fwd.hpp" #include "utils/optional_fwd.hpp" +using engine::debugger; + namespace drivers { namespace debug_test { @@ -68,7 +71,8 @@ }; -result drive(const utils::fs::path&, const utils::optional< utils::fs::path >, +result drive(std::shared_ptr< debugger >, + const utils::fs::path&, const utils::optional< utils::fs::path >, const engine::test_filter&, const utils::config::tree&, const utils::fs::path&, const utils::fs::path&); diff --git a/contrib/kyua/drivers/debug_test.cpp b/contrib/kyua/drivers/debug_test.cpp --- a/contrib/kyua/drivers/debug_test.cpp +++ b/contrib/kyua/drivers/debug_test.cpp @@ -63,7 +63,8 @@ /// /// \returns A structure with all results computed by this driver. drivers::debug_test::result -drivers::debug_test::drive(const fs::path& kyuafile_path, +drivers::debug_test::drive(engine::debugger_ptr debugger, + const fs::path& kyuafile_path, const optional< fs::path > build_root, const engine::test_filter& filter, const config::tree& user_config, @@ -92,6 +93,10 @@ const model::test_program_ptr test_program = match.get().first; const std::string& test_case_name = match.get().second; + const model::test_case test_case = test_program->find(test_case_name); + if (debugger) + test_case.attach_debugger(debugger); + scheduler::result_handle_ptr result_handle = handle.debug_test( test_program, test_case_name, user_config, stdout_path, stderr_path); diff --git a/contrib/kyua/drivers/debug_test.hpp b/contrib/kyua/engine/debugger.hpp copy from contrib/kyua/drivers/debug_test.hpp copy to contrib/kyua/engine/debugger.hpp --- a/contrib/kyua/drivers/debug_test.hpp +++ b/contrib/kyua/engine/debugger.hpp @@ -1,4 +1,4 @@ -// Copyright 2011 The Kyua Authors. +// Copyright 2025 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -26,54 +26,46 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -/// \file drivers/debug_test.hpp -/// Driver to run a single test in a controlled manner. -/// -/// This driver module implements the logic to execute a particular test -/// with hooks into the runtime procedure. This is to permit debugging the -/// behavior of the test. +/// \file engine/debugger.hpp +/// The interface between the engine and the users outside. -#if !defined(DRIVERS_DEBUG_TEST_HPP) -#define DRIVERS_DEBUG_TEST_HPP +#if !defined(ENGINE_DEBUGGER_HPP) +#define ENGINE_DEBUGGER_HPP -#include "engine/filters.hpp" -#include "model/test_result.hpp" -#include "utils/config/tree_fwd.hpp" -#include "utils/fs/path_fwd.hpp" +#include "model/test_case_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result_fwd.hpp" #include "utils/optional_fwd.hpp" +#include "utils/process/executor_fwd.hpp" -namespace drivers { -namespace debug_test { +namespace executor = utils::process::executor; +using utils::optional; -/// Tuple containing the results of this driver. -class result { + +namespace engine { + + +/// Abstract debugger interface. +class debugger { public: - /// A filter matching the executed test case only. - engine::test_filter test_case; - - /// The result of the test case. - model::test_result test_result; - - /// Initializer for the tuple's fields. - /// - /// \param test_case_ The matched test case. - /// \param test_result_ The result of the test case. - result(const engine::test_filter& test_case_, - const model::test_result& test_result_) : - test_case(test_case_), - test_result(test_result_) - { - } + debugger() {} + virtual ~debugger() {} + + /// Called right before test cleanup. + virtual void before_cleanup( + const model::test_program_ptr&, + const model::test_case&, + optional< model::test_result >&, + executor::exit_handle&) const = 0; }; -result drive(const utils::fs::path&, const utils::optional< utils::fs::path >, - const engine::test_filter&, const utils::config::tree&, - const utils::fs::path&, const utils::fs::path&); +/// Pointer to a debugger implementation. +typedef std::shared_ptr< debugger > debugger_ptr; + +} // namespace engine -} // namespace debug_test -} // namespace drivers -#endif // !defined(DRIVERS_DEBUG_TEST_HPP) +#endif // !defined(ENGINE_DEBUGGER_HPP) diff --git a/contrib/kyua/engine/scheduler.cpp b/contrib/kyua/engine/scheduler.cpp --- a/contrib/kyua/engine/scheduler.cpp +++ b/contrib/kyua/engine/scheduler.cpp @@ -39,6 +39,7 @@ #include #include "engine/config.hpp" +#include "engine/debugger.hpp" #include "engine/exceptions.hpp" #include "engine/execenv/execenv.hpp" #include "engine/requirements.hpp" @@ -1396,8 +1397,15 @@ handle.stderr_file()); } + std::shared_ptr< debugger > debugger = test_case.get_debugger(); + if (debugger) { + debugger->before_cleanup(test_data->test_program, test_case, + result, handle); + } + if (test_data->needs_cleanup) { INV(test_case.get_metadata().has_cleanup()); + // The test body has completed and we have processed it. If there // is a cleanup routine, trigger it now and wait for any other test // completion. The caller never knows about cleanup routines. diff --git a/contrib/kyua/model/test_case.hpp b/contrib/kyua/model/test_case.hpp --- a/contrib/kyua/model/test_case.hpp +++ b/contrib/kyua/model/test_case.hpp @@ -38,11 +38,13 @@ #include #include +#include "engine/debugger.hpp" #include "model/metadata_fwd.hpp" #include "model/test_result_fwd.hpp" #include "utils/noncopyable.hpp" #include "utils/optional_fwd.hpp" + namespace model { @@ -71,6 +73,9 @@ const metadata& get_raw_metadata(void) const; utils::optional< test_result > fake_result(void) const; + void attach_debugger(engine::debugger_ptr) const; + engine::debugger_ptr get_debugger() const; + bool operator==(const test_case&) const; bool operator!=(const test_case&) const; }; diff --git a/contrib/kyua/model/test_case.cpp b/contrib/kyua/model/test_case.cpp --- a/contrib/kyua/model/test_case.cpp +++ b/contrib/kyua/model/test_case.cpp @@ -60,6 +60,9 @@ /// Fake result to return instead of running the test case. optional< model::test_result > fake_result; + /// Optional pointer to a debugger attached. + engine::debugger_ptr debugger; + /// Constructor. /// /// \param name_ The name of the test case within the test program. @@ -233,6 +236,24 @@ } +/// Attach a debugger to the test case. +void +model::test_case::attach_debugger(engine::debugger_ptr debugger) const +{ + _pimpl->debugger = debugger; +} + + +/// Gets the optional pointer to a debugger. +/// +/// \return An optional pointer to a debugger. +engine::debugger_ptr +model::test_case::get_debugger() const +{ + return _pimpl->debugger; +} + + /// Gets the fake result pre-stored for this test case. /// /// \return A fake result, or none if not defined.