git: 8a21c17ccfec - main - kyua: Add "debug -x|--execute cmd" option

From: Igor Ostapenko <igoro_at_FreeBSD.org>
Date: Sun, 11 Jan 2026 13:44:39 UTC
The branch main has been updated by igoro:

URL: https://cgit.FreeBSD.org/src/commit/?id=8a21c17ccfecf0ee54becd46d3e08ccab76ca3d6

commit 8a21c17ccfecf0ee54becd46d3e08ccab76ca3d6
Author:     Igor Ostapenko <igoro@FreeBSD.org>
AuthorDate: 2026-01-11 13:43:33 +0000
Commit:     Igor Ostapenko <igoro@FreeBSD.org>
CommitDate: 2026-01-11 13:43:33 +0000

    kyua: Add "debug -x|--execute cmd" option
    
    With execenv=jail specified, the "cmd" runs inside the test's jail.
    
    Reviewed by:    ngie, markj
    Differential Revision:  https://reviews.freebsd.org/D52642
---
 contrib/kyua/cli/cmd_debug.cpp         | 82 +++++++++++++++++++++++++++++++++-
 contrib/kyua/engine/debugger.hpp       |  7 +++
 contrib/kyua/engine/scheduler.cpp      |  3 ++
 contrib/kyua/utils/cmdline/options.cpp | 20 +++++++--
 contrib/kyua/utils/cmdline/options.hpp |  8 +++-
 contrib/kyua/utils/cmdline/parser.cpp  | 20 ++++++---
 contrib/kyua/utils/process/child.cpp   | 24 ++++++++++
 contrib/kyua/utils/process/child.hpp   |  5 +++
 contrib/kyua/utils/process/child.ipp   | 20 +++++++++
 9 files changed, 178 insertions(+), 11 deletions(-)

diff --git a/contrib/kyua/cli/cmd_debug.cpp b/contrib/kyua/cli/cmd_debug.cpp
index 978ccae0fdf1..060113d137fa 100644
--- a/contrib/kyua/cli/cmd_debug.cpp
+++ b/contrib/kyua/cli/cmd_debug.cpp
@@ -28,6 +28,10 @@
 
 #include "cli/cmd_debug.hpp"
 
+extern "C" {
+#include <unistd.h>
+}
+
 #include <cstdlib>
 #include <iostream>
 
@@ -39,13 +43,20 @@
 #include "utils/cmdline/parser.ipp"
 #include "utils/cmdline/ui.hpp"
 #include "utils/format/macros.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/process/child.ipp"
 #include "utils/process/executor.hpp"
+#include "utils/process/operations.hpp"
+#include "utils/process/status.hpp"
 
 namespace cmdline = utils::cmdline;
 namespace config = utils::config;
 namespace executor = utils::process::executor;
+namespace process = utils::process;
 
 using cli::cmd_debug;
+using utils::process::args_vector;
+using utils::process::child;
 
 
 namespace {
@@ -62,6 +73,57 @@ const cmdline::bool_option pause_before_cleanup_option(
     "Pauses right before the test cleanup");
 
 
+static const char* DEFAULT_CMD = "$SHELL";
+const cmdline::string_option execute_option(
+    'x', "execute",
+    "A command to run within the given execenv upon test failure",
+    "cmd", DEFAULT_CMD, true);
+
+
+/// Functor to execute a program.
+class execute {
+    const std::string& _cmd;
+    executor::exit_handle& _eh;
+
+public:
+    /// Constructor.
+    ///
+    /// \param program Program binary absolute path.
+    /// \param args Program arguments.
+    execute(
+        const std::string& cmd_,
+        executor::exit_handle& eh_) :
+        _cmd(cmd_),
+        _eh(eh_)
+    {
+    }
+
+    /// Body of the subprocess.
+    void
+    operator()(void)
+    {
+        if (::chdir(_eh.work_directory().c_str()) == -1) {
+            std::cerr << "execute: chdir() errors: "
+                << strerror(errno) << ".\n";
+            std::exit(EXIT_FAILURE);
+        }
+
+        std::string program_path = "/bin/sh";
+        const char* shell = std::getenv("SHELL");
+        if (shell)
+            program_path = shell;
+
+        args_vector av;
+        if (!(_cmd.empty() || _cmd == DEFAULT_CMD)) {
+            av.push_back("-c");
+            av.push_back(_cmd);
+        }
+
+        process::exec(utils::fs::path(program_path), av);
+    }
+};
+
+
 /// The debugger interface implementation.
 class dbg : public engine::debugger {
     /// Object to interact with the I/O of the program.
@@ -103,6 +165,21 @@ public:
         }
     };
 
