require 'date' require 'drb/drb' require 'colorize' require 'fileutils' require 'json' require 'ostruct' require 'rake' require 'yaml' require_relative 'config' require_relative 'email_client' require_relative 'task' require_relative 'execution' require_relative 'input' require_relative 'logger' require_relative 'logger_service' require_relative 'container/version' module Henry # Henry Container class Container # Initialize the Container with the given options # # @param [Hash] the execution given options. def initialize(opts) @opts = opts end # Executes the loaded tasks and stores their results. def execute self.before_execution self.params.each_with_index do |params, index| tasks_results_set = [] self.tasks.select {|task| task.enabled?}.each do |task| task_params = (params['all'] || {}).merge(params[task.name] || {}) task.configure(task_params,self.task_extended_context(task.name)) task.export_params(task_params) task.export_config({output_directory:task.data.system[:output_directory]}) task.execution.params = task_params task.before_execute self.execute_task(task) task.after_execute tasks_results_set << task.report end self.tasks_results << tasks_results_set end self.update_results self.dump_results end protected # Initial state the conteiner execution results. # # @return [Hash] the results template. RESULTS_TEMPLATE = { summary: { total: 0, breakdownByStatusCode: { 'OK' => 0, 'ERROR' => 0, 'WARNING' => 0 } }, tasks: [] } # Paths collection of the out.henry templates # # @return [Hash] the paths of the out.henry templates TEMPLATE_PATHS = { json_parse_error: 'templates/errors/json_parse_error.json' } def default_task_system_context { output_directory: self.output_directory } end # Executes the task inside a timeout (defined by the task itself). # On Timeout::Error adds an erro mmessage to the Task Execution. # # @param [Henry::Task] task the task to be executed. def execute_task(task) begin Timeout::timeout(task.timeout) do task.execute end rescue Timeout::Error task.execution.code = 'ERROR' task.execution.message = 'The task execution exeeded the timeout.' ensure begin `ps o pid= --ppid #{Process.pid}`.scan(/\d+/).each do |pid| begin Process.kill('KILL', pid.to_i) rescue Errno::ESRCH end end rescue Errno::ENOENT warn 'Can not cleanup this process children' end end end # Returns the input file path based on the given options. # # @return [String] the input file path. def input_file_path "#{@opts[:"in-dir"]}/#{@opts[:in]}" end # Returns the output directory path based on the given options. # # @return [String] the output directory path. def output_directory @opts[:"out-dir"] end # Returns the output file path based on the given options. # # @return [String] the output file path. def output_file_path "#{self.output_directory}/#{@opts[:out]}" end # Returns the task hints file path based on the given options. # # @return [String] the task hints file path. def hints_file_path "#{@opts[:"hints-dir"]}/#{@opts[:hints]}" end # Returns the task hints patched file path based on the given options. # # @return [String] the task hints file path. def hints_file_patched_path "#{@opts[:"in-dir"]}/#{@opts[:hints]}" end # Executes required validations before execution. def before_execution unless File.exists?('henry-context.yml') abort "'henry-context.yml' file does not exist. You need it in order to execute a Henry compatible test.".red end end # Returns the execution params. # # @return [<{String => String}>] collection of execution params. def params @params ||= self.load_params end # Loads and parses the @opts[:in] params, if they exists. # # @return [<{String => String}>] collection of execution params. def load_params return [{}] unless File.exists?(self.input_file_path) params = JSON.parse(File.read(self.input_file_path)) return [{}] if params.empty? params end # Returns the default params. # @note Custom task_params will overwrite the defaults. # # @return [Hash] the default params. def default_params @default_params ||= (self.params['all'] || {}) end # Returns the custom params for the given task. # @note default_params will be used for undefined keys. # # @param [String] task_name the target Task name. # @return [Array] the Task custom params. def task_params(task_name) self.params.collect do |params| (params['all'] || {}).merge(params[task_name] || {}) end end # Return the default extended execution. # @note Custom extended_executions will overwrite the defaults. # # @return [Hash] the Task default extended context def default_extended_context {} end # Retrun the custom extended_context fo the given task. # @note default_extended_context will be used for undefined keys. # # @param [String] task_name the target Task name. # @return [Hash] the Task custom extended context. def extended_context(task_name) {}['extended_context'] end # Returns the custom execution_context for the given task. # @note default_execution_context will be used for undefined keys. # # @param [String] task_name the target Task name. # @return [Hash] the Task custom execution_context. def task_extended_context(task_name) self.default_extended_context.merge(self.extended_context(task_name) || {}) end # Returns de task hints (tasks to be executed) or an empty array if they are not defined. # # @return [] the task hints. def task_hints self.hints["tasks"] || [] end # Returns de hints or an empty Hash if they are not defined. # # @return [Hash] the hints. def hints @hints ||= self.load_hints end # Loads and parses the @opts[:hints] params, if they exists. # # @return [Hash] the hints:w def load_hints # To be removed once that the Worker starts using the --hints-path if File.exists?(self.hints_file_patched_path) return JSON.parse(File.read(self.hints_file_patched_path)) end return {} unless File.exists?(self.hints_file_path) JSON.parse(File.read(self.hints_file_path)) end # Load and parses the henry-context.yml params. # # @return [{String => String}] the execution context. def context @context ||= OpenStruct.new(YAML.load_file('henry-context.yml')) end # Returns the tasks to be executed based on the context. # # @return [] tasks to be executed. def tasks @tasks ||= self.build_tasks end # Builds and disables the Tasks to be executed based on the context and hints. # # @return [] tasks to be executed. def build_tasks tasks = self.context.tasks.collect do |task_data| Task.create(task_data["name"], task_data.merge({system: self.default_task_system_context})) end return tasks if self.task_hints.empty? tasks.each do |task| task.disable! unless self.task_hints.include? task.name end end # Returns the execution's current results. # # @return [Hash] the execution's current resutls. def results @results ||= RESULTS_TEMPLATE end # Returns the tasks_results node from the results hash. # # @return [Array] def tasks_results self.results[:tasks] end # Sets the tasks_results node of the results hash. # # @param [Array] results the tasks_results collection. def tasks_results=(results) self.results[:tasks] = results end # Updates the results attributes based on the task_results. def update_results self.tasks_results.each do |task_results_set| task_results_set.each do |task_results| self.results[:summary][:total] += 1 self.results[:summary][:breakdownByStatusCode][task_results[:code]] += 1 end end end # Writes into @opts[:out] the execution results. def dump_results File.open(self.output_file_path, 'w') { |f| f.write(results.to_json) } begin JSON.parse(File.read(self.output_file_path)) rescue JSON::ParserError FileUtils.copy(TEMPLATE_PATHS[:json_parse_error], self.output_file_path) end end end end