diff --git a/lib/syskit/cli/main.rb b/lib/syskit/cli/main.rb index eb975b013..df9288b56 100644 --- a/lib/syskit/cli/main.rb +++ b/lib/syskit/cli/main.rb @@ -41,6 +41,91 @@ def orogen_test(*args) "commands related to monitoring and commanding a running Syskit system" subcommand "telemetry", Telemetry::CLI + desc "bundle_dir NAME", "print the directory of a bundle" + def bundle_dir(name) + require "rock/bundle" unless defined?(Rock::Bundles) + + bundle = Rock::Bundles.each_bundle.find { |b| b.name == name } + if bundle + puts bundle.path + else + $stderr.puts "No bundle named '#{name}' found." + $stderr.puts "Available bundles are: #{Rock::Bundles.each_bundle.map(&:name).sort.join(', ')}" + exit 1 + end + end + + desc "config ROBOT", "print the configuration of a given robot" + option :key, type: :string, repeatable: true, default: [] + option :from_bundle, type: :string, default: nil + def config(robot_name) + require "yaml" + + if (from_bundle = options[:from_bundle]) + require "rock/bundle" unless defined?(Rock::Bundles) + + bundle = Rock::Bundles.each_bundle.find { |b| b.name == from_bundle } + if bundle + Dir.chdir(bundle.path) + else + $stderr.puts "No bundle named '#{from_bundle}' found." + $stderr.puts "Available bundles are: #{Rock::Bundles.each_bundle.map(&:name).sort.join(', ')}" + exit 1 + end + end + + app = Roby.app + app.require_app_dir + app.load_config_yaml + app.setup_robot_names_from_config_dir + app.robot(robot_name) + + merger = proc do |_, old_val, new_val| + if old_val.is_a?(Hash) && new_val.is_a?(Hash) + old_val.merge(new_val, &merger) + elsif old_val.is_a?(Array) && new_val.is_a?(Array) + old_val + new_val + else + new_val + end + end + + config_path = ["config", "robots", "ROBOT.yml"] + merged_hash = app.find_files( + *config_path, all: true, order: :specific_last + ).each_with_object({}) do |path, config_hash| + values = YAML.safe_load(File.read(path)) || {} + config_hash.merge!(values, &merger) + end + + if options[:key].empty? + result = merged_hash + else + resolved_queries = options[:key].map do |key_query| + keys = key_query.split(".") + parent = keys.size > 1 ? merged_hash.dig(*keys[0...-1]) : merged_hash + + unless parent.is_a?(Hash) && parent.has_key?(keys.last) + $stderr.puts "Error: Key '#{key_query}' not found." + exit 1 + end + + [keys, merged_hash.dig(*keys)] + end + + if resolved_queries.size == 1 + result = resolved_queries.first.last + else + result = resolved_queries.reduce({}) do |accum, (keys, value)| + nested_path_hash = keys.reverse.reduce(value) { |v, k| { k => v } } + accum.merge(nested_path_hash, &merger) + end + end + end + + puts YAML.dump(result) + end + no_commands do def setup_interface(*, **) interface_version = options[:interface_version] || 1 diff --git a/test/cli/test_main.rb b/test/cli/test_main.rb index ff73106c9..f684a4ccb 100644 --- a/test/cli/test_main.rb +++ b/test/cli/test_main.rb @@ -61,6 +61,163 @@ module CLI assert_command_stops run_cmd end end + + describe "bundle_dir" do + it "returns the absolute path of an existing bundle" do + require "tmpdir" + Dir.mktmpdir do |dir| + bundle_dir = File.join(dir, "my_temp_bundle") + FileUtils.mkdir_p(File.join(bundle_dir, "config")) + FileUtils.touch(File.join(bundle_dir, "config", "bundle.yml")) + + set_environment_variable("ROCK_BUNDLE_PATH", dir) + cmd = run_command_and_stop "syskit bundle_dir my_temp_bundle" + assert_equal File.realdirpath(bundle_dir), File.realdirpath(cmd.stdout.strip) + assert_equal 0, cmd.exit_status + end + end + + it "returns an error for a non-existent bundle" do + require "tmpdir" + Dir.mktmpdir do |dir| + set_environment_variable("ROCK_BUNDLE_PATH", dir) + cmd = run_command_and_stop "syskit bundle_dir non_existent_bundle_test_cli", + fail_on_error: false + assert_match /No bundle named 'non_existent_bundle_test_cli' found/, cmd.stderr + assert_equal 1, cmd.exit_status + end + end + end + + describe "config" do + before do + run_command_and_stop "roby gen app" + write_file "config/robots/gazebo.yml", <<~YAML + system_definitions: + sdf_model_parameters: + param1: value1 + param2: value2 + array_param: [1, 2] + enable_something: false + nil_param: null + other_config: + something: value3 + YAML + write_file "config/robots/gazebo_test.yml", <<~YAML + system_definitions: + sdf_model_parameters: + param2: overriden_value2 + array_param: [3] + YAML + # Declare the robot and alias in app.yml so Roby knows about it + write_file "config/app.yml", <<~YAML + robots: + aliases: + test_alias: gazebo_test + robots: + gazebo_test: gazebo + YAML + end + + it "prints the full configuration of a given robot when no keys are specified" do + cmd = run_command_and_stop "syskit config gazebo_test" + expected = { + "system_definitions" => { + "sdf_model_parameters" => { + "param1" => "value1", + "param2" => "overriden_value2", + "array_param" => [1, 2, 3] + }, + "enable_something" => false, + "nil_param" => nil + }, + "other_config" => { + "something" => "value3" + } + } + assert_equal expected, YAML.safe_load(cmd.stdout) + end + + it "prints the value of a single key when --key is specified" do + cmd = run_command_and_stop "syskit config gazebo_test --key system_definitions.sdf_model_parameters.param1" + assert_equal "value1", YAML.safe_load(cmd.stdout) + end + + it "properly preserves boolean false values when --key is specified" do + cmd = run_command_and_stop "syskit config gazebo_test --key system_definitions.enable_something" + assert_equal false, YAML.safe_load(cmd.stdout) + end + + it "properly preserves explicit nil values when --key is specified" do + cmd = run_command_and_stop "syskit config gazebo_test --key system_definitions.nil_param" + assert_nil YAML.safe_load(cmd.stdout) + end + + it "returns an error when querying a nested key inside a non-hash value" do + cmd = run_command_and_stop "syskit config gazebo_test --key system_definitions.enable_something.nested", fail_on_error: false + assert_match /Key 'system_definitions.enable_something.nested' not found/, cmd.stderr + assert_equal 1, cmd.exit_status + end + + it "returns an error for a non-existent key" do + cmd = run_command_and_stop "syskit config gazebo_test --key non_existent", fail_on_error: false + assert_match /Key 'non_existent' not found/, cmd.stderr + assert_equal 1, cmd.exit_status + end + + it "supports cumulative key filtering when multiple --key options are specified" do + cmd = run_command_and_stop "syskit config gazebo_test --key system_definitions.sdf_model_parameters.param1 --key other_config.something" + expected = { + "system_definitions" => { + "sdf_model_parameters" => { + "param1" => "value1" + } + }, + "other_config" => { + "something" => "value3" + } + } + assert_equal expected, YAML.safe_load(cmd.stdout) + end + + it "runs the command from a specific bundle when --from-bundle is specified" do + require "tmpdir" + Dir.mktmpdir do |dir| + bundle_dir = File.join(dir, "my_temp_bundle") + FileUtils.mkdir_p(File.join(bundle_dir, "config", "robots")) + FileUtils.touch(File.join(bundle_dir, "config", "bundle.yml")) + File.write(File.join(bundle_dir, "config", "robots", "gazebo.yml"), <<~YAML) + system_definitions: + enable_something: true + YAML + File.write(File.join(bundle_dir, "config", "app.yml"), <<~YAML) + robots: + robots: + gazebo: gazebo + YAML + + set_environment_variable("ROCK_BUNDLE_PATH", dir) + cmd = run_command_and_stop "syskit config gazebo --from-bundle my_temp_bundle" + expected = { + "system_definitions" => { + "enable_something" => true + } + } + assert_equal expected, YAML.safe_load(cmd.stdout) + assert_equal 0, cmd.exit_status + end + end + + it "returns an error for a non-existent bundle with --from-bundle" do + require "tmpdir" + Dir.mktmpdir do |dir| + set_environment_variable("ROCK_BUNDLE_PATH", dir) + cmd = run_command_and_stop "syskit config gazebo --from-bundle non_existent_bundle_test_cli", fail_on_error: false + assert_match /No bundle named 'non_existent_bundle_test_cli' found/, cmd.stderr + assert_equal 1, cmd.exit_status + end + end + end end end end