+    void upon_test_failure(
+        const model::test_program_ptr&,
+        const model::test_case&,
+        optional< model::test_result >&,
+        executor::exit_handle& eh) const
+    {
+        if (!_cmdline.has_option(execute_option.long_name()))
+            return;
+        const std::string& cmd = _cmdline.get_option<cmdline::string_option>(
+            execute_option.long_name());
+        std::unique_ptr< process::child > child = child::fork_interactive(
+            execute(cmd, eh));
+        (void) child->wait();
+    };
+
 };
 
 
@@ -127,6 +204,8 @@ cmd_debug::cmd_debug(void) : cli_command(
     add_option(cmdline::path_option(
         "stderr", "Where to direct the standard error of the test case",
         "path", "/dev/stderr"));
+
+    add_option(execute_option);
 }
 
 
@@ -151,7 +230,8 @@ cmd_debug::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline,
 
     engine::debugger_ptr debugger = nullptr;
     if (cmdline.has_option(pause_before_cleanup_upon_fail_option.long_name())
-        || cmdline.has_option(pause_before_cleanup_option.long_name())) {
+        || cmdline.has_option(pause_before_cleanup_option.long_name())
+        || cmdline.has_option(execute_option.long_name())) {
         debugger = std::shared_ptr< engine::debugger >(new dbg(ui, cmdline));
     }
 
diff --git a/contrib/kyua/engine/debugger.hpp b/contrib/kyua/engine/debugger.hpp
index 3c4d087f8ad0..ce87d41ed94d 100644
--- a/contrib/kyua/engine/debugger.hpp
+++ b/contrib/kyua/engine/debugger.hpp
@@ -58,6 +58,13 @@ public:
         const model::test_case&,
         optional< model::test_result >&,
         executor::exit_handle&) const = 0;
+
+    /// Called upon test failure.
+    virtual void upon_test_failure(
+        const model::test_program_ptr&,
+        const model::test_case&,
+        optional< model::test_result >&,
+        executor::exit_handle&) const = 0;
 };
 
 
diff --git a/contrib/kyua/engine/scheduler.cpp b/contrib/kyua/engine/scheduler.cpp
index a0719ec461ab..d04fc1dfbd3d 100644
--- a/contrib/kyua/engine/scheduler.cpp
+++ b/contrib/kyua/engine/scheduler.cpp
@@ -1403,6 +1403,9 @@ scheduler::scheduler_handle::wait_any(void)
         if (debugger) {
             debugger->before_cleanup(test_data->test_program, test_case,
                 result, handle);
+            if (!result.get().good())
+                debugger->upon_test_failure(test_data->test_program, test_case,
+                    result, handle);
         }
 
         if (test_data->needs_cleanup) {
diff --git a/contrib/kyua/utils/cmdline/options.cpp b/contrib/kyua/utils/cmdline/options.cpp
index 61736e31c11e..9d448503e3f9 100644
--- a/contrib/kyua/utils/cmdline/options.cpp
+++ b/contrib/kyua/utils/cmdline/options.cpp
@@ -53,15 +53,18 @@ namespace text = utils::text;
 ///     purposes.
 /// \param default_value_ If not NULL, specifies that the option has a default
 ///     value for the mandatory argument.
+/// \param arg_is_optional_ Specifies if a value must be provided or not.
 cmdline::base_option::base_option(const char short_name_,
                                   const char* long_name_,
                                   const char* description_,
                                   const char* arg_name_,
-                                  const char* default_value_) :
+                                  const char* default_value_,
+                                  bool arg_is_optional_) :
     _short_name(short_name_),
     _long_name(long_name_),
     _description(description_),
     _arg_name(arg_name_ == NULL ? "" : arg_name_),
