From 80f3b01685514219113a59cead87505a85cd3fa7 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kostiuk Date: Mon, 16 Feb 2026 19:48:24 +0200 Subject: [PATCH] Make CLI classes typed Rework all attributes to the Sorbet struct property. Unfortunately, sorbet typecheck cannot validate OptionParser option type assignment to the sorbet property type, but it works at runtime. This is done as part of preparation for the ability to bring up the previous session. For example, the following code will pass typecheck but will fail at runtime with type cast error: ``` prop :id, Integer, default: 2 parser.on('--id ', String, 'Set ID for AutoHCK run', &method(:id=)) ``` /ruby/3.3.0/gems/sorbet-runtime-0.6.12935/lib/types/configuration.rb:293:in `call_validation_error_handler_default': Parameter 'id': Can't set AutoHCK::CliCommonOptions.id to "s" (instance of String) - need a Integer (TypeError) Signed-off-by: Vitalii Chulak Signed-off-by: Kostiantyn Kostiuk --- lib/cli.rb | 518 ++++++++++++++++++++++++++++------------------------- 1 file changed, 270 insertions(+), 248 deletions(-) diff --git a/lib/cli.rb b/lib/cli.rb index 93dbb829..a9dd4fec 100644 --- a/lib/cli.rb +++ b/lib/cli.rb @@ -1,290 +1,312 @@ +# typed: true # frozen_string_literal: true -# AutoHCK module module AutoHCK - # class CLI - class CLI - attr_reader :common, :install, :test, :mode - - def initialize - @common = CommonOptions.new - @test = TestOptions.new - @install = InstallOptions.new - - @sub_parser = { - 'test' => @test.create_parser, - 'install' => @install.create_parser - } - - @parser = @common.create_parser(@sub_parser) - end - - # class CommonOptions - class CommonOptions - attr_accessor :verbose, :config, :client_world_net, :id, :share_on_host_path, :workspace_path, - :client_ctrl_net_dev, :attach_debug_net - - def create_parser(sub_parser) - OptionParser.new do |parser| - parser.banner = 'Usage: auto_hck.rb [common options] [command options]' - parser.separator '' - define_options(parser) - parser.on_tail('-h', '--help', 'Show this message') do - puts parser - sub_parser&.each_value do |v| - puts v - end - exit + class CliCommonOptions < T::Struct + extend T::Sig + + prop :verbose, T::Boolean, default: false + prop :config, T.nilable(String) + prop :client_world_net, T::Boolean, default: false + prop :id, Integer, default: 2 + prop :share_on_host_path, T.nilable(String) + prop :workspace_path, T.nilable(String) + prop :client_ctrl_net_dev, T.nilable(String) + prop :attach_debug_net, T::Boolean, default: false + + def create_parser(sub_parser) + OptionParser.new do |parser| + parser.banner = 'Usage: auto_hck.rb [common options] [command options]' + parser.separator '' + define_options(parser) + parser.on_tail('-h', '--help', 'Show this message') do + puts parser + sub_parser&.each_value do |v| + puts v end + exit end end + end - # rubocop:disable Metrics/AbcSize,Metrics/MethodLength - def define_options(parser) - @verbose = false - @config = nil - @client_world_net = false - @id = 2 - @share_on_host_path = nil - @attach_debug_net = false - - parser.on('--share-on-host-path ', String, - 'For using Transfer Network specify the directory to share on host machine') do |share_on_host_path| - @share_on_host_path = share_on_host_path - end + # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + def define_options(parser) + parser.on('--share-on-host-path ', String, + 'For using Transfer Network specify the directory to share on host machine', + &method(:share_on_host_path=)) - parser.on('--verbose', TrueClass, - 'Enable verbose logging', - &method(:verbose=)) + parser.on('--verbose', TrueClass, + 'Enable verbose logging', + &method(:verbose=)) - parser.on('--config ', String, - 'Path to custom override.json file', - &method(:config=)) + parser.on('--config ', String, + 'Path to custom override.json file', + &method(:config=)) - parser.on('--client_world_net', TrueClass, - 'Attach world bridge to clients VM', - &method(:client_world_net=)) + parser.on('--client_world_net', TrueClass, + 'Attach world bridge to clients VM', + &method(:client_world_net=)) - parser.on('--client-ctrl-net-dev ', String, - 'Client VM control network device (make sure that driver is installed)', - &method(:client_ctrl_net_dev=)) + parser.on('--client-ctrl-net-dev ', String, + 'Client VM control network device (make sure that driver is installed)', + &method(:client_ctrl_net_dev=)) - parser.on('--attach-debug-net', TrueClass, - 'Attach debug network to all VMs', - &method(:attach_debug_net=)) + parser.on('--attach-debug-net', TrueClass, + 'Attach debug network to all VMs', + &method(:attach_debug_net=)) - parser.on('--id ', Integer, - 'Set ID for AutoHCK run', - &method(:id=)) + parser.on('--id ', Integer, + 'Set ID for AutoHCK run', + &method(:id=)) - parser.on('-v', '--version', - 'Display version information and exit') do - puts "AutoHCK Version: #{AutoHCK::VERSION}" - exit - end - - parser.on('-w ', String, - 'Internal use only', - &method(:workspace_path=)) + parser.on('-v', '--version', + 'Display version information and exit') do + puts "AutoHCK Version: #{AutoHCK::VERSION}" + exit end - # rubocop:enable Metrics/AbcSize,Metrics/MethodLength + + parser.on('-w ', String, + 'Internal use only', + &method(:workspace_path=)) end + # rubocop:enable Metrics/AbcSize,Metrics/MethodLength + end - # class TestOptions - class TestOptions - attr_accessor :platform, :drivers, :driver_path, :supplemental_path, :package_with_driver, - :commit, :svvp, :dump, :gthb_context_prefix, :gthb_context_suffix, :playlist, - :select_test_names, :reject_test_names, :reject_report_sections, :boot_device, - :allow_test_duplication, :manual, :package_with_playlist, :enable_vbs, :tag_suffix, - :fs_test_image_format, :extensions, :net_test_speed - - def create_parser - OptionParser.new do |parser| - parser.banner = 'Usage: auto_hck.rb test [test options]' - parser.separator '' - define_options(parser) - parser.on_tail('-h', '--help', 'Show this message') do - puts parser - exit - end + class CliTestOptions < T::Struct + extend T::Sig + + prop :platform, T.nilable(String) + prop :drivers, T::Array[String], default: [] + prop :driver_path, T.nilable(String) + prop :supplemental_path, T.nilable(String) + prop :package_with_driver, T::Boolean, default: false + prop :commit, T.nilable(String) + prop :svvp, T::Boolean, default: false + prop :dump, T::Boolean, default: false + prop :gthb_context_prefix, T.nilable(String) + prop :gthb_context_suffix, T.nilable(String) + prop :playlist, T.nilable(String) + prop :select_test_names, T.nilable(String) + prop :reject_test_names, T.nilable(String) + prop :reject_report_sections, T::Array[String], default: [] + prop :boot_device, T.nilable(String) + prop :allow_test_duplication, T::Boolean, default: false + prop :manual, T::Boolean, default: false + prop :package_with_playlist, T::Boolean, default: false + prop :enable_vbs, T::Boolean, default: false + prop :tag_suffix, T.nilable(String) + prop :fs_test_image_format, String, default: 'qcow2' + prop :extensions, T::Array[String], default: [] + prop :net_test_speed, Integer, default: 10_000 + + def create_parser + OptionParser.new do |parser| + parser.banner = 'Usage: auto_hck.rb test [test options]' + parser.separator '' + define_options(parser) + parser.on_tail('-h', '--help', 'Show this message') do + puts parser + exit end end + end - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def define_options(parser) - @reject_report_sections = [] - @extensions = [] - @svvp = false - @enable_vbs = false - - parser.on('-p', '--platform ', String, - 'Platform for run test', - &method(:platform=)) - - parser.on('-d', '--drivers ', Array, - 'List of driver for run test', - &method(:drivers=)) - - parser.on('--driver-path ', String, - 'Path to the location of the driver wanted to be tested', - &method(:driver_path=)) - - parser.on('--supplemental-path ', String, - 'Path to the supplemental content folder (e.g. for README)', - &method(:supplemental_path=)) - - parser.on('--package-with-driver', TrueClass, - 'Include driver files in HLKX package (requires --driver-path)', - &method(:package_with_driver=)) - - parser.on('-c', '--commit ', String, - 'Commit hash for CI status update', - &method(:commit=)) - - parser.on('--svvp', TrueClass, - 'Run SVVP tests for specified platform instead of driver tests', - &method(:svvp=)) - - parser.on('--dump', TrueClass, - 'Create machines snapshots and generate scripts for run it manualy', - &method(:dump=)) - - parser.on('--gthb_context_prefix ', String, - 'Add custom prefix for GitHub CI results context', - &method(:gthb_context_prefix=)) - - parser.on('--gthb_context_suffix ', String, - 'Add custom suffix for GitHub CI results context', - &method(:gthb_context_suffix=)) - - parser.on('--playlist ', String, - 'Use custom Microsoft XML playlist', - &method(:playlist=)) - - parser.on('--select-test-names ', String, - 'Use custom user text playlist', - &method(:select_test_names=)) - - parser.on('--reject-test-names ', String, - 'Use custom CI text ignore list', - &method(:reject_test_names=)) - - parser.on('--enable-vbs', TrueClass, - 'Enable VBS state for clients', - &method(:enable_vbs=)) - - parser.on('--reject-report-sections ', Array, - 'List of section to reject from HTML results', - '(use "--reject-report-sections=help" to list sections)') do |reject_report_sections| - if reject_report_sections.first == 'help' - puts Tests::RESULTS_REPORT_SECTIONS.join("\n") - exit - end - - extra_keys = reject_report_sections - Tests::RESULTS_REPORT_SECTIONS + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def define_options(parser) + parser.on('-p', '--platform ', String, + 'Platform for run test', + &method(:platform=)) + + parser.on('-d', '--drivers ', Array, + 'List of driver for run test', + &method(:drivers=)) + + parser.on('--driver-path ', String, + 'Path to the location of the driver wanted to be tested', + &method(:driver_path=)) + + parser.on('--supplemental-path ', String, + 'Path to the supplemental content folder (e.g. for README)', + &method(:supplemental_path=)) + + parser.on('--package-with-driver', TrueClass, + 'Include driver files in HLKX package (requires --driver-path)', + &method(:package_with_driver=)) + + parser.on('-c', '--commit ', String, + 'Commit hash for CI status update', + &method(:commit=)) + + parser.on('--svvp', TrueClass, + 'Run SVVP tests for specified platform instead of driver tests', + &method(:svvp=)) + + parser.on('--dump', TrueClass, + 'Create machines snapshots and generate scripts for run it manually', + &method(:dump=)) + + parser.on('--gthb_context_prefix ', String, + 'Add custom prefix for GitHub CI results context', + &method(:gthb_context_prefix=)) + + parser.on('--gthb_context_suffix ', String, + 'Add custom suffix for GitHub CI results context', + &method(:gthb_context_suffix=)) + + parser.on('--playlist ', String, + 'Use custom Microsoft XML playlist', + &method(:playlist=)) + + parser.on('--select-test-names ', String, + 'Use custom user text playlist', + &method(:select_test_names=)) + + parser.on('--reject-test-names ', String, + 'Use custom CI text ignore list', + &method(:reject_test_names=)) + + parser.on('--enable-vbs', TrueClass, + 'Enable VBS state for clients', + &method(:enable_vbs=)) + + parser.on('--reject-report-sections ', Array, + 'List of section to reject from HTML results', + '(use "--reject-report-sections=help" to list sections)') do |reject_report_sections| + if reject_report_sections.first == 'help' + puts Tests::RESULTS_REPORT_SECTIONS.join("\n") + exit + end - raise(AutoHCKError, "Unknown report sections: #{extra_keys.join(', ')}.") unless extra_keys.empty? + extra_keys = reject_report_sections - Tests::RESULTS_REPORT_SECTIONS - @reject_report_sections = reject_report_sections - end + raise(AutoHCKError, "Unknown report sections: #{extra_keys.join(', ')}.") unless extra_keys.empty? - parser.on('--boot-device ', String, - 'VM boot device', - &method(:boot_device=)) - - parser.on('--allow-test-duplication', TrueClass, - 'Allow run the same test several times.', - 'Works only with custom user text playlist.', - 'Test results table can be broken. (experimental)', - &method(:allow_test_duplication=)) - - parser.on('--manual', TrueClass, - 'Run AutoHCK in manual mode', - &method(:manual=)) - - parser.on('--package-with-playlist', TrueClass, - 'Load playlist into HLKX project package', - &method(:package_with_playlist=)) - - parser.on('--tag-suffix ', String, - 'Add custom suffix to HCK-CI tag to prevent name conflicts when using shared controller', - &method(:tag_suffix=)) - - parser.on('--fs-test-image-format ', String, - 'Filesystem test image format (qcow2/raw). Default is qcow2.', - 'Has effect only when testing storage drivers.', - &method(:fs_test_image_format=)) - - parser.on('--extensions ', Array, - 'List of extensions for run test', - &method(:extensions=)) - - parser.on('--net-test-speed ', Integer, - 'Network test speed (in Mbps). Default is 10000.', - 'Has effect only when testing virtio-net-pci network device.', - &method(:net_test_speed=)) + self.reject_report_sections = reject_report_sections end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + parser.on('--boot-device ', String, + 'VM boot device', + &method(:boot_device=)) + + parser.on('--allow-test-duplication', TrueClass, + 'Allow run the same test several times.', + 'Works only with custom user text playlist.', + 'Test results table can be broken. (experimental)', + &method(:allow_test_duplication=)) + + parser.on('--manual', TrueClass, + 'Run AutoHCK in manual mode', + &method(:manual=)) + + parser.on('--package-with-playlist', TrueClass, + 'Load playlist into HLKX project package', + &method(:package_with_playlist=)) + + parser.on('--tag-suffix ', String, + 'Add custom suffix to HCK-CI tag to prevent name conflicts when using shared controller', + &method(:tag_suffix=)) + + parser.on('--fs-test-image-format ', String, + 'Filesystem test image format (qcow2/raw). Default is qcow2.', + 'Has effect only when testing storage drivers.', + &method(:fs_test_image_format=)) + + parser.on('--extensions ', Array, + 'List of extensions for run test', + &method(:extensions=)) + + parser.on('--net-test-speed ', Integer, + 'Network test speed (in Mbps). Default is 10000.', + 'Has effect only when testing virtio-net-pci network device.', + &method(:net_test_speed=)) end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + end - # class InstallOptions - class InstallOptions - attr_accessor :platform, :force, :skip_client, :drivers, :driver_path, :debug, :no_reboot_after_bugcheck - - def create_parser - OptionParser.new do |parser| - parser.banner = 'Usage: auto_hck.rb install [install options]' - parser.separator '' - define_options(parser) - parser.on_tail('-h', '--help', 'Show this message') do - puts parser - exit - end + class CliInstallOptions < T::Struct + extend T::Sig + + prop :platform, T.nilable(String) + prop :force, T::Boolean, default: false + prop :skip_client, T::Boolean, default: false + prop :drivers, T::Array[String], default: [] + prop :driver_path, T.nilable(String) + prop :debug, T::Boolean, default: false + prop :no_reboot_after_bugcheck, T::Boolean, default: false + + def create_parser + OptionParser.new do |parser| + parser.banner = 'Usage: auto_hck.rb install [install options]' + parser.separator '' + define_options(parser) + parser.on_tail('-h', '--help', 'Show this message') do + puts parser + exit end end + end - # rubocop:disable Metrics/MethodLength - def define_options(parser) - @force = false - @skip_client = false - @drivers = [] - @debug = false - @no_reboot_after_bugcheck = false + def define_options(parser) + parser.on('--debug', TrueClass, 'Enable debug mode', + &method(:debug=)) - parser.on('--debug', TrueClass, 'Enable debug mode', - &method(:debug=)) + parser.on('-p', '--platform ', String, + 'Install VM for specified platform', + &method(:platform=)) - parser.on('-p', '--platform ', String, - 'Install VM for specified platform', - &method(:platform=)) + parser.on('-f', '--force', TrueClass, + 'Install all VM, replace studio if exist', + &method(:force=)) - parser.on('-f', '--force', TrueClass, - 'Install all VM, replace studio if exist', - &method(:force=)) + parser.on('--skip_client', TrueClass, + 'Skip client images installation', + &method(:skip_client=)) - parser.on('--skip_client', TrueClass, - 'Skip client images installation', - &method(:skip_client=)) + parser.on('-d', '--drivers ', Array, + 'List of driver attach in install', + &method(:drivers=)) - parser.on('-d', '--drivers ', Array, - 'List of driver attach in install', - &method(:drivers=)) + parser.on('--driver-path ', String, + 'Path to the location of the driver wanted to be installed', + &method(:driver_path=)) - parser.on('--driver-path ', String, - 'Path to the location of the driver wanted to be installed', - &method(:driver_path=)) + parser.on('--no-reboot-after-bugcheck', TrueClass, + 'Keep system in crashed state after crash for debugging (disables automatic reboot)', + &method(:no_reboot_after_bugcheck=)) + end + end - parser.on('--no-reboot-after-bugcheck', TrueClass, - 'Keep system in crashed state after crash for debugging (disables automatic reboot)', - &method(:no_reboot_after_bugcheck=)) - end - # rubocop:enable Metrics/MethodLength + class CLI < T::Struct + extend AutoHCK::Models::JsonHelper + extend T::Sig + + prop :test, CliTestOptions, factory: -> { CliTestOptions.new } + prop :common, CliCommonOptions, factory: -> { CliCommonOptions.new } + prop :install, CliInstallOptions, factory: -> { CliInstallOptions.new } + prop :mode, T.nilable(String), default: nil + + sig { returns(T::Hash[String, OptionParser]) } + def sub_parser + @sub_parser ||= { + 'test' => test.create_parser, + 'install' => install.create_parser + } + end + + sig { returns(OptionParser) } + def parser + @parser ||= common.create_parser(sub_parser) end + sig { params(args: T::Array[String]).returns(T::Array[String]) } def parse(args) - left = @parser.order(args) - @mode = left.shift - @sub_parser[@mode]&.order!(left) unless @mode.nil? + left = parser.order(args) + self.mode = left.shift + if mode.nil? + left + else + sub_parser[T.must(mode)]&.order!(left) + end end end end