diff --git a/config.json b/config.json index cea2ebc1..689143c1 100644 --- a/config.json +++ b/config.json @@ -11,5 +11,6 @@ "install_engine": "hckinstall", "config_engine": "config_manager", "functest_engine": "functest", + "merge_engine": "hlkxmerge", "time_out": "9" } diff --git a/lib/all.rb b/lib/all.rb index 7c5ec4a0..05ae6749 100644 --- a/lib/all.rb +++ b/lib/all.rb @@ -53,6 +53,7 @@ module AutoHCK autoload_relative :HCKInstall, 'engines/hckinstall/hckinstall' autoload_relative :HCKStudio, 'setupmanagers/hckstudio' autoload_relative :HCKTest, 'engines/hcktest/hcktest' + autoload_relative :HLKXMerge, 'engines/hlkxmerge/hlkxmerge' autoload_relative :Helper, 'helper' autoload_relative :InvalidConfigFile, 'exceptions' autoload_relative :InvalidEngineTypeError, 'engines/exceptions' diff --git a/lib/cli.rb b/lib/cli.rb index a9536d9a..917c1b7c 100644 --- a/lib/cli.rb +++ b/lib/cli.rb @@ -334,6 +334,40 @@ def define_options(parser) end end + class CliMergeOptions < T::Struct + extend T::Sig + + prop :platform, T.nilable(String) + prop :packages, T::Array[String], default: [] + prop :output_file, T.nilable(String) + + def create_parser + OptionParser.new do |parser| + parser.banner = 'Usage: auto_hck.rb merge [merge options]' + parser.separator '' + define_options(parser) + parser.on_tail('-h', '--help', 'Show this message') do + puts parser + exit + end + end + end + + def define_options(parser) + parser.on('-p', '--platform ', String, + 'Platform for Studio VM (determines HLK version)', + &method(:platform=)) + + parser.on('--packages ', Array, + 'Comma-separated list of HLKX package paths to merge (minimum 2)', + &method(:packages=)) + + parser.on('--output ', String, + 'Output path for the merged HLKX package', + &method(:output_file=)) + end + end + class CLI < T::Struct extend AutoHCK::Models::JsonHelper extend T::Sig @@ -341,6 +375,7 @@ class CLI < T::Struct prop :test, CliTestOptions, factory: -> { CliTestOptions.new } prop :common, CliCommonOptions, factory: -> { CliCommonOptions.new } prop :install, CliInstallOptions, factory: -> { CliInstallOptions.new } + prop :merge, CliMergeOptions, factory: -> { CliMergeOptions.new } prop :mode, T.nilable(String), default: nil sig { returns(T::Hash[String, OptionParser]) } @@ -348,7 +383,8 @@ def sub_parser @sub_parser ||= { 'test' => test.create_parser, 'install' => install.create_parser, - 'functest' => test.create_parser + 'functest' => test.create_parser, + 'merge' => merge.create_parser } end diff --git a/lib/engines/engine.rb b/lib/engines/engine.rb index 25da1867..e085e0e2 100644 --- a/lib/engines/engine.rb +++ b/lib/engines/engine.rb @@ -6,7 +6,8 @@ module Engine hcktest: HCKTest, hckinstall: HCKInstall, config_manager: ConfigManager, - functest: FunctestEngine + functest: FunctestEngine, + hlkxmerge: HLKXMerge }.freeze def self.select(name) diff --git a/lib/engines/hcktest/tools.rb b/lib/engines/hcktest/tools.rb index 69d69def..9d2cbe80 100644 --- a/lib/engines/hcktest/tools.rb +++ b/lib/engines/hcktest/tools.rb @@ -83,7 +83,7 @@ def connect(conn) def config_clients_addrs clients_addrs = {} - @clients.each_value do |client| + @clients&.each_value do |client| clients_addrs[client.name] = { addr: client.winrm_addr, port: client.winrm_port @@ -400,6 +400,12 @@ def create_project_package(project, playlist = nil, handler = nil, driver_path = end end + def merge_hlkx_packages(packages, output = nil) + retry_tools_command(__method__) do + act_with_tools { _1.merge_hlkx_packages(packages, output) } + end + end + def connection_check retry_tools_command(__method__) do act_with_tools(&:connection_check) diff --git a/lib/engines/hlkxmerge/hlkxmerge.rb b/lib/engines/hlkxmerge/hlkxmerge.rb new file mode 100644 index 00000000..0fdbe0f1 --- /dev/null +++ b/lib/engines/hlkxmerge/hlkxmerge.rb @@ -0,0 +1,135 @@ +# typed: true +# frozen_string_literal: true + +module AutoHCK + # HLKXMerge engine: merges multiple HLKX packages into one using the HLK API + # running on a Studio VM. No HLK clients or test execution are needed. + # + # Usage: + # auto_hck merge --platform --packages a.hlkx,b.hlkx --output merged.hlkx + class HLKXMerge + extend T::Sig + include Helper + + ENGINE_MODE = 'merge' + PLATFORMS_JSON_DIR = 'lib/engines/hcktest/platforms' + + def initialize(project) + @project = project + @logger = project.logger + @project.append_multilog('hlkxmerge.log') + end + + def self.tag(options) + "merge-#{options.merge.platform}" + end + + sig { params(logger: MultiLogger, options: CLI).returns(Models::HLKPlatform) } + def self.platform(logger, options) + platform_name = options.merge.platform + raise AutoHCKError, 'Missing required option: --platform' if platform_name.nil? + + Models::HLKPlatform.from_json_file( + "#{PLATFORMS_JSON_DIR}/#{platform_name}.json", + logger + ) + end + + def result_uploader_needed? + false + end + + def drivers + [] + end + + def test_steps + [] + end + + sig { returns(T::Hash[String, T.untyped]) } + def clients_system_info + {} + end + + def run + validate_options + + ResourceScope.open do |scope| + boot_and_connect_studio(scope) + run_merge + end + end + + private + + def validate_options + packages = @project.options.merge.packages + + raise AutoHCKError, 'At least 2 packages are required for merge' if packages.size < 2 + + packages.each do |pkg| + raise InvalidPathError, "Package file not found: #{pkg}" unless File.exist?(pkg) + end + end + + def boot_and_connect_studio(scope) + @studio = @project.setup_manager.run_hck_studio(scope, {}) + @logger.info('Waiting for Studio to load...') + sleep 5 until @studio.up? + @studio.connect + @studio.verify_tools + @logger.info('Studio connected') + end + + def upload_packages + @project.options.merge.packages.map.with_index do |local_path, i| + filename = File.basename(local_path) + remote_path = "C:\\AutoHCK\\merge_input_#{i}_#{filename}" + @logger.info("Uploading #{filename} to Studio...") + @studio.tools.upload_to_studio(local_path, remote_path) + remote_path + end + end + + def print_merge_results(result) + messages = (result['messages'] || []).map { " -- #{_1}\n" }.join + if result['iserror'] == false + @logger.info('Merge completed successfully') + @logger.debug("Merge result:\n#{messages}") + else + @logger.warn('Merge got an error. Check the logs for details.') + @logger.warn("Merge result:\n#{messages}") + end + end + + def run_merge + remote_packages = upload_packages + + @logger.info("Merging #{remote_packages.size} packages...") + result = @studio.tools.merge_hlkx_packages(remote_packages) + raise AutoHCKError, 'Merge failed: invalid response from Studio tools' unless result.is_a?(Hash) + + print_merge_results(result) + raise AutoHCKError, 'Merge failed: Studio tools reported an error' unless result['iserror'] == false + + downloaded_path = result['hostprojectpackagepath'] + raise AutoHCKError, 'Merge failed: merged package path not found' unless downloaded_path + + save_output(downloaded_path) + end + + def save_output(downloaded_path) + output_file = @project.options.merge.output_file + if output_file && downloaded_path != output_file + FileUtils.mkdir_p(File.dirname(output_file)) + FileUtils.mv(downloaded_path, output_file) + @logger.info("Merge complete. Output: #{output_file}") + else + @logger.info("Merge complete. Output: #{downloaded_path}") + end + rescue SystemCallError => e + raise AutoHCKError, "Failed to save merged package to #{output_file}: #{e.message}" + end + end +end