+    _arg_is_optional(arg_is_optional_),
     _has_default_value(default_value_ != NULL),
     _default_value(default_value_ == NULL ? "" : default_value_)
 {
@@ -164,6 +167,16 @@ cmdline::base_option::arg_name(void) const
 }
 
 
+/// Returns optionality of the argument.
+///
+/// \return The optionality.
+bool
+cmdline::base_option::arg_is_optional(void) const
+{
+    return _arg_is_optional;
+}
+
+
 /// Checks whether the option has a default value for its argument.
 ///
 /// \pre needs_arg() must be true.
@@ -558,9 +571,10 @@ cmdline::string_option::string_option(const char short_name_,
                                       const char* long_name_,
                                       const char* description_,
                                       const char* arg_name_,
-                                      const char* default_value_) :
+                                      const char* default_value_,
+                                      bool arg_is_optional_) :
     base_option(short_name_, long_name_, description_, arg_name_,
-                default_value_)
+                default_value_, arg_is_optional_)
 {
 }
 
diff --git a/contrib/kyua/utils/cmdline/options.hpp b/contrib/kyua/utils/cmdline/options.hpp
index f3a83889e491..d11de14af514 100644
--- a/contrib/kyua/utils/cmdline/options.hpp
+++ b/contrib/kyua/utils/cmdline/options.hpp
@@ -91,6 +91,9 @@ class base_option {
     /// Descriptive name of the required argument; empty if not allowed.
     std::string _arg_name;
 
+    /// If the option can be used without an explicit argument provided.
+    bool _arg_is_optional = false;
+
     /// Whether the option has a default value or not.
     ///
     /// \todo We should probably be using the optional class here.
@@ -101,7 +104,7 @@ class base_option {
 
 public:
     base_option(const char, const char*, const char*, const char* = NULL,
-                const char* = NULL);
+                const char* = NULL, bool = false);
     base_option(const char*, const char*, const char* = NULL,
                 const char* = NULL);
     virtual ~base_option(void);
@@ -113,6 +116,7 @@ public:
 
     bool needs_arg(void) const;
     const std::string& arg_name(void) const;
+    bool arg_is_optional(void) const;
 
     bool has_default_value(void) const;
     const std::string& default_value(void) const;
@@ -219,7 +223,7 @@ public:
 class string_option : public base_option {
 public:
     string_option(const char, const char*, const char*, const char*,
-                  const char* = NULL);
+                  const char* = NULL, bool = false);
     string_option(const char*, const char*, const char*, const char* = NULL);
     virtual ~string_option(void) {}
 
diff --git a/contrib/kyua/utils/cmdline/parser.cpp b/contrib/kyua/utils/cmdline/parser.cpp
index 5c83f6d69cc4..29dd4612f6ad 100644
--- a/contrib/kyua/utils/cmdline/parser.cpp
+++ b/contrib/kyua/utils/cmdline/parser.cpp
@@ -88,7 +88,10 @@ options_to_getopt_data(const cmdline::options_vector& options,
 
         long_option.name = option->long_name().c_str();
         if (option->needs_arg())
-            long_option.has_arg = required_argument;
+            if (option->arg_is_optional())
+                long_option.has_arg = optional_argument;
+            else
+                long_option.has_arg = required_argument;
         else
             long_option.has_arg = no_argument;
 
@@ -96,7 +99,7 @@ options_to_getopt_data(const cmdline::options_vector& options,
         if (option->has_short_name()) {
             data.short_options += option->short_name();
             if (option->needs_arg())
-                data.short_options += ':';
+                data.short_options += option->arg_is_optional() ? "::" : ":";
             id = option->short_name();
         } else {
             id = cur_id++;
@@ -320,9 +323,11 @@ cmdline::parse(const int argc, const char* const* argv,
     for (cmdline::options_vector::const_iterator iter = options.begin();
          iter != options.end(); iter++) {
         const cmdline::base_option* option = *iter;
-        if (option->needs_arg() && option->has_default_value())
+        if (option->needs_arg() && option->has_default_value() &&
+            !option->arg_is_optional()) {
             option_values[option->long_name()].push_back(
                 option->default_value());
+        }
     }
 
     args_vector args;
@@ -357,8 +362,13 @@ cmdline::parse(const int argc, const char* const* argv,
                 if (::optarg != NULL) {
                     option->validate(::optarg);
                     option_values[option->long_name()].push_back(::optarg);
-                } else
-                    INV(option->has_default_value());
+                } else {
+                    if (option->arg_is_optional())
+                        option_values[option->long_name()].push_back(
+                            option->default_value());
+                    else
+                        INV(option->has_default_value());
+                }
             } else {
                 option_values[option->long_name()].push_back("");
             }
diff --git a/contrib/kyua/utils/process/child.cpp b/contrib/kyua/utils/process/child.cpp
index 36b6b6b3e51f..c51c39e6d1ff 100644
--- a/contrib/kyua/utils/process/child.cpp
+++ b/contrib/kyua/utils/process/child.cpp
@@ -235,6 +235,30 @@ process::child::fork_capture_aux(void)
 }
 
 
+std::unique_ptr< process::child >
+process::child::fork_interactive(void)
+{
+    std::cout.flush();
+    std::cerr.flush();
+
+    std::unique_ptr< signals::interrupts_inhibiter > inhibiter(
+        new signals::interrupts_inhibiter);
+    pid_t pid = detail::syscall_fork();
+    if (pid == -1) {
+        inhibiter.reset();  // Unblock signals.
+        throw process::system_error("fork(2) failed", errno);
+    } else if (pid == 0) {
+        inhibiter.reset();  // Unblock signals.
+        return {};
+    } else {
+        signals::add_pid_to_kill(pid);
+        inhibiter.reset(NULL);  // Unblock signals.
+        return std::unique_ptr< process::child >(
+            new process::child(new impl(pid, NULL)));
+    }
+}
+
+
 /// Helper function for fork().
 ///
 /// Please note: if you update this function to change the return type or to
diff --git a/contrib/kyua/utils/process/child.hpp b/contrib/kyua/utils/process/child.hpp
index 3e00cea8752c..fbe32311a05b 100644
--- a/contrib/kyua/utils/process/child.hpp
+++ b/contrib/kyua/utils/process/child.hpp
@@ -80,6 +80,8 @@ class child : noncopyable {
 
     static std::unique_ptr< child > fork_capture_aux(void);
 
+    static std::unique_ptr< child > fork_interactive(void);
+
     static std::unique_ptr< child > fork_files_aux(const fs::path&,
                                                  const fs::path&);
 
@@ -92,6 +94,9 @@ public:
     static std::unique_ptr< child > fork_capture(Hook);
     std::istream& output(void);
 
+    template< typename Hook >
+    static std::unique_ptr< child > fork_interactive(Hook);
+
     template< typename Hook >
     static std::unique_ptr< child > fork_files(Hook, const fs::path&,
                                              const fs::path&);
diff --git a/contrib/kyua/utils/process/child.ipp b/contrib/kyua/utils/process/child.ipp
index beb2ea3b0b0a..86bc01fc0d6e 100644
--- a/contrib/kyua/utils/process/child.ipp
+++ b/contrib/kyua/utils/process/child.ipp
@@ -104,6 +104,26 @@ child::fork_capture(Hook hook)
 }
 
 
+template< typename Hook >
+std::unique_ptr< child >
+child::fork_interactive(Hook hook)
+{
+    std::unique_ptr< child > child = fork_interactive();
+    if (child.get() == NULL) {
+        try {
+            hook();
+            std::abort();
+        } catch (const std::runtime_error& e) {
+            detail::report_error_and_abort(e);
+        } catch (...) {
+            detail::report_error_and_abort();
+        }
+    }
+
+    return child;
+}
+
+
 }  // namespace process
 }  // namespace utils