This is an automated email from the ASF dual-hosted git repository.

dmeden pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new bdf2c200b8 traffic_ctl: Add `--append` option to append debug tags 
instead of replacing them. (inc ArgParser support). (#12804)
bdf2c200b8 is described below

commit bdf2c200b8bf5776a6f6b4de2f3a644cf9339035
Author: Damian Meden <[email protected]>
AuthorDate: Mon Feb 2 11:35:23 2026 +0100

    traffic_ctl: Add `--append` option to append debug tags instead of 
replacing them. (inc ArgParser support). (#12804)
    
    * ArgParser: Add option dependencies (with_required) and multiple example 
usages
    
    - Add with_required() method to specify that an option requires another
    - Change add_example_usage() to support multiple examples per command
    - Add unit tests for option dependencies
    - Update ArgParser documentation
    
    * traffic_ctl: Add --append flag to 'server debug enable' for appending tags
    
    - Add --append/-a option to append debug tags instead of replacing them
    - Uses ArgParser's with_required() to enforce --tags dependency
    - Add autest for server debug enable/disable commands
    - Update traffic_ctl documentation
---
 doc/appendices/command-line/traffic_ctl.en.rst     |  16 +-
 .../internal-libraries/ArgParser.en.rst            |  59 ++++-
 include/tscore/ArgParser.h                         |  18 +-
 src/traffic_ctl/CtrlCommands.cc                    |  21 +-
 src/traffic_ctl/CtrlCommands.h                     |   1 +
 src/traffic_ctl/traffic_ctl.cc                     |   5 +-
 src/tscore/ArgParser.cc                            |  93 +++++++-
 src/tscore/CMakeLists.txt                          |   1 +
 .../test_ArgParser_OptionDependencies.cc           | 237 +++++++++++++++++++++
 tests/gold_tests/traffic_ctl/gold/test_2.gold      |   1 -
 tests/gold_tests/traffic_ctl/gold/test_3.gold      |   1 -
 .../traffic_ctl/traffic_ctl_server_debug.test.py   |  80 +++++++
 .../traffic_ctl/traffic_ctl_test_utils.py          |  54 +++++
 13 files changed, 569 insertions(+), 18 deletions(-)

diff --git a/doc/appendices/command-line/traffic_ctl.en.rst 
b/doc/appendices/command-line/traffic_ctl.en.rst
index 55dbee5570..2871866647 100644
--- a/doc/appendices/command-line/traffic_ctl.en.rst
+++ b/doc/appendices/command-line/traffic_ctl.en.rst
@@ -535,6 +535,11 @@ traffic_ctl server
    This string should contain an anchored regular expression that filters the 
messages based on the debug tag tag.
    Please refer to :ts:cv:`proxy.config.diags.debug.tags` for more information
 
+   .. option:: --append, -a
+
+   Append the specified tags to the existing debug tags instead of replacing 
them. This option requires
+   ``--tags`` to be specified. The new tags will be combined with existing 
tags using the ``|`` separator.
+
    .. option:: --client_ip, -c ip
 
    Please see :ts:cv:`proxy.config.diags.debug.client_ip` for information.
@@ -547,13 +552,22 @@ traffic_ctl server
    Disables logging for diagnostic messages. Equivalent to set 
:ts:cv:`proxy.config.diags.debug.enabled` to ``0``.
 
 
-   Example:
+   Examples:
 
    .. code-block:: bash
 
+      # Set debug tags (replaces existing tags)
       $ traffic_ctl server debug enable --tags "quic|quiche"
       ■ TS Runtime debug set to »ON(1)« - tags »"quic|quiche"«, client_ip 
»unchanged«
 
+      # Append debug tags to existing tags
+      $ traffic_ctl server debug enable --tags "http" --append
+      ■ TS Runtime debug set to »ON(1)« - tags »"quic|quiche|http"«, client_ip 
»unchanged«
+
+      # Disable debug logging
+      $ traffic_ctl server debug disable
+      ■ TS Runtime debug set to »OFF(0)«
+
 .. _traffic-control-command-storage:
 
 traffic_ctl storage
diff --git a/doc/developer-guide/internal-libraries/ArgParser.en.rst 
b/doc/developer-guide/internal-libraries/ArgParser.en.rst
index 9336f8b843..c15a552ff5 100644
--- a/doc/developer-guide/internal-libraries/ArgParser.en.rst
+++ b/doc/developer-guide/internal-libraries/ArgParser.en.rst
@@ -155,6 +155,40 @@ Example with a required group:
 
    // User must specify either --json or --xml, but not both
 
+Option Dependencies
+-------------------
+
+ArgParser supports option dependencies, where one option requires another 
option to be present.
+This is useful when an option only makes sense in combination with another 
option.
+
+To specify that an option requires another option, use the ``with_required()`` 
method immediately after
+adding the option:
+
+.. code-block:: cpp
+
+   command.add_option("--tags", "-t", "Debug tags", "", 1)
+   command.add_option("--append", "-a", "Append tags to existing tags")
+     .with_required("--tags");  // --append requires --tags to be present
+
+When ``--append`` is used without ``--tags``, ArgParser will display an error 
message and exit:
+
+.. code-block:: text
+
+   Error: Option '--append' requires '--tags' to be specified
+
+Multiple dependencies can be specified by chaining ``with_required()`` calls:
+
+.. code-block:: cpp
+
+   command.add_option("--verbose-append", "-V", "Verbose append mode")
+     .with_required("--tags")
+     .with_required("--append");  // requires both --tags and --append
+
+.. Note::
+
+   The ``with_required()`` method must be called immediately after 
``add_option()`` or
+   ``add_option_to_group()``. It applies to the most recently added option.
+
 Parsing Arguments
 -----------------
 
@@ -267,10 +301,23 @@ Classes
    is called under certain command, it will be added as a subcommand for the 
current command. For Example, :code:`command1.add_command("command2", 
"description")`
    will make :code:`command2` a subcommand of :code:`command1`. 
:code:`require_commands()` is also available within :class:`Command`.
 
-   .. function:: void add_example_usage(std::string const &usage)
+   .. function:: Command &add_example_usage(std::string const &usage)
+
+      Add an example usage for the command to output in ``help_message``. This 
method can be
+      called multiple times to add multiple examples. Returns the Command 
instance for chained calls.
+
+      Example::
 
-      Add an example usage for the command to output in `help_message`.
-      For Example: :code:`command.add_example_usage("traffic_blabla init 
--path=/path/to/file")`.
+         command.add_example_usage("traffic_ctl server debug enable -t 
my_tags")
+                .add_example_usage("traffic_ctl server debug enable -t new_tag 
-a  # append mode");
+
+      This will output in help:
+
+      .. code-block:: text
+
+         Example Usage:
+           traffic_ctl server debug enable -t my_tags
+           traffic_ctl server debug enable -t new_tag -a  # append mode
 
    .. function:: Command &set_default()
 
@@ -284,6 +331,12 @@ Classes
 
       Add an option to a mutually exclusive group for this command.
 
+   .. function:: Command &with_required(std::string const &required_option)
+
+      Specify that the last added option requires another option to be present.
+      Must be called immediately after ``add_option()`` or 
``add_option_to_group()``.
+      Returns the Command instance for chained calls.
+
 .. class:: Arguments
 
    :class:`Arguments` holds the parsed arguments and function to invoke.
diff --git a/include/tscore/ArgParser.h b/include/tscore/ArgParser.h
index 49e27fb8a2..a4bd5916fe 100644
--- a/include/tscore/ArgParser.h
+++ b/include/tscore/ArgParser.h
@@ -176,6 +176,13 @@ public:
                                  std::string const &description, std::string 
const &envvar = "", unsigned arg_num = 0,
                                  std::string const &default_value = "", 
std::string const &key = "");
 
+    /** Specify that the last added option requires another option to be 
present.
+        Must be called immediately after add_option() or add_option_to_group().
+        @param required_option The option that must be present (e.g., "--tags")
+        @return The Command instance for chained calls.
+    */
+    Command &with_required(std::string const &required_option);
+
     /** Two ways of adding a sub-command to current command:
         @return The new sub-command instance.
     */
@@ -217,6 +224,8 @@ public:
     void append_option_data(Arguments &ret, AP_StrVec &args, int index);
     // Helper method to validate mutually exclusive groups
     void validate_mutex_groups(Arguments &ret) const;
+    // Helper method to validate option dependencies
+    void validate_dependencies(Arguments &ret) const;
     // The command name and help message
     std::string _name;
     std::string _description;
@@ -225,8 +234,8 @@ public:
     unsigned _arg_num = 0;
     // Stored Env variable
     std::string _envvar;
-    // An example usage can be added for the help message
-    std::string _example_usage;
+    // Example usages can be added for the help message
+    std::vector<std::string> _example_usages;
     // Function associated with this command
     Function _f;
     // look up key
@@ -248,6 +257,11 @@ public:
     // Key: long option name. Value: group name
     std::map<std::string, std::string> _option_to_group;
 
+    // Option dependencies: dependent_option -> list of required options
+    std::map<std::string, std::vector<std::string>> _option_dependencies;
+    // Track the last added option for with_required() chaining
+    std::string _last_added_option;
+
     // require command / option for this parser
     bool _command_required = false;
 
diff --git a/src/traffic_ctl/CtrlCommands.cc b/src/traffic_ctl/CtrlCommands.cc
index c2f888e87e..83c3483efe 100644
--- a/src/traffic_ctl/CtrlCommands.cc
+++ b/src/traffic_ctl/CtrlCommands.cc
@@ -695,12 +695,31 @@ ServerCommand::server_debug()
 {
   // Set ATS to enable or disable debug at runtime.
   const bool enable = get_parsed_arguments()->get(ENABLE_STR);
+  const bool append = get_parsed_arguments()->get(APPEND_STR);
 
   // If the following is not passed as options then the request will ignore 
them as default values
   // will be set.
-  const std::string tags      = get_parsed_arguments()->get(TAGS_STR).value();
+  std::string       tags      = get_parsed_arguments()->get(TAGS_STR).value();
   const std::string client_ip = 
get_parsed_arguments()->get(CLIENT_IP_STR).value();
 
+  // If append mode is enabled and tags are provided, fetch current tags and 
combine
+  if (append && !tags.empty()) {
+    shared::rpc::RecordLookupRequest lookup_request;
+    lookup_request.emplace_rec("proxy.config.diags.debug.tags", 
shared::rpc::NOT_REGEX, shared::rpc::CONFIG_REC_TYPES);
+    auto lookup_response = invoke_rpc(lookup_request);
+
+    if (!lookup_response.is_error()) {
+      auto const &records = 
lookup_response.result.as<shared::rpc::RecordLookUpResponse>();
+      if (!records.recordList.empty()) {
+        std::string current_tags = records.recordList[0].currentValue;
+        if (!current_tags.empty()) {
+          // Combine: current|new
+          tags = current_tags + "|" + tags;
+        }
+      }
+    }
+  }
+
   const SetDebugServerRequest         request{enable, tags, client_ip};
   shared::rpc::JSONRPCResponse const &response = invoke_rpc(request);
 
diff --git a/src/traffic_ctl/CtrlCommands.h b/src/traffic_ctl/CtrlCommands.h
index c10e697ed5..8d5fc2b931 100644
--- a/src/traffic_ctl/CtrlCommands.h
+++ b/src/traffic_ctl/CtrlCommands.h
@@ -236,6 +236,7 @@ private:
   static inline const std::string ENABLE_STR{"enable"};
   static inline const std::string DISABLE_STR{"disable"};
   static inline const std::string TAGS_STR{"tags"};
+  static inline const std::string APPEND_STR{"append"};
   static inline const std::string CLIENT_IP_STR{"client_ip"};
 
   static inline const std::string STATUS_STR{"status"};
diff --git a/src/traffic_ctl/traffic_ctl.cc b/src/traffic_ctl/traffic_ctl.cc
index a018304a3f..6529c0f88b 100644
--- a/src/traffic_ctl/traffic_ctl.cc
+++ b/src/traffic_ctl/traffic_ctl.cc
@@ -193,8 +193,11 @@ main([[maybe_unused]] int argc, const char **argv)
     server_command.add_command("debug", "Enable/Disable ATS for diagnostic 
messages at runtime").require_commands();
   debug_command.add_command("enable", "Enables logging for diagnostic messages 
at runtime", Command_Execute)
     .add_option("--tags", "-t", "Debug tags", "TS_DEBUG_TAGS", 1)
+    .add_option("--append", "-a", "Append tags to existing tags instead of 
replacing")
+    .with_required("--tags")
     .add_option("--client_ip", "-c", "Client's ip", "", 1, "")
-    .add_example_usage("traffic_ctl server debug enable -t my_tags -c 
X.X.X.X");
+    .add_example_usage("traffic_ctl server debug enable -t my_tags -c X.X.X.X")
+    .add_example_usage("traffic_ctl server debug enable -t new_tag -a  # 
append mode");
   debug_command.add_command("disable", "Disables logging for diagnostic 
messages at runtime", Command_Execute)
     .add_example_usage("traffic_ctl server debug disable");
 
diff --git a/src/tscore/ArgParser.cc b/src/tscore/ArgParser.cc
index 00a4fd20b1..65298317a6 100644
--- a/src/tscore/ArgParser.cc
+++ b/src/tscore/ArgParser.cc
@@ -132,9 +132,12 @@ ArgParser::Command::help_message(std::string_view err) 
const
     std::cout << "\nOptions ======================= Default ===== Description 
=============" << std::endl;
     output_option();
   }
-  // output example usage
-  if (!_example_usage.empty()) {
-    std::cout << "\nExample Usage: " << _example_usage << std::endl;
+  // output example usages
+  if (!_example_usages.empty()) {
+    std::cout << "\nExample Usage:" << std::endl;
+    for (const auto &example : _example_usages) {
+      std::cout << "  " << example << std::endl;
+    }
   }
   // standard return code
   ArgParser::do_exit(usage_return_code);
@@ -303,6 +306,7 @@ ArgParser::Command::add_option(std::string const 
&long_option, std::string const
   if (short_option != "-" && !short_option.empty()) {
     _option_map[short_option] = long_option;
   }
+  _last_added_option = long_option; // track for with_required() chaining
   return *this;
 }
 
@@ -340,7 +344,7 @@ ArgParser::Command::add_option_to_group(std::string const 
&group_name, std::stri
     ArgParser::do_exit(1);
   }
 
-  // Add the option normally
+  // Add the option normally (this also sets _last_added_option)
   add_option(long_option, short_option, description, envvar, arg_num, 
default_value, key);
 
   // Track this option in the mutex group
@@ -375,7 +379,7 @@ ArgParser::Command::add_command(std::string const 
&cmd_name, std::string const &
 ArgParser::Command &
 ArgParser::Command::add_example_usage(std::string const &usage)
 {
-  _example_usage = usage;
+  _example_usages.push_back(usage);
   return *this;
 }
 
@@ -443,11 +447,28 @@ ArgParser::Command::output_option() const
         msg = msg + std::string(INDENT_ONE - msg.size(), ' ') + 
it.second.default_value;
       }
     }
-    if (!it.second.description.empty()) {
+    // Build description with dependency info if applicable
+    std::string desc   = it.second.description;
+    auto        dep_it = _option_dependencies.find(it.first);
+    if (dep_it != _option_dependencies.end() && !dep_it->second.empty()) {
+      if (!desc.empty()) {
+        desc += " ";
+      }
+      desc += "(requires";
+      for (size_t i = 0; i < dep_it->second.size(); ++i) {
+        desc += " " + dep_it->second[i];
+        if (i < dep_it->second.size() - 1) {
+          desc += ",";
+        }
+      }
+      desc += ")";
+    }
+
+    if (!desc.empty()) {
       if (INDENT_TWO - static_cast<int>(msg.size()) < 0) {
-        std::cout << msg << "\n" << std::string(INDENT_TWO, ' ') << 
it.second.description << std::endl;
+        std::cout << msg << "\n" << std::string(INDENT_TWO, ' ') << desc << 
std::endl;
       } else {
-        std::cout << msg << std::string(INDENT_TWO - msg.size(), ' ') << 
it.second.description << std::endl;
+        std::cout << msg << std::string(INDENT_TWO - msg.size(), ' ') << desc 
<< std::endl;
       }
     }
   }
@@ -574,6 +595,59 @@ ArgParser::Command::validate_mutex_groups(Arguments &ret) 
const
   }
 }
 
+// Specify that the last added option requires another option
+ArgParser::Command &
+ArgParser::Command::with_required(std::string const &required_option)
+{
+  if (_last_added_option.empty()) {
+    std::cerr << "Error: with_required() must be called after add_option()" << 
std::endl;
+    ArgParser::do_exit(1);
+  }
+
+  // Validate that required option exists
+  if (_option_list.find(required_option) == _option_list.end()) {
+    std::cerr << "Error: Required option '" << required_option << "' not 
found" << std::endl;
+    ArgParser::do_exit(1);
+  }
+
+  _option_dependencies[_last_added_option].push_back(required_option);
+
+  return *this;
+}
+
+// Validate option dependencies
+void
+ArgParser::Command::validate_dependencies(Arguments &ret) const
+{
+  for (const auto &[dependent, required_list] : _option_dependencies) {
+    // Get the key for the dependent option
+    auto it = _option_list.find(dependent);
+    if (it == _option_list.end()) {
+      continue;
+    }
+
+    const std::string &dep_key = it->second.key;
+
+    // Check if dependent option was used
+    if (ret.get(dep_key)) {
+      // Dependent option was used, check all required options
+      for (const auto &required : required_list) {
+        auto req_it = _option_list.find(required);
+        if (req_it == _option_list.end()) {
+          continue;
+        }
+
+        const std::string &req_key = req_it->second.key;
+
+        if (!ret.get(req_key)) {
+          std::string error_msg = "Option '" + dependent + "' requires '" + 
required + "' to be specified";
+          help_message(error_msg); // exit with status code 64 (EX_USAGE - 
command line usage error)
+        }
+      }
+    }
+  }
+}
+
 // Append the args of option to parsed data. Return true if there is any 
option called
 void
 ArgParser::Command::append_option_data(Arguments &ret, AP_StrVec &args, int 
index)
@@ -694,6 +768,9 @@ ArgParser::Command::parse(Arguments &ret, AP_StrVec &args)
 
     // Validate mutually exclusive groups
     validate_mutex_groups(ret);
+
+    // Validate option dependencies
+    validate_dependencies(ret);
   }
 
   if (command_called) {
diff --git a/src/tscore/CMakeLists.txt b/src/tscore/CMakeLists.txt
index 03ade9cfda..9b2382e1c4 100644
--- a/src/tscore/CMakeLists.txt
+++ b/src/tscore/CMakeLists.txt
@@ -165,6 +165,7 @@ if(BUILD_TESTING)
     unit_tests/test_scoped_resource.cc
     unit_tests/test_Version.cc
     unit_tests/test_ArgParser_MutexGroup.cc
+    unit_tests/test_ArgParser_OptionDependencies.cc
     unit_tests/test_Allocator.cc
   )
   target_link_libraries(
diff --git a/src/tscore/unit_tests/test_ArgParser_OptionDependencies.cc 
b/src/tscore/unit_tests/test_ArgParser_OptionDependencies.cc
new file mode 100644
index 0000000000..c996a91613
--- /dev/null
+++ b/src/tscore/unit_tests/test_ArgParser_OptionDependencies.cc
@@ -0,0 +1,237 @@
+/** @file
+
+  Unit test for ArgParser option dependencies (with_required)
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+#include "tscore/ArgParser.h"
+
+class TestArgParser : public ts::ArgParser
+{
+public:
+  TestArgParser() { ts::ArgParser::set_test_mode(true); }
+};
+
+TEST_CASE("Option dependencies - basic dependency", "[option_dependencies]")
+{
+  ts::ArgParser parser;
+  parser.add_description("Test basic option dependency");
+  parser.add_global_usage("test [OPTIONS]");
+
+  parser.add_option("--tags", "-t", "Debug tags", "", 1);
+  parser.add_option("--append", "-a", "Append to existing 
tags").with_required("--tags");
+
+  // Test with both options - should work
+  const char   *argv1[] = {"test", "--tags", "http", "--append", nullptr};
+  ts::Arguments args1   = parser.parse(argv1);
+  REQUIRE(args1.get("tags") == true);
+  REQUIRE(args1.get("tags").value() == "http");
+  REQUIRE(args1.get("append") == true);
+
+  // Test with only --tags - should work
+  const char   *argv2[] = {"test", "--tags", "dns", nullptr};
+  ts::Arguments args2   = parser.parse(argv2);
+  REQUIRE(args2.get("tags") == true);
+  REQUIRE(args2.get("tags").value() == "dns");
+  REQUIRE(args2.get("append") == false);
+
+  // Test with neither option - should work
+  const char   *argv3[] = {"test", nullptr};
+  ts::Arguments args3   = parser.parse(argv3);
+  REQUIRE(args3.get("tags") == false);
+  REQUIRE(args3.get("append") == false);
+}
+
+TEST_CASE("Option dependencies - violation detection", "[option_dependencies]")
+{
+  TestArgParser parser;
+  parser.add_description("Test dependency violation");
+  parser.add_global_usage("test [OPTIONS]");
+
+  parser.add_option("--tags", "-t", "Debug tags", "", 1);
+  parser.add_option("--append", "-a", "Append to existing 
tags").with_required("--tags");
+
+  // Test with only --append (without --tags) - should error
+  const char *argv[] = {"test", "--append", nullptr};
+  REQUIRE_THROWS(parser.parse(argv));
+}
+
+TEST_CASE("Option dependencies - short option violation", 
"[option_dependencies]")
+{
+  TestArgParser parser;
+  parser.add_description("Test dependency violation with short option");
+  parser.add_global_usage("test [OPTIONS]");
+
+  parser.add_option("--tags", "-t", "Debug tags", "", 1);
+  parser.add_option("--append", "-a", "Append to existing 
tags").with_required("--tags");
+
+  // Test with short option -a (without --tags) - should error
+  const char *argv[] = {"test", "-a", nullptr};
+  REQUIRE_THROWS(parser.parse(argv));
+}
+
+TEST_CASE("Option dependencies - multiple dependencies", 
"[option_dependencies]")
+{
+  ts::ArgParser parser;
+  parser.add_description("Test multiple option dependencies");
+  parser.add_global_usage("test [OPTIONS]");
+
+  parser.add_option("--tags", "-t", "Debug tags", "", 1);
+  parser.add_option("--append", "-a", "Append mode");
+  parser.add_option("--verbose-append", "-V", "Verbose append 
mode").with_required("--tags").with_required("--append");
+
+  // Test with all options - should work
+  const char   *argv1[] = {"test", "--tags", "http", "--append", 
"--verbose-append", nullptr};
+  ts::Arguments args1   = parser.parse(argv1);
+  REQUIRE(args1.get("tags") == true);
+  REQUIRE(args1.get("append") == true);
+  REQUIRE(args1.get("verbose-append") == true);
+}
+
+TEST_CASE("Option dependencies - multiple dependencies violation", 
"[option_dependencies]")
+{
+  TestArgParser parser;
+  parser.add_description("Test multiple dependency violation");
+  parser.add_global_usage("test [OPTIONS]");
+
+  parser.add_option("--tags", "-t", "Debug tags", "", 1);
+  parser.add_option("--append", "-a", "Append mode");
+  parser.add_option("--verbose-append", "-V", "Verbose append 
mode").with_required("--tags").with_required("--append");
+
+  // Test with --verbose-append but only --tags (missing --append) - should 
error
+  const char *argv[] = {"test", "--tags", "http", "--verbose-append", nullptr};
+  REQUIRE_THROWS(parser.parse(argv));
+}
+
+TEST_CASE("Option dependencies - with subcommands", "[option_dependencies]")
+{
+  ts::ArgParser           parser;
+  ts::ArgParser::Command &cmd = parser.add_command("debug", "Debug commands");
+
+  cmd.add_option("--tags", "-t", "Debug tags", "", 1);
+  cmd.add_option("--append", "-a", "Append to existing 
tags").with_required("--tags");
+
+  // Test with subcommand and both options - should work
+  const char   *argv1[] = {"test", "debug", "--tags", "http", "--append", 
nullptr};
+  ts::Arguments args1   = parser.parse(argv1);
+  REQUIRE(args1.get("debug") == true);
+  REQUIRE(args1.get("tags") == true);
+  REQUIRE(args1.get("append") == true);
+
+  // Test with subcommand and only --tags - should work
+  const char   *argv2[] = {"test", "debug", "-t", "dns", nullptr};
+  ts::Arguments args2   = parser.parse(argv2);
+  REQUIRE(args2.get("debug") == true);
+  REQUIRE(args2.get("tags") == true);
+  REQUIRE(args2.get("append") == false);
+}
+
+TEST_CASE("Option dependencies - subcommand violation", 
"[option_dependencies]")
+{
+  TestArgParser           parser;
+  TestArgParser::Command &cmd = parser.add_command("debug", "Debug commands");
+
+  cmd.add_option("--tags", "-t", "Debug tags", "", 1);
+  cmd.add_option("--append", "-a", "Append to existing 
tags").with_required("--tags");
+
+  // Test with subcommand and only --append - should error
+  const char *argv[] = {"test", "debug", "--append", nullptr};
+  REQUIRE_THROWS(parser.parse(argv));
+}
+
+TEST_CASE("Option dependencies - invalid required option", 
"[option_dependencies]")
+{
+  TestArgParser parser;
+  parser.add_description("Test invalid required option");
+  parser.add_global_usage("test [OPTIONS]");
+
+  parser.add_option("--append", "-a", "Append mode");
+
+  // Try to require an option that doesn't exist - should throw
+  REQUIRE_THROWS(parser.add_option("--verbose", "-v", "Verbose 
mode").with_required("--nonexistent"));
+}
+
+TEST_CASE("Option dependencies - with_required without add_option", 
"[option_dependencies]")
+{
+  TestArgParser parser;
+  parser.add_description("Test with_required without prior add_option");
+  parser.add_global_usage("test [OPTIONS]");
+
+  // Calling with_required() without first calling add_option() should error
+  // This is a bit tricky to test since with_required returns Command&
+  // The error would occur at runtime when there's no _last_added_option
+  // We need to test this via the Command directly
+  parser.add_option("--first", "-f", "First option");
+
+  // Add a second option and require the first - this should work
+  parser.add_option("--second", "-s", "Second 
option").with_required("--first");
+
+  const char   *argv[] = {"test", "--first", "--second", nullptr};
+  ts::Arguments args   = parser.parse(argv);
+  REQUIRE(args.get("first") == true);
+  REQUIRE(args.get("second") == true);
+}
+
+TEST_CASE("Option dependencies - combined with mutex groups", 
"[option_dependencies]")
+{
+  ts::ArgParser parser;
+  parser.add_description("Test dependencies combined with mutex groups");
+  parser.add_global_usage("test [OPTIONS]");
+
+  // Mutex group for mode
+  parser.add_mutex_group("mode", false, "Operation mode");
+  parser.add_option_to_group("mode", "--enable", "-e", "Enable mode");
+  parser.add_option_to_group("mode", "--disable", "-d", "Disable mode");
+
+  // Option that requires --enable
+  parser.add_option("--tags", "-t", "Debug tags", "", 
1).with_required("--enable");
+
+  // Test with --enable and --tags - should work
+  const char   *argv1[] = {"test", "--enable", "--tags", "http", nullptr};
+  ts::Arguments args1   = parser.parse(argv1);
+  REQUIRE(args1.get("enable") == true);
+  REQUIRE(args1.get("tags") == true);
+
+  // Test with --disable only - should work (--tags not used)
+  const char   *argv2[] = {"test", "--disable", nullptr};
+  ts::Arguments args2   = parser.parse(argv2);
+  REQUIRE(args2.get("disable") == true);
+  REQUIRE(args2.get("tags") == false);
+}
+
+TEST_CASE("Option dependencies - combined with mutex groups violation", 
"[option_dependencies]")
+{
+  TestArgParser parser;
+  parser.add_description("Test dependencies combined with mutex groups 
violation");
+  parser.add_global_usage("test [OPTIONS]");
+
+  // Mutex group for mode
+  parser.add_mutex_group("mode", false, "Operation mode");
+  parser.add_option_to_group("mode", "--enable", "-e", "Enable mode");
+  parser.add_option_to_group("mode", "--disable", "-d", "Disable mode");
+
+  // Option that requires --enable
+  parser.add_option("--tags", "-t", "Debug tags", "", 
1).with_required("--enable");
+
+  // Test with --disable and --tags - should error (--tags requires --enable)
+  const char *argv[] = {"test", "--disable", "--tags", "http", nullptr};
+  REQUIRE_THROWS(parser.parse(argv));
+}
diff --git a/tests/gold_tests/traffic_ctl/gold/test_2.gold 
b/tests/gold_tests/traffic_ctl/gold/test_2.gold
deleted file mode 100644
index 3c75916cf7..0000000000
--- a/tests/gold_tests/traffic_ctl/gold/test_2.gold
+++ /dev/null
@@ -1 +0,0 @@
-proxy.config.diags.debug.enabled: 1
diff --git a/tests/gold_tests/traffic_ctl/gold/test_3.gold 
b/tests/gold_tests/traffic_ctl/gold/test_3.gold
deleted file mode 100644
index e12f994bef..0000000000
--- a/tests/gold_tests/traffic_ctl/gold/test_3.gold
+++ /dev/null
@@ -1 +0,0 @@
-proxy.config.diags.debug.tags: rpc # default http|dns
diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_server_debug.test.py 
b/tests/gold_tests/traffic_ctl/traffic_ctl_server_debug.test.py
new file mode 100644
index 0000000000..f899f98070
--- /dev/null
+++ b/tests/gold_tests/traffic_ctl/traffic_ctl_server_debug.test.py
@@ -0,0 +1,80 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import sys
+
+# To include util classes
+sys.path.insert(0, f'{Test.TestDirectory}')
+
+from traffic_ctl_test_utils import Make_traffic_ctl
+
+Test.Summary = '''
+Test traffic_ctl server debug enable/disable commands.
+'''
+
+Test.ContinueOnFail = True
+
+records_yaml = '''
+diags:
+  debug:
+    enabled: 0
+    tags: xyz
+'''
+
+traffic_ctl = Make_traffic_ctl(Test, records_yaml)
+
+######
+# Test 1: Enable debug with tags
+traffic_ctl.server().debug().enable(tags="http").exec()
+# Test 2: Verify debug is enabled and tags are set
+traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled:
 1")
+# Test 3: Verify tags are set
+traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags:
 http")
+
+# Test 4: Disable debug
+traffic_ctl.server().debug().disable().exec()
+# Test 5: Verify debug is disabled
+traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled:
 0")
+
+# Test 6: Enable debug with new tags (replace mode)
+traffic_ctl.server().debug().enable(tags="cache").exec()
+# Test 7: Verify tags are replaced
+traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags:
 cache")
+
+# Test 8: Enable debug with append mode - should combine with existing tags
+traffic_ctl.server().debug().enable(tags="http", append=True).exec()
+# Test 9: Verify tags are appended
+traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags:
 cache|http")
+
+# Test 10: Append another tag
+traffic_ctl.server().debug().enable(tags="dns", append=True).exec()
+# Test 11: Verify all tags are present
+traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags:
 cache|http|dns")
+
+# Test 12: Disable and verify
+traffic_ctl.server().debug().disable().exec()
+# Test 13: Verify debug is disabled
+traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled:
 0")
+
+# Test 14: Verify --append requires --tags (should fail with error)
+# This tests the ArgParser requires() functionality
+tr = Test.AddTestRun("test --append without --tags")
+tr.Processes.Default.Env = traffic_ctl._ts.Env
+tr.Processes.Default.Command = "traffic_ctl server debug enable --append"
+tr.Processes.Default.ReturnCode = 64  # EX_USAGE - command line usage error
+tr.Processes.Default.Streams.All = Testers.ContainsExpression(
+    "Option \'--append\' requires \'--tags\' to be specified", "Should show 
error that --append requires --tags")
+tr.StillRunningAfter = traffic_ctl._ts
diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py 
b/tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py
index b8c4b18f74..a40d60e86b 100644
--- a/tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py
+++ b/tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py
@@ -233,6 +233,50 @@ class Config(Common):
         self._finish()
 
 
+class Debug(Common):
+    """
+        Handy class to map traffic_ctl server debug options.
+    """
+
+    def __init__(self, dir, tr, tn):
+        super().__init__(tr)
+        self._cmd = "traffic_ctl server debug "
+        self._dir = dir
+        self._tn = tn
+
+    def enable(self, tags=None, append=False, client_ip=None):
+        """
+        Enable debug logging at runtime.
+
+        Args:
+            tags: Debug tags to set (e.g., "http|dns")
+            append: If True, append tags to existing tags instead of replacing
+            client_ip: Client IP filter for debug output
+
+        Example:
+            traffic_ctl.server().debug().enable(tags="http").exec()
+            traffic_ctl.server().debug().enable(tags="dns", append=True).exec()
+        """
+        self._cmd = f'{self._cmd} enable'
+        if tags:
+            self._cmd = f'{self._cmd} --tags {tags}'
+        if append:
+            self._cmd = f'{self._cmd} --append'
+        if client_ip:
+            self._cmd = f'{self._cmd} --client_ip {client_ip}'
+        return self
+
+    def disable(self):
+        """
+        Disable debug logging at runtime.
+
+        Example:
+            traffic_ctl.server().debug().disable().exec()
+        """
+        self._cmd = f'{self._cmd} disable'
+        return self
+
+
 class Server(Common):
     """
         Handy class to map traffic_ctl server options.
@@ -254,6 +298,16 @@ class Server(Common):
             self._cmd = f'{self._cmd} --undo'
         return self
 
+    def debug(self):
+        """
+        Returns a Debug object for debug enable/disable commands.
+
+        Example:
+            traffic_ctl.server().debug().enable(tags="http").exec()
+            traffic_ctl.server().debug().disable().exec()
+        """
+        return Debug(self._dir, self._tr, self._tn)
+
     def as_json(self):
         self._cmd = f'{self._cmd} -f json'
         return self


Reply via email to