# # Author:: Fletcher Nichol () # # Copyright (C) 2012, 2013, 2014, Fletcher Nichol # # Licensed 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. module Kitchen # Base configuration class for Kitchen. This class exposes configuration such # as the location of the Kitchen config file, instances, log_levels, etc. # This object is a factory object, meaning that it is responsible for # consuming the desired testing configuration in and returning Ruby objects # which are used to perfom the work. # # Most internal objects are created with the expectation of being # *immutable*, meaning that internal state cannot be modified after creation. # Any data manipulation or thread-unsafe activity is performed in this object # so that the subsequently created objects (such as Instances, Platforms, # Drivers, etc.) can safely run in concurrent threads of execution. To # prevent the re-creation of duplicate objects, most created objects are # memoized. The consequence of this is that once the Instance Array has # been requested (with the `#instances` message), you will always be returned # the same Instance objects. # # @example fetching all instances # # Kitchen::Config.new.instances # # @example fetching an instance by name # # Kitchen::Config.new.instances.get("default-ubuntu-16.04") # # @example fetching all instances matching a regular expression # # Kitchen::Config.new.instances.get_all(/ubuntu/) # # @author Fletcher Nichol class Config # @return [String] the absolute path to the root of a Test Kitchen project # @api private attr_reader :kitchen_root # @return [String] the absolute path to the directory into which all Test # Kitchen log files will be written # @api private attr_reader :log_root # @return [#read] the data loader that responds to a `#read` message, # returning a Hash data structure # @api private attr_reader :loader # @return [Symbol] the logging verbosity level # @api private attr_accessor :log_level # @return [Boolean] whether to overwrite the log file when # Test Kitchen runs # @api private attr_accessor :log_overwrite # @return [String] an absolute path to the directory containing test suites # @api private attr_accessor :test_base_path # @return [Boolean] whether to force color output or not in logger # @api private attr_accessor :colorize # @return [Boolean] whether to enable debugging in the provisioner/verifier plugin or not # @api private attr_accessor :debug # Creates a new configuration, representing a particular testing # configuration for a project. # # @param [Hash] options configuration # @option options [#read] :loader an object that responds to `#read` with # a Hash structure suitable for manipulating # (default: `Kitchen::Loader::YAML.new`) # @option options [String] :kitchen_root an absolute path to the root of a # Test Kitchen project, usually containing a `.kitchen.yml` file # (default `Dir.pwd`) # @option options [String] :log_root an absolute path to the directory # into which all Test Kitchen log files will be written # (default: `"#{kitchen_root}/.kitchen/logs"`) # @option options [String] :test_base_path an absolute path to the # directory containing test suites and other testing-related files and # directories (default: `"#{kitchen_root}/test/integration"`) # @option options [Symbol] :log_level the log level verbosity that the # loggers will use when outputing information (default: `:info`) def initialize(options = {}) @loader = options.fetch(:loader) { Kitchen::Loader::YAML.new } @kitchen_root = options.fetch(:kitchen_root) { Dir.pwd } @log_level = options.fetch(:log_level) { Kitchen::DEFAULT_LOG_LEVEL } @log_overwrite = options.fetch(:log_overwrite) { Kitchen::DEFAULT_LOG_OVERWRITE } @colorize = options.fetch(:colorize) { Kitchen.tty? } @log_root = options.fetch(:log_root) { default_log_root } @test_base_path = options.fetch(:test_base_path) { default_test_base_path } @debug = options.fetch(:debug) { false } end # @return [Collection] all instances, resulting from all # platform and suite combinations def instances @instances ||= Collection.new(build_instances) end # @return [Collection] all defined platforms which will be used # in convergence integration def platforms @platforms ||= Collection.new( data.platform_data.map { |pdata| Platform.new(pdata) } ) end # @return [Collection] all defined suites which will be used in # convergence integration def suites @suites ||= Collection.new( data.suite_data.map { |sdata| Suite.new(sdata) } ) end private # Builds the filtered list of Instance objects. # # @return [Array>] an Array of Suite/Platform # tuples # @api private def filter_instances suites.product(platforms).select do |suite, platform| if !suite.includes.empty? suite.includes.include?(platform.name) elsif !suite.excludes.empty? !suite.excludes.include?(platform.name) else true end end end # Determines the String name for an Instance, given a Suite and a Platform. # # @param suite [Suite,#name] a Suite # @param platform [Platform,#name] a Platform # @return [String] an Instance name # @api private def instance_name(suite, platform) Instance.name_for(suite, platform) end # Generates the immutable Test Kitchen configuration and reasonable # defaults for Drivers, Provisioners and Transports. # # @return [Hash] a configuration Hash # @api private def kitchen_config @kitchen_config ||= { defaults: { driver: Driver::DEFAULT_PLUGIN, provisioner: Provisioner::DEFAULT_PLUGIN, verifier: Verifier::DEFAULT_PLUGIN, transport: lambda do |_suite, platform| /^win/i.match?(platform) ? "winrm" : Transport::DEFAULT_PLUGIN end, }, kitchen_root: kitchen_root, test_base_path: test_base_path, log_level: log_level, log_overwrite: log_overwrite, debug: debug, } end # Builds a newly configured Driver object, for a given Suite and Platform. # # @param suite [Suite,#name] a Suite # @param platform [Platform,#name] a Platform # @return [Driver] a new Driver object # @api private def new_driver(suite, platform) ddata = data.driver_data_for(suite.name, platform.name) Driver.for_plugin(ddata[:name], ddata) end # Builds a newly configured Instance object, for a given Suite and # Platform. # # @param suite [Suite,#name] a Suite # @param platform [Platform,#name] a Platform # @param index [Integer] an index used for colorizing output # @return [Instance] a new Instance object # @api private def new_instance(suite, platform, index) Instance.new( driver: new_driver(suite, platform), lifecycle_hooks: new_lifecycle_hooks(suite, platform), logger: new_instance_logger(suite, platform, index), suite: suite, platform: platform, provisioner: new_provisioner(suite, platform), transport: new_transport(suite, platform), verifier: new_verifier(suite, platform), state_file: new_state_file(suite, platform) ) end # Builds a newly configured Logger object, for a given Suite and # Platform. # # @param suite [Suite,#name] a Suite # @param platform [Platform,#name] a Platform # @param index [Integer] an index used for colorizing output # @return [Logger] a new Logger object # @api private def new_instance_logger(suite, platform, index) name = instance_name(suite, platform) log_location = File.join(log_root, "#{name}.log").to_s Logger.new( stdout: STDOUT, color: Color::COLORS[index % Color::COLORS.size].to_sym, logdev: log_location, level: Util.to_logger_level(log_level), log_overwrite: log_overwrite, progname: name, colorize: @colorize ) end # Builds a newly configured LifecycleHooks object, for a given a Suite and # Platform. # # @param suite [Suite,#name] a Suite # @param platform [Platform,#name] a Platform # @return [LifecycleHooks] a new LifecycleHooks object # @api private def new_lifecycle_hooks(suite, platform) lhdata = data.lifecycle_hooks_data_for(suite.name, platform.name) LifecycleHooks.new(lhdata) end # Builds a newly configured Provisioner object, for a given Suite and # Platform. # # @param suite [Suite,#name] a Suite # @param platform [Platform,#name] a Platform # @return [Provisioner] a new Provisioner object # @api private def new_provisioner(suite, platform) pdata = data.provisioner_data_for(suite.name, platform.name) Provisioner.for_plugin(pdata[:name], pdata) end # Builds a newly configured StateFile object, for a given Suite and # Platform. # # @param suite [Suite,#name] a Suite # @param platform [Platform,#name] a Platform # @return [StateFile] a new StateFile object # @api private def new_state_file(suite, platform) StateFile.new(kitchen_root, instance_name(suite, platform)) end # Builds a newly configured Transport object, for a given Suite and # Platform. # # @param suite [Suite,#name] a Suite # @param platform [Platform,#name] a Platform # @return [Transport] a new Transport object # @api private def new_transport(suite, platform) tdata = data.transport_data_for(suite.name, platform.name) Transport.for_plugin(tdata[:name], tdata) end # Builds a newly configured Verifier object, for a given a Suite and # Platform. # # @param suite [Suite,#name] a Suite # @param platform [Platform,#name] a Platform # @return [Verifier] a new Verifier object # @api private def new_verifier(suite, platform) vdata = data.verifier_data_for(suite.name, platform.name) Verifier.for_plugin(vdata[:name], vdata) end